libindi/0000775000175000017500000000000013263645632011464 5ustar jasemjasemlibindi/test/0000775000175000017500000000000013263645557012451 5ustar jasemjasemlibindi/test/CMakeLists.txt0000664000175000017500000000142713263645557015215 0ustar jasemjasemCMAKE_MINIMUM_REQUIRED (VERSION 3.0) FIND_PACKAGE (GMock REQUIRED) MESSAGE (STATUS "GTEST_BOTH_LIBRARIES ${GTEST_BOTH_LIBRARIES}") MESSAGE (STATUS "GTEST_MAIN_LIBRARIES ${GTEST_MAIN_LIBRARIES}") MESSAGE (STATUS "GTEST_LIBRARIES ${GTEST_LIBRARIES}") MESSAGE (STATUS "GTEST_INCLUDE_DIRS ${GTEST_INCLUDE_DIRS}") MESSAGE (STATUS "GMOCK_LIBRARIES ${GMOCK_LIBRARIES}") MESSAGE (STATUS "GMOCK_INCLUDE_DIRS ${GMOCK_INCLUDE_DIRS}") ENABLE_TESTING() INCLUDE_DIRECTORIES ( ${GTEST_INCLUDE_DIRS} ) INCLUDE_DIRECTORIES ( ${GMOCK_INCLUDE_DIRS} ) INCLUDE_DIRECTORIES ( ${CMAKE_SOURCE_DIR} ) # Workaround for fixing a linking error caused by "-pie" flag in CMakeCommon STRING(REPLACE "-pie" "" CMAKE_EXE_LINKER_FLAGS ${CMAKE_EXE_LINKER_FLAGS}) ADD_SUBDIRECTORY(core) ADD_SUBDIRECTORY(celestrondriver) libindi/test/celestrondriver/0000775000175000017500000000000013263645557015663 5ustar jasemjasemlibindi/test/celestrondriver/test_celestrondriver.cpp0000664000175000017500000002013513263645557022641 0ustar jasemjasem#include #include #include #include #include #include #include "indilogger.h" #include "celestrondriver.h" using namespace Celestron; using ::testing::_; using ::testing::StrEq; // Define a new matcher that compares two byte arrays MATCHER_P2(MemEq, buf, n, "") { uint8_t *b1 = (uint8_t *)buf; uint8_t *b2 = (uint8_t *)arg; for (int i=0; i #ifdef HAVE_CONFIG_H #include "config.h" #endif #include #include #include "base64.h" TEST(CORE_BASE64, Test_to64frombits) { int len = 0, size = sizeof("FOOBARBAZ") - 1 * 4 / 3 + 4 + 1; const unsigned char convert[] = "FOOBARBAZ"; unsigned char *p_outbuf = nullptr; p_outbuf = (unsigned char *)calloc(1, size); ASSERT_TRUE(p_outbuf); len = to64frombits(p_outbuf, convert, sizeof(convert) - 1); ASSERT_EQ(sizeof("Rk9PQkFSQkFa") - 1, len); ASSERT_STREQ("Rk9PQkFSQkFa", (const char *)p_outbuf); free(p_outbuf); } TEST(CORE_BASE64, Test_from64tobits) { int len = 0, size = sizeof("Rk9PQkFSQkFa") - 1 * 3 / 4 + 1; const char convert[] = "Rk9PQkFSQkFa"; char *p_outbuf = nullptr; p_outbuf = (char *)calloc(1, size); ASSERT_TRUE(p_outbuf); len = from64tobits(p_outbuf, convert); ASSERT_EQ(sizeof("FOOBARBAZ") - 1, len); ASSERT_STREQ("FOOBARBAZ", (char *)p_outbuf); free(p_outbuf); } TEST(CORE_BASE64, Test_from64tobits_fast) { int len = 0, size = sizeof("Rk9PQkFSQkFa") - 1 * 3 / 4 + 1; const char convert[] = "Rk9PQkFSQkFa"; char *p_outbuf = nullptr; p_outbuf = (char *)calloc(1, size); ASSERT_TRUE(p_outbuf); len = from64tobits_fast(p_outbuf, convert, strlen(convert)); ASSERT_EQ(sizeof("FOOBARBAZ") - 1, len); ASSERT_STREQ("FOOBARBAZ", (char *)p_outbuf); free(p_outbuf); } libindi/test/core/.CMakeLists.txt.swp0000664000175000017500000003000013263645557017020 0ustar jasemjasemb0VIM 7.4þ¢iW,*ÍHajkajk-kubuntu~ajk/github/A-j-K/indi/libindi/test/core/CMakeLists.txtutf-8 3210#"! Utpad™ ÿþè×ÕÔÓ¸£¡y`L1/.-   ʇ†JØ ¢ b " !   Ý Ü Û Ú Ù INCLUDE_DIRECTORIES ( ${CMAKE_SOURINCLUDE_DIRECTORIES ( ${CMAINCLUDE_DIRECTORIES ( ${CMAKE_SOURCE_DIR} )INCLUDE_DIRECTORIINCLUDE_DIRECTORIES ( ${CMAKE_SOURCE_DIR} )INCLUDE_DIRECTORIINCLUDE_DIRECTORIES ( ${CMAKE_SOURCE_DIR} )INCLUDEINCLUDE_DIRECTORIES ( ${CMAKE_SOURCE_DIR} )INCLUDE_DIRECINCLUDE_DIRECTORIES ( ${CMAKE_SOURCE_DIR} )INCLUDEINCLUDE_DIRECTORIES ( ${CMAKE_SOURCE_DIR} )INCLUDE_DIRECINCLUDE_DIRECTINCLUDE_DIRECTORIES ( ${CMAKE_SOURCE_DIR} )ADD_TEST(test_base64 test_base64)) ${test_base64_SRCS}ADDADD_TEST(test_base64 test_base64)) ${CMAKE_THREAD_LIBS_INIT} ${GMOCK_LIBRARIES} ${GTEST_BOTH_LIBRARIES} indiTARGET_LINK_LIBRARIES(test_base64) ${test_base64_SRCS}ADD_EXECUTABLE(test_base64) test_base64.cppSET (test_base64_SRCSlibindi/ChangeLog0000664000175000017500000005545413263645557013261 0ustar jasemjasemFrom 1.6.3 to 1.7.0 + Updated QHY SDK. + FLI drivers are now based on libusb rather than legacy kernel driver. + New driver for CEM120 mount. + Several memory leaks were fixed. + Added support for background flushing for FLI CCDs. + Added preliminary support for CCD rapid captures on the millisecond range. + SX CCD driver updated to support ICX453 & M25C. + SX AO driver updated to emply INDI serial connection plugin. + Fix timing issue with GPhoto making it stuck in busy state after initial capture. + ASI driver enhancements. Video format recall fix. + MaxDomeII driver refactored and updated. + Several fixes for Gemini Integra driver. + Polling period for most drivers is now customizable. + GPhoto driver supported Abort exposure. Subframing fixes. + GPS driver can set system time from GPS source. + Astrophyics Experimental Driver with multi-parking support. + Numerous OnStep driver fixes and updates. + SkySensor2000 Pulse guiding support. + Prevent sandbox ACCESS_VIOLATION on Gentoo + Celestron driver refactoring and support for high-precision formats. + Fixed script execution in scripting gateways + Fix flags for Cygwin. + Fix non-standard POSIX C functions. + Replace deprecated usleep with nanosleep. + CCD & Telescope simulator updated so that can be used effectively in any combination with physical devices. From 1.6.0 to 1.6.2 + INDI API version was not updated to 1.6.X+ + cmake_modules directory was missing from release. + Fixed crash with joystick driver with joysticks with odd axis. From 1.5.0 to 1.6.0 + INDI Base Client is now supported on Windows, MacOS, and Linux. + Added 10Micron Mount support. + Added spectrum support and libDSPAU. + Added NexDome support. + Added Pyxis Rotator support. + Added Pegasus Focuser support. + Added MBox Weather box support. + Added SnapCap dust cap and flat panel support. + Added Sesto-Senso focuser support. + Added USB_DewPoint support. + Added GPS-NMEA sources support. + Added Gemini Telescope Design Integra85 Focusing Rotator WIP support. + Added Lunatico's Armadillo and Platypus support. + Video Streaming support for MacOS. + Video Recording with libtheora (Optional). + Video Streaming with MJPEG encoder. + IOptron fixes and improvements. + More NextstarEvo improvements. + Dedicated Guider Simulator. + QHY & ASI updated to latest SDK. + Apogee fixes for MacOS. + INova fixes and improvements. Updated SDK. + QHY, Apogee, DSI builds for MacOS. + EQMod Horizon fixes. + Skywatcher Alt-Az Mount & Syncscan fixes and improvements including guide support. + StarSense support in Celestron Driver. + Improvements to mount parking & unparking. + New experimental Astrophysics Driver. + Support for Losmandy UDP protocol. + Fixed Dome slaving slew and tracking. + Added Radio Antenna Simulator. + New INDI Rotator Interface. + UDEV rule to disable automount of DSLR cameras. + Sky Quality Meter simulator. + Improvements to INDI GPS drivers. + TCFS Fixes. + SkySensorPC2000 fixes. + Fixed WatchDog behavior in case of unavailable dome. + Improved Continious Integration support with Travis & CircleCI using Docker. From 1.4.0 to 1.5.0 + New Detector Interface for photon and radio detectors. + New Software-Defined-Radio driver (RTLSDR). + New Connections plugin system to facilitate driver development. + New standarized tracking properties system for all mounts. + New Digital Settings Circiles (DSC) driver. + New Lacerta MGen driver. + New NightCrawler Focusing Rotator driver. + New Optec Gemini Focusing Rotater driver. + New iNovaPLX CCD driver. + QHY SDK update to 1.10.0. Support for QHY PoleMaster. + QSI SDK update to 7.6.0 + Support for INDI client under Windows. + Support for Pier Side in many mount drivers. + Support for SkySafari. + Fix FLIUSB for 4.6 and 4.9 kernels. + Fixed wrong time format in generated SER files. + ZWO ASI drivers for MacOS. + Various GPSD fixes. + Proper handing of ISO8601 timestamps in the generated filenames. + Improved Gemini mount driver with more functionality and bug fixes. + Many V4L2 fixes and improvements. Support for V4L2 integer menus. + Ability to define multiple primary/guide scope configurations. + ZEQ25 Improvements and fixes. + NStep driver improvements and fixes. + Added Ccache support. + Support for Gotonova driver. + Added Unity Build support. + Improved Astrophysics driver. + Added USB connectivity to SQM device driver. + More robust handling of reading pier side from mount. + Warn client that no devices are detected in case of Multiple-Devices-Per-Driver drivers. + Added PEC control to INDI::Telescope. Each driver must handle the low level protocol to actually enable or disable PEC. + Added security (hardening) flags. + SoftPEC implementation for Virtuoso mounts in skywatcherAPIMount. + Added TELESCOP, OBSERVER, and OBJECT keywords to the FITS header. From 1.3.1 to 1.4.0 + Support for HitecAstro DC Focuser. + Support for SQL-LE Sky Quality Meter unit. + Support for USB Focus V3. + Support for Quantum Filter Wheel. + Support for 10micron mounts. + ZWO ASI filter wheel support. Driver updated to latest SDK. Fix infinite loop exposure. + QHY driver updated to latest SDK. + Added preliminary support to TCP server connection for all mounts. + Updated and improved Nexstar Evo driver. + Fixed reset of filter wheel names to default values under some circumstances. + Fixed feedback loop issue in chained INDI server. + Handle correctly broken frames in FLI driver; convert time left from ms to seconds as it should be. + V4L2 CCD driver updated to properly work with DMK cameras. + Several bugfixes for Moravian CCD driver. + CCD Simulator allows for up to 4096x4096 resolution. + Raw color video streaming now uses RGB24 instead of RGBA to conserve bandwidth. + New Dome and Mount safety interlocks mechanism. + Fix the Virtuoso mount detection in SkyWatcherMountAPI driver. + Support relative driver paths to INDI server. + Fix property cache collision conflict in case of multiple devices per driver. + Moonlite driver can now sync to any value instead of reset to zero. + Store OBJECTRA and OBJECTDEC as sexigesimal strings. + New Axis Lock feature to limit joystick to specific motion axis. + INDI server now reaps zombie processes as they appear. + EQMod support for AUX encoder values. ST4 Guide Rates settings. PPEC Switches. + Fix for TELESCOPE_PIER_SIDE implementation in EQMod driver. + Several fixes for Pulsar2 driver. + Fix SER file generation for color frames. Added timestamps for each recorded frame. Support subframed video streams. + Debug and Logging options can be saved in the config file. + New CCD_TRANSFER_FORMAT property. + libindi can now be compiled under MacOS and Cygwin. Non-Linux specific 3rd party drivers are also supported under MacOS and Cygwin. + When a request for snooped is sent, it is echoed to drivers so that they send the snopped value immediately if it exists. + libindi shared library is dropped. libindi now offsers indidriver (shared), indiclient (static), and indiclientqt5 (static) libraries. + Legacy drivers removed: SkyCommander, Intelliscope, MagellanI, TruTech, SBIG STV From 1.3.0 to 1.3.1 + Support for Optec IFW Filter Wheel. + Added new method in base client getBLOBMode to retrieve previously set BLOB mode for a device/property pair. + QHY driver use software binning by default for all cameras. + Minor bug fixes and improvements across all drivers. From 1.2.0 to 1.3.0 + Support for Shelyak eShel spectrograph. + Support for NStep focuser. + Support for ASI Filter Wheel. + Support for OneStep Telescope Controller. + Support for Moravian CCD driver. + Support for GigE machine vision cameras. + Experimental SSAG CCD driver. + Adding support for fast BLOB with ENCLEN. With Fast blob mode enabled, blob performance is now significantly faster. + FITS Min/Max calculations are disabled by default to save processing time unless explicitly enabled by the user at compile time. + Qt5 based client class is added to enable multiplatform client development. + New significantly faster base64 encoding/decoding routines. + Selectable alignment modes for INDI EQMod that includes built-in EQMod alignment and INDI Alignment Subsystem. + CCD sequence queue number is no longer limited to 999. + More Starlight Xpress CCDs cameras supported. + Updated Starlight Xpress Adaptive Optics driver. + Updated Temma Takahashi mount driver. + Updated Apogee CCD driver. + Updated QHY CCD driver with numerous fixes. + Updated FLI CCD driver. + Updated Pulasr2 driver. + Updated ASI CCD with support for arm64 architecture, 1600 model and a lot more. + Significant improvements to GPhoto driver including better support for Nikon DSLRs. + Improved GPSd driver. + New CCD_FILE_PATH property to indicate remote file path. + New TELESCOPE_PIER_SIDE property. + Dome & Telescope Scripting Gateway added. + Added support for servos in Indiduino + Fix BuiltinMathPlugin crash for Southern Hemisphere + Add scope park aware feature to the rolloff roof simulator + INDI Logs are now stored under ~/.indi/logs and arranged by date and time per driver. From 1.1.0 to 1.2.0 + Support for Davis Vantage Pro/Pro2/Vue Weather Station. + Support for XAGYL Filter Wheel. + Support for Optec Flip Flat. + Support for Pulasr2 mount. + Support for JMI Smart Focus. + Support for GPS driver based on gpsd. + 3rdParty: QHY Updates and Fixes. Color camera support. + 3rdParty: GPhoto focus and live streaming fixes. + 3rdParty: Starlight Xpress support for multiple identical devices and driver improvements. + 3rdParty: ASI updated to latest SDK. Temperature readout for all cameras. ROI & Cooler fixes. + FocusLynx: Support 2 focusers. + Celestron: Support pulse guiding. Support hibernation and wakup on supported firmware. + SynScan: Complete rewrite to support latest features in the firmware. + GPhoto: Mirror lock support before capture. + RoboFocus: Update to comply to INDI::Focus standards. + SBIG: Support for connecting to Ethernet based CCDs. + ZEQ25: Updates and fixes. + MaxDomeII: Driver updated and tested under latest INDI::Dome standards. + QSI: Added anti-blooming option. + INDI WatchDog driver: Can be configured to perform observatory shutdown. + EQMod: Initial support for INDI Alignment Subsystem. Do not stop motor in low speed (guiding issue). + Temma: Complete rewrite based on INDI::Telescope and using INDI Alignment Subsystem. + AutoDome park feature in Dome devices. + Fix Crash on ODroid. + Added Meta-Weather driver to aggregate weather data from multiple sources. + Support for remotely controller dust caps and light box devices. + Handle 16bit images in Rapid Guide mode. + BaseClient non-blocking connect support. + Unified Streaming/Recording support for multiple drivers (ASI/QHY/V4L2). From 1.0.0 to 1.1.0 + INDI::Weather for support of weather devices. + INDI::GPS for support of GPS devices. + WunderGround weather driver. + Meade DSI I & II support. + FocusLynx focuser support. + PerfectStar focuser support. + World Coordinate System (WCS) support in generated FITS. + Updated Losmany driver. + New Celestron driver. + New IEQPro driver. + Support for custom parking in dome drivers. + Support Open Loop dome controllers. + Various QHY CCD & Filter Wheel fixes and QHY OSX support. + SBIG External Guide CCD fixes. + Custom parking support for Celestron/Astrophysics/AstroElectronics/IEQPro + Updated ASI ZWO drivers. + Updated Apogee library. Improved NET support in INDI Apogee driver. + Standarizing mount slew and track rates. + Video4Linux 2 fixes and improvements: (16bpp pixel formats(Y16 and BYR2), pwc flashled, colorSpace/linearization, stacking), Simutaneous record/stream/exp. Stream rate divisor. Rec. file patterns. + EQMod fixes: Keep tracking after joystick motion is stopped. Park initialization always set encoders. + Improved support for drivers on ARM architecture. + Improved logging capability. + Deprecated: LX200Legacy and indimain library. From 0.9.9 to 1.0.0 + 3rdparty: Support for QHY CCDs and CFWs (BETA). + 3rdparty: Support for Meade DSI (BETA). + 3rdparty: Support for FFMV cameras. + Support for STAR2000. + Support for Baader dome. + Support for Baader SteelDrive focuser. + Support for dome slaving (BETA). + Subframing and debayer support in GPhoto driver. + Improved CFW handling and external tracking CCD for SBIG CCDs. + Add debayer support for color cameras with user-configurable options. Debayer is performed at the client level. + Fixed deinterlacing and subframing support in Starlight Xpress drivers. + Fixed issues with joystick support in some drivers. Added joystick support to focuser and filter wheels. + Improvements in generation of FITS header. + Improvement in performance of some drivers under SBCs like Raspberry PI. + Added fan and readout speed controls to QSI CCD. + Fixed locale issue in INDI driver. + Fixed regression in LX200Basic driver. + Fixed issue with INDI Server resetting environment variable for skeleton and config files. + Various fixes for loading/saving of user configuration. From 0.9.8 to 0.9.9 + Thread-safe INDI Library. + Support for Rigelsys NFocus Focuser driver. + 3rdparty: Additional VID/PID for QHY. + 3rdparty: added USB Bandiwdth control for ZWO Optical cameras. + 3rdparty: Updated and improved INDI Apogee driver and Apogee Library major update. + 3rdparty: Live preview support for GPhoto driver. Tested on Canon. + 3rdparty: Various bugfixes and improvements in INDI SBIG driver including working guide chip support. + 3rdparty: Fixed time drift error in EQMod. Added Horizon limits. Added Backlash comp. + Updated and improved Image Agent. + Improved Astrophysics driver support. + Fixed location bug in Celestron GPS driver. + Additional information in FITS header such as filter name. + Joystick support for focuser and filter wheel devices. + Added option to enable local & remote save for FITS images in all CCD drivers. + Older V4L driver is deprecated (indi_v4l_legacy) and is replaced by indi_v4l2_ccd + V4L2: added recording (SER files) for use with Registax + V4L2: added RGGB & UYVY pixel format support to V4L2 drivers. + V4L2: fixed LX long exposure times. + Updated and improved tutorials. + Fixed few OSX compatibility issues. + Various bug fixes and improvements. From 0.9.7 to 0.9.8 + Support for Telescope Alignment Subsystem Infrastructure. This includes an implementation of Markley's singular value decomposition (SVD) based algorithm for the computation of sky/telescope coordinate conversion transforms, in addition to multiple plugin support. + Suppprt for SkyWatcherAPI Mount with Alignment Subsystem. + Support for ZWO Optics ASI Cameras (3rd party) + Support for AAG Cloud Watcher station (3rd Party). + Support for MoonLite focusers. + Support for Fishcamp CCDs (3rd Party). + Support for Imager Agent. + Improved EQMod driver including custom parking position. + New and updated Astrophysics mount driver. + New and updated QHY CCD Driver (3rd Party). + New and improved GPhoto driver to support DSLRs. + New and updated Video4Linux CCD Driver including support for long exposures (LX) and Imaging Source cameras. + New and improved Apogee CCD driver. + Improved support for SBIG CCDs including ST-I. + Updated and improved TCFS Focuser drivers. + Drivers migrated to libusb 1.0 framework. + Rapid Guide Support for CCD Drivers. + Improved compatibility with Mac OSX including INDI Server and GUI. + Various bug fixes and improvements. From 0.9.6 to 0.9.7 + Support for EQMod mount driver (3rd party). + Support for ATIK CCDs and Filter Wheels (3rd party). + Support for Shoestring Astronomy FCUSB (3rd party). + Support for joysticks and game pads under Linux. + LX200, Celeston, and EQMod drivers support joystick input. + Improved LX200 & Celestron telescope drivers. + Improved simulator drivers. + INDI server support for multiple devices per driver. + New universal logging and debugging framework for INDI developers. + Fixed an issue in TCFS driver where a connect may fail if focuser is put into sleep mode. + Fixed an issue where the client thread in INDI::BaseClient is not being terminated gracefully in blocking mode. + Fixed an issue involving non-English clients that utilize INDI client library to communicate with INDI server. + Fixed an issue where some properties in some drivers are sent before getting defined by INDI. From 0.9.5 to 0.9.6 + Support for Starlight Xpress Adaptive Optics unit. + Improved support for Startlight Xpress CCDs and Filter wheels. + Support for Arduino boards, with customizable drivers for common observatory auxiliary devices. + Support for GPUSB Guide Port Interface. + Improved support for QSI CCDs and Filter wheels. + Support for filters with absolute positioning. + Support for cameras with guiding chip. + Fixed INDI server FIFO CPU utilization bug. + Fixed various bugs with v4l drivers due to code regression. + Improved support for Mac OS X. + Improved simulators. + _REQUEST properties are now deprecated. + Updated tutorials and API. From 0.9 to 0.95 # Focuser simulator driver. # CCD, Telescope, Focuser, and Filter simulators improvements including periodic error effects, FWHM, and more. # Major improvements to INDI Base Library and INDI Client Library. # Fixed minor bugs in LX200 Generic, LX200 FS2, Magellan, and Celestron drivers. # Minor bugfixes and improvements. From 0.8 to 0.9 # iEQ45 GoTo German Equatorial Mount Driver. # INDI::Base drivers are now used for most classes of astrnomical instruments. # New improved QSI CCD & Filter driver. # New improved Starlight Xpress CCD & Filter driver. # New improved RoboFocus driver. # libboost is no longer required to build libindi. # Numerous bug fixes and minor improvements. From 0.7.2 to 0.8 # TCF-S Focuser driver. # Synscan Telescope driver. From 0.7.0 to 0.7.1 # Fixed change filter bug in true technology filter wheel. (JM) # setINDI updated and improved. (ED) # Improved INDI::Mediator functionality. (JM) # Fixed buffer reading in INDI::BaseClient. (JM) # Add new tutorial for INDI::BaseClient. (JM). From 0.6.2 to 0.7.0 # Dynamic renaming of drivers upon run time. # Standard helper API to create and utilize INDI clients. # Ability to load driver properties from an external XML file. # Ability to write/read XML configuration files for driver values to be loaded at run time. # Facilitating debugging and simulation of drivers. # New C++ framework to facilitate the development of new INDI drivers. # Several bug fixes for current drivers and framework. From 0.6.1 to 0.6.2 # Build related updates. From 0.6 to 0.6.1 # Updating drivers.xml to comply to new XML structure for group and devices metadata descriptions. From 0.5 to 0.6 # Devices: + Astrophysics mount is now working. + Apogee driver is now working. # Features: + New libindi structure to streamline development. + Drivers using Standard Property _REQUEST WO to make requests. This facilitates scripting and automation of drivers. + Updated inter-driver communication with INDI SNOOP. From 0.4 to 0.5 # Devices: + True Technology Filter Wheel + SBIG STV # Features: + Added INDI Observer pattern to enable flexible inter-driver communication. + getINDI now supports BLOBs. + LX200 Drivers use client timestamp to update the telescope internal clock. The old behavior was to use to system's time. + Added a new INDI Standard Property: UTC_OFFSET. + Dropping threaded INDI server in favor of the slightly better non-threaded version due to performance considerations. # Bugs + SBIG CCD driver was updated to fix problems with CFITSIO. + Updated TTY API to include error reporting, in addition to fixing a few bugs. + Fixed INDI Mac OSX Crash. # Known Issues + Astrophysics Mount driver (apmount) is not working. It is currently under new development and is intented to be released in the next version as it matures. + Meade LPI exposure is locked to 1 second. The Video4Linux support for timed exposures is limited. A fix should be available in the next release. + The SBIG driver does not allow autoguiding. When one selects the guider CCD, any exposure on the imaging CCD is cancelled and vice-versa. From v0.3 to v0.4: # Devices: + SBIG CCD + SBIG CFW + RoboFocus + FLI Precision Focuser + Orion Atlas / Sky Scan # Other: + Added more API documentation and revised existing documentation for accuracy and consistency. + Fixed UTC correction bug in LX200 driver. + Fixed pallete selection in V4L 2 drivers. + Fixed bug in eventloop that can cause IE timers to crash. + Added variable focus speed for Meade Autostar and GPS. + Added CFITSIO, a mature and robust FITS library. + New RS232 API for common access routines. From v0.2 to v0.3: # Devices: + Apogee CCD (Experimental) + SkyCommander + Temma Takahashi + FLI Filter Wheel + Meade Lunar Planetary Imager (Experimental) + Astrophysics AP # Other: + Support for Video 4 Linux 2 + Multi-threaded INDI server + Binary transfer via BLOB + INDI scripting tools + Various bug fixing INDI Library v1.1 conforms to INDI wire protocol v1.7 libindi/COPYING.BSD0000664000175000017500000000320013263645557013127 0ustar jasemjasemFiles: cmake/* Copyright: Bryan Donlan Carsten Niehaus 2006, Alexander Neundorf 2006, Allen Winter 2006, 2008, 2011, Jasem Mutlaq 2009, Geoffrey Hausheer License: BSD-3-clause Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: . 1. Redistributions of source code must retain the copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 3. The name of the author may not be used to endorse or promote products derived from this software without specific prior written permission. . THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. libindi/CMakeLists.txt0000664000175000017500000016115213263645557014240 0ustar jasemjasemcmake_minimum_required(VERSION 3.0) PROJECT(libindi C CXX) # Tell CMake to run moc when necessary: set(CMAKE_AUTOMOC ON) # As moc files are generated in the binary dir, tell CMake # to always look for includes there: set(CMAKE_INCLUDE_CURRENT_DIR ON) LIST(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake_modules/") LIST(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/../cmake_modules/") include(GNUInstallDirs) include(FeatureSummary) if(ANDROID OR "${CMAKE_SYSTEM_NAME}" STREQUAL "Android") set(ANDROID ON) add_definitions(-DANDROID) endif() include(CMakeCommon) # Clang Format support IF (UNIX OR APPLE) SET(FORMAT_CODE OFF CACHE BOOL "Enable Clang Format") IF (FORMAT_CODE MATCHES ON) FILE(GLOB_RECURSE ALL_SOURCE_FILES *.c *.cpp *.h) FOREACH(SOURCE_FILE ${ALL_SOURCE_FILES}) STRING(FIND ${SOURCE_FILE} ${CMAKE_SOURCE_DIR} DIR_FOUND) IF (NOT ${DIR_FOUND} EQUAL 0) LIST(REMOVE_ITEM ALL_SOURCE_FILES ${SOURCE_FILE}) ENDIF () ENDFOREACH () FIND_PROGRAM(CLANGFORMAT_EXE NAMES clang-format-5.0) IF (CLANGFORMAT_EXE) ADD_CUSTOM_TARGET(clang-format COMMAND ${CLANGFORMAT_EXE} -style=file -i ${ALL_SOURCE_FILES}) ENDIF () ENDIF () ENDIF () ##################################### INDI version ################################################ # N.B. DO NOT Forget to update version also in indiapi.h # Proper way is to use indiversion.h.cmake file but this would break make existing applications so let us stick to the old proven way set(INDI_SOVERSION "1") set(CMAKE_INDI_VERSION_MAJOR 1) set(CMAKE_INDI_VERSION_MINOR 7) set(CMAKE_INDI_VERSION_RELEASE 1) set(CMAKE_INDI_VERSION_STRING "${CMAKE_INDI_VERSION_MAJOR}.${CMAKE_INDI_VERSION_MINOR}.${CMAKE_INDI_VERSION_RELEASE}") set(INDI_VERSION ${CMAKE_INDI_VERSION_MAJOR}.${CMAKE_INDI_VERSION_MINOR}.${CMAKE_INDI_VERSION_RELEASE}) ######################################## Paths ################################################### set(DATA_INSTALL_DIR "${CMAKE_INSTALL_PREFIX}/share/indi/") set(BIN_INSTALL_DIR "${CMAKE_INSTALL_PREFIX}/bin") set(INCLUDE_INSTALL_DIR "${CMAKE_INSTALL_PREFIX}/include") IF(APPLE) set(CMAKE_SHARED_LINKER_FLAGS "-undefined dynamic_lookup") ENDIF(APPLE) ################################## Install Directories ########################################### ## the following are directories where stuff will be installed to set(INCLUDE_INSTALL_DIR "${CMAKE_INSTALL_PREFIX}/include/") set(PKGCONFIG_INSTALL_PREFIX "${CMAKE_INSTALL_LIBDIR}/pkgconfig/") set(UDEVRULES_INSTALL_DIR "/lib/udev/rules.d" CACHE STRING "Base directory for udev rules") set(PKG_CONFIG_LIBDIR ${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_LIBDIR}) ##################################### Build Options ############################################## # Select which components to build and what options to apply OPTION (INDI_BUILD_SERVER "Build INDI Server" ON) OPTION (INDI_BUILD_DRIVERS "Build INDI Drivers, Tools, and Examples" ON) OPTION (INDI_BUILD_CLIENT "Build INDI POSIX Client" ON) OPTION (INDI_BUILD_QT5_CLIENT "Build INDI Qt5 Client" OFF) OPTION (INDI_BUILD_UNITTESTS "Build INDI tests" OFF) OPTION (INDI_FAST_BLOB "Build INDI with Fast BLOB support" ON) OPTION (INDI_CALCULATE_MINMAX "Calculate and store image minimum and maximum values in FITS header" OFF) ################################################################################################### ######################################### Fast Blob ############################################# ################################################################################################### IF (INDI_FAST_BLOB) # Append ENCLEN attribute to outgoing BLOB elements to enable fast parsing by clients add_definitions(-DWITH_ENCLEN) ENDIF(INDI_FAST_BLOB) ################################################################################################### ###################################### Calculate Min/Max ######################################### ################################################################################################### IF (INDI_CALCULATE_MINMAX) # Calculate Min/Max values to store them in FITS header add_definitions(-DWITH_MINMAX) ENDIF(INDI_CALCULATE_MINMAX) ################################################################################################### ##################################### Components ################################################ ################################################################################################### set_package_properties(Nova PROPERTIES DESCRIPTION "A general purpose, double precision, Celestial Mechanics, Astrometry and Astrodynamics library" URL "http://libnova.sourceforge.net" TYPE REQUIRED PURPOSE "Provides INDI with astrodynamics library.") set_package_properties(CFITSIO PROPERTIES DESCRIPTION "A library for reading and writing data files in FITS (Flexible Image Transport System) data format" URL "http://heasarc.gsfc.nasa.gov/fitsio/fitsio.html" TYPE REQUIRED PURPOSE "Provides INDI with FITS I/O support.") #################################################################################################### # # Component : INDI Server # Dependencies: pthreads # Supported OS: Linux, BSD, MacOS, Cygwin # ################################################################################################# if (INDI_BUILD_SERVER) if (WIN32 OR ANDROID) message(WARNING "INDI Server is only supported under Linux, BSD, MacOS, and Cygwin while current system is " ${CMAKE_SYSTEM_NAME}) else() # 1. Dependencies find_package(Threads REQUIRED) # 2. Includes include_directories( ${CMAKE_CURRENT_SOURCE_DIR}) # 3. Build SET(indiserver_SRC ${CMAKE_CURRENT_SOURCE_DIR}/indiserver.c ${CMAKE_CURRENT_SOURCE_DIR}/fq.c ${CMAKE_CURRENT_SOURCE_DIR}/libs/lilxml.c) IF (UNITY_BUILD) ENABLE_UNITY_BUILD(indiserver indiserver_SRC 10 c) ENDIF () add_executable(indiserver ${indiserver_SRC}) target_link_libraries(indiserver ${CMAKE_THREAD_LIBS_INIT}) install(TARGETS indiserver RUNTIME DESTINATION bin) endif (WIN32 OR ANDROID) endif (INDI_BUILD_SERVER) ################################################################################################# # # Component : INDI Client # Dependencies: zlib, cfitsio # Supported OS: Linux, BSD, MacOS, Windows, Cygwin # N.B. Windows support pending migration of networking code ################################################################################################# if (INDI_BUILD_CLIENT AND NOT ANDROID) # 1. Dependencies find_package(Threads REQUIRED) find_package(ZLIB REQUIRED) find_package(CFITSIO REQUIRED) # 2. Includes include_directories( ${CMAKE_CURRENT_BINARY_DIR}) include_directories( ${CMAKE_CURRENT_SOURCE_DIR}) include_directories( ${CMAKE_CURRENT_SOURCE_DIR}/libs) include_directories( ${CMAKE_CURRENT_SOURCE_DIR}/libs/indibase) include_directories( ${ZLIB_INCLUDE_DIR}) include_directories( ${CFITSIO_INCLUDE_DIR}) # 3. Build SET(indiclient_C_SRC ${CMAKE_CURRENT_SOURCE_DIR}/libs/lilxml.c ${CMAKE_CURRENT_SOURCE_DIR}/base64.c) SET(indiclient_CXX_SRC ${CMAKE_CURRENT_SOURCE_DIR}/libs/indibase/basedevice.cpp ${CMAKE_CURRENT_SOURCE_DIR}/libs/indibase/baseclient.cpp ${CMAKE_CURRENT_SOURCE_DIR}/libs/indibase/indiproperty.cpp ${CMAKE_CURRENT_SOURCE_DIR}/libs/indibase/indistandardproperty.cpp) IF (UNITY_BUILD) ENABLE_UNITY_BUILD(indiclient_c indiclient_C_SRC 10 c) ENABLE_UNITY_BUILD(indiclient_cxx indiclient_CXX_SRC 10 cpp) ENDIF () SET(indiclient_C_SRC ${indiclient_C_SRC} ${CMAKE_CURRENT_SOURCE_DIR}/libs/indicom.c) add_library(indiclient STATIC ${indiclient_C_SRC} ${indiclient_CXX_SRC}) if (NOT CYGWIN AND NOT WIN32) set_target_properties(indiclient PROPERTIES COMPILE_FLAGS "-fPIC") endif (NOT CYGWIN AND NOT WIN32) target_link_libraries(indiclient ${CMAKE_THREAD_LIBS_INIT}) install(TARGETS indiclient ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}) install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/libs/indibase/baseclient.h DESTINATION ${INCLUDE_INSTALL_DIR}/libindi COMPONENT Devel) endif (INDI_BUILD_CLIENT AND NOT ANDROID) ################################################################################################# # # Component : INDI Qt5 Client # Dependencies: Qt5Network, zlib, cfitsio # Supported OS: Linux, BSD, MacOS, Cygwin, Windows, Android # ################################################################################################# if (INDI_BUILD_QT5_CLIENT) set(QT_ANDROID "" CACHE path "Qt Android path") # 1. Dependencies if (ANDROID) include(${QT_ANDROID}/lib/cmake/Qt5Network/Qt5NetworkConfig.cmake) if (NOT CFITSIO_DIR) message(FATAL_ERROR CFITSIO_DIR must be set) else () set(CFITSIO_INCLUDE_DIR ${CFITSIO_DIR}) set(CFITSIO_LIBRARIES ${CFITSIO_DIR}/libcfitsio.a) endif () else () find_package(Qt5Network) find_package(ZLIB REQUIRED) find_package(CFITSIO REQUIRED) endif () # 2. Includes include_directories( ${CMAKE_CURRENT_BINARY_DIR}) include_directories( ${CMAKE_CURRENT_SOURCE_DIR}) include_directories( ${CMAKE_CURRENT_SOURCE_DIR}/libs) include_directories( ${CMAKE_CURRENT_SOURCE_DIR}/libs/indibase) include_directories( ${CFITSIO_INCLUDE_DIR}) # 3. Build message(STATUS "Building INDI Client with Qt5 support") SET(indiclientqt_C_SRC ${CMAKE_CURRENT_SOURCE_DIR}/libs/lilxml.c ${CMAKE_CURRENT_SOURCE_DIR}/base64.c) SET(indiclientqt_CXX_SRC ${CMAKE_CURRENT_SOURCE_DIR}/libs/indibase/basedevice.cpp ${CMAKE_CURRENT_SOURCE_DIR}/libs/indibase/baseclientqt.cpp ${CMAKE_CURRENT_SOURCE_DIR}/libs/indibase/indiproperty.cpp ${CMAKE_CURRENT_SOURCE_DIR}/libs/indibase/indistandardproperty.cpp) IF (UNITY_BUILD) ENABLE_UNITY_BUILD(indiclientqt_c indiclientqt_C_SRC 10 c) ENABLE_UNITY_BUILD(indiclientqt_cxx indiclientqt_CXX_SRC 10 cpp) ENDIF () SET(indiclientqt_C_SRC ${indiclientqt_C_SRC} ${CMAKE_CURRENT_SOURCE_DIR}/libs/indicom.c) add_library(indiclientqt STATIC ${indiclientqt_C_SRC} ${indiclientqt_CXX_SRC}) if (NOT CYGWIN AND NOT WIN32) set_target_properties(indiclientqt PROPERTIES COMPILE_FLAGS "-fPIC") endif(NOT CYGWIN AND NOT WIN32) qt5_use_modules(indiclientqt Network) if (WIN32 OR ANDROID) install(TARGETS indiclientqt ARCHIVE DESTINATION lib) else(WIN32 OR ANDROID) install(TARGETS indiclientqt ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}) endif(WIN32 OR ANDROID) install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/libs/indibase/baseclientqt.h DESTINATION ${INCLUDE_INSTALL_DIR}/libindi COMPONENT Devel) endif (INDI_BUILD_QT5_CLIENT) ################################################################################################# # # Component : INDI Drivers, Tools, and Examples # Dependencies: pthreads, usb1, zLib, cfitsio, nova, curl, jpeg (Linux Only) # Supported OS: Linux, BSD, MacOS, Cygwin # N.B. Webcam drivers only supported under Linux (Video4Linux2). Joystick support only under Linux # ################################################################################################# if (INDI_BUILD_DRIVERS) if (WIN32 OR ANDROID) message(WARNING "INDI drivers are only supported under Linux, BSD, MacOS, and Cygwin while current system is " ${CMAKE_SYSTEM_NAME}) else(WIN32 OR ANDROID) # 1. Dependencies find_package(Threads REQUIRED) find_package(ZLIB REQUIRED) find_package(CFITSIO REQUIRED) find_package(Nova REQUIRED) find_package(USB1 REQUIRED) find_package(CURL REQUIRED) find_package(GSL REQUIRED) find_package(JPEG REQUIRED) # Math Library FIND_LIBRARY(M_LIB m) # 2. Includes include_directories( ${CMAKE_CURRENT_BINARY_DIR}) include_directories( ${CMAKE_CURRENT_SOURCE_DIR}) include_directories( ${CMAKE_CURRENT_SOURCE_DIR}/libs) include_directories( ${CMAKE_CURRENT_SOURCE_DIR}/libs/indibase) include_directories( ${CMAKE_CURRENT_SOURCE_DIR}/libs/stream) include_directories( ${CFITSIO_INCLUDE_DIR}) include_directories( ${NOVA_INCLUDE_DIR}) include_directories( ${USB1_INCLUDE_DIRS}) include_directories( ${GSL_INCLUDE_DIRS}) include_directories( ${JPEG_INCLUDE_DIR} ) IF (${CMAKE_SYSTEM_NAME} MATCHES "Linux") include_directories( ${CMAKE_CURRENT_SOURCE_DIR}/libs/webcam) ENDIF (${CMAKE_SYSTEM_NAME} MATCHES "Linux") configure_file(${CMAKE_CURRENT_SOURCE_DIR}/config-usb.h.cmake ${CMAKE_CURRENT_BINARY_DIR}/config-usb.h) ################################################################################################### ######################################## Sources ################################################ ################################################################################################### IF (APPLE) SET(hidapi_SRCS ${CMAKE_CURRENT_SOURCE_DIR}/libs/indibase/hid_mac.c) ELSEIF (WIN32) SET(hidapi_SRCS ${CMAKE_CURRENT_SOURCE_DIR}/libs/indibase/hid_win.c) ELSE () SET(hidapi_SRCS ${CMAKE_CURRENT_SOURCE_DIR}/libs/indibase/hid_libusb.c) ENDIF() IF (UNIX) find_package(OggTheora) IF (OGGTHEORA_FOUND) INCLUDE_DIRECTORIES(${THEORA_INCLUDE_DIRS}) SET(HAVE_THEORA 1) SET (theorarecorder_CXX_SRC ${CMAKE_CURRENT_SOURCE_DIR}/libs/stream/recorder/theorarecorder.cpp) ENDIF(OGGTHEORA_FOUND) SET(libstream_CXX_SRC ${CMAKE_CURRENT_SOURCE_DIR}/libs/stream/streammanager.cpp ${CMAKE_CURRENT_SOURCE_DIR}/libs/stream/recorder/recorderinterface.cpp ${CMAKE_CURRENT_SOURCE_DIR}/libs/stream/recorder/recordermanager.cpp ${CMAKE_CURRENT_SOURCE_DIR}/libs/stream/recorder/serrecorder.cpp ${CMAKE_CURRENT_SOURCE_DIR}/libs/stream/encoder/encodermanager.cpp ${CMAKE_CURRENT_SOURCE_DIR}/libs/stream/encoder/encoderinterface.cpp ${CMAKE_CURRENT_SOURCE_DIR}/libs/stream/encoder/rawencoder.cpp ${CMAKE_CURRENT_SOURCE_DIR}/libs/stream/encoder/mjpegencoder.cpp ${theorarecorder_CXX_SRC} ) SET(libstream_C_SRC ${CMAKE_CURRENT_SOURCE_DIR}/libs/stream/jpegutils.c) IF (UNITY_BUILD) ENABLE_UNITY_BUILD(libstream libstream_C_SRC 10 c) ENABLE_UNITY_BUILD(libstream libstream_CXX_SRC 10 cpp) ENDIF () IF (${CMAKE_SYSTEM_NAME} MATCHES "Linux") SET(libwebcam_C_SRC ${CMAKE_CURRENT_SOURCE_DIR}/libs/webcam/v4l2_colorspace.c ${CMAKE_CURRENT_SOURCE_DIR}/libs/webcam/ccvt_c2.c ${CMAKE_CURRENT_SOURCE_DIR}/libs/webcam/ccvt_misc.c) SET(libwebcam_CXX_SRC ${CMAKE_CURRENT_SOURCE_DIR}/libs/webcam/v4l2_base.cpp ${CMAKE_CURRENT_SOURCE_DIR}/libs/webcam/v4l2_decode/v4l2_decode.cpp ${CMAKE_CURRENT_SOURCE_DIR}/libs/webcam/v4l2_decode/v4l2_builtin_decoder.cpp ) IF (UNITY_BUILD) ENABLE_UNITY_BUILD(libwebcam libwebcam_C_SRC 10 c) ENABLE_UNITY_BUILD(libwebcam libwebcam_CXX_SRC 10 cpp) ENDIF (UNITY_BUILD) ENDIF (${CMAKE_SYSTEM_NAME} MATCHES "Linux") ENDIF(UNIX) SET(indidriver_C_SRC ${CMAKE_CURRENT_SOURCE_DIR}/indidriver.c ${CMAKE_CURRENT_SOURCE_DIR}/indidrivermain.c ${CMAKE_CURRENT_SOURCE_DIR}/eventloop.c ${CMAKE_CURRENT_SOURCE_DIR}/libs/lilxml.c ${CMAKE_CURRENT_SOURCE_DIR}/base64.c) SET(indidriver_CXX_SRC ${CMAKE_CURRENT_SOURCE_DIR}/libs/indibase/basedevice.cpp ${CMAKE_CURRENT_SOURCE_DIR}/libs/indibase/defaultdevice.cpp ${CMAKE_CURRENT_SOURCE_DIR}/libs/indibase/indiproperty.cpp ${CMAKE_CURRENT_SOURCE_DIR}/libs/indibase/indiccd.cpp ${CMAKE_CURRENT_SOURCE_DIR}/libs/indibase/indidetector.cpp ${CMAKE_CURRENT_SOURCE_DIR}/libs/indibase/inditelescope.cpp ${CMAKE_CURRENT_SOURCE_DIR}/libs/indibase/indifilterwheel.cpp ${CMAKE_CURRENT_SOURCE_DIR}/libs/indibase/indifocuserinterface.cpp ${CMAKE_CURRENT_SOURCE_DIR}/libs/indibase/indifocuser.cpp ${CMAKE_CURRENT_SOURCE_DIR}/libs/indibase/indirotator.cpp ${CMAKE_CURRENT_SOURCE_DIR}/libs/indibase/indiusbdevice.cpp ${CMAKE_CURRENT_SOURCE_DIR}/libs/indibase/indiguiderinterface.cpp ${CMAKE_CURRENT_SOURCE_DIR}/libs/indibase/indifilterinterface.cpp ${CMAKE_CURRENT_SOURCE_DIR}/libs/indibase/indirotatorinterface.cpp ${CMAKE_CURRENT_SOURCE_DIR}/libs/indibase/indidome.cpp ${CMAKE_CURRENT_SOURCE_DIR}/libs/indibase/indigps.cpp ${CMAKE_CURRENT_SOURCE_DIR}/libs/indibase/indiweather.cpp ${CMAKE_CURRENT_SOURCE_DIR}/libs/indibase/indidustcapinterface.cpp ${CMAKE_CURRENT_SOURCE_DIR}/libs/indibase/indilightboxinterface.cpp ${CMAKE_CURRENT_SOURCE_DIR}/libs/indibase/indilogger.cpp ${CMAKE_CURRENT_SOURCE_DIR}/libs/indibase/indicontroller.cpp ${CMAKE_CURRENT_SOURCE_DIR}/libs/indibase/indistandardproperty.cpp ${CMAKE_CURRENT_SOURCE_DIR}/libs/indibase/connectionplugins/connectioninterface.cpp ${CMAKE_CURRENT_SOURCE_DIR}/libs/indibase/connectionplugins/connectionserial.cpp ${CMAKE_CURRENT_SOURCE_DIR}/libs/indibase/connectionplugins/connectiontcp.cpp) IF (UNITY_BUILD) ENABLE_UNITY_BUILD(indidriver_c indidriver_C_SRC 10 c) ENABLE_UNITY_BUILD(indidriver_cxx indidriver_CXX_SRC 10 cpp) ENDIF () SET(indidriver_C_SRC ${indidriver_C_SRC} ${CMAKE_CURRENT_SOURCE_DIR}/libs/indicom.c) ################################################## ########## INDI Default Driver Library ########### ################################################## if (CYGWIN) ## For Cygwin we only build static library add_definitions(-U__STRICT_ANSI__) find_package(Iconv REQUIRED) add_library(indidriver STATIC ${indidriver_C_SRC} ${indidriver_CXX_SRC} ${libstream_C_SRC} ${libstream_CXX_SRC} ${hidapi_SRCS}) target_compile_definitions(indidriver PRIVATE "-DHAVE_LIBNOVA") set_target_properties(indidriver PROPERTIES VERSION ${CMAKE_INDI_VERSION_STRING} SOVERSION ${INDI_SOVERSION} OUTPUT_NAME indidriver) target_link_libraries(indidriver ${ICONV_LIBRARIES} ${USB1_LIBRARIES} ${NOVA_LIBRARIES} ${CMAKE_THREAD_LIBS_INIT} ${CFITSIO_LIBRARIES} ${M_LIB} ${ZLIB_LIBRARY} ${JPEG_LIBRARY}) IF (OGGTHEORA_FOUND) target_link_libraries(indidriver ${OGGTHEORA_LIBRARIES} ${THEORA_LIBRARIES}) ENDIF() install(TARGETS indidriver ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}) else() ## Static indidriver Library add_library(indidriverstatic STATIC ${indidriver_C_SRC} ${indidriver_CXX_SRC} ${libstream_C_SRC} ${libstream_CXX_SRC} ${libwebcam_C_SRC} ${libwebcam_CXX_SRC} ${hidapi_SRCS}) set_target_properties(indidriverstatic PROPERTIES COMPILE_FLAGS "-fPIC") target_compile_definitions(indidriverstatic PRIVATE "-DHAVE_LIBNOVA") set_target_properties(indidriverstatic PROPERTIES VERSION ${CMAKE_INDI_VERSION_STRING} SOVERSION ${INDI_SOVERSION} OUTPUT_NAME indidriver) target_link_libraries(indidriverstatic ${USB1_LIBRARIES} ${NOVA_LIBRARIES} ${CMAKE_THREAD_LIBS_INIT} ${CFITSIO_LIBRARIES} ${M_LIB} ${ZLIB_LIBRARY} ${JPEG_LIBRARY}) IF (OGGTHEORA_FOUND) target_link_libraries(indidriverstatic ${OGGTHEORA_LIBRARIES} ${THEORA_LIBRARIES}) ENDIF() install(TARGETS indidriverstatic ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}) ## Dynamic indidriver Library add_library(indidriver SHARED ${indidriver_C_SRC} ${indidriver_CXX_SRC} ${libstream_C_SRC} ${libstream_CXX_SRC} ${libwebcam_C_SRC} ${libwebcam_CXX_SRC} ${hidapi_SRCS}) set_target_properties(indidriver PROPERTIES COMPILE_FLAGS "-fPIC") target_compile_definitions(indidriver PRIVATE "-DHAVE_LIBNOVA") set_target_properties(indidriver PROPERTIES VERSION ${CMAKE_INDI_VERSION_STRING} SOVERSION ${INDI_SOVERSION} OUTPUT_NAME indidriver) target_link_libraries(indidriver ${USB1_LIBRARIES} ${NOVA_LIBRARIES} ${CMAKE_THREAD_LIBS_INIT} ${CFITSIO_LIBRARIES} ${M_LIB} ${ZLIB_LIBRARY} ${JPEG_LIBRARY}) IF (OGGTHEORA_FOUND) target_link_libraries(indidriver ${OGGTHEORA_LIBRARIES} ${THEORA_LIBRARIES}) ENDIF() #IF (${CMAKE_SYSTEM_NAME} MATCHES "Linux") # target_link_libraries(indidriver -lpthread) #ENDIF () install(TARGETS indidriver LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}) set(PKG_CONFIG_LIBS "${PKG_CONFIG_LIBS} -lindidriver -lindiAlignmentDriver") endif(CYGWIN) ################################################## ########### INDI Alignment Subsystem ############# ################################################## add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/libs/indibase/alignment) ##################################### ######## AGENT GROUP ################ ##################################### ########### Imager ############## set(imager_SRCS ${CMAKE_CURRENT_SOURCE_DIR}/drivers/agent/agent_imager.cpp ${CMAKE_CURRENT_SOURCE_DIR}/drivers/agent/group.cpp ) add_executable(indi_imager_agent ${imager_SRCS}) target_link_libraries(indi_imager_agent indidriver indiclient) install(TARGETS indi_imager_agent RUNTIME DESTINATION bin) ################################################################################# ##################################### ########## TELESCOPE GROUP ########## ##################################### ########### LX200 Basic ############# SET(lx200basic_SRC ${CMAKE_CURRENT_SOURCE_DIR}/drivers/telescope/lx200driver.cpp ${CMAKE_CURRENT_SOURCE_DIR}/drivers/telescope/lx200basic.cpp) IF (UNITY_BUILD) ENABLE_UNITY_BUILD(lx200basic lx200basic_SRC 10 cpp) ENDIF () add_executable(indi_lx200basic ${lx200basic_SRC}) target_link_libraries(indi_lx200basic indidriver) install(TARGETS indi_lx200basic RUNTIME DESTINATION bin) ################################################################################# ########### LX200 Generic ########### SET(lx200generic_SRCS ${CMAKE_CURRENT_SOURCE_DIR}/drivers/telescope/lx200driver.cpp ${CMAKE_CURRENT_SOURCE_DIR}/drivers/telescope/lx200autostar.cpp ${CMAKE_CURRENT_SOURCE_DIR}/drivers/telescope/lx200_16.cpp ${CMAKE_CURRENT_SOURCE_DIR}/drivers/telescope/lx200gps.cpp ${CMAKE_CURRENT_SOURCE_DIR}/drivers/telescope/lx200generic.cpp ${CMAKE_CURRENT_SOURCE_DIR}/drivers/telescope/lx200telescope.cpp ${CMAKE_CURRENT_SOURCE_DIR}/drivers/telescope/lx200classic.cpp ${CMAKE_CURRENT_SOURCE_DIR}/drivers/telescope/lx200apdriver.cpp ${CMAKE_CURRENT_SOURCE_DIR}/drivers/telescope/lx200gemini.cpp ${CMAKE_CURRENT_SOURCE_DIR}/drivers/telescope/lx200zeq25.cpp ${CMAKE_CURRENT_SOURCE_DIR}/drivers/telescope/lx200gotonova.cpp ${CMAKE_CURRENT_SOURCE_DIR}/drivers/telescope/lx200pulsar2.cpp ${CMAKE_CURRENT_SOURCE_DIR}/drivers/telescope/lx200ap_experimentaldriver.cpp ${CMAKE_CURRENT_SOURCE_DIR}/drivers/telescope/lx200ap_experimental.cpp ${CMAKE_CURRENT_SOURCE_DIR}/drivers/telescope/lx200ap.cpp ${CMAKE_CURRENT_SOURCE_DIR}/drivers/telescope/lx200ap_gtocp2.cpp ${CMAKE_CURRENT_SOURCE_DIR}/drivers/telescope/lx200fs2.cpp ${CMAKE_CURRENT_SOURCE_DIR}/drivers/telescope/lx200ss2000pc.cpp ${CMAKE_CURRENT_SOURCE_DIR}/drivers/telescope/lx200_OnStep.cpp ${CMAKE_CURRENT_SOURCE_DIR}/drivers/telescope/lx200_10micron.cpp ${CMAKE_CURRENT_SOURCE_DIR}/drivers/telescope/ioptronHC8406.cpp) IF (UNITY_BUILD) ENABLE_UNITY_BUILD(lx200generic lx200generic_SRCS 10 cpp) ENDIF () add_executable(indi_lx200generic ${lx200generic_SRCS}) target_compile_definitions(indi_lx200generic PRIVATE -D_XOPEN_SOURCE=600 -D_POSIX_C_SOURCE=200809L) target_link_libraries(indi_lx200generic indidriver) install(TARGETS indi_lx200generic RUNTIME DESTINATION bin ) file(WRITE ${CMAKE_CURRENT_BINARY_DIR}/make_lx200generic_symlink.cmake "exec_program(\"${CMAKE_COMMAND}\" ARGS -E create_symlink indi_lx200generic \$ENV{DESTDIR}${BIN_INSTALL_DIR}/indi_lx200classic)\n exec_program(\"${CMAKE_COMMAND}\" ARGS -E create_symlink indi_lx200generic \$ENV{DESTDIR}${BIN_INSTALL_DIR}/indi_lx200autostar)\n exec_program(\"${CMAKE_COMMAND}\" ARGS -E create_symlink indi_lx200generic \$ENV{DESTDIR}${BIN_INSTALL_DIR}/indi_lx200_16)\n exec_program(\"${CMAKE_COMMAND}\" ARGS -E create_symlink indi_lx200generic \$ENV{DESTDIR}${BIN_INSTALL_DIR}/indi_lx200gps)\n exec_program(\"${CMAKE_COMMAND}\" ARGS -E create_symlink indi_lx200generic \$ENV{DESTDIR}${BIN_INSTALL_DIR}/indi_lx200ap_experimental)\n exec_program(\"${CMAKE_COMMAND}\" ARGS -E create_symlink indi_lx200generic \$ENV{DESTDIR}${BIN_INSTALL_DIR}/indi_lx200ap_gtocp2)\n exec_program(\"${CMAKE_COMMAND}\" ARGS -E create_symlink indi_lx200generic \$ENV{DESTDIR}${BIN_INSTALL_DIR}/indi_lx200ap)\n exec_program(\"${CMAKE_COMMAND}\" ARGS -E create_symlink indi_lx200generic \$ENV{DESTDIR}${BIN_INSTALL_DIR}/indi_lx200gemini)\n exec_program(\"${CMAKE_COMMAND}\" ARGS -E create_symlink indi_lx200generic \$ENV{DESTDIR}${BIN_INSTALL_DIR}/indi_lx200zeq25)\n exec_program(\"${CMAKE_COMMAND}\" ARGS -E create_symlink indi_lx200generic \$ENV{DESTDIR}${BIN_INSTALL_DIR}/indi_lx200gotonova)\n exec_program(\"${CMAKE_COMMAND}\" ARGS -E create_symlink indi_lx200generic \$ENV{DESTDIR}${BIN_INSTALL_DIR}/indi_lx200pulsar2)\n exec_program(\"${CMAKE_COMMAND}\" ARGS -E create_symlink indi_lx200generic \$ENV{DESTDIR}${BIN_INSTALL_DIR}/indi_lx200fs2)\n exec_program(\"${CMAKE_COMMAND}\" ARGS -E create_symlink indi_lx200generic \$ENV{DESTDIR}${BIN_INSTALL_DIR}/indi_lx200ss2000pc)\n exec_program(\"${CMAKE_COMMAND}\" ARGS -E create_symlink indi_lx200generic \$ENV{DESTDIR}${BIN_INSTALL_DIR}/indi_lx200_OnStep)\n exec_program(\"${CMAKE_COMMAND}\" ARGS -E create_symlink indi_lx200generic \$ENV{DESTDIR}${BIN_INSTALL_DIR}/indi_lx200_10micron)\n exec_program(\"${CMAKE_COMMAND}\" ARGS -E create_symlink indi_lx200generic \$ENV{DESTDIR}${BIN_INSTALL_DIR}/indi_ioptronHC8406)\n ") set_target_properties(indi_lx200generic PROPERTIES POST_INSTALL_SCRIPT ${CMAKE_CURRENT_BINARY_DIR}/make_lx200generic_symlink.cmake) ################################################################################# ########### Celestron GPS ############ SET(celestrongps_SRC ${CMAKE_CURRENT_SOURCE_DIR}/drivers/telescope/celestrondriver.cpp ${CMAKE_CURRENT_SOURCE_DIR}/drivers/telescope/celestrongps.cpp) add_executable(indi_celestron_gps ${celestrongps_SRC}) target_link_libraries(indi_celestron_gps indidriver) install(TARGETS indi_celestron_gps RUNTIME DESTINATION bin) ################################################################################# ########### Takahashi Temma ########## SET(temma_SRC ${CMAKE_CURRENT_SOURCE_DIR}/drivers/telescope/temmadriver.cpp ) add_executable(indi_temma_telescope ${temma_SRC}) target_link_libraries(indi_temma_telescope indidriver AlignmentDriver) install(TARGETS indi_temma_telescope RUNTIME DESTINATION bin) ################################################################################# ########### Paramount ########## SET(paramount_SRC ${CMAKE_CURRENT_SOURCE_DIR}/drivers/telescope/paramount.cpp) add_executable(indi_paramount_telescope ${paramount_SRC}) target_link_libraries(indi_paramount_telescope indidriver) install(TARGETS indi_paramount_telescope RUNTIME DESTINATION bin) ################################################################################# ########### Syncscan ############### SET(synscan_SRC ${CMAKE_CURRENT_SOURCE_DIR}/drivers/telescope/synscanmount.cpp) add_executable(indi_synscan_telescope ${synscan_SRC}) target_link_libraries(indi_synscan_telescope indidriver AlignmentDriver) install(TARGETS indi_synscan_telescope RUNTIME DESTINATION bin) ########### Sky Commander ############### SET(skycommander_SRC ${CMAKE_CURRENT_SOURCE_DIR}/drivers/telescope/skycommander.cpp) add_executable(indi_skycommander_telescope ${skycommander_SRC}) target_link_libraries(indi_skycommander_telescope indidriver) install(TARGETS indi_skycommander_telescope RUNTIME DESTINATION bin) ########### Generic Digital Setting Circle ############### SET(dsc_SRC ${CMAKE_CURRENT_SOURCE_DIR}/drivers/telescope/dsc.cpp) add_executable(indi_dsc_telescope ${dsc_SRC}) target_link_libraries(indi_dsc_telescope indidriver AlignmentDriver) install(TARGETS indi_dsc_telescope RUNTIME DESTINATION bin) ########### IEQ Pro / CEM60 ############# SET(ieq_SRC ${CMAKE_CURRENT_SOURCE_DIR}/drivers/telescope/ieqprodriver.cpp ${CMAKE_CURRENT_SOURCE_DIR}/drivers/telescope/ieqpro.cpp) IF (UNITY_BUILD) ENABLE_UNITY_BUILD(ieq ieq_SRC 10 cpp) ENDIF () add_executable(indi_ieq_telescope ${ieq_SRC}) target_link_libraries(indi_ieq_telescope indidriver) install(TARGETS indi_ieq_telescope RUNTIME DESTINATION bin) ########### IOptronV3 / CEM120 ############# SET(ioptronv3_SRC ${CMAKE_CURRENT_SOURCE_DIR}/drivers/telescope/ioptronv3driver.cpp ${CMAKE_CURRENT_SOURCE_DIR}/drivers/telescope/ioptronv3.cpp) IF (UNITY_BUILD) ENABLE_UNITY_BUILD(ioptronv3 ioptronv3_SRC 10 cpp) ENDIF () add_executable(indi_ioptronv3_telescope ${ioptronv3_SRC}) target_link_libraries(indi_ioptronv3_telescope indidriver) install(TARGETS indi_ioptronv3_telescope RUNTIME DESTINATION bin) ########### Explore Scientific PMC8 ############# SET(pmc8_SRC ${CMAKE_CURRENT_SOURCE_DIR}/drivers/telescope/pmc8driver.cpp ${CMAKE_CURRENT_SOURCE_DIR}/drivers/telescope/pmc8.cpp) add_executable(indi_pmc8_telescope ${pmc8_SRC}) target_link_libraries(indi_pmc8_telescope indidriver) install(TARGETS indi_pmc8_telescope RUNTIME DESTINATION bin) ########### Telescope Simulator ############## SET(telescopesimulator_SRC ${CMAKE_CURRENT_SOURCE_DIR}/drivers/telescope/telescope_simulator.cpp) add_executable(indi_simulator_telescope ${telescopesimulator_SRC}) target_link_libraries(indi_simulator_telescope indidriver) install(TARGETS indi_simulator_telescope RUNTIME DESTINATION bin) ########### Telescope Scripting Gateway ############## SET(telescopescript_SRC ${CMAKE_CURRENT_SOURCE_DIR}/drivers/telescope/telescope_script.cpp) add_executable(indi_script_telescope ${telescopescript_SRC}) target_link_libraries(indi_script_telescope indidriver) install(TARGETS indi_script_telescope RUNTIME DESTINATION bin) ########### CCD Simulator ############## SET(ccdsimulator_SRC ${CMAKE_CURRENT_SOURCE_DIR}/drivers/ccd/ccd_simulator.cpp) add_executable(indi_simulator_ccd ${ccdsimulator_SRC}) target_link_libraries(indi_simulator_ccd indidriver) install(TARGETS indi_simulator_ccd RUNTIME DESTINATION bin) ########### Guide Simulator ############## SET(guidesimulator_SRC ${CMAKE_CURRENT_SOURCE_DIR}/drivers/ccd/guide_simulator.cpp) add_executable(indi_simulator_guide ${guidesimulator_SRC}) target_link_libraries(indi_simulator_guide indidriver) install(TARGETS indi_simulator_guide RUNTIME DESTINATION bin) ##################################### ########## FOCUSER GROUP ############ ##################################### ################################################################################# ################ Focuser Simulator ################ SET(focussimulator_SRC ${CMAKE_CURRENT_SOURCE_DIR}/drivers/focuser/focus_simulator.cpp) add_executable(indi_simulator_focus ${focussimulator_SRC}) target_link_libraries(indi_simulator_focus indidriver) install(TARGETS indi_simulator_focus RUNTIME DESTINATION bin) ################ Robo Focuser ################ SET(robofocus_SRC ${CMAKE_CURRENT_SOURCE_DIR}/drivers/focuser/robofocus.cpp) add_executable(indi_robo_focus ${robofocus_SRC}) target_link_libraries(indi_robo_focus indidriver) install(TARGETS indi_robo_focus RUNTIME DESTINATION bin) ################ Rigelsys NFocus Focuser ################ SET(nfocus_SRC ${CMAKE_CURRENT_SOURCE_DIR}/drivers/focuser/nfocus.cpp) add_executable(indi_nfocus ${nfocus_SRC}) target_link_libraries(indi_nfocus indidriver) install(TARGETS indi_nfocus RUNTIME DESTINATION bin) ################ Rigelsys NStep Focuser ################ SET(nstep_SRC ${CMAKE_CURRENT_SOURCE_DIR}/drivers/focuser/nstep.cpp) add_executable(indi_nstep_focus ${nstep_SRC}) target_link_libraries(indi_nstep_focus indidriver) install(TARGETS indi_nstep_focus RUNTIME DESTINATION bin) ################ Moonlite Focuser ################ SET(moonlite_SRC ${CMAKE_CURRENT_SOURCE_DIR}/drivers/focuser/moonlite.cpp) add_executable(indi_moonlite_focus ${moonlite_SRC}) target_link_libraries(indi_moonlite_focus indidriver) install(TARGETS indi_moonlite_focus RUNTIME DESTINATION bin) ################ Sesto Senso Focuser ################ SET(sesto_SRC ${CMAKE_CURRENT_SOURCE_DIR}/drivers/focuser/sestosenso.cpp) add_executable(indi_sestosenso_focus ${sesto_SRC}) target_link_libraries(indi_sestosenso_focus indidriver) install(TARGETS indi_sestosenso_focus RUNTIME DESTINATION bin) ################ Televue FocusMaster ################ #SET(focusmaster_SRC # ${CMAKE_CURRENT_SOURCE_DIR}/drivers/focuser/focusmaster.cpp) #add_executable(indi_focusmaster_focus ${focusmaster_SRC}) #target_link_libraries(indi_focusmaster_focus indidriver) #install(TARGETS indi_focusmaster_focus RUNTIME DESTINATION bin) ########### Lakeside ########### set(indilakeside_SRCS ${CMAKE_CURRENT_SOURCE_DIR}/drivers/focuser/lakeside.cpp) add_executable(indi_lakeside_focus ${indilakeside_SRCS}) IF (NOT "${CMAKE_CXX_COMPILER_ID}" STREQUAL "Clang" AND NOT "${CMAKE_CXX_COMPILER_ID}" STREQUAL "AppleClang") set_target_properties(indi_lakeside_focus PROPERTIES COMPILE_FLAGS "-Wno-format-overflow") ENDIF () target_link_libraries(indi_lakeside_focus indidriver) install(TARGETS indi_lakeside_focus RUNTIME DESTINATION bin ) ################ Pegasus DMFC Focuser ################ SET(pegasus_SRC ${CMAKE_CURRENT_SOURCE_DIR}/drivers/focuser/dmfc.cpp) add_executable(indi_dmfc_focus ${pegasus_SRC}) target_link_libraries(indi_dmfc_focus indidriver) install(TARGETS indi_dmfc_focus RUNTIME DESTINATION bin) ################## USB Focus V3 ################## SET(usbfocusv3_SRC ${CMAKE_CURRENT_SOURCE_DIR}/drivers/focuser/usbfocusv3.cpp) add_executable(indi_usbfocusv3_focus ${usbfocusv3_SRC}) target_link_libraries(indi_usbfocusv3_focus indidriver) install(TARGETS indi_usbfocusv3_focus RUNTIME DESTINATION bin) ################ Microtouch Focuser ################ SET(microtouch_SRC ${CMAKE_CURRENT_SOURCE_DIR}/drivers/focuser/microtouch.cpp) add_executable(indi_microtouch_focus ${microtouch_SRC}) target_link_libraries(indi_microtouch_focus indidriver) install(TARGETS indi_microtouch_focus RUNTIME DESTINATION bin) ################ Baader SteelDrive Focuser ################ SET(steeldrive_SRC ${CMAKE_CURRENT_SOURCE_DIR}/drivers/focuser/steeldrive.cpp) add_executable(indi_steeldrive_focus ${steeldrive_SRC}) target_link_libraries(indi_steeldrive_focus indidriver) install(TARGETS indi_steeldrive_focus RUNTIME DESTINATION bin) ################ FocusLynx Focuser ################ SET(focuslynx_SRC ${CMAKE_CURRENT_SOURCE_DIR}/drivers/focuser/focuslynxbase.cpp ${CMAKE_CURRENT_SOURCE_DIR}/drivers/focuser/focuslynx.cpp) IF (UNITY_BUILD) ENABLE_UNITY_BUILD(focuslynx focuslynx_SRC 10 cpp) ENDIF () add_executable(indi_lynx_focus ${focuslynx_SRC}) target_link_libraries(indi_lynx_focus indidriver) install(TARGETS indi_lynx_focus RUNTIME DESTINATION bin) ################ PerfectStar Focuser ################ SET(perfectstar_SRC ${CMAKE_CURRENT_SOURCE_DIR}/drivers/focuser/perfectstar.cpp) add_executable(indi_perfectstar_focus ${perfectstar_SRC}) target_link_libraries(indi_perfectstar_focus indidriver) install(TARGETS indi_perfectstar_focus RUNTIME DESTINATION bin) ################ hitechfocus Focuser ################ SET(hitecastrodcfocuser_SRC ${CMAKE_CURRENT_SOURCE_DIR}/drivers/focuser/hitecastrodcfocuser.cpp) add_executable(indi_hitecastrodc_focus ${hitecastrodcfocuser_SRC}) target_link_libraries(indi_hitecastrodc_focus indidriver) install(TARGETS indi_hitecastrodc_focus RUNTIME DESTINATION bin) ################ JMI Smart Focus Focuser ################ SET(smartfocus_SRC ${CMAKE_CURRENT_SOURCE_DIR}/drivers/focuser/smartfocus.cpp) add_executable(indi_smartfocus_focus ${smartfocus_SRC}) target_link_libraries(indi_smartfocus_focus indidriver) install(TARGETS indi_smartfocus_focus RUNTIME DESTINATION bin) ################ Optec TCF-S ################ SET(tcfs_SRC ${CMAKE_CURRENT_SOURCE_DIR}/drivers/focuser/tcfs.cpp) add_executable(indi_tcfs_focus ${tcfs_SRC}) target_link_libraries(indi_tcfs_focus indidriver) install(TARGETS indi_tcfs_focus RUNTIME DESTINATION bin) file(WRITE ${CMAKE_CURRENT_BINARY_DIR}/make_tcfs_symlink.cmake "exec_program(\"${CMAKE_COMMAND}\" ARGS -E create_symlink indi_tcfs_focus \$ENV{DESTDIR}${BIN_INSTALL_DIR}/indi_tcfs3_focus)\n") set_target_properties(indi_tcfs_focus PROPERTIES POST_INSTALL_SCRIPT ${CMAKE_CURRENT_BINARY_DIR}/make_tcfs_symlink.cmake) #### UDEV Rules File for Focusers #### #### One rule for for all INDI standard focusers #### IF (${CMAKE_SYSTEM_NAME} MATCHES "Linux") install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/drivers/focuser/99-focusers.rules DESTINATION ${UDEVRULES_INSTALL_DIR}) ENDIF () ################################################################################# ##################################### ######## Rotator GROUP ######### ##################################### ################ Optec Gemini Focusing Rotator ######## SET(gemini_SRC ${CMAKE_CURRENT_SOURCE_DIR}/drivers/rotator/gemini.cpp) IF (UNITY_BUILD) ENABLE_UNITY_BUILD(gemini gemini_SRC 10 cpp) ENDIF () add_executable(indi_gemini_focus ${gemini_SRC}) target_link_libraries(indi_gemini_focus indidriver) install(TARGETS indi_gemini_focus RUNTIME DESTINATION bin) ################ NightCrawler Focusing Rotator ################ SET(nightcrawler_SRC ${CMAKE_CURRENT_SOURCE_DIR}/drivers/rotator/nightcrawler.cpp) add_executable(indi_nightcrawler_focus ${nightcrawler_SRC}) target_link_libraries(indi_nightcrawler_focus indidriver) install(TARGETS indi_nightcrawler_focus RUNTIME DESTINATION bin) ################ Pyxis Rotator ################ SET(pyxis_SRC ${CMAKE_CURRENT_SOURCE_DIR}/drivers/rotator/pyxis.cpp) add_executable(indi_pyxis_rotator ${pyxis_SRC}) target_link_libraries(indi_pyxis_rotator indidriver) install(TARGETS indi_pyxis_rotator RUNTIME DESTINATION bin) ################ Integra85 Focusing Rotator ################ SET(integra_SRC ${CMAKE_CURRENT_SOURCE_DIR}/drivers/rotator/integra.cpp) add_executable(indi_integra_focus ${integra_SRC}) target_link_libraries(indi_integra_focus indidriver) install(TARGETS indi_integra_focus RUNTIME DESTINATION bin) ################################################################################# ##################################### ######## FILTER WHEEL GROUP ######### ##################################### ########### XAGYL Wheel ############## SET(xagylwheel_SRC ${CMAKE_CURRENT_SOURCE_DIR}/drivers/filter_wheel/xagyl_wheel.cpp) add_executable(indi_xagyl_wheel ${xagylwheel_SRC}) target_link_libraries(indi_xagyl_wheel indidriver) install(TARGETS indi_xagyl_wheel RUNTIME DESTINATION bin ) ########### Filter Simulator ############## SET(filtersimulator_SRC ${CMAKE_CURRENT_SOURCE_DIR}/drivers/filter_wheel/filter_simulator.cpp) add_executable(indi_simulator_wheel ${filtersimulator_SRC}) target_link_libraries(indi_simulator_wheel indidriver) install(TARGETS indi_simulator_wheel RUNTIME DESTINATION bin) ########## Optec Wheel IFW ############ SET(optecwheel_SRC ${CMAKE_CURRENT_SOURCE_DIR}/drivers/filter_wheel/ifwoptec.cpp) add_executable(indi_optec_wheel ${optecwheel_SRC}) target_link_libraries(indi_optec_wheel indidriver) install(TARGETS indi_optec_wheel RUNTIME DESTINATION bin) ########## Quantum Wheel ############ SET(quantum_wheel_SRC ${CMAKE_CURRENT_SOURCE_DIR}/drivers/filter_wheel/quantum_wheel.cpp) add_executable(indi_quantum_wheel ${quantum_wheel_SRC}) target_link_libraries(indi_quantum_wheel indidriver) install(TARGETS indi_quantum_wheel RUNTIME DESTINATION bin) ########## TruTech Wheel ############ SET(trutechwheel_SRC ${CMAKE_CURRENT_SOURCE_DIR}/drivers/filter_wheel/trutech_wheel.cpp) add_executable(indi_trutech_wheel ${trutechwheel_SRC}) target_link_libraries(indi_trutech_wheel indidriver) install(TARGETS indi_trutech_wheel RUNTIME DESTINATION bin) ################################################################################# ##################################### ########## DOME GROUP ############ ##################################### ################ Dome Simulator ################ SET(domesimulator_SRC ${CMAKE_CURRENT_SOURCE_DIR}/drivers/dome/dome_simulator.cpp) add_executable(indi_simulator_dome ${domesimulator_SRC}) target_link_libraries(indi_simulator_dome indidriver) install(TARGETS indi_simulator_dome RUNTIME DESTINATION bin) ################ Roll Off ################ SET(rolloff_SRC ${CMAKE_CURRENT_SOURCE_DIR}/drivers/dome/roll_off.cpp) add_executable(indi_rolloff_dome ${rolloff_SRC}) target_link_libraries(indi_rolloff_dome indidriver) install(TARGETS indi_rolloff_dome RUNTIME DESTINATION bin) ################ Baader Dome ################ SET(baaderdome_SRC ${CMAKE_CURRENT_SOURCE_DIR}/drivers/dome/baader_dome.cpp) add_executable(indi_baader_dome ${baaderdome_SRC}) target_link_libraries(indi_baader_dome indidriver) install(TARGETS indi_baader_dome RUNTIME DESTINATION bin) ########### Dome Scripting Gateway ############## SET(domescript_SRC ${CMAKE_CURRENT_SOURCE_DIR}/drivers/dome/dome_script.cpp) add_executable(indi_script_dome ${domescript_SRC}) target_link_libraries(indi_script_dome indidriver) install(TARGETS indi_script_dome RUNTIME DESTINATION bin) ################################################################################# ######################################### ########### VIDEO GROUP ############### ######################################### ########### INDI::CCD V4L Driver ############### IF (${CMAKE_SYSTEM_NAME} MATCHES "Linux") SET(v4l2driverccd_SRC ${CMAKE_CURRENT_SOURCE_DIR}/libs/lx/Lx.cpp ${CMAKE_CURRENT_SOURCE_DIR}/drivers/video/v4l2driver.cpp ${CMAKE_CURRENT_SOURCE_DIR}/drivers/video/indi_v4l2driver.cpp) IF (UNITY_BUILD) ENABLE_UNITY_BUILD(v4l2driverccd v4l2driverccd_SRC 10 cpp) ENDIF () add_executable(indi_v4l2_ccd ${v4l2driverccd_SRC} ${libwebcam_C_SRC} ${libwebcam_CXX_SRC}) target_link_libraries(indi_v4l2_ccd ${JPEG_LIBRARY} indidriver) install(TARGETS indi_v4l2_ccd RUNTIME DESTINATION bin ) ENDIF () ################################################################################# ##################################### ############ AUX GROUP ############## ##################################### ########### SkySafari Middleware ############## IF (INDI_BUILD_CLIENT) SET(skysafari_SRC ${CMAKE_CURRENT_SOURCE_DIR}/drivers/auxiliary/skysafari.cpp ${CMAKE_CURRENT_SOURCE_DIR}/drivers/auxiliary/skysafariclient.cpp) IF (UNITY_BUILD) ENABLE_UNITY_BUILD(skysafari skysafari_SRC 10 cpp) ENDIF () add_executable(indi_skysafari ${skysafari_SRC}) target_link_libraries(indi_skysafari indidriver indiclient) install(TARGETS indi_skysafari RUNTIME DESTINATION bin) ELSE () MESSAGE(WARNING "Skipping build of INDI SkySafari driver since INDI POSIX Client is not built") ENDIF () ########### Watch dog ############### if (INDI_BUILD_CLIENT) SET(watchdog_SRC ${CMAKE_CURRENT_SOURCE_DIR}/drivers/auxiliary/watchdog.cpp ${CMAKE_CURRENT_SOURCE_DIR}/drivers/auxiliary/watchdogclient.cpp) IF (UNITY_BUILD) ENABLE_UNITY_BUILD(watchdog watchdog_SRC 10 cpp) ENDIF () add_executable(indi_watchdog ${watchdog_SRC}) target_link_libraries(indi_watchdog indidriver indiclient) install(TARGETS indi_watchdog RUNTIME DESTINATION bin) ELSE () MESSAGE(WARNING "Skipping build of INDI WatchDog driver since INDI POSIX Client is not built") ENDIF () ########### Flip Flat & Flip Man Driver ############### SET(flipflat_SRC ${CMAKE_CURRENT_SOURCE_DIR}/drivers/auxiliary/flip_flat.cpp) add_executable(indi_flipflat ${flipflat_SRC}) target_link_libraries(indi_flipflat indidriver) install(TARGETS indi_flipflat RUNTIME DESTINATION bin) IF (${CMAKE_SYSTEM_NAME} MATCHES "Linux") INSTALL(FILES ${CMAKE_CURRENT_SOURCE_DIR}/drivers/auxiliary/99-flipflat.rules DESTINATION ${UDEVRULES_INSTALL_DIR}) ENDIF () ########### SnapCap Driver ############### SET(snapcap_SRC ${CMAKE_CURRENT_SOURCE_DIR}/drivers/auxiliary/snapcap.cpp) add_executable(indi_snapcap ${snapcap_SRC}) target_link_libraries(indi_snapcap indidriver) install(TARGETS indi_snapcap RUNTIME DESTINATION bin) ########### Sky Quality Meter ############### SET(sqm_SRC ${CMAKE_CURRENT_SOURCE_DIR}/drivers/auxiliary/sqm.cpp) add_executable(indi_sqm_weather ${sqm_SRC}) target_link_libraries(indi_sqm_weather indidriver) install(TARGETS indi_sqm_weather RUNTIME DESTINATION bin) ########### Sky Quality Meter Simulator ############### SET(sqm_simulator_SRC ${CMAKE_CURRENT_SOURCE_DIR}/drivers/auxiliary/sqm_simulator.cpp) add_executable(indi_simulator_sqm ${sqm_simulator_SRC}) target_link_libraries(indi_simulator_sqm indidriver) install(TARGETS indi_simulator_sqm RUNTIME DESTINATION bin) ########### Astrometry Driver ############### SET(astrometry_SRC ${CMAKE_CURRENT_SOURCE_DIR}/drivers/auxiliary/astrometrydriver.cpp) add_executable(indi_astrometry ${astrometry_SRC}) target_link_libraries(indi_astrometry indidriver) install(TARGETS indi_astrometry RUNTIME DESTINATION bin) ########### STAR2000 Driver ############### SET(STAR2000_SRC ${CMAKE_CURRENT_SOURCE_DIR}/drivers/auxiliary/STAR2kdriver.c ${CMAKE_CURRENT_SOURCE_DIR}/drivers/auxiliary/STAR2000.cpp) add_executable(indi_star2000 ${STAR2000_SRC}) target_link_libraries(indi_star2000 indidriver) install(TARGETS indi_star2000 RUNTIME DESTINATION bin) ########### GPUSB Driver ############### SET(gpusb_SRC ${CMAKE_CURRENT_SOURCE_DIR}/drivers/auxiliary/gpdriver.cpp ${CMAKE_CURRENT_SOURCE_DIR}/drivers/auxiliary/gpusb.cpp) IF (UNITY_BUILD) ENABLE_UNITY_BUILD(gpusb gpusb_SRC 10 cpp) ENDIF () add_executable(indi_gpusb ${gpusb_SRC}) target_link_libraries(indi_gpusb indidriver) install(TARGETS indi_gpusb RUNTIME DESTINATION bin) IF (${CMAKE_SYSTEM_NAME} MATCHES "Linux") INSTALL(FILES ${CMAKE_CURRENT_SOURCE_DIR}/drivers/auxiliary/99-gpusb.rules DESTINATION ${UDEVRULES_INSTALL_DIR}) ENDIF () ########### Joystick Driver ############### IF (${CMAKE_SYSTEM_NAME} MATCHES "Linux") SET(joystick_SRC ${CMAKE_CURRENT_SOURCE_DIR}/drivers/auxiliary/joystickdriver.cpp ${CMAKE_CURRENT_SOURCE_DIR}/drivers/auxiliary/joystick.cpp) IF (UNITY_BUILD) ENABLE_UNITY_BUILD(joystick joystick_SRC 10 cpp) ENDIF () add_executable(indi_joystick ${joystick_SRC}) target_link_libraries(indi_joystick indidriver) install(TARGETS indi_joystick RUNTIME DESTINATION bin) ENDIF () ########### GPS Simulator Driver ############### SET(gpssimulator_SRC ${CMAKE_CURRENT_SOURCE_DIR}/drivers/auxiliary/gps_simulator.cpp) add_executable(indi_simulator_gps ${gpssimulator_SRC}) target_link_libraries(indi_simulator_gps indidriver) install(TARGETS indi_simulator_gps RUNTIME DESTINATION bin) ########### USB_Dewpoint Driver ############### SET(usb_dewpoint_SRC ${CMAKE_CURRENT_SOURCE_DIR}/drivers/auxiliary/usb_dewpoint.cpp) add_executable(indi_usbdewpoint ${usb_dewpoint_SRC}) target_link_libraries(indi_usbdewpoint indidriver) install(TARGETS indi_usbdewpoint RUNTIME DESTINATION bin) ##################################### ########## WEATHER GROUP ############ ##################################### ########### Weather Meta Driver ############### SET(weathermeta_SRC ${CMAKE_CURRENT_SOURCE_DIR}/drivers/weather/weathermeta.cpp) add_executable(indi_meta_weather ${weathermeta_SRC}) target_link_libraries(indi_meta_weather indidriver) install(TARGETS indi_meta_weather RUNTIME DESTINATION bin ) ########### MBox Driver ############### SET(mbox_SRC ${CMAKE_CURRENT_SOURCE_DIR}/drivers/weather/mbox.cpp) add_executable(indi_mbox_weather ${mbox_SRC}) target_link_libraries(indi_mbox_weather indidriver) install(TARGETS indi_mbox_weather RUNTIME DESTINATION bin) ########### Vantage Driver ############### SET(vantage_SRC ${CMAKE_CURRENT_SOURCE_DIR}/drivers/weather/vantage.cpp) add_executable(indi_vantage_weather ${vantage_SRC}) target_link_libraries(indi_vantage_weather indidriver) install(TARGETS indi_vantage_weather RUNTIME DESTINATION bin) IF (${CMAKE_SYSTEM_NAME} MATCHES "Linux") INSTALL(FILES ${CMAKE_CURRENT_SOURCE_DIR}/drivers/weather/99-vantage.rules DESTINATION ${UDEVRULES_INSTALL_DIR}) ENDIF () ########### WunderGround Driver ############### SET(WunderGround_SRC ${CMAKE_CURRENT_SOURCE_DIR}/drivers/weather/gason.cpp ${CMAKE_CURRENT_SOURCE_DIR}/drivers/weather/wunderground.cpp) IF (UNITY_BUILD) ENABLE_UNITY_BUILD(WunderGround WunderGround_SRC 10 cpp) ENDIF () add_executable(indi_wunderground_weather ${WunderGround_SRC}) target_link_libraries(indi_wunderground_weather indidriver ${CURL_LIBRARIES}) install(TARGETS indi_wunderground_weather RUNTIME DESTINATION bin) ##################################### ############ INDI TOOLS ############# ##################################### ########### getINDI ############## SET(indi_get_SRC ${CMAKE_CURRENT_SOURCE_DIR}/eventloop.c ${CMAKE_CURRENT_SOURCE_DIR}/base64.c ${CMAKE_CURRENT_SOURCE_DIR}/tools/getINDIproperty.c ${CMAKE_CURRENT_SOURCE_DIR}/libs/lilxml.c) IF (UNITY_BUILD) ENABLE_UNITY_BUILD(indi_get indi_get_SRC 10 c) ENDIF () SET(indi_get_SRC ${indi_get_SRC} ${CMAKE_CURRENT_SOURCE_DIR}/libs/indicom.c) add_executable(indi_getprop ${indi_get_SRC}) target_link_libraries(indi_getprop ${NOVA_LIBRARIES} ${M_LIB} ${ZLIB_LIBRARY}) install(TARGETS indi_getprop RUNTIME DESTINATION bin ) ################################################################################# ########### setINDI ############## SET(indi_set_SRC ${CMAKE_CURRENT_SOURCE_DIR}/eventloop.c ${CMAKE_CURRENT_SOURCE_DIR}/base64.c ${CMAKE_CURRENT_SOURCE_DIR}/tools/setINDIproperty.c ${CMAKE_CURRENT_SOURCE_DIR}/libs/lilxml.c) IF (UNITY_BUILD) ENABLE_UNITY_BUILD(indi_set indi_set_SRC 10 c) ENDIF () SET(indi_set_SRC ${indi_set_SRC} ${CMAKE_CURRENT_SOURCE_DIR}/libs/indicom.c) add_executable(indi_setprop ${indi_set_SRC}) target_link_libraries(indi_setprop ${NOVA_LIBRARIES} ${M_LIB} ${ZLIB_LIBRARY}) install(TARGETS indi_setprop RUNTIME DESTINATION bin ) ################################################################################# ########### evalINDI ############## SET(indi_eval_SRC ${CMAKE_CURRENT_SOURCE_DIR}/eventloop.c ${CMAKE_CURRENT_SOURCE_DIR}/base64.c ${CMAKE_CURRENT_SOURCE_DIR}/tools/compiler.c ${CMAKE_CURRENT_SOURCE_DIR}/tools/evalINDI.c ${CMAKE_CURRENT_SOURCE_DIR}/libs/lilxml.c) IF (UNITY_BUILD) ENABLE_UNITY_BUILD(indi_eval indi_eval_SRC 10 c) ENDIF () SET(indi_eval_SRC ${indi_eval_SRC} ${CMAKE_CURRENT_SOURCE_DIR}/libs/indicom.c) add_executable(indi_eval ${indi_eval_SRC}) target_link_libraries(indi_eval ${NOVA_LIBRARIES} ${M_LIB} ${ZLIB_LIBRARY}) install(TARGETS indi_eval RUNTIME DESTINATION bin ) ########### HID Test ############## SET(indi_hid_SRC ${CMAKE_CURRENT_SOURCE_DIR}/libs/indibase/hidtest.cpp ) IF (UNITY_BUILD) ENABLE_UNITY_BUILD(indi_hid indi_hid_SRC 10 cpp) ENDIF () if(APPLE) set(LIBS "-framework IOKit -framework CoreFoundation") elseif(CYGWIN) set(LIBS ${ICONV_LIBRARIES}) endif(APPLE) SET(indi_hid_SRC ${indi_hid_SRC} ${hidapi_SRCS}) add_executable(indi_hid_test ${indi_hid_SRC}) target_link_libraries(indi_hid_test ${USB1_LIBRARIES} ${CMAKE_THREAD_LIBS_INIT} ${LIBS}) install(TARGETS indi_hid_test RUNTIME DESTINATION bin ) ################################################################################# ## Build Examples IF (INDI_BUILD_CLIENT) add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/examples) ELSE () message(WARNING "Skipping build of examples since INDI POSIX client is not built") ENDIF () ################################################################################# install( FILES drivers.xml ${CMAKE_CURRENT_SOURCE_DIR}/drivers/focuser/indi_tcfs_sk.xml DESTINATION ${DATA_INSTALL_DIR}) configure_file(${CMAKE_CURRENT_SOURCE_DIR}/libindi.pc.cmake ${CMAKE_CURRENT_BINARY_DIR}/libindi.pc @ONLY) install(FILES ${CMAKE_CURRENT_BINARY_DIR}/libindi.pc DESTINATION ${PKGCONFIG_INSTALL_PREFIX}) if (UNIX) INSTALL(FILES ${CMAKE_CURRENT_SOURCE_DIR}/libs/stream/streammanager.h ${CMAKE_CURRENT_SOURCE_DIR}/libs/stream/jpegutils.h DESTINATION ${INCLUDE_INSTALL_DIR}/libindi/stream COMPONENT Devel) INSTALL(FILES ${CMAKE_CURRENT_SOURCE_DIR}/libs/stream/encoder/encodermanager.h ${CMAKE_CURRENT_SOURCE_DIR}/libs/stream/encoder/encoderinterface.h ${CMAKE_CURRENT_SOURCE_DIR}/libs/stream/encoder/rawencoder.h ${CMAKE_CURRENT_SOURCE_DIR}/libs/stream/encoder/mjpegencoder.h DESTINATION ${INCLUDE_INSTALL_DIR}/libindi/stream/encoder COMPONENT Devel) INSTALL(FILES ${CMAKE_CURRENT_SOURCE_DIR}/libs/stream/recorder/recordermanager.h ${CMAKE_CURRENT_SOURCE_DIR}/libs/stream/recorder/recorderinterface.h ${CMAKE_CURRENT_SOURCE_DIR}/libs/stream/recorder/serrecorder.h DESTINATION ${INCLUDE_INSTALL_DIR}/libindi/stream/recorder COMPONENT Devel) if (${CMAKE_SYSTEM_NAME} MATCHES "Linux") INSTALL(FILES ${CMAKE_CURRENT_SOURCE_DIR}/libs/webcam/ccvt.h ${CMAKE_CURRENT_SOURCE_DIR}/libs/webcam/ccvt_types.h ${CMAKE_CURRENT_SOURCE_DIR}/libs/webcam/v4l2_decode/v4l2_decode.h ${CMAKE_CURRENT_SOURCE_DIR}/libs/webcam/v4l2_decode/v4l2_builtin_decoder.h ${CMAKE_CURRENT_SOURCE_DIR}/libs/webcam/ccvt_types.h ${CMAKE_CURRENT_SOURCE_DIR}/libs/webcam/v4l2_colorspace.h DESTINATION ${INCLUDE_INSTALL_DIR}/libindi COMPONENT Devel) ENDIF() ENDIF () ################################################################################################### ######################################### Tests ################################################# ################################################################################################### find_package (GTest) find_package (GMock) IF (GTEST_FOUND) IF (INDI_BUILD_UNITTESTS) MESSAGE (STATUS "Building unit tests") ADD_SUBDIRECTORY(test) ELSE (INDI_BUILD_UNITTESTS) MESSAGE (STATUS "Not building unit tests") ENDIF (INDI_BUILD_UNITTESTS) ELSE() MESSAGE (STATUS "GTEST not found, not building unit tests") ENDIF (GTEST_FOUND) endif (WIN32 OR ANDROID) endif(INDI_BUILD_DRIVERS) ################################################################################################### ####################################### config.h ################################################ ################################################################################################### # Generate config.h from template configure_file(${CMAKE_CURRENT_SOURCE_DIR}/config.h.cmake ${CMAKE_CURRENT_BINARY_DIR}/config.h ) configure_file(${CMAKE_CURRENT_SOURCE_DIR}/indiversion.h.cmake ${CMAKE_CURRENT_BINARY_DIR}/indiversion.h ) if (MSVC) add_definitions(-D_CRT_SECURE_NO_WARNINGS) add_definitions(-D_WINSOCK_DEPRECATED_NO_WARNINGS) endif() # Install common dev files for all except server if (INDI_BUILD_DRIVERS OR INDI_BUILD_CLIENT OR INDI_BUILD_QT5_CLIENT) install(FILES ${CMAKE_CURRENT_BINARY_DIR}/indiversion.h DESTINATION ${INCLUDE_INSTALL_DIR}/libindi COMPONENT Devel) install( FILES ${CMAKE_CURRENT_SOURCE_DIR}/indiapi.h ${CMAKE_CURRENT_SOURCE_DIR}/indidevapi.h ${CMAKE_CURRENT_SOURCE_DIR}/base64.h ${CMAKE_CURRENT_SOURCE_DIR}/libs/lilxml.h ${CMAKE_CURRENT_SOURCE_DIR}/libs/indicom.h ${CMAKE_CURRENT_SOURCE_DIR}/eventloop.h ${CMAKE_CURRENT_SOURCE_DIR}/indidriver.h ${CMAKE_CURRENT_SOURCE_DIR}/libs/indibase/indibase.h ${CMAKE_CURRENT_SOURCE_DIR}/libs/indibase/indibasetypes.h ${CMAKE_CURRENT_SOURCE_DIR}/libs/indibase/basedevice.h ${CMAKE_CURRENT_SOURCE_DIR}/libs/indibase/defaultdevice.h ${CMAKE_CURRENT_SOURCE_DIR}/libs/indibase/indiccd.h ${CMAKE_CURRENT_SOURCE_DIR}/libs/indibase/indidetector.h ${CMAKE_CURRENT_SOURCE_DIR}/libs/indibase/indifilterwheel.h ${CMAKE_CURRENT_SOURCE_DIR}/libs/indibase/indifocuserinterface.h ${CMAKE_CURRENT_SOURCE_DIR}/libs/indibase/indifocuser.h ${CMAKE_CURRENT_SOURCE_DIR}/libs/indibase/indirotator.h ${CMAKE_CURRENT_SOURCE_DIR}/libs/indibase/inditelescope.h ${CMAKE_CURRENT_SOURCE_DIR}/libs/indibase/indiguiderinterface.h ${CMAKE_CURRENT_SOURCE_DIR}/libs/indibase/indifilterinterface.h ${CMAKE_CURRENT_SOURCE_DIR}/libs/indibase/indirotatorinterface.h ${CMAKE_CURRENT_SOURCE_DIR}/libs/indibase/indiproperty.h ${CMAKE_CURRENT_SOURCE_DIR}/libs/indibase/indistandardproperty.h ${CMAKE_CURRENT_SOURCE_DIR}/libs/indibase/indidome.h ${CMAKE_CURRENT_SOURCE_DIR}/libs/indibase/indigps.h ${CMAKE_CURRENT_SOURCE_DIR}/libs/indibase/indilightboxinterface.h ${CMAKE_CURRENT_SOURCE_DIR}/libs/indibase/indidustcapinterface.h ${CMAKE_CURRENT_SOURCE_DIR}/libs/indibase/indiweather.h ${CMAKE_CURRENT_SOURCE_DIR}/libs/indibase/indilogger.h ${CMAKE_CURRENT_SOURCE_DIR}/libs/indibase/indicontroller.h ${CMAKE_CURRENT_SOURCE_DIR}/libs/indibase/indiusbdevice.h ${CMAKE_CURRENT_SOURCE_DIR}/libs/indibase/hidapi.h DESTINATION ${INCLUDE_INSTALL_DIR}/libindi COMPONENT Devel) install( FILES ${CMAKE_CURRENT_SOURCE_DIR}/libs/indibase/connectionplugins/connectioninterface.h ${CMAKE_CURRENT_SOURCE_DIR}/libs/indibase/connectionplugins/connectionserial.h ${CMAKE_CURRENT_SOURCE_DIR}/libs/indibase/connectionplugins/connectiontcp.h DESTINATION ${INCLUDE_INSTALL_DIR}/libindi/connectionplugins COMPONENT Devel) endif (INDI_BUILD_DRIVERS OR INDI_BUILD_CLIENT OR INDI_BUILD_QT5_CLIENT) feature_summary(WHAT ALL FATAL_ON_MISSING_REQUIRED_PACKAGES) message(STATUS "The following components are going to be built:") if (INDI_BUILD_SERVER) message(STATUS "## INDI Server") endif() if (INDI_BUILD_DRIVERS) message(STATUS "## INDI Drivers, Tools, and Examples") endif() if (INDI_BUILD_CLIENT) message(STATUS "## INDI Client") endif() if (INDI_BUILD_QT5_CLIENT) message(STATUS "## INDI Qt5 Client") endif() libindi/indidevapi.h0000664000175000017500000011216313263645557013763 0ustar jasemjasem#if 0 INDI Copyright (C) 2003 - 2006 Elwood C. Downey Modified by Jasem Mutlaq (2003 - 2015) 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 #pragma once /** \file indidevapi.h \brief Interface to the reference INDI C API device implementation on the Device Driver side. * \author Elwood C. Downey \author Jasem Mutlaq This file is divided into two main sections:\n
  1. Functions the INDI device driver framework defines which the Driver may call:
    • IDxxx functions to send messages to an INDI client.
    • IExxx functions to implement the event driven model.
    • IUxxx functions to perform handy utility functions.
  2. Functions the INDI device driver framework calls which the Driver must define:
    • ISxxx to respond to messages from a Client.

These functions are the interface to the INDI C-language Device Driver reference implementation library. Any driver that uses this interface is expected to \#include "indidevapi.h" and to link with indidrivermain.o and eventloop.o. Indidevapi.h further includes indiapi.h. The former contains the prototypes for the functions documented here, although many functions take arguments defined in the latter.

These functions make it much easier to write a compliant INDI driver than starting from scratch, and also serve as a concrete example of the interactions an INDI driver, in any language, is expected to accommodate.

The reference driver framework and the optimizations made within the reference indiserver both assume and require that one driver program implements exactly one logical INDI device.

The functions in this framework fall into two broad categories. Some are functions that a driver must define because they are called by the reference framework; these functions begin with IS. The remaining functions are library utilities a driver may use to do important operations.

A major point to realize is that an INDI driver built with this framework does not contain the C main() function. As soon as a driver begins executing, it listens on stdin for INDI messages. Only when a valid and appropriate message is received will it then call the driver via one of the IS functions. The driver is then expected to respond promptly by calling one of the ID library functions. It may also use any of the IU utility functions as desired to make processing a message easier.

Rather separate from these IS, ID and IU functions are a collection of functions that utilize the notion of a callback. In a callback design, the driver registers a function with the framework to be called under certain circumstances. When said circumstances occur, the framework will call the callback function. The driver never calls these callbacks directly. These callback functions begin with IE. They can arrange for a callback function to be called under three kinds of circumstances: when a given file descriptor may be read without blocking (because either data is available or EOF has been encountered), when a given time interval has elapsed, or when the framework has nothing urgent to do. The callback functions for each circumstance must be written according to a well defined prototype since, after all, the framework must know how to call the callback correctlty.

*/ /******************************************************************************* * get the data structures */ #include "indiapi.h" #include "lilxml.h" /******************************************************************************* ******************************************************************************* * * Functions the INDI device driver framework defines which the Driver may call * ******************************************************************************* ******************************************************************************* */ #ifdef __cplusplus extern "C" { #endif /** * \defgroup d2cFunctions IDDef Functions: Functions drivers call to define their properties to clients.

Each of the following functions creates the appropriate XML formatted INDI message from its arguments and writes it to stdout. From there, is it typically read by indiserver which then sends it to the clients that have expressed interest in messages from the Device indicated in the message.

In addition to type-specific arguments, all end with a printf-style format string, and appropriate subsequent arguments, that form the \param msg attribute within the INDI message. If the format argument is NULL, no message attribute is included with the message. Note that a \e timestamp attribute is also always added automatically based on the clock on the computer on which this driver is running.

*/ /*@{*/ /** \brief Tell client to create a text vector property. \param t pointer to the vector text property to be defined. \param msg message in printf style to send to the client. May be NULL. */ extern void IDDefText(const ITextVectorProperty *t, const char *msg, ...) #ifdef __GNUC__ __attribute__((format(printf, 2, 3))) #endif ; /** \brief Tell client to create a number number property. \param n pointer to the vector number property to be defined. \param msg message in printf style to send to the client. May be NULL. */ extern void IDDefNumber(const INumberVectorProperty *n, const char *msg, ...) #ifdef __GNUC__ __attribute__((format(printf, 2, 3))) #endif ; /** \brief Tell client to create a switch vector property. \param s pointer to the vector switch property to be defined. \param msg message in printf style to send to the client. May be NULL. */ extern void IDDefSwitch(const ISwitchVectorProperty *s, const char *msg, ...) #ifdef __GNUC__ __attribute__((format(printf, 2, 3))) #endif ; /** \brief Tell client to create a light vector property. \param l pointer to the vector light property to be defined. \param msg message in printf style to send to the client. May be NULL. */ extern void IDDefLight(const ILightVectorProperty *l, const char *msg, ...) #ifdef __GNUC__ __attribute__((format(printf, 2, 3))) #endif ; /** \brief Tell client to create a BLOB vector property. \param b pointer to the vector BLOB property to be defined. \param msg message in printf style to send to the client. May be NULL. */ extern void IDDefBLOB(const IBLOBVectorProperty *b, const char *msg, ...) #ifdef __GNUC__ __attribute__((format(printf, 2, 3))) #endif ; /*@}*/ /** * \defgroup d2cuFunctions IDSet Functions: Functions drivers call to tell clients of new values for existing properties. */ /*@{*/ /** \brief Tell client to update an existing text vector property. \param t pointer to the vector text property. \param msg message in printf style to send to the client. May be NULL. */ extern void IDSetText(const ITextVectorProperty *t, const char *msg, ...) #ifdef __GNUC__ __attribute__((format(printf, 2, 3))) #endif ; /** \brief Tell client to update an existing number vector property. \param n pointer to the vector number property. \param msg message in printf style to send to the client. May be NULL. */ extern void IDSetNumber(const INumberVectorProperty *n, const char *msg, ...) #ifdef __GNUC__ __attribute__((format(printf, 2, 3))) #endif ; /** \brief Tell client to update an existing switch vector property. \param s pointer to the vector switch property. \param msg message in printf style to send to the client. May be NULL. */ extern void IDSetSwitch(const ISwitchVectorProperty *s, const char *msg, ...) #ifdef __GNUC__ __attribute__((format(printf, 2, 3))) #endif ; /** \brief Tell client to update an existing light vector property. \param l pointer to the vector light property. \param msg message in printf style to send to the client. May be NULL. */ extern void IDSetLight(const ILightVectorProperty *l, const char *msg, ...) #ifdef __GNUC__ __attribute__((format(printf, 2, 3))) #endif ; /** \brief Tell client to update an existing BLOB vector property. \param b pointer to the vector BLOB property. \param msg message in printf style to send to the client. May be NULL. */ extern void IDSetBLOB(const IBLOBVectorProperty *b, const char *msg, ...) #ifdef __GNUC__ __attribute__((format(printf, 2, 3))) #endif ; /*@}*/ /** * \defgroup d2duFunctions ID Functions: Functions to delete properties, and log messages locally or remotely. */ /*@{*/ /** \brief Function Drivers call to send log messages to Clients. If dev is specified the Client shall associate the message with that device; if dev is NULL the Client shall treat the message as generic from no specific Device. \param dev device name \param msg message in printf style to send to the client. */ extern void IDMessage(const char *dev, const char *msg, ...) #ifdef __GNUC__ __attribute__((format(printf, 2, 3))) #endif ; /** \brief Function Drivers call to inform Clients a Property is no longer available, or the entire device is gone if name is NULL. \param dev device name. If device name is NULL, the entire device will be deleted. \param name property name to be deleted. \param msg message in printf style to send to the client. */ extern void IDDelete(const char *dev, const char *name, const char *msg, ...) #ifdef __GNUC__ __attribute__((format(printf, 3, 4))) #endif ; /** \brief Function Drivers call to log a message locally. The message is not sent to any Clients. \param msg message in printf style to send to the client. */ extern void IDLog(const char *msg, ...) #ifdef __GNUC__ __attribute__((format(printf, 1, 2))) #endif ; /*@}*/ /** * \defgroup snoopFunctions ISnoop Functions: Functions drivers call to snoop on other drivers. */ /*@{*/ /** \typedef BLOBHandling \brief How drivers handle BLOBs incoming from snooping drivers */ typedef enum { B_NEVER = 0, /*!< Never receive BLOBs */ B_ALSO, /*!< Receive BLOBs along with normal messages */ B_ONLY /*!< ONLY receive BLOBs from drivers, ignore all other traffic */ } BLOBHandling; /** \brief Function a Driver calls to snoop on another Device. Snooped messages will then arrive via ISSnoopDevice. \param snooped_device name of the device to snoop. \param snooped_property name of the snooped property in the device. */ extern void IDSnoopDevice(const char *snooped_device, const char *snooped_property); /** \brief Function a Driver calls to control whether they will receive BLOBs from snooped devices. \param snooped_device name of the device to snoop. \param snooped_property name of property to snoop. If NULL, then all BLOBs from the given device are snooped. \param bh How drivers handle BLOBs incoming from snooping drivers. */ extern void IDSnoopBLOBs(const char *snooped_device, const char *snooped_property, BLOBHandling bh); /*@}*/ /** * \defgroup deventFunctions IE Functions: Functions drivers call to register with the INDI event utilities. Callbacks are called when a read on a file descriptor will not block. Timers are called once after a specified interval. Workprocs are called when there is nothing else to do. The "Add" functions return a unique id for use with their corresponding "Rm" removal function. An arbitrary pointer may be specified when a function is registered which will be stored and forwarded unchanged when the function is later invoked. */ /*@{*/ /* signature of a callback, timout caller and work procedure function */ /** \typedef IE_CBF \brief Signature of a callback. */ typedef void(IE_CBF)(int readfiledes, void *userpointer); /** \typedef IE_TCF \brief Signature of a timeout caller. */ typedef void(IE_TCF)(void *userpointer); /** \typedef IE_WPF \brief Signature of a work procedure function. */ typedef void(IE_WPF)(void *userpointer); /* functions to add and remove callbacks, timers and work procedures */ /** \brief Register a new callback, \e fp, to be called with \e userpointer as argument when \e readfiledes is ready. * * \param readfiledes file descriptor. * \param fp a pointer to the callback function. * \param userpointer a pointer to be passed to the callback function when called. * \return a unique callback id for use with IERmCallback(). */ extern int IEAddCallback(int readfiledes, IE_CBF *fp, void *userpointer); /** \brief Remove a callback function. * * \param callbackid the callback ID returned from IEAddCallback() */ extern void IERmCallback(int callbackid); /** \brief Register a new timer function, \e fp, to be called with \e ud as argument after \e ms. Add to list in order of decreasing time from epoch, ie, last entry runs soonest. The timer will only invoke the callback function \b once. You need to call addTimer again if you want to repeat the process. * * \param millisecs timer period in milliseconds. * \param fp a pointer to the callback function. * \param userpointer a pointer to be passed to the callback function when called. * \return a unique id for use with IERmTimer(). */ extern int IEAddTimer(int millisecs, IE_TCF *fp, void *userpointer); /** \brief Remove the timer with the given \e timerid, as returned from IEAddTimer. * * \param timerid the timer callback ID returned from IEAddTimer(). */ extern void IERmTimer(int timerid); /** \brief Add a new work procedure, fp, to be called with ud when nothing else to do. * * \param fp a pointer to the work procedure callback function. * \param userpointer a pointer to be passed to the callback function when called. * \return a unique id for use with IERmWorkProc(). */ extern int IEAddWorkProc(IE_WPF *fp, void *userpointer); /** \brief Remove a work procedure. * * \param workprocid The unique ID for the work procedure to be removed. */ extern void IERmWorkProc(int workprocid); /* wait in-line for a flag to set, presumably by another event function */ extern int IEDeferLoop(int maxms, int *flagp); extern int IEDeferLoop0(int maxms, int *flagp); /*@}*/ /** * \defgroup dutilFunctions IU Functions: Functions drivers call to perform handy utility routines.

This section describes handy utility functions that are provided by the framework for tasks commonly required in the processing of client messages. It is not strictly necessary to use these functions, but it both prudent and efficient to do so.

These do not communicate with the Client in any way.

*/ /*@{*/ /** \brief Find an IText member in a vector text property. * * \param tvp a pointer to a text vector property. * \param name the name of the member to search for. * \return a pointer to an IText member on match, or NULL if nothing is found. */ extern IText *IUFindText(const ITextVectorProperty *tvp, const char *name); /** \brief Find an INumber member in a number text property. * * \param nvp a pointer to a number vector property. * \param name the name of the member to search for. * \return a pointer to an INumber member on match, or NULL if nothing is found. */ extern INumber *IUFindNumber(const INumberVectorProperty *nvp, const char *name); /** \brief Find an ISwitch member in a vector switch property. * * \param svp a pointer to a switch vector property. * \param name the name of the member to search for. * \return a pointer to an ISwitch member on match, or NULL if nothing is found. */ extern ISwitch *IUFindSwitch(const ISwitchVectorProperty *svp, const char *name); /** \brief Find an ILight member in a vector Light property. * * \param lvp a pointer to a Light vector property. * \param name the name of the member to search for. * \return a pointer to an ILight member on match, or NULL if nothing is found. */ extern ILight *IUFindLight(const ILightVectorProperty *lvp, const char *name); /** \brief Find an IBLOB member in a vector BLOB property. * * \param bvp a pointer to a BLOB vector property. * \param name the name of the member to search for. * \return a pointer to an IBLOB member on match, or NULL if nothing is found. */ extern IBLOB *IUFindBLOB(const IBLOBVectorProperty *bvp, const char *name); /** \brief Returns the first ON switch it finds in the vector switch property. * \note This is only valid for ISR_1OFMANY mode. That is, when only one switch out of many is allowed to be ON. Do not use this function if you can have multiple ON switches in the same vector property. * * \param sp a pointer to a switch vector property. * \return a pointer to the \e first ON ISwitch member if found. If all switches are off, NULL is returned. */ extern ISwitch *IUFindOnSwitch(const ISwitchVectorProperty *sp); /** \brief Returns the index of the string in a string array * * \param needle the string to match against each element in the hay * \param hay a pointer to a string array to search in * \param n the size of hay * \return index of needle if found in the hay. Otherwise -1 if not found. */ extern int IUFindIndex(const char *needle, char **hay, unsigned int n); /** \brief Returns the index of first ON switch it finds in the vector switch property. * \note This is only valid for ISR_1OFMANY mode. That is, when only one switch out of many is allowed to be ON. Do not use this function if you can have multiple ON switches in the same vector property. * * \param sp a pointer to a switch vector property. * \return index to the \e first ON ISwitch member if found. If all switches are off, -1 is returned. */ extern int IUFindOnSwitchIndex(const ISwitchVectorProperty *sp); /** \brief Returns the name of the first ON switch it finds in the supplied arguments. * \note This is only valid for ISR_1OFMANY mode. That is, when only one switch out of many is allowed to be ON. Do not use this function if you can have multiple ON switches in the same vector property. * \note This is a convience function intended to be used in ISNewSwitch(...) function to find out ON switch name without having to change actual switch state via IUUpdateSwitch(..) * * \param states list of switch states passed by ISNewSwitch() * \param names list of switch names passed by ISNewSwitch() * \param n number of switches passed by ISNewSwitch() * \return name of the \e first ON ISwitch member if found. If all switches are off, NULL is returned. */ extern const char *IUFindOnSwitchName(ISState *states, char *names[], int n); /** \brief Reset all switches in a switch vector property to OFF. * * \param svp a pointer to a switch vector property. */ extern void IUResetSwitch(ISwitchVectorProperty *svp); /** \brief Update all switches in a switch vector property. * * \param svp a pointer to a switch vector property. * \param states the states of the new ISwitch members. * \param names the names of the ISwtich members to update. * \param n the number of ISwitch members to update. * \return 0 if update successful, -1 otherwise. */ extern int IUUpdateSwitch(ISwitchVectorProperty *svp, ISState *states, char *names[], int n); /** \brief Update all numbers in a number vector property. * * \param nvp a pointer to a number vector property. * \param values the states of the new INumber members. * \param names the names of the INumber members to update. * \param n the number of INumber members to update. * \return 0 if update successful, -1 otherwise. Update will fail if values are out of scope, or in case of property name mismatch. */ extern int IUUpdateNumber(INumberVectorProperty *nvp, double values[], char *names[], int n); /** \brief Update all text members in a text vector property. * * \param tvp a pointer to a text vector property. * \param texts a pointer to the text members * \param names the names of the IText members to update. * \param n the number of IText members to update. * \return 0 if update successful, -1 otherwise. Update will fail in case of property name mismatch. */ extern int IUUpdateText(ITextVectorProperty *tvp, char *texts[], char *names[], int n); /** \brief Update all BLOB members in a BLOB vector property. * * \param bvp a pointer to a BLOB vector property. * \param sizes sizes of the blobs. * \param blobsizes size of the blobs, raw without compression. * \param blobs a pointer to the BLOB members * \param names the names of the IBLOB members to update. * \param formats The blob format or extension. * \param n the number of IBLOB members to update. * \return 0 if update successful, -1 otherwise. Update will fail in case of property name mismatch. */ extern int IUUpdateBLOB(IBLOBVectorProperty *bvp, int sizes[], int blobsizes[], char *blobs[], char *formats[], char *names[], int n); /** \brief Function to save blob metadata in the corresponding blob. \param bp pointer to an IBLOB member. \param size size of the blob buffer encoded in base64 \param blobsize actual size of the buffer after base64 decoding. This is the actual byte count used in drivers. \param blob pointer to the blob buffer \param format format of the blob buffer \note Do not call this function directly, it is called internally by IUUpdateBLOB. */ extern int IUSaveBLOB(IBLOB *bp, int size, int blobsize, char *blob, char *format); /** \brief Function to update the min and max elements of a number in the client \param nvp pointer to an INumberVectorProperty. \warning This call is not INDI protocol compliant. It sends setNumberVector along with updated Min/Max/Step values so that the client updates the range accordingly for this property. In the INDI-compliant paradigm, it is NOT possible to update min/max/step step of an existing number property and the only way is to do so is to delete and redefine the number property again. However, due to the many problems with approach in device drivers, INDI Library defines this function to simplify the update process without requiring a complete delete/define cycle. */ extern void IUUpdateMinMax(const INumberVectorProperty *nvp); /** \brief Function to reliably save new text in a IText. \param tp pointer to an IText member. \param newtext the new text to be saved */ extern void IUSaveText(IText *tp, const char *newtext); /** \brief Assign attributes for a switch property. The switch's auxiliary elements will be set to NULL. \param sp pointer a switch property to fill \param name the switch name \param label the switch label \param s the switch state (ISS_ON or ISS_OFF) */ extern void IUFillSwitch(ISwitch *sp, const char *name, const char *label, ISState s); /** \brief Assign attributes for a light property. The light's auxiliary elements will be set to NULL. \param lp pointer a light property to fill \param name the light name \param label the light label \param s the light state (IDLE, WARNING, OK, ALERT) */ extern void IUFillLight(ILight *lp, const char *name, const char *label, IPState s); /** \brief Assign attributes for a number property. The number's auxiliary elements will be set to NULL. \param np pointer a number property to fill \param name the number name \param label the number label \param format the number format in printf style (e.g. "%02d") \param min the minimum possible value \param max the maximum possible value \param step the step used to climb from minimum value to maximum value \param value the number's current value */ extern void IUFillNumber(INumber *np, const char *name, const char *label, const char *format, double min, double max, double step, double value); /** \brief Assign attributes for a text property. The text's auxiliary elements will be set to NULL. \param tp pointer a text property to fill \param name the text name \param label the text label \param initialText the initial text */ extern void IUFillText(IText *tp, const char *name, const char *label, const char *initialText); /** \brief Assign attributes for a BLOB property. The BLOB's data and auxiliary elements will be set to NULL. \param bp pointer a BLOB property to fill \param name the BLOB name \param label the BLOB label \param format the BLOB format. */ extern void IUFillBLOB(IBLOB *bp, const char *name, const char *label, const char *format); /** \brief Assign attributes for a switch vector property. The vector's auxiliary elements will be set to NULL. \param svp pointer a switch vector property to fill \param sp pointer to an array of switches \param nsp the dimension of sp \param dev the device name this vector property belongs to \param name the vector property name \param label the vector property label \param group the vector property group \param p the vector property permission \param r the switches behavior \param timeout vector property timeout in seconds \param s the vector property initial state. */ extern void IUFillSwitchVector(ISwitchVectorProperty *svp, ISwitch *sp, int nsp, const char *dev, const char *name, const char *label, const char *group, IPerm p, ISRule r, double timeout, IPState s); /** \brief Assign attributes for a light vector property. The vector's auxiliary elements will be set to NULL. \param lvp pointer a light vector property to fill \param lp pointer to an array of lights \param nlp the dimension of lp \param dev the device name this vector property belongs to \param name the vector property name \param label the vector property label \param group the vector property group \param s the vector property initial state. */ extern void IUFillLightVector(ILightVectorProperty *lvp, ILight *lp, int nlp, const char *dev, const char *name, const char *label, const char *group, IPState s); /** \brief Assign attributes for a number vector property. The vector's auxiliary elements will be set to NULL. \param nvp pointer a number vector property to fill \param np pointer to an array of numbers \param nnp the dimension of np \param dev the device name this vector property belongs to \param name the vector property name \param label the vector property label \param group the vector property group \param p the vector property permission \param timeout vector property timeout in seconds \param s the vector property initial state. */ extern void IUFillNumberVector(INumberVectorProperty *nvp, INumber *np, int nnp, const char *dev, const char *name, const char *label, const char *group, IPerm p, double timeout, IPState s); /** \brief Assign attributes for a text vector property. The vector's auxiliary elements will be set to NULL. \param tvp pointer a text vector property to fill \param tp pointer to an array of texts \param ntp the dimension of tp \param dev the device name this vector property belongs to \param name the vector property name \param label the vector property label \param group the vector property group \param p the vector property permission \param timeout vector property timeout in seconds \param s the vector property initial state. */ extern void IUFillTextVector(ITextVectorProperty *tvp, IText *tp, int ntp, const char *dev, const char *name, const char *label, const char *group, IPerm p, double timeout, IPState s); /** \brief Assign attributes for a BLOB vector property. The vector's auxiliary elements will be set to NULL. \param bvp pointer a BLOB vector property to fill \param bp pointer to an array of BLOBs \param nbp the dimension of bp \param dev the device name this vector property belongs to \param name the vector property name \param label the vector property label \param group the vector property group \param p the vector property permission \param timeout vector property timeout in seconds \param s the vector property initial state. */ extern void IUFillBLOBVector(IBLOBVectorProperty *bvp, IBLOB *bp, int nbp, const char *dev, const char *name, const char *label, const char *group, IPerm p, double timeout, IPState s); /** \brief Update a snooped number vector property from the given XML root element. \param root XML root elememnt containing the snopped property content \param nvp a pointer to the number vector property to be updated. \return 0 if cracking the XML element and updating the property proceeded without errors, -1 if trouble. */ extern int IUSnoopNumber(XMLEle *root, INumberVectorProperty *nvp); /** \brief Update a snooped text vector property from the given XML root element. \param root XML root elememnt containing the snopped property content \param tvp a pointer to the text vector property to be updated. \return 0 if cracking the XML element and updating the property proceeded without errors, -1 if trouble. */ extern int IUSnoopText(XMLEle *root, ITextVectorProperty *tvp); /** \brief Update a snooped light vector property from the given XML root element. \param root XML root elememnt containing the snopped property content \param lvp a pointer to the light vector property to be updated. \return 0 if cracking the XML element and updating the property proceeded without errors, -1 if trouble. */ extern int IUSnoopLight(XMLEle *root, ILightVectorProperty *lvp); /** \brief Update a snooped switch vector property from the given XML root element. \param root XML root elememnt containing the snopped property content \param svp a pointer to the switch vector property to be updated. \return 0 if cracking the XML element and updating the property proceeded without errors, -1 if trouble. */ extern int IUSnoopSwitch(XMLEle *root, ISwitchVectorProperty *svp); /** \brief Update a snooped BLOB vector property from the given XML root element. \param root XML root elememnt containing the snopped property content \param bvp a pointer to the BLOB vector property to be updated. \return 0 if cracking the XML element and updating the property proceeded without errors, -1 if trouble. */ extern int IUSnoopBLOB(XMLEle *root, IBLOBVectorProperty *bvp); /*@}*/ /******************************************************************************* ******************************************************************************* * * Functions the INDI Device Driver framework calls which the Driver must * define. * ******************************************************************************* ******************************************************************************* */ /** * \defgroup dcuFunctions IS Functions: Functions all drivers must define. This section defines functions that must be defined in each driver. These functions are never called by the driver, but are called by the driver framework. These must always be defined even if they do nothing. */ /*@{*/ /** \brief Get Device Properties \param dev the name of the device. This function is called by the framework whenever the driver has received a getProperties message from an INDI client. The argument \param dev is either a string containing the name of the device specified within the message, or NULL if no device was specified. If the driver does not recognize the device, it should ignore the message and do nothing. If dev matches the device the driver is implementing, or dev is NULL, the driver must respond by sending one defXXX message to describe each property defined by this device, including its current (or initial) value. The recommended way to send these messages is to call the appropriate IDDef functions. */ extern void ISGetProperties(const char *dev); /** \brief Update the value of an existing text vector property. \param dev the name of the device. \param name the name of the text vector property to update. \param texts an array of text values. \param names parallel names to the array of text values. \param n the dimension of texts[]. \note You do not need to call this function, it is called by INDI when new text values arrive from the client. */ extern void ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n); /** \brief Update the value of an existing number vector property. \param dev the name of the device. \param name the name of the number vector property to update. \param values an array of number values. \param names parallel names to the array of number values. \param n the dimension of doubles[]. \note You do not need to call this function, it is called by INDI when new number values arrive from the client. */ extern void ISNewNumber(const char *dev, const char *name, double *values, char *names[], int n); /** \brief Update the value of an existing switch vector property. \param dev the name of the device. \param name the name of the switch vector property to update. \param states an array of switch states. \param names parallel names to the array of switch states. \param n the dimension of states[]. \note You do not need to call this function, it is called by INDI when new switch values arrive from the client. */ extern void ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n); /** \brief Update data of an existing blob vector property. \param dev the name of the device. \param name the name of the blob vector property to update. \param sizes an array of base64 blob sizes in bytes \e before decoding. \param blobsizes an array of the sizes of blobs \e after decoding from base64. \param blobs an array of decoded data. Each blob size is found in \e blobsizes array. \param formats Blob data format (e.g. fits.z). \param names names of blob members to update. \param n the number of blobs to update. \note You do not need to call this function, it is called by INDI when new blob values arrive from the client. e.g. BLOB element with name names[0] has data located in blobs[0] with size sizes[0] and format formats[0]. */ extern void ISNewBLOB(const char *dev, const char *name, int sizes[], int blobsizes[], char *blobs[], char *formats[], char *names[], int n); /** \brief Function defined by Drivers that is called when another Driver it is snooping (by having previously called IDSnoopDevice()) sent any INDI message. \param root The argument contains the full message exactly as it was sent by the driver. \e Hint: use the IUSnoopXXX utility functions to help crack the message if it was one of setXXX or defXXX. */ extern void ISSnoopDevice(XMLEle *root); /*@}*/ /* Handy readability macro to avoid unused variables warnings */ #define INDI_UNUSED(x) (void)x /** \brief Extract dev and name attributes from an XML element. \param root The XML element to be parsed. \param dev pointer to an allocated buffer to save the extracted element device name attribute. The buffer size must be at least MAXINDIDEVICE bytes. \param name pointer to an allocated buffer to save the extracted elemented name attribute. The buffer size must be at least MAXINDINAME bytes. \param msg pointer to an allocated char buffer to store error messages. The minimum buffer size is MAXRBUF. \return 0 if successful, -1 if error is encountered and msg is set. */ extern int crackDN(XMLEle *root, char **dev, char **name, char msg[]); /** \brief Extract property state (Idle, OK, Busy, Alert) from the supplied string. \param str A string representation of the state. \param ip Pointer to IPState structure to store the extracted property state. \return 0 if successful, -1 if error is encountered. */ extern int crackIPState(const char *str, IPState *ip); /** \brief Extract switch state (On or Off) from the supplied string. \param str A string representation of the switch state. \param ip Pointer to ISState structure to store the extracted switch state. \return 0 if successful, -1 if error is encountered. */ extern int crackISState(const char *str, ISState *ip); /** \brief Extract property permission state (RW, RO, WO) from the supplied string. \param str A string representation of the permission state. \param ip Pointer to IPerm structure to store the extracted permission state. \return 0 if successful, -1 if error is encountered. */ extern int crackIPerm(const char *str, IPerm *ip); /** \brief Extract switch rule (OneOfMany, OnlyOne..etc) from the supplied string. \param str A string representation of the switch rule. \param ip Pointer to ISRule structure to store the extracted switch rule. \return 0 if successful, -1 if error is encountered. */ extern int crackISRule(const char *str, ISRule *ip); /** \return Returns a string representation of the supplied property state. */ extern const char *pstateStr(IPState s); /** \return Returns a string representation of the supplied switch status. */ extern const char *sstateStr(ISState s); /** \return Returns a string representation of the supplied switch rule. */ extern const char *ruleStr(ISRule r); /** \return Returns a string representation of the supplied permission value. */ extern const char *permStr(IPerm p); extern void xmlv1(); #ifdef __cplusplus } #endif libindi/README0000664000175000017500000001023513263645557012353 0ustar jasemjasemlibindi v1.6.0 ============== The code here demonstrates the use of INDI, an Instrument-Neutral Device Interface protocol. See http://www.clearskyinstitute.com/INDI/INDI.pdf. Architecture: Typical INDI Client / Server / Driver / Device connectivity: INDI Client 1 ----| |---- INDI Driver A ---- Dev X | | INDI Client 2 ----| |---- INDI Driver B ---- Dev Y | | | ... |--- indiserver ---| |-- Dev Z | | | | INDI Client n ----| |---- INDI Driver C ---- Dev T Client INET Server UNIX Driver Hardware processes sockets process pipes processes devices Indiserver is the public network access point where one or more INDI Clients may contact one or more INDI Drivers. Indiserver launches each driver process and arranges for it to receive the INDI protocol from Clients on its stdin and expects to find commands destined for Clients on the driver's stdout. Anything arriving from a driver process' stderr is copied to indiserver's stderr. Indiserver only provides convenient port, fork and data steering services. If desired, a Client may run and connect to INDI Drivers directly. Construction: An INDI driver typically consists of one .c file, eg, mydriver.c, which #includes indiapi.h to access the reference API declarations. It is compiled then linked with indidrivermain.o, eventloop.o and liblilxml.a to form an INDI process. These supporting files contain the implementation of the INDI Driver API and need not be changed in any way. Note that evenloop.[ch] provide a nice callback facility independent of INDI which may be used in other projects if desired. The driver implementation, again in our example mydriver.c, does not contain a main() but is expected to operate as an event-driver program. The driver must implement each ISxxx() function but never call them. The IS() functions are called by the reference implementation main() as messages arrive from Clients. Within each IS function the driver performs the desired tasks then may report back to the Client by calling the IDxxx() functions. The reference API provides IE() functions to allow the driver to add its own callback functions if desired. The driver can arrange for functions to be called when reading a file descriptor will not block; when a time interval has expired; or when there is no other client traffic in progress. The sample indiserver is a stand alone process that may be used to run one or more INDI-compliant drivers. It takes the names of each driver process to run in its command line args. To build indiserver type 'make indiserver'; to build all the sample drivers type 'make drivers'; to run the sample server with all drivers type 'make run'. Killing indiserver will also kill all the drivers it started. Secure remote operation: Suppose we want to run indiserver and its clients on a remote machine, r, and connect them to our favorite INDI client, XEphem, running on the local machine. From the local machine log onto the remote machine, r, by typing: ssh2 -L 7624:s:7624 r after logging in, run indiserver on the remote machine: make run Back on the local machine, start XEphem, then open Views -> Sky View -> Telescope -> INDI panel. XEphem will connect to the remote INDI server securely and automatically begin running. Sweet. Testing: A low-level way to test the socket, forking and data steering abilities of indiserver is to use the 'hose' command from the netpipes collection (http://web.purplefrog.com/~thoth/netpipes/netpipes.html): 1. start indiserver using UNIX' cat program as the only INDI "device": % indiserver cat & 2. use hose to connect to the "cat" device driver which just copies back: % hose localhost 7624 --slave hello world hello world more stuff more stuff libindi/cmake_modules/0000775000175000017500000000000013263645566014302 5ustar jasemjasemlibindi/cmake_modules/FindARAVIS.cmake0000664000175000017500000000272313263645566017076 0ustar jasemjasem# - Find the native sqlite3 includes and library # # This module defines # ARV_INCLUDE_DIR, where to find libgphoto2 header files # ARV_LIBRARIES, the libraries to link against to use libgphoto2 # ARV_FOUND, If false, do not try to use libgphoto2. # ARV_VERSION_STRING, e.g. 2.4.14 # ARV_VERSION_MAJOR, e.g. 2 # ARV_VERSION_MINOR, e.g. 4 # ARV_VERSION_PATCH, e.g. 14 # # also defined, but not for general use are # ARV_LIBRARY, where to find the sqlite3 library. #============================================================================= # Copyright 2010 henrik andersson #============================================================================= SET(ARV_FIND_REQUIRED ${Arv_FIND_REQUIRED}) find_path(ARV_INCLUDE_DIR aravis-0.6/arv.h) mark_as_advanced(ARV_INCLUDE_DIR) set(ARV_NAMES ${ARV_NAMES} aravis-0.6) find_library(ARV_LIBRARY NAMES ${ARV_NAMES} ) mark_as_advanced(ARV_LIBRARY) set(ARV_VERSION_MAJOR "0") set(ARV_VERSION_MINOR "6") set(ARV_VERSION_STRING "${ARV_VERSION_MAJOR}.${ARV_VERSION_MINOR}") # handle the QUIETLY and REQUIRED arguments and set ARV_FOUND to TRUE if # all listed variables are TRUE include(FindPackageHandleStandardArgs) find_package_handle_standard_args(ARV DEFAULT_MSG ARV_LIBRARY ARV_INCLUDE_DIR) IF(ARV_FOUND) #SET(Arv_LIBRARIES ${ARV_LIBRARY}) SET(Arv_LIBRARIES "aravis-0.6") SET(Arv_INCLUDE_DIRS "${ARV_INCLUDE_DIR}/aravis-0.6") MESSAGE (STATUS "Found aravis: ${Arv_LIBRARIES} ${Arv_INCLUDE_DIRS}") ENDIF(ARV_FOUND) libindi/cmake_modules/FindNova.cmake0000664000175000017500000000304613263645566017013 0ustar jasemjasem# - Try to find NOVA # Once done this will define # # NOVA_FOUND - system has NOVA # NOVA_INCLUDE_DIR - the NOVA include directory # NOVA_LIBRARIES - Link these to use NOVA # Copyright (c) 2006, Jasem Mutlaq # Based on FindLibfacile by Carsten Niehaus, # # Redistribution and use is allowed according to the terms of the BSD license. # For details see the accompanying COPYING-CMAKE-SCRIPTS file. if (NOVA_INCLUDE_DIR AND NOVA_LIBRARIES) # in cache already set(NOVA_FOUND TRUE) message(STATUS "Found libnova: ${NOVA_LIBRARIES}") else (NOVA_INCLUDE_DIR AND NOVA_LIBRARIES) find_path(NOVA_INCLUDE_DIR libnova.h PATH_SUFFIXES libnova ${_obIncDir} ${GNUWIN32_DIR}/include ) find_library(NOVA_LIBRARIES NAMES nova libnova libnovad PATHS ${_obLinkDir} ${GNUWIN32_DIR}/lib ) set(CMAKE_REQUIRED_INCLUDES ${NOVA_INCLUDE_DIR}) set(CMAKE_REQUIRED_LIBRARIES ${NOVA_LIBRARIES}) if(NOVA_INCLUDE_DIR AND NOVA_LIBRARIES) set(NOVA_FOUND TRUE) else (NOVA_INCLUDE_DIR AND NOVA_LIBRARIES) set(NOVA_FOUND FALSE) endif(NOVA_INCLUDE_DIR AND NOVA_LIBRARIES) if (NOVA_FOUND) if (NOT Nova_FIND_QUIETLY) message(STATUS "Found NOVA: ${NOVA_LIBRARIES}") endif (NOT Nova_FIND_QUIETLY) else (NOVA_FOUND) if (Nova_FIND_REQUIRED) message(FATAL_ERROR "libnova not found. Please install libnova development package.") endif (Nova_FIND_REQUIRED) endif (NOVA_FOUND) mark_as_advanced(NOVA_INCLUDE_DIR NOVA_LIBRARIES) endif (NOVA_INCLUDE_DIR AND NOVA_LIBRARIES) libindi/cmake_modules/FindGMock.cmake0000664000175000017500000001162313263645566017110 0ustar jasemjasem#.rst: # FindGMock # --------- # # Locate the Google C++ Mocking Framework. # # Defines the following variables: # # :: # # GMOCK_FOUND - Found the Google Mocking framework # GMOCK_INCLUDE_DIRS - Include directories # # # # Also defines the library variables below as normal variables. These # contain debug/optimized keywords when a debugging library is found. # # :: # # GMOCK_LIBRARIES - libgmock # # # # Accepts the following variables as input: # # :: # # GMOCK_ROOT - (as a CMake or environment variable) # The root directory of the gmock install prefix # # # # :: # # GMOCK_MSVC_SEARCH - If compiling with MSVC, this variable can be set to # "MD" or "MT" to enable searching a GMock build tree # (defaults: "MD") # # # # Example Usage: # # :: # # find_package(GMock REQUIRED) # include_directories(${GMOCK_INCLUDE_DIRS}) # # # # :: # # add_executable(foo foo.cc) # target_link_libraries(foo ${GMOCK_LIBRARIES}) # # # # :: # # add_test(AllMocksInFoo foo) # # # # # # If you would like each Google test to show up in CMock as a test you # may use the following macro. NOTE: It will slow down your tests by # running an executable for each test and test fixture. You will also # have to rerun CMake after adding or removing tests or test fixtures. # # GMOCK_ADD_MOCKS(executable extra_args ARGN) # # :: # # executable = The path to the test executable # extra_args = Pass a list of extra arguments to be passed to # executable enclosed in quotes (or "" for none) # ARGN = A list of source files to search for tests & test # fixtures. # # # # :: # # Example: # set(FooMockArgs --foo 1 --bar 2) # add_executable(FooMock FooUnitMock.cc) # GMOCK_ADD_MOCKS(FooMock "${FooMockArgs}" FooUnitMock.cc) #============================================================================= # Copyright 2009 Kitware, Inc. # Copyright 2009 Philip Lowman # Copyright 2009 Daniel Blezek # # Distributed under the OSI-approved BSD License (the "License"); # see accompanying file Copyright.txt for details. # # This software is distributed WITHOUT ANY WARRANTY; without even the # implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. # See the License for more information. #============================================================================= # (To distribute this file outside of CMake, substitute the full # License text for the above reference.) # # Thanks to Daniel Blezek for the GMOCK_ADD_MOCKS code function(GMOCK_ADD_MOCKS executable extra_args) if(NOT ARGN) message(FATAL_ERROR "Missing ARGN: Read the documentation for GMOCK_ADD_MOCKS") endif() foreach(source ${ARGN}) file(READ "${source}" contents) string(REGEX MATCHALL "MOCK_?F?\\(([A-Za-z_0-9 ,]+)\\)" found_tests ${contents}) foreach(hit ${found_tests}) string(REGEX REPLACE ".*\\( *([A-Za-z_0-9]+), *([A-Za-z_0-9]+) *\\).*" "\\1.\\2" test_name ${hit}) add_test(${test_name} ${executable} --gmock_filter=${test_name} ${extra_args}) endforeach() endforeach() endfunction() function(_gmock_append_debugs _endvar _library) if(${_library} AND ${_library}_DEBUG) set(_output optimized ${${_library}} debug ${${_library}_DEBUG}) else() set(_output ${${_library}}) endif() set(${_endvar} ${_output} PARENT_SCOPE) endfunction() function(_gmock_find_library _name) find_library(${_name} NAMES ${ARGN} HINTS ENV GMOCK_ROOT ${GMOCK_ROOT} PATH_SUFFIXES ${_gmock_libpath_suffixes} ) mark_as_advanced(${_name}) endfunction() # if(NOT DEFINED GMOCK_MSVC_SEARCH) set(GMOCK_MSVC_SEARCH MD) endif() set(_gmock_libpath_suffixes lib) if(MSVC) if(GMOCK_MSVC_SEARCH STREQUAL "MD") list(APPEND _gmock_libpath_suffixes msvc/gmock-md/Debug msvc/gmock-md/Release) elseif(GMOCK_MSVC_SEARCH STREQUAL "MT") list(APPEND _gmock_libpath_suffixes msvc/gmock/Debug msvc/gmock/Release) endif() endif() find_path(GMOCK_INCLUDE_DIR gmock/gmock.h HINTS $ENV{GMOCK_ROOT}/include ${GMOCK_ROOT}/include ) mark_as_advanced(GMOCK_INCLUDE_DIR) if(MSVC AND GMOCK_MSVC_SEARCH STREQUAL "MD") # The provided /MD project files for Google Mock add -md suffixes to the # library names. _gmock_find_library(GMOCK_LIBRARY gmock-md gmock) _gmock_find_library(GMOCK_LIBRARY_DEBUG gmock-mdd gmockd) else() _gmock_find_library(GMOCK_LIBRARY gmock) _gmock_find_library(GMOCK_LIBRARY_DEBUG gmockd) endif() FIND_PACKAGE_HANDLE_STANDARD_ARGS(GMock DEFAULT_MSG GMOCK_LIBRARY GMOCK_INCLUDE_DIR) if(GMOCK_FOUND) set(GMOCK_INCLUDE_DIRS ${GMOCK_INCLUDE_DIR}) _gmock_append_debugs(GMOCK_LIBRARIES GMOCK_LIBRARY) endif() libindi/cmake_modules/FindFLI.cmake0000664000175000017500000000264513263645566016526 0ustar jasemjasem# - Try to find Finger Lakes Instruments Library # Once done this will define # # FLI_FOUND - system has FLI # FLI_INCLUDE_DIR - the FLI include directory # FLI_LIBRARIES - Link these to use FLI # Copyright (c) 2008, Jasem Mutlaq # Based on FindLibfacile by Carsten Niehaus, # # Redistribution and use is allowed according to the terms of the BSD license. # For details see the accompanying COPYING-CMAKE-SCRIPTS file. if (FLI_INCLUDE_DIR AND FLI_LIBRARIES) # in cache already set(FLI_FOUND TRUE) message(STATUS "Found libfli: ${FLI_LIBRARIES}") else (FLI_INCLUDE_DIR AND FLI_LIBRARIES) find_path(FLI_INCLUDE_DIR libfli.h PATH_SUFFIXES fli ${_obIncDir} ${GNUWIN32_DIR}/include ) find_library(FLI_LIBRARIES NAMES fli PATHS ${_obLinkDir} ${GNUWIN32_DIR}/lib ) if(FLI_INCLUDE_DIR AND FLI_LIBRARIES) set(FLI_FOUND TRUE) else (FLI_INCLUDE_DIR AND FLI_LIBRARIES) set(FLI_FOUND FALSE) endif(FLI_INCLUDE_DIR AND FLI_LIBRARIES) if (FLI_FOUND) if (NOT FLI_FIND_QUIETLY) message(STATUS "Found FLI: ${FLI_LIBRARIES}") endif (NOT FLI_FIND_QUIETLY) else (FLI_FOUND) if (FLI_FIND_REQUIRED) message(FATAL_ERROR "FLI not found. Please install libfli-dev. http://www.indilib.org") endif (FLI_FIND_REQUIRED) endif (FLI_FOUND) mark_as_advanced(FLI_INCLUDE_DIR FLI_LIBRARIES) endif (FLI_INCLUDE_DIR AND FLI_LIBRARIES) libindi/cmake_modules/FindALUT.cmake0000664000175000017500000000504213263645566016653 0ustar jasemjasem# - Locate ALUT # This module defines # ALUT_LIBRARY # ALUT_FOUND, if false, do not try to link to OpenAL # ALUT_INCLUDE_DIR, where to find the headers # # $OPENALDIR is an environment variable that would # correspond to the ./configure --prefix=$OPENALDIR # used in building OpenAL. # # Created by Bryan Donlan, based on the FindOpenAL.cmake module by Eric Wang. FIND_PATH(ALUT_INCLUDE_DIR alut.h $ENV{OPENALDIR}/include ~/Library/Frameworks/OpenAL.framework/Headers /Library/Frameworks/OpenAL.framework/Headers /System/Library/Frameworks/OpenAL.framework/Headers # Tiger /usr/local/include/AL /usr/local/include/OpenAL /usr/local/include /usr/include/AL /usr/include/OpenAL /usr/include /sw/include/AL # Fink /sw/include/OpenAL /sw/include /opt/local/include/AL # DarwinPorts /opt/local/include/OpenAL /opt/local/include /opt/csw/include/AL # Blastwave /opt/csw/include/OpenAL /opt/csw/include /opt/include/AL /opt/include/OpenAL /opt/include ) # I'm not sure if I should do a special casing for Apple. It is # unlikely that other Unix systems will find the framework path. # But if they do ([Next|Open|GNU]Step?), # do they want the -framework option also? IF(${ALUT_INCLUDE_DIR} MATCHES ".framework") STRING(REGEX REPLACE "(.*)/.*\\.framework/.*" "\\1" ALUT_FRAMEWORK_PATH_TMP ${ALUT_INCLUDE_DIR}) IF("${ALUT_FRAMEWORK_PATH_TMP}" STREQUAL "/Library/Frameworks" OR "${ALUT_FRAMEWORK_PATH_TMP}" STREQUAL "/System/Library/Frameworks" ) # String is in default search path, don't need to use -F SET (ALUT_LIBRARY "-framework OpenAL" CACHE STRING "OpenAL framework for OSX") ELSE("${ALUT_FRAMEWORK_PATH_TMP}" STREQUAL "/Library/Frameworks" OR "${ALUT_FRAMEWORK_PATH_TMP}" STREQUAL "/System/Library/Frameworks" ) # String is not /Library/Frameworks, need to use -F SET(ALUT_LIBRARY "-F${ALUT_FRAMEWORK_PATH_TMP} -framework OpenAL" CACHE STRING "OpenAL framework for OSX") ENDIF("${ALUT_FRAMEWORK_PATH_TMP}" STREQUAL "/Library/Frameworks" OR "${ALUT_FRAMEWORK_PATH_TMP}" STREQUAL "/System/Library/Frameworks" ) # Clear the temp variable so nobody can see it SET(ALUT_FRAMEWORK_PATH_TMP "" CACHE INTERNAL "") ELSE(${ALUT_INCLUDE_DIR} MATCHES ".framework") FIND_LIBRARY(ALUT_LIBRARY NAMES alut PATHS $ENV{OPENALDIR}/lib $ENV{OPENALDIR}/libs /usr/local/lib /usr/lib /sw/lib /opt/local/lib /opt/csw/lib /opt/lib ) ENDIF(${ALUT_INCLUDE_DIR} MATCHES ".framework") SET(ALUT_FOUND "NO") IF(ALUT_LIBRARY) SET(ALUT_FOUND "YES") ENDIF(ALUT_LIBRARY) libindi/cmake_modules/UnityBuild.cmake0000664000175000017500000001627713263645566017411 0ustar jasemjasem# # Copyright (c) 2009-2012 Christoph Heindl # Copyright (c) 2015 Csaba Kertész (csaba.kertesz@gmail.com) # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright # notice, this list of conditions and the following disclaimer in the # documentation and/or other materials provided with the distribution. # * Neither the name of the nor the # names of its contributors may be used to endorse or promote products # derived from this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE # DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY # DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND # ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # MACRO (COMMIT_UNITY_FILE UNITY_FILE FILE_CONTENT) SET(DIRTY FALSE) # Check if the build file exists SET(OLD_FILE_CONTENT "") IF (NOT EXISTS ${${UNITY_FILE}} AND NOT EXISTS ${CMAKE_CURRENT_BINARY_DIR}/${${UNITY_FILE}}) SET(DIRTY TRUE) ELSE () # Check the file content FILE(STRINGS ${${UNITY_FILE}} OLD_FILE_CONTENT) STRING(REPLACE ";" "" OLD_FILE_CONTENT "${OLD_FILE_CONTENT}") STRING(REPLACE "\n" "" NEW_CONTENT "${${FILE_CONTENT}}") STRING(COMPARE EQUAL "${OLD_FILE_CONTENT}" "${NEW_CONTENT}" EQUAL_CHECK) IF (NOT EQUAL_CHECK EQUAL 1) SET(DIRTY TRUE) ENDIF () ENDIF () IF (DIRTY MATCHES TRUE) MESSAGE(STATUS "Write Unity Build file: " ${${UNITY_FILE}}) FILE(WRITE ${${UNITY_FILE}} "${${FILE_CONTENT}}") ENDIF () # Create a dummy copy of the unity file to trigger CMake reconfigure if it is deleted. SET(UNITY_FILE_PATH "") SET(UNITY_FILE_NAME "") GET_FILENAME_COMPONENT(UNITY_FILE_PATH ${${UNITY_FILE}} PATH) GET_FILENAME_COMPONENT(UNITY_FILE_NAME ${${UNITY_FILE}} NAME) CONFIGURE_FILE(${${UNITY_FILE}} ${UNITY_FILE_PATH}/CMakeFiles/${UNITY_FILE_NAME}.dummy) ENDMACRO () MACRO (ENABLE_UNITY_BUILD TARGET_NAME SOURCE_VARIABLE_NAME UNIT_SIZE EXTENSION) # Limit is zero based conversion of unit_size MATH(EXPR LIMIT ${UNIT_SIZE}-1) SET(FILES ${SOURCE_VARIABLE_NAME}) # Effectivly ignore the source files from the build, but keep track them for changes. SET_SOURCE_FILES_PROPERTIES(${${FILES}} PROPERTIES HEADER_FILE_ONLY true) # Counts the number of source files up to the threshold SET(COUNTER ${LIMIT}) # Have one or more unity build files SET(FILE_NUMBER 0) SET(BUILD_FILE "") SET(BUILD_FILE_CONTENT "") SET(UNITY_BUILD_FILES "") SET(_DEPS "") FOREACH (SOURCE_FILE ${${FILES}}) IF (COUNTER EQUAL LIMIT) SET(_DEPS "") # Write the actual Unity Build file IF (NOT ${BUILD_FILE} STREQUAL "" AND NOT ${BUILD_FILE_CONTENT} STREQUAL "") COMMIT_UNITY_FILE(BUILD_FILE BUILD_FILE_CONTENT) ENDIF () SET(UNITY_BUILD_FILES ${UNITY_BUILD_FILES} ${BUILD_FILE}) # Set the variables for the current Unity Build file SET(BUILD_FILE ${CMAKE_CURRENT_BINARY_DIR}/unitybuild_${FILE_NUMBER}_${TARGET_NAME}.${EXTENSION}) SET(BUILD_FILE_CONTENT "// Unity Build file generated by CMake\n") MATH(EXPR FILE_NUMBER ${FILE_NUMBER}+1) SET(COUNTER 0) ENDIF () # Add source path to the file name if it is not there yet. SET(FINAL_SOURCE_FILE "") SET(SOURCE_PATH "") GET_FILENAME_COMPONENT(SOURCE_PATH ${SOURCE_FILE} PATH) IF (SOURCE_PATH STREQUAL "" OR NOT EXISTS ${SOURCE_FILE}) SET(FINAL_SOURCE_FILE ${CMAKE_CURRENT_SOURCE_DIR}/${SOURCE_FILE}) ELSE () SET(FINAL_SOURCE_FILE ${SOURCE_FILE}) ENDIF () # Treat only the existing files or moc_*.cpp files STRING(FIND ${SOURCE_FILE} "moc_" MOC_POS) IF (EXISTS ${FINAL_SOURCE_FILE} OR MOC_POS GREATER -1) # Add md5 hash of the source file (except moc files) to the build file content IF (MOC_POS LESS 0) SET(MD5_HASH "") FILE(MD5 ${FINAL_SOURCE_FILE} MD5_HASH) SET(BUILD_FILE_CONTENT "${BUILD_FILE_CONTENT}// md5: ${MD5_HASH}\n") ENDIF () # Add the source file to the build file content IF (MOC_POS GREATER -1) SET(BUILD_FILE_CONTENT "${BUILD_FILE_CONTENT}#include <${SOURCE_FILE}>\n") ELSE () SET(BUILD_FILE_CONTENT "${BUILD_FILE_CONTENT}#include <${FINAL_SOURCE_FILE}>\n") ENDIF () # Add the source dependencies to the Unity Build file GET_SOURCE_FILE_PROPERTY(_FILE_DEPS ${SOURCE_FILE} OBJECT_DEPENDS) IF (_FILE_DEPS) SET(_DEPS ${_DEPS} ${_FILE_DEPS}) SET_SOURCE_FILES_PROPERTIES(${BUILD_FILE} PROPERTIES OBJECT_DEPENDS "${_DEPS}") ENDIF() # Keep counting up to the threshold. Increment counter. MATH(EXPR COUNTER ${COUNTER}+1) ENDIF () ENDFOREACH () # Write out the last Unity Build file IF (NOT ${BUILD_FILE} STREQUAL "" AND NOT ${BUILD_FILE_CONTENT} STREQUAL "") COMMIT_UNITY_FILE(BUILD_FILE BUILD_FILE_CONTENT) ENDIF () SET(UNITY_BUILD_FILES ${UNITY_BUILD_FILES} ${BUILD_FILE}) SET(${SOURCE_VARIABLE_NAME} ${${SOURCE_VARIABLE_NAME}} ${UNITY_BUILD_FILES}) ENDMACRO () MACRO (UNITY_GENERATE_MOC TARGET_NAME SOURCES HEADERS) SET(NEW_SOURCES "") FOREACH (HEADER_FILE ${${HEADERS}}) IF (NOT EXISTS ${HEADER_FILE}) MESSAGE(FATAL_ERROR "Header file does not exist (mocing): ${HEADER_FILE}") ENDIF () FILE(READ ${HEADER_FILE} FILE_CONTENT) STRING(FIND "${FILE_CONTENT}" "Q_OBJECT" QOBJECT_POS) STRING(FIND "${FILE_CONTENT}" "Q_SLOTS" QSLOTS_POS) STRING(FIND "${FILE_CONTENT}" "Q_SIGNALS" QSIGNALS_POS) STRING(FIND "${FILE_CONTENT}" "QObject" OBJECT_POS) STRING(FIND "${FILE_CONTENT}" "slots" SLOTS_POS) STRING(FIND "${FILE_CONTENT}" "signals" SIGNALS_POS) IF (QOBJECT_POS GREATER 0 OR OBJECT_POS GREATER 0 OR QSLOTS_POS GREATER 0 OR Q_SIGNALS GREATER 0 OR SLOTS_POS GREATER 0 OR SIGNALS GREATER 0) # Generate the moc filename GET_FILENAME_COMPONENT(HEADER_BASENAME ${HEADER_FILE} NAME_WE) SET(MOC_FILENAME "moc_${HEADER_BASENAME}.cpp") SET(NEW_SOURCES ${NEW_SOURCES} ; "${CMAKE_CURRENT_BINARY_DIR}/${MOC_FILENAME}") ADD_CUSTOM_COMMAND(OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/${MOC_FILENAME}" DEPENDS ${HEADER_FILE} COMMAND ${QT_MOC_EXECUTABLE} ${HEADER_FILE} -o "${CMAKE_CURRENT_BINARY_DIR}/${MOC_FILENAME}") ENDIF () ENDFOREACH () IF (NEW_SOURCES) SET_SOURCE_FILES_PROPERTIES(${NEW_SOURCES} PROPERTIES GENERATED TRUE) SET(${SOURCES} ${${SOURCES}} ; ${NEW_SOURCES}) ENDIF () ENDMACRO () libindi/cmake_modules/FindDSPAU.cmake0000664000175000017500000000300713263645566016761 0ustar jasemjasem# - Try to find Digital Signal Processing for Astronomical Usage # Once done this will define # # DSPAU_FOUND - system has DSPAU # DSPAU_INCLUDE_DIR - the DSPAU include directory # DSPAU_LIBRARIES - Link these to use DSPAU # Copyright (c) 2008, Jasem Mutlaq # Based on FindLibfacile by Carsten Niehaus, # # Redistribution and use is allowed according to the terms of the BSD license. # For details see the accompanying COPYING-CMAKE-SCRIPTS file. if (DSPAU_INCLUDE_DIR AND DSPAU_LIBRARIES) # in cache already set(DSPAU_FOUND TRUE) message(STATUS "Found libdspau: ${DSPAU_LIBRARIES}") else (DSPAU_INCLUDE_DIR AND DSPAU_LIBRARIES) find_path(DSPAU_INCLUDE_DIR libdspau.h PATH_SUFFIXES dspau ${_obIncDir} ${GNUWIN32_DIR}/include ) find_library(DSPAU_LIBRARIES NAMES dspau PATHS ${_obLinkDir} ${GNUWIN32_DIR}/lib ) if(DSPAU_INCLUDE_DIR AND DSPAU_LIBRARIES) set(DSPAU_FOUND TRUE) else (DSPAU_INCLUDE_DIR AND DSPAU_LIBRARIES) set(DSPAU_FOUND FALSE) endif(DSPAU_INCLUDE_DIR AND DSPAU_LIBRARIES) if (DSPAU_FOUND) if (NOT DSPAU_FIND_QUIETLY) message(STATUS "Found DSPAU: ${DSPAU_LIBRARIES}") endif (NOT DSPAU_FIND_QUIETLY) else (DSPAU_FOUND) if (DSPAU_FIND_REQUIRED) message(FATAL_ERROR "DSPAU not found. Please install libdspau-dev. http://www.indilib.org") endif (DSPAU_FIND_REQUIRED) endif (DSPAU_FOUND) mark_as_advanced(DSPAU_INCLUDE_DIR DSPAU_LIBRARIES) endif (DSPAU_INCLUDE_DIR AND DSPAU_LIBRARIES) libindi/cmake_modules/FindUSB1.cmake0000664000175000017500000000562313263645566016625 0ustar jasemjasem# - Try to find libusb-1.0 # Once done this will define # # USB1_FOUND - system has libusb-1.0 # USB1_INCLUDE_DIRS - the libusb-1.0 include directories # USB1_LIBRARIES - Link these to use libusb-1.0 # USB1_DEFINITIONS - Compiler switches required for using libusb-1.0 # # USB1_HAS_LIBUSB_ERROR_NAME - defined when libusb-1.0 has libusb_error_name() #============================================================================= # Copyright (c) 2017 Pino Toscano # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions # are met: # # 1. Redistributions of source code must retain the copyright # notice, this list of conditions and the following disclaimer. # 2. Redistributions in binary form must reproduce the copyright # notice, this list of conditions and the following disclaimer in the # documentation and/or other materials provided with the distribution. # 3. The name of the author may not be used to endorse or promote products # derived from this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR # IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES # OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. # IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT # NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF # THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #============================================================================= find_package(PkgConfig) pkg_check_modules(PC_LIBUSB1 QUIET libusb-1.0) find_path(USB1_INCLUDE_DIR NAMES libusb.h HINTS ${PC_LIBUSB1_INCLUDE_DIRS} PATH_SUFFIXES libusb-1.0 ) find_library(USB1_LIBRARY NAMES ${PC_LIBUSB1_LIBRARIES} usb-1.0 HINTS ${PC_LIBUSB1_LIBRARY_DIRS} ) set(USB1_INCLUDE_DIRS ${USB1_INCLUDE_DIR}) set(USB1_LIBRARIES ${USB1_LIBRARY}) include(FindPackageHandleStandardArgs) find_package_handle_standard_args(USB1 FOUND_VAR USB1_FOUND REQUIRED_VARS USB1_LIBRARY USB1_INCLUDE_DIR VERSION_VAR PC_LIBUSB1_VERSION ) mark_as_advanced(USB1_INCLUDE_DIRS USB1_LIBRARIES) if(USB1_FOUND) include(CheckCXXSourceCompiles) include(CMakePushCheckState) cmake_push_check_state(RESET) set(CMAKE_REQUIRED_INCLUDES ${USB1_INCLUDE_DIRS}) set(CMAKE_REQUIRED_LIBRARIES ${USB1_LIBRARIES}) check_cxx_source_compiles("#include int main() { libusb_error_name(0); return 0; }" USB1_HAS_LIBUSB_ERROR_NAME) cmake_pop_check_state() endif() libindi/cmake_modules/FindINDI.cmake0000664000175000017500000003247213263645566016640 0ustar jasemjasem# Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # http://www.apache.org/licenses/LICENSE-2.0 # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # # This module can find INDI Library # # Requirements: # - CMake >= 2.8.3 (for new version of find_package_handle_standard_args) # # The following variables will be defined for your use: # - INDI_FOUND : were all of your specified components found (include dependencies)? # - INDI_INCLUDE_DIR : INDI include directory # - INDI_DATA_DIR : INDI include directory # - INDI_LIBRARIES : INDI libraries # - INDI_DRIVER_LIBRARIES : Same as above maintained for backward compatibility # - INDI_VERSION : complete version of INDI (x.y.z) # - INDI_MAJOR_VERSION : major version of INDI # - INDI_MINOR_VERSION : minor version of INDI # - INDI_RELEASE_VERSION : release version of INDI # - INDI__FOUND : were found? (FALSE for non specified component if it is not a dependency) # # For windows or non standard installation, define INDI_ROOT variable to point to the root installation of INDI. Two ways: # - run cmake with -DINDI_ROOT= # - define an environment variable with the same name before running cmake # With cmake-gui, before pressing "Configure": # 1) Press "Add Entry" button # 2) Add a new entry defined as: # - Name: INDI_ROOT # - Type: choose PATH in the selection list # - Press "..." button and select the root installation of INDI # # Example Usage: # # 1. Copy this file in the root of your project source directory # 2. Then, tell CMake to search this non-standard module in your project directory by adding to your CMakeLists.txt: # set(CMAKE_MODULE_PATH ${PROJECT_SOURCE_DIR}) # 3. Finally call find_package() once, here are some examples to pick from # # Require INDI 1.4 or later # find_package(INDI 1.4 REQUIRED) # # if(INDI_FOUND) # include_directories(${INDI_INCLUDE_DIR}) # add_executable(myapp myapp.cpp) # target_link_libraries(myapp ${INDI_LIBRARIES}) # endif(INDI_FOUND) # # # Using Components: # # You can search for specific components. Currently, the following components are available # * driver # * align # * client # * clientqt5 # # By default, if you do not specify any components, driver and align components are searched. # # Example: # # To use INDI Qt5 Client library only in your application: # # find_package(INDI CLIENTQT5 REQUIRED) # # if(INDI_FOUND) # include_directories(${INDI_INCLUDE_DIR}) # add_executable(myapp myapp.cpp) # target_link_libraries(myapp ${INDI_CLIENTQT5_LIBRARIES}) # endif(INDI_FOUND) # #============================================================================= # Copyright (c) 2011-2013, julp # Copyright (c) 2017 Jasem Mutlaq # # Distributed under the OSI-approved BSD License # # This software is distributed WITHOUT ANY WARRANTY; without even the # implied warranty of MERCHANTABILITY or FITNESS FOR A PARTINDILAR PURPOSE. #============================================================================= find_package(PkgConfig QUIET) ########## Private ########## if(NOT DEFINED INDI_PUBLIC_VAR_NS) set(INDI_PUBLIC_VAR_NS "INDI") # Prefix for all INDI relative public variables endif(NOT DEFINED INDI_PUBLIC_VAR_NS) if(NOT DEFINED INDI_PRIVATE_VAR_NS) set(INDI_PRIVATE_VAR_NS "_${INDI_PUBLIC_VAR_NS}") # Prefix for all INDI relative internal variables endif(NOT DEFINED INDI_PRIVATE_VAR_NS) if(NOT DEFINED PC_INDI_PRIVATE_VAR_NS) set(PC_INDI_PRIVATE_VAR_NS "_PC${INDI_PRIVATE_VAR_NS}") # Prefix for all pkg-config relative internal variables endif(NOT DEFINED PC_INDI_PRIVATE_VAR_NS) function(indidebug _VARNAME) if(${INDI_PUBLIC_VAR_NS}_DEBUG) if(DEFINED ${INDI_PUBLIC_VAR_NS}_${_VARNAME}) message("${INDI_PUBLIC_VAR_NS}_${_VARNAME} = ${${INDI_PUBLIC_VAR_NS}_${_VARNAME}}") else(DEFINED ${INDI_PUBLIC_VAR_NS}_${_VARNAME}) message("${INDI_PUBLIC_VAR_NS}_${_VARNAME} = ") endif(DEFINED ${INDI_PUBLIC_VAR_NS}_${_VARNAME}) endif(${INDI_PUBLIC_VAR_NS}_DEBUG) endfunction(indidebug) set(${INDI_PRIVATE_VAR_NS}_ROOT "") if(DEFINED ENV{INDI_ROOT}) set(${INDI_PRIVATE_VAR_NS}_ROOT "$ENV{INDI_ROOT}") endif(DEFINED ENV{INDI_ROOT}) if (DEFINED INDI_ROOT) set(${INDI_PRIVATE_VAR_NS}_ROOT "${INDI_ROOT}") endif(DEFINED INDI_ROOT) set(${INDI_PRIVATE_VAR_NS}_BIN_SUFFIXES ) set(${INDI_PRIVATE_VAR_NS}_LIB_SUFFIXES ) if(CMAKE_SIZEOF_VOID_P EQUAL 8) list(APPEND ${INDI_PRIVATE_VAR_NS}_BIN_SUFFIXES "bin64") list(APPEND ${INDI_PRIVATE_VAR_NS}_LIB_SUFFIXES "lib64") endif(CMAKE_SIZEOF_VOID_P EQUAL 8) list(APPEND ${INDI_PRIVATE_VAR_NS}_BIN_SUFFIXES "bin") list(APPEND ${INDI_PRIVATE_VAR_NS}_LIB_SUFFIXES "lib") set(${INDI_PRIVATE_VAR_NS}_COMPONENTS ) # ... macro(INDI_declare_component _NAME) list(APPEND ${INDI_PRIVATE_VAR_NS}_COMPONENTS ${_NAME}) set("${INDI_PRIVATE_VAR_NS}_COMPONENTS_${_NAME}" ${ARGN}) endmacro(INDI_declare_component) INDI_declare_component(driver indidriver) INDI_declare_component(align indiAlignmentDriver) INDI_declare_component(client indiclient) INDI_declare_component(clientqt5 indiclientqt5) ########## Public ########## set(${INDI_PUBLIC_VAR_NS}_FOUND TRUE) set(${INDI_PUBLIC_VAR_NS}_LIBRARIES ) set(${INDI_PUBLIC_VAR_NS}_INCLUDE_DIR ) foreach(${INDI_PRIVATE_VAR_NS}_COMPONENT ${${INDI_PRIVATE_VAR_NS}_COMPONENTS}) string(TOUPPER "${${INDI_PRIVATE_VAR_NS}_COMPONENT}" ${INDI_PRIVATE_VAR_NS}_UPPER_COMPONENT) set("${INDI_PUBLIC_VAR_NS}_${${INDI_PRIVATE_VAR_NS}_UPPER_COMPONENT}_FOUND" FALSE) # may be done in the INDI_declare_component macro endforeach(${INDI_PRIVATE_VAR_NS}_COMPONENT) # Check components if(NOT ${INDI_PUBLIC_VAR_NS}_FIND_COMPONENTS) # driver and posix client by default set(${INDI_PUBLIC_VAR_NS}_FIND_COMPONENTS driver align) else(NOT ${INDI_PUBLIC_VAR_NS}_FIND_COMPONENTS) #list(APPEND ${INDI_PUBLIC_VAR_NS}_FIND_COMPONENTS uc) list(REMOVE_DUPLICATES ${INDI_PUBLIC_VAR_NS}_FIND_COMPONENTS) foreach(${INDI_PRIVATE_VAR_NS}_COMPONENT ${${INDI_PUBLIC_VAR_NS}_FIND_COMPONENTS}) if(NOT DEFINED ${INDI_PRIVATE_VAR_NS}_COMPONENTS_${${INDI_PRIVATE_VAR_NS}_COMPONENT}) message(FATAL_ERROR "Unknown INDI component: ${${INDI_PRIVATE_VAR_NS}_COMPONENT}") endif(NOT DEFINED ${INDI_PRIVATE_VAR_NS}_COMPONENTS_${${INDI_PRIVATE_VAR_NS}_COMPONENT}) endforeach(${INDI_PRIVATE_VAR_NS}_COMPONENT) endif(NOT ${INDI_PUBLIC_VAR_NS}_FIND_COMPONENTS) # Includes find_path( ${INDI_PUBLIC_VAR_NS}_INCLUDE_DIR indidevapi.h PATH_SUFFIXES libindi ${PC_INDI_INCLUDE_DIR} ${_obIncDir} ${GNUWIN32_DIR}/include HINTS ${${INDI_PRIVATE_VAR_NS}_ROOT} DOC "Include directory for INDI" ) find_path(${INDI_PUBLIC_VAR_NS}_DATA_DIR drivers.xml PATH_SUFFIXES share/indi DOC "Data directory for INDI" ) if(${INDI_PUBLIC_VAR_NS}_INCLUDE_DIR) if(EXISTS "${${INDI_PUBLIC_VAR_NS}_INCLUDE_DIR}/indiversion.h") # INDI >= 1.4 file(READ "${${INDI_PUBLIC_VAR_NS}_INCLUDE_DIR}/indiversion.h" ${INDI_PRIVATE_VAR_NS}_VERSION_HEADER_CONTENTS) else() message(FATAL_ERROR "INDI version header not found") endif() if(${INDI_PRIVATE_VAR_NS}_VERSION_HEADER_CONTENTS MATCHES ".*INDI_VERSION ([0-9]+).([0-9]+).([0-9]+)") set(${INDI_PUBLIC_VAR_NS}_MAJOR_VERSION "${CMAKE_MATCH_1}") set(${INDI_PUBLIC_VAR_NS}_MINOR_VERSION "${CMAKE_MATCH_2}") set(${INDI_PUBLIC_VAR_NS}_RELEASE_VERSION "${CMAKE_MATCH_3}") else() message(FATAL_ERROR "failed to detect INDI version") endif() set(${INDI_PUBLIC_VAR_NS}_VERSION "${${INDI_PUBLIC_VAR_NS}_MAJOR_VERSION}.${${INDI_PUBLIC_VAR_NS}_MINOR_VERSION}.${${INDI_PUBLIC_VAR_NS}_RELEASE_VERSION}") # Check libraries foreach(${INDI_PRIVATE_VAR_NS}_COMPONENT ${${INDI_PUBLIC_VAR_NS}_FIND_COMPONENTS}) set(${INDI_PRIVATE_VAR_NS}_POSSIBLE_RELEASE_NAMES ) set(${INDI_PRIVATE_VAR_NS}_POSSIBLE_DEBUG_NAMES ) foreach(${INDI_PRIVATE_VAR_NS}_BASE_NAME ${${INDI_PRIVATE_VAR_NS}_COMPONENTS_${${INDI_PRIVATE_VAR_NS}_COMPONENT}}) list(APPEND ${INDI_PRIVATE_VAR_NS}_POSSIBLE_RELEASE_NAMES "${${INDI_PRIVATE_VAR_NS}_BASE_NAME}") list(APPEND ${INDI_PRIVATE_VAR_NS}_POSSIBLE_DEBUG_NAMES "${${INDI_PRIVATE_VAR_NS}_BASE_NAME}d") list(APPEND ${INDI_PRIVATE_VAR_NS}_POSSIBLE_RELEASE_NAMES "${${INDI_PRIVATE_VAR_NS}_BASE_NAME}${INDI_MAJOR_VERSION}${INDI_MINOR_VERSION}") list(APPEND ${INDI_PRIVATE_VAR_NS}_POSSIBLE_DEBUG_NAMES "${${INDI_PRIVATE_VAR_NS}_BASE_NAME}${INDI_MAJOR_VERSION}${INDI_MINOR_VERSION}d") endforeach(${INDI_PRIVATE_VAR_NS}_BASE_NAME) find_library( ${INDI_PRIVATE_VAR_NS}_LIB_RELEASE_${${INDI_PRIVATE_VAR_NS}_COMPONENT} NAMES ${${INDI_PRIVATE_VAR_NS}_POSSIBLE_RELEASE_NAMES} HINTS ${${INDI_PRIVATE_VAR_NS}_ROOT} PATH_SUFFIXES ${_INDI_LIB_SUFFIXES} DOC "Release libraries for INDI" ) find_library( ${INDI_PRIVATE_VAR_NS}_LIB_DEBUG_${${INDI_PRIVATE_VAR_NS}_COMPONENT} NAMES ${${INDI_PRIVATE_VAR_NS}_POSSIBLE_DEBUG_NAMES} HINTS ${${INDI_PRIVATE_VAR_NS}_ROOT} PATH_SUFFIXES ${_INDI_LIB_SUFFIXES} DOC "Debug libraries for INDI" ) string(TOUPPER "${${INDI_PRIVATE_VAR_NS}_COMPONENT}" ${INDI_PRIVATE_VAR_NS}_UPPER_COMPONENT) if(NOT ${INDI_PRIVATE_VAR_NS}_LIB_RELEASE_${${INDI_PRIVATE_VAR_NS}_COMPONENT} AND NOT ${INDI_PRIVATE_VAR_NS}_LIB_DEBUG_${${INDI_PRIVATE_VAR_NS}_COMPONENT}) # both not found set("${INDI_PUBLIC_VAR_NS}_${${INDI_PRIVATE_VAR_NS}_UPPER_COMPONENT}_FOUND" FALSE) set("${INDI_PUBLIC_VAR_NS}_FOUND" FALSE) else(NOT ${INDI_PRIVATE_VAR_NS}_LIB_RELEASE_${${INDI_PRIVATE_VAR_NS}_COMPONENT} AND NOT ${INDI_PRIVATE_VAR_NS}_LIB_DEBUG_${${INDI_PRIVATE_VAR_NS}_COMPONENT}) # one or both found set("${INDI_PUBLIC_VAR_NS}_${${INDI_PRIVATE_VAR_NS}_UPPER_COMPONENT}_FOUND" TRUE) if(NOT ${INDI_PRIVATE_VAR_NS}_LIB_RELEASE_${${INDI_PRIVATE_VAR_NS}_COMPONENT}) # release not found => we are in debug set(${INDI_PRIVATE_VAR_NS}_LIB_${${INDI_PRIVATE_VAR_NS}_COMPONENT} "${${INDI_PRIVATE_VAR_NS}_LIB_DEBUG_${${INDI_PRIVATE_VAR_NS}_COMPONENT}}") elseif(NOT ${INDI_PRIVATE_VAR_NS}_LIB_DEBUG_${${INDI_PRIVATE_VAR_NS}_COMPONENT}) # debug not found => we are in release set(${INDI_PRIVATE_VAR_NS}_LIB_${${INDI_PRIVATE_VAR_NS}_COMPONENT} "${${INDI_PRIVATE_VAR_NS}_LIB_RELEASE_${${INDI_PRIVATE_VAR_NS}_COMPONENT}}") else() # both found set( ${INDI_PRIVATE_VAR_NS}_LIB_${${INDI_PRIVATE_VAR_NS}_COMPONENT} optimized ${${INDI_PRIVATE_VAR_NS}_LIB_RELEASE_${${INDI_PRIVATE_VAR_NS}_COMPONENT}} debug ${${INDI_PRIVATE_VAR_NS}_LIB_DEBUG_${${INDI_PRIVATE_VAR_NS}_COMPONENT}} ) endif() list(APPEND ${INDI_PUBLIC_VAR_NS}_LIBRARIES ${${INDI_PRIVATE_VAR_NS}_LIB_${${INDI_PRIVATE_VAR_NS}_COMPONENT}}) endif(NOT ${INDI_PRIVATE_VAR_NS}_LIB_RELEASE_${${INDI_PRIVATE_VAR_NS}_COMPONENT} AND NOT ${INDI_PRIVATE_VAR_NS}_LIB_DEBUG_${${INDI_PRIVATE_VAR_NS}_COMPONENT}) endforeach(${INDI_PRIVATE_VAR_NS}_COMPONENT) # Check find_package arguments include(FindPackageHandleStandardArgs) if(${INDI_PUBLIC_VAR_NS}_FIND_REQUIRED AND NOT ${INDI_PUBLIC_VAR_NS}_FIND_QUIETLY) find_package_handle_standard_args( ${INDI_PUBLIC_VAR_NS} REQUIRED_VARS ${INDI_PUBLIC_VAR_NS}_LIBRARIES ${INDI_PUBLIC_VAR_NS}_INCLUDE_DIR VERSION_VAR ${INDI_PUBLIC_VAR_NS}_VERSION ) else(${INDI_PUBLIC_VAR_NS}_FIND_REQUIRED AND NOT ${INDI_PUBLIC_VAR_NS}_FIND_QUIETLY) find_package_handle_standard_args(${INDI_PUBLIC_VAR_NS} "INDI not found" ${INDI_PUBLIC_VAR_NS}_LIBRARIES ${INDI_PUBLIC_VAR_NS}_INCLUDE_DIR) endif(${INDI_PUBLIC_VAR_NS}_FIND_REQUIRED AND NOT ${INDI_PUBLIC_VAR_NS}_FIND_QUIETLY) else(${INDI_PUBLIC_VAR_NS}_INCLUDE_DIR) set("${INDI_PUBLIC_VAR_NS}_FOUND" FALSE) if(${INDI_PUBLIC_VAR_NS}_FIND_REQUIRED AND NOT ${INDI_PUBLIC_VAR_NS}_FIND_QUIETLY) message(FATAL_ERROR "Could not find INDI include directory") endif(${INDI_PUBLIC_VAR_NS}_FIND_REQUIRED AND NOT ${INDI_PUBLIC_VAR_NS}_FIND_QUIETLY) endif(${INDI_PUBLIC_VAR_NS}_INCLUDE_DIR) mark_as_advanced( ${INDI_PUBLIC_VAR_NS}_INCLUDE_DIR ${INDI_PUBLIC_VAR_NS}_LIBRARIES ) # IN (args) indidebug("FIND_COMPONENTS") indidebug("FIND_REQUIRED") indidebug("FIND_QUIETLY") indidebug("FIND_VERSION") # OUT # Found indidebug("FOUND") indidebug("SERVER_FOUND") indidebug("DRIVERS_FOUND") indidebug("CLIENT_FOUND") indidebug("QT5CLIENT_FOUND") # Linking indidebug("INCLUDE_DIR") indidebug("DATA_DIR") indidebug("LIBRARIES") # Backward compatibility set(${INDI_PUBLIC_VAR_NS}_DRIVER_LIBRARIES ${${INDI_PUBLIC_VAR_NS}_LIBRARIES}) indidebug("DRIVER_LIBRARIES") # Version indidebug("MAJOR_VERSION") indidebug("MINOR_VERSION") indidebug("RELEASE_VERSION") indidebug("VERSION") libindi/cmake_modules/FindPackageHandleStandardArgs.cmake0000664000175000017500000003565713263645566023072 0ustar jasemjasem#[=======================================================================[.rst: FindPackageHandleStandardArgs ----------------------------- This module provides a function intended to be used in :ref:`Find Modules` implementing :command:`find_package()` calls. It handles the ``REQUIRED``, ``QUIET`` and version-related arguments of ``find_package``. It also sets the ``_FOUND`` variable. The package is considered found if all variables listed contain valid results, e.g. valid filepaths. .. command:: find_package_handle_standard_args There are two signatures:: find_package_handle_standard_args( (DEFAULT_MSG|) ... ) find_package_handle_standard_args( [FOUND_VAR ] [REQUIRED_VARS ...] [VERSION_VAR ] [HANDLE_COMPONENTS] [CONFIG_MODE] [FAIL_MESSAGE ] ) The ``_FOUND`` variable will be set to ``TRUE`` if all the variables ``...`` are valid and any optional constraints are satisfied, and ``FALSE`` otherwise. A success or failure message may be displayed based on the results and on whether the ``REQUIRED`` and/or ``QUIET`` option was given to the :command:`find_package` call. The options are: ``(DEFAULT_MSG|)`` In the simple signature this specifies the failure message. Use ``DEFAULT_MSG`` to ask for a default message to be computed (recommended). Not valid in the full signature. ``FOUND_VAR `` Obsolete. Specifies either ``_FOUND`` or ``_FOUND`` as the result variable. This exists only for compatibility with older versions of CMake and is now ignored. Result variables of both names are always set for compatibility. ``REQUIRED_VARS ...`` Specify the variables which are required for this package. These may be named in the generated failure message asking the user to set the missing variable values. Therefore these should typically be cache entries such as ``FOO_LIBRARY`` and not output variables like ``FOO_LIBRARIES``. ``VERSION_VAR `` Specify the name of a variable that holds the version of the package that has been found. This version will be checked against the (potentially) specified required version given to the :command:`find_package` call, including its ``EXACT`` option. The default messages include information about the required version and the version which has been actually found, both if the version is ok or not. ``HANDLE_COMPONENTS`` Enable handling of package components. In this case, the command will report which components have been found and which are missing, and the ``_FOUND`` variable will be set to ``FALSE`` if any of the required components (i.e. not the ones listed after the ``OPTIONAL_COMPONENTS`` option of :command:`find_package`) are missing. ``CONFIG_MODE`` Specify that the calling find module is a wrapper around a call to ``find_package( NO_MODULE)``. This implies a ``VERSION_VAR`` value of ``_VERSION``. The command will automatically check whether the package configuration file was found. ``FAIL_MESSAGE `` Specify a custom failure message instead of using the default generated message. Not recommended. Example for the simple signature: .. code-block:: cmake find_package_handle_standard_args(LibXml2 DEFAULT_MSG LIBXML2_LIBRARY LIBXML2_INCLUDE_DIR) The ``LibXml2`` package is considered to be found if both ``LIBXML2_LIBRARY`` and ``LIBXML2_INCLUDE_DIR`` are valid. Then also ``LibXml2_FOUND`` is set to ``TRUE``. If it is not found and ``REQUIRED`` was used, it fails with a :command:`message(FATAL_ERROR)`, independent whether ``QUIET`` was used or not. If it is found, success will be reported, including the content of the first ````. On repeated CMake runs, the same message will not be printed again. Example for the full signature: .. code-block:: cmake find_package_handle_standard_args(LibArchive REQUIRED_VARS LibArchive_LIBRARY LibArchive_INCLUDE_DIR VERSION_VAR LibArchive_VERSION) In this case, the ``LibArchive`` package is considered to be found if both ``LibArchive_LIBRARY`` and ``LibArchive_INCLUDE_DIR`` are valid. Also the version of ``LibArchive`` will be checked by using the version contained in ``LibArchive_VERSION``. Since no ``FAIL_MESSAGE`` is given, the default messages will be printed. Another example for the full signature: .. code-block:: cmake find_package(Automoc4 QUIET NO_MODULE HINTS /opt/automoc4) find_package_handle_standard_args(Automoc4 CONFIG_MODE) In this case, a ``FindAutmoc4.cmake`` module wraps a call to ``find_package(Automoc4 NO_MODULE)`` and adds an additional search directory for ``automoc4``. Then the call to ``find_package_handle_standard_args`` produces a proper success/failure message. #]=======================================================================] #============================================================================= # Copyright 2007-2009 Kitware, Inc. # # Distributed under the OSI-approved BSD License (the "License"); # see accompanying file Copyright.txt for details. # # This software is distributed WITHOUT ANY WARRANTY; without even the # implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. # See the License for more information. #============================================================================= # (To distribute this file outside of CMake, substitute the full # License text for the above reference.) include(${CMAKE_CURRENT_LIST_DIR}/FindPackageMessage.cmake) include(${CMAKE_CURRENT_LIST_DIR}/CMakeParseArguments.cmake) # internal helper macro macro(_FPHSA_FAILURE_MESSAGE _msg) if (${_NAME}_FIND_REQUIRED) message(FATAL_ERROR "${_msg}") else () if (NOT ${_NAME}_FIND_QUIETLY) message(STATUS "${_msg}") endif () endif () endmacro() # internal helper macro to generate the failure message when used in CONFIG_MODE: macro(_FPHSA_HANDLE_FAILURE_CONFIG_MODE) # _CONFIG is set, but FOUND is false, this means that some other of the REQUIRED_VARS was not found: if(${_NAME}_CONFIG) _FPHSA_FAILURE_MESSAGE("${FPHSA_FAIL_MESSAGE}: missing: ${MISSING_VARS} (found ${${_NAME}_CONFIG} ${VERSION_MSG})") else() # If _CONSIDERED_CONFIGS is set, the config-file has been found, but no suitable version. # List them all in the error message: if(${_NAME}_CONSIDERED_CONFIGS) set(configsText "") list(LENGTH ${_NAME}_CONSIDERED_CONFIGS configsCount) math(EXPR configsCount "${configsCount} - 1") foreach(currentConfigIndex RANGE ${configsCount}) list(GET ${_NAME}_CONSIDERED_CONFIGS ${currentConfigIndex} filename) list(GET ${_NAME}_CONSIDERED_VERSIONS ${currentConfigIndex} version) set(configsText "${configsText} ${filename} (version ${version})\n") endforeach() if (${_NAME}_NOT_FOUND_MESSAGE) set(configsText "${configsText} Reason given by package: ${${_NAME}_NOT_FOUND_MESSAGE}\n") endif() _FPHSA_FAILURE_MESSAGE("${FPHSA_FAIL_MESSAGE} ${VERSION_MSG}, checked the following files:\n${configsText}") else() # Simple case: No Config-file was found at all: _FPHSA_FAILURE_MESSAGE("${FPHSA_FAIL_MESSAGE}: found neither ${_NAME}Config.cmake nor ${_NAME_LOWER}-config.cmake ${VERSION_MSG}") endif() endif() endmacro() function(FIND_PACKAGE_HANDLE_STANDARD_ARGS _NAME _FIRST_ARG) # set up the arguments for CMAKE_PARSE_ARGUMENTS and check whether we are in # new extended or in the "old" mode: set(options CONFIG_MODE HANDLE_COMPONENTS) set(oneValueArgs FAIL_MESSAGE VERSION_VAR FOUND_VAR) set(multiValueArgs REQUIRED_VARS) set(_KEYWORDS_FOR_EXTENDED_MODE ${options} ${oneValueArgs} ${multiValueArgs} ) list(FIND _KEYWORDS_FOR_EXTENDED_MODE "${_FIRST_ARG}" INDEX) if(${INDEX} EQUAL -1) set(FPHSA_FAIL_MESSAGE ${_FIRST_ARG}) set(FPHSA_REQUIRED_VARS ${ARGN}) set(FPHSA_VERSION_VAR) else() CMAKE_PARSE_ARGUMENTS(FPHSA "${options}" "${oneValueArgs}" "${multiValueArgs}" ${_FIRST_ARG} ${ARGN}) if(FPHSA_UNPARSED_ARGUMENTS) message(FATAL_ERROR "Unknown keywords given to FIND_PACKAGE_HANDLE_STANDARD_ARGS(): \"${FPHSA_UNPARSED_ARGUMENTS}\"") endif() if(NOT FPHSA_FAIL_MESSAGE) set(FPHSA_FAIL_MESSAGE "DEFAULT_MSG") endif() endif() # now that we collected all arguments, process them if("x${FPHSA_FAIL_MESSAGE}" STREQUAL "xDEFAULT_MSG") set(FPHSA_FAIL_MESSAGE "Could NOT find ${_NAME}") endif() # In config-mode, we rely on the variable _CONFIG, which is set by find_package() # when it successfully found the config-file, including version checking: if(FPHSA_CONFIG_MODE) list(INSERT FPHSA_REQUIRED_VARS 0 ${_NAME}_CONFIG) list(REMOVE_DUPLICATES FPHSA_REQUIRED_VARS) set(FPHSA_VERSION_VAR ${_NAME}_VERSION) endif() if(NOT FPHSA_REQUIRED_VARS) message(FATAL_ERROR "No REQUIRED_VARS specified for FIND_PACKAGE_HANDLE_STANDARD_ARGS()") endif() list(GET FPHSA_REQUIRED_VARS 0 _FIRST_REQUIRED_VAR) string(TOUPPER ${_NAME} _NAME_UPPER) string(TOLOWER ${_NAME} _NAME_LOWER) if(FPHSA_FOUND_VAR) if(FPHSA_FOUND_VAR MATCHES "^${_NAME}_FOUND$" OR FPHSA_FOUND_VAR MATCHES "^${_NAME_UPPER}_FOUND$") set(_FOUND_VAR ${FPHSA_FOUND_VAR}) else() message(FATAL_ERROR "The argument for FOUND_VAR is \"${FPHSA_FOUND_VAR}\", but only \"${_NAME}_FOUND\" and \"${_NAME_UPPER}_FOUND\" are valid names.") endif() else() set(_FOUND_VAR ${_NAME_UPPER}_FOUND) endif() # collect all variables which were not found, so they can be printed, so the # user knows better what went wrong (#6375) set(MISSING_VARS "") set(DETAILS "") # check if all passed variables are valid set(FPHSA_FOUND_${_NAME} TRUE) foreach(_CURRENT_VAR ${FPHSA_REQUIRED_VARS}) if(NOT ${_CURRENT_VAR}) set(FPHSA_FOUND_${_NAME} FALSE) set(MISSING_VARS "${MISSING_VARS} ${_CURRENT_VAR}") else() set(DETAILS "${DETAILS}[${${_CURRENT_VAR}}]") endif() endforeach() if(FPHSA_FOUND_${_NAME}) set(${_NAME}_FOUND TRUE) set(${_NAME_UPPER}_FOUND TRUE) else() set(${_NAME}_FOUND FALSE) set(${_NAME_UPPER}_FOUND FALSE) endif() # component handling unset(FOUND_COMPONENTS_MSG) unset(MISSING_COMPONENTS_MSG) if(FPHSA_HANDLE_COMPONENTS) foreach(comp ${${_NAME}_FIND_COMPONENTS}) if(${_NAME}_${comp}_FOUND) if(NOT DEFINED FOUND_COMPONENTS_MSG) set(FOUND_COMPONENTS_MSG "found components: ") endif() set(FOUND_COMPONENTS_MSG "${FOUND_COMPONENTS_MSG} ${comp}") else() if(NOT DEFINED MISSING_COMPONENTS_MSG) set(MISSING_COMPONENTS_MSG "missing components: ") endif() set(MISSING_COMPONENTS_MSG "${MISSING_COMPONENTS_MSG} ${comp}") if(${_NAME}_FIND_REQUIRED_${comp}) set(${_NAME}_FOUND FALSE) set(MISSING_VARS "${MISSING_VARS} ${comp}") endif() endif() endforeach() set(COMPONENT_MSG "${FOUND_COMPONENTS_MSG} ${MISSING_COMPONENTS_MSG}") set(DETAILS "${DETAILS}[c${COMPONENT_MSG}]") endif() # version handling: set(VERSION_MSG "") set(VERSION_OK TRUE) set(VERSION ${${FPHSA_VERSION_VAR}}) # check with DEFINED here as the requested or found version may be "0" if (DEFINED ${_NAME}_FIND_VERSION) if(DEFINED ${FPHSA_VERSION_VAR}) if(${_NAME}_FIND_VERSION_EXACT) # exact version required # count the dots in the version string string(REGEX REPLACE "[^.]" "" _VERSION_DOTS "${VERSION}") # add one dot because there is one dot more than there are components string(LENGTH "${_VERSION_DOTS}." _VERSION_DOTS) if (_VERSION_DOTS GREATER ${_NAME}_FIND_VERSION_COUNT) # Because of the C++ implementation of find_package() ${_NAME}_FIND_VERSION_COUNT # is at most 4 here. Therefore a simple lookup table is used. if (${_NAME}_FIND_VERSION_COUNT EQUAL 1) set(_VERSION_REGEX "[^.]*") elseif (${_NAME}_FIND_VERSION_COUNT EQUAL 2) set(_VERSION_REGEX "[^.]*\\.[^.]*") elseif (${_NAME}_FIND_VERSION_COUNT EQUAL 3) set(_VERSION_REGEX "[^.]*\\.[^.]*\\.[^.]*") else () set(_VERSION_REGEX "[^.]*\\.[^.]*\\.[^.]*\\.[^.]*") endif () string(REGEX REPLACE "^(${_VERSION_REGEX})\\..*" "\\1" _VERSION_HEAD "${VERSION}") unset(_VERSION_REGEX) if (NOT ${_NAME}_FIND_VERSION VERSION_EQUAL _VERSION_HEAD) set(VERSION_MSG "Found unsuitable version \"${VERSION}\", but required is exact version \"${${_NAME}_FIND_VERSION}\"") set(VERSION_OK FALSE) else () set(VERSION_MSG "(found suitable exact version \"${VERSION}\")") endif () unset(_VERSION_HEAD) else () if (NOT ${_NAME}_FIND_VERSION VERSION_EQUAL VERSION) set(VERSION_MSG "Found unsuitable version \"${VERSION}\", but required is exact version \"${${_NAME}_FIND_VERSION}\"") set(VERSION_OK FALSE) else () set(VERSION_MSG "(found suitable exact version \"${VERSION}\")") endif () endif () unset(_VERSION_DOTS) else() # minimum version specified: if (${_NAME}_FIND_VERSION VERSION_GREATER VERSION) set(VERSION_MSG "Found unsuitable version \"${VERSION}\", but required is at least \"${${_NAME}_FIND_VERSION}\"") set(VERSION_OK FALSE) else () set(VERSION_MSG "(found suitable version \"${VERSION}\", minimum required is \"${${_NAME}_FIND_VERSION}\")") endif () endif() else() # if the package was not found, but a version was given, add that to the output: if(${_NAME}_FIND_VERSION_EXACT) set(VERSION_MSG "(Required is exact version \"${${_NAME}_FIND_VERSION}\")") else() set(VERSION_MSG "(Required is at least version \"${${_NAME}_FIND_VERSION}\")") endif() endif() else () if(VERSION) set(VERSION_MSG "(found version \"${VERSION}\")") endif() endif () if(VERSION_OK) set(DETAILS "${DETAILS}[v${VERSION}(${${_NAME}_FIND_VERSION})]") else() set(${_NAME}_FOUND FALSE) endif() # print the result: if (${_NAME}_FOUND) FIND_PACKAGE_MESSAGE(${_NAME} "Found ${_NAME}: ${${_FIRST_REQUIRED_VAR}} ${VERSION_MSG} ${COMPONENT_MSG}" "${DETAILS}") else () if(FPHSA_CONFIG_MODE) _FPHSA_HANDLE_FAILURE_CONFIG_MODE() else() if(NOT VERSION_OK) _FPHSA_FAILURE_MESSAGE("${FPHSA_FAIL_MESSAGE}: ${VERSION_MSG} (found ${${_FIRST_REQUIRED_VAR}})") else() _FPHSA_FAILURE_MESSAGE("${FPHSA_FAIL_MESSAGE} (missing: ${MISSING_VARS}) ${VERSION_MSG}") endif() endif() endif () set(${_NAME}_FOUND ${${_NAME}_FOUND} PARENT_SCOPE) set(${_NAME_UPPER}_FOUND ${${_NAME}_FOUND} PARENT_SCOPE) endfunction() libindi/cmake_modules/FindINOVASDK.cmake0000664000175000017500000000275313263645566017332 0ustar jasemjasem# - Try to find INOVASDK Universal Library # Once done this will define # # INOVASDK_FOUND - system has INOVASDK # INOVASDK_INCLUDE_DIR - the INOVASDK include directory # INOVASDK_LIBRARIES - Link these to use INOVASDK # Redistribution and use is allowed according to the terms of the BSD license. # For details see the accompanying COPYING-CMAKE-SCRIPTS file. if (INOVASDK_INCLUDE_DIR AND INOVASDK_LIBRARIES) # in cache already set(INOVASDK_FOUND TRUE) message(STATUS "Found libinovasdk: ${INOVASDK_LIBRARIES}") else (INOVASDK_INCLUDE_DIR AND INOVASDK_LIBRARIES) find_path(INOVASDK_INCLUDE_DIR inovasdk.h PATH_SUFFIXES inovasdk ${_obIncDir} ${GNUWIN32_DIR}/include ) find_library(INOVASDK_LIBRARIES NAMES inovasdk PATHS ${_obLinkDir} ${GNUWIN32_DIR}/lib ) if(INOVASDK_INCLUDE_DIR AND INOVASDK_LIBRARIES) set(INOVASDK_FOUND TRUE) else (INOVASDK_INCLUDE_DIR AND INOVASDK_LIBRARIES) set(INOVASDK_FOUND FALSE) endif(INOVASDK_INCLUDE_DIR AND INOVASDK_LIBRARIES) if (INOVASDK_FOUND) if (NOT INOVASDK_FIND_QUIETLY) message(STATUS "Found INOVASDK: ${INOVASDK_LIBRARIES}") endif (NOT INOVASDK_FIND_QUIETLY) else (INOVASDK_FOUND) if (INOVASDK_FIND_REQUIRED) message(FATAL_ERROR "INOVASDK not found. Please install INOVASDK Library http://www.indilib.org") endif (INOVASDK_FIND_REQUIRED) endif (INOVASDK_FOUND) mark_as_advanced(INOVASDK_INCLUDE_DIR INOVASDK_LIBRARIES) endif (INOVASDK_INCLUDE_DIR AND INOVASDK_LIBRARIES) libindi/cmake_modules/FindQSI.cmake0000664000175000017500000000245513263645566016547 0ustar jasemjasem# - Try to find Quantum Scientific Imaging Library # Once done this will define # # QSI_FOUND - system has QSI # QSI_INCLUDE_DIR - the QSI include directory # QSI_LIBRARIES - Link these to use QSI # Redistribution and use is allowed according to the terms of the BSD license. # For details see the accompanying COPYING-CMAKE-SCRIPTS file. if (QSI_INCLUDE_DIR AND QSI_LIBRARIES) # in cache already set(QSI_FOUND TRUE) message(STATUS "Found libqsiapi: ${QSI_LIBRARIES}") else (QSI_INCLUDE_DIR AND QSI_LIBRARIES) find_path(QSI_INCLUDE_DIR qsiapi.h PATH_SUFFIXES qsiapi ${_obIncDir} ${GNUWIN32_DIR}/include ) find_library(QSI_LIBRARIES NAMES qsiapi PATHS ${_obLinkDir} ${GNUWIN32_DIR}/lib ) if(QSI_INCLUDE_DIR AND QSI_LIBRARIES) set(QSI_FOUND TRUE) else (QSI_INCLUDE_DIR AND QSI_LIBRARIES) set(QSI_FOUND FALSE) endif(QSI_INCLUDE_DIR AND QSI_LIBRARIES) if (QSI_FOUND) if (NOT QSI_FIND_QUIETLY) message(STATUS "Found QSI: ${QSI_LIBRARIES}") endif (NOT QSI_FIND_QUIETLY) else (QSI_FOUND) if (QSI_FIND_REQUIRED) message(FATAL_ERROR "QSI not found. Please install libqsi http://www.indilib.org") endif (QSI_FIND_REQUIRED) endif (QSI_FOUND) mark_as_advanced(QSI_INCLUDE_DIR QSI_LIBRARIES) endif (QSI_INCLUDE_DIR AND QSI_LIBRARIES) libindi/cmake_modules/FindLibRaw.cmake0000664000175000017500000000562513263645566017275 0ustar jasemjasem# - Find LibRaw # Find the LibRaw library # This module defines # LibRaw_VERSION_STRING, the version string of LibRaw # LibRaw_INCLUDE_DIR, where to find libraw.h # LibRaw_LIBRARIES, the libraries needed to use LibRaw (non-thread-safe) # LibRaw_r_LIBRARIES, the libraries needed to use LibRaw (thread-safe) # LibRaw_DEFINITIONS, the definitions needed to use LibRaw (non-thread-safe) # LibRaw_r_DEFINITIONS, the definitions needed to use LibRaw (thread-safe) # # Copyright (c) 2013, Pino Toscano # Copyright (c) 2013, Gilles Caulier # # Redistribution and use is allowed according to the terms of the BSD license. # For details see the accompanying COPYING-CMAKE-SCRIPTS file. FIND_PACKAGE(PkgConfig) IF(PKG_CONFIG_FOUND) PKG_CHECK_MODULES(PC_LIBRAW libraw) SET(LibRaw_DEFINITIONS ${PC_LIBRAW_CFLAGS_OTHER}) PKG_CHECK_MODULES(PC_LIBRAW_R libraw_r) SET(LibRaw_r_DEFINITIONS ${PC_LIBRAW_R_CFLAGS_OTHER}) ENDIF() FIND_PATH(LibRaw_INCLUDE_DIR libraw.h HINTS ${PC_LIBRAW_INCLUDEDIR} ${PC_LibRaw_INCLUDE_DIRS} PATH_SUFFIXES libraw ) FIND_LIBRARY(LibRaw_LIBRARIES NAMES raw HINTS ${PC_LIBRAW_LIBDIR} ${PC_LIBRAW_LIBRARY_DIRS} ) FIND_LIBRARY(LibRaw_r_LIBRARIES NAMES raw_r HINTS ${PC_LIBRAW_R_LIBDIR} ${PC_LIBRAW_R_LIBRARY_DIRS} ) IF(LibRaw_INCLUDE_DIR) FILE(READ ${LibRaw_INCLUDE_DIR}/libraw_version.h _libraw_version_content) STRING(REGEX MATCH "#define LIBRAW_MAJOR_VERSION[ \t]*([0-9]*)\n" _version_major_match ${_libraw_version_content}) SET(_libraw_version_major "${CMAKE_MATCH_1}") STRING(REGEX MATCH "#define LIBRAW_MINOR_VERSION[ \t]*([0-9]*)\n" _version_minor_match ${_libraw_version_content}) SET(_libraw_version_minor "${CMAKE_MATCH_1}") STRING(REGEX MATCH "#define LIBRAW_PATCH_VERSION[ \t]*([0-9]*)\n" _version_patch_match ${_libraw_version_content}) SET(_libraw_version_patch "${CMAKE_MATCH_1}") IF(_version_major_match AND _version_minor_match AND _version_patch_match) SET(LibRaw_VERSION_STRING "${_libraw_version_major}.${_libraw_version_minor}.${_libraw_version_patch}") ELSE() IF(NOT LibRaw_FIND_QUIETLY) MESSAGE(STATUS "Failed to get version information from ${LibRaw_INCLUDE_DIR}/libraw_version.h") ENDIF() ENDIF() ENDIF() INCLUDE(FindPackageHandleStandardArgs) FIND_PACKAGE_HANDLE_STANDARD_ARGS(LibRaw REQUIRED_VARS LibRaw_LIBRARIES LibRaw_INCLUDE_DIR VERSION_VAR LibRaw_VERSION_STRING ) MARK_AS_ADVANCED(LibRaw_VERSION_STRING LibRaw_INCLUDE_DIR LibRaw_LIBRARIES LibRaw_r_LIBRARIES LibRaw_DEFINITIONS LibRaw_r_DEFINITIONS ) libindi/cmake_modules/FindIconv.cmake0000664000175000017500000000426113263645566017166 0ustar jasemjasem# # Copyright (C) 2010 Michael Bell # 2015-2016 MariaDB Corporation AB # # Redistribution and use is allowed according to the terms of the New # BSD license. # For details see the COPYING-CMAKE-SCRIPTS file. # # ICONV_EXTERNAL - Iconv is an external library (not libc) # ICONV_FOUND - system has Iconv # ICONV_INCLUDE_DIR - the Iconv include directory # ICONV_LIBRARIES - Link these to use Iconv # ICONV_SECOND_ARGUMENT_IS_CONST - the second argument for iconv() is const # ICONV_VERSION - Iconv version string if (ICONV_INCLUDE_DIR AND ICONV_LIBRARIES) # Already in cache, be silent set(ICONV_FIND_QUIETLY TRUE) endif (ICONV_INCLUDE_DIR AND ICONV_LIBRARIES) find_path(ICONV_INCLUDE_DIR iconv.h) IF(CMAKE_SYSTEM_NAME MATCHES "SunOS") # There is some libiconv.so in /usr/local that must # be avoided, iconv routines are in libc ELSEIF(APPLE) find_library(ICONV_LIBRARIES NAMES iconv libiconv PATHS /usr/lib/ NO_CMAKE_SYSTEM_PATH) SET(ICONV_EXTERNAL TRUE) ELSE() find_library(ICONV_LIBRARIES NAMES iconv libiconv libiconv-2) IF(ICONV_LIBRARIES) SET(ICONV_EXTERNAL TRUE) ENDIF() ENDIF() if (ICONV_INCLUDE_DIR AND ICONV_LIBRARIES) set (ICONV_FOUND TRUE) endif (ICONV_INCLUDE_DIR AND ICONV_LIBRARIES) set(CMAKE_REQUIRED_INCLUDES ${ICONV_INCLUDE_DIR}) IF(ICONV_EXTERNAL) set(CMAKE_REQUIRED_LIBRARIES ${ICONV_LIBRARIES}) ENDIF() if (ICONV_FOUND) include(CheckCSourceCompiles) CHECK_C_SOURCE_COMPILES(" #include int main(){ iconv_t conv = 0; const char* in = 0; size_t ilen = 0; char* out = 0; size_t olen = 0; iconv(conv, &in, &ilen, &out, &olen); return 0; } " ICONV_SECOND_ARGUMENT_IS_CONST ) endif (ICONV_FOUND) set (CMAKE_REQUIRED_INCLUDES) set (CMAKE_REQUIRED_LIBRARIES) if (ICONV_FOUND) if (NOT ICONV_FIND_QUIETLY) message (STATUS "Found Iconv: ${ICONV_LIBRARIES}") endif (NOT ICONV_FIND_QUIETLY) else (ICONV_FOUND) if (Iconv_FIND_REQUIRED) message (FATAL_ERROR "Could not find Iconv") endif (Iconv_FIND_REQUIRED) endif (ICONV_FOUND) MARK_AS_ADVANCED( ICONV_INCLUDE_DIR ICONV_LIBRARIES ICONV_EXTERNAL ICONV_SECOND_ARGUMENT_IS_CONST ) libindi/cmake_modules/FindFTDI1.cmake0000664000175000017500000000267213263645566016723 0ustar jasemjasem# - Try to find FTDI1 # Once done this will define # # FTDI1_FOUND - system has FTDI # FTDI1_INCLUDE_DIR - the FTDI include directory # FTDI1_LIBRARIES - Link these to use FTDI # # N.B. You must include the file as following: # #include # # Redistribution and use is allowed according to the terms of the BSD license. # For details see the accompanying COPYING-CMAKE-SCRIPTS file. if (FTDI1_INCLUDE_DIR AND FTDI1_LIBRARIES) # in cache already set(FTDI1_FOUND TRUE) message(STATUS "Found libftdi1: ${FTDI1_LIBRARIES}") else (FTDI1_INCLUDE_DIR AND FTDI1_LIBRARIES) find_path(FTDI1_INCLUDE_DIR ftdi.h PATH_SUFFIXES libftdi1 ${_obIncDir} ${GNUWIN32_DIR}/include /usr/local/include ) find_library(FTDI1_LIBRARIES NAMES ftdi1 PATHS ${_obLinkDir} ${GNUWIN32_DIR}/lib /usr/local/lib ) if(FTDI1_INCLUDE_DIR AND FTDI1_LIBRARIES) set(FTDI1_FOUND TRUE) else (FTDI1_INCLUDE_DIR AND FTDI1_LIBRARIES) set(FTDI1_FOUND FALSE) endif(FTDI1_INCLUDE_DIR AND FTDI1_LIBRARIES) if (FTDI1_FOUND) if (NOT FTDI1_FIND_QUIETLY) message(STATUS "Found FTDI1: ${FTDI1_LIBRARIES}") endif (NOT FTDI1_FIND_QUIETLY) else (FTDI1_FOUND) if (FTDI1_FIND_REQUIRED) message(FATAL_ERROR "FTDI not found. Please install libftdi1-dev") endif (FTDI1_FIND_REQUIRED) endif (FTDI1_FOUND) mark_as_advanced(FTDI1_INCLUDE_DIR FTDI1_LIBRARIES) endif (FTDI1_INCLUDE_DIR AND FTDI1_LIBRARIES) libindi/cmake_modules/FindQHY.cmake0000664000175000017500000000242613263645566016552 0ustar jasemjasem# - Try to find QHY Library # Once done this will define # # QHY_FOUND - system has QHY # QHY_INCLUDE_DIR - the QHY include directory # QHY_LIBRARIES - Link these to use QHY # Redistribution and use is allowed according to the terms of the BSD license. # For details see the accompanying COPYING-CMAKE-SCRIPTS file. if (QHY_INCLUDE_DIR AND QHY_LIBRARIES) # in cache already set(QHY_FOUND TRUE) message(STATUS "Found libqhyccd: ${QHY_LIBRARIES}") else (QHY_INCLUDE_DIR AND QHY_LIBRARIES) find_path(QHY_INCLUDE_DIR qhyccd.h PATH_SUFFIXES libqhy ${_obIncDir} ${GNUWIN32_DIR}/include ) find_library(QHY_LIBRARIES NAMES qhyccd PATHS ${_obLinkDir} ${GNUWIN32_DIR}/lib ) if(QHY_INCLUDE_DIR AND QHY_LIBRARIES) set(QHY_FOUND TRUE) else (QHY_INCLUDE_DIR AND QHY_LIBRARIES) set(QHY_FOUND FALSE) endif(QHY_INCLUDE_DIR AND QHY_LIBRARIES) if (QHY_FOUND) if (NOT QHY_FIND_QUIETLY) message(STATUS "Found QHY: ${QHY_LIBRARIES}") endif (NOT QHY_FIND_QUIETLY) else (QHY_FOUND) if (QHY_FIND_REQUIRED) message(FATAL_ERROR "QHY not found. Please install libqhy http://www.indilib.org") endif (QHY_FIND_REQUIRED) endif (QHY_FOUND) mark_as_advanced(QHY_INCLUDE_DIR QHY_LIBRARIES) endif (QHY_INCLUDE_DIR AND QHY_LIBRARIES) libindi/cmake_modules/FindAPOGEE.cmake0000664000175000017500000000311513263645566017045 0ustar jasemjasem# - Try to find Apogee Instruments Library # Once done this will define # # APOGEE_FOUND - system has APOGEE # APOGEE_INCLUDE_DIR - the APOGEE include directory # APOGEE_LIBRARY - Link these to use APOGEE # Copyright (c) 2008, Jasem Mutlaq # Based on FindLibfacile by Carsten Niehaus, # # Redistribution and use is allowed according to the terms of the BSD license. # For details see the accompanying COPYING-CMAKE-SCRIPTS file. if (APOGEE_INCLUDE_DIR AND APOGEE_LIBRARY) # in cache already set(APOGEE_FOUND TRUE) message(STATUS "Found libapogee: ${APOGEE_LIBRARY}") else (APOGEE_INCLUDE_DIR AND APOGEE_LIBRARY) find_path(APOGEE_INCLUDE_DIR ApogeeCam.h PATH_SUFFIXES libapogee ${_obIncDir} ${GNUWIN32_DIR}/include ) # Find Apogee Library find_library(APOGEE_LIBRARY NAMES apogee PATHS ${_obLinkDir} ${GNUWIN32_DIR}/lib ) if(APOGEE_INCLUDE_DIR AND APOGEE_LIBRARY) set(APOGEE_FOUND TRUE) else (APOGEE_INCLUDE_DIR AND APOGEE_LIBRARY) set(APOGEE_FOUND FALSE) endif(APOGEE_INCLUDE_DIR AND APOGEE_LIBRARY) if (APOGEE_FOUND) if (NOT APOGEE_FIND_QUIETLY) message(STATUS "Found APOGEE: ${APOGEE_LIBRARY}") endif (NOT APOGEE_FIND_QUIETLY) else (APOGEE_FOUND) if (APOGEE_FIND_REQUIRED) message(FATAL_ERROR "libapogee not found. Cannot compile Apogee CCD Driver. Please install libapogee and try again. http://www.indilib.org") endif (APOGEE_FIND_REQUIRED) endif (APOGEE_FOUND) mark_as_advanced(APOGEE_INCLUDE_DIR APOGEE_LIBRARY) endif (APOGEE_INCLUDE_DIR AND APOGEE_LIBRARY) libindi/cmake_modules/FindMEADE.cmake0000664000175000017500000000247713263645566016732 0ustar jasemjasem# - Try to find Meade DSI Library. # Once done this will define # # MEADEDSI_FOUND - system has Meade DSI # MEADEDSI_LIBRARIES - Link these to use Meade DSI # Copyright (c) 2006, Jasem Mutlaq # Based on FindLibfacile by Carsten Niehaus, # # Redistribution and use is allowed according to the terms of the BSD license. # For details see the accompanying COPYING-CMAKE-SCRIPTS file. if (MEADEDSI_LIBRARIES) # in cache already set(MEADEDSI_FOUND TRUE) message(STATUS "Found MEADEDSI: ${MEADEDSI_LIBRARIES}") else (MEADEDSI_LIBRARIES) find_library(MEADEDSI_LIBRARIES NAMES dsi PATHS ${_obLinkDir} ${GNUWIN32_DIR}/lib ) set(CMAKE_REQUIRED_LIBRARIES ${MEADEDSI_LIBRARIES}) if(MEADEDSI_LIBRARIES) set(MEADEDSI_FOUND TRUE) else (MEADEDSI_LIBRARIES) set(MEADEDSI_FOUND FALSE) endif(MEADEDSI_LIBRARIES) if (MEADEDSI_FOUND) if (NOT MEADEDSI_FIND_QUIETLY) message(STATUS "Found Meade DSI: ${MEADEDSI_LIBRARIES}") endif (NOT MEADEDSI_FIND_QUIETLY) else (MEADEDSI_FOUND) if (MEADEDSI_FIND_REQUIRED) message(FATAL_ERROR "Meade DSI not found. Please install Meade DSI library. http://linuxdsi.sourceforge.net") endif (MEADEDSI_FIND_REQUIRED) endif (MEADEDSI_FOUND) mark_as_advanced(MEADEDSI_LIBRARIES) endif (MEADEDSI_LIBRARIES) libindi/cmake_modules/FindOpenAL.cmake0000664000175000017500000000660413263645566017231 0ustar jasemjasem# Locate OpenAL # This module defines # OPENAL_LIBRARY # OPENAL_FOUND, if false, do not try to link to OpenAL # OPENAL_INCLUDE_DIR, where to find the headers # # $OPENALDIR is an environment variable that would # correspond to the ./configure --prefix=$OPENALDIR # used in building OpenAL. # # Created by Eric Wing. This was influenced by the FindSDL.cmake module. # This makes the presumption that you are include al.h like # #include "al.h" # and not # #include # The reason for this is that the latter is not entirely portable. # Windows/Creative Labs does not by default put their headers in AL/ and # OS X uses the convention . # # For Windows, Creative Labs seems to have added a registry key for their # OpenAL 1.1 installer. I have added that key to the list of search paths, # however, the key looks like it could be a little fragile depending on # if they decide to change the 1.00.0000 number for bug fix releases. # Also, they seem to have laid down groundwork for multiple library platforms # which puts the library in an extra subdirectory. Currently there is only # Win32 and I have hardcoded that here. This may need to be adjusted as # platforms are introduced. # The OpenAL 1.0 installer doesn't seem to have a useful key I can use. # I do not know if the Nvidia OpenAL SDK has a registry key. # # For OS X, remember that OpenAL was added by Apple in 10.4 (Tiger). # To support the framework, I originally wrote special framework detection # code in this module which I have now removed with CMake's introduction # of native support for frameworks. # In addition, OpenAL is open source, and it is possible to compile on Panther. # Furthermore, due to bugs in the initial OpenAL release, and the # transition to OpenAL 1.1, it is common to need to override the built-in # framework. # Per my request, CMake should search for frameworks first in # the following order: # ~/Library/Frameworks/OpenAL.framework/Headers # /Library/Frameworks/OpenAL.framework/Headers # /System/Library/Frameworks/OpenAL.framework/Headers # # On OS X, this will prefer the Framework version (if found) over others. # People will have to manually change the cache values of # OPENAL_LIBRARY to override this selection or set the CMake environment # CMAKE_INCLUDE_PATH to modify the search paths. FIND_PATH(OPENAL_INCLUDE_DIR al.h PATHS $ENV{OPENALDIR} NO_DEFAULT_PATH PATH_SUFFIXES include/AL include/OpenAL include ) FIND_PATH(OPENAL_INCLUDE_DIR al.h PATHS ~/Library/Frameworks /Library/Frameworks /usr/local /usr /sw # Fink /opt/local # DarwinPorts /opt/csw # Blastwave /opt [HKEY_LOCAL_MACHINE\\SOFTWARE\\Creative\ Labs\\OpenAL\ 1.1\ Software\ Development\ Kit\\1.00.0000;InstallDir] PATH_SUFFIXES include/AL include/OpenAL include ) FIND_LIBRARY(OPENAL_LIBRARY NAMES OpenAL al openal OpenAL32 PATHS $ENV{OPENALDIR} NO_DEFAULT_PATH PATH_SUFFIXES lib64 lib libs64 libs libs/Win32 libs/Win64 ) FIND_LIBRARY(OPENAL_LIBRARY NAMES OpenAL al openal OpenAL32 PATHS ~/Library/Frameworks /Library/Frameworks /usr/local /usr /sw /opt/local /opt/csw /opt [HKEY_LOCAL_MACHINE\\SOFTWARE\\Creative\ Labs\\OpenAL\ 1.1\ Software\ Development\ Kit\\1.00.0000;InstallDir] PATH_SUFFIXES lib64 lib libs64 libs libs/Win32 libs/Win64 ) SET(OPENAL_FOUND "NO") IF(OPENAL_LIBRARY AND OPENAL_INCLUDE_DIR) SET(OPENAL_FOUND "YES") ENDIF(OPENAL_LIBRARY AND OPENAL_INCLUDE_DIR) libindi/cmake_modules/FindVorbis.cmake0000664000175000017500000000204413263645566017351 0ustar jasemjasem# - Find vorbis # Find the native vorbis includes and libraries # # VORBIS_INCLUDE_DIR - where to find vorbis.h, etc. # VORBIS_LIBRARIES - List of libraries when using vorbis(file). # VORBIS_FOUND - True if vorbis found. if(VORBIS_INCLUDE_DIR) # Already in cache, be silent set(VORBIS_FIND_QUIETLY TRUE) endif(VORBIS_INCLUDE_DIR) find_path(VORBIS_INCLUDE_DIR vorbis/vorbisfile.h) find_library(OGG_LIBRARY NAMES ogg) find_library(VORBIS_LIBRARY NAMES vorbis) find_library(VORBISFILE_LIBRARY NAMES vorbisfile) # Handle the QUIETLY and REQUIRED arguments and set VORBIS_FOUND to TRUE if # all listed variables are TRUE. include(FindPackageHandleStandardArgs) find_package_handle_standard_args(VORBIS DEFAULT_MSG VORBIS_INCLUDE_DIR OGG_LIBRARY VORBIS_LIBRARY VORBIS_LIBRARY) if(VORBIS_FOUND) set(VORBIS_LIBRARIES ${VORBISFILE_LIBRARY} ${VORBIS_LIBRARY} ${OGG_LIBRARY}) else(VORBIS_FOUND) set(VORBIS_LIBRARIES) endif(VORBIS_FOUND) mark_as_advanced(VORBIS_INCLUDE_DIR) mark_as_advanced(OGG_LIBRARY VORBIS_LIBRARY VORBISFILE_LIBRARY) libindi/cmake_modules/FindASI.cmake0000664000175000017500000000243313263645566016523 0ustar jasemjasem# - Try to find ASI Library # Once done this will define # # ASI_FOUND - system has ASI # ASI_INCLUDE_DIR - the ASI include directory # ASI_LIBRARIES - Link these to use ASI # Redistribution and use is allowed according to the terms of the BSD license. # For details see the accompanying COPYING-CMAKE-SCRIPTS file. if (ASI_INCLUDE_DIR AND ASI_LIBRARIES) # in cache already set(ASI_FOUND TRUE) message(STATUS "Found libasi: ${ASI_LIBRARIES}") else (ASI_INCLUDE_DIR AND ASI_LIBRARIES) find_path(ASI_INCLUDE_DIR ASICamera2.h PATH_SUFFIXES libasi ${_obIncDir} ${GNUWIN32_DIR}/include ) find_library(ASI_LIBRARIES NAMES ASICamera2 PATHS ${_obLinkDir} ${GNUWIN32_DIR}/lib ) if(ASI_INCLUDE_DIR AND ASI_LIBRARIES) set(ASI_FOUND TRUE) else (ASI_INCLUDE_DIR AND ASI_LIBRARIES) set(ASI_FOUND FALSE) endif(ASI_INCLUDE_DIR AND ASI_LIBRARIES) if (ASI_FOUND) if (NOT ASI_FIND_QUIETLY) message(STATUS "Found ASI: ${ASI_LIBRARIES}") endif (NOT ASI_FIND_QUIETLY) else (ASI_FOUND) if (ASI_FIND_REQUIRED) message(FATAL_ERROR "ASI not found. Please install libasi http://www.indilib.org") endif (ASI_FIND_REQUIRED) endif (ASI_FOUND) mark_as_advanced(ASI_INCLUDE_DIR ASI_LIBRARIES) endif (ASI_INCLUDE_DIR AND ASI_LIBRARIES) libindi/cmake_modules/FindFISHCAMP.cmake0000664000175000017500000000322513263645566017301 0ustar jasemjasem# - Try to find FISHCAMP CCD # Once done this will define # # FISHCAMP_FOUND - system has FISHCAMP # FISHCAMP_LIBRARIES - Link these to use FISHCAMP # FISHCAMP_INCLUDE_DIR - Fishcamp include directory # Copyright (c) 2006, Jasem Mutlaq # Based on FindLibfacile by Carsten Niehaus, # # Redistribution and use is allowed according to the terms of the BSD license. # For details see the accompanying COPYING-CMAKE-SCRIPTS file. if (FISHCAMP_LIBRARIES AND FISHCAMP_INCLUDE_DIR) # in cache already set(FISHCAMP_FOUND TRUE) message(STATUS "Found FISHCAMP: ${FISHCAMP_LIBRARIES}") else (FISHCAMP_LIBRARIES AND FISHCAMP_INCLUDE_DIR) find_library(FISHCAMP_LIBRARIES NAMES fishcamp PATHS ${_obLinkDir} ${GNUWIN32_DIR}/lib ) find_path(FISHCAMP_INCLUDE_DIR fishcamp.h PATH_SUFFIXES libfishcamp ${_obIncDir} ${GNUWIN32_DIR}/include ) set(CMAKE_REQUIRED_LIBRARIES ${FISHCAMP_LIBRARIES}) if(FISHCAMP_LIBRARIES AND FISHCAMP_INCLUDE_DIR) set(FISHCAMP_FOUND TRUE) else (FISHCAMP_LIBRARIES AND FISHCAMP_INCLUDE_DIR) set(FISHCAMP_FOUND FALSE) endif(FISHCAMP_LIBRARIES AND FISHCAMP_INCLUDE_DIR) if (FISHCAMP_FOUND) if (NOT FISHCAMP_FIND_QUIETLY) message(STATUS "Found FISHCAMP: ${FISHCAMP_LIBRARIES}") endif (NOT FISHCAMP_FIND_QUIETLY) else (FISHCAMP_FOUND) if (FISHCAMP_FIND_REQUIRED) message(FATAL_ERROR "FISHCAMP not found. Please install FISHCAMP library. http://www.indilib.org") endif (FISHCAMP_FIND_REQUIRED) endif (FISHCAMP_FOUND) mark_as_advanced(FISHCAMP_LIBRARIES FISHCAMP_INCLUDE_DIR) endif (FISHCAMP_LIBRARIES AND FISHCAMP_INCLUDE_DIR) libindi/cmake_modules/FindGLIB2.cmake0000664000175000017500000001327113263645566016710 0ustar jasemjasem# - Try to find GLib2 # Once done this will define # # GLIB2_FOUND - system has GLib2 # GLIB2_INCLUDE_DIRS - the GLib2 include directory # GLIB2_LIBRARIES - Link these to use GLib2 # # HAVE_GLIB_GREGEX_H glib has gregex.h header and # supports g_regex_match_simple # # Copyright (c) 2006 Andreas Schneider # Copyright (c) 2006 Philippe Bernery # Copyright (c) 2007 Daniel Gollub # Copyright (c) 2007 Alban Browaeys # Copyright (c) 2008 Michael Bell # Copyright (c) 2008 Bjoern Ricks # # Redistribution and use is allowed according to the terms of the New # BSD license. # For details see the accompanying COPYING-CMAKE-SCRIPTS file. # IF (GLIB2_LIBRARIES AND GLIB2_INCLUDE_DIRS ) # in cache already SET(GLIB2_FOUND TRUE) ELSE (GLIB2_LIBRARIES AND GLIB2_INCLUDE_DIRS ) INCLUDE(FindPkgConfig) ## Glib IF ( GLIB2_FIND_REQUIRED ) SET( _pkgconfig_REQUIRED "REQUIRED" ) ELSE ( GLIB2_FIND_REQUIRED ) SET( _pkgconfig_REQUIRED "" ) ENDIF ( GLIB2_FIND_REQUIRED ) IF ( GLIB2_MIN_VERSION ) PKG_SEARCH_MODULE( GLIB2 ${_pkgconfig_REQUIRED} glib-2.0>=${GLIB2_MIN_VERSION} ) ELSE ( GLIB2_MIN_VERSION ) PKG_SEARCH_MODULE( GLIB2 ${_pkgconfig_REQUIRED} glib-2.0 ) ENDIF ( GLIB2_MIN_VERSION ) IF ( PKG_CONFIG_FOUND ) IF ( GLIB2_FOUND ) SET ( GLIB2_CORE_FOUND TRUE ) ELSE ( GLIB2_FOUND ) SET ( GLIB2_CORE_FOUND FALSE ) ENDIF ( GLIB2_FOUND ) ENDIF ( PKG_CONFIG_FOUND ) # Look for glib2 include dir and libraries w/o pkgconfig IF ( NOT GLIB2_FOUND AND NOT PKG_CONFIG_FOUND ) FIND_PATH( _glibconfig_include_DIR NAMES glibconfig.h PATHS /opt/gnome/lib64 /opt/gnome/lib /opt/lib/ /opt/local/lib /sw/lib/ /usr/lib64 /usr/lib /usr/local/include ${CMAKE_LIBRARY_PATH} PATH_SUFFIXES glib-2.0/include ) FIND_PATH( _glib2_include_DIR NAMES glib.h PATHS /opt/gnome/include /opt/local/include /sw/include /usr/include /usr/local/include PATH_SUFFIXES glib-2.0 ) #MESSAGE(STATUS "Glib headers: ${_glib2_include_DIR}") FIND_LIBRARY( _glib2_link_DIR NAMES glib-2.0 glib PATHS /opt/gnome/lib /opt/local/lib /sw/lib /usr/lib /usr/local/lib ) IF ( _glib2_include_DIR AND _glib2_link_DIR ) SET ( _glib2_FOUND TRUE ) ENDIF ( _glib2_include_DIR AND _glib2_link_DIR ) IF ( _glib2_FOUND ) SET ( GLIB2_INCLUDE_DIRS ${_glib2_include_DIR} ${_glibconfig_include_DIR} ) SET ( GLIB2_LIBRARIES ${_glib2_link_DIR} ) SET ( GLIB2_CORE_FOUND TRUE ) ELSE ( _glib2_FOUND ) SET ( GLIB2_CORE_FOUND FALSE ) ENDIF ( _glib2_FOUND ) # Handle dependencies # libintl IF ( NOT LIBINTL_FOUND ) FIND_PATH(LIBINTL_INCLUDE_DIR NAMES libintl.h PATHS /opt/gnome/include /opt/local/include /sw/include /usr/include /usr/local/include ) FIND_LIBRARY(LIBINTL_LIBRARY NAMES intl PATHS /opt/gnome/lib /opt/local/lib /sw/lib /usr/local/lib /usr/lib ) IF (LIBINTL_LIBRARY AND LIBINTL_INCLUDE_DIR) SET (LIBINTL_FOUND TRUE) ENDIF (LIBINTL_LIBRARY AND LIBINTL_INCLUDE_DIR) ENDIF ( NOT LIBINTL_FOUND ) # libiconv IF ( NOT LIBICONV_FOUND ) FIND_PATH(LIBICONV_INCLUDE_DIR NAMES iconv.h PATHS /opt/gnome/include /opt/local/include /opt/local/include /sw/include /sw/include /usr/local/include /usr/include PATH_SUFFIXES glib-2.0 ) FIND_LIBRARY(LIBICONV_LIBRARY NAMES iconv PATHS /opt/gnome/lib /opt/local/lib /sw/lib /usr/lib /usr/local/lib ) IF (LIBICONV_LIBRARY AND LIBICONV_INCLUDE_DIR) SET (LIBICONV_FOUND TRUE) ENDIF (LIBICONV_LIBRARY AND LIBICONV_INCLUDE_DIR) ENDIF ( NOT LIBICONV_FOUND ) IF (LIBINTL_FOUND) SET (GLIB2_LIBRARIES ${GLIB2_LIBRARIES} ${LIBINTL_LIBRARY}) SET (GLIB2_INCLUDE_DIRS ${GLIB2_INCLUDE_DIRS} ${LIBINTL_INCLUDE_DIR}) ENDIF (LIBINTL_FOUND) IF (LIBICONV_FOUND) SET (GLIB2_LIBRARIES ${GLIB2_LIBRARIES} ${LIBICONV_LIBRARY}) SET (GLIB2_INCLUDE_DIRS ${GLIB2_INCLUDE_DIRS} ${LIBICONV_INCLUDE_DIR}) ENDIF (LIBICONV_FOUND) ENDIF ( NOT GLIB2_FOUND AND NOT PKG_CONFIG_FOUND ) ## IF (GLIB2_CORE_FOUND AND GLIB2_INCLUDE_DIRS AND GLIB2_LIBRARIES) SET (GLIB2_FOUND TRUE) ENDIF (GLIB2_CORE_FOUND AND GLIB2_INCLUDE_DIRS AND GLIB2_LIBRARIES) IF (GLIB2_FOUND) IF (NOT GLIB2_FIND_QUIETLY) MESSAGE (STATUS "Found GLib2: ${GLIB2_LIBRARIES} ${GLIB2_INCLUDE_DIRS}") ENDIF (NOT GLIB2_FIND_QUIETLY) ELSE (GLIB2_FOUND) IF (GLIB2_FIND_REQUIRED) MESSAGE (SEND_ERROR "Could not find GLib2") ENDIF (GLIB2_FIND_REQUIRED) ENDIF (GLIB2_FOUND) # show the GLIB2_INCLUDE_DIRS and GLIB2_LIBRARIES variables only in the advanced view MARK_AS_ADVANCED(GLIB2_INCLUDE_DIRS GLIB2_LIBRARIES) MARK_AS_ADVANCED(LIBICONV_INCLUDE_DIR LIBICONV_LIBRARY) MARK_AS_ADVANCED(LIBINTL_INCLUDE_DIR LIBINTL_LIBRARY) ENDIF (GLIB2_LIBRARIES AND GLIB2_INCLUDE_DIRS) IF ( GLIB2_FOUND ) # Check if system has a newer version of glib # which supports g_regex_match_simple INCLUDE( CheckIncludeFiles ) SET( CMAKE_REQUIRED_INCLUDES ${GLIB2_INCLUDE_DIRS} ) CHECK_INCLUDE_FILES ( glib/gregex.h HAVE_GLIB_GREGEX_H ) # Reset CMAKE_REQUIRED_INCLUDES SET( CMAKE_REQUIRED_INCLUDES "" ) ENDIF( GLIB2_FOUND ) libindi/cmake_modules/FindGPSD.cmake0000664000175000017500000000103613263645566016642 0ustar jasemjasem# - Find GPSD # Find the native GPSD includes and library FIND_PATH(GPSD_INCLUDE_DIR libgpsmm.h gps.h) SET(GPSD_NAMES ${GPSD_NAMES} gps) FIND_LIBRARY(GPSD_LIBRARY NAMES ${GPSD_NAMES} ) # handle the QUIETLY and REQUIRED arguments and set JPEG_FOUND to TRUE if # all listed variables are TRUE INCLUDE(FindPackageHandleStandardArgs) FIND_PACKAGE_HANDLE_STANDARD_ARGS(GPSD DEFAULT_MSG GPSD_LIBRARY GPSD_INCLUDE_DIR) IF(GPSD_FOUND) SET(GPSD_LIBRARIES ${GPSD_LIBRARY}) message(STATUS "Found libgps: ${GPSD_LIBRARIES}") ENDIF(GPSD_FOUND) libindi/cmake_modules/CMakeCommon.cmake0000664000175000017500000001337213263645566017443 0ustar jasemjasem include(CheckCCompilerFlag) IF (NOT ${CMAKE_CXX_COMPILER_ID} STREQUAL "MSVC") SET(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -std=gnu99") SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++11") ENDIF () # Ccache support IF (ANDROID OR UNIX OR APPLE) FIND_PROGRAM(CCACHE_FOUND ccache) SET(CCACHE_SUPPORT OFF CACHE BOOL "Enable ccache support") IF ((CCACHE_FOUND OR ANDROID) AND CCACHE_SUPPORT MATCHES ON) SET_PROPERTY(GLOBAL PROPERTY RULE_LAUNCH_COMPILE ccache) SET_PROPERTY(GLOBAL PROPERTY RULE_LAUNCH_LINK ccache) ENDIF () ENDIF () # Add security (hardening flags) IF (UNIX OR APPLE OR ANDROID) # Older compilers are predefining _FORTIFY_SOURCE, so defining it causes a # warning, which is then considered an error. Second issue is that for # these compilers, _FORTIFY_SOURCE must be used while optimizing, else # causes a warning, which also results in an error. And finally, CMake is # not using optimization when testing for libraries, hence breaking the build. CHECK_C_COMPILER_FLAG("-Werror -D_FORTIFY_SOURCE=2" COMPATIBLE_FORTIFY_SOURCE) IF (${COMPATIBLE_FORTIFY_SOURCE}) SET(SEC_COMP_FLAGS "-D_FORTIFY_SOURCE=2") ENDIF () SET(SEC_COMP_FLAGS "${SEC_COMP_FLAGS} -fstack-protector-all -fPIE") # Make sure to add optimization flag. Some systems require this for _FORTIFY_SOURCE. IF (NOT CMAKE_BUILD_TYPE MATCHES "MinSizeRel" AND NOT CMAKE_BUILD_TYPE MATCHES "Release" AND NOT CMAKE_BUILD_TYPE MATCHES "Debug") SET(SEC_COMP_FLAGS "${SEC_COMP_FLAGS} -O1") ENDIF () IF (NOT ANDROID AND NOT "${CMAKE_CXX_COMPILER_ID}" STREQUAL "Clang" AND NOT APPLE AND NOT CYGWIN) SET(SEC_COMP_FLAGS "${SEC_COMP_FLAGS} -Wa,--noexecstack") ENDIF () SET(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} ${SEC_COMP_FLAGS}") SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${SEC_COMP_FLAGS}") SET(SEC_LINK_FLAGS "") IF (NOT APPLE AND NOT CYGWIN) SET(SEC_LINK_FLAGS "${SEC_LINK_FLAGS} -Wl,-z,nodump -Wl,-z,noexecstack -Wl,-z,relro -Wl,-z,now") ENDIF () IF (NOT ANDROID AND NOT APPLE) SET(SEC_LINK_FLAGS "${SEC_LINK_FLAGS} -pie") ENDIF () SET(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} ${SEC_LINK_FLAGS}") SET(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} ${SEC_LINK_FLAGS}") ENDIF () # Warning, debug and linker flags SET(FIX_WARNINGS OFF CACHE BOOL "Enable strict compilation mode to turn compiler warnings to errors") IF (UNIX OR APPLE) SET(COMP_FLAGS "") SET(LINKER_FLAGS "") # Verbose warnings and turns all to errors SET(COMP_FLAGS "${COMP_FLAGS} -Wall -Wextra") IF (FIX_WARNINGS) SET(COMP_FLAGS "${COMP_FLAGS} -Werror") ENDIF () # Omit problematic warnings IF ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "GNU") SET(COMP_FLAGS "${COMP_FLAGS} -Wno-unused-but-set-variable") ENDIF () IF ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "GNU" AND CMAKE_CXX_COMPILER_VERSION VERSION_GREATER 6.9.9) SET(COMP_FLAGS "${COMP_FLAGS} -Wno-format-truncation") ENDIF () IF ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "AppleClang") SET(COMP_FLAGS "${COMP_FLAGS} -Wno-nonnull -Wno-deprecated-declarations") ENDIF () # Minimal debug info with Clang IF ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "Clang") SET(COMP_FLAGS "${COMP_FLAGS} -gline-tables-only") ELSE () SET(COMP_FLAGS "${COMP_FLAGS} -g") ENDIF () # Note: The following flags are problematic on older systems with gcc 4.8 IF ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "Clang" OR ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "GNU" AND CMAKE_CXX_COMPILER_VERSION VERSION_GREATER 4.9.9)) IF ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "Clang" OR "${CMAKE_CXX_COMPILER_ID}" STREQUAL "AppleClang") SET(COMP_FLAGS "${COMP_FLAGS} -Wno-unused-command-line-argument") ENDIF () FIND_PROGRAM(LDGOLD_FOUND ld.gold) SET(LDGOLD_SUPPORT OFF CACHE BOOL "Enable ld.gold support") # Optional ld.gold is 2x faster than normal ld IF (LDGOLD_FOUND AND LDGOLD_SUPPORT MATCHES ON AND NOT APPLE AND NOT CMAKE_SYSTEM_PROCESSOR MATCHES arm) SET(LINKER_FLAGS "${LINKER_FLAGS} -fuse-ld=gold") # Use Identical Code Folding SET(COMP_FLAGS "${COMP_FLAGS} -ffunction-sections") SET(LINKER_FLAGS "${LINKER_FLAGS} -Wl,--icf=safe") # Compress the debug sections # Note: Before valgrind 3.12.0, patch should be applied for valgrind (https://bugs.kde.org/show_bug.cgi?id=303877) IF (NOT APPLE AND NOT ANDROID AND NOT CMAKE_SYSTEM_PROCESSOR MATCHES arm AND NOT CMAKE_CXX_CLANG_TIDY) SET(COMP_FLAGS "${COMP_FLAGS} -Wa,--compress-debug-sections") SET(LINKER_FLAGS "${LINKER_FLAGS} -Wl,--compress-debug-sections=zlib") ENDIF () ENDIF () ENDIF () # Apply the flags SET(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} ${COMP_FLAGS}") SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${COMP_FLAGS}") SET(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} ${LINKER_FLAGS}") SET(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} ${LINKER_FLAGS}") ENDIF () # Sanitizer support SET(CLANG_SANITIZERS OFF CACHE BOOL "Clang's sanitizer support") IF (CLANG_SANITIZERS AND ((UNIX AND "${CMAKE_CXX_COMPILER_ID}" STREQUAL "Clang") OR (APPLE AND "${CMAKE_CXX_COMPILER_ID}" STREQUAL "AppleClang"))) SET(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fsanitize=address,undefined -fno-omit-frame-pointer") SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fsanitize=address,undefined -fno-omit-frame-pointer") SET(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -fsanitize=address,undefined -fno-omit-frame-pointer") SET(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -fsanitize=address,undefined -fno-omit-frame-pointer") ENDIF () # Unity Build support include(UnityBuild) libindi/cmake_modules/FindCFITSIO.cmake0000664000175000017500000000434613263645566017214 0ustar jasemjasem# - Try to find CFITSIO # Once done this will define # # CFITSIO_FOUND - system has CFITSIO # CFITSIO_INCLUDE_DIR - the CFITSIO include directory # CFITSIO_LIBRARIES - Link these to use CFITSIO # CFITSIO_VERSION_STRING - Human readable version number of cfitsio # CFITSIO_VERSION_MAJOR - Major version number of cfitsio # CFITSIO_VERSION_MINOR - Minor version number of cfitsio # Copyright (c) 2006, Jasem Mutlaq # Based on FindLibfacile by Carsten Niehaus, # # Redistribution and use is allowed according to the terms of the BSD license. # For details see the accompanying COPYING-CMAKE-SCRIPTS file. if (CFITSIO_INCLUDE_DIR AND CFITSIO_LIBRARIES) # in cache already set(CFITSIO_FOUND TRUE) message(STATUS "Found CFITSIO: ${CFITSIO_LIBRARIES}") else (CFITSIO_INCLUDE_DIR AND CFITSIO_LIBRARIES) # JM: Packages from different distributions have different suffixes find_path(CFITSIO_INCLUDE_DIR fitsio.h PATH_SUFFIXES libcfitsio3 libcfitsio0 cfitsio ${_obIncDir} ${GNUWIN32_DIR}/include ) find_library(CFITSIO_LIBRARIES NAMES cfitsio PATHS ${_obLinkDir} ${GNUWIN32_DIR}/lib ) if(CFITSIO_INCLUDE_DIR AND CFITSIO_LIBRARIES) set(CFITSIO_FOUND TRUE) else (CFITSIO_INCLUDE_DIR AND CFITSIO_LIBRARIES) set(CFITSIO_FOUND FALSE) endif(CFITSIO_INCLUDE_DIR AND CFITSIO_LIBRARIES) if (CFITSIO_FOUND) # Find the version of the cfitsio header file(STRINGS ${CFITSIO_INCLUDE_DIR}/fitsio.h CFITSIO_VERSION_STRING LIMIT_COUNT 1 REGEX "CFITSIO_VERSION") STRING(REGEX REPLACE "[^0-9.]" "" CFITSIO_VERSION_STRING ${CFITSIO_VERSION_STRING}) STRING(REGEX REPLACE "^([0-9]+)[.]([0-9]+)" "\\1" CFITSIO_VERSION_MAJOR ${CFITSIO_VERSION_STRING}) STRING(REGEX REPLACE "^([0-9]+)[.]([0-9]+)" "\\2" CFITSIO_VERSION_MINOR ${CFITSIO_VERSION_STRING}) if (NOT CFITSIO_FIND_QUIETLY) message(STATUS "Found CFITSIO ${CFITSIO_VERSION_STRING}: ${CFITSIO_LIBRARIES}") endif (NOT CFITSIO_FIND_QUIETLY) else (CFITSIO_FOUND) if (CFITSIO_FIND_REQUIRED) message(STATUS "CFITSIO not found.") endif (CFITSIO_FIND_REQUIRED) endif (CFITSIO_FOUND) mark_as_advanced(CFITSIO_INCLUDE_DIR CFITSIO_LIBRARIES) endif (CFITSIO_INCLUDE_DIR AND CFITSIO_LIBRARIES) libindi/cmake_modules/FindMODBUS.cmake0000664000175000017500000000320113263645566017072 0ustar jasemjasem# - Try to find libmodbus # Once done this will define # # MODBUS_FOUND - system has MODBUS # MODBUS_INCLUDE_DIR - the MODBUS include directory # MODBUS_LIBRARIES - Link these to use MODBUS # Copyright (c) 2006, Jasem Mutlaq # Based on FindLibfacile by Carsten Niehaus, # # Redistribution and use is allowed according to the terms of the BSD license. # For details see the accompanying COPYING-CMAKE-SCRIPTS file. if (MODBUS_INCLUDE_DIR AND MODBUS_LIBRARIES) # in cache already set(MODBUS_FOUND TRUE) message(STATUS "Found libmodbus: ${MODBUS_LIBRARIES}") else (MODBUS_INCLUDE_DIR AND MODBUS_LIBRARIES) find_path(MODBUS_INCLUDE_DIR modbus.h PATH_SUFFIXES modbus ${_obIncDir} ${GNUWIN32_DIR}/include ) find_library(MODBUS_LIBRARIES NAMES modbus PATHS ${_obLinkDir} ${GNUWIN32_DIR}/lib ) set(CMAKE_REQUIRED_INCLUDES ${MODBUS_INCLUDE_DIR}) set(CMAKE_REQUIRED_LIBRARIES ${MODBUS_LIBRARIES}) if(MODBUS_INCLUDE_DIR AND MODBUS_LIBRARIES) set(MODBUS_FOUND TRUE) else (MODBUS_INCLUDE_DIR AND MODBUS_LIBRARIES) set(MODBUS_FOUND FALSE) endif(MODBUS_INCLUDE_DIR AND MODBUS_LIBRARIES) if (MODBUS_FOUND) if (NOT MODBUS_FIND_QUIETLY) message(STATUS "Found libmodbus: ${MODBUS_LIBRARIES}") endif (NOT MODBUS_FIND_QUIETLY) else (MODBUS_FOUND) if (MODBUS_FIND_REQUIRED) message(FATAL_ERROR "libmodbus not found. Please install libmodbus-devel. https://launchpad.net/libmodbus/") endif (MODBUS_FIND_REQUIRED) endif (MODBUS_FOUND) mark_as_advanced(MODBUS_INCLUDE_DIR MODBUS_LIBRARIES) endif (MODBUS_INCLUDE_DIR AND MODBUS_LIBRARIES) libindi/cmake_modules/FindDC1394.cmake0000664000175000017500000000261613263645566016721 0ustar jasemjasem# - Try to find dc1394 library (version 2) and include files # Once done this will define # # DC1394_FOUND - system has DC1394 # DC1394_INCLUDE_DIR - the DC1394 include directory # DC1394_LIBRARIES - Link these to use DC1394 # Redistribution and use is allowed according to the terms of the BSD license. # For details see the accompanying COPYING-CMAKE-SCRIPTS file. if (DC1394_INCLUDE_DIR AND DC1394_LIBRARIES) # in cache already set(DC1394_FOUND TRUE) message(STATUS "Found libdc1394: ${DC1394_LIBRARIES}") else (DC1394_INCLUDE_DIR AND DC1394_LIBRARIES) find_path(DC1394_INCLUDE_DIR control.h PATH_SUFFIXES dc1394 ${_obIncDir} ${GNUWIN32_DIR}/include ) find_library(DC1394_LIBRARIES NAMES dc1394 PATHS ${_obLinkDir} ${GNUWIN32_DIR}/lib ) if(DC1394_INCLUDE_DIR AND DC1394_LIBRARIES) set(DC1394_FOUND TRUE) else (DC1394_INCLUDE_DIR AND DC1394_LIBRARIES) set(DC1394_FOUND FALSE) endif(DC1394_INCLUDE_DIR AND DC1394_LIBRARIES) if (DC1394_FOUND) if (NOT DC1394_FIND_QUIETLY) message(STATUS "Found DC1394: ${DC1394_LIBRARIES}") endif (NOT DC1394_FIND_QUIETLY) else (DC1394_FOUND) if (DC1394_FIND_REQUIRED) message(FATAL_ERROR "DC1394 not found. Please install libdc1395") endif (DC1394_FIND_REQUIRED) endif (DC1394_FOUND) mark_as_advanced(DC1394_INCLUDE_DIR DC1394_LIBRARIES) endif (DC1394_INCLUDE_DIR AND DC1394_LIBRARIES) libindi/cmake_modules/FindFTDI.cmake0000664000175000017500000000267113263645566016641 0ustar jasemjasem# - Try to find FTDI # This finds libFTDI that is compatible with old libusb v 0.1 # For newer libusb > 1.0, use FindFTDI1.cmake # Once done this will define # # FTDI_FOUND - system has FTDI # FTDI_INCLUDE_DIR - the FTDI include directory # FTDI_LIBRARIES - Link these to use FTDI # Redistribution and use is allowed according to the terms of the BSD license. # For details see the accompanying COPYING-CMAKE-SCRIPTS file. if (FTDI_INCLUDE_DIR AND FTDI_LIBRARIES) # in cache already set(FTDI_FOUND TRUE) message(STATUS "Found libftdi: ${FTDI_LIBRARIES}") else (FTDI_INCLUDE_DIR AND FTDI_LIBRARIES) find_path(FTDI_INCLUDE_DIR ftdi.h PATH_SUFFIXES libftdi1 ${_obIncDir} ${GNUWIN32_DIR}/include /usr/local/include ) find_library(FTDI_LIBRARIES NAMES ftdi ftdi1 PATHS ${_obLinkDir} ${GNUWIN32_DIR}/lib /usr/local/lib ) if(FTDI_INCLUDE_DIR AND FTDI_LIBRARIES) set(FTDI_FOUND TRUE) else (FTDI_INCLUDE_DIR AND FTDI_LIBRARIES) set(FTDI_FOUND FALSE) endif(FTDI_INCLUDE_DIR AND FTDI_LIBRARIES) if (FTDI_FOUND) if (NOT FTDI_FIND_QUIETLY) message(STATUS "Found FTDI: ${FTDI_LIBRARIES}") endif (NOT FTDI_FIND_QUIETLY) else (FTDI_FOUND) if (FTDI_FIND_REQUIRED) message(FATAL_ERROR "FTDI not found. Please install libftdi-dev") endif (FTDI_FIND_REQUIRED) endif (FTDI_FOUND) mark_as_advanced(FTDI_INCLUDE_DIR FTDI_LIBRARIES) endif (FTDI_INCLUDE_DIR AND FTDI_LIBRARIES) libindi/cmake_modules/FindRTLSDR.cmake0000664000175000017500000000265113263645566017123 0ustar jasemjasem# - Try to find RTLSDR # Once done this will define # # RTLSDR_FOUND - system has RTLSDR # RTLSDR_INCLUDE_DIR - the RTLSDR include directory # RTLSDR_LIBRARIES - Link these to use RTLSDR # RTLSDR_VERSION_STRING - Human readable version number of rtlsdr # RTLSDR_VERSION_MAJOR - Major version number of rtlsdr # RTLSDR_VERSION_MINOR - Minor version number of rtlsdr # Copyright (c) 2017, Ilia Platone, # Based on FindLibfacile by Carsten Niehaus, # # Redistribution and use is allowed according to the terms of the BSD license. # For details see the accompanying COPYING-CMAKE-SCRIPTS file. if (RTLSDR_LIBRARIES) # in cache already set(RTLSDR_FOUND TRUE) message(STATUS "Found RTLSDR: ${RTLSDR_LIBRARIES}") else (RTLSDR_LIBRARIES) find_library(RTLSDR_LIBRARIES NAMES rtlsdr rtlsdr0 PATHS ${_obLinkDir} ${GNUWIN32_DIR}/lib /usr/local/lib ) if(RTLSDR_LIBRARIES) set(RTLSDR_FOUND TRUE) else (RTLSDR_LIBRARIES) set(RTLSDR_FOUND FALSE) endif(RTLSDR_LIBRARIES) if (RTLSDR_FOUND) if (NOT RTLSDR_FIND_QUIETLY) message(STATUS "Found RTLSDR: ${RTLSDR_LIBRARIES}") endif (NOT RTLSDR_FIND_QUIETLY) else (RTLSDR_FOUND) if (RTLSDR_FIND_REQUIRED) message(FATAL_ERROR "RTLSDR not found. Please install librtlsdr-dev") endif (RTLSDR_FIND_REQUIRED) endif (RTLSDR_FOUND) mark_as_advanced(RTLSDR_LIBRARIES) endif (RTLSDR_LIBRARIES) libindi/cmake_modules/FindGPHOTO2.cmake0000664000175000017500000000562613263645566017200 0ustar jasemjasem# - Find the native sqlite3 includes and library # # This module defines # GPHOTO2_INCLUDE_DIR, where to find libgphoto2 header files # GPHOTO2_LIBRARIES, the libraries to link against to use libgphoto2 # GPHOTO2_FOUND, If false, do not try to use libgphoto2. # GPHOTO2_VERSION_STRING, e.g. 2.4.14 # GPHOTO2_VERSION_MAJOR, e.g. 2 # GPHOTO2_VERSION_MINOR, e.g. 4 # GPHOTO2_VERSION_PATCH, e.g. 14 # # also defined, but not for general use are # GPHOTO2_LIBRARY, where to find the sqlite3 library. #============================================================================= # Copyright 2010 henrik andersson #============================================================================= SET(GPHOTO2_FIND_REQUIRED ${Gphoto2_FIND_REQUIRED}) find_path(GPHOTO2_INCLUDE_DIR gphoto2/gphoto2.h) mark_as_advanced(GPHOTO2_INCLUDE_DIR) set(GPHOTO2_NAMES ${GPHOTO2_NAMES} gphoto2 libgphoto2) set(GPHOTO2_PORT_NAMES ${GPHOTO2_PORT_NAMES} gphoto2_port libgphoto2_port) find_library(GPHOTO2_LIBRARY NAMES ${GPHOTO2_NAMES} ) find_library(GPHOTO2_PORT_LIBRARY NAMES ${GPHOTO2_PORT_NAMES} ) mark_as_advanced(GPHOTO2_LIBRARY) mark_as_advanced(GPHOTO2_PORT_LIBRARY) # Detect libgphoto2 version FIND_PROGRAM(GPHOTO2CONFIG_EXECUTABLE NAMES gphoto2-config) IF(GPHOTO2CONFIG_EXECUTABLE) EXEC_PROGRAM(${GPHOTO2CONFIG_EXECUTABLE} ARGS --version RETURN_VALUE _return_VALUE OUTPUT_VARIABLE GPHOTO2_VERSION) string(REGEX REPLACE "^.*libgphoto2 ([0-9]+).*$" "\\1" GPHOTO2_VERSION_MAJOR "${GPHOTO2_VERSION}") string(REGEX REPLACE "^.*libgphoto2 [0-9]+\\.([0-9]+).*$" "\\1" GPHOTO2_VERSION_MINOR "${GPHOTO2_VERSION}") string(REGEX REPLACE "^.*libgphoto2 [0-9]+\\.[0-9]+\\.([0-9]+).*$" "\\1" GPHOTO2_VERSION_PATCH "${GPHOTO2_VERSION}") set(GPHOTO2_VERSION_STRING "${GPHOTO2_VERSION_MAJOR}.${GPHOTO2_VERSION_MINOR}.${GPHOTO2_VERSION_PATCH}") ENDIF(GPHOTO2CONFIG_EXECUTABLE) # handle the QUIETLY and REQUIRED arguments and set GPHOTO2_FOUND to TRUE if # all listed variables are TRUE include(FindPackageHandleStandardArgs) find_package_handle_standard_args(GPHOTO2 DEFAULT_MSG GPHOTO2_LIBRARY GPHOTO2_INCLUDE_DIR) IF(GPHOTO2_FOUND) SET(Gphoto2_LIBRARIES ${GPHOTO2_LIBRARY} ${GPHOTO2_PORT_LIBRARY}) SET(Gphoto2_INCLUDE_DIRS ${GPHOTO2_INCLUDE_DIR}) # libgphoto2 dynamically loads and unloads usb library # without calling any cleanup functions (since they are absent from libusb-0.1). # This leaves usb event handling threads running with invalid callback and return addresses, # which causes a crash after any usb event is generated, at least in Mac OS X. # libusb1 backend does correctly call exit function, but ATM it crashes anyway. # Workaround is to link against libusb so that it wouldn't get unloaded. IF(APPLE) find_library(USB_LIBRARY NAMES usb-1.0 libusb-1.0) mark_as_advanced(USB_LIBRARY) IF(USB_LIBRARY) SET(Gphoto2_LIBRARIES ${Gphoto2_LIBRARIES} ${USB_LIBRARY}) ENDIF(USB_LIBRARY) ENDIF(APPLE) ENDIF(GPHOTO2_FOUND) libindi/cmake_modules/FindJPEG.cmake0000664000175000017500000000166313263645566016640 0ustar jasemjasem# - Find JPEG # Find the native JPEG includes and library # This module defines # JPEG_INCLUDE_DIR, where to find jpeglib.h, etc. # JPEG_LIBRARIES, the libraries needed to use JPEG. # JPEG_FOUND, If false, do not try to use JPEG. # also defined, but not for general use are # JPEG_LIBRARY, where to find the JPEG library. FIND_PATH(JPEG_INCLUDE_DIR jpeglib.h) SET(JPEG_NAMES ${JPEG_NAMES} jpeg) FIND_LIBRARY(JPEG_LIBRARY NAMES ${JPEG_NAMES} ) # handle the QUIETLY and REQUIRED arguments and set JPEG_FOUND to TRUE if # all listed variables are TRUE INCLUDE(FindPackageHandleStandardArgs) FIND_PACKAGE_HANDLE_STANDARD_ARGS(JPEG DEFAULT_MSG JPEG_LIBRARY JPEG_INCLUDE_DIR) IF(JPEG_FOUND) SET(JPEG_LIBRARIES ${JPEG_LIBRARY}) ENDIF(JPEG_FOUND) # Deprecated declarations. SET (NATIVE_JPEG_INCLUDE_PATH ${JPEG_INCLUDE_DIR} ) GET_FILENAME_COMPONENT (NATIVE_JPEG_LIB_PATH ${JPEG_LIBRARY} PATH) MARK_AS_ADVANCED(JPEG_LIBRARY JPEG_INCLUDE_DIR ) libindi/cmake_modules/FindGSL.cmake0000664000175000017500000002224113263645566016533 0ustar jasemjasem#.rst: # FindGSL # -------- # # Find the native GSL includes and libraries. # # The GNU Scientific Library (GSL) is a numerical library for C and C++ # programmers. It is free software under the GNU General Public # License. # # Imported Targets # ^^^^^^^^^^^^^^^^ # # If GSL is found, this module defines the following :prop_tgt:`IMPORTED` # targets:: # # GSL::gsl - The main GSL library. # GSL::gslcblas - The CBLAS support library used by GSL. # # Result Variables # ^^^^^^^^^^^^^^^^ # # This module will set the following variables in your project:: # # GSL_FOUND - True if GSL found on the local system # GSL_INCLUDE_DIRS - Location of GSL header files. # GSL_LIBRARIES - The GSL libraries. # GSL_VERSION - The version of the discovered GSL install. # # Hints # ^^^^^ # # Set ``GSL_ROOT_DIR`` to a directory that contains a GSL installation. # # This script expects to find libraries at ``$GSL_ROOT_DIR/lib`` and the GSL # headers at ``$GSL_ROOT_DIR/include/gsl``. The library directory may # optionally provide Release and Debug folders. For Unix-like systems, this # script will use ``$GSL_ROOT_DIR/bin/gsl-config`` (if found) to aid in the # discovery GSL. # # Cache Variables # ^^^^^^^^^^^^^^^ # # This module may set the following variables depending on platform and type # of GSL installation discovered. These variables may optionally be set to # help this module find the correct files:: # # GSL_CLBAS_LIBRARY - Location of the GSL CBLAS library. # GSL_CBLAS_LIBRARY_DEBUG - Location of the debug GSL CBLAS library (if any). # GSL_CONFIG_EXECUTABLE - Location of the ``gsl-config`` script (if any). # GSL_LIBRARY - Location of the GSL library. # GSL_LIBRARY_DEBUG - Location of the debug GSL library (if any). # #============================================================================= # Copyright 2014 Kelly Thompson # # Distributed under the OSI-approved BSD License (the "License"); # see accompanying file Copyright.txt for details. # # This software is distributed WITHOUT ANY WARRANTY; without even the # implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. # See the License for more information. #============================================================================= # (To distribute this file outside of CMake, substitute the full # License text for the above reference.) # Include these modules to handle the QUIETLY and REQUIRED arguments. include(${CMAKE_CURRENT_LIST_DIR}/FindPackageHandleStandardArgs.cmake) #============================================================================= # If the user has provided ``GSL_ROOT_DIR``, use it! Choose items found # at this location over system locations. if( EXISTS "$ENV{GSL_ROOT_DIR}" ) file( TO_CMAKE_PATH "$ENV{GSL_ROOT_DIR}" GSL_ROOT_DIR ) set( GSL_ROOT_DIR "${GSL_ROOT_DIR}" CACHE PATH "Prefix for GSL installation." ) endif() if( NOT EXISTS "${GSL_ROOT_DIR}" ) set( GSL_USE_PKGCONFIG ON ) endif() #============================================================================= # As a first try, use the PkgConfig module. This will work on many # *NIX systems. See :module:`findpkgconfig` # This will return ``GSL_INCLUDEDIR`` and ``GSL_LIBDIR`` used below. if( GSL_USE_PKGCONFIG ) find_package(PkgConfig) pkg_check_modules( GSL QUIET gsl ) if( EXISTS "${GSL_INCLUDEDIR}" ) get_filename_component( GSL_ROOT_DIR "${GSL_INCLUDEDIR}" DIRECTORY CACHE) endif() endif() #============================================================================= # Set GSL_INCLUDE_DIRS and GSL_LIBRARIES. If we skipped the PkgConfig step, try # to find the libraries at $GSL_ROOT_DIR (if provided) or in standard system # locations. These find_library and find_path calls will prefer custom # locations over standard locations (HINTS). If the requested file is not found # at the HINTS location, standard system locations will be still be searched # (/usr/lib64 (Redhat), lib/i386-linux-gnu (Debian)). find_path( GSL_INCLUDE_DIR NAMES gsl/gsl_sf.h HINTS ${GSL_ROOT_DIR}/include ${GSL_INCLUDEDIR} ) find_library( GSL_LIBRARY NAMES gsl HINTS ${GSL_ROOT_DIR}/lib ${GSL_LIBDIR} PATH_SUFFIXES Release Debug ) find_library( GSL_CBLAS_LIBRARY NAMES gslcblas cblas HINTS ${GSL_ROOT_DIR}/lib ${GSL_LIBDIR} PATH_SUFFIXES Release Debug ) # Do we also have debug versions? find_library( GSL_LIBRARY_DEBUG NAMES gsl HINTS ${GSL_ROOT_DIR}/lib ${GSL_LIBDIR} PATH_SUFFIXES Debug ) find_library( GSL_CBLAS_LIBRARY_DEBUG NAMES gslcblas cblas HINTS ${GSL_ROOT_DIR}/lib ${GSL_LIBDIR} PATH_SUFFIXES Debug ) set( GSL_INCLUDE_DIRS ${GSL_INCLUDE_DIR} ) set( GSL_LIBRARIES ${GSL_LIBRARY} ${GSL_CBLAS_LIBRARY} ) # If we didn't use PkgConfig, try to find the version via gsl-config or by # reading gsl_version.h. if( NOT GSL_VERSION ) # 1. If gsl-config exists, query for the version. find_program( GSL_CONFIG_EXECUTABLE NAMES gsl-config HINTS "${GSL_ROOT_DIR}/bin" ) if( EXISTS "${GSL_CONFIG_EXECUTABLE}" ) execute_process( COMMAND "${GSL_CONFIG_EXECUTABLE}" --version OUTPUT_VARIABLE GSL_VERSION OUTPUT_STRIP_TRAILING_WHITESPACE ) endif() # 2. If gsl-config is not available, try looking in gsl/gsl_version.h if( NOT GSL_VERSION AND EXISTS "${GSL_INCLUDE_DIRS}/gsl/gsl_version.h" ) file( STRINGS "${GSL_INCLUDE_DIRS}/gsl/gsl_version.h" gsl_version_h_contents REGEX "define GSL_VERSION" ) string( REGEX REPLACE ".*([0-9].[0-9][0-9]).*" "\\1" GSL_VERSION ${gsl_version_h_contents} ) endif() # might also try scraping the directory name for a regex match "gsl-X.X" endif() #============================================================================= # handle the QUIETLY and REQUIRED arguments and set GSL_FOUND to TRUE if all # listed variables are TRUE find_package_handle_standard_args( GSL FOUND_VAR GSL_FOUND REQUIRED_VARS GSL_INCLUDE_DIR GSL_LIBRARY GSL_CBLAS_LIBRARY VERSION_VAR GSL_VERSION ) mark_as_advanced( GSL_ROOT_DIR GSL_VERSION GSL_LIBRARY GSL_INCLUDE_DIR GSL_CBLAS_LIBRARY GSL_LIBRARY_DEBUG GSL_CBLAS_LIBRARY_DEBUG GSL_USE_PKGCONFIG GSL_CONFIG ) #============================================================================= # Register imported libraries: # 1. If we can find a Windows .dll file (or if we can find both Debug and # Release libraries), we will set appropriate target properties for these. # 2. However, for most systems, we will only register the import location and # include directory. # Look for dlls, or Release and Debug libraries. if(WIN32) string( REPLACE ".lib" ".dll" GSL_LIBRARY_DLL "${GSL_LIBRARY}" ) string( REPLACE ".lib" ".dll" GSL_CBLAS_LIBRARY_DLL "${GSL_CBLAS_LIBRARY}" ) string( REPLACE ".lib" ".dll" GSL_LIBRARY_DEBUG_DLL "${GSL_LIBRARY_DEBUG}" ) string( REPLACE ".lib" ".dll" GSL_CBLAS_LIBRARY_DEBUG_DLL "${GSL_CBLAS_LIBRARY_DEBUG}" ) endif() if( GSL_FOUND AND NOT TARGET GSL::gsl ) if( EXISTS "${GSL_LIBRARY_DLL}" AND EXISTS "${GSL_CBLAS_LIBRARY_DLL}") # Windows systems with dll libraries. add_library( GSL::gsl SHARED IMPORTED ) add_library( GSL::gslcblas SHARED IMPORTED ) # Windows with dlls, but only Release libraries. set_target_properties( GSL::gslcblas PROPERTIES IMPORTED_LOCATION_RELEASE "${GSL_CBLAS_LIBRARY_DLL}" IMPORTED_IMPLIB "${GSL_CBLAS_LIBRARY}" INTERFACE_INCLUDE_DIRECTORIES "${GSL_INCLUDE_DIRS}" IMPORTED_CONFIGURATIONS Release IMPORTED_LINK_INTERFACE_LANGUAGES "C" ) set_target_properties( GSL::gsl PROPERTIES IMPORTED_LOCATION_RELEASE "${GSL_LIBRARY_DLL}" IMPORTED_IMPLIB "${GSL_LIBRARY}" INTERFACE_INCLUDE_DIRECTORIES "${GSL_INCLUDE_DIRS}" IMPORTED_CONFIGURATIONS Release IMPORTED_LINK_INTERFACE_LANGUAGES "C" INTERFACE_LINK_LIBRARIES GSL::gslcblas ) # If we have both Debug and Release libraries if( EXISTS "${GSL_LIBRARY_DEBUG_DLL}" AND EXISTS "${GSL_CBLAS_LIBRARY_DEBUG_DLL}") set_property( TARGET GSL::gslcblas APPEND PROPERTY IMPORTED_CONFIGURATIONS Debug ) set_target_properties( GSL::gslcblas PROPERTIES IMPORTED_LOCATION_DEBUG "${GSL_CBLAS_LIBRARY_DEBUG_DLL}" IMPORTED_IMPLIB_DEBUG "${GSL_CBLAS_LIBRARY_DEBUG}" ) set_property( TARGET GSL::gsl APPEND PROPERTY IMPORTED_CONFIGURATIONS Debug ) set_target_properties( GSL::gsl PROPERTIES IMPORTED_LOCATION_DEBUG "${GSL_LIBRARY_DEBUG_DLL}" IMPORTED_IMPLIB_DEBUG "${GSL_LIBRARY_DEBUG}" ) endif() else() # For all other environments (ones without dll libraries), create # the imported library targets. add_library( GSL::gsl UNKNOWN IMPORTED ) add_library( GSL::gslcblas UNKNOWN IMPORTED ) set_target_properties( GSL::gslcblas PROPERTIES IMPORTED_LOCATION "${GSL_CBLAS_LIBRARY}" INTERFACE_INCLUDE_DIRECTORIES "${GSL_INCLUDE_DIRS}" IMPORTED_LINK_INTERFACE_LANGUAGES "C" ) set_target_properties( GSL::gsl PROPERTIES IMPORTED_LOCATION "${GSL_LIBRARY}" INTERFACE_INCLUDE_DIRECTORIES "${GSL_INCLUDE_DIRS}" IMPORTED_LINK_INTERFACE_LANGUAGES "C" INTERFACE_LINK_LIBRARIES GSL::gslcblas ) endif() endif() libindi/cmake_modules/FindAIOUSB.cmake0000664000175000017500000000371313263645566017073 0ustar jasemjasem# - Try to find libaiousb # Once done this will define # # AIOUSB_FOUND - system has AIOUSB # AIOUSB_INCLUDE_DIR - the AIOUSB include directory # AIOUSB_LIBRARIES - Link these to use AIOUSB (C) # AIOUSB_CPP_LIBRARIES - Link these to use AIOUSB (C++) # Copyright (c) 2006, Jasem Mutlaq # Based on FindLibfacile by Carsten Niehaus, # # Redistribution and use is allowed according to the terms of the BSD license. # For details see the accompanying COPYING-CMAKE-SCRIPTS file. if (AIOUSB_INCLUDE_DIR AND AIOUSB_LIBRARIES AND AIOUSB_CPP_LIBRARIES) # in cache already set(AIOUSB_FOUND TRUE) message(STATUS "Found libaiusb: ${AIOUSB_LIBRARIES}") message(STATUS "Found libaiusbcpp: ${AIOUSB_CPP_LIBRARIES}") else (AIOUSB_INCLUDE_DIR AND AIOUSB_LIBRARIES AND AIOUSB_CPP_LIBRARIES) find_path(AIOUSB_INCLUDE_DIR aiousb.h ${_obIncDir} ${GNUWIN32_DIR}/include ) find_library(AIOUSB_LIBRARIES NAMES aiousb PATHS ${_obLinkDir} ${GNUWIN32_DIR}/lib ) find_library(AIOUSB_CPP_LIBRARIES NAMES aiousbcpp PATHS ${_obLinkDir} ${GNUWIN32_DIR}/lib ) if(AIOUSB_INCLUDE_DIR AND AIOUSB_LIBRARIES AND AIOUSB_CPP_LIBRARIES) set(AIOUSB_FOUND TRUE) else (AIOUSB_INCLUDE_DIR AND AIOUSB_LIBRARIES AND AIOUSB_CPP_LIBRARIES) set(AIOUSB_FOUND FALSE) endif(AIOUSB_INCLUDE_DIR AND AIOUSB_LIBRARIES AND AIOUSB_CPP_LIBRARIES) if (AIOUSB_FOUND) if (NOT AIOUSB_FIND_QUIETLY) message(STATUS "Found libaiousb: ${AIOUSB_LIBRARIES}") message(STATUS "Found libaiusbcpp: ${AIOUSB_CPP_LIBRARIES}") endif (NOT AIOUSB_FIND_QUIETLY) else (AIOUSB_FOUND) if (AIOUSB_FIND_REQUIRED) message(FATAL_ERROR "libaiousb not found. Please install libaiousb. https://www.accesio.com") endif (AIOUSB_FIND_REQUIRED) endif (AIOUSB_FOUND) mark_as_advanced(AIOUSB_INCLUDE_DIR AIOUSB_LIBRARIES AIOUSB_CPP_LIBRARIES) endif (AIOUSB_INCLUDE_DIR AND AIOUSB_LIBRARIES AND AIOUSB_CPP_LIBRARIES) libindi/cmake_modules/FindFFTW3.cmake0000664000175000017500000000257313263645566016745 0ustar jasemjasem# - Try to find FFTW3 # Once done this will define # # FFTW3_FOUND - system has FFTW3 # FFTW3_INCLUDE_DIR - the FFTW3 include directory # FFTW3_LIBRARIES - Link these to use FFTW3 # FFTW3_VERSION_STRING - Human readable version number of fftw3 # FFTW3_VERSION_MAJOR - Major version number of fftw3 # FFTW3_VERSION_MINOR - Minor version number of fftw3 # Copyright (c) 2017, Ilia Platone, # Based on FindLibfacile by Carsten Niehaus, # # Redistribution and use is allowed according to the terms of the BSD license. # For details see the accompanying COPYING-CMAKE-SCRIPTS file. if (FFTW3_LIBRARIES) # in cache already set(FFTW3_FOUND TRUE) message(STATUS "Found FFTW3: ${FFTW3_LIBRARIES}") else (FFTW3_LIBRARIES) find_library(FFTW3_LIBRARIES NAMES fftw3 PATHS ${_obLinkDir} ${GNUWIN32_DIR}/lib /usr/local/lib ) if(FFTW3_LIBRARIES) set(FFTW3_FOUND TRUE) else (FFTW3_LIBRARIES) set(FFTW3_FOUND FALSE) endif(FFTW3_LIBRARIES) if (FFTW3_FOUND) if (NOT FFTW3_FIND_QUIETLY) message(STATUS "Found FFTW3: ${FFTW3_LIBRARIES}") endif (NOT FFTW3_FIND_QUIETLY) else (FFTW3_FOUND) if (FFTW3_FIND_REQUIRED) message(FATAL_ERROR "FFTW3 not found. Please install libfftw3-dev") endif (FFTW3_FIND_REQUIRED) endif (FFTW3_FOUND) mark_as_advanced(FFTW3_LIBRARIES) endif (FFTW3_LIBRARIES) libindi/cmake_modules/FindOggTheora.cmake0000664000175000017500000000247213263645566017771 0ustar jasemjasem# # Find the native Ogg/Theora includes and libraries # # This module defines # OGGTHEORA_INCLUDE_DIR, where to find ogg/ogg.h and theora/theora.h # OGGTHEORA_LIBRARIES, the libraries to link against to use Ogg/Theora. # OGGTHEORA_FOUND, If false, do not try to use Ogg/Theora. FIND_PATH(OGGTHEORA_ogg_INCLUDE_DIR ogg/ogg.h) FIND_PATH(OGGTHEORA_theora_INCLUDE_DIR theora/theora.h) FIND_LIBRARY(OGGTHEORA_ogg_LIBRARY ogg) FIND_LIBRARY(OGGTHEORA_theoraenc_LIBRARY theoraenc) FIND_LIBRARY(OGGTHEORA_theoradec_LIBRARY theoradec) SET(OGGTHEORA_INCLUDE_DIRS ${OGGTHEORA_ogg_INCLUDE_DIR} ${OGGTHEORA_theora_INCLUDE_DIR} ) #HACK multiple directories SET(OGGTHEORA_INCLUDE_DIR ${OGGTHEORA_INCLUDE_DIRS}) SET(OGGTHEORA_LIBRARIES ${OGGTHEORA_theoraenc_LIBRARY} ${OGGTHEORA_theoradec_LIBRARY} ${OGGTHEORA_ogg_LIBRARY} ) #HACK multiple libraries SET(OGGTHEORA_LIBRARY ${OGGTHEORA_LIBRARIES}) INCLUDE(FindPackageHandleStandardArgs) FIND_PACKAGE_HANDLE_STANDARD_ARGS(OGGTHEORA "Could NOT find the ogg and theora libraries" OGGTHEORA_ogg_LIBRARY OGGTHEORA_theoraenc_LIBRARY OGGTHEORA_theoradec_LIBRARY OGGTHEORA_ogg_INCLUDE_DIR OGGTHEORA_theora_INCLUDE_DIR ) MARK_AS_ADVANCED(OGGTHEORA_ogg_INCLUDE_DIR OGGTHEORA_theora_INCLUDE_DIR OGGTHEORA_ogg_LIBRARY OGGTHEORA_theoraenc_LIBRARY OGGTHEORA_theoradec_LIBRARY ) libindi/cmake_modules/FindPackageMessage.cmake0000664000175000017500000000375513263645566020757 0ustar jasemjasem#.rst: # FindPackageMessage # ------------------ # # # # FIND_PACKAGE_MESSAGE( "message for user" "find result details") # # This macro is intended to be used in FindXXX.cmake modules files. It # will print a message once for each unique find result. This is useful # for telling the user where a package was found. The first argument # specifies the name (XXX) of the package. The second argument # specifies the message to display. The third argument lists details # about the find result so that if they change the message will be # displayed again. The macro also obeys the QUIET argument to the # find_package command. # # Example: # # :: # # if(X11_FOUND) # FIND_PACKAGE_MESSAGE(X11 "Found X11: ${X11_X11_LIB}" # "[${X11_X11_LIB}][${X11_INCLUDE_DIR}]") # else() # ... # endif() #============================================================================= # Copyright 2008-2009 Kitware, Inc. # # Distributed under the OSI-approved BSD License (the "License"); # see accompanying file Copyright.txt for details. # # This software is distributed WITHOUT ANY WARRANTY; without even the # implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. # See the License for more information. #============================================================================= # (To distribute this file outside of CMake, substitute the full # License text for the above reference.) function(FIND_PACKAGE_MESSAGE pkg msg details) # Avoid printing a message repeatedly for the same find result. if(NOT ${pkg}_FIND_QUIETLY) string(REPLACE "\n" "" details "${details}") set(DETAILS_VAR FIND_PACKAGE_MESSAGE_DETAILS_${pkg}) if(NOT "${details}" STREQUAL "${${DETAILS_VAR}}") # The message has not yet been printed. message(STATUS "${msg}") # Save the find details in the cache to avoid printing the same # message again. set("${DETAILS_VAR}" "${details}" CACHE INTERNAL "Details about finding ${pkg}") endif() endif() endfunction() libindi/cmake_modules/FindSBIG.cmake0000664000175000017500000000251213263645566016631 0ustar jasemjasem# - Try to find SBIG Universal Library # Once done this will define # # SBIG_FOUND - system has SBIG # SBIG_INCLUDE_DIR - the SBIG include directory # SBIG_LIBRARIES - Link these to use SBIG # Redistribution and use is allowed according to the terms of the BSD license. # For details see the accompanying COPYING-CMAKE-SCRIPTS file. if (SBIG_INCLUDE_DIR AND SBIG_LIBRARIES) # in cache already set(SBIG_FOUND TRUE) message(STATUS "Found libsbig: ${SBIG_LIBRARIES}") else (SBIG_INCLUDE_DIR AND SBIG_LIBRARIES) find_path(SBIG_INCLUDE_DIR sbigudrv.h PATH_SUFFIXES libsbig ${_obIncDir} ${GNUWIN32_DIR}/include ) find_library(SBIG_LIBRARIES NAMES sbig PATHS ${_obLinkDir} ${GNUWIN32_DIR}/lib ) if(SBIG_INCLUDE_DIR AND SBIG_LIBRARIES) set(SBIG_FOUND TRUE) else (SBIG_INCLUDE_DIR AND SBIG_LIBRARIES) set(SBIG_FOUND FALSE) endif(SBIG_INCLUDE_DIR AND SBIG_LIBRARIES) if (SBIG_FOUND) if (NOT SBIG_FIND_QUIETLY) message(STATUS "Found SBIG: ${SBIG_LIBRARIES}") endif (NOT SBIG_FIND_QUIETLY) else (SBIG_FOUND) if (SBIG_FIND_REQUIRED) message(FATAL_ERROR "SBIG not found. Please install SBIG Library http://www.indilib.org") endif (SBIG_FIND_REQUIRED) endif (SBIG_FOUND) mark_as_advanced(SBIG_INCLUDE_DIR SBIG_LIBRARIES) endif (SBIG_INCLUDE_DIR AND SBIG_LIBRARIES) libindi/cmake_modules/CMakeParseArguments.cmake0000664000175000017500000000164213263645566021150 0ustar jasemjasem#.rst: # CMakeParseArguments # ------------------- # # This module once implemented the :command:`cmake_parse_arguments` command # that is now implemented natively by CMake. It is now an empty placeholder # for compatibility with projects that include it to get the command from # CMake 3.4 and lower. #============================================================================= # Copyright 2010 Alexander Neundorf # # Distributed under the OSI-approved BSD License (the "License"); # see accompanying file Copyright.txt for details. # # This software is distributed WITHOUT ANY WARRANTY; without even the # implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. # See the License for more information. #============================================================================= # (To distribute this file outside of CMake, substitute the full # License text for the above reference.) libindi/COPYRIGHT0000664000175000017500000001306213263645557012767 0ustar jasemjasemFiles: * Copyright: 1996-1998, Patrick Reynolds 2003-2007, Elwood C. Downey 2003-2007, 2010, Jasem Mutlaq 2003, Jason Harris 2003, John Kielkopf 2004, Francois Meyer 2005, Bruce Bockius 2005, Douglas Philipson 2005, Gaetano Vocca 2006-2007, Markus Wildi License: LGPL-2.1+ Files: cmake/* Copyright: Bryan Donlan Carsten Niehaus 2006, Alexander Neundorf 2006, Allen Winter 2006, 2008, 2011, Jasem Mutlaq 2009, Geoffrey Hausheer License: BSD-3-clause Files: libs/webcam/ccvt_c2.c libs/webcam/ccvt.h libs/webcam/ccvt_misc.c libs/webcam/ccvt_types.h libs/webcam/vcvt.h libs/webcam/videodev.h libs/webcam/videodev2.h Copyright: Justin Schoeman 2001, Tony Hague 2002, Nemosoft Unv. 2006, Bill Dirks 2010, Gerry Rozema License: GPL-2+ Files: libs/webcam/port.* libs/webcam/pwc-ioctl.h Copyright: Anders Arpteg Patrick Reynolds Charles Henrich 1996-1998, Patrick Reynolds 2001-2004, Nemosoft Unv. 2004-2006, Luc Saillard License: LGPL-2+ Files: debian/* Copyright: 2011, Pino Toscano License: GPL-2+ License: BSD-3-clause Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: . 1. Redistributions of source code must retain the copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 3. The name of the author may not be used to endorse or promote products derived from this software without specific prior written permission. . THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. License: LGPL-2+ This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2 as published by the Free Software Foundation. . 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 Library General Public License for more details. . You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. . On Debian systems, the complete text of the GNU Library General Public License version 2 can be found in `/usr/share/common-licenses/LGPL-2'. License: LGPL-2.1+ This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2.1 as published by the Free Software Foundation. . 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 Library General Public License for more details. . You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. . On Debian systems, the complete text of the GNU Library General Public License version 2.1 can be found in `/usr/share/common-licenses/LGPL-2.1'. License: GPL-2+ This package is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. . This package 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 General Public License for more details. . You should have received a copy of the GNU General Public License along with this program. If not, see . On Debian systems, the complete texts of the GNU General Public Licenses version 2 and 3 can be found in `/usr/share/common-licenses/GPL-2' and `/usr/share/common-licenses/GPL-3'. libindi/drivers/0000775000175000017500000000000013263645557013150 5ustar jasemjasemlibindi/drivers/weather/0000775000175000017500000000000013263645557014607 5ustar jasemjasemlibindi/drivers/weather/weathermeta.cpp0000664000175000017500000002020213263645557017615 0ustar jasemjasem/******************************************************************************* Copyright(c) 2015 Jasem Mutlaq. All rights reserved. INDI Weather Underground (TM) Weather Driver This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program 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 General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. The full GNU General Public License is included in this distribution in the file called LICENSE. *******************************************************************************/ #include "weathermeta.h" #include #include // We declare an auto pointer to WeatherMeta. std::unique_ptr weatherMeta(new WeatherMeta()); void ISGetProperties(const char *dev) { weatherMeta->ISGetProperties(dev); } void ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) { weatherMeta->ISNewSwitch(dev, name, states, names, n); } void ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n) { weatherMeta->ISNewText(dev, name, texts, names, n); } void ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) { weatherMeta->ISNewNumber(dev, name, values, names, n); } void ISNewBLOB(const char *dev, const char *name, int sizes[], int blobsizes[], char *blobs[], char *formats[], char *names[], int n) { INDI_UNUSED(dev); INDI_UNUSED(name); INDI_UNUSED(sizes); INDI_UNUSED(blobsizes); INDI_UNUSED(blobs); INDI_UNUSED(formats); INDI_UNUSED(names); INDI_UNUSED(n); } void ISSnoopDevice(XMLEle *root) { weatherMeta->ISSnoopDevice(root); } WeatherMeta::WeatherMeta() { setVersion(1, 0); updatePeriods[0] = updatePeriods[1] = updatePeriods[2] = updatePeriods[3] = -1; } const char *WeatherMeta::getDefaultName() { return (const char *)"Weather Meta"; } bool WeatherMeta::Connect() { return true; } bool WeatherMeta::Disconnect() { return true; } bool WeatherMeta::initProperties() { INDI::DefaultDevice::initProperties(); // Active Devices IUFillText(&ActiveDeviceT[0], "ACTIVE_WEATHER_1", "Station #1", nullptr); IUFillText(&ActiveDeviceT[1], "ACTIVE_WEATHER_2", "Station #2", nullptr); IUFillText(&ActiveDeviceT[2], "ACTIVE_WEATHER_3", "Station #3", nullptr); IUFillText(&ActiveDeviceT[3], "ACTIVE_WEATHER_4", "Station #4", nullptr); IUFillTextVector(&ActiveDeviceTP, ActiveDeviceT, 4, getDeviceName(), "ACTIVE_DEVICES", "Stations", OPTIONS_TAB, IP_RW, 60, IPS_IDLE); // Station Status IUFillLight(&StationL[0], "STATION_STATUS_1", "Station #1", IPS_IDLE); IUFillLight(&StationL[1], "STATION_STATUS_2", "Station #2", IPS_IDLE); IUFillLight(&StationL[2], "STATION_STATUS_3", "Station #3", IPS_IDLE); IUFillLight(&StationL[3], "STATION_STATUS_4", "Station #4", IPS_IDLE); IUFillLightVector(&StationLP, StationL, 4, getDeviceName(), "WEATHER_STATUS", "Status", MAIN_CONTROL_TAB, IPS_IDLE); // Update Period IUFillNumber(&UpdatePeriodN[0], "PERIOD", "Period (secs)", "%4.2f", 0, 3600, 60, 60); IUFillNumberVector(&UpdatePeriodNP, UpdatePeriodN, 1, getDeviceName(), "WEATHER_UPDATE", "Update", MAIN_CONTROL_TAB, IP_RO, 60, IPS_IDLE); addDebugControl(); setDriverInterface(AUX_INTERFACE); return true; } void WeatherMeta::ISGetProperties(const char *dev) { INDI::DefaultDevice::ISGetProperties(dev); defineText(&ActiveDeviceTP); loadConfig(true, "ACTIVE_DEVICES"); } bool WeatherMeta::updateProperties() { INDI::DefaultDevice::updateProperties(); if (isConnected()) { // If Active devices are already defined, let's set the active devices as labels for (int i = 0; i < 4; i++) { if (ActiveDeviceT[i].text != nullptr && ActiveDeviceT[i].text[0] != 0) strncpy(StationL[i].label, ActiveDeviceT[i].text, MAXINDILABEL); } defineLight(&StationLP); defineNumber(&UpdatePeriodNP); } else { deleteProperty(StationLP.name); deleteProperty(UpdatePeriodNP.name); } return true; } bool WeatherMeta::ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n) { if (dev != nullptr && strcmp(dev, getDeviceName()) == 0) { if (strcmp(name, ActiveDeviceTP.name) == 0) { ActiveDeviceTP.s = IPS_OK; IUUpdateText(&ActiveDeviceTP, texts, names, n); // Update client display IDSetText(&ActiveDeviceTP, nullptr); if (ActiveDeviceT[0].text != nullptr) { IDSnoopDevice(ActiveDeviceT[0].text, "WEATHER_STATUS"); IDSnoopDevice(ActiveDeviceT[0].text, "WEATHER_UPDATE"); } if (ActiveDeviceT[1].text != nullptr) { IDSnoopDevice(ActiveDeviceT[1].text, "WEATHER_STATUS"); IDSnoopDevice(ActiveDeviceT[0].text, "WEATHER_UPDATE"); } if (ActiveDeviceT[2].text != nullptr) { IDSnoopDevice(ActiveDeviceT[2].text, "WEATHER_STATUS"); IDSnoopDevice(ActiveDeviceT[2].text, "WEATHER_UPDATE"); } if (ActiveDeviceT[3].text != nullptr) { IDSnoopDevice(ActiveDeviceT[3].text, "WEATHER_STATUS"); IDSnoopDevice(ActiveDeviceT[2].text, "WEATHER_UPDATE"); } return true; } } return INDI::DefaultDevice::ISNewText(dev, name, texts, names, n); } bool WeatherMeta::saveConfigItems(FILE *fp) { INDI::DefaultDevice::saveConfigItems(fp); IUSaveConfigText(fp, &ActiveDeviceTP); IUSaveConfigNumber(fp, &UpdatePeriodNP); return true; } bool WeatherMeta::ISSnoopDevice(XMLEle *root) { const char *propName = findXMLAttValu(root, "name"); const char *deviceName = findXMLAttValu(root, "device"); if (isConnected()) { if (strcmp(propName, "WEATHER_STATUS") == 0) { for (int i = 0; i < 4; i++) { if (ActiveDeviceT[i].text != nullptr && strcmp(ActiveDeviceT[i].text, deviceName) == 0) { IPState stationState; if (crackIPState(findXMLAttValu(root, "state"), &stationState) < 0) break; StationL[i].s = stationState; updateOverallState(); break; } } return true; } if (strcmp(propName, "WEATHER_UPDATE") == 0) { XMLEle *ep = nextXMLEle(root, 1); for (int i = 0; i < 4; i++) { if (ActiveDeviceT[i].text != nullptr && strcmp(ActiveDeviceT[i].text, deviceName) == 0) { updatePeriods[i] = atof(pcdataXMLEle(ep)); updateUpdatePeriod(); break; } } } } return INDI::DefaultDevice::ISSnoopDevice(root); } void WeatherMeta::updateOverallState() { StationLP.s = IPS_IDLE; for (int i = 0; i < 4; i++) { if (StationL[i].s > StationLP.s) StationLP.s = StationL[i].s; } IDSetLight(&StationLP, nullptr); } void WeatherMeta::updateUpdatePeriod() { double minPeriod = UpdatePeriodN[0].max; for (int i = 0; i < 4; i++) { if (updatePeriods[i] > 0 && updatePeriods[i] < minPeriod) minPeriod = updatePeriods[i]; } if (minPeriod != UpdatePeriodN[0].max) { UpdatePeriodN[0].value = minPeriod; IDSetNumber(&UpdatePeriodNP, nullptr); } } libindi/drivers/weather/mbox.h0000664000175000017500000000434513263645557015733 0ustar jasemjasem/******************************************************************************* Copyright(c) 2017 Jasem Mutlaq. All rights reserved. INDI MBox Driver This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program 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 General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. The full GNU General Public License is included in this distribution in the file called LICENSE. *******************************************************************************/ #pragma once #include "indiweather.h" class MBox : public INDI::Weather { public: MBox(); virtual ~MBox() = default; // Generic indi device entries virtual bool Handshake() override; virtual const char *getDefaultName() override; virtual bool initProperties() override; virtual bool updateProperties() override; virtual bool ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) override; virtual bool ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) override; protected: virtual IPState updateWeather() override; private: typedef enum { ACK_OK_STARTUP, ACK_OK_INIT, ACK_ERROR } AckResponse; typedef enum { CAL_PRESSURE, CAL_TEMPERATURE, CAL_HUMIDITY } CalibrationType; AckResponse ack(); bool verifyCRC(const char *response); bool getCalibration(bool sendCommand=true); bool setCalibration(CalibrationType type); bool resetCalibration(); INumber CalibrationN[3]; INumberVectorProperty CalibrationNP; ISwitch ResetS[1]; ISwitchVectorProperty ResetSP; IText FirmwareT[1] {}; ITextVectorProperty FirmwareTP; }; libindi/drivers/weather/weathermeta.h0000664000175000017500000000416013263645557017267 0ustar jasemjasem/******************************************************************************* Copyright(c) 2015 Jasem Mutlaq. All rights reserved. INDI Weather Meta Driver. It watches up to 4 weather drivers and report worst case of each in a single property. This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program 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 General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. The full GNU General Public License is included in this distribution in the file called LICENSE. *******************************************************************************/ #pragma once #include "defaultdevice.h" class WeatherMeta : public INDI::DefaultDevice { public: WeatherMeta(); virtual ~WeatherMeta() = default; // Generic indi device entries bool Connect(); bool Disconnect(); const char *getDefaultName(); virtual bool ISSnoopDevice(XMLEle *root); virtual bool initProperties(); virtual bool updateProperties(); virtual void ISGetProperties(const char *dev); virtual bool ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n); protected: virtual bool saveConfigItems(FILE *fp); private: void updateOverallState(); void updateUpdatePeriod(); // Active stations IText ActiveDeviceT[4] {}; ITextVectorProperty ActiveDeviceTP; // Stations status ILight StationL[4]; ILightVectorProperty StationLP; // Update Period INumber UpdatePeriodN[1]; INumberVectorProperty UpdatePeriodNP; double updatePeriods[4]; }; libindi/drivers/weather/wunderground.cpp0000664000175000017500000002225613263645557020045 0ustar jasemjasem/******************************************************************************* Copyright(c) 2015 Jasem Mutlaq. All rights reserved. INDI Weather Underground (TM) Weather Driver This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program 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 General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. The full GNU General Public License is included in this distribution in the file called LICENSE. *******************************************************************************/ #include "wunderground.h" #include "gason.h" #include "locale_compat.h" #include #include #include // We declare an auto pointer to WunderGround. std::unique_ptr wunderGround(new WunderGround()); static size_t WriteCallback(void *contents, size_t size, size_t nmemb, void *userp) { ((std::string *)userp)->append((char *)contents, size * nmemb); return size * nmemb; } void ISGetProperties(const char *dev) { wunderGround->ISGetProperties(dev); } void ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) { wunderGround->ISNewSwitch(dev, name, states, names, n); } void ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n) { wunderGround->ISNewText(dev, name, texts, names, n); } void ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) { wunderGround->ISNewNumber(dev, name, values, names, n); } void ISNewBLOB(const char *dev, const char *name, int sizes[], int blobsizes[], char *blobs[], char *formats[], char *names[], int n) { INDI_UNUSED(dev); INDI_UNUSED(name); INDI_UNUSED(sizes); INDI_UNUSED(blobsizes); INDI_UNUSED(blobs); INDI_UNUSED(formats); INDI_UNUSED(names); INDI_UNUSED(n); } void ISSnoopDevice(XMLEle *root) { wunderGround->ISSnoopDevice(root); } WunderGround::WunderGround() { setVersion(1, 0); wunderLat = -1000; wunderLong = -1000; setWeatherConnection(CONNECTION_NONE); } WunderGround::~WunderGround() { } const char *WunderGround::getDefaultName() { return (const char *)"WunderGround"; } bool WunderGround::Connect() { if (wunderAPIKeyT[0].text == nullptr) { LOG_ERROR("Weather Underground API Key is not available. Please register your API key at " "www.wunderground.com and save it under Options."); return false; } return true; } bool WunderGround::Disconnect() { return true; } bool WunderGround::initProperties() { INDI::Weather::initProperties(); IUFillText(&wunderAPIKeyT[0], "API_KEY", "API Key", nullptr); IUFillTextVector(&wunderAPIKeyTP, wunderAPIKeyT, 1, getDeviceName(), "WUNDER_API_KEY", "Wunder", OPTIONS_TAB, IP_RW, 60, IPS_IDLE); addParameter("WEATHER_FORECAST", "Weather", 0, 0, 0, 1); addParameter("WEATHER_TEMPERATURE", "Temperature (C)", -10, 30, -20, 40); addParameter("WEATHER_WIND_SPEED", "Wind (kph)", 0, 20, 0, 40); addParameter("WEATHER_WIND_GUST", "Gust (kph)", 0, 20, 0, 50); addParameter("WEATHER_RAIN_HOUR", "Precip (mm)", 0, 0, 0, 0); setCriticalParameter("WEATHER_FORECAST"); setCriticalParameter("WEATHER_TEMPERATURE"); setCriticalParameter("WEATHER_WIND_SPEED"); setCriticalParameter("WEATHER_RAIN_HOUR"); addDebugControl(); return true; } void WunderGround::ISGetProperties(const char *dev) { INDI::Weather::ISGetProperties(dev); defineText(&wunderAPIKeyTP); loadConfig(true, "WUNDER_API_KEY"); } bool WunderGround::ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n) { if (dev != nullptr && strcmp(dev, getDeviceName()) == 0) { if (!strcmp(wunderAPIKeyTP.name, name)) { IUUpdateText(&wunderAPIKeyTP, texts, names, n); wunderAPIKeyTP.s = IPS_OK; IDSetText(&wunderAPIKeyTP, nullptr); return true; } } return INDI::Weather::ISNewText(dev, name, texts, names, n); } bool WunderGround::updateLocation(double latitude, double longitude, double elevation) { INDI_UNUSED(elevation); wunderLat = latitude; wunderLong = (longitude > 180) ? (longitude - 360) : longitude; return true; } IPState WunderGround::updateWeather() { CURL *curl; CURLcode res; std::string readBuffer; char requestURL[MAXRBUF]; // If location is not updated yet, return busy if (wunderLat == -1000 || wunderLong == -1000) return IPS_BUSY; AutoCNumeric locale; snprintf(requestURL, MAXRBUF, "http://api.wunderground.com/api/%s/conditions/q/%g,%g.json", wunderAPIKeyT[0].text, wunderLat, wunderLong); curl = curl_easy_init(); if (curl) { curl_easy_setopt(curl, CURLOPT_URL, requestURL); curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, WriteCallback); curl_easy_setopt(curl, CURLOPT_WRITEDATA, &readBuffer); res = curl_easy_perform(curl); curl_easy_cleanup(curl); } char srcBuffer[readBuffer.size()]; strncpy(srcBuffer, readBuffer.c_str(), readBuffer.size()); char *source = srcBuffer; // do not forget terminate source string with 0 char *endptr; JsonValue value; JsonAllocator allocator; int status = jsonParse(source, &endptr, &value, allocator); if (status != JSON_OK) { LOGF_ERROR("%s at %zd", jsonStrError(status), endptr - source); LOGF_DEBUG("%s", requestURL); LOGF_DEBUG("%s", readBuffer.c_str()); return IPS_ALERT; } JsonIterator it; JsonIterator observationIterator; for (it = begin(value); it != end(value); ++it) { if (!strcmp(it->key, "current_observation")) { for (observationIterator = begin(it->value); observationIterator != end(it->value); ++observationIterator) { if (!strcmp(observationIterator->key, "weather")) { char *value = observationIterator->value.toString(); if (!strcmp(value, "Clear")) setParameterValue("WEATHER_FORECAST", 0); else if (!strcmp(value, "Unknown") || !strcmp(value, "Scattered Clouds") || !strcmp(value, "Partly Cloudy") || !strcmp(value, "Overcast") || !strcmp(value, "Patches of Fog") || !strcmp(value, "Partial Fog") || !strcmp(value, "Light Haze")) setParameterValue("WEATHER_FORECAST", 1); else setParameterValue("WEATHER_FORECAST", 2); LOGF_INFO("Weather condition: %s", value); } else if (!strcmp(observationIterator->key, "temp_c")) { if (observationIterator->value.isDouble()) setParameterValue("WEATHER_TEMPERATURE", observationIterator->value.toNumber()); else setParameterValue("WEATHER_TEMPERATURE", atof(observationIterator->value.toString())); } else if (!strcmp(observationIterator->key, "wind_kph")) { if (observationIterator->value.isDouble()) setParameterValue("WEATHER_WIND_SPEED", observationIterator->value.toNumber()); else setParameterValue("WEATHER_WIND_SPEED", atof(observationIterator->value.toString())); } else if (!strcmp(observationIterator->key, "wind_gust_kph")) { if (observationIterator->value.isDouble()) setParameterValue("WEATHER_WIND_GUST", observationIterator->value.toNumber()); else setParameterValue("WEATHER_WIND_GUST", atof(observationIterator->value.toString())); } else if (!strcmp(observationIterator->key, "precip_1hr_metric")) { char *value = observationIterator->value.toString(); double mm = -1; if (!strcmp(value, "--")) setParameterValue("WEATHER_RAIN_HOUR", 0); else { mm = atof(value); if (mm >= 0) setParameterValue("WEATHER_RAIN_HOUR", mm); } } } } } return IPS_OK; } bool WunderGround::saveConfigItems(FILE *fp) { INDI::Weather::saveConfigItems(fp); IUSaveConfigText(fp, &wunderAPIKeyTP); return true; } libindi/drivers/weather/vantage.h0000664000175000017500000000277313263645557016416 0ustar jasemjasem/******************************************************************************* Copyright(c) 2015 Jasem Mutlaq. All rights reserved. INDI Davis Vantage Pro/Pro2/Vue Weather Driver This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program 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 General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. The full GNU General Public License is included in this distribution in the file called LICENSE. *******************************************************************************/ #pragma once #include "indiweather.h" class Vantage : public INDI::Weather { public: Vantage(); virtual ~Vantage() = default; // Generic indi device entries virtual bool Handshake() override; virtual const char *getDefaultName() override; virtual bool initProperties() override; protected: virtual IPState updateWeather() override; private: bool ack(); bool wakeup(); }; libindi/drivers/weather/vantage.cpp0000664000175000017500000003116513263645557016746 0ustar jasemjasem/******************************************************************************* Copyright(c) 2015 Jasem Mutlaq. All rights reserved. INDI Davis Vantage Pro/Pro2/Vue Weather Driver This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program 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 General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. The full GNU General Public License is included in this distribution in the file called LICENSE. *******************************************************************************/ #include "vantage.h" #include "indicom.h" #include "connectionplugins/connectionserial.h" #include #include #include #define VANTAGE_CMD 8 #define VANTAGE_RES 128 #define VANTAGE_TIMEOUT 2 static uint16_t crc_table[] = { 0x0000, 0x1021, 0x2042, 0x3063, 0x4084, 0x50a5, 0x60c6, 0x70e7, 0x8108, 0x9129, 0xa14a, 0xb16b, 0xc18c, 0xd1ad, 0xe1ce, 0xf1ef, 0x1231, 0x0210, 0x3273, 0x2252, 0x52b5, 0x4294, 0x72f7, 0x62d6, 0x9339, 0x8318, 0xb37b, 0xa35a, 0xd3bd, 0xc39c, 0xf3ff, 0xe3de, 0x2462, 0x3443, 0x0420, 0x1401, 0x64e6, 0x74c7, 0x44a4, 0x5485, 0xa56a, 0xb54b, 0x8528, 0x9509, 0xe5ee, 0xf5cf, 0xc5ac, 0xd58d, 0x3653, 0x2672, 0x1611, 0x0630, 0x76d7, 0x66f6, 0x5695, 0x46b4, 0xb75b, 0xa77a, 0x9719, 0x8738, 0xf7df, 0xe7fe, 0xd79d, 0xc7bc, 0x48c4, 0x58e5, 0x6886, 0x78a7, 0x0840, 0x1861, 0x2802, 0x3823, 0xc9cc, 0xd9ed, 0xe98e, 0xf9af, 0x8948, 0x9969, 0xa90a, 0xb92b, 0x5af5, 0x4ad4, 0x7ab7, 0x6a96, 0x1a71, 0x0a50, 0x3a33, 0x2a12, 0xdbfd, 0xcbdc, 0xfbbf, 0xeb9e, 0x9b79, 0x8b58, 0xbb3b, 0xab1a, 0x6ca6, 0x7c87, 0x4ce4, 0x5cc5, 0x2c22, 0x3c03, 0x0c60, 0x1c41, 0xedae, 0xfd8f, 0xcdec, 0xddcd, 0xad2a, 0xbd0b, 0x8d68, 0x9d49, 0x7e97, 0x6eb6, 0x5ed5, 0x4ef4, 0x3e13, 0x2e32, 0x1e51, 0x0e70, 0xff9f, 0xefbe, 0xdfdd, 0xcffc, 0xbf1b, 0xaf3a, 0x9f59, 0x8f78, 0x9188, 0x81a9, 0xb1ca, 0xa1eb, 0xd10c, 0xc12d, 0xf14e, 0xe16f, 0x1080, 0x00a1, 0x30c2, 0x20e3, 0x5004, 0x4025, 0x7046, 0x6067, 0x83b9, 0x9398, 0xa3fb, 0xb3da, 0xc33d, 0xd31c, 0xe37f, 0xf35e, 0x02b1, 0x1290, 0x22f3, 0x32d2, 0x4235, 0x5214, 0x6277, 0x7256, 0xb5ea, 0xa5cb, 0x95a8, 0x8589, 0xf56e, 0xe54f, 0xd52c, 0xc50d, 0x34e2, 0x24c3, 0x14a0, 0x0481, 0x7466, 0x6447, 0x5424, 0x4405, 0xa7db, 0xb7fa, 0x8799, 0x97b8, 0xe75f, 0xf77e, 0xc71d, 0xd73c, 0x26d3, 0x36f2, 0x0691, 0x16b0, 0x6657, 0x7676, 0x4615, 0x5634, 0xd94c, 0xc96d, 0xf90e, 0xe92f, 0x99c8, 0x89e9, 0xb98a, 0xa9ab, 0x5844, 0x4865, 0x7806, 0x6827, 0x18c0, 0x08e1, 0x3882, 0x28a3, 0xcb7d, 0xdb5c, 0xeb3f, 0xfb1e, 0x8bf9, 0x9bd8, 0xabbb, 0xbb9a, 0x4a75, 0x5a54, 0x6a37, 0x7a16, 0x0af1, 0x1ad0, 0x2ab3, 0x3a92, 0xfd2e, 0xed0f, 0xdd6c, 0xcd4d, 0xbdaa, 0xad8b, 0x9de8, 0x8dc9, 0x7c26, 0x6c07, 0x5c64, 0x4c45, 0x3ca2, 0x2c83, 0x1ce0, 0x0cc1, 0xef1f, 0xff3e, 0xcf5d, 0xdf7c, 0xaf9b, 0xbfba, 0x8fd9, 0x9ff8, 0x6e17, 0x7e36, 0x4e55, 0x5e74, 0x2e93, 0x3eb2, 0x0ed1, 0x1ef0, }; uint16_t crc16(const void *c_ptr, size_t len) { const uint8_t *c = (uint8_t *)c_ptr; uint16_t crc = 0; while (len--) crc = crc_table[((crc >> 8) ^ *c++)] ^ (crc << 8); return crc; } // We declare an auto pointer to Vantage. std::unique_ptr vantage(new Vantage()); void ISGetProperties(const char *dev) { vantage->ISGetProperties(dev); } void ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) { vantage->ISNewSwitch(dev, name, states, names, n); } void ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n) { vantage->ISNewText(dev, name, texts, names, n); } void ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) { vantage->ISNewNumber(dev, name, values, names, n); } void ISNewBLOB(const char *dev, const char *name, int sizes[], int blobsizes[], char *blobs[], char *formats[], char *names[], int n) { INDI_UNUSED(dev); INDI_UNUSED(name); INDI_UNUSED(sizes); INDI_UNUSED(blobsizes); INDI_UNUSED(blobs); INDI_UNUSED(formats); INDI_UNUSED(names); INDI_UNUSED(n); } void ISSnoopDevice(XMLEle *root) { vantage->ISSnoopDevice(root); } Vantage::Vantage() { setVersion(1, 0); } const char *Vantage::getDefaultName() { return (const char *)"Vantage"; } bool Vantage::initProperties() { INDI::Weather::initProperties(); addParameter("WEATHER_FORECAST", "Forecast", 0, 0, 0, 1); addParameter("WEATHER_TEMPERATURE", "Temperature (C)", -10, 30, -20, 40); addParameter("WEATHER_BAROMETER", "Barometer (mbar)", 20, 32.5, 20, 32.5); addParameter("WEATHER_WIND_SPEED", "Wind (kph)", 0, 20, 0, 40); addParameter("WEAHTER_WIND_DIRECTION", "Wind Direction", 0, 360, 0, 360); addParameter("WEATHER_HUMIDITY", "Humidity %", 0, 100, 0, 100); addParameter("WEATHER_RAIN_RATE", "Rain (mm/h)", 0, 0, 0, 0); addParameter("WEATHER_SOLAR_RADIATION", "Solar Radiation (w/m^2)", 0, 10000, 0, 10000); setCriticalParameter("WEATHER_FORECAST"); setCriticalParameter("WEATHER_TEMPERATURE"); setCriticalParameter("WEATHER_WIND_SPEED"); setCriticalParameter("WEATHER_RAIN_RATE"); addDebugControl(); serialConnection->setDefaultBaudRate(Connection::Serial::B_19200); return true; } bool Vantage::Handshake() { return ack(); } IPState Vantage::updateWeather() { int nbytes_written = 0, nbytes_read = 0, rc = -1; char errstr[MAXRBUF]; char command[VANTAGE_CMD]; char response[VANTAGE_RES]; if (!wakeup()) return IPS_ALERT; strncpy(command, "LOOP 1", VANTAGE_CMD); command[6] = 0; tcflush(PortFD, TCIOFLUSH); LOGF_DEBUG("CMD (%s)", command); command[6] = 0xA; if ((rc = tty_write(PortFD, command, 7, &nbytes_written)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("Loop error: %s.", errstr); return IPS_ALERT; } if ((rc = tty_read(PortFD, response, 1, VANTAGE_TIMEOUT, &nbytes_read)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("Loop error: %s.", errstr); return IPS_ALERT; } if (response[0] != 0x06) { LOGF_ERROR("Expecting 0x06, received %#X", response[0]); return IPS_ALERT; } if ((rc = tty_read(PortFD, response, 99, VANTAGE_TIMEOUT, &nbytes_read)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("Loop error: %s.", errstr); return IPS_ALERT; } uint16_t crc = crc16(response, 99); if (crc != 0) { LOG_ERROR("CRC check failed."); return IPS_ALERT; } uint8_t *loopData = (uint8_t *)response; LOGF_DEBUG("Packet Type (%d)", loopData[4]); uint8_t forecastValue = loopData[89]; LOGF_DEBUG("Raw Forecast (%d)", forecastValue); switch (forecastValue) { // Clear case 0x08: LOG_INFO("Forecast: Mostly Clear."); setParameterValue("WEATHER_FORECAST", 0); break; case 0x06: LOG_INFO("Forecast: Partly Cloudy."); setParameterValue("WEATHER_FORECAST", 1); break; case 0x02: LOG_INFO("Forecast: Mostly Cloudy."); setParameterValue("WEATHER_FORECAST", 2); break; case 0x03: LOG_INFO("Forecast: Mostly Cloudy. Rain within 12 hours."); setParameterValue("WEATHER_FORECAST", 2); break; case 0x12: LOG_INFO("Forecast: Mostly Cloudy. Snow within 12 hours."); setParameterValue("WEATHER_FORECAST", 2); break; case 0x13: LOG_INFO("Forecast: Mostly Cloudy. Rain or Snow within 12 hours."); setParameterValue("WEATHER_FORECAST", 2); break; case 0x07: LOG_INFO("Forecast: Partly Cloudy. Rain within 12 hours."); setParameterValue("WEATHER_FORECAST", 1); break; case 0x16: LOG_INFO("Forecast: Partly Cloudy. Snow within 12 hours."); setParameterValue("WEATHER_FORECAST", 1); break; case 0x17: LOG_INFO("Forecast: Partly Cloudy. Rain or Snow within 12 hours."); setParameterValue("WEATHER_FORECAST", 1); break; } // Inside Temperature uint16_t temperatureValue = loopData[10] << 8 | loopData[9]; setParameterValue("WEATHER_TEMPERATURE", ((temperatureValue / 10.0) - 32) / 1.8); LOGF_DEBUG("Raw Temperature (%d) [%#4X %#4X]", temperatureValue, loopData[9], loopData[10]); // Barometer uint16_t barometerValue = loopData[8] << 8 | loopData[7]; setParameterValue("WEATHER_BAROMETER", (barometerValue / 1000.0) * 33.8639); LOGF_DEBUG("Raw Barometer (%d) [%#4X %#4X]", barometerValue, loopData[7], loopData[8]); // Wind Speed uint8_t windValue = loopData[14]; LOGF_DEBUG("Raw Wind Speed (%d) [%#X4]", windValue, loopData[14]); setParameterValue("WEATHER_WIND_SPEED", windValue / 0.62137); // Wind Direction uint16_t windDir = loopData[17] << 8 | loopData[16]; LOGF_DEBUG("Raw Wind Direction (%d) [%#4X,%#4X]", windDir, loopData[16], loopData[17]); setParameterValue("WEATHER_WIND_DIRECTION", windDir); // Rain Rate uint16_t rainRate = loopData[42] << 8 | loopData[41]; LOGF_DEBUG("Raw Rain Rate (%d) [%#4X,%#4X]", rainRate, loopData[41], loopData[42]); setParameterValue("WEATHER_RAIN_RATE", rainRate / (100 * 0.039370)); // Solar Radiation uint16_t solarRadiation = loopData[45] << 8 | loopData[44]; LOGF_DEBUG("Raw Solar Radiation (%d) [%#4X,%#4X]", solarRadiation, loopData[44], loopData[45]); if (solarRadiation == 32767) solarRadiation = 0; setParameterValue("WEATHER_SOLAR_RADIATION", solarRadiation); return IPS_OK; } bool Vantage::wakeup() { int nbytes_written = 0, nbytes_read = 0, rc = -1; char errstr[MAXRBUF]; char command[VANTAGE_CMD]; char response[VANTAGE_RES]; tcflush(PortFD, TCIOFLUSH); command[0] = 0xA; for (int i = 0; i < 3; i++) { LOGF_DEBUG("CMD (%#X)", command[0]); if ((rc = tty_write(PortFD, command, 1, &nbytes_written)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("Wakup error: %s.", errstr); return false; } if ((rc = tty_read_section(PortFD, response, 0xD, VANTAGE_TIMEOUT, &nbytes_read)) != TTY_OK) continue; else { if (nbytes_read == 2) { LOG_DEBUG("Console is awake."); return true; } } } return false; } bool Vantage::ack() { int nbytes_written = 0, nbytes_read = 0, rc = -1; char errstr[MAXRBUF]; char command[VANTAGE_CMD]; char response[VANTAGE_RES]; if (!wakeup()) return false; command[0] = 'V'; command[1] = 'E'; command[2] = 'R'; command[3] = 0; tcflush(PortFD, TCIOFLUSH); LOGF_DEBUG("CMD (%s)", command); command[3] = 0xA; command[4] = 0; if ((rc = tty_write(PortFD, command, 4, &nbytes_written)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("Ack error: %s.", errstr); return false; } if ((rc = tty_read_section(PortFD, response, 0xD, VANTAGE_TIMEOUT, &nbytes_read)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("Ack error: %s.", errstr); return false; } if (response[1] != 0xD) { LOGF_ERROR("Expecting 0xD, received %#X", response[1]); return false; } if ((rc = tty_read_section(PortFD, response, 0xD, VANTAGE_TIMEOUT, &nbytes_read)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("Ack error: %s.", errstr); return false; } response[nbytes_read - 2] = 0; if (strcmp(response, "OK") != 0) { LOGF_ERROR("Error response: %s", response); return false; } if ((rc = tty_read_section(PortFD, response, 0xD, VANTAGE_TIMEOUT, &nbytes_read)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("Ack error: %s.", errstr); return false; } response[nbytes_read - 2] = 0; LOGF_DEBUG("RES (%s)", response); return true; } libindi/drivers/weather/wunderground.h0000664000175000017500000000350013263645557017501 0ustar jasemjasem/******************************************************************************* Copyright(c) 2015 Jasem Mutlaq. All rights reserved. INDI Weather Underground (TM) Weather Driver This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program 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 General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. The full GNU General Public License is included in this distribution in the file called LICENSE. *******************************************************************************/ #pragma once #include "indiweather.h" class WunderGround : public INDI::Weather { public: WunderGround(); virtual ~WunderGround(); // Generic indi device entries bool Connect(); bool Disconnect(); const char *getDefaultName(); virtual bool initProperties(); virtual void ISGetProperties(const char *dev); virtual bool ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n); protected: virtual IPState updateWeather(); virtual bool saveConfigItems(FILE *fp); virtual bool updateLocation(double latitude, double longitude, double elevation); private: IText wunderAPIKeyT[1] {}; ITextVectorProperty wunderAPIKeyTP; double wunderLat, wunderLong; }; libindi/drivers/weather/gason.cpp0000664000175000017500000002767113263645557016437 0ustar jasemjasem/* The MIT License (MIT) Copyright (c) 2013-2015 Ivan Vashchaev Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ #include "gason.h" #include #define JSON_ZONE_SIZE 4096 #define JSON_STACK_SIZE 32 const char *jsonStrError(int err) { switch (err) { #define XX(no, str) \ case JSON_##no: \ return str; JSON_ERRNO_MAP(XX) #undef XX default: return "unknown"; } } void *JsonAllocator::allocate(size_t size) { size = (size + 7) & ~7; if (head && head->used + size <= JSON_ZONE_SIZE) { char *p = (char *)head + head->used; head->used += size; return p; } size_t allocSize = sizeof(Zone) + size; Zone *zone = (Zone *)malloc(allocSize <= JSON_ZONE_SIZE ? JSON_ZONE_SIZE : allocSize); if (zone == nullptr) return nullptr; zone->used = allocSize; if (allocSize <= JSON_ZONE_SIZE || head == nullptr) { zone->next = head; head = zone; } else { zone->next = head->next; head->next = zone; } return (char *)zone + sizeof(Zone); } void JsonAllocator::deallocate() { while (head) { Zone *next = head->next; free(head); head = next; } } static inline bool isspace(char c) { return c == ' ' || (c >= '\t' && c <= '\r'); } static inline bool isdelim(char c) { return c == ',' || c == ':' || c == ']' || c == '}' || isspace(c) || !c; } static inline bool isdigit(char c) { return c >= '0' && c <= '9'; } static inline bool isxdigit(char c) { return (c >= '0' && c <= '9') || ((c & ~' ') >= 'A' && (c & ~' ') <= 'F'); } static inline int char2int(char c) { if (c <= '9') return c - '0'; return (c & ~' ') - 'A' + 10; } static double string2double(char *s, char **endptr) { char ch = *s; if (ch == '-') ++s; double result = 0; while (isdigit(*s)) result = (result * 10) + (*s++ - '0'); if (*s == '.') { ++s; double fraction = 1; while (isdigit(*s)) { fraction *= 0.1; result += (*s++ - '0') * fraction; } } if (*s == 'e' || *s == 'E') { ++s; double base = 10; if (*s == '+') ++s; else if (*s == '-') { ++s; base = 0.1; } unsigned int exponent = 0; while (isdigit(*s)) exponent = (exponent * 10) + (*s++ - '0'); double power = 1; for (; exponent; exponent >>= 1, base *= base) if (exponent & 1) power *= base; result *= power; } *endptr = s; return ch == '-' ? -result : result; } static inline JsonNode *insertAfter(JsonNode *tail, JsonNode *node) { if (!tail) return node->next = node; node->next = tail->next; tail->next = node; return node; } static inline JsonValue listToValue(JsonTag tag, JsonNode *tail) { if (tail) { auto head = tail->next; tail->next = nullptr; return JsonValue(tag, head); } return JsonValue(tag, nullptr); } int jsonParse(char *s, char **endptr, JsonValue *value, JsonAllocator &allocator) { JsonNode *tails[JSON_STACK_SIZE]; JsonTag tags[JSON_STACK_SIZE]; char *keys[JSON_STACK_SIZE]; JsonValue o; int pos = -1; bool separator = true; JsonNode *node; *endptr = s; while (*s) { while (isspace(*s)) { ++s; if (!*s) break; } *endptr = s++; switch (**endptr) { #if __GNUC__ > 6 #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wimplicit-fallthrough" #endif case '-': if (!isdigit(*s) && *s != '.') { *endptr = s; return JSON_BAD_NUMBER; } /* Falls through. */ case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': o = JsonValue(string2double(*endptr, &s)); if (!isdelim(*s)) { *endptr = s; return JSON_BAD_NUMBER; } break; #if __GNUC__ > 6 #pragma GCC diagnostic pop #endif case '"': o = JsonValue(JSON_STRING, s); for (char *it = s; *s; ++it, ++s) { int c = *it = *s; if (c == '\\') { c = *++s; switch (c) { case '\\': case '"': case '/': *it = c; break; case 'b': *it = '\b'; break; case 'f': *it = '\f'; break; case 'n': *it = '\n'; break; case 'r': *it = '\r'; break; case 't': *it = '\t'; break; case 'u': c = 0; for (int i = 0; i < 4; ++i) { if (isxdigit(*++s)) { c = c * 16 + char2int(*s); } else { *endptr = s; return JSON_BAD_STRING; } } if (c < 0x80) { *it = c; } else if (c < 0x800) { *it++ = 0xC0 | (c >> 6); *it = 0x80 | (c & 0x3F); } else { *it++ = 0xE0 | (c >> 12); *it++ = 0x80 | ((c >> 6) & 0x3F); *it = 0x80 | (c & 0x3F); } break; default: *endptr = s; return JSON_BAD_STRING; } } else if ((unsigned int)c < ' ' || c == '\x7F') { *endptr = s; return JSON_BAD_STRING; } else if (c == '"') { *it = 0; ++s; break; } } if (!isdelim(*s)) { *endptr = s; return JSON_BAD_STRING; } break; case 't': if (!(s[0] == 'r' && s[1] == 'u' && s[2] == 'e' && isdelim(s[3]))) return JSON_BAD_IDENTIFIER; o = JsonValue(JSON_TRUE); s += 3; break; case 'f': if (!(s[0] == 'a' && s[1] == 'l' && s[2] == 's' && s[3] == 'e' && isdelim(s[4]))) return JSON_BAD_IDENTIFIER; o = JsonValue(JSON_FALSE); s += 4; break; case 'n': if (!(s[0] == 'u' && s[1] == 'l' && s[2] == 'l' && isdelim(s[3]))) return JSON_BAD_IDENTIFIER; o = JsonValue(JSON_NULL); s += 3; break; case ']': if (pos == -1) return JSON_STACK_UNDERFLOW; if (tags[pos] != JSON_ARRAY) return JSON_MISMATCH_BRACKET; o = listToValue(JSON_ARRAY, tails[pos--]); break; case '}': if (pos == -1) return JSON_STACK_UNDERFLOW; if (tags[pos] != JSON_OBJECT) return JSON_MISMATCH_BRACKET; if (keys[pos] != nullptr) return JSON_UNEXPECTED_CHARACTER; o = listToValue(JSON_OBJECT, tails[pos--]); break; case '[': if (++pos == JSON_STACK_SIZE) return JSON_STACK_OVERFLOW; tails[pos] = nullptr; tags[pos] = JSON_ARRAY; keys[pos] = nullptr; separator = true; continue; case '{': if (++pos == JSON_STACK_SIZE) return JSON_STACK_OVERFLOW; tails[pos] = nullptr; tags[pos] = JSON_OBJECT; keys[pos] = nullptr; separator = true; continue; case ':': if (separator || keys[pos] == nullptr) return JSON_UNEXPECTED_CHARACTER; separator = true; continue; case ',': if (separator || keys[pos] != nullptr) return JSON_UNEXPECTED_CHARACTER; separator = true; continue; case '\0': continue; default: return JSON_UNEXPECTED_CHARACTER; } separator = false; if (pos == -1) { *endptr = s; *value = o; return JSON_OK; } if (tags[pos] == JSON_OBJECT) { if (!keys[pos]) { if (o.getTag() != JSON_STRING) return JSON_UNQUOTED_KEY; keys[pos] = o.toString(); continue; } if ((node = (JsonNode *)allocator.allocate(sizeof(JsonNode))) == nullptr) return JSON_ALLOCATION_FAILURE; tails[pos] = insertAfter(tails[pos], node); tails[pos]->key = keys[pos]; keys[pos] = nullptr; } else { if ((node = (JsonNode *)allocator.allocate(sizeof(JsonNode) - sizeof(char *))) == nullptr) return JSON_ALLOCATION_FAILURE; tails[pos] = insertAfter(tails[pos], node); } tails[pos]->value = o; } return JSON_BREAKING_BAD; } libindi/drivers/weather/mbox.cpp0000664000175000017500000004110713263645557016263 0ustar jasemjasem/******************************************************************************* Copyright(c) 2017 Jasem Mutlaq. All rights reserved. INDI MBox Driver This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program 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 General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. The full GNU General Public License is included in this distribution in the file called LICENSE. *******************************************************************************/ #include "mbox.h" #include "indicom.h" #include "connectionplugins/connectionserial.h" #include #include #include #include #define MBOX_TIMEOUT 6 #define MBOX_BUF 64 // We declare an auto pointer to MBox. std::unique_ptr mbox(new MBox()); void ISGetProperties(const char *dev) { mbox->ISGetProperties(dev); } void ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) { mbox->ISNewSwitch(dev, name, states, names, n); } void ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n) { mbox->ISNewText(dev, name, texts, names, n); } void ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) { mbox->ISNewNumber(dev, name, values, names, n); } void ISNewBLOB(const char *dev, const char *name, int sizes[], int blobsizes[], char *blobs[], char *formats[], char *names[], int n) { INDI_UNUSED(dev); INDI_UNUSED(name); INDI_UNUSED(sizes); INDI_UNUSED(blobsizes); INDI_UNUSED(blobs); INDI_UNUSED(formats); INDI_UNUSED(names); INDI_UNUSED(n); } void ISSnoopDevice(XMLEle *root) { mbox->ISSnoopDevice(root); } MBox::MBox() { setVersion(1, 0); } const char *MBox::getDefaultName() { return (const char *)"MBox"; } bool MBox::initProperties() { INDI::Weather::initProperties(); addParameter("WEATHER_TEMPERATURE", "Temperature (C)", -10, 30, -20, 40); addParameter("WEATHER_BAROMETER", "Barometer (mbar)", 20, 32.5, 20, 32.5); addParameter("WEATHER_HUMIDITY", "Humidity %", 0, 100, 0, 100); addParameter("WEATHER_DEWPOINT", "Dew Point (C)", 0, 100, 0, 100); setCriticalParameter("WEATHER_TEMPERATURE"); // Reset Calibration IUFillSwitch(&ResetS[0], "RESET", "Reset", ISS_OFF); IUFillSwitchVector(&ResetSP, ResetS, 1, getDeviceName(), "CALIBRATION_RESET", "Reset", MAIN_CONTROL_TAB, IP_RW, ISR_1OFMANY, 0, IPS_IDLE); // Calibration Properties IUFillNumber(&CalibrationN[CAL_TEMPERATURE], "CAL_TEMPERATURE", "Temperature", "%.f", -50, 50, 1, 0); IUFillNumber(&CalibrationN[CAL_PRESSURE], "CAL_PRESSURE", "Pressure", "%.f", -100, 100, 10, 0); IUFillNumber(&CalibrationN[CAL_HUMIDITY], "CAL_HUMIDITY", "Humidity", "%.f", -50, 50, 1, 0); IUFillNumberVector(&CalibrationNP, CalibrationN, 3, getDeviceName(), "CALIBRATION", "Cabliration", MAIN_CONTROL_TAB, IP_RW, 0, IPS_IDLE); // Firmware Information IUFillText(&FirmwareT[0], "VERSION", "Version", "--"); IUFillTextVector(&FirmwareTP, FirmwareT, 1, getDeviceName(), "DEVICE_FIRMWARE", "Firmware", MAIN_CONTROL_TAB, IP_RO, 0, IPS_IDLE); serialConnection->setDefaultBaudRate(Connection::Serial::B_38400); addAuxControls(); return true; } bool MBox::updateProperties() { INDI::Weather::updateProperties(); if (isConnected()) { defineNumber(&CalibrationNP); defineSwitch(&ResetSP); defineText(&FirmwareTP); } else { deleteProperty(CalibrationNP.name); deleteProperty(ResetSP.name); deleteProperty(FirmwareTP.name); } return true; } bool MBox::Handshake() { tty_set_debug(1); AckResponse rc = ack(); if (rc == ACK_OK_STARTUP) { getCalibration(false); return true; } else if (rc == ACK_OK_INIT) { //getCalibration(true); CalibrationNP.s = IPS_BUSY; return true; } return false; } IPState MBox::updateWeather() { int nbytes_read = 0, rc = -1; char errstr[MAXRBUF]; char response[MBOX_BUF]; if (CalibrationNP.s == IPS_BUSY) { if (getCalibration(true)) { CalibrationNP.s = IPS_OK; IDSetNumber(&CalibrationNP, nullptr); } } if (isSimulation()) { strncpy(response, "$PXDR,P,96276.0,P,0,C,31.8,C,1,H,40.8,P,2,C,16.8,C,3,1.1*31\r\n", MBOX_BUF); nbytes_read = strlen(response); } else { if ((rc = tty_read_section(PortFD, response, 0xA, MBOX_TIMEOUT, &nbytes_read)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("%s error: %s.", __FUNCTION__, errstr); return IPS_ALERT; } tcflush(PortFD, TCIOFLUSH); } // Remove \r\n response[nbytes_read - 2] = '\0'; LOGF_DEBUG("RES <%s>", response); if (verifyCRC(response) == false) { LOG_ERROR("CRC check failed!"); return IPS_ALERT; } // Remove * and checksum char *end = strstr(response, "*"); *end = '\0'; // PXDR char *token = std::strtok(response, ","); // Sensor Type: Pressure token = std::strtok(NULL, ","); // P Sensor Value token = std::strtok(NULL, ","); if (token == nullptr) { LOG_ERROR("Invalid response."); return IPS_ALERT; } // Convert Pascal to mbar setParameterValue("WEATHER_BAROMETER", atof(token)/100.0); // Sensor Units (Pascal) token = std::strtok(NULL, ","); // Sensor ID token = std::strtok(NULL, ","); // Sensor Type: Temperature token = std::strtok(NULL, ","); // T Sensor value Temperature token = std::strtok(NULL, ","); if (token == nullptr) { LOG_ERROR("Invalid response."); return IPS_ALERT; } setParameterValue("WEATHER_TEMPERATURE", atof(token)); // Sensor Units (C) token = std::strtok(NULL, ","); // Sensor ID token = std::strtok(NULL, ","); // Sensor Type: Humidity token = std::strtok(NULL, ","); // Humidity token = std::strtok(NULL, ","); if (token == nullptr) { LOG_ERROR("Invalid response."); return IPS_ALERT; } setParameterValue("WEATHER_HUMIDITY", atof(token)); // Sensor Units (Percentage) token = std::strtok(NULL, ","); // Sensor ID token = std::strtok(NULL, ","); // Sensor Type: Dew Point token = std::strtok(NULL, ","); // Dew Point token = std::strtok(NULL, ","); if (token == nullptr) { LOG_ERROR("Invalid response."); return IPS_ALERT; } setParameterValue("WEATHER_DEWPOINT", atof(token)); // Sensor Units (C) token = std::strtok(NULL, ","); // Sensor ID token = std::strtok(NULL, ","); // Firmware token = std::strtok(NULL, ","); if (strcmp(token, FirmwareT[0].text)) { IUSaveText(&FirmwareT[0], token); FirmwareTP.s = IPS_OK; IDSetText(&FirmwareTP, nullptr); } return IPS_OK; } MBox::AckResponse MBox::ack() { int nbytes_read = 0, rc = -1; char errstr[MAXRBUF]; char response[MBOX_BUF]; if (isSimulation()) { strncpy(response, "MBox by Astromi.ch\r\n", 64); nbytes_read = strlen(response); } else { if ((rc = tty_read_section(PortFD, response, 0xA, MBOX_TIMEOUT, &nbytes_read)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("%s error: %s.", __FUNCTION__, errstr); return ACK_ERROR; } // Read again if we only recieved a newline character if (response[0] == '\n') { if ((rc = tty_read_section(PortFD, response, 0xA, MBOX_TIMEOUT, &nbytes_read)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("%s error: %s.", __FUNCTION__, errstr); return ACK_ERROR; } } } // Remove \r\n response[nbytes_read - 2] = '\0'; LOGF_DEBUG("RES <%s>", response); if (strstr(response, "MBox")) return ACK_OK_STARTUP; // Check if already initialized else if (strstr(response, "PXDR")) return ACK_OK_INIT; return ACK_ERROR; } bool MBox::ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) { if (dev != nullptr && strcmp(dev, getDeviceName()) == 0) { if (!strcmp(name, CalibrationNP.name)) { double prevPressure = CalibrationN[CAL_PRESSURE].value; double prevTemperature = CalibrationN[CAL_TEMPERATURE].value; double prevHumidaty = CalibrationN[CAL_HUMIDITY].value; IUUpdateNumber(&CalibrationNP, values, names, n); double targetPressure = CalibrationN[CAL_PRESSURE].value; double targetTemperature = CalibrationN[CAL_TEMPERATURE].value; double targetHumidity = CalibrationN[CAL_HUMIDITY].value; bool rc = true; if (targetPressure != prevPressure) { rc = setCalibration(CAL_PRESSURE); usleep(200000); } if (targetTemperature != prevTemperature) { rc = setCalibration(CAL_TEMPERATURE); usleep(200000); } if (targetHumidity != prevHumidaty) { rc = setCalibration(CAL_HUMIDITY); } CalibrationNP.s = rc ? IPS_OK : IPS_ALERT; IDSetNumber(&CalibrationNP, nullptr); return true; } } return INDI::Weather::ISNewNumber(dev, name, values, names, n); } bool MBox::ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) { if (dev != nullptr && strcmp(dev, getDeviceName()) == 0) { if (!strcmp(name, ResetSP.name)) { if (resetCalibration()) { ResetSP.s = IPS_OK; IDSetSwitch(&ResetSP, nullptr); LOG_INFO("Calibration values are reset."); CalibrationN[CAL_PRESSURE].value = 0; CalibrationN[CAL_TEMPERATURE].value = 0; CalibrationN[CAL_HUMIDITY].value = 0; CalibrationNP.s = IPS_IDLE; IDSetNumber(&CalibrationNP, nullptr); } else { ResetSP.s = IPS_ALERT; IDSetSwitch(&ResetSP, nullptr); } return true; } } return INDI::Weather::ISNewSwitch(dev, name, states, names, n); } bool MBox::getCalibration(bool sendCommand) { int nbytes_written = 0, nbytes_read = 0, rc = -1; char errstr[MAXRBUF]; const char *command = ":calget*"; char response[MBOX_BUF]; if (sendCommand) LOGF_DEBUG("CMD <%s>", command); if (isSimulation()) { strncpy(response, "$PCAL,P,20,T,50,H,-10*79\r\n", 64); nbytes_read = strlen(response); } else { if (sendCommand) { tcflush(PortFD, TCIOFLUSH); if ((rc = tty_write(PortFD, command, strlen(command), &nbytes_written)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("%s write error: %s.", __FUNCTION__, errstr); return false; } } if ((rc = tty_read_section(PortFD, response, 0xA, MBOX_TIMEOUT, &nbytes_read)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("%s read error: %s.", __FUNCTION__, errstr); return false; } // If token is invalid, read again if (strstr(response, "$PCAL") == nullptr) { if ((rc = tty_read_section(PortFD, response, 0xA, MBOX_TIMEOUT, &nbytes_read)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("%s read error: %s.", __FUNCTION__, errstr); return false; } } } // Remove \r\n response[nbytes_read - 2] = '\0'; LOGF_DEBUG("RES <%s>", response); if (verifyCRC(response) == false) { LOG_ERROR("CRC check failed!"); return false; } // Remove * and checksum char *end = strstr(response, "*"); *end = '\0'; // PCAL char *token = std::strtok(response, ","); // P token = std::strtok(NULL, ","); // Pressure token = std::strtok(NULL, ","); if (token == nullptr) { LOG_ERROR("Invalid response."); return false; } CalibrationN[CAL_PRESSURE].value = atof(token)/10.0; // T token = std::strtok(NULL, ","); // Temperature token = std::strtok(NULL, ","); if (token == nullptr) { LOG_ERROR("Invalid response."); return false; } CalibrationN[CAL_TEMPERATURE].value = atof(token)/10.0; // H token = std::strtok(NULL, ","); // Humidity token = std::strtok(NULL, ","); if (token == nullptr) { LOG_ERROR("Invalid response."); return false; } CalibrationN[CAL_HUMIDITY].value = atof(token)/10.0; return true; } bool MBox::setCalibration(CalibrationType type) { int nbytes_written = 0, rc = -1; char errstr[MAXRBUF]; char command[16] = {0}; if (type == CAL_PRESSURE) { // Pressure. snprintf(command, 16, ":calp,%d*", static_cast(CalibrationN[CAL_PRESSURE].value*10.0)); LOGF_DEBUG("CMD <%s>", command); if (isSimulation() == false) { tcflush(PortFD, TCIOFLUSH); if ((rc = tty_write(PortFD, command, strlen(command), &nbytes_written)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("%s error: %s.", __FUNCTION__, errstr); return false; } } } else if (type == CAL_TEMPERATURE) { // Temperature snprintf(command, 16, ":calt,%d*", static_cast(CalibrationN[CAL_TEMPERATURE].value*10.0)); LOGF_DEBUG("CMD <%s>", command); if (isSimulation() == false) { tcflush(PortFD, TCIOFLUSH); if ((rc = tty_write(PortFD, command, strlen(command), &nbytes_written)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("%s error: %s.", __FUNCTION__, errstr); return false; } } } else { // Humidity snprintf(command, 16, ":calh,%d*", static_cast(CalibrationN[CAL_HUMIDITY].value*10.0)); LOGF_DEBUG("CMD <%s>", command); if (isSimulation() == false) { tcflush(PortFD, TCIOFLUSH); if ((rc = tty_write(PortFD, command, strlen(command), &nbytes_written)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("%s error: %s.", __FUNCTION__, errstr); return false; } } } return getCalibration(false); } bool MBox::resetCalibration() { int nbytes_written = 0, rc = -1; char errstr[MAXRBUF]; const char *command = ":calreset*"; LOGF_DEBUG("CMD <%s>", command); if (isSimulation() == false) { tcflush(PortFD, TCIOFLUSH); if ((rc = tty_write(PortFD, command, strlen(command), &nbytes_written)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("%s error: %s.", __FUNCTION__, errstr); return false; } } return true; } bool MBox::verifyCRC(const char *response) { // Start with $ and ends with * followed by checksum value of the response uint8_t calculated_checksum = 0; // Skip starting $. Copy string char checksum_string[MBOX_BUF]; strncpy(checksum_string, response, MBOX_BUF); char *token = std::strtok(checksum_string, "*"); // Checksum string token = std::strtok(NULL, "*"); // Hex value uint8_t response_checksum_val = std::stoi(token, 0, 16); // Terminate it *token = '\0'; char *str = checksum_string+1; // Calculate checksum of message XOR while (*str) calculated_checksum ^= *str++; return (calculated_checksum == response_checksum_val); } libindi/drivers/weather/99-vantage.rules0000664000175000017500000000017013263645557017545 0ustar jasemjasemSUBSYSTEMS=="usb", ATTRS{idVendor}=="10c4", ATTRS{idProduct}=="ea60", GROUP="plugdev", MODE="0666", SYMLINK+="vantage" libindi/drivers/weather/gason.h0000664000175000017500000001065613263645557016077 0ustar jasemjasem/* The MIT License (MIT) Copyright (c) 2013-2015 Ivan Vashchaev Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ #pragma once #include #include #include enum JsonTag { JSON_NUMBER = 0, JSON_STRING, JSON_ARRAY, JSON_OBJECT, JSON_TRUE, JSON_FALSE, JSON_NULL = 0xF }; struct JsonNode; #define JSON_VALUE_PAYLOAD_MASK 0x00007FFFFFFFFFFFULL #define JSON_VALUE_NAN_MASK 0x7FF8000000000000ULL #define JSON_VALUE_TAG_MASK 0xF #define JSON_VALUE_TAG_SHIFT 47 union JsonValue { uint64_t ival; double fval; JsonValue(double x) : fval(x) {} JsonValue(JsonTag tag = JSON_NULL, void *payload = nullptr) { #if !defined(__arm__) assert((uint64_t)(uintptr_t)payload <= JSON_VALUE_PAYLOAD_MASK); #endif ival = JSON_VALUE_NAN_MASK | ((uint64_t)tag << JSON_VALUE_TAG_SHIFT) | (uintptr_t)payload; } bool isDouble() const { return (int64_t)ival <= (int64_t)JSON_VALUE_NAN_MASK; } JsonTag getTag() const { return isDouble() ? JSON_NUMBER : JsonTag((ival >> JSON_VALUE_TAG_SHIFT) & JSON_VALUE_TAG_MASK); } uint64_t getPayload() const { assert(!isDouble()); return ival & JSON_VALUE_PAYLOAD_MASK; } double toNumber() const { assert(getTag() == JSON_NUMBER); return fval; } char *toString() const { assert(getTag() == JSON_STRING); return (char *)getPayload(); } JsonNode *toNode() const { assert(getTag() == JSON_ARRAY || getTag() == JSON_OBJECT); return (JsonNode *)getPayload(); } }; struct JsonNode { JsonValue value; JsonNode *next; char *key; }; struct JsonIterator { JsonNode *p; void operator++() { p = p->next; } bool operator!=(const JsonIterator &x) const { return p != x.p; } JsonNode *operator*() const { return p; } JsonNode *operator->() const { return p; } }; inline JsonIterator begin(JsonValue o) { return JsonIterator{ o.toNode() }; } inline JsonIterator end(JsonValue) { return JsonIterator{ nullptr }; } #define JSON_ERRNO_MAP(XX) \ XX(OK, "ok") \ XX(BAD_NUMBER, "bad number") \ XX(BAD_STRING, "bad string") \ XX(BAD_IDENTIFIER, "bad identifier") \ XX(STACK_OVERFLOW, "stack overflow") \ XX(STACK_UNDERFLOW, "stack underflow") \ XX(MISMATCH_BRACKET, "mismatch bracket") \ XX(UNEXPECTED_CHARACTER, "unexpected character") \ XX(UNQUOTED_KEY, "unquoted key") \ XX(BREAKING_BAD, "breaking bad") \ XX(ALLOCATION_FAILURE, "allocation failure") enum JsonErrno { #define XX(no, str) JSON_##no, JSON_ERRNO_MAP(XX) #undef XX }; const char *jsonStrError(int err); class JsonAllocator { struct Zone { Zone *next; size_t used; } *head = nullptr; public: JsonAllocator() = default; JsonAllocator(const JsonAllocator &) = delete; JsonAllocator &operator=(const JsonAllocator &) = delete; JsonAllocator(JsonAllocator &&x) : head(x.head) { x.head = nullptr; } JsonAllocator &operator=(JsonAllocator &&x) { head = x.head; x.head = nullptr; return *this; } ~JsonAllocator() { deallocate(); } void *allocate(size_t size); void deallocate(); }; int jsonParse(char *str, char **endptr, JsonValue *value, JsonAllocator &allocator); libindi/drivers/rotator/0000775000175000017500000000000013263645557014642 5ustar jasemjasemlibindi/drivers/rotator/integra.h0000664000175000017500000000755313263645557016456 0ustar jasemjasem/* Gemini Telescope Design Integra85 Focusing Rotator. Copyright (C) 2017 Hans Lambermont 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 */ #pragma once #include "indifocuser.h" #include "indirotatorinterface.h" class Integra : public INDI::Focuser, public INDI::RotatorInterface { public: typedef enum { MOTOR_FOCUS, MOTOR_ROTATOR } MotorType; typedef enum { VERSION_25012017, VERSION_20122017 } FirmwareVersion; enum INTEGRA_HOMING_STATE { HOMING_IDLE, HOMING_START, HOMING_ABORT, HOMING_COUNT }; Integra(); virtual ~Integra() = default; virtual bool Handshake(); const char * getDefaultName(); virtual bool initProperties(); virtual bool updateProperties(); virtual bool ISNewNumber (const char * dev, const char * name, double values[], char * names[], int n); virtual bool ISNewSwitch (const char * dev, const char * name, ISState * states, char * names[], int n); protected: // Focuser virtual IPState MoveAbsFocuser(uint32_t targetTicks); virtual IPState MoveRelFocuser(FocusDirection dir, uint32_t ticks); virtual bool AbortFocuser(); // Rotator virtual IPState MoveRotator(double angle); virtual bool AbortRotator(); virtual bool SyncRotator(double angle); virtual bool ReverseRotator(bool enabled); // Misc. virtual bool saveConfigItems(FILE *fp); virtual void TimerHit(); private: bool genericIntegraCommand(const char *name, const char *cmd, const char *expectStart, char *returnValueString); bool integraGetCommand(const char *name, int command, char *returnValueString ); bool integraMotorGetCommand(const char *name, int command, MotorType motor, char *returnValueString); bool integraMotorSetCommand(const char *name, int command, MotorType motor, int value, char *returnValueString); bool getFirmware(); bool getFocuserType(); bool Ack(); bool gotoMotor(MotorType type, int32_t position); bool relativeGotoMotor(MotorType type, int32_t relativePosition); bool getPosition(MotorType type); bool stopMotor(MotorType type); bool isMotorMoving(MotorType type); bool getTemperature(); bool findHome(); bool abortHome(); bool isHomingComplete(); void cleanPrint(const char *cmd, char *cleancmd); bool saveToEEPROM(); bool getMaxPosition(MotorType type); uint32_t rotatorDegreesToTicks(double angle); double rotatorTicksToDegrees(uint32_t ticks); INumber MaxPositionN[2]; INumberVectorProperty MaxPositionNP; INumber SensorN[2]; INumberVectorProperty SensorNP; enum { SENSOR_TEMPERATURE }; ISwitch FindHomeS[HOMING_COUNT]; ISwitchVectorProperty FindHomeSP; INumber RotatorAbsPosN[1]; INumberVectorProperty RotatorAbsPosNP; double lastTemperature { 0 }; int timeToReadTemperature = 0; double rotatorTicksPerDegree { 0 }; double rotatorDegreesPerTick = 0.0; uint32_t lastFocuserPosition { 0 }; bool haveReadFocusPositionAtLeastOnce = false; uint32_t lastRotatorPosition { 0 }; bool haveReadRotatorPositionAtLeastOnce = false; uint32_t targetPosition { 0 }; FirmwareVersion firmwareVersion { VERSION_25012017 }; }; libindi/drivers/rotator/integra.cpp0000664000175000017500000007002313263645557017001 0ustar jasemjasem/* Gemini Telescope Design Integra85 Focusing Rotator. Copyright (C) 2017 Hans Lambermont 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 */ #include "integra.h" #include "indicom.h" #include "connectionplugins/connectionserial.h" #include #include #include #include #define INTEGRA_TIMEOUT_IN_S 5 #define INTEGRA_TEMPERATURE_LOOP_SKIPS 60 #define INTEGRA_TEMPERATURE_TRESHOLD_IN_C 0.1 #define INTEGRA_ROUNDING_FUDGE 0.001 #define ROTATOR_TAB "Rotator" #define SETTINGS_TAB "Settings" std::unique_ptr integra(new Integra()); typedef struct { const char cmd[12]; char ret[2][3]; } COMMANDDESC; static const COMMANDDESC IntegraProtocol[] = { { "@SW%d,0\r\n", { "S", "SW"}}, { "@CS%d,0\r\n", { "C", "CS"}}, { "@CE%d,0\r\n", { "CE", "CE"}}, { "@CR%d,0\r\n", { "CR", "CR"}}, { "@TR\r\n", { "T", "TR"}}, { "@PW%d,0\r\n", { "P", "PW"}}, { "@PR%d,0\r\n", { "P", "PR"}}, { "@MI%d,%d\r\n", { "M", "MI"}}, { "@MO%d,%d\r\n", { "M", "MO"}}, { "@RR%d,0\r\n", { "R", "RR"}}, { "X\r\n", { "", "X"}}, { "@IW%d,0\r\n", { "I", "IW"}}, { "@ZW\r\n", { "", "ZW"}} }; enum { stop_motor, calibrate, calibrate_interrupt, calibration_state, get_temperature, set_motstep, get_motstep, move_mot_in, move_mot_out, get_motrange, is_moving, invert_dir, EEPROMwrite }; void ISGetProperties(const char *dev) { integra->ISGetProperties(dev); } void ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) { integra->ISNewSwitch(dev, name, states, names, n); } void ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n) { integra->ISNewText(dev, name, texts, names, n); } void ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) { integra->ISNewNumber(dev, name, values, names, n); } void ISNewBLOB (const char *dev, const char *name, int sizes[], int blobsizes[], char *blobs[], char *formats[], char *names[], int n) { INDI_UNUSED(dev); INDI_UNUSED(name); INDI_UNUSED(sizes); INDI_UNUSED(blobsizes); INDI_UNUSED(blobs); INDI_UNUSED(formats); INDI_UNUSED(names); INDI_UNUSED(n); } void ISSnoopDevice (XMLEle *root) { integra->ISSnoopDevice(root); } Integra::Integra() : RotatorInterface(this) { // Rotator RI::SetCapability(ROTATOR_CAN_ABORT | ROTATOR_CAN_SYNC | ROTATOR_CAN_REVERSE); // Focuser FI::SetCapability(FOCUSER_CAN_ABS_MOVE | FOCUSER_CAN_REL_MOVE | FOCUSER_CAN_ABORT); setConnection(CONNECTION_SERIAL); } bool Integra::initProperties() { INDI::Focuser::initProperties(); IUFillNumber(&MaxPositionN[0], "Steps", "Focuser", "%.f", 0, 188600., 0., 188600.); IUFillNumber(&MaxPositionN[1], "Steps", "Rotator", "%.f", 0, 61802.0, 0., 61802.); IUFillNumberVector(&MaxPositionNP, MaxPositionN, 2, getDeviceName(), "MAX_POSITION", "Max position", SETTINGS_TAB, IP_RO, 0, IPS_IDLE); FocusSpeedN[0].min = 1; FocusSpeedN[0].max = 1; FocusSpeedN[0].value = 1; // Temperature Sensor IUFillNumber(&SensorN[SENSOR_TEMPERATURE], "TEMPERATURE", "Temperature (C)", "%.2f", -100, 100., 1., 0.); IUFillNumberVector(&SensorNP, SensorN, 1, getDeviceName(), "SENSORS", "Sensors", MAIN_CONTROL_TAB, IP_RO, 0, IPS_IDLE ); // Home Find IUFillSwitch(&FindHomeS[HOMING_IDLE], "HOMING_IDLE", "Idle", ISS_ON); IUFillSwitch(&FindHomeS[HOMING_START], "HOMING_START", "Start", ISS_OFF); IUFillSwitch(&FindHomeS[HOMING_ABORT], "HOMING_ABORT", "Abort", ISS_OFF); IUFillSwitchVector(&FindHomeSP, FindHomeS, HOMING_COUNT, getDeviceName(), "HOMING", "Home at Center", SETTINGS_TAB, IP_RW, ISR_1OFMANY, 60, IPS_IDLE); // Relative and absolute movement FocusAbsPosN[0].min = 0; FocusAbsPosN[0].max = MaxPositionN[0].value; FocusAbsPosN[0].step = MaxPositionN[0].value / 50.0; FocusAbsPosN[0].value = 0; FocusRelPosN[0].max = (FocusAbsPosN[0].max - FocusAbsPosN[0].min) / 2; FocusRelPosN[0].min = 0; FocusRelPosN[0].step = FocusRelPosN[0].max / 100.0; FocusRelPosN[0].value = 100; RI::initProperties(ROTATOR_TAB); // Rotator Ticks IUFillNumber(&RotatorAbsPosN[0], "ROTATOR_ABSOLUTE_POSITION", "Ticks", "%.f", 0., 61802., 1., 0.); IUFillNumberVector(&RotatorAbsPosNP, RotatorAbsPosN, 1, getDeviceName(), "ABS_ROTATOR_POSITION", "Goto", ROTATOR_TAB, IP_RW, 0, IPS_IDLE ); addDebugControl(); // The device uses an Arduino which shows up as /dev/ttyACM0 on Linux // An udev rule example is provided that can create a more logical name like /dev/integra_focusing_rotator0 serialConnection->setDefaultPort("/dev/ttyACM0"); // Set mandatory baud speed. The device does not work with anything else. serialConnection->setDefaultBaudRate(Connection::Serial::B_115200); return true; } bool Integra::updateProperties() { INDI::Focuser::updateProperties(); if (isConnected()) { defineNumber(&MaxPositionNP); // Focus defineNumber(&SensorNP); defineSwitch(&FindHomeSP); // Rotator RI::updateProperties(); defineNumber(&RotatorAbsPosNP); } else { deleteProperty(MaxPositionNP.name); // Focus deleteProperty(SensorNP.name); deleteProperty(FindHomeSP.name); // Rotator RI::updateProperties(); deleteProperty(RotatorAbsPosNP.name); } return true; } bool Integra::Handshake() { if (Ack()) return true; LOG_INFO("Error retrieving data from Integra, please ensure Integra controller is powered and the port is correct."); return false; } const char * Integra::getDefaultName() { return "Integra85"; } void Integra::cleanPrint(const char *cmd, char *cleancmd) { size_t len = strlen(cmd); int j = 0; for (size_t i = 0; i<=len; i++) { if (cmd[i] == 0xA) { cleancmd[j++] = '\\'; cleancmd[j++] = 'n'; } else if (cmd[i] == 0xD) { cleancmd[j++] = '\\'; cleancmd[j++] = 'r'; } else { cleancmd[j++] = cmd[i]; } } } bool Integra::Ack() { bool rcFirmware = getFirmware(); bool rcType = getFocuserType(); bool rcMaxPositionMotorFocus = getMaxPosition(MOTOR_FOCUS); bool rcMaxPositionMotorRotator = getMaxPosition(MOTOR_ROTATOR); return (rcFirmware && rcType && rcMaxPositionMotorFocus && rcMaxPositionMotorRotator); } bool Integra::getFirmware() { char resp[64] = "not available"; // two firmware versions : 25.01.2017 & 20.12.2017 // still no direct command to retrieve the version but the protocol // has changed. the later version is returning the full command prefix as response prefix // version 25.01.2017 : cmd RR1,0 => R188600# // version 20.12.2017 : cmd RR1.0 => RR188600# // to identify the version we try both protocol. if ( genericIntegraCommand(__FUNCTION__, "@RR1,0\r\n", "RR", nullptr)) { strcpy(resp, "20.12.2017"); this->firmwareVersion = VERSION_20122017; } else if ( genericIntegraCommand(__FUNCTION__, "@RR1,0\r\n", "R", nullptr)) { strcpy(resp, "25.01.2017"); this->firmwareVersion = VERSION_25012017; } else { return false; // cannot retrieve firmware session. } LOGF_INFO("Firmware version %s", resp); return true; } bool Integra::getFocuserType() { char resp[64] = "Integra85"; // TODO this is an assumption until the device can report its type LOGF_INFO("Focuser Type %s", resp); if (strcmp(resp, "Integra85") == 0) { RotatorAbsPosN[0].min = 0; RotatorAbsPosN[0].max = 61802; } rotatorTicksPerDegree = RotatorAbsPosN[0].max / 360.0; rotatorDegreesPerTick = 360.0 / RotatorAbsPosN[0].max; return true; } bool Integra::relativeGotoMotor(MotorType type, int32_t relativePosition) { int motorMoveCommand; LOGF_DEBUG("Start relativeGotoMotor to %d ...", relativePosition); if (relativePosition > 0) motorMoveCommand = move_mot_out; else motorMoveCommand = move_mot_in; if (type == MOTOR_FOCUS) { if (relativePosition > 0) { if (lastFocuserPosition + relativePosition > MaxPositionN[MOTOR_FOCUS].value) { int newRelativePosition = (int32_t)floor(MaxPositionN[MOTOR_FOCUS].value) - lastFocuserPosition; LOGF_INFO("Focus position change %d clipped to %d to stay at MAX %d", relativePosition, newRelativePosition, MaxPositionN[MOTOR_FOCUS].value); relativePosition = newRelativePosition; } } else { if ((int32_t )lastFocuserPosition + relativePosition < 0) { int newRelativePosition = -lastFocuserPosition; LOGF_INFO("Focus position change %d clipped to %d to stay at MIN 0", relativePosition, newRelativePosition); relativePosition = newRelativePosition; } } } else if (type == MOTOR_ROTATOR) { if (relativePosition > 0) { if (lastRotatorPosition + relativePosition > MaxPositionN[MOTOR_ROTATOR].value) { int newRelativePosition = (int32_t)floor(MaxPositionN[MOTOR_ROTATOR].value) - lastRotatorPosition; LOGF_INFO("Rotator position change %d clipped to %d to stay at MAX %d", relativePosition, newRelativePosition, MaxPositionN[MOTOR_ROTATOR].value); relativePosition = newRelativePosition; } } else { if (lastRotatorPosition + relativePosition < - MaxPositionN[MOTOR_ROTATOR].value) { int newRelativePosition = - (int32_t)floor(MaxPositionN[MOTOR_ROTATOR].value) - lastRotatorPosition; LOGF_INFO("Rotator position change %d clipped to %d to stay at MIN %d", relativePosition, newRelativePosition, - MaxPositionN[MOTOR_ROTATOR].value); relativePosition = newRelativePosition; } } } return integraMotorSetCommand( __FUNCTION__, motorMoveCommand, type, abs(relativePosition), nullptr); } bool Integra::gotoMotor(MotorType type, int32_t position) { LOGF_DEBUG("Start gotoMotor to %d", position); if (type == MOTOR_FOCUS) { return relativeGotoMotor(type, position - lastFocuserPosition); } else if (type == MOTOR_ROTATOR) { return relativeGotoMotor(type, position - lastRotatorPosition); } else { LOGF_ERROR("%s error: MotorType %d is unknown.", __FUNCTION__, type); } return false; } bool Integra::getPosition(MotorType type) { char res[16] = {0}; if ( !integraMotorGetCommand(__FUNCTION__, get_motstep, type, res )) { return false; } int position = -1e6; position = atoi(res); if (position != -1e6) { if (type == MOTOR_FOCUS) { if (FocusAbsPosN[0].value != position) { auto position_from = (int) FocusAbsPosN[0].value; int position_to = position; if (haveReadFocusPositionAtLeastOnce) { LOGF_DEBUG("Focus position changed from %d to %d", position_from, position_to); } else { LOGF_DEBUG("Focus position is %d", position_to); } FocusAbsPosN[0].value = position; } } else if (type == MOTOR_ROTATOR) { if (RotatorAbsPosN[0].value != position) { auto position_from = (int) RotatorAbsPosN[0].value; int position_to = position; if (haveReadRotatorPositionAtLeastOnce) { LOGF_DEBUG("Rotator changed angle from %.2f to %.2f, position from %d to %d", rotatorTicksToDegrees(position_from), rotatorTicksToDegrees(position_to), position_from, position_to); } else { LOGF_DEBUG("Rotator angle is %.2f, position is %d", rotatorTicksToDegrees(position_to), position_to); } RotatorAbsPosN[0].value = position; } } else { LOGF_ERROR("%s error: motor type %d is unknown", __FUNCTION__, type); } return true; } LOGF_DEBUG("Invalid Position! %d", position); return false; } bool Integra::ISNewSwitch (const char * dev, const char * name, ISState * states, char * names[], int n) { if(strcmp(dev, getDeviceName()) == 0) { if (strcmp(name, FindHomeSP.name) == 0) { IUUpdateSwitch(&FindHomeSP, states, names, n); int index = IUFindOnSwitchIndex(&FindHomeSP); switch (index) { case HOMING_IDLE: LOG_INFO("Homing state is IDLE"); FindHomeS[HOMING_IDLE].s = ISS_ON; FindHomeSP.s = IPS_OK; break; case HOMING_START: if (findHome()) { FindHomeSP.s = IPS_BUSY; FindHomeS[HOMING_START].s = ISS_ON; DEBUG(INDI::Logger::DBG_WARNING, "Homing process can take up to 2 minutes. You cannot control the unit until the process is fully complete."); } else { FindHomeSP.s = IPS_ALERT; FindHomeS[HOMING_START].s = ISS_OFF; LOG_ERROR("Failed to start homing process."); } break; case HOMING_ABORT: if (abortHome()) { FindHomeSP.s = IPS_IDLE; FindHomeS[HOMING_ABORT].s = ISS_ON; LOG_WARN("Homing aborted"); } else { FindHomeSP.s = IPS_ALERT; FindHomeS[HOMING_ABORT].s = ISS_OFF; LOG_ERROR("Failed to abort homing process."); } break; default: FindHomeSP.s = IPS_ALERT; IDSetSwitch(&FindHomeSP, "Unknown homing index %d", index); return false; } IDSetSwitch(&FindHomeSP, nullptr); return true; } else if (strstr(name, "ROTATOR")) { if (INDI::RotatorInterface::processSwitch(dev, name, states, names, n)) return true; } } return INDI::Focuser::ISNewSwitch(dev, name, states, names, n); } bool Integra::ISNewNumber (const char * dev, const char * name, double values[], char * names[], int n) { if (dev != nullptr && strcmp(dev, getDeviceName()) == 0) { if (strcmp(name, RotatorAbsPosNP.name) == 0) { RotatorAbsPosNP.s = (gotoMotor(MOTOR_ROTATOR, static_cast(values[0])) ? IPS_BUSY : IPS_ALERT); IDSetNumber(&RotatorAbsPosNP, nullptr); if (RotatorAbsPosNP.s == IPS_BUSY) LOGF_DEBUG("Rotator moving from %d to %.f ticks...", lastRotatorPosition, values[0]); return true; } else if (strstr(name, "ROTATOR")) { if (INDI::RotatorInterface::processNumber(dev, name, values, names, n)) return true; } } return INDI::Focuser::ISNewNumber(dev, name, values, names, n); } IPState Integra::MoveAbsFocuser(uint32_t targetTicks) { targetPosition = targetTicks; LOGF_DEBUG("Focuser will move absolute from %d to %d ...", lastFocuserPosition, targetTicks); bool rc = false; rc = gotoMotor(MOTOR_FOCUS, targetPosition); if (!rc) return IPS_ALERT; FocusAbsPosNP.s = IPS_BUSY; return IPS_BUSY; } IPState Integra::MoveRelFocuser(FocusDirection dir, uint32_t ticks) { double newPosition = 0; bool rc = false; LOGF_DEBUG("Focuser will move in direction %d relative %d ticks...", dir, ticks); if (dir == FOCUS_INWARD) newPosition = FocusAbsPosN[0].value - ticks; else newPosition = FocusAbsPosN[0].value + ticks; rc = gotoMotor(MOTOR_FOCUS, newPosition); if (!rc) return IPS_ALERT; FocusRelPosN[0].value = ticks; FocusRelPosNP.s = IPS_BUSY; return IPS_BUSY; } void Integra::TimerHit() { if (!isConnected()) { SetTimer(POLLMS); return; } bool rc = false; bool savePositionsToEEPROM = false; // #1 If we're homing, we check if homing is complete as we cannot check for anything else if (FindHomeSP.s == IPS_BUSY) { if (isHomingComplete()) { FindHomeS[0].s = ISS_OFF; FindHomeSP.s = IPS_OK; IDSetSwitch(&FindHomeSP, nullptr); LOG_INFO("Homing is complete"); // Next read positions and save to EEPROM : haveReadFocusPositionAtLeastOnce = false; haveReadRotatorPositionAtLeastOnce = false; } else { LOG_DEBUG("Homing"); } SetTimer(POLLMS); return; } // #2 Get Temperature, only read this when no motors are active, and about once per minute if (FocusAbsPosNP.s != IPS_BUSY && FocusRelPosNP.s != IPS_BUSY && RotatorAbsPosNP.s != IPS_BUSY && timeToReadTemperature <= 0) { rc = getTemperature(); if ( ! rc) rc = getTemperature(); if (rc) { timeToReadTemperature = INTEGRA_TEMPERATURE_LOOP_SKIPS; if (fabs(SensorN[SENSOR_TEMPERATURE].value - lastTemperature) > INTEGRA_TEMPERATURE_TRESHOLD_IN_C) { lastTemperature = SensorN[SENSOR_TEMPERATURE].value; IDSetNumber(&SensorNP, nullptr); } } } else { timeToReadTemperature--; } // #5 Focus Position & Status if (!haveReadFocusPositionAtLeastOnce || FocusAbsPosNP.s == IPS_BUSY || FocusRelPosNP.s == IPS_BUSY) { if ( ! isMotorMoving(MOTOR_FOCUS)) { LOG_DEBUG("Focuser stopped"); FocusAbsPosNP.s = IPS_OK; FocusRelPosNP.s = IPS_OK; rc = getPosition(MOTOR_FOCUS); if (rc) { if (FocusAbsPosN[0].value != lastFocuserPosition) { lastFocuserPosition = FocusAbsPosN[0].value; IDSetNumber(&FocusAbsPosNP, nullptr); IDSetNumber(&FocusRelPosNP, nullptr); if (haveReadFocusPositionAtLeastOnce) { LOGF_INFO("Focuser reached requested position %d", lastFocuserPosition); } else { LOGF_INFO("Focuser position is %d", lastFocuserPosition); haveReadFocusPositionAtLeastOnce = true; } savePositionsToEEPROM = true; } } } else { LOG_DEBUG("Focusing"); } } // #6 Rotator Position & Status if (!haveReadRotatorPositionAtLeastOnce || RotatorAbsPosNP.s == IPS_BUSY) { if ( ! isMotorMoving(MOTOR_ROTATOR)) { LOG_DEBUG("Rotator stopped"); RotatorAbsPosNP.s = IPS_OK; GotoRotatorNP.s = IPS_OK; rc = getPosition(MOTOR_ROTATOR); if (rc) { if (RotatorAbsPosN[0].value != lastRotatorPosition) { lastRotatorPosition = RotatorAbsPosN[0].value; GotoRotatorN[0].value = rotatorTicksToDegrees(lastRotatorPosition); //range360(RotatorAbsPosN[0].value / rotatorTicksPerDegree); IDSetNumber(&RotatorAbsPosNP, nullptr); IDSetNumber(&GotoRotatorNP, nullptr); if (haveReadRotatorPositionAtLeastOnce) LOGF_INFO("Rotator reached requested angle %.2f, position %d", rotatorTicksToDegrees(lastRotatorPosition), lastRotatorPosition); else { LOGF_INFO("Rotator is at angle %.2f, position %d", rotatorTicksToDegrees(lastRotatorPosition), lastRotatorPosition); haveReadRotatorPositionAtLeastOnce = true; } savePositionsToEEPROM = true; } } } else { LOG_DEBUG("Rotating"); } } if (savePositionsToEEPROM) { saveToEEPROM(); } SetTimer(POLLMS); } bool Integra::AbortFocuser() { return stopMotor(MOTOR_FOCUS); } bool Integra::stopMotor(MotorType type) { // TODO (if focuser?) handle CR 2 if (integraMotorGetCommand(__FUNCTION__, stop_motor,type, nullptr)) { if (type == MOTOR_FOCUS) { haveReadFocusPositionAtLeastOnce = false; } else { haveReadRotatorPositionAtLeastOnce = false; } return true; } return false; } bool Integra::isMotorMoving(MotorType type) { char res[16] = {0}; if ( ! integraGetCommand( __FUNCTION__, is_moving, res)) { return false; } if (type == MOTOR_FOCUS) { if (res[0] == '1') { LOG_DEBUG("Focus motor is running"); return true; } else { LOG_DEBUG("Focus motor is not running"); return false; } } else { // bug, both motors return 1 at res[0] when running // return (res[0] == '2'); if (res[0] == '1') { LOG_DEBUG("Rotator motor is running"); return true; } else { LOG_DEBUG("Rotator motor is not running"); return false; } } } bool Integra::getMaxPosition(MotorType type) { char res[16] = {0}; if ( ! integraMotorGetCommand(__FUNCTION__, get_motrange, type, res)) { return false; } int position = atoi(res); MaxPositionN[type].value = position; LOGF_INFO("Motor %d max position is %d", type, position); return position > 0; // cannot consider a max position == 0 as a valid max. } bool Integra::saveToEEPROM() { return integraGetCommand(__FUNCTION__, EEPROMwrite, nullptr); } bool Integra::getTemperature() { char res[16] = {0}; if (integraGetCommand(__FUNCTION__, get_temperature, res ) ) { SensorN[SENSOR_TEMPERATURE].value = strtod(res, nullptr); return true; } return false; } bool Integra::findHome() { return integraMotorGetCommand(__FUNCTION__, calibrate, MOTOR_FOCUS, nullptr); } bool Integra::abortHome() { return integraMotorGetCommand(__FUNCTION__, calibrate_interrupt, MOTOR_FOCUS, nullptr); } bool Integra::isHomingComplete() { char res[16] = {0}; if (integraMotorGetCommand(__FUNCTION__, calibration_state, MOTOR_FOCUS, res)) { return (res[0] == '1'); } return false; } bool Integra::saveConfigItems(FILE *fp) { Focuser::saveConfigItems(fp); return true; } // Integra position 0..61802 ticks , angle 0..360 deg, position 0 corresponds to 180 deg // We need to map the Integra frame to that of the IndiRotatorInterface. // INDI rotatorInterface: Only absolute position Rotators are supported. // Angle is ranged from 0 to 360 increasing clockwise when looking at the back of the camera. IPState Integra::MoveRotator(double angle) { uint32_t p1 = lastRotatorPosition; uint32_t p2 = rotatorDegreesToTicks(angle); LOGF_INFO("MoveRotator from %.2f to %.2f degrees, from position %d to %d ...", rotatorTicksToDegrees(lastRotatorPosition), angle, p1, p2); bool rc = relativeGotoMotor(MOTOR_ROTATOR, p2 - p1); if (rc) { RotatorAbsPosNP.s = IPS_BUSY; IDSetNumber(&RotatorAbsPosNP, nullptr); return IPS_BUSY; } return IPS_ALERT; } bool Integra::AbortRotator() { bool rc = stopMotor(MOTOR_ROTATOR); if (rc && RotatorAbsPosNP.s != IPS_OK) { RotatorAbsPosNP.s = IPS_OK; IDSetNumber(&RotatorAbsPosNP, nullptr); } return rc; } uint32_t Integra::rotatorDegreesToTicks(double angle) { uint32_t position = 61802 / 2; if (angle >= 0.0 && angle <= 180.0) { position = (uint32_t) lround(61802.0 - (180.0 - angle) / rotatorDegreesPerTick); } else if (angle > 180 && angle <= 360) { position = (uint32_t) lround(61802.0 - (540.0 - angle) / rotatorDegreesPerTick); } else { LOGF_ERROR("%s error: %.2f is out of range", __FUNCTION__, angle); } return position; } double Integra::rotatorTicksToDegrees(uint32_t ticks) { double degrees = range360(180.0 + ticks * rotatorDegreesPerTick + INTEGRA_ROUNDING_FUDGE); return degrees; } bool Integra::SyncRotator(double angle) { uint32_t position = rotatorDegreesToTicks(angle); if ( integraMotorSetCommand(__FUNCTION__, set_motstep, MOTOR_ROTATOR, position, nullptr )) { haveReadRotatorPositionAtLeastOnce = false; return true; } return false; } bool Integra::ReverseRotator(bool) { return integraMotorGetCommand(__FUNCTION__, invert_dir, MOTOR_ROTATOR, nullptr); } bool Integra::integraGetCommand( const char *name,int commandIdx, char *returnValueString ) { char cmd[16] = {0}; snprintf(cmd, 16, "%s", IntegraProtocol[commandIdx].cmd); return genericIntegraCommand(name, cmd , IntegraProtocol[commandIdx].ret[this->firmwareVersion], returnValueString); } bool Integra::integraMotorGetCommand( const char *name,int commandIdx, MotorType motor, char *returnValueString ) { char cmd[16] = {0}; snprintf(cmd, 16, IntegraProtocol[commandIdx].cmd, motor+1); return genericIntegraCommand(name, cmd , IntegraProtocol[commandIdx].ret[this->firmwareVersion], returnValueString); } bool Integra::integraMotorSetCommand(const char *name, int commandIdx, MotorType motor, int value, char *returnValueString ) { char cmd[16] = {0}; snprintf(cmd, 16, IntegraProtocol[commandIdx].cmd, motor+1, value); return genericIntegraCommand(name, cmd , IntegraProtocol[commandIdx].ret[this->firmwareVersion], returnValueString); } bool Integra::genericIntegraCommand(const char *name, const char *cmd, const char *expectStart, char *returnValueString) { char cmdnocrlf[16] = {0}; int nbytes_written = 0, nbytes_read = 0, rc = -1; char res[16] = {0}; char *correctRes = nullptr; char errstr[MAXRBUF]; cleanPrint(cmd, cmdnocrlf); LOGF_INFO("CMD %s (%s)", name, cmdnocrlf); tcflush(PortFD, TCIOFLUSH); if ( (rc = tty_write(PortFD, cmd, (int)strlen(cmd), &nbytes_written)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("%s: %s.", name, errstr); return false; } if ( (rc = tty_read_section(PortFD, res, '#', INTEGRA_TIMEOUT_IN_S, &nbytes_read)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("%s error: %s.", name, errstr); return false; } LOGF_INFO("RES %s (%s)", name, res); // check begin of result string if (expectStart != nullptr) { correctRes = strstr(res, expectStart); // the hw sometimes returns /r or /n at the beginning ot the response if (correctRes == nullptr) { LOGF_ERROR("%s error: invalid response (%s)", name, res); return false; } } // check end of result string if (res[nbytes_read-1] != '#') { LOGF_ERROR("%s error: invalid response 2 (%s)", name, res); return false; } res[nbytes_read-1] = '\0'; // wipe the # if (returnValueString != nullptr && expectStart != nullptr) { size_t expectStrlen = strlen(expectStart); strcpy(returnValueString, correctRes + expectStrlen); } return true; } libindi/drivers/rotator/gemini.cpp0000664000175000017500000032033413263645557016623 0ustar jasemjasem/* Optec Gemini Focuser Rotator INDI driver Copyright (C) 2017 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 */ #include "gemini.h" #include "indicom.h" #include "connectionplugins/connectionserial.h" #include #include #include #include #include #define GEMINI_MAX_RETRIES 1 #define GEMINI_TIMEOUT 3 #define GEMINI_MAXBUF 16 #define GEMINI_TEMPERATURE_FREQ 20 /* Update every 20 POLLMS cycles. For POLLMS 500ms = 10 seconds freq */ #define GEMINI_POSITION_THRESHOLD 5 /* Only send position updates to client if the diff exceeds 5 steps */ #define FOCUS_SETTINGS_TAB "Settings" #define STATUS_TAB "Status" #define ROTATOR_TAB "Rotator" #define HUB_TAB "Hub" std::unique_ptr geminiFR(new Gemini()); void ISGetProperties(const char *dev) { geminiFR->ISGetProperties(dev); } void ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) { geminiFR->ISNewSwitch(dev, name, states, names, n); } void ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n) { geminiFR->ISNewText(dev, name, texts, names, n); } void ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) { geminiFR->ISNewNumber(dev, name, values, names, n); } void ISNewBLOB(const char *dev, const char *name, int sizes[], int blobsizes[], char *blobs[], char *formats[], char *names[], int n) { INDI_UNUSED(dev); INDI_UNUSED(name); INDI_UNUSED(sizes); INDI_UNUSED(blobsizes); INDI_UNUSED(blobs); INDI_UNUSED(formats); INDI_UNUSED(names); INDI_UNUSED(n); } void ISSnoopDevice(XMLEle *root) { geminiFR->ISSnoopDevice(root); } /************************************************************************************ * * ***********************************************************************************/ Gemini::Gemini() : RotatorInterface(this) { focusMoveRequest = 0; focuserSimPosition = 0; // Can move in Absolute & Relative motions and can AbortFocuser motion. FI::SetCapability(FOCUSER_CAN_ABORT | FOCUSER_CAN_ABS_MOVE | FOCUSER_CAN_REL_MOVE); // Rotator capabilities RI::SetCapability(ROTATOR_CAN_ABORT | ROTATOR_CAN_HOME | ROTATOR_CAN_REVERSE); isFocuserAbsolute = true; isFocuserHoming = false; focuserSimStatus[STATUS_MOVING] = ISS_OFF; focuserSimStatus[STATUS_HOMING] = ISS_OFF; focuserSimStatus[STATUS_HOMED] = ISS_OFF; focuserSimStatus[STATUS_FFDETECT] = ISS_OFF; focuserSimStatus[STATUS_TMPPROBE] = ISS_ON; focuserSimStatus[STATUS_REMOTEIO] = ISS_ON; focuserSimStatus[STATUS_HNDCTRL] = ISS_ON; focuserSimStatus[STATUS_REVERSE] = ISS_OFF; DBG_FOCUS = INDI::Logger::getInstance().addDebugLevel("Verbose", "Verbose"); } /************************************************************************************ * * ***********************************************************************************/ Gemini::~Gemini() { } /************************************************************************************ * * ***********************************************************************************/ bool Gemini::initProperties() { INDI::Focuser::initProperties(); //////////////////////////////////////////////////////////// // Focuser Properties /////////////////////////////////////////////////////////// IUFillNumber(&TemperatureN[0], "TEMPERATURE", "Celsius", "%6.2f", -50, 70., 0., 0.); IUFillNumberVector(&TemperatureNP, TemperatureN, 1, getDeviceName(), "FOCUS_TEMPERATURE", "Temperature", MAIN_CONTROL_TAB, IP_RO, 0, IPS_IDLE); // Enable/Disable temperature compensation IUFillSwitch(&TemperatureCompensateS[0], "Enable", "", ISS_OFF); IUFillSwitch(&TemperatureCompensateS[1], "Disable", "", ISS_ON); IUFillSwitchVector(&TemperatureCompensateSP, TemperatureCompensateS, 2, getDeviceName(), "T. Compensation", "", FOCUS_SETTINGS_TAB, IP_RW, ISR_1OFMANY, 0, IPS_IDLE); // Enable/Disable temperature compensation on start IUFillSwitch(&TemperatureCompensateOnStartS[0], "Enable", "", ISS_OFF); IUFillSwitch(&TemperatureCompensateOnStartS[1], "Disable", "", ISS_ON); IUFillSwitchVector(&TemperatureCompensateOnStartSP, TemperatureCompensateOnStartS, 2, getDeviceName(), "T. Compensation @Start", "", FOCUS_SETTINGS_TAB, IP_RW, ISR_1OFMANY, 0, IPS_IDLE); // Temperature Coefficient IUFillNumber(&TemperatureCoeffN[0], "A", "", "%.f", -9999, 9999, 100., 0.); IUFillNumber(&TemperatureCoeffN[1], "B", "", "%.f", -9999, 9999, 100., 0.); IUFillNumber(&TemperatureCoeffN[2], "C", "", "%.f", -9999, 9999, 100., 0.); IUFillNumber(&TemperatureCoeffN[3], "D", "", "%.f", -9999, 9999, 100., 0.); IUFillNumber(&TemperatureCoeffN[4], "E", "", "%.f", -9999, 9999, 100., 0.); IUFillNumberVector(&TemperatureCoeffNP, TemperatureCoeffN, 5, getDeviceName(), "T. Coeff", "", FOCUS_SETTINGS_TAB, IP_RW, 0, IPS_IDLE); // Enable/Disable Home on Start IUFillSwitch(&FocuserHomeOnStartS[0], "Enable", "", ISS_OFF); IUFillSwitch(&FocuserHomeOnStartS[1], "Disable", "", ISS_ON); IUFillSwitchVector(&FocuserHomeOnStartSP, FocuserHomeOnStartS, 2, getDeviceName(), "FOCUSER_HOME_ON_START", "Home on Start", FOCUS_SETTINGS_TAB, IP_RW, ISR_1OFMANY, 0, IPS_IDLE); // Enable/Disable temperature Mode IUFillSwitch(&TemperatureCompensateModeS[0], "A", "", ISS_OFF); IUFillSwitch(&TemperatureCompensateModeS[1], "B", "", ISS_OFF); IUFillSwitch(&TemperatureCompensateModeS[2], "C", "", ISS_OFF); IUFillSwitch(&TemperatureCompensateModeS[3], "D", "", ISS_OFF); IUFillSwitch(&TemperatureCompensateModeS[4], "E", "", ISS_OFF); IUFillSwitchVector(&TemperatureCompensateModeSP, TemperatureCompensateModeS, 5, getDeviceName(), "Compensate Mode", "", FOCUS_SETTINGS_TAB, IP_RW, ISR_ATMOST1, 0, IPS_IDLE); // Enable/Disable backlash IUFillSwitch(&FocuserBacklashCompensationS[0], "Enable", "", ISS_OFF); IUFillSwitch(&FocuserBacklashCompensationS[1], "Disable", "", ISS_ON); IUFillSwitchVector(&FocuserBacklashCompensationSP, FocuserBacklashCompensationS, 2, getDeviceName(), "FOCUSER_BACKLASH_COMPENSATION", "Backlash Compensation", FOCUS_SETTINGS_TAB, IP_RW, ISR_1OFMANY, 0, IPS_IDLE); // Backlash Value IUFillNumber(&FocuserBacklashN[0], "Value", "", "%.f", 0, 99, 5., 0.); IUFillNumberVector(&FocuserBacklashNP, FocuserBacklashN, 1, getDeviceName(), "FOCUSER_BACKLASH", "Backlash", FOCUS_SETTINGS_TAB, IP_RW, 0, IPS_IDLE); // Go to home/center IUFillSwitch(&FocuserGotoS[GOTO_CENTER], "Center", "", ISS_OFF); IUFillSwitch(&FocuserGotoS[GOTO_HOME], "Home", "", ISS_OFF); IUFillSwitchVector(&FocuserGotoSP, FocuserGotoS, 2, getDeviceName(), "FOCUSER_GOTO", "Goto", MAIN_CONTROL_TAB, IP_RW, ISR_1OFMANY, 0, IPS_IDLE); // Focus Status indicators IUFillLight(&FocuserStatusL[STATUS_MOVING], "Is Moving", "", IPS_IDLE); IUFillLight(&FocuserStatusL[STATUS_HOMING], "Is Homing", "", IPS_IDLE); IUFillLight(&FocuserStatusL[STATUS_HOMED], "Is Homed", "", IPS_IDLE); IUFillLight(&FocuserStatusL[STATUS_FFDETECT], "FF Detect", "", IPS_IDLE); IUFillLight(&FocuserStatusL[STATUS_TMPPROBE], "Tmp Probe", "", IPS_IDLE); IUFillLight(&FocuserStatusL[STATUS_REMOTEIO], "Remote IO", "", IPS_IDLE); IUFillLight(&FocuserStatusL[STATUS_HNDCTRL], "Hnd Ctrl", "", IPS_IDLE); IUFillLight(&FocuserStatusL[STATUS_REVERSE], "Reverse", "", IPS_IDLE); IUFillLightVector(&FocuserStatusLP, FocuserStatusL, 8, getDeviceName(), "FOCUSER_STATUS", "Focuser", STATUS_TAB, IPS_IDLE); //////////////////////////////////////////////////////////// // Rotator Properties /////////////////////////////////////////////////////////// // Enable/Disable Home on Start IUFillSwitch(&RotatorHomeOnStartS[0], "Enable", "", ISS_OFF); IUFillSwitch(&RotatorHomeOnStartS[1], "Disable", "", ISS_ON); IUFillSwitchVector(&RotatorHomeOnStartSP, RotatorHomeOnStartS, 2, getDeviceName(), "ROTATOR_HOME_ON_START", "Home on Start", ROTATOR_TAB, IP_RW, ISR_1OFMANY, 0, IPS_IDLE); // Rotator Status indicators IUFillLight(&RotatorStatusL[STATUS_MOVING], "Is Moving", "", IPS_IDLE); IUFillLight(&RotatorStatusL[STATUS_HOMING], "Is Homing", "", IPS_IDLE); IUFillLight(&RotatorStatusL[STATUS_HOMED], "Is Homed", "", IPS_IDLE); IUFillLight(&RotatorStatusL[STATUS_FFDETECT], "FF Detect", "", IPS_IDLE); IUFillLight(&RotatorStatusL[STATUS_TMPPROBE], "Tmp Probe", "", IPS_IDLE); IUFillLight(&RotatorStatusL[STATUS_REMOTEIO], "Remote IO", "", IPS_IDLE); IUFillLight(&RotatorStatusL[STATUS_HNDCTRL], "Hnd Ctrl", "", IPS_IDLE); IUFillLight(&RotatorStatusL[STATUS_REVERSE], "Reverse", "", IPS_IDLE); IUFillLightVector(&RotatorStatusLP, RotatorStatusL, 8, getDeviceName(), "ROTATOR_STATUS", "Rotator", STATUS_TAB, IPS_IDLE); INDI::RotatorInterface::initProperties(ROTATOR_TAB); // Rotator Ticks IUFillNumber(&RotatorAbsPosN[0], "ROTATOR_ABSOLUTE_POSITION", "Ticks", "%.f", 0., 0., 0., 0.); IUFillNumberVector(&RotatorAbsPosNP, RotatorAbsPosN, 1, getDeviceName(), "ABS_ROTATOR_POSITION", "Goto", ROTATOR_TAB, IP_RW, 0, IPS_IDLE ); #if 0 // Rotator Degree IUFillNumber(&RotatorAbsAngleN[0], "ANGLE", "Degrees", "%.2f", 0, 360., 10., 0.); IUFillNumberVector(&RotatorAbsAngleNP, RotatorAbsAngleN, 1, getDeviceName(), "ABS_ROTATOR_ANGLE", "Angle", ROTATOR_TAB, IP_RW, 0, IPS_IDLE ); // Abort Rotator IUFillSwitch(&AbortRotatorS[0], "ABORT", "Abort", ISS_OFF); IUFillSwitchVector(&AbortRotatorSP, AbortRotatorS, 1, getDeviceName(), "ROTATOR_ABORT_MOTION", "Abort Motion", ROTATOR_TAB, IP_RW, ISR_ATMOST1, 0, IPS_IDLE); // Rotator Go to home/center IUFillSwitch(&RotatorGotoS[GOTO_CENTER], "Center", "", ISS_OFF); IUFillSwitch(&RotatorGotoS[GOTO_HOME], "Home", "", ISS_OFF); IUFillSwitchVector(&RotatorGotoSP, RotatorGotoS, 2, getDeviceName(), "ROTATOR_GOTO", "Goto", ROTATOR_TAB, IP_RW, ISR_1OFMANY, 0, IPS_IDLE); #endif // Enable/Disable backlash IUFillSwitch(&RotatorBacklashCompensationS[0], "Enable", "", ISS_OFF); IUFillSwitch(&RotatorBacklashCompensationS[1], "Disable", "", ISS_ON); IUFillSwitchVector(&RotatorBacklashCompensationSP, RotatorBacklashCompensationS, 2, getDeviceName(), "ROTATOR_BACKLASH_COMPENSATION", "Backlash Compensation", ROTATOR_TAB, IP_RW, ISR_1OFMANY, 0, IPS_IDLE); // Backlash Value IUFillNumber(&RotatorBacklashN[0], "Value", "", "%.f", 0, 99, 5., 0.); IUFillNumberVector(&RotatorBacklashNP, RotatorBacklashN, 1, getDeviceName(), "ROTATOR_BACKLASH", "Backlash", ROTATOR_TAB, IP_RW, 0, IPS_IDLE); //////////////////////////////////////////////////////////// // Hub Properties /////////////////////////////////////////////////////////// // Focus name configure in the HUB IUFillText(&HFocusNameT[DEVICE_FOCUSER], "FocusName", "Focuser name", ""); IUFillText(&HFocusNameT[DEVICE_ROTATOR], "RotatorName", "Rotator name", ""); IUFillTextVector(&HFocusNameTP, HFocusNameT, 2, getDeviceName(), "HUBNAMES", "HUB", HUB_TAB, IP_RW, 0, IPS_IDLE); // Led intensity value IUFillNumber(&LedN[0], "Intensity", "", "%.f", 0, 100, 5., 0.); IUFillNumberVector(&LedNP, LedN, 1, getDeviceName(), "Led", "", HUB_TAB, IP_RW, 0, IPS_IDLE); // Reset to Factory setting IUFillSwitch(&ResetS[0], "Factory", "", ISS_OFF); IUFillSwitchVector(&ResetSP, ResetS, 1, getDeviceName(), "Reset", "", HUB_TAB, IP_RW, ISR_ATMOST1, 0, IPS_IDLE); addAuxControls(); serialConnection->setDefaultBaudRate(Connection::Serial::B_115200); return true; } /************************************************************************************ * * ***********************************************************************************/ bool Gemini::updateProperties() { INDI::Focuser::updateProperties(); if (isConnected()) { // Focuser Properties defineNumber(&TemperatureNP); defineNumber(&TemperatureCoeffNP); defineSwitch(&TemperatureCompensateModeSP); defineSwitch(&TemperatureCompensateSP); defineSwitch(&TemperatureCompensateOnStartSP); defineSwitch(&FocuserBacklashCompensationSP); defineNumber(&FocuserBacklashNP); defineSwitch(&FocuserHomeOnStartSP); defineSwitch(&FocuserGotoSP); defineLight(&FocuserStatusLP); // Rotator Properties INDI::RotatorInterface::updateProperties(); /* defineNumber(&RotatorAbsAngleNP); defineSwitch(&AbortRotatorSP); defineSwitch(&RotatorGotoSP); defineSwitch(&ReverseRotatorSP); */ defineNumber(&RotatorAbsPosNP); defineSwitch(&RotatorBacklashCompensationSP); defineNumber(&RotatorBacklashNP); defineSwitch(&RotatorHomeOnStartSP); defineLight(&RotatorStatusLP); // Hub Properties defineText(&HFocusNameTP); defineSwitch(&ResetSP); defineNumber(&LedNP); if (getFocusConfig() && getRotatorConfig()) LOG_INFO("Gemini paramaters updated, rotating focuser ready for use."); else { LOG_ERROR("Failed to retrieve rotating focuser configuration settings..."); return false; } } else { // Focuser Properties deleteProperty(TemperatureNP.name); deleteProperty(TemperatureCoeffNP.name); deleteProperty(TemperatureCompensateModeSP.name); deleteProperty(TemperatureCompensateSP.name); deleteProperty(TemperatureCompensateOnStartSP.name); deleteProperty(FocuserBacklashCompensationSP.name); deleteProperty(FocuserBacklashNP.name); deleteProperty(FocuserGotoSP.name); deleteProperty(FocuserHomeOnStartSP.name); deleteProperty(FocuserStatusLP.name); // Rotator Properties INDI::RotatorInterface::updateProperties(); /* deleteProperty(RotatorAbsAngleNP.name); deleteProperty(AbortRotatorSP.name); deleteProperty(RotatorGotoSP.name); deleteProperty(ReverseRotatorSP.name); */ deleteProperty(RotatorAbsPosNP.name); deleteProperty(RotatorBacklashCompensationSP.name); deleteProperty(RotatorBacklashNP.name); deleteProperty(RotatorHomeOnStartSP.name); deleteProperty(RotatorStatusLP.name); // Hub Properties deleteProperty(HFocusNameTP.name); deleteProperty(LedNP.name); deleteProperty(ResetSP.name); } return true; } /************************************************************************************ * * ***********************************************************************************/ bool Gemini::Handshake() { if (ack()) { LOG_INFO("Gemini is online. Getting focus parameters..."); return true; } LOG_INFO("Error retreiving data from Gemini, please ensure Gemini controller is " "powered and the port is correct."); return false; } /************************************************************************************ * * ***********************************************************************************/ const char *Gemini::getDefaultName() { // Has to be overide by child instance return "Gemini Focusing Rotator"; } /************************************************************************************ * * ***********************************************************************************/ bool Gemini::ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) { if (dev != nullptr && strcmp(dev, getDeviceName()) == 0) { // Temperature Compensation if (strcmp(TemperatureCompensateSP.name, name) == 0) { int prevIndex = IUFindOnSwitchIndex(&TemperatureCompensateSP); IUUpdateSwitch(&TemperatureCompensateSP, states, names, n); if (setTemperatureCompensation(TemperatureCompensateS[0].s == ISS_ON)) { TemperatureCompensateSP.s = IPS_OK; } else { IUResetSwitch(&TemperatureCompensateSP); TemperatureCompensateSP.s = IPS_ALERT; TemperatureCompensateS[prevIndex].s = ISS_ON; } IDSetSwitch(&TemperatureCompensateSP, nullptr); return true; } // Temperature Compensation on Start if (!strcmp(TemperatureCompensateOnStartSP.name, name)) { int prevIndex = IUFindOnSwitchIndex(&TemperatureCompensateOnStartSP); IUUpdateSwitch(&TemperatureCompensateOnStartSP, states, names, n); if (setTemperatureCompensationOnStart(TemperatureCompensateOnStartS[0].s == ISS_ON)) { TemperatureCompensateOnStartSP.s = IPS_OK; } else { IUResetSwitch(&TemperatureCompensateOnStartSP); TemperatureCompensateOnStartSP.s = IPS_ALERT; TemperatureCompensateOnStartS[prevIndex].s = ISS_ON; } IDSetSwitch(&TemperatureCompensateOnStartSP, nullptr); return true; } // Temperature Compensation Mode if (!strcmp(TemperatureCompensateModeSP.name, name)) { int prevIndex = IUFindOnSwitchIndex(&TemperatureCompensateModeSP); IUUpdateSwitch(&TemperatureCompensateModeSP, states, names, n); char mode = IUFindOnSwitchIndex(&TemperatureCompensateModeSP) + 'A'; if (setTemperatureCompensationMode(mode)) { TemperatureCompensateModeSP.s = IPS_OK; } else { IUResetSwitch(&TemperatureCompensateModeSP); TemperatureCompensateModeSP.s = IPS_ALERT; TemperatureCompensateModeS[prevIndex].s = ISS_ON; } IDSetSwitch(&TemperatureCompensateModeSP, nullptr); return true; } // Focuser Home on Start Enable/Disable if (!strcmp(FocuserHomeOnStartSP.name, name)) { int prevIndex = IUFindOnSwitchIndex(&FocuserHomeOnStartSP); IUUpdateSwitch(&FocuserHomeOnStartSP, states, names, n); if (homeOnStart(DEVICE_FOCUSER, FocuserHomeOnStartS[0].s == ISS_ON)) { FocuserHomeOnStartSP.s = IPS_OK; } else { IUResetSwitch(&FocuserHomeOnStartSP); FocuserHomeOnStartSP.s = IPS_ALERT; FocuserHomeOnStartS[prevIndex].s = ISS_ON; } IDSetSwitch(&FocuserHomeOnStartSP, nullptr); return true; } // Rotator Home on Start Enable/Disable if (!strcmp(RotatorHomeOnStartSP.name, name)) { int prevIndex = IUFindOnSwitchIndex(&RotatorHomeOnStartSP); IUUpdateSwitch(&RotatorHomeOnStartSP, states, names, n); if (homeOnStart(DEVICE_ROTATOR, RotatorHomeOnStartS[0].s == ISS_ON)) { RotatorHomeOnStartSP.s = IPS_OK; } else { IUResetSwitch(&RotatorHomeOnStartSP); RotatorHomeOnStartSP.s = IPS_ALERT; RotatorHomeOnStartS[prevIndex].s = ISS_ON; } IDSetSwitch(&RotatorHomeOnStartSP, nullptr); return true; } // Focuser Backlash enable/disable if (!strcmp(FocuserBacklashCompensationSP.name, name)) { int prevIndex = IUFindOnSwitchIndex(&FocuserBacklashCompensationSP); IUUpdateSwitch(&FocuserBacklashCompensationSP, states, names, n); if (setBacklashCompensation(DEVICE_FOCUSER, FocuserBacklashCompensationS[0].s == ISS_ON)) { FocuserBacklashCompensationSP.s = IPS_OK; } else { IUResetSwitch(&FocuserBacklashCompensationSP); FocuserBacklashCompensationSP.s = IPS_ALERT; FocuserBacklashCompensationS[prevIndex].s = ISS_ON; } IDSetSwitch(&FocuserBacklashCompensationSP, nullptr); return true; } // Rotator Backlash enable/disable if (!strcmp(RotatorBacklashCompensationSP.name, name)) { int prevIndex = IUFindOnSwitchIndex(&RotatorBacklashCompensationSP); IUUpdateSwitch(&RotatorBacklashCompensationSP, states, names, n); if (setBacklashCompensation(DEVICE_ROTATOR, RotatorBacklashCompensationS[0].s == ISS_ON)) { RotatorBacklashCompensationSP.s = IPS_OK; } else { IUResetSwitch(&RotatorBacklashCompensationSP); RotatorBacklashCompensationSP.s = IPS_ALERT; RotatorBacklashCompensationS[prevIndex].s = ISS_ON; } IDSetSwitch(&RotatorBacklashCompensationSP, nullptr); return true; } // Reset to Factory setting if (strcmp(ResetSP.name, name) == 0) { IUResetSwitch(&ResetSP); if (resetFactory()) ResetSP.s = IPS_OK; else ResetSP.s = IPS_ALERT; IDSetSwitch(&ResetSP, nullptr); return true; } // Focser Go to home/center if (!strcmp(FocuserGotoSP.name, name)) { IUUpdateSwitch(&FocuserGotoSP, states, names, n); if (FocuserGotoS[GOTO_HOME].s == ISS_ON) { if (home(DEVICE_FOCUSER)) { FocuserGotoSP.s = IPS_BUSY; FocusAbsPosNP.s = IPS_BUSY; IDSetNumber(&FocusAbsPosNP, nullptr); isFocuserHoming = true; LOG_INFO("Focuser moving to home position..."); } else FocuserGotoSP.s = IPS_ALERT; } else { if (center(DEVICE_FOCUSER)) { FocuserGotoSP.s = IPS_BUSY; LOG_INFO("Focuser moving to center position..."); FocusAbsPosNP.s = IPS_BUSY; IDSetNumber(&FocusAbsPosNP, nullptr); } else FocuserGotoSP.s = IPS_ALERT; } IDSetSwitch(&FocuserGotoSP, nullptr); return true; } // Process all rotator properties if (strstr(name, "ROTATOR")) { if (INDI::RotatorInterface::processSwitch(dev, name, states, names, n)) return true; } // Rotator Go to home/center #if 0 if (!strcmp(RotatorGotoSP.name, name)) { IUUpdateSwitch(&RotatorGotoSP, states, names, n); if (RotatorGotoS[GOTO_HOME].s == ISS_ON) { if (home(DEVICE_ROTATOR)) { RotatorGotoSP.s = IPS_BUSY; RotatorAbsPosNP.s = IPS_BUSY; IDSetNumber(&RotatorAbsPosNP, nullptr); isRotatorHoming = true; LOG_INFO("Rotator moving to home position..."); } else RotatorGotoSP.s = IPS_ALERT; } else { if (center(DEVICE_ROTATOR)) { RotatorGotoSP.s = IPS_BUSY; LOG_INFO("Rotator moving to center position..."); RotatorAbsPosNP.s = IPS_BUSY; IDSetNumber(&RotatorAbsPosNP, nullptr); } else RotatorGotoSP.s = IPS_ALERT; } IDSetSwitch(&RotatorGotoSP, nullptr); return true; } // Reverse Direction if (!strcmp(ReverseRotatorSP.name, name)) { IUUpdateSwitch(&ReverseRotatorSP, states, names, n); if (reverseRotator(ReverseRotatorS[0].s == ISS_ON)) ReverseRotatorSP.s = IPS_OK; else ReverseRotatorSP.s = IPS_ALERT; IDSetSwitch(&ReverseRotatorSP, nullptr); return true; } // Halt Rotator if (!strcmp(AbortRotatorSP.name, name)) { if (halt(DEVICE_ROTATOR)) { RotatorAbsPosNP.s = RotatorAbsAngleNP.s = RotatorGotoSP.s = IPS_IDLE; IDSetNumber(&RotatorAbsPosNP, nullptr); IDSetNumber(&RotatorAbsAngleNP, nullptr); IUResetSwitch(&RotatorGotoSP); IDSetSwitch(&RotatorGotoSP, nullptr); AbortRotatorSP.s = IPS_OK; } else AbortRotatorSP.s = IPS_ALERT; IDSetSwitch(&AbortRotatorSP, nullptr); return true; } #endif } return INDI::Focuser::ISNewSwitch(dev, name, states, names, n); } /************************************************************************************ * * ***********************************************************************************/ bool Gemini::ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n) { if (dev != nullptr && strcmp(dev, getDeviceName()) == 0) { // Set device nickname to the HUB itself if (!strcmp(name, HFocusNameTP.name)) { IUUpdateText(&HFocusNameTP, texts, names, n); if (setNickname(DEVICE_FOCUSER, HFocusNameT[DEVICE_FOCUSER].text) && setNickname(DEVICE_ROTATOR, HFocusNameT[DEVICE_ROTATOR].text)) HFocusNameTP.s = IPS_OK; else HFocusNameTP.s = IPS_ALERT; IDSetText(&HFocusNameTP, nullptr); return true; } } return INDI::Focuser::ISNewText(dev, name, texts, names, n); } /************************************************************************************ * * ***********************************************************************************/ bool Gemini::ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) { if (dev != nullptr && strcmp(dev, getDeviceName()) == 0) { // Temperature Coefficient if (!strcmp(TemperatureCoeffNP.name, name)) { IUUpdateNumber(&TemperatureCoeffNP, values, names, n); for (int i = 0; i < n; i++) { if (setTemperatureCompensationCoeff('A' + i, TemperatureCoeffN[i].value) == false) { LOG_ERROR("Failed to set temperature coefficeints."); TemperatureCoeffNP.s = IPS_ALERT; IDSetNumber(&TemperatureCoeffNP, nullptr); return false; } } TemperatureCoeffNP.s = IPS_OK; IDSetNumber(&TemperatureCoeffNP, nullptr); return true; } // Focuser Backlash Value if (!strcmp(FocuserBacklashNP.name, name)) { IUUpdateNumber(&FocuserBacklashNP, values, names, n); if (setBacklashCompensationSteps(DEVICE_FOCUSER, FocuserBacklashN[0].value) == false) { LOG_ERROR("Failed to set focuser backlash value."); FocuserBacklashNP.s = IPS_ALERT; IDSetNumber(&FocuserBacklashNP, nullptr); return false; } FocuserBacklashNP.s = IPS_OK; IDSetNumber(&FocuserBacklashNP, nullptr); return true; } // Rotator Backlash Value if (!strcmp(RotatorBacklashNP.name, name)) { IUUpdateNumber(&RotatorBacklashNP, values, names, n); if (setBacklashCompensationSteps(DEVICE_ROTATOR, RotatorBacklashN[0].value) == false) { LOG_ERROR("Failed to set rotator backlash value."); RotatorBacklashNP.s = IPS_ALERT; IDSetNumber(&RotatorBacklashNP, nullptr); return false; } RotatorBacklashNP.s = IPS_OK; IDSetNumber(&RotatorBacklashNP, nullptr); return true; } // Set LED intensity to the HUB itself via function setLedLevel() if (!strcmp(LedNP.name, name)) { IUUpdateNumber(&LedNP, values, names, n); if (setLedLevel(LedN[0].value)) LedNP.s = IPS_OK; else LedNP.s = IPS_ALERT; LOGF_INFO("Focuser LED level intensity : %f", LedN[0].value); IDSetNumber(&LedNP, nullptr); return true; } // Set Rotator Absolute Steps if (!strcmp(RotatorAbsPosNP.name, name)) { IUUpdateNumber(&RotatorAbsPosNP, values, names, n); RotatorAbsPosNP.s = MoveAbsRotatorTicks(static_cast(RotatorAbsPosN[0].value)); IDSetNumber(&RotatorAbsPosNP, nullptr); return true; } if (strstr(name, "ROTATOR")) { if (INDI::RotatorInterface::processNumber(dev, name, values, names, n)) return true; } #if 0 // Set Rotator Absolute Steps if (!strcmp(RotatorAbsPosNP.name, name)) { IUUpdateNumber(&RotatorAbsPosNP, values, names, n); RotatorAbsPosNP.s = MoveAbsRotatorTicks(static_cast(RotatorAbsPosN[0].value)); IDSetNumber(&RotatorAbsPosNP, nullptr); return true; } // Set Rotator Absolute Angle if (!strcmp(RotatorAbsAngleNP.name, name)) { IUUpdateNumber(&RotatorAbsAngleNP, values, names, n); RotatorAbsAngleNP.s = MoveAbsRotatorAngle(RotatorAbsAngleN[0].value); IDSetNumber(&RotatorAbsAngleNP, nullptr); return true; } #endif } return INDI::Focuser::ISNewNumber(dev, name, values, names, n); } /************************************************************************************ * * ***********************************************************************************/ bool Gemini::ack() { const char *cmd = ""; int errcode = 0; char errmsg[MAXRBUF]; char response[16]; int nbytes_read = 0; int nbytes_written = 0; memset(response, 0, sizeof(response)); LOGF_DEBUG("CMD (%s)", cmd); if (isSimulation()) { strncpy(response, "Castor", 16); nbytes_read = strlen(response) + 1; } else { if ((errcode = tty_write(PortFD, cmd, strlen(cmd), &nbytes_written)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); LOGF_ERROR("%s", errmsg); return false; } if (isResponseOK() == false) return false; if ((errcode = tty_read_section(PortFD, response, 0xA, GEMINI_TIMEOUT, &nbytes_read)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); LOGF_ERROR("%s", errmsg); return false; } } if (nbytes_read > 0) { response[nbytes_read - 1] = '\0'; LOGF_DEBUG("RES (%s)", response); LOGF_INFO("%s is detected.", response); // Read 'END' tty_read_section(PortFD, response, 0xA, GEMINI_TIMEOUT, &nbytes_read); tcflush(PortFD, TCIFLUSH); return true; } tcflush(PortFD, TCIFLUSH); return false; } /************************************************************************************ * * ***********************************************************************************/ bool Gemini::getFocusConfig() { const char *cmd = ""; int errcode = 0; char errmsg[MAXRBUF]; char response[64]; int nbytes_read = 0; int nbytes_written = 0; char key[16]; memset(response, 0, sizeof(response)); LOGF_DEBUG("CMD (%s)", cmd); if (isSimulation()) { strncpy(response, "!00", sizeof(response)); nbytes_read = strlen(response) + 1; } else { tcflush(PortFD, TCIFLUSH); if ((errcode = tty_write(PortFD, cmd, strlen(cmd), &nbytes_written)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); LOGF_ERROR("%s", errmsg); return false; } if (isResponseOK() == false) return false; /*if ((errcode = tty_read_section(PortFD, response, 0xA, GEMINI_TIMEOUT, &nbytes_read)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); LOGF_ERROR("%s", errmsg); return false; }*/ } /*if (nbytes_read > 0) { response[nbytes_read - 1] = '\0'; LOGF_DEBUG("RES (%s)", response); if ((strcmp(response, "CONFIG1")) && (strcmp(response, "CONFIG2"))) return false; }*/ memset(response, 0, sizeof(response)); // Nickname if (isSimulation()) { strncpy(response, "NickName=Tommy\n", sizeof(response)); nbytes_read = strlen(response); } else if ((errcode = tty_read_section(PortFD, response, 0xA, GEMINI_TIMEOUT, &nbytes_read)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); LOGF_ERROR("%s", errmsg); return false; } response[nbytes_read - 1] = '\0'; LOGF_DEBUG("RES (%s)", response); char nickname[16]; int rc = sscanf(response, "%16[^=]=%16[^\n]s", key, nickname); if (rc != 2) return false; IUSaveText(&HFocusNameT[0], nickname); IDSetText(&HFocusNameTP, nullptr); HFocusNameTP.s = IPS_OK; IDSetText(&HFocusNameTP, nullptr); memset(response, 0, sizeof(response)); // Get Max Position if (isSimulation()) { snprintf(response, sizeof(response), "Max Pos = %06d\n", 100000); nbytes_read = strlen(response); } else if ((errcode = tty_read_section(PortFD, response, 0xA, GEMINI_TIMEOUT, &nbytes_read)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); LOGF_ERROR("%s", errmsg); return false; } response[nbytes_read - 1] = '\0'; LOGF_DEBUG("RES (%s)", response); uint32_t maxPos = 0; rc = sscanf(response, "%16[^=]=%d", key, &maxPos); if (rc == 2) { FocusAbsPosN[0].max = maxPos; FocusAbsPosN[0].step = maxPos / 50.0; FocusAbsPosN[0].min = 0; FocusRelPosN[0].max = maxPos / 2; FocusRelPosN[0].step = maxPos / 100.0; FocusRelPosN[0].min = 0; IUUpdateMinMax(&FocusAbsPosNP); IUUpdateMinMax(&FocusRelPosNP); maxControllerTicks = maxPos; } else return false; memset(response, 0, sizeof(response)); // Get Device Type if (isSimulation()) { strncpy(response, "Dev Typ = A\n", sizeof(response)); nbytes_read = strlen(response); } else if ((errcode = tty_read_section(PortFD, response, 0xA, GEMINI_TIMEOUT, &nbytes_read)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); LOGF_ERROR("%s", errmsg); return false; } response[nbytes_read - 1] = '\0'; LOGF_DEBUG("RES (%s)", response); // Get Status Parameters memset(response, 0, sizeof(response)); // Temperature Compensation On? if (isSimulation()) { snprintf(response, sizeof(response), "TComp ON = %d\n", TemperatureCompensateS[0].s == ISS_ON ? 1 : 0); nbytes_read = strlen(response); } else if ((errcode = tty_read_section(PortFD, response, 0xA, GEMINI_TIMEOUT, &nbytes_read)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); LOGF_ERROR("%s", errmsg); return false; } response[nbytes_read - 1] = '\0'; LOGF_DEBUG("RES (%s)", response); int TCompOn; rc = sscanf(response, "%16[^=]=%d", key, &TCompOn); if (rc != 2) return false; IUResetSwitch(&TemperatureCompensateSP); TemperatureCompensateS[0].s = TCompOn ? ISS_ON : ISS_OFF; TemperatureCompensateS[0].s = TCompOn ? ISS_OFF : ISS_ON; TemperatureCompensateSP.s = IPS_OK; IDSetSwitch(&TemperatureCompensateSP, nullptr); memset(response, 0, sizeof(response)); // Temperature Coeff A if (isSimulation()) { snprintf(response, sizeof(response), "TempCo A = %d\n", (int)TemperatureCoeffN[FOCUS_A_COEFF].value); nbytes_read = strlen(response); } else if ((errcode = tty_read_section(PortFD, response, 0xA, GEMINI_TIMEOUT, &nbytes_read)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); LOGF_ERROR("%s", errmsg); return false; } response[nbytes_read - 1] = '\0'; LOGF_DEBUG("RES (%s)", response); int TCoeffA; rc = sscanf(response, "%16[^=]=%d", key, &TCoeffA); if (rc != 2) return false; TemperatureCoeffN[FOCUS_A_COEFF].value = TCoeffA; memset(response, 0, sizeof(response)); // Temperature Coeff B if (isSimulation()) { snprintf(response, sizeof(response), "TempCo B = %d\n", (int)TemperatureCoeffN[FOCUS_B_COEFF].value); nbytes_read = strlen(response); } else if ((errcode = tty_read_section(PortFD, response, 0xA, GEMINI_TIMEOUT, &nbytes_read)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); LOGF_ERROR("%s", errmsg); return false; } response[nbytes_read - 1] = '\0'; LOGF_DEBUG("RES (%s)", response); int TCoeffB; rc = sscanf(response, "%16[^=]=%d", key, &TCoeffB); if (rc != 2) return false; TemperatureCoeffN[FOCUS_B_COEFF].value = TCoeffB; memset(response, 0, sizeof(response)); // Temperature Coeff C if (isSimulation()) { snprintf(response, sizeof(response), "TempCo C = %d\n", (int)TemperatureCoeffN[FOCUS_C_COEFF].value); nbytes_read = strlen(response); } else if ((errcode = tty_read_section(PortFD, response, 0xA, GEMINI_TIMEOUT, &nbytes_read)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); LOGF_ERROR("%s", errmsg); return false; } response[nbytes_read - 1] = '\0'; LOGF_DEBUG("RES (%s)", response); int TCoeffC; rc = sscanf(response, "%16[^=]=%d", key, &TCoeffC); if (rc != 2) return false; TemperatureCoeffN[FOCUS_C_COEFF].value = TCoeffC; memset(response, 0, sizeof(response)); // Temperature Coeff D if (isSimulation()) { snprintf(response, sizeof(response), "TempCo D = %d\n", (int)TemperatureCoeffN[FOCUS_D_COEFF].value); nbytes_read = strlen(response); } else if ((errcode = tty_read_section(PortFD, response, 0xA, GEMINI_TIMEOUT, &nbytes_read)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); LOGF_ERROR("%s", errmsg); return false; } response[nbytes_read - 1] = '\0'; LOGF_DEBUG("RES (%s)", response); int TCoeffD; rc = sscanf(response, "%16[^=]=%d", key, &TCoeffD); if (rc != 2) return false; TemperatureCoeffN[FOCUS_D_COEFF].value = TCoeffD; memset(response, 0, sizeof(response)); // Temperature Coeff E if (isSimulation()) { snprintf(response, sizeof(response), "TempCo E = %d\n", (int)TemperatureCoeffN[FOCUS_E_COEFF].value); nbytes_read = strlen(response); } else if ((errcode = tty_read_section(PortFD, response, 0xA, GEMINI_TIMEOUT, &nbytes_read)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); LOGF_ERROR("%s", errmsg); return false; } response[nbytes_read - 1] = '\0'; LOGF_DEBUG("RES (%s)", response); int TCoeffE; rc = sscanf(response, "%16[^=]=%d", key, &TCoeffE); if (rc != 2) return false; TemperatureCoeffN[FOCUS_E_COEFF].value = TCoeffE; TemperatureCoeffNP.s = IPS_OK; IDSetNumber(&TemperatureCoeffNP, nullptr); memset(response, 0, sizeof(response)); // Temperature Compensation Mode if (isSimulation()) { snprintf(response, sizeof(response), "TC Mode = %c\n", 'C'); nbytes_read = strlen(response); } else if ((errcode = tty_read_section(PortFD, response, 0xA, GEMINI_TIMEOUT, &nbytes_read)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); LOGF_ERROR("%s", errmsg); return false; } response[nbytes_read - 1] = '\0'; LOGF_DEBUG("RES (%s)", response); char compensateMode; rc = sscanf(response, "%16[^=]= %c", key, &compensateMode); if (rc != 2) return false; IUResetSwitch(&TemperatureCompensateModeSP); int index = compensateMode - 'A'; if (index >= 0 && index <= 5) { TemperatureCompensateModeS[index].s = ISS_ON; TemperatureCompensateModeSP.s = IPS_OK; } else { LOGF_ERROR("Invalid index %d for compensation mode.", index); TemperatureCompensateModeSP.s = IPS_ALERT; } IDSetSwitch(&TemperatureCompensateModeSP, nullptr); // Backlash Compensation memset(response, 0, sizeof(response)); if (isSimulation()) { snprintf(response, sizeof(response), "BLC En = %d\n", FocuserBacklashCompensationS[0].s == ISS_ON ? 1 : 0); nbytes_read = strlen(response); } else if ((errcode = tty_read_section(PortFD, response, 0xA, GEMINI_TIMEOUT, &nbytes_read)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); LOGF_ERROR("%s", errmsg); return false; } response[nbytes_read - 1] = '\0'; LOGF_DEBUG("RES (%s)", response); int BLCCompensate; rc = sscanf(response, "%16[^=]=%d", key, &BLCCompensate); if (rc != 2) return false; IUResetSwitch(&FocuserBacklashCompensationSP); FocuserBacklashCompensationS[0].s = BLCCompensate ? ISS_ON : ISS_OFF; FocuserBacklashCompensationS[1].s = BLCCompensate ? ISS_OFF : ISS_ON; FocuserBacklashCompensationSP.s = IPS_OK; IDSetSwitch(&FocuserBacklashCompensationSP, nullptr); // Backlash Value memset(response, 0, sizeof(response)); if (isSimulation()) { snprintf(response, sizeof(response), "BLC Stps = %d\n", 50); nbytes_read = strlen(response); } else if ((errcode = tty_read_section(PortFD, response, 0xA, GEMINI_TIMEOUT, &nbytes_read)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); LOGF_ERROR("%s", errmsg); return false; } response[nbytes_read - 1] = '\0'; LOGF_DEBUG("RES (%s)", response); int BLCValue; rc = sscanf(response, "%16[^=]=%d", key, &BLCValue); if (rc != 2) return false; FocuserBacklashN[0].value = BLCValue; FocuserBacklashNP.s = IPS_OK; IDSetNumber(&FocuserBacklashNP, nullptr); // Temperature Compensation on Start memset(response, 0, sizeof(response)); if (isSimulation()) { snprintf(response, sizeof(response), "TC Start = %d\n", TemperatureCompensateOnStartS[0].s == ISS_ON ? 1 : 0); nbytes_read = strlen(response); } else if ((errcode = tty_read_section(PortFD, response, 0xA, GEMINI_TIMEOUT, &nbytes_read)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); LOGF_ERROR("%s", errmsg); return false; } response[nbytes_read - 1] = '\0'; LOGF_DEBUG("RES (%s)", response); int TCOnStart; rc = sscanf(response, "%16[^=]=%d", key, &TCOnStart); if (rc != 2) return false; IUResetSwitch(&TemperatureCompensateOnStartSP); TemperatureCompensateOnStartS[0].s = TCOnStart ? ISS_ON : ISS_OFF; TemperatureCompensateOnStartS[1].s = TCOnStart ? ISS_OFF : ISS_ON; TemperatureCompensateOnStartSP.s = IPS_OK; IDSetSwitch(&TemperatureCompensateOnStartSP, nullptr); // Get Status Parameters memset(response, 0, sizeof(response)); // Home on start on? if (isSimulation()) { snprintf(response, sizeof(response), "HOnStart = %d\n", FocuserHomeOnStartS[0].s == ISS_ON ? 1 : 0); nbytes_read = strlen(response); } else if ((errcode = tty_read_section(PortFD, response, 0xA, GEMINI_TIMEOUT, &nbytes_read)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); LOGF_ERROR("%s", errmsg); return false; } response[nbytes_read - 1] = '\0'; LOGF_DEBUG("RES (%s)", response); int StartOnHome; rc = sscanf(response, "%16[^=]=%d", key, &StartOnHome); if (rc != 2) return false; IUResetSwitch(&FocuserHomeOnStartSP); FocuserHomeOnStartS[0].s = StartOnHome ? ISS_ON : ISS_OFF; FocuserHomeOnStartS[1].s = StartOnHome ? ISS_OFF : ISS_ON; FocuserHomeOnStartSP.s = IPS_OK; IDSetSwitch(&FocuserHomeOnStartSP, nullptr); // Added By Philippe Besson the 28th of June for 'END' evalution // END is reached memset(response, 0, sizeof(response)); if (isSimulation()) { strncpy(response, "END\n", 16); nbytes_read = strlen(response); } else if ((errcode = tty_read_section(PortFD, response, 0xA, GEMINI_TIMEOUT, &nbytes_read)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); LOGF_ERROR("%s", errmsg); return false; } if (nbytes_read > 0) { response[nbytes_read - 1] = '\0'; // Display the response to be sure to have read the complet TTY Buffer. LOGF_DEBUG("RES (%s)", response); if (strcmp(response, "END")) return false; } // End of added code by Philippe Besson tcflush(PortFD, TCIFLUSH); focuserConfigurationComplete = true; return true; } /************************************************************************************ * * ***********************************************************************************/ bool Gemini::getRotatorStatus() { const char *cmd = ""; int errcode = 0; char errmsg[MAXRBUF]; char response[32]; int nbytes_read = 0; int nbytes_written = 0; char key[16]; memset(response, 0, sizeof(response)); LOGF_DEBUG("CMD (%s)", cmd); if (isSimulation()) { strncpy(response, "!00", 16); nbytes_read = strlen(response) + 1; } else { tcflush(PortFD, TCIFLUSH); if ((errcode = tty_write(PortFD, cmd, strlen(cmd), &nbytes_written)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); LOGF_ERROR("%s", errmsg); return false; } if (isResponseOK() == false) return false; /*if ((errcode = tty_read_section(PortFD, response, 0xA, GEMINI_TIMEOUT, &nbytes_read)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); LOGF_ERROR("%s", errmsg); return false; }*/ } /////////////////////////////////////// // #1 Get Current Position /////////////////////////////////////// memset(response, 0, sizeof(response)); if (isSimulation()) { snprintf(response, 32, "CurrStep = %06d\n", rotatorSimPosition); nbytes_read = strlen(response); } else if ((errcode = tty_read_section(PortFD, response, 0xA, GEMINI_TIMEOUT, &nbytes_read)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); LOGF_ERROR("%s", errmsg); return false; } response[nbytes_read - 1] = '\0'; DEBUGF(DBG_FOCUS, "RES (%s)", response); int currPos = 0; int rc = sscanf(response, "%16[^=]=%d", key, &currPos); if (rc == 2) { // Do not spam unless there is an actual change if (RotatorAbsPosN[0].value != currPos) { RotatorAbsPosN[0].value = currPos; IDSetNumber(&RotatorAbsPosNP, nullptr); } } else return false; /////////////////////////////////////// // #2 Get Target Position /////////////////////////////////////// memset(response, 0, sizeof(response)); if (isSimulation()) { snprintf(response, 32, "TargStep = %06d\n", targetFocuserPosition); nbytes_read = strlen(response); } else if ((errcode = tty_read_section(PortFD, response, 0xA, GEMINI_TIMEOUT, &nbytes_read)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); LOGF_ERROR("%s", errmsg); return false; } response[nbytes_read - 1] = '\0'; DEBUGF(DBG_FOCUS, "RES (%s)", response); /////////////////////////////////////// // #3 Get Current PA /////////////////////////////////////// memset(response, 0, sizeof(response)); if (isSimulation()) { snprintf(response, 32, "CurenPA = %06d\n", rotatorSimPA); nbytes_read = strlen(response); } else if ((errcode = tty_read_section(PortFD, response, 0xA, GEMINI_TIMEOUT, &nbytes_read)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); LOGF_ERROR("%s", errmsg); return false; } response[nbytes_read - 1] = '\0'; DEBUGF(DBG_FOCUS, "RES (%s)", response); int currPA = 0; rc = sscanf(response, "%16[^=]=%d", key, &currPA); if (rc == 2) { // Only send when above a threshold double diffPA = fabs(GotoRotatorN[0].value - currPA / 1000.0); if (diffPA >= 0.01) { GotoRotatorN[0].value = currPA / 1000.0; IDSetNumber(&GotoRotatorNP, nullptr); } } else return false; /////////////////////////////////////// // #3 Get Target PA /////////////////////////////////////// memset(response, 0, sizeof(response)); if (isSimulation()) { snprintf(response, 32, "TargetPA = %06d\n", targetFocuserPosition); nbytes_read = strlen(response); } else if ((errcode = tty_read_section(PortFD, response, 0xA, GEMINI_TIMEOUT, &nbytes_read)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); LOGF_ERROR("%s", errmsg); return false; } response[nbytes_read - 1] = '\0'; DEBUGF(DBG_FOCUS, "RES (%s)", response); // Get Status Parameters /////////////////////////////////////// // #5 is Moving? /////////////////////////////////////// memset(response, 0, sizeof(response)); if (isSimulation()) { snprintf(response, 32, "IsMoving = %d\n", (rotatorSimStatus[STATUS_MOVING] == ISS_ON) ? 1 : 0); nbytes_read = strlen(response); } else if ((errcode = tty_read_section(PortFD, response, 0xA, GEMINI_TIMEOUT, &nbytes_read)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); LOGF_ERROR("%s", errmsg); return false; } response[nbytes_read - 1] = '\0'; DEBUGF(DBG_FOCUS, "RES (%s)", response); int isMoving; rc = sscanf(response, "%16[^=]=%d", key, &isMoving); if (rc != 2) return false; RotatorStatusL[STATUS_MOVING].s = isMoving ? IPS_BUSY : IPS_IDLE; /////////////////////////////////////// // #6 is Homing? /////////////////////////////////////// memset(response, 0, sizeof(response)); if (isSimulation()) { snprintf(response, 32, "IsHoming = %d\n", (rotatorSimStatus[STATUS_HOMING] == ISS_ON) ? 1 : 0); nbytes_read = strlen(response); } else if ((errcode = tty_read_section(PortFD, response, 0xA, GEMINI_TIMEOUT, &nbytes_read)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); LOGF_ERROR("%s", errmsg); return false; } response[nbytes_read - 1] = '\0'; DEBUGF(DBG_FOCUS, "RES (%s)", response); int _isHoming; rc = sscanf(response, "%16[^=]=%d", key, &_isHoming); if (rc != 2) return false; RotatorStatusL[STATUS_HOMING].s = _isHoming ? IPS_BUSY : IPS_IDLE; // We set that isHoming in process, but we don't set it to false here it must be reset in TimerHit if (RotatorStatusL[STATUS_HOMING].s == IPS_BUSY) isRotatorHoming = true; /////////////////////////////////////// // #6 is Homed? /////////////////////////////////////// memset(response, 0, sizeof(response)); if (isSimulation()) { snprintf(response, 32, "IsHomed = %d\n", (rotatorSimStatus[STATUS_HOMED] == ISS_ON) ? 1 : 0); nbytes_read = strlen(response); } else if ((errcode = tty_read_section(PortFD, response, 0xA, GEMINI_TIMEOUT, &nbytes_read)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); LOGF_ERROR("%s", errmsg); return false; } response[nbytes_read - 1] = '\0'; DEBUGF(DBG_FOCUS, "RES (%s)", response); int isHomed; rc = sscanf(response, "%16[^=]=%d", key, &isHomed); if (rc != 2) return false; RotatorStatusL[STATUS_HOMED].s = isHomed ? IPS_OK : IPS_IDLE; IDSetLight(&RotatorStatusLP, nullptr); // Added By Philippe Besson the 28th of June for 'END' evalution // END is reached memset(response, 0, sizeof(response)); if (isSimulation()) { strncpy(response, "END\n", 16); nbytes_read = strlen(response); } else if ((errcode = tty_read_section(PortFD, response, 0xA, GEMINI_TIMEOUT, &nbytes_read)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); LOGF_ERROR("%s", errmsg); return false; } if (nbytes_read > 0) { response[nbytes_read - 1] = '\0'; // Display the response to be sure to have read the complet TTY Buffer. LOGF_DEBUG("RES (%s)", response); if (strcmp(response, "END")) { LOG_WARN("Invalid END response."); return false; } } // End of added code by Philippe Besson tcflush(PortFD, TCIFLUSH); return true; } /************************************************************************************ * * ***********************************************************************************/ bool Gemini::getRotatorConfig() { const char *cmd = ""; int errcode = 0; char errmsg[MAXRBUF]; char response[64]; int nbytes_read = 0; int nbytes_written = 0; char key[16]; memset(response, 0, sizeof(response)); LOGF_DEBUG("CMD (%s)", cmd); if (isSimulation()) { strncpy(response, "!00", sizeof(response)); nbytes_read = strlen(response) + 1; } else { tcflush(PortFD, TCIFLUSH); if ((errcode = tty_write(PortFD, cmd, strlen(cmd), &nbytes_written)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); LOGF_ERROR("%s", errmsg); return false; } if (isResponseOK() == false) return false; /*if ((errcode = tty_read_section(PortFD, response, 0xA, GEMINI_TIMEOUT, &nbytes_read)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); LOGF_ERROR("%s", errmsg); return false; }*/ } /*if (nbytes_read > 0) { response[nbytes_read - 1] = '\0'; LOGF_DEBUG("RES (%s)", response); if ((strcmp(response, "CONFIG1")) && (strcmp(response, "CONFIG2"))) return false; }*/ memset(response, 0, sizeof(response)); //////////////////////////////////////////////////////////// // Nickname //////////////////////////////////////////////////////////// if (isSimulation()) { strncpy(response, "NickName=Juli\n", sizeof(response)); nbytes_read = strlen(response); } else if ((errcode = tty_read_section(PortFD, response, 0xA, GEMINI_TIMEOUT, &nbytes_read)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); LOGF_ERROR("%s", errmsg); return false; } response[nbytes_read - 1] = '\0'; LOGF_DEBUG("RES (%s)", response); char nickname[16]; int rc = sscanf(response, "%16[^=]=%16[^\n]s", key, nickname); if (rc != 2) return false; IUSaveText(&HFocusNameT[DEVICE_ROTATOR], nickname); HFocusNameTP.s = IPS_OK; IDSetText(&HFocusNameTP, nullptr); memset(response, 0, sizeof(response)); //////////////////////////////////////////////////////////// // Get Max steps //////////////////////////////////////////////////////////// if (isSimulation()) { snprintf(response, sizeof(response), "MaxSteps = %06d\n", 100000); nbytes_read = strlen(response); } else if ((errcode = tty_read_section(PortFD, response, 0xA, GEMINI_TIMEOUT, &nbytes_read)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); LOGF_ERROR("%s", errmsg); return false; } response[nbytes_read - 1] = '\0'; LOGF_DEBUG("RES (%s)", response); uint32_t maxPos = 0; rc = sscanf(response, "%16[^=]=%d", key, &maxPos); if (rc == 2) { RotatorAbsPosN[0].min = 0; RotatorAbsPosN[0].max = maxPos; RotatorAbsPosN[0].step = maxPos/50.0; IUUpdateMinMax(&RotatorAbsPosNP); } else return false; memset(response, 0, sizeof(response)); //////////////////////////////////////////////////////////// // Get Device Type //////////////////////////////////////////////////////////// if (isSimulation()) { strncpy(response, "Dev Type = B\n", sizeof(response)); nbytes_read = strlen(response); } else if ((errcode = tty_read_section(PortFD, response, 0xA, GEMINI_TIMEOUT, &nbytes_read)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); LOGF_ERROR("%s", errmsg); return false; } response[nbytes_read - 1] = '\0'; LOGF_DEBUG("RES (%s)", response); // Get Status Parameters memset(response, 0, sizeof(response)); //////////////////////////////////////////////////////////// // Backlash Compensation //////////////////////////////////////////////////////////// memset(response, 0, sizeof(response)); if (isSimulation()) { snprintf(response, sizeof(response), "BLCSteps = %d\n", RotatorBacklashCompensationS[0].s == ISS_ON ? 1 : 0); nbytes_read = strlen(response); } else if ((errcode = tty_read_section(PortFD, response, 0xA, GEMINI_TIMEOUT, &nbytes_read)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); LOGF_ERROR("%s", errmsg); return false; } response[nbytes_read - 1] = '\0'; LOGF_DEBUG("RES (%s)", response); int BLCCompensate; rc = sscanf(response, "%16[^=]=%d", key, &BLCCompensate); if (rc != 2) return false; IUResetSwitch(&RotatorBacklashCompensationSP); RotatorBacklashCompensationS[0].s = BLCCompensate ? ISS_ON : ISS_OFF; RotatorBacklashCompensationS[1].s = BLCCompensate ? ISS_OFF : ISS_ON; RotatorBacklashCompensationSP.s = IPS_OK; IDSetSwitch(&RotatorBacklashCompensationSP, nullptr); //////////////////////////////////////////////////////////// // Backlash Value //////////////////////////////////////////////////////////// memset(response, 0, sizeof(response)); if (isSimulation()) { snprintf(response, sizeof(response), "BLCSteps = %d\n", 50); nbytes_read = strlen(response); } else if ((errcode = tty_read_section(PortFD, response, 0xA, GEMINI_TIMEOUT, &nbytes_read)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); LOGF_ERROR("%s", errmsg); return false; } response[nbytes_read - 1] = '\0'; LOGF_DEBUG("RES (%s)", response); int BLCValue; rc = sscanf(response, "%16[^=]=%d", key, &BLCValue); if (rc != 2) return false; RotatorBacklashN[0].value = BLCValue; RotatorBacklashNP.s = IPS_OK; IDSetNumber(&RotatorBacklashNP, nullptr); //////////////////////////////////////////////////////////// // Home on start on? //////////////////////////////////////////////////////////// memset(response, 0, sizeof(response)); if (isSimulation()) { snprintf(response, sizeof(response), "HOnStart = %d\n", RotatorHomeOnStartS[0].s == ISS_ON ? 1 : 0); nbytes_read = strlen(response); } else if ((errcode = tty_read_section(PortFD, response, 0xA, GEMINI_TIMEOUT, &nbytes_read)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); LOGF_ERROR("%s", errmsg); return false; } response[nbytes_read - 1] = '\0'; LOGF_DEBUG("RES (%s)", response); int StartOnHome; rc = sscanf(response, "%16[^=]=%d", key, &StartOnHome); if (rc != 2) return false; IUResetSwitch(&RotatorHomeOnStartSP); RotatorHomeOnStartS[0].s = StartOnHome ? ISS_ON : ISS_OFF; RotatorHomeOnStartS[1].s = StartOnHome ? ISS_OFF : ISS_ON; RotatorHomeOnStartSP.s = IPS_OK; IDSetSwitch(&RotatorHomeOnStartSP, nullptr); //////////////////////////////////////////////////////////// // Reverse? //////////////////////////////////////////////////////////// memset(response, 0, sizeof(response)); if (isSimulation()) { snprintf(response, 32, "Reverse = %d\n", (rotatorSimStatus[STATUS_REVERSE] == ISS_ON) ? 1 : 0); nbytes_read = strlen(response); } else if ((errcode = tty_read_section(PortFD, response, 0xA, GEMINI_TIMEOUT, &nbytes_read)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); LOGF_ERROR("%s", errmsg); return false; } response[nbytes_read - 1] = '\0'; DEBUGF(DBG_FOCUS, "RES (%s)", response); int reverse; rc = sscanf(response, "%16[^=]=%d", key, &reverse); if (rc != 2) return false; RotatorStatusL[STATUS_REVERSE].s = reverse ? IPS_OK : IPS_IDLE; // If reverse is enable and switch shows disabled, let's change that // same thing is reverse is disabled but switch is enabled if ((reverse && ReverseRotatorS[1].s == ISS_ON) || (!reverse && ReverseRotatorS[0].s == ISS_ON)) { IUResetSwitch(&ReverseRotatorSP); ReverseRotatorS[0].s = (reverse == 1) ? ISS_ON : ISS_OFF; ReverseRotatorS[1].s = (reverse == 0) ? ISS_ON : ISS_OFF; IDSetSwitch(&ReverseRotatorSP, nullptr); } RotatorStatusLP.s = IPS_OK; IDSetLight(&RotatorStatusLP, nullptr); //////////////////////////////////////////////////////////// // Max Speed - Not used //////////////////////////////////////////////////////////// memset(response, 0, sizeof(response)); if (isSimulation()) { snprintf(response, 32, "MaxSpeed = %d\n", 800); nbytes_read = strlen(response); } else if ((errcode = tty_read_section(PortFD, response, 0xA, GEMINI_TIMEOUT, &nbytes_read)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); LOGF_ERROR("%s", errmsg); return false; } response[nbytes_read - 1] = '\0'; DEBUGF(DBG_FOCUS, "RES (%s)", response); // Added By Philippe Besson the 28th of June for 'END' evalution // END is reached memset(response, 0, sizeof(response)); if (isSimulation()) { strncpy(response, "END\n", 16); nbytes_read = strlen(response); } else if ((errcode = tty_read_section(PortFD, response, 0xA, GEMINI_TIMEOUT, &nbytes_read)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); LOGF_ERROR("%s", errmsg); return false; } if (nbytes_read > 0) { response[nbytes_read - 1] = '\0'; // Display the response to be sure to have read the complet TTY Buffer. LOGF_DEBUG("RES (%s)", response); if (strcmp(response, "END")) return false; } // End of added code by Philippe Besson tcflush(PortFD, TCIFLUSH); rotatorConfigurationComplete = true; return true; } /************************************************************************************ * * ***********************************************************************************/ bool Gemini::getFocusStatus() { const char *cmd = ""; int errcode = 0; char errmsg[MAXRBUF]; char response[32]; int nbytes_read = 0; int nbytes_written = 0; char key[16]; memset(response, 0, sizeof(response)); LOGF_DEBUG("CMD (%s)", cmd); if (isSimulation()) { strncpy(response, "!00", 16); nbytes_read = strlen(response) + 1; } else { tcflush(PortFD, TCIFLUSH); if ((errcode = tty_write(PortFD, cmd, strlen(cmd), &nbytes_written)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); LOGF_ERROR("%s", errmsg); return false; } if (isResponseOK() == false) return false; /*if ((errcode = tty_read_section(PortFD, response, 0xA, GEMINI_TIMEOUT, &nbytes_read)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); LOGF_ERROR("%s", errmsg); return false; }*/ } // Get Temperature memset(response, 0, sizeof(response)); if (isSimulation()) { strncpy(response, "CurrTemp = +21.7\n", 16); nbytes_read = strlen(response); } else if ((errcode = tty_read_section(PortFD, response, 0xA, GEMINI_TIMEOUT, &nbytes_read)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); LOGF_ERROR("%s", errmsg); return false; } response[nbytes_read - 1] = '\0'; DEBUGF(DBG_FOCUS, "RES (%s)", response); float temperature = 0; int rc = sscanf(response, "%16[^=]=%f", key, &temperature); if (rc == 2) { TemperatureN[0].value = temperature; IDSetNumber(&TemperatureNP, nullptr); } else { char np[8]; int rc = sscanf(response, "%16[^=]= %s", key, np); if (rc != 2 || strcmp(np, "NP")) { if (TemperatureNP.s != IPS_ALERT) { TemperatureNP.s = IPS_ALERT; IDSetNumber(&TemperatureNP, nullptr); } return false; } } /////////////////////////////////////// // #1 Get Current Position /////////////////////////////////////// memset(response, 0, sizeof(response)); if (isSimulation()) { snprintf(response, 32, "CurrStep = %06d\n", focuserSimPosition); nbytes_read = strlen(response); } else if ((errcode = tty_read_section(PortFD, response, 0xA, GEMINI_TIMEOUT, &nbytes_read)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); LOGF_ERROR("%s", errmsg); return false; } response[nbytes_read - 1] = '\0'; DEBUGF(DBG_FOCUS, "RES (%s)", response); uint32_t currPos = 0; rc = sscanf(response, "%16[^=]=%d", key, &currPos); if (rc == 2) { FocusAbsPosN[0].value = currPos; IDSetNumber(&FocusAbsPosNP, nullptr); } else return false; /////////////////////////////////////// // #2 Get Target Position /////////////////////////////////////// memset(response, 0, sizeof(response)); if (isSimulation()) { snprintf(response, 32, "TargStep = %06d\n", targetFocuserPosition); nbytes_read = strlen(response); } else if ((errcode = tty_read_section(PortFD, response, 0xA, GEMINI_TIMEOUT, &nbytes_read)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); LOGF_ERROR("%s", errmsg); return false; } response[nbytes_read - 1] = '\0'; DEBUGF(DBG_FOCUS, "RES (%s)", response); // Get Status Parameters /////////////////////////////////////// // #3 is Moving? /////////////////////////////////////// memset(response, 0, sizeof(response)); if (isSimulation()) { snprintf(response, 32, "IsMoving = %d\n", (focuserSimStatus[STATUS_MOVING] == ISS_ON) ? 1 : 0); nbytes_read = strlen(response); } else if ((errcode = tty_read_section(PortFD, response, 0xA, GEMINI_TIMEOUT, &nbytes_read)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); LOGF_ERROR("%s", errmsg); return false; } response[nbytes_read - 1] = '\0'; DEBUGF(DBG_FOCUS, "RES (%s)", response); int isMoving; rc = sscanf(response, "%16[^=]=%d", key, &isMoving); if (rc != 2) return false; FocuserStatusL[STATUS_MOVING].s = isMoving ? IPS_BUSY : IPS_IDLE; /////////////////////////////////////// // #4 is Homing? /////////////////////////////////////// memset(response, 0, sizeof(response)); if (isSimulation()) { snprintf(response, 32, "IsHoming = %d\n", (focuserSimStatus[STATUS_HOMING] == ISS_ON) ? 1 : 0); nbytes_read = strlen(response); } else if ((errcode = tty_read_section(PortFD, response, 0xA, GEMINI_TIMEOUT, &nbytes_read)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); LOGF_ERROR("%s", errmsg); return false; } response[nbytes_read - 1] = '\0'; DEBUGF(DBG_FOCUS, "RES (%s)", response); int _isHoming; rc = sscanf(response, "%16[^=]=%d", key, &_isHoming); if (rc != 2) return false; FocuserStatusL[STATUS_HOMING].s = _isHoming ? IPS_BUSY : IPS_IDLE; // For relative focusers home is not applicable. if (isFocuserAbsolute == false) FocuserStatusL[STATUS_HOMING].s = IPS_IDLE; // We set that isHoming in process, but we don't set it to false here it must be reset in TimerHit if (FocuserStatusL[STATUS_HOMING].s == IPS_BUSY) isFocuserHoming = true; /////////////////////////////////////// // #6 is Homed? /////////////////////////////////////// memset(response, 0, sizeof(response)); if (isSimulation()) { snprintf(response, 32, "IsHomed = %d\n", (focuserSimStatus[STATUS_HOMED] == ISS_ON) ? 1 : 0); nbytes_read = strlen(response); } else if ((errcode = tty_read_section(PortFD, response, 0xA, GEMINI_TIMEOUT, &nbytes_read)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); LOGF_ERROR("%s", errmsg); return false; } response[nbytes_read - 1] = '\0'; DEBUGF(DBG_FOCUS, "RES (%s)", response); int isHomed; rc = sscanf(response, "%16[^=]=%d", key, &isHomed); if (rc != 2) return false; FocuserStatusL[STATUS_HOMED].s = isHomed ? IPS_OK : IPS_IDLE; // For relative focusers home is not applicable. if (isFocuserAbsolute == false) FocuserStatusL[STATUS_HOMED].s = IPS_IDLE; /////////////////////////////////////// // #7 Temperature probe? /////////////////////////////////////// memset(response, 0, sizeof(response)); if (isSimulation()) { snprintf(response, 32, "TempProb = %d\n", (focuserSimStatus[STATUS_TMPPROBE] == ISS_ON) ? 1 : 0); nbytes_read = strlen(response); } else if ((errcode = tty_read_section(PortFD, response, 0xA, GEMINI_TIMEOUT, &nbytes_read)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); LOGF_ERROR("%s", errmsg); return false; } response[nbytes_read - 1] = '\0'; DEBUGF(DBG_FOCUS, "RES (%s)", response); int TmpProbe; rc = sscanf(response, "%16[^=]=%d", key, &TmpProbe); if (rc != 2) return false; FocuserStatusL[STATUS_TMPPROBE].s = TmpProbe ? IPS_OK : IPS_IDLE; /////////////////////////////////////// // #8 Remote IO? /////////////////////////////////////// memset(response, 0, sizeof(response)); if (isSimulation()) { snprintf(response, 32, "RemoteIO = %d\n", (focuserSimStatus[STATUS_REMOTEIO] == ISS_ON) ? 1 : 0); nbytes_read = strlen(response); } else if ((errcode = tty_read_section(PortFD, response, 0xA, GEMINI_TIMEOUT, &nbytes_read)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); LOGF_ERROR("%s", errmsg); return false; } response[nbytes_read - 1] = '\0'; DEBUGF(DBG_FOCUS, "RES (%s)", response); int RemoteIO; rc = sscanf(response, "%16[^=]=%d", key, &RemoteIO); if (rc != 2) return false; FocuserStatusL[STATUS_REMOTEIO].s = RemoteIO ? IPS_OK : IPS_IDLE; /////////////////////////////////////// // #9 Hand controller? /////////////////////////////////////// memset(response, 0, sizeof(response)); if (isSimulation()) { snprintf(response, 32, "HCStatus = %d\n", (focuserSimStatus[STATUS_HNDCTRL] == ISS_ON) ? 1 : 0); nbytes_read = strlen(response); } else if ((errcode = tty_read_section(PortFD, response, 0xA, GEMINI_TIMEOUT, &nbytes_read)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); LOGF_ERROR("%s", errmsg); return false; } response[nbytes_read - 1] = '\0'; DEBUGF(DBG_FOCUS, "RES (%s)", response); int HndCtlr; rc = sscanf(response, "%16[^=]=%d", key, &HndCtlr); if (rc != 2) return false; FocuserStatusL[STATUS_HNDCTRL].s = HndCtlr ? IPS_OK : IPS_IDLE; FocuserStatusLP.s = IPS_OK; IDSetLight(&FocuserStatusLP, nullptr); // Added By Philippe Besson the 28th of June for 'END' evalution // END is reached memset(response, 0, sizeof(response)); if (isSimulation()) { strncpy(response, "END\n", 16); nbytes_read = strlen(response); } else if ((errcode = tty_read_section(PortFD, response, 0xA, GEMINI_TIMEOUT, &nbytes_read)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); LOGF_ERROR("%s", errmsg); return false; } if (nbytes_read > 0) { response[nbytes_read - 1] = '\0'; // Display the response to be sure to have read the complet TTY Buffer. LOGF_DEBUG("RES (%s)", response); if (strcmp(response, "END")) { LOG_WARN("Invalid END response."); return false; } } // End of added code by Philippe Besson tcflush(PortFD, TCIFLUSH); return true; } /************************************************************************************ * * ***********************************************************************************/ bool Gemini::setLedLevel(int level) // Write via the serial port to the HUB the selected LED intensity level { char cmd[16]; int errcode = 0; char errmsg[MAXRBUF]; char response[16]; int nbytes_read = 0; int nbytes_written = 0; memset(response, 0, sizeof(response)); snprintf(cmd, 16, "", level); LOGF_DEBUG("CMD (%s)", cmd); if (isSimulation()) { strncpy(response, "SET", 16); nbytes_read = strlen(response) + 1; } else { tcflush(PortFD, TCIFLUSH); if ((errcode = tty_write(PortFD, cmd, strlen(cmd), &nbytes_written)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); LOGF_ERROR("%s", errmsg); return false; } if (isResponseOK() == false) return false; if ((errcode = tty_read_section(PortFD, response, 0xA, GEMINI_TIMEOUT, &nbytes_read)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); LOGF_ERROR("%s", errmsg); return false; } } if (nbytes_read > 0) { response[nbytes_read - 1] = '\0'; LOGF_DEBUG("RES (%s)", response); tcflush(PortFD, TCIFLUSH); if (!strcmp(response, "SET")) return true; else return false; } return false; } /************************************************************************************ * * ***********************************************************************************/ bool Gemini::setNickname(DeviceType type, const char *nickname) // Write via the serial port to the HUB the choiced nikname of he focuser { char cmd[32]; int errcode = 0; char errmsg[MAXRBUF]; char response[16]; int nbytes_read=0; int nbytes_written = 0; memset(response, 0, sizeof(response)); snprintf(cmd, 32, "<%c100SETDNN%s>", (type == DEVICE_FOCUSER ? 'F' : 'R'), nickname); LOGF_DEBUG("CMD (%s)", cmd); if (!isSimulation()) { tcflush(PortFD, TCIFLUSH); if ((errcode = tty_write(PortFD, cmd, strlen(cmd), &nbytes_written)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); LOGF_ERROR("%s", errmsg); return false; } if (isResponseOK() == false) return false; // Read the 'END' tty_read_section(PortFD, response, 0xA, GEMINI_TIMEOUT, &nbytes_read); } tcflush(PortFD, TCIFLUSH); return true; } /************************************************************************************ * * ***********************************************************************************/ bool Gemini::halt(DeviceType type) { char cmd[32];; int errcode = 0; char errmsg[MAXRBUF]; char response[16]; int nbytes_read=0; int nbytes_written = 0; memset(response, 0, sizeof(response)); snprintf(cmd, 32, "<%c100DOHALT>", (type == DEVICE_FOCUSER ? 'F' : 'R')); LOGF_DEBUG("CMD (%s)", cmd); if (isSimulation()) { if (type == DEVICE_FOCUSER) focuserSimStatus[STATUS_MOVING] = ISS_OFF; else { rotatorSimStatus[STATUS_MOVING] = ISS_OFF; isRotatorHoming = false; } } else { tcflush(PortFD, TCIFLUSH); if ((errcode = tty_write(PortFD, cmd, strlen(cmd), &nbytes_written)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); LOGF_ERROR("%s", errmsg); return false; } if (isResponseOK() == false) return false; // Read the 'END' tty_read_section(PortFD, response, 0xA, GEMINI_TIMEOUT, &nbytes_read); } isRotatorHoming = false; tcflush(PortFD, TCIFLUSH); return true; } /************************************************************************************ * * ***********************************************************************************/ bool Gemini::home(DeviceType type) { char cmd[32];; int errcode = 0; char errmsg[MAXRBUF]; char response[16]; int nbytes_read=0; int nbytes_written = 0; memset(response, 0, sizeof(response)); snprintf(cmd, 32, "<%c100DOHOME>", (type == DEVICE_FOCUSER ? 'F' : 'R')); LOGF_DEBUG("CMD (%s)", cmd); if (isSimulation()) { if (type == DEVICE_FOCUSER) { focuserSimStatus[STATUS_HOMING] = ISS_ON; targetFocuserPosition = 0; } else { rotatorSimStatus[STATUS_HOMING] = ISS_ON; targetRotatorPosition = 0; } } else { tcflush(PortFD, TCIFLUSH); if ((errcode = tty_write(PortFD, cmd, strlen(cmd), &nbytes_written)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); LOGF_ERROR("%s", errmsg); return false; } if (isResponseOK() == false) return false; // Read the 'END' tty_read_section(PortFD, response, 0xA, GEMINI_TIMEOUT, &nbytes_read); } tcflush(PortFD, TCIFLUSH); return true; } /************************************************************************************ * * ***********************************************************************************/ bool Gemini::homeOnStart(DeviceType type, bool enable) { char cmd[32];; int errcode = 0; char errmsg[MAXRBUF]; char response[16]; int nbytes_read=0; int nbytes_written = 0; memset(response, 0, sizeof(response)); snprintf(cmd, 32, "<%c100SETHOS%d>", (type == DEVICE_FOCUSER ? 'F' : 'R'), enable ? 1 : 0); LOGF_DEBUG("CMD (%s)", cmd); if (isSimulation() == false) { tcflush(PortFD, TCIFLUSH); if ((errcode = tty_write(PortFD, cmd, strlen(cmd), &nbytes_written)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); LOGF_ERROR("%s", errmsg); return false; } if (isResponseOK() == false) return false; // Read the 'END' tty_read_section(PortFD, response, 0xA, GEMINI_TIMEOUT, &nbytes_read); } tcflush(PortFD, TCIFLUSH); return true; } /************************************************************************************ * * ***********************************************************************************/ bool Gemini::center(DeviceType type) { if (type == DEVICE_ROTATOR) return MoveAbsRotatorTicks(RotatorAbsPosN[0].max/2); const char * cmd = ""; int errcode = 0; char errmsg[MAXRBUF]; char response[16]; int nbytes_read=0; int nbytes_written = 0; memset(response, 0, sizeof(response)); LOGF_DEBUG("CMD (%s)", cmd); if (isSimulation()) { if (type == DEVICE_FOCUSER) { focuserSimStatus[STATUS_MOVING] = ISS_ON; targetFocuserPosition = FocusAbsPosN[0].max / 2; } else { rotatorSimStatus[STATUS_MOVING] = ISS_ON; targetRotatorPosition = RotatorAbsPosN[0].max / 2; } } else { tcflush(PortFD, TCIFLUSH); if ((errcode = tty_write(PortFD, cmd, strlen(cmd), &nbytes_written)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); LOGF_ERROR("%s", errmsg); return false; } if (isResponseOK() == false) return false; // Read the 'END' tty_read_section(PortFD, response, 0xA, GEMINI_TIMEOUT, &nbytes_read); } tcflush(PortFD, TCIFLUSH); return true; } /************************************************************************************ * * ***********************************************************************************/ bool Gemini::setTemperatureCompensation(bool enable) { char cmd[16]; int errcode = 0; char errmsg[MAXRBUF]; char response[16]; int nbytes_read=0; int nbytes_written = 0; memset(response, 0, sizeof(response)); snprintf(cmd, 16, "", enable ? 1 : 0); LOGF_DEBUG("CMD (%s)", cmd); if (isSimulation() == false) { tcflush(PortFD, TCIFLUSH); if ((errcode = tty_write(PortFD, cmd, strlen(cmd), &nbytes_written)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); LOGF_ERROR("%s", errmsg); return false; } if (isResponseOK() == false) return false; // Read the 'END' tty_read_section(PortFD, response, 0xA, GEMINI_TIMEOUT, &nbytes_read); } return true; } /************************************************************************************ * * ***********************************************************************************/ bool Gemini::setTemperatureCompensationMode(char mode) { char cmd[16]; int errcode = 0; char errmsg[MAXRBUF]; char response[16]; int nbytes_read=0; int nbytes_written = 0; memset(response, 0, sizeof(response)); snprintf(cmd, 16, "", mode); LOGF_DEBUG("CMD (%s)", cmd); if (isSimulation() == false) { tcflush(PortFD, TCIFLUSH); if ((errcode = tty_write(PortFD, cmd, strlen(cmd), &nbytes_written)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); LOGF_ERROR("%s", errmsg); return false; } if (isResponseOK() == false) return false; // Read the 'END' tty_read_section(PortFD, response, 0xA, GEMINI_TIMEOUT, &nbytes_read); } return true; } /************************************************************************************ * * ***********************************************************************************/ bool Gemini::setTemperatureCompensationCoeff(char mode, int16_t coeff) { char cmd[32]; int errcode = 0; char errmsg[MAXRBUF]; char response[16]; int nbytes_read=0; int nbytes_written = 0; memset(response, 0, sizeof(response)); snprintf(cmd, 32, "", mode, coeff >= 0 ? '+' : '-', (int)std::abs(coeff)); LOGF_DEBUG("CMD (%s)", cmd); if (isSimulation() == false) { tcflush(PortFD, TCIFLUSH); if ((errcode = tty_write(PortFD, cmd, strlen(cmd), &nbytes_written)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); LOGF_ERROR("%s", errmsg); return false; } if (isResponseOK() == false) return false; // Read the 'END' tty_read_section(PortFD, response, 0xA, GEMINI_TIMEOUT, &nbytes_read); } return true; } /************************************************************************************ * * ***********************************************************************************/ bool Gemini::setTemperatureCompensationOnStart(bool enable) { char cmd[16]; int errcode = 0; char errmsg[MAXRBUF]; char response[16]; int nbytes_read=0; int nbytes_written = 0; memset(response, 0, sizeof(response)); snprintf(cmd, 16, "", enable ? 1 : 0); LOGF_DEBUG("CMD (%s)", cmd); tcflush(PortFD, TCIFLUSH); if (isSimulation() == false) { if ((errcode = tty_write(PortFD, cmd, strlen(cmd), &nbytes_written)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); LOGF_ERROR("%s", errmsg); return false; } if (isResponseOK() == false) return false; // Read the 'END' tty_read_section(PortFD, response, 0xA, GEMINI_TIMEOUT, &nbytes_read); } return true; } /************************************************************************************ * * ***********************************************************************************/ bool Gemini::setBacklashCompensation(DeviceType type, bool enable) { char cmd[16]; int errcode = 0; char errmsg[MAXRBUF]; char response[16]; int nbytes_read=0; int nbytes_written = 0; memset(response, 0, sizeof(response)); snprintf(cmd, 16, "<%c100SETBCE%d>", (type == DEVICE_FOCUSER ? 'F' : 'R'), enable ? 1 : 0); LOGF_DEBUG("CMD (%s)", cmd); if (isSimulation() == false) { if ((errcode = tty_write(PortFD, cmd, strlen(cmd), &nbytes_written)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); LOGF_ERROR("%s", errmsg); return false; } if (isResponseOK() == false) return false; // Read the 'END' tty_read_section(PortFD, response, 0xA, GEMINI_TIMEOUT, &nbytes_read); } return true; } /************************************************************************************ * * ***********************************************************************************/ bool Gemini::setBacklashCompensationSteps(DeviceType type, uint16_t steps) { char cmd[16]; int errcode = 0; char errmsg[MAXRBUF]; char response[16]; int nbytes_read=0; int nbytes_written = 0; memset(response, 0, sizeof(response)); snprintf(cmd, 16, "<%c100SETBCS%02d>", (type == DEVICE_FOCUSER ? 'F' : 'R'), steps); LOGF_DEBUG("CMD (%s)", cmd); if (isSimulation() == false) { if ((errcode = tty_write(PortFD, cmd, strlen(cmd), &nbytes_written)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); LOGF_ERROR("%s", errmsg); return false; } if (isResponseOK() == false) return false; // Read the 'END' tty_read_section(PortFD, response, 0xA, GEMINI_TIMEOUT, &nbytes_read); } return true; } /************************************************************************************ * * ***********************************************************************************/ bool Gemini::reverseRotator(bool enable) { char cmd[16]; int errcode = 0; char errmsg[MAXRBUF]; char response[16]; int nbytes_read = 0; int nbytes_written = 0; memset(response, 0, sizeof(response)); snprintf(cmd, 16, "", enable ? 1 : 0); LOGF_DEBUG("CMD (%s)", cmd); if (isSimulation() == false) { if ((errcode = tty_write(PortFD, cmd, strlen(cmd), &nbytes_written)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); LOGF_ERROR("%s", errmsg); return false; } if (isResponseOK() == false) return false; // Read the 'END' tty_read_section(PortFD, response, 0xA, GEMINI_TIMEOUT, &nbytes_read); } return true; } /************************************************************************************ * * ***********************************************************************************/ bool Gemini::resetFactory() { const char *cmd = ""; int errcode = 0; char errmsg[MAXRBUF]; char response[16]; int nbytes_read = 0; int nbytes_written = 0; memset(response, 0, sizeof(response)); LOGF_DEBUG("CMD (%s)", cmd); if (isSimulation()) { strncpy(response, "SET", 16); nbytes_read = strlen(response) + 1; } else { tcflush(PortFD, TCIFLUSH); if ((errcode = tty_write(PortFD, cmd, strlen(cmd), &nbytes_written)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); LOGF_ERROR("%s", errmsg); return false; } if (isResponseOK() == false) return false; if ((errcode = tty_read_section(PortFD, response, 0xA, GEMINI_TIMEOUT, &nbytes_read)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); LOGF_ERROR("%s", errmsg); return false; } } if (nbytes_read > 0) { response[nbytes_read - 1] = '\0'; LOGF_DEBUG("RES (%s)", response); tcflush(PortFD, TCIFLUSH); if (!strcmp(response, "SET")) { return true; getFocusConfig(); getRotatorConfig(); } else return false; } return false; } /************************************************************************************ * * ***********************************************************************************/ bool Gemini::isResponseOK() { int errcode = 0; char errmsg[MAXRBUF]; char response[32]; int nbytes_read = 0; memset(response, 0, sizeof(response)); if (isSimulation()) { strcpy(response, "!00"); nbytes_read = strlen(response) + 1; } else { if ((errcode = tty_read_section(PortFD, response, 0xA, GEMINI_TIMEOUT, &nbytes_read)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); LOGF_ERROR("TTY error: %s", errmsg); return false; } } if (nbytes_read > 0) { response[nbytes_read - 1] = '\0'; LOGF_DEBUG("RES (%s)", response); if (!strcmp(response, "!00")) return true; else { memset(response, 0, sizeof(response)); while (strstr(response, "END") == nullptr) { if ((errcode = tty_read_section(PortFD, response, 0xA, GEMINI_TIMEOUT, &nbytes_read)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); LOGF_ERROR("TTY error: %s", errmsg); return false; } response[nbytes_read - 1] = '\0'; LOGF_ERROR("Controller error: %s", response); } return false; } } return true; } /************************************************************************************ * * ***********************************************************************************/ IPState Gemini::MoveFocuser(FocusDirection dir, int speed, uint16_t duration) { char cmd[16]; int errcode = 0; char errmsg[MAXRBUF]; char response[16]; int nbytes_written = 0; INDI_UNUSED(speed); memset(response, 0, sizeof(response)); snprintf(cmd, 16, "", (dir == FOCUS_INWARD) ? '0' : '1'); LOGF_DEBUG("CMD (%s)", cmd); if (!isSimulation()) { tcflush(PortFD, TCIFLUSH); if ((errcode = tty_write(PortFD, cmd, strlen(cmd), &nbytes_written)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); LOGF_ERROR("%s", errmsg); return IPS_ALERT; } if (isResponseOK() == false) return IPS_ALERT; gettimeofday(&focusMoveStart, nullptr); focusMoveRequest = duration / 1000.0; } if (duration <= POLLMS) { usleep(POLLMS * 1000); AbortFocuser(); return IPS_OK; } tcflush(PortFD, TCIFLUSH); return IPS_BUSY; } /************************************************************************************ * * ***********************************************************************************/ IPState Gemini::MoveAbsFocuser(uint32_t targetTicks) { char cmd[32]; int errcode = 0; char errmsg[MAXRBUF]; char response[16]; int nbytes_written = 0; targetFocuserPosition = targetTicks; memset(response, 0, sizeof(response)); snprintf(cmd, 32, "", targetTicks); LOGF_DEBUG("CMD (%s)", cmd); if (!isSimulation()) { tcflush(PortFD, TCIFLUSH); if ((errcode = tty_write(PortFD, cmd, strlen(cmd), &nbytes_written)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); LOGF_ERROR("%s", errmsg); return IPS_ALERT; } if (isResponseOK() == false) return IPS_ALERT; } FocusAbsPosNP.s = IPS_BUSY; tcflush(PortFD, TCIFLUSH); return IPS_BUSY; } /************************************************************************************ * * ***********************************************************************************/ IPState Gemini::MoveRelFocuser(FocusDirection dir, uint32_t ticks) { uint32_t newPosition = 0; if (dir == FOCUS_INWARD) newPosition = FocusAbsPosN[0].value - ticks; else newPosition = FocusAbsPosN[0].value + ticks; return MoveAbsFocuser(newPosition); } /************************************************************************************ * * ***********************************************************************************/ void Gemini::TimerHit() { if (!isConnected()) return; if (focuserConfigurationComplete == false || rotatorConfigurationComplete == false) { SetTimer(POLLMS); return; } // Focuser Status bool statusrc = false; for (int i = 0; i < 2; i++) { statusrc = getFocusStatus(); if (statusrc) break; } if (statusrc == false) { LOG_WARN("Unable to read focuser status...."); SetTimer(POLLMS); return; } if (FocusAbsPosNP.s == IPS_BUSY || FocusRelPosNP.s == IPS_BUSY) { if (isSimulation()) { if (FocusAbsPosN[0].value < targetFocuserPosition) focuserSimPosition += 100; else focuserSimPosition -= 100; focuserSimStatus[STATUS_MOVING] = ISS_ON; if (std::abs((int64_t)focuserSimPosition - (int64_t)targetFocuserPosition) < 100) { FocusAbsPosN[0].value = targetFocuserPosition; focuserSimPosition = FocusAbsPosN[0].value; focuserSimStatus[STATUS_MOVING] = ISS_OFF; FocuserStatusL[STATUS_MOVING].s = IPS_IDLE; if (focuserSimStatus[STATUS_HOMING] == ISS_ON) { FocuserStatusL[STATUS_HOMED].s = IPS_OK; focuserSimStatus[STATUS_HOMING] = ISS_OFF; } } } if (isFocuserHoming && FocuserStatusL[STATUS_HOMED].s == IPS_OK) { isFocuserHoming = false; FocuserGotoSP.s = IPS_OK; IUResetSwitch(&FocuserGotoSP); FocuserGotoS[GOTO_HOME].s = ISS_ON; IDSetSwitch(&FocuserGotoSP, nullptr); FocusAbsPosNP.s = IPS_OK; IDSetNumber(&FocusRelPosNP, nullptr); LOG_INFO("Focuser reached home position."); } else if (FocuserStatusL[STATUS_MOVING].s == IPS_IDLE) { FocusAbsPosNP.s = IPS_OK; FocusRelPosNP.s = IPS_OK; IDSetNumber(&FocusAbsPosNP, nullptr); IDSetNumber(&FocusRelPosNP, nullptr); if (FocuserGotoSP.s == IPS_BUSY) { IUResetSwitch(&FocuserGotoSP); FocuserGotoSP.s = IPS_OK; IDSetSwitch(&FocuserGotoSP, nullptr); } LOG_INFO("Focuser reached requested position."); } } if (FocuserStatusL[STATUS_HOMING].s == IPS_BUSY && FocuserGotoSP.s != IPS_BUSY) { FocuserGotoSP.s = IPS_BUSY; IDSetSwitch(&FocuserGotoSP, nullptr); } // Rotator Status statusrc = false; for (int i = 0; i < 2; i++) { statusrc = getRotatorStatus(); if (statusrc) break; } if (statusrc == false) { LOG_WARN("Unable to read rotator status...."); SetTimer(POLLMS); return; } if (RotatorAbsPosNP.s == IPS_BUSY || GotoRotatorNP.s == IPS_BUSY) { /*if (isSimulation()) { if (RotatorAbsPosN[0].value < targetRotatorPosition) RotatorSimPosition += 100; else RotatorSimPosition -= 100; RotatorSimStatus[STATUS_MOVING] = ISS_ON; if (std::abs((int64_t)RotatorSimPosition - (int64_t)targetRotatorPosition) < 100) { RotatorAbsPosN[0].value = targetRotatorPosition; RotatorSimPosition = RotatorAbsPosN[0].value; RotatorSimStatus[STATUS_MOVING] = ISS_OFF; RotatorStatusL[STATUS_MOVING].s = IPS_IDLE; if (RotatorSimStatus[STATUS_HOMING] == ISS_ON) { RotatorStatusL[STATUS_HOMED].s = IPS_OK; RotatorSimStatus[STATUS_HOMING] = ISS_OFF; } } }*/ if (isRotatorHoming && RotatorStatusL[STATUS_HOMED].s == IPS_OK) { isRotatorHoming = false; HomeRotatorSP.s = IPS_OK; IUResetSwitch(&HomeRotatorSP); IDSetSwitch(&HomeRotatorSP, nullptr); RotatorAbsPosNP.s = IPS_OK; IDSetNumber(&RotatorAbsPosNP, nullptr); GotoRotatorNP.s = IPS_OK; IDSetNumber(&GotoRotatorNP, nullptr); LOG_INFO("Rotator reached home position."); } else if (RotatorStatusL[STATUS_MOVING].s == IPS_IDLE) { RotatorAbsPosNP.s = IPS_OK; IDSetNumber(&RotatorAbsPosNP, nullptr); GotoRotatorNP.s = IPS_OK; IDSetNumber(&GotoRotatorNP, nullptr); if (HomeRotatorSP.s == IPS_BUSY) { IUResetSwitch(&HomeRotatorSP); HomeRotatorSP.s = IPS_OK; IDSetSwitch(&HomeRotatorSP, nullptr); } LOG_INFO("Rotator reached requested position."); } } if (RotatorStatusL[STATUS_HOMING].s == IPS_BUSY && HomeRotatorSP.s != IPS_BUSY) { HomeRotatorSP.s = IPS_BUSY; IDSetSwitch(&HomeRotatorSP, nullptr); } SetTimer(POLLMS); } /************************************************************************************ * * ***********************************************************************************/ bool Gemini::AbortFocuser() { const char *cmd = ""; int errcode = 0; char errmsg[MAXRBUF]; char response[16]; int nbytes_read = 0; int nbytes_written = 0; memset(response, 0, sizeof(response)); LOGF_DEBUG("CMD (%s)", cmd); if (isSimulation()) { strncpy(response, "!00", 16); nbytes_read = strlen(response) + 1; focuserSimStatus[STATUS_MOVING] = ISS_OFF; focuserSimStatus[STATUS_HOMING] = ISS_OFF; } else { tcflush(PortFD, TCIFLUSH); if ((errcode = tty_write(PortFD, cmd, strlen(cmd), &nbytes_written)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); LOGF_ERROR("%s", errmsg); return false; } if (isResponseOK() == false) return false; } if (FocusRelPosNP.s == IPS_BUSY) { FocusRelPosNP.s = IPS_IDLE; IDSetNumber(&FocusRelPosNP, nullptr); } FocusTimerNP.s = FocusAbsPosNP.s = FocuserGotoSP.s = IPS_IDLE; IUResetSwitch(&FocuserGotoSP); IDSetNumber(&FocusAbsPosNP, nullptr); IDSetSwitch(&FocuserGotoSP, nullptr); tcflush(PortFD, TCIFLUSH); return true; } /************************************************************************************ * * ***********************************************************************************/ float Gemini::calcTimeLeft(timeval start, float req) { double timesince; double timeleft; struct timeval now { 0, 0 }; gettimeofday(&now, nullptr); timesince = (double)(now.tv_sec * 1000.0 + now.tv_usec / 1000) - (double)(start.tv_sec * 1000.0 + start.tv_usec / 1000); timesince = timesince / 1000; timeleft = req - timesince; return timeleft; } /************************************************************************************ * * ***********************************************************************************/ IPState Gemini::MoveAbsRotatorTicks(uint32_t targetTicks) { char cmd[32]; int errcode = 0; char errmsg[MAXRBUF]; char response[16]; int nbytes_written = 0; targetRotatorPosition = targetTicks; memset(response, 0, sizeof(response)); snprintf(cmd, 32, "", targetTicks); LOGF_DEBUG("CMD (%s)", cmd); if (!isSimulation()) { tcflush(PortFD, TCIFLUSH); if ((errcode = tty_write(PortFD, cmd, strlen(cmd), &nbytes_written)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); LOGF_ERROR("%s", errmsg); return IPS_ALERT; } if (isResponseOK() == false) return IPS_ALERT; } RotatorAbsPosNP.s = IPS_BUSY; tcflush(PortFD, TCIFLUSH); return IPS_BUSY; } /************************************************************************************ * * ***********************************************************************************/ IPState Gemini::MoveAbsRotatorAngle(double angle) { char cmd[32]; int errcode = 0; char errmsg[MAXRBUF]; char response[16]; int nbytes_written = 0; targetRotatorAngle = angle * 1000.0; memset(response, 0, sizeof(response)); snprintf(cmd, 32, "", targetRotatorAngle); LOGF_DEBUG("CMD (%s)", cmd); if (!isSimulation()) { tcflush(PortFD, TCIFLUSH); if ((errcode = tty_write(PortFD, cmd, strlen(cmd), &nbytes_written)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); LOGF_ERROR("%s", errmsg); return IPS_ALERT; } if (isResponseOK() == false) return IPS_ALERT; } GotoRotatorNP.s = IPS_BUSY; tcflush(PortFD, TCIFLUSH); return IPS_BUSY; } /************************************************************************************ * * ***********************************************************************************/ bool Gemini::saveConfigItems(FILE *fp) { INDI::Focuser::saveConfigItems(fp); IUSaveConfigSwitch(fp, &TemperatureCompensateSP); IUSaveConfigSwitch(fp, &TemperatureCompensateOnStartSP); IUSaveConfigNumber(fp, &TemperatureCoeffNP); IUSaveConfigSwitch(fp, &TemperatureCompensateModeSP); IUSaveConfigSwitch(fp, &FocuserBacklashCompensationSP); IUSaveConfigSwitch(fp, &FocuserHomeOnStartSP); IUSaveConfigNumber(fp, &FocuserBacklashNP); IUSaveConfigSwitch(fp, &ReverseRotatorSP); IUSaveConfigSwitch(fp, &RotatorBacklashCompensationSP); IUSaveConfigNumber(fp, &RotatorBacklashNP); IUSaveConfigSwitch(fp, &RotatorHomeOnStartSP); return true; } /************************************************************************************ * * ***********************************************************************************/ IPState Gemini::MoveRotator(double angle) { IPState state = MoveAbsRotatorAngle(angle); RotatorAbsPosNP.s = state; IDSetNumber(&RotatorAbsPosNP, nullptr); return state; } /************************************************************************************ * * ***********************************************************************************/ IPState Gemini::HomeRotator() { return (home(DEVICE_ROTATOR) ? IPS_BUSY : IPS_ALERT); } /************************************************************************************ * * ***********************************************************************************/ bool Gemini::ReverseRotator(bool enabled) { return reverseRotator(enabled); } libindi/drivers/rotator/nightcrawler.cpp0000664000175000017500000011644113263645557020046 0ustar jasemjasem/* NightCrawler NightCrawler Focuser & Rotator Copyright (C) 2017 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 */ #include "nightcrawler.h" #include "indicom.h" #include "connectionplugins/connectionserial.h" #include #include #include #include #define NIGHTCRAWLER_TIMEOUT 3 #define NIGHTCRAWLER_THRESHOLD 0.1 #define NC_25_STEPS 374920 #define NC_30_STEPS 444080 #define NC_35_STEPS 505960 #define ROTATOR_TAB "Rotator" #define AUX_TAB "Aux" #define SETTINGS_TAB "Settings" // Well, it is time I name something, even if simple, after Tommy, my loyal German Shephard companion. // By the time of writing this, he is almost 4 years old. Live long and prosper, my good boy! std::unique_ptr tommyGoodBoy(new NightCrawler()); void ISGetProperties(const char *dev) { tommyGoodBoy->ISGetProperties(dev); } void ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) { tommyGoodBoy->ISNewSwitch(dev, name, states, names, n); } void ISNewText( const char *dev, const char *name, char *texts[], char *names[], int n) { tommyGoodBoy->ISNewText(dev, name, texts, names, n); } void ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) { tommyGoodBoy->ISNewNumber(dev, name, values, names, n); } void ISNewBLOB (const char *dev, const char *name, int sizes[], int blobsizes[], char *blobs[], char *formats[], char *names[], int n) { INDI_UNUSED(dev); INDI_UNUSED(name); INDI_UNUSED(sizes); INDI_UNUSED(blobsizes); INDI_UNUSED(blobs); INDI_UNUSED(formats); INDI_UNUSED(names); INDI_UNUSED(n); } void ISSnoopDevice (XMLEle *root) { tommyGoodBoy->ISSnoopDevice(root); } NightCrawler::NightCrawler() : RotatorInterface(this) { // Can move in Absolute & Relative motions, can AbortFocuser motion, and has variable speed. FI::SetCapability(FOCUSER_CAN_ABS_MOVE | FOCUSER_CAN_REL_MOVE | FOCUSER_CAN_ABORT); RI::SetCapability(ROTATOR_CAN_ABORT | ROTATOR_CAN_HOME | ROTATOR_CAN_SYNC); } bool NightCrawler::initProperties() { INDI::Focuser::initProperties(); FocusSpeedN[0].min = 1; FocusSpeedN[0].max = 1; FocusSpeedN[0].value = 1; // Focus Sync IUFillNumber(&SyncFocusN[0], "FOCUS_SYNC_OFFSET", "Ticks", "%.f", 0, 100000., 0., 0.); IUFillNumberVector(&SyncFocusNP, SyncFocusN, 1, getDeviceName(), "FOCUS_SYNC", "Sync", MAIN_CONTROL_TAB, IP_RW, 0, IPS_IDLE ); // Temperature + Voltage Sensors IUFillNumber(&SensorN[SENSOR_TEMPERATURE], "TEMPERATURE", "Temperature (C)", "%.2f", -100, 100., 1., 0.); IUFillNumber(&SensorN[SENSOR_VOLTAGE], "VOLTAGE", "Voltage (V)", "%.2f", 0, 20., 1., 0.); IUFillNumberVector(&SensorNP, SensorN, 2, getDeviceName(), "SENSORS", "Sensors", MAIN_CONTROL_TAB, IP_RO, 0, IPS_IDLE ); // Temperature offset IUFillNumber(&TemperatureOffsetN[0], "OFFSET", "Offset", "%.2f", -15, 15., 1., 0.); IUFillNumberVector(&TemperatureOffsetNP, TemperatureOffsetN, 1, getDeviceName(), "TEMPERATURE_OFFSET", "Temperature", MAIN_CONTROL_TAB, IP_WO, 0, IPS_IDLE ); // Motor Step Delay IUFillNumber(&FocusStepDelayN[0], "FOCUS_STEP", "Value", "%.f", 7, 100., 1., 7.); IUFillNumberVector(&FocusStepDelayNP, FocusStepDelayN, 1, getDeviceName(), "FOCUS_STEP_DELAY", "Step Rate", SETTINGS_TAB, IP_RW, 0, IPS_IDLE ); // Limit Switch IUFillLight(&LimitSwitchL[ROTATION_SWITCH], "ROTATION_SWITCH", "Rotation Home", IPS_OK); IUFillLight(&LimitSwitchL[OUT_SWITCH], "OUT_SWITCH", "Focus Out Limit", IPS_OK); IUFillLight(&LimitSwitchL[IN_SWITCH], "IN_SWITCH", "Focus In Limit", IPS_OK); IUFillLightVector(&LimitSwitchLP, LimitSwitchL, 3, getDeviceName(), "LIMIT_SWITCHES", "Limit Switch", SETTINGS_TAB, IPS_IDLE); // Home selection IUFillSwitch(&HomeSelectionS[MOTOR_FOCUS], "FOCUS", "Focuser", ISS_ON); IUFillSwitch(&HomeSelectionS[MOTOR_ROTATOR], "ROTATOR", "Rotator", ISS_ON); IUFillSwitch(&HomeSelectionS[MOTOR_AUX], "AUX", "Aux", ISS_OFF); IUFillSwitchVector(&HomeSelectionSP, HomeSelectionS, 3, getDeviceName(), "HOME_SELECTION", "Home Select", SETTINGS_TAB, IP_RW, ISR_NOFMANY, 0, IPS_IDLE); // Home Find IUFillSwitch(&FindHomeS[0], "FIND", "Start", ISS_OFF); IUFillSwitchVector(&FindHomeSP, FindHomeS, 1, getDeviceName(), "FIND_HOME", "Home Find", SETTINGS_TAB, IP_RW, ISR_1OFMANY, 0, IPS_IDLE); // Encoders IUFillSwitch(&EncoderS[0], "ENABLED", "Enabled", ISS_ON); IUFillSwitch(&EncoderS[1], "DISABLED", "Disabled", ISS_OFF); IUFillSwitchVector(&EncoderSP, EncoderS, 2, getDeviceName(), "ENCODERS", "Encoders", SETTINGS_TAB, IP_RW, ISR_1OFMANY, 0, IPS_IDLE); // Brightness IUFillNumber(&BrightnessN[BRIGHTNESS_DISPLAY], "BRIGHTNESS_DISPLAY", "Display", "%.f", 0, 255., 10., 150.); IUFillNumber(&BrightnessN[BRIGHTNESS_SLEEP], "BRIGHTNESS_SLEEP", "Sleep", "%.f", 1, 255., 10., 16.); IUFillNumberVector(&BrightnessNP, BrightnessN, 2, getDeviceName(), "BRIGHTNESS", "Brightness", SETTINGS_TAB, IP_RW, 0, IPS_IDLE ); ////////////////////////////////////////////////////// // Rotator Properties ///////////////////////////////////////////////////// INDI::RotatorInterface::initProperties(ROTATOR_TAB); // Rotator Ticks IUFillNumber(&RotatorAbsPosN[0], "ROTATOR_ABSOLUTE_POSITION", "Ticks", "%.f", 0., 100000., 1000., 0.); IUFillNumberVector(&RotatorAbsPosNP, RotatorAbsPosN, 1, getDeviceName(), "ABS_ROTATOR_POSITION", "Goto", ROTATOR_TAB, IP_RW, 0, IPS_IDLE ); // Rotator Step Delay IUFillNumber(&RotatorStepDelayN[0], "ROTATOR_STEP", "Value", "%.f", 7, 100., 1., 7.); IUFillNumberVector(&RotatorStepDelayNP, RotatorStepDelayN, 1, getDeviceName(), "ROTATOR_STEP_DELAY", "Step Rate", ROTATOR_TAB, IP_RW, 0, IPS_IDLE ); ////////////////////////////////////////////////////// // Aux Properties ///////////////////////////////////////////////////// // Aux GOTO IUFillNumber(&GotoAuxN[0], "AUX_ABSOLUTE_POSITION", "Ticks", "%.f", 0, 100000., 0., 0.); IUFillNumberVector(&GotoAuxNP, GotoAuxN, 1, getDeviceName(), "ABS_AUX_POSITION", "Goto", AUX_TAB, IP_RW, 0, IPS_IDLE ); // Abort Aux IUFillSwitch(&AbortAuxS[0], "ABORT", "Abort", ISS_OFF); IUFillSwitchVector(&AbortAuxSP, AbortAuxS, 1, getDeviceName(), "AUX_ABORT_MOTION", "Abort Motion", AUX_TAB, IP_RW, ISR_ATMOST1, 0, IPS_IDLE); // Aux Sync IUFillNumber(&SyncAuxN[0], "AUX_SYNC_TICK", "Ticks", "%.f", 0, 100000., 0., 0.); IUFillNumberVector(&SyncAuxNP, SyncAuxN, 1, getDeviceName(), "SYNC_AUX", "Sync", AUX_TAB, IP_RW, 0, IPS_IDLE ); // Aux Step Delay IUFillNumber(&AuxStepDelayN[0], "AUX_STEP", "Value", "%.f", 7, 100., 1., 7.); IUFillNumberVector(&AuxStepDelayNP, AuxStepDelayN, 1, getDeviceName(), "AUX_STEP_DELAY", "Step Rate", AUX_TAB, IP_RW, 0, IPS_IDLE ); /* Relative and absolute movement */ FocusRelPosN[0].min = 0.; FocusRelPosN[0].max = 50000.; FocusRelPosN[0].value = 0; FocusRelPosN[0].step = 1000; FocusAbsPosN[0].min = 0.; FocusAbsPosN[0].max = 100000.; FocusAbsPosN[0].value = 0; FocusAbsPosN[0].step = 1000; addDebugControl(); setDefaultPollingPeriod(500); serialConnection->setDefaultBaudRate(Connection::Serial::B_57600); return true; } bool NightCrawler::updateProperties() { INDI::Focuser::updateProperties(); if (isConnected()) { // Focus defineNumber(&SyncFocusNP); defineNumber(&SensorNP); defineNumber(&TemperatureOffsetNP); defineNumber(&FocusStepDelayNP); defineLight(&LimitSwitchLP); defineSwitch(&EncoderSP); defineNumber(&BrightnessNP); defineSwitch(&HomeSelectionSP); defineSwitch(&FindHomeSP); // Rotator INDI::RotatorInterface::updateProperties(); defineNumber(&RotatorAbsPosNP); defineNumber(&RotatorStepDelayNP); // Aux defineNumber(&GotoAuxNP); defineSwitch(&AbortAuxSP); defineNumber(&SyncAuxNP); defineNumber(&AuxStepDelayNP); } else { // Focus deleteProperty(SyncFocusNP.name); deleteProperty(SensorNP.name); deleteProperty(TemperatureOffsetNP.name); deleteProperty(FocusStepDelayNP.name); deleteProperty(LimitSwitchLP.name); deleteProperty(EncoderSP.name); deleteProperty(BrightnessNP.name); deleteProperty(FindHomeSP.name); deleteProperty(HomeSelectionSP.name); // Rotator INDI::RotatorInterface::updateProperties(); deleteProperty(RotatorAbsPosNP.name); deleteProperty(RotatorStepDelayNP.name); // Aux deleteProperty(GotoAuxNP.name); deleteProperty(AbortAuxSP.name); deleteProperty(SyncAuxNP.name); deleteProperty(AuxStepDelayNP.name); } return true; } bool NightCrawler::Handshake() { if (Ack()) return true; LOG_INFO("Error retreiving data from NightCrawler, please ensure NightCrawler controller is powered and the port is correct."); return false; } const char * NightCrawler::getDefaultName() { return "NightCrawler"; } bool NightCrawler::Ack() { bool rcFirmware = getFirmware(); bool rcType = getFocuserType(); return (rcFirmware && rcType); } bool NightCrawler::getFirmware() { int nbytes_written = 0, nbytes_read = 0, rc = -1; char errstr[MAXRBUF]; char resp[64]; tcflush(PortFD, TCIOFLUSH); if ( (rc = tty_write(PortFD, "PV#", 3, &nbytes_written)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("getFirmware error: %s.", errstr); return false; } if ( (rc = tty_read_section(PortFD, resp, '#', NIGHTCRAWLER_TIMEOUT, &nbytes_read)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("getFirmware error: %s.", errstr); return false; } tcflush(PortFD, TCIOFLUSH); resp[nbytes_read-1] = '\0'; LOGF_INFO("Firmware %s", resp); return true; } bool NightCrawler::getFocuserType() { int nbytes_written = 0, nbytes_read = 0, rc = -1; char errstr[MAXRBUF]; char resp[64]; tcflush(PortFD, TCIOFLUSH); if ( (rc = tty_write(PortFD, "PF#", 3, &nbytes_written)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("getFirmware error: %s.", errstr); return false; } if ( (rc = tty_read_section(PortFD, resp, '#', NIGHTCRAWLER_TIMEOUT, &nbytes_read)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("getFirmware error: %s.", errstr); return false; } tcflush(PortFD, TCIOFLUSH); resp[nbytes_read-1] = '\0'; LOGF_INFO("Focuser Type %s", resp); if (strcmp(resp, "2.5 NC") == 0) { RotatorAbsPosN[0].min = -NC_25_STEPS; RotatorAbsPosN[0].max = NC_25_STEPS; } else if (strcmp(resp, "3.0 NC") == 0) { RotatorAbsPosN[0].min = -NC_30_STEPS; RotatorAbsPosN[0].max = NC_30_STEPS; } else { RotatorAbsPosN[0].min = -NC_35_STEPS; RotatorAbsPosN[0].max = NC_35_STEPS; } ticksPerDegree = RotatorAbsPosN[0].max / 360.0; return true; } bool NightCrawler::gotoMotor(MotorType type, int32_t position) { char cmd[16] = {0}; char res[16] = {0}; int nbytes_written = 0, nbytes_read = 0, rc = -1; char errstr[MAXRBUF]; snprintf(cmd, 16, "%dSN %d#", type+1, position); LOGF_DEBUG("CMD <%s>", cmd); tcflush(PortFD, TCIOFLUSH); if ( (rc = tty_write(PortFD, cmd, strlen(cmd), &nbytes_written)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("%s: %s.", __FUNCTION__, errstr); return false; } if ( (rc = tty_read(PortFD, res, 1, NIGHTCRAWLER_TIMEOUT, &nbytes_read)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("%s error: %s.", __FUNCTION__, errstr); return false; } LOGF_DEBUG("RES <%s>", res); return startMotor(type); } bool NightCrawler::getPosition(MotorType type) { char cmd[16] = {0}; char res[16] = {0}; int position = -1e6; int nbytes_written = 0, nbytes_read = 0, rc = -1; char errstr[MAXRBUF]; snprintf(cmd, 16, "%dGP#", type+1); LOGF_DEBUG("CMD <%s>", cmd); tcflush(PortFD, TCIOFLUSH); if ( (rc = tty_write(PortFD, cmd, strlen(cmd), &nbytes_written)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("%s: %s.", __FUNCTION__, errstr); return false; } if ( (rc = tty_read(PortFD, res, 8, NIGHTCRAWLER_TIMEOUT, &nbytes_read)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("%s error: %s.", __FUNCTION__, errstr); return false; } res[nbytes_read] = '\0'; LOGF_DEBUG("RES <%s>", res); position = atoi(res); if (position != -1e6) { if (type == MOTOR_FOCUS) FocusAbsPosN[0].value = position; else if (type == MOTOR_ROTATOR) RotatorAbsPosN[0].value = position; else GotoAuxN[0].value = position; return true; } LOGF_DEBUG("Invalid Position! %d", position); return false; } bool NightCrawler::ISNewSwitch (const char * dev, const char * name, ISState * states, char * names[], int n) { if(strcmp(dev, getDeviceName()) == 0) { if (strcmp(name, HomeSelectionSP.name) == 0) { bool atLeastOne = false; for (int i=0; i < n; i++) { if (states[i] == ISS_ON) { atLeastOne = true; break; } } if (!atLeastOne) { HomeSelectionSP.s = IPS_ALERT; LOG_ERROR("At least one selection must be on."); IDSetSwitch(&HomeSelectionSP, nullptr); return false; } IUUpdateSwitch(&HomeSelectionSP, states, names, n); HomeSelectionSP.s = IPS_OK; IDSetSwitch(&HomeSelectionSP, nullptr); return true; } else if (strcmp(name, FindHomeSP.name) == 0) { uint8_t selection = 0; if (HomeSelectionS[MOTOR_FOCUS].s == ISS_ON) selection |= 0x01; if (HomeSelectionS[MOTOR_ROTATOR].s == ISS_ON) selection |= 0x02; if (HomeSelectionS[MOTOR_AUX].s == ISS_ON) selection |= 0x04; if (findHome(selection)) { FindHomeSP.s = IPS_BUSY; FindHomeS[0].s = ISS_ON; LOG_WARN("Homing process can take up to 10 minutes. You cannot control the unit until the process is fully complete."); } else { FindHomeSP.s = IPS_ALERT; FindHomeS[0].s = ISS_OFF; LOG_ERROR("Failed to start homing process."); } IDSetSwitch(&FindHomeSP, nullptr); return true; } else if (strcmp(name, EncoderSP.name) == 0) { IUUpdateSwitch(&EncoderSP, states, names, n); EncoderSP.s = setEncodersEnabled(EncoderS[0].s == ISS_ON) ? IPS_OK : IPS_ALERT; if (EncoderSP.s == IPS_OK) LOGF_INFO("Encoders are %s", (EncoderS[0].s == ISS_ON) ? "ON" : "OFF"); IDSetSwitch(&EncoderSP, nullptr); return true; } else if (strcmp(name, AbortAuxSP.name) == 0) { AbortAuxSP.s = stopMotor(MOTOR_AUX) ? IPS_OK : IPS_ALERT; IDSetSwitch(&AbortAuxSP, nullptr); if (AbortAuxSP.s == IPS_OK) { if (GotoAuxNP.s != IPS_OK) { GotoAuxNP.s = IPS_OK; IDSetNumber(&GotoAuxNP, nullptr); } } return true; } else if (strstr(name, "ROTATOR")) { if (INDI::RotatorInterface::processSwitch(dev, name, states, names, n)) return true; } } return INDI::Focuser::ISNewSwitch(dev, name, states, names, n); } bool NightCrawler::ISNewNumber (const char * dev, const char * name, double values[], char * names[], int n) { if (dev != nullptr && strcmp(dev, getDeviceName()) == 0) { if (strcmp(name, SyncFocusNP.name) == 0) { bool rc = syncMotor(MOTOR_FOCUS, static_cast(values[0])); SyncFocusNP.s = rc ? IPS_OK : IPS_ALERT; if (rc) SyncFocusN[0].value = values[0]; IDSetNumber(&SyncFocusNP, nullptr); return true; } else if (strcmp(name, SyncAuxNP.name) == 0) { bool rc = syncMotor(MOTOR_AUX, static_cast(values[0])); SyncAuxNP.s = rc ? IPS_OK : IPS_ALERT; if (rc) SyncAuxN[0].value = values[0]; IDSetNumber(&SyncAuxNP, nullptr); return true; } else if (strcmp(name, TemperatureOffsetNP.name) == 0) { bool rc = setTemperatureOffset(values[0]); TemperatureOffsetNP.s = rc ? IPS_OK : IPS_ALERT; IDSetNumber(&TemperatureOffsetNP, nullptr); return true; } else if (strcmp(name, FocusStepDelayNP.name) == 0) { bool rc = setStepDelay(MOTOR_FOCUS, static_cast(values[0])); FocusStepDelayNP.s = rc ? IPS_OK : IPS_ALERT; if (rc) FocusStepDelayN[0].value = values[0]; IDSetNumber(&FocusStepDelayNP, nullptr); return true; } else if (strcmp(name, RotatorStepDelayNP.name) == 0) { bool rc = setStepDelay(MOTOR_ROTATOR, static_cast(values[0])); RotatorStepDelayNP.s = rc ? IPS_OK : IPS_ALERT; if (rc) RotatorStepDelayN[0].value = values[0]; IDSetNumber(&RotatorStepDelayNP, nullptr); return true; } else if (strcmp(name, AuxStepDelayNP.name) == 0) { bool rc = setStepDelay(MOTOR_AUX, static_cast(values[0])); AuxStepDelayNP.s = rc ? IPS_OK : IPS_ALERT; if (rc) AuxStepDelayN[0].value = values[0]; IDSetNumber(&AuxStepDelayNP, nullptr); return true; } else if (strcmp(name, BrightnessNP.name) == 0) { IUUpdateNumber(&BrightnessNP, values, names, n); bool rcDisplay = setDisplayBrightness(static_cast(BrightnessN[BRIGHTNESS_DISPLAY].value)); bool rcSleep = setSleepBrightness(static_cast(BrightnessN[BRIGHTNESS_SLEEP].value)); if (rcDisplay && rcSleep) BrightnessNP.s = IPS_OK; else BrightnessNP.s = IPS_ALERT; IDSetNumber(&BrightnessNP, nullptr); return true; } else if (strcmp(name, GotoAuxNP.name) == 0) { bool rc = gotoMotor(MOTOR_AUX, static_cast(values[0])); GotoAuxNP.s = rc ? IPS_BUSY : IPS_OK; IDSetNumber(&GotoAuxNP, nullptr); LOGF_INFO("Aux moving to %.f...", values[0]); return true; } else if (strcmp(name, RotatorAbsPosNP.name) == 0) { RotatorAbsPosNP.s = (gotoMotor(MOTOR_ROTATOR, static_cast(values[0])) ? IPS_BUSY : IPS_ALERT); IDSetNumber(&RotatorAbsPosNP, nullptr); if (RotatorAbsPosNP.s == IPS_BUSY) LOGF_INFO("Rotator moving to %.f ticks...", values[0]); return true; } else if (strstr(name, "ROTATOR")) { if (INDI::RotatorInterface::processNumber(dev, name, values, names, n)) return true; } } return INDI::Focuser::ISNewNumber(dev, name, values, names, n); } IPState NightCrawler::MoveAbsFocuser(uint32_t targetTicks) { targetPosition = targetTicks; bool rc = false; rc = gotoMotor(MOTOR_FOCUS, targetPosition); if (!rc) return IPS_ALERT; FocusAbsPosNP.s = IPS_BUSY; return IPS_BUSY; } IPState NightCrawler::MoveRelFocuser(FocusDirection dir, uint32_t ticks) { double newPosition = 0; bool rc = false; if (dir == FOCUS_INWARD) newPosition = FocusAbsPosN[0].value - ticks; else newPosition = FocusAbsPosN[0].value + ticks; rc = gotoMotor(MOTOR_FOCUS, newPosition); if (!rc) return IPS_ALERT; FocusRelPosN[0].value = ticks; FocusRelPosNP.s = IPS_BUSY; return IPS_BUSY; } void NightCrawler::TimerHit() { if (!isConnected()) { SetTimer(POLLMS); return; } bool rc = false; bool sensorsUpdated=false; // #1 If we're homing, we check if homing is complete as we cannot check for anything else if (FindHomeSP.s == IPS_BUSY || HomeRotatorSP.s == IPS_BUSY) { if (isHomingComplete()) { HomeRotatorS[0].s = ISS_OFF; HomeRotatorSP.s = IPS_OK; IDSetSwitch(&HomeRotatorSP, nullptr); FindHomeS[0].s = ISS_OFF; FindHomeSP.s = IPS_OK; IDSetSwitch(&FindHomeSP, nullptr); LOG_INFO("Homing is complete."); } SetTimer(POLLMS); return; } // #2 Get Temperature rc = getTemperature(); if (rc && fabs(SensorN[SENSOR_TEMPERATURE].value - lastTemperature) > NIGHTCRAWLER_THRESHOLD) { lastTemperature = SensorN[SENSOR_TEMPERATURE].value; sensorsUpdated = true; } // #3 Get Voltage rc = getVoltage(); if (rc && fabs(SensorN[SENSOR_VOLTAGE].value - lastVoltage) > NIGHTCRAWLER_THRESHOLD) { lastVoltage = SensorN[SENSOR_VOLTAGE].value; sensorsUpdated = true; } if (sensorsUpdated) IDSetNumber(&SensorNP, nullptr); // #4 Get Limit Switch Status rc = getLimitSwitchStatus(); if (rc && (LimitSwitchL[ROTATION_SWITCH].s != rotationLimit || LimitSwitchL[OUT_SWITCH].s != outSwitchLimit || LimitSwitchL[IN_SWITCH].s != inSwitchLimit)) { rotationLimit = LimitSwitchL[ROTATION_SWITCH].s; outSwitchLimit = LimitSwitchL[OUT_SWITCH].s; inSwitchLimit = LimitSwitchL[IN_SWITCH].s; IDSetLight(&LimitSwitchLP, nullptr); } // #5 Focus Position & Status bool absFocusUpdated = false; if (FocusAbsPosNP.s == IPS_BUSY) { // Stopped moving if (!isMotorMoving(MOTOR_FOCUS)) { FocusAbsPosNP.s = IPS_OK; if (FocusRelPosNP.s != IPS_OK) { FocusRelPosNP.s = IPS_OK; IDSetNumber(&FocusRelPosNP, nullptr); } absFocusUpdated = true; } } rc = getPosition(MOTOR_FOCUS); if (rc && FocusAbsPosN[0].value != lastFocuserPosition) { lastFocuserPosition = FocusAbsPosN[0].value; absFocusUpdated = true; } if (absFocusUpdated) IDSetNumber(&FocusAbsPosNP, nullptr); // #6 Rotator Position & Status bool absRotatorUpdated = false; if (RotatorAbsPosNP.s == IPS_BUSY) { // Stopped moving if (!isMotorMoving(MOTOR_ROTATOR)) { RotatorAbsPosNP.s = IPS_OK; GotoRotatorNP.s = IPS_OK; absRotatorUpdated = true; LOG_INFO("Rotator motion complete."); } } rc = getPosition(MOTOR_ROTATOR); if (rc && RotatorAbsPosN[0].value != lastRotatorPosition) { lastRotatorPosition = RotatorAbsPosN[0].value; GotoRotatorN[0].value = range360(RotatorAbsPosN[0].value / ticksPerDegree); absRotatorUpdated = true; } if (absRotatorUpdated) { IDSetNumber(&RotatorAbsPosNP, nullptr); IDSetNumber(&GotoRotatorNP, nullptr); } // #7 Aux Position & Status bool absAuxUpdated = false; if (GotoAuxNP.s == IPS_BUSY) { // Stopped moving if (!isMotorMoving(MOTOR_AUX)) { GotoAuxNP.s = IPS_OK; absAuxUpdated = true; LOG_INFO("Aux motion complete."); } } rc = getPosition(MOTOR_AUX); if (rc && GotoAuxN[0].value != lastAuxPosition) { lastAuxPosition = GotoAuxN[0].value; absAuxUpdated = true; } if (absAuxUpdated) IDSetNumber(&GotoAuxNP, nullptr); SetTimer(POLLMS); } bool NightCrawler::AbortFocuser() { return stopMotor(MOTOR_FOCUS); } bool NightCrawler::syncMotor(MotorType type, uint32_t position) { char cmd[16] = {0}; char res[16] = {0}; int nbytes_written = 0, nbytes_read = 0, rc = -1; char errstr[MAXRBUF]; snprintf(cmd, 16, "%dSP %d#", type+1, position); LOGF_DEBUG("CMD <%s>", cmd); tcflush(PortFD, TCIOFLUSH); if ( (rc = tty_write(PortFD, cmd, strlen(cmd), &nbytes_written)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("%s: %s.", __FUNCTION__, errstr); return false; } if ( (rc = tty_read(PortFD, res, 1, NIGHTCRAWLER_TIMEOUT, &nbytes_read)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("%s error: %s.", __FUNCTION__, errstr); return false; } res[nbytes_read] = '\0'; LOGF_DEBUG("RES <%s>", res); return (res[0] == '#'); } bool NightCrawler::startMotor(MotorType type) { char cmd[16] = {0}; char res[16] = {0}; int nbytes_written = 0, nbytes_read = 0, rc = -1; char errstr[MAXRBUF]; snprintf(cmd, 16, "%dSM#", type+1); LOGF_DEBUG("CMD <%s>", cmd); tcflush(PortFD, TCIOFLUSH); if ( (rc = tty_write(PortFD, cmd, strlen(cmd), &nbytes_written)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("%s: %s.", __FUNCTION__, errstr); return false; } if ( (rc = tty_read(PortFD, res, 1, NIGHTCRAWLER_TIMEOUT, &nbytes_read)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("%s error: %s.", __FUNCTION__, errstr); return false; } res[nbytes_read] = '\0'; LOGF_DEBUG("RES <%s>", res); return (res[0] == '#'); } bool NightCrawler::stopMotor(MotorType type) { char cmd[16] = {0}; char res[16] = {0}; int nbytes_written = 0, nbytes_read = 0, rc = -1; char errstr[MAXRBUF]; snprintf(cmd, 16, "%dSQ#", type+1); LOGF_DEBUG("CMD <%s>", cmd); tcflush(PortFD, TCIOFLUSH); if ( (rc = tty_write(PortFD, cmd, strlen(cmd), &nbytes_written)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("%s: %s.", __FUNCTION__, errstr); return false; } if ( (rc = tty_read(PortFD, res, 1, NIGHTCRAWLER_TIMEOUT, &nbytes_read)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("%s error: %s.", __FUNCTION__, errstr); return false; } res[nbytes_read] = '\0'; LOGF_DEBUG("RES <%s>", res); return (res[0] == '#'); } bool NightCrawler::isMotorMoving(MotorType type) { char cmd[16] = {0}; char res[16] = {0}; int nbytes_written = 0, nbytes_read = 0, rc = -1; char errstr[MAXRBUF]; snprintf(cmd, 16, "%dGM#", type+1); LOGF_DEBUG("CMD <%s>", cmd); tcflush(PortFD, TCIOFLUSH); if ( (rc = tty_write(PortFD, cmd, strlen(cmd), &nbytes_written)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("%s error: %s.", __FUNCTION__, errstr); return false; } if ( (rc = tty_read_section(PortFD, res, '#', NIGHTCRAWLER_TIMEOUT, &nbytes_read)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("%s error: %s.", __FUNCTION__, errstr); return false; } res[nbytes_read-1] = '\0'; LOGF_DEBUG("RES <%s>", res); return (strcmp("01", res) == 0); } bool NightCrawler::getTemperature() { char cmd[16] = "GT#"; char res[16] = {0}; int nbytes_written = 0, nbytes_read = 0, rc = -1; char errstr[MAXRBUF]; LOGF_DEBUG("CMD <%s>", cmd); tcflush(PortFD, TCIOFLUSH); if ( (rc = tty_write(PortFD, cmd, strlen(cmd), &nbytes_written)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("%s error: %s.", __FUNCTION__, errstr); return false; } if ( (rc = tty_read_section(PortFD, res, '#', NIGHTCRAWLER_TIMEOUT, &nbytes_read)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("%s error: %s.", __FUNCTION__, errstr); return false; } res[nbytes_read-1] = '\0'; LOGF_DEBUG("RES <%s>", res); SensorN[SENSOR_TEMPERATURE].value = atoi(res) / 10.0; return true; } bool NightCrawler::getVoltage() { char cmd[16] = "GV#"; char res[16] = {0}; int nbytes_written = 0, nbytes_read = 0, rc = -1; char errstr[MAXRBUF]; LOGF_DEBUG("CMD <%s>", cmd); tcflush(PortFD, TCIOFLUSH); if ( (rc = tty_write(PortFD, cmd, strlen(cmd), &nbytes_written)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("%s error: %s.", __FUNCTION__, errstr); return false; } if ( (rc = tty_read_section(PortFD, res, '#', NIGHTCRAWLER_TIMEOUT, &nbytes_read)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("%s error: %s.", __FUNCTION__, errstr); return false; } res[nbytes_read-1] = '\0'; LOGF_DEBUG("RES <%s>", res); SensorN[SENSOR_VOLTAGE].value = atoi(res) / 10.0; return true; } bool NightCrawler::setTemperatureOffset(double offset) { char cmd[16] = {0}; int nbytes_written = 0, rc = -1; char errstr[MAXRBUF]; snprintf(cmd, 16, "Pt %03d#", static_cast(offset*10)); LOGF_DEBUG("CMD <%s>", cmd); tcflush(PortFD, TCIOFLUSH); if ( (rc = tty_write(PortFD, cmd, strlen(cmd), &nbytes_written)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("%s error: %s.", __FUNCTION__, errstr); return false; } return true; } bool NightCrawler::getStepDelay(MotorType type) { char cmd[16] = {0}; char res[16] = {0}; int nbytes_written = 0, nbytes_read = 0, rc = -1; char errstr[MAXRBUF]; snprintf(cmd, 16, "%dSR#", type+1); LOGF_DEBUG("CMD <%s>", cmd); tcflush(PortFD, TCIOFLUSH); if ( (rc = tty_write(PortFD, cmd, strlen(cmd), &nbytes_written)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("%s: %s.", __FUNCTION__, errstr); return false; } if ( (rc = tty_read_section(PortFD, res, '#', NIGHTCRAWLER_TIMEOUT, &nbytes_read)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("%s error: %s.", __FUNCTION__, errstr); return false; } res[nbytes_read-1] = '\0'; LOGF_DEBUG("RES <%s>", res); if (type == MOTOR_FOCUS) FocusStepDelayN[0].value = atoi(res); else if (type == MOTOR_ROTATOR) RotatorStepDelayN[0].value = atoi(res); else AuxStepDelayN[0].value = atoi(res); return true; } bool NightCrawler::setStepDelay(MotorType type, uint32_t delay) { char cmd[16] = {0}; char res[16] = {0}; int nbytes_written = 0, nbytes_read = 0, rc = -1; char errstr[MAXRBUF]; snprintf(cmd, 16, "%dSR %03d#", type+1, delay); LOGF_DEBUG("CMD <%s>", cmd); tcflush(PortFD, TCIOFLUSH); if ( (rc = tty_write(PortFD, cmd, strlen(cmd), &nbytes_written)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("%s error: %s.", __FUNCTION__, errstr); return false; } if ( (rc = tty_read(PortFD, res, 1, NIGHTCRAWLER_TIMEOUT, &nbytes_read)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("%s error: %s.", __FUNCTION__, errstr); return false; } res[nbytes_read] = '\0'; LOGF_DEBUG("RES <%s>", res); return (res[0] == '#'); } bool NightCrawler::getLimitSwitchStatus() { char cmd[16] = "GS#"; char res[16] = {0}; int nbytes_written = 0, nbytes_read = 0, rc = -1; char errstr[MAXRBUF]; LOGF_DEBUG("CMD <%s>", cmd); tcflush(PortFD, TCIOFLUSH); if ( (rc = tty_write(PortFD, cmd, strlen(cmd), &nbytes_written)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("%s error: %s.", __FUNCTION__, errstr); return false; } if ( (rc = tty_read_section(PortFD, res, '#', NIGHTCRAWLER_TIMEOUT, &nbytes_read)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("%s error: %s.", __FUNCTION__, errstr); return false; } res[nbytes_read-1] = '\0'; LOGF_DEBUG("RES <%s>", res); int value = atoi(res); LimitSwitchL[ROTATION_SWITCH].s = (value & 0x01) ? IPS_ALERT : IPS_OK; LimitSwitchL[OUT_SWITCH].s = (value & 0x02) ? IPS_ALERT : IPS_OK; LimitSwitchL[IN_SWITCH].s = (value & 0x04) ? IPS_ALERT : IPS_OK; return true; } bool NightCrawler::findHome(uint8_t motorTypes) { char cmd[16] = {0}; char res[16] = {0}; int nbytes_written = 0, nbytes_read = 0, rc = -1; char errstr[MAXRBUF]; snprintf(cmd, 16, "SH %02d#", motorTypes); LOGF_DEBUG("CMD <%s>", cmd); tcflush(PortFD, TCIOFLUSH); if ( (rc = tty_write(PortFD, cmd, strlen(cmd), &nbytes_written)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("%s error: %s.", __FUNCTION__, errstr); return false; } if ( (rc = tty_read(PortFD, res, 1, NIGHTCRAWLER_TIMEOUT, &nbytes_read)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("%s error: %s.", __FUNCTION__, errstr); return false; } res[nbytes_read] = '\0'; LOGF_DEBUG("RES <%s>", res); return (res[0] == '#'); } bool NightCrawler::isHomingComplete() { char res[16] = {0}; int nbytes_read = 0, rc = -1; if ( (rc = tty_read_section(PortFD, res, '#', NIGHTCRAWLER_TIMEOUT, &nbytes_read)) != TTY_OK) { // No error as we are waiting until controller returns "OK#" LOG_DEBUG("Waiting for NightCrawler to complete homing..."); return false; } res[nbytes_read-1] = '\0'; LOGF_DEBUG("RES <%s>", res); return (strcmp("OK", res) == 0); } bool NightCrawler::setEncodersEnabled(bool enable) { char cmd[16] = {0}; char res[16] = {0}; int nbytes_written = 0, nbytes_read = 0, rc = -1; char errstr[MAXRBUF]; snprintf(cmd, 16, "PE %s#", enable ? "01" : "00"); LOGF_DEBUG("CMD <%s>", cmd); tcflush(PortFD, TCIOFLUSH); if ( (rc = tty_write(PortFD, cmd, strlen(cmd), &nbytes_written)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("%s error: %s.", __FUNCTION__, errstr); return false; } if ( (rc = tty_read_section(PortFD, res, '#', NIGHTCRAWLER_TIMEOUT, &nbytes_read)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("%s error: %s.", __FUNCTION__, errstr); return false; } res[nbytes_read] = '\0'; LOGF_DEBUG("RES <%s>", res); return true; } bool NightCrawler::setDisplayBrightness(uint8_t value) { char cmd[16] = {0}; char res[16] = {0}; int nbytes_written = 0, nbytes_read = 0, rc = -1; char errstr[MAXRBUF]; snprintf(cmd, 16, "PD %03d#", value); LOGF_DEBUG("CMD <%s>", cmd); tcflush(PortFD, TCIOFLUSH); if ( (rc = tty_write(PortFD, cmd, strlen(cmd), &nbytes_written)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("%s error: %s.", __FUNCTION__, errstr); return false; } if ( (rc = tty_read(PortFD, res, 1, NIGHTCRAWLER_TIMEOUT, &nbytes_read)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("%s error: %s.", __FUNCTION__, errstr); return false; } res[nbytes_read] = '\0'; LOGF_DEBUG("RES <%s>", res); return (res[0] == '#'); } bool NightCrawler::setSleepBrightness(uint8_t value) { char cmd[16] = {0}; char res[16] = {0}; int nbytes_written = 0, nbytes_read = 0, rc = -1; char errstr[MAXRBUF]; snprintf(cmd, 16, "PL %03d#", value); LOGF_DEBUG("CMD <%s>", cmd); tcflush(PortFD, TCIOFLUSH); if ( (rc = tty_write(PortFD, cmd, strlen(cmd), &nbytes_written)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("%s error: %s.", __FUNCTION__, errstr); return false; } if ( (rc = tty_read(PortFD, res, 1, NIGHTCRAWLER_TIMEOUT, &nbytes_read)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("%s error: %s.", __FUNCTION__, errstr); return false; } res[nbytes_read] = '\0'; LOGF_DEBUG("RES <%s>", res); return (res[0] == '#'); } bool NightCrawler::saveConfigItems(FILE *fp) { Focuser::saveConfigItems(fp); IUSaveConfigNumber(fp, &BrightnessNP); IUSaveConfigNumber(fp, &FocusStepDelayNP); IUSaveConfigNumber(fp, &RotatorStepDelayNP); IUSaveConfigNumber(fp, &AuxStepDelayNP); return true; } IPState NightCrawler::HomeRotator() { if (findHome(0x02)) { FindHomeSP.s = IPS_BUSY; FindHomeS[0].s = ISS_ON; IDSetSwitch(&FindHomeSP, nullptr); LOG_WARN("Homing process can take up to 10 minutes. You cannot control the unit until the process is fully complete."); return IPS_BUSY; } else { FindHomeSP.s = IPS_ALERT; FindHomeS[0].s = ISS_OFF; IDSetSwitch(&FindHomeSP, nullptr); LOG_ERROR("Failed to start homing process."); return IPS_ALERT; } return IPS_ALERT; } IPState NightCrawler::MoveRotator(double angle) { // Find shortest distance given target degree double a=angle; double b=GotoRotatorN[0].value; double d=fabs(a-b); double r=(d > 180) ? 360 - d : d; int sign = (a - b >= 0 && a - b <= 180) || (a - b <=-180 && a- b>= -360) ? 1 : -1; r *= sign; double newTarget = (r+b) * ticksPerDegree; if (newTarget < RotatorAbsPosN[0].min) newTarget -= RotatorAbsPosN[0].min; else if (newTarget > RotatorAbsPosN[0].max) newTarget -= RotatorAbsPosN[0].max; bool rc = gotoMotor(MOTOR_ROTATOR, static_cast(newTarget)); if (rc) { RotatorAbsPosNP.s = IPS_BUSY; IDSetNumber(&RotatorAbsPosNP, nullptr); return IPS_BUSY; } return IPS_ALERT; } bool NightCrawler::SyncRotator(double angle) { // Find shortest distance given target degree double a=angle; double b=GotoRotatorN[0].value; double d=fabs(a-b); double r=(d > 180) ? 360 - d : d; int sign = (a - b >= 0 && a - b <= 180) || (a - b <=-180 && a- b>= -360) ? 1 : -1; r *= sign; double newTarget = (r+b) * ticksPerDegree; if (newTarget < RotatorAbsPosN[0].min) newTarget -= RotatorAbsPosN[0].min; else if (newTarget > RotatorAbsPosN[0].max) newTarget -= RotatorAbsPosN[0].max; return syncMotor(MOTOR_ROTATOR, static_cast(newTarget)); } bool NightCrawler::AbortRotator() { bool rc = stopMotor(MOTOR_ROTATOR); if (rc && RotatorAbsPosNP.s != IPS_OK) { RotatorAbsPosNP.s = IPS_OK; IDSetNumber(&RotatorAbsPosNP, nullptr); } return rc; } libindi/drivers/rotator/gemini.h0000664000175000017500000001754113263645557016273 0ustar jasemjasem/* Optec Gemini Focuser Rotator INDI driver Copyright (C) 2017 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 */ #pragma once #include "indifocuser.h" #include "indirotatorinterface.h" #include class Gemini : public INDI::Focuser, public INDI::RotatorInterface { public: Gemini(); ~Gemini(); enum { FOCUS_A_COEFF, FOCUS_B_COEFF, FOCUS_C_COEFF, FOCUS_D_COEFF, FOCUS_E_COEFF, FOCUS_F_COEFF }; enum { STATUS_MOVING, STATUS_HOMING, STATUS_HOMED, STATUS_FFDETECT, STATUS_TMPPROBE, STATUS_REMOTEIO, STATUS_HNDCTRL, STATUS_REVERSE, STATUS_UNKNOWN }; enum { GOTO_CENTER, GOTO_HOME }; typedef enum { DEVICE_FOCUSER, DEVICE_ROTATOR } DeviceType; virtual bool ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) override; virtual bool ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) override; virtual bool ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n) override; protected: virtual bool Handshake() override; virtual const char *getDefaultName() override; virtual bool initProperties() override; virtual bool updateProperties() override; virtual bool saveConfigItems(FILE *fp) override; // Focuser Functions virtual IPState MoveAbsFocuser(uint32_t targetPosition) override; virtual IPState MoveRelFocuser(FocusDirection dir, uint32_t ticks) override; virtual IPState MoveFocuser(FocusDirection dir, int speed, uint16_t duration) override; virtual bool AbortFocuser() override; virtual void TimerHit() override; // Misc functions bool ack(); bool isResponseOK(); protected: // Move from private to public to validate bool focuserConfigurationComplete = false; bool rotatorConfigurationComplete = false; // Rotator Overrides virtual IPState HomeRotator() override; virtual IPState MoveRotator(double angle) override; virtual bool ReverseRotator(bool enabled) override; private: uint32_t focuserSimPosition=0; uint32_t rotatorSimPosition=0; uint32_t rotatorSimPA=0; uint32_t targetFocuserPosition=0; uint32_t targetRotatorPosition=0; uint32_t targetRotatorAngle=0; uint32_t maxControllerTicks=0; ISState focuserSimStatus[8]; ISState rotatorSimStatus[8]; bool simCompensationOn; char focusTarget[8]; struct timeval focusMoveStart; float focusMoveRequest; //////////////////////////////////////////////////////////// // Focuser Functions /////////////////////////////////////////////////////////// // Get functions bool getFocusConfig(); bool getFocusStatus(); // Set functions // Position bool setFocusPosition(u_int16_t position); // Temperature bool setTemperatureCompensation(bool enable); bool setTemperatureCompensationMode(char mode); bool setTemperatureCompensationCoeff(char mode, int16_t coeff); bool setTemperatureCompensationOnStart(bool enable); // Backlash bool setBacklashCompensation(DeviceType type, bool enable); bool setBacklashCompensationSteps(DeviceType type, uint16_t steps); // Motion functions bool home(DeviceType type); bool halt(DeviceType type); bool center(DeviceType type); bool homeOnStart(DeviceType type, bool enable); //////////////////////////////////////////////////////////// // Focuser Properties /////////////////////////////////////////////////////////// // Set/Get Temperature INumber TemperatureN[1]; INumberVectorProperty TemperatureNP; // Enable/Disable temperature compnesation ISwitch TemperatureCompensateS[2]; ISwitchVectorProperty TemperatureCompensateSP; // Enable/Disable temperature compnesation on start ISwitch TemperatureCompensateOnStartS[2]; ISwitchVectorProperty TemperatureCompensateOnStartSP; // Temperature Coefficient INumber TemperatureCoeffN[5]; INumberVectorProperty TemperatureCoeffNP; // Temperature Coefficient Mode ISwitch TemperatureCompensateModeS[5]; ISwitchVectorProperty TemperatureCompensateModeSP; // Enable/Disable backlash ISwitch FocuserBacklashCompensationS[2]; ISwitchVectorProperty FocuserBacklashCompensationSP; // Backlash Value INumber FocuserBacklashN[1]; INumberVectorProperty FocuserBacklashNP; // Home On Start ISwitch FocuserHomeOnStartS[2]; ISwitchVectorProperty FocuserHomeOnStartSP; // Go to home/center ISwitch FocuserGotoS[2]; ISwitchVectorProperty FocuserGotoSP; // Status indicators ILight FocuserStatusL[8]; ILightVectorProperty FocuserStatusLP; bool isFocuserAbsolute; bool isFocuserHoming; //////////////////////////////////////////////////////////// // Rotator Functions /////////////////////////////////////////////////////////// // Get functions bool getRotatorConfig(); bool getRotatorStatus(); IPState MoveAbsRotatorTicks(uint32_t targetTicks); IPState MoveAbsRotatorAngle(double angle); bool reverseRotator(bool enable); //////////////////////////////////////////////////////////// // Rotator Properties /////////////////////////////////////////////////////////// // Status ILight RotatorStatusL[8]; ILightVectorProperty RotatorStatusLP; // Rotator Steps INumber RotatorAbsPosN[1]; INumberVectorProperty RotatorAbsPosNP; #if 0 // Reverse Direction ISwitch RotatorReverseS[2]; ISwitchVectorProperty RotatorReverseSP; // Rotator Degrees or PA (Position Angle) INumber RotatorAbsAngleN[1]; INumberVectorProperty RotatorAbsAngleNP; // Abort ISwitch AbortRotatorS[1]; ISwitchVectorProperty AbortRotatorSP; // Go to home/center ISwitch RotatorGotoS[2]; ISwitchVectorProperty RotatorGotoSP; #endif // Enable/Disable backlash ISwitch RotatorBacklashCompensationS[2]; ISwitchVectorProperty RotatorBacklashCompensationSP; // Backlash Value INumber RotatorBacklashN[1]; INumberVectorProperty RotatorBacklashNP; // Home On Start ISwitch RotatorHomeOnStartS[2]; ISwitchVectorProperty RotatorHomeOnStartSP; bool isRotatorHoming; //////////////////////////////////////////////////////////// // Hub Functions /////////////////////////////////////////////////////////// // Led level bool setLedLevel(int level); // Device Nickname bool setNickname(DeviceType type, const char *nickname); // Misc functions bool resetFactory(); float calcTimeLeft(timeval, float); //////////////////////////////////////////////////////////// // Hub Properties /////////////////////////////////////////////////////////// // Reset to Factory setting ISwitch ResetS[1]; ISwitchVectorProperty ResetSP; // Focus and rotator name configure in the HUB IText HFocusNameT[2] {}; ITextVectorProperty HFocusNameTP; // Led Intensity Value INumber LedN[1]; INumberVectorProperty LedNP; uint32_t DBG_FOCUS; }; libindi/drivers/rotator/pyxis.h0000664000175000017500000000423213263645557016170 0ustar jasemjasem/* Optec Pyrix Rotator Copyright (C) 2017 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 */ #pragma once #include "indirotator.h" class Pyxis : public INDI::Rotator { public: Pyxis(); virtual ~Pyxis() = default; virtual bool Handshake(); const char * getDefaultName(); virtual bool initProperties(); virtual bool updateProperties(); virtual bool ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n); virtual bool ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n); protected: // Rotator Overrides virtual IPState HomeRotator(); virtual IPState MoveRotator(double angle); virtual bool ReverseRotator(bool enabled); // Misc. virtual void TimerHit(); private: // Check if connection is OK bool Ack(); bool isMotionComplete(); bool getPA(uint16_t & PA); int getReverseStatus(); bool setSteppingMode(uint8_t mode); bool setRotationRate(uint8_t rate); bool sleepController(); bool wakeupController(); void queryParams(); // Rotation Rate INumber RotationRateN[1]; INumberVectorProperty RotationRateNP; // Stepping ISwitch SteppingS[2]; ISwitchVectorProperty SteppingSP; enum { FULL_STEP, HALF_STEP}; // Power ISwitch PowerS[2]; ISwitchVectorProperty PowerSP; enum { POWER_SLEEP, POWER_WAKEUP}; uint16_t targetPA = {0}; }; libindi/drivers/rotator/pyxis.cpp0000664000175000017500000004013513263645557016525 0ustar jasemjasem/* Optec Pyrix Rotator Copyright (C) 2017 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 */ #include "pyxis.h" #include "indicom.h" #include "connectionplugins/connectionserial.h" #include #include #include #include #define PYXIS_TIMEOUT 3 #define PYRIX_BUF 7 #define PYRIX_CMD 6 #define SETTINGS_TAB "Settings" std::unique_ptr pyxis(new Pyxis()); void ISGetProperties(const char *dev) { pyxis->ISGetProperties(dev); } void ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) { pyxis->ISNewSwitch(dev, name, states, names, n); } void ISNewText( const char *dev, const char *name, char *texts[], char *names[], int n) { pyxis->ISNewText(dev, name, texts, names, n); } void ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) { pyxis->ISNewNumber(dev, name, values, names, n); } void ISNewBLOB (const char *dev, const char *name, int sizes[], int blobsizes[], char *blobs[], char *formats[], char *names[], int n) { INDI_UNUSED(dev); INDI_UNUSED(name); INDI_UNUSED(sizes); INDI_UNUSED(blobsizes); INDI_UNUSED(blobs); INDI_UNUSED(formats); INDI_UNUSED(names); INDI_UNUSED(n); } void ISSnoopDevice (XMLEle *root) { pyxis->ISSnoopDevice(root); } Pyxis::Pyxis() { // We do not have absolute ticks RI::SetCapability(ROTATOR_CAN_HOME | ROTATOR_CAN_REVERSE); setRotatorConnection(CONNECTION_SERIAL); } bool Pyxis::initProperties() { INDI::Rotator::initProperties(); // Rotation Rate IUFillNumber(&RotationRateN[0], "RATE", "Rate", "%.f", 0, 99, 10, 8); IUFillNumberVector(&RotationRateNP, RotationRateN, 1, getDeviceName(), "ROTATION_RATE", "Rotation", SETTINGS_TAB, IP_RW, 0, IPS_IDLE); // Stepping IUFillSwitch(&SteppingS[FULL_STEP], "FULL_STEP", "Full", ISS_OFF); IUFillSwitch(&SteppingS[HALF_STEP], "HALF_STEP", "Half", ISS_OFF); IUFillSwitchVector(&SteppingSP, SteppingS, 2, getDeviceName(), "STEPPING_RATE", "Stepping", SETTINGS_TAB, IP_RW, ISR_ATMOST1, 0, IPS_IDLE); // Power IUFillSwitch(&PowerS[POWER_SLEEP], "POWER_SLEEP", "Sleep", ISS_OFF); IUFillSwitch(&PowerS[POWER_WAKEUP], "POWER_WAKEUP", "Wake Up", ISS_OFF); IUFillSwitchVector(&PowerSP, PowerS, 2, getDeviceName(), "POWER_STATE", "Power", SETTINGS_TAB, IP_RW, ISR_ATMOST1, 0, IPS_IDLE); serialConnection->setDefaultBaudRate(Connection::Serial::B_19200); return true; } bool Pyxis::Handshake() { if (Ack()) return true; LOG_INFO("Error retreiving data from Pyrix, please ensure Pyrix controller is powered and the port is correct."); return false; } const char * Pyxis::getDefaultName() { return "Pyxis"; } bool Pyxis::updateProperties() { INDI::Rotator::updateProperties(); if (isConnected()) { defineNumber(&RotationRateNP); defineSwitch(&SteppingSP); defineSwitch(&PowerSP); queryParams(); } else { deleteProperty(RotationRateNP.name); deleteProperty(SteppingSP.name); deleteProperty(PowerSP.name); } return true; } void Pyxis::queryParams() { //////////////////////////////////////////// // Reverse Parameter //////////////////////////////////////////// int dir = getReverseStatus(); IUResetSwitch(&ReverseRotatorSP); ReverseRotatorSP.s = IPS_OK; if (dir == 0) ReverseRotatorS[REVERSE_DISABLED].s = ISS_ON; else if (dir == 1) ReverseRotatorS[REVERSE_ENABLED].s = ISS_ON; else ReverseRotatorSP.s = IPS_ALERT; IDSetSwitch(&ReverseRotatorSP, nullptr); } bool Pyxis::Ack() { const char *cmd = "CCLINK"; char res[1] = {0}; int nbytes_written = 0, nbytes_read = 0, rc = -1; char errstr[MAXRBUF]; LOGF_DEBUG("CMD <%s>", cmd); tcflush(PortFD, TCIOFLUSH); if ( (rc = tty_write(PortFD, cmd, PYRIX_CMD, &nbytes_written)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("%s: %s.", __FUNCTION__, errstr); return false; } if ( (rc = tty_read(PortFD, res, 1, PYXIS_TIMEOUT, &nbytes_read)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("%s error: %s.", __FUNCTION__, errstr); return false; } LOGF_DEBUG("RES <%c>", res[0]); tcflush(PortFD, TCIOFLUSH); if (res[0] != '!') { LOG_ERROR("Cannot establish communication. Check power is on and homing is complete."); return false; } return true; } bool Pyxis::ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) { if (dev != nullptr && strcmp(dev, getDeviceName()) == 0) { if (!strcmp(name, RotationRateNP.name)) { bool rc = setRotationRate(static_cast(values[0])); if (rc) { RotationRateNP.s = IPS_OK; RotationRateN[0].value = values[0]; } else RotationRateNP.s = IPS_ALERT; IDSetNumber(&RotationRateNP, nullptr); return true; } } return Rotator::ISNewNumber(dev, name, values, names, n); } bool Pyxis::ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) { if (dev != nullptr && strcmp(dev, getDeviceName()) == 0) { ///////////////////////////////////////////////////// // Stepping //////////////////////////////////////////////////// if (!strcmp(name, SteppingSP.name)) { bool rc = false; if (!strcmp(IUFindOnSwitchName(states, names, n), SteppingS[FULL_STEP].name)) rc = setSteppingMode(FULL_STEP); else rc = setSteppingMode(HALF_STEP); if (rc) { IUUpdateSwitch(&SteppingSP, states, names, n); SteppingSP.s = IPS_OK; } else SteppingSP.s = IPS_ALERT; IDSetSwitch(&SteppingSP, nullptr); return true; } ///////////////////////////////////////////////////// // Power //////////////////////////////////////////////////// if (!strcmp(name, PowerSP.name)) { bool rc = false; if (!strcmp(IUFindOnSwitchName(states, names, n), PowerS[POWER_WAKEUP].name)) { // If not sleeping if (PowerS[POWER_SLEEP].s == ISS_OFF) { PowerSP.s = IPS_OK; LOG_WARN("Controller is not in sleep mode."); IDSetSwitch(&PowerSP, nullptr); return true; } rc = wakeupController(); if (rc) { IUResetSwitch(&PowerSP); PowerSP.s = IPS_OK; LOG_INFO("Controller is awake."); } else PowerSP.s = IPS_ALERT; IDSetSwitch(&PowerSP, nullptr); return true; } else { bool rc = sleepController(); IUResetSwitch(&PowerSP); if (rc) { PowerSP.s = IPS_OK; PowerS[POWER_SLEEP].s = ISS_ON; LOG_INFO("Controller in sleep mode. No functions can be used until controller is waken up."); } else PowerSP.s = IPS_ALERT; IDSetSwitch(&PowerSP, nullptr); return true; } return true; } } return Rotator::ISNewSwitch(dev, name, states, names, n); } bool Pyxis::setSteppingMode(uint8_t mode) { char cmd[PYRIX_BUF] = {0}; int nbytes_written = 0, rc = -1; char errstr[MAXRBUF]; snprintf(cmd, PYRIX_BUF, "CZ%dxxx", mode); LOGF_DEBUG("CMD <%s>", cmd); tcflush(PortFD, TCIOFLUSH); if ( (rc = tty_write(PortFD, cmd, PYRIX_CMD, &nbytes_written)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("%s: %s.", __FUNCTION__, errstr); return false; } return true; } bool Pyxis::setRotationRate(uint8_t rate) { char cmd[PYRIX_BUF] = {0}; int nbytes_written = 0, rc = -1; char errstr[MAXRBUF]; snprintf(cmd, PYRIX_BUF, "CTxx%02d", rate); LOGF_DEBUG("CMD <%s>", cmd); tcflush(PortFD, TCIOFLUSH); if ( (rc = tty_write(PortFD, cmd, PYRIX_CMD, &nbytes_written)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("%s: %s.", __FUNCTION__, errstr); return false; } tcflush(PortFD, TCIOFLUSH); return true; } bool Pyxis::sleepController() { const char *cmd = "CSLEEP"; int nbytes_written = 0, rc = -1; char errstr[MAXRBUF]; LOGF_DEBUG("CMD <%s>", cmd); tcflush(PortFD, TCIOFLUSH); if ( (rc = tty_write(PortFD, cmd, PYRIX_CMD, &nbytes_written)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("%s: %s.", __FUNCTION__, errstr); return false; } return true; } bool Pyxis::wakeupController() { const char *cmd = "CWAKEUP"; char res[1] = { 0 }; int nbytes_written = 0, nbytes_read = 0, rc = -1; char errstr[MAXRBUF]; LOGF_DEBUG("CMD <%s>", cmd); tcflush(PortFD, TCIOFLUSH); if ( (rc = tty_write(PortFD, cmd, PYRIX_CMD, &nbytes_written)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("%s: %s.", __FUNCTION__, errstr); return false; } if ( (rc = tty_read(PortFD, res, 1, PYXIS_TIMEOUT, &nbytes_read)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("%s error: %s.", __FUNCTION__, errstr); return false; } tcflush(PortFD, TCIOFLUSH); LOGF_DEBUG("RES <%c>", res[0]); return (res[0] == '!'); } IPState Pyxis::HomeRotator() { const char *cmd = "CHOMES"; int nbytes_written = 0, rc = -1; char errstr[MAXRBUF]; LOGF_DEBUG("CMD <%s>", cmd); tcflush(PortFD, TCIOFLUSH); if ( (rc = tty_write(PortFD, cmd, PYRIX_CMD, &nbytes_written)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("%s: %s.", __FUNCTION__, errstr); return IPS_ALERT; } return IPS_BUSY; } IPState Pyxis::MoveRotator(double angle) { char cmd[PYRIX_BUF] = {0}; int nbytes_written = 0, rc = -1; char errstr[MAXRBUF]; targetPA = static_cast(round(angle)); if (targetPA > 359) targetPA = 0; snprintf(cmd, PYRIX_BUF, "CPA%03d", targetPA); LOGF_DEBUG("CMD <%s>", cmd); tcflush(PortFD, TCIOFLUSH); if ( (rc = tty_write(PortFD, cmd, strlen(cmd), &nbytes_written)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("%s: %s.", __FUNCTION__, errstr); return IPS_ALERT; } return IPS_BUSY; } bool Pyxis::ReverseRotator(bool enabled) { char cmd[PYRIX_BUF] = {0}; int nbytes_written = 0, rc = -1; char errstr[MAXRBUF]; snprintf(cmd, PYRIX_BUF, "CD%dxxx", enabled ? 1 : 0); LOGF_DEBUG("CMD <%s>", cmd); tcflush(PortFD, TCIOFLUSH); if ( (rc = tty_write(PortFD, cmd, strlen(cmd), &nbytes_written)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("%s: %s.", __FUNCTION__, errstr); return false; } return true; } void Pyxis::TimerHit() { if (!isConnected() || PowerS[POWER_SLEEP].s == ISS_ON) { SetTimer(POLLMS); return; } if (HomeRotatorSP.s == IPS_BUSY) { if (isMotionComplete()) { HomeRotatorSP.s = IPS_OK; HomeRotatorS[0].s = ISS_OFF; IDSetSwitch(&HomeRotatorSP, nullptr); LOG_INFO("Homing is complete."); } else { // Fast timer SetTimer(POLLMS); return; } } else if (GotoRotatorNP.s == IPS_BUSY) { if (isMotionComplete()) { GotoRotatorNP.s = IPS_OK; } else { // Fast timer SetTimer(POLLMS); return; } //if (PA == targetPA) } uint16_t PA = 0; if (getPA(PA) && (PA != static_cast(GotoRotatorN[0].value))) { GotoRotatorN[0].value = PA; IDSetNumber(&GotoRotatorNP, nullptr); } SetTimer(POLLMS); } bool Pyxis::isMotionComplete() { int nbytes_read = 0, rc = -1; char errstr[MAXRBUF]; char res[256] = { 0 }; if ( (rc = tty_nread_section(PortFD, res, 255, 'F', 1, &nbytes_read)) != TTY_OK) { // '!' motion is not complete yet if (rc == TTY_TIME_OUT) return false; tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("%s error: %s.", __FUNCTION__, errstr); if (HomeRotatorSP.s == IPS_BUSY) { HomeRotatorS[0].s = ISS_OFF; HomeRotatorSP.s = IPS_ALERT; LOG_ERROR("Homing failed. Check possible jam."); tcflush(PortFD, TCIOFLUSH); } return false; } LOGF_DEBUG("RES <%s>", res); return true; } #if 0 bool Pyxis::isMotionComplete() { int nbytes_read = 0, rc = -1; char errstr[MAXRBUF]; char res[1] = { 0 }; if ( (rc = tty_read(PortFD, res, 1, PYXIS_TIMEOUT, &nbytes_read)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("%s error: %s.", __FUNCTION__, errstr); return false; } LOGF_DEBUG("RES <%c>", res[0]); // Homing still in progress if (res[0] == '!') return false; // Homing is complete else if (res[0] == 'F') return true; // Error else if (HomeRotatorSP.s == IPS_BUSY) { HomeRotatorS[0].s = ISS_OFF; HomeRotatorSP.s = IPS_ALERT; LOG_ERROR("Homing failed. Check possible jam."); tcflush(PortFD, TCIOFLUSH); } return false; } #endif bool Pyxis::getPA(uint16_t &PA) { const char *cmd = "CGETPA"; char res[4] = {0}; int nbytes_written = 0, nbytes_read = 0, rc = -1; char errstr[MAXRBUF]; LOGF_DEBUG("CMD <%s>", cmd); tcflush(PortFD, TCIOFLUSH); if ( (rc = tty_write(PortFD, cmd, PYRIX_CMD, &nbytes_written)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("%s: %s.", __FUNCTION__, errstr); return false; } if ( (rc = tty_read(PortFD, res, 3, PYXIS_TIMEOUT, &nbytes_read)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("%s error: %s.", __FUNCTION__, errstr); return false; } tcflush(PortFD, TCIOFLUSH); LOGF_DEBUG("RES <%s>", res); if (res[0] == '!') return false; PA = atoi(res); return true; } int Pyxis::getReverseStatus() { const char *cmd = "CMREAD"; char res[1] = {0}; int nbytes_written = 0, nbytes_read = 0, rc = -1; char errstr[MAXRBUF]; LOGF_DEBUG("CMD <%s>", cmd); tcflush(PortFD, TCIOFLUSH); if ( (rc = tty_write(PortFD, cmd, PYRIX_CMD, &nbytes_written)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("%s: %s.", __FUNCTION__, errstr); return -1; } if ( (rc = tty_read(PortFD, res, 1, PYXIS_TIMEOUT, &nbytes_read)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("%s error: %s.", __FUNCTION__, errstr); return -1; } tcflush(PortFD, TCIOFLUSH); LOGF_DEBUG("RES <%c>", res[0]); // Subtract from '0' to get actual number (0 or 1) return (res[0] - 0x30); } libindi/drivers/rotator/nightcrawler.h0000664000175000017500000001111013263645557017476 0ustar jasemjasem/* NightCrawler NightCrawler Focuser & Rotator Copyright (C) 2017 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 */ #pragma once #include "indifocuser.h" #include "indirotatorinterface.h" class NightCrawler : public INDI::Focuser, public INDI::RotatorInterface { public: typedef enum { MOTOR_FOCUS, MOTOR_ROTATOR, MOTOR_AUX } MotorType; NightCrawler(); virtual ~NightCrawler() = default; virtual bool Handshake(); const char * getDefaultName(); virtual bool initProperties(); virtual bool updateProperties(); virtual bool ISNewNumber (const char * dev, const char * name, double values[], char * names[], int n); virtual bool ISNewSwitch (const char * dev, const char * name, ISState * states, char * names[], int n); protected: // Focuser virtual IPState MoveAbsFocuser(uint32_t targetTicks); virtual IPState MoveRelFocuser(FocusDirection dir, uint32_t ticks); virtual bool AbortFocuser(); // Rotator virtual IPState HomeRotator(); virtual IPState MoveRotator(double angle); virtual bool SyncRotator(double angle); virtual bool AbortRotator(); // Misc. virtual bool saveConfigItems(FILE *fp); virtual void TimerHit(); private: // Get Firmware bool getFirmware(); // Get Focuer Type bool getFocuserType(); // Check if connection is OK bool Ack(); // Goto Position bool gotoMotor(MotorType type, int32_t position); // Get Position bool getPosition(MotorType type); // Sync to Position bool syncMotor(MotorType type, uint32_t position); // Start/Stop Motors bool startMotor(MotorType type); bool stopMotor(MotorType type); bool isMotorMoving(MotorType type); // Sensors (Temperature + Voltage) bool getTemperature(); bool getVoltage(); // Temperature offset bool setTemperatureOffset(double offset); // Motor step rate in 100 microsecond intervals bool getStepDelay(MotorType type); bool setStepDelay(MotorType type, uint32_t delay); // Limit Switch bool getLimitSwitchStatus(); // Home bool findHome(uint8_t motorTypes); bool isHomingComplete(); // Encoders bool setEncodersEnabled(bool enable); // Brightness bool setDisplayBrightness(uint8_t value); bool setSleepBrightness(uint8_t value); INumber GotoAuxN[1]; INumberVectorProperty GotoAuxNP; INumber SyncFocusN[1]; INumberVectorProperty SyncFocusNP; INumber SyncAuxN[1]; INumberVectorProperty SyncAuxNP; ISwitch AbortAuxS[1]; ISwitchVectorProperty AbortAuxSP; INumber SensorN[2]; INumberVectorProperty SensorNP; enum { SENSOR_TEMPERATURE, SENSOR_VOLTAGE }; INumber TemperatureOffsetN[1]; INumberVectorProperty TemperatureOffsetNP; INumber FocusStepDelayN[1]; INumberVectorProperty FocusStepDelayNP; INumber RotatorStepDelayN[1]; INumberVectorProperty RotatorStepDelayNP; INumber AuxStepDelayN[1]; INumberVectorProperty AuxStepDelayNP; ILight LimitSwitchL[3]; ILightVectorProperty LimitSwitchLP; enum { ROTATION_SWITCH, OUT_SWITCH, IN_SWITCH }; ISwitch HomeSelectionS[3]; ISwitchVectorProperty HomeSelectionSP; ISwitch FindHomeS[1]; ISwitchVectorProperty FindHomeSP; ISwitch EncoderS[2]; ISwitchVectorProperty EncoderSP; INumber BrightnessN[2]; INumberVectorProperty BrightnessNP; enum { BRIGHTNESS_DISPLAY, BRIGHTNESS_SLEEP }; // Rotator Steps INumber RotatorAbsPosN[1]; INumberVectorProperty RotatorAbsPosNP; double lastTemperature { 0 }; double lastVoltage { 0 }; double ticksPerDegree { 0 }; uint32_t lastFocuserPosition { 0 }; uint32_t lastRotatorPosition { 0 }; uint32_t lastAuxPosition { 0 }; uint32_t targetPosition { 0 }; IPState rotationLimit { IPS_IDLE }; IPState outSwitchLimit { IPS_IDLE }; IPState inSwitchLimit { IPS_IDLE }; }; libindi/drivers/filter_wheel/0000775000175000017500000000000013263645557015621 5ustar jasemjasemlibindi/drivers/filter_wheel/filter_simulator.h0000664000175000017500000000247013263645557021361 0ustar jasemjasem/******************************************************************************* Copyright(c) 2010 Gerry Rozema. All rights reserved. This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2 as published by the Free Software Foundation. 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 Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. *******************************************************************************/ #pragma once #include "indifilterwheel.h" /** * @brief The FilterSim class provides a simple simulator to change filters. The filter names are saved to a config file when updated. */ class FilterSim : public INDI::FilterWheel { public: FilterSim() = default; virtual ~FilterSim() = default; const char *getDefaultName(); bool Connect(); bool Disconnect(); bool SelectFilter(int); void TimerHit(); }; libindi/drivers/filter_wheel/quantum_wheel.h0000664000175000017500000000244213263645557020652 0ustar jasemjasem/******************************************************************************* Copyright(c) 2016 Radek Kaczorek This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2 as published by the Free Software Foundation. . 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 Library General Public License for more details. . You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. *******************************************************************************/ #pragma once #include "indifilterwheel.h" class QFW : public INDI::FilterWheel { public: QFW(); virtual ~QFW() = default; void debugTriggered(bool enable); void simulationTriggered(bool enable); bool Handshake(); const char *getDefaultName(); bool initProperties(); void ISGetProperties(const char *dev); int QueryFilter(); bool SelectFilter(int); }; libindi/drivers/filter_wheel/trutech_wheel.cpp0000664000175000017500000001254113263645557021172 0ustar jasemjasem/******************************************************************************* Copyright(c) 2017 Jasem Mutlaq. All rights reserved. Tru Technology Filter Wheel This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2 as published by the Free Software Foundation. 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 Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. *******************************************************************************/ #include "trutech_wheel.h" #include "indicom.h" #include #include #define CMD_SIZE 5 //const uint8_t COMM_PRE = 0x01; const uint8_t COMM_INIT = 0xA5; const uint8_t COMM_FILL = 0x20; // We declare an auto pointer to TruTech. std::unique_ptr tru_wheel(new TruTech()); void ISPoll(void *p); void ISGetProperties(const char *dev) { tru_wheel->ISGetProperties(dev); } void ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) { tru_wheel->ISNewSwitch(dev, name, states, names, n); } void ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n) { tru_wheel->ISNewText(dev, name, texts, names, n); } void ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) { tru_wheel->ISNewNumber(dev, name, values, names, n); } void ISNewBLOB(const char *dev, const char *name, int sizes[], int blobsizes[], char *blobs[], char *formats[], char *names[], int n) { INDI_UNUSED(dev); INDI_UNUSED(name); INDI_UNUSED(sizes); INDI_UNUSED(blobsizes); INDI_UNUSED(blobs); INDI_UNUSED(formats); INDI_UNUSED(names); INDI_UNUSED(n); } void ISSnoopDevice(XMLEle *root) { tru_wheel->ISSnoopDevice(root); } TruTech::TruTech() { setFilterConnection(CONNECTION_SERIAL | CONNECTION_TCP); } const char *TruTech::getDefaultName() { return (const char *)"TruTech Wheel"; } bool TruTech::initProperties() { INDI::FilterWheel::initProperties(); IUFillSwitch(&HomeS[0], "Find", "Find", ISS_OFF); IUFillSwitchVector(&HomeSP, HomeS, 1, getDeviceName(), "HOME", "Home", MAIN_CONTROL_TAB, IP_RW, ISR_1OFMANY, 60, IPS_IDLE); CurrentFilter = 1; FilterSlotN[0].min = 1; FilterSlotN[0].max = 5; addAuxControls(); return true; } bool TruTech::updateProperties() { INDI::FilterWheel::updateProperties(); if (isConnected()) defineSwitch(&HomeSP); else deleteProperty(HomeSP.name); return true; } bool TruTech::ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) { if (dev != nullptr && strcmp(dev, getDeviceName()) == 0) { if (strcmp(HomeSP.name, name) == 0) { int rc = 0, nbytes_written = 0; uint8_t type = 0x03; uint8_t chksum = COMM_INIT + type + COMM_FILL; char filter_command[CMD_SIZE]; snprintf(filter_command, CMD_SIZE, "%c%c%c%c", COMM_INIT, type, COMM_FILL, chksum); LOGF_DEBUG("CMD: %#02X %#02X %#02X %#02X", COMM_INIT, type, COMM_FILL, chksum); if (!isSimulation() && (rc = tty_write(PortFD, filter_command, CMD_SIZE, &nbytes_written)) != TTY_OK) { char error_message[ERRMSG_SIZE]; tty_error_msg(rc, error_message, ERRMSG_SIZE); HomeSP.s = IPS_ALERT; LOGF_ERROR("Sending command Home to filter failed: %s", error_message); } else { CurrentFilter = 1; FilterSlotN[0].value = 1; FilterSlotNP.s = IPS_OK; HomeSP.s = IPS_OK; LOG_INFO("Filter set to Home."); IDSetNumber(&FilterSlotNP, nullptr); } IDSetSwitch(&HomeSP, nullptr); return true; } } return INDI::FilterWheel::ISNewSwitch(dev, name, states, names, n); } bool TruTech::Handshake() { // How do we do handshake with TruTech? We return true for now return true; } bool TruTech::SelectFilter(int f) { TargetFilter = f; int rc = 0, nbytes_written = 0; char filter_command[CMD_SIZE]; uint8_t type = 0x01; uint8_t chksum = COMM_INIT + type + static_cast(f); snprintf(filter_command, CMD_SIZE, "%c%c%c%c", COMM_INIT, type, f, chksum); LOGF_DEBUG("CMD: %#02X %#02X %#02X %#02X", COMM_INIT, type, f, chksum); if (!isSimulation() && (rc = tty_write(PortFD, filter_command, CMD_SIZE, &nbytes_written)) != TTY_OK) { char error_message[ERRMSG_SIZE]; tty_error_msg(rc, error_message, ERRMSG_SIZE); LOGF_ERROR("Sending command select filter failed: %s", error_message); return false; } // How do we check on TruTech if filter arrived? Check later CurrentFilter = f; SelectFilterDone(CurrentFilter); return true; } void TruTech::TimerHit() { // Maybe needed later? } libindi/drivers/filter_wheel/quantum_wheel.cpp0000664000175000017500000001020513263645557021201 0ustar jasemjasem/******************************************************************************* Copyright(c) 2016 Radek Kaczorek This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2 as published by the Free Software Foundation. . 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 Library General Public License for more details. . You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. *******************************************************************************/ #include "quantum_wheel.h" #include "connectionplugins/connectionserial.h" #include #include #include #define VERSION_MAJOR 0 #define VERSION_MINOR 2 std::unique_ptr qfw(new QFW()); void ISGetProperties(const char *dev) { qfw->ISGetProperties(dev); } void ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) { qfw->ISNewSwitch(dev, name, states, names, n); } void ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n) { qfw->ISNewText(dev, name, texts, names, n); } void ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) { qfw->ISNewNumber(dev, name, values, names, n); } void ISNewBLOB(const char *dev, const char *name, int sizes[], int blobsizes[], char *blobs[], char *formats[], char *names[], int n) { INDI_UNUSED(dev); INDI_UNUSED(name); INDI_UNUSED(sizes); INDI_UNUSED(blobsizes); INDI_UNUSED(blobs); INDI_UNUSED(formats); INDI_UNUSED(names); INDI_UNUSED(n); } void ISSnoopDevice(XMLEle *root) { qfw->ISSnoopDevice(root); } QFW::QFW() { setDeviceName(QFW::getDefaultName()); setVersion(VERSION_MAJOR, VERSION_MINOR); setFilterConnection(CONNECTION_SERIAL | CONNECTION_TCP); } void QFW::debugTriggered(bool enable) { INDI_UNUSED(enable); } void QFW::simulationTriggered(bool enable) { INDI_UNUSED(enable); } const char *QFW::getDefaultName() { return (const char *)"Quantum Wheel"; } bool QFW::Handshake() { if (isSimulation()) { IDMessage(getDeviceName(), "Simulation: connected"); PortFD = 1; } else { // check serial connection if (PortFD < 0 || isatty(PortFD) == 0) { IDMessage(getDeviceName(), "Device /dev/ttyACM0 is not available\n"); return false; } } return true; } bool QFW::initProperties() { INDI::FilterWheel::initProperties(); // addDebugControl(); addSimulationControl(); serialConnection->setDefaultPort("/dev/ttyACM0"); FilterSlotN[0].min = 1; FilterSlotN[0].max = 7; CurrentFilter = 1; return true; } void QFW::ISGetProperties(const char *dev) { INDI::FilterWheel::ISGetProperties(dev); } int QFW::QueryFilter() { return CurrentFilter; } bool QFW::SelectFilter(int position) { // count from 0 to 6 for positions 1 to 7 position = position - 1; if (position < 0 || position > 6) return false; if (isSimulation()) { CurrentFilter = position + 1; SelectFilterDone(CurrentFilter); return true; } // goto char targetpos[255]={0}; char curpos[255]={0}; int res; // format target position G[0-6] sprintf(targetpos, "G%d\r\n\n", position); // write command res = write(PortFD, targetpos, strlen(targetpos)); // format target marker P[0-6] sprintf(targetpos, "P%d\r\n", position); // check current position do { usleep(100 * 1000); res = read(PortFD, curpos, 255); curpos[res] = 0; } while (strncmp(targetpos, curpos, 2) != 0); // return current position to indi CurrentFilter = position + 1; SelectFilterDone(CurrentFilter); return true; } libindi/drivers/filter_wheel/xagyl_wheel.cpp0000664000175000017500000005620013263645557020640 0ustar jasemjasem/******************************************************************************* Copyright(c) 2015 Jasem Mutlaq. All rights reserved. This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2 as published by the Free Software Foundation. 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 Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. *******************************************************************************/ #include "xagyl_wheel.h" #include "indicom.h" #include #include #include #define XAGYL_MAXBUF 32 #define SETTINGS_TAB "Settings" // We declare an auto pointer to XAGYLWheel. std::unique_ptr xagylWheel(new XAGYLWheel()); void ISPoll(void *p); void ISGetProperties(const char *dev) { xagylWheel->ISGetProperties(dev); } void ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) { xagylWheel->ISNewSwitch(dev, name, states, names, n); } void ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n) { xagylWheel->ISNewText(dev, name, texts, names, n); } void ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) { xagylWheel->ISNewNumber(dev, name, values, names, n); } void ISNewBLOB(const char *dev, const char *name, int sizes[], int blobsizes[], char *blobs[], char *formats[], char *names[], int n) { INDI_UNUSED(dev); INDI_UNUSED(name); INDI_UNUSED(sizes); INDI_UNUSED(blobsizes); INDI_UNUSED(blobs); INDI_UNUSED(formats); INDI_UNUSED(names); INDI_UNUSED(n); } void ISSnoopDevice(XMLEle *root) { xagylWheel->ISSnoopDevice(root); } XAGYLWheel::XAGYLWheel() { simData.position = 1; simData.speed = 0xA; simData.pulseWidth = 1500; simData.threshold = 30; simData.jitter = 1; simData.offset[0] = simData.offset[1] = simData.offset[2] = simData.offset[3] = simData.offset[4] = 0; strncpy(simData.product, "Xagyl FW5125VX", 16); strncpy(simData.version, "FW3.1.5", 16); strncpy(simData.serial, "S/N: 123456", 16); setVersion(0, 2); setFilterConnection(CONNECTION_SERIAL | CONNECTION_TCP); setDefaultPollingPeriod(500); } XAGYLWheel::~XAGYLWheel() { delete[] OffsetN; } const char *XAGYLWheel::getDefaultName() { return (const char *)"XAGYL Wheel"; } bool XAGYLWheel::initProperties() { INDI::FilterWheel::initProperties(); // Firmware info IUFillText(&FirmwareInfoT[0], "Product", "Product", nullptr); IUFillText(&FirmwareInfoT[1], "Firmware", "Firmware", nullptr); IUFillText(&FirmwareInfoT[2], "Serial #", "Serial #", nullptr); IUFillTextVector(&FirmwareInfoTP, FirmwareInfoT, 3, getDeviceName(), "Info", "Info", MAIN_CONTROL_TAB, IP_RO, 60, IPS_IDLE); // Settings IUFillNumber(&SettingsN[0], "Speed", "Speed", "%.f", 0, 100, 10., 0.); IUFillNumber(&SettingsN[1], "Jitter", "Jitter", "%.f", 0, 10, 1., 0.); IUFillNumber(&SettingsN[2], "Threshold", "Threshold", "%.f", 0, 100, 10., 0.); IUFillNumber(&SettingsN[3], "Pulse Width", "Pulse", "%.f", 100, 10000, 100., 0.); IUFillNumberVector(&SettingsNP, SettingsN, 4, getDeviceName(), "Settings", "Settings", SETTINGS_TAB, IP_RW, 0, IPS_IDLE); // Reset IUFillSwitch(&ResetS[0], "Reboot", "Reboot", ISS_OFF); IUFillSwitch(&ResetS[1], "Initialize", "Initialize", ISS_OFF); IUFillSwitch(&ResetS[2], "Clear Calibration", "Clear Calibration", ISS_OFF); IUFillSwitch(&ResetS[3], "Perform Calibration", "Perform Calibration", ISS_OFF); IUFillSwitchVector(&ResetSP, ResetS, 4, getDeviceName(), "Commands", "Commands", MAIN_CONTROL_TAB, IP_RW, ISR_ATMOST1, 0, IPS_IDLE); addAuxControls(); return true; } bool XAGYLWheel::updateProperties() { INDI::FilterWheel::updateProperties(); if (isConnected()) { getStartupData(); defineSwitch(&ResetSP); defineNumber(&OffsetNP); defineText(&FirmwareInfoTP); defineNumber(&SettingsNP); } else { deleteProperty(ResetSP.name); deleteProperty(OffsetNP.name); deleteProperty(FirmwareInfoTP.name); deleteProperty(SettingsNP.name); } return true; } bool XAGYLWheel::Handshake() { char resp[XAGYL_MAXBUF]; bool rc = getCommand(INFO_FIRMWARE_VERSION, resp); if (rc) { int fwver = 0; int fw_rc = sscanf(resp, "%d", &fwver); if (fw_rc != 1) fw_rc = sscanf(resp, "FW %d", &fwver); if (fw_rc > 0) { firmwareVersion = fwver; // We don't have pulse width for version < 3 if (firmwareVersion < 3) SettingsNP.nnp--; if (getMaxFilterSlots()) { initOffset(); LOG_INFO("XAGYL is online. Getting filter parameters..."); return true; } } else LOGF_ERROR("Unable to parse (%s)", resp); } LOG_INFO("Error retreiving data from XAGYL Filter Wheel, please ensure filter wheel is " "powered and the port is correct."); return false; } bool XAGYLWheel::ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) { if (dev != nullptr && strcmp(dev, getDeviceName()) == 0) { if (strcmp(ResetSP.name, name) == 0) { IUUpdateSwitch(&ResetSP, states, names, n); int value = IUFindOnSwitchIndex(&ResetSP); IUResetSwitch(&ResetSP); if (value == 3) value = 6; bool rc = reset(value); if (rc) { switch (value) { case 0: LOG_INFO("Executing hard reboot..."); break; case 1: LOG_INFO("Restarting and moving to filter position #1..."); break; case 2: LOG_INFO("Calibration removed."); break; case 6: LOG_INFO("Calibrating..."); break; } } ResetSP.s = rc ? IPS_OK : IPS_ALERT; IDSetSwitch(&ResetSP, nullptr); return true; } } return INDI::FilterWheel::ISNewSwitch(dev, name, states, names, n); } bool XAGYLWheel::ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) { if (dev != nullptr && strcmp(dev, getDeviceName()) == 0) { if (strcmp(OffsetNP.name, name) == 0) { bool rc_offset = true; for (int i = 0; i < n; i++) { if (strcmp(names[i], OffsetN[i].name) == 0) { while (values[i] != OffsetN[i].value && rc_offset) { if (values[i] > OffsetN[i].value) rc_offset = setOffset(i, 1); else rc_offset = setOffset(i, -1); } } } OffsetNP.s = rc_offset ? IPS_OK : IPS_ALERT; IDSetNumber(&OffsetNP, nullptr); return true; } if (strcmp(SettingsNP.name, name) == 0) { double newSpeed = 0, newJitter = 0, newThreshold = 0, newPulseWidth = 0; for (int i = 0; i < n; i++) { if (strcmp(names[i], SettingsN[SET_SPEED].name) == 0) newSpeed = values[i]; else if (strcmp(names[i], SettingsN[SET_JITTER].name) == 0) newJitter = values[i]; if (strcmp(names[i], SettingsN[SET_THRESHOLD].name) == 0) newThreshold = values[i]; if (strcmp(names[i], SettingsN[SET_PULSE_WITDH].name) == 0) newPulseWidth = values[i]; } bool rc_speed = true, rc_jitter = true, rc_threshold = true, rc_pulsewidth = true; if (newSpeed != SettingsN[SET_SPEED].value) { rc_speed = setCommand(SET_SPEED, newSpeed); getMaximumSpeed(); } // Jitter while (newJitter != SettingsN[SET_JITTER].value && rc_jitter) { if (newJitter > SettingsN[SET_JITTER].value) { rc_jitter &= setCommand(SET_JITTER, 1); getJitter(); } else { rc_jitter &= setCommand(SET_JITTER, -1); getJitter(); } } // Threshold while (newThreshold != SettingsN[SET_THRESHOLD].value && rc_threshold) { if (newThreshold > SettingsN[SET_THRESHOLD].value) { rc_threshold &= setCommand(SET_THRESHOLD, 1); getThreshold(); } else { rc_threshold &= setCommand(SET_THRESHOLD, -1); getThreshold(); } } // Pulse width while (firmwareVersion >= 3 && newPulseWidth != SettingsN[SET_PULSE_WITDH].value && rc_pulsewidth) { if (newPulseWidth > SettingsN[SET_PULSE_WITDH].value) { rc_pulsewidth &= setCommand(SET_PULSE_WITDH, 1); getPulseWidth(); } else { rc_pulsewidth &= setCommand(SET_PULSE_WITDH, -1); getPulseWidth(); } } if (rc_speed && rc_jitter && rc_threshold && rc_pulsewidth) SettingsNP.s = IPS_OK; else SettingsNP.s = IPS_ALERT; IDSetNumber(&SettingsNP, nullptr); return true; } } return INDI::FilterWheel::ISNewNumber(dev, name, values, names, n); } void XAGYLWheel::initOffset() { delete [] OffsetN; OffsetN = new INumber[static_cast(FilterSlotN[0].max)]; char offsetName[MAXINDINAME], offsetLabel[MAXINDILABEL]; for (int i = 0; i < FilterSlotN[0].max; i++) { snprintf(offsetName, MAXINDINAME, "OFFSET_%d", i + 1); snprintf(offsetLabel, MAXINDINAME, "#%d Offset", i + 1); IUFillNumber(OffsetN + i, offsetName, offsetLabel, "%.f", -99, 99, 10, 0); } IUFillNumberVector(&OffsetNP, OffsetN, FilterSlotN[0].max, getDeviceName(), "Offsets", "", FILTER_TAB, IP_RW, 0, IPS_IDLE); } bool XAGYLWheel::getCommand(GET_COMMAND cmd, char *result) { int nbytes_written = 0, nbytes_read = 0, rc = -1; char errstr[MAXRBUF]; char command[XAGYL_MAXBUF]; tcflush(PortFD, TCIOFLUSH); snprintf(command, XAGYL_MAXBUF, "I%d", cmd); LOGF_DEBUG("CMD <%s>", command); if (!isSimulation() && (rc = tty_write(PortFD, command, strlen(command), &nbytes_written)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("%s error: %s.", command, errstr); return false; } if (isSimulation()) { switch (cmd) { case INFO_PRODUCT_NAME: snprintf(result, XAGYL_MAXBUF, "%s", simData.product); break; case INFO_FIRMWARE_VERSION: snprintf(result, XAGYL_MAXBUF, "%s", simData.version); break; case INFO_SERIAL_NUMBER: snprintf(result, XAGYL_MAXBUF, "%s", simData.serial); break; case INFO_FILTER_POSITION: snprintf(result, XAGYL_MAXBUF, "P%d", simData.position); break; case INFO_MAX_SPEED: snprintf(result, XAGYL_MAXBUF, "MaxSpeed %02d%%", simData.speed * 10); break; case INFO_JITTER: snprintf(result, XAGYL_MAXBUF, "Jitter %d", simData.jitter); break; case INFO_OFFSET: snprintf(result, XAGYL_MAXBUF, "P%d Offset %02d", CurrentFilter, simData.offset[CurrentFilter - 1]); break; case INFO_THRESHOLD: snprintf(result, XAGYL_MAXBUF, "Threshold %02d", simData.threshold); break; case INFO_MAX_SLOTS: snprintf(result, XAGYL_MAXBUF, "FilterSlots %d", 5); break; case INFO_PULSE_WIDTH: snprintf(result, XAGYL_MAXBUF, "Pulse Width %05duS", simData.pulseWidth); break; } } else { if ((rc = tty_read_section(PortFD, result, 0xA, XAGYL_MAXBUF, &nbytes_read)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("%s: %s.", command, errstr); return false; } result[nbytes_read - 1] = '\0'; } LOGF_DEBUG("RES <%s>", result); return true; } bool XAGYLWheel::setCommand(SET_COMMAND cmd, int value) { int nbytes_written = 0, nbytes_read = 0, rc = -1; char errstr[MAXRBUF]; char command[XAGYL_MAXBUF]; tcflush(PortFD, TCIOFLUSH); switch (cmd) { case SET_SPEED: snprintf(command, XAGYL_MAXBUF, "S%X", value / 10); break; case SET_JITTER: snprintf(command, XAGYL_MAXBUF, "%s0", value > 0 ? "]" : "["); break; case SET_THRESHOLD: snprintf(command, XAGYL_MAXBUF, "%s0", value > 0 ? "}" : "{"); break; case SET_PULSE_WITDH: snprintf(command, XAGYL_MAXBUF, "%s0", value > 0 ? "M" : "N"); break; case SET_POSITION: snprintf(command, XAGYL_MAXBUF, "G%X", value); break; } LOGF_DEBUG("CMD <%s>", command); if (!isSimulation() && (rc = tty_write(PortFD, command, strlen(command), &nbytes_written)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("%s error: %s.", command, errstr); return false; } // Commands that have no reply switch (cmd) { case SET_POSITION: simData.position = value; return true; break; default: break; } char response[XAGYL_MAXBUF]; if (isSimulation()) { switch (cmd) { case SET_SPEED: simData.speed = value / 10; snprintf(response, XAGYL_MAXBUF, "Speed=%3d%%", simData.speed * 10); break; case SET_JITTER: simData.jitter += (value > 0 ? 1 : -1); if (simData.jitter > SettingsN[SET_JITTER].max) simData.jitter = SettingsN[SET_JITTER].max; else if (simData.jitter < SettingsN[SET_JITTER].min) simData.jitter = SettingsN[SET_JITTER].min; snprintf(response, XAGYL_MAXBUF, "Jitter %d", simData.jitter); break; case SET_THRESHOLD: simData.threshold += (value > 0 ? 1 : -1); if (simData.threshold > SettingsN[SET_THRESHOLD].max) simData.threshold = SettingsN[SET_THRESHOLD].max; else if (simData.threshold < SettingsN[SET_THRESHOLD].min) simData.threshold = SettingsN[SET_THRESHOLD].min; snprintf(response, XAGYL_MAXBUF, "Threshold %d", simData.threshold); break; case SET_PULSE_WITDH: simData.pulseWidth += 100 * (value > 0 ? 1 : -1); if (simData.pulseWidth > SettingsN[SET_PULSE_WITDH].max) simData.pulseWidth = SettingsN[SET_PULSE_WITDH].max; else if (simData.pulseWidth < SettingsN[SET_PULSE_WITDH].min) simData.pulseWidth = SettingsN[SET_PULSE_WITDH].min; snprintf(response, XAGYL_MAXBUF, "pulseWidth %d", simData.pulseWidth); break; default: break; } } else if ((rc = tty_read_section(PortFD, response, 0xA, XAGYL_MAXBUF, &nbytes_read)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("%s: %s.", command, errstr); return false; } LOGF_DEBUG("RES <%s>", response); return true; } bool XAGYLWheel::SelectFilter(int f) { TargetFilter = f; bool rc = setCommand(SET_POSITION, f); if (rc) { SetTimer(POLLMS); return true; } else return false; } void XAGYLWheel::TimerHit() { bool rc = getFilterPosition(); if (rc == false) { SetTimer(POLLMS); return; } if (CurrentFilter == TargetFilter) SelectFilterDone(CurrentFilter); else SetTimer(POLLMS); } bool XAGYLWheel::getStartupData() { bool rc1 = getFirmwareInfo(); bool rc2 = getSettingInfo(); for (int i = 0; i < OffsetNP.nnp; i++) getOffset(i); return (rc1 && rc2); } bool XAGYLWheel::getFirmwareInfo() { char resp[XAGYL_MAXBUF]; bool rc1 = getCommand(INFO_PRODUCT_NAME, resp); if (rc1) IUSaveText(&FirmwareInfoT[0], resp); bool rc2 = getCommand(INFO_FIRMWARE_VERSION, resp); if (rc2) IUSaveText(&FirmwareInfoT[1], resp); bool rc3 = getCommand(INFO_SERIAL_NUMBER, resp); if (rc3) IUSaveText(&FirmwareInfoT[2], resp); return (rc1 && rc2 && rc3); } bool XAGYLWheel::getSettingInfo() { bool rc1 = getMaximumSpeed(); bool rc2 = getJitter(); bool rc3 = getThreshold(); bool rc4 = true; if (firmwareVersion >= 3) rc4 = getPulseWidth(); return (rc1 && rc2 && rc3 && rc4); } bool XAGYLWheel::getFilterPosition() { char resp[XAGYL_MAXBUF]; if (!getCommand(INFO_FILTER_POSITION, resp)) return false; int rc = sscanf(resp, "P%d", &CurrentFilter); if (rc > 0) { FilterSlotN[0].value = CurrentFilter; return true; } else return false; } bool XAGYLWheel::getMaximumSpeed() { char resp[XAGYL_MAXBUF]; if (!getCommand(INFO_MAX_SPEED, resp)) return false; int maxSpeed = 0; int rc = sscanf(resp, "MaxSpeed %d%%", &maxSpeed); if (rc > 0) { SettingsN[SET_SPEED].value = maxSpeed; return true; } return false; } bool XAGYLWheel::getJitter() { char resp[XAGYL_MAXBUF]; if (!getCommand(INFO_JITTER, resp)) return false; int jitter = 0; int rc = sscanf(resp, "Jitter %d", &jitter); if (rc > 0) { SettingsN[SET_JITTER].value = jitter; return true; } return false; } bool XAGYLWheel::getThreshold() { char resp[XAGYL_MAXBUF]; if (!getCommand(INFO_THRESHOLD, resp)) return false; int threshold = 0; int rc = sscanf(resp, "Threshold %d", &threshold); if (rc > 0) { SettingsN[SET_THRESHOLD].value = threshold; return true; } return false; } bool XAGYLWheel::getPulseWidth() { char resp[XAGYL_MAXBUF]; if (!getCommand(INFO_PULSE_WIDTH, resp)) return false; int pulseWidth = 0; int rc = sscanf(resp, "Pulse Width %duS", &pulseWidth); if (rc > 0) { SettingsN[SET_PULSE_WITDH].value = pulseWidth; return true; } return false; } bool XAGYLWheel::getMaxFilterSlots() { char resp[XAGYL_MAXBUF]; if (!getCommand(INFO_MAX_SLOTS, resp)) return false; int maxFilterSlots = 0; int rc = sscanf(resp, "FilterSlots %d", &maxFilterSlots); if (rc > 0) { FilterSlotN[0].max = maxFilterSlots; return true; } return false; } bool XAGYLWheel::reset(int value) { int nbytes_written = 0, rc = -1; char errstr[MAXRBUF]; char command[XAGYL_MAXBUF]; tcflush(PortFD, TCIOFLUSH); snprintf(command, XAGYL_MAXBUF, "R%d", value); LOGF_DEBUG("CMD (%s)", command); if (!isSimulation() && (rc = tty_write(PortFD, command, strlen(command), &nbytes_written)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("%s error: %s.", command, errstr); return false; } if (value == 1) simData.position = 1; getFilterPosition(); return true; } bool XAGYLWheel::setOffset(int filter, int value) { int nbytes_written = 0, nbytes_read = 0, rc = -1; char errstr[MAXRBUF]; char command[XAGYL_MAXBUF]; char resp[XAGYL_MAXBUF]; tcflush(PortFD, TCIOFLUSH); snprintf(command, XAGYL_MAXBUF, "%s", value > 0 ? "(" : ")"); LOGF_DEBUG("CMD (%s)", command); if (!isSimulation() && (rc = tty_write(PortFD, command, strlen(command), &nbytes_written)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("%s error: %s.", command, errstr); return false; } if (isSimulation()) { simData.offset[filter] += value; snprintf(resp, XAGYL_MAXBUF, "P%d Offset %02d", filter + 1, simData.offset[filter]); } else if ((rc = tty_read_section(PortFD, resp, 0xA, XAGYL_MAXBUF, &nbytes_read)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("%s: %s.", command, errstr); return false; } LOGF_DEBUG("RES (%s)", resp); int filter_num = 0, offset = 0; rc = sscanf(resp, "P%d Offset %d", &filter_num, &offset); if (rc > 0) { OffsetN[filter_num - 1].value = offset; return true; } else return false; } bool XAGYLWheel::getOffset(int filter) { int nbytes_written = 0, nbytes_read = 0, rc = -1; char errstr[MAXRBUF]; char command[XAGYL_MAXBUF]; char resp[XAGYL_MAXBUF]; tcflush(PortFD, TCIOFLUSH); snprintf(command, XAGYL_MAXBUF, "O%d", filter + 1); LOGF_DEBUG("CMD (%s)", command); if (!isSimulation() && (rc = tty_write(PortFD, command, strlen(command), &nbytes_written)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("%s error: %s.", command, errstr); return false; } if (isSimulation()) snprintf(resp, XAGYL_MAXBUF, "P%d Offset %02d", filter + 1, simData.offset[filter]); else { if ((rc = tty_read_section(PortFD, resp, 0xA, XAGYL_MAXBUF, &nbytes_read)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("%s: %s.", command, errstr); return false; } resp[nbytes_read - 1] = '\0'; } LOGF_DEBUG("RES (%s)", resp); int filter_num = 0, offset = 0; rc = sscanf(resp, "P%d Offset %d", &filter_num, &offset); if (rc > 0) { OffsetN[filter_num - 1].value = offset; return true; } else return false; } bool XAGYLWheel::saveConfigItems(FILE *fp) { INDI::FilterWheel::saveConfigItems(fp); IUSaveConfigNumber(fp, &SettingsNP); if (OffsetN != nullptr) IUSaveConfigNumber(fp, &OffsetNP); return true; } libindi/drivers/filter_wheel/filter_simulator.cpp0000664000175000017500000000464613263645557021723 0ustar jasemjasem/******************************************************************************* Copyright(c) 2010 Gerry Rozema. All rights reserved. This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2 as published by the Free Software Foundation. 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 Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. *******************************************************************************/ #include "filter_simulator.h" #include // We declare an auto pointer to FilterSim. std::unique_ptr filter_sim(new FilterSim()); void ISPoll(void *p); void ISGetProperties(const char *dev) { filter_sim->ISGetProperties(dev); } void ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) { filter_sim->ISNewSwitch(dev, name, states, names, n); } void ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n) { filter_sim->ISNewText(dev, name, texts, names, n); } void ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) { filter_sim->ISNewNumber(dev, name, values, names, n); } void ISNewBLOB(const char *dev, const char *name, int sizes[], int blobsizes[], char *blobs[], char *formats[], char *names[], int n) { INDI_UNUSED(dev); INDI_UNUSED(name); INDI_UNUSED(sizes); INDI_UNUSED(blobsizes); INDI_UNUSED(blobs); INDI_UNUSED(formats); INDI_UNUSED(names); INDI_UNUSED(n); } void ISSnoopDevice(XMLEle *root) { filter_sim->ISSnoopDevice(root); } const char *FilterSim::getDefaultName() { return (const char *)"Filter Simulator"; } bool FilterSim::Connect() { CurrentFilter = 1; FilterSlotN[0].min = 1; FilterSlotN[0].max = 8; return true; } bool FilterSim::Disconnect() { return true; } bool FilterSim::SelectFilter(int f) { CurrentFilter = f; SetTimer(500); return true; } void FilterSim::TimerHit() { SelectFilterDone(CurrentFilter); } libindi/drivers/filter_wheel/ifwoptec.cpp0000664000175000017500000010365313263645557020155 0ustar jasemjasem/******************************************************************************* Copyright(c) 2016 Philippe Besson. All rights reserved. This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2 as published by the Free Software Foundation. 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 Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. *******************************************************************************/ #include "ifwoptec.h" #include "indicom.h" #include "indicontroller.h" #include "connectionplugins/connectionserial.h" #include #include #include #include std::unique_ptr filter_ifw(new FilterIFW()); /************************************************************************************ * ************************************************************************************/ void ISInit() { static int isInit = 0; if (isInit == 1) return; isInit = 1; if (filter_ifw.get() == nullptr) filter_ifw.reset(new FilterIFW()); } /************************************************************************************ * ************************************************************************************/ void ISGetProperties(const char *dev) { ISInit(); filter_ifw->ISGetProperties(dev); } /************************************************************************************ * ************************************************************************************/ void ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) { ISInit(); filter_ifw->ISNewSwitch(dev, name, states, names, n); } /************************************************************************************ * ************************************************************************************/ void ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n) { ISInit(); filter_ifw->ISNewText(dev, name, texts, names, n); } /************************************************************************************ * ************************************************************************************/ void ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) { ISInit(); filter_ifw->ISNewNumber(dev, name, values, names, n); } /************************************************************************************ * ************************************************************************************/ void ISNewBLOB(const char *dev, const char *name, int sizes[], int blobsizes[], char *blobs[], char *formats[], char *names[], int n) { INDI_UNUSED(dev); INDI_UNUSED(name); INDI_UNUSED(sizes); INDI_UNUSED(blobsizes); INDI_UNUSED(blobs); INDI_UNUSED(formats); INDI_UNUSED(names); INDI_UNUSED(n); } /************************************************************************************ * ************************************************************************************/ void ISSnoopDevice(XMLEle *root) { filter_ifw->ISSnoopDevice(root); } /************************************************************************************ * ************************************************************************************/ FilterIFW::FilterIFW() { //ctor setVersion(VERSION, SUBVERSION); strncpy(filterSim, filterSim5, sizeof(filterSim)); // For simulation mode // Set communication to serail only and avoid driver crash at starting up setFilterConnection(CONNECTION_SERIAL); // We add an additional debug level so we can log verbose member function starting // DBG_TAG is used by macro DEBUGTAG() define in ifwoptec.h int DBG_TAG = 0; DBG_TAG = INDI::Logger::getInstance().addDebugLevel("Function tag", "Tag"); } /************************************************************************************ * ************************************************************************************/ const char *FilterIFW::getDefaultName() { return (const char *)"Optec IFW"; } /************************************************************************************** * ***************************************************************************************/ bool FilterIFW::initProperties() { INDI::FilterWheel::initProperties(); // Settings IUFillText(&WheelIDT[0], "ID", "ID", "-"); IUFillTextVector(&WheelIDTP, WheelIDT, 1, getDeviceName(), "WHEEL_ID", "Wheel", FILTER_TAB, IP_RO, 60, IPS_IDLE); // Command IUFillSwitch(&HomeS[0], "HOME", "Home", ISS_OFF); IUFillSwitchVector(&HomeSP, HomeS, 1, getDeviceName(), "HOME", "Home", MAIN_CONTROL_TAB, IP_RW, ISR_1OFMANY, 0, IPS_IDLE); // Within simulation mode, provide possibilities to select the kind of filter wheel: 5 or 8 filters IUFillSwitch(&FilterNbrS[0], "VAL5", "5", ISS_ON); IUFillSwitch(&FilterNbrS[1], "VAL6", "6", ISS_OFF); IUFillSwitch(&FilterNbrS[2], "VAL8", "8", ISS_OFF); IUFillSwitch(&FilterNbrS[3], "VAL9", "9", ISS_OFF); IUFillSwitchVector(&FilterNbrSP, FilterNbrS, 4, getDeviceName(), "FILTER_NBR", "Filter nbr", FILTER_TAB, IP_RW, ISR_1OFMANY, 0, IPS_IDLE); // User could choice to unrestrict chars set to set the filternames if he accepts to have crazy display name on IFW box // Within simulation mode, provide possibilities to select the kind of filter wheel: 5 or 8 filters IUFillSwitch(&CharSetS[0], "RES", "Restricted", ISS_ON); IUFillSwitch(&CharSetS[1], "UNRES", "All", ISS_OFF); IUFillSwitchVector(&CharSetSP, CharSetS, 2, getDeviceName(), "CHARSET", "Chars allowed", FILTER_TAB, IP_RW, ISR_1OFMANY, 0, IPS_IDLE); // Firmware of the IFW IUFillText(&FirmwareT[0], "FIRMWARE", "Firmware", "Unknown"); IUFillTextVector(&FirmwareTP, FirmwareT, 1, getDeviceName(), "FIRMWARE_ID", "IFW", FILTER_TAB, IP_RO, 60, IPS_IDLE); serialConnection->setDefaultBaudRate(Connection::Serial::B_19200); addAuxControls(); return true; } /************************************************************************************ * ************************************************************************************/ bool FilterIFW::updateProperties() { if (isConnected()) { defineSwitch(&HomeSP); defineText(&FirmwareTP); defineText(&WheelIDTP); // ID of the wheel first in Filter tab page if (isSimulation()) defineSwitch( &FilterNbrSP); // Then the button only for Simulation to select the number of filter of the Wheel (5 or 8) defineSwitch(&CharSetSP); defineNumber(&FilterSlotNP); controller->updateProperties(); GetFirmware(); // Try to get Firmware version of the IFW. NOt all Firmware support this function moveHome(); // Initialisation of the physical IFW } else { deleteProperty(HomeSP.name); deleteProperty(FirmwareTP.name); deleteProperty(WheelIDTP.name); deleteProperty(CharSetSP.name); deleteProperty(FilterNbrSP.name); deleteProperty(FilterSlotNP.name); deleteProperty(FilterNameTP->name); controller->updateProperties(); } return true; } /************************************************************************************ * ************************************************************************************/ bool FilterIFW::WriteTTY(char *command) { char cmd[OPTEC_MAXLEN_CMD]; int errcode = 0; char errmsg[MAXRBUF]; int nbytes_written = 0; snprintf(cmd, OPTEC_MAXLEN_CMD, "%s%s", command, "\n\r"); LOGF_DEBUG("CMD (%s)", cmd); if (!isSimulation()) { if ((errcode = tty_write(PortFD, cmd, strlen(cmd), &nbytes_written)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); LOGF_ERROR("%s", errmsg); return false; } } return true; } /************************************************************************************ * ************************************************************************************/ bool FilterIFW::ReadTTY(char *resp, char *simulation, int timeout) { int errcode = 0; char errmsg[MAXRBUF]; char response[OPTEC_MAXLEN_RESP + 1]; int nbytes_read = 0; memset(response, 0, sizeof(response)); if (isSimulation()) { strncpy(response, simulation, sizeof(response)); nbytes_read = strlen(response) + 2; // +2 for simulation = "\n\r" see below } else { if ((errcode = tty_read_section(PortFD, response, 0xd, timeout, &nbytes_read)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); LOGF_ERROR("%s() TTY error: %s", __FUNCTION__, "errmsg"); return false; } } if (nbytes_read <= 0) { LOG_ERROR("Controller error: Nothing returned by the IFW"); response[0] = '\0'; return false; } response[nbytes_read - 2] = '\0'; //Remove control char from string (\n\r) LOGF_DEBUG("RES (%s)", response); strncpy(resp, response, sizeof(response)); return true; } /************************************************************************************ * ************************************************************************************/ bool FilterIFW::Handshake() { char response[OPTEC_MAXLEN_RESP + 1]; memset(response, 0, sizeof(response)); if (!WriteTTY((char *)"WSMODE")) { LOGF_ERROR("(Function %s()) failed to write to TTY", __FUNCTION__); return false; } if (!ReadTTY(response, (char *)"!", OPTEC_TIMEOUT)) { LOGF_ERROR("(Function %s()) failed to read to TTY", __FUNCTION__); return false; } if (strcmp(response, "!") != 0) { LOG_ERROR("failed, wrong response from IFW"); LOGF_DEBUG("Response : (%s)", response); return false; } LOGF_DEBUG("Success, response from IFW is : %s", response); LOG_INFO("IFW is online"); return true; } /************************************************************************************ * ************************************************************************************/ bool FilterIFW::Disconnect() { DEBUGTAG(); char response[OPTEC_MAXLEN_RESP + 1]; memset(response, 0, sizeof(response)); if (!WriteTTY((char *)"WEXITS")) { LOGF_ERROR("(Function %s()) failed to write to TTY", __FUNCTION__); return false; } if (!ReadTTY(response, (char *)"END", OPTEC_TIMEOUT)) { LOGF_ERROR("(Function %s()) failed to read to TTY", __FUNCTION__); return false; } if (strcmp(response, "END") != 0) { LOG_ERROR("failed, wrong response from IFW"); return false; } LOGF_DEBUG("IFW return in manual mode, response from IFW is : %s", response); LOG_INFO("IFW is offline."); return INDI::FilterWheel::Disconnect(); } /************************************************************************************ * ************************************************************************************/ bool FilterIFW::ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n) { if (dev != nullptr && strcmp(dev, getDeviceName()) == 0) { // User has changed one or more names from filter related to the Wheel ID present in the IFW if (strcmp(FilterNameTP->name, name) == 0) { // Only these chars are allowed to be able to the IFW display to show names correctly std::regex rx("^[A-Z0-9=.#/%[:space:]-]{1,8}$"); bool match = true; //Check only if user allowed chars restriction if (CharSetS[0].s == ISS_ON) { for (int i = 0; i < n; i++) { LOGF_DEBUG("FilterName request N°%d : %s", i, texts[i]); match = std::regex_match(texts[i], rx); if (!match) break; } } if (match) { IUUpdateText(FilterNameTP, texts, names, n); FilterNameTP->s = SetFilterNames() ? IPS_OK : IPS_ALERT; IDSetText(FilterNameTP, nullptr); } else { FilterNameTP->s = IPS_ALERT; IDSetText(FilterNameTP, nullptr); LOG_INFO("WARNING *****************************************************"); DEBUG(INDI::Logger::DBG_SESSION, "One of the filter name is not valid. It should not have more than 8 chars"); LOG_INFO("Valid chars are A to Z, 0 to 9 = . # / - percent or space"); LOG_INFO("WARNING *****************************************************"); return false; } return true; } } return INDI::FilterWheel::ISNewText(dev, name, texts, names, n); } /************************************************************************************ * ************************************************************************************/ bool FilterIFW::ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) { if (dev != nullptr && strcmp(dev, getDeviceName()) == 0) { if (strcmp(HomeSP.name, name) == 0) { bool result = true; // User request the IWF reset (Home procedure will read the Wheel ID, load from EEProm the filters names and goes to filter N°1 IUUpdateSwitch(&HomeSP, states, names, n); IUResetSwitch(&HomeSP); LOG_INFO("Executing Home command..."); FilterNameTP->s = IPS_BUSY; IDSetText(FilterNameTP, nullptr); if (!moveHome()) { HomeSP.s = IPS_ALERT; result = false; } else { LOG_DEBUG("Getting filter information..."); if (!(GetFilterNames() && GetFilterPos() != 0)) { HomeSP.s = IPS_ALERT; result = false; } } IDSetSwitch(&HomeSP, nullptr); if (!result) { LOGF_INFO("%s() failed to get information", __FUNCTION__); LOG_INFO("Please check unit and press 'Home' button"); return false; } return true; } if (strcmp(FilterNbrSP.name, name) == 0) { IUUpdateSwitch(&FilterNbrSP, states, names, n); // Is simulation active, User can change from 5 postions wheel to 6, 8 or 9 ones // Check if selection is different from active one if ((FilterNbrS[0].s == ISS_ON) & (FilterSlotN[0].max != 5)) { strncpy(filterSim, filterSim5, sizeof(filterSim)); FilterNbrSP.s = (GetFilterNames() && GetFilterPos() != 0) ? IPS_OK : IPS_ALERT; } else if ((FilterNbrS[1].s == ISS_ON) & (FilterSlotN[0].max != 6)) { strncpy(filterSim, filterSim6, sizeof(filterSim)); FilterNbrSP.s = (GetFilterNames() && GetFilterPos() != 0) ? IPS_OK : IPS_ALERT; } else if ((FilterNbrS[2].s == ISS_ON) & (FilterSlotN[0].max != 8)) { strncpy(filterSim, filterSim8, sizeof(filterSim)); FilterNbrSP.s = (GetFilterNames() && GetFilterPos() != 0) ? IPS_OK : IPS_ALERT; } else if ((FilterNbrS[3].s == ISS_ON) & (FilterSlotN[0].max != 9)) { strncpy(filterSim, filterSim9, sizeof(filterSim)); FilterNbrSP.s = (GetFilterNames() && GetFilterPos() != 0) ? IPS_OK : IPS_ALERT; } else FilterNbrSP.s = IPS_OK; if (FilterNbrSP.s == IPS_ALERT) { IDSetSwitch(&FilterNbrSP, "%s() failed to change number of filters", __FUNCTION__); return false; } else IDSetSwitch(&FilterNbrSP, nullptr); return true; } // Set switch from user selection to allowed use of all chars or restricted to display IFW // 0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ=.#/-% if (strcmp(CharSetSP.name, name) == 0) { IUUpdateSwitch(&CharSetSP, states, names, n); CharSetSP.s = IPS_OK; IDSetSwitch(&CharSetSP, nullptr); return true; } } return INDI::FilterWheel::ISNewSwitch(dev, name, states, names, n); } /************************************************************************************ * ************************************************************************************/ void FilterIFW::simulationTriggered(bool enable) { // toogle buttons to select 5 or 8 filters depend if Simulation active or not if (enable) { if (isConnected()) { defineSwitch(&FilterNbrSP); } } else deleteProperty(FilterNbrSP.name); } /************************************************************************************ * ************************************************************************************/ void FilterIFW::TimerHit() { // not use with IFW } /************************************************************************************ * ************************************************************************************/ bool FilterIFW::SelectFilter(int f) { DEBUGTAG(); bool result = true; char cmd[32]={0}; char response[OPTEC_MAXLEN_RESP + 1]; memset(response, 0, sizeof(response)); snprintf(cmd, 32, "%s%d", "WGOTO", f); FilterSlotNP.s = IPS_BUSY; IDSetNumber(&FilterSlotNP, "*** Moving to filter n° %d ***", f); if (!WriteTTY(cmd)) { LOGF_ERROR("(Function %s()) failed to write to TTY", __FUNCTION__); result = false; } else { if (isSimulation()) { // Time depend of rotation direction. Goes via shortest way int maxFilter = FilterSlotN[0].max; int way1, way2; if (f > actualSimFilter) { // CW calcul way1 = f - actualSimFilter; // CCW calcul way2 = actualSimFilter + maxFilter - f; } else { // CCW calcul way1 = actualSimFilter - f; // CW calcul way2 = maxFilter - actualSimFilter + f; } // About 2 second to change 1 position if (way1 < way2) sleep(2 * way1); else sleep(2 * way2); // Save actual value for Simulation actualSimFilter = f; } if (!ReadTTY(response, (char *)"*", OPTEC_TIMEOUT_MOVE)) { LOGF_ERROR("(Function %s()) failed to read to TTY", __FUNCTION__); result = false; } else if (strncmp(response, "*", 1) != 0) { LOGF_INFO("Error: %s", response); PRINT_ER(response); result = false; } } if (!result) { FilterSlotNP.s = IPS_ALERT; IDSetNumber(&FilterSlotNP, "*** UNABLE TO SELECT THE FILTER ***"); return false; } // As to be called when filter has moved to new position: SelectFilterDone(GetFilterPos()); FilterSlotNP.s = IPS_OK; IDSetNumber(&FilterSlotNP, "Selected filter position reached"); return true; } /************************************************************************************ * ************************************************************************************/ bool FilterIFW::GetFilterNames() { DEBUGTAG(); bool result = true; char filterName[MAXINDINAME]; char filterLabel[MAXINDILABEL]; char filterList[OPTEC_MAXLEN_NAMES + 9]; // tempo string used fo display filtername debug information char response[OPTEC_MAXLEN_RESP + 1]; int lenResponse = 0; // Nbr of char in the response string int maxFilter = 0; memset(response, 0, sizeof(response)); FilterNameTP->s = IPS_BUSY; IDSetText(FilterNameTP, nullptr); if (!WriteTTY((char *)"WREAD")) { LOGF_ERROR("(Function %s()) failed to write to TTY", __FUNCTION__); result = false; } else if (!ReadTTY(response, filterSim, OPTEC_TIMEOUT)) { LOGF_ERROR("(Function %s()) failed to read to TTY", __FUNCTION__); result = false; } if (result) { // Check the size of response to know if this is a 5 or 8 postion wheel as from R2.x IFW support both lenResponse = strlen(response); switch (lenResponse) { case 40: maxFilter = 5; break; case 48: maxFilter = 6; break; case 64: maxFilter = 8; break; case 72: maxFilter = 9; break; default: maxFilter = 0; // Means error somewhere } LOGF_DEBUG("Length of response %d", lenResponse); LOGF_DEBUG("MaxFilter %d", maxFilter); if (maxFilter != 0) { LOGF_DEBUG("Success, response from IFW is : %s", response); // Start parsing from IFW message char *p = response; char filterNameIFW[OPTEC_MAX_FILTER][9]; filterList[0] = '\0'; for (int i = 0; i < maxFilter; i++) { strncpy(filterNameIFW[i], p, OPTEC_LEN_FLTNAME); filterNameIFW[i][OPTEC_LEN_FLTNAME] = '\0'; p = p + OPTEC_LEN_FLTNAME; LOGF_DEBUG("filterNameIFW[%d] : %s", i, filterNameIFW[i]); strncat(filterList, filterNameIFW[i], OPTEC_LEN_FLTNAME); strncat(filterList, "/", 1); } filterList[strlen(filterList) - 1] = '\0'; //Remove last "/" LOG_DEBUG("Redo filters name list"); // Set new max value on the filter_slot property FilterSlotN[0].max = maxFilter; if (isSimulation()) actualSimFilter = FilterSlotN[0].value = 1; IUUpdateMinMax(&FilterSlotNP); IDSetNumber(&FilterSlotNP, nullptr); deleteProperty(FilterNameTP->name); if (FilterNameT != nullptr) { for (int i=0; i < FilterNameTP->ntp; i++) free(FilterNameT[i].text); delete [] FilterNameT; } FilterNameT = new IText[maxFilter]; memset(FilterNameT, 0, sizeof(IText) * maxFilter); for (int i = 0; i < maxFilter; i++) { snprintf(filterName, MAXINDINAME, "FILTER_SLOT_NAME_%d", i + 1); snprintf(filterLabel, MAXINDILABEL, "Filter n° %d", i + 1); IUFillText(&FilterNameT[i], filterName, filterLabel, filterNameIFW[i]); } IUFillTextVector(FilterNameTP, FilterNameT, maxFilter, getDeviceName(), "FILTER_NAME", "Filters", FilterSlotNP.group, IP_RW, 0, IPS_OK); defineText(FilterNameTP); // filterList only use for purpose information // Remove space from filterList char *withSpace = filterList; char *withoutSpace = filterList; while (*withSpace != '\0') { if (*withSpace != ' ') { *withoutSpace = *withSpace; withoutSpace++; } withSpace++; } *withoutSpace = '\0'; IDSetText(FilterNameTP, "IFW Filters name -> %s", filterList); return true; } else LOGF_ERROR("List of filters name is wrong Nbr char red are: %s", lenResponse); } FilterNameTP->s = IPS_ALERT; LOG_ERROR("Failed to read filter names!"); IDSetText(FilterNameTP, nullptr); return false; } /************************************************************************************ * ************************************************************************************/ bool FilterIFW::SetFilterNames() { DEBUGTAG(); bool result = true; char cmd[72]={0}; char tempo[OPTEC_LEN_FLTNAME + 1]; char response[OPTEC_MAXLEN_RESP + 1]; int tempolen; memset(response, 0, sizeof(response)); FilterNameTP->s = FilterSlotNP.s = WheelIDTP.s = IPS_BUSY; IDSetText(FilterNameTP, "*** Saving filters name to IFW... ***"); IDSetNumber(&FilterSlotNP, nullptr); IDSetText(&WheelIDTP, nullptr); snprintf(cmd, 8, "WLOAD%s*", WheelIDT[0].text); for (int i = 0; i < FilterSlotN[0].max; i++) { // Prepare string in tempo with blank space at right to complete to 8 chars for each filter name memset(tempo, ' ', sizeof(tempo)); //Check max len of 8 char for the filter name tempolen = strlen(FilterNameT[i].text); if (tempolen > OPTEC_LEN_FLTNAME) tempolen = OPTEC_LEN_FLTNAME; //memcpy(tempo + (8 - tempolen), FilterNameT[i].text, tempolen); // spaces at begin of name memcpy(tempo, FilterNameT[i].text, tempolen); // spaces at the end of name tempo[8] = '\0'; strcat(cmd, tempo); strncpy(FilterNameT[i].text, tempo, OPTEC_LEN_FLTNAME); FilterNameT[i].text[OPTEC_LEN_FLTNAME] = '\0'; LOGF_DEBUG("Value of the command :%s", cmd); //memset(response, 0, sizeof(tempo)); } LOGF_DEBUG("Length of the command to write to IFW = %d", strlen(cmd)); if (isSimulation()) { strncpy(filterSim, cmd + 7, OPTEC_MAXLEN_NAMES); filterSim[OPTEC_MAXLEN_NAMES] = '\0'; } if (!WriteTTY(cmd)) { LOGF_ERROR("(Function %s()) failed to write to TTY", __FUNCTION__); FilterNameTP->s = IPS_ALERT; IDSetText(FilterNameTP, nullptr); result = false; // Have to wait at least 10 ms for EEPROM writing before next command // Wait 50 mS to be safe usleep(50000); } else { if (!ReadTTY(response, (char *)"!", OPTEC_TIMEOUT)) { LOGF_ERROR("(Function %s()) failed to read to TTY", __FUNCTION__); result = false; } else { if (strncmp(response, "ER=", 3) == 0) { LOGF_INFO("Error: %s", response); PRINT_ER(response); result = false; } } } if (!result) { FilterNameTP->s = IPS_ALERT; IDSetText(FilterNameTP, "*** UNABLE TO WRITE FILTERS NAME ***"); return false; } LOG_INFO("Filters name are saved in IFW"); // Interface not ready before the message "DATA OK" disapear from the display IFW for (int i = OPTEC_WAIT_DATA_OK; i > 0; i--) { LOGF_INFO("Please wait for HOME command start... %d", i); sleep(1); } // Do HOME command to load EEProm new names and getFilter to read new value to validate FilterNameTP->s = moveHome() ? IPS_OK : IPS_ALERT; IDSetText(FilterNameTP, nullptr); return true; } /************************************************************************************ * ************************************************************************************/ bool FilterIFW::GetWheelID() { DEBUGTAG(); bool result = true; char response[OPTEC_MAXLEN_RESP + 1]; memset(response, 0, sizeof(response)); WheelIDTP.s = IPS_BUSY; IDSetText(&WheelIDTP, nullptr); if (!WriteTTY((char *)"WIDENT")) { LOGF_ERROR("(Function %s()) failed to write to TTY", __FUNCTION__); result = false; } else { if (!ReadTTY(response, (char *)"C", OPTEC_TIMEOUT)) { LOGF_ERROR("(Function %s()) failed to read to TTY", __FUNCTION__); result = false; } else if (strncmp(response, "ER=", 3) == 0) { LOGF_INFO("Get wheel ID error: %s", response); PRINT_ER(response); result = false; } } if (!result) { WheelIDTP.s = IPS_ALERT; IDSetText(&WheelIDTP, "*** UNABLE TO GET WHEEL ID ***"); return false; } WheelIDTP.s = IPS_OK; IUSaveText(&WheelIDT[0], response); IDSetText(&WheelIDTP, "IFW wheel active is %s", response); return true; } /************************************************************************************ * ************************************************************************************/ int FilterIFW::GetFilterPos() { DEBUGTAG(); int result = 1; char response[OPTEC_MAXLEN_RESP + 1]; char filter[2]={0}; memset(response, 0, sizeof(response)); FilterSlotNP.s = IPS_BUSY; IDSetNumber(&FilterSlotNP, nullptr); if (!WriteTTY((char *)"WFILTR")) { LOGF_ERROR("(Function %s()) failed to write to TTY", __FUNCTION__); result = -1; } else { // actualSimFilter for simulation value. int value need to be char* snprintf(filter, 2, "%d", actualSimFilter); if (!ReadTTY(response, filter, OPTEC_TIMEOUT)) { LOGF_ERROR("(Function %s()) failed to read to TTY", __FUNCTION__); result = -1; } } if (result == -1) { FilterSlotNP.s = IPS_ALERT; IDSetNumber(&FilterSlotNP, "*** UNABLE TO GET ACTIVE FILTER ***"); return result; } result = atoi(response); FilterSlotN[0].value = result; FilterSlotNP.s = IPS_OK; IDSetNumber(&FilterSlotNP, "IFW filter active is n° %s -> %s", response, FilterNameT[result - 1].text); return result; } /************************************************************************************ * ************************************************************************************/ bool FilterIFW::moveHome() { DEBUGTAG(); bool result = true; char response[OPTEC_MAXLEN_RESP + 1]; memset(response, 0, sizeof(response)); HomeSP.s = WheelIDTP.s = FilterSlotNP.s = IPS_BUSY; IDSetSwitch(&HomeSP, "*** Initialisation of the IFW. Please wait... ***"); IDSetText(&WheelIDTP, nullptr); IDSetNumber(&FilterSlotNP, nullptr); if (!WriteTTY((char *)"WHOME")) { LOGF_ERROR("(Function %s()) failed to write to TTY", __FUNCTION__); result = false; } else { if (isSimulation()) sleep(10); // About the same time as real filter if (!ReadTTY(response, (char *)"A", OPTEC_TIMEOUT_WHOME)) { LOGF_ERROR("(Function %s()) failed to read from TTY", __FUNCTION__); result = false; } else { if (strncmp(response, "ER=", 3) == 0) { LOGF_INFO("Move to Home error: %s", response); PRINT_ER(response); result = false; } } } if (!result || !GetWheelID() || !GetFilterNames() || (GetFilterPos() <= 0)) { HomeSP.s = WheelIDTP.s = IPS_ALERT; IDSetSwitch(&HomeSP, "*** INITIALISATION FAILED ***"); return false; } HomeSP.s = WheelIDTP.s = IPS_OK; IDSetSwitch(&HomeSP, "IFW ready"); return true; } /************************************************************************************ * ************************************************************************************/ bool FilterIFW::GetFirmware() { DEBUGTAG(); bool result = true; char response[OPTEC_MAXLEN_RESP + 1]; memset(response, 0, sizeof(response)); FirmwareTP.s = IPS_BUSY; IDSetText(&FirmwareTP, nullptr); if (!WriteTTY((char *)"WVAAAA")) { LOGF_ERROR("(Function %s()) failed to write to TTY", __FUNCTION__); result = false; } else { if (!ReadTTY(response, (char *)"V= 2.04", OPTEC_TIMEOUT_FIRMWARE)) { LOGF_ERROR("(Function %s()) failed to read to TTY", __FUNCTION__); result = false; } else if (strncmp(response, "ER=", 3) == 0) { LOGF_INFO("Get wheel ID error: %s", response); PRINT_ER(response); result = false; } } if (!result) { FirmwareTP.s = IPS_ALERT; IDSetText(&FirmwareTP, "*** UNABLE TO GET FIRMWARE ***"); return false; } // remove chars fomr the string to get only the nzuméric value of the Firmware version char *p = nullptr; for (int i = 0; i < (int)strlen(response); i++) { if (isdigit(response[i]) != 0) { p = response + i; break; } } FirmwareTP.s = IPS_OK; IUSaveText(&FirmwareT[0], p); IDSetText(&FirmwareTP, "IFW Firmware is %s", response); return true; } /************************************************************************************ * ************************************************************************************/ bool FilterIFW::saveConfigItems(FILE *fp) { INDI::FilterWheel::saveConfigItems(fp); IUSaveConfigSwitch(fp, &CharSetSP); IUSaveConfigSwitch(fp, &FilterNbrSP); return true; } /************************************************************************************ * ************************************************************************************/ bool FilterIFW::loadConfig(bool silent, const char *property) { bool result; if (property == nullptr) { result = INDI::DefaultDevice::loadConfig(silent, "CHARSET"); result = (INDI::DefaultDevice::loadConfig(silent, "FILTER_NBR") && result); result = (INDI::DefaultDevice::loadConfig(silent, "USEJOYSTICK") && result); result = (INDI::DefaultDevice::loadConfig(silent, "JOYSTICKSETTINGS") && result); } else result = INDI::DefaultDevice::loadConfig(silent, property); return result; } libindi/drivers/filter_wheel/xagyl_wheel.h0000664000175000017500000000602313263645557020303 0ustar jasemjasem/******************************************************************************* Copyright(c) 2015 Jasem Mutlaq. All rights reserved. This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2 as published by the Free Software Foundation. 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 Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. *******************************************************************************/ #pragma once #include "indibase/indifilterwheel.h" typedef struct { int speed; int position; int pulseWidth; int jitter; int threshold; int offset[5]; char product[16]; char version[16]; char serial[16]; } SimData; class XAGYLWheel : public INDI::FilterWheel { public: typedef enum { INFO_PRODUCT_NAME, INFO_FIRMWARE_VERSION, INFO_FILTER_POSITION, INFO_SERIAL_NUMBER, INFO_MAX_SPEED, INFO_JITTER, INFO_OFFSET, INFO_THRESHOLD, INFO_MAX_SLOTS, INFO_PULSE_WIDTH } GET_COMMAND; typedef enum { SET_SPEED, SET_JITTER, SET_THRESHOLD, SET_PULSE_WITDH, SET_POSITION } SET_COMMAND; XAGYLWheel(); virtual ~XAGYLWheel(); virtual bool initProperties() override; virtual bool updateProperties() override; virtual bool ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) override; virtual bool ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) override; protected: const char *getDefaultName() override; bool Handshake() override; void TimerHit() override; bool SelectFilter(int) override; bool saveConfigItems(FILE *fp) override; private: bool getCommand(GET_COMMAND cmd, char *result); bool setCommand(SET_COMMAND cmd, int value); void initOffset(); bool getStartupData(); bool getFirmwareInfo(); bool getSettingInfo(); bool getFilterPosition(); bool getMaximumSpeed(); bool getJitter(); bool getThreshold(); bool getMaxFilterSlots(); bool getPulseWidth(); // Calibration offset bool getOffset(int filter); bool setOffset(int filter, int value); // Reset bool reset(int value); // Firmware info ITextVectorProperty FirmwareInfoTP; IText FirmwareInfoT[3]; // Settings INumberVectorProperty SettingsNP; INumber SettingsN[4]; // Filter Offset INumberVectorProperty OffsetNP; INumber *OffsetN { nullptr }; // Reset ISwitchVectorProperty ResetSP; ISwitch ResetS[4]; SimData simData; uint8_t firmwareVersion { 0 }; }; libindi/drivers/filter_wheel/ifwoptec.h0000664000175000017500000001226213263645557017615 0ustar jasemjasem/******************************************************************************* Copyright(c) 2016 Philippe Besson. All rights reserved. This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2 as published by the Free Software Foundation. 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 Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. *******************************************************************************/ #pragma once #include "indifilterwheel.h" #define VERSION 0 #define SUBVERSION 2 #define OPTEC_TIMEOUT 5 #define OPTEC_TIMEOUT_MOVE 10 #define OPTEC_TIMEOUT_WHOME 40 #define OPTEC_TIMEOUT_FIRMWARE 1 #define OPTEC_MAX_FILTER 9 #define OPTEC_LEN_FLTNAME 8 #define OPTEC_MAXLEN_CMD ((OPTEC_MAX_FILTER)*OPTEC_LEN_FLTNAME) + 10 #define OPTEC_MAXLEN_RESP OPTEC_MAX_FILTER *OPTEC_LEN_FLTNAME #define OPTEC_MAXLEN_NAMES OPTEC_MAX_FILTER *OPTEC_LEN_FLTNAME #define OPTEC_WAIT_DATA_OK 5 #define filterSim5 "RED GREEN BLUE H-ALPHA LIGHT " #define filterSim6 "RED GREEN BLUE H-ALPHA LIGHT OIII " #define filterSim8 "RED GREEN BLUE H-ALPHA LIGHT OIII IR-CUT SII " #define filterSim9 "RED GREEN BLUE H-ALPHA LIGHT OIII IR-CUT SII ORANGE " /******************************************************************************* Define text message error from IFW *******************************************************************************/ #define MER1 "the number of steps to find position 1 is excessive" #define MER2 "the SBIG pulse does not have the proper width for the IFW" #define MER3 "the filter ID is not found/send successfully" #define MER4 "the wheel is stuck in a position" #define MER5 "the filter number is not in the set (1, 2, 3, 4, 5)" #define MER6 "the wheel is slipping and takes too many steps to the next position" #define MERO "Unknown error received from IFW" #define PRINT_ER(error) \ if (!strcmp(error, "ER=1")) \ LOGF_ERROR("%s -> %s", error, MER1); \ else if (!strcmp(error, "ER=2")) \ LOGF_ERROR("%s -> %s", error, MER2); \ else if (!strcmp(error, "ER=3")) \ LOGF_ERROR("%s -> %s", error, MER3); \ else if (!strcmp(error, "ER=4")) \ LOGF_ERROR("%s -> %s", error, MER4); \ else if (!strcmp(error, "ER=5")) \ LOGF_ERROR("%s -> %s", error, MER5); \ else if (!strcmp(error, "ER=6")) \ LOGF_ERROR("%s -> %s", error, MER6); \ else if (!strcmp(error, "ER=0")) \ LOGF_ERROR("%s -> %s", error, MERO); #define DEBUGTAG() DEBUGF(INDI::Logger::DBG_EXTRA_1, "DEBUG -> Function %s() is executing", __FUNCTION__); /******************************************************************************* * Class FilterIFW *******************************************************************************/ class FilterIFW : public INDI::FilterWheel { private: public: FilterIFW(); virtual ~FilterIFW() = default; virtual bool initProperties() override; virtual bool ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n) override; virtual bool ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) override; protected: virtual bool updateProperties() override; virtual bool Handshake() override; virtual bool Disconnect() override; bool WriteTTY(char *command); bool ReadTTY(char *resp, char *simulation, int timeout); virtual const char *getDefaultName() override; bool moveHome(); virtual bool SelectFilter(int) override; virtual void TimerHit() override; virtual bool saveConfigItems(FILE *fp) override; virtual void simulationTriggered(bool enable) override; virtual bool loadConfig(bool silent = false, const char *property = nullptr) override; virtual bool SetFilterNames() override; virtual bool GetFilterNames() override; bool GetWheelID(); int GetFilterPos(); bool GetFirmware(); // Filter Wheel ID ITextVectorProperty WheelIDTP; IText WheelIDT[1] {}; // Home function ISwitchVectorProperty HomeSP; ISwitch HomeS[1]; //Simulation, number of filter function ISwitchVectorProperty FilterNbrSP; ISwitch FilterNbrS[4]; // CharSet unrestricted for FilterNames ISwitchVectorProperty CharSetSP; ISwitch CharSetS[2]; // Firmware of teh IFW ITextVectorProperty FirmwareTP; IText FirmwareT[1] {}; //Filter position in simulation mode int actualSimFilter { 1 }; // Filter name list for simulation char filterSim[OPTEC_MAXLEN_NAMES + 1]; }; libindi/drivers/filter_wheel/trutech_wheel.h0000664000175000017500000000262513263645557020641 0ustar jasemjasem/******************************************************************************* Copyright(c) 2017 Jasem Mutlaq. All rights reserved. Tru Technology Filter Wheel This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2 as published by the Free Software Foundation. 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 Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. *******************************************************************************/ #pragma once #include "indifilterwheel.h" class TruTech : public INDI::FilterWheel { public: TruTech(); virtual ~TruTech() = default; virtual bool ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n); protected: const char *getDefaultName(); bool initProperties(); bool updateProperties(); bool Handshake(); bool SelectFilter(int); void TimerHit(); private: ISwitch HomeS[1]; ISwitchVectorProperty HomeSP; }; libindi/drivers/auxiliary/0000775000175000017500000000000013263645557015157 5ustar jasemjasemlibindi/drivers/auxiliary/gps_simulator.cpp0000664000175000017500000000577013263645557020564 0ustar jasemjasem/******************************************************************************* Copyright(c) 2015 Jasem Mutlaq. All rights reserved. Simple GPS Simulator This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program 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 General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. The full GNU General Public License is included in this distribution in the file called LICENSE. *******************************************************************************/ #include "gps_simulator.h" #include #include // We declare an auto pointer to GPSSimulator. std::unique_ptr gpsSimulator(new GPSSimulator()); void ISGetProperties(const char *dev) { gpsSimulator->ISGetProperties(dev); } void ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) { gpsSimulator->ISNewSwitch(dev, name, states, names, n); } void ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n) { gpsSimulator->ISNewText(dev, name, texts, names, n); } void ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) { gpsSimulator->ISNewNumber(dev, name, values, names, n); } void ISNewBLOB(const char *dev, const char *name, int sizes[], int blobsizes[], char *blobs[], char *formats[], char *names[], int n) { INDI_UNUSED(dev); INDI_UNUSED(name); INDI_UNUSED(sizes); INDI_UNUSED(blobsizes); INDI_UNUSED(blobs); INDI_UNUSED(formats); INDI_UNUSED(names); INDI_UNUSED(n); } void ISSnoopDevice(XMLEle *root) { INDI_UNUSED(root); } GPSSimulator::GPSSimulator() { setVersion(1, 0); } const char *GPSSimulator::getDefaultName() { return (const char *)"GPS Simulator"; } bool GPSSimulator::Connect() { return true; } bool GPSSimulator::Disconnect() { return true; } IPState GPSSimulator::updateGPS() { static char ts[32]={0}; struct tm *utc, *local; time_t raw_time; time(&raw_time); utc = gmtime(&raw_time); strftime(ts, sizeof(ts), "%Y-%m-%dT%H:%M:%S", utc); IUSaveText(&TimeT[0], ts); local = localtime(&raw_time); snprintf(ts, sizeof(ts), "%4.2f", (local->tm_gmtoff / 3600.0)); IUSaveText(&TimeT[1], ts); TimeTP.s = IPS_OK; LocationN[LOCATION_LATITUDE].value = 29.1; LocationN[LOCATION_LONGITUDE].value = 48.5; LocationN[LOCATION_ELEVATION].value = 12; LocationNP.s = IPS_OK; return IPS_OK; } libindi/drivers/auxiliary/sqm_simulator.h0000664000175000017500000000355413263645557020236 0ustar jasemjasem/******************************************************************************* Copyright(c) 2017 Ralph Rogge. All rights reserved. INDI Sky Quality Meter Simulator This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program 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 General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. The full GNU General Public License is included in this distribution in the file called LICENSE. *******************************************************************************/ #pragma once #include "defaultdevice.h" class SQMSimulator : public INDI::DefaultDevice { public: SQMSimulator(); virtual ~SQMSimulator() = default; virtual bool ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n); virtual bool initProperties(); virtual bool updateProperties(); protected: bool Connect(); bool Disconnect(); const char *getDefaultName(); private: bool getReading(); bool getUnit(); // Reading static const int READING_NUMBER_OF_VALUES = 5; INumberVectorProperty readingProperties; INumber readingValues[READING_NUMBER_OF_VALUES]; // Unit static const int UNIT_NUMBER_OF_VALUES = 4; INumberVectorProperty unitProperties; INumber unitValues[UNIT_NUMBER_OF_VALUES]; }; libindi/drivers/auxiliary/watchdogclient.h0000664000175000017500000000470213263645557020332 0ustar jasemjasem/******************************************************************************* Copyright(c) 2015 Jasem Mutlaq. All rights reserved. INDI Watchdog Client. The clients communicates with INDI server to put devices in a safe state for shutdown This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program 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 General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. The full GNU General Public License is included in this distribution in the file called LICENSE. *******************************************************************************/ #pragma once #include "baseclient.h" #include "basedevice.h" #include class WatchDogClient : public INDI::BaseClient { public: WatchDogClient(); ~WatchDogClient(); bool isBusy() { return isRunning; } bool isConnected() { return isReady; } void setDome(const std::string &value); void setMount(const std::string &value); bool parkDome(); bool parkMount(); IPState getDomeParkState(); IPState getMountParkState(); protected: virtual void newDevice(INDI::BaseDevice *dp); virtual void removeDevice(INDI::BaseDevice */*dp*/) {} virtual void newProperty(INDI::Property *property); virtual void removeProperty(INDI::Property */*property*/) {} virtual void newBLOB(IBLOB */*bp*/) {} virtual void newSwitch(ISwitchVectorProperty */*svp*/) {} virtual void newNumber(INumberVectorProperty */*nvp*/) {} virtual void newMessage(INDI::BaseDevice */*dp*/, int /*messageID*/) {} virtual void newText(ITextVectorProperty */*tvp*/) {} virtual void newLight(ILightVectorProperty */*lvp*/) {} virtual void serverConnected() {} virtual void serverDisconnected(int /*exit_code*/) {} private: std::string dome, mount; bool isReady, isRunning, domeOnline, mountOnline; ISwitchVectorProperty *mountParkSP, *domeParkSP; }; libindi/drivers/auxiliary/sqm_simulator.cpp0000664000175000017500000001361313263645557020566 0ustar jasemjasem/******************************************************************************* Copyright(c) 2017 Ralph Rogge. All rights reserved. INDI Sky Quality Meter Simulator This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program 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 General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. The full GNU General Public License is included in this distribution in the file called LICENSE. *******************************************************************************/ #include "sqm_simulator.h" #include #include #include #include std::unique_ptr sqmSimulator(new SQMSimulator()); #define UNIT_TAB "Unit" enum { READING_BRIGHTNESS_INDEX, READING_FREQUENCY_INDEX, READING_COUNTER_INDEX, READING_TIME_INDEX, READING_TEMPERATURE_INDEX }; enum { UNIT_PROTOCOL_INDEX, UNIT_MODEL_INDEX, UNIT_FEATURE_INDEX, UNIT_SERIAL_INDEX }; void ISGetProperties(const char *dev) { sqmSimulator->ISGetProperties(dev); } void ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) { sqmSimulator->ISNewSwitch(dev, name, states, names, n); } void ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n) { sqmSimulator->ISNewText(dev, name, texts, names, n); } void ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) { sqmSimulator->ISNewNumber(dev, name, values, names, n); } void ISNewBLOB(const char *dev, const char *name, int sizes[], int blobsizes[], char *blobs[], char *formats[], char *names[], int n) { INDI_UNUSED(dev); INDI_UNUSED(name); INDI_UNUSED(sizes); INDI_UNUSED(blobsizes); INDI_UNUSED(blobs); INDI_UNUSED(formats); INDI_UNUSED(names); INDI_UNUSED(n); } void ISSnoopDevice(XMLEle *root) { sqmSimulator->ISSnoopDevice(root); } SQMSimulator::SQMSimulator() { setVersion(1, 0); } bool SQMSimulator::Connect() { readingProperties.s = getReading() ? IPS_OK : IPS_ALERT; IDSetNumber(&readingProperties, nullptr); unitProperties.s = getUnit() ? IPS_OK : IPS_ALERT; IDSetNumber(&unitProperties, nullptr); return true; } bool SQMSimulator::Disconnect() { return true; } bool SQMSimulator::initProperties() { INDI::DefaultDevice::initProperties(); // Readings IUFillNumber(&readingValues[READING_BRIGHTNESS_INDEX], "SKY_BRIGHTNESS", "Quality (mag/arcsec^2)", "%6.2f", -20, 30, 0, 0); IUFillNumber(&readingValues[READING_FREQUENCY_INDEX], "SENSOR_FREQUENCY", "Freq (Hz)", "%6.2f", 0, 1000000, 0, 0); IUFillNumber(&readingValues[READING_COUNTER_INDEX], "SENSOR_COUNTS", "Period (counts)", "%6.2f", 0, 1000000, 0, 0); IUFillNumber(&readingValues[READING_TIME_INDEX], "SENSOR_PERIOD", "Period (s)", "%6.2f", 0, 1000000, 0, 0); IUFillNumber(&readingValues[READING_TEMPERATURE_INDEX], "SKY_TEMPERATURE", "Temperature (C)", "%6.2f", -50, 80, 0, 0); IUFillNumberVector(&readingProperties, readingValues, READING_NUMBER_OF_VALUES, getDeviceName(), "SKY_QUALITY", "Readings", MAIN_CONTROL_TAB, IP_RW, 0, IPS_IDLE); // Unit Info IUFillNumber(&unitValues[UNIT_PROTOCOL_INDEX], "Protocol", "", "%.f", 0, 1000000, 0, 0); IUFillNumber(&unitValues[UNIT_MODEL_INDEX], "Model", "", "%.f", 0, 1000000, 0, 0); IUFillNumber(&unitValues[UNIT_FEATURE_INDEX], "Feature", "", "%.f", 0, 1000000, 0, 0); IUFillNumber(&unitValues[UNIT_SERIAL_INDEX], "Serial", "", "%.f", 0, 1000000, 0, 0); IUFillNumberVector(&unitProperties, unitValues, UNIT_NUMBER_OF_VALUES, getDeviceName(), "Unit Info", "", UNIT_TAB, IP_RW, 0, IPS_IDLE); addDebugControl(); return true; } const char * SQMSimulator::getDefaultName() { return (const char *) "SQM Simulator"; } bool SQMSimulator::getReading() { readingValues[READING_BRIGHTNESS_INDEX].value = 16.9; readingValues[READING_FREQUENCY_INDEX].value = 15.0; readingValues[READING_COUNTER_INDEX].value = 28856; readingValues[READING_TIME_INDEX].value = 0.063; readingValues[READING_TEMPERATURE_INDEX].value = 21.5; return true; } bool SQMSimulator::getUnit() { unitValues[UNIT_PROTOCOL_INDEX].value = 4; unitValues[UNIT_MODEL_INDEX].value = 3; unitValues[UNIT_FEATURE_INDEX].value = 49; unitValues[UNIT_SERIAL_INDEX].value = 1234; return true; } bool SQMSimulator::ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) { if (dev != nullptr && strcmp(dev, getDeviceName()) == 0) { if (strcmp(name, readingProperties.name) == 0) { IUUpdateNumber(&readingProperties, values, names, n); readingProperties.s = IPS_OK; IDSetNumber(&readingProperties, nullptr); return true; } if (strcmp(name, unitProperties.name) == 0) { IUUpdateNumber(&unitProperties, values, names, n); unitProperties.s = IPS_OK; IDSetNumber(&unitProperties, nullptr); return true; } } return INDI::DefaultDevice::ISNewNumber(dev, name, values, names, n); } bool SQMSimulator::updateProperties() { INDI::DefaultDevice::updateProperties(); if (isConnected()) { defineNumber(&readingProperties); defineNumber(&unitProperties); } else { deleteProperty(readingProperties.name); deleteProperty(unitProperties.name); } return true; } libindi/drivers/auxiliary/astrometrydriver.h0000664000175000017500000001063313263645557020760 0ustar jasemjasem/******************************************************************************* Copyright(c) 2017 Jasem Mutlaq. All rights reserved. INDI Astrometry.net Driver This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program 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 General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. The full GNU General Public License is included in this distribution in the file called LICENSE. *******************************************************************************/ #pragma once #include "defaultdevice.h" #include /** * @brief The AstrometryDriver class is an INDI driver frontend for astrometry.net * * There are two supported methods to solve an image: * * 1. Upload an image using the BLOB property. * 2. Listen to uploaded BLOBs as emitted from a CCD driver. Set the CCD driver name to listen to in Options. * * The solver settings should be set before running the solver in order to ensure correct and timely response from astrometry.net * It is assumed that astrometry.net is property set-up in the same machine the driver is running along with the appropiate index files. * * If the solver is successfull, the driver sets the solver results which include: * + Pixel Scale (arcsec/pixel). * + Orientation (E or W) degrees. * + Center RA (J2000) Hours. * + Center DE (J2000) Degrees. * + Parity * * @author Jasem Mutlaq */ class AstrometryDriver : public INDI::DefaultDevice { public: enum { ASTROMETRY_SETTINGS_BINARY, ASTROMETRY_SETTINGS_OPTIONS }; enum { ASTROMETRY_RESULTS_PIXSCALE, ASTROMETRY_RESULTS_ORIENTATION, ASTROMETRY_RESULTS_RA, ASTROMETRY_RESULTS_DE, ASTROMETRY_RESULTS_PARITY }; AstrometryDriver(); ~AstrometryDriver() = default; virtual void ISGetProperties(const char *dev); virtual bool initProperties(); virtual bool updateProperties(); virtual bool ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n); virtual bool ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n); virtual bool ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n); virtual bool ISNewBLOB(const char *dev, const char *name, int sizes[], int blobsizes[], char *blobs[], char *formats[], char *names[], int n); virtual bool ISSnoopDevice(XMLEle *root); static void *runSolverHelper(void *context); protected: // Generic indi device entries bool Connect(); bool Disconnect(); const char *getDefaultName(); virtual bool saveConfigItems(FILE *fp); // Astrometry // Enable/Disable solver ISwitch SolverS[2]; ISwitchVectorProperty SolverSP; // Solver Settings IText SolverSettingsT[2] {}; ITextVectorProperty SolverSettingsTP; // Solver Results INumber SolverResultN[5]; INumberVectorProperty SolverResultNP; ITextVectorProperty ActiveDeviceTP; IText ActiveDeviceT[1] {}; IBLOBVectorProperty SolverDataBP; IBLOB SolverDataB[1]; IBLOB CCDDataB[1]; IBLOBVectorProperty CCDDataBP; private: // Run solver thread void runSolver(); /** * @brief processBLOB Read blob FITS. Uncompress if necessary, write to temporary file, and run * solver against it. * @param data raw data FITS buffer * @param size size of FITS data * @param len size of raw data. If no compression is used then len = size. If compression is used, * then len is the compressed buffer size and size is the uncompressed final valid data size. * @return True if blob buffer was processed correctly and solver started, false otherwise. */ bool processBLOB(uint8_t *data, uint32_t size, uint32_t len); // Thread for listenINDI() pthread_t solverThread; pthread_mutex_t lock; }; libindi/drivers/auxiliary/joystick.h0000664000175000017500000000532513263645557017174 0ustar jasemjasem/******************************************************************************* Copyright(c) 2013 Jasem Mutlaq. All rights reserved. This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program 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 General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. The full GNU General Public License is included in this distribution in the file called LICENSE. *******************************************************************************/ #pragma once #include "defaultdevice.h" class JoyStickDriver; /** * @brief The JoyStick class provides an INDI driver that displays event data from game pads. The INDI driver can be encapsulated in any other driver * via snooping on properties of interesting. * */ class JoyStick : public INDI::DefaultDevice { public: JoyStick(); virtual ~JoyStick(); virtual bool initProperties() override; virtual bool updateProperties() override; virtual void ISGetProperties(const char *dev) override; virtual bool ISSnoopDevice(XMLEle *root) override; virtual bool ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n) override; static void joystickHelper(int joystick_n, double mag, double angle); static void axisHelper(int axis_n, int value); static void buttonHelper(int button_n, int value); protected: // Generic indi device entries virtual bool Connect() override; virtual bool Disconnect() override; virtual const char *getDefaultName() override; bool saveConfigItems(FILE *fp) override; void setupParams(); void joystickEvent(int joystick_n, double mag, double angle); void axisEvent(int axis_n, int value); void buttonEvent(int button_n, int value); INumberVectorProperty *JoyStickNP = nullptr; INumber *JoyStickN = nullptr; INumberVectorProperty AxisNP; INumber *AxisN = nullptr; ISwitchVectorProperty ButtonSP; ISwitch *ButtonS = nullptr; ITextVectorProperty PortTP; // A text vector that stores out physical port name IText PortT[1] {}; ITextVectorProperty JoystickInfoTP; IText JoystickInfoT[5] {}; JoyStickDriver *driver = nullptr; }; libindi/drivers/auxiliary/STAR2kdriver.h0000664000175000017500000000310013263645557017544 0ustar jasemjasem/******************************************************************************* created 2014 G. Schmidt This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program 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 General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. The full GNU General Public License is included in this distribution in the file called LICENSE. *******************************************************************************/ #pragma once /********************************** some defines ******************************/ #define S2K_WEST_1 0x01 #define S2K_SOUTH_1 0x02 #define S2K_NORTH_1 0x04 #define S2K_EAST_1 0x08 #define S2K_WEST_0 0x0E #define S2K_SOUTH_0 0x0D #define S2K_NORTH_0 0x0B #define S2K_EAST_0 0x07 #define NORTH 0 #define WEST 1 #define EAST 2 #define SOUTH 3 #define ALL -1 #ifdef __cplusplus extern "C" { #endif int ConnectSTAR2k(char *port); void DisconnectSTAR2k(); void StartPulse(int direction); void StopPulse(int direction); #ifdef __cplusplus } #endif libindi/drivers/auxiliary/skysafari.h0000664000175000017500000000650413263645557017331 0ustar jasemjasem/******************************************************************************* Copyright(c) 2017 Jasem Mutlaq. All rights reserved. INDI SkySafar Middleware Driver. The driver expects a heartbeat from the client every X minutes. If no heartbeat is received, the driver executes the shutdown procedures. This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program 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 General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. The full GNU General Public License is included in this distribution in the file called LICENSE. *******************************************************************************/ #pragma once #include "defaultdevice.h" class SkySafariClient; class SkySafari : public INDI::DefaultDevice { public: SkySafari(); virtual ~SkySafari(); virtual void ISGetProperties(const char *dev); //virtual bool ISSnoopDevice (XMLEle *root); virtual bool ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n); virtual bool ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n); virtual bool ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n); protected: virtual bool initProperties(); //virtual bool updateProperties(); virtual void TimerHit(); virtual bool Connect(); virtual bool Disconnect(); virtual const char *getDefaultName(); virtual bool saveConfigItems(FILE *fp); private: void processCommand(std::string cmd); bool startServer(); bool stopServer(); bool sendSkySafari(const char *message); void sendGeographicCoords(); void sendUTCtimedate(); template void split(const std::string &s, char delim, Out result); std::vector split(const std::string &s, char delim); // Settings ITextVectorProperty SettingsTP; IText SettingsT[3] {}; enum { INDISERVER_HOST, INDISERVER_PORT, SKYSAFARI_PORT }; // Active Devices ITextVectorProperty ActiveDeviceTP; IText ActiveDeviceT[1] {}; enum { ACTIVE_TELESCOPE }; // Server Control ISwitchVectorProperty ServerControlSP; ISwitch ServerControlS[2]; enum { SERVER_ENABLE, SERVER_DISABLE }; // Our client SkySafariClient *skySafariClient = nullptr; int lsocket = -1, clientFD = -1; bool isSkySafariConnected = false, haveLatitude = false, haveLongitude = false; bool haveUTCoffset = false, haveUTCtime = false, haveUTCdate = false; double siteLatitude = 0, siteLongitude = 0; double RA = 0, DE = 0; double timeUTCOffset = 0; int timeYear = 0, timeMonth = 0, timeDay = 0, timeHour = 0, timeMin = 0, timeSec = 0; }; libindi/drivers/auxiliary/skysafari.cpp0000664000175000017500000005503313263645557017665 0ustar jasemjasem/******************************************************************************* Copyright(c) 2017 Jasem Mutlaq. All rights reserved. INDI SkySafar Middleware Driver. The driver expects a heartbeat from the client every X minutes. If no heartbeat is received, the driver executes the shutdown procedures. This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program 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 General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. The full GNU General Public License is included in this distribution in the file called LICENSE. *******************************************************************************/ #include "skysafari.h" #include "skysafariclient.h" #include "indicom.h" #include #include #include #include #include #include #include #include // We declare unique pointer to my lovely German Shephard Tommy (http://indilib.org/images/juli_tommy.jpg) std::unique_ptr tommyGoodBoy(new SkySafari()); void ISGetProperties(const char *dev) { tommyGoodBoy->ISGetProperties(dev); } void ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) { tommyGoodBoy->ISNewSwitch(dev, name, states, names, n); } void ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n) { tommyGoodBoy->ISNewText(dev, name, texts, names, n); } void ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) { tommyGoodBoy->ISNewNumber(dev, name, values, names, n); } void ISNewBLOB(const char *dev, const char *name, int sizes[], int blobsizes[], char *blobs[], char *formats[], char *names[], int n) { INDI_UNUSED(dev); INDI_UNUSED(name); INDI_UNUSED(sizes); INDI_UNUSED(blobsizes); INDI_UNUSED(blobs); INDI_UNUSED(formats); INDI_UNUSED(names); INDI_UNUSED(n); } void ISSnoopDevice(XMLEle *root) { tommyGoodBoy->ISSnoopDevice(root); } SkySafari::SkySafari() { setVersion(0, 1); setDriverInterface(AUX_INTERFACE); skySafariClient = new SkySafariClient(); } SkySafari::~SkySafari() { delete (skySafariClient); } const char *SkySafari::getDefaultName() { return (const char *)"SkySafari"; } bool SkySafari::Connect() { bool rc = startServer(); if (rc) { skySafariClient->setMount(ActiveDeviceT[ACTIVE_TELESCOPE].text); skySafariClient->connectServer(); SetTimer(POLLMS); } return rc; } bool SkySafari::Disconnect() { return stopServer(); } bool SkySafari::initProperties() { INDI::DefaultDevice::initProperties(); IUFillText(&SettingsT[INDISERVER_HOST], "INDISERVER_HOST", "indiserver host", "localhost"); IUFillText(&SettingsT[INDISERVER_PORT], "INDISERVER_PORT", "indiserver port", "7624"); IUFillText(&SettingsT[SKYSAFARI_PORT], "SKYSAFARI_PORT", "SkySafari port", "9624"); IUFillTextVector(&SettingsTP, SettingsT, 3, getDeviceName(), "WATCHDOG_SETTINGS", "Settings", MAIN_CONTROL_TAB, IP_RW, 60, IPS_IDLE); IUFillSwitch(&ServerControlS[SERVER_ENABLE], "Enable", "", ISS_OFF); IUFillSwitch(&ServerControlS[SERVER_DISABLE], "Disable", "", ISS_ON); IUFillSwitchVector(&ServerControlSP, ServerControlS, 2, getDeviceName(), "Server", "", MAIN_CONTROL_TAB, IP_RW, ISR_1OFMANY, 0, IPS_IDLE); IUFillText(&ActiveDeviceT[ACTIVE_TELESCOPE], "ACTIVE_TELESCOPE", "Telescope", "Telescope Simulator"); IUFillTextVector(&ActiveDeviceTP, ActiveDeviceT, 1, getDeviceName(), "ACTIVE_DEVICES", "Active devices", OPTIONS_TAB, IP_RW, 60, IPS_IDLE); addDebugControl(); setDefaultPollingPeriod(100); return true; } void SkySafari::ISGetProperties(const char *dev) { // First we let our parent populate DefaultDevice::ISGetProperties(dev); defineText(&SettingsTP); defineText(&ActiveDeviceTP); //defineSwitch(&ServerControlSP); loadConfig(true); //watchdogClient->setTelescope(ActiveDeviceT[0].text); //watchdogClient->setDome(ActiveDeviceT[1].text); } bool SkySafari::ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n) { if (dev != nullptr && strcmp(dev, getDeviceName()) == 0) { if (!strcmp(SettingsTP.name, name)) { IUUpdateText(&SettingsTP, texts, names, n); SettingsTP.s = IPS_OK; IDSetText(&SettingsTP, nullptr); return true; } if (!strcmp(ActiveDeviceTP.name, name)) { IUUpdateText(&ActiveDeviceTP, texts, names, n); ActiveDeviceTP.s = IPS_OK; IDSetText(&ActiveDeviceTP, nullptr); return true; } } return INDI::DefaultDevice::ISNewText(dev, name, texts, names, n); } bool SkySafari::ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) { return DefaultDevice::ISNewNumber(dev, name, values, names, n); } bool SkySafari::ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) { if (dev != nullptr && strcmp(dev, getDeviceName()) == 0) { if (!strcmp(ServerControlSP.name, name)) { bool rc = false; if (!strcmp(IUFindOnSwitchName(states, names, n), ServerControlS[SERVER_ENABLE].name)) { // If already working, do nothing if (ServerControlS[SERVER_ENABLE].s == ISS_ON) { ServerControlSP.s = IPS_OK; IDSetSwitch(&ServerControlSP, nullptr); return true; } rc = startServer(); ServerControlSP.s = (rc ? IPS_OK : IPS_ALERT); } else { if (!strcmp(IUFindOnSwitchName(states, names, n), ServerControlS[SERVER_DISABLE].name)) { // If already working, do nothing if (ServerControlS[SERVER_DISABLE].s == ISS_ON) { ServerControlSP.s = IPS_IDLE; IDSetSwitch(&ServerControlSP, nullptr); return true; } rc = stopServer(); ServerControlSP.s = (rc ? IPS_IDLE : IPS_ALERT); } } IUUpdateSwitch(&ServerControlSP, states, names, n); IDSetSwitch(&ServerControlSP, nullptr); return true; } } return DefaultDevice::ISNewSwitch(dev, name, states, names, n); } bool SkySafari::saveConfigItems(FILE *fp) { IUSaveConfigText(fp, &SettingsTP); IUSaveConfigText(fp, &ActiveDeviceTP); return true; } void SkySafari::TimerHit() { if (!isConnected()) return; if (clientFD == -1) { struct sockaddr_in cli_socket; socklen_t cli_len; int cli_fd = -1; /* get a private connection to new client */ cli_len = sizeof(cli_socket); cli_fd = accept(lsocket, (struct sockaddr *)&cli_socket, &cli_len); if (cli_fd < 0 && (errno == EAGAIN || errno == EWOULDBLOCK)) { // Try again later SetTimer(POLLMS); return; } else if (cli_fd < 0) { LOGF_ERROR("Failed to connect to SkySafari. %s", strerror(errno)); SetTimer(POLLMS); return; } clientFD = cli_fd; int flags = 0; // Get socket flags if ((flags = fcntl(clientFD, F_GETFL, 0)) < 0) { LOGF_ERROR("Error connecting to SkySafari. F_GETFL: %s", strerror(errno)); } // Set to Non-Blocking if (fcntl(clientFD, F_SETFL, flags | O_NONBLOCK) < 0) { LOGF_ERROR("Error connecting to SkySafari. F_SETFL: %s", strerror(errno)); } // Only show message first time SkySafari connects if (isSkySafariConnected == false) { LOG_INFO("Connected to SkySafari."); isSkySafariConnected = true; } } else { // Read from SkySafari char buffer[64] = { 0 }; int rc = read(clientFD, buffer, 64); if (rc > 0) { std::vector commands = split(buffer, '#'); for (std::string cmd : commands) { // Remove the : cmd.erase(0, 1); processCommand(cmd); } } // EOF else if (rc == 0) { //LOG_ERROR("SkySafari Disconnected? Reconnect again."); close(clientFD); clientFD = -1; } // Otherwise EAGAIN so we just try shortly } SetTimer(POLLMS); } bool SkySafari::startServer() { struct sockaddr_in serv_socket; int sfd; int flags = 0; int reuse = 1; /* make socket endpoint */ if ((sfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0) { LOGF_ERROR("Error starting server. socket: %s", strerror(errno)); return false; } // Get socket flags if ((flags = fcntl(sfd, F_GETFL, 0)) < 0) { LOGF_ERROR("Error starting server. F_GETFL: %s", strerror(errno)); } // Set to Non-Blocking if (fcntl(sfd, F_SETFL, flags | O_NONBLOCK) < 0) { LOGF_ERROR("Error starting server. F_SETFL: %s", strerror(errno)); } /* bind to given port for any IP address */ memset(&serv_socket, 0, sizeof(serv_socket)); serv_socket.sin_family = AF_INET; serv_socket.sin_addr.s_addr = htonl(INADDR_ANY); serv_socket.sin_port = htons((unsigned short)atoi(SettingsT[SKYSAFARI_PORT].text)); if (setsockopt(sfd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse)) < 0) { LOGF_ERROR("Error starting server. setsockopt: %s", strerror(errno)); return false; } if (::bind(sfd, (struct sockaddr *)&serv_socket, sizeof(serv_socket)) < 0) { LOGF_ERROR("Error starting server. bind: %s", strerror(errno)); return false; } /* willing to accept connections with a backlog of 5 pending */ if (listen(sfd, 5) < 0) { LOGF_ERROR("Error starting server. listen: %s", strerror(errno)); return false; } lsocket = sfd; DEBUG(INDI::Logger::DBG_SESSION, "SkySafari Server is running. Connect the App now to this machine using SkySafari LX200 driver."); return true; } bool SkySafari::stopServer() { if (clientFD > 0) close(clientFD); if (lsocket > 0) close(lsocket); clientFD = lsocket = -1; return true; } void SkySafari::processCommand(std::string cmd) { LOGF_DEBUG("CMD <%s>", cmd.c_str()); if (skySafariClient->isConnected() == false) { LOG_ERROR("Internal client is not connected! Please make sure the mount name is set in the Options tab. Disconnect and reconnect to try again."); return; } // Set site Latitude if (cmd.compare(0, 2, "St") == 0) { int dd = 0, mm = 0; if (sscanf(cmd.c_str(), "St%d%*c%d", &dd, &mm) == 2) { haveLatitude = true; siteLatitude = dd + mm / 60.0; } // Always respond with valid sendSkySafari("1"); // Try sending geographic coords if all is available sendGeographicCoords(); } // Set site Longitude else if (cmd.compare(0, 2, "Sg") == 0) { int ddd = 0, mm = 0; if (sscanf(cmd.c_str(), "Sg%d%*c%d", &ddd, &mm) == 2) { haveLongitude = true; siteLongitude = ddd + mm / 60.0; // Convert to INDI format (0 to 360 Eastwards). Meade is 0 to 360 Westwards. siteLongitude = 360 - siteLongitude; } // Always respond with valid sendSkySafari("1"); // Try sending geographic coords if all is available sendGeographicCoords(); } // set the number of hours added to local time to yield UTC else if (cmd.compare(0, 2, "SG") == 0) { int ofs; if (sscanf(cmd.c_str(), "SG%d", &ofs) == 1) { ofs = -ofs; LOGF_DEBUG("UTC Offset: %d", ofs); timeUTCOffset = ofs; haveUTCoffset = true; } // Always respond with valid sendSkySafari("1"); // Try sending geographic coords if all is available sendUTCtimedate(); } // set the local time else if (cmd.compare(0, 2, "SL") == 0) { int hh, mm, ss; if (sscanf(cmd.c_str(), "SL%d:%d:%d", &hh, &mm, &ss) == 3) { LOGF_DEBUG("TIME : %02d:%02d:%02d", hh, mm, ss); timeHour = hh; timeMin = mm; timeSec = ss; haveUTCtime = true; } // Always respond with valid sendSkySafari("1"); // Try sending geographic coords if all is available sendUTCtimedate(); } // set the local date else if (cmd.compare(0, 2, "SC") == 0) { int yyyy, mm, dd; if (sscanf(cmd.c_str(), "SC%d/%d/%d", &mm, &dd, &yyyy) == 3) { LOGF_DEBUG("DATE : %02d-%02d-%02d", yyyy, mm, dd); timeYear = yyyy; timeMonth = mm; timeDay = dd; haveUTCdate = true; } // Always respond with valid sendSkySafari("1"); // Try sending geographic coords if all is available sendUTCtimedate(); } // Get RA else if (cmd == "GR") { INumberVectorProperty *eqCoordsNP = skySafariClient->getEquatorialCoords(); if (eqCoordsNP == nullptr) { LOG_WARN("Unable to communicate with mount, is mount turned on and connected?"); return; } int hh, mm, ss; char output[32] = { 0 }; getSexComponents(eqCoordsNP->np[AXIS_RA].value, &hh, &mm, &ss); snprintf(output, 32, "%02d:%02d:%02d#", hh, mm, ss); sendSkySafari(output); } // Get DE else if (cmd == "GD") { INumberVectorProperty *eqCoordsNP = skySafariClient->getEquatorialCoords(); if (eqCoordsNP == nullptr) { LOG_WARN("Unable to communicate with mount, is mount turned on and connected?"); return; } int dd, mm, ss; char output[32] = { 0 }; getSexComponents(eqCoordsNP->np[AXIS_DE].value, &dd, &mm, &ss); snprintf(output, 32, "%+02d:%02d:%02d#", dd, mm, ss); sendSkySafari(output); } // Set RA else if (cmd.compare(0, 2, "Sr") == 0) { int hh = 0, mm = 0, ss = 0; if (sscanf(cmd.c_str(), "Sr%d:%d:%d", &hh, &mm, &ss) == 3) { RA = hh + mm / 60.0 + ss / 3600.0; } // Always respond with valid sendSkySafari("1"); } // Set DE else if (cmd.compare(0, 2, "Sd") == 0) { int dd = 0, mm = 0, ss = 0; if (sscanf(cmd.c_str(), "Sd%d*%d:%d", &dd, &mm, &ss) == 3) { DE = abs(dd) + mm / 60.0 + ss / 3600.0; if (dd < 0) DE *= -1; } // Always respond with valid sendSkySafari("1"); } // GOTO else if (cmd == "MS") { ISwitchVectorProperty *gotoModeSP = skySafariClient->getGotoMode(); if (gotoModeSP == nullptr) { sendSkySafari("2#"); return; } // Set mode first ISwitch *trackSW = IUFindSwitch(gotoModeSP, "TRACK"); if (trackSW == nullptr) { sendSkySafari("2#"); return; } IUResetSwitch(gotoModeSP); trackSW->s = ISS_ON; skySafariClient->sendGotoMode(); INumberVectorProperty *eqCoordsNP = skySafariClient->getEquatorialCoords(); eqCoordsNP->np[AXIS_RA].value = RA; eqCoordsNP->np[AXIS_DE].value = DE; skySafariClient->sendEquatorialCoords(); sendSkySafari("0"); } // Sync else if (cmd == "CM") { ISwitchVectorProperty *gotoModeSP = skySafariClient->getGotoMode(); if (gotoModeSP == nullptr) { sendSkySafari("Not Supported#"); return; } // Set mode first ISwitch *syncSW = IUFindSwitch(gotoModeSP, "SYNC"); if (syncSW == nullptr) { sendSkySafari("Not Supported#"); return; } IUResetSwitch(gotoModeSP); syncSW->s = ISS_ON; skySafariClient->sendGotoMode(); INumberVectorProperty *eqCoordsNP = skySafariClient->getEquatorialCoords(); eqCoordsNP->np[AXIS_RA].value = RA; eqCoordsNP->np[AXIS_DE].value = DE; skySafariClient->sendEquatorialCoords(); sendSkySafari(" M31 EX GAL MAG 3.5 SZ178.0'#"); } // Abort else if (cmd == "Q") { skySafariClient->abort(); } // RG else if (cmd == "RG") { skySafariClient->setSlewRate(0); } // RC else if (cmd == "RC") { skySafariClient->setSlewRate(1); } // RM else if (cmd == "RM") { skySafariClient->setSlewRate(2); } // RS else if (cmd == "RS") { skySafariClient->setSlewRate(3); } // Mn else if (cmd == "Mn") { ISwitchVectorProperty *motionNSNP = skySafariClient->getMotionNS(); if (motionNSNP) { IUResetSwitch(motionNSNP); motionNSNP->sp[0].s = ISS_ON; skySafariClient->setMotionNS(); } } // Qn else if (cmd == "Qn") { ISwitchVectorProperty *motionNSNP = skySafariClient->getMotionNS(); if (motionNSNP) { IUResetSwitch(motionNSNP); skySafariClient->setMotionNS(); } } // Ms else if (cmd == "Ms") { ISwitchVectorProperty *motionNSNP = skySafariClient->getMotionNS(); if (motionNSNP) { IUResetSwitch(motionNSNP); motionNSNP->sp[1].s = ISS_ON; skySafariClient->setMotionNS(); } } // Qs else if (cmd == "Qs") { ISwitchVectorProperty *motionNSNP = skySafariClient->getMotionNS(); if (motionNSNP) { IUResetSwitch(motionNSNP); skySafariClient->setMotionNS(); } } // Mw else if (cmd == "Mw") { ISwitchVectorProperty *motionWENP = skySafariClient->getMotionWE(); if (motionWENP) { IUResetSwitch(motionWENP); motionWENP->sp[0].s = ISS_ON; skySafariClient->setMotionWE(); } } // Qw else if (cmd == "Qw") { ISwitchVectorProperty *motionWENP = skySafariClient->getMotionWE(); if (motionWENP) { IUResetSwitch(motionWENP); skySafariClient->setMotionWE(); } } // Me else if (cmd == "Me") { ISwitchVectorProperty *motionWENP = skySafariClient->getMotionWE(); if (motionWENP) { IUResetSwitch(motionWENP); motionWENP->sp[1].s = ISS_ON; skySafariClient->setMotionWE(); } } // Qe else if (cmd == "Qe") { ISwitchVectorProperty *motionWENP = skySafariClient->getMotionWE(); if (motionWENP) { IUResetSwitch(motionWENP); skySafariClient->setMotionWE(); } } } void SkySafari::sendGeographicCoords() { INumberVectorProperty *geographicCoords = skySafariClient->getGeographiCoords(); if (geographicCoords && haveLatitude && haveLongitude) { INumber *latitude = IUFindNumber(geographicCoords, "LAT"); INumber *longitude = IUFindNumber(geographicCoords, "LONG"); if (latitude && longitude) { latitude->value = siteLatitude; longitude->value = siteLongitude; skySafariClient->sendGeographicCoords(); // Reset haveLatitude = haveLongitude = false; } } } bool SkySafari::sendSkySafari(const char *message) { LOGF_DEBUG("RES <%s>", message); int bytesWritten = 0, totalBytes = strlen(message); while (bytesWritten < totalBytes) { int bytesSent = write(clientFD, message, totalBytes - bytesWritten); if (bytesSent >= 0) bytesWritten += bytesSent; else { LOGF_ERROR("Error writing to SkySafari. %s", strerror(errno)); return false; } } return true; } void SkySafari::sendUTCtimedate() { ITextVectorProperty *timeUTC = skySafariClient->getTimeUTC(); if (timeUTC && haveUTCoffset && haveUTCtime && haveUTCdate) { int yyyy = timeYear; if (yyyy < 100) yyyy += 2000; // local to UTC ln_zonedate zonedate; ln_date utcdate; zonedate.years = yyyy; zonedate.months = timeMonth; zonedate.days = timeDay; zonedate.hours = timeHour; zonedate.minutes = timeMin; zonedate.seconds = timeSec; zonedate.gmtoff = timeUTCOffset * 3600.0; ln_zonedate_to_date(&zonedate, &utcdate); char bufDT[32]={0}; char bufOff[8]={0}; snprintf(bufDT, 32, "%04d-%02d-%02dT%02d:%02d:%02d", utcdate.years, utcdate.months, utcdate.days, utcdate.hours, utcdate.minutes, (int)(utcdate.seconds)); snprintf(bufOff, 8, "%4.2f", timeUTCOffset); IUSaveText(IUFindText(timeUTC, "UTC"), bufDT); IUSaveText(IUFindText(timeUTC, "OFFSET"), bufOff); LOGF_DEBUG("send to timedate. %s, %s", bufDT, bufOff); skySafariClient->setTimeUTC(); // Reset haveUTCoffset = haveUTCtime = haveUTCdate = false; } } // Had to get this from stackoverflow, why C++ STL lacks such basic functionality?!!! std::vector SkySafari::split(const std::string &text, char sep) { std::vector tokens; std::size_t start = 0, end = 0; while ((end = text.find(sep, start)) != std::string::npos) { tokens.push_back(text.substr(start, end - start)); start = end + 1; } tokens.push_back(text.substr(start)); return tokens; } libindi/drivers/auxiliary/watchdog.cpp0000664000175000017500000003565413263645557017500 0ustar jasemjasem/******************************************************************************* Copyright(c) 2015 Jasem Mutlaq. All rights reserved. INDI Watchdog driver. The driver expects a heartbeat from the client every X minutes. If no heartbeat is received, the driver executes the shutdown procedures. This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program 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 General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. The full GNU General Public License is included in this distribution in the file called LICENSE. *******************************************************************************/ #include "watchdog.h" #include "watchdogclient.h" #include #include #include #include // We declare unique pointer to my lovely German Shephard Juli (http://indilib.org/images/juli_tommy.jpg) std::unique_ptr goodgirrrl(new WatchDog()); void ISGetProperties(const char *dev) { goodgirrrl->ISGetProperties(dev); } void ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) { goodgirrrl->ISNewSwitch(dev, name, states, names, n); } void ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n) { goodgirrrl->ISNewText(dev, name, texts, names, n); } void ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) { goodgirrrl->ISNewNumber(dev, name, values, names, n); } void ISNewBLOB(const char *dev, const char *name, int sizes[], int blobsizes[], char *blobs[], char *formats[], char *names[], int n) { INDI_UNUSED(dev); INDI_UNUSED(name); INDI_UNUSED(sizes); INDI_UNUSED(blobsizes); INDI_UNUSED(blobs); INDI_UNUSED(formats); INDI_UNUSED(names); INDI_UNUSED(n); } void ISSnoopDevice(XMLEle *root) { goodgirrrl->ISSnoopDevice(root); } WatchDog::WatchDog() { setVersion(0, 2); setDriverInterface(AUX_INTERFACE); watchdogClient = new WatchDogClient(); watchDogTimer = -1; shutdownStage = WATCHDOG_IDLE; } WatchDog::~WatchDog() { delete (watchdogClient); } const char *WatchDog::getDefaultName() { return (const char *)"WatchDog"; } bool WatchDog::Connect() { if (HeartBeatN[0].value > 0) { DEBUGF(INDI::Logger::DBG_SESSION, "Watchdog is enabled. Shutdown is triggered after %g minutes of communication loss with the client.", HeartBeatN[0].value); watchDogTimer = SetTimer(HeartBeatN[0].value * 60 * 1000); } else LOG_INFO("Watchdog is disabled."); return true; } bool WatchDog::Disconnect() { if (watchDogTimer > 0) { RemoveTimer(watchDogTimer); LOG_INFO("Watchdog is disabled."); } shutdownStage = WATCHDOG_IDLE; return true; } bool WatchDog::initProperties() { INDI::DefaultDevice::initProperties(); IUFillNumber(&HeartBeatN[0], "WATCHDOG_HEARTBEAT_VALUE", "Threshold (min)", "%g", 0, 180, 10, 0); IUFillNumberVector(&HeartBeatNP, HeartBeatN, 1, getDeviceName(), "WATCHDOG_HEARTBEAT", "Heart beat", MAIN_CONTROL_TAB, IP_RW, 60, IPS_IDLE); IUFillText(&SettingsT[0], "INDISERVER_HOST", "indiserver host", "localhost"); IUFillText(&SettingsT[1], "INDISERVER_PORT", "indiserver port", "7624"); IUFillText(&SettingsT[2], "SHUTDOWN_SCRIPT", "shutdown script", nullptr); IUFillTextVector(&SettingsTP, SettingsT, 3, getDeviceName(), "WATCHDOG_SETTINGS", "Settings", MAIN_CONTROL_TAB, IP_RW, 60, IPS_IDLE); IUFillSwitch(&ShutdownProcedureS[PARK_MOUNT], "PARK_MOUNT", "Park Mount", ISS_OFF); IUFillSwitch(&ShutdownProcedureS[PARK_DOME], "PARK_DOME", "Park Dome", ISS_OFF); IUFillSwitch(&ShutdownProcedureS[EXECUTE_SCRIPT], "EXECUTE_SCRIPT", "Execute Script", ISS_OFF); IUFillSwitchVector(&ShutdownProcedureSP, ShutdownProcedureS, 3, getDeviceName(), "WATCHDOG_SHUTDOWN", "Shutdown", MAIN_CONTROL_TAB, IP_RW, ISR_NOFMANY, 60, IPS_IDLE); IUFillText(&ActiveDeviceT[ACTIVE_TELESCOPE], "ACTIVE_TELESCOPE", "Telescope", "Telescope Simulator"); IUFillText(&ActiveDeviceT[ACTIVE_DOME], "ACTIVE_DOME", "Dome", "Dome Simulator"); IUFillTextVector(&ActiveDeviceTP, ActiveDeviceT, 2, getDeviceName(), "ACTIVE_DEVICES", "Active devices", OPTIONS_TAB, IP_RW, 60, IPS_IDLE); addDebugControl(); return true; } void WatchDog::ISGetProperties(const char *dev) { // First we let our parent populate DefaultDevice::ISGetProperties(dev); defineNumber(&HeartBeatNP); defineText(&SettingsTP); defineSwitch(&ShutdownProcedureSP); defineText(&ActiveDeviceTP); // Only load config first time and not on subsequent client connections if (watchDogTimer == -1) loadConfig(true); //watchdogClient->setTelescope(ActiveDeviceT[0].text); //watchdogClient->setDome(ActiveDeviceT[1].text); } bool WatchDog::ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n) { // first check if it's for our device if (dev != nullptr && strcmp(dev, getDeviceName()) == 0) { if (!strcmp(SettingsTP.name, name)) { IUUpdateText(&SettingsTP, texts, names, n); SettingsTP.s = IPS_OK; IDSetText(&SettingsTP, nullptr); return true; } if (!strcmp(ActiveDeviceTP.name, name)) { if (watchdogClient->isBusy()) { ActiveDeviceTP.s = IPS_ALERT; IDSetText(&ActiveDeviceTP, nullptr); LOG_ERROR("Cannot change devices names while shutdown is in progress..."); return true; } IUUpdateText(&ActiveDeviceTP, texts, names, n); ActiveDeviceTP.s = IPS_OK; IDSetText(&ActiveDeviceTP, nullptr); //watchdogClient->setTelescope(ActiveDeviceT[0].text); //watchdogClient->setDome(ActiveDeviceT[1].text); return true; } } return INDI::DefaultDevice::ISNewText(dev, name, texts, names, n); } bool WatchDog::ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) { if (dev != nullptr && strcmp(dev, getDeviceName()) == 0) { if (!strcmp(HeartBeatNP.name, name)) { double prevHeartBeat = HeartBeatN[0].value; if (watchdogClient->isBusy()) { HeartBeatNP.s = IPS_ALERT; IDSetNumber(&HeartBeatNP, nullptr); LOG_ERROR("Cannot change heart beat while shutdown is in progress..."); return true; } IUUpdateNumber(&HeartBeatNP, values, names, n); HeartBeatNP.s = IPS_OK; if (HeartBeatN[0].value == 0) LOG_INFO("Watchdog is disabled."); else { if (isConnected()) { if (prevHeartBeat != HeartBeatN[0].value) DEBUGF(INDI::Logger::DBG_SESSION, "Watchdog is enabled. Shutdown is triggered after %g minutes of communication loss with " "the client.", HeartBeatN[0].value); LOG_DEBUG("Received heart beat from client."); RemoveTimer(watchDogTimer); watchDogTimer = SetTimer(HeartBeatN[0].value * 60 * 1000); } else LOG_INFO("Watchdog is armed. Please connect to enable it."); } IDSetNumber(&HeartBeatNP, nullptr); return true; } } return DefaultDevice::ISNewNumber(dev, name, values, names, n); } bool WatchDog::ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) { if (dev != nullptr && strcmp(dev, getDeviceName()) == 0) { if (!strcmp(ShutdownProcedureSP.name, name)) { IUUpdateSwitch(&ShutdownProcedureSP, states, names, n); if (ShutdownProcedureS[EXECUTE_SCRIPT].s == ISS_ON && (SettingsT[EXECUTE_SCRIPT].text == nullptr || SettingsT[EXECUTE_SCRIPT].text[0] == '\0')) { LOG_ERROR("Error: shutdown script file is not set."); ShutdownProcedureSP.s = IPS_ALERT; ShutdownProcedureS[EXECUTE_SCRIPT].s = ISS_OFF; } else ShutdownProcedureSP.s = IPS_OK; IDSetSwitch(&ShutdownProcedureSP, nullptr); return true; } } return DefaultDevice::ISNewSwitch(dev, name, states, names, n); } bool WatchDog::saveConfigItems(FILE *fp) { INDI::DefaultDevice::saveConfigItems(fp); IUSaveConfigNumber(fp, &HeartBeatNP); IUSaveConfigText(fp, &SettingsTP); IUSaveConfigText(fp, &ActiveDeviceTP); IUSaveConfigSwitch(fp, &ShutdownProcedureSP); return true; } void WatchDog::TimerHit() { // Timer is up, we need to start shutdown procedure // If nothing to do, then return if (ShutdownProcedureS[PARK_DOME].s == ISS_OFF && ShutdownProcedureS[PARK_MOUNT].s == ISS_OFF && ShutdownProcedureS[EXECUTE_SCRIPT].s == ISS_OFF) return; switch (shutdownStage) { // Connect to server case WATCHDOG_IDLE: ShutdownProcedureSP.s = IPS_BUSY; IDSetSwitch(&ShutdownProcedureSP, nullptr); LOG_WARN("Warning! Heartbeat threshold timed out, executing shutdown procedure..."); // No need to start client if only we need to execute the script if (ShutdownProcedureS[PARK_MOUNT].s == ISS_OFF && ShutdownProcedureS[PARK_DOME].s == ISS_OFF && ShutdownProcedureS[EXECUTE_SCRIPT].s == ISS_ON) { executeScript(); break; } // Watch mount if requied if (ShutdownProcedureS[PARK_MOUNT].s == ISS_ON) watchdogClient->setMount(ActiveDeviceT[0].text); // Watch dome if (ShutdownProcedureS[PARK_DOME].s == ISS_ON) watchdogClient->setDome(ActiveDeviceT[1].text); // Set indiserver host and port watchdogClient->setServer(SettingsT[0].text, atoi(SettingsT[1].text)); LOG_DEBUG("Connecting to INDI server..."); watchdogClient->connectServer(); shutdownStage = WATCHDOG_CLIENT_STARTED; break; case WATCHDOG_CLIENT_STARTED: // Check if client is ready if (watchdogClient->isConnected()) { LOGF_DEBUG("Connected to INDI server %s @ %s", SettingsT[0].text, SettingsT[1].text); if (ShutdownProcedureS[PARK_MOUNT].s == ISS_ON) parkMount(); else if (ShutdownProcedureS[PARK_DOME].s == ISS_ON) parkDome(); else if (ShutdownProcedureS[EXECUTE_SCRIPT].s == ISS_ON) executeScript(); } else LOG_DEBUG("Waiting for INDI server connection..."); break; case WATCHDOG_MOUNT_PARKED: { // check if mount is parked IPState mountState = watchdogClient->getMountParkState(); if (mountState == IPS_OK || mountState == IPS_IDLE) { LOG_INFO("Mount parked."); if (ShutdownProcedureS[PARK_DOME].s == ISS_ON) parkDome(); else if (ShutdownProcedureS[EXECUTE_SCRIPT].s == ISS_ON) executeScript(); else shutdownStage = WATCHDOG_COMPLETE; } } break; case WATCHDOG_DOME_PARKED: { // check if dome is parked IPState domeState = watchdogClient->getDomeParkState(); if (domeState == IPS_OK || domeState == IPS_IDLE) { LOG_INFO("Dome parked."); if (ShutdownProcedureS[EXECUTE_SCRIPT].s == ISS_ON) executeScript(); else shutdownStage = WATCHDOG_COMPLETE; } } break; case WATCHDOG_COMPLETE: LOG_INFO("Shutdown procedure complete."); ShutdownProcedureSP.s = IPS_OK; IDSetSwitch(&ShutdownProcedureSP, nullptr); watchdogClient->disconnectServer(); shutdownStage = WATCHDOG_IDLE; return; case WATCHDOG_ERROR: ShutdownProcedureSP.s = IPS_ALERT; IDSetSwitch(&ShutdownProcedureSP, nullptr); return; } SetTimer(POLLMS); } void WatchDog::parkDome() { if (watchdogClient->parkDome() == false) { LOG_ERROR("Error: Unable to park dome! Shutdown procedure terminated."); shutdownStage = WATCHDOG_ERROR; return; } LOG_INFO("Parking dome..."); shutdownStage = WATCHDOG_DOME_PARKED; } void WatchDog::parkMount() { if (watchdogClient->parkMount() == false) { LOG_ERROR("Error: Unable to park mount! Shutdown procedure terminated."); shutdownStage = WATCHDOG_ERROR; return; } LOG_INFO("Parking mount..."); shutdownStage = WATCHDOG_MOUNT_PARKED; } void WatchDog::executeScript() { // child if (fork() == 0) { int rc = execlp(SettingsT[EXECUTE_SCRIPT].text, SettingsT[EXECUTE_SCRIPT].text, nullptr); if (rc) exit(rc); } // parent else { int statval; LOGF_INFO("Executing script %s...", SettingsT[EXECUTE_SCRIPT].text); LOGF_INFO("Waiting for script with PID %d to complete...", getpid()); wait(&statval); if (WIFEXITED(statval)) { int exit_code = WEXITSTATUS(statval); LOGF_INFO("Script complete with exit code %d", exit_code); if (exit_code == 0) shutdownStage = WATCHDOG_COMPLETE; else { LOGF_ERROR("Error: script %s failed. Shutdown procedure terminated.", SettingsT[EXECUTE_SCRIPT].text); shutdownStage = WATCHDOG_ERROR; return; } } else { LOGF_ERROR( "Error: script %s did not terminate with exit. Shutdown procedure terminated.", SettingsT[EXECUTE_SCRIPT].text); shutdownStage = WATCHDOG_ERROR; return; } } } libindi/drivers/auxiliary/sqm.cpp0000664000175000017500000001654413263645557016475 0ustar jasemjasem/******************************************************************************* Copyright(c) 2016 Jasem Mutlaq. All rights reserved. INDI Sky Quality Meter Driver This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program 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 General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. The full GNU General Public License is included in this distribution in the file called LICENSE. *******************************************************************************/ #include "sqm.h" #include "connectionplugins/connectiontcp.h" #include "connectionplugins/connectionserial.h" #include #include #include #include // We declare an auto pointer to SQM. std::unique_ptr sqm(new SQM()); #define UNIT_TAB "Unit" void ISGetProperties(const char *dev) { sqm->ISGetProperties(dev); } void ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) { sqm->ISNewSwitch(dev, name, states, names, n); } void ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n) { sqm->ISNewText(dev, name, texts, names, n); } void ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) { sqm->ISNewNumber(dev, name, values, names, n); } void ISNewBLOB(const char *dev, const char *name, int sizes[], int blobsizes[], char *blobs[], char *formats[], char *names[], int n) { INDI_UNUSED(dev); INDI_UNUSED(name); INDI_UNUSED(sizes); INDI_UNUSED(blobsizes); INDI_UNUSED(blobs); INDI_UNUSED(formats); INDI_UNUSED(names); INDI_UNUSED(n); } void ISSnoopDevice(XMLEle *root) { sqm->ISSnoopDevice(root); } SQM::SQM() { setVersion(1, 0); } bool SQM::initProperties() { INDI::DefaultDevice::initProperties(); // Average Readings IUFillNumber(&AverageReadingN[0], "SKY_BRIGHTNESS", "Quality (mag/arcsec^2)", "%6.2f", -20, 30, 0, 0); IUFillNumber(&AverageReadingN[1], "SENSOR_FREQUENCY", "Freq (Hz)", "%6.2f", 0, 1000000, 0, 0); IUFillNumber(&AverageReadingN[2], "SENSOR_COUNTS", "Period (counts)", "%6.2f", 0, 1000000, 0, 0); IUFillNumber(&AverageReadingN[3], "SENSOR_PERIOD", "Period (s)", "%6.2f", 0, 1000000, 0, 0); IUFillNumber(&AverageReadingN[4], "SKY_TEMPERATURE", "Temperature (C)", "%6.2f", -50, 80, 0, 0); IUFillNumberVector(&AverageReadingNP, AverageReadingN, 5, getDeviceName(), "SKY_QUALITY", "Readings", MAIN_CONTROL_TAB, IP_RO, 0, IPS_IDLE); // Unit Info IUFillNumber(&UnitInfoN[0], "Protocol", "", "%.f", 0, 1000000, 0, 0); IUFillNumber(&UnitInfoN[1], "Model", "", "%.f", 0, 1000000, 0, 0); IUFillNumber(&UnitInfoN[2], "Feature", "", "%.f", 0, 1000000, 0, 0); IUFillNumber(&UnitInfoN[3], "Serial", "", "%.f", 0, 1000000, 0, 0); IUFillNumberVector(&UnitInfoNP, UnitInfoN, 4, getDeviceName(), "Unit Info", "", UNIT_TAB, IP_RW, 0, IPS_IDLE); if (sqmConnection & CONNECTION_SERIAL) { serialConnection = new Connection::Serial(this); serialConnection->registerHandshake([&]() { return getDeviceInfo(); }); registerConnection(serialConnection); } if (sqmConnection & CONNECTION_TCP) { tcpConnection = new Connection::TCP(this); tcpConnection->setDefaultHost("192.168.1.1"); tcpConnection->setDefaultPort(10001); tcpConnection->registerHandshake([&]() { return getDeviceInfo(); }); registerConnection(tcpConnection); } addDebugControl(); return true; } bool SQM::updateProperties() { INDI::DefaultDevice::updateProperties(); if (isConnected()) { // Already called by handshake //getDeviceInfo(); defineNumber(&AverageReadingNP); defineNumber(&UnitInfoNP); } else { deleteProperty(AverageReadingNP.name); deleteProperty(UnitInfoNP.name); } return true; } bool SQM::getReadings() { const char *cmd = "rx"; char buffer[57]={0}; LOGF_DEBUG("CMD: %s", cmd); ssize_t written = write(PortFD, cmd, 2); if (written < 2) { LOGF_ERROR("Error getting device readings: %s", strerror(errno)); return false; } ssize_t received = 0; while (received < 57) { ssize_t response = read(PortFD, buffer + received, 57 - received); if (response < 0) { LOGF_ERROR("Error getting device readings: %s", strerror(errno)); return false; } received += response; } if (received < 57) { LOG_ERROR("Error getting device readings"); return false; } LOGF_DEBUG("RES: %s", buffer); float mpsas, period_seconds, temperature; int frequency, period_counts; int rc = sscanf(buffer, "r,%fm,%dHz,%dc,%fs,%fC", &mpsas, &frequency, &period_counts, &period_seconds, &temperature); if (rc < 5) { LOGF_ERROR("Failed to parse input %s", buffer); return false; } AverageReadingN[0].value = mpsas; AverageReadingN[1].value = frequency; AverageReadingN[2].value = period_counts; AverageReadingN[3].value = period_seconds; AverageReadingN[4].value = temperature; return true; } const char *SQM::getDefaultName() { return (const char *)"SQM"; } bool SQM::getDeviceInfo() { const char *cmd = "ix"; char buffer[39]={0}; if (getActiveConnection() == serialConnection) { PortFD = serialConnection->getPortFD(); } else if (getActiveConnection() == tcpConnection) { PortFD = tcpConnection->getPortFD(); } LOGF_DEBUG("CMD: %s", cmd); ssize_t written = write(PortFD, cmd, 2); if (written < 2) { LOGF_ERROR("Error getting device info while writing to device: %s", strerror(errno)); return false; } ssize_t received = 0; while (received < 39) { ssize_t response = read(PortFD, buffer + received, 39 - received); if (response < 0) { LOGF_ERROR("Error getting device info while reading response: %s", strerror(errno)); return false; } received += response; } if (received < 39) { LOG_ERROR("Error getting device info"); return false; } LOGF_DEBUG("RES: %s", buffer); int protocol, model, feature, serial; int rc = sscanf(buffer, "i,%d,%d,%d,%d", &protocol, &model, &feature, &serial); if (rc < 4) { LOGF_ERROR("Failed to parse input %s", buffer); return false; } UnitInfoN[0].value = protocol; UnitInfoN[1].value = model; UnitInfoN[2].value = feature; UnitInfoN[3].value = serial; return true; } void SQM::TimerHit() { if (!isConnected()) return; bool rc = getReadings(); AverageReadingNP.s = rc ? IPS_OK : IPS_ALERT; IDSetNumber(&AverageReadingNP, nullptr); SetTimer(POLLMS); } libindi/drivers/auxiliary/skysafariclient.h0000664000175000017500000000647513263645557020537 0ustar jasemjasem/******************************************************************************* Copyright(c) 2017 Jasem Mutlaq. All rights reserved. INDI SkySafari Client for INDI Mounts. The clients communicates with INDI server to control the mount from SkySafari This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program 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 General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. The full GNU General Public License is included in this distribution in the file called LICENSE. *******************************************************************************/ #pragma once #include "baseclient.h" #include "basedevice.h" class SkySafariClient : public INDI::BaseClient { public: typedef enum { SLEW, TRACK, SYNC } GotoMode; SkySafariClient(); ~SkySafariClient(); bool isConnected() { return isReady; } void setMount(const std::string &value); INumberVectorProperty *getEquatorialCoords() { return eqCoordsNP; } bool sendEquatorialCoords(); INumberVectorProperty *getGeographiCoords() { return geoCoordsNP; } bool sendGeographicCoords(); ISwitchVectorProperty *getGotoMode() { return gotoModeSP; } bool sendGotoMode(); ISwitchVectorProperty *getMotionNS() { return motionNSSP; } bool setMotionNS(); ISwitchVectorProperty *getMotionWE() { return motionWESP; } bool setMotionWE(); bool parkMount(); IPState getMountParkState(); bool setSlewRate(int slewRate); bool abort(); ITextVectorProperty *getTimeUTC() { return timeUTC; } bool setTimeUTC(); protected: virtual void newDevice(INDI::BaseDevice *dp); virtual void removeDevice(INDI::BaseDevice */*dp*/) {} virtual void newProperty(INDI::Property *property); virtual void removeProperty(INDI::Property */*property*/) {} virtual void newBLOB(IBLOB */*bp*/) {} virtual void newSwitch(ISwitchVectorProperty */*svp*/) {} virtual void newNumber(INumberVectorProperty */*nvp*/) {} virtual void newMessage(INDI::BaseDevice */*dp*/, int /*messageID*/) {} virtual void newText(ITextVectorProperty */*tvp*/) {} virtual void newLight(ILightVectorProperty */*lvp*/) {} virtual void serverConnected() {} virtual void serverDisconnected(int /*exit_code*/) {} private: std::string mount; bool isReady, mountOnline; ISwitchVectorProperty *mountParkSP = nullptr; ISwitchVectorProperty *gotoModeSP = nullptr; INumberVectorProperty *eqCoordsNP = nullptr; INumberVectorProperty *geoCoordsNP = nullptr; ISwitchVectorProperty *abortSP = nullptr; ISwitchVectorProperty *slewRateSP = nullptr; ISwitchVectorProperty *motionNSSP = nullptr; ISwitchVectorProperty *motionWESP = nullptr; ITextVectorProperty *timeUTC = nullptr; }; libindi/drivers/auxiliary/99-gpusb.rules0000664000175000017500000000007013263645557017607 0ustar jasemjasemSUBSYSTEMS=="usb", ATTRS{idVendor}=="134a", MODE="0666" libindi/drivers/auxiliary/usb_dewpoint.h0000664000175000017500000001020213263645557020025 0ustar jasemjasem/* USB_Dewpoint Copyright (C) 2017 Jarno Paananen 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 */ #pragma once #include /***************************** USB_Dewpoint Commands **************************/ // All commands are exactly 6 bytes, no start/end markers #define UDP_CMD_LEN 6 #define UDP_STATUS_CMD "SGETAL" #define UDP_OUTPUT_CMD "S%1uO%03u" // channel 1-3, power 0-100 #define UDP_THRESHOLD_CMD "STHR%1u%1u" // channel 1-2, value 0-9 #define UDP_CALIBRATION_CMD "SCA%1u%1u%1u" // channel 1-2-A, value 0-9 #define UDP_LINK_CMD "SLINK%1u" // 0 or 1 to link channels 2 and 3 #define UDP_AUTO_CMD "SAUTO%1u" // 0 or 1 to enable auto mode #define UDP_AGGRESSIVITY_CMD "SAGGR%1u" // 1-4 (1, 2, 5, 10) #define UDP_IDENTIFY_CMD "SWHOIS" #define UDP_RESET_CMD "SEERAZ" /**************************** USB_Dewpoint Constants **************************/ // Responses also include "\n" #define UDP_DONE_RESPONSE "DONE" // Status response is like: // ##22.37/22.62/23.35/50.77/12.55/0/0/0/0/0/0/2/2/0/0/4** // Fields are in order: // temperature ch 1 // temperature ch 2 // temperature ambient // relative humidity // dew point // output ch 1 // output ch 2 // output ch 3 // calibration ch 1 // calibration ch 2 // calibration ambient // threshold ch 1 // threshold ch 2 // auto mode // outputs ch 2 & 3 linked // aggressivity #define UDP_STATUS_RESPONSE "##%f/%f/%f/%f/%f/%u/%u/%u/%u/%u/%u/%u/%u/%u/%u/%u**" #define UDP_STATUS_START "##" #define UDP_STATUS_SEPARATOR "/" #define UDP_STATUS_END "**" #define UDP_IDENTIFY_RESPONSE "UDP2(%u)" // Firmware version? Mine is "UDP2(1446)" /******************************************************************************/ namespace Connection { class Serial; }; class USBDewpoint : public INDI::DefaultDevice { public: USBDewpoint(); virtual ~USBDewpoint() = default; virtual const char *getDefaultName() override; virtual bool initProperties() override; virtual bool updateProperties() override; virtual bool ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) override; virtual bool ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) override; virtual void TimerHit() override; private: bool Handshake(); bool reset(); bool readSettings(); bool setOutput(unsigned int channel, unsigned int value); bool setCalibrations(unsigned int ch1, unsigned int ch2, unsigned int ambient); bool setThresholds(unsigned int ch1, unsigned int ch2); bool setAutoMode(bool enable); bool setLinkMode(bool enable); bool setAggressivity(unsigned int aggressivity); Connection::Serial *serialConnection{ nullptr }; int PortFD{ -1 }; INumber OutputsN[3]; INumberVectorProperty OutputsNP; INumber TemperaturesN[3]; INumberVectorProperty TemperaturesNP; INumber CalibrationsN[3]; INumberVectorProperty CalibrationsNP; INumber ThresholdsN[2]; INumberVectorProperty ThresholdsNP; INumber HumidityN[1]; INumberVectorProperty HumidityNP; INumber DewpointN[1]; INumberVectorProperty DewpointNP; INumber AggressivityN[1]; INumberVectorProperty AggressivityNP; ISwitch AutoModeS[2]; ISwitchVectorProperty AutoModeSP; ISwitch LinkOut23S[2]; ISwitchVectorProperty LinkOut23SP; ISwitch ResetS[1]; ISwitchVectorProperty ResetSP; INumber FWversionN[1]; INumberVectorProperty FWversionNP; }; libindi/drivers/auxiliary/STAR2kdriver.c0000664000175000017500000001034413263645557017547 0ustar jasemjasem/******************************************************************************* created 2014 G. Schmidt This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program 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 General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. The full GNU General Public License is included in this distribution in the file called LICENSE. *******************************************************************************/ #include "STAR2kdriver.h" #ifndef _WIN32 #include #endif #include #include #include #include /* STAR2000 RS232 box control functions */ int ConnectSTAR2k(char *port); void DisconnectSTAR2k(void); void StartPulse(int direction); void StopPulse(int direction); /* Serial communication utilities (taken from celestronprotocol.c) */ typedef fd_set telfds; static int writen(int fd, char *ptr, int nbytes); /* some static variables */ static int STAR2kPortFD; static int STAR2kConnectFlag = 0; static char STAR2kOpStat = 0; /************************* STAR2000 control functions *************************/ int ConnectSTAR2k(char *port) { #ifdef _WIN32 return (-1); #else struct termios tty; char initCmd[] = { 0x0D, 0x00 }; fprintf(stderr, "Connecting to port: %s\n", port); if (STAR2kConnectFlag != 0) return (0); /* Make the connection */ STAR2kPortFD = open(port, O_RDWR); if (STAR2kPortFD == -1) return (-1); tcgetattr(STAR2kPortFD, &tty); cfsetospeed(&tty, (speed_t)B9600); cfsetispeed(&tty, (speed_t)B9600); tty.c_cflag = (tty.c_cflag & ~CSIZE) | CS8; tty.c_iflag = IGNBRK; tty.c_lflag = 0; tty.c_oflag = 0; tty.c_cflag |= CLOCAL | CREAD; tty.c_cc[VMIN] = 1; tty.c_cc[VTIME] = 5; tty.c_iflag &= ~(IXON | IXOFF | IXANY); tty.c_cflag &= ~(PARENB | PARODD); tcsetattr(STAR2kPortFD, TCSANOW, &tty); /* Flush the input (read) buffer */ tcflush(STAR2kPortFD, TCIOFLUSH); /* initialize connection */ usleep(500000); writen(STAR2kPortFD, initCmd, 2); STAR2kOpStat = 0; return (0); #endif } /* Start a slew in chosen direction at slewRate */ /* Use auxilliary NexStar command set through the hand control computer */ void StartPulse(int direction) { if (direction == NORTH) { STAR2kOpStat |= S2K_NORTH_1; } else if (direction == EAST) { STAR2kOpStat |= S2K_EAST_1; } else if (direction == SOUTH) { STAR2kOpStat |= S2K_SOUTH_1; } else if (direction == WEST) { STAR2kOpStat |= S2K_WEST_1; } writen(STAR2kPortFD, &STAR2kOpStat, 1); } void StopPulse(int direction) { if (direction == NORTH) { STAR2kOpStat &= S2K_NORTH_0; } else if (direction == EAST) { STAR2kOpStat &= S2K_EAST_0; } else if (direction == SOUTH) { STAR2kOpStat &= S2K_SOUTH_0; } else if (direction == WEST) { STAR2kOpStat &= S2K_WEST_0; } else if (direction == ALL) { STAR2kOpStat = 0; } writen(STAR2kPortFD, &STAR2kOpStat, 1); } void DisconnectSTAR2k() { StopPulse(ALL); if (STAR2kConnectFlag == 1) close(STAR2kPortFD); STAR2kConnectFlag = 0; } /******************************* Serial port utilities ************************/ static int writen(int fd, char *ptr, int nbytes) { int nleft, nwritten; nleft = nbytes; while (nleft > 0) { nwritten = write(fd, ptr, nleft); if (nwritten <= 0) break; nleft -= nwritten; ptr += nwritten; } return (nbytes - nleft); } /******************************************************************************/ libindi/drivers/auxiliary/gpdriver.cpp0000664000175000017500000000731213263645557017510 0ustar jasemjasem/******************************************************************************* Copyright(c) 2012 Jasem Mutlaq. All rights reserved. This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program 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 General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. The full GNU General Public License is included in this distribution in the file called LICENSE. *******************************************************************************/ #include "gpdriver.h" GPUSBDriver::GPUSBDriver() { //ctor guideCMD[0] = 0; debug = false; } GPUSBDriver::~GPUSBDriver() { //dtor // usb_close(usb_handle); } bool GPUSBDriver::Connect() { dev = FindDevice(0x134A, 0x9020, 0); if (dev == nullptr) { IDLog("Error: No GPUSB device found\n"); return false; } int rc = Open(); return (rc != -1); } bool GPUSBDriver::Disconnect() { Close(); return true; } bool GPUSBDriver::startPulse(int direction) { int rc = 0; switch (direction) { case GPUSB_NORTH: guideCMD[0] &= GPUSB_CLEAR_DEC; guideCMD[0] |= (GPUSB_NORTH | GPUSB_LED_ON) & ~GPUSB_LED_RED; break; case GPUSB_WEST: guideCMD[0] &= GPUSB_CLEAR_RA; guideCMD[0] |= (GPUSB_WEST | GPUSB_LED_ON) & ~GPUSB_LED_RED; break; case GPUSB_SOUTH: guideCMD[0] &= GPUSB_CLEAR_DEC; guideCMD[0] |= GPUSB_SOUTH | GPUSB_LED_ON | GPUSB_LED_RED; break; case GPUSB_EAST: guideCMD[0] &= GPUSB_CLEAR_RA; guideCMD[0] |= GPUSB_EAST | GPUSB_LED_ON | GPUSB_LED_RED; break; } if (debug) IDLog("start command value is 0x%X\n", guideCMD[0]); rc = WriteBulk((unsigned char *)guideCMD, 1, 1000); if (debug) IDLog("startPulse WriteBulk returns %d\n", rc); if (rc == 1) return true; return false; } bool GPUSBDriver::stopPulse(int direction) { int rc = 0; switch (direction) { case GPUSB_NORTH: if (debug) IDLog("Stop North\n"); guideCMD[0] &= GPUSB_CLEAR_DEC; break; case GPUSB_WEST: if (debug) IDLog("Stop West\n"); guideCMD[0] &= GPUSB_CLEAR_RA; break; case GPUSB_SOUTH: if (debug) IDLog("Stop South\n"); guideCMD[0] &= GPUSB_CLEAR_DEC; break; case GPUSB_EAST: if (debug) IDLog("Stop East\n"); guideCMD[0] &= GPUSB_CLEAR_RA; break; } if ((guideCMD[0] & GPUSB_NORTH) || (guideCMD[0] & GPUSB_WEST)) guideCMD[0] &= ~GPUSB_LED_RED; else if ((guideCMD[0] & GPUSB_SOUTH) || (guideCMD[0] & GPUSB_EAST)) guideCMD[0] |= GPUSB_LED_RED; if ((guideCMD[0] & 0xF) == 0) guideCMD[0] = 0; if (debug) IDLog("stop command value is 0x%X\n", guideCMD[0]); rc = WriteBulk((unsigned char *)guideCMD, 1, 1000); if (debug) IDLog("stopPulse WriteBulk returns %d\n", rc); if (rc == 1) return true; return false; } libindi/drivers/auxiliary/gps_simulator.h0000664000175000017500000000312213263645557020216 0ustar jasemjasem/******************************************************************************* Copyright(c) 2015 Jasem Mutlaq. All rights reserved. Simple GPS Simulator This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program 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 General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. The full GNU General Public License is included in this distribution in the file called LICENSE. *******************************************************************************/ #pragma once #include "indigps.h" /** * @brief The GPSSimulator class provides a simple simulator that provide GPS Time and Location services. * * The time is fetched from the system time while the location is hard-coded to Latitude: 29.1 and Longitude: 48.5 */ class GPSSimulator : public INDI::GPS { public: GPSSimulator(); virtual ~GPSSimulator() = default; protected: // Generic indi device entries bool Connect(); bool Disconnect(); const char *getDefaultName(); IPState updateGPS(); }; libindi/drivers/auxiliary/gpusb.cpp0000664000175000017500000002235613263645557017013 0ustar jasemjasem/******************************************************************************* Copyright(c) 2012 Jasem Mutlaq. All rights reserved. This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program 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 General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. The full GNU General Public License is included in this distribution in the file called LICENSE. *******************************************************************************/ #include "gpusb.h" #include "gpdriver.h" #include #include #include // We declare an auto pointer to gpGuide. std::unique_ptr gpGuide(new GPUSB()); void ISGetProperties(const char *dev) { gpGuide->ISGetProperties(dev); } void ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) { gpGuide->ISNewSwitch(dev, name, states, names, n); } void ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n) { gpGuide->ISNewText(dev, name, texts, names, n); } void ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) { gpGuide->ISNewNumber(dev, name, values, names, n); } void ISNewBLOB(const char *dev, const char *name, int sizes[], int blobsizes[], char *blobs[], char *formats[], char *names[], int n) { INDI_UNUSED(dev); INDI_UNUSED(name); INDI_UNUSED(sizes); INDI_UNUSED(blobsizes); INDI_UNUSED(blobs); INDI_UNUSED(formats); INDI_UNUSED(names); INDI_UNUSED(n); } void ISSnoopDevice(XMLEle *root) { INDI_UNUSED(root); } GPUSB::GPUSB() { driver = new GPUSBDriver(); WEDir = NSDir = 0; InWEPulse = InNSPulse = false; WEPulseRequest = NSPulseRequest = 0; WEtimerID = NStimerID = 0; } GPUSB::~GPUSB() { //dtor delete (driver); } const char *GPUSB::getDefaultName() { return (const char *)"GPUSB"; } bool GPUSB::Connect() { driver->setDebug(isDebug()); bool rc = driver->Connect(); if (rc) LOG_INFO("GPUSB is online."); else LOG_ERROR("Error: cannot find GPUSB device."); return rc; } bool GPUSB::Disconnect() { LOG_INFO("GPSUSB is offline."); return driver->Disconnect(); } bool GPUSB::initProperties() { INDI::DefaultDevice::initProperties(); initGuiderProperties(getDeviceName(), MAIN_CONTROL_TAB); addDebugControl(); setDefaultPollingPeriod(250); return true; } bool GPUSB::updateProperties() { INDI::DefaultDevice::updateProperties(); if (isConnected()) { defineNumber(&GuideNSNP); defineNumber(&GuideWENP); } else { deleteProperty(GuideNSNP.name); deleteProperty(GuideWENP.name); } return true; } void GPUSB::ISGetProperties(const char *dev) { INDI::DefaultDevice::ISGetProperties(dev); } bool GPUSB::ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) { if (dev != nullptr && strcmp(dev, getDeviceName()) == 0) { if (!strcmp(name, GuideNSNP.name) || !strcmp(name, GuideWENP.name)) { processGuiderProperties(name, values, names, n); return true; } } return INDI::DefaultDevice::ISNewNumber(dev, name, values, names, n); } bool GPUSB::ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) { return INDI::DefaultDevice::ISNewSwitch(dev, name, states, names, n); } bool GPUSB::ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n) { return INDI::DefaultDevice::ISNewText(dev, name, texts, names, n); } bool GPUSB::ISSnoopDevice(XMLEle *root) { return INDI::DefaultDevice::ISSnoopDevice(root); } void GPUSB::debugTriggered(bool enable) { driver->setDebug(enable); } float GPUSB::CalcWEPulseTimeLeft() { double timesince; double timeleft; struct timeval now { 0, 0 }; gettimeofday(&now, nullptr); timesince = (double)(now.tv_sec * 1000.0 + now.tv_usec / 1000) - (double)(WEPulseStart.tv_sec * 1000.0 + WEPulseStart.tv_usec / 1000); timesince = timesince / 1000; timeleft = WEPulseRequest - timesince; return timeleft; } float GPUSB::CalcNSPulseTimeLeft() { double timesince; double timeleft; struct timeval now { 0, 0 }; gettimeofday(&now, nullptr); timesince = (double)(now.tv_sec * 1000.0 + now.tv_usec / 1000) - (double)(NSPulseStart.tv_sec * 1000.0 + NSPulseStart.tv_usec / 1000); timesince = timesince / 1000; timeleft = NSPulseRequest - timesince; return timeleft; } void GPUSB::TimerHit() { float timeleft; if (InWEPulse) { timeleft = CalcWEPulseTimeLeft(); if (timeleft < 1.0) { if (timeleft > 0.25) { // a quarter of a second or more // just set a tighter timer WEtimerID = SetTimer(250); } else { if (timeleft > 0.07) { // use an even tighter timer WEtimerID = SetTimer(50); } else { // it's real close now, so spin on it while (timeleft > 0) { int slv; slv = 100000 * timeleft; //IDLog("usleep %d\n",slv); usleep(slv); timeleft = CalcWEPulseTimeLeft(); } driver->stopPulse(WEDir); InWEPulse = false; // If we have another pulse, keep going if (!InNSPulse) SetTimer(250); } } } else if (!InNSPulse) { WEtimerID = SetTimer(250); } } if (InNSPulse) { timeleft = CalcNSPulseTimeLeft(); if (timeleft < 1.0) { if (timeleft > 0.25) { // a quarter of a second or more // just set a tighter timer NStimerID = SetTimer(250); } else { if (timeleft > 0.07) { // use an even tighter timer NStimerID = SetTimer(50); } else { // it's real close now, so spin on it while (timeleft > 0) { int slv; slv = 100000 * timeleft; //IDLog("usleep %d\n",slv); usleep(slv); timeleft = CalcNSPulseTimeLeft(); } driver->stopPulse(NSDir); InNSPulse = false; } } } else { NStimerID = SetTimer(250); } } } IPState GPUSB::GuideNorth(float ms) { RemoveTimer(NStimerID); driver->startPulse(GPUSB_NORTH); NSDir = GPUSB_NORTH; LOG_DEBUG("Starting NORTH guide"); if (ms <= POLLMS) { usleep(ms * 1000); driver->stopPulse(GPUSB_NORTH); return IPS_OK; } NSPulseRequest = ms / 1000.0; gettimeofday(&NSPulseStart, nullptr); InNSPulse = true; NStimerID = SetTimer(ms - 50); return IPS_BUSY; } IPState GPUSB::GuideSouth(float ms) { RemoveTimer(NStimerID); driver->startPulse(GPUSB_SOUTH); LOG_DEBUG("Starting SOUTH guide"); NSDir = GPUSB_SOUTH; if (ms <= POLLMS) { usleep(ms * 1000); driver->stopPulse(GPUSB_SOUTH); return IPS_OK; } NSPulseRequest = ms / 1000.0; gettimeofday(&NSPulseStart, nullptr); InNSPulse = true; NStimerID = SetTimer(ms - 50); return IPS_BUSY; } IPState GPUSB::GuideEast(float ms) { RemoveTimer(WEtimerID); driver->startPulse(GPUSB_EAST); LOG_DEBUG("Starting EAST guide"); WEDir = GPUSB_EAST; if (ms <= POLLMS) { usleep(ms * 1000); driver->stopPulse(GPUSB_EAST); return IPS_OK; } WEPulseRequest = ms / 1000.0; gettimeofday(&WEPulseStart, nullptr); InWEPulse = true; WEtimerID = SetTimer(ms - 50); return IPS_BUSY; } IPState GPUSB::GuideWest(float ms) { RemoveTimer(WEtimerID); driver->startPulse(GPUSB_WEST); LOG_DEBUG("Starting WEST guide"); WEDir = GPUSB_WEST; if (ms <= POLLMS) { usleep(ms * 1000); driver->stopPulse(GPUSB_WEST); return IPS_OK; } WEPulseRequest = ms / 1000.0; gettimeofday(&WEPulseStart, nullptr); InWEPulse = true; WEtimerID = SetTimer(ms - 50); return IPS_BUSY; } libindi/drivers/auxiliary/watchdog.h0000664000175000017500000000545613263645557017142 0ustar jasemjasem/******************************************************************************* Copyright(c) 2015 Jasem Mutlaq. All rights reserved. INDI Watchdog driver. The driver expects a heartbeat from the client every X minutes. If no heartbeat is received, the driver executes the shutdown procedures. This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program 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 General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. The full GNU General Public License is included in this distribution in the file called LICENSE. *******************************************************************************/ #pragma once #include "defaultdevice.h" class WatchDogClient; class WatchDog : public INDI::DefaultDevice { public: typedef enum { WATCHDOG_IDLE, WATCHDOG_CLIENT_STARTED, WATCHDOG_MOUNT_PARKED, WATCHDOG_DOME_PARKED, WATCHDOG_COMPLETE, WATCHDOG_ERROR } ShutdownStages; typedef enum { PARK_MOUNT, PARK_DOME, EXECUTE_SCRIPT } ShutdownProcedure; WatchDog(); virtual ~WatchDog(); virtual void ISGetProperties(const char *dev); //virtual bool ISSnoopDevice (XMLEle *root); virtual bool ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n); virtual bool ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n); virtual bool ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n); protected: virtual bool initProperties(); //virtual bool updateProperties(); virtual void TimerHit(); virtual bool Connect(); virtual bool Disconnect(); virtual const char *getDefaultName(); virtual bool saveConfigItems(FILE *fp); private: void parkDome(); void parkMount(); void executeScript(); INumberVectorProperty HeartBeatNP; INumber HeartBeatN[1]; ITextVectorProperty SettingsTP; IText SettingsT[3] {}; ISwitchVectorProperty ShutdownProcedureSP; ISwitch ShutdownProcedureS[3]; ITextVectorProperty ActiveDeviceTP; IText ActiveDeviceT[2] {}; enum { ACTIVE_TELESCOPE, ACTIVE_DOME }; WatchDogClient *watchdogClient; int watchDogTimer; ShutdownStages shutdownStage; }; libindi/drivers/auxiliary/joystick.cpp0000664000175000017500000002245413263645557017531 0ustar jasemjasem/******************************************************************************* Copyright(c) 2013 Jasem Mutlaq. All rights reserved. This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program 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 General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. The full GNU General Public License is included in this distribution in the file called LICENSE. *******************************************************************************/ #include "joystick.h" #include "joystickdriver.h" #include "indistandardproperty.h" #include #include // We declare an auto pointer to joystick. std::unique_ptr joystick(new JoyStick()); void ISGetProperties(const char *dev) { joystick->ISGetProperties(dev); } void ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) { joystick->ISNewSwitch(dev, name, states, names, n); } void ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n) { joystick->ISNewText(dev, name, texts, names, n); } void ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) { joystick->ISNewNumber(dev, name, values, names, n); } void ISNewBLOB(const char *dev, const char *name, int sizes[], int blobsizes[], char *blobs[], char *formats[], char *names[], int n) { INDI_UNUSED(dev); INDI_UNUSED(name); INDI_UNUSED(sizes); INDI_UNUSED(blobsizes); INDI_UNUSED(blobs); INDI_UNUSED(formats); INDI_UNUSED(names); INDI_UNUSED(n); } void ISSnoopDevice(XMLEle *root) { joystick->ISSnoopDevice(root); } JoyStick::JoyStick() { driver = new JoyStickDriver(); } JoyStick::~JoyStick() { delete (driver); } const char *JoyStick::getDefaultName() { return (const char *)"Joystick"; } bool JoyStick::Connect() { bool rc = driver->Connect(); if (rc) { LOG_INFO("Joystick is online."); setupParams(); } else LOG_INFO("Error: cannot find Joystick device."); return rc; } bool JoyStick::Disconnect() { LOG_INFO("Joystick is offline."); return driver->Disconnect(); } void JoyStick::setupParams() { char propName[16]={0}, propLabel[16]={0}; if (driver == nullptr) return; int nAxis = driver->getNumOfAxes(); int nJoysticks = driver->getNumOfJoysticks(); int nButtons = driver->getNumrOfButtons(); JoyStickNP = new INumberVectorProperty[nJoysticks]; JoyStickN = new INumber[nJoysticks * 2]; AxisN = new INumber[nAxis]; ButtonS = new ISwitch[nButtons]; for (int i = 0; i < nJoysticks * 2; i += 2) { snprintf(propName, 16, "JOYSTICK_%d", i / 2 + 1); snprintf(propLabel, 16, "Joystick %d", i / 2 + 1); IUFillNumber(&JoyStickN[i], "JOYSTICK_MAGNITUDE", "Magnitude", "%g", -32767.0, 32767.0, 0, 0); IUFillNumber(&JoyStickN[i + 1], "JOYSTICK_ANGLE", "Angle", "%g", 0, 360.0, 0, 0); IUFillNumberVector(&JoyStickNP[i / 2], JoyStickN + i, 2, getDeviceName(), propName, propLabel, "Monitor", IP_RO, 0, IPS_IDLE); } for (int i = 0; i < nAxis; i++) { snprintf(propName, 16, "AXIS_%d", i + 1); snprintf(propLabel, 16, "Axis %d", i + 1); IUFillNumber(&AxisN[i], propName, propLabel, "%g", -32767.0, 32767.0, 0, 0); } IUFillNumberVector(&AxisNP, AxisN, nAxis, getDeviceName(), "JOYSTICK_AXES", "Axes", "Monitor", IP_RO, 0, IPS_IDLE); for (int i = 0; i < nButtons; i++) { snprintf(propName, 16, "BUTTON_%d", i + 1); snprintf(propLabel, 16, "Button %d", i + 1); IUFillSwitch(&ButtonS[i], propName, propLabel, ISS_OFF); } IUFillSwitchVector(&ButtonSP, ButtonS, nButtons, getDeviceName(), "JOYSTICK_BUTTONS", "Buttons", "Monitor", IP_RO, ISR_NOFMANY, 0, IPS_IDLE); } bool JoyStick::initProperties() { INDI::DefaultDevice::initProperties(); IUFillText(&PortT[0], "PORT", "Port", "/dev/input/js0"); IUFillTextVector(&PortTP, PortT, 1, getDeviceName(), INDI::SP::DEVICE_PORT, "Ports", OPTIONS_TAB, IP_RW, 60, IPS_IDLE); IUFillText(&JoystickInfoT[0], "JOYSTICK_NAME", "Name", ""); IUFillText(&JoystickInfoT[1], "JOYSTICK_VERSION", "Version", ""); IUFillText(&JoystickInfoT[2], "JOYSTICK_NJOYSTICKS", "# Joysticks", ""); IUFillText(&JoystickInfoT[3], "JOYSTICK_NAXES", "# Axes", ""); IUFillText(&JoystickInfoT[4], "JOYSTICK_NBUTTONS", "# Buttons", ""); IUFillTextVector(&JoystickInfoTP, JoystickInfoT, 5, getDeviceName(), "JOYSTICK_INFO", "Joystick Info", MAIN_CONTROL_TAB, IP_RO, 60, IPS_IDLE); addDebugControl(); return true; } bool JoyStick::updateProperties() { INDI::DefaultDevice::updateProperties(); if (isConnected()) { char buf[8]; // Name IUSaveText(&JoystickInfoT[0], driver->getName()); // Version snprintf(buf, 8, "%d", driver->getVersion()); IUSaveText(&JoystickInfoT[1], buf); // # of Joysticks snprintf(buf, 8, "%d", driver->getNumOfJoysticks()); IUSaveText(&JoystickInfoT[2], buf); // # of Axes snprintf(buf, 8, "%d", driver->getNumOfAxes()); IUSaveText(&JoystickInfoT[3], buf); // # of buttons snprintf(buf, 8, "%d", driver->getNumrOfButtons()); IUSaveText(&JoystickInfoT[4], buf); defineText(&JoystickInfoTP); for (int i = 0; i < driver->getNumOfJoysticks(); i++) defineNumber(&JoyStickNP[i]); defineNumber(&AxisNP); defineSwitch(&ButtonSP); // N.B. Only set callbacks AFTER we define our properties above // because these calls backs otherwise can be called asynchronously // and they mess up INDI XML output driver->setJoystickCallback(joystickHelper); driver->setAxisCallback(axisHelper); driver->setButtonCallback(buttonHelper); } else { deleteProperty(JoystickInfoTP.name); for (int i = 0; i < driver->getNumOfJoysticks(); i++) deleteProperty(JoyStickNP[i].name); deleteProperty(AxisNP.name); deleteProperty(ButtonSP.name); delete[] JoyStickNP; delete[] JoyStickN; delete[] AxisN; delete[] ButtonS; } return true; } void JoyStick::ISGetProperties(const char *dev) { INDI::DefaultDevice::ISGetProperties(dev); defineText(&PortTP); loadConfig(true, INDI::SP::DEVICE_PORT); /* if (isConnected()) { for (int i = 0; i < driver->getNumOfJoysticks(); i++) defineNumber(&JoyStickNP[i]); defineNumber(&AxisNP); defineSwitch(&ButtonSP); } */ } bool JoyStick::ISSnoopDevice(XMLEle *root) { return INDI::DefaultDevice::ISSnoopDevice(root); } bool JoyStick::ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n) { if (dev != nullptr && strcmp(dev, getDeviceName()) == 0) { if (strcmp(name, PortTP.name) == 0) { PortTP.s = IPS_OK; IUUpdateText(&PortTP, texts, names, n); // Update client display IDSetText(&PortTP, nullptr); driver->setPort(PortT[0].text); return true; } } return DefaultDevice::ISNewText(dev, name, texts, names, n); } void JoyStick::joystickHelper(int joystick_n, double mag, double angle) { joystick->joystickEvent(joystick_n, mag, angle); } void JoyStick::buttonHelper(int button_n, int value) { joystick->buttonEvent(button_n, value); } void JoyStick::axisHelper(int axis_n, int value) { joystick->axisEvent(axis_n, value); } void JoyStick::joystickEvent(int joystick_n, double mag, double angle) { if (!isConnected()) return; LOGF_DEBUG("joystickEvent[%d]: %g @ %g", joystick_n, mag, angle); if (mag == 0) JoyStickNP[joystick_n].s = IPS_IDLE; else JoyStickNP[joystick_n].s = IPS_BUSY; JoyStickNP[joystick_n].np[0].value = mag; JoyStickNP[joystick_n].np[1].value = angle; IDSetNumber(&JoyStickNP[joystick_n], nullptr); } void JoyStick::axisEvent(int axis_n, int value) { if (!isConnected()) return; LOGF_DEBUG("axisEvent[%d]: %d", axis_n, value); if (value == 0) AxisNP.s = IPS_IDLE; else AxisNP.s = IPS_BUSY; AxisNP.np[axis_n].value = value; IDSetNumber(&AxisNP, nullptr); } void JoyStick::buttonEvent(int button_n, int value) { if (!isConnected()) return; LOGF_DEBUG("buttonEvent[%d]: %s", button_n, value > 0 ? "On" : "Off"); ButtonSP.s = IPS_OK; ButtonS[button_n].s = (value == 0) ? ISS_OFF : ISS_ON; IDSetSwitch(&ButtonSP, nullptr); } bool JoyStick::saveConfigItems(FILE *fp) { INDI::DefaultDevice::saveConfigItems(fp); IUSaveConfigText(fp, &PortTP); return true; } libindi/drivers/auxiliary/gpdriver.h0000664000175000017500000000325613263645557017160 0ustar jasemjasem/******************************************************************************* Copyright(c) 2012 Jasem Mutlaq. All rights reserved. This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program 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 General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. The full GNU General Public License is included in this distribution in the file called LICENSE. *******************************************************************************/ #pragma once #include "libs/indibase/indiusbdevice.h" enum { GPUSB_NORTH = 0x08, GPUSB_SOUTH = 0x04, GPUSB_EAST = 0x01, GPUSB_WEST = 0x02, GPUSB_LED_RED = 0x10, GPUSB_LED_ON = 0x20, GPUSB_CLEAR_RA = 0xFC, GPUSB_CLEAR_DEC = 0xF3 }; class GPUSBDriver : public INDI::USBDevice { public: GPUSBDriver(); virtual ~GPUSBDriver(); // Generic indi device entries bool Connect(); bool Disconnect(); bool startPulse(int direction); bool stopPulse(int direction); void setDebug(bool enable) { debug = enable; } private: char guideCMD[1]; bool debug; }; libindi/drivers/auxiliary/snapcap.cpp0000664000175000017500000005417513263645557017324 0ustar jasemjasem/******************************************************************************* Copyright(c) 2017 Jarno Paananen. All right reserved. Based on Flip Flat driver by: Copyright(c) 2015 Jasem Mutlaq. All rights reserved. This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program 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 General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. The full GNU General Public License is included in this distribution in the file called LICENSE. *******************************************************************************/ #include "snapcap.h" #include "indicom.h" #include "connectionplugins/connectionserial.h" #include #include #include #include #include // We declare an auto pointer to SnapCap. std::unique_ptr snapcap(new SnapCap()); #define SNAP_CMD 7 #define SNAP_RES 8 #define SNAP_TIMEOUT 3 void ISGetProperties(const char *dev) { snapcap->ISGetProperties(dev); } void ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) { snapcap->ISNewSwitch(dev, name, states, names, n); } void ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n) { snapcap->ISNewText(dev, name, texts, names, n); } void ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) { snapcap->ISNewNumber(dev, name, values, names, n); } void ISNewBLOB(const char *dev, const char *name, int sizes[], int blobsizes[], char *blobs[], char *formats[], char *names[], int n) { INDI_UNUSED(dev); INDI_UNUSED(name); INDI_UNUSED(sizes); INDI_UNUSED(blobsizes); INDI_UNUSED(blobs); INDI_UNUSED(formats); INDI_UNUSED(names); INDI_UNUSED(n); } void ISSnoopDevice(XMLEle *root) { snapcap->ISSnoopDevice(root); } SnapCap::SnapCap() : LightBoxInterface(this, true) { setVersion(1, 0); } bool SnapCap::initProperties() { INDI::DefaultDevice::initProperties(); // Status IUFillText(&StatusT[0], "Cover", "", nullptr); IUFillText(&StatusT[1], "Light", "", nullptr); IUFillText(&StatusT[2], "Motor", "", nullptr); IUFillTextVector(&StatusTP, StatusT, 3, getDeviceName(), "Status", "", MAIN_CONTROL_TAB, IP_RO, 60, IPS_IDLE); // Firmware version IUFillText(&FirmwareT[0], "Version", "", nullptr); IUFillTextVector(&FirmwareTP, FirmwareT, 1, getDeviceName(), "Firmware", "", MAIN_CONTROL_TAB, IP_RO, 60, IPS_IDLE); // Abort and force open/close buttons IUFillSwitch(&AbortS[0], "Abort", "", ISS_OFF); IUFillSwitchVector(&AbortSP, AbortS, 1, getDeviceName(), "Abort", "", MAIN_CONTROL_TAB, IP_RW, ISR_1OFMANY, 0, IPS_IDLE); IUFillSwitch(&ForceS[0], "Off", "", ISS_ON); IUFillSwitch(&ForceS[1], "On", "", ISS_OFF); IUFillSwitchVector(&ForceSP, ForceS, 2, getDeviceName(), "Force movement", "", MAIN_CONTROL_TAB, IP_RW, ISR_1OFMANY, 0, IPS_IDLE); initDustCapProperties(getDeviceName(), MAIN_CONTROL_TAB); initLightBoxProperties(getDeviceName(), MAIN_CONTROL_TAB); LightIntensityN[0].min = 25; LightIntensityN[0].max = 255; LightIntensityN[0].step = 10; hasLight = true; setDriverInterface(AUX_INTERFACE | LIGHTBOX_INTERFACE | DUSTCAP_INTERFACE); addAuxControls(); serialConnection = new Connection::Serial(this); serialConnection->registerHandshake([&]() { return Handshake(); }); registerConnection(serialConnection); serialConnection->setDefaultBaudRate(Connection::Serial::B_38400); return true; } void SnapCap::ISGetProperties(const char *dev) { INDI::DefaultDevice::ISGetProperties(dev); // Get Light box properties isGetLightBoxProperties(dev); } bool SnapCap::updateProperties() { INDI::DefaultDevice::updateProperties(); if (isConnected()) { defineSwitch(&ParkCapSP); if (hasLight) { defineSwitch(&LightSP); defineNumber(&LightIntensityNP); updateLightBoxProperties(); } defineText(&StatusTP); defineText(&FirmwareTP); defineSwitch(&AbortSP); defineSwitch(&ForceSP); getStartupData(); } else { deleteProperty(ParkCapSP.name); if (hasLight) { deleteProperty(LightSP.name); deleteProperty(LightIntensityNP.name); updateLightBoxProperties(); } deleteProperty(StatusTP.name); deleteProperty(FirmwareTP.name); deleteProperty(AbortSP.name); deleteProperty(ForceSP.name); } return true; } const char *SnapCap::getDefaultName() { return (const char *)"SnapCap"; } bool SnapCap::Handshake() { if (isSimulation()) { LOGF_INFO("Connected successfully to simulated %s. Retrieving startup data...", getDeviceName()); SetTimer(POLLMS); return true; } PortFD = serialConnection->getPortFD(); /* Drop RTS */ int i = 0; i |= TIOCM_RTS; if (ioctl(PortFD, TIOCMBIC, &i) != 0) { LOGF_ERROR("IOCTL error %s.", strerror(errno)); return false; } i |= TIOCM_RTS; if (ioctl(PortFD, TIOCMGET, &i) != 0) { LOGF_ERROR("IOCTL error %s.", strerror(errno)); return false; } if (!ping()) { LOG_ERROR("Device ping failed."); return false; } return true; } bool SnapCap::ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) { if (processLightBoxNumber(dev, name, values, names, n)) return true; return INDI::DefaultDevice::ISNewNumber(dev, name, values, names, n); } bool SnapCap::ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n) { if (dev != nullptr && strcmp(dev, getDeviceName()) == 0) { if (processLightBoxText(dev, name, texts, names, n)) return true; } return INDI::DefaultDevice::ISNewText(dev, name, texts, names, n); } bool SnapCap::ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) { if (dev != nullptr && strcmp(dev, getDeviceName()) == 0) { if (strcmp(AbortSP.name, name) == 0) { IUResetSwitch(&AbortSP); AbortSP.s = Abort(); IDSetSwitch(&AbortSP, nullptr); return true; } if (strcmp(ForceSP.name, name) == 0) { IUUpdateSwitch(&ForceSP, states, names, n); IDSetSwitch(&AbortSP, nullptr); return true; } if (processDustCapSwitch(dev, name, states, names, n)) return true; if (processLightBoxSwitch(dev, name, states, names, n)) return true; } return INDI::DefaultDevice::ISNewSwitch(dev, name, states, names, n); } bool SnapCap::ISSnoopDevice(XMLEle *root) { snoopLightBox(root); return INDI::DefaultDevice::ISSnoopDevice(root); } bool SnapCap::saveConfigItems(FILE *fp) { INDI::DefaultDevice::saveConfigItems(fp); return saveLightBoxConfigItems(fp); } bool SnapCap::ping() { bool found = getFirmwareVersion(); // Sometimes the controller does a corrupt reply at first connect // so retry once just in case if (!found) found = getFirmwareVersion(); return found; } bool SnapCap::getStartupData() { bool rc1 = getFirmwareVersion(); bool rc2 = getStatus(); bool rc3 = getBrightness(); return (rc1 && rc2 && rc3); } IPState SnapCap::ParkCap() { if (isSimulation()) { simulationWorkCounter = 3; return IPS_BUSY; } int nbytes_written = 0, nbytes_read = 0, rc = -1; char errstr[MAXRBUF]; char command[SNAP_CMD]; char response[SNAP_RES]; tcflush(PortFD, TCIOFLUSH); if (ForceS[1].s == ISS_ON) strncpy(command, ">c000", SNAP_CMD); // Force close command else strncpy(command, ">C000", SNAP_CMD); LOGF_DEBUG("CMD (%s)", command); command[SNAP_CMD - 2] = 0xD; command[SNAP_CMD - 1] = 0xA; if ((rc = tty_write(PortFD, command, SNAP_CMD, &nbytes_written)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("%s error: %s.", command, errstr); return IPS_ALERT; } if ((rc = tty_read_section(PortFD, response, 0xA, SNAP_TIMEOUT, &nbytes_read)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("%s: %s.", command, errstr); return IPS_ALERT; } response[nbytes_read - 2] = '\0'; LOGF_DEBUG("RES (%s)", response); if (strcmp(response, "*C000") == 0 || strcmp(response, "*c000") == 0) { // Set cover status to random value outside of range to force it to refresh prevCoverStatus = 10; targetCoverStatus = 2; return IPS_BUSY; } else return IPS_ALERT; } IPState SnapCap::UnParkCap() { if (isSimulation()) { simulationWorkCounter = 3; return IPS_BUSY; } int nbytes_written = 0, nbytes_read = 0, rc = -1; char errstr[MAXRBUF]; char command[SNAP_CMD]; char response[SNAP_RES]; tcflush(PortFD, TCIOFLUSH); if (ForceS[1].s == ISS_ON) strncpy(command, ">o000", SNAP_CMD); // Force open command else strncpy(command, ">O000", SNAP_CMD); LOGF_DEBUG("CMD (%s)", command); command[SNAP_CMD - 2] = 0xD; command[SNAP_CMD - 1] = 0xA; if ((rc = tty_write(PortFD, command, SNAP_CMD, &nbytes_written)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("%s error: %s.", command, errstr); return IPS_ALERT; } if ((rc = tty_read_section(PortFD, response, 0xA, SNAP_TIMEOUT, &nbytes_read)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("%s: %s.", command, errstr); return IPS_ALERT; } response[nbytes_read - 2] = '\0'; LOGF_DEBUG("RES (%s)", response); if (strcmp(response, "*O000") == 0 || strcmp(response, "*o000") == 0) { // Set cover status to random value outside of range to force it to refresh prevCoverStatus = 10; targetCoverStatus = 1; return IPS_BUSY; } else return IPS_ALERT; } IPState SnapCap::Abort() { if (isSimulation()) { simulationWorkCounter = 0; return IPS_OK; } int nbytes_written = 0, nbytes_read = 0, rc = -1; char errstr[MAXRBUF]; char command[SNAP_CMD]; char response[SNAP_RES]; tcflush(PortFD, TCIOFLUSH); strncpy(command, ">A000", SNAP_CMD); LOGF_DEBUG("CMD (%s)", command); command[SNAP_CMD - 2] = 0xD; command[SNAP_CMD - 1] = 0xA; if ((rc = tty_write(PortFD, command, SNAP_CMD, &nbytes_written)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("%s error: %s.", command, errstr); return IPS_ALERT; } if ((rc = tty_read_section(PortFD, response, 0xA, SNAP_TIMEOUT, &nbytes_read)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("%s: %s.", command, errstr); return IPS_ALERT; } response[nbytes_read - 2] = '\0'; LOGF_DEBUG("RES (%s)", response); if (strcmp(response, "*A000") == 0) { // Set cover status to random value outside of range to force it to refresh prevCoverStatus = 10; return IPS_OK; } else return IPS_ALERT; } bool SnapCap::EnableLightBox(bool enable) { int nbytes_written = 0, nbytes_read = 0, rc = -1; char errstr[MAXRBUF]; char command[SNAP_CMD]; char response[SNAP_RES]; if (hasLight && ParkCapS[CAP_UNPARK].s == ISS_ON) { LOG_ERROR("Cannot control light while cap is unparked."); return false; } if (isSimulation()) return true; tcflush(PortFD, TCIOFLUSH); if (enable) strncpy(command, ">L000", SNAP_CMD); else strncpy(command, ">D000", SNAP_CMD); LOGF_DEBUG("CMD (%s)", command); command[SNAP_CMD - 2] = 0xD; command[SNAP_CMD - 1] = 0xA; if ((rc = tty_write(PortFD, command, SNAP_CMD, &nbytes_written)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("%s error: %s.", command, errstr); return false; } if ((rc = tty_read_section(PortFD, response, 0xA, SNAP_TIMEOUT, &nbytes_read)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("%s: %s.", command, errstr); return false; } response[nbytes_read - 2] = '\0'; LOGF_DEBUG("RES (%s)", response); char expectedResponse[SNAP_RES]; if (enable) snprintf(expectedResponse, SNAP_RES, "*L000"); else snprintf(expectedResponse, SNAP_RES, "*D000"); if (strcmp(response, expectedResponse) == 0) return true; return false; } bool SnapCap::getStatus() { int nbytes_written = 0, nbytes_read = 0, rc = -1; char errstr[MAXRBUF]; char command[SNAP_CMD]; char response[SNAP_RES]; if (isSimulation()) { if (ParkCapSP.s == IPS_BUSY && --simulationWorkCounter <= 0) { ParkCapSP.s = IPS_IDLE; IDSetSwitch(&ParkCapSP, nullptr); simulationWorkCounter = 0; } if (ParkCapSP.s == IPS_BUSY) { response[2] = '1'; response[4] = '0'; } else { response[2] = '0'; // Parked/Closed if (ParkCapS[CAP_PARK].s == ISS_ON) response[4] = '2'; else response[4] = '1'; } response[3] = (LightS[FLAT_LIGHT_ON].s == ISS_ON) ? '1' : '0'; } else { tcflush(PortFD, TCIOFLUSH); strncpy(command, ">S000", SNAP_CMD); LOGF_DEBUG("CMD (%s)", command); command[SNAP_CMD - 2] = 0xD; command[SNAP_CMD - 1] = 0xA; if ((rc = tty_write(PortFD, command, SNAP_CMD, &nbytes_written)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("%s error: %s.", command, errstr); return false; } if ((rc = tty_read_section(PortFD, response, 0xA, SNAP_TIMEOUT, &nbytes_read)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("%s: %s.", command, errstr); return false; } response[nbytes_read - 2] = '\0'; LOGF_DEBUG("RES (%s)", response); } char motorStatus = response[2] - '0'; char lightStatus = response[3] - '0'; char coverStatus = response[4] - '0'; // Force cover status as it doesn't reflect moving state otherwise... if (motorStatus) { coverStatus = 0; } bool statusUpdated = false; if (coverStatus != prevCoverStatus) { prevCoverStatus = coverStatus; statusUpdated = true; switch (coverStatus) { case 0: IUSaveText(&StatusT[0], "Opening/closing"); break; case 1: if ((targetCoverStatus == 1 && ParkCapSP.s == IPS_BUSY) || ParkCapSP.s == IPS_IDLE) { IUSaveText(&StatusT[0], "Open"); IUResetSwitch(&ParkCapSP); ParkCapS[CAP_UNPARK].s = ISS_ON; ParkCapSP.s = IPS_OK; LOG_INFO("Cover open."); IDSetSwitch(&ParkCapSP, nullptr); } break; case 2: if ((targetCoverStatus == 2 && ParkCapSP.s == IPS_BUSY) || ParkCapSP.s == IPS_IDLE) { IUSaveText(&StatusT[0], "Closed"); IUResetSwitch(&ParkCapSP); ParkCapS[CAP_PARK].s = ISS_ON; ParkCapSP.s = IPS_OK; LOG_INFO("Cover closed."); IDSetSwitch(&ParkCapSP, nullptr); } break; case 3: IUSaveText(&StatusT[0], "Timed out"); break; case 4: IUSaveText(&StatusT[0], "Open circuit"); break; case 5: IUSaveText(&StatusT[0], "Overcurrent"); break; case 6: IUSaveText(&StatusT[0], "User abort"); break; } } if (lightStatus != prevLightStatus) { prevLightStatus = lightStatus; statusUpdated = true; switch (lightStatus) { case 0: IUSaveText(&StatusT[1], "Off"); if (LightS[0].s == ISS_ON) { LightS[0].s = ISS_OFF; LightS[1].s = ISS_ON; IDSetSwitch(&LightSP, nullptr); } break; case 1: IUSaveText(&StatusT[1], "On"); if (LightS[1].s == ISS_ON) { LightS[0].s = ISS_ON; LightS[1].s = ISS_OFF; IDSetSwitch(&LightSP, nullptr); } break; } } if (motorStatus != prevMotorStatus) { prevMotorStatus = motorStatus; statusUpdated = true; switch (motorStatus) { case 0: IUSaveText(&StatusT[2], "Stopped"); break; case 1: IUSaveText(&StatusT[2], "Running"); break; } } if (statusUpdated) IDSetText(&StatusTP, nullptr); return true; } bool SnapCap::getFirmwareVersion() { if (isSimulation()) { IUSaveText(&FirmwareT[0], "Simulation"); IDSetText(&FirmwareTP, nullptr); return true; } int nbytes_written = 0, nbytes_read = 0, rc = -1; char errstr[MAXRBUF]; char command[SNAP_CMD]; char response[SNAP_RES]; tcflush(PortFD, TCIOFLUSH); strncpy(command, ">V000", SNAP_CMD); LOGF_DEBUG("CMD (%s)", command); command[SNAP_CMD - 2] = 0xD; command[SNAP_CMD - 1] = 0xA; if ((rc = tty_write(PortFD, command, SNAP_CMD, &nbytes_written)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("%s error: %s.", command, errstr); return false; } if ((rc = tty_read_section(PortFD, response, 0xA, SNAP_TIMEOUT, &nbytes_read)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("%s: %s.", command, errstr); return false; } response[nbytes_read - 2] = '\0'; LOGF_DEBUG("RES (%s)", response); char versionString[4] = { 0 }; snprintf(versionString, 4, "%s", response + 2); IUSaveText(&FirmwareT[0], versionString); IDSetText(&FirmwareTP, nullptr); return true; } void SnapCap::TimerHit() { if (!isConnected()) return; getStatus(); SetTimer(POLLMS); } bool SnapCap::getBrightness() { if (isSimulation()) { return true; } int nbytes_written = 0, nbytes_read = 0, rc = -1; char errstr[MAXRBUF]; char command[SNAP_CMD]; char response[SNAP_RES]; tcflush(PortFD, TCIOFLUSH); strncpy(command, ">J000", SNAP_CMD); LOGF_DEBUG("CMD (%s)", command); command[SNAP_CMD - 2] = 0xD; command[SNAP_CMD - 1] = 0xA; if ((rc = tty_write(PortFD, command, SNAP_CMD, &nbytes_written)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("%s error: %s.", command, errstr); return false; } if ((rc = tty_read_section(PortFD, response, 0xA, SNAP_TIMEOUT, &nbytes_read)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("%s: %s.", command, errstr); return false; } response[nbytes_read - 2] = '\0'; LOGF_DEBUG("RES (%s)", response); char brightnessString[4] = { 0 }; snprintf(brightnessString, 4, "%s", response + 2); int brightnessValue = 0; rc = sscanf(brightnessString, "%d", &brightnessValue); if (rc <= 0) { LOGF_ERROR("Unable to parse brightness value (%s)", response); return false; } if (brightnessValue != prevBrightness) { prevBrightness = brightnessValue; LightIntensityN[0].value = brightnessValue; IDSetNumber(&LightIntensityNP, nullptr); } return true; } bool SnapCap::SetLightBoxBrightness(uint16_t value) { if (isSimulation()) { LightIntensityN[0].value = value; IDSetNumber(&LightIntensityNP, nullptr); return true; } int nbytes_written = 0, nbytes_read = 0, rc = -1; char errstr[MAXRBUF]; char command[SNAP_CMD]; char response[SNAP_RES]; tcflush(PortFD, TCIOFLUSH); snprintf(command, SNAP_CMD, ">B%03d", value); LOGF_DEBUG("CMD (%s)", command); command[SNAP_CMD - 2] = 0xD; command[SNAP_CMD - 1] = 0xA; if ((rc = tty_write(PortFD, command, SNAP_CMD, &nbytes_written)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("%s error: %s.", command, errstr); return false; } if ((rc = tty_read_section(PortFD, response, 0xA, SNAP_TIMEOUT, &nbytes_read)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("%s: %s.", command, errstr); return false; } response[nbytes_read - 2] = '\0'; LOGF_DEBUG("RES (%s)", response); char brightnessString[4] = { 0 }; snprintf(brightnessString, 4, "%s", response + 2); int brightnessValue = 0; rc = sscanf(brightnessString, "%d", &brightnessValue); if (rc <= 0) { LOGF_ERROR("Unable to parse brightness value (%s)", response); return false; } if (brightnessValue != prevBrightness) { prevBrightness = brightnessValue; LightIntensityN[0].value = brightnessValue; IDSetNumber(&LightIntensityNP, nullptr); } return true; } libindi/drivers/auxiliary/sqm.h0000664000175000017500000000401013263645557016123 0ustar jasemjasem/******************************************************************************* Copyright(c) 2016 Jasem Mutlaq. All rights reserved. INDI Sky Quality Meter Driver This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program 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 General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. The full GNU General Public License is included in this distribution in the file called LICENSE. *******************************************************************************/ #pragma once #include "defaultdevice.h" class SQM : public INDI::DefaultDevice { public: SQM(); virtual ~SQM() = default; virtual bool initProperties(); virtual bool updateProperties(); /** * @struct SqmConnection * @brief Holds the connection mode of the device. */ enum { CONNECTION_NONE = 1 << 0, CONNECTION_SERIAL = 1 << 1, CONNECTION_TCP = 1 << 2 } SqmConnection; protected: const char *getDefaultName(); void TimerHit(); private: bool getReadings(); bool getDeviceInfo(); // Readings INumberVectorProperty AverageReadingNP; INumber AverageReadingN[5]; // Device Information INumberVectorProperty UnitInfoNP; INumber UnitInfoN[4]; Connection::Serial *serialConnection { nullptr }; Connection::TCP *tcpConnection { nullptr }; int PortFD { -1 }; uint8_t sqmConnection { CONNECTION_SERIAL | CONNECTION_TCP }; }; libindi/drivers/auxiliary/99-flipflat.rules0000664000175000017500000000012213263645557020266 0ustar jasemjasemSUBSYSTEMS=="usb", ATTRS{idVendor}=="0403", ATTRS{idProduct}=="6001", MODE="0666" libindi/drivers/auxiliary/joystickdriver.cpp0000664000175000017500000001421013263645557020734 0ustar jasemjasem/******************************************************************************* Copyright(c) 2012 Jasem Mutlaq. All rights reserved. This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program 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 General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. The full GNU General Public License is included in this distribution in the file called LICENSE. *******************************************************************************/ #include "joystickdriver.h" #include #define MAX_JOYSTICKS 3 JoyStickDriver::JoyStickDriver() { active = false; pollMS = 100; joystick_fd = 0; joystick_ev = new js_event(); joystick_st = new joystick_state(); strncpy(dev_path, JOYSTICK_DEV, 256); joystickCallbackFunc = joystickEvent; axisCallbackFunc = axisEvent; buttonCallbackFunc = buttonEvent; } JoyStickDriver::~JoyStickDriver() { Disconnect(); delete joystick_st; delete joystick_ev; } void JoyStickDriver::setJoystickCallback(joystickFunc JoystickCallback) { joystickCallbackFunc = JoystickCallback; } void JoyStickDriver::setAxisCallback(axisFunc AxisCallback) { axisCallbackFunc = AxisCallback; } void JoyStickDriver::setButtonCallback(buttonFunc buttonCallback) { buttonCallbackFunc = buttonCallback; } void JoyStickDriver::setPort(const char *port) { strncpy(dev_path, port, 256); } bool JoyStickDriver::Connect() { joystick_fd = open(dev_path, O_RDONLY | O_NONBLOCK); if (joystick_fd > 0) { ioctl(joystick_fd, JSIOCGNAME(256), name); ioctl(joystick_fd, JSIOCGVERSION, &version); ioctl(joystick_fd, JSIOCGAXES, &axes); ioctl(joystick_fd, JSIOCGBUTTONS, &buttons); joystick_st->axis.reserve(axes); joystick_st->button.reserve(buttons); active = true; pthread_create(&thread, 0, &JoyStickDriver::loop, this); return true; } return false; } bool JoyStickDriver::Disconnect() { if (joystick_fd > 0) { active = false; pthread_join(thread, 0); close(joystick_fd); } joystick_fd = 0; return true; } void *JoyStickDriver::loop(void *obj) { while (reinterpret_cast(obj)->active) reinterpret_cast(obj)->readEv(); return obj; } void JoyStickDriver::readEv() { int bytes = read(joystick_fd, joystick_ev, sizeof(*joystick_ev)); if (bytes > 0) { joystick_ev->type &= ~JS_EVENT_INIT; if (joystick_ev->type & JS_EVENT_BUTTON) { joystick_st->button[joystick_ev->number] = joystick_ev->value; buttonCallbackFunc(joystick_ev->number, joystick_ev->value); } if (joystick_ev->type & JS_EVENT_AXIS) { joystick_st->axis[joystick_ev->number] = joystick_ev->value; int joystick_n = joystick_ev->number; if (joystick_n % 2 != 0) joystick_n--; if (joystick_n / 2.0 < MAX_JOYSTICKS) { joystick_position pos = joystickPosition(joystick_n); joystickCallbackFunc(joystick_n / 2, pos.r, pos.theta); } axisCallbackFunc(joystick_ev->number, joystick_ev->value); } } else usleep(pollMS * 1000); } joystick_position JoyStickDriver::joystickPosition(int n) { joystick_position pos; if (n > -1 && n < axes) { int i0 = n, i1 = n + 1; float x0 = joystick_st->axis[i0] / 32767.0f, y0 = -joystick_st->axis[i1] / 32767.0f; float x = x0 * sqrt(1 - pow(y0, 2) / 2.0f), y = y0 * sqrt(1 - pow(x0, 2) / 2.0f); pos.x = x0; pos.y = y0; pos.theta = atan2(y, x) * (180.0 / 3.141592653589); pos.r = sqrt(pow(y, 2) + pow(x, 2)); // For direction keys and scale/throttle keys if (pos.r == 0) { pos.r = -joystick_st->axis[i1]; if (pos.r < 0) { // Left if ((i0 % 2 == 0)) pos.theta = 180; // Down else pos.theta = 270; } else { // Up if ((i0 % 2 == 0)) pos.theta = 90; // Right else pos.theta = 0; } } else if (pos.theta < 0) pos.theta += 360; // Make sure to reset angle if magnitude is zero if (pos.r == 0) pos.theta = 0; } else { pos.theta = pos.r = pos.x = pos.y = 0.0f; } return pos; } bool JoyStickDriver::buttonPressed(int n) { return n > -1 && n < buttons ? joystick_st->button[n] : 0; } void JoyStickDriver::setPoll(int ms) { pollMS = ms; } void JoyStickDriver::joystickEvent(int joystick_n, double mag, double angle) { (void)joystick_n; (void)mag; (void)angle; } void JoyStickDriver::axisEvent(int axis_n, int value) { (void)axis_n; (void)value; } void JoyStickDriver::buttonEvent(int button_n, int button_value) { (void)button_n; (void)button_value; } const char *JoyStickDriver::getName() { return name; } __u32 JoyStickDriver::getVersion() { return version; } __u8 JoyStickDriver::getNumOfJoysticks() { int n_joysticks = axes / 2; if (axes % 2 != 0) n_joysticks++; if (n_joysticks > MAX_JOYSTICKS) n_joysticks = MAX_JOYSTICKS; return n_joysticks; } __u8 JoyStickDriver::getNumOfAxes() { return axes; } __u8 JoyStickDriver::getNumrOfButtons() { return buttons; } libindi/drivers/auxiliary/astrometrydriver.cpp0000664000175000017500000003515413263645557021320 0ustar jasemjasem/******************************************************************************* Copyright(c) 2017 Jasem Mutlaq. All rights reserved. INDI Astrometry.net Driver This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program 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 General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. The full GNU General Public License is included in this distribution in the file called LICENSE. *******************************************************************************/ #include "astrometrydriver.h" #include #include #include #include // We declare an auto pointer to AstrometryDriver. std::unique_ptr astrometry(new AstrometryDriver()); void ISGetProperties(const char *dev) { astrometry->ISGetProperties(dev); } void ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) { astrometry->ISNewSwitch(dev, name, states, names, n); } void ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n) { astrometry->ISNewText(dev, name, texts, names, n); } void ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) { astrometry->ISNewNumber(dev, name, values, names, n); } void ISNewBLOB(const char *dev, const char *name, int sizes[], int blobsizes[], char *blobs[], char *formats[], char *names[], int n) { astrometry->ISNewBLOB(dev, name, sizes, blobsizes, blobs, formats, names, n); } void ISSnoopDevice(XMLEle *root) { astrometry->ISSnoopDevice(root); } AstrometryDriver::AstrometryDriver() { setVersion(1, 0); } bool AstrometryDriver::initProperties() { INDI::DefaultDevice::initProperties(); /**********************************************/ /**************** Astrometry ******************/ /**********************************************/ // Solver Enable/Disable IUFillSwitch(&SolverS[0], "ASTROMETRY_SOLVER_ENABLE", "Enable", ISS_OFF); IUFillSwitch(&SolverS[1], "ASTROMETRY_SOLVER_DISABLE", "Disable", ISS_ON); IUFillSwitchVector(&SolverSP, SolverS, 2, getDeviceName(), "ASTROMETRY_SOLVER", "Solver", MAIN_CONTROL_TAB, IP_RW, ISR_1OFMANY, 0, IPS_IDLE); // Solver Settings IUFillText(&SolverSettingsT[ASTROMETRY_SETTINGS_BINARY], "ASTROMETRY_SETTINGS_BINARY", "Solver", "/usr/bin/solve-field"); IUFillText(&SolverSettingsT[ASTROMETRY_SETTINGS_OPTIONS], "ASTROMETRY_SETTINGS_OPTIONS", "Options", "--no-verify --no-plots --no-fits2fits --resort --downsample 2 -O"); IUFillTextVector(&SolverSettingsTP, SolverSettingsT, 2, getDeviceName(), "ASTROMETRY_SETTINGS", "Settings", MAIN_CONTROL_TAB, IP_WO, 0, IPS_IDLE); // Solver Results IUFillNumber(&SolverResultN[ASTROMETRY_RESULTS_PIXSCALE], "ASTROMETRY_RESULTS_PIXSCALE", "Pixscale (arcsec/pixel)", "%g", 0, 10000, 1, 0); IUFillNumber(&SolverResultN[ASTROMETRY_RESULTS_ORIENTATION], "ASTROMETRY_RESULTS_ORIENTATION", "Orientation (E of N) °", "%g", -360, 360, 1, 0); IUFillNumber(&SolverResultN[ASTROMETRY_RESULTS_RA], "ASTROMETRY_RESULTS_RA", "RA (J2000)", "%g", 0, 24, 1, 0); IUFillNumber(&SolverResultN[ASTROMETRY_RESULTS_DE], "ASTROMETRY_RESULTS_DE", "DE (J2000)", "%g", -90, 90, 1, 0); IUFillNumber(&SolverResultN[ASTROMETRY_RESULTS_PARITY], "ASTROMETRY_RESULTS_PARITY", "Parity", "%g", -1, 1, 1, 0); IUFillNumberVector(&SolverResultNP, SolverResultN, 5, getDeviceName(), "ASTROMETRY_RESULTS", "Results", MAIN_CONTROL_TAB, IP_RO, 0, IPS_IDLE); // Solver Data Blob IUFillBLOB(&SolverDataB[0], "ASTROMETRY_DATA_BLOB", "Image", ""); IUFillBLOBVector(&SolverDataBP, SolverDataB, 1, getDeviceName(), "ASTROMETRY_DATA", "Upload", MAIN_CONTROL_TAB, IP_WO, 60, IPS_IDLE); /**********************************************/ /**************** Snooping ********************/ /**********************************************/ // Snooped Devices IUFillText(&ActiveDeviceT[0], "ACTIVE_CCD", "CCD", "CCD Simulator"); IUFillTextVector(&ActiveDeviceTP, ActiveDeviceT, 1, getDeviceName(), "ACTIVE_DEVICES", "Snoop devices", OPTIONS_TAB, IP_RW, 60, IPS_IDLE); // Primary CCD Chip Data Blob IUFillBLOB(&CCDDataB[0], "CCD1", "Image", ""); IUFillBLOBVector(&CCDDataBP, CCDDataB, 1, ActiveDeviceT[0].text, "CCD1", "Image Data", "Image Info", IP_RO, 60, IPS_IDLE); IDSnoopDevice(ActiveDeviceT[0].text, "CCD1"); IDSnoopBLOBs(ActiveDeviceT[0].text, "CCD1", B_ONLY); addDebugControl(); return true; } void AstrometryDriver::ISGetProperties(const char *dev) { DefaultDevice::ISGetProperties(dev); defineText(&ActiveDeviceTP); loadConfig(true, "ACTIVE_DEVICES"); } bool AstrometryDriver::updateProperties() { INDI::DefaultDevice::updateProperties(); if (isConnected()) { defineSwitch(&SolverSP); defineText(&SolverSettingsTP); defineBLOB(&SolverDataBP); } else { if (SolverS[0].s == ISS_ON) { deleteProperty(SolverResultNP.name); } deleteProperty(SolverSP.name); deleteProperty(SolverSettingsTP.name); deleteProperty(SolverDataBP.name); } return true; } const char *AstrometryDriver::getDefaultName() { return (const char *)"Astrometry"; } bool AstrometryDriver::Connect() { return true; } bool AstrometryDriver::Disconnect() { return true; } bool AstrometryDriver::ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) { return INDI::DefaultDevice::ISNewNumber(dev, name, values, names, n); } bool AstrometryDriver::ISNewBLOB(const char *dev, const char *name, int sizes[], int blobsizes[], char *blobs[], char *formats[], char *names[], int n) { if (dev != nullptr && strcmp(dev, getDeviceName()) == 0) { if (strcmp(name, SolverDataBP.name) == 0) { SolverDataBP.s = IPS_OK; IDSetBLOB(&SolverDataBP, nullptr); // If the client explicitly uploaded the data then we solve it. if (SolverS[0].s == ISS_OFF) { SolverS[0].s = ISS_ON; SolverS[1].s = ISS_OFF; SolverSP.s = IPS_BUSY; LOG_INFO("Astrometry solver is enabled."); defineNumber(&SolverResultNP); } processBLOB(reinterpret_cast(blobs[0]), static_cast(sizes[0]), static_cast(blobsizes[0])); return true; } } return INDI::DefaultDevice::ISNewBLOB(dev, name, sizes, blobsizes, blobs, formats, names, n); } bool AstrometryDriver::ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n) { if (dev != nullptr && strcmp(dev, getDeviceName()) == 0) { // This is for our device // Now lets see if it's something we process here if (strcmp(name, ActiveDeviceTP.name) == 0) { ActiveDeviceTP.s = IPS_OK; IUUpdateText(&ActiveDeviceTP, texts, names, n); IDSetText(&ActiveDeviceTP, nullptr); // Update the property name! strncpy(CCDDataBP.device, ActiveDeviceT[0].text, MAXINDIDEVICE); IDSnoopDevice(ActiveDeviceT[0].text, "CCD1"); IDSnoopBLOBs(ActiveDeviceT[0].text, "CCD1", B_ONLY); // We processed this one, so, tell the world we did it return true; } if (strcmp(name, SolverSettingsTP.name) == 0) { IUUpdateText(&SolverSettingsTP, texts, names, n); SolverSettingsTP.s = IPS_OK; IDSetText(&SolverSettingsTP, nullptr); return true; } } return INDI::DefaultDevice::ISNewText(dev, name, texts, names, n); } bool AstrometryDriver::ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) { if (dev != nullptr && strcmp(dev, getDeviceName()) == 0) { // Astrometry Enable/Disable if (strcmp(name, SolverSP.name) == 0) { pthread_mutex_lock(&lock); IUUpdateSwitch(&SolverSP, states, names, n); SolverSP.s = IPS_OK; if (SolverS[0].s == ISS_ON) { LOG_INFO("Astrometry solver is enabled."); defineNumber(&SolverResultNP); } else { LOG_INFO("Astrometry solver is disabled."); deleteProperty(SolverResultNP.name); } IDSetSwitch(&SolverSP, nullptr); pthread_mutex_unlock(&lock); return true; } } return INDI::DefaultDevice::ISNewSwitch(dev, name, states, names, n); } bool AstrometryDriver::ISSnoopDevice(XMLEle *root) { if (SolverS[0].s == ISS_ON && IUSnoopBLOB(root, &CCDDataBP) == 0) { processBLOB(reinterpret_cast(CCDDataB[0].blob), static_cast(CCDDataB[0].size), static_cast(CCDDataB[0].bloblen)); return true; } return INDI::DefaultDevice::ISSnoopDevice(root); } bool AstrometryDriver::saveConfigItems(FILE *fp) { IUSaveConfigText(fp, &ActiveDeviceTP); IUSaveConfigText(fp, &SolverSettingsTP); return true; } bool AstrometryDriver::processBLOB(uint8_t *data, uint32_t size, uint32_t len) { FILE *fp = nullptr; char imageFileName[MAXRBUF]; uint8_t *processedData = data; // If size != len then we have compressed buffer if (size != len) { uint8_t *dataBuffer = new uint8_t[size]; uLongf destLen = size; if (dataBuffer == nullptr) { LOG_DEBUG("Unable to allocate memory for data buffer"); return false; } int r = uncompress(dataBuffer, &destLen, data, len); if (r != Z_OK) { LOGF_ERROR("Astrometry compression error: %d", r); delete[] dataBuffer; return false; } if (destLen != size) { LOGF_WARN("Discrepency between uncompressed data size %ld and expected size %ld", size, destLen); } processedData = dataBuffer; } strncpy(imageFileName, "/tmp/ccdsolver.fits", MAXRBUF); fp = fopen(imageFileName, "w"); if (fp == nullptr) { LOGF_ERROR("Unable to save image file (%s). %s", imageFileName, strerror(errno)); if (size != len) delete[] processedData; return false; } int n = 0; for (uint32_t nr = 0; nr < size; nr += n) n = fwrite(processedData + nr, 1, size - nr, fp); fclose(fp); // Do not forget to release uncompressed buffer if (size != len) delete[] processedData; pthread_mutex_lock(&lock); SolverSP.s = IPS_BUSY; LOG_INFO("Solving image..."); IDSetSwitch(&SolverSP, nullptr); pthread_mutex_unlock(&lock); int result = pthread_create(&solverThread, nullptr, &AstrometryDriver::runSolverHelper, this); if (result != 0) { SolverSP.s = IPS_ALERT; LOGF_INFO("Failed to create solver thread: %s", strerror(errno)); IDSetSwitch(&SolverSP, nullptr); } return true; } void *AstrometryDriver::runSolverHelper(void *context) { (static_cast(context))->runSolver(); return nullptr; } void AstrometryDriver::runSolver() { char cmd[MAXRBUF]={0}, line[256]={0}, parity_str[8]={0}; float ra = -1000, dec = -1000, angle = -1000, pixscale = -1000, parity = 0; snprintf(cmd, MAXRBUF, "%s %s -W /tmp/solution.wcs /tmp/ccdsolver.fits", SolverSettingsT[ASTROMETRY_SETTINGS_BINARY].text, SolverSettingsT[ASTROMETRY_SETTINGS_OPTIONS].text); LOGF_DEBUG("%s", cmd); FILE *handle = popen(cmd, "r"); if (handle == nullptr) { LOGF_DEBUG("Failed to run solver: %s", strerror(errno)); pthread_mutex_lock(&lock); SolverSP.s = IPS_ALERT; IDSetSwitch(&SolverSP, nullptr); pthread_mutex_unlock(&lock); return; } while (fgets(line, sizeof(line), handle) != nullptr) { LOGF_DEBUG("%s", line); sscanf(line, "Field rotation angle: up is %f", &angle); sscanf(line, "Field center: (RA,Dec) = (%f,%f)", &ra, &dec); sscanf(line, "Field parity: %s", parity_str); sscanf(line, "%*[^p]pixel scale %f", &pixscale); if (strcmp(parity_str, "pos") == 0) parity = 1; else if (strcmp(parity_str, "neg") == 0) parity = -1; if (ra != -1000 && dec != -1000 && angle != -1000 && pixscale != -1000) { // Pixscale is arcsec/pixel. Astrometry result is in arcmin SolverResultN[ASTROMETRY_RESULTS_PIXSCALE].value = pixscale; // Astrometry.net angle, E of N SolverResultN[ASTROMETRY_RESULTS_ORIENTATION].value = angle; // Astrometry.net J2000 RA in degrees SolverResultN[ASTROMETRY_RESULTS_RA].value = ra; // Astrometry.net J2000 DEC in degrees SolverResultN[ASTROMETRY_RESULTS_DE].value = dec; // Astrometry.net parity SolverResultN[ASTROMETRY_RESULTS_PARITY].value = parity; SolverResultNP.s = IPS_OK; IDSetNumber(&SolverResultNP, nullptr); pthread_mutex_lock(&lock); SolverSP.s = IPS_OK; IDSetSwitch(&SolverSP, nullptr); pthread_mutex_unlock(&lock); fclose(handle); LOG_INFO("Solver complete."); return; } pthread_mutex_lock(&lock); if (SolverS[1].s == ISS_ON) { SolverSP.s = IPS_IDLE; IDSetSwitch(&SolverSP, nullptr); pthread_mutex_unlock(&lock); fclose(handle); LOG_INFO("Solver cancelled."); return; } pthread_mutex_unlock(&lock); } fclose(handle); pthread_mutex_lock(&lock); SolverSP.s = IPS_ALERT; IDSetSwitch(&SolverSP, nullptr); LOG_INFO("Solver failed."); pthread_mutex_unlock(&lock); pthread_exit(nullptr); } libindi/drivers/auxiliary/flip_flat.cpp0000664000175000017500000005065213263645557017633 0ustar jasemjasem/******************************************************************************* Copyright(c) 2015 Jasem Mutlaq. All rights reserved. Simple GPS Simulator This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program 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 General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. The full GNU General Public License is included in this distribution in the file called LICENSE. *******************************************************************************/ #include "flip_flat.h" #include "indicom.h" #include "connectionplugins/connectionserial.h" #include #include #include #include #include // We declare an auto pointer to FlipFlat. std::unique_ptr flipflat(new FlipFlat()); #define FLAT_CMD 6 #define FLAT_RES 8 #define FLAT_TIMEOUT 3 void ISGetProperties(const char *dev) { flipflat->ISGetProperties(dev); } void ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) { flipflat->ISNewSwitch(dev, name, states, names, n); } void ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n) { flipflat->ISNewText(dev, name, texts, names, n); } void ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) { flipflat->ISNewNumber(dev, name, values, names, n); } void ISNewBLOB(const char *dev, const char *name, int sizes[], int blobsizes[], char *blobs[], char *formats[], char *names[], int n) { INDI_UNUSED(dev); INDI_UNUSED(name); INDI_UNUSED(sizes); INDI_UNUSED(blobsizes); INDI_UNUSED(blobs); INDI_UNUSED(formats); INDI_UNUSED(names); INDI_UNUSED(n); } void ISSnoopDevice(XMLEle *root) { flipflat->ISSnoopDevice(root); } FlipFlat::FlipFlat() : LightBoxInterface(this, true) { setVersion(1, 0); } bool FlipFlat::initProperties() { INDI::DefaultDevice::initProperties(); // Status IUFillText(&StatusT[0], "Cover", "", nullptr); IUFillText(&StatusT[1], "Light", "", nullptr); IUFillText(&StatusT[2], "Motor", "", nullptr); IUFillTextVector(&StatusTP, StatusT, 3, getDeviceName(), "Status", "", MAIN_CONTROL_TAB, IP_RO, 60, IPS_IDLE); // Firmware version IUFillText(&FirmwareT[0], "Version", "", nullptr); IUFillTextVector(&FirmwareTP, FirmwareT, 1, getDeviceName(), "Firmware", "", MAIN_CONTROL_TAB, IP_RO, 60, IPS_IDLE); initDustCapProperties(getDeviceName(), MAIN_CONTROL_TAB); initLightBoxProperties(getDeviceName(), MAIN_CONTROL_TAB); LightIntensityN[0].min = 0; LightIntensityN[0].max = 255; LightIntensityN[0].step = 10; // Set DUSTCAP_INTEFACE later on connect after we verify whether it's flip-flat (dust cover + light) or just flip-man (light only) setDriverInterface(AUX_INTERFACE | LIGHTBOX_INTERFACE); addAuxControls(); serialConnection = new Connection::Serial(this); serialConnection->registerHandshake([&]() { return Handshake(); }); registerConnection(serialConnection); return true; } void FlipFlat::ISGetProperties(const char *dev) { INDI::DefaultDevice::ISGetProperties(dev); // Get Light box properties isGetLightBoxProperties(dev); } bool FlipFlat::updateProperties() { INDI::DefaultDevice::updateProperties(); if (isConnected()) { if (isFlipFlat) defineSwitch(&ParkCapSP); defineSwitch(&LightSP); defineNumber(&LightIntensityNP); defineText(&StatusTP); defineText(&FirmwareTP); updateLightBoxProperties(); getStartupData(); } else { if (isFlipFlat) deleteProperty(ParkCapSP.name); deleteProperty(LightSP.name); deleteProperty(LightIntensityNP.name); deleteProperty(StatusTP.name); deleteProperty(FirmwareTP.name); updateLightBoxProperties(); } return true; } const char *FlipFlat::getDefaultName() { return (const char *)"Flip Flat"; } bool FlipFlat::Handshake() { if (isSimulation()) { LOGF_INFO("Connected successfuly to simulated %s. Retrieving startup data...", getDeviceName()); SetTimer(POLLMS); setDriverInterface(AUX_INTERFACE | LIGHTBOX_INTERFACE | DUSTCAP_INTERFACE); isFlipFlat = true; return true; } PortFD = serialConnection->getPortFD(); /* Drop RTS */ int i = 0; i |= TIOCM_RTS; if (ioctl(PortFD, TIOCMBIC, &i) != 0) { LOGF_ERROR("IOCTL error %s.", strerror(errno)); return false; } i |= TIOCM_RTS; if (ioctl(PortFD, TIOCMGET, &i) != 0) { LOGF_ERROR("IOCTL error %s.", strerror(errno)); return false; } if (!ping()) { LOG_ERROR("Device ping failed."); return false; } return true; } bool FlipFlat::ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) { if (processLightBoxNumber(dev, name, values, names, n)) return true; return INDI::DefaultDevice::ISNewNumber(dev, name, values, names, n); } bool FlipFlat::ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n) { if (dev != nullptr && strcmp(dev, getDeviceName()) == 0) { if (processLightBoxText(dev, name, texts, names, n)) return true; } return INDI::DefaultDevice::ISNewText(dev, name, texts, names, n); } bool FlipFlat::ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) { if (dev != nullptr && strcmp(dev, getDeviceName()) == 0) { if (processDustCapSwitch(dev, name, states, names, n)) return true; if (processLightBoxSwitch(dev, name, states, names, n)) return true; } return INDI::DefaultDevice::ISNewSwitch(dev, name, states, names, n); } bool FlipFlat::ISSnoopDevice(XMLEle *root) { snoopLightBox(root); return INDI::DefaultDevice::ISSnoopDevice(root); } bool FlipFlat::saveConfigItems(FILE *fp) { INDI::DefaultDevice::saveConfigItems(fp); return saveLightBoxConfigItems(fp); } bool FlipFlat::ping() { int nbytes_written = 0, nbytes_read = 0, rc = -1; char errstr[MAXRBUF]; char command[FLAT_CMD]; char response[FLAT_RES]; int i = 0; tcflush(PortFD, TCIOFLUSH); strncpy(command, ">P000", FLAT_CMD); LOGF_DEBUG("CMD (%s)", command); command[FLAT_CMD - 1] = 0xA; for (i = 0; i < 3; i++) { if ((rc = tty_write(PortFD, command, FLAT_CMD, &nbytes_written)) != TTY_OK) continue; if ((rc = tty_read_section(PortFD, response, 0xA, 1, &nbytes_read)) != TTY_OK) continue; else break; } if (i == 3) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("%s: %s.", command, errstr); return false; } response[nbytes_read - 1] = '\0'; LOGF_DEBUG("RES (%s)", response); char productString[3] = { 0 }; snprintf(productString, 3, "%s", response + 2); rc = sscanf(productString, "%d", &productID); if (rc <= 0) { LOGF_ERROR("Unable to parse input (%s)", response); return false; } if (productID == 99) { setDriverInterface(AUX_INTERFACE | LIGHTBOX_INTERFACE | DUSTCAP_INTERFACE); isFlipFlat = true; } else isFlipFlat = false; return true; } bool FlipFlat::getStartupData() { bool rc1 = getFirmwareVersion(); bool rc2 = getStatus(); bool rc3 = getBrightness(); return (rc1 && rc2 && rc3); } IPState FlipFlat::ParkCap() { if (isSimulation()) { simulationWorkCounter = 3; return IPS_BUSY; } int nbytes_written = 0, nbytes_read = 0, rc = -1; char errstr[MAXRBUF]; char command[FLAT_CMD]; char response[FLAT_RES]; tcflush(PortFD, TCIOFLUSH); strncpy(command, ">C000", FLAT_CMD); LOGF_DEBUG("CMD (%s)", command); command[FLAT_CMD - 1] = 0xA; if ((rc = tty_write(PortFD, command, FLAT_CMD, &nbytes_written)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("%s error: %s.", command, errstr); return IPS_ALERT; } if ((rc = tty_read_section(PortFD, response, 0xA, FLAT_TIMEOUT, &nbytes_read)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("%s: %s.", command, errstr); return IPS_ALERT; } response[nbytes_read - 1] = '\0'; LOGF_DEBUG("RES (%s)", response); char expectedResponse[FLAT_RES]; snprintf(expectedResponse, FLAT_RES, "*C%02d000", productID); if (strcmp(response, expectedResponse) == 0) { // Set cover status to random value outside of range to force it to refresh prevCoverStatus = 10; return IPS_BUSY; } else return IPS_ALERT; } IPState FlipFlat::UnParkCap() { if (isSimulation()) { simulationWorkCounter = 3; return IPS_BUSY; } int nbytes_written = 0, nbytes_read = 0, rc = -1; char errstr[MAXRBUF]; char command[FLAT_CMD]; char response[FLAT_RES]; tcflush(PortFD, TCIOFLUSH); strncpy(command, ">O000", FLAT_CMD); LOGF_DEBUG("CMD (%s)", command); command[FLAT_CMD - 1] = 0xA; if ((rc = tty_write(PortFD, command, FLAT_CMD, &nbytes_written)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("%s error: %s.", command, errstr); return IPS_ALERT; } if ((rc = tty_read_section(PortFD, response, 0xA, FLAT_TIMEOUT, &nbytes_read)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("%s: %s.", command, errstr); return IPS_ALERT; } response[nbytes_read - 1] = '\0'; LOGF_DEBUG("RES (%s)", response); char expectedResponse[FLAT_RES]; snprintf(expectedResponse, FLAT_RES, "*O%02d000", productID); if (strcmp(response, expectedResponse) == 0) { // Set cover status to random value outside of range to force it to refresh prevCoverStatus = 10; return IPS_BUSY; } else return IPS_ALERT; } bool FlipFlat::EnableLightBox(bool enable) { int nbytes_written = 0, nbytes_read = 0, rc = -1; char errstr[MAXRBUF]; char command[FLAT_CMD]; char response[FLAT_RES]; if (isFlipFlat && ParkCapS[1].s == ISS_ON) { LOG_ERROR("Cannot control light while cap is unparked."); return false; } if (isSimulation()) return true; tcflush(PortFD, TCIOFLUSH); if (enable) strncpy(command, ">L000", FLAT_CMD); else strncpy(command, ">D000", FLAT_CMD); LOGF_DEBUG("CMD (%s)", command); command[FLAT_CMD - 1] = 0xA; if ((rc = tty_write(PortFD, command, FLAT_CMD, &nbytes_written)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("%s error: %s.", command, errstr); return false; } if ((rc = tty_read_section(PortFD, response, 0xA, FLAT_TIMEOUT, &nbytes_read)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("%s: %s.", command, errstr); return false; } response[nbytes_read - 1] = '\0'; LOGF_DEBUG("RES (%s)", response); char expectedResponse[FLAT_RES]; if (enable) snprintf(expectedResponse, FLAT_RES, "*L%02d000", productID); else snprintf(expectedResponse, FLAT_RES, "*D%02d000", productID); if (strcmp(response, expectedResponse) == 0) return true; return false; } bool FlipFlat::getStatus() { int nbytes_written = 0, nbytes_read = 0, rc = -1; char errstr[MAXRBUF]; char command[FLAT_CMD]; char response[FLAT_RES]; if (isSimulation()) { if (ParkCapSP.s == IPS_BUSY && --simulationWorkCounter <= 0) { ParkCapSP.s = IPS_OK; IDSetSwitch(&ParkCapSP, nullptr); simulationWorkCounter = 0; } if (ParkCapSP.s == IPS_BUSY) { response[4] = '1'; response[6] = '0'; } else { response[4] = '0'; // Parked/Closed if (ParkCapS[CAP_PARK].s == ISS_ON) response[6] = '1'; else response[6] = '2'; } response[5] = (LightS[FLAT_LIGHT_ON].s == ISS_ON) ? '1' : '0'; } else { tcflush(PortFD, TCIOFLUSH); strncpy(command, ">S000", FLAT_CMD); LOGF_DEBUG("CMD (%s)", command); command[FLAT_CMD - 1] = 0xA; if ((rc = tty_write(PortFD, command, FLAT_CMD, &nbytes_written)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("%s error: %s.", command, errstr); return false; } if ((rc = tty_read_section(PortFD, response, 0xA, FLAT_TIMEOUT, &nbytes_read)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("%s: %s.", command, errstr); return false; } response[nbytes_read - 1] = '\0'; LOGF_DEBUG("RES (%s)", response); } char motorStatus = *(response + 4) - '0'; char lightStatus = *(response + 5) - '0'; char coverStatus = *(response + 6) - '0'; bool statusUpdated = false; if (coverStatus != prevCoverStatus) { prevCoverStatus = coverStatus; statusUpdated = true; switch (coverStatus) { case 0: IUSaveText(&StatusT[0], "Not Open/Closed"); break; case 1: IUSaveText(&StatusT[0], "Closed"); if (ParkCapSP.s == IPS_BUSY || ParkCapSP.s == IPS_IDLE) { IUResetSwitch(&ParkCapSP); ParkCapS[0].s = ISS_ON; ParkCapSP.s = IPS_OK; LOG_INFO("Cover closed."); IDSetSwitch(&ParkCapSP, nullptr); } break; case 2: IUSaveText(&StatusT[0], "Open"); if (ParkCapSP.s == IPS_BUSY || ParkCapSP.s == IPS_IDLE) { IUResetSwitch(&ParkCapSP); ParkCapS[1].s = ISS_ON; ParkCapSP.s = IPS_OK; LOG_INFO("Cover open."); IDSetSwitch(&ParkCapSP, nullptr); } break; case 3: IUSaveText(&StatusT[0], "Timed out"); break; } } if (lightStatus != prevLightStatus) { prevLightStatus = lightStatus; statusUpdated = true; switch (lightStatus) { case 0: IUSaveText(&StatusT[1], "Off"); if (LightS[0].s == ISS_ON) { LightS[0].s = ISS_OFF; LightS[1].s = ISS_ON; IDSetSwitch(&LightSP, nullptr); } break; case 1: IUSaveText(&StatusT[1], "On"); if (LightS[1].s == ISS_ON) { LightS[0].s = ISS_ON; LightS[1].s = ISS_OFF; IDSetSwitch(&LightSP, nullptr); } break; } } if (motorStatus != prevMotorStatus) { prevMotorStatus = motorStatus; statusUpdated = true; switch (motorStatus) { case 0: IUSaveText(&StatusT[2], "Stopped"); break; case 1: IUSaveText(&StatusT[2], "Running"); break; } } if (statusUpdated) IDSetText(&StatusTP, nullptr); return true; } bool FlipFlat::getFirmwareVersion() { if (isSimulation()) { IUSaveText(&FirmwareT[0], "Simulation"); IDSetText(&FirmwareTP, nullptr); return true; } int nbytes_written = 0, nbytes_read = 0, rc = -1; char errstr[MAXRBUF]; char command[FLAT_CMD]; char response[FLAT_RES]; tcflush(PortFD, TCIOFLUSH); strncpy(command, ">V000", FLAT_CMD); LOGF_DEBUG("CMD (%s)", command); command[FLAT_CMD - 1] = 0xA; if ((rc = tty_write(PortFD, command, FLAT_CMD, &nbytes_written)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("%s error: %s.", command, errstr); return false; } if ((rc = tty_read_section(PortFD, response, 0xA, FLAT_TIMEOUT, &nbytes_read)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("%s: %s.", command, errstr); return false; } response[nbytes_read - 1] = '\0'; LOGF_DEBUG("RES (%s)", response); char versionString[4]={0}; snprintf(versionString, 4, "%s", response + 4); IUSaveText(&FirmwareT[0], versionString); IDSetText(&FirmwareTP, nullptr); return true; } void FlipFlat::TimerHit() { if (!isConnected()) return; getStatus(); // parking or unparking timed out, try again if (ParkCapSP.s == IPS_BUSY && !strcmp(StatusT[0].text, "Timed out")) { if (ParkCapS[0].s == ISS_ON) ParkCap(); else UnParkCap(); } SetTimer(POLLMS); } bool FlipFlat::getBrightness() { if (isSimulation()) { return true; } int nbytes_written = 0, nbytes_read = 0, rc = -1; char errstr[MAXRBUF]; char command[FLAT_CMD]; char response[FLAT_RES]; tcflush(PortFD, TCIOFLUSH); strncpy(command, ">J000", FLAT_CMD); LOGF_DEBUG("CMD (%s)", command); command[FLAT_CMD - 1] = 0xA; if ((rc = tty_write(PortFD, command, FLAT_CMD, &nbytes_written)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("%s error: %s.", command, errstr); return false; } if ((rc = tty_read_section(PortFD, response, 0xA, FLAT_TIMEOUT, &nbytes_read)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("%s: %s.", command, errstr); return false; } response[nbytes_read - 1] = '\0'; LOGF_DEBUG("RES (%s)", response); char brightnessString[4]={0}; snprintf(brightnessString, 4, "%s", response + 4); int brightnessValue = 0; rc = sscanf(brightnessString, "%d", &brightnessValue); if (rc <= 0) { LOGF_ERROR("Unable to parse brightness value (%s)", response); return false; } if (brightnessValue != prevBrightness) { prevBrightness = brightnessValue; LightIntensityN[0].value = brightnessValue; IDSetNumber(&LightIntensityNP, nullptr); } return true; } bool FlipFlat::SetLightBoxBrightness(uint16_t value) { if (isSimulation()) { LightIntensityN[0].value = value; IDSetNumber(&LightIntensityNP, nullptr); return true; } int nbytes_written = 0, nbytes_read = 0, rc = -1; char errstr[MAXRBUF]; char command[FLAT_CMD]; char response[FLAT_RES]; tcflush(PortFD, TCIOFLUSH); snprintf(command, FLAT_CMD, ">B%03d", value); LOGF_DEBUG("CMD (%s)", command); command[FLAT_CMD - 1] = 0xA; if ((rc = tty_write(PortFD, command, FLAT_CMD, &nbytes_written)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("%s error: %s.", command, errstr); return false; } if ((rc = tty_read_section(PortFD, response, 0xA, FLAT_TIMEOUT, &nbytes_read)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("%s: %s.", command, errstr); return false; } response[nbytes_read - 1] = '\0'; LOGF_DEBUG("RES (%s)", response); char brightnessString[4]={0}; snprintf(brightnessString, 4, "%s", response + 4); int brightnessValue = 0; rc = sscanf(brightnessString, "%d", &brightnessValue); if (rc <= 0) { LOGF_ERROR("Unable to parse brightness value (%s)", response); return false; } if (brightnessValue != prevBrightness) { prevBrightness = brightnessValue; LightIntensityN[0].value = brightnessValue; IDSetNumber(&LightIntensityNP, nullptr); } return true; } libindi/drivers/auxiliary/skysafariclient.cpp0000664000175000017500000001653613263645557021071 0ustar jasemjasem/******************************************************************************* Copyright(c) 2017 Jasem Mutlaq. All rights reserved. INDI SkySafari Client for INDI Mounts. The clients communicates with INDI server to control the mount from SkySafari This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program 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 General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. The full GNU General Public License is included in this distribution in the file called LICENSE. *******************************************************************************/ #include "skysafariclient.h" #include #include /************************************************************************************** ** ***************************************************************************************/ SkySafariClient::SkySafariClient() { isReady = mountOnline = false; } /************************************************************************************** ** ***************************************************************************************/ SkySafariClient::~SkySafariClient() { } /************************************************************************************** ** ***************************************************************************************/ void SkySafariClient::newDevice(INDI::BaseDevice *dp) { IDLog("Receiving %s Device...\n", dp->getDeviceName()); if (std::string(dp->getDeviceName()) == mount) mountOnline = true; if (mountOnline) isReady = true; } /************************************************************************************** ** *************************************************************************************/ void SkySafariClient::newProperty(INDI::Property *property) { if (!strcmp(property->getName(), "TELESCOPE_PARK")) mountParkSP = property->getSwitch(); else if (!strcmp(property->getName(), "EQUATORIAL_EOD_COORD")) eqCoordsNP = property->getNumber(); else if (!strcmp(property->getName(), "GEOGRAPHIC_COORD")) geoCoordsNP = property->getNumber(); else if (!strcmp(property->getName(), "ON_COORD_SET")) gotoModeSP = property->getSwitch(); else if (!strcmp(property->getName(), "TELESCOPE_ABORT_MOTION")) abortSP = property->getSwitch(); else if (!strcmp(property->getName(), "TELESCOPE_SLEW_RATE")) slewRateSP = property->getSwitch(); else if (!strcmp(property->getName(), "TELESCOPE_MOTION_NS")) motionNSSP = property->getSwitch(); else if (!strcmp(property->getName(), "TELESCOPE_MOTION_WE")) motionWESP = property->getSwitch(); else if (!strcmp(property->getName(), "TIME_UTC")) timeUTC = property->getText(); } /************************************************************************************** ** ***************************************************************************************/ void SkySafariClient::setMount(const std::string &value) { mount = value; watchDevice(mount.c_str()); } /************************************************************************************** ** ***************************************************************************************/ bool SkySafariClient::parkMount() { if (mountParkSP == nullptr) return false; ISwitch *sw = IUFindSwitch(mountParkSP, "PARK"); if (sw == nullptr) return false; IUResetSwitch(mountParkSP); sw->s = ISS_ON; mountParkSP->s = IPS_BUSY; sendNewSwitch(mountParkSP); return true; } /************************************************************************************** ** ***************************************************************************************/ IPState SkySafariClient::getMountParkState() { return mountParkSP->s; } /************************************************************************************** ** ***************************************************************************************/ bool SkySafariClient::sendEquatorialCoords() { if (eqCoordsNP == nullptr) return false; eqCoordsNP->s = IPS_BUSY; sendNewNumber(eqCoordsNP); return true; } /************************************************************************************** ** ***************************************************************************************/ bool SkySafariClient::sendGeographicCoords() { if (geoCoordsNP == nullptr) return false; geoCoordsNP->s = IPS_BUSY; sendNewNumber(geoCoordsNP); return true; } /************************************************************************************** ** ***************************************************************************************/ bool SkySafariClient::sendGotoMode() { if (gotoModeSP == nullptr) return false; sendNewSwitch(gotoModeSP); return true; } /************************************************************************************** ** ***************************************************************************************/ bool SkySafariClient::abort() { if (abortSP == nullptr) return false; abortSP->sp[0].s = ISS_ON; sendNewSwitch(abortSP); return true; } /************************************************************************************** ** We get 0 to 3 which we have to map to whatever supported by mount, if any ***************************************************************************************/ bool SkySafariClient::setSlewRate(int slewRate) { if (slewRateSP == nullptr) return false; int maxSlewRate = slewRateSP->nsp - 1; int finalSlewRate = slewRate; // If slew rate is betwee min and max, we intepolate if (slewRate > 0 && slewRate < maxSlewRate) finalSlewRate = static_cast(ceil(slewRate * maxSlewRate / 3.0)); IUResetSwitch(slewRateSP); slewRateSP->sp[finalSlewRate].s = ISS_ON; sendNewSwitch(slewRateSP); return true; } /************************************************************************************** ** ***************************************************************************************/ bool SkySafariClient::setMotionNS() { if (motionNSSP == nullptr) return false; sendNewSwitch(motionNSSP); return true; } /************************************************************************************** ** ***************************************************************************************/ bool SkySafariClient::setMotionWE() { if (motionWESP == nullptr) return false; sendNewSwitch(motionWESP); return true; } /************************************************************************************** ** ***************************************************************************************/ bool SkySafariClient::setTimeUTC() { if (timeUTC == nullptr) return false; sendNewText(timeUTC); return true; } libindi/drivers/auxiliary/flip_flat.h0000664000175000017500000000550113263645557017271 0ustar jasemjasem/******************************************************************************* Copyright(c) 2015 Jasem Mutlaq. All rights reserved. Simple GPS Simulator This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program 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 General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. The full GNU General Public License is included in this distribution in the file called LICENSE. *******************************************************************************/ #pragma once #include "defaultdevice.h" #include "indilightboxinterface.h" #include "indidustcapinterface.h" #include namespace Connection { class Serial; } class FlipFlat : public INDI::DefaultDevice, public INDI::LightBoxInterface, public INDI::DustCapInterface { public: FlipFlat(); virtual ~FlipFlat() = default; virtual bool initProperties(); virtual void ISGetProperties(const char *dev); virtual bool updateProperties(); virtual bool ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n); virtual bool ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n); virtual bool ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n); virtual bool ISSnoopDevice(XMLEle *root); protected: const char *getDefaultName(); virtual bool saveConfigItems(FILE *fp); void TimerHit(); // From Dust Cap virtual IPState ParkCap(); virtual IPState UnParkCap(); // From Light Box virtual bool SetLightBoxBrightness(uint16_t value); virtual bool EnableLightBox(bool enable); private: bool getStartupData(); bool ping(); bool getStatus(); bool getFirmwareVersion(); bool getBrightness(); bool Handshake(); // Status ITextVectorProperty StatusTP; IText StatusT[3] {}; // Firmware version ITextVectorProperty FirmwareTP; IText FirmwareT[1] {}; int PortFD { -1 }; int productID { 0 }; bool isFlipFlat { false }; uint8_t simulationWorkCounter { 0 }; uint8_t prevCoverStatus { 0xFF }; uint8_t prevLightStatus { 0xFF }; uint8_t prevMotorStatus { 0xFF }; uint8_t prevBrightness { 0xFF }; Connection::Serial *serialConnection { nullptr }; }; libindi/drivers/auxiliary/usb_dewpoint.cpp0000664000175000017500000005075413263645557020400 0ustar jasemjasem/* USB_Dewpoint Copyright (C) 2017 Jarno Paananen 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 */ #include "usb_dewpoint.h" #include "connectionplugins/connectionserial.h" #include "indicom.h" #include #include #include #include #include #define USBDEWPOINT_TIMEOUT 3 std::unique_ptr usbDewpoint(new USBDewpoint()); void ISGetProperties(const char *dev) { usbDewpoint->ISGetProperties(dev); } void ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) { usbDewpoint->ISNewSwitch(dev, name, states, names, n); } void ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n) { usbDewpoint->ISNewText(dev, name, texts, names, n); } void ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) { usbDewpoint->ISNewNumber(dev, name, values, names, n); } void ISNewBLOB(const char *dev, const char *name, int sizes[], int blobsizes[], char *blobs[], char *formats[], char *names[], int n) { INDI_UNUSED(dev); INDI_UNUSED(name); INDI_UNUSED(sizes); INDI_UNUSED(blobsizes); INDI_UNUSED(blobs); INDI_UNUSED(formats); INDI_UNUSED(names); INDI_UNUSED(n); } void ISSnoopDevice(XMLEle *root) { usbDewpoint->ISSnoopDevice(root); } USBDewpoint::USBDewpoint() { setVersion(1, 0); } bool USBDewpoint::initProperties() { DefaultDevice::initProperties(); /* Channel duty cycles */ IUFillNumber(&OutputsN[0], "CHANNEL1", "Channel 1", "%3.0f", 0., 100., 10., 0.); IUFillNumber(&OutputsN[1], "CHANNEL2", "Channel 2", "%3.0f", 0., 100., 10., 0.); IUFillNumber(&OutputsN[2], "CHANNEL3", "Channel 3", "%3.0f", 0., 100., 10., 0.); IUFillNumberVector(&OutputsNP, OutputsN, 3, getDeviceName(), "OUTPUT", "Outputs", MAIN_CONTROL_TAB, IP_RW, 0, IPS_IDLE); /* Temperatures */ IUFillNumber(&TemperaturesN[0], "CHANNEL1", "Channel 1", "%3.2f", -50., 70., 0., 0.); IUFillNumber(&TemperaturesN[1], "CHANNEL2", "Channel 2", "%3.2f", -50., 70., 0., 0.); IUFillNumber(&TemperaturesN[2], "AMBIENT", "Ambient", "%3.2f", -50., 70., 0., 0.); IUFillNumberVector(&TemperaturesNP, TemperaturesN, 3, getDeviceName(), "TEMPERATURES", "Temperatures", MAIN_CONTROL_TAB, IP_RO, 0, IPS_IDLE); /* Humidity */ IUFillNumber(&HumidityN[0], "HUMIDITY", "Humidity", "%3.2f", 0., 100., 0., 0.); IUFillNumberVector(&HumidityNP, HumidityN, 1, getDeviceName(), "HUMIDITY", "Humidity", MAIN_CONTROL_TAB, IP_RO, 0, IPS_IDLE); /* Dew point */ IUFillNumber(&DewpointN[0], "DEWPOINT", "Dew point", "%3.2f", -50., 70., 0., 0.); IUFillNumberVector(&DewpointNP, DewpointN, 1, getDeviceName(), "DEWPOINT", "Dew point", MAIN_CONTROL_TAB, IP_RO, 0, IPS_IDLE); /* Temperature calibration values */ IUFillNumber(&CalibrationsN[0], "CHANNEL1", "Channel 1", "%1.0f", 0., 9., 1., 0.); IUFillNumber(&CalibrationsN[1], "CHANNEL2", "Channel 2", "%1.0f", 0., 9., 1., 0.); IUFillNumber(&CalibrationsN[2], "AMBIENT", "Ambient", "%1.0f", 0., 9., 1., 0.); IUFillNumberVector(&CalibrationsNP, CalibrationsN, 3, getDeviceName(), "CALIBRATIONS", "Calibrations", OPTIONS_TAB, IP_RW, 0, IPS_IDLE); /* Temperature threshold values */ IUFillNumber(&ThresholdsN[0], "CHANNEL1", "Channel 1", "%1.0f", 0., 9., 1., 0.); IUFillNumber(&ThresholdsN[1], "CHANNEL2", "Channel 2", "%1.0f", 0., 9., 1., 0.); IUFillNumberVector(&ThresholdsNP, ThresholdsN, 2, getDeviceName(), "THRESHOLDS", "Thresholds", OPTIONS_TAB, IP_RW, 0, IPS_IDLE); /* Heating aggressivity */ IUFillNumber(&AggressivityN[0], "AGGRESSIVITY", "Aggressivity", "%1.0f", 1., 4., 1., 1.); IUFillNumberVector(&AggressivityNP, AggressivityN, 1, getDeviceName(), "AGGRESSIVITY", "Aggressivity", OPTIONS_TAB, IP_RW, 0, IPS_IDLE); /* Automatic mode enable */ IUFillSwitch(&AutoModeS[0], "MANUAL", "Manual", ISS_OFF); IUFillSwitch(&AutoModeS[1], "AUTO", "Automatic", ISS_ON); IUFillSwitchVector(&AutoModeSP, AutoModeS, 2, getDeviceName(), "MODE", "Operating mode", MAIN_CONTROL_TAB, IP_RW, ISR_1OFMANY, 0, IPS_IDLE); /* Link channel 2 & 3 */ IUFillSwitch(&LinkOut23S[0], "INDEPENDENT", "Independent", ISS_ON); IUFillSwitch(&LinkOut23S[1], "LINK", "Link", ISS_OFF); IUFillSwitchVector(&LinkOut23SP, LinkOut23S, 2, getDeviceName(), "LINK23", "Link ch 2&3", OPTIONS_TAB, IP_RW, ISR_1OFMANY, 0, IPS_IDLE); /* Reset settings */ IUFillSwitch(&ResetS[0], "Reset", "", ISS_OFF); IUFillSwitchVector(&ResetSP, ResetS, 1, getDeviceName(), "Reset", "", OPTIONS_TAB, IP_RW, ISR_1OFMANY, 0, IPS_IDLE); /* Firmware version */ IUFillNumber(&FWversionN[0], "FIRMWARE", "Firmware Version", "%4.0f", 0., 65535., 1., 0.); IUFillNumberVector(&FWversionNP, FWversionN, 1, getDeviceName(), "FW_VERSION", "Firmware", OPTIONS_TAB, IP_RO, 0, IPS_IDLE); setDriverInterface(AUX_INTERFACE); addDebugControl(); addConfigurationControl(); setDefaultPollingPeriod(10000); // No simulation control for now serialConnection = new Connection::Serial(this); serialConnection->registerHandshake([&]() { return Handshake(); }); registerConnection(serialConnection); return true; } bool USBDewpoint::updateProperties() { DefaultDevice::updateProperties(); if (isConnected()) { defineNumber(&OutputsNP); defineNumber(&TemperaturesNP); defineNumber(&HumidityNP); defineNumber(&DewpointNP); defineNumber(&CalibrationsNP); defineNumber(&ThresholdsNP); defineNumber(&AggressivityNP); defineSwitch(&AutoModeSP); defineSwitch(&LinkOut23SP); defineSwitch(&ResetSP); defineNumber(&FWversionNP); loadConfig(true); LOG_INFO("USB_Dewpoint paramaters updated, device ready for use."); SetTimer(POLLMS); } else { deleteProperty(OutputsNP.name); deleteProperty(TemperaturesNP.name); deleteProperty(HumidityNP.name); deleteProperty(DewpointNP.name); deleteProperty(CalibrationsNP.name); deleteProperty(ThresholdsNP.name); deleteProperty(AggressivityNP.name); deleteProperty(AutoModeSP.name); deleteProperty(LinkOut23SP.name); deleteProperty(ResetSP.name); deleteProperty(FWversionNP.name); } return true; } bool USBDewpoint::Handshake() { PortFD = serialConnection->getPortFD(); LOG_INFO("USB_Dewpoint is online. Getting device parameters..."); char cmd[] = UDP_IDENTIFY_CMD; int nbytes_written = 0, nbytes_read = 0, rc = -1; char errstr[MAXRBUF]; char resp[64]; tcflush(PortFD, TCIOFLUSH); LOGF_DEBUG("CMD: %s.", cmd); if ((rc = tty_write(PortFD, cmd, strlen(cmd), &nbytes_written)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("HandShake error: %s.", errstr); return false; } if ((rc = tty_read_section(PortFD, resp, '\n', USBDEWPOINT_TIMEOUT, &nbytes_read)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("HandShake error: %s.", errstr); return false; } resp[nbytes_read] = 0; LOGF_DEBUG("Resp: %s.", resp); int firmware = -1; int ok = sscanf(resp, UDP_IDENTIFY_RESPONSE, &firmware); if (ok != 1) { return false; } FWversionN[0].value = firmware; FWversionNP.s = IPS_OK; return true; } const char *USBDewpoint::getDefaultName() { return "USB_Dewpoint"; } bool USBDewpoint::setOutput(unsigned int channel, unsigned int value) { char cmd[UDP_CMD_LEN + 1]; int nbytes_written = 0, nbytes_read = 0, rc = -1; char errstr[MAXRBUF]; char resp[64]; sprintf(cmd, UDP_OUTPUT_CMD, channel, value); tcflush(PortFD, TCIOFLUSH); LOGF_DEBUG("CMD: %s.", cmd); if ((rc = tty_write(PortFD, cmd, UDP_CMD_LEN, &nbytes_written)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("setOutputs error: %s.", errstr); return false; } if ((rc = tty_read_section(PortFD, resp, '\n', USBDEWPOINT_TIMEOUT, &nbytes_read)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("setOutputs error: %s.", errstr); return false; } resp[nbytes_read] = 0; LOGF_DEBUG("Resp: %s.", resp); return true; } bool USBDewpoint::setCalibrations(unsigned int ch1, unsigned int ch2, unsigned int ambient) { char cmd[UDP_CMD_LEN + 1]; int nbytes_written = 0, nbytes_read = 0, rc = -1; char errstr[MAXRBUF]; char resp[64]; sprintf(cmd, UDP_CALIBRATION_CMD, ch1, ch2, ambient); tcflush(PortFD, TCIOFLUSH); LOGF_DEBUG("CMD: %s.", cmd); if ((rc = tty_write(PortFD, cmd, UDP_CMD_LEN, &nbytes_written)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("setCalibrations error: %s.", errstr); return false; } if ((rc = tty_read_section(PortFD, resp, '\n', USBDEWPOINT_TIMEOUT, &nbytes_read)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("setCalibrations error: %s.", errstr); return false; } resp[nbytes_read] = 0; LOGF_DEBUG("Resp: %s.", resp); return true; } bool USBDewpoint::setThresholds(unsigned int ch1, unsigned int ch2) { char cmd[UDP_CMD_LEN + 1]; int nbytes_written = 0, nbytes_read = 0, rc = -1; char errstr[MAXRBUF]; char resp[64]; sprintf(cmd, UDP_THRESHOLD_CMD, ch1, ch2); tcflush(PortFD, TCIOFLUSH); LOGF_DEBUG("CMD: %s.", cmd); if ((rc = tty_write(PortFD, cmd, UDP_CMD_LEN, &nbytes_written)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("setThresholds error: %s.", errstr); return false; } if ((rc = tty_read_section(PortFD, resp, '\n', USBDEWPOINT_TIMEOUT, &nbytes_read)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("setThresholds error: %s.", errstr); return false; } resp[nbytes_read] = 0; LOGF_DEBUG("Resp: %s.", resp); return true; } bool USBDewpoint::setAggressivity(unsigned int aggressivity) { char cmd[UDP_CMD_LEN + 1]; int nbytes_written = 0, nbytes_read = 0, rc = -1; char errstr[MAXRBUF]; char resp[64]; sprintf(cmd, UDP_AGGRESSIVITY_CMD, aggressivity); tcflush(PortFD, TCIOFLUSH); LOGF_DEBUG("CMD: %s.", cmd); if ((rc = tty_write(PortFD, cmd, UDP_CMD_LEN, &nbytes_written)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("setAggressivity error: %s.", errstr); return false; } if ((rc = tty_read_section(PortFD, resp, '\n', USBDEWPOINT_TIMEOUT, &nbytes_read)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("setAggressivity error: %s.", errstr); return false; } resp[nbytes_read] = 0; LOGF_DEBUG("Resp: %s.", resp); return true; } bool USBDewpoint::reset() { char cmd[] = UDP_RESET_CMD; int nbytes_written = 0, nbytes_read = 0, rc = -1; char errstr[MAXRBUF]; char resp[64]; tcflush(PortFD, TCIOFLUSH); LOGF_DEBUG("CMD: %s.", cmd); if ((rc = tty_write(PortFD, cmd, UDP_CMD_LEN, &nbytes_written)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("reset error: %s.", errstr); return false; } if ((rc = tty_read_section(PortFD, resp, '\n', USBDEWPOINT_TIMEOUT, &nbytes_read)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("reset error: %s.", errstr); return false; } resp[nbytes_read] = 0; LOGF_DEBUG("Resp: %s.", resp); return true; } bool USBDewpoint::setAutoMode(bool enable) { char cmd[UDP_CMD_LEN + 1]; int nbytes_written = 0, nbytes_read = 0, rc = -1; char errstr[MAXRBUF]; char resp[64]; sprintf(cmd, UDP_AUTO_CMD, enable ? 1 : 0); tcflush(PortFD, TCIOFLUSH); LOGF_DEBUG("CMD: %s.", cmd); if ((rc = tty_write(PortFD, cmd, UDP_CMD_LEN, &nbytes_written)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("setAutoMode error: %s.", errstr); return false; } if ((rc = tty_read_section(PortFD, resp, '\n', USBDEWPOINT_TIMEOUT, &nbytes_read)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("setAutoMode error: %s.", errstr); return false; } resp[nbytes_read] = 0; LOGF_DEBUG("Resp: %s.", resp); return true; } bool USBDewpoint::setLinkMode(bool enable) { char cmd[UDP_CMD_LEN + 1]; int nbytes_written = 0, nbytes_read = 0, rc = -1; char errstr[MAXRBUF]; char resp[64]; sprintf(cmd, UDP_LINK_CMD, enable ? 1 : 0); tcflush(PortFD, TCIOFLUSH); LOGF_DEBUG("CMD: %s.", cmd); if ((rc = tty_write(PortFD, cmd, UDP_CMD_LEN, &nbytes_written)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("setLinkMode error: %s.", errstr); return false; } if ((rc = tty_read_section(PortFD, resp, '\n', USBDEWPOINT_TIMEOUT, &nbytes_read)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("setLinkMode error: %s.", errstr); return false; } resp[nbytes_read] = 0; LOGF_DEBUG("Resp: %s.", resp); return true; } bool USBDewpoint::ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) { if (dev != nullptr && strcmp(dev, getDeviceName()) == 0) { if (strcmp(AutoModeSP.name, name) == 0) { IUUpdateSwitch(&AutoModeSP, states, names, n); int target_mode = IUFindOnSwitchIndex(&AutoModeSP); AutoModeSP.s = IPS_BUSY; IDSetSwitch(&AutoModeSP, nullptr); setAutoMode(target_mode == 1); readSettings(); return true; } if (strcmp(LinkOut23SP.name, name) == 0) { IUUpdateSwitch(&LinkOut23SP, states, names, n); int target_mode = IUFindOnSwitchIndex(&LinkOut23SP); LinkOut23SP.s = IPS_BUSY; IDSetSwitch(&LinkOut23SP, nullptr); setLinkMode(target_mode == 1); readSettings(); return true; } if (strcmp(ResetSP.name, name) == 0) { IUResetSwitch(&ResetSP); if (reset()) { ResetSP.s = IPS_OK; readSettings(); } else ResetSP.s = IPS_ALERT; IDSetSwitch(&ResetSP, nullptr); return true; } } return INDI::DefaultDevice::ISNewSwitch(dev, name, states, names, n); } bool USBDewpoint::ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) { if (dev != nullptr && strcmp(dev, getDeviceName()) == 0) { if (strcmp(name, OutputsNP.name) == 0) { // Warn if we are in auto mode int target_mode = IUFindOnSwitchIndex(&AutoModeSP); if (target_mode == 1) { LOG_WARN("Setting output power is ignored in auto mode!"); return true; } IUUpdateNumber(&OutputsNP, values, names, n); OutputsNP.s = IPS_BUSY; IDSetNumber(&OutputsNP, nullptr); setOutput(1, OutputsN[0].value); setOutput(2, OutputsN[1].value); setOutput(3, OutputsN[2].value); readSettings(); return true; } if (strcmp(name, CalibrationsNP.name) == 0) { IUUpdateNumber(&CalibrationsNP, values, names, n); CalibrationsNP.s = IPS_BUSY; IDSetNumber(&CalibrationsNP, nullptr); setCalibrations(CalibrationsN[0].value, CalibrationsN[1].value, CalibrationsN[2].value); readSettings(); return true; } if (strcmp(name, ThresholdsNP.name) == 0) { IUUpdateNumber(&ThresholdsNP, values, names, n); ThresholdsNP.s = IPS_BUSY; IDSetNumber(&ThresholdsNP, nullptr); setThresholds(ThresholdsN[0].value, ThresholdsN[1].value); readSettings(); return true; } if (strcmp(name, AggressivityNP.name) == 0) { IUUpdateNumber(&AggressivityNP, values, names, n); AggressivityNP.s = IPS_BUSY; IDSetNumber(&AggressivityNP, nullptr); setAggressivity(AggressivityN[0].value); readSettings(); return true; } if (strcmp(name, FWversionNP.name) == 0) { IUUpdateNumber(&FWversionNP, values, names, n); FWversionNP.s = IPS_OK; IDSetNumber(&FWversionNP, nullptr); return true; } } return INDI::DefaultDevice::ISNewNumber(dev, name, values, names, n); } bool USBDewpoint::readSettings() { char cmd[] = UDP_STATUS_CMD; int nbytes_written = 0, nbytes_read = 0, rc = -1; char errstr[MAXRBUF]; char resp[64]; tcflush(PortFD, TCIOFLUSH); LOGF_DEBUG("CMD: %s.", cmd); if ((rc = tty_write(PortFD, cmd, UDP_CMD_LEN, &nbytes_written)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("readSettings error: %s.", errstr); return false; } if ((rc = tty_read_section(PortFD, resp, '\n', USBDEWPOINT_TIMEOUT, &nbytes_read)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("readSettings error: %s.", errstr); return false; } resp[63] = 0; LOGF_DEBUG("Resp: %s.", resp); // Status response is like: // ##22.37/22.62/23.35/50.77/12.55/0/0/0/0/0/0/2/2/0/0/4** float temp1, temp2, temp_ambient, humidity, dewpoint; unsigned int output1, output2, output3; unsigned int calibration1, calibration2, calibration_ambient; unsigned int threshold1, threshold2; unsigned int automode, linkout23, aggressivity; int ok = sscanf(resp, UDP_STATUS_RESPONSE, &temp1, &temp2, &temp_ambient, &humidity, &dewpoint, &output1, &output2, &output3, &calibration1, &calibration2, &calibration_ambient, &threshold1, &threshold2, &automode, &linkout23, &aggressivity); if (ok == 16) { TemperaturesN[0].value = temp1; TemperaturesN[1].value = temp2; TemperaturesN[2].value = temp_ambient; TemperaturesNP.s = IPS_OK; IDSetNumber(&TemperaturesNP, nullptr); HumidityN[0].value = humidity; HumidityNP.s = IPS_OK; IDSetNumber(&HumidityNP, nullptr); DewpointN[0].value = dewpoint; DewpointNP.s = IPS_OK; IDSetNumber(&DewpointNP, nullptr); OutputsN[0].value = output1; OutputsN[1].value = output2; OutputsN[2].value = output3; OutputsNP.s = IPS_OK; IDSetNumber(&OutputsNP, nullptr); CalibrationsN[0].value = calibration1; CalibrationsN[1].value = calibration2; CalibrationsN[2].value = calibration_ambient; CalibrationsNP.s = IPS_OK; IDSetNumber(&CalibrationsNP, nullptr); ThresholdsN[0].value = threshold1; ThresholdsN[1].value = threshold2; ThresholdsNP.s = IPS_OK; IDSetNumber(&ThresholdsNP, nullptr); IUResetSwitch(&AutoModeSP); AutoModeS[automode].s = ISS_ON; AutoModeSP.s = IPS_OK; IDSetSwitch(&AutoModeSP, nullptr); IUResetSwitch(&LinkOut23SP); LinkOut23S[linkout23].s = ISS_ON; LinkOut23SP.s = IPS_OK; IDSetSwitch(&LinkOut23SP, nullptr); AggressivityN[0].value = aggressivity; AggressivityNP.s = IPS_OK; IDSetNumber(&AggressivityNP, nullptr); } return true; } void USBDewpoint::TimerHit() { if (!isConnected()) { return; } // Get temperatures etc. readSettings(); SetTimer(POLLMS); } libindi/drivers/auxiliary/STAR2000.h0000664000175000017500000000511413263645557016404 0ustar jasemjasem/******************************************************************************* created 2014 G. Schmidt derived from gpusb code from Jasem Mutlaq This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program 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 General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. The full GNU General Public License is included in this distribution in the file called LICENSE. *******************************************************************************/ #pragma once #include "defaultdevice.h" #include "indiguiderinterface.h" #include class STAR2000 : public INDI::GuiderInterface, public INDI::DefaultDevice { public: STAR2000() = default; virtual bool initProperties(); virtual bool updateProperties(); virtual void ISGetProperties(const char *dev); virtual bool ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n); virtual bool ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n); virtual bool ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n); virtual bool ISSnoopDevice(XMLEle *root); protected: virtual bool saveConfigItems(FILE *fp); // Generic indi device entries bool Connect(); bool Connect(char *); bool Disconnect(); const char *getDefaultName(); void TimerHit(); virtual IPState GuideNorth(float ms); virtual IPState GuideSouth(float ms); virtual IPState GuideEast(float ms); virtual IPState GuideWest(float ms); private: float CalcWEPulseTimeLeft(); float CalcNSPulseTimeLeft(); public: // STAR2000 box RS232 port ITextVectorProperty PortTP; IText PortT[1]; private: bool InWEPulse { false }; float WEPulseRequest { 0 }; struct timeval WEPulseStart { 0, 0 }; int WEtimerID { 0 }; bool InNSPulse { false }; float NSPulseRequest { 0 }; struct timeval NSPulseStart { 0, 0 }; int NStimerID { 0 }; int WEDir { 0 }; int NSDir { 0 }; }; libindi/drivers/auxiliary/joystickdriver.h0000664000175000017500000000776213263645557020417 0ustar jasemjasem/******************************************************************************* Copyright(c) 2013 Jasem Mutlaq. All rights reserved. Based on code by Keith Lantz. This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program 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 General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. The full GNU General Public License is included in this distribution in the file called LICENSE. *******************************************************************************/ #pragma once #include #include #include #include #include #include #include #include #define JOYSTICK_DEV "/dev/input/js0" typedef struct { float theta, r, x, y; } joystick_position; typedef struct { std::vector button; std::vector axis; } joystick_state; /** * @brief The JoyStickDriver class provides basic functionality to read events from supported game pads under Linux. * It provides functions to read the button, axis, and joystick status and values. By definition, a joystick is the combination of two axis. * A game pad may have one or more joysticks depending on the number of reported axis. You can utilize the class in an event driven fashion by using callbacks. * The callbacks have a specific signature and must be set. Alternatively, you may query the status and position of the buttons & axis at any time as well. * * Since the class runs a non-blocking thread, the thread goes to sleep when there are no events detected in order to reduce CPU utilization. The sleep period is set by * default to 100 milliseconds and can be adjusted by the \i setPoll function. * * Each joystick has a normalized magnitude [0 to 1] and an angle. The magnitude is 0 when the stick is not depressed, and 1 when depressed all the way. * The angles are measured counter clock wise [0 to 360] with right/east direction being zero. * * The axis value is reported in raw values [-32767.0 to 32767.0]. * * The buttons are either off (0) or on (1) * */ class JoyStickDriver { public: JoyStickDriver(); ~JoyStickDriver(); typedef std::function joystickFunc; typedef std::function axisFunc; typedef std::function buttonFunc; bool Connect(); bool Disconnect(); void setPort(const char *port); void setPoll(int ms); const char *getName(); __u32 getVersion(); __u8 getNumOfJoysticks(); __u8 getNumOfAxes(); __u8 getNumrOfButtons(); joystick_position joystickPosition(int n); bool buttonPressed(int n); void setJoystickCallback(joystickFunc joystickCallback); void setAxisCallback(axisFunc axisCallback); void setButtonCallback(buttonFunc buttonCallback); protected: static void joystickEvent(int joystick_n, double mag, double angle); static void axisEvent(int axis_n, int value); static void buttonEvent(int button_n, int value); static void *loop(void *obj); void readEv(); joystickFunc joystickCallbackFunc; buttonFunc buttonCallbackFunc; axisFunc axisCallbackFunc; private: pthread_t thread; bool active; int joystick_fd; js_event *joystick_ev; joystick_state *joystick_st; __u32 version; __u8 axes; __u8 buttons; char name[256]; char dev_path[256]; int pollMS; }; libindi/drivers/auxiliary/gpusb.h0000664000175000017500000000461213263645557016453 0ustar jasemjasem/******************************************************************************* Copyright(c) 2012 Jasem Mutlaq. All rights reserved. This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program 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 General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. The full GNU General Public License is included in this distribution in the file called LICENSE. *******************************************************************************/ #pragma once #include "defaultdevice.h" #include "indiguiderinterface.h" class GPUSBDriver; class GPUSB : public INDI::GuiderInterface, public INDI::DefaultDevice { public: GPUSB(); virtual ~GPUSB(); virtual bool initProperties(); virtual bool updateProperties(); virtual void ISGetProperties(const char *dev); virtual bool ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n); virtual bool ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n); virtual bool ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n); virtual bool ISSnoopDevice(XMLEle *root); protected: // Generic indi device entries bool Connect(); bool Disconnect(); const char *getDefaultName(); void debugTriggered(bool enable); void TimerHit(); virtual IPState GuideNorth(float ms); virtual IPState GuideSouth(float ms); virtual IPState GuideEast(float ms); virtual IPState GuideWest(float ms); private: float CalcWEPulseTimeLeft(); float CalcNSPulseTimeLeft(); bool InWEPulse; float WEPulseRequest; struct timeval WEPulseStart; int WEtimerID; bool InNSPulse; float NSPulseRequest; struct timeval NSPulseStart; int NStimerID; int WEDir; int NSDir; GPUSBDriver *driver; }; libindi/drivers/auxiliary/snapcap.h0000664000175000017500000000614213263645557016760 0ustar jasemjasem/******************************************************************************* Copyright(c) 2017 Jarno Paananen. All right reserved. Driver for SnapCap dust cap / flat panel based on Flip Flat driver by: Copyright(c) 2015 Jasem Mutlaq. All rights reserved. This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program 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 General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. The full GNU General Public License is included in this distribution in the file called LICENSE. *******************************************************************************/ #pragma once #include "defaultdevice.h" #include "indilightboxinterface.h" #include "indidustcapinterface.h" #include namespace Connection { class Serial; } class SnapCap : public INDI::DefaultDevice, public INDI::LightBoxInterface, public INDI::DustCapInterface { public: SnapCap(); virtual ~SnapCap() = default; virtual bool initProperties(); virtual void ISGetProperties(const char *dev); virtual bool updateProperties(); virtual bool ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n); virtual bool ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n); virtual bool ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n); virtual bool ISSnoopDevice(XMLEle *root); protected: const char *getDefaultName(); virtual bool saveConfigItems(FILE *fp); void TimerHit(); // From Dust Cap virtual IPState ParkCap(); virtual IPState UnParkCap(); // From Light Box virtual bool SetLightBoxBrightness(uint16_t value); virtual bool EnableLightBox(bool enable); private: bool getStartupData(); bool ping(); bool getStatus(); bool getFirmwareVersion(); bool getBrightness(); bool Handshake(); IPState Abort(); // Status ITextVectorProperty StatusTP; IText StatusT[3] {}; // Firmware version ITextVectorProperty FirmwareTP; IText FirmwareT[1] {}; // Abort ISwitch AbortS[1]; ISwitchVectorProperty AbortSP; // Force open & close ISwitch ForceS[2]; ISwitchVectorProperty ForceSP; int PortFD{ -1 }; bool hasLight{ true }; uint8_t simulationWorkCounter{ 0 }; uint8_t targetCoverStatus{ 0xFF }; uint8_t prevCoverStatus{ 0xFF }; uint8_t prevLightStatus{ 0xFF }; uint8_t prevMotorStatus{ 0xFF }; uint8_t prevBrightness{ 0xFF }; Connection::Serial *serialConnection{ nullptr }; }; libindi/drivers/auxiliary/STAR2000.cpp0000664000175000017500000002374313263645557016747 0ustar jasemjasem/******************************************************************************* created 2014 G. Schmidt derived from gpusb code from Jasem Mutlaq This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program 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 General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. The full GNU General Public License is included in this distribution in the file called LICENSE. *******************************************************************************/ #include "STAR2000.h" #include "STAR2kdriver.h" #include "indistandardproperty.h" #include #include #include // We declare an auto pointer to gpGuide. std::unique_ptr s2kGuide(new STAR2000()); void ISGetProperties(const char *dev) { s2kGuide->ISGetProperties(dev); } void ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) { s2kGuide->ISNewSwitch(dev, name, states, names, n); } void ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n) { s2kGuide->ISNewText(dev, name, texts, names, n); } void ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) { s2kGuide->ISNewNumber(dev, name, values, names, n); } void ISNewBLOB(const char *dev, const char *name, int sizes[], int blobsizes[], char *blobs[], char *formats[], char *names[], int n) { INDI_UNUSED(dev); INDI_UNUSED(name); INDI_UNUSED(sizes); INDI_UNUSED(blobsizes); INDI_UNUSED(blobs); INDI_UNUSED(formats); INDI_UNUSED(names); INDI_UNUSED(n); } void ISSnoopDevice(XMLEle *root) { INDI_UNUSED(root); } const char *STAR2000::getDefaultName() { return (const char *)"STAR2000"; } bool STAR2000::Connect() { bool rc = false; if (isConnected()) return true; rc = Connect(PortT[0].text); if (rc) SetTimer(POLLMS); return rc; } bool STAR2000::Connect(char *port) { if (isSimulation()) { IDMessage(getDeviceName(), "Simulated STAR2000 box is online."); return true; } if (ConnectSTAR2k(port) < 0) { IDMessage(getDeviceName(), "Error connecting to port %s. Make sure you have BOTH write and read permission to your port.", port); return false; } IDMessage(getDeviceName(), "STAR2000 box is online."); return true; } bool STAR2000::Disconnect() { IDMessage(getDeviceName(), "STAR200 box is offline."); if (!isSimulation()) DisconnectSTAR2k(); return true; } bool STAR2000::initProperties() { bool rc = INDI::DefaultDevice::initProperties(); IUFillText(&PortT[0], "PORT", "Port", "/dev/ttyUSB0"); IUFillTextVector(&PortTP, PortT, 1, getDeviceName(), INDI::SP::DEVICE_PORT, "Ports", OPTIONS_TAB, IP_RW, 60, IPS_IDLE); initGuiderProperties(getDeviceName(), MAIN_CONTROL_TAB); addDebugControl(); setDriverInterface(TELESCOPE_INTERFACE); setDefaultPollingPeriod(250); return (rc); } bool STAR2000::updateProperties() { INDI::DefaultDevice::updateProperties(); if (isConnected()) { defineNumber(&GuideNSNP); defineNumber(&GuideWENP); } else { deleteProperty(GuideNSNP.name); deleteProperty(GuideWENP.name); } return true; } void STAR2000::ISGetProperties(const char *dev) { INDI::DefaultDevice::ISGetProperties(dev); defineText(&PortTP); loadConfig(true, INDI::SP::DEVICE_PORT); } bool STAR2000::ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) { if (dev != nullptr && strcmp(dev, getDeviceName()) == 0) { if (strcmp(name, GuideNSNP.name) == 0 || strcmp(name, GuideWENP.name) == 0) { processGuiderProperties(name, values, names, n); return true; } } return INDI::DefaultDevice::ISNewNumber(dev, name, values, names, n); } bool STAR2000::ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) { return INDI::DefaultDevice::ISNewSwitch(dev, name, states, names, n); } bool STAR2000::ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n) { if (strcmp(name, PortTP.name) == 0) { PortTP.s = IPS_OK; IUUpdateText(&PortTP, texts, names, n); IDSetText(&PortTP, nullptr); return true; } return INDI::DefaultDevice::ISNewText(dev, name, texts, names, n); } bool STAR2000::ISSnoopDevice(XMLEle *root) { return INDI::DefaultDevice::ISSnoopDevice(root); } bool STAR2000::saveConfigItems(FILE *fp) { IUSaveConfigText(fp, &PortTP); return true; } float STAR2000::CalcWEPulseTimeLeft() { double timesince; double timeleft; struct timeval now { 0, 0 }; gettimeofday(&now, nullptr); timesince = (double)(now.tv_sec * 1000.0 + now.tv_usec / 1000) - (double)(WEPulseStart.tv_sec * 1000.0 + WEPulseStart.tv_usec / 1000); timesince = timesince / 1000; timeleft = WEPulseRequest - timesince; return timeleft; } float STAR2000::CalcNSPulseTimeLeft() { double timesince; double timeleft; struct timeval now { 0, 0 }; gettimeofday(&now, nullptr); timesince = (double)(now.tv_sec * 1000.0 + now.tv_usec / 1000) - (double)(NSPulseStart.tv_sec * 1000.0 + NSPulseStart.tv_usec / 1000); timesince = timesince / 1000; timeleft = NSPulseRequest - timesince; return timeleft; } void STAR2000::TimerHit() { float timeleft; if (InWEPulse) { timeleft = CalcWEPulseTimeLeft(); if (timeleft < 1.0) { if (timeleft > 0.25) { // a quarter of a second or more // just set a tighter timer WEtimerID = SetTimer(250); } else { if (timeleft > 0.07) { // use an even tighter timer WEtimerID = SetTimer(50); } else { // it's real close now, so spin on it while (timeleft > 0) { int slv; slv = 100000 * timeleft; //IDLog("usleep %d\n",slv); usleep(slv); timeleft = CalcWEPulseTimeLeft(); } StopPulse(WEDir); InWEPulse = false; // If we have another pulse, keep going if (!InNSPulse) SetTimer(250); } } } else if (!InNSPulse) { WEtimerID = SetTimer(250); } } if (InNSPulse) { timeleft = CalcNSPulseTimeLeft(); if (timeleft < 1.0) { if (timeleft > 0.25) { // a quarter of a second or more // just set a tighter timer NStimerID = SetTimer(250); } else { if (timeleft > 0.07) { // use an even tighter timer NStimerID = SetTimer(50); } else { // it's real close now, so spin on it while (timeleft > 0) { int slv; slv = 100000 * timeleft; //IDLog("usleep %d\n",slv); usleep(slv); timeleft = CalcNSPulseTimeLeft(); } StopPulse(NSDir); InNSPulse = false; } } } else { NStimerID = SetTimer(250); } } } IPState STAR2000::GuideNorth(float ms) { RemoveTimer(NStimerID); StartPulse(NORTH); NSDir = NORTH; LOG_DEBUG("Starting NORTH guide"); if (ms <= POLLMS) { usleep(ms * 1000); StopPulse(NORTH); return IPS_OK; } NSPulseRequest = ms / 1000.0; gettimeofday(&NSPulseStart, nullptr); InNSPulse = true; NStimerID = SetTimer(ms - 50); return IPS_BUSY; } IPState STAR2000::GuideSouth(float ms) { RemoveTimer(NStimerID); StartPulse(SOUTH); LOG_DEBUG("Starting SOUTH guide"); NSDir = SOUTH; if (ms <= POLLMS) { usleep(ms * 1000); StopPulse(SOUTH); return IPS_OK; } NSPulseRequest = ms / 1000.0; gettimeofday(&NSPulseStart, nullptr); InNSPulse = true; NStimerID = SetTimer(ms - 50); return IPS_BUSY; } IPState STAR2000::GuideEast(float ms) { RemoveTimer(WEtimerID); StartPulse(EAST); LOG_DEBUG("Starting EAST guide"); WEDir = EAST; if (ms <= POLLMS) { usleep(ms * 1000); StopPulse(EAST); return IPS_OK; } WEPulseRequest = ms / 1000.0; gettimeofday(&WEPulseStart, nullptr); InWEPulse = true; WEtimerID = SetTimer(ms - 50); return IPS_BUSY; } IPState STAR2000::GuideWest(float ms) { RemoveTimer(WEtimerID); StartPulse(WEST); LOG_DEBUG("Starting WEST guide"); WEDir = WEST; if (ms <= POLLMS) { usleep(ms * 1000); StopPulse(WEST); return IPS_OK; } WEPulseRequest = ms / 1000.0; gettimeofday(&WEPulseStart, nullptr); InWEPulse = true; WEtimerID = SetTimer(ms - 50); return IPS_BUSY; } libindi/drivers/auxiliary/watchdogclient.cpp0000664000175000017500000001142013263645557020660 0ustar jasemjasem/******************************************************************************* Copyright(c) 2015 Jasem Mutlaq. All rights reserved. INDI Watchdog Client. The clients communicates with INDI server to put devices in a safe state for shutdown INDI Watchdog driver. This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program 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 General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. The full GNU General Public License is included in this distribution in the file called LICENSE. *******************************************************************************/ #include "watchdogclient.h" #include #include /************************************************************************************** ** ***************************************************************************************/ WatchDogClient::WatchDogClient() { isReady = isRunning = mountOnline = domeOnline = false; mountParkSP = domeParkSP = nullptr; } /************************************************************************************** ** ***************************************************************************************/ WatchDogClient::~WatchDogClient() { } /************************************************************************************** ** ***************************************************************************************/ void WatchDogClient::newDevice(INDI::BaseDevice *dp) { IDLog("Receiving new device: %s\n", dp->getDeviceName()); if (dome.empty() || std::string(dp->getDeviceName()) == dome) domeOnline = true; if (mount.empty() || std::string(dp->getDeviceName()) == mount) mountOnline = true; isReady = (domeOnline && mountOnline); } /************************************************************************************** ** *************************************************************************************/ void WatchDogClient::newProperty(INDI::Property *property) { if (!strcmp(property->getName(), "TELESCOPE_PARK")) mountParkSP = property->getSwitch(); else if (!strcmp(property->getName(), "DOME_PARK")) domeParkSP = property->getSwitch(); } /************************************************************************************** ** ***************************************************************************************/ void WatchDogClient::setMount(const std::string &value) { mount = value; watchDevice(mount.c_str()); } /************************************************************************************** ** ***************************************************************************************/ void WatchDogClient::setDome(const std::string &value) { dome = value; watchDevice(dome.c_str()); } /************************************************************************************** ** ***************************************************************************************/ bool WatchDogClient::parkDome() { if (domeParkSP == nullptr) return false; ISwitch *sw = IUFindSwitch(domeParkSP, "PARK"); if (sw == nullptr) return false; IUResetSwitch(domeParkSP); sw->s = ISS_ON; domeParkSP->s = IPS_BUSY; sendNewSwitch(domeParkSP); return true; } /************************************************************************************** ** ***************************************************************************************/ bool WatchDogClient::parkMount() { if (mountParkSP == nullptr) return false; ISwitch *sw = IUFindSwitch(mountParkSP, "PARK"); if (sw == nullptr) return false; IUResetSwitch(mountParkSP); sw->s = ISS_ON; mountParkSP->s = IPS_BUSY; sendNewSwitch(mountParkSP); return true; } /************************************************************************************** ** ***************************************************************************************/ IPState WatchDogClient::getDomeParkState() { return domeParkSP->s; } /************************************************************************************** ** ***************************************************************************************/ IPState WatchDogClient::getMountParkState() { return mountParkSP->s; } libindi/drivers/agent/0000775000175000017500000000000013263645557014246 5ustar jasemjasemlibindi/drivers/agent/agent_imager.h0000664000175000017500000001025713263645557017046 0ustar jasemjasem/******************************************************************************* Copyright(c) 2013-2016 CloudMakers, s. r. o. All rights reserved. Copyright(c) 2017 Marco Gulino This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2 as published by the Free Software Foundation. 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 Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. *******************************************************************************/ #pragma once #include "baseclient.h" #include "defaultdevice.h" #define MAX_GROUP_COUNT 16 class Group; class Imager : public virtual INDI::DefaultDevice, public virtual INDI::BaseClient { public: static const std::string DEVICE_NAME; Imager(); virtual ~Imager() = default; // DefaultDevice virtual bool initProperties(); virtual bool updateProperties(); virtual void ISGetProperties(const char *dev); virtual bool ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n); virtual bool ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n); virtual bool ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n); virtual bool ISNewBLOB(const char *dev, const char *name, int sizes[], int blobsizes[], char *blobs[], char *formats[], char *names[], int n); virtual bool ISSnoopDevice(XMLEle *root); // BaseClient virtual void newDevice(INDI::BaseDevice *dp); virtual void newProperty(INDI::Property *property); virtual void removeProperty(INDI::Property *property); virtual void removeDevice(INDI::BaseDevice *dp); virtual void newBLOB(IBLOB *bp); virtual void newSwitch(ISwitchVectorProperty *svp); virtual void newNumber(INumberVectorProperty *nvp); virtual void newText(ITextVectorProperty *tvp); virtual void newLight(ILightVectorProperty *lvp); virtual void newMessage(INDI::BaseDevice *dp, int messageID); virtual void serverConnected(); virtual void serverDisconnected(int exit_code); protected: virtual const char *getDefaultName(); virtual bool Connect(); virtual bool Disconnect(); private: bool isRunning(); bool isCCDConnected(); bool isFilterConnected(); void defineProperties(); void deleteProperties(); void initiateNextFilter(); void initiateNextCapture(); void startBatch(); void abortBatch(); void batchDone(); void initiateDownload(); char format[16]; int group { 0 }; int maxGroup { 0 }; int image { 0 }; int maxImage { 0 }; char *controlledCCD { nullptr }; char *controlledFilterWheel { nullptr }; ITextVectorProperty ControlledDeviceTP; IText ControlledDeviceT[2]; INumberVectorProperty GroupCountNP; INumber GroupCountN[1]; INumberVectorProperty ProgressNP; INumber ProgressN[3]; ISwitchVectorProperty BatchSP; ISwitch BatchS[2]; ILightVectorProperty StatusLP; ILight StatusL[2]; ITextVectorProperty ImageNameTP; IText ImageNameT[2]; INumberVectorProperty DownloadNP; INumber DownloadN[2]; IBLOBVectorProperty FitsBP; IBLOB FitsB[1]; INumberVectorProperty CCDImageExposureNP; INumber CCDImageExposureN[1]; INumberVectorProperty CCDImageBinNP; INumber CCDImageBinN[2]; ISwitch CCDUploadS[3]; ISwitchVectorProperty CCDUploadSP; IText CCDUploadSettingsT[2]; ITextVectorProperty CCDUploadSettingsTP; INumberVectorProperty FilterSlotNP; INumber FilterSlotN[1]; std::vector> groups; std::shared_ptr currentGroup() const; std::shared_ptr nextGroup() const; std::shared_ptr getGroup(int index) const; }; libindi/drivers/agent/group.cpp0000664000175000017500000000606113263645557016111 0ustar jasemjasem/******************************************************************************* Copyright(c) 2013-2016 CloudMakers, s. r. o. All rights reserved. Copyright(c) 2017-2018 Marco Gulino This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2 as published by the Free Software Foundation. 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 Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. *******************************************************************************/ #include "group.h" #include "agent_imager.h" #include #include #define GROUP_PREFIX "GROUP_" #define GROUP_PREFIX_LEN 6 #define IMAGE_COUNT 0 #define CCD_BINNING 1 #define FILTER_SLOT 2 #define CCD_EXPOSURE_VALUE 3 Group::Group(int id, Imager *imager) : imager{imager} { id++; std::stringstream groupNameStream; groupNameStream << "Image group " << id; groupName = groupNameStream.str(); std::stringstream groupSettingsNameStream; groupSettingsNameStream << GROUP_PREFIX << std::setw(2) << std::setfill('0') << id; groupSettingsName = groupSettingsNameStream.str(); GroupSettingsN.resize(4); IUFillNumber(&GroupSettingsN[IMAGE_COUNT], "IMAGE_COUNT", "Image count", "%3.0f", 1, 100, 1, 1); IUFillNumber(&GroupSettingsN[CCD_BINNING], "CCD_BINNING", "Binning", "%1.0f", 1, 4, 1, 1); IUFillNumber(&GroupSettingsN[FILTER_SLOT], "FILTER_SLOT", "Filter", "%2.f", 0, 12, 1, 0); IUFillNumber(&GroupSettingsN[CCD_EXPOSURE_VALUE], "CCD_EXPOSURE_VALUE", "Duration (s)", "%5.2f", 0, 36000, 0, 1.0); IUFillNumberVector(&GroupSettingsNP, GroupSettingsN.data(), GroupSettingsN.size(), Imager::DEVICE_NAME.c_str(), groupSettingsName.c_str(), "Image group settings", groupName.c_str(), IP_RW, 60, IPS_IDLE); } int Group::binning() const { return GroupSettingsN[CCD_BINNING].value; } int Group::filterSlot() const { return GroupSettingsN[FILTER_SLOT].value; } double Group::exposure() const { return GroupSettingsN[CCD_EXPOSURE_VALUE].value; } int Group::count() const { return GroupSettingsN[IMAGE_COUNT].value; } bool Group::ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) { INDI_UNUSED(dev); if (groupSettingsName == name) { IUUpdateNumber(&GroupSettingsNP, values, names, n); GroupSettingsNP.s = IPS_OK; IDSetNumber(&GroupSettingsNP, nullptr); return true; } return false; } void Group::defineProperties() { imager->defineNumber(&GroupSettingsNP); } void Group::deleteProperties() { imager->deleteProperty(GroupSettingsNP.name); } libindi/drivers/agent/agent_imager.cpp0000664000175000017500000005363213263645557017405 0ustar jasemjasem/******************************************************************************* Copyright(c) 2013-2016 CloudMakers, s. r. o. All rights reserved. Copyright(c) 2017 Marco Gulino This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2 as published by the Free Software Foundation. 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 Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. *******************************************************************************/ #include "agent_imager.h" #include "indistandardproperty.h" #include #include #include #include "group.h" #define DOWNLOAD_TAB "Download images" #define IMAGE_NAME "%s/%s_%d_%03d%s" #define IMAGE_PREFIX "_TMP_" #define GROUP_PREFIX "GROUP_" #define GROUP_PREFIX_LEN 6 const std::string Imager::DEVICE_NAME = "Imager Agent"; std::shared_ptr imager(new Imager()); // Driver entry points ---------------------------------------------------------------------------- void ISGetProperties(const char *dev) { imager->ISGetProperties(dev); } void ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) { imager->ISNewSwitch(dev, name, states, names, n); } void ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n) { imager->ISNewText(dev, name, texts, names, n); } void ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) { imager->ISNewNumber(dev, name, values, names, n); } void ISNewBLOB(const char *dev, const char *name, int sizes[], int blobsizes[], char *blobs[], char *formats[], char *names[], int n) { imager->ISNewBLOB(dev, name, sizes, blobsizes, blobs, formats, names, n); } void ISSnoopDevice(XMLEle *root) { imager->ISSnoopDevice(root); } // Imager ---------------------------------------------------------------------------- Imager::Imager() { setVersion(1, 2); groups.resize(MAX_GROUP_COUNT); int i=0; std::generate(groups.begin(), groups.end(), [this, &i] { return std::make_shared(i++, this); }); } bool Imager::isRunning() { return ProgressNP.s == IPS_BUSY; } bool Imager::isCCDConnected() { return StatusL[0].s == IPS_OK; } bool Imager::isFilterConnected() { return StatusL[1].s == IPS_OK; } std::shared_ptr Imager::getGroup(int index) const { if(index > -1 && index <= maxGroup) return groups[index]; return {}; } std::shared_ptr Imager::currentGroup() const { return getGroup(group - 1); } std::shared_ptr Imager::nextGroup() const { return getGroup(group); } void Imager::initiateNextFilter() { if (!isRunning()) return; if (group > 0 && image > 0 && group <= maxGroup && image <= maxImage) { int filterSlot = currentGroup()->filterSlot(); if (!isFilterConnected()) { if (filterSlot != 0) { ProgressNP.s = IPS_ALERT; IDSetNumber(&ProgressNP, "Filter wheel is not connected"); return; } else { initiateNextCapture(); } } else if (filterSlot != 0 && FilterSlotN[0].value != filterSlot) { FilterSlotN[0].value = filterSlot; sendNewNumber(&FilterSlotNP); LOGF_DEBUG("Group %d of %d, image %d of %d, filer %d, filter set initiated on %s", group, maxGroup, image, maxImage, (int)FilterSlotN[0].value, FilterSlotNP.device); } else { initiateNextCapture(); } } } void Imager::initiateNextCapture() { if (isRunning()) { if (group > 0 && image > 0 && group <= maxGroup && image <= maxImage) { if (!isCCDConnected()) { ProgressNP.s = IPS_ALERT; IDSetNumber(&ProgressNP, "CCD is not connected"); return; } CCDImageBinN[0].value = CCDImageBinN[1].value = currentGroup()->binning(); sendNewNumber(&CCDImageBinNP); CCDImageExposureN[0].value = currentGroup()->exposure(); sendNewNumber(&CCDImageExposureNP); IUSaveText(&CCDUploadSettingsT[0], ImageNameT[0].text); IUSaveText(&CCDUploadSettingsT[1], "_TMP_"); sendNewSwitch(&CCDUploadSP); sendNewText(&CCDUploadSettingsTP); LOGF_DEBUG("Group %d of %d, image %d of %d, duration %.1fs, binning %d, capture initiated on %s", group, maxGroup, image, maxImage, CCDImageExposureN[0].value, (int)CCDImageBinN[0].value, CCDImageExposureNP.device); } } } void Imager::startBatch() { LOG_DEBUG("Batch started"); ProgressN[0].value = group = 1; ProgressN[1].value = image = 1; maxImage = currentGroup()->count(); ProgressNP.s = IPS_BUSY; IDSetNumber(&ProgressNP, nullptr); initiateNextFilter(); } void Imager::abortBatch() { ProgressNP.s = IPS_ALERT; IDSetNumber(&ProgressNP, "Batch aborted"); } void Imager::batchDone() { ProgressNP.s = IPS_OK; IDSetNumber(&ProgressNP, "Batch done"); } void Imager::initiateDownload() { int group = (int)DownloadN[0].value; int image = (int)DownloadN[1].value; char name[128]={0}; std::ifstream file; if (group == 0 || image == 0) return; sprintf(name, IMAGE_NAME, ImageNameT[0].text, ImageNameT[1].text, group, image, format); file.open(name, std::ios::in | std::ios::binary | std::ios::ate); DownloadN[0].value = 0; DownloadN[1].value = 0; if (file.is_open()) { long size = file.tellg(); char *data = new char[size]; file.seekg(0, std::ios::beg); file.read(data, size); file.close(); remove(name); LOGF_DEBUG("Group %d, image %d, download initiated", group, image); DownloadNP.s = IPS_BUSY; IDSetNumber(&DownloadNP, "Download initiated"); strncpy(FitsB[0].format, format, sizeof(format)); FitsB[0].blob = data; FitsB[0].bloblen = FitsB[0].size = size; FitsBP.s = IPS_OK; IDSetBLOB(&FitsBP, nullptr); DownloadNP.s = IPS_OK; IDSetNumber(&DownloadNP, "Download finished"); } else { DownloadNP.s = IPS_ALERT; IDSetNumber(&DownloadNP, "Download failed"); LOGF_DEBUG("Group %d, image %d, upload failed", group, image); } } // DefaultDevice ---------------------------------------------------------------------------- const char *Imager::getDefaultName() { return Imager::DEVICE_NAME.c_str(); } bool Imager::initProperties() { INDI::DefaultDevice::initProperties(); addDebugControl(); IUFillNumber(&GroupCountN[0], "GROUP_COUNT", "Image group count", "%3.0f", 1, MAX_GROUP_COUNT, 1, maxGroup = 1); IUFillNumberVector(&GroupCountNP, GroupCountN, 1, getDefaultName(), "GROUPS", "Image groups", MAIN_CONTROL_TAB, IP_RW, 60, IPS_IDLE); IUFillText(&ControlledDeviceT[0], "CCD", "CCD", "CCD Simulator"); IUFillText(&ControlledDeviceT[1], "FILTER", "Filter wheel", "Filter Simulator"); IUFillTextVector(&ControlledDeviceTP, ControlledDeviceT, 2, getDefaultName(), "DEVICES", "Controlled devices", MAIN_CONTROL_TAB, IP_RW, 60, IPS_IDLE); controlledCCD = ControlledDeviceT[0].text; controlledFilterWheel = ControlledDeviceT[1].text; IUFillLight(&StatusL[0], "CCD", controlledCCD, IPS_IDLE); IUFillLight(&StatusL[1], "FILTER", controlledFilterWheel, IPS_IDLE); IUFillLightVector(&StatusLP, StatusL, 2, getDefaultName(), "STATUS", "Controlled devices", MAIN_CONTROL_TAB, IPS_IDLE); IUFillNumber(&ProgressN[0], "GROUP", "Current group", "%3.0f", 1, MAX_GROUP_COUNT, 1, 0); IUFillNumber(&ProgressN[1], "IMAGE", "Current image", "%3.0f", 1, 100, 1, 0); IUFillNumber(&ProgressN[2], "REMAINING_TIME", "Remaining time", "%5.2f", 0, 36000, 0, 0.0); IUFillNumberVector(&ProgressNP, ProgressN, 3, getDefaultName(), "PROGRESS", "Batch execution progress", MAIN_CONTROL_TAB, IP_RO, 60, IPS_IDLE); IUFillSwitch(&BatchS[0], "START", "Start batch", ISS_OFF); IUFillSwitch(&BatchS[1], "ABORT", "Abort batch", ISS_OFF); IUFillSwitchVector(&BatchSP, BatchS, 2, getDefaultName(), "BATCH", "Batch control", MAIN_CONTROL_TAB, IP_RW, ISR_NOFMANY, 60, IPS_IDLE); IUFillText(&ImageNameT[0], "IMAGE_FOLDER", "Image folder", "/tmp"); IUFillText(&ImageNameT[1], "IMAGE_PREFIX", "Image prefix", "IMG"); IUFillTextVector(&ImageNameTP, ImageNameT, 2, getDefaultName(), "IMAGE_NAME", "Image name", OPTIONS_TAB, IP_RW, 60, IPS_IDLE); IUFillNumber(&DownloadN[0], "GROUP", "Group", "%3.0f", 1, MAX_GROUP_COUNT, 1, 1); IUFillNumber(&DownloadN[1], "IMAGE", "Image", "%3.0f", 1, 100, 1, 1); IUFillNumberVector(&DownloadNP, DownloadN, 2, getDefaultName(), "DOWNLOAD", "Download image", DOWNLOAD_TAB, IP_RW, 60, IPS_IDLE); IUFillBLOB(&FitsB[0], "IMAGE", "Image", ""); IUFillBLOBVector(&FitsBP, FitsB, 1, getDefaultName(), "IMAGE", "Image Data", DOWNLOAD_TAB, IP_RO, 60, IPS_IDLE); defineNumber(&GroupCountNP); defineText(&ControlledDeviceTP); defineText(&ImageNameTP); for (int i = 0; i < GroupCountN[0].value; i++) { groups[i]->defineProperties(); } IUFillNumber(&CCDImageExposureN[0], "CCD_EXPOSURE_VALUE", "Duration (s)", "%5.2f", 0, 36000, 0, 1.0); IUFillNumberVector(&CCDImageExposureNP, CCDImageExposureN, 1, ControlledDeviceT[0].text, "CCD_EXPOSURE", "Expose", MAIN_CONTROL_TAB, IP_RW, 60, IPS_IDLE); IUFillNumber(&CCDImageBinN[0], "HOR_BIN", "X", "%2.0f", 1, 4, 1, 1); IUFillNumber(&CCDImageBinN[1], "VER_BIN", "Y", "%2.0f", 1, 4, 1, 1); IUFillNumberVector(&CCDImageBinNP, CCDImageBinN, 2, ControlledDeviceT[0].text, "CCD_BINNING", "Binning", MAIN_CONTROL_TAB, IP_RW, 60, IPS_IDLE); IUFillSwitch(&CCDUploadS[0], "UPLOAD_CLIENT", "Client", ISS_OFF); IUFillSwitch(&CCDUploadS[1], "UPLOAD_LOCAL", "Local", ISS_ON); IUFillSwitch(&CCDUploadS[2], "UPLOAD_BOTH", "Both", ISS_OFF); IUFillSwitchVector(&CCDUploadSP, CCDUploadS, 3, ControlledDeviceT[0].text, "UPLOAD_MODE", "Upload", OPTIONS_TAB, IP_RW, ISR_1OFMANY, 0, IPS_IDLE); IUFillText(&CCDUploadSettingsT[0], "UPLOAD_DIR", "Dir", ""); IUFillText(&CCDUploadSettingsT[1], "UPLOAD_PREFIX", "Prefix", IMAGE_PREFIX); IUFillTextVector(&CCDUploadSettingsTP, CCDUploadSettingsT, 2, ControlledDeviceT[0].text, "UPLOAD_SETTINGS", "Upload Settings", OPTIONS_TAB, IP_RW, 60, IPS_IDLE); IUFillNumber(&FilterSlotN[0], "FILTER_SLOT_VALUE", "Filter", "%3.0f", 1.0, 12.0, 1.0, 1.0); IUFillNumberVector(&FilterSlotNP, FilterSlotN, 1, ControlledDeviceT[1].text, "FILTER_SLOT", "Filter Slot", MAIN_CONTROL_TAB, IP_RW, 60, IPS_IDLE); return true; } bool Imager::updateProperties() { if (isConnected()) { defineLight(&StatusLP); ProgressN[0].value = group = 0; ProgressN[1].value = image = 0; ProgressNP.s = IPS_IDLE; defineNumber(&ProgressNP); BatchSP.s = IPS_IDLE; defineSwitch(&BatchSP); DownloadN[0].value = 0; DownloadN[1].value = 0; DownloadNP.s = IPS_IDLE; defineNumber(&DownloadNP); FitsBP.s = IPS_IDLE; defineBLOB(&FitsBP); } else { deleteProperty(StatusLP.name); deleteProperty(ProgressNP.name); deleteProperty(BatchSP.name); deleteProperty(DownloadNP.name); deleteProperty(FitsBP.name); } return true; } void Imager::ISGetProperties(const char *dev) { DefaultDevice::ISGetProperties(dev); } bool Imager::ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) { if (Imager::DEVICE_NAME == dev) { if (std::string{name} == std::string{GroupCountNP.name}) { for (int i = 0; i < maxGroup; i++) groups[i]->deleteProperties(); IUUpdateNumber(&GroupCountNP, values, names, n); maxGroup = (int)GroupCountN[0].value; if (maxGroup > MAX_GROUP_COUNT) GroupCountN[0].value = maxGroup = MAX_GROUP_COUNT; for (int i = 0; i < maxGroup; i++) groups[i]->defineProperties(); GroupCountNP.s = IPS_OK; IDSetNumber(&GroupCountNP, nullptr); return true; } if (std::string{name} == std::string{DownloadNP.name}) { IUUpdateNumber(&DownloadNP, values, names, n); initiateDownload(); return true; } if (strncmp(name, GROUP_PREFIX, GROUP_PREFIX_LEN) == 0) { for (int i = 0; i < GroupCountN[0].value; i++) if (groups[i]->ISNewNumber(dev, name, values, names, n)) { return true; } return false; } } return DefaultDevice::ISNewNumber(dev, name, values, names, n); } bool Imager::ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) { if (Imager::DEVICE_NAME == dev) { if (std::string{name} == std::string{BatchSP.name}) { for (int i = 0; i < n; i++) { if (strcmp(names[i], BatchS[0].name) == 0 && states[i] == ISS_ON) { if (!isRunning()) startBatch(); } if (strcmp(names[i], BatchS[1].name) == 0 && states[i] == ISS_ON) { if (isRunning()) abortBatch(); } } BatchSP.s = IPS_OK; IDSetSwitch(&BatchSP, nullptr); return true; } } return DefaultDevice::ISNewSwitch(dev, name, states, names, n); } bool Imager::ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n) { if (Imager::DEVICE_NAME == dev) { if (std::string{name} == std::string{ControlledDeviceTP.name}) { IUUpdateText(&ControlledDeviceTP, texts, names, n); IDSetText(&ControlledDeviceTP, nullptr); strncpy(StatusL[0].label, ControlledDeviceT[0].text, sizeof(StatusL[0].label)); strncpy(CCDImageExposureNP.device, ControlledDeviceT[0].text, sizeof(CCDImageExposureNP.device)); strncpy(CCDImageBinNP.device, ControlledDeviceT[0].text, sizeof(CCDImageBinNP.device)); strncpy(StatusL[1].label, ControlledDeviceT[1].text, sizeof(StatusL[1].label)); strncpy(FilterSlotNP.device, ControlledDeviceT[1].text, sizeof(FilterSlotNP.device)); return true; } if (std::string{name} == std::string{ImageNameTP.name}) { IUUpdateText(&ImageNameTP, texts, names, n); IDSetText(&ImageNameTP, nullptr); return true; } } return INDI::DefaultDevice::ISNewText(dev, name, texts, names, n); } bool Imager::ISNewBLOB(const char *dev, const char *name, int sizes[], int blobsizes[], char *blobs[], char *formats[], char *names[], int n) { return INDI::DefaultDevice::ISNewBLOB(dev, name, sizes, blobsizes, blobs, formats, names, n); } bool Imager::ISSnoopDevice(XMLEle *root) { return INDI::DefaultDevice::ISSnoopDevice(root); } bool Imager::Connect() { setServer("localhost", 7624); // TODO configuration options watchDevice(controlledCCD); watchDevice(controlledFilterWheel); connectServer(); setBLOBMode(B_ALSO, controlledCCD, nullptr); return true; } bool Imager::Disconnect() { if (isRunning()) abortBatch(); disconnectServer(); return true; } // BaseClient ---------------------------------------------------------------------------- void Imager::serverConnected() { LOG_DEBUG("Server connected"); StatusL[0].s = IPS_ALERT; StatusL[1].s = IPS_ALERT; IDSetLight(&StatusLP, nullptr); } void Imager::newDevice(INDI::BaseDevice *dp) { std::string deviceName{dp->getDeviceName()}; LOGF_DEBUG("Device %s detected", deviceName.c_str()); if (deviceName == controlledCCD) StatusL[0].s = IPS_BUSY; if (deviceName == controlledFilterWheel) StatusL[1].s = IPS_BUSY; IDSetLight(&StatusLP, nullptr); } void Imager::newProperty(INDI::Property *property) { std::string deviceName{property->getDeviceName()}; if (strcmp(property->getName(), INDI::SP::CONNECTION) == 0) { bool state = ((ISwitchVectorProperty *)property->getProperty())->sp[0].s != ISS_OFF; if (deviceName == controlledCCD) { if (state) { StatusL[0].s = IPS_OK; } else { connectDevice(controlledCCD); LOGF_DEBUG("Connecting %s", controlledCCD); } } if (deviceName == controlledFilterWheel) { if (state) { StatusL[1].s = IPS_OK; } else { connectDevice(controlledFilterWheel); LOGF_DEBUG("Connecting %s", controlledFilterWheel); } } IDSetLight(&StatusLP, nullptr); } } void Imager::removeProperty(INDI::Property *property) { INDI_UNUSED(property); } void Imager::removeDevice(INDI::BaseDevice *dp) { INDI_UNUSED(dp); } void Imager::newBLOB(IBLOB *bp) { if (ProgressNP.s == IPS_BUSY) { char name[128]={0}; std::ofstream file; strncpy(format, bp->format, 16); sprintf(name, IMAGE_NAME, ImageNameT[0].text, ImageNameT[1].text, group, image, format); file.open(name, std::ios::out | std::ios::binary | std::ios::trunc); file.write(static_cast(bp->blob), bp->bloblen); file.close(); LOGF_DEBUG("Group %d of %d, image %d of %d, saved to %s", group, maxGroup, image, maxImage, name); if (image == maxImage) { if (group == maxGroup) { batchDone(); } else { maxImage = nextGroup()->count(); ProgressN[0].value = group = group + 1; ProgressN[1].value = image = 1; IDSetNumber(&ProgressNP, nullptr); initiateNextFilter(); } } else { ProgressN[1].value = image = image + 1; IDSetNumber(&ProgressNP, nullptr); initiateNextFilter(); } } } void Imager::newSwitch(ISwitchVectorProperty *svp) { std::string deviceName{svp->device}; bool state = svp->sp[0].s != ISS_OFF; if (strcmp(svp->name, INDI::SP::CONNECTION) == 0) { if (deviceName == controlledCCD) { if (state) { StatusL[0].s = IPS_OK; } else { StatusL[0].s = IPS_BUSY; } } if (deviceName == controlledFilterWheel) { if (state) { StatusL[1].s = IPS_OK; } else { StatusL[1].s = IPS_BUSY; } } IDSetLight(&StatusLP, nullptr); } } void Imager::newNumber(INumberVectorProperty *nvp) { std::string deviceName{nvp->device}; if (deviceName == controlledCCD) { if (strcmp(nvp->name, "CCD_EXPOSURE") == 0) { ProgressN[2].value = nvp->np[0].value; IDSetNumber(&ProgressNP, nullptr); } } if (deviceName == controlledFilterWheel) { if (strcmp(nvp->name, "FILTER_SLOT") == 0) { FilterSlotN[0].value = nvp->np->value; if (nvp->s == IPS_OK) initiateNextCapture(); } } } void Imager::newText(ITextVectorProperty *tvp) { std::string deviceName{tvp->device}; if (deviceName == controlledCCD) { if (strcmp(tvp->name, "CCD_FILE_PATH") == 0) { char name[128]={0}; strncpy(format, strrchr(tvp->tp[0].text, '.'), sizeof(format)); sprintf(name, IMAGE_NAME, ImageNameT[0].text, ImageNameT[1].text, group, image, format); rename(tvp->tp[0].text, name); LOGF_DEBUG("Group %d of %d, image %d of %d, saved to %s", group, maxGroup, image, maxImage, name); if (image == maxImage) { if (group == maxGroup) { batchDone(); } else { maxImage = nextGroup()->count(); ProgressN[0].value = group = group + 1; ProgressN[1].value = image = 1; IDSetNumber(&ProgressNP, nullptr); initiateNextFilter(); } } else { ProgressN[1].value = image = image + 1; IDSetNumber(&ProgressNP, nullptr); initiateNextFilter(); } } } } void Imager::newLight(ILightVectorProperty *lvp) { INDI_UNUSED(lvp); } void Imager::newMessage(INDI::BaseDevice *dp, int messageID) { INDI_UNUSED(dp); INDI_UNUSED(messageID); } void Imager::serverDisconnected(int exit_code) { INDI_UNUSED(exit_code); LOG_DEBUG("Server disconnected"); StatusL[0].s = IPS_ALERT; StatusL[1].s = IPS_ALERT; } libindi/drivers/agent/group.h0000664000175000017500000000306113263645557015553 0ustar jasemjasem/******************************************************************************* Copyright(c) 2013-2016 CloudMakers, s. r. o. All rights reserved. Copyright(c) 2017-2018 Marco Gulino This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2 as published by the Free Software Foundation. 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 Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. *******************************************************************************/ #pragma once #include "baseclient.h" #include "defaultdevice.h" #include class Imager; class Group { public: explicit Group(int id, Imager *imager); bool ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n); void defineProperties(); void deleteProperties(); int filterSlot() const; int binning() const; double exposure() const; int count() const; private: std::string groupName; std::string groupSettingsName; Imager* imager; INumberVectorProperty GroupSettingsNP; std::vector GroupSettingsN; }; libindi/drivers/agent/agent_imager.txt0000664000175000017500000001200613263645557017430 0ustar jasemjasem/******************************************************************************* Copyright(c) 2013-2016 CloudMakers, s. r. o. All rights reserved. This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2 as published by the Free Software Foundation. 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 Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. *******************************************************************************/ IMAGER AGENT version 1.2 Purpose of this virtual driver is an unattended capture of a batch of groups of images. Each group can have different settings for a number of images, binning, filter slot and exposure duration. The point is, that no connected client and no network connection is needed during batch execution. 1. Changes in recent version - IMAGE_FOLDER property is renamed to IMAGE_NAME, IMAGE_PREFIX item is added. - Debug logging added. - Agent utilises local upload mode of CCD drivers. 2. How to use it Run in indiserver together with CCD driver and optional Filter wheel driver: e.g. $ indiserver indi_imager_agent indi_simulator_ccd indi_simulator_wheel Define batch and controlled devices: Property Item Type Description ====================================================================================== GROUPS GROUP_COUNT number Number of groups in batch. GROUP_XX IMAGE_COUNT number Number of images in group XX. CCD_BINNING number Binning (both x and y) for images. FILTER_SLOT number Filter slot for images. Set to 0 if you don't use Filter wheel. CCD_EXPOSURE_VALUE number Exposure duration for images. DEVICES CCD text Controlled CCD device name. FILTER text Controlled Filter wheel device name. IMAGE_NAME IMAGE_FOLDER text Local folder to store the captured images. IMAGE_PREFIX text File name prefix for the captured images. ====================================================================================== Connect, disconnect control batch execution and monitor the status: Property Item Type Description ====================================================================================== CONNECTION CONNECT switch Connect the agent and the controlled devices, if they are not already connected. DISCONNECT switch Disconnect agent. STATUS CCD light Status of the controlled CCD driver. FILTER light Status of the controlled Filter driver. BATCH START switch Start the batch execution. ABORT switch Abort the batch execution. PROGRESS GROUP number The current group in progress. IMAGE number The current image in progress. REMAINING_TIME number The remaining duration for the current image. ====================================================================================== Download captured images: Property Item Type Description ====================================================================================== DOWNLOAD GROUP number The group number for image to download. IMAGE number The image number to download. IMAGE IMAGE BLOB The image data for image selected by DOWNLOAD property. The image is deleted from the image folder after download. ====================================================================================== 3. Known issues and TODOs - The server host and port for controlled devices can't be configured, it is always localhost:7624. - Image format (compressed vs. raw image) can't be configured, the format of last captured image is always used for subsequent download. - It can't be reliable detected that CCD or filter wheel driver was disconnected during batch execution. - The images which are not downloaded remains in the image folder when the driver is stopped.libindi/drivers/ccd/0000775000175000017500000000000013263645557013701 5ustar jasemjasemlibindi/drivers/ccd/guide_simulator.cpp0000664000175000017500000010244113263645557017603 0ustar jasemjasem/******************************************************************************* Copyright(c) 2017 Jasem Mutlaq. All rights reserved. Copyright(c) 2010 Gerry Rozema. All rights reserved. This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2 as published by the Free Software Foundation. . 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 Library General Public License for more details. . You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. *******************************************************************************/ #include "guide_simulator.h" #include "stream/streammanager.h" #include "locale_compat.h" #include #include #include #include pthread_cond_t cv = PTHREAD_COND_INITIALIZER; pthread_mutex_t condMutex = PTHREAD_MUTEX_INITIALIZER; // We declare an auto pointer to GuideSim. std::unique_ptr guideSim(new GuideSim()); void ISPoll(void *p); void ISGetProperties(const char *dev) { guideSim->ISGetProperties(dev); } void ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) { guideSim->ISNewSwitch(dev, name, states, names, n); } void ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n) { guideSim->ISNewText(dev, name, texts, names, n); } void ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) { guideSim->ISNewNumber(dev, name, values, names, n); } void ISNewBLOB(const char *dev, const char *name, int sizes[], int blobsizes[], char *blobs[], char *formats[], char *names[], int n) { INDI_UNUSED(dev); INDI_UNUSED(name); INDI_UNUSED(sizes); INDI_UNUSED(blobsizes); INDI_UNUSED(blobs); INDI_UNUSED(formats); INDI_UNUSED(names); INDI_UNUSED(n); } void ISSnoopDevice(XMLEle *root) { guideSim->ISSnoopDevice(root); } GuideSim::GuideSim() { currentRA = RA; currentDE = Dec; streamPredicate = 0; terminateThread = false; primaryFocalLength = 900; // focal length of the telescope in millimeters time(&RunStart); SimulatorSettingsNV = new INumberVectorProperty; TimeFactorSV = new ISwitchVectorProperty; setDefaultPollingPeriod(750); } bool GuideSim::SetupParms() { SetCCDParams(SimulatorSettingsN[0].value, SimulatorSettingsN[1].value, 16, SimulatorSettingsN[2].value, SimulatorSettingsN[3].value); // Kwiq maxnoise = SimulatorSettingsN[8].value; skyglow = SimulatorSettingsN[9].value; maxval = SimulatorSettingsN[4].value; bias = SimulatorSettingsN[5].value; limitingmag = SimulatorSettingsN[7].value; saturationmag = SimulatorSettingsN[6].value; OAGoffset = SimulatorSettingsN[10].value; // An oag is offset this much from center of scope position (arcminutes); polarError = SimulatorSettingsN[11].value; polarDrift = SimulatorSettingsN[12].value; rotationCW = SimulatorSettingsN[13].value; int nbuf = PrimaryCCD.getXRes() * PrimaryCCD.getYRes() * PrimaryCCD.getBPP() / 8; PrimaryCCD.setFrameBufferSize(nbuf); Streamer->setPixelFormat(INDI_MONO, 16); Streamer->setSize(PrimaryCCD.getXRes(), PrimaryCCD.getYRes()); return true; } bool GuideSim::Connect() { pthread_create(&primary_thread, nullptr, &streamVideoHelper, this); SetTimer(POLLMS); return true; } bool GuideSim::Disconnect() { pthread_mutex_lock(&condMutex); streamPredicate = 1; terminateThread = true; pthread_cond_signal(&cv); pthread_mutex_unlock(&condMutex); return true; } const char *GuideSim::getDefaultName() { return (const char *)"Guide Simulator"; } bool GuideSim::initProperties() { // Most hardware layers wont actually have indi properties defined // but the simulators are a special case INDI::CCD::initProperties(); IUFillNumber(&SimulatorSettingsN[0], "SIM_XRES", "CCD X resolution", "%4.0f", 0, 8192, 0, 1280); IUFillNumber(&SimulatorSettingsN[1], "SIM_YRES", "CCD Y resolution", "%4.0f", 0, 8192, 0, 1024); IUFillNumber(&SimulatorSettingsN[2], "SIM_XSIZE", "CCD X Pixel Size", "%4.2f", 0, 60, 0, 5.2); IUFillNumber(&SimulatorSettingsN[3], "SIM_YSIZE", "CCD Y Pixel Size", "%4.2f", 0, 60, 0, 5.2); IUFillNumber(&SimulatorSettingsN[4], "SIM_MAXVAL", "CCD Maximum ADU", "%4.0f", 0, 65000, 0, 65000); IUFillNumber(&SimulatorSettingsN[5], "SIM_BIAS", "CCD Bias", "%4.0f", 0, 6000, 0, 10); IUFillNumber(&SimulatorSettingsN[6], "SIM_SATURATION", "Saturation Mag", "%4.1f", 0, 20, 0, 1.0); IUFillNumber(&SimulatorSettingsN[7], "SIM_LIMITINGMAG", "Limiting Mag", "%4.1f", 0, 20, 0, 17.0); IUFillNumber(&SimulatorSettingsN[8], "SIM_NOISE", "CCD Noise", "%4.0f", 0, 6000, 0, 10); IUFillNumber(&SimulatorSettingsN[9], "SIM_SKYGLOW", "Sky Glow (magnitudes)", "%4.1f", 0, 6000, 0, 19.5); IUFillNumber(&SimulatorSettingsN[10], "SIM_OAGOFFSET", "Oag Offset (arcminutes)", "%4.1f", 0, 6000, 0, 0); IUFillNumber(&SimulatorSettingsN[11], "SIM_POLAR", "PAE (arcminutes)", "%4.1f", -600, 600, 0, 0); /* PAE = Polar Alignment Error */ IUFillNumber(&SimulatorSettingsN[12], "SIM_POLARDRIFT", "PAE Drift (minutes)", "%4.1f", 0, 6000, 0, 0); IUFillNumber(&SimulatorSettingsN[13], "SIM_ROTATION", "Rotation CW (degrees)", "%4.1f", -360, 360, 0, 0); IUFillNumberVector(SimulatorSettingsNV, SimulatorSettingsN, 14, getDeviceName(), "SIMULATOR_SETTINGS", "Simulator Settings", "Simulator Config", IP_RW, 60, IPS_IDLE); IUFillSwitch(&TimeFactorS[0], "1X", "Actual Time", ISS_ON); IUFillSwitch(&TimeFactorS[1], "10X", "10x", ISS_OFF); IUFillSwitch(&TimeFactorS[2], "100X", "100x", ISS_OFF); IUFillSwitchVector(TimeFactorSV, TimeFactorS, 3, getDeviceName(), "ON_TIME_FACTOR", "Time Factor", "Simulator Config", IP_RW, ISR_1OFMANY, 60, IPS_IDLE); IUFillNumber(&FWHMN[0], "SIM_FWHM", "FWHM (arcseconds)", "%4.2f", 0, 60, 0, 7.5); IUFillNumberVector(&FWHMNP, FWHMN, 1, ActiveDeviceT[1].text, "FWHM", "FWHM", OPTIONS_TAB, IP_RO, 60, IPS_IDLE); IUFillNumber(&EqPEN[0], "RA_PE", "RA (hh:mm:ss)", "%010.6m", 0, 24, 0, 0); IUFillNumber(&EqPEN[1], "DEC_PE", "DEC (dd:mm:ss)", "%010.6m", -90, 90, 0, 0); IUFillNumberVector(&EqPENP, EqPEN, 2, ActiveDeviceT[0].text, "EQUATORIAL_PE", "EQ PE", "Main Control", IP_RW, 60, IPS_IDLE); IDSnoopDevice(ActiveDeviceT[0].text, "EQUATORIAL_PE"); IDSnoopDevice(ActiveDeviceT[1].text, "FWHM"); uint32_t cap = 0; cap |= CCD_CAN_ABORT; cap |= CCD_CAN_BIN; cap |= CCD_CAN_SUBFRAME; cap |= CCD_HAS_ST4_PORT; cap |= CCD_HAS_STREAMING; SetCCDCapability(cap); addDebugControl(); return true; } void GuideSim::ISGetProperties(const char *dev) { // First we let our parent populate //IDLog("GuideSim IsGetProperties with %s\n",dev); INDI::CCD::ISGetProperties(dev); defineNumber(SimulatorSettingsNV); defineSwitch(TimeFactorSV); } bool GuideSim::updateProperties() { INDI::CCD::updateProperties(); if (isConnected()) { SetupParms(); } return true; } bool GuideSim::StartExposure(float duration) { if (std::isnan(RA) && std::isnan(Dec)) { LOG_ERROR("Telescope coordinates missing. Make sure telescope is connected and its name is set in CCD Options."); return false; } // for the simulator, we can just draw the frame now // and it will get returned at the right time // by the timer routines AbortPrimaryFrame = false; ExposureRequest = duration; PrimaryCCD.setExposureDuration(duration); gettimeofday(&ExpStart, nullptr); // Leave the proper time showing for the draw routines DrawCcdFrame(&PrimaryCCD); // Now compress the actual wait time ExposureRequest = duration * TimeFactor; InExposure = true; return true; } bool GuideSim::AbortExposure() { if (!InExposure) return true; AbortPrimaryFrame = true; return true; } float GuideSim::CalcTimeLeft(timeval start, float req) { double timesince; double timeleft; struct timeval now { 0, 0 }; gettimeofday(&now, nullptr); timesince = (double)(now.tv_sec * 1000.0 + now.tv_usec / 1000) - (double)(start.tv_sec * 1000.0 + start.tv_usec / 1000); timesince = timesince / 1000; timeleft = req - timesince; return timeleft; } void GuideSim::TimerHit() { uint32_t nextTimer = POLLMS; if (!isConnected()) return; // No need to reset timer if we are not connected anymore if (InExposure) { if (AbortPrimaryFrame) { InExposure = false; AbortPrimaryFrame = false; } else { float timeleft; timeleft = CalcTimeLeft(ExpStart, ExposureRequest); //IDLog("CCD Exposure left: %g - Requset: %g\n", timeleft, ExposureRequest); if (timeleft < 0) timeleft = 0; PrimaryCCD.setExposureLeft(timeleft); if (timeleft < 1.0) { if (timeleft <= 0.001) { InExposure = false; PrimaryCCD.binFrame(); ExposureComplete(&PrimaryCCD); } else { nextTimer = timeleft * 1000; // set a shorter timer } } } } SetTimer(nextTimer); } int GuideSim::DrawCcdFrame(INDI::CCDChip *targetChip) { // CCD frame is 16 bit data uint16_t val; float ExposureTime; float targetFocalLength; uint16_t *ptr = reinterpret_cast(targetChip->getFrameBuffer()); if (Streamer->isStreaming()) ExposureTime = (ExposureRequest < 1) ? (ExposureRequest * 100) : ExposureRequest * 2; else ExposureTime = ExposureRequest; if (TelescopeTypeS[TELESCOPE_PRIMARY].s == ISS_ON) targetFocalLength = primaryFocalLength; else targetFocalLength = guiderFocalLength; if (ShowStarField) { char gsccmd[250]; FILE *pp; int stars = 0; int lines = 0; int drawn = 0; int x, y; float PEOffset; float PESpot; float decDrift; double rad; // telescope ra in degrees double rar; // telescope ra in radians double decr; // telescope dec in radians; int nwidth = 0, nheight = 0; double timesince; time_t now; time(&now); // Lets figure out where we are on the pe curve timesince = difftime(now, RunStart); // This is our spot in the curve PESpot = timesince / PEPeriod; // Now convert to radians PESpot = PESpot * 2.0 * 3.14159; PEOffset = PEMax * std::sin(PESpot); //fprintf(stderr,"PEOffset = %4.2f arcseconds timesince %4.2f\n",PEOffset,timesince); PEOffset = PEOffset / 3600; // convert to degrees //PeOffset=PeOffset/15; // ra is in h:mm // Start by clearing the frame buffer memset(targetChip->getFrameBuffer(), 0, targetChip->getFrameBufferSize()); // Spin up a set of plate constants that will relate // ra/dec of stars, to our fictitious ccd layout // to account for various rotations etc // we should spin up some plate constants here // then we can use these constants to rotate and offset // the standard co-ordinates on each star for drawing // a ccd frame; double pa, pb, pc, pd, pe, pf; // Pixels per radian double pprx, ppry; // Scale in arcsecs per pixel double Scalex; double Scaley; // CCD width in pixels double ccdW = targetChip->getXRes(); // Pixels per radian pprx = targetFocalLength / targetChip->getPixelSizeX() * 1000; ppry = targetFocalLength / targetChip->getPixelSizeY() * 1000; // we do a simple scale for x and y locations // based on the focal length and pixel size // focal length in mm, pixels in microns // JM: 2015-03-17: Using a simpler formula, Scalex and Scaley are in arcsecs/pixel Scalex = (targetChip->getPixelSizeX() / targetFocalLength) * 206.3; Scaley = (targetChip->getPixelSizeY() / targetFocalLength) * 206.3; #if 0 DEBUGF( INDI::Logger::DBG_DEBUG, "pprx: %g pixels per radian ppry: %g pixels per radian ScaleX: %g arcsecs/pixel ScaleY: %g arcsecs/pixel", pprx, ppry, Scalex, Scaley); #endif double theta = rotationCW + 270; if (theta > 360) theta -= 360; else if (theta < -360) theta += 360; // JM: 2015-03-17: Next we do a rotation assuming CW for angle theta pa = pprx * cos(theta * M_PI / 180.0); pb = ppry * sin(theta * M_PI / 180.0); pd = pprx * -sin(theta * M_PI / 180.0); pe = ppry * cos(theta * M_PI / 180.0); nwidth = targetChip->getXRes(); pc = nwidth / 2; nheight = targetChip->getYRes(); pf = nheight / 2; ImageScalex = Scalex; ImageScaley = Scaley; #ifdef USE_EQUATORIAL_PE if (!usePE) { #endif currentRA = RA; currentDE = Dec; ln_equ_posn epochPos { 0, 0 }, J2000Pos { 0, 0 }; epochPos.ra = currentRA * 15.0; epochPos.dec = currentDE; // Convert from JNow to J2000 ln_get_equ_prec2(&epochPos, ln_get_julian_from_sys(), JD2000, &J2000Pos); currentRA = J2000Pos.ra / 15.0; currentDE = J2000Pos.dec; currentDE += guideNSOffset; currentRA += guideWEOffset; #ifdef USE_EQUATORIAL_PE } #endif // calc this now, we will use it a lot later rad = currentRA * 15.0; rar = rad * 0.0174532925; // offsetting the dec by the guide head offset float cameradec; cameradec = currentDE + OAGoffset / 60; decr = cameradec * 0.0174532925; decDrift = (polarDrift * polarError * cos(decr)) / 3.81; // Add declination drift, if any. decr += decDrift / 3600.0 * 0.0174532925; //fprintf(stderr,"decPE %7.5f cameradec %7.5f CenterOffsetDec %4.4f\n",decPE,cameradec,decr); // now lets calculate the radius we need to fetch float radius; radius = sqrt((Scalex * Scalex * targetChip->getXRes() / 2.0 * targetChip->getXRes() / 2.0) + (Scaley * Scaley * targetChip->getYRes() / 2.0 * targetChip->getYRes() / 2.0)); // we have radius in arcseconds now radius = radius / 60; // convert to arcminutes #if 0 LOGF_DEBUG("Lookup radius %4.2f", radius); #endif // A saturationmag star saturates in one second // and a limitingmag produces a one adu level in one second // solve for zero point and system gain k = (saturationmag - limitingmag) / ((-2.5 * log(maxval)) - (-2.5 * log(1.0 / 2.0))); z = saturationmag - k * (-2.5 * log(maxval)); //z=z+saturationmag; //IDLog("K=%4.2f Z=%4.2f\n",k,z); // Should probably do some math here to figure out the dimmest // star we can see on this exposure // and only fetch to that magnitude // for now, just use the limiting mag number with some room to spare float lookuplimit = limitingmag; if (radius > 60) lookuplimit = 11; // if this is a light frame, we need a star field drawn INDI::CCDChip::CCD_FRAME ftype = targetChip->getFrameType(); if (ftype == INDI::CCDChip::LIGHT_FRAME) { AutoCNumeric locale; //sprintf(gsccmd,"gsc -c %8.6f %+8.6f -r 120 -m 0 9.1",rad+PEOffset,decPE); sprintf(gsccmd, "gsc -c %8.6f %+8.6f -r %4.1f -m 0 %4.2f -n 3000", rad + PEOffset, cameradec, radius, lookuplimit); LOGF_DEBUG("%s", gsccmd); pp = popen(gsccmd, "r"); if (pp != nullptr) { char line[256]; while (fgets(line, 256, pp) != nullptr) { //fprintf(stderr,"%s",line); // ok, lets parse this line for specifcs we want char id[20]; char plate[6]; char ob[6]; float mag; float mage; float ra; float dec; float pose; int band; float dist; int dir; int c; int rc; rc = sscanf(line, "%10s %f %f %f %f %f %d %d %4s %2s %f %d", id, &ra, &dec, &pose, &mag, &mage, &band, &c, plate, ob, &dist, &dir); //fprintf(stderr,"Parsed %d items\n",rc); if (rc == 12) { lines++; //if(c==0) { stars++; //fprintf(stderr,"%s %8.4f %8.4f %5.2f %5.2f %d\n",id,ra,dec,mag,dist,dir); // Convert the ra/dec to standard co-ordinates double sx; // standard co-ords double sy; // double srar; // star ra in radians double sdecr; // star dec in radians; double ccdx; double ccdy; //fprintf(stderr,"line %s",line); //fprintf(stderr,"parsed %6.5f %6.5f\n",ra,dec); srar = ra * 0.0174532925; sdecr = dec * 0.0174532925; // Handbook of astronomical image processing // page 253 // equations 9.1 and 9.2 // convert ra/dec to standard co-ordinates sx = cos(sdecr) * sin(srar - rar) / (cos(decr) * cos(sdecr) * cos(srar - rar) + sin(decr) * sin(sdecr)); sy = (sin(decr) * cos(sdecr) * cos(srar - rar) - cos(decr) * sin(sdecr)) / (cos(decr) * cos(sdecr) * cos(srar - rar) + sin(decr) * sin(sdecr)); // now convert to pixels ccdx = pa * sx + pb * sy + pc; ccdy = pd * sx + pe * sy + pf; // Invert horizontally ccdx = ccdW - ccdx; rc = DrawImageStar(targetChip, mag, ccdx, ccdy, ExposureTime); drawn += rc; if (rc == 1) { //LOGF_DEBUG("star %s scope %6.4f %6.4f star %6.4f %6.4f ccd %6.2f %6.2f",id,rad,decPE,ra,dec,ccdx,ccdy); //LOGF_DEBUG("star %s ccd %6.2f %6.2f",id,ccdx,ccdy); } } } pclose(pp); } else { LOG_ERROR("Error looking up stars, is gsc installed with appropriate environment variables set ??"); } if (drawn == 0) { LOG_ERROR("Got no stars, is gsc installed with appropriate environment variables set ??"); } } //fprintf(stderr,"Got %d stars from %d lines drew %d\n",stars,lines,drawn); // now we need to add background sky glow, with vignetting // this is essentially the same math as drawing a dim star with // fwhm equivalent to the full field of view if (ftype == INDI::CCDChip::LIGHT_FRAME || ftype == INDI::CCDChip::FLAT_FRAME) { float skyflux; // calculate flux from our zero point and gain values float glow = skyglow; if (ftype == INDI::CCDChip::FLAT_FRAME) { // Assume flats are done with a diffuser // in broad daylight, so, the sky magnitude // is much brighter than at night glow = skyglow / 10; } //fprintf(stderr,"Using glow %4.2f\n",glow); skyflux = pow(10, ((glow - z) * k / -2.5)); // ok, flux represents one second now // scale up linearly for exposure time skyflux = skyflux * ExposureTime; //IDLog("SkyFlux = %g ExposureRequest %g\n",skyflux,ExposureTime); unsigned short *pt; pt = (uint16_t *)targetChip->getFrameBuffer(); nheight = targetChip->getSubH(); nwidth = targetChip->getSubW(); for (int y = 0; y < nheight; y++) { for (int x = 0; x < nwidth; x++) { float dc; // distance from center float fp; // flux this pixel; float sx, sy; float vig; sx = nwidth / 2 - x; sy = nheight / 2 - y; vig = nwidth; vig = vig * ImageScalex; // need to make this account for actual pixel size dc = std::sqrt(sx * sx * ImageScalex * ImageScalex + sy * sy * ImageScaley * ImageScaley); // now we have the distance from center, in arcseconds // now lets plot a gaussian falloff to the edges // float fa; fa = exp(-2.0 * 0.7 * (dc * dc) / vig / vig); // get the current value fp = pt[0]; // Add the sky glow fp += skyflux; // now scale it for the vignetting fp = fa * fp; // clamp to limits if (fp > maxval) fp = maxval; if (fp > maxpix) maxpix = fp; if (fp < minpix) minpix = fp; // and put it back pt[0] = fp; pt++; } } } // Now we add some bias and read noise int subX = targetChip->getSubX(); int subY = targetChip->getSubY(); int subW = targetChip->getSubW() + subX; int subH = targetChip->getSubH() + subY; if (maxnoise > 0) { for (x = subX; x < subW; x++) { for (y = subY; y < subH; y++) { int noise; noise = random(); noise = noise % maxnoise; // //IDLog("noise is %d\n", noise); AddToPixel(targetChip, x, y, bias + noise); } } } } else { testvalue++; if (testvalue > 255) testvalue = 0; val = testvalue; int nbuf = targetChip->getSubW() * targetChip->getSubH(); for (int x = 0; x < nbuf; x++) { *ptr = val++; ptr++; } } return 0; } int GuideSim::DrawImageStar(INDI::CCDChip *targetChip, float mag, float x, float y, float ExposureTime) { //float d; //float r; int sx, sy; int drew = 0; int boxsizex = 5; int boxsizey = 5; float flux; int subX = targetChip->getSubX(); int subY = targetChip->getSubY(); int subW = targetChip->getSubW() + subX; int subH = targetChip->getSubH() + subY; if ((x < subX) || (x > subW || (y < subY) || (y > subH))) { // this star is not on the ccd frame anyways return 0; } // calculate flux from our zero point and gain values flux = pow(10, ((mag - z) * k / -2.5)); // ok, flux represents one second now // scale up linearly for exposure time flux = flux * ExposureTime; float qx; // we need a box size that gives a radius at least 3 times fwhm qx = seeing / ImageScalex; qx = qx * 3; boxsizex = (int)qx; boxsizex++; qx = seeing / ImageScaley; qx = qx * 3; boxsizey = (int)qx; boxsizey++; //IDLog("BoxSize %d %d\n",boxsizex,boxsizey); for (sy = -boxsizey; sy <= boxsizey; sy++) { for (sx = -boxsizey; sx <= boxsizey; sx++) { int rc; float dc; // distance from center float fp; // flux this pixel; // need to make this account for actual pixel size dc = std::sqrt(sx * sx * ImageScalex * ImageScalex + sy * sy * ImageScaley * ImageScaley); // now we have the distance from center, in arcseconds // This should be gaussian, but, for now we'll just go with // a simple linear function float fa = exp(-2.0 * 0.7 * (dc * dc) / seeing / seeing); fp = fa * flux; if (fp < 0) fp = 0; rc = AddToPixel(targetChip, x + sx, y + sy, fp); if (rc != 0) drew = 1; } } return drew; } int GuideSim::AddToPixel(INDI::CCDChip *targetChip, int x, int y, int val) { int nwidth = targetChip->getSubW(); int nheight = targetChip->getSubH(); x -= targetChip->getSubX(); y -= targetChip->getSubY(); int drew = 0; if (x >= 0) { if (x < nwidth) { if (y >= 0) { if (y < nheight) { unsigned short *pt; int newval; drew++; pt = (uint16_t *)targetChip->getFrameBuffer(); pt += (y * nwidth); pt += x; newval = pt[0]; newval += val; if (newval > maxval) newval = maxval; if (newval > maxpix) maxpix = newval; if (newval < minpix) minpix = newval; pt[0] = newval; } } } } return drew; } IPState GuideSim::GuideNorth(float v) { guideNSOffset += v / 1000 * GuideRate / 3600; return IPS_OK; } IPState GuideSim::GuideSouth(float v) { guideNSOffset += v / -1000 * GuideRate / 3600; return IPS_OK; } IPState GuideSim::GuideEast(float v) { float c = v / 1000 * GuideRate; c = c/ 3600.0 / 15.0; c = c/ (cos(currentDE * 0.0174532925)); guideWEOffset += c; return IPS_OK; } IPState GuideSim::GuideWest(float v) { float c = v / -1000 * GuideRate; c = c/ 3600.0 / 15.0; c = c/ (cos(currentDE * 0.0174532925)); guideWEOffset += c; return IPS_OK; } bool GuideSim::ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) { // first check if it's for our device //IDLog("INDI::CCD::ISNewNumber %s\n",name); if (dev != nullptr && strcmp(dev, getDeviceName()) == 0) { // This is for our device // Now lets see if it's something we process here //IDLog("GuideSim::ISNewNumber %s\n",name); if (strcmp(name, "SIMULATOR_SETTINGS") == 0) { IUUpdateNumber(SimulatorSettingsNV, values, names, n); SimulatorSettingsNV->s = IPS_OK; // Reset our parameters now SetupParms(); IDSetNumber(SimulatorSettingsNV, nullptr); //IDLog("Frame set to %4.0f,%4.0f %4.0f x %4.0f\n",CcdFrameN[0].value,CcdFrameN[1].value,CcdFrameN[2].value,CcdFrameN[3].value); //seeing=SimulatorSettingsN[0].value; return true; } } // if we didn't process it, continue up the chain, let somebody else // give it a shot return INDI::CCD::ISNewNumber(dev, name, values, names, n); } bool GuideSim::ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) { if (dev != nullptr && strcmp(dev, getDeviceName()) == 0) { if (strcmp(name, "ON_TIME_FACTOR") == 0) { // client is telling us what to do with co-ordinate requests TimeFactorSV->s = IPS_OK; IUUpdateSwitch(TimeFactorSV, states, names, n); // Update client display IDSetSwitch(TimeFactorSV, nullptr); if (TimeFactorS[0].s == ISS_ON) { //IDLog("GuideSim:: Time Factor 1\n"); TimeFactor = 1; } if (TimeFactorS[1].s == ISS_ON) { //IDLog("GuideSim:: Time Factor 0.1\n"); TimeFactor = 0.1; } if (TimeFactorS[2].s == ISS_ON) { //IDLog("GuideSim:: Time Factor 0.01\n"); TimeFactor = 0.01; } return true; } } // Nobody has claimed this, so, ignore it return INDI::CCD::ISNewSwitch(dev, name, states, names, n); } void GuideSim::activeDevicesUpdated() { IDSnoopDevice(ActiveDeviceT[0].text, "EQUATORIAL_PE"); IDSnoopDevice(ActiveDeviceT[1].text, "FWHM"); strncpy(EqPENP.device, ActiveDeviceT[0].text, MAXINDIDEVICE); strncpy(FWHMNP.device, ActiveDeviceT[1].text, MAXINDIDEVICE); } bool GuideSim::ISSnoopDevice(XMLEle *root) { if (IUSnoopNumber(root, &FWHMNP) == 0) { seeing = FWHMNP.np[0].value; //IDLog("CCD Simulator: New FWHM value of %g\n", seeing); return true; } // We try to snoop EQPEC first, if not found, we snoop regular EQNP if (IUSnoopNumber(root, &EqPENP) == 0) { double newra, newdec; newra = EqPEN[0].value; newdec = EqPEN[1].value; if ((newra != currentRA) || (newdec != currentDE)) { ln_equ_posn epochPos { 0, 0 }, J2000Pos { 0, 0 }; epochPos.ra = newra * 15.0; epochPos.dec = newdec; ln_get_equ_prec2(&epochPos, ln_get_julian_from_sys(), JD2000, &J2000Pos); currentRA = J2000Pos.ra / 15.0; currentDE = J2000Pos.dec; usePE = true; LOGF_DEBUG("raPE %g decPE %g Snooped raPE %g decPE %g", currentRA, currentDE, newra, newdec); return true; } } return INDI::CCD::ISSnoopDevice(root); } bool GuideSim::saveConfigItems(FILE *fp) { // Save CCD Config INDI::CCD::saveConfigItems(fp); // Save CCD Simulator Config IUSaveConfigNumber(fp, SimulatorSettingsNV); IUSaveConfigSwitch(fp, TimeFactorSV); return true; } bool GuideSim::StartStreaming() { ExposureRequest = 1.0 / Streamer->getTargetFPS(); pthread_mutex_lock(&condMutex); streamPredicate = 1; pthread_mutex_unlock(&condMutex); pthread_cond_signal(&cv); return true; } bool GuideSim::StopStreaming() { pthread_mutex_lock(&condMutex); streamPredicate = 0; pthread_mutex_unlock(&condMutex); pthread_cond_signal(&cv); return true; } bool GuideSim::UpdateCCDFrame(int x, int y, int w, int h) { long bin_width = w / PrimaryCCD.getBinX(); long bin_height = h / PrimaryCCD.getBinY(); bin_width = bin_width - (bin_width % 2); bin_height = bin_height - (bin_height % 2); Streamer->setSize(bin_width, bin_height); return INDI::CCD::UpdateCCDFrame(x,y,w,h); } bool GuideSim::UpdateCCDBin(int hor, int ver) { if (hor == 3 || ver == 3) { LOG_ERROR("3x3 binning is not supported."); return false; } long bin_width = PrimaryCCD.getSubW() / hor; long bin_height = PrimaryCCD.getSubH() / ver; bin_width = bin_width - (bin_width % 2); bin_height = bin_height - (bin_height % 2); Streamer->setSize(bin_width, bin_height); return INDI::CCD::UpdateCCDBin(hor,ver); } void *GuideSim::streamVideoHelper(void *context) { return ((GuideSim *)context)->streamVideo(); } void *GuideSim::streamVideo() { struct itimerval tframe1, tframe2; double s1, s2, deltas; while (true) { pthread_mutex_lock(&condMutex); while (streamPredicate == 0) { pthread_cond_wait(&cv, &condMutex); ExposureRequest = 1.0 / Streamer->getTargetFPS(); } if (terminateThread) break; // release condMutex pthread_mutex_unlock(&condMutex); // Simulate exposure time //usleep(ExposureRequest*1e5); // 16 bit DrawCcdFrame(&PrimaryCCD); PrimaryCCD.binFrame(); getitimer(ITIMER_REAL, &tframe1); s1 = ((double)tframe1.it_value.tv_sec) + ((double)tframe1.it_value.tv_usec / 1e6); s2 = ((double)tframe2.it_value.tv_sec) + ((double)tframe2.it_value.tv_usec / 1e6); deltas = fabs(s2 - s1); if (deltas < ExposureRequest) usleep(fabs(ExposureRequest-deltas)*1e6); uint32_t size = PrimaryCCD.getFrameBufferSize() / (PrimaryCCD.getBinX()*PrimaryCCD.getBinY()); Streamer->newFrame(PrimaryCCD.getFrameBuffer(), size); getitimer(ITIMER_REAL, &tframe2); } pthread_mutex_unlock(&condMutex); return 0; } libindi/drivers/ccd/ccd_simulator.h0000664000175000017500000001302613263645557016704 0ustar jasemjasem/******************************************************************************* Copyright(c) 2017 Jasem Mutlaq. All rights reserved. Copyright(c) 2010 Gerry Rozema. All rights reserved. This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2 as published by the Free Software Foundation. . 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 Library General Public License for more details. . You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. *******************************************************************************/ #pragma once #include "indiccd.h" #include "indifilterinterface.h" /** * @brief The CCDSim class provides an advanced simulator for a CCD that includes a dedicated on-board guide chip. * * The CCD driver can generate star fields given that General-Star-Catalog (gsc) tool is installed on the same machine the driver is running. * * Many simulator parameters can be configured to generate the final star field image. In addition to support guider chip and guiding pulses (ST4), * a filter wheel support is provided for 8 filter wheels. Cooler and temperature control is also supported. * * The driver can snoop the mount equatorial coords to draw the star field. It listens to EQUATORIAL_PE property and also defines it so that the user * can set it manually. * * Video streaming can be enabled from the Stream property group with several encoders and recorders supported. * * @author Gerry Rozema * @author Jasem Mutlaq */ class CCDSim : public INDI::CCD, public INDI::FilterInterface { public: CCDSim(); virtual ~CCDSim() = default; const char *getDefaultName() override; bool initProperties() override; bool updateProperties() override; void ISGetProperties(const char *dev) override; virtual bool ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) override; virtual bool ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) override; virtual bool ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n) override; virtual bool ISSnoopDevice(XMLEle *root) override; static void *streamVideoHelper(void *context); void *streamVideo(); protected: bool Connect() override; bool Disconnect() override; bool StartExposure(float duration) override; bool StartGuideExposure(float) override; bool AbortExposure() override; bool AbortGuideExposure() override; void TimerHit() override; int DrawCcdFrame(INDI::CCDChip *targetChip); int DrawImageStar(INDI::CCDChip *targetChip, float, float, float, float ExposureTime); int AddToPixel(INDI::CCDChip *targetChip, int, int, int); IPState GuideNorth(float) override; IPState GuideSouth(float) override; IPState GuideEast(float) override; IPState GuideWest(float) override; virtual bool saveConfigItems(FILE *fp) override; virtual void activeDevicesUpdated() override; virtual int SetTemperature(double temperature) override; virtual bool UpdateCCDFrame(int x, int y, int w, int h) override; virtual bool UpdateCCDBin(int hor, int ver) override; virtual bool StartStreaming() override; virtual bool StopStreaming() override; // Filter bool SelectFilter(int) override; int QueryFilter() override; private: float CalcTimeLeft(timeval, float); bool SetupParms(); float TemperatureRequest { 0 }; float ExposureRequest { 0 }; struct timeval ExpStart { 0, 0 }; float GuideExposureRequest { 0 }; struct timeval GuideExpStart { 0, 0 }; int testvalue { 0 }; bool ShowStarField { true }; int bias { 1500 }; int maxnoise { 20 }; int maxval { 65000 }; int maxpix { 0 }; int minpix { 65000 }; float skyglow { 40 }; float limitingmag { 11.5 }; float saturationmag { 2 }; float seeing { 3.5 }; float ImageScalex { 1.0 }; float ImageScaley { 1.0 }; // An oag is offset this much from center of scope position (arcminutes) float OAGoffset { 0 }; float rotationCW { 0 }; float TimeFactor { 1 }; // our zero point calcs used for drawing stars float k { 0 }; float z { 0 }; bool AbortGuideFrame { false }; bool AbortPrimaryFrame { false }; /// Guide rate is 7 arcseconds per second float GuideRate { 7 }; /// Our PEPeriod is 8 minutes and we have a 22 arcsecond swing float PEPeriod { 8*60 }; float PEMax { 11 }; double currentRA { 0 }; double currentDE { 0 }; bool usePE { false }; time_t RunStart; float guideNSOffset {0}; float guideWEOffset {0}; float polarError { 0 }; float polarDrift { 0 }; int streamPredicate; pthread_t primary_thread; bool terminateThread; // And this lives in our simulator settings page INumberVectorProperty *SimulatorSettingsNV; INumber SimulatorSettingsN[14]; ISwitch TimeFactorS[3]; ISwitchVectorProperty *TimeFactorSV; // We are going to snoop these from focuser INumberVectorProperty FWHMNP; INumber FWHMN[1]; INumberVectorProperty EqPENP; INumber EqPEN[2]; ISwitch CoolerS[2]; ISwitchVectorProperty CoolerSP; }; libindi/drivers/ccd/ccd_simulator.cpp0000664000175000017500000012004213263645557017234 0ustar jasemjasem/******************************************************************************* Copyright(c) 2017 Jasem Mutlaq. All rights reserved. Copyright(c) 2010 Gerry Rozema. All rights reserved. This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2 as published by the Free Software Foundation. . 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 Library General Public License for more details. . You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. *******************************************************************************/ #include "ccd_simulator.h" #include "indicom.h" #include "stream/streammanager.h" #include "locale_compat.h" #include #include #include #include pthread_cond_t cv = PTHREAD_COND_INITIALIZER; pthread_mutex_t condMutex = PTHREAD_MUTEX_INITIALIZER; // We declare an auto pointer to ccdsim. std::unique_ptr ccdsim(new CCDSim()); void ISPoll(void *p); void ISGetProperties(const char *dev) { ccdsim->ISGetProperties(dev); } void ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) { ccdsim->ISNewSwitch(dev, name, states, names, n); } void ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n) { ccdsim->ISNewText(dev, name, texts, names, n); } void ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) { ccdsim->ISNewNumber(dev, name, values, names, n); } void ISNewBLOB(const char *dev, const char *name, int sizes[], int blobsizes[], char *blobs[], char *formats[], char *names[], int n) { INDI_UNUSED(dev); INDI_UNUSED(name); INDI_UNUSED(sizes); INDI_UNUSED(blobsizes); INDI_UNUSED(blobs); INDI_UNUSED(formats); INDI_UNUSED(names); INDI_UNUSED(n); } void ISSnoopDevice(XMLEle *root) { ccdsim->ISSnoopDevice(root); } CCDSim::CCDSim() : INDI::FilterInterface(this) { currentRA = RA; currentDE = Dec; streamPredicate = 0; terminateThread = false; primaryFocalLength = 900; // focal length of the telescope in millimeters guiderFocalLength = 300; time(&RunStart); SimulatorSettingsNV = new INumberVectorProperty; TimeFactorSV = new ISwitchVectorProperty; // Filter stuff FilterSlotN[0].min = 1; FilterSlotN[0].max = 8; } bool CCDSim::SetupParms() { int nbuf; SetCCDParams(SimulatorSettingsN[0].value, SimulatorSettingsN[1].value, 16, SimulatorSettingsN[2].value, SimulatorSettingsN[3].value); if (HasCooler()) { TemperatureN[0].value = 20; IDSetNumber(&TemperatureNP, nullptr); } // Kwiq maxnoise = SimulatorSettingsN[8].value; skyglow = SimulatorSettingsN[9].value; maxval = SimulatorSettingsN[4].value; bias = SimulatorSettingsN[5].value; limitingmag = SimulatorSettingsN[7].value; saturationmag = SimulatorSettingsN[6].value; OAGoffset = SimulatorSettingsN[10].value; // An oag is offset this much from center of scope position (arcminutes); polarError = SimulatorSettingsN[11].value; polarDrift = SimulatorSettingsN[12].value; rotationCW = SimulatorSettingsN[13].value; nbuf = PrimaryCCD.getXRes() * PrimaryCCD.getYRes() * PrimaryCCD.getBPP() / 8; //nbuf += 512; PrimaryCCD.setFrameBufferSize(nbuf); Streamer->setPixelFormat(INDI_MONO, 16); Streamer->setSize(PrimaryCCD.getXRes(), PrimaryCCD.getYRes()); return true; } bool CCDSim::Connect() { pthread_create(&primary_thread, nullptr, &streamVideoHelper, this); SetTimer(POLLMS); return true; } bool CCDSim::Disconnect() { pthread_mutex_lock(&condMutex); streamPredicate = 1; terminateThread = true; pthread_cond_signal(&cv); pthread_mutex_unlock(&condMutex); return true; } const char *CCDSim::getDefaultName() { return (const char *)"CCD Simulator"; } bool CCDSim::initProperties() { // Most hardware layers wont actually have indi properties defined // but the simulators are a special case INDI::CCD::initProperties(); IUFillNumber(&SimulatorSettingsN[0], "SIM_XRES", "CCD X resolution", "%4.0f", 0, 8192, 0, 1280); IUFillNumber(&SimulatorSettingsN[1], "SIM_YRES", "CCD Y resolution", "%4.0f", 0, 8192, 0, 1024); IUFillNumber(&SimulatorSettingsN[2], "SIM_XSIZE", "CCD X Pixel Size", "%4.2f", 0, 60, 0, 5.2); IUFillNumber(&SimulatorSettingsN[3], "SIM_YSIZE", "CCD Y Pixel Size", "%4.2f", 0, 60, 0, 5.2); IUFillNumber(&SimulatorSettingsN[4], "SIM_MAXVAL", "CCD Maximum ADU", "%4.0f", 0, 65000, 0, 65000); IUFillNumber(&SimulatorSettingsN[5], "SIM_BIAS", "CCD Bias", "%4.0f", 0, 6000, 0, 10); IUFillNumber(&SimulatorSettingsN[6], "SIM_SATURATION", "Saturation Mag", "%4.1f", 0, 20, 0, 1.0); IUFillNumber(&SimulatorSettingsN[7], "SIM_LIMITINGMAG", "Limiting Mag", "%4.1f", 0, 20, 0, 17.0); IUFillNumber(&SimulatorSettingsN[8], "SIM_NOISE", "CCD Noise", "%4.0f", 0, 6000, 0, 10); IUFillNumber(&SimulatorSettingsN[9], "SIM_SKYGLOW", "Sky Glow (magnitudes)", "%4.1f", 0, 6000, 0, 19.5); IUFillNumber(&SimulatorSettingsN[10], "SIM_OAGOFFSET", "Oag Offset (arcminutes)", "%4.1f", 0, 6000, 0, 0); IUFillNumber(&SimulatorSettingsN[11], "SIM_POLAR", "PAE (arcminutes)", "%4.1f", -600, 600, 0, 0); /* PAE = Polar Alignment Error */ IUFillNumber(&SimulatorSettingsN[12], "SIM_POLARDRIFT", "PAE Drift (minutes)", "%4.1f", 0, 6000, 0, 0); IUFillNumber(&SimulatorSettingsN[13], "SIM_ROTATION", "Rotation CW (degrees)", "%4.1f", -360, 360, 0, 0); IUFillNumberVector(SimulatorSettingsNV, SimulatorSettingsN, 14, getDeviceName(), "SIMULATOR_SETTINGS", "Simulator Settings", "Simulator Config", IP_RW, 60, IPS_IDLE); IUFillSwitch(&TimeFactorS[0], "1X", "Actual Time", ISS_ON); IUFillSwitch(&TimeFactorS[1], "10X", "10x", ISS_OFF); IUFillSwitch(&TimeFactorS[2], "100X", "100x", ISS_OFF); IUFillSwitchVector(TimeFactorSV, TimeFactorS, 3, getDeviceName(), "ON_TIME_FACTOR", "Time Factor", "Simulator Config", IP_RW, ISR_1OFMANY, 60, IPS_IDLE); IUFillNumber(&FWHMN[0], "SIM_FWHM", "FWHM (arcseconds)", "%4.2f", 0, 60, 0, 7.5); IUFillNumberVector(&FWHMNP, FWHMN, 1, ActiveDeviceT[1].text, "FWHM", "FWHM", OPTIONS_TAB, IP_RO, 60, IPS_IDLE); IUFillSwitch(&CoolerS[0], "COOLER_ON", "ON", ISS_OFF); IUFillSwitch(&CoolerS[1], "COOLER_OFF", "OFF", ISS_ON); IUFillSwitchVector(&CoolerSP, CoolerS, 2, getDeviceName(), "CCD_COOLER", "Cooler", MAIN_CONTROL_TAB, IP_WO, ISR_1OFMANY, 0, IPS_IDLE); IUFillNumber(&EqPEN[0], "RA_PE", "RA (hh:mm:ss)", "%010.6m", 0, 24, 0, 0); IUFillNumber(&EqPEN[1], "DEC_PE", "DEC (dd:mm:ss)", "%010.6m", -90, 90, 0, 0); IUFillNumberVector(&EqPENP, EqPEN, 2, getDeviceName(), "EQUATORIAL_PE", "EQ PE", "Simulator Config" , IP_RW, 60, IPS_IDLE); #ifdef USE_EQUATORIAL_PE IDSnoopDevice(ActiveDeviceT[0].text, "EQUATORIAL_PE"); #else IDSnoopDevice(ActiveDeviceT[0].text, "EQUATORIAL_EOD_COORD"); #endif IDSnoopDevice(ActiveDeviceT[1].text, "FWHM"); uint32_t cap = 0; cap |= CCD_CAN_ABORT; cap |= CCD_CAN_BIN; cap |= CCD_CAN_SUBFRAME; cap |= CCD_HAS_COOLER; cap |= CCD_HAS_GUIDE_HEAD; cap |= CCD_HAS_SHUTTER; cap |= CCD_HAS_ST4_PORT; cap |= CCD_HAS_STREAMING; SetCCDCapability(cap); INDI::FilterInterface::initProperties(FILTER_TAB); FilterSlotN[0].min = 1; FilterSlotN[0].max = 8; addDebugControl(); setDriverInterface(getDriverInterface() | FILTER_INTERFACE); return true; } void CCDSim::ISGetProperties(const char *dev) { // First we let our parent populate //IDLog("CCDSim IsGetProperties with %s\n",dev); INDI::CCD::ISGetProperties(dev); defineNumber(SimulatorSettingsNV); defineSwitch(TimeFactorSV); defineNumber(&EqPENP); } bool CCDSim::updateProperties() { INDI::CCD::updateProperties(); if (isConnected()) { if (HasCooler()) defineSwitch(&CoolerSP); SetupParms(); if (HasGuideHead()) { SetGuiderParams(500, 290, 16, 9.8, 12.6); GuideCCD.setFrameBufferSize(GuideCCD.getXRes() * GuideCCD.getYRes() * 2); } // Define the Filter Slot and name properties INDI::FilterInterface::updateProperties(); } else { if (HasCooler()) deleteProperty(CoolerSP.name); INDI::FilterInterface::updateProperties(); } return true; } int CCDSim::SetTemperature(double temperature) { TemperatureRequest = temperature; if (fabs(temperature - TemperatureN[0].value) < 0.1) { TemperatureN[0].value = temperature; return 1; } CoolerS[0].s = ISS_ON; CoolerS[1].s = ISS_OFF; CoolerSP.s = IPS_BUSY; IDSetSwitch(&CoolerSP, nullptr); return 0; } bool CCDSim::StartExposure(float duration) { if (std::isnan(RA) && std::isnan(Dec)) { LOG_ERROR("Telescope coordinates missing. Make sure telescope is connected and its name is set in CCD Options."); return false; } // for the simulator, we can just draw the frame now // and it will get returned at the right time // by the timer routines AbortPrimaryFrame = false; ExposureRequest = duration; PrimaryCCD.setExposureDuration(duration); gettimeofday(&ExpStart, nullptr); // Leave the proper time showing for the draw routines DrawCcdFrame(&PrimaryCCD); // Now compress the actual wait time ExposureRequest = duration * TimeFactor; InExposure = true; return true; } bool CCDSim::StartGuideExposure(float n) { GuideExposureRequest = n; AbortGuideFrame = false; GuideCCD.setExposureDuration(n); DrawCcdFrame(&GuideCCD); gettimeofday(&GuideExpStart, nullptr); InGuideExposure = true; return true; } bool CCDSim::AbortExposure() { if (!InExposure) return true; AbortPrimaryFrame = true; return true; } bool CCDSim::AbortGuideExposure() { //IDLog("Enter AbortGuideExposure\n"); if (!InGuideExposure) return true; // no need to abort if we aren't doing one AbortGuideFrame = true; return true; } float CCDSim::CalcTimeLeft(timeval start, float req) { double timesince; double timeleft; struct timeval now { 0, 0 }; gettimeofday(&now, nullptr); timesince = (double)(now.tv_sec * 1000.0 + now.tv_usec / 1000) - (double)(start.tv_sec * 1000.0 + start.tv_usec / 1000); timesince = timesince / 1000; timeleft = req - timesince; return timeleft; } void CCDSim::TimerHit() { uint32_t nextTimer = POLLMS; // No need to reset timer if we are not connected anymore if (!isConnected()) return; if (InExposure) { if (AbortPrimaryFrame) { InExposure = false; AbortPrimaryFrame = false; } else { float timeleft; timeleft = CalcTimeLeft(ExpStart, ExposureRequest); //IDLog("CCD Exposure left: %g - Requset: %g\n", timeleft, ExposureRequest); if (timeleft < 0) timeleft = 0; PrimaryCCD.setExposureLeft(timeleft); if (timeleft < 1.0) { if (timeleft <= 0.001) { InExposure = false; PrimaryCCD.binFrame(); ExposureComplete(&PrimaryCCD); } else { // set a shorter timer nextTimer = timeleft * 1000; } } } } if (InGuideExposure) { float timeleft; timeleft = CalcTimeLeft(GuideExpStart, GuideExposureRequest); //IDLog("GUIDE Exposure left: %g - Requset: %g\n", timeleft, GuideExposureRequest); if (timeleft < 0) timeleft = 0; //ImageExposureN[0].value = timeleft; //IDSetNumber(ImageExposureNP, nullptr); GuideCCD.setExposureLeft(timeleft); if (timeleft < 1.0) { if (timeleft <= 0.001) { InGuideExposure = false; if (!AbortGuideFrame) { GuideCCD.binFrame(); ExposureComplete(&GuideCCD); if (InGuideExposure) { // the call to complete triggered another exposure timeleft = CalcTimeLeft(GuideExpStart, GuideExposureRequest); if (timeleft < 1.0) { nextTimer = timeleft * 1000; } } } else { //IDLog("Not sending guide frame cuz of abort\n"); } AbortGuideFrame = false; } else { nextTimer = timeleft * 1000; // set a shorter timer } } } if (TemperatureNP.s == IPS_BUSY) { if (fabs(TemperatureRequest - TemperatureN[0].value) <= 0.5) { LOGF_INFO("Temperature reached requested value %.2f degrees C", TemperatureRequest); TemperatureN[0].value = TemperatureRequest; TemperatureNP.s = IPS_OK; } else { if (TemperatureRequest < TemperatureN[0].value) TemperatureN[0].value -= 0.5; else TemperatureN[0].value += 0.5; } IDSetNumber(&TemperatureNP, nullptr); // Above 20, cooler is off if (TemperatureN[0].value >= 20) { CoolerS[0].s = ISS_OFF; CoolerS[0].s = ISS_ON; CoolerSP.s = IPS_IDLE; IDSetSwitch(&CoolerSP, nullptr); } } SetTimer(nextTimer); } int CCDSim::DrawCcdFrame(INDI::CCDChip *targetChip) { // CCD frame is 16 bit data uint16_t val; float ExposureTime; float targetFocalLength; uint16_t *ptr = reinterpret_cast(targetChip->getFrameBuffer()); if (targetChip->getXRes() == 500) ExposureTime = GuideExposureRequest*4; else if (Streamer->isStreaming()) ExposureTime = (ExposureRequest < 1) ? (ExposureRequest * 100) : ExposureRequest * 2; else ExposureTime = ExposureRequest; if (TelescopeTypeS[TELESCOPE_PRIMARY].s == ISS_ON) targetFocalLength = primaryFocalLength; else targetFocalLength = guiderFocalLength; if (ShowStarField) { char gsccmd[250]; FILE *pp; int stars = 0; int lines = 0; int drawn = 0; int x, y; float PEOffset; float PESpot; float decDrift; double rad; // telescope ra in degrees double rar; // telescope ra in radians double decr; // telescope dec in radians; int nwidth = 0, nheight = 0; double timesince; time_t now; time(&now); // Lets figure out where we are on the pe curve timesince = difftime(now, RunStart); // This is our spot in the curve PESpot = timesince / PEPeriod; // Now convert to radians PESpot = PESpot * 2.0 * 3.14159; PEOffset = PEMax * std::sin(PESpot); //fprintf(stderr,"PEOffset = %4.2f arcseconds timesince %4.2f\n",PEOffset,timesince); PEOffset = PEOffset / 3600; // convert to degrees //PeOffset=PeOffset/15; // ra is in h:mm // Start by clearing the frame buffer memset(targetChip->getFrameBuffer(), 0, targetChip->getFrameBufferSize()); // Spin up a set of plate constants that will relate // ra/dec of stars, to our fictitious ccd layout // to account for various rotations etc // we should spin up some plate constants here // then we can use these constants to rotate and offset // the standard co-ordinates on each star for drawing // a ccd frame; double pa, pb, pc, pd, pe, pf; // Pixels per radian double pprx, ppry; // Scale in arcsecs per pixel double Scalex; double Scaley; // CCD width in pixels double ccdW = targetChip->getXRes(); // Pixels per radian pprx = targetFocalLength / targetChip->getPixelSizeX() * 1000; ppry = targetFocalLength / targetChip->getPixelSizeY() * 1000; // we do a simple scale for x and y locations // based on the focal length and pixel size // focal length in mm, pixels in microns // JM: 2015-03-17: Using a simpler formula, Scalex and Scaley are in arcsecs/pixel Scalex = (targetChip->getPixelSizeX() / targetFocalLength) * 206.3; Scaley = (targetChip->getPixelSizeY() / targetFocalLength) * 206.3; #if 0 DEBUGF( INDI::Logger::DBG_DEBUG, "pprx: %g pixels per radian ppry: %g pixels per radian ScaleX: %g arcsecs/pixel ScaleY: %g arcsecs/pixel", pprx, ppry, Scalex, Scaley); #endif double theta = rotationCW + 270; if (theta > 360) theta -= 360; else if (theta < -360) theta += 360; // JM: 2015-03-17: Next we do a rotation assuming CW for angle theta pa = pprx * cos(theta * M_PI / 180.0); pb = ppry * sin(theta * M_PI / 180.0); pd = pprx * -sin(theta * M_PI / 180.0); pe = ppry * cos(theta * M_PI / 180.0); nwidth = targetChip->getXRes(); pc = nwidth / 2; nheight = targetChip->getYRes(); pf = nheight / 2; ImageScalex = Scalex; ImageScaley = Scaley; #ifdef USE_EQUATORIAL_PE if (!usePE) { #endif currentRA = RA; currentDE = Dec; ln_equ_posn epochPos { 0, 0 }, J2000Pos { 0, 0 }; epochPos.ra = currentRA * 15.0; epochPos.dec = currentDE; // Convert from JNow to J2000 ln_get_equ_prec2(&epochPos, ln_get_julian_from_sys(), JD2000, &J2000Pos); currentRA = J2000Pos.ra / 15.0; currentDE = J2000Pos.dec; currentDE += guideNSOffset; currentRA += guideWEOffset; #ifdef USE_EQUATORIAL_PE } #endif // calc this now, we will use it a lot later rad = currentRA * 15.0; rar = rad * 0.0174532925; // offsetting the dec by the guide head offset float cameradec; cameradec = currentDE + OAGoffset / 60; decr = cameradec * 0.0174532925; decDrift = (polarDrift * polarError * cos(decr)) / 3.81; // Add declination drift, if any. decr += decDrift / 3600.0 * 0.0174532925; //fprintf(stderr,"decPE %7.5f cameradec %7.5f CenterOffsetDec %4.4f\n",decPE,cameradec,decr); // now lets calculate the radius we need to fetch float radius; radius = sqrt((Scalex * Scalex * targetChip->getXRes() / 2.0 * targetChip->getXRes() / 2.0) + (Scaley * Scaley * targetChip->getYRes() / 2.0 * targetChip->getYRes() / 2.0)); // we have radius in arcseconds now radius = radius / 60; // convert to arcminutes #if 0 LOGF_DEBUG("Lookup radius %4.2f", radius); #endif // A saturationmag star saturates in one second // and a limitingmag produces a one adu level in one second // solve for zero point and system gain k = (saturationmag - limitingmag) / ((-2.5 * log(maxval)) - (-2.5 * log(1.0 / 2.0))); z = saturationmag - k * (-2.5 * log(maxval)); //z=z+saturationmag; //IDLog("K=%4.2f Z=%4.2f\n",k,z); // Should probably do some math here to figure out the dimmest // star we can see on this exposure // and only fetch to that magnitude // for now, just use the limiting mag number with some room to spare float lookuplimit = limitingmag; if (radius > 60) lookuplimit = 11; // if this is a light frame, we need a star field drawn INDI::CCDChip::CCD_FRAME ftype = targetChip->getFrameType(); if (ftype == INDI::CCDChip::LIGHT_FRAME) { AutoCNumeric locale; //sprintf(gsccmd,"gsc -c %8.6f %+8.6f -r 120 -m 0 9.1",rad+PEOffset,decPE); sprintf(gsccmd, "gsc -c %8.6f %+8.6f -r %4.1f -m 0 %4.2f -n 3000", rad + PEOffset, cameradec, radius, lookuplimit); LOGF_DEBUG("%s", gsccmd); pp = popen(gsccmd, "r"); if (pp != nullptr) { char line[256]; while (fgets(line, 256, pp) != nullptr) { //fprintf(stderr,"%s",line); // ok, lets parse this line for specifcs we want char id[20]; char plate[6]; char ob[6]; float mag; float mage; float ra; float dec; float pose; int band; float dist; int dir; int c; int rc; rc = sscanf(line, "%10s %f %f %f %f %f %d %d %4s %2s %f %d", id, &ra, &dec, &pose, &mag, &mage, &band, &c, plate, ob, &dist, &dir); //fprintf(stderr,"Parsed %d items\n",rc); if (rc == 12) { lines++; //if(c==0) { stars++; //fprintf(stderr,"%s %8.4f %8.4f %5.2f %5.2f %d\n",id,ra,dec,mag,dist,dir); // Convert the ra/dec to standard co-ordinates double sx; // standard co-ords double sy; // double srar; // star ra in radians double sdecr; // star dec in radians; double ccdx; double ccdy; //fprintf(stderr,"line %s",line); //fprintf(stderr,"parsed %6.5f %6.5f\n",ra,dec); srar = ra * 0.0174532925; sdecr = dec * 0.0174532925; // Handbook of astronomical image processing // page 253 // equations 9.1 and 9.2 // convert ra/dec to standard co-ordinates sx = cos(sdecr) * sin(srar - rar) / (cos(decr) * cos(sdecr) * cos(srar - rar) + sin(decr) * sin(sdecr)); sy = (sin(decr) * cos(sdecr) * cos(srar - rar) - cos(decr) * sin(sdecr)) / (cos(decr) * cos(sdecr) * cos(srar - rar) + sin(decr) * sin(sdecr)); // now convert to pixels ccdx = pa * sx + pb * sy + pc; ccdy = pd * sx + pe * sy + pf; // Invert horizontally ccdx = ccdW - ccdx; rc = DrawImageStar(targetChip, mag, ccdx, ccdy, ExposureTime); drawn += rc; if (rc == 1) { //LOGF_DEBUG("star %s scope %6.4f %6.4f star %6.4f %6.4f ccd %6.2f %6.2f",id,rad,decPE,ra,dec,ccdx,ccdy); //LOGF_DEBUG("star %s ccd %6.2f %6.2f",id,ccdx,ccdy); } } } pclose(pp); } else { LOG_ERROR("Error looking up stars, is gsc installed with appropriate environment variables set ??"); } if (drawn == 0) { LOG_ERROR("Got no stars, is gsc installed with appropriate environment variables set ??"); } } //fprintf(stderr,"Got %d stars from %d lines drew %d\n",stars,lines,drawn); // now we need to add background sky glow, with vignetting // this is essentially the same math as drawing a dim star with // fwhm equivalent to the full field of view if (ftype == INDI::CCDChip::LIGHT_FRAME || ftype == INDI::CCDChip::FLAT_FRAME) { float skyflux; // calculate flux from our zero point and gain values float glow = skyglow; if (ftype == INDI::CCDChip::FLAT_FRAME) { // Assume flats are done with a diffuser // in broad daylight, so, the sky magnitude // is much brighter than at night glow = skyglow / 10; } //fprintf(stderr,"Using glow %4.2f\n",glow); skyflux = pow(10, ((glow - z) * k / -2.5)); // ok, flux represents one second now // scale up linearly for exposure time skyflux = skyflux * ExposureTime; //IDLog("SkyFlux = %g ExposureRequest %g\n",skyflux,ExposureTime); unsigned short *pt; pt = (uint16_t *)targetChip->getFrameBuffer(); nheight = targetChip->getSubH(); nwidth = targetChip->getSubW(); for (int y = 0; y < nheight; y++) { for (int x = 0; x < nwidth; x++) { float dc; // distance from center float fp; // flux this pixel; float sx, sy; float vig; sx = nwidth / 2 - x; sy = nheight / 2 - y; vig = nwidth; vig = vig * ImageScalex; // need to make this account for actual pixel size dc = std::sqrt(sx * sx * ImageScalex * ImageScalex + sy * sy * ImageScaley * ImageScaley); // now we have the distance from center, in arcseconds // now lets plot a gaussian falloff to the edges // float fa; fa = exp(-2.0 * 0.7 * (dc * dc) / vig / vig); // get the current value fp = pt[0]; // Add the sky glow fp += skyflux; // now scale it for the vignetting fp = fa * fp; // clamp to limits if (fp > maxval) fp = maxval; if (fp > maxpix) maxpix = fp; if (fp < minpix) minpix = fp; // and put it back pt[0] = fp; pt++; } } } // Now we add some bias and read noise int subX = targetChip->getSubX(); int subY = targetChip->getSubY(); int subW = targetChip->getSubW() + subX; int subH = targetChip->getSubH() + subY; if (maxnoise > 0) { for (x = subX; x < subW; x++) { for (y = subY; y < subH; y++) { int noise; noise = random(); noise = noise % maxnoise; // //IDLog("noise is %d\n", noise); AddToPixel(targetChip, x, y, bias + noise); } } } } else { testvalue++; if (testvalue > 255) testvalue = 0; val = testvalue; int nbuf = targetChip->getSubW() * targetChip->getSubH(); for (int x = 0; x < nbuf; x++) { *ptr = val++; ptr++; } } return 0; } int CCDSim::DrawImageStar(INDI::CCDChip *targetChip, float mag, float x, float y, float ExposureTime) { //float d; //float r; int sx, sy; int drew = 0; int boxsizex = 5; int boxsizey = 5; float flux; int subX = targetChip->getSubX(); int subY = targetChip->getSubY(); int subW = targetChip->getSubW() + subX; int subH = targetChip->getSubH() + subY; if ((x < subX) || (x > subW || (y < subY) || (y > subH))) { // this star is not on the ccd frame anyways return 0; } // calculate flux from our zero point and gain values flux = pow(10, ((mag - z) * k / -2.5)); // ok, flux represents one second now // scale up linearly for exposure time flux = flux * ExposureTime; float qx; // we need a box size that gives a radius at least 3 times fwhm qx = seeing / ImageScalex; qx = qx * 3; boxsizex = (int)qx; boxsizex++; qx = seeing / ImageScaley; qx = qx * 3; boxsizey = (int)qx; boxsizey++; //IDLog("BoxSize %d %d\n",boxsizex,boxsizey); for (sy = -boxsizey; sy <= boxsizey; sy++) { for (sx = -boxsizey; sx <= boxsizey; sx++) { int rc; float dc; // distance from center float fp; // flux this pixel; // need to make this account for actual pixel size dc = std::sqrt(sx * sx * ImageScalex * ImageScalex + sy * sy * ImageScaley * ImageScaley); // now we have the distance from center, in arcseconds // This should be gaussian, but, for now we'll just go with // a simple linear function float fa = exp(-2.0 * 0.7 * (dc * dc) / seeing / seeing); fp = fa * flux; if (fp < 0) fp = 0; rc = AddToPixel(targetChip, x + sx, y + sy, fp); if (rc != 0) drew = 1; } } return drew; } int CCDSim::AddToPixel(INDI::CCDChip *targetChip, int x, int y, int val) { int nwidth = targetChip->getSubW(); int nheight = targetChip->getSubH(); x -= targetChip->getSubX(); y -= targetChip->getSubY(); int drew = 0; if (x >= 0) { if (x < nwidth) { if (y >= 0) { if (y < nheight) { unsigned short *pt; int newval; drew++; pt = (uint16_t *)targetChip->getFrameBuffer(); pt += (y * nwidth); pt += x; newval = pt[0]; newval += val; if (newval > maxval) newval = maxval; if (newval > maxpix) maxpix = newval; if (newval < minpix) minpix = newval; pt[0] = newval; } } } } return drew; } IPState CCDSim::GuideNorth(float v) { guideNSOffset += v / 1000 * GuideRate / 3600; return IPS_OK; } IPState CCDSim::GuideSouth(float v) { guideNSOffset += v / -1000 * GuideRate / 3600; return IPS_OK; } IPState CCDSim::GuideEast(float v) { float c = v / 1000 * GuideRate; c = c/ 3600.0 / 15.0; c = c/ (cos(currentDE * 0.0174532925)); guideWEOffset += c; return IPS_OK; } IPState CCDSim::GuideWest(float v) { float c = v / -1000 * GuideRate; c = c/ 3600.0 / 15.0; c = c/ (cos(currentDE * 0.0174532925)); guideWEOffset += c; return IPS_OK; } bool CCDSim::ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n) { if (dev != nullptr && strcmp(dev, getDeviceName()) == 0) { // This is for our device // Now lets see if it's something we process here if (strcmp(name, FilterNameTP->name) == 0) { INDI::FilterInterface::processText(dev, name, texts, names, n); return true; } } return INDI::CCD::ISNewText(dev, name, texts, names, n); } bool CCDSim::ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) { if (dev != nullptr && strcmp(dev, getDeviceName()) == 0) { if (strcmp(name, "SIMULATOR_SETTINGS") == 0) { IUUpdateNumber(SimulatorSettingsNV, values, names, n); SimulatorSettingsNV->s = IPS_OK; // Reset our parameters now SetupParms(); IDSetNumber(SimulatorSettingsNV, nullptr); return true; } // Record PE EQ to simulate different position in the sky than actual mount coordinate // This can be useful to simulate Periodic Error or cone error or any arbitrary error. if (!strcmp(name, EqPENP.name)) { IUUpdateNumber(&EqPENP, values, names, n); EqPENP.s = IPS_OK; ln_equ_posn epochPos { 0, 0 }, J2000Pos { 0, 0 }; epochPos.ra = EqPEN[AXIS_RA].value * 15.0; epochPos.dec = EqPEN[AXIS_DE].value; RA = EqPEN[AXIS_RA].value; Dec = EqPEN[AXIS_DE].value; ln_get_equ_prec2(&epochPos, ln_get_julian_from_sys(), JD2000, &J2000Pos); currentRA = J2000Pos.ra / 15.0; currentDE = J2000Pos.dec; usePE = true; IDSetNumber(&EqPENP, nullptr); return true; } if (strcmp(name, FilterSlotNP.name) == 0) { INDI::FilterInterface::processNumber(dev, name, values, names, n); return true; } } return INDI::CCD::ISNewNumber(dev, name, values, names, n); } bool CCDSim::ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) { if (dev != nullptr && strcmp(dev, getDeviceName()) == 0) { if (strcmp(name, "ON_TIME_FACTOR") == 0) { // client is telling us what to do with co-ordinate requests TimeFactorSV->s = IPS_OK; IUUpdateSwitch(TimeFactorSV, states, names, n); // Update client display IDSetSwitch(TimeFactorSV, nullptr); if (TimeFactorS[0].s == ISS_ON) { //IDLog("CCDSim:: Time Factor 1\n"); TimeFactor = 1; } if (TimeFactorS[1].s == ISS_ON) { //IDLog("CCDSim:: Time Factor 0.1\n"); TimeFactor = 0.1; } if (TimeFactorS[2].s == ISS_ON) { //IDLog("CCDSim:: Time Factor 0.01\n"); TimeFactor = 0.01; } return true; } } if (strcmp(name, CoolerSP.name) == 0) { IUUpdateSwitch(&CoolerSP, states, names, n); if (CoolerS[0].s == ISS_ON) CoolerSP.s = IPS_BUSY; else { CoolerSP.s = IPS_IDLE; TemperatureRequest = 20; TemperatureNP.s = IPS_BUSY; } IDSetSwitch(&CoolerSP, nullptr); return true; } // Nobody has claimed this, so, ignore it return INDI::CCD::ISNewSwitch(dev, name, states, names, n); } void CCDSim::activeDevicesUpdated() { #ifdef USE_EQUATORIAL_PE IDSnoopDevice(ActiveDeviceT[0].text, "EQUATORIAL_PE"); #else IDSnoopDevice(ActiveDeviceT[0].text, "EQUATORIAL_EOD_COORD"); #endif IDSnoopDevice(ActiveDeviceT[1].text, "FWHM"); strncpy(FWHMNP.device, ActiveDeviceT[1].text, MAXINDIDEVICE); } bool CCDSim::ISSnoopDevice(XMLEle *root) { if (IUSnoopNumber(root, &FWHMNP) == 0) { seeing = FWHMNP.np[0].value; return true; } // We try to snoop EQPEC first, if not found, we snoop regular EQNP #ifdef USE_EQUATORIAL_PE const char *propName = findXMLAttValu(root, "name"); if (!strcmp(propName, EqPENP.name)) { XMLEle *ep = nullptr; int rc_ra = -1, rc_de = -1; double newra = 0, newdec = 0; for (ep = nextXMLEle(root, 1); ep != nullptr; ep = nextXMLEle(root, 0)) { const char *elemName = findXMLAttValu(ep, "name"); if (!strcmp(elemName, "RA_PE")) rc_ra = f_scansexa(pcdataXMLEle(ep), &newra); else if (!strcmp(elemName, "DEC_PE")) rc_de = f_scansexa(pcdataXMLEle(ep), &newdec); } if (rc_ra == 0 && rc_de == 0 && ((newra != raPE) || (newdec != decPE))) { ln_equ_posn epochPos { 0, 0 }, J2000Pos { 0, 0 }; epochPos.ra = newra * 15.0; epochPos.dec = newdec; ln_get_equ_prec2(&epochPos, ln_get_julian_from_sys(), JD2000, &J2000Pos); raPE = J2000Pos.ra / 15.0; decPE = J2000Pos.dec; usePE = true; EqPEN[AXIS_RA].value = newra; EqPEN[AXIS_DE].value = newdec; IDSetNumber(&EqPENP, nullptr); LOGF_DEBUG("raPE %g decPE %g Snooped raPE %g decPE %g", raPE, decPE, newra, newdec); return true; } } #endif return INDI::CCD::ISSnoopDevice(root); } bool CCDSim::saveConfigItems(FILE *fp) { // Save CCD Config INDI::CCD::saveConfigItems(fp); // Save Filter Wheel Config INDI::FilterInterface::saveConfigItems(fp); // Save CCD Simulator Config IUSaveConfigNumber(fp, SimulatorSettingsNV); IUSaveConfigSwitch(fp, TimeFactorSV); return true; } bool CCDSim::SelectFilter(int f) { CurrentFilter = f; SelectFilterDone(f); return true; } int CCDSim::QueryFilter() { return CurrentFilter; } bool CCDSim::StartStreaming() { ExposureRequest = 1.0 / Streamer->getTargetFPS(); pthread_mutex_lock(&condMutex); streamPredicate = 1; pthread_mutex_unlock(&condMutex); pthread_cond_signal(&cv); return true; } bool CCDSim::StopStreaming() { pthread_mutex_lock(&condMutex); streamPredicate = 0; pthread_mutex_unlock(&condMutex); pthread_cond_signal(&cv); return true; } bool CCDSim::UpdateCCDFrame(int x, int y, int w, int h) { long bin_width = w / PrimaryCCD.getBinX(); long bin_height = h / PrimaryCCD.getBinY(); bin_width = bin_width - (bin_width % 2); bin_height = bin_height - (bin_height % 2); Streamer->setSize(bin_width, bin_height); return INDI::CCD::UpdateCCDFrame(x,y,w,h); } bool CCDSim::UpdateCCDBin(int hor, int ver) { if (hor == 3 || ver == 3) { LOG_ERROR("3x3 binning is not supported."); return false; } long bin_width = PrimaryCCD.getSubW() / hor; long bin_height = PrimaryCCD.getSubH() / ver; bin_width = bin_width - (bin_width % 2); bin_height = bin_height - (bin_height % 2); Streamer->setSize(bin_width, bin_height); return INDI::CCD::UpdateCCDBin(hor,ver); } void *CCDSim::streamVideoHelper(void *context) { return ((CCDSim *)context)->streamVideo(); } void *CCDSim::streamVideo() { struct itimerval tframe1, tframe2; double s1, s2, deltas; while (true) { pthread_mutex_lock(&condMutex); while (streamPredicate == 0) { pthread_cond_wait(&cv, &condMutex); ExposureRequest = 1.0 / Streamer->getTargetFPS(); } if (terminateThread) break; // release condMutex pthread_mutex_unlock(&condMutex); // Simulate exposure time //usleep(ExposureRequest*1e5); // 16 bit DrawCcdFrame(&PrimaryCCD); PrimaryCCD.binFrame(); getitimer(ITIMER_REAL, &tframe1); s1 = ((double)tframe1.it_value.tv_sec) + ((double)tframe1.it_value.tv_usec / 1e6); s2 = ((double)tframe2.it_value.tv_sec) + ((double)tframe2.it_value.tv_usec / 1e6); deltas = fabs(s2 - s1); if (deltas < ExposureRequest) usleep(fabs(ExposureRequest-deltas)*1e6); uint32_t size = PrimaryCCD.getFrameBufferSize() / (PrimaryCCD.getBinX()*PrimaryCCD.getBinY()); Streamer->newFrame(PrimaryCCD.getFrameBuffer(), size); getitimer(ITIMER_REAL, &tframe2); } pthread_mutex_unlock(&condMutex); return 0; } libindi/drivers/ccd/guide_simulator.h0000664000175000017500000001042413263645557017247 0ustar jasemjasem/******************************************************************************* Copyright(c) 2017 Jasem Mutlaq. All rights reserved. Copyright(c) 2010 Gerry Rozema. All rights reserved. This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2 as published by the Free Software Foundation. . 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 Library General Public License for more details. . You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. *******************************************************************************/ #pragma once #include "indiccd.h" /** * @brief The GuideSim class provides a simple Guide CCD simulator driver. * * It can stream video and generate images based on General-Star-Catalog tool (gsc). It simulates guiding pulses. */ class GuideSim : public INDI::CCD { public: GuideSim(); virtual ~GuideSim() = default; const char *getDefaultName() override; bool initProperties() override; bool updateProperties() override; void ISGetProperties(const char *dev) override; virtual bool ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) override; virtual bool ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) override; virtual bool ISSnoopDevice(XMLEle *root) override; static void *streamVideoHelper(void *context); void *streamVideo(); protected: bool Connect() override; bool Disconnect() override; bool StartExposure(float duration) override; bool AbortExposure() override; void TimerHit() override; int DrawCcdFrame(INDI::CCDChip *targetChip); int DrawImageStar(INDI::CCDChip *targetChip, float, float, float, float ExposureTime); int AddToPixel(INDI::CCDChip *targetChip, int, int, int); IPState GuideNorth(float) override; IPState GuideSouth(float) override; IPState GuideEast(float) override; IPState GuideWest(float) override; virtual bool saveConfigItems(FILE *fp) override; virtual void activeDevicesUpdated() override; virtual bool UpdateCCDFrame(int x, int y, int w, int h) override; virtual bool UpdateCCDBin(int hor, int ver) override; virtual bool StartStreaming() override; virtual bool StopStreaming() override; private: float CalcTimeLeft(timeval, float); bool SetupParms(); float ExposureRequest { 0 }; struct timeval ExpStart { 0, 0 }; int testvalue { 0 }; bool ShowStarField { true }; int bias { 1500 }; int maxnoise { 20 }; int maxval { 65000 }; int maxpix { 0 }; int minpix { 65000 }; float skyglow { 40 }; float limitingmag { 11.5 }; float saturationmag { 2 }; float seeing { 3.5 }; float ImageScalex { 1.0 }; float ImageScaley { 1.0 }; // An oag is offset this much from center of scope position (arcminutes) float OAGoffset { 0 }; float rotationCW { 0 }; float TimeFactor { 1 }; // our zero point calcs used for drawing stars float k { 0 }; float z { 0 }; float guideNSOffset {0}; float guideWEOffset {0}; bool AbortPrimaryFrame { false }; /// Guide rate is 7 arcseconds per second float GuideRate { 7 }; /// Our PEPeriod is 8 minutes and we have a 22 arcsecond swing float PEPeriod { 8*60 }; float PEMax { 11 }; double currentRA { 0 }; double currentDE { 0 }; bool usePE { false }; time_t RunStart; float polarError { 0 }; float polarDrift { 0 }; int streamPredicate; pthread_t primary_thread; bool terminateThread; // And this lives in our simulator settings page INumberVectorProperty *SimulatorSettingsNV; INumber SimulatorSettingsN[14]; ISwitch TimeFactorS[3]; ISwitchVectorProperty *TimeFactorSV; // We are going to snoop these from focuser INumberVectorProperty FWHMNP; INumber FWHMN[1]; INumberVectorProperty EqPENP; INumber EqPEN[2]; }; libindi/drivers/focuser/0000775000175000017500000000000013263645557014616 5ustar jasemjasemlibindi/drivers/focuser/focuslynxbase.h0000664000175000017500000001505413263645557017661 0ustar jasemjasem/* Focus Lynx INDI driver Copyright (C) 2015 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 */ #pragma once #include "indifocuser.h" #include #include #include "connectionplugins/connectionserial.h" #include "connectionplugins/connectiontcp.h" #include #include #include #include #include #include #define LYNXFOCUS_MAX_RETRIES 1 #define LYNXFOCUS_TIMEOUT 2 #define LYNXFOCUS_MAXBUF 16 #define LYNXFOCUS_TEMPERATURE_FREQ 20 /* Update every 20 POLLMS cycles. For POLLMS 500ms = 10 seconds freq */ #define LYNXFOCUS_POSITION_THRESHOLD 5 /* Only send position updates to client if the diff exceeds 5 steps */ #define FOCUS_SETTINGS_TAB "Settings" #define FOCUS_STATUS_TAB "Status" #define HUB_SETTINGS_TAB "Device" #define VERSION 1 #define SUBVERSION 3 class FocusLynxBase : public INDI::Focuser { public: FocusLynxBase(); FocusLynxBase(const char *target); ~FocusLynxBase(); enum { FOCUS_A_COEFF, FOCUS_B_COEFF, FOCUS_C_COEFF, FOCUS_D_COEFF, FOCUS_E_COEFF, FOCUS_F_COEFF }; typedef enum { STATUS_MOVING, STATUS_HOMING, STATUS_HOMED, STATUS_FFDETECT, STATUS_TMPPROBE, STATUS_REMOTEIO, STATUS_HNDCTRL, STATUS_REVERSE, STATUS_UNKNOWN } LYNX_STATUS; enum { GOTO_CENTER, GOTO_HOME }; virtual bool Handshake() override; virtual const char *getDefaultName() override; virtual bool initProperties() override; virtual void ISGetProperties(const char *dev) override; virtual bool updateProperties() override; virtual bool saveConfigItems(FILE *fp) override; virtual bool ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) override; virtual bool ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) override; virtual bool ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n) override; virtual IPState MoveAbsFocuser(uint32_t targetPosition) override; virtual IPState MoveRelFocuser(FocusDirection dir, uint32_t ticks) override; virtual IPState MoveFocuser(FocusDirection dir, int speed, uint16_t duration) override; virtual bool AbortFocuser() override; virtual void TimerHit() override; virtual int getVersion(int *major, int *minor, int *sub); void setFocusTarget(const char *target); const char *getFocusTarget(); virtual void debugTriggered(bool enable) override; // Device bool setDeviceType(int index); uint32_t DBG_FOCUS; // Misc functions bool ack(); bool isResponseOK(); protected: // Move from private to public to validate bool configurationComplete; // List all supported models ISwitch *ModelS; ISwitchVectorProperty ModelSP; // Led Intensity Value INumber LedN[1]; INumberVectorProperty LedNP; // Store version of the firmware from the HUB char version[16]; private: uint32_t simPosition; uint32_t targetPosition; uint32_t maxControllerTicks; ISState simStatus[8]; bool simCompensationOn; char focusTarget[8]; std::map lynxModels; struct timeval focusMoveStart; float focusMoveRequest; // Get functions bool getFocusConfig(); bool getFocusStatus(); // Set functions // Position bool setFocusPosition(u_int16_t position); // Temperature bool setTemperatureCompensation(bool enable); bool setTemperatureCompensationMode(char mode); bool setTemperatureCompensationCoeff(char mode, int16_t coeff); bool setTemperatureCompensationOnStart(bool enable); // Backlash bool setBacklashCompensation(bool enable); bool setBacklashCompensationSteps(uint16_t steps); // Sync bool sync(uint32_t position); // Motion functions bool stop(); bool home(); bool center(); bool reverse(bool enable); // Led level bool setLedLevel(int level); // Device Nickname bool setDeviceNickname(const char *nickname); // Misc functions bool resetFactory(); float calcTimeLeft(timeval, float); // Properties // Set/Get Temperature INumber TemperatureN[1]; INumberVectorProperty TemperatureNP; // Enable/Disable temperature compensation ISwitch TemperatureCompensateS[2]; ISwitchVectorProperty TemperatureCompensateSP; // Enable/Disable temperature compensation on start ISwitch TemperatureCompensateOnStartS[2]; ISwitchVectorProperty TemperatureCompensateOnStartSP; // Temperature Coefficient INumber TemperatureCoeffN[5]; INumberVectorProperty TemperatureCoeffNP; // Temperature Coefficient Mode ISwitch TemperatureCompensateModeS[5]; ISwitchVectorProperty TemperatureCompensateModeSP; // Enable/Disable backlash ISwitch BacklashCompensationS[2]; ISwitchVectorProperty BacklashCompensationSP; // Backlash Value INumber BacklashN[1]; INumberVectorProperty BacklashNP; // Reset to Factory setting ISwitch ResetS[1]; ISwitchVectorProperty ResetSP; // Reverse Direction ISwitch ReverseS[2]; ISwitchVectorProperty ReverseSP; // Go to home/center ISwitch GotoS[2]; ISwitchVectorProperty GotoSP; // Status indicators ILight StatusL[8]; ILightVectorProperty StatusLP; // Sync to a particular position INumber SyncN[1]; INumberVectorProperty SyncNP; // Max Travel for relative focusers INumber MaxTravelN[1]; INumberVectorProperty MaxTravelNP; // Focus name configure in the HUB IText HFocusNameT[1] {}; ITextVectorProperty HFocusNameTP; bool isAbsolute; bool isSynced; bool isHoming; }; libindi/drivers/focuser/robofocus.h0000664000175000017500000000630513263645557016774 0ustar jasemjasem/* RoboFocus Copyright (C) 2006 Markus Wildi (markus.wildi@datacomm.ch) 2011 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 */ #pragma once #include "indifocuser.h" class RoboFocus : public INDI::Focuser { public: RoboFocus(); virtual ~RoboFocus() = default; virtual bool Handshake(); const char *getDefaultName(); virtual bool initProperties(); virtual bool updateProperties(); virtual bool ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n); virtual bool ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n); virtual IPState MoveAbsFocuser(uint32_t targetTicks); virtual IPState MoveRelFocuser(FocusDirection dir, uint32_t ticks); virtual bool AbortFocuser(); virtual void TimerHit(); protected: bool saveConfigItems(FILE *fp); private: unsigned char CheckSum(char *rf_cmd); unsigned char CalculateSum(const char *rf_cmd); int SendCommand(char *rf_cmd); int ReadResponse(char *buf); void GetFocusParams(); int updateRFPosition(double *value); int updateRFTemperature(double *value); int updateRFBacklash(double *value); int updateRFFirmware(char *rf_cmd); int updateRFMotorSettings(double *duty, double *delay, double *ticks); int updateRFPositionRelativeInward(double value); int updateRFPositionRelativeOutward(double value); int updateRFPositionAbsolute(double value); int updateRFPowerSwitches(int s, int new_sn, int *cur_s1LL, int *cur_s2LR, int *cur_s3RL, int *cur_s4RR); int updateRFMaxPosition(double *value); int updateRFSetPosition(const double *value); int ReadUntilComplete(char *buf, int timeout); int timerID { -1 }; double targetPos { 0 }; double simulatedTemperature { 0 }; double simulatedPosition { 0 }; INumber TemperatureN[1]; INumberVectorProperty TemperatureNP; INumber SettingsN[3]; INumberVectorProperty SettingsNP; ISwitch PowerSwitchesS[4]; ISwitchVectorProperty PowerSwitchesSP; INumber MinMaxPositionN[2]; INumberVectorProperty MinMaxPositionNP; INumber MaxTravelN[1]; INumberVectorProperty MaxTravelNP; INumber SetRegisterPositionN[1]; INumberVectorProperty SetRegisterPositionNP; INumber RelMovementN[1]; INumberVectorProperty RelMovementNP; INumber AbsMovementN[1]; INumberVectorProperty AbsMovementNP; INumber SetBacklashN[1]; INumberVectorProperty SetBacklashNP; }; libindi/drivers/focuser/steeldrive.cpp0000664000175000017500000013117013263645557017473 0ustar jasemjasem/* Baader Steeldrive Focuser Copyright (C) 2014 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 */ #include "steeldrive.h" #include "indicom.h" #include #include #include #include #include #define STEELDRIVE_MAX_RETRIES 1 #define STEELDRIVE_TIMEOUT 1 #define STEELDRIVE_MAXBUF 16 #define STEELDRIVE_CMD 9 #define STEELDRIVE_CMD_LONG 11 #define STEELDRIVE_TEMPERATURE_FREQ 20 /* Update every 20 POLLMS cycles. For POLLMS 500ms = 10 seconds freq */ #define STEELDIVE_POSITION_THRESHOLD 5 /* Only send position updates to client if the diff exceeds 5 steps */ #define FOCUS_SETTINGS_TAB "Settings" std::unique_ptr steelDrive(new SteelDrive()); void ISGetProperties(const char *dev) { steelDrive->ISGetProperties(dev); } void ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) { steelDrive->ISNewSwitch(dev, name, states, names, n); } void ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n) { steelDrive->ISNewText(dev, name, texts, names, n); } void ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) { steelDrive->ISNewNumber(dev, name, values, names, n); } void ISNewBLOB(const char *dev, const char *name, int sizes[], int blobsizes[], char *blobs[], char *formats[], char *names[], int n) { INDI_UNUSED(dev); INDI_UNUSED(name); INDI_UNUSED(sizes); INDI_UNUSED(blobsizes); INDI_UNUSED(blobs); INDI_UNUSED(formats); INDI_UNUSED(names); INDI_UNUSED(n); } void ISSnoopDevice(XMLEle *root) { steelDrive->ISSnoopDevice(root); } SteelDrive::SteelDrive() { // Can move in Absolute & Relative motions, can AbortFocuser motion, and has variable speed. FI::SetCapability(FOCUSER_CAN_ABS_MOVE | FOCUSER_CAN_REL_MOVE | FOCUSER_CAN_ABORT | FOCUSER_HAS_VARIABLE_SPEED); } bool SteelDrive::initProperties() { INDI::Focuser::initProperties(); FocusSpeedN[0].min = 350; FocusSpeedN[0].max = 1000; FocusSpeedN[0].value = 500; FocusSpeedN[0].step = 50; // Focuser temperature IUFillNumber(&TemperatureN[0], "TEMPERATURE", "Celsius", "%6.2f", -50, 70., 0., 0.); IUFillNumberVector(&TemperatureNP, TemperatureN, 1, getDeviceName(), "FOCUS_TEMPERATURE", "Temperature", MAIN_CONTROL_TAB, IP_RO, 0, IPS_IDLE); // Temperature Settings IUFillNumber(&TemperatureSettingN[FOCUS_T_COEFF], "Coefficient", "", "%.3f", 0, 0.999, 0.1, 0.1); IUFillNumber(&TemperatureSettingN[FOCUS_T_SAMPLES], "# of Samples", "", "%3.0f", 16, 128, 16, 16); IUFillNumberVector(&TemperatureSettingNP, TemperatureSettingN, 2, getDeviceName(), "Temperature Settings", "", FOCUS_SETTINGS_TAB, IP_RW, 0, IPS_IDLE); // Compensate for temperature IUFillSwitch(&TemperatureCompensateS[0], "Enable", "", ISS_OFF); IUFillSwitch(&TemperatureCompensateS[1], "Disable", "", ISS_ON); IUFillSwitchVector(&TemperatureCompensateSP, TemperatureCompensateS, 2, getDeviceName(), "Temperature Compensate", "", FOCUS_SETTINGS_TAB, IP_RW, ISR_1OFMANY, 0, IPS_IDLE); // Focuser Models IUFillSwitch(&ModelS[0], "NT2", "", ISS_OFF); fSettings[0].maxTrip = 30; fSettings[0].gearRatio = 0.25040; IUFillSwitch(&ModelS[1], "SC2", "", ISS_OFF); fSettings[1].maxTrip = 30; fSettings[1].gearRatio = 0.25040; IUFillSwitch(&ModelS[2], "RT2", "", ISS_OFF); fSettings[2].maxTrip = 80; fSettings[2].gearRatio = 0.25040; IUFillSwitch(&ModelS[3], "RT3", "", ISS_OFF); fSettings[3].maxTrip = 115; fSettings[3].gearRatio = 0.25040; IUFillSwitch(&ModelS[4], "Custom", "", ISS_ON); fSettings[4].maxTrip = 30; fSettings[4].gearRatio = 0.25040; IUFillSwitchVector(&ModelSP, ModelS, 5, getDeviceName(), "Model", "", FOCUS_SETTINGS_TAB, IP_RW, ISR_1OFMANY, 0, IPS_IDLE); // Custom Settings IUFillNumber(&CustomSettingN[FOCUS_MAX_TRIP], "Max Trip (mm)", "", "%6.2f", 20, 150, 0, 30); IUFillNumber(&CustomSettingN[FOCUS_GEAR_RATIO], "Gear Ratio", "", "%.5f", 0.1, 1, 0, .25040); IUFillNumberVector(&CustomSettingNP, CustomSettingN, 2, getDeviceName(), "Custom Settings", "", FOCUS_SETTINGS_TAB, IP_RW, 0, IPS_IDLE); // Focuser Accleration IUFillNumber(&AccelerationN[0], "Ramp", "", "%3.0f", 1500., 3000., 100., 2000.); IUFillNumberVector(&AccelerationNP, AccelerationN, 1, getDeviceName(), "FOCUS_ACCELERATION", "Acceleration", FOCUS_SETTINGS_TAB, IP_RW, 0, IPS_IDLE); // Focus Sync IUFillNumber(&SyncN[0], "Position (steps)", "", "%3.0f", 0., 200000., 100., 0.); IUFillNumberVector(&SyncNP, SyncN, 1, getDeviceName(), "FOCUS_SYNC", "Sync", MAIN_CONTROL_TAB, IP_RW, 0, IPS_IDLE); // Version IUFillText(&VersionT[0], "HW Rev.", "", nullptr); IUFillText(&VersionT[1], "FW Rev.", "", nullptr); IUFillTextVector(&VersionTP, VersionT, 2, getDeviceName(), "FOCUS_VERSION", "Version", MAIN_CONTROL_TAB, IP_RO, 0, IPS_IDLE); FocusRelPosN[0].value = 0; FocusAbsPosN[0].value = 0; simPosition = FocusAbsPosN[0].value; updateFocusMaxRange(fSettings[4].maxTrip, fSettings[4].gearRatio); addAuxControls(); setDefaultPollingPeriod(500); return true; } bool SteelDrive::updateProperties() { INDI::Focuser::updateProperties(); if (isConnected()) { defineNumber(&TemperatureNP); defineNumber(&TemperatureSettingNP); defineSwitch(&TemperatureCompensateSP); defineSwitch(&ModelSP); defineNumber(&CustomSettingNP); defineNumber(&AccelerationNP); defineNumber(&SyncNP); defineText(&VersionTP); GetFocusParams(); //loadConfig(true); LOG_INFO("SteelDrive paramaters updated, focuser ready for use."); } else { deleteProperty(TemperatureNP.name); deleteProperty(TemperatureSettingNP.name); deleteProperty(TemperatureCompensateSP.name); deleteProperty(ModelSP.name); deleteProperty(CustomSettingNP.name); deleteProperty(AccelerationNP.name); deleteProperty(SyncNP.name); deleteProperty(VersionTP.name); } return true; } bool SteelDrive::Handshake() { if (isSimulation()) return true; if (Ack()) { LOG_INFO("SteelDrive is online. Getting focus parameters..."); temperatureUpdateCounter = 0; return true; } LOG_INFO("Error retreiving data from SteelDrive, please ensure SteelDrive controller is " "powered and the port is correct."); return false; } const char *SteelDrive::getDefaultName() { return "Baader SteelDrive"; } /************************************************************************************ * * ***********************************************************************************/ bool SteelDrive::Ack() { int nbytes_written = 0, nbytes_read = 0, rc = -1; char errstr[MAXRBUF]; char resp[STEELDRIVE_MAXBUF]; char hwVer[STEELDRIVE_MAXBUF]; tcflush(PortFD, TCIOFLUSH); if (!sim && (rc = tty_write(PortFD, ":FVERSIO#", STEELDRIVE_CMD, &nbytes_written)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR(":FVERSIO# getHWVersion error: %s.", errstr); return false; } LOG_DEBUG("CMD (:FVERSIO#)"); if (sim) { strncpy(resp, ":FV2.00812#", STEELDRIVE_CMD_LONG); nbytes_read = STEELDRIVE_CMD_LONG; } else if ((rc = tty_read_section(PortFD, resp, '#', STEELDRIVE_TIMEOUT, &nbytes_read)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("getHWVersion error: %s.", errstr); return false; } resp[nbytes_read] = '\0'; LOGF_DEBUG("RES (%s)", resp); rc = sscanf(resp, ":FV%s#", hwVer); return rc > 0; } /************************************************************************************ * * ***********************************************************************************/ bool SteelDrive::updateVersion() { int nbytes_written = 0, nbytes_read = 0, rc = -1; char errstr[MAXRBUF]; char resp[STEELDRIVE_MAXBUF]; char hardware_string[MAXRBUF]; char firmware_string[MAXRBUF]; char hwdate[STEELDRIVE_MAXBUF / 2]; char hwrev[STEELDRIVE_MAXBUF / 2]; char fwdate[STEELDRIVE_MAXBUF / 2]; char fwrev[STEELDRIVE_MAXBUF / 2]; memset(hwdate, 0, sizeof(hwdate)); memset(hwrev, 0, sizeof(hwrev)); memset(fwdate, 0, sizeof(fwdate)); memset(fwrev, 0, sizeof(fwrev)); tcflush(PortFD, TCIOFLUSH); if (!sim && (rc = tty_write(PortFD, ":FVERSIO#", STEELDRIVE_CMD, &nbytes_written)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR(":FVERSIO# getHWVersion write error: %s.", errstr); return false; } LOG_DEBUG("CMD (:FVERSIO#)"); if (sim) { strncpy(resp, ":FV2.00812#", STEELDRIVE_CMD_LONG); nbytes_read = STEELDRIVE_CMD_LONG; } else if ((rc = tty_read_section(PortFD, resp, '#', STEELDRIVE_TIMEOUT, &nbytes_read)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("FVERSIO# getHWVersion read error: %s.", errstr); return false; } resp[nbytes_read] = '\0'; LOGF_DEBUG("RES (%s)", resp); rc = sscanf(resp, ":FV%s#", hardware_string); if (rc > 0) { strncpy(hwrev, hardware_string, 3); strncpy(hwdate, hardware_string + 3, 4); char mon[3], year[3]; memset(mon, 0, sizeof(mon)); memset(year, 0, sizeof(year)); strncpy(mon, hwdate, 2); strncpy(year, hwdate + 2, 2); snprintf(hardware_string, MAXRBUF, "Version: %s Date: %s.%s", hwrev, mon, year); IUSaveText(&VersionT[0], hardware_string); } else { LOGF_ERROR("Unknown error: getHWVersion value (%s)", resp); return false; } tcflush(PortFD, TCIOFLUSH); if (!sim && (rc = tty_write(PortFD, ":FNFIRMW#", STEELDRIVE_CMD, &nbytes_written)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR(":FNFIRMW# getSWVersion write error: %s.", errstr); return false; } LOG_DEBUG("CMD (:FNFIRMW#)"); if (sim) { strncpy(resp, ":FN2.21012#", STEELDRIVE_CMD_LONG); nbytes_read = STEELDRIVE_CMD_LONG; } else if ((rc = tty_read_section(PortFD, resp, '#', STEELDRIVE_TIMEOUT, &nbytes_read)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("FNFIRMW# getSWVersion read error: %s.", errstr); return false; } resp[nbytes_read] = '\0'; LOGF_DEBUG("RES (%s)", resp); rc = sscanf(resp, ":FN%s#", firmware_string); if (rc > 0) { strncpy(fwrev, firmware_string, 3); strncpy(fwdate, firmware_string + 3, 4); char mon[3], year[3]; memset(mon, 0, sizeof(mon)); memset(year, 0, sizeof(year)); strncpy(mon, fwdate, 2); strncpy(year, fwdate + 2, 2); snprintf(firmware_string, MAXRBUF, "Version: %s Date: %s.%s", fwrev, mon, year); IUSaveText(&VersionT[1], firmware_string); } else { LOGF_ERROR("Unknown error: getSWVersion value (%s)", resp); return false; } return true; } /************************************************************************************ * * ***********************************************************************************/ bool SteelDrive::updateTemperature() { int nbytes_written = 0, nbytes_read = 0, rc = -1; char errstr[MAXRBUF]; char resp[STEELDRIVE_MAXBUF]; int temperature; tcflush(PortFD, TCIOFLUSH); if (!sim && (rc = tty_write(PortFD, ":F5ASKT0#", STEELDRIVE_CMD, &nbytes_written)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR(":F5ASKT0# updateTemperature write error: %s.", errstr); return false; } LOG_DEBUG("CMD (:F5ASKT0#)"); if (sim) { strncpy(resp, ":F5+1810#", STEELDRIVE_CMD); nbytes_read = STEELDRIVE_CMD; } else if ((rc = tty_read_section(PortFD, resp, '#', STEELDRIVE_TIMEOUT, &nbytes_read)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR(":F5ASKT0# updateTemperature read error: %s.", errstr); return false; } resp[nbytes_read] = '\0'; LOGF_DEBUG("RES (%s)", resp); rc = sscanf(resp, ":F5%d#", &temperature); if (rc > 0) { TemperatureN[0].value = ((double)temperature) / 100.0; TemperatureNP.s = IPS_OK; } else { char junk[STEELDRIVE_MAXBUF]; rc = sscanf(resp, ":F5%s#", junk); if (rc > 0) { TemperatureN[0].value = 0; LOG_DEBUG("Temperature probe is not connected."); } else LOGF_ERROR("Unknown error: focuser temperature value (%s)", resp); TemperatureNP.s = IPS_ALERT; return false; } return true; } /************************************************************************************ * * ***********************************************************************************/ bool SteelDrive::updatePosition() { int nbytes_written = 0, nbytes_read = 0, rc = -1; char errstr[MAXRBUF]; char resp[STEELDRIVE_MAXBUF]; unsigned short pos = 0; int retries = 0; for (retries = 0; retries < STEELDRIVE_MAX_RETRIES; retries++) { tcflush(PortFD, TCIOFLUSH); if (!sim && (rc = tty_write(PortFD, ":F8ASKS0#", STEELDRIVE_CMD, &nbytes_written)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR(":F8ASKS0# updatePostion write error: %s.", errstr); return false; } LOG_DEBUG("CMD (:F8ASKS0#)"); if (sim) { snprintf(resp, STEELDRIVE_CMD_LONG, ":F8%07u#", (int)simPosition); nbytes_read = STEELDRIVE_CMD_LONG; break; } else if ((rc = tty_read_section(PortFD, resp, '#', STEELDRIVE_TIMEOUT - retries, &nbytes_read)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); resp[nbytes_read] = '\0'; LOGF_DEBUG(":F8ASKS0# updatePosition read error: %s. Retry: %d. Bytes: %d. Buffer (%s)", errstr, retries, nbytes_read, resp); } else break; } if (retries == STEELDRIVE_MAX_RETRIES) { LOG_ERROR("UpdatePosition: failed to read."); return false; } resp[nbytes_read] = '\0'; LOGF_DEBUG("RES (%s)", resp); rc = sscanf(resp, ":F8%hu#", &pos); if (rc > 0) { FocusAbsPosN[0].value = pos; } else { LOGF_ERROR("Unknown error: focuser position value (%s)", resp); return false; } return true; } /************************************************************************************ * * ***********************************************************************************/ bool SteelDrive::updateSpeed() { int nbytes_written = 0, nbytes_read = 0, rc = -1; char errstr[MAXRBUF]; char resp[STEELDRIVE_MAXBUF]; unsigned short speed; tcflush(PortFD, TCIOFLUSH); if (!sim && (rc = tty_write(PortFD, ":FGSPMAX#", STEELDRIVE_CMD, &nbytes_written)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR(":FGSPMAX# updateSpeed write error: %s.", errstr); return false; } LOG_DEBUG("CMD (:FGSPMAX#)"); if (sim) { strncpy(resp, ":FG00350#", STEELDRIVE_CMD); nbytes_read = STEELDRIVE_CMD; } else if ((rc = tty_read_section(PortFD, resp, '#', STEELDRIVE_TIMEOUT, &nbytes_read)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR(":FGSPMAX# updateSpeed read error: %s.", errstr); return false; } resp[nbytes_read] = '\0'; LOGF_DEBUG("RES (%s)", resp); rc = sscanf(resp, ":FG%hu#", &speed); if (rc > 0) { FocusSpeedN[0].value = speed; } else { LOGF_ERROR("Unknown error: focuser speed value (%s)", resp); return false; } return true; } /************************************************************************************ * * ***********************************************************************************/ bool SteelDrive::updateAcceleration() { int nbytes_written = 0, nbytes_read = 0, rc = -1; char errstr[MAXRBUF]; char resp[STEELDRIVE_MAXBUF]; unsigned short accel; tcflush(PortFD, TCIOFLUSH); if (!sim && (rc = tty_write(PortFD, ":FHSPMIN#", STEELDRIVE_CMD, &nbytes_written)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR(":FHSPMIN# updateAcceleration write error: %s.", errstr); return false; } LOG_DEBUG("CMD (:FHSPMIN#)"); if (sim) { strncpy(resp, ":FH01800#", STEELDRIVE_CMD); nbytes_read = STEELDRIVE_CMD; } else if ((rc = tty_read_section(PortFD, resp, '#', STEELDRIVE_TIMEOUT, &nbytes_read)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR(":FHSPMIN# updateAcceleration read error: %s.", errstr); return false; } resp[nbytes_read] = '\0'; LOGF_DEBUG("RES (%s)", resp); rc = sscanf(resp, ":FH%hu#", &accel); if (rc > 0) { AccelerationN[0].value = accel; } else { LOGF_ERROR("Unknown error: updateAcceleration value (%s)", resp); return false; } return true; } /************************************************************************************ * * ***********************************************************************************/ bool SteelDrive::updateTemperatureSettings() { int nbytes_written = 0, nbytes_read = 0, rc = -1; char errstr[MAXRBUF]; char resp[STEELDRIVE_MAXBUF]; char selectedFocuser[1], coeff[3], enabled[1], tResp[STEELDRIVE_MAXBUF]; tcflush(PortFD, TCIOFLUSH); if (!sim && (rc = tty_write(PortFD, ":F7ASKC0#", STEELDRIVE_CMD, &nbytes_written)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR(":F7ASKC0# updateTemperatureSettings write error: %s.", errstr); return false; } LOG_DEBUG("CMD (:F7ASKC0#)"); if (sim) { strncpy(resp, ":F710004#", STEELDRIVE_CMD); nbytes_read = STEELDRIVE_CMD; } else if ((rc = tty_read_section(PortFD, resp, '#', STEELDRIVE_TIMEOUT, &nbytes_read)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR(":F7ASKC0# updateTemperatureSettings read error: %s.", errstr); return false; } resp[nbytes_read] = '\0'; LOGF_DEBUG("RES (%s)", resp); rc = sscanf(resp, ":F7%s#", tResp); if (rc > 0) { strncpy(coeff, tResp, 3); strncpy(enabled, tResp + 3, 1); strncpy(selectedFocuser, tResp + 4, 1); TemperatureSettingN[FOCUS_T_COEFF].value = atof(coeff) / 1000.0; IUResetSwitch(&TemperatureCompensateSP); if (enabled[0] == '0') TemperatureCompensateS[1].s = ISS_ON; else TemperatureCompensateS[0].s = ISS_ON; } else { LOGF_ERROR("Unknown error: updateTemperatureSettings value (%s)", resp); return false; } return true; } /************************************************************************************ * * ***********************************************************************************/ bool SteelDrive::updateCustomSettings() { int nbytes_written = 0, nbytes_read = 0, rc = -1; char errstr[MAXRBUF]; char resp[STEELDRIVE_MAXBUF]; char selectedFocuser[2], maxTrip[8], tResp[STEELDRIVE_MAXBUF]; int gearR; double gearRatio; tcflush(PortFD, TCIOFLUSH); // Get Gear Ratio if (!sim && (rc = tty_write(PortFD, ":FEASKGR#", STEELDRIVE_CMD, &nbytes_written)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR(":FEASKGR# updateCustomSettings write error: %s.", errstr); return false; } LOG_DEBUG("CMD (:FEASKGR#)"); if (sim) { strncpy(resp, ":FE25040#", STEELDRIVE_CMD); nbytes_read = STEELDRIVE_CMD; } else if ((rc = tty_read_section(PortFD, resp, '#', STEELDRIVE_TIMEOUT, &nbytes_read)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR(":FEASKGR# updateCustomSettings read error: %s.", errstr); return false; } resp[nbytes_read] = '\0'; LOGF_DEBUG("RES (%s)", resp); rc = sscanf(resp, ":FE%d#", &gearR); if (rc > 0) { gearRatio = ((double)gearR) / 100000.0; } else { LOGF_ERROR("Unknown error: updateCustomSettings value (%s)", resp); return false; } tcflush(PortFD, TCIOFLUSH); // Get Max Trip if (!sim && (rc = tty_write(PortFD, ":F8ASKS1#", STEELDRIVE_CMD, &nbytes_written)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR(":F8ASKS1# updateCustomSettings write error: %s.", errstr); return false; } LOG_DEBUG("CMD (:F8ASKS1#)"); if (sim) { strncpy(resp, ":F40011577#", STEELDRIVE_CMD_LONG); nbytes_read = STEELDRIVE_CMD_LONG; } else if ((rc = tty_read_section(PortFD, resp, '#', STEELDRIVE_TIMEOUT, &nbytes_read)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR(":F8ASKS1# updateCustomSettings read error: %s.", errstr); return false; } resp[nbytes_read] = '\0'; LOGF_DEBUG("RES (%s)", resp); rc = sscanf(resp, ":F%s#", tResp); if (rc > 0) { strncpy(selectedFocuser, tResp, 1); strncpy(maxTrip, tResp + 1, 7); int sFocuser = atoi(selectedFocuser); IUResetSwitch(&ModelSP); ModelS[sFocuser].s = ISS_ON; fSettings[sFocuser].maxTrip = (atof(maxTrip) * gearRatio) / 100.0; fSettings[sFocuser].gearRatio = gearRatio; CustomSettingN[FOCUS_MAX_TRIP].value = fSettings[sFocuser].maxTrip; CustomSettingN[FOCUS_GEAR_RATIO].value = fSettings[sFocuser].gearRatio; LOGF_DEBUG("Updated max trip: %g gear ratio: %g", fSettings[sFocuser].maxTrip, fSettings[sFocuser].gearRatio); } else { LOGF_ERROR("Unknown error: updateCustomSettings value (%s)", resp); return false; } return true; } /************************************************************************************ * * ***********************************************************************************/ bool SteelDrive::setTemperatureSamples(unsigned int targetSamples, unsigned int *finalSample) { int nbytes_written = 0, rc = -1; char errstr[MAXRBUF]; char cmd[STEELDRIVE_MAXBUF]; int maxSample = TemperatureSettingN[FOCUS_T_SAMPLES].max; int sample = 0; for (int i = maxSample; i > 0;) { if (targetSamples & maxSample) { sample = maxSample; break; } maxSample >>= 1; } int value = 0; if (sample == 16) value = 5000; else if (sample == 32) value = 15000; else if (sample == 64) value = 25000; else value = 35000; snprintf(cmd, STEELDRIVE_CMD + 1, ":FI%05d#", value); tcflush(PortFD, TCIOFLUSH); LOGF_DEBUG("CMD (%s)", cmd); if (!sim && (rc = tty_write(PortFD, cmd, STEELDRIVE_CMD, &nbytes_written)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("%s setTemperatureSamples write error: %s.", cmd, errstr); return false; } TemperatureSettingN[FOCUS_T_SAMPLES].value = sample; *finalSample = sample; return true; } /************************************************************************************ * * ***********************************************************************************/ bool SteelDrive::setTemperatureCompensation() { int nbytes_written = 0, rc = -1; char errstr[MAXRBUF]; char cmd[STEELDRIVE_MAXBUF]; double coeff = TemperatureSettingN[FOCUS_T_COEFF].value; bool enable = TemperatureCompensateS[0].s == ISS_ON; int selectedFocus = IUFindOnSwitchIndex(&ModelSP); snprintf(cmd, STEELDRIVE_CMD + 1, ":F%02d%03d%d#", selectedFocus, (int)(coeff * 1000), enable ? 2 : 0); tcflush(PortFD, TCIOFLUSH); LOGF_DEBUG("CMD (%s)", cmd); if (!sim && (rc = tty_write(PortFD, cmd, STEELDRIVE_CMD, &nbytes_written)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("setTemperatureCoefficient error: %s.", errstr); return false; } return true; } /************************************************************************************ * * ***********************************************************************************/ bool SteelDrive::setCustomSettings(double maxTrip, double gearRatio) { int nbytes_written = 0, rc = -1; char errstr[MAXRBUF]; char cmd[STEELDRIVE_MAXBUF]; unsigned short mmTrip = (unsigned short int)(maxTrip / gearRatio * 100.0); snprintf(cmd, STEELDRIVE_CMD_LONG + 1, ":FC%07d#", mmTrip); tcflush(PortFD, TCIOFLUSH); LOGF_DEBUG("CMD (%s)", cmd); if (!sim && (rc = tty_write(PortFD, cmd, STEELDRIVE_CMD_LONG, &nbytes_written)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("setCustomSettings error: %s.", errstr); return false; } snprintf(cmd, STEELDRIVE_CMD + 1, ":FD%05d#", (int)(gearRatio * 100000)); tcflush(PortFD, TCIOFLUSH); LOGF_DEBUG("CMD (%s)", cmd); if (!sim && (rc = tty_write(PortFD, cmd, STEELDRIVE_CMD, &nbytes_written)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("setCustomSettings error: %s.", errstr); return false; } return true; } /************************************************************************************ * * ***********************************************************************************/ bool SteelDrive::Sync(unsigned int position) { int nbytes_written = 0, rc = -1; char errstr[MAXRBUF]; char cmd[STEELDRIVE_MAXBUF]; snprintf(cmd, STEELDRIVE_CMD_LONG + 1, ":FB%07d#", position); tcflush(PortFD, TCIOFLUSH); LOGF_DEBUG("CMD (%s)", cmd); if (!sim && (rc = tty_write(PortFD, cmd, STEELDRIVE_CMD_LONG, &nbytes_written)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("Sync error: %s.", errstr); return false; } simPosition = position; return true; } /************************************************************************************ * * ***********************************************************************************/ bool SteelDrive::moveFocuser(unsigned int position) { int nbytes_written = 0, rc = -1; char errstr[MAXRBUF]; char cmd[STEELDRIVE_MAXBUF]; if (position < FocusAbsPosN[0].min || position > FocusAbsPosN[0].max) { LOGF_ERROR("Requested position value out of bound: %d", position); return false; } if (FocusAbsPosNP.s == IPS_BUSY) AbortFocuser(); snprintf(cmd, STEELDRIVE_CMD_LONG + 1, ":F9%07d#", position); tcflush(PortFD, TCIOFLUSH); LOGF_DEBUG("CMD (%s)", cmd); // Goto absolute step if (!sim && (rc = tty_write(PortFD, cmd, STEELDRIVE_CMD_LONG, &nbytes_written)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("setPosition error: %s.", errstr); return false; } targetPos = position; return true; } /************************************************************************************ * * ***********************************************************************************/ bool SteelDrive::startMotion(FocusDirection dir) { int nbytes_written = 0, rc = -1; char errstr[MAXRBUF]; char cmd[STEELDRIVE_MAXBUF]; // inward --> decreasing value --> DOWN // outward --> increasing value --> UP strncpy(cmd, (dir == FOCUS_INWARD) ? ":F2MDOW0#" : ":F1MUP00#", STEELDRIVE_CMD); tcflush(PortFD, TCIOFLUSH); LOGF_DEBUG("CMD (%s)", cmd); if (!sim && (rc = tty_write(PortFD, cmd, STEELDRIVE_CMD, &nbytes_written)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("StartMotion error: %s.", errstr); return false; } return true; } /************************************************************************************ * * ***********************************************************************************/ bool SteelDrive::setSpeed(unsigned short speed) { int nbytes_written = 0, rc = -1; char errstr[MAXRBUF]; char cmd[STEELDRIVE_MAXBUF]; snprintf(cmd, STEELDRIVE_CMD + 1, ":Fg%05d#", speed); tcflush(PortFD, TCIOFLUSH); LOGF_DEBUG("CMD (%s)", cmd); if (!sim && (rc = tty_write(PortFD, cmd, STEELDRIVE_CMD, &nbytes_written)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("setSpeed error: %s.", errstr); return false; } currentSpeed = speed; return true; } /************************************************************************************ * * ***********************************************************************************/ bool SteelDrive::setAcceleration(unsigned short accel) { int nbytes_written = 0, rc = -1; char errstr[MAXRBUF]; char cmd[STEELDRIVE_MAXBUF]; snprintf(cmd, STEELDRIVE_CMD + 1, ":Fh%05d#", accel); tcflush(PortFD, TCIOFLUSH); LOGF_DEBUG("CMD (%s)", cmd); if (!sim && (rc = tty_write(PortFD, cmd, STEELDRIVE_CMD, &nbytes_written)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("setAcceleration error: %s.", errstr); return false; } return true; } /************************************************************************************ * * ***********************************************************************************/ bool SteelDrive::ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) { if (dev != nullptr && strcmp(dev, getDeviceName()) == 0) { if (strcmp(TemperatureCompensateSP.name, name) == 0) { int last_index = IUFindOnSwitchIndex(&TemperatureCompensateSP); IUUpdateSwitch(&TemperatureCompensateSP, states, names, n); bool rc = setTemperatureCompensation(); if (!rc) { TemperatureCompensateSP.s = IPS_ALERT; IUResetSwitch(&TemperatureCompensateSP); TemperatureCompensateS[last_index].s = ISS_ON; IDSetSwitch(&TemperatureCompensateSP, nullptr); return false; } TemperatureCompensateSP.s = IPS_OK; IDSetSwitch(&TemperatureCompensateSP, nullptr); return true; } if (strcmp(ModelSP.name, name) == 0) { IUUpdateSwitch(&ModelSP, states, names, n); int i = IUFindOnSwitchIndex(&ModelSP); double focusMaxPos = floor(fSettings[i].maxTrip / fSettings[i].gearRatio) * 100; FocusAbsPosN[0].max = focusMaxPos; IUUpdateMinMax(&FocusAbsPosNP); CustomSettingN[FOCUS_MAX_TRIP].value = fSettings[i].maxTrip; CustomSettingN[FOCUS_GEAR_RATIO].value = fSettings[i].gearRatio; IDSetNumber(&CustomSettingNP, nullptr); ModelSP.s = IPS_OK; IDSetSwitch(&ModelSP, nullptr); return true; } } return INDI::Focuser::ISNewSwitch(dev, name, states, names, n); } /************************************************************************************ * * ***********************************************************************************/ bool SteelDrive::ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) { if (dev != nullptr && strcmp(dev, getDeviceName()) == 0) { // Set Accelration if (strcmp(name, AccelerationNP.name) == 0) { if (setAcceleration((int)values[0])) { IUUpdateNumber(&AccelerationNP, values, names, n); AccelerationNP.s = IPS_OK; IDSetNumber(&AccelerationNP, nullptr); return true; } else { AccelerationNP.s = IPS_ALERT; IDSetNumber(&AccelerationNP, nullptr); return false; } } // Set Temperature Settings if (strcmp(name, TemperatureSettingNP.name) == 0) { // Coeff is only needed when we enable or disable the temperature compensation. Here we only set the # of samples unsigned int targetSamples; if (strcmp(names[0], TemperatureSettingN[FOCUS_T_SAMPLES].name) == 0) targetSamples = (int)values[0]; else targetSamples = (int)values[1]; unsigned int finalSample = targetSamples; if (setTemperatureSamples(targetSamples, &finalSample)) { IUUpdateNumber(&TemperatureSettingNP, values, names, n); TemperatureSettingN[FOCUS_T_SAMPLES].value = finalSample; if (TemperatureSettingN[FOCUS_T_COEFF].value > TemperatureSettingN[FOCUS_T_COEFF].max) TemperatureSettingN[FOCUS_T_COEFF].value = TemperatureSettingN[FOCUS_T_COEFF].max; TemperatureSettingNP.s = IPS_OK; IDSetNumber(&TemperatureSettingNP, nullptr); return true; } else { TemperatureSettingNP.s = IPS_ALERT; IDSetNumber(&TemperatureSettingNP, nullptr); return true; } } // Set Custom Settings if (strcmp(name, CustomSettingNP.name) == 0) { int i = IUFindOnSwitchIndex(&ModelSP); // If the model is NOT set to custom, then the values cannot be updated if (i != 4) { CustomSettingNP.s = IPS_IDLE; LOG_WARN("You can not set custom values for a non-custom focuser."); IDSetNumber(&CustomSettingNP, nullptr); return false; } double maxTrip, gearRatio; if (strcmp(names[0], CustomSettingN[FOCUS_MAX_TRIP].name) == 0) { maxTrip = values[0]; gearRatio = values[1]; } else { maxTrip = values[1]; gearRatio = values[0]; } if (setCustomSettings(maxTrip, gearRatio)) { IUUpdateNumber(&CustomSettingNP, values, names, n); CustomSettingNP.s = IPS_OK; IDSetNumber(&CustomSettingNP, nullptr); updateFocusMaxRange(maxTrip, gearRatio); IUUpdateMinMax(&FocusAbsPosNP); IUUpdateMinMax(&FocusRelPosNP); } else { CustomSettingNP.s = IPS_ALERT; IDSetNumber(&CustomSettingNP, nullptr); } } // Set Sync Position if (strcmp(name, SyncNP.name) == 0) { if (Sync((unsigned int)values[0])) { IUUpdateNumber(&SyncNP, values, names, n); SyncNP.s = IPS_OK; IDSetNumber(&SyncNP, nullptr); if (updatePosition()) IDSetNumber(&FocusAbsPosNP, nullptr); return true; } else { SyncNP.s = IPS_ALERT; IDSetNumber(&SyncNP, nullptr); return false; } } } return INDI::Focuser::ISNewNumber(dev, name, values, names, n); } /************************************************************************************ * * ***********************************************************************************/ void SteelDrive::GetFocusParams() { if (updateVersion()) IDSetText(&VersionTP, nullptr); if (updateTemperature()) IDSetNumber(&TemperatureNP, nullptr); if (updateTemperatureSettings()) IDSetNumber(&TemperatureSettingNP, nullptr); if (updatePosition()) IDSetNumber(&FocusAbsPosNP, nullptr); if (updateSpeed()) IDSetNumber(&FocusSpeedNP, nullptr); if (updateAcceleration()) IDSetNumber(&AccelerationNP, nullptr); if (updateCustomSettings()) { IDSetNumber(&CustomSettingNP, nullptr); IDSetSwitch(&ModelSP, nullptr); } } bool SteelDrive::SetFocuserSpeed(int speed) { bool rc = false; rc = setSpeed(speed); if (!rc) return false; currentSpeed = speed; FocusSpeedNP.s = IPS_OK; IDSetNumber(&FocusSpeedNP, nullptr); return true; } IPState SteelDrive::MoveFocuser(FocusDirection dir, int speed, uint16_t duration) { if (speed != (int)currentSpeed) { bool rc = setSpeed(speed); if (!rc) return IPS_ALERT; } gettimeofday(&focusMoveStart, nullptr); focusMoveRequest = duration / 1000.0; startMotion(dir); if (duration <= POLLMS) { usleep(POLLMS * 1000); AbortFocuser(); return IPS_OK; } return IPS_BUSY; } IPState SteelDrive::MoveAbsFocuser(uint32_t targetTicks) { targetPos = targetTicks; bool rc = false; rc = moveFocuser(targetPos); if (!rc) return IPS_ALERT; FocusAbsPosNP.s = IPS_BUSY; return IPS_BUSY; } IPState SteelDrive::MoveRelFocuser(FocusDirection dir, uint32_t ticks) { double newPosition = 0; bool rc = false; if (dir == FOCUS_INWARD) newPosition = FocusAbsPosN[0].value - ticks; else newPosition = FocusAbsPosN[0].value + ticks; rc = moveFocuser(newPosition); if (!rc) return IPS_ALERT; FocusRelPosN[0].value = ticks; FocusRelPosNP.s = IPS_BUSY; FocusAbsPosNP.s = IPS_BUSY; return IPS_BUSY; } void SteelDrive::TimerHit() { if (!isConnected()) return; bool rc = updatePosition(); if (rc) { if (fabs(lastPos - FocusAbsPosN[0].value) > STEELDIVE_POSITION_THRESHOLD) { IDSetNumber(&FocusAbsPosNP, nullptr); lastPos = FocusAbsPosN[0].value; } } if (temperatureUpdateCounter++ > STEELDRIVE_TEMPERATURE_FREQ) { temperatureUpdateCounter = 0; rc = updateTemperature(); if (rc) { if (fabs(lastTemperature - TemperatureN[0].value) >= 0.5) lastTemperature = TemperatureN[0].value; } IDSetNumber(&TemperatureNP, nullptr); } if (FocusTimerNP.s == IPS_BUSY) { float remaining = CalcTimeLeft(focusMoveStart, focusMoveRequest); if (sim) { if (FocusMotionS[FOCUS_INWARD].s == ISS_ON) { FocusAbsPosN[0].value -= FocusSpeedN[0].value; if (FocusAbsPosN[0].value <= 0) FocusAbsPosN[0].value = 0; simPosition = FocusAbsPosN[0].value; } else { FocusAbsPosN[0].value += FocusSpeedN[0].value; if (FocusAbsPosN[0].value >= FocusAbsPosN[0].max) FocusAbsPosN[0].value = FocusAbsPosN[0].max; simPosition = FocusAbsPosN[0].value; } } // If we exceed focus abs values, stop timer and motion if (FocusAbsPosN[0].value <= 0 || FocusAbsPosN[0].value >= FocusAbsPosN[0].max) { AbortFocuser(); if (FocusAbsPosN[0].value <= 0) FocusAbsPosN[0].value = 0; else FocusAbsPosN[0].value = FocusAbsPosN[0].max; FocusTimerN[0].value = 0; FocusTimerNP.s = IPS_IDLE; } else if (remaining <= 0) { FocusTimerNP.s = IPS_OK; FocusTimerN[0].value = 0; AbortFocuser(); } else FocusTimerN[0].value = remaining * 1000.0; IDSetNumber(&FocusTimerNP, nullptr); } if (FocusAbsPosNP.s == IPS_BUSY || FocusRelPosNP.s == IPS_BUSY) { if (sim) { if (FocusAbsPosN[0].value < targetPos) simPosition += 100; else simPosition -= 100; if (fabs(simPosition - targetPos) < 100) { FocusAbsPosN[0].value = targetPos; simPosition = FocusAbsPosN[0].value; } } // Set it OK to within 5 steps if (fabs(targetPos - FocusAbsPosN[0].value) < 5) { FocusAbsPosNP.s = IPS_OK; FocusRelPosNP.s = IPS_OK; IDSetNumber(&FocusAbsPosNP, nullptr); IDSetNumber(&FocusRelPosNP, nullptr); lastPos = FocusAbsPosN[0].value; LOG_INFO("Focuser reached requested position."); } } SetTimer(POLLMS); } /************************************************************************************ * * ***********************************************************************************/ bool SteelDrive::AbortFocuser() { int nbytes_written = 0, rc = -1; char errstr[MAXRBUF]; tcflush(PortFD, TCIOFLUSH); LOG_DEBUG("CMD :F3STOP0#"); if (!sim && (rc = tty_write(PortFD, ":F3STOP0#", STEELDRIVE_CMD, &nbytes_written)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR(":F3STOP0# Stop error: %s.", errstr); return false; } if (FocusRelPosNP.s == IPS_BUSY) { FocusRelPosNP.s = IPS_IDLE; IDSetNumber(&FocusRelPosNP, nullptr); } FocusTimerNP.s = FocusAbsPosNP.s = IPS_IDLE; IDSetNumber(&FocusTimerNP, nullptr); IDSetNumber(&FocusAbsPosNP, nullptr); return true; } /************************************************************************************ * * ***********************************************************************************/ float SteelDrive::CalcTimeLeft(timeval start, float req) { double timesince; double timeleft; struct timeval now { 0, 0 }; gettimeofday(&now, nullptr); timesince = (double)(now.tv_sec * 1000.0 + now.tv_usec / 1000) - (double)(start.tv_sec * 1000.0 + start.tv_usec / 1000); timesince = timesince / 1000; timeleft = req - timesince; return timeleft; } /************************************************************************************ * * ***********************************************************************************/ bool SteelDrive::saveConfigItems(FILE *fp) { INDI::Focuser::saveConfigItems(fp); IUSaveConfigNumber(fp, &TemperatureSettingNP); IUSaveConfigSwitch(fp, &TemperatureCompensateSP); IUSaveConfigNumber(fp, &FocusSpeedNP); IUSaveConfigNumber(fp, &AccelerationNP); IUSaveConfigNumber(fp, &CustomSettingNP); IUSaveConfigSwitch(fp, &ModelSP); return saveFocuserConfig(); } /************************************************************************************ * * ***********************************************************************************/ bool SteelDrive::saveFocuserConfig() { int nbytes_written = 0, rc = -1; char errstr[MAXRBUF]; tcflush(PortFD, TCIOFLUSH); LOG_DEBUG("CMD (:FFPOWER#)"); if (!sim && (rc = tty_write(PortFD, ":FFPOWER#", STEELDRIVE_CMD, &nbytes_written)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR(":FFPOWER# saveFocuserConfig error: %s.", errstr); return false; } return true; } /************************************************************************************ * * ***********************************************************************************/ void SteelDrive::debugTriggered(bool enable) { tty_set_debug(enable ? 1 : 0); } /************************************************************************************ * * ***********************************************************************************/ void SteelDrive::updateFocusMaxRange(double maxTrip, double gearRatio) { double maxSteps = floor(maxTrip / gearRatio * 100.0); /* Relative and absolute movement */ FocusRelPosN[0].min = 0; FocusRelPosN[0].max = floor(maxSteps / 2.0); FocusRelPosN[0].step = 100; FocusAbsPosN[0].min = 0; FocusAbsPosN[0].max = maxSteps; FocusAbsPosN[0].step = 1000; } libindi/drivers/focuser/nstep.h0000664000175000017500000000433513263645557016125 0ustar jasemjasem/* NStep Focuser Copyright (c) 2016 Cloudmakers, s. r. o. All Rights Reserved. Thanks to Rigel Systems, especially Gene Nolan and Leon Palmer, for their support in writing this driver. 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 */ #pragma once #include "indifocuser.h" class NSTEP : public INDI::Focuser { public: NSTEP(); ~NSTEP(); virtual bool Handshake(); const char *getDefaultName(); bool initProperties(); bool updateProperties(); bool ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n); bool ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n); void TimerHit(); IPState MoveRelFocuser(FocusDirection dir, unsigned int ticks); IPState MoveAbsFocuser(uint32_t targetTicks); bool AbortFocuser(); bool SetFocuserSpeed(int speed); bool saveConfigItems(FILE *fp); private: bool command(const char *request, char *response, int count); IPState moveFocuserRelative(FocusDirection dir, unsigned int ticks); char buf[MAXRBUF]; long sim_position { 0 }; long position { 0 }; int temperature { 0 }; char steppingMode { 0 }; char steppingPhase { 0 }; pthread_mutex_t lock; INumber TempN[1]; INumberVectorProperty TempNP; ISwitch TempCompS[2]; ISwitchVectorProperty TempCompSP; INumber TempCompN[2]; INumberVectorProperty TempCompNP; ISwitch SteppingModeS[3]; ISwitchVectorProperty SteppingModeSP; ISwitch SteppingPhaseS[3]; ISwitchVectorProperty SteppingPhaseSP; }; libindi/drivers/focuser/hitecastrodcfocuser.cpp0000664000175000017500000002653613263645557021401 0ustar jasemjasem/******************************************************************************* Copyright(c) 2016 Andy Kirkham. All rights reserved. HitecAstroDCFocuser Focuser This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2 as published by the Free Software Foundation. . 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 Library General Public License for more details. . You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. *******************************************************************************/ #include "hitecastrodcfocuser.h" #include #include #define HID_TIMEOUT 10000 /* 10s */ #define FUDGE_FACTOR_H 1000 #define FUDGE_FACTOR_L 885 #define FOCUS_SETTINGS_TAB "Settings" std::unique_ptr hitecastroDcFocuser(new HitecAstroDCFocuser()); void ISPoll(void *p); void ISGetProperties(const char *dev) { hitecastroDcFocuser->ISGetProperties(dev); } void ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) { hitecastroDcFocuser->ISNewSwitch(dev, name, states, names, n); } void ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n) { hitecastroDcFocuser->ISNewText(dev, name, texts, names, n); } void ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) { hitecastroDcFocuser->ISNewNumber(dev, name, values, names, n); } void ISNewBLOB(const char *dev, const char *name, int sizes[], int blobsizes[], char *blobs[], char *formats[], char *names[], int n) { INDI_UNUSED(dev); INDI_UNUSED(name); INDI_UNUSED(sizes); INDI_UNUSED(blobsizes); INDI_UNUSED(blobs); INDI_UNUSED(formats); INDI_UNUSED(names); INDI_UNUSED(n); } void ISSnoopDevice(XMLEle *root) { hitecastroDcFocuser->ISSnoopDevice(root); } HitecAstroDCFocuser::HitecAstroDCFocuser() : _handle(nullptr) { FI::SetCapability(FOCUSER_CAN_REL_MOVE); // | FOCUSER_HAS_VARIABLE_SPEED); setConnection(CONNECTION_NONE); } HitecAstroDCFocuser::~HitecAstroDCFocuser() { if (_handle != nullptr) { hid_close(_handle); _handle = nullptr; } } bool HitecAstroDCFocuser::Connect() { sim = isSimulation(); if (sim) { SetTimer(POLLMS); return true; } if (hid_init() != 0) { LOG_ERROR("hid_init() failed."); } _handle = hid_open(0x04D8, 0xFAC2, nullptr); if (_handle == nullptr) _handle = hid_open(0x04D8, 0xF53A, nullptr); LOG_DEBUG(_handle ? "HitecAstroDCFocuser opened." : "HitecAstroDCFocuser failed."); if (_handle != nullptr) { DEBUG(INDI::Logger::DBG_SESSION, "Experimental driver. Report issues to https://github.com/A-j-K/hitecastrodcfocuser/issues"); SetTimer(POLLMS); return true; } LOGF_ERROR("Failed to connect to focuser: %s", hid_error(_handle)); return false; } bool HitecAstroDCFocuser::Disconnect() { if (!sim && _handle != nullptr) { hid_close(_handle); _handle = nullptr; } LOG_DEBUG("HitecAstroDCFocuser closed."); return true; } const char *HitecAstroDCFocuser::getDefaultName() { return (const char *)"HitecAstro DC"; } bool HitecAstroDCFocuser::initProperties() { INDI::Focuser::initProperties(); //IDMessage(getDeviceName(), "HitecAstroDCFocuser::initProperties()"); addDebugControl(); addSimulationControl(); IUFillNumber(&MaxPositionN[0], "Steps", "", "%.f", 0, 500000, 0., 10000); IUFillNumberVector(&MaxPositionNP, MaxPositionN, 1, getDeviceName(), "MAX_POSITION", "Max position", FOCUS_SETTINGS_TAB, IP_RW, 0, IPS_IDLE); IUFillNumber(&SlewSpeedN[0], "Steps/sec", "", "%.f", 1, 100, 0., 50); IUFillNumberVector(&SlewSpeedNP, SlewSpeedN, 1, getDeviceName(), "SLEW_SPEED", "Slew speed", MAIN_CONTROL_TAB, IP_RW, 0, IPS_IDLE); IUFillSwitch(&ReverseDirectionS[0], "ENABLED", "Reverse direction", ISS_OFF); IUFillSwitchVector(&ReverseDirectionSP, ReverseDirectionS, 1, getDeviceName(), "REVERSE_DIRECTION", "Reverse direction", OPTIONS_TAB, IP_RW, ISR_NOFMANY, 0, IPS_IDLE); FocusSpeedN[0].value = 100.; FocusSpeedN[0].min = 1.; FocusSpeedN[0].max = 100.; FocusSpeedN[0].value = 100.; FocusAbsPosN[0].min = 0; FocusAbsPosN[0].max = MaxPositionN[0].value; FocusAbsPosN[0].step = MaxPositionN[0].value / 50.0; FocusAbsPosN[0].value = 0; FocusRelPosN[0].min = 1; FocusRelPosN[0].max = (FocusAbsPosN[0].max - FocusAbsPosN[0].min) / 2; FocusRelPosN[0].step = FocusRelPosN[0].max / 100.0; FocusRelPosN[0].value = 100; setDefaultPollingPeriod(500); return true; } bool HitecAstroDCFocuser::updateProperties() { bool f = INDI::Focuser::updateProperties(); if (isConnected()) { //defineNumber(&MaxPositionNP); defineNumber(&SlewSpeedNP); defineSwitch(&ReverseDirectionSP); } else { //deleteProperty(MaxPositionNP.name); deleteProperty(SlewSpeedNP.name); deleteProperty(ReverseDirectionSP.name); } return f; } void HitecAstroDCFocuser::TimerHit() { if (_state == SLEWING && _duration > 0) { --_duration; if (_duration == 0) { int rc; unsigned char command[8]={0}; _state = IDLE; memset(command, 0, 8); command[0] = _stop; rc = hid_write(_handle, command, 8); if (rc < 0) { LOGF_DEBUG("::MoveFocuser() fail (%s)", hid_error(_handle)); } hid_read_timeout(_handle, command, 8, 1000); } } SetTimer(1); } bool HitecAstroDCFocuser::ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) { if (dev != nullptr && strcmp(dev, getDeviceName()) == 0) { if (strcmp(name, ReverseDirectionSP.name) == 0) { IUUpdateSwitch(&ReverseDirectionSP, states, names, n); ReverseDirectionSP.s = IPS_OK; IDSetSwitch(&ReverseDirectionSP, nullptr); return true; } } return INDI::Focuser::ISNewSwitch(dev, name, states, names, n); } bool HitecAstroDCFocuser::ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) { if (dev != nullptr && strcmp(dev, getDeviceName()) == 0) { if (strcmp(name, MaxPositionNP.name) == 0) { IUUpdateNumber(&MaxPositionNP, values, names, n); MaxPositionNP.s = IPS_OK; IDSetNumber(&MaxPositionNP, nullptr); return true; } if (strcmp(name, SlewSpeedNP.name) == 0) { if (values[0] > 100) { SlewSpeedNP.s = IPS_ALERT; return false; } IUUpdateNumber(&SlewSpeedNP, values, names, n); SlewSpeedNP.s = IPS_OK; IDSetNumber(&SlewSpeedNP, nullptr); return true; } } return INDI::Focuser::ISNewNumber(dev, name, values, names, n); } IPState HitecAstroDCFocuser::MoveRelFocuser(FocusDirection dir, uint32_t ticks) { int rc, speed = (int)SlewSpeedN[0].value; //_slew_speed; // int32_t iticks = ticks; unsigned char command[8]={0}; IPState rval; LOGF_DEBUG("::move() begin %d ticks at speed %d", ticks, speed); if (_handle == nullptr) { LOG_DEBUG("::move() bad handle"); return IPS_ALERT; } FocusRelPosNP.s = IPS_BUSY; IDSetNumber(&FocusRelPosNP, nullptr); // JM 2017-03-16: iticks is not used, FIXME. // if (dir == FOCUS_INWARD) // { // iticks = ticks * -1; // } if (ReverseDirectionS[0].s == ISS_ON) { dir = dir == FOCUS_INWARD ? FOCUS_OUTWARD : FOCUS_INWARD; } if (speed > 100) { LOGF_DEBUG("::move() over speed %d, limiting to 100", ticks, speed); speed = 100; } ticks *= FUDGE_FACTOR_H; ticks /= FUDGE_FACTOR_L; memset(command, 0, 8); command[0] = dir == FOCUS_INWARD ? 0x50 : 0x52; command[1] = (unsigned char)((ticks >> 8) & 0xFF); command[2] = (unsigned char)(ticks & 0xFF); command[3] = 0x05; command[4] = (unsigned char)(speed & 0xFF); command[5] = 0; command[6] = 0; command[7] = 0; LOGF_DEBUG("==> TX %2.2x %2.2x%2.2x %2.2x %2.2x %2.2x%2.2x%2.2x", command[0], command[1], command[2], command[3], command[4], command[5], command[6], command[7]); rc = hid_write(_handle, command, 8); if (rc < 0) { LOGF_DEBUG("::MoveRelFocuser() fail (%s)", hid_error(_handle)); return IPS_ALERT; } FocusRelPosNP.s = IPS_BUSY; memset(command, 0, 8); hid_read_timeout(_handle, command, 8, HID_TIMEOUT); LOGF_DEBUG("==> RX %2.2x %2.2x%2.2x %2.2x %2.2x %2.2x%2.2x%2.2x", command[0], command[1], command[2], command[3], command[4], command[5], command[6], command[7]); rval = command[1] == 0x21 ? IPS_OK : IPS_ALERT; FocusRelPosNP.s = rval; IDSetNumber(&FocusRelPosNP, nullptr); return rval; } IPState HitecAstroDCFocuser::MoveFocuser(FocusDirection dir, int speed, uint16_t duration) { int rc; unsigned char command[8]={0}; IPState rval; LOGF_DEBUG("::MoveFocuser(%d %d %d)", dir, speed, duration); if (_handle == nullptr) { return IPS_ALERT; } FocusSpeedNP.s = IPS_BUSY; IDSetNumber(&FocusSpeedNP, nullptr); if (ReverseDirectionS[0].s == ISS_ON) { dir = dir == FOCUS_INWARD ? FOCUS_OUTWARD : FOCUS_INWARD; } if (speed > 100) { LOGF_DEBUG("::MoveFocuser() over speed %d, limiting to 100", speed); speed = 100; } _stop = dir == FOCUS_INWARD ? 0xB0 : 0xBA; memset(command, 0, 8); command[0] = dir == FOCUS_INWARD ? 0x54 : 0x56; command[1] = (unsigned char)((speed >> 8) & 0xFF); command[2] = (unsigned char)(speed & 0xFF); command[3] = 0x05; command[4] = 0; command[5] = 0; command[6] = 0; command[7] = 0; LOGF_DEBUG("==> TX %2.2x %2.2x%2.2x %2.2x %2.2x %2.2x%2.2x%2.2x", command[0], command[1], command[2], command[3], command[4], command[5], command[6], command[7]); rc = hid_write(_handle, command, 8); if (rc < 0) { LOGF_DEBUG("::MoveFocuser() fail (%s)", hid_error(_handle)); return IPS_ALERT; } memset(command, 0, 8); hid_read_timeout(_handle, command, 8, HID_TIMEOUT); LOGF_DEBUG("==> RX %2.2x %2.2x%2.2x %2.2x %2.2x %2.2x%2.2x%2.2x", command[0], command[1], command[2], command[3], command[4], command[5], command[6], command[7]); rval = command[1] == 0x24 ? IPS_OK : IPS_ALERT; FocusSpeedNP.s = rval; IDSetNumber(&FocusSpeedNP, nullptr); _duration = duration; _state = SLEWING; return IPS_BUSY; } bool HitecAstroDCFocuser::saveConfigItems(FILE *fp) { INDI::Focuser::saveConfigItems(fp); IUSaveConfigNumber(fp, &MaxPositionNP); IUSaveConfigNumber(fp, &SlewSpeedNP); IUSaveConfigSwitch(fp, &ReverseDirectionSP); return true; } libindi/drivers/focuser/tcfs.cpp0000664000175000017500000005112613263645557016266 0ustar jasemjasem/* INDI Driver for Optec TCF-S Focuser Copyright (C) 2010 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 */ #include "tcfs.h" #include "indicom.h" #include "connectionplugins/connectionserial.h" #include #include #include #include #define mydev "Optec TCF-S" #define currentPosition FocusAbsPosN[0].value // We declare an auto pointer to TCFS. std::unique_ptr tcfs(new TCFS()); void ISPoll(void *p); void ISGetProperties(const char *dev) { tcfs->ISGetProperties(dev); } void ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) { tcfs->ISNewSwitch(dev, name, states, names, n); } void ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n) { tcfs->ISNewText(dev, name, texts, names, n); } void ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) { tcfs->ISNewNumber(dev, name, values, names, n); } void ISNewBLOB(const char *dev, const char *name, int sizes[], int blobsizes[], char *blobs[], char *formats[], char *names[], int n) { INDI_UNUSED(dev); INDI_UNUSED(name); INDI_UNUSED(sizes); INDI_UNUSED(blobsizes); INDI_UNUSED(blobs); INDI_UNUSED(formats); INDI_UNUSED(names); INDI_UNUSED(n); } void ISSnoopDevice(XMLEle *root) { tcfs->ISSnoopDevice(root); } /**************************************************************** ** ** *****************************************************************/ TCFS::TCFS() { FI::SetCapability(FOCUSER_CAN_ABS_MOVE | FOCUSER_CAN_REL_MOVE); } /**************************************************************** ** ** *****************************************************************/ bool TCFS::initProperties() { INDI::Focuser::initProperties(); // Set upper limit for TCF-S3 focuser if (strcmp(me, "indi_tcfs3_focus") == 0) { isTCFS3 = true; FocusAbsPosN[0].max = 9999; FocusRelPosN[0].max = 2000; FocusRelPosN[0].step = FocusAbsPosN[0].step = 100; FocusRelPosN[0].value = 0; LOG_DEBUG("TCF-S3 detected. Updating maximum position value to 9999."); } else { isTCFS3 = false; FocusAbsPosN[0].max = 7000; FocusRelPosN[0].max = 2000; FocusRelPosN[0].step = FocusAbsPosN[0].step = 100; FocusRelPosN[0].value = 0; LOG_DEBUG("TCF-S detected. Updating maximum position value to 7000."); } setDynamicPropertiesBehavior(false, false); buildSkeleton("indi_tcfs_sk.xml"); FocusTemperatureNP = getNumber("FOCUS_TEMPERATURE"); FocusPowerSP = getSwitch("FOCUS_POWER"); FocusModeSP = getSwitch("FOCUS_MODE"); FocusGotoSP = getSwitch("FOCUS_GOTO"); // Default to 19200 serialConnection->setDefaultBaudRate(Connection::Serial::B_19200); addAuxControls(); setDefaultPollingPeriod(500); return true; } /**************************************************************** ** ** *****************************************************************/ bool TCFS::updateProperties() { INDI::Focuser::updateProperties(); if (isConnected()) { defineSwitch(FocusGotoSP); defineNumber(FocusTemperatureNP); defineSwitch(FocusPowerSP); defineSwitch(FocusModeSP); } else { deleteProperty(FocusGotoSP->name); deleteProperty(FocusTemperatureNP->name); deleteProperty(FocusPowerSP->name); deleteProperty(FocusModeSP->name); } return true; } /**************************************************************** ** ** *****************************************************************/ bool TCFS::Handshake() { if (isSimulation()) { LOG_INFO("TCF-S: Simulating connection."); currentPosition = simulated_position; return true; } char response[TCFS_MAX_CMD] = { 0 }; dispatch_command(FWAKUP); read_tcfs(response); for(int retry=0; retry<5; retry++) { dispatch_command(FMMODE); read_tcfs(response); if (strcmp(response, "!") == 0) { tcflush(PortFD, TCIOFLUSH); LOG_INFO("Successfully connected to TCF-S Focuser in Manual Mode."); return true; } } tcflush(PortFD, TCIOFLUSH); LOG_ERROR("Failed connection to TCF-S Focuser."); return false; } /**************************************************************** ** ** *****************************************************************/ bool TCFS::Disconnect() { FocusTemperatureNP->s = IPS_IDLE; IDSetNumber(FocusTemperatureNP, nullptr); dispatch_command(FFMODE); return INDI::Focuser::Disconnect(); } /**************************************************************** ** ** *****************************************************************/ bool TCFS::ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) { if (dev != nullptr && strcmp(dev, getDeviceName()) == 0) { char response[TCFS_MAX_CMD] = { 0 }; if (!strcmp(FocusPowerSP->name, name)) { IUUpdateSwitch(FocusPowerSP, states, names, n); bool sleep = false; ISwitch *sp = IUFindOnSwitch(FocusPowerSP); // Sleep if (!strcmp(sp->name, "FOCUS_SLEEP")) { dispatch_command(FSLEEP); sleep = true; } // Wake Up else dispatch_command(FWAKUP); if (read_tcfs(response) == false) { IUResetSwitch(FocusPowerSP); FocusPowerSP->s = IPS_ALERT; IDSetSwitch(FocusPowerSP, "Error reading TCF-S reply."); return true; } if (sleep) { if (isSimulation()) strncpy(response, "ZZZ", TCFS_MAX_CMD); if (strcmp(response, "ZZZ") == 0) { FocusPowerSP->s = IPS_OK; IDSetSwitch(FocusPowerSP, "Focuser is set into sleep mode."); FocusAbsPosNP.s = IPS_IDLE; IDSetNumber(&FocusAbsPosNP, nullptr); if (FocusTemperatureNP) { FocusTemperatureNP->s = IPS_IDLE; IDSetNumber(FocusTemperatureNP, nullptr); } return true; } else { FocusPowerSP->s = IPS_ALERT; IDSetSwitch(FocusPowerSP, "Focuser sleep mode operation failed. Response: %s.", response); return true; } } else { if (isSimulation()) strncpy(response, "WAKE", TCFS_MAX_CMD); if (strcmp(response, "WAKE") == 0) { FocusPowerSP->s = IPS_OK; IDSetSwitch(FocusPowerSP, "Focuser is awake."); FocusAbsPosNP.s = IPS_OK; IDSetNumber(&FocusAbsPosNP, nullptr); if (FocusTemperatureNP) { FocusTemperatureNP->s = IPS_OK; IDSetNumber(FocusTemperatureNP, nullptr); } return true; } else { FocusPowerSP->s = IPS_ALERT; IDSetSwitch(FocusPowerSP, "Focuser wake up operation failed. Response: %s", response); return true; } } } // Do not process any command if focuser is asleep if (isConnected() && FocusPowerSP->sp[0].s == ISS_ON) { ISwitchVectorProperty *svp = getSwitch(name); if (svp) { svp->s = IPS_IDLE; LOG_WARN("Focuser is still in sleep mode. Wake up in order to issue commands."); IDSetSwitch(svp, nullptr); } return true; } if (!strcmp(FocusModeSP->name, name)) { IUUpdateSwitch(FocusModeSP, states, names, n); FocusModeSP->s = IPS_OK; ISwitch *sp = IUFindOnSwitch(FocusModeSP); if (!strcmp(sp->name, "Manual")) { dispatch_command(FMMODE); read_tcfs(response); if (!isSimulation() && strcmp(response, "!") != 0) { IUResetSwitch(FocusModeSP); FocusModeSP->s = IPS_ALERT; IDSetSwitch(FocusModeSP, "Error switching to manual mode. No reply from TCF-S. Try again."); return true; } } else if (!strcmp(sp->name, "Auto A")) { dispatch_command(FAMODE); read_tcfs(response); if (!isSimulation() && strcmp(response, "A") != 0) { IUResetSwitch(FocusModeSP); FocusModeSP->s = IPS_ALERT; IDSetSwitch(FocusModeSP, "Error switching to Auto Mode A. No reply from TCF-S. Try again."); return true; } } else { dispatch_command(FBMODE); read_tcfs(response); if (!isSimulation() && strcmp(response, "B") != 0) { IUResetSwitch(FocusModeSP); FocusModeSP->s = IPS_ALERT; IDSetSwitch(FocusModeSP, "Error switching to Auto Mode B. No reply from TCF-S. Try again."); return true; } } IDSetSwitch(FocusModeSP, nullptr); return true; } if (!strcmp(FocusGotoSP->name, name)) { if (FocusModeSP->sp[0].s != ISS_ON) { FocusGotoSP->s = IPS_IDLE; IDSetSwitch(FocusGotoSP, nullptr); LOG_WARN("The focuser can only be moved in Manual mode."); return false; } IUUpdateSwitch(FocusGotoSP, states, names, n); FocusGotoSP->s = IPS_BUSY; ISwitch *sp = IUFindOnSwitch(FocusGotoSP); // Min if (!strcmp(sp->name, "FOCUS_MIN")) { targetTicks = currentPosition; MoveRelFocuser(FOCUS_INWARD, currentPosition); IDSetSwitch(FocusGotoSP, "Moving focuser to minimum position..."); } // Center else if (!strcmp(sp->name, "FOCUS_CENTER")) { dispatch_command(FCENTR); FocusAbsPosNP.s = FocusRelPosNP.s = IPS_BUSY; IDSetNumber(&FocusAbsPosNP, nullptr); IDSetNumber(&FocusRelPosNP, nullptr); IDSetSwitch(FocusGotoSP, "Moving focuser to center position %d...", isTCFS3 ? 5000 : 3500); return true; } // Max else if (!strcmp(sp->name, "FOCUS_MAX")) { unsigned int delta = 0; delta = FocusAbsPosN[0].max - currentPosition; MoveRelFocuser(FOCUS_OUTWARD, delta); IDSetSwitch(FocusGotoSP, "Moving focuser to maximum position %g...", FocusAbsPosN[0].max); } // Home else if (!strcmp(sp->name, "FOCUS_HOME")) { dispatch_command(FHOME); read_tcfs(response); if (isSimulation()) strncpy(response, "DONE", TCFS_MAX_CMD); if (strcmp(response, "DONE") == 0) { IUResetSwitch(FocusGotoSP); FocusGotoSP->s = IPS_OK; IDSetSwitch(FocusGotoSP, "Moving focuser to new calculated position based on temperature..."); return true; } else { IUResetSwitch(FocusGotoSP); FocusGotoSP->s = IPS_ALERT; IDSetSwitch(FocusGotoSP, "Failed to move focuser to home position!"); return true; } } IDSetSwitch(FocusGotoSP, nullptr); return true; } } return INDI::Focuser::ISNewSwitch(dev, name, states, names, n); } IPState TCFS::MoveAbsFocuser(uint32_t targetTicks) { int delta = targetTicks - currentPosition; if (delta < 0) return MoveRelFocuser(FOCUS_INWARD, (uint32_t)std::abs(delta)); return MoveRelFocuser(FOCUS_OUTWARD, (uint32_t)std::abs(delta)); } IPState TCFS::MoveRelFocuser(FocusDirection dir, uint32_t ticks) { if (FocusModeSP->sp[0].s != ISS_ON) { LOG_WARN("The focuser can only be moved in Manual mode."); return IPS_ALERT; } targetTicks = ticks; targetPosition = currentPosition; // Inward if (dir == FOCUS_INWARD) { targetPosition -= targetTicks; dispatch_command(FIN); } // Outward else { targetPosition += targetTicks; dispatch_command(FOUT); } FocusAbsPosNP.s = IPS_BUSY; FocusRelPosNP.s = IPS_BUSY; IDSetNumber(&FocusAbsPosNP, nullptr); IDSetNumber(&FocusRelPosNP, nullptr); simulated_position = targetPosition; return IPS_BUSY; } bool TCFS::dispatch_command(TCFSCommand command_type) { int err_code = 0, nbytes_written = 0; char tcfs_error[TCFS_ERROR_BUFFER]; char command[TCFS_MAX_CMD] = {0}; switch (command_type) { // Focuser Manual Mode case FMMODE: strncpy(command, "FMMODE", TCFS_MAX_CMD); break; // Focuser Free Mode case FFMODE: strncpy(command, "FFMODE", TCFS_MAX_CMD); break; // Focuser Auto-A Mode case FAMODE: strncpy(command, "FAMODE", TCFS_MAX_CMD); break; // Focuser Auto-A Mode case FBMODE: strncpy(command, "FBMODE", TCFS_MAX_CMD); break; // Focus Center case FCENTR: strncpy(command, "FCENTR", TCFS_MAX_CMD); break; // Focuser In “nnnn†case FIN: simulated_position = currentPosition; snprintf(command, TCFS_MAX_CMD, "FI%04d", targetTicks); break; // Focuser Out “nnnn†case FOUT: simulated_position = currentPosition; snprintf(command, TCFS_MAX_CMD, "FO%04d", targetTicks); break; // Focuser Position Read Out case FPOSRO: strncpy(command, "FPOSRO", TCFS_MAX_CMD); break; // Focuser Position Read Out case FTMPRO: strncpy(command, "FTMPRO", TCFS_MAX_CMD); break; // Focuser Sleep case FSLEEP: strncpy(command, "FSLEEP", TCFS_MAX_CMD); break; // Focuser Wake Up case FWAKUP: strncpy(command, "FWAKUP", TCFS_MAX_CMD); break; // Focuser Home Command case FHOME: strncpy(command, "FHOME", TCFS_MAX_CMD); break; } LOGF_DEBUG("CMD <%s>", command); if (isSimulation()) return true; tcflush(PortFD, TCIOFLUSH); if ((err_code = tty_write(PortFD, command, strlen(command), &nbytes_written) != TTY_OK)) { tty_error_msg(err_code, tcfs_error, TCFS_ERROR_BUFFER); LOGF_ERROR("TTY error detected: %s", tcfs_error); return false; } return true; } void TCFS::TimerHit() { static double lastPosition = -1, lastTemperature = -1000; if (!isConnected()) { SetTimer(POLLMS); return; } int f_position = 0; float f_temperature = 0; char response[TCFS_MAX_CMD] = { 0 }; if (FocusGotoSP->s == IPS_BUSY) { ISwitch *sp = IUFindOnSwitch(FocusGotoSP); if (sp != nullptr && strcmp(sp->name, "FOCUS_CENTER") == 0) { bool rc = read_tcfs(response, true); if (!rc) { SetTimer(POLLMS); return; } if (isSimulation()) strncpy(response, "CENTER", TCFS_MAX_CMD); if (strcmp(response, "CENTER") == 0) { IUResetSwitch(FocusGotoSP); FocusGotoSP->s = IPS_OK; FocusAbsPosNP.s = IPS_OK; IDSetSwitch(FocusGotoSP, nullptr); IDSetNumber(&FocusAbsPosNP, nullptr); LOG_INFO("Focuser moved to center position."); } } } switch (FocusAbsPosNP.s) { case IPS_OK: if (FocusModeSP->sp[0].s == ISS_ON) dispatch_command(FPOSRO); if (read_tcfs(response) == false) { SetTimer(POLLMS); return; } if (isSimulation()) snprintf(response, TCFS_MAX_CMD, "P=%04d", (int)simulated_position); sscanf(response, "P=%d", &f_position); currentPosition = f_position; if (lastPosition != currentPosition) { lastPosition = currentPosition; IDSetNumber(&FocusAbsPosNP, nullptr); } break; case IPS_BUSY: if (read_tcfs(response, true) == false) { SetTimer(POLLMS); return; } // Ignore error if (strstr(response, "ER") != nullptr) { LOGF_DEBUG("Received error: %s", response); SetTimer(POLLMS); return; } if (isSimulation()) strncpy(response, "*", 2); if (strcmp(response, "*") == 0) { LOGF_DEBUG("Moving focuser %d steps to position %d.", targetTicks, targetPosition); FocusAbsPosNP.s = IPS_OK; FocusRelPosNP.s = IPS_OK; FocusGotoSP->s = IPS_OK; IDSetNumber(&FocusAbsPosNP, nullptr); IDSetNumber(&FocusRelPosNP, nullptr); IDSetSwitch(FocusGotoSP, nullptr); } else { FocusAbsPosNP.s = IPS_ALERT; LOGF_ERROR("Unable to read response from focuser #%s#.", response); IDSetNumber(&FocusAbsPosNP, nullptr); } break; default: break; } if (FocusTemperatureNP->s != IPS_IDLE) { // Read Temperature // Manual Mode if (FocusModeSP->sp[0].s == ISS_ON) dispatch_command(FTMPRO); if (read_tcfs(response) == false) { SetTimer(POLLMS); return; } if (isSimulation()) snprintf(response, TCFS_MAX_CMD, "T=%0.1f", simulated_temperature); sscanf(response, "T=%f", &f_temperature); FocusTemperatureNP->np[0].value = f_temperature; if (lastTemperature != FocusTemperatureNP->np[0].value) { lastTemperature = FocusTemperatureNP->np[0].value; IDSetNumber(FocusTemperatureNP, nullptr); } } SetTimer(POLLMS); } bool TCFS::read_tcfs(char *response, bool silent) { int err_code = 0, nbytes_read = 0; char err_msg[TCFS_ERROR_BUFFER]; if (isSimulation()) { strncpy(response, "SIMULATION", TCFS_MAX_CMD); return true; } // Read until encountring a CR if ((err_code = tty_read_section(PortFD, response, 0x0D, 5, &nbytes_read)) != TTY_OK) { if (!silent) { tty_error_msg(err_code, err_msg, 32); LOGF_ERROR("TTY error detected: %s", err_msg); } return false; } // Remove LF & CR response[nbytes_read - 2] = '\0'; LOGF_DEBUG("RES <%s>", response); return true; } const char *TCFS::getDefaultName() { return mydev; } libindi/drivers/focuser/usbfocusv3.cpp0000664000175000017500000010733013263645557017430 0ustar jasemjasem/* USB Focus V3 Copyright (C) 2016 G. Schmidt 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 */ #include "usbfocusv3.h" #include "indicom.h" #include #include #include #include #include #define USBFOCUSV3_TIMEOUT 3 #define SRTUS 25000 /***************************** Class USBFocusV3 *******************************/ std::unique_ptr usbFocusV3(new USBFocusV3()); void ISGetProperties(const char *dev) { usbFocusV3->ISGetProperties(dev); } void ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) { usbFocusV3->ISNewSwitch(dev, name, states, names, n); } void ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n) { usbFocusV3->ISNewText(dev, name, texts, names, n); } void ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) { usbFocusV3->ISNewNumber(dev, name, values, names, n); } void ISNewBLOB(const char *dev, const char *name, int sizes[], int blobsizes[], char *blobs[], char *formats[], char *names[], int n) { INDI_UNUSED(dev); INDI_UNUSED(name); INDI_UNUSED(sizes); INDI_UNUSED(blobsizes); INDI_UNUSED(blobs); INDI_UNUSED(formats); INDI_UNUSED(names); INDI_UNUSED(n); } void ISSnoopDevice(XMLEle *root) { usbFocusV3->ISSnoopDevice(root); } USBFocusV3::USBFocusV3() { // Can move in Absolute & Relative motions, can AbortFocuser motion, and has variable speed. FI::SetCapability(FOCUSER_CAN_ABS_MOVE | FOCUSER_CAN_REL_MOVE | FOCUSER_CAN_ABORT | FOCUSER_HAS_VARIABLE_SPEED); } bool USBFocusV3::initProperties() { INDI::Focuser::initProperties(); /*** init controller parameters ***/ direction = 0; stepmode = 1; speed = 3; stepsdeg = 20; tcomp_thr = 5; firmware = 0; maxpos = 65535; /*** init driver parameters ***/ FocusSpeedN[0].min = 1; FocusSpeedN[0].max = 3; FocusSpeedN[0].value = 2; /* Step Mode */ IUFillSwitch(&StepModeS[0], "Half Step", "", ISS_ON); IUFillSwitch(&StepModeS[1], "Full Step", "", ISS_OFF); IUFillSwitchVector(&StepModeSP, StepModeS, 2, getDeviceName(), "Step Mode", "", OPTIONS_TAB, IP_RW, ISR_1OFMANY, 0, IPS_IDLE); /* Direction */ IUFillSwitch(&RotDirS[0], "Standard rotation", "", ISS_ON); IUFillSwitch(&RotDirS[1], "Reverse rotation", "", ISS_OFF); IUFillSwitchVector(&RotDirSP, RotDirS, 2, getDeviceName(), "Rotation Mode", "", OPTIONS_TAB, IP_RW, ISR_1OFMANY, 0, IPS_IDLE); /* Focuser temperature */ IUFillNumber(&TemperatureN[0], "TEMPERATURE", "Celsius", "%6.2f", -50., 70., 0., 0.); IUFillNumberVector(&TemperatureNP, TemperatureN, 1, getDeviceName(), "FOCUS_TEMPERATURE", "Temperature", MAIN_CONTROL_TAB, IP_RO, 0, IPS_IDLE); // Maximum Position IUFillNumber(&MaxPositionN[0], "MAXPOSITION", "Maximum position", "%5.0f", 1., 65535., 0., 65535.); IUFillNumberVector(&MaxPositionNP, MaxPositionN, 1, getDeviceName(), "FOCUS_MAXPOSITION", "Max. Position", OPTIONS_TAB, IP_RW, 0, IPS_IDLE); // Temperature Settings IUFillNumber(&TemperatureSettingN[0], "Coefficient", "", "%3.0f", 0., 999., 1., 15.); IUFillNumber(&TemperatureSettingN[1], "Threshold", "", "%3.0f", 0., 999., 1., 10.); IUFillNumberVector(&TemperatureSettingNP, TemperatureSettingN, 2, getDeviceName(), "Temp. Settings", "", OPTIONS_TAB, IP_RW, 0, IPS_IDLE); /* Temperature Compensation Sign */ IUFillSwitch(&TempCompSignS[0], "negative", "", ISS_OFF); IUFillSwitch(&TempCompSignS[1], "positive", "", ISS_ON); IUFillSwitchVector(&TempCompSignSP, TempCompSignS, 2, getDeviceName(), "TComp. Sign", "", OPTIONS_TAB, IP_RW, ISR_1OFMANY, 0, IPS_IDLE); // Compensate for temperature IUFillSwitch(&TemperatureCompensateS[0], "Enable", "", ISS_OFF); IUFillSwitch(&TemperatureCompensateS[1], "Disable", "", ISS_ON); IUFillSwitchVector(&TemperatureCompensateSP, TemperatureCompensateS, 2, getDeviceName(), "Temp. Comp.", "", MAIN_CONTROL_TAB, IP_RW, ISR_1OFMANY, 0, IPS_IDLE); // Reset IUFillSwitch(&ResetS[0], "Reset", "", ISS_OFF); IUFillSwitchVector(&ResetSP, ResetS, 1, getDeviceName(), "Reset", "", MAIN_CONTROL_TAB, IP_RW, ISR_1OFMANY, 0, IPS_IDLE); // Firmware version IUFillNumber(&FWversionN[0], "FIRMWARE", "Firmware Version", "%5.0f", 0., 65535., 1., 0.); IUFillNumberVector(&FWversionNP, FWversionN, 1, getDeviceName(), "FW_VERSION", "Firmware", OPTIONS_TAB, IP_RO, 0, IPS_IDLE); /* Relative and absolute movement */ FocusRelPosN[0].min = 0.; FocusRelPosN[0].max = float(maxpos); FocusRelPosN[0].value = 0; FocusRelPosN[0].step = 1; FocusAbsPosN[0].min = 0.; FocusAbsPosN[0].max = float(maxpos); FocusAbsPosN[0].value = 0; FocusAbsPosN[0].step = 1; addDebugControl(); setDefaultPollingPeriod(500); return true; } bool USBFocusV3::updateProperties() { INDI::Focuser::updateProperties(); if (isConnected()) { defineNumber(&TemperatureNP); defineNumber(&MaxPositionNP); defineSwitch(&StepModeSP); defineSwitch(&RotDirSP); defineNumber(&MaxPositionNP); defineNumber(&TemperatureSettingNP); defineSwitch(&TempCompSignSP); defineSwitch(&TemperatureCompensateSP); defineSwitch(&ResetSP); defineNumber(&FWversionNP); GetFocusParams(); loadConfig(true); LOG_INFO("USB Focus V3 paramaters updated, focuser ready for use."); } else { deleteProperty(TemperatureNP.name); deleteProperty(MaxPositionNP.name); deleteProperty(StepModeSP.name); deleteProperty(RotDirSP.name); deleteProperty(TemperatureSettingNP.name); deleteProperty(TempCompSignSP.name); deleteProperty(TemperatureCompensateSP.name); deleteProperty(ResetSP.name); deleteProperty(FWversionNP.name); } return true; } bool USBFocusV3::Handshake() { if (Ack()) { LOG_INFO("USB Focus V3 is online. Getting focus parameters..."); return true; } LOG_INFO("Error retreiving data from USB Focus V3, please ensure USB Focus V3 controller " "is powered and the port is correct."); return false; } const char *USBFocusV3::getDefaultName() { return "USBFocusV3"; } bool USBFocusV3::Ack() { char cmd[] = UFOCDEVID; int nbytes_written = 0, nbytes_read = 0, rc = -1; char errstr[MAXRBUF]; char resp[UFORIDLEN + 1]; do { tcflush(PortFD, TCIOFLUSH); LOGF_DEBUG("CMD: %s.", cmd); if ((rc = tty_write(PortFD, cmd, strlen(cmd), &nbytes_written)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("Error requesting focuser ID: %s.", errstr); return false; } usleep(SRTUS); if ((rc = tty_read(PortFD, resp, UFORIDLEN, USBFOCUSV3_TIMEOUT, &nbytes_read)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("Error reading focuser ID: %s.", errstr); return false; } LOGF_DEBUG("RES: %s.", resp); usleep(SRTUS); resp[UFORIDLEN] = '\0'; } while (oneMoreRead(resp, UFORIDLEN)); if (strncmp(resp, UFOID, UFORIDLEN) == 0) { return true; } else { LOGF_ERROR("USB Focus V3 not properly identified! Answer was: %s.", resp); return false; } } bool USBFocusV3::getControllerStatus() { char cmd[] = UFOCREADPARAM; int nbytes_written = 0, nbytes_read = 0, rc = -1; char errstr[MAXRBUF]; char resp[UFORSTLEN + 1]; do { tcflush(PortFD, TCIOFLUSH); LOGF_DEBUG("CMD: %s.", cmd); if ((rc = tty_write(PortFD, cmd, strlen(cmd), &nbytes_written)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("getControllerStatus error: %s.", errstr); return false; } usleep(SRTUS); if ((rc = tty_read(PortFD, resp, UFORSTLEN, USBFOCUSV3_TIMEOUT, &nbytes_read)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("getControllerStatus error: %s.", errstr); return false; } LOGF_DEBUG("RES: %s.", resp); usleep(SRTUS); resp[UFORSTLEN] = '\0'; } while (oneMoreRead(resp, UFORSTLEN)); sscanf(resp, "C=%u-%u-%u-%u-%u-%u-%u", &direction, &stepmode, &speed, &stepsdeg, &tcomp_thr, &firmware, &maxpos); return true; } bool USBFocusV3::updateStepMode() { IUResetSwitch(&StepModeSP); if (stepmode == 1) StepModeS[0].s = ISS_ON; else if (stepmode == 0) StepModeS[1].s = ISS_ON; else { LOGF_ERROR("Unknown error: focuser step value (%d)", stepmode); return false; } return true; } bool USBFocusV3::updateRotDir() { IUResetSwitch(&RotDirSP); if (direction == 0) RotDirS[0].s = ISS_ON; else if (direction == 1) RotDirS[1].s = ISS_ON; else { LOGF_ERROR("Unknown error: rotation direction (%d)", direction); return false; } return true; } bool USBFocusV3::updateTemperature() { char cmd[] = UFOCREADTEMP; int nbytes_written = 0, nbytes_read = 0, rc = -1; char errstr[MAXRBUF]; char resp[UFORTEMPLEN + 1]; float temp; do { tcflush(PortFD, TCIOFLUSH); LOGF_DEBUG("CMD: %s.", cmd); if ((rc = tty_write(PortFD, cmd, strlen(cmd), &nbytes_written)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("updateTemperature error: %s.", errstr); return false; } usleep(SRTUS); if ((rc = tty_read(PortFD, resp, UFORTEMPLEN, USBFOCUSV3_TIMEOUT, &nbytes_read)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("updateTemperature error: %s.", errstr); return false; } LOGF_DEBUG("RES: %s.", resp); usleep(SRTUS); resp[UFORTEMPLEN] = '\0'; } while (oneMoreRead(resp, UFORTEMPLEN)); rc = sscanf(resp, "T=%f", &temp); if (rc > 0) { TemperatureN[0].value = temp; } else { LOGF_ERROR("Unknown error: focuser temperature value (%s)", resp); return false; } return true; } bool USBFocusV3::updateFWversion() { FWversionN[0].value = firmware; return true; } bool USBFocusV3::updatePosition() { char cmd[] = UFOCREADPOS; int nbytes_written = 0, nbytes_read = 0, rc = -1; char errstr[MAXRBUF]; char resp[UFORPOSLEN + 1]; int pos = -1; do { tcflush(PortFD, TCIOFLUSH); LOGF_DEBUG("CMD: %s.", cmd); if ((rc = tty_write(PortFD, cmd, strlen(cmd), &nbytes_written)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("updatePostion error: %s.", errstr); return false; } usleep(SRTUS); if ((rc = tty_read(PortFD, resp, UFORPOSLEN, USBFOCUSV3_TIMEOUT, &nbytes_read)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("updatePostion error: %s.", errstr); return false; } LOGF_DEBUG("RES: %s.", resp); usleep(SRTUS); resp[UFORPOSLEN] = '\0'; } while (oneMoreRead(resp, UFORPOSLEN)); rc = sscanf(resp, "P=%u", &pos); if (rc > 0) { FocusAbsPosN[0].value = pos; } else { LOGF_ERROR("Unknown error: focuser position value (%s)", resp); return false; } return true; } bool USBFocusV3::updateMaxPos() { MaxPositionN[0].value = maxpos; FocusAbsPosN[0].max = maxpos; return true; } bool USBFocusV3::updateTempCompSettings() { TemperatureSettingN[0].value = stepsdeg; TemperatureSettingN[1].value = tcomp_thr; return true; } bool USBFocusV3::updateTempCompSign() { char cmd[] = UFOCGETSIGN; int nbytes_written = 0, nbytes_read = 0, rc = -1; char errstr[MAXRBUF]; char resp[UFORSIGNLEN + 1]; unsigned int sign; do { tcflush(PortFD, TCIOFLUSH); LOGF_DEBUG("CMD: %s.", cmd); if ((rc = tty_write(PortFD, cmd, strlen(cmd), &nbytes_written)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("updateTempCompSign error: %s.", errstr); return false; } usleep(SRTUS); if ((rc = tty_read(PortFD, resp, UFORSIGNLEN, USBFOCUSV3_TIMEOUT, &nbytes_read)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("updateTempCompSign error: %s.", errstr); return false; } LOGF_DEBUG("RES: %s.", resp); usleep(SRTUS); resp[UFORSIGNLEN] = '\0'; } while (oneMoreRead(resp, UFORSIGNLEN)); rc = sscanf(resp, "A=%u", &sign); if (rc > 0) { IUResetSwitch(&TempCompSignSP); if (sign == 1) TempCompSignS[0].s = ISS_ON; else if (sign == 0) TempCompSignS[1].s = ISS_ON; else { LOGF_ERROR("Unknown error: temp. comp. sign (%d)", sign); return false; } } else { LOGF_ERROR("Unknown error: temp. comp. sign value (%s)", resp); return false; } return true; } bool USBFocusV3::updateSpeed() { int drvspeed; switch (speed) { case UFOPSPDAV: drvspeed = 3; break; case UFOPSPDSL: drvspeed = 2; break; case UFOPSPDUS: drvspeed = 1; break; default: drvspeed = 0; break; } if (drvspeed != 0) { currentSpeed = drvspeed; FocusSpeedN[0].value = drvspeed; } else { LOGF_ERROR("Unknown error: focuser speed value (%d)", speed); return false; } return true; } bool USBFocusV3::setAutoTempCompThreshold(unsigned int thr) { int nbytes_written = 0, nbytes_read = 0, rc = -1; char errstr[MAXRBUF]; char cmd[UFOCTLEN + 1]; char resp[UFORDONELEN + 1]; snprintf(cmd, UFOCTLEN + 1, "%s%03u", UFOCSETTCTHR, thr); do { tcflush(PortFD, TCIOFLUSH); LOGF_DEBUG("CMD: %s.", cmd); if ((rc = tty_write(PortFD, cmd, UFOCTLEN, &nbytes_written)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("setAutoTempCompThreshold error: %s.", errstr); return false; } usleep(SRTUS); if ((rc = tty_read(PortFD, resp, UFORDONELEN, USBFOCUSV3_TIMEOUT, &nbytes_read)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("setAutoTempCompThreshold error: %s.", errstr); return false; } LOGF_DEBUG("RES: %s.", resp); usleep(SRTUS); resp[UFORDONELEN] = '\0'; if (strncmp(resp, UFORSDONE, strlen(UFORSDONE)) == 0) { tcomp_thr = thr; return true; } } while (oneMoreRead(resp, UFORDONELEN)); sprintf(errstr, "did not receive DONE."); LOGF_ERROR("setAutoTempCompThreshold error: %s.", errstr); return false; } bool USBFocusV3::setTemperatureCoefficient(unsigned int coefficient) { int nbytes_written = 0, rc = -1; char errstr[MAXRBUF]; char cmd[UFOCTLEN + 1]; char resp[UFORDONELEN + 1]; snprintf(cmd, UFOCTLEN + 1, "%s%03u", UFOCSETSTDEG, coefficient); do { tcflush(PortFD, TCIOFLUSH); LOGF_DEBUG("CMD: %s.", cmd); if ((rc = tty_write(PortFD, cmd, UFOCTLEN, &nbytes_written)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("setTemperatureCoefficient error: %s.", errstr); return false; } usleep(SRTUS); resp[UFORDONELEN] = '\0'; if (strncmp(resp, UFORSDONE, strlen(UFORSDONE)) == 0) { stepsdeg = coefficient; return true; } } while (oneMoreRead(resp, UFORDONELEN)); sprintf(errstr, "did not receive DONE."); LOGF_ERROR("setTemperatureCoefficient error: %s.", errstr); return false; } bool USBFocusV3::reset() { char cmd[] = UFOCRESET; int nbytes_written = 0, rc = -1; char errstr[MAXRBUF]; LOGF_DEBUG("CMD: %s.", cmd); if ((rc = tty_write(PortFD, cmd, strlen(cmd), &nbytes_written)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("reset error: %s.", errstr); return false; } GetFocusParams(); return true; } bool USBFocusV3::MoveFocuserUF(FocusDirection dir, unsigned int rticks) { int nbytes_written = 0, rc = -1; char errstr[MAXRBUF]; char cmd[UFOCMLEN + 1]; unsigned int ticks; if ((dir == FOCUS_INWARD) && (rticks > FocusAbsPosN[0].value)) { ticks = FocusAbsPosN[0].value; LOGF_WARN("Requested %u ticks but inward movement has been limited to %u ticks", rticks, ticks); } else if ((dir == FOCUS_OUTWARD) && ((FocusAbsPosN[0].value + rticks) > MaxPositionN[0].value)) { ticks = MaxPositionN[0].value - FocusAbsPosN[0].value; LOGF_WARN("Requested %u ticks but outward movement has been limited to %u ticks", rticks, ticks); } else { ticks = rticks; } if (dir == FOCUS_INWARD) snprintf(cmd, UFOCMLEN + 1, "%s%05u", UFOCMOVEIN, ticks); else snprintf(cmd, UFOCMLEN + 1, "%s%05u", UFOCMOVEOUT, ticks); LOGF_DEBUG("CMD: %s.", cmd); if ((rc = tty_write(PortFD, cmd, UFOCMLEN, &nbytes_written)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("MoveFocuserUF error: %s.", errstr); return false; } return true; } bool USBFocusV3::setStepMode(FocusStepMode mode) { int nbytes_written = 0, rc = -1; char errstr[MAXRBUF]; char cmd[UFOCSMLEN + 1]; if (mode == FOCUS_HALF_STEP) snprintf(cmd, UFOCSMLEN + 1, "%s", UFOCSETHSTEPS); else snprintf(cmd, UFOCSMLEN + 1, "%s", UFOCSETFSTEPS); LOGF_DEBUG("CMD: %s.", cmd); if ((rc = tty_write(PortFD, cmd, UFOCSMLEN, &nbytes_written)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("setStepMode error: %s.", errstr); return false; } else { stepmode = mode; } return true; } bool USBFocusV3::setRotDir(unsigned int dir) { int nbytes_written = 0, rc = -1; char errstr[MAXRBUF]; char cmd[UFOCDLEN + 1]; if (dir == UFOPSDIR) snprintf(cmd, UFOCDLEN + 1, "%s", UFOCSETSDIR); else snprintf(cmd, UFOCDLEN + 1, "%s", UFOCSETRDIR); tcflush(PortFD, TCIOFLUSH); LOGF_DEBUG("CMD: %s.", cmd); if ((rc = tty_write(PortFD, cmd, UFOCDLEN, &nbytes_written)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("setRotDir error: %s.", errstr); return false; } else { direction = dir; } return true; } bool USBFocusV3::setMaxPos(unsigned int maxp) { int nbytes_written = 0, nbytes_read = 0, rc = -1; char errstr[MAXRBUF]; char cmd[UFOCMMLEN + 1]; char resp[UFORDONELEN + 1]; if (maxp >= 1 && maxp <= 65535) { snprintf(cmd, UFOCMMLEN + 1, "%s%05u", UFOCSETMAX, maxp); } else { LOGF_ERROR("Focuser max. pos. value %d out of bounds", maxp); return false; } do { tcflush(PortFD, TCIOFLUSH); LOGF_DEBUG("CMD: %s.", cmd); if ((rc = tty_write(PortFD, cmd, UFOCMMLEN, &nbytes_written)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("setMaxPos error: %s.", errstr); return false; } usleep(SRTUS); if ((rc = tty_read(PortFD, resp, UFORDONELEN, USBFOCUSV3_TIMEOUT, &nbytes_read)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("setMaxPos error: %s.", errstr); return false; } LOGF_DEBUG("RES: %s.", resp); usleep(SRTUS); resp[UFORDONELEN] = '\0'; if (strncmp(resp, UFORSDONE, strlen(UFORSDONE)) == 0) { maxpos = maxp; FocusAbsPosN[0].max = maxpos; return true; } } while (oneMoreRead(resp, UFORDONELEN)); sprintf(errstr, "did not receive DONE."); LOGF_ERROR("setMaxPos error: %s.", errstr); return false; } bool USBFocusV3::setSpeed(unsigned short drvspeed) { int nbytes_written = 0, nbytes_read = 0, rc = -1; char errstr[MAXRBUF]; char cmd[UFOCSLEN + 1]; char resp[UFORDONELEN + 1]; unsigned int spd; switch (drvspeed) { case 3: spd = UFOPSPDAV; break; case 2: spd = UFOPSPDSL; break; case 1: spd = UFOPSPDUS; break; default: spd = UFOPSPDERR; break; } if (spd != UFOPSPDERR) { snprintf(cmd, UFOCSLEN + 1, "%s%03u", UFOCSETSPEED, spd); } else { LOGF_ERROR("Focuser speed value %d out of bounds", drvspeed); return false; } do { tcflush(PortFD, TCIOFLUSH); LOGF_DEBUG("CMD: %s.", cmd); if ((rc = tty_write(PortFD, cmd, UFOCSLEN, &nbytes_written)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("setSpeed error: %s.", errstr); return false; } usleep(SRTUS); if ((rc = tty_read(PortFD, resp, UFORDONELEN, USBFOCUSV3_TIMEOUT, &nbytes_read)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("setSpeed error: %s.", errstr); return false; } LOGF_DEBUG("RES: %s.", resp); usleep(SRTUS); resp[UFORDONELEN] = '\0'; if (strncmp(resp, UFORSDONE, strlen(UFORSDONE)) == 0) { speed = spd; return true; } } while (oneMoreRead(resp, UFORDONELEN)); sprintf(errstr, "did not receive DONE."); LOGF_ERROR("setSpeed error: %s.", errstr); return false; } bool USBFocusV3::setTemperatureCompensation(bool enable) { int nbytes_written = 0, rc = -1; char errstr[MAXRBUF]; char cmd[UFOCTCLEN + 1]; if (enable) snprintf(cmd, UFOCTCLEN + 1, "%s", UFOCSETAUTO); else snprintf(cmd, UFOCTCLEN + 1, "%s", UFOCSETMANU); LOGF_DEBUG("CMD: %s.", cmd); if ((rc = tty_write(PortFD, cmd, UFOCTCLEN, &nbytes_written)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("setTemperatureCompensation error: %s.", errstr); return false; } return true; } bool USBFocusV3::setTempCompSign(unsigned int sign) { int nbytes_written = 0, nbytes_read = 0, rc = -1; char errstr[MAXRBUF]; char cmd[UFOCTLEN + 1]; char resp[UFORDONELEN + 1]; snprintf(cmd, UFOCDLEN + 1, "%s%1u", UFOCSETSIGN, sign); do { tcflush(PortFD, TCIOFLUSH); LOGF_DEBUG("CMD: %s.", cmd); if ((rc = tty_write(PortFD, cmd, UFOCDLEN, &nbytes_written)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("setTempCompSign error: %s.", errstr); return false; } usleep(SRTUS); if ((rc = tty_read(PortFD, resp, UFORDONELEN, USBFOCUSV3_TIMEOUT, &nbytes_read)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("setTempCompSign error: %s.", errstr); return false; } LOGF_DEBUG("RES: %s.", resp); usleep(SRTUS); resp[UFORDONELEN] = '\0'; if (strncmp(resp, UFORSDONE, strlen(UFORSDONE)) == 0) { return true; } } while (oneMoreRead(resp, UFORDONELEN)); sprintf(errstr, "did not receive DONE."); LOGF_ERROR("setTempCompSign error: %s.", errstr); return false; } bool USBFocusV3::ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) { if (dev != nullptr && strcmp(dev, getDeviceName()) == 0) { if (strcmp(StepModeSP.name, name) == 0) { bool rc = false; int current_mode = IUFindOnSwitchIndex(&StepModeSP); IUUpdateSwitch(&StepModeSP, states, names, n); int target_mode = IUFindOnSwitchIndex(&StepModeSP); if (current_mode == target_mode) { StepModeSP.s = IPS_OK; IDSetSwitch(&StepModeSP, nullptr); } if (target_mode == 0) rc = setStepMode(FOCUS_HALF_STEP); else rc = setStepMode(FOCUS_FULL_STEP); if (!rc) { IUResetSwitch(&StepModeSP); StepModeS[current_mode].s = ISS_ON; StepModeSP.s = IPS_ALERT; IDSetSwitch(&StepModeSP, nullptr); return false; } StepModeSP.s = IPS_OK; IDSetSwitch(&StepModeSP, nullptr); return true; } if (strcmp(RotDirSP.name, name) == 0) { bool rc = false; int current_mode = IUFindOnSwitchIndex(&RotDirSP); IUUpdateSwitch(&RotDirSP, states, names, n); int target_mode = IUFindOnSwitchIndex(&RotDirSP); if (current_mode == target_mode) { RotDirSP.s = IPS_OK; IDSetSwitch(&RotDirSP, nullptr); } if (target_mode == 0) rc = setRotDir(UFOPSDIR); else rc = setRotDir(UFOPRDIR); if (!rc) { IUResetSwitch(&RotDirSP); RotDirS[current_mode].s = ISS_ON; RotDirSP.s = IPS_ALERT; IDSetSwitch(&RotDirSP, nullptr); return false; } RotDirSP.s = IPS_OK; IDSetSwitch(&RotDirSP, nullptr); return true; } if (strcmp(TemperatureCompensateSP.name, name) == 0) { int last_index = IUFindOnSwitchIndex(&TemperatureCompensateSP); IUUpdateSwitch(&TemperatureCompensateSP, states, names, n); bool rc = setTemperatureCompensation((TemperatureCompensateS[0].s == ISS_ON)); if (!rc) { TemperatureCompensateSP.s = IPS_ALERT; IUResetSwitch(&TemperatureCompensateSP); TemperatureCompensateS[last_index].s = ISS_ON; IDSetSwitch(&TemperatureCompensateSP, nullptr); return false; } TemperatureCompensateSP.s = IPS_OK; IDSetSwitch(&TemperatureCompensateSP, nullptr); return true; } if (strcmp(TempCompSignSP.name, name) == 0) { bool rc = false; int current_mode = IUFindOnSwitchIndex(&TempCompSignSP); IUUpdateSwitch(&TempCompSignSP, states, names, n); int target_mode = IUFindOnSwitchIndex(&TempCompSignSP); if (current_mode == target_mode) { TempCompSignSP.s = IPS_OK; IDSetSwitch(&TempCompSignSP, nullptr); } if (target_mode == 0) rc = setTempCompSign(UFOPNSIGN); else rc = setTempCompSign(UFOPPSIGN); if (!rc) { IUResetSwitch(&TempCompSignSP); TempCompSignS[current_mode].s = ISS_ON; TempCompSignSP.s = IPS_ALERT; IDSetSwitch(&TempCompSignSP, nullptr); return false; } TempCompSignSP.s = IPS_OK; IDSetSwitch(&TempCompSignSP, nullptr); return true; } if (strcmp(ResetSP.name, name) == 0) { IUResetSwitch(&ResetSP); if (reset()) ResetSP.s = IPS_OK; else ResetSP.s = IPS_ALERT; IDSetSwitch(&ResetSP, nullptr); return true; } } return INDI::Focuser::ISNewSwitch(dev, name, states, names, n); } bool USBFocusV3::ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) { if (dev != nullptr && strcmp(dev, getDeviceName()) == 0) { if (strcmp(name, MaxPositionNP.name) == 0) { IUUpdateNumber(&MaxPositionNP, values, names, n); if (!setMaxPos(MaxPositionN[0].value)) { MaxPositionNP.s = IPS_ALERT; IDSetNumber(&MaxPositionNP, nullptr); return false; } MaxPositionNP.s = IPS_OK; IDSetNumber(&MaxPositionNP, nullptr); return true; } if (strcmp(name, TemperatureSettingNP.name) == 0) { IUUpdateNumber(&TemperatureSettingNP, values, names, n); if (!setAutoTempCompThreshold(TemperatureSettingN[1].value) || !setTemperatureCoefficient(TemperatureSettingN[0].value)) { TemperatureSettingNP.s = IPS_ALERT; IDSetNumber(&TemperatureSettingNP, nullptr); return false; } TemperatureSettingNP.s = IPS_OK; IDSetNumber(&TemperatureSettingNP, nullptr); return true; } if (strcmp(name, FWversionNP.name) == 0) { IUUpdateNumber(&FWversionNP, values, names, n); FWversionNP.s = IPS_OK; IDSetNumber(&FWversionNP, nullptr); return true; } } return INDI::Focuser::ISNewNumber(dev, name, values, names, n); } bool USBFocusV3::oneMoreRead(char *response, unsigned int maxlen) { if (strncmp(response, UFORSACK, std::min((unsigned int)strlen(UFORSACK), maxlen)) == 0) { return true; } if (strncmp(response, UFORSEQU, std::min((unsigned int)strlen(UFORSEQU), maxlen)) == 0) { return true; } if (strncmp(response, UFORSAUTO, std::min((unsigned int)strlen(UFORSAUTO), maxlen)) == 0) { return true; } if (strncmp(response, UFORSERR, std::min((unsigned int)strlen(UFORSERR), maxlen)) == 0) { return true; } if (strncmp(response, UFORSDONE, std::min((unsigned int)strlen(UFORSDONE), maxlen)) == 0) { return true; } if (strncmp(response, UFORSRESET, std::min((unsigned int)strlen(UFORSRESET), maxlen)) == 0) { return true; } return false; } void USBFocusV3::GetFocusParams() { getControllerStatus(); if (updatePosition()) IDSetNumber(&FocusAbsPosNP, nullptr); if (updateMaxPos()) { IDSetNumber(&MaxPositionNP, nullptr); IDSetNumber(&FocusAbsPosNP, nullptr); } if (updateTemperature()) IDSetNumber(&TemperatureNP, nullptr); if (updateTempCompSettings()) IDSetNumber(&TemperatureSettingNP, nullptr); if (updateTempCompSign()) IDSetSwitch(&TempCompSignSP, nullptr); if (updateSpeed()) IDSetNumber(&FocusSpeedNP, nullptr); if (updateStepMode()) IDSetSwitch(&StepModeSP, nullptr); if (updateRotDir()) IDSetSwitch(&RotDirSP, nullptr); if (updateFWversion()) IDSetNumber(&FWversionNP, nullptr); } bool USBFocusV3::SetFocuserSpeed(int speed) { bool rc = false; rc = setSpeed(speed); if (!rc) return false; currentSpeed = speed; FocusSpeedNP.s = IPS_OK; IDSetNumber(&FocusSpeedNP, nullptr); return true; } IPState USBFocusV3::MoveAbsFocuser(uint32_t targetTicks) { long ticks; targetPos = targetTicks; ticks = targetPos - FocusAbsPosN[0].value; bool rc = false; if (ticks < 0) rc = MoveFocuserUF(FOCUS_INWARD, (unsigned int)labs(ticks)); else if (ticks > 0) rc = MoveFocuserUF(FOCUS_OUTWARD, (unsigned int)labs(ticks)); if (!rc) return IPS_ALERT; FocusAbsPosNP.s = IPS_BUSY; return IPS_BUSY; } IPState USBFocusV3::MoveRelFocuser(FocusDirection dir, uint32_t ticks) { bool rc = false; uint32_t aticks; if ((dir == FOCUS_INWARD) && (ticks > FocusAbsPosN[0].value)) { aticks = FocusAbsPosN[0].value; DEBUGF(INDI::Logger::DBG_WARNING, "Requested %u ticks but relative inward movement has been limited to %u ticks", ticks, aticks); ticks = aticks; } else if ((dir == FOCUS_OUTWARD) && ((FocusAbsPosN[0].value + ticks) > MaxPositionN[0].value)) { aticks = MaxPositionN[0].value - FocusAbsPosN[0].value; DEBUGF(INDI::Logger::DBG_WARNING, "Requested %u ticks but relative outward movement has been limited to %u ticks", ticks, aticks); ticks = aticks; } rc = MoveFocuserUF(dir, (unsigned int)ticks); if (!rc) return IPS_ALERT; FocusRelPosN[0].value = ticks; FocusRelPosNP.s = IPS_BUSY; return IPS_BUSY; } void USBFocusV3::TimerHit() { if (!isConnected()) { SetTimer(POLLMS); return; } bool rc = updatePosition(); if (rc) { if (fabs(lastPos - FocusAbsPosN[0].value) > 5) { IDSetNumber(&FocusAbsPosNP, nullptr); lastPos = FocusAbsPosN[0].value; } } rc = updateTemperature(); if (rc) { if (fabs(lastTemperature - TemperatureN[0].value) >= 0.5) { IDSetNumber(&TemperatureNP, nullptr); lastTemperature = TemperatureN[0].value; } } if (FocusTimerNP.s == IPS_BUSY) { float remaining = CalcTimeLeft(focusMoveStart, focusMoveRequest); if (remaining <= 0) { FocusTimerNP.s = IPS_OK; FocusTimerN[0].value = 0; AbortFocuser(); } else FocusTimerN[0].value = remaining * 1000.0; IDSetNumber(&FocusTimerNP, nullptr); } if (FocusAbsPosNP.s == IPS_BUSY || FocusRelPosNP.s == IPS_BUSY) { if (fabs(targetPos - FocusAbsPosN[0].value) < 1) { FocusAbsPosNP.s = IPS_OK; FocusRelPosNP.s = IPS_OK; IDSetNumber(&FocusAbsPosNP, nullptr); IDSetNumber(&FocusRelPosNP, nullptr); lastPos = FocusAbsPosN[0].value; LOG_INFO("Focuser reached requested position."); } } SetTimer(POLLMS); } bool USBFocusV3::AbortFocuser() { char cmd[] = UFOCABORT; int nbytes_written; LOGF_DEBUG("CMD: %s.", cmd); if (tty_write(PortFD, cmd, strlen(cmd), &nbytes_written) == TTY_OK) { FocusAbsPosNP.s = IPS_IDLE; FocusRelPosNP.s = IPS_IDLE; IDSetNumber(&FocusAbsPosNP, nullptr); IDSetNumber(&FocusRelPosNP, nullptr); return true; } else return false; } float USBFocusV3::CalcTimeLeft(timeval start, float req) { double timesince; double timeleft; struct timeval now { 0, 0 }; gettimeofday(&now, nullptr); timesince = (double)(now.tv_sec * 1000.0 + now.tv_usec / 1000) - (double)(start.tv_sec * 1000.0 + start.tv_usec / 1000); timesince = timesince / 1000; timeleft = req - timesince; return timeleft; } libindi/drivers/focuser/nfocus.h0000664000175000017500000000540013263645557016263 0ustar jasemjasem/* NFocus Copyright (C) 2006 Markus Wildi (markus.wildi@datacomm.ch) 2011 Jasem Mutlaq (mutlaqja@ikarustech.com) 2013 Felix Krämer (rigelsys@felix-kraemer.de) Thanks to Rigel Systems, especially Gene Nolan and Leon Palmer, for their support in writing this driver. 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 */ #pragma once #include "indifocuser.h" class NFocus : public INDI::Focuser { public: NFocus(); virtual ~NFocus() = default; virtual bool Handshake(); const char *getDefaultName(); virtual bool initProperties(); virtual bool updateProperties(); virtual bool ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n); virtual IPState MoveAbsFocuser(uint32_t targetTicks); virtual IPState MoveRelFocuser(FocusDirection dir, uint32_t ticks); protected: bool saveConfigItems(FILE *fp); private: unsigned char CalculateSum(char *rf_cmd); int SendCommand(char *rf_cmd); int SendRequest(char *rf_cmd); int ReadResponse(char *buf, int nbytes, int timeout); bool GetFocusParams(); int updateNFTemperature(double *value); int updateNFInOutScalar(double *value); int updateNFMotorSettings(double *onTime, double *offTime, double *fastDelay); int moveNFInward(const double *value); int moveNFOutward(const double *value); int getNFAbsolutePosition(double *value); int setNFAbsolutePosition(const double *value); int setNFMaxPosition(double *value); int syncNF(const double *value); INumber TemperatureN[1]; INumberVectorProperty TemperatureNP; INumber SettingsN[3]; INumberVectorProperty SettingsNP; INumber MinMaxPositionN[2]; INumberVectorProperty MinMaxPositionNP; INumber MaxTravelN[1]; INumberVectorProperty MaxTravelNP; INumber SyncN[1]; INumberVectorProperty SyncNP; INumber InOutScalarN[1]; INumberVectorProperty InOutScalarNP; INumber RelMovementN[1]; INumberVectorProperty RelMovementNP; INumber AbsMovementN[1]; INumberVectorProperty AbsMovementNP; }; libindi/drivers/focuser/lakeside.h0000664000175000017500000001055213263645557016553 0ustar jasemjasem/* Lakeside Focuser Copyright (C) 2017 Phil Shepherd (psjshep@googlemail.com) Technical Information kindly supplied by Peter Chance at LakesideAstro (info@lakeside-astro.com) Code template from original Moonlite code by 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 */ #pragma once #include "indifocuser.h" #include class Lakeside : public INDI::Focuser { public: Lakeside(); ~Lakeside() = default; const char * getDefaultName(); virtual bool initProperties(); virtual bool updateProperties(); virtual bool ISNewNumber (const char *dev, const char *name, double values[], char *names[], int n); virtual bool ISNewSwitch (const char *dev, const char *name, ISState *states, char *names[], int n); protected: virtual bool Handshake(); virtual IPState MoveAbsFocuser(uint32_t ticks); virtual IPState MoveRelFocuser(FocusDirection dir, uint32_t ticks); virtual bool AbortFocuser(); virtual void TimerHit(); private: double targetPos, lastPos, lastTemperature; uint32_t currentSpeed; struct timeval focusMoveStart; float focusMoveRequest; void GetFocusParams(); bool updateMoveDirection(); bool updateMaxTravel(); bool updateStepSize(); bool updateBacklash(); bool updateTemperature(); bool updateTemperatureK(); bool updatePosition(); bool LakesideOnline(); bool updateActiveTemperatureSlope(); bool updateSlope1Inc(); bool updateSlope1Dir(); bool updateSlope1Deadband(); bool updateSlope1Period(); bool updateSlope2Inc(); bool updateSlope2Dir(); bool updateSlope2Deadband(); bool updateSlope2Period(); bool GetLakesideStatus(); char DecodeBuffer(char* in_response); bool SendCmd(const char *in_cmd); bool ReadBuffer(char* response); bool gotoPosition(uint32_t position); bool setCalibration(); bool setTemperatureTracking(bool enable); bool setBacklash(int backlash ); bool setStepSize(int stepsize ); bool setMaxTravel(int); bool setMoveDirection(int direction); bool setActiveTemperatureSlope(uint32_t active_slope); bool setSlope1Inc(uint32_t slope1_inc); bool setSlope1Dir(uint32_t slope1_direction); bool setSlope1Deadband(uint32_t slope1_deadband); bool setSlope1Period(uint32_t slope1_period); bool setSlope2Inc(uint32_t slope2_inc); bool setSlope2Dir(uint32_t slope2_direction); bool setSlope2Deadband(uint32_t slope2_deadband); bool setSlope2Period(uint32_t slope2_period); INumber TemperatureN[1]; INumberVectorProperty TemperatureNP; INumber TemperatureKN[1]; INumberVectorProperty TemperatureKNP; ISwitch MoveDirectionS[2]; ISwitchVectorProperty MoveDirectionSP; INumber StepSizeN[1]; INumberVectorProperty StepSizeNP; INumber BacklashN[1]; INumberVectorProperty BacklashNP; INumber MaxTravelN[1]; INumberVectorProperty MaxTravelNP; ISwitch TemperatureTrackingS[2]; ISwitchVectorProperty TemperatureTrackingSP; ISwitch ActiveTemperatureSlopeS[2]; ISwitchVectorProperty ActiveTemperatureSlopeSP; INumber Slope1IncN[1]; INumberVectorProperty Slope1IncNP; ISwitch Slope1DirS[2]; ISwitchVectorProperty Slope1DirSP; INumber Slope1DeadbandN[1]; INumberVectorProperty Slope1DeadbandNP; INumber Slope1PeriodN[1]; INumberVectorProperty Slope1PeriodNP; INumber Slope2IncN[1]; INumberVectorProperty Slope2IncNP; ISwitch Slope2DirS[2]; ISwitchVectorProperty Slope2DirSP; INumber Slope2DeadbandN[1]; INumberVectorProperty Slope2DeadbandNP; INumber Slope2PeriodN[1]; INumberVectorProperty Slope2PeriodNP; }; libindi/drivers/focuser/smartfocus.cpp0000664000175000017500000003406313263645557017516 0ustar jasemjasem/******************************************************************************* Copyright(c) 2015 Camiel Severijns. All rights reserved. This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2 as published by the Free Software Foundation. . 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 Library General Public License for more details. . You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. *******************************************************************************/ #include "smartfocus.h" #include "indicom.h" #include #include #include #include #include #include namespace { static constexpr const SmartFocus::Position PositionInvalid { static_cast(0xFFFF) }; // Interval to check the focuser state (in milliseconds) static constexpr const int TimerInterval { 500 }; // in seconds static constexpr const int ReadTimeOut { 1 }; // SmartFocus command and response characters static constexpr const char goto_position { 'g' }; static constexpr const char stop_focuser { 's' }; static constexpr const char read_id_register { 'b' }; static constexpr const char read_id_respons { 'j' }; static constexpr const char read_position { 'p' }; static constexpr const char read_flags { 't' }; static constexpr const char motion_complete { 'c' }; static constexpr const char motion_error { 'r' }; static constexpr const char motion_stopped { 's' }; } // namespace std::unique_ptr smartFocus(new SmartFocus()); void ISGetProperties(const char *dev) { smartFocus->ISGetProperties(dev); } void ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) { smartFocus->ISNewSwitch(dev, name, states, names, n); } void ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n) { smartFocus->ISNewText(dev, name, texts, names, n); } void ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) { smartFocus->ISNewNumber(dev, name, values, names, n); } void ISNewBLOB(const char *dev, const char *name, int sizes[], int blobsizes[], char *blobs[], char *formats[], char *names[], int n) { INDI_UNUSED(dev); INDI_UNUSED(name); INDI_UNUSED(sizes); INDI_UNUSED(blobsizes); INDI_UNUSED(blobs); INDI_UNUSED(formats); INDI_UNUSED(names); INDI_UNUSED(n); } void ISSnoopDevice(XMLEle *root) { smartFocus->ISSnoopDevice(root); } SmartFocus::SmartFocus() { FI::SetCapability(FOCUSER_CAN_ABS_MOVE | FOCUSER_CAN_REL_MOVE | FOCUSER_CAN_ABORT); } bool SmartFocus::initProperties() { INDI::Focuser::initProperties(); // No speed for SmartFocus FocusSpeedN[0].min = FocusSpeedN[0].max = FocusSpeedN[0].value = 1; IUUpdateMinMax(&FocusSpeedNP); IUFillLight(&FlagsL[STATUS_SERIAL_FRAMING_ERROR], "SERIAL_FRAMING_ERROR", "Serial framing error", IPS_OK); IUFillLight(&FlagsL[STATUS_SERIAL_OVERRUN_ERROR], "SERIAL_OVERRUN_ERROR", "Serial overrun error", IPS_OK); IUFillLight(&FlagsL[STATUS_MOTOR_ENCODE_ERROR], "MOTOR_ENCODER_ERROR", "Motor/encoder error", IPS_OK); IUFillLight(&FlagsL[STATUS_AT_ZERO_POSITION], "AT_ZERO_POSITION", "At zero position", IPS_OK); IUFillLight(&FlagsL[STATUS_AT_MAX_POSITION], "AT_MAX_POSITION", "At max. position", IPS_OK); IUFillLightVector(&FlagsLP, FlagsL, STATUS_NUM_FLAGS, getDeviceName(), "FLAGS", "Status Flags", MAIN_CONTROL_TAB, IPS_IDLE); IUFillNumber(&MaxPositionN[0], "MAXPOSITION", "Maximum position", "%6.0f", 1., 100000., 0., 1833); IUFillNumberVector(&MaxPositionNP, MaxPositionN, 1, getDeviceName(), "FOCUS_MAXPOSITION", "Max. position", OPTIONS_TAB, IP_RW, 0, IPS_IDLE); FocusRelPosN[0].min = 0.; FocusRelPosN[0].max = MaxPositionN[0].value; FocusRelPosN[0].value = 10; FocusRelPosN[0].step = 1; FocusAbsPosN[0].min = 0.; FocusAbsPosN[0].max = MaxPositionN[0].value; FocusAbsPosN[0].value = 0; FocusAbsPosN[0].step = 1; POLLMS = TimerInterval; return true; } bool SmartFocus::updateProperties() { INDI::Focuser::updateProperties(); if (isConnected()) { defineLight(&FlagsLP); defineNumber(&MaxPositionNP); SFgetState(); IDMessage(getDeviceName(), "SmartFocus focuser ready for use."); } else { deleteProperty(FlagsLP.name); deleteProperty(MaxPositionNP.name); } return true; } bool SmartFocus::Handshake() { if (isSimulation()) return true; if (!SFacknowledge()) { LOG_DEBUG("SmartFocus is not communicating."); return false; } LOG_DEBUG("SmartFocus is communicating."); return true; } const char *SmartFocus::getDefaultName() { return "SmartFocus"; } bool SmartFocus::ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) { if (dev != nullptr && strcmp(dev, getDeviceName()) == 0) { if (strcmp(name, MaxPositionNP.name) == 0) { if (values[0] > 0) { IUUpdateNumber(&MaxPositionNP, values, names, n); FocusAbsPosN[0].min = 0; FocusAbsPosN[0].max = MaxPositionN[0].value; IUUpdateMinMax(&FocusAbsPosNP); MaxPositionNP.s = IPS_OK; IDSetNumber(&MaxPositionNP, nullptr); return true; } else return false; } } return INDI::Focuser::ISNewNumber(dev, name, values, names, n); } //bool SmartFocus::ISNewSwitch (const char *dev, const char *name, ISState *states, char *names[], int n) { // return INDI::Focuser::ISNewSwitch(dev,name,states,names,n); //} bool SmartFocus::AbortFocuser() { bool result = true; if (!isSimulation() && SFisMoving()) { LOG_DEBUG("AbortFocuser: stopping motion"); result = send(&stop_focuser, sizeof(stop_focuser), "AbortFocuser"); // N.B.: The respons to this stop command will be captured in the TimerHit method! } return result; } IPState SmartFocus::MoveAbsFocuser(uint32_t targetPosition) { const Position destination = static_cast(targetPosition); IPState result = IPS_ALERT; if (isSimulation()) { position = destination; state = Idle; result = IPS_OK; } else { char command[3]; command[0] = goto_position; command[1] = ((destination >> 8) & 0xFF); command[2] = (destination & 0xFF); LOGF_DEBUG("MoveAbsFocuser: destination= %d", destination); tcflush(PortFD, TCIOFLUSH); if (send(command, sizeof(command), "MoveAbsFocuser")) { char respons; if (recv(&respons, sizeof(respons), "MoveAbsFocuser")) { LOGF_DEBUG("MoveAbsFocuser received echo: %c", respons); if (respons != goto_position) LOGF_ERROR("MoveAbsFocuser received unexpected respons: %c (0x02x)", respons, respons); else { state = MovingTo; result = IPS_BUSY; } } } } return result; } IPState SmartFocus::MoveRelFocuser(FocusDirection dir, uint32_t ticks) { return MoveAbsFocuser(position + (dir == FOCUS_INWARD ? -ticks : ticks)); } class NonBlockingIO { public: NonBlockingIO(const char *_device, const int _fd) : device(_device), fd(_fd), flags(fcntl(_fd, F_GETFL, 0)) { if (flags == -1) DEBUGFDEVICE(device, INDI::Logger::DBG_ERROR, "NonBlockingIO::NonBlockingIO() fcntl get error: errno=%d", errno); else if (fcntl(fd, F_SETFL, (flags | O_NONBLOCK)) == -1) DEBUGFDEVICE(device, INDI::Logger::DBG_ERROR, "NonBlockingIO::NonBlockingIO() fcntl set error: errno=%d", errno); } ~NonBlockingIO() { if (flags != -1 && fcntl(fd, F_SETFL, flags) == -1) DEBUGFDEVICE(device, INDI::Logger::DBG_ERROR, "NonBlockinIO::~NonBlockingIO() fcntl set error: errno=%d", errno); } private: const char *device; const int fd; const int flags; }; void SmartFocus::TimerHit() { // Wait for the end-of-motion character (c,r, or s) when the focuser is moving // due to a request from this driver. Otherwise, retrieve the current position // and state flags of the SmartFocus unit to keep the driver up-to-date with // motion commands issued manually. // TODO: What happens when the smartFocus unit is switched off? if (!isConnected()) return; if (!isSimulation() && SFisMoving()) { NonBlockingIO non_blocking(getDeviceName(), PortFD); // Automatically controls blocking IO by its scope char respons; if (read(PortFD, &respons, sizeof(respons)) == sizeof(respons)) { LOGF_DEBUG("TimerHit() received character: %c (0x%02x)", respons, respons); if (respons != motion_complete && respons != motion_error && respons != motion_stopped) LOGF_ERROR("TimerHit() received unexpected character: %c (0x%02x)", respons, respons); state = Idle; } } if (SFisIdle()) SFgetState(); timer_id = SetTimer(TimerInterval); } bool SmartFocus::saveConfigItems(FILE *fp) { IUSaveConfigNumber(fp, &MaxPositionNP); return true; } bool SmartFocus::SFacknowledge() { bool success = false; if (isSimulation()) success = true; else { tcflush(PortFD, TCIOFLUSH); if (send(&read_id_register, sizeof(read_id_register), "SFacknowledge")) { char respons[2]; if (recv(respons, sizeof(respons), "SFacknowledge", false)) { LOGF_DEBUG("SFacknowledge received: %c%c", respons[0], respons[1]); success = (respons[0] == read_id_register && respons[1] == read_id_respons); if (!success) LOGF_ERROR("SFacknowledge received unexpected respons: %c%c (0x02 0x02x)", respons[0], respons[1], respons[0], respons[1]); } } } return success; } SmartFocus::Position SmartFocus::SFgetPosition() { Position result = PositionInvalid; if (isSimulation()) result = position; else { tcflush(PortFD, TCIOFLUSH); if (send(&read_position, sizeof(read_position), "SFgetPosition")) { char respons[3]; if (recv(respons, sizeof(respons), "SFgetPosition")) { if (respons[0] == read_position) { result = (((static_cast(respons[1]) << 8) & 0xFF00) | (static_cast(respons[2]) & 0x00FF)); LOGF_DEBUG("SFgetPosition: position=%d", result); } else LOGF_ERROR("SFgetPosition received unexpected respons: %c (0x02x)", respons[0], respons[0]); } } } return result; } SmartFocus::Flags SmartFocus::SFgetFlags() { Flags result = 0x00; if (!isSimulation()) { tcflush(PortFD, TCIOFLUSH); if (send(&read_flags, sizeof(read_flags), "SFgetFlags")) { char respons[2]; if (recv(respons, sizeof(respons), "SFgetFlags")) { if (respons[0] == read_flags) { result = static_cast(respons[1]); LOGF_DEBUG("SFgetFlags: flags=0x%02x", result); } else LOGF_ERROR("SFgetFlags received unexpected respons: %c (0x02x)", respons[0], respons[0]); } } } return result; } void SmartFocus::SFgetState() { const Flags flags = SFgetFlags(); FlagsL[STATUS_SERIAL_FRAMING_ERROR].s = (flags & SerFramingError ? IPS_ALERT : IPS_OK); FlagsL[STATUS_SERIAL_OVERRUN_ERROR].s = (flags & SerOverrunError ? IPS_ALERT : IPS_OK); FlagsL[STATUS_MOTOR_ENCODE_ERROR].s = (flags & MotorEncoderError ? IPS_ALERT : IPS_OK); FlagsL[STATUS_AT_ZERO_POSITION].s = (flags & AtZeroPosition ? IPS_ALERT : IPS_OK); FlagsL[STATUS_AT_MAX_POSITION].s = (flags & AtMaxPosition ? IPS_ALERT : IPS_OK); IDSetLight(&FlagsLP, nullptr); if ((position = SFgetPosition()) == PositionInvalid) { FocusAbsPosNP.s = IPS_ALERT; IDSetNumber(&FocusAbsPosNP, "Error while reading SmartFocus position"); } else { FocusAbsPosN[0].value = position; FocusAbsPosNP.s = IPS_OK; IDSetNumber(&FocusAbsPosNP, nullptr); } } bool SmartFocus::send(const char *command, const size_t nbytes, const char *from, const bool log_error) { int nbytes_written = 0; const int rc = tty_write(PortFD, command, nbytes, &nbytes_written); const bool success = (rc == TTY_OK && nbytes_written == (int)nbytes); if (!success && log_error) { char errstr[MAXRBUF]; tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("%s: %s (%d of %d bytes written).", from, errstr, nbytes_written, nbytes); } return success; } bool SmartFocus::recv(char *respons, const size_t nbytes, const char *from, const bool log_error) { int nbytes_read = 0; const int rc = tty_read(PortFD, respons, nbytes, ReadTimeOut, &nbytes_read); const bool success = (rc == TTY_OK && nbytes_read == (int)nbytes); if (!success && log_error) { char errstr[MAXRBUF]; tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("%s: %s (%d of %d bytes read).", errstr, from, nbytes_read, nbytes); } return success; } libindi/drivers/focuser/tcfs.h0000664000175000017500000000517713263645557015740 0ustar jasemjasem/* INDI Driver for Optec TCF-S Focuser Copyright (C) 2010 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 */ #pragma once #include "indifocuser.h" #include #define TCFS_MAX_CMD 16 #define TCFS_ERROR_BUFFER 1024 class TCFS : public INDI::Focuser { public: enum TCFSCommand { FMMODE, // Focuser Manual Mode FFMODE, // Focuser Free Mode FAMODE, // Focuser Auto-A Mode FBMODE, // Focuser Auto-B Mode FCENTR, // Focus Center FIN, // Focuser In “nnnn†FOUT, // Focuser Out “nnnn†FPOSRO, // Focuser Position Read Out FTMPRO, // Focuser Temperature Read Out FSLEEP, // Focuser Sleep FWAKUP, // Focuser Wake Up FHOME, // Focuser Home Command }; enum TCFSError { NO_ERROR, ER_1, ER_2, ER_3 }; TCFS(); virtual ~TCFS() = default; // Standard INDI interface fucntions virtual bool Handshake(); virtual bool Disconnect(); const char *getDefaultName(); virtual bool initProperties(); virtual bool updateProperties(); virtual bool ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n); protected: virtual IPState MoveAbsFocuser(uint32_t targetTicks); virtual IPState MoveRelFocuser(FocusDirection dir, uint32_t ticks); virtual void TimerHit(); private: bool read_tcfs(char *response, bool silent = false); bool dispatch_command(TCFSCommand command_type); ISwitchVectorProperty *FocusPowerSP { nullptr }; ISwitchVectorProperty *FocusModeSP { nullptr }; ISwitchVectorProperty *FocusGotoSP { nullptr }; INumberVectorProperty *FocusTemperatureNP { nullptr }; unsigned int simulated_position { 3000 }; float simulated_temperature { 25.4 }; unsigned int targetTicks { 0 }; unsigned int targetPosition { 0 }; bool isTCFS3 { false }; }; libindi/drivers/focuser/sestosenso.cpp0000664000175000017500000003231113263645557017527 0ustar jasemjasem/* SestoSenso Focuser Copyright (C) 2013 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 */ #include "sestosenso.h" #include "indicom.h" #include #include #include #include #include #define SESTOSENSO_TIMEOUT 3 std::unique_ptr sesto(new SestoSenso()); void ISGetProperties(const char *dev) { sesto->ISGetProperties(dev); } void ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) { sesto->ISNewSwitch(dev, name, states, names, n); } void ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n) { sesto->ISNewText(dev, name, texts, names, n); } void ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) { sesto->ISNewNumber(dev, name, values, names, n); } void ISNewBLOB(const char *dev, const char *name, int sizes[], int blobsizes[], char *blobs[], char *formats[], char *names[], int n) { INDI_UNUSED(dev); INDI_UNUSED(name); INDI_UNUSED(sizes); INDI_UNUSED(blobsizes); INDI_UNUSED(blobs); INDI_UNUSED(formats); INDI_UNUSED(names); INDI_UNUSED(n); } void ISSnoopDevice(XMLEle *root) { sesto->ISSnoopDevice(root); } SestoSenso::SestoSenso() { // Can move in Absolute & Relative motions, can AbortFocuser motion. FI::SetCapability(FOCUSER_CAN_ABS_MOVE | FOCUSER_CAN_REL_MOVE | FOCUSER_CAN_ABORT); } bool SestoSenso::initProperties() { INDI::Focuser::initProperties(); // Firmware Information IUFillText(&FirmwareT[0], "VERSION", "Version", ""); IUFillTextVector(&FirmwareTP, FirmwareT, 1, getDeviceName(), "FOCUS_FIRMWARE", "Firmware", MAIN_CONTROL_TAB, IP_RO, 0, IPS_IDLE); // Focuser temperature IUFillNumber(&TemperatureN[0], "TEMPERATURE", "Celsius", "%6.2f", -50, 70., 0., 0.); IUFillNumberVector(&TemperatureNP, TemperatureN, 1, getDeviceName(), "FOCUS_TEMPERATURE", "Temperature", MAIN_CONTROL_TAB, IP_RO, 0, IPS_IDLE); // Sync IUFillNumber(&SyncN[0], "FOCUS_SYNC_OFFSET", "Offset", "%6.0f", 0, 60000., 0., 0.); IUFillNumberVector(&SyncNP, SyncN, 1, getDeviceName(), "FOCUS_SYNC", "Sync", MAIN_CONTROL_TAB, IP_RW, 0, IPS_IDLE); // Relative and absolute movement FocusRelPosN[0].min = 0.; FocusRelPosN[0].max = 50000.; FocusRelPosN[0].value = 0; FocusRelPosN[0].step = 1000; FocusAbsPosN[0].min = 0.; FocusAbsPosN[0].max = 100000.; FocusAbsPosN[0].value = 0; FocusAbsPosN[0].step = 1000; addAuxControls(); setDefaultPollingPeriod(500); return true; } bool SestoSenso::updateProperties() { INDI::Focuser::updateProperties(); if (isConnected()) { defineNumber(&SyncNP); defineNumber(&TemperatureNP); defineText(&FirmwareTP); LOG_INFO("SestoSenso paramaters updated, focuser ready for use."); GetFocusParams(); } else { deleteProperty(SyncNP.name); deleteProperty(TemperatureNP.name); deleteProperty(FirmwareTP.name); } return true; } bool SestoSenso::Handshake() { if (Ack()) { LOG_INFO("SestoSenso is online. Getting focus parameters..."); return true; } DEBUG(INDI::Logger::DBG_SESSION, "Error retreiving data from SestoSenso, please ensure SestoSenso controller is powered and the port is correct."); return false; } const char *SestoSenso::getDefaultName() { return "Sesto Senso"; } bool SestoSenso::Ack() { int nbytes_written = 0, nbytes_read = 0, rc = -1; char errstr[MAXRBUF]; char resp[16]={0}; LOG_DEBUG("CMD <#QF!>"); if (isSimulation()) { strncpy(resp, "1.0 Simulation", 16); nbytes_read = strlen(resp) + 1; } else { tcflush(PortFD, TCIOFLUSH); if ((rc = tty_write(PortFD, "#QF!", 4, &nbytes_written)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("%s error: %s.", __FUNCTION__, errstr); return false; } if ((rc = tty_read_section(PortFD, resp, 0xD, SESTOSENSO_TIMEOUT, &nbytes_read)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("%s error: %s.", __FUNCTION__, errstr); return false; } tcflush(PortFD, TCIOFLUSH); } resp[nbytes_read-1] = '\0'; LOGF_DEBUG("RES <%s>", resp); IUSaveText(&FirmwareT[0], resp); return true; } bool SestoSenso::updateTemperature() { int nbytes_written = 0, nbytes_read = 0, rc = -1; char errstr[MAXRBUF]; char resp[16]={0}; LOG_DEBUG("CMD <#QT!>"); if (isSimulation()) { strncpy(resp, "23.45", 16); nbytes_read = strlen(resp) + 1; } else { tcflush(PortFD, TCIOFLUSH); if ((rc = tty_write(PortFD, "#QT!", 4, &nbytes_written)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("%s error: %s.", __FUNCTION__, errstr); TemperatureNP.s = IPS_ALERT; return false; } if ((rc = tty_read_section(PortFD, resp, 0xD, SESTOSENSO_TIMEOUT, &nbytes_read)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("%s error: %s.", __FUNCTION__, errstr); TemperatureNP.s = IPS_ALERT; return false; } tcflush(PortFD, TCIOFLUSH); } resp[nbytes_read-1] = '\0'; LOGF_DEBUG("RES <%s>", resp); TemperatureN[0].value = atof(resp); TemperatureNP.s = (TemperatureN[0].value == 99.00) ? IPS_IDLE : IPS_OK; return true; } bool SestoSenso::updatePosition() { int nbytes_written = 0, nbytes_read = 0, rc = -1; char errstr[MAXRBUF]; char resp[16]={0}; LOG_DEBUG("CMD <#QP!>"); if (isSimulation()) { snprintf(resp, 16, "%d", static_cast(FocusAbsPosN[0].value)); nbytes_read = strlen(resp)+1; } else { tcflush(PortFD, TCIOFLUSH); if ((rc = tty_write(PortFD, "#QP!", 4, &nbytes_written)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("%s error: %s.", __FUNCTION__, errstr); FocusAbsPosNP.s = IPS_ALERT; return false; } if ((rc = tty_read_section(PortFD, resp, 0xD, SESTOSENSO_TIMEOUT, &nbytes_read)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("%s error: %s.", __FUNCTION__, errstr); FocusAbsPosNP.s = IPS_ALERT; return false; } tcflush(PortFD, TCIOFLUSH); } resp[nbytes_read-1] = '\0'; LOGF_DEBUG("RES <%s>", resp); FocusAbsPosN[0].value = atoi(resp); FocusAbsPosNP.s = IPS_OK; return true; } bool SestoSenso::isMotionComplete() { int nbytes_read = 0, rc = -1; char errstr[MAXRBUF]; char resp[16]={0}; if (isSimulation()) { int32_t nextPos = FocusAbsPosN[0].value; int32_t targPos = static_cast(targetPos); if (targPos > nextPos) nextPos += 250; else if (targPos < nextPos) nextPos -= 250; if (abs(nextPos-targPos) < 250) nextPos = targetPos; else if (nextPos < 0) nextPos = 0; else if (nextPos > FocusAbsPosN[0].max) nextPos = FocusAbsPosN[0].max; snprintf(resp, 16, "%d", nextPos); nbytes_read = strlen(resp)+1; } else { if ((rc = tty_read_section(PortFD, resp, 0xD, SESTOSENSO_TIMEOUT, &nbytes_read)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("%s error: %s.", __FUNCTION__, errstr); return false; } } resp[nbytes_read-1] = '\0'; if (!strcmp(resp, "GTok!")) return true; uint32_t newPos = atoi(resp); FocusAbsPosN[0].value = newPos; if (newPos == targetPos) return true; return false; } bool SestoSenso::sync(uint32_t newPosition) { int nbytes_written = 0, rc = -1; char errstr[MAXRBUF]; char cmd[16]={0}; snprintf(cmd, 16, "#SP%d!", newPosition); LOGF_DEBUG("CMD <%s>", cmd); if (isSimulation()) { FocusAbsPosN[0].value = newPosition; } else { tcflush(PortFD, TCIOFLUSH); // Sync if ((rc = tty_write(PortFD, cmd, strlen(cmd), &nbytes_written)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("%s error: %s.", __FUNCTION__, errstr); return false; } } return isCommandOK("SP"); } bool SestoSenso::ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) { if (dev != nullptr && strcmp(dev, getDeviceName()) == 0) { if (strcmp(name, SyncNP.name) == 0) { IUUpdateNumber(&SyncNP, values, names, n); if (sync(SyncN[0].value)) SyncNP.s = IPS_OK; else SyncNP.s = IPS_ALERT; IDSetNumber(&SyncNP, nullptr); return true; } } return INDI::Focuser::ISNewNumber(dev, name, values, names, n); } IPState SestoSenso::MoveAbsFocuser(uint32_t targetTicks) { targetPos = targetTicks; int nbytes_written = 0, rc = -1; char errstr[MAXRBUF]; char cmd[16]={0}; snprintf(cmd, 16, "#GT%d!", targetTicks); LOGF_DEBUG("CMD <%s>", cmd); if (isSimulation() == false) { if ((rc = tty_write(PortFD, cmd, strlen(cmd), &nbytes_written)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("%s error: %s.", __FUNCTION__, errstr); return IPS_ALERT; } } return IPS_BUSY; /*if (isCommandOK("GT")) { FocusAbsPosNP.s = IPS_BUSY; return IPS_BUSY; } return IPS_ALERT; */ } IPState SestoSenso::MoveRelFocuser(FocusDirection dir, uint32_t ticks) { double newPosition = 0; bool rc = false; if (dir == FOCUS_INWARD) newPosition = FocusAbsPosN[0].value - ticks; else newPosition = FocusAbsPosN[0].value + ticks; rc = MoveAbsFocuser(newPosition); if (!rc) return IPS_ALERT; FocusRelPosN[0].value = ticks; FocusRelPosNP.s = IPS_BUSY; return IPS_BUSY; } bool SestoSenso::AbortFocuser() { if (isSimulation()) return true; int nbytes_written; if (tty_write(PortFD, "#MA!", 4, &nbytes_written) == TTY_OK) { FocusAbsPosNP.s = IPS_IDLE; FocusRelPosNP.s = IPS_IDLE; IDSetNumber(&FocusAbsPosNP, nullptr); IDSetNumber(&FocusRelPosNP, nullptr); } return isCommandOK("MA"); } bool SestoSenso::isCommandOK(const char *cmd) { int nbytes_read = 0, rc = -1; char errstr[MAXRBUF], resp[16]; char expectedResp[16]; snprintf(expectedResp, 16, "%sok!", cmd); if (isSimulation()) { strncpy(resp, expectedResp, 16); nbytes_read = strlen(resp)+1; } else { if ((rc = tty_read_section(PortFD, resp, 0xD, SESTOSENSO_TIMEOUT, &nbytes_read)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("%s error for command %s: %s.", __FUNCTION__, cmd, errstr); return false; } } resp[nbytes_read-1] = '\0'; LOGF_DEBUG("RES <%s>", resp); return (!strcmp(resp, expectedResp)); } void SestoSenso::TimerHit() { if (!isConnected()) { SetTimer(POLLMS); return; } if (FocusAbsPosNP.s == IPS_BUSY || FocusRelPosNP.s == IPS_BUSY) { if (isMotionComplete()) { FocusAbsPosNP.s = IPS_OK; FocusRelPosNP.s = IPS_OK; IDSetNumber(&FocusRelPosNP, nullptr); IDSetNumber(&FocusAbsPosNP, nullptr); LOG_INFO("Focuser reached requested position."); } else IDSetNumber(&FocusAbsPosNP, nullptr); lastPos = FocusAbsPosN[0].value; SetTimer(POLLMS); return; } bool rc = updatePosition(); if (rc) { if (lastPos != FocusAbsPosN[0].value) { IDSetNumber(&FocusAbsPosNP, nullptr); lastPos = FocusAbsPosN[0].value; } } rc = updateTemperature(); if (rc) { if (fabs(lastTemperature - TemperatureN[0].value) >= 0.1) { IDSetNumber(&TemperatureNP, nullptr); lastTemperature = TemperatureN[0].value; } } SetTimer(POLLMS); } void SestoSenso::GetFocusParams() { if (updatePosition()) IDSetNumber(&FocusAbsPosNP, nullptr); if (updateTemperature()) IDSetNumber(&TemperatureNP, nullptr); } libindi/drivers/focuser/moonlite.h0000664000175000017500000000553313263645557016623 0ustar jasemjasem/* Moonlite Focuser Copyright (C) 2013 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 */ #pragma once #include "indifocuser.h" class MoonLite : public INDI::Focuser { public: MoonLite(); virtual ~MoonLite() = default; typedef enum { FOCUS_HALF_STEP, FOCUS_FULL_STEP } FocusStepMode; virtual bool Handshake(); const char *getDefaultName(); virtual bool initProperties(); virtual bool updateProperties(); virtual bool ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n); virtual bool ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n); virtual IPState MoveFocuser(FocusDirection dir, int speed, uint16_t duration); virtual IPState MoveAbsFocuser(uint32_t targetTicks); virtual IPState MoveRelFocuser(FocusDirection dir, uint32_t ticks); virtual bool SetFocuserSpeed(int speed); virtual bool AbortFocuser(); virtual void TimerHit(); private: void GetFocusParams(); bool sync(uint16_t offset); bool updateStepMode(); bool updateTemperature(); bool updatePosition(); bool updateSpeed(); bool isMoving(); bool Ack(); bool MoveFocuser(unsigned int position); bool setStepMode(FocusStepMode mode); bool setSpeed(unsigned short speed); bool setTemperatureCalibration(double calibration); bool setTemperatureCoefficient(double coefficient); bool setTemperatureCompensation(bool enable); float CalcTimeLeft(timeval, float); double targetPos { 0 }; double lastPos { 0 }; double lastTemperature { 0 }; unsigned int currentSpeed { 0 }; struct timeval focusMoveStart { 0, 0 }; float focusMoveRequest { 0 }; INumber TemperatureN[1]; INumberVectorProperty TemperatureNP; ISwitch StepModeS[2]; ISwitchVectorProperty StepModeSP; INumber MaxTravelN[1]; INumberVectorProperty MaxTravelNP; INumber TemperatureSettingN[2]; INumberVectorProperty TemperatureSettingNP; ISwitch TemperatureCompensateS[2]; ISwitchVectorProperty TemperatureCompensateSP; INumber SyncN[1]; INumberVectorProperty SyncNP; }; libindi/drivers/focuser/steeldrive.h0000664000175000017500000000753713263645557017151 0ustar jasemjasem/* Baader Steeldrive Focuser Copyright (C) 2014 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 */ #pragma once #include "indifocuser.h" class SteelDrive : public INDI::Focuser { public: SteelDrive(); virtual ~SteelDrive() = default; typedef struct { double maxTrip; double gearRatio; } FocusCustomSetting; typedef enum { FOCUS_HALF_STEP, FOCUS_FULL_STEP } FocusStepMode; enum { FOCUS_MAX_TRIP, FOCUS_GEAR_RATIO }; enum { FOCUS_T_COEFF, FOCUS_T_SAMPLES }; virtual bool Handshake(); const char *getDefaultName(); virtual bool initProperties(); virtual bool updateProperties(); virtual bool saveConfigItems(FILE *fp); virtual bool ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n); virtual bool ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n); virtual IPState MoveFocuser(FocusDirection dir, int speed, uint16_t duration); virtual IPState MoveAbsFocuser(uint32_t targetTicks); virtual IPState MoveRelFocuser(FocusDirection dir, unsigned int ticks); virtual bool SetFocuserSpeed(int speed); virtual bool AbortFocuser(); virtual void TimerHit(); void debugTriggered(bool enable); private: // Get functions bool updateVersion(); bool updateTemperature(); bool updateTemperatureSettings(); bool updatePosition(); bool updateSpeed(); bool updateAcceleration(); bool updateCustomSettings(); // Set functions bool setStepMode(FocusStepMode mode); bool setSpeed(unsigned short speed); bool setCustomSettings(double maxTrip, double gearRatio); bool setAcceleration(unsigned short accel); bool setTemperatureSamples(unsigned int targetSamples, unsigned int *finalSample); bool setTemperatureCompensation(); bool Sync(unsigned int position); // Motion functions bool moveFocuser(unsigned int position); bool stop(); bool startMotion(FocusDirection dir); // Misc functions bool saveFocuserConfig(); bool Ack(); void GetFocusParams(); float CalcTimeLeft(timeval, float); void updateFocusMaxRange(double maxTrip, double gearRatio); double targetPos { 0 }; double lastPos { 0 }; double lastTemperature { 0 }; double simPosition { 0 }; unsigned int currentSpeed { 0 }; unsigned int temperatureUpdateCounter { 0 }; bool sim { false }; struct timeval focusMoveStart { 0, 0 }; float focusMoveRequest { 0 }; INumber TemperatureN[1]; INumberVectorProperty TemperatureNP; INumber AccelerationN[1]; INumberVectorProperty AccelerationNP; INumber TemperatureSettingN[2]; INumberVectorProperty TemperatureSettingNP; ISwitch TemperatureCompensateS[2]; ISwitchVectorProperty TemperatureCompensateSP; ISwitch ModelS[5]; ISwitchVectorProperty ModelSP; INumber CustomSettingN[2]; INumberVectorProperty CustomSettingNP; INumber SyncN[1]; INumberVectorProperty SyncNP; IText VersionT[2] {}; ITextVectorProperty VersionTP; FocusCustomSetting fSettings[5]; }; libindi/drivers/focuser/hitecastrodcfocuser.h0000664000175000017500000000412013263645557021027 0ustar jasemjasem/******************************************************************************* Copyright(c) 2016 Andy Kirkham. All rights reserved. HitecAstroDCFocuser Focuser This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2 as published by the Free Software Foundation. . 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 Library General Public License for more details. . You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. *******************************************************************************/ #pragma once #include "hidapi.h" #include "indifocuser.h" #include "indiusbdevice.h" class HitecAstroDCFocuser : public INDI::Focuser, public INDI::USBDevice { public: typedef enum { IDLE, SLEWING } STATE; HitecAstroDCFocuser(); virtual ~HitecAstroDCFocuser(); virtual bool ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n); virtual bool ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n); const char *getDefaultName(); virtual bool initProperties(); virtual bool updateProperties(); virtual bool saveConfigItems(FILE *fp); bool Connect(); bool Disconnect(); void TimerHit(); virtual IPState MoveRelFocuser(FocusDirection dir, uint32_t ticks); virtual IPState MoveFocuser(FocusDirection dir, int speed, uint16_t duration); private: hid_device *_handle; bool sim; char _stop; STATE _state; uint16_t _duration; INumber MaxPositionN[1]; INumberVectorProperty MaxPositionNP; INumber SlewSpeedN[1]; INumberVectorProperty SlewSpeedNP; ISwitch ReverseDirectionS[1]; ISwitchVectorProperty ReverseDirectionSP; }; libindi/drivers/focuser/perfectstar.cpp0000664000175000017500000003634113263645557017653 0ustar jasemjasem/******************************************************************************* Copyright(c) 2015 Jasem Mutlaq. All rights reserved. PerfectStar Focuser This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2 as published by the Free Software Foundation. . 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 Library General Public License for more details. . You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. *******************************************************************************/ #include "perfectstar.h" #include #include #include #define PERFECTSTAR_TIMEOUT 1000 /* 1000 ms */ #define FOCUS_SETTINGS_TAB "Settings" // We declare an auto pointer to PerfectStar. std::unique_ptr perfectStar(new PerfectStar()); void ISPoll(void *p); void ISGetProperties(const char *dev) { perfectStar->ISGetProperties(dev); } void ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) { perfectStar->ISNewSwitch(dev, name, states, names, n); } void ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n) { perfectStar->ISNewText(dev, name, texts, names, n); } void ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) { perfectStar->ISNewNumber(dev, name, values, names, n); } void ISNewBLOB(const char *dev, const char *name, int sizes[], int blobsizes[], char *blobs[], char *formats[], char *names[], int n) { INDI_UNUSED(dev); INDI_UNUSED(name); INDI_UNUSED(sizes); INDI_UNUSED(blobsizes); INDI_UNUSED(blobs); INDI_UNUSED(formats); INDI_UNUSED(names); INDI_UNUSED(n); } void ISSnoopDevice(XMLEle *root) { perfectStar->ISSnoopDevice(root); } PerfectStar::PerfectStar() { FI::SetCapability(FOCUSER_CAN_ABS_MOVE | FOCUSER_CAN_REL_MOVE | FOCUSER_CAN_ABORT); setConnection(CONNECTION_NONE); } bool PerfectStar::Connect() { sim = isSimulation(); if (sim) { SetTimer(POLLMS); return true; } handle = hid_open(0x04D8, 0xF812, nullptr); if (handle == nullptr) { LOG_ERROR("No PerfectStar focuser found."); return false; } else SetTimer(POLLMS); return (handle != nullptr); } bool PerfectStar::Disconnect() { if (!sim) { hid_close(handle); hid_exit(); } return true; } const char *PerfectStar::getDefaultName() { return (const char *)"PerfectStar"; } bool PerfectStar::initProperties() { INDI::Focuser::initProperties(); // Max Position IUFillNumber(&MaxPositionN[0], "Steps", "", "%.f", 0, 500000, 0., 10000); IUFillNumberVector(&MaxPositionNP, MaxPositionN, 1, getDeviceName(), "Max Position", "", FOCUS_SETTINGS_TAB, IP_RW, 0, IPS_IDLE); // Sync to a particular position IUFillNumber(&SyncN[0], "Ticks", "", "%.f", 0, 100000, 100., 0.); IUFillNumberVector(&SyncNP, SyncN, 1, getDeviceName(), "Sync", "", MAIN_CONTROL_TAB, IP_RW, 0, IPS_IDLE); FocusAbsPosN[0].min = SyncN[0].min = 0; FocusAbsPosN[0].max = SyncN[0].max = MaxPositionN[0].value; FocusAbsPosN[0].step = SyncN[0].step = MaxPositionN[0].value / 50.0; FocusAbsPosN[0].value = 0; FocusRelPosN[0].max = (FocusAbsPosN[0].max - FocusAbsPosN[0].min) / 2; FocusRelPosN[0].step = FocusRelPosN[0].max / 100.0; FocusRelPosN[0].value = 100; addSimulationControl(); return true; } bool PerfectStar::updateProperties() { INDI::Focuser::updateProperties(); if (isConnected()) { defineNumber(&SyncNP); defineNumber(&MaxPositionNP); } else { deleteProperty(SyncNP.name); deleteProperty(MaxPositionNP.name); } return true; } void PerfectStar::TimerHit() { if (!isConnected()) return; uint32_t currentTicks = 0; bool rc = getPosition(¤tTicks); if (rc) FocusAbsPosN[0].value = currentTicks; getStatus(&status); if (FocusAbsPosNP.s == IPS_BUSY || FocusRelPosNP.s == IPS_BUSY) { if (sim) { if (FocusAbsPosN[0].value < targetPosition) simPosition += 500; else simPosition -= 500; if (std::abs((int64_t)simPosition - (int64_t)targetPosition) < 500) { FocusAbsPosN[0].value = targetPosition; simPosition = FocusAbsPosN[0].value; status = PS_NOOP; } FocusAbsPosN[0].value = simPosition; } if (status == PS_HALT && targetPosition == FocusAbsPosN[0].value) { if (FocusRelPosNP.s == IPS_BUSY) { FocusRelPosNP.s = IPS_OK; IDSetNumber(&FocusRelPosNP, nullptr); } FocusAbsPosNP.s = IPS_OK; LOG_DEBUG("Focuser reached target position."); } else if (status == PS_NOOP) { if (FocusRelPosNP.s == IPS_BUSY) { FocusRelPosNP.s = IPS_OK; IDSetNumber(&FocusRelPosNP, nullptr); } FocusAbsPosNP.s = IPS_OK; LOG_INFO("Focuser reached home position."); } } IDSetNumber(&FocusAbsPosNP, nullptr); SetTimer(POLLMS); } bool PerfectStar::ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) { if (dev != nullptr && strcmp(dev, getDeviceName()) == 0) { // Max Travel if (strcmp(MaxPositionNP.name, name) == 0) { IUUpdateNumber(&MaxPositionNP, values, names, n); if (MaxPositionN[0].value > 0) { FocusAbsPosN[0].min = SyncN[0].min = 0; FocusAbsPosN[0].max = SyncN[0].max = MaxPositionN[0].value; FocusAbsPosN[0].step = SyncN[0].step = MaxPositionN[0].value / 50.0; FocusRelPosN[0].max = (FocusAbsPosN[0].max - FocusAbsPosN[0].min) / 2; FocusRelPosN[0].step = FocusRelPosN[0].max / 100.0; FocusRelPosN[0].min = 0; IUUpdateMinMax(&FocusAbsPosNP); IUUpdateMinMax(&FocusRelPosNP); IUUpdateMinMax(&SyncNP); LOGF_INFO("Focuser absolute limits: min (%g) max (%g)", FocusAbsPosN[0].min, FocusAbsPosN[0].max); } MaxPositionNP.s = IPS_OK; IDSetNumber(&MaxPositionNP, nullptr); return true; } // Sync if (strcmp(SyncNP.name, name) == 0) { IUUpdateNumber(&SyncNP, values, names, n); if (!sync(SyncN[0].value)) SyncNP.s = IPS_ALERT; else SyncNP.s = IPS_OK; IDSetNumber(&SyncNP, nullptr); return true; } } return INDI::Focuser::ISNewNumber(dev, name, values, names, n); } IPState PerfectStar::MoveAbsFocuser(uint32_t targetTicks) { bool rc = setPosition(targetTicks); if (!rc) return IPS_ALERT; targetPosition = targetTicks; rc = setStatus(PS_GOTO); if (!rc) return IPS_ALERT; FocusAbsPosNP.s = IPS_BUSY; return IPS_BUSY; } IPState PerfectStar::MoveRelFocuser(FocusDirection dir, uint32_t ticks) { uint32_t finalTicks = FocusAbsPosN[0].value + (ticks * (dir == FOCUS_INWARD ? -1 : 1)); return MoveAbsFocuser(finalTicks); } bool PerfectStar::setPosition(uint32_t ticks) { int rc = 0; unsigned char command[3]; unsigned char response[3]; // 20 bit resolution position. 4 high bits + 16 lower bits // Send 4 high bits first command[0] = 0x28; command[1] = (ticks & 0x40000) >> 16; LOGF_DEBUG("Set Position (%ld)", ticks); LOGF_DEBUG("CMD (%02X %02X)", command[0], command[1]); if (sim) rc = 2; else rc = hid_write(handle, command, 2); if (rc < 0) { LOGF_ERROR("setPosition: Error writing to device (%s)", hid_error(handle)); return false; } if (sim) { rc = 2; response[0] = 0x28; response[1] = command[1]; } else rc = hid_read_timeout(handle, response, 2, PERFECTSTAR_TIMEOUT); if (rc < 0) { LOGF_ERROR("setPosition: Error reading from device (%s)", hid_error(handle)); return false; } LOGF_DEBUG("RES (%02X %02X)", response[0], response[1]); // Send lower 16 bit command[0] = 0x20; // Low Byte command[1] = ticks & 0xFF; // High Byte command[2] = (ticks & 0xFF00) >> 8; LOGF_DEBUG("CMD (%02X %02X %02X)", command[0], command[1], command[2]); if (sim) rc = 3; else rc = hid_write(handle, command, 3); if (rc < 0) { LOGF_ERROR("setPosition: Error writing to device (%s)", hid_error(handle)); return false; } if (sim) { rc = 3; response[0] = command[0]; response[1] = command[1]; response[2] = command[2]; } else rc = hid_read_timeout(handle, response, 3, PERFECTSTAR_TIMEOUT); if (rc < 0) { LOGF_ERROR("setPosition: Error reading from device (%s)", hid_error(handle)); return false; } LOGF_DEBUG("RES (%02X %02X %02X)", response[0], response[1], response[2]); targetPosition = ticks; // TODO add checking later return true; } bool PerfectStar::getPosition(uint32_t *ticks) { int rc = 0; uint32_t pos = 0; unsigned char command[1]; unsigned char response[3]; // 20 bit resolution position. 4 high bits + 16 lower bits // Get 4 high bits first command[0] = 0x29; LOG_DEBUG("Get Position (High 4 bits)"); LOGF_DEBUG("CMD (%02X)", command[0]); if (sim) rc = 2; else rc = hid_write(handle, command, 1); if (rc < 0) { LOGF_ERROR("getPosition: Error writing to device (%s)", hid_error(handle)); return false; } if (sim) { rc = 2; response[0] = command[0]; response[1] = simPosition >> 16; } else rc = hid_read_timeout(handle, response, 2, PERFECTSTAR_TIMEOUT); if (rc < 0) { LOGF_ERROR("getPosition: Error reading from device (%s)", hid_error(handle)); return false; } LOGF_DEBUG("RES (%02X %02X)", response[0], response[1]); // Store 4 high bits part of a 20 bit number pos = response[1] << 16; // Get 16 lower bits command[0] = 0x21; LOG_DEBUG("Get Position (Lower 16 bits)"); LOGF_DEBUG("CMD (%02X)", command[0]); if (sim) rc = 1; else rc = hid_write(handle, command, 1); if (rc < 0) { LOGF_ERROR("getPosition: Error writing to device (%s)", hid_error(handle)); return false; } if (sim) { rc = 3; response[0] = command[0]; response[1] = simPosition & 0xFF; response[2] = (simPosition & 0xFF00) >> 8; } else rc = hid_read_timeout(handle, response, 3, PERFECTSTAR_TIMEOUT); if (rc < 0) { LOGF_ERROR("getPosition: Error reading from device (%s)", hid_error(handle)); return false; } LOGF_DEBUG("RES (%02X %02X %02X)", response[0], response[1], response[2]); // Res[1] is lower byte and Res[2] is high byte. Combine them and add them to ticks. pos |= response[1] | response[2] << 8; *ticks = pos; LOGF_DEBUG("Position: %ld", pos); return true; } bool PerfectStar::setStatus(PS_STATUS targetStatus) { int rc = 0; unsigned char command[2]; unsigned char response[3]; command[0] = 0x10; command[1] = (targetStatus == PS_HALT) ? 0xFF : targetStatus; LOGF_DEBUG("CMD (%02X %02X)", command[0], command[1]); if (sim) rc = 2; else rc = hid_write(handle, command, 2); if (rc < 0) { LOGF_ERROR("setStatus: Error writing to device (%s)", hid_error(handle)); return false; } if (sim) { rc = 3; response[0] = command[0]; response[1] = 0; response[2] = command[1]; status = targetStatus; // Convert Goto to either "moving in" or "moving out" status if (status == PS_GOTO) { // Moving in state if (targetPosition < FocusAbsPosN[0].value) status = PS_IN; else // Moving out state status = PS_OUT; } } else rc = hid_read_timeout(handle, response, 3, PERFECTSTAR_TIMEOUT); if (rc < 0) { LOGF_ERROR("setStatus: Error reading from device (%s)", hid_error(handle)); return false; } LOGF_DEBUG("RES (%02X %02X %02X)", response[0], response[1], response[2]); if (response[1] == 0xFF) { LOG_ERROR("setStatus: Invalid state change."); return false; } return true; } bool PerfectStar::getStatus(PS_STATUS *currentStatus) { int rc = 0; unsigned char command[1]; unsigned char response[2]; command[0] = 0x11; LOGF_DEBUG("CMD (%02X)", command[0]); if (sim) rc = 1; else rc = hid_write(handle, command, 1); if (rc < 0) { LOGF_ERROR("getStatus: Error writing to device (%s)", hid_error(handle)); return false; } if (sim) { rc = 2; response[0] = command[0]; response[1] = status; // Halt/SetPos is state = 0 "not moving". if (response[1] == PS_HALT || response[1] == PS_SETPOS) response[1] = 0; } else rc = hid_read_timeout(handle, response, 2, PERFECTSTAR_TIMEOUT); if (rc < 0) { LOGF_ERROR("getStatus: Error reading from device (%s)", hid_error(handle)); return false; } LOGF_DEBUG("RES (%02X %02X)", response[0], response[1]); switch (response[1]) { case 0: *currentStatus = PS_HALT; LOG_DEBUG("State: Not moving."); break; case 1: *currentStatus = PS_IN; LOG_DEBUG("State: Moving in."); break; case 3: *currentStatus = PS_GOTO; LOG_DEBUG("State: Goto."); break; case 2: *currentStatus = PS_OUT; LOG_DEBUG("State: Moving out."); break; case 5: *currentStatus = PS_LOCKED; LOG_DEBUG("State: Locked."); break; default: LOGF_WARN("Warning: Unknown status (%d)", response[1]); return false; break; } return true; } bool PerfectStar::AbortFocuser() { return setStatus(PS_HALT); } bool PerfectStar::sync(uint32_t ticks) { bool rc = setPosition(ticks); if (!rc) return false; simPosition = ticks; rc = setStatus(PS_SETPOS); return rc; } bool PerfectStar::saveConfigItems(FILE *fp) { INDI::Focuser::saveConfigItems(fp); IUSaveConfigNumber(fp, &MaxPositionNP); return true; } libindi/drivers/focuser/microtouch.cpp0000664000175000017500000005565113263645557017512 0ustar jasemjasem/* Microtouch Focuser Copyright (C) 2016 Marco Peters (mpeters@rzpeters.de) Copyright (C) 2013 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 */ #include "microtouch.h" #include "indicom.h" #include "connectionplugins/connectionserial.h" #include #include #include #include #include #define MICROTOUCH_TIMEOUT 3 std::unique_ptr microTouch(new Microtouch()); void ISGetProperties(const char *dev) { microTouch->ISGetProperties(dev); } void ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) { microTouch->ISNewSwitch(dev, name, states, names, n); } void ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n) { microTouch->ISNewText(dev, name, texts, names, n); } void ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) { microTouch->ISNewNumber(dev, name, values, names, n); } void ISNewBLOB(const char *dev, const char *name, int sizes[], int blobsizes[], char *blobs[], char *formats[], char *names[], int n) { INDI_UNUSED(dev); INDI_UNUSED(name); INDI_UNUSED(sizes); INDI_UNUSED(blobsizes); INDI_UNUSED(blobs); INDI_UNUSED(formats); INDI_UNUSED(names); INDI_UNUSED(n); } void ISSnoopDevice(XMLEle *root) { microTouch->ISSnoopDevice(root); } Microtouch::Microtouch() { // Can move in Absolute & Relative motions, can AbortFocuser motion, and has variable speed. FI::SetCapability(FOCUSER_CAN_ABS_MOVE | FOCUSER_CAN_REL_MOVE | FOCUSER_CAN_ABORT /*| FOCUSER_HAS_VARIABLE_SPEED*/); } bool Microtouch::initProperties() { INDI::Focuser::initProperties(); FocusSpeedN[0].min = 1; FocusSpeedN[0].max = 5; FocusSpeedN[0].value = 1; /* Step Mode */ IUFillSwitch(&MotorSpeedS[0], "Normal", "", ISS_ON); IUFillSwitch(&MotorSpeedS[1], "Fast", "", ISS_OFF); IUFillSwitchVector(&MotorSpeedSP, MotorSpeedS, 2, getDeviceName(), "Motor Speed", "", OPTIONS_TAB, IP_RW, ISR_1OFMANY, 0, IPS_IDLE); /* Focuser temperature */ IUFillNumber(&TemperatureN[0], "TEMPERATURE", "Celsius", "%6.2f", -50, 70., 0., 0.); IUFillNumberVector(&TemperatureNP, TemperatureN, 1, getDeviceName(), "FOCUS_TEMPERATURE", "Temperature", MAIN_CONTROL_TAB, IP_RO, 0, IPS_IDLE); // Maximum Travel IUFillNumber(&MaxTravelN[0], "MAXTRAVEL", "Maximum travel", "%6.0f", 1., 60000., 0., 10000.); IUFillNumberVector(&MaxTravelNP, MaxTravelN, 1, getDeviceName(), "FOCUS_MAXTRAVEL", "Max. travel", OPTIONS_TAB, IP_RW, 0, IPS_IDLE); // Temperature Settings IUFillNumber(&TemperatureSettingN[0], "Calibration", "", "%6.2f", -20, 20, 0.01, 0); IUFillNumber(&TemperatureSettingN[1], "Coefficient", "", "%6.2f", -20, 20, 0.01, 0); IUFillNumberVector(&TemperatureSettingNP, TemperatureSettingN, 2, getDeviceName(), "Temperature Settings", "", OPTIONS_TAB, IP_RW, 0, IPS_IDLE); // Compensate for temperature IUFillSwitch(&TemperatureCompensateS[0], "Enable", "", ISS_OFF); IUFillSwitch(&TemperatureCompensateS[1], "Disable", "", ISS_ON); IUFillSwitchVector(&TemperatureCompensateSP, TemperatureCompensateS, 2, getDeviceName(), "Temperature Compensate", "", MAIN_CONTROL_TAB, IP_RW, ISR_1OFMANY, 0, IPS_IDLE); // Reset IUFillSwitch(&ResetS[0], "Zero", "", ISS_OFF); IUFillSwitchVector(&ResetSP, ResetS, 1, getDeviceName(), "Reset", "", MAIN_CONTROL_TAB, IP_RW, ISR_1OFMANY, 0, IPS_IDLE); IUFillNumber(&ResetToPosN[0], "Position", "", "%6.0f", 0, 60000, 1, 0); IUFillNumberVector(&ResetToPosNP, ResetToPosN, 1, getDeviceName(), "Reset to Position", "", MAIN_CONTROL_TAB, IP_RW, 0, IPS_IDLE); /* Relative and absolute movement */ FocusRelPosN[0].min = 0.; FocusRelPosN[0].max = 30000.; FocusRelPosN[0].value = 0; FocusRelPosN[0].step = 1000.; FocusAbsPosN[0].min = 0.; FocusAbsPosN[0].max = 60000.; FocusAbsPosN[0].value = 0; FocusAbsPosN[0].step = 1000.; addDebugControl(); serialConnection->setDefaultBaudRate(Connection::Serial::B_19200); return true; } bool Microtouch::updateProperties() { INDI::Focuser::updateProperties(); if (isConnected()) { defineNumber(&TemperatureNP); defineNumber(&MaxTravelNP); defineSwitch(&MotorSpeedSP); defineNumber(&TemperatureSettingNP); defineSwitch(&TemperatureCompensateSP); defineSwitch(&ResetSP); defineNumber(&ResetToPosNP); GetFocusParams(); LOG_INFO("Microtouch paramaters updated, focuser ready for use."); } else { deleteProperty(TemperatureNP.name); deleteProperty(MaxTravelNP.name); deleteProperty(MotorSpeedSP.name); deleteProperty(TemperatureSettingNP.name); deleteProperty(TemperatureCompensateSP.name); deleteProperty(ResetSP.name); deleteProperty(ResetToPosNP.name); } return true; } bool Microtouch::Handshake() { tcflush(PortFD, TCIOFLUSH); if (Ack()) { LOG_INFO("Microtouch is online. Getting focus parameters..."); return true; } LOG_INFO("Error retreiving data from Microtouch, please ensure Microtouch controller is " "powered and the port is correct."); return false; } const char *Microtouch::getDefaultName() { return "Microtouch"; } bool Microtouch::Ack() { signed short int pos = WriteCmdGetShortInt(CMD_GET_POSITION); if (pos > -1) { FocusAbsPosN[0].value = pos; return true; } else return false; } bool Microtouch::updateTemperature() { char resp[7]; short int ttemp = 0, tcoeff = 0; double raw_temp = 0, raw_coeff = 0, tcomp_coeff = 0; if (!(WriteCmdGetResponse(CMD_GET_TEMPERATURE, resp, 6))) return false; ttemp = ((short int)resp[1] << 8 | ((short int)resp[2] & 0xff)); raw_temp = ((double)ttemp) / 16; tcoeff = ((short int)resp[5] << 8 | ((short int)resp[4] & 0xff)); raw_coeff = ((double)tcoeff) / 16; tcomp_coeff = (double)(((double)WriteCmdGetInt(CMD_GET_COEFF)) / 128); LOGF_DEBUG("updateTemperature : RESP (%02X %02X %02X %02X %02X %02X)", resp[0], resp[1], resp[2], resp[3], resp[4], resp[5]); TemperatureN[0].value = raw_temp + raw_coeff; TemperatureSettingN[0].value = raw_coeff; TemperatureSettingN[1].value = tcomp_coeff; return true; } bool Microtouch::updatePosition() { int pos = (int)WriteCmdGetShortInt(CMD_GET_POSITION); if (pos >= 0) { FocusAbsPosN[0].value = (double)pos; return true; } else return false; } bool Microtouch::updateSpeed() { // TODO /* int nbytes_written=0, nbytes_read=0, rc=-1; char errstr[MAXRBUF]; char resp[3]; short speed; tcflush(PortFD, TCIOFLUSH); if ( (rc = tty_write(PortFD, ":GD#", 4, &nbytes_written)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("updateSpeed error: %s.", errstr); return false; } if ( (rc = tty_read(PortFD, resp, 3, MICROTOUCH_TIMEOUT, &nbytes_read)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("updateSpeed error: %s.", errstr); return false; } rc = sscanf(resp, "%hX#", &speed); if (rc > 0) { int focus_speed=-1; while (speed > 0) { speed >>= 1; focus_speed++; } currentSpeed = focus_speed; FocusSpeedN[0].value = focus_speed; } else { LOGF_ERROR("Unknown error: focuser speed value (%s)", resp); return false; } */ return true; } bool Microtouch::updateMotorSpeed() { IUResetSwitch(&MotorSpeedSP); LOGF_DEBUG("MotorSpeed: %d.", WriteCmdGetByte(CMD_GET_MOTOR_SPEED)); if (WriteCmdGetByte(CMD_GET_MOTOR_SPEED) == 8) MotorSpeedS[0].s = ISS_ON; else if (WriteCmdGetByte(CMD_GET_MOTOR_SPEED) == 4) MotorSpeedS[1].s = ISS_ON; else { LOGF_ERROR("Unknown error: updateMotorSpeed (%s)", WriteCmdGetByte(CMD_GET_MOTOR_SPEED)); return false; } return true; } bool Microtouch::isMoving() { return (WriteCmdGetByte(CMD_IS_MOVING) > 0); } bool Microtouch::setTemperatureCalibration(double calibration) { return WriteCmdSetShortInt(CMD_SET_TEMP_OFFSET, (short int)(calibration * 16)); } bool Microtouch::setTemperatureCoefficient(double coefficient) { int tcoeff = (int)(coefficient * 128); LOGF_DEBUG("Setting new temperaturecoefficient : %d.", tcoeff); if (!(WriteCmdSetInt(CMD_SET_COEFF, tcoeff))) { LOG_ERROR("setTemperatureCoefficient error: Setting temperaturecoefficient failed."); return false; } return true; } bool Microtouch::reset() { WriteCmdSetIntAsDigits(CMD_RESET_POSITION, 0x00); return true; } bool Microtouch::reset(double pos) { WriteCmdSetIntAsDigits(CMD_RESET_POSITION, (int)pos); return true; } bool Microtouch::MoveFocuser(unsigned int position) { LOGF_DEBUG("MoveFocuser to Position: %d", position); if (position < FocusAbsPosN[0].min || position > FocusAbsPosN[0].max) { LOGF_ERROR("Requested position value out of bound: %d", position); return false; } return WriteCmdSetIntAsDigits(CMD_UPDATE_POSITION, position); } bool Microtouch::setMotorSpeed(char speed) { if (speed == FOCUS_MOTORSPEED_NORMAL) WriteCmdSetByte(CMD_SET_MOTOR_SPEED, 8); else WriteCmdSetByte(CMD_SET_MOTOR_SPEED, 4); return true; } bool Microtouch::setSpeed(unsigned short speed) { INDI_UNUSED(speed); /* int nbytes_written=0, rc=-1; char errstr[MAXRBUF]; char cmd[7]; int hex_value=1; hex_value <<= speed; snprintf(cmd, 7, ":SD%02X#", hex_value); if ( (rc = tty_write(PortFD, cmd, 6, &nbytes_written)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("setSpeed error: %s.", errstr); return false; } */ return true; } bool Microtouch::setTemperatureCompensation(bool enable) { if (enable) { if (WriteCmd(CMD_TEMPCOMP_ON)) return true; else return false; } else if (WriteCmd(CMD_TEMPCOMP_OFF)) return true; return false; } bool Microtouch::ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) { if (dev != nullptr && strcmp(dev, getDeviceName()) == 0) { // Focus Motor Speed if (strcmp(MotorSpeedSP.name, name) == 0) { bool rc = false; int current_mode = IUFindOnSwitchIndex(&MotorSpeedSP); IUUpdateSwitch(&MotorSpeedSP, states, names, n); int target_mode = IUFindOnSwitchIndex(&MotorSpeedSP); if (current_mode == target_mode) { MotorSpeedSP.s = IPS_OK; IDSetSwitch(&MotorSpeedSP, nullptr); } if (target_mode == 0) rc = setMotorSpeed(FOCUS_MOTORSPEED_NORMAL); else rc = setMotorSpeed(FOCUS_MOTORSPEED_FAST); if (!rc) { IUResetSwitch(&MotorSpeedSP); MotorSpeedS[current_mode].s = ISS_ON; MotorSpeedSP.s = IPS_ALERT; IDSetSwitch(&MotorSpeedSP, nullptr); return false; } MotorSpeedSP.s = IPS_OK; IDSetSwitch(&MotorSpeedSP, nullptr); return true; } if (strcmp(TemperatureCompensateSP.name, name) == 0) { int last_index = IUFindOnSwitchIndex(&TemperatureCompensateSP); IUUpdateSwitch(&TemperatureCompensateSP, states, names, n); bool rc = setTemperatureCompensation((TemperatureCompensateS[0].s == ISS_ON)); if (!rc) { TemperatureCompensateSP.s = IPS_ALERT; IUResetSwitch(&TemperatureCompensateSP); TemperatureCompensateS[last_index].s = ISS_ON; IDSetSwitch(&TemperatureCompensateSP, nullptr); return false; } TemperatureCompensateSP.s = IPS_OK; IDSetSwitch(&TemperatureCompensateSP, nullptr); return true; } if (strcmp(ResetSP.name, name) == 0) { IUResetSwitch(&ResetSP); if (reset()) ResetSP.s = IPS_OK; else ResetSP.s = IPS_ALERT; IDSetSwitch(&ResetSP, nullptr); return true; } } return INDI::Focuser::ISNewSwitch(dev, name, states, names, n); } bool Microtouch::ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) { if (dev != nullptr && strcmp(dev, getDeviceName()) == 0) { if (strcmp(name, MaxTravelNP.name) == 0) { IUUpdateNumber(&MaxTravelNP, values, names, n); MaxTravelNP.s = IPS_OK; IDSetNumber(&MaxTravelNP, nullptr); return true; } if (strcmp(name, TemperatureSettingNP.name) == 0) { IUUpdateNumber(&TemperatureSettingNP, values, names, n); if (!setTemperatureCalibration(TemperatureSettingN[0].value) || !setTemperatureCoefficient(TemperatureSettingN[1].value)) { TemperatureSettingNP.s = IPS_ALERT; IDSetNumber(&TemperatureSettingNP, nullptr); return false; } TemperatureSettingNP.s = IPS_OK; IDSetNumber(&TemperatureSettingNP, nullptr); } if (strcmp(name, ResetToPosNP.name) == 0) { IUUpdateNumber(&ResetToPosNP, values, names, n); if (!reset(ResetToPosN[0].value)) { ResetToPosNP.s = IPS_ALERT; IDSetNumber(&ResetToPosNP, nullptr); return false; } ResetToPosNP.s = IPS_OK; IDSetNumber(&ResetToPosNP, nullptr); } } return INDI::Focuser::ISNewNumber(dev, name, values, names, n); } void Microtouch::GetFocusParams() { if (updatePosition()) IDSetNumber(&FocusAbsPosNP, nullptr); if (updateTemperature()) { IDSetNumber(&TemperatureNP, nullptr); IDSetNumber(&TemperatureSettingNP, nullptr); } /* if (updateSpeed()) IDSetNumber(&FocusSpeedNP, nullptr); */ if (updateMotorSpeed()) IDSetSwitch(&MotorSpeedSP, nullptr); } bool Microtouch::SetFocuserSpeed(int speed) { bool rc = false; rc = setSpeed(speed); if (!rc) return false; currentSpeed = speed; FocusSpeedNP.s = IPS_OK; IDSetNumber(&FocusSpeedNP, nullptr); return true; } IPState Microtouch::MoveFocuser(FocusDirection dir, int speed, uint16_t duration) { if (speed != (int)currentSpeed) { bool rc = setSpeed(speed); if (!rc) return IPS_ALERT; } gettimeofday(&focusMoveStart, nullptr); focusMoveRequest = duration / 1000.0; if (dir == FOCUS_INWARD) MoveFocuser(0); else MoveFocuser(FocusAbsPosN[0].value + MaxTravelN[0].value - 1); if (duration <= POLLMS) { usleep(duration * 1000); AbortFocuser(); return IPS_OK; } return IPS_BUSY; } IPState Microtouch::MoveAbsFocuser(uint32_t targetTicks) { targetPos = targetTicks; bool rc = false; rc = MoveFocuser(targetPos); if (!rc) return IPS_ALERT; FocusAbsPosNP.s = IPS_BUSY; return IPS_BUSY; } IPState Microtouch::MoveRelFocuser(FocusDirection dir, uint32_t ticks) { double newPosition = 0; bool rc = false; if (dir == FOCUS_INWARD) newPosition = FocusAbsPosN[0].value - ticks; else newPosition = FocusAbsPosN[0].value + ticks; rc = MoveFocuser(newPosition); if (!rc) return IPS_ALERT; FocusRelPosN[0].value = ticks; FocusRelPosNP.s = IPS_BUSY; return IPS_BUSY; } void Microtouch::TimerHit() { if (!isConnected()) { return; } bool rc = updatePosition(); if (rc) { if (fabs(lastPos - FocusAbsPosN[0].value) > 1) { IDSetNumber(&FocusAbsPosNP, nullptr); lastPos = FocusAbsPosN[0].value; } } rc = updateTemperature(); if (rc) { if (fabs(lastTemperature - TemperatureN[0].value) >= 0.01) { IDSetNumber(&TemperatureNP, nullptr); lastTemperature = TemperatureN[0].value; } } if (FocusTimerNP.s == IPS_BUSY) { float remaining = CalcTimeLeft(focusMoveStart, focusMoveRequest); if (remaining <= 0) { FocusTimerNP.s = IPS_OK; FocusTimerN[0].value = 0; AbortFocuser(); } else FocusTimerN[0].value = remaining * 1000.0; IDSetNumber(&FocusTimerNP, nullptr); } if (FocusAbsPosNP.s == IPS_BUSY || FocusRelPosNP.s == IPS_BUSY) { if (!isMoving()) { FocusAbsPosNP.s = IPS_OK; FocusRelPosNP.s = IPS_OK; IDSetNumber(&FocusAbsPosNP, nullptr); IDSetNumber(&FocusRelPosNP, nullptr); lastPos = FocusAbsPosN[0].value; LOG_INFO("Focuser reached requested position."); } } SetTimer(POLLMS); } bool Microtouch::AbortFocuser() { WriteCmd(CMD_HALT); FocusAbsPosNP.s = IPS_IDLE; FocusRelPosNP.s = IPS_IDLE; IDSetNumber(&FocusAbsPosNP, nullptr); IDSetNumber(&FocusRelPosNP, nullptr); return true; } float Microtouch::CalcTimeLeft(timeval start, float req) { double timesince; double timeleft; struct timeval now { 0, 0 }; gettimeofday(&now, nullptr); timesince = (double)(now.tv_sec * 1000.0 + now.tv_usec / 1000) - (double)(start.tv_sec * 1000.0 + start.tv_usec / 1000); timesince = timesince / 1000; timeleft = req - timesince; return timeleft; } bool Microtouch::WriteCmd(char cmd) { int nbytes_written = 0, rc = -1; char errstr[MAXRBUF]; tcflush(PortFD, TCIOFLUSH); LOGF_DEBUG("WriteCmd : %02x ", cmd); if ((rc = tty_write(PortFD, &cmd, 1, &nbytes_written)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("WriteCmd error: %s.", errstr); return false; } return true; } bool Microtouch::WriteCmdGetResponse(char cmd, char *readbuffer, char numbytes) { int nbytes_read = 0, rc = -1; char errstr[MAXRBUF]; if (WriteCmd(cmd)) { if ((rc = tty_read(PortFD, readbuffer, numbytes, MICROTOUCH_TIMEOUT, &nbytes_read)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("WriteCmdGetResponse error: %s.", errstr); return false; } return true; } else return false; } char Microtouch::WriteCmdGetByte(char cmd) { char read[2]; if (WriteCmdGetResponse(cmd, read, 2)) { LOGF_DEBUG("WriteCmdGetByte : %02x %02x ", read[0], read[1]); return read[1]; } else return -1; } bool Microtouch::WriteCmdSetByte(char cmd, char val) { int nbytes_written = 0, rc = -1; char errstr[MAXRBUF]; char write_buffer[2]; write_buffer[0] = cmd; write_buffer[1] = val; LOGF_DEBUG("WriteCmdSetByte : CMD %02x %02x ", write_buffer[0], write_buffer[1]); tcflush(PortFD, TCIOFLUSH); if ((rc = tty_write(PortFD, write_buffer, 2, &nbytes_written)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("WriteCmdSetByte error: %s.", errstr); return false; } return true; } signed short int Microtouch::WriteCmdGetShortInt(char cmd) { char read[3]; if (WriteCmdGetResponse(cmd, read, 3)) return ((unsigned char)read[2] << 8 | (unsigned char)read[1]); else return -1; } bool Microtouch::WriteCmdSetShortInt(char cmd, short int val) { int nbytes_written = 0, rc = -1; char errstr[MAXRBUF]; char write_buffer[3]; write_buffer[0] = cmd; write_buffer[1] = val & 0xFF; write_buffer[2] = val >> 8; LOGF_DEBUG("WriteCmdSetShortInt : %02x %02x %02x ", write_buffer[0], write_buffer[1], write_buffer[2]); tcflush(PortFD, TCIOFLUSH); if ((rc = tty_write(PortFD, write_buffer, 3, &nbytes_written)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("WriteCmdSetShortInt error: %s.", errstr); return false; } return true; } int Microtouch::WriteCmdGetInt(char cmd) { char read[5]; if (WriteCmdGetResponse(cmd, read, 5)) return ((unsigned char)read[4] << 24 | (unsigned char)read[3] << 16 | (unsigned char)read[2] << 8 | (unsigned char)read[1]); else return -100000; } bool Microtouch::WriteCmdSetInt(char cmd, int val) { int nbytes_written = 0, rc = -1; char errstr[MAXRBUF]; char write_buffer[5]; write_buffer[0] = cmd; write_buffer[1] = val & 0xFF; write_buffer[2] = val >> 8; write_buffer[3] = val >> 16; write_buffer[4] = val >> 24; LOGF_DEBUG("WriteCmdSetInt : %02x %02x %02x %02x %02x ", write_buffer[0], write_buffer[1], write_buffer[2], write_buffer[3], write_buffer[4]); tcflush(PortFD, TCIOFLUSH); if ((rc = tty_write(PortFD, write_buffer, 5, &nbytes_written)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("WriteCmdSetInt error: %s.", errstr); return false; } return true; } bool Microtouch::WriteCmdSetIntAsDigits(char cmd, int val) { int nbytes_written = 0, rc = -1; char errstr[MAXRBUF]; char write_buffer[5]; write_buffer[0] = cmd; write_buffer[1] = val % 10; write_buffer[2] = (val / 10) % 10; write_buffer[3] = (val / 100) % 10; write_buffer[4] = (val / 1000); LOGF_DEBUG("WriteCmdSetIntAsDigits : CMD (%02x %02x %02x %02x %02x) ", write_buffer[0], write_buffer[1], write_buffer[2], write_buffer[3], write_buffer[4]); tcflush(PortFD, TCIOFLUSH); if ((rc = tty_write(PortFD, write_buffer, 5, &nbytes_written)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("WriteCmdSetIntAsDigits error: %s.", errstr); return false; } return true; } libindi/drivers/focuser/focuslynx.h0000664000175000017500000000421413263645557017022 0ustar jasemjasem/* FocusLynx/FocusBoss II INDI driver Copyright (C) 2015 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 */ #pragma once #include "focuslynxbase.h" #define FOCUSNAMEF1 "FocusLynx F1" #define FOCUSNAMEF2 "FocusLynx F2" class FocusLynxF1 : public FocusLynxBase { public: explicit FocusLynxF1(const char *target); ~FocusLynxF1(); const char *getDefaultName() override; virtual bool Connect() override; virtual bool Disconnect() override; virtual bool updateProperties() override; virtual bool initProperties() override; // virtual void ISGetProperties(const char *dev) override; int getPortFD(); virtual void setSimulation(bool enable); virtual void setDebug(bool enable); private: // Get functions bool getHubConfig(); // HUB Main Parameter IText HubT[2] {}; ITextVectorProperty HubTP; // Network Wired Info IText WiredT[2] {}; ITextVectorProperty WiredTP; //Network WIFI Info IText WifiT[9] {}; ITextVectorProperty WifiTP; }; class FocusLynxF2 : public FocusLynxBase { public: explicit FocusLynxF2(const char *target); ~FocusLynxF2(); const char *getDefaultName() override; virtual bool Connect() override; virtual bool Disconnect() override; virtual bool RemoteDisconnect(); virtual bool initProperties() override; virtual void simulationTriggered(bool enable) override; virtual void debugTriggered(bool enable) override; }; libindi/drivers/focuser/focuslynx.cpp0000664000175000017500000006551113263645557017364 0ustar jasemjasem/* Focus Lynx/Focus Boss II INDI driver Copyright (C) 2015 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 */ #include "focuslynx.h" #include "indicom.h" #include "connectionplugins/connectionserial.h" #include #include #include #define FOCUSNAMEF1 "FocusLynx F1" #define FOCUSNAMEF2 "FocusLynx F2" #define FOCUSLYNX_TIMEOUT 2 #define HUB_SETTINGS_TAB "Device" std::unique_ptr lynxDriveF1(new FocusLynxF1("F1")); std::unique_ptr lynxDriveF2(new FocusLynxF2("F2")); void ISGetProperties(const char *dev) { lynxDriveF1->ISGetProperties(dev); lynxDriveF2->ISGetProperties(dev); } void ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) { // Only call the corrected Focuser to execute evaluate the newSwitch if (!strcmp(dev, lynxDriveF1->getDeviceName())) lynxDriveF1->ISNewSwitch(dev, name, states, names, n); else if (!strcmp(dev, lynxDriveF2->getDeviceName())) lynxDriveF2->ISNewSwitch(dev, name, states, names, n); } void ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n) { // Only call the corrected Focuser to execute evaluate the newText if (!strcmp(dev, lynxDriveF1->getDeviceName())) lynxDriveF1->ISNewText(dev, name, texts, names, n); else if (!strcmp(dev, lynxDriveF2->getDeviceName())) lynxDriveF2->ISNewText(dev, name, texts, names, n); } void ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) { // Only call the corrected Focuser to execute evaluate the newNumber if (!strcmp(dev, lynxDriveF1->getDeviceName())) lynxDriveF1->ISNewNumber(dev, name, values, names, n); else if (!strcmp(dev, lynxDriveF2->getDeviceName())) lynxDriveF2->ISNewNumber(dev, name, values, names, n); } void ISNewBLOB(const char *dev, const char *name, int sizes[], int blobsizes[], char *blobs[], char *formats[], char *names[], int n) { INDI_UNUSED(dev); INDI_UNUSED(name); INDI_UNUSED(sizes); INDI_UNUSED(blobsizes); INDI_UNUSED(blobs); INDI_UNUSED(formats); INDI_UNUSED(names); INDI_UNUSED(n); } void ISSnoopDevice(XMLEle *root) { lynxDriveF1->ISSnoopDevice(root); lynxDriveF2->ISSnoopDevice(root); } /************************************************************************************ * * First Focuser (F1) * *************************************************************************************/ /************************************************************************************ * * ***********************************************************************************/ FocusLynxF1::FocusLynxF1(const char *target) { /* Override the original constructor * and give the Focuser target * F1 or F2 to set the target of the created instance */ setFocusTarget(target); // Both communication available, Serial and network (tcp/ip) setConnection(CONNECTION_SERIAL | CONNECTION_TCP); // explain in connect() function Only set on the F1 constructor, not on the F2 one PortFD = -1; DBG_FOCUS = INDI::Logger::getInstance().addDebugLevel("Focus F1 Verbose", "FOCUS F1"); } /************************************************************************************ * * ***********************************************************************************/ FocusLynxF1::~FocusLynxF1() { } /************************************************************************************** * ***************************************************************************************/ bool FocusLynxF1::initProperties() /* New properties * Common properties for both focusers, Hub setting * Only display and managed by Focuser F1 * TODO: * Make this properties writable to give possibility to set these via IndiDriver */ { FocusLynxBase::initProperties(); // General info IUFillText(&HubT[0], "Firmware", "", ""); IUFillText(&HubT[1], "Sleeping", "", ""); IUFillTextVector(&HubTP, HubT, 2, getDeviceName(), "HUB-INFO", "Hub", HUB_SETTINGS_TAB, IP_RO, 0, IPS_IDLE); // Wired network IUFillText(&WiredT[0], "IP address", "", ""); IUFillText(&WiredT[1], "DHCP active", "", ""); IUFillTextVector(&WiredTP, WiredT, 2, getDeviceName(), "WIRED-INFO", "Wired", HUB_SETTINGS_TAB, IP_RO, 0, IPS_IDLE); // Wifi network IUFillText(&WifiT[0], "Installed", "", ""); IUFillText(&WifiT[1], "Connected", "", ""); IUFillText(&WifiT[2], "Firmware", "", ""); IUFillText(&WifiT[3], "FV OK", "", ""); IUFillText(&WifiT[4], "SSID", "", ""); IUFillText(&WifiT[5], "Ip address", "", ""); IUFillText(&WifiT[6], "Security mode", "", ""); IUFillText(&WifiT[7], "Security key", "", ""); IUFillText(&WifiT[8], "Wep key", "", ""); IUFillTextVector(&WifiTP, WifiT, 9, getDeviceName(), "WIFI-INFO", "Wifi", HUB_SETTINGS_TAB, IP_RO, 0, IPS_IDLE); serialConnection->setDefaultBaudRate(Connection::Serial::B_115200); tcpConnection->setDefaultPort(9760); // To avoid confusion has Debug levels only visible on F2 remove it from F1 // Simultation option and Debug option present only on F2 deleteProperty("SIMULATION"); deleteProperty("DEBUG"); return true; } /************************************************************************************ * * ***********************************************************************************/ const char *FocusLynxF1::getDefaultName() { return FOCUSNAMEF1; } /************************************************************************************ * * ***********************************************************************************/ bool FocusLynxF1::Connect() /* Overide of connect() function * different for F1 or F2 focuser * F1 connect only himself to the driver and * it is the only one who's connect to the communication port to establish the physical communication */ { configurationComplete = false; if (isSimulation()) /* PortFD value used to give the /dev/ttyUSBx or TCP descriptor * if -1 = no physical port selected or simulation mode * if 0 = no descriptor created, F1 not connected (error) * other value = descriptor number */ PortFD = -1; else if (!INDI::Focuser::Connect()) return false; return Handshake(); } /************************************************************************************ * * ***********************************************************************************/ bool FocusLynxF1::Disconnect() { // If we disconnect F1, the socket would be close. INDI::Focuser::Disconnect(); // Get value of PortFD, should be -1 if (getActiveConnection() == serialConnection) PortFD = serialConnection->getPortFD(); else if (getActiveConnection() == tcpConnection) PortFD = tcpConnection->getPortFD(); // Then we have to disconnect the second focuser F2 lynxDriveF2->RemoteDisconnect(); LOGF_INFO("Value of PortFD = %d", PortFD); return true; } /************************************************************************************ * * ***********************************************************************************/ int FocusLynxF1::getPortFD() // Would be used by F2 instance to communicate with the HUB { LOGF_INFO("F1 PortFD : %d", PortFD); return PortFD; } /************************************************************************************ * * ***********************************************************************************/ bool FocusLynxF1::updateProperties() /* Add the HUB properties on the driver * Only displayed and used by the first focuser F1 */ { FocusLynxBase::updateProperties(); if (isConnected()) { defineText(&HubTP); defineText(&WiredTP); defineText(&WifiTP); defineNumber(&LedNP); if (getHubConfig()) LOG_INFO("HUB paramaters updated."); else { LOG_ERROR("Failed to retrieve HUB configuration settings..."); return false; } } else { deleteProperty(HubTP.name); deleteProperty(WiredTP.name); deleteProperty(WifiTP.name); deleteProperty(LedNP.name); } return true; } /************************************************************************************ * * ***********************************************************************************/ bool FocusLynxF1::getHubConfig() { char cmd[32]={0}; int errcode = 0; char errmsg[MAXRBUF]; char response[32]={0}; int nbytes_read = 0; int nbytes_written = 0; char key[16]={0}; char text[32]={0}; /* Answer from the HUB ! HUB INFO Hub FVer = 2.0.4 Sleeping = 0 Wired IP = 169.254.190.196 DHCPisOn = 1 WF Atchd = 0 WF Conn = 0 WF FVer = 0.0.0 WF FV OK = 0 WF SSID = WF IP = 0.0.0.0 WF SecMd = A WF SecKy = WF WepKI = 0 END * */ memset(response, 0, sizeof(response)); strncpy(cmd, "", 16); LOGF_DEBUG("CMD (%s)", cmd); if (isSimulation()) { strncpy(response, "HUB INFO\n", 16); nbytes_read = strlen(response); } else { if ((errcode = tty_write(PortFD, cmd, strlen(cmd), &nbytes_written)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); LOGF_ERROR("%s", errmsg); return false; } if (isResponseOK() == false) return false; if ( (errcode = tty_read_section(PortFD, response, 0xA, LYNXFOCUS_TIMEOUT, &nbytes_read)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); LOGF_ERROR("%s", errmsg); return false; } } if (nbytes_read > 0) { response[nbytes_read - 1] = '\0'; LOGF_DEBUG("RES (%s)", response); if (strcmp(response, "HUB INFO")) return false; } memset(response, 0, sizeof(response)); // Hub Version if (isSimulation()) { strncpy(response, "Hub FVer = 2.0.4\n", 32); nbytes_read = strlen(response); } else if ( (errcode = tty_read_section(PortFD, response, 0xA, LYNXFOCUS_TIMEOUT, &nbytes_read)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); LOGF_ERROR("%s", errmsg); return false; } response[nbytes_read - 1] = '\0'; LOGF_DEBUG("RES (%s)", response); int rc = sscanf(response, "%16[^=]=%16[^\n]s", key, text); if (rc == 2) { HubTP.s = IPS_OK; IUSaveText(&HubT[0], text); IDSetText(&HubTP, nullptr); //Save localy the Version of the firmware's Hub strncpy(version, text, sizeof(version)); LOGF_DEBUG("Text = %s, Key = %s", text, key); } else if (rc != 1) return false; memset(response, 0, sizeof(response)); memset(text, 0, sizeof(text)); // Sleeping status if (isSimulation()) { strncpy(response, "Sleeping = 0\n", 16); nbytes_read = strlen(response); } else if ( (errcode = tty_read_section(PortFD, response, 0xA, LYNXFOCUS_TIMEOUT, &nbytes_read)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); LOGF_ERROR("%s", errmsg); return false; } response[nbytes_read - 1] = '\0'; LOGF_DEBUG("RES (%s)", response); rc = sscanf(response, "%16[^=]=%s", key, text); if (rc == 2) { HubTP.s = IPS_OK; IUSaveText(&HubT[1], text); IDSetText(&HubTP, nullptr); LOGF_DEBUG("Text = %s, Key = %s", text, key); } else if (rc != 1) return false; memset(response, 0, sizeof(response)); memset(text, 0, sizeof(text)); // Wired IP address if (isSimulation()) { strncpy(response, "Wired IP = 169.168.1.10\n", 32); nbytes_read = strlen(response); } else if ( (errcode = tty_read_section(PortFD, response, 0xA, LYNXFOCUS_TIMEOUT, &nbytes_read)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); LOGF_ERROR("%s", errmsg); return false; } response[nbytes_read - 1] = '\0'; LOGF_DEBUG("RES (%s)", response); rc = sscanf(response, "%16[^=]=%s", key, text); if (rc == 2) { WiredTP.s = IPS_OK; IUSaveText(&WiredT[0], text); IDSetText(&WiredTP, nullptr); LOGF_DEBUG("Text = %s, Key = %s", text, key); } else if (rc != 1) return false; memset(response, 0, sizeof(response)); memset(text, 0, sizeof(text)); // DHCP on/off if (isSimulation()) { strncpy(response, "DHCPisOn = 1\n", 32); nbytes_read = strlen(response); } else if ( (errcode = tty_read_section(PortFD, response, 0xA, LYNXFOCUS_TIMEOUT, &nbytes_read)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); LOGF_ERROR("%s", errmsg); return false; } response[nbytes_read - 1] = '\0'; LOGF_DEBUG("RES (%s)", response); rc = sscanf(response, "%16[^=]=%16[^\n]s", key, text); if (rc == 2) { WiredTP.s = IPS_OK; IUSaveText(&WiredT[1], text); IDSetText(&WiredTP, nullptr); LOGF_DEBUG("Text = %s, Key = %s", text, key); } else if (rc != 1) return false; memset(response, 0, sizeof(response)); memset(text, 0, sizeof(text)); // Is WIFI module present if (isSimulation()) { strncpy(response, "WF Atchd = 1\n", 32); nbytes_read = strlen(response); } else if ( (errcode = tty_read_section(PortFD, response, 0xA, LYNXFOCUS_TIMEOUT, &nbytes_read)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); LOGF_ERROR("%s", errmsg); return false; } response[nbytes_read - 1] = '\0'; LOGF_DEBUG("RES (%s)", response); rc = sscanf(response, "%16[^=]=%s", key, text); if (rc == 2) { WifiTP.s = IPS_OK; IUSaveText(&WifiT[0], text); LOGF_DEBUG("Text = %s, Key = %s", text, key); } else if (rc != 1) return false; memset(response, 0, sizeof(response)); memset(text, 0, sizeof(text)); // Is WIFI connected if (isSimulation()) { strncpy(response, "WF Conn = 1\n", 32); nbytes_read = strlen(response); } else if ( (errcode = tty_read_section(PortFD, response, 0xA, LYNXFOCUS_TIMEOUT, &nbytes_read)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); LOGF_ERROR("%s", errmsg); return false; } response[nbytes_read - 1] = '\0'; LOGF_DEBUG("RES (%s)", response); rc = sscanf(response, "%16[^=]=%s", key, text); if (rc == 2) { WifiTP.s = IPS_OK; IUSaveText(&WifiT[1], text); LOGF_DEBUG("Text = %s, Key = %s", text, key); } else if (rc != 1) return false; memset(response, 0, sizeof(response)); memset(text, 0, sizeof(text)); // WIFI Version firmware if (isSimulation()) { strncpy(response, "WF FVer = 1.0.0\n", 32); nbytes_read = strlen(response); } else if ( (errcode = tty_read_section(PortFD, response, 0xA, LYNXFOCUS_TIMEOUT, &nbytes_read)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); LOGF_ERROR("%s", errmsg); return false; } response[nbytes_read - 1] = '\0'; LOGF_DEBUG("RES (%s)", response); rc = sscanf(response, "%16[^=]=%s", key, text); if (rc == 2) { WifiTP.s = IPS_OK; IUSaveText(&WifiT[2], text); LOGF_DEBUG("Text = %s, Key = %s", text, key); } else if (rc != 1) return false; memset(response, 0, sizeof(response)); memset(text, 0, sizeof(text)); // WIFI OK if (isSimulation()) { strncpy(response, "WF FV OK = 1\n", 32); nbytes_read = strlen(response); } else if ( (errcode = tty_read_section(PortFD, response, 0xA, LYNXFOCUS_TIMEOUT, &nbytes_read)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); LOGF_ERROR("%s", errmsg); return false; } response[nbytes_read - 1] = '\0'; LOGF_DEBUG("RES (%s)", response); rc = sscanf(response, "%16[^=]=%s", key, text); if (rc == 2) { WifiTP.s = IPS_OK; IUSaveText(&WifiT[3], text); LOGF_DEBUG("Text = %s, Key = %s", text, key); } else if (rc != 1) return false; memset(response, 0, sizeof(response)); memset(text, 0, sizeof(text)); // WIFI SSID if (isSimulation()) { strncpy(response, "WF SSID = FocusLynxConfig\n", 32); nbytes_read = strlen(response); } else if ( (errcode = tty_read_section(PortFD, response, 0xA, LYNXFOCUS_TIMEOUT, &nbytes_read)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); LOGF_ERROR("%s", errmsg); return false; } response[nbytes_read - 1] = '\0'; LOGF_DEBUG("RES (%s)", response); rc = sscanf(response, "%32[^=]=%s", key, text); if (rc == 2) { WifiTP.s = IPS_OK; IUSaveText(&WifiT[4], text); LOGF_DEBUG("Text = %s, Key = %s", text, key); } else if (rc != 1) return false; memset(response, 0, sizeof(response)); memset(text, 0, sizeof(text)); // WIFI IP adress if (isSimulation()) { strncpy(response, "WF IP = 192.168.1.11\n", 32); nbytes_read = strlen(response); } else if ( (errcode = tty_read_section(PortFD, response, 0xA, LYNXFOCUS_TIMEOUT, &nbytes_read)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); LOGF_ERROR("%s", errmsg); return false; } response[nbytes_read - 1] = '\0'; LOGF_DEBUG("RES (%s)", response); rc = sscanf(response, "%16[^=]=%s", key, text); if (rc == 2) { WifiTP.s = IPS_OK; IUSaveText(&WifiT[5], text); LOGF_DEBUG("Text = %s, Key = %s", text, key); } else if (rc != 1) return false; memset(response, 0, sizeof(response)); memset(text, 0, sizeof(text)); // WIFI Security mode if (isSimulation()) { strncpy(response, "WF SecMd = A\n", 32); nbytes_read = strlen(response); } else if ( (errcode = tty_read_section(PortFD, response, 0xA, LYNXFOCUS_TIMEOUT, &nbytes_read)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); LOGF_ERROR("%s", errmsg); return false; } response[nbytes_read - 1] = '\0'; LOGF_DEBUG("RES (%s)", response); rc = sscanf(response, "%16[^=]= %s", key, text); if (rc == 2) { WifiTP.s = IPS_OK; IUSaveText(&WifiT[6], text); LOGF_DEBUG("Text = %s, Key = %s", text, key); } else if (rc != 1) return false; memset(response, 0, sizeof(response)); memset(text, 0, sizeof(text)); // WF Security key if (isSimulation()) { strncpy(response, "WF SecKy =\n", 32); nbytes_read = strlen(response); } else if ( (errcode = tty_read_section(PortFD, response, 0xA, LYNXFOCUS_TIMEOUT, &nbytes_read)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); LOGF_ERROR("%s", errmsg); return false; } response[nbytes_read - 1] = '\0'; LOGF_DEBUG("RES (%s)", response); rc = sscanf(response, "%16[^=]=%s", key, text); if (rc == 2) { WifiTP.s = IPS_OK; IUSaveText(&WifiT[7], text); LOGF_DEBUG("Text = %s, Key = %s", text, key); } else if (rc != 1) return false; memset(response, 0, sizeof(response)); memset(text, 0, sizeof(text)); // WIFI Wep if (isSimulation()) { strncpy(response, "WF WepKI = 0\n", 32); nbytes_read = strlen(response); } else if ( (errcode = tty_read_section(PortFD, response, 0xA, LYNXFOCUS_TIMEOUT, &nbytes_read)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); LOGF_ERROR("%s", errmsg); return false; } response[nbytes_read - 1] = '\0'; LOGF_DEBUG("RES (%s)", response); rc = sscanf(response, "%16[^=]=%s", key, text); if (rc == 2) { WifiTP.s = IPS_OK; IUSaveText(&WifiT[8], text); LOGF_DEBUG("Text = %s, Key = %s", text, key); } else if (rc != 1) return false; memset(response, 0, sizeof(response)); memset(text, 0, sizeof(text)); // Set the light to ILDE if no module WIFI detected if (!strcmp(WifiT[0].text, "0")) { LOGF_INFO("Wifi module = %s", WifiT[0].text); WifiTP.s = IPS_IDLE; } IDSetText(&WifiTP, nullptr); // END is reached if (isSimulation()) { strncpy(response, "END\n", 16); nbytes_read = strlen(response); } else if ( (errcode = tty_read_section(PortFD, response, 0xA, LYNXFOCUS_TIMEOUT, &nbytes_read)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); LOGF_ERROR("%s", errmsg); return false; } if (nbytes_read > 0) { response[nbytes_read - 1] = '\0'; // Display the response to be sure to have read the complet TTY Buffer. LOGF_DEBUG("RES (%s)", response); if (strcmp(response, "END")) return false; } tcflush(PortFD, TCIFLUSH); configurationComplete = true; int a, b, c, temp; temp = getVersion(&a, &b, &c); if (temp != 0) LOGF_INFO("Version major: %d, minor: %d, subversion: %d", a, b, c); else LOG_INFO("Couldn't get version information"); return true; } /************************************************************************************ * * ***********************************************************************************/ void FocusLynxF1::setSimulation(bool enable) { // call by F2 to set the Simulation option INDI::DefaultDevice::setSimulation(enable); } /************************************************************************************ * * ***********************************************************************************/ void FocusLynxF1::setDebug(bool enable) { // Call by F2 to set the Debug option INDI::DefaultDevice::setDebug(enable); } /************************************************************************************ * * Second Focuser (F2) * *************************************************************************************/ /************************************************************************************ * * ***********************************************************************************/ FocusLynxF2::FocusLynxF2(const char *target) { setFocusTarget(target); // The second focuser has no direct communication with the hub setConnection(CONNECTION_NONE); DBG_FOCUS = INDI::Logger::getInstance().addDebugLevel("Focus F2 Verbose", "FOCUS F2"); } /************************************************************************************ * * ***********************************************************************************/ FocusLynxF2::~FocusLynxF2() { } /************************************************************************************** * ***************************************************************************************/ bool FocusLynxF2::initProperties() { FocusLynxBase::initProperties(); // Remove from F2 to avoid confusion, already present on F1 deleteProperty("DRIVER_INFO"); return true; } /************************************************************************************ * * ***********************************************************************************/ const char *FocusLynxF2::getDefaultName() { return FOCUSNAMEF2; } /************************************************************************************ * * ***********************************************************************************/ bool FocusLynxF2::Connect() /* Overide of connect() function * different for F2 or F1 focuser * F2 don't connect himself to the hub */ { configurationComplete = false; if (!lynxDriveF1->isConnected()) { if (!lynxDriveF1->Connect()) { LOG_INFO("Focus F1 should be connected before try to connect F2"); return false; } lynxDriveF1->setConnected(true, IPS_OK); lynxDriveF1->updateProperties(); } PortFD = lynxDriveF1->getPortFD(); //Get the socket descriptor open by focuser F1 connect() LOGF_INFO("F2 PortFD : %d", PortFD); int modelIndex = IUFindOnSwitchIndex(&ModelSP); if (ack()) { LOG_INFO("FocusLynx is online. Getting focus parameters..."); setDeviceType(modelIndex); SetTimer(POLLMS); return true; } DEBUG(INDI::Logger::DBG_SESSION, "Error retreiving data from FocusLynx, please ensure FocusLynx controller is powered and the port is correct."); return false; } /************************************************************************************ * * ***********************************************************************************/ bool FocusLynxF2::Disconnect() { // If we disconnect F2, No socket to close, set local PortFD to -1 PortFD = -1; DEBUGF(INDI::Logger::DBG_SESSION,"%s is offline.", getDeviceName()); LOGF_INFO("Value of F2 PortFD = %d", PortFD); return true; } /************************************************************************************ * * ***********************************************************************************/ bool FocusLynxF2::RemoteDisconnect() { if (isConnected()) { setConnected(false, IPS_IDLE); updateProperties(); } // When called by F1, the PortFD should be -1; For debbug purpose PortFD = lynxDriveF1->getPortFD(); DEBUGF(INDI::Logger::DBG_SESSION,"Remote disconnection: %s is offline.", getDeviceName()); LOGF_INFO("Value of F2 PortFD = %d", PortFD); return true; } /************************************************************************************ * * ***********************************************************************************/ void FocusLynxF2::simulationTriggered(bool enable) { INDI::Focuser::simulationTriggered(enable); // Set the simultation mode on F1 as selected by the user lynxDriveF1->setSimulation(enable); } /************************************************************************************ * * ***********************************************************************************/ void FocusLynxF2::debugTriggered(bool enable) { INDI::Focuser::debugTriggered(enable); // Set the Debug mode on F1 as selected by the user lynxDriveF1->setDebug(enable); } libindi/drivers/focuser/microtouch.h0000664000175000017500000000754513263645557017156 0ustar jasemjasem/* Microtouch Focuser Copyright (C) 2016 Marco Peters (mpeters@rzpeters.de) Copyright (C) 2013 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 */ #pragma once #include "indifocuser.h" #define CMD_GET_STATUS 0x80 #define CMD_RESET_POSITION 0x81 #define CMD_IS_MOVING 0x82 #define CMD_HALT 0x83 #define CMD_GET_TEMPERATURE 0x84 #define CMD_SET_COEFF 0x85 #define CMD_GET_COEFF 0x86 #define CMD_TEMPCOMP_ON 0x87 #define CMD_TEMPCOMP_OFF 0x88 #define CMD_UPDATE_POSITION 0x8c #define CMD_GET_POSITION 0x8d #define CMD_SET_MOTOR_SPEED 0x9d #define CMD_GET_MOTOR_SPEED 0x9e #define CMD_SET_TEMP_OFFSET 0x9f #define FOCUS_MOTORSPEED_NORMAL 8 #define FOCUS_MOTORSPEED_FAST 4 class Microtouch : public INDI::Focuser { public: Microtouch(); virtual ~Microtouch() = default; virtual bool Handshake(); const char *getDefaultName(); virtual bool initProperties(); virtual bool updateProperties(); virtual bool ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n); virtual bool ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n); virtual IPState MoveFocuser(FocusDirection dir, int speed, uint16_t duration); virtual IPState MoveAbsFocuser(uint32_t targetTicks); virtual IPState MoveRelFocuser(FocusDirection dir, uint32_t ticks); virtual bool SetFocuserSpeed(int speed); virtual bool AbortFocuser(); virtual void TimerHit(); private: void GetFocusParams(); bool reset(); bool reset(double pos); bool updateMotorSpeed(); bool updateTemperature(); bool updatePosition(); bool updateSpeed(); bool isMoving(); bool Ack(); bool MoveFocuser(unsigned int position); bool setMotorSpeed(char speed); bool setSpeed(unsigned short speed); bool setTemperatureCalibration(double calibration); bool setTemperatureCoefficient(double coefficient); bool setTemperatureCompensation(bool enable); float CalcTimeLeft(timeval, float); bool WriteCmd(char cmd); bool WriteCmdGetResponse(char cmd, char *readbuffer, char numbytes); char WriteCmdGetByte(char cmd); bool WriteCmdSetByte(char cmd, char val); signed short int WriteCmdGetShortInt(char cmd); bool WriteCmdSetShortInt(char cmd, short int val); int WriteCmdGetInt(char cmd); bool WriteCmdSetInt(char cmd, int val); bool WriteCmdSetIntAsDigits(char cmd, int val); double targetPos { 0 }; double lastPos { 0 }; double lastTemperature { 0 }; unsigned int currentSpeed { 0 }; struct timeval focusMoveStart { 0, 0 }; float focusMoveRequest { 0 }; INumber TemperatureN[1]; INumberVectorProperty TemperatureNP; ISwitch MotorSpeedS[2]; ISwitchVectorProperty MotorSpeedSP; INumber MaxTravelN[1]; INumberVectorProperty MaxTravelNP; INumber TemperatureSettingN[2]; INumberVectorProperty TemperatureSettingNP; ISwitch TemperatureCompensateS[2]; ISwitchVectorProperty TemperatureCompensateSP; ISwitch ResetS[1]; ISwitchVectorProperty ResetSP; INumber ResetToPosN[1]; INumberVectorProperty ResetToPosNP; }; libindi/drivers/focuser/robofocus.cpp0000664000175000017500000012126513263645557017332 0ustar jasemjasem/* RoboFocus Copyright (C) 2006 Markus Wildi (markus.wildi@datacomm.ch) 2011 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 */ #include "robofocus.h" #include "indicom.h" #include #include #include #define RF_MAX_CMD 9 #define RF_TIMEOUT 3 #define BACKLASH_READOUT 99999 #define MAXTRAVEL_READOUT 99999 #define currentSpeed SpeedN[0].value #define currentPosition FocusAbsPosN[0].value #define currentTemperature TemperatureN[0].value #define currentBacklash SetBacklashN[0].value #define currentDuty SettingsN[0].value #define currentDelay SettingsN[1].value #define currentTicks SettingsN[2].value #define currentRelativeMovement FocusRelPosN[0].value #define currentAbsoluteMovement FocusAbsPosN[0].value #define currentSetBacklash SetBacklashN[0].value #define currentMinPosition MinMaxPositionN[0].value #define currentMaxPosition MinMaxPositionN[1].value #define currentMaxTravel MaxTravelN[0].value #define SETTINGS_TAB "Settings" std::unique_ptr roboFocus(new RoboFocus()); void ISGetProperties(const char *dev) { roboFocus->ISGetProperties(dev); } void ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) { roboFocus->ISNewSwitch(dev, name, states, names, n); } void ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n) { roboFocus->ISNewText(dev, name, texts, names, n); } void ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) { roboFocus->ISNewNumber(dev, name, values, names, n); } void ISNewBLOB(const char *dev, const char *name, int sizes[], int blobsizes[], char *blobs[], char *formats[], char *names[], int n) { INDI_UNUSED(dev); INDI_UNUSED(name); INDI_UNUSED(sizes); INDI_UNUSED(blobsizes); INDI_UNUSED(blobs); INDI_UNUSED(formats); INDI_UNUSED(names); INDI_UNUSED(n); } void ISSnoopDevice(XMLEle *root) { roboFocus->ISSnoopDevice(root); } RoboFocus::RoboFocus() { FI::SetCapability(FOCUSER_CAN_ABS_MOVE | FOCUSER_CAN_REL_MOVE | FOCUSER_CAN_ABORT); } bool RoboFocus::initProperties() { INDI::Focuser::initProperties(); /* Focuser temperature */ IUFillNumber(&TemperatureN[0], "TEMPERATURE", "Celsius", "%6.2f", 0, 65000., 0., 10000.); IUFillNumberVector(&TemperatureNP, TemperatureN, 1, getDeviceName(), "FOCUS_TEMPERATURE", "Temperature", MAIN_CONTROL_TAB, IP_RO, 0, IPS_IDLE); /* Settings of the Robofocus */ IUFillNumber(&SettingsN[0], "Duty cycle", "Duty cycle", "%6.0f", 0., 255., 0., 1.0); IUFillNumber(&SettingsN[1], "Step Delay", "Step delay", "%6.0f", 0., 255., 0., 1.0); IUFillNumber(&SettingsN[2], "Motor Steps", "Motor steps per tick", "%6.0f", 0., 255., 0., 1.0); IUFillNumberVector(&SettingsNP, SettingsN, 3, getDeviceName(), "FOCUS_SETTINGS", "Settings", SETTINGS_TAB, IP_RW, 0, IPS_IDLE); /* Power Switches of the Robofocus */ IUFillSwitch(&PowerSwitchesS[0], "1", "Switch 1", ISS_OFF); IUFillSwitch(&PowerSwitchesS[1], "2", "Switch 2", ISS_OFF); IUFillSwitch(&PowerSwitchesS[2], "3", "Switch 3", ISS_OFF); IUFillSwitch(&PowerSwitchesS[3], "4", "Switch 4", ISS_ON); IUFillSwitchVector(&PowerSwitchesSP, PowerSwitchesS, 4, getDeviceName(), "SWTICHES", "Power", SETTINGS_TAB, IP_RW, ISR_1OFMANY, 0, IPS_IDLE); /* Robofocus should stay within these limits */ IUFillNumber(&MinMaxPositionN[0], "MINPOS", "Minimum Tick", "%6.0f", 1., 65000., 0., 100.); IUFillNumber(&MinMaxPositionN[1], "MAXPOS", "Maximum Tick", "%6.0f", 1., 65000., 0., 55000.); IUFillNumberVector(&MinMaxPositionNP, MinMaxPositionN, 2, getDeviceName(), "FOCUS_MINMAXPOSITION", "Extrema", SETTINGS_TAB, IP_RW, 0, IPS_IDLE); IUFillNumber(&MaxTravelN[0], "MAXTRAVEL", "Maximum travel", "%6.0f", 1., 64000., 0., 10000.); IUFillNumberVector(&MaxTravelNP, MaxTravelN, 1, getDeviceName(), "FOCUS_MAXTRAVEL", "Max. travel", SETTINGS_TAB, IP_RW, 0, IPS_IDLE); /* Set Robofocus position register to this position */ IUFillNumber(&SetRegisterPositionN[0], "SETPOS", "Position", "%6.0f", 0, 64000., 0., 0.); IUFillNumberVector(&SetRegisterPositionNP, SetRegisterPositionN, 1, getDeviceName(), "FOCUS_REGISTERPOSITION", "Sync", SETTINGS_TAB, IP_RW, 0, IPS_IDLE); /* Backlash */ IUFillNumber(&SetBacklashN[0], "SETBACKLASH", "Backlash", "%6.0f", -255., 255., 0., 0.); IUFillNumberVector(&SetBacklashNP, SetBacklashN, 1, getDeviceName(), "FOCUS_BACKLASH", "Set Register", SETTINGS_TAB, IP_RW, 0, IPS_IDLE); /* Relative and absolute movement */ FocusRelPosN[0].min = -5000.; FocusRelPosN[0].max = 5000.; FocusRelPosN[0].value = 100; FocusRelPosN[0].step = 100; FocusAbsPosN[0].min = 0.; FocusAbsPosN[0].max = 64000.; FocusAbsPosN[0].value = 0; FocusAbsPosN[0].step = 1000; simulatedTemperature = 600.0; simulatedPosition = 20000; addDebugControl(); addSimulationControl(); return true; } bool RoboFocus::updateProperties() { INDI::Focuser::updateProperties(); if (isConnected()) { defineNumber(&TemperatureNP); defineSwitch(&PowerSwitchesSP); defineNumber(&SettingsNP); defineNumber(&MinMaxPositionNP); defineNumber(&MaxTravelNP); defineNumber(&SetRegisterPositionNP); defineNumber(&SetBacklashNP); defineNumber(&FocusRelPosNP); defineNumber(&FocusAbsPosNP); GetFocusParams(); LOG_DEBUG("RoboFocus paramaters readout complete, focuser ready for use."); } else { deleteProperty(TemperatureNP.name); deleteProperty(SettingsNP.name); deleteProperty(PowerSwitchesSP.name); deleteProperty(MinMaxPositionNP.name); deleteProperty(MaxTravelNP.name); deleteProperty(SetRegisterPositionNP.name); deleteProperty(SetBacklashNP.name); deleteProperty(FocusRelPosNP.name); deleteProperty(FocusAbsPosNP.name); } return true; } bool RoboFocus::Handshake() { char firmeware[] = "FV0000000"; if (isSimulation()) { timerID = SetTimer(POLLMS); LOG_INFO("Simulated Robofocus is online. Getting focus parameters..."); FocusAbsPosN[0].value = simulatedPosition; updateRFFirmware(firmeware); return true; } if ((updateRFFirmware(firmeware)) < 0) { /* This would be the end*/ LOG_ERROR("Unknown error while reading Robofocus firmware."); return false; } return true; } const char *RoboFocus::getDefaultName() { return "RoboFocus"; } unsigned char RoboFocus::CheckSum(char *rf_cmd) { char substr[255]; unsigned char val = 0; for (int i = 0; i < 8; i++) substr[i] = rf_cmd[i]; val = CalculateSum(substr); if (val != (unsigned char)rf_cmd[8]) LOGF_WARN("Checksum: Wrong (%s,%ld), %x != %x", rf_cmd, strlen(rf_cmd), val, (unsigned char)rf_cmd[8]); return val; } unsigned char RoboFocus::CalculateSum(const char *rf_cmd) { unsigned char val = 0; for (int i = 0; i < 8; i++) val = val + (unsigned char)rf_cmd[i]; return val % 256; } int RoboFocus::SendCommand(char *rf_cmd) { int nbytes_written = 0, err_code = 0; char rf_cmd_cks[32], robofocus_error[MAXRBUF]; unsigned char val = 0; val = CalculateSum(rf_cmd); for (int i = 0; i < 8; i++) rf_cmd_cks[i] = rf_cmd[i]; rf_cmd_cks[8] = (unsigned char)val; rf_cmd_cks[9] = 0; if (isSimulation()) return 0; tcflush(PortFD, TCIOFLUSH); LOGF_DEBUG("CMD (%#02X %#02X %#02X %#02X %#02X %#02X %#02X %#02X %#02X)", rf_cmd_cks[0], rf_cmd_cks[1], rf_cmd_cks[2], rf_cmd_cks[3], rf_cmd_cks[4], rf_cmd_cks[5], rf_cmd_cks[6], rf_cmd_cks[7], rf_cmd_cks[8]); if ((err_code = tty_write(PortFD, rf_cmd_cks, RF_MAX_CMD, &nbytes_written) != TTY_OK)) { tty_error_msg(err_code, robofocus_error, MAXRBUF); LOGF_ERROR("TTY error detected: %s", robofocus_error); return -1; } return nbytes_written; } int RoboFocus::ReadResponse(char *buf) { char robofocus_error[MAXRBUF]; char robofocus_char[1]; int bytesRead = 0; int err_code; char motion = 0; bool externalMotion = false; if (isSimulation()) return RF_MAX_CMD; while (true) { if ((err_code = tty_read(PortFD, robofocus_char, 1, RF_TIMEOUT, &bytesRead)) != TTY_OK) { tty_error_msg(err_code, robofocus_error, MAXRBUF); LOGF_ERROR("TTY error detected: %s", robofocus_error); return -1; } switch (robofocus_char[0]) { // Catch 'I' case 0x49: if (motion != 0x49) { motion = 0x49; LOG_INFO("Moving inward..."); if (FocusAbsPosNP.s != IPS_BUSY) { externalMotion = true; FocusAbsPosNP.s = IPS_BUSY; IDSetNumber(&FocusAbsPosNP, nullptr); } } //usleep(100000); break; // catch 'O' case 0x4F: if (motion != 0x4F) { motion = 0x4F; LOG_INFO("Moving outward..."); if (FocusAbsPosNP.s != IPS_BUSY) { externalMotion = true; FocusAbsPosNP.s = IPS_BUSY; IDSetNumber(&FocusAbsPosNP, nullptr); } } //usleep(100000); break; // Start of frame case 0x46: buf[0] = 0x46; // Read rest of frame if ((err_code = tty_read(PortFD, buf + 1, RF_MAX_CMD - 1, RF_TIMEOUT, &bytesRead)) != TTY_OK) { tty_error_msg(err_code, robofocus_error, MAXRBUF); LOGF_ERROR("TTY error detected: %s", robofocus_error); return -1; } if (motion != 0) { LOG_INFO("Stopped."); // If we set it busy due to external motion, let's set it to OK if (externalMotion) { FocusAbsPosNP.s = IPS_OK; IDSetNumber(&FocusAbsPosNP, nullptr); } } tcflush(PortFD, TCIOFLUSH); return (bytesRead + 1); break; default: break; } } return -1; } int RoboFocus::updateRFPosition(double *value) { float temp; char rf_cmd[RF_MAX_CMD]; int robofocus_rc; LOG_DEBUG("Querying Position..."); if (isSimulation()) { *value = simulatedPosition; return 0; } strncpy(rf_cmd, "FG000000", 9); if ((robofocus_rc = SendCommand(rf_cmd)) < 0) return robofocus_rc; if ((robofocus_rc = ReadResponse(rf_cmd)) < 0) return robofocus_rc; if (sscanf(rf_cmd, "FD%6f", &temp) < 1) return -1; *value = (double)temp; LOGF_DEBUG("Position: %g", *value); return 0; } int RoboFocus::updateRFTemperature(double *value) { LOGF_DEBUG("Update Temperature: %g", value); float temp; char rf_cmd[32]; int robofocus_rc; strncpy(rf_cmd, "FT000000", 9); if ((robofocus_rc = SendCommand(rf_cmd)) < 0) return robofocus_rc; if (isSimulation()) snprintf(rf_cmd, 32, "FT%6g", simulatedTemperature); else if ((robofocus_rc = ReadResponse(rf_cmd)) < 0) return robofocus_rc; if (sscanf(rf_cmd, "FT%6f", &temp) < 1) return -1; *value = (double)temp / 2. - 273.15; return 0; } int RoboFocus::updateRFBacklash(double *value) { LOGF_DEBUG("Update Backlash: %g", value); float temp; char rf_cmd[32]; char vl_tmp[4]; int robofocus_rc; int sign = 0; if (isSimulation()) return 0; if (*value == BACKLASH_READOUT) { strncpy(rf_cmd, "FB000000", 9); } else { rf_cmd[0] = 'F'; rf_cmd[1] = 'B'; if (*value > 0) { rf_cmd[2] = '3'; } else { *value = -*value; rf_cmd[2] = '2'; } rf_cmd[3] = '0'; rf_cmd[4] = '0'; if (*value > 99) { sprintf(vl_tmp, "%3d", (int)*value); } else if (*value > 9) { sprintf(vl_tmp, "0%2d", (int)*value); } else { sprintf(vl_tmp, "00%1d", (int)*value); } rf_cmd[5] = vl_tmp[0]; rf_cmd[6] = vl_tmp[1]; rf_cmd[7] = vl_tmp[2]; } if ((robofocus_rc = SendCommand(rf_cmd)) < 0) return robofocus_rc; if ((robofocus_rc = ReadResponse(rf_cmd)) < 0) return robofocus_rc; if (sscanf(rf_cmd, "FB%1d%5f", &sign, &temp) < 1) return -1; *value = (double)temp; if ((sign == 2) && (*value > 0)) { *value = -(*value); } return 0; } int RoboFocus::updateRFFirmware(char *rf_cmd) { int robofocus_rc; LOG_DEBUG("Querying RoboFocus Firmware..."); strncpy(rf_cmd, "FV000000", 9); if ((robofocus_rc = SendCommand(rf_cmd)) < 0) return robofocus_rc; if (isSimulation()) strncpy(rf_cmd, "SIM", 4); else if ((robofocus_rc = ReadResponse(rf_cmd)) < 0) return robofocus_rc; return 0; } int RoboFocus::updateRFMotorSettings(double *duty, double *delay, double *ticks) { LOGF_DEBUG("Update Motor Settings: Duty (%g), Delay (%g), Ticks(%g)", *duty, *delay, *ticks); char rf_cmd[32]; int robofocus_rc; if (isSimulation()) { *duty = 100; *delay = 0; *ticks = 0; return 0; } if ((*duty == 0) && (*delay == 0) && (*ticks == 0)) { strncpy(rf_cmd, "FC000000", 9); } else { rf_cmd[0] = 'F'; rf_cmd[1] = 'C'; rf_cmd[2] = (char)*duty; rf_cmd[3] = (char)*delay; rf_cmd[4] = (char)*ticks; rf_cmd[5] = '0'; rf_cmd[6] = '0'; rf_cmd[7] = '0'; rf_cmd[8] = 0; } if ((robofocus_rc = SendCommand(rf_cmd)) < 0) return robofocus_rc; if ((robofocus_rc = ReadResponse(rf_cmd)) < 0) return robofocus_rc; *duty = (float)rf_cmd[2]; *delay = (float)rf_cmd[3]; *ticks = (float)rf_cmd[4]; return 0; } int RoboFocus::updateRFPositionRelativeInward(double value) { char rf_cmd[32]; int robofocus_rc; //float temp ; rf_cmd[0] = 0; LOGF_DEBUG("Update Relative Position Inward: %g", value); if (isSimulation()) { simulatedPosition += value; //value = simulatedPosition; return 0; } if (value > 9999) { sprintf(rf_cmd, "FI0%5d", (int)value); } else if (value > 999) { sprintf(rf_cmd, "FI00%4d", (int)value); } else if (value > 99) { sprintf(rf_cmd, "FI000%3d", (int)value); } else if (value > 9) { sprintf(rf_cmd, "FI0000%2d", (int)value); } else { sprintf(rf_cmd, "FI00000%1d", (int)value); } if ((robofocus_rc = SendCommand(rf_cmd)) < 0) return robofocus_rc; return 0; } int RoboFocus::updateRFPositionRelativeOutward(double value) { char rf_cmd[32]; int robofocus_rc; //float temp ; LOGF_DEBUG("Update Relative Position Outward: %g", value); if (isSimulation()) { simulatedPosition -= value; //value = simulatedPosition; return 0; } rf_cmd[0] = 0; if (value > 9999) { sprintf(rf_cmd, "FO0%5d", (int)value); } else if (value > 999) { sprintf(rf_cmd, "FO00%4d", (int)value); } else if (value > 99) { sprintf(rf_cmd, "FO000%3d", (int)value); } else if (value > 9) { sprintf(rf_cmd, "FO0000%2d", (int)value); } else { sprintf(rf_cmd, "FO00000%1d", (int)value); } if ((robofocus_rc = SendCommand(rf_cmd)) < 0) return robofocus_rc; return 0; } int RoboFocus::updateRFPositionAbsolute(double value) { char rf_cmd[32]; int robofocus_rc; LOGF_DEBUG("Moving Absolute Position: %g", value); if (isSimulation()) { simulatedPosition = value; return 0; } rf_cmd[0] = 0; if (value > 9999) { sprintf(rf_cmd, "FG0%5d", (int)value); } else if (value > 999) { sprintf(rf_cmd, "FG00%4d", (int)value); } else if (value > 99) { sprintf(rf_cmd, "FG000%3d", (int)value); } else if (value > 9) { sprintf(rf_cmd, "FG0000%2d", (int)value); } else { sprintf(rf_cmd, "FG00000%1d", (int)value); } if ((robofocus_rc = SendCommand(rf_cmd)) < 0) return robofocus_rc; return 0; } int RoboFocus::updateRFPowerSwitches(int s, int new_sn, int *cur_s1LL, int *cur_s2LR, int *cur_s3RL, int *cur_s4RR) { INDI_UNUSED(s); char rf_cmd[32]; char rf_cmd_tmp[32]; int robofocus_rc; int i = 0; if (isSimulation()) { return 0; } LOG_DEBUG("Get switch status..."); /* Get first the status */ strncpy(rf_cmd_tmp, "FP000000", 9); if ((robofocus_rc = SendCommand(rf_cmd_tmp)) < 0) return robofocus_rc; if ((robofocus_rc = ReadResponse(rf_cmd_tmp)) < 0) return robofocus_rc; for (i = 0; i < 9; i++) { rf_cmd[i] = rf_cmd_tmp[i]; } if (rf_cmd[new_sn + 4] == '2') { rf_cmd[new_sn + 4] = '1'; } else { rf_cmd[new_sn + 4] = '2'; } rf_cmd[8] = 0; if ((robofocus_rc = SendCommand(rf_cmd)) < 0) return robofocus_rc; if ((robofocus_rc = ReadResponse(rf_cmd)) < 0) return robofocus_rc; *cur_s1LL = *cur_s2LR = *cur_s3RL = *cur_s4RR = ISS_OFF; if (rf_cmd[4] == '2') { *cur_s1LL = ISS_ON; } if (rf_cmd[5] == '2') { *cur_s2LR = ISS_ON; } if (rf_cmd[6] == '2') { *cur_s3RL = ISS_ON; } if (rf_cmd[7] == '2') { *cur_s4RR = ISS_ON; } return 0; } int RoboFocus::updateRFMaxPosition(double *value) { LOG_DEBUG("Query max position..."); float temp; char rf_cmd[32]; char vl_tmp[6]; int robofocus_rc; char waste[1]; if (isSimulation()) { return 0; } if (*value == MAXTRAVEL_READOUT) { strncpy(rf_cmd, "FL000000", 9); } else { rf_cmd[0] = 'F'; rf_cmd[1] = 'L'; rf_cmd[2] = '0'; if (*value > 9999) { sprintf(vl_tmp, "%5d", (int)*value); } else if (*value > 999) { sprintf(vl_tmp, "0%4d", (int)*value); } else if (*value > 99) { sprintf(vl_tmp, "00%3d", (int)*value); } else if (*value > 9) { sprintf(vl_tmp, "000%2d", (int)*value); } else { sprintf(vl_tmp, "0000%1d", (int)*value); } rf_cmd[3] = vl_tmp[0]; rf_cmd[4] = vl_tmp[1]; rf_cmd[5] = vl_tmp[2]; rf_cmd[6] = vl_tmp[3]; rf_cmd[7] = vl_tmp[4]; rf_cmd[8] = 0; } if ((robofocus_rc = SendCommand(rf_cmd)) < 0) return robofocus_rc; if ((robofocus_rc = ReadResponse(rf_cmd)) < 0) return robofocus_rc; if (sscanf(rf_cmd, "FL%1c%5f", waste, &temp) < 1) return -1; *value = (double)temp; LOGF_DEBUG("Max position: %g", *value); return 0; } int RoboFocus::updateRFSetPosition(const double *value) { LOGF_DEBUG("Set Max position: %g", *value); char rf_cmd[32]; char vl_tmp[6]; int robofocus_rc; if (isSimulation()) { simulatedPosition = *value; return 0; } rf_cmd[0] = 'F'; rf_cmd[1] = 'S'; rf_cmd[2] = '0'; if (*value > 9999) { sprintf(vl_tmp, "%5d", (int)*value); } else if (*value > 999) { sprintf(vl_tmp, "0%4d", (int)*value); } else if (*value > 99) { sprintf(vl_tmp, "00%3d", (int)*value); } else if (*value > 9) { sprintf(vl_tmp, "000%2d", (int)*value); } else { sprintf(vl_tmp, "0000%1d", (int)*value); } rf_cmd[3] = vl_tmp[0]; rf_cmd[4] = vl_tmp[1]; rf_cmd[5] = vl_tmp[2]; rf_cmd[6] = vl_tmp[3]; rf_cmd[7] = vl_tmp[4]; rf_cmd[8] = 0; if ((robofocus_rc = SendCommand(rf_cmd)) < 0) return robofocus_rc; return 0; } bool RoboFocus::ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) { if (dev != nullptr && strcmp(dev, getDeviceName()) == 0) { if (strcmp(name, PowerSwitchesSP.name) == 0) { int ret = -1; int nset = 0; int i = 0; int new_s = -1; int new_sn = -1; int cur_s1LL = 0; int cur_s2LR = 0; int cur_s3RL = 0; int cur_s4RR = 0; ISwitch *sp; PowerSwitchesSP.s = IPS_BUSY; IDSetSwitch(&PowerSwitchesSP, nullptr); for (nset = i = 0; i < n; i++) { /* Find numbers with the passed names in the SettingsNP property */ sp = IUFindSwitch(&PowerSwitchesSP, names[i]); /* If the state found is (PowerSwitchesS[0]) then process it */ if (sp == &PowerSwitchesS[0]) { new_s = (states[i]); new_sn = 0; nset++; } else if (sp == &PowerSwitchesS[1]) { new_s = (states[i]); new_sn = 1; nset++; } else if (sp == &PowerSwitchesS[2]) { new_s = (states[i]); new_sn = 2; nset++; } else if (sp == &PowerSwitchesS[3]) { new_s = (states[i]); new_sn = 3; nset++; } } if (nset == 1) { cur_s1LL = cur_s2LR = cur_s3RL = cur_s4RR = 0; if ((ret = updateRFPowerSwitches(new_s, new_sn, &cur_s1LL, &cur_s2LR, &cur_s3RL, &cur_s4RR)) < 0) { PowerSwitchesSP.s = IPS_ALERT; IDSetSwitch(&PowerSwitchesSP, "Unknown error while reading Robofocus power swicht settings"); return true; } } else { /* Set property state to idle */ PowerSwitchesSP.s = IPS_IDLE; IDSetNumber(&SettingsNP, "Power switch settings absent or bogus."); return true; } } } return INDI::Focuser::ISNewSwitch(dev, name, states, names, n); } bool RoboFocus::ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) { int nset = 0, i = 0; if (dev != nullptr && strcmp(dev, getDeviceName()) == 0) { if (strcmp(name, SettingsNP.name) == 0) { /* new speed */ double new_duty = 0; double new_delay = 0; double new_ticks = 0; int ret = -1; for (nset = i = 0; i < n; i++) { /* Find numbers with the passed names in the SettingsNP property */ INumber *eqp = IUFindNumber(&SettingsNP, names[i]); /* If the number found is (SettingsN[0]) then process it */ if (eqp == &SettingsN[0]) { new_duty = (values[i]); nset += static_cast(new_duty >= 0 && new_duty <= 255); } else if (eqp == &SettingsN[1]) { new_delay = (values[i]); nset += static_cast(new_delay >= 0 && new_delay <= 255); } else if (eqp == &SettingsN[2]) { new_ticks = (values[i]); nset += static_cast(new_ticks >= 0 && new_ticks <= 255); } } /* Did we process the three numbers? */ if (nset == 3) { /* Set the robofocus state to BUSY */ SettingsNP.s = IPS_BUSY; IDSetNumber(&SettingsNP, nullptr); if ((ret = updateRFMotorSettings(&new_duty, &new_delay, &new_ticks)) < 0) { IDSetNumber(&SettingsNP, "Changing to new settings failed"); return false; } currentDuty = new_duty; currentDelay = new_delay; currentTicks = new_ticks; SettingsNP.s = IPS_OK; IDSetNumber(&SettingsNP, "Motor settings are now %3.0f %3.0f %3.0f", currentDuty, currentDelay, currentTicks); return true; } else { /* Set property state to idle */ SettingsNP.s = IPS_IDLE; IDSetNumber(&SettingsNP, "Settings absent or bogus."); return false; } } if (strcmp(name, SetBacklashNP.name) == 0) { double new_back = 0; int nset = 0; int ret = -1; for (nset = i = 0; i < n; i++) { /* Find numbers with the passed names in the SetBacklashNP property */ INumber *eqp = IUFindNumber(&SetBacklashNP, names[i]); /* If the number found is SetBacklash (SetBacklashN[0]) then process it */ if (eqp == &SetBacklashN[0]) { new_back = (values[i]); /* limits */ nset += static_cast(new_back >= -0xff && new_back <= 0xff); } if (nset == 1) { /* Set the robofocus state to BUSY */ SetBacklashNP.s = IPS_BUSY; IDSetNumber(&SetBacklashNP, nullptr); if ((ret = updateRFBacklash(&new_back)) < 0) { SetBacklashNP.s = IPS_IDLE; IDSetNumber(&SetBacklashNP, "Setting new backlash failed."); return false; } currentSetBacklash = new_back; SetBacklashNP.s = IPS_OK; IDSetNumber(&SetBacklashNP, "Backlash is now %3.0f", currentSetBacklash); return true; } else { SetBacklashNP.s = IPS_IDLE; IDSetNumber(&SetBacklashNP, "Need exactly one parameter."); return false; } } } if (strcmp(name, MinMaxPositionNP.name) == 0) { /* new positions */ double new_min = 0; double new_max = 0; for (nset = i = 0; i < n; i++) { /* Find numbers with the passed names in the MinMaxPositionNP property */ INumber *mmpp = IUFindNumber(&MinMaxPositionNP, names[i]); /* If the number found is (MinMaxPositionN[0]) then process it */ if (mmpp == &MinMaxPositionN[0]) { new_min = (values[i]); nset += static_cast(new_min >= 1 && new_min <= 65000); } else if (mmpp == &MinMaxPositionN[1]) { new_max = (values[i]); nset += static_cast(new_max >= 1 && new_max <= 65000); } } /* Did we process the two numbers? */ if (nset == 2) { /* Set the robofocus state to BUSY */ MinMaxPositionNP.s = IPS_BUSY; currentMinPosition = new_min; currentMaxPosition = new_max; MinMaxPositionNP.s = IPS_OK; IDSetNumber(&MinMaxPositionNP, "Minimum and Maximum settings are now %3.0f %3.0f", currentMinPosition, currentMaxPosition); return true; } else { /* Set property state to idle */ MinMaxPositionNP.s = IPS_IDLE; IDSetNumber(&MinMaxPositionNP, "Minimum and maximum limits absent or bogus."); return false; } } if (strcmp(name, MaxTravelNP.name) == 0) { double new_maxt = 0; int ret = -1; for (nset = i = 0; i < n; i++) { /* Find numbers with the passed names in the MinMaxPositionNP property */ INumber *mmpp = IUFindNumber(&MaxTravelNP, names[i]); /* If the number found is (MaxTravelN[0]) then process it */ if (mmpp == &MaxTravelN[0]) { new_maxt = (values[i]); nset += static_cast(new_maxt >= 1 && new_maxt <= 64000); } } /* Did we process the one number? */ if (nset == 1) { IDSetNumber(&MinMaxPositionNP, nullptr); if ((ret = updateRFMaxPosition(&new_maxt)) < 0) { MaxTravelNP.s = IPS_IDLE; IDSetNumber(&MaxTravelNP, "Changing to new maximum travel failed"); return false; } currentMaxTravel = new_maxt; MaxTravelNP.s = IPS_OK; IDSetNumber(&MaxTravelNP, "Maximum travel is now %3.0f", currentMaxTravel); return true; } else { /* Set property state to idle */ MaxTravelNP.s = IPS_IDLE; IDSetNumber(&MaxTravelNP, "Maximum travel absent or bogus."); return false; } } if (strcmp(name, SetRegisterPositionNP.name) == 0) { double new_apos = 0; int nset = 0; int ret = -1; for (nset = i = 0; i < n; i++) { /* Find numbers with the passed names in the SetRegisterPositionNP property */ INumber *srpp = IUFindNumber(&SetRegisterPositionNP, names[i]); /* If the number found is SetRegisterPosition (SetRegisterPositionN[0]) then process it */ if (srpp == &SetRegisterPositionN[0]) { new_apos = (values[i]); /* limits are absolute */ nset += static_cast(new_apos >= 0 && new_apos <= 64000); } if (nset == 1) { if ((new_apos < currentMinPosition) || (new_apos > currentMaxPosition)) { SetRegisterPositionNP.s = IPS_ALERT; IDSetNumber(&SetRegisterPositionNP, "Value out of limits %5.0f", new_apos); return false; } /* Set the robofocus state to BUSY */ SetRegisterPositionNP.s = IPS_BUSY; IDSetNumber(&SetRegisterPositionNP, nullptr); if ((ret = updateRFSetPosition(&new_apos)) < 0) { SetRegisterPositionNP.s = IPS_OK; IDSetNumber(&SetRegisterPositionNP, "Read out of the set position to %3d failed. Trying to recover the position", ret); if ((ret = updateRFPosition(¤tPosition)) < 0) { FocusAbsPosNP.s = IPS_ALERT; IDSetNumber(&FocusAbsPosNP, "Unknown error while reading Robofocus position: %d", ret); SetRegisterPositionNP.s = IPS_IDLE; IDSetNumber(&SetRegisterPositionNP, "Relative movement failed."); } SetRegisterPositionNP.s = IPS_OK; IDSetNumber(&SetRegisterPositionNP, nullptr); FocusAbsPosNP.s = IPS_OK; IDSetNumber(&FocusAbsPosNP, "Robofocus position recovered %5.0f", currentPosition); LOG_DEBUG("Robofocus position recovered resuming normal operation"); /* We have to leave here, because new_apos is not set */ return true; } currentPosition = new_apos; SetRegisterPositionNP.s = IPS_OK; IDSetNumber(&SetRegisterPositionNP, "Robofocus register set to %5.0f", currentPosition); FocusAbsPosNP.s = IPS_OK; IDSetNumber(&FocusAbsPosNP, "Robofocus position is now %5.0f", currentPosition); return true; } else { SetRegisterPositionNP.s = IPS_IDLE; IDSetNumber(&SetRegisterPositionNP, "Need exactly one parameter."); return false; } if ((ret = updateRFPosition(¤tPosition)) < 0) { FocusAbsPosNP.s = IPS_ALERT; LOGF_ERROR("Unknown error while reading Robofocus position: %d", ret); IDSetNumber(&FocusAbsPosNP, nullptr); return false; } SetRegisterPositionNP.s = IPS_OK; SetRegisterPositionN[0].value = currentPosition; IDSetNumber(&SetRegisterPositionNP, "Robofocus has accepted new register setting"); FocusAbsPosNP.s = IPS_OK; LOGF_INFO("Robofocus new position %5.0f", currentPosition); IDSetNumber(&FocusAbsPosNP, nullptr); return true; } } } return INDI::Focuser::ISNewNumber(dev, name, values, names, n); } void RoboFocus::GetFocusParams() { int ret = -1; int cur_s1LL = 0; int cur_s2LR = 0; int cur_s3RL = 0; int cur_s4RR = 0; if ((ret = updateRFPosition(¤tPosition)) < 0) { FocusAbsPosNP.s = IPS_ALERT; LOGF_ERROR("Unknown error while reading Robofocus position: %d", ret); IDSetNumber(&FocusAbsPosNP, nullptr); return; } FocusAbsPosNP.s = IPS_OK; IDSetNumber(&FocusAbsPosNP, nullptr); if ((ret = updateRFTemperature(¤tTemperature)) < 0) { TemperatureNP.s = IPS_ALERT; LOG_ERROR("Unknown error while reading Robofocus temperature."); IDSetNumber(&TemperatureNP, nullptr); return; } TemperatureNP.s = IPS_OK; IDSetNumber(&TemperatureNP, nullptr); currentBacklash = BACKLASH_READOUT; if ((ret = updateRFBacklash(¤tBacklash)) < 0) { SetBacklashNP.s = IPS_ALERT; LOG_ERROR("Unknown error while reading Robofocus backlash."); IDSetNumber(&SetBacklashNP, nullptr); return; } SetBacklashNP.s = IPS_OK; IDSetNumber(&SetBacklashNP, nullptr); currentDuty = currentDelay = currentTicks = 0; if ((ret = updateRFMotorSettings(¤tDuty, ¤tDelay, ¤tTicks)) < 0) { SettingsNP.s = IPS_ALERT; LOG_ERROR("Unknown error while reading Robofocus motor settings."); IDSetNumber(&SettingsNP, nullptr); return; } SettingsNP.s = IPS_OK; IDSetNumber(&SettingsNP, nullptr); if ((ret = updateRFPowerSwitches(-1, -1, &cur_s1LL, &cur_s2LR, &cur_s3RL, &cur_s4RR)) < 0) { PowerSwitchesSP.s = IPS_ALERT; LOG_ERROR("Unknown error while reading Robofocus power switch settings."); IDSetSwitch(&PowerSwitchesSP, nullptr); return; } PowerSwitchesS[0].s = PowerSwitchesS[1].s = PowerSwitchesS[2].s = PowerSwitchesS[3].s = ISS_OFF; if (cur_s1LL == ISS_ON) { PowerSwitchesS[0].s = ISS_ON; } if (cur_s2LR == ISS_ON) { PowerSwitchesS[1].s = ISS_ON; } if (cur_s3RL == ISS_ON) { PowerSwitchesS[2].s = ISS_ON; } if (cur_s4RR == ISS_ON) { PowerSwitchesS[3].s = ISS_ON; } PowerSwitchesSP.s = IPS_OK; IDSetSwitch(&PowerSwitchesSP, nullptr); currentMaxTravel = MAXTRAVEL_READOUT; if ((ret = updateRFMaxPosition(¤tMaxTravel)) < 0) { MaxTravelNP.s = IPS_ALERT; LOG_ERROR("Unknown error while reading Robofocus maximum travel"); IDSetNumber(&MaxTravelNP, nullptr); return; } MaxTravelNP.s = IPS_OK; IDSetNumber(&MaxTravelNP, nullptr); } IPState RoboFocus::MoveAbsFocuser(uint32_t targetTicks) { int ret = -1; targetPos = targetTicks; if (targetTicks < FocusAbsPosN[0].min || targetTicks > FocusAbsPosN[0].max) { LOG_DEBUG("Error, requested position is out of range."); return IPS_ALERT; } if ((ret = updateRFPositionAbsolute(targetPos)) < 0) { LOGF_DEBUG("Read out of the absolute movement failed %3d", ret); return IPS_ALERT; } RemoveTimer(timerID); timerID = SetTimer(250); return IPS_BUSY; } IPState RoboFocus::MoveRelFocuser(FocusDirection dir, uint32_t ticks) { return MoveAbsFocuser(FocusAbsPosN[0].value + (ticks * (dir == FOCUS_INWARD ? -1 : 1))); } bool RoboFocus::saveConfigItems(FILE *fp) { IUSaveConfigNumber(fp, &SettingsNP); IUSaveConfigNumber(fp, &SetBacklashNP); return INDI::Focuser::saveConfigItems(fp); } void RoboFocus::TimerHit() { if (!isConnected()) return; double prevPos = currentPosition; double newPos = 0; if (FocusAbsPosNP.s == IPS_OK || FocusAbsPosNP.s == IPS_IDLE) { int rc = updateRFPosition(&newPos); if (rc >= 0) { currentPosition = newPos; if (prevPos != currentPosition) IDSetNumber(&FocusAbsPosNP, nullptr); } } else if (FocusAbsPosNP.s == IPS_BUSY) { float newPos = 0; int nbytes_read = 0; char rf_cmd[RF_MAX_CMD] = { 0 }; //nbytes_read= ReadUntilComplete(rf_cmd, RF_TIMEOUT) ; nbytes_read = ReadResponse(rf_cmd); rf_cmd[nbytes_read - 1] = 0; if (nbytes_read != 9 || (sscanf(rf_cmd, "FD0%5f", &newPos) <= 0)) { DEBUGF(INDI::Logger::DBG_WARNING, "Bogus position: (%#02X %#02X %#02X %#02X %#02X %#02X %#02X %#02X %#02X) - Bytes read: %d", rf_cmd[0], rf_cmd[1], rf_cmd[2], rf_cmd[3], rf_cmd[4], rf_cmd[5], rf_cmd[6], rf_cmd[7], rf_cmd[8], nbytes_read); timerID = SetTimer(POLLMS); return; } else if (nbytes_read < 0) { FocusAbsPosNP.s = IPS_ALERT; LOG_ERROR("Read error! Reconnect and try again."); IDSetNumber(&FocusAbsPosNP, nullptr); return; } currentPosition = newPos; if (currentPosition == targetPos) { FocusAbsPosNP.s = IPS_OK; if (FocusRelPosNP.s == IPS_BUSY) { FocusRelPosNP.s = IPS_OK; IDSetNumber(&FocusRelPosNP, nullptr); } } IDSetNumber(&FocusAbsPosNP, nullptr); if (FocusAbsPosNP.s == IPS_BUSY) { timerID = SetTimer(250); return; } } timerID = SetTimer(POLLMS); } bool RoboFocus::AbortFocuser() { LOG_DEBUG("Aborting focuser..."); int nbytes_written; const char *buf = "\r"; return tty_write(PortFD, buf, strlen(buf), &nbytes_written) == TTY_OK; } libindi/drivers/focuser/nfocus.cpp0000664000175000017500000006700313263645557016625 0ustar jasemjasem/* NFocus Copyright (C) 2013 Felix Krämer (rigelsys@felix-kraemer.de) Based on the work for robofocus by 2006 Markus Wildi (markus.wildi@datacomm.ch) 2011 Jasem Mutlaq (mutlaqja@ikarustech.com) 2017-05-31: Jasem Mutlaq + Removed obsolete properties and functions + Added proper Sync. + Using INDI::Logger whenever possible. + Removed timer-based motion. + Set Focuser capabilities on startup. + Removed duplicate properties. 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 */ #include "nfocus.h" #include "indicom.h" #include #include #include #define NF_MAX_CMD 8 /* cmd length */ #define NF_TIMEOUT 15 /* com timeout */ #define NF_STEP_RES 5 /* step res for a given time period */ #define NF_MAX_TRIES 3 #define NF_MAX_DELAY 100000 #define BACKLASH_READOUT 0 #define MAXTRAVEL_READOUT 99999 #define INOUTSCALAR_READOUT 1 #define currentSpeed SpeedN[0].value #define currentPosition FocusAbsPosN[0].value #define currentTemperature TemperatureN[0].value #define currentOnTime SettingsN[0].value #define currentOffTime SettingsN[1].value #define currentFastDelay SettingsN[2].value #define currentInOutScalar InOutScalarN[0].value #define currentRelativeMovement FocusRelPosN[0].value #define currentAbsoluteMovement FocusAbsPosN[0].value #define currentSetBacklash SetBacklashN[0].value #define currentMinPosition MinMaxPositionN[0].value #define currentMaxPosition MinMaxPositionN[1].value #define currentMaxTravel MaxTravelN[0].value std::unique_ptr nFocus(new NFocus()); void ISGetProperties(const char *dev) { nFocus->ISGetProperties(dev); } void ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) { nFocus->ISNewSwitch(dev, name, states, names, n); } void ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n) { nFocus->ISNewText(dev, name, texts, names, n); } void ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) { nFocus->ISNewNumber(dev, name, values, names, n); } void ISNewBLOB(const char *dev, const char *name, int sizes[], int blobsizes[], char *blobs[], char *formats[], char *names[], int n) { INDI_UNUSED(dev); INDI_UNUSED(name); INDI_UNUSED(sizes); INDI_UNUSED(blobsizes); INDI_UNUSED(blobs); INDI_UNUSED(formats); INDI_UNUSED(names); INDI_UNUSED(n); } void ISSnoopDevice(XMLEle *root) { nFocus->ISSnoopDevice(root); } NFocus::NFocus() { FI::SetCapability(FOCUSER_CAN_ABS_MOVE | FOCUSER_CAN_REL_MOVE); } bool NFocus::initProperties() { INDI::Focuser::initProperties(); // No speed for nfocus FocusSpeedN[0].min = FocusSpeedN[0].max = FocusSpeedN[0].value = 1; IUUpdateMinMax(&FocusSpeedNP); /* Focuser temperature */ IUFillNumber(&TemperatureN[0], "TEMPERATURE", "Celsius", "%6.2f", 0, 65000., 0., 10000.); IUFillNumberVector(&TemperatureNP, TemperatureN, 1, getDeviceName(), "FOCUS_TEMPERATURE", "Temperature", MAIN_CONTROL_TAB, IP_RO, 0, IPS_IDLE); /* Settings of the Nfocus */ IUFillNumber(&SettingsN[0], "ON time", "ON waiting time", "%6.0f", 10., 250., 0., 73.); IUFillNumber(&SettingsN[1], "OFF time", "OFF waiting time", "%6.0f", 1., 250., 0., 15.); IUFillNumber(&SettingsN[2], "Fast Mode Delay", "Fast Mode Delay", "%6.0f", 0., 255., 0., 9.); IUFillNumberVector(&SettingsNP, SettingsN, 3, getDeviceName(), "FOCUS_SETTINGS", "Settings", OPTIONS_TAB, IP_RW, 0, IPS_IDLE); /* tick scaling factor. Inward dir ticks times this factor is considered to be the same as outward dir tick numbers. This is to compensate for DC motor incapabilities with regards to exact positioning at least a bit on the mass dependent path.... */ IUFillNumber(&InOutScalarN[0], "In/Out Scalar", "In/Out Scalar", "%1.2f", 0., 2., 1., 1.0); IUFillNumberVector(&InOutScalarNP, InOutScalarN, 1, getDeviceName(), "FOCUS_DIRSCALAR", "Direction scaling factor", OPTIONS_TAB, IP_RW, 0, IPS_IDLE); /* Nfocus should stay within these limits */ IUFillNumber(&MinMaxPositionN[0], "MINPOS", "Minimum Tick", "%6.0f", -65000., 65000., 0., -65000.); IUFillNumber(&MinMaxPositionN[1], "MAXPOS", "Maximum Tick", "%6.0f", 1., 65000., 0., 65000.); IUFillNumberVector(&MinMaxPositionNP, MinMaxPositionN, 2, getDeviceName(), "FOCUS_MINMAXPOSITION", "Extrema", OPTIONS_TAB, IP_RW, 0, IPS_IDLE); IUFillNumber(&MaxTravelN[0], "MAXTRAVEL", "Maximum travel", "%6.0f", 1., 64000., 0., 10000.); IUFillNumberVector(&MaxTravelNP, MaxTravelN, 1, getDeviceName(), "FOCUS_MAXTRAVEL", "Max. travel", OPTIONS_TAB, IP_RW, 0, IPS_IDLE); /* Set Nfocus position register to this position */ IUFillNumber(&SyncN[0], "FOCUS_SYNC_OFFSET", "Offset", "%.f", 0, 64000., 0., 0.); IUFillNumberVector(&SyncNP, SyncN, 1, getDeviceName(), "FOCUS_SYNC", "Sync", MAIN_CONTROL_TAB, IP_RW, 0, IPS_IDLE); /* Relative and absolute movement */ FocusRelPosN[0].min = 0; FocusRelPosN[0].max = MinMaxPositionN[1].value; FocusRelPosN[0].value = 1000; FocusRelPosN[0].step = 100; FocusAbsPosN[0].min = MinMaxPositionN[0].min; FocusAbsPosN[0].max = MinMaxPositionN[0].max; FocusAbsPosN[0].value = 0; FocusAbsPosN[0].step = 10000; addAuxControls(); return true; } bool NFocus::updateProperties() { INDI::Focuser::updateProperties(); if (isConnected()) { defineNumber(&TemperatureNP); defineNumber(&SettingsNP); defineNumber(&InOutScalarNP); defineNumber(&MinMaxPositionNP); defineNumber(&MaxTravelNP); defineNumber(&SyncNP); if (GetFocusParams()) LOG_INFO("NFocus is ready."); } else { deleteProperty(TemperatureNP.name); deleteProperty(SettingsNP.name); deleteProperty(InOutScalarNP.name); deleteProperty(MinMaxPositionNP.name); deleteProperty(MaxTravelNP.name); deleteProperty(SyncNP.name); } return true; } bool NFocus::Handshake() { // TODO add an actual check here return true; } const char *NFocus::getDefaultName() { return "NFocus"; } int NFocus::SendCommand(char *rf_cmd) { int nbytes_written = 0, err_code = 0; char nfocus_error[MAXRBUF]; LOGF_DEBUG("CMD <%s>", rf_cmd); if (!isSimulation()) { tcflush(PortFD, TCIOFLUSH); if ((err_code = tty_write(PortFD, rf_cmd, strlen(rf_cmd) + 1, &nbytes_written) != TTY_OK)) { tty_error_msg(err_code, nfocus_error, MAXRBUF); LOGF_ERROR("TTY error detected: %s", nfocus_error); return -1; } } return 0; } int NFocus::SendRequest(char *rf_cmd) { int nbytes_read = 0; LOGF_DEBUG("CMD <%s>", rf_cmd); SendCommand(rf_cmd); if (!isSimulation()) { nbytes_read = ReadResponse(rf_cmd, strlen(rf_cmd), NF_TIMEOUT); if (nbytes_read < 1) return nbytes_read; rf_cmd[nbytes_read - 1] = 0; } LOGF_DEBUG("RES <%s>", rf_cmd); return 0; } int NFocus::ReadResponse(char *buf, int nbytes, int timeout) { char nfocus_error[MAXRBUF]; int bytesRead = 0; int totalBytesRead = 0; int err_code; while (totalBytesRead < nbytes) { if ((err_code = tty_read(PortFD, buf + totalBytesRead, nbytes - totalBytesRead, timeout, &bytesRead)) != TTY_OK) { tty_error_msg(err_code, nfocus_error, MAXRBUF); LOGF_ERROR("TTY error detected: %s", nfocus_error); return -1; } if (bytesRead < 0) { LOG_ERROR("TTY error detected: Bytes read < 0"); return -1; } totalBytesRead += bytesRead; } tcflush(PortFD, TCIOFLUSH); LOGF_DEBUG("RES <%s>", buf); /* if (isDebug()) { fprintf(stderr, "READ : (%s,%d), %d\n", buf, 9, totalBytesRead) ; fprintf(stderr, "READ : ") ; for(int i = 0; i < 9; i++) { fprintf(stderr, "0x%2x ", (unsigned char)buf[i]) ; } }*/ return 9; } int NFocus::updateNFTemperature(double *value) { float temp; char rf_cmd[32]; int ret_read_tmp; strncpy(rf_cmd, ":RT", 4); if ((ret_read_tmp = SendRequest(rf_cmd)) < 0) return ret_read_tmp; // Always set to 20C for simulation if (isSimulation()) strncpy(rf_cmd, "200", 32); if (sscanf(rf_cmd, "%6f", &temp) == -888) return -1; *value = (double)temp / 10.; return 0; } int NFocus::updateNFInOutScalar(double *value) { double temp = currentInOutScalar; *value = (double)temp; return temp; } int NFocus::updateNFMotorSettings(double *onTime, double *offTime, double *fastDelay) { char rf_cmd[32]; int ret_read_tmp; if (isSimulation()) return 0; if ((*onTime >= 100) && (*onTime <= 250)) { snprintf(rf_cmd, 32, ":CO%3d#", (int)*onTime); } else if ((*onTime >= 10) && (*onTime <= 99)) { snprintf(rf_cmd, 32, ":CO0%2d#", (int)*onTime); } if ((ret_read_tmp = SendCommand(rf_cmd)) < 0) return ret_read_tmp; strncpy(rf_cmd, ":RO\0", 4); if ((ret_read_tmp = SendRequest(rf_cmd)) < 0) return ret_read_tmp; *onTime = (float)atof(rf_cmd); if ((*offTime >= 100) && (*offTime <= 250)) { snprintf(rf_cmd, 32, ":CF%3d#", (int)*offTime); } else if ((*offTime >= 10) && (*offTime <= 99)) { snprintf(rf_cmd, 32, ":CF0%2d#", (int)*offTime); } else if ((*offTime >= 1) && (*offTime <= 9)) { snprintf(rf_cmd, 32, ":CF00%1d#", (int)*offTime); } if ((ret_read_tmp = SendCommand(rf_cmd)) < 0) return ret_read_tmp; strncpy(rf_cmd, ":RF\0", 4); if ((ret_read_tmp = SendRequest(rf_cmd)) < 0) return ret_read_tmp; *offTime = (float)atof(rf_cmd); if ((*fastDelay >= 1) && (*fastDelay <= 9)) { snprintf(rf_cmd, 32, ":CS00%1d#", (int)*fastDelay); } if ((ret_read_tmp = SendCommand(rf_cmd)) < 0) return ret_read_tmp; strncpy(rf_cmd, ":RS\0", 4); if ((ret_read_tmp = SendRequest(rf_cmd)) < 0) return ret_read_tmp; *fastDelay = (float)atof(rf_cmd); return 0; } int NFocus::moveNFInward(const double *value) { char rf_cmd[32]; int ret_read_tmp; rf_cmd[0] = 0; int newval = (int)(currentInOutScalar * (*value)); LOGF_DEBUG("Moving %d real steps but virtually counting %.0f", newval, *value); if (!isSimulation()) { do { if (newval > 999) { strncpy(rf_cmd, ":F01999#\0", 9); newval -= 999; } else if (newval > 99) { snprintf(rf_cmd, 32, ":F01%3d#", (int)newval); newval = 0; } else if (newval > 9) { snprintf(rf_cmd, 32, ":F010%2d#", (int)newval); newval = 0; } else { snprintf(rf_cmd, 32, ":F0100%1d#", (int)newval); newval = 0; } if ((ret_read_tmp = SendCommand(rf_cmd)) < 0) return ret_read_tmp; do { strncpy(rf_cmd, "S\0", 2); if ((ret_read_tmp = SendRequest(rf_cmd)) < 0) return ret_read_tmp; } while (atoi(rf_cmd) != 0); } while (newval > 0); } currentPosition -= *value; return 0; } int NFocus::moveNFOutward(const double *value) { char rf_cmd[32]; int ret_read_tmp; rf_cmd[0] = 0; int newval = (int)(*value); if (!isSimulation()) { do { if (newval > 999) { strncpy(rf_cmd, ":F11999#\0", 9); newval -= 999; } else if (newval > 99) { snprintf(rf_cmd, 32, ":F11%3d#", (int)newval); newval = 0; } else if (newval > 9) { snprintf(rf_cmd, 32, ":F110%2d#", (int)newval); newval = 0; } else { snprintf(rf_cmd, 32, ":F1100%1d#", (int)newval); newval = 0; } if ((ret_read_tmp = SendCommand(rf_cmd)) < 0) return ret_read_tmp; do { strncpy(rf_cmd, "S\0", 2); if ((ret_read_tmp = SendRequest(rf_cmd)) < 0) return ret_read_tmp; } while (atoi(rf_cmd) != 0); } while (newval > 0); } currentPosition += *value; return 0; } int NFocus::setNFAbsolutePosition(const double *value) { double newAbsPos = 0; int rc = 0; if ((*value - currentPosition) >= 0) { newAbsPos = *value - currentPosition; rc = moveNFOutward(&newAbsPos); } else if ((*value - currentPosition) < 0) { newAbsPos = currentPosition - *value; rc = moveNFInward(&newAbsPos); } return rc; } int NFocus::setNFMaxPosition(double *value) { float temp; char rf_cmd[32]; char vl_tmp[6]; int ret_read_tmp; char waste[1]; if (isSimulation()) return 0; if (*value == MAXTRAVEL_READOUT) { strncpy(rf_cmd, "FL000000", 9); } else { rf_cmd[0] = 'F'; rf_cmd[1] = 'L'; rf_cmd[2] = '0'; if (*value > 9999) { snprintf(vl_tmp, 6, "%5d", (int)*value); } else if (*value > 999) { snprintf(vl_tmp, 6, "0%4d", (int)*value); } else if (*value > 99) { snprintf(vl_tmp, 6, "00%3d", (int)*value); } else if (*value > 9) { snprintf(vl_tmp, 6, "000%2d", (int)*value); } else { snprintf(vl_tmp, 6, "0000%1d", (int)*value); } rf_cmd[3] = vl_tmp[0]; rf_cmd[4] = vl_tmp[1]; rf_cmd[5] = vl_tmp[2]; rf_cmd[6] = vl_tmp[3]; rf_cmd[7] = vl_tmp[4]; rf_cmd[8] = 0; } if ((ret_read_tmp = SendCommand(rf_cmd)) < 0) return ret_read_tmp; if (sscanf(rf_cmd, "FL%1c%5f", waste, &temp) < 1) return -1; *value = (double)temp; return 0; } int NFocus::syncNF(const double *value) { char rf_cmd[32]; char vl_tmp[6]; int ret_read_tmp; if (isSimulation()) { currentPosition = *value; return 0; } rf_cmd[0] = 'F'; rf_cmd[1] = 'S'; rf_cmd[2] = '0'; if (*value > 9999) { snprintf(vl_tmp, 6, "%5d", (int)*value); } else if (*value > 999) { snprintf(vl_tmp, 6, "0%4d", (int)*value); } else if (*value > 99) { snprintf(vl_tmp, 6, "00%3d", (int)*value); } else if (*value > 9) { snprintf(vl_tmp, 6, "000%2d", (int)*value); } else { snprintf(vl_tmp, 6, "0000%1d", (int)*value); } rf_cmd[3] = vl_tmp[0]; rf_cmd[4] = vl_tmp[1]; rf_cmd[5] = vl_tmp[2]; rf_cmd[6] = vl_tmp[3]; rf_cmd[7] = vl_tmp[4]; rf_cmd[8] = 0; if ((ret_read_tmp = SendCommand(rf_cmd)) < 0) return ret_read_tmp; return 0; } bool NFocus::ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) { int nset = 0, i = 0; if (dev != nullptr && strcmp(dev, getDeviceName()) == 0) { if (strcmp(name, SettingsNP.name) == 0) { /* new speed */ double new_onTime = 0; double new_offTime = 0; double new_fastDelay = 0; int ret = -1; for (nset = i = 0; i < n; i++) { /* Find numbers with the passed names in the SettingsNP property */ INumber *eqp = IUFindNumber(&SettingsNP, names[i]); /* If the number found is (SettingsN[0]) then process it */ if (eqp == &SettingsN[0]) { new_onTime = (values[i]); nset += static_cast(new_onTime >= 10 && new_onTime <= 250); } else if (eqp == &SettingsN[1]) { new_offTime = (values[i]); nset += static_cast(new_offTime >= 1 && new_offTime <= 250); } else if (eqp == &SettingsN[2]) { new_fastDelay = (values[i]); nset += static_cast(new_fastDelay >= 1 && new_fastDelay <= 9); } } /* Did we process the three numbers? */ if (nset == 3) { if ((ret = updateNFMotorSettings(&new_onTime, &new_offTime, &new_fastDelay)) < 0) { IDSetNumber(&SettingsNP, "Changing to new settings failed"); return false; } currentOnTime = new_onTime; currentOffTime = new_offTime; currentFastDelay = new_fastDelay; SettingsNP.s = IPS_OK; IDSetNumber(&SettingsNP, "Motor settings are now %3.0f %3.0f %3.0f", currentOnTime, currentOffTime, currentFastDelay); return true; } else { /* Set property state to idle */ SettingsNP.s = IPS_IDLE; IDSetNumber(&SettingsNP, "Settings absent or bogus."); return false; } } if (strcmp(name, InOutScalarNP.name) == 0) { double new_ioscalar = 0; for (nset = i = 0; i < n; i++) { /* Find numbers with the passed names in the InOutScalarNP property */ INumber *eqp = IUFindNumber(&InOutScalarNP, names[i]); /* If the number found is SetBacklash (SetBacklashN[0]) then process it */ if (eqp == &InOutScalarN[0]) { new_ioscalar = (values[i]); /* limits */ nset += static_cast(new_ioscalar >= 0 && new_ioscalar <= 2); } if (nset == 1) { /* Set the nfocus state to BUSY */ InOutScalarNP.s = IPS_BUSY; /* kraemerf IDSetNumber(&InOutScalarNP, nullptr); if(( ret= updateNFInOutScalar(&new_ioscalar)) < 0) { InOutScalarNP.s = IPS_IDLE; IDSetNumber(&InOutScalarNP, "Setting new in/out scalar failed."); return false ; } */ currentInOutScalar = new_ioscalar; InOutScalarNP.s = IPS_OK; IDSetNumber(&InOutScalarNP, "Direction Scalar is now %1.2f", currentInOutScalar); return true; } else { InOutScalarNP.s = IPS_IDLE; IDSetNumber(&InOutScalarNP, "Need exactly one parameter."); return false; } } } if (strcmp(name, MinMaxPositionNP.name) == 0) { /* new positions */ double new_min = 0; double new_max = 0; for (nset = i = 0; i < n; i++) { /* Find numbers with the passed names in the MinMaxPositionNP property */ INumber *mmpp = IUFindNumber(&MinMaxPositionNP, names[i]); /* If the number found is (MinMaxPositionN[0]) then process it */ if (mmpp == &MinMaxPositionN[0]) { new_min = (values[i]); nset += static_cast(new_min >= 1 && new_min <= 65000); } else if (mmpp == &MinMaxPositionN[1]) { new_max = (values[i]); nset += static_cast(new_max >= 1 && new_max <= 65000); } } /* Did we process the two numbers? */ if (nset == 2) { /* Set the nfocus state to BUSY */ MinMaxPositionNP.s = IPS_BUSY; currentMinPosition = new_min; currentMaxPosition = new_max; MinMaxPositionNP.s = IPS_OK; IDSetNumber(&MinMaxPositionNP, "Minimum and Maximum settings are now %3.0f %3.0f", currentMinPosition, currentMaxPosition); return true; } else { /* Set property state to idle */ MinMaxPositionNP.s = IPS_IDLE; IDSetNumber(&MinMaxPositionNP, "Minimum and maximum limits absent or bogus."); return false; } } if (strcmp(name, MaxTravelNP.name) == 0) { double new_maxt = 0; int ret = -1; for (nset = i = 0; i < n; i++) { /* Find numbers with the passed names in the MinMaxPositionNP property */ INumber *mmpp = IUFindNumber(&MaxTravelNP, names[i]); /* If the number found is (MaxTravelN[0]) then process it */ if (mmpp == &MaxTravelN[0]) { new_maxt = (values[i]); nset += static_cast(new_maxt >= 1 && new_maxt <= 64000); } } /* Did we process the one number? */ if (nset == 1) { IDSetNumber(&MinMaxPositionNP, nullptr); if ((ret = setNFMaxPosition(&new_maxt)) < 0) { MaxTravelNP.s = IPS_IDLE; IDSetNumber(&MaxTravelNP, "Changing to new maximum travel failed"); return false; } currentMaxTravel = new_maxt; MaxTravelNP.s = IPS_OK; FocusAbsPosN[0].max = currentMaxTravel; IUUpdateMinMax(&FocusAbsPosNP); IDSetNumber(&MaxTravelNP, "Maximum travel is now %3.0f", currentMaxTravel); return true; } else { /* Set property state to idle */ MaxTravelNP.s = IPS_IDLE; IDSetNumber(&MaxTravelNP, "Maximum travel absent or bogus."); return false; } } // Sync if (strcmp(name, SyncNP.name) == 0) { double new_apos = values[0]; int rc = 0; if ((new_apos < currentMinPosition) || (new_apos > currentMaxPosition)) { SyncNP.s = IPS_ALERT; IDSetNumber(&SyncNP, "Value out of limits %5.0f", new_apos); return false; } if ((rc = syncNF(&new_apos)) < 0) { SyncNP.s = IPS_ALERT; IDSetNumber(&SyncNP, "Read out of the set position to %3d failed.", rc); return false; } LOGF_DEBUG("Focuser sycned to %g ticks", new_apos); SyncN[0].value = new_apos; SyncNP.s = IPS_OK; IDSetNumber(&SyncNP, nullptr); IDSetNumber(&FocusAbsPosNP, nullptr); return true; } } return INDI::Focuser::ISNewNumber(dev, name, values, names, n); } int NFocus::getNFAbsolutePosition(double *value) { *value = currentPosition; return 0; } bool NFocus::GetFocusParams() { int ret = -1; currentInOutScalar = INOUTSCALAR_READOUT; if ((ret = updateNFInOutScalar(¤tInOutScalar)) < 0) { InOutScalarNP.s = IPS_ALERT; IDSetNumber(&InOutScalarNP, "Unknown error while reading Nfocus direction tick scalar"); return false; } InOutScalarNP.s = IPS_OK; IDSetNumber(&InOutScalarNP, nullptr); if ((ret = updateNFTemperature(¤tTemperature)) < 0) { TemperatureNP.s = IPS_ALERT; IDSetNumber(&TemperatureNP, "Unknown error while reading Nfocus temperature"); return false; } TemperatureNP.s = IPS_OK; IDSetNumber(&TemperatureNP, nullptr); currentOnTime = currentOffTime = currentFastDelay = 0; if ((ret = updateNFMotorSettings(¤tOnTime, ¤tOffTime, ¤tFastDelay)) < 0) { SettingsNP.s = IPS_ALERT; IDSetNumber(&SettingsNP, "Unknown error while reading Nfocus motor settings"); return false; } SettingsNP.s = IPS_OK; IDSetNumber(&SettingsNP, nullptr); currentMaxTravel = MAXTRAVEL_READOUT; if ((ret = setNFMaxPosition(¤tMaxTravel)) < 0) { MaxTravelNP.s = IPS_ALERT; IDSetNumber(&MaxTravelNP, "Unknown error while reading Nfocus maximum travel"); return false; } MaxTravelNP.s = IPS_OK; IDSetNumber(&MaxTravelNP, nullptr); return true; } IPState NFocus::MoveAbsFocuser(uint32_t targetTicks) { int ret = -1; double new_apos = targetTicks; LOGF_DEBUG("Focuser is moving to requested position %d", targetTicks); if ((ret = setNFAbsolutePosition(&new_apos)) < 0) { return IPS_ALERT; } return IPS_OK; } IPState NFocus::MoveRelFocuser(FocusDirection dir, uint32_t ticks) { double cur_rpos = 0; double new_rpos = 0; int ret = 0; bool nset = false; cur_rpos = new_rpos = ticks; /* CHECK 2006-01-26, limits are relative to the actual position */ nset = new_rpos >= -0xffff && new_rpos <= 0xffff; if (nset) { if (dir == FOCUS_OUTWARD) { if ((currentPosition + new_rpos < currentMinPosition) || (currentPosition + new_rpos > currentMaxPosition)) { IDMessage(getDeviceName(), "Value out of limits %5.0f", currentPosition + new_rpos); return IPS_ALERT; } ret = moveNFOutward(&new_rpos); } else { if ((currentPosition - new_rpos < currentMinPosition) || (currentPosition - new_rpos > currentMaxPosition)) { IDMessage(getDeviceName(), "Value out of limits %5.0f", currentPosition - new_rpos); return IPS_ALERT; } ret = moveNFInward(&new_rpos); } if (ret < 0) { // We have to leave here, because new_rpos is not set return IPS_ALERT; } currentRelativeMovement = cur_rpos; return IPS_OK; } { IDMessage(getDeviceName(), "Value out of limits."); return IPS_ALERT; } } bool NFocus::saveConfigItems(FILE *fp) { INDI::Focuser::saveConfigItems(fp); IUSaveConfigNumber(fp, &SettingsNP); IUSaveConfigNumber(fp, &InOutScalarNP); return true; } libindi/drivers/focuser/usbfocusv3.h0000664000175000017500000001401713263645557017074 0ustar jasemjasem/* USB Focus V3 Copyright (C) 2016 G. Schmidt 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 */ #pragma once #include "indifocuser.h" /***************************** USB Focus V3 Commands **************************/ #define UFOCREADPARAM "SGETAL" #define UFOCDEVID "SWHOIS" #define UFOCREADPOS "FPOSRO" #define UFOCREADTEMP "FTMPRO" #define UFOCMOVEOUT "O" #define UFOCMOVEIN "I" #define UFOCABORT "FQUITx" #define UFOCSETMAX "M" #define UFOCSETSPEED "SMO" #define UFOCSETTCTHR "SMA" #define UFOCSETSDIR "SMROTH" #define UFOCSETRDIR "SMROTT" #define UFOCSETFSTEPS "SMSTPF" #define UFOCSETHSTEPS "SMSTPD" #define UFOCSETSTDEG "FLA" #define UFOCGETSIGN "FTAXXA" #define UFOCSETSIGN "FZAXX" #define UFOCSETAUTO "FAMODE" #define UFOCSETMANU "FMMODE" #define UFOCRESET "SEERAZ" /**************************** USB Focus V3 Constants **************************/ #define UFOID "UFO" #define UFORSACK "*" #define UFORSEQU "=" #define UFORSAUTO "AP" #define UFORSDONE "DONE" #define UFORSERR "ER=" #define UFORSRESET "EEPROM RESET" #define UFOPSDIR 0 // standard direction #define UFOPRDIR 1 // reverse direction #define UFOPFSTEPS 0 // full steps #define UFOPHSTEPS 1 // half steps #define UFOPPSIGN 0 // positive temp. comp. sign #define UFOPNSIGN 1 // negative temp. comp. sign #define UFOPSPDERR 0 // invalid speed #define UFOPSPDAV 2 // average speed #define UFOPSPDSL 3 // slow speed #define UFOPSPDUS 4 // ultra slow speed #define UFORTEMPLEN 8 // maximum length of returned temperature string #define UFORSIGNLEN 3 // maximum length of temp. comp. sign string #define UFORPOSLEN 7 // maximum length of returned position string #define UFORSTLEN 26 // maximum length of returned status string #define UFORIDLEN 3 // maximum length of returned temperature string #define UFORDONELEN 4 // length of done response #define UFOCTLEN 6 // length of temp parameter setting commands #define UFOCMLEN 6 // length of move commands #define UFOCMMLEN 6 // length of max. move commands #define UFOCSLEN 6 // length of speed commands #define UFOCDLEN 6 // length of direction commands #define UFOCSMLEN 6 // length of step mode commands #define UFOCTCLEN 6 // length of temp compensation commands /******************************************************************************/ class USBFocusV3 : public INDI::Focuser { public: USBFocusV3(); virtual ~USBFocusV3() = default; typedef enum { FOCUS_HALF_STEP, FOCUS_FULL_STEP } FocusStepMode; virtual bool Handshake() override; bool getControllerStatus(); virtual const char *getDefaultName() override; virtual bool initProperties() override; virtual bool updateProperties() override; virtual bool ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) override; virtual bool ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) override; virtual IPState MoveAbsFocuser(uint32_t targetTicks) override; virtual IPState MoveRelFocuser(FocusDirection dir, uint32_t ticks) override; virtual bool SetFocuserSpeed(int speed) override; virtual bool AbortFocuser() override; virtual void TimerHit() override; private: bool oneMoreRead(char *response, unsigned int maxlen); void GetFocusParams(); bool reset(); bool updateStepMode(); bool updateRotDir(); bool updateTemperature(); bool updatePosition(); bool updateMaxPos(); bool updateTempCompSettings(); bool updateTempCompSign(); bool updateSpeed(); bool updateFWversion(); bool isMoving(); bool Ack(); bool MoveFocuserUF(FocusDirection dir, unsigned int rticks); bool setStepMode(FocusStepMode mode); bool setRotDir(unsigned int dir); bool setMaxPos(unsigned int maxp); bool setSpeed(unsigned short drvspeed); bool setAutoTempCompThreshold(unsigned int thr); bool setTemperatureCoefficient(unsigned int coefficient); bool setTempCompSign(unsigned int sign); bool setTemperatureCompensation(bool enable); float CalcTimeLeft(timeval, float); unsigned int direction { 0 }; // 0 standard, 1 reverse unsigned int stepmode { 0 }; // 0 full steps, 1 half steps unsigned int speed { 0 }; // 2 average, 3 slow, 4 ultra slow unsigned int stepsdeg { 0 }; // steps per degree for temperature compensation unsigned int tcomp_thr { 0 }; // temperature compensation threshold unsigned int firmware { 0 }; // firmware version unsigned int maxpos { 0 }; // maximum step position (0..65535) double targetPos { 0 }; double lastPos { 0 }; double lastTemperature { 0 }; unsigned int currentSpeed { 0 }; struct timeval focusMoveStart { 0, 0 }; float focusMoveRequest { 0 }; INumber TemperatureN[1]; INumberVectorProperty TemperatureNP; ISwitch StepModeS[2]; ISwitchVectorProperty StepModeSP; ISwitch RotDirS[2]; ISwitchVectorProperty RotDirSP; INumber MaxPositionN[1]; INumberVectorProperty MaxPositionNP; INumber TemperatureSettingN[2]; INumberVectorProperty TemperatureSettingNP; ISwitch TempCompSignS[2]; ISwitchVectorProperty TempCompSignSP; ISwitch TemperatureCompensateS[2]; ISwitchVectorProperty TemperatureCompensateSP; ISwitch ResetS[1]; ISwitchVectorProperty ResetSP; INumber FWversionN[1]; INumberVectorProperty FWversionNP; }; libindi/drivers/focuser/indi_tcfs_sk.xml0000664000175000017500000000255113263645557020002 0ustar jasemjasem On Off Off Off Off Off Off 0 Off Off libindi/drivers/focuser/moonlite.cpp0000664000175000017500000005411413263645557017155 0ustar jasemjasem/* Moonlite Focuser Copyright (C) 2013 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 */ #include "moonlite.h" #include "indicom.h" #include #include #include #include #include #define MOONLITE_TIMEOUT 3 std::unique_ptr moonLite(new MoonLite()); void ISGetProperties(const char *dev) { moonLite->ISGetProperties(dev); } void ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) { moonLite->ISNewSwitch(dev, name, states, names, n); } void ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n) { moonLite->ISNewText(dev, name, texts, names, n); } void ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) { moonLite->ISNewNumber(dev, name, values, names, n); } void ISNewBLOB(const char *dev, const char *name, int sizes[], int blobsizes[], char *blobs[], char *formats[], char *names[], int n) { INDI_UNUSED(dev); INDI_UNUSED(name); INDI_UNUSED(sizes); INDI_UNUSED(blobsizes); INDI_UNUSED(blobs); INDI_UNUSED(formats); INDI_UNUSED(names); INDI_UNUSED(n); } void ISSnoopDevice(XMLEle *root) { moonLite->ISSnoopDevice(root); } MoonLite::MoonLite() { // Can move in Absolute & Relative motions, can AbortFocuser motion, and has variable speed. FI::SetCapability(FOCUSER_CAN_ABS_MOVE | FOCUSER_CAN_REL_MOVE | FOCUSER_CAN_ABORT | FOCUSER_HAS_VARIABLE_SPEED); } bool MoonLite::initProperties() { INDI::Focuser::initProperties(); FocusSpeedN[0].min = 1; FocusSpeedN[0].max = 5; FocusSpeedN[0].value = 1; /* Step Mode */ IUFillSwitch(&StepModeS[0], "Half Step", "", ISS_OFF); IUFillSwitch(&StepModeS[1], "Full Step", "", ISS_ON); IUFillSwitchVector(&StepModeSP, StepModeS, 2, getDeviceName(), "Step Mode", "", OPTIONS_TAB, IP_RW, ISR_1OFMANY, 0, IPS_IDLE); /* Focuser temperature */ IUFillNumber(&TemperatureN[0], "TEMPERATURE", "Celsius", "%6.2f", -50, 70., 0., 0.); IUFillNumberVector(&TemperatureNP, TemperatureN, 1, getDeviceName(), "FOCUS_TEMPERATURE", "Temperature", MAIN_CONTROL_TAB, IP_RO, 0, IPS_IDLE); // Maximum Travel IUFillNumber(&MaxTravelN[0], "MAXTRAVEL", "Maximum travel", "%6.0f", 1., 60000., 0., 10000.); IUFillNumberVector(&MaxTravelNP, MaxTravelN, 1, getDeviceName(), "FOCUS_MAXTRAVEL", "Max. travel", OPTIONS_TAB, IP_RW, 0, IPS_IDLE); // Temperature Settings IUFillNumber(&TemperatureSettingN[0], "Calibration", "", "%6.2f", -20, 20, 0.5, 0); IUFillNumber(&TemperatureSettingN[1], "Coefficient", "", "%6.2f", -20, 20, 0.5, 0); IUFillNumberVector(&TemperatureSettingNP, TemperatureSettingN, 2, getDeviceName(), "Temperature Settings", "", OPTIONS_TAB, IP_RW, 0, IPS_IDLE); // Compensate for temperature IUFillSwitch(&TemperatureCompensateS[0], "Enable", "", ISS_OFF); IUFillSwitch(&TemperatureCompensateS[1], "Disable", "", ISS_ON); IUFillSwitchVector(&TemperatureCompensateSP, TemperatureCompensateS, 2, getDeviceName(), "Temperature Compensate", "", MAIN_CONTROL_TAB, IP_RW, ISR_1OFMANY, 0, IPS_IDLE); // Sync IUFillNumber(&SyncN[0], "FOCUS_SYNC_OFFSET", "Offset", "%6.0f", 0, 60000., 0., 0.); IUFillNumberVector(&SyncNP, SyncN, 1, getDeviceName(), "FOCUS_SYNC", "Sync", MAIN_CONTROL_TAB, IP_RW, 0, IPS_IDLE); /* Relative and absolute movement */ FocusRelPosN[0].min = 0.; FocusRelPosN[0].max = 30000.; FocusRelPosN[0].value = 0; FocusRelPosN[0].step = 1000; FocusAbsPosN[0].min = 0.; FocusAbsPosN[0].max = 60000.; FocusAbsPosN[0].value = 0; FocusAbsPosN[0].step = 1000; setDefaultPollingPeriod(500); addDebugControl(); return true; } bool MoonLite::updateProperties() { INDI::Focuser::updateProperties(); if (isConnected()) { defineNumber(&TemperatureNP); defineNumber(&MaxTravelNP); defineSwitch(&StepModeSP); defineNumber(&TemperatureSettingNP); defineSwitch(&TemperatureCompensateSP); defineNumber(&SyncNP); GetFocusParams(); LOG_INFO("MoonLite paramaters updated, focuser ready for use."); } else { deleteProperty(TemperatureNP.name); deleteProperty(MaxTravelNP.name); deleteProperty(StepModeSP.name); deleteProperty(TemperatureSettingNP.name); deleteProperty(TemperatureCompensateSP.name); deleteProperty(SyncNP.name); } return true; } bool MoonLite::Handshake() { if (Ack()) { LOG_INFO("MoonLite is online. Getting focus parameters..."); return true; } DEBUG(INDI::Logger::DBG_SESSION, "Error retreiving data from MoonLite, please ensure MoonLite controller is powered and the port is correct."); return false; } const char *MoonLite::getDefaultName() { return "MoonLite"; } bool MoonLite::Ack() { int nbytes_written = 0, nbytes_read = 0, rc = -1; char errstr[MAXRBUF]; char resp[5]={0}; short pos = -1; tcflush(PortFD, TCIOFLUSH); if ((rc = tty_write(PortFD, ":GP#", 4, &nbytes_written)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("updatePostion error: %s.", errstr); return false; } if ((rc = tty_read(PortFD, resp, 5, MOONLITE_TIMEOUT, &nbytes_read)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("updatePostion error: %s.", errstr); return false; } tcflush(PortFD, TCIOFLUSH); rc = sscanf(resp, "%hX#", &pos); return rc > 0; } bool MoonLite::updateStepMode() { int nbytes_written = 0, nbytes_read = 0, rc = -1; char errstr[MAXRBUF]; char resp[4]={0}; tcflush(PortFD, TCIOFLUSH); if ((rc = tty_write(PortFD, ":GH#", 4, &nbytes_written)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("updateStepMode error: %s.", errstr); return false; } if ((rc = tty_read(PortFD, resp, 3, MOONLITE_TIMEOUT, &nbytes_read)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("updateStepMode error: %s.", errstr); return false; } tcflush(PortFD, TCIOFLUSH); resp[3] = '\0'; IUResetSwitch(&StepModeSP); if (strcmp(resp, "FF#") == 0) StepModeS[0].s = ISS_ON; else if (strcmp(resp, "00#") == 0) StepModeS[1].s = ISS_ON; else { LOGF_ERROR("Unknown error: focuser step value (%s)", resp); return false; } return true; } bool MoonLite::updateTemperature() { int nbytes_written = 0, nbytes_read = 0, rc = -1; char errstr[MAXRBUF]; char resp[16]={0}; tcflush(PortFD, TCIOFLUSH); tty_write(PortFD, ":C#", 3, &nbytes_written); if ((rc = tty_write(PortFD, ":GT#", 4, &nbytes_written)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("updateTemperature error: %s.", errstr); return false; } if ((rc = tty_read_section(PortFD, resp, '#', MOONLITE_TIMEOUT, &nbytes_read)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("updateTemperature error: %s.", errstr); return false; } tcflush(PortFD, TCIOFLUSH); resp[nbytes_read-1] = '\0'; uint32_t temp = 0; rc = sscanf(resp, "%X", &temp); if (rc > 0) { // Signed hex TemperatureN[0].value = static_cast(temp) / 2.0; } else { LOGF_ERROR("Unknown error: focuser temperature value (%s)", resp); return false; } return true; } bool MoonLite::updatePosition() { int nbytes_written = 0, nbytes_read = 0, rc = -1; char errstr[MAXRBUF]; char resp[5]={0}; int pos = -1; tcflush(PortFD, TCIOFLUSH); if ((rc = tty_write(PortFD, ":GP#", 4, &nbytes_written)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("updatePostion error: %s.", errstr); return false; } if ((rc = tty_read(PortFD, resp, 5, MOONLITE_TIMEOUT, &nbytes_read)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("updatePostion error: %s.", errstr); return false; } tcflush(PortFD, TCIOFLUSH); rc = sscanf(resp, "%X#", &pos); if (rc > 0) { FocusAbsPosN[0].value = pos; } else { LOGF_ERROR("Unknown error: focuser position value (%s)", resp); return false; } return true; } bool MoonLite::updateSpeed() { int nbytes_written = 0, nbytes_read = 0, rc = -1; char errstr[MAXRBUF]; char resp[3]={0}; short speed; tcflush(PortFD, TCIOFLUSH); if ((rc = tty_write(PortFD, ":GD#", 4, &nbytes_written)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("updateSpeed error: %s.", errstr); return false; } if ((rc = tty_read(PortFD, resp, 3, MOONLITE_TIMEOUT, &nbytes_read)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("updateSpeed error: %s.", errstr); return false; } tcflush(PortFD, TCIOFLUSH); rc = sscanf(resp, "%hX#", &speed); if (rc > 0) { int focus_speed = -1; while (speed > 0) { speed >>= 1; focus_speed++; } currentSpeed = focus_speed; FocusSpeedN[0].value = focus_speed; } else { LOGF_ERROR("Unknown error: focuser speed value (%s)", resp); return false; } return true; } bool MoonLite::isMoving() { int nbytes_written = 0, nbytes_read = 0, rc = -1; char errstr[MAXRBUF]; char resp[4]={0}; tcflush(PortFD, TCIOFLUSH); if ((rc = tty_write(PortFD, ":GI#", 4, &nbytes_written)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("isMoving error: %s.", errstr); return false; } if ((rc = tty_read(PortFD, resp, 3, MOONLITE_TIMEOUT, &nbytes_read)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("isMoving error: %s.", errstr); return false; } tcflush(PortFD, TCIOFLUSH); resp[3] = '\0'; if (strcmp(resp, "01#") == 0) return true; else if (strcmp(resp, "00#") == 0) return false; LOGF_ERROR("Unknown error: isMoving value (%s)", resp); return false; } bool MoonLite::setTemperatureCalibration(double calibration) { int nbytes_written = 0, rc = -1; char errstr[MAXRBUF]; char cmd[7]={0}; int cal = calibration * 2; snprintf(cmd, 7, ":PO%02X#", cal); tcflush(PortFD, TCIOFLUSH); if ((rc = tty_write(PortFD, cmd, 6, &nbytes_written)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("setTemperatureCalibration error: %s.", errstr); return false; } return true; } bool MoonLite::setTemperatureCoefficient(double coefficient) { int nbytes_written = 0, rc = -1; char errstr[MAXRBUF]; char cmd[7]={0}; int coeff = coefficient * 2; snprintf(cmd, 7, ":SC%02X#", coeff); tcflush(PortFD, TCIOFLUSH); if ((rc = tty_write(PortFD, cmd, 6, &nbytes_written)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("setTemperatureCoefficient error: %s.", errstr); return false; } return true; } bool MoonLite::sync(uint16_t offset) { int nbytes_written = 0, rc = -1; char errstr[MAXRBUF]; char cmd[9]={0}; snprintf(cmd, 9, ":SP%04X#", offset); // Set Position if ((rc = tty_write(PortFD, cmd, 8, &nbytes_written)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("reset error: %s.", errstr); return false; } return true; } bool MoonLite::MoveFocuser(unsigned int position) { int nbytes_written = 0, rc = -1; char errstr[MAXRBUF]; char cmd[9]={0}; if (position < FocusAbsPosN[0].min || position > FocusAbsPosN[0].max) { LOGF_ERROR("Requested position value out of bound: %d", position); return false; } /*if (fabs(position - FocusAbsPosN[0].value) > MaxTravelN[0].value) { LOGF_ERROR("Requested position value of %d exceeds maximum travel limit of %g", position, MaxTravelN[0].value); return false; }*/ snprintf(cmd, 9, ":SN%04X#", position); // Set Position if ((rc = tty_write(PortFD, cmd, 8, &nbytes_written)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("setPosition error: %s.", errstr); return false; } // MoveFocuser to Position if ((rc = tty_write(PortFD, ":FG#", 4, &nbytes_written)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("MoveFocuser error: %s.", errstr); return false; } return true; } bool MoonLite::setStepMode(FocusStepMode mode) { int nbytes_written = 0, rc = -1; char errstr[MAXRBUF]; char cmd[4]={0}; tcflush(PortFD, TCIOFLUSH); if (mode == FOCUS_HALF_STEP) strncpy(cmd, ":SH#", 4); else strncpy(cmd, ":SF#", 4); if ((rc = tty_write(PortFD, cmd, 4, &nbytes_written)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("setStepMode error: %s.", errstr); return false; } return true; } bool MoonLite::setSpeed(unsigned short speed) { int nbytes_written = 0, rc = -1; char errstr[MAXRBUF]; char cmd[7]={0}; int hex_value = 1; hex_value <<= speed; snprintf(cmd, 7, ":SD%02X#", hex_value); if ((rc = tty_write(PortFD, cmd, 6, &nbytes_written)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("setSpeed error: %s.", errstr); return false; } return true; } bool MoonLite::setTemperatureCompensation(bool enable) { int nbytes_written = 0, rc = -1; char errstr[MAXRBUF]; char cmd[4]={0}; tcflush(PortFD, TCIOFLUSH); if (enable) strncpy(cmd, ":+#", 4); else strncpy(cmd, ":-#", 4); if ((rc = tty_write(PortFD, cmd, 3, &nbytes_written)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("setTemperatureCompensation error: %s.", errstr); return false; } return true; } bool MoonLite::ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) { if (dev != nullptr && strcmp(dev, getDeviceName()) == 0) { // Focus Step Mode if (strcmp(StepModeSP.name, name) == 0) { bool rc = false; int current_mode = IUFindOnSwitchIndex(&StepModeSP); IUUpdateSwitch(&StepModeSP, states, names, n); int target_mode = IUFindOnSwitchIndex(&StepModeSP); if (current_mode == target_mode) { StepModeSP.s = IPS_OK; IDSetSwitch(&StepModeSP, nullptr); } if (target_mode == 0) rc = setStepMode(FOCUS_HALF_STEP); else rc = setStepMode(FOCUS_FULL_STEP); if (!rc) { IUResetSwitch(&StepModeSP); StepModeS[current_mode].s = ISS_ON; StepModeSP.s = IPS_ALERT; IDSetSwitch(&StepModeSP, nullptr); return false; } StepModeSP.s = IPS_OK; IDSetSwitch(&StepModeSP, nullptr); return true; } if (strcmp(TemperatureCompensateSP.name, name) == 0) { int last_index = IUFindOnSwitchIndex(&TemperatureCompensateSP); IUUpdateSwitch(&TemperatureCompensateSP, states, names, n); bool rc = setTemperatureCompensation((TemperatureCompensateS[0].s == ISS_ON)); if (!rc) { TemperatureCompensateSP.s = IPS_ALERT; IUResetSwitch(&TemperatureCompensateSP); TemperatureCompensateS[last_index].s = ISS_ON; IDSetSwitch(&TemperatureCompensateSP, nullptr); return false; } TemperatureCompensateSP.s = IPS_OK; IDSetSwitch(&TemperatureCompensateSP, nullptr); return true; } } return INDI::Focuser::ISNewSwitch(dev, name, states, names, n); } bool MoonLite::ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) { if (dev != nullptr && strcmp(dev, getDeviceName()) == 0) { if (strcmp(name, SyncNP.name) == 0) { IUUpdateNumber(&SyncNP, values, names, n); if (sync(SyncN[0].value)) SyncNP.s = IPS_OK; else SyncNP.s = IPS_ALERT; IDSetNumber(&SyncNP, nullptr); return true; } if (strcmp(name, MaxTravelNP.name) == 0) { IUUpdateNumber(&MaxTravelNP, values, names, n); MaxTravelNP.s = IPS_OK; IDSetNumber(&MaxTravelNP, nullptr); return true; } if (strcmp(name, TemperatureSettingNP.name) == 0) { IUUpdateNumber(&TemperatureSettingNP, values, names, n); if (!setTemperatureCalibration(TemperatureSettingN[0].value) || !setTemperatureCoefficient(TemperatureSettingN[1].value)) { TemperatureSettingNP.s = IPS_ALERT; IDSetNumber(&TemperatureSettingNP, nullptr); return false; } TemperatureSettingNP.s = IPS_OK; IDSetNumber(&TemperatureSettingNP, nullptr); return true; } } return INDI::Focuser::ISNewNumber(dev, name, values, names, n); } void MoonLite::GetFocusParams() { if (updatePosition()) IDSetNumber(&FocusAbsPosNP, nullptr); if (updateTemperature()) IDSetNumber(&TemperatureNP, nullptr); if (updateSpeed()) IDSetNumber(&FocusSpeedNP, nullptr); if (updateStepMode()) IDSetSwitch(&StepModeSP, nullptr); } bool MoonLite::SetFocuserSpeed(int speed) { bool rc = false; rc = setSpeed(speed); if (!rc) return false; currentSpeed = speed; FocusSpeedNP.s = IPS_OK; IDSetNumber(&FocusSpeedNP, nullptr); return true; } IPState MoonLite::MoveFocuser(FocusDirection dir, int speed, uint16_t duration) { if (speed != (int)currentSpeed) { bool rc = setSpeed(speed); if (!rc) return IPS_ALERT; } gettimeofday(&focusMoveStart, nullptr); focusMoveRequest = duration / 1000.0; if (dir == FOCUS_INWARD) MoveFocuser(0); else MoveFocuser(FocusAbsPosN[0].value + MaxTravelN[0].value - 1); if (duration <= POLLMS) { usleep(duration * 1000); AbortFocuser(); return IPS_OK; } return IPS_BUSY; } IPState MoonLite::MoveAbsFocuser(uint32_t targetTicks) { targetPos = targetTicks; bool rc = false; rc = MoveFocuser(targetPos); if (!rc) return IPS_ALERT; FocusAbsPosNP.s = IPS_BUSY; return IPS_BUSY; } IPState MoonLite::MoveRelFocuser(FocusDirection dir, uint32_t ticks) { double newPosition = 0; bool rc = false; if (dir == FOCUS_INWARD) newPosition = FocusAbsPosN[0].value - ticks; else newPosition = FocusAbsPosN[0].value + ticks; rc = MoveFocuser(newPosition); if (!rc) return IPS_ALERT; FocusRelPosN[0].value = ticks; FocusRelPosNP.s = IPS_BUSY; return IPS_BUSY; } void MoonLite::TimerHit() { if (!isConnected()) { SetTimer(POLLMS); return; } bool rc = updatePosition(); if (rc) { if (fabs(lastPos - FocusAbsPosN[0].value) > 5) { IDSetNumber(&FocusAbsPosNP, nullptr); lastPos = FocusAbsPosN[0].value; } } rc = updateTemperature(); if (rc) { if (fabs(lastTemperature - TemperatureN[0].value) >= 0.5) { IDSetNumber(&TemperatureNP, nullptr); lastTemperature = TemperatureN[0].value; } } if (FocusTimerNP.s == IPS_BUSY) { float remaining = CalcTimeLeft(focusMoveStart, focusMoveRequest); if (remaining <= 0) { FocusTimerNP.s = IPS_OK; FocusTimerN[0].value = 0; AbortFocuser(); } else FocusTimerN[0].value = remaining * 1000.0; IDSetNumber(&FocusTimerNP, nullptr); } if (FocusAbsPosNP.s == IPS_BUSY || FocusRelPosNP.s == IPS_BUSY) { if (!isMoving()) { FocusAbsPosNP.s = IPS_OK; FocusRelPosNP.s = IPS_OK; IDSetNumber(&FocusAbsPosNP, nullptr); IDSetNumber(&FocusRelPosNP, nullptr); lastPos = FocusAbsPosN[0].value; LOG_INFO("Focuser reached requested position."); } } SetTimer(POLLMS); } bool MoonLite::AbortFocuser() { int nbytes_written; if (tty_write(PortFD, ":FQ#", 4, &nbytes_written) == TTY_OK) { FocusAbsPosNP.s = IPS_IDLE; FocusRelPosNP.s = IPS_IDLE; IDSetNumber(&FocusAbsPosNP, nullptr); IDSetNumber(&FocusRelPosNP, nullptr); return true; } else return false; } float MoonLite::CalcTimeLeft(timeval start, float req) { double timesince; double timeleft; struct timeval now { 0, 0 }; gettimeofday(&now, nullptr); timesince = (double)(now.tv_sec * 1000.0 + now.tv_usec / 1000) - (double)(start.tv_sec * 1000.0 + start.tv_usec / 1000); timesince = timesince / 1000; timeleft = req - timesince; return timeleft; } libindi/drivers/focuser/nstep.cpp0000664000175000017500000004261413263645557016462 0ustar jasemjasem/* NStep Focuser Copyright (c) 2016 Cloudmakers, s. r. o. All Rights Reserved. Thanks to Gene Nolan and Leon Palmer for their support. 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 */ #include "nstep.h" #include "indicom.h" #include #include #define currentSpeed SpeedN[0].value #define currentPosition FocusAbsPosN[0].value #define currentTemperature TemperatureN[0].value #define currentRelativeMovement FocusRelPosN[0].value #define currentAbsoluteMovement FocusAbsPosN[0].value std::unique_ptr nstep(new NSTEP()); void ISGetProperties(const char *dev) { nstep->ISGetProperties(dev); } void ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) { nstep->ISNewSwitch(dev, name, states, names, n); } void ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n) { nstep->ISNewText(dev, name, texts, names, n); } void ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) { nstep->ISNewNumber(dev, name, values, names, n); } void ISNewBLOB(const char *dev, const char *name, int sizes[], int blobsizes[], char *blobs[], char *formats[], char *names[], int n) { INDI_UNUSED(dev); INDI_UNUSED(name); INDI_UNUSED(sizes); INDI_UNUSED(blobsizes); INDI_UNUSED(blobs); INDI_UNUSED(formats); INDI_UNUSED(names); INDI_UNUSED(n); } void ISSnoopDevice(XMLEle *root) { nstep->ISSnoopDevice(root); } NSTEP::NSTEP() { setVersion(1, 0); FI::SetCapability(FOCUSER_CAN_ABORT | FOCUSER_CAN_ABS_MOVE | FOCUSER_CAN_REL_MOVE); } NSTEP::~NSTEP() { if (isConnected()) NSTEP::Disconnect(); } bool NSTEP::initProperties() { INDI::Focuser::initProperties(); addDebugControl(); addSimulationControl(); FocusAbsPosN[0].min = -999999; FocusAbsPosN[0].max = 999999; FocusAbsPosN[0].step = 1; FocusRelPosN[0].min = -999; FocusRelPosN[0].max = 999; FocusRelPosN[0].step = 1; FocusSpeedN[0].min = 1; FocusSpeedN[0].max = 254; FocusSpeedN[0].step = 1; FocusMotionSP.r = ISR_1OFMANY; IUFillSwitch(&TempCompS[0], "ENABLED", "Temperature compensation enabled", ISS_OFF); IUFillSwitch(&TempCompS[1], "DISABLED", "Temperature compensation disabled", ISS_ON); IUFillSwitchVector(&TempCompSP, TempCompS, 2, getDeviceName(), "COMPENSATION_MODE", "Compensation mode", MAIN_CONTROL_TAB, IP_RW, ISR_1OFMANY, 0, IPS_OK); IUFillNumber(&TempCompN[0], "TEMP_CHANGE", "Temperature change", "%.1f", -99, 99, 0.1, 0); IUFillNumber(&TempCompN[1], "TEMP_MOVE", "Compensation move", "%.0f", 0, 999, 1, 0); IUFillNumberVector(&TempCompNP, TempCompN, 2, getDeviceName(), "COMPENSATION_SETTING", "Compensation settings", MAIN_CONTROL_TAB, IP_RW, 0, IPS_OK); IUFillNumber(&TempN[0], "TEMPERATURE", "Temperature", "%.1f", 0, 999, 0, 0); IUFillNumberVector(&TempNP, TempN, 1, getDeviceName(), "TEMPERATURE", "Temperature", MAIN_CONTROL_TAB, IP_RO, 0, IPS_OK); IUFillSwitch(&SteppingPhaseS[0], "0", "0", ISS_ON); IUFillSwitch(&SteppingPhaseS[1], "1", "1", ISS_OFF); IUFillSwitch(&SteppingPhaseS[2], "2", "2", ISS_OFF); IUFillSwitchVector(&SteppingPhaseSP, SteppingPhaseS, 3, getDeviceName(), "PHASE_WIRING", "Phase wiring", MAIN_CONTROL_TAB, IP_RW, ISR_1OFMANY, 0, IPS_OK); IUFillSwitch(&SteppingModeS[0], "WAVE", "Wave", ISS_OFF); IUFillSwitch(&SteppingModeS[1], "HALF", "Half", ISS_OFF); IUFillSwitch(&SteppingModeS[2], "FULL", "Full", ISS_ON); IUFillSwitchVector(&SteppingModeSP, SteppingModeS, 3, getDeviceName(), "STEPPING_MODE", "Stepping mode", MAIN_CONTROL_TAB, IP_RW, ISR_1OFMANY, 0, IPS_OK); steppingMode = '2'; setDefaultPollingPeriod(2000); return true; } bool NSTEP::updateProperties() { if (isConnected()) { command(":CC1", nullptr, 0); if (command(":RP", buf, 7)) { sscanf(buf, "%ld", &position); FocusAbsPosN[0].value = position; defineNumber(&FocusAbsPosNP); } else { LOG_ERROR("Failed to read position"); } if (command(":RT", buf, 4)) { sscanf(buf, "%d", &temperature); if (temperature != -888) { TempN[0].value = temperature / 10.0; defineNumber(&TempNP); if (command(":RA", buf, 4)) { int value; sscanf(buf, "%d", &value); TempCompN[0].value = value / 10.0; defineNumber(&TempCompNP); } else { LOG_ERROR("Failed to read temperature change for compensation"); } if (command(":RB", buf, 3)) { int value; sscanf(buf, "%d", &value); TempCompN[1].value = value; defineNumber(&TempCompNP); } else { LOG_ERROR("Failed to read temperature step for compensation"); } if (command(":RG", buf, 1)) { char value = *buf; TempCompS[0].s = value == '2' ? ISS_ON : ISS_OFF; TempCompS[1].s = value == '0' ? ISS_ON : ISS_OFF; defineSwitch(&TempCompSP); } else { LOG_ERROR("Failed to read compensation mode"); } } else { LOG_ERROR("Temperature sensor is not connected"); } } else { LOG_ERROR("Failed to read temperature"); } if (command(":RS", buf, 3)) { int value; sscanf(buf, "%d", &value); FocusSpeedN[0].min = value; if (command(":RO", buf, 3)) { int value; sscanf(buf, "%d", &value); FocusSpeedN[0].value = value; defineNumber(&FocusSpeedNP); } else { LOG_ERROR("Failed to read step rate"); } } else { LOG_ERROR("Failed to read max step rate"); } if (command(":RW", buf, 1)) { steppingPhase = *buf; SteppingPhaseS[0].s = steppingPhase == '0' ? ISS_ON : ISS_OFF; SteppingPhaseS[1].s = steppingPhase == '1' ? ISS_ON : ISS_OFF; SteppingPhaseS[2].s = steppingPhase == '2' ? ISS_ON : ISS_OFF; defineSwitch(&SteppingPhaseSP); } else { LOG_ERROR("Failed to read stepping phase"); } defineSwitch(&SteppingModeSP); } else { deleteProperty(FocusAbsPosNP.name); deleteProperty(TempNP.name); deleteProperty(TempCompNP.name); deleteProperty(TempCompSP.name); deleteProperty(FocusSpeedNP.name); deleteProperty(SteppingModeSP.name); deleteProperty(SteppingPhaseSP.name); } return INDI::Focuser::updateProperties(); } bool NSTEP::Handshake() { if (isSimulation()) { LOG_INFO("NStep simulation is connected."); return true; } char b = 0x06; int actual = 0; int rc = tty_write(PortFD, &b, 1, &actual); if (rc == TTY_OK) { rc = tty_read(PortFD, &b, 1, 5, &actual); if (rc == TTY_OK && b == 'S') return true; } return false; } const char *NSTEP::getDefaultName() { return "Rigelsys NStep"; } bool NSTEP::command(const char *request, char *response, int count) { LOGF_DEBUG("Write [%s]", request); if (isSimulation()) { if (strcmp(request, ":RT") == 0) { strncpy(response, "+150", 5); LOGF_DEBUG("Read [%s]", response); return true; } if (strcmp(request, ":RP") == 0) { sprintf(response, "%+07ld", sim_position); LOGF_DEBUG("Read [%s]", response); return true; } if (strcmp(request, ":RS") == 0) { strncpy(response, "100", 4); LOGF_DEBUG("Read [%s]", response); return true; } if (strcmp(request, ":RO") == 0) { strncpy(response, "001", 4); LOGF_DEBUG("Read [%s]", response); return true; } if (strcmp(request, ":RA") == 0) { strncpy(response, "+010", 5); LOGF_DEBUG("Read [%s]", response); return true; } if (strcmp(request, ":RB") == 0) { strncpy(response, "005", 4); LOGF_DEBUG("Read [%s]", response); return true; } if (strcmp(request, ":RG") == 0) { strncpy(response, "2", 2); LOGF_DEBUG("Read [%s]", response); return true; } if (strcmp(request, ":RW") == 0) { strncpy(response, "0", 2); LOGF_DEBUG("Read [%s]", response); return true; } if (strcmp(request, "S") == 0) { strncpy(response, "0", 2); LOGF_DEBUG("Read [%s]", response); return true; } return true; } int actual, total; pthread_mutex_lock(&lock); int rc = TTY_OK; if (request != nullptr) rc = tty_write(PortFD, request, strlen(request), &actual); if (rc == TTY_OK && response != nullptr) { total = 0; while (rc == TTY_OK && count > 0) { rc = tty_read(PortFD, response + total, count, 5, &actual); total += actual; count -= actual; } response[total] = 0; } if (rc != TTY_OK) { char message[MAXRBUF]; tty_error_msg(rc, message, MAXRBUF); LOGF_ERROR("%s", message); pthread_mutex_unlock(&lock); return false; } LOGF_DEBUG("Read [%s]", response); pthread_mutex_unlock(&lock); return true; } bool NSTEP::ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) { if (dev != nullptr && strcmp(dev, getDeviceName()) == 0) { if (strcmp(name, TempCompSP.name) == 0) { IUUpdateSwitch(&TempCompSP, states, names, n); TempCompSP.s = IPS_OK; if (TempCompS[0].s == ISS_ON) { if (!command(":TA2", nullptr, 0)) { TempCompSP.s = IPS_ALERT; } if (!command(":TC30#", nullptr, 0)) { TempCompSP.s = IPS_ALERT; } } else { if (!command(":TA0", nullptr, 0)) { TempCompSP.s = IPS_ALERT; } } IDSetSwitch(&TempCompSP, nullptr); return true; } if (strcmp(name, SteppingPhaseSP.name) == 0) { IUUpdateSwitch(&SteppingPhaseSP, states, names, n); SteppingPhaseSP.s = IPS_OK; if (SteppingPhaseS[0].s == ISS_ON) { steppingPhase = '0'; if (!command(":CW0", nullptr, 0)) { SteppingPhaseSP.s = IPS_ALERT; } } else if (SteppingPhaseS[1].s == ISS_ON) { steppingPhase = '1'; if (!command(":CW1", nullptr, 0)) { SteppingPhaseSP.s = IPS_ALERT; } } else if (SteppingPhaseS[2].s == ISS_ON) { steppingPhase = '2'; if (!command(":CW2", nullptr, 0)) { SteppingPhaseSP.s = IPS_ALERT; } } IDSetSwitch(&SteppingPhaseSP, nullptr); return true; } if (strcmp(name, SteppingModeSP.name) == 0) { IUUpdateSwitch(&SteppingModeSP, states, names, n); SteppingModeSP.s = IPS_OK; if (SteppingModeS[0].s == ISS_ON) { steppingMode = '0'; } else if (SteppingModeS[1].s == ISS_ON) { steppingMode = '1'; } else if (SteppingModeS[2].s == ISS_ON) { steppingMode = '2'; } IDSetSwitch(&SteppingModeSP, nullptr); return true; } } return INDI::Focuser::ISNewSwitch(dev, name, states, names, n); } bool NSTEP::ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) { if (dev != nullptr && strcmp(dev, getDeviceName()) == 0) { if (strcmp(name, TempCompNP.name) == 0) { IUUpdateNumber(&TempCompNP, values, names, n); PresetNP.s = IPS_OK; sprintf(buf, ":TT%+04d#", (int)(TempCompN[0].value * 10)); if (!command(buf, nullptr, 0)) { PresetNP.s = IPS_ALERT; } sprintf(buf, ":TS%03d#", (int)(TempCompN[1].value)); if (!command(buf, nullptr, 0)) { PresetNP.s = IPS_ALERT; } IDSetNumber(&TempCompNP, nullptr); return true; } } return INDI::Focuser::ISNewNumber(dev, name, values, names, n); } IPState NSTEP::moveFocuserRelative(FocusDirection dir, unsigned int ticks) { unsigned int originalTicks = ticks; unsigned int subTicks; if (isSimulation()) { if (dir == FOCUS_INWARD) sim_position -= ticks; else sim_position += ticks; return IPS_BUSY; } do { if (ticks > 999) { subTicks = 999; ticks -= 999; } else { subTicks = ticks; ticks = 0; } sprintf(buf, ":F%c%c%03d#", dir == FOCUS_INWARD ? '0' : '1', steppingMode, subTicks); LOGF_DEBUG("About to send command %s", buf); if (!command(buf, nullptr, 0)) { FocusAbsPosNP.s = IPS_ALERT; IDSetNumber(&FocusAbsPosNP, nullptr); return IPS_ALERT; } do { if (!command("S", buf, 1)) { FocusAbsPosNP.s = IPS_ALERT; IDSetNumber(&FocusAbsPosNP, nullptr); return IPS_ALERT; } } while (*buf != '0'); } while (ticks > 0); FocusAbsPosNP.s = IPS_BUSY; if (dir == FOCUS_INWARD) currentPosition -= originalTicks; else currentPosition += originalTicks; IDSetNumber(&FocusAbsPosNP, nullptr); return IPS_BUSY; } IPState NSTEP::MoveAbsFocuser(uint32_t targetTicks) { LOGF_INFO("Focuser is moving to requested position %d", targetTicks); unsigned int newAbsPos = 0; IPState retCode = IPS_ALERT; if ((targetTicks - currentPosition) >= 0) { newAbsPos = targetTicks - currentPosition; retCode = moveFocuserRelative(FOCUS_OUTWARD, newAbsPos); } else if ((targetTicks - currentPosition) < 0) { newAbsPos = currentPosition - targetTicks; retCode = moveFocuserRelative(FOCUS_INWARD, newAbsPos); } return retCode; } IPState NSTEP::MoveRelFocuser(FocusDirection dir, unsigned int ticks) { return moveFocuserRelative(dir, ticks); } bool NSTEP::AbortFocuser() { sprintf(buf, ":F1%c000#", steppingMode); return command(buf, nullptr, 0); } bool NSTEP::SetFocuserSpeed(int speed) { sprintf(buf, ":CS%03d#", speed); return command(buf, buf, 2); } void NSTEP::TimerHit() { if (isConnected()) { if (command(":RT", buf, 4)) { int tmp; sscanf(buf, "%d", &tmp); if (tmp != temperature) { temperature = tmp; TempN[0].value = temperature / 10.0; IDSetNumber(&TempNP, nullptr); } } else { LOG_ERROR("Failed to read temperature"); } if (command("S", buf, 1)) { if (*buf == '0' && FocusAbsPosNP.s == IPS_BUSY) { FocusAbsPosNP.s = IPS_OK; IDSetNumber(&FocusAbsPosNP, nullptr); FocusRelPosNP.s = IPS_OK; IDSetNumber(&FocusRelPosNP, nullptr); } } SetTimer(POLLMS); } } bool NSTEP::saveConfigItems(FILE *fp) { IUSaveConfigSwitch(fp, &TempCompSP); IUSaveConfigSwitch(fp, &SteppingModeSP); IUSaveConfigNumber(fp, &TempCompNP); return INDI::Focuser::saveConfigItems(fp); } libindi/drivers/focuser/dmfc.h0000664000175000017500000000607613263645557015711 0ustar jasemjasem/* Pegasus DMFC Focuser Copyright (C) 2017 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 */ #pragma once #include "indifocuser.h" class DMFC : public INDI::Focuser { public: DMFC(); virtual ~DMFC() = default; virtual bool Handshake(); const char *getDefaultName(); virtual bool initProperties(); virtual bool updateProperties(); virtual bool ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n); virtual bool ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n); protected: virtual IPState MoveAbsFocuser(uint32_t targetTicks); virtual IPState MoveRelFocuser(FocusDirection dir, uint32_t ticks); virtual bool AbortFocuser(); virtual void TimerHit(); virtual bool saveConfigItems(FILE *fp); private: bool updateFocusParams(); bool sync(uint32_t newPosition); bool move(uint32_t newPosition); bool setMaxSpeed(uint16_t speed); bool setReverseEnabled(bool enable); bool setLedEnabled(bool enable); bool setEncodersEnabled(bool enable); bool setBacklash(uint16_t value); bool setMotorType(uint8_t type); bool ack(); uint32_t currentPosition { 0 }; uint32_t targetPosition { 0 }; bool isMoving = false; // Temperature probe INumber TemperatureN[1]; INumberVectorProperty TemperatureNP; // Sync Position INumber SyncN[1]; INumberVectorProperty SyncNP; // Motor Mode ISwitch MotorTypeS[2]; ISwitchVectorProperty MotorTypeSP; enum { MOTOR_DC, MOTOR_STEPPER }; // Rotator Encoders ISwitch EncoderS[2]; ISwitchVectorProperty EncoderSP; enum { ENCODERS_ON, ENCODERS_OFF }; // Enable/Disable backlash ISwitch BacklashCompensationS[2]; ISwitchVectorProperty BacklashCompensationSP; enum { BACKLASH_ENABLED, BACKLASH_DISABLED }; // Backlash Value INumber BacklashN[1]; INumberVectorProperty BacklashNP; // Reverse Direction ISwitch ReverseS[2]; ISwitchVectorProperty ReverseSP; enum { DIRECTION_NORMAL, DIRECTION_REVERSED }; // LED ISwitch LEDS[2]; ISwitchVectorProperty LEDSP; enum { LED_OFF, LED_ON }; // Maximum Speed INumber MaxSpeedN[1]; INumberVectorProperty MaxSpeedNP; // Firmware Version IText FirmwareVersionT[1]; ITextVectorProperty FirmwareVersionTP; }; libindi/drivers/focuser/sestosenso.h0000664000175000017500000000373413263645557017203 0ustar jasemjasem/* SestoSenso Focuser Copyright (C) 2013 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 */ #pragma once #include "indifocuser.h" class SestoSenso : public INDI::Focuser { public: SestoSenso(); virtual ~SestoSenso() = default; typedef enum { FOCUS_HALF_STEP, FOCUS_FULL_STEP } FocusStepMode; const char *getDefaultName(); virtual bool initProperties(); virtual bool updateProperties(); virtual bool ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n); protected: virtual bool Handshake(); virtual IPState MoveRelFocuser(FocusDirection dir, uint32_t ticks); virtual IPState MoveAbsFocuser(uint32_t targetTicks); virtual bool AbortFocuser(); virtual void TimerHit(); private: bool Ack(); void GetFocusParams(); bool isCommandOK(const char *cmd); bool isMotionComplete(); bool sync(uint32_t newPosition); bool updateTemperature(); bool updatePosition(); uint32_t targetPos { 0 }; uint32_t lastPos { 0 }; double lastTemperature { 0 }; INumber TemperatureN[1]; INumberVectorProperty TemperatureNP; IText FirmwareT[1] {}; ITextVectorProperty FirmwareTP; INumber SyncN[1]; INumberVectorProperty SyncNP; }; libindi/drivers/focuser/focus_simulator.cpp0000664000175000017500000002641613263645557020551 0ustar jasemjasem/******************************************************************************* Copyright(c) 2012 Jasem Mutlaq. All rights reserved. This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2 as published by the Free Software Foundation. . 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 Library General Public License for more details. . You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. *******************************************************************************/ #include "focus_simulator.h" #include #include #include #include // We declare an auto pointer to focusSim. std::unique_ptr focusSim(new FocusSim()); // Focuser takes 100 microsecond to move for each step, completing 100,000 steps in 10 seconds #define FOCUS_MOTION_DELAY 100 void ISPoll(void *p); void ISGetProperties(const char *dev) { focusSim->ISGetProperties(dev); } void ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) { focusSim->ISNewSwitch(dev, name, states, names, n); } void ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n) { focusSim->ISNewText(dev, name, texts, names, n); } void ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) { focusSim->ISNewNumber(dev, name, values, names, n); } void ISNewBLOB(const char *dev, const char *name, int sizes[], int blobsizes[], char *blobs[], char *formats[], char *names[], int n) { INDI_UNUSED(dev); INDI_UNUSED(name); INDI_UNUSED(sizes); INDI_UNUSED(blobsizes); INDI_UNUSED(blobs); INDI_UNUSED(formats); INDI_UNUSED(names); INDI_UNUSED(n); } void ISSnoopDevice(XMLEle *root) { focusSim->ISSnoopDevice(root); } /************************************************************************************ * ************************************************************************************/ FocusSim::FocusSim() { FI::SetCapability(FOCUSER_CAN_ABS_MOVE | FOCUSER_CAN_REL_MOVE | FOCUSER_HAS_VARIABLE_SPEED); } /************************************************************************************ * ************************************************************************************/ bool FocusSim::Connect() { SetTimer(1000); return true; } /************************************************************************************ * ************************************************************************************/ bool FocusSim::Disconnect() { return true; } /************************************************************************************ * ************************************************************************************/ const char *FocusSim::getDefaultName() { return (const char *)"Focuser Simulator"; } /************************************************************************************ * ************************************************************************************/ void FocusSim::ISGetProperties(const char *dev) { if (dev != nullptr && strcmp(dev, getDeviceName()) != 0) return; INDI::Focuser::ISGetProperties(dev); defineSwitch(&ModeSP); loadConfig(true, "Mode"); } /************************************************************************************ * ************************************************************************************/ bool FocusSim::initProperties() { INDI::Focuser::initProperties(); IUFillNumber(&SeeingN[0], "SIM_SEEING", "arcseconds", "%4.2f", 0, 60, 0, 3.5); IUFillNumberVector(&SeeingNP, SeeingN, 1, getDeviceName(), "SEEING_SETTINGS", "Seeing", MAIN_CONTROL_TAB, IP_RW, 60, IPS_IDLE); IUFillNumber(&FWHMN[0], "SIM_FWHM", "arcseconds", "%4.2f", 0, 60, 0, 7.5); IUFillNumberVector(&FWHMNP, FWHMN, 1, getDeviceName(), "FWHM", "FWHM", MAIN_CONTROL_TAB, IP_RO, 60, IPS_IDLE); IUFillSwitch(&ModeS[MODE_ALL], "All", "All", ISS_ON); IUFillSwitch(&ModeS[MODE_ABSOLUTE], "Absolute", "Absolute", ISS_OFF); IUFillSwitch(&ModeS[MODE_RELATIVE], "Relative", "Relative", ISS_OFF); IUFillSwitch(&ModeS[MODE_TIMER], "Timer", "Timer", ISS_OFF); IUFillSwitchVector(&ModeSP, ModeS, MODE_COUNT, getDeviceName(), "Mode", "Mode", MAIN_CONTROL_TAB, IP_RW, ISR_1OFMANY, 60, IPS_IDLE); initTicks = sqrt(FWHMN[0].value - SeeingN[0].value) / 0.75; FocusSpeedN[0].min = 1; FocusSpeedN[0].max = 5; FocusSpeedN[0].step = 1; FocusSpeedN[0].value = 1; FocusAbsPosN[0].value = FocusAbsPosN[0].max / 2; internalTicks = FocusAbsPosN[0].value; return true; } /************************************************************************************ * ************************************************************************************/ bool FocusSim::updateProperties() { INDI::Focuser::updateProperties(); if (isConnected()) { defineNumber(&SeeingNP); defineNumber(&FWHMNP); } else { deleteProperty(SeeingNP.name); deleteProperty(FWHMNP.name); } return true; } /************************************************************************************ * ************************************************************************************/ bool FocusSim::ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) { if (dev != nullptr && strcmp(dev, getDeviceName()) == 0) { // Modes if (strcmp(ModeSP.name, name) == 0) { IUUpdateSwitch(&ModeSP, states, names, n); uint32_t cap = 0; int index = IUFindOnSwitchIndex(&ModeSP); switch (index) { case MODE_ALL: cap = FOCUSER_CAN_ABS_MOVE | FOCUSER_CAN_REL_MOVE | FOCUSER_HAS_VARIABLE_SPEED; break; case MODE_ABSOLUTE: cap = FOCUSER_CAN_ABS_MOVE; break; case MODE_RELATIVE: cap = FOCUSER_CAN_REL_MOVE; break; case MODE_TIMER: cap = FOCUSER_HAS_VARIABLE_SPEED; break; default: ModeSP.s = IPS_ALERT; IDSetSwitch(&ModeSP, "Unknown mode index %d", index); return true; } FI::SetCapability(cap); ModeSP.s = IPS_OK; IDSetSwitch(&ModeSP, nullptr); return true; } } return INDI::Focuser::ISNewSwitch(dev, name, states, names, n); } /************************************************************************************ * ************************************************************************************/ bool FocusSim::ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) { if (dev != nullptr && strcmp(dev, getDeviceName()) == 0) { if (strcmp(name, "SEEING_SETTINGS") == 0) { SeeingNP.s = IPS_OK; IUUpdateNumber(&SeeingNP, values, names, n); IDSetNumber(&SeeingNP, nullptr); return true; } } // Let INDI::Focuser handle any other number properties return INDI::Focuser::ISNewNumber(dev, name, values, names, n); } /************************************************************************************ * ************************************************************************************/ IPState FocusSim::MoveFocuser(FocusDirection dir, int speed, uint16_t duration) { double mid = (FocusAbsPosN[0].max - FocusAbsPosN[0].min) / 2; int mode = IUFindOnSwitchIndex(&ModeSP); double targetTicks = ((dir == FOCUS_INWARD) ? -1 : 1) * (speed * duration); internalTicks += targetTicks; if (mode == MODE_ALL) { if (internalTicks < FocusAbsPosN[0].min || internalTicks > FocusAbsPosN[0].max) { internalTicks -= targetTicks; LOG_ERROR("Cannot move focuser in this direction any further."); return IPS_ALERT; } } // simulate delay in motion as the focuser moves to the new position usleep(duration * 1000); double ticks = initTicks + (internalTicks - mid) / 5000.0; FWHMN[0].value = 0.5625 * ticks * ticks + SeeingN[0].value; LOGF_DEBUG("TIMER Current internal ticks: %g FWHM ticks: %g FWHM: %g", internalTicks, ticks, FWHMN[0].value); if (mode == MODE_ALL) { FocusAbsPosN[0].value = internalTicks; IDSetNumber(&FocusAbsPosNP, nullptr); } if (FWHMN[0].value < SeeingN[0].value) FWHMN[0].value = SeeingN[0].value; IDSetNumber(&FWHMNP, nullptr); return IPS_OK; } /************************************************************************************ * ************************************************************************************/ IPState FocusSim::MoveAbsFocuser(uint32_t targetTicks) { double mid = (FocusAbsPosN[0].max - FocusAbsPosN[0].min) / 2; internalTicks = targetTicks; // Limit to +/- 10 from initTicks double ticks = initTicks + (targetTicks - mid) / 5000.0; // simulate delay in motion as the focuser moves to the new position usleep(std::abs((int)(targetTicks - FocusAbsPosN[0].value) * FOCUS_MOTION_DELAY)); FocusAbsPosN[0].value = targetTicks; FWHMN[0].value = 0.5625 * ticks * ticks + SeeingN[0].value; LOGF_DEBUG("ABS Current internal ticks: %g FWHM ticks: %g FWHM: %g", internalTicks, ticks, FWHMN[0].value); if (FWHMN[0].value < SeeingN[0].value) FWHMN[0].value = SeeingN[0].value; IDSetNumber(&FWHMNP, nullptr); return IPS_OK; } /************************************************************************************ * ************************************************************************************/ IPState FocusSim::MoveRelFocuser(FocusDirection dir, uint32_t ticks) { double mid = (FocusAbsPosN[0].max - FocusAbsPosN[0].min) / 2; int mode = IUFindOnSwitchIndex(&ModeSP); if (mode == MODE_ALL || mode == MODE_ABSOLUTE) { uint32_t targetTicks = FocusAbsPosN[0].value + (ticks * (dir == FOCUS_INWARD ? -1 : 1)); FocusAbsPosNP.s = IPS_BUSY; IDSetNumber(&FocusAbsPosNP, nullptr); return MoveAbsFocuser(targetTicks); } internalTicks += (dir == FOCUS_INWARD ? -1 : 1) * static_cast(ticks); ticks = initTicks + (internalTicks - mid) / 5000.0; LOGF_DEBUG("REL Current internal ticks: %g FWHM ticks: %g FWHM: %g", internalTicks, ticks, FWHMN[0].value); FWHMN[0].value = 0.5625 * ticks * ticks + SeeingN[0].value; if (FWHMN[0].value < SeeingN[0].value) FWHMN[0].value = SeeingN[0].value; IDSetNumber(&FWHMNP, nullptr); return IPS_OK; } /************************************************************************************ * ************************************************************************************/ bool FocusSim::SetFocuserSpeed(int speed) { INDI_UNUSED(speed); return true; } libindi/drivers/focuser/smartfocus.h0000664000175000017500000000560313263645557017161 0ustar jasemjasem/******************************************************************************* Copyright(c) 2015 Camiel Severijns. All rights reserved. This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2 as published by the Free Software Foundation. . 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 Library General Public License for more details. . You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. *******************************************************************************/ #pragma once #include "indifocuser.h" class SmartFocus : public INDI::Focuser { public: SmartFocus(); const char *getDefaultName() override; bool initProperties() override; bool updateProperties() override; virtual bool Handshake() override; virtual bool ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) override; //virtual bool ISNewSwitch (const char *dev, const char *name, ISState *states, char *names[], int n) override; virtual bool AbortFocuser() override; virtual IPState MoveAbsFocuser(uint32_t targetPosition) override; virtual IPState MoveRelFocuser(FocusDirection dir, uint32_t ticks) override; virtual void TimerHit() override; typedef unsigned short Position; protected: virtual bool saveConfigItems(FILE *fp) override; private: typedef unsigned char Flags; enum State { Idle, MovingTo, MovingIn, MovingOut }; enum StatusFlags { STATUS_SERIAL_FRAMING_ERROR = 0, STATUS_SERIAL_OVERRUN_ERROR, STATUS_MOTOR_ENCODE_ERROR, STATUS_AT_ZERO_POSITION, STATUS_AT_MAX_POSITION, STATUS_NUM_FLAGS }; enum FlagBits { SerFramingError = 0x02, SerOverrunError = 0x04, MotorEncoderError = 0x08, AtZeroPosition = 0x40, AtMaxPosition = 0x80 }; bool SFacknowledge(); bool SFisIdle() const { return (state == Idle); } bool SFisMoving() const { return !SFisIdle(); } Position SFgetPosition(); Flags SFgetFlags(); void SFgetState(); bool send(const char *command, const size_t nbytes, const char *from, const bool log_error = true); bool recv(char *respons, const size_t nbytes, const char *from, const bool log_error = true); Position position { 0 }; State state { Idle }; int timer_id { 0 }; ILight FlagsL[5]; ILightVectorProperty FlagsLP; INumber MaxPositionN[1]; INumberVectorProperty MaxPositionNP; }; libindi/drivers/focuser/lakeside.cpp0000664000175000017500000017260113263645557017112 0ustar jasemjasem/* Lakeside Focuser Copyright (C) 2017 Phil Shepherd (psjshep@googlemail.com) Technical Information kindly supplied by Peter Chance at LakesideAstro (info@lakeside-astro.com) Code template from original Moonlite code by 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 */ /* Modifications 0.1 psjshep xx-xxx-xxxx - 1st version .. .. 0.11 psjshep 17-Mar-2017 - changed PortT[0].text to serialConnection->port() */ #define LAKESIDE_VERSION_MAJOR 1 #define LAKESIDE_VERSION_MINOR 0 #include "lakeside.h" #include #include "indicom.h" #include "connectionplugins/connectionserial.h" #include #include #include #include #include #include #include // tty_read_section timeout in seconds #define LAKESIDE_TIMEOUT 2 #define LAKESIDE_LEN 7 // Max number of Timeouts for a tty_read_section // This is in case a buffer read is too fast // or nothing in the buffer during GetLakesideStatus() #define LAKESIDE_TIMEOUT_RETRIES 2 std::unique_ptr lakeside(new Lakeside()); void ISGetProperties(const char *dev) { lakeside->ISGetProperties(dev); } void ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int num) { lakeside->ISNewSwitch(dev, name, states, names, num); } void ISNewText( const char *dev, const char *name, char *texts[], char *names[], int num) { lakeside->ISNewText(dev, name, texts, names, num); } void ISNewNumber(const char *dev, const char *name, double values[], char *names[], int num) { lakeside->ISNewNumber(dev, name, values, names, num); } void ISNewBLOB (const char *dev, const char *name, int sizes[], int blobsizes[], char *blobs[], char *formats[], char *names[], int n) { INDI_UNUSED(dev); INDI_UNUSED(name); INDI_UNUSED(sizes); INDI_UNUSED(blobsizes); INDI_UNUSED(blobs); INDI_UNUSED(formats); INDI_UNUSED(names); INDI_UNUSED(n); } void ISSnoopDevice (XMLEle *root) { lakeside->ISSnoopDevice(root); } Lakeside::Lakeside() { setVersion(LAKESIDE_VERSION_MAJOR, LAKESIDE_VERSION_MINOR); FI::SetCapability(FOCUSER_CAN_ABS_MOVE | FOCUSER_CAN_ABORT ); lastPos = 0; lastTemperature = 0; } // Initialise bool Lakeside::initProperties() { INDI::Focuser::initProperties(); // Current Direction IUFillSwitch(&MoveDirectionS[0], "Normal", "", ISS_ON); IUFillSwitch(&MoveDirectionS[1], "Reverse", "", ISS_OFF); IUFillSwitchVector(&MoveDirectionSP, MoveDirectionS, 2, getDeviceName(), "","Move Direction", MAIN_CONTROL_TAB, IP_RW, ISR_1OFMANY, 0, IPS_IDLE); // Focuser temperature (degrees C) - read only IUFillNumber(&TemperatureN[0], "TEMPERATURE", "Celsius", "%3.2f", -50, 70., 0., 0.); IUFillNumberVector(&TemperatureNP, TemperatureN, 1, getDeviceName(), "FOCUS_TEMPERATURE", "Temperature (C)", MAIN_CONTROL_TAB, IP_RO, 0, IPS_IDLE); // Focuser temperature (Kelvin)- read only & only read once at connect IUFillNumber(&TemperatureKN[0], "TEMPERATUREK", "Kelvin", "%3.2f", 0., 373.15, 0., 0.); IUFillNumberVector(&TemperatureKNP, TemperatureKN, 1, getDeviceName(), "FOCUS_TEMPERATUREK", "Temperature (K)", MAIN_CONTROL_TAB, IP_RO, 0, IPS_IDLE); // Compensate for temperature IUFillSwitch(&TemperatureTrackingS[0], "Enable", "", ISS_OFF); IUFillSwitch(&TemperatureTrackingS[1], "Disable", "", ISS_ON); IUFillSwitchVector(&TemperatureTrackingSP, TemperatureTrackingS, 2, getDeviceName(), "Temperature Track", "", MAIN_CONTROL_TAB, IP_RW, ISR_1OFMANY, 0, IPS_IDLE); // Backlash 0-255 IUFillNumber(&BacklashN[0], "BACKLASH", "(0-255)", "%.f", 0, 255, 0, 0); IUFillNumberVector(&BacklashNP, BacklashN, 1, getDeviceName(), "BACKLASH", "Backlash", OPTIONS_TAB, IP_RW, 0, IPS_IDLE ); // Maximum Travel - read only IUFillNumber(&MaxTravelN[0], "MAXTRAVEL", "No. Steps", "%.f", 1, 65536, 0, 10000); IUFillNumberVector(&MaxTravelNP, MaxTravelN, 1, getDeviceName(), "MAXTRAVEL", "Max travel(Via Ctrlr)", OPTIONS_TAB, IP_RO, 0, IPS_IDLE ); // Step Size - read only IUFillNumber(&StepSizeN[0], "STEPSIZE", "No. Steps", "%.f", 1, 65536, 0, 1); IUFillNumberVector(&StepSizeNP, StepSizeN, 1, getDeviceName(), "STEPSIZE", "Step Size(Via Ctrlr)", OPTIONS_TAB, IP_RO, 0, IPS_IDLE); // Active Temperature Slope - select 1 or 2 IUFillSwitch(&ActiveTemperatureSlopeS[0], "Slope 1", "", ISS_ON); IUFillSwitch(&ActiveTemperatureSlopeS[1], "Slope 2", "", ISS_OFF); IUFillSwitchVector(&ActiveTemperatureSlopeSP, ActiveTemperatureSlopeS, 2, getDeviceName(), "Active Slope", "", OPTIONS_TAB, IP_RW, ISR_1OFMANY, 0, IPS_IDLE); // Slope 1 : Directions IUFillSwitch(&Slope1DirS[0], "0", "", ISS_ON); IUFillSwitch(&Slope1DirS[1], "1", "", ISS_OFF); IUFillSwitchVector(&Slope1DirSP, Slope1DirS, 2, getDeviceName(), "Slope 1 Direction", "", OPTIONS_TAB, IP_RW, ISR_1OFMANY, 0, IPS_IDLE); // Slope 1 : Slope Increments (counts per degree, 0.1 step increments IUFillNumber(&Slope1IncN[0], "SLOPE1INC", "No. Steps (0-655356", "%.f", 0, 65536, 0, 0); IUFillNumberVector(&Slope1IncNP, Slope1IncN, 1, getDeviceName(), "SLOPE1INC","Slope1 Increments", OPTIONS_TAB, IP_RW, 0, IPS_IDLE ); // slope 1 : Deadband - value between 0 and 255 IUFillNumber(&Slope1DeadbandN[0], "SLOPE1DEADBAND", "(0-255)", "%.f", 0, 255, 0, 0); IUFillNumberVector(&Slope1DeadbandNP, Slope1DeadbandN, 1, getDeviceName(), "SLOPE1DEADBAND", "Slope 1 Deadband", OPTIONS_TAB, IP_RW, 0, IPS_IDLE ); // Slope 1 : Time Period (Minutes, 0.1 step increments IUFillNumber(&Slope1PeriodN[0], "SLOPE1PERIOD", "Minutes (0-99)", "%.f", 0, 99, 0, 0); IUFillNumberVector(&Slope1PeriodNP, Slope1PeriodN, 1, getDeviceName(), "SLOPE1PERIOD", "Slope 1 Period", OPTIONS_TAB, IP_RW, 0, IPS_IDLE ); // Slope 2 : Direction IUFillSwitch(&Slope2DirS[0], "0", "", ISS_ON); IUFillSwitch(&Slope2DirS[1], "1", "", ISS_OFF); IUFillSwitchVector(&Slope2DirSP, Slope2DirS, 2, getDeviceName(), "Slope 2 Direction", "", OPTIONS_TAB, IP_RW, ISR_1OFMANY, 0, IPS_IDLE); // slope 2 : Slope Increments (counts per degree, 0.1 step increments IUFillNumber(&Slope2IncN[0], "SLOPE2INC", "No. Steps (0-65536)", "%.f", 0, 65536, 0, 0); IUFillNumberVector(&Slope2IncNP, Slope2IncN, 1, getDeviceName(), "SLOPE2INC", "Slope 2 Increments", OPTIONS_TAB, IP_RW, 0, IPS_IDLE ); // slope 2 : Deadband - value between 0 and 255 IUFillNumber(&Slope2DeadbandN[0], "SLOPE2DEADBAND", "Steps (0-255)", "%.f", 0, 255, 0, 0); IUFillNumberVector(&Slope2DeadbandNP, Slope2DeadbandN, 1, getDeviceName(), "SLOPE2DEADBAND", "Slope 2 Deadband", OPTIONS_TAB, IP_RW, 0, IPS_IDLE ); // slope 2 : Time Period (Minutes, 0.1 step increments IUFillNumber(&Slope2PeriodN[0], "SLOPE2PERIOD", "Minutes (0-99)", "%.f", 0, 99, 0, 0); IUFillNumberVector(&Slope2PeriodNP, Slope2PeriodN, 1, getDeviceName(), "SLOPE2PERIOD", "Slope 2 Period", OPTIONS_TAB, IP_RW, 0, IPS_IDLE ); FocusAbsPosN[0].min = 0.; // shephpj - not used //FocusAbsPosN[0].max = 65536.; setDefaultPollingPeriod(1000); addDebugControl(); return true; } bool Lakeside::updateProperties() { INDI::Focuser::updateProperties(); if (isConnected()) { defineNumber(&BacklashNP); defineNumber(&MaxTravelNP); defineNumber(&StepSizeNP); defineNumber(&TemperatureNP); defineNumber(&TemperatureKNP); defineSwitch(&MoveDirectionSP); defineSwitch(&TemperatureTrackingSP); defineSwitch(&ActiveTemperatureSlopeSP); defineSwitch(&Slope1DirSP); defineNumber(&Slope1IncNP); defineNumber(&Slope1DeadbandNP); defineNumber(&Slope1PeriodNP); defineSwitch(&Slope2DirSP); defineNumber(&Slope2IncNP); defineNumber(&Slope2DeadbandNP); defineNumber(&Slope2PeriodNP); GetFocusParams(); LOG_INFO("Lakeside paramaters updated, focuser ready for use."); } else { deleteProperty(BacklashNP.name); deleteProperty(MaxTravelNP.name); deleteProperty(StepSizeNP.name); deleteProperty(MoveDirectionSP.name); deleteProperty(TemperatureNP.name); deleteProperty(TemperatureKNP.name); deleteProperty(TemperatureTrackingSP.name); deleteProperty(ActiveTemperatureSlopeSP.name); deleteProperty(Slope1DirSP.name); deleteProperty(Slope1IncNP.name); deleteProperty(Slope1DeadbandNP.name); deleteProperty(Slope1PeriodNP.name); deleteProperty(Slope2DirSP.name); deleteProperty(Slope2IncNP.name); deleteProperty(Slope2DeadbandNP.name); deleteProperty(Slope2PeriodNP.name); } return true; } #if 0 // connect to focuser port // // 9600 baud // 8 bits // 0 parity // 1 stop bit // bool Lakeside::Connect() { int rc=0; char errorMsg[MAXRBUF]; // if ( (rc = tty_connect(PortT[0].text, 9600, 8, 0, 1, &PortFD)) != TTY_OK) if ( (rc = tty_connect(serialConnection->port(), 9600, 8, 0, 1, &PortFD)) != TTY_OK) { tty_error_msg(rc, errorMsg, MAXRBUF); LOGF_INFO("Failed to connect to port %s, with Error %s", serialConnection->port(), errorMsg); return false; } LOGF_INFO("Connected to port %s",serialConnection->port()); if (LakesideOnline()) { LOGF_INFO("Lakeside is online on port %s",serialConnection->port()); SetTimer(POLLMS); return true; } else { LOGF_INFO("Unable to connect to Lakeside Focuser. Please ensure the controller is powered on and the port (%s) is correct.",serialConnection->port()); return false; } } // Disconnect from focuser bool Lakeside::Disconnect() { LOG_INFO("Lakeside is offline."); return INDI::Focuser::Disconnect(); } #endif bool Lakeside::Handshake() { return LakesideOnline(); } const char * Lakeside::getDefaultName() { return "Lakeside"; } // // Send Lakeside a command // // In : // in_cmd : command to send to the focuser // // Returns true for successful write // false for failed write // bool Lakeside::SendCmd(const char* in_cmd) { int nbytes_written=0, rc=-1; char errstr[MAXRBUF]; if ( (rc = tty_write_string(PortFD, in_cmd, &nbytes_written)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("SendCmd: Write for command (%s) failed - %s", in_cmd,errstr); return false; } else { LOGF_DEBUG("SendCmd: Successfully sent (%s)", in_cmd); } return true; } // // Read the Lakeside buffer, setting response to the contents // // Returns // true : something to read in the buffer // false : error reading the buffer // bool Lakeside::ReadBuffer(char* response) { int nbytes_read=0, rc=-1; char errstr[MAXRBUF]; char resp[LAKESIDE_LEN] = {0}; //strcpy(resp," "); // read until 0x23 (#) received if ( (rc = tty_read_section(PortFD, resp, 0x23, LAKESIDE_TIMEOUT, &nbytes_read)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("ReadBuffer: Read failed - %s",errstr); strncpy(response,"ERROR", LAKESIDE_LEN); return false; } else { LOGF_DEBUG("ReadBuffer: Received (%s)", resp); } strncpy(response,resp, LAKESIDE_LEN); return true; } // // check for OK# from Lakeside - i.e. it is responding // bool Lakeside::LakesideOnline() { char resp[LAKESIDE_LEN] = {0}; const char *cmd="??#"; //strcpy(resp," "); if (!SendCmd(cmd)) { return false; } LOGF_DEBUG("LakesideOnline: Successfully sent (%s)", cmd); if (!ReadBuffer(resp)) { return false; } // if SendCmd succeeded, resp contains response from the command LOGF_DEBUG("LakesideOnline: Received (%s)", resp); if (!strncmp(resp,"OK#",3)) { LOG_DEBUG("LakesideOnline: Received OK# - Lakeside responded"); return true; } else { LOGF_ERROR("LakesideOnline: OK# not found. Instead, received (%s)",resp); return false; } } // get current movement direction // // 0 = Normal // 1 = Reversed bool Lakeside::updateMoveDirection() { int rc=-1, temp=-1; char resp[LAKESIDE_LEN] = {0}; char cmd[]="?D#"; if (!SendCmd(cmd)) { return false; } if (!ReadBuffer(resp)) { return false; } IUResetSwitch(&MoveDirectionSP); // direction is in form Dnnnnn# // where nnnnn is 0 for normal or 1 for reversed rc = sscanf(resp, "D%5d#", &temp); if ( temp == 0) { MoveDirectionS[0].s = ISS_ON; LOGF_DEBUG("updateMoveDirection: Move Direction is (%d)", temp); } else if ( temp == 1) { MoveDirectionS[1].s = ISS_ON; LOGF_DEBUG("updateMoveDirection: Move Direction is (%d)", temp); } else { LOGF_ERROR("updateMoveDirection: Unknown move Direction response (%s)", resp); return false; } return true; } // Decode contents of buffer // Returns: // P : Position update found - FocusAbsPosN[0].value updated // T : Temperature update found - TemperatureN[0].value // K : Temperature in Kelvin update found - TemperatureKN[0].value // D : DONE# received // O : OK# received // E : Error due to unknown/misformed command having been sent // ? : unknown response received char Lakeside::DecodeBuffer(char* in_response) { int temp=0, pos=0, rc=-1; LOGF_DEBUG("DecodeBuffer: in_response (%s)", in_response); // if focuser finished moving, DONE# received if (!strncmp(in_response,"DONE#",5)) { return 'D'; } // if focuser returned OK# if (!strncmp(in_response,"OK#",3)) { return 'O'; } // if focuser returns an error for unknow command if (!strncmp(in_response,"!#",2)) { return 'E'; } // Temperature update is Tnnnnnn# where nnnnn is left space padded rc = sscanf(in_response, "T%5d#", &temp); if (rc > 0) { // need to divide result by 2 TemperatureN[0].value = ((int) temp)/2.0; LOGF_DEBUG("DecodeBuffer: Result (%3.1f)", TemperatureN[0].value); return 'T'; } // Temperature update is Knnnnnn# where nnnnn is left space padded rc = sscanf(in_response, "K%5d#", &temp); if (rc > 0) { // need to divide result by 2 TemperatureKN[0].value = ((int) temp)/2.00; LOGF_DEBUG("DecodeBuffer: Result (%3.2f)", TemperatureKN[0].value); return 'K'; } // look for step info Pnnnnn# rc = sscanf(in_response, "P%5d#", &pos); // focuser position returned Pnnnnn# if (rc > 0) { FocusAbsPosN[0].value = pos; IDSetNumber(&FocusAbsPosNP, NULL); LOGF_DEBUG("DecodeBuffer: Returned position (%d)", pos); return 'P'; } else { LOGF_ERROR("DecodeBuffer: Unknown response : (%s)", in_response); return '?'; } } // Get Temperature in C from focuser // // Return : // true : successfully got Temperature & updated INDI // false : Unable to get & update Temperature (timeout or other) // bool Lakeside::updateTemperature() { char resp[LAKESIDE_LEN] = {0}; char cmd[]="?T#"; char buffer_response='?'; if (!SendCmd(cmd)) { return false; } if (!ReadBuffer(resp)) { return false; } LOGF_DEBUG("updateTemperature: Read response (%s)", resp); // ascertain contents of buffer & update temp if necessary buffer_response=DecodeBuffer(resp); // if temperature updated, then return true if ( buffer_response == 'T' ) { return true; } else { return false; } } // Get Temperature in K from focuser // // Return : // true : successfully got Temperature in K & updated INDI // false : Unable to get & update Temperature in K (timeout or other) // bool Lakeside::updateTemperatureK() { char resp[LAKESIDE_LEN] = {0}; char cmd[]="?K#"; char buffer_response='?'; if (!SendCmd(cmd)) { return false; } if (!ReadBuffer(resp)) { return false; } LOGF_DEBUG("updateTemperatureK: Read response (%s)", resp); // ascertain contents of buffer & update temp in K if necessary buffer_response=DecodeBuffer(resp); // if temperature updated, then return true if ( buffer_response == 'K' ) { return true; } else { return false; } } // Get position of focuser // // Return : // true : successfully got focus position & updated INDI // false : Unable to get & update position (timeout or other) // bool Lakeside::updatePosition() { char resp[LAKESIDE_LEN] = {0}; char cmd[]="?P#"; char buffer_response='?'; if (!SendCmd(cmd)) { return false; } LOGF_DEBUG("updatePosition: Successfully sent (%s)",cmd); if (!ReadBuffer(resp)) { return false; } LOGF_DEBUG("updatePosition: Fetched (%s)", resp); // ascertain contents of buffer & update position if necessary buffer_response=DecodeBuffer(resp); if ( buffer_response == 'P' ) { return true; } else { return false; } } // Get Backlash compensation bool Lakeside::updateBacklash() { int rc=-1, temp=-1; char resp[LAKESIDE_LEN] = {0}; char cmd[]="?B#"; if (!SendCmd(cmd)) { return false; } if (!ReadBuffer(resp)) { return false; } // Backlash is in form Bnnnnn# // where nnnnn is 0 - 255, space left padded rc = sscanf(resp, "B%5d#", &temp); if ( temp >= 0) { BacklashN[0].value = temp; LOGF_DEBUG("updateBacklash: Backlash is (%d)", temp); } else { LOGF_ERROR("updateBacklash: Backlash request error (%s)", resp); return false; } return true; } // get Slope 1 Increments bool Lakeside::updateSlope1Inc() { int rc=-1, temp=-1; char resp[LAKESIDE_LEN]; char cmd[]="?1#"; if (!SendCmd(cmd)) { return false; } if (!ReadBuffer(resp)) { return false; } // Slope 1 Increment is in form 1nnnnn# // where nnnnn is number of 0.1 step increments, space left padded rc = sscanf(resp, "1%5d#", &temp); if ( temp >= 0) { Slope1IncN[0].value = temp; LOGF_DEBUG("updateSlope1Inc: Slope 1 Increments is (%d)", temp); } else { LOGF_ERROR("updateSlope1Inc: Slope 1 Increment request error (%s)", resp); return false; } return true; } // get Slope 2 Increments bool Lakeside::updateSlope2Inc() { int rc=-1, temp=-1; char resp[LAKESIDE_LEN] = {0}; char cmd[]="?2#"; if (!SendCmd(cmd)) { return false; } if (!ReadBuffer(resp)) { return false; } // Slope 1 Increment is in form 1nnnnn# // where nnnnn is number of 0.1 step increments, space left padded rc = sscanf(resp, "2%5d#", &temp); if ( temp >= 0) { Slope2IncN[0].value = temp; LOGF_DEBUG("updateSlope2Inc: Slope 2 Increments is (%d)", temp); } else { LOGF_ERROR("updateSlope2Inc: Slope 2 Increment request error (%s)", resp); return false; } return true; } // get Slope 1 direction : 0 or 1 bool Lakeside::updateSlope1Dir() { int rc=-1, temp=-1; char resp[LAKESIDE_LEN] = {0}; char cmd[]="?a#"; if (!SendCmd(cmd)) { return false; } if (!ReadBuffer(resp)) { return false; } // Slope 1 Direction is in form annnnn# // where nnnnn is either 0 or 1, space left padded rc = sscanf(resp, "a%5d#", &temp); if ( temp == 0) { Slope1DirS[0].s = ISS_ON; LOGF_DEBUG("updateSlope1Dir: Slope 1 Direction is (%d)", temp); } else if ( temp == 1) { Slope1DirS[1].s = ISS_ON; } else { LOGF_ERROR("updateSlope1Dir: Unknown Slope 1 Direction response (%s)", resp); return false; } return true; } // get Slope 2 direction : 0 or 1 bool Lakeside::updateSlope2Dir() { int rc=-1, temp=-1; char resp[LAKESIDE_LEN] = {0}; char cmd[]="?b#"; if (!SendCmd(cmd)) { return false; } if (!ReadBuffer(resp)) { return false; } // Slope 2 Direction is in form annnnn# // where nnnnn is either 0 or 1, space left padded rc = sscanf(resp, "b%5d#", &temp); if ( temp == 0) { Slope2DirS[0].s = ISS_ON; LOGF_DEBUG("updateSlope2Dir: Slope 2 Direction is (%d)", temp); } else if ( temp == 1) { Slope2DirS[1].s = ISS_ON; } else { LOGF_ERROR("updateSlope2Dir: Unknown Slope 2 Direction response (%s)", resp); return false; } return true; } // Get slope 1 deadband bool Lakeside::updateSlope1Deadband() { int rc=-1, temp=-1; char resp[LAKESIDE_LEN] = {0}; char cmd[]="?c#"; if (!SendCmd(cmd)) { return false; } if (!ReadBuffer(resp)) { return false; } // Deadband is in form cnnnnn# // where nnnnn is 0 - 255, space left padded rc = sscanf(resp, "c%5d#", &temp); if ( temp >= 0) { Slope1DeadbandN[0].value = temp; LOGF_DEBUG("updateSlope1Deadband: Slope 1 Deadband is (%d)", temp); } else { LOGF_ERROR("updateSlope1Deadband: Slope 1 Deadband request error (%s)", resp); return false; } return true; } // Get slope 2 deadband bool Lakeside::updateSlope2Deadband() { int rc=-1, temp=-1; char resp[LAKESIDE_LEN] = {0}; char cmd[]="?d#"; if (!SendCmd(cmd)) { return false; } if (!ReadBuffer(resp)) { return false; } // Deadband is in form dnnnnn# // where nnnnn is 0 - 255, space left padded rc = sscanf(resp, "d%5d#", &temp); if ( temp >= 0) { Slope2DeadbandN[0].value = temp; LOGF_DEBUG("updateSlope2Deadband: Slope 2 Deadband is (%d)", temp); } else { LOGF_ERROR("updateSlope2Deadband: Slope 2 Deadband request error (%s)", resp); return false; } return true; } // get Slope 1 time period bool Lakeside::updateSlope1Period() { int rc=-1, temp=-1; char resp[LAKESIDE_LEN] = {0}; char cmd[]="?e#"; if (!SendCmd(cmd)) { return false; } if (!ReadBuffer(resp)) { return false; } // Slope 1 Period is in form ennnnn# // where nnnnn is number of 0.1 step increments, space left padded rc = sscanf(resp, "e%5d#", &temp); if ( temp >= 0) { Slope1PeriodN[0].value = temp; LOGF_DEBUG("updateSlope1Period: Slope 1 Period is (%d)", temp); } else { LOGF_ERROR("updateSlope1Period: Slope 1 Period request error (%s)", resp); return false; } return true; } // get Slope 2 time period bool Lakeside::updateSlope2Period() { int rc=-1, temp=-1; char resp[LAKESIDE_LEN] = {0}; char cmd[]="?f#"; if (!SendCmd(cmd)) { return false; } if (!ReadBuffer(resp)) { return false; } // Slope 2 Period is in form ennnnn# // where nnnnn is number of 0.1 step increments, space left padded rc = sscanf(resp, "f%5d#", &temp); if ( temp >= 0) { Slope2PeriodN[0].value = temp; LOGF_DEBUG("updateSlope2Period: Slope 2 Period is (%d)", temp); } else { LOGF_ERROR("updateSlope2Period: Slope 2 Period request error (%s)", resp); return false; } return true; } // Get Max travel bool Lakeside::updateMaxTravel() { int rc=-1, temp=-1; char resp[LAKESIDE_LEN] = {0}; char cmd[]="?I#"; if (!SendCmd(cmd)) { return false; } if (!ReadBuffer(resp)) { return false; } // MaxTravel is in form Innnnn# // where nnnnn is 0 - 65536, space left padded rc = sscanf(resp, "I%5d#", &temp); if ( temp > 0) { MaxTravelN[0].value = temp; LOGF_DEBUG("updateMaxTravel: MaxTravel is (%d)", temp); } else { LOGF_ERROR("updateMaxTravel: MaxTravel request error (%s)", resp); return false; } return true; } // get step size bool Lakeside::updateStepSize() { int rc=-1, temp=-1; char resp[LAKESIDE_LEN] = {0}; char cmd[]="?S#"; if (!SendCmd(cmd)) { return false; } LOGF_DEBUG("updateStepSize: Sent (%s)", cmd); if (!ReadBuffer(resp)) { return false; } // StepSize is in form Snnnnn# // where nnnnn is 0 - ??, space left padded rc = sscanf(resp, "S%5d#", &temp); if ( temp > 0) { StepSizeN[0].value = temp; LOGF_DEBUG("updateStepSize: step size is (%d)", temp); } else { LOGF_ERROR("updateStepSize: StepSize request error (%s)", resp); return false; } return true; } // // NOTE : set via hand controller // bool Lakeside::setCalibration() { return true; } // Move focuser to "position" bool Lakeside::gotoPosition(uint32_t position) { int calc_steps=0; char cmd[LAKESIDE_LEN] = {0}; // Lakeside only uses move NNNNN steps - goto step not available. // calculate as steps to move = current position - new position // if -ve then move out, else +ve moves in calc_steps = FocusAbsPosN[0].value - position; // MaxTravelN[0].value is set by "calibrate" via the control box, & read at connect if ( position > MaxTravelN[0].value ) { LOGF_ERROR("Position requested (%ld) is out of bounds between %g and %g", position, FocusAbsPosN[0].min, MaxTravelN[0].value); FocusAbsPosNP.s = IPS_ALERT; return false; } // -ve == Move Out if ( calc_steps < 0 ) { sprintf(cmd,"CO%d#",abs(calc_steps)); LOGF_DEBUG("MoveFocuser: move-out cmd to send (%s)", cmd); } else // ve == Move In if ( calc_steps > 0 ) { // Move in nnnnn steps = CInnnnn# sprintf(cmd,"CI%d#",calc_steps); LOGF_DEBUG("MoveFocuser: move-in cmd to send (%s)", cmd); } else { // Zero == no steps to move LOGF_DEBUG("MoveFocuser: No steps to move. calc_steps = %d", calc_steps); FocusAbsPosNP.s = IPS_OK; return false; } // flush ready to move tcflush(PortFD, TCIOFLUSH); if (!SendCmd(cmd)) { FocusAbsPosNP.s = IPS_ALERT; return false; } else LOGF_DEBUG("MoveFocuser: Sent cmd (%s)", cmd); // At this point, the move command has been sent, so set BUSY & return true FocusAbsPosNP.s = IPS_BUSY; return true; } // // Set backlash compensation // bool Lakeside::setBacklash(int backlash ) { char cmd[LAKESIDE_LEN] = {0}; char resp[LAKESIDE_LEN] = {0}; tcflush(PortFD, TCIOFLUSH); //CRBnnn# sprintf(cmd,"CRB%d#",backlash); if (!SendCmd(cmd)) { return false; } if (!ReadBuffer(resp)) { return false; } if (!strncmp(resp,"OK#",3)) { LOGF_INFO("Backlash steps set to %d", backlash); } else { LOGF_ERROR("setBacklash: Unknown result (%s)", resp); return false; } return true; } // // NOTE : set via hand controller // Here for example // bool Lakeside::setStepSize(int stepsize ) { char cmd[LAKESIDE_LEN] = {0}; char resp[LAKESIDE_LEN] = {0}; tcflush(PortFD, TCIOFLUSH); // CRSnnnnn# sprintf(cmd,"CRS%d#",stepsize); if (!SendCmd(cmd)) { return false; } if (!ReadBuffer(resp)) { return false; } if (!strncmp(resp,"OK#",3)) { LOGF_DEBUG("setStepSize: cmd (%s) - %s", cmd, resp); } else { LOGF_ERROR("setStepSize: Unknown result (%s)", resp); return false; } return true; } // // NOTE : set via hand controller // Use calibrate routine on controller box // bool Lakeside::setMaxTravel(int /*maxtravel*/ ) { return true; } // Change Move Direction // 0 = Normal direction // 1 = Reverse direction // In case motor connection is on reverse side of the focus shaft // NOTE : This just reverses the voltage sent to the motor // & does NOT reverse the CI / CO commands bool Lakeside::setMoveDirection(int direction) { char cmd[LAKESIDE_LEN] = {0}; char resp[LAKESIDE_LEN] = {0}; tcflush(PortFD, TCIOFLUSH); if (direction == 0) strncpy(cmd, "CRD0#", LAKESIDE_LEN); else if (direction == 1) strncpy(cmd, "CRD1#", LAKESIDE_LEN); else { LOGF_ERROR("setMoveDirection: Unknown direction (%d)", direction); return false; } if (!SendCmd(cmd)) { return false; } if (!ReadBuffer(resp)) { return false; } if (!strncmp(resp,"OK#",3)) { LOGF_DEBUG("setMoveDirection: Completed cmd (%s). Result - %s", cmd, resp); if (direction == 0) LOG_INFO("Move Direction : Normal"); else LOG_INFO("Move Direction : Reversed"); } else { LOGF_ERROR("setMoveDirection: Unknown result (%s)", resp); return false; } return true; } // Enable/disable Temperature Tracking functionality bool Lakeside::setTemperatureTracking(bool enable) { int nbytes_written=0, rc=-1; char errstr[MAXRBUF]; char cmd[LAKESIDE_LEN] = {0}; // flush all tcflush(PortFD, TCIOFLUSH); if (enable) strncpy(cmd, "CTN#", LAKESIDE_LEN); else strncpy(cmd, "CTF#", LAKESIDE_LEN); if ( (rc = tty_write_string(PortFD, cmd, &nbytes_written)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("setTemperatureTracking: Write for command (%s) failed - %s", cmd, errstr); return false; } else { LOGF_DEBUG("setTemperatureTracking: Sent (%s)", cmd); if (enable) LOG_INFO("Temperature Tracking : Enabled"); else LOG_INFO("Temperature Tracking : Disabled"); } // NOTE: NO reply string is sent back return true; } // Set which Active Temperature slope to use : 1 or 2 bool Lakeside::setActiveTemperatureSlope(uint32_t active_slope) { char cmd[LAKESIDE_LEN] = {0}; char resp[LAKESIDE_LEN] = {0}; // flush all tcflush(PortFD, TCIOFLUSH); // slope in is either 1 or 2 // CRg1# : Slope 1 // CRg2# : Slope 2 sprintf(cmd,"CRg%d#",active_slope); if (!SendCmd(cmd)) { return false; } LOGF_DEBUG("setActiveTemperatureSlope: Sent (%s)", cmd); if (!ReadBuffer(resp)) { return false; } if (!strncmp(resp,"OK#",3)) { LOGF_INFO("Selected Active Temperature Slope is %d",active_slope); } else { LOGF_ERROR("setActiveTemperatureSlope: Unknown result (%s)", resp); return false; } return true; } // // Set Slope 1 0.1 step increments // bool Lakeside::setSlope1Inc(uint32_t slope1_inc) { char cmd[LAKESIDE_LEN] = {0}; char resp[LAKESIDE_LEN] = {0}; tcflush(PortFD, TCIOFLUSH); //CR1nnn# sprintf(cmd,"CR1%d#",slope1_inc); if (!SendCmd(cmd)) { return false; } if (!ReadBuffer(resp)) { return false; } if (!strncmp(resp,"OK#",3)) { LOGF_INFO("Slope 1 0.1 counts per degree set to %d", slope1_inc); } else { LOGF_ERROR("setSlope1Inc: Unknown result (%s)", resp); return false; } return true; } // // Set Slope 2 0.1 step increments // bool Lakeside::setSlope2Inc(uint32_t slope2_inc) { char cmd[LAKESIDE_LEN] = {0}; char resp[LAKESIDE_LEN] = {0}; tcflush(PortFD, TCIOFLUSH); //CR2nnn# sprintf(cmd,"CR2%d#",slope2_inc); if (!SendCmd(cmd)) { return false; } if (!ReadBuffer(resp)) { return false; } if (!strncmp(resp,"OK#",3)) { LOGF_INFO("Slope 2 0.1 counts per degree set to %d", slope2_inc); } else { LOGF_ERROR("setSlope2Inc: Unknown result (%s)", resp); return false; } return true; } // // Set slope 1 direction 0 or 1 // bool Lakeside::setSlope1Dir(uint32_t slope1_direction) { char cmd[LAKESIDE_LEN] = {0}; char resp[LAKESIDE_LEN] = {0}; tcflush(PortFD, TCIOFLUSH); //CRannn# sprintf(cmd,"CRa%d#",slope1_direction); if (!SendCmd(cmd)) { return false; } if (!ReadBuffer(resp)) { return false; } if (!strncmp(resp,"OK#",3)) { LOGF_INFO("Slope 1 Direction set to %d", slope1_direction); } else { LOGF_ERROR("setSlope1Dir: Unknown result (%s)", resp); return false; } return true; } // // Set Slope 2 Direction 0 or 1 // bool Lakeside::setSlope2Dir(uint32_t slope2_direction) { char cmd[LAKESIDE_LEN] = {0}; char resp[LAKESIDE_LEN] = {0}; tcflush(PortFD, TCIOFLUSH); //CRannn# sprintf(cmd,"CRb%d#",slope2_direction); if (!SendCmd(cmd)) { return false; } if (!ReadBuffer(resp)) { return false; } if (!strncmp(resp,"OK#",3)) { LOGF_INFO("Slope 2 Direction set to %d", slope2_direction); } else { LOGF_ERROR("setSlope2Dir: Unknown result (%s)", resp); return false; } return true; } // // Set Slope 1 Deadband 0 - 255 // bool Lakeside::setSlope1Deadband(uint32_t slope1_deadband) { char cmd[LAKESIDE_LEN] = {0}; char resp[LAKESIDE_LEN] = {0}; tcflush(PortFD, TCIOFLUSH); //CRcnnn# sprintf(cmd,"CRc%d#",slope1_deadband); if (!SendCmd(cmd)) { return false; } if (!ReadBuffer(resp)) { return false; } if (!strncmp(resp,"OK#",3)) { LOGF_INFO("Slope 1 deadband set to %d", slope1_deadband); } else { LOGF_ERROR("setSlope1Deadband: Unknown result (%s)", resp); return false; } return true; } // // Set Slope 1 Deadband 0 - 255 // bool Lakeside::setSlope2Deadband(uint32_t slope2_deadband) { char cmd[LAKESIDE_LEN] = {0}; char resp[LAKESIDE_LEN] = {0}; tcflush(PortFD, TCIOFLUSH); //CRdnnn# sprintf(cmd,"CRd%d#",slope2_deadband); if (!SendCmd(cmd)) { return false; } if (!ReadBuffer(resp)) { return false; } if (!strncmp(resp,"OK#",3)) { LOGF_INFO("Slope 2 deadband set to %d", slope2_deadband); } else { LOGF_ERROR("setSlope2Deadband: Unknown result (%s)", resp); return false; } return true; } // // Set Slope 1 Period in minutes // bool Lakeside::setSlope1Period(uint32_t slope1_period) { char cmd[LAKESIDE_LEN] = {0}; char resp[LAKESIDE_LEN] = {0}; tcflush(PortFD, TCIOFLUSH); //CRennn# sprintf(cmd,"CRe%d#",slope1_period); if (!SendCmd(cmd)) { return false; } if (!ReadBuffer(resp)) { return false; } if (!strncmp(resp,"OK#",3)) { LOGF_INFO("Slope 1 Period set to %d", slope1_period); } else { LOGF_ERROR("setSlope1Period: Unknown result (%s)", resp); return false; } return true; } // // Set Slope 2 Period in minutes // bool Lakeside::setSlope2Period(uint32_t slope2_period) { char cmd[LAKESIDE_LEN] = {0}; char resp[LAKESIDE_LEN] = {0}; tcflush(PortFD, TCIOFLUSH); //CRfnnn# sprintf(cmd,"CRf%d#",slope2_period); if (!SendCmd(cmd)) { return false; } if (!ReadBuffer(resp)) { return false; } if (!strncmp(resp,"OK#",3)) { LOGF_INFO("Slope 2 Period set to %d", slope2_period); } else { LOGF_ERROR("setSlope2Period: Unknown result (%s)", resp); return false; } return true; } // // Process client new switch // bool Lakeside::ISNewSwitch (const char *dev, const char *name, ISState *states, char *names[], int n) { if(strcmp(dev,getDeviceName())==0) { // Move Direction if (!strcmp(MoveDirectionSP.name, name)) { bool rc=false; int current_mode = IUFindOnSwitchIndex(&MoveDirectionSP); IUUpdateSwitch(&MoveDirectionSP, states, names, n); int target_mode = IUFindOnSwitchIndex(&MoveDirectionSP); if (current_mode == target_mode) { MoveDirectionSP.s = IPS_OK; IDSetSwitch(&MoveDirectionSP, NULL); } // switch will be either 0 for normal or 1 for reverse rc = setMoveDirection(target_mode); if (rc == false) { IUResetSwitch(&MoveDirectionSP); MoveDirectionS[current_mode].s = ISS_ON; MoveDirectionSP.s = IPS_ALERT; IDSetSwitch(&MoveDirectionSP, NULL); return false; } MoveDirectionSP.s = IPS_OK; IDSetSwitch(&MoveDirectionSP, NULL); return true; } // Temperature Tracking if (!strcmp(TemperatureTrackingSP.name, name)) { int last_index = IUFindOnSwitchIndex(&TemperatureTrackingSP); IUUpdateSwitch(&TemperatureTrackingSP, states, names, n); bool rc = setTemperatureTracking((TemperatureTrackingS[0].s == ISS_ON)); if (rc == false) { TemperatureTrackingSP.s = IPS_ALERT; IUResetSwitch(&TemperatureTrackingSP); TemperatureTrackingS[last_index].s = ISS_ON; IDSetSwitch(&TemperatureTrackingSP, NULL); return false; } TemperatureTrackingSP.s = IPS_OK; IDSetSwitch(&TemperatureTrackingSP, NULL); return true; } // Active Temperature Slope if (!strcmp(ActiveTemperatureSlopeSP.name, name)) { bool rc=false; int current_slope = IUFindOnSwitchIndex(&ActiveTemperatureSlopeSP); // current slope Selection will be either 1 or 2 // Need to add 1 to array index, as it starts at 0 current_slope++; IUUpdateSwitch(&ActiveTemperatureSlopeSP, states, names, n); int target_slope = IUFindOnSwitchIndex(&ActiveTemperatureSlopeSP); // target slope Selection will be either 1 or 2 // Need to add 1 to array index, as it starts at 0 target_slope++; if (current_slope == target_slope) { ActiveTemperatureSlopeSP.s = IPS_OK; IDSetSwitch(&ActiveTemperatureSlopeSP, NULL); } rc = setActiveTemperatureSlope(target_slope); if (rc == false) { current_slope--; IUResetSwitch(&ActiveTemperatureSlopeSP); ActiveTemperatureSlopeS[current_slope].s = ISS_ON; ActiveTemperatureSlopeSP.s = IPS_ALERT; IDSetSwitch(&ActiveTemperatureSlopeSP, NULL); return false; } ActiveTemperatureSlopeSP.s = IPS_OK; IDSetSwitch(&ActiveTemperatureSlopeSP, NULL); return true; } // Slope 1 direction - either 0 or 1 if (!strcmp(Slope1DirSP.name, name)) { bool rc=false; int current_slope_dir1 = IUFindOnSwitchIndex(&Slope1DirSP); // current slope 1 Direction will be either 0 or 1 IUUpdateSwitch(&Slope1DirSP, states, names, n); int target_slope_dir1 = IUFindOnSwitchIndex(&Slope1DirSP); // target slope Selection will be either 0 or 1 if (current_slope_dir1 == target_slope_dir1) { Slope1DirSP.s = IPS_OK; IDSetSwitch(&Slope1DirSP, NULL); } rc = setSlope1Dir(target_slope_dir1); if (rc == false) { IUResetSwitch(&Slope1DirSP); Slope1DirS[current_slope_dir1].s = ISS_ON; Slope1DirSP.s = IPS_ALERT; IDSetSwitch(&Slope1DirSP, NULL); return false; } Slope1DirSP.s = IPS_OK; IDSetSwitch(&Slope1DirSP, NULL); return true; } } // Slope 2 direction - either 0 or 1 if (!strcmp(Slope2DirSP.name, name)) { bool rc=false; int current_slope_dir2 = IUFindOnSwitchIndex(&Slope2DirSP); // current slope 2 Direction will be either 0 or 1 IUUpdateSwitch(&Slope2DirSP, states, names, n); int target_slope_dir2 = IUFindOnSwitchIndex(&Slope2DirSP); // target slope 2 Selection will be either 0 or 1 if (current_slope_dir2 == target_slope_dir2) { Slope2DirSP.s = IPS_OK; IDSetSwitch(&Slope2DirSP, NULL); } rc = setSlope2Dir(target_slope_dir2); if (rc == false) { IUResetSwitch(&Slope2DirSP); Slope2DirS[current_slope_dir2].s = ISS_ON; Slope2DirSP.s = IPS_ALERT; IDSetSwitch(&Slope2DirSP, NULL); return false; } Slope2DirSP.s = IPS_OK; IDSetSwitch(&Slope2DirSP, NULL); return true; } return INDI::Focuser::ISNewSwitch(dev, name, states, names, n); } // // Process client new number // bool Lakeside::ISNewNumber (const char *dev, const char *name, double values[], char *names[], int n) { int i=0; if(strcmp(dev,getDeviceName())==0) { // max travel - read only if (!strcmp (name, MaxTravelNP.name)) { IUUpdateNumber(&MaxTravelNP, values, names, n); MaxTravelNP.s = IPS_OK; IDSetNumber(&MaxTravelNP, NULL); return true; } // Backlash compensation if (!strcmp (name, BacklashNP.name)) { int new_back = 0 ; int nset = 0; for (nset = i = 0; i < n; i++) { //Find numbers with the passed names in SetBacklashNP property INumber *eqp = IUFindNumber (&BacklashNP, names[i]); //If the number found is Backlash (BacklashN[0]) then process it if (eqp == &BacklashN[0]){ new_back = (values[i]); // limits nset += new_back >= -0xff && new_back <= 0xff; } if (nset == 1) { // Set the Lakeside state to BUSY BacklashNP.s = IPS_BUSY; IDSetNumber(&BacklashNP, NULL); if( !setBacklash(new_back)) { BacklashNP.s = IPS_IDLE; IDSetNumber(&BacklashNP, "Setting new backlash failed."); return false ; } BacklashNP.s = IPS_OK; BacklashN[0].value = new_back; IDSetNumber(&BacklashNP, NULL); return true; } else { BacklashNP.s = IPS_IDLE; IDSetNumber(&BacklashNP, "Need exactly one parameter."); return false ; } } } // Step size - read only if (!strcmp (name, StepSizeNP.name)) { IUUpdateNumber(&StepSizeNP, values, names, n); StepSizeNP.s = IPS_OK; IDSetNumber(&StepSizeNP, NULL); return true; } // Slope 1 Increments if (!strcmp (name, Slope1IncNP.name)) { int new_Slope1Inc = 0 ; int nset = 0; for (nset = i = 0; i < n; i++) { //Find numbers with the passed names in SetSlope1IncNP property INumber *eqp = IUFindNumber (&Slope1IncNP, names[i]); //If the number found is Slope1Inc (Slope1IncN[0]) then process it if (eqp == &Slope1IncN[0]){ new_Slope1Inc = (values[i]); // limits nset += new_Slope1Inc >= -0xff && new_Slope1Inc <= 0xff; } if (nset == 1) { // Set the Lakeside state to BUSY Slope1IncNP.s = IPS_BUSY; IDSetNumber(&Slope1IncNP, NULL); if( !setSlope1Inc(new_Slope1Inc)) { Slope1IncNP.s = IPS_IDLE; IDSetNumber(&Slope1IncNP, "Setting new Slope1 increment failed."); return false ; } Slope1IncNP.s = IPS_OK; Slope1IncN[0].value = new_Slope1Inc; IDSetNumber(&Slope1IncNP, NULL) ; return true; } else { Slope1IncNP.s = IPS_IDLE; IDSetNumber(&Slope1IncNP, "Need exactly one parameter."); return false ; } } } // Slope 2 Increments if (!strcmp (name, Slope2IncNP.name)) { int new_Slope2Inc = 0 ; int nset = 0; for (nset = i = 0; i < n; i++) { //Find numbers with the passed names in SetSlope2IncNP property INumber *eqp = IUFindNumber (&Slope2IncNP, names[i]); //If the number found is Slope2Inc (Slope2IncN[0]) then process it if (eqp == &Slope2IncN[0]){ new_Slope2Inc = (values[i]); // limits nset += new_Slope2Inc >= -0xff && new_Slope2Inc <= 0xff; } if (nset == 1) { // Set the Lakeside state to BUSY Slope2IncNP.s = IPS_BUSY; IDSetNumber(&Slope2IncNP, NULL); if( !setSlope2Inc(new_Slope2Inc)) { Slope2IncNP.s = IPS_IDLE; IDSetNumber(&Slope2IncNP, "Setting new Slope2 increment failed."); return false ; } Slope2IncNP.s = IPS_OK; Slope2IncN[0].value = new_Slope2Inc; IDSetNumber(&Slope2IncNP, NULL); return true; } else { Slope2IncNP.s = IPS_IDLE; IDSetNumber(&Slope2IncNP, "Need exactly one parameter."); return false ; } } } // Slope 1 Deadband if (!strcmp (name, Slope1DeadbandNP.name)) { int new_Slope1Deadband = 0 ; int nset = 0; for (nset = i = 0; i < n; i++) { //Find numbers with the passed names in SetSlope1DeadbandNP property INumber *eqp = IUFindNumber (&Slope1DeadbandNP, names[i]); //If the number found is Slope1Deadband (Slope1DeadbandN[0]) then process it if (eqp == &Slope1DeadbandN[0]){ new_Slope1Deadband = (values[i]); // limits nset += new_Slope1Deadband >= -0xff && new_Slope1Deadband <= 0xff; } if (nset == 1) { // Set the Lakeside state to BUSY Slope1DeadbandNP.s = IPS_BUSY; IDSetNumber(&Slope1DeadbandNP, NULL); if( !setSlope1Deadband(new_Slope1Deadband)) { Slope1DeadbandNP.s = IPS_IDLE; IDSetNumber(&Slope1DeadbandNP, "Setting new Slope 1 Deadband failed."); return false ; } Slope1DeadbandNP.s = IPS_OK; Slope1DeadbandN[0].value = new_Slope1Deadband; IDSetNumber(&Slope1DeadbandNP, NULL) ; return true; } else { Slope1DeadbandNP.s = IPS_IDLE; IDSetNumber(&Slope1DeadbandNP, "Need exactly one parameter."); return false ; } } } // Slope 2 Deadband if (!strcmp (name, Slope2DeadbandNP.name)) { int new_Slope2Deadband = 0 ; int nset = 0; for (nset = i = 0; i < n; i++) { //Find numbers with the passed names in SetSlope2DeadbandNP property INumber *eqp = IUFindNumber (&Slope2DeadbandNP, names[i]); //If the number found is Slope2Deadband (Slope2DeadbandN[0]) then process it if (eqp == &Slope2DeadbandN[0]){ new_Slope2Deadband = (values[i]); // limits nset += new_Slope2Deadband >= -0xff && new_Slope2Deadband <= 0xff; } if (nset == 1) { // Set the Lakeside state to BUSY Slope2DeadbandNP.s = IPS_BUSY; IDSetNumber(&Slope2DeadbandNP, NULL); if( !setSlope2Deadband(new_Slope2Deadband)) { Slope2DeadbandNP.s = IPS_IDLE; IDSetNumber(&Slope2DeadbandNP, "Setting new Slope 2 Deadband failed."); return false ; } Slope2DeadbandNP.s = IPS_OK; Slope2DeadbandN[0].value = new_Slope2Deadband; IDSetNumber(&Slope2DeadbandNP, NULL) ; return true; } else { Slope2DeadbandNP.s = IPS_IDLE; IDSetNumber(&Slope2DeadbandNP, "Need exactly one parameter."); return false ; } } } // Slope 1 Period Minutes if (!strcmp (name, Slope1PeriodNP.name)) { int new_Slope1Period = 0 ; int nset = 0; for (nset = i = 0; i < n; i++) { //Find numbers with the passed names in SetSlope1PeriodNP property INumber *eqp = IUFindNumber (&Slope1PeriodNP, names[i]); //If the number found is Slope1Period (Slope1PeriodN[0]) then process it if (eqp == &Slope1PeriodN[0]){ new_Slope1Period = (values[i]); // limits nset += new_Slope1Period >= -0xff && new_Slope1Period <= 0xff; } if (nset == 1) { // Set the Lakeside state to BUSY Slope1PeriodNP.s = IPS_BUSY; IDSetNumber(&Slope1PeriodNP, NULL); if( !setSlope1Period(new_Slope1Period)) { Slope1PeriodNP.s = IPS_IDLE; IDSetNumber(&Slope1PeriodNP, "Setting new Slope 1 Period failed."); return false ; } Slope1PeriodNP.s = IPS_OK; Slope1PeriodN[0].value = new_Slope1Period; IDSetNumber(&Slope1PeriodNP, NULL); return true; } else { Slope1PeriodNP.s = IPS_IDLE; IDSetNumber(&Slope1PeriodNP, "Need exactly one parameter."); return false ; } } } // Slope 2 Period Minutes if (!strcmp (name, Slope2PeriodNP.name)) { int new_Slope2Period = 0 ; int nset = 0; for (nset = i = 0; i < n; i++) { //Find numbers with the passed names in SetSlope2PeriodNP property INumber *eqp = IUFindNumber (&Slope2PeriodNP, names[i]); //If the number found is Slope2Period (Slope2PeriodN[0]) then process it if (eqp == &Slope2PeriodN[0]){ new_Slope2Period = (values[i]); // limits nset += new_Slope2Period >= -0xff && new_Slope2Period <= 0xff; } if (nset == 1) { // Set the Lakeside state to BUSY Slope2PeriodNP.s = IPS_BUSY; IDSetNumber(&Slope2PeriodNP, NULL); if( !setSlope2Period(new_Slope2Period)) { Slope2PeriodNP.s = IPS_IDLE; IDSetNumber(&Slope2PeriodNP, "Setting new Slope 2 Period failed."); return false ; } Slope2PeriodNP.s = IPS_OK; Slope2PeriodN[0].value = new_Slope2Period; IDSetNumber(&Slope2PeriodNP, NULL); return true; } else { Slope2PeriodNP.s = IPS_IDLE; IDSetNumber(&Slope2PeriodNP, "Need exactly one parameter."); return false ; } } } } return INDI::Focuser::ISNewNumber(dev, name, values, names, n); } // // Get focus paraameters // void Lakeside::GetFocusParams () { if (updatePosition()) IDSetNumber(&FocusAbsPosNP, NULL); if (updateTemperature()) IDSetNumber(&TemperatureNP, NULL); // This is currently the only time Kelvin is read - just a nice to have if (updateTemperatureK()) IDSetNumber(&TemperatureKNP, NULL); if (updateBacklash()) IDSetNumber(&BacklashNP, NULL); if (updateMaxTravel()) IDSetNumber(&MaxTravelNP, NULL); if (updateStepSize()) IDSetNumber(&StepSizeNP, NULL); if (updateMoveDirection()) IDSetSwitch(&MoveDirectionSP, NULL); if (updateSlope1Inc()) IDSetNumber(&Slope1IncNP, NULL); if (updateSlope2Inc()) IDSetNumber(&Slope2IncNP, NULL); if (updateSlope1Dir()) IDSetSwitch(&Slope1DirSP, NULL); if (updateSlope2Dir()) IDSetSwitch(&Slope2DirSP, NULL); if (updateSlope1Deadband()) IDSetNumber(&Slope1DeadbandNP, NULL); if (updateSlope2Deadband()) IDSetNumber(&Slope2DeadbandNP, NULL); if (updateSlope1Period()) IDSetNumber(&Slope1PeriodNP, NULL); if (updateSlope1Period()) IDSetNumber(&Slope2PeriodNP, NULL); } IPState Lakeside::MoveRelFocuser(FocusDirection dir, uint32_t ticks) { double newPosition = 0; bool rc = false; if (dir == FOCUS_INWARD) newPosition = FocusAbsPosN[0].value - ticks; else newPosition = FocusAbsPosN[0].value + ticks; rc = gotoPosition((uint32_t)newPosition); if (!rc) return IPS_ALERT; FocusRelPosN[0].value = ticks; FocusRelPosNP.s = IPS_BUSY; return IPS_BUSY; } // // Main Lakeside Absolute movement routine // IPState Lakeside::MoveAbsFocuser(uint32_t targetTicks) { targetPos = targetTicks; bool rc = false; rc = gotoPosition((uint32_t)targetPos); // if MoveFocuser succeed, then move send successfully if (rc == true) { FocusAbsPosNP.s = IPS_BUSY; //LOG_DEBUG("MoveAbsFocuser: returning IPS_BUSY"); return IPS_BUSY; } else { LOG_DEBUG("MoveAbsFocuser: move failed"); return FocusAbsPosNP.s; } } // // Main timer hit routine // void Lakeside::TimerHit() { bool IsMoving=false; int rc=-1; if (isConnected() == false) { SetTimer(POLLMS); return; } // focuser supposedly moving... if (FocusAbsPosNP.s == IPS_BUSY ) { // Get actual status from focuser // Note: GetLakesideStatus sends position count when moving. // Status returns IMoving if moving IsMoving = GetLakesideStatus(); if ( IsMoving ) { // GetLakesideStatus() shows position as it is moving LOG_DEBUG("TimerHit: Focuser still moving"); } else { // no longer moving, so reset state to IPS_OK or IDLE? // IPS_OK turns light green FocusAbsPosNP.s = IPS_OK; // update position // This is necessary in case user clicks short step moves in quick succession // Lakeside will abort move if command received during move rc=updatePosition(); IDSetNumber(&FocusAbsPosNP, NULL); LOGF_INFO("Focuser reached requested position %.f",FocusAbsPosN[0].value); } } // focuser not moving, get temperature updates instead if (FocusAbsPosNP.s == IPS_OK || FocusAbsPosNP.s == IPS_IDLE) { // Get a temperature rc=updateTemperature(); if (rc) { IDSetNumber(&TemperatureNP, NULL); lastTemperature = TemperatureN[0].value; } } // IPS_ALERT - any alert situation generated if ( FocusAbsPosNP.s == IPS_ALERT ) { LOG_DEBUG("TimerHit: Focuser state = IPS_ALERT"); } SetTimer(POLLMS); } // // This will check the status is the focuser - used to check if moving // // Returns Pnnnnn# : Focuser Moving : return true // empty (time out) : Focuser Idle - NOT moving : return false // Returns DONE# : Focuser Finished moving : return false // Returns OK# : Focuser NOT moving (catchall) : return false bool Lakeside::GetLakesideStatus() { int rc=-1, nbytes_read=0, count_timeouts=1, pos=0; char errstr[MAXRBUF]; char resp[LAKESIDE_LEN] = {0}; bool read_buffer=true; char buffer_response='?'; // read buffer up to LAKESIDE_TIMEOUT_RETRIES times while (read_buffer) { //strcpy(resp," "); memset(resp, 0, sizeof(resp)); // read until 0x23 (#) received if ( (rc = tty_read_section(PortFD, resp, 0x23, LAKESIDE_TIMEOUT, &nbytes_read)) != TTY_OK) { // Retry LAKESIDE_TIMEOUT_RETRIES times to make sure focuser // is not in between status returns count_timeouts++; LOGF_DEBUG("GetLakesideStatus: read buffer retry attempts : %d, error=%s", count_timeouts,errstr); if (count_timeouts > LAKESIDE_TIMEOUT_RETRIES) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_DEBUG("GetLakesideStatus: Timeout limit (%d) reached reading buffer. Error - %s", LAKESIDE_TIMEOUT_RETRIES, errstr); // force a get focuser position update rc=updatePosition(); // return false as focuser is NOT known to be moving return false; } // if (count_timeouts > LAKESIDE_TIMEOUT_RETRIES) } else read_buffer = false; // break out of loop as buffer has been read } // end while // At this point, something has been returned from the buffer // Therefore, decode response LOGF_DEBUG("GetLakesideStatus: Read buffer contains : %s", resp); // decode the contents of the buffer (Temp & Pos are also updated) buffer_response=DecodeBuffer(resp); // If DONE# then focuser has finished a move, so get position if ( buffer_response == 'D' ) { LOG_DEBUG("GetLakesideStatus: Found DONE# after move request"); // update the current position rc = updatePosition(); // IPS_IDLE turns off light, IPS_OK turns light green FocusAbsPosNP.s = IPS_OK; // return false as focuser is not known to be moving return false; } // If focuser moving > 200 steps, DecodeBuffer returns 'P' // & updates position if ( buffer_response == 'P' ) { // get step position for update message rc = sscanf(resp, "P%5d#", &pos); LOGF_INFO("Focuser Moving... position : %d",pos); // Update current position FocusAbsPosN[0].value = pos; IDSetNumber(&FocusAbsPosNP, NULL); // return true as focuser IS moving return true; } // Possible that Temperature response still in the buffer? if ( buffer_response == 'T' ) { LOGF_DEBUG("GetLakesideStatus: Temperature status response found - %s",resp); // return false as focuser is not known to be moving // IPS_IDLE turns off light, IPS_OK turns light green FocusAbsPosNP.s = IPS_OK; return false; } // Possible that Temperature in K response still in the buffer? if ( buffer_response == 'K' ) { LOGF_DEBUG("GetLakesideStatus: Temperature in K status response found - %s",resp); // return false as focuser is not known to be moving // IPS_IDLE turns off light, IPS_OK turns light green FocusAbsPosNP.s = IPS_OK; return false; } // At this point, something else is returned LOGF_DEBUG("GetLakesideStatus: Unknown response from buffer read : (%s)",resp); FocusAbsPosNP.s = IPS_OK; // return false as focuser is not known to be moving return false; } // // send abort command // bool Lakeside::AbortFocuser() { int rc=-1; char errstr[MAXRBUF]; char cmd[]="CH#"; if (SendCmd(cmd)) { // IPS_IDLE turns off light, IPS_OK turns light green FocusAbsPosNP.s = IPS_IDLE; FocusAbsPosNP.s = IPS_OK; LOG_INFO("Focuser Abort Sent"); return true; } else { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("AbortFocuser: Write command (%s) failed - %s",cmd, errstr); return false; } } // End Lakeside Focuser libindi/drivers/focuser/focuslynxbase.cpp0000664000175000017500000025445413263645557020225 0ustar jasemjasem/* Focus Lynx INDI driver Copyright (C) 2015 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 */ #include "focuslynxbase.h" #define LYNXFOCUS_MAX_RETRIES 1 #define LYNXFOCUS_TIMEOUT 2 #define LYNXFOCUS_MAXBUF 16 #define LYNXFOCUS_TEMPERATURE_FREQ 20 /* Update every 20 POLLMS cycles. For POLLMS 500ms = 10 seconds freq */ #define LYNXFOCUS_POSITION_THRESHOLD 5 /* Only send position updates to client if the diff exceeds 5 steps */ #define FOCUS_SETTINGS_TAB "Settings" #define FOCUS_STATUS_TAB "Status" /************************************************************************************ * * ***********************************************************************************/ FocusLynxBase::FocusLynxBase(const char *target) { INDI_UNUSED(target); } /************************************************************************************ * * ***********************************************************************************/ FocusLynxBase::FocusLynxBase() { setVersion(VERSION, SUBVERSION); lynxModels["Optec TCF-Lynx 2"] = "OA"; lynxModels["Optec TCF-Lynx 3"] = "OB"; lynxModels["Optec TCF-Lynx 2 with Extended Travel"] = "OC"; lynxModels["Optec Fast Focus Secondary Focuser"] = "OD"; lynxModels["Optec TCF-S Classic converted"] = "OE"; lynxModels["Optec TCF-S3 Classic converted"] = "OF"; // lynxModels["Optec Gemini (reserved for future use)"] = "OG"; lynxModels["FocusLynx QuickSync FT Hi-Torque"] = "FA"; lynxModels["FocusLynx QuickSync FT Hi-Speed"] = "FB"; // lynxModels["FocusLynx QuickSync SV (reserved for future use)"] = "FC"; lynxModels["DirectSync TEC with bipolar motor - higher speed"] = "FD"; lynxModels["FocusLynx QuickSync Long Travel Hi-Torque"] = "FE"; lynxModels["FocusLynx QuickSync Long Travel Hi-Speed"] = "FF"; lynxModels["FeatherTouch Motor PDMS"] = "FE"; lynxModels["FeatherTouch Motor Hi-Speed"] = "SO"; lynxModels["FeatherTouch Motor Hi-Torque"] = "SP"; lynxModels["Starlight Instruments - FTM with MicroTouch"] = "SQ"; lynxModels["Televue Focuser"] = "TA"; ModelS = nullptr; focusMoveRequest = 0; simPosition = 0; // Can move in Absolute & Relative motions, can AbortFocuser motion, and has variable speed. FI::SetCapability(FOCUSER_CAN_ABORT | FOCUSER_CAN_ABS_MOVE | FOCUSER_CAN_REL_MOVE); isAbsolute = false; isSynced = false; isHoming = false; simStatus[STATUS_MOVING] = ISS_OFF; simStatus[STATUS_HOMING] = ISS_OFF; simStatus[STATUS_HOMED] = ISS_OFF; simStatus[STATUS_FFDETECT] = ISS_OFF; simStatus[STATUS_TMPPROBE] = ISS_ON; simStatus[STATUS_REMOTEIO] = ISS_ON; simStatus[STATUS_HNDCTRL] = ISS_ON; simStatus[STATUS_REVERSE] = ISS_OFF; } /************************************************************************************ * * ***********************************************************************************/ FocusLynxBase::~FocusLynxBase() { } /************************************************************************************ * * ***********************************************************************************/ bool FocusLynxBase::initProperties() { INDI::Focuser::initProperties(); // Focuser temperature IUFillNumber(&TemperatureN[0], "TEMPERATURE", "Celsius", "%6.2f", -50, 70., 0., 0.); IUFillNumberVector(&TemperatureNP, TemperatureN, 1, getDeviceName(), "FOCUS_TEMPERATURE", "Temperature", MAIN_CONTROL_TAB, IP_RO, 0, IPS_IDLE); // Enable/Disable temperature compensation IUFillSwitch(&TemperatureCompensateS[0], "Enable", "", ISS_OFF); IUFillSwitch(&TemperatureCompensateS[1], "Disable", "", ISS_ON); IUFillSwitchVector(&TemperatureCompensateSP, TemperatureCompensateS, 2, getDeviceName(), "T. Compensation", "", FOCUS_SETTINGS_TAB, IP_RW, ISR_1OFMANY, 0, IPS_IDLE); // Enable/Disable temperature compensation on start IUFillSwitch(&TemperatureCompensateOnStartS[0], "Enable", "", ISS_OFF); IUFillSwitch(&TemperatureCompensateOnStartS[1], "Disable", "", ISS_ON); IUFillSwitchVector(&TemperatureCompensateOnStartSP, TemperatureCompensateOnStartS, 2, getDeviceName(), "T. Compensation @Start", "", FOCUS_SETTINGS_TAB, IP_RW, ISR_1OFMANY, 0, IPS_IDLE); // Temperature Coefficient IUFillNumber(&TemperatureCoeffN[0], "A", "", "%.f", -9999, 9999, 100., 0.); IUFillNumber(&TemperatureCoeffN[1], "B", "", "%.f", -9999, 9999, 100., 0.); IUFillNumber(&TemperatureCoeffN[2], "C", "", "%.f", -9999, 9999, 100., 0.); IUFillNumber(&TemperatureCoeffN[3], "D", "", "%.f", -9999, 9999, 100., 0.); IUFillNumber(&TemperatureCoeffN[4], "E", "", "%.f", -9999, 9999, 100., 0.); IUFillNumberVector(&TemperatureCoeffNP, TemperatureCoeffN, 5, getDeviceName(), "T. Coeff", "", FOCUS_SETTINGS_TAB, IP_RW, 0, IPS_IDLE); // Enable/Disable temperature Mode IUFillSwitch(&TemperatureCompensateModeS[0], "A", "", ISS_OFF); IUFillSwitch(&TemperatureCompensateModeS[1], "B", "", ISS_OFF); IUFillSwitch(&TemperatureCompensateModeS[2], "C", "", ISS_OFF); IUFillSwitch(&TemperatureCompensateModeS[3], "D", "", ISS_OFF); IUFillSwitch(&TemperatureCompensateModeS[4], "E", "", ISS_OFF); IUFillSwitchVector(&TemperatureCompensateModeSP, TemperatureCompensateModeS, 5, getDeviceName(), "Compensate Mode", "", FOCUS_SETTINGS_TAB, IP_RW, ISR_ATMOST1, 0, IPS_IDLE); // Enable/Disable backlash IUFillSwitch(&BacklashCompensationS[0], "Enable", "", ISS_OFF); IUFillSwitch(&BacklashCompensationS[1], "Disable", "", ISS_ON); IUFillSwitchVector(&BacklashCompensationSP, BacklashCompensationS, 2, getDeviceName(), "Backlash Compensation", "", FOCUS_SETTINGS_TAB, IP_RW, ISR_1OFMANY, 0, IPS_IDLE); // Backlash Value IUFillNumber(&BacklashN[0], "Value", "", "%.f", 0, 99, 5., 0.); IUFillNumberVector(&BacklashNP, BacklashN, 1, getDeviceName(), "Backlash", "", FOCUS_SETTINGS_TAB, IP_RW, 0, IPS_IDLE); // Max Travel relative focusers IUFillNumber(&MaxTravelN[0], "Ticks", "", "%.f", 0, 100000, 0., 0.); IUFillNumberVector(&MaxTravelNP, MaxTravelN, 1, getDeviceName(), "Max Travel", "", FOCUS_SETTINGS_TAB, IP_RW, 0, IPS_IDLE); // Reset to Factory setting IUFillSwitch(&ResetS[0], "Factory", "", ISS_OFF); IUFillSwitchVector(&ResetSP, ResetS, 1, getDeviceName(), "Reset", "", FOCUS_SETTINGS_TAB, IP_RW, ISR_ATMOST1, 0, IPS_IDLE); // Go to home/center IUFillSwitch(&GotoS[GOTO_CENTER], "Center", "", ISS_OFF); IUFillSwitch(&GotoS[GOTO_HOME], "Home", "", ISS_OFF); IUFillSwitchVector(&GotoSP, GotoS, 2, getDeviceName(), "GOTO", "", MAIN_CONTROL_TAB, IP_RW, ISR_1OFMANY, 0, IPS_IDLE); // Reverse direction IUFillSwitch(&ReverseS[0], "Enable", "", ISS_OFF); IUFillSwitch(&ReverseS[1], "Disable", "", ISS_ON); IUFillSwitchVector(&ReverseSP, ReverseS, 2, getDeviceName(), "Reverse", "", FOCUS_SETTINGS_TAB, IP_RW, ISR_1OFMANY, 0, IPS_IDLE); // List all supported models std::map::iterator iter; int nModels = 1; ModelS = (ISwitch *)malloc(sizeof(ISwitch)); // Need to be able to select no focuser to avoid troubles with Ekos IUFillSwitch(ModelS, "No Focuser", "No Focuser", ISS_ON); for (iter = lynxModels.begin(); iter != lynxModels.end(); ++iter) { ModelS = (ISwitch *)realloc(ModelS, (nModels + 1) * sizeof(ISwitch)); IUFillSwitch(ModelS + nModels, (iter->first).c_str(), (iter->first).c_str(), ISS_OFF); nModels++; } IUFillSwitchVector(&ModelSP, ModelS, nModels, getDeviceName(), "Model", "", MAIN_CONTROL_TAB, IP_RW, ISR_1OFMANY, 0, IPS_IDLE); // Sync to a particular position IUFillNumber(&SyncN[0], "Ticks", "", "%.f", 0, 200000, 100., 0.); IUFillNumberVector(&SyncNP, SyncN, 1, getDeviceName(), "Sync", "", MAIN_CONTROL_TAB, IP_RW, 0, IPS_IDLE); // Status indicators IUFillLight(&StatusL[STATUS_MOVING], "Is Moving", "", IPS_IDLE); IUFillLight(&StatusL[STATUS_HOMING], "Is Homing", "", IPS_IDLE); IUFillLight(&StatusL[STATUS_HOMED], "Is Homed", "", IPS_IDLE); IUFillLight(&StatusL[STATUS_FFDETECT], "FF Detect", "", IPS_IDLE); IUFillLight(&StatusL[STATUS_TMPPROBE], "Tmp Probe", "", IPS_IDLE); IUFillLight(&StatusL[STATUS_REMOTEIO], "Remote IO", "", IPS_IDLE); IUFillLight(&StatusL[STATUS_HNDCTRL], "Hnd Ctrl", "", IPS_IDLE); IUFillLight(&StatusL[STATUS_REVERSE], "Reverse", "", IPS_IDLE); IUFillLightVector(&StatusLP, StatusL, 8, getDeviceName(), "Status", "", FOCUS_STATUS_TAB, IPS_IDLE); // Focus name configure in the HUB IUFillText(&HFocusNameT[0], "FocusName", "Focuser name", ""); IUFillTextVector(&HFocusNameTP, HFocusNameT, 1, getDeviceName(), "FOCUSNAME", "HUB", FOCUS_SETTINGS_TAB, IP_RW, 0, IPS_IDLE); // Led intensity value IUFillNumber(&LedN[0], "Intensity", "", "%.f", 0, 100, 5., 0.); IUFillNumberVector(&LedNP, LedN, 1, getDeviceName(), "Led", "", FOCUS_SETTINGS_TAB, IP_RW, 0, IPS_IDLE); //simPosition = FocusAbsPosN[0].value; addAuxControls(); return true; } /************************************************************************************ * * ***********************************************************************************/ void FocusLynxBase::ISGetProperties(const char *dev) { if (dev != nullptr && strcmp(dev, getDeviceName()) != 0) return; INDI::Focuser::ISGetProperties(dev); defineSwitch(&ModelSP); loadConfig(true, "Model"); } /************************************************************************************ * * ***********************************************************************************/ bool FocusLynxBase::updateProperties() { INDI::Focuser::updateProperties(); if (isConnected()) { defineText(&HFocusNameTP); // If focuser is relative, we define SYNC command. if (isAbsolute == false) defineNumber(&SyncNP); defineNumber(&TemperatureNP); defineNumber(&TemperatureCoeffNP); defineSwitch(&TemperatureCompensateModeSP); defineSwitch(&TemperatureCompensateSP); defineSwitch(&TemperatureCompensateOnStartSP); defineSwitch(&BacklashCompensationSP); defineNumber(&BacklashNP); // For absolute focusers the vector is set to RO, as we get value from the HUB if (isAbsolute == false) MaxTravelNP.p = IP_RW; else MaxTravelNP.p = IP_RO; defineNumber(&MaxTravelNP); defineSwitch(&ResetSP); // If focuser is relative, we only exposure "Center" command as it cannot home if (isAbsolute == false) GotoSP.nsp = 1; else GotoSP.nsp = 2; defineSwitch(&GotoSP); defineSwitch(&ReverseSP); defineLight(&StatusLP); if (getFocusConfig()) LOG_INFO("FocusLynx paramaters updated, focuser ready for use."); else { LOG_ERROR("Failed to retrieve focuser configuration settings..."); return false; } } else { if (isAbsolute == false) deleteProperty(SyncNP.name); deleteProperty(TemperatureNP.name); deleteProperty(TemperatureCoeffNP.name); deleteProperty(TemperatureCompensateModeSP.name); deleteProperty(TemperatureCompensateSP.name); deleteProperty(TemperatureCompensateOnStartSP.name); deleteProperty(BacklashCompensationSP.name); deleteProperty(BacklashNP.name); deleteProperty(MaxTravelNP.name); deleteProperty(ResetSP.name); deleteProperty(GotoSP.name); deleteProperty(ReverseSP.name); deleteProperty(StatusLP.name); deleteProperty(HFocusNameTP.name); } return true; } /************************************************************************************ * * ***********************************************************************************/ bool FocusLynxBase::Handshake() { if (ack()) { LOG_INFO("FocusLynx is online. Getting focus parameters..."); int modelIndex = IUFindOnSwitchIndex(&ModelSP); setDeviceType(modelIndex); SetTimer(POLLMS); return true; } LOG_INFO("Error retreiving data from FocusLynx, please ensure FocusLynxBase controller is " "powered and the port is correct."); return false; } /************************************************************************************ * * ***********************************************************************************/ const char *FocusLynxBase::getDefaultName() { // Has to be overide by child instance return "FocusLynxBase"; } /************************************************************************************ * * ***********************************************************************************/ bool FocusLynxBase::ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) { LOGF_INFO("Device: %s", dev); if (dev != nullptr && strcmp(dev, getDeviceName()) == 0) { // Models if (strcmp(ModelSP.name, name) == 0) { IUUpdateSwitch(&ModelSP, states, names, n); ModelSP.s = IPS_OK; IDSetSwitch(&ModelSP, nullptr); if (isConnected()) LOG_INFO("Focuser model set. Please disconnect and reconnect now..."); else LOG_INFO("Focuser model set. Please connect now..."); const char *focusName = IUFindOnSwitch(&ModelSP)->label; // Check if we have absolute or relative focusers if (strstr(focusName, "TCF") || !strcmp(focusName, "FastFocus")) { LOG_DEBUG("Absolute focuser detected."); isAbsolute = true; } else { LOG_DEBUG("Relative focuser detected."); isAbsolute = false; } return true; } // Temperature Compensation if (strcmp(TemperatureCompensateSP.name, name) == 0) { int prevIndex = IUFindOnSwitchIndex(&TemperatureCompensateSP); IUUpdateSwitch(&TemperatureCompensateSP, states, names, n); if (setTemperatureCompensation(TemperatureCompensateS[0].s == ISS_ON)) { TemperatureCompensateSP.s = IPS_OK; } else { IUResetSwitch(&TemperatureCompensateSP); TemperatureCompensateSP.s = IPS_ALERT; TemperatureCompensateS[prevIndex].s = ISS_ON; } IDSetSwitch(&TemperatureCompensateSP, nullptr); return true; } // Temperature Compensation on Start if (!strcmp(TemperatureCompensateOnStartSP.name, name)) { int prevIndex = IUFindOnSwitchIndex(&TemperatureCompensateOnStartSP); IUUpdateSwitch(&TemperatureCompensateOnStartSP, states, names, n); if (setTemperatureCompensationOnStart(TemperatureCompensateOnStartS[0].s == ISS_ON)) { TemperatureCompensateOnStartSP.s = IPS_OK; } else { IUResetSwitch(&TemperatureCompensateOnStartSP); TemperatureCompensateOnStartSP.s = IPS_ALERT; TemperatureCompensateOnStartS[prevIndex].s = ISS_ON; } IDSetSwitch(&TemperatureCompensateOnStartSP, nullptr); return true; } // Temperature Compensation Mode if (!strcmp(TemperatureCompensateModeSP.name, name)) { int prevIndex = IUFindOnSwitchIndex(&TemperatureCompensateModeSP); IUUpdateSwitch(&TemperatureCompensateModeSP, states, names, n); char mode = IUFindOnSwitchIndex(&TemperatureCompensateModeSP) + 'A'; if (setTemperatureCompensationMode(mode)) { TemperatureCompensateModeSP.s = IPS_OK; } else { IUResetSwitch(&TemperatureCompensateModeSP); TemperatureCompensateModeSP.s = IPS_ALERT; TemperatureCompensateModeS[prevIndex].s = ISS_ON; } IDSetSwitch(&TemperatureCompensateModeSP, nullptr); return true; } // Backlash enable/disable if (!strcmp(BacklashCompensationSP.name, name)) { int prevIndex = IUFindOnSwitchIndex(&BacklashCompensationSP); IUUpdateSwitch(&BacklashCompensationSP, states, names, n); if (setBacklashCompensation(BacklashCompensationS[0].s == ISS_ON)) { BacklashCompensationSP.s = IPS_OK; } else { IUResetSwitch(&BacklashCompensationSP); BacklashCompensationSP.s = IPS_ALERT; BacklashCompensationS[prevIndex].s = ISS_ON; } IDSetSwitch(&BacklashCompensationSP, nullptr); return true; } // Reset to Factory setting if (strcmp(ResetSP.name, name) == 0) { IUResetSwitch(&ResetSP); if (resetFactory()) ResetSP.s = IPS_OK; else ResetSP.s = IPS_ALERT; IDSetSwitch(&ResetSP, nullptr); return true; } // Go to home/center if (!strcmp(GotoSP.name, name)) { IUUpdateSwitch(&GotoSP, states, names, n); if (GotoS[GOTO_HOME].s == ISS_ON) { if (home()) GotoSP.s = IPS_BUSY; else GotoSP.s = IPS_ALERT; } else { if (center()) GotoSP.s = IPS_BUSY; else GotoSP.s = IPS_ALERT; } IDSetSwitch(&GotoSP, nullptr); return true; } // Reverse Direction if (!strcmp(ReverseSP.name, name)) { IUUpdateSwitch(&ReverseSP, states, names, n); if (reverse(ReverseS[0].s == ISS_ON)) ReverseSP.s = IPS_OK; else ReverseSP.s = IPS_ALERT; IDSetSwitch(&ReverseSP, nullptr); return true; } } return INDI::Focuser::ISNewSwitch(dev, name, states, names, n); } /************************************************************************************ * * ***********************************************************************************/ bool FocusLynxBase::ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n) { if (dev != nullptr && strcmp(dev, getDeviceName()) == 0) { // Set device nickname to the HUB itself if (!strcmp(name, HFocusNameTP.name)) { IUUpdateText(&HFocusNameTP, texts, names, n); if (setDeviceNickname(HFocusNameT[0].text)) HFocusNameTP.s = IPS_OK; else HFocusNameTP.s = IPS_ALERT; IDSetText(&HFocusNameTP, nullptr); return true; } } return INDI::Focuser::ISNewText(dev, name, texts, names, n); } /************************************************************************************ * * ***********************************************************************************/ bool FocusLynxBase::ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) { if (dev != nullptr && strcmp(dev, getDeviceName()) == 0) { // Temperature Coefficient if (!strcmp(TemperatureCoeffNP.name, name)) { IUUpdateNumber(&TemperatureCoeffNP, values, names, n); for (int i = 0; i < n; i++) { if (setTemperatureCompensationCoeff('A' + i, TemperatureCoeffN[i].value) == false) { LOG_ERROR("Failed to set temperature coefficeints."); TemperatureCoeffNP.s = IPS_ALERT; IDSetNumber(&TemperatureCoeffNP, nullptr); return false; } } TemperatureCoeffNP.s = IPS_OK; IDSetNumber(&TemperatureCoeffNP, nullptr); return true; } // Backlash Value if (!strcmp(BacklashNP.name, name)) { IUUpdateNumber(&BacklashNP, values, names, n); if (setBacklashCompensationSteps(BacklashN[0].value) == false) { LOG_ERROR("Failed to set temperature coefficients."); BacklashNP.s = IPS_ALERT; IDSetNumber(&BacklashNP, nullptr); return false; } BacklashNP.s = IPS_OK; IDSetNumber(&BacklashNP, nullptr); return true; } // Sync if (strcmp(SyncNP.name, name) == 0) { IUUpdateNumber(&SyncNP, values, names, n); if (sync(SyncN[0].value) == false) SyncNP.s = IPS_ALERT; else SyncNP.s = IPS_OK; IDSetNumber(&SyncNP, nullptr); return true; } // Max Travel if relative focusers if (!strcmp(MaxTravelNP.name, name)) // Max Travel if (strcmp(MaxTravelNP.name, name) == 0) { IUUpdateNumber(&MaxTravelNP, values, names, n); if (MaxTravelN[0].value > 0) { // If reverse is enabled. if (ReverseS[0].s == ISS_ON) { FocusAbsPosN[0].min = SyncN[0].min = (maxControllerTicks - MaxTravelN[0].value); FocusAbsPosN[0].max = SyncN[0].max = maxControllerTicks; FocusAbsPosN[0].step = SyncN[0].step = maxControllerTicks / 50.0; } // If reverse is disabled else { FocusAbsPosN[0].min = SyncN[0].min = 0; FocusAbsPosN[0].max = SyncN[0].max = MaxTravelN[0].value; FocusAbsPosN[0].step = SyncN[0].step = MaxTravelN[0].value / 50.0; } FocusRelPosN[0].max = (FocusAbsPosN[0].max - FocusAbsPosN[0].min) / 2; FocusRelPosN[0].step = FocusRelPosN[0].max / 100.0; FocusRelPosN[0].min = 0; IUUpdateMinMax(&FocusAbsPosNP); IUUpdateMinMax(&FocusRelPosNP); IUUpdateMinMax(&SyncNP); LOGF_INFO("Focuser absolute limits: min (%g) max (%g)", FocusAbsPosN[0].min, FocusAbsPosN[0].max); } MaxTravelNP.s = IPS_OK; IDSetNumber(&MaxTravelNP, nullptr); return true; } // Set LED intensity to the HUB itself via function setLedLevel() if (!strcmp(LedNP.name, name)) { IUUpdateNumber(&LedNP, values, names, n); if (setLedLevel(LedN[0].value)) LedNP.s = IPS_OK; else LedNP.s = IPS_ALERT; LOGF_INFO("Focuser LED level intensity : %f", LedN[0].value); IDSetNumber(&LedNP, nullptr); return true; } } return INDI::Focuser::ISNewNumber(dev, name, values, names, n); } /************************************************************************************ * * ***********************************************************************************/ bool FocusLynxBase::ack() { char cmd[32]; int errcode = 0; char errmsg[MAXRBUF]; char response[32]; int nbytes_read = 0; int nbytes_written = 0; memset(response, 0, sizeof(response)); snprintf(cmd, 32, "<%sHELLO>", getFocusTarget()); LOGF_DEBUG("CMD (%s)", cmd); if (isSimulation()) { const char *focusName = IUFindOnSwitch(&ModelSP)->label; strncpy(response, focusName, 32); response[31] = '\0'; nbytes_read = strlen(response) + 1; } else { if ((errcode = tty_write(PortFD, cmd, strlen(cmd), &nbytes_written)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); LOGF_ERROR("%s", errmsg); return false; } if (isResponseOK() == false) return false; if ((errcode = tty_read_section(PortFD, response, 0xA, LYNXFOCUS_TIMEOUT, &nbytes_read)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); LOGF_ERROR("%s", errmsg); return false; } } if (nbytes_read > 0) { response[nbytes_read - 1] = '\0'; LOGF_DEBUG("RES (%s)", response); LOGF_INFO("%s is detected.", response); return true; } return false; } /************************************************************************************ * * ***********************************************************************************/ bool FocusLynxBase::getFocusConfig() { char cmd[32]; int errcode = 0; char errmsg[MAXRBUF]; char response[32]; int nbytes_read = 0; int nbytes_written = 0; char key[16]; memset(response, 0, sizeof(response)); snprintf(cmd, 32, "<%sGETCONFIG>", getFocusTarget()); LOGF_DEBUG("CMD (%s)", cmd); if (isSimulation()) { if (!strcmp(getFocusTarget(), "F1")) strncpy(response, "CONFIG1", 16); else strncpy(response, "CONFIG2", 16); nbytes_read = strlen(response) + 1; } else { if ((errcode = tty_write(PortFD, cmd, strlen(cmd), &nbytes_written)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); LOGF_ERROR("%s", errmsg); return false; } if (isResponseOK() == false) return false; if ((errcode = tty_read_section(PortFD, response, 0xA, LYNXFOCUS_TIMEOUT, &nbytes_read)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); LOGF_ERROR("%s", errmsg); return false; } } if (nbytes_read > 0) { response[nbytes_read - 1] = '\0'; LOGF_DEBUG("RES (%s)", response); if ((strcmp(response, "CONFIG1")) && (strcmp(response, "CONFIG2"))) return false; } memset(response, 0, sizeof(response)); // Nickname if (isSimulation()) { snprintf(response, sizeof(response), "NickName=Focuser#%s\n", getFocusTarget()); nbytes_read = strlen(response); } else if ((errcode = tty_read_section(PortFD, response, 0xA, LYNXFOCUS_TIMEOUT, &nbytes_read)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); LOGF_ERROR("%s", errmsg); return false; } response[nbytes_read - 1] = '\0'; LOGF_DEBUG("RES (%s)", response); char nickname[16]; int rc = sscanf(response, "%16[^=]=%16[^\n]s", key, nickname); if (rc != 2) return false; IUSaveText(&HFocusNameT[0], nickname); IDSetText(&HFocusNameTP, nullptr); HFocusNameTP.s = IPS_OK; IDSetText(&HFocusNameTP, nullptr); memset(response, 0, sizeof(response)); // Get Max Position if (isSimulation()) { if (isAbsolute == false) // Value with high limit to give freedom to user of emulation range snprintf(response, 32, "Max Pos = %06d\n", 100000); else // Value from the TCF-S absolute focuser snprintf(response, 32, "Max Pos = %06d\n", 7000); nbytes_read = strlen(response); } else if ((errcode = tty_read_section(PortFD, response, 0xA, LYNXFOCUS_TIMEOUT, &nbytes_read)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); LOGF_ERROR("%s", errmsg); return false; } response[nbytes_read - 1] = '\0'; LOGF_DEBUG("RES (%s)", response); uint32_t maxPos = 0; rc = sscanf(response, "%16[^=]=%d", key, &maxPos); if (rc == 2) { FocusAbsPosN[0].max = SyncN[0].max = maxPos; FocusAbsPosN[0].step = SyncN[0].step = maxPos / 50.0; FocusAbsPosN[0].min = SyncN[0].min = 0; FocusRelPosN[0].max = maxPos / 2; FocusRelPosN[0].step = maxPos / 100.0; FocusRelPosN[0].min = 0; IUUpdateMinMax(&FocusAbsPosNP); IUUpdateMinMax(&FocusRelPosNP); IUUpdateMinMax(&SyncNP); maxControllerTicks = maxPos; // if it is relative focuser and the backup have a value, MaxTravNP[0].value // will be overide by the backup restore call MaxTravelNP.s = IPS_OK; MaxTravelN[0].value = maxPos; IDSetNumber(&MaxTravelNP, NULL); } else return false; memset(response, 0, sizeof(response)); // Get Device Type if (isSimulation()) { snprintf(response, 32, "Dev Typ = %s\n", "OA"); nbytes_read = strlen(response); } else if ((errcode = tty_read_section(PortFD, response, 0xA, LYNXFOCUS_TIMEOUT, &nbytes_read)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); LOGF_ERROR("%s", errmsg); return false; } response[nbytes_read - 1] = '\0'; LOGF_DEBUG("RES (%s)", response); // Get Status Parameters memset(response, 0, sizeof(response)); // Temperature Compensation On? if (isSimulation()) { snprintf(response, 32, "TComp ON = %d\n", TemperatureCompensateS[0].s == ISS_ON ? 1 : 0); nbytes_read = strlen(response); } else if ((errcode = tty_read_section(PortFD, response, 0xA, LYNXFOCUS_TIMEOUT, &nbytes_read)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); LOGF_ERROR("%s", errmsg); return false; } response[nbytes_read - 1] = '\0'; LOGF_DEBUG("RES (%s)", response); int TCompOn; rc = sscanf(response, "%16[^=]=%d", key, &TCompOn); if (rc != 2) return false; IUResetSwitch(&TemperatureCompensateSP); TemperatureCompensateS[0].s = TCompOn ? ISS_ON : ISS_OFF; TemperatureCompensateS[1].s = TCompOn ? ISS_OFF : ISS_ON; TemperatureCompensateSP.s = IPS_OK; IDSetSwitch(&TemperatureCompensateSP, nullptr); memset(response, 0, sizeof(response)); // Temperature Coeff A if (isSimulation()) { snprintf(response, 32, "TempCo A = %d\n", (int)TemperatureCoeffN[FOCUS_A_COEFF].value); nbytes_read = strlen(response); } else if ((errcode = tty_read_section(PortFD, response, 0xA, LYNXFOCUS_TIMEOUT, &nbytes_read)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); LOGF_ERROR("%s", errmsg); return false; } response[nbytes_read - 1] = '\0'; LOGF_DEBUG("RES (%s)", response); int TCoeffA; rc = sscanf(response, "%16[^=]=%d", key, &TCoeffA); if (rc != 2) return false; TemperatureCoeffN[FOCUS_A_COEFF].value = TCoeffA; memset(response, 0, sizeof(response)); // Temperature Coeff B if (isSimulation()) { snprintf(response, 32, "TempCo B = %d\n", (int)TemperatureCoeffN[FOCUS_B_COEFF].value); nbytes_read = strlen(response); } else if ((errcode = tty_read_section(PortFD, response, 0xA, LYNXFOCUS_TIMEOUT, &nbytes_read)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); LOGF_ERROR("%s", errmsg); return false; } response[nbytes_read - 1] = '\0'; LOGF_DEBUG("RES (%s)", response); int TCoeffB; rc = sscanf(response, "%16[^=]=%d", key, &TCoeffB); if (rc != 2) return false; TemperatureCoeffN[FOCUS_B_COEFF].value = TCoeffB; memset(response, 0, sizeof(response)); // Temperature Coeff C if (isSimulation()) { snprintf(response, 32, "TempCo C = %d\n", (int)TemperatureCoeffN[FOCUS_C_COEFF].value); nbytes_read = strlen(response); } else if ((errcode = tty_read_section(PortFD, response, 0xA, LYNXFOCUS_TIMEOUT, &nbytes_read)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); LOGF_ERROR("%s", errmsg); return false; } response[nbytes_read - 1] = '\0'; LOGF_DEBUG("RES (%s)", response); int TCoeffC; rc = sscanf(response, "%16[^=]=%d", key, &TCoeffC); if (rc != 2) return false; TemperatureCoeffN[FOCUS_C_COEFF].value = TCoeffC; memset(response, 0, sizeof(response)); // Temperature Coeff D if (isSimulation()) { snprintf(response, 32, "TempCo D = %d\n", (int)TemperatureCoeffN[FOCUS_D_COEFF].value); nbytes_read = strlen(response); } else if ((errcode = tty_read_section(PortFD, response, 0xA, LYNXFOCUS_TIMEOUT, &nbytes_read)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); LOGF_ERROR("%s", errmsg); return false; } response[nbytes_read - 1] = '\0'; LOGF_DEBUG("RES (%s)", response); int TCoeffD; rc = sscanf(response, "%16[^=]=%d", key, &TCoeffD); if (rc != 2) return false; TemperatureCoeffN[FOCUS_D_COEFF].value = TCoeffD; memset(response, 0, sizeof(response)); // Temperature Coeff E if (isSimulation()) { snprintf(response, 32, "TempCo E = %d\n", (int)TemperatureCoeffN[FOCUS_E_COEFF].value); nbytes_read = strlen(response); } else if ((errcode = tty_read_section(PortFD, response, 0xA, LYNXFOCUS_TIMEOUT, &nbytes_read)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); LOGF_ERROR("%s", errmsg); return false; } response[nbytes_read - 1] = '\0'; LOGF_DEBUG("RES (%s)", response); int TCoeffE; rc = sscanf(response, "%16[^=]=%d", key, &TCoeffE); if (rc != 2) return false; TemperatureCoeffN[FOCUS_E_COEFF].value = TCoeffE; TemperatureCoeffNP.s = IPS_OK; IDSetNumber(&TemperatureCoeffNP, nullptr); memset(response, 0, sizeof(response)); // Temperature Compensation Mode if (isSimulation()) { snprintf(response, 32, "TC Mode = %c\n", 'C'); nbytes_read = strlen(response); } else if ((errcode = tty_read_section(PortFD, response, 0xA, LYNXFOCUS_TIMEOUT, &nbytes_read)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); LOGF_ERROR("%s", errmsg); return false; } response[nbytes_read - 1] = '\0'; LOGF_DEBUG("RES (%s)", response); char compensateMode; rc = sscanf(response, "%16[^=]= %c", key, &compensateMode); if (rc != 2) return false; IUResetSwitch(&TemperatureCompensateModeSP); int index = compensateMode - 'A'; if (index >= 0 && index <= 5) { TemperatureCompensateModeS[index].s = ISS_ON; TemperatureCompensateModeSP.s = IPS_OK; } else { LOGF_ERROR("Invalid index %d for compensation mode.", index); TemperatureCompensateModeSP.s = IPS_ALERT; } IDSetSwitch(&TemperatureCompensateModeSP, nullptr); // Backlash Compensation memset(response, 0, sizeof(response)); if (isSimulation()) { snprintf(response, 32, "BLC En = %d\n", BacklashCompensationS[0].s == ISS_ON ? 1 : 0); nbytes_read = strlen(response); } else if ((errcode = tty_read_section(PortFD, response, 0xA, LYNXFOCUS_TIMEOUT, &nbytes_read)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); LOGF_ERROR("%s", errmsg); return false; } response[nbytes_read - 1] = '\0'; LOGF_DEBUG("RES (%s)", response); int BLCCompensate; rc = sscanf(response, "%16[^=]=%d", key, &BLCCompensate); if (rc != 2) return false; IUResetSwitch(&BacklashCompensationSP); BacklashCompensationS[0].s = BLCCompensate ? ISS_ON : ISS_OFF; BacklashCompensationS[1].s = BLCCompensate ? ISS_OFF : ISS_ON; BacklashCompensationSP.s = IPS_OK; IDSetSwitch(&BacklashCompensationSP, nullptr); // Backlash Value memset(response, 0, sizeof(response)); if (isSimulation()) { snprintf(response, 32, "BLC Stps = %d\n", 50); nbytes_read = strlen(response); } else if ((errcode = tty_read_section(PortFD, response, 0xA, LYNXFOCUS_TIMEOUT, &nbytes_read)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); LOGF_ERROR("%s", errmsg); return false; } response[nbytes_read - 1] = '\0'; LOGF_DEBUG("RES (%s)", response); int BLCValue; rc = sscanf(response, "%16[^=]=%d", key, &BLCValue); if (rc != 2) return false; BacklashN[0].value = BLCValue; BacklashNP.s = IPS_OK; IDSetNumber(&BacklashNP, nullptr); // Led brightnesss memset(response, 0, sizeof(response)); if (isSimulation()) { snprintf(response, 32, "LED Brt = %d\n", 75); nbytes_read = strlen(response); } else if ((errcode = tty_read_section(PortFD, response, 0xA, LYNXFOCUS_TIMEOUT, &nbytes_read)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); LOGF_ERROR("%s", errmsg); return false; } response[nbytes_read - 1] = '\0'; LOGF_DEBUG("RES (%s)", response); int LEDBrightness; rc = sscanf(response, "%16[^=]=%d", key, &LEDBrightness); if (rc != 2) return false; LedN[0].value = LEDBrightness; LedNP.s = IPS_OK; IDSetNumber(&LedNP, nullptr); // Temperature Compensation on Start memset(response, 0, sizeof(response)); if (isSimulation()) { snprintf(response, 32, "TC@Start = %d\n", TemperatureCompensateOnStartS[0].s == ISS_ON ? 1 : 0); nbytes_read = strlen(response); } else if ((errcode = tty_read_section(PortFD, response, 0xA, LYNXFOCUS_TIMEOUT, &nbytes_read)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); LOGF_ERROR("%s", errmsg); return false; } response[nbytes_read - 1] = '\0'; LOGF_DEBUG("RES (%s)", response); int TCOnStart; rc = sscanf(response, "%16[^=]=%d", key, &TCOnStart); if (rc != 2) return false; IUResetSwitch(&TemperatureCompensateOnStartSP); TemperatureCompensateOnStartS[0].s = TCOnStart ? ISS_ON : ISS_OFF; TemperatureCompensateOnStartS[1].s = TCOnStart ? ISS_OFF : ISS_ON; TemperatureCompensateOnStartSP.s = IPS_OK; IDSetSwitch(&TemperatureCompensateOnStartSP, nullptr); // END is reached memset(response, 0, sizeof(response)); if (isSimulation()) { strncpy(response, "END\n", 16); nbytes_read = strlen(response); } else if ((errcode = tty_read_section(PortFD, response, 0xA, LYNXFOCUS_TIMEOUT, &nbytes_read)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); LOGF_ERROR("%s", errmsg); return false; } if (nbytes_read > 0) { response[nbytes_read - 1] = '\0'; // Display the response to be sure to have read the complet TTY Buffer. LOGF_DEBUG("RES (%s)", response); if (strcmp(response, "END")) return false; } tcflush(PortFD, TCIFLUSH); configurationComplete = true; return true; } /************************************************************************************ * * ***********************************************************************************/ bool FocusLynxBase::getFocusStatus() { char cmd[32]; int errcode = 0; char errmsg[MAXRBUF]; char response[32]; int nbytes_read = 0; int nbytes_written = 0; char key[16]; memset(response, 0, sizeof(response)); snprintf(cmd, 32, "<%sGETSTATUS>", getFocusTarget()); LOGF_DEBUG("CMD (%s)", cmd); if (isSimulation()) { if (!strcmp(getFocusTarget(), "F1")) strncpy(response, "STATUS1", 16); else strncpy(response, "STATUS2", 16); nbytes_read = strlen(response) + 1; } else { //tcflush(PortFD, TCIFLUSH); if ((errcode = tty_write(PortFD, cmd, strlen(cmd), &nbytes_written)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); LOGF_ERROR("%s", errmsg); return false; } if (isResponseOK() == false) return false; if ((errcode = tty_read_section(PortFD, response, 0xA, LYNXFOCUS_TIMEOUT, &nbytes_read)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); LOGF_ERROR("%s", errmsg); return false; } } if (nbytes_read > 0) { response[nbytes_read - 1] = '\0'; DEBUGF(DBG_FOCUS, "RES (%s)", response); if ((strcmp(response, "STATUS1")) && (strcmp(response, "STATUS2"))) return false; // Get Temperature memset(response, 0, sizeof(response)); if (isSimulation()) { strncpy(response, "Temp(C) = +21.7\n", 16); nbytes_read = strlen(response); } else if ((errcode = tty_read_section(PortFD, response, 0xA, LYNXFOCUS_TIMEOUT, &nbytes_read)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); LOGF_ERROR("%s", errmsg); return false; } response[nbytes_read - 1] = '\0'; DEBUGF(DBG_FOCUS, "RES (%s)", response); float temperature = 0; int rc = sscanf(response, "%16[^=]=%f", key, &temperature); if (rc == 2) { TemperatureN[0].value = temperature; IDSetNumber(&TemperatureNP, nullptr); } else { char np[8]; int rc = sscanf(response, "%16[^=]= %s", key, np); if (rc != 2 || strcmp(np, "NP")) { if (TemperatureNP.s != IPS_ALERT) { TemperatureNP.s = IPS_ALERT; IDSetNumber(&TemperatureNP, nullptr); } return false; } } // Get Current Position memset(response, 0, sizeof(response)); if (isSimulation()) { snprintf(response, 32, "Curr Pos = %06d\n", simPosition); nbytes_read = strlen(response); } else if ((errcode = tty_read_section(PortFD, response, 0xA, LYNXFOCUS_TIMEOUT, &nbytes_read)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); LOGF_ERROR("%s", errmsg); return false; } response[nbytes_read - 1] = '\0'; DEBUGF(DBG_FOCUS, "RES (%s)", response); uint32_t currPos = 0; rc = sscanf(response, "%16[^=]=%d", key, &currPos); if (rc == 2) { FocusAbsPosN[0].value = currPos; IDSetNumber(&FocusAbsPosNP, nullptr); } else return false; // Get Target Position memset(response, 0, sizeof(response)); if (isSimulation()) { snprintf(response, 32, "Targ Pos = %06d\n", targetPosition); nbytes_read = strlen(response); } else if ((errcode = tty_read_section(PortFD, response, 0xA, LYNXFOCUS_TIMEOUT, &nbytes_read)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); LOGF_ERROR("%s", errmsg); return false; } response[nbytes_read - 1] = '\0'; DEBUGF(DBG_FOCUS, "RES (%s)", response); // Get Status Parameters // #1 is Moving? memset(response, 0, sizeof(response)); if (isSimulation()) { snprintf(response, 32, "Is Moving = %d\n", (simStatus[STATUS_MOVING] == ISS_ON) ? 1 : 0); nbytes_read = strlen(response); } else if ((errcode = tty_read_section(PortFD, response, 0xA, LYNXFOCUS_TIMEOUT, &nbytes_read)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); LOGF_ERROR("%s", errmsg); return false; } response[nbytes_read - 1] = '\0'; DEBUGF(DBG_FOCUS, "RES (%s)", response); int isMoving; rc = sscanf(response, "%16[^=]=%d", key, &isMoving); if (rc != 2) return false; StatusL[STATUS_MOVING].s = isMoving ? IPS_BUSY : IPS_IDLE; // #2 is Homing? memset(response, 0, sizeof(response)); if (isSimulation()) { snprintf(response, 32, "Is Homing = %d\n", (simStatus[STATUS_HOMING] == ISS_ON) ? 1 : 0); nbytes_read = strlen(response); } else if ((errcode = tty_read_section(PortFD, response, 0xA, LYNXFOCUS_TIMEOUT, &nbytes_read)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); LOGF_ERROR("%s", errmsg); return false; } response[nbytes_read - 1] = '\0'; DEBUGF(DBG_FOCUS, "RES (%s)", response); int _isHoming; rc = sscanf(response, "%16[^=]=%d", key, &_isHoming); if (rc != 2) return false; StatusL[STATUS_HOMING].s = _isHoming ? IPS_BUSY : IPS_IDLE; // For relative focusers home is not applicable. if (isAbsolute == false) StatusL[STATUS_HOMING].s = IPS_IDLE; // We set that isHoming in process, but we don't set it to false here it must be reset in TimerHit if (StatusL[STATUS_HOMING].s == IPS_BUSY) isHoming = true; // #3 is Homed? memset(response, 0, sizeof(response)); if (isSimulation()) { snprintf(response, 32, "Is Homed = %d\n", (simStatus[STATUS_HOMED] == ISS_ON) ? 1 : 0); nbytes_read = strlen(response); } else if ((errcode = tty_read_section(PortFD, response, 0xA, LYNXFOCUS_TIMEOUT, &nbytes_read)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); LOGF_ERROR("%s", errmsg); return false; } response[nbytes_read - 1] = '\0'; DEBUGF(DBG_FOCUS, "RES (%s)", response); int isHomed; rc = sscanf(response, "%16[^=]=%d", key, &isHomed); if (rc != 2) return false; StatusL[STATUS_HOMED].s = isHomed ? IPS_OK : IPS_IDLE; // For relative focusers home is not applicable. if (isAbsolute == false) StatusL[STATUS_HOMED].s = IPS_IDLE; // #4 FF Detected? memset(response, 0, sizeof(response)); if (isSimulation()) { snprintf(response, 32, "FFDetect = %d\n", (simStatus[STATUS_FFDETECT] == ISS_ON) ? 1 : 0); nbytes_read = strlen(response); } else if ((errcode = tty_read_section(PortFD, response, 0xA, LYNXFOCUS_TIMEOUT, &nbytes_read)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); LOGF_ERROR("%s", errmsg); return false; } response[nbytes_read - 1] = '\0'; DEBUGF(DBG_FOCUS, "RES (%s)", response); int FFDetect; rc = sscanf(response, "%16[^=]=%d", key, &FFDetect); if (rc != 2) return false; StatusL[STATUS_FFDETECT].s = FFDetect ? IPS_OK : IPS_IDLE; // #5 Temperature probe? memset(response, 0, sizeof(response)); if (isSimulation()) { snprintf(response, 32, "TmpProbe = %d\n", (simStatus[STATUS_TMPPROBE] == ISS_ON) ? 1 : 0); nbytes_read = strlen(response); } else if ((errcode = tty_read_section(PortFD, response, 0xA, LYNXFOCUS_TIMEOUT, &nbytes_read)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); LOGF_ERROR("%s", errmsg); return false; } response[nbytes_read - 1] = '\0'; DEBUGF(DBG_FOCUS, "RES (%s)", response); int TmpProbe; rc = sscanf(response, "%16[^=]=%d", key, &TmpProbe); if (rc != 2) return false; StatusL[STATUS_TMPPROBE].s = TmpProbe ? IPS_OK : IPS_IDLE; // #6 Remote IO? memset(response, 0, sizeof(response)); if (isSimulation()) { snprintf(response, 32, "RemoteIO = %d\n", (simStatus[STATUS_REMOTEIO] == ISS_ON) ? 1 : 0); nbytes_read = strlen(response); } else if ((errcode = tty_read_section(PortFD, response, 0xA, LYNXFOCUS_TIMEOUT, &nbytes_read)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); LOGF_ERROR("%s", errmsg); return false; } response[nbytes_read - 1] = '\0'; DEBUGF(DBG_FOCUS, "RES (%s)", response); int RemoteIO; rc = sscanf(response, "%16[^=]=%d", key, &RemoteIO); if (rc != 2) return false; StatusL[STATUS_REMOTEIO].s = RemoteIO ? IPS_OK : IPS_IDLE; // #7 Hand controller? memset(response, 0, sizeof(response)); if (isSimulation()) { snprintf(response, 32, "Hnd Ctlr = %d\n", (simStatus[STATUS_HNDCTRL] == ISS_ON) ? 1 : 0); nbytes_read = strlen(response); } else if ((errcode = tty_read_section(PortFD, response, 0xA, LYNXFOCUS_TIMEOUT, &nbytes_read)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); LOGF_ERROR("%s", errmsg); return false; } response[nbytes_read - 1] = '\0'; DEBUGF(DBG_FOCUS, "RES (%s)", response); int HndCtlr; rc = sscanf(response, "%16[^=]=%d", key, &HndCtlr); if (rc != 2) return false; StatusL[STATUS_HNDCTRL].s = HndCtlr ? IPS_OK : IPS_IDLE; // #8 Reverse? memset(response, 0, sizeof(response)); if (isSimulation()) { snprintf(response, 32, "Reverse = %d\n", (simStatus[STATUS_REVERSE] == ISS_ON) ? 1 : 0); nbytes_read = strlen(response); } else if ((errcode = tty_read_section(PortFD, response, 0xA, LYNXFOCUS_TIMEOUT, &nbytes_read)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); LOGF_ERROR("%s", errmsg); return false; } response[nbytes_read - 1] = '\0'; DEBUGF(DBG_FOCUS, "RES (%s)", response); int reverse; rc = sscanf(response, "%16[^=]=%d", key, &reverse); if (rc != 2) return false; StatusL[STATUS_REVERSE].s = reverse ? IPS_OK : IPS_IDLE; // If reverse is enable and switch shows disabled, let's change that // same thing is reverse is disabled but switch is enabled if ((reverse && ReverseS[1].s == ISS_ON) || (!reverse && ReverseS[0].s == ISS_ON)) { IUResetSwitch(&ReverseSP); ReverseS[0].s = (reverse == 1) ? ISS_ON : ISS_OFF; ReverseS[1].s = (reverse == 0) ? ISS_ON : ISS_OFF; IDSetSwitch(&ReverseSP, nullptr); } StatusLP.s = IPS_OK; IDSetLight(&StatusLP, nullptr); // END is reached memset(response, 0, sizeof(response)); if (isSimulation()) { strncpy(response, "END\n", 16); nbytes_read = strlen(response); } else if ((errcode = tty_read_section(PortFD, response, 0xA, LYNXFOCUS_TIMEOUT, &nbytes_read)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); LOGF_ERROR("%s", errmsg); return false; } if (nbytes_read > 0) { response[nbytes_read - 1] = '\0'; // Display the response to be sure to have read the complet TTY Buffer. LOGF_DEBUG("RES (%s)", response); if (strcmp(response, "END")) return false; } tcflush(PortFD, TCIFLUSH); return true; } return false; } /************************************************************************************ * * ***********************************************************************************/ bool FocusLynxBase::setDeviceType(int index) { char cmd[16]; int errcode = 0; char errmsg[MAXRBUF]; char response[16]; int nbytes_read = 0; int nbytes_written = 0; memset(response, 0, sizeof(response)); snprintf(cmd, 16, "<%sSCDT%s>", getFocusTarget(), index > 0 ? lynxModels[ModelS[index].name].c_str() : "ZZ"); LOGF_DEBUG("CMD (%s)", cmd); if (isSimulation()) { strncpy(response, "SET", 16); nbytes_read = strlen(response) + 1; } else { tcflush(PortFD, TCIFLUSH); if ((errcode = tty_write(PortFD, cmd, strlen(cmd), &nbytes_written)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); LOGF_ERROR("%s", errmsg); return false; } if (isResponseOK() == false) return false; if ((errcode = tty_read_section(PortFD, response, 0xA, LYNXFOCUS_TIMEOUT, &nbytes_read)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); LOGF_ERROR("%s", errmsg); return false; } } if (nbytes_read > 0) { response[nbytes_read - 1] = '\0'; LOGF_DEBUG("RES (%s)", response); tcflush(PortFD, TCIFLUSH); if (!strcmp(response, "SET")) return true; else return false; } return false; } /************************************************************************************ * * ***********************************************************************************/ bool FocusLynxBase::setLedLevel(int level) // Write via the connected port to the HUB the selected LED intensity level { char cmd[16]; int errcode = 0; char errmsg[MAXRBUF]; char response[16]; int nbytes_read = 0; int nbytes_written = 0; memset(response, 0, sizeof(response)); snprintf(cmd, 16, "", level); LOGF_DEBUG("CMD (%s)", cmd); if (isSimulation()) { strncpy(response, "SET", 16); nbytes_read = strlen(response) + 1; } else { tcflush(PortFD, TCIFLUSH); if ((errcode = tty_write(PortFD, cmd, strlen(cmd), &nbytes_written)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); LOGF_ERROR("%s", errmsg); return false; } if (isResponseOK() == false) return false; if ((errcode = tty_read_section(PortFD, response, 0xA, LYNXFOCUS_TIMEOUT, &nbytes_read)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); LOGF_ERROR("%s", errmsg); return false; } } if (nbytes_read > 0) { response[nbytes_read - 1] = '\0'; LOGF_DEBUG("RES (%s)", response); tcflush(PortFD, TCIFLUSH); if (!strcmp(response, "SET")) return true; else return false; } return false; } /************************************************************************************ * * ***********************************************************************************/ bool FocusLynxBase::setDeviceNickname(const char *nickname) // Write via the connected port to the HUB the choiced nikname of the focuser { char cmd[32]; int errcode = 0; char errmsg[MAXRBUF]; char response[16]; int nbytes_read = 0; int nbytes_written = 0; memset(response, 0, sizeof(response)); snprintf(cmd, 32, "<%sSCNN%s>", getFocusTarget(), nickname); LOGF_DEBUG("CMD (%s)", cmd); if (isSimulation()) { strncpy(response, "SET", 16); nbytes_read = strlen(response) + 1; } else { tcflush(PortFD, TCIFLUSH); if ((errcode = tty_write(PortFD, cmd, strlen(cmd), &nbytes_written)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); LOGF_ERROR("%s", errmsg); return false; } if (isResponseOK() == false) return false; if ((errcode = tty_read_section(PortFD, response, 0xA, LYNXFOCUS_TIMEOUT, &nbytes_read)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); LOGF_ERROR("%s", errmsg); return false; } } if (nbytes_read > 0) { response[nbytes_read - 1] = '\0'; LOGF_DEBUG("RES (%s)", response); tcflush(PortFD, TCIFLUSH); if (!strcmp(response, "SET")) return true; else return false; } return false; } /************************************************************************************ * * ***********************************************************************************/ bool FocusLynxBase::home() { char cmd[32]; int errcode = 0; char errmsg[MAXRBUF]; char response[16]; int nbytes_read = 0; int nbytes_written = 0; memset(response, 0, sizeof(response)); snprintf(cmd, 32, "<%sHOME>", getFocusTarget()); LOGF_DEBUG("CMD (%s)", cmd); if (isSimulation()) { strncpy(response, "H", 16); nbytes_read = strlen(response) + 1; targetPosition = 0; FocusAbsPosN[0].value = MaxTravelN[0].value; FocusAbsPosNP.s = IPS_OK; IDSetNumber(&FocusAbsPosNP, NULL); simStatus[STATUS_HOMING] = ISS_ON; simStatus[STATUS_HOMED] = ISS_OFF; simPosition = FocusAbsPosN[0].value; } else { tcflush(PortFD, TCIFLUSH); if ((errcode = tty_write(PortFD, cmd, strlen(cmd), &nbytes_written)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); LOGF_ERROR("%s", errmsg); return false; } if (isResponseOK() == false) return false; if ((errcode = tty_read_section(PortFD, response, 0xA, LYNXFOCUS_TIMEOUT, &nbytes_read)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); LOGF_ERROR("%s", errmsg); return false; } } if (nbytes_read > 0) { response[nbytes_read - 1] = '\0'; LOGF_DEBUG("RES (%s)", response); FocusAbsPosNP.s = IPS_BUSY; IDSetNumber(&FocusAbsPosNP, nullptr); isHoming = true; LOG_INFO("Focuser moving to home position..."); tcflush(PortFD, TCIFLUSH); return true; } return false; } /************************************************************************************ * * ***********************************************************************************/ bool FocusLynxBase::center() { char cmd[32]; int errcode = 0; char errmsg[MAXRBUF]; char response[16]; int nbytes_read = 0; int nbytes_written = 0; if (isAbsolute == false) return (MoveAbsFocuser(FocusAbsPosN[0].max / 2) != IPS_ALERT); memset(response, 0, sizeof(response)); snprintf(cmd, 32, "<%sCENTER>", getFocusTarget()); LOGF_DEBUG("CMD (%s)", cmd); if (isSimulation()) { strncpy(response, "M", 16); nbytes_read = strlen(response) + 1; simStatus[STATUS_MOVING] = ISS_ON; targetPosition = FocusAbsPosN[0].max / 2; } else { tcflush(PortFD, TCIFLUSH); if ((errcode = tty_write(PortFD, cmd, strlen(cmd), &nbytes_written)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); LOGF_ERROR("%s", errmsg); return false; } if (isResponseOK() == false) return false; if ((errcode = tty_read_section(PortFD, response, 0xA, LYNXFOCUS_TIMEOUT, &nbytes_read)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); LOGF_ERROR("%s", errmsg); return false; } } if (nbytes_read > 0) { response[nbytes_read - 1] = '\0'; LOGF_DEBUG("RES (%s)", response); LOG_INFO("Focuser moving to center position..."); FocusAbsPosNP.s = IPS_BUSY; IDSetNumber(&FocusAbsPosNP, nullptr); tcflush(PortFD, TCIFLUSH); return true; } return false; } /************************************************************************************ * * ***********************************************************************************/ bool FocusLynxBase::setTemperatureCompensation(bool enable) { char cmd[16]; int errcode = 0; char errmsg[MAXRBUF]; char response[16]; int nbytes_read = 0; int nbytes_written = 0; memset(response, 0, sizeof(response)); snprintf(cmd, 16, "<%sSCTE%d>", getFocusTarget(), enable ? 1 : 0); LOGF_DEBUG("CMD (%s)", cmd); if (isSimulation()) { strncpy(response, "SET", 16); nbytes_read = strlen(response) + 1; } else { tcflush(PortFD, TCIFLUSH); if ((errcode = tty_write(PortFD, cmd, strlen(cmd), &nbytes_written)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); LOGF_ERROR("%s", errmsg); return false; } if (isResponseOK() == false) return false; if ((errcode = tty_read_section(PortFD, response, 0xA, LYNXFOCUS_TIMEOUT, &nbytes_read)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); LOGF_ERROR("%s", errmsg); return false; } } if (nbytes_read > 0) { response[nbytes_read - 1] = '\0'; LOGF_DEBUG("RES (%s)", response); tcflush(PortFD, TCIFLUSH); if (!strcmp(response, "SET")) return true; else return false; } return false; } /************************************************************************************ * * ***********************************************************************************/ bool FocusLynxBase::setTemperatureCompensationMode(char mode) { char cmd[16]; int errcode = 0; char errmsg[MAXRBUF]; char response[16]; int nbytes_read = 0; int nbytes_written = 0; memset(response, 0, sizeof(response)); snprintf(cmd, 16, "<%sSCTM%c>", getFocusTarget(), mode); LOGF_DEBUG("CMD (%s)", cmd); if (isSimulation()) { strncpy(response, "SET", 16); nbytes_read = strlen(response) + 1; } else { tcflush(PortFD, TCIFLUSH); if ((errcode = tty_write(PortFD, cmd, strlen(cmd), &nbytes_written)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); LOGF_ERROR("%s", errmsg); return false; } if (isResponseOK() == false) return false; if ((errcode = tty_read_section(PortFD, response, 0xA, LYNXFOCUS_TIMEOUT, &nbytes_read)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); LOGF_ERROR("%s", errmsg); return false; } } if (nbytes_read > 0) { response[nbytes_read - 1] = '\0'; LOGF_DEBUG("RES (%s)", response); tcflush(PortFD, TCIFLUSH); if (!strcmp(response, "SET")) return true; else return false; } return false; } /************************************************************************************ * * ***********************************************************************************/ bool FocusLynxBase::setTemperatureCompensationCoeff(char mode, int16_t coeff) { char cmd[16]; int errcode = 0; char errmsg[MAXRBUF]; char response[16]; int nbytes_read = 0; int nbytes_written = 0; memset(response, 0, sizeof(response)); snprintf(cmd, 16, "<%sSCTC%c%c%04d>", getFocusTarget(), mode, coeff >= 0 ? '+' : '-', (int)std::abs(coeff)); LOGF_DEBUG("CMD (%s)", cmd); if (isSimulation()) { strncpy(response, "SET", 16); nbytes_read = strlen(response) + 1; } else { tcflush(PortFD, TCIFLUSH); if ((errcode = tty_write(PortFD, cmd, strlen(cmd), &nbytes_written)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); LOGF_ERROR("%s", errmsg); return false; } if (isResponseOK() == false) return false; if ((errcode = tty_read_section(PortFD, response, 0xA, LYNXFOCUS_TIMEOUT, &nbytes_read)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); LOGF_ERROR("%s", errmsg); return false; } } if (nbytes_read > 0) { response[nbytes_read - 1] = '\0'; LOGF_DEBUG("RES (%s)", response); tcflush(PortFD, TCIFLUSH); if (!strcmp(response, "SET")) return true; else return false; } return false; } /************************************************************************************ * * ***********************************************************************************/ bool FocusLynxBase::setTemperatureCompensationOnStart(bool enable) { char cmd[16]; int errcode = 0; char errmsg[MAXRBUF]; char response[16]; int nbytes_read = 0; int nbytes_written = 0; memset(response, 0, sizeof(response)); snprintf(cmd, 16, "<%sSCTS%d>", getFocusTarget(), enable ? 1 : 0); LOGF_DEBUG("CMD (%s)", cmd); if (isSimulation()) { strncpy(response, "SET", 16); nbytes_read = strlen(response) + 1; } else { tcflush(PortFD, TCIFLUSH); if ((errcode = tty_write(PortFD, cmd, strlen(cmd), &nbytes_written)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); LOGF_ERROR("%s", errmsg); return false; } if (isResponseOK() == false) return false; if ((errcode = tty_read_section(PortFD, response, 0xA, LYNXFOCUS_TIMEOUT, &nbytes_read)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); LOGF_ERROR("%s", errmsg); return false; } } if (nbytes_read > 0) { response[nbytes_read - 1] = '\0'; LOGF_DEBUG("RES (%s)", response); tcflush(PortFD, TCIFLUSH); if (!strcmp(response, "SET")) return true; else return false; } return false; } /************************************************************************************ * * ***********************************************************************************/ bool FocusLynxBase::setBacklashCompensation(bool enable) { char cmd[16]; int errcode = 0; char errmsg[MAXRBUF]; char response[16]; int nbytes_read = 0; int nbytes_written = 0; memset(response, 0, sizeof(response)); snprintf(cmd, 16, "<%sSCBE%d>", getFocusTarget(), enable ? 1 : 0); LOGF_DEBUG("CMD (%s)", cmd); if (isSimulation()) { strncpy(response, "SET", 16); nbytes_read = strlen(response) + 1; } else { if ((errcode = tty_write(PortFD, cmd, strlen(cmd), &nbytes_written)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); LOGF_ERROR("%s", errmsg); return false; } if (isResponseOK() == false) return false; if ((errcode = tty_read_section(PortFD, response, 0xA, LYNXFOCUS_TIMEOUT, &nbytes_read)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); LOGF_ERROR("%s", errmsg); return false; } } if (nbytes_read > 0) { response[nbytes_read - 1] = '\0'; LOGF_DEBUG("RES (%s)", response); tcflush(PortFD, TCIFLUSH); if (!strcmp(response, "SET")) return true; else return false; } return false; } /************************************************************************************ * * ***********************************************************************************/ bool FocusLynxBase::setBacklashCompensationSteps(uint16_t steps) { char cmd[16]; int errcode = 0; char errmsg[MAXRBUF]; char response[16]; int nbytes_read = 0; int nbytes_written = 0; memset(response, 0, sizeof(response)); snprintf(cmd, 16, "<%sSCBS%02d>", getFocusTarget(), steps); LOGF_DEBUG("CMD (%s)", cmd); if (isSimulation()) { strncpy(response, "SET", 16); nbytes_read = strlen(response) + 1; } else { if ((errcode = tty_write(PortFD, cmd, strlen(cmd), &nbytes_written)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); LOGF_ERROR("%s", errmsg); return false; } if (isResponseOK() == false) return false; if ((errcode = tty_read_section(PortFD, response, 0xA, LYNXFOCUS_TIMEOUT, &nbytes_read)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); LOGF_ERROR("%s", errmsg); return false; } } if (nbytes_read > 0) { response[nbytes_read - 1] = '\0'; LOGF_DEBUG("RES (%s)", response); tcflush(PortFD, TCIFLUSH); if (!strcmp(response, "SET")) return true; else return false; } return false; } /************************************************************************************ * * ***********************************************************************************/ bool FocusLynxBase::reverse(bool enable) { char cmd[16]; int errcode = 0; char errmsg[MAXRBUF]; char response[16]; int nbytes_read = 0; int nbytes_written = 0; memset(response, 0, sizeof(response)); snprintf(cmd, 16, "<%sREVERSE%d>", getFocusTarget(), enable ? 1 : 0); LOGF_DEBUG("CMD (%s)", cmd); if (isSimulation()) { strncpy(response, "SET", 16); nbytes_read = strlen(response) + 1; simStatus[STATUS_REVERSE] = enable ? ISS_ON : ISS_OFF; } else { if ((errcode = tty_write(PortFD, cmd, strlen(cmd), &nbytes_written)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); LOGF_ERROR("%s", errmsg); return false; } if (isResponseOK() == false) return false; if ((errcode = tty_read_section(PortFD, response, 0xA, LYNXFOCUS_TIMEOUT, &nbytes_read)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); LOGF_ERROR("%s", errmsg); return false; } } if (nbytes_read > 0) { response[nbytes_read - 1] = '\0'; LOGF_DEBUG("RES (%s)", response); tcflush(PortFD, TCIFLUSH); if (!strcmp(response, "SET")) return true; else return false; } return false; } /************************************************************************************ * * ***********************************************************************************/ bool FocusLynxBase::sync(uint32_t position) { char cmd[32]; int errcode = 0; char errmsg[MAXRBUF]; char response[16]; int nbytes_written = 0; memset(response, 0, sizeof(response)); snprintf(cmd, 32, "<%sSCCP%06d>", getFocusTarget(), position); LOGF_DEBUG("CMD (%s)", cmd); if (isSimulation()) { simPosition = position; } else { tcflush(PortFD, TCIFLUSH); if ((errcode = tty_write(PortFD, cmd, strlen(cmd), &nbytes_written)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); LOGF_ERROR("%s", errmsg); return false; } if (isResponseOK() == false) return false; } tcflush(PortFD, TCIFLUSH); LOGF_INFO("Setting current position to %d", position); isSynced = true; return true; } /************************************************************************************ * * ***********************************************************************************/ bool FocusLynxBase::resetFactory() { char cmd[32]; int errcode = 0; char errmsg[MAXRBUF]; char response[16]; int nbytes_read = 0; int nbytes_written = 0; memset(response, 0, sizeof(response)); snprintf(cmd, 32, "<%sRESET>", getFocusTarget()); LOGF_DEBUG("CMD (%s)", cmd); if (isSimulation()) { strncpy(response, "SET", 16); nbytes_read = strlen(response) + 1; } else { tcflush(PortFD, TCIFLUSH); if ((errcode = tty_write(PortFD, cmd, strlen(cmd), &nbytes_written)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); LOGF_ERROR("%s", errmsg); return false; } if (isResponseOK() == false) return false; if ((errcode = tty_read_section(PortFD, response, 0xA, LYNXFOCUS_TIMEOUT, &nbytes_read)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); LOGF_ERROR("%s", errmsg); return false; } } if (nbytes_read > 0) { response[nbytes_read - 1] = '\0'; LOGF_DEBUG("RES (%s)", response); tcflush(PortFD, TCIFLUSH); if (!strcmp(response, "SET")) { return true; getFocusConfig(); } else return false; } return false; } /************************************************************************************ * * ***********************************************************************************/ bool FocusLynxBase::isResponseOK() { int errcode = 0; char errmsg[MAXRBUF]; char response[32]; int nbytes_read = 0; memset(response, 0, sizeof(response)); if (isSimulation()) { strcpy(response, "!"); nbytes_read = strlen(response) + 1; } else { if ((errcode = tty_read_section(PortFD, response, 0xA, LYNXFOCUS_TIMEOUT, &nbytes_read)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); LOGF_ERROR("TTY error: %s", errmsg); return false; } } if (nbytes_read > 0) { response[nbytes_read - 1] = '\0'; LOGF_DEBUG("RES (%s)", response); if (response[0] == '!') return true; else { LOGF_ERROR("Controller error: %s", response); return false; } } return true; } /************************************************************************************ * * ***********************************************************************************/ IPState FocusLynxBase::MoveFocuser(FocusDirection dir, int speed, uint16_t duration) { char cmd[16]; int errcode = 0; char errmsg[MAXRBUF]; char response[16]; int nbytes_read = 0; int nbytes_written = 0; // Relative focusers must be synced initially. if (isAbsolute == false && isSynced == false) { DEBUG(INDI::Logger::DBG_ERROR, "Relative focusers must be synced. Please sync before issuing any motion commands."); return IPS_ALERT; } memset(response, 0, sizeof(response)); snprintf(cmd, 16, "<%sM%cR%c>", getFocusTarget(), (dir == FOCUS_INWARD) ? 'I' : 'O', (speed == 0) ? '0' : '1'); LOGF_DEBUG("CMD (%s)", cmd); if (isSimulation()) { strncpy(response, "M", 16); nbytes_read = strlen(response) + 1; } else { tcflush(PortFD, TCIFLUSH); if ((errcode = tty_write(PortFD, cmd, strlen(cmd), &nbytes_written)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); LOGF_ERROR("%s", errmsg); return IPS_ALERT; } if (isResponseOK() == false) return IPS_ALERT; gettimeofday(&focusMoveStart, nullptr); focusMoveRequest = duration / 1000.0; if ((errcode = tty_read_section(PortFD, response, 0xA, LYNXFOCUS_TIMEOUT, &nbytes_read)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); LOGF_ERROR("%s", errmsg); return IPS_ALERT; } } if (nbytes_read > 0) { response[nbytes_read - 1] = '\0'; LOGF_DEBUG("RES (%s)", response); if (duration <= POLLMS) { usleep(POLLMS * 1000); AbortFocuser(); return IPS_OK; } tcflush(PortFD, TCIFLUSH); return IPS_BUSY; } return IPS_ALERT; } /************************************************************************************ * * ***********************************************************************************/ IPState FocusLynxBase::MoveAbsFocuser(uint32_t targetTicks) { char cmd[32]; int errcode = 0; char errmsg[MAXRBUF]; char response[16]; int nbytes_read = 0; int nbytes_written = 0; // Relative focusers must be synced initially. if (isAbsolute == false && isSynced == false) { DEBUG(INDI::Logger::DBG_ERROR, "Relative focusers must be synced. Please sync before issuing any motion commands."); return IPS_ALERT; } targetPosition = targetTicks; memset(response, 0, sizeof(response)); snprintf(cmd, 32, "<%sMA%06d>", getFocusTarget(), targetTicks); LOGF_DEBUG("CMD (%s)", cmd); if (isSimulation()) { strncpy(response, "M", 16); nbytes_read = strlen(response) + 1; simStatus[STATUS_MOVING] = ISS_ON; } else { tcflush(PortFD, TCIFLUSH); if ((errcode = tty_write(PortFD, cmd, strlen(cmd), &nbytes_written)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); LOGF_ERROR("%s", errmsg); return IPS_ALERT; } if (isResponseOK() == false) return IPS_ALERT; if ((errcode = tty_read_section(PortFD, response, 0xA, LYNXFOCUS_TIMEOUT, &nbytes_read)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); LOGF_ERROR("%s", errmsg); return IPS_ALERT; } } if (nbytes_read > 0) { response[nbytes_read - 1] = '\0'; LOGF_DEBUG("RES (%s)", response); FocusAbsPosNP.s = IPS_BUSY; tcflush(PortFD, TCIFLUSH); return IPS_BUSY; } return IPS_ALERT; } /************************************************************************************ * * ***********************************************************************************/ IPState FocusLynxBase::MoveRelFocuser(FocusDirection dir, uint32_t ticks) { uint32_t newPosition = 0; // Relative focusers must be synced initially. if (isAbsolute == false && isSynced == false) { DEBUG(INDI::Logger::DBG_ERROR, "Relative focusers must be synced. Please sync before issuing any motion commands."); return IPS_ALERT; } if (dir == FOCUS_INWARD) newPosition = FocusAbsPosN[0].value - ticks; else newPosition = FocusAbsPosN[0].value + ticks; return MoveAbsFocuser(newPosition); } /************************************************************************************ * * ***********************************************************************************/ void FocusLynxBase::TimerHit() { if (!isConnected()) return; if (configurationComplete == false) { SetTimer(POLLMS); return; } bool statusrc = false; for (int i = 0; i < 2; i++) { statusrc = getFocusStatus(); if (statusrc) break; } if (statusrc == false) { LOG_WARN("Unable to read focuser status...."); SetTimer(POLLMS); return; } if (FocusAbsPosNP.s == IPS_BUSY || FocusRelPosNP.s == IPS_BUSY) { if (isSimulation()) { if (FocusAbsPosN[0].value < targetPosition) simPosition += 100; else simPosition -= 100; simStatus[STATUS_MOVING] = ISS_ON; if (std::abs((int64_t)simPosition - (int64_t)targetPosition) < 100) { FocusAbsPosN[0].value = targetPosition; simPosition = FocusAbsPosN[0].value; simStatus[STATUS_MOVING] = ISS_OFF; StatusL[STATUS_MOVING].s = IPS_IDLE; if (simStatus[STATUS_HOMING] == ISS_ON) { StatusL[STATUS_HOMED].s = IPS_OK; StatusL[STATUS_HOMING].s = IPS_IDLE; simStatus[STATUS_HOMING] = ISS_OFF; simStatus[STATUS_HOMED] = ISS_ON; } } else StatusL[STATUS_MOVING].s = IPS_BUSY; IDSetLight(&StatusLP, NULL); } if (isHoming && StatusL[STATUS_HOMED].s == IPS_OK) { isHoming = false; GotoSP.s = IPS_OK; IUResetSwitch(&GotoSP); GotoS[GOTO_HOME].s = ISS_ON; IDSetSwitch(&GotoSP, nullptr); FocusAbsPosNP.s = IPS_OK; IDSetNumber(&FocusRelPosNP, nullptr); LOG_INFO("Focuser reached home position."); if (isSimulation()) center(); } else if (StatusL[STATUS_MOVING].s == IPS_IDLE) { FocusAbsPosNP.s = IPS_OK; FocusRelPosNP.s = IPS_OK; IDSetNumber(&FocusAbsPosNP, nullptr); IDSetNumber(&FocusRelPosNP, nullptr); if (GotoSP.s == IPS_BUSY) { IUResetSwitch(&GotoSP); GotoSP.s = IPS_OK; IDSetSwitch(&GotoSP, nullptr); } LOG_INFO("Focuser reached requested position."); } else if (StatusL[STATUS_MOVING].s == IPS_BUSY && focusMoveRequest > 0) { float remaining = calcTimeLeft(focusMoveStart, focusMoveRequest); if (remaining < POLLMS) { sleep(remaining); AbortFocuser(); focusMoveRequest = 0; } } } if (StatusL[STATUS_HOMING].s == IPS_BUSY && GotoSP.s != IPS_BUSY) { GotoSP.s = IPS_BUSY; IDSetSwitch(&GotoSP, nullptr); } SetTimer(POLLMS); } /************************************************************************************ * * ***********************************************************************************/ bool FocusLynxBase::AbortFocuser() { char cmd[32]; int errcode = 0; char errmsg[MAXRBUF]; char response[16]; int nbytes_read = 0; int nbytes_written = 0; memset(response, 0, sizeof(response)); snprintf(cmd, 32, "<%sHALT>", getFocusTarget()); LOGF_DEBUG("CMD (%s)", cmd); if (isSimulation()) { strncpy(response, "HALTED", 16); nbytes_read = strlen(response) + 1; simStatus[STATUS_MOVING] = ISS_OFF; simStatus[STATUS_HOMING] = ISS_OFF; } else { tcflush(PortFD, TCIFLUSH); if ((errcode = tty_write(PortFD, cmd, strlen(cmd), &nbytes_written)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); LOGF_ERROR("%s", errmsg); return false; } if (isResponseOK() == false) return false; if ((errcode = tty_read_section(PortFD, response, 0xA, LYNXFOCUS_TIMEOUT, &nbytes_read)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); LOGF_ERROR("%s", errmsg); return false; } } if (nbytes_read > 0) { response[nbytes_read - 1] = '\0'; LOGF_DEBUG("RES (%s)", response); if (FocusRelPosNP.s == IPS_BUSY) { FocusRelPosNP.s = IPS_IDLE; IDSetNumber(&FocusRelPosNP, nullptr); } FocusTimerNP.s = FocusAbsPosNP.s = GotoSP.s = IPS_IDLE; IUResetSwitch(&GotoSP); IDSetNumber(&FocusTimerNP, nullptr); IDSetNumber(&FocusAbsPosNP, nullptr); IDSetSwitch(&GotoSP, nullptr); tcflush(PortFD, TCIFLUSH); return true; } return false; } /************************************************************************************ * * ***********************************************************************************/ float FocusLynxBase::calcTimeLeft(timeval start, float req) { double timesince; double timeleft; struct timeval now { 0, 0 }; gettimeofday(&now, nullptr); timesince = (double)(now.tv_sec * 1000.0 + now.tv_usec / 1000) - (double)(start.tv_sec * 1000.0 + start.tv_usec / 1000); timesince = timesince / 1000; timeleft = req - timesince; return timeleft; } /************************************************************************************ * * ***********************************************************************************/ bool FocusLynxBase::saveConfigItems(FILE *fp) { INDI::Focuser::saveConfigItems(fp); IUSaveConfigSwitch(fp, &ModelSP); IUSaveConfigSwitch(fp, &TemperatureCompensateSP); IUSaveConfigSwitch(fp, &TemperatureCompensateOnStartSP); IUSaveConfigSwitch(fp, &ReverseSP); IUSaveConfigNumber(fp, &TemperatureCoeffNP); IUSaveConfigSwitch(fp, &TemperatureCompensateModeSP); IUSaveConfigSwitch(fp, &BacklashCompensationSP); IUSaveConfigNumber(fp, &BacklashNP); if (isAbsolute == false) IUSaveConfigNumber(fp, &MaxTravelNP); return true; } /************************************************************************************ * * ***********************************************************************************/ void FocusLynxBase::debugTriggered(bool enable) { INDI_UNUSED(enable); //tty_set_debug(enable ? 1 : 0); } /************************************************************************************ * * ***********************************************************************************/ void FocusLynxBase::setFocusTarget(const char *target) // Use to set the string of the private char[] focusTarget { strncpy(focusTarget, target, 8); } /************************************************************************************ * * ***********************************************************************************/ const char *FocusLynxBase::getFocusTarget() // Use to get the string of the private char[] focusTarget { return focusTarget; } /************************************************************************************ * * ***********************************************************************************/ int FocusLynxBase::getVersion(int *major, int *minor, int *sub) { INDI_UNUSED(major); INDI_UNUSED(minor); INDI_UNUSED(sub); /* For future use of implementation of new firmware 2.0.0 * and give ability to keep compatible to actual 1.0.9 * Will be to avoid calling to new functions * Not yet implemented in this version of the driver */ char sMajor[8], sMinor[8], sSub[8]; int rc = sscanf(version, "%[^.].%[^.].%s",sMajor, sMinor, sSub); LOGF_DEBUG("Version major: %s, minor: %s, subversion: %s", sMajor, sMinor, sSub); *major = atoi(sMajor); *minor = atoi(sMinor); *sub = atoi(sSub); if (rc == 3) return *major; return 0; // 0 Means error in this case } libindi/drivers/focuser/perfectstar.h0000664000175000017500000000436013263645557017314 0ustar jasemjasem/******************************************************************************* Copyright(c) 2015 Jasem Mutlaq. All rights reserved. PerfectStar Focuser This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2 as published by the Free Software Foundation. . 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 Library General Public License for more details. . You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. *******************************************************************************/ #pragma once #include "indifocuser.h" #include "hidapi.h" class PerfectStar : public INDI::Focuser { public: // Perfect Star (PS) status typedef enum { PS_NOOP, PS_IN, PS_OUT, PS_GOTO, PS_SETPOS, PS_LOCKED, PS_HALT = 0xFF } PS_STATUS; PerfectStar(); virtual ~PerfectStar() = default; virtual bool ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n); const char *getDefaultName(); virtual bool initProperties(); virtual bool updateProperties(); virtual bool saveConfigItems(FILE *fp); bool Connect(); bool Disconnect(); void TimerHit(); virtual IPState MoveAbsFocuser(uint32_t targetTicks); virtual IPState MoveRelFocuser(FocusDirection dir, uint32_t ticks); virtual bool AbortFocuser(); private: bool setPosition(uint32_t ticks); bool getPosition(uint32_t *ticks); bool setStatus(PS_STATUS targetStatus); bool getStatus(PS_STATUS *currentStatus); bool sync(uint32_t ticks); hid_device *handle { nullptr }; PS_STATUS status { PS_NOOP }; bool sim { false }; uint32_t simPosition { 0 }; uint32_t targetPosition { 0 }; // Max position in ticks INumber MaxPositionN[1]; INumberVectorProperty MaxPositionNP; // Sync to a particular position INumber SyncN[1]; INumberVectorProperty SyncNP; }; libindi/drivers/focuser/focus_simulator.h0000664000175000017500000000523413263645557020211 0ustar jasemjasem/******************************************************************************* Copyright(c) 2012 Jasem Mutlaq. All rights reserved. This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2 as published by the Free Software Foundation. . 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 Library General Public License for more details. . You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. *******************************************************************************/ #pragma once #include "indifocuser.h" /** * @brief The FocusSim class provides a simple Focuser simulator that can simulator the following devices: * + Absolute Focuser with encoders. * + Relative Focuser. * + Simple DC Focuser. * * The focuser type must be selected before establishing connection to the focuser. * * The driver defines FWHM property that is used in the @ref CCDSim "CCD Simulator" driver to simulate the fuzziness of star images. * It can be used to test AutoFocus routines among other applications. */ class FocusSim : public INDI::Focuser { public: FocusSim(); virtual ~FocusSim() = default; const char *getDefaultName(); bool initProperties(); void ISGetProperties(const char *dev); bool updateProperties(); bool Connect(); bool Disconnect(); virtual bool ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n); virtual bool ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n); virtual IPState MoveFocuser(FocusDirection dir, int speed, uint16_t duration); virtual IPState MoveAbsFocuser(uint32_t targetTicks); virtual IPState MoveRelFocuser(FocusDirection dir, uint32_t ticks); virtual bool SetFocuserSpeed(int speed); private: double internalTicks { 0 }; double initTicks { 0 }; // Seeing in arcseconds INumberVectorProperty SeeingNP; INumber SeeingN[1]; // FWHM to be used by CCD driver to draw 'fuzzy' stars INumberVectorProperty FWHMNP; INumber FWHMN[1]; // Current mode of Focus simulator for testing purposes enum { MODE_ALL, MODE_ABSOLUTE, MODE_RELATIVE, MODE_TIMER, MODE_COUNT }; ISwitchVectorProperty ModeSP; ISwitch ModeS[MODE_COUNT]; }; libindi/drivers/focuser/dmfc.cpp0000664000175000017500000005636413263645557016251 0ustar jasemjasem/* Pegasus DMFC Focuser Copyright (C) 2017 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 */ #include "dmfc.h" #include "indicom.h" #include "connectionplugins/connectionserial.h" #include #include #include #include #include #define DMFC_TIMEOUT 3 #define FOCUS_SETTINGS_TAB "Settings" #define TEMPERATURE_THRESHOLD 0.1 std::unique_ptr dmfc(new DMFC()); void ISGetProperties(const char *dev) { dmfc->ISGetProperties(dev); } void ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) { dmfc->ISNewSwitch(dev, name, states, names, n); } void ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n) { dmfc->ISNewText(dev, name, texts, names, n); } void ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) { dmfc->ISNewNumber(dev, name, values, names, n); } void ISNewBLOB(const char *dev, const char *name, int sizes[], int blobsizes[], char *blobs[], char *formats[], char *names[], int n) { INDI_UNUSED(dev); INDI_UNUSED(name); INDI_UNUSED(sizes); INDI_UNUSED(blobsizes); INDI_UNUSED(blobs); INDI_UNUSED(formats); INDI_UNUSED(names); INDI_UNUSED(n); } void ISSnoopDevice(XMLEle *root) { dmfc->ISSnoopDevice(root); } DMFC::DMFC() { // Can move in Absolute & Relative motions, can AbortFocuser motion. FI::SetCapability(FOCUSER_CAN_ABS_MOVE | FOCUSER_CAN_REL_MOVE | FOCUSER_CAN_ABORT); } bool DMFC::initProperties() { INDI::Focuser::initProperties(); // Sync IUFillNumber(&SyncN[0], "FOCUS_SYNC_OFFSET", "Offset", "%6.0f", 0, 60000., 0., 0.); IUFillNumberVector(&SyncNP, SyncN, 1, getDeviceName(), "FOCUS_SYNC", "Sync", MAIN_CONTROL_TAB, IP_RW, 0, IPS_IDLE); // Focuser temperature IUFillNumber(&TemperatureN[0], "TEMPERATURE", "Celsius", "%6.2f", -50, 70., 0., 0.); IUFillNumberVector(&TemperatureNP, TemperatureN, 1, getDeviceName(), "FOCUS_TEMPERATURE", "Temperature", MAIN_CONTROL_TAB, IP_RO, 0, IPS_IDLE); // Reverse direction IUFillSwitch(&ReverseS[DIRECTION_NORMAL], "Normal", "", ISS_ON); IUFillSwitch(&ReverseS[DIRECTION_REVERSED], "Reverse", "", ISS_OFF); IUFillSwitchVector(&ReverseSP, ReverseS, 2, getDeviceName(), "Reverse", "", FOCUS_SETTINGS_TAB, IP_RW, ISR_1OFMANY, 0, IPS_IDLE); // Max Speed IUFillNumber(&MaxSpeedN[0], "Value", "", "%6.2f", 100, 1000., 100., 400.); IUFillNumberVector(&MaxSpeedNP, MaxSpeedN, 1, getDeviceName(), "MaxSpeed", "", FOCUS_SETTINGS_TAB, IP_RW, 0, IPS_IDLE); // Enable/Disable backlash IUFillSwitch(&BacklashCompensationS[BACKLASH_ENABLED], "Enable", "", ISS_OFF); IUFillSwitch(&BacklashCompensationS[BACKLASH_DISABLED], "Disable", "", ISS_ON); IUFillSwitchVector(&BacklashCompensationSP, BacklashCompensationS, 2, getDeviceName(), "Backlash Compensation", "", FOCUS_SETTINGS_TAB, IP_RW, ISR_1OFMANY, 0, IPS_IDLE); // Backlash Value IUFillNumber(&BacklashN[0], "Value", "", "%.f", 0, 9999, 100., 0.); IUFillNumberVector(&BacklashNP, BacklashN, 1, getDeviceName(), "Backlash", "", FOCUS_SETTINGS_TAB, IP_RW, 0, IPS_IDLE); // Encoders IUFillSwitch(&EncoderS[ENCODERS_ON], "On", "", ISS_ON); IUFillSwitch(&EncoderS[ENCODERS_OFF], "Off", "", ISS_OFF); IUFillSwitchVector(&EncoderSP, EncoderS, 2, getDeviceName(), "Encoders", "", FOCUS_SETTINGS_TAB, IP_RW, ISR_1OFMANY, 0, IPS_IDLE); // Motor Modes IUFillSwitch(&MotorTypeS[MOTOR_DC], "DC", "", ISS_OFF); IUFillSwitch(&MotorTypeS[MOTOR_STEPPER], "Stepper", "", ISS_ON); IUFillSwitchVector(&MotorTypeSP, MotorTypeS, 2, getDeviceName(), "Motor Type", "", FOCUS_SETTINGS_TAB, IP_RW, ISR_1OFMANY, 0, IPS_IDLE); // LED IUFillSwitch(&LEDS[LED_OFF], "Off", "", ISS_ON); IUFillSwitch(&LEDS[LED_ON], "On", "", ISS_OFF); IUFillSwitchVector(&LEDSP, LEDS, 2, getDeviceName(), "LED", "", FOCUS_SETTINGS_TAB, IP_RW, ISR_1OFMANY, 0, IPS_IDLE); // Firmware Version IUFillText(&FirmwareVersionT[0], "Version", "Version", ""); IUFillTextVector(&FirmwareVersionTP, FirmwareVersionT, 1, getDeviceName(), "Firmware", "Firmware", MAIN_CONTROL_TAB, IP_RO, 0, IPS_IDLE); // Relative and absolute movement FocusRelPosN[0].min = 0.; FocusRelPosN[0].max = 50000.; FocusRelPosN[0].value = 0; FocusRelPosN[0].step = 1000; FocusAbsPosN[0].min = 0.; FocusAbsPosN[0].max = 100000.; FocusAbsPosN[0].value = 0; FocusAbsPosN[0].step = 1000; addDebugControl(); setDefaultPollingPeriod(500); serialConnection->setDefaultBaudRate(Connection::Serial::B_19200); return true; } bool DMFC::updateProperties() { INDI::Focuser::updateProperties(); if (isConnected()) { defineNumber(&TemperatureNP); defineNumber(&SyncNP); defineSwitch(&ReverseSP); defineSwitch(&BacklashCompensationSP); defineNumber(&BacklashNP); defineSwitch(&EncoderSP); defineSwitch(&MotorTypeSP); defineNumber(&MaxSpeedNP); defineSwitch(&LEDSP); defineText(&FirmwareVersionTP); } else { deleteProperty(TemperatureNP.name); deleteProperty(SyncNP.name); deleteProperty(ReverseSP.name); deleteProperty(BacklashCompensationSP.name); deleteProperty(BacklashNP.name); deleteProperty(EncoderSP.name); deleteProperty(MotorTypeSP.name); deleteProperty(MaxSpeedNP.name); deleteProperty(LEDSP.name); deleteProperty(FirmwareVersionTP.name); } return true; } bool DMFC::Handshake() { if (ack()) { LOG_INFO("DMFC is online. Getting focus parameters..."); return true; } DEBUG(INDI::Logger::DBG_SESSION, "Error retreiving data from DMFC, please ensure DMFC controller is powered and the port is correct."); return false; } const char *DMFC::getDefaultName() { return "Pegasus DMFC"; } bool DMFC::ack() { int nbytes_written = 0, nbytes_read = 0, rc = -1; char errstr[MAXRBUF]; char cmd[2] = {0}; char res[16]={0}; cmd[0] = '#'; cmd[1] = 0xA; LOGF_DEBUG("CMD <%#02X>", cmd[0]); tcflush(PortFD, TCIOFLUSH); if ((rc = tty_write(PortFD, cmd, 2, &nbytes_written)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("Ack error: %s.", errstr); return false; } if ((rc = tty_read_section(PortFD, res, 0xA, DMFC_TIMEOUT, &nbytes_read)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("Ack error: %s.", errstr); return false; } // Get rid of 0xA res[nbytes_read-1] = 0; LOGF_DEBUG("RES <%s>", res); tcflush(PortFD, TCIOFLUSH); return (!strcmp(res, "OK_DMFCN")); } bool DMFC::sync(uint32_t newPosition) { int nbytes_written = 0, rc = -1; char errstr[MAXRBUF]; char cmd[16]={0}; snprintf(cmd, 16, "W:%d", newPosition); cmd[strlen(cmd)] = 0xA; LOGF_DEBUG("CMD <%s>", cmd); // Set Position if ((rc = tty_write(PortFD, cmd, strlen(cmd), &nbytes_written)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("sync error: %s.", errstr); return false; } return true; } bool DMFC::move(uint32_t newPosition) { int nbytes_written = 0, rc = -1; char errstr[MAXRBUF]; char cmd[16]={0}; snprintf(cmd, 16, "M:%d", newPosition); cmd[strlen(cmd)] = 0xA; LOGF_DEBUG("CMD <%s>", cmd); // Set Position if ((rc = tty_write(PortFD, cmd, strlen(cmd), &nbytes_written)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("move error: %s.", errstr); return false; } return true; } bool DMFC::ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) { if (dev != nullptr && strcmp(dev, getDeviceName()) == 0) { ///////////////////////////////////////////// // Backlash ///////////////////////////////////////////// if (!strcmp(name, BacklashCompensationSP.name)) { IUUpdateSwitch(&BacklashCompensationSP, states, names, n); bool rc = false; if (IUFindOnSwitchIndex(&BacklashCompensationSP) == BACKLASH_ENABLED) rc = setBacklash(BacklashN[0].value); else rc = setBacklash(0); BacklashCompensationSP.s = rc ? IPS_OK : IPS_ALERT; IDSetSwitch(&BacklashCompensationSP, nullptr); return true; } ///////////////////////////////////////////// // Encoders ///////////////////////////////////////////// else if (!strcmp(name, EncoderSP.name)) { IUUpdateSwitch(&EncoderSP, states, names, n); bool rc = setEncodersEnabled(EncoderS[ENCODERS_ON].s == ISS_ON); EncoderSP.s = rc ? IPS_OK : IPS_ALERT; IDSetSwitch(&EncoderSP, nullptr); return true; } ///////////////////////////////////////////// // LED ///////////////////////////////////////////// else if (!strcmp(name, LEDSP.name)) { IUUpdateSwitch(&LEDSP, states, names, n); bool rc = setLedEnabled(LEDS[LED_ON].s == ISS_ON); LEDSP.s = rc ? IPS_OK : IPS_ALERT; IDSetSwitch(&LEDSP, nullptr); return true; } ///////////////////////////////////////////// // Reverse ///////////////////////////////////////////// else if (!strcmp(name, ReverseSP.name)) { IUUpdateSwitch(&ReverseSP, states, names, n); bool rc = setReverseEnabled(ReverseS[DIRECTION_REVERSED].s == ISS_ON); ReverseSP.s = rc ? IPS_OK : IPS_ALERT; IDSetSwitch(&ReverseSP, nullptr); return true; } ///////////////////////////////////////////// // Motor Type ///////////////////////////////////////////// else if (!strcmp(name, MotorTypeSP.name)) { IUUpdateSwitch(&MotorTypeSP, states, names, n); bool rc = setMotorType(IUFindOnSwitchIndex(&MotorTypeSP)); MotorTypeSP.s = rc ? IPS_OK : IPS_ALERT; IDSetSwitch(&MotorTypeSP, nullptr); return true; } } return INDI::Focuser::ISNewSwitch(dev, name, states, names, n); } bool DMFC::ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) { if (dev != nullptr && strcmp(dev, getDeviceName()) == 0) { ///////////////////////////////////////////// // Sync ///////////////////////////////////////////// if (strcmp(name, SyncNP.name) == 0) { IUUpdateNumber(&SyncNP, values, names, n); bool rc = sync(SyncN[0].value); SyncNP.s = rc ? IPS_OK : IPS_ALERT; IDSetNumber(&SyncNP, nullptr); return true; } ///////////////////////////////////////////// // Backlash ///////////////////////////////////////////// else if (strcmp(name, BacklashNP.name) == 0) { IUUpdateNumber(&BacklashNP, values, names, n); // Only updaet backlash value if compensation is enabled if (BacklashCompensationS[BACKLASH_ENABLED].s == ISS_ON) { bool rc = setBacklash(BacklashN[0].value); BacklashNP.s = rc ? IPS_OK : IPS_ALERT; } else BacklashNP.s = IPS_OK; IDSetNumber(&BacklashNP, nullptr); return true; } ///////////////////////////////////////////// // MaxSpeed ///////////////////////////////////////////// else if (strcmp(name, MaxSpeedNP.name) == 0) { IUUpdateNumber(&MaxSpeedNP, values, names, n); bool rc = setMaxSpeed(MaxSpeedN[0].value); MaxSpeedNP.s = rc ? IPS_OK : IPS_ALERT; IDSetNumber(&MaxSpeedNP, nullptr); return true; } } return INDI::Focuser::ISNewNumber(dev, name, values, names, n); } bool DMFC::updateFocusParams() { int nbytes_written = 0, nbytes_read = 0, rc = -1; char errstr[MAXRBUF]; char cmd[3] = {0}; char res[64]; cmd[0] = 'A'; cmd[1] = 0xA; LOGF_DEBUG("CMD <%#02X>", cmd[0]); tcflush(PortFD, TCIOFLUSH); if ((rc = tty_write(PortFD, cmd, 2, &nbytes_written)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("GetFocusParams error: %s.", errstr); return false; } if ((rc = tty_read_section(PortFD, res, 0xA, DMFC_TIMEOUT, &nbytes_read)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("GetFocusParams error: %s.", errstr); return false; } res[nbytes_read-1] = 0; LOGF_DEBUG("RES <%s>", res); tcflush(PortFD, TCIOFLUSH); char *token = std::strtok(res, ":"); // #1 Status if (token == nullptr || strcmp(token, "OK_DMFCN")) { LOG_ERROR("Invalid status response."); return false; } // #2 Version token = std::strtok(NULL, ":"); if (token == nullptr) { LOG_ERROR("Invalid version response."); return false; } if (FirmwareVersionT[0].text == nullptr || strcmp(FirmwareVersionT[0].text, token)) { IUSaveText(&FirmwareVersionT[0], token); FirmwareVersionTP.s = IPS_OK; IDSetText(&FirmwareVersionTP, nullptr); } // #3 Motor Type token = std::strtok(NULL, ":"); if (token == nullptr) { LOG_ERROR("Invalid motor mode response."); return false; } int motorType = atoi(token); if (motorType != IUFindOnSwitchIndex(&MotorTypeSP) && motorType >= 0 && motorType <= 1) { IUResetSwitch(&MotorTypeSP); MotorTypeS[motorType].s = ISS_ON; MotorTypeSP.s = IPS_OK; IDSetSwitch(&MotorTypeSP, nullptr); } // #4 Temperature token = std::strtok(NULL, ":"); if (token == nullptr) { LOG_ERROR("Invalid temperature response."); return false; } double temperature = atof(token); if (temperature == -127) { TemperatureNP.s = IPS_ALERT; IDSetNumber(&TemperatureNP, nullptr); } else { if (fabs(temperature - TemperatureN[0].value) > TEMPERATURE_THRESHOLD) { TemperatureN[0].value = temperature; TemperatureNP.s = IPS_OK; IDSetNumber(&TemperatureNP, nullptr); } } // #5 Position token = std::strtok(NULL, ":"); if (token == nullptr) { LOG_ERROR("Invalid position response."); return false; } currentPosition = atoi(token); if (currentPosition != FocusAbsPosN[0].value) { FocusAbsPosN[0].value = currentPosition; IDSetNumber(&FocusAbsPosNP, nullptr); } // #6 Moving Status token = std::strtok(NULL, ":"); if (token == nullptr) { LOG_ERROR("Invalid moving satus response."); return false; } isMoving = (token[0] == '1'); // #7 LED Status token = std::strtok(NULL, ":"); if (token == nullptr) { LOG_ERROR("Invalid LED response."); return false; } int ledStatus = atoi(token); if (ledStatus != IUFindOnSwitchIndex(&LEDSP) && ledStatus >= 0 && ledStatus <= 1) { IUResetSwitch(&LEDSP); LEDS[ledStatus].s = ISS_ON; LEDSP.s = IPS_OK; IDSetSwitch(&LEDSP, nullptr); } // #8 Reverse Status token = std::strtok(NULL, ":"); if (token == nullptr) { LOG_ERROR("Invalid reverse response."); return false; } int reverseStatus = atoi(token); if (reverseStatus != IUFindOnSwitchIndex(&ReverseSP) && reverseStatus >= 0 && reverseStatus <= 1) { IUResetSwitch(&ReverseSP); ReverseS[reverseStatus].s = ISS_ON; ReverseSP.s = IPS_OK; IDSetSwitch(&ReverseSP, nullptr); } // #9 Encoder status token = std::strtok(NULL, ":"); if (token == nullptr) { LOG_ERROR("Invalid encoder response."); return false; } int encoderStatus = atoi(token); if (encoderStatus != IUFindOnSwitchIndex(&EncoderSP) && encoderStatus >= 0 && encoderStatus <= 1) { IUResetSwitch(&EncoderSP); EncoderS[encoderStatus].s = ISS_ON; EncoderSP.s = IPS_OK; IDSetSwitch(&EncoderSP, nullptr); } // #10 Backlash token = std::strtok(NULL, ":"); if (token == nullptr) { LOG_ERROR("Invalid encoder response."); return false; } int backlash = atoi(token); // If backlash is zero then compensation is disabled if (backlash == 0 && BacklashCompensationS[BACKLASH_ENABLED].s == ISS_ON) { BacklashCompensationS[BACKLASH_ENABLED].s = ISS_OFF; BacklashCompensationS[BACKLASH_DISABLED].s = ISS_ON; BacklashCompensationSP.s = IPS_IDLE; IDSetSwitch(&BacklashCompensationSP, nullptr); } else if (backlash > 0 && (BacklashCompensationS[BACKLASH_DISABLED].s == ISS_ON || backlash != BacklashN[0].value)) { if (backlash != BacklashN[0].value) { BacklashN[0].value = backlash; BacklashNP.s = IPS_OK; IDSetNumber(&BacklashNP, NULL); } if (BacklashCompensationS[BACKLASH_DISABLED].s == ISS_ON) { BacklashCompensationS[BACKLASH_ENABLED].s = ISS_OFF; BacklashCompensationS[BACKLASH_DISABLED].s = ISS_ON; BacklashCompensationSP.s = IPS_IDLE; IDSetSwitch(&BacklashCompensationSP, nullptr); } } return true; } bool DMFC::setMaxSpeed(uint16_t speed) { int nbytes_written = 0, rc = -1; char errstr[MAXRBUF]; char cmd[16]={0}; snprintf(cmd, 16, "S:%d", speed); cmd[strlen(cmd)] = 0xA; LOGF_DEBUG("CMD <%s>", cmd); tcflush(PortFD, TCIOFLUSH); // Set Speed if ((rc = tty_write(PortFD, cmd, strlen(cmd), &nbytes_written)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("setMaxSpeed error: %s.", errstr); return false; } return true; } bool DMFC::setReverseEnabled(bool enable) { int nbytes_written = 0, rc = -1; char errstr[MAXRBUF]; char cmd[16]={0}; snprintf(cmd, 16, "N:%d", enable ? 1 : 0); cmd[strlen(cmd)] = 0xA; LOGF_DEBUG("CMD <%s>", cmd); tcflush(PortFD, TCIOFLUSH); // Reverse if ((rc = tty_write(PortFD, cmd, strlen(cmd), &nbytes_written)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("Reverse error: %s.", errstr); return false; } return true; } bool DMFC::setLedEnabled(bool enable) { int nbytes_written = 0, rc = -1; char errstr[MAXRBUF]; char cmd[16]={0}; snprintf(cmd, 16, "L:%d", enable ? 2 : 1); cmd[strlen(cmd)] = 0xA; LOGF_DEBUG("CMD <%s>", cmd); tcflush(PortFD, TCIOFLUSH); // Led if ((rc = tty_write(PortFD, cmd, strlen(cmd), &nbytes_written)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("Led error: %s.", errstr); return false; } return true; } bool DMFC::setEncodersEnabled(bool enable) { int nbytes_written = 0, rc = -1; char errstr[MAXRBUF]; char cmd[16]={0}; snprintf(cmd, 16, "E:%d", enable ? 0 : 1); cmd[strlen(cmd)] = 0xA; LOGF_DEBUG("CMD <%s>", cmd); tcflush(PortFD, TCIOFLUSH); // Encoders if ((rc = tty_write(PortFD, cmd, strlen(cmd), &nbytes_written)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("Encoder error: %s.", errstr); return false; } return true; } bool DMFC::setBacklash(uint16_t value) { int nbytes_written = 0, rc = -1; char errstr[MAXRBUF]; char cmd[16]={0}; snprintf(cmd, 16, "C:%d", value); cmd[strlen(cmd)] = 0xA; LOGF_DEBUG("CMD <%s>", cmd); tcflush(PortFD, TCIOFLUSH); // Backlash if ((rc = tty_write(PortFD, cmd, strlen(cmd), &nbytes_written)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("Backlash error: %s.", errstr); return false; } return true; } bool DMFC::setMotorType(uint8_t type) { int nbytes_written = 0, rc = -1; char errstr[MAXRBUF]; char cmd[16]={0}; snprintf(cmd, 16, "E:%d", (type == MOTOR_STEPPER) ? 1 : 0); cmd[strlen(cmd)] = 0xA; LOGF_DEBUG("CMD <%s>", cmd); tcflush(PortFD, TCIOFLUSH); // Motor Type if ((rc = tty_write(PortFD, cmd, strlen(cmd), &nbytes_written)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("Motor type error: %s.", errstr); return false; } return true; } IPState DMFC::MoveAbsFocuser(uint32_t targetTicks) { targetPosition = targetTicks; bool rc = false; rc = move(targetPosition); if (!rc) return IPS_ALERT; FocusAbsPosNP.s = IPS_BUSY; return IPS_BUSY; } IPState DMFC::MoveRelFocuser(FocusDirection dir, uint32_t ticks) { double newPosition = 0; bool rc = false; if (dir == FOCUS_INWARD) newPosition = FocusAbsPosN[0].value - ticks; else newPosition = FocusAbsPosN[0].value + ticks; rc = move(newPosition); if (!rc) return IPS_ALERT; FocusRelPosN[0].value = ticks; FocusRelPosNP.s = IPS_BUSY; return IPS_BUSY; } void DMFC::TimerHit() { if (!isConnected()) { SetTimer(POLLMS); return; } bool rc = updateFocusParams(); if (rc) { if (FocusAbsPosNP.s == IPS_BUSY || FocusRelPosNP.s == IPS_BUSY) { if (isMoving == false) { FocusAbsPosNP.s = IPS_OK; FocusRelPosNP.s = IPS_OK; IDSetNumber(&FocusAbsPosNP, nullptr); IDSetNumber(&FocusRelPosNP, nullptr); LOG_INFO("Focuser reached requested position."); } } } SetTimer(POLLMS); } bool DMFC::AbortFocuser() { int nbytes_written; char cmd[2] = { 'H', 0xA }; if (tty_write(PortFD, cmd, 2, &nbytes_written) == TTY_OK) { FocusAbsPosNP.s = IPS_IDLE; FocusRelPosNP.s = IPS_IDLE; IDSetNumber(&FocusAbsPosNP, nullptr); IDSetNumber(&FocusRelPosNP, nullptr); return true; } else return false; } bool DMFC::saveConfigItems(FILE *fp) { INDI::Focuser::saveConfigItems(fp); IUSaveConfigSwitch(fp, &ReverseSP); IUSaveConfigNumber(fp, &BacklashNP); IUSaveConfigSwitch(fp, &BacklashCompensationSP); IUSaveConfigSwitch(fp, &EncoderSP); IUSaveConfigSwitch(fp, &MotorTypeSP); IUSaveConfigNumber(fp, &MaxSpeedNP); IUSaveConfigSwitch(fp, &LEDSP); return true; } libindi/drivers/focuser/99-focusers.rules0000664000175000017500000000103313263645557017757 0ustar jasemjasem# HitechAstro & Perfect star SUBSYSTEMS=="usb", ATTRS{idVendor}=="04d8", MODE="0666" # Focus Master SUBSYSTEMS=="usb", ATTRS{idVendor}=="134a", MODE="0666" # Gemini Telescope Design Integra85 Focusing Rotator # Uncomment the SUBSYSTEMS line below if you have no other Arduino devices and want to use a # more logical name than ttyACM0 and activate with: udevadm control --reload-rules #SUBSYSTEMS=="usb", ATTRS{idVendor}=="2a03", ATTRS{idProduct}=="0043", MODE="0666", SYMLINK+="integra_focusing_rotator%n", ENV{ID_MM_DEVICE_IGNORE}="1" libindi/drivers/video/0000775000175000017500000000000013263645557014256 5ustar jasemjasemlibindi/drivers/video/indi_v4l2driver.cpp0000664000175000017500000000417013263645557017772 0ustar jasemjasem#if 0 V4L Philips LX INDI Driver INDI Interface for V4L devices (Philips) 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 "v4l2driver.h" V4L2_Driver *MainCam = nullptr; /* Main and only camera */ /* send client definitions of all properties */ void ISInit() { if (MainCam == nullptr) { MainCam = new V4L2_Driver(); //MainCam->initProperties(); MainCam->initCamBase(); } } void ISGetProperties(const char *dev) { ISInit(); MainCam->ISGetProperties(dev); } void ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) { ISInit(); MainCam->ISNewSwitch(dev, name, states, names, n); } void ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n) { ISInit(); MainCam->ISNewText(dev, name, texts, names, n); } void ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) { ISInit(); MainCam->ISNewNumber(dev, name, values, names, n); } void ISNewBLOB(const char *dev, const char *name, int sizes[], int blobsizes[], char *blobs[], char *formats[], char *names[], int n) { INDI_UNUSED(dev); INDI_UNUSED(name); INDI_UNUSED(sizes); INDI_UNUSED(blobsizes); INDI_UNUSED(blobs); INDI_UNUSED(formats); INDI_UNUSED(names); INDI_UNUSED(n); } void ISSnoopDevice(XMLEle *root) { MainCam->ISSnoopDevice(root); } libindi/drivers/video/v4l2driver.cpp0000664000175000017500000016225113263645557016774 0ustar jasemjasem#if 0 V4L INDI Driver INDI Interface for V4L devices Copyright (C) 2003 - 2013 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 #include "v4l2driver.h" #include "indistandardproperty.h" #include "lx/Lx.h" V4L2_Driver::V4L2_Driver() { //sigevent sevp; //struct itimerspec fpssettings; allocateBuffers(); divider = 128.; is_capturing = false; Options = nullptr; v4loptions = 0; AbsExposureN = nullptr; ManualExposureSP = nullptr; stackMode = STACK_NONE; lx = new Lx(); lxtimer = -1; stdtimer = -1; } V4L2_Driver::~V4L2_Driver() { releaseBuffers(); } void V4L2_Driver::updateFrameSize() { if (ISS_ON == ImageColorS[IMAGE_GRAYSCALE].s) frameBytes = PrimaryCCD.getSubW() * PrimaryCCD.getSubH() * (PrimaryCCD.getBPP() / 8 + (PrimaryCCD.getBPP() % 8 ? 1 : 0)); else frameBytes = PrimaryCCD.getSubW() * PrimaryCCD.getSubH() * (PrimaryCCD.getBPP() / 8 + (PrimaryCCD.getBPP() % 8 ? 1 : 0)) * 3; PrimaryCCD.setFrameBufferSize(frameBytes); LOGF_DEBUG("%s: frame bytes %d", __FUNCTION__, PrimaryCCD.getFrameBufferSize()); } bool V4L2_Driver::initProperties() { INDI::CCD::initProperties(); addDebugControl(); /* Port */ IUFillText(&PortT[0], "PORT", "Port", "/dev/video0"); IUFillTextVector(&PortTP, PortT, NARRAY(PortT), getDeviceName(), INDI::SP::DEVICE_PORT, "Ports", OPTIONS_TAB, IP_RW, 0, IPS_IDLE); /* Color space */ IUFillSwitch(&ImageColorS[IMAGE_GRAYSCALE], "CCD_COLOR_GRAY", "Gray", ISS_ON); IUFillSwitch(&ImageColorS[1], "CCD_COLOR_RGB", "Color", ISS_OFF); IUFillSwitchVector(&ImageColorSP, ImageColorS, NARRAY(ImageColorS), getDeviceName(), "CCD_COLOR_SPACE", "Image Type", IMAGE_SETTINGS_TAB, IP_RW, ISR_1OFMANY, 0, IPS_IDLE); /* Image depth */ IUFillSwitch(&ImageDepthS[0], "8 bits", "", ISS_ON); IUFillSwitch(&ImageDepthS[1], "16 bits", "", ISS_OFF); IUFillSwitchVector(&ImageDepthSP, ImageDepthS, NARRAY(ImageDepthS), getDeviceName(), "Image Depth", "", IMAGE_SETTINGS_TAB, IP_RW, ISR_1OFMANY, 0, IPS_IDLE); /* Camera Name */ IUFillText(&camNameT[0], "Model", "", nullptr); IUFillTextVector(&camNameTP, camNameT, NARRAY(camNameT), getDeviceName(), "Camera", "", IMAGE_INFO_TAB, IP_RO, 0, IPS_IDLE); /* Stacking Mode */ IUFillSwitch(&StackModeS[STACK_NONE], "None", "", ISS_ON); IUFillSwitch(&StackModeS[STACK_MEAN], "Mean", "", ISS_OFF); IUFillSwitch(&StackModeS[STACK_ADDITIVE], "Additive", "", ISS_OFF); IUFillSwitch(&StackModeS[STACK_TAKE_DARK], "Take Dark", "", ISS_OFF); IUFillSwitch(&StackModeS[STACK_RESET_DARK], "Reset Dark", "", ISS_OFF); IUFillSwitchVector(&StackModeSP, StackModeS, NARRAY(StackModeS), getDeviceName(), "Stack", "", MAIN_CONTROL_TAB, IP_RW, ISR_1OFMANY, 0, IPS_IDLE); stackMode = 0; /* Inputs */ IUFillSwitchVector(&InputsSP, nullptr, 0, getDeviceName(), "V4L2_INPUT", "Inputs", CAPTURE_FORMAT, IP_RW, ISR_1OFMANY, 0, IPS_IDLE); /* Capture Formats */ IUFillSwitchVector(&CaptureFormatsSP, nullptr, 0, getDeviceName(), "V4L2_FORMAT", "Capture Format", CAPTURE_FORMAT, IP_RW, ISR_1OFMANY, 0, IPS_IDLE); /* Capture Sizes */ IUFillSwitchVector(&CaptureSizesSP, nullptr, 0, getDeviceName(), "V4L2_SIZE_DISCRETE", "Capture Size", CAPTURE_FORMAT, IP_RW, ISR_1OFMANY, 0, IPS_IDLE); IUFillNumberVector(&CaptureSizesNP, nullptr, 0, getDeviceName(), "V4L2_SIZE_STEP", "Capture Size", CAPTURE_FORMAT, IP_RW, 0, IPS_IDLE); /* Frame Rate */ IUFillSwitchVector(&FrameRatesSP, nullptr, 0, getDeviceName(), "V4L2_FRAMEINT_DISCRETE", "Frame Interval", CAPTURE_FORMAT, IP_RW, ISR_1OFMANY, 0, IPS_IDLE); IUFillNumberVector(&FrameRateNP, nullptr, 0, getDeviceName(), "V4L2_FRAMEINT_STEP", "Frame Interval", CAPTURE_FORMAT, IP_RW, 60, IPS_IDLE); /* Capture Colorspace */ IUFillText(&CaptureColorSpaceT[0], "Name", "", nullptr); IUFillText(&CaptureColorSpaceT[1], "YCbCr Encoding", "", nullptr); IUFillText(&CaptureColorSpaceT[2], "Quantization", "", nullptr); IUFillTextVector(&CaptureColorSpaceTP, CaptureColorSpaceT, NARRAY(CaptureColorSpaceT), getDeviceName(), "V4L2_COLORSPACE", "ColorSpace", IMAGE_INFO_TAB, IP_RO, 0, IPS_IDLE); /* Color Processing */ IUFillSwitch(&ColorProcessingS[0], "Quantization", "", ISS_ON); IUFillSwitch(&ColorProcessingS[1], "Color Conversion", "", ISS_OFF); IUFillSwitch(&ColorProcessingS[2], "Linearization", "", ISS_OFF); IUFillSwitchVector(&ColorProcessingSP, ColorProcessingS, NARRAY(ColorProcessingS), getDeviceName(), "V4L2_COLOR_PROCESSING", "Color Process", CAPTURE_FORMAT, IP_RW, ISR_NOFMANY, 0, IPS_IDLE); /* V4L2 Settings */ IUFillNumberVector(&ImageAdjustNP, nullptr, 0, getDeviceName(), "Image Adjustments", "", IMAGE_GROUP, IP_RW, 60, IPS_IDLE); PrimaryCCD.getCCDInfo()->p = IP_RW; PrimaryCCD.setMinMaxStep("CCD_EXPOSURE", "CCD_EXPOSURE_VALUE", 0.001, 3600, 1, false); if (!lx->initProperties(this)) LOG_WARN("Can not init Long Exposure"); SetCCDCapability(CCD_CAN_BIN | CCD_CAN_SUBFRAME | CCD_HAS_STREAMING | CCD_CAN_ABORT); v4l_base->setDeviceName(getDeviceName()); return true; } void V4L2_Driver::initCamBase() { v4l_base = new INDI::V4L2_Base(); } void V4L2_Driver::ISGetProperties(const char *dev) { if (dev != nullptr && strcmp(getDeviceName(), dev) != 0) return; INDI::CCD::ISGetProperties(dev); defineText(&PortTP); loadConfig(true, INDI::SP::DEVICE_PORT); if (isConnected()) { defineText(&camNameTP); defineSwitch(&ImageColorSP); defineSwitch(&InputsSP); defineSwitch(&CaptureFormatsSP); if (CaptureSizesSP.sp != nullptr) defineSwitch(&CaptureSizesSP); else if (CaptureSizesNP.np != nullptr) defineNumber(&CaptureSizesNP); if (FrameRatesSP.sp != nullptr) defineSwitch(&FrameRatesSP); else if (FrameRateNP.np != nullptr) defineNumber(&FrameRateNP); #ifdef WITH_V4L2_EXPERIMENTS defineSwitch(&ImageDepthSP); defineSwitch(&StackModeSP); defineSwitch(&ColorProcessingSP); defineText(&CaptureColorSpaceTP); #endif } } bool V4L2_Driver::updateProperties() { INDI::CCD::updateProperties(); if (isConnected()) { //ExposeTimeNP=getNumber("CCD_EXPOSURE"); //ExposeTimeN=ExposeTimeNP->np; CompressSP = getSwitch("CCD_COMPRESSION"); CompressS = CompressSP->sp; FrameNP = getNumber("CCD_FRAME"); FrameN = FrameNP->np; defineText(&camNameTP); getBasicData(); defineSwitch(&ImageColorSP); defineSwitch(&InputsSP); defineSwitch(&CaptureFormatsSP); if (CaptureSizesSP.sp != nullptr) defineSwitch(&CaptureSizesSP); else if (CaptureSizesNP.np != nullptr) defineNumber(&CaptureSizesNP); if (FrameRatesSP.sp != nullptr) defineSwitch(&FrameRatesSP); else if (FrameRateNP.np != nullptr) defineNumber(&FrameRateNP); #ifdef WITH_V4L2_EXPERIMENTS defineSwitch(&ImageDepthSP); defineSwitch(&StackModeSP); defineSwitch(&ColorProcessingSP); defineText(&CaptureColorSpaceTP); #endif SetCCDParams(V4LFrame->width, V4LFrame->height, V4LFrame->bpp, 5.6, 5.6); PrimaryCCD.setImageExtension("fits"); //v4l_base->setRecorder(Streamer->getRecorder()); if (v4l_base->isLXmodCapable()) lx->updateProperties(); return true; } else { unsigned int i; if (v4l_base->isLXmodCapable()) lx->updateProperties(); deleteProperty(camNameTP.name); deleteProperty(ImageColorSP.name); deleteProperty(InputsSP.name); deleteProperty(CaptureFormatsSP.name); if (CaptureSizesSP.sp != nullptr) deleteProperty(CaptureSizesSP.name); else if (CaptureSizesNP.np != nullptr) deleteProperty(CaptureSizesNP.name); if (FrameRatesSP.sp != nullptr) deleteProperty(FrameRatesSP.name); else if (FrameRateNP.np != nullptr) deleteProperty(FrameRateNP.name); deleteProperty(ImageAdjustNP.name); for (i = 0; i < v4loptions; i++) deleteProperty(Options[i].name); if (Options) free(Options); Options = nullptr; v4loptions = 0; #ifdef WITH_V4L2_EXPERIMENTS deleteProperty(ImageDepthSP.name); deleteProperty(StackModeSP.name); deleteProperty(ColorProcessingSP.name); deleteProperty(CaptureColorSpaceTP.name); #endif return true; } } bool V4L2_Driver::ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) { char errmsg[ERRMSGSIZ]; unsigned int iopt; /* ignore if not ours */ if (dev != nullptr && strcmp(getDeviceName(), dev) != 0) return true; /* Input */ if (strcmp(name, InputsSP.name) == 0) { //if ((StreamSP.s == IPS_BUSY) || (ExposeTimeNP->s == IPS_BUSY) || (RecordStreamSP.s == IPS_BUSY)) { if (PrimaryCCD.isExposing() || Streamer->isBusy()) { LOG_ERROR("Can not set input while capturing."); InputsSP.s = IPS_ALERT; IDSetSwitch(&InputsSP, nullptr); return false; } else { unsigned int inputindex, oldindex; oldindex = IUFindOnSwitchIndex(&InputsSP); IUResetSwitch(&InputsSP); IUUpdateSwitch(&InputsSP, states, names, n); inputindex = IUFindOnSwitchIndex(&InputsSP); if (v4l_base->setinput(inputindex, errmsg) == -1) { LOGF_INFO("ERROR (setinput): %s", errmsg); IUResetSwitch(&InputsSP); InputsSP.sp[oldindex].s = ISS_ON; InputsSP.s = IPS_ALERT; IDSetSwitch(&InputsSP, nullptr); return false; } deleteProperty(CaptureFormatsSP.name); v4l_base->getcaptureformats(&CaptureFormatsSP); defineSwitch(&CaptureFormatsSP); if (CaptureSizesSP.sp != nullptr) deleteProperty(CaptureSizesSP.name); else if (CaptureSizesNP.np != nullptr) deleteProperty(CaptureSizesNP.name); v4l_base->getcapturesizes(&CaptureSizesSP, &CaptureSizesNP); if (CaptureSizesSP.sp != nullptr) defineSwitch(&CaptureSizesSP); else if (CaptureSizesNP.np != nullptr) defineNumber(&CaptureSizesNP); InputsSP.s = IPS_OK; IDSetSwitch(&InputsSP, nullptr); LOGF_INFO("Capture input: %d. %s", inputindex, InputsSP.sp[inputindex].name); return true; } } /* Capture Format */ if (strcmp(name, CaptureFormatsSP.name) == 0) { //if ((StreamSP.s == IPS_BUSY) || (ExposeTimeNP->s == IPS_BUSY) || (RecordStreamSP.s == IPS_BUSY)) { if (PrimaryCCD.isExposing() || Streamer->isBusy()) { LOG_ERROR("Can not set format while capturing."); CaptureFormatsSP.s = IPS_ALERT; IDSetSwitch(&CaptureFormatsSP, nullptr); return false; } else { unsigned int index, oldindex; oldindex = IUFindOnSwitchIndex(&CaptureFormatsSP); IUResetSwitch(&CaptureFormatsSP); IUUpdateSwitch(&CaptureFormatsSP, states, names, n); index = IUFindOnSwitchIndex(&CaptureFormatsSP); if (v4l_base->setcaptureformat(*((unsigned int *)CaptureFormatsSP.sp[index].aux), errmsg) == -1) { LOGF_INFO("ERROR (setformat): %s", errmsg); IUResetSwitch(&CaptureFormatsSP); CaptureFormatsSP.sp[oldindex].s = ISS_ON; CaptureFormatsSP.s = IPS_ALERT; IDSetSwitch(&CaptureFormatsSP, nullptr); return false; } V4LFrame->bpp = v4l_base->getBpp(); PrimaryCCD.setBPP(V4LFrame->bpp); if (CaptureSizesSP.sp != nullptr) deleteProperty(CaptureSizesSP.name); else if (CaptureSizesNP.np != nullptr) deleteProperty(CaptureSizesNP.name); v4l_base->getcapturesizes(&CaptureSizesSP, &CaptureSizesNP); if (CaptureSizesSP.sp != nullptr) defineSwitch(&CaptureSizesSP); else if (CaptureSizesNP.np != nullptr) defineNumber(&CaptureSizesNP); CaptureFormatsSP.s = IPS_OK; #ifdef WITH_V4L2_EXPERIMENTS IUSaveText(&CaptureColorSpaceT[0], getColorSpaceName(&v4l_base->fmt)); IUSaveText(&CaptureColorSpaceT[1], getYCbCrEncodingName(&v4l_base->fmt)); IUSaveText(&CaptureColorSpaceT[2], getQuantizationName(&v4l_base->fmt)); IDSetText(&CaptureColorSpaceTP, nullptr); #endif //direct_record=recorder->setpixelformat(v4l_base->fmt.fmt.pix.pixelformat); INDI_PIXEL_FORMAT pixelFormat; uint8_t pixelDepth=8; if (getPixelFormat(v4l_base->fmt.fmt.pix.pixelformat, pixelFormat, pixelDepth)) Streamer->setPixelFormat(pixelFormat, pixelDepth); IDSetSwitch(&CaptureFormatsSP, "Capture format: %d. %s", index, CaptureFormatsSP.sp[index].name); return true; } } /* Capture Size (Discrete) */ if (strcmp(name, CaptureSizesSP.name) == 0) { //if ((StreamSP.s == IPS_BUSY) || (ExposeTimeNP->s == IPS_BUSY) || (RecordStreamSP.s == IPS_BUSY)) { if (PrimaryCCD.isExposing() || Streamer->isBusy()) { LOG_ERROR("Can not set capture size while capturing."); CaptureSizesSP.s = IPS_ALERT; IDSetSwitch(&CaptureSizesSP, nullptr); return false; } else { unsigned int index, w, h; IUUpdateSwitch(&CaptureSizesSP, states, names, n); index = IUFindOnSwitchIndex(&CaptureSizesSP); sscanf(CaptureSizesSP.sp[index].name, "%dx%d", &w, &h); if (v4l_base->setcapturesize(w, h, errmsg) == -1) { LOGF_INFO("ERROR (setsize): %s", errmsg); CaptureSizesSP.s = IPS_ALERT; IDSetSwitch(&CaptureSizesSP, nullptr); return false; } if (FrameRatesSP.sp != nullptr) deleteProperty(FrameRatesSP.name); else if (FrameRateNP.np != nullptr) deleteProperty(FrameRateNP.name); v4l_base->getframerates(&FrameRatesSP, &FrameRateNP); if (FrameRatesSP.sp != nullptr) defineSwitch(&FrameRatesSP); else if (FrameRateNP.np != nullptr) defineNumber(&FrameRateNP); PrimaryCCD.setFrame(0, 0, w, h); V4LFrame->width = w; V4LFrame->height = h; PrimaryCCD.setResolution(w, h); updateFrameSize(); Streamer->setSize(w, h); CaptureSizesSP.s = IPS_OK; IDSetSwitch(&CaptureSizesSP, "Capture size (discrete): %d. %s", index, CaptureSizesSP.sp[index].name); return true; } } /* Frame Rate (Discrete) */ if (strcmp(name, FrameRatesSP.name) == 0) { if (PrimaryCCD.isExposing() || Streamer->isBusy()) { LOG_ERROR("Can not change frame rate while capturing."); FrameRatesSP.s = IPS_ALERT; IDSetSwitch(&FrameRatesSP, nullptr); return false; } unsigned int index; struct v4l2_fract frate; IUUpdateSwitch(&FrameRatesSP, states, names, n); index = IUFindOnSwitchIndex(&FrameRatesSP); sscanf(FrameRatesSP.sp[index].name, "%d/%d", &frate.numerator, &frate.denominator); if ((v4l_base->*(v4l_base->setframerate))(frate, errmsg) == -1) { LOGF_INFO("ERROR (setframerate): %s", errmsg); FrameRatesSP.s = IPS_ALERT; IDSetSwitch(&FrameRatesSP, nullptr); return false; } FrameRatesSP.s = IPS_OK; IDSetSwitch(&FrameRatesSP, "Frame Period (discrete): %d. %s", index, FrameRatesSP.sp[index].name); return true; } /* Image Type */ if (strcmp(name, ImageColorSP.name) == 0) { if (Streamer->isRecording()) { LOG_WARN("Can not set Image type (GRAY/COLOR) while recording."); return false; } IUResetSwitch(&ImageColorSP); IUUpdateSwitch(&ImageColorSP, states, names, n); ImageColorSP.s = IPS_OK; if (ImageColorS[IMAGE_GRAYSCALE].s == ISS_ON) { //PrimaryCCD.setBPP(8); PrimaryCCD.setNAxis(2); } else { //PrimaryCCD.setBPP(32); //PrimaryCCD.setBPP(8); PrimaryCCD.setNAxis(3); } updateFrameSize(); #if 0 INDI_PIXEL_FORMAT pixelFormat; uint8_t pixelDepth=8; if (getPixelFormat(v4l_base->fmt.fmt.pix.pixelformat, pixelFormat, pixelDepth)) Streamer->setPixelFormat(pixelFormat, pixelDepth); #endif Streamer->setPixelFormat((ImageColorS[IMAGE_GRAYSCALE].s == ISS_ON) ? INDI_MONO : INDI_RGB, 8); IDSetSwitch(&ImageColorSP, nullptr); return true; } /* Image Depth */ if (strcmp(name, ImageDepthSP.name) == 0) { if (Streamer->isRecording()) { LOG_WARN("Can not set Image depth (8/16bits) while recording."); return false; } IUResetSwitch(&ImageDepthSP); IUUpdateSwitch(&ImageDepthSP, states, names, n); ImageDepthSP.s = IPS_OK; if (ImageDepthS[0].s == ISS_ON) { PrimaryCCD.setBPP(8); } else { PrimaryCCD.setBPP(16); } IDSetSwitch(&ImageDepthSP, nullptr); return true; } /* Stacking Mode */ if (strcmp(name, StackModeSP.name) == 0) { IUResetSwitch(&StackModeSP); IUUpdateSwitch(&StackModeSP, states, names, n); StackModeSP.s = IPS_OK; stackMode = IUFindOnSwitchIndex(&StackModeSP); if (stackMode == STACK_RESET_DARK) { if (V4LFrame->darkFrame != nullptr) { free(V4LFrame->darkFrame); V4LFrame->darkFrame = nullptr; } } IDSetSwitch(&StackModeSP, "Setting Stacking Mode: %s", StackModeS[stackMode].name); return true; } /* V4L2 Options/Menus */ for (iopt = 0; iopt < v4loptions; iopt++) if (strcmp(Options[iopt].name, name) == 0) break; if (iopt < v4loptions) { unsigned int ctrl_id, optindex, ctrlindex; LOGF_DEBUG("Toggle switch %s=%s", Options[iopt].name, Options[iopt].label); Options[iopt].s = IPS_IDLE; IUResetSwitch(&Options[iopt]); if (IUUpdateSwitch(&Options[iopt], states, names, n) < 0) return false; optindex = IUFindOnSwitchIndex(&Options[iopt]); if (Options[iopt].sp[optindex].aux != nullptr) ctrlindex = *(unsigned int *)(Options[iopt].sp[optindex].aux); else ctrlindex = optindex; ctrl_id = (*((unsigned int *)Options[iopt].aux)); LOGF_DEBUG(" On switch is (%d) %s=\"%s\", ctrl_id = 0x%X ctrl_index=%d", optindex, Options[iopt].sp[optindex].name, Options[iopt].sp[optindex].label, ctrl_id, ctrlindex); if (v4l_base->setOPTControl(ctrl_id, ctrlindex, errmsg) < 0) { if (Options[iopt].nsp == 1) // button { Options[iopt].sp[optindex].s = ISS_OFF; } Options[iopt].s = IPS_ALERT; IDSetSwitch(&Options[iopt], nullptr); LOGF_ERROR("Unable to adjust setting. %s", errmsg); return false; } if (Options[iopt].nsp == 1) // button { Options[iopt].sp[optindex].s = ISS_OFF; } Options[iopt].s = IPS_OK; IDSetSwitch(&Options[iopt], nullptr); return true; } /* ColorProcessing */ if (strcmp(name, ColorProcessingSP.name) == 0) { if (ImageColorS[IMAGE_GRAYSCALE].s == ISS_ON) { IUUpdateSwitch(&ColorProcessingSP, states, names, n); v4l_base->setColorProcessing(ColorProcessingS[0].s == ISS_ON, ColorProcessingS[1].s == ISS_ON, ColorProcessingS[2].s == ISS_ON); ColorProcessingSP.s = IPS_OK; IDSetSwitch(&ColorProcessingSP, nullptr); V4LFrame->bpp = v4l_base->getBpp(); PrimaryCCD.setBPP(V4LFrame->bpp); PrimaryCCD.setBPP(V4LFrame->bpp); updateFrameSize(); return true; } else { LOG_WARN("No color processing in color mode "); return false; } } lx->ISNewSwitch(dev, name, states, names, n); return INDI::CCD::ISNewSwitch(dev, name, states, names, n); } bool V4L2_Driver::ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n) { IText *tp; /* ignore if not ours */ if (dev != nullptr && strcmp(getDeviceName(), dev) != 0) return true; if (strcmp(name, PortTP.name) == 0) { PortTP.s = IPS_OK; tp = IUFindText(&PortTP, names[0]); if (!tp) return false; IUSaveText(tp, texts[0]); IDSetText(&PortTP, nullptr); return true; } lx->ISNewText(dev, name, texts, names, n); return INDI::CCD::ISNewText(dev, name, texts, names, n); } bool V4L2_Driver::ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) { char errmsg[ERRMSGSIZ]; /* ignore if not ours */ if (dev != nullptr && strcmp(getDeviceName(), dev) != 0) return true; /* Capture Size (Step/Continuous) */ if (strcmp(name, CaptureSizesNP.name) == 0) { if (PrimaryCCD.isExposing() || Streamer->isBusy()) { LOG_ERROR("Can not set capture size while capturing."); CaptureSizesNP.s = IPS_BUSY; IDSetNumber(&CaptureSizesNP, nullptr); return false; } else { unsigned int sizes[2], w = 0, h = 0; double rsizes[2]; if (strcmp(names[0], "Width") == 0) { sizes[0] = values[0]; sizes[1] = values[1]; } else { sizes[0] = values[1]; sizes[1] = values[0]; } if (v4l_base->setcapturesize(sizes[0], sizes[1], errmsg) == -1) { LOGF_INFO("ERROR (setsize): %s", errmsg); CaptureSizesNP.s = IPS_ALERT; IDSetNumber(&CaptureSizesNP, nullptr); return false; } if (strcmp(names[0], "Width") == 0) { w = v4l_base->getWidth(); rsizes[0] = (double)w; h = v4l_base->getHeight(); rsizes[1] = (double)h; } else { w = v4l_base->getWidth(); rsizes[1] = (double)w; h = v4l_base->getHeight(); rsizes[0] = (double)h; } PrimaryCCD.setFrame(0, 0, w, h); IUUpdateNumber(&CaptureSizesNP, rsizes, names, n); V4LFrame->width = w; V4LFrame->height = h; PrimaryCCD.setResolution(w, h); CaptureSizesNP.s = IPS_OK; updateFrameSize(); Streamer->setSize(w, h); IDSetNumber(&CaptureSizesNP, "Capture size (step/cont): %dx%d", w, h); return true; } } if (strcmp(ImageAdjustNP.name, name) == 0) { ImageAdjustNP.s = IPS_IDLE; if (IUUpdateNumber(&ImageAdjustNP, values, names, n) < 0) return false; for (int i = 0; i < ImageAdjustNP.nnp; i++) { unsigned int const ctrl_id = *((unsigned int *)ImageAdjustNP.np[i].aux0); double const value = ImageAdjustNP.np[i].value; LOGF_DEBUG(" Setting %s (%s) to %f, ctrl_id = 0x%X", ImageAdjustNP.np[i].name, ImageAdjustNP.np[i].label, value, ctrl_id); if (v4l_base->setINTControl(ctrl_id, ImageAdjustNP.np[i].value, errmsg) < 0) { /* Some controls may become read-only depending on selected options */ LOGF_WARN("Unable to adjust %s (ctrl_id = 0x%X)", ImageAdjustNP.np[i].label, ctrl_id); } /* Some controls may have been ajusted by the driver */ /* a read is mandatory as VIDIOC_S_CTRL is write only and does not return the actual new value */ v4l_base->getControl(ctrl_id, &(ImageAdjustNP.np[i].value), errmsg); /* Warn the client if the control returned another value than what was set */ if(value != ImageAdjustNP.np[i].value) { LOGF_WARN("Control %s set to %f returned %f (ctrl_id = 0x%X)", ImageAdjustNP.np[i].label, value, ImageAdjustNP.np[i].value, ctrl_id); } } ImageAdjustNP.s = IPS_OK; IDSetNumber(&ImageAdjustNP, nullptr); return true; } return INDI::CCD::ISNewNumber(dev, name, values, names, n); } bool V4L2_Driver::StartExposure(float duration) { /* Clicking the "Expose" set button while an exposure is running arrives here. * Now that V4L2 CCD has the option to abort, this will properly abort the exposure. * If CAN_ABORT is not set, we have to tell the caller we're busy until the end of this exposure. * If we don't, PrimaryCCD will stop exposing nonetheless and we won't be able to restart an exposure. */ { if (Streamer->isBusy()) { LOG_ERROR("Cannot start new exposure while streamer is busy, stop streaming first"); return !(GetCCDCapability() & CCD_CAN_ABORT); } if (is_capturing) { LOGF_ERROR( "Cannot start new exposure until the current one completes (%.3f seconds left).", getRemainingExposure()); return !(GetCCDCapability() & CCD_CAN_ABORT); } } if (setShutter(duration)) { V4LFrame->expose = duration; PrimaryCCD.setExposureDuration(duration); if (!lx->isEnabled() || lx->getLxmode() == LXSERIAL) start_capturing(false); /* Update exposure duration in client */ /* FIXME: exposure update timer has period hardcoded 1 second */ if (is_capturing && 1.0f < duration) { if (-1 != stdtimer) IERmTimer(stdtimer); stdtimer = IEAddTimer(1000, (IE_TCF *)stdtimerCallback, this); } else stdtimer = -1; } return is_capturing; } bool V4L2_Driver::setShutter(double duration) { if (lx->isEnabled()) { LOGF_INFO("Using long exposure mode for %.3f sec frame.", duration); if (startlongexposure(duration)) { LOGF_INFO("Started %.3f-second long exposure.", duration); return true; } else { DEBUGF(INDI::Logger::DBG_WARNING, "Unable to start %.3f-second long exposure, falling back to auto exposure", duration); return false; } } else if (setManualExposure(duration)) { exposure_duration.tv_sec = (long) duration; exposure_duration.tv_usec = (long) ((duration - (double) exposure_duration.tv_sec) * 1000000.0f); gettimeofday(&capture_start, nullptr); frameCount = 0; subframeCount = 0; LOGF_INFO("Started %.3f-second manual exposure.", duration); return true; } else { LOGF_WARN("Failed %.3f-second manual exposure, no adequate control is registered.", duration); return false; } } bool V4L2_Driver::setManualExposure(double duration) { if (nullptr == AbsExposureN) { LOGF_ERROR("Failed exposing, the absolute exposure duration control is undefined", ""); return false; } char errmsg[MAXRBUF]; /* Manual mode should be set before changing Exposure (Auto), if possible. * In some cases there might be no control available, so don't fail and try to continue. */ if (ManualExposureSP) { if (ManualExposureSP->sp[0].s == ISS_OFF) { ManualExposureSP->sp[0].s = ISS_ON; ManualExposureSP->sp[1].s = ISS_OFF; ManualExposureSP->s = IPS_IDLE; unsigned int const ctrlindex = ManualExposureSP->sp[0].aux ? *(unsigned int *)(ManualExposureSP->sp[0].aux) : 0; unsigned int const ctrl_id = (*((unsigned int *)ManualExposureSP->aux)); if (v4l_base->setOPTControl(ctrl_id, ctrlindex, errmsg) < 0) { ManualExposureSP->sp[0].s = ISS_OFF; ManualExposureSP->sp[1].s = ISS_ON; ManualExposureSP->s = IPS_ALERT; IDSetSwitch(ManualExposureSP, nullptr); LOGF_ERROR("Unable to adjust manual/auto exposure control. %s", errmsg); return false; } ManualExposureSP->s = IPS_OK; IDSetSwitch(ManualExposureSP, nullptr); } } else { LOGF_ERROR("Failed switching to manual exposure, control is unavailable", ""); /* Don't fail, let the driver try to set the absolute duration, we'll see what happens */ /* return false; */ } /* N.B. Check how this differs from one camera to another. This is just a proof of concept for now */ /* With DMx 21AU04.AS, exposing twice with the same duration causes an incomplete frame to pop in the buffer list * This can be worked around by verifying the buffer size, but it won't work for anything else than Y8/Y16, so set * exposure unconditionally */ /*if (duration * 10000 != AbsExposureN->value)*/ // INT control for manual exposure duration is an integer in 1/10000 seconds long const ticks = lround(duration * 10000.0f); if (AbsExposureN->min <= ticks && ticks <= AbsExposureN->max) { double const restoredValue = AbsExposureN->value; AbsExposureN->value = ticks; LOGF_DEBUG("%.3f-second exposure translates to %ld 1/10,000th-second device ticks.", duration, ticks); unsigned int const ctrl_id = *((unsigned int *)AbsExposureN->aux0); if (v4l_base->setINTControl(ctrl_id, AbsExposureN->value, errmsg) < 0) { ImageAdjustNP.s = IPS_ALERT; AbsExposureN->value = restoredValue; IDSetNumber(&ImageAdjustNP, "Failed requesting %.3f-second exposure to the driver (%s).", duration, errmsg); return false; } ImageAdjustNP.s = IPS_OK; IDSetNumber(&ImageAdjustNP, nullptr); } else { LOGF_WARN("Failed %.3f-second manual exposure, out of device bounds [%.3f,%.3f].", duration, (double) AbsExposureN->min / 10000.0f, (double) AbsExposureN->max / 10000.0f); return false; } return true; } /** \internal Timer callback. * * This provides a very rough estimation of the remaining exposure to the client. */ void V4L2_Driver::stdtimerCallback(void *userpointer) { V4L2_Driver *p = (V4L2_Driver *)userpointer; float remaining = p->getRemainingExposure(); //DEBUGF(INDI::Logger::DBG_SESSION,"Exposure running, %f seconds left...", remaining); if (1.0f < remaining) p->stdtimer = IEAddTimer(1000, (IE_TCF *)stdtimerCallback, userpointer); else p->stdtimer = -1; p->PrimaryCCD.setExposureLeft(remaining); } bool V4L2_Driver::start_capturing(bool do_stream) { // FIXME Must migrate completely to Stream // The class shouldn't be making calls to encoder/recorder directly // Stream? Yes or No // Direct Record? INDI_UNUSED(do_stream); if (Streamer->isBusy()) { LOG_WARN("Cannot start exposure while streaming is in progress"); return false; } if (is_capturing) { LOGF_WARN("Cannot start exposure while another is in progress (%.3f seconds left)", getRemainingExposure()); return false; } char errmsg[ERRMSGSIZ]; if (v4l_base->start_capturing(errmsg)) { LOGF_WARN("V4L2 base failed starting capture (%s)", errmsg); return false; } //if (do_stream) //v4l_base->doRecord(Streamer->isDirectRecording()); is_capturing = true; return true; } bool V4L2_Driver::stop_capturing() { if (!is_capturing) { LOG_WARN("No exposure or streaming in progress"); return true; } if (!Streamer->isBusy() && 0.0f < getRemainingExposure()) { LOGF_WARN("Stopping running exposure %.3f seconds before completion", getRemainingExposure()); } // FIXME what to do with doRecord? //if(Streamer->isDirectRecording()) //v4l_base->doRecord(false); char errmsg[ERRMSGSIZ]; if (v4l_base->stop_capturing(errmsg)) { LOGF_WARN("V4L2 base failed stopping capture (%s)", errmsg); } is_capturing = false; return true; } bool V4L2_Driver::startlongexposure(double timeinsec) { lxtimer = IEAddTimer((int)(timeinsec * 1000.0), (IE_TCF *)lxtimerCallback, this); v4l_base->setlxstate(LX_ACCUMULATING); return (lx->startLx()); } void V4L2_Driver::lxtimerCallback(void *userpointer) { V4L2_Driver *p = (V4L2_Driver *)userpointer; p->lx->stopLx(); if (p->lx->getLxmode() == LXSERIAL) { p->v4l_base->setlxstate(LX_TRIGGERED); } else { p->v4l_base->setlxstate(LX_ACTIVE); } IERmTimer(p->lxtimer); if (!p->v4l_base->isstreamactive()) p->start_capturing(false); // jump to new/updateFrame //p->v4l_base->start_capturing(errmsg); // jump to new/updateFrame } bool V4L2_Driver::UpdateCCDBin(int hor, int ver) { if (ImageColorS[IMAGE_COLOR].s == ISS_ON) { if (hor == 1 && ver == 1) { PrimaryCCD.setBin(hor, ver); Streamer->setSize(PrimaryCCD.getSubW(), PrimaryCCD.getSubH()); return true; } LOG_WARN("Binning color frames is currently not supported."); return false; } if (hor != ver) { LOGF_WARN("Cannot accept asymmetrical binning %dx%d.", hor, ver); return false; } if (hor != 1 && hor != 2 && hor != 4) { LOG_WARN("Can only accept 1x1, 2x2, and 4x4 binning."); return false; } if (Streamer->isBusy()) { LOG_WARN("Cannot change binning while streaming/recording."); return false; } PrimaryCCD.setBin(hor, ver); Streamer->setSize(PrimaryCCD.getSubW()/hor, PrimaryCCD.getSubH()/ver); return true; } bool V4L2_Driver::UpdateCCDFrame(int x, int y, int w, int h) { char errmsg[ERRMSGSIZ]; //LOGF_INFO("calling updateCCDFrame: %d %d %d %d", x, y, w, h); //IDLog("calling updateCCDFrame: %d %d %d %d\n", x, y, w, h); if (v4l_base->setcroprect(x, y, w, h, errmsg) != -1) { struct v4l2_rect crect; crect = v4l_base->getcroprect(); V4LFrame->width = crect.width; V4LFrame->height = crect.height; PrimaryCCD.setFrame(x, y, w, h); updateFrameSize(); Streamer->setSize(w, h); return true; } else { LOGF_INFO("ERROR (setcroprect): %s", errmsg); } return false; } void V4L2_Driver::newFrame(void *p) { ((V4L2_Driver *)(p))->newFrame(); } void V4L2_Driver::stackFrame() { if (!V4LFrame->stackedFrame) { float *src = nullptr, *dest = nullptr; V4LFrame->stackedFrame = (float *)malloc(sizeof(float) * v4l_base->getWidth() * v4l_base->getHeight()); src = v4l_base->getLinearY(); dest = V4LFrame->stackedFrame; for (int i = 0; i < v4l_base->getWidth() * v4l_base->getHeight(); i++) *dest++ = *src++; subframeCount = 1; } else { float *src = nullptr, *dest = nullptr; src = v4l_base->getLinearY(); dest = V4LFrame->stackedFrame; for (int i = 0; i < v4l_base->getWidth() * v4l_base->getHeight(); i++) { *dest++ += *src++; } subframeCount += 1; } } struct timeval V4L2_Driver::getElapsedExposure() const { struct timeval now = { .tv_sec = 0, .tv_usec = 0 }, elapsed = { .tv_sec = 0, .tv_usec = 0 }; gettimeofday(&now, nullptr); timersub(&now, &capture_start, &elapsed); return elapsed; } float V4L2_Driver::getRemainingExposure() const { struct timeval elapsed = getElapsedExposure(), remaining = { .tv_sec = 0, .tv_usec = 0 }; timersub(&exposure_duration,&elapsed,&remaining); return (float) remaining.tv_sec + (float) remaining.tv_usec / 1000000.0f; } void V4L2_Driver::newFrame() { if (Streamer->isBusy()) { int width = v4l_base->getWidth(); int height = v4l_base->getHeight(); int bpp = v4l_base->getBpp(); int dbpp = 8; int totalBytes = 0; unsigned char *buffer = nullptr; if (ImageColorS[IMAGE_GRAYSCALE].s == ISS_ON) { V4LFrame->Y = v4l_base->getY(); totalBytes = width * height * (dbpp / 8); buffer = V4LFrame->Y; } else { V4LFrame->RGB24Buffer = v4l_base->getRGBBuffer(); totalBytes = width * height * (dbpp / 8) * 3; buffer = V4LFrame->RGB24Buffer; } // downscale Y10 Y12 Y16 if (bpp > dbpp) { unsigned short *src = (unsigned short *)buffer; unsigned char *dest = buffer; unsigned char shift = 0; if (bpp < 16) { switch (bpp) { case 10: shift = 2; break; case 12: shift = 4; break; } for (int i = 0; i < totalBytes; i++) { *dest++ = *(src++) >> shift; } } else { unsigned char *src = (unsigned char *)buffer + 1; // Y16 is little endian for (int i = 0; i < totalBytes; i++) { *dest++ = *src; src += 2; } } } if (PrimaryCCD.getBinX() > 1) { memcpy(PrimaryCCD.getFrameBuffer(), buffer, totalBytes); PrimaryCCD.binFrame(); Streamer->newFrame(PrimaryCCD.getFrameBuffer(), frameBytes/PrimaryCCD.getBinX()); } else Streamer->newFrame(buffer, frameBytes); return; } if (PrimaryCCD.isExposing()) { // Stack Mono frames if ((stackMode) && !(lx->isEnabled()) && !(ImageColorS[1].s == ISS_ON)) { stackFrame(); } struct timeval const current_exposure = getElapsedExposure(); if ((stackMode) && !(lx->isEnabled()) && !(ImageColorS[1].s == ISS_ON) && (timercmp(¤t_exposure, &exposure_duration, <))) return; // go on stacking //IDLog("Copying frame.\n"); if (ImageColorS[IMAGE_GRAYSCALE].s == ISS_ON) { if (!stackMode) { unsigned char *src, *dest; src = v4l_base->getY(); dest = (unsigned char *)PrimaryCCD.getFrameBuffer(); memcpy(dest, src, frameBytes); //for (i=0; i< frameBytes; i++) //*(dest++) = *(src++); PrimaryCCD.binFrame(); } else { float *src = V4LFrame->stackedFrame; if ((stackMode != STACK_TAKE_DARK) && (V4LFrame->darkFrame != nullptr)) { float *dark = V4LFrame->darkFrame; for (int i = 0; i < v4l_base->getWidth() * v4l_base->getHeight(); i++) { if (*src > *dark) *src -= *dark; else *src = 0.0; src++; dark++; } src = V4LFrame->stackedFrame; } //IDLog("Copying stack frame from %p to %p.\n", src, dest); if (stackMode == STACK_MEAN) { if (ImageDepthS[0].s == ISS_ON) { // depth 8 bits unsigned char *dest = (unsigned char *)PrimaryCCD.getFrameBuffer(); for (int i = 0; i < v4l_base->getWidth() * v4l_base->getHeight(); i++) *dest++ = (unsigned char)((*src++ * 255) / subframeCount); } else { // depth 16 bits unsigned short *dest = (unsigned short *)PrimaryCCD.getFrameBuffer(); for (int i = 0; i < v4l_base->getWidth() * v4l_base->getHeight(); i++) *dest++ = (unsigned short)((*src++ * 65535) / subframeCount); } free(V4LFrame->stackedFrame); V4LFrame->stackedFrame = nullptr; } else if (stackMode == STACK_ADDITIVE) { if (ImageDepthS[0].s == ISS_ON) { // depth 8 bits unsigned char *dest = (unsigned char *)PrimaryCCD.getFrameBuffer(); for (int i = 0; i < v4l_base->getWidth() * v4l_base->getHeight(); i++) *dest++ = (unsigned char)((*src++ * 255)); } else { // depth 16 bits unsigned short *dest = (unsigned short *)PrimaryCCD.getFrameBuffer(); for (int i = 0; i < v4l_base->getWidth() * v4l_base->getHeight(); i++) *dest++ = (unsigned short)((*src++ * 65535)); } free(V4LFrame->stackedFrame); V4LFrame->stackedFrame = nullptr; } else if (stackMode == STACK_TAKE_DARK) { if (V4LFrame->darkFrame != nullptr) free(V4LFrame->darkFrame); V4LFrame->darkFrame = V4LFrame->stackedFrame; V4LFrame->stackedFrame = nullptr; src = V4LFrame->darkFrame; if (ImageDepthS[0].s == ISS_ON) { // depth 8 bits unsigned char *dest = (unsigned char *)PrimaryCCD.getFrameBuffer(); for (int i = 0; i < v4l_base->getWidth() * v4l_base->getHeight(); i++) *dest++ = (unsigned char)((*src++ * 255)); } else { // depth 16 bits unsigned short *dest = (unsigned short *)PrimaryCCD.getFrameBuffer(); for (int i = 0; i < v4l_base->getWidth() * v4l_base->getHeight(); i++) *dest++ = (unsigned short)((*src++ * 65535)); } } } } else { // Binning not supported in color images for now unsigned char *src = v4l_base->getRGBBuffer(); unsigned char *dest = PrimaryCCD.getFrameBuffer(); // We have RGB RGB RGB data but for FITS file we need each color in separate plane. i.e. RRR GGG BBB ..etc unsigned char *red = dest; unsigned char *green = dest + v4l_base->getWidth() * v4l_base->getHeight() * (v4l_base->getBpp() / 8); unsigned char *blue = dest + v4l_base->getWidth() * v4l_base->getHeight() * (v4l_base->getBpp() / 8) * 2; for (int i = 0; i < (int)frameBytes; i += 3) { *(red++) = *(src + i); *(green++) = *(src + i + 1); *(blue++) = *(src + i + 2); } // TODO NO BINNING YET for color frames. We can bin each plane above separately it should be fine //PrimaryCCD.binFrame(); } //IDLog("Copy frame finished.\n"); frameCount += 1; if (lx->isEnabled()) { //if (!is_streaming && !is_recording) if (Streamer->isBusy() == false) stop_capturing(); LOGF_INFO("Capture of LX frame took %ld.%06ld seconds.", current_exposure.tv_sec, current_exposure.tv_usec); ExposureComplete(&PrimaryCCD); //PrimaryCCD.setFrameBufferSize(frameBytes); //} } else { //if (!is_streaming && !is_recording) stop_capturing(); if (Streamer->isBusy() == false) stop_capturing(); else IDLog("%s: streamer is busy, continue capturing\n", __FUNCTION__); LOGF_INFO("Capture of one frame (%d stacked frames) took %ld.%06ld seconds.", subframeCount, current_exposure.tv_sec, current_exposure.tv_usec); ExposureComplete(&PrimaryCCD); //PrimaryCCD.setFrameBufferSize(frameBytes); } } else { /* If we arrive here, PrimaryCCD is not exposing anymore, we can't forward the frame and we can't be aborted neither, thus abort the exposure right now. * That issue can be reproduced when clicking the "Set" button on the "Main Control" tab while an exposure is running. * Note that the patch in StartExposure returning busy instead of error prevents the flow from coming here, so now it's only a safeguard. */ IDLog("%s: frame received while not exposing, force-aborting capture\n", __FUNCTION__); AbortExposure(); } } bool V4L2_Driver::AbortExposure() { if (lx->isEnabled()) { lx->stopLx(); return true; } else if (!Streamer->isBusy()) { if (-1 != stdtimer) IERmTimer(stdtimer); return stop_capturing(); } LOG_WARN("Cannot abort exposure while video streamer is busy, stop streaming first"); return false; } bool V4L2_Driver::Connect() { char errmsg[ERRMSGSIZ]; if (!isConnected()) { if (v4l_base->connectCam(PortT[0].text, errmsg) < 0) { LOGF_ERROR("Error: unable to open device. %s", errmsg); return false; } /* Sucess! */ LOG_INFO("V4L2 CCD Device is online. Initializing properties."); v4l_base->registerCallback(newFrame, this); lx->setCamerafd(v4l_base->fd); if (!(strcmp((const char *)v4l_base->cap.driver, "pwc"))) DEBUG(INDI::Logger::DBG_SESSION, "To use LED Long exposure mode with recent kernels, see https://code.google.com/p/pwc-lxled/"); } return true; } bool V4L2_Driver::Disconnect() { if (isConnected()) { v4l_base->disconnectCam(PrimaryCCD.isExposing() || Streamer->isBusy()); if (PrimaryCCD.isExposing() || Streamer->isBusy()) Streamer->close(); } return true; } const char *V4L2_Driver::getDefaultName() { return (const char *)"V4L2 CCD"; } /* Retrieves basic data from the device upon connection.*/ void V4L2_Driver::getBasicData() { //int xmax, ymax, xmin, ymin; unsigned int w, h; int inputindex = -1, formatindex = -1; struct v4l2_fract frate; v4l_base->getinputs(&InputsSP); v4l_base->getcaptureformats(&CaptureFormatsSP); v4l_base->getcapturesizes(&CaptureSizesSP, &CaptureSizesNP); v4l_base->getframerates(&FrameRatesSP, &FrameRateNP); w = v4l_base->getWidth(); h = v4l_base->getHeight(); V4LFrame->width = w; V4LFrame->height = h; V4LFrame->bpp = v4l_base->getBpp(); inputindex = IUFindOnSwitchIndex(&InputsSP); formatindex = IUFindOnSwitchIndex(&CaptureFormatsSP); frate = (v4l_base->*(v4l_base->getframerate))(); if (inputindex >= 0 && formatindex >= 0) LOGF_INFO("Found intial Input \"%s\", Format \"%s\", Size %dx%d, Frame interval %d/%ds", InputsSP.sp[inputindex].name, CaptureFormatsSP.sp[formatindex].name, w, h, frate.numerator, frate.denominator); else LOGF_INFO("Found intial size %dx%d, frame interval %d/%ds", w, h, frate.numerator, frate.denominator); IUSaveText(&camNameT[0], v4l_base->getDeviceName()); IDSetText(&camNameTP, nullptr); #ifdef WITH_V4L2_EXPERIMENTS IUSaveText(&CaptureColorSpaceT[0], getColorSpaceName(&v4l_base->fmt)); IUSaveText(&CaptureColorSpaceT[1], getYCbCrEncodingName(&v4l_base->fmt)); IUSaveText(&CaptureColorSpaceT[2], getQuantizationName(&v4l_base->fmt)); IDSetText(&CaptureColorSpaceTP, nullptr); #endif if (Options) free(Options); Options = nullptr; v4loptions = 0; updateV4L2Controls(); PrimaryCCD.setResolution(w, h); PrimaryCCD.setFrame(0, 0, w, h); PrimaryCCD.setBPP(V4LFrame->bpp); updateFrameSize(); //direct_record=recorder->setpixelformat(v4l_base->fmt.fmt.pix.pixelformat); //recorder->setsize(w, h); INDI_PIXEL_FORMAT pixelFormat; uint8_t pixelDepth=8; if (getPixelFormat(v4l_base->fmt.fmt.pix.pixelformat, pixelFormat, pixelDepth)) Streamer->setPixelFormat(pixelFormat, pixelDepth); Streamer->setSize(w, h); } void V4L2_Driver::updateV4L2Controls() { unsigned int i; LOG_DEBUG("Enumerating V4L2 controls..."); // #1 Query for INTEGER controls, and fill up the structure free(ImageAdjustNP.np); ImageAdjustNP.nnp = 0; //if (v4l_base->queryINTControls(&ImageAdjustNP) > 0) //defineNumber(&ImageAdjustNP); v4l_base->enumerate_ext_ctrl(); useExtCtrl = false; if (v4l_base->queryExtControls(&ImageAdjustNP, &v4ladjustments, &Options, &v4loptions, getDeviceName(), IMAGE_BOOLEAN)) useExtCtrl = true; else v4l_base->queryControls(&ImageAdjustNP, &v4ladjustments, &Options, &v4loptions, getDeviceName(), IMAGE_BOOLEAN); if (v4ladjustments > 0) { LOGF_DEBUG("Found %d V4L2 adjustments", v4ladjustments); defineNumber(&ImageAdjustNP); for (int i = 0; i < ImageAdjustNP.nnp; i++) { if (strcmp(ImageAdjustNP.np[i].label, "Exposure (Absolute)") == 0 || strcmp(ImageAdjustNP.np[i].label, "Exposure Time, Absolute") == 0) { AbsExposureN = ImageAdjustNP.np + i; LOGF_DEBUG("- %s (used for absolute exposure duration)", ImageAdjustNP.np[i].label); } else LOGF_DEBUG("- %s", ImageAdjustNP.np[i].label); } } LOGF_DEBUG("Found %d V4L2 options", v4loptions); for (i = 0; i < v4loptions; i++) { defineSwitch(&Options[i]); if (strcmp(Options[i].label, "Exposure, Auto") == 0 || strcmp(Options[i].label, "Auto Exposure") == 0) { ManualExposureSP = Options + i; LOGF_DEBUG("- %s (used for manual/auto exposure control)", Options[i].label); } else LOGF_DEBUG("- %s", Options[i].label); } if(!AbsExposureN) DEBUGF(INDI::Logger::DBG_WARNING,"Absolute exposure duration control is not possible on the device!",""); if(!ManualExposureSP) DEBUGF(INDI::Logger::DBG_WARNING,"Manual/auto exposure control is not possible on the device!",""); //v4l_base->enumerate_ctrl(); } void V4L2_Driver::allocateBuffers() { V4LFrame = new img_t; if (V4LFrame == nullptr) { LOG_ERROR("Critial Error: Unable to initialize driver. Low memory."); exit(-1); } V4LFrame->Y = nullptr; V4LFrame->U = nullptr; V4LFrame->V = nullptr; V4LFrame->RGB24Buffer = nullptr; V4LFrame->stackedFrame = nullptr; V4LFrame->darkFrame = nullptr; } void V4L2_Driver::releaseBuffers() { delete (V4LFrame); } bool V4L2_Driver::StartStreaming() { if (PrimaryCCD.getBinX() > 1 && PrimaryCCD.getNAxis() > 2) { LOG_WARN("Cannot stream binned color frame."); return false; } /* Callee will take care of checking states */ return start_capturing(true); } bool V4L2_Driver::StopStreaming() { if (!Streamer->isBusy() /*&& is_capturing*/) { /* Strange situation indeed, but it's theoretically possible to try to stop streaming while exposing - safeguard actually */ LOGF_WARN("Cannot stop streaming, exposure running (%.1f seconds remaining)", getRemainingExposure()); return false; } return stop_capturing(); } bool V4L2_Driver::saveConfigItems(FILE *fp) { INDI::CCD::saveConfigItems(fp); return Streamer->saveConfigItems(fp); } bool V4L2_Driver::getPixelFormat(uint32_t v4l2format, INDI_PIXEL_FORMAT & pixelFormat, uint8_t & pixelDepth) { //IDLog("recorder: setpixelformat %d\n", format); pixelDepth = 8; switch (v4l2format) { case V4L2_PIX_FMT_GREY: #ifdef V4L2_PIX_FMT_Y10 case V4L2_PIX_FMT_Y10: #endif #ifdef V4L2_PIX_FMT_Y12 case V4L2_PIX_FMT_Y12: #endif #ifdef V4L2_PIX_FMT_Y16 case V4L2_PIX_FMT_Y16: #endif pixelFormat = INDI_MONO; #ifdef V4L2_PIX_FMT_Y10 if (v4l2format == V4L2_PIX_FMT_Y10) pixelDepth = 10; #endif #ifdef V4L2_PIX_FMT_Y12 if (v4l2format == V4L2_PIX_FMT_Y12) pixelDepth = 12; #endif #ifdef V4L2_PIX_FMT_Y16 if (v4l2format == V4L2_PIX_FMT_Y16) pixelDepth = 16; #endif return true; case V4L2_PIX_FMT_SBGGR8: #ifdef V4L2_PIX_FMT_SBGGR10 case V4L2_PIX_FMT_SBGGR10: #endif #ifdef V4L2_PIX_FMT_SBGGR12 case V4L2_PIX_FMT_SBGGR12: #endif case V4L2_PIX_FMT_SBGGR16: pixelFormat = INDI_BAYER_BGGR; #ifdef V4L2_PIX_FMT_SBGGR10 if (v4l2format == V4L2_PIX_FMT_SBGGR10) pixelDepth = 10; #endif #ifdef V4L2_PIX_FMT_SBGGR12 if (v4l2format == V4L2_PIX_FMT_SBGGR12) pixelDepth = 12; #endif if (v4l2format == V4L2_PIX_FMT_SBGGR16) pixelDepth = 16; return true; case V4L2_PIX_FMT_SGBRG8: #ifdef V4L2_PIX_FMT_SGBRG10 case V4L2_PIX_FMT_SGBRG10: #endif #ifdef V4L2_PIX_FMT_SGBRG12 case V4L2_PIX_FMT_SGBRG12: #endif pixelFormat = INDI_BAYER_GBRG; #ifdef V4L2_PIX_FMT_SGBRG10 if (v4l2format == V4L2_PIX_FMT_SGBRG10) pixelDepth = 10; #endif #ifdef V4L2_PIX_FMT_SGBRG12 if (v4l2format == V4L2_PIX_FMT_SGBRG12) pixelDepth = 12; #endif return true; #if defined(V4L2_PIX_FMT_SGRBG8) || defined(V4L2_PIX_FMT_SGRBG10) || defined(V4L2_PIX_FMT_SGRBG12) #ifdef V4L2_PIX_FMT_SGRBG8 case V4L2_PIX_FMT_SGRBG8: #endif #ifdef V4L2_PIX_FMT_SGRBG10 case V4L2_PIX_FMT_SGRBG10: #endif #ifdef V4L2_PIX_FMT_SGRBG12 case V4L2_PIX_FMT_SGRBG12: #endif pixelDepth = INDI_BAYER_GRBG; #ifdef V4L2_PIX_FMT_SGRBG10 if (v4l2format == V4L2_PIX_FMT_SGRBG10) pixelDepth = 10; #endif #ifdef V4L2_PIX_FMT_SGRBG12 if (v4l2format == V4L2_PIX_FMT_SGRBG12) pixelDepth = 12; #endif return true; #endif #if defined(V4L2_PIX_FMT_SRGGB8) || defined(V4L2_PIX_FMT_SRGGB10) || defined(V4L2_PIX_FMT_SRGGB12) #ifdef V4L2_PIX_FMT_SRGGB8 case V4L2_PIX_FMT_SRGGB8: #endif #ifdef V4L2_PIX_FMT_SRGGB10 case V4L2_PIX_FMT_SRGGB10: #endif #ifdef V4L2_PIX_FMT_SRGGB12 case V4L2_PIX_FMT_SRGGB12: #endif pixelFormat = INDI_BAYER_RGGB; #ifdef V4L2_PIX_FMT_SRGGB10 if (v4l2format == V4L2_PIX_FMT_SRGGB10) pixelDepth = 10; #endif #ifdef V4L2_PIX_FMT_SRGGB12 if (v4l2format == V4L2_PIX_FMT_SRGGB12) pixelDepth = 12; #endif return true; #endif case V4L2_PIX_FMT_RGB24: pixelFormat = INDI_RGB; return true; case V4L2_PIX_FMT_BGR24: pixelFormat = INDI_BGR; return true; default: return false; } } libindi/drivers/video/v4l2driver.h0000664000175000017500000001375613263645557016446 0ustar jasemjasem#if 0 V4L2 INDI Driver INDI Interface for V4L2 devices Copyright (C) 2003 - 2005 Jasem Mutlaq (mutlaqja@ikarustech.com) Copyright (C) 2013 Geehalel (geehalel@gmail.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 #pragma once #include "indiccd.h" #include "webcam/v4l2_base.h" #define IMAGE_CONTROL "Image Control" #define IMAGE_GROUP "V4L2 Control" #define IMAGE_BOOLEAN "V4L2 Options" #define CAPTURE_FORMAT "Capture Options" #define MAX_PIXELS 4096 /* Max number of pixels in one dimension */ #define ERRMSGSIZ 1024 #define TEMPFILE_LEN 16 class Lx; class V4L2_Driver : public INDI::CCD { public: V4L2_Driver(); virtual ~V4L2_Driver(); /* INDI Functions that must be called from indidrivermain */ virtual void ISGetProperties(const char *dev); virtual bool ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n); virtual bool ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n); virtual bool ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n); virtual bool initProperties(); virtual bool updateProperties(); virtual void initCamBase(); static void newFrame(void *p); void stackFrame(); void newFrame(); protected: virtual bool Connect(); virtual bool Disconnect(); virtual const char *getDefaultName(); virtual bool StartExposure(float duration); virtual bool AbortExposure(); virtual bool UpdateCCDFrame(int x, int y, int w, int h); virtual bool UpdateCCDBin(int hor, int ver); virtual bool saveConfigItems(FILE *fp); virtual bool StartStreaming(); virtual bool StopStreaming(); /* Structs */ typedef struct { int width; int height; int bpp; //int expose; double expose; unsigned char *Y; unsigned char *U; unsigned char *V; unsigned char *RGB24Buffer; unsigned char *compressedFrame; float *stackedFrame; float *darkFrame; } img_t; enum stackmodes { STACK_NONE = 0, STACK_MEAN = 1, STACK_ADDITIVE = 2, STACK_TAKE_DARK = 3, STACK_RESET_DARK = 4 }; /* Switches */ ISwitch *CompressS; ISwitch ImageColorS[2]; enum { IMAGE_GRAYSCALE, IMAGE_COLOR }; ISwitch ImageDepthS[2]; ISwitch StackModeS[5]; ISwitch ColorProcessingS[3]; /* Texts */ IText PortT[1] {}; IText camNameT[1] {}; IText CaptureColorSpaceT[3] {}; /* Numbers */ //INumber *ExposeTimeN; INumber *FrameN; INumber FrameRateN[1]; /* Switch vectors */ ISwitchVectorProperty *CompressSP; /* Compress stream switch */ ISwitchVectorProperty ImageColorSP; /* Color or grey switch */ ISwitchVectorProperty ImageDepthSP; /* 8 bits or 16 bits switch */ ISwitchVectorProperty StackModeSP; /* StackMode switch */ ISwitchVectorProperty InputsSP; /* Select input switch */ ISwitchVectorProperty CaptureFormatsSP; /* Select Capture format switch */ ISwitchVectorProperty CaptureSizesSP; /* Select Capture size switch (Discrete)*/ ISwitchVectorProperty FrameRatesSP; /* Select Frame rate (Discrete) */ ISwitchVectorProperty *Options; ISwitchVectorProperty ColorProcessingSP; unsigned int v4loptions; unsigned int v4ladjustments; bool useExtCtrl; /* Number vectors */ //INumberVectorProperty *ExposeTimeNP; /* Exposure */ INumberVectorProperty CaptureSizesNP; /* Select Capture size switch (Step/Continuous)*/ INumberVectorProperty FrameRateNP; /* Frame rate (Step/Continuous) */ INumberVectorProperty *FrameNP; /* Frame dimenstion */ INumberVectorProperty ImageAdjustNP; /* Image controls */ /* Text vectors */ ITextVectorProperty PortTP; ITextVectorProperty camNameTP; ITextVectorProperty CaptureColorSpaceTP; /* Pointers to optional properties */ INumber *AbsExposureN; ISwitchVectorProperty *ManualExposureSP; /* Initilization functions */ //virtual void connectCamera(); virtual void getBasicData(); bool getPixelFormat(uint32_t v4l2format, INDI_PIXEL_FORMAT & pixelFormat, uint8_t & pixelDepth); void allocateBuffers(); void releaseBuffers(); void updateFrameSize(); /* Shutter control */ bool setShutter(double duration); bool setManualExposure(double duration); bool startlongexposure(double timeinsec); static void lxtimerCallback(void *userpointer); static void stdtimerCallback(void *userpointer); /* start/stop functions */ bool start_capturing(bool do_stream); bool stop_capturing(); virtual void updateV4L2Controls(); /* Variables */ INDI::V4L2_Base *v4l_base; char device_name[MAXINDIDEVICE]; int subframeCount; /* For stacking */ int frameCount; double divider; /* For limits */ img_t *V4LFrame; /* Video frame */ struct timeval capture_start; /* To calculate how long a frame take */ //struct timeval capture_end; struct timeval exposure_duration; struct timeval getElapsedExposure() const; float getRemainingExposure() const; unsigned int stackMode; ulong frameBytes; bool is_capturing; bool is_exposing; //Long Exposure Lx *lx; int lxtimer; int stdtimer; short lxstate; }; libindi/drivers/video/stvdriver.h0000664000175000017500000001761213263645557016466 0ustar jasemjasem#if 0 STV driver Copyright (C) 2006 Markus Wildi, markus.wildi@datacomm.ch 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 #pragma once #define OFF 0 #define ON 1 #define REQUEST_DOWNLOAD 0x00 #define REQUEST_DOWNLOAD_ALL 0x01 #define DOWNLOAD_COMPLETE 0x02 #define REQUEST_BUFFER_STATUS 0x03 #define REQUEST_IMAGE_INFO 0x04 #define REQUEST_IMAGE_DATA 0x05 #define ACK 0x06 #define REQUEST_COMPRESSED_IMAGE_DATA 0x07 #define SEND_KEY_PATTERN 0x08 #define DISPLAY_ECHO 0x09 #define FILE_STATUS 0x0b #define REQUEST_ACK 0x10 #define NACK 0x15 /* Rotary Knob Key Patterns */ #define LR_ROTARY_DECREASE_PATTERN 0x8000 #define LR_ROTARY_INCREASE_PATTERN 0x4000 #define UD_ROTARY_DECREASE_PATTERN 0x2000 #define UD_ROTARY_INCREASE_PATTERN 0x1000 #define SHIFT_PATTERN 0x0008 /* increases rotary speed when 1 */ /* Mode Key Patterns */ #define CAL_KEY_PATTERN 0x0100 #define TRACK_KEY_PATTERN 0x0200 #define DISPLAY_KEY_PATTERN 0x0400 #define FILEOPS_KEY_PATTERN 0x0800 #define A_KEY_PATTERN 0x0010 #define SETUP_KEY_PATTERN 0x0020 #define B_KEY_PATTERN 0x0040 #define INT_KEY_PATTERN 0x0080 #define FOCUS_KEY_PATTERN 0x0001 #define IMAGE_KEY_PATTERN 0x0002 #define MONITOR_KEY_PATTERN 0x0004 /* The following bit masks have been take from Sbig's documentation */ #define ID_BITS_MASK 0x0001 /* mask for no bits*/ #define ID_BITS_10 0x0001 /* image is full 10 bits*/ #define ID_BITS_8 0x0000 /* image from focus, only 8 bits*/ #define ID_UNITS_MASK 0x0002 /* mask for units for scope*/ #define ID_UNITS_INCHES 0x0002 /* units were inches*/ #define ID_UNITS_CM 0x0000 /* units were cm*/ #define ID_SCOPE_MASK 0x0004 /* mask for telescope type*/ #define ID_SCOPE_REFRACTOR 0x0004 /* scope was refractor*/ #define ID_SCOPE_REFLECTOR 0x0000 /* scope was reflector*/ #define ID_DATETIME_MASK 0x0008 /* mask for date/time valid*/ #define ID_DATETIME_VALID 0x0008 /* date/time was set*/ #define ID_DATETIME_INVALID 0x0000 /* date/time was not set*/ #define ID_BIN_MASK 0x0030 /* mask for binning mode*/ #define ID_BIN_1X1 0x0010 /* binning was 1x1*/ #define ID_BIN_2X2 0x0020 /* binning was 2x2*/ #define ID_BIN_3X3 0x0030 /* binning was 3x3*/ #define ID_PM_MASK 0x0400 /* mask for am/pm in time*/ #define ID_PM_PM 0x0400 /* time was pm, add 12 hours*/ #define ID_PM_AM 0x0000 /* time was am, don;t add 12 hours*/ #define ID_FILTER_MASK 0x0800 /* mask for filter status*/ #define ID_FILTER_LUNAR 0x0800 /* lunar filter was used for image*/ #define ID_FILTER_NP 0x0000 /* no filter was used for image*/ #define ID_DARKSUB_MASK 0x1000 /* mask for dark subtraction*/ #define ID_DARKSUB_YES 0x1000 /* image was dark subtracted*/ #define ID_DARKSUB_NO 0x0000 /* image was not dark subtracted*/ #define ID_MOSAIC_MASK 0x6000 /* mask for mosaic status*/ #define ID_MOSAIC_NONE 0x0000 /* no mosaic, one image per frame*/ #define ID_MOSAIC_SMALL 0x2000 /* small mosaic: 40x40 pixels/image*/ #define ID_MOSAIC_LARGE 0x4000 /* large mosaic: 106x100 pixels/image*/ /* IMAGE_INFO - data for the image Notes: height - 0 or 0xFFFF if no data present exposure - 100-60000 = 1.00 - 600 secs by 0.01 60001-60999 = 0.001 - 0.999 secs by 0.001 packedDate - bits 6-0 = year - 1999 (0 -127) bits 11-7 = day ( 1-31) bits 15-12 = month (1-12) packedTime - bits 6-0 = seconds (0-59) bits 7-12 = minutes (0-59) bits 15-13 = hours mod 12 (0-11) + bit in descriptor can add 12 */ typedef struct { unsigned int descriptor; /* set of bits*/ unsigned int height, width; /* image sze */ unsigned int top, left; /* position in buffer */ double exposure; /* exposure time */ unsigned int noExposure; /* number of exposures added */ unsigned int analogGain; /*analog gain */ int digitalGain; /* digital gain */ unsigned int focalLength; /*focal length of telescope */ unsigned int aperture; /* aperture diameter */ unsigned int packedDate; /* date of image */ unsigned int year; unsigned int month; unsigned int day; unsigned int packedTime; /* time of image */ unsigned int seconds; /* time of image */ unsigned int minutes; /* time of image */ unsigned int hours; /* time of image */ double ccdTemp; /* temperature of ccd in 1/100 deg C */ unsigned int siteID; /* site id */ unsigned int eGain; /* eGain in 1/100th e/ADU */ unsigned int background; /* background for display */ unsigned int range; /* range for display */ unsigned int pedestal; /* Track and Accumulate pedestal */ unsigned int ccdTop, ccdLeft; /* position of pixels on CCD array */ unsigned int adcResolution; /* value, 8 or 10 bits */ unsigned int units; /* 0= cm, 1=inch */ unsigned int telescopeType; /* 0=refractor, 1= reflector */ unsigned int dateTimeValid; /* 0= valid */ unsigned int binning; /* 1x1=1, 2x2=2, 3x3=3 */ unsigned int filterStatus; /* 0= no filter, 1= lunar filter */ unsigned int darkFrameSuntracted; /* 0= no, 1= yes */ unsigned int imageIsMosaic; /* 0=no, 1=40x40 pixels, 2=106x100 pixels */ double pixelSize; /* 7.4 um */ double minValue; /* Pixel Contents */ double maxValue; } IMAGE_INFO; /* * $Id: serial.h 49 2006-08-25 18:07:14Z lukas $ * * Copyright (C) 2006, Lukas Zimmermann, Basel, Switzerland. * * This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2, or (at your option) any later version. This program 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 General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. Or visit http://www.gnu.org/licenses/gpl.html. */ #define PARITY_NONE 0 #define PARITY_EVEN 1 #define PARITY_ODD 2 typedef unsigned char byte; /* define byte type */ /* Restores terminal settings of open serial port device and close the file. */ void shutdown_serial(int fd); /* Opens and initializes a serial device and returns it's file descriptor. */ int init_serial(char *device_name, int bit_rate, int word_size, int parity, int stop_bits); /* Calculates the 16 bit CRC of an array of bytes and returns it. */ unsigned int calc_crc(byte byte_array[], int size); libindi/drivers/telescope/0000775000175000017500000000000013263645557015133 5ustar jasemjasemlibindi/drivers/telescope/ieqprodriver.h0000664000175000017500000001371713263645557020030 0ustar jasemjasem/* IEQ Pro driver Copyright (C) 2015 Jasem Mutlaq 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 */ #pragma once #include typedef enum { GPS_OFF, GPS_ON, GPS_DATA_OK } IEQ_GPS_STATUS; typedef enum { ST_STOPPED, ST_TRACKING_PEC_OFF, ST_SLEWING, ST_GUIDING, ST_MERIDIAN_FLIPPING, ST_TRACKING_PEC_ON, ST_PARKED, ST_HOME } IEQ_SYSTEM_STATUS; typedef enum { TR_SIDEREAL, TR_LUNAR, TR_SOLAR, TR_KING, TR_CUSTOM } IEQ_TRACK_RATE; typedef enum { SR_1, SR_2, SR_3, SR_4, SR_5, SR_6, SR_7, SR_8, SR_MAX } IEQ_SLEW_RATE; typedef enum { TS_RS232, TS_CONTROLLER, TS_GPS } IEQ_TIME_SOURCE; typedef enum { HEMI_SOUTH, HEMI_NORTH } IEQ_HEMISPHERE; typedef enum { FW_MODEL, FW_BOARD, FW_CONTROLLER, FW_RA, FW_DEC } IEQ_FIRMWARE; typedef enum { RA_AXIS, DEC_AXIS } IEQ_AXIS; typedef enum { IEQ_N, IEQ_S, IEQ_W, IEQ_E } IEQ_DIRECTION; typedef enum { IEQ_FIND_HOME, IEQ_SET_HOME, IEQ_GOTO_HOME } IEQ_HOME_OPERATION; typedef struct { IEQ_GPS_STATUS gpsStatus; IEQ_SYSTEM_STATUS systemStatus; IEQ_SYSTEM_STATUS rememberSystemStatus; IEQ_TRACK_RATE trackRate; IEQ_SLEW_RATE slewRate; IEQ_TIME_SOURCE timeSource; IEQ_HEMISPHERE hemisphere; } IEQInfo; typedef struct { std::string Model; std::string MainBoardFirmware; std::string ControllerFirmware; std::string RAFirmware; std::string DEFirmware; } FirmwareInfo; /************************************************************************** Misc. **************************************************************************/ void set_ieqpro_debug(bool enable); void set_ieqpro_simulation(bool enable); void set_ieqpro_device(const char *name); /************************************************************************** Simulation **************************************************************************/ void set_sim_gps_status(IEQ_GPS_STATUS value); void set_sim_system_status(IEQ_SYSTEM_STATUS value); void set_sim_track_rate(IEQ_TRACK_RATE value); void set_sim_slew_rate(IEQ_SLEW_RATE value); void set_sim_time_source(IEQ_TIME_SOURCE value); void set_sim_hemisphere(IEQ_HEMISPHERE value); void set_sim_ra(double ra); void set_sim_dec(double dec); void set_sim_guide_rate(double rate); /************************************************************************** Diagnostics **************************************************************************/ bool check_ieqpro_connection(int fd); /************************************************************************** Get Info **************************************************************************/ /** Get iEQ current status info */ bool get_ieqpro_status(int fd, IEQInfo *info); /** Get All firmware informatin in addition to mount model */ bool get_ieqpro_firmware(int fd, FirmwareInfo *info); /** Get mainboard and controller firmware only */ bool get_ieqpro_main_firmware(int fd, FirmwareInfo *info); /** Get RA and DEC firmware info */ bool get_ieqpro_radec_firmware(int fd, FirmwareInfo *info); /** Get Mount model */ bool get_ieqpro_model(int fd, FirmwareInfo *info); /** Get RA/DEC */ bool get_ieqpro_coords(int fd, double *ra, double *dec); /** Get UTC/Date/Time */ bool get_ieqpro_utc_date_time(int fd, double *utc_hours, int *yy, int *mm, int *dd, int *hh, int *minute, int *ss); /************************************************************************** Motion **************************************************************************/ bool start_ieqpro_motion(int fd, IEQ_DIRECTION dir); bool stop_ieqpro_motion(int fd, IEQ_DIRECTION dir); bool set_ieqpro_slew_rate(int fd, IEQ_SLEW_RATE rate); bool set_ieqpro_custom_ra_track_rate(int fd, double rate); bool set_ieqpro_custom_de_track_rate(int fd, double rate); bool set_ieqpro_track_mode(int fd, IEQ_TRACK_RATE rate); bool set_ieqpro_track_enabled(int fd, bool enabled); bool abort_ieqpro(int fd); bool slew_ieqpro(int fd); bool sync_ieqpro(int fd); bool set_ieqpro_ra(int fd, double ra); bool set_ieqpro_dec(int fd, double dec); /************************************************************************** Home **************************************************************************/ bool find_ieqpro_home(int fd); bool goto_ieqpro_home(int fd); bool set_ieqpro_current_home(int fd); /************************************************************************** Park **************************************************************************/ bool park_ieqpro(int fd); bool unpark_ieqpro(int fd); /************************************************************************** Guide **************************************************************************/ bool set_ieqpro_guide_rate(int fd, double rate); bool get_ieqpro_guide_rate(int fd, double *rate); bool start_ieqpro_guide(int fd, IEQ_DIRECTION dir, int ms); /************************************************************************** Time & Location **************************************************************************/ bool set_ieqpro_longitude(int fd, double longitude); bool set_ieqpro_latitude(int fd, double latitude); bool get_ieqpro_longitude(int fd, double *longitude); bool get_ieqpro_latitude(int fd, double *latitude); bool set_ieqpro_local_date(int fd, int yy, int mm, int dd); bool set_ieqpro_local_time(int fd, int hh, int mm, int ss); bool set_ieqpro_utc_offset(int fd, double offset_hours); bool set_ieqpro_daylight_saving(int fd, bool enabled); libindi/drivers/telescope/ioptronv3.h0000664000175000017500000001030113263645557017242 0ustar jasemjasem/* INDI IOptron v3 Driver for firmware version 20171001 or later. Copyright (C) 2018 Jasem Mutlaq 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 */ #pragma once #include #include "ioptronv3driver.h" #include "indiguiderinterface.h" #include "inditelescope.h" class IOptronV3 : public INDI::Telescope, public INDI::GuiderInterface { public: IOptronV3(); ~IOptronV3() = default; virtual bool ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) override; virtual bool ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) override; protected: virtual const char *getDefaultName() override; virtual bool Handshake() override; virtual bool initProperties() override; virtual bool updateProperties() override; virtual bool ReadScopeStatus() override; virtual bool MoveNS(INDI_DIR_NS dir, TelescopeMotionCommand command) override; virtual bool MoveWE(INDI_DIR_WE dir, TelescopeMotionCommand command) override; virtual bool saveConfigItems(FILE *fp) override; virtual bool Park() override; virtual bool UnPark() override; virtual bool Sync(double ra, double de) override; virtual bool Goto(double ra, double de) override; virtual bool Abort() override; virtual bool updateTime(ln_date *utc, double utc_offset) override; virtual bool updateLocation(double latitude, double longitude, double elevation) override; virtual void debugTriggered(bool enable) override; virtual void simulationTriggered(bool enable) override; // Parking virtual bool SetCurrentPark() override; virtual bool SetDefaultPark() override; // Track Mode virtual bool SetTrackMode(uint8_t mode) override; // Track Rate virtual bool SetTrackRate(double raRate, double deRate) override; // Track On/Off virtual bool SetTrackEnabled(bool enabled) override; // Slew Rate virtual bool SetSlewRate(int index) override; // Sim void mountSim(); // Guide virtual IPState GuideNorth(float ms) override; virtual IPState GuideSouth(float ms) override; virtual IPState GuideEast(float ms) override; virtual IPState GuideWest(float ms) override; private: /** * @brief getStartupData Get initial mount info on startup. */ void getStartupData(); /* Firmware */ IText FirmwareT[5] {}; ITextVectorProperty FirmwareTP; /* GPS Status */ ISwitch GPSStatusS[3]; ISwitchVectorProperty GPSStatusSP; /* Time Source */ ISwitch TimeSourceS[3]; ISwitchVectorProperty TimeSourceSP; /* Hemisphere */ ISwitch HemisphereS[2]; ISwitchVectorProperty HemisphereSP; /* Home Control */ ISwitch HomeS[3]; ISwitchVectorProperty HomeSP; /* Guide Rate */ INumber GuideRateN[2]; INumberVectorProperty GuideRateNP; /* Slew Mode */ ISwitch SlewModeS[2]; ISwitchVectorProperty SlewModeSP; /* Counterweight Status */ ISwitch CWStateS[2]; ISwitchVectorProperty CWStateSP; // TODO #if 0 /* PE Recording */ ISwitch PERecordS[2]; ISwitchVectorProperty PERecordSP; /* PEC Playback */ ISwitch PEPlaybackS[2]; ISwitchVectorProperty PEPlaybackSP; #endif /* Daylight Saving */ ISwitch DaylightS[2]; ISwitchVectorProperty DaylightSP; uint32_t DBG_SCOPE; double currentRA, currentDEC; double targetRA, targetDEC; IOPv3::IOPInfo scopeInfo; IOPv3::FirmwareInfo firmwareInfo; std::unique_ptr driver; }; libindi/drivers/telescope/lx200ap.cpp0000664000175000017500000010204513263645557017027 0ustar jasemjasem/* Astro-Physics INDI driver Copyright (C) 2014 Jasem Mutlaq Based on INDI Astrophysics Driver by Markus Wildi 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 */ #include "lx200ap.h" #include "indicom.h" #include "lx200driver.h" #include "lx200apdriver.h" #include #include #include #include #include #define FIRMWARE_TAB "Firmware data" #define MOUNT_TAB "Mount" /* Constructor */ LX200AstroPhysics::LX200AstroPhysics() : LX200Generic() { setLX200Capability(LX200_HAS_PULSE_GUIDING); SetTelescopeCapability(GetTelescopeCapability() | TELESCOPE_HAS_PIER_SIDE | TELESCOPE_HAS_PEC | TELESCOPE_CAN_CONTROL_TRACK | TELESCOPE_HAS_TRACK_RATE, 4); sendLocationOnStartup = false; sendTimeOnStartup = false; } const char *LX200AstroPhysics::getDefaultName() { return (const char *)"AstroPhysics"; } bool LX200AstroPhysics::initProperties() { LX200Generic::initProperties(); timeFormat = LX200_24; IUFillSwitch(&StartUpS[0], "COLD", "Cold", ISS_OFF); IUFillSwitch(&StartUpS[1], "WARM", "Warm", ISS_OFF); IUFillSwitchVector(&StartUpSP, StartUpS, 2, getDeviceName(), "STARTUP", "Mount init.", MAIN_CONTROL_TAB, IP_RW, ISR_1OFMANY, 0, IPS_IDLE); IUFillNumber(&HourangleCoordsN[0], "HA", "HA H:M:S", "%10.6m", 0., 24., 0., 0.); IUFillNumber(&HourangleCoordsN[1], "DEC", "Dec D:M:S", "%10.6m", -90.0, 90.0, 0., 0.); IUFillNumberVector(&HourangleCoordsNP, HourangleCoordsN, 2, getDeviceName(), "HOURANGLE_COORD", "Hourangle Coords", MAIN_CONTROL_TAB, IP_RO, 0, IPS_IDLE); IUFillNumber(&HorizontalCoordsN[0], "AZ", "Az D:M:S", "%10.6m", 0., 360., 0., 0.); IUFillNumber(&HorizontalCoordsN[1], "ALT", "Alt D:M:S", "%10.6m", -90., 90., 0., 0.); IUFillNumberVector(&HorizontalCoordsNP, HorizontalCoordsN, 2, getDeviceName(), "HORIZONTAL_COORD", "Horizontal Coords", MAIN_CONTROL_TAB, IP_RW, 120, IPS_IDLE); // Max rate is 999.99999X for the GTOCP4. // Using :RR998.9999# just to be safe. 15.041067*998.99999 = 15026.02578 TrackRateN[AXIS_RA].min = -15026.0258; TrackRateN[AXIS_RA].max = 15026.0258; TrackRateN[AXIS_DE].min = -998.9999; TrackRateN[AXIS_DE].max = 998.9999; // Motion speed of axis when pressing NSWE buttons IUFillSwitch(&SlewRateS[0], "12", "12x", ISS_OFF); IUFillSwitch(&SlewRateS[1], "64", "64x", ISS_ON); IUFillSwitch(&SlewRateS[2], "600", "600x", ISS_OFF); IUFillSwitch(&SlewRateS[3], "1200", "1200x", ISS_OFF); IUFillSwitchVector(&SlewRateSP, SlewRateS, 4, getDeviceName(), "TELESCOPE_SLEW_RATE", "Slew Rate", MOTION_TAB, IP_RW, ISR_1OFMANY, 0, IPS_IDLE); // Slew speed when performing regular GOTO IUFillSwitch(&APSlewSpeedS[0], "600", "600x", ISS_ON); IUFillSwitch(&APSlewSpeedS[1], "900", "900x", ISS_OFF); IUFillSwitch(&APSlewSpeedS[2], "1200", "1200x", ISS_OFF); IUFillSwitchVector(&APSlewSpeedSP, APSlewSpeedS, 3, getDeviceName(), "GOTO Rate", "", MOTION_TAB, IP_RW, ISR_1OFMANY, 0, IPS_IDLE); IUFillSwitch(&SwapS[0], "NS", "North/South", ISS_OFF); IUFillSwitch(&SwapS[1], "EW", "East/West", ISS_OFF); IUFillSwitchVector(&SwapSP, SwapS, 2, getDeviceName(), "SWAP", "Swap buttons", MOTION_TAB, IP_RW, ISR_1OFMANY, 0, IPS_IDLE); IUFillSwitch(&SyncCMRS[USE_REGULAR_SYNC], ":CM#", ":CM#", ISS_OFF); IUFillSwitch(&SyncCMRS[USE_CMR_SYNC], ":CMR#", ":CMR#", ISS_ON); IUFillSwitchVector(&SyncCMRSP, SyncCMRS, 2, getDeviceName(), "SYNCCMR", "Sync", MOTION_TAB, IP_RW, ISR_1OFMANY, 0, IPS_IDLE); // guide speed IUFillSwitch(&APGuideSpeedS[0], "0.25", "0.25x", ISS_OFF); IUFillSwitch(&APGuideSpeedS[1], "0.5", "0.50x", ISS_OFF); IUFillSwitch(&APGuideSpeedS[2], "1.0", "1.0x", ISS_ON); IUFillSwitchVector(&APGuideSpeedSP, APGuideSpeedS, 3, getDeviceName(), "Guide Rate", "", GUIDE_TAB, IP_RW, ISR_1OFMANY, 0, IPS_IDLE); IUFillText(&VersionT[0], "Number", "", 0); IUFillTextVector(&VersionInfo, VersionT, 1, getDeviceName(), "Firmware Info", "", FIRMWARE_TAB, IP_RO, 0, IPS_IDLE); IUFillText(&DeclinationAxisT[0], "RELHA", "rel. to HA", "undefined"); IUFillTextVector(&DeclinationAxisTP, DeclinationAxisT, 1, getDeviceName(), "DECLINATIONAXIS", "Declination axis", MOUNT_TAB, IP_RO, 0, IPS_IDLE); // Slew threshold IUFillNumber(&SlewAccuracyN[0], "SlewRA", "RA (arcmin)", "%10.6m", 0., 60., 1., 3.0); IUFillNumber(&SlewAccuracyN[1], "SlewDEC", "Dec (arcmin)", "%10.6m", 0., 60., 1., 3.0); IUFillNumberVector(&SlewAccuracyNP, SlewAccuracyN, 2, getDeviceName(), "Slew Accuracy", "", MOUNT_TAB, IP_RW, 0, IPS_IDLE); SetParkDataType(PARK_AZ_ALT); return true; } void LX200AstroPhysics::ISGetProperties(const char *dev) { LX200Generic::ISGetProperties(dev); /* if (isConnected()) { defineSwitch(&StartUpSP); defineText(&VersionInfo); //defineText(&DeclinationAxisTP); // Motion group defineSwitch(&APSlewSpeedSP); defineSwitch(&SwapSP); defineSwitch(&SyncCMRSP); defineSwitch(&APGuideSpeedSP); defineNumber(&SlewAccuracyNP); LOG_INFO("Please initialize the mount before issuing any command."); } */ } bool LX200AstroPhysics::updateProperties() { LX200Generic::updateProperties(); if (isConnected()) { defineSwitch(&StartUpSP); defineText(&VersionInfo); //defineText(&DeclinationAxisTP); /* Motion group */ defineSwitch(&APSlewSpeedSP); defineSwitch(&SwapSP); defineSwitch(&SyncCMRSP); defineSwitch(&APGuideSpeedSP); defineNumber(&SlewAccuracyNP); LOG_INFO("Please initialize the mount before issuing any command."); } else { deleteProperty(StartUpSP.name); deleteProperty(VersionInfo.name); //deleteProperty(DeclinationAxisTP.name); deleteProperty(APSlewSpeedSP.name); deleteProperty(SwapSP.name); deleteProperty(SyncCMRSP.name); deleteProperty(APGuideSpeedSP.name); deleteProperty(SlewAccuracyNP.name); } return true; } bool LX200AstroPhysics::ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) { int err = 0; // ignore if not ours // if (strcmp(getDeviceName(), dev)) return false; // ============================================================ // Satisfy AP mount initialization, see AP key pad manual p. 76 // ============================================================ if (!strcmp(name, StartUpSP.name)) { int switch_nr; IUUpdateSwitch(&StartUpSP, states, names, n); if (initStatus == MOUNTNOTINITIALIZED) { if (timeUpdated == false || locationUpdated == false) { StartUpSP.s = IPS_ALERT; LOG_ERROR("Time and location must be set before mount initialization is invoked."); IDSetSwitch(&StartUpSP, nullptr); return false; } if (StartUpSP.sp[0].s == ISS_ON) // do it only in case a power on (cold start) { if (setBasicDataPart1() == false) { StartUpSP.s = IPS_ALERT; IDSetSwitch(&StartUpSP, "Cold mount initialization failed."); return false; } } initStatus = MOUNTINITIALIZED; if (isSimulation()) { SlewRateSP.s = IPS_OK; IDSetSwitch(&SlewRateSP, nullptr); APSlewSpeedSP.s = IPS_OK; IDSetSwitch(&APSlewSpeedSP, nullptr); IUSaveText(&VersionT[0], "1.0"); VersionInfo.s = IPS_OK; IDSetText(&VersionInfo, nullptr); StartUpSP.s = IPS_OK; IDSetSwitch(&StartUpSP, "Mount initialized."); //currentRA = 0; //currentDEC = 90; } else { // Make sure that the mount is setup according to the properties switch_nr = IUFindOnSwitchIndex(&TrackModeSP); if ( (err = selectAPTrackingMode(PortFD, switch_nr)) < 0) { LOGF_ERROR("StartUpSP: Error setting tracking mode (%d).", err); return false; } TrackState = (switch_nr != AP_TRACKING_OFF) ? SCOPE_TRACKING : SCOPE_IDLE; // On most mounts SlewRateS defines the MoveTo AND Slew (GOTO) speeds // lx200ap is different - some of the MoveTo speeds are not VALID // Slew speeds so we have to keep two lists. // // SlewRateS is used as the MoveTo speed switch_nr = IUFindOnSwitchIndex(&SlewRateSP); if ( (err = selectAPMoveToRate(PortFD, switch_nr)) < 0) { LOGF_ERROR("StartUpSP: Error setting move rate (%d).", err); return false; } SlewRateSP.s = IPS_OK; IDSetSwitch(&SlewRateSP, nullptr); // APSlewSpeedsS defines the Slew (GOTO) speeds valid on the AP mounts switch_nr = IUFindOnSwitchIndex(&APSlewSpeedSP); if ( (err = selectAPSlewRate(PortFD, switch_nr)) < 0) { LOGF_ERROR("StartUpSP: Error setting slew to rate (%d).", err); return false; } APSlewSpeedSP.s = IPS_OK; IDSetSwitch(&APSlewSpeedSP, nullptr); getLX200RA(PortFD, ¤tRA); getLX200DEC(PortFD, ¤tDEC); // make a IDSet in order the dome controller is aware of the initial values targetRA = currentRA; targetDEC = currentDEC; NewRaDec(currentRA, currentDEC); char versionString[64]; getAPVersionNumber(PortFD, versionString); VersionInfo.s = IPS_OK; IUSaveText(&VersionT[0], versionString); IDSetText(&VersionInfo, nullptr); // TODO check controller type here INDI_UNUSED(controllerType); INDI_UNUSED(servoType); //controllerType = ...; StartUpSP.s = IPS_OK; IDSetSwitch(&StartUpSP, "Mount initialized."); } } else { StartUpSP.s = IPS_OK; IDSetSwitch(&StartUpSP, "Mount is already initialized."); } return true; } // ======================================= // Swap Buttons // ======================================= if (!strcmp(name, SwapSP.name)) { int currentSwap; IUResetSwitch(&SwapSP); IUUpdateSwitch(&SwapSP, states, names, n); currentSwap = IUFindOnSwitchIndex(&SwapSP); if ((!isSimulation() && (err = swapAPButtons(PortFD, currentSwap)) < 0)) { LOGF_ERROR("Error swapping buttons (%d).", err); return false; } SwapS[0].s = ISS_OFF; SwapS[1].s = ISS_OFF; SwapSP.s = IPS_OK; IDSetSwitch(&SwapSP, nullptr); return true; } // =========================================================== // GOTO ("slew") Speed. // =========================================================== if (!strcmp(name, APSlewSpeedSP.name)) { IUUpdateSwitch(&APSlewSpeedSP, states, names, n); int slewRate = IUFindOnSwitchIndex(&APSlewSpeedSP); if (!isSimulation() && (err = selectAPSlewRate(PortFD, slewRate) < 0)) { LOGF_ERROR("Error setting move to rate (%d).", err); return false; } APSlewSpeedSP.s = IPS_OK; IDSetSwitch(&APSlewSpeedSP, nullptr); return true; } // =========================================================== // Guide Speed. // =========================================================== if (!strcmp(name, APGuideSpeedSP.name)) { IUUpdateSwitch(&APGuideSpeedSP, states, names, n); int guideRate = IUFindOnSwitchIndex(&APGuideSpeedSP); if (!isSimulation() && (err = selectAPGuideRate(PortFD, guideRate) < 0)) { LOGF_ERROR("Error setting guiding to rate (%d).", err); return false; } APGuideSpeedSP.s = IPS_OK; IDSetSwitch(&APGuideSpeedSP, nullptr); return true; } // ======================================= // Choose the appropriate sync command // ======================================= if (!strcmp(name, SyncCMRSP.name)) { IUResetSwitch(&SyncCMRSP); IUUpdateSwitch(&SyncCMRSP, states, names, n); IUFindOnSwitchIndex(&SyncCMRSP); SyncCMRSP.s = IPS_OK; IDSetSwitch(&SyncCMRSP, nullptr); return true; } // ======================================= // Choose the PEC playback mode // ======================================= if (!strcmp(name, PECStateSP.name)) { IUResetSwitch(&PECStateSP); IUUpdateSwitch(&PECStateSP, states, names, n); IUFindOnSwitchIndex(&PECStateSP); int pecstate = IUFindOnSwitchIndex(&PECStateSP); if (!isSimulation() && (err = selectAPPECState(PortFD, pecstate) < 0)) { LOGF_ERROR("Error setting PEC state (%d).", err); return false; } PECStateSP.s = IPS_OK; IDSetSwitch(&PECStateSP, nullptr); return true; } return LX200Generic::ISNewSwitch(dev, name, states, names, n); } /************************************************************************************** ** ***************************************************************************************/ bool LX200AstroPhysics::ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) { if (strcmp(getDeviceName(), dev)) return false; // Update slew precision limit if (!strcmp(name, SlewAccuracyNP.name)) { if (IUUpdateNumber(&SlewAccuracyNP, values, names, n) < 0) return false; SlewAccuracyNP.s = IPS_OK; if (SlewAccuracyN[0].value < 3 || SlewAccuracyN[1].value < 3) IDSetNumber(&SlewAccuracyNP, "Warning: Setting the slew accuracy too low may result in a dead lock"); IDSetNumber(&SlewAccuracyNP, nullptr); return true; } return LX200Generic::ISNewNumber(dev, name, values, names, n); } bool LX200AstroPhysics::isMountInit() { return (StartUpSP.s != IPS_IDLE); } bool LX200AstroPhysics::ReadScopeStatus() { if (!isMountInit()) return false; if (isSimulation()) { mountSim(); return true; } if (getLX200RA(PortFD, ¤tRA) < 0 || getLX200DEC(PortFD, ¤tDEC) < 0) { EqNP.s = IPS_ALERT; IDSetNumber(&EqNP, "Error reading RA/DEC."); return false; } if (TrackState == SCOPE_SLEWING) { double dx = targetRA - currentRA; double dy = targetDEC - currentDEC; // Wait until acknowledged or within threshold if (fabs(dx) <= (SlewAccuracyN[0].value / (900.0)) && fabs(dy) <= (SlewAccuracyN[1].value / 60.0)) { TrackState = SCOPE_TRACKING; LOG_INFO("Slew is complete. Tracking..."); } } else if (TrackState == SCOPE_PARKING) { double currentAlt, currentAz; if (getLX200Az(PortFD, ¤tAz) < 0 || getLX200Alt(PortFD, ¤tAlt) < 0) { EqNP.s = IPS_ALERT; IDSetNumber(&EqNP, "Error reading Az/Alt."); return false; } double dx = GetAxis1Park() - currentAz; double dy = GetAxis2Park() - currentAlt; LOGF_DEBUG("Parking... targetAz: %g currentAz: %g dx: %g targetAlt: %g currentAlt: %g dy: %g", GetAxis1Park(), currentAz, dx, GetAxis2Park(), currentAlt, dy); if (fabs(dx) <= (SlewAccuracyN[0].value / (60.0)) && fabs(dy) <= (SlewAccuracyN[1].value / 60.0)) { LOG_DEBUG("Parking slew is complete. Asking astrophysics mount to park..."); if (!isSimulation() && setAPPark(PortFD) < 0) { LOG_ERROR("Parking Failed."); return false; } SetParked(true); } } NewRaDec(currentRA, currentDEC); syncSideOfPier(); return true; } bool LX200AstroPhysics::setBasicDataPart0() { int err; //struct ln_date utm; //struct ln_zonedate ltm; if (isSimulation() == true) { LOG_INFO("setBasicDataPart0 simulation complete."); return true; } if ((err = setAPClearBuffer(PortFD)) < 0) { LOGF_ERROR("Error clearing the buffer (%d): %s", err, strerror(err)); return false; } if ((err = setAPLongFormat(PortFD)) < 0) { LOGF_ERROR("Error setting long format failed (%d): %s", err, strerror(err)); return false; } if ((err = setAPBackLashCompensation(PortFD, 0, 0, 0)) < 0) { // It seems we need to send it twice before it works! if ((err = setAPBackLashCompensation(PortFD, 0, 0, 0)) < 0) { LOGF_ERROR("Error setting back lash compensation (%d): %s.", err, strerror(err)); return false; } } // Detect and set fomat. It should be LONG. checkLX200Format(PortFD); return true; } bool LX200AstroPhysics::setBasicDataPart1() { int err = 0; if (InitPark()) { // If loading parking data is successful, we just set the default parking values. SetAxis1ParkDefault(LocationN[LOCATION_LATITUDE].value >= 0 ? 0 : 180); SetAxis2ParkDefault(LocationN[LOCATION_LATITUDE].value); } else { // Otherwise, we set all parking data to default in case no parking data is found. SetAxis1Park(LocationN[LOCATION_LATITUDE].value >= 0 ? 0 : 180); SetAxis1ParkDefault(LocationN[LOCATION_LATITUDE].value); SetAxis1ParkDefault(LocationN[LOCATION_LATITUDE].value >= 0 ? 0 : 180); SetAxis2ParkDefault(LocationN[LOCATION_LATITUDE].value); } // Unpark UnPark(); // Stop if (!isSimulation() && (err = setAPMotionStop(PortFD)) < 0) { LOGF_ERROR("Stop motion (:Q#) failed, check the mount (%d): %s", err, strerror(err)); return false; } // AP always track after unpark? Must check TrackState = SCOPE_TRACKING; return true; } bool LX200AstroPhysics::Goto(double r, double d) { const struct timespec timeout = {0, 100000000L}; targetRA = r; targetDEC = d; char RAStr[64], DecStr[64]; fs_sexa(RAStr, targetRA, 2, 3600); fs_sexa(DecStr, targetDEC, 2, 3600); // If moving, let's stop it first. if (EqNP.s == IPS_BUSY) { if (!isSimulation() && abortSlew(PortFD) < 0) { AbortSP.s = IPS_ALERT; IDSetSwitch(&AbortSP, "Abort slew failed."); return false; } AbortSP.s = IPS_OK; EqNP.s = IPS_IDLE; IDSetSwitch(&AbortSP, "Slew aborted."); IDSetNumber(&EqNP, nullptr); if (MovementNSSP.s == IPS_BUSY || MovementWESP.s == IPS_BUSY) { MovementNSSP.s = MovementWESP.s = IPS_IDLE; EqNP.s = IPS_IDLE; IUResetSwitch(&MovementNSSP); IUResetSwitch(&MovementWESP); IDSetSwitch(&MovementNSSP, nullptr); IDSetSwitch(&MovementWESP, nullptr); } // sleep for 100 mseconds nanosleep(&timeout, NULL); } if (!isSimulation()) { if (setAPObjectRA(PortFD, targetRA) < 0 || (setAPObjectDEC(PortFD, targetDEC)) < 0) { EqNP.s = IPS_ALERT; IDSetNumber(&EqNP, "Error setting RA/DEC."); return false; } int err = 0; /* Slew reads the '0', that is not the end of the slew */ if ((err = Slew(PortFD))) { EqNP.s = IPS_ALERT; IDSetNumber(&EqNP, "Error Slewing to JNow RA %s - DEC %s\n", RAStr, DecStr); slewError(err); return false; } } TrackState = SCOPE_SLEWING; EqNP.s = IPS_BUSY; LOGF_INFO("Slewing to RA: %s - DEC: %s", RAStr, DecStr); return true; } int LX200AstroPhysics::SendPulseCmd(int direction, int duration_msec) { return APSendPulseCmd(PortFD, direction, duration_msec); } bool LX200AstroPhysics::Handshake() { if (isSimulation()) { LOG_INFO("Simulated Astrophysics is online. Retrieving basic data..."); return true; } return setBasicDataPart0(); } bool LX200AstroPhysics::Disconnect() { timeUpdated = false; locationUpdated = false; return LX200Generic::Disconnect(); } bool LX200AstroPhysics::Sync(double ra, double dec) { char syncString[256]; int syncType = IUFindOnSwitchIndex(&SyncCMRSP); if (!isSimulation()) { if (setAPObjectRA(PortFD, ra) < 0 || setAPObjectDEC(PortFD, dec) < 0) { EqNP.s = IPS_ALERT; IDSetNumber(&EqNP, "Error setting RA/DEC. Unable to Sync."); return false; } bool syncOK = true; switch (syncType) { case USE_REGULAR_SYNC: if (::Sync(PortFD, syncString) < 0) syncOK = false; break; case USE_CMR_SYNC: if (APSyncCMR(PortFD, syncString) < 0) syncOK = false; break; default: break; } if (syncOK == false) { EqNP.s = IPS_ALERT; IDSetNumber(&EqNP, "Synchronization failed."); return false; } } currentRA = ra; currentDEC = dec; LOGF_DEBUG("%s Synchronization successful %s", (syncType == USE_REGULAR_SYNC ? "CM" : "CMR"), syncString); LOG_INFO("Synchronization successful."); EqNP.s = IPS_OK; NewRaDec(currentRA, currentDEC); return true; } bool LX200AstroPhysics::updateTime(ln_date *utc, double utc_offset) { struct ln_zonedate ltm; if (isSimulation()) { timeUpdated = true; return true; } ln_date_to_zonedate(utc, <m, utc_offset * 3600.0); JD = ln_get_julian_day(utc); LOGF_DEBUG("New JD is %.2f", JD); // Set Local Time if (setLocalTime(PortFD, ltm.hours, ltm.minutes, (int)ltm.seconds) < 0) { LOG_ERROR("Error setting local time."); return false; } LOGF_DEBUG("Set Local Time %02d:%02d:%02d is successful.", ltm.hours, ltm.minutes, (int)ltm.seconds); if (setCalenderDate(PortFD, ltm.days, ltm.months, ltm.years) < 0) { LOG_ERROR("Error setting local date."); return false; } LOGF_DEBUG("Set Local Date %02d/%02d/%02d is successful.", ltm.days, ltm.months, ltm.years); if (setAPUTCOffset(PortFD, fabs(utc_offset)) < 0) { LOG_ERROR("Error setting UTC Offset."); return false; } LOGF_DEBUG("Set UTC Offset %g (always positive for AP) is successful.", fabs(utc_offset)); LOG_INFO("Time updated."); timeUpdated = true; return true; } bool LX200AstroPhysics::updateLocation(double latitude, double longitude, double elevation) { INDI_UNUSED(elevation); if (isSimulation()) { locationUpdated = true; return true; } if (!isSimulation() && setAPSiteLongitude(PortFD, 360.0 - longitude) < 0) { LOG_ERROR("Error setting site longitude coordinates"); return false; } if (!isSimulation() && setAPSiteLatitude(PortFD, latitude) < 0) { LOG_ERROR("Error setting site latitude coordinates"); return false; } char l[32], L[32]; fs_sexa(l, latitude, 3, 3600); fs_sexa(L, longitude, 4, 3600); LOGF_INFO("Site location updated to Lat %.32s - Long %.32s", l, L); locationUpdated = true; return true; } void LX200AstroPhysics::debugTriggered(bool enable) { INDI_UNUSED(enable); LX200Generic::debugTriggered(enable); set_lx200ap_name(getDeviceName(), DBG_SCOPE); } // For most mounts the SetSlewRate() method sets both the MoveTo and Slew (GOTO) speeds. // For AP mounts these two speeds are handled separately - so SetSlewRate() actually sets the MoveTo speed for AP mounts - confusing! // ApSetSlew bool LX200AstroPhysics::SetSlewRate(int index) { if (!isSimulation() && selectAPMoveToRate(PortFD, index) < 0) { SlewRateSP.s = IPS_ALERT; IDSetSwitch(&SlewRateSP, "Error setting slew mode."); return false; } SlewRateSP.s = IPS_OK; IDSetSwitch(&SlewRateSP, nullptr); return true; } bool LX200AstroPhysics::Park() { if (initStatus == MOUNTNOTINITIALIZED) { LOG_WARN("You must initialize the mount before parking."); return false; } double parkAz = GetAxis1Park(); double parkAlt = GetAxis2Park(); char AzStr[16], AltStr[16]; fs_sexa(AzStr, parkAz, 2, 3600); fs_sexa(AltStr, parkAlt, 2, 3600); LOGF_DEBUG("Parking to Az (%s) Alt (%s)...", AzStr, AltStr); if (isSimulation()) { ln_lnlat_posn observer; observer.lat = LocationN[LOCATION_LATITUDE].value; observer.lng = LocationN[LOCATION_LONGITUDE].value; if (observer.lng > 180) observer.lng -= 360; ln_hrz_posn horizontalPos; // Libnova south = 0, west = 90, north = 180, east = 270 horizontalPos.az = parkAz + 180; if (horizontalPos.az > 360) horizontalPos.az -= 360; horizontalPos.alt = parkAlt; ln_equ_posn equatorialPos; ln_get_equ_from_hrz(&horizontalPos, &observer, ln_get_julian_from_sys(), &equatorialPos); Goto(equatorialPos.ra / 15.0, equatorialPos.dec); } else { if (setAPObjectAZ(PortFD, parkAz) < 0 || setAPObjectAlt(PortFD, parkAlt) < 0) { LOG_ERROR("Error setting Az/Alt."); return false; } int err = 0; /* Slew reads the '0', that is not the end of the slew */ if ((err = Slew(PortFD))) { LOGF_ERROR("Error Slewing to Az %s - Alt %s", AzStr, AltStr); slewError(err); return false; } } EqNP.s = IPS_BUSY; TrackState = SCOPE_PARKING; LOG_INFO("Parking is in progress..."); return true; } bool LX200AstroPhysics::UnPark() { // First we unpark astrophysics if (isSimulation() == false) { if (setAPUnPark(PortFD) < 0) { LOG_ERROR("UnParking Failed."); return false; } } // Then we sync with to our last stored position double parkAz = GetAxis1Park(); double parkAlt = GetAxis2Park(); char AzStr[16], AltStr[16]; fs_sexa(AzStr, parkAz, 2, 3600); fs_sexa(AltStr, parkAlt, 2, 3600); LOGF_DEBUG("Syncing to parked coordinates Az (%s) Alt (%s)...", AzStr, AltStr); if (isSimulation()) { ln_lnlat_posn observer; observer.lat = LocationN[LOCATION_LATITUDE].value; observer.lng = LocationN[LOCATION_LONGITUDE].value; if (observer.lng > 180) observer.lng -= 360; ln_hrz_posn horizontalPos; // Libnova south = 0, west = 90, north = 180, east = 270 horizontalPos.az = parkAz + 180; if (horizontalPos.az > 360) horizontalPos.az -= 360; horizontalPos.alt = parkAlt; ln_equ_posn equatorialPos; ln_get_equ_from_hrz(&horizontalPos, &observer, ln_get_julian_from_sys(), &equatorialPos); currentRA = equatorialPos.ra / 15.0; currentDEC= equatorialPos.dec; } else { if (setAPObjectAZ(PortFD, parkAz) < 0 || (setAPObjectAlt(PortFD, parkAlt)) < 0) { LOG_ERROR("Error setting Az/Alt."); return false; } char syncString[256]; if (APSyncCM(PortFD, syncString) < 0) { LOG_WARN("Sync failed."); return false; } } SetParked(false); return true; } bool LX200AstroPhysics::SetCurrentPark() { ln_hrz_posn horizontalPos; // Libnova south = 0, west = 90, north = 180, east = 270 ln_lnlat_posn observer; observer.lat = LocationN[LOCATION_LATITUDE].value; observer.lng = LocationN[LOCATION_LONGITUDE].value; if (observer.lng > 180) observer.lng -= 360; ln_equ_posn equatorialPos; equatorialPos.ra = currentRA * 15; equatorialPos.dec = currentDEC; ln_get_hrz_from_equ(&equatorialPos, &observer, ln_get_julian_from_sys(), &horizontalPos); double parkAZ = horizontalPos.az - 180; if (parkAZ < 0) parkAZ += 360; double parkAlt = horizontalPos.alt; char AzStr[16], AltStr[16]; fs_sexa(AzStr, parkAZ, 2, 3600); fs_sexa(AltStr, parkAlt, 2, 3600); LOGF_DEBUG("Setting current parking position to coordinates Az (%s) Alt (%s)...", AzStr, AltStr); SetAxis1Park(parkAZ); SetAxis2Park(parkAlt); return true; } bool LX200AstroPhysics::SetDefaultPark() { // Az = 0 for North hemisphere SetAxis1Park(LocationN[LOCATION_LATITUDE].value > 0 ? 0 : 180); // Alt = Latitude SetAxis2Park(LocationN[LOCATION_LATITUDE].value); return true; } void LX200AstroPhysics::syncSideOfPier() { const char *cmd = ":pS#"; // Response char response[16] = { 0 }; int rc = 0, nbytes_read = 0, nbytes_written = 0; LOGF_DEBUG("CMD: <%s>", cmd); tcflush(PortFD, TCIOFLUSH); if ((rc = tty_write(PortFD, cmd, strlen(cmd), &nbytes_written)) != TTY_OK) { char errmsg[256]; tty_error_msg(rc, errmsg, 256); LOGF_ERROR("Error writing to device %s (%d)", errmsg, rc); return; } // Read Side if ((rc = tty_read_section(PortFD, response, '#', 3, &nbytes_read)) != TTY_OK) { char errmsg[256]; tty_error_msg(rc, errmsg, 256); LOGF_ERROR("Error reading from device %s (%d)", errmsg, rc); return; } response[nbytes_read - 1] = '\0'; tcflush(PortFD, TCIOFLUSH); LOGF_DEBUG("RES: <%s>", response); if (!strcmp(response, "East")) setPierSide(INDI::Telescope::PIER_EAST); else if (!strcmp(response, "West")) setPierSide(INDI::Telescope::PIER_WEST); else LOGF_ERROR("Invalid pier side response from device-> %s", response); } bool LX200AstroPhysics::saveConfigItems(FILE *fp) { LX200Generic::saveConfigItems(fp); IUSaveConfigSwitch(fp, &SyncCMRSP); IUSaveConfigSwitch(fp, &APSlewSpeedSP); IUSaveConfigSwitch(fp, &APGuideSpeedSP); return true; } bool LX200AstroPhysics::SetTrackMode(uint8_t mode) { int err=0; if (mode == TRACK_CUSTOM) { if (!isSimulation() && (err = selectAPTrackingMode(PortFD, AP_TRACKING_SIDEREAL)) < 0) { LOGF_ERROR("Error setting tracking mode (%d).", err); return false; } return SetTrackRate(TrackRateN[AXIS_RA].value, TrackRateN[AXIS_DE].value); } if (!isSimulation() && (err = selectAPTrackingMode(PortFD, mode)) < 0) { LOGF_ERROR("Error setting tracking mode (%d).", err); return false; } return true; } bool LX200AstroPhysics::SetTrackEnabled(bool enabled) { return SetTrackMode(enabled ? IUFindOnSwitchIndex(&TrackModeSP) : AP_TRACKING_OFF); } bool LX200AstroPhysics::SetTrackRate(double raRate, double deRate) { // Convert to arcsecs/s to AP sidereal multiplier /* :RR0.0000# = normal sidereal tracking in RA - similar to :RT2# :RR+1.0000# = 1 + normal sidereal = 2X sidereal :RR+9.0000# = 9 + normal sidereal = 10X sidereal :RR-1.0000# = normal sidereal - 1 = 0 or Stop - similar to :RT9# :RR-11.0000# = normal sidereal - 11 = -10X sidereal (East at 10X) :RD0.0000# = normal zero rate for Dec. :RD5.0000# = 5 + normal zero rate = 5X sidereal clockwise from above - equivalent to South :RD-5.0000# = normal zero rate - 5 = 5X sidereal counter-clockwise from above - equivalent to North */ double APRARate = (raRate - TRACKRATE_SIDEREAL) / TRACKRATE_SIDEREAL; double APDERate = deRate / TRACKRATE_SIDEREAL; if (!isSimulation()) { if (setAPRATrackRate(PortFD, APRARate) < 0 || setAPDETrackRate(PortFD, APDERate) < 0) return false; } return true; } bool LX200AstroPhysics::getUTFOffset(double *offset) { return (getAPUTCOffset(PortFD, offset) == 0); } libindi/drivers/telescope/lx200ap.h0000664000175000017500000000755013263645557016501 0ustar jasemjasem/* Astro-Physics INDI driver Copyright (C) 2014 Jasem Mutlaq Based on INDI Astrophysics Driver by Markus Wildi 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 */ #pragma once #include "lx200generic.h" #define SYNCCM 0 #define SYNCCMR 1 #define NOTESTABLISHED 0 #define ESTABLISHED 1 #define MOUNTNOTINITIALIZED 0 #define MOUNTINITIALIZED 1 class LX200AstroPhysics : public LX200Generic { public: LX200AstroPhysics(); ~LX200AstroPhysics() {} typedef enum { MCV_G, MCV_H, MCV_I, MCV_J, MCV_L, MCV_UNKNOWN} ControllerVersion; typedef enum { GTOCP1, GTOCP2, GTOCP3, GTOCP4, GTOCP_UNKNOWN} ServoVersion; virtual bool ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) override; virtual bool ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) override; virtual void ISGetProperties(const char *dev) override; protected: virtual const char *getDefaultName() override; virtual bool initProperties() override; virtual bool updateProperties() override; virtual bool ReadScopeStatus() override; virtual bool Handshake() override; virtual bool Disconnect() override; // Parking virtual bool SetCurrentPark() override; virtual bool SetDefaultPark() override; virtual bool Park() override; virtual bool UnPark() override; virtual bool Sync(double ra, double dec) override; virtual bool Goto(double, double) override; virtual bool updateTime(ln_date *utc, double utc_offset) override; virtual bool updateLocation(double latitude, double longitude, double elevation) override; virtual bool SetSlewRate(int index) override; virtual int SendPulseCmd(int direction, int duration_msec) override; virtual bool getUTFOffset(double *offset) override; // Tracking virtual bool SetTrackMode(uint8_t mode) override; virtual bool SetTrackEnabled(bool enabled) override; virtual bool SetTrackRate(double raRate, double deRate) override; virtual bool saveConfigItems(FILE *fp) override; virtual void debugTriggered(bool enable) override; ISwitch StartUpS[2]; ISwitchVectorProperty StartUpSP; INumber HourangleCoordsN[2]; INumberVectorProperty HourangleCoordsNP; INumber HorizontalCoordsN[2]; INumberVectorProperty HorizontalCoordsNP; ISwitch APSlewSpeedS[3]; ISwitchVectorProperty APSlewSpeedSP; ISwitch SwapS[2]; ISwitchVectorProperty SwapSP; ISwitch SyncCMRS[2]; ISwitchVectorProperty SyncCMRSP; enum { USE_REGULAR_SYNC, USE_CMR_SYNC }; ISwitch APGuideSpeedS[3]; ISwitchVectorProperty APGuideSpeedSP; IText VersionT[1] {}; ITextVectorProperty VersionInfo; IText DeclinationAxisT[1] {}; ITextVectorProperty DeclinationAxisTP; INumber SlewAccuracyN[2]; INumberVectorProperty SlewAccuracyNP; private: bool isMountInit(); bool setBasicDataPart0(); bool setBasicDataPart1(); // Side of pier void syncSideOfPier(); bool timeUpdated=false, locationUpdated=false; ControllerVersion controllerType = MCV_UNKNOWN; ServoVersion servoType = GTOCP_UNKNOWN; uint8_t initStatus = MOUNTNOTINITIALIZED; }; libindi/drivers/telescope/ioptronv3driver.cpp0000664000175000017500000003733513263645557021031 0ustar jasemjasem/* INDI IOptron v3 Driver for firmware version 20171001 or later. Copyright (C) 2018 Jasem Mutlaq 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 */ #include "ioptronv3driver.h" #include "indicom.h" #include #include #include #include #include #include namespace IOPv3 { const std::map Driver::models = { {"0010", "Cube II EQ"}, {"0011", "SmartEQ Pro+"}, {"0025", "CEM25"}, {"0026", "CEM25-EC"}, {"0030", "iEQ30 Pro"}, {"0045", "iEQ45 Pro EQ"}, {"0060", "CEM60"}, {"0061", "CEM60-EC"}, {"0120", "CEM120"}, {"0121", "CEM120-EC"}, {"0122", "CEM120-EC2"}, {"5010", "Cube II AA"}, {"5035", "AZ Mount Pro"}, {"5045", "iEQ45 Pro AA"} }; const uint16_t Driver::IOP_SLEW_RATES[] = {1, 2, 8, 16, 64, 128, 256, 512, 1024}; Driver::Driver(const char *deviceName): m_DeviceName(deviceName) {} bool Driver::sendCommand(const char *command, int count, char *response, uint8_t timeout, uint8_t debugLog) { int errCode = 0; int nbytes_read = 0; int nbytes_written = 0; char errMsg[MAXRBUF]; char res[IOP_BUFFER] = {0}; DEBUGFDEVICE(m_DeviceName, debugLog, "CMD <%s>", command); if (m_Simulation) return true; tcflush(PortFD, TCIOFLUSH); if ((errCode = tty_write(PortFD, command, strlen(command), &nbytes_written)) != TTY_OK) { tty_error_msg(errCode, errMsg, MAXRBUF); DEBUGFDEVICE(m_DeviceName, INDI::Logger::DBG_ERROR, "Write Command Error: %s", errMsg); return false; } if (count == 0) return true; if (count == -1) errCode = tty_read_section(PortFD, res, '#', timeout, &nbytes_read); else errCode = tty_read(PortFD, res, count, timeout, &nbytes_read); if (errCode != TTY_OK) { tty_error_msg(errCode, errMsg, MAXRBUF); DEBUGFDEVICE(m_DeviceName, INDI::Logger::DBG_ERROR, "Read Command Error: %s", errMsg); return false; } // Remove the extra # if (count == -1) res[nbytes_read-1] = 0; DEBUGFDEVICE(m_DeviceName, debugLog, "RES <%s>", res); tcflush(PortFD, TCIOFLUSH); // Copy response to buffer if (response) strncpy(response, res, IOP_BUFFER); if (count == -1 || (count == 1 && res[0] == '1')) return true; return false; } bool Driver::checkConnection(int fd) { char res[IOP_BUFFER]={0}; DEBUGDEVICE(m_DeviceName, INDI::Logger::DBG_DEBUG, "Initializing IOptron using :V# CMD..."); // Set FD for use PortFD = fd; if (m_Simulation) return true; for (int i = 0; i < 2; i++) { if (sendCommand(":V#", -1, res, 3) == false) { usleep(50000); continue; } return (!strcmp(res, "V1.00")); } return false; } void Driver::setDebug(bool enable) { m_Debug = enable; } void Driver::setSimulation(bool enable) { m_Simulation = enable; simData.ra_guide_rate = 0.5; simData.de_guide_rate = 0.5; simData.pier_state = IOP_PIER_WEST; simData.cw_state = IOP_CW_NORMAL; simData.JD = ln_get_julian_from_sys(); simData.utc_offset_minutes = 3 * 60; simData.day_light_saving = false; simData.simInfo.gpsStatus = GPS_DATA_OK; simData.simInfo.hemisphere = HEMI_NORTH; simData.simInfo.slewRate = SR_6; simData.simInfo.timeSource = TS_GPS; simData.simInfo.trackRate = TR_SIDEREAL; simData.simInfo.longitude = 48.1; simData.simInfo.latitude = 29.5; } void Driver::setSimGPSstatus(IOP_GPS_STATUS value) { simData.simInfo.gpsStatus = value; } void Driver::setSimSytemStatus(IOP_SYSTEM_STATUS value) { simData.simInfo.systemStatus = value; } void Driver::setSimTrackRate(IOP_TRACK_RATE value) { simData.simInfo.trackRate = value; } void Driver::setSimSlewRate(IOP_SLEW_RATE value) { simData.simInfo.slewRate = value; } void Driver::setSimTimeSource(IOP_TIME_SOURCE value) { simData.simInfo.timeSource = value; } void Driver::setSimHemisphere(IOP_HEMISPHERE value) { simData.simInfo.hemisphere = value; } void Driver::setSimRA(double ra) { simData.ra = ra; } void Driver::setSimDE(double de) { simData.de = de; } void Driver::setSimGuideRate(double raRate, double deRate) { simData.ra_guide_rate = raRate; simData.de_guide_rate = deRate; } void Driver::setSimLongLat(double longitude, double latitude) { simData.simInfo.longitude = longitude; simData.simInfo.latitude = latitude; } bool Driver::getStatus(IOPInfo *info) { char res[IOP_BUFFER] = {0}; if (m_Simulation) { int iopLongitude = simData.simInfo.longitude * 360000; int iopLatitude = (simData.simInfo.latitude+90) * 360000; snprintf(res, IOP_BUFFER, "%c%08d%08d%d%d%d%d%d%d", simData.simInfo.longitude > 0 ? '+' : '-', iopLongitude, iopLatitude, simData.simInfo.gpsStatus, simData.simInfo.systemStatus, simData.simInfo.trackRate, simData.simInfo.slewRate, simData.simInfo.timeSource, simData.simInfo.hemisphere); } else if (sendCommand(":GLS#", -1, res) == false) return false; char longPart[16]={0}, latPart[16]={0}; strncpy(longPart, res, 9); strncpy(latPart, res+9, 8); int arcsecLongitude = atoi(longPart); int arcsecLatitude = atoi(latPart); info->longitude = arcsecLongitude / 360000.0; info->latitude = arcsecLatitude / 360000.0 - 90.0; info->gpsStatus = (IOP_GPS_STATUS)(res[17] - '0'); info->systemStatus = (IOP_SYSTEM_STATUS)(res[18] - '0'); info->trackRate = (IOP_TRACK_RATE)(res[19] - '0'); info->slewRate = (IOP_SLEW_RATE)(res[20] - '0'); info->timeSource = (IOP_TIME_SOURCE)(res[21] - '0'); info->hemisphere = (IOP_HEMISPHERE)(res[22] - '0'); return true; } bool Driver::getFirmwareInfo(FirmwareInfo *info) { bool rc1 = getModel(info->Model); bool rc2 = getMainFirmware(info->MainBoardFirmware, info->ControllerFirmware); bool rc3 = getRADEFirmware(info->RAFirmware, info->DEFirmware); return (rc1 && rc2 && rc3); } bool Driver::getModel(std::string &model) { char res[IOP_BUFFER] = {0}; if (m_Simulation) strcpy(res, "0120"); else if (sendCommand(":MountInfo#", 4, res) == false) return false; if (models.find(res) != models.end()) model = models.at(res); else model = "Unknown"; return true; } bool Driver::getMainFirmware(std::string &mainFirmware, std::string &controllerFirmware) { char res[IOP_BUFFER] = {0}; if (m_Simulation) strcpy(res, "180321171001"); else if (sendCommand(":FW1#", -1, res) == false) return false; char mStr[16]={0}, cStr[16]={0}; strncpy(mStr, res, 6); strncpy(cStr, res+6, 6); mainFirmware = mStr; controllerFirmware = cStr; return true; } bool Driver::getRADEFirmware(std::string &RAFirmware, std::string &DEFirmware) { char res[IOP_BUFFER] = {0}; if (m_Simulation) strcpy(res, "140324140101"); else if (sendCommand(":FW2#", -1, res) == false) return false; char mStr[16]={0}, cStr[16]={0}; strncpy(mStr, res, 6); strncpy(cStr, res+6, 6); RAFirmware = mStr; DEFirmware = cStr; return true; } bool Driver::startMotion(IOP_DIRECTION dir) { switch (dir) { case IOP_N: return sendCommand(":mn#", 0); break; case IOP_S: return sendCommand(":ms#", 0); break; case IOP_W: return sendCommand(":mw#", 0); break; case IOP_E: return sendCommand(":me#", 0); break; } return false; } bool Driver::stopMotion(IOP_DIRECTION dir) { switch (dir) { case IOP_N: case IOP_S: return sendCommand(":qD#"); break; case IOP_W: case IOP_E: return sendCommand(":qR#"); break; } return false; } bool Driver::findHome() { return sendCommand(":MSH#"); } bool Driver::gotoHome() { return sendCommand(":MH#"); } bool Driver::setCurrentHome() { return sendCommand(":SZP#"); } bool Driver::setSlewRate(IOP_SLEW_RATE rate) { char cmd[IOP_BUFFER] = {0}; snprintf(cmd, IOP_BUFFER, ":SR%d#", ((int)rate) + 1); simData.simInfo.slewRate = rate; return sendCommand(cmd); } bool Driver::setTrackMode(IOP_TRACK_RATE rate) { simData.simInfo.trackRate = rate; switch (rate) { case TR_SIDEREAL: return sendCommand(":RT0#"); break; case TR_LUNAR: return sendCommand(":RT1#"); break; case TR_SOLAR: return sendCommand(":RT2#"); break; case TR_KING: return sendCommand(":RT3#"); break; case TR_CUSTOM: return sendCommand(":RT4#"); break; } return false; } bool Driver::setCustomRATrackRate(double rate) { if (rate < 0.1 || rate > 1.9) return false; char cmd[IOP_BUFFER] = {0}; snprintf(cmd, IOP_BUFFER, ":RR%05d#", static_cast(rate*10000)); return sendCommand(cmd); } bool Driver::setGuideRate(double RARate, double DERate) { if (RARate < 0.01 || RARate > 0.9 || DERate < 0.01 || DERate > 0.9) return false; char cmd[IOP_BUFFER] = {0}; snprintf(cmd, IOP_BUFFER, ":RG%02d%02d#", static_cast(RARate*100), static_cast(DERate*100)); return sendCommand(cmd); } bool Driver::getGuideRate(double *RARate, double *DERate) { char res[IOP_BUFFER] = {0}; if (m_Simulation) snprintf(res, IOP_BUFFER, "%02d%02d", static_cast(simData.ra_guide_rate*100), static_cast(simData.de_guide_rate*100)); else if (sendCommand(":AG#", -1, res) == false) return false; char raStr[8]={0}, deStr[8]={0}; strncpy(raStr, res, 2); strncpy(deStr, res+2, 2); *RARate = atoi(raStr) / 100.0; *DERate = atoi(deStr) / 100.0; return true; } bool Driver::startGuide(IOP_DIRECTION dir, uint32_t ms) { char cmd[IOP_BUFFER] = {0}; char dir_c = 0; switch (dir) { case IOP_N: dir_c = 'n'; break; case IOP_S: dir_c = 's'; break; case IOP_W: dir_c = 'w'; break; case IOP_E: dir_c = 'e'; break; } snprintf(cmd, IOP_BUFFER, ":M%c%05d#", dir_c, ms); return sendCommand(cmd, 0); } bool Driver::park() { return sendCommand(":MP1#"); } bool Driver::unpark() { //NB: This command only available in CEM120 series, CEM60 series, iEQ45 Pro, iEQ45 Pro //AA and iEQ30 Pro. setSimSytemStatus(ST_STOPPED); return sendCommand(":MP0#"); } bool Driver::abort() { if (simData.simInfo.systemStatus == ST_SLEWING) simData.simInfo.systemStatus = simData.simInfo.rememberSystemStatus; return sendCommand(":Q#"); } bool Driver::slewNormal() { simData.simInfo.rememberSystemStatus = simData.simInfo.systemStatus; simData.simInfo.systemStatus = ST_SLEWING; return sendCommand(":#MS1"); } bool Driver::slewCWUp() { simData.simInfo.rememberSystemStatus = simData.simInfo.systemStatus; simData.simInfo.systemStatus = ST_SLEWING; return sendCommand(":#MS2"); } bool Driver::sync() { return sendCommand(":CM#"); } bool Driver::setTrackEnabled(bool enabled) { simData.simInfo.systemStatus = enabled ? ST_TRACKING_PEC_ON : ST_STOPPED; char cmd[IOP_BUFFER] = {0}; snprintf(cmd, IOP_BUFFER, ":ST%d#", enabled ? 1 : 0); return sendCommand(cmd); } bool Driver::setRA(double ra) { // Send RA in centi-arcsecond (0.01) resolution. // ra is passed as hours. casRA is in centi-arcseconds in degrees. uint32_t casRA = ra * 15 * 60 * 60 * 100; simData.ra = ra; char cmd[IOP_BUFFER] = {0}; snprintf(cmd, IOP_BUFFER, ":SRA%09d#", casRA); return sendCommand(cmd); } bool Driver::setDE(double de) { // Send DE in centi-arcsecond (0.01) resolution. // de is passed as degrees. casDE is in centi-arcseconds in degrees. uint32_t casDE = fabs(de) * 60 * 60 * 100; simData.de = de; char cmd[IOP_BUFFER] = {0}; snprintf(cmd, IOP_BUFFER, ":Sd%c%08d#", de >= 0 ? '+' : '-', casDE); return sendCommand(cmd); } bool Driver::setLongitude(double longitude) { uint32_t casLongitude = fabs(longitude) * 60 * 60 * 100; simData.simInfo.longitude = longitude; char cmd[IOP_BUFFER] = {0}; snprintf(cmd, IOP_BUFFER, ":SLO%c%08d#", longitude >= 0 ? '+' : '-', casLongitude); return sendCommand(cmd); } bool Driver::setLatitude(double latitude) { uint32_t casLatitude = fabs(latitude) * 60 * 60 * 100; simData.simInfo.latitude = latitude; char cmd[IOP_BUFFER] = {0}; snprintf(cmd, IOP_BUFFER, ":SLA%c%08d#", latitude >= 0 ? '+' : '-', casLatitude); return sendCommand(cmd); } bool Driver::setUTCDateTime(double JD) { uint64_t msJD = (JD - J2000) * 8.64e+7; char cmd[IOP_BUFFER] = {0}; snprintf(cmd, IOP_BUFFER, ":SUT%013" PRIu64 "#", msJD); simData.JD = JD; return sendCommand(cmd); } bool Driver::setUTCOffset(int offsetMinutes) { char cmd[IOP_BUFFER] = {0}; snprintf(cmd, IOP_BUFFER, ":SG%c%03d#", offsetMinutes >= 0 ? '+' : '-', abs(offsetMinutes)); simData.utc_offset_minutes = offsetMinutes; return sendCommand(cmd); } bool Driver::setDaylightSaving(bool enabled) { char cmd[IOP_BUFFER] = {0}; snprintf(cmd, IOP_BUFFER, ":SDS%c#", enabled ? '1' : '0'); simData.day_light_saving = enabled; return sendCommand(cmd); } bool Driver::getCoords(double *ra, double *de, IOP_PIER_STATE *pierState, IOP_CW_STATE *cwState) { char res[IOP_BUFFER] = {0}; if (m_Simulation) { snprintf(res, IOP_BUFFER, "%c%08d%09d%d%d", (simData.de >= 0 ? '+' : '-'), static_cast(fabs(simData.de)*60*60*100), static_cast(simData.ra*15*60*60*100), simData.pier_state, simData.cw_state); } else if (sendCommand(":GEP#", -1, res, IOP_TIMEOUT, INDI::Logger::DBG_EXTRA_1) == false) return false; char deStr[16]={0}, raStr[16]={0}; strncpy(deStr, res, 9); strncpy(raStr, res+9, 9); *de = atoi(deStr) / (60.0*60.0*100.0); *ra = atoi(raStr) / (15.0*60.0*60.0*100.0); *pierState = static_cast(res[18] - '0'); *cwState = static_cast(res[19] - '0'); return true; } bool Driver::getUTCDateTime(double *JD, int *utcOffsetMinutes, bool *dayLightSaving) { char res[IOP_BUFFER] = {0}; if (m_Simulation) { snprintf(res, IOP_BUFFER, "%c%03d%c%013" PRIu64, (simData.utc_offset_minutes >= 0 ? '+' : '-'), abs(simData.utc_offset_minutes), (simData.day_light_saving ? '1' : '0'), static_cast((simData.JD-J2000)*8.64e+7)); } else if (sendCommand(":GUT#", -1, res) == false) return false; char offsetStr[16]={0}, JDStr[16]={0}; strncpy(offsetStr, res, 4); strncpy(JDStr, res+5, 13); *utcOffsetMinutes = atoi(offsetStr); *dayLightSaving = (res[4] == '1'); uint64_t iopJD = std::stoull(JDStr); *JD = (iopJD / 8.64e+7) + J2000; return true; } } libindi/drivers/telescope/lx200zeq25.cpp0000664000175000017500000010162013263645557017373 0ustar jasemjasem/* ZEQ25 INDI driver Copyright (C) 2015 Jasem Mutlaq 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 */ #include "lx200zeq25.h" #include "indicom.h" #include "lx200driver.h" #include #include #include #include #include /* Simulation Parameters */ #define SLEWRATE 1 /* slew rate, degrees/s */ #define SIDRATE 0.004178 /* sidereal rate, degrees/s */ LX200ZEQ25::LX200ZEQ25() { setVersion(1, 3); setLX200Capability(LX200_HAS_PULSE_GUIDING); SetTelescopeCapability(TELESCOPE_CAN_PARK | TELESCOPE_CAN_SYNC | TELESCOPE_CAN_GOTO | TELESCOPE_CAN_ABORT | TELESCOPE_HAS_TIME | TELESCOPE_HAS_LOCATION | TELESCOPE_HAS_TRACK_MODE, 9); } bool LX200ZEQ25::initProperties() { LX200Generic::initProperties(); SetParkDataType(PARK_AZ_ALT); strcpy(SlewRateS[0].label, "1x"); strcpy(SlewRateS[1].label, "2x"); strcpy(SlewRateS[2].label, "8x"); strcpy(SlewRateS[3].label, "16x"); strcpy(SlewRateS[4].label, "64x"); strcpy(SlewRateS[5].label, "128x"); strcpy(SlewRateS[6].label, "256x"); strcpy(SlewRateS[7].label, "512x"); strcpy(SlewRateS[8].label, "MAX"); IUFillSwitch(&HomeS[0], "Home", "", ISS_OFF); IUFillSwitchVector(&HomeSP, HomeS, 1, getDeviceName(), "Home", "Home", MAIN_CONTROL_TAB, IP_RW, ISR_ATMOST1, 0, IPS_IDLE); /* How fast do we guide compared to sidereal rate */ IUFillNumber(&GuideRateN[0], "GUIDE_RATE", "x Sidereal", "%g", 0.1, 0.9, 0.1, 0.5); IUFillNumberVector(&GuideRateNP, GuideRateN, 1, getDeviceName(), "GUIDE_RATE", "Guiding Rate", MOTION_TAB, IP_RW, 0, IPS_IDLE); return true; } bool LX200ZEQ25::updateProperties() { LX200Generic::updateProperties(); if (isConnected()) { defineSwitch(&HomeSP); defineNumber(&GuideRateNP); } else { deleteProperty(HomeSP.name); deleteProperty(GuideRateNP.name); } return true; } const char *LX200ZEQ25::getDefaultName() { return (const char *)"ZEQ25"; } bool LX200ZEQ25::checkConnection() { if (isSimulation()) return true; const struct timespec timeout = {0, 50000000L}; char initCMD[] = ":V#"; int errcode = 0; char errmsg[MAXRBUF]; char response[8]; int nbytes_read = 0; int nbytes_written = 0; LOG_DEBUG("Initializing IOptron using :V# CMD..."); for (int i = 0; i < 2; i++) { if ((errcode = tty_write(PortFD, initCMD, 3, &nbytes_written)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); LOGF_ERROR("%s", errmsg); nanosleep(&timeout, NULL); continue; } if ((errcode = tty_read_section(PortFD, response, '#', 3, &nbytes_read))) { tty_error_msg(errcode, errmsg, MAXRBUF); LOGF_ERROR("%s", errmsg); nanosleep(&timeout, NULL); continue; } if (nbytes_read > 0) { response[nbytes_read] = '\0'; LOGF_DEBUG("RES (%s)", response); if (!strcmp(response, "V1.00#")) return true; } nanosleep(&timeout, NULL); } return false; } bool LX200ZEQ25::ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) { if (dev != nullptr && strcmp(dev, getDeviceName()) == 0) { if (strcmp(HomeSP.name, name) == 0) { // If already home, nothing to be done //if (HomeS[0].s == ISS_ON) if (isZEQ25Home()) { LOG_WARN("Telescope is already homed."); HomeS[0].s = ISS_ON; HomeSP.s = IPS_OK; IDSetSwitch(&HomeSP, nullptr); return true; } if (gotoZEQ25Home() < 0) { HomeSP.s = IPS_ALERT; LOG_ERROR("Error slewing to home position."); } else { HomeSP.s = IPS_BUSY; LOG_INFO("Slewing to home position."); } IDSetSwitch(&HomeSP, nullptr); return true; } } return LX200Generic::ISNewSwitch(dev, name, states, names, n); } bool LX200ZEQ25::ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) { if (dev != nullptr && strcmp(dev, getDeviceName()) == 0) { // Guiding Rate if (!strcmp(name, GuideRateNP.name)) { IUUpdateNumber(&GuideRateNP, values, names, n); if (setZEQ25GuideRate(GuideRateN[0].value) == TTY_OK) GuideRateNP.s = IPS_OK; else GuideRateNP.s = IPS_ALERT; IDSetNumber(&GuideRateNP, nullptr); return true; } } return LX200Generic::ISNewNumber(dev, name, values, names, n); } bool LX200ZEQ25::isZEQ25Home() { const struct timespec timeout = {0, 10000000L}; char bool_return[2]; int error_type; int nbytes_write = 0, nbytes_read = 0; if (isSimulation()) return true; DEBUG(DBG_SCOPE, "CMD <:AH#>"); if ((error_type = tty_write_string(PortFD, ":AH#", &nbytes_write)) != TTY_OK) return false; error_type = tty_read(PortFD, bool_return, 1, 5, &nbytes_read); // JM: Hack from Jon in the INDI forums to fix longitude/latitude settings failure on ZEQ25 nanosleep(&timeout, NULL); tcflush(PortFD, TCIFLUSH); nanosleep(&timeout, NULL); if (nbytes_read < 1) return false; DEBUGF(DBG_SCOPE, "RES <%c>", bool_return[0]); return (bool_return[0] == '1'); } int LX200ZEQ25::gotoZEQ25Home() { return setZEQ25StandardProcedure(PortFD, ":MH#"); } bool LX200ZEQ25::isSlewComplete() { int errcode = 0; char errmsg[MAXRBUF]; char response[8]; int nbytes_read = 0; int nbytes_written = 0; //strncpy(cmd, ":SE#", 16); const char *cmd = ":SE#"; LOGF_DEBUG("CMD <%s>", cmd); tcflush(PortFD, TCIOFLUSH); if ((errcode = tty_write(PortFD, cmd, 4, &nbytes_written)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); LOGF_ERROR("%s", errmsg); return false; } if ((errcode = tty_read(PortFD, response, 1, 3, &nbytes_read))) { tty_error_msg(errcode, errmsg, MAXRBUF); LOGF_ERROR("%s", errmsg); return false; } if (nbytes_read > 0) { response[nbytes_read] = '\0'; LOGF_DEBUG("RES (%s)", response); tcflush(PortFD, TCIFLUSH); if (response[0] == '0') return true; else return false; } LOGF_ERROR("Only received #%d bytes, expected 1.", nbytes_read); return false; } bool LX200ZEQ25::getMountInfo() { char cmd[] = ":MountInfo#"; int errcode = 0; char errmsg[MAXRBUF]; char response[16]; int nbytes_read = 0; int nbytes_written = 0; LOGF_DEBUG("CMD <%s>", cmd); if ((errcode = tty_write(PortFD, cmd, strlen(cmd), &nbytes_written)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); LOGF_ERROR("%s", errmsg); return false; } if ((errcode = tty_read(PortFD, response, 4, 3, &nbytes_read))) { tty_error_msg(errcode, errmsg, MAXRBUF); LOGF_ERROR("%s", errmsg); return false; } if (nbytes_read > 0) { response[nbytes_read] = '\0'; LOGF_DEBUG("RES (%s)", response); if (nbytes_read == 4) { if (!strcmp(response, "8407")) LOG_INFO("Detected iEQ45/iEQ30 Mount."); else if (!strcmp(response, "8497")) LOG_INFO("Detected iEQ45 AA Mount."); else if (!strcmp(response, "8408")) LOG_INFO("Detected ZEQ25 Mount."); else if (!strcmp(response, "8498")) LOG_INFO("Detected SmartEQ Mount."); else LOG_INFO("Unknown mount detected."); tcflush(PortFD, TCIFLUSH); return true; } } LOGF_ERROR("Only received #%d bytes, expected 4.", nbytes_read); return false; } void LX200ZEQ25::getBasicData() { getMountInfo(); int moveRate = getZEQ25MoveRate(); if (moveRate >= 0) { IUResetSwitch(&SlewRateSP); SlewRateS[moveRate].s = ISS_ON; SlewRateSP.s = IPS_OK; IDSetSwitch(&SlewRateSP, nullptr); } if (InitPark()) { // If loading parking data is successful, we just set the default parking values. SetAxis1ParkDefault(LocationN[LOCATION_LATITUDE].value >= 0 ? 0 : 180); SetAxis2ParkDefault(LocationN[LOCATION_LATITUDE].value); } else { // Otherwise, we set all parking data to default in case no parking data is found. SetAxis1Park(LocationN[LOCATION_LATITUDE].value >= 0 ? 0 : 180); SetAxis2Park(LocationN[LOCATION_LATITUDE].value); SetAxis1ParkDefault(LocationN[LOCATION_LATITUDE].value >= 0 ? 0 : 180); SetAxis2ParkDefault(LocationN[LOCATION_LATITUDE].value); } bool isMountParked = isZEQ25Parked(); if (isMountParked != isParked()) SetParked(isMountParked); // Is home? LOG_DEBUG("Checking if mount is at home position..."); if (isZEQ25Home()) { HomeS[0].s = ISS_ON; HomeSP.s = IPS_OK; IDSetSwitch(&HomeSP, nullptr); } LOG_DEBUG("Getting guiding rate..."); double guideRate = 0; if (getZEQ25GuideRate(&guideRate) == TTY_OK) { GuideRateN[0].value = guideRate; IDSetNumber(&GuideRateNP, nullptr); } } bool LX200ZEQ25::Goto(double r, double d) { const struct timespec timeout = {0, 100000000L}; targetRA = r; targetDEC = d; char RAStr[64], DecStr[64]; fs_sexa(RAStr, targetRA, 2, 3600); fs_sexa(DecStr, targetDEC, 2, 3600); // If moving, let's stop it first. if (EqNP.s == IPS_BUSY) { if (!isSimulation() && abortSlew(PortFD) < 0) { AbortSP.s = IPS_ALERT; IDSetSwitch(&AbortSP, "Abort slew failed."); return false; } AbortSP.s = IPS_OK; EqNP.s = IPS_IDLE; IDSetSwitch(&AbortSP, "Slew aborted."); IDSetNumber(&EqNP, nullptr); if (MovementNSSP.s == IPS_BUSY || MovementWESP.s == IPS_BUSY) { MovementNSSP.s = MovementWESP.s = IPS_IDLE; EqNP.s = IPS_IDLE; IUResetSwitch(&MovementNSSP); IUResetSwitch(&MovementWESP); IDSetSwitch(&MovementNSSP, nullptr); IDSetSwitch(&MovementWESP, nullptr); } // sleep for 100 mseconds nanosleep(&timeout, NULL); } if (!isSimulation()) { if (setObjectRA(PortFD, targetRA) < 0 || (setObjectDEC(PortFD, targetDEC)) < 0) { EqNP.s = IPS_ALERT; IDSetNumber(&EqNP, "Error setting RA/DEC."); return false; } if (slewZEQ25() == false) { EqNP.s = IPS_ALERT; LOGF_DEBUG("Error Slewing to JNow RA %s - DEC %s\n", RAStr, DecStr); slewError(1); return false; } } TrackState = SCOPE_SLEWING; EqNP.s = IPS_BUSY; LOGF_INFO("Slewing to RA: %s - DEC: %s", RAStr, DecStr); return true; } bool LX200ZEQ25::slewZEQ25() { DEBUGF(DBG_SCOPE, "<%s>", __FUNCTION__); char slewNum[2]; int error_type; int nbytes_write = 0, nbytes_read = 0; DEBUGF(DBG_SCOPE, "CMD <%s>", ":MS#"); if ((error_type = tty_write_string(PortFD, ":MS#", &nbytes_write)) != TTY_OK) return error_type; error_type = tty_read(PortFD, slewNum, 1, 3, &nbytes_read); if (nbytes_read < 1) { DEBUGF(DBG_SCOPE, "RES ERROR <%d>", error_type); return error_type; } /* We don't need to read the string message, just return corresponding error code */ tcflush(PortFD, TCIFLUSH); DEBUGF(DBG_SCOPE, "RES <%c>", slewNum[0]); return (slewNum[0] == '1'); } bool LX200ZEQ25::SetSlewRate(int index) { if (isSimulation()) return true; char cmd[8]; int errcode = 0; char errmsg[MAXRBUF]; char response[2]; int nbytes_read = 0; int nbytes_written = 0; snprintf(cmd, 8, ":SR%d#", index + 1); LOGF_DEBUG("CMD <%s>", cmd); if ((errcode = tty_write(PortFD, cmd, strlen(cmd), &nbytes_written)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); LOGF_ERROR("%s", errmsg); return false; } if ((errcode = tty_read(PortFD, response, 1, 3, &nbytes_read))) { tty_error_msg(errcode, errmsg, MAXRBUF); LOGF_ERROR("%s", errmsg); return false; } if (nbytes_read > 0) { response[nbytes_read] = '\0'; LOGF_DEBUG("RES (%s)", response); tcflush(PortFD, TCIFLUSH); return (response[0] == '1'); } LOGF_ERROR("Only received #%d bytes, expected 1.", nbytes_read); return false; } int LX200ZEQ25::getZEQ25MoveRate() { if (isSimulation()) { return IUFindOnSwitchIndex(&SlewRateSP); } char cmd[] = ":Gr#"; int errcode = 0; char errmsg[MAXRBUF]; char response[3]; int nbytes_read = 0; int nbytes_written = 0; LOGF_DEBUG("CMD <%s>", cmd); if ((errcode = tty_write(PortFD, cmd, strlen(cmd), &nbytes_written)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); LOGF_ERROR("%s", errmsg); return -1; } if ((errcode = tty_read_section(PortFD, response, '#', 3, &nbytes_read))) { tty_error_msg(errcode, errmsg, MAXRBUF); LOGF_ERROR("%s", errmsg); return -1; } if (nbytes_read > 0) { response[nbytes_read - 1] = '\0'; LOGF_DEBUG("RES (%s)", response); tcflush(PortFD, TCIFLUSH); int moveRate = -1; sscanf(response, "%d", &moveRate); return moveRate; } LOGF_ERROR("Only received #%d bytes, expected 2.", nbytes_read); return -1; } bool LX200ZEQ25::updateTime(ln_date *utc, double utc_offset) { struct ln_zonedate ltm; if (isSimulation()) return true; ln_date_to_zonedate(utc, <m, utc_offset * 3600.0); JD = ln_get_julian_day(utc); LOGF_DEBUG("New JD is %f", (float)JD); // Set Local Time if (setLocalTime(PortFD, ltm.hours, ltm.minutes, ltm.seconds) < 0) { LOG_ERROR("Error setting local time."); return false; } if (setCalenderDate(PortFD, ltm.days, ltm.months, ltm.years) < 0) { LOG_ERROR("Error setting local date."); return false; } if (setZEQ25UTCOffset(utc_offset) < 0) { LOG_ERROR("Error setting UTC Offset."); return false; } return true; } bool LX200ZEQ25::updateLocation(double latitude, double longitude, double elevation) { INDI_UNUSED(elevation); if (isSimulation()) return true; double final_longitude; if (longitude > 180) final_longitude = longitude - 360.0; else final_longitude = longitude; if (!isSimulation() && setZEQ25Longitude(final_longitude) < 0) { LOG_ERROR("Error setting site longitude coordinates"); return false; } if (!isSimulation() && setZEQ25Latitude(latitude) < 0) { LOG_ERROR("Error setting site latitude coordinates"); return false; } char l[32], L[32]; fs_sexa(l, latitude, 3, 3600); fs_sexa(L, longitude, 4, 3600); LOGF_INFO("Site location updated to Lat %.32s - Long %.32s", l, L); return true; } int LX200ZEQ25::setZEQ25Longitude(double Long) { int d, m, s; char sign; char temp_string[32]; if (Long > 0) sign = '+'; else sign = '-'; getSexComponents(Long, &d, &m, &s); snprintf(temp_string, sizeof(temp_string), ":Sg %c%03d:%02d:%02d#", sign, abs(d), m, s); return (setZEQ25StandardProcedure(PortFD, temp_string)); } int LX200ZEQ25::setZEQ25Latitude(double Lat) { int d, m, s; char sign; char temp_string[32]; if (Lat > 0) sign = '+'; else sign = '-'; getSexComponents(Lat, &d, &m, &s); snprintf(temp_string, sizeof(temp_string), ":St %c%02d:%02d:%02d#", sign, abs(d), m, s); return (setZEQ25StandardProcedure(PortFD, temp_string)); } int LX200ZEQ25::setZEQ25UTCOffset(double hours) { char temp_string[16]; char sign; int h = 0, m = 0, s = 0; if (hours > 0) sign = '+'; else sign = '-'; getSexComponents(hours, &h, &m, &s); snprintf(temp_string, sizeof(temp_string), ":SG %c%02d:%02d#", sign, abs(h), m); return (setZEQ25StandardProcedure(PortFD, temp_string)); } int LX200ZEQ25::setZEQ25StandardProcedure(int fd, const char *data) { const struct timespec timeout = {0, 10000000L}; char bool_return[2]; int error_type; int nbytes_write = 0, nbytes_read = 0; DEBUGF(DBG_SCOPE, "CMD <%s>", data); if ((error_type = tty_write_string(fd, data, &nbytes_write)) != TTY_OK) return error_type; error_type = tty_read(fd, bool_return, 1, 5, &nbytes_read); // JM: Hack from Jon in the INDI forums to fix longitude/latitude settings failure on ZEQ25 nanosleep(&timeout, NULL); tcflush(fd, TCIFLUSH); nanosleep(&timeout, NULL); if (nbytes_read < 1) return error_type; DEBUGF(DBG_SCOPE, "RES <%c>", bool_return[0]); if (bool_return[0] == '0') { DEBUGF(DBG_SCOPE, "CMD <%s> failed.", data); return -1; } DEBUGF(DBG_SCOPE, "CMD <%s> successful.", data); return 0; } bool LX200ZEQ25::MoveNS(INDI_DIR_NS dir, TelescopeMotionCommand command) { int current_move = (dir == DIRECTION_NORTH) ? LX200_NORTH : LX200_SOUTH; switch (command) { case MOTION_START: if (!isSimulation() && moveZEQ25To(current_move) < 0) { LOG_ERROR("Error setting N/S motion direction."); return false; } else LOGF_INFO("Moving toward %s.", (current_move == LX200_NORTH) ? "North" : "South"); break; case MOTION_STOP: if (!isSimulation() && haltZEQ25Movement() < 0) { LOG_ERROR("Error stopping N/S motion."); return false; } else LOGF_INFO("Movement toward %s halted.", (current_move == LX200_NORTH) ? "North" : "South"); break; } return true; } bool LX200ZEQ25::MoveWE(INDI_DIR_WE dir, TelescopeMotionCommand command) { int current_move = (dir == DIRECTION_WEST) ? LX200_WEST : LX200_EAST; switch (command) { case MOTION_START: if (!isSimulation() && moveZEQ25To(current_move) < 0) { LOG_ERROR("Error setting W/E motion direction."); return false; } else LOGF_INFO("Moving toward %s.", (current_move == LX200_WEST) ? "West" : "East"); break; case MOTION_STOP: if (!isSimulation() && haltZEQ25Movement() < 0) { LOG_ERROR("Error stopping W/E motion."); return false; } else LOGF_INFO("Movement toward %s halted.", (current_move == LX200_WEST) ? "West" : "East"); break; } return true; } int LX200ZEQ25::moveZEQ25To(int direction) { DEBUGF(DBG_SCOPE, "<%s>", __FUNCTION__); int nbytes_write = 0; switch (direction) { case LX200_NORTH: DEBUGF(DBG_SCOPE, "CMD <%s>", ":mn#"); tty_write_string(PortFD, ":mn#", &nbytes_write); break; case LX200_WEST: DEBUGF(DBG_SCOPE, "CMD <%s>", ":mw#"); tty_write_string(PortFD, ":mw#", &nbytes_write); break; case LX200_EAST: DEBUGF(DBG_SCOPE, "CMD <%s>", ":me#"); tty_write_string(PortFD, ":me#", &nbytes_write); break; case LX200_SOUTH: DEBUGF(DBG_SCOPE, "CMD <%s>", ":ms#"); tty_write_string(PortFD, ":ms#", &nbytes_write); break; default: break; } tcflush(PortFD, TCIFLUSH); return 0; } int LX200ZEQ25::haltZEQ25Movement() { DEBUGF(DBG_SCOPE, "<%s>", __FUNCTION__); int error_type; int nbytes_write = 0; if ((error_type = tty_write_string(PortFD, ":q#", &nbytes_write)) != TTY_OK) return error_type; tcflush(PortFD, TCIFLUSH); return 0; } bool LX200ZEQ25::SetTrackMode(uint8_t mode) { return (setZEQ25TrackMode(mode) == 0); } int LX200ZEQ25::setZEQ25TrackMode(int mode) { DEBUGF(DBG_SCOPE, "<%s>", __FUNCTION__); // We don't support KING mode :RT3, so we turn mode=3 to custom :RT4# if (mode == 3) mode = 4; char cmd[6]; snprintf(cmd, 6, ":RT%d#", mode); return setZEQ25StandardProcedure(PortFD, cmd); } int LX200ZEQ25::setZEQ25Park() { int error_type; int nbytes_write = 0; LOGF_DEBUG("CMD <%s>", ":MP1#"); if ((error_type = tty_write_string(PortFD, ":MP1#", &nbytes_write)) != TTY_OK) return error_type; tcflush(PortFD, TCIFLUSH); return 0; } int LX200ZEQ25::setZEQ25UnPark() { int error_type; int nbytes_write = 0; LOGF_DEBUG("CMD <%s>", ":MP0#"); if ((error_type = tty_write_string(PortFD, ":MP0#", &nbytes_write)) != TTY_OK) return error_type; tcflush(PortFD, TCIFLUSH); return 0; } bool LX200ZEQ25::isZEQ25Parked() { if (isSimulation()) { return isParked(); } char cmd[] = ":AP#"; int errcode = 0; char errmsg[MAXRBUF]; char response[2]; int nbytes_read = 0; int nbytes_written = 0; LOGF_DEBUG("CMD <%s>", cmd); if ((errcode = tty_write(PortFD, cmd, strlen(cmd), &nbytes_written)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); LOGF_ERROR("%s", errmsg); return false; } if ((errcode = tty_read(PortFD, response, 1, 3, &nbytes_read))) { tty_error_msg(errcode, errmsg, MAXRBUF); LOGF_ERROR("%s", errmsg); return false; } if (nbytes_read > 0) { response[nbytes_read] = '\0'; LOGF_DEBUG("RES (%s)", response); tcflush(PortFD, TCIFLUSH); return (response[0] == '1'); } LOGF_ERROR("Only received #%d bytes, expected 1.", nbytes_read); return false; } bool LX200ZEQ25::SetCurrentPark() { ln_hrz_posn horizontalPos; // Libnova south = 0, west = 90, north = 180, east = 270 ln_lnlat_posn observer; observer.lat = LocationN[LOCATION_LATITUDE].value; observer.lng = LocationN[LOCATION_LONGITUDE].value; if (observer.lng > 180) observer.lng -= 360; ln_equ_posn equatorialPos; equatorialPos.ra = currentRA * 15; equatorialPos.dec = currentDEC; ln_get_hrz_from_equ(&equatorialPos, &observer, ln_get_julian_from_sys(), &horizontalPos); double parkAZ = horizontalPos.az - 180; if (parkAZ < 0) parkAZ += 360; double parkAlt = horizontalPos.alt; char AzStr[16], AltStr[16]; fs_sexa(AzStr, parkAZ, 2, 3600); fs_sexa(AltStr, parkAlt, 2, 3600); LOGF_DEBUG("Setting current parking position to coordinates Az (%s) Alt (%s)...", AzStr, AltStr); SetAxis1Park(parkAZ); SetAxis2Park(parkAlt); return true; } bool LX200ZEQ25::SetDefaultPark() { // Az = 0 for North hemisphere SetAxis1Park(LocationN[LOCATION_LATITUDE].value > 0 ? 0 : 180); // Alt = Latitude SetAxis2Park(LocationN[LOCATION_LATITUDE].value); return true; } bool LX200ZEQ25::Park() { double parkAz = GetAxis1Park(); double parkAlt = GetAxis2Park(); char AzStr[16], AltStr[16]; fs_sexa(AzStr, parkAz, 2, 3600); fs_sexa(AltStr, parkAlt, 2, 3600); LOGF_DEBUG("Parking to Az (%s) Alt (%s)...", AzStr, AltStr); ln_hrz_posn horizontalPos; // Libnova south = 0, west = 90, north = 180, east = 270 horizontalPos.alt = parkAlt; horizontalPos.az = parkAz + 180; if (horizontalPos.az > 360) horizontalPos.az -= 360; ln_lnlat_posn observer; observer.lat = LocationN[LOCATION_LATITUDE].value; observer.lng = LocationN[LOCATION_LONGITUDE].value; if (observer.lng > 180) observer.lng -= 360; ln_equ_posn equatorialPos; ln_get_equ_from_hrz(&horizontalPos, &observer, ln_get_julian_from_sys(), &equatorialPos); equatorialPos.ra /= 15.0; if (setObjectRA(PortFD, equatorialPos.ra) < 0 || (setObjectDEC(PortFD, equatorialPos.dec)) < 0) { LOG_ERROR("Error setting RA/Dec."); return false; } /* Slew reads the '0', that is not the end of the slew */ if (slewZEQ25() == false) { LOGF_ERROR("Error Slewing to Az %s - Alt %s", AzStr, AltStr); slewError(1); return false; } EqNP.s = IPS_BUSY; TrackState = SCOPE_PARKING; LOG_INFO("Parking is in progress..."); return true; } bool LX200ZEQ25::UnPark() { // First we unpark astrophysics if (!isSimulation()) { if (setZEQ25UnPark() < 0) { LOG_ERROR("UnParking Failed."); return false; } } // Then we sync with to our last stored position #if 0 double parkAz = GetAxis1Park(); double parkAlt = GetAxis2Park(); char AzStr[16], AltStr[16]; fs_sexa(AzStr, parkAz, 2, 3600); fs_sexa(AltStr, parkAlt, 2, 3600); LOGF_DEBUG("Syncing to parked coordinates Az (%s) Alt (%s)...", AzStr, AltStr); ln_hrz_posn horizontalPos; // Libnova south = 0, west = 90, north = 180, east = 270 horizontalPos.alt = parkAlt; horizontalPos.az = parkAz + 180; if (horizontalPos.az > 360) horizontalPos.az -= 360; ln_lnlat_posn observer; observer.lat = LocationN[LOCATION_LATITUDE].value; observer.lng = LocationN[LOCATION_LONGITUDE].value; if (observer.lng > 180) observer.lng -= 360; ln_equ_posn equatorialPos; ln_get_equ_from_hrz(&horizontalPos, &observer, ln_get_julian_from_sys(), &equatorialPos); equatorialPos.ra /= 15.0; if (setObjectRA(PortFD, equatorialPos.ra) < 0 || (setObjectDEC(PortFD, equatorialPos.dec)) < 0) { LOG_ERROR("Error setting RA/DEC."); return false; } if (Sync(equatorialPos.ra, equatorialPos.dec) == false) { LOG_WARN("Sync failed."); return false; } #endif SetParked(false); return true; } bool LX200ZEQ25::ReadScopeStatus() { if (!isConnected()) return false; if (isSimulation()) { mountSim(); return true; } //if (check_lx200_connection(PortFD)) //return false; if (HomeSP.s == IPS_BUSY) { if (isZEQ25Home()) { HomeS[0].s = ISS_ON; HomeSP.s = IPS_OK; LOG_INFO("Telescope arrived at home position."); IDSetSwitch(&HomeSP, nullptr); } } if (TrackState == SCOPE_SLEWING) { // Check if LX200 is done slewing if (isSlewComplete()) { TrackState = SCOPE_TRACKING; LOG_INFO("Slew is complete. Tracking..."); } } else if (TrackState == SCOPE_PARKING) { if (isSlewComplete()) { setZEQ25Park(); SetParked(true); } } if (getLX200RA(PortFD, ¤tRA) < 0 || getLX200DEC(PortFD, ¤tDEC) < 0) { EqNP.s = IPS_ALERT; IDSetNumber(&EqNP, "Error reading RA/DEC."); return false; } NewRaDec(currentRA, currentDEC); return true; } void LX200ZEQ25::mountSim() { static struct timeval ltv; struct timeval tv; double dt, da, dx; int nlocked; /* update elapsed time since last poll, don't presume exactly POLLMS */ gettimeofday(&tv, nullptr); if (ltv.tv_sec == 0 && ltv.tv_usec == 0) ltv = tv; dt = tv.tv_sec - ltv.tv_sec + (tv.tv_usec - ltv.tv_usec) / 1e6; ltv = tv; da = SLEWRATE * dt; /* Process per current state. We check the state of EQUATORIAL_COORDS and act acoordingly */ switch (TrackState) { case SCOPE_TRACKING: /* RA moves at sidereal, Dec stands still */ currentRA += (SIDRATE * dt / 15.); break; case SCOPE_SLEWING: case SCOPE_PARKING: /* slewing - nail it when both within one pulse @ SLEWRATE */ nlocked = 0; dx = targetRA - currentRA; if (fabs(dx) <= da) { currentRA = targetRA; nlocked++; } else if (dx > 0) currentRA += da / 15.; else currentRA -= da / 15.; dx = targetDEC - currentDEC; if (fabs(dx) <= da) { currentDEC = targetDEC; nlocked++; } else if (dx > 0) currentDEC += da; else currentDEC -= da; if (nlocked == 2) { if (TrackState == SCOPE_SLEWING) TrackState = SCOPE_TRACKING; else SetParked(true); } break; default: break; } NewRaDec(currentRA, currentDEC); } int LX200ZEQ25::getZEQ25GuideRate(double *rate) { char cmd[] = ":AG#"; int errcode = 0; char errmsg[MAXRBUF]; char response[8]; int nbytes_read = 0; int nbytes_written = 0; LOGF_DEBUG("CMD <%s>", cmd); if (isSimulation()) { snprintf(response, 8, "%3d#", (int)(GuideRateN[0].value * 100)); nbytes_read = strlen(response); } else { tcflush(PortFD, TCIFLUSH); if ((errcode = tty_write(PortFD, cmd, strlen(cmd), &nbytes_written)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); LOGF_ERROR("%s", errmsg); return errcode; } if ((errcode = tty_read(PortFD, response, 4, 3, &nbytes_read))) { tty_error_msg(errcode, errmsg, MAXRBUF); LOGF_ERROR("%s", errmsg); return errcode; } } if (nbytes_read > 0) { response[nbytes_read] = '\0'; LOGF_DEBUG("RES (%s)", response); int rate_num; if (sscanf(response, "%d#", &rate_num) > 0) { *rate = rate_num / 100.0; tcflush(PortFD, TCIFLUSH); return TTY_OK; } else { LOGF_ERROR("Error: Malformed result (%s).", response); return -1; } } LOGF_ERROR("Only received #%d bytes, expected 1.", nbytes_read); return -1; } int LX200ZEQ25::setZEQ25GuideRate(double rate) { char cmd[16]; int errcode = 0; char errmsg[MAXRBUF]; char response[8]; int nbytes_read = 0; int nbytes_written = 0; int num = rate * 100; snprintf(cmd, 16, ":RG%03d#", num); LOGF_DEBUG("CMD <%s>", cmd); if (isSimulation()) { strcpy(response, "1"); nbytes_read = strlen(response); } else { tcflush(PortFD, TCIFLUSH); if ((errcode = tty_write(PortFD, cmd, strlen(cmd), &nbytes_written)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); LOGF_ERROR("%s", errmsg); return errcode; } if ((errcode = tty_read(PortFD, response, 1, 3, &nbytes_read))) { tty_error_msg(errcode, errmsg, MAXRBUF); LOGF_ERROR("%s", errmsg); return errcode; } } if (nbytes_read > 0) { response[nbytes_read] = '\0'; LOGF_DEBUG("RES (%s)", response); tcflush(PortFD, TCIFLUSH); return true; } LOGF_ERROR("Only received #%d bytes, expected 1.", nbytes_read); return -1; } int LX200ZEQ25::SendPulseCmd(int direction, int duration_msec) { int nbytes_write = 0; char cmd[20]; switch (direction) { case LX200_NORTH: sprintf(cmd, ":Mn%04d#", duration_msec); break; case LX200_SOUTH: sprintf(cmd, ":Ms%04d#", duration_msec); break; case LX200_EAST: sprintf(cmd, ":Me%04d#", duration_msec); break; case LX200_WEST: sprintf(cmd, ":Mw%04d#", duration_msec); break; default: return 1; } LOGF_DEBUG("CMD <%s>", cmd); tty_write_string(PortFD, cmd, &nbytes_write); tcflush(PortFD, TCIFLUSH); return 0; } libindi/drivers/telescope/dsc.cpp0000664000175000017500000005042013263645557016411 0ustar jasemjasem/******************************************************************************* Copyright(c) 2017 Jasem Mutlaq. All rights reserved. It just gets the encoder positoin and outputs current coordinates. Calibratoin and syncing not supported yet. This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2 as published by the Free Software Foundation. . 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 Library General Public License for more details. . You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. *******************************************************************************/ #include "dsc.h" #include "indicom.h" #include #include #include #include #include #define DSC_TIMEOUT 2 #define AXIS_TAB "Axis Settings" #include // For DBG_ALIGNMENT using namespace INDI::AlignmentSubsystem; // We declare an auto pointer to DSC. std::unique_ptr dsc(new DSC()); void ISGetProperties(const char *dev) { dsc->ISGetProperties(dev); } void ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) { dsc->ISNewSwitch(dev, name, states, names, n); } void ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n) { dsc->ISNewText(dev, name, texts, names, n); } void ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) { dsc->ISNewNumber(dev, name, values, names, n); } void ISNewBLOB(const char *dev, const char *name, int sizes[], int blobsizes[], char *blobs[], char *formats[], char *names[], int n) { INDI_UNUSED(dev); INDI_UNUSED(name); INDI_UNUSED(sizes); INDI_UNUSED(blobsizes); INDI_UNUSED(blobs); INDI_UNUSED(formats); INDI_UNUSED(names); INDI_UNUSED(n); } void ISSnoopDevice(XMLEle *root) { dsc->ISSnoopDevice(root); } DSC::DSC() { SetTelescopeCapability(TELESCOPE_CAN_SYNC | TELESCOPE_HAS_LOCATION, 0); } const char *DSC::getDefaultName() { return (const char *)"Digital Setting Circle"; } bool DSC::initProperties() { INDI::Telescope::initProperties(); // Raw encoder values IUFillNumber(&EncoderN[AXIS1_ENCODER], "AXIS1_ENCODER", "Axis 1", "%0.f", 0, 1e6, 0, 0); IUFillNumber(&EncoderN[AXIS2_ENCODER], "AXIS2_ENCODER", "Axis 2", "%0.f", 0, 1e6, 0, 0); IUFillNumber(&EncoderN[AXIS1_RAW_ENCODER], "AXIS1_RAW_ENCODER", "RAW Axis 1", "%0.f", -1e6, 1e6, 0, 0); IUFillNumber(&EncoderN[AXIS2_RAW_ENCODER], "AXIS2_RAW_ENCODER", "RAW Axis 2", "%0.f", -1e6, 1e6, 0, 0); IUFillNumberVector(&EncoderNP, EncoderN, 4, getDeviceName(), "DCS_ENCODER", "Encoders", MAIN_CONTROL_TAB, IP_RO, 0, IPS_IDLE); // Encoder Settings IUFillNumber(&AxisSettingsN[AXIS1_TICKS], "AXIS1_TICKS", "#1 ticks/rev", "%g", 256, 1e6, 0, 4096); IUFillNumber(&AxisSettingsN[AXIS1_DEGREE_OFFSET], "AXIS1_DEGREE_OFFSET", "#1 Degrees Offset", "%g", -180, 180, 30, 0); IUFillNumber(&AxisSettingsN[AXIS2_TICKS], "AXIS2_TICKS", "#2 ticks/rev", "%g", 256, 1e6, 0, 4096); IUFillNumber(&AxisSettingsN[AXIS2_DEGREE_OFFSET], "AXIS2_DEGREE_OFFSET", "#2 Degrees Offset", "%g", -180, 180, 30, 0); IUFillNumberVector(&AxisSettingsNP, AxisSettingsN, 4, getDeviceName(), "AXIS_SETTINGS", "Axis Resolution", AXIS_TAB, IP_RW, 0, IPS_IDLE); // Axis Range IUFillSwitch(&AxisRangeS[AXIS_FULL_STEP], "AXIS_FULL_STEP", "Full Step", ISS_ON); IUFillSwitch(&AxisRangeS[AXIS_HALF_STEP], "AXIS_HALF_STEP", "Half Step", ISS_OFF); IUFillSwitchVector(&AxisRangeSP, AxisRangeS, 2, getDeviceName(), "AXIS_RANGE", "Axis Range", AXIS_TAB, IP_RW, ISR_1OFMANY, 0, IPS_IDLE); // Reverse Encoder Direction IUFillSwitch(&ReverseS[AXIS1_ENCODER], "AXIS1_REVERSE", "Axis 1", ISS_OFF); IUFillSwitch(&ReverseS[AXIS2_ENCODER], "AXIS2_REVERSE", "Axis 2", ISS_OFF); IUFillSwitchVector(&ReverseSP, ReverseS, 2, getDeviceName(), "AXIS_REVERSE", "Reverse", AXIS_TAB, IP_RW, ISR_NOFMANY, 0, IPS_IDLE); // Offsets applied to raw encoder values to adjust them as necessary #if 0 IUFillNumber(&EncoderOffsetN[OFFSET_AXIS1_SCALE], "OFFSET_AXIS1_SCALE", "#1 Ticks Scale", "%g", 0, 1e6, 0, 1); IUFillNumber(&EncoderOffsetN[OFFSET_AXIS1_OFFSET], "OFFSET_AXIS1_OFFSET", "#1 Ticks Offset", "%g", -1e6, 1e6, 0, 0); IUFillNumber(&EncoderOffsetN[AXIS1_DEGREE_OFFSET], "AXIS1_DEGREE_OFFSET", "#1 Degrees Offset", "%g", -180, 180, 30, 0); IUFillNumber(&EncoderOffsetN[OFFSET_AXIS2_SCALE], "OFFSET_AIXS2_SCALE", "#2 Ticks Scale", "%g", 0, 1e6, 0, 1); IUFillNumber(&EncoderOffsetN[OFFSET_AXIS2_OFFSET], "OFFSET_AXIS2_OFFSET", "#2 Ticks Offset", "%g", -1e6, 1e6, 0, 0); IUFillNumber(&EncoderOffsetN[AXIS2_DEGREE_OFFSET], "AXIS2_DEGREE_OFFSET", "#2 Degrees Offset", "%g", -180, 180, 30, 0); IUFillNumberVector(&EncoderOffsetNP, EncoderOffsetN, 6, getDeviceName(), "AXIS_OFFSET", "Offsets", AXIS_TAB, IP_RW, 0, IPS_IDLE); #endif // Mount Type IUFillSwitch(&MountTypeS[MOUNT_EQUATORIAL], "MOUNT_EQUATORIAL", "Equatorial", ISS_ON); IUFillSwitch(&MountTypeS[MOUNT_ALTAZ], "MOUNT_ALTAZ", "AltAz", ISS_OFF); IUFillSwitchVector(&MountTypeSP, MountTypeS, 2, getDeviceName(), "MOUNT_TYPE", "Mount Type", MAIN_CONTROL_TAB, IP_RW, ISR_1OFMANY, 0, IPS_IDLE); // Simulation encoder values IUFillNumber(&SimEncoderN[AXIS1_ENCODER], "AXIS1_ENCODER", "Axis 1", "%0.f", -1e6, 1e6, 0, 0); IUFillNumber(&SimEncoderN[AXIS2_ENCODER], "AXIS2_ENCODER", "Axis 2", "%0.f", -1e6, 1e6, 0, 0); IUFillNumberVector(&SimEncoderNP, SimEncoderN, 2, getDeviceName(), "SIM_ENCODER", "Sim Encoders", MAIN_CONTROL_TAB, IP_RW, 0, IPS_IDLE); addAuxControls(); InitAlignmentProperties(this); return true; } bool DSC::updateProperties() { INDI::Telescope::updateProperties(); if (isConnected()) { defineNumber(&EncoderNP); defineNumber(&AxisSettingsNP); defineSwitch(&AxisRangeSP); defineSwitch(&ReverseSP); //defineNumber(&EncoderOffsetNP); defineSwitch(&MountTypeSP); if (isSimulation()) defineNumber(&SimEncoderNP); SetAlignmentSubsystemActive(true); } else { deleteProperty(EncoderNP.name); deleteProperty(AxisSettingsNP.name); deleteProperty(AxisRangeSP.name); deleteProperty(ReverseSP.name); //deleteProperty(EncoderOffsetNP.name); deleteProperty(MountTypeSP.name); if (isSimulation()) deleteProperty(SimEncoderNP.name); } return true; } bool DSC::saveConfigItems(FILE *fp) { INDI::Telescope::saveConfigItems(fp); IUSaveConfigNumber(fp, &AxisSettingsNP); //IUSaveConfigNumber(fp, &EncoderOffsetNP); IUSaveConfigSwitch(fp, &AxisRangeSP); IUSaveConfigSwitch(fp, &ReverseSP); IUSaveConfigSwitch(fp, &MountTypeSP); return true; } bool DSC::ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n) { ProcessAlignmentTextProperties(this, name, texts, names, n); return INDI::Telescope::ISNewText(dev, name, texts, names, n); } bool DSC::ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) { if (dev != nullptr && strcmp(dev, getDeviceName()) == 0) { if (strcmp(name, AxisSettingsNP.name) == 0) { IUUpdateNumber(&AxisSettingsNP, values, names, n); AxisSettingsNP.s = IPS_OK; IDSetNumber(&AxisSettingsNP, nullptr); return true; } /*if(strcmp(name,EncoderOffsetNP.name) == 0) { IUUpdateNumber(&EncoderOffsetNP, values, names, n); EncoderOffsetNP.s = IPS_OK; IDSetNumber(&EncoderOffsetNP, nullptr); return true; }*/ if (strcmp(name, SimEncoderNP.name) == 0) { IUUpdateNumber(&SimEncoderNP, values, names, n); SimEncoderNP.s = IPS_OK; IDSetNumber(&SimEncoderNP, nullptr); return true; } ProcessAlignmentNumberProperties(this, name, values, names, n); } return INDI::Telescope::ISNewNumber(dev, name, values, names, n); } bool DSC::ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) { if (dev != nullptr && strcmp(dev, getDeviceName()) == 0) { if (strcmp(name, ReverseSP.name) == 0) { IUUpdateSwitch(&ReverseSP, states, names, n); ReverseSP.s = IPS_OK; IDSetSwitch(&ReverseSP, nullptr); return true; } if (strcmp(name, MountTypeSP.name) == 0) { IUUpdateSwitch(&MountTypeSP, states, names, n); MountTypeSP.s = IPS_OK; IDSetSwitch(&MountTypeSP, nullptr); return true; } if (strcmp(name, AxisRangeSP.name) == 0) { IUUpdateSwitch(&AxisRangeSP, states, names, n); AxisRangeSP.s = IPS_OK; if (AxisRangeS[AXIS_FULL_STEP].s == ISS_ON) { LOGF_INFO("Axis range is from 0 to %.f", AxisSettingsN[AXIS1_TICKS].value); } else { LOGF_INFO("Axis range is from -%.f to %.f", AxisSettingsN[AXIS1_TICKS].value / 2, AxisSettingsN[AXIS1_TICKS].value / 2); } IDSetSwitch(&AxisRangeSP, nullptr); return true; } ProcessAlignmentSwitchProperties(this, name, states, names, n); } return INDI::Telescope::ISNewSwitch(dev, name, states, names, n); } bool DSC::Handshake() { return true; } bool DSC::ReadScopeStatus() { // Send 'Q' char CR[1] = { 0x51 }; // Response char response[16] = { 0 }; int rc = 0, nbytes_read = 0, nbytes_written = 0; LOGF_DEBUG("CMD: %#02X", CR[0]); if (isSimulation()) { snprintf(response, 16, "%06.f\t%06.f", SimEncoderN[AXIS1_ENCODER].value, SimEncoderN[AXIS2_ENCODER].value); } else { tcflush(PortFD, TCIFLUSH); if ((rc = tty_write(PortFD, CR, 1, &nbytes_written)) != TTY_OK) { char errmsg[256]; tty_error_msg(rc, errmsg, 256); LOGF_ERROR("Error writing to device %s (%d)", errmsg, rc); return false; } // Read until we encounter a CR if ((rc = tty_read_section(PortFD, response, 0x0D, DSC_TIMEOUT, &nbytes_read)) != TTY_OK) { // if we read enough, let's try to process, otherwise throw error // 6 characters for each number if (nbytes_read < 12) { char errmsg[256]; tty_error_msg(rc, errmsg, 256); LOGF_ERROR("Error reading from device %s (%d)", errmsg, rc); return false; } } } LOGF_DEBUG("RES: %s", response); double Axis1Encoder = 0, Axis2Encoder = 0; std::regex rgx(R"((\+?\-?\d+)\s(\+?\-?\d+))"); std::smatch match; std::string input(response); if (std::regex_search(input, match, rgx)) { Axis1Encoder = atof(match.str(1).c_str()); Axis2Encoder = atof(match.str(2).c_str()); } else { LOGF_ERROR("Error processing response: %s", response); EncoderNP.s = IPS_ALERT; IDSetNumber(&EncoderNP, nullptr); return false; } LOGF_DEBUG("Raw Axis encoders. Axis1: %g Axis2: %g", Axis1Encoder, Axis2Encoder); EncoderN[AXIS1_RAW_ENCODER].value = Axis1Encoder; EncoderN[AXIS2_RAW_ENCODER].value = Axis2Encoder; // Convert Half Step to Full Step if (AxisRangeS[AXIS_HALF_STEP].s == ISS_ON) { if (Axis1Encoder < 0) Axis1Encoder += AxisSettingsN[AXIS1_TICKS].value; if (Axis2Encoder < 0) Axis2Encoder += AxisSettingsN[AXIS2_TICKS].value; } // Calculate reverse values double Axis1 = Axis1Encoder; if (ReverseS[AXIS1_ENCODER].s == ISS_ON) Axis1 = AxisSettingsN[AXIS1_TICKS].value - Axis1; #if 0 if (Axis1Diff < 0) Axis1Diff += AxisSettingsN[AXIS1_TICKS].value; else if (Axis1Diff > AxisSettingsN[AXIS1_TICKS].value) Axis1Diff -= AxisSettingsN[AXIS1_TICKS].value; #endif double Axis2 = Axis2Encoder; if (ReverseS[AXIS2_ENCODER].s == ISS_ON) Axis2 = AxisSettingsN[AXIS2_TICKS].value - Axis2; LOGF_DEBUG("Axis encoders after reverse. Axis1: %g Axis2: %g", Axis1, Axis2); // Apply raw offsets // It seems having encoder offsets like this is confusing for users //Axis1 = (Axis1 * EncoderOffsetN[OFFSET_AXIS1_SCALE].value + EncoderOffsetN[OFFSET_AXIS1_OFFSET].value); //Axis2 = (Axis2 * EncoderOffsetN[OFFSET_AXIS2_SCALE].value + EncoderOffsetN[OFFSET_AXIS2_OFFSET].value); //LOGF_DEBUG("Axis encoders after raw offsets. Axis1: %g Axis2: %g", Axis1, Axis2); EncoderN[AXIS1_ENCODER].value = Axis1; EncoderN[AXIS2_ENCODER].value = Axis2; EncoderNP.s = IPS_OK; IDSetNumber(&EncoderNP, nullptr); double Axis1Degrees = (Axis1 / AxisSettingsN[AXIS1_TICKS].value * 360.0) + AxisSettingsN[AXIS1_DEGREE_OFFSET].value; double Axis2Degrees = (Axis2 / AxisSettingsN[AXIS2_TICKS].value * 360.0) + AxisSettingsN[AXIS2_DEGREE_OFFSET].value; Axis1Degrees = range360(Axis1Degrees); Axis2Degrees = range360(Axis2Degrees); // Adjust for LST double LST = get_local_sidereal_time(observer.lng); // Final aligned equatorial position ln_equ_posn eq { 0, 0 }; // Now we proceed depending on mount type if (MountTypeS[MOUNT_EQUATORIAL].s == ISS_ON) { encoderEquatorialCoordinates.ra = Axis1Degrees / 15.0; encoderEquatorialCoordinates.ra += LST; encoderEquatorialCoordinates.ra = range24(encoderEquatorialCoordinates.ra); encoderEquatorialCoordinates.dec = rangeDec(Axis2Degrees); // Do alignment eq = TelescopeEquatorialToSky(); } else { encoderHorizontalCoordinates.az = Axis1Degrees; encoderHorizontalCoordinates.az += 180; encoderHorizontalCoordinates.az = range360(encoderHorizontalCoordinates.az); encoderHorizontalCoordinates.alt = Axis2Degrees; // Do alignment eq = TelescopeHorizontalToSky(); char AzStr[64], AltStr[64]; fs_sexa(AzStr, Axis1Degrees, 2, 3600); fs_sexa(AltStr, Axis2Degrees, 2, 3600); LOGF_DEBUG("Current Az: %s Current Alt: %s", AzStr, AltStr); //ln_get_equ_from_hrz(&encoderHorizontalCoordinates, &observer, ln_get_julian_from_sys(), &encoderEquatorialCoordinates); //equatorialPos.ra /= 15.0; //encoderEquatorialCoordinates.ra = range24(encoderEquatorialCoordinates.ra); //encoderEquatorialCoordinates.dec = rangeDec(encoderEquatorialCoordinates.dec); } // Now feed the rest of the system with corrected data NewRaDec(eq.ra, eq.dec); return true; } bool DSC::Sync(double ra, double dec) { AlignmentDatabaseEntry NewEntry; struct ln_equ_posn RaDec { 0, 0 }; struct ln_hrz_posn AltAz { 0, 0 }; if (MountTypeS[MOUNT_EQUATORIAL].s == ISS_ON) { double LST = get_local_sidereal_time(observer.lng); RaDec.ra = ((LST - encoderEquatorialCoordinates.ra) * 360.0) / 24.0; RaDec.dec = encoderEquatorialCoordinates.dec; } else { AltAz.az = encoderHorizontalCoordinates.az; AltAz.alt = encoderHorizontalCoordinates.alt; } NewEntry.ObservationJulianDate = ln_get_julian_from_sys(); NewEntry.RightAscension = ra; NewEntry.Declination = dec; if (MountTypeS[MOUNT_EQUATORIAL].s == ISS_ON) NewEntry.TelescopeDirection = TelescopeDirectionVectorFromLocalHourAngleDeclination(RaDec); else NewEntry.TelescopeDirection = TelescopeDirectionVectorFromAltitudeAzimuth(AltAz); NewEntry.PrivateDataSize = 0; DEBUGF(INDI::AlignmentSubsystem::DBG_ALIGNMENT, "New sync point Date %lf RA %lf DEC %lf TDV(x %lf y %lf z %lf)", NewEntry.ObservationJulianDate, NewEntry.RightAscension, NewEntry.Declination, NewEntry.TelescopeDirection.x, NewEntry.TelescopeDirection.y, NewEntry.TelescopeDirection.z); if (!CheckForDuplicateSyncPoint(NewEntry)) { GetAlignmentDatabase().push_back(NewEntry); // Tell the client about size change UpdateSize(); // Tell the math plugin to reinitialise Initialise(this); return true; } return false; } ln_equ_posn DSC::TelescopeEquatorialToSky() { double RightAscension, Declination; ln_equ_posn eq { 0, 0 }; if (GetAlignmentDatabase().size() > 1) { TelescopeDirectionVector TDV; /* and here we convert from ra/dec to hour angle / dec before calling alignment stuff */ double lha, lst; lst = get_local_sidereal_time(LocationN[LOCATION_LONGITUDE].value); lha = get_local_hour_angle(lst, encoderEquatorialCoordinates.ra); // convert lha to degrees lha = lha * 360 / 24; eq.ra = lha; eq.dec = encoderEquatorialCoordinates.dec; TDV = TelescopeDirectionVectorFromLocalHourAngleDeclination(eq); if (!TransformTelescopeToCelestial(TDV, RightAscension, Declination)) { RightAscension = encoderEquatorialCoordinates.ra; Declination = encoderEquatorialCoordinates.dec; } } else { // With less than 2 align points // Just return raw data RightAscension = encoderEquatorialCoordinates.ra; Declination = encoderEquatorialCoordinates.dec; } eq.ra = RightAscension; eq.dec = Declination; return eq; } ln_equ_posn DSC::TelescopeHorizontalToSky() { ln_equ_posn eq { 0, 0 }; TelescopeDirectionVector TDV = TelescopeDirectionVectorFromAltitudeAzimuth(encoderHorizontalCoordinates); double RightAscension, Declination; if (!TransformTelescopeToCelestial(TDV, RightAscension, Declination)) { struct ln_equ_posn EquatorialCoordinates { 0, 0 }; TelescopeDirectionVector RotatedTDV(TDV); switch (GetApproximateMountAlignment()) { case ZENITH: break; case NORTH_CELESTIAL_POLE: // Rotate the TDV coordinate system anticlockwise (positive) around the y axis by 90 minus // the (positive)observatory latitude. The vector itself is rotated clockwise RotatedTDV.RotateAroundY(90.0 - observer.lat); AltitudeAzimuthFromTelescopeDirectionVector(RotatedTDV, encoderHorizontalCoordinates); break; case SOUTH_CELESTIAL_POLE: // Rotate the TDV coordinate system clockwise (negative) around the y axis by 90 plus // the (negative)observatory latitude. The vector itself is rotated anticlockwise RotatedTDV.RotateAroundY(-90.0 - observer.lat); AltitudeAzimuthFromTelescopeDirectionVector(RotatedTDV, encoderHorizontalCoordinates); break; } ln_get_equ_from_hrz(&encoderHorizontalCoordinates, &observer, ln_get_julian_from_sys(), &EquatorialCoordinates); // libnova works in decimal degrees RightAscension = EquatorialCoordinates.ra * 24.0 / 360.0; Declination = EquatorialCoordinates.dec; } eq.ra = RightAscension; eq.dec = Declination; return eq; } bool DSC::updateLocation(double latitude, double longitude, double elevation) { UpdateLocation(latitude, longitude, elevation); INDI_UNUSED(elevation); // JM: INDI Longitude is 0 to 360 increasing EAST. libnova East is Positive, West is negative observer.lng = longitude; if (observer.lng > 180) observer.lng -= 360; observer.lat = latitude; LOGF_INFO("Location updated: Longitude (%g) Latitude (%g)", observer.lng, observer.lat); return true; } void DSC::simulationTriggered(bool enable) { if (!isConnected()) return; if (enable) { defineNumber(&SimEncoderNP); } else { deleteProperty(SimEncoderNP.name); } } libindi/drivers/telescope/paramount.h0000664000175000017500000000747013263645557017322 0ustar jasemjasem/******************************************************************************* Copyright(c) 2017 Jasem Mutlaq. All rights reserved. Driver for using TheSky6 Pro Scripted operations for mounts via the TCP server. While this technically can operate any mount connected to the TheSky6 Pro, it is intended for Paramount mounts control. This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2 as published by the Free Software Foundation. . 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 Library General Public License for more details. . You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. *******************************************************************************/ #pragma once #include "indiguiderinterface.h" #include "inditelescope.h" class Paramount : public INDI::Telescope, public INDI::GuiderInterface { public: Paramount(); virtual ~Paramount() = default; virtual const char *getDefaultName() override; virtual bool Handshake() override; virtual bool ReadScopeStatus() override; virtual bool initProperties() override; virtual bool updateProperties() override; virtual bool ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) override; virtual bool ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) override; protected: virtual bool MoveNS(INDI_DIR_NS dir, TelescopeMotionCommand command) override; virtual bool MoveWE(INDI_DIR_WE dir, TelescopeMotionCommand command) override; virtual bool Abort() override; virtual bool updateLocation(double latitude, double longitude, double elevation) override; virtual bool updateTime(ln_date *utc, double utc_offset) override; virtual bool SetParkPosition(double Axis1Value, double Axis2Value) override; virtual bool Goto(double, double) override; virtual bool Park() override; virtual bool UnPark() override; virtual bool Sync(double ra, double dec) override; // Tracking virtual bool SetTrackMode(uint8_t mode) override; virtual bool SetTrackRate(double raRate, double deRate) override; virtual bool SetTrackEnabled(bool enabled) override; // Parking virtual bool SetCurrentPark() override; virtual bool SetDefaultPark() override; // Guiding virtual IPState GuideNorth(float ms) override; virtual IPState GuideSouth(float ms) override; virtual IPState GuideEast(float ms) override; virtual IPState GuideWest(float ms) override; private: void mountSim(); bool getMountRADE(); bool isSlewComplete(); bool sendTheSkyOKCommand(const char *command, const char *errorMessage); bool isTheSkyParked(); bool isTheSkyTracking(); bool startOpenLoopMotion(uint8_t motion, uint16_t rate); bool stopOpenLoopMotion(); bool setTheSkyTracking(bool enable, bool isSidereal, double raRate, double deRate); double currentRA { 0 }; double currentDEC { 90 }; double targetRA { 0 }; double targetDEC { 0 }; ln_lnlat_posn lnobserver { 0, 0 }; ln_hrz_posn lnaltaz { 0, 0 }; unsigned int DBG_SCOPE { 0 }; // Jog Rate INumber JogRateN[2]; INumberVectorProperty JogRateNP; // Guide Rate INumber GuideRateN[2]; INumberVectorProperty GuideRateNP; // Tracking Mode ISwitch TrackModeS[4]; ISwitchVectorProperty TrackModeSP; // Tracking Rate // INumber TrackRateN[2]; // INumberVectorProperty TrackRateNP; }; libindi/drivers/telescope/pmc8driver.h0000664000175000017500000001327113263645557017373 0ustar jasemjasem/* INDI Explore Scientific PMC8 driver Copyright (C) 2017 Michael Fulbright Based on IEQPro driver. 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 */ #pragma once #include #include "inditelescope.h" typedef enum { ST_STOPPED, ST_TRACKING, ST_SLEWING, ST_GUIDING, ST_MERIDIAN_FLIPPING, ST_PARKED, ST_HOME } PMC8_SYSTEM_STATUS; typedef enum { PMC8_TRACK_SIDEREAL, PMC8_TRACK_LUNAR, PMC8_TRACK_SOLAR, PMC8_TRACK_CUSTOM } PMC8_TRACK_RATE; typedef enum { PMC8_MOVE_4X, PMC8_MOVE_16X, PMC8_MOVE_64X, PMC8_MOVE_256X } PMC8_MOVE_RATE; //typedef enum { HEMI_SOUTH, HEMI_NORTH } PMC8_HEMISPHERE; //typedef enum { FW_MODEL, FW_BOARD, FW_CONTROLLER, FW_RA, FW_DEC } IEQ_FIRMWARE; typedef enum { PMC8_AXIS_RA=0, PMC8_AXIS_DEC=1 } PMC8_AXIS; typedef enum { PMC8_N, PMC8_S, PMC8_W, PMC8_E } PMC8_DIRECTION; typedef struct { PMC8_SYSTEM_STATUS systemStatus; PMC8_SYSTEM_STATUS rememberSystemStatus; // PMC8_HEMISPHERE hemisphere; } PMC8Info; typedef struct { std::string Model; std::string MainBoardFirmware; } FirmwareInfo; /************************************************************************** Misc. **************************************************************************/ void set_pmc8_debug(bool enable); void set_pmc8_simulation(bool enable); void set_pmc8_device(const char *name); /************************************************************************** Simulation **************************************************************************/ void set_pmc8_sim_system_status(PMC8_SYSTEM_STATUS value); void set_pmc8_sim_track_rate(PMC8_TRACK_RATE value); void set_pmc8_sim_move_rate(PMC8_MOVE_RATE value); //void set_sim_hemisphere(IEQ_HEMISPHERE value); void set_pmc8_sim_ra(double ra); void set_pmc8_sim_dec(double dec); //void set_sim_guide_rate(double rate); /************************************************************************** Diagnostics **************************************************************************/ bool check_pmc8_connection(int fd); /************************************************************************** Get Info **************************************************************************/ /** Get PMC8 current status info */ bool get_pmc8_status(int fd, PMC8Info *info); /** Get All firmware informatin in addition to mount model */ bool get_pmc8_firmware(int fd, FirmwareInfo *info); /** Get RA/DEC */ bool get_pmc8_coords(int fd, double &ra, double &dec); /************************************************************************** Motion **************************************************************************/ bool start_pmc8_motion(int fd, PMC8_DIRECTION dir, int speedindex); bool stop_pmc8_motion(int fd, PMC8_DIRECTION dir); bool stop_pmc8_tracking_motion(int fd); bool set_pmc8_custom_ra_track_rate(int fd, double rate); bool set_pmc8_custom_dec_track_rate(int fd, double rate); bool set_pmc8_custom_ra_move_rate(int fd, double rate); bool set_pmc8_custom_dec_move_rate(int fd, double rate); bool set_pmc8_track_mode(int fd, uint rate); //bool set_pmc8_track_enabled(int fd, bool enabled); bool get_pmc8_is_scope_slewing(int fd, bool &isslew); bool get_pmc8_direction_axis(int fd, PMC8_AXIS axis, int &dir); bool set_pmc8_direction_axis(int fd, PMC8_AXIS axis, int dir, bool fast); bool abort_pmc8(int fd); bool slew_pmc8(int fd, double ra, double dec); bool sync_pmc8(int fd, double ra, double dec); bool set_pmc8_radec(int fd, double ra, double dec); INDI::Telescope::TelescopePierSide destSideOfPier(double ra, double dec); /************************************************************************** Home **************************************************************************/ //bool find_ieqpro_home(int fd); //bool goto_pmc8_home(int fd); //bool set_ieqpro_current_home(int fd); /************************************************************************** Park **************************************************************************/ bool park_pmc8(int fd); bool unpark_pmc8(int fd); /************************************************************************** Guide **************************************************************************/ bool set_pmc8_guide_rate(int fd, double rate); //bool get_pmc8_guide_rate(int fd, double *rate); bool start_pmc8_guide(int fd, PMC8_DIRECTION gdir, int ms, long &timetaken_us); bool stop_pmc8_guide(int fd, PMC8_DIRECTION gdir); /************************************************************************** Time & Location **************************************************************************/ void set_pmc8_location(double latitude, double longitude); //bool set_ieqpro_longitude(int fd, double longitude); //bool set_ieqpro_latitude(int fd, double latitude); //bool get_ieqpro_longitude(int fd, double *longitude); //bool get_ieqpro_latitude(int fd, double *latitude); //bool set_ieqpro_local_date(int fd, int yy, int mm, int dd); //bool set_ieqpro_local_time(int fd, int hh, int mm, int ss); //bool set_ieqpro_utc_offset(int fd, double offset_hours); //bool set_ieqpro_daylight_saving(int fd, bool enabled); libindi/drivers/telescope/lx200_16.cpp0000664000175000017500000002536113263645557017021 0ustar jasemjasem/* LX200 16" Copyright (C) 2003 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 */ #include "lx200_16.h" #include "indicom.h" #include "lx200driver.h" #include #include #include #define LX16_TAB "GPS/16 inch Features" LX200_16::LX200_16() : LX200GPS() { MaxReticleFlashRate = 3; } const char *LX200_16::getDefaultName() { return (const char *)"LX200 16"; } bool LX200_16::initProperties() { LX200GPS::initProperties(); IUFillSwitch(&FanStatusS[0], "On", "", ISS_OFF); IUFillSwitch(&FanStatusS[1], "Off", "", ISS_OFF); IUFillSwitchVector(&FanStatusSP, FanStatusS, 2, getDeviceName(), "Fan", "", LX16_TAB, IP_RW, ISR_1OFMANY, 0, IPS_IDLE); IUFillSwitch(&HomeSearchS[0], "Save Home", "", ISS_OFF); IUFillSwitch(&HomeSearchS[1], "Set Home", "", ISS_OFF); IUFillSwitchVector(&HomeSearchSP, HomeSearchS, 2, getDeviceName(), "Home", "", LX16_TAB, IP_RW, ISR_1OFMANY, 0, IPS_IDLE); IUFillSwitch(&FieldDeRotatorS[0], "On", "", ISS_OFF); IUFillSwitch(&FieldDeRotatorS[1], "Off", "", ISS_OFF); IUFillSwitchVector(&FieldDeRotatorSP, FieldDeRotatorS, 2, getDeviceName(), "Field De-Rotator", "", LX16_TAB, IP_RW, ISR_1OFMANY, 0, IPS_IDLE); IUFillNumber(&HorizontalCoordsN[0], "ALT", "Alt D:M:S", "%10.6m", -90., 90.0, 0.0, 0); IUFillNumber(&HorizontalCoordsN[1], "AZ", "Az D:M:S", "%10.6m", 0.0, 360.0, 0.0, 0); IUFillNumberVector(&HorizontalCoordsNP, HorizontalCoordsN, 2, getDeviceName(), "HORIZONTAL_COORD", "Horizontal Coord", MAIN_CONTROL_TAB, IP_RW, 0, IPS_IDLE); return true; } void LX200_16::ISGetProperties(const char *dev) { if (dev != nullptr && strcmp(dev, getDeviceName()) != 0) return; // process parent first LX200GPS::ISGetProperties(dev); /* if (isConnected()) { defineNumber(&HorizontalCoordsNP); defineSwitch(&FanStatusSP); defineSwitch(&HomeSearchSP); defineSwitch(&FieldDeRotatorSP); } */ } bool LX200_16::updateProperties() { // process parent first LX200GPS::updateProperties(); if (isConnected()) { defineNumber(&HorizontalCoordsNP); defineSwitch(&FanStatusSP); defineSwitch(&HomeSearchSP); defineSwitch(&FieldDeRotatorSP); } else { deleteProperty(HorizontalCoordsNP.name); deleteProperty(FanStatusSP.name); deleteProperty(HomeSearchSP.name); deleteProperty(FieldDeRotatorSP.name); } return true; } bool LX200_16::ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) { double newAlt = 0, newAz = 0; if (dev != nullptr && strcmp(dev, getDeviceName()) == 0) { if (!strcmp(name, HorizontalCoordsNP.name)) { int i = 0, nset = 0; for (nset = i = 0; i < n; i++) { INumber *horp = IUFindNumber(&HorizontalCoordsNP, names[i]); if (horp == &HorizontalCoordsN[0]) { newAlt = values[i]; nset += newAlt >= -90. && newAlt <= 90.0; } else if (horp == &HorizontalCoordsN[1]) { newAz = values[i]; nset += newAz >= 0. && newAz <= 360.0; } } if (nset == 2) { if (!isSimulation() && (setObjAz(PortFD, newAz) < 0 || setObjAlt(PortFD, newAlt) < 0)) { HorizontalCoordsNP.s = IPS_ALERT; IDSetNumber(&HorizontalCoordsNP, "Error setting Alt/Az."); return false; } targetAZ = newAz; targetALT = newAlt; return handleAltAzSlew(); } else { HorizontalCoordsNP.s = IPS_ALERT; IDSetNumber(&HorizontalCoordsNP, "Altitude or Azimuth missing or invalid"); return false; } } } LX200GPS::ISNewNumber(dev, name, values, names, n); return true; } bool LX200_16::ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) { int index = 0; if (dev != nullptr && strcmp(dev, getDeviceName()) == 0) { if (!strcmp(name, FanStatusSP.name)) { IUResetSwitch(&FanStatusSP); IUUpdateSwitch(&FanStatusSP, states, names, n); index = IUFindOnSwitchIndex(&FanStatusSP); if (index == 0) { if (turnFanOn(PortFD) < 0) { FanStatusSP.s = IPS_ALERT; IDSetSwitch(&FanStatusSP, "Error changing fan status."); return false; } } else { if (turnFanOff(PortFD) < 0) { FanStatusSP.s = IPS_ALERT; IDSetSwitch(&FanStatusSP, "Error changing fan status."); return false; } } FanStatusSP.s = IPS_OK; IDSetSwitch(&FanStatusSP, index == 0 ? "Fan is ON" : "Fan is OFF"); return true; } if (!strcmp(name, HomeSearchSP.name)) { int ret = 0; IUResetSwitch(&HomeSearchSP); IUUpdateSwitch(&HomeSearchSP, states, names, n); index = IUFindOnSwitchIndex(&HomeSearchSP); if (index == 0) ret = seekHomeAndSave(PortFD); else ret = seekHomeAndSet(PortFD); HomeSearchSP.s = IPS_BUSY; IDSetSwitch(&HomeSearchSP, index == 0 ? "Seek Home and Save" : "Seek Home and Set"); return true; } if (!strcmp(name, FieldDeRotatorSP.name)) { int ret = 0; IUResetSwitch(&FieldDeRotatorSP); IUUpdateSwitch(&FieldDeRotatorSP, states, names, n); index = IUFindOnSwitchIndex(&FieldDeRotatorSP); if (index == 0) ret = turnFieldDeRotatorOn(PortFD); else ret = turnFieldDeRotatorOff(PortFD); FieldDeRotatorSP.s = IPS_OK; IDSetSwitch(&FieldDeRotatorSP, index == 0 ? "Field deRotator is ON" : "Field deRotator is OFF"); return true; } } return LX200GPS::ISNewSwitch(dev, name, states, names, n); } bool LX200_16::handleAltAzSlew() { const struct timespec timeout = {0, 100000000L}; char altStr[64], azStr[64]; if (HorizontalCoordsNP.s == IPS_BUSY) { abortSlew(PortFD); // sleep for 100 mseconds nanosleep(&timeout, NULL); } if (!isSimulation() && slewToAltAz(PortFD)) { HorizontalCoordsNP.s = IPS_ALERT; IDSetNumber(&HorizontalCoordsNP, "Slew is not possible."); return false; } HorizontalCoordsNP.s = IPS_BUSY; fs_sexa(azStr, targetAZ, 2, 3600); fs_sexa(altStr, targetALT, 2, 3600); TrackState = SCOPE_SLEWING; IDSetNumber(&HorizontalCoordsNP, "Slewing to Alt %s - Az %s", altStr, azStr); return true; } bool LX200_16::ReadScopeStatus() { int searchResult = 0; double dx, dy; LX200Generic::ReadScopeStatus(); switch (HomeSearchSP.s) { case IPS_IDLE: break; case IPS_BUSY: if (isSimulation()) searchResult = 1; else if (getHomeSearchStatus(PortFD, &searchResult) < 0) { HomeSearchSP.s = IPS_ALERT; IDSetSwitch(&HomeSearchSP, "Error updating home search status."); return false; } if (searchResult == 0) { HomeSearchSP.s = IPS_ALERT; IDSetSwitch(&HomeSearchSP, "Home search failed."); } else if (searchResult == 1) { HomeSearchSP.s = IPS_OK; IDSetSwitch(&HomeSearchSP, "Home search successful."); } else if (searchResult == 2) IDSetSwitch(&HomeSearchSP, "Home search in progress..."); else { HomeSearchSP.s = IPS_ALERT; IDSetSwitch(&HomeSearchSP, "Home search error."); } break; case IPS_OK: break; case IPS_ALERT: break; } switch (HorizontalCoordsNP.s) { case IPS_IDLE: break; case IPS_BUSY: if (isSimulation()) { currentAZ = targetAZ; currentALT = targetALT; TrackState = SCOPE_TRACKING; return true; } if (getLX200Az(PortFD, ¤tAZ) < 0 || getLX200Alt(PortFD, ¤tALT) < 0) { HorizontalCoordsNP.s = IPS_ALERT; IDSetNumber(&HorizontalCoordsNP, "Error geting Alt/Az."); return false; } dx = targetAZ - currentAZ; dy = targetALT - currentALT; HorizontalCoordsNP.np[0].value = currentALT; HorizontalCoordsNP.np[1].value = currentAZ; // accuracy threshold (3'), can be changed as desired. if (fabs(dx) <= 0.05 && fabs(dy) <= 0.05) { HorizontalCoordsNP.s = IPS_OK; currentAZ = targetAZ; currentALT = targetALT; IDSetNumber(&HorizontalCoordsNP, "Slew is complete."); } else IDSetNumber(&HorizontalCoordsNP, nullptr); break; case IPS_OK: break; case IPS_ALERT: break; } return true; } void LX200_16::getBasicData() { LX200GPS::getBasicData(); if (!isSimulation()) { getLX200Az(PortFD, ¤tAZ); getLX200Alt(PortFD, ¤tALT); HorizontalCoordsNP.np[0].value = currentALT; HorizontalCoordsNP.np[1].value = currentAZ; IDSetNumber(&HorizontalCoordsNP, nullptr); } } libindi/drivers/telescope/telescope_simulator.cpp0000664000175000017500000006444413263645557021735 0ustar jasemjasem/******************************************************************************* Copyright(c) 2015 Jasem Mutlaq. All rights reserved. This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2 as published by the Free Software Foundation. . 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 Library General Public License for more details. . You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. *******************************************************************************/ #include "telescope_simulator.h" #include "indicom.h" #include #include #include #include #include // We declare an auto pointer to ScopeSim. std::unique_ptr telescope_sim(new ScopeSim()); #define GOTO_RATE 6.5 /* slew rate, degrees/s */ #define SLEW_RATE 2.5 /* slew rate, degrees/s */ #define FINE_SLEW_RATE 0.5 /* slew rate, degrees/s */ #define GOTO_LIMIT 5 /* Move at GOTO_RATE until distance from target is GOTO_LIMIT degrees */ #define SLEW_LIMIT 1 /* Move at SLEW_LIMIT until distance from target is SLEW_LIMIT degrees */ #define RA_AXIS 0 #define DEC_AXIS 1 #define GUIDE_NORTH 0 #define GUIDE_SOUTH 1 #define GUIDE_WEST 0 #define GUIDE_EAST 1 #define MIN_AZ_FLIP 180 #define MAX_AZ_FLIP 200 void ISPoll(void *p); void ISGetProperties(const char *dev) { telescope_sim->ISGetProperties(dev); } void ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) { telescope_sim->ISNewSwitch(dev, name, states, names, n); } void ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n) { telescope_sim->ISNewText(dev, name, texts, names, n); } void ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) { telescope_sim->ISNewNumber(dev, name, values, names, n); } void ISNewBLOB(const char *dev, const char *name, int sizes[], int blobsizes[], char *blobs[], char *formats[], char *names[], int n) { INDI_UNUSED(dev); INDI_UNUSED(name); INDI_UNUSED(sizes); INDI_UNUSED(blobsizes); INDI_UNUSED(blobs); INDI_UNUSED(formats); INDI_UNUSED(names); INDI_UNUSED(n); } void ISSnoopDevice(XMLEle *root) { telescope_sim->ISSnoopDevice(root); } ScopeSim::ScopeSim() { DBG_SCOPE = INDI::Logger::getInstance().addDebugLevel("Scope Verbose", "SCOPE"); SetTelescopeCapability(TELESCOPE_CAN_PARK | TELESCOPE_CAN_SYNC | TELESCOPE_CAN_GOTO | TELESCOPE_CAN_ABORT | TELESCOPE_HAS_TIME | TELESCOPE_HAS_LOCATION | TELESCOPE_HAS_TRACK_MODE | TELESCOPE_CAN_CONTROL_TRACK | TELESCOPE_HAS_TRACK_RATE, 4); /* initialize random seed: */ srand(time(nullptr)); } const char *ScopeSim::getDefaultName() { return (const char *)"Telescope Simulator"; } bool ScopeSim::initProperties() { /* Make sure to init parent properties first */ INDI::Telescope::initProperties(); #ifdef USE_EQUATORIAL_PE /* Simulated periodic error in RA, DEC */ IUFillNumber(&EqPEN[RA_AXIS], "RA_PE", "RA (hh:mm:ss)", "%010.6m", 0, 24, 0, 15.); IUFillNumber(&EqPEN[DEC_AXIS], "DEC_PE", "DEC (dd:mm:ss)", "%010.6m", -90, 90, 0, 15.); IUFillNumberVector(&EqPENV, EqPEN, 2, getDeviceName(), "EQUATORIAL_PE", "Periodic Error", MOTION_TAB, IP_RO, 60, IPS_IDLE); /* Enable client to manually add periodic error northward or southward for simulation purposes */ IUFillSwitch(&PEErrNSS[DIRECTION_NORTH], "PE_N", "North", ISS_OFF); IUFillSwitch(&PEErrNSS[DIRECTION_SOUTH], "PE_S", "South", ISS_OFF); IUFillSwitchVector(&PEErrNSSP, PEErrNSS, 2, getDeviceName(), "PE_NS", "PE N/S", MOTION_TAB, IP_RW, ISR_ATMOST1, 60, IPS_IDLE); /* Enable client to manually add periodic error westward or easthward for simulation purposes */ IUFillSwitch(&PEErrWES[DIRECTION_WEST], "PE_W", "West", ISS_OFF); IUFillSwitch(&PEErrWES[DIRECTION_EAST], "PE_E", "East", ISS_OFF); IUFillSwitchVector(&PEErrWESP, PEErrWES, 2, getDeviceName(), "PE_WE", "PE W/E", MOTION_TAB, IP_RW, ISR_ATMOST1, 60, IPS_IDLE); #endif /* How fast do we guide compared to sidereal rate */ IUFillNumber(&GuideRateN[RA_AXIS], "GUIDE_RATE_WE", "W/E Rate", "%g", 0, 1, 0.1, 0.5); IUFillNumber(&GuideRateN[DEC_AXIS], "GUIDE_RATE_NS", "N/S Rate", "%g", 0, 1, 0.1, 0.5); IUFillNumberVector(&GuideRateNP, GuideRateN, 2, getDeviceName(), "GUIDE_RATE", "Guiding Rate", MOTION_TAB, IP_RW, 0, IPS_IDLE); IUFillSwitch(&SlewRateS[SLEW_GUIDE], "SLEW_GUIDE", "Guide", ISS_OFF); IUFillSwitch(&SlewRateS[SLEW_CENTERING], "SLEW_CENTERING", "Centering", ISS_OFF); IUFillSwitch(&SlewRateS[SLEW_FIND], "SLEW_FIND", "Find", ISS_OFF); IUFillSwitch(&SlewRateS[SLEW_MAX], "SLEW_MAX", "Max", ISS_ON); IUFillSwitchVector(&SlewRateSP, SlewRateS, 4, getDeviceName(), "TELESCOPE_SLEW_RATE", "Slew Rate", MOTION_TAB, IP_RW, ISR_1OFMANY, 0, IPS_IDLE); // Add Tracking Modes AddTrackMode("TRACK_SIDEREAL", "Sidereal", true); AddTrackMode("TRACK_CUSTOM", "Custom"); // Let's simulate it to be an F/7.5 120mm telescope ScopeParametersN[0].value = 120; ScopeParametersN[1].value = 900; ScopeParametersN[2].value = 120; ScopeParametersN[3].value = 900; TrackState = SCOPE_IDLE; SetParkDataType(PARK_RA_DEC); initGuiderProperties(getDeviceName(), MOTION_TAB); /* Add debug controls so we may debug driver if necessary */ addDebugControl(); setDriverInterface(getDriverInterface() | GUIDER_INTERFACE); double longitude=0, latitude=90; // Get value from config file if it exists. IUGetConfigNumber(getDeviceName(), "GEOGRAPHIC_COORD", "LONG", &longitude); currentRA = get_local_sidereal_time(longitude); IUGetConfigNumber(getDeviceName(), "GEOGRAPHIC_COORD", "LAT", &latitude); currentDEC = latitude > 0 ? 90 : -90; setDefaultPollingPeriod(250); return true; } void ScopeSim::ISGetProperties(const char *dev) { /* First we let our parent populate */ INDI::Telescope::ISGetProperties(dev); /* if (isConnected()) { defineNumber(&GuideNSNP); defineNumber(&GuideWENP); defineNumber(&GuideRateNP); defineNumber(&EqPENV); defineSwitch(&PEErrNSSP); defineSwitch(&PEErrWESP); } */ } bool ScopeSim::updateProperties() { INDI::Telescope::updateProperties(); if (isConnected()) { defineNumber(&GuideNSNP); defineNumber(&GuideWENP); defineNumber(&GuideRateNP); #ifdef USE_EQUATORIAL_PE defineNumber(&EqPENV); defineSwitch(&PEErrNSSP); defineSwitch(&PEErrWESP); #endif if (InitPark()) { // If loading parking data is successful, we just set the default parking values. SetAxis1ParkDefault(currentRA); SetAxis2ParkDefault(currentDEC); if (isParked()) { currentRA = ParkPositionN[AXIS_RA].value; currentDEC= ParkPositionN[AXIS_DE].value; } } else { // Otherwise, we set all parking data to default in case no parking data is found. SetAxis1Park(currentRA); SetAxis2Park(currentDEC); SetAxis1ParkDefault(currentRA); SetAxis2ParkDefault(currentDEC); } sendTimeFromSystem(); } else { deleteProperty(GuideNSNP.name); deleteProperty(GuideWENP.name); #ifdef USE_EQUATORIAL_PE deleteProperty(EqPENV.name); deleteProperty(PEErrNSSP.name); deleteProperty(PEErrWESP.name); #endif deleteProperty(GuideRateNP.name); } return true; } bool ScopeSim::Connect() { LOG_INFO("Telescope simulator is online."); SetTimer(POLLMS); return true; } bool ScopeSim::Disconnect() { LOG_INFO("Telescope simulator is offline."); return true; } bool ScopeSim::ReadScopeStatus() { static struct timeval ltv { 0, 0 }; struct timeval tv { 0, 0 }; double dt = 0, da_ra = 0, da_dec = 0, dx = 0, dy = 0, ra_guide_dt = 0, dec_guide_dt = 0; int nlocked, ns_guide_dir = -1, we_guide_dir = -1; #ifdef USE_EQUATORIAL_PE static double last_dx = 0, last_dy = 0; char RA_DISP[64], DEC_DISP[64], RA_GUIDE[64], DEC_GUIDE[64], RA_PE[64], DEC_PE[64], RA_TARGET[64], DEC_TARGET[64]; #endif /* update elapsed time since last poll, don't presume exactly POLLMS */ gettimeofday(&tv, nullptr); if (ltv.tv_sec == 0 && ltv.tv_usec == 0) ltv = tv; // Time diff is seconds dt = tv.tv_sec - ltv.tv_sec + (tv.tv_usec - ltv.tv_usec) / 1e6; ltv = tv; if (fabs(targetRA - currentRA) * 15. >= GOTO_LIMIT) da_ra = GOTO_RATE * dt; else if (fabs(targetRA - currentRA) * 15. >= SLEW_LIMIT) da_ra = SLEW_RATE * dt; else da_ra = FINE_SLEW_RATE * dt; if (fabs(targetDEC - currentDEC) >= GOTO_LIMIT) da_dec = GOTO_RATE * dt; else if (fabs(targetDEC - currentDEC) >= SLEW_LIMIT) da_dec = SLEW_RATE * dt; else da_dec = FINE_SLEW_RATE * dt; if (MovementNSSP.s == IPS_BUSY || MovementWESP.s == IPS_BUSY) { int rate = IUFindOnSwitchIndex(&SlewRateSP); switch (rate) { case SLEW_GUIDE: da_ra = TrackRateN[AXIS_RA].value/(3600.0*15) * GuideRateN[RA_AXIS].value * dt; da_dec = TrackRateN[AXIS_RA].value/3600.0 * GuideRateN[DEC_AXIS].value * dt;; break; case SLEW_CENTERING: da_ra = FINE_SLEW_RATE * dt * .1; da_dec = FINE_SLEW_RATE * dt * .1; break; case SLEW_FIND: da_ra = SLEW_RATE * dt; da_dec = SLEW_RATE * dt; break; default: da_ra = GOTO_RATE * dt; da_dec = GOTO_RATE * dt; break; } switch (MovementNSSP.s) { case IPS_BUSY: if (MovementNSS[DIRECTION_NORTH].s == ISS_ON) currentDEC += da_dec; else if (MovementNSS[DIRECTION_SOUTH].s == ISS_ON) currentDEC -= da_dec; break; default: break; } switch (MovementWESP.s) { case IPS_BUSY: if (MovementWES[DIRECTION_WEST].s == ISS_ON) currentRA += da_ra / 15.; else if (MovementWES[DIRECTION_EAST].s == ISS_ON) currentRA -= da_ra / 15.; break; default: break; } NewRaDec(currentRA, currentDEC); return true; } /* Process per current state. We check the state of EQUATORIAL_EOD_COORDS_REQUEST and act acoordingly */ switch (TrackState) { /*case SCOPE_IDLE: EqNP.s = IPS_IDLE; break;*/ case SCOPE_SLEWING: case SCOPE_PARKING: /* slewing - nail it when both within one pulse @ SLEWRATE */ nlocked = 0; dx = targetRA - currentRA; // Always take the shortcut, don't go all around the globe // If the difference between target and current is more than 12 hours, then we need to take the shortest path if (dx > 12) dx -= 24; else if (dx < -12) dx += 24; // In meridian flip, alway force eastward motion (increasing RA) until target is reached. if (forceMeridianFlip) { dx = fabs(dx); if (dx == 0) { dx = 1; da_ra = GOTO_LIMIT; } } if (fabs(dx) * 15. <= da_ra) { currentRA = targetRA; nlocked++; } else if (dx > 0) currentRA += da_ra / 15.; else currentRA -= da_ra / 15.; currentRA = range24(currentRA); dy = targetDEC - currentDEC; if (fabs(dy) <= da_dec) { currentDEC = targetDEC; nlocked++; } else if (dy > 0) currentDEC += da_dec; else currentDEC -= da_dec; EqNP.s = IPS_BUSY; if (nlocked == 2) { forceMeridianFlip = false; if (TrackState == SCOPE_SLEWING) { // Initially no PE in both axis. #ifdef USE_EQUATORIAL_PE EqPEN[0].value = currentRA; EqPEN[1].value = currentDEC; IDSetNumber(&EqPENV, nullptr); #endif TrackState = SCOPE_TRACKING; if (IUFindOnSwitchIndex(&SlewRateSP) != SLEW_CENTERING) { IUResetSwitch(&SlewRateSP); SlewRateS[SLEW_CENTERING].s = ISS_ON; IDSetSwitch(&SlewRateSP, nullptr); } EqNP.s = IPS_OK; LOG_INFO("Telescope slew is complete. Tracking..."); } else { SetParked(true); EqNP.s = IPS_IDLE; } } break; case SCOPE_IDLE: //currentRA += (TRACKRATE_SIDEREAL/3600.0 * dt) / 15.0; currentRA += (TrackRateN[AXIS_RA].value/3600.0 * dt) / 15.0; currentRA = range24(currentRA); break; case SCOPE_TRACKING: // In case of custom tracking rate if (TrackModeS[1].s == ISS_ON) { currentRA += ( ((TRACKRATE_SIDEREAL/3600.0) - (TrackRateN[AXIS_RA].value/3600.0)) * dt) / 15.0; currentDEC += ( (TrackRateN[AXIS_DE].value/3600.0) * dt); } dt *= 1000; if (guiderNSTarget[GUIDE_NORTH] > 0) { LOGF_DEBUG("Commanded to GUIDE NORTH for %g ms", guiderNSTarget[GUIDE_NORTH]); ns_guide_dir = GUIDE_NORTH; } else if (guiderNSTarget[GUIDE_SOUTH] > 0) { LOGF_DEBUG("Commanded to GUIDE SOUTH for %g ms", guiderNSTarget[GUIDE_SOUTH]); ns_guide_dir = GUIDE_SOUTH; } // WE Guide Selection if (guiderEWTarget[GUIDE_WEST] > 0) { we_guide_dir = GUIDE_WEST; LOGF_DEBUG("Commanded to GUIDE WEST for %g ms", guiderEWTarget[GUIDE_WEST]); } else if (guiderEWTarget[GUIDE_EAST] > 0) { we_guide_dir = GUIDE_EAST; LOGF_DEBUG("Commanded to GUIDE EAST for %g ms", guiderEWTarget[GUIDE_EAST]); } if ( (ns_guide_dir != -1 || we_guide_dir != -1) && IUFindOnSwitchIndex(&SlewRateSP) != SLEW_GUIDE) { IUResetSwitch(&SlewRateSP); SlewRateS[SLEW_GUIDE].s = ISS_ON; IDSetSwitch(&SlewRateSP, nullptr); } if (ns_guide_dir != -1) { dec_guide_dt = (TrackRateN[AXIS_RA].value * GuideRateN[DEC_AXIS].value * guiderNSTarget[ns_guide_dir] / 1000.0 * (ns_guide_dir == GUIDE_NORTH ? 1 : -1)) / 3600.0; guiderNSTarget[ns_guide_dir] = 0; GuideNSNP.s = IPS_IDLE; IDSetNumber(&GuideNSNP, nullptr); #ifdef USE_EQUATORIAL_PE EqPEN[DEC_AXIS].value += dec_guide_dt; #else currentDEC += dec_guide_dt; #endif } if (we_guide_dir != -1) { ra_guide_dt = (TrackRateN[AXIS_RA].value * GuideRateN[RA_AXIS].value * guiderEWTarget[we_guide_dir] / 1000.0 * (we_guide_dir == GUIDE_WEST ? -1 : 1)) / (3600.0*15.0); ra_guide_dt /= (cos(currentDEC * 0.0174532925)); guiderEWTarget[we_guide_dir] = 0; GuideWENP.s = IPS_IDLE; IDSetNumber(&GuideWENP, nullptr); #ifdef USE_EQUATORIAL_PE EqPEN[RA_AXIS].value += ra_guide_dt; #else currentRA += ra_guide_dt; #endif } //Mention the followng: // Current RA displacemet and direction // Current DEC displacement and direction // Amount of RA GUIDING correction and direction // Amount of DEC GUIDING correction and direction #ifdef USE_EQUATORIAL_PE dx = EqPEN[RA_AXIS].value - targetRA; dy = EqPEN[DEC_AXIS].value - targetDEC; fs_sexa(RA_DISP, fabs(dx), 2, 3600); fs_sexa(DEC_DISP, fabs(dy), 2, 3600); fs_sexa(RA_GUIDE, fabs(ra_guide_dt), 2, 3600); fs_sexa(DEC_GUIDE, fabs(dec_guide_dt), 2, 3600); fs_sexa(RA_PE, EqPEN[RA_AXIS].value, 2, 3600); fs_sexa(DEC_PE, EqPEN[DEC_AXIS].value, 2, 3600); fs_sexa(RA_TARGET, targetRA, 2, 3600); fs_sexa(DEC_TARGET, targetDEC, 2, 3600); if (dx != last_dx || dy != last_dy || ra_guide_dt != 0.0 || dec_guide_dt != 0.0) { last_dx = dx; last_dy = dy; //LOGF_DEBUG("dt is %g\n", dt); LOGF_DEBUG("RA Displacement (%c%s) %s -- %s of target RA %s", dx >= 0 ? '+' : '-', RA_DISP, RA_PE, (EqPEN[RA_AXIS].value - targetRA) > 0 ? "East" : "West", RA_TARGET); LOGF_DEBUG("DEC Displacement (%c%s) %s -- %s of target RA %s", dy >= 0 ? '+' : '-', DEC_DISP, DEC_PE, (EqPEN[DEC_AXIS].value - targetDEC) > 0 ? "North" : "South", DEC_TARGET); LOGF_DEBUG("RA Guide Correction (%g) %s -- Direction %s", ra_guide_dt, RA_GUIDE, ra_guide_dt > 0 ? "East" : "West"); LOGF_DEBUG("DEC Guide Correction (%g) %s -- Direction %s", dec_guide_dt, DEC_GUIDE, dec_guide_dt > 0 ? "North" : "South"); } if (ns_guide_dir != -1 || we_guide_dir != -1) IDSetNumber(&EqPENV, nullptr); #endif break; default: break; } char RAStr[64], DecStr[64]; fs_sexa(RAStr, currentRA, 2, 3600); fs_sexa(DecStr, currentDEC, 2, 3600); DEBUGF(DBG_SCOPE, "Current RA: %s Current DEC: %s", RAStr, DecStr); NewRaDec(currentRA, currentDEC); return true; } bool ScopeSim::Goto(double r, double d) { targetRA = r; targetDEC = d; char RAStr[64], DecStr[64]; fs_sexa(RAStr, targetRA, 2, 3600); fs_sexa(DecStr, targetDEC, 2, 3600); ln_equ_posn lnradec { 0, 0 }; lnradec.ra = (currentRA * 360) / 24.0; lnradec.dec = currentDEC; ln_get_hrz_from_equ(&lnradec, &lnobserver, ln_get_julian_from_sys(), &lnaltaz); /* libnova measures azimuth from south towards west */ double current_az = range360(lnaltaz.az + 180); //double current_alt =lnaltaz.alt; if (current_az > MIN_AZ_FLIP && current_az < MAX_AZ_FLIP) { lnradec.ra = (r * 360) / 24.0; lnradec.dec = d; ln_get_hrz_from_equ(&lnradec, &lnobserver, ln_get_julian_from_sys(), &lnaltaz); double target_az = range360(lnaltaz.az + 180); //if (targetAz > currentAz && target_az > MIN_AZ_FLIP && target_az < MAX_AZ_FLIP) if (target_az >= current_az && target_az > MIN_AZ_FLIP) { forceMeridianFlip = true; } } if (IUFindOnSwitchIndex(&TrackModeSP) != SLEW_MAX) { IUResetSwitch(&TrackModeSP); TrackModeS[SLEW_MAX].s = ISS_ON; IDSetSwitch(&TrackModeSP, nullptr); } TrackState = SCOPE_SLEWING; EqNP.s = IPS_BUSY; LOGF_INFO("Slewing to RA: %s - DEC: %s", RAStr, DecStr); return true; } bool ScopeSim::Sync(double ra, double dec) { currentRA = ra; currentDEC = dec; #ifdef USE_EQUATORIAL_PE EqPEN[RA_AXIS].value = ra; EqPEN[DEC_AXIS].value = dec; IDSetNumber(&EqPENV, nullptr); #endif LOG_INFO("Sync is successful."); EqNP.s = IPS_OK; NewRaDec(currentRA, currentDEC); return true; } bool ScopeSim::Park() { targetRA = GetAxis1Park(); targetDEC = GetAxis2Park(); TrackState = SCOPE_PARKING; LOG_INFO("Parking telescope in progress..."); return true; } bool ScopeSim::UnPark() { SetParked(false); return true; } bool ScopeSim::ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) { // first check if it's for our device if (dev != nullptr && strcmp(dev, getDeviceName()) == 0) { if (strcmp(name, "GUIDE_RATE") == 0) { IUUpdateNumber(&GuideRateNP, values, names, n); GuideRateNP.s = IPS_OK; IDSetNumber(&GuideRateNP, nullptr); return true; } if (strcmp(name, GuideNSNP.name) == 0 || strcmp(name, GuideWENP.name) == 0) { processGuiderProperties(name, values, names, n); return true; } } // if we didn't process it, continue up the chain, let somebody else // give it a shot return INDI::Telescope::ISNewNumber(dev, name, values, names, n); } bool ScopeSim::ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) { if (dev != nullptr && strcmp(dev, getDeviceName()) == 0) { // Slew mode if (strcmp(name, SlewRateSP.name) == 0) { if (IUUpdateSwitch(&SlewRateSP, states, names, n) < 0) return false; SlewRateSP.s = IPS_OK; IDSetSwitch(&SlewRateSP, nullptr); return true; } #ifdef USE_EQUATORIAL_PE if (strcmp(name, "PE_NS") == 0) { IUUpdateSwitch(&PEErrNSSP, states, names, n); PEErrNSSP.s = IPS_OK; if (PEErrNSS[DIRECTION_NORTH].s == ISS_ON) { EqPEN[DEC_AXIS].value += TRACKRATE_SIDEREAL/3600.0 * GuideRateN[DEC_AXIS].value; LOGF_DEBUG("Simulating PE in NORTH direction for value of %g", TRACKRATE_SIDEREAL/3600.0); } else { EqPEN[DEC_AXIS].value -= TRACKRATE_SIDEREAL/3600.0 * GuideRateN[DEC_AXIS].value; LOGF_DEBUG("Simulating PE in SOUTH direction for value of %g", TRACKRATE_SIDEREAL/3600.0); } IUResetSwitch(&PEErrNSSP); IDSetSwitch(&PEErrNSSP, nullptr); IDSetNumber(&EqPENV, nullptr); return true; } if (strcmp(name, "PE_WE") == 0) { IUUpdateSwitch(&PEErrWESP, states, names, n); PEErrWESP.s = IPS_OK; if (PEErrWES[DIRECTION_WEST].s == ISS_ON) { EqPEN[RA_AXIS].value -= TRACKRATE_SIDEREAL/3600.0 / 15. * GuideRateN[RA_AXIS].value; LOGF_DEBUG("Simulator PE in WEST direction for value of %g", TRACKRATE_SIDEREAL/3600.0); } else { EqPEN[RA_AXIS].value += TRACKRATE_SIDEREAL/3600.0 / 15. * GuideRateN[RA_AXIS].value; LOGF_DEBUG("Simulator PE in EAST direction for value of %g", TRACKRATE_SIDEREAL/3600.0); } IUResetSwitch(&PEErrWESP); IDSetSwitch(&PEErrWESP, nullptr); IDSetNumber(&EqPENV, nullptr); return true; } #endif } // Nobody has claimed this, so, ignore it return INDI::Telescope::ISNewSwitch(dev, name, states, names, n); } bool ScopeSim::Abort() { return true; } bool ScopeSim::MoveNS(INDI_DIR_NS dir, TelescopeMotionCommand command) { INDI_UNUSED(dir); INDI_UNUSED(command); if (TrackState == SCOPE_PARKED) { LOG_ERROR("Please unpark the mount before issuing any motion commands."); return false; } return true; } bool ScopeSim::MoveWE(INDI_DIR_WE dir, TelescopeMotionCommand command) { INDI_UNUSED(dir); INDI_UNUSED(command); if (TrackState == SCOPE_PARKED) { LOG_ERROR("Please unpark the mount before issuing any motion commands."); return false; } return true; } IPState ScopeSim::GuideNorth(float ms) { guiderNSTarget[GUIDE_NORTH] = ms; guiderNSTarget[GUIDE_SOUTH] = 0; return IPS_BUSY; } IPState ScopeSim::GuideSouth(float ms) { guiderNSTarget[GUIDE_SOUTH] = ms; guiderNSTarget[GUIDE_NORTH] = 0; return IPS_BUSY; } IPState ScopeSim::GuideEast(float ms) { guiderEWTarget[GUIDE_EAST] = ms; guiderEWTarget[GUIDE_WEST] = 0; return IPS_BUSY; } IPState ScopeSim::GuideWest(float ms) { guiderEWTarget[GUIDE_WEST] = ms; guiderEWTarget[GUIDE_EAST] = 0; return IPS_BUSY; } bool ScopeSim::updateLocation(double latitude, double longitude, double elevation) { INDI_UNUSED(elevation); // JM: INDI Longitude is 0 to 360 increasing EAST. libnova East is Positive, West is negative lnobserver.lng = longitude; if (lnobserver.lng > 180) lnobserver.lng -= 360; lnobserver.lat = latitude; LOGF_INFO("Location updated: Longitude (%g) Latitude (%g)", lnobserver.lng, lnobserver.lat); return true; } bool ScopeSim::SetCurrentPark() { SetAxis1Park(currentRA); SetAxis2Park(currentDEC); return true; } bool ScopeSim::SetDefaultPark() { // By default set RA to HA SetAxis1Park(get_local_sidereal_time(LocationN[LOCATION_LONGITUDE].value)); // Set DEC to 90 or -90 depending on the hemisphere SetAxis2Park((LocationN[LOCATION_LATITUDE].value > 0) ? 90 : -90); return true; } bool ScopeSim::SetTrackMode(uint8_t mode) { INDI_UNUSED(mode); return true; } bool ScopeSim::SetTrackEnabled(bool enabled) { INDI_UNUSED(enabled); return true; } bool ScopeSim::SetTrackRate(double raRate, double deRate) { INDI_UNUSED(raRate); INDI_UNUSED(deRate); return true; } libindi/drivers/telescope/ieqpro.cpp0000664000175000017500000006240113263645557017141 0ustar jasemjasem/* INDI IEQ Pro driver Copyright (C) 2015 Jasem Mutlaq 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 */ #include "ieqpro.h" #include "indicom.h" #include #include #include #include /* Simulation Parameters */ #define SLEWRATE 1 /* slew rate, degrees/s */ #define MOUNTINFO_TAB "Mount Info" // We declare an auto pointer to IEQPro. std::unique_ptr scope(new IEQPro()); void ISGetProperties(const char *dev) { scope->ISGetProperties(dev); } void ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) { scope->ISNewSwitch(dev, name, states, names, n); } void ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n) { scope->ISNewText(dev, name, texts, names, n); } void ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) { scope->ISNewNumber(dev, name, values, names, n); } void ISNewBLOB(const char *dev, const char *name, int sizes[], int blobsizes[], char *blobs[], char *formats[], char *names[], int n) { INDI_UNUSED(dev); INDI_UNUSED(name); INDI_UNUSED(sizes); INDI_UNUSED(blobsizes); INDI_UNUSED(blobs); INDI_UNUSED(formats); INDI_UNUSED(names); INDI_UNUSED(n); } void ISSnoopDevice(XMLEle *root) { scope->ISSnoopDevice(root); } /* Constructor */ IEQPro::IEQPro() { set_ieqpro_device(getDeviceName()); scopeInfo.gpsStatus = GPS_OFF; scopeInfo.systemStatus = ST_STOPPED; scopeInfo.trackRate = TR_SIDEREAL; scopeInfo.slewRate = SR_1; scopeInfo.timeSource = TS_RS232; scopeInfo.hemisphere = HEMI_NORTH; DBG_SCOPE = INDI::Logger::getInstance().addDebugLevel("Scope Verbose", "SCOPE"); SetTelescopeCapability(TELESCOPE_CAN_PARK | TELESCOPE_CAN_SYNC | TELESCOPE_CAN_GOTO | TELESCOPE_CAN_ABORT | TELESCOPE_HAS_TIME | TELESCOPE_HAS_LOCATION | TELESCOPE_HAS_TRACK_MODE | TELESCOPE_CAN_CONTROL_TRACK | TELESCOPE_HAS_TRACK_RATE, 9); } const char *IEQPro::getDefaultName() { return (const char *)"iEQ"; } bool IEQPro::initProperties() { INDI::Telescope::initProperties(); /* Firmware */ IUFillText(&FirmwareT[FW_MODEL], "Model", "", 0); IUFillText(&FirmwareT[FW_BOARD], "Board", "", 0); IUFillText(&FirmwareT[FW_CONTROLLER], "Controller", "", 0); IUFillText(&FirmwareT[FW_RA], "RA", "", 0); IUFillText(&FirmwareT[FW_DEC], "DEC", "", 0); IUFillTextVector(&FirmwareTP, FirmwareT, 5, getDeviceName(), "Firmware Info", "", MOUNTINFO_TAB, IP_RO, 0, IPS_IDLE); /* Tracking Mode */ AddTrackMode("TRACK_SIDEREAL", "Sidereal", true); AddTrackMode("TRACK_SOLAR", "Solar"); AddTrackMode("TRACK_LUNAR", "Lunar"); AddTrackMode("TRACK_KING", "King"); AddTrackMode("TRACK_CUSTOM", "Custom"); // Set TrackRate limits within +/- 0.0100 of Sidereal rate TrackRateN[AXIS_RA].min = TRACKRATE_SIDEREAL - 0.01; TrackRateN[AXIS_RA].max = TRACKRATE_SIDEREAL + 0.01; TrackRateN[AXIS_DE].min = -0.01; TrackRateN[AXIS_DE].max = 0.01; /* GPS Status */ IUFillSwitch(&GPSStatusS[GPS_OFF], "Off", "", ISS_ON); IUFillSwitch(&GPSStatusS[GPS_ON], "On", "", ISS_OFF); IUFillSwitch(&GPSStatusS[GPS_DATA_OK], "Data OK", "", ISS_OFF); IUFillSwitchVector(&GPSStatusSP, GPSStatusS, 3, getDeviceName(), "GPS_STATUS", "GPS", MOUNTINFO_TAB, IP_RO, ISR_1OFMANY, 0, IPS_IDLE); /* Time Source */ IUFillSwitch(&TimeSourceS[TS_RS232], "RS232", "", ISS_ON); IUFillSwitch(&TimeSourceS[TS_CONTROLLER], "Controller", "", ISS_OFF); IUFillSwitch(&TimeSourceS[TS_GPS], "GPS", "", ISS_OFF); IUFillSwitchVector(&TimeSourceSP, TimeSourceS, 3, getDeviceName(), "TIME_SOURCE", "Time Source", MOUNTINFO_TAB, IP_RO, ISR_1OFMANY, 0, IPS_IDLE); /* Hemisphere */ IUFillSwitch(&HemisphereS[HEMI_SOUTH], "South", "", ISS_OFF); IUFillSwitch(&HemisphereS[HEMI_NORTH], "North", "", ISS_ON); IUFillSwitchVector(&HemisphereSP, HemisphereS, 2, getDeviceName(), "HEMISPHERE", "Hemisphere", MOUNTINFO_TAB, IP_RO, ISR_1OFMANY, 0, IPS_IDLE); /* Home */ IUFillSwitch(&HomeS[IEQ_FIND_HOME], "FindHome", "Find Home", ISS_OFF); IUFillSwitch(&HomeS[IEQ_SET_HOME], "SetCurrentAsHome", "Set current as Home", ISS_OFF); IUFillSwitch(&HomeS[IEQ_GOTO_HOME], "GoToHome", "Go to Home", ISS_OFF); IUFillSwitchVector(&HomeSP, HomeS, 3, getDeviceName(), "HOME", "Home", MAIN_CONTROL_TAB, IP_RW, ISR_ATMOST1, 0, IPS_IDLE); /* How fast do we guide compared to sidereal rate */ IUFillNumber(&GuideRateN[0], "GUIDE_RATE", "x Sidereal", "%g", 0.1, 0.9, 0.1, 0.5); IUFillNumberVector(&GuideRateNP, GuideRateN, 1, getDeviceName(), "GUIDE_RATE", "Guiding Rate", MOTION_TAB, IP_RW, 0, IPS_IDLE); TrackState = SCOPE_IDLE; initGuiderProperties(getDeviceName(), MOTION_TAB); setDriverInterface(getDriverInterface() | GUIDER_INTERFACE); SetParkDataType(PARK_RA_DEC); addAuxControls(); double longitude=0, latitude=90; // Get value from config file if it exists. IUGetConfigNumber(getDeviceName(), "GEOGRAPHIC_COORD", "LONG", &longitude); currentRA = get_local_sidereal_time(longitude); IUGetConfigNumber(getDeviceName(), "GEOGRAPHIC_COORD", "LAT", &latitude); currentDEC = latitude > 0 ? 90 : -90; return true; } bool IEQPro::updateProperties() { INDI::Telescope::updateProperties(); if (isConnected()) { defineSwitch(&HomeSP); defineNumber(&GuideNSNP); defineNumber(&GuideWENP); defineNumber(&GuideRateNP); defineText(&FirmwareTP); defineSwitch(&GPSStatusSP); defineSwitch(&TimeSourceSP); defineSwitch(&HemisphereSP); getStartupData(); } else { deleteProperty(HomeSP.name); deleteProperty(GuideNSNP.name); deleteProperty(GuideWENP.name); deleteProperty(GuideRateNP.name); deleteProperty(FirmwareTP.name); deleteProperty(GPSStatusSP.name); deleteProperty(TimeSourceSP.name); deleteProperty(HemisphereSP.name); } return true; } void IEQPro::getStartupData() { LOG_DEBUG("Getting firmware data..."); if (get_ieqpro_firmware(PortFD, &firmwareInfo)) { IUSaveText(&FirmwareT[0], firmwareInfo.Model.c_str()); IUSaveText(&FirmwareT[1], firmwareInfo.MainBoardFirmware.c_str()); IUSaveText(&FirmwareT[2], firmwareInfo.ControllerFirmware.c_str()); IUSaveText(&FirmwareT[3], firmwareInfo.RAFirmware.c_str()); IUSaveText(&FirmwareT[4], firmwareInfo.DEFirmware.c_str()); FirmwareTP.s = IPS_OK; IDSetText(&FirmwareTP, nullptr); } LOG_DEBUG("Getting guiding rate..."); double guideRate = 0; if (get_ieqpro_guide_rate(PortFD, &guideRate)) { GuideRateN[0].value = guideRate; IDSetNumber(&GuideRateNP, nullptr); } double utc_offset; int yy, dd, mm, hh, minute, ss; if (get_ieqpro_utc_date_time(PortFD, &utc_offset, &yy, &mm, &dd, &hh, &minute, &ss)) { char isoDateTime[32]={0}; char utcOffset[8]={0}; snprintf(isoDateTime, 32, "%04d-%02d-%02dT%02d:%02d:%02d", yy, mm, dd, hh, minute, ss); snprintf(utcOffset, 8, "%4.2f", utc_offset); IUSaveText(IUFindText(&TimeTP, "UTC"), isoDateTime); IUSaveText(IUFindText(&TimeTP, "OFFSET"), utcOffset); LOGF_INFO("Mount UTC offset is %s. UTC time is %s", utcOffset, isoDateTime); IDSetText(&TimeTP, nullptr); } // Get Longitude and Latitude from mount double longitude = 0, latitude = 0; if (get_ieqpro_latitude(PortFD, &latitude) && get_ieqpro_longitude(PortFD, &longitude)) { // Convert to INDI standard longitude (0 to 360 Eastward) if (longitude < 0) longitude += 360; LocationN[LOCATION_LATITUDE].value = latitude; LocationN[LOCATION_LONGITUDE].value = longitude; LocationNP.s = IPS_OK; IDSetNumber(&LocationNP, nullptr); } double DEC = (latitude > 0) ? 90 : -90; if (InitPark()) { // If loading parking data is successful, we just set the default parking values. SetAxis1ParkDefault(currentRA); SetAxis2ParkDefault(DEC); } else { // Otherwise, we set all parking data to default in case no parking data is found. SetAxis1Park(currentRA); SetAxis2Park(DEC); SetAxis1ParkDefault(currentRA); SetAxis2ParkDefault(DEC); } if (isSimulation()) { if (isParked()) set_sim_system_status(ST_PARKED); else set_sim_system_status(ST_STOPPED); } } bool IEQPro::ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) { if (dev != nullptr && strcmp(dev, getDeviceName()) == 0) { // Guiding Rate if (!strcmp(name, GuideRateNP.name)) { IUUpdateNumber(&GuideRateNP, values, names, n); if (set_ieqpro_guide_rate(PortFD, GuideRateN[0].value)) GuideRateNP.s = IPS_OK; else GuideRateNP.s = IPS_ALERT; IDSetNumber(&GuideRateNP, nullptr); return true; } if (!strcmp(name, GuideNSNP.name) || !strcmp(name, GuideWENP.name)) { processGuiderProperties(name, values, names, n); return true; } } return INDI::Telescope::ISNewNumber(dev, name, values, names, n); } bool IEQPro::ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) { if (!strcmp(getDeviceName(), dev)) { if (!strcmp(name, HomeSP.name)) { IUUpdateSwitch(&HomeSP, states, names, n); IEQ_HOME_OPERATION operation = (IEQ_HOME_OPERATION)IUFindOnSwitchIndex(&HomeSP); IUResetSwitch(&HomeSP); switch (operation) { case IEQ_FIND_HOME: if (firmwareInfo.Model.find("CEM") == std::string::npos) { HomeSP.s = IPS_IDLE; IDSetSwitch(&HomeSP, nullptr); LOG_WARN("Home search is not supported in this model."); return true; } if (find_ieqpro_home(PortFD) == false) { HomeSP.s = IPS_ALERT; IDSetSwitch(&HomeSP, nullptr); return false; } HomeSP.s = IPS_OK; IDSetSwitch(&HomeSP, nullptr); LOG_INFO("Searching for home position..."); return true; break; case IEQ_SET_HOME: if (set_ieqpro_current_home(PortFD) == false) { HomeSP.s = IPS_ALERT; IDSetSwitch(&HomeSP, nullptr); return false; } HomeSP.s = IPS_OK; IDSetSwitch(&HomeSP, nullptr); LOG_INFO("Home position set to current coordinates."); return true; break; case IEQ_GOTO_HOME: if (goto_ieqpro_home(PortFD) == false) { HomeSP.s = IPS_ALERT; IDSetSwitch(&HomeSP, nullptr); return false; } HomeSP.s = IPS_OK; IDSetSwitch(&HomeSP, nullptr); LOG_INFO("Slewing to home position..."); return true; break; } return true; } } return INDI::Telescope::ISNewSwitch(dev, name, states, names, n); } bool IEQPro::ReadScopeStatus() { bool rc = false; IEQInfo newInfo; if (isSimulation()) mountSim(); rc = get_ieqpro_status(PortFD, &newInfo); if (rc) { IUResetSwitch(&GPSStatusSP); GPSStatusS[newInfo.gpsStatus].s = ISS_ON; IDSetSwitch(&GPSStatusSP, nullptr); IUResetSwitch(&TimeSourceSP); TimeSourceS[newInfo.timeSource].s = ISS_ON; IDSetSwitch(&TimeSourceSP, nullptr); IUResetSwitch(&HemisphereSP); HemisphereS[newInfo.hemisphere].s = ISS_ON; IDSetSwitch(&HemisphereSP, nullptr); /* TelescopeTrackMode trackMode = TRACK_SIDEREAL; switch (newInfo.trackRate) { case TR_SIDEREAL: trackMode = TRACK_SIDEREAL; break; case TR_SOLAR: trackMode = TRACK_SOLAR; break; case TR_LUNAR: trackMode = TRACK_LUNAR; break; case TR_KING: trackMode = TRACK_SIDEREAL; break; case TR_CUSTOM: trackMode = TRACK_CUSTOM; break; }*/ switch (newInfo.systemStatus) { case ST_STOPPED: TrackModeSP.s = IPS_IDLE; TrackState = SCOPE_IDLE; break; case ST_PARKED: TrackModeSP.s = IPS_IDLE; TrackState = SCOPE_PARKED; if (!isParked()) SetParked(true); break; case ST_HOME: TrackModeSP.s = IPS_IDLE; TrackState = SCOPE_IDLE; break; case ST_SLEWING: case ST_MERIDIAN_FLIPPING: if (TrackState != SCOPE_SLEWING && TrackState != SCOPE_PARKING) TrackState = SCOPE_SLEWING; break; case ST_TRACKING_PEC_OFF: case ST_TRACKING_PEC_ON: case ST_GUIDING: // If slew to parking position is complete, issue park command now. if (TrackState == SCOPE_PARKING) park_ieqpro(PortFD); else { TrackModeSP.s = IPS_BUSY; TrackState = SCOPE_TRACKING; if (scopeInfo.systemStatus == ST_SLEWING) LOG_INFO("Slew complete, tracking..."); else if (scopeInfo.systemStatus == ST_MERIDIAN_FLIPPING) LOG_INFO("Meridian flip complete, tracking..."); } break; } IUResetSwitch(&TrackModeSP); TrackModeS[newInfo.trackRate].s = ISS_ON; IDSetSwitch(&TrackModeSP, nullptr); scopeInfo = newInfo; } rc = get_ieqpro_coords(PortFD, ¤tRA, ¤tDEC); if (rc) NewRaDec(currentRA, currentDEC); return rc; } bool IEQPro::Goto(double r, double d) { targetRA = r; targetDEC = d; char RAStr[64]={0}, DecStr[64]={0}; fs_sexa(RAStr, targetRA, 2, 3600); fs_sexa(DecStr, targetDEC, 2, 3600); if (set_ieqpro_ra(PortFD, r) == false || set_ieqpro_dec(PortFD, d) == false) { LOG_ERROR("Error setting RA/DEC."); return false; } if (slew_ieqpro(PortFD) == false) { LOG_ERROR("Failed to slew."); return false; } TrackState = SCOPE_SLEWING; LOGF_INFO("Slewing to RA: %s - DEC: %s", RAStr, DecStr); return true; } bool IEQPro::Sync(double ra, double dec) { if (set_ieqpro_ra(PortFD, ra) == false || set_ieqpro_dec(PortFD, dec) == false) { LOG_ERROR("Error setting RA/DEC."); return false; } if (sync_ieqpro(PortFD) == false) { LOG_ERROR("Failed to sync."); } EqNP.s = IPS_OK; currentRA = ra; currentDEC = dec; NewRaDec(currentRA, currentDEC); return true; } bool IEQPro::Abort() { return abort_ieqpro(PortFD); } bool IEQPro::Park() { targetRA = GetAxis1Park(); targetDEC = GetAxis2Park(); if (set_ieqpro_ra(PortFD, targetRA) == false || set_ieqpro_dec(PortFD, targetDEC) == false) { LOG_ERROR("Error setting RA/DEC."); return false; } if (slew_ieqpro(PortFD) == false) { LOG_ERROR("Failed to slew tp parking position."); return false; } char RAStr[64]={0}, DecStr[64]={0}; fs_sexa(RAStr, targetRA, 2, 3600); fs_sexa(DecStr, targetDEC, 2, 3600); TrackState = SCOPE_PARKING; LOGF_INFO("Telescope parking in progress to RA: %s DEC: %s", RAStr, DecStr); return true; } bool IEQPro::UnPark() { if (unpark_ieqpro(PortFD)) { SetParked(false); TrackState = SCOPE_IDLE; return true; } else return false; } bool IEQPro::Handshake() { if (isSimulation()) { set_sim_gps_status(GPS_DATA_OK); set_sim_system_status(ST_STOPPED); set_sim_track_rate(TR_SIDEREAL); set_sim_slew_rate(SR_3); set_sim_time_source(TS_GPS); set_sim_hemisphere(HEMI_NORTH); } if (check_ieqpro_connection(PortFD) == false) return false; return true; } bool IEQPro::updateTime(ln_date *utc, double utc_offset) { struct ln_zonedate ltm; ln_date_to_zonedate(utc, <m, utc_offset * 3600.0); // Set Local Time if (set_ieqpro_local_time(PortFD, ltm.hours, ltm.minutes, ltm.seconds) == false) { LOG_ERROR("Error setting local time."); return false; } // Send it as YY (i.e. 2015 --> 15) ltm.years -= 2000; // Set Local date if (set_ieqpro_local_date(PortFD, ltm.years, ltm.months, ltm.days) == false) { LOG_ERROR("Error setting local date."); return false; } // UTC Offset if (set_ieqpro_utc_offset(PortFD, utc_offset) == false) { LOG_ERROR("Error setting UTC Offset."); return false; } LOG_INFO("Time and date updated."); return true; } bool IEQPro::updateLocation(double latitude, double longitude, double elevation) { INDI_UNUSED(elevation); if (longitude > 180) longitude -= 360; if (set_ieqpro_longitude(PortFD, longitude) == false) { LOG_ERROR("Failed to set longitude."); return false; } if (set_ieqpro_latitude(PortFD, latitude) == false) { LOG_ERROR("Failed to set longitude."); return false; } char l[32]={0}, L[32]={0}; fs_sexa(l, latitude, 3, 3600); fs_sexa(L, longitude, 4, 3600); LOGF_INFO("Site location updated to Lat %.32s - Long %.32s", l, L); return true; } void IEQPro::debugTriggered(bool enable) { set_ieqpro_debug(enable); } void IEQPro::simulationTriggered(bool enable) { set_ieqpro_simulation(enable); } bool IEQPro::MoveNS(INDI_DIR_NS dir, TelescopeMotionCommand command) { if (TrackState == SCOPE_PARKED) { LOG_ERROR("Please unpark the mount before issuing any motion commands."); return false; } switch (command) { case MOTION_START: if (start_ieqpro_motion(PortFD, (dir == DIRECTION_NORTH ? IEQ_N : IEQ_S)) == false) { LOG_ERROR("Error setting N/S motion direction."); return false; } else LOGF_INFO("Moving toward %s.", (dir == DIRECTION_NORTH) ? "North" : "South"); break; case MOTION_STOP: if (stop_ieqpro_motion(PortFD, (dir == DIRECTION_NORTH ? IEQ_N : IEQ_S)) == false) { LOG_ERROR("Error stopping N/S motion."); return false; } else LOGF_INFO("%s motion stopped.", (dir == DIRECTION_NORTH) ? "North" : "South"); break; } return true; } bool IEQPro::MoveWE(INDI_DIR_WE dir, TelescopeMotionCommand command) { if (TrackState == SCOPE_PARKED) { LOG_ERROR("Please unpark the mount before issuing any motion commands."); return false; } switch (command) { case MOTION_START: if (start_ieqpro_motion(PortFD, (dir == DIRECTION_WEST ? IEQ_W : IEQ_E)) == false) { LOG_ERROR("Error setting N/S motion direction."); return false; } else LOGF_INFO("Moving toward %s.", (dir == DIRECTION_WEST) ? "West" : "East"); break; case MOTION_STOP: if (stop_ieqpro_motion(PortFD, (dir == DIRECTION_WEST ? IEQ_W : IEQ_E)) == false) { LOG_ERROR("Error stopping W/E motion."); return false; } else LOGF_INFO("%s motion stopped.", (dir == DIRECTION_WEST) ? "West" : "East"); break; } return true; } IPState IEQPro::GuideNorth(float ms) { bool rc = start_ieqpro_guide(PortFD, IEQ_N, (int)ms); return (rc ? IPS_OK : IPS_ALERT); } IPState IEQPro::GuideSouth(float ms) { bool rc = start_ieqpro_guide(PortFD, IEQ_S, (int)ms); return (rc ? IPS_OK : IPS_ALERT); } IPState IEQPro::GuideEast(float ms) { bool rc = start_ieqpro_guide(PortFD, IEQ_E, (int)ms); return (rc ? IPS_OK : IPS_ALERT); } IPState IEQPro::GuideWest(float ms) { bool rc = start_ieqpro_guide(PortFD, IEQ_W, (int)ms); return (rc ? IPS_OK : IPS_ALERT); } bool IEQPro::SetSlewRate(int index) { IEQ_SLEW_RATE rate = (IEQ_SLEW_RATE)index; return set_ieqpro_slew_rate(PortFD, rate); } bool IEQPro::saveConfigItems(FILE *fp) { INDI::Telescope::saveConfigItems(fp); return true; } void IEQPro::mountSim() { static struct timeval ltv; struct timeval tv; double dt, da, dx; int nlocked; /* update elapsed time since last poll, don't presume exactly POLLMS */ gettimeofday(&tv, nullptr); if (ltv.tv_sec == 0 && ltv.tv_usec == 0) ltv = tv; dt = tv.tv_sec - ltv.tv_sec + (tv.tv_usec - ltv.tv_usec) / 1e6; ltv = tv; da = SLEWRATE * dt; /* Process per current state. We check the state of EQUATORIAL_COORDS and act acoordingly */ switch (TrackState) { case SCOPE_IDLE: currentRA += (TrackRateN[AXIS_RA].value/3600.0 * dt) / 15.0; currentRA = range24(currentRA); break; case SCOPE_TRACKING: if (TrackModeS[1].s == ISS_ON) { currentRA += ( ((TRACKRATE_SIDEREAL/3600.0) - (TrackRateN[AXIS_RA].value/3600.0)) * dt) / 15.0; currentDEC += ( (TrackRateN[AXIS_DE].value/3600.0) * dt); } break; case SCOPE_SLEWING: case SCOPE_PARKING: /* slewing - nail it when both within one pulse @ SLEWRATE */ nlocked = 0; dx = targetRA - currentRA; // Take shortest path if (fabs(dx) > 12) dx *= -1; if (fabs(dx) <= da) { currentRA = targetRA; nlocked++; } else if (dx > 0) currentRA += da / 15.; else currentRA -= da / 15.; if (currentRA < 0) currentRA += 24; else if (currentRA > 24) currentRA -= 24; dx = targetDEC - currentDEC; if (fabs(dx) <= da) { currentDEC = targetDEC; nlocked++; } else if (dx > 0) currentDEC += da; else currentDEC -= da; if (nlocked == 2) { if (TrackState == SCOPE_SLEWING) set_sim_system_status(ST_TRACKING_PEC_OFF); else set_sim_system_status(ST_PARKED); } break; default: break; } set_sim_ra(currentRA); set_sim_dec(currentDEC); } bool IEQPro::SetCurrentPark() { SetAxis1Park(currentRA); SetAxis2Park(currentDEC); return true; } bool IEQPro::SetDefaultPark() { // By default set RA to LST SetAxis1Park(get_local_sidereal_time(LocationN[LOCATION_LONGITUDE].value)); // Set DEC to 90 or -90 depending on the hemisphere SetAxis2Park((HemisphereS[HEMI_NORTH].s == ISS_ON) ? 90 : -90); return true; } bool IEQPro::SetTrackMode(uint8_t mode) { IEQ_TRACK_RATE rate = static_cast(mode); if (set_ieqpro_track_mode(PortFD, rate)) return true; return false; } bool IEQPro::SetTrackRate(double raRate, double deRate) { static bool deRateWarning = true; // Convert to arcsecs/s to +/- 0.0100 accepted by double ieqRARate = raRate - TRACKRATE_SIDEREAL; if (deRate != 0 && deRateWarning) { // Only send warning once per session deRateWarning = false; LOG_WARN("Custom Declination tracking rate is not implemented yet."); } if (set_ieqpro_custom_ra_track_rate(PortFD, ieqRARate)) return true; return false; } bool IEQPro::SetTrackEnabled(bool enabled) { if (enabled) { // If we are engaging tracking, let us first set tracking mode, and if we have custom mode, then tracking rate. // NOTE: Is this the correct order? or should tracking be switched on first before making these changes? Need to test. SetTrackMode(IUFindOnSwitchIndex(&TrackModeSP)); if (TrackModeS[TR_CUSTOM].s == ISS_ON) SetTrackRate(TrackRateN[AXIS_RA].value, TrackRateN[AXIS_DE].value); } return set_ieqpro_track_enabled(PortFD, enabled); } libindi/drivers/telescope/telescope_script.cpp0000664000175000017500000002251313263645557021211 0ustar jasemjasem/******************************************************************************* Copyright(c) 2016 CloudMakers, s. r. o.. All rights reserved. This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2 as published by the Free Software Foundation. . 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 Library General Public License for more details. . You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. *******************************************************************************/ #include "telescope_script.h" #include #include #include #include #define MAXARGS 20 typedef enum { SCRIPT_CONNECT = 1, SCRIPT_DISCONNECT, SCRIPT_STATUS, SCRIPT_GOTO, SCRIPT_SYNC, SCRIPT_PARK, SCRIPT_UNPARK, SCRIPT_MOVE_NORTH, SCRIPT_MOVE_EAST, SCRIPT_MOVE_SOUTH, SCRIPT_MOVE_WEST, SCRIPT_ABORT, SCRIPT_COUNT } scripts; std::unique_ptr scope_script(new ScopeScript()); void ISGetProperties(const char *dev) { scope_script->ISGetProperties(dev); } void ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) { scope_script->ISNewSwitch(dev, name, states, names, n); } void ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n) { scope_script->ISNewText(dev, name, texts, names, n); } void ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) { scope_script->ISNewNumber(dev, name, values, names, n); } void ISNewBLOB(const char *dev, const char *name, int sizes[], int blobsizes[], char *blobs[], char *formats[], char *names[], int n) { INDI_UNUSED(dev); INDI_UNUSED(name); INDI_UNUSED(sizes); INDI_UNUSED(blobsizes); INDI_UNUSED(blobs); INDI_UNUSED(formats); INDI_UNUSED(names); INDI_UNUSED(n); } void ISSnoopDevice(XMLEle *root) { scope_script->ISSnoopDevice(root); } ScopeScript::ScopeScript() { SetTelescopeCapability(TELESCOPE_CAN_PARK | TELESCOPE_CAN_SYNC | TELESCOPE_CAN_GOTO | TELESCOPE_CAN_ABORT, 4); } const char *ScopeScript::getDefaultName() { return (const char *)"Telescope Scripting Gateway"; } bool ScopeScript::initProperties() { INDI::Telescope::initProperties(); #if defined(__APPLE__) IUFillText(&ScriptsT[0], "FOLDER", "Folder", "/usr/local/share/indi/scripts"); #else IUFillText(&ScriptsT[0], "FOLDER", "Folder", "/usr/share/indi/scripts"); #endif IUFillText(&ScriptsT[SCRIPT_CONNECT], "SCRIPT_CONNECT", "Connect script", "connect.py"); IUFillText(&ScriptsT[SCRIPT_DISCONNECT], "SCRIPT_DISCONNECT", "Disconnect script", "disconnect.py"); IUFillText(&ScriptsT[SCRIPT_STATUS], "SCRIPT_STATUS", "Get status script", "status.py"); IUFillText(&ScriptsT[SCRIPT_GOTO], "SCRIPT_GOTO", "Goto script", "goto.py"); IUFillText(&ScriptsT[SCRIPT_SYNC], "SCRIPT_SYNC", "Sync script", "sync.py"); IUFillText(&ScriptsT[SCRIPT_PARK], "SCRIPT_PARK", "Park script", "park.py"); IUFillText(&ScriptsT[SCRIPT_UNPARK], "SCRIPT_UNPARK", "Unpark script", "unpark.py"); IUFillText(&ScriptsT[SCRIPT_MOVE_NORTH], "SCRIPT_MOVE_NORTH", "Move north script", "move_north.py"); IUFillText(&ScriptsT[SCRIPT_MOVE_EAST], "SCRIPT_MOVE_EAST", "Move east script", "move_east.py"); IUFillText(&ScriptsT[SCRIPT_MOVE_SOUTH], "SCRIPT_MOVE_SOUTH", "Move south script", "move_south.py"); IUFillText(&ScriptsT[SCRIPT_MOVE_WEST], "SCRIPT_MOVE_WEST", "Move west script", "move_west.py"); IUFillText(&ScriptsT[SCRIPT_ABORT], "SCRIPT_ABORT", "Abort motion script", "abort.py"); IUFillTextVector(&ScriptsTP, ScriptsT, SCRIPT_COUNT, getDefaultName(), "SCRIPTS", "Scripts", OPTIONS_TAB, IP_RW, 60, IPS_IDLE); addDebugControl(); setDriverInterface(getDriverInterface()); return true; } bool ScopeScript::saveConfigItems(FILE *fp) { INDI::Telescope::saveConfigItems(fp); IUSaveConfigText(fp, &ScriptsTP); return true; } void ScopeScript::ISGetProperties(const char *dev) { INDI::Telescope::ISGetProperties(dev); defineText(&ScriptsTP); } bool ScopeScript::ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n) { if (strcmp(dev, getDeviceName()) == 0 && strcmp(name, ScriptsTP.name) == 0) { IUUpdateText(&ScriptsTP, texts, names, n); IDSetText(&ScriptsTP, nullptr); return true; } return Telescope::ISNewText(dev, name, texts, names, n); } bool ScopeScript::RunScript(int script, ...) { char tmp[256]; strncpy(tmp, ScriptsT[script].text, sizeof(tmp)); char **args = (char **)malloc(MAXARGS * sizeof(char *)); int arg = 1; char *p = tmp; args[0] = p; while (arg < MAXARGS) { char *pp = strstr(p, " "); if (pp == nullptr) break; *pp++ = 0; args[arg++] = pp; p = pp; } va_list ap; va_start(ap, script); while (arg < MAXARGS) { char *pp = va_arg(ap, char *); args[arg++] = pp; if (pp == nullptr) break; } va_end(ap); char path[1024]; snprintf(path, sizeof(path), "%s/%s", ScriptsT[0].text, tmp); if (isDebug()) { char dbg[8 * 1024]; snprintf(dbg, sizeof(dbg), "execvp('%s'", path); for (int i = 0; args[i]; i++) { strcat(dbg, ", '"); strcat(dbg, args[i]); strcat(dbg, "'"); } strcat(dbg, ", NULL)"); LOG_DEBUG(dbg); } int pid = fork(); if (pid == -1) { LOG_ERROR("Fork failed"); return false; } else if (pid == 0) { execvp(path, args); LOG_DEBUG("Failed to execute script"); exit(0); } else { int status; waitpid(pid, &status, 0); LOGF_DEBUG("Script %s returned %d", ScriptsT[script].text, status); return status == 0; } } bool ScopeScript::Handshake() { return true; } bool ScopeScript::Connect() { if (isConnected()) return true; bool status = RunScript(SCRIPT_CONNECT, nullptr); if (status) { LOG_INFO("Successfully connected"); ReadScopeStatus(); SetTimer(POLLMS); } return status; } bool ScopeScript::Disconnect() { bool status = RunScript(SCRIPT_DISCONNECT, nullptr); if (status) { LOG_INFO("Successfully disconnected"); } return status; } bool ScopeScript::ReadScopeStatus() { char name[1024]; char *s = tmpnam(name); INDI_UNUSED(s); bool status = RunScript(SCRIPT_STATUS, name, nullptr); if (status) { int parked = 0; float ra = 0, dec = 0; FILE *file = fopen(name, "r"); int ret = 0; ret = fscanf(file, "%d %f %f", &parked, &ra, &dec); fclose(file); unlink(name); if (parked != 0) { if (!isParked()) { SetParked(true); LOG_INFO("Park succesfully executed"); } } else { if (isParked()) { SetParked(false); LOG_INFO("Unpark succesfully executed"); } } NewRaDec(ra, dec); } else { LOG_ERROR("Failed to read status"); } return status; } bool ScopeScript::Goto(double ra, double dec) { char _ra[16], _dec[16]; snprintf(_ra, 16, "%f", ra); snprintf(_dec, 16, "%f", dec); bool status = RunScript(SCRIPT_GOTO, _ra, _dec, nullptr); if (status) { LOG_INFO("Goto succesfully executed"); } return status; } bool ScopeScript::Sync(double ra, double dec) { char _ra[16], _dec[16]; snprintf(_ra, 16, "%f", ra); snprintf(_dec, 16, "%f", dec); bool status = RunScript(SCRIPT_SYNC, _ra, _dec, nullptr); if (status) { LOG_INFO("Sync succesfully executed"); } return status; } bool ScopeScript::Park() { bool status = RunScript(SCRIPT_PARK, nullptr); if (!status) { LOG_ERROR("Failed to park"); } return status; } bool ScopeScript::UnPark() { bool status = RunScript(SCRIPT_UNPARK, nullptr); if (!status) { LOG_ERROR("Failed to unpark"); } return status; } bool ScopeScript::MoveNS(INDI_DIR_NS dir, TelescopeMotionCommand command) { char _rate[] = { (char)('0' + IUFindOnSwitchIndex(&SlewRateSP)), 0 }; bool status = RunScript(command == MOTION_STOP ? SCRIPT_ABORT : dir == DIRECTION_NORTH ? SCRIPT_MOVE_NORTH : SCRIPT_MOVE_SOUTH, _rate, nullptr, nullptr); return status; } bool ScopeScript::MoveWE(INDI_DIR_WE dir, TelescopeMotionCommand command) { char _rate[] = { (char)('0' + IUFindOnSwitchIndex(&SlewRateSP)), 0 }; bool status = RunScript(command == MOTION_STOP ? SCRIPT_ABORT : dir == DIRECTION_WEST ? SCRIPT_MOVE_WEST : SCRIPT_MOVE_EAST, _rate, nullptr, nullptr); return status; } bool ScopeScript::Abort() { bool status = RunScript(SCRIPT_ABORT, nullptr); if (status) { LOG_INFO("Successfully aborted"); } else { LOG_ERROR("Failed to abort"); } return status; } libindi/drivers/telescope/lx200ap_experimental.h0000664000175000017500000001107413263645557021252 0ustar jasemjasem/* Astro-Physics INDI driver Copyright (C) 2014 Jasem Mutlaq Based on INDI Astrophysics Driver by Markus Wildi 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 */ #pragma once #include "lx200generic.h" class LX200AstroPhysicsExperimental : public LX200Generic { public: LX200AstroPhysicsExperimental(); ~LX200AstroPhysicsExperimental() {} typedef enum { MCV_E, MCV_F, MCV_G, MCV_H, MCV_I, MCV_J, MCV_K_UNUSED, MCV_L, MCV_M, MCV_N, MCV_O, MCV_P, MCV_Q, MCV_R, MCV_S, MCV_T, MCV_U, MCV_V, MCV_UNKNOWN} ControllerVersion; typedef enum { GTOCP1=1, GTOCP2, GTOCP3, GTOCP4, GTOCP_UNKNOWN} ServoVersion; typedef enum { PARK_LAST=0, PARK_CUSTOM=0, PARK_PARK1=1, PARK_PARK2=2, PARK_PARK3=3, PARK_PARK4=4} ParkPosition; virtual bool ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) override; virtual bool ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) override; virtual void ISGetProperties(const char *dev) override; protected: virtual const char *getDefaultName() override; virtual bool initProperties() override; virtual bool updateProperties() override; virtual bool ReadScopeStatus() override; virtual bool Handshake() override; virtual bool Disconnect() override; // Parking virtual bool SetCurrentPark() override; virtual bool SetDefaultPark() override; virtual bool Park() override; virtual bool UnPark() override; virtual bool Sync(double ra, double dec) override; virtual bool Goto(double, double) override; virtual bool updateTime(ln_date *utc, double utc_offset) override; virtual bool updateLocation(double latitude, double longitude, double elevation) override; virtual bool SetSlewRate(int index) override; virtual int SendPulseCmd(int direction, int duration_msec) override; virtual bool getUTFOffset(double *offset) override; // Tracking virtual bool SetTrackMode(uint8_t mode) override; virtual bool SetTrackEnabled(bool enabled) override; virtual bool SetTrackRate(double raRate, double deRate) override; // NSWE Motion Commands virtual bool MoveNS(INDI_DIR_NS dir, TelescopeMotionCommand command) override; virtual bool MoveWE(INDI_DIR_WE dir, TelescopeMotionCommand command) override; virtual bool saveConfigItems(FILE *fp) override; virtual void debugTriggered(bool enable) override; void handleGTOCP2MotionBug(); INumber HourangleCoordsN[2]; INumberVectorProperty HourangleCoordsNP; INumber HorizontalCoordsN[2]; INumberVectorProperty HorizontalCoordsNP; ISwitch APSlewSpeedS[3]; ISwitchVectorProperty APSlewSpeedSP; ISwitch SwapS[2]; ISwitchVectorProperty SwapSP; ISwitch SyncCMRS[2]; ISwitchVectorProperty SyncCMRSP; enum { USE_REGULAR_SYNC, USE_CMR_SYNC }; ISwitch APGuideSpeedS[3]; ISwitchVectorProperty APGuideSpeedSP; ISwitch UnparkFromS[5]; ISwitchVectorProperty UnparkFromSP; ISwitch ParkToS[5]; ISwitchVectorProperty ParkToSP; INumberVectorProperty MeridianDelayNP; INumber MeridianDelayN[1]; IText VersionT[1] {}; ITextVectorProperty VersionInfo; private: bool initMount(); // Side of pier void syncSideOfPier(); bool IsMountInitialized(bool *initialized); bool IsMountParked(bool *isParked); bool getMountStatus(bool *isParked); bool getFirmwareVersion(void); bool calcParkPosition(ParkPosition pos, double *parkAlt, double *parkAz); void disclaimerMessage(void); bool timeUpdated=false, locationUpdated=false; ControllerVersion firmwareVersion = MCV_UNKNOWN; ServoVersion servoType = GTOCP_UNKNOWN; double currentAlt=0, currentAz=0; double lastRA=0, lastDE=0; double lastAZ=0, lastAL=0; bool motionCommanded=false; bool mountInitialized=false; bool mountParked=false; }; libindi/drivers/telescope/dsc.h0000664000175000017500000000663013263645557016062 0ustar jasemjasem/******************************************************************************* Copyright(c) 2017 Jasem Mutlaq. All rights reserved. Generic Digital Setting Circles Driver It just gets the encoder positoin and outputs current coordinates. Calibratoin and syncing not supported yet. This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2 as published by the Free Software Foundation. . 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 Library General Public License for more details. . You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. *******************************************************************************/ #pragma once #include "inditelescope.h" #include "alignment/AlignmentSubsystemForDrivers.h" #include class DSC : public INDI::Telescope, INDI::AlignmentSubsystem::AlignmentSubsystemForDrivers { public: DSC(); virtual ~DSC() = default; virtual bool ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n) override; virtual bool ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) override; virtual bool ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) override; protected: virtual const char *getDefaultName() override; virtual bool Handshake() override; virtual bool initProperties() override; virtual bool updateProperties() override; virtual bool saveConfigItems(FILE *fp) override; virtual bool ReadScopeStatus() override; virtual bool Sync(double ra, double dec) override; virtual bool updateLocation(double latitude, double longitude, double elevation) override; virtual void simulationTriggered(bool enable) override; private: ln_equ_posn TelescopeEquatorialToSky(); ln_equ_posn TelescopeHorizontalToSky(); INumber EncoderN[4]; INumberVectorProperty EncoderNP; enum { AXIS1_ENCODER, AXIS2_ENCODER, AXIS1_RAW_ENCODER, AXIS2_RAW_ENCODER }; INumber AxisSettingsN[4]; INumberVectorProperty AxisSettingsNP; //enum { AXIS1_TICKS, AXIS2_TICKS}; enum { AXIS1_TICKS, AXIS1_DEGREE_OFFSET, AXIS2_TICKS, AXIS2_DEGREE_OFFSET }; ISwitch AxisRangeS[2]; ISwitchVectorProperty AxisRangeSP; enum { AXIS_FULL_STEP, AXIS_HALF_STEP }; ISwitch ReverseS[2]; ISwitchVectorProperty ReverseSP; ISwitch MountTypeS[2]; ISwitchVectorProperty MountTypeSP; enum { MOUNT_EQUATORIAL, MOUNT_ALTAZ }; //INumber EncoderOffsetN[6]; //INumberVectorProperty EncoderOffsetNP; //enum { OFFSET_AXIS1_SCALE, OFFSET_AXIS1_OFFSET, AXIS1_DEGREE_OFFSET, OFFSET_AXIS2_SCALE, OFFSET_AXIS2_OFFSET, AXIS2_DEGREE_OFFSET }; // Simulation Only INumber SimEncoderN[2]; INumberVectorProperty SimEncoderNP; ln_lnlat_posn observer { 0, 0 }; ln_hrz_posn encoderHorizontalCoordinates { 0, 0 }; ln_equ_posn encoderEquatorialCoordinates { 0, 0 }; }; libindi/drivers/telescope/lx200basic.h0000664000175000017500000000353313263645557017157 0ustar jasemjasem/* LX200 Basic Driver Copyright (C) 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 */ #pragma once #include "inditelescope.h" class LX200Basic : public INDI::Telescope { public: LX200Basic(); ~LX200Basic() = default; virtual const char *getDefaultName() override; virtual bool Handshake() override; virtual bool ReadScopeStatus() override; virtual void ISGetProperties(const char *dev) override; virtual bool initProperties() override; virtual bool updateProperties() override; virtual bool ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) override; protected: virtual bool Abort() override; virtual bool Goto(double, double) override; virtual bool Sync(double ra, double dec) override; virtual void debugTriggered(bool enable) override; void getBasicData(); private: bool isSlewComplete(); void slewError(int slewCode); void mountSim(); INumber SlewAccuracyN[2]; INumberVectorProperty SlewAccuracyNP; double targetRA, targetDEC; double currentRA, currentDEC; unsigned int DBG_SCOPE; }; libindi/drivers/telescope/pmc8.h0000664000175000017500000000746413263645557016166 0ustar jasemjasem/* INDI Explore Scientific PMC8 driver Copyright (C) 2017 Michael Fulbright Based on IEQPro driver. 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 */ #pragma once #include "pmc8driver.h" #include "indiguiderinterface.h" #include "inditelescope.h" class PMC8 : public INDI::Telescope, public INDI::GuiderInterface { public: PMC8(); ~PMC8(); virtual bool ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) override; virtual bool ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) override; protected: virtual const char *getDefaultName() override; virtual bool Handshake() override; virtual bool initProperties() override; virtual bool updateProperties() override; virtual bool ReadScopeStatus() override; virtual bool MoveNS(INDI_DIR_NS dir, TelescopeMotionCommand command) override; virtual bool MoveWE(INDI_DIR_WE dir, TelescopeMotionCommand command) override; virtual bool saveConfigItems(FILE *fp) override; virtual bool Park() override; virtual bool UnPark() override; virtual bool Sync(double ra, double dec) override; virtual bool Goto(double, double) override; virtual bool Abort() override; virtual bool updateTime(ln_date *utc, double utc_offset) override; virtual bool updateLocation(double latitude, double longitude, double elevation) override; virtual void debugTriggered(bool enable) override; virtual void simulationTriggered(bool enable) override; // Parking virtual bool SetCurrentPark() override; virtual bool SetDefaultPark() override; // Track Mode virtual bool SetTrackMode(uint8_t mode) override; // Track Rate virtual bool SetTrackRate(double raRate, double deRate) override; // Track On/Off virtual bool SetTrackEnabled(bool enabled) override; // Slew Rate virtual bool SetSlewRate(int index) override; // Sim void mountSim(); // Guide virtual IPState GuideNorth(float ms) override; virtual IPState GuideSouth(float ms) override; virtual IPState GuideEast(float ms) override; virtual IPState GuideWest(float ms) override; // Pulse Guide static void guideTimeoutHelperN(void *p); static void guideTimeoutHelperS(void *p); static void guideTimeoutHelperE(void *p); static void guideTimeoutHelperW(void *p); void guideTimeout(PMC8_DIRECTION calldir); //GUIDE variables. int GuideNSTID; int GuideWETID; private: /** * @brief getStartupData Get initial mount info on startup. */ void getStartupData(); /* Firmware */ IText FirmwareT[1] {}; ITextVectorProperty FirmwareTP; /* Tracking Mode */ //ISwitchVectorProperty TrackModeSP; //ISwitch TrackModeS[4]; /* Custom Tracking Rate */ //INumber CustomTrackRateN[1]; //INumberVectorProperty CustomTrackRateNP; /* Guide Rate */ INumber GuideRateN[1]; INumberVectorProperty GuideRateNP; unsigned int DBG_SCOPE; double currentRA, currentDEC; double targetRA, targetDEC; //PMC8Info scopeInfo; FirmwareInfo firmwareInfo; }; libindi/drivers/telescope/ioptronHC8406.h0000664000175000017500000000702113263645557017533 0ustar jasemjasem/* ioptronHC8406 INDI driver Copyright (C) 2017 Nacho Mas. Base on GotoNova driver by Jasem Mutlaq 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 */ #pragma once #include "lx200generic.h" class ioptronHC8406 : public LX200Generic { public: ioptronHC8406(); ~ioptronHC8406() {} virtual bool updateProperties() override; virtual bool initProperties() override; virtual bool ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) override; protected: virtual const char *getDefaultName() override; virtual void getBasicData() override; virtual bool checkConnection() override; virtual bool isSlewComplete() override; virtual bool ReadScopeStatus() override; virtual bool SetTrackEnabled(bool enabled) override; virtual bool SetTrackMode(uint8_t mode) override; virtual bool Goto(double, double) override; virtual bool Sync(double ra, double dec) override; virtual bool updateTime(ln_date *utc, double utc_offset) override; virtual bool updateLocation(double latitude, double longitude, double elevation) override; virtual int SendPulseCmd(int direction, int duration_msec) override; virtual bool saveConfigItems(FILE *fp) override; virtual bool Park() override; virtual bool UnPark() override; void sendScopeTime() ; private: int setioptronHC8406StandardProcedure(int fd, const char *data); int ioptronHC8406SyncCMR(char *matchedObject); // Mount Initialization. void ioptronHC8406Init(); // Settings int setioptronHC8406Latitude(double Lat); int setioptronHC8406Longitude(double Long); int setioptronHC8406UTCOffset(double hours); int getCommandString(int fd, char *data, const char *cmd); int setCalenderDate(int fd, int dd, int mm, int yy); // Motion int slewioptronHC8406(); // Track Mode int setioptronHC8406TrackMode(int mode); //Set move rates int setMoveRate(int rate,int move_type); // Guide Rate int setioptronHC8406GuideRate(int rate); // Center Rate int setioptronHC8406CenterRate(int rate); // Slew Rate int setioptronHC8406SlewRate(int rate); // Pier Side void syncSideOfPier(); // Simulation void mountSim(); // Sync type ISwitch SyncCMRS[2]; ISwitchVectorProperty SyncCMRSP; enum { USE_REGULAR_SYNC, USE_CMR_SYNC }; //Cursor move speed ISwitch CursorMoveSpeedS[3]; ISwitchVectorProperty CursorMoveSpeedSP; enum { USE_GUIDE_SPEED, USE_CENTERING_SPEED, USE_SLEW_SPEED }; int setioptronHC8406CursorMoveSpeed(int type); /* Guide Rate */ ISwitch GuideRateS[3]; ISwitchVectorProperty GuideRateSP; /* Center Rate */ ISwitch CenterRateS[4]; ISwitchVectorProperty CenterRateSP; /* Center Rate */ ISwitch SlewRateS[3]; ISwitchVectorProperty SlewRateSP; }; libindi/drivers/telescope/ieqprodriver.cpp0000664000175000017500000015614013263645557020361 0ustar jasemjasem/* IEQ Pro driver Copyright (C) 2015 Jasem Mutlaq 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 */ #include "ieqprodriver.h" #include "indicom.h" #include "indilogger.h" #include #include #include #include #include #define IEQPRO_TIMEOUT 5 /* FD timeout in seconds */ bool ieqpro_debug = false; bool ieqpro_simulation = false; char ieqpro_device[MAXINDIDEVICE] = "iEQ"; IEQInfo simInfo; struct { double ra; double dec; double guide_rate; } simData; void set_ieqpro_debug(bool enable) { ieqpro_debug = enable; } void set_ieqpro_simulation(bool enable) { ieqpro_simulation = enable; if (enable) simData.guide_rate = 0.5; } void set_ieqpro_device(const char *name) { strncpy(ieqpro_device, name, MAXINDIDEVICE); } void set_sim_gps_status(IEQ_GPS_STATUS value) { simInfo.gpsStatus = value; } void set_sim_system_status(IEQ_SYSTEM_STATUS value) { simInfo.systemStatus = value; } void set_sim_track_rate(IEQ_TRACK_RATE value) { simInfo.trackRate = value; } void set_sim_slew_rate(IEQ_SLEW_RATE value) { simInfo.slewRate = value; } void set_sim_time_source(IEQ_TIME_SOURCE value) { simInfo.timeSource = value; } void set_sim_hemisphere(IEQ_HEMISPHERE value) { simInfo.hemisphere = value; } void set_sim_ra(double ra) { simData.ra = ra; } void set_sim_dec(double dec) { simData.dec = dec; } void set_sim_guide_rate(double rate) { simData.guide_rate = rate; } bool check_ieqpro_connection(int fd) { char initCMD[] = ":V#"; int errcode = 0; char errmsg[MAXRBUF]; char response[8]; int nbytes_read = 0; int nbytes_written = 0; DEBUGDEVICE(ieqpro_device, INDI::Logger::DBG_DEBUG, "Initializing IOptron using :V# CMD..."); for (int i = 0; i < 2; i++) { if (ieqpro_simulation) { strcpy(response, "V1.00#"); nbytes_read = strlen(response); } else { tcflush(fd, TCIFLUSH); if ((errcode = tty_write(fd, initCMD, 3, &nbytes_written)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); DEBUGFDEVICE(ieqpro_device, INDI::Logger::DBG_ERROR, "%s", errmsg); usleep(50000); continue; } if ((errcode = tty_read_section(fd, response, '#', IEQPRO_TIMEOUT, &nbytes_read))) { tty_error_msg(errcode, errmsg, MAXRBUF); DEBUGFDEVICE(ieqpro_device, INDI::Logger::DBG_ERROR, "%s", errmsg); usleep(50000); continue; } } if (nbytes_read > 0) { response[nbytes_read] = '\0'; DEBUGFDEVICE(ieqpro_device, INDI::Logger::DBG_DEBUG, "RES (%s)", response); if (!strcmp(response, "V1.00#")) return true; } usleep(50000); } return false; } bool get_ieqpro_status(int fd, IEQInfo *info) { char cmd[] = ":GAS#"; int errcode = 0; char errmsg[MAXRBUF]; char response[8]; int nbytes_read = 0; int nbytes_written = 0; DEBUGFDEVICE(ieqpro_device, INDI::Logger::DBG_EXTRA_1, "CMD (%s)", cmd); if (ieqpro_simulation) { snprintf(response, 8, "%d%d%d%d%d%d#", simInfo.gpsStatus, simInfo.systemStatus, simInfo.trackRate, simInfo.slewRate + 1, simInfo.timeSource, simInfo.hemisphere); nbytes_read = strlen(response); } else { tcflush(fd, TCIFLUSH); if ((errcode = tty_write(fd, cmd, strlen(cmd), &nbytes_written)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); DEBUGFDEVICE(ieqpro_device, INDI::Logger::DBG_ERROR, "%s", errmsg); return false; } if ((errcode = tty_read_section(fd, response, '#', IEQPRO_TIMEOUT, &nbytes_read))) { tty_error_msg(errcode, errmsg, MAXRBUF); DEBUGFDEVICE(ieqpro_device, INDI::Logger::DBG_ERROR, "%s", errmsg); return false; } } if (nbytes_read > 0) { response[nbytes_read] = '\0'; DEBUGFDEVICE(ieqpro_device, INDI::Logger::DBG_EXTRA_1, "RES (%s)", response); if (nbytes_read == 7) { info->gpsStatus = (IEQ_GPS_STATUS)(response[0] - '0'); info->systemStatus = (IEQ_SYSTEM_STATUS)(response[1] - '0'); info->trackRate = (IEQ_TRACK_RATE)(response[2] - '0'); info->slewRate = (IEQ_SLEW_RATE)(response[3] - '0' - 1); info->timeSource = (IEQ_TIME_SOURCE)(response[4] - '0'); info->hemisphere = (IEQ_HEMISPHERE)(response[5] - '0'); tcflush(fd, TCIFLUSH); return true; } } DEBUGFDEVICE(ieqpro_device, INDI::Logger::DBG_ERROR, "Only received #%d bytes, expected 7.", nbytes_read); return false; } bool get_ieqpro_firmware(int fd, FirmwareInfo *info) { bool rc = false; rc = get_ieqpro_model(fd, info); if (!rc) return rc; rc = get_ieqpro_main_firmware(fd, info); if (!rc) return rc; rc = get_ieqpro_radec_firmware(fd, info); return rc; } bool get_ieqpro_model(int fd, FirmwareInfo *info) { char cmd[] = ":MountInfo#"; int errcode = 0; char errmsg[MAXRBUF]; char response[16]; int nbytes_read = 0; int nbytes_written = 0; DEBUGFDEVICE(ieqpro_device, INDI::Logger::DBG_DEBUG, "CMD (%s)", cmd); if (ieqpro_simulation) { strcpy(response, "0045"); nbytes_read = strlen(response); } else { tcflush(fd, TCIFLUSH); if ((errcode = tty_write(fd, cmd, strlen(cmd), &nbytes_written)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); DEBUGFDEVICE(ieqpro_device, INDI::Logger::DBG_ERROR, "%s", errmsg); return false; } if ((errcode = tty_read(fd, response, 4, IEQPRO_TIMEOUT, &nbytes_read))) { tty_error_msg(errcode, errmsg, MAXRBUF); DEBUGFDEVICE(ieqpro_device, INDI::Logger::DBG_ERROR, "%s", errmsg); return false; } } if (nbytes_read > 0) { response[nbytes_read] = '\0'; DEBUGFDEVICE(ieqpro_device, INDI::Logger::DBG_DEBUG, "RES (%s)", response); if (nbytes_read == 4) { if (!strcmp(response, "0060")) info->Model = "CEM60"; else if (!strcmp(response, "0061")) info->Model = "CEM60-EC"; else if (!strcmp(response, "0045")) info->Model = "iEQ45 Pro"; else if (!strcmp(response, "0046")) info->Model = "iEQ45 Pro AA"; else if (!strcmp(response, "0025")) info->Model = "CEM25"; else info->Model = "Unknown"; tcflush(fd, TCIFLUSH); return true; } } DEBUGFDEVICE(ieqpro_device, INDI::Logger::DBG_ERROR, "Only received #%d bytes, expected 4.", nbytes_read); return false; } bool get_ieqpro_main_firmware(int fd, FirmwareInfo *info) { char cmd[] = ":FW1#"; int errcode = 0; char errmsg[MAXRBUF]; char response[16]; int nbytes_read = 0; int nbytes_written = 0; DEBUGFDEVICE(ieqpro_device, INDI::Logger::DBG_DEBUG, "CMD (%s)", cmd); if (ieqpro_simulation) { strcpy(response, "150324150101#"); nbytes_read = strlen(response); } else { tcflush(fd, TCIFLUSH); if ((errcode = tty_write(fd, cmd, strlen(cmd), &nbytes_written)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); DEBUGFDEVICE(ieqpro_device, INDI::Logger::DBG_ERROR, "%s", errmsg); return false; } if ((errcode = tty_read_section(fd, response, '#', IEQPRO_TIMEOUT, &nbytes_read))) { tty_error_msg(errcode, errmsg, MAXRBUF); DEBUGFDEVICE(ieqpro_device, INDI::Logger::DBG_ERROR, "%s", errmsg); return false; } } if (nbytes_read > 0) { response[nbytes_read] = '\0'; DEBUGFDEVICE(ieqpro_device, INDI::Logger::DBG_DEBUG, "RES (%s)", response); if (nbytes_read == 13) { char board[8] = {0}, controller[8] = {0}; strncpy(board, response, 6); strncpy(controller, response + 6, 6); info->MainBoardFirmware.assign(board, 6); info->ControllerFirmware.assign(controller, 6); tcflush(fd, TCIFLUSH); return true; } } DEBUGFDEVICE(ieqpro_device, INDI::Logger::DBG_ERROR, "Only received #%d bytes, expected 13.", nbytes_read); return false; } bool get_ieqpro_radec_firmware(int fd, FirmwareInfo *info) { char cmd[] = ":FW2#"; int errcode = 0; char errmsg[MAXRBUF]; char response[16]; int nbytes_read = 0; int nbytes_written = 0; DEBUGFDEVICE(ieqpro_device, INDI::Logger::DBG_DEBUG, "CMD (%s)", cmd); if (ieqpro_simulation) { strcpy(response, "140324140101#"); nbytes_read = strlen(response); } else { tcflush(fd, TCIFLUSH); if ((errcode = tty_write(fd, cmd, strlen(cmd), &nbytes_written)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); DEBUGFDEVICE(ieqpro_device, INDI::Logger::DBG_ERROR, "%s", errmsg); return false; } if ((errcode = tty_read_section(fd, response, '#', IEQPRO_TIMEOUT, &nbytes_read))) { tty_error_msg(errcode, errmsg, MAXRBUF); DEBUGFDEVICE(ieqpro_device, INDI::Logger::DBG_ERROR, "%s", errmsg); return false; } } if (nbytes_read > 0) { response[nbytes_read] = '\0'; DEBUGFDEVICE(ieqpro_device, INDI::Logger::DBG_DEBUG, "RES (%s)", response); if (nbytes_read == 13) { char ra[8] = {0}, dec[8] = {0}; strncpy(ra, response, 6); strncpy(dec, response + 6, 6); info->RAFirmware.assign(ra, 6); info->DEFirmware.assign(dec, 6); tcflush(fd, TCIFLUSH); return true; } } DEBUGFDEVICE(ieqpro_device, INDI::Logger::DBG_ERROR, "Only received #%d bytes, expected 13.", nbytes_read); return false; } bool start_ieqpro_motion(int fd, IEQ_DIRECTION dir) { char cmd[16]; int errcode = 0; char errmsg[MAXRBUF]; int nbytes_written = 0; switch (dir) { case IEQ_N: strcpy(cmd, ":mn#"); break; case IEQ_S: strcpy(cmd, ":ms#"); break; case IEQ_W: strcpy(cmd, ":mw#"); break; case IEQ_E: strcpy(cmd, ":me#"); break; } DEBUGFDEVICE(ieqpro_device, INDI::Logger::DBG_DEBUG, "CMD (%s)", cmd); if (ieqpro_simulation) return true; tcflush(fd, TCIFLUSH); if ((errcode = tty_write(fd, cmd, strlen(cmd), &nbytes_written)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); DEBUGFDEVICE(ieqpro_device, INDI::Logger::DBG_ERROR, "%s", errmsg); return false; } tcflush(fd, TCIFLUSH); return true; } bool stop_ieqpro_motion(int fd, IEQ_DIRECTION dir) { char cmd[16]; int errcode = 0; char errmsg[MAXRBUF]; char response[8]; int nbytes_read = 0; int nbytes_written = 0; switch (dir) { case IEQ_N: case IEQ_S: strcpy(cmd, ":qD#"); break; case IEQ_W: case IEQ_E: strcpy(cmd, ":qR#"); break; } DEBUGFDEVICE(ieqpro_device, INDI::Logger::DBG_DEBUG, "CMD (%s)", cmd); if (ieqpro_simulation) { strcpy(response, "1"); nbytes_read = strlen(response); } else { tcflush(fd, TCIFLUSH); if ((errcode = tty_write(fd, cmd, strlen(cmd), &nbytes_written)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); DEBUGFDEVICE(ieqpro_device, INDI::Logger::DBG_ERROR, "%s", errmsg); return false; } if ((errcode = tty_read(fd, response, 1, IEQPRO_TIMEOUT, &nbytes_read))) { tty_error_msg(errcode, errmsg, MAXRBUF); DEBUGFDEVICE(ieqpro_device, INDI::Logger::DBG_ERROR, "%s", errmsg); return false; } } if (nbytes_read > 0) { response[nbytes_read] = '\0'; DEBUGFDEVICE(ieqpro_device, INDI::Logger::DBG_DEBUG, "RES (%s)", response); tcflush(fd, TCIFLUSH); return true; } DEBUGFDEVICE(ieqpro_device, INDI::Logger::DBG_ERROR, "Only received #%d bytes, expected 1.", nbytes_read); return false; } bool find_ieqpro_home(int fd) { char cmd[] = ":MSH#"; int errcode = 0; char errmsg[MAXRBUF]; char response[8]; int nbytes_read = 0; int nbytes_written = 0; DEBUGFDEVICE(ieqpro_device, INDI::Logger::DBG_DEBUG, "CMD (%s)", cmd); if (ieqpro_simulation) { strcpy(response, "1"); nbytes_read = strlen(response); } else { tcflush(fd, TCIFLUSH); if ((errcode = tty_write(fd, cmd, strlen(cmd), &nbytes_written)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); DEBUGFDEVICE(ieqpro_device, INDI::Logger::DBG_ERROR, "%s", errmsg); return false; } if ((errcode = tty_read(fd, response, 1, IEQPRO_TIMEOUT, &nbytes_read))) { tty_error_msg(errcode, errmsg, MAXRBUF); DEBUGFDEVICE(ieqpro_device, INDI::Logger::DBG_ERROR, "%s", errmsg); return false; } } if (nbytes_read > 0) { response[nbytes_read] = '\0'; DEBUGFDEVICE(ieqpro_device, INDI::Logger::DBG_DEBUG, "RES (%s)", response); tcflush(fd, TCIFLUSH); return true; } DEBUGFDEVICE(ieqpro_device, INDI::Logger::DBG_ERROR, "Only received #%d bytes, expected 1.", nbytes_read); return false; } bool goto_ieqpro_home(int fd) { char cmd[] = ":MH#"; int errcode = 0; char errmsg[MAXRBUF]; char response[8]; int nbytes_read = 0; int nbytes_written = 0; DEBUGFDEVICE(ieqpro_device, INDI::Logger::DBG_DEBUG, "CMD (%s)", cmd); if (ieqpro_simulation) { strcpy(response, "1"); nbytes_read = strlen(response); } else { tcflush(fd, TCIFLUSH); if ((errcode = tty_write(fd, cmd, strlen(cmd), &nbytes_written)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); DEBUGFDEVICE(ieqpro_device, INDI::Logger::DBG_ERROR, "%s", errmsg); return false; } if ((errcode = tty_read(fd, response, 1, IEQPRO_TIMEOUT, &nbytes_read))) { tty_error_msg(errcode, errmsg, MAXRBUF); DEBUGFDEVICE(ieqpro_device, INDI::Logger::DBG_ERROR, "%s", errmsg); return false; } } if (nbytes_read > 0) { response[nbytes_read] = '\0'; DEBUGFDEVICE(ieqpro_device, INDI::Logger::DBG_DEBUG, "RES (%s)", response); tcflush(fd, TCIFLUSH); return true; } DEBUGFDEVICE(ieqpro_device, INDI::Logger::DBG_ERROR, "Only received #%d bytes, expected 1.", nbytes_read); return false; } bool set_ieqpro_current_home(int fd) { char cmd[] = ":SZP#"; int errcode = 0; char errmsg[MAXRBUF]; char response[8]; int nbytes_read = 0; int nbytes_written = 0; DEBUGFDEVICE(ieqpro_device, INDI::Logger::DBG_DEBUG, "CMD (%s)", cmd); if (ieqpro_simulation) { strcpy(response, "1"); nbytes_read = strlen(response); } else { tcflush(fd, TCIFLUSH); if ((errcode = tty_write(fd, cmd, strlen(cmd), &nbytes_written)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); DEBUGFDEVICE(ieqpro_device, INDI::Logger::DBG_ERROR, "%s", errmsg); return false; } if ((errcode = tty_read(fd, response, 1, IEQPRO_TIMEOUT, &nbytes_read))) { tty_error_msg(errcode, errmsg, MAXRBUF); DEBUGFDEVICE(ieqpro_device, INDI::Logger::DBG_ERROR, "%s", errmsg); return false; } } if (nbytes_read > 0) { response[nbytes_read] = '\0'; DEBUGFDEVICE(ieqpro_device, INDI::Logger::DBG_DEBUG, "RES (%s)", response); tcflush(fd, TCIFLUSH); return true; } DEBUGFDEVICE(ieqpro_device, INDI::Logger::DBG_ERROR, "Only received #%d bytes, expected 1.", nbytes_read); return false; } bool set_ieqpro_slew_rate(int fd, IEQ_SLEW_RATE rate) { char cmd[16]; int errcode = 0; char errmsg[MAXRBUF]; char response[8]; int nbytes_read = 0; int nbytes_written = 0; snprintf(cmd, 16, ":SR%d#", ((int)rate) + 1); DEBUGFDEVICE(ieqpro_device, INDI::Logger::DBG_DEBUG, "CMD (%s)", cmd); if (ieqpro_simulation) { simInfo.slewRate = rate; strcpy(response, "1"); nbytes_read = strlen(response); } else { tcflush(fd, TCIFLUSH); if ((errcode = tty_write(fd, cmd, strlen(cmd), &nbytes_written)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); DEBUGFDEVICE(ieqpro_device, INDI::Logger::DBG_ERROR, "%s", errmsg); return false; } if ((errcode = tty_read(fd, response, 1, IEQPRO_TIMEOUT, &nbytes_read))) { tty_error_msg(errcode, errmsg, MAXRBUF); DEBUGFDEVICE(ieqpro_device, INDI::Logger::DBG_ERROR, "%s", errmsg); return false; } } if (nbytes_read > 0) { response[nbytes_read] = '\0'; DEBUGFDEVICE(ieqpro_device, INDI::Logger::DBG_DEBUG, "RES (%s)", response); tcflush(fd, TCIFLUSH); return true; } DEBUGFDEVICE(ieqpro_device, INDI::Logger::DBG_ERROR, "Only received #%d bytes, expected 1.", nbytes_read); return false; } bool set_ieqpro_track_mode(int fd, IEQ_TRACK_RATE rate) { char cmd[16]; int errcode = 0; char errmsg[MAXRBUF]; char response[8]; int nbytes_read = 0; int nbytes_written = 0; switch (rate) { case TR_SIDEREAL: strcpy(cmd, ":RT0#"); break; case TR_LUNAR: strcpy(cmd, ":RT1#"); break; case TR_SOLAR: strcpy(cmd, ":RT2#"); break; case TR_KING: strcpy(cmd, ":RT3#"); break; case TR_CUSTOM: strcpy(cmd, ":RT4#"); break; } DEBUGFDEVICE(ieqpro_device, INDI::Logger::DBG_DEBUG, "CMD (%s)", cmd); if (ieqpro_simulation) { simInfo.trackRate = rate; strcpy(response, "1"); nbytes_read = strlen(response); } else { tcflush(fd, TCIFLUSH); if ((errcode = tty_write(fd, cmd, strlen(cmd), &nbytes_written)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); DEBUGFDEVICE(ieqpro_device, INDI::Logger::DBG_ERROR, "%s", errmsg); return false; } if ((errcode = tty_read(fd, response, 1, IEQPRO_TIMEOUT, &nbytes_read))) { tty_error_msg(errcode, errmsg, MAXRBUF); DEBUGFDEVICE(ieqpro_device, INDI::Logger::DBG_ERROR, "%s", errmsg); return false; } } if (nbytes_read > 0) { response[nbytes_read] = '\0'; DEBUGFDEVICE(ieqpro_device, INDI::Logger::DBG_DEBUG, "RES (%s)", response); tcflush(fd, TCIFLUSH); return true; } DEBUGFDEVICE(ieqpro_device, INDI::Logger::DBG_ERROR, "Only received #%d bytes, expected 1.", nbytes_read); return false; } bool set_ieqpro_custom_ra_track_rate(int fd, double rate) { char cmd[16]; char sign; int errcode = 0; char errmsg[MAXRBUF]; char response[8]; int nbytes_read = 0; int nbytes_written = 0; if (rate < 0) sign = '-'; else sign = '+'; snprintf(cmd, 16, ":RR%c%07.4f#", sign, fabs(rate)); DEBUGFDEVICE(ieqpro_device, INDI::Logger::DBG_DEBUG, "CMD (%s)", cmd); if (ieqpro_simulation) { strcpy(response, "1"); nbytes_read = strlen(response); } else { tcflush(fd, TCIFLUSH); if ((errcode = tty_write(fd, cmd, strlen(cmd), &nbytes_written)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); DEBUGFDEVICE(ieqpro_device, INDI::Logger::DBG_ERROR, "%s", errmsg); return false; } if ((errcode = tty_read(fd, response, 1, IEQPRO_TIMEOUT, &nbytes_read))) { tty_error_msg(errcode, errmsg, MAXRBUF); DEBUGFDEVICE(ieqpro_device, INDI::Logger::DBG_ERROR, "%s", errmsg); return false; } } if (nbytes_read > 0) { response[nbytes_read] = '\0'; DEBUGFDEVICE(ieqpro_device, INDI::Logger::DBG_DEBUG, "RES (%s)", response); tcflush(fd, TCIFLUSH); return true; } DEBUGFDEVICE(ieqpro_device, INDI::Logger::DBG_ERROR, "Only received #%d bytes, expected 1.", nbytes_read); return false; } bool set_ieqpro_custom_de_track_rate(int fd, double rate) { char cmd[16]; char sign; int errcode = 0; char errmsg[MAXRBUF]; char response[8]; int nbytes_read = 0; int nbytes_written = 0; if (rate < 0) sign = '-'; else sign = '+'; snprintf(cmd, 16, ":RD%c%07.4f#", sign, fabs(rate)); DEBUGFDEVICE(ieqpro_device, INDI::Logger::DBG_DEBUG, "CMD (%s)", cmd); if (ieqpro_simulation) { strcpy(response, "1"); nbytes_read = strlen(response); } else { tcflush(fd, TCIFLUSH); if ((errcode = tty_write(fd, cmd, strlen(cmd), &nbytes_written)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); DEBUGFDEVICE(ieqpro_device, INDI::Logger::DBG_ERROR, "%s", errmsg); return false; } if ((errcode = tty_read(fd, response, 1, IEQPRO_TIMEOUT, &nbytes_read))) { tty_error_msg(errcode, errmsg, MAXRBUF); DEBUGFDEVICE(ieqpro_device, INDI::Logger::DBG_ERROR, "%s", errmsg); return false; } } if (nbytes_read > 0) { response[nbytes_read] = '\0'; DEBUGFDEVICE(ieqpro_device, INDI::Logger::DBG_DEBUG, "RES (%s)", response); tcflush(fd, TCIFLUSH); return true; } DEBUGFDEVICE(ieqpro_device, INDI::Logger::DBG_ERROR, "Only received #%d bytes, expected 1.", nbytes_read); return false; } bool set_ieqpro_guide_rate(int fd, double rate) { char cmd[16]; int errcode = 0; char errmsg[MAXRBUF]; char response[8]; int nbytes_read = 0; int nbytes_written = 0; int num = rate * 100; snprintf(cmd, 16, ":RG%03d#", num); DEBUGFDEVICE(ieqpro_device, INDI::Logger::DBG_DEBUG, "CMD (%s)", cmd); if (ieqpro_simulation) { simData.guide_rate = rate; strcpy(response, "1"); nbytes_read = strlen(response); } else { tcflush(fd, TCIFLUSH); if ((errcode = tty_write(fd, cmd, strlen(cmd), &nbytes_written)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); DEBUGFDEVICE(ieqpro_device, INDI::Logger::DBG_ERROR, "%s", errmsg); return false; } if ((errcode = tty_read(fd, response, 1, IEQPRO_TIMEOUT, &nbytes_read))) { tty_error_msg(errcode, errmsg, MAXRBUF); DEBUGFDEVICE(ieqpro_device, INDI::Logger::DBG_ERROR, "%s", errmsg); return false; } } if (nbytes_read > 0) { response[nbytes_read] = '\0'; DEBUGFDEVICE(ieqpro_device, INDI::Logger::DBG_DEBUG, "RES (%s)", response); tcflush(fd, TCIFLUSH); return true; } DEBUGFDEVICE(ieqpro_device, INDI::Logger::DBG_ERROR, "Only received #%d bytes, expected 1.", nbytes_read); return false; } bool get_ieqpro_guide_rate(int fd, double *rate) { char cmd[] = ":AG#"; int errcode = 0; char errmsg[MAXRBUF]; char response[8]; int nbytes_read = 0; int nbytes_written = 0; DEBUGFDEVICE(ieqpro_device, INDI::Logger::DBG_DEBUG, "CMD (%s)", cmd); if (ieqpro_simulation) { snprintf(response, 8, "%3d#", (int)(simData.guide_rate * 100)); nbytes_read = strlen(response); } else { tcflush(fd, TCIFLUSH); if ((errcode = tty_write(fd, cmd, strlen(cmd), &nbytes_written)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); DEBUGFDEVICE(ieqpro_device, INDI::Logger::DBG_ERROR, "%s", errmsg); return false; } if ((errcode = tty_read_section(fd, response, '#', IEQPRO_TIMEOUT, &nbytes_read))) { tty_error_msg(errcode, errmsg, MAXRBUF); DEBUGFDEVICE(ieqpro_device, INDI::Logger::DBG_ERROR, "%s", errmsg); return false; } } if (nbytes_read > 0) { response[nbytes_read-1] = '\0'; DEBUGFDEVICE(ieqpro_device, INDI::Logger::DBG_DEBUG, "RES (%s)", response); int rate_num; if (sscanf(response, "%d", &rate_num) > 0) { *rate = rate_num / 100.0; tcflush(fd, TCIFLUSH); return true; } else { DEBUGFDEVICE(ieqpro_device, INDI::Logger::DBG_ERROR, "Error: Malformed result (%s).", response); return false; } } DEBUGFDEVICE(ieqpro_device, INDI::Logger::DBG_ERROR, "Only received #%d bytes, expected 1.", nbytes_read); return false; } bool start_ieqpro_guide(int fd, IEQ_DIRECTION dir, int ms) { char cmd[16]; int errcode = 0; char errmsg[MAXRBUF]; int nbytes_written = 0; char dir_c = 0; switch (dir) { case IEQ_N: dir_c = 'n'; break; case IEQ_S: dir_c = 's'; break; case IEQ_W: dir_c = 'w'; break; case IEQ_E: dir_c = 'e'; break; } snprintf(cmd, 16, ":M%c%05d#", dir_c, ms); DEBUGFDEVICE(ieqpro_device, INDI::Logger::DBG_DEBUG, "CMD (%s)", cmd); if (ieqpro_simulation) return true; else { tcflush(fd, TCIFLUSH); if ((errcode = tty_write(fd, cmd, strlen(cmd), &nbytes_written)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); DEBUGFDEVICE(ieqpro_device, INDI::Logger::DBG_ERROR, "%s", errmsg); return false; } } tcflush(fd, TCIFLUSH); return true; } bool park_ieqpro(int fd) { char cmd[] = ":MP1#"; int errcode = 0; char errmsg[MAXRBUF]; char response[8]; int nbytes_read = 0; int nbytes_written = 0; DEBUGFDEVICE(ieqpro_device, INDI::Logger::DBG_DEBUG, "CMD (%s)", cmd); if (ieqpro_simulation) { simInfo.rememberSystemStatus = simInfo.systemStatus; set_sim_system_status(ST_SLEWING); strcpy(response, "1"); nbytes_read = strlen(response); } else { tcflush(fd, TCIFLUSH); if ((errcode = tty_write(fd, cmd, strlen(cmd), &nbytes_written)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); DEBUGFDEVICE(ieqpro_device, INDI::Logger::DBG_ERROR, "%s", errmsg); return false; } if ((errcode = tty_read(fd, response, 1, IEQPRO_TIMEOUT, &nbytes_read))) { tty_error_msg(errcode, errmsg, MAXRBUF); DEBUGFDEVICE(ieqpro_device, INDI::Logger::DBG_ERROR, "%s", errmsg); return false; } } if (nbytes_read > 0) { response[nbytes_read] = '\0'; DEBUGFDEVICE(ieqpro_device, INDI::Logger::DBG_DEBUG, "RES (%s)", response); if (!strcmp(response, "1")) { tcflush(fd, TCIFLUSH); return true; } else { DEBUGDEVICE(ieqpro_device, INDI::Logger::DBG_ERROR, "Error: Requested parking position is below horizon."); return false; } } DEBUGFDEVICE(ieqpro_device, INDI::Logger::DBG_ERROR, "Only received #%d bytes, expected 1.", nbytes_read); return false; } bool unpark_ieqpro(int fd) { char cmd[] = ":MP0#"; int errcode = 0; char errmsg[MAXRBUF]; char response[8]; int nbytes_read = 0; int nbytes_written = 0; DEBUGFDEVICE(ieqpro_device, INDI::Logger::DBG_DEBUG, "CMD (%s)", cmd); if (ieqpro_simulation) { set_sim_system_status(ST_STOPPED); strcpy(response, "1"); nbytes_read = strlen(response); } else { tcflush(fd, TCIFLUSH); if ((errcode = tty_write(fd, cmd, strlen(cmd), &nbytes_written)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); DEBUGFDEVICE(ieqpro_device, INDI::Logger::DBG_ERROR, "%s", errmsg); return false; } if ((errcode = tty_read(fd, response, 1, IEQPRO_TIMEOUT, &nbytes_read))) { tty_error_msg(errcode, errmsg, MAXRBUF); DEBUGFDEVICE(ieqpro_device, INDI::Logger::DBG_ERROR, "%s", errmsg); return false; } } if (nbytes_read > 0) { response[nbytes_read] = '\0'; DEBUGFDEVICE(ieqpro_device, INDI::Logger::DBG_DEBUG, "RES (%s)", response); tcflush(fd, TCIFLUSH); return true; } DEBUGFDEVICE(ieqpro_device, INDI::Logger::DBG_ERROR, "Only received #%d bytes, expected 1.", nbytes_read); return false; } bool abort_ieqpro(int fd) { char cmd[] = ":Q#"; int errcode = 0; char errmsg[MAXRBUF]; char response[8]; int nbytes_read = 0; int nbytes_written = 0; DEBUGFDEVICE(ieqpro_device, INDI::Logger::DBG_DEBUG, "CMD (%s)", cmd); if (ieqpro_simulation) { if (simInfo.systemStatus == ST_SLEWING) simInfo.systemStatus = simInfo.rememberSystemStatus; strcpy(response, "1"); nbytes_read = strlen(response); } else { tcflush(fd, TCIFLUSH); if ((errcode = tty_write(fd, cmd, strlen(cmd), &nbytes_written)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); DEBUGFDEVICE(ieqpro_device, INDI::Logger::DBG_ERROR, "%s", errmsg); return false; } if ((errcode = tty_read(fd, response, 1, IEQPRO_TIMEOUT, &nbytes_read))) { tty_error_msg(errcode, errmsg, MAXRBUF); DEBUGFDEVICE(ieqpro_device, INDI::Logger::DBG_ERROR, "%s", errmsg); return false; } } if (nbytes_read > 0) { response[nbytes_read] = '\0'; DEBUGFDEVICE(ieqpro_device, INDI::Logger::DBG_DEBUG, "RES (%s)", response); tcflush(fd, TCIFLUSH); return true; } DEBUGFDEVICE(ieqpro_device, INDI::Logger::DBG_ERROR, "Only received #%d bytes, expected 1.", nbytes_read); return false; } bool slew_ieqpro(int fd) { char cmd[] = ":MS#"; int errcode = 0; char errmsg[MAXRBUF]; char response[8]; int nbytes_read = 0; int nbytes_written = 0; DEBUGFDEVICE(ieqpro_device, INDI::Logger::DBG_DEBUG, "CMD (%s)", cmd); if (ieqpro_simulation) { simInfo.rememberSystemStatus = simInfo.systemStatus; simInfo.systemStatus = ST_SLEWING; strcpy(response, "1"); nbytes_read = strlen(response); } else { tcflush(fd, TCIFLUSH); if ((errcode = tty_write(fd, cmd, strlen(cmd), &nbytes_written)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); DEBUGFDEVICE(ieqpro_device, INDI::Logger::DBG_ERROR, "%s", errmsg); return false; } if ((errcode = tty_read(fd, response, 1, IEQPRO_TIMEOUT, &nbytes_read))) { tty_error_msg(errcode, errmsg, MAXRBUF); DEBUGFDEVICE(ieqpro_device, INDI::Logger::DBG_ERROR, "%s", errmsg); return false; } } if (nbytes_read > 0) { response[nbytes_read] = '\0'; DEBUGFDEVICE(ieqpro_device, INDI::Logger::DBG_DEBUG, "RES (%s)", response); if (!strcmp(response, "1")) { tcflush(fd, TCIFLUSH); return true; } else { DEBUGDEVICE(ieqpro_device, INDI::Logger::DBG_ERROR, "Requested object is below horizon."); tcflush(fd, TCIFLUSH); return false; } } DEBUGFDEVICE(ieqpro_device, INDI::Logger::DBG_ERROR, "Only received #%d bytes, expected 1.", nbytes_read); return false; } bool sync_ieqpro(int fd) { char cmd[] = ":CM#"; int errcode = 0; char errmsg[MAXRBUF]; char response[8]; int nbytes_read = 0; int nbytes_written = 0; DEBUGFDEVICE(ieqpro_device, INDI::Logger::DBG_DEBUG, "CMD (%s)", cmd); if (ieqpro_simulation) { strcpy(response, "1"); nbytes_read = strlen(response); } else { tcflush(fd, TCIFLUSH); if ((errcode = tty_write(fd, cmd, strlen(cmd), &nbytes_written)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); DEBUGFDEVICE(ieqpro_device, INDI::Logger::DBG_ERROR, "%s", errmsg); return false; } if ((errcode = tty_read(fd, response, 1, IEQPRO_TIMEOUT, &nbytes_read))) { tty_error_msg(errcode, errmsg, MAXRBUF); DEBUGFDEVICE(ieqpro_device, INDI::Logger::DBG_ERROR, "%s", errmsg); return false; } } if (nbytes_read > 0) { response[nbytes_read] = '\0'; DEBUGFDEVICE(ieqpro_device, INDI::Logger::DBG_DEBUG, "RES (%s)", response); tcflush(fd, TCIFLUSH); return true; } DEBUGFDEVICE(ieqpro_device, INDI::Logger::DBG_ERROR, "Only received #%d bytes, expected 1.", nbytes_read); return false; } bool set_ieqpro_track_enabled(int fd, bool enabled) { char cmd[32]; int errcode = 0; char errmsg[MAXRBUF]; char response[8]; int nbytes_read = 0; int nbytes_written = 0; snprintf(cmd, 32, ":ST%d#", enabled ? 1 : 0); DEBUGFDEVICE(ieqpro_device, INDI::Logger::DBG_DEBUG, "CMD (%s)", cmd); if (ieqpro_simulation) { simInfo.systemStatus = enabled ? ST_TRACKING_PEC_ON : ST_STOPPED; strcpy(response, "1"); nbytes_read = strlen(response); } else { tcflush(fd, TCIFLUSH); if ((errcode = tty_write(fd, cmd, strlen(cmd), &nbytes_written)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); DEBUGFDEVICE(ieqpro_device, INDI::Logger::DBG_ERROR, "%s", errmsg); return false; } if ((errcode = tty_read(fd, response, 1, IEQPRO_TIMEOUT, &nbytes_read))) { tty_error_msg(errcode, errmsg, MAXRBUF); DEBUGFDEVICE(ieqpro_device, INDI::Logger::DBG_ERROR, "%s", errmsg); return false; } } if (nbytes_read > 0) { response[nbytes_read] = '\0'; DEBUGFDEVICE(ieqpro_device, INDI::Logger::DBG_DEBUG, "RES (%s)", response); tcflush(fd, TCIFLUSH); return true; } DEBUGFDEVICE(ieqpro_device, INDI::Logger::DBG_ERROR, "Only received #%d bytes, expected 1.", nbytes_read); return false; } bool set_ieqpro_ra(int fd, double ra) { char cmd[32]; int errcode = 0; char errmsg[MAXRBUF]; char response[8]; int nbytes_read = 0; int nbytes_written = 0; // Send as milliseconds resolution int ieqValue = ra * 60 * 60 * 1000; snprintf(cmd, 32, ":Sr%08d#", ieqValue); DEBUGFDEVICE(ieqpro_device, INDI::Logger::DBG_DEBUG, "CMD (%s)", cmd); if (ieqpro_simulation) { simData.ra = ra; strcpy(response, "1"); nbytes_read = strlen(response); } else { tcflush(fd, TCIFLUSH); if ((errcode = tty_write(fd, cmd, strlen(cmd), &nbytes_written)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); DEBUGFDEVICE(ieqpro_device, INDI::Logger::DBG_ERROR, "%s", errmsg); return false; } if ((errcode = tty_read(fd, response, 1, IEQPRO_TIMEOUT, &nbytes_read))) { tty_error_msg(errcode, errmsg, MAXRBUF); DEBUGFDEVICE(ieqpro_device, INDI::Logger::DBG_ERROR, "%s", errmsg); return false; } } if (nbytes_read > 0) { response[nbytes_read] = '\0'; DEBUGFDEVICE(ieqpro_device, INDI::Logger::DBG_DEBUG, "RES (%s)", response); tcflush(fd, TCIFLUSH); return true; } DEBUGFDEVICE(ieqpro_device, INDI::Logger::DBG_ERROR, "Only received #%d bytes, expected 1.", nbytes_read); return false; } bool set_ieqpro_dec(int fd, double dec) { char cmd[32]; char sign; int errcode = 0; char errmsg[MAXRBUF]; char response[8]; int nbytes_read = 0; int nbytes_written = 0; if (dec >= 0) sign = '+'; else sign = '-'; // Send as 0.01 arcseconds resolution int ieqValue = fabs(dec) * 60 * 60 * 100; snprintf(cmd, 32, ":Sd%c%08d#", sign, ieqValue); DEBUGFDEVICE(ieqpro_device, INDI::Logger::DBG_DEBUG, "CMD (%s)", cmd); if (ieqpro_simulation) { simData.dec = dec; strcpy(response, "1"); nbytes_read = strlen(response); } else { tcflush(fd, TCIFLUSH); if ((errcode = tty_write(fd, cmd, strlen(cmd), &nbytes_written)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); DEBUGFDEVICE(ieqpro_device, INDI::Logger::DBG_ERROR, "%s", errmsg); return false; } if ((errcode = tty_read(fd, response, 1, IEQPRO_TIMEOUT, &nbytes_read))) { tty_error_msg(errcode, errmsg, MAXRBUF); DEBUGFDEVICE(ieqpro_device, INDI::Logger::DBG_ERROR, "%s", errmsg); return false; } } if (nbytes_read > 0) { response[nbytes_read] = '\0'; DEBUGFDEVICE(ieqpro_device, INDI::Logger::DBG_DEBUG, "RES (%s)", response); tcflush(fd, TCIFLUSH); return true; } DEBUGFDEVICE(ieqpro_device, INDI::Logger::DBG_ERROR, "Only received #%d bytes, expected 1.", nbytes_read); return false; } bool set_ieqpro_longitude(int fd, double longitude) { char cmd[16]; char sign; int errcode = 0; char errmsg[MAXRBUF]; char response[8]; int nbytes_read = 0; int nbytes_written = 0; if (longitude >= 0) sign = '+'; else sign = '-'; int longitude_arcsecs = fabs(longitude) * 60 * 60; snprintf(cmd, 16, ":Sg%c%06d#", sign, longitude_arcsecs); DEBUGFDEVICE(ieqpro_device, INDI::Logger::DBG_DEBUG, "CMD (%s)", cmd); if (ieqpro_simulation) { strcpy(response, "1"); nbytes_read = strlen(response); } else { tcflush(fd, TCIFLUSH); if ((errcode = tty_write(fd, cmd, strlen(cmd), &nbytes_written)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); DEBUGFDEVICE(ieqpro_device, INDI::Logger::DBG_ERROR, "%s", errmsg); return false; } if ((errcode = tty_read(fd, response, 1, IEQPRO_TIMEOUT, &nbytes_read))) { tty_error_msg(errcode, errmsg, MAXRBUF); DEBUGFDEVICE(ieqpro_device, INDI::Logger::DBG_ERROR, "%s", errmsg); return false; } } if (nbytes_read > 0) { response[nbytes_read] = '\0'; DEBUGFDEVICE(ieqpro_device, INDI::Logger::DBG_DEBUG, "RES (%s)", response); tcflush(fd, TCIFLUSH); return true; } DEBUGFDEVICE(ieqpro_device, INDI::Logger::DBG_ERROR, "Only received #%d bytes, expected 1.", nbytes_read); return false; } bool set_ieqpro_latitude(int fd, double latitude) { char cmd[16]; char sign; int errcode = 0; char errmsg[MAXRBUF]; char response[8]; int nbytes_read = 0; int nbytes_written = 0; if (latitude >= 0) sign = '+'; else sign = '-'; int latitude_arcsecs = fabs(latitude) * 60 * 60; snprintf(cmd, 16, ":St%c%06d#", sign, latitude_arcsecs); DEBUGFDEVICE(ieqpro_device, INDI::Logger::DBG_DEBUG, "CMD (%s)", cmd); if (ieqpro_simulation) { strcpy(response, "1"); nbytes_read = strlen(response); } else { tcflush(fd, TCIFLUSH); if ((errcode = tty_write(fd, cmd, strlen(cmd), &nbytes_written)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); DEBUGFDEVICE(ieqpro_device, INDI::Logger::DBG_ERROR, "%s", errmsg); return false; } if ((errcode = tty_read(fd, response, 1, IEQPRO_TIMEOUT, &nbytes_read))) { tty_error_msg(errcode, errmsg, MAXRBUF); DEBUGFDEVICE(ieqpro_device, INDI::Logger::DBG_ERROR, "%s", errmsg); return false; } } if (nbytes_read > 0) { response[nbytes_read] = '\0'; DEBUGFDEVICE(ieqpro_device, INDI::Logger::DBG_DEBUG, "RES (%s)", response); tcflush(fd, TCIFLUSH); return true; } DEBUGFDEVICE(ieqpro_device, INDI::Logger::DBG_ERROR, "Only received #%d bytes, expected 1.", nbytes_read); return false; } bool get_ieqpro_longitude(int fd, double *longitude) { char cmd[16]; int errcode = 0; char errmsg[MAXRBUF]; char response[8]; int nbytes_read = 0; int nbytes_written = 0; strcpy(cmd, ":Gg#"); DEBUGFDEVICE(ieqpro_device, INDI::Logger::DBG_DEBUG, "CMD (%s)", cmd); if (ieqpro_simulation) { strcpy(response, "+172800"); nbytes_read = strlen(response); } else { tcflush(fd, TCIFLUSH); if ((errcode = tty_write(fd, cmd, strlen(cmd), &nbytes_written)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); DEBUGFDEVICE(ieqpro_device, INDI::Logger::DBG_ERROR, "%s", errmsg); return false; } if ((errcode = tty_read_section(fd, response, '#', IEQPRO_TIMEOUT, &nbytes_read))) { tty_error_msg(errcode, errmsg, MAXRBUF); DEBUGFDEVICE(ieqpro_device, INDI::Logger::DBG_ERROR, "%s", errmsg); return false; } } if (nbytes_read > 0) { response[nbytes_read-1] = '\0'; DEBUGFDEVICE(ieqpro_device, INDI::Logger::DBG_DEBUG, "RES (%s)", response); tcflush(fd, TCIFLUSH); int longitude_arcsecs = 0; if (sscanf(response, "%d", &longitude_arcsecs) > 0) { *longitude = longitude_arcsecs / 3600.0; return true; } DEBUGFDEVICE(ieqpro_device, INDI::Logger::DBG_ERROR, "Error: Malformed result (%s).", response); return false; } DEBUGFDEVICE(ieqpro_device, INDI::Logger::DBG_ERROR, "Only received #%d bytes, expected 8.", nbytes_read); return false; } bool get_ieqpro_latitude(int fd, double *latitude) { char cmd[16]; int errcode = 0; char errmsg[MAXRBUF]; char response[8]; int nbytes_read = 0; int nbytes_written = 0; strcpy(cmd, ":Gt#"); DEBUGFDEVICE(ieqpro_device, INDI::Logger::DBG_DEBUG, "CMD (%s)", cmd); if (ieqpro_simulation) { strcpy(response, "+106200"); nbytes_read = strlen(response); } else { tcflush(fd, TCIFLUSH); if ((errcode = tty_write(fd, cmd, strlen(cmd), &nbytes_written)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); DEBUGFDEVICE(ieqpro_device, INDI::Logger::DBG_ERROR, "%s", errmsg); return false; } if ((errcode = tty_read_section(fd, response, '#', IEQPRO_TIMEOUT, &nbytes_read))) { tty_error_msg(errcode, errmsg, MAXRBUF); DEBUGFDEVICE(ieqpro_device, INDI::Logger::DBG_ERROR, "%s", errmsg); return false; } } if (nbytes_read > 0) { response[nbytes_read-1] = '\0'; DEBUGFDEVICE(ieqpro_device, INDI::Logger::DBG_DEBUG, "RES (%s)", response); tcflush(fd, TCIFLUSH); int latitude_arcsecs = 0; if (sscanf(response, "%d", &latitude_arcsecs) > 0) { *latitude = latitude_arcsecs / 3600.0; return true; } DEBUGFDEVICE(ieqpro_device, INDI::Logger::DBG_ERROR, "Error: Malformed result (%s).", response); return false; } DEBUGFDEVICE(ieqpro_device, INDI::Logger::DBG_ERROR, "Only received #%d bytes, expected 8.", nbytes_read); return false; } bool set_ieqpro_local_date(int fd, int yy, int mm, int dd) { char cmd[16]; int errcode = 0; char errmsg[MAXRBUF]; char response[8]; int nbytes_read = 0; int nbytes_written = 0; snprintf(cmd, 16, ":SC%02d%02d%02d#", yy, mm, dd); DEBUGFDEVICE(ieqpro_device, INDI::Logger::DBG_DEBUG, "CMD (%s)", cmd); if (ieqpro_simulation) { strcpy(response, "1"); nbytes_read = strlen(response); } else { tcflush(fd, TCIFLUSH); if ((errcode = tty_write(fd, cmd, strlen(cmd), &nbytes_written)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); DEBUGFDEVICE(ieqpro_device, INDI::Logger::DBG_ERROR, "%s", errmsg); return false; } if ((errcode = tty_read(fd, response, 1, IEQPRO_TIMEOUT, &nbytes_read))) { tty_error_msg(errcode, errmsg, MAXRBUF); DEBUGFDEVICE(ieqpro_device, INDI::Logger::DBG_ERROR, "%s", errmsg); return false; } } if (nbytes_read > 0) { response[nbytes_read] = '\0'; DEBUGFDEVICE(ieqpro_device, INDI::Logger::DBG_DEBUG, "RES (%s)", response); tcflush(fd, TCIFLUSH); return true; } DEBUGFDEVICE(ieqpro_device, INDI::Logger::DBG_ERROR, "Only received #%d bytes, expected 1.", nbytes_read); return false; } bool set_ieqpro_local_time(int fd, int hh, int mm, int ss) { char cmd[16]; int errcode = 0; char errmsg[MAXRBUF]; char response[8]; int nbytes_read = 0; int nbytes_written = 0; snprintf(cmd, 16, ":SL%02d%02d%02d#", hh, mm, ss); DEBUGFDEVICE(ieqpro_device, INDI::Logger::DBG_DEBUG, "CMD (%s)", cmd); if (ieqpro_simulation) { strcpy(response, "1"); nbytes_read = strlen(response); } else { tcflush(fd, TCIFLUSH); if ((errcode = tty_write(fd, cmd, strlen(cmd), &nbytes_written)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); DEBUGFDEVICE(ieqpro_device, INDI::Logger::DBG_ERROR, "%s", errmsg); return false; } if ((errcode = tty_read(fd, response, 1, IEQPRO_TIMEOUT, &nbytes_read))) { tty_error_msg(errcode, errmsg, MAXRBUF); DEBUGFDEVICE(ieqpro_device, INDI::Logger::DBG_ERROR, "%s", errmsg); return false; } } if (nbytes_read > 0) { response[nbytes_read] = '\0'; DEBUGFDEVICE(ieqpro_device, INDI::Logger::DBG_DEBUG, "RES (%s)", response); tcflush(fd, TCIFLUSH); return true; } DEBUGFDEVICE(ieqpro_device, INDI::Logger::DBG_ERROR, "Only received #%d bytes, expected 1.", nbytes_read); return false; } bool set_ieqpro_daylight_saving(int fd, bool enabled) { char cmd[16]; int errcode = 0; char errmsg[MAXRBUF]; char response[8]; int nbytes_read = 0; int nbytes_written = 0; if (enabled) strcpy(cmd, ":SDS1#"); else strcpy(cmd, ":SDS0#"); DEBUGFDEVICE(ieqpro_device, INDI::Logger::DBG_DEBUG, "CMD (%s)", cmd); if (ieqpro_simulation) { strcpy(response, "1"); nbytes_read = strlen(response); } else { tcflush(fd, TCIFLUSH); if ((errcode = tty_write(fd, cmd, strlen(cmd), &nbytes_written)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); DEBUGFDEVICE(ieqpro_device, INDI::Logger::DBG_ERROR, "%s", errmsg); return false; } if ((errcode = tty_read(fd, response, 1, IEQPRO_TIMEOUT, &nbytes_read))) { tty_error_msg(errcode, errmsg, MAXRBUF); DEBUGFDEVICE(ieqpro_device, INDI::Logger::DBG_ERROR, "%s", errmsg); return false; } } if (nbytes_read > 0) { response[nbytes_read] = '\0'; DEBUGFDEVICE(ieqpro_device, INDI::Logger::DBG_DEBUG, "RES (%s)", response); tcflush(fd, TCIFLUSH); return true; } DEBUGFDEVICE(ieqpro_device, INDI::Logger::DBG_ERROR, "Only received #%d bytes, expected 1.", nbytes_read); return false; } bool set_ieqpro_utc_offset(int fd, double offset) { char cmd[16]; char sign; int errcode = 0; char errmsg[MAXRBUF]; char response[8]; int nbytes_read = 0; int nbytes_written = 0; if (offset >= 0) sign = '+'; else sign = '-'; int offset_minutes = fabs(offset) * 60.0; snprintf(cmd, 16, ":SG%c%03d#", sign, offset_minutes); DEBUGFDEVICE(ieqpro_device, INDI::Logger::DBG_DEBUG, "CMD (%s)", cmd); if (ieqpro_simulation) { strcpy(response, "1"); nbytes_read = strlen(response); } else { tcflush(fd, TCIFLUSH); if ((errcode = tty_write(fd, cmd, strlen(cmd), &nbytes_written)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); DEBUGFDEVICE(ieqpro_device, INDI::Logger::DBG_ERROR, "%s", errmsg); return false; } if ((errcode = tty_read(fd, response, 1, IEQPRO_TIMEOUT, &nbytes_read))) { tty_error_msg(errcode, errmsg, MAXRBUF); DEBUGFDEVICE(ieqpro_device, INDI::Logger::DBG_ERROR, "%s", errmsg); return false; } } if (nbytes_read > 0) { response[nbytes_read] = '\0'; DEBUGFDEVICE(ieqpro_device, INDI::Logger::DBG_DEBUG, "RES (%s)", response); tcflush(fd, TCIFLUSH); return true; } DEBUGFDEVICE(ieqpro_device, INDI::Logger::DBG_ERROR, "Only received #%d bytes, expected 1.", nbytes_read); return false; } bool get_ieqpro_coords(int fd, double *ra, double *dec) { char cmd[] = ":GEC#"; int errcode = 0; char errmsg[MAXRBUF]; char response[32]; int nbytes_read = 0; int nbytes_written = 0; DEBUGFDEVICE(ieqpro_device, INDI::Logger::DBG_EXTRA_1, "CMD (%s)", cmd); if (ieqpro_simulation) { char ra_str[16], dec_str[16]; char sign; if (simData.dec >= 0) sign = '+'; else sign = '-'; int ieqDEC = fabs(simData.dec) * 60 * 60 * 100; snprintf(dec_str, 16, "%c%08d", sign, ieqDEC); int ieqRA = simData.ra * 60 * 60 * 1000; snprintf(ra_str, 16, "%08d", ieqRA); snprintf(response, 32, "%s%s#", dec_str, ra_str); nbytes_read = strlen(response); } else { tcflush(fd, TCIFLUSH); if ((errcode = tty_write(fd, cmd, strlen(cmd), &nbytes_written)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); DEBUGFDEVICE(ieqpro_device, INDI::Logger::DBG_ERROR, "%s", errmsg); return false; } if ((errcode = tty_read_section(fd, response, '#', IEQPRO_TIMEOUT, &nbytes_read))) { tty_error_msg(errcode, errmsg, MAXRBUF); DEBUGFDEVICE(ieqpro_device, INDI::Logger::DBG_ERROR, "%s", errmsg); return false; } } if (nbytes_read > 0) { tcflush(fd, TCIFLUSH); response[nbytes_read] = '\0'; DEBUGFDEVICE(ieqpro_device, INDI::Logger::DBG_EXTRA_1, "RES (%s)", response); char ra_str[16]= {0}, dec_str[16] = {0}; strncpy(dec_str, response, 9); strncpy(ra_str, response + 9, 8); int ieqDEC = atoi(dec_str); int ieqRA = atoi(ra_str); *ra = ieqRA / (60.0 * 60.0 * 1000.0); *dec = ieqDEC / (60.0 * 60.0 * 100.0); return true; } DEBUGFDEVICE(ieqpro_device, INDI::Logger::DBG_ERROR, "Only received #%d bytes, expected 1.", nbytes_read); return false; } bool get_ieqpro_utc_date_time(int fd, double *utc_hours, int *yy, int *mm, int *dd, int *hh, int *minute, int *ss) { char cmd[] = ":GLT#"; int errcode = 0; char errmsg[MAXRBUF]; char response[32]; int nbytes_read = 0; int nbytes_written = 0; DEBUGFDEVICE(ieqpro_device, INDI::Logger::DBG_DEBUG, "CMD (%s)", cmd); // Format according to Manual is sMMMYYMMDDHHMMSS# // However as pointed out by user Shepherd on INDI forums, actual format is // sMMMxYYMMDDHHMMSS# // Where x is either 0 or 1 denoting daying savings if (ieqpro_simulation) { strncpy(response, "+1800150331173000#", 32); nbytes_read = strlen(response); } else { tcflush(fd, TCIFLUSH); if ((errcode = tty_write(fd, cmd, strlen(cmd), &nbytes_written)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); DEBUGFDEVICE(ieqpro_device, INDI::Logger::DBG_ERROR, "%s", errmsg); return false; } if ((errcode = tty_read_section(fd, response, '#', IEQPRO_TIMEOUT, &nbytes_read))) { tty_error_msg(errcode, errmsg, MAXRBUF); DEBUGFDEVICE(ieqpro_device, INDI::Logger::DBG_ERROR, "%s", errmsg); return false; } } if (nbytes_read > 0) { tcflush(fd, TCIFLUSH); response[nbytes_read] = '\0'; DEBUGFDEVICE(ieqpro_device, INDI::Logger::DBG_DEBUG, "RES (%s)", response); char utc_str[8]={0}, yy_str[8]={0}, mm_str[8]={0}, dd_str[8]={0}, hh_str[8]={0}, minute_str[8]={0}, ss_str[8]={0}, dst_str[8]={0}; // UTC Offset strncpy(utc_str, response, 4); // Daylight savings strncpy(dst_str, response + 4, 1); // Year strncpy(yy_str, response + 5, 2); // Month strncpy(mm_str, response + 7, 2); // Day strncpy(dd_str, response + 9, 2); // Hour strncpy(hh_str, response + 11, 2); // Minute strncpy(minute_str, response + 13, 2); // Second strncpy(ss_str, response + 15, 2); *utc_hours = atoi(utc_str) / 60.0; *yy = atoi(yy_str) + 2000; *mm = atoi(mm_str) + 1; *dd = atoi(dd_str); *hh = atoi(hh_str); *minute = atoi(minute_str); *ss = atoi(ss_str); ln_zonedate localTime; ln_date utcTime; localTime.years = *yy; localTime.months = *mm; localTime.days = *dd; localTime.hours = *hh; localTime.minutes = *minute; localTime.seconds = *ss; localTime.gmtoff = *utc_hours * 3600; ln_zonedate_to_date(&localTime, &utcTime); *yy = utcTime.years; *mm = utcTime.months; *dd = utcTime.days; *hh = utcTime.hours; *minute = utcTime.minutes; *ss = utcTime.seconds; return true; } DEBUGFDEVICE(ieqpro_device, INDI::Logger::DBG_ERROR, "Only received #%d bytes, expected 1.", nbytes_read); return false; } libindi/drivers/telescope/skywatcherAltAzSimple.h0000664000175000017500000001413613263645557021603 0ustar jasemjasem/*! * \file SkywatcherAltAzSimple.h * * \author Roger James * \author Gerry Rozema * \author Jean-Luc Geehalel * \date 13th November 2013 * * This file contains the definitions for a C++ implementatiom of a INDI telescope driver using the Skywatcher API. * It is based on work from three sources. * A C++ implementation of the API by Roger James. * The indi_eqmod driver by Jean-Luc Geehalel. * The synscanmount driver by Gerry Rozema. */ #pragma once #include "indiguiderinterface.h" #include "skywatcherAPI.h" typedef enum { PARK_COUNTERCLOCKWISE = 0, PARK_CLOCKWISE } ParkDirection_t; typedef enum { PARK_NORTH = 0, PARK_EAST, PARK_SOUTH, PARK_WEST } ParkPosition_t; struct GuidingPulse { float DeltaAlt { 0 }; float DeltaAz { 0 }; }; class SkywatcherAltAzSimple : public SkywatcherAPI, public INDI::Telescope, public INDI::GuiderInterface { public: SkywatcherAltAzSimple(); virtual ~SkywatcherAltAzSimple() = default; // overrides of base class virtual functions virtual bool Abort() override; virtual bool Handshake() override; virtual const char *getDefaultName() override; virtual bool Goto(double ra, double dec) override; virtual bool initProperties() override; virtual void ISGetProperties(const char *dev) override; virtual bool ISNewBLOB(const char *dev, const char *name, int sizes[], int blobsizes[], char *blobs[], char *formats[], char *names[], int n) override; virtual bool ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) override; virtual bool ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) override; virtual bool ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n) override; double GetSlewRate(); virtual bool MoveNS(INDI_DIR_NS dir, TelescopeMotionCommand command) override; virtual bool MoveWE(INDI_DIR_WE dir, TelescopeMotionCommand command) override; double GetParkDeltaAz(ParkDirection_t target_direction, ParkPosition_t target_position); virtual bool Park() override; virtual bool UnPark() override; virtual bool ReadScopeStatus() override; virtual bool saveConfigItems(FILE *fp) override; virtual bool Sync(double ra, double dec) override; virtual void TimerHit() override; virtual bool updateProperties() override; virtual IPState GuideNorth(float ms) override; virtual IPState GuideSouth(float ms) override; virtual IPState GuideEast(float ms) override; virtual IPState GuideWest(float ms) override; private: void ResetGuidePulses(); void UpdateScopeConfigSwitch(); // Overrides for the pure virtual functions in SkyWatcherAPI virtual int skywatcher_tty_read(int fd, char *buf, int nbytes, int timeout, int *nbytes_read) override; virtual int skywatcher_tty_write(int fd, const char *buffer, int nbytes, int *nbytes_written) override; void UpdateDetailedMountInformation(bool InformClient); ln_hrz_posn GetAltAzPosition(double ra, double dec, double offset_in_sec = 0); ln_equ_posn GetRaDecPosition(double alt, double az); void LogMessage(const char* format, ...); // Properties static constexpr const char *DetailedMountInfoPage { "Detailed Mount Information" }; enum { MOTOR_CONTROL_FIRMWARE_VERSION, MOUNT_CODE, MOUNT_NAME, IS_DC_MOTOR }; IText BasicMountInfo[4] {}; ITextVectorProperty BasicMountInfoV; enum { MICROSTEPS_PER_REVOLUTION, STEPPER_CLOCK_FREQUENCY, HIGH_SPEED_RATIO, MICROSTEPS_PER_WORM_REVOLUTION }; INumber AxisOneInfo[4]; INumberVectorProperty AxisOneInfoV; INumber AxisTwoInfo[4]; INumberVectorProperty AxisTwoInfoV; enum { FULL_STOP, SLEWING, SLEWING_TO, SLEWING_FORWARD, HIGH_SPEED, NOT_INITIALISED }; ISwitch AxisOneState[6]; ISwitchVectorProperty AxisOneStateV; ISwitch AxisTwoState[6]; ISwitchVectorProperty AxisTwoStateV; enum { RAW_MICROSTEPS, MICROSTEPS_PER_ARCSEC, OFFSET_FROM_INITIAL, DEGREES_FROM_INITIAL }; INumber AxisOneEncoderValues[4]; INumberVectorProperty AxisOneEncoderValuesV; INumber AxisTwoEncoderValues[4]; INumberVectorProperty AxisTwoEncoderValuesV; // A switch for silent/highspeed slewing modes enum { SLEW_SILENT, SLEW_NORMAL }; ISwitch SlewModes[2]; ISwitchVectorProperty SlewModesSP; // A switch for wedge mode enum { WEDGE_SIMPLE, WEDGE_EQ, WEDGE_DISABLED }; ISwitch WedgeMode[3]; ISwitchVectorProperty WedgeModeSP; // A switch for tracking logging enum { TRACKLOG_ENABLED, TRACKLOG_DISABLED }; ISwitch TrackLogMode[2]; ISwitchVectorProperty TrackLogModeSP; // Guiding rates (RA/Dec) INumber GuidingRatesN[2]; INumberVectorProperty GuidingRatesNP; // Tracking values INumber TrackingValuesN[3]; INumberVectorProperty TrackingValuesNP; // A switch for park movement directions (clockwise/counterclockwise) ISwitch ParkMovementDirection[2]; ISwitchVectorProperty ParkMovementDirectionSP; // A switch for park positions ISwitch ParkPosition[4]; ISwitchVectorProperty ParkPositionSP; // A switch for unpark positions ISwitch UnparkPosition[4]; ISwitchVectorProperty UnparkPositionSP; // Tracking ln_equ_posn CurrentTrackingTarget { 0, 0 }; long OldTrackingTarget[2] { 0, 0 }; struct ln_hrz_posn CurrentAltAz { 0, 0 }; bool ResetTrackingSeconds { false }; int TrackingMsecs { 0 }; int TrackingStartTimer { 0 }; double GuideDeltaAlt { 0 }; double GuideDeltaAz { 0 }; int TimeoutDuration { 500 }; const std::string TrackLogFileName; int UpdateCount { 0 }; /// Save the serial port name std::string SerialPortName; /// Recover after disconnection bool RecoverAfterReconnection { false }; bool VerboseScopeStatus { false }; std::vector GuidingPulses; }; libindi/drivers/telescope/lx200ap_experimentaldriver.cpp0000664000175000017500000001165713263645557023030 0ustar jasemjasem#if 0 LX200 Astro- Physics Driver Copyright (C) 2007 Markus Wildi 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 // This file contains functions which require the 'V' firmware level and will NOT work with previous versions! // Used only by the lx200ap_experimental driver in conjuction with the routines in lx200ap_driver.cpp which work // with all firmware versions #include //#include "lx200apdriver.h" #include "indicom.h" #include "indilogger.h" #include "lx200ap_experimentaldriver.h" #include #include #ifndef _WIN32 #include #endif #define LX200_TIMEOUT 5 /* FD timeout in seconds */ char lx200ap_exp_name[MAXINDIDEVICE]; unsigned int AP_EXP_DBG_SCOPE; void set_lx200ap_exp_name(const char *deviceName, unsigned int debug_level) { strncpy(lx200ap_exp_name, deviceName, MAXINDIDEVICE); AP_EXP_DBG_SCOPE = debug_level; } // experimental functions!!! int setAPMeridianDelay(int fd, double mdelay) { char cmd[32]; char hourstr[16]; int nbytes_write = 0; DEBUGFDEVICE(lx200ap_exp_name, AP_EXP_DBG_SCOPE, "<%s>", __FUNCTION__); if (mdelay < 0) { DEBUGFDEVICE(lx200ap_exp_name, INDI::Logger::DBG_ERROR, "Meridian delay < 0 not supp\ orted! mdelay=%f", mdelay); return -1; } // convert from decimal hours to format for command if (fs_sexa(hourstr, mdelay, 2, 3600) < 0) { DEBUGFDEVICE(lx200ap_exp_name, INDI::Logger::DBG_ERROR, "Unable to format meridian d\ elay %f to time format!", mdelay); return -1; } DEBUGFDEVICE(lx200ap_exp_name, AP_EXP_DBG_SCOPE, "Meridian Delay %f -> %s", mdelay, hourstr)\ ; sprintf(cmd, ":SM%s#", hourstr); DEBUGFDEVICE(lx200ap_exp_name, AP_EXP_DBG_SCOPE, "CMD <%s>", cmd); tty_write_string(fd, cmd, &nbytes_write); tcflush(fd, TCIFLUSH); return 0; } int getAPMeridianDelay(int fd, double *mdelay) { int error_type; int nbytes_write = 0; int nbytes_read = 0; char temp_string[16]; DEBUGFDEVICE(lx200ap_exp_name, AP_EXP_DBG_SCOPE, "<%s>", __FUNCTION__); DEBUGFDEVICE(lx200ap_exp_name, AP_EXP_DBG_SCOPE, "CMD <%s>", "#:GM#"); if ((error_type = tty_write_string(fd, "#:GM#", &nbytes_write)) != TTY_OK) return error_type; if ((error_type = tty_read_section(fd, temp_string, '#', LX200_TIMEOUT, &nbytes_read\ )) != TTY_OK) { DEBUGFDEVICE(lx200ap_exp_name, INDI::Logger::DBG_ERROR, "getAPMeridianDelay: error %\ d, %d", error_type, nbytes_read); return error_type; } tcflush(fd, TCIFLUSH); DEBUGFDEVICE(lx200ap_exp_name, AP_EXP_DBG_SCOPE, "RES <%s>", temp_string); if (f_scansexa(temp_string, mdelay)) { DEBUGFDEVICE(lx200ap_exp_name, INDI::Logger::DBG_ERROR, "getAPMeridianDelay: unable \ to process %s", temp_string); return -1; } return 0; } int check_lx200ap_status(int fd, char *parkStatus, char *slewStatus) { char temp_string[64]; int error_type; int nbytes_write = 0; int nbytes_read = 0; DEBUGDEVICE(lx200ap_exp_name, INDI::Logger::DBG_DEBUG, "EXPERIMENTAL: check status..."); if (fd <= 0) { DEBUGDEVICE(lx200ap_exp_name, INDI::Logger::DBG_ERROR, "check_lx200ap_connection: not a valid file descriptor received"); return -1; } if ((error_type = tty_write_string(fd, "#:GOS#", &nbytes_write)) != TTY_OK) { DEBUGFDEVICE(lx200ap_exp_name, INDI::Logger::DBG_ERROR, "check_lx200ap_connection: unsuccessful write to telescope, %d", nbytes_write); return error_type; } tty_read_section(fd, temp_string, '#', LX200_TIMEOUT, &nbytes_read); tcflush(fd, TCIFLUSH); if (nbytes_read > 1) { temp_string[nbytes_read - 1] = '\0'; DEBUGFDEVICE(lx200ap_exp_name, INDI::Logger::DBG_DEBUG, "check_lx200ap_status: received bytes %d, [%s]", nbytes_write, temp_string); *parkStatus = temp_string[0]; *slewStatus = temp_string[3]; return 0; } DEBUGDEVICE(lx200ap_exp_name, INDI::Logger::DBG_ERROR, "check_lx200ap_status: wrote, but nothing received."); return -1; } libindi/drivers/telescope/celestrondriver.cpp0000664000175000017500000004536313263645557021064 0ustar jasemjasem/* Celestron driver Copyright (C) 2015 Jasem Mutlaq Copyright (C) 2017 Juan Menendez 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 */ /* Version with experimental pulse guide support. GC 04.12.2015 */ #include "indicom.h" #include "indilogger.h" #include "celestrondriver.h" #include #include #include #include #include #include #define CELESTRON_TIMEOUT 5 /* FD timeout in seconds */ using namespace Celestron; char device_str[MAXINDIDEVICE] = "Celestron GPS"; // Account for the quadrant in declination double Celestron::trimDecAngle(double angle) { angle = angle - 360*floor(angle/360); if (angle < 0) angle += 360.0; if ((angle > 90.) && (angle <= 270.)) angle = 180. - angle; else if ((angle > 270.) && (angle <= 360.)) angle = angle - 360.; return angle; } // Convert decimal degrees to NexStar angle uint16_t Celestron::dd2nex(double angle) { angle = angle - 360*floor(angle/360); if (angle < 0) angle += 360.0; return (uint16_t)(angle * 0x10000 / 360.0); } // Convert decimal degrees to NexStar angle (precise) uint32_t Celestron::dd2pnex(double angle) { angle = angle - 360*floor(angle/360); if (angle < 0) angle += 360.0; return (uint32_t)(angle * 0x100000000 / 360.0); } // Convert NexStar angle to decimal degrees double Celestron::nex2dd(uint16_t value) { return 360.0 * ((double)value / 0x10000); } // Convert NexStar angle to decimal degrees (precise) double Celestron::pnex2dd(uint32_t value) { return 360.0 * ((double)value / 0x100000000); } void hex_dump(char *buf, const char *data, int size) { for (int i = 0; i < size; i++) sprintf(buf + 3 * i, "%02X ", (unsigned char)data[i]); if (size > 0) buf[3 * size - 1] = '\0'; } // This method is required by the logging macros const char *CelestronDriver::getDeviceName() { return device_str; } void CelestronDriver::set_device(const char *name) { strncpy(device_str, name, MAXINDIDEVICE); } // Virtual method for testing int CelestronDriver::serial_write(const char *cmd, int nbytes, int *nbytes_written) { tcflush(fd, TCIOFLUSH); return tty_write(fd, cmd, nbytes, nbytes_written); } // Virtual method for testing int CelestronDriver::serial_read(int nbytes, int *nbytes_read) { return tty_read(fd, response, nbytes, CELESTRON_TIMEOUT, nbytes_read); } // Virtual method for testing int CelestronDriver::serial_read_section(char stop_char, int *nbytes_read) { return tty_read_section(fd, response, stop_char, CELESTRON_TIMEOUT, nbytes_read); } // Set the expected response for a command in simulation mode void CelestronDriver::set_sim_response(const char *fmt, ...) { if (simulation) { va_list args; va_start(args, fmt); vsprintf(response, fmt, args); va_end(args); } } // Send a command to the mount. Return the number of bytes received or 0 if // case of error int CelestronDriver::send_command(const char *cmd, int cmd_len, char *resp, int resp_len, bool ascii_cmd, bool ascii_resp) { int err; int nbytes = resp_len; char errmsg[MAXRBUF]; char hexbuf[3 * MAX_RESP_SIZE]; if (ascii_cmd) LOGF_DEBUG("CMD <%s>", cmd); else { // Non-ASCII commands should be represented as hex strings hex_dump(hexbuf, cmd, cmd_len); LOGF_DEBUG("CMD <%s>", hexbuf); } if (!simulation && fd) { if ((err = serial_write(cmd, cmd_len, &nbytes)) != TTY_OK) { tty_error_msg(err, errmsg, MAXRBUF); LOGF_ERROR("Serial write error: %s", errmsg); return 0; } if (resp_len > 0) { if (ascii_resp) err = serial_read_section('#', &nbytes); else err = serial_read(resp_len, &nbytes); if (err) { tty_error_msg(err, errmsg, MAXRBUF); LOGF_ERROR("Serial read error: %s", errmsg); return 0; } } } if (resp_len == 0) return true; if (nbytes != resp_len) { LOGF_ERROR("Received %d bytes, expected %d.", nbytes, resp_len); return 0; } resp[nbytes] = '\0'; if (ascii_resp) LOGF_DEBUG("RES <%s>", resp); else { // Non-ASCII commands should be represented as hex strings hex_dump(hexbuf, resp, resp_len); LOGF_DEBUG("RES <%s>", hexbuf); } return nbytes; } // Send a 'passthrough command' to the mount. Return the number of bytes // received or 0 in case of error int CelestronDriver::send_passthrough(int dest, int cmd_id, const char *payload, int payload_len, char *response, int response_len) { char cmd[8] = {0}; cmd[0] = 0x50; cmd[1] = (char)(payload_len + 1); cmd[2] = (char)dest; cmd[3] = (char)cmd_id; cmd[7] = (char)response_len; // payload_len must be <= 3 ! memcpy(cmd + 4, payload, payload_len); return send_command(cmd, 8, response, response_len + 1, false, false); } bool CelestronDriver::check_connection() { LOG_DEBUG("Initializing Celestron using Kx CMD..."); for (int i = 0; i < 2; i++) { if (echo()) return true; usleep(50000); } return false; } bool CelestronDriver::get_firmware(FirmwareInfo *info) { char version[8], model[16], RAVersion[8], DEVersion[8]; LOG_DEBUG("Getting controller version..."); if (!get_version(version, 8)) return false; info->Version = version; info->controllerVersion = atof(version); LOG_DEBUG("Getting controller variant..."); info->controllerVariant = ISNEXSTAR; get_variant(&(info->controllerVariant)); if (((info->controllerVariant == ISSTARSENSE) && info->controllerVersion >= MINSTSENSVER) || (info->controllerVersion >= 2.2)) { LOG_DEBUG("Getting controller model..."); if (!get_model(model, 16)) return false; info->Model = model; } else info->Model = "Unknown"; //LOG_DEBUG("Getting GPS firmware version..."); // char GPSVersion[8]; //if (!get_dev_firmware(CELESTRON_DEV_GPS, GPSVersion, 8)) //return false; //info->GPSFirmware = GPSVersion; info->GPSFirmware = "0.0"; LOG_DEBUG("Getting RA firmware version..."); if (!get_dev_firmware(CELESTRON_DEV_RA, RAVersion, 8)) return false; info->RAFirmware = RAVersion; LOG_DEBUG("Getting DEC firmware version..."); if (!get_dev_firmware(CELESTRON_DEV_DEC, DEVersion, 8)) return false; info->DEFirmware = DEVersion; return true; } bool CelestronDriver::echo() { set_sim_response("x#"); if (!send_command("Kx", 2, response, 2, true, true)) return false; return !strcmp(response, "x#"); } bool CelestronDriver::get_version(char *version, int size) { set_sim_response("\x04\x29#"); if (!send_command("V", 1, response, 3, true, false)) return false; snprintf(version, size, "%d.%02d", response[0], response[1]); LOGF_INFO("Controller version: %s", version); return true; } //TODO: no critical errors for this command bool CelestronDriver::get_variant(char *variant) { set_sim_response("\x11#"); if (!send_command("v", 1, response, 2, true, false)) return false; *variant = response[0]; return true; } bool CelestronDriver::get_model(char *model, int size) { std::map models = { {1, "GPS Series"}, {3, "i-Series"}, {4, "i-Series SE"}, {5, "CGE"}, {6, "Advanced GT"}, {7, "SLT"}, {9, "CPC"}, {10, "GT"}, {11, "4/5 SE"}, {12, "6/8 SE"}, {13, "CGE Pro"}, {14, "CGEM DX"}, {20, "AVX"}, }; set_sim_response("\x06#"); // Simulated response if (!send_command("m", 1, response, 2, true, false)) return false; int m = response[0]; if (models.find(m) != models.end()) { strncpy(model, models[m].c_str(), size); LOGF_INFO("Mount model: %s", model); } else { strncpy(model, "Unknown", size); LOGF_WARN("Unrecognized model (%d).", model); } return true; } bool CelestronDriver::get_dev_firmware(int dev, char *version, int size) { set_sim_response("\x01\x09#"); int rlen = send_passthrough(dev, 0xfe, NULL, 0, response, 2); if (rlen == 3) snprintf(version, size, "%d.%02d", response[0], response[1]); else if (rlen == 2) // some GPS models return only 2 bytes snprintf(version, size, "%01d.0", response[0]); else return false; return true; } /***************************************************************** PulseGuide commands, experimental ******************************************************************/ /***************************************************************** Send a guiding pulse to the mount in direction "dir". "rate" should be a signed 8-bit integer in the range (-100,100) that represents the pulse velocity in % of sidereal. "duration_csec" is an unsigned 8-bit integer (0,255) with the pulse duration in centiseconds (i.e. 1/100 s = 10ms). The max pulse duration is 2550 ms. ******************************************************************/ int CelestronDriver::send_pulse(CELESTRON_DIRECTION dir, signed char rate, unsigned char duration_csec) { int dev = (dir == CELESTRON_N || dir == CELESTRON_S) ? CELESTRON_DEV_DEC : CELESTRON_DEV_RA; char payload[2]; payload[0] = (dir == CELESTRON_N || dir == CELESTRON_W) ? rate : -rate; payload[1] = duration_csec; set_sim_response("#"); return send_passthrough(dev, 0x26, payload, 2, response, 1); } /***************************************************************** Send the guiding pulse status check command to the mount for the motor responsible for "dir". If a pulse is being executed, "pulse_state" is set to 1, whereas if the pulse motion has been completed it is set to 0. Return "false" if the status command fails, otherwise return "true". ******************************************************************/ int CelestronDriver::get_pulse_status(CELESTRON_DIRECTION dir, bool &pulse_state) { int dev = (dir == CELESTRON_N || dir == CELESTRON_S) ? CELESTRON_DEV_DEC : CELESTRON_DEV_RA; char payload[2] = {0, 0}; set_sim_response("#"); if (!send_passthrough(dev, 0x27, payload, 2, response, 1)) return false; pulse_state = (bool)response[0]; return true; } bool CelestronDriver::start_motion(CELESTRON_DIRECTION dir, CELESTRON_SLEW_RATE rate) { int dev = (dir == CELESTRON_N || dir == CELESTRON_S) ? CELESTRON_DEV_DEC : CELESTRON_DEV_RA; int cmd_id = (dir == CELESTRON_N || dir == CELESTRON_W) ? 0x24 : 0x25; char payload[1]; payload[0] = rate + 1; set_sim_response("#"); return send_passthrough(dev, cmd_id, payload, 1, response, 1); } bool CelestronDriver::stop_motion(CELESTRON_DIRECTION dir) { int dev = (dir == CELESTRON_N || dir == CELESTRON_S) ? CELESTRON_DEV_DEC : CELESTRON_DEV_RA; char payload[] = { 0 }; set_sim_response("#"); return send_passthrough(dev, 0x24, payload, 1, response, 1); } bool CelestronDriver::abort() { set_sim_response("#"); return send_command("M", 1, response, 1, true, true); } bool CelestronDriver::slew_radec(double ra, double dec, bool precise) { char RAStr[16], DecStr[16]; fs_sexa(RAStr, ra, 2, 3600); fs_sexa(DecStr, dec, 2, 3600); LOGF_DEBUG("Goto RA-DEC(%s,%s)", RAStr, DecStr); set_sim_slewing(true); char cmd[20]; if (precise) sprintf(cmd, "r%08X,%08X", dd2pnex(ra*15), dd2pnex(dec)); else sprintf(cmd, "R%04X,%04X", dd2nex(ra*15), dd2nex(dec)); set_sim_response("#"); return send_command(cmd, strlen(cmd), response, 1, true, true); } bool CelestronDriver::slew_azalt(double az, double alt, bool precise) { char AzStr[16], AltStr[16]; fs_sexa(AzStr, az, 3, 3600); fs_sexa(AltStr, alt, 2, 3600); LOGF_DEBUG("Goto AZM-ALT (%s,%s)", AzStr, AltStr); set_sim_slewing(true); char cmd[20]; if (precise) sprintf(cmd, "b%08X,%08X", dd2pnex(az), dd2pnex(alt)); else sprintf(cmd, "B%04X,%04X", dd2nex(az), dd2nex(alt)); set_sim_response("#"); return send_command(cmd, strlen(cmd), response, 1, true, true); } bool CelestronDriver::sync(double ra, double dec, bool precise) { char RAStr[16], DecStr[16]; fs_sexa(RAStr, ra, 2, 3600); fs_sexa(DecStr, dec, 2, 3600); LOGF_DEBUG("Sync (%s,%s)", RAStr, DecStr); sim_data.ra = ra; sim_data.dec = dec; char cmd[20]; if (precise) sprintf(cmd, "s%08X,%08X", dd2pnex(ra*15), dd2pnex(dec)); else sprintf(cmd, "S%04X,%04X", dd2nex(ra*15), dd2nex(dec)); set_sim_response("#"); return send_command(cmd, strlen(cmd), response, 1, true, true); } void parseCoordsResponse(char *response, double *d1, double *d2, bool precise) { uint32_t d1_int = 0, d2_int = 0; sscanf(response, "%x,%x#", &d1_int, &d2_int); if (precise) { *d1 = pnex2dd(d1_int); *d2 = pnex2dd(d2_int); } else { *d1 = nex2dd(d1_int); *d2 = nex2dd(d2_int); } } bool CelestronDriver::get_radec(double *ra, double *dec, bool precise) { if (precise) { set_sim_response("%08X,%08X#", dd2pnex(sim_data.ra*15), dd2pnex(sim_data.dec)); if (!send_command("e", 1, response, 18, true, true)) return false; } else { set_sim_response("%04X,%04X#", dd2nex(sim_data.ra*15), dd2nex(sim_data.dec)); if (!send_command("E", 1, response, 10, true, true)) return false; } parseCoordsResponse(response, ra, dec, precise); *ra /= 15.0; *dec = trimDecAngle(*dec); char RAStr[16], DecStr[16]; fs_sexa(RAStr, *ra, 2, 3600); fs_sexa(DecStr, *dec, 2, 3600); LOGF_EXTRA1("RA-DEC (%s,%s)", RAStr, DecStr); return true; } bool CelestronDriver::get_azalt(double *az, double *alt, bool precise) { if (precise) { set_sim_response("%08X,%08X#", dd2pnex(sim_data.az), dd2pnex(sim_data.alt)); if (!send_command("z", 1, response, 18, true, true)) return false; } else { set_sim_response("%04X,%04X#", dd2nex(sim_data.az), dd2nex(sim_data.alt)); if (!send_command("Z", 1, response, 10, true, true)) return false; } parseCoordsResponse(response, az, alt, precise); char AzStr[16], AltStr[16]; fs_sexa(AzStr, *az, 3, 3600); fs_sexa(AltStr, *alt, 2, 3600); LOGF_EXTRA1("RES <%s> ==> AZM-ALT (%s,%s)", response, AzStr, AltStr); return true; } bool CelestronDriver::set_location(double longitude, double latitude) { LOGF_DEBUG("Setting location (%.3f,%.3f)", longitude, latitude); // Convert from INDI standard to regular east/west -180 to 180 if (longitude > 180) longitude -= 360; int lat_d, lat_m, lat_s; int long_d, long_m, long_s; getSexComponents(latitude, &lat_d, &lat_m, &lat_s); getSexComponents(longitude, &long_d, &long_m, &long_s); char cmd[9]; cmd[0] = 'W'; cmd[1] = abs(lat_d); cmd[2] = lat_m; cmd[3] = lat_s; cmd[4] = lat_d > 0 ? 0 : 1; cmd[5] = abs(long_d); cmd[6] = long_m; cmd[7] = long_s; cmd[8] = long_d > 0 ? 0 : 1; set_sim_response("#"); return send_command(cmd, 9, response, 1, false, true); } bool CelestronDriver::set_datetime(struct ln_date *utc, double utc_offset) { struct ln_zonedate local_date; // Celestron takes local time ln_date_to_zonedate(utc, &local_date, utc_offset * 3600); char cmd[9]; cmd[0] = 'H'; cmd[1] = local_date.hours; cmd[2] = local_date.minutes; cmd[3] = local_date.seconds; cmd[4] = local_date.months; cmd[5] = local_date.days; cmd[6] = local_date.years - 2000; if (utc_offset < 0) cmd[7] = 256 - ((uint16_t)fabs(utc_offset)); else cmd[7] = ((uint16_t)fabs(utc_offset)); // Always assume standard time cmd[8] = 0; set_sim_response("#"); return send_command(cmd, 9, response, 1, false, true); } bool CelestronDriver::get_utc_date_time(double *utc_hours, int *yy, int *mm, int *dd, int *hh, int *minute, int *ss) { // Simulated response (HH MM SS MONTH DAY YEAR OFFSET DAYLIGHT) set_sim_response("%c%c%c%c%c%c%c%c#", 17, 30, 10, 4, 1, 15, 3, 0); if (!send_command("h", 1, response, 9, true, false)) return false; // HH MM SS MONTH DAY YEAR OFFSET DAYLIGHT *hh = response[0]; *minute = response[1]; *ss = response[2]; *mm = response[3]; *dd = response[4]; *yy = response[5] + 2000; *utc_hours = response[6]; if (*utc_hours > 12) *utc_hours -= 256; ln_zonedate localTime; ln_date utcTime; localTime.years = *yy; localTime.months = *mm; localTime.days = *dd; localTime.hours = *hh; localTime.minutes = *minute; localTime.seconds = *ss; localTime.gmtoff = *utc_hours * 3600; ln_zonedate_to_date(&localTime, &utcTime); *yy = utcTime.years; *mm = utcTime.months; *dd = utcTime.days; *hh = utcTime.hours; *minute = utcTime.minutes; *ss = utcTime.seconds; return true; } bool CelestronDriver::is_slewing() { set_sim_response("%d#", sim_data.isSlewing); if (!send_command("L", 1, response, 2, true, true)) return false; return response[0] != '0'; } bool CelestronDriver::get_track_mode(CELESTRON_TRACK_MODE *mode) { set_sim_response("\02#"); if (!send_command("t", 1, response, 2, true, false)) return false; *mode = ((CELESTRON_TRACK_MODE)response[0]); return true; } bool CelestronDriver::set_track_mode(CELESTRON_TRACK_MODE mode) { char cmd[3]; sprintf(cmd, "T%c", mode); set_sim_response("#"); return send_command(cmd, 2, response, 1, false, true); } bool CelestronDriver::hibernate() { return send_command("x#", 2, response, 0, true, true); } bool CelestronDriver::wakeup() { set_sim_response("#"); return send_command("y#", 2, response, 1, true, true); } libindi/drivers/telescope/skywatcherAPIMount.h0000664000175000017500000001504213263645557021047 0ustar jasemjasem/*! * \file skywatcherAPIMount.h * * \author Roger James * \author Gerry Rozema * \author Jean-Luc Geehalel * \date 13th November 2013 * * This file contains the definitions for a C++ implementatiom of a INDI telescope driver using the Skywatcher API. * It is based on work from three sources. * A C++ implementation of the API by Roger James. * The indi_eqmod driver by Jean-Luc Geehalel. * The synscanmount driver by Gerry Rozema. */ #pragma once #include "indiguiderinterface.h" #include "skywatcherAPI.h" #include "alignment/AlignmentSubsystemForDrivers.h" typedef enum { PARK_COUNTERCLOCKWISE = 0, PARK_CLOCKWISE } ParkDirection_t; typedef enum { PARK_NORTH = 0, PARK_EAST, PARK_SOUTH, PARK_WEST } ParkPosition_t; struct GuidingPulse { double DeltaAlt { 0 }; double DeltaAz { 0 }; int Duration { 0 }; int OriginalDuration { 0 }; }; class SkywatcherAPIMount : public SkywatcherAPI, public INDI::Telescope, public INDI::GuiderInterface, public INDI::AlignmentSubsystem::AlignmentSubsystemForDrivers { public: SkywatcherAPIMount(); virtual ~SkywatcherAPIMount() = default; // overrides of base class virtual functions virtual bool Abort() override; virtual bool Handshake() override; virtual const char *getDefaultName() override; virtual bool Goto(double ra, double dec) override; virtual bool initProperties() override; virtual void ISGetProperties(const char *dev) override; virtual bool ISNewBLOB(const char *dev, const char *name, int sizes[], int blobsizes[], char *blobs[], char *formats[], char *names[], int n) override; virtual bool ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) override; virtual bool ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) override; virtual bool ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n) override; double GetSlewRate(); virtual bool MoveNS(INDI_DIR_NS dir, TelescopeMotionCommand command) override; virtual bool MoveWE(INDI_DIR_WE dir, TelescopeMotionCommand command) override; double GetParkDeltaAz(ParkDirection_t target_direction, ParkPosition_t target_position); virtual bool Park() override; virtual bool UnPark() override; virtual bool ReadScopeStatus() override; virtual bool saveConfigItems(FILE *fp) override; virtual bool Sync(double ra, double dec) override; virtual void TimerHit() override; virtual bool updateLocation(double latitude, double longitude, double elevation) override; virtual bool updateProperties() override; virtual IPState GuideNorth(float ms) override; virtual IPState GuideSouth(float ms) override; virtual IPState GuideEast(float ms) override; virtual IPState GuideWest(float ms) override; private: void CalculateGuidePulses(); void ResetGuidePulses(); void ConvertGuideCorrection(double delta_ra, double delta_dec, double &delta_alt, double &delta_az); void UpdateScopeConfigSwitch(); // Overrides for the pure virtual functions in SkyWatcherAPI virtual int skywatcher_tty_read(int fd, char *buf, int nbytes, int timeout, int *nbytes_read) override; virtual int skywatcher_tty_write(int fd, const char *buffer, int nbytes, int *nbytes_written) override; void SkywatcherMicrostepsFromTelescopeDirectionVector( const INDI::AlignmentSubsystem::TelescopeDirectionVector TelescopeDirectionVector, long &Axis1Microsteps, long &Axis2Microsteps); const INDI::AlignmentSubsystem::TelescopeDirectionVector TelescopeDirectionVectorFromSkywatcherMicrosteps(long Axis1Microsteps, long Axis2Microsteps); void UpdateDetailedMountInformation(bool InformClient); // Properties static constexpr const char *DetailedMountInfoPage { "Detailed Mount Information" }; enum { MOTOR_CONTROL_FIRMWARE_VERSION, MOUNT_CODE, MOUNT_NAME, IS_DC_MOTOR }; IText BasicMountInfo[4] {}; ITextVectorProperty BasicMountInfoV; enum { MICROSTEPS_PER_REVOLUTION, STEPPER_CLOCK_FREQUENCY, HIGH_SPEED_RATIO, MICROSTEPS_PER_WORM_REVOLUTION }; INumber AxisOneInfo[4]; INumberVectorProperty AxisOneInfoV; INumber AxisTwoInfo[4]; INumberVectorProperty AxisTwoInfoV; enum { FULL_STOP, SLEWING, SLEWING_TO, SLEWING_FORWARD, HIGH_SPEED, NOT_INITIALISED }; ISwitch AxisOneState[6]; ISwitchVectorProperty AxisOneStateV; ISwitch AxisTwoState[6]; ISwitchVectorProperty AxisTwoStateV; enum { RAW_MICROSTEPS, MICROSTEPS_PER_ARCSEC, OFFSET_FROM_INITIAL, DEGREES_FROM_INITIAL }; INumber AxisOneEncoderValues[4]; INumberVectorProperty AxisOneEncoderValuesV; INumber AxisTwoEncoderValues[4]; INumberVectorProperty AxisTwoEncoderValuesV; // A switch for silent/highspeed slewing modes enum { SLEW_SILENT, SLEW_NORMAL }; ISwitch SlewModes[2]; ISwitchVectorProperty SlewModesSP; // A switch for SoftPEC modes enum { SOFTPEC_ENABLED, SOFTPEC_DISABLED }; ISwitch SoftPECModes[2]; ISwitchVectorProperty SoftPECModesSP; // SoftPEC value for tracking mode INumber SoftPecN; INumberVectorProperty SoftPecNP; // Guiding rates (RA/Dec) INumber GuidingRatesN[2]; INumberVectorProperty GuidingRatesNP; // A switch for park movement directions (clockwise/counterclockwise) ISwitch ParkMovementDirection[2]; ISwitchVectorProperty ParkMovementDirectionSP; // A switch for park positions ISwitch ParkPosition[4]; ISwitchVectorProperty ParkPositionSP; // A switch for unpark positions ISwitch UnparkPosition[4]; ISwitchVectorProperty UnparkPositionSP; // Tracking ln_equ_posn CurrentTrackingTarget { 0, 0 }; long OldTrackingTarget[2] { 0, 0 }; struct ln_hrz_posn CurrentAltAz { 0, 0 }; struct ln_hrz_posn TrackedAltAz { 0, 0 }; bool ResetTrackingSeconds { false }; int TrackingMsecs { 0 }; double GuideDeltaAlt { 0 }; double GuideDeltaAz { 0 }; int TimeoutDuration { 500 }; /// Save the serial port name std::string SerialPortName; /// Recover after disconnection bool RecoverAfterReconnection { false }; GuidingPulse NorthPulse; GuidingPulse WestPulse; std::vector GuidingPulses; #ifdef USE_INITIAL_JULIAN_DATE double InitialJulianDate { 0 }; #endif }; libindi/drivers/telescope/lx200generic.cpp0000664000175000017500000001624613263645557020052 0ustar jasemjasem#if 0 LX200 Generic Copyright (C) 2003 - 2017 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 2013 - 10 - 27: Updated driver to use INDI::Telescope (JM) 2015 - 11 - 25: Use variable POLLMS instead of static POLLMS #endif #include "lx200generic.h" #include "lx200_10micron.h" #include "lx200_16.h" #include "lx200_OnStep.h" #include "lx200ap_experimental.h" #include "lx200ap_gtocp2.h" #include "lx200ap.h" #include "lx200classic.h" #include "lx200fs2.h" #include "lx200gemini.h" #include "lx200pulsar2.h" #include "lx200ss2000pc.h" #include "lx200zeq25.h" #include "lx200gotonova.h" #include "ioptronHC8406.h" #include #include #include #include // We declare an auto pointer to LX200Generic. std::unique_ptr telescope; /* There is _one_ binary for all LX200 drivers, but each binary is renamed ** to its device name (i.e. lx200gps, lx200_16..etc). The main function will ** fetch from std args the binary name and ISInit will create the apporpiate ** device afterwards. If the binary name does not match any known devices, ** we simply create a generic device. */ extern char *me; #define LX200_TRACK 0 #define LX200_SYNC 1 /* send client definitions of all properties */ void ISInit() { static int isInit = 0; if (isInit) return; isInit = 1; if (strstr(me, "indi_lx200classic")) { IDLog("initializing from LX200 classic device...\n"); if (telescope.get() == 0) telescope.reset(new LX200Classic()); } if (strstr(me, "indi_lx200_OnStep")) { IDLog("initializing from LX200 OnStep device...\n"); if (telescope.get() == 0) telescope.reset(new LX200_OnStep()); } else if (strstr(me, "indi_lx200gps")) { IDLog("initializing from LX200 GPS device...\n"); if (telescope.get() == 0) telescope.reset(new LX200GPS()); } else if (strstr(me, "indi_lx200_16")) { IDLog("Initializing from LX200 16 device...\n"); if (telescope.get() == 0) telescope.reset(new LX200_16()); } else if (strstr(me, "indi_lx200autostar")) { IDLog("initializing from Autostar device...\n"); if (telescope.get() == 0) telescope.reset(new LX200Autostar()); } else if (strstr(me, "indi_lx200ap_experimental")) { IDLog("initializing from Astrophysics Experiemtal device...\n"); if (telescope.get() == 0) telescope.reset(new LX200AstroPhysicsExperimental()); } else if (strstr(me, "indi_lx200ap_gtocp2")) { IDLog("initializing from Astrophysics GTOCP2 device...\n"); if (telescope.get() == 0) telescope.reset(new LX200AstroPhysicsGTOCP2()); } else if (strstr(me, "indi_lx200ap")) { IDLog("initializing from Astrophysics device...\n"); if (telescope.get() == 0) telescope.reset(new LX200AstroPhysics()); } else if (strstr(me, "indi_lx200gemini")) { IDLog("initializing from Losmandy Gemini device...\n"); if (telescope.get() == 0) telescope.reset(new LX200Gemini()); } else if (strstr(me, "indi_lx200zeq25")) { IDLog("initializing from ZEQ25 device...\n"); if (telescope.get() == 0) telescope.reset(new LX200ZEQ25()); } else if (strstr(me, "indi_lx200gotonova")) { IDLog("initializing from GotoNova device...\n"); if (telescope.get() == 0) telescope.reset(new LX200GotoNova()); } else if (strstr(me, "indi_ioptronHC8406")) { IDLog("initializing from ioptron telescope Hand Controller HC8406 device...\n"); if (telescope.get() == 0) telescope.reset(new ioptronHC8406()); } else if (strstr(me, "indi_lx200pulsar2")) { IDLog("initializing from pulsar2 device...\n"); if (telescope.get() == 0) telescope.reset(new LX200Pulsar2()); } else if (strstr(me, "indi_lx200ss2000pc")) { IDLog("initializing from skysensor2000pc device...\n"); if (telescope.get() == 0) telescope.reset(new LX200SS2000PC()); } else if (strstr(me, "indi_lx200fs2")) { IDLog("initializing from Astro-Electronic FS-2...\n"); if (telescope.get() == 0) telescope.reset(new LX200FS2()); } else if (strstr(me, "indi_lx200_10micron")) { IDLog("initializing for 10Micron mount...\n"); if (telescope.get() == 0) telescope.reset(new LX200_10MICRON()); } // be nice and give them a generic device else if (telescope.get() == 0) telescope.reset(new LX200Generic()); } void ISGetProperties(const char *dev) { ISInit(); telescope->ISGetProperties(dev); } void ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) { ISInit(); telescope->ISNewSwitch(dev, name, states, names, n); } void ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n) { ISInit(); telescope->ISNewText(dev, name, texts, names, n); } void ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) { ISInit(); telescope->ISNewNumber(dev, name, values, names, n); } void ISNewBLOB(const char *dev, const char *name, int sizes[], int blobsizes[], char *blobs[], char *formats[], char *names[], int n) { INDI_UNUSED(dev); INDI_UNUSED(name); INDI_UNUSED(sizes); INDI_UNUSED(blobsizes); INDI_UNUSED(blobs); INDI_UNUSED(formats); INDI_UNUSED(names); INDI_UNUSED(n); } void ISSnoopDevice(XMLEle *root) { ISInit(); telescope->ISSnoopDevice(root); } /************************************************** *** LX200 Generic Implementation ***************************************************/ LX200Generic::LX200Generic() { setVersion(2, 0); currentSiteNum = 1; trackingMode = LX200_TRACK_SIDEREAL; GuideNSTID = 0; GuideWETID = 0; DBG_SCOPE = INDI::Logger::getInstance().addDebugLevel("Scope Verbose", "SCOPE"); setLX200Capability(LX200_HAS_FOCUS | LX200_HAS_TRACKING_FREQ | LX200_HAS_ALIGNMENT_TYPE | LX200_HAS_SITES | LX200_HAS_PULSE_GUIDING); SetTelescopeCapability(TELESCOPE_CAN_PARK | TELESCOPE_CAN_SYNC | TELESCOPE_CAN_GOTO | TELESCOPE_CAN_ABORT | TELESCOPE_HAS_TIME | TELESCOPE_HAS_LOCATION | TELESCOPE_HAS_TRACK_MODE, 4); LOG_DEBUG("Initializing from Generic LX200 device..."); } libindi/drivers/telescope/lx200fs2.cpp0000664000175000017500000001663213263645557017127 0ustar jasemjasem/* Astro-Electronic FS-2 Driver Copyright (C) 2015 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 */ #include "lx200fs2.h" #include "indicom.h" #include #include #include LX200FS2::LX200FS2() : LX200Generic() { setVersion(2, 1); SetTelescopeCapability( TELESCOPE_CAN_PARK | TELESCOPE_CAN_SYNC | TELESCOPE_CAN_GOTO | TELESCOPE_HAS_LOCATION | TELESCOPE_CAN_ABORT, 4); } bool LX200FS2::initProperties() { LX200Generic::initProperties(); IUFillNumber(&SlewAccuracyN[0], "SlewRA", "RA (arcmin)", "%10.6m", 0., 60., 1., 3.0); IUFillNumber(&SlewAccuracyN[1], "SlewDEC", "Dec (arcmin)", "%10.6m", 0., 60., 1., 3.0); IUFillNumberVector(&SlewAccuracyNP, SlewAccuracyN, NARRAY(SlewAccuracyN), getDeviceName(), "Slew Accuracy", "", OPTIONS_TAB, IP_RW, 0, IPS_IDLE); SetParkDataType(PARK_AZ_ALT); return true; } bool LX200FS2::updateProperties() { INDI::Telescope::updateProperties(); if (isConnected()) { defineSwitch(&SlewRateSP); defineNumber(&SlewAccuracyNP); if (InitPark()) { // If loading parking data is successful, we just set the default parking values. SetAxis1ParkDefault(0); SetAxis2ParkDefault(LocationN[LOCATION_LATITUDE].value); } else { // Otherwise, we set all parking data to default in case no parking data is found. SetAxis1Park(0); SetAxis2Park(LocationN[LOCATION_LATITUDE].value); SetAxis1ParkDefault(0); SetAxis2ParkDefault(LocationN[LOCATION_LATITUDE].value); } } else { deleteProperty(SlewRateSP.name); deleteProperty(SlewAccuracyNP.name); } return true; } bool LX200FS2::ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) { if (dev != nullptr && strcmp(dev, getDeviceName()) == 0) { if (!strcmp(name, SlewAccuracyNP.name)) { if (IUUpdateNumber(&SlewAccuracyNP, values, names, n) < 0) return false; SlewAccuracyNP.s = IPS_OK; if (SlewAccuracyN[0].value < 3 || SlewAccuracyN[1].value < 3) IDSetNumber(&SlewAccuracyNP, "Warning: Setting the slew accuracy too low may result in a dead lock"); IDSetNumber(&SlewAccuracyNP, nullptr); return true; } } return LX200Generic::ISNewNumber(dev, name, values, names, n); } const char *LX200FS2::getDefaultName() { return (const char *)"Astro-Electronic FS-2"; } bool LX200FS2::isSlewComplete() { const double dx = targetRA - currentRA; const double dy = targetDEC - currentDEC; return fabs(dx) <= (SlewAccuracyN[0].value / (900.0)) && fabs(dy) <= (SlewAccuracyN[1].value / 60.0); } bool LX200FS2::checkConnection() { return true; } bool LX200FS2::saveConfigItems(FILE *fp) { INDI::Telescope::saveConfigItems(fp); IUSaveConfigNumber(fp, &SlewAccuracyNP); return true; } bool LX200FS2::Park() { double parkAZ = GetAxis1Park(); double parkAlt = GetAxis2Park(); char AzStr[16], AltStr[16]; fs_sexa(AzStr, parkAZ, 2, 3600); fs_sexa(AltStr, parkAlt, 2, 3600); LOGF_DEBUG("Parking to Az (%s) Alt (%s)...", AzStr, AltStr); ln_hrz_posn horizontalPos; // Libnova south = 0, west = 90, north = 180, east = 270 horizontalPos.az = parkAZ + 180; if (horizontalPos.az >= 360) horizontalPos.az -= 360; horizontalPos.alt = parkAlt; ln_lnlat_posn observer; observer.lat = LocationN[LOCATION_LATITUDE].value; observer.lng = LocationN[LOCATION_LONGITUDE].value; if (observer.lng > 180) observer.lng -= 360; ln_equ_posn equatorialPos; ln_get_equ_from_hrz(&horizontalPos, &observer, ln_get_julian_from_sys(), &equatorialPos); char RAStr[16], DEStr[16]; fs_sexa(RAStr, equatorialPos.ra / 15.0, 2, 3600); fs_sexa(DEStr, equatorialPos.dec, 2, 3600); LOGF_DEBUG("Parking to RA (%s) DEC (%s)...", RAStr, DEStr); if (Goto(equatorialPos.ra / 15.0, equatorialPos.dec)) { TrackState = SCOPE_PARKING; LOG_INFO("Parking is in progress..."); return true; } else return false; } bool LX200FS2::UnPark() { double parkAZ = GetAxis1Park(); double parkAlt = GetAxis2Park(); char AzStr[16], AltStr[16]; fs_sexa(AzStr, parkAZ, 2, 3600); fs_sexa(AltStr, parkAlt, 2, 3600); LOGF_DEBUG("Unparking from Az (%s) Alt (%s)...", AzStr, AltStr); ln_hrz_posn horizontalPos; // Libnova south = 0, west = 90, north = 180, east = 270 horizontalPos.az = parkAZ + 180; if (horizontalPos.az >= 360) horizontalPos.az -= 360; horizontalPos.alt = parkAlt; ln_lnlat_posn observer; observer.lat = LocationN[LOCATION_LATITUDE].value; observer.lng = LocationN[LOCATION_LONGITUDE].value; if (observer.lng > 180) observer.lng -= 360; ln_equ_posn equatorialPos; ln_get_equ_from_hrz(&horizontalPos, &observer, ln_get_julian_from_sys(), &equatorialPos); char RAStr[16], DEStr[16]; fs_sexa(RAStr, equatorialPos.ra / 15.0, 2, 3600); fs_sexa(DEStr, equatorialPos.dec, 2, 3600); LOGF_DEBUG("Syncing to parked coordinates RA (%s) DEC (%s)...", RAStr, DEStr); if (Sync(equatorialPos.ra / 15.0, equatorialPos.dec)) { SetParked(false); return true; } else return false; } bool LX200FS2::SetCurrentPark() { ln_hrz_posn horizontalPos; // Libnova south = 0, west = 90, north = 180, east = 270 ln_lnlat_posn observer; observer.lat = LocationN[LOCATION_LATITUDE].value; observer.lng = LocationN[LOCATION_LONGITUDE].value; if (observer.lng > 180) observer.lng -= 360; ln_equ_posn equatorialPos; equatorialPos.ra = currentRA * 15; equatorialPos.dec = currentDEC; ln_get_hrz_from_equ(&equatorialPos, &observer, ln_get_julian_from_sys(), &horizontalPos); double parkAZ = horizontalPos.az - 180; if (parkAZ < 0) parkAZ += 360; double parkAlt = horizontalPos.alt; char AzStr[16], AltStr[16]; fs_sexa(AzStr, parkAZ, 2, 3600); fs_sexa(AltStr, parkAlt, 2, 3600); LOGF_DEBUG("Setting current parking position to coordinates Az (%s) Alt (%s)...", AzStr, AltStr); SetAxis1Park(parkAZ); SetAxis2Park(parkAlt); return true; } bool LX200FS2::SetDefaultPark() { // By defualt azimuth 0 SetAxis1Park(0); // Altitude = latitude of observer SetAxis2Park(LocationN[LOCATION_LATITUDE].value); return true; } bool LX200FS2::updateLocation(double latitude, double longitude, double elevation) { INDI_UNUSED(latitude); INDI_UNUSED(longitude); INDI_UNUSED(elevation); return true; } libindi/drivers/telescope/skycommander.h0000664000175000017500000000230213263645557017775 0ustar jasemjasem/******************************************************************************* Copyright(c) 2017 Jasem Mutlaq. All rights reserved. Simple SkyCommander DSC Driver This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2 as published by the Free Software Foundation. . 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 Library General Public License for more details. . You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. *******************************************************************************/ #pragma once #include "inditelescope.h" class SkyCommander : public INDI::Telescope { public: SkyCommander(); virtual ~SkyCommander() = default; protected: virtual const char *getDefaultName(); virtual bool Handshake(); virtual bool ReadScopeStatus(); }; libindi/drivers/telescope/lx200telescope.h0000664000175000017500000001416013263645557020057 0ustar jasemjasem#ifndef LX200TELESCOPE_H #define LX200TELESCOPE_H #pragma once /* * Standard LX200 implementation. Copyright (C) 2003 - 2018 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 */ #include "indiguiderinterface.h" #include "inditelescope.h" class LX200Telescope : public INDI::Telescope, public INDI::GuiderInterface { public: LX200Telescope() = default; virtual ~LX200Telescope() = default; /** * \struct LX200Capability * \brief Holds properties of LX200 Generic that might be used by child classes */ enum { LX200_HAS_FOCUS = 1 << 0, /** Define focus properties */ LX200_HAS_TRACKING_FREQ = 1 << 1, /** Define Tracking Frequency */ LX200_HAS_ALIGNMENT_TYPE = 1 << 2, /** Define Alignment Type */ LX200_HAS_SITES = 1 << 3, /** Define Sites */ LX200_HAS_PULSE_GUIDING = 1 << 4, /** Define Pulse Guiding */ } LX200Capability; uint32_t getLX200Capability() const { return genericCapability; } void setLX200Capability(uint32_t cap) { genericCapability = cap; } virtual const char *getDefaultName() override; virtual const char *getDriverName() override; virtual bool Handshake() override; virtual bool ReadScopeStatus() override; virtual void ISGetProperties(const char *dev) override; virtual bool initProperties() override; virtual bool updateProperties() override; virtual bool ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) override; virtual bool ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) override; virtual bool ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n) override; void updateFocusTimer(); void guideTimeout(); protected: // Slew Rate virtual bool SetSlewRate(int index) override; // Track Mode (Sidereal, Solar..etc) virtual bool SetTrackMode(uint8_t mode) override; // NSWE Motion Commands virtual bool MoveNS(INDI_DIR_NS dir, TelescopeMotionCommand command) override; virtual bool MoveWE(INDI_DIR_WE dir, TelescopeMotionCommand command) override; // Abort ALL motion virtual bool Abort() override; // Time and Location virtual bool updateTime(ln_date *utc, double utc_offset) override; virtual bool updateLocation(double latitude, double longitude, double elevation) override; // Guide Commands virtual IPState GuideNorth(float ms) override; virtual IPState GuideSouth(float ms) override; virtual IPState GuideEast(float ms) override; virtual IPState GuideWest(float ms) override; // Guide Pulse Commands virtual int SendPulseCmd(int direction, int duration_msec); // Goto virtual bool Goto(double ra, double dec) override; // Is slew over? virtual bool isSlewComplete(); // Park Mount virtual bool Park() override; // Sync coordinates virtual bool Sync(double ra, double dec) override; // Check if mount is responsive virtual bool checkConnection(); // Save properties in config file virtual bool saveConfigItems(FILE *fp) override; // Action to perform when Debug is turned on or off virtual void debugTriggered(bool enable) override; // Initial function to get data after connection is successful virtual void getBasicData(); // Get local calender date (NOT UTC) from mount. Expected format is YYYY-MM-DD virtual bool getLocalDate(char *dateString); virtual bool setLocalDate(uint8_t days, uint8_t months, uint16_t years); // Get Local time in 24 hour format from mount. Expected format is HH:MM:SS virtual bool getLocalTime(char *timeString); virtual bool setLocalTime24(uint8_t hour, uint8_t minute, uint8_t second); // Return UTC Offset from mount in hours. virtual bool setUTCOffset(double offset); virtual bool getUTFOffset(double * offset); // Send slew error message to client void slewError(int slewCode); // Get mount alignment type (AltAz..etc) void getAlignment(); // Send Mount time and location settings to client bool sendScopeTime(); bool sendScopeLocation(); // Simulate Mount in simulation mode void mountSim(); static void updateFocusHelper(void *p); static void guideTimeoutHelper(void *p); int GuideNSTID; int GuideWETID; int timeFormat=-1; int currentSiteNum; int trackingMode; long guide_direction; bool sendTimeOnStartup=true, sendLocationOnStartup=true; unsigned int DBG_SCOPE; double JD; double targetRA, targetDEC; double currentRA, currentDEC; int MaxReticleFlashRate; /* Telescope Alignment Mode */ ISwitchVectorProperty AlignmentSP; ISwitch AlignmentS[3]; /* Tracking Frequency */ INumberVectorProperty TrackingFreqNP; INumber TrackFreqN[1]; /* Use pulse-guide commands */ ISwitchVectorProperty UsePulseCmdSP; ISwitch UsePulseCmdS[2]; /* Site Management */ ISwitchVectorProperty SiteSP; ISwitch SiteS[4]; /* Site Name */ ITextVectorProperty SiteNameTP; IText SiteNameT[1] {}; /* Focus motion */ ISwitchVectorProperty FocusMotionSP; ISwitch FocusMotionS[2]; /* Focus Timer */ INumberVectorProperty FocusTimerNP; INumber FocusTimerN[1]; /* Focus Mode */ ISwitchVectorProperty FocusModeSP; ISwitch FocusModeS[3]; uint32_t genericCapability; }; #endif // LX200TELESCOPE_H libindi/drivers/telescope/temmadriver.h0000664000175000017500000001021213263645557017617 0ustar jasemjasem/******************************************************************************* Copyright(c) 2016 Gerry Rozema. All rights reserved. Copyright(c) 2017 Jasem Mutlaq. All rights reserved. This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2 as published by the Free Software Foundation. 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 Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. *******************************************************************************/ #pragma once #include "indiguiderinterface.h" #include "inditelescope.h" #include "alignment/AlignmentSubsystemForDrivers.h" class TemmaMount : public INDI::Telescope, public INDI::GuiderInterface /*,public INDI::AlignmentSubsystem::AlignmentSubsystemForDrivers*/ { public: TemmaMount(); virtual ~TemmaMount() = default; virtual bool initProperties() override; virtual bool updateProperties() override; virtual const char *getDefaultName() override; virtual bool Handshake() override; virtual bool ReadScopeStatus() override; virtual bool Goto(double ra, double dec) override; virtual bool Sync(double ra, double dec) override; virtual bool Park() override; virtual bool UnPark() override; virtual bool Abort() override; //virtual bool SetSlewRate(int index) override; virtual bool MoveNS(INDI_DIR_NS dir, TelescopeMotionCommand command) override; virtual bool MoveWE(INDI_DIR_WE dir, TelescopeMotionCommand command) override; virtual bool updateLocation(double latitude, double longitude, double elevation) override; virtual bool SetCurrentPark() override; virtual bool SetDefaultPark() override; virtual bool ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) override; virtual bool ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) override; virtual bool ISNewBLOB(const char *dev, const char *name, int sizes[], int blobsizes[], char *blobs[], char *formats[], char *names[], int n) override; virtual bool ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n) override; // methods added for guider interface virtual IPState GuideNorth(float ms) override; virtual IPState GuideSouth(float ms) override; virtual IPState GuideEast(float ms) override; virtual IPState GuideWest(float ms) override; // Initial implementation doesn't need this one //virtual void GuideComplete(INDI_EQ_AXIS axis); private: //int TemmaRead(char *buf, int size); void mountSim(); bool GetVersion(); bool GetCoords(); // Send command to mount, and optionally read a response. CR LF is appended to the command. // Pass nullptr to response to skip reading the respone. // Response size must be 64 bytes (TEMMA_BUFFER). CR LF is removed from response. bool SendCommand(const char *cmd, char *response = nullptr); bool GetMotorStatus(); bool SetMotorStatus(bool enable); // LST & Latitude functions bool SetLST(); bool GetLST(double &lst); bool SetLattitude(double lat); bool GetLattitude(double &lat); //ln_equ_posn TelescopeToSky(double ra, double dec); //ln_equ_posn SkyToTelescope(double ra, double dec); //bool TemmaConnect(const char *port); double currentRA=0, currentDEC=0, targetRA=0, targetDEC=0, alignedRA=0, alignedDEC=0; bool MotorStatus { false }; bool TemmaInitialized { false }; double Longitude { 0 }; double Latitude { 0 }; int SlewRate { 1 }; bool SlewActive { false }; unsigned char Slewbits { 0 }; //INumber GuideRateN[2]; //INumberVectorProperty GuideRateNP; }; libindi/drivers/telescope/ieqpro.h0000664000175000017500000000746113263645557016613 0ustar jasemjasem/* INDI IEQ Pro driver Copyright (C) 2015 Jasem Mutlaq 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 */ #pragma once #include "ieqprodriver.h" #include "indiguiderinterface.h" #include "inditelescope.h" class IEQPro : public INDI::Telescope, public INDI::GuiderInterface { public: IEQPro(); ~IEQPro() = default; virtual bool ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) override; virtual bool ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) override; protected: virtual const char *getDefaultName() override; virtual bool Handshake() override; virtual bool initProperties() override; virtual bool updateProperties() override; virtual bool ReadScopeStatus() override; virtual bool MoveNS(INDI_DIR_NS dir, TelescopeMotionCommand command) override; virtual bool MoveWE(INDI_DIR_WE dir, TelescopeMotionCommand command) override; virtual bool saveConfigItems(FILE *fp) override; virtual bool Park() override; virtual bool UnPark() override; virtual bool Sync(double ra, double dec) override; virtual bool Goto(double, double) override; virtual bool Abort() override; virtual bool updateTime(ln_date *utc, double utc_offset) override; virtual bool updateLocation(double latitude, double longitude, double elevation) override; virtual void debugTriggered(bool enable) override; virtual void simulationTriggered(bool enable) override; // Parking virtual bool SetCurrentPark() override; virtual bool SetDefaultPark() override; // Track Mode virtual bool SetTrackMode(uint8_t mode) override; // Track Rate virtual bool SetTrackRate(double raRate, double deRate) override; // Track On/Off virtual bool SetTrackEnabled(bool enabled) override; // Slew Rate virtual bool SetSlewRate(int index) override; // Sim void mountSim(); // Guide virtual IPState GuideNorth(float ms) override; virtual IPState GuideSouth(float ms) override; virtual IPState GuideEast(float ms) override; virtual IPState GuideWest(float ms) override; private: /** * @brief getStartupData Get initial mount info on startup. */ void getStartupData(); /* Firmware */ IText FirmwareT[5] {}; ITextVectorProperty FirmwareTP; /* Tracking Mode */ //ISwitchVectorProperty TrackModeSP; //ISwitch TrackModeS[4]; /* Custom Tracking Rate */ //INumber CustomTrackRateN[1]; //INumberVectorProperty CustomTrackRateNP; /* GPS Status */ ISwitch GPSStatusS[3]; ISwitchVectorProperty GPSStatusSP; /* Time Source */ ISwitch TimeSourceS[3]; ISwitchVectorProperty TimeSourceSP; /* Hemisphere */ ISwitch HemisphereS[2]; ISwitchVectorProperty HemisphereSP; /* Home Control */ ISwitch HomeS[3]; ISwitchVectorProperty HomeSP; /* Guide Rate */ INumber GuideRateN[1]; INumberVectorProperty GuideRateNP; unsigned int DBG_SCOPE; double currentRA, currentDEC; double targetRA, targetDEC; IEQInfo scopeInfo; FirmwareInfo firmwareInfo; }; libindi/drivers/telescope/lx200ss2000pc.cpp0000664000175000017500000002356313263645557017710 0ustar jasemjasem/* SkySensor2000PC Copyright (C) 2015 Camiel Severijns 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 */ #include "lx200ss2000pc.h" #include "indicom.h" #include "lx200driver.h" #include #include #include #include #include const int LX200SS2000PC::ShortTimeOut = 2; // In seconds. const int LX200SS2000PC::LongTimeOut = 10; // In seconds. LX200SS2000PC::LX200SS2000PC(void) : LX200Generic() { setVersion(1, 1); setLX200Capability(LX200_HAS_PULSE_GUIDING); SetTelescopeCapability( TELESCOPE_CAN_SYNC | TELESCOPE_CAN_GOTO | TELESCOPE_CAN_ABORT | TELESCOPE_HAS_TIME | TELESCOPE_HAS_LOCATION, 4); } bool LX200SS2000PC::initProperties() { LX200Generic::initProperties(); IUFillNumber(&SlewAccuracyN[0], "SlewRA", "RA (arcmin)", "%10.6m", 0., 60., 1., 3.0); IUFillNumber(&SlewAccuracyN[1], "SlewDEC", "Dec (arcmin)", "%10.6m", 0., 60., 1., 3.0); IUFillNumberVector(&SlewAccuracyNP, SlewAccuracyN, NARRAY(SlewAccuracyN), getDeviceName(), "Slew Accuracy", "", OPTIONS_TAB, IP_RW, 0, IPS_IDLE); return true; } bool LX200SS2000PC::updateProperties() { LX200Generic::updateProperties(); if (isConnected()) { defineNumber(&SlewAccuracyNP); } else { deleteProperty(SlewAccuracyNP.name); } return true; } bool LX200SS2000PC::ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) { if (strcmp(dev, getDeviceName()) == 0) { if (!strcmp(name, SlewAccuracyNP.name)) { if (IUUpdateNumber(&SlewAccuracyNP, values, names, n) < 0) return false; SlewAccuracyNP.s = IPS_OK; if (SlewAccuracyN[0].value < 3 || SlewAccuracyN[1].value < 3) IDSetNumber(&SlewAccuracyNP, "Warning: Setting the slew accuracy too low may result in a dead lock"); IDSetNumber(&SlewAccuracyNP, nullptr); return true; } } return LX200Generic::ISNewNumber(dev, name, values, names, n); } bool LX200SS2000PC::saveConfigItems(FILE *fp) { LX200Generic::saveConfigItems(fp); IUSaveConfigNumber(fp, &SlewAccuracyNP); return true; } const char *LX200SS2000PC::getDefaultName(void) { return const_cast("SkySensor2000PC"); } bool LX200SS2000PC::updateTime(ln_date *utc, double utc_offset) { bool result = true; // This method is largely identical to the one in the LX200Generic class. // The difference is that it ensures that updates that require planetary // data to be recomputed by the SkySensor2000PC are only done when really // necessary because this takes quite some time. if (!isSimulation()) { result = false; struct ln_zonedate ltm; ln_date_to_zonedate(utc, <m, static_cast(utc_offset * 3600.0 + 0.5)); LOGF_DEBUG("New zonetime is %04d-%02d-%02d %02d:%02d:%06.3f (offset=%ld)", ltm.years, ltm.months, ltm.days, ltm.hours, ltm.minutes, ltm.seconds, ltm.gmtoff); JD = ln_get_julian_day(utc); LOGF_DEBUG("New JD is %f", JD); if (setLocalTime(PortFD, ltm.hours, ltm.minutes, static_cast(ltm.seconds + 0.5)) < 0) { LOG_ERROR("Error setting local time."); } else if (!setCalenderDate(ltm.years, ltm.months, ltm.days)) { LOG_ERROR("Error setting local date."); } // Meade defines UTC Offset as the offset ADDED to local time to yield UTC, which // is the opposite of the standard definition of UTC offset! else if (!setUTCOffset(-(utc_offset))) { LOG_ERROR("Error setting UTC Offset."); } else { LOG_INFO("Time updated."); result = true; } } return result; } void LX200SS2000PC::getBasicData(void) { if (!isSimulation()) checkLX200Format(PortFD); sendScopeLocation(); sendScopeTime(); } bool LX200SS2000PC::isSlewComplete() { const double dx = targetRA - currentRA; const double dy = targetDEC - currentDEC; return fabs(dx) <= (SlewAccuracyN[0].value / (900.0)) && fabs(dy) <= (SlewAccuracyN[1].value / 60.0); } bool LX200SS2000PC::getCalendarDate(int &year, int &month, int &day) { char date[16]; bool result = (getCommandString(PortFD, date, ":GC#") == 0); LOGF_DEBUG("LX200SS2000PC::getCalendarDate():: Date string from telescope: %s", date); if (result) { result = (sscanf(date, "%d%*c%d%*c%d", &month, &day, &year) == 3); // Meade format is MM/DD/YY LOGF_DEBUG("setCalenderDate: Date retrieved from telescope: %02d/%02d/%02d.", month, day, year); if (result) year += (year > 50 ? 1900 : 2000); // Year 50 or later is in the 20th century, anything less is in the 21st century. } return result; } bool LX200SS2000PC::setCalenderDate(int year, int month, int day) { // This method differs from the setCalenderDate function in lx200driver.cpp // in that it reads and checks the complete response from the SkySensor2000PC. // In addition, this method only sends the date when it differs from the date // of the SkySensor2000PC because the resulting update of the planetary data // takes quite some time. bool result = true; int ss_year = 0, ss_month = 0, ss_day = 0; const bool send_to_skysensor = (!getCalendarDate(ss_year, ss_month, ss_day) || year != ss_year || month != ss_month || day != ss_day); LOGF_DEBUG("LX200SS2000PC::setCalenderDate(): Driver date %02d/%02d/%02d, SS2000PC date %02d/%02d/%02d.", month, day, year, ss_month, ss_day, ss_year); if (send_to_skysensor) { char buffer[64]; int nbytes_written = 0; snprintf(buffer, sizeof(buffer), ":SC %02d/%02d/%02d#", month, day, (year % 100)); result = (tty_write_string(PortFD, buffer, &nbytes_written) == TTY_OK && nbytes_written == (int)strlen(buffer)); if (result) { int nbytes_read = 0; result = (tty_read(PortFD, buffer, 1, ShortTimeOut, &nbytes_read) == TTY_OK && nbytes_read == 1 && buffer[0] == '1'); if (result) { if (tty_read_section(PortFD, buffer, '#', ShortTimeOut, &nbytes_read) != TTY_OK || strncmp(buffer, "Updating planetary data#", 24) != 0) { LOGF_ERROR( "LX200SS2000PC::setCalenderDate(): Received unexpected first line '%s'.", buffer); result = false; } else if (tty_read_section(PortFD, buffer, '#', LongTimeOut, &nbytes_read) != TTY_OK && strncmp(buffer, " #", 24) != 0) { LOGF_ERROR( "LX200SS2000PC::setCalenderDate(): Received unexpected second line '%s'.", buffer); result = false; } } } } return result; } bool LX200SS2000PC::setUTCOffset(double offset) { bool result = true; int ss_timezone; const bool send_to_skysensor = (getUTCOffset(PortFD, &ss_timezone) != 0 || offset != ss_timezone); if (send_to_skysensor) { char temp_string[12]; snprintf(temp_string, sizeof(temp_string), ":SG %+03d#", static_cast(offset)); result = (setStandardProcedure(PortFD, temp_string) == 0); } return result; } bool LX200SS2000PC::updateLocation(double latitude, double longitude, double elevation) { INDI_UNUSED(elevation); if (isSimulation()) return true; if (latitude == 0.0 && longitude == 0.0) return true; if (setSiteLatitude(PortFD, latitude) < 0) { LOG_ERROR("Error setting site latitude coordinates"); } if (setSiteLongitude(PortFD, 360.0 - longitude) < 0) { LOG_ERROR("Error setting site longitude coordinates"); return false; } char slat[32], slong[32]; fs_sexa(slat, latitude, 3, 3600); fs_sexa(slong, longitude, 4, 3600); LOGF_INFO("Site location updated to Latitude: %.32s - Longitude: %.32s", slat, slong); return true; } // This override is needed, because the Sky Sensor 2000 PC requires a space // between the command its argument, unlike the 'standard' LX200 mounts, which // does not work on this mount. int LX200SS2000PC::setSiteLatitude(int fd, double Lat) { int d, m, s; char sign; char temp_string[32]; if (Lat > 0) sign = '+'; else sign = '-'; getSexComponents(Lat, &d, &m, &s); snprintf(temp_string, sizeof(temp_string), ":St %c%03d*%02d#", sign, d, m); return setStandardProcedure(fd, temp_string); } // This override is needed, because the Sky Sensor 2000 PC requires a space // between the command its argument, unlike the 'standard' LX200 mounts, which // does not work on this mount. int LX200SS2000PC::setSiteLongitude(int fd, double Long) { int d, m, s; char temp_string[32]; getSexComponents(Long, &d, &m, &s); snprintf(temp_string, sizeof(temp_string), ":Sg %03d*%02d#", d, m); return setStandardProcedure(fd, temp_string); } libindi/drivers/telescope/lx200apdriver.h0000664000175000017500000000527513263645557017717 0ustar jasemjasem/* LX200 AP Driver Copyright (C) 2007 Markus Wildi 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 */ #pragma once #define getAPDeclinationAxis(fd, x) getCommandString(fd, x, "#:pS#") #define getAPVersionNumber(fd, x) getCommandString(fd, x, "#:V#") #define setAPPark(fd) write(fd, "#:KA", 4) #define setAPUnPark(fd) write(fd, "#:PO", 4) #define setAPLongFormat(fd) write(fd, "#:U", 3) #define setAPClearBuffer(fd) write(fd, "#", 1) /* AP key pad manual startup sequence */ #define setAPBackLashCompensation(fd, x, y, z) setCommandXYZ(fd, x, y, z, "#:Br") #define setAPMotionStop(fd) write(fd, "#:Q", 3) #define AP_TRACKING_SIDEREAL 0 #define AP_TRACKING_SOLAR 1 #define AP_TRACKING_LUNAR 2 #define AP_TRACKING_CUSTOM 3 #define AP_TRACKING_OFF 4 #ifdef __cplusplus extern "C" { #endif void set_lx200ap_name(const char *deviceName, unsigned int debug_level); int check_lx200ap_connection(int fd); int getAPUTCOffset(int fd, double *value); int setAPObjectAZ(int fd, double az); int setAPObjectAlt(int fd, double alt); int setAPUTCOffset(int fd, double hours); int setAPSlewMode(int fd, int slewMode); int APSyncCM(int fd, char *matchedObject); int APSyncCMR(int fd, char *matchedObject); int selectAPMoveToRate(int fd, int moveToRate); int selectAPSlewRate(int fd, int slewRate); int selectAPTrackingMode(int fd, int trackMode); int selectAPGuideRate(int fd, int guideRate); int selectAPPECState(int fd, int pecstate); int swapAPButtons(int fd, int currentSwap); int setAPObjectRA(int fd, double ra); int setAPObjectDEC(int fd, double dec); int setAPSiteLongitude(int fd, double Long); int setAPSiteLatitude(int fd, double Lat); int setAPRATrackRate(int fd, double rate); int setAPDETrackRate(int fd, double rate); int APSendPulseCmd(int fd, int direction, int duration_msec); //int check_lx200ap_status(int fd, char *parkStatus, char *slewStatus); #ifdef __cplusplus } #endif libindi/drivers/telescope/lx200classic.cpp0000664000175000017500000002652713263645557020062 0ustar jasemjasem/* LX200 Classoc Copyright (C) 2003 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 */ #include "lx200classic.h" #include "lx200driver.h" #include #define LIBRARY_TAB "Library" LX200Classic::LX200Classic() : LX200Generic() { currentCatalog = LX200_STAR_C; currentSubCatalog = 0; MaxReticleFlashRate = 3; setVersion(1, 0); } const char *LX200Classic::getDefaultName() { return (const char *)"LX200 Classic"; } bool LX200Classic::initProperties() { LX200Generic::initProperties(); IUFillText(&ObjectInfoT[0], "Info", "", ""); IUFillTextVector(&ObjectInfoTP, ObjectInfoT, 1, getDeviceName(), "Object Info", "", MAIN_CONTROL_TAB, IP_RO, 0, IPS_IDLE); IUFillSwitch(&StarCatalogS[0], "Star", "", ISS_ON); IUFillSwitch(&StarCatalogS[1], "SAO", "", ISS_OFF); IUFillSwitch(&StarCatalogS[2], "GCVS", "", ISS_OFF); IUFillSwitchVector(&StarCatalogSP, StarCatalogS, 3, getDeviceName(), "Star Catalogs", "", LIBRARY_TAB, IP_RW, ISR_1OFMANY, 0, IPS_IDLE); IUFillSwitch(&DeepSkyCatalogS[0], "NGC", "", ISS_ON); IUFillSwitch(&DeepSkyCatalogS[1], "IC", "", ISS_OFF); IUFillSwitch(&DeepSkyCatalogS[2], "UGC", "", ISS_OFF); IUFillSwitch(&DeepSkyCatalogS[3], "Caldwell", "", ISS_OFF); IUFillSwitch(&DeepSkyCatalogS[4], "Arp", "", ISS_OFF); IUFillSwitch(&DeepSkyCatalogS[5], "Abell", "", ISS_OFF); IUFillSwitch(&DeepSkyCatalogS[6], "Messier", "", ISS_OFF); IUFillSwitchVector(&DeepSkyCatalogSP, DeepSkyCatalogS, 7, getDeviceName(), "Deep Sky Catalogs", "", LIBRARY_TAB, IP_RW, ISR_1OFMANY, 0, IPS_IDLE); IUFillSwitch(&SolarS[0], "Select", "Select item", ISS_ON); IUFillSwitch(&SolarS[1], "1", "Mercury", ISS_OFF); IUFillSwitch(&SolarS[2], "2", "Venus", ISS_OFF); IUFillSwitch(&SolarS[3], "3", "Moon", ISS_OFF); IUFillSwitch(&SolarS[4], "4", "Mars", ISS_OFF); IUFillSwitch(&SolarS[5], "5", "Jupiter", ISS_OFF); IUFillSwitch(&SolarS[6], "6", "Saturn", ISS_OFF); IUFillSwitch(&SolarS[7], "7", "Uranus", ISS_OFF); IUFillSwitch(&SolarS[8], "8", "Neptune", ISS_OFF); IUFillSwitch(&SolarS[9], "9", "Pluto", ISS_OFF); IUFillSwitchVector(&SolarSP, SolarS, 10, getDeviceName(), "SOLAR_SYSTEM", "Solar System", LIBRARY_TAB, IP_RW, ISR_1OFMANY, 0, IPS_IDLE); IUFillNumber(&ObjectNoN[0], "ObjectN", "Number", "%+03f", 1.0, 1000.0, 1.0, 0); IUFillNumberVector(&ObjectNoNP, ObjectNoN, 1, getDeviceName(), "Object Number", "", LIBRARY_TAB, IP_RW, 0, IPS_IDLE); IUFillNumber(&MaxSlewRateN[0], "maxSlew", "Rate", "%g", 2.0, 9.0, 1.0, 9.0); IUFillNumberVector(&MaxSlewRateNP, MaxSlewRateN, 1, getDeviceName(), "Max slew Rate", "", MOTION_TAB, IP_RW, 0, IPS_IDLE); IUFillNumber(&ElevationLimitN[0], "minAlt", "Speed", "%+03f", -90.0, 90.0, 0.0, 0.0); //azwing removed typo double %% in fromat IUFillNumber(&ElevationLimitN[1], "maxAlt", "Speed", "%+03f", -90.0, 90.0, 0.0, 0.0); IUFillNumberVector(&ElevationLimitNP, ElevationLimitN, 1, getDeviceName(), "Slew elevation Limit", "", MAIN_CONTROL_TAB, IP_RW, 0, IPS_IDLE); return true; } void LX200Classic::ISGetProperties(const char *dev) { if (dev != nullptr && strcmp(dev, getDeviceName()) != 0) return; LX200Generic::ISGetProperties(dev); /* if (isConnected()) { defineNumber(&ElevationLimitNP); defineText(&ObjectInfoTP); defineSwitch(&SolarSP); defineSwitch(&StarCatalogSP); defineSwitch(&DeepSkyCatalogSP); defineNumber(&ObjectNoNP); defineNumber(&MaxSlewRateNP); } */ } bool LX200Classic::updateProperties() { LX200Generic::updateProperties(); if (isConnected()) { defineNumber(&ElevationLimitNP); defineText(&ObjectInfoTP); defineSwitch(&SolarSP); defineSwitch(&StarCatalogSP); defineSwitch(&DeepSkyCatalogSP); defineNumber(&ObjectNoNP); defineNumber(&MaxSlewRateNP); return true; } else { deleteProperty(ElevationLimitNP.name); deleteProperty(ObjectInfoTP.name); deleteProperty(SolarSP.name); deleteProperty(StarCatalogSP.name); deleteProperty(DeepSkyCatalogSP.name); deleteProperty(ObjectNoNP.name); deleteProperty(MaxSlewRateNP.name); return true; } } bool LX200Classic::ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) { if (dev != nullptr && strcmp(dev, getDeviceName()) == 0) { if (!strcmp(name, ObjectNoNP.name)) { char object_name[256]={0}; if (selectCatalogObject(PortFD, currentCatalog, (int)values[0]) < 0) { ObjectNoNP.s = IPS_ALERT; IDSetNumber(&ObjectNoNP, "Failed to select catalog object."); return false; } getLX200RA(PortFD, &targetRA); getLX200DEC(PortFD, &targetDEC); ObjectNoNP.s = IPS_OK; IDSetNumber(&ObjectNoNP, "Object updated."); if (getObjectInfo(PortFD, object_name) < 0) IDMessage(getDeviceName(), "Getting object info failed."); else { IUSaveText(&ObjectInfoTP.tp[0], object_name); IDSetText(&ObjectInfoTP, nullptr); } Goto(targetRA, targetDEC); return true; } if (!strcmp(name, MaxSlewRateNP.name)) { if (setMaxSlewRate(PortFD, (int)values[0]) < 0) { MaxSlewRateNP.s = IPS_ALERT; IDSetNumber(&MaxSlewRateNP, "Error setting maximum slew rate."); return false; } MaxSlewRateNP.s = IPS_OK; MaxSlewRateNP.np[0].value = values[0]; IDSetNumber(&MaxSlewRateNP, nullptr); return true; } if (!strcmp(name, ElevationLimitNP.name)) { // new elevation limits double minAlt = 0, maxAlt = 0; int i, nset; for (nset = i = 0; i < n; i++) { INumber *altp = IUFindNumber(&ElevationLimitNP, names[i]); if (altp == &ElevationLimitN[0]) { minAlt = values[i]; nset += minAlt >= -90.0 && minAlt <= 90.0; } else if (altp == &ElevationLimitN[1]) { maxAlt = values[i]; nset += maxAlt >= -90.0 && maxAlt <= 90.0; } } if (nset == 2) { if (setMinElevationLimit(PortFD, (int)minAlt) < 0) { ElevationLimitNP.s = IPS_ALERT; IDSetNumber(&ElevationLimitNP, "Error setting elevation limit."); return false; } setMaxElevationLimit(PortFD, (int)maxAlt); ElevationLimitNP.np[0].value = minAlt; ElevationLimitNP.np[1].value = maxAlt; ElevationLimitNP.s = IPS_OK; IDSetNumber(&ElevationLimitNP, nullptr); return true; } else { ElevationLimitNP.s = IPS_IDLE; IDSetNumber(&ElevationLimitNP, "elevation limit missing or invalid."); return false; } } } return LX200Generic::ISNewNumber(dev, name, values, names, n); } bool LX200Classic::ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) { int index = 0; if (dev != nullptr && strcmp(dev, getDeviceName()) == 0) { // Star Catalog if (!strcmp(name, StarCatalogSP.name)) { IUResetSwitch(&StarCatalogSP); IUUpdateSwitch(&StarCatalogSP, states, names, n); index = IUFindOnSwitchIndex(&StarCatalogSP); currentCatalog = LX200_STAR_C; if (selectSubCatalog(PortFD, currentCatalog, index)) { currentSubCatalog = index; StarCatalogSP.s = IPS_OK; IDSetSwitch(&StarCatalogSP, nullptr); return true; } else { StarCatalogSP.s = IPS_IDLE; IDSetSwitch(&StarCatalogSP, "Catalog unavailable."); return false; } } // Deep sky catalog if (!strcmp(name, DeepSkyCatalogSP.name)) { IUResetSwitch(&DeepSkyCatalogSP); IUUpdateSwitch(&DeepSkyCatalogSP, states, names, n); index = IUFindOnSwitchIndex(&DeepSkyCatalogSP); if (index == LX200_MESSIER_C) { currentCatalog = index; DeepSkyCatalogSP.s = IPS_OK; IDSetSwitch(&DeepSkyCatalogSP, nullptr); } else currentCatalog = LX200_DEEPSKY_C; if (selectSubCatalog(PortFD, currentCatalog, index)) { currentSubCatalog = index; DeepSkyCatalogSP.s = IPS_OK; IDSetSwitch(&DeepSkyCatalogSP, nullptr); } else { DeepSkyCatalogSP.s = IPS_IDLE; IDSetSwitch(&DeepSkyCatalogSP, "Catalog unavailable"); return false; } return true; } // Solar system if (!strcmp(name, SolarSP.name)) { if (IUUpdateSwitch(&SolarSP, states, names, n) < 0) return false; index = IUFindOnSwitchIndex(&SolarSP); // We ignore the first option : "Select item" if (index == 0) { SolarSP.s = IPS_IDLE; IDSetSwitch(&SolarSP, nullptr); return true; } selectSubCatalog(PortFD, LX200_STAR_C, LX200_STAR); selectCatalogObject(PortFD, LX200_STAR_C, index + 900); ObjectNoNP.s = IPS_OK; SolarSP.s = IPS_OK; getObjectInfo(PortFD, ObjectInfoTP.tp[0].text); IDSetNumber(&ObjectNoNP, "Object updated."); IDSetSwitch(&SolarSP, nullptr); if (currentCatalog == LX200_STAR_C || currentCatalog == LX200_DEEPSKY_C) selectSubCatalog(PortFD, currentCatalog, currentSubCatalog); getObjectRA(PortFD, &targetRA); getObjectDEC(PortFD, &targetDEC); Goto(targetRA, targetDEC); return true; } } return LX200Generic::ISNewSwitch(dev, name, states, names, n); } libindi/drivers/telescope/lx200gotonova.cpp0000664000175000017500000005702213263645557020267 0ustar jasemjasem/* GotoNova INDI driver Copyright (C) 2017 Jasem Mutlaq 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 */ #include "lx200gotonova.h" #include "indicom.h" #include "lx200driver.h" #include #include #include #include #include /* Simulation Parameters */ #define SLEWRATE 1 /* slew rate, degrees/s */ #define SIDRATE 0.004178 /* sidereal rate, degrees/s */ #define GOTONOVA_TIMEOUT 5 /* timeout */ #define GOTONOVA_CALDATE_RESULT " # #" /* result of calendar date */ LX200GotoNova::LX200GotoNova() { setVersion(1, 0); setLX200Capability(LX200_HAS_FOCUS); SetTelescopeCapability(TELESCOPE_CAN_PARK | TELESCOPE_CAN_SYNC | TELESCOPE_CAN_GOTO | TELESCOPE_CAN_ABORT | TELESCOPE_HAS_TIME | TELESCOPE_HAS_LOCATION | TELESCOPE_HAS_TRACK_MODE, 4); } bool LX200GotoNova::initProperties() { LX200Generic::initProperties(); strcpy(SlewRateS[0].label, "16x"); strcpy(SlewRateS[1].label, "64x"); strcpy(SlewRateS[2].label, "256x"); strcpy(SlewRateS[3].label, "512x"); // Sync Type IUFillSwitch(&SyncCMRS[USE_REGULAR_SYNC], ":CM#", ":CM#", ISS_ON); IUFillSwitch(&SyncCMRS[USE_CMR_SYNC], ":CMR#", ":CMR#", ISS_OFF); IUFillSwitchVector(&SyncCMRSP, SyncCMRS, 2, getDeviceName(), "SYNCCMR", "Sync", MOTION_TAB, IP_RW, ISR_1OFMANY, 0, IPS_IDLE); // Park Position IUFillSwitch(&ParkPositionS[PS_NORTH_POLE], "North Pole", "", ISS_ON); IUFillSwitch(&ParkPositionS[PS_LEFT_VERTICAL], "Left and Vertical", "", ISS_OFF); IUFillSwitch(&ParkPositionS[PS_LEFT_HORIZON], "Left and Horizon", "", ISS_OFF); IUFillSwitch(&ParkPositionS[PS_RIGHT_VERTICAL], "Right and Vertical", "", ISS_OFF); IUFillSwitch(&ParkPositionS[PS_RIGHT_HORIZON], "Right and Horizon", "", ISS_OFF); IUFillSwitchVector(&ParkPositionSP, ParkPositionS, 5, getDeviceName(), "PARKING_POSITION", "Parking Position", SITE_TAB, IP_RW, ISR_1OFMANY, 0, IPS_IDLE); // Guide Rate IUFillSwitch(&GuideRateS[0], "1.0x", "", ISS_ON); IUFillSwitch(&GuideRateS[1], "0.8x", "", ISS_OFF); IUFillSwitch(&GuideRateS[2], "0.6x", "", ISS_OFF); IUFillSwitch(&GuideRateS[3], "0.4x", "", ISS_OFF); IUFillSwitchVector(&GuideRateSP, GuideRateS, 4, getDeviceName(), "GUIDE_RATE", "Guide Rate", MOTION_TAB, IP_RW, ISR_1OFMANY, 0, IPS_IDLE); // Track Mode -- We do not support Custom so let's just define the first 3 properties TrackModeSP.nsp = 3; return true; } bool LX200GotoNova::updateProperties() { LX200Generic::updateProperties(); if (isConnected()) { defineSwitch(&SyncCMRSP); defineSwitch(&ParkPositionSP); defineSwitch(&GuideRateSP); } else { deleteProperty(SyncCMRSP.name); deleteProperty(ParkPositionSP.name); deleteProperty(GuideRateSP.name); } return true; } const char *LX200GotoNova::getDefaultName() { return (const char *)"GotoNova"; } bool LX200GotoNova::checkConnection() { if (isSimulation()) return true; const struct timespec timeout = {0, 50000000L}; char initCMD[] = ":V#"; int errcode = 0; char errmsg[MAXRBUF]; char response[8]; int nbytes_read = 0; int nbytes_written = 0; LOG_DEBUG("Initializing IOptron using :V# CMD..."); for (int i = 0; i < 2; i++) { if ((errcode = tty_write(PortFD, initCMD, 3, &nbytes_written)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); LOGF_ERROR("%s", errmsg); nanosleep(&timeout, NULL); continue; } if ((errcode = tty_read_section(PortFD, response, '#', 3, &nbytes_read))) { tty_error_msg(errcode, errmsg, MAXRBUF); LOGF_ERROR("%s", errmsg); nanosleep(&timeout, NULL); continue; } if (nbytes_read > 0) { response[nbytes_read] = '\0'; LOGF_DEBUG("RES (%s)", response); if (!strcmp(response, "V1.00#")) return true; } nanosleep(&timeout, NULL); } return false; } bool LX200GotoNova::ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) { if (dev != nullptr && strcmp(dev, getDeviceName()) == 0) { // Park Position if (!strcmp(ParkPositionSP.name, name)) { int currentSwitch = IUFindOnSwitchIndex(&ParkPositionSP); IUUpdateSwitch(&ParkPositionSP, states, names, n); if (setGotoNovaParkPosition(IUFindOnSwitchIndex(&ParkPositionSP)) == TTY_OK) ParkPositionSP.s = IPS_OK; else { IUResetSwitch(&ParkPositionSP); ParkPositionS[currentSwitch].s = ISS_ON; ParkPositionSP.s = IPS_ALERT; } IDSetSwitch(&ParkPositionSP, nullptr); return true; } // Guide Rate if (!strcmp(GuideRateSP.name, name)) { int currentSwitch = IUFindOnSwitchIndex(&ParkPositionSP); IUUpdateSwitch(&GuideRateSP, states, names, n); if (setGotoNovaGuideRate(IUFindOnSwitchIndex(&GuideRateSP)) == TTY_OK) GuideRateSP.s = IPS_OK; else { IUResetSwitch(&GuideRateSP); GuideRateS[currentSwitch].s = ISS_ON; GuideRateSP.s = IPS_ALERT; } IDSetSwitch(&GuideRateSP, nullptr); return true; } // Sync type if (!strcmp(name, SyncCMRSP.name)) { IUResetSwitch(&SyncCMRSP); IUUpdateSwitch(&SyncCMRSP, states, names, n); IUFindOnSwitchIndex(&SyncCMRSP); SyncCMRSP.s = IPS_OK; IDSetSwitch(&SyncCMRSP, nullptr); return true; } } return LX200Generic::ISNewSwitch(dev, name, states, names, n); } bool LX200GotoNova::isSlewComplete() { int errcode = 0; char errmsg[MAXRBUF]; char response[8]; int nbytes_read = 0; int nbytes_written = 0; const char *cmd = ":SE?#"; LOGF_DEBUG("CMD (%s)", cmd); if ((errcode = tty_write(PortFD, cmd, strlen(cmd), &nbytes_written)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); LOGF_ERROR("%s", errmsg); return false; } if ((errcode = tty_read(PortFD, response, 1, 3, &nbytes_read))) { tty_error_msg(errcode, errmsg, MAXRBUF); LOGF_ERROR("%s", errmsg); return false; } if (nbytes_read > 0) { response[nbytes_read] = '\0'; LOGF_DEBUG("RES (%s)", response); tcflush(PortFD, TCIFLUSH); if (response[0] == '0') return true; else return false; } LOGF_ERROR("Only received #%d bytes, expected 1.", nbytes_read); return false; } void LX200GotoNova::getBasicData() { int guideRate=-1; int rc = getGotoNovaGuideRate(&guideRate); if (rc == TTY_OK) { IUResetSwitch(&GuideRateSP); GuideRateS[guideRate].s = ISS_ON; GuideRateSP.s = IPS_OK; IDSetSwitch(&GuideRateSP, nullptr); } } bool LX200GotoNova::Goto(double r, double d) { const struct timespec timeout = {0, 100000000L}; targetRA = r; targetDEC = d; char RAStr[64], DecStr[64]; fs_sexa(RAStr, targetRA, 2, 3600); fs_sexa(DecStr, targetDEC, 2, 3600); // If moving, let's stop it first. if (EqNP.s == IPS_BUSY) { if (!isSimulation() && abortSlew(PortFD) < 0) { AbortSP.s = IPS_ALERT; IDSetSwitch(&AbortSP, "Abort slew failed."); return false; } AbortSP.s = IPS_OK; EqNP.s = IPS_IDLE; IDSetSwitch(&AbortSP, "Slew aborted."); IDSetNumber(&EqNP, nullptr); if (MovementNSSP.s == IPS_BUSY || MovementWESP.s == IPS_BUSY) { MovementNSSP.s = MovementWESP.s = IPS_IDLE; EqNP.s = IPS_IDLE; IUResetSwitch(&MovementNSSP); IUResetSwitch(&MovementWESP); IDSetSwitch(&MovementNSSP, nullptr); IDSetSwitch(&MovementWESP, nullptr); } // sleep for 100 mseconds nanosleep(&timeout, NULL); } if (!isSimulation()) { if (setObjectRA(PortFD, targetRA) < 0 || (setObjectDEC(PortFD, targetDEC)) < 0) { EqNP.s = IPS_ALERT; IDSetNumber(&EqNP, "Error setting RA/DEC."); return false; } if (slewGotoNova() == 0) { EqNP.s = IPS_ALERT; IDSetNumber(&EqNP, "Error Slewing to JNow RA %s - DEC %s\n", RAStr, DecStr); slewError(1); return false; } } TrackState = SCOPE_SLEWING; EqNP.s = IPS_BUSY; LOGF_INFO("Slewing to RA: %s - DEC: %s", RAStr, DecStr); return true; } bool LX200GotoNova::Sync(double ra, double dec) { char syncString[256]; int syncType = IUFindOnSwitchIndex(&SyncCMRSP); if (!isSimulation()) { if (setObjectRA(PortFD, ra) < 0 || setObjectDEC(PortFD, dec) < 0) { EqNP.s = IPS_ALERT; IDSetNumber(&EqNP, "Error setting RA/DEC. Unable to Sync."); return false; } bool syncOK = true; switch (syncType) { case USE_REGULAR_SYNC: if (::Sync(PortFD, syncString) < 0) syncOK = false; break; case USE_CMR_SYNC: if (GotonovaSyncCMR(syncString) < 0) syncOK = false; break; default: break; } if (syncOK == false) { EqNP.s = IPS_ALERT; IDSetNumber(&EqNP, "Synchronization failed."); return false; } } currentRA = ra; currentDEC = dec; LOGF_DEBUG("%s Synchronization successful %s", (syncType == USE_REGULAR_SYNC ? "CM" : "CMR"), syncString); LOG_INFO("Synchronization successful."); EqNP.s = IPS_OK; NewRaDec(currentRA, currentDEC); return true; } int LX200GotoNova::GotonovaSyncCMR(char *matchedObject) { const struct timespec timeout = {0, 10000000L}; int error_type; int nbytes_write = 0; int nbytes_read = 0; LOGF_DEBUG("CMD <%s>", "#:CMR#"); if ((error_type = tty_write_string(PortFD, ":CMR#", &nbytes_write)) != TTY_OK) return error_type; if ((error_type = tty_read_section(PortFD, matchedObject, '#', 3, &nbytes_read)) != TTY_OK) return error_type; matchedObject[nbytes_read - 1] = '\0'; LOGF_DEBUG("RES <%s>", matchedObject); /* Sleep 10ms before flushing. This solves some issues with LX200 compatible devices. */ nanosleep(&timeout, NULL); tcflush(PortFD, TCIFLUSH); return 0; } int LX200GotoNova::slewGotoNova() { DEBUGF(DBG_SCOPE, "<%s>", __FUNCTION__); char slewNum[2]; int error_type; int nbytes_write = 0, nbytes_read = 0; DEBUGF(DBG_SCOPE, "CMD <%s>", ":MS#"); if ((error_type = tty_write_string(PortFD, ":MS#", &nbytes_write)) != TTY_OK) return error_type; error_type = tty_read(PortFD, slewNum, 1, 3, &nbytes_read); if (nbytes_read < 1) { DEBUGF(DBG_SCOPE, "RES ERROR <%d>", error_type); return error_type; } /* We don't need to read the string message, just return corresponding error code */ tcflush(PortFD, TCIFLUSH); DEBUGF(DBG_SCOPE, "RES <%c>", slewNum[0]); return slewNum[0]; } bool LX200GotoNova::SetSlewRate(int index) { if (isSimulation()) return true; char cmd[8]; int errcode = 0; char errmsg[MAXRBUF]; int nbytes_written = 0; snprintf(cmd, 8, ":RC%d#", index); LOGF_DEBUG("CMD (%s)", cmd); if ((errcode = tty_write(PortFD, cmd, strlen(cmd), &nbytes_written)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); LOGF_ERROR("%s", errmsg); return false; } return true; } bool LX200GotoNova::updateTime(ln_date *utc, double utc_offset) { struct ln_zonedate ltm; if (isSimulation()) return true; ln_date_to_zonedate(utc, <m, utc_offset * 3600.0); JD = ln_get_julian_day(utc); LOGF_DEBUG("New JD is %f", (float)JD); // Set Local Time if (setLocalTime(PortFD, ltm.hours, ltm.minutes, ltm.seconds) < 0) { LOG_ERROR("Error setting local time."); return false; } if (setCalenderDate(PortFD, ltm.days, ltm.months, ltm.years) < 0) { LOG_ERROR("Error setting local date."); return false; } if (setGotoNovaUTCOffset(utc_offset) < 0) { LOG_ERROR("Error setting UTC Offset."); return false; } return true; } int LX200GotoNova::setCalenderDate(int fd, int dd, int mm, int yy) { const struct timespec timeout = {0, 10000000L}; char read_buffer[16]; char response[67]; char good_result[] = GOTONOVA_CALDATE_RESULT; int error_type; int nbytes_write = 0, nbytes_read = 0; yy = yy % 100; snprintf(read_buffer, sizeof(read_buffer), ":SC %02d/%02d/%02d#", mm, dd, yy); DEBUGF(DBG_SCOPE, "CMD <%s>", read_buffer); tcflush(fd, TCIFLUSH); if ((error_type = tty_write_string(fd, read_buffer, &nbytes_write)) != TTY_OK) return error_type; error_type = tty_read(fd, response, sizeof(response), GOTONOVA_TIMEOUT, &nbytes_read); tcflush(fd, TCIFLUSH); if (nbytes_read < 1) { LOG_ERROR("Unable to read response"); return error_type; } response[nbytes_read] = '\0'; DEBUGF(DBG_SCOPE, "RES <%s>", response); if (strncmp(response, good_result, strlen(good_result)) == 0) { return 0; } /* Sleep 10ms before flushing. This solves some issues with LX200 compatible devices. */ nanosleep(&timeout, NULL); tcflush(fd, TCIFLUSH); LOGF_DEBUG("Set date failed! Response: <%s>", response); return -1; } bool LX200GotoNova::updateLocation(double latitude, double longitude, double elevation) { INDI_UNUSED(elevation); if (isSimulation()) return true; double final_longitude; if (longitude > 180) final_longitude = longitude - 360.0; else final_longitude = longitude; if (!isSimulation() && setGotoNovaLongitude(final_longitude) < 0) { LOG_ERROR("Error setting site longitude coordinates"); return false; } if (!isSimulation() && setGotoNovaLatitude(latitude) < 0) { LOG_ERROR("Error setting site latitude coordinates"); return false; } char l[32], L[32]; fs_sexa(l, latitude, 3, 3600); fs_sexa(L, longitude, 4, 3600); LOGF_INFO("Site location updated to Lat %.32s - Long %.32s", l, L); return true; } int LX200GotoNova::setGotoNovaLongitude(double Long) { int d, m, s; char sign; char temp_string[32]; if (Long > 0) sign = '+'; else sign = '-'; getSexComponents(Long, &d, &m, &s); snprintf(temp_string, sizeof(temp_string), ":Sg %c%03d*%02d:%02d#", sign, abs(d), m, s); return (setGotoNovaStandardProcedure(PortFD, temp_string)); } int LX200GotoNova::setGotoNovaLatitude(double Lat) { int d, m, s; char sign; char temp_string[32]; if (Lat > 0) sign = '+'; else sign = '-'; getSexComponents(Lat, &d, &m, &s); snprintf(temp_string, sizeof(temp_string), ":St %c%02d*%02d:%02d#", sign, abs(d), m, s); return (setGotoNovaStandardProcedure(PortFD, temp_string)); } int LX200GotoNova::setGotoNovaUTCOffset(double hours) { char temp_string[16]; char sign; int h = 0, m = 0, s = 0; if (hours > 0) sign = '+'; else sign = '-'; getSexComponents(hours, &h, &m, &s); snprintf(temp_string, sizeof(temp_string), ":SG %c%02d#", sign, abs(h)); return (setGotoNovaStandardProcedure(PortFD, temp_string)); } int LX200GotoNova::setGotoNovaStandardProcedure(int fd, const char *data) { const struct timespec timeout = {0, 10000000L}; char bool_return[2]; int error_type; int nbytes_write = 0, nbytes_read = 0; DEBUGF(DBG_SCOPE, "CMD <%s>", data); if ((error_type = tty_write_string(fd, data, &nbytes_write)) != TTY_OK) return error_type; error_type = tty_read(fd, bool_return, 1, 5, &nbytes_read); // JM: Hack from Jon in the INDI forums to fix longitude/latitude settings failure on GotoNova nanosleep(&timeout, NULL); tcflush(fd, TCIFLUSH); nanosleep(&timeout, NULL); if (nbytes_read < 1) return error_type; DEBUGF(DBG_SCOPE, "RES <%c>", bool_return[0]); if (bool_return[0] == '0') { DEBUGF(DBG_SCOPE, "CMD <%s> failed.", data); return -1; } DEBUGF(DBG_SCOPE, "CMD <%s> successful.", data); return 0; } bool LX200GotoNova::SetTrackMode(uint8_t mode) { return (setGotoNovaTrackMode(mode) == 0); } int LX200GotoNova::setGotoNovaTrackMode(int mode) { DEBUGF(DBG_SCOPE, "<%s>", __FUNCTION__); char cmd[8]; snprintf(cmd, 8, ":STR%d#", mode); return setGotoNovaStandardProcedure(PortFD, cmd); } bool LX200GotoNova::Park() { DEBUGF(DBG_SCOPE, "<%s>", __FUNCTION__); int error_type; int nbytes_write = 0; if ((error_type = tty_write_string(PortFD, ":PK#", &nbytes_write)) != TTY_OK) return error_type; tcflush(PortFD, TCIFLUSH); EqNP.s = IPS_BUSY; TrackState = SCOPE_PARKING; LOG_INFO("Parking is in progress..."); return true; } bool LX200GotoNova::UnPark() { SetParked(false); return true; } bool LX200GotoNova::ReadScopeStatus() { if (!isConnected()) return false; if (isSimulation()) { mountSim(); return true; } if (TrackState == SCOPE_SLEWING) { // Check if LX200 is done slewing if (isSlewComplete()) { TrackState = SCOPE_TRACKING; LOG_INFO("Slew is complete. Tracking..."); } } else if (TrackState == SCOPE_PARKING) { if (isSlewComplete()) { SetParked(true); } } if (getLX200RA(PortFD, ¤tRA) < 0 || getLX200DEC(PortFD, ¤tDEC) < 0) { EqNP.s = IPS_ALERT; IDSetNumber(&EqNP, "Error reading RA/DEC."); return false; } NewRaDec(currentRA, currentDEC); syncSideOfPier(); return true; } void LX200GotoNova::mountSim() { static struct timeval ltv; struct timeval tv; double dt, da, dx; int nlocked; /* update elapsed time since last poll, don't presume exactly POLLMS */ gettimeofday(&tv, nullptr); if (ltv.tv_sec == 0 && ltv.tv_usec == 0) ltv = tv; dt = tv.tv_sec - ltv.tv_sec + (tv.tv_usec - ltv.tv_usec) / 1e6; ltv = tv; da = SLEWRATE * dt; /* Process per current state. We check the state of EQUATORIAL_COORDS and act acoordingly */ switch (TrackState) { case SCOPE_TRACKING: /* RA moves at sidereal, Dec stands still */ currentRA += (SIDRATE * dt / 15.); break; case SCOPE_SLEWING: case SCOPE_PARKING: /* slewing - nail it when both within one pulse @ SLEWRATE */ nlocked = 0; dx = targetRA - currentRA; if (fabs(dx) <= da) { currentRA = targetRA; nlocked++; } else if (dx > 0) currentRA += da / 15.; else currentRA -= da / 15.; dx = targetDEC - currentDEC; if (fabs(dx) <= da) { currentDEC = targetDEC; nlocked++; } else if (dx > 0) currentDEC += da; else currentDEC -= da; if (nlocked == 2) { if (TrackState == SCOPE_SLEWING) TrackState = SCOPE_TRACKING; else SetParked(true); } break; default: break; } NewRaDec(currentRA, currentDEC); } int LX200GotoNova::getGotoNovaGuideRate(int *rate) { char cmd[] = ":GGS#"; int errcode = 0; char errmsg[MAXRBUF]; char response[8]; int nbytes_read = 0; int nbytes_written = 0; LOGF_DEBUG("CMD (%s)", cmd); if (isSimulation()) { snprintf(response, 8, "%d#", IUFindOnSwitchIndex(&GuideRateSP)); nbytes_read = strlen(response); } else { tcflush(PortFD, TCIFLUSH); if ((errcode = tty_write(PortFD, cmd, strlen(cmd), &nbytes_written)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); LOGF_ERROR("%s", errmsg); return errcode; } if ((errcode = tty_read(PortFD, response, 1, 3, &nbytes_read))) { tty_error_msg(errcode, errmsg, MAXRBUF); LOGF_ERROR("%s", errmsg); return errcode; } } if (nbytes_read > 0) { response[nbytes_read] = '\0'; LOGF_DEBUG("RES (%s)", response); *rate = atoi(response); return 0; } LOGF_ERROR("Only received #%d bytes, expected 1.", nbytes_read); return -1; } int LX200GotoNova::setGotoNovaGuideRate(int rate) { char cmd[16]; int errcode = 0; char errmsg[MAXRBUF]; int nbytes_written = 0; snprintf(cmd, 16, ":SGS%0d#", rate); LOGF_DEBUG("CMD (%s)", cmd); if (isSimulation()) { return 0; } tcflush(PortFD, TCIFLUSH); if ((errcode = tty_write(PortFD, cmd, strlen(cmd), &nbytes_written)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); LOGF_ERROR("%s", errmsg); return errcode; } return 0; } int LX200GotoNova::setGotoNovaParkPosition(int position) { char temp_string[16]; snprintf(temp_string, sizeof(temp_string), ":STPKP%d#", position); return (setGotoNovaStandardProcedure(PortFD, temp_string)); } void LX200GotoNova::syncSideOfPier() { const char *cmd = ":pS#"; // Response char response[16] = { 0 }; int rc = 0, nbytes_read = 0, nbytes_written = 0; LOGF_DEBUG("CMD: <%s>", cmd); tcflush(PortFD, TCIOFLUSH); if ((rc = tty_write(PortFD, cmd, strlen(cmd), &nbytes_written)) != TTY_OK) { char errmsg[256]; tty_error_msg(rc, errmsg, 256); LOGF_ERROR("Error writing to device %s (%d)", errmsg, rc); return; } // Read Side if ((rc = tty_read_section(PortFD, response, '#', 3, &nbytes_read)) != TTY_OK) { char errmsg[256]; tty_error_msg(rc, errmsg, 256); LOGF_ERROR("Error reading from device %s (%d)", errmsg, rc); return; } response[nbytes_read - 1] = '\0'; tcflush(PortFD, TCIOFLUSH); LOGF_DEBUG("RES: <%s>", response); if (!strcmp(response, "East")) setPierSide(INDI::Telescope::PIER_EAST); else setPierSide(INDI::Telescope::PIER_WEST); } bool LX200GotoNova::saveConfigItems(FILE *fp) { LX200Generic::saveConfigItems(fp); IUSaveConfigSwitch(fp, &SyncCMRSP); IUSaveConfigSwitch(fp, &ParkPositionSP); return true; } libindi/drivers/telescope/ioptronHC8406.cpp0000664000175000017500000010252513263645557020073 0ustar jasemjasem/* ioptronHC8406 INDI driver Copyright (C) 2017 Nacho Mas. Base on GotoNova driver by Jasem Mutlaq 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 */ /* HC8406 CMD hardware TEST V1.10 March 21, 2011 UPGRADE INFO ON: http://www.ioptron.com/Articles.asp?ID=268 INFO ---- # -> repeat last command :GG# +08:00:00 UTC OffSet :Gg# -003*18:03# longitud :Gt# +41*06:56# latitude :GL# 7:02:47.0# local time :GS# 20:12: 3.3# Sideral Time :GR# 2:12:57.4# RA :GD# +90* 0: 0# DEC :GA# +41* 6:55# ALT :GZ# 0* 0: 0# AZ :GC# 03:12:09# Calendar day :pS# East# pier side :FirmWareDate# :20110506# :V# V1.00# COMMANDS -------- :CM# Coordinates matched. # :CMR# Coordinates matched. # This only works if the mount is not stopped (tracking) :RT0# --> Lunar :RT1# --> solar :RT2# --> sideral :RT9# --> zero but not work!! !!!There isn't a command to start/stop tracking !!! You have to do manualy This speeds only are taken into account for protocol buttons, not for the HC Buttons :RG# --> Select guide speed for :Mn#,:Ms# .... :RG0,1,2 -->preselect guide speed 0.25x, 0.5x, 1.0x (HC shows it) :RC# --> Select center speed for :Mn#,:Ms# .... (Not Works) :RC0,1,2 -->preselect guide speed (HC doesn't shows it) :Mn# :Ms# :Me# :Mw# (move until :Q# at guiding or center speed :RG# (works)or :RC#(not work, use :RC0/1/2 instead)) :MnXXX# :MsXXX# :MeXXX# :MwXXX# (move XXX ms at guiding speed no mather what :RCx#,:RGX# or :RSX# was issue) Firmware update (HC v1.10 -> v1.12, also mainboard firmware) ------------------------------------------------------------------ HC8406 CMD hardware TEST V1.12 2011-08-12 UPGRADE INFO ON: http://www.ioptron.com/Articles.asp?ID=268 :RT9# -> stop tracking :RT0# -> start tracking at sidera speed. Formerly only preselect sideral speed but not start the tracking itself :Me# :Mw# :Mexxx# :Mwxxx# ->Al this commands are broken. Only RA axes, the equivalent :Ms#,:Mn#... work. Summarizing: old firmware v1.10: is not possible to start/stop tracking. Guide/move commands OK new firmware v1.12: is possible to start/stop tracking. Guide/move commands NOT OK */ /* SOCAT sniffer socat -v PTY,link=/tmp/serial,wait-slave,raw /dev/ttyUSB0,raw */ #include "ioptronHC8406.h" #include "indicom.h" #include "lx200driver.h" #include #include #include #include #include /* Simulation Parameters */ #define SLEWRATE 1 /* slew rate, degrees/s */ #define SIDRATE 0.004178 /* sidereal rate, degrees/s */ #define ioptronHC8406_TIMEOUT 1 /* timeout */ #define ioptronHC8406_CALDATE_RESULT " # " /* result of calendar date */ ioptronHC8406::ioptronHC8406() { setVersion(1, 1); setLX200Capability(LX200_HAS_FOCUS | LX200_HAS_PULSE_GUIDING); SetTelescopeCapability(TELESCOPE_CAN_PARK | TELESCOPE_CAN_SYNC | TELESCOPE_CAN_GOTO | TELESCOPE_CAN_ABORT | TELESCOPE_HAS_TIME | TELESCOPE_HAS_LOCATION | TELESCOPE_HAS_TRACK_MODE | TELESCOPE_CAN_CONTROL_TRACK); } bool ioptronHC8406::initProperties() { LX200Generic::initProperties(); // Sync Type IUFillSwitch(&SyncCMRS[USE_REGULAR_SYNC], "USE_REGULAR_SYNC", ":CM#", ISS_ON); IUFillSwitch(&SyncCMRS[USE_CMR_SYNC], "USE_CMR_SYNC", ":CMR#", ISS_OFF); IUFillSwitchVector(&SyncCMRSP, SyncCMRS, 2, getDeviceName(), "SYNC_MODE", "Sync", MAIN_CONTROL_TAB, IP_RW, ISR_1OFMANY, 0,IPS_IDLE); // Cursor move Guiding/Center IUFillSwitch(&CursorMoveSpeedS[USE_GUIDE_SPEED],"USE_GUIDE_SPEED", "Guide Speed", ISS_ON); IUFillSwitch(&CursorMoveSpeedS[USE_CENTERING_SPEED],"USE_CENTERING_SPEED", "Centering Speed", ISS_OFF); IUFillSwitchVector(&CursorMoveSpeedSP, CursorMoveSpeedS, 2, getDeviceName(), "CURSOR_MOVE_MODE", "Cursor Move Speed", MOTION_TAB, IP_RO, ISR_1OFMANY, 0,IPS_IDLE); // Guide Rate IUFillSwitch(&GuideRateS[0], "0.25x", "", ISS_OFF); IUFillSwitch(&GuideRateS[1], "0.50x", "", ISS_ON); IUFillSwitch(&GuideRateS[2], "1.0x", "", ISS_OFF); IUFillSwitchVector(&GuideRateSP, GuideRateS, 3, getDeviceName(), "GUIDE_RATE", "Guide Speed", MOTION_TAB, IP_RW, ISR_1OFMANY, 0,IPS_IDLE); // Center Rate IUFillSwitch(&CenterRateS[0], "12x", "", ISS_OFF); IUFillSwitch(&CenterRateS[1], "64x", "", ISS_ON); IUFillSwitch(&CenterRateS[2], "600x", "", ISS_OFF); IUFillSwitch(&CenterRateS[3], "1200x", "", ISS_OFF); IUFillSwitchVector(&CenterRateSP, CenterRateS, 4, getDeviceName(), "CENTER_RATE", "Center Speed", MOTION_TAB, IP_RW, ISR_1OFMANY, 0,IPS_IDLE); // Slew Rate //NOT WORK!! IUFillSwitch(&SlewRateS[0], "600x", "", ISS_OFF); IUFillSwitch(&SlewRateS[1], "900x", "", ISS_OFF); IUFillSwitch(&SlewRateS[2], "1200x", "", ISS_ON); IUFillSwitchVector(&SlewRateSP, SlewRateS, 3, getDeviceName(), "SLEW_RATE", "Slew Speed", MOTION_TAB, IP_RW, ISR_1OFMANY, 0,IPS_IDLE); TrackModeSP.nsp = 3; return true; } bool ioptronHC8406::updateProperties() { LX200Generic::updateProperties(); if (isConnected()) { defineSwitch(&SyncCMRSP); defineSwitch(&GuideRateSP); defineSwitch(&CenterRateSP); //defineSwitch(&SlewRateSP); //NOT WORK!! defineSwitch(&CursorMoveSpeedSP); ioptronHC8406Init(); } else { deleteProperty(SyncCMRSP.name); deleteProperty(GuideRateSP.name); deleteProperty(CenterRateSP.name); //deleteProperty(SlewRateSP.name); //NOT WORK!! deleteProperty(CursorMoveSpeedSP.name); } return true; } const char *ioptronHC8406::getDefaultName() { return (const char *)"iOptron HC8406"; } bool ioptronHC8406::checkConnection() { const timespec timeout = {0, 50000000L}; if (isSimulation()) return true; char initCMD[] = ":V#"; int errcode = 0; char errmsg[MAXRBUF]; char response[8]; int nbytes_read = 0; int nbytes_written = 0; LOG_DEBUG("Initializing iOptron using :V# CMD..."); for (int i = 0; i < 2; i++) { if ((errcode = tty_write(PortFD, initCMD, 3, &nbytes_written)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); LOGF_ERROR("%s", errmsg); nanosleep(&timeout, NULL); continue; } if ((errcode = tty_read_section(PortFD, response, '#', 3, &nbytes_read))) { tty_error_msg(errcode, errmsg, MAXRBUF); LOGF_ERROR("%s", errmsg); nanosleep(&timeout, NULL); continue; } if (nbytes_read > 0) { response[nbytes_read] = '\0'; LOGF_DEBUG("RES (%s)", response); if (!strcmp(response, "V1.00#")) return true; } nanosleep(&timeout, NULL); } return false; } bool ioptronHC8406::ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) { if (dev != nullptr && strcmp(dev, getDeviceName()) == 0) { // Sync type if (!strcmp(name, SyncCMRSP.name)) { IUResetSwitch(&SyncCMRSP); IUUpdateSwitch(&SyncCMRSP, states, names, n); IUFindOnSwitchIndex(&SyncCMRSP); SyncCMRSP.s = IPS_OK; IDSetSwitch(&SyncCMRSP, nullptr); return true; } // Cursor move type if (!strcmp(name, CursorMoveSpeedSP.name)) { int currentSwitch = IUFindOnSwitchIndex(&CursorMoveSpeedSP); IUUpdateSwitch(&CursorMoveSpeedSP, states, names, n); if (setioptronHC8406CursorMoveSpeed(IUFindOnSwitchIndex(&CursorMoveSpeedSP)) == TTY_OK) CursorMoveSpeedSP.s = IPS_OK; else { IUResetSwitch(&CursorMoveSpeedSP); CursorMoveSpeedS[currentSwitch].s = ISS_ON; CursorMoveSpeedSP.s = IPS_ALERT; } return true; } // Guide Rate if (!strcmp(GuideRateSP.name, name)) { int currentSwitch = IUFindOnSwitchIndex(&GuideRateSP); IUUpdateSwitch(&GuideRateSP, states, names, n); if (setioptronHC8406GuideRate(IUFindOnSwitchIndex(&GuideRateSP)) == TTY_OK) { GuideRateSP.s = IPS_OK; //Shows guide speed selected CursorMoveSpeedS[USE_GUIDE_SPEED].s = ISS_ON; CursorMoveSpeedS[USE_CENTERING_SPEED].s = ISS_OFF; CursorMoveSpeedSP.s = IPS_OK; IDSetSwitch(&CursorMoveSpeedSP, nullptr); } else { IUResetSwitch(&GuideRateSP); GuideRateS[currentSwitch].s = ISS_ON; GuideRateSP.s = IPS_ALERT; } IDSetSwitch(&GuideRateSP, nullptr); return true; } // Center Rate if (!strcmp(CenterRateSP.name, name)) { int currentSwitch = IUFindOnSwitchIndex(&CenterRateSP); IUUpdateSwitch(&CenterRateSP, states, names, n); if (setioptronHC8406CenterRate(IUFindOnSwitchIndex(&CenterRateSP)) == TTY_OK) { CenterRateSP.s = IPS_OK; //Shows centering speed selected CursorMoveSpeedS[USE_GUIDE_SPEED].s = ISS_OFF; CursorMoveSpeedS[USE_CENTERING_SPEED].s = ISS_ON; CursorMoveSpeedSP.s = IPS_OK; IDSetSwitch(&CursorMoveSpeedSP, nullptr); } else { IUResetSwitch(&CenterRateSP); CenterRateS[currentSwitch].s = ISS_ON; CenterRateSP.s = IPS_ALERT; } IDSetSwitch(&CenterRateSP, nullptr); return true; } // Slew Rate if (!strcmp(SlewRateSP.name, name)) { int currentSwitch = IUFindOnSwitchIndex(&SlewRateSP); IUUpdateSwitch(&SlewRateSP, states, names, n); if (setioptronHC8406SlewRate(IUFindOnSwitchIndex(&SlewRateSP)) == TTY_OK) SlewRateSP.s = IPS_OK; else { IUResetSwitch(&SlewRateSP); SlewRateS[currentSwitch].s = ISS_ON; SlewRateSP.s = IPS_ALERT; } IDSetSwitch(&SlewRateSP, nullptr); return true; } } return LX200Generic::ISNewSwitch(dev, name, states, names, n); } bool ioptronHC8406::isSlewComplete() { /* HC8406 doesn't have :SE# or :SE? command, thus we check if the slew is completed comparing targetRA/DEC with actual RA/DEC */ float tolerance=1/3600.; // 5 arcsec if (fabs(currentRA-targetRA) <= tolerance && fabs(currentDEC-targetDEC) <= tolerance) return true; return false; } void ioptronHC8406::getBasicData() { checkLX200Format(PortFD); sendScopeLocation(); sendScopeTime(); } void ioptronHC8406::ioptronHC8406Init() { //This mount doesn't report anything so we send some CMD //just to get syncronize with the GUI at start time LOG_WARN("Sending init CMDs. Unpark, Stop tracking"); UnPark(); TrackState = SCOPE_IDLE; SetTrackEnabled(false); setioptronHC8406GuideRate(1); } bool ioptronHC8406::Goto(double r, double d) { const timespec timeout = {0, 100000000L}; targetRA = r; targetDEC = d; char RAStr[64], DecStr[64]; fs_sexa(RAStr, targetRA, 2, 3600); fs_sexa(DecStr, targetDEC, 2, 3600); LOGF_DEBUG(" %s/%s",RAStr,DecStr); // If moving, let's stop it first. if (EqNP.s == IPS_BUSY) { if (!isSimulation() && abortSlew(PortFD) < 0) { AbortSP.s = IPS_ALERT; IDSetSwitch(&AbortSP, "Abort slew failed."); return false; } AbortSP.s = IPS_OK; EqNP.s = IPS_IDLE; IDSetSwitch(&AbortSP, "Slew aborted."); IDSetNumber(&EqNP, nullptr); if (MovementNSSP.s == IPS_BUSY || MovementWESP.s == IPS_BUSY) { MovementNSSP.s = MovementWESP.s = IPS_IDLE; EqNP.s = IPS_IDLE; IUResetSwitch(&MovementNSSP); IUResetSwitch(&MovementWESP); IDSetSwitch(&MovementNSSP, nullptr); IDSetSwitch(&MovementWESP, nullptr); } // sleep for 100 mseconds nanosleep(&timeout, NULL); } // If parking/parked, let's unpark it first. /* if (TrackState == SCOPE_PARKING || TrackState == SCOPE_PARKED) { UnPark(); }*/ if (!isSimulation()) { if (setObjectRA(PortFD, targetRA) < 0 || (setObjectDEC(PortFD, targetDEC)) < 0) { EqNP.s = IPS_ALERT; IDSetNumber(&EqNP, "Error setting RA/DEC."); return false; } if (slewioptronHC8406() == 0) //action { EqNP.s = IPS_ALERT; IDSetNumber(&EqNP, "Error Slewing to JNow RA %s - DEC %s\n", RAStr, DecStr); slewError(1); return false; } } TrackState = SCOPE_SLEWING; EqNP.s = IPS_BUSY; LOGF_DEBUG("Slewing to RA: %s - DEC: %s",RAStr,DecStr); return true; } bool ioptronHC8406::Sync(double ra, double dec) { char syncString[256]; int syncType = IUFindOnSwitchIndex(&SyncCMRSP); if (!isSimulation()) { if (setObjectRA(PortFD, ra) < 0 || setObjectDEC(PortFD, dec) < 0) { EqNP.s = IPS_ALERT; IDSetNumber(&EqNP, "Error setting RA/DEC. Unable to Sync."); return false; } bool syncOK = true; switch (syncType) { case USE_REGULAR_SYNC: if (::Sync(PortFD, syncString) < 0) syncOK = false; break; case USE_CMR_SYNC: if (ioptronHC8406SyncCMR(syncString) < 0) syncOK = false; break; default: break; } if (syncOK == false) { EqNP.s = IPS_ALERT; IDSetNumber(&EqNP, "Synchronization failed."); return false; } } currentRA = ra; currentDEC = dec; LOGF_DEBUG("%s Synchronization successful %s", (syncType == USE_REGULAR_SYNC ? "CM" : "CMR"), syncString); LOG_INFO("Synchronization successful."); EqNP.s = IPS_OK; NewRaDec(currentRA, currentDEC); return true; } int ioptronHC8406::ioptronHC8406SyncCMR(char *matchedObject) { const timespec timeout = {0, 10000000L}; int error_type; int nbytes_write = 0; int nbytes_read = 0; LOGF_DEBUG("CMD <%s>", ":CMR#"); if ((error_type = tty_write_string(PortFD, ":CMR#", &nbytes_write)) != TTY_OK) return error_type; if ((error_type = tty_read_section(PortFD, matchedObject, '#', 3, &nbytes_read)) != TTY_OK) return error_type; matchedObject[nbytes_read - 1] = '\0'; LOGF_DEBUG("RES <%s>", matchedObject); /* Sleep 10ms before flushing. This solves some issues with LX200 compatible devices. */ nanosleep(&timeout, NULL); tcflush(PortFD, TCIFLUSH); return 0; } int ioptronHC8406::slewioptronHC8406() { DEBUGF(DBG_SCOPE, "<%s>", __FUNCTION__); char slewNum[2]; int error_type; int nbytes_write = 0, nbytes_read = 0; DEBUGF(DBG_SCOPE, "CMD <%s>", ":MS#"); if ((error_type = tty_write_string(PortFD, ":MS#", &nbytes_write)) != TTY_OK) return error_type; error_type = tty_read(PortFD, slewNum, 1, 3, &nbytes_read); if (nbytes_read < 1) { DEBUGF(DBG_SCOPE, "RES ERROR <%d>", error_type); return error_type; } /* We don't need to read the string message, just return corresponding error code */ tcflush(PortFD, TCIFLUSH); DEBUGF(DBG_SCOPE, "RES <%c>", slewNum[0]); return slewNum[0]; } bool ioptronHC8406::updateTime(ln_date *utc, double utc_offset) { struct ln_zonedate ltm; if (isSimulation()) return true; ln_date_to_zonedate(utc, <m, utc_offset * 3600.0); JD = ln_get_julian_day(utc); LOGF_DEBUG("New JD is %.2f", JD); // Set Local Time if (setLocalTime(PortFD, ltm.hours, ltm.minutes, ltm.seconds) < 0) { LOG_ERROR("Error setting local time."); return false; } if (setCalenderDate(PortFD, ltm.days, ltm.months, ltm.years) < 0) { LOG_ERROR("Error setting local date."); return false; } if (setioptronHC8406UTCOffset(utc_offset) < 0) { LOG_ERROR("Error setting UTC Offset."); return false; } return true; } int ioptronHC8406::setCalenderDate(int fd, int dd, int mm, int yy) { char read_buffer[16]; char response[67]; char good_result[] = ioptronHC8406_CALDATE_RESULT; int error_type; int nbytes_write = 0, nbytes_read = 0; yy = yy % 100; snprintf(read_buffer, sizeof(read_buffer), ":SC %02d:%02d:%02d#", mm, dd, yy); DEBUGF(DBG_SCOPE, "CMD <%s>", read_buffer); tcflush(fd, TCIFLUSH); /* Sleep 100ms before flushing. This solves some issues with LX200 compatible devices. */ //usleep(10); if ((error_type = tty_write_string(fd, read_buffer, &nbytes_write)) != TTY_OK) return error_type; error_type = tty_read(fd, response, sizeof(response), ioptronHC8406_TIMEOUT, &nbytes_read); tcflush(fd, TCIFLUSH); if (nbytes_read < 1) { LOG_ERROR("Unable to read response"); return error_type; } response[nbytes_read] = '\0'; DEBUGF(DBG_SCOPE, "RES <%s>", response); if (strncmp(response, good_result, strlen(good_result)) == 0) { return 0; } tcflush(fd, TCIFLUSH); LOGF_DEBUG("Set date failed! Response: <%s>", response); return -1; } bool ioptronHC8406::updateLocation(double latitude, double longitude, double elevation) { INDI_UNUSED(elevation); if (isSimulation()) return true; double final_longitude; if (longitude > 180) final_longitude = longitude - 360.0; else final_longitude = longitude; if (!isSimulation() && setioptronHC8406Longitude(final_longitude) < 0) { LOG_ERROR("Error setting site longitude coordinates"); return false; } if (!isSimulation() && setioptronHC8406Latitude(latitude) < 0) { LOG_ERROR("Error setting site latitude coordinates"); return false; } char l[32], L[32]; fs_sexa(l, latitude, 3, 3600); fs_sexa(L, longitude, 4, 3600); IDMessage(getDeviceName(), "Site location updated to Lat %.32s - Long %.32s", l, L); return true; } int ioptronHC8406::setioptronHC8406Longitude(double Long) { int d, m, s; char sign; char temp_string[32]; if (Long > 0) sign = '+'; else sign = '-'; Long=360-Long; getSexComponents(Long, &d, &m, &s); snprintf(temp_string, sizeof(temp_string), ":Sg %03d*%02d:%02d#", abs(d), m, s); return (setioptronHC8406StandardProcedure(PortFD, temp_string)); } int ioptronHC8406::setioptronHC8406Latitude(double Lat) { int d, m, s; char sign; char temp_string[32]; if (Lat > 0) sign = '+'; else sign = '-'; getSexComponents(Lat, &d, &m, &s); snprintf(temp_string, sizeof(temp_string), ":St %c%02d*%02d:%02d#", sign, abs(d), m, s); return (setioptronHC8406StandardProcedure(PortFD, temp_string)); } int ioptronHC8406::setioptronHC8406UTCOffset(double hours) { char temp_string[16]; char sign; int h = 0, m = 0, s = 0; if (hours > 0) sign = '+'; else sign = '-'; getSexComponents(hours, &h, &m, &s); snprintf(temp_string, sizeof(temp_string), ":SG %c%02d#", sign, abs(h)); return (setioptronHC8406StandardProcedure(PortFD, temp_string)); } int ioptronHC8406::setioptronHC8406StandardProcedure(int fd, const char *data) { const timespec timeout = {0, 10000000L}; char bool_return[2]; int error_type=0; int nbytes_write = 0, nbytes_read = 0; DEBUGF(DBG_SCOPE, "CMD <%s>", data); if ((error_type = tty_write_string(fd, data, &nbytes_write)) != TTY_OK) return error_type; error_type = tty_read(fd, bool_return, 1, 5, &nbytes_read); // JM: Hack from Jon in the INDI forums to fix longitude/latitude settings failure nanosleep(&timeout, NULL); tcflush(fd, TCIFLUSH); nanosleep(&timeout, NULL); if (nbytes_read < 1) return error_type; DEBUGF(DBG_SCOPE, "RES <%c>", bool_return[0]); if (bool_return[0] == '0') { DEBUGF(DBG_SCOPE, "CMD <%s> failed.", data); return -1; } DEBUGF(DBG_SCOPE, "CMD <%s> successful.", data); return 0; } bool ioptronHC8406::SetTrackEnabled(bool enabled) { if (enabled) { LOG_WARN(" START TRACKING AT SIDERAL SPEED (:RT2#)"); return setioptronHC8406TrackMode(0); } else { LOG_WARN(" STOP TRACKING (:RT9#)"); return setioptronHC8406TrackMode(3); } } bool ioptronHC8406::SetTrackMode(uint8_t mode) { return (setioptronHC8406TrackMode(mode)); } int ioptronHC8406::setioptronHC8406TrackMode(int mode) { char cmd[8]; int mmode=0; int error_type=0; int nbytes_write = 0 ; DEBUGF(DBG_SCOPE, "<%s>", __FUNCTION__); if (mode == 0 ) { mmode=2; } else if (mode ==1) { mmode=1; } else if (mode ==2) { mmode=0; } else if (mode ==3) { mmode=9; } snprintf(cmd, 8, ":RT%d#", mmode); DEBUGF(DBG_SCOPE, "CMD <%s>", cmd); //None return value so just write cmd and exit without reading the response if ((error_type = tty_write_string(PortFD, cmd, &nbytes_write)) != TTY_OK) return error_type; return 1; } bool ioptronHC8406::Park() { DEBUGF(DBG_SCOPE, "<%s>", __FUNCTION__); int error_type; int nbytes_write = 0; if ((error_type = tty_write_string(PortFD, ":KA#", &nbytes_write)) != TTY_OK) return error_type; tcflush(PortFD, TCIFLUSH); DEBUG(DBG_SCOPE, "CMD <:KA#>"); EqNP.s = IPS_BUSY; TrackState = SCOPE_PARKING; LOG_INFO("Parking is in progress..."); return true; } bool ioptronHC8406::UnPark() { SetParked(false); return true; } bool ioptronHC8406::ReadScopeStatus() { const timespec timeout = {1, 0L}; //return true; //for debug if (!isConnected()) return false; if (isSimulation()) { mountSim(); return true; } switch (TrackState) { case SCOPE_IDLE: LOG_WARN(" IDLE"); break; case SCOPE_SLEWING: LOG_WARN(" SLEWING"); break; case SCOPE_TRACKING: LOG_WARN(" TRACKING"); break; case SCOPE_PARKING: LOG_WARN(" PARKING"); break; case SCOPE_PARKED: LOG_WARN(" PARKED"); break; default: LOG_WARN(" UNDEFINED"); break; } if (TrackState == SCOPE_SLEWING ) { // Check if LX200 is done slewing if (isSlewComplete()) { nanosleep(&timeout, NULL); //Wait until :MS# finish if (IUFindSwitch(&CoordSP, "SYNC")->s == ISS_ON || IUFindSwitch(&CoordSP, "SLEW")->s == ISS_ON) { TrackState = SCOPE_IDLE; LOG_WARN("Slew is complete. IDLE"); SetTrackEnabled(false); } else { TrackState = SCOPE_TRACKING; LOG_WARN("Slew is complete. TRACKING"); SetTrackEnabled(true); } } } else if (TrackState == SCOPE_PARKING) { // isSlewComplete() not work because is base on actual RA/DEC vs target RA/DEC. DO ALWAYS if (true || isSlewComplete()) { SetParked(true); TrackState = SCOPE_PARKED; } } if (getLX200RA(PortFD, ¤tRA) < 0 || getLX200DEC(PortFD, ¤tDEC) < 0) { EqNP.s = IPS_ALERT; IDSetNumber(&EqNP, "Error reading RA/DEC."); return false; } NewRaDec(currentRA, currentDEC); //sendScopeTime(); //syncSideOfPier(); return true; } void ioptronHC8406::mountSim() { static struct timeval ltv; struct timeval tv; double dt, da, dx; int nlocked; /* update elapsed time since last poll, don't presume exactly POLLMS */ gettimeofday(&tv, nullptr); if (ltv.tv_sec == 0 && ltv.tv_usec == 0) ltv = tv; dt = tv.tv_sec - ltv.tv_sec + (tv.tv_usec - ltv.tv_usec) / 1e6; ltv = tv; da = SLEWRATE * dt; /* Process per current state. We check the state of EQUATORIAL_COORDS and act acoordingly */ switch (TrackState) { case SCOPE_TRACKING: /* RA moves at sidereal, Dec stands still */ currentRA += (SIDRATE * dt / 15.); break; case SCOPE_SLEWING: case SCOPE_PARKING: /* slewing - nail it when both within one pulse @ SLEWRATE */ nlocked = 0; dx = targetRA - currentRA; if (fabs(dx) <= da) { currentRA = targetRA; nlocked++; } else if (dx > 0) currentRA += da / 15.; else currentRA -= da / 15.; dx = targetDEC - currentDEC; if (fabs(dx) <= da) { currentDEC = targetDEC; nlocked++; } else if (dx > 0) currentDEC += da; else currentDEC -= da; if (nlocked == 2) { if (TrackState == SCOPE_SLEWING) TrackState = SCOPE_TRACKING; else SetParked(true); } break; default: break; } NewRaDec(currentRA, currentDEC); } int ioptronHC8406::setioptronHC8406GuideRate(int rate) { return setMoveRate(rate,USE_GUIDE_SPEED); } int ioptronHC8406::setioptronHC8406CenterRate(int rate) { return setMoveRate(rate,USE_CENTERING_SPEED); } int ioptronHC8406::setioptronHC8406SlewRate(int rate) { return setMoveRate(rate,USE_SLEW_SPEED); } int ioptronHC8406::setioptronHC8406CursorMoveSpeed(int type) { return setMoveRate(-1,type); } int ioptronHC8406::setMoveRate(int rate,int move_type) { char cmd[16]; int errcode = 0; char errmsg[MAXRBUF]; int nbytes_written = 0; if (isSimulation()) { return 0; } if (rate>=0) { switch (move_type) { case USE_GUIDE_SPEED: snprintf(cmd, 16, ":RG%0d#", rate); break; case USE_CENTERING_SPEED: snprintf(cmd, 16, ":RC%0d#", rate); break; case USE_SLEW_SPEED: snprintf(cmd, 16, ":RS%0d#", rate); //NOT WORK!! break; default: break; } } else { switch (move_type) { case USE_GUIDE_SPEED: snprintf(cmd, 16, ":RG#"); break; case USE_CENTERING_SPEED: snprintf(cmd, 16, ":RC#"); //NOT WORK!! break; case USE_SLEW_SPEED: snprintf(cmd, 16, ":RS#"); //NOT WORK!! break; default: break; } } LOGF_DEBUG("CMD (%s)", cmd); tcflush(PortFD, TCIFLUSH); if ((errcode = tty_write(PortFD, cmd, strlen(cmd), &nbytes_written)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); LOGF_ERROR("%s", errmsg); return errcode; } return 0; } void ioptronHC8406::syncSideOfPier() { const char *cmd = ":pS#"; // Response char response[16] = { 0 }; int rc = 0, nbytes_read = 0, nbytes_written = 0; LOGF_DEBUG("CMD: <%s>", cmd); tcflush(PortFD, TCIOFLUSH); if ((rc = tty_write(PortFD, cmd, strlen(cmd), &nbytes_written)) != TTY_OK) { char errmsg[256]; tty_error_msg(rc, errmsg, 256); LOGF_ERROR("Error writing to device %s (%d)", errmsg, rc); return; } // Read Side if ((rc = tty_read_section(PortFD, response, '#', 3, &nbytes_read)) != TTY_OK) { char errmsg[256]; tty_error_msg(rc, errmsg, 256); LOGF_ERROR("Error reading from device %s (%d)", errmsg, rc); return; } response[nbytes_read - 1] = '\0'; tcflush(PortFD, TCIOFLUSH); LOGF_DEBUG("RES: <%s>", response); if (!strcmp(response, "East")) setPierSide(INDI::Telescope::PIER_EAST); else setPierSide(INDI::Telescope::PIER_WEST); } bool ioptronHC8406::saveConfigItems(FILE *fp) { LX200Generic::saveConfigItems(fp); IUSaveConfigSwitch(fp, &SyncCMRSP); return true; } int ioptronHC8406::getCommandString(int fd, char *data, const char *cmd) { char *term; int error_type; int nbytes_write = 0, nbytes_read = 0; if ((error_type = tty_write_string(fd, cmd, &nbytes_write)) != TTY_OK) return error_type; error_type = tty_read_section(fd, data, '#', ioptronHC8406_TIMEOUT, &nbytes_read); tcflush(fd, TCIFLUSH); if (error_type != TTY_OK) return error_type; term = strchr(data, '#'); if (term) *term = '\0'; return 0; } void ioptronHC8406::sendScopeTime() { char cdate[32]={0}; double ctime; int h, m, s; int utc_h, utc_m, utc_s; double lx200_utc_offset = 0; char utc_offset_res[32]={0}; int day, month, year, result; struct tm ltm; struct tm utm; time_t time_epoch; if (isSimulation()) { snprintf(cdate, 32, "%d-%02d-%02dT%02d:%02d:%02d", 1979, 6, 25, 3, 30, 30); IDLog("Telescope ISO date and time: %s\n", cdate); IUSaveText(&TimeT[0], cdate); IUSaveText(&TimeT[1], "3"); IDSetText(&TimeTP, nullptr); return; } //getCommandSexa(PortFD, &lx200_utc_offset, ":GG#"); //tcflush(PortFD, TCIOFLUSH); getCommandString(PortFD, utc_offset_res, ":GG#"); f_scansexa(utc_offset_res,&lx200_utc_offset); result = sscanf(utc_offset_res, "%d%*c%d%*c%d", &utc_h, &utc_m, &utc_s); if (result != 3) { LOG_ERROR("Error reading UTC offset from Telescope."); return; } LOGF_DEBUG(" UTC offset: %d:%d:%d --->%g",utc_h,utc_m, utc_s, lx200_utc_offset); // LX200 TimeT Offset is defined at the number of hours added to LOCAL TIME to get TimeT. This is contrary to the normal definition. LOGF_DEBUG(" UTC offset str: %s",utc_offset_res); IUSaveText(&TimeT[1], utc_offset_res); //IUSaveText(&TimeT[1], lx200_utc_offset); getLocalTime24(PortFD, &ctime); getSexComponents(ctime, &h, &m, &s); getCalendarDate(PortFD, cdate); result = sscanf(cdate, "%d%*c%d%*c%d", &year, &month, &day); if (result != 3) { LOG_ERROR("Error reading date from Telescope."); return; } // Let's fill in the local time ltm.tm_sec = s; ltm.tm_min = m; ltm.tm_hour = h; ltm.tm_mday = day; ltm.tm_mon = month - 1; ltm.tm_year = year - 1900; // Get time epoch time_epoch = mktime(<m); // Convert to TimeT //time_epoch -= (int)(atof(TimeT[1].text) * 3600.0); time_epoch -= (int)(lx200_utc_offset * 3600.0); // Get UTC (we're using localtime_r, but since we shifted time_epoch above by UTCOffset, we should be getting the real UTC time localtime_r(&time_epoch, &utm); /* Format it into ISO 8601 */ strftime(cdate, 32, "%Y-%m-%dT%H:%M:%S", &utm); IUSaveText(&TimeT[0], cdate); LOGF_DEBUG("Mount controller Local Time: %02d:%02d:%02d", h, m, s); LOGF_DEBUG("Mount controller UTC Time: %s", TimeT[0].text); // Let's send everything to the client IDSetText(&TimeTP, nullptr); } int ioptronHC8406::SendPulseCmd(int direction, int Tduration_msec) { LOGF_DEBUG("<%s>", __FUNCTION__); const timespec timeout = {1, 0L}; int rc = 0, nbytes_written = 0; char cmd[20]; int duration_msec,Rduration; if (Tduration_msec >=1000) { duration_msec=999; //limited to 999 Rduration=Tduration_msec-duration_msec; //pending ms } else { duration_msec=Tduration_msec; Rduration=0; LOGF_DEBUG("Pulse %d <999 Sent only one",Tduration_msec); } switch (direction) { case LX200_NORTH: sprintf(cmd, ":Mn%03d#", duration_msec); break; case LX200_SOUTH: sprintf(cmd, ":Ms%03d#", duration_msec); break; case LX200_EAST: sprintf(cmd, ":Me%03d#", duration_msec); break; case LX200_WEST: sprintf(cmd, ":Mw%03d#", duration_msec); break; default: return 1; } LOGF_DEBUG("CMD <%s>", cmd); if ((rc = tty_write(PortFD, cmd, strlen(cmd), &nbytes_written)) != TTY_OK) { char errmsg[256]; tty_error_msg(rc, errmsg, 256); LOGF_ERROR("Error writing to device %s (%d)", errmsg, rc); return 1; } tcflush(PortFD, TCIFLUSH); if (Rduration!=0) { LOGF_DEBUG("pulse guide. Pulse >999. ms left:%d",Rduration); nanosleep(&timeout, NULL); //wait until the previous one has fineshed return SendPulseCmd(direction,Rduration); } return 0; } libindi/drivers/telescope/lx200gps.cpp0000664000175000017500000002737113263645557017230 0ustar jasemjasem/* LX200 GPS Copyright (C) 2003 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 */ #include "lx200gps.h" #include "lx200driver.h" #include #include #define GPS_TAB "Extended GPS Features" LX200GPS::LX200GPS() : LX200Autostar() { MaxReticleFlashRate = 9; } const char *LX200GPS::getDefaultName() { return (const char *)"LX200 GPS"; } bool LX200GPS::initProperties() { LX200Autostar::initProperties(); IUFillSwitch(&GPSPowerS[0], "On", "", ISS_OFF); IUFillSwitch(&GPSPowerS[1], "Off", "", ISS_OFF); IUFillSwitchVector(&GPSPowerSP, GPSPowerS, 2, getDeviceName(), "GPS Power", "", GPS_TAB, IP_RW, ISR_1OFMANY, 0, IPS_IDLE); IUFillSwitch(&GPSStatusS[0], "Sleep", "", ISS_OFF); IUFillSwitch(&GPSStatusS[1], "Wake Up", "", ISS_OFF); IUFillSwitch(&GPSStatusS[2], "Restart", "", ISS_OFF); IUFillSwitchVector(&GPSStatusSP, GPSStatusS, 3, getDeviceName(), "GPS Status", "", GPS_TAB, IP_RW, ISR_1OFMANY, 0, IPS_IDLE); IUFillSwitch(&GPSUpdateS[0], "Update GPS", "", ISS_OFF); IUFillSwitch(&GPSUpdateS[1], "Update Client", "", ISS_OFF); IUFillSwitchVector(&GPSUpdateSP, GPSUpdateS, 2, getDeviceName(), "GPS System", "", GPS_TAB, IP_RW, ISR_1OFMANY, 0, IPS_IDLE); IUFillSwitch(&AltDecPecS[0], "Enable", "", ISS_OFF); IUFillSwitch(&AltDecPecS[1], "Disable", "", ISS_OFF); IUFillSwitchVector(&AltDecPecSP, AltDecPecS, 2, getDeviceName(), "Alt/Dec PEC", "", GPS_TAB, IP_RW, ISR_1OFMANY, 0, IPS_IDLE); IUFillSwitch(&AzRaPecS[0], "Enable", "", ISS_OFF); IUFillSwitch(&AzRaPecS[1], "Disable", "", ISS_OFF); IUFillSwitchVector(&AzRaPecSP, AzRaPecS, 2, getDeviceName(), "Az/RA PEC", "", GPS_TAB, IP_RW, ISR_1OFMANY, 0, IPS_IDLE); IUFillSwitch(&SelenSyncS[0], "Sync", "", ISS_OFF); IUFillSwitchVector(&SelenSyncSP, SelenSyncS, 1, getDeviceName(), "Selenographic Sync", "", GPS_TAB, IP_RW, ISR_ATMOST1, 0, IPS_IDLE); IUFillSwitch(&AltDecBacklashS[0], "Activate", "", ISS_OFF); IUFillSwitchVector(&AltDecBacklashSP, AltDecBacklashS, 1, getDeviceName(), "Alt/Dec Anti-backlash", "", GPS_TAB, IP_RW, ISR_ATMOST1, 0, IPS_IDLE); IUFillSwitch(&AzRaBacklashS[0], "Activate", "", ISS_OFF); IUFillSwitchVector(&AzRaBacklashSP, AzRaBacklashS, 1, getDeviceName(), "Az/Ra Anti-backlash", "", GPS_TAB, IP_RW, ISR_ATMOST1, 0, IPS_IDLE); IUFillSwitch(&OTAUpdateS[0], "Update", "", ISS_OFF); IUFillSwitchVector(&OTAUpdateSP, OTAUpdateS, 1, getDeviceName(), "OTA Update", "", GPS_TAB, IP_RW, ISR_ATMOST1, 0, IPS_IDLE); IUFillNumber(&OTATempN[0], "Temp", "", "%03g", -200.0, 500.0, 0.0, 0); IUFillNumberVector(&OTATempNP, OTATempN, 1, getDeviceName(), "OTA Temp (C)", "", GPS_TAB, IP_RO, 0, IPS_IDLE); return true; } void LX200GPS::ISGetProperties(const char *dev) { if (dev != nullptr && strcmp(dev, getDeviceName()) != 0) return; // process parent first LX200Autostar::ISGetProperties(dev); /* if (isConnected()) { defineSwitch(&GPSPowerSP); defineSwitch(&GPSStatusSP); defineSwitch(&GPSUpdateSP); defineSwitch(&AltDecPecSP); defineSwitch(&AzRaPecSP); defineSwitch(&SelenSyncSP); defineSwitch(&AltDecBacklashSP); defineSwitch(&AzRaBacklashSP); defineNumber(&OTATempNP); defineSwitch(&OTAUpdateSP); } */ } bool LX200GPS::updateProperties() { LX200Autostar::updateProperties(); if (isConnected()) { defineSwitch(&GPSPowerSP); defineSwitch(&GPSStatusSP); defineSwitch(&GPSUpdateSP); defineSwitch(&AltDecPecSP); defineSwitch(&AzRaPecSP); defineSwitch(&SelenSyncSP); defineSwitch(&AltDecBacklashSP); defineSwitch(&AzRaBacklashSP); defineNumber(&OTATempNP); defineSwitch(&OTAUpdateSP); } else { deleteProperty(GPSPowerSP.name); deleteProperty(GPSStatusSP.name); deleteProperty(GPSUpdateSP.name); deleteProperty(AltDecPecSP.name); deleteProperty(AzRaPecSP.name); deleteProperty(SelenSyncSP.name); deleteProperty(AltDecBacklashSP.name); deleteProperty(AzRaBacklashSP.name); deleteProperty(OTATempNP.name); deleteProperty(OTAUpdateSP.name); } return true; } bool LX200GPS::ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) { int index = 0; char msg[64]; if (dev != nullptr && strcmp(dev, getDeviceName()) == 0) { /* GPS Power */ if (!strcmp(name, GPSPowerSP.name)) { int ret = 0; if (IUUpdateSwitch(&GPSPowerSP, states, names, n) < 0) return false; index = IUFindOnSwitchIndex(&GPSPowerSP); if (index == 0) ret = turnGPSOn(PortFD); else ret = turnGPSOff(PortFD); GPSPowerSP.s = IPS_OK; IDSetSwitch(&GPSPowerSP, index == 0 ? "GPS System is ON" : "GPS System is OFF"); return true; } /* GPS Status Update */ if (!strcmp(name, GPSStatusSP.name)) { int ret = 0; if (IUUpdateSwitch(&GPSStatusSP, states, names, n) < 0) return false; index = IUFindOnSwitchIndex(&GPSStatusSP); if (index == 0) { ret = gpsSleep(PortFD); strncpy(msg, "GPS system is in sleep mode.", 64); } else if (index == 1) { ret = gpsWakeUp(PortFD); strncpy(msg, "GPS system is reactivated.", 64); } else { ret = gpsRestart(PortFD); strncpy(msg, "GPS system is restarting...", 64); sendScopeTime(); sendScopeLocation(); } GPSStatusSP.s = IPS_OK; IDSetSwitch(&GPSStatusSP, "%s", msg); return true; } /* GPS Update */ if (!strcmp(name, GPSUpdateSP.name)) { if (IUUpdateSwitch(&GPSUpdateSP, states, names, n) < 0) return false; index = IUFindOnSwitchIndex(&GPSUpdateSP); GPSUpdateSP.s = IPS_OK; if (index == 0) { IDSetSwitch(&GPSUpdateSP, "Updating GPS system. This operation might take few minutes to complete..."); if (updateGPS_System(PortFD)) { IDSetSwitch(&GPSUpdateSP, "GPS system update successful."); sendScopeTime(); sendScopeLocation(); } else { GPSUpdateSP.s = IPS_IDLE; IDSetSwitch(&GPSUpdateSP, "GPS system update failed."); } } else { sendScopeTime(); sendScopeLocation(); IDSetSwitch(&GPSUpdateSP, "Client time and location is synced to LX200 GPS Data."); } return true; } /* Alt Dec Periodic Error correction */ if (!strcmp(name, AltDecPecSP.name)) { int ret = 0; if (IUUpdateSwitch(&AltDecPecSP, states, names, n) < 0) return false; index = IUFindOnSwitchIndex(&AltDecPecSP); if (index == 0) { ret = enableDecAltPec(PortFD); strncpy(msg, "Alt/Dec Compensation Enabled.", 64); } else { ret = disableDecAltPec(PortFD); strncpy(msg, "Alt/Dec Compensation Disabled.", 64); } AltDecPecSP.s = IPS_OK; IDSetSwitch(&AltDecPecSP, "%s", msg); return true; } /* Az RA periodic error correction */ if (!strcmp(name, AzRaPecSP.name)) { int ret = 0; if (IUUpdateSwitch(&AzRaPecSP, states, names, n) < 0) return false; index = IUFindOnSwitchIndex(&AzRaPecSP); if (index == 0) { ret = enableRaAzPec(PortFD); strncpy(msg, "Ra/Az Compensation Enabled.", 64); } else { ret = disableRaAzPec(PortFD); strncpy(msg, "Ra/Az Compensation Disabled.", 64); } AzRaPecSP.s = IPS_OK; IDSetSwitch(&AzRaPecSP, "%s", msg); return true; } if (!strcmp(name, AltDecBacklashSP.name)) { int ret = 0; ret = activateAltDecAntiBackSlash(PortFD); AltDecBacklashSP.s = IPS_OK; IDSetSwitch(&AltDecBacklashSP, "Alt/Dec Anti-backlash enabled"); return true; } if (!strcmp(name, AzRaBacklashSP.name)) { int ret = 0; ret = activateAzRaAntiBackSlash(PortFD); AzRaBacklashSP.s = IPS_OK; IDSetSwitch(&AzRaBacklashSP, "Az/Ra Anti-backlash enabled"); return true; } if (!strcmp(name, OTAUpdateSP.name)) { IUResetSwitch(&OTAUpdateSP); if (getOTATemp(PortFD, &OTATempNP.np[0].value) < 0) { OTAUpdateSP.s = IPS_ALERT; OTATempNP.s = IPS_ALERT; IDSetNumber(&OTATempNP, "Error: OTA temperature read timed out."); return false; } else { OTAUpdateSP.s = IPS_OK; OTATempNP.s = IPS_OK; IDSetNumber(&OTATempNP, nullptr); IDSetSwitch(&OTAUpdateSP, nullptr); return true; } } } return LX200Autostar::ISNewSwitch(dev, name, states, names, n); } bool LX200GPS::updateTime(ln_date *utc, double utc_offset) { ln_zonedate ltm; if (isSimulation()) return true; JD = ln_get_julian_day(utc); LOGF_DEBUG("New JD is %.2f", JD); ln_date_to_zonedate(utc, <m, utc_offset * 3600); LOGF_DEBUG("Local time is %02d:%02d:%02g", ltm.hours, ltm.minutes, ltm.seconds); // Set Local Time if (setLocalTime24(ltm.hours, ltm.minutes, ltm.seconds) == false) { LOG_ERROR("Error setting local time time."); return false; } // UTC Date, it's not Local for LX200GPS if (setLocalDate(utc->days, utc->months, utc->years) == false) { LOG_ERROR("Error setting UTC date."); return false; } // Meade defines UTC Offset as the offset ADDED to local time to yield UTC, which // is the opposite of the standard definition of UTC offset! if (setUTCOffset(utc_offset) == false) { LOG_ERROR("Error setting UTC Offset."); return false; } LOG_INFO("Time updated, updating planetary data..."); return true; } bool LX200GPS::UnPark() { int ret = 0; ret = initTelescope(PortFD); TrackState = SCOPE_IDLE; return true; } libindi/drivers/telescope/lx200classic.h0000664000175000017500000000351413263645557017516 0ustar jasemjasem/* LX200 Classic Copyright (C) 2003 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 */ #pragma once #include "lx200generic.h" class LX200Classic : public LX200Generic { public: LX200Classic(); ~LX200Classic() {} const char *getDefaultName(); bool initProperties(); void ISGetProperties(const char *dev); bool updateProperties(); bool ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n); bool ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n); protected: ITextVectorProperty ObjectInfoTP; IText ObjectInfoT[1] {}; ISwitchVectorProperty StarCatalogSP; ISwitch StarCatalogS[3]; ISwitchVectorProperty DeepSkyCatalogSP; ISwitch DeepSkyCatalogS[7]; ISwitchVectorProperty SolarSP; ISwitch SolarS[10]; INumberVectorProperty ObjectNoNP; INumber ObjectNoN[1]; INumberVectorProperty MaxSlewRateNP; INumber MaxSlewRateN[1]; INumberVectorProperty ElevationLimitNP; INumber ElevationLimitN[2]; private: int currentCatalog; int currentSubCatalog; }; libindi/drivers/telescope/lx200pulsar2.cpp0000664000175000017500000014650213263645557020025 0ustar jasemjasem/* Pulsar2 INDI driver Copyright (C) 2016, 2017 Jasem Mutlaq and Camiel Severijns 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 */ #include "lx200pulsar2.h" #include "indicom.h" #include "lx200driver.h" #include #include #include #include #include extern char lx200Name[MAXINDIDEVICE]; extern unsigned int DBG_SCOPE; namespace Pulsar2Commands { // Reimplement LX200 commands to solve intermittent problems with tcflush() calls on the input stream. // This implementation parses all input received from the Pulsar controller. static constexpr int TimeOut = 1; static constexpr int BufferSize = 32; static constexpr int MaxAttempts = 3; static constexpr char Null = '\0'; static constexpr char Acknowledge = '\006'; static constexpr char Termination = '#'; static bool resynchronize_needed = false; // Indicates whether the input and output on the port needs to be resynchronized due to a timeout error. int ACK(const int fd) { DEBUGFDEVICE(lx200Name, DBG_SCOPE, "CMD <%02X>", Acknowledge); if (write(fd, &Acknowledge, sizeof(Acknowledge)) < 0) { DEBUGFDEVICE(lx200Name, DBG_SCOPE, "Error sending ACK: %s", strerror(errno)); return -1; } char MountAlign[2]; int nbytes_read = 0; const int error_type = tty_read(fd, MountAlign, 1, TimeOut, &nbytes_read); if (error_type != TTY_OK) DEBUGFDEVICE(lx200Name, DBG_SCOPE, "Error receiving ACK: %s", strerror(errno)); else DEBUGFDEVICE(lx200Name, DBG_SCOPE, "RES <%c>", MountAlign[0]); return (nbytes_read == 1 ? MountAlign[0] : error_type); } void resynchronize(const int fd) { class ACKChecker { public: ACKChecker() : previous(Null) {} bool operator()(const int c) { // We need two successful acknowledges bool result = false; if (previous == Null) { if (c == 'P' || c == 'A' || c == 'L') previous = c; // Remember first acknowledge response } else { result = (c == previous); // Second acknowledge response must equal to previous one previous = Null; // Both on success or failure, reset the previous character } return result; } private: int previous; }; DEBUGDEVICE(lx200Name, DBG_SCOPE, "RESYNC"); ACKChecker valid; for (int c = ACK(fd); !valid(c); c = ACK(fd)) tcflush(fd, TCIFLUSH); resynchronize_needed = false; } // Send a command string without waiting for any response from the Pulsar controller bool send(const int fd, const char *cmd) { if (resynchronize_needed) resynchronize(fd); DEBUGFDEVICE(lx200Name, DBG_SCOPE, "CMD <%s>", cmd); const int nbytes = strlen(cmd); int nbytes_written = 0; do { const int errcode = tty_write(fd, &cmd[nbytes_written], nbytes - nbytes_written, &nbytes_written); if (errcode != TTY_OK) { char errmsg[MAXRBUF]; tty_error_msg(errcode, errmsg, MAXRBUF); DEBUGFDEVICE(lx200Name, DBG_SCOPE, "Error: %s (%s)", errmsg, strerror(errno)); return false; } } while (nbytes_written < nbytes); // Ensure that all characters have been sent return true; } // Send a command string and wait for a single character response indicating success or failure // Ignore leading # characters bool confirmed(const int fd, const char *cmd, char &response) { response = Termination; if (send(fd, cmd)) { for (int attempt = 0; response == Termination; ++attempt) { int nbytes_read = 0; const int errcode = tty_read(fd, &response, sizeof(response), TimeOut, &nbytes_read); if (errcode != TTY_OK) { char errmsg[MAXRBUF]; tty_error_msg(errcode, errmsg, MAXRBUF); DEBUGFDEVICE(lx200Name, DBG_SCOPE, "Error: %s (%s, attempt %d)", errmsg, strerror(errno), attempt); if (attempt == MaxAttempts - 1) { resynchronize_needed = true; return false; } } else // tty_read was successful and nbytes_read is garantueed to be 1 DEBUGFDEVICE(lx200Name, DBG_SCOPE, "RES <%c> (attempt %d)", response, attempt); } } return true; } // Receive a terminated response string bool receive(const int fd, char response[]) { response[0] = Null; bool done = false; int nbytes_read_total = 0; int attempt; for (attempt = 0; !done; ++attempt) { int nbytes_read = 0; const int errcode = tty_read_section(fd, response + nbytes_read_total, Termination, TimeOut, &nbytes_read); if (errcode != TTY_OK) { char errmsg[MAXRBUF]; tty_error_msg(errcode, errmsg, MAXRBUF); DEBUGFDEVICE(lx200Name, DBG_SCOPE, "Error: %s (%s, attempt %d)", errmsg, strerror(errno), attempt); nbytes_read_total += nbytes_read; // Keep track of how many characters have been read successfully despite the error if (attempt == MaxAttempts - 1) { resynchronize_needed = (errcode == TTY_TIME_OUT); response[nbytes_read_total] = Null; return false; } } else { // Skip response strings consisting of a single termination character if (nbytes_read_total == 0 && response[0] == Termination) response[0] = Null; else done = true; } } response[nbytes_read_total - 1] = Null; // Remove the termination character DEBUGFDEVICE(lx200Name, DBG_SCOPE, "RES <%s> (attempt %d)", response, attempt); return true; } inline bool getString(const int fd, const char *cmd, char response[]) { return (send(fd, cmd) && receive(fd, response)); } inline bool getVersion(const int fd, char response[]) { return getString(fd, ":YV#", response); } enum PECorrection { PECorrectionOff = 0, PECorrectionOn = 1 }; bool getPECorrection(const int fd, PECorrection *PECra, PECorrection *PECdec) { char response[8]; bool success = getString(fd, "#:YGP#", response); if (success) { success = (sscanf(response, "%1d,%1d", reinterpret_cast(PECra), reinterpret_cast(PECdec)) == 2); } return success; } enum RCorrection { RCorrectionOff = 0, RCorrectionOn = 1 }; bool getRCorrection(const int fd, RCorrection *Rra, RCorrection *Rdec) { char response[8]; bool success = getString(fd, "#:YGR#", response); if (success) { success = (sscanf(response, "%1d,%1d", reinterpret_cast(Rra), reinterpret_cast(Rdec)) == 2); } return success; } bool getInt(const int fd, const char *cmd, int *value) { char response[16]; bool success = getString(fd, cmd, response); if (success) { success = (sscanf(response, "%d", value) == 1); if (success) DEBUGFDEVICE(lx200Name, DBG_SCOPE, "VAL [%d]", *value); else DEBUGDEVICE(lx200Name, DBG_SCOPE, "Unable to parse response"); } return success; } enum SideOfPier { EastOfPier = 0, WestOfPier = 1 }; inline bool getSideOfPier(const int fd, SideOfPier *side_of_pier) { return getInt(fd, "#:YGN#", reinterpret_cast(side_of_pier)); } enum PoleCrossing { PoleCrossingOff = 0, PoleCrossingOn = 1 }; inline bool getPoleCrossing(const int fd, PoleCrossing *pole_crossing) { return getInt(fd, "#:YGQ#", reinterpret_cast(pole_crossing)); } bool getSexa(const int fd, const char *cmd, double *value) { char response[16]; bool success = getString(fd, cmd, response); if (success) { success = (f_scansexa(response, value) == 0); if (success) DEBUGFDEVICE(lx200Name, DBG_SCOPE, "VAL [%g]", *value); else DEBUGDEVICE(lx200Name, DBG_SCOPE, "Unable to parse response"); } return success; } inline bool getObjectRADec(const int fd, double *ra, double *dec) { return (getSexa(fd, "#:GR#", ra) && getSexa(fd, "#:GD#", dec)); } bool getDegreesMinutes(const int fd, const char *cmd, int *d, int *m) { *d = *m = 0; char response[16]; bool success = getString(fd, cmd, response); if (success) { success = (sscanf(response, "%d%*c%d", d, m) == 2); if (success) DEBUGFDEVICE(lx200Name, DBG_SCOPE, "VAL [%+03d:%02d]", *d, *m); else DEBUGDEVICE(lx200Name, DBG_SCOPE, "Unable to parse response"); } return success; } inline bool getSiteLatitude(const int fd, int *d, int *m) { return getDegreesMinutes(fd, "#:Gt#", d, m); } inline bool getSiteLongitude(const int fd, int *d, int *m) { return getDegreesMinutes(fd, "#:Gg#", d, m); } bool getUTCDate(const int fd, int *m, int *d, int *y) { char response[12]; bool success = getString(fd, "#:GC#", response); if (success) { success = (sscanf(response, "%2d%*c%2d%*c%2d", m, d, y) == 3); if (success) { *y += (*y < 50 ? 2000 : 1900); DEBUGFDEVICE(lx200Name, DBG_SCOPE, "VAL [%02d/%02d/%04d]", *m, *d, *y); } else DEBUGDEVICE(lx200Name, DBG_SCOPE, "Unable to parse date string"); } return success; } bool getUTCTime(const int fd, int *h, int *m, int *s) { char response[12]; bool success = getString(fd, "#:GL#", response); if (success) { success = (sscanf(response, "%2d%*c%2d%*c%2d", h, m, s) == 3); if (success) DEBUGFDEVICE(lx200Name, DBG_SCOPE, "VAL [%02d:%02d:%02d]", *h, *m, *s); else DEBUGDEVICE(lx200Name, DBG_SCOPE, "Unable to parse time string"); } return success; } bool setDegreesMinutes(const int fd, const char *cmd, const double value) { int degrees, minutes, seconds; getSexComponents(value, °rees, &minutes, &seconds); char full_cmd[32]; snprintf(full_cmd, sizeof(full_cmd), "#:%s %03d:%02d#", cmd, degrees, minutes); char response; return (confirmed(fd, full_cmd, response) && response == '1'); } inline bool setSite(const int fd, const double longitude, const double latitude) { return (setDegreesMinutes(fd, "Sl", 360.0 - longitude) && setDegreesMinutes(fd, "St", latitude)); } enum SlewMode { SlewMax = 0, SlewFind, SlewCenter, SlewGuide, NumSlewRates }; bool setSlewMode(const int fd, const SlewMode slewMode) { static const char *commands[NumSlewRates]{ "#:RS#", "#:RM#", "#:RC#", "#:RG#" }; return send(fd, commands[slewMode]); } enum Direction { North = 0, East, South, West, NumDirections }; static const char *DirectionName[NumDirections] = { "North", "East", "South", "West" }; bool moveTo(const int fd, const Direction direction) { static const char *commands[NumDirections] = { "#:Mn#", "#:Me#", "#:Ms#", "#:Mw#" }; return send(fd, commands[direction]); } bool haltMovement(const int fd, const Direction direction) { static const char *commands[NumDirections] = { "#:Qn#", "#:Qe#", "#:Qs#", "#:Qw#" }; return send(fd, commands[direction]); } inline bool startSlew(const int fd) { char response[4]; const bool success = (getString(fd, "#:MS#", response) && response[0] == '0'); return success; } inline bool abortSlew(const int fd) { return send(fd, "#:Q#"); } // Pulse guide commands are only supported by the Pulsar2 controller and not the older Pulsar controller bool pulseGuide(const int fd, const Direction direction, const float ms) { static const char code[NumDirections] = { 'n', 'e', 's', 'w' }; const int pulse = std::min(std::max(1, static_cast(1000.0 * (ms + 0.5))), 999); char full_cmd[16]; snprintf(full_cmd, sizeof(full_cmd), "#:MG%c%03d3#", code[direction], pulse); return send(fd, full_cmd); } bool setTime(const int fd, const int h, const int m, const int s) { char full_cmd[32]; snprintf(full_cmd, sizeof(full_cmd), "#:SL %02d:%02d:%02d#", h, m, s); char response; return (confirmed(fd, full_cmd, response) && response == '1'); } bool setDate(const int fd, const int dd, const int mm, const int yy) { char cmd[64]; snprintf(cmd, sizeof(cmd), ":SC %02d/%02d/%02d#", mm, dd, (yy % 100)); char response; const bool success = (confirmed(fd, cmd, response) && response == '1'); if (success) { // Read dumped data char dumpPlanetaryUpdateString[64]; int nbytes_read = 0; (void)tty_read_section(fd, dumpPlanetaryUpdateString, Termination, 1, &nbytes_read); (void)tty_read_section(fd, dumpPlanetaryUpdateString, Termination, 1, &nbytes_read); } return success; } bool ensureLongFormat(const int fd) { char response[16] = { 0 }; bool success = getString(fd, "#:GR#", response); if (response[5] == '.') { // In case of short format, set long format success = (confirmed(fd, "#:U#", response[0]) && response[0] == '1'); } return success; } bool setObjectRA(const int fd, const double ra) { int h, m, s; getSexComponents(ra, &h, &m, &s); char full_cmd[32]; snprintf(full_cmd, sizeof(full_cmd), "#:Sr %02d:%02d:%02d#", h, m, s); char response; return (confirmed(fd, full_cmd, response) && response == '1'); } bool setObjectDEC(const int fd, const double dec) { int d, m, s; getSexComponents(dec, &d, &m, &s); char full_cmd[32]; snprintf(full_cmd, sizeof(full_cmd), "#:Sd %+03d:%02d:%02d#", d, m, s); char response; return (confirmed(fd, full_cmd, response) && response == '1'); } inline bool setObjectRADec(const int fd, const double ra, const double dec) { return (setObjectRA(fd, ra) && setObjectDEC(fd, dec)); } inline bool park(const int fd) { int success = 0; return (getInt(fd, "#:YH#", &success) && success == 1); } inline bool unpark(const int fd) { int success = 0; return (getInt(fd, "#:YL#", &success) && success == 1); } inline bool sync(const int fd) { return send(fd, "#:CM#"); } static const char OnOff[2] = { '0', '1' }; bool setSideOfPier(const int fd, const SideOfPier side_of_pier) { static char cmd[] = "#:YSN_#"; cmd[5] = OnOff[side_of_pier]; char response; return (confirmed(fd, cmd, response) && response == '1'); } bool setPECorrection(const int fd, const PECorrection pec_ra, const PECorrection pec_dec) { static char cmd[] = "#:YSP_,_#"; cmd[5] = OnOff[pec_ra]; cmd[7] = OnOff[pec_dec]; char response; return (confirmed(fd, cmd, response) && response == '1'); } bool setPoleCrossing(const int fd, const PoleCrossing pole_crossing) { static char cmd[] = "#:YSQ_#"; cmd[5] = OnOff[pole_crossing]; char response; return (confirmed(fd, cmd, response) && response == '1'); } bool setRCorrection(const int fd, const RCorrection rc_ra, const RCorrection rc_dec) { static char cmd[] = "#:YSR_,_#"; cmd[5] = OnOff[rc_ra]; cmd[7] = OnOff[rc_dec]; char response; return (confirmed(fd, cmd, response) && response == '1'); } inline bool isHomeSet(const int fd) { int is_home_set = -1; return (getInt(fd, "#:YGh#", &is_home_set) && is_home_set == 1); } inline bool isParked(const int fd) { int is_parked = -1; return (getInt(fd, "#:YGk#", &is_parked) && is_parked == 1); } inline bool isParking(const int fd) { int is_parking = -1; return (getInt(fd, "#:YGj#", &is_parking) && is_parking == 1); } }; LX200Pulsar2::LX200Pulsar2() : LX200Generic(), just_started_slewing(false) { setVersion(1, 1); setLX200Capability(0); SetTelescopeCapability(TELESCOPE_CAN_SYNC | TELESCOPE_CAN_GOTO | TELESCOPE_CAN_PARK | TELESCOPE_CAN_ABORT | TELESCOPE_HAS_TIME | TELESCOPE_HAS_LOCATION | TELESCOPE_HAS_PIER_SIDE, 4); } const char *LX200Pulsar2::getDefaultName() { return static_cast("Pulsar2"); } bool LX200Pulsar2::Connect() { const bool success = INDI::Telescope::Connect(); if (success) { if (isParked()) { LOGF_DEBUG("%s", "Trying to wake up the mount."); UnPark(); } else LOGF_DEBUG("%s", "The mount is already tracking."); } return success; } bool LX200Pulsar2::Handshake() { // Anything needs to be done besides this? INDI::Telescope would call ReadScopeStatus but // maybe we need to UnPark() before ReadScopeStatus() can return valid results? return true; } bool LX200Pulsar2::ReadScopeStatus() { bool success = isConnected(); if (success) { success = isSimulation(); if (success) mountSim(); else { switch (TrackState) { case SCOPE_SLEWING: // Check if LX200 is done slewing if (isSlewComplete()) { // Set slew mode to "Centering" IUResetSwitch(&SlewRateSP); SlewRateS[SLEW_CENTERING].s = ISS_ON; IDSetSwitch(&SlewRateSP, nullptr); TrackState = SCOPE_TRACKING; IDMessage(getDeviceName(), "Slew is complete. Tracking..."); } break; case SCOPE_PARKING: if (isSlewComplete()) SetParked(true); break; default: break; } success = Pulsar2Commands::getObjectRADec(PortFD, ¤tRA, ¤tDEC); if (success) NewRaDec(currentRA, currentDEC); else { EqNP.s = IPS_ALERT; IDSetNumber(&EqNP, "Error reading RA/DEC."); } } } Pulsar2Commands::SideOfPier side_of_pier = Pulsar2Commands::EastOfPier; if (Pulsar2Commands::getSideOfPier(PortFD, &side_of_pier)) { //PierSideS[side_of_pier].s = ISS_ON; //IDSetSwitch(&PierSideSP, nullptr); setPierSide((side_of_pier == Pulsar2Commands::EastOfPier) ? PIER_EAST : PIER_WEST); } else { PierSideSP.s = IPS_ALERT; IDSetSwitch(&PierSideSP, "Can't check at which side of the pier the telescope is."); } return success; } void LX200Pulsar2::ISGetProperties(const char *dev) { if (dev != nullptr && strcmp(dev, getDeviceName()) != 0) return; LX200Generic::ISGetProperties(dev); /*if (isConnected()) { defineSwitch(&PeriodicErrorCorrectionSP); defineSwitch(&PoleCrossingSP); defineSwitch(&RefractionCorrectionSP); }*/ } bool LX200Pulsar2::initProperties() { const bool result = LX200Generic::initProperties(); if (result) { IUFillSwitch(&PeriodicErrorCorrectionS[0], "PEC_OFF", "Off", ISS_OFF); IUFillSwitch(&PeriodicErrorCorrectionS[1], "PEC_ON", "On", ISS_ON); IUFillSwitchVector(&PeriodicErrorCorrectionSP, PeriodicErrorCorrectionS, 2, getDeviceName(), "PE_CORRECTION", "P.E. Correction", MAIN_CONTROL_TAB, IP_RW, ISR_1OFMANY, 0, IPS_IDLE); IUFillSwitch(&PoleCrossingS[0], "POLE_CROSS_OFF", "Off", ISS_OFF); IUFillSwitch(&PoleCrossingS[1], "POLE_CROSS_ON", "On", ISS_ON); IUFillSwitchVector(&PoleCrossingSP, PoleCrossingS, 2, getDeviceName(), "POLE_CROSSING", "Pole Crossing", MAIN_CONTROL_TAB, IP_RW, ISR_1OFMANY, 0, IPS_IDLE); IUFillSwitch(&RefractionCorrectionS[0], "REFR_CORR_OFF", "Off", ISS_OFF); IUFillSwitch(&RefractionCorrectionS[1], "REFR_CORR_ON", "On", ISS_ON); IUFillSwitchVector(&RefractionCorrectionSP, RefractionCorrectionS, 2, getDeviceName(), "REFR_CORRECTION", "Refraction Corr.", MAIN_CONTROL_TAB, IP_RW, ISR_1OFMANY, 0, IPS_IDLE); // PierSide property is RW here so we override PierSideSP.p = IP_RW; } return result; } bool LX200Pulsar2::updateProperties() { LX200Generic::updateProperties(); if (isConnected()) { defineSwitch(&PeriodicErrorCorrectionSP); defineSwitch(&PoleCrossingSP); defineSwitch(&RefractionCorrectionSP); getBasicData(); } else { deleteProperty(PeriodicErrorCorrectionSP.name); deleteProperty(PoleCrossingSP.name); deleteProperty(RefractionCorrectionSP.name); } return true; } bool LX200Pulsar2::ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) { if (dev != nullptr && strcmp(dev, getDeviceName()) == 0) { if (strcmp(name, PierSideSP.name) == 0) { if (IUUpdateSwitch(&PierSideSP, states, names, n) < 0) return false; if (!isSimulation()) { // Define which side of the pier the telescope is. // Required for the sync command. This is *not* related to a meridian flip. const bool success = Pulsar2Commands::setSideOfPier( PortFD, (PierSideS[1].s == ISS_ON ? Pulsar2Commands::WestOfPier : Pulsar2Commands::EastOfPier)); if (success) { PierSideSP.s = IPS_OK; IDSetSwitch(&PierSideSP, nullptr); } else { PierSideSP.s = IPS_ALERT; IDSetSwitch(&PierSideSP, "Could not set side of mount"); } return success; } } if (strcmp(name, PeriodicErrorCorrectionSP.name) == 0) { if (IUUpdateSwitch(&PeriodicErrorCorrectionSP, states, names, n) < 0) return false; if (!isSimulation()) { // Only control PEC in RA. PEC in decl. doesn't seem usefull. const bool success = Pulsar2Commands::setPECorrection(PortFD, (PeriodicErrorCorrectionS[1].s == ISS_ON ? Pulsar2Commands::PECorrectionOn : Pulsar2Commands::PECorrectionOff), Pulsar2Commands::PECorrectionOff); if (success) { PeriodicErrorCorrectionSP.s = IPS_OK; IDSetSwitch(&PeriodicErrorCorrectionSP, nullptr); } else { PeriodicErrorCorrectionSP.s = IPS_ALERT; IDSetSwitch(&PeriodicErrorCorrectionSP, "Could not change the periodic error correction"); } return success; } } if (strcmp(name, PoleCrossingSP.name) == 0) { if (IUUpdateSwitch(&PoleCrossingSP, states, names, n) < 0) return false; if (!isSimulation()) { const bool success = Pulsar2Commands::setPoleCrossing(PortFD, (PoleCrossingS[1].s == ISS_ON ? Pulsar2Commands::PoleCrossingOn : Pulsar2Commands::PoleCrossingOff)); if (success) { PoleCrossingSP.s = IPS_OK; IDSetSwitch(&PoleCrossingSP, nullptr); } else { PoleCrossingSP.s = IPS_ALERT; IDSetSwitch(&PoleCrossingSP, "Could not change the pole crossing"); } return success; } } if (strcmp(name, RefractionCorrectionSP.name) == 0) { if (IUUpdateSwitch(&RefractionCorrectionSP, states, names, n) < 0) return false; if (!isSimulation()) { // Control refraction correction in both RA and decl. const Pulsar2Commands::RCorrection rc = (RefractionCorrectionS[1].s == ISS_ON ? Pulsar2Commands::RCorrectionOn : Pulsar2Commands::RCorrectionOff); const bool success = Pulsar2Commands::setRCorrection(PortFD, rc, rc); if (success) { RefractionCorrectionSP.s = IPS_OK; IDSetSwitch(&RefractionCorrectionSP, nullptr); } else { RefractionCorrectionSP.s = IPS_ALERT; IDSetSwitch(&RefractionCorrectionSP, "Could not change the refraction correction"); } return success; } } } // Nobody has claimed this, so pass it to the parent return LX200Generic::ISNewSwitch(dev, name, states, names, n); } bool LX200Pulsar2::ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n) { if (dev != nullptr && strcmp(dev, getDeviceName()) == 0) { // Nothing to do yet } return LX200Generic::ISNewText(dev, name, texts, names, n); } bool LX200Pulsar2::SetSlewRate(int index) { // Convert index to Meade format index = 3 - index; const bool success = (isSimulation() || Pulsar2Commands::setSlewMode(PortFD, static_cast(index))); if (success) { SlewRateSP.s = IPS_OK; IDSetSwitch(&SlewRateSP, nullptr); } else { SlewRateSP.s = IPS_ALERT; IDSetSwitch(&SlewRateSP, "Error setting slew mode."); } return success; } bool LX200Pulsar2::MoveNS(INDI_DIR_NS dir, TelescopeMotionCommand command) { const Pulsar2Commands::Direction current_move = (dir == DIRECTION_NORTH ? Pulsar2Commands::North : Pulsar2Commands::South); bool success = true; switch (command) { case MOTION_START: success = (isSimulation() || Pulsar2Commands::moveTo(PortFD, current_move)); if (success) LOGF_INFO("Moving toward %s.", Pulsar2Commands::DirectionName[current_move]); else LOG_ERROR("Error starting N/S motion."); break; case MOTION_STOP: success = (isSimulation() || Pulsar2Commands::haltMovement(PortFD, current_move)); if (success) LOGF_INFO("Movement toward %s halted.", Pulsar2Commands::DirectionName[current_move]); else LOG_ERROR("Error stopping N/S motion."); break; } return success; } bool LX200Pulsar2::MoveWE(INDI_DIR_WE dir, TelescopeMotionCommand command) { const Pulsar2Commands::Direction current_move = (dir == DIRECTION_WEST ? Pulsar2Commands::West : Pulsar2Commands::East); bool success = true; switch (command) { case MOTION_START: success = (isSimulation() || Pulsar2Commands::moveTo(PortFD, current_move)); if (success) LOGF_INFO("Moving toward %s.", Pulsar2Commands::DirectionName[current_move]); else LOG_ERROR("Error starting W/E motion."); break; case MOTION_STOP: success = (isSimulation() || Pulsar2Commands::haltMovement(PortFD, current_move)); if (success) LOGF_INFO("Movement toward %s halted.", Pulsar2Commands::DirectionName[current_move]); else LOG_ERROR("Error stopping W/E motion."); break; } return success; } bool LX200Pulsar2::Abort() { const bool success = (isSimulation() || Pulsar2Commands::abortSlew(PortFD)); if (success) { if (GuideNSNP.s == IPS_BUSY || GuideWENP.s == IPS_BUSY) { GuideNSNP.s = GuideWENP.s = IPS_IDLE; GuideNSN[0].value = GuideNSN[1].value = 0.0; GuideWEN[0].value = GuideWEN[1].value = 0.0; if (GuideNSTID) { IERmTimer(GuideNSTID); GuideNSTID = 0; } if (GuideWETID) { IERmTimer(GuideWETID); GuideNSTID = 0; } IDMessage(getDeviceName(), "Guide aborted."); IDSetNumber(&GuideNSNP, nullptr); IDSetNumber(&GuideWENP, nullptr); } } else LOG_ERROR("Failed to abort slew."); return success; } IPState LX200Pulsar2::GuideNorth(float ms) { const int use_pulse_cmd = IUFindOnSwitchIndex(&UsePulseCmdSP); if (!use_pulse_cmd && (MovementNSSP.s == IPS_BUSY || MovementWESP.s == IPS_BUSY)) { LOG_ERROR("Cannot guide while moving."); return IPS_ALERT; } // If already moving (no pulse command), then stop movement if (MovementNSSP.s == IPS_BUSY) { const int dir = IUFindOnSwitchIndex(&MovementNSSP); MoveNS(dir == 0 ? DIRECTION_NORTH : DIRECTION_SOUTH, MOTION_STOP); } if (GuideNSTID) { IERmTimer(GuideNSTID); GuideNSTID = 0; } if (use_pulse_cmd) (void)Pulsar2Commands::pulseGuide(PortFD, Pulsar2Commands::North, ms); else { if (!Pulsar2Commands::setSlewMode(PortFD, Pulsar2Commands::SlewGuide)) { SlewRateSP.s = IPS_ALERT; IDSetSwitch(&SlewRateSP, "Error setting slew mode."); return IPS_ALERT; } MovementNSS[0].s = ISS_ON; MoveNS(DIRECTION_NORTH, MOTION_START); } // Set slew to guiding IUResetSwitch(&SlewRateSP); SlewRateS[SLEW_GUIDE].s = ISS_ON; IDSetSwitch(&SlewRateSP, nullptr); guide_direction = LX200_NORTH; GuideNSTID = IEAddTimer(ms, guideTimeoutHelper, this); return IPS_BUSY; } IPState LX200Pulsar2::GuideSouth(float ms) { const int use_pulse_cmd = IUFindOnSwitchIndex(&UsePulseCmdSP); if (!use_pulse_cmd && (MovementNSSP.s == IPS_BUSY || MovementWESP.s == IPS_BUSY)) { LOG_ERROR("Cannot guide while moving."); return IPS_ALERT; } // If already moving (no pulse command), then stop movement if (MovementNSSP.s == IPS_BUSY) { const int dir = IUFindOnSwitchIndex(&MovementNSSP); MoveNS(dir == 0 ? DIRECTION_NORTH : DIRECTION_SOUTH, MOTION_STOP); } if (GuideNSTID) { IERmTimer(GuideNSTID); GuideNSTID = 0; } if (use_pulse_cmd) (void)Pulsar2Commands::pulseGuide(PortFD, Pulsar2Commands::South, ms); else { if (!Pulsar2Commands::setSlewMode(PortFD, Pulsar2Commands::SlewGuide)) { SlewRateSP.s = IPS_ALERT; IDSetSwitch(&SlewRateSP, "Error setting slew mode."); return IPS_ALERT; } MovementNSS[1].s = ISS_ON; MoveNS(DIRECTION_SOUTH, MOTION_START); } // Set slew to guiding IUResetSwitch(&SlewRateSP); SlewRateS[SLEW_GUIDE].s = ISS_ON; IDSetSwitch(&SlewRateSP, nullptr); guide_direction = LX200_SOUTH; GuideNSTID = IEAddTimer(ms, guideTimeoutHelper, this); return IPS_BUSY; } IPState LX200Pulsar2::GuideEast(float ms) { const int use_pulse_cmd = IUFindOnSwitchIndex(&UsePulseCmdSP); if (!use_pulse_cmd && (MovementNSSP.s == IPS_BUSY || MovementWESP.s == IPS_BUSY)) { LOG_ERROR("Cannot guide while moving."); return IPS_ALERT; } // If already moving (no pulse command), then stop movement if (MovementWESP.s == IPS_BUSY) { const int dir = IUFindOnSwitchIndex(&MovementWESP); MoveWE(dir == 0 ? DIRECTION_WEST : DIRECTION_EAST, MOTION_STOP); } if (GuideWETID) { IERmTimer(GuideWETID); GuideWETID = 0; } if (use_pulse_cmd) (void)Pulsar2Commands::pulseGuide(PortFD, Pulsar2Commands::East, ms); else { if (!Pulsar2Commands::setSlewMode(PortFD, Pulsar2Commands::SlewGuide)) { SlewRateSP.s = IPS_ALERT; IDSetSwitch(&SlewRateSP, "Error setting slew mode."); return IPS_ALERT; } MovementWES[1].s = ISS_ON; MoveWE(DIRECTION_EAST, MOTION_START); } // Set slew to guiding IUResetSwitch(&SlewRateSP); SlewRateS[SLEW_GUIDE].s = ISS_ON; IDSetSwitch(&SlewRateSP, nullptr); guide_direction = LX200_EAST; GuideWETID = IEAddTimer(ms, guideTimeoutHelper, this); return IPS_BUSY; } IPState LX200Pulsar2::GuideWest(float ms) { const int use_pulse_cmd = IUFindOnSwitchIndex(&UsePulseCmdSP); if (!use_pulse_cmd && (MovementNSSP.s == IPS_BUSY || MovementWESP.s == IPS_BUSY)) { LOG_ERROR("Cannot guide while moving."); return IPS_ALERT; } // If already moving (no pulse command), then stop movement if (MovementWESP.s == IPS_BUSY) { const int dir = IUFindOnSwitchIndex(&MovementWESP); MoveWE(dir == 0 ? DIRECTION_WEST : DIRECTION_EAST, MOTION_STOP); } if (GuideWETID) { IERmTimer(GuideWETID); GuideWETID = 0; } if (use_pulse_cmd) (void)Pulsar2Commands::pulseGuide(PortFD, Pulsar2Commands::West, ms); else { if (!Pulsar2Commands::setSlewMode(PortFD, Pulsar2Commands::SlewGuide)) { SlewRateSP.s = IPS_ALERT; IDSetSwitch(&SlewRateSP, "Error setting slew mode."); return IPS_ALERT; } MovementWES[0].s = ISS_ON; MoveWE(DIRECTION_WEST, MOTION_START); } // Set slew to guiding IUResetSwitch(&SlewRateSP); SlewRateS[SLEW_GUIDE].s = ISS_ON; IDSetSwitch(&SlewRateSP, nullptr); guide_direction = LX200_WEST; GuideWETID = IEAddTimer(ms, guideTimeoutHelper, this); return IPS_BUSY; } bool LX200Pulsar2::updateTime(ln_date *utc, double utc_offset) { INDI_UNUSED(utc_offset); bool success = true; if (!isSimulation()) { struct ln_zonedate ltm; ln_date_to_zonedate(utc, <m, 0.0); // One should use UTC only with Pulsar! JD = ln_get_julian_day(utc); LOGF_DEBUG("New JD is %f", static_cast(JD)); success = Pulsar2Commands::setTime(PortFD, ltm.hours, ltm.minutes, ltm.seconds); if (success) { success = Pulsar2Commands::setDate(PortFD, ltm.days, ltm.months, ltm.years); if (success) LOG_INFO("Time updated, updating planetary data..."); else LOG_ERROR("Error setting UTC date."); } else LOG_ERROR("Error setting UTC time."); // Pulsar cannot set UTC offset (?) } return success; } bool LX200Pulsar2::updateLocation(double latitude, double longitude, double elevation) { INDI_UNUSED(elevation); bool success = true; if (!isSimulation()) { success = Pulsar2Commands::setSite(PortFD, longitude, latitude); if (success) { char l[32], L[32]; fs_sexa(l, latitude, 3, 3600); fs_sexa(L, longitude, 4, 3600); IDMessage(getDeviceName(), "Site location updated to Lat %.32s - Long %.32s", l, L); } else LOG_ERROR("Error setting site coordinates"); } return success; } bool LX200Pulsar2::Goto(double r, double d) { const struct timespec timeout = {0, 100000000L}; char RAStr[64], DecStr[64]; fs_sexa(RAStr, targetRA = r, 2, 3600); fs_sexa(DecStr, targetDEC = d, 2, 3600); // If moving, let's stop it first. if (EqNP.s == IPS_BUSY) { if (!isSimulation() && !Pulsar2Commands::abortSlew(PortFD)) { AbortSP.s = IPS_ALERT; IDSetSwitch(&AbortSP, "Abort slew failed."); return false; } AbortSP.s = IPS_OK; EqNP.s = IPS_IDLE; IDSetSwitch(&AbortSP, "Slew aborted."); IDSetNumber(&EqNP, nullptr); if (MovementNSSP.s == IPS_BUSY || MovementWESP.s == IPS_BUSY) { MovementNSSP.s = MovementWESP.s = IPS_IDLE; EqNP.s = IPS_IDLE; IUResetSwitch(&MovementNSSP); IUResetSwitch(&MovementWESP); IDSetSwitch(&MovementNSSP, nullptr); IDSetSwitch(&MovementWESP, nullptr); } nanosleep(&timeout, NULL); } if (!isSimulation()) { if (!Pulsar2Commands::setObjectRADec(PortFD, targetRA, targetDEC)) { EqNP.s = IPS_ALERT; IDSetNumber(&EqNP, "Error setting RA/DEC."); return false; } if (!Pulsar2Commands::startSlew(PortFD)) { EqNP.s = IPS_ALERT; IDSetNumber(&EqNP, "Error Slewing to JNow RA %s - DEC %s\n", RAStr, DecStr); slewError(3); return false; } just_started_slewing = true; } TrackState = SCOPE_SLEWING; EqNP.s = IPS_BUSY; LOGF_INFO("Slewing to RA: %s - DEC: %s", RAStr, DecStr); return true; } bool LX200Pulsar2::Park() { const struct timespec timeout = {0, 100000000L}; if (!isSimulation()) { if (!Pulsar2Commands::isHomeSet(PortFD)) { ParkSP.s = IPS_ALERT; IDSetSwitch(&ParkSP, "No parking position defined."); return false; } if (Pulsar2Commands::isParked(PortFD)) { ParkSP.s = IPS_ALERT; IDSetSwitch(&ParkSP, "Scope has already been parked."); return false; } } // If scope is moving, let's stop it first. if (EqNP.s == IPS_BUSY) { if (!isSimulation() && !Pulsar2Commands::abortSlew(PortFD)) { AbortSP.s = IPS_ALERT; IDSetSwitch(&AbortSP, "Abort slew failed."); return false; } AbortSP.s = IPS_OK; EqNP.s = IPS_IDLE; IDSetSwitch(&AbortSP, "Slew aborted."); IDSetNumber(&EqNP, nullptr); if (MovementNSSP.s == IPS_BUSY || MovementWESP.s == IPS_BUSY) { MovementNSSP.s = MovementWESP.s = IPS_IDLE; EqNP.s = IPS_IDLE; IUResetSwitch(&MovementNSSP); IUResetSwitch(&MovementWESP); IDSetSwitch(&MovementNSSP, nullptr); IDSetSwitch(&MovementWESP, nullptr); } nanosleep(&timeout, NULL); } if (!isSimulation() && !Pulsar2Commands::park(PortFD)) { ParkSP.s = IPS_ALERT; IDSetSwitch(&ParkSP, "Parking Failed."); return false; } ParkSP.s = IPS_BUSY; TrackState = SCOPE_PARKING; IDMessage(getDeviceName(), "Parking telescope in progress..."); return true; } bool LX200Pulsar2::Sync(double ra, double dec) { const struct timespec timeout = {0, 300000000L}; bool result = true; if (!isSimulation()) { result = Pulsar2Commands::setObjectRADec(PortFD, ra, dec); if (!result) { EqNP.s = IPS_ALERT; IDSetNumber(&EqNP, "Error setting RA/DEC. Unable to Sync."); } else { nanosleep(&timeout, NULL); // This seems to be necessary result = Pulsar2Commands::sync(PortFD); if (result) { LOG_INFO("Reading sync response"); // Pulsar sends coordinates separated by # characters (##) char RAresponse[Pulsar2Commands::BufferSize]; result = Pulsar2Commands::receive(PortFD, RAresponse); if (result) { LOGF_DEBUG("First synchronization string: '%s'.", RAresponse); char DECresponse[Pulsar2Commands::BufferSize]; result = Pulsar2Commands::receive(PortFD, DECresponse); if (result) LOGF_DEBUG("Second synchronization string: '%s'.", DECresponse); } //TODO: Check that the received coordinates match the original coordinates if (!result) { EqNP.s = IPS_ALERT; IDSetNumber(&EqNP, "Synchronization failed."); } } } } if (result) { currentRA = ra; currentDEC = dec; LOG_INFO("Synchronization successful."); EqNP.s = IPS_OK; NewRaDec(currentRA, currentDEC); } return result; } bool LX200Pulsar2::UnPark() { if (!isSimulation()) { if (!Pulsar2Commands::isParked(PortFD)) { ParkSP.s = IPS_ALERT; IDSetSwitch(&ParkSP, "Mount is not parked."); return false; } if (!Pulsar2Commands::unpark(PortFD)) { ParkSP.s = IPS_ALERT; IDSetSwitch(&ParkSP, "Unparking failed."); return false; } } ParkSP.s = IPS_OK; TrackState = SCOPE_IDLE; SetParked(false); IDMessage(getDeviceName(), "Telescope has been unparked."); return true; } bool LX200Pulsar2::isSlewComplete() { bool result = false; switch (TrackState) { case SCOPE_SLEWING: result = !isSlewing(); break; case SCOPE_PARKING: result = !Pulsar2Commands::isParking(PortFD); break; default: break; } return result; } bool LX200Pulsar2::checkConnection() { const struct timespec timeout = {0, 50000000L}; if (isSimulation()) return true; if (LX200Generic::checkConnection()) { LOG_DEBUG("Checking Pulsar version ..."); for (int i = 0; i < 2; ++i) { char response[Pulsar2Commands::BufferSize]; if (Pulsar2Commands::getVersion(PortFD, response)) { // Determine which Pulsar version this is. Expected response similar to: 'PULSAR V2.66aR ,2008.12.10. #' char version[16]; int year, month, day; (void)sscanf(response, "PULSAR V%8s ,%4d.%2d.%2d. ", version, &year, &month, &day); LOGF_INFO("%s version %s dated %04d.%02d.%02d", (version[0] > '2' ? "Pulsar2" : "Pulsar"), version, year, month, day); return true; } nanosleep(&timeout, NULL); } } return false; } void LX200Pulsar2::getBasicData() { if (!isSimulation()) { if (!Pulsar2Commands::ensureLongFormat(PortFD)) { LOG_DEBUG("Failed to ensure that long format coordinates are used."); } if (!Pulsar2Commands::getObjectRADec(PortFD, ¤tRA, ¤tDEC)) { EqNP.s = IPS_ALERT; IDSetNumber(&EqNP, "Error reading RA/DEC."); return; } NewRaDec(currentRA, currentDEC); Pulsar2Commands::SideOfPier side_of_pier = Pulsar2Commands::EastOfPier; if (Pulsar2Commands::getSideOfPier(PortFD, &side_of_pier)) { //PierSideS[side_of_pier].s = ISS_ON; //IDSetSwitch(&PierSideSP, nullptr); setPierSide((side_of_pier == Pulsar2Commands::EastOfPier) ? PIER_EAST : PIER_WEST); } else { PierSideSP.s = IPS_ALERT; IDSetSwitch(&PierSideSP, "Can't check at which side of the pier the telescope is."); } // There are separate values for RA and DEC but we only use the RA value for now Pulsar2Commands::PECorrection pec_ra = Pulsar2Commands::PECorrectionOff, pec_dec = Pulsar2Commands::PECorrectionOff; if (Pulsar2Commands::getPECorrection(PortFD, &pec_ra, &pec_dec)) { PeriodicErrorCorrectionS[pec_ra].s = ISS_ON; IDSetSwitch(&PeriodicErrorCorrectionSP, nullptr); } else { PeriodicErrorCorrectionSP.s = IPS_ALERT; IDSetSwitch(&PeriodicErrorCorrectionSP, "Can't check whether PEC is enabled."); } Pulsar2Commands::PoleCrossing pole_crossing = Pulsar2Commands::PoleCrossingOff; if (Pulsar2Commands::getPoleCrossing(PortFD, &pole_crossing)) { PoleCrossingS[pole_crossing].s = ISS_ON; IDSetSwitch(&PoleCrossingSP, nullptr); } else { PoleCrossingSP.s = IPS_ALERT; IDSetSwitch(&PoleCrossingSP, "Can't check whether pole crossing is enabled."); } // There are separate values for RA and DEC but we only use the RA value for now Pulsar2Commands::RCorrection rc_ra = Pulsar2Commands::RCorrectionOff, rc_dec = Pulsar2Commands::RCorrectionOn; if (Pulsar2Commands::getRCorrection(PortFD, &rc_ra, &rc_dec)) { RefractionCorrectionS[rc_ra].s = ISS_ON; IDSetSwitch(&RefractionCorrectionSP, nullptr); } else { RefractionCorrectionSP.s = IPS_ALERT; IDSetSwitch(&RefractionCorrectionSP, "Can't check whether refraction correction is enabled."); } } sendScopeLocation(); sendScopeTime(); } void LX200Pulsar2::sendScopeLocation() { LocationNP.s = IPS_OK; int dd = 29, mm = 30; if (isSimulation() || Pulsar2Commands::getSiteLatitude(PortFD, &dd, &mm)) { LocationNP.np[0].value = (dd < 0 ? -1 : 1) * (abs(dd) + mm / 60.0); if (isDebug()) { IDLog("Pulsar latitude: %d:%d\n", dd, mm); IDLog("INDI Latitude: %g\n", LocationNP.np[0].value); } } else { IDMessage(getDeviceName(), "Failed to get site latitude from Pulsar controller."); LocationNP.s = IPS_ALERT; } dd = 48, mm = 0; if (isSimulation() || Pulsar2Commands::getSiteLongitude(PortFD, &dd, &mm)) { LocationNP.np[1].value = (dd > 0 ? 360.0 - (dd + mm / 60.0) : -(dd - mm / 60.0)); if (isDebug()) { IDLog("Pulsar longitude: %d:%d\n", dd, mm); IDLog("INDI Longitude: %g\n", LocationNP.np[1].value); } } else { IDMessage(getDeviceName(), "Failed to get site longitude from Pulsar controller."); LocationNP.s = IPS_ALERT; } IDSetNumber(&LocationNP, nullptr); } void LX200Pulsar2::sendScopeTime() { struct tm ltm; if (isSimulation()) { const time_t t = time(nullptr); if (gmtime_r(&t, <m) == nullptr) return; } else { if (!Pulsar2Commands::getUTCTime(PortFD, <m.tm_hour, <m.tm_min, <m.tm_sec) || !Pulsar2Commands::getUTCDate(PortFD, <m.tm_mon, <m.tm_mday, <m.tm_year)) return; ltm.tm_mon -= 1; ltm.tm_year -= 1900; } // Get time epoch and convert to TimeT const time_t time_epoch = mktime(<m); struct tm utm; localtime_r(&time_epoch, &utm); // Format it into ISO 8601 char cdate[32]; strftime(cdate, sizeof(cdate), "%Y-%m-%dT%H:%M:%S", &utm); IUSaveText(&TimeT[0], cdate); IUSaveText(&TimeT[1], "0"); // Pulsar maintains time in UTC only if (isDebug()) { IDLog("Telescope Local Time: %02d:%02d:%02d\n", ltm.tm_hour, ltm.tm_min, ltm.tm_sec); IDLog("Telescope TimeT Offset: %s\n", TimeT[1].text); IDLog("Telescope UTC Time: %s\n", TimeT[0].text); } // Let's send everything to the client IDSetText(&TimeTP, nullptr); } void LX200Pulsar2::guideTimeoutHelper(void *p) { static_cast(p)->guideTimeout(); } void LX200Pulsar2::guideTimeout() { const int use_pulse_cmd = IUFindOnSwitchIndex(&UsePulseCmdSP); if (guide_direction == -1) { Pulsar2Commands::haltMovement(PortFD, Pulsar2Commands::North); Pulsar2Commands::haltMovement(PortFD, Pulsar2Commands::South); Pulsar2Commands::haltMovement(PortFD, Pulsar2Commands::East); Pulsar2Commands::haltMovement(PortFD, Pulsar2Commands::West); MovementNSSP.s = IPS_IDLE; MovementWESP.s = IPS_IDLE; IUResetSwitch(&MovementNSSP); IUResetSwitch(&MovementWESP); IDSetSwitch(&MovementNSSP, nullptr); IDSetSwitch(&MovementWESP, nullptr); IERmTimer(GuideNSTID); IERmTimer(GuideWETID); } else if (!use_pulse_cmd) { switch (guide_direction) { case LX200_NORTH: case LX200_SOUTH: MoveNS(guide_direction == LX200_NORTH ? DIRECTION_NORTH : DIRECTION_SOUTH, MOTION_STOP); GuideNSNP.np[(guide_direction == LX200_NORTH ? 0 : 1)].value = 0; GuideNSNP.s = IPS_IDLE; IDSetNumber(&GuideNSNP, nullptr); MovementNSSP.s = IPS_IDLE; IUResetSwitch(&MovementNSSP); IDSetSwitch(&MovementNSSP, nullptr); break; case LX200_WEST: case LX200_EAST: MoveWE(guide_direction == LX200_WEST ? DIRECTION_WEST : DIRECTION_EAST, MOTION_STOP); GuideWENP.np[(guide_direction == LX200_WEST ? 0 : 1)].value = 0; GuideWENP.s = IPS_IDLE; IDSetNumber(&GuideWENP, nullptr); MovementWESP.s = IPS_IDLE; IUResetSwitch(&MovementWESP); IDSetSwitch(&MovementWESP, nullptr); break; } } if (guide_direction == LX200_NORTH || guide_direction == LX200_SOUTH || guide_direction == -1) { GuideNSNP.np[0].value = 0; GuideNSNP.np[1].value = 0; GuideNSNP.s = IPS_IDLE; GuideNSTID = 0; IDSetNumber(&GuideNSNP, nullptr); } if (guide_direction == LX200_WEST || guide_direction == LX200_EAST || guide_direction == -1) { GuideWENP.np[0].value = 0; GuideWENP.np[1].value = 0; GuideWENP.s = IPS_IDLE; GuideWETID = 0; IDSetNumber(&GuideWENP, nullptr); } } bool LX200Pulsar2::isSlewing() { // A problem with the Pulsar controller is that the :YGi# command starts // returning the value 1 only a few seconds after a slew has been started. // This also means that a (short) slew can end before this happens. auto mount_is_off_target = [this](void) { return (fabs(currentRA - targetRA) > 1.0 / 3600.0 || fabs(currentDEC - targetDEC) > 5.0 / 3600.0); }; // Detect the end of a short slew bool result = (just_started_slewing ? mount_is_off_target() : true); if (result) { int is_slewing = -1; if (Pulsar2Commands::getInt(PortFD, "#:YGi#", &is_slewing)) { if (is_slewing == 1) // When the Pulsar controller indicates that it is slewing, we can rely on it from now on result = true, just_started_slewing = false; else // ... otherwise we have to rely on the value of the attribute just_started_slewing result = just_started_slewing; } else // Fallback in case of error result = mount_is_off_target(); } // Make sure that just_started_slewing is reset at the end of a slew if (!result) just_started_slewing = false; return result; } libindi/drivers/telescope/lx200driver.h0000664000175000017500000002504313263645557017371 0ustar jasemjasem/* LX200 Driver Copyright (C) 2003 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 */ #pragma once /* Slew speeds */ enum TSlew { LX200_SLEW_MAX, LX200_SLEW_FIND, LX200_SLEW_CENTER, LX200_SLEW_GUIDE }; /* Alignment modes */ enum TAlign { LX200_ALIGN_POLAR, LX200_ALIGN_ALTAZ, LX200_ALIGN_LAND }; /* Directions */ enum TDirection { LX200_NORTH, LX200_WEST, LX200_EAST, LX200_SOUTH, LX200_ALL }; /* Formats of Right Ascension and Declination */ enum TFormat { LX200_SHORT_FORMAT, LX200_LONG_FORMAT, LX200_LONGER_FORMAT }; /* Time Format */ enum TTimeFormat { LX200_24, LX200_AM, LX200_PM }; /* Focus operation */ enum TFocusMotion { LX200_FOCUSIN, LX200_FOCUSOUT }; enum TFocusSpeed { LX200_HALTFOCUS = 0, LX200_FOCUSSLOW, LX200_FOCUSFAST }; /* Library catalogs */ enum TCatalog { LX200_STAR_C, LX200_DEEPSKY_C }; /* Frequency mode */ enum StarCatalog { LX200_STAR, LX200_SAO, LX200_GCVS }; /* Deep Sky Catalogs */ enum DeepSkyCatalog { LX200_NGC, LX200_IC, LX200_UGC, LX200_CALDWELL, LX200_ARP, LX200_ABELL, LX200_MESSIER_C }; /* Mount tracking frequency, in Hz */ enum TFreq { LX200_TRACK_SIDEREAL, LX200_TRACK_SOLAR, LX200_TRACK_LUNAR, LX200_TRACK_MANUAL }; #define MaxReticleDutyCycle 15 #define MaxFocuserSpeed 4 /* GET formatted sexagisemal value from device, return as double */ #define getLX200RA(fd, x) getCommandSexa(fd, x, ":GR#") #define getLX200DEC(fd, x) getCommandSexa(fd, x, ":GD#") #define getObjectRA(fd, x) getCommandSexa(fd, x, ":Gr#") #define getObjectDEC(fd, x) getCommandSexa(fd, x, ":Gd#") #define getLocalTime12(fd, x) getCommandSexa(fd, x, ":Ga#") #define getLocalTime24(fd, x) getCommandSexa(fd, x, ":GL#") #define getSDTime(fd, x) getCommandSexa(fd, x, ":GS#") #define getLX200Alt(fd, x) getCommandSexa(fd, x, ":GA#") #define getLX200Az(fd, x) getCommandSexa(fd, x, ":GZ#") /* GET String from device and store in supplied buffer x */ #define getObjectInfo(fd, x) getCommandString(fd, x, ":LI#") #define getVersionDate(fd, x) getCommandString(fd, x, ":GVD#") #define getVersionTime(fd, x) getCommandString(fd, x, ":GVT#") #define getFullVersion(fd, x) getCommandString(fd, x, ":GVF#") #define getVersionNumber(fd, x) getCommandString(fd, x, ":GVN#") #define getProductName(fd, x) getCommandString(fd, x, ":GVP#") #define turnGPS_StreamOn(fd) getCommandString(fd, x, ":gps#") /* GET Int from device and store in supplied pointer to integer x */ #define getUTCOffset(fd, x) getCommandInt(fd, x, ":GG#") #define getMaxElevationLimit(fd, x) getCommandInt(fd, x, ":Go#") #define getMinElevationLimit(fd, x) getCommandInt(fd, x, ":Gh#") /* Generic set, x is an integer */ #define setReticleDutyFlashCycle(fd, x) setCommandInt(fd, x, ":BD") #define setReticleFlashRate(fd, x) setCommandInt(fd, x, ":B") #define setFocuserSpeed(fd, x) setCommandInt(fd, x, ":F") #define setSlewSpeed(fd, x) setCommandInt(fd, x, ":Sw") /* Set X:Y:Z */ #define setLocalTime(fd, x, y, z) setCommandXYZ(fd, x, y, z, ":SL") #define setSDTime(fd, x, y, z) setCommandXYZ(fd, x, y, z, ":SS") /* GPS Specefic */ #define turnGPSOn(fd) write(fd, ":g+#", 4) #define turnGPSOff(fd) write(fd, ":g-#", 4) #define alignGPSScope(fd) write(fd, ":Aa#", 4) #define gpsSleep(fd) write(fd, ":hN#", 4) #define gpsWakeUp(fd) write(fd, ":hW#", 4); #define gpsRestart(fd) write(fd, ":I#", 3); #define updateGPS_System(fd) setStandardProcedure(fd, ":gT#") #define enableDecAltPec(fd) write(fd, ":QA+#", 4) #define disableDecAltPec(fd) write(fd, ":QA-#", 4) #define enableRaAzPec(fd) write(fd, ":QZ+#", 4) #define disableRaAzPec(fd) write(fd, ":QZ-#", 4) #define activateAltDecAntiBackSlash(fd) write(fd, ":$BAdd#", 7) #define activateAzRaAntiBackSlash(fd) write(fd, ":$BZdd#", 7) #define SelenographicSync(fd) write(fd, ":CL#", 4); #define slewToAltAz(fd) setStandardProcedure(fd, ":MA#") #define toggleTimeFormat(fd) write(fd, ":H#", 3) #define increaseReticleBrightness(fd) write(fd, ":B+#", 4) #define decreaseReticleBrightness(fd) write(fd, ":B-#", 4) #define turnFanOn(fd) write(fd, ":f+#", 4) #define turnFanOff(fd) write(fd, ":f-#", 4) #define seekHomeAndSave(fd) write(fd, ":hS#", 4) #define seekHomeAndSet(fd) write(fd, ":hF#", 4) #define turnFieldDeRotatorOn(fd) write(fd, ":r+#", 4) #define turnFieldDeRotatorOff(fd) write(fd, ":r-#", 4) #define slewToPark(fd) write(fd, ":hP#", 4) #define initTelescope(fd) write(fd, ":I#", 3) /************************************************************************** Basic I/O - OBSELETE **************************************************************************/ /*int openPort(const char *portID); int portRead(char *buf, int nbytes, int timeout); int portWrite(const char * buf); int LX200readOut(int timeout); int Connect(const char* device); void Disconnect();*/ /************************************************************************** Diagnostics **************************************************************************/ char ACK(int fd); /*int testTelescope(); int testAP();*/ int check_lx200_connection(int fd); /************************************************************************** Get Commands: store data in the supplied buffer. Return 0 on success or -1 on failure **************************************************************************/ /* Get Double from Sexagisemal */ int getCommandSexa(int fd, double *value, const char *cmd); /* Get String */ int getCommandString(int fd, char *data, const char *cmd); /* Get Int */ int getCommandInt(int fd, int *value, const char *cmd); /* Get tracking frequency */ int getTrackFreq(int fd, double *value); /* Get site Latitude */ int getSiteLatitude(int fd, int *dd, int *mm); /* Get site Longitude */ int getSiteLongitude(int fd, int *ddd, int *mm); /* Get Calender data */ int getCalendarDate(int fd, char *date); /* Get site Name */ int getSiteName(int fd, char *siteName, int siteNum); /* Get Home Search Status */ int getHomeSearchStatus(int fd, int *status); /* Get OTA Temperature */ int getOTATemp(int fd, double *value); /* Get time format: 12 or 24 */ int getTimeFormat(int fd, int *format); /* Get RA, DEC from Sky Commander controller */ int updateSkyCommanderCoord(int fd, double *ra, double *dec); /* Get RA, DEC from Intelliscope/SkyWizard controllers */ int updateIntelliscopeCoord(int fd, double *ra, double *dec); /************************************************************************** Set Commands **************************************************************************/ /* Set Int */ int setCommandInt(int fd, int data, const char *cmd); /* Set Sexigesimal */ int setCommandXYZ(int fd, int x, int y, int z, const char *cmd); /* Common routine for Set commands */ int setStandardProcedure(int fd, const char *writeData); /* Set Slew Mode */ int setSlewMode(int fd, int slewMode); /* Set Alignment mode */ int setAlignmentMode(int fd, unsigned int alignMode); /* Set Object RA */ int setObjectRA(int fd, double ra); /* set Object DEC */ int setObjectDEC(int fd, double dec); /* Set Calender date */ int setCalenderDate(int fd, int dd, int mm, int yy); /* Set UTC offset */ int setUTCOffset(int fd, double hours); /* Set Track Freq */ int setTrackFreq(int fd, double trackF); /* Set current site longitude */ int setSiteLongitude(int fd, double Long); /* Set current site latitude */ int setSiteLatitude(int fd, double Lat); /* Set Object Azimuth */ int setObjAz(int fd, double az); /* Set Object Altitude */ int setObjAlt(int fd, double alt); /* Set site name */ int setSiteName(int fd, char *siteName, int siteNum); /* Set maximum slew rate */ int setMaxSlewRate(int fd, int slewRate); /* Set focuser motion */ int setFocuserMotion(int fd, int motionType); /* SET GPS Focuser raneg (1 to 4) */ int setGPSFocuserSpeed(int fd, int speed); /* Set focuser speed mode */ int setFocuserSpeedMode(int fd, int speedMode); /* Set minimum elevation limit */ int setMinElevationLimit(int fd, int min); /* Set maximum elevation limit */ int setMaxElevationLimit(int fd, int max); /************************************************************************** Motion Commands **************************************************************************/ /* Slew to the selected coordinates */ int Slew(int fd); /* Synchronize to the selected coordinates and return the matching object if any */ int Sync(int fd, char *matchedObject); /* Abort slew in all axes */ int abortSlew(int fd); /* Move into one direction, two valid directions can be stacked */ int MoveTo(int fd, int direction); /* Halt movement in a particular direction */ int HaltMovement(int fd, int direction); /* Select the tracking mode */ int selectTrackingMode(int fd, int trackMode); /* Is Slew complete? 0 if complete, 1 if in progress, otherwise return an error */ int isSlewComplete(int fd); /* Send Pulse-Guide command (timed guide move), two valid directions can be stacked */ int SendPulseCmd(int fd, int direction, int duration_msec); /************************************************************************** Other Commands **************************************************************************/ /* Determines LX200 RA/DEC format, tries to set to long if found short */ int checkLX200Format(int fd); /* return the controller_format enum value */ int getLX200Format(); /* Select a site from the LX200 controller */ int selectSite(int fd, int siteNum); /* Select a catalog object */ int selectCatalogObject(int fd, int catalog, int NNNN); /* Select a sub catalog */ int selectSubCatalog(int fd, int catalog, int subCatalog); /* Set Debug */ void setLX200Debug(const char *deviceName, unsigned int debug_level); libindi/drivers/telescope/lx200pulsar2.h0000664000175000017500000000537013263645557017467 0ustar jasemjasem/* Pulsar 2 INDI driver Copyright (C) 2016, 2017 Jasem Mutlaq and Camiel Severijns 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 */ #pragma once #include "lx200generic.h" class LX200Pulsar2 : public LX200Generic { public: LX200Pulsar2(); virtual ~LX200Pulsar2() {} virtual const char *getDefaultName(); virtual bool Connect(); virtual bool Handshake(); virtual bool ReadScopeStatus(); virtual void ISGetProperties(const char *dev); virtual bool initProperties(); virtual bool updateProperties(); virtual bool ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n); virtual bool ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n); protected: virtual bool SetSlewRate(int index); virtual bool MoveNS(INDI_DIR_NS dir, TelescopeMotionCommand command); virtual bool MoveWE(INDI_DIR_WE dir, TelescopeMotionCommand command); virtual bool Abort(); virtual IPState GuideNorth(float ms); virtual IPState GuideSouth(float ms); virtual IPState GuideEast(float ms); virtual IPState GuideWest(float ms); virtual bool updateTime(ln_date *utc, double utc_offset); virtual bool updateLocation(double latitude, double longitude, double elevation); virtual bool Goto(double, double); virtual bool Park(); virtual bool Sync(double ra, double dec); virtual bool UnPark(); virtual bool isSlewComplete(); virtual bool checkConnection(); virtual void getBasicData(); // Periodic error correction on or off ISwitchVectorProperty PeriodicErrorCorrectionSP; ISwitch PeriodicErrorCorrectionS[2]; // Pole crossing on or off ISwitchVectorProperty PoleCrossingSP; ISwitch PoleCrossingS[2]; // Refraction correction on or off ISwitchVectorProperty RefractionCorrectionSP; ISwitch RefractionCorrectionS[2]; private: void sendScopeLocation(); void sendScopeTime(); void guideTimeout(); static void guideTimeoutHelper(void *p); bool isSlewing(); bool just_started_slewing; }; libindi/drivers/telescope/lx200autostar.h0000664000175000017500000000276513263645557017746 0ustar jasemjasem/* LX200 Autostar Copyright (C) 2003 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 */ #pragma once #include "lx200generic.h" class LX200Autostar : public LX200Generic { public: LX200Autostar(); ~LX200Autostar() {} const char *getDefaultName(); virtual void ISGetProperties(const char *dev); virtual bool ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n); virtual bool ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n); virtual void getBasicData(); protected: virtual bool initProperties(); virtual bool updateProperties(); ITextVectorProperty VersionTP; IText VersionT[5] {}; INumberVectorProperty FocusSpeedNP; INumber FocusSpeedN[1]; }; libindi/drivers/telescope/lx200zeq25.h0000664000175000017500000000565213263645557017050 0ustar jasemjasem/* ZEQ25 INDI driver Copyright (C) 2015 Jasem Mutlaq 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 */ #pragma once #include "lx200generic.h" class LX200ZEQ25 : public LX200Generic { public: LX200ZEQ25(); ~LX200ZEQ25() {} virtual bool updateProperties() override; virtual bool initProperties() override; virtual bool ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) override; virtual bool ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) override; protected: virtual const char *getDefaultName() override; virtual void getBasicData() override; virtual bool checkConnection() override; virtual bool isSlewComplete() override; virtual bool ReadScopeStatus() override; virtual bool SetSlewRate(int index) override; virtual bool SetTrackMode(uint8_t mode) override; virtual bool Goto(double, double) override; virtual bool updateTime(ln_date *utc, double utc_offset) override; virtual bool updateLocation(double latitude, double longitude, double elevation) override; virtual bool MoveNS(INDI_DIR_NS dir, TelescopeMotionCommand command) override; virtual bool MoveWE(INDI_DIR_WE dir, TelescopeMotionCommand command) override; // Parking virtual bool SetCurrentPark() override; virtual bool SetDefaultPark() override; virtual bool Park() override; virtual bool UnPark() override; virtual int SendPulseCmd(int direction, int duration_msec) override; private: int setZEQ25StandardProcedure(int fd, const char *data); int setZEQ25Latitude(double Lat); int setZEQ25Longitude(double Long); int setZEQ25UTCOffset(double hours); bool slewZEQ25(); int moveZEQ25To(int direction); int haltZEQ25Movement(); int getZEQ25MoveRate(); int setZEQ25Park(); int setZEQ25UnPark(); int setZEQ25TrackMode(int mode); int getZEQ25GuideRate(double *rate); int setZEQ25GuideRate(double rate); bool isZEQ25Home(); int gotoZEQ25Home(); bool isZEQ25Parked(); bool getMountInfo(); void mountSim(); ISwitch HomeS[1]; ISwitchVectorProperty HomeSP; /* Guide Rate */ INumber GuideRateN[1]; INumberVectorProperty GuideRateNP; }; libindi/drivers/telescope/ioptronv3.cpp0000664000175000017500000007347413263645557017621 0ustar jasemjasem/* INDI IOptron v3 Driver for firmware version 20171001 or later. Copyright (C) 2018 Jasem Mutlaq 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 */ #include "ioptronv3.h" #include "indicom.h" #include #include #include #include using namespace IOPv3; #define MOUNTINFO_TAB "Mount Info" #define PEC_TAB "PEC" // We declare an auto pointer to IOptronV3. std::unique_ptr scope(new IOptronV3()); void ISGetProperties(const char *dev) { scope->ISGetProperties(dev); } void ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) { scope->ISNewSwitch(dev, name, states, names, n); } void ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n) { scope->ISNewText(dev, name, texts, names, n); } void ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) { scope->ISNewNumber(dev, name, values, names, n); } void ISNewBLOB(const char *dev, const char *name, int sizes[], int blobsizes[], char *blobs[], char *formats[], char *names[], int n) { INDI_UNUSED(dev); INDI_UNUSED(name); INDI_UNUSED(sizes); INDI_UNUSED(blobsizes); INDI_UNUSED(blobs); INDI_UNUSED(formats); INDI_UNUSED(names); INDI_UNUSED(n); } void ISSnoopDevice(XMLEle *root) { scope->ISSnoopDevice(root); } /* Constructor */ IOptronV3::IOptronV3() { driver.reset(new Driver(getDeviceName())); scopeInfo.gpsStatus = GPS_OFF; scopeInfo.systemStatus = ST_STOPPED; scopeInfo.trackRate = TR_SIDEREAL; scopeInfo.slewRate = SR_1; scopeInfo.timeSource = TS_RS232; scopeInfo.hemisphere = HEMI_NORTH; DBG_SCOPE = INDI::Logger::getInstance().addDebugLevel("Scope Verbose", "SCOPE"); SetTelescopeCapability(TELESCOPE_CAN_PARK | TELESCOPE_CAN_SYNC | TELESCOPE_CAN_GOTO | TELESCOPE_CAN_ABORT | TELESCOPE_HAS_TIME | TELESCOPE_HAS_LOCATION | TELESCOPE_HAS_TRACK_MODE | TELESCOPE_CAN_CONTROL_TRACK | TELESCOPE_HAS_TRACK_RATE | TELESCOPE_HAS_PIER_SIDE, 9); } const char *IOptronV3::getDefaultName() { return (const char *)"iOptronV3"; } bool IOptronV3::initProperties() { INDI::Telescope::initProperties(); // According to iOptron Documentation strcpy(SlewRateS[0].label, "1x"); strcpy(SlewRateS[1].label, "2x"); strcpy(SlewRateS[2].label, "8x"); strcpy(SlewRateS[3].label, "16x"); strcpy(SlewRateS[4].label, "64x"); strcpy(SlewRateS[5].label, "128x"); strcpy(SlewRateS[6].label, "256x"); strcpy(SlewRateS[7].label, "512x"); strcpy(SlewRateS[8].label, "MAX"); /* Firmware */ IUFillText(&FirmwareT[FW_MODEL], "Model", "", 0); IUFillText(&FirmwareT[FW_BOARD], "Board", "", 0); IUFillText(&FirmwareT[FW_CONTROLLER], "Controller", "", 0); IUFillText(&FirmwareT[FW_RA], "RA", "", 0); IUFillText(&FirmwareT[FW_DEC], "DEC", "", 0); IUFillTextVector(&FirmwareTP, FirmwareT, 5, getDeviceName(), "Firmware Info", "", MOUNTINFO_TAB, IP_RO, 0, IPS_IDLE); /* Tracking Mode */ AddTrackMode("TRACK_SIDEREAL", "Sidereal", true); AddTrackMode("TRACK_SOLAR", "Solar"); AddTrackMode("TRACK_LUNAR", "Lunar"); AddTrackMode("TRACK_KING", "King"); AddTrackMode("TRACK_CUSTOM", "Custom"); /* GPS Status */ IUFillSwitch(&GPSStatusS[GPS_OFF], "Off", "", ISS_ON); IUFillSwitch(&GPSStatusS[GPS_ON], "On", "", ISS_OFF); IUFillSwitch(&GPSStatusS[GPS_DATA_OK], "Data OK", "", ISS_OFF); IUFillSwitchVector(&GPSStatusSP, GPSStatusS, 3, getDeviceName(), "GPS_STATUS", "GPS", MOUNTINFO_TAB, IP_RO, ISR_1OFMANY, 0, IPS_IDLE); /* Time Source */ IUFillSwitch(&TimeSourceS[TS_RS232], "RS232", "", ISS_ON); IUFillSwitch(&TimeSourceS[TS_CONTROLLER], "Controller", "", ISS_OFF); IUFillSwitch(&TimeSourceS[TS_GPS], "GPS", "", ISS_OFF); IUFillSwitchVector(&TimeSourceSP, TimeSourceS, 3, getDeviceName(), "TIME_SOURCE", "Time Source", MOUNTINFO_TAB, IP_RO, ISR_1OFMANY, 0, IPS_IDLE); /* Hemisphere */ IUFillSwitch(&HemisphereS[HEMI_SOUTH], "South", "", ISS_OFF); IUFillSwitch(&HemisphereS[HEMI_NORTH], "North", "", ISS_ON); IUFillSwitchVector(&HemisphereSP, HemisphereS, 2, getDeviceName(), "HEMISPHERE", "Hemisphere", MOUNTINFO_TAB, IP_RO, ISR_1OFMANY, 0, IPS_IDLE); /* Home */ IUFillSwitch(&HomeS[IOP_FIND_HOME], "FindHome", "Find Home", ISS_OFF); IUFillSwitch(&HomeS[IOP_SET_HOME], "SetCurrentAsHome", "Set current as Home", ISS_OFF); IUFillSwitch(&HomeS[IOP_GOTO_HOME], "GoToHome", "Go to Home", ISS_OFF); IUFillSwitchVector(&HomeSP, HomeS, 3, getDeviceName(), "HOME", "Home", MAIN_CONTROL_TAB, IP_RW, ISR_ATMOST1, 0, IPS_IDLE); /* How fast do we guide compared to sidereal rate */ IUFillNumber(&GuideRateN[0], "RA_GUIDE_RATE", "x Sidereal", "%g", 0.01, 0.9, 0.1, 0.5); IUFillNumber(&GuideRateN[1], "DE_GUIDE_RATE", "x Sidereal", "%g", 0.01, 0.99, 0.1, 0.5); IUFillNumberVector(&GuideRateNP, GuideRateN, 2, getDeviceName(), "GUIDE_RATE", "Guiding Rate", MOTION_TAB, IP_RW, 0, IPS_IDLE); /* Slew Mode. Normal vs Counter Weight up */ IUFillSwitch(&SlewModeS[IOP_CW_NORMAL], "Normal", "Normal", ISS_ON); IUFillSwitch(&SlewModeS[IOP_CW_UP], "Counterweight UP", "Counterweight up", ISS_OFF); IUFillSwitchVector(&SlewModeSP, SlewModeS, 2, getDeviceName(), "Slew Type", "Slew Type", MOTION_TAB, IP_RW, ISR_1OFMANY, 0, IPS_IDLE); /* Daylight Savings */ IUFillSwitch(&DaylightS[0], "ON", "ON", ISS_OFF); IUFillSwitch(&DaylightS[1], "OFF", "OFF", ISS_ON); IUFillSwitchVector(&DaylightSP, DaylightS, 2, getDeviceName(), "DaylightSaving", "Daylight Savings", SITE_TAB, IP_RW, ISR_1OFMANY, 0, IPS_IDLE); /* Counter Weight State */ IUFillSwitch(&CWStateS[IOP_CW_NORMAL], "Normal", "Normal", ISS_ON); IUFillSwitch(&CWStateS[IOP_CW_UP], "Up", "Up", ISS_OFF); IUFillSwitchVector(&CWStateSP, CWStateS, 2, getDeviceName(), "CWState", "Counter weights", MOTION_TAB, IP_RO, ISR_1OFMANY, 0, IPS_IDLE); // TODO // Add PEC Properties TrackState = SCOPE_IDLE; initGuiderProperties(getDeviceName(), MOTION_TAB); setDriverInterface(getDriverInterface() | GUIDER_INTERFACE); SetParkDataType(PARK_RA_DEC); addAuxControls(); double longitude=0, latitude=90; // Get value from config file if it exists. IUGetConfigNumber(getDeviceName(), "GEOGRAPHIC_COORD", "LONG", &longitude); currentRA = get_local_sidereal_time(longitude); IUGetConfigNumber(getDeviceName(), "GEOGRAPHIC_COORD", "LAT", &latitude); currentDEC = latitude > 0 ? 90 : -90; driver->setSimLongLat(longitude > 180 ? longitude - 360 : longitude , latitude); return true; } bool IOptronV3::updateProperties() { INDI::Telescope::updateProperties(); if (isConnected()) { defineSwitch(&HomeSP); defineNumber(&GuideNSNP); defineNumber(&GuideWENP); defineNumber(&GuideRateNP); defineText(&FirmwareTP); defineSwitch(&GPSStatusSP); defineSwitch(&TimeSourceSP); defineSwitch(&HemisphereSP); defineSwitch(&SlewModeSP); defineSwitch(&DaylightSP); defineSwitch(&CWStateSP); getStartupData(); } else { deleteProperty(HomeSP.name); deleteProperty(GuideNSNP.name); deleteProperty(GuideWENP.name); deleteProperty(GuideRateNP.name); deleteProperty(FirmwareTP.name); deleteProperty(GPSStatusSP.name); deleteProperty(TimeSourceSP.name); deleteProperty(HemisphereSP.name); deleteProperty(SlewModeSP.name); deleteProperty(DaylightSP.name); deleteProperty(CWStateSP.name); } return true; } void IOptronV3::getStartupData() { LOG_DEBUG("Getting firmware data..."); if (driver->getFirmwareInfo(&firmwareInfo)) { IUSaveText(&FirmwareT[0], firmwareInfo.Model.c_str()); IUSaveText(&FirmwareT[1], firmwareInfo.MainBoardFirmware.c_str()); IUSaveText(&FirmwareT[2], firmwareInfo.ControllerFirmware.c_str()); IUSaveText(&FirmwareT[3], firmwareInfo.RAFirmware.c_str()); IUSaveText(&FirmwareT[4], firmwareInfo.DEFirmware.c_str()); FirmwareTP.s = IPS_OK; IDSetText(&FirmwareTP, nullptr); } LOG_DEBUG("Getting guiding rate..."); double RARate=0, DERate=0; if (driver->getGuideRate(&RARate, &DERate)) { GuideRateN[RA_AXIS].value = RARate; GuideRateN[DEC_AXIS].value = DERate; IDSetNumber(&GuideRateNP, nullptr); } int utcOffsetMinutes=0; bool dayLightSavings=false; double JD = 0; if (driver->getUTCDateTime(&JD, &utcOffsetMinutes, &dayLightSavings)) { time_t utc_time; ln_get_timet_from_julian(JD, &utc_time); // UTC Time char ts[32]={0}; struct tm *utc; utc = gmtime(&utc_time); strftime(ts, sizeof(ts), "%Y-%m-%dT%H:%M:%S", utc); IUSaveText(&TimeT[0], ts); LOGF_INFO("Mount UTC: %s", ts); // UTC Offset char offset[8]={0}; snprintf(offset, 8, "%.2f", utcOffsetMinutes/60.0); IUSaveText(&TimeT[0], ts); LOGF_INFO("Mount UTC Offset: %s", offset); IDSetText(&TimeTP, nullptr); LOGF_INFO("Mount Daylight Savings: %s", dayLightSavings ? "ON" : "OFF"); DaylightS[0].s = dayLightSavings ? ISS_ON : ISS_OFF; DaylightS[1].s = !dayLightSavings ? ISS_ON : ISS_OFF; DaylightSP.s = IPS_OK; IDSetSwitch(&DaylightSP, nullptr); } // Get Longitude and Latitude from mount if (driver->getStatus(&scopeInfo)) { LocationN[LOCATION_LATITUDE].value = scopeInfo.latitude; // Convert to INDI standard longitude (0 to 360 Eastward) LocationN[LOCATION_LONGITUDE].value = (scopeInfo.longitude < 0) ? scopeInfo.longitude + 360 : scopeInfo.longitude; LocationNP.s = IPS_OK; IDSetNumber(&LocationNP, nullptr); char l[32]={0}, L[32]={0}; fs_sexa(l, LocationN[LOCATION_LATITUDE].value, 3, 3600); fs_sexa(L, LocationN[LOCATION_LONGITUDE].value, 4, 3600); LOGF_INFO("Mount Location: Lat %.32s - Long %.32s", l, L); } double DEC = (scopeInfo.latitude > 0) ? 90 : -90; if (InitPark()) { // If loading parking data is successful, we just set the default parking values. SetAxis1ParkDefault(currentRA); SetAxis2ParkDefault(DEC); } else { // Otherwise, we set all parking data to default in case no parking data is found. SetAxis1Park(currentRA); SetAxis2Park(DEC); SetAxis1ParkDefault(currentRA); SetAxis2ParkDefault(DEC); } if (isSimulation()) { if (isParked()) driver->setSimSytemStatus(ST_PARKED); else driver->setSimSytemStatus(ST_STOPPED); } } bool IOptronV3::ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) { if (dev != nullptr && strcmp(dev, getDeviceName()) == 0) { // Guiding Rate if (!strcmp(name, GuideRateNP.name)) { IUUpdateNumber(&GuideRateNP, values, names, n); if (driver->setGuideRate(GuideRateN[RA_AXIS].value, GuideRateN[DEC_AXIS].value)) GuideRateNP.s = IPS_OK; else GuideRateNP.s = IPS_ALERT; IDSetNumber(&GuideRateNP, nullptr); return true; } if (!strcmp(name, GuideNSNP.name) || !strcmp(name, GuideWENP.name)) { processGuiderProperties(name, values, names, n); return true; } } return INDI::Telescope::ISNewNumber(dev, name, values, names, n); } bool IOptronV3::ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) { if (!strcmp(getDeviceName(), dev)) { /******************************************************* * Home Operations *******************************************************/ if (!strcmp(name, HomeSP.name)) { IUUpdateSwitch(&HomeSP, states, names, n); IOP_HOME_OPERATION operation = (IOP_HOME_OPERATION)IUFindOnSwitchIndex(&HomeSP); IUResetSwitch(&HomeSP); switch (operation) { case IOP_FIND_HOME: if (firmwareInfo.Model.find("CEM") == std::string::npos) { HomeSP.s = IPS_IDLE; IDSetSwitch(&HomeSP, nullptr); LOG_WARN("Home search is not supported in this model."); return true; } if (driver->findHome() == false) { HomeSP.s = IPS_ALERT; IDSetSwitch(&HomeSP, nullptr); return false; } HomeSP.s = IPS_OK; IDSetSwitch(&HomeSP, nullptr); LOG_INFO("Searching for home position..."); return true; break; case IOP_SET_HOME: if (driver->setCurrentHome() == false) { HomeSP.s = IPS_ALERT; IDSetSwitch(&HomeSP, nullptr); return false; } HomeSP.s = IPS_OK; IDSetSwitch(&HomeSP, nullptr); LOG_INFO("Home position set to current coordinates."); return true; break; case IOP_GOTO_HOME: if (driver->gotoHome() == false) { HomeSP.s = IPS_ALERT; IDSetSwitch(&HomeSP, nullptr); return false; } HomeSP.s = IPS_OK; IDSetSwitch(&HomeSP, nullptr); LOG_INFO("Slewing to home position..."); return true; break; } return true; } /******************************************************* * Slew Mode Operations *******************************************************/ if (!strcmp(name, SlewModeSP.name)) { IUUpdateSwitch(&SlewModeSP, states, names, n); SlewModeSP.s = IPS_OK; IDSetSwitch(&SlewModeSP, nullptr); return true; } /******************************************************* * Daylight Savings Operations *******************************************************/ if (!strcmp(name, DaylightSP.name)) { IUUpdateSwitch(&DaylightSP, states, names, n); if (driver->setDaylightSaving(DaylightS[0].s == ISS_ON)) DaylightSP.s = IPS_OK; else DaylightSP.s = IPS_ALERT; IDSetSwitch(&DaylightSP, nullptr); return true; } } return INDI::Telescope::ISNewSwitch(dev, name, states, names, n); } bool IOptronV3::ReadScopeStatus() { bool rc = false; IOPInfo newInfo; if (isSimulation()) mountSim(); rc = driver->getStatus(&newInfo); if (rc) { if (IUFindOnSwitchIndex(&GPSStatusSP) != newInfo.gpsStatus) { IUResetSwitch(&GPSStatusSP); GPSStatusS[newInfo.gpsStatus].s = ISS_ON; IDSetSwitch(&GPSStatusSP, nullptr); } if (IUFindOnSwitchIndex(&TimeSourceSP) != newInfo.timeSource) { IUResetSwitch(&TimeSourceSP); TimeSourceS[newInfo.timeSource].s = ISS_ON; IDSetSwitch(&TimeSourceSP, nullptr); } if (IUFindOnSwitchIndex(&HemisphereSP) != newInfo.hemisphere) { IUResetSwitch(&HemisphereSP); HemisphereS[newInfo.hemisphere].s = ISS_ON; IDSetSwitch(&HemisphereSP, nullptr); } if (IUFindOnSwitchIndex(&SlewRateSP) != newInfo.slewRate-1) { IUResetSwitch(&SlewRateSP); SlewRateS[newInfo.slewRate-1].s = ISS_ON; IDSetSwitch(&SlewRateSP, nullptr); } /* TelescopeTrackMode trackMode = TRACK_SIDEREAL; switch (newInfo.trackRate) { case TR_SIDEREAL: trackMode = TRACK_SIDEREAL; break; case TR_SOLAR: trackMode = TRACK_SOLAR; break; case TR_LUNAR: trackMode = TRACK_LUNAR; break; case TR_KING: trackMode = TRACK_SIDEREAL; break; case TR_CUSTOM: trackMode = TRACK_CUSTOM; break; }*/ switch (newInfo.systemStatus) { case ST_STOPPED: TrackModeSP.s = IPS_IDLE; TrackState = SCOPE_IDLE; break; case ST_PARKED: TrackModeSP.s = IPS_IDLE; TrackState = SCOPE_PARKED; if (!isParked()) SetParked(true); break; case ST_HOME: TrackModeSP.s = IPS_IDLE; TrackState = SCOPE_IDLE; break; case ST_SLEWING: case ST_MERIDIAN_FLIPPING: if (TrackState != SCOPE_SLEWING && TrackState != SCOPE_PARKING) TrackState = SCOPE_SLEWING; break; case ST_TRACKING_PEC_OFF: case ST_TRACKING_PEC_ON: case ST_GUIDING: // If slew to parking position is complete, issue park command now. if (TrackState == SCOPE_PARKING) driver->park(); else { TrackModeSP.s = IPS_BUSY; TrackState = SCOPE_TRACKING; if (scopeInfo.systemStatus == ST_SLEWING) LOG_INFO("Slew complete, tracking..."); else if (scopeInfo.systemStatus == ST_MERIDIAN_FLIPPING) LOG_INFO("Meridian flip complete, tracking..."); } break; } if (IUFindOnSwitchIndex(&TrackModeSP) != newInfo.trackRate) { IUResetSwitch(&TrackModeSP); TrackModeS[newInfo.trackRate].s = ISS_ON; IDSetSwitch(&TrackModeSP, nullptr); } scopeInfo = newInfo; } IOP_PIER_STATE pierState = IOP_PIER_UNKNOWN; IOP_CW_STATE cwState = IOP_CW_NORMAL; rc = driver->getCoords(¤tRA, ¤tDEC, &pierState, &cwState); if (rc) { if (pierState == IOP_PIER_UNKNOWN) setPierSide(PIER_UNKNOWN); else setPierSide(pierState == IOP_PIER_EAST ? PIER_EAST : PIER_WEST); if (IUFindOnSwitchIndex(&CWStateSP) != cwState) { IUResetSwitch(&CWStateSP); CWStateS[cwState].s = ISS_ON; IDSetSwitch(&CWStateSP, nullptr); } NewRaDec(currentRA, currentDEC); } return rc; } bool IOptronV3::Goto(double ra, double de) { targetRA = ra; targetDEC = de; char RAStr[64]={0}, DecStr[64]={0}; fs_sexa(RAStr, targetRA, 2, 3600); fs_sexa(DecStr, targetDEC, 2, 3600); if (driver->setRA(ra) == false || driver->setDE(de) == false) { LOG_ERROR("Error setting RA/DEC."); return false; } bool rc = false; if (IUFindOnSwitchIndex(&SlewModeSP) == IOP_CW_NORMAL) rc = driver->slewNormal(); else rc = driver->slewCWUp(); if (rc == false) { LOG_ERROR("Failed to slew."); return false; } TrackState = SCOPE_SLEWING; LOGF_INFO("Slewing to RA: %s - DEC: %s", RAStr, DecStr); return true; } bool IOptronV3::Sync(double ra, double de) { if (driver->setRA(ra) == false || driver->setDE(de) == false) { LOG_ERROR("Error setting RA/DEC."); return false; } if (driver->sync() == false) { LOG_ERROR("Failed to sync."); } EqNP.s = IPS_OK; currentRA = ra; currentDEC = de; NewRaDec(currentRA, currentDEC); return true; } bool IOptronV3::Abort() { return driver->abort(); } bool IOptronV3::Park() { if (firmwareInfo.Model.find("CEM") == std::string::npos && firmwareInfo.Model.find("iEQ45 Pro") == std::string::npos && firmwareInfo.Model.find("iEQ35") == std::string::npos) { LOG_ERROR("Parking is not supported in this mount model."); return false; } targetRA = GetAxis1Park(); targetDEC = GetAxis2Park(); if (driver->setRA(targetRA) == false || driver->setDE(targetDEC) == false) { LOG_ERROR("Error setting RA/DEC."); return false; } bool rc = false; if (IUFindOnSwitchIndex(&SlewModeSP) == IOP_CW_NORMAL) rc = driver->slewNormal(); else rc = driver->slewCWUp(); if (rc == false) { LOG_ERROR("Failed to slew tp parking position."); return false; } char RAStr[64]={0}, DecStr[64]={0}; fs_sexa(RAStr, targetRA, 2, 3600); fs_sexa(DecStr, targetDEC, 2, 3600); TrackState = SCOPE_PARKING; LOGF_INFO("Telescope parking in progress to RA: %s DEC: %s", RAStr, DecStr); return true; } bool IOptronV3::UnPark() { if (firmwareInfo.Model.find("CEM") == std::string::npos && firmwareInfo.Model.find("iEQ45 Pro") == std::string::npos && firmwareInfo.Model.find("iEQ35") == std::string::npos) { LOG_ERROR("Unparking is not supported in this mount model."); return false; } if (driver->unpark()) { SetParked(false); TrackState = SCOPE_IDLE; return true; } else return false; } bool IOptronV3::Handshake() { driver->setSimulation(isSimulation()); if (driver->checkConnection(PortFD) == false) return false; return true; } bool IOptronV3::updateTime(ln_date *utc, double utc_offset) { bool rc1 = driver->setUTCDateTime(ln_get_julian_day(utc)); bool rc2 = driver->setUTCOffset(utc_offset*60); return (rc1 && rc2); } bool IOptronV3::updateLocation(double latitude, double longitude, double elevation) { INDI_UNUSED(elevation); if (longitude > 180) longitude -= 360; if (driver->setLongitude(longitude) == false) { LOG_ERROR("Failed to set longitude."); return false; } if (driver->setLatitude(latitude) == false) { LOG_ERROR("Failed to set longitude."); return false; } char l[32]={0}, L[32]={0}; fs_sexa(l, latitude, 3, 3600); fs_sexa(L, longitude, 4, 3600); LOGF_INFO("Site location updated to Lat %.32s - Long %.32s", l, L); return true; } void IOptronV3::debugTriggered(bool enable) { driver->setDebug(enable); } void IOptronV3::simulationTriggered(bool enable) { driver->setSimulation(enable); } bool IOptronV3::MoveNS(INDI_DIR_NS dir, TelescopeMotionCommand command) { if (TrackState == SCOPE_PARKED) { LOG_ERROR("Please unpark the mount before issuing any motion commands."); return false; } switch (command) { case MOTION_START: if (driver->startMotion(dir == DIRECTION_NORTH ? IOP_N : IOP_S) == false) { LOG_ERROR("Error setting N/S motion direction."); return false; } else LOGF_INFO("Moving toward %s.", (dir == DIRECTION_NORTH) ? "North" : "South"); break; case MOTION_STOP: if (driver->stopMotion(dir == DIRECTION_NORTH ? IOP_N : IOP_S) == false) { LOG_ERROR("Error stopping N/S motion."); return false; } else LOGF_INFO("%s motion stopped.", (dir == DIRECTION_NORTH) ? "North" : "South"); break; } return true; } bool IOptronV3::MoveWE(INDI_DIR_WE dir, TelescopeMotionCommand command) { if (TrackState == SCOPE_PARKED) { LOG_ERROR("Please unpark the mount before issuing any motion commands."); return false; } switch (command) { case MOTION_START: if (driver->startMotion(dir == DIRECTION_WEST ? IOP_W : IOP_E) == false) { LOG_ERROR("Error setting N/S motion direction."); return false; } else LOGF_INFO("Moving toward %s.", (dir == DIRECTION_WEST) ? "West" : "East"); break; case MOTION_STOP: if (driver->stopMotion(dir == DIRECTION_WEST ? IOP_W : IOP_E) == false) { LOG_ERROR("Error stopping W/E motion."); return false; } else LOGF_INFO("%s motion stopped.", (dir == DIRECTION_WEST) ? "West" : "East"); break; } return true; } IPState IOptronV3::GuideNorth(float ms) { bool rc = driver->startGuide(IOP_N, (uint32_t)ms); return (rc ? IPS_OK : IPS_ALERT); } IPState IOptronV3::GuideSouth(float ms) { bool rc = driver->startGuide(IOP_S, (uint32_t)ms); return (rc ? IPS_OK : IPS_ALERT); } IPState IOptronV3::GuideEast(float ms) { bool rc = driver->startGuide(IOP_E, (uint32_t)ms); return (rc ? IPS_OK : IPS_ALERT); } IPState IOptronV3::GuideWest(float ms) { bool rc = driver->startGuide(IOP_W, (uint32_t)ms); return (rc ? IPS_OK : IPS_ALERT); } bool IOptronV3::SetSlewRate(int index) { IOP_SLEW_RATE rate = (IOP_SLEW_RATE) (index+1); return driver->setSlewRate(rate); } bool IOptronV3::saveConfigItems(FILE *fp) { INDI::Telescope::saveConfigItems(fp); IUSaveConfigSwitch(fp, &SlewModeSP); IUSaveConfigSwitch(fp, &DaylightSP); return true; } void IOptronV3::mountSim() { static struct timeval ltv; struct timeval tv; double dt, da, dx; int nlocked; /* update elapsed time since last poll, don't presume exactly POLLMS */ gettimeofday(&tv, nullptr); if (ltv.tv_sec == 0 && ltv.tv_usec == 0) ltv = tv; dt = tv.tv_sec - ltv.tv_sec + (tv.tv_usec - ltv.tv_usec) / 1e6; ltv = tv; double currentSlewRate = Driver::IOP_SLEW_RATES[IUFindOnSwitchIndex(&SlewRateSP)] * TRACKRATE_SIDEREAL/3600.0; da = currentSlewRate * dt; /* Process per current state. We check the state of EQUATORIAL_COORDS and act acoordingly */ switch (TrackState) { case SCOPE_IDLE: currentRA += (TrackRateN[AXIS_RA].value/3600.0 * dt) / 15.0; currentRA = range24(currentRA); break; case SCOPE_TRACKING: if (TrackModeS[TR_CUSTOM].s == ISS_ON) { currentRA += ( ((TRACKRATE_SIDEREAL/3600.0) - (TrackRateN[AXIS_RA].value/3600.0)) * dt) / 15.0; currentDEC += ( (TrackRateN[AXIS_DE].value/3600.0) * dt); } break; case SCOPE_SLEWING: case SCOPE_PARKING: /* slewing - nail it when both within one pulse @ SLEWRATE */ nlocked = 0; dx = targetRA - currentRA; // Take shortest path if (fabs(dx) > 12) dx *= -1; if (fabs(dx) <= da) { currentRA = targetRA; nlocked++; } else if (dx > 0) currentRA += da / 15.; else currentRA -= da / 15.; if (currentRA < 0) currentRA += 24; else if (currentRA > 24) currentRA -= 24; dx = targetDEC - currentDEC; if (fabs(dx) <= da) { currentDEC = targetDEC; nlocked++; } else if (dx > 0) currentDEC += da; else currentDEC -= da; if (nlocked == 2) { if (TrackState == SCOPE_SLEWING) driver->setSimSytemStatus(ST_TRACKING_PEC_OFF); else driver->setSimSytemStatus(ST_PARKED); } break; default: break; } driver->setSimRA(currentRA); driver->setSimDE(currentDEC); } bool IOptronV3::SetCurrentPark() { SetAxis1Park(currentRA); SetAxis2Park(currentDEC); return true; } bool IOptronV3::SetDefaultPark() { // By default set RA to LST SetAxis1Park(get_local_sidereal_time(LocationN[LOCATION_LONGITUDE].value)); // Set DEC to 90 or -90 depending on the hemisphere SetAxis2Park((HemisphereS[HEMI_NORTH].s == ISS_ON) ? 90 : -90); return true; } bool IOptronV3::SetTrackMode(uint8_t mode) { IOP_TRACK_RATE rate = static_cast(mode); if (driver->setTrackMode(rate)) return true; return false; } bool IOptronV3::SetTrackRate(double raRate, double deRate) { INDI_UNUSED(deRate); // Convert to arcsecs/s to rate double ieqRARate = raRate / TRACKRATE_SIDEREAL; if (ieqRARate < 0.1 || ieqRARate > 1.9) { LOG_ERROR("Rate is outside permitted limits of 0.1 to 1.9 sidereal rate (1.504 to 28.578 arcsecs/s)"); return false; } if (driver->setCustomRATrackRate(ieqRARate)) return true; return false; } bool IOptronV3::SetTrackEnabled(bool enabled) { if (enabled) { // If we are engaging tracking, let us first set tracking mode, and if we have custom mode, then tracking rate. // NOTE: Is this the correct order? or should tracking be switched on first before making these changes? Need to test. SetTrackMode(IUFindOnSwitchIndex(&TrackModeSP)); if (TrackModeS[TR_CUSTOM].s == ISS_ON) SetTrackRate(TrackRateN[AXIS_RA].value, TrackRateN[AXIS_DE].value); } return driver->setTrackEnabled(enabled); } libindi/drivers/telescope/ioptron_drivers.txt0000664000175000017500000000000013263645557021112 0ustar jasemjasemlibindi/drivers/telescope/paramount.cpp0000664000175000017500000007514113263645557017655 0ustar jasemjasem/******************************************************************************* Copyright(c) 2017 Jasem Mutlaq. All rights reserved. Driver for using TheSky6 Pro Scripted operations for mounts via the TCP server. While this technically can operate any mount connected to the TheSky6 Pro, it is intended for Paramount mounts control. This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2 as published by the Free Software Foundation. . 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 Library General Public License for more details. . You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. *******************************************************************************/ #include "paramount.h" #include "indicom.h" #include #include #include #include #include #include // We declare an auto pointer to Paramount. std::unique_ptr paramount_mount(new Paramount()); #define GOTO_RATE 5 /* slew rate, degrees/s */ #define SLEW_RATE 0.5 /* slew rate, degrees/s */ #define FINE_SLEW_RATE 0.1 /* slew rate, degrees/s */ #define GOTO_LIMIT 5.5 /* Move at GOTO_RATE until distance from target is GOTO_LIMIT degrees */ #define SLEW_LIMIT 1 /* Move at SLEW_LIMIT until distance from target is SLEW_LIMIT degrees */ #define PARAMOUNT_TIMEOUT 3 /* Timeout in seconds */ #define PARAMOUNT_NORTH 0 #define PARAMOUNT_SOUTH 1 #define PARAMOUNT_EAST 2 #define PARAMOUNT_WEST 3 #define RA_AXIS 0 #define DEC_AXIS 1 #define STELLAR_DAY 86164.098903691 #define TRACKRATE_SIDEREAL ((360.0 * 3600.0) / STELLAR_DAY) #define SOLAR_DAY 86400 #define TRACKRATE_SOLAR ((360.0 * 3600.0) / SOLAR_DAY) #define TRACKRATE_LUNAR 14.511415 /* Preset Slew Speeds */ #define SLEWMODES 9 const double slewspeeds[SLEWMODES] = { 1.0, 2.0, 4.0, 8.0, 32.0, 64.0, 128.0, 256.0, 512.0 }; void ISPoll(void *p); void ISGetProperties(const char *dev) { paramount_mount->ISGetProperties(dev); } void ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) { paramount_mount->ISNewSwitch(dev, name, states, names, n); } void ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n) { paramount_mount->ISNewText(dev, name, texts, names, n); } void ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) { paramount_mount->ISNewNumber(dev, name, values, names, n); } void ISNewBLOB(const char *dev, const char *name, int sizes[], int blobsizes[], char *blobs[], char *formats[], char *names[], int n) { INDI_UNUSED(dev); INDI_UNUSED(name); INDI_UNUSED(sizes); INDI_UNUSED(blobsizes); INDI_UNUSED(blobs); INDI_UNUSED(formats); INDI_UNUSED(names); INDI_UNUSED(n); } void ISSnoopDevice(XMLEle *root) { paramount_mount->ISSnoopDevice(root); } Paramount::Paramount() { DBG_SCOPE = INDI::Logger::getInstance().addDebugLevel("Scope Verbose", "SCOPE"); SetTelescopeCapability(TELESCOPE_CAN_PARK | TELESCOPE_CAN_SYNC | TELESCOPE_CAN_GOTO | TELESCOPE_CAN_ABORT | TELESCOPE_HAS_TIME | TELESCOPE_HAS_LOCATION | TELESCOPE_HAS_TRACK_MODE | TELESCOPE_HAS_TRACK_RATE | TELESCOPE_CAN_CONTROL_TRACK, 9); setTelescopeConnection(CONNECTION_TCP); } const char *Paramount::getDefaultName() { return (const char *)"Paramount"; } bool Paramount::initProperties() { /* Make sure to init parent properties first */ INDI::Telescope::initProperties(); for (int i = 0; i < SlewRateSP.nsp-1; i++) { sprintf(SlewRateSP.sp[i].label, "%.fx", slewspeeds[i]); SlewRateSP.sp[i].aux = (void *)&slewspeeds[i]; } // Set 64x as default speed SlewRateSP.sp[5].s = ISS_ON; /* How fast do we guide compared to sidereal rate */ IUFillNumber(&JogRateN[RA_AXIS], "JOG_RATE_WE", "W/E Rate (arcmin)", "%g", 0, 600, 60, 30); IUFillNumber(&JogRateN[DEC_AXIS], "JOG_RATE_NS", "N/S Rate (arcmin)", "%g", 0, 600, 60, 30); IUFillNumberVector(&JogRateNP, JogRateN, 2, getDeviceName(), "JOG_RATE", "Jog Rate", MOTION_TAB, IP_RW, 0, IPS_IDLE); /* How fast do we guide compared to sidereal rate */ IUFillNumber(&GuideRateN[RA_AXIS], "GUIDE_RATE_WE", "W/E Rate", "%1.1f", 0.0, 1.0, 0.1, 0.5); IUFillNumber(&GuideRateN[DEC_AXIS], "GUIDE_RATE_NS", "N/S Rate", "%1.1f", 0.0, 1.0, 0.1, 0.5); IUFillNumberVector(&GuideRateNP, GuideRateN, 2, getDeviceName(), "GUIDE_RATE", "Guiding Rate", MOTION_TAB, IP_RW, 0, IPS_IDLE); // Tracking Mode #if 0 IUFillSwitch(&TrackModeS[TRACK_SIDEREAL], "TRACK_SIDEREAL", "Sidereal", ISS_OFF); IUFillSwitch(&TrackModeS[TRACK_SOLAR], "TRACK_SOLAR", "Solar", ISS_OFF); IUFillSwitch(&TrackModeS[TRACK_LUNAR], "TRACK_LUNAR", "Lunar", ISS_OFF); IUFillSwitch(&TrackModeS[TRACK_CUSTOM], "TRACK_CUSTOM", "Custom", ISS_OFF); IUFillSwitchVector(&TrackModeSP, TrackModeS, 4, getDeviceName(), "TELESCOPE_TRACK_MODE", "Track Mode", MAIN_CONTROL_TAB, IP_RW, ISR_ATMOST1, 0, IPS_IDLE); #endif AddTrackMode("TRACK_SIDEREAL", "Sidereal", true); AddTrackMode("TRACK_SOLAR", "Solar"); AddTrackMode("TRACK_LUNAR", "Lunar"); AddTrackMode("TRACK_CUSTOM", "Custom"); // Custom Tracking Rate #if 0 IUFillNumber(&TrackRateN[0], "TRACK_RATE_RA", "RA (arcsecs/s)", "%.6f", -16384.0, 16384.0, 0.000001, 15.041067); IUFillNumber(&TrackRateN[1], "TRACK_RATE_DE", "DE (arcsecs/s)", "%.6f", -16384.0, 16384.0, 0.000001, 0); IUFillNumberVector(&TrackRateNP, TrackRateN, 2, getDeviceName(), "TELESCOPE_TRACK_RATE", "Track Rates", MAIN_CONTROL_TAB, IP_RW, 60, IPS_IDLE); #endif // Let's simulate it to be an F/7.5 120mm telescope with 50m 175mm guide scope ScopeParametersN[0].value = 120; ScopeParametersN[1].value = 900; ScopeParametersN[2].value = 50; ScopeParametersN[3].value = 175; TrackState = SCOPE_IDLE; SetParkDataType(PARK_RA_DEC); initGuiderProperties(getDeviceName(), MOTION_TAB); setDriverInterface(getDriverInterface() | GUIDER_INTERFACE); addAuxControls(); double longitude=0, latitude=90; // Get value from config file if it exists. IUGetConfigNumber(getDeviceName(), "GEOGRAPHIC_COORD", "LONG", &longitude); currentRA = get_local_sidereal_time(longitude); IUGetConfigNumber(getDeviceName(), "GEOGRAPHIC_COORD", "LAT", &latitude); currentDEC = latitude > 0 ? 90 : -90; return true; } bool Paramount::updateProperties() { INDI::Telescope::updateProperties(); if (isConnected()) { if (isTheSkyTracking()) { IUResetSwitch(&TrackModeSP); TrackModeS[TRACK_SIDEREAL].s = ISS_ON; TrackState = SCOPE_TRACKING; } else { IUResetSwitch(&TrackModeSP); TrackState = SCOPE_IDLE; } //defineSwitch(&TrackModeSP); //defineNumber(&TrackRateNP); defineNumber(&JogRateNP); defineNumber(&GuideNSNP); defineNumber(&GuideWENP); defineNumber(&GuideRateNP); // Initial currentRA and currentDEC to LST and +90 or -90 if (InitPark()) { // If loading parking data is successful, we just set the default parking values. SetAxis1ParkDefault(currentRA); SetAxis2ParkDefault(currentDEC); } else { // Otherwise, we set all parking data to default in case no parking data is found. SetAxis1Park(currentRA); SetAxis2Park(currentDEC); SetAxis1ParkDefault(currentRA); SetAxis2ParkDefault(currentDEC); } SetParked(isTheSkyParked()); } else { //deleteProperty(TrackModeSP.name); //deleteProperty(TrackRateNP.name); deleteProperty(JogRateNP.name); deleteProperty(GuideNSNP.name); deleteProperty(GuideWENP.name); deleteProperty(GuideRateNP.name); } return true; } bool Paramount::Handshake() { if (isSimulation()) return true; int rc = 0, nbytes_written = 0, nbytes_read = 0; char pCMD[MAXRBUF]={0}, pRES[MAXRBUF]={0}; strncpy(pCMD, "/* Java Script */" "var Out;" "sky6RASCOMTele.ConnectAndDoNotUnpark();" "Out = sky6RASCOMTele.IsConnected;", MAXRBUF); LOGF_DEBUG("CMD: %s", pCMD); if ((rc = tty_write_string(PortFD, pCMD, &nbytes_written)) != TTY_OK) { LOG_ERROR("Error writing to TheSky6 TCP server."); return false; } // Should we read until we encounter string terminator? or what? if (static_cast(rc == tty_read_section(PortFD, pRES, '\0', PARAMOUNT_TIMEOUT, &nbytes_read)) != TTY_OK) { LOG_ERROR("Error reading from TheSky6 TCP server."); return false; } LOGF_DEBUG("RES: %s", pRES); int isTelescopeConnected = -1; std::regex rgx(R"((\d+)\|(.+)\. Error = (\d+)\.)"); std::smatch match; std::string input(pRES); if (std::regex_search(input, match, rgx)) isTelescopeConnected = atoi(match.str(1).c_str()); if (isTelescopeConnected <= 0) { LOGF_ERROR("Error connecting to telescope: %s (%d).", match.str(1).c_str(), atoi(match.str(2).c_str())); return false; } return true; } bool Paramount::getMountRADE() { int rc = 0, nbytes_written = 0, nbytes_read = 0, errorCode = 0; char pCMD[MAXRBUF]={0}, pRES[MAXRBUF]={0}; //"if (sky6RASCOMTele.IsConnected==0) sky6RASCOMTele.Connect();" strncpy(pCMD, "/* Java Script */" "var Out;" "sky6RASCOMTele.GetRaDec();" "Out = String(sky6RASCOMTele.dRa) + ',' + String(sky6RASCOMTele.dDec);", MAXRBUF); LOGF_DEBUG("CMD: %s", pCMD); if ((rc = tty_write_string(PortFD, pCMD, &nbytes_written)) != TTY_OK) { LOG_ERROR("Error writing to TheSky6 TCP server."); return false; } // Should we read until we encounter string terminator? or what? if (static_cast(rc == tty_read_section(PortFD, pRES, '\0', PARAMOUNT_TIMEOUT, &nbytes_read)) != TTY_OK) { LOG_ERROR("Error reading from TheSky6 TCP server."); return false; } LOGF_DEBUG("RES: %s", pRES); std::regex rgx(R"((.+),(.+)\|(.+)\. Error = (\d+)\.)"); std::smatch match; std::string input(pRES); bool coordsOK = false; if (std::regex_search(input, match, rgx)) { errorCode = atoi(match.str(4).c_str()); if (errorCode == 0) { currentRA = atof(match.str(1).c_str()); currentDEC = atof(match.str(2).c_str()); coordsOK = true; } } if (coordsOK) return true; LOGF_ERROR("Error reading coordinates %s (%d).", match.str(3).c_str(), errorCode); return false; } bool Paramount::ReadScopeStatus() { if (isSimulation()) { mountSim(); return true; } if (TrackState == SCOPE_SLEWING) { // Check if LX200 is done slewing if (isSlewComplete()) { TrackState = SCOPE_TRACKING; LOG_INFO("Slew is complete. Tracking..."); } } else if (TrackState == SCOPE_PARKING) { if (isSlewComplete()) { SetParked(true); //LOG_INFO("Mount is parked. Disconnecting..."); //Disconnect(); } //return true; } if (!getMountRADE()) return false; char RAStr[64], DecStr[64]; fs_sexa(RAStr, currentRA, 2, 3600); fs_sexa(DecStr, currentDEC, 2, 3600); DEBUGF(DBG_SCOPE, "Current RA: %s Current DEC: %s", RAStr, DecStr); NewRaDec(currentRA, currentDEC); return true; } bool Paramount::Goto(double r, double d) { targetRA = r; targetDEC = d; char RAStr[64], DecStr[64]; fs_sexa(RAStr, targetRA, 2, 3600); fs_sexa(DecStr, targetDEC, 2, 3600); ln_equ_posn lnradec { 0, 0 }; lnradec.ra = (currentRA * 360) / 24.0; lnradec.dec = currentDEC; ln_get_hrz_from_equ(&lnradec, &lnobserver, ln_get_julian_from_sys(), &lnaltaz); /* libnova measures azimuth from south towards west */ // double current_az = range360(lnaltaz.az + 180); //double current_alt =lnaltaz.alt; char pCMD[MAXRBUF]={0}; snprintf(pCMD, MAXRBUF, "sky6RASCOMTele.Asynchronous = true;" "sky6RASCOMTele.SlewToRaDec(%g, %g,'');", targetRA, targetDEC); if (!sendTheSkyOKCommand(pCMD, "Slewing to target")) return false; TrackState = SCOPE_SLEWING; EqNP.s = IPS_BUSY; LOGF_INFO("Slewing to RA: %s - DEC: %s", RAStr, DecStr); return true; } bool Paramount::isSlewComplete() { int rc = 0, nbytes_written = 0, nbytes_read = 0, errorCode = 0; char pCMD[MAXRBUF]={0}, pRES[MAXRBUF]={0}; strncpy(pCMD, "/* Java Script */" "var Out;" "Out = sky6RASCOMTele.IsSlewComplete;", MAXRBUF); LOGF_DEBUG("CMD: %s", pCMD); if ((rc = tty_write_string(PortFD, pCMD, &nbytes_written)) != TTY_OK) { LOG_ERROR("Error writing to TheSky6 TCP server."); return false; } // Should we read until we encounter string terminator? or what? if (static_cast(rc == tty_read_section(PortFD, pRES, '\0', PARAMOUNT_TIMEOUT, &nbytes_read)) != TTY_OK) { LOG_ERROR("Error reading from TheSky6 TCP server."); return false; } LOGF_DEBUG("RES: %s", pRES); std::regex rgx(R"((.+)|(.+)\. Error = (\d+)\.)"); std::smatch match; std::string input(pRES); if (std::regex_search(input, match, rgx)) { errorCode = atoi(match.str(3).c_str()); if (errorCode == 0) { int isComplete = atoi(match.str(1).c_str()); return (isComplete == 1); } } LOGF_ERROR("Error reading isSlewComplete %s (%d).", match.str(2).c_str(), errorCode); return false; } bool Paramount::isTheSkyParked() { int rc = 0, nbytes_written = 0, nbytes_read = 0; char pCMD[MAXRBUF]={0}, pRES[MAXRBUF]={0}; strncpy(pCMD, "/* Java Script */" "var Out;" "Out = sky6RASCOMTele.IsParked();", MAXRBUF); LOGF_DEBUG("CMD: %s", pCMD); if ((rc = tty_write_string(PortFD, pCMD, &nbytes_written)) != TTY_OK) { LOG_ERROR("Error writing to TheSky6 TCP server."); return false; } // Should we read until we encounter string terminator? or what? if (static_cast(rc == tty_read_section(PortFD, pRES, '\0', PARAMOUNT_TIMEOUT, &nbytes_read)) != TTY_OK) { LOG_ERROR("Error reading from TheSky6 TCP server."); return false; } LOGF_DEBUG("RES: %s", pRES); std::regex rgx(R"((.+)\|(.+)\. Error = (\d+)\.)"); std::smatch match; std::string input(pRES); if (std::regex_search(input, match, rgx)) { return strcmp("true", match.str(1).c_str()) == 0; } LOGF_ERROR("Error checking for park. Invalid response: %s", pRES); return false; } bool Paramount::isTheSkyTracking() { int rc = 0, nbytes_written = 0, nbytes_read = 0; char pCMD[MAXRBUF]={0}, pRES[MAXRBUF]={0}; strncpy(pCMD, "/* Java Script */" "var Out;" "Out = sky6RASCOMTele.IsTracking;", MAXRBUF); LOGF_DEBUG("CMD: %s", pCMD); if ((rc = tty_write_string(PortFD, pCMD, &nbytes_written)) != TTY_OK) { LOG_ERROR("Error writing to TheSky6 TCP server."); return false; } // Should we read until we encounter string terminator? or what? if (static_cast(rc == tty_read_section(PortFD, pRES, '\0', PARAMOUNT_TIMEOUT, &nbytes_read)) != TTY_OK) { LOG_ERROR("Error reading from TheSky6 TCP server."); return false; } LOGF_DEBUG("RES: %s", pRES); std::regex rgx(R"((.+)\|(.+)\. Error = (\d+)\.)"); std::smatch match; std::string input(pRES); if (std::regex_search(input, match, rgx)) { return strcmp("1", match.str(1).c_str()) == 0; } LOGF_ERROR("Error checking for tracking. Invalid response: %s", pRES); return false; } bool Paramount::Sync(double ra, double dec) { char pCMD[MAXRBUF]={0}; snprintf(pCMD, MAXRBUF, "sky6RASCOMTele.Sync(%g, %g,'');", targetRA, targetDEC); if (!sendTheSkyOKCommand(pCMD, "Syncing to target")) return false; currentRA = ra; currentDEC = dec; LOG_INFO("Sync is successful."); EqNP.s = IPS_OK; NewRaDec(currentRA, currentDEC); return true; } bool Paramount::Park() { targetRA = GetAxis1Park(); targetDEC = GetAxis2Park(); char pCMD[MAXRBUF]={0}; strncpy(pCMD, "sky6RASCOMTele.ParkAndDoNotDisconnect();", MAXRBUF); if (!sendTheSkyOKCommand(pCMD, "Parking mount")) return false; TrackState = SCOPE_PARKING; LOG_INFO("Parking telescope in progress..."); return true; } bool Paramount::UnPark() { char pCMD[MAXRBUF]={0}; strncpy(pCMD, "sky6RASCOMTele.Unpark();", MAXRBUF); if (!sendTheSkyOKCommand(pCMD, "Unparking mount")) return false; SetParked(false); return true; } bool Paramount::ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) { // first check if it's for our device if (dev != nullptr && strcmp(dev, getDeviceName()) == 0) { if (strcmp(name, "JOG_RATE") == 0) { IUUpdateNumber(&JogRateNP, values, names, n); JogRateNP.s = IPS_OK; IDSetNumber(&JogRateNP, nullptr); return true; } // Guiding Rate if (strcmp(name, GuideRateNP.name) == 0) { IUUpdateNumber(&GuideRateNP, values, names, n); GuideRateNP.s = IPS_OK; IDSetNumber(&GuideRateNP, nullptr); return true; } if (strcmp(name, GuideNSNP.name) == 0 || strcmp(name, GuideWENP.name) == 0) { processGuiderProperties(name, values, names, n); return true; } } // if we didn't process it, continue up the chain, let somebody else // give it a shot return INDI::Telescope::ISNewNumber(dev, name, values, names, n); } bool Paramount::ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) { if (dev != nullptr && strcmp(dev, getDeviceName()) == 0) { } // Nobody has claimed this, so, ignore it return INDI::Telescope::ISNewSwitch(dev, name, states, names, n); } bool Paramount::Abort() { char pCMD[MAXRBUF]={0}; strncpy(pCMD, "sky6RASCOMTele.Abort();", MAXRBUF); return sendTheSkyOKCommand(pCMD, "Abort mount slew"); } bool Paramount::MoveNS(INDI_DIR_NS dir, TelescopeMotionCommand command) { if (TrackState == SCOPE_PARKED) { LOG_ERROR("Please unpark the mount before issuing any motion commands."); return false; } int motion = (dir == DIRECTION_NORTH) ? PARAMOUNT_NORTH : PARAMOUNT_SOUTH; //int rate = IUFindOnSwitchIndex(&SlewRateSP); int rate = slewspeeds[IUFindOnSwitchIndex(&SlewRateSP)]; switch (command) { case MOTION_START: if (!isSimulation() && !startOpenLoopMotion(motion, rate)) { LOG_ERROR("Error setting N/S motion direction."); return false; } else LOGF_INFO("Moving toward %s.", (motion == PARAMOUNT_NORTH) ? "North" : "South"); break; case MOTION_STOP: if (!isSimulation() && !stopOpenLoopMotion()) { LOG_ERROR("Error stopping N/S motion."); return false; } else LOGF_INFO("Moving toward %s halted.", (motion == PARAMOUNT_NORTH) ? "North" : "South"); break; } return true; } bool Paramount::MoveWE(INDI_DIR_WE dir, TelescopeMotionCommand command) { if (TrackState == SCOPE_PARKED) { LOG_ERROR("Please unpark the mount before issuing any motion commands."); return false; } int motion = (dir == DIRECTION_WEST) ? PARAMOUNT_WEST : PARAMOUNT_EAST; int rate = IUFindOnSwitchIndex(&SlewRateSP); switch (command) { case MOTION_START: if (!isSimulation() && !startOpenLoopMotion(motion, rate)) { LOG_ERROR("Error setting W/E motion direction."); return false; } else LOGF_INFO("Moving toward %s.", (motion == PARAMOUNT_WEST) ? "West" : "East"); break; case MOTION_STOP: if (!isSimulation() && !stopOpenLoopMotion()) { LOG_ERROR("Error stopping W/E motion."); return false; } else LOGF_INFO("Movement toward %s halted.", (motion == PARAMOUNT_WEST) ? "West" : "East"); break; } return true; } bool Paramount::startOpenLoopMotion(uint8_t motion, uint16_t rate) { char pCMD[MAXRBUF]={0}; snprintf(pCMD, MAXRBUF, "sky6RASCOMTele.DoCommand(9,'%d|%d');", motion, rate); return sendTheSkyOKCommand(pCMD, "Starting open loop motion"); } bool Paramount::stopOpenLoopMotion() { char pCMD[MAXRBUF]={0}; strncpy(pCMD, "sky6RASCOMTele.DoCommand(10,'');", MAXRBUF); return sendTheSkyOKCommand(pCMD, "Stopping open loop motion"); } bool Paramount::updateLocation(double latitude, double longitude, double elevation) { INDI_UNUSED(elevation); // JM: INDI Longitude is 0 to 360 increasing EAST. libnova East is Positive, West is negative lnobserver.lng = longitude; if (lnobserver.lng > 180) lnobserver.lng -= 360; lnobserver.lat = latitude; LOGF_INFO("Location updated: Longitude (%g) Latitude (%g)", lnobserver.lng, lnobserver.lat); return true; } bool Paramount::updateTime(ln_date *utc, double utc_offset) { INDI_UNUSED(utc); INDI_UNUSED(utc_offset); return true; } bool Paramount::SetCurrentPark() { char pCMD[MAXRBUF]={0}; strncpy(pCMD, "sky6RASCOMTele.SetParkPosition();", MAXRBUF); if (!sendTheSkyOKCommand(pCMD, "Setting Park Position")) return false; SetAxis1Park(currentRA); SetAxis2Park(currentDEC); return true; } bool Paramount::SetDefaultPark() { // By default set RA to HA SetAxis1Park(get_local_sidereal_time(LocationN[LOCATION_LONGITUDE].value)); // Set DEC to 90 or -90 depending on the hemisphere SetAxis2Park((LocationN[LOCATION_LATITUDE].value > 0) ? 90 : -90); return true; } bool Paramount::SetParkPosition(double Axis1Value, double Axis2Value) { INDI_UNUSED(Axis1Value); INDI_UNUSED(Axis2Value); LOG_ERROR("Setting custom parking position directly is not supported. Slew to the desired " "parking position and click Current."); return false; } void Paramount::mountSim() { static struct timeval ltv { 0, 0 }; struct timeval tv { 0, 0 }; double dt, dx, da_ra = 0, da_dec = 0; int nlocked; /* update elapsed time since last poll, don't presume exactly POLLMS */ gettimeofday(&tv, nullptr); if (ltv.tv_sec == 0 && ltv.tv_usec == 0) ltv = tv; dt = tv.tv_sec - ltv.tv_sec + (tv.tv_usec - ltv.tv_usec) / 1e6; ltv = tv; if (fabs(targetRA - currentRA) * 15. >= GOTO_LIMIT) da_ra = GOTO_RATE * dt; else if (fabs(targetRA - currentRA) * 15. >= SLEW_LIMIT) da_ra = SLEW_RATE * dt; else da_ra = FINE_SLEW_RATE * dt; if (fabs(targetDEC - currentDEC) >= GOTO_LIMIT) da_dec = GOTO_RATE * dt; else if (fabs(targetDEC - currentDEC) >= SLEW_LIMIT) da_dec = SLEW_RATE * dt; else da_dec = FINE_SLEW_RATE * dt; double motionRate = 0; if (MovementNSSP.s == IPS_BUSY) motionRate = JogRateN[0].value; else if (MovementWESP.s == IPS_BUSY) motionRate = JogRateN[1].value; if (motionRate != 0) { da_ra = motionRate * dt * 0.05; da_dec = motionRate * dt * 0.05; switch (MovementNSSP.s) { case IPS_BUSY: if (MovementNSS[DIRECTION_NORTH].s == ISS_ON) currentDEC += da_dec; else if (MovementNSS[DIRECTION_SOUTH].s == ISS_ON) currentDEC -= da_dec; break; default: break; } switch (MovementWESP.s) { case IPS_BUSY: if (MovementWES[DIRECTION_WEST].s == ISS_ON) currentRA += da_ra / 15.; else if (MovementWES[DIRECTION_EAST].s == ISS_ON) currentRA -= da_ra / 15.; break; default: break; } NewRaDec(currentRA, currentDEC); return; } /* Process per current state. We check the state of EQUATORIAL_COORDS and act acoordingly */ switch (TrackState) { case SCOPE_IDLE: /* RA moves at sidereal, Dec stands still */ currentRA += (TRACKRATE_SIDEREAL/3600.0 * dt / 15.); break; case SCOPE_SLEWING: case SCOPE_PARKING: /* slewing - nail it when both within one pulse @ SLEWRATE */ nlocked = 0; dx = targetRA - currentRA; // Take shortest path if (fabs(dx) > 12) dx *= -1; if (fabs(dx) <= da_ra) { currentRA = targetRA; nlocked++; } else if (dx > 0) currentRA += da_ra / 15.; else currentRA -= da_ra / 15.; if (currentRA < 0) currentRA += 24; else if (currentRA > 24) currentRA -= 24; dx = targetDEC - currentDEC; if (fabs(dx) <= da_dec) { currentDEC = targetDEC; nlocked++; } else if (dx > 0) currentDEC += da_dec; else currentDEC -= da_dec; if (nlocked == 2) { if (TrackState == SCOPE_SLEWING) TrackState = SCOPE_TRACKING; else SetParked(true); } break; default: break; } NewRaDec(currentRA, currentDEC); } bool Paramount::sendTheSkyOKCommand(const char *command, const char *errorMessage) { int rc = 0, nbytes_written = 0, nbytes_read = 0; char pCMD[MAXRBUF]={0}, pRES[MAXRBUF]={0}; snprintf(pCMD, MAXRBUF, "/* Java Script */" "var Out;" "try {" "%s" "Out = 'OK'; }" "catch (err) {Out = err; }", command); LOGF_DEBUG("CMD: %s", pCMD); if ((rc = tty_write_string(PortFD, pCMD, &nbytes_written)) != TTY_OK) { LOG_ERROR("Error writing to TheSky6 TCP server."); return false; } if (static_cast(rc == tty_read_section(PortFD, pRES, '\0', PARAMOUNT_TIMEOUT, &nbytes_read)) != TTY_OK) { LOG_ERROR("Error reading from TheSky6 TCP server."); return false; } LOGF_DEBUG("RES: %s", pRES); std::regex rgx(R"((.+)\|(.+)\. Error = (\d+)\.)"); std::smatch match; std::string input(pRES); if (std::regex_search(input, match, rgx)) { // If NOT OK, then fail if (strcmp("OK", match.str(1).c_str()) != 0) { LOGF_ERROR("Error %s %s", errorMessage, match.str(1).c_str()); return false; } } else { LOGF_ERROR("Error %s. Invalid response: %s", errorMessage, pRES); return false; } return true; } IPState Paramount::GuideNorth(float ms) { // Movement in arcseconds double dDec = GuideRateN[DEC_AXIS].value * TRACKRATE_SIDEREAL * ms / 1000.0; char pCMD[MAXRBUF]={0}; snprintf(pCMD, MAXRBUF, "sky6DirectGuide.MoveTelescope(%g, %g);", 0., dDec); if (!sendTheSkyOKCommand(pCMD, "Guiding north")) return IPS_ALERT; return IPS_OK; } IPState Paramount::GuideSouth(float ms) { // Movement in arcseconds double dDec = GuideRateN[DEC_AXIS].value * TRACKRATE_SIDEREAL * ms / -1000.0; char pCMD[MAXRBUF]={0}; snprintf(pCMD, MAXRBUF, "sky6DirectGuide.MoveTelescope(%g, %g);", 0., dDec); if (!sendTheSkyOKCommand(pCMD, "Guiding south")) return IPS_ALERT; return IPS_OK; } IPState Paramount::GuideEast(float ms) { // Movement in arcseconds double dRA = GuideRateN[RA_AXIS].value * TRACKRATE_SIDEREAL * ms / 1000.0; char pCMD[MAXRBUF]={0}; snprintf(pCMD, MAXRBUF, "sky6DirectGuide.MoveTelescope(%g, %g);", dRA, 0.); if (!sendTheSkyOKCommand(pCMD, "Guiding east")) return IPS_ALERT; return IPS_OK; } IPState Paramount::GuideWest(float ms) { // Movement in arcseconds double dRA = GuideRateN[RA_AXIS].value * TRACKRATE_SIDEREAL * ms / -1000.0; char pCMD[MAXRBUF]={0}; snprintf(pCMD, MAXRBUF, "sky6DirectGuide.MoveTelescope(%g, %g);", dRA, 0.); if (!sendTheSkyOKCommand(pCMD, "Guiding west")) return IPS_ALERT; return IPS_OK; } bool Paramount::setTheSkyTracking(bool enable, bool isSidereal, double raRate, double deRate) { int on = enable ? 1 : 0; int ignore = isSidereal ? 1 : 0; char pCMD[MAXRBUF]={0}; snprintf(pCMD, MAXRBUF, "sky6RASCOMTele.SetTracking(%d, %d, %g, %g);", on, ignore, raRate, deRate); return sendTheSkyOKCommand(pCMD, "Setting tracking rate"); } bool Paramount::SetTrackRate(double raRate, double deRate) { return setTheSkyTracking(true, false, raRate, deRate); } bool Paramount::SetTrackMode(uint8_t mode) { bool isSidereal = (mode == TRACK_SIDEREAL); double dRA = TRACKRATE_SIDEREAL, dDE = 0; if (mode == TRACK_SOLAR) dRA = TRACKRATE_SOLAR; else if (mode == TRACK_LUNAR) dRA = TRACKRATE_LUNAR; else if (mode == TRACK_CUSTOM) { dRA = TrackRateN[RA_AXIS].value; dDE = TrackRateN[DEC_AXIS].value; } return setTheSkyTracking(true, isSidereal, dRA, dDE); } bool Paramount::SetTrackEnabled(bool enabled) { // On engaging track, we simply set the current track mode and it will take care of the rest including custom track rates. if (enabled) return SetTrackMode(IUFindOnSwitchIndex(&TrackModeSP)); else // Otherwise, simply switch everything off return setTheSkyTracking(false, false, 0, 0); } libindi/drivers/telescope/skywatcherAltAzSimple.cpp0000664000175000017500000017256713263645557022153 0ustar jasemjasem/*! * \file SkywatcherAltAzSimple.cpp * * \author Roger James * \author Gerry Rozema * \author Jean-Luc Geehalel * \date 13th November 2013 * * This file contains the implementation in C++ of a INDI telescope driver using the Skywatcher API. * It is based on work from three sources. * A C++ implementation of the API by Roger James. * The indi_eqmod driver by Jean-Luc Geehalel. * The synscanmount driver by Gerry Rozema. */ #include "skywatcherAltAzSimple.h" #include "indicom.h" #include "connectionplugins/connectionserial.h" #include // libnova specifies round() on old systems and it collides with the new gcc 5.x/6.x headers #define HAVE_ROUND #include #include #include #include #include #include // We declare an auto pointer to SkywatcherAltAzSimple. std::unique_ptr SkywatcherAltAzSimplePtr(new SkywatcherAltAzSimple()); /* Preset Slew Speeds */ #define SLEWMODES 9 double SlewSpeeds[SLEWMODES] = { 1.0, 2.0, 4.0, 8.0, 16.0, 32.0, 64.0, 128.0, 600.0 }; void ISPoll(void *p); namespace { bool FileExists(const std::string &name) { std::ifstream File(name.c_str()); return File.good(); } std::string GetLogTimestamp() { time_t Now = time(NULL); struct tm TimeStruct; char Buffer[60]; std::string FinalStr; TimeStruct = *localtime(&Now); strftime(Buffer, sizeof(Buffer), "%Y%m%d %H:%M:%S", &TimeStruct); FinalStr = Buffer; // Add the millisecond part std::chrono::system_clock::time_point NowClock = std::chrono::system_clock::now(); std::chrono::system_clock::duration TimePassed = NowClock.time_since_epoch(); TimePassed -= std::chrono::duration_cast(TimePassed); FinalStr += "."+std::to_string(static_cast(TimePassed / std::chrono::milliseconds(1))); return FinalStr; } } // namespace void ISGetProperties(const char *dev) { SkywatcherAltAzSimplePtr->ISGetProperties(dev); } void ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) { SkywatcherAltAzSimplePtr->ISNewSwitch(dev, name, states, names, n); } void ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n) { SkywatcherAltAzSimplePtr->ISNewText(dev, name, texts, names, n); } void ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) { SkywatcherAltAzSimplePtr->ISNewNumber(dev, name, values, names, n); } void ISNewBLOB(const char *dev, const char *name, int sizes[], int blobsizes[], char *blobs[], char *formats[], char *names[], int n) { SkywatcherAltAzSimplePtr->ISNewBLOB(dev, name, sizes, blobsizes, blobs, formats, names, n); } void ISSnoopDevice(XMLEle *root) { INDI_UNUSED(root); } SkywatcherAltAzSimple::SkywatcherAltAzSimple() : TrackLogFileName(GetHomeDirectory()+"/.indi/sw_mount_track_log.txt") { // Set up the logging pointer in SkyWatcherAPI pChildTelescope = this; SetTelescopeCapability(TELESCOPE_CAN_PARK | TELESCOPE_CAN_SYNC | TELESCOPE_CAN_GOTO | TELESCOPE_CAN_ABORT | TELESCOPE_HAS_TIME | TELESCOPE_HAS_LOCATION, SLEWMODES); std::remove(TrackLogFileName.c_str()); } bool SkywatcherAltAzSimple::Abort() { DEBUG(DBG_SCOPE, "SkywatcherAltAzSimple::Abort"); LogMessage("MOVE ABORT"); SlowStop(AXIS1); SlowStop(AXIS2); TrackState = SCOPE_IDLE; if (GuideNSNP.s == IPS_BUSY || GuideWENP.s == IPS_BUSY) { GuideNSNP.s = GuideWENP.s = IPS_IDLE; GuideNSN[0].value = GuideNSN[1].value = 0.0; GuideWEN[0].value = GuideWEN[1].value = 0.0; IDMessage(getDeviceName(), "Guide aborted."); IDSetNumber(&GuideNSNP, nullptr); IDSetNumber(&GuideWENP, nullptr); return true; } return true; } bool SkywatcherAltAzSimple::Handshake() { DEBUG(DBG_SCOPE, "SkywatcherAltAzSimple::Handshake"); SetSerialPort(PortFD); bool Result = InitMount(RecoverAfterReconnection); if (getActiveConnection() == serialConnection) { SerialPortName = serialConnection->port(); } else { SerialPortName = ""; } RecoverAfterReconnection = false; DEBUGF(DBG_SCOPE, "SkywatcherAltAzSimple::Handshake - Result: %d", Result); return Result; } const char *SkywatcherAltAzSimple::getDefaultName() { //DEBUG(DBG_SCOPE, "SkywatcherAltAzSimple::getDefaultName\n"); return "Skywatcher Alt-Az Wedge"; } bool SkywatcherAltAzSimple::Goto(double ra, double dec) { DEBUG(DBG_SCOPE, "SkywatcherAltAzSimple::Goto"); if (TrackState != SCOPE_IDLE) Abort(); DEBUGF(DBG_SCOPE, "RA %lf DEC %lf", ra, dec); if (IUFindSwitch(&CoordSP, "TRACK")->s == ISS_ON || IUFindSwitch(&CoordSP, "SLEW")->s == ISS_ON) { char RAStr[32], DecStr[32]; fs_sexa(RAStr, ra, 2, 3600); fs_sexa(DecStr, dec, 2, 3600); CurrentTrackingTarget.ra = ra; CurrentTrackingTarget.dec = dec; DEBUGF(INDI::Logger::DBG_SESSION, "New Tracking target RA %s DEC %s", RAStr, DecStr); } ln_hrz_posn AltAz { 0, 0 }; AltAz = GetAltAzPosition(ra, dec); DEBUGF(DBG_SCOPE, "New Altitude %lf degrees %ld microsteps Azimuth %lf degrees %ld microsteps", AltAz.alt, DegreesToMicrosteps(AXIS2, AltAz.alt), AltAz.az, DegreesToMicrosteps(AXIS1, AltAz.az)); LogMessage("NEW GOTO TARGET: Ra %lf Dec %lf - Alt %lf Az %lf - microsteps %ld %ld", ra, dec, AltAz.alt, AltAz.az, DegreesToMicrosteps(AXIS2, AltAz.alt), DegreesToMicrosteps(AXIS1, AltAz.az)); // Update the current encoder positions GetEncoder(AXIS1); GetEncoder(AXIS2); long AltitudeOffsetMicrosteps = DegreesToMicrosteps(AXIS2, AltAz.alt) + ZeroPositionEncoders[AXIS2] - CurrentEncoders[AXIS2]; long AzimuthOffsetMicrosteps = DegreesToMicrosteps(AXIS1, AltAz.az) + ZeroPositionEncoders[AXIS1] - CurrentEncoders[AXIS1]; DEBUGF(DBG_SCOPE, "Initial deltas Altitude %ld microsteps Azimuth %ld microsteps", AltitudeOffsetMicrosteps, AzimuthOffsetMicrosteps); if (AltitudeOffsetMicrosteps > MicrostepsPerRevolution[AXIS2] / 2) { // Going the long way round - send it the other way AltitudeOffsetMicrosteps -= MicrostepsPerRevolution[AXIS2]; } if (AzimuthOffsetMicrosteps > MicrostepsPerRevolution[AXIS1] / 2) { // Going the long way round - send it the other way AzimuthOffsetMicrosteps -= MicrostepsPerRevolution[AXIS1]; } if (AltitudeOffsetMicrosteps < -MicrostepsPerRevolution[AXIS2] / 2) { // Going the long way round - send it the other way AltitudeOffsetMicrosteps += MicrostepsPerRevolution[AXIS2]; } if (AzimuthOffsetMicrosteps < -MicrostepsPerRevolution[AXIS1] / 2) { // Going the long way round - send it the other way AzimuthOffsetMicrosteps += MicrostepsPerRevolution[AXIS1]; } DEBUGF(DBG_SCOPE, "Initial Axis2 %ld microsteps Axis1 %ld microsteps", ZeroPositionEncoders[AXIS2], ZeroPositionEncoders[AXIS1]); DEBUGF(DBG_SCOPE, "Current Axis2 %ld microsteps Axis1 %ld microsteps", CurrentEncoders[AXIS2], CurrentEncoders[AXIS1]); DEBUGF(DBG_SCOPE, "Altitude offset %ld microsteps Azimuth offset %ld microsteps", AltitudeOffsetMicrosteps, AzimuthOffsetMicrosteps); if (IUFindSwitch(&SlewModesSP, "SLEW_NORMAL")->s == ISS_ON) { SilentSlewMode = false; } else { SilentSlewMode = true; } SlewTo(AXIS1, AzimuthOffsetMicrosteps); SlewTo(AXIS2, AltitudeOffsetMicrosteps); TrackState = SCOPE_SLEWING; EqNP.s = IPS_BUSY; return true; } bool SkywatcherAltAzSimple::initProperties() { IDLog("SkywatcherAltAzSimple::initProperties\n"); // Allow the base class to initialise its visible before connection properties INDI::Telescope::initProperties(); for (int i = 0; i < SlewRateSP.nsp; ++i) { sprintf(SlewRateSP.sp[i].label, "%.fx", SlewSpeeds[i]); SlewRateSP.sp[i].aux = (void *)&SlewSpeeds[i]; } strncpy(SlewRateSP.sp[SlewRateSP.nsp - 1].name, "SLEW_MAX", MAXINDINAME); // Add default properties addDebugControl(); addConfigurationControl(); // Set up property variables IUFillText(&BasicMountInfo[MOTOR_CONTROL_FIRMWARE_VERSION], "MOTOR_CONTROL_FIRMWARE_VERSION", "Motor control firmware version", "-"); IUFillText(&BasicMountInfo[MOUNT_CODE], "MOUNT_CODE", "Mount code", "-"); IUFillText(&BasicMountInfo[MOUNT_NAME], "MOUNT_NAME", "Mount name", "-"); IUFillText(&BasicMountInfo[IS_DC_MOTOR], "IS_DC_MOTOR", "Is DC motor", "-"); IUFillTextVector(&BasicMountInfoV, BasicMountInfo, 4, getDeviceName(), "BASIC_MOUNT_INFO", "Basic mount information", DetailedMountInfoPage, IP_RO, 60, IPS_IDLE); IUFillNumber(&AxisOneInfo[MICROSTEPS_PER_REVOLUTION], "MICROSTEPS_PER_REVOLUTION", "Microsteps per revolution", "%.0f", 0, 0xFFFFFF, 1, 0); IUFillNumber(&AxisOneInfo[STEPPER_CLOCK_FREQUENCY], "STEPPER_CLOCK_FREQUENCY", "Stepper clock frequency", "%.0f", 0, 0xFFFFFF, 1, 0); IUFillNumber(&AxisOneInfo[HIGH_SPEED_RATIO], "HIGH_SPEED_RATIO", "High speed ratio", "%.0f", 0, 0xFFFFFF, 1, 0); IUFillNumber(&AxisOneInfo[MICROSTEPS_PER_WORM_REVOLUTION], "MICROSTEPS_PER_WORM_REVOLUTION", "Microsteps per worm revolution", "%.0f", 0, 0xFFFFFF, 1, 0); IUFillNumberVector(&AxisOneInfoV, AxisOneInfo, 4, getDeviceName(), "AXIS_ONE_INFO", "Axis one information", DetailedMountInfoPage, IP_RO, 60, IPS_IDLE); IUFillSwitch(&AxisOneState[FULL_STOP], "FULL_STOP", "FULL_STOP", ISS_OFF); IUFillSwitch(&AxisOneState[SLEWING], "SLEWING", "SLEWING", ISS_OFF); IUFillSwitch(&AxisOneState[SLEWING_TO], "SLEWING_TO", "SLEWING_TO", ISS_OFF); IUFillSwitch(&AxisOneState[SLEWING_FORWARD], "SLEWING_FORWARD", "SLEWING_FORWARD", ISS_OFF); IUFillSwitch(&AxisOneState[HIGH_SPEED], "HIGH_SPEED", "HIGH_SPEED", ISS_OFF); IUFillSwitch(&AxisOneState[NOT_INITIALISED], "NOT_INITIALISED", "NOT_INITIALISED", ISS_ON); IUFillSwitchVector(&AxisOneStateV, AxisOneState, 6, getDeviceName(), "AXIS_ONE_STATE", "Axis one state", DetailedMountInfoPage, IP_RO, ISR_NOFMANY, 60, IPS_IDLE); IUFillNumber(&AxisTwoInfo[MICROSTEPS_PER_REVOLUTION], "MICROSTEPS_PER_REVOLUTION", "Microsteps per revolution", "%.0f", 0, 0xFFFFFF, 1, 0); IUFillNumber(&AxisTwoInfo[STEPPER_CLOCK_FREQUENCY], "STEPPER_CLOCK_FREQUENCY", "Step timer frequency", "%.0f", 0, 0xFFFFFF, 1, 0); IUFillNumber(&AxisTwoInfo[HIGH_SPEED_RATIO], "HIGH_SPEED_RATIO", "High speed ratio", "%.0f", 0, 0xFFFFFF, 1, 0); IUFillNumber(&AxisTwoInfo[MICROSTEPS_PER_WORM_REVOLUTION], "MICROSTEPS_PER_WORM_REVOLUTION", "Mictosteps per worm revolution", "%.0f", 0, 0xFFFFFF, 1, 0); IUFillNumberVector(&AxisTwoInfoV, AxisTwoInfo, 4, getDeviceName(), "AXIS_TWO_INFO", "Axis two information", DetailedMountInfoPage, IP_RO, 60, IPS_IDLE); IUFillSwitch(&AxisTwoState[FULL_STOP], "FULL_STOP", "FULL_STOP", ISS_OFF); IUFillSwitch(&AxisTwoState[SLEWING], "SLEWING", "SLEWING", ISS_OFF); IUFillSwitch(&AxisTwoState[SLEWING_TO], "SLEWING_TO", "SLEWING_TO", ISS_OFF); IUFillSwitch(&AxisTwoState[SLEWING_FORWARD], "SLEWING_FORWARD", "SLEWING_FORWARD", ISS_OFF); IUFillSwitch(&AxisTwoState[HIGH_SPEED], "HIGH_SPEED", "HIGH_SPEED", ISS_OFF); IUFillSwitch(&AxisTwoState[NOT_INITIALISED], "NOT_INITIALISED", "NOT_INITIALISED", ISS_ON); IUFillSwitchVector(&AxisTwoStateV, AxisTwoState, 6, getDeviceName(), "AXIS_TWO_STATE", "Axis two state", DetailedMountInfoPage, IP_RO, ISR_NOFMANY, 60, IPS_IDLE); IUFillNumber(&AxisOneEncoderValues[RAW_MICROSTEPS], "RAW_MICROSTEPS", "Raw Microsteps", "%.0f", 0, 0xFFFFFF, 1, 0); IUFillNumber(&AxisOneEncoderValues[MICROSTEPS_PER_ARCSEC], "MICROSTEPS_PER_ARCSEC", "Microsteps/arcsecond", "%.4f", 0, 0xFFFFFF, 1, 0); IUFillNumber(&AxisOneEncoderValues[OFFSET_FROM_INITIAL], "OFFSET_FROM_INITIAL", "Offset from initial", "%.0f", 0, 0xFFFFFF, 1, 0); IUFillNumber(&AxisOneEncoderValues[DEGREES_FROM_INITIAL], "DEGREES_FROM_INITIAL", "Degrees from initial", "%.2f", -1000.0, 1000.0, 1, 0); IUFillNumberVector(&AxisOneEncoderValuesV, AxisOneEncoderValues, 4, getDeviceName(), "AXIS1_ENCODER_VALUES", "Axis 1 Encoder values", DetailedMountInfoPage, IP_RO, 60, IPS_IDLE); IUFillNumber(&AxisTwoEncoderValues[RAW_MICROSTEPS], "RAW_MICROSTEPS", "Raw Microsteps", "%.0f", 0, 0xFFFFFF, 1, 0); IUFillNumber(&AxisTwoEncoderValues[MICROSTEPS_PER_ARCSEC], "MICROSTEPS_PER_ARCSEC", "Microsteps/arcsecond", "%.4f", 0, 0xFFFFFF, 1, 0); IUFillNumber(&AxisTwoEncoderValues[OFFSET_FROM_INITIAL], "OFFSET_FROM_INITIAL", "Offset from initial", "%.0f", 0, 0xFFFFFF, 1, 0); IUFillNumber(&AxisTwoEncoderValues[DEGREES_FROM_INITIAL], "DEGREES_FROM_INITIAL", "Degrees from initial", "%.2f", -1000.0, 1000.0, 1, 0); IUFillNumberVector(&AxisTwoEncoderValuesV, AxisTwoEncoderValues, 4, getDeviceName(), "AXIS2_ENCODER_VALUES", "Axis 2 Encoder values", DetailedMountInfoPage, IP_RO, 60, IPS_IDLE); // Register any visible before connection properties // Slew modes IUFillSwitch(&SlewModes[SLEW_SILENT], "SLEW_SILENT", "Silent", ISS_OFF); IUFillSwitch(&SlewModes[SLEW_NORMAL], "SLEW_NORMAL", "Normal", ISS_OFF); IUFillSwitchVector(&SlewModesSP, SlewModes, 2, getDeviceName(), "TELESCOPE_MOTION_SLEWMODE", "Slew Mode", MOTION_TAB, IP_RW, ISR_ATMOST1, 60, IPS_IDLE); // Wedge mode IUFillSwitch(&WedgeMode[WEDGE_SIMPLE], "WEDGE_SIMPLE", "Simple wedge", ISS_OFF); IUFillSwitch(&WedgeMode[WEDGE_EQ], "WEDGE_EQ", "EQ wedge", ISS_OFF); IUFillSwitch(&WedgeMode[WEDGE_DISABLED], "WEDGE_DISABLED", "Disabled", ISS_OFF); IUFillSwitchVector(&WedgeModeSP, WedgeMode, 3, getDeviceName(), "TELESCOPE_MOTION_WEDGEMODE", "Wedge Mode", MOTION_TAB, IP_RW, ISR_ATMOST1, 60, IPS_IDLE); // Track logging mode IUFillSwitch(&TrackLogMode[TRACKLOG_ENABLED], "TRACKLOG_ENABLED", "Enable logging", ISS_OFF); IUFillSwitch(&TrackLogMode[TRACKLOG_DISABLED], "TRACKLOG_DISABLED", "Disabled", ISS_ON); IUFillSwitchVector(&TrackLogModeSP, TrackLogMode, 2, getDeviceName(), "TELESCOPE_MOTION_TRACKLOGMODE", "Track Logging Mode", MOTION_TAB, IP_RW, ISR_ATMOST1, 60, IPS_IDLE); // Guiding rates for RA/DEC axes IUFillNumber(&GuidingRatesN[0], "GUIDERA_RATE", "microsteps/seconds (RA)", "%1.3f", 0.00001, 100000.0, 0.00001, 1.0); IUFillNumber(&GuidingRatesN[1], "GUIDEDEC_RATE", "microsteps/seconds (Dec)", "%1.3f", 0.00001, 100000.0, 0.00001, 1.0); IUFillNumberVector(&GuidingRatesNP, GuidingRatesN, 2, getDeviceName(), "GUIDE_RATES", "Guide Rates", MOTION_TAB, IP_RW, 60, IPS_IDLE); // Tracking rate // For Skywatcher Virtuoso: // Alt rate: 0.72, Az rate: 0.72, timeout: 1000 msec // For Skywatcher Merlin: // Alt rate: 0.64, Az rate: 0.64, timeout: 1000 msec IUFillNumber(&TrackingValuesN[0], "TRACKING_RATE_ALT", "rate (Alt)", "%1.3f", 0.001, 10.0, 0.000001, 0.64); IUFillNumber(&TrackingValuesN[1], "TRACKING_RATE_AZ", "rate (Az)", "%1.3f", 0.001, 10.0, 0.000001, 0.64); IUFillNumber(&TrackingValuesN[2], "TRACKING_TIMEOUT", "msec (period)", "%1.3f", 0.001, 10000.0, 0.000001, 1000.0); IUFillNumberVector(&TrackingValuesNP, TrackingValuesN, 3, getDeviceName(), "TRACKING_VALUES", "Tracking Values", MOTION_TAB, IP_RW, 60, IPS_IDLE); // Park movement directions IUFillSwitch(&ParkMovementDirection[PARK_COUNTERCLOCKWISE], "PMD_COUNTERCLOCKWISE", "Counterclockwise", ISS_ON); IUFillSwitch(&ParkMovementDirection[PARK_CLOCKWISE], "PMD_CLOCKWISE", "Clockwise", ISS_OFF); IUFillSwitchVector(&ParkMovementDirectionSP, ParkMovementDirection, 2, getDeviceName(), "PARK_DIRECTION", "Park Direction", MOTION_TAB, IP_RW, ISR_ATMOST1, 60, IPS_IDLE); // Park positions IUFillSwitch(&ParkPosition[PARK_NORTH], "PARK_NORTH", "North", ISS_ON); IUFillSwitch(&ParkPosition[PARK_EAST], "PARK_EAST", "East", ISS_OFF); IUFillSwitch(&ParkPosition[PARK_SOUTH], "PARK_SOUTH", "South", ISS_OFF); IUFillSwitch(&ParkPosition[PARK_WEST], "PARK_WEST", "West", ISS_OFF); IUFillSwitchVector(&ParkPositionSP, ParkPosition, 4, getDeviceName(), "PARK_POSITION", "Park Position", MOTION_TAB, IP_RW, ISR_ATMOST1, 60, IPS_IDLE); // Unpark positions IUFillSwitch(&UnparkPosition[PARK_NORTH], "UNPARK_NORTH", "North", ISS_OFF); IUFillSwitch(&UnparkPosition[PARK_EAST], "UNPARK_EAST", "East", ISS_OFF); IUFillSwitch(&UnparkPosition[PARK_SOUTH], "UNPARK_SOUTH", "South", ISS_OFF); IUFillSwitch(&UnparkPosition[PARK_WEST], "UNPARK_WEST", "West", ISS_OFF); IUFillSwitchVector(&UnparkPositionSP, UnparkPosition, 4, getDeviceName(), "UNPARK_POSITION", "Unpark Position", MOTION_TAB, IP_RW, ISR_ATMOST1, 60, IPS_IDLE); // Guiding support initGuiderProperties(getDeviceName(), GUIDE_TAB); setDriverInterface(getDriverInterface() | GUIDER_INTERFACE); return true; } void SkywatcherAltAzSimple::ISGetProperties(const char *dev) { IDLog("SkywatcherAltAzSimple::ISGetProperties\n"); INDI::Telescope::ISGetProperties(dev); if (isConnected()) { // Fill in any real values now available MCInit should have been called already UpdateDetailedMountInformation(false); // Define our connected only properties to the base driver // e.g. defineNumber(MyNumberVectorPointer); // This will register our properties and send a IDDefXXXX mewssage to any connected clients defineText(&BasicMountInfoV); defineNumber(&AxisOneInfoV); defineSwitch(&AxisOneStateV); defineNumber(&AxisTwoInfoV); defineSwitch(&AxisTwoStateV); defineNumber(&AxisOneEncoderValuesV); defineNumber(&AxisTwoEncoderValuesV); defineSwitch(&SlewModesSP); defineSwitch(&WedgeModeSP); defineSwitch(&TrackLogModeSP); defineNumber(&GuidingRatesNP); defineNumber(&TrackingValuesNP); defineSwitch(&ParkMovementDirectionSP); defineSwitch(&ParkPositionSP); defineSwitch(&UnparkPositionSP); defineNumber(&GuideNSNP); defineNumber(&GuideWENP); } } bool SkywatcherAltAzSimple::ISNewBLOB(const char *dev, const char *name, int sizes[], int blobsizes[], char *blobs[], char *formats[], char *names[], int n) { if (dev != nullptr && strcmp(dev, getDeviceName()) == 0) { // It is for us } // Pass it up the chain return INDI::Telescope::ISNewBLOB(dev, name, sizes, blobsizes, blobs, formats, names, n); } bool SkywatcherAltAzSimple::ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) { if (dev != nullptr && strcmp(dev, getDeviceName()) == 0) { if (strcmp(name, "GUIDE_RATES") == 0) { ResetGuidePulses(); GuidingRatesNP.s = IPS_OK; IUUpdateNumber(&GuidingRatesNP, values, names, n); IDSetNumber(&GuidingRatesNP, nullptr); return true; } if (strcmp(name, "TRACKING_VALUES") == 0) { TrackingValuesNP.s = IPS_OK; IUUpdateNumber(&TrackingValuesNP, values, names, n); IDSetNumber(&TrackingValuesNP, nullptr); return true; } // Let our driver do sync operation in park position if (strcmp(name, "EQUATORIAL_EOD_COORD") == 0) { double ra = -1; double dec = -100; for (int x = 0; x < n; x++) { INumber *eqp = IUFindNumber(&EqNP, names[x]); if (eqp == &EqN[AXIS_RA]) { ra = values[x]; } else if (eqp == &EqN[AXIS_DE]) { dec = values[x]; } } if ((ra >= 0) && (ra <= 24) && (dec >= -90) && (dec <= 90)) { ISwitch *sw = IUFindSwitch(&CoordSP, "SYNC"); if (sw != nullptr && sw->s == ISS_ON && isParked()) { return Sync(ra, dec); } } } processGuiderProperties(name, values, names, n); } // Pass it up the chain return INDI::Telescope::ISNewNumber(dev, name, values, names, n); } bool SkywatcherAltAzSimple::ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) { IUUpdateSwitch(getSwitch(name), states, names, n); if (dev != nullptr && strcmp(dev, getDeviceName()) == 0) { // It is for us } // Pass it up the chain return INDI::Telescope::ISNewSwitch(dev, name, states, names, n); } bool SkywatcherAltAzSimple::ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n) { if (dev != nullptr && strcmp(dev, getDeviceName()) == 0) { // It is for us } // Pass it up the chain bool Ret = INDI::Telescope::ISNewText(dev, name, texts, names, n); // The scope config switch must be updated after the config is saved to disk if (dev != nullptr && strcmp(dev, getDeviceName()) == 0) { if (name && std::string(name) == "SCOPE_CONFIG_NAME") { UpdateScopeConfigSwitch(); } } return Ret; } void SkywatcherAltAzSimple::UpdateScopeConfigSwitch() { if (!CheckFile(ScopeConfigFileName, false)) { DEBUGF(INDI::Logger::DBG_SESSION, "Can't open XML file (%s) for read", ScopeConfigFileName.c_str()); return; } LilXML *XmlHandle = newLilXML(); FILE *FilePtr = fopen(ScopeConfigFileName.c_str(), "r"); XMLEle *RootXmlNode = nullptr; XMLEle *CurrentXmlNode = nullptr; XMLAtt *Ap = nullptr; bool DeviceFound = false; char ErrMsg[512]; RootXmlNode = readXMLFile(FilePtr, XmlHandle, ErrMsg); delLilXML(XmlHandle); XmlHandle = nullptr; if (!RootXmlNode) { DEBUGF(INDI::Logger::DBG_SESSION, "Failed to parse XML file (%s): %s", ScopeConfigFileName.c_str(), ErrMsg); return; } if (std::string(tagXMLEle(RootXmlNode)) != ScopeConfigRootXmlNode) { DEBUGF(INDI::Logger::DBG_SESSION, "Not a scope config XML file (%s)", ScopeConfigFileName.c_str()); delXMLEle(RootXmlNode); return; } CurrentXmlNode = nextXMLEle(RootXmlNode, 1); // Find the current telescope in the config file while (CurrentXmlNode) { if (std::string(tagXMLEle(CurrentXmlNode)) != ScopeConfigDeviceXmlNode) { CurrentXmlNode = nextXMLEle(RootXmlNode, 0); continue; } Ap = findXMLAtt(CurrentXmlNode, ScopeConfigNameXmlNode.c_str()); if (Ap && !strcmp(valuXMLAtt(Ap), getDeviceName())) { DeviceFound = true; break; } CurrentXmlNode = nextXMLEle(RootXmlNode, 0); } if (!DeviceFound) { DEBUGF(INDI::Logger::DBG_SESSION, "No a scope config found for %s in the XML file (%s)", getDeviceName(), ScopeConfigFileName.c_str()); delXMLEle(RootXmlNode); return; } // Read the values XMLEle *XmlNode = nullptr; XMLEle *DeviceXmlNode = CurrentXmlNode; std::string ConfigName; for (int i = 1; i < 7; ++i) { bool Found = true; CurrentXmlNode = findXMLEle(DeviceXmlNode, ("config"+std::to_string(i)).c_str()); if (CurrentXmlNode) { XmlNode = findXMLEle(CurrentXmlNode, ScopeConfigLabelApXmlNode.c_str()); if (XmlNode) { ConfigName = pcdataXMLEle(XmlNode); } } else { Found = false; } // Change the switch label ISwitch *configSwitch = IUFindSwitch(&ScopeConfigsSP, ("SCOPE_CONFIG"+std::to_string(i)).c_str()); if (configSwitch != nullptr) { // The config is not used yet if (!Found) { strncpy(configSwitch->label, ("Config #"+std::to_string(i)+" - Not used").c_str(), MAXINDILABEL); continue; } // Empty switch label if (ConfigName.empty()) { strncpy(configSwitch->label, ("Config #"+std::to_string(i)+" - Untitled").c_str(), MAXINDILABEL); continue; } strncpy(configSwitch->label, ("Config #"+std::to_string(i)+" - "+ConfigName).c_str(), MAXINDILABEL); } } delXMLEle(RootXmlNode); // Delete the joystick control to get the telescope config switch to the bottom of the page deleteProperty("USEJOYSTICK"); // Recreate the switch control deleteProperty(ScopeConfigsSP.name); defineSwitch(&ScopeConfigsSP); } double SkywatcherAltAzSimple::GetSlewRate() { ISwitch *Switch = IUFindOnSwitch(&SlewRateSP); double Rate = *((double *)Switch->aux); return Rate; } bool SkywatcherAltAzSimple::MoveNS(INDI_DIR_NS dir, TelescopeMotionCommand command) { DEBUG(DBG_SCOPE, "SkywatcherAltAzSimple::MoveNS"); double speed = (dir == DIRECTION_NORTH) ? GetSlewRate() * LOW_SPEED_MARGIN / 2 : -GetSlewRate() * LOW_SPEED_MARGIN / 2; const char *dirStr = (dir == DIRECTION_NORTH) ? "North" : "South"; if (IsMerlinMount()) { speed = -speed; } switch (command) { case MOTION_START: DEBUGF(DBG_SCOPE, "Starting Slew %s", dirStr); // Ignore the silent mode because MoveNS() is called by the manual motion UI controls. Slew(AXIS2, speed, true); break; case MOTION_STOP: DEBUGF(DBG_SCOPE, "Stopping Slew %s", dirStr); SlowStop(AXIS2); break; } return true; } bool SkywatcherAltAzSimple::MoveWE(INDI_DIR_WE dir, TelescopeMotionCommand command) { DEBUG(DBG_SCOPE, "SkywatcherAltAzSimple::MoveWE"); double speed = (dir == DIRECTION_WEST) ? GetSlewRate() * LOW_SPEED_MARGIN / 2 : -GetSlewRate() * LOW_SPEED_MARGIN / 2; const char *dirStr = (dir == DIRECTION_WEST) ? "West" : "East"; speed = -speed; switch (command) { case MOTION_START: DEBUGF(DBG_SCOPE, "Starting Slew %s", dirStr); // Ignore the silent mode because MoveNS() is called by the manual motion UI controls. Slew(AXIS1, speed, true); break; case MOTION_STOP: DEBUGF(DBG_SCOPE, "Stopping Slew %s", dirStr); SlowStop(AXIS1); break; } return true; } double SkywatcherAltAzSimple::GetParkDeltaAz(ParkDirection_t target_direction, ParkPosition_t target_position) { double Result = 0; DEBUGF(DBG_SCOPE, "GetParkDeltaAz: direction %d - position: %d", (int)target_direction, (int)target_position); // Calculate delta degrees (target: NORTH) if (target_position == PARK_NORTH) { if (target_direction == PARK_COUNTERCLOCKWISE) { Result = -CurrentAltAz.az; } else { Result = 360 - CurrentAltAz.az; } } // Calculate delta degrees (target: EAST) if (target_position == PARK_EAST) { if (target_direction == PARK_COUNTERCLOCKWISE) { if (CurrentAltAz.az > 0 && CurrentAltAz.az < 90) Result = -270 - CurrentAltAz.az; else Result = -CurrentAltAz.az + 90; } else { if (CurrentAltAz.az > 0 && CurrentAltAz.az < 90) Result = 90 - CurrentAltAz.az; else Result = 360 - CurrentAltAz.az + 90; } } // Calculate delta degrees (target: SOUTH) if (target_position == PARK_SOUTH) { if (target_direction == PARK_COUNTERCLOCKWISE) { if (CurrentAltAz.az > 0 && CurrentAltAz.az < 180) Result = -180 - CurrentAltAz.az; else Result = -CurrentAltAz.az + 180; } else { if (CurrentAltAz.az > 0 && CurrentAltAz.az < 180) Result = 180 - CurrentAltAz.az; else Result = 360 - CurrentAltAz.az + 180; } } // Calculate delta degrees (target: WEST) if (target_position == PARK_WEST) { if (target_direction == PARK_COUNTERCLOCKWISE) { if (CurrentAltAz.az > 0 && CurrentAltAz.az < 270) Result = -90 - CurrentAltAz.az; else Result = -CurrentAltAz.az + 270; } else { if (CurrentAltAz.az > 0 && CurrentAltAz.az < 270) Result = 270 - CurrentAltAz.az; else Result = 360 - CurrentAltAz.az + 270; } } if (Result >= 360) { Result -= 360; } if (Result <= -360) { Result += 360; } return Result; } bool SkywatcherAltAzSimple::Park() { DEBUG(DBG_SCOPE, "SkywatcherAltAzSimple::Park"); ParkPosition_t TargetPosition = PARK_NORTH; ParkDirection_t TargetDirection = PARK_COUNTERCLOCKWISE; double DeltaAlt = 0; double DeltaAz = 0; // Determinate the target position and direction if (IUFindSwitch(&ParkPositionSP, "PARK_NORTH") != nullptr && IUFindSwitch(&ParkPositionSP, "PARK_NORTH")->s == ISS_ON) { TargetPosition = PARK_NORTH; } if (IUFindSwitch(&ParkPositionSP, "PARK_EAST") != nullptr && IUFindSwitch(&ParkPositionSP, "PARK_EAST")->s == ISS_ON) { TargetPosition = PARK_EAST; } if (IUFindSwitch(&ParkPositionSP, "PARK_SOUTH") != nullptr && IUFindSwitch(&ParkPositionSP, "PARK_SOUTH")->s == ISS_ON) { TargetPosition = PARK_SOUTH; } if (IUFindSwitch(&ParkPositionSP, "PARK_WEST") != nullptr && IUFindSwitch(&ParkPositionSP, "PARK_WEST")->s == ISS_ON) { TargetPosition = PARK_WEST; } if (IUFindSwitch(&ParkMovementDirectionSP, "PMD_COUNTERCLOCKWISE") != nullptr && IUFindSwitch(&ParkMovementDirectionSP, "PMD_COUNTERCLOCKWISE")->s == ISS_ON) { TargetDirection = PARK_COUNTERCLOCKWISE; } if (IUFindSwitch(&ParkMovementDirectionSP, "PMD_CLOCKWISE") != nullptr && IUFindSwitch(&ParkMovementDirectionSP, "PMD_CLOCKWISE")->s == ISS_ON) { TargetDirection = PARK_CLOCKWISE; } DeltaAz = GetParkDeltaAz(TargetDirection, TargetPosition); // Move the telescope to the desired position long AltitudeOffsetMicrosteps = DegreesToMicrosteps(AXIS2, DeltaAlt); long AzimuthOffsetMicrosteps = DegreesToMicrosteps(AXIS1, DeltaAz); DEBUGF(DBG_SCOPE, "Parking: Delta altitude %1.2f - delta azimuth %1.2f", DeltaAlt, DeltaAz); DEBUGF(DBG_SCOPE,"Parking: Altitude offset %ld microsteps Azimuth offset %ld microsteps", AltitudeOffsetMicrosteps, AzimuthOffsetMicrosteps); if (IUFindSwitch(&SlewModesSP, "SLEW_NORMAL")->s == ISS_ON) { SilentSlewMode = false; } else { SilentSlewMode = true; } SlewTo(AXIS1, AzimuthOffsetMicrosteps); SlewTo(AXIS2, AltitudeOffsetMicrosteps); TrackState = SCOPE_PARKING; return true; } bool SkywatcherAltAzSimple::UnPark() { DEBUG(DBG_SCOPE, "SkywatcherAltAzSimple::UnPark"); ParkPosition_t TargetPosition = PARK_NORTH; ParkDirection_t TargetDirection = PARK_COUNTERCLOCKWISE; double DeltaAlt = 0; double DeltaAz = 0; // Determinate the target position and direction if (IUFindSwitch(&UnparkPositionSP, "UNPARK_NORTH") != nullptr && IUFindSwitch(&UnparkPositionSP, "UNPARK_NORTH")->s == ISS_ON) { TargetPosition = PARK_NORTH; } if (IUFindSwitch(&UnparkPositionSP, "UNPARK_EAST") != nullptr && IUFindSwitch(&UnparkPositionSP, "UNPARK_EAST")->s == ISS_ON) { TargetPosition = PARK_EAST; } if (IUFindSwitch(&UnparkPositionSP, "UNPARK_SOUTH") != nullptr && IUFindSwitch(&UnparkPositionSP, "UNPARK_SOUTH")->s == ISS_ON) { TargetPosition = PARK_SOUTH; } if (IUFindSwitch(&UnparkPositionSP, "UNPARK_WEST") != nullptr && IUFindSwitch(&UnparkPositionSP, "UNPARK_WEST")->s == ISS_ON) { TargetPosition = PARK_WEST; } // Note: The reverse direction is used for unparking. if (IUFindSwitch(&ParkMovementDirectionSP, "PMD_COUNTERCLOCKWISE") != nullptr && IUFindSwitch(&ParkMovementDirectionSP, "PMD_COUNTERCLOCKWISE")->s == ISS_ON) { TargetDirection = PARK_CLOCKWISE; } if (IUFindSwitch(&ParkMovementDirectionSP, "PMD_CLOCKWISE") != nullptr && IUFindSwitch(&ParkMovementDirectionSP, "PMD_CLOCKWISE")->s == ISS_ON) { TargetDirection = PARK_COUNTERCLOCKWISE; } DeltaAz = GetParkDeltaAz(TargetDirection, TargetPosition); // Altitude 3360 points the telescope upwards DeltaAlt = CurrentAltAz.alt - 3360; // Move the telescope to the desired position long AltitudeOffsetMicrosteps = DegreesToMicrosteps(AXIS2, DeltaAlt); long AzimuthOffsetMicrosteps = DegreesToMicrosteps(AXIS1, DeltaAz); DEBUGF(DBG_SCOPE, "Unparking: Delta altitude %1.2f - delta azimuth %1.2f", DeltaAlt, DeltaAz); DEBUGF(DBG_SCOPE, "Unparking: Altitude offset %ld microsteps Azimuth offset %ld microsteps", AltitudeOffsetMicrosteps, AzimuthOffsetMicrosteps); if (IUFindSwitch(&SlewModesSP, "SLEW_NORMAL")->s == ISS_ON) { SilentSlewMode = false; } else { SilentSlewMode = true; } SlewTo(AXIS1, AzimuthOffsetMicrosteps); SlewTo(AXIS2, AltitudeOffsetMicrosteps); SetParked(false); TrackState = SCOPE_SLEWING; return true; } bool SkywatcherAltAzSimple::ReadScopeStatus() { // DEBUG(DBG_SCOPE, "SkywatcherAltAzSimple::ReadScopeStatus"); // leave the following stuff in for the time being it is mostly harmless // Quick check of the mount if (UpdateCount == 0 && !GetMotorBoardVersion(AXIS1)) return false; if (!GetStatus(AXIS1)) return false; if (!GetStatus(AXIS2)) return false; // Update Axis Position if (!GetEncoder(AXIS1)) return false; if (!GetEncoder(AXIS2)) return false; if (UpdateCount % 5 == 0) UpdateDetailedMountInformation(true); UpdateCount++; if (TrackState == SCOPE_PARKING) { if (!IsInMotion(AXIS1) && !IsInMotion(AXIS2)) { SetParked(true); } } // Calculate new RA DEC ln_hrz_posn AltAz { 0, 0 }; AltAz.alt = MicrostepsToDegrees(AXIS2, CurrentEncoders[AXIS2] - ZeroPositionEncoders[AXIS2]); if (VerboseScopeStatus) { DEBUGF(DBG_SCOPE, "Axis2 encoder %ld initial %ld alt(degrees) %lf", CurrentEncoders[AXIS2], ZeroPositionEncoders[AXIS2], AltAz.alt); } AltAz.az = MicrostepsToDegrees(AXIS1, CurrentEncoders[AXIS1] - ZeroPositionEncoders[AXIS1]); CurrentAltAz = AltAz; if (VerboseScopeStatus) { DEBUGF(DBG_SCOPE, "Axis1 encoder %ld initial %ld az(degrees) %lf", CurrentEncoders[AXIS1], ZeroPositionEncoders[AXIS1], AltAz.az); } ln_equ_posn RaDec { 0, 0 }; RaDec = GetRaDecPosition(AltAz.alt, AltAz.az); if (VerboseScopeStatus) { DEBUGF(DBG_SCOPE, "New RA %lf (hours) DEC %lf (degrees)", RaDec.ra, RaDec.dec); } LogMessage("STATUS: Ra %lf Dec %lf - Alt %lf Az %lf - microsteps %ld %ld", RaDec.ra, RaDec.dec, AltAz.alt, AltAz.az, CurrentEncoders[AXIS2]-ZeroPositionEncoders[AXIS2], CurrentEncoders[AXIS1]-ZeroPositionEncoders[AXIS1]); NewRaDec(RaDec.ra, RaDec.dec); VerboseScopeStatus = false; return true; } bool SkywatcherAltAzSimple::saveConfigItems(FILE *fp) { IUSaveConfigSwitch(fp, &SlewModesSP); IUSaveConfigSwitch(fp, &WedgeModeSP); IUSaveConfigSwitch(fp, &TrackLogModeSP); IUSaveConfigNumber(fp, &GuidingRatesNP); IUSaveConfigNumber(fp, &TrackingValuesNP); IUSaveConfigSwitch(fp, &ParkMovementDirectionSP); IUSaveConfigSwitch(fp, &ParkPositionSP); IUSaveConfigSwitch(fp, &UnparkPositionSP); return INDI::Telescope::saveConfigItems(fp); } bool SkywatcherAltAzSimple::Sync(double ra, double dec) { DEBUG(DBG_SCOPE, "SkywatcherAltAzSimple::Sync"); // Compute a telescope direction vector from the current encoders if (!GetEncoder(AXIS1)) return false; if (!GetEncoder(AXIS2)) return false; ln_hrz_posn AltAz { 0, 0 }; AltAz = GetAltAzPosition(ra, dec); double DeltaAz = CurrentAltAz.az-AltAz.az; double DeltaAlt = CurrentAltAz.alt-AltAz.alt; LogMessage("SYNC: Ra %lf Dec %lf", ra, dec); MYDEBUGF(INDI::Logger::DBG_SESSION, "Sync ra: %lf dec: %lf => CurAz: %lf -> NewAz: %lf", ra, dec, CurrentAltAz.az, AltAz.az); PolarisPositionEncoders[AXIS1] += DegreesToMicrosteps(AXIS1, DeltaAz); PolarisPositionEncoders[AXIS2] += DegreesToMicrosteps(AXIS2, DeltaAlt); ZeroPositionEncoders[AXIS1] = PolarisPositionEncoders[AXIS1]; ZeroPositionEncoders[AXIS2] = PolarisPositionEncoders[AXIS2]; // The tracking seconds should be reset to restart the drift compensation ResetTrackingSeconds = true; // Stop any movements if (TrackState != SCOPE_IDLE && TrackState != SCOPE_PARKED) { Abort(); } // Might as well do this UpdateDetailedMountInformation(true); return true; } void SkywatcherAltAzSimple::TimerHit() { static bool Slewing = false; static bool Tracking = false; static int ElapsedTime = 0; if (!ReadScopeStatus()) { SetTimer(TimeoutDuration); return; } LogMessage("SET TIMER: %d msec", TimeoutDuration); SetTimer(TimeoutDuration); ElapsedTime += TimeoutDuration; if (ElapsedTime >= 5000) { ElapsedTime = 0; VerboseScopeStatus = true; } switch (TrackState) { case SCOPE_SLEWING: if (!Slewing) { DEBUG(INDI::Logger::DBG_SESSION, "Slewing started"); TrackingStartTimer = 0; } TrackingMsecs = 0; GuideDeltaAlt = 0; GuideDeltaAz = 0; ResetGuidePulses(); TimeoutDuration = 400; Tracking = false; Slewing = true; GuidingPulses.clear(); if ((AxesStatus[AXIS1].FullStop) && (AxesStatus[AXIS2].FullStop)) { TrackingStartTimer += TimeoutDuration; if (TrackingStartTimer < 3000) return; if (IUFindSwitch(&WedgeModeSP, "WEDGE_EQ")->s == ISS_ON || IUFindSwitch(&CoordSP, "TRACK")->s == ISS_ON) { // Goto has finished start tracking TrackState = SCOPE_TRACKING; } else { TrackState = SCOPE_IDLE; break; } } break; case SCOPE_TRACKING: { if (!Tracking) { DEBUG(INDI::Logger::DBG_SESSION, "Tracking started"); TrackingMsecs = 0; TimeoutDuration = (int)IUFindNumber(&TrackingValuesNP, "TRACKING_TIMEOUT")->value; GuideDeltaAlt = 0; GuideDeltaAz = 0; ResetGuidePulses(); } // Restart the drift compensation after syncing if (ResetTrackingSeconds) { ResetTrackingSeconds = false; TrackingMsecs = 0; GuideDeltaAlt = 0; GuideDeltaAz = 0; ResetGuidePulses(); } TrackingMsecs += TimeoutDuration; if (TrackingMsecs % 60000 == 0) { DEBUGF(INDI::Logger::DBG_SESSION, "Tracking in progress (%d seconds elapsed)", TrackingMsecs / 1000); } Tracking = true; Slewing = false; // Continue or start tracking // ln_hrz_posn AltAz { 0, 0 }; ln_hrz_posn FutureAltAz { 0, 0 }; // AltAz.alt = MicrostepsToDegrees(AXIS2, CurrentEncoders[AXIS2] - ZeroPositionEncoders[AXIS2]); // AltAz.az = MicrostepsToDegrees(AXIS1, CurrentEncoders[AXIS1] - ZeroPositionEncoders[AXIS1]); FutureAltAz = GetAltAzPosition(CurrentTrackingTarget.ra, CurrentTrackingTarget.dec, (double)TimeoutDuration / 1000); // DEBUGF(DBG_SCOPE, // "Tracking AXIS1 CurrentEncoder %ld OldTrackingTarget %ld AXIS2 CurrentEncoder %ld OldTrackingTarget " // "%ld", // CurrentEncoders[AXIS1], OldTrackingTarget[AXIS1], CurrentEncoders[AXIS2], OldTrackingTarget[AXIS2]); // DEBUGF(DBG_SCOPE, // "New Tracking Target Altitude %lf degrees %ld microsteps Azimuth %lf degrees %ld microsteps", // AltAz.alt, DegreesToMicrosteps(AXIS2, AltAz.alt), AltAz.az, DegreesToMicrosteps(AXIS1, AltAz.az)); // Calculate the auto-guiding delta degrees for (auto pulse : GuidingPulses) { GuideDeltaAlt += pulse.DeltaAlt; GuideDeltaAz += pulse.DeltaAz; } GuidingPulses.clear(); long AltitudeOffsetMicrosteps = DegreesToMicrosteps(AXIS2, FutureAltAz.alt-CurrentAltAz.alt+GuideDeltaAlt); long AzimuthOffsetMicrosteps = DegreesToMicrosteps(AXIS1, FutureAltAz.az-CurrentAltAz.az+GuideDeltaAz); // When the Alt/Az mount is on the top of an EQ mount, the EQ mount already tracks in // sidereal speed. Only autoguiding is enabled in tracking mode. if (IUFindSwitch(&WedgeModeSP, "WEDGE_EQ")->s == ISS_ON) { AltitudeOffsetMicrosteps = (long)((float)IUFindNumber(&GuidingRatesNP, "GUIDEDEC_RATE")->value*GuideDeltaAlt); AzimuthOffsetMicrosteps = (long)((float)IUFindNumber(&GuidingRatesNP, "GUIDERA_RATE")->value*GuideDeltaAz); GuideDeltaAlt = 0; GuideDeltaAz = 0; // Correct the movements of the EQ mount double DeltaAz = CurrentAltAz.az-FutureAltAz.az; double DeltaAlt = CurrentAltAz.alt-FutureAltAz.alt; PolarisPositionEncoders[AXIS1] += DegreesToMicrosteps(AXIS1, DeltaAz); PolarisPositionEncoders[AXIS2] += DegreesToMicrosteps(AXIS2, DeltaAlt); ZeroPositionEncoders[AXIS1] = PolarisPositionEncoders[AXIS1]; ZeroPositionEncoders[AXIS2] = PolarisPositionEncoders[AXIS2]; } if (AltitudeOffsetMicrosteps > MicrostepsPerRevolution[AXIS2] / 2) { // Going the long way round - send it the other way AltitudeOffsetMicrosteps -= MicrostepsPerRevolution[AXIS2]; } if (AzimuthOffsetMicrosteps > MicrostepsPerRevolution[AXIS1] / 2) { // Going the long way round - send it the other way AzimuthOffsetMicrosteps -= MicrostepsPerRevolution[AXIS1]; } if (AltitudeOffsetMicrosteps < -MicrostepsPerRevolution[AXIS2] / 2) { // Going the long way round - send it the other way AltitudeOffsetMicrosteps += MicrostepsPerRevolution[AXIS2]; } if (AzimuthOffsetMicrosteps < -MicrostepsPerRevolution[AXIS1] / 2) { // Going the long way round - send it the other way AzimuthOffsetMicrosteps += MicrostepsPerRevolution[AXIS1]; } AltitudeOffsetMicrosteps = (long)((double)AltitudeOffsetMicrosteps*IUFindNumber(&TrackingValuesNP, "TRACKING_RATE_ALT")->value); AzimuthOffsetMicrosteps = (long)((double)AzimuthOffsetMicrosteps*IUFindNumber(&TrackingValuesNP, "TRACKING_RATE_AZ")->value); LogMessage("TRACKING: now Alt %lf Az %lf - future Alt %lf Az %lf - microsteps_diff Alt %ld Az %ld", CurrentAltAz.alt, CurrentAltAz.az, FutureAltAz.alt, FutureAltAz.az, AltitudeOffsetMicrosteps, AzimuthOffsetMicrosteps); // DEBUGF(DBG_SCOPE, "New Tracking Target AltitudeOffset %ld microsteps AzimuthOffset %ld microsteps", // AltitudeOffsetMicrosteps, AzimuthOffsetMicrosteps); if (0 != AzimuthOffsetMicrosteps) { SlewTo(AXIS1, AzimuthOffsetMicrosteps, false); } else { // Nothing to do - stop the axis SlowStop(AXIS1); } if (0 != AltitudeOffsetMicrosteps) { SlewTo(AXIS2, AltitudeOffsetMicrosteps, false); } else { // Nothing to do - stop the axis SlowStop(AXIS2); } DEBUGF(DBG_SCOPE, "Tracking - AXIS1 error %d (offset: %ld) AXIS2 error %d (offset: %ld)", OldTrackingTarget[AXIS1] - CurrentEncoders[AXIS1], AzimuthOffsetMicrosteps, OldTrackingTarget[AXIS2] - CurrentEncoders[AXIS2], AltitudeOffsetMicrosteps); OldTrackingTarget[AXIS1] = AzimuthOffsetMicrosteps + CurrentEncoders[AXIS1]; OldTrackingTarget[AXIS2] = AltitudeOffsetMicrosteps + CurrentEncoders[AXIS2]; break; } break; default: if (Slewing) { DEBUG(INDI::Logger::DBG_SESSION, "Slewing stopped"); } if (Tracking) { DEBUG(INDI::Logger::DBG_SESSION, "Tracking stopped"); } TrackingMsecs = 0; GuideDeltaAlt = 0; GuideDeltaAz = 0; ResetGuidePulses(); TimeoutDuration = 1000; Tracking = false; Slewing = false; GuidingPulses.clear(); break; } } bool SkywatcherAltAzSimple::updateProperties() { INDI::Telescope::updateProperties(); if (isConnected()) { // Fill in any real values now available MCInit should have been called already UpdateDetailedMountInformation(false); // Define our connected only properties to the base driver // e.g. defineNumber(MyNumberVectorPointer); // This will register our properties and send a IDDefXXXX message to any connected clients // I have now idea why I have to do this here as well as in ISGetProperties. It makes me // concerned there is a design or implementation flaw somewhere. defineText(&BasicMountInfoV); defineNumber(&AxisOneInfoV); defineSwitch(&AxisOneStateV); defineNumber(&AxisTwoInfoV); defineSwitch(&AxisTwoStateV); defineNumber(&AxisOneEncoderValuesV); defineNumber(&AxisTwoEncoderValuesV); defineSwitch(&SlewModesSP); defineSwitch(&WedgeModeSP); defineSwitch(&TrackLogModeSP); defineNumber(&GuidingRatesNP); defineNumber(&TrackingValuesNP); defineSwitch(&ParkMovementDirectionSP); defineSwitch(&ParkPositionSP); defineSwitch(&UnparkPositionSP); defineNumber(&GuideNSNP); defineNumber(&GuideWENP); return true; } else { // Delete any connected only properties from the base driver's list // e.g. deleteProperty(MyNumberVector.name); deleteProperty(BasicMountInfoV.name); deleteProperty(AxisOneInfoV.name); deleteProperty(AxisOneStateV.name); deleteProperty(AxisTwoInfoV.name); deleteProperty(AxisTwoStateV.name); deleteProperty(AxisOneEncoderValuesV.name); deleteProperty(AxisTwoEncoderValuesV.name); deleteProperty(SlewModesSP.name); deleteProperty(WedgeModeSP.name); deleteProperty(TrackLogModeSP.name); deleteProperty(GuidingRatesNP.name); deleteProperty(TrackingValuesNP.name); deleteProperty(ParkMovementDirectionSP.name); deleteProperty(ParkPositionSP.name); deleteProperty(UnparkPositionSP.name); deleteProperty(GuideNSNP.name); deleteProperty(GuideWENP.name); return true; } } IPState SkywatcherAltAzSimple::GuideNorth(float ms) { GuidingPulse Pulse; LogMessage("GUIDE NORTH: %1.4f", ms); Pulse.DeltaAz = 0; Pulse.DeltaAlt = ms; GuidingPulses.push_back(Pulse); return IPS_OK; } IPState SkywatcherAltAzSimple::GuideSouth(float ms) { GuidingPulse Pulse; LogMessage("GUIDE SOUTH: %1.4f", ms); Pulse.DeltaAz = 0; Pulse.DeltaAlt = -ms; GuidingPulses.push_back(Pulse); return IPS_OK; } IPState SkywatcherAltAzSimple::GuideWest(float ms) { GuidingPulse Pulse; LogMessage("GUIDE WEST: %1.4f", ms); Pulse.DeltaAz = ms; Pulse.DeltaAlt = 0; GuidingPulses.push_back(Pulse); return IPS_OK; } IPState SkywatcherAltAzSimple::GuideEast(float ms) { GuidingPulse Pulse; LogMessage("GUIDE EAST: %1.4f", ms); Pulse.DeltaAz = -ms; Pulse.DeltaAlt = 0; GuidingPulses.push_back(Pulse); return IPS_OK; } // Private methods void SkywatcherAltAzSimple::ResetGuidePulses() { GuidingPulses.clear(); } int SkywatcherAltAzSimple::skywatcher_tty_read(int fd, char *buf, int nbytes, int timeout, int *nbytes_read) { if (!RecoverAfterReconnection && !SerialPortName.empty() && !FileExists(SerialPortName)) { RecoverAfterReconnection = true; serialConnection->Disconnect(); serialConnection->Refresh(); std::this_thread::sleep_for(std::chrono::milliseconds(1000)); if (!serialConnection->Connect()) { RecoverAfterReconnection = true; std::this_thread::sleep_for(std::chrono::milliseconds(1000)); if (!serialConnection->Connect()) { RecoverAfterReconnection = false; return 0; } } SetSerialPort(serialConnection->getPortFD()); SerialPortName = serialConnection->port(); RecoverAfterReconnection = false; } return tty_read(fd, buf, nbytes, timeout, nbytes_read); } int SkywatcherAltAzSimple::skywatcher_tty_write(int fd, const char *buffer, int nbytes, int *nbytes_written) { if (!RecoverAfterReconnection && !SerialPortName.empty() && !FileExists(SerialPortName)) { RecoverAfterReconnection = true; serialConnection->Disconnect(); serialConnection->Refresh(); std::this_thread::sleep_for(std::chrono::milliseconds(1000)); if (!serialConnection->Connect()) { RecoverAfterReconnection = true; std::this_thread::sleep_for(std::chrono::milliseconds(1000)); if (!serialConnection->Connect()) { RecoverAfterReconnection = false; return 0; } } SetSerialPort(serialConnection->getPortFD()); SerialPortName = serialConnection->port(); RecoverAfterReconnection = false; } return tty_write(fd, buffer, nbytes, nbytes_written); } void SkywatcherAltAzSimple::UpdateDetailedMountInformation(bool InformClient) { bool BasicMountInfoHasChanged = false; if (std::string(BasicMountInfo[MOTOR_CONTROL_FIRMWARE_VERSION].text) != std::to_string(MCVersion)) { IUSaveText(&BasicMountInfo[MOTOR_CONTROL_FIRMWARE_VERSION], std::to_string(MCVersion).c_str()); BasicMountInfoHasChanged = true; } if (std::string(BasicMountInfo[MOUNT_CODE].text) != std::to_string(MountCode)) { IUSaveText(&BasicMountInfo[MOUNT_CODE], std::to_string(MountCode).c_str()); BasicMountInfoHasChanged = true; } if (std::string(BasicMountInfo[IS_DC_MOTOR].text) != std::to_string(IsDCMotor)) { IUSaveText(&BasicMountInfo[IS_DC_MOTOR], std::to_string(IsDCMotor).c_str()); BasicMountInfoHasChanged = true; } if (BasicMountInfoHasChanged && InformClient) IDSetText(&BasicMountInfoV, nullptr); if (MountCode == 128) IUSaveText(&BasicMountInfo[MOUNT_NAME], "Merlin"); else if (MountCode >= 129 && MountCode <= 143) IUSaveText(&BasicMountInfo[MOUNT_NAME], "Az Goto"); else if (MountCode >= 144 && MountCode <= 159) IUSaveText(&BasicMountInfo[MOUNT_NAME], "Dob Goto"); else if (MountCode == 161) IUSaveText(&BasicMountInfo[MOUNT_NAME], "Virtuoso"); else if (MountCode >= 160) IUSaveText(&BasicMountInfo[MOUNT_NAME], "AllView Goto"); bool AxisOneInfoHasChanged = false; if (AxisOneInfo[MICROSTEPS_PER_REVOLUTION].value != MicrostepsPerRevolution[0]) { AxisOneInfo[MICROSTEPS_PER_REVOLUTION].value = MicrostepsPerRevolution[0]; AxisOneInfoHasChanged = true; } if (AxisOneInfo[STEPPER_CLOCK_FREQUENCY].value != StepperClockFrequency[0]) { AxisOneInfo[STEPPER_CLOCK_FREQUENCY].value = StepperClockFrequency[0]; AxisOneInfoHasChanged = true; } if (AxisOneInfo[HIGH_SPEED_RATIO].value != HighSpeedRatio[0]) { AxisOneInfo[HIGH_SPEED_RATIO].value = HighSpeedRatio[0]; AxisOneInfoHasChanged = true; } if (AxisOneInfo[MICROSTEPS_PER_WORM_REVOLUTION].value != MicrostepsPerWormRevolution[0]) { AxisOneInfo[MICROSTEPS_PER_WORM_REVOLUTION].value = MicrostepsPerWormRevolution[0]; AxisOneInfoHasChanged = true; } if (AxisOneInfoHasChanged && InformClient) IDSetNumber(&AxisOneInfoV, nullptr); bool AxisOneStateHasChanged = false; if (AxisOneState[FULL_STOP].s != (AxesStatus[0].FullStop ? ISS_ON : ISS_OFF)) { AxisOneState[FULL_STOP].s = AxesStatus[0].FullStop ? ISS_ON : ISS_OFF; AxisOneStateHasChanged = true; } if (AxisOneState[SLEWING].s != (AxesStatus[0].Slewing ? ISS_ON : ISS_OFF)) { AxisOneState[SLEWING].s = AxesStatus[0].Slewing ? ISS_ON : ISS_OFF; AxisOneStateHasChanged = true; } if (AxisOneState[SLEWING_TO].s != (AxesStatus[0].SlewingTo ? ISS_ON : ISS_OFF)) { AxisOneState[SLEWING_TO].s = AxesStatus[0].SlewingTo ? ISS_ON : ISS_OFF; AxisOneStateHasChanged = true; } if (AxisOneState[SLEWING_FORWARD].s != (AxesStatus[0].SlewingForward ? ISS_ON : ISS_OFF)) { AxisOneState[SLEWING_FORWARD].s = AxesStatus[0].SlewingForward ? ISS_ON : ISS_OFF; AxisOneStateHasChanged = true; } if (AxisOneState[HIGH_SPEED].s != (AxesStatus[0].HighSpeed ? ISS_ON : ISS_OFF)) { AxisOneState[HIGH_SPEED].s = AxesStatus[0].HighSpeed ? ISS_ON : ISS_OFF; AxisOneStateHasChanged = true; } if (AxisOneState[NOT_INITIALISED].s != (AxesStatus[0].NotInitialized ? ISS_ON : ISS_OFF)) { AxisOneState[NOT_INITIALISED].s = AxesStatus[0].NotInitialized ? ISS_ON : ISS_OFF; AxisOneStateHasChanged = true; } if (AxisOneStateHasChanged && InformClient) IDSetSwitch(&AxisOneStateV, nullptr); bool AxisTwoInfoHasChanged = false; if (AxisTwoInfo[MICROSTEPS_PER_REVOLUTION].value != MicrostepsPerRevolution[1]) { AxisTwoInfo[MICROSTEPS_PER_REVOLUTION].value = MicrostepsPerRevolution[1]; AxisTwoInfoHasChanged = true; } if (AxisTwoInfo[STEPPER_CLOCK_FREQUENCY].value != StepperClockFrequency[1]) { AxisTwoInfo[STEPPER_CLOCK_FREQUENCY].value = StepperClockFrequency[1]; AxisTwoInfoHasChanged = true; } if (AxisTwoInfo[HIGH_SPEED_RATIO].value != HighSpeedRatio[1]) { AxisTwoInfo[HIGH_SPEED_RATIO].value = HighSpeedRatio[1]; AxisTwoInfoHasChanged = true; } if (AxisTwoInfo[MICROSTEPS_PER_WORM_REVOLUTION].value != MicrostepsPerWormRevolution[1]) { AxisTwoInfo[MICROSTEPS_PER_WORM_REVOLUTION].value = MicrostepsPerWormRevolution[1]; AxisTwoInfoHasChanged = true; } if (AxisTwoInfoHasChanged && InformClient) IDSetNumber(&AxisTwoInfoV, nullptr); bool AxisTwoStateHasChanged = false; if (AxisTwoState[FULL_STOP].s != (AxesStatus[1].FullStop ? ISS_ON : ISS_OFF)) { AxisTwoState[FULL_STOP].s = AxesStatus[1].FullStop ? ISS_ON : ISS_OFF; AxisTwoStateHasChanged = true; } if (AxisTwoState[SLEWING].s != (AxesStatus[1].Slewing ? ISS_ON : ISS_OFF)) { AxisTwoState[SLEWING].s = AxesStatus[1].Slewing ? ISS_ON : ISS_OFF; AxisTwoStateHasChanged = true; } if (AxisTwoState[SLEWING_TO].s != (AxesStatus[1].SlewingTo ? ISS_ON : ISS_OFF)) { AxisTwoState[SLEWING_TO].s = AxesStatus[1].SlewingTo ? ISS_ON : ISS_OFF; AxisTwoStateHasChanged = true; } if (AxisTwoState[SLEWING_FORWARD].s != (AxesStatus[1].SlewingForward ? ISS_ON : ISS_OFF)) { AxisTwoState[SLEWING_FORWARD].s = AxesStatus[1].SlewingForward ? ISS_ON : ISS_OFF; AxisTwoStateHasChanged = true; } if (AxisTwoState[HIGH_SPEED].s != (AxesStatus[1].HighSpeed ? ISS_ON : ISS_OFF)) { AxisTwoState[HIGH_SPEED].s = AxesStatus[1].HighSpeed ? ISS_ON : ISS_OFF; AxisTwoStateHasChanged = true; } if (AxisTwoState[NOT_INITIALISED].s != (AxesStatus[1].NotInitialized ? ISS_ON : ISS_OFF)) { AxisTwoState[NOT_INITIALISED].s = AxesStatus[1].NotInitialized ? ISS_ON : ISS_OFF; AxisTwoStateHasChanged = true; } if (AxisTwoStateHasChanged && InformClient) IDSetSwitch(&AxisTwoStateV, nullptr); bool AxisOneEncoderValuesHasChanged = false; if ((AxisOneEncoderValues[RAW_MICROSTEPS].value != CurrentEncoders[AXIS1]) || (AxisOneEncoderValues[OFFSET_FROM_INITIAL].value != CurrentEncoders[AXIS1] - ZeroPositionEncoders[AXIS1])) { AxisOneEncoderValues[RAW_MICROSTEPS].value = CurrentEncoders[AXIS1]; AxisOneEncoderValues[MICROSTEPS_PER_ARCSEC].value = MicrostepsPerDegree[AXIS1] / 3600.0; AxisOneEncoderValues[OFFSET_FROM_INITIAL].value = CurrentEncoders[AXIS1] - ZeroPositionEncoders[AXIS1]; AxisOneEncoderValues[DEGREES_FROM_INITIAL].value = MicrostepsToDegrees(AXIS1, CurrentEncoders[AXIS1] - ZeroPositionEncoders[AXIS1]); AxisOneEncoderValuesHasChanged = true; } if (AxisOneEncoderValuesHasChanged && InformClient) IDSetNumber(&AxisOneEncoderValuesV, nullptr); bool AxisTwoEncoderValuesHasChanged = false; if ((AxisTwoEncoderValues[RAW_MICROSTEPS].value != CurrentEncoders[AXIS2]) || (AxisTwoEncoderValues[OFFSET_FROM_INITIAL].value != CurrentEncoders[AXIS2] - ZeroPositionEncoders[AXIS2])) { AxisTwoEncoderValues[RAW_MICROSTEPS].value = CurrentEncoders[AXIS2]; AxisTwoEncoderValues[MICROSTEPS_PER_ARCSEC].value = MicrostepsPerDegree[AXIS2] / 3600.0; AxisTwoEncoderValues[OFFSET_FROM_INITIAL].value = CurrentEncoders[AXIS2] - ZeroPositionEncoders[AXIS2]; AxisTwoEncoderValues[DEGREES_FROM_INITIAL].value = MicrostepsToDegrees(AXIS2, CurrentEncoders[AXIS2] - ZeroPositionEncoders[AXIS2]); AxisTwoEncoderValuesHasChanged = true; } if (AxisTwoEncoderValuesHasChanged && InformClient) IDSetNumber(&AxisTwoEncoderValuesV, nullptr); } ln_hrz_posn SkywatcherAltAzSimple::GetAltAzPosition(double ra, double dec, double offset_in_sec) { ln_lnlat_posn Location { 0, 0 }; ln_equ_posn Eq { 0, 0 }; ln_hrz_posn AltAz { 0, 0 }; double JulianOffset = offset_in_sec / (24.0*60*60); // Set the current location if (IUFindSwitch(&WedgeModeSP, "WEDGE_SIMPLE")->s == ISS_OFF && IUFindSwitch(&WedgeModeSP, "WEDGE_EQ")->s == ISS_OFF) { Location.lat = LocationN[LOCATION_LATITUDE].value; Location.lng = LocationN[LOCATION_LONGITUDE].value; } else { if (LocationN[LOCATION_LATITUDE].value > 0) { Location.lat = 90; Location.lng = 0; } else { Location.lat = -90; Location.lng = 0; } } Eq.ra = ra*360.0 / 24.0; Eq.dec = dec; ln_get_hrz_from_equ(&Eq, &Location, ln_get_julian_from_sys()+JulianOffset, &AltAz); AltAz.az -= 180; if (AltAz.az < 0) AltAz.az += 360; return AltAz; } ln_equ_posn SkywatcherAltAzSimple::GetRaDecPosition(double alt, double az) { ln_lnlat_posn Location { 0, 0 }; ln_equ_posn Eq { 0, 0 }; ln_hrz_posn AltAz { az, alt }; // Set the current location if (IUFindSwitch(&WedgeModeSP, "WEDGE_SIMPLE")->s == ISS_OFF && IUFindSwitch(&WedgeModeSP, "WEDGE_EQ")->s == ISS_OFF) { Location.lat = LocationN[LOCATION_LATITUDE].value; Location.lng = LocationN[LOCATION_LONGITUDE].value; } else { if (LocationN[LOCATION_LATITUDE].value > 0) { Location.lat = 90; Location.lng = 0; } else { Location.lat = -90; Location.lng = 0; } } AltAz.az -= 180; if (AltAz.az < 0) AltAz.az += 360; ln_get_equ_from_hrz(&AltAz, &Location, ln_get_julian_from_sys(), &Eq); Eq.ra = Eq.ra / 360.0 * 24.0; return Eq; } void SkywatcherAltAzSimple::LogMessage(const char* format, ...) { if (!format || IUFindSwitch(&TrackLogModeSP, "TRACKLOG_ENABLED")->s == ISS_OFF) return; va_list Ap; va_start(Ap, format); char TempStr[512]; std::ofstream LogFile; LogFile.open(TrackLogFileName.c_str(), std::ios::out | std::ios::app); if (!LogFile.is_open()) { return; } vsnprintf(TempStr, sizeof(TempStr), format, Ap); LogFile << GetLogTimestamp() << " | " << TempStr << "\n"; LogFile.close(); va_end(Ap); } libindi/drivers/telescope/lx200_OnStep.h0000664000175000017500000001175713263645557017454 0ustar jasemjasem/* LX200 OnStep based on LX200 Classic azwing (alain@zwingelstein.org) Contributors: James Lan https://github.com/james-lan Ray Wells https://github.com/blueshawk Copyright (C) 2003 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 */ #pragma once #include "lx200generic.h" #include "lx200driver.h" #include "indicom.h" #include #include #include #define setParkOnStep(fd) write(fd, "#:hQ#", 5) #define ReticPlus(fd) write(fd, "#:B+#", 5) #define ReticMoins(fd) write(fd, "#:B-#", 5) #define OnStepalign1(fd) write(fd, "#:A1#", 5) #define OnStepalign2(fd) write(fd, "#:A2#", 5) #define OnStepalign3(fd) write(fd, "#:A3#", 5) #define OnStepalignOK(fd) write(fd, "#:A+#", 5) enum Errors {ERR_NONE, ERR_MOTOR_FAULT, ERR_ALT, ERR_LIMIT_SENSE, ERR_DEC, ERR_AZM, ERR_UNDER_POLE, ERR_MERIDIAN, ERR_SYNC}; class LX200_OnStep : public LX200Generic { public: LX200_OnStep(); ~LX200_OnStep() {} virtual const char *getDefaultName() override; virtual bool initProperties() override; virtual void ISGetProperties(const char *dev) override; virtual bool updateProperties() override; virtual bool ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) override; virtual bool ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) override; protected: virtual void getBasicData() override; virtual bool Park() override; virtual bool UnPark() override; virtual bool SetCurrentPark() override; virtual bool SetDefaultPark() override; virtual bool SetTrackEnabled(bool enabled) override; virtual bool updateLocation(double latitude, double longitude, double elevation) override; virtual bool setLocalDate(uint8_t days, uint8_t months, uint16_t years) override; virtual bool ReadScopeStatus() override; virtual int setSiteLongitude(int fd, double Long); virtual bool GetAlignStatus(); virtual bool kdedialog(const char * commande); bool sendOnStepCommand(const char *cmd); bool sendOnStepCommandBlind(const char *cmd); int setMaxElevationLimit(int fd, int max); void OSUpdateFocuser(); ITextVectorProperty ObjectInfoTP; IText ObjectInfoT[1] {}; ISwitchVectorProperty StarCatalogSP; ISwitch StarCatalogS[3]; ISwitchVectorProperty DeepSkyCatalogSP; ISwitch DeepSkyCatalogS[7]; ISwitchVectorProperty SolarSP; ISwitch SolarS[10]; INumberVectorProperty ObjectNoNP; INumber ObjectNoN[1]; INumberVectorProperty MaxSlewRateNP; INumber MaxSlewRateN[2]; INumberVectorProperty BacklashNP; //test INumber BacklashN[2]; //Test INumberVectorProperty ElevationLimitNP; INumber ElevationLimitN[2]; ITextVectorProperty VersionTP; IText VersionT[5] {}; // OnStep Status controls ITextVectorProperty OnstepStatTP; IText OnstepStat[10] {}; // Focuser controls // Focuser 1 //ISwitchVectorProperty OSFocus1SelSP; //ISwitch OSFocus1SelS[2]; bool OSFocuser1=false; ISwitchVectorProperty OSFocus1RateSP; ISwitch OSFocus1RateS[4]; ISwitchVectorProperty OSFocus1MotionSP; ISwitch OSFocus1MotionS[3]; INumberVectorProperty OSFocus1TargNP; INumber OSFocus1TargN[1]; // Focuser 2 //ISwitchVectorProperty OSFocus2SelSP; //ISwitch OSFocus2SelS[2]; bool OSFocuser2=false; ISwitchVectorProperty OSFocus2RateSP; ISwitch OSFocus2RateS[4]; ISwitchVectorProperty OSFocus2MotionSP; ISwitch OSFocus2MotionS[3]; INumberVectorProperty OSFocus2TargNP; INumber OSFocus2TargN[1]; int IsTracking = 0; // Reticle +/- Buttons ISwitchVectorProperty ReticSP; ISwitch ReticS[2]; // Align Buttons ISwitchVectorProperty OSAlignSP; ISwitch OSAlignS[4]; IText OSAlignT[1] {}; ITextVectorProperty OSAlignTP; ISwitchVectorProperty TrackCompSP; ISwitch TrackCompS[3]; ISwitchVectorProperty SetHomeSP; ISwitch SetHomeS[2]; char OSStat[20]; char OldOSStat[20]; char OSAlignStat[10]; char oldOSAlignStat[10]; bool OSAlignProcess=false; bool OSAlignFlag=false; bool OSAlignOn=false; char OSPier[2]; char OldOSPier[2]; private: int currentCatalog; int currentSubCatalog; bool FirstRead=true; }; libindi/drivers/telescope/lx200ap_gtocp2.cpp0000664000175000017500000007175713263645557020324 0ustar jasemjasem/* Astro-Physics INDI driver Tailored for GTOCP2 Copyright (C) 2018 Jasem Mutlaq 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 */ #include "lx200ap_gtocp2.h" #include "indicom.h" #include "lx200driver.h" #include "lx200apdriver.h" #include #include #include #include #include /* Constructor */ LX200AstroPhysicsGTOCP2::LX200AstroPhysicsGTOCP2() : LX200Generic() { setLX200Capability(LX200_HAS_PULSE_GUIDING); SetTelescopeCapability(GetTelescopeCapability() | TELESCOPE_HAS_PIER_SIDE | TELESCOPE_HAS_PEC | TELESCOPE_CAN_CONTROL_TRACK | TELESCOPE_HAS_TRACK_RATE, 4); sendLocationOnStartup = false; sendTimeOnStartup = false; } const char *LX200AstroPhysicsGTOCP2::getDefaultName() { return (const char *)"AstroPhysics GTOCP2"; } bool LX200AstroPhysicsGTOCP2::initProperties() { LX200Generic::initProperties(); timeFormat = LX200_24; IUFillNumber(&HourangleCoordsN[0], "HA", "HA H:M:S", "%10.6m", 0., 24., 0., 0.); IUFillNumber(&HourangleCoordsN[1], "DEC", "Dec D:M:S", "%10.6m", -90.0, 90.0, 0., 0.); IUFillNumberVector(&HourangleCoordsNP, HourangleCoordsN, 2, getDeviceName(), "HOURANGLE_COORD", "Hourangle Coords", MAIN_CONTROL_TAB, IP_RO, 0, IPS_IDLE); IUFillNumber(&HorizontalCoordsN[0], "AZ", "Az D:M:S", "%10.6m", 0., 360., 0., 0.); IUFillNumber(&HorizontalCoordsN[1], "ALT", "Alt D:M:S", "%10.6m", -90., 90., 0., 0.); IUFillNumberVector(&HorizontalCoordsNP, HorizontalCoordsN, 2, getDeviceName(), "HORIZONTAL_COORD", "Horizontal Coords", MAIN_CONTROL_TAB, IP_RW, 120, IPS_IDLE); // Max rate is 999.99999X for the GTOCP4. // Using :RR998.9999# just to be safe. 15.041067*998.99999 = 15026.02578 TrackRateN[AXIS_RA].min = -15026.0258; TrackRateN[AXIS_RA].max = 15026.0258; TrackRateN[AXIS_DE].min = -998.9999; TrackRateN[AXIS_DE].max = 998.9999; // Motion speed of axis when pressing NSWE buttons IUFillSwitch(&SlewRateS[0], "12", "12x", ISS_OFF); IUFillSwitch(&SlewRateS[1], "64", "64x", ISS_ON); IUFillSwitch(&SlewRateS[2], "600", "600x", ISS_OFF); IUFillSwitch(&SlewRateS[3], "1200", "1200x", ISS_OFF); IUFillSwitchVector(&SlewRateSP, SlewRateS, 4, getDeviceName(), "TELESCOPE_SLEW_RATE", "Slew Rate", MOTION_TAB, IP_RW, ISR_1OFMANY, 0, IPS_IDLE); // Slew speed when performing regular GOTO IUFillSwitch(&APSlewSpeedS[0], "600", "600x", ISS_ON); IUFillSwitch(&APSlewSpeedS[1], "900", "900x", ISS_OFF); IUFillSwitch(&APSlewSpeedS[2], "1200", "1200x", ISS_OFF); IUFillSwitchVector(&APSlewSpeedSP, APSlewSpeedS, 3, getDeviceName(), "GOTO Rate", "", MOTION_TAB, IP_RW, ISR_1OFMANY, 0, IPS_IDLE); IUFillSwitch(&SwapS[0], "NS", "North/South", ISS_OFF); IUFillSwitch(&SwapS[1], "EW", "East/West", ISS_OFF); IUFillSwitchVector(&SwapSP, SwapS, 2, getDeviceName(), "SWAP", "Swap buttons", MOTION_TAB, IP_RW, ISR_1OFMANY, 0, IPS_IDLE); IUFillSwitch(&SyncCMRS[USE_REGULAR_SYNC], ":CM#", ":CM#", ISS_ON); IUFillSwitch(&SyncCMRS[USE_CMR_SYNC], ":CMR#", ":CMR#", ISS_OFF); IUFillSwitchVector(&SyncCMRSP, SyncCMRS, 2, getDeviceName(), "SYNCCMR", "Sync", MOTION_TAB, IP_RW, ISR_1OFMANY, 0, IPS_IDLE); // guide speed IUFillSwitch(&APGuideSpeedS[0], "0.25", "0.25x", ISS_OFF); IUFillSwitch(&APGuideSpeedS[1], "0.5", "0.50x", ISS_ON); IUFillSwitch(&APGuideSpeedS[2], "1.0", "1.0x", ISS_OFF); IUFillSwitchVector(&APGuideSpeedSP, APGuideSpeedS, 3, getDeviceName(), "Guide Rate", "", GUIDE_TAB, IP_RW, ISR_1OFMANY, 0, IPS_IDLE); IUFillText(&VersionT[0], "Version", "Version", ""); IUFillTextVector(&VersionInfo, VersionT, 1, getDeviceName(), "Firmware", "Firmware", MAIN_CONTROL_TAB, IP_RO, 0, IPS_IDLE); SetParkDataType(PARK_AZ_ALT); return true; } void LX200AstroPhysicsGTOCP2::ISGetProperties(const char *dev) { LX200Generic::ISGetProperties(dev); if (isConnected()) { defineText(&VersionInfo); /* Motion group */ defineSwitch(&APSlewSpeedSP); defineSwitch(&SwapSP); defineSwitch(&SyncCMRSP); defineSwitch(&APGuideSpeedSP); } } bool LX200AstroPhysicsGTOCP2::updateProperties() { LX200Generic::updateProperties(); if (isConnected()) { defineText(&VersionInfo); /* Motion group */ defineSwitch(&APSlewSpeedSP); defineSwitch(&SwapSP); defineSwitch(&SyncCMRSP); defineSwitch(&APGuideSpeedSP); if (InitPark()) { // If loading parking data is successful, we just set the default parking values. SetAxis1ParkDefault(LocationN[LOCATION_LATITUDE].value >= 0 ? 0 : 180); SetAxis2ParkDefault(LocationN[LOCATION_LATITUDE].value); } else { // Otherwise, we set all parking data to default in case no parking data is found. SetAxis1Park(LocationN[LOCATION_LATITUDE].value >= 0 ? 0 : 180); SetAxis1ParkDefault(LocationN[LOCATION_LATITUDE].value); SetAxis1ParkDefault(LocationN[LOCATION_LATITUDE].value >= 0 ? 0 : 180); SetAxis2ParkDefault(LocationN[LOCATION_LATITUDE].value); } double longitude=-1000, latitude=-1000; // Get value from config file if it exists. IUGetConfigNumber(getDeviceName(), "GEOGRAPHIC_COORD", "LONG", &longitude); IUGetConfigNumber(getDeviceName(), "GEOGRAPHIC_COORD", "LAT", &latitude); if (longitude != -1000 && latitude != -1000) updateLocation(latitude, longitude, 0); } else { deleteProperty(VersionInfo.name); deleteProperty(APSlewSpeedSP.name); deleteProperty(SwapSP.name); deleteProperty(SyncCMRSP.name); deleteProperty(APGuideSpeedSP.name); } return true; } bool LX200AstroPhysicsGTOCP2::initMount() { // Make sure that the mount is setup according to the properties int err=0; bool raOK=false, deOK=false; if (isSimulation()) { raOK = deOK = true; } else { raOK = (getLX200RA(PortFD, ¤tRA) == 0); deOK = (getLX200DEC(PortFD, ¤tDEC) == 0); } // If we either failed to get coords; OR // RA and DE are zero, then mount is not initialized and we need to initialized it. if ( (raOK == false && deOK == false) || (currentRA == 0 && (currentDEC == 0 || currentDEC == 90))) { LOG_DEBUG("Mount is not yet initialized. Initializing it..."); if (isSimulation() == false) { // This is how to init the mount in case RA/DE are missing. // :PO# if (setAPUnPark(PortFD) < 0) { LOG_ERROR("UnParking Failed."); return false; } // Stop :Q# abortSlew(PortFD); } } mountInitialized = true; LOG_DEBUG("Mount is initialized."); // Astrophysics mount is always unparked on startup // In this driver, unpark only sets the tracking ON. // setAPUnPark() is NOT called as this function, despite its name, is only used for initialization purposes. UnPark(); // On most mounts SlewRateS defines the MoveTo AND Slew (GOTO) speeds // lx200ap is different - some of the MoveTo speeds are not VALID // Slew speeds so we have to keep two lists. // // SlewRateS is used as the MoveTo speed if (isSimulation() == false && (err = selectAPMoveToRate(PortFD, IUFindOnSwitchIndex(&SlewRateSP))) < 0) { LOGF_ERROR("Error setting move rate (%d).", err); return false; } SlewRateSP.s = IPS_OK; IDSetSwitch(&SlewRateSP, nullptr); // APSlewSpeedsS defines the Slew (GOTO) speeds valid on the AP mounts if (isSimulation() == false && (err = selectAPSlewRate(PortFD, IUFindOnSwitchIndex(&APSlewSpeedSP))) < 0) { LOGF_ERROR("Error setting slew to rate (%d).", err); return false; } APSlewSpeedSP.s = IPS_OK; IDSetSwitch(&APSlewSpeedSP, nullptr); char versionString[128]; if (isSimulation()) strncpy(versionString, "VCP4-P01-01", 128); else getAPVersionNumber(PortFD, versionString); VersionInfo.s = IPS_OK; IUSaveText(&VersionT[0], versionString); IDSetText(&VersionInfo, nullptr); if (strlen(versionString) != 1) { LOGF_ERROR("Version not supported GTOCP2 driver: %s", versionString); return false; } int typeIndex = VersionT[0].text[0] - 'E'; if (typeIndex >= 0) { firmwareVersion = static_cast(typeIndex); LOGF_DEBUG("Firmware version index: %d", typeIndex); LOGF_INFO("Firmware Version: %c", VersionT[0].text[0]); } else { LOGF_ERROR("Invalid version: %s", versionString); return false; } return true; } bool LX200AstroPhysicsGTOCP2::ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) { int err = 0; // ignore if not ours // if (strcmp(getDeviceName(), dev)) return false; // ======================================= // Swap Buttons // ======================================= if (!strcmp(name, SwapSP.name)) { int currentSwap; IUResetSwitch(&SwapSP); IUUpdateSwitch(&SwapSP, states, names, n); currentSwap = IUFindOnSwitchIndex(&SwapSP); if ((!isSimulation() && (err = swapAPButtons(PortFD, currentSwap)) < 0)) { LOGF_ERROR("Error swapping buttons (%d).", err); return false; } SwapS[0].s = ISS_OFF; SwapS[1].s = ISS_OFF; SwapSP.s = IPS_OK; IDSetSwitch(&SwapSP, nullptr); return true; } // =========================================================== // GOTO ("slew") Speed. // =========================================================== if (!strcmp(name, APSlewSpeedSP.name)) { IUUpdateSwitch(&APSlewSpeedSP, states, names, n); int slewRate = IUFindOnSwitchIndex(&APSlewSpeedSP); if (!isSimulation() && (err = selectAPSlewRate(PortFD, slewRate) < 0)) { LOGF_ERROR("Error setting move to rate (%d).", err); return false; } APSlewSpeedSP.s = IPS_OK; IDSetSwitch(&APSlewSpeedSP, nullptr); return true; } // =========================================================== // Guide Speed. // =========================================================== if (!strcmp(name, APGuideSpeedSP.name)) { IUUpdateSwitch(&APGuideSpeedSP, states, names, n); int guideRate = IUFindOnSwitchIndex(&APGuideSpeedSP); if (!isSimulation() && (err = selectAPGuideRate(PortFD, guideRate) < 0)) { LOGF_ERROR("Error setting guiding to rate (%d).", err); return false; } APGuideSpeedSP.s = IPS_OK; IDSetSwitch(&APGuideSpeedSP, nullptr); return true; } // ======================================= // Choose the appropriate sync command // ======================================= if (!strcmp(name, SyncCMRSP.name)) { IUResetSwitch(&SyncCMRSP); IUUpdateSwitch(&SyncCMRSP, states, names, n); IUFindOnSwitchIndex(&SyncCMRSP); SyncCMRSP.s = IPS_OK; IDSetSwitch(&SyncCMRSP, nullptr); return true; } // ======================================= // Choose the PEC playback mode // ======================================= if (!strcmp(name, PECStateSP.name)) { IUResetSwitch(&PECStateSP); IUUpdateSwitch(&PECStateSP, states, names, n); IUFindOnSwitchIndex(&PECStateSP); int pecstate = IUFindOnSwitchIndex(&PECStateSP); if (!isSimulation() && (err = selectAPPECState(PortFD, pecstate) < 0)) { LOGF_ERROR("Error setting PEC state (%d).", err); return false; } PECStateSP.s = IPS_OK; IDSetSwitch(&PECStateSP, nullptr); return true; } return LX200Generic::ISNewSwitch(dev, name, states, names, n); } bool LX200AstroPhysicsGTOCP2::ReadScopeStatus() { if (isSimulation()) { mountSim(); return true; } if (getLX200RA(PortFD, ¤tRA) < 0 || getLX200DEC(PortFD, ¤tDEC) < 0) { EqNP.s = IPS_ALERT; IDSetNumber(&EqNP, "Error reading RA/DEC."); return false; } if (TrackState == SCOPE_SLEWING) { double dx = lastRA - currentRA; double dy = lastDE - currentDEC; LOGF_DEBUG("Slewing... currentRA: %g dx: %g currentDE: %g dy: %g", currentRA, dx, currentDEC, dy); // Wait until acknowledged if (dx == 0 && dy == 0) { TrackState = SCOPE_TRACKING; LOG_INFO("Slew is complete. Tracking..."); } // Keep try of last values to determine if the mount settled. lastRA = currentRA; lastDE = currentDEC; } else if (TrackState == SCOPE_PARKING) { if (getLX200Az(PortFD, ¤tAz) < 0 || getLX200Alt(PortFD, ¤tAlt) < 0) { EqNP.s = IPS_ALERT; IDSetNumber(&EqNP, "Error reading Az/Alt."); return false; } double dx = lastAZ - currentAz; double dy = lastAL - currentAlt; LOGF_DEBUG("Parking... currentAz: %g dx: %g currentAlt: %g dy: %g", currentAz, dx, currentAlt, dy); if (dx == 0 && dy == 0) { LOG_DEBUG("Parking slew is complete. Asking astrophysics mount to park..."); if (!isSimulation() && setAPPark(PortFD) < 0) { LOG_ERROR("Parking Failed."); return false; } // Turn off tracking. SetTrackEnabled(false); SetParked(true); LOG_INFO("Please disconnect and power off the mount."); } lastAZ = currentAz; lastAL = currentAlt; } NewRaDec(currentRA, currentDEC); syncSideOfPier(); return true; } bool LX200AstroPhysicsGTOCP2::Goto(double r, double d) { const struct timespec timeout = {0, 100000000L}; targetRA = r; targetDEC = d; char RAStr[64], DecStr[64]; fs_sexa(RAStr, targetRA, 2, 3600); fs_sexa(DecStr, targetDEC, 2, 3600); // If moving, let's stop it first. if (EqNP.s == IPS_BUSY) { if (!isSimulation() && abortSlew(PortFD) < 0) { AbortSP.s = IPS_ALERT; IDSetSwitch(&AbortSP, "Abort slew failed."); return false; } AbortSP.s = IPS_OK; EqNP.s = IPS_IDLE; IDSetSwitch(&AbortSP, "Slew aborted."); IDSetNumber(&EqNP, nullptr); if (MovementNSSP.s == IPS_BUSY || MovementWESP.s == IPS_BUSY) { MovementNSSP.s = MovementWESP.s = IPS_IDLE; EqNP.s = IPS_IDLE; IUResetSwitch(&MovementNSSP); IUResetSwitch(&MovementWESP); IDSetSwitch(&MovementNSSP, nullptr); IDSetSwitch(&MovementWESP, nullptr); } // sleep for 100 mseconds nanosleep(&timeout, NULL); } if (!isSimulation()) { if (setAPObjectRA(PortFD, targetRA) < 0 || (setAPObjectDEC(PortFD, targetDEC)) < 0) { EqNP.s = IPS_ALERT; IDSetNumber(&EqNP, "Error setting RA/DEC."); return false; } int err = 0; /* Slew reads the '0', that is not the end of the slew */ if ((err = Slew(PortFD))) { EqNP.s = IPS_ALERT; IDSetNumber(&EqNP, "Error Slewing to JNow RA %s - DEC %s\n", RAStr, DecStr); slewError(err); return false; } motionCommanded = true; lastRA = targetRA; lastDE = targetDEC; } TrackState = SCOPE_SLEWING; EqNP.s = IPS_BUSY; LOGF_INFO("Slewing to RA: %s - DEC: %s", RAStr, DecStr); return true; } int LX200AstroPhysicsGTOCP2::SendPulseCmd(int direction, int duration_msec) { if (firmwareVersion == MCV_E) handleGTOCP2MotionBug(); return APSendPulseCmd(PortFD, direction, duration_msec); } bool LX200AstroPhysicsGTOCP2::Handshake() { if (isSimulation()) { LOG_INFO("Simulated Astrophysics is online. Retrieving basic data..."); return true; } int err=0; if ((err = setAPClearBuffer(PortFD)) < 0) { LOGF_ERROR("Error clearing the buffer (%d): %s", err, strerror(err)); return false; } if ((err = setAPBackLashCompensation(PortFD, 0, 0, 0)) < 0) { // It seems we need to send it twice before it works! if ((err = setAPBackLashCompensation(PortFD, 0, 0, 0)) < 0) { LOGF_ERROR("Error setting back lash compensation (%d): %s.", err, strerror(err)); return false; } } // Detect and set fomat. It should be LONG. return (checkLX200Format(PortFD) == 0); } bool LX200AstroPhysicsGTOCP2::Disconnect() { timeUpdated = false; //locationUpdated = false; mountInitialized = false; return LX200Generic::Disconnect(); } bool LX200AstroPhysicsGTOCP2::Sync(double ra, double dec) { char syncString[256]; int syncType = IUFindOnSwitchIndex(&SyncCMRSP); if (!isSimulation()) { if (setAPObjectRA(PortFD, ra) < 0 || setAPObjectDEC(PortFD, dec) < 0) { EqNP.s = IPS_ALERT; IDSetNumber(&EqNP, "Error setting RA/DEC. Unable to Sync."); return false; } bool syncOK = true; switch (syncType) { case USE_REGULAR_SYNC: if (::Sync(PortFD, syncString) < 0) syncOK = false; break; case USE_CMR_SYNC: if (APSyncCMR(PortFD, syncString) < 0) syncOK = false; break; default: break; } if (syncOK == false) { EqNP.s = IPS_ALERT; IDSetNumber(&EqNP, "Synchronization failed."); return false; } } currentRA = ra; currentDEC = dec; LOGF_DEBUG("%s Synchronization successful %s", (syncType == USE_REGULAR_SYNC ? "CM" : "CMR"), syncString); LOG_INFO("Synchronization successful."); EqNP.s = IPS_OK; NewRaDec(currentRA, currentDEC); return true; } bool LX200AstroPhysicsGTOCP2::updateTime(ln_date *utc, double utc_offset) { struct ln_zonedate ltm; ln_date_to_zonedate(utc, <m, utc_offset * 3600.0); JD = ln_get_julian_day(utc); LOGF_DEBUG("New JD is %.2f", JD); // Set Local Time if (isSimulation() == false && setLocalTime(PortFD, ltm.hours, ltm.minutes, (int)ltm.seconds) < 0) { LOG_ERROR("Error setting local time."); return false; } LOGF_DEBUG("Set Local Time %02d:%02d:%02d is successful.", ltm.hours, ltm.minutes, (int)ltm.seconds); if (isSimulation() == false && setCalenderDate(PortFD, ltm.days, ltm.months, ltm.years) < 0) { LOG_ERROR("Error setting local date."); return false; } LOGF_DEBUG("Set Local Date %02d/%02d/%02d is successful.", ltm.days, ltm.months, ltm.years); if (isSimulation() == false && setAPUTCOffset(PortFD, fabs(utc_offset)) < 0) { LOG_ERROR("Error setting UTC Offset."); return false; } LOGF_DEBUG("Set UTC Offset %g (always positive for AP) is successful.", fabs(utc_offset)); LOG_INFO("Time updated."); timeUpdated = true; if (locationUpdated && timeUpdated && mountInitialized == false) initMount(); return true; } bool LX200AstroPhysicsGTOCP2::updateLocation(double latitude, double longitude, double elevation) { INDI_UNUSED(elevation); if (!isSimulation() && setAPSiteLongitude(PortFD, 360.0 - longitude) < 0) { LOG_ERROR("Error setting site longitude coordinates"); return false; } if (!isSimulation() && setAPSiteLatitude(PortFD, latitude) < 0) { LOG_ERROR("Error setting site latitude coordinates"); return false; } char l[32], L[32]; fs_sexa(l, latitude, 3, 3600); fs_sexa(L, longitude, 4, 3600); LOGF_INFO("Site location updated to Lat %.32s - Long %.32s", l, L); locationUpdated = true; if (locationUpdated && timeUpdated && mountInitialized == false) initMount(); return true; } void LX200AstroPhysicsGTOCP2::debugTriggered(bool enable) { LX200Generic::debugTriggered(enable); set_lx200ap_name(getDeviceName(), DBG_SCOPE); } // For most mounts the SetSlewRate() method sets both the MoveTo and Slew (GOTO) speeds. // For AP mounts these two speeds are handled separately - so SetSlewRate() actually sets the MoveTo speed for AP mounts - confusing! // ApSetSlew bool LX200AstroPhysicsGTOCP2::SetSlewRate(int index) { if (!isSimulation() && selectAPMoveToRate(PortFD, index) < 0) { SlewRateSP.s = IPS_ALERT; IDSetSwitch(&SlewRateSP, "Error setting slew mode."); return false; } SlewRateSP.s = IPS_OK; IDSetSwitch(&SlewRateSP, nullptr); return true; } bool LX200AstroPhysicsGTOCP2::Park() { double parkAz = GetAxis1Park(); double parkAlt = GetAxis2Park(); char AzStr[16], AltStr[16]; fs_sexa(AzStr, parkAz, 2, 3600); fs_sexa(AltStr, parkAlt, 2, 3600); LOGF_DEBUG("Parking to Az (%s) Alt (%s)...", AzStr, AltStr); if (isSimulation()) { ln_lnlat_posn observer; observer.lat = LocationN[LOCATION_LATITUDE].value; observer.lng = LocationN[LOCATION_LONGITUDE].value; if (observer.lng > 180) observer.lng -= 360; ln_hrz_posn horizontalPos; // Libnova south = 0, west = 90, north = 180, east = 270 horizontalPos.az = parkAz + 180; if (horizontalPos.az > 360) horizontalPos.az -= 360; horizontalPos.alt = parkAlt; ln_equ_posn equatorialPos; ln_get_equ_from_hrz(&horizontalPos, &observer, ln_get_julian_from_sys(), &equatorialPos); Goto(equatorialPos.ra / 15.0, equatorialPos.dec); } else { if (setAPObjectAZ(PortFD, parkAz) < 0 || setAPObjectAlt(PortFD, parkAlt) < 0) { LOG_ERROR("Error setting Az/Alt."); return false; } int err = 0; /* Slew reads the '0', that is not the end of the slew */ if ((err = Slew(PortFD))) { LOGF_ERROR("Error Slewing to Az %s - Alt %s", AzStr, AltStr); slewError(err); return false; } motionCommanded = true; lastAZ = parkAz; lastAL = parkAlt; } EqNP.s = IPS_BUSY; TrackState = SCOPE_PARKING; LOG_INFO("Parking is in progress..."); return true; } bool LX200AstroPhysicsGTOCP2::UnPark() { // The AP :PO# should only be used during initilization and not here as indicated by email from Preston on 2017-12-12 // Enable tracking SetTrackEnabled(true); SetParked(false); return true; } bool LX200AstroPhysicsGTOCP2::SetCurrentPark() { ln_hrz_posn horizontalPos; // Libnova south = 0, west = 90, north = 180, east = 270 ln_lnlat_posn observer; observer.lat = LocationN[LOCATION_LATITUDE].value; observer.lng = LocationN[LOCATION_LONGITUDE].value; if (observer.lng > 180) observer.lng -= 360; ln_equ_posn equatorialPos; equatorialPos.ra = currentRA * 15; equatorialPos.dec = currentDEC; ln_get_hrz_from_equ(&equatorialPos, &observer, ln_get_julian_from_sys(), &horizontalPos); double parkAZ = horizontalPos.az - 180; if (parkAZ < 0) parkAZ += 360; double parkAlt = horizontalPos.alt; char AzStr[16], AltStr[16]; fs_sexa(AzStr, parkAZ, 2, 3600); fs_sexa(AltStr, parkAlt, 2, 3600); LOGF_DEBUG("Setting current parking position to coordinates Az (%s) Alt (%s)", AzStr, AltStr); SetAxis1Park(parkAZ); SetAxis2Park(parkAlt); return true; } bool LX200AstroPhysicsGTOCP2::SetDefaultPark() { // Az = 0 for North hemisphere SetAxis1Park(LocationN[LOCATION_LATITUDE].value > 0 ? 0 : 180); // Alt = Latitude SetAxis2Park(LocationN[LOCATION_LATITUDE].value); return true; } void LX200AstroPhysicsGTOCP2::syncSideOfPier() { const char *cmd = ":pS#"; // Response char response[16] = { 0 }; int rc = 0, nbytes_read = 0, nbytes_written = 0; LOGF_DEBUG("CMD: <%s>", cmd); tcflush(PortFD, TCIOFLUSH); if ((rc = tty_write(PortFD, cmd, strlen(cmd), &nbytes_written)) != TTY_OK) { char errmsg[256]; tty_error_msg(rc, errmsg, 256); LOGF_ERROR("Error writing to device %s (%d)", errmsg, rc); return; } // Read Side if ((rc = tty_read_section(PortFD, response, '#', 3, &nbytes_read)) != TTY_OK) { char errmsg[256]; tty_error_msg(rc, errmsg, 256); LOGF_ERROR("Error reading from device %s (%d)", errmsg, rc); return; } response[nbytes_read - 1] = '\0'; tcflush(PortFD, TCIOFLUSH); LOGF_DEBUG("RES: <%s>", response); if (!strcmp(response, "East")) setPierSide(INDI::Telescope::PIER_EAST); else if (!strcmp(response, "West")) setPierSide(INDI::Telescope::PIER_WEST); else LOGF_ERROR("Invalid pier side response from device-> %s", response); } bool LX200AstroPhysicsGTOCP2::saveConfigItems(FILE *fp) { LX200Generic::saveConfigItems(fp); IUSaveConfigSwitch(fp, &SyncCMRSP); IUSaveConfigSwitch(fp, &APSlewSpeedSP); IUSaveConfigSwitch(fp, &APGuideSpeedSP); return true; } bool LX200AstroPhysicsGTOCP2::SetTrackMode(uint8_t mode) { int err=0; if (mode == TRACK_CUSTOM) { if (!isSimulation() && (err = selectAPTrackingMode(PortFD, AP_TRACKING_SIDEREAL)) < 0) { LOGF_ERROR("Error setting tracking mode (%d).", err); return false; } return SetTrackRate(TrackRateN[AXIS_RA].value, TrackRateN[AXIS_DE].value); } if (!isSimulation() && (err = selectAPTrackingMode(PortFD, mode)) < 0) { LOGF_ERROR("Error setting tracking mode (%d).", err); return false; } return true; } bool LX200AstroPhysicsGTOCP2::SetTrackEnabled(bool enabled) { return SetTrackMode(enabled ? IUFindOnSwitchIndex(&TrackModeSP) : AP_TRACKING_OFF); } bool LX200AstroPhysicsGTOCP2::SetTrackRate(double raRate, double deRate) { // Convert to arcsecs/s to AP sidereal multiplier /* :RR0.0000# = normal sidereal tracking in RA - similar to :RT2# :RR+1.0000# = 1 + normal sidereal = 2X sidereal :RR+9.0000# = 9 + normal sidereal = 10X sidereal :RR-1.0000# = normal sidereal - 1 = 0 or Stop - similar to :RT9# :RR-11.0000# = normal sidereal - 11 = -10X sidereal (East at 10X) :RD0.0000# = normal zero rate for Dec. :RD5.0000# = 5 + normal zero rate = 5X sidereal clockwise from above - equivalent to South :RD-5.0000# = normal zero rate - 5 = 5X sidereal counter-clockwise from above - equivalent to North */ double APRARate = (raRate - TRACKRATE_SIDEREAL) / TRACKRATE_SIDEREAL; double APDERate = deRate / TRACKRATE_SIDEREAL; if (!isSimulation()) { if (setAPRATrackRate(PortFD, APRARate) < 0 || setAPDETrackRate(PortFD, APDERate) < 0) return false; } return true; } bool LX200AstroPhysicsGTOCP2::getUTFOffset(double *offset) { return (getAPUTCOffset(PortFD, offset) == 0); } bool LX200AstroPhysicsGTOCP2::MoveNS(INDI_DIR_NS dir, TelescopeMotionCommand command) { bool rc = LX200Generic::MoveNS(dir, command); if (command == MOTION_START) motionCommanded = true; return rc; } bool LX200AstroPhysicsGTOCP2::MoveWE(INDI_DIR_WE dir, TelescopeMotionCommand command) { bool rc = LX200Generic::MoveWE(dir, command); if (command == MOTION_START) motionCommanded = true; return rc; } void LX200AstroPhysicsGTOCP2::handleGTOCP2MotionBug() { LOGF_DEBUG("%s: Motion commanded? %s", __FUNCTION__, motionCommanded ? "True":"False"); // GTOCP2 (Version 'E' and earilar) has a bug that would reset the guide rate to whatever last motion took place // So it must be reset to the user setting in order for guiding to work properly. if (motionCommanded) { LOGF_DEBUG("%s: Issuing select guide rate index: %d", __FUNCTION__, IUFindOnSwitchIndex(&APGuideSpeedSP)); selectAPGuideRate(PortFD, IUFindOnSwitchIndex(&APGuideSpeedSP)); motionCommanded = false; } } libindi/drivers/telescope/lx200_10micron.h0000664000175000017500000001272413263645557017667 0ustar jasemjasem/* 10micron INDI driver Copyright (C) 2017 Hans Lambermont 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 */ #pragma once #include "lx200generic.h" class LX200_10MICRON : public LX200Generic { public: enum LX200_10MICRON_PRODUCT_INFO { PRODUCT_NAME, PRODUCT_CONTROL_BOX, PRODUCT_FIRMWARE_VERSION, PRODUCT_FIRMWARE_DATE, PRODUCT_COUNT }; enum LX200_10MICRON_10MICRON_GSTAT { GSTAT_UNSET = -999, GSTAT_TRACKING = 0, GSTAT_STOPPED = 1, GSTAT_PARKING = 2, GSTAT_UNPARKING = 3, GSTAT_SLEWING_TO_HOME = 4, GSTAT_PARKED = 5, GSTAT_SLEWING_OR_STOPPING = 6, GSTAT_NOT_TRACKING_AND_NOT_MOVING = 7, GSTAT_MOTORS_TOO_COLD = 8, GSTAT_TRACKING_OUTSIDE_LIMITS = 9, GSTAT_FOLLOWING_SATELLITE = 10, GSTAT_NEED_USEROK = 11, GSTAT_UNKNOWN_STATUS = 98, GSTAT_ERROR = 99 }; enum LX200_10MICRON_ALIGNMENT_POINT { ALP_MRA, // Mount Right Ascension ALP_MDEC, // Mount Declination ALP_MSIDE, // Mount Pier Side ALP_SIDTIME, // Sidereal Time ALP_PRA, // Plate solved Right Ascension ALP_PDEC, // Plate solved Declination ALP_COUNT }; enum LX200_10MICRON_MINI_ALIGNMENT_POINT_RO { MALPRO_MRA, // Mount Right Ascension MALPRO_MDEC, // Mount Declination MALPRO_MSIDE, // Mount Pier Side MALPRO_SIDTIME, // Sidereal Time MALPRO_COUNT }; enum LX200_10MICRON_MINI_ALIGNMENT_POINT { MALP_PRA, // Plate solved Right Ascension MALP_PDEC, // Plate solved Declination MALP_COUNT }; enum LX200_10MICRON_ALIGNMENT_STATE { ALIGN_IDLE, ALIGN_START, ALIGN_END, ALIGN_DELETE_CURRENT, ALIGN_COUNT }; LX200_10MICRON(); ~LX200_10MICRON() {} bool ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) override; bool ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) override; bool ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n) override; const char *getDefaultName() override; bool Handshake() override; bool initProperties() override; bool updateProperties() override; bool ReadScopeStatus() override; bool Park() override; bool UnPark() override; bool SyncConfigBehaviour(bool cmcfg); bool setLocalDate(uint8_t days, uint8_t months, uint16_t years) override; int AddSyncPoint(double MRa, double MDec, double MSide, double PRa, double PDec, double SidTime); int AddSyncPointHere(double PRa, double PDec); // TODO move these things elsewhere int monthToNumber(const char *monthName); int setStandardProcedureWithoutRead(int fd, const char *data); int setStandardProcedureAndExpect(int fd, const char *data, const char *expect); int setStandardProcedureAndReturnResponse(int fd, const char *data, char *response, int max_response_length); protected: void getBasicData() override; IText ProductT[4] {}; ITextVectorProperty ProductTP; virtual int SetRefractionModelTemperature(double temperature); INumber RefractionModelTemperatureN[1]; INumberVectorProperty RefractionModelTemperatureNP; virtual int SetRefractionModelPressure(double pressure); INumber RefractionModelPressureN[1]; INumberVectorProperty RefractionModelPressureNP; INumber ModelCountN[1]; INumberVectorProperty ModelCountNP; INumber AlignmentPointsN[1]; INumberVectorProperty AlignmentPointsNP; ISwitch AlignmentStateS[ALIGN_COUNT]; ISwitchVectorProperty AlignmentSP; INumber MiniNewAlpRON[MALPRO_COUNT]; INumberVectorProperty MiniNewAlpRONP; INumber MiniNewAlpN[MALP_COUNT]; INumberVectorProperty MiniNewAlpNP; INumber NewAlpN[ALP_COUNT]; INumberVectorProperty NewAlpNP; INumber NewAlignmentPointsN[1]; INumberVectorProperty NewAlignmentPointsNP; IText NewModelNameT[1] {}; ITextVectorProperty NewModelNameTP; private: int fd = -1; // short notation for PortFD/sockfd bool getMountInfo(); int OldGstat = GSTAT_UNSET; struct _Ginfo { float RA_JNOW = 0.0; float DEC_JNOW = 0.0; char SideOfPier = 'x'; float AZ = 0.0; float ALT = 0.0; float Jdate = 0.0; int Gstat = -1; int SlewStatus = -1; // added : double SiderealTime = -1; } Ginfo; int AlignmentState = ALIGN_IDLE; }; libindi/drivers/telescope/lx200autostar.cpp0000664000175000017500000001370313263645557020273 0ustar jasemjasem/* LX200 Autostar Copyright (C) 2003 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 */ #include "lx200autostar.h" #include "lx200driver.h" #include #define FIRMWARE_TAB "Firmware data" /******************************************** Property: Park telescope to HOME *********************************************/ LX200Autostar::LX200Autostar() : LX200Generic() { MaxReticleFlashRate = 9; } const char *LX200Autostar::getDefaultName() { return (const char *)"LX200 Autostar"; } bool LX200Autostar::initProperties() { LX200Generic::initProperties(); IUFillText(&VersionT[0], "Date", "", ""); IUFillText(&VersionT[1], "Time", "", ""); IUFillText(&VersionT[2], "Number", "", ""); IUFillText(&VersionT[3], "Full", "", ""); IUFillText(&VersionT[4], "Name", "", ""); IUFillTextVector(&VersionTP, VersionT, 5, getDeviceName(), "Firmware Info", "", FIRMWARE_TAB, IP_RO, 0, IPS_IDLE); IUFillNumber(&FocusSpeedN[0], "SPEED", "Speed", "%0.f", 0, 4.0, 1.0, 0); IUFillNumberVector(&FocusSpeedNP, FocusSpeedN, 1, getDeviceName(), "FOCUS_SPEED", "Speed", FOCUS_TAB, IP_RW, 0, IPS_IDLE); return true; } void LX200Autostar::ISGetProperties(const char *dev) { if (dev != nullptr && strcmp(dev, getDeviceName()) != 0) return; LX200Generic::ISGetProperties(dev); /* if (isConnected()) { defineText(&VersionTP); defineNumber(&FocusSpeedNP); // For Autostar, we have a different focus speed method // Therefore, we don't need the classical one deleteProperty(FocusModeSP.name); } */ } bool LX200Autostar::updateProperties() { LX200Generic::updateProperties(); if (isConnected()) { defineText(&VersionTP); defineNumber(&FocusSpeedNP); // For Autostar, we have a different focus speed method // Therefore, we don't need the classical one deleteProperty(FocusModeSP.name); return true; } else { deleteProperty(VersionTP.name); deleteProperty(FocusSpeedNP.name); return true; } } bool LX200Autostar::ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) { if (dev != nullptr && strcmp(dev, getDeviceName()) == 0) { // Focus speed if (!strcmp(name, FocusSpeedNP.name)) { if (IUUpdateNumber(&FocusSpeedNP, values, names, n) < 0) return false; if (!isSimulation()) setGPSFocuserSpeed(PortFD, ((int)FocusSpeedN[0].value)); FocusSpeedNP.s = IPS_OK; IDSetNumber(&FocusSpeedNP, nullptr); return true; } } return LX200Generic::ISNewNumber(dev, name, values, names, n); } bool LX200Autostar::ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) { int index = 0; if (dev != nullptr && strcmp(dev, getDeviceName()) == 0) { // Focus Motion if (!strcmp(name, FocusMotionSP.name)) { // If speed is "halt" if (FocusSpeedN[0].value == 0) { FocusMotionSP.s = IPS_IDLE; IDSetSwitch(&FocusMotionSP, nullptr); return false; } int last_motion = IUFindOnSwitchIndex(&FocusMotionSP); if (IUUpdateSwitch(&FocusMotionSP, states, names, n) < 0) return false; index = IUFindOnSwitchIndex(&FocusMotionSP); // If same direction and we're busy, stop if (last_motion == index && FocusMotionSP.s == IPS_BUSY) { IUResetSwitch(&FocusMotionSP); FocusMotionSP.s = IPS_IDLE; setFocuserSpeedMode(PortFD, 0); IDSetSwitch(&FocusMotionSP, nullptr); return true; } if (!isSimulation() && setFocuserMotion(PortFD, index) < 0) { FocusMotionSP.s = IPS_ALERT; IDSetSwitch(&FocusMotionSP, "Error setting focuser speed."); return false; } FocusMotionSP.s = IPS_BUSY; // with a timer if (FocusTimerNP.np[0].value > 0) { FocusTimerNP.s = IPS_BUSY; if (isDebug()) IDLog("Starting Focus Timer BUSY\n"); IEAddTimer(50, LX200Generic::updateFocusHelper, this); } IDSetSwitch(&FocusMotionSP, nullptr); return true; } } return LX200Generic::ISNewSwitch(dev, name, states, names, n); } void LX200Autostar::getBasicData() { // process parent LX200Generic::getBasicData(); if (!isSimulation()) { VersionTP.tp[0].text = new char[64]; getVersionDate(PortFD, VersionTP.tp[0].text); VersionTP.tp[1].text = new char[64]; getVersionTime(PortFD, VersionTP.tp[1].text); VersionTP.tp[2].text = new char[64]; getVersionNumber(PortFD, VersionTP.tp[2].text); VersionTP.tp[3].text = new char[128]; getFullVersion(PortFD, VersionTP.tp[3].text); VersionTP.tp[4].text = new char[128]; getProductName(PortFD, VersionTP.tp[4].text); IDSetText(&VersionTP, nullptr); } } libindi/drivers/telescope/telescope_script.h0000664000175000017500000000376313263645557020664 0ustar jasemjasem/******************************************************************************* Copyright(c) 2016 CloudMakers, s. r. o.. All rights reserved. This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2 as published by the Free Software Foundation. . 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 Library General Public License for more details. . You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. *******************************************************************************/ #pragma once #include "inditelescope.h" class ScopeScript : public INDI::Telescope { public: ScopeScript(); virtual ~ScopeScript() = default; virtual const char *getDefaultName() override; virtual bool initProperties() override; virtual bool saveConfigItems(FILE *fp) override; void ISGetProperties(const char *dev) override; bool ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n) override; virtual bool Connect() override; virtual bool Disconnect() override; virtual bool Handshake() override; protected: virtual bool MoveNS(INDI_DIR_NS dir, TelescopeMotionCommand command) override; virtual bool MoveWE(INDI_DIR_WE dir, TelescopeMotionCommand command) override; virtual bool Abort() override; virtual bool ReadScopeStatus() override; virtual bool Goto(double, double) override; virtual bool Sync(double ra, double dec) override; virtual bool Park() override; virtual bool UnPark() override; private: bool RunScript(int script, ...); ITextVectorProperty ScriptsTP; IText ScriptsT[15] {}; }; libindi/drivers/telescope/synscanmount.cpp0000664000175000017500000010703513263645557020406 0ustar jasemjasem/******************************************************************************* Copyright(c) 2010 Gerry Rozema. All rights reserved. This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2 as published by the Free Software Foundation. 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 Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. *******************************************************************************/ #include "synscanmount.h" #include "indicom.h" #include // libnova specifies round() on old systems and it collides with the new gcc 5.x/6.x headers #define HAVE_ROUND #include #include #include #include #define SYNSCAN_SLEW_RATES 9 // We declare an auto pointer to Synscan. std::unique_ptr synscan(new SynscanMount()); void ISGetProperties(const char *dev) { synscan->ISGetProperties(dev); } void ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) { synscan->ISNewSwitch(dev, name, states, names, n); } void ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n) { synscan->ISNewText(dev, name, texts, names, n); } void ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) { synscan->ISNewNumber(dev, name, values, names, n); } void ISNewBLOB(const char *dev, const char *name, int sizes[], int blobsizes[], char *blobs[], char *formats[], char *names[], int n) { INDI_UNUSED(dev); INDI_UNUSED(name); INDI_UNUSED(sizes); INDI_UNUSED(blobsizes); INDI_UNUSED(blobs); INDI_UNUSED(formats); INDI_UNUSED(names); INDI_UNUSED(n); } void ISSnoopDevice(XMLEle *root) { synscan->ISSnoopDevice(root); } SynscanMount::SynscanMount() { SetTelescopeCapability(TELESCOPE_CAN_PARK | TELESCOPE_CAN_ABORT | TELESCOPE_CAN_SYNC | TELESCOPE_CAN_GOTO | TELESCOPE_HAS_TIME | TELESCOPE_HAS_LOCATION, SYNSCAN_SLEW_RATES); SetParkDataType(PARK_RA_DEC_ENCODER); strncpy(LastParkRead, "", 1); } bool SynscanMount::Connect() { if (isConnected()) return true; bool Ret = INDI::Telescope::Connect(); if (Ret) { AnalyzeHandset(); } return Ret; } const char *SynscanMount::getDefaultName() { return "SynScan"; } bool SynscanMount::initProperties() { bool Ret = INDI::Telescope::initProperties(); SetTelescopeCapability(TELESCOPE_CAN_PARK | TELESCOPE_CAN_ABORT | TELESCOPE_CAN_SYNC | TELESCOPE_CAN_GOTO | TELESCOPE_HAS_TIME | TELESCOPE_HAS_LOCATION, SYNSCAN_SLEW_RATES); SetParkDataType(PARK_RA_DEC_ENCODER); // probably want to debug this addDebugControl(); addConfigurationControl(); // Set up property variables IUFillText(&BasicMountInfo[(int)MountInfoItems::FwVersion], "FW_VERSION", "Firmware version", "-"); IUFillText(&BasicMountInfo[(int)MountInfoItems::MountCode], "MOUNT_CODE", "Mount code", "-"); IUFillText(&BasicMountInfo[(int)MountInfoItems::AlignmentStatus], "ALIGNMENT_STATUS", "Alignment status", "-"); IUFillText(&BasicMountInfo[(int)MountInfoItems::GotoStatus], "GOTO_STATUS", "Goto status", "-"); IUFillText(&BasicMountInfo[(int)MountInfoItems::MountPointingStatus], "MOUNT_POINTING_STATUS", "Mount pointing status", "-"); IUFillText(&BasicMountInfo[(int)MountInfoItems::TrackingMode], "TRACKING_MODE", "Tracking mode", "-"); IUFillTextVector(&BasicMountInfoV, BasicMountInfo, 6, getDeviceName(), "BASIC_MOUNT_INFO", "Mount information", MountInfoPage.c_str(), IP_RO, 60, IPS_IDLE); return Ret; } void SynscanMount::ISGetProperties(const char *dev) { /* First we let our parent populate */ INDI::Telescope::ISGetProperties(dev); /*if (isConnected()) { UpdateMountInformation(false); defineText(&BasicMountInfoV); }*/ } bool SynscanMount::ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) { return INDI::Telescope::ISNewNumber(dev, name, values, names, n); } bool SynscanMount::ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) { return INDI::Telescope::ISNewSwitch(dev, name, states, names, n); } bool SynscanMount::ISNewBLOB(const char *dev, const char *name, int sizes[], int blobsizes[], char *blobs[], char *formats[], char *names[], int n) { return INDI::Telescope::ISNewBLOB(dev, name, sizes, blobsizes, blobs, formats, names, n); } bool SynscanMount::ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n) { return INDI::Telescope::ISNewText(dev, name, texts, names, n); } bool SynscanMount::updateProperties() { INDI::Telescope::updateProperties(); LOG_INFO("Update Properties"); if (isConnected()) { UpdateMountInformation(false); defineText(&BasicMountInfoV); } else { deleteProperty(BasicMountInfoV.name); } return true; } int SynscanMount::HexStrToInteger(const std::string &str) { return std::stoi(str, nullptr, 16); } bool SynscanMount::AnalyzeHandset() { bool rc = true; int caps = 0; int tmp = 0; int bytesWritten = 0; int bytesRead, numread; char str[20]; caps = GetTelescopeCapability(); rc = ReadLocation(); if (rc) { CanSetLocation = true; ReadTime(); } bytesRead = 0; memset(str, 0, 20); tty_write(PortFD, "J", 1, &bytesWritten); tty_read(PortFD, str, 2, 2, &bytesRead); // Read the handset version bytesRead = 0; memset(str, 0, 20); tty_write(PortFD, "V", 1, &bytesWritten); tty_read(PortFD, str, 7, 2, &bytesRead); if (bytesRead == 3) { int tmp1 { 0 }, tmp2 { 0 }; tmp = str[0]; tmp1 = str[1]; tmp2 = str[2]; FirmwareVersion = tmp2; FirmwareVersion /= 100; FirmwareVersion += tmp1; FirmwareVersion /= 100; FirmwareVersion += tmp; } else { FirmwareVersion = (double)HexStrToInteger(std::string(&str[0], 2)); FirmwareVersion += (double)HexStrToInteger(std::string(&str[2], 2)) / 100; FirmwareVersion += (double)HexStrToInteger(std::string(&str[4], 2)) / 10000; } LOGF_INFO("Firmware version: %lf", FirmwareVersion); if (FirmwareVersion < 3.38 || (FirmwareVersion >= 4.0 && FirmwareVersion < 4.38)) { IDMessage(nullptr, "Update Synscan firmware to V3.38/V4.38 or above"); LOG_INFO("Too old firmware version!"); } else { NewFirmware = true; } HandsetFwVersion = std::to_string(FirmwareVersion); memset(str, 0, 2); tty_write(PortFD, "m", 1, &bytesWritten); tty_read(PortFD, str, 2, 2, &bytesRead); if (bytesRead == 2) { // This workaround is needed because the firmware 3.39 sends these bytes swapped. if (str[1] == '#') MountCode = (int)*reinterpret_cast(&str[0]); else MountCode = (int)*reinterpret_cast(&str[1]); } // Check the tracking status memset(str, 0, 2); tty_write(PortFD, "t", 1, &bytesWritten); numread = tty_read(PortFD, str, 2, 2, &bytesRead); if (str[1] == '#' && (int)str[0] != 0) { TrackState = SCOPE_TRACKING; } SetTelescopeCapability(caps, SYNSCAN_SLEW_RATES); if (InitPark()) { SetAxis1ParkDefault(0); SetAxis2ParkDefault(90); } else { SetAxis1Park(0); SetAxis2Park(90); SetAxis1ParkDefault(0); SetAxis2ParkDefault(90); } return true; } bool SynscanMount::ReadScopeStatus() { char str[20]; int bytesWritten, bytesRead; int numread; double ra, dec; long unsigned int n1, n2; tty_write(PortFD, "Ka", 2, &bytesWritten); // test for an echo tty_read(PortFD, str, 2, 2, &bytesRead); // Read 2 bytes of response if (str[1] != '#') { LOG_INFO("Synscan Mount not responding"); // Usually, Abort() recovers the communication RecoverTrials++; Abort(); // HasFailed = true; return false; } RecoverTrials = 0; /* // With 3.37 firmware, on the older line of eq6 mounts // The handset does not always initialize the communication with the motors correctly // We can check for this condition by querying the motors for firmware version // and if it returns zero, it means we need to power cycle the handset // and try again after it restarts again if(HasFailed) { int v1,v2; //fprintf(stderr,"Calling passthru command to get motor firmware versions\n"); v1=PassthruCommand(0xfe,0x11,1,0,2); v2=PassthruCommand(0xfe,0x10,1,0,2); fprintf(stderr,"Motor firmware versions %d %d\n",v1,v2); if((v1==0)||(v2==0)) { IDMessage(getDeviceName(),"Cannot proceed"); IDMessage(getDeviceName(),"Handset is responding, but Motors are Not Responding"); return false; } // if we get here, both motors are responding again // so the problem is solved HasFailed=false; } */ // on subsequent passes, we just need to read the time if (HasTime()) { ReadTime(); } if (HasLocation()) { // this flag is set when we get a new lat/long from the host // so we should go thru the read routine once now, so things update // correctly in the client displays if (ReadLatLong) { ReadLocation(); } } // Query mount information memset(str, 0, 2); tty_write(PortFD, "J", 1, &bytesWritten); numread = tty_read(PortFD, str, 2, 2, &bytesRead); if (str[1] == '#') { AlignmentStatus = std::to_string((int)str[0]); } memset(str, 0, 2); tty_write(PortFD, "L", 1, &bytesWritten); numread = tty_read(PortFD, str, 2, 2, &bytesRead); if (str[1] == '#') { GotoStatus = str[0]; } memset(str, 0, 2); tty_write(PortFD, "p", 1, &bytesWritten); numread = tty_read(PortFD, str, 2, 2, &bytesRead); if (str[1] == '#') { MountPointingStatus = str[0]; } memset(str, 0, 2); tty_write(PortFD, "t", 1, &bytesWritten); numread = tty_read(PortFD, str, 2, 2, &bytesRead); if (str[1] == '#') { if ((int)str[0] == 0) TrackingMode = "Tracking off"; if ((int)str[0] == 1) TrackingMode = "Alt/Az tracking"; if ((int)str[0] == 2) TrackingMode = "EQ tracking"; if ((int)str[0] == 3) TrackingMode = "PEC mode"; } UpdateMountInformation(true); if (TrackState == SCOPE_SLEWING) { // We have a slew in progress // lets see if it's complete // This only works for ra/dec goto commands // The goto complete flag doesn't trip for ALT/AZ commands if (GotoStatus != "0") { // Nothing to do here } else { if (TrackState == SCOPE_PARKING) TrackState = SCOPE_PARKED; } } if (TrackState == SCOPE_PARKING) { if (FirmwareVersion == 4.103500) { // With this firmware the correct way // is to check the slewing flat memset(str, 0, 3); tty_write(PortFD, "L", 1, &bytesWritten); numread = tty_read(PortFD, str, 2, 3, &bytesRead); if (str[0] != 48) { // Nothing to do here } else { if (NumPark++ < 2) { Park(); } else { TrackState = SCOPE_PARKED; SetParked(true); } } } else { // ok, lets try read where we are // and see if we have reached the park position // newer firmware versions dont read it back the same way // so we watch now to see if we get the same read twice in a row // to confirm that it has stopped moving memset(str, 0, 20); tty_write(PortFD, "z", 1, &bytesWritten); numread = tty_read(PortFD, str, 18, 2, &bytesRead); //IDMessage(getDeviceName(),"Park Read %s %d",str,StopCount); if (strncmp((char *)str, LastParkRead, 18) == 0) { // We find that often after it stops from park // it's off the park position by a small amount // issuing another park command gets a small movement and then if (++StopCount > 2) { if (NumPark++ < 2) { StopCount = 0; //IDMessage(getDeviceName(),"Sending park again"); Park(); } else { TrackState = SCOPE_PARKED; //ParkSP.s=IPS_OK; //IDSetSwitch(&ParkSP,nullptr); //IDMessage(getDeviceName(),"Telescope is Parked."); SetParked(true); } } else { //StopCount=0; } } else { StopCount = 0; } strncpy(LastParkRead, str, sizeof(str)); } } memset(str, 0, 20); tty_write(PortFD, "e", 1, &bytesWritten); numread = tty_read(PortFD, str, 18, 1, &bytesRead); if (bytesRead != 18) { LOG_DEBUG("Read current position failed"); return false; } sscanf(str, "%lx,%lx#", &n1, &n2); ra = (double)n1 / 0x100000000 * 24.0; dec = (double)n2 / 0x100000000 * 360.0; CurrentRA = ra; CurrentDEC = dec; // Now feed the rest of the system with corrected data NewRaDec(ra, dec); if (TrackState == SCOPE_SLEWING && MountCode >= 128 && (SlewTargetAz != -1 || SlewTargetAlt != -1)) { ln_hrz_posn CurrentAltAz { 0, 0 }; double DiffAlt { 0 }; double DiffAz { 0 }; CurrentAltAz = GetAltAzPosition(ra, dec); DiffAlt = CurrentAltAz.alt-SlewTargetAlt; if (SlewTargetAlt != -1 && std::abs(DiffAlt) > 0.01) { int NewRate = 2; if (std::abs(DiffAlt) > 4) { NewRate = 9; } else if (std::abs(DiffAlt) > 1.2) { NewRate = 7; } else if (std::abs(DiffAlt) > 0.5) { NewRate = 5; } else if (std::abs(DiffAlt) > 0.2) { NewRate = 4; } else if (std::abs(DiffAlt) > 0.025) { NewRate = 3; } LOGF_DEBUG("Slewing Alt axis: %1.3f-%1.3f -> %1.3f (speed: %d)", CurrentAltAz.alt, SlewTargetAlt, CurrentAltAz.alt-SlewTargetAlt, CustomNSSlewRate); if (NewRate != CustomNSSlewRate) { if (DiffAlt < 0) { CustomNSSlewRate = NewRate; MoveNS(DIRECTION_NORTH, MOTION_START); } else { CustomNSSlewRate = NewRate; MoveNS(DIRECTION_SOUTH, MOTION_START); } } } else if (SlewTargetAlt != -1 && std::abs(DiffAlt) < 0.01) { MoveNS(DIRECTION_NORTH, MOTION_STOP); SlewTargetAlt = -1; LOG_INFO("Slewing on Alt axis finished"); } DiffAz = CurrentAltAz.az-SlewTargetAz; if (DiffAz < -180) DiffAz = (DiffAz+360)*2; else if (DiffAz > 180) DiffAz = (DiffAz-360)*2; if (SlewTargetAz != -1 && std::abs(DiffAz) > 0.01) { int NewRate = 2; if (std::abs(DiffAz) > 4) { NewRate = 9; } else if (std::abs(DiffAz) > 1.2) { NewRate = 7; } else if (std::abs(DiffAz) > 0.5) { NewRate = 5; } else if (std::abs(DiffAz) > 0.2) { NewRate = 4; } else if (std::abs(DiffAz) > 0.025) { NewRate = 3; } LOGF_DEBUG("Slewing Az axis: %1.3f-%1.3f -> %1.3f (speed: %d)", CurrentAltAz.az, SlewTargetAz, CurrentAltAz.az-SlewTargetAz, CustomWESlewRate); if (NewRate != CustomWESlewRate) { if (DiffAz > 0) { CustomWESlewRate = NewRate; MoveWE(DIRECTION_WEST, MOTION_START); } else { CustomWESlewRate = NewRate; MoveWE(DIRECTION_EAST, MOTION_START); } } } else if (SlewTargetAz != -1 && std::abs(DiffAz) < 0.01) { MoveWE(DIRECTION_WEST, MOTION_STOP); SlewTargetAz = -1; LOG_INFO("Slewing on Az axis finished"); } if (SlewTargetAz == -1 && SlewTargetAlt == -1) { StartTrackMode(); } } return true; } bool SynscanMount::StartTrackMode() { char str[20]; int numread, bytesWritten, bytesRead; TrackState = SCOPE_TRACKING; LOG_INFO("Tracking started"); // Start tracking str[0] = 'T'; // Check the mount type to choose tracking mode if (MountCode >= 128) { // Alt/Az tracking mode str[1] = 1; } else { // EQ tracking mode str[1] = 2; } tty_write(PortFD, str, 2, &bytesWritten); numread = tty_read(PortFD, str, 1, 2, &bytesRead); if (bytesRead != 1 || str[0] != '#') { LOG_DEBUG("Timeout waiting for scope to start tracking."); return false; } return true; } bool SynscanMount::Goto(double ra, double dec) { char str[20]; int bytesWritten, bytesRead; ln_hrz_posn TargetAltAz { 0, 0 }; tty_write(PortFD, "Ka", 2, &bytesWritten); // test for an echo tty_read(PortFD, str, 2, 2, &bytesRead); // Read 2 bytes of response if (str[1] != '#') { LOG_INFO("Wrong answer from the mount"); // this is not a correct echo // so we are not talking to a mount properly return false; } TrackState = SCOPE_SLEWING; // EQ mount has a different Goto mode if (MountCode < 128) { int n1 = ra * 0x1000000 / 24; int n2 = dec * 0x1000000 / 360; int numread; n1 = n1 << 8; n2 = n2 << 8; sprintf((char*)str, "r%08X,%08X", n1, n2); tty_write(PortFD, str, 18, &bytesWritten); memset(&str[18], 0, 1); LOGF_DEBUG("Goto - ra: %g de: %g", ra, dec); numread = tty_read(PortFD, str, 1, 60, &bytesRead); if (bytesRead != 1 || str[0] != '#') { LOG_DEBUG("Timeout waiting for scope to complete goto."); return false; } return true; } TargetAltAz = GetAltAzPosition(ra, dec); LOGF_DEBUG("Goto - ra: %g de: %g (az: %g alt: %g)", ra, dec, TargetAltAz.az, TargetAltAz.alt); SlewTargetAz = TargetAltAz.az; SlewTargetAlt = TargetAltAz.alt; return true; } bool SynscanMount::Park() { char str[20]; int numread, bytesWritten, bytesRead; strncpy(LastParkRead, "", 1); memset(str, 0, 3); tty_write(PortFD, "Ka", 2, &bytesWritten); // test for an echo tty_read(PortFD, str, 2, 2, &bytesRead); // Read 2 bytes of response if (str[1] != '#') { // this is not a correct echo // so we are not talking to a mount properly return false; } // Now we stop tracking str[0] = 'T'; str[1] = 0; tty_write(PortFD, str, 2, &bytesWritten); numread = tty_read(PortFD, str, 1, 60, &bytesRead); if (bytesRead != 1 || str[0] != '#') { LOG_DEBUG("Timeout waiting for scope to stop tracking."); return false; } //sprintf((char *)str,"b%08X,%08X",0x0,0x40000000); tty_write(PortFD, "b00000000,40000000", 18, &bytesWritten); numread = tty_read(PortFD, str, 1, 60, &bytesRead); if (bytesRead != 1 || str[0] != '#') { LOG_DEBUG("Timeout waiting for scope to respond to park."); return false; } TrackState = SCOPE_PARKING; if (NumPark == 0) { LOG_INFO("Parking Mount..."); } StopCount = 0; return true; } bool SynscanMount::UnPark() { SetParked(false); NumPark = 0; return true; } bool SynscanMount::SetCurrentPark() { LOG_INFO("Setting arbitrary park positions is not supported yet."); return false; } bool SynscanMount::SetDefaultPark() { // By default az to north, and alt to pole LOG_DEBUG("Setting Park Data to Default."); SetAxis1Park(0); SetAxis2Park(90); return true; } bool SynscanMount::Abort() { if (TrackState == SCOPE_IDLE || RecoverTrials >= 3) return true; char str[20]; int numread, bytesWritten, bytesRead; LOG_INFO("Abort any motions"); TrackState = SCOPE_IDLE; SlewTargetAlt = -1; SlewTargetAz = -1; CustomNSSlewRate = -1; CustomWESlewRate = -1; // Stop tracking str[0] = 'T'; str[1] = 0; tty_write(PortFD, str, 2, &bytesWritten); numread = tty_read(PortFD, str, 1, 2, &bytesRead); if (bytesRead != 1 || str[0] != '#') { LOG_DEBUG("Timeout waiting for scope to stop tracking."); return false; } // Hmmm twice only stops it tty_write(PortFD, "M", 1, &bytesWritten); tty_read(PortFD, str, 1, 1, &bytesRead); // Read 1 bytes of response tty_write(PortFD, "M", 1, &bytesWritten); tty_read(PortFD, str, 1, 1, &bytesRead); // Read 1 bytes of response return true; } bool SynscanMount::MoveNS(INDI_DIR_NS dir, TelescopeMotionCommand command) { if (command != MOTION_START) { PassthruCommand(37, 17, 2, 0, 0); } else { int tt = (CustomNSSlewRate == -1 ? SlewRate : CustomNSSlewRate); tt = tt << 16; if (dir != DIRECTION_NORTH) { PassthruCommand(37, 17, 2, tt, 0); } else { PassthruCommand(36, 17, 2, tt, 0); } } return true; } bool SynscanMount::MoveWE(INDI_DIR_WE dir, TelescopeMotionCommand command) { if (command != MOTION_START) { PassthruCommand(37, 16, 2, 0, 0); } else { int tt = (CustomWESlewRate == -1 ? SlewRate : CustomWESlewRate); tt = tt << 16; if (dir != DIRECTION_WEST) { PassthruCommand(36, 16, 2, tt, 0); } else { PassthruCommand(37, 16, 2, tt, 0); } } return true; } bool SynscanMount::SetSlewRate(int s) { SlewRate = s + 1; return true; } int SynscanMount::PassthruCommand(int cmd, int target, int msgsize, int data, int numReturn) { char test[20]; int bytesRead, bytesWritten; char a, b, c; int tt = data; a = tt % 256; tt = tt >> 8; b = tt % 256; tt = tt >> 8; c = tt % 256; // format up a passthru command memset(test, 0, 20); test[0] = 80; // passhtru test[1] = msgsize; // set message size test[2] = target; // set the target test[3] = cmd; // set the command test[4] = c; // set data bytes test[5] = b; test[6] = a; test[7] = numReturn; tty_write(PortFD, test, 8, &bytesWritten); memset(test, 0, 20); tty_read(PortFD, test, numReturn + 1, 2, &bytesRead); if (numReturn > 0) { int retval = 0; retval = test[0]; if (numReturn > 1) { retval = retval << 8; retval += test[1]; } if (numReturn > 2) { retval = retval << 8; retval += test[2]; } return retval; } return 0; } bool SynscanMount::ReadTime() { char str[20]; int bytesWritten = 0, bytesRead = 0; // lets see if this hand controller responds to a time request bytesRead = 0; tty_write(PortFD, "h", 1, &bytesWritten); tty_read(PortFD, str, 9, 2, &bytesRead); if (str[8] == '#') { ln_zonedate localTime; ln_date utcTime; int offset, daylightflag; localTime.hours = str[0]; localTime.minutes = str[1]; localTime.seconds = str[2]; localTime.months = str[3]; localTime.days = str[4]; localTime.years = str[5]; offset = (int)str[6]; // Negative GMT offset is read. It needs special treatment if (offset > 200) offset -= 256; localTime.gmtoff = offset; daylightflag = str[7]; // this is the daylight savings flag in the hand controller, needed if we did not set the time localTime.years += 2000; localTime.gmtoff *= 3600; // now convert to utc ln_zonedate_to_date(&localTime, &utcTime); // now we have time from the hand controller, we need to set some variables int sec; char utc[100]; char ofs[10]; sec = (int)utcTime.seconds; sprintf(utc, "%04d-%02d-%dT%d:%02d:%02d", utcTime.years, utcTime.months, utcTime.days, utcTime.hours, utcTime.minutes, sec); if (daylightflag == 1) offset = offset + 1; sprintf(ofs, "%d", offset); IUSaveText(&TimeT[0], utc); IUSaveText(&TimeT[1], ofs); TimeTP.s = IPS_OK; IDSetText(&TimeTP, nullptr); return true; } return false; } bool SynscanMount::ReadLocation() { char str[20]; int bytesWritten = 0, bytesRead = 0; tty_write(PortFD, "Ka", 2, &bytesWritten); // test for an echo tty_read(PortFD, str, 2, 2, &bytesRead); // Read 2 bytes of response if (str[1] != '#') { LOG_INFO("Bad echo in ReadLocation"); } else { // lets see if this hand controller responds to a location request bytesRead = 0; tty_write(PortFD, "w", 1, &bytesWritten); tty_read(PortFD, str, 9, 2, &bytesRead); if (str[8] == '#') { double lat, lon; // lets parse this data now int a, b, c, d, e, f, g, h; a = str[0]; b = str[1]; c = str[2]; d = str[3]; e = str[4]; f = str[5]; g = str[6]; h = str[7]; //fprintf(stderr,"Pos %d:%d:%d %d:%d:%d\n",a,b,c,e,f,g); double t1, t2, t3; t1 = c; t2 = b; t3 = a; t1 = t1 / 3600.0; t2 = t2 / 60.0; lat = t1 + t2 + t3; t1 = g; t2 = f; t3 = e; t1 = t1 / 3600.0; t2 = t2 / 60.0; lon = t1 + t2 + t3; if (d == 1) lat = lat * -1; if (h == 1) lon = 360 - lon; LocationN[LOCATION_LATITUDE].value = lat; LocationN[LOCATION_LONGITUDE].value = lon; IDSetNumber(&LocationNP, nullptr); // We dont need to keep reading this one on every cycle // only need to read it when it's been changed ReadLatLong = false; return true; } else { LOG_INFO("Mount does not support setting location"); } } return false; } bool SynscanMount::updateTime(ln_date *utc, double utc_offset) { char str[20]; int bytesWritten = 0, bytesRead = 0; // start by formatting a time for the hand controller // we are going to set controller to local time // struct ln_zonedate ltm; ln_date_to_zonedate(utc, <m, (long)utc_offset * 3600.0); int yr = ltm.years; yr = yr % 100; str[0] = 'H'; str[1] = ltm.hours; str[2] = ltm.minutes; str[3] = (char)(int)ltm.seconds; str[4] = ltm.months; str[5] = ltm.days; str[6] = yr; // Strangely enough static_cast(double) results 0 for negative values on arm // We need to use old C-like casts in this case. str[7] = (char)(int)utc_offset; // offset from utc so hand controller is running in local time str[8] = 0; // and no daylight savings adjustments, it's already included in the offset // lets write a time to the hand controller bytesRead = 0; tty_write(PortFD, str, 9, &bytesWritten); tty_read(PortFD, str, 1, 2, &bytesRead); if (str[0] != '#') { LOG_INFO("Invalid return from set time"); } return true; } bool SynscanMount::updateLocation(double latitude, double longitude, double elevation) { INDI_UNUSED(elevation); char str[20]; int bytesWritten = 0, bytesRead = 0; int s = 0; bool IsWest = false; double tmp = 0; ln_lnlat_posn p1 { 0, 0 }; lnh_lnlat_posn p2; LocationN[LOCATION_LATITUDE].value = latitude; LocationN[LOCATION_LONGITUDE].value = longitude; IDSetNumber(&LocationNP, nullptr); if (!CanSetLocation) { return true; } else { if (longitude > 180) { p1.lng = 360.0 - longitude; IsWest = true; } else { p1.lng = longitude; } p1.lat = latitude; ln_lnlat_to_hlnlat(&p1, &p2); LOGF_INFO("Update location - latitude %d:%d:%1.2f longitude %d:%d:%1.2f\n", p2.lat.degrees, p2.lat.minutes, p2.lat.seconds, p2.lng.degrees, p2.lng.minutes, p2.lng.seconds); str[0] = 'W'; str[1] = p2.lat.degrees; str[2] = p2.lat.minutes; tmp = p2.lat.seconds + 0.5; s = (int)tmp; // put in an int that's rounded str[3] = s; if (p2.lat.neg == 0) { str[4] = 0; } else { str[4] = 1; } str[5] = p2.lng.degrees; str[6] = p2.lng.minutes; s = (int)(p2.lng.seconds + 0.5); // make an int, that's rounded str[7] = s; if (IsWest) str[8] = 1; else str[8] = 0; // All formatted, now send to the hand controller; bytesRead = 0; tty_write(PortFD, str, 9, &bytesWritten); tty_read(PortFD, str, 1, 2, &bytesRead); if (str[0] != '#') { LOG_INFO("Invalid response for location setting"); } // want to read it on the next cycle, so we update the fields in the client ReadLatLong = true; return true; } } bool SynscanMount::Sync(double ra, double dec) { bool IsTrackingBeforeSync = (TrackState == SCOPE_TRACKING); // Abort any motion before syncing Abort(); LOGF_INFO("Sync %g %g -> %g %g", CurrentRA, CurrentDEC, ra, dec); char str[20]; int numread, bytesWritten, bytesRead; ln_hrz_posn TargetAltAz { 0, 0 }; TargetAltAz = GetAltAzPosition(ra, dec); if (isDebug()) { LOGF_INFO("Sync - ra: %g de: %g to az: %g alt: %g", ra, dec, TargetAltAz.az, TargetAltAz.alt); } else { LOGF_DEBUG("Sync - ra: %g de: %g to az: %g alt: %g", ra, dec, TargetAltAz.az, TargetAltAz.alt); } // Assemble the Reset Position command for Az axis int Az = (int)(TargetAltAz.az*16777216 / 360); str[0] = 'P'; str[1] = 4; str[2] = 16; str[3] = 4; *reinterpret_cast(&str[4]) = (unsigned char)(Az / 65536); Az -= (Az / 65536)*65536; *reinterpret_cast(&str[5]) = (unsigned char)(Az / 256); Az -= (Az / 256)*256; *reinterpret_cast(&str[6]) = (unsigned char)Az; str[7] = 0; tty_write(PortFD, str, 8, &bytesWritten); numread = tty_read(PortFD, str, 1, 3, &bytesRead); // Assemble the Reset Position command for Alt axis int Alt = (int)(TargetAltAz.alt*16777216 / 360); str[0] = 'P'; str[1] = 4; str[2] = 17; str[3] = 4; *reinterpret_cast(&str[4]) = (unsigned char)(Alt / 65536); Alt -= (Alt / 65536)*65536; *reinterpret_cast(&str[5]) = (unsigned char)(Alt / 256); Alt -= (Alt / 256)*256; *reinterpret_cast(&str[6]) = (unsigned char)Alt; str[7] = 0; tty_write(PortFD, str, 8, &bytesWritten); numread = tty_read(PortFD, str, 1, 2, &bytesRead); // Pass the sync command to the handset int n1 = ra * 0x1000000 / 24; int n2 = dec * 0x1000000 / 360; n1 = n1 << 8; n2 = n2 << 8; sprintf((char*)str, "s%08X,%08X", n1, n2); memset(&str[18], 0, 1); LOGF_DEBUG("Send Sync command to the handset (%s)", str); tty_write(PortFD, str, 18, &bytesWritten); numread = tty_read(PortFD, str, 1, 60, &bytesRead); if (bytesRead != 1 || str[0] != '#') { LOG_DEBUG("Timeout waiting for scope to complete syncing."); return false; } // Start tracking again if (IsTrackingBeforeSync) StartTrackMode(); return true; } ln_hrz_posn SynscanMount::GetAltAzPosition(double ra, double dec) { ln_lnlat_posn Location { 0, 0 }; ln_equ_posn Eq { 0, 0 }; ln_hrz_posn AltAz { 0, 0 }; // Set the current location Location.lat = LocationN[LOCATION_LATITUDE].value; Location.lng = LocationN[LOCATION_LONGITUDE].value; Eq.ra = ra*360.0 / 24.0; Eq.dec = dec; ln_get_hrz_from_equ(&Eq, &Location, ln_get_julian_from_sys(), &AltAz); AltAz.az -= 180; if (AltAz.az < 0) AltAz.az += 360; return AltAz; } void SynscanMount::UpdateMountInformation(bool inform_client) { bool BasicMountInfoHasChanged = false; std::string MountCodeStr = std::to_string(MountCode); if (std::string(BasicMountInfo[(int)MountInfoItems::FwVersion].text) != HandsetFwVersion) { IUSaveText(&BasicMountInfo[(int)MountInfoItems::FwVersion], HandsetFwVersion.c_str()); BasicMountInfoHasChanged = true; } if (std::string(BasicMountInfo[(int)MountInfoItems::MountCode].text) != MountCodeStr) { IUSaveText(&BasicMountInfo[(int)MountInfoItems::MountCode], MountCodeStr.c_str()); BasicMountInfoHasChanged = true; } if (std::string(BasicMountInfo[(int)MountInfoItems::AlignmentStatus].text) != AlignmentStatus) { IUSaveText(&BasicMountInfo[(int)MountInfoItems::AlignmentStatus], AlignmentStatus.c_str()); BasicMountInfoHasChanged = true; } if (std::string(BasicMountInfo[(int)MountInfoItems::GotoStatus].text) != GotoStatus) { IUSaveText(&BasicMountInfo[(int)MountInfoItems::GotoStatus], GotoStatus.c_str()); BasicMountInfoHasChanged = true; } if (std::string(BasicMountInfo[(int)MountInfoItems::MountPointingStatus].text) != MountPointingStatus) { IUSaveText(&BasicMountInfo[(int)MountInfoItems::MountPointingStatus], MountPointingStatus.c_str()); BasicMountInfoHasChanged = true; } if (std::string(BasicMountInfo[(int)MountInfoItems::TrackingMode].text) != TrackingMode) { IUSaveText(&BasicMountInfo[(int)MountInfoItems::TrackingMode], TrackingMode.c_str()); BasicMountInfoHasChanged = true; } if (BasicMountInfoHasChanged && inform_client) IDSetText(&BasicMountInfoV, nullptr); } libindi/drivers/telescope/celestrongps.cpp0000664000175000017500000012240713263645557020355 0ustar jasemjasem#if 0 Celestron GPS Copyright (C) 2003-2017 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 Version with experimental pulse guide support. GC 04.12.2015 #endif #include "celestrongps.h" #include "indicom.h" #include #include #include #include #include // Simulation Parameters #define GOTO_RATE 5 // slew rate, degrees/s #define SLEW_RATE 0.5 // slew rate, degrees/s #define FINE_SLEW_RATE 0.1 // slew rate, degrees/s #define GOTO_LIMIT 5.5 // Move at GOTO_RATE until distance from target is GOTO_LIMIT degrees #define SLEW_LIMIT 1 // Move at SLEW_LIMIT until distance from target is SLEW_LIMIT degrees #define FINE_SLEW_LIMIT 0.5 // Move at FINE_SLEW_RATE until distance from target is FINE_SLEW_LIMIT degrees #define MOUNTINFO_TAB "Mount Info" std::unique_ptr telescope(new CelestronGPS()); void ISGetProperties(const char *dev) { telescope->ISGetProperties(dev); } void ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) { telescope->ISNewSwitch(dev, name, states, names, n); } void ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n) { telescope->ISNewText(dev, name, texts, names, n); } void ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) { telescope->ISNewNumber(dev, name, values, names, n); } void ISNewBLOB(const char *dev, const char *name, int sizes[], int blobsizes[], char *blobs[], char *formats[], char *names[], int n) { INDI_UNUSED(dev); INDI_UNUSED(name); INDI_UNUSED(sizes); INDI_UNUSED(blobsizes); INDI_UNUSED(blobs); INDI_UNUSED(formats); INDI_UNUSED(names); INDI_UNUSED(n); } void ISSnoopDevice(XMLEle *root) { telescope->ISSnoopDevice(root); } CelestronGPS::CelestronGPS() { setVersion(3, 1); fwInfo.Version = "Invalid"; fwInfo.controllerVersion = 0; fwInfo.controllerVariant = ISNEXSTAR; INDI::Logger::getInstance().addDebugLevel("Scope Verbose", "SCOPE"); currentRA = 0; currentDEC = 90; currentAZ = 0; currentALT = 0; targetAZ = 0; targetALT = 0; } bool CelestronGPS::checkMinVersion(float minVersion, const char *feature) { if (((fwInfo.controllerVariant == ISSTARSENSE) && (fwInfo.controllerVersion < MINSTSENSVER)) || ((fwInfo.controllerVariant == ISNEXSTAR) && (fwInfo.controllerVersion < minVersion))) { LOGF_WARN("Firmware v%3.1f does not support %s. Minimun required version is %3.1f", fwInfo.controllerVersion, feature, minVersion); return false; } return true; } const char *CelestronGPS::getDefaultName() { return ((const char *)"Celestron GPS"); } bool CelestronGPS::initProperties() { INDI::Telescope::initProperties(); // Firmware IUFillText(&FirmwareT[FW_MODEL], "Model", "", 0); IUFillText(&FirmwareT[FW_VERSION], "Version", "", 0); IUFillText(&FirmwareT[FW_GPS], "GPS", "", 0); IUFillText(&FirmwareT[FW_RA], "RA", "", 0); IUFillText(&FirmwareT[FW_DEC], "DEC", "", 0); IUFillTextVector(&FirmwareTP, FirmwareT, 5, getDeviceName(), "Firmware Info", "", MOUNTINFO_TAB, IP_RO, 0, IPS_IDLE); AddTrackMode("TRACK_ALTAZ", "Alt/Az"); AddTrackMode("TRACK_EQN", "Eq North", true); AddTrackMode("TRACK_EQS", "Eq South"); IUFillSwitch(&UseHibernateS[0], "Enable", "", ISS_OFF); IUFillSwitch(&UseHibernateS[1], "Disable", "", ISS_ON); IUFillSwitchVector(&UseHibernateSP, UseHibernateS, 2, getDeviceName(), "Hibernate", "", OPTIONS_TAB, IP_RW, ISR_1OFMANY, 0, IPS_IDLE); //GUIDE Define "Use Pulse Cmd" property (Switch). IUFillSwitch(&UsePulseCmdS[0], "Off", "", ISS_ON); IUFillSwitch(&UsePulseCmdS[1], "On", "", ISS_OFF); IUFillSwitchVector(&UsePulseCmdSP, UsePulseCmdS, 2, getDeviceName(), "Use Pulse Cmd", "", MAIN_CONTROL_TAB, IP_RW, ISR_1OFMANY, 0, IPS_IDLE); SetParkDataType(PARK_AZ_ALT); //GUIDE Initialize guiding properties. initGuiderProperties(getDeviceName(), GUIDE_TAB); addAuxControls(); //GUIDE Set guider interface. setDriverInterface(getDriverInterface() | GUIDER_INTERFACE); return true; } void CelestronGPS::ISGetProperties(const char *dev) { static bool configLoaded = false; if (dev != nullptr && strcmp(dev, getDeviceName()) != 0) return; INDI::Telescope::ISGetProperties(dev); defineSwitch(&UseHibernateSP); if (configLoaded == false) { configLoaded = true; loadConfig(true, "Hibernate"); } /* if (isConnected()) { //defineNumber(&HorizontalCoordsNP); defineSwitch(&SlewRateSP); //defineSwitch(&TrackSP); //GUIDE Define guiding properties defineSwitch(&UsePulseCmdSP); defineNumber(&GuideNSNP); defineNumber(&GuideWENP); if (fwInfo.Version != "Invalid") defineText(&FirmwareTP); } */ } bool CelestronGPS::updateProperties() { if (isConnected()) { uint32_t cap = TELESCOPE_CAN_GOTO | TELESCOPE_CAN_ABORT; if (driver.get_firmware(&fwInfo)) { IUSaveText(&FirmwareT[FW_MODEL], fwInfo.Model.c_str()); IUSaveText(&FirmwareT[FW_VERSION], fwInfo.Version.c_str()); IUSaveText(&FirmwareT[FW_GPS], fwInfo.GPSFirmware.c_str()); IUSaveText(&FirmwareT[FW_RA], fwInfo.RAFirmware.c_str()); IUSaveText(&FirmwareT[FW_DEC], fwInfo.DEFirmware.c_str()); usePreciseCoords = (fwInfo.controllerVersion > 2.2); } else { fwInfo.Version = "Invalid"; LOG_WARN("Failed to retrive firmware information."); } // Since issues have been observed with Starsense, enabe parking only with Nexstar controller if (fwInfo.controllerVariant == ISSTARSENSE) { if (fwInfo.controllerVersion >= MINSTSENSVER) LOG_INFO("Starsense controller detected."); else LOGF_WARN("Starsense controller detected, but firmware is too old. " "Current version is %4.2f, but minimum required version is %4.2f. " "Please update your Starsense firmware.", fwInfo.controllerVersion, MINSTSENSVER); } else cap |= TELESCOPE_CAN_PARK; if (checkMinVersion(4.1, "sync")) cap |= TELESCOPE_CAN_SYNC; if (checkMinVersion(2.3, "updating time and location settings")) cap |= TELESCOPE_HAS_TIME | TELESCOPE_HAS_LOCATION; if (fwInfo.controllerVersion >= 2.3) cap |= TELESCOPE_HAS_TRACK_MODE | TELESCOPE_CAN_CONTROL_TRACK; else LOG_WARN("Mount firmware does not support track mode."); SetTelescopeCapability(cap, 9); INDI::Telescope::updateProperties(); if (fwInfo.Version != "Invalid") defineText(&FirmwareTP); if (InitPark()) { // If loading parking data is successful, we just set the default parking values. SetAxis1ParkDefault(LocationN[LOCATION_LATITUDE].value >= 0 ? 0 : 180); SetAxis2ParkDefault(LocationN[LOCATION_LATITUDE].value); } else { // Otherwise, we set all parking data to default in case no parking data is found. SetAxis1Park(LocationN[LOCATION_LATITUDE].value >= 0 ? 0 : 180); SetAxis2Park(LocationN[LOCATION_LATITUDE].value); SetAxis1ParkDefault(LocationN[LOCATION_LATITUDE].value >= 0 ? 0 : 180); SetAxis2ParkDefault(LocationN[LOCATION_LATITUDE].value); } //GUIDE Update properties. defineSwitch(&UsePulseCmdSP); defineNumber(&GuideNSNP); defineNumber(&GuideWENP); // Track Mode (t) is only supported for 2.3+ if (fwInfo.controllerVersion >= 2.3) { CELESTRON_TRACK_MODE mode; if (isSimulation()) { if (isParked()) driver.set_sim_track_mode(TRACKING_OFF); else driver.set_sim_track_mode(TRACK_EQN); } if (driver.get_track_mode(&mode)) { if (mode != TRACKING_OFF) { //IUResetSwitch(&TrackSP); IUResetSwitch(&TrackModeSP); TrackModeS[mode-1].s = ISS_ON; TrackModeSP.s = IPS_OK; // If tracking is ON then mount is NOT parked if (isParked()) SetParked(false); TrackState = SCOPE_TRACKING; } else { LOG_INFO("Mount tracking is off."); TrackState = isParked() ? SCOPE_PARKED : SCOPE_IDLE; } } else TrackModeSP.s = IPS_ALERT; IDSetSwitch(&TrackModeSP, nullptr); } // JM 2014-04-14: User (davidw) reported AVX mount serial communication times out issuing "h" command with firmware 5.28 // Therefore disabling query until it is fixed. // 2017-07-06: Looks like CGE Pro also does not support this if (fwInfo.controllerVersion >= 2.3 && fwInfo.Model != "AVX" && fwInfo.Model != "CGE Pro") { double utc_offset; int yy, dd, mm, hh, minute, ss; if (driver.get_utc_date_time(&utc_offset, &yy, &mm, &dd, &hh, &minute, &ss)) { char isoDateTime[32]; char utcOffset[8]; snprintf(isoDateTime, 32, "%04d-%02d-%02dT%02d:%02d:%02d", yy, mm, dd, hh, minute, ss); snprintf(utcOffset, 8, "%4.2f", utc_offset); IUSaveText(IUFindText(&TimeTP, "UTC"), isoDateTime); IUSaveText(IUFindText(&TimeTP, "OFFSET"), utcOffset); LOGF_INFO("Mount UTC offset is %s. UTC time is %s", utcOffset, isoDateTime); IDSetText(&TimeTP, nullptr); } } else LOG_WARN("Mount does not support retrieval of date and time settings."); } else { INDI::Telescope::updateProperties(); //GUIDE Delete properties. deleteProperty(UsePulseCmdSP.name); deleteProperty(GuideNSNP.name); deleteProperty(GuideWENP.name); //deleteProperty(TrackSP.name); if (fwInfo.Version != "Invalid") deleteProperty(FirmwareTP.name); } return true; } bool CelestronGPS::Goto(double ra, double dec) { targetRA = ra; targetDEC = dec; if (EqNP.s == IPS_BUSY || MovementNSSP.s == IPS_BUSY || MovementWESP.s == IPS_BUSY) { driver.abort(); // sleep for 500 mseconds usleep(500000); } if (driver.slew_radec(targetRA, targetDEC, usePreciseCoords) == false) { LOG_ERROR("Failed to slew telescope in RA/DEC."); return false; } //HorizontalCoordsNP.s = IPS_BUSY; TrackState = SCOPE_SLEWING; char RAStr[32], DecStr[32]; fs_sexa(RAStr, targetRA, 2, 3600); fs_sexa(DecStr, targetDEC, 2, 3600); LOGF_INFO("Slewing to JNOW RA %s - DEC %s", RAStr, DecStr); return true; } bool CelestronGPS::Sync(double ra, double dec) { if (!checkMinVersion(4.1, "sync")) return false; if (driver.sync(ra, dec, usePreciseCoords) == false) { LOG_ERROR("Sync failed."); return false; } currentRA = ra; currentDEC = dec; LOG_INFO("Sync successful."); return true; } /* bool CelestronGPS::GotoAzAlt(double az, double alt) { if (isSimulation()) { ln_hrz_posn horizontalPos; // Libnova south = 0, west = 90, north = 180, east = 270 horizontalPos.az = az + 180; if (horizontalPos.az >= 360) horizontalPos.az -= 360; horizontalPos.alt = alt; ln_lnlat_posn observer; observer.lat = LocationN[LOCATION_LATITUDE].value; observer.lng = LocationN[LOCATION_LONGITUDE].value; if (observer.lng > 180) observer.lng -= 360; ln_equ_posn equatorialPos; ln_get_equ_from_hrz(&horizontalPos, &observer, ln_get_julian_from_sys(), &equatorialPos); targetRA = equatorialPos.ra/15.0; targetDEC = equatorialPos.dec; } if (driver.slew_azalt(LocationN[LOCATION_LATITUDE].value, az, alt) == false) { LOG_ERROR("Failed to slew telescope in Az/Alt."); return false; } targetAZ = az; targetALT= alt; TrackState = SCOPE_SLEWING; HorizontalCoordsNP.s = IPS_BUSY; char AZStr[16], ALTStr[16]; fs_sexa(AZStr, targetAZ, 3, 3600); fs_sexa(ALTStr, targetALT, 2, 3600); LOGF_INFO("Slewing to Az %s - Alt %s", AZStr, ALTStr); return true; } */ bool CelestronGPS::MoveNS(INDI_DIR_NS dir, TelescopeMotionCommand command) { CELESTRON_DIRECTION move = (dir == DIRECTION_NORTH) ? CELESTRON_N : CELESTRON_S; CELESTRON_SLEW_RATE rate = (CELESTRON_SLEW_RATE)IUFindOnSwitchIndex(&SlewRateSP); switch (command) { case MOTION_START: if (driver.start_motion(move, rate) == false) { LOG_ERROR("Error setting N/S motion direction."); return false; } else LOGF_INFO("Moving toward %s.", (move == CELESTRON_N) ? "North" : "South"); break; case MOTION_STOP: if (driver.stop_motion(move) == false) { LOG_ERROR("Error stopping N/S motion."); return false; } else LOGF_INFO("Movement toward %s halted.", (move == CELESTRON_N) ? "North" : "South"); break; } return true; } bool CelestronGPS::MoveWE(INDI_DIR_WE dir, TelescopeMotionCommand command) { CELESTRON_DIRECTION move = (dir == DIRECTION_WEST) ? CELESTRON_W : CELESTRON_E; CELESTRON_SLEW_RATE rate = (CELESTRON_SLEW_RATE)IUFindOnSwitchIndex(&SlewRateSP); switch (command) { case MOTION_START: if (driver.start_motion(move, rate) == false) { LOG_ERROR("Error setting W/E motion direction."); return false; } else LOGF_INFO("Moving toward %s.", (move == CELESTRON_W) ? "West" : "East"); break; case MOTION_STOP: if (driver.stop_motion(move) == false) { LOG_ERROR("Error stopping W/E motion."); return false; } else LOGF_INFO("Movement toward %s halted.", (move == CELESTRON_W) ? "West" : "East"); break; } return true; } bool CelestronGPS::ReadScopeStatus() { if (isSimulation()) mountSim(); if (driver.get_radec(¤tRA, ¤tDEC, usePreciseCoords) == false) { LOG_ERROR("Failed to read RA/DEC values."); return false; } /*if (driver.get_coords_azalt(LocationN[LOCATION_LATITUDE].value, ¤tAZ, ¤tALT) == false) LOG_WARN("Failed to read AZ/ALT values."); else { HorizontalCoordsN[AXIS_AZ].value = currentAZ; HorizontalCoordsN[AXIS_ALT].value = currentALT; }*/ switch (TrackState) { case SCOPE_SLEWING: // are we done? if (driver.is_slewing() == false) { LOG_INFO("Slew complete, tracking..."); TrackState = SCOPE_TRACKING; //HorizontalCoordsNP.s = IPS_OK; } break; case SCOPE_PARKING: // are we done? if (driver.is_slewing() == false) { if (driver.set_track_mode(TRACKING_OFF)) LOG_DEBUG("Mount tracking is off."); SetParked(true); //HorizontalCoordsNP.s = IPS_OK; saveConfig(true); // Check if we need to hibernate if (UseHibernateS[0].s == ISS_ON) { LOG_INFO("Hibernating mount..."); if (driver.hibernate()) LOG_INFO("Mount hibernated. Please disconnect now and turn off your mount."); else LOG_ERROR("Hibernating mount failed!"); } } break; default: break; } //IDSetNumber(&HorizontalCoordsNP, nullptr); NewRaDec(currentRA, currentDEC); return true; } bool CelestronGPS::Abort() { driver.stop_motion(CELESTRON_N); driver.stop_motion(CELESTRON_S); driver.stop_motion(CELESTRON_W); driver.stop_motion(CELESTRON_E); //GUIDE Abort guide operations. if (GuideNSNP.s == IPS_BUSY || GuideWENP.s == IPS_BUSY) { GuideNSNP.s = GuideWENP.s = IPS_IDLE; GuideNSN[0].value = GuideNSN[1].value = 0.0; GuideWEN[0].value = GuideWEN[1].value = 0.0; if (GuideNSTID) { IERmTimer(GuideNSTID); GuideNSTID = 0; } if (GuideWETID) { IERmTimer(GuideWETID); GuideWETID = 0; } LOG_INFO("Guide aborted."); IDSetNumber(&GuideNSNP, nullptr); IDSetNumber(&GuideWENP, nullptr); return true; } return driver.abort(); } bool CelestronGPS::Handshake() { driver.set_device(getDeviceName()); driver.set_port_fd(PortFD); if (isSimulation()) { driver.set_simulation(true); driver.set_sim_slew_rate(SR_5); driver.set_sim_ra(0); driver.set_sim_dec(90); } bool parkDataValid = (LoadParkData() == nullptr); // Check if we need to wake up IF: // 1. Park data exists in ParkData.xml // 2. Mount is currently parked // 3. Hibernate option is enabled if (parkDataValid && isParked() && UseHibernateS[0].s == ISS_ON) { LOG_INFO("Waking up mount..."); if (!driver.wakeup()) { LOG_ERROR("Waking up mount failed! Make sure mount is powered and connected. " "Hibernate requires firmware version >= 5.21"); return false; } } if (driver.check_connection() == false) { LOG_ERROR("Failed to communicate with the mount, check the logs for details."); return false; } return true; } bool CelestronGPS::ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) { if (!strcmp(getDeviceName(), dev)) { // Enable/Disable hibernate if (!strcmp(name, UseHibernateSP.name)) { IUUpdateSwitch(&UseHibernateSP, states, names, n); if (UseHibernateS[0].s == ISS_ON && checkMinVersion(4.22, "hibernation") == false) { UseHibernateS[0].s = ISS_OFF; UseHibernateS[1].s = ISS_ON; UseHibernateSP.s = IPS_ALERT; } else UseHibernateSP.s = IPS_OK; IDSetSwitch(&UseHibernateSP, nullptr); return true; } //GUIDE Pulse-Guide command support if (!strcmp(name, UsePulseCmdSP.name)) { IUResetSwitch(&UsePulseCmdSP); IUUpdateSwitch(&UsePulseCmdSP, states, names, n); UsePulseCmdSP.s = IPS_OK; IDSetSwitch(&UsePulseCmdSP, nullptr); return true; } } return INDI::Telescope::ISNewSwitch(dev, name, states, names, n); } bool CelestronGPS::ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) { //double newAlt=0, newAz=0; if (dev != nullptr && strcmp(dev, getDeviceName()) == 0) { /*if ( !strcmp (name, HorizontalCoordsNP.name) ) { int i=0, nset=0; for (nset = i = 0; i < n; i++) { INumber *horp = IUFindNumber (&HorizontalCoordsNP, names[i]); if (horp == &HorizontalCoordsN[AXIS_AZ]) { newAz = values[i]; nset += newAz >= 0. && newAz <= 360.0; } else if (horp == &HorizontalCoordsN[AXIS_ALT]) { newAlt = values[i]; nset += newAlt >= -90. && newAlt <= 90.0; } } if (nset == 2) { char AzStr[16], AltStr[16]; fs_sexa(AzStr, newAz, 3, 3600); fs_sexa(AltStr, newAlt, 2, 3600); if (GotoAzAlt(newAz, newAlt) == false) { HorizontalCoordsNP.s = IPS_ALERT; LOGF_ERROR("Error slewing to Az: %s Alt: %s", AzStr, AltStr); IDSetNumber(&HorizontalCoordsNP, nullptr); return false; } return true; } else { HorizontalCoordsNP.s = IPS_ALERT; LOG_ERROR("Altitude or Azimuth missing or invalid"); IDSetNumber(&HorizontalCoordsNP, nullptr); return false; } }*/ //GUIDE process Guider properties. processGuiderProperties(name, values, names, n); } INDI::Telescope::ISNewNumber(dev, name, values, names, n); return true; } void CelestronGPS::mountSim() { static struct timeval ltv; struct timeval tv; double dt, dx, da_ra = 0, da_dec = 0; int nlocked; // update elapsed time since last poll, don't presume exactly POLLMS gettimeofday(&tv, nullptr); if (ltv.tv_sec == 0 && ltv.tv_usec == 0) ltv = tv; dt = tv.tv_sec - ltv.tv_sec + (tv.tv_usec - ltv.tv_usec) / 1e6; ltv = tv; if (fabs(targetRA - currentRA) * 15. >= GOTO_LIMIT) da_ra = GOTO_RATE * dt; else if (fabs(targetRA - currentRA) * 15. >= SLEW_LIMIT) da_ra = SLEW_RATE * dt; else da_ra = FINE_SLEW_RATE * dt; if (fabs(targetDEC - currentDEC) >= GOTO_LIMIT) da_dec = GOTO_RATE * dt; else if (fabs(targetDEC - currentDEC) >= SLEW_LIMIT) da_dec = SLEW_RATE * dt; else da_dec = FINE_SLEW_RATE * dt; if (MovementNSSP.s == IPS_BUSY || MovementWESP.s == IPS_BUSY) { int rate = IUFindOnSwitchIndex(&SlewRateSP); switch (rate) { case SLEW_GUIDE: da_ra = FINE_SLEW_RATE * dt * 0.05; da_dec = FINE_SLEW_RATE * dt * 0.05; break; case SLEW_CENTERING: da_ra = FINE_SLEW_RATE * dt * .1; da_dec = FINE_SLEW_RATE * dt * .1; break; case SLEW_FIND: da_ra = SLEW_RATE * dt; da_dec = SLEW_RATE * dt; break; default: da_ra = GOTO_RATE * dt; da_dec = GOTO_RATE * dt; break; } switch (MovementNSSP.s) { case IPS_BUSY: if (MovementNSS[DIRECTION_NORTH].s == ISS_ON) currentDEC += da_dec; else if (MovementNSS[DIRECTION_SOUTH].s == ISS_ON) currentDEC -= da_dec; break; default: break; } switch (MovementWESP.s) { case IPS_BUSY: if (MovementWES[DIRECTION_WEST].s == ISS_ON) currentRA += da_ra / 15.; else if (MovementWES[DIRECTION_EAST].s == ISS_ON) currentRA -= da_ra / 15.; break; default: break; } driver.set_sim_ra(currentRA); driver.set_sim_dec(currentDEC); /*ln_equ_posn equatorialPos; equatorialPos.ra = currentRA * 15; equatorialPos.dec = currentDEC; ln_lnlat_posn observer; observer.lat = LocationN[LOCATION_LATITUDE].value; observer.lng = LocationN[LOCATION_LONGITUDE].value; if (observer.lng > 180) observer.lng -= 360; ln_hrz_posn horizontalPos; ln_get_hrz_from_equ(&equatorialPos, &observer, ln_get_julian_from_sys(), &horizontalPos); // Libnova south = 0, west = 90, north = 180, east = 270 horizontalPos.az -= 180; if (horizontalPos.az < 0) horizontalPos.az += 360; set_sim_az(horizontalPos.az); set_sim_alt(horizontalPos.alt);*/ NewRaDec(currentRA, currentDEC); return; } // Process per current state. We check the state of EQUATORIAL_COORDS and act acoordingly switch (TrackState) { case SCOPE_IDLE: currentRA = driver.get_sim_ra() + (TRACKRATE_SIDEREAL/3600.0 * dt) / 15.0; currentRA = range24(currentRA); break; case SCOPE_SLEWING: case SCOPE_PARKING: // slewing - nail it when both within one pulse @ SLEWRATE nlocked = 0; dx = targetRA - currentRA; // Take shortest path if (fabs(dx) > 12) dx *= -1; if (fabs(dx) <= da_ra) { currentRA = targetRA; nlocked++; } else if (dx > 0) currentRA += da_ra / 15.; else currentRA -= da_ra / 15.; if (currentRA < 0) currentRA += 24; else if (currentRA > 24) currentRA -= 24; dx = targetDEC - currentDEC; if (fabs(dx) <= da_dec) { currentDEC = targetDEC; nlocked++; } else if (dx > 0) currentDEC += da_dec; else currentDEC -= da_dec; if (nlocked == 2) { driver.set_sim_slewing(false); } break; default: break; } driver.set_sim_ra(currentRA); driver.set_sim_dec(currentDEC); /* ln_equ_posn equatorialPos; equatorialPos.ra = currentRA * 15; equatorialPos.dec = currentDEC; ln_lnlat_posn observer; observer.lat = LocationN[LOCATION_LATITUDE].value; observer.lng = LocationN[LOCATION_LONGITUDE].value; if (observer.lng > 180) observer.lng -= 360; ln_hrz_posn horizontalPos; ln_get_hrz_from_equ(&equatorialPos, &observer, ln_get_julian_from_sys(), &horizontalPos); // Libnova south = 0, west = 90, north = 180, east = 270 horizontalPos.az -= 180; if (horizontalPos.az < 0) horizontalPos.az += 360; set_sim_az(horizontalPos.az); set_sim_alt(horizontalPos.alt);*/ } void CelestronGPS::simulationTriggered(bool enable) { driver.set_simulation(enable); } bool CelestronGPS::updateLocation(double latitude, double longitude, double elevation) { INDI_UNUSED(elevation); if (!checkMinVersion(2.3, "updating location")) return false; return driver.set_location(longitude, latitude); } bool CelestronGPS::updateTime(ln_date *utc, double utc_offset) { if (!checkMinVersion(2.3, "updating time")) return false; return (driver.set_datetime(utc, utc_offset)); } bool CelestronGPS::Park() { double parkAZ = GetAxis1Park(); double parkAlt = GetAxis2Park(); char AzStr[16], AltStr[16]; fs_sexa(AzStr, parkAZ, 2, 3600); fs_sexa(AltStr, parkAlt, 2, 3600); LOGF_DEBUG("Parking to Az (%s) Alt (%s)...", AzStr, AltStr); if (driver.slew_azalt(parkAZ, parkAlt, usePreciseCoords)) { TrackState = SCOPE_PARKING; LOG_INFO("Parking is in progress..."); return true; } return false; #if 0 ln_hrz_posn horizontalPos; // Libnova south = 0, west = 90, north = 180, east = 270 horizontalPos.az = parkAZ + 180; if (horizontalPos.az >= 360) horizontalPos.az -= 360; horizontalPos.alt = parkAlt; ln_lnlat_posn observer; observer.lat = LocationN[LOCATION_LATITUDE].value; observer.lng = LocationN[LOCATION_LONGITUDE].value; if (observer.lng > 180) observer.lng -= 360; ln_equ_posn equatorialPos; ln_get_equ_from_hrz(&horizontalPos, &observer, ln_get_julian_from_sys(), &equatorialPos); char RAStr[16], DEStr[16]; fs_sexa(RAStr, equatorialPos.ra / 15.0, 2, 3600); fs_sexa(DEStr, equatorialPos.dec, 2, 3600); LOGF_DEBUG("Parking to RA (%s) DEC (%s)...", RAStr, DEStr); if (Goto(equatorialPos.ra / 15.0, equatorialPos.dec)) { TrackState = SCOPE_PARKING; LOG_INFO("Parking is in progress..."); return true; } else return false; #endif } bool CelestronGPS::UnPark() { // Set tracking mode to whatever it was stored before SetParked(false); loadConfig(true, "TELESCOPE_TRACK_MODE"); return true; #if 0 double parkAZ = GetAxis1Park(); double parkAlt = GetAxis2Park(); char AzStr[16], AltStr[16]; fs_sexa(AzStr, parkAZ, 2, 3600); fs_sexa(AltStr, parkAlt, 2, 3600); LOGF_DEBUG("Unparking from Az (%s) Alt (%s)...", AzStr, AltStr); ln_hrz_posn horizontalPos; // Libnova south = 0, west = 90, north = 180, east = 270 horizontalPos.az = parkAZ + 180; if (horizontalPos.az >= 360) horizontalPos.az -= 360; horizontalPos.alt = parkAlt; ln_lnlat_posn observer; observer.lat = LocationN[LOCATION_LATITUDE].value; observer.lng = LocationN[LOCATION_LONGITUDE].value; if (observer.lng > 180) observer.lng -= 360; ln_equ_posn equatorialPos; ln_get_equ_from_hrz(&horizontalPos, &observer, ln_get_julian_from_sys(), &equatorialPos); char RAStr[16], DEStr[16]; fs_sexa(RAStr, equatorialPos.ra / 15.0, 2, 3600); fs_sexa(DEStr, equatorialPos.dec, 2, 3600); LOGF_DEBUG("Syncing to parked coordinates RA (%s) DEC (%s)...", RAStr, DEStr); if (Sync(equatorialPos.ra / 15.0, equatorialPos.dec)) { SetParked(false); loadConfig(true, "TELESCOPE_TRACK_MODE"); return true; } else return false; #endif } bool CelestronGPS::SetCurrentPark() { // The Goto Alt-Az and Get Alt-Az menu items have been renamed Goto Axis Postn and Get Axis Postn // where Postn is an abbreviation for Position. Since this feature doesn't actually refer // to altitude and azimuth when mounted on a wedge, the new designation is more accurate. // Source : NexStarHandControlVersion4UsersGuide.pdf /*ln_hrz_posn horizontalPos; // Libnova south = 0, west = 90, north = 180, east = 270 ln_lnlat_posn observer; observer.lat = LocationN[LOCATION_LATITUDE].value; observer.lng = LocationN[LOCATION_LONGITUDE].value; if (observer.lng > 180) observer.lng -= 360; ln_equ_posn equatorialPos; equatorialPos.ra = currentRA * 15; equatorialPos.dec = currentDEC; ln_get_hrz_from_equ(&equatorialPos, &observer, ln_get_julian_from_sys(), &horizontalPos); double parkAZ = horizontalPos.az - 180; if (parkAZ < 0) parkAZ += 360; double parkAlt = horizontalPos.alt;*/ if (driver.get_azalt(¤tAZ, ¤tALT, usePreciseCoords) == false) { LOG_ERROR("Failed to read AZ/ALT values."); return false; } double parkAZ = currentAZ; double parkAlt = currentALT; char AzStr[16], AltStr[16]; fs_sexa(AzStr, parkAZ, 2, 3600); fs_sexa(AltStr, parkAlt, 2, 3600); LOGF_DEBUG("Setting current parking position to coordinates Az (%s) Alt (%s)...", AzStr, AltStr); SetAxis1Park(parkAZ); SetAxis2Park(parkAlt); return true; } bool CelestronGPS::SetDefaultPark() { // The Goto Alt-Az and Get Alt-Az menu items have been renamed Goto Axis Postn and Get Axis Postn // where Postn is an abbreviation for Position. Since this feature doesn't actually refer // to altitude and azimuth when mounted on a wedge, the new designation is more accurate. // Source : NexStarHandControlVersion4UsersGuide.pdf // By default azimuth 90° ( hemisphere doesn't matter) SetAxis1Park(90); // Altitude = 90° (latitude doesn't matter) SetAxis2Park(90); return true; } bool CelestronGPS::saveConfigItems(FILE *fp) { INDI::Telescope::saveConfigItems(fp); IUSaveConfigSwitch(fp, &UseHibernateSP); //IUSaveConfigSwitch(fp, &TrackSP); IUSaveConfigSwitch(fp, &UsePulseCmdSP); return true; } bool CelestronGPS::setTrackMode(CELESTRON_TRACK_MODE mode) { if (driver.set_track_mode(mode)) { TrackState = (mode == TRACKING_OFF) ? SCOPE_IDLE : SCOPE_TRACKING; LOGF_DEBUG("Tracking mode set to %s.", TrackModeS[mode - 1].label); return true; } return false; } //GUIDE Guiding functions. IPState CelestronGPS::GuideNorth(float ms) { LOGF_DEBUG("GUIDE CMD: N %.0f ms", ms); int use_pulse_cmd; use_pulse_cmd = IUFindOnSwitchIndex(&UsePulseCmdSP); if (!use_pulse_cmd && (MovementNSSP.s == IPS_BUSY || MovementWESP.s == IPS_BUSY)) { LOG_ERROR("Cannot guide while moving."); return IPS_ALERT; } // If already moving (no pulse command), then stop movement if (MovementNSSP.s == IPS_BUSY) { int dir = IUFindOnSwitchIndex(&MovementNSSP); MoveNS(dir == 0 ? DIRECTION_NORTH : DIRECTION_SOUTH, MOTION_STOP); } if (GuideNSTID) { IERmTimer(GuideNSTID); GuideNSTID = 0; } if (use_pulse_cmd) { driver.send_pulse(CELESTRON_N, 50, ms / 10.0); } else { MovementNSS[0].s = ISS_ON; MoveNS(DIRECTION_NORTH, MOTION_START); } // Set slew to guiding IUResetSwitch(&SlewRateSP); SlewRateS[SLEW_GUIDE].s = ISS_ON; IDSetSwitch(&SlewRateSP, nullptr); guide_direction = CELESTRON_N; GuideNSTID = IEAddTimer(ms, guideTimeoutHelperN, this); return IPS_BUSY; } IPState CelestronGPS::GuideSouth(float ms) { LOGF_DEBUG("GUIDE CMD: S %.0f ms", ms); int use_pulse_cmd; use_pulse_cmd = IUFindOnSwitchIndex(&UsePulseCmdSP); if (!use_pulse_cmd && (MovementNSSP.s == IPS_BUSY || MovementWESP.s == IPS_BUSY)) { LOG_ERROR("Cannot guide while moving."); return IPS_ALERT; } // If already moving (no pulse command), then stop movement if (MovementNSSP.s == IPS_BUSY) { int dir = IUFindOnSwitchIndex(&MovementNSSP); MoveNS(dir == 0 ? DIRECTION_NORTH : DIRECTION_SOUTH, MOTION_STOP); } if (GuideNSTID) { IERmTimer(GuideNSTID); GuideNSTID = 0; } if (use_pulse_cmd) { driver.send_pulse(CELESTRON_S, 50, ms / 10.0); } else { MovementNSS[1].s = ISS_ON; MoveNS(DIRECTION_SOUTH, MOTION_START); } // Set slew to guiding IUResetSwitch(&SlewRateSP); SlewRateS[SLEW_GUIDE].s = ISS_ON; IDSetSwitch(&SlewRateSP, nullptr); guide_direction = CELESTRON_S; GuideNSTID = IEAddTimer(ms, guideTimeoutHelperS, this); return IPS_BUSY; } IPState CelestronGPS::GuideEast(float ms) { LOGF_DEBUG("GUIDE CMD: E %.0f ms", ms); int use_pulse_cmd; use_pulse_cmd = IUFindOnSwitchIndex(&UsePulseCmdSP); if (!use_pulse_cmd && (MovementNSSP.s == IPS_BUSY || MovementWESP.s == IPS_BUSY)) { LOG_ERROR("Cannot guide while moving."); return IPS_ALERT; } // If already moving (no pulse command), then stop movement if (MovementWESP.s == IPS_BUSY) { int dir = IUFindOnSwitchIndex(&MovementWESP); MoveWE(dir == 0 ? DIRECTION_WEST : DIRECTION_EAST, MOTION_STOP); } if (GuideWETID) { IERmTimer(GuideWETID); GuideWETID = 0; } if (use_pulse_cmd) { driver.send_pulse(CELESTRON_E, 50, ms / 10.0); } else { MovementWES[1].s = ISS_ON; MoveWE(DIRECTION_EAST, MOTION_START); } // Set slew to guiding IUResetSwitch(&SlewRateSP); SlewRateS[SLEW_GUIDE].s = ISS_ON; IDSetSwitch(&SlewRateSP, nullptr); guide_direction = CELESTRON_E; GuideWETID = IEAddTimer(ms, guideTimeoutHelperE, this); return IPS_BUSY; } IPState CelestronGPS::GuideWest(float ms) { LOGF_DEBUG("GUIDE CMD: W %.0f ms", ms); int use_pulse_cmd; use_pulse_cmd = IUFindOnSwitchIndex(&UsePulseCmdSP); if (!use_pulse_cmd && (MovementNSSP.s == IPS_BUSY || MovementWESP.s == IPS_BUSY)) { LOG_ERROR("Cannot guide while moving."); return IPS_ALERT; } // If already moving (no pulse command), then stop movement if (MovementWESP.s == IPS_BUSY) { int dir = IUFindOnSwitchIndex(&MovementWESP); MoveWE(dir == 0 ? DIRECTION_WEST : DIRECTION_EAST, MOTION_STOP); } if (GuideWETID) { IERmTimer(GuideWETID); GuideWETID = 0; } if (use_pulse_cmd) { driver.send_pulse(CELESTRON_W, 50, ms / 10.0); } else { MovementWES[0].s = ISS_ON; MoveWE(DIRECTION_WEST, MOTION_START); } // Set slew to guiding IUResetSwitch(&SlewRateSP); SlewRateS[SLEW_GUIDE].s = ISS_ON; IDSetSwitch(&SlewRateSP, nullptr); guide_direction = CELESTRON_W; GuideWETID = IEAddTimer(ms, guideTimeoutHelperW, this); return IPS_BUSY; } //GUIDE The timer helper functions. void CelestronGPS::guideTimeoutHelperN(void *p) { ((CelestronGPS *)p)->guideTimeout(CELESTRON_N); } void CelestronGPS::guideTimeoutHelperS(void *p) { ((CelestronGPS *)p)->guideTimeout(CELESTRON_S); } void CelestronGPS::guideTimeoutHelperW(void *p) { ((CelestronGPS *)p)->guideTimeout(CELESTRON_W); } void CelestronGPS::guideTimeoutHelperE(void *p) { ((CelestronGPS *)p)->guideTimeout(CELESTRON_E); } //GUIDE The timer function /* Here I splitted the behaviour depending upon the direction * of the guide command which generates the timer; this was * done because the member variable "guide_direction" could * be modified by a pulse command on the other axis BEFORE * the calling pulse command is terminated. */ void CelestronGPS::guideTimeout(CELESTRON_DIRECTION calldir) { int use_pulse_cmd; use_pulse_cmd = IUFindOnSwitchIndex(&UsePulseCmdSP); //LOG_DEBUG(" END-OF-TIMER"); //LOGF_DEBUG(" USE_PULSE_CMD = %i", use_pulse_cmd); //LOGF_DEBUG(" GUIDE_DIRECTION = %i", (int)guide_direction); //LOGF_DEBUG(" CALL_DIRECTION = %i", calldir); // if (guide_direction == -1) // { // driver.stop_motion(CELESTRON_N); // driver.stop_motion(CELESTRON_S); // driver.stop_motion(CELESTRON_E); // driver.stop_motion(CELESTRON_W); // // MovementNSSP.s = IPS_IDLE; // MovementWESP.s = IPS_IDLE; // IUResetSwitch(&MovementNSSP); // IUResetSwitch(&MovementWESP); // IDSetSwitch(&MovementNSSP, nullptr); // IDSetSwitch(&MovementWESP, nullptr); // IERmTimer(GuideNSTID); // IERmTimer(GuideWETID); // } else if (!use_pulse_cmd) { if (calldir == CELESTRON_N || calldir == CELESTRON_S) { MoveNS(calldir == CELESTRON_N ? DIRECTION_NORTH : DIRECTION_SOUTH, MOTION_STOP); if (calldir == CELESTRON_N) GuideNSNP.np[0].value = 0; else GuideNSNP.np[1].value = 0; GuideNSNP.s = IPS_IDLE; IDSetNumber(&GuideNSNP, nullptr); MovementNSSP.s = IPS_IDLE; IUResetSwitch(&MovementNSSP); IDSetSwitch(&MovementNSSP, nullptr); } if (calldir == CELESTRON_W || calldir == CELESTRON_E) { MoveWE(calldir == CELESTRON_W ? DIRECTION_WEST : DIRECTION_EAST, MOTION_STOP); if (calldir == CELESTRON_W) GuideWENP.np[0].value = 0; else GuideWENP.np[1].value = 0; GuideWENP.s = IPS_IDLE; IDSetNumber(&GuideWENP, nullptr); MovementWESP.s = IPS_IDLE; IUResetSwitch(&MovementWESP); IDSetSwitch(&MovementWESP, nullptr); } } //LOG_DEBUG(" CALL SendPulseStatusCmd"); bool pulseguide_state; if (!driver.get_pulse_status(calldir, pulseguide_state)) LOG_ERROR("PULSE STATUS UNDETERMINED"); else if (pulseguide_state) LOG_WARN("PULSE STILL IN PROGRESS, POSSIBLE MOUNT JAM."); if (calldir == CELESTRON_N || calldir == CELESTRON_S) { GuideNSNP.np[0].value = 0; GuideNSNP.np[1].value = 0; GuideNSNP.s = IPS_IDLE; GuideNSTID = 0; IDSetNumber(&GuideNSNP, nullptr); } if (calldir == CELESTRON_W || calldir == CELESTRON_E) { GuideWENP.np[0].value = 0; GuideWENP.np[1].value = 0; GuideWENP.s = IPS_IDLE; GuideWETID = 0; IDSetNumber(&GuideWENP, nullptr); } //LOG_WARN("GUIDE CMD COMPLETED"); } bool CelestronGPS::SetTrackMode(uint8_t mode) { return setTrackMode(static_cast(mode+1)); } bool CelestronGPS::SetTrackEnabled(bool enabled) { return setTrackMode(enabled ? static_cast(IUFindOnSwitchIndex(&TrackModeSP)+1) : TRACKING_OFF); } libindi/drivers/telescope/lx200_16.h0000664000175000017500000000332113263645557016456 0ustar jasemjasem/* LX200 16" Copyright (C) 2003 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 */ #pragma once #include "lx200gps.h" class LX200_16 : public LX200GPS { public: LX200_16(); ~LX200_16() {} const char *getDefaultName(); bool initProperties(); bool updateProperties(); void ISGetProperties(const char *dev); bool ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n); bool ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n); bool ReadScopeStatus(); void getBasicData(); bool handleAltAzSlew(); protected: ISwitchVectorProperty FieldDeRotatorSP; ISwitch FieldDeRotatorS[2]; ISwitchVectorProperty HomeSearchSP; ISwitch HomeSearchS[2]; ISwitchVectorProperty FanStatusSP; ISwitch FanStatusS[2]; INumberVectorProperty HorizontalCoordsNP; INumber HorizontalCoordsN[2]; private: double targetAZ, targetALT; double currentAZ, currentALT; }; libindi/drivers/telescope/skycommander.cpp0000664000175000017500000000671113263645557020340 0ustar jasemjasem/******************************************************************************* Copyright(c) 2017 Jasem Mutlaq. All rights reserved. Simple SkyCommander DSC Driver This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2 as published by the Free Software Foundation. . 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 Library General Public License for more details. . You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. *******************************************************************************/ #include "skycommander.h" #include "indicom.h" #include #include #define SKYCOMMANDER_TIMEOUT 3 // We declare an auto pointer to SkyCommander. std::unique_ptr skycommander(new SkyCommander()); void ISGetProperties(const char *dev) { skycommander->ISGetProperties(dev); } void ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) { skycommander->ISNewSwitch(dev, name, states, names, n); } void ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n) { skycommander->ISNewText(dev, name, texts, names, n); } void ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) { skycommander->ISNewNumber(dev, name, values, names, n); } void ISNewBLOB(const char *dev, const char *name, int sizes[], int blobsizes[], char *blobs[], char *formats[], char *names[], int n) { INDI_UNUSED(dev); INDI_UNUSED(name); INDI_UNUSED(sizes); INDI_UNUSED(blobsizes); INDI_UNUSED(blobs); INDI_UNUSED(formats); INDI_UNUSED(names); INDI_UNUSED(n); } void ISSnoopDevice(XMLEle *root) { skycommander->ISSnoopDevice(root); } SkyCommander::SkyCommander() { SetTelescopeCapability(0, 0); } const char *SkyCommander::getDefaultName() { return (const char *)"SkyCommander"; } bool SkyCommander::Handshake() { return true; } bool SkyCommander::ReadScopeStatus() { char CR[1] = { 0x0D }; int rc = 0, nbytes_read = 0, nbytes_written = 0; LOGF_DEBUG("CMD: %#02X", CR[0]); tcflush(PortFD, TCIFLUSH); if ((rc = tty_write(PortFD, CR, 1, &nbytes_written)) != TTY_OK) { char errmsg[256]; tty_error_msg(rc, errmsg, 256); LOGF_ERROR("Error writing to SkyCommander %s (%d)", errmsg, rc); return false; } char coords[16]; if ((rc = tty_read(PortFD, coords, 16, SKYCOMMANDER_TIMEOUT, &nbytes_read)) != TTY_OK) { char errmsg[256]; tty_error_msg(rc, errmsg, 256); LOGF_ERROR("Error reading from SkyCommander %s (%d)", errmsg, rc); return false; } LOGF_DEBUG("RES: %s", coords); float RA = 0.0, DEC = 0.0; nbytes_read = sscanf(coords, " %g %g", &RA, &DEC); if (nbytes_read < 2) { LOGF_ERROR("Error in Sky commander number format (%s).", coords); return false; } char RAStr[64], DecStr[64]; fs_sexa(RAStr, RA, 2, 3600); fs_sexa(DecStr, DEC, 2, 3600); LOGF_DEBUG("Current RA: %s Current DEC: %s", RAStr, DecStr); NewRaDec(RA, DEC); return true; } libindi/drivers/telescope/lx200driver.cpp0000664000175000017500000012600613263645557017725 0ustar jasemjasem#if 0 LX200 Driver Copyright (C) 2003 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 "lx200driver.h" #include "indicom.h" #include "indilogger.h" #include #include #ifndef _WIN32 #include #endif #define LX200_TIMEOUT 5 /* FD timeout in seconds */ #define RB_MAX_LEN 64 int controller_format; char lx200Name[MAXINDIDEVICE]; unsigned int DBG_SCOPE; void setLX200Debug(const char *deviceName, unsigned int debug_level) { strncpy(lx200Name, deviceName, MAXINDIDEVICE); DBG_SCOPE = debug_level; } /************************************************************************** Diagnostics **************************************************************************/ char ACK(int fd); int check_lx200_connection(int fd); /************************************************************************** Get Commands: store data in the supplied buffer. Return 0 on success or -1 on failure **************************************************************************/ /* Get Double from Sexagisemal */ int getCommandSexa(int fd, double *value, const char *cmd); /* Get String */ int getCommandString(int fd, char *data, const char *cmd); /* Get Int */ int getCommandInt(int fd, int *value, const char *cmd); /* Get tracking frequency */ int getTrackFreq(int fd, double *value); /* Get site Latitude */ int getSiteLatitude(int fd, int *dd, int *mm); /* Get site Longitude */ int getSiteLongitude(int fd, int *ddd, int *mm); /* Get Calender data */ int getCalendarDate(int fd, char *date); /* Get site Name */ int getSiteName(int fd, char *siteName, int siteNum); /* Get Home Search Status */ int getHomeSearchStatus(int fd, int *status); /* Get OTA Temperature */ int getOTATemp(int fd, double *value); /* Get time format: 12 or 24 */ int getTimeFormat(int fd, int *format); /************************************************************************** Set Commands **************************************************************************/ /* Set Int */ int setCommandInt(int fd, int data, const char *cmd); /* Set Sexagesimal */ int setCommandXYZ(int fd, int x, int y, int z, const char *cmd); /* Common routine for Set commands */ int setStandardProcedure(int fd, const char *writeData); /* Set Slew Mode */ int setSlewMode(int fd, int slewMode); /* Set Alignment mode */ int setAlignmentMode(int fd, unsigned int alignMode); /* Set Object RA */ int setObjectRA(int fd, double ra); /* set Object DEC */ int setObjectDEC(int fd, double dec); /* Set Calendar date */ int setCalenderDate(int fd, int dd, int mm, int yy); /* Set UTC offset */ int setUTCOffset(int fd, double hours); /* Set Track Freq */ int setTrackFreq(int fd, double trackF); /* Set current site longitude */ int setSiteLongitude(int fd, double Long); /* Set current site latitude */ int setSiteLatitude(int fd, double Lat); /* Set Object Azimuth */ int setObjAz(int fd, double az); /* Set Object Altitude */ int setObjAlt(int fd, double alt); /* Set site name */ int setSiteName(int fd, char *siteName, int siteNum); /* Set maximum slew rate */ int setMaxSlewRate(int fd, int slewRate); /* Set focuser motion */ int setFocuserMotion(int fd, int motionType); /* Set focuser speed mode */ int setFocuserSpeedMode(int fd, int speedMode); /* Set minimum elevation limit */ int setMinElevationLimit(int fd, int min); /* Set maximum elevation limit */ int setMaxElevationLimit(int fd, int max); /************************************************************************** Motion Commands **************************************************************************/ /* Slew to the selected coordinates */ int Slew(int fd); /* Synchronize to the selected coordinates and return the matching object if any */ int Sync(int fd, char *matchedObject); /* Abort slew in all axes */ int abortSlew(int fd); /* Move into one direction, two valid directions can be stacked */ int MoveTo(int fd, int direction); /* Half movement in a particular direction */ int HaltMovement(int fd, int direction); /* Select the tracking mode */ int selectTrackingMode(int fd, int trackMode); /* Send Pulse-Guide command (timed guide move), two valid directions can be stacked */ int SendPulseCmd(int fd, int direction, int duration_msec); /************************************************************************** Other Commands **************************************************************************/ /* Determines LX200 RA/DEC format, tries to set to long if found short */ int checkLX200Format(int fd); /* return the controller_format enum value */ int getLX200Format(); /* Select a site from the LX200 controller */ int selectSite(int fd, int siteNum); /* Select a catalog object */ int selectCatalogObject(int fd, int catalog, int NNNN); /* Select a sub catalog */ int selectSubCatalog(int fd, int catalog, int subCatalog); int check_lx200_connection(int in_fd) { const struct timespec timeout = {0, 50000000L}; int i = 0; char ack[1] = { (char)0x06 }; char MountAlign[64]; int nbytes_read = 0; DEBUGDEVICE(lx200Name, INDI::Logger::DBG_DEBUG, "Testing telescope connection using ACK..."); if (in_fd <= 0) return -1; for (i = 0; i < 2; i++) { if (write(in_fd, ack, 1) < 0) return -1; tty_read(in_fd, MountAlign, 1, LX200_TIMEOUT, &nbytes_read); if (nbytes_read == 1) { DEBUGDEVICE(lx200Name, INDI::Logger::DBG_DEBUG, "Testing successful!"); return 0; } nanosleep(&timeout, NULL); } DEBUGDEVICE(lx200Name, INDI::Logger::DBG_DEBUG, "Failure. Telescope is not responding to ACK!"); return -1; } /********************************************************************** * GET **********************************************************************/ char ACK(int fd) { DEBUGFDEVICE(lx200Name, DBG_SCOPE, "<%s>", __FUNCTION__); char ack[1] = { (char)0x06 }; char MountAlign[2]; int nbytes_write = 0, nbytes_read = 0, error_type; DEBUGFDEVICE(lx200Name, DBG_SCOPE, "CMD <%#02X>", ack[0]); nbytes_write = write(fd, ack, 1); if (nbytes_write < 0) return -1; error_type = tty_read(fd, MountAlign, 1, LX200_TIMEOUT, &nbytes_read); DEBUGFDEVICE(lx200Name, DBG_SCOPE, "RES <%c>", MountAlign[0]); if (nbytes_read == 1) return MountAlign[0]; else return error_type; } int getCommandSexa(int fd, double *value, const char *cmd) { char read_buffer[RB_MAX_LEN]={0}; int error_type; int nbytes_write = 0, nbytes_read = 0; tcflush(fd, TCIFLUSH); DEBUGFDEVICE(lx200Name, DBG_SCOPE, "CMD <%s>", cmd); if ((error_type = tty_write_string(fd, cmd, &nbytes_write)) != TTY_OK) return error_type; error_type = tty_read_section(fd, read_buffer, '#', LX200_TIMEOUT, &nbytes_read); tcflush(fd, TCIFLUSH); if (error_type != TTY_OK) return error_type; read_buffer[nbytes_read - 1] = '\0'; DEBUGFDEVICE(lx200Name, DBG_SCOPE, "RES <%s>", read_buffer); if (f_scansexa(read_buffer, value)) { DEBUGDEVICE(lx200Name, DBG_SCOPE, "Unable to parse response"); return -1; } DEBUGFDEVICE(lx200Name, DBG_SCOPE, "VAL [%g]", *value); tcflush(fd, TCIFLUSH); return 0; } int getCommandInt(int fd, int *value, const char *cmd) { char read_buffer[RB_MAX_LEN]={0}; float temp_number; int error_type; int nbytes_write = 0, nbytes_read = 0; tcflush(fd, TCIFLUSH); DEBUGFDEVICE(lx200Name, DBG_SCOPE, "CMD <%s>", cmd); if ((error_type = tty_write_string(fd, cmd, &nbytes_write)) != TTY_OK) return error_type; error_type = tty_read_section(fd, read_buffer, '#', LX200_TIMEOUT, &nbytes_read); tcflush(fd, TCIFLUSH); if (error_type != TTY_OK) return error_type; read_buffer[nbytes_read - 1] = '\0'; DEBUGFDEVICE(lx200Name, DBG_SCOPE, "RES <%s>", read_buffer); /* Float */ if (strchr(read_buffer, '.')) { if (sscanf(read_buffer, "%f", &temp_number) != 1) return -1; *value = (int)temp_number; } /* Int */ else if (sscanf(read_buffer, "%d", value) != 1) return -1; DEBUGFDEVICE(lx200Name, DBG_SCOPE, "VAL [%d]", *value); return 0; } int getCommandString(int fd, char *data, const char *cmd) { char *term; int error_type; int nbytes_write = 0, nbytes_read = 0; DEBUGFDEVICE(lx200Name, DBG_SCOPE, "CMD <%s>", cmd); if ((error_type = tty_write_string(fd, cmd, &nbytes_write)) != TTY_OK) return error_type; error_type = tty_read_section(fd, data, '#', LX200_TIMEOUT, &nbytes_read); tcflush(fd, TCIFLUSH); if (error_type != TTY_OK) return error_type; term = strchr(data, '#'); if (term) *term = '\0'; DEBUGFDEVICE(lx200Name, DBG_SCOPE, "RES <%s>", data); return 0; } int isSlewComplete(int fd) { DEBUGFDEVICE(lx200Name, DBG_SCOPE, "<%s>", __FUNCTION__); char data[8] = { 0 }; int error_type; int nbytes_write = 0, nbytes_read = 0; const char *cmd = ":D#"; DEBUGFDEVICE(lx200Name, DBG_SCOPE, "CMD <%s>", cmd); if ((error_type = tty_write_string(fd, cmd, &nbytes_write)) != TTY_OK) return error_type; error_type = tty_read_section(fd, data, '#', LX200_TIMEOUT, &nbytes_read); tcflush(fd, TCIOFLUSH); if (error_type != TTY_OK) return error_type; DEBUGFDEVICE(lx200Name, DBG_SCOPE, "RES <%s>", data); if (data[0] == '#') return 1; else return 0; } int getCalendarDate(int fd, char *date) { DEBUGFDEVICE(lx200Name, DBG_SCOPE, "<%s>", __FUNCTION__); int dd, mm, yy, YYYY; int error_type; int nbytes_read = 0; char mell_prefix[3]={0}; int len = 0; if ((error_type = getCommandString(fd, date, ":GC#"))) return error_type; len = strnlen(date, 32); if (len == 10) { /* 10Micron Ultra Precision mode calendar date format is YYYY-MM-DD */ nbytes_read = sscanf(date, "%4d-%2d-%2d", &YYYY, &mm, &dd); if (nbytes_read < 3) return -1; /* We're done, date is already in ISO format */ } else { /* Meade format is MM/DD/YY */ nbytes_read = sscanf(date, "%d%*c%d%*c%d", &mm, &dd, &yy); if (nbytes_read < 3) return -1; /* We consider years 50 or more to be in the last century, anything less in the 21st century.*/ if (yy > 50) strncpy(mell_prefix, "19", 3); else strncpy(mell_prefix, "20", 3); /* We need to have it in YYYY-MM-DD ISO format */ snprintf(date, 32, "%s%02d-%02d-%02d", mell_prefix, yy, mm, dd); } return (0); } int getTimeFormat(int fd, int *format) { DEBUGFDEVICE(lx200Name, DBG_SCOPE, "<%s>", __FUNCTION__); char read_buffer[RB_MAX_LEN]={0}; char formatString[6] = {0}; int error_type; int nbytes_write = 0, nbytes_read = 0; int tMode; DEBUGFDEVICE(lx200Name, DBG_SCOPE, "CMD <%s>", ":Gc#"); if ((error_type = tty_write_string(fd, ":Gc#", &nbytes_write)) != TTY_OK) return error_type; if ((error_type = tty_read_section(fd, read_buffer, '#', LX200_TIMEOUT, &nbytes_read)) != TTY_OK) return error_type; tcflush(fd, TCIFLUSH); if (nbytes_read < 1) return error_type; read_buffer[nbytes_read - 1] = '\0'; DEBUGFDEVICE(lx200Name, DBG_SCOPE, "RES <%s>", read_buffer); // The Losmandy Gemini puts () around it's time format if (strstr(read_buffer, "(")) strcpy(formatString, "(%d)"); else strcpy(formatString, "%d"); nbytes_read = sscanf(read_buffer, formatString, &tMode); if (nbytes_read < 1) return -1; else *format = tMode; return 0; } int getSiteName(int fd, char *siteName, int siteNum) { DEBUGFDEVICE(lx200Name, DBG_SCOPE, "<%s>", __FUNCTION__); char *term; int error_type; int nbytes_write = 0, nbytes_read = 0; switch (siteNum) { case 1: DEBUGFDEVICE(lx200Name, DBG_SCOPE, "CMD <%s>", ":GM#"); if ((error_type = tty_write_string(fd, ":GM#", &nbytes_write)) != TTY_OK) return error_type; break; case 2: DEBUGFDEVICE(lx200Name, DBG_SCOPE, "CMD <%s>", ":GN#"); if ((error_type = tty_write_string(fd, ":GN#", &nbytes_write)) != TTY_OK) return error_type; break; case 3: DEBUGFDEVICE(lx200Name, DBG_SCOPE, "CMD <%s>", ":GO#"); if ((error_type = tty_write_string(fd, ":GO#", &nbytes_write)) != TTY_OK) return error_type; break; case 4: DEBUGFDEVICE(lx200Name, DBG_SCOPE, "CMD <%s>", ":GP#"); if ((error_type = tty_write_string(fd, ":GP#", &nbytes_write)) != TTY_OK) return error_type; break; default: return -1; } error_type = tty_read_section(fd, siteName, '#', LX200_TIMEOUT, &nbytes_read); tcflush(fd, TCIFLUSH); if (nbytes_read < 1) return error_type; siteName[nbytes_read - 1] = '\0'; DEBUGFDEVICE(lx200Name, DBG_SCOPE, "RES <%s>", siteName); term = strchr(siteName, ' '); if (term) *term = '\0'; term = strchr(siteName, '<'); if (term) strcpy(siteName, "unused site"); DEBUGFDEVICE(lx200Name, INDI::Logger::DBG_DEBUG, "Site Name <%s>", siteName); return 0; } int getSiteLatitude(int fd, int *dd, int *mm) { DEBUGFDEVICE(lx200Name, DBG_SCOPE, "<%s>", __FUNCTION__); char read_buffer[RB_MAX_LEN]={0}; int error_type; int nbytes_write = 0, nbytes_read = 0; DEBUGFDEVICE(lx200Name, DBG_SCOPE, "CMD <%s>", ":Gt#"); tcflush(fd, TCIFLUSH); if ((error_type = tty_write_string(fd, ":Gt#", &nbytes_write)) != TTY_OK) return error_type; error_type = tty_read_section(fd, read_buffer, '#', LX200_TIMEOUT, &nbytes_read); tcflush(fd, TCIFLUSH); if (nbytes_read < 1) return error_type; read_buffer[nbytes_read - 1] = '\0'; DEBUGFDEVICE(lx200Name, DBG_SCOPE, "RES <%s>", read_buffer); if (sscanf(read_buffer, "%d%*c%d", dd, mm) < 2) { DEBUGDEVICE(lx200Name, DBG_SCOPE, "Unable to parse response"); return -1; } DEBUGFDEVICE(lx200Name, DBG_SCOPE, "VAL [%d,%d]", *dd, *mm); return 0; } int getSiteLongitude(int fd, int *ddd, int *mm) { DEBUGFDEVICE(lx200Name, DBG_SCOPE, "<%s>", __FUNCTION__); char read_buffer[RB_MAX_LEN]={0}; int error_type; int nbytes_write = 0, nbytes_read = 0; DEBUGFDEVICE(lx200Name, DBG_SCOPE, "CMD <%s>", ":Gg#"); if ((error_type = tty_write_string(fd, ":Gg#", &nbytes_write)) != TTY_OK) return error_type; error_type = tty_read_section(fd, read_buffer, '#', LX200_TIMEOUT, &nbytes_read); tcflush(fd, TCIFLUSH); if (nbytes_read < 1) return error_type; read_buffer[nbytes_read - 1] = '\0'; DEBUGFDEVICE(lx200Name, DBG_SCOPE, "RES <%s>", read_buffer); if (sscanf(read_buffer, "%d%*c%d", ddd, mm) < 2) { DEBUGDEVICE(lx200Name, DBG_SCOPE, "Unable to parse response"); return -1; } DEBUGFDEVICE(lx200Name, DBG_SCOPE, "VAL [%d,%d]", *ddd, *mm); return 0; } int getTrackFreq(int fd, double *value) { DEBUGFDEVICE(lx200Name, DBG_SCOPE, "<%s>", __FUNCTION__); float Freq; char read_buffer[RB_MAX_LEN]={0}; int error_type; int nbytes_write = 0, nbytes_read = 0; DEBUGFDEVICE(lx200Name, DBG_SCOPE, "CMD <%s>", ":GT#"); if ((error_type = tty_write_string(fd, ":GT#", &nbytes_write)) != TTY_OK) return error_type; error_type = tty_read_section(fd, read_buffer, '#', LX200_TIMEOUT, &nbytes_read); tcflush(fd, TCIFLUSH); if (nbytes_read < 1) return error_type; read_buffer[nbytes_read] = '\0'; DEBUGFDEVICE(lx200Name, DBG_SCOPE, "RES <%s>", read_buffer); if (sscanf(read_buffer, "%f#", &Freq) < 1) { DEBUGDEVICE(lx200Name, DBG_SCOPE, "Unable to parse response"); return -1; } *value = (double)Freq; DEBUGFDEVICE(lx200Name, DBG_SCOPE, "VAL [%g]", *value); return 0; } int getHomeSearchStatus(int fd, int *status) { DEBUGFDEVICE(lx200Name, DBG_SCOPE, "<%s>", __FUNCTION__); char read_buffer[RB_MAX_LEN]={0}; int error_type; int nbytes_write = 0, nbytes_read = 0; DEBUGFDEVICE(lx200Name, DBG_SCOPE, "CMD <%s>", ":h?#"); if ((error_type = tty_write_string(fd, ":h?#", &nbytes_write)) != TTY_OK) return error_type; error_type = tty_read_section(fd, read_buffer, '#', LX200_TIMEOUT, &nbytes_read); tcflush(fd, TCIFLUSH); if (nbytes_read < 1) return error_type; read_buffer[1] = '\0'; DEBUGFDEVICE(lx200Name, DBG_SCOPE, "RES <%s>", read_buffer); if (read_buffer[0] == '0') *status = 0; else if (read_buffer[0] == '1') *status = 1; else if (read_buffer[0] == '2') *status = 1; DEBUGFDEVICE(lx200Name, DBG_SCOPE, "VAL [%d]", *status); return 0; } int getOTATemp(int fd, double *value) { DEBUGFDEVICE(lx200Name, DBG_SCOPE, "<%s>", __FUNCTION__); char read_buffer[RB_MAX_LEN]={0}; int error_type; int nbytes_write = 0, nbytes_read = 0; float temp; DEBUGFDEVICE(lx200Name, DBG_SCOPE, "CMD <%s>", ":fT#"); if ((error_type = tty_write_string(fd, ":fT#", &nbytes_write)) != TTY_OK) return error_type; error_type = tty_read_section(fd, read_buffer, '#', LX200_TIMEOUT, &nbytes_read); if (nbytes_read < 1) return error_type; read_buffer[nbytes_read - 1] = '\0'; DEBUGFDEVICE(lx200Name, DBG_SCOPE, "RES <%s>", read_buffer); if (sscanf(read_buffer, "%f", &temp) < 1) { DEBUGDEVICE(lx200Name, DBG_SCOPE, "Unable to parse response"); return -1; } *value = (double)temp; DEBUGFDEVICE(lx200Name, DBG_SCOPE, "VAL [%g]", *value); return 0; } /********************************************************************** * SET **********************************************************************/ int setStandardProcedure(int fd, const char *data) { char bool_return[2]; int error_type; int nbytes_write = 0, nbytes_read = 0; DEBUGFDEVICE(lx200Name, DBG_SCOPE, "CMD <%s>", data); tcflush(fd, TCIFLUSH); if ((error_type = tty_write_string(fd, data, &nbytes_write)) != TTY_OK) return error_type; error_type = tty_read(fd, bool_return, 1, LX200_TIMEOUT, &nbytes_read); tcflush(fd, TCIFLUSH); if (nbytes_read < 1) return error_type; if (bool_return[0] == '0') { DEBUGFDEVICE(lx200Name, DBG_SCOPE, "CMD <%s> failed.", data); return -1; } DEBUGFDEVICE(lx200Name, DBG_SCOPE, "CMD <%s> successful.", data); return 0; } int setCommandInt(int fd, int data, const char *cmd) { char read_buffer[RB_MAX_LEN]={0}; int error_type; int nbytes_write = 0; snprintf(read_buffer, sizeof(read_buffer), "%s%d#", cmd, data); DEBUGFDEVICE(lx200Name, DBG_SCOPE, "CMD <%s>", read_buffer); tcflush(fd, TCIFLUSH); if ((error_type = tty_write_string(fd, read_buffer, &nbytes_write)) != TTY_OK) { DEBUGFDEVICE(lx200Name, DBG_SCOPE, "CMD <%s> failed.", read_buffer); return error_type; } tcflush(fd, TCIFLUSH); DEBUGFDEVICE(lx200Name, DBG_SCOPE, "CMD <%s> successful.", read_buffer); return 0; } int setMinElevationLimit(int fd, int min) { DEBUGFDEVICE(lx200Name, DBG_SCOPE, "<%s>", __FUNCTION__); char read_buffer[RB_MAX_LEN]={0}; snprintf(read_buffer, sizeof(read_buffer), ":Sh%02d#", min); return (setStandardProcedure(fd, read_buffer)); } int setMaxElevationLimit(int fd, int max) { DEBUGFDEVICE(lx200Name, DBG_SCOPE, "<%s>", __FUNCTION__); char read_buffer[RB_MAX_LEN]={0}; snprintf(read_buffer, sizeof(read_buffer), ":So%02d*#", max); return (setStandardProcedure(fd, read_buffer)); } int setMaxSlewRate(int fd, int slewRate) { DEBUGFDEVICE(lx200Name, DBG_SCOPE, "<%s>", __FUNCTION__); char read_buffer[RB_MAX_LEN]={0}; if (slewRate < 2 || slewRate > 8) return -1; snprintf(read_buffer, sizeof(read_buffer), ":Sw%d#", slewRate); return (setStandardProcedure(fd, read_buffer)); } int setObjectRA(int fd, double ra) { DEBUGFDEVICE(lx200Name, DBG_SCOPE, "<%s>", __FUNCTION__); int h, m, s; char read_buffer[22]; switch (controller_format) { case LX200_LONG_FORMAT: getSexComponents(ra, &h, &m, &s); snprintf(read_buffer, sizeof(read_buffer), ":Sr %02d:%02d:%02d#", h, m, s); break; case LX200_LONGER_FORMAT: double d_s; getSexComponentsIID(ra, &h, &m, &d_s); snprintf(read_buffer, sizeof(read_buffer), ":Sr %02d:%02d:%05.02f#", h, m, d_s); break; case LX200_SHORT_FORMAT: int frac_m; getSexComponents(ra, &h, &m, &s); frac_m = (s / 60.0) * 10.; snprintf(read_buffer, sizeof(read_buffer), ":Sr %02d:%02d.%01d#", h, m, frac_m); break; default: DEBUGFDEVICE(lx200Name, DBG_SCOPE, "Unknown controller_format <%d>", controller_format); return -1; break; } return (setStandardProcedure(fd, read_buffer)); } int setObjectDEC(int fd, double dec) { DEBUGFDEVICE(lx200Name, DBG_SCOPE, "<%s>", __FUNCTION__); int d, m, s; char read_buffer[22]; switch (controller_format) { case LX200_LONG_FORMAT: getSexComponents(dec, &d, &m, &s); /* case with negative zero */ if (!d && dec < 0) snprintf(read_buffer, sizeof(read_buffer), ":Sd -%02d:%02d:%02d#", d, m, s); else snprintf(read_buffer, sizeof(read_buffer), ":Sd %+03d:%02d:%02d#", d, m, s); break; case LX200_LONGER_FORMAT: double d_s; getSexComponentsIID(dec, &d, &m, &d_s); /* case with negative zero */ if (!d && dec < 0) snprintf(read_buffer, sizeof(read_buffer), ":Sd -%02d:%02d:%05.02f#", d, m, d_s); else snprintf(read_buffer, sizeof(read_buffer), ":Sd %+03d:%02d:%05.02f#", d, m, d_s); break; case LX200_SHORT_FORMAT: getSexComponents(dec, &d, &m, &s); /* case with negative zero */ if (!d && dec < 0) snprintf(read_buffer, sizeof(read_buffer), ":Sd -%02d*%02d#", d, m); else snprintf(read_buffer, sizeof(read_buffer), ":Sd %+03d*%02d#", d, m); break; default: DEBUGFDEVICE(lx200Name, DBG_SCOPE, "Unknown controller_format <%d>", controller_format); return -1; break; } return (setStandardProcedure(fd, read_buffer)); } int setCommandXYZ(int fd, int x, int y, int z, const char *cmd) { char read_buffer[RB_MAX_LEN]={0}; snprintf(read_buffer, sizeof(read_buffer), "%s %02d:%02d:%02d#", cmd, x, y, z); return (setStandardProcedure(fd, read_buffer)); } int setAlignmentMode(int fd, unsigned int alignMode) { int error_type; int nbytes_write = 0; switch (alignMode) { case LX200_ALIGN_POLAR: DEBUGFDEVICE(lx200Name, DBG_SCOPE, "CMD <%s>", ":AP#"); if ((error_type = tty_write_string(fd, ":AP#", &nbytes_write)) != TTY_OK) return error_type; break; case LX200_ALIGN_ALTAZ: DEBUGFDEVICE(lx200Name, DBG_SCOPE, "CMD <%s>", ":AA#"); if ((error_type = tty_write_string(fd, ":AA#", &nbytes_write)) != TTY_OK) return error_type; break; case LX200_ALIGN_LAND: DEBUGFDEVICE(lx200Name, DBG_SCOPE, "CMD <%s>", ":AL#"); if ((error_type = tty_write_string(fd, ":AL#", &nbytes_write)) != TTY_OK) return error_type; break; } tcflush(fd, TCIFLUSH); return 0; } int setCalenderDate(int fd, int dd, int mm, int yy) { DEBUGFDEVICE(lx200Name, DBG_SCOPE, "<%s>", __FUNCTION__); const struct timespec timeout = {0, 10000000L}; char read_buffer[64]; char dummy_buffer[64]; int error_type; int nbytes_write = 0, nbytes_read = 0; yy = yy % 100; snprintf(read_buffer, sizeof(read_buffer), ":SC %02d/%02d/%02d#", mm, dd, yy); DEBUGFDEVICE(lx200Name, DBG_SCOPE, "CMD <%s>", read_buffer); tcflush(fd, TCIFLUSH); if ((error_type = tty_write_string(fd, read_buffer, &nbytes_write)) != TTY_OK) return error_type; error_type = tty_read_section(fd, read_buffer, '#', LX200_TIMEOUT, &nbytes_read); // Read the next section whih has 24 blanks and then a # // Can't just use the tcflush to clear the stream because it doesn't seem to work correctly on sockets tty_read_section(fd, dummy_buffer, '#', LX200_TIMEOUT, &nbytes_read); tcflush(fd, TCIFLUSH); if (nbytes_read < 1) { DEBUGDEVICE(lx200Name, DBG_SCOPE, "Unable to parse response"); return error_type; } read_buffer[1] = '\0'; DEBUGFDEVICE(lx200Name, DBG_SCOPE, "RES <%s>", read_buffer); if (read_buffer[0] == '0') return -1; /* Sleep 10ms before flushing. This solves some issues with LX200 compatible devices. */ nanosleep(&timeout, NULL); tcflush(fd, TCIFLUSH); return 0; } int setUTCOffset(int fd, double hours) { DEBUGFDEVICE(lx200Name, DBG_SCOPE, "<%s>", __FUNCTION__); char read_buffer[RB_MAX_LEN]={0}; snprintf(read_buffer, sizeof(read_buffer), ":SG %+03d#", (int)hours); return (setStandardProcedure(fd, read_buffer)); } // Meade defines longitude as 0 to 360 WESTWARD int setSiteLongitude(int fd, double Long) { DEBUGFDEVICE(lx200Name, DBG_SCOPE, "<%s>", __FUNCTION__); int d, m, s; char read_buffer[32]; getSexComponents(Long, &d, &m, &s); snprintf(read_buffer, sizeof(read_buffer), ":Sg%03d:%02d#", d, m); return (setStandardProcedure(fd, read_buffer)); } int setSiteLatitude(int fd, double Lat) { DEBUGFDEVICE(lx200Name, DBG_SCOPE, "<%s>", __FUNCTION__); int d, m, s; char read_buffer[32]; getSexComponents(Lat, &d, &m, &s); snprintf(read_buffer, sizeof(read_buffer), ":St%+03d:%02d:%02d#", d, m, s); return (setStandardProcedure(fd, read_buffer)); } int setObjAz(int fd, double az) { DEBUGFDEVICE(lx200Name, DBG_SCOPE, "<%s>", __FUNCTION__); int d, m, s; char read_buffer[RB_MAX_LEN]={0}; getSexComponents(az, &d, &m, &s); snprintf(read_buffer, sizeof(read_buffer), ":Sz%03d:%02d#", d, m); return (setStandardProcedure(fd, read_buffer)); } int setObjAlt(int fd, double alt) { DEBUGFDEVICE(lx200Name, DBG_SCOPE, "<%s>", __FUNCTION__); int d, m, s; char read_buffer[RB_MAX_LEN]={0}; getSexComponents(alt, &d, &m, &s); snprintf(read_buffer, sizeof(read_buffer), ":Sa%+02d*%02d#", d, m); return (setStandardProcedure(fd, read_buffer)); } int setSiteName(int fd, char *siteName, int siteNum) { DEBUGFDEVICE(lx200Name, DBG_SCOPE, "<%s>", __FUNCTION__); char read_buffer[RB_MAX_LEN]={0}; switch (siteNum) { case 1: snprintf(read_buffer, sizeof(read_buffer), ":SM %s#", siteName); break; case 2: snprintf(read_buffer, sizeof(read_buffer), ":SN %s#", siteName); break; case 3: snprintf(read_buffer, sizeof(read_buffer), ":SO %s#", siteName); break; case 4: snprintf(read_buffer, sizeof(read_buffer), ":SP %s#", siteName); break; default: return -1; } return (setStandardProcedure(fd, read_buffer)); } int setSlewMode(int fd, int slewMode) { DEBUGFDEVICE(lx200Name, DBG_SCOPE, "<%s>", __FUNCTION__); int error_type; int nbytes_write = 0; switch (slewMode) { case LX200_SLEW_MAX: DEBUGFDEVICE(lx200Name, DBG_SCOPE, "CMD <%s>", ":RS#"); if ((error_type = tty_write_string(fd, ":RS#", &nbytes_write)) != TTY_OK) return error_type; break; case LX200_SLEW_FIND: DEBUGFDEVICE(lx200Name, DBG_SCOPE, "CMD <%s>", ":RM#"); if ((error_type = tty_write_string(fd, ":RM#", &nbytes_write)) != TTY_OK) return error_type; break; case LX200_SLEW_CENTER: DEBUGFDEVICE(lx200Name, DBG_SCOPE, "CMD <%s>", ":RC#"); if ((error_type = tty_write_string(fd, ":RC#", &nbytes_write)) != TTY_OK) return error_type; break; case LX200_SLEW_GUIDE: DEBUGFDEVICE(lx200Name, DBG_SCOPE, "CMD <%s>", ":RG#"); if ((error_type = tty_write_string(fd, ":RG#", &nbytes_write)) != TTY_OK) return error_type; break; default: break; } tcflush(fd, TCIFLUSH); return 0; } int setFocuserMotion(int fd, int motionType) { DEBUGFDEVICE(lx200Name, DBG_SCOPE, "<%s>", __FUNCTION__); int error_type; int nbytes_write = 0; switch (motionType) { case LX200_FOCUSIN: DEBUGFDEVICE(lx200Name, DBG_SCOPE, "CMD <%s>", ":F+#"); if ((error_type = tty_write_string(fd, ":F+#", &nbytes_write)) != TTY_OK) return error_type; break; case LX200_FOCUSOUT: DEBUGFDEVICE(lx200Name, DBG_SCOPE, "CMD <%s>", ":F-#"); if ((error_type = tty_write_string(fd, ":F-#", &nbytes_write)) != TTY_OK) return error_type; break; } tcflush(fd, TCIFLUSH); return 0; } int setFocuserSpeedMode(int fd, int speedMode) { DEBUGFDEVICE(lx200Name, DBG_SCOPE, "<%s>", __FUNCTION__); int error_type; int nbytes_write = 0; switch (speedMode) { case LX200_HALTFOCUS: DEBUGFDEVICE(lx200Name, DBG_SCOPE, "CMD <%s>", ":FQ#"); if ((error_type = tty_write_string(fd, ":FQ#", &nbytes_write)) != TTY_OK) return error_type; break; case LX200_FOCUSSLOW: DEBUGFDEVICE(lx200Name, DBG_SCOPE, "CMD <%s>", ":FS#"); if ((error_type = tty_write_string(fd, ":FS#", &nbytes_write)) != TTY_OK) return error_type; break; case LX200_FOCUSFAST: DEBUGFDEVICE(lx200Name, DBG_SCOPE, "CMD <%s>", ":FF#"); if ((error_type = tty_write_string(fd, ":FF#", &nbytes_write)) != TTY_OK) return error_type; break; } tcflush(fd, TCIFLUSH); return 0; } int setGPSFocuserSpeed(int fd, int speed) { DEBUGFDEVICE(lx200Name, DBG_SCOPE, "<%s>", __FUNCTION__); char speed_str[8]; int error_type; int nbytes_write = 0; if (speed == 0) { DEBUGFDEVICE(lx200Name, DBG_SCOPE, "CMD <%s>", ":FQ#"); if ((error_type = tty_write_string(fd, ":FQ#", &nbytes_write)) != TTY_OK) return error_type; tcflush(fd, TCIFLUSH); return 0; } snprintf(speed_str, 8, ":F%d#", speed); DEBUGFDEVICE(lx200Name, DBG_SCOPE, "CMD <%s>", speed_str); if ((error_type = tty_write_string(fd, speed_str, &nbytes_write)) != TTY_OK) return error_type; tcflush(fd, TCIFLUSH); return 0; } int setTrackFreq(int fd, double trackF) { DEBUGFDEVICE(lx200Name, DBG_SCOPE, "<%s>", __FUNCTION__); char read_buffer[RB_MAX_LEN]={0}; snprintf(read_buffer, sizeof(read_buffer), ":ST %04.1f#", trackF); return (setStandardProcedure(fd, read_buffer)); } /********************************************************************** * Misc *********************************************************************/ int Slew(int fd) { DEBUGFDEVICE(lx200Name, DBG_SCOPE, "<%s>", __FUNCTION__); char slewNum[2]; int error_type; int nbytes_write = 0, nbytes_read = 0; DEBUGFDEVICE(lx200Name, DBG_SCOPE, "CMD <%s>", ":MS#"); if ((error_type = tty_write_string(fd, ":MS#", &nbytes_write)) != TTY_OK) return error_type; error_type = tty_read(fd, slewNum, 1, LX200_TIMEOUT, &nbytes_read); if (nbytes_read < 1) { DEBUGFDEVICE(lx200Name, DBG_SCOPE, "RES ERROR <%d>", error_type); return error_type; } /* We don't need to read the string message, just return corresponding error code */ tcflush(fd, TCIFLUSH); DEBUGFDEVICE(lx200Name, DBG_SCOPE, "RES <%c>", slewNum[0]); if (slewNum[0] == '0') return 0; else if (slewNum[0] == '1') return 1; else return 2; } int MoveTo(int fd, int direction) { DEBUGFDEVICE(lx200Name, DBG_SCOPE, "<%s>", __FUNCTION__); int nbytes_write = 0; switch (direction) { case LX200_NORTH: DEBUGFDEVICE(lx200Name, DBG_SCOPE, "CMD <%s>", ":Mn#"); tty_write_string(fd, ":Mn#", &nbytes_write); break; case LX200_WEST: DEBUGFDEVICE(lx200Name, DBG_SCOPE, "CMD <%s>", ":Mw#"); tty_write_string(fd, ":Mw#", &nbytes_write); break; case LX200_EAST: DEBUGFDEVICE(lx200Name, DBG_SCOPE, "CMD <%s>", ":Me#"); tty_write_string(fd, ":Me#", &nbytes_write); break; case LX200_SOUTH: DEBUGFDEVICE(lx200Name, DBG_SCOPE, "CMD <%s>", ":Ms#"); tty_write_string(fd, ":Ms#", &nbytes_write); break; default: break; } tcflush(fd, TCIFLUSH); return 0; } int SendPulseCmd(int fd, int direction, int duration_msec) { DEBUGFDEVICE(lx200Name, DBG_SCOPE, "<%s>", __FUNCTION__); int nbytes_write = 0; char cmd[20]; switch (direction) { case LX200_NORTH: sprintf(cmd, ":Mgn%04d#", duration_msec); break; case LX200_SOUTH: sprintf(cmd, ":Mgs%04d#", duration_msec); break; case LX200_EAST: sprintf(cmd, ":Mge%04d#", duration_msec); break; case LX200_WEST: sprintf(cmd, ":Mgw%04d#", duration_msec); break; default: return 1; } DEBUGFDEVICE(lx200Name, DBG_SCOPE, "CMD <%s>", cmd); tty_write_string(fd, cmd, &nbytes_write); tcflush(fd, TCIFLUSH); return 0; } int HaltMovement(int fd, int direction) { DEBUGFDEVICE(lx200Name, DBG_SCOPE, "<%s>", __FUNCTION__); int error_type; int nbytes_write = 0; switch (direction) { case LX200_NORTH: DEBUGFDEVICE(lx200Name, DBG_SCOPE, "CMD <%s>", ":Qn#"); if ((error_type = tty_write_string(fd, ":Qn#", &nbytes_write)) != TTY_OK) return error_type; break; case LX200_WEST: DEBUGFDEVICE(lx200Name, DBG_SCOPE, "CMD <%s>", ":Qw#"); if ((error_type = tty_write_string(fd, ":Qw#", &nbytes_write)) != TTY_OK) return error_type; break; case LX200_EAST: DEBUGFDEVICE(lx200Name, DBG_SCOPE, "CMD <%s>", ":Qe#"); if ((error_type = tty_write_string(fd, ":Qe#", &nbytes_write)) != TTY_OK) return error_type; break; case LX200_SOUTH: DEBUGFDEVICE(lx200Name, DBG_SCOPE, "CMD <%s>", ":Qs#"); if ((error_type = tty_write_string(fd, ":Qs#", &nbytes_write)) != TTY_OK) return error_type; break; case LX200_ALL: DEBUGFDEVICE(lx200Name, DBG_SCOPE, "CMD <%s>", ":Q#"); if ((error_type = tty_write_string(fd, ":Q#", &nbytes_write)) != TTY_OK) return error_type; break; default: return -1; break; } tcflush(fd, TCIFLUSH); return 0; } int abortSlew(int fd) { DEBUGFDEVICE(lx200Name, DBG_SCOPE, "<%s>", __FUNCTION__); int error_type; int nbytes_write = 0; if ((error_type = tty_write_string(fd, ":Q#", &nbytes_write)) != TTY_OK) return error_type; tcflush(fd, TCIFLUSH); return 0; } int Sync(int fd, char *matchedObject) { DEBUGFDEVICE(lx200Name, DBG_SCOPE, "<%s>", __FUNCTION__); const struct timespec timeout = {0, 10000000L}; int error_type; int nbytes_write = 0, nbytes_read = 0; DEBUGFDEVICE(lx200Name, DBG_SCOPE, "CMD <%s>", ":CM#"); if ((error_type = tty_write_string(fd, ":CM#", &nbytes_write)) != TTY_OK) return error_type; error_type = tty_read_section(fd, matchedObject, '#', LX200_TIMEOUT, &nbytes_read); if (nbytes_read < 1) return error_type; matchedObject[nbytes_read - 1] = '\0'; DEBUGFDEVICE(lx200Name, DBG_SCOPE, "RES <%s>", matchedObject); /* Sleep 10ms before flushing. This solves some issues with LX200 compatible devices. */ nanosleep(&timeout, NULL); tcflush(fd, TCIFLUSH); return 0; } int selectSite(int fd, int siteNum) { DEBUGFDEVICE(lx200Name, DBG_SCOPE, "<%s>", __FUNCTION__); int error_type; int nbytes_write = 0; switch (siteNum) { case 1: DEBUGFDEVICE(lx200Name, DBG_SCOPE, "CMD <%s>", ":W1#"); if ((error_type = tty_write_string(fd, ":W0#", &nbytes_write)) != TTY_OK) //azwing index starts at 0 not 1 return error_type; break; case 2: DEBUGFDEVICE(lx200Name, DBG_SCOPE, "CMD <%s>", ":W2#"); if ((error_type = tty_write_string(fd, ":W1#", &nbytes_write)) != TTY_OK) //azwing index starts at 0 not 1 return error_type; break; case 3: DEBUGFDEVICE(lx200Name, DBG_SCOPE, "CMD <%s>", ":W3#"); if ((error_type = tty_write_string(fd, ":W2#", &nbytes_write)) != TTY_OK) //azwing index starts at 0 not 1 return error_type; break; case 4: DEBUGFDEVICE(lx200Name, DBG_SCOPE, "CMD <%s>", ":W4#"); if ((error_type = tty_write_string(fd, ":W3#", &nbytes_write)) != TTY_OK) //azwing index starts at 0 not 1 return error_type; break; default: return -1; break; } tcflush(fd, TCIFLUSH); return 0; } int selectCatalogObject(int fd, int catalog, int NNNN) { DEBUGFDEVICE(lx200Name, DBG_SCOPE, "<%s>", __FUNCTION__); char read_buffer[RB_MAX_LEN]={0}; int error_type; int nbytes_write = 0; switch (catalog) { case LX200_STAR_C: snprintf(read_buffer, sizeof(read_buffer), ":LS%d#", NNNN); break; case LX200_DEEPSKY_C: snprintf(read_buffer, sizeof(read_buffer), ":LC%d#", NNNN); break; case LX200_MESSIER_C: snprintf(read_buffer, sizeof(read_buffer), ":LM%d#", NNNN); break; default: return -1; } DEBUGFDEVICE(lx200Name, DBG_SCOPE, "CMD <%s>", read_buffer); if ((error_type = tty_write_string(fd, read_buffer, &nbytes_write)) != TTY_OK) return error_type; tcflush(fd, TCIFLUSH); return 0; } int selectSubCatalog(int fd, int catalog, int subCatalog) { DEBUGFDEVICE(lx200Name, DBG_SCOPE, "<%s>", __FUNCTION__); char read_buffer[RB_MAX_LEN]={0}; switch (catalog) { case LX200_STAR_C: snprintf(read_buffer, sizeof(read_buffer), ":LsD%d#", subCatalog); break; case LX200_DEEPSKY_C: snprintf(read_buffer, sizeof(read_buffer), ":LoD%d#", subCatalog); break; case LX200_MESSIER_C: return 1; default: return 0; } return (setStandardProcedure(fd, read_buffer)); } int getLX200Format() { return controller_format; } int checkLX200Format(int fd) { char read_buffer[RB_MAX_LEN] = {0}; controller_format = LX200_LONG_FORMAT; int error_type; int nbytes_write = 0, nbytes_read = 0; DEBUGFDEVICE(lx200Name, DBG_SCOPE, "CMD <%s>", ":GR#"); tcflush(fd, TCIFLUSH); if ((error_type = tty_write_string(fd, ":GR#", &nbytes_write)) != TTY_OK) return error_type; error_type = tty_read_section(fd, read_buffer, '#', LX200_TIMEOUT, &nbytes_read); if (nbytes_read < 1) { DEBUGFDEVICE(lx200Name, DBG_SCOPE, "RES ERROR <%d>", error_type); return error_type; } read_buffer[nbytes_read - 1] = '\0'; DEBUGFDEVICE(lx200Name, DBG_SCOPE, "RES <%s>", read_buffer); /* If it's short format, try to toggle to high precision format */ if (read_buffer[5] == '.') { DEBUGDEVICE(lx200Name, DBG_SCOPE, "Detected low precision format, attempting to switch to high precision."); if ((error_type = tty_write_string(fd, ":U#", &nbytes_write)) != TTY_OK) return error_type; } else if (read_buffer[8] == '.') { controller_format = LX200_LONGER_FORMAT; DEBUGDEVICE(lx200Name, DBG_SCOPE, "Coordinate format is ultra high precision."); return 0; } else { controller_format = LX200_LONG_FORMAT; DEBUGDEVICE(lx200Name, DBG_SCOPE, "Coordinate format is high precision."); return 0; } DEBUGFDEVICE(lx200Name, DBG_SCOPE, "CMD <%s>", ":GR#"); tcflush(fd, TCIFLUSH); if ((error_type = tty_write_string(fd, ":GR#", &nbytes_write)) != TTY_OK) return error_type; error_type = tty_read_section(fd, read_buffer, '#', LX200_TIMEOUT, &nbytes_read); if (nbytes_read < 1) { DEBUGFDEVICE(lx200Name, DBG_SCOPE, "RES ERROR <%d>", error_type); return error_type; } read_buffer[nbytes_read - 1] = '\0'; DEBUGFDEVICE(lx200Name, DBG_SCOPE, "RES <%s>", read_buffer); if (read_buffer[5] == '.') { controller_format = LX200_SHORT_FORMAT; DEBUGDEVICE(lx200Name, DBG_SCOPE, "Coordinate format is low precision."); } else { controller_format = LX200_LONG_FORMAT; DEBUGDEVICE(lx200Name, DBG_SCOPE, "Coordinate format is high precision."); } tcflush(fd, TCIFLUSH); return 0; } int selectTrackingMode(int fd, int trackMode) { DEBUGFDEVICE(lx200Name, DBG_SCOPE, "<%s>", __FUNCTION__); int error_type; int nbytes_write = 0; switch (trackMode) { case LX200_TRACK_SIDEREAL: DEBUGFDEVICE(lx200Name, DBG_SCOPE, "CMD <%s>", ":TQ#"); if ((error_type = tty_write_string(fd, ":TQ#", &nbytes_write)) != TTY_OK) return error_type; break; case LX200_TRACK_SOLAR: DEBUGFDEVICE(lx200Name, DBG_SCOPE, "CMD <%s>", ":TS#"); if ((error_type = tty_write_string(fd, ":TS#", &nbytes_write)) != TTY_OK) return error_type; break; case LX200_TRACK_LUNAR: DEBUGFDEVICE(lx200Name, DBG_SCOPE, "CMD <%s>", ":TL#"); if ((error_type = tty_write_string(fd, ":TL#", &nbytes_write)) != TTY_OK) return error_type; break; case LX200_TRACK_MANUAL: DEBUGFDEVICE(lx200Name, DBG_SCOPE, "CMD <%s>", ":TM#"); if ((error_type = tty_write_string(fd, ":TM#", &nbytes_write)) != TTY_OK) return error_type; break; default: return -1; break; } tcflush(fd, TCIFLUSH); return 0; } libindi/drivers/telescope/skywatcherAPI.cpp0000664000175000017500000005466713263645557020377 0ustar jasemjasem/*! * \file skywatcherAPI.cpp * * \author Roger James * \author Gerry Rozema * \author Jean-Luc Geehalel * \date 13th November 2013 * * This file contains an implementation in C++ of the Skywatcher API. * It is based on work from four sources. * A C++ implementation of the API by Roger James. * The indi_eqmod driver by Jean-Luc Geehalel. * The synscanmount driver by Gerry Rozema. * The C# implementation published by Skywatcher/Synta */ #include "skywatcherAPI.h" #include #include #include #include void AXISSTATUS::SetFullStop() { FullStop = true; SlewingTo = Slewing = false; } void AXISSTATUS::SetSlewing(bool forward, bool highspeed) { FullStop = SlewingTo = false; Slewing = true; SlewingForward = forward; HighSpeed = highspeed; } void AXISSTATUS::SetSlewingTo(bool forward, bool highspeed) { FullStop = Slewing = false; SlewingTo = true; SlewingForward = forward; HighSpeed = highspeed; } SkywatcherAPI::SkywatcherAPI() { // I add an additional debug level so I can log verbose scope status DBG_SCOPE = INDI::Logger::getInstance().addDebugLevel("Scope Verbose", "SCOPE"); RadiansPerMicrostep[AXIS1] = RadiansPerMicrostep[AXIS2] = 0; MicrostepsPerRadian[AXIS1] = MicrostepsPerRadian[AXIS2] = 0; DegreesPerMicrostep[AXIS1] = DegreesPerMicrostep[AXIS2] = 0; MicrostepsPerDegree[AXIS1] = MicrostepsPerDegree[AXIS2] = 0; CurrentEncoders[AXIS1] = CurrentEncoders[AXIS2] = 0; PolarisPositionEncoders[AXIS1] = PolarisPositionEncoders[AXIS2] = 0; ZeroPositionEncoders[AXIS1] = ZeroPositionEncoders[AXIS2] = 0; SlewingSpeed[AXIS1] = SlewingSpeed[AXIS2] = 0; } unsigned long SkywatcherAPI::BCDstr2long(std::string &String) { if (String.size() != 6) { return 0; } unsigned long value = 0; #define HEX(c) (((c) < 'A') ? ((c) - '0') : ((c) - 'A') + 10) value = HEX(String[4]); value <<= 4; value |= HEX(String[5]); value <<= 4; value |= HEX(String[2]); value <<= 4; value |= HEX(String[3]); value <<= 4; value |= HEX(String[0]); value <<= 4; value |= HEX(String[1]); #undef HEX return value; } unsigned long SkywatcherAPI::Highstr2long(std::string &String) { if (String.size() < 2) { return 0; } unsigned long res = 0; #define HEX(c) (((c) < 'A') ? ((c) - '0') : ((c) - 'A') + 10) res = HEX(String[0]); res <<= 4; res |= HEX(String[1]); #undef HEX return res; } bool SkywatcherAPI::CheckIfDCMotor() { MYDEBUG(DBG_SCOPE, "CheckIfDCMotor"); // Flush the tty read buffer char input[20]; int rc; int nbytes; while (true) { rc = skywatcher_tty_read(MyPortFD, input, 20, 5, &nbytes); if (TTY_TIME_OUT == rc) break; if (TTY_OK != rc) return false; } if (TTY_OK != skywatcher_tty_write(MyPortFD, ":", 1, &nbytes)) return false; rc = skywatcher_tty_read(MyPortFD, input, 1, 5, &nbytes); if ((TTY_OK == rc) && (1 == nbytes) && (':' == input[0])) { IsDCMotor = true; return true; } if (TTY_TIME_OUT == rc) { IsDCMotor = false; return true; } return false; } bool SkywatcherAPI::IsVirtuosoMount() const { return MountCode >= 0x90; } bool SkywatcherAPI::IsMerlinMount() const { return MountCode >= 0x80 && MountCode < 0x90; } long SkywatcherAPI::DegreesPerSecondToClocksTicksPerMicrostep(AXISID Axis, double DegreesPerSecond) { double MicrostepsPerSecond = DegreesPerSecond * MicrostepsPerDegree[Axis]; return long((double(StepperClockFrequency[Axis]) / MicrostepsPerSecond)); } long SkywatcherAPI::DegreesToMicrosteps(AXISID Axis, double AngleInDegrees) { return (long)(AngleInDegrees * MicrostepsPerDegree[(int)Axis]); } bool SkywatcherAPI::GetEncoder(AXISID Axis) { // MYDEBUG(DBG_SCOPE, "GetEncoder"); std::string Parameters, Response; if (!TalkWithAxis(Axis, 'j', Parameters, Response)) return false; long Microsteps = BCDstr2long(Response); CurrentEncoders[(int)Axis] = Microsteps; return true; } bool SkywatcherAPI::GetHighSpeedRatio(AXISID Axis) { MYDEBUG(DBG_SCOPE, "GetHighSpeedRatio"); std::string Parameters, Response; if (!TalkWithAxis(Axis, 'g', Parameters, Response)) return false; unsigned long highSpeedRatio = Highstr2long(Response); HighSpeedRatio[(int)Axis] = highSpeedRatio; return true; } bool SkywatcherAPI::GetMicrostepsPerRevolution(AXISID Axis) { MYDEBUG(DBG_SCOPE, "GetMicrostepsPerRevolution"); std::string Parameters, Response; if (!TalkWithAxis(Axis, 'a', Parameters, Response)) return false; long tmpMicrostepsPerRevolution = BCDstr2long(Response); // There is a bug in the earlier version firmware(Before 2.00) of motor controller MC001. // Overwrite the MicrostepsPerRevolution reported by the MC for 80GT mount and 114GT mount. // kecsap: The Merlin mounts use the same mount code and it brakes the operation. // if (MountCode == GT) // tmpMicrostepsPerRevolution = 0x162B97; // for 80GT mount if (MountCode == _114GT) tmpMicrostepsPerRevolution = 0x205318; // for 114GT mount if (IsMerlinMount()) tmpMicrostepsPerRevolution = (long)((double)tmpMicrostepsPerRevolution*0.655); MicrostepsPerRevolution[(int)Axis] = tmpMicrostepsPerRevolution; MicrostepsPerRadian[(int)Axis] = tmpMicrostepsPerRevolution / (2 * M_PI); RadiansPerMicrostep[(int)Axis] = 2 * M_PI / tmpMicrostepsPerRevolution; MicrostepsPerDegree[(int)Axis] = tmpMicrostepsPerRevolution / 360.0; DegreesPerMicrostep[(int)Axis] = 360.0 / tmpMicrostepsPerRevolution; MYDEBUGF(DBG_SCOPE, "Axis %d: %lf microsteps/degree, %lf microsteps/arcsec", Axis, (double)tmpMicrostepsPerRevolution / 360.0, (double)tmpMicrostepsPerRevolution / 360.0 / 60 / 60); return true; } bool SkywatcherAPI::GetMicrostepsPerWormRevolution(AXISID Axis) { MYDEBUG(DBG_SCOPE, "GetMicrostepsPerWormRevolution"); std::string Parameters, Response; if (!TalkWithAxis(Axis, 's', Parameters, Response)) return false; MicrostepsPerWormRevolution[(int)Axis] = BCDstr2long(Response); return true; } bool SkywatcherAPI::GetMotorBoardVersion(AXISID Axis) { // MYDEBUG(DBG_SCOPE, "GetMotorBoardVersion"); std::string Parameters, Response; if (!TalkWithAxis(Axis, 'e', Parameters, Response)) return false; unsigned long tmpMCVersion = BCDstr2long(Response); MCVersion = ((tmpMCVersion & 0xFF) << 16) | ((tmpMCVersion & 0xFF00)) | ((tmpMCVersion & 0xFF0000) >> 16); return true; } SkywatcherAPI::PositiveRotationSense_t SkywatcherAPI::GetPositiveRotationDirection(AXISID Axis) { INDI_UNUSED(Axis); if (MountCode == _114GT) return CLOCKWISE; return ANTICLOCKWISE; } bool SkywatcherAPI::GetStepperClockFrequency(AXISID Axis) { MYDEBUG(DBG_SCOPE, "GetStepperClockFrequency"); std::string Parameters, Response; if (!TalkWithAxis(Axis, 'b', Parameters, Response)) return false; StepperClockFrequency[(int)Axis] = BCDstr2long(Response); return true; } bool SkywatcherAPI::GetStatus(AXISID Axis) { // MYDEBUG(DBG_SCOPE, "GetStatus"); std::string Parameters, Response; if (!TalkWithAxis(Axis, 'f', Parameters, Response)) return false; if ((Response[1] & 0x01) != 0) { // Axis is running AxesStatus[(int)Axis].FullStop = false; if ((Response[0] & 0x01) != 0) { AxesStatus[(int)Axis].Slewing = true; // Axis in slewing(AstroMisc speed) mode. AxesStatus[(int)Axis].SlewingTo = false; } else { AxesStatus[(int)Axis].SlewingTo = true; // Axis in SlewingTo mode. AxesStatus[(int)Axis].Slewing = false; } } else { // SlewTo Debugging if (AxesStatus[(int)Axis].SlewingTo) { // If the mount was doing a slew to GetEncoder(Axis); // MYDEBUGF(INDI::Logger::DBG_SESSION, // "Axis %s SlewTo complete - offset to target %ld microsteps %lf arc seconds " // "LastSlewToTarget %ld CurrentEncoder %ld", // Axis == AXIS1 ? "AXIS1" : "AXIS2", LastSlewToTarget[Axis] - CurrentEncoders[Axis], // MicrostepsToDegrees(Axis, LastSlewToTarget[Axis] - CurrentEncoders[Axis]) * 3600, // LastSlewToTarget[Axis], CurrentEncoders[Axis]); } AxesStatus[(int)Axis].FullStop = true; // FullStop = 1; // Axis is fully stop. AxesStatus[(int)Axis].Slewing = false; AxesStatus[(int)Axis].SlewingTo = false; } if ((Response[0] & 0x02) == 0) AxesStatus[(int)Axis].SlewingForward = true; // Angle increase = 1; else AxesStatus[(int)Axis].SlewingForward = false; if ((Response[0] & 0x04) != 0) AxesStatus[(int)Axis].HighSpeed = true; // HighSpeed running mode = 1; else AxesStatus[(int)Axis].HighSpeed = false; if ((Response[2] & 1) == 0) AxesStatus[(int)Axis].NotInitialized = true; // MC is not initialized. else AxesStatus[(int)Axis].NotInitialized = false; return true; } // Set initialization done ":F3", where '3'= Both CH1 and CH2. bool SkywatcherAPI::InitializeMC() { MYDEBUG(DBG_SCOPE, "InitializeMC"); std::string Parameters, Response; if (!TalkWithAxis(AXIS1, 'F', Parameters, Response)) return false; if (!TalkWithAxis(AXIS2, 'F', Parameters, Response)) return false; return true; } bool SkywatcherAPI::InitMount(bool recover) { MYDEBUG(DBG_SCOPE, "InitMount"); if (!CheckIfDCMotor()) return false; if (!GetMotorBoardVersion(AXIS1)) return false; MountCode = MCVersion & 0xFF; // Disable EQ mounts if (MountCode < 0x80) return false; //// NOTE: Simulator settings, Mount dependent Settings // Inquire Gear Rate if (!GetMicrostepsPerRevolution(AXIS1)) return false; if (!GetMicrostepsPerRevolution(AXIS2)) return false; // Get stepper clock frequency if (!GetStepperClockFrequency(AXIS1)) return false; if (!GetStepperClockFrequency(AXIS2)) return false; // Inquire motor high speed ratio if (!GetHighSpeedRatio(AXIS1)) return false; if (!GetHighSpeedRatio(AXIS2)) return false; // Inquire PEC period // DC motor controller does not support PEC if (!IsDCMotor) { GetMicrostepsPerWormRevolution(AXIS1); GetMicrostepsPerWormRevolution(AXIS2); } // Inquire Axis Position if (!GetEncoder(AXIS1)) return false; if (!GetEncoder(AXIS2)) return false; MYDEBUGF(DBG_SCOPE, "Encoders before init Axis1 %ld Axis2 %ld", CurrentEncoders[AXIS1], CurrentEncoders[AXIS2]); // Set initial axis positions // These are used to define the arbitrary zero position vector for the axis if (!recover) { PolarisPositionEncoders[AXIS1] = CurrentEncoders[AXIS1]; PolarisPositionEncoders[AXIS2] = CurrentEncoders[AXIS2]; ZeroPositionEncoders[AXIS1] = PolarisPositionEncoders[AXIS1]; ZeroPositionEncoders[AXIS2] = PolarisPositionEncoders[AXIS2]; } if (!InitializeMC()) return false; if (!GetEncoder(AXIS1)) return false; if (!GetEncoder(AXIS2)) return false; MYDEBUGF(DBG_SCOPE, "Encoders after init Axis1 %ld Axis2 %ld", CurrentEncoders[AXIS1], CurrentEncoders[AXIS2]); // These two LowSpeedGotoMargin are calculate from slewing for 5 seconds in 128x sidereal rate LowSpeedGotoMargin[(int)AXIS1] = (long)(640 * SIDEREALRATE * MicrostepsPerRadian[(int)AXIS1]); LowSpeedGotoMargin[(int)AXIS2] = (long)(640 * SIDEREALRATE * MicrostepsPerRadian[(int)AXIS2]); return true; } bool SkywatcherAPI::InstantStop(AXISID Axis) { // Request a slow stop MYDEBUG(DBG_SCOPE, "InstantStop"); std::string Parameters, Response; if (!TalkWithAxis(Axis, 'L', Parameters, Response)) return false; AxesStatus[(int)Axis].SetFullStop(); return true; } void SkywatcherAPI::Long2BCDstr(long Number, std::string &String) { std::stringstream Temp; Temp << std::hex << std::setfill('0') << std::uppercase << std::setw(2) << (Number & 0xff) << std::setw(2) << ((Number & 0xff00) >> 8) << std::setw(2) << ((Number & 0xff0000) >> 16); String = Temp.str(); } double SkywatcherAPI::MicrostepsToDegrees(AXISID Axis, long Microsteps) { return Microsteps * DegreesPerMicrostep[(int)Axis]; } double SkywatcherAPI::MicrostepsToRadians(AXISID Axis, long Microsteps) { return Microsteps * RadiansPerMicrostep[(int)Axis]; } void SkywatcherAPI::PrepareForSlewing(AXISID Axis, double Speed) { // Update the axis status if (!GetStatus(Axis)) return; if (!AxesStatus[Axis].FullStop) { // Axis is running if ((AxesStatus[Axis].SlewingTo) // slew to (GOTO) in progress || (AxesStatus[Axis].HighSpeed) // currently high speed slewing || (std::abs(Speed) >= LOW_SPEED_MARGIN) // I am about to request high speed || ((AxesStatus[Axis].SlewingForward) && (Speed < 0)) // Direction change || (!(AxesStatus[Axis].SlewingForward) && (Speed > 0))) // Direction change { // I need to stop the axis first SlowStop(Axis); } else return; // NO need change motion mode // Horrible bit A POLLING LOOP !!!!!!!!!! while (true) { // Update status GetStatus(Axis); if (AxesStatus[Axis].FullStop) break; std::this_thread::sleep_for(std::chrono::milliseconds(100)); // Sleep for 1/10 second } } char Direction; if (Speed > 0.0) Direction = '0'; else { Direction = '1'; Speed = -Speed; } if (Speed > LOW_SPEED_MARGIN) SetMotionMode(Axis, '3', Direction); else SetMotionMode(Axis, '1', Direction); } long SkywatcherAPI::RadiansPerSecondToClocksTicksPerMicrostep(AXISID Axis, double RadiansPerSecond) { double MicrostepsPerSecond = RadiansPerSecond * MicrostepsPerRadian[Axis]; return long((double(StepperClockFrequency[Axis]) / MicrostepsPerSecond)); } long SkywatcherAPI::RadiansToMicrosteps(AXISID Axis, double AngleInRadians) { return (long)(AngleInRadians * MicrostepsPerRadian[(int)Axis]); } bool SkywatcherAPI::SetEncoder(AXISID Axis, long Microsteps) { MYDEBUG(DBG_SCOPE, "SetEncoder"); std::string Parameters, Response; Long2BCDstr(Microsteps, Parameters); return TalkWithAxis(Axis, 'L', Parameters, Response); } bool SkywatcherAPI::SetGotoTargetOffset(AXISID Axis, long OffsetInMicrosteps) { // MYDEBUG(DBG_SCOPE, "SetGotoTargetOffset"); std::string Parameters, Response; Long2BCDstr(OffsetInMicrosteps, Parameters); return TalkWithAxis(Axis, 'H', Parameters, Response); } /// Func - 0 High speed slew to mode (goto) /// Func - 1 Low speed slew mode /// Func - 2 Low speed slew to mode (goto) /// Func - 3 High speed slew mode bool SkywatcherAPI::SetMotionMode(AXISID Axis, char Func, char Direction) { // MYDEBUG(DBG_SCOPE, "SetMotionMode"); std::string Parameters, Response; Parameters.push_back(Func); Parameters.push_back(Direction); return TalkWithAxis(Axis, 'G', Parameters, Response); } bool SkywatcherAPI::SetClockTicksPerMicrostep(AXISID Axis, long ClockTicksPerMicrostep) { MYDEBUG(DBG_SCOPE, "SetClockTicksPerMicrostep"); std::string Parameters, Response; Long2BCDstr(ClockTicksPerMicrostep, Parameters); return TalkWithAxis(Axis, 'I', Parameters, Response); } bool SkywatcherAPI::SetSlewModeDeccelerationRampLength(AXISID Axis, long Microsteps) { // MYDEBUG(DBG_SCOPE, "SetSlewModeDeccelerationRampLength"); std::string Parameters, Response; Long2BCDstr(Microsteps, Parameters); return TalkWithAxis(Axis, 'U', Parameters, Response); } bool SkywatcherAPI::SetSlewToModeDeccelerationRampLength(AXISID Axis, long Microsteps) { // MYDEBUG(DBG_SCOPE, "SetSlewToModeDeccelerationRampLength"); std::string Parameters, Response; Long2BCDstr(Microsteps, Parameters); return TalkWithAxis(Axis, 'M', Parameters, Response); } bool SkywatcherAPI::SetSwitch(bool OnOff) { MYDEBUG(DBG_SCOPE, "SetSwitch"); std::string Parameters, Response; if (OnOff) Parameters = "1"; else Parameters = "0"; return TalkWithAxis(AXIS1, 'O', Parameters, Response); } void SkywatcherAPI::Slew(AXISID Axis, double SpeedInRadiansPerSecond, bool IgnoreSilentMode) { MYDEBUGF(DBG_SCOPE, "Slew axis: %d speed: %1.6f", (int)Axis, SpeedInRadiansPerSecond); // Clamp to MAX_SPEED if (SpeedInRadiansPerSecond > MAX_SPEED) SpeedInRadiansPerSecond = MAX_SPEED; else if (SpeedInRadiansPerSecond < -MAX_SPEED) SpeedInRadiansPerSecond = -MAX_SPEED; double InternalSpeed = SpeedInRadiansPerSecond; if (std::abs(InternalSpeed) <= SIDEREALRATE / 1000.0) { SlowStop(Axis); return; } // Stop motor and set motion mode if necessary PrepareForSlewing(Axis, InternalSpeed); bool Forward; if (InternalSpeed > 0.0) Forward = true; else { InternalSpeed = -InternalSpeed; Forward = false; } bool HighSpeed = false; if (InternalSpeed > LOW_SPEED_MARGIN && (IgnoreSilentMode || !SilentSlewMode)) { InternalSpeed = InternalSpeed / (double)HighSpeedRatio[Axis]; HighSpeed = true; } long SpeedInt = RadiansPerSecondToClocksTicksPerMicrostep(Axis, InternalSpeed); if ((MCVersion == 0x010600) || (MCVersion == 0x0010601)) // Cribbed from Mount_Skywatcher.cs SpeedInt -= 3; if (SpeedInt < 6) SpeedInt = 6; SetClockTicksPerMicrostep(Axis, SpeedInt); StartMotion(Axis); AxesStatus[Axis].SetSlewing(Forward, HighSpeed); SlewingSpeed[Axis] = SpeedInRadiansPerSecond; } void SkywatcherAPI::SlewTo(AXISID Axis, long OffsetInMicrosteps, bool verbose) { if (verbose) { MYDEBUGF(INDI::Logger::DBG_SESSION, "SlewTo axis: %d offset: %ld", (int)Axis, OffsetInMicrosteps); } if (0 == OffsetInMicrosteps) // Nothing to do return; // Debugging LastSlewToTarget[Axis] = CurrentEncoders[Axis] + OffsetInMicrosteps; if (verbose) { MYDEBUGF(INDI::Logger::DBG_SESSION, "SlewTo axis %d Offset %ld CurrentEncoder %ld SlewToTarget %ld", Axis, OffsetInMicrosteps, CurrentEncoders[Axis], LastSlewToTarget[Axis]); } char Direction; bool Forward; if (OffsetInMicrosteps > 0) { Forward = true; Direction = '0'; } else { Forward = false; Direction = '1'; OffsetInMicrosteps = -OffsetInMicrosteps; } bool HighSpeed = false; if (OffsetInMicrosteps > LowSpeedGotoMargin[Axis] && !SilentSlewMode) HighSpeed = true; if (!GetStatus(Axis)) return; if (!AxesStatus[Axis].FullStop) { // Axis is running if ((AxesStatus[Axis].SlewingTo) // slew to (GOTO) in progress || (AxesStatus[Axis].HighSpeed) // currently high speed slewing || HighSpeed // I am about to request high speed || ((AxesStatus[Axis].SlewingForward) && !Forward) // Direction change || (!(AxesStatus[Axis].SlewingForward) && Forward)) // Direction change { // I need to stop the axis first SlowStop(Axis); // Horrible bit A POLLING LOOP !!!!!!!!!! while (true) { // Update status GetStatus(Axis); if (AxesStatus[Axis].FullStop) break; std::this_thread::sleep_for(std::chrono::milliseconds(100)); // Sleep for 1/10 second } } } if (HighSpeed) SetMotionMode(Axis, '0', Direction); else SetMotionMode(Axis, '2', Direction); SetGotoTargetOffset(Axis, OffsetInMicrosteps); if (HighSpeed) SetSlewToModeDeccelerationRampLength(Axis, OffsetInMicrosteps > 3200 ? 3200 : OffsetInMicrosteps); else SetSlewToModeDeccelerationRampLength(Axis, OffsetInMicrosteps > 200 ? 200 : OffsetInMicrosteps); StartMotion(Axis); AxesStatus[Axis].SetSlewingTo(Forward, HighSpeed); } bool SkywatcherAPI::SlowStop(AXISID Axis) { // Request a slow stop // MYDEBUG(DBG_SCOPE, "SlowStop"); std::string Parameters, Response; return TalkWithAxis(Axis, 'K', Parameters, Response); } bool SkywatcherAPI::StartMotion(AXISID Axis) { // MYDEBUG(DBG_SCOPE, "StartMotion"); std::string Parameters, Response; return TalkWithAxis(Axis, 'J', Parameters, Response); } bool SkywatcherAPI::TalkWithAxis(AXISID Axis, char Command, std::string &cmdDataStr, std::string &responseStr) { // MYDEBUGF(DBG_SCOPE, "TalkWithAxis Axis %s Command %c Data (%s)", Axis == AXIS1 ? "AXIS1" : "AXIS2", Command, // cmdDataStr.c_str()); std::string SendBuffer; int bytesWritten; int bytesRead; bool StartReading = false; bool EndReading = false; bool mount_response = false; SendBuffer.push_back(':'); SendBuffer.push_back(Command); SendBuffer.push_back(Axis == AXIS1 ? '1' : '2'); SendBuffer.append(cmdDataStr); SendBuffer.push_back('\r'); skywatcher_tty_write(MyPortFD, SendBuffer.c_str(), SendBuffer.size(), &bytesWritten); std::this_thread::sleep_for(std::chrono::milliseconds(5)); while (!EndReading) { std::this_thread::sleep_for(std::chrono::milliseconds(5)); char c; int rc = skywatcher_tty_read(MyPortFD, &c, 1, 10, &bytesRead); if ((rc != TTY_OK) || (bytesRead != 1)) return false; if ((c == '=') || (c == '!')) { mount_response = (c == '='); StartReading = true; continue; } if ((c == '\r') && StartReading) { EndReading = true; continue; } if (StartReading) responseStr.push_back(c); } // MYDEBUGF(DBG_SCOPE, "TalkWithAxis - %s Response (%s)", mount_response ? "Good" : "Bad", responseStr.c_str()); return true; } bool SkywatcherAPI::IsInMotion(AXISID Axis) { MYDEBUG(DBG_SCOPE, "IsInMotion"); return AxesStatus[(int)Axis].Slewing || AxesStatus[(int)Axis].SlewingTo; } libindi/drivers/telescope/lx200ap_gtocp2.h0000664000175000017500000000707513263645557017761 0ustar jasemjasem/* Astro-Physics INDI driver Tailored for GTOCP2 Copyright (C) 2018 Jasem Mutlaq 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 */ #pragma once #include "lx200generic.h" class LX200AstroPhysicsGTOCP2 : public LX200Generic { public: LX200AstroPhysicsGTOCP2(); ~LX200AstroPhysicsGTOCP2() {} typedef enum { MCV_E, MCV_F, MCV_G, MCV_H, MCV_I, MCV_J, MCV_L, MCV_P, MCV_UNKNOWN} ControllerVersion; virtual bool ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) override; virtual void ISGetProperties(const char *dev) override; protected: virtual const char *getDefaultName() override; virtual bool initProperties() override; virtual bool updateProperties() override; virtual bool ReadScopeStatus() override; virtual bool Handshake() override; virtual bool Disconnect() override; // Parking virtual bool SetCurrentPark() override; virtual bool SetDefaultPark() override; virtual bool Park() override; virtual bool UnPark() override; virtual bool Sync(double ra, double dec) override; virtual bool Goto(double, double) override; virtual bool updateTime(ln_date *utc, double utc_offset) override; virtual bool updateLocation(double latitude, double longitude, double elevation) override; virtual bool SetSlewRate(int index) override; virtual int SendPulseCmd(int direction, int duration_msec) override; virtual bool getUTFOffset(double *offset) override; // Tracking virtual bool SetTrackMode(uint8_t mode) override; virtual bool SetTrackEnabled(bool enabled) override; virtual bool SetTrackRate(double raRate, double deRate) override; // NSWE Motion Commands virtual bool MoveNS(INDI_DIR_NS dir, TelescopeMotionCommand command) override; virtual bool MoveWE(INDI_DIR_WE dir, TelescopeMotionCommand command) override; virtual bool saveConfigItems(FILE *fp) override; virtual void debugTriggered(bool enable) override; void handleGTOCP2MotionBug(); INumber HourangleCoordsN[2]; INumberVectorProperty HourangleCoordsNP; INumber HorizontalCoordsN[2]; INumberVectorProperty HorizontalCoordsNP; ISwitch APSlewSpeedS[3]; ISwitchVectorProperty APSlewSpeedSP; ISwitch SwapS[2]; ISwitchVectorProperty SwapSP; ISwitch SyncCMRS[2]; ISwitchVectorProperty SyncCMRSP; enum { USE_REGULAR_SYNC, USE_CMR_SYNC }; ISwitch APGuideSpeedS[3]; ISwitchVectorProperty APGuideSpeedSP; IText VersionT[1] {}; ITextVectorProperty VersionInfo; private: bool initMount(); // Side of pier void syncSideOfPier(); bool timeUpdated=false, locationUpdated=false; ControllerVersion firmwareVersion = MCV_UNKNOWN; double currentAlt=0, currentAz=0; double lastRA=0, lastDE=0; double lastAZ=0, lastAL=0; bool motionCommanded=true; bool mountInitialized=false; }; libindi/drivers/telescope/lx200ss2000pc.h0000664000175000017500000000355613263645557017355 0ustar jasemjasem/* SkySensor2000 PC Copyright (C) 2015 Camiel Severijns 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 */ #pragma once #include "lx200generic.h" class LX200SS2000PC : public LX200Generic { public: LX200SS2000PC(void); virtual const char *getDefaultName(void); virtual bool updateTime(ln_date *utc, double utc_offset); virtual bool initProperties(); virtual bool updateProperties(); virtual bool ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n); protected: virtual void getBasicData(void); virtual bool isSlewComplete(void); virtual bool saveConfigItems(FILE *fp); virtual bool setUTCOffset(double offset); //bool setUTCOffset(const int offset_in_hours); private: bool getCalendarDate(int &year, int &month, int &day); bool setCalenderDate(int year, int month, int day); bool updateLocation(double latitude, double longitude, double elevation); int setSiteLongitude(int fd, double Long); int setSiteLatitude(int fd, double Long); INumber SlewAccuracyN[2]; INumberVectorProperty SlewAccuracyNP; static const int ShortTimeOut; static const int LongTimeOut; }; libindi/drivers/telescope/pmc8driver.cpp0000664000175000017500000014720313263645557017731 0ustar jasemjasem/* INDI Explore Scientific PMC8 driver Copyright (C) 2017 Michael Fulbright Based on IEQPro driver. 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 */ #include "pmc8driver.h" #include "indicom.h" #include "indilogger.h" #include "inditelescope.h" #include #include #include #include #include #include // only used for test timing of pulse guiding #include #define PMC8_TIMEOUT 5 /* FD timeout in seconds */ #define PMC8_SIMUL_VERSION_RESP "ESGvES06B9T9" // FIXME - these should be read from the controller? Depends on mount type. #define PMC8_AXIS0_SCALE 4608000.0 #define PMC8_AXIS1_SCALE 4608000.0 #define ARCSEC_IN_CIRCLE 1296000.0 // FIXME - (just placeholders need better way to represent // This value is from PMC8 SDK document #define PMC8_MAX_PRECISE_MOTOR_RATE 2641 // set max settable slew rate as move rate as 256x sidereal #define PMC8_MAX_MOVE_MOTOR_RATE (256*15) // if tracking speed above this then mount is slewing // NOTE - 55 is fine since sidereal rate is 53 in these units // BUT if custom tracking rates are allowed in future // must change this limit to accomodate possibility // custom rate is higher than sidereal #define PMC8_MINSLEWRATE 55 // any guide pulses less than this are ignored as it will not result in any actual motor motion #define PMC8_PULSE_GUIDE_MIN_MS 20 // guide pulses longer than this require using a timer #define PMC8_PULSE_GUIDE_MAX_NOTIMER 250 // dont send pulses if already moving faster than this #define PMC8_PULSE_GUIDE_MAX_CURRATE 120 bool pmc8_debug = false; bool pmc8_simulation = false; char pmc8_device[MAXINDIDEVICE] = "PMC8"; double pmc8_latitude = 0; // must be kept updated by pmc8.cpp when it is changed! double pmc8_longitude = 0; // must be kept updated by pmc8.cpp when it is changed! double pmc8_guide_rate = 0.5*15.0; // default to 0.5 sidereal PMC8Info simPMC8Info; // state variable for driver based pulse guiding typedef struct { bool pulseguideactive = false; bool fakepulse = false; int ms; long long pulse_start_us; int cur_ra_rate; int cur_dec_rate; int cur_ra_dir; int cur_dec_dir; int new_ra_rate; int new_dec_rate; int new_ra_dir; int new_dec_dir; } PulseGuideState; // need one for NS and EW pulses which may be simultaneous PulseGuideState NS_PulseGuideState, EW_PulseGuideState; //bool pulse_guide_active = false; struct { double ra; double dec; int raDirection; int decDirection; double trackRate; double moveRate; double guide_rate; } simPMC8Data; void set_pmc8_debug(bool enable) { pmc8_debug = enable; } void set_pmc8_simulation(bool enable) { pmc8_simulation = enable; if (enable) simPMC8Data.guide_rate = 0.5; } void set_pmc8_device(const char *name) { strncpy(pmc8_device, name, MAXINDIDEVICE); } void set_pmc8_location(double latitude, double longitude) { pmc8_latitude = latitude; pmc8_longitude = longitude; DEBUGFDEVICE(pmc8_device, INDI::Logger::DBG_DEBUG, "Set PMC8 'lowlevel' lat:%f long:%f",pmc8_latitude, pmc8_longitude); } void set_pmc8_sim_system_status(PMC8_SYSTEM_STATUS value) { simPMC8Info.systemStatus = value; if (value == ST_PARKED) { double lst; double ra; lst = get_local_sidereal_time(pmc8_longitude); ra = lst + 6; if (ra > 24) ra -= 24; set_pmc8_sim_ra(ra); set_pmc8_sim_dec(90.0); } } void set_pmc8_sim_track_rate(PMC8_TRACK_RATE value) { simPMC8Data.trackRate = value; } void set_pmc8_sim_move_rate(PMC8_MOVE_RATE value) { simPMC8Data.moveRate = value; } void set_pmc8_sim_ra(double ra) { simPMC8Data.ra = ra; } void set_pmc8_sim_dec(double dec) { simPMC8Data.dec = dec; } //void set_pmc8_sim_guide_rate(double rate) //{ // simPMC8Data.guide_rate = rate; //} bool check_pmc8_connection(int fd) { char initCMD[] = "ESGv!"; int errcode = 0; char errmsg[MAXRBUF]; char response[16]; int nbytes_read = 0; int nbytes_written = 0; DEBUGDEVICE(pmc8_device, INDI::Logger::DBG_DEBUG, "Initializing PMC8 using ESGv! CMD..."); for (int i = 0; i < 2; i++) { if (pmc8_simulation) { strcpy(response, PMC8_SIMUL_VERSION_RESP); nbytes_read = strlen(response); } else { tcflush(fd, TCIFLUSH); if ((errcode = tty_write(fd, initCMD, strlen(initCMD), &nbytes_written)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); DEBUGFDEVICE(pmc8_device, INDI::Logger::DBG_ERROR, "1 %s", errmsg); usleep(50000); continue; } if ((errcode = tty_read_section(fd, response, '!', PMC8_TIMEOUT, &nbytes_read))) { tty_error_msg(errcode, errmsg, MAXRBUF); DEBUGFDEVICE(pmc8_device, INDI::Logger::DBG_ERROR, "check_pmc8_connection(): Error connecting - %s. " "Please read the instructions at: " "http://indilib.org/devices/telescopes/explore-scientific-g11-pmc-eight/ " "before using this driver!" "A special USB SERIAL cable setup is REQUIRED for it work currently.", errmsg); usleep(50000); continue; } } if (nbytes_read > 0) { response[nbytes_read] = '\0'; DEBUGFDEVICE(pmc8_device, INDI::Logger::DBG_DEBUG, "RES (%s)", response); // FIXME - need to put in better check for a valid firmware version response if (!strncmp(response, "ESGvES", 6)) return true; } usleep(50000); } return false; } bool get_pmc8_model(int fd, FirmwareInfo *info) { INDI_UNUSED(fd); // FIXME - only one model for now info->Model.assign("PMC-Eight"); return true; } bool get_pmc8_main_firmware(int fd, FirmwareInfo *info) { char cmd[] = "ESGv!"; char board[16]; int errcode = 0; char errmsg[MAXRBUF]; char response[24]; int nbytes_read = 0; int nbytes_written = 0; DEBUGFDEVICE(pmc8_device, INDI::Logger::DBG_DEBUG, "CMD (%s)", cmd); if (pmc8_simulation) { strcpy(response, PMC8_SIMUL_VERSION_RESP); nbytes_read = strlen(response); } else { tcflush(fd, TCIFLUSH); if ((errcode = tty_write(fd, cmd, strlen(cmd), &nbytes_written)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); DEBUGFDEVICE(pmc8_device, INDI::Logger::DBG_ERROR, "3 %s", errmsg); return false; } if ((errcode = tty_read_section(fd, response, '!', PMC8_TIMEOUT, &nbytes_read))) { tty_error_msg(errcode, errmsg, MAXRBUF); DEBUGFDEVICE(pmc8_device, INDI::Logger::DBG_ERROR, "get_pmc8_main_firmware(): Error reading response - %s", errmsg); return false; } } if (nbytes_read > 0) { response[nbytes_read] = '\0'; DEBUGFDEVICE(pmc8_device, INDI::Logger::DBG_DEBUG, "RES (%s)", response); if (nbytes_read == 14) { response[13] = '\0'; strncpy(board, response+6, 6); info->MainBoardFirmware.assign(board, 6); tcflush(fd, TCIFLUSH); return true; } } DEBUGFDEVICE(pmc8_device, INDI::Logger::DBG_ERROR, "Only received #%d bytes, expected 14.", nbytes_read); return false; } bool get_pmc8_firmware(int fd, FirmwareInfo *info) { bool rc = false; rc = get_pmc8_model(fd, info); if (rc == false) return rc; rc = get_pmc8_main_firmware(fd, info); return rc; } bool get_pmc8_tracking_rate_axis(int fd, PMC8_AXIS axis, int &rate) { char cmd[32]; int errcode = 0; char errmsg[MAXRBUF]; char response[16]; int nbytes_read = 0; int nbytes_written = 0; snprintf(cmd, sizeof(cmd), "ESGr%d!", axis); DEBUGFDEVICE(pmc8_device, INDI::Logger::DBG_DEBUG, "CMD (%s)", cmd); if (pmc8_simulation) { if (axis == PMC8_AXIS_RA) rate = simPMC8Data.trackRate; else if (axis == PMC8_AXIS_DEC) rate = 0; // DEC tracking not supported yet else return false; return true; } tcflush(fd, TCIFLUSH); if ((errcode = tty_write(fd, cmd, strlen(cmd), &nbytes_written)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); DEBUGFDEVICE(pmc8_device, INDI::Logger::DBG_ERROR, "%s", errmsg); return false; } if ((errcode = tty_read(fd, response, 10, PMC8_TIMEOUT, &nbytes_read))) { tty_error_msg(errcode, errmsg, MAXRBUF); DEBUGFDEVICE(pmc8_device, INDI::Logger::DBG_ERROR, "%s", errmsg); return false; } response[nbytes_read] = '\0'; DEBUGFDEVICE(pmc8_device, INDI::Logger::DBG_DEBUG, "RES (%s)", response); if (nbytes_read != 10) { DEBUGDEVICE(pmc8_device, INDI::Logger::DBG_ERROR, "Axis get track rate cmd response incorrect"); return false; } char num_str[16]= {0}; strcpy(num_str, "0X"); strncat(num_str, response+5, 6); rate = (int)strtol(num_str, NULL, 0); return true; } bool get_pmc8_direction_axis(int fd, PMC8_AXIS axis, int &dir) { char cmd[32]; int errcode = 0; char errmsg[MAXRBUF]; char response[16]; int nbytes_read = 0; int nbytes_written = 0; snprintf(cmd, sizeof(cmd), "ESGd%d!", axis); DEBUGFDEVICE(pmc8_device, INDI::Logger::DBG_DEBUG, "CMD (%s)", cmd); if (pmc8_simulation) { if (axis == PMC8_AXIS_RA) dir = simPMC8Data.raDirection; else if (axis == PMC8_AXIS_DEC) dir = simPMC8Data.decDirection; else return false; return true; } tcflush(fd, TCIFLUSH); if ((errcode = tty_write(fd, cmd, strlen(cmd), &nbytes_written)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); DEBUGFDEVICE(pmc8_device, INDI::Logger::DBG_ERROR, "%s", errmsg); return false; } if ((errcode = tty_read(fd, response, 7, PMC8_TIMEOUT, &nbytes_read))) { tty_error_msg(errcode, errmsg, MAXRBUF); DEBUGFDEVICE(pmc8_device, INDI::Logger::DBG_ERROR, "%s", errmsg); return false; } response[nbytes_read] = '\0'; DEBUGFDEVICE(pmc8_device, INDI::Logger::DBG_DEBUG, "RES (%s)", response); if (nbytes_read != 7) { DEBUGDEVICE(pmc8_device, INDI::Logger::DBG_ERROR, "Axis get dir cmd response incorrect"); return false; } char num_str[16]= {0}; strncat(num_str, response+5, 2); dir = (int)strtol(num_str, NULL, 0); return true; } // if fast is true dont wait on response! Used for psuedo-pulse guide // NOTE that this will possibly mean the response will be read by a following command if it is called before // response comes from controller, since next command will flush before data is in buffer! bool set_pmc8_direction_axis(int fd, PMC8_AXIS axis, int dir, bool fast) { char cmd[32], expresp[32]; int errcode = 0; char errmsg[MAXRBUF]; char response[16]; int nbytes_read = 0; int nbytes_written = 0; snprintf(cmd, sizeof(cmd), "ESSd%d%d!", axis, dir); DEBUGFDEVICE(pmc8_device, INDI::Logger::DBG_DEBUG, "CMD (%s)", cmd); if (pmc8_simulation) { if (axis == PMC8_AXIS_RA) simPMC8Data.raDirection = (PMC8_DIRECTION) dir; else if (axis == PMC8_AXIS_DEC) simPMC8Data.decDirection = (PMC8_DIRECTION) dir; else return false; return true; } tcflush(fd, TCIFLUSH); if ((errcode = tty_write(fd, cmd, strlen(cmd), &nbytes_written)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); DEBUGFDEVICE(pmc8_device, INDI::Logger::DBG_ERROR, "%s", errmsg); return false; } if (fast) return true; if ((errcode = tty_read(fd, response, 7, PMC8_TIMEOUT, &nbytes_read))) { tty_error_msg(errcode, errmsg, MAXRBUF); DEBUGFDEVICE(pmc8_device, INDI::Logger::DBG_ERROR, "%s", errmsg); return false; } response[nbytes_read] = '\0'; snprintf(expresp, sizeof(expresp), "ESGd%d%d!", axis, dir); DEBUGFDEVICE(pmc8_device, INDI::Logger::DBG_DEBUG, "RES (%s)", response); if (nbytes_read != 7 || strcmp(response, expresp)) { DEBUGFDEVICE(pmc8_device, INDI::Logger::DBG_ERROR, "Axis get dir cmd response incorrect: expected=%s", expresp); return false; } return true; } bool get_pmc8_is_scope_slewing(int fd, bool &isslew) { int rarate; int decrate; bool rc; rc=get_pmc8_tracking_rate_axis(fd, PMC8_AXIS_RA, rarate); if (!rc) { DEBUGDEVICE(pmc8_device, INDI::Logger::DBG_ERROR, "get_pmc8_is_scope_slewing(): Error reading RA tracking rate"); return false; } rc=get_pmc8_tracking_rate_axis(fd, PMC8_AXIS_DEC, decrate); if (!rc) { DEBUGDEVICE(pmc8_device, INDI::Logger::DBG_ERROR, "get_pmc8_is_scope_slewing(): Error reading DEC tracking rate"); return false; } if (pmc8_simulation) { isslew = (simPMC8Info.systemStatus == ST_SLEWING); return true; } else { isslew = ((rarate > PMC8_MINSLEWRATE) || (decrate > PMC8_MINSLEWRATE)); } return true; } int convert_movespeedindex_to_rate(int mode) { int r=0; switch (mode) { case 0: r = 4*15; break; case 1: r = 16*15; break; case 2: r = 64*15; break; case 3: r = 256*15; break; default: r = 0; break; } return r; } bool start_pmc8_motion(int fd, PMC8_DIRECTION dir, int mode) { bool isslew; // check speed if (get_pmc8_is_scope_slewing(fd, isslew) == false) { DEBUGDEVICE(pmc8_device, INDI::Logger::DBG_ERROR, "start_pmc8_motion(): Error reading slew state"); return false; } if (isslew) { DEBUGDEVICE(pmc8_device, INDI::Logger::DBG_ERROR, "start_pmc8_motion(): cannot start motion during slew!"); return false; } int rarate = 0; int decrate = 0; int reqrate = 0; reqrate = convert_movespeedindex_to_rate(mode); if (reqrate > PMC8_MAX_MOVE_MOTOR_RATE) reqrate = PMC8_MAX_MOVE_MOTOR_RATE; else if (reqrate < -PMC8_MAX_MOVE_MOTOR_RATE) reqrate = -PMC8_MAX_MOVE_MOTOR_RATE; switch (dir) { case PMC8_N: decrate = reqrate; break; case PMC8_S: decrate = -reqrate; break; case PMC8_W: rarate = reqrate; // doesn't accord for sidereal motion break; case PMC8_E: rarate = -reqrate; // doesn't accord for sidereal motion break; } if (rarate != 0) set_pmc8_custom_ra_move_rate(fd, rarate); if (decrate != 0) set_pmc8_custom_dec_move_rate(fd, decrate); return true; } bool stop_pmc8_tracking_motion(int fd) { bool rc; // stop tracking rc = set_pmc8_custom_ra_track_rate(fd, 0); if (!rc) { DEBUGDEVICE(pmc8_device, INDI::Logger::DBG_ERROR, "Error stopping RA axis!"); return false; } return true; } bool stop_pmc8_motion(int fd, PMC8_DIRECTION dir) { bool rc = false; switch (dir) { case PMC8_N: case PMC8_S: rc = set_pmc8_custom_dec_move_rate(fd, 0); break; case PMC8_W: case PMC8_E: rc = set_pmc8_custom_ra_move_rate(fd, 0); break; default: return false; break; } return rc; } // convert mount count to 6 character two complement hex string void convert_motor_counts_to_hex(int val, char *hex) { unsigned tmp; char h[16]; if (val < 0) { tmp=abs(val); tmp=~tmp; tmp++; } else { tmp=val; } sprintf(h, "%08X", tmp); strcpy(hex, h+2); DEBUGFDEVICE(pmc8_device, INDI::Logger::DBG_DEBUG, "convert_motor_counts_to_hex val=%d, h=%s, hex=%s", val, h, hex); } // convert rate in arcsec/sidereal_second to internal PMC8 motor rate for RA axis ONLY bool convert_precise_rate_to_motor(float rate, int *mrate) { *mrate = (int)(25*rate*(PMC8_AXIS0_SCALE/ARCSEC_IN_CIRCLE)); if (*mrate > PMC8_MAX_PRECISE_MOTOR_RATE) *mrate = PMC8_MAX_PRECISE_MOTOR_RATE; else if (*mrate < -PMC8_MAX_PRECISE_MOTOR_RATE) *mrate = -PMC8_MAX_PRECISE_MOTOR_RATE; return true; } // convert rate in arcsec/sidereal_second to internal PMC8 motor rate for move action (not slewing) bool convert_move_rate_to_motor(float rate, int *mrate) { *mrate = (int)(rate*(PMC8_AXIS0_SCALE/ARCSEC_IN_CIRCLE)); if (*mrate > PMC8_MAX_MOVE_MOTOR_RATE) *mrate = PMC8_MAX_MOVE_MOTOR_RATE; else if (*mrate < -PMC8_MAX_MOVE_MOTOR_RATE) *mrate = -PMC8_MAX_MOVE_MOTOR_RATE; return true; } // convert rate internal PMC8 motor rate to arcsec/sec for move action (not slewing) bool convert_motor_rate_to_move_rate(int mrate, float *rate) { *rate = ((double)mrate)*ARCSEC_IN_CIRCLE/PMC8_AXIS0_SCALE; return true; } // set speed for move action (MoveNS/MoveWE) NOT slews! This version DOESNT handle direciton and expects a motor rate! // if fast is true dont wait on response! Used for psuedo-pulse guide // NOTE that this will possibly mean the response will be read by a following command if it is called before // response comes from controller, since next command will flush before data is in buffer! bool set_pmc8_axis_motor_rate(int fd, PMC8_AXIS axis, int mrate, bool fast) { char cmd[24]; int errcode = 0; char errmsg[MAXRBUF]; char response[24]; int nbytes_read = 0; int nbytes_written = 0; snprintf(cmd, sizeof(cmd), "ESSr%d%04X!", axis, mrate); DEBUGFDEVICE(pmc8_device, INDI::Logger::DBG_DEBUG, "CMD (%s)", cmd); if (pmc8_simulation) { return true; } tcflush(fd, TCIFLUSH); if ((errcode = tty_write(fd, cmd, strlen(cmd), &nbytes_written)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); DEBUGFDEVICE(pmc8_device, INDI::Logger::DBG_ERROR, "%s", errmsg); return false; } if (fast) return true; if ((errcode = tty_read(fd, response, strlen(cmd), PMC8_TIMEOUT, &nbytes_read))) { tty_error_msg(errcode, errmsg, MAXRBUF); DEBUGFDEVICE(pmc8_device, INDI::Logger::DBG_ERROR, "%s", errmsg); return false; } if (nbytes_read == 10) { response[nbytes_read] = '\0'; DEBUGFDEVICE(pmc8_device, INDI::Logger::DBG_DEBUG, "RES (%s)", response); tcflush(fd, TCIFLUSH); return true; } DEBUGFDEVICE(pmc8_device, INDI::Logger::DBG_ERROR, "Only received #%d bytes, expected 10.", nbytes_read); return false; } // set speed for move action (MoveNS/MoveWE) NOT slews! This version accepts arcsec/sec as rate. // also handles direction bool set_pmc8_axis_move_rate(int fd, PMC8_AXIS axis, float rate) { bool rc; int motor_rate; // set direction if (rate < 0) rc=set_pmc8_direction_axis(fd, axis, 0, false); else rc=set_pmc8_direction_axis(fd, axis, 1, false); if (!rc) return rc; if (!convert_move_rate_to_motor(fabs(rate), &motor_rate)) { DEBUGFDEVICE(pmc8_device, INDI::Logger::DBG_ERROR, "Error converting rate %f", rate); return false; } rc = set_pmc8_axis_motor_rate(fd, axis, motor_rate, false); if (pmc8_simulation) { simPMC8Data.moveRate = rate; return true; } return rc; } #if 0 bool set_pmc8_track_enabled(int fd, bool enabled) { char cmd[32]; int errcode = 0; char errmsg[MAXRBUF]; char response[8]; int nbytes_read = 0; int nbytes_written = 0; snprintf(cmd, 32, ":ST%d#", enabled ? 1 : 0); DEBUGFDEVICE(pmc8_device, INDI::Logger::DBG_DEBUG, "CMD (%s)", cmd); if (pmc8_simulation) { // FIXME - need to implement pmc8 track enabled sim // simPMC8Info.systemStatus = enabled ? ST_TRACKING_PEC_ON : ST_STOPPED; // strcpy(response, "1"); // nbytes_read = strlen(response); DEBUGDEVICE(pmc8_device, INDI::Logger::DBG_ERROR, "Need to implement pmc8 track enabled sim"); return false; } else { // determine current tracking mode // return SetTrackMode(enabled ? IUFindOnSwitchIndex(&TrackModeSP) : AP_TRACKING_OFF); } } #endif bool set_pmc8_track_mode(int fd, uint rate) { float ratereal=0; switch (rate) { case PMC8_TRACK_SIDEREAL: ratereal = 15.0; break; case PMC8_TRACK_LUNAR: ratereal = 14.453; break; case PMC8_TRACK_SOLAR: ratereal = 15.041; break; default: return false; break; } return set_pmc8_custom_ra_track_rate(fd, ratereal); } bool set_pmc8_custom_ra_track_rate(int fd, double rate) { DEBUGFDEVICE(pmc8_device, INDI::Logger::DBG_DEBUG, "set_pmc8_custom_ra_track_rate() called rate=%f ", rate); char cmd[24]; int errcode = 0; char errmsg[MAXRBUF]; char response[24]; int nbytes_read = 0; int nbytes_written = 0; int rateval; if (!convert_precise_rate_to_motor(rate, &rateval)) { DEBUGFDEVICE(pmc8_device, INDI::Logger::DBG_ERROR, "Error converting rate %f", rate); return false; } snprintf(cmd, sizeof(cmd), "ESTr%04X!", rateval); DEBUGFDEVICE(pmc8_device, INDI::Logger::DBG_DEBUG, "CMD (%s)", cmd); if (pmc8_simulation) { simPMC8Data.trackRate = rate; return true; } else { tcflush(fd, TCIFLUSH); if ((errcode = tty_write(fd, cmd, strlen(cmd), &nbytes_written)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); DEBUGFDEVICE(pmc8_device, INDI::Logger::DBG_ERROR, "%s", errmsg); return false; } if ((errcode = tty_read(fd, response, strlen(cmd), PMC8_TIMEOUT, &nbytes_read))) { tty_error_msg(errcode, errmsg, MAXRBUF); DEBUGFDEVICE(pmc8_device, INDI::Logger::DBG_ERROR, "%s", errmsg); return false; } } if (nbytes_read != 9) { DEBUGFDEVICE(pmc8_device, INDI::Logger::DBG_ERROR, "Only received #%d bytes, expected 9.", nbytes_read); return false; } response[nbytes_read] = '\0'; DEBUGFDEVICE(pmc8_device, INDI::Logger::DBG_DEBUG, "RES (%s)", response); tcflush(fd, TCIFLUSH); // set direction to 1 return set_pmc8_direction_axis(fd, PMC8_AXIS_RA, 1, false); } #if 0 bool set_pmc8_custom_dec_track_rate(int fd, double rate) { bool rc; DEBUGFDEVICE(pmc8_device, INDI::Logger::DBG_DEBUG, "set_pmc8_custom_dec_track_rate() called rate=%f ", rate); if (pmc8_simulation) { DEBUGDEVICE(pmc8_device, INDI::Logger::DBG_ERROR, "set_pmc8_custom_dec_track_rate simulation not implemented"); rc=false; } else { rc=set_pmc8_axis_rate(fd, PMC8_AXIS_DEC, rate); } return rc; } #else bool set_pmc8_custom_dec_track_rate(int fd, double rate) { INDI_UNUSED(fd); INDI_UNUSED(rate); DEBUGDEVICE(pmc8_device, INDI::Logger::DBG_ERROR, "set_pmc8_custom_dec_track_rate not implemented!"); return false; } #endif bool set_pmc8_custom_ra_move_rate(int fd, double rate) { bool rc; DEBUGFDEVICE(pmc8_device, INDI::Logger::DBG_DEBUG, "set_pmc8_custom_ra move_rate() called rate=%f ", rate); // safe guard for now - only all use to STOP slewing or MOVE commands with this if (fabs(rate) > PMC8_MAX_MOVE_MOTOR_RATE) { DEBUGDEVICE(pmc8_device, INDI::Logger::DBG_ERROR, "set_pmc8_custom_ra_move rate only supports low rates currently"); return false; } rc=set_pmc8_axis_move_rate(fd, PMC8_AXIS_RA, rate); return rc; } bool set_pmc8_custom_dec_move_rate(int fd, double rate) { bool rc; DEBUGFDEVICE(pmc8_device, INDI::Logger::DBG_DEBUG, "set_pmc8_custom_dec_move_rate() called rate=%f ", rate); // safe guard for now - only all use to STOP slewing with this if (fabs(rate) > PMC8_MAX_MOVE_MOTOR_RATE) { DEBUGDEVICE(pmc8_device, INDI::Logger::DBG_ERROR, "set_pmc8_custom_dec_move_rate only supports low rates currently"); return false; } rc=set_pmc8_axis_move_rate(fd, PMC8_AXIS_DEC, rate); return rc; } // rate is fraction of sidereal bool set_pmc8_guide_rate(int fd, double rate) { INDI_UNUSED(fd); pmc8_guide_rate = rate * 15.0; DEBUGFDEVICE(pmc8_device, INDI::Logger::DBG_DEBUG, "set_pmc8_guide_rate: guide rate set to %f arcsec/sec", pmc8_guide_rate); return true; } // not yet implemented for PMC8 bool get_pmc8_guide_rate(int fd, double *rate) { INDI_UNUSED(fd); INDI_UNUSED(rate); DEBUGDEVICE(pmc8_device, INDI::Logger::DBG_ERROR, "get_pmc8_guide_rate not implemented!"); return false; } // if return value is true then timetaken will return how much pulse time has already occurred bool start_pmc8_guide(int fd, PMC8_DIRECTION gdir, int ms, long &timetaken_us) { bool rc; int cur_ra_rate; int cur_dec_rate; int cur_ra_dir; int cur_dec_dir; // used to test timing struct timeval tp; long long pulse_start_us; long long pulse_sofar_us; PulseGuideState *pstate; DEBUGFDEVICE(pmc8_device, INDI::Logger::DBG_DEBUG, "pmc8_start_guide(): pulse dir=%d dur=%d ms", gdir, ms); // figure out which state variable to use switch (gdir) { case PMC8_N: case PMC8_S: pstate = &NS_PulseGuideState; break; case PMC8_W: case PMC8_E: pstate = &EW_PulseGuideState; break; default: return false; break; } if (pstate->pulseguideactive) { DEBUGDEVICE(pmc8_device, INDI::Logger::DBG_ERROR, "pmc8_start_guide(): already executing a pulse guide!"); return false; } // ignore short pulses - they do nothing if (ms < PMC8_PULSE_GUIDE_MIN_MS) { DEBUGFDEVICE(pmc8_device, INDI::Logger::DBG_DEBUG, "pmc8_start_guide(): ignore short pulse ms=%d ms", ms); timetaken_us = ms*1000; pstate->pulseguideactive = true; pstate->fakepulse = true; return true; } // experimental implementation: // 1) Get current rates // 2) Set new rates based on guide direction // 3) Wait guide duration // 4) Set back to initial rates // // NOTE FIXME HACK: This blocks for duration of guide correction!! // rc = get_pmc8_tracking_rate_axis(fd, PMC8_AXIS_RA, cur_ra_rate); if (!rc) { DEBUGDEVICE(pmc8_device, INDI::Logger::DBG_ERROR, "pmc8_start_guide(): error reading current RA rate!"); return rc; } rc = get_pmc8_tracking_rate_axis(fd, PMC8_AXIS_DEC, cur_dec_rate); if (!rc) { DEBUGDEVICE(pmc8_device, INDI::Logger::DBG_ERROR, "pmc8_start_guide(): error reading current DEC rate!"); return rc; } // if slewing abort if ((cur_ra_rate > PMC8_PULSE_GUIDE_MAX_CURRATE) || (cur_dec_rate > PMC8_PULSE_GUIDE_MAX_CURRATE)) { DEBUGFDEVICE(pmc8_device, INDI::Logger::DBG_ERROR, "pmc8_start_guide(): Cannot send guide correction while slewing! rarate=%d decrate=%d", cur_ra_rate, cur_dec_rate); return rc; } rc = get_pmc8_direction_axis(fd, PMC8_AXIS_RA, cur_ra_dir); if (!rc) { DEBUGDEVICE(pmc8_device, INDI::Logger::DBG_ERROR, "pmc8_start_guide(): error reading current RA DIR!"); return rc; } rc = get_pmc8_direction_axis(fd, PMC8_AXIS_DEC, cur_dec_dir); if (!rc) { DEBUGDEVICE(pmc8_device, INDI::Logger::DBG_ERROR, "pmc8_start_guide(): error reading current DEC DIR!"); return rc; } // compute new rates int new_ra_rate; int new_dec_rate; int new_ra_dir; int new_dec_dir; new_ra_rate = cur_ra_rate; new_dec_rate = cur_dec_rate; new_ra_dir = cur_ra_dir; new_dec_dir = cur_dec_dir; // FIXME - hard code guide rate as 0.5 siedereal for testing int guide_mrate; // convert to motor rate convert_move_rate_to_motor(pmc8_guide_rate, &guide_mrate); switch (gdir) { case PMC8_N: new_dec_rate = cur_dec_rate + guide_mrate; break; case PMC8_S: new_dec_rate = cur_dec_rate - guide_mrate; break; case PMC8_W: new_ra_rate = cur_ra_rate + guide_mrate; break; case PMC8_E: new_ra_rate = cur_ra_rate - guide_mrate; break; default: return false; break; } // see if we need to switch direction if (new_ra_rate < 0) { new_ra_rate = abs(new_ra_rate); if (cur_ra_dir == 0) new_ra_dir = 1; else new_ra_dir = 0; } // FIXME - not sure about handling direction for guide corrections in relation to N/S and EpW vs WpE?? if (new_dec_rate < 0) new_dec_dir = 1; else new_dec_dir = 0; new_dec_rate = abs(new_dec_rate); // DEBUGFDEVICE(pmc8_device, INDI::Logger::DBG_DEBUG, "pmc8_start_guide(): gdir=%d dur=%d cur_ra_rate=%d cur_ra_dir=%d cur_dec_rate=%d cur_dec_dir=%d", // gdir, ms, cur_ra_rate, cur_ra_dir, cur_dec_rate, cur_dec_dir); // DEBUGFDEVICE(pmc8_device, INDI::Logger::DBG_DEBUG, "pmc8_start_guide(): new_ra_rate=%d new_ra_dir=%d new_dec_rate=%d new_dec_dir=%d", // new_ra_rate, new_ra_dir, new_dec_rate, new_dec_dir); // measure time when we start pulse gettimeofday(&tp, NULL); pulse_start_us = tp.tv_sec*1000000+tp.tv_usec; // we will either send an RA pulse or DEC pulse but not both // code above ensures we either are moving E/W or N/S but not a combination if ((new_ra_rate != cur_ra_rate) || (new_ra_dir != cur_ra_dir)) { // measure time when we start pulse // gettimeofday(&tp, NULL); // pulse_start_us = tp.tv_sec*1000000+tp.tv_usec; // the commands to set rate and direction take 10-20 msec to return due to they wait for the response from the mount // we need to incorporate that time in our total pulse delay later! // not sure if best to flip dir or rate first! if (new_ra_rate != cur_ra_rate) if (!set_pmc8_axis_motor_rate(fd, PMC8_AXIS_RA, new_ra_rate, false)) DEBUGDEVICE(pmc8_device, INDI::Logger::DBG_DEBUG, "pmc8_start_guide(): error settings new_ra_rate"); if (new_ra_dir != cur_ra_dir) if (!set_pmc8_direction_axis(fd, PMC8_AXIS_RA, new_ra_dir, false)) DEBUGDEVICE(pmc8_device, INDI::Logger::DBG_DEBUG, "pmc8_start_guide(): error settings new_ra_dir"); } else if ((new_dec_rate != cur_dec_rate) || (new_dec_dir != cur_dec_dir)) { // measure time when we start pulse // gettimeofday(&tp, NULL); // pulse_start_us = tp.tv_sec*1000000+tp.tv_usec; // the commands to set rate and direction take 10-20 msec to return due to they wait for the response from the mount // we need to incorporate that time in our total pulse delay later! // not sure if best to flip dir or rate first! if (new_dec_rate != cur_dec_rate) if (!set_pmc8_axis_motor_rate(fd, PMC8_AXIS_DEC, new_dec_rate, false)) DEBUGDEVICE(pmc8_device, INDI::Logger::DBG_DEBUG, "pmc8_start_guide(): error settings new_dec_rate"); if (new_dec_dir != cur_dec_dir) if (!set_pmc8_direction_axis(fd, PMC8_AXIS_DEC, new_dec_dir, false)) DEBUGDEVICE(pmc8_device, INDI::Logger::DBG_DEBUG, "pmc8_start_guide(): error settings new_dec_dir"); } // store state pstate->pulseguideactive = true; pstate->fakepulse = false; pstate->ms = ms; pstate->pulse_start_us = pulse_start_us; pstate->cur_ra_rate = cur_ra_rate; pstate->cur_ra_dir = cur_ra_dir; pstate->cur_dec_rate = cur_dec_rate; pstate->cur_dec_dir = cur_dec_dir; pstate->new_ra_rate = new_ra_rate; pstate->new_ra_dir = new_ra_dir; pstate->new_dec_rate = new_dec_rate; pstate->new_dec_dir = new_dec_dir; // see how long we've waited gettimeofday(&tp, NULL); pulse_sofar_us = (tp.tv_sec*1000000+tp.tv_usec) - pulse_start_us; timetaken_us = pulse_sofar_us; DEBUGFDEVICE(pmc8_device, INDI::Logger::DBG_DEBUG, "pmc8_start_guide(): timetaken_us=%d us", timetaken_us); return true; } bool stop_pmc8_guide(int fd, PMC8_DIRECTION gdir) { struct timeval tp; long long pulse_end_us; PulseGuideState *pstate; // figure out which state variable to use switch (gdir) { case PMC8_N: case PMC8_S: pstate = &NS_PulseGuideState; break; case PMC8_W: case PMC8_E: pstate = &EW_PulseGuideState; break; default: return false; break; } DEBUGFDEVICE(pmc8_device, INDI::Logger::DBG_DEBUG, "pmc8_stop_guide(): pulse dir=%d dur=%d ms", gdir, pstate->ms); if (!pstate->pulseguideactive) { DEBUGDEVICE(pmc8_device, INDI::Logger::DBG_ERROR, "pmc8_stop_guide(): pulse guide not active!!"); return false; } // flush any responses to commands we ignored above! tcflush(fd, TCIFLUSH); // "fake pulse" - it was so short we would have overshot its length AND the motors wouldn't have moved anyways if (pstate->fakepulse) { DEBUGDEVICE(pmc8_device, INDI::Logger::DBG_DEBUG, "pmc8_stop_guide(): fake pulse done"); pstate->pulseguideactive = false; return true; } // restore previous tracking - only change ones we need to! if ((pstate->new_ra_rate != pstate->cur_ra_rate) || (pstate->new_ra_dir != pstate->cur_ra_dir)) { // not sure if best to flip dir or rate first! // if (new_ra_rate != cur_ra_rate) // set_pmc8_axis_motor_rate(fd, PMC8_AXIS_RA, cur_ra_rate, true); // FIXME - for now restore sidereal tracking gettimeofday(&tp, NULL); pulse_end_us = tp.tv_sec*1000000+tp.tv_usec; if (pstate->new_ra_rate != pstate->cur_ra_rate) set_pmc8_track_mode(fd, PMC8_TRACK_SIDEREAL); if (pstate->new_ra_dir != pstate->cur_ra_dir) set_pmc8_direction_axis(fd, PMC8_AXIS_RA, pstate->cur_ra_dir, false); DEBUGFDEVICE(pmc8_device, INDI::Logger::DBG_DEBUG, "pmc8_stop_guide(): requested = %d ms, actual = %f ms", pstate->ms, (pulse_end_us-pstate->pulse_start_us)/1000.0); } if ((pstate->new_dec_rate != pstate->cur_dec_rate) || (pstate->new_dec_dir != pstate->cur_dec_dir)) { // not sure if best to flip dir or rate first! gettimeofday(&tp, NULL); pulse_end_us = tp.tv_sec*1000000+tp.tv_usec; if (pstate->new_dec_rate != pstate->cur_dec_rate) set_pmc8_axis_motor_rate(fd, PMC8_AXIS_DEC, pstate->cur_dec_rate, false); if (pstate->new_dec_dir != pstate->cur_dec_dir) set_pmc8_direction_axis(fd, PMC8_AXIS_DEC, pstate->cur_dec_dir, false); DEBUGFDEVICE(pmc8_device, INDI::Logger::DBG_DEBUG, "pmc8_stop_guide(): requested = %d ms, actual = %f ms", pstate->ms, (pulse_end_us-pstate->pulse_start_us)/1000.0); } // sleep to let any responses occurs and clean up! // usleep(15000); // flush any responses to commands we ignored above! tcflush(fd, TCIFLUSH); // mark pulse done pstate->pulseguideactive = false; return true; } // convert from axis position returned by controller to motor counts used in conversion to RA/DEC int convert_axispos_to_motor(int axispos) { int r; if (axispos > 8388608) r = 0 - (16777216 - axispos); else r = axispos; return r; } bool convert_ra_to_motor(double ra, INDI::Telescope::TelescopePierSide sop, int *mcounts) { double motor_angle; double hour_angle; double lst; // DEBUGFDEVICE(pmc8_device, INDI::Logger::DBG_DEBUG, "convert_ra_to_motor - ra=%f sop=%d", ra, sop); lst = get_local_sidereal_time(pmc8_longitude); hour_angle = lst- ra; // limit values to +/- 12 hours if (hour_angle > 12) hour_angle = hour_angle - 24; else if (hour_angle <= -12) hour_angle = hour_angle + 24; if (sop == INDI::Telescope::PIER_EAST) motor_angle = hour_angle - 6; else if (sop == INDI::Telescope::PIER_WEST) motor_angle = hour_angle + 6; else return false; // DEBUGFDEVICE(pmc8_device, INDI::Logger::DBG_DEBUG, "convert_ra_to_motor - lst = %f hour_angle=%f", lst, hour_angle); *mcounts = motor_angle * PMC8_AXIS0_SCALE / 24; // DEBUGFDEVICE(pmc8_device, INDI::Logger::DBG_DEBUG, "convert_ra_to_motor - motor_angle=%f *mcounts=%d", motor_angle, *mcounts); return true; } bool convert_motor_to_radec(int racounts, int deccounts, double &ra_value, double &dec_value) { double motor_angle; double hour_angle; //double sid_time; double lst; lst = get_local_sidereal_time(pmc8_longitude); // DEBUGFDEVICE(pmc8_device, INDI::Logger::DBG_DEBUG, "lst = %f", lst); motor_angle = (24.0 * racounts) / PMC8_AXIS0_SCALE; // DEBUGFDEVICE(pmc8_device, INDI::Logger::DBG_DEBUG, "racounts = %d motor_angle = %f", racounts, motor_angle); if (deccounts < 0) hour_angle = motor_angle + 6; else hour_angle = motor_angle - 6; // DEBUGFDEVICE(pmc8_device, INDI::Logger::DBG_DEBUG, "hour_angle = %f", hour_angle); ra_value = lst - hour_angle; // DEBUGFDEVICE(pmc8_device, INDI::Logger::DBG_DEBUG, "ra_value = %f", ra_value); if (ra_value >= 24.0) ra_value = ra_value - 24.0; else if (ra_value < 0.0) ra_value = ra_value + 24.0; // DEBUGFDEVICE(pmc8_device, INDI::Logger::DBG_DEBUG, "ra_value (final) = %f", ra_value); motor_angle = (360.0 * deccounts) / PMC8_AXIS1_SCALE; if (motor_angle >= 0) dec_value = 90 - motor_angle; else dec_value = 90 + motor_angle; return true; } bool convert_dec_to_motor(double dec, INDI::Telescope::TelescopePierSide sop, int *mcounts) { double motor_angle; if (sop == INDI::Telescope::PIER_EAST) motor_angle = (dec - 90.0); else if (sop == INDI::Telescope::PIER_WEST) motor_angle = -(dec - 90.0); else return false; *mcounts = (motor_angle / 360.0) * PMC8_AXIS1_SCALE; // DEBUGFDEVICE(pmc8_device, INDI::Logger::DBG_DEBUG, "convert_dec_to_motor dec = %f, sop = %d", dec, sop); // DEBUGFDEVICE(pmc8_device, INDI::Logger::DBG_DEBUG, "convert_dec_to_motor motor_angle = %f, motor_counts= %d", motor_angle, *mcounts); return true; } bool set_pmc8_target_position_axis(int fd, PMC8_AXIS axis, int point) { char cmd[32]; char expresp[32]; char hexpt[16]; int errcode = 0; char errmsg[MAXRBUF]; char response[16]; int nbytes_read = 0; int nbytes_written = 0; convert_motor_counts_to_hex(point, hexpt); snprintf(cmd, sizeof(cmd), "ESPt%d%s!", axis, hexpt); DEBUGFDEVICE(pmc8_device, INDI::Logger::DBG_DEBUG, "CMD (%s)", cmd); if (pmc8_simulation) return true; tcflush(fd, TCIFLUSH); if ((errcode = tty_write(fd, cmd, strlen(cmd), &nbytes_written)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); DEBUGFDEVICE(pmc8_device, INDI::Logger::DBG_ERROR, "%s", errmsg); return false; } if ((errcode = tty_read(fd, response, strlen(cmd), PMC8_TIMEOUT, &nbytes_read))) { tty_error_msg(errcode, errmsg, MAXRBUF); DEBUGFDEVICE(pmc8_device, INDI::Logger::DBG_ERROR, "%s", errmsg); return false; } response[nbytes_read] = '\0'; if (nbytes_read > 0) DEBUGFDEVICE(pmc8_device, INDI::Logger::DBG_DEBUG, "RES (%s)", response); // compare to expected response snprintf(expresp, sizeof(expresp), "ESGt%d%s!", axis, hexpt); if (strncmp(response, expresp, strlen(response))) { DEBUGFDEVICE(pmc8_device, INDI::Logger::DBG_ERROR, "Axis Set Point cmd response incorrect: %s - expected %s", response, expresp); return false; } return true; } bool set_pmc8_target_position(int fd, int rapoint, int decpoint) { bool rc; rc = set_pmc8_target_position_axis(fd, PMC8_AXIS_RA, rapoint); if (!rc) return rc; rc = set_pmc8_target_position_axis(fd, PMC8_AXIS_DEC, decpoint); return rc; } bool set_pmc8_position_axis(int fd, PMC8_AXIS axis, int point) { char cmd[32]; char expresp[32]; char hexpt[16]; int errcode = 0; char errmsg[MAXRBUF]; char response[16]; int nbytes_read = 0; int nbytes_written = 0; if (pmc8_simulation) { // FIXME - need to implement simulation code for setting point position return true; } convert_motor_counts_to_hex(point, hexpt); snprintf(cmd, sizeof(cmd), "ESSp%d%s!", axis, hexpt); DEBUGFDEVICE(pmc8_device, INDI::Logger::DBG_DEBUG, "CMD (%s)", cmd); tcflush(fd, TCIFLUSH); if ((errcode = tty_write(fd, cmd, strlen(cmd), &nbytes_written)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); DEBUGFDEVICE(pmc8_device, INDI::Logger::DBG_ERROR, "%s", errmsg); return false; } if ((errcode = tty_read(fd, response, strlen(cmd), PMC8_TIMEOUT, &nbytes_read))) { tty_error_msg(errcode, errmsg, MAXRBUF); DEBUGFDEVICE(pmc8_device, INDI::Logger::DBG_ERROR, "%s", errmsg); return false; } response[nbytes_read] = '\0'; if (nbytes_read > 0) DEBUGFDEVICE(pmc8_device, INDI::Logger::DBG_DEBUG, "RES (%s)", response); // compare to expected response snprintf(expresp, sizeof(expresp), "ESGp%d%s!", axis, hexpt); if (strncmp(response, expresp, strlen(response))) { DEBUGFDEVICE(pmc8_device, INDI::Logger::DBG_ERROR, "Axis Set Point cmd response incorrect: %s - expected %s", response, expresp); return false; } return true; } bool set_pmc8_position(int fd, int rapoint, int decpoint) { bool rc; rc = set_pmc8_position_axis(fd, PMC8_AXIS_RA, rapoint); if (!rc) return rc; rc = set_pmc8_position_axis(fd, PMC8_AXIS_DEC, decpoint); return rc; } bool get_pmc8_position_axis(int fd, PMC8_AXIS axis, int &point) { char cmd[32]; int errcode = 0; char errmsg[MAXRBUF]; char response[16]; int nbytes_read = 0; int nbytes_written = 0; if (pmc8_simulation) { // FIXME - need to implement simulation code for setting point position return true; } snprintf(cmd, sizeof(cmd), "ESGp%d!", axis); DEBUGFDEVICE(pmc8_device, INDI::Logger::DBG_DEBUG, "CMD (%s)", cmd); tcflush(fd, TCIFLUSH); if ((errcode = tty_write(fd, cmd, strlen(cmd), &nbytes_written)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); DEBUGFDEVICE(pmc8_device, INDI::Logger::DBG_ERROR, "%s", errmsg); return false; } if ((errcode = tty_read(fd, response, 12, PMC8_TIMEOUT, &nbytes_read))) { tty_error_msg(errcode, errmsg, MAXRBUF); DEBUGFDEVICE(pmc8_device, INDI::Logger::DBG_ERROR, "%s", errmsg); return false; } response[nbytes_read] = '\0'; DEBUGFDEVICE(pmc8_device, INDI::Logger::DBG_DEBUG, "RES (%s)", response); if (nbytes_read != 12) { DEBUGDEVICE(pmc8_device, INDI::Logger::DBG_ERROR, "Axis Set Point cmd response incorrect"); return false; } char num_str[16]= {0}; strcpy(num_str, "0X"); strncat(num_str, response+5, 6); //point = atoi(num_str); point = (int)strtol(num_str, NULL, 0); // DEBUGFDEVICE(pmc8_device, INDI::Logger::DBG_DEBUG, "get pos num_str = %s atoi() returns %d", num_str, point); return true; } bool get_pmc8_position(int fd, int &rapoint, int &decpoint) { bool rc; int axis_ra_pos, axis_dec_pos; rc = get_pmc8_position_axis(fd, PMC8_AXIS_RA, axis_ra_pos); if (!rc) return rc; rc = get_pmc8_position_axis(fd, PMC8_AXIS_DEC, axis_dec_pos); if (!rc) return rc; // convert from axis position to motor counts rapoint = convert_axispos_to_motor(axis_ra_pos); decpoint = convert_axispos_to_motor(axis_dec_pos); // DEBUGFDEVICE(pmc8_device, INDI::Logger::DBG_DEBUG, "ra axis pos = 0x%x motor_counts=%d", axis_ra_pos, rapoint); // DEBUGFDEVICE(pmc8_device, INDI::Logger::DBG_DEBUG, "dec axis pos = 0x%x motor_counts=%d", axis_dec_pos, decpoint); return rc; } bool park_pmc8(int fd) { bool rc; rc = set_pmc8_target_position(fd, 0, 0); // FIXME - Need to add code to handle simulation and also setting any scope state values return rc; } bool unpark_pmc8(int fd) { INDI_UNUSED(fd); // nothing really to do for PMC8 there is no unpark command if (pmc8_simulation) { set_pmc8_sim_system_status(ST_STOPPED); return true; } // FIXME - probably need to set a state variable to show we're unparked DEBUGDEVICE(pmc8_device, INDI::Logger::DBG_DEBUG, "PMC8 unparked"); return true; } bool abort_pmc8(int fd) { bool rc; if (pmc8_simulation) { // FIXME - need to do something to represent mount has stopped slewing DEBUGDEVICE(pmc8_device, INDI::Logger::DBG_DEBUG, "PMC8 slew stopped in simulation - need to add more code?"); return true; } // stop move/slew rates rc = set_pmc8_custom_ra_move_rate(fd, 0); if (!rc) { DEBUGDEVICE(pmc8_device, INDI::Logger::DBG_ERROR, "Error stopping RA axis!"); return false; } rc = set_pmc8_custom_dec_move_rate(fd, 0); if (!rc) { DEBUGDEVICE(pmc8_device, INDI::Logger::DBG_ERROR, "Error stopping DEC axis!"); return false; } return true; } // "slew" on PMC8 is instantaneous once you set the target ra/dec // no concept of setting target and then starting a slew operation as two steps bool slew_pmc8(int fd, double ra, double dec) { bool rc; int racounts, deccounts; INDI::Telescope::TelescopePierSide sop; DEBUGFDEVICE(pmc8_device, INDI::Logger::DBG_DEBUG, "slew_pmc8: ra=%f dec=%f", ra, dec); sop = destSideOfPier(ra, dec); rc = convert_ra_to_motor(ra, sop, &racounts); if (!rc) { DEBUGDEVICE(pmc8_device, INDI::Logger::DBG_ERROR, "slew_pmc8: error convering RA to motor counts"); return false; } rc = convert_dec_to_motor(dec, sop, &deccounts); if (!rc) { DEBUGDEVICE(pmc8_device, INDI::Logger::DBG_ERROR, "slew_pmc8: error convering DEC to motor counts"); return false; } rc = set_pmc8_target_position(fd, racounts, deccounts); if (!rc) { DEBUGDEVICE(pmc8_device, INDI::Logger::DBG_ERROR, "Error slewing PMC8"); return false; } if (pmc8_simulation) { set_pmc8_sim_system_status(ST_SLEWING); } return true; } INDI::Telescope::TelescopePierSide destSideOfPier(double ra, double dec) { double hour_angle; double lst; INDI_UNUSED(dec); lst = get_local_sidereal_time(pmc8_longitude); hour_angle = lst - ra; // limit values to +/- 12 hours if (hour_angle > 12) hour_angle = hour_angle - 24; else if (hour_angle <= -12) hour_angle = hour_angle + 24; if (hour_angle < 0.0) return INDI::Telescope::PIER_WEST; else return INDI::Telescope::PIER_EAST; } bool sync_pmc8(int fd, double ra, double dec) { bool rc; int racounts, deccounts; INDI::Telescope::TelescopePierSide sop; DEBUGFDEVICE(pmc8_device, INDI::Logger::DBG_DEBUG, "sync_pmc8: ra=%f dec=%f", ra, dec); sop = destSideOfPier(ra, dec); rc = convert_ra_to_motor(ra, sop, &racounts); if (!rc) { DEBUGDEVICE(pmc8_device, INDI::Logger::DBG_ERROR, "sync_pmc8: error convering RA to motor counts"); return false; } rc = convert_dec_to_motor(dec, sop, &deccounts); if (!rc) { DEBUGDEVICE(pmc8_device, INDI::Logger::DBG_ERROR, "sync_pmc8: error convering DEC to motor counts"); return false; } if (pmc8_simulation) { // FIXME - need to implement pmc8 sync sim // strcpy(response, "1"); // nbytes_read = strlen(response); DEBUGDEVICE(pmc8_device, INDI::Logger::DBG_ERROR, "Need to implement PMC8 sync simulation"); return false; } else { rc = set_pmc8_position(fd, racounts, deccounts); } if (!rc) { DEBUGDEVICE(pmc8_device, INDI::Logger::DBG_ERROR, "Error setting pmc8 position"); return false; } return true; } bool set_pmc8_radec(int fd, double ra, double dec) { bool rc; int racounts, deccounts; INDI::Telescope::TelescopePierSide sop; sop = destSideOfPier(ra, dec); rc = convert_ra_to_motor(ra, sop, &racounts); if (!rc) { DEBUGDEVICE(pmc8_device, INDI::Logger::DBG_ERROR, "set_pmc8_radec: error convering RA to motor counts"); return false; } rc = convert_dec_to_motor(ra, sop, &deccounts); if (!rc) { DEBUGDEVICE(pmc8_device, INDI::Logger::DBG_ERROR, "set_pmc8_radec: error convering DEC to motor counts"); return false; } if (pmc8_simulation) { // FIXME - need to implement pmc8 sync sim // strcpy(response, "1"); // nbytes_read = strlen(response); DEBUGDEVICE(pmc8_device, INDI::Logger::DBG_ERROR, "Need to implement PMC8 sync simulation"); return false; } else { rc = set_pmc8_target_position(fd, racounts, deccounts); } if (!rc) { DEBUGDEVICE(pmc8_device, INDI::Logger::DBG_ERROR, "Error setting target positoin"); return false; } return true; } bool get_pmc8_coords(int fd, double &ra, double &dec) { int racounts, deccounts; bool rc; if (pmc8_simulation) { // sortof silly but convert simulated RA/DEC to counts so we can then convert // back to RA/DEC to test that conversion code INDI::Telescope::TelescopePierSide sop; sop = destSideOfPier(simPMC8Data.ra, simPMC8Data.dec); rc = convert_ra_to_motor(simPMC8Data.ra, sop, &racounts); if (!rc) return rc; rc = convert_dec_to_motor(simPMC8Data.dec, sop, &deccounts); if (!rc) return rc; } else { rc = get_pmc8_position(fd, racounts, deccounts); } if (!rc) { DEBUGDEVICE(pmc8_device, INDI::Logger::DBG_DEBUG, "Error getting PMC8 motor position"); return false; } // convert motor counts to ra/dec convert_motor_to_radec(racounts, deccounts, ra, dec); // DEBUGFDEVICE(pmc8_device, INDI::Logger::DBG_DEBUG, "ra motor_counts=%d RA = %f", racounts, ra); // DEBUGFDEVICE(pmc8_device, INDI::Logger::DBG_DEBUG, "dec motor_counts=%d DEC = %f", deccounts, dec); return rc; } libindi/drivers/telescope/temmadriver.cpp0000664000175000017500000011067113263645557020164 0ustar jasemjasem/******************************************************************************* Copyright(c) 2010 Gerry Rozema. All rights reserved. Copyright(c) 2017 Jasem Mutlaq. All rights reserved. This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2 as published by the Free Software Foundation. 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 Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. *******************************************************************************/ #include "temmadriver.h" #include #include #include #include #include #define TEMMA_SLEW_RATES 2 #define TEMMA_TIMEOUT 5 #define TEMMA_BUFFER 64 #define TEMMA_SLEWRATE 5 /* slew rate, degrees/s */ // TODO enable Alignment System Later #if 0 #include using namespace INDI::AlignmentSubsystem; #endif // We declare an auto pointer to temma. std::unique_ptr temma(new TemmaMount()); void ISPoll(void *p); void ISGetProperties(const char *dev) { temma->ISGetProperties(dev); } void ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) { temma->ISNewSwitch(dev, name, states, names, n); } void ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n) { temma->ISNewText(dev, name, texts, names, n); } void ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) { temma->ISNewNumber(dev, name, values, names, n); } void ISNewBLOB(const char *dev, const char *name, int sizes[], int blobsizes[], char *blobs[], char *formats[], char *names[], int n) { INDI_UNUSED(dev); INDI_UNUSED(name); INDI_UNUSED(sizes); INDI_UNUSED(blobsizes); INDI_UNUSED(blobs); INDI_UNUSED(formats); INDI_UNUSED(names); INDI_UNUSED(n); } void ISSnoopDevice(XMLEle *root) { temma->ISSnoopDevice(root); } TemmaMount::TemmaMount() { SetTelescopeCapability(TELESCOPE_CAN_PARK | TELESCOPE_CAN_ABORT | TELESCOPE_CAN_SYNC | TELESCOPE_CAN_GOTO | TELESCOPE_HAS_TIME | TELESCOPE_HAS_LOCATION | TELESCOPE_HAS_PIER_SIDE, TEMMA_SLEW_RATES); // JM 2017-12-10: Use HA/DE instead of RA/DE for parking type? SetParkDataType(PARK_HA_DEC); // Should be set to invalid first Longitude = std::numeric_limits::quiet_NaN(); Latitude = std::numeric_limits::quiet_NaN(); setVersion(0, 2); } const char *TemmaMount::getDefaultName() { return "Temma"; } bool TemmaMount::initProperties() { INDI::Telescope::initProperties(); initGuiderProperties(getDeviceName(), MOTION_TAB); /*IUFillSwitch(&SlewRateS[0], "SLEW_GUIDE", "Guide", ISS_OFF); IUFillSwitch(&SlewRateS[1], "SLEW_MAX", "Max", ISS_ON); IUFillSwitchVector(&SlewRateSP, SlewRateS, 2, getDeviceName(), "TELESCOPE_SLEW_RATE", "Slew Rate", MOTION_TAB, IP_RW, ISR_1OFMANY, 0, IPS_IDLE);*/ // Temma runs at 19200 8 e 1 serialConnection->setDefaultBaudRate(Connection::Serial::B_19200); serialConnection->setParity(1); addSimulationControl(); // TODO enable later #if 0 InitAlignmentProperties(this); // Force the alignment system to always be on getSwitch("ALIGNMENT_SUBSYSTEM_ACTIVE")->sp[0].s = ISS_ON; #endif double longitude=0, latitude=90; // Get value from config file if it exists. IUGetConfigNumber(getDeviceName(), "GEOGRAPHIC_COORD", "LONG", &longitude); currentRA = get_local_sidereal_time(longitude); IUGetConfigNumber(getDeviceName(), "GEOGRAPHIC_COORD", "LAT", &latitude); currentDEC = latitude > 0 ? 90 : -90; return true; } #if 0 void TemmaMount::ISGetProperties(const char *dev) { /* First we let our parent populate */ INDI::Telescope::ISGetProperties(dev); //defineNumber(&GuideNSNP); //defineNumber(&GuideWENP); // JM 2016-11-10: This is not used anywhere in the code now. Enable it again when you use it //defineNumber(&GuideRateNP); } #endif bool TemmaMount::ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) { if (dev != nullptr && strcmp(dev, getDeviceName()) == 0) { // It is for us // call upstream for guider properties /*if (!strcmp(name, "GUIDE_RATE")) { IUUpdateNumber(&GuideRateNP, values, names, n); GuideRateNP.s = IPS_OK; IDSetNumber(&GuideRateNP, nullptr); return true; }*/ if (strcmp(name, GuideNSNP.name) == 0 || strcmp(name, GuideWENP.name) == 0) { processGuiderProperties(name, values, names, n); return true; } // Check alignment properties //ProcessAlignmentNumberProperties(this, name, values, names, n); } return INDI::Telescope::ISNewNumber(dev, name, values, names, n); } bool TemmaMount::ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) { if (dev != nullptr && strcmp(dev, getDeviceName()) == 0) { // Check alignment properties //ProcessAlignmentSwitchProperties(this, name, states, names, n); } return INDI::Telescope::ISNewSwitch(dev, name, states, names, n); } bool TemmaMount::ISNewBLOB(const char *dev, const char *name, int sizes[], int blobsizes[], char *blobs[], char *formats[], char *names[], int n) { if (dev != nullptr && strcmp(dev, getDeviceName()) == 0) { // It is for us //ProcessAlignmentBLOBProperties(this, name, sizes, blobsizes, blobs, formats, names, n); } // Pass it up the chain return INDI::Telescope::ISNewBLOB(dev, name, sizes, blobsizes, blobs, formats, names, n); } bool TemmaMount::ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n) { if (dev != nullptr && strcmp(dev, getDeviceName()) == 0) { //ProcessAlignmentTextProperties(this, name, texts, names, n); } // Pass it up the chain return INDI::Telescope::ISNewText(dev, name, texts, names, n); } bool TemmaMount::updateProperties() { INDI::Telescope::updateProperties(); if (isConnected()) { //LOG_DEBUG("Temma updating park stuff"); if (InitPark()) { //LOG_DEBUG("Success loading park data"); // If loading parking data is successful, we just set the default parking values. SetAxis1ParkDefault(0); SetAxis2ParkDefault(90); } else { //LOG_DEBUG("Setting park data to default"); // Otherwise, we set all parking data to default in case no parking data is found. SetAxis1Park(0); SetAxis2Park(90); SetAxis1ParkDefault(0); SetAxis2ParkDefault(90); } defineNumber(&GuideNSNP); defineNumber(&GuideWENP); // Load location so that it could trigger mount initiailization loadConfig(true, "GEOGRAPHIC_COORD"); // JM 2016-11-10: This is not used anywhere in the code now. Enable it again when you use it //defineNumber(&GuideRateNP); } else { deleteProperty(GuideNSNP.name); deleteProperty(GuideWENP.name); } return true; } bool TemmaMount::SendCommand(const char *cmd, char *response) { int bytesWritten = 0, bytesRead = 0, rc = 0; char errmsg[MAXRBUF]; char cmd_temma[TEMMA_BUFFER] = {0}; strncpy(cmd_temma, cmd, TEMMA_BUFFER); int cmd_size = strlen(cmd_temma); if (cmd_size-2 >= TEMMA_BUFFER) { LOG_ERROR("Command is too long!"); return false; } cmd_temma[cmd_size] = 0xD; cmd_temma[cmd_size+1] = 0xA; // Special case for M since it has slewbits if (cmd[0] == 'M') { std::string binary = std::bitset<8>(cmd[1]).to_string(); LOGF_DEBUG("CMD ", binary.c_str()); } else LOGF_DEBUG("CMD <%s>", cmd); if (isSimulation()) { if (response == nullptr) return true; switch (cmd[0]) { // Version case 'v': strncpy(response, "vSimulation v1.0", TEMMA_BUFFER); break; // Get LST case 'g': if (TemmaInitialized == false || std::isnan(Longitude)) return false; else { double lst = get_local_sidereal_time(Longitude); snprintf(response, TEMMA_BUFFER, "%02d%02d%02d", (int)lst, ((int)(lst * 60)) % 60, ((int)(lst * 3600)) % 60); } break; // Get coords case 'E': { char sign; double dec = currentDEC; if (dec < 0) sign = '-'; else sign = '+'; dec = fabs(dec); // Computing meridian side is quite involved.. so for simulation now just set it to east always if slewing or parking char state = 'E'; if (TrackState == SCOPE_PARKED || TrackState == SCOPE_IDLE || TrackState == SCOPE_TRACKING) state = 'F'; snprintf(response, TEMMA_BUFFER, "E%.2d%.2d%.2d%c%.2d%.2d%.1d%c", (int)currentRA, (int)(currentRA * (double)60) % 60, ((int)(currentRA * (double)6000)) % 100, sign, (int)dec, (int)(dec * (double)60) % 60, ((int)(dec * (double)600)) % 10, state); } break; // Sync case 'D': currentRA = targetRA; currentDEC = targetDEC; strncpy(response, "R0", TEMMA_BUFFER); break; // Goto case 'P': TrackState = SCOPE_SLEWING; strncpy(response, "R0", TEMMA_BUFFER); break; default: LOGF_ERROR("Command %s is unhandled in Simulation.", cmd); return false; } return true; } tcflush(PortFD, TCIOFLUSH); // Ask mount for current position if ( (rc = tty_write(PortFD, cmd_temma, strlen(cmd_temma), &bytesWritten)) != TTY_OK) { tty_error_msg(rc, errmsg, MAXRBUF); LOGF_ERROR("%s: %s", __FUNCTION__, errmsg); return false; } // If no response is required, let's return. if (response == nullptr) return true; memset(response, 0, 64); if ( (rc = tty_nread_section(PortFD, response, TEMMA_BUFFER, 0xA, TEMMA_TIMEOUT, &bytesRead)) != TTY_OK) { tty_error_msg(rc, errmsg, MAXRBUF); LOGF_ERROR("%s: %s", __FUNCTION__, errmsg); return false; } tcflush(PortFD, TCIOFLUSH); response[bytesRead-2] = 0; LOGF_DEBUG("RES <%s>", response); return true; } bool TemmaMount::GetCoords() { char response[TEMMA_BUFFER]; bool rc = SendCommand("E", response); if (rc == false) return false; if (response[0] != 'E') return false; int d, m, s; sscanf(response+1, "%02d%02d%02d", &d, &m, &s); //LOG_DEBUG"%d %d %d\n",d,m,s); currentRA = d * 3600 + m * 60 + s * .6; currentRA /= 3600; sscanf(response+8, "%02d%02d%01d", &d, &m, &s); //LOG_DEBUG"%d %d %d\n",d,m,s); currentDEC = d * 3600 + m * 60 + s * 6; currentDEC /= 3600; if(response[7]=='-') currentDEC*=-1; char side=response[13]; switch(side) { case 'E': setPierSide(PIER_EAST); break; case 'W': setPierSide(PIER_WEST); break; case 'F': // lets see if our goto has finished if (strstr(response, "F") != nullptr) { if (TrackState == SCOPE_SLEWING) { TrackState = SCOPE_TRACKING; } if (TrackState == SCOPE_PARKING) { SetParked(true); // turn off the motor LOG_DEBUG("Parked"); SetMotorStatus(false); } } else { LOG_DEBUG("Goto in Progress"); } break; } return true; } bool TemmaMount::ReadScopeStatus() { // JM: Do not read mount until it is initilaized. if (TemmaInitialized == false) return false; if (isSimulation()) { mountSim(); return true; } bool rc = GetCoords(); if (rc == false) return false; alignedRA = currentRA; alignedDEC = currentDEC; // TODO enable alignment system later #if 0 if (IsAlignmentSubsystemActive()) { const char *maligns[3] = {"ZENITH", "NORTH", "SOUTH"}; double juliandate, lst; //double alignedRA, alignedDEC; struct ln_equ_posn RaDec; bool aligned; juliandate = ln_get_julian_from_sys(); lst = ln_get_apparent_sidereal_time(juliandate) + (LocationN[1].value * 24.0 /360.0); // Use HA/Dec as telescope coordinate system RaDec.ra = ((lst - currentRA) * 360.0) / 24.0; //RaDec.ra = (ra * 360.0) / 24.0; RaDec.dec = currentDEC; INDI::AlignmentSubsystem::TelescopeDirectionVector TDV = TelescopeDirectionVectorFromLocalHourAngleDeclination(RaDec); //AlignmentSubsystem::TelescopeDirectionVector TDV = TelescopeDirectionVectorFromEquatorialCoordinates(RaDec); DEBUGF(INDI::AlignmentSubsystem::DBG_ALIGNMENT, "Status: Mnt. Algnt. %s LST %lf RA %lf DEC %lf HA %lf TDV(x %lf y %lf z %lf)", maligns[GetApproximateMountAlignment()], lst, currentRA, currentDEC, RaDec.ra, TDV.x, TDV.y, TDV.z); aligned=true; if (!TransformTelescopeToCelestial(TDV, alignedRA, alignedDEC)) { aligned = false; DEBUGF(INDI::AlignmentSubsystem::DBG_ALIGNMENT, "Failed TransformTelescopeToCelestial: Scope RA=%g Scope DE=%f, Aligned RA=%f DE=%f", currentRA, currentDEC, alignedRA, alignedDEC); } else { DEBUGF(INDI::AlignmentSubsystem::DBG_ALIGNMENT, "TransformTelescopeToCelestial: Scope RA=%f Scope DE=%f, Aligned RA=%f DE=%f", currentRA, currentDEC, alignedRA, alignedDEC); } } if (!aligned && (syncdata.lst != 0.0)) { DEBUGF(DBG_SCOPE_STATUS, "Aligning with last sync delta RA %g DE %g", syncdata.deltaRA, syncdata.deltaDEC); // should check values are in range! alignedRA += syncdata.deltaRA; alignedDEC += syncdata.deltaDEC; if (alignedDEC > 90.0 || alignedDEC < -90.0) { alignedRA += 12.00; if (alignedDEC > 0.0) alignedDEC = 180.0 - alignedDEC; else alignedDEC = -180.0 - alignedDEC; } alignedRA = range24(alignedRA); } #endif NewRaDec(currentRA, currentDEC); // If NSWE directional slew is ongoing, continue to command the mount. if (SlewActive) { char cmd[3] = {0}; cmd[0] = 'M'; cmd[1] = Slewbits; SendCommand(cmd, nullptr); } return true; } bool TemmaMount::Sync(double ra, double dec) { char cmd[TEMMA_BUFFER] = {0}, res[TEMMA_BUFFER] = {0}; char sign; targetRA = ra; targetDEC = dec; /* sync involves jumping thru considerable hoops first we have to set local sideral time then we have to send a Z then we set local sideral time again and finally we send the co-ordinates we are syncing on */ LOG_DEBUG("Sending LST --> Z --> LST before Sync."); SetLST(); SendCommand("Z"); SetLST(); // now lets format up our sync command if (dec < 0) sign = '-'; else sign = '+'; dec = fabs(dec); snprintf(cmd, TEMMA_BUFFER, "D%.2d%.2d%.2d%c%.2d%.2d%.1d", (int)ra, (int)(ra * (double)60) % 60, ((int)(ra * (double)6000)) % 100, sign, (int)dec, (int)(dec * (double)60) % 60, ((int)(dec * (double)600)) % 10); if (SendCommand(cmd, res) == false) return false; // if the first character is an R, it's a correct response if (res[0] != 'R') return false; if (res[1] != '0') { // 0 = success // 1 = RA error // 2 = Dec error // 3 = To many digits return false; } return true; } bool TemmaMount::Goto(double ra, double dec) { char cmd[TEMMA_BUFFER] = {0}, res[TEMMA_BUFFER] = {0}; char sign; targetRA = ra; targetDEC = dec; /* goto involves hoops, but, not as many as a sync first set sideral time then issue the goto command */ if (MotorStatus == false) { LOG_DEBUG("Goto turns on motors"); SetMotorStatus(true); } SetLST(); // now lets format up our goto command if (dec < 0) sign = '-'; else sign = '+'; dec = fabs(dec); snprintf(cmd, TEMMA_BUFFER, "P%.2d%.2d%.2d%c%.2d%.2d%.1d", (int)ra, (int)(ra * (double)60) % 60, ((int)(ra * (double)6000)) % 100, sign, (int)dec, (int)(dec * (double)60) % 60, ((int)(dec * (double)600)) % 10); if (SendCommand(cmd, res) == false) return false; // if the first character is an R, it's a correct response if (res[0] != 'R') return false; if (res[1] != '0') { // 0 = success // 1 = RA error // 2 = Dec error // 3 = To many digits return false; } return true; } bool TemmaMount::Park() { double lst; double lha; double RightAscension; lha = rangeHA(GetAxis1Park()); lst = get_local_sidereal_time(Longitude); RightAscension = lst - (lha); // Get the park position RightAscension = range24(RightAscension); LOGF_DEBUG("head to Park position %4.2f %4.2f %4.2f %4.2f", GetAxis1Park(), lha, RightAscension, GetAxis2Park()); Goto(RightAscension, GetAxis2Park()); // if motors are in standby, turn em on //if(!MotorState) SetTemmaMotorStatus(true); //Goto(RightAscension,90); //ParkInProgress = true; //LOG_DEBUG"Temma::Park()\n"); //SetTemmaMotorStatus(false); //GetTemmaMotorStatus(); //SetParked(true); return true; } bool TemmaMount::UnPark() { SetParked(false); //SetTemmaMotorStatus(true); GetMotorStatus(); return true; } bool TemmaMount::SetCurrentPark() { double lha; double lst; //IDMessage(getDeviceName(),"Setting Default Park Position"); //IDMessage(getDeviceName(),"Arbitrary park positions not yet supported."); //SetAxis1Park(0); //SetAxis2Park(90); lst = get_local_sidereal_time(Longitude); //lha = lst - alignedRA; lha = lst - currentRA; lha = rangeHA(lha); // base class wont store a negative number here //lha = range24(lha); SetAxis1Park(lha); SetAxis2Park(currentDEC); return true; } bool TemmaMount::SetDefaultPark() { // By default az to north, and alt to pole //IDMessage(getDeviceName(), "Setting Park Data to Default."); SetAxis1Park(0); SetAxis2Park(90); return true; } bool TemmaMount::Abort() { char res[TEMMA_BUFFER] = {0}; if (SendCommand("PS") == false) return false; if (SendCommand("s", res) == false) return false; return true; } bool TemmaMount::MoveNS(INDI_DIR_NS dir, TelescopeMotionCommand command) { char cmd[TEMMA_BUFFER] = {0}; LOGF_DEBUG("Temma::MoveNS %d dir %d", command, dir); if (!MotorStatus) SetMotorStatus(true); if (!MotorStatus) return false; Slewbits = 0; Slewbits |= 64; // dec says always on LOGF_DEBUG("Temma::MoveNS %d dir %d", command, dir); if (command == MOTION_START) { if (SlewRate != 0) Slewbits |= 1; if (dir != DIRECTION_NORTH) { LOG_DEBUG("Start slew Dec Up"); Slewbits |= 16; } else { LOG_DEBUG("Start Slew Dec down"); Slewbits |= 8; } //sprintf(buf,"M \r\n"); //buf[1]=Slewbits; //tty_write(PortFD,buf,4,&bytesWritten); SlewActive = true; } else { // No direction bytes to turn it off LOG_DEBUG("Abort slew e/w"); //Abort(); SlewActive = false; } cmd[0] = 'M'; cmd[1] = Slewbits; return SendCommand(cmd); } bool TemmaMount::MoveWE(INDI_DIR_WE dir, TelescopeMotionCommand command) { char cmd[TEMMA_BUFFER] = {0}; if (!MotorStatus) SetMotorStatus(true); if (!MotorStatus) return false; Slewbits = 0; Slewbits |= 64; // documentation says always on LOGF_DEBUG("Temma::MoveWE %d dir %d", command, dir); if (command == MOTION_START) { if (SlewRate != 0) Slewbits |= 1; if (dir != DIRECTION_WEST) { LOG_DEBUG("Start slew East"); Slewbits |= 2; } else { LOG_DEBUG("Start Slew West"); Slewbits |= 4; } //sprintf(buf,"M \r\n"); //buf[1]=Slewbits; //tty_write(PortFD,buf,4,&bytesWritten); SlewActive = true; } else { // No direction bytes to turn it off LOG_DEBUG("Abort slew e/w"); //Abort(); SlewActive = false; } cmd[0] = 'M'; cmd[1] = Slewbits; return SendCommand(cmd); } #if 0 bool TemmaMount::SetSlewRate(int index) { // Is this possible for Temma? //LOGF_DEBUG("Temma::Slew rate %d", index); SlewRate = index; return true; } #endif // TODO For large ms > 1000ms this function should be asynchronous IPState TemmaMount::GuideNorth(float ms) { char cmd[TEMMA_BUFFER] = {0}; char bits; LOGF_DEBUG("Guide North %4.0f", ms); if (!MotorStatus) return IPS_ALERT; if (SlewActive) return IPS_ALERT; bits = 0; bits |= 64; // documentation says always on bits |= 8; // going north cmd[0] = 'M'; cmd[1] = bits; SendCommand(cmd); usleep(ms * 1000); bits = 64; cmd[1] = bits; SendCommand(cmd); return IPS_OK; } IPState TemmaMount::GuideSouth(float ms) { char cmd[TEMMA_BUFFER] = {0}; char bits; LOGF_DEBUG("Guide South %4.0f", ms); if (!MotorStatus) return IPS_ALERT; if (SlewActive) return IPS_ALERT; bits = 0; bits |= 64; // documentation says always on bits |= 16; // going south cmd[0] = 'M'; cmd[1] = bits; SendCommand(cmd); usleep(ms * 1000); bits = 64; cmd[1] = bits; SendCommand(cmd); return IPS_OK; } IPState TemmaMount::GuideEast(float ms) { char cmd[TEMMA_BUFFER] = {0}; char bits; LOGF_DEBUG("Guide East %4.0f", ms); if (!MotorStatus) return IPS_ALERT; if (SlewActive) return IPS_ALERT; bits = 0; bits |= 64; // doc says always on bits |= 2; // going east cmd[0] = 'M'; cmd[1] = bits; SendCommand(cmd); usleep(ms * 1000); bits = 64; cmd[1] = bits; SendCommand(cmd); return IPS_OK; } IPState TemmaMount::GuideWest(float ms) { char cmd[TEMMA_BUFFER] = {0}; char bits; LOGF_DEBUG("Guide West %4.0f", ms); if (!MotorStatus) return IPS_ALERT; if (SlewActive) return IPS_ALERT; bits = 0; bits |= 64; // documentation says always on bits |= 4; // going west cmd[0] = 'M'; cmd[1] = bits; SendCommand(cmd); usleep(ms * 1000); bits = 64; cmd[1] = bits; SendCommand(cmd); return IPS_OK; } #if 0 bool TemmaMount::updateTime(ln_date *utc, double utc_offset) { INDI_UNUSED(utc); INDI_UNUSED(utc_offset); LOG_DEBUG("Temma::UpdateTime()"); return true; } #endif bool TemmaMount::updateLocation(double latitude, double longitude, double elevation) { INDI_UNUSED(elevation); Longitude = longitude; Latitude = latitude; double lst = get_local_sidereal_time(Longitude); // A temma mount must have the LST and Latitude set // Prior to these being set, reads will return garbage if (TemmaInitialized == false) { SetLattitude(latitude); SetLST(); TemmaInitialized = true; // We were NOT intialized, so, in case there is not park position set // Sync to the position of bar vertical, telescope pointed at pole double RightAscension; RightAscension = lst - (-6); // Hour angle is negative 6 in this case RightAscension = range24(RightAscension); LOGF_DEBUG("Temma is initilized. Latitude: %.2f LST: %.2f", latitude, lst); //TemmaSync(RightAscension, 90); Sync(RightAscension, 90); LOGF_DEBUG("Initial sync on %4.2f", RightAscension); } // if the mount is parked, then we should sync it on our park position if (isParked()) { // Here we have to sync on our park position double RightAscension; // Get the park position RightAscension = lst - (rangeHA(GetAxis1Park())); RightAscension = range24(RightAscension); LOGF_DEBUG("Sync to Park position %4.2f %4.2f %4.2f", GetAxis1Park(), RightAscension, GetAxis2Park()); //TemmaSync(RightAscension, GetAxis2Park()); Sync(RightAscension, GetAxis2Park()); LOG_DEBUG("Turn motors off"); SetMotorStatus(false); } else { sleep(1); LOG_DEBUG("Mount is not parked"); //SetTemmaMotorStatus(true); } return true; } #if 0 ln_equ_posn TemmaMount::TelescopeToSky(double ra, double dec) { double RightAscension, Declination; ln_equ_posn eq { 0, 0 }; if (GetAlignmentDatabase().size() > 1) { TelescopeDirectionVector TDV; /* Use this if we ar converting eq co-ords // but it's broken for now eq.ra=ra*360/24; eq.dec=dec; TDV=TelescopeDirectionVectorFromEquatorialCoordinates(eq); */ /* This code does a conversion from ra/dec to alt/az // before calling the alignment stuff ln_lnlat_posn here; ln_hrz_posn altaz; here.lat=LocationN[LOCATION_LATITUDE].value; here.lng=LocationN[LOCATION_LONGITUDE].value; eq.ra=ra*360.0/24.0; // this is wanted in degrees, not hours eq.dec=dec; ln_get_hrz_from_equ(&eq,&here,ln_get_julian_from_sys(),&altaz); TDV=TelescopeDirectionVectorFromAltitudeAzimuth(altaz); */ /* and here we convert from ra/dec to hour angle / dec before calling alignment stuff */ double lha, lst; lst = get_local_sidereal_time(LocationN[LOCATION_LONGITUDE].value); lha = get_local_hour_angle(lst, ra); // convert lha to degrees lha = lha * 360 / 24; eq.ra = lha; eq.dec = dec; TDV = TelescopeDirectionVectorFromLocalHourAngleDeclination(eq); if (TransformTelescopeToCelestial(TDV, RightAscension, Declination)) { // if we get here, the conversion was successful //LOG_DEBUG"new values %6.4f %6.4f %6.4f %6.4f Deltas %3.0lf %3.0lf\n",ra,dec,RightAscension,Declination,(ra-RightAscension)*60,(dec-Declination)*60); } else { //if the conversion failed, return raw data RightAscension = ra; Declination = dec; } } else { // With less than 2 align points // Just return raw data RightAscension = ra; Declination = dec; } eq.ra = RightAscension; eq.dec = Declination; return eq; } ln_equ_posn TemmaMount::SkyToTelescope(double ra, double dec) { ln_equ_posn eq { 0, 0 }; TelescopeDirectionVector TDV; double RightAscension, Declination; /* */ if (GetAlignmentDatabase().size() > 1) { // if the alignment system has been turned off // this transformation will fail, and we fall thru // to using raw co-ordinates from the mount if (TransformCelestialToTelescope(ra, dec, 0.0, TDV)) { /* Initial attemp, using RA/DEC co-ordinates talking to alignment system EquatorialCoordinatesFromTelescopeDirectionVector(TDV,eq); RightAscension=eq.ra*24.0/360; Declination=eq.dec; if(RightAscension < 0) RightAscension+=24.0; DEBUGF(INDI::Logger::DBG_SESSION,"Transformed Co-ordinates %g %g\n",RightAscension,Declination); */ // nasty altaz kludge, use al/az co-ordinates to talk to alignment system /* eq.ra=ra*360/24; eq.dec=dec; here.lat=LocationN[LOCATION_LATITUDE].value; here.lng=LocationN[LOCATION_LONGITUDE].value; ln_get_hrz_from_equ(&eq,&here,ln_get_julian_from_sys(),&altaz); AltitudeAzimuthFromTelescopeDirectionVector(TDV,altaz); // now convert the resulting altaz into radec ln_get_equ_from_hrz(&altaz,&here,ln_get_julian_from_sys(),&eq); RightAscension=eq.ra*24.0/360.0; Declination=eq.dec; */ /* now lets convert from telescope to lha/dec */ double lst; LocalHourAngleDeclinationFromTelescopeDirectionVector(TDV, eq); // and now we have to convert from lha back to RA lst = get_local_sidereal_time(LocationN[LOCATION_LONGITUDE].value); eq.ra = eq.ra * 24 / 360; RightAscension = lst - eq.ra; RightAscension = range24(RightAscension); Declination = eq.dec; } else { LOGF_INFO("Transform failed, using raw co-ordinates %g %g", ra, dec); RightAscension = ra; Declination = dec; } } else { RightAscension = ra; Declination = dec; } eq.ra = RightAscension; eq.dec = Declination; //eq.ra=ra; //eq.dec=dec; return eq; } #endif bool TemmaMount::GetVersion() { char res[TEMMA_BUFFER] = {0}; if (SendCommand("v", res) == false) return false; if (res[0] != 'v') { // Sometimes there is garbage in the buffers and we dont get what we expect // Lets read a big chunk and assume it will time out LOG_ERROR("Read Version failed."); return false; } LOGF_INFO("Detected version: %s", res+1); return true; } bool TemmaMount::GetMotorStatus() { char res[TEMMA_BUFFER] = {0}; if (SendCommand("STN-COD", res) == false) return false; if (strstr(res, "off") != nullptr) MotorStatus = true; else MotorStatus = false; LOGF_DEBUG("Motor is %s", res); return MotorStatus; } bool TemmaMount::SetMotorStatus(bool enable) { char res[TEMMA_BUFFER] = {0}; bool rc = false; if (enable) rc = SendCommand("STN-ON", res); else rc = SendCommand("STN-OFF", res); if (rc == false) return false; GetMotorStatus(); return true; } /* bit of a hack, returns true if lst is a sane value, false if it is not sane */ bool TemmaMount::GetLST(double &lst) { char res[TEMMA_BUFFER] = {0}; if (SendCommand("g", res) == false) return false; int hh=0,mm=0,ss=0; if (sscanf(res+1, "%02d%02d%02d", &hh, &mm, &ss) == 3) { lst = hh + mm/60.0 + ss/3600.0; return true; } return false; } bool TemmaMount::SetLST() { char cmd[TEMMA_BUFFER] = {0}; double lst = get_local_sidereal_time(Longitude); snprintf(cmd, TEMMA_BUFFER, "T%.2d%.2d%.2d", (int)lst, ((int)(lst * 60)) % 60, ((int)(lst * 3600)) % 60); return SendCommand(cmd); } bool TemmaMount::GetLattitude(double &lat) { char res[TEMMA_BUFFER] = {0}; if (SendCommand("i", res) == false) return false; int dd=0,mm=0,parial_m=0; if (sscanf(res+1, "%02d%02d%01d", &dd, &mm, &parial_m) == 3) { lat = dd + mm / 60.0 + parial_m / 600.0; return true; } return false; } bool TemmaMount::SetLattitude(double lat) { char cmd[TEMMA_BUFFER]; char sign; double l; int d, m, s; if (lat > 0) { sign = '+'; } else { sign = '-'; } l = fabs(lat); d = (int)l; l = (l - d) * 60; m = (int)l; l = (l - m) * 6; s = (int)l; snprintf(cmd, TEMMA_BUFFER, "I%c%.2d%.2d%.1d", sign, d, m, s); return SendCommand(cmd); } bool TemmaMount::Handshake() { //LOG_DEBUG("Calling get version"); /* This is an ugly hack, but, it works On first open we often dont get an immediate read from the temma but it seems to read much more reliably if we enforce a short wait between opening the port and doing the first query for version */ usleep(100); if (GetVersion() == false) return false; double lst=0; if (GetLST(lst)) { LOG_DEBUG("Temma is intialized."); TemmaInitialized = true; } else { LOG_DEBUG("Temma is not initialized."); TemmaInitialized = false; } GetMotorStatus(); return true; } #if 0 int TemmaMount::TemmaRead(char *buf, int size) { int bytesRead = 0; int count = 0; int ptr = 0; int rc = 0; while (ptr < size) { rc = tty_read(PortFD, &buf[ptr], 1, 2, &bytesRead); // Read 1 byte of response into the buffer with 5 second timeout if (bytesRead == 1) { // we got a byte count++; if (count > 1) { //LOG_DEBUG"%d ",buf[ptr]); if (buf[ptr] == 10) { if (buf[ptr - 1] == 13) { // we have the cr/lf from the response // Send it back to the caller return count; } } } ptr++; } else { fprintf(stderr,"timed out reading %d\n",count); LOGF_DEBUG("We timed out reading bytes %d", count); return 0; } } // if we get here, we got more than size bytes, and still dont have a cr/lf fprintf(stderr,"Read error after %d bytes\n",bytesRead); LOGF_DEBUG("Read return error after %d bytes", bytesRead); return -1; } #endif void TemmaMount::mountSim() { static struct timeval ltv; struct timeval tv; double dt=0, da=0, dx=0; int nlocked=0; /* update elapsed time since last poll, don't presume exactly POLLMS */ gettimeofday(&tv, nullptr); if (ltv.tv_sec == 0 && ltv.tv_usec == 0) ltv = tv; dt = tv.tv_sec - ltv.tv_sec + (tv.tv_usec - ltv.tv_usec) / 1e6; ltv = tv; da = TEMMA_SLEWRATE * dt; /* Process per current state. We check the state of EQUATORIAL_COORDS and act acoordingly */ switch (TrackState) { case SCOPE_IDLE: currentRA += (TRACKRATE_SIDEREAL/3600.0 * dt / 15.); break; case SCOPE_TRACKING: switch (IUFindOnSwitchIndex(&TrackModeSP)) { case TRACK_SIDEREAL: da = 0; dx = 0; break; case TRACK_LUNAR: da = ((TRACKRATE_LUNAR-TRACKRATE_SIDEREAL)/3600.0 * dt / 15.); dx = 0; break; case TRACK_SOLAR: da = ((TRACKRATE_SOLAR-TRACKRATE_SIDEREAL)/3600.0 * dt / 15.); dx = 0; break; case TRACK_CUSTOM: da = ((TrackRateN[AXIS_RA].value-TRACKRATE_SIDEREAL)/3600.0 * dt / 15.); dx = (TrackRateN[AXIS_DE].value/3600.0 * dt); break; } currentRA += da; currentDEC += dx; break; case SCOPE_SLEWING: case SCOPE_PARKING: /* slewing - nail it when both within one pulse @ LX200_GENERIC_SLEWRATE */ nlocked = 0; dx = targetRA - currentRA; if (fabs(dx) <= da) { currentRA = targetRA; nlocked++; } else if (dx > 0) currentRA += da / 15.; else currentRA -= da / 15.; dx = targetDEC - currentDEC; if (fabs(dx) <= da) { currentDEC = targetDEC; nlocked++; } else if (dx > 0) currentDEC += da; else currentDEC -= da; if (nlocked == 2) { if (TrackState == SCOPE_SLEWING) TrackState = SCOPE_TRACKING; else SetParked(true); } break; default: break; } NewRaDec(currentRA, currentDEC); } libindi/drivers/telescope/lx200fs2.h0000664000175000017500000000330113263645557016561 0ustar jasemjasem/* Astro-Electronic FS-2 Copyright (C) 2015 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 */ #pragma once #include "lx200generic.h" class LX200FS2 : public LX200Generic { public: LX200FS2(); virtual bool initProperties() override; virtual bool updateProperties() override; virtual bool ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) override; protected: virtual const char *getDefaultName() override; virtual bool isSlewComplete() override; virtual bool checkConnection() override; virtual bool saveConfigItems(FILE *fp) override; // Parking virtual bool Park() override; virtual bool UnPark() override; virtual bool SetCurrentPark() override; virtual bool SetDefaultPark() override; // Fake Location virtual bool updateLocation(double latitude, double longitude, double elevation) override; INumber SlewAccuracyN[2]; INumberVectorProperty SlewAccuracyNP; }; libindi/drivers/telescope/lx200ap_experimental.cpp0000664000175000017500000012106313263645557021605 0ustar jasemjasem/* Astro-Physics INDI driver Copyright (C) 2014 Jasem Mutlaq Based on INDI Astrophysics Driver by Markus Wildi 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 */ #include "lx200ap_experimental.h" #include "indicom.h" #include "lx200driver.h" #include "lx200apdriver.h" #include "lx200ap_experimentaldriver.h" #include #include #include #include #include void LX200AstroPhysicsExperimental::disclaimerMessage() { LOG_INFO("This is an _EXPERIMENTAL_ driver for Astro-Physics mounts - use at own risk!"); LOG_INFO("BEFORE USING PLEASE READ the documentation at:"); LOG_INFO(" http://indilib.org/devices/telescopes/astrophysics.html"); } /* Constructor */ LX200AstroPhysicsExperimental::LX200AstroPhysicsExperimental() : LX200Generic() { setLX200Capability(LX200_HAS_PULSE_GUIDING); SetTelescopeCapability(GetTelescopeCapability() | TELESCOPE_HAS_PIER_SIDE | TELESCOPE_HAS_PEC | TELESCOPE_CAN_CONTROL_TRACK | TELESCOPE_HAS_TRACK_RATE, 4); sendLocationOnStartup = false; sendTimeOnStartup = false; disclaimerMessage(); } const char *LX200AstroPhysicsExperimental::getDefaultName() { return (const char *)"AstroPhysics Experimental"; } bool LX200AstroPhysicsExperimental::initProperties() { LX200Generic::initProperties(); timeFormat = LX200_24; IUFillNumber(&HourangleCoordsN[0], "HA", "HA H:M:S", "%10.6m", 0., 24., 0., 0.); IUFillNumber(&HourangleCoordsN[1], "DEC", "Dec D:M:S", "%10.6m", -90.0, 90.0, 0., 0.); IUFillNumberVector(&HourangleCoordsNP, HourangleCoordsN, 2, getDeviceName(), "HOURANGLE_COORD", "Hourangle Coords", MAIN_CONTROL_TAB, IP_RO, 0, IPS_IDLE); IUFillNumber(&HorizontalCoordsN[0], "AZ", "Az D:M:S", "%10.6m", 0., 360., 0., 0.); IUFillNumber(&HorizontalCoordsN[1], "ALT", "Alt D:M:S", "%10.6m", -90., 90., 0., 0.); IUFillNumberVector(&HorizontalCoordsNP, HorizontalCoordsN, 2, getDeviceName(), "HORIZONTAL_COORD", "Horizontal Coords", MAIN_CONTROL_TAB, IP_RW, 120, IPS_IDLE); // Max rate is 999.99999X for the GTOCP4. // Using :RR998.9999# just to be safe. 15.041067*998.99999 = 15026.02578 TrackRateN[AXIS_RA].min = -15026.0258; TrackRateN[AXIS_RA].max = 15026.0258; TrackRateN[AXIS_DE].min = -998.9999; TrackRateN[AXIS_DE].max = 998.9999; // Motion speed of axis when pressing NSWE buttons IUFillSwitch(&SlewRateS[0], "12", "12x", ISS_OFF); IUFillSwitch(&SlewRateS[1], "64", "64x", ISS_ON); IUFillSwitch(&SlewRateS[2], "600", "600x", ISS_OFF); IUFillSwitch(&SlewRateS[3], "1200", "1200x", ISS_OFF); IUFillSwitchVector(&SlewRateSP, SlewRateS, 4, getDeviceName(), "TELESCOPE_SLEW_RATE", "Slew Rate", MOTION_TAB, IP_RW, ISR_1OFMANY, 0, IPS_IDLE); // Slew speed when performing regular GOTO IUFillSwitch(&APSlewSpeedS[0], "600", "600x", ISS_ON); IUFillSwitch(&APSlewSpeedS[1], "900", "900x", ISS_OFF); IUFillSwitch(&APSlewSpeedS[2], "1200", "1200x", ISS_OFF); IUFillSwitchVector(&APSlewSpeedSP, APSlewSpeedS, 3, getDeviceName(), "GOTO Rate", "", MOTION_TAB, IP_RW, ISR_1OFMANY, 0, IPS_IDLE); IUFillSwitch(&SwapS[0], "NS", "North/South", ISS_OFF); IUFillSwitch(&SwapS[1], "EW", "East/West", ISS_OFF); IUFillSwitchVector(&SwapSP, SwapS, 2, getDeviceName(), "SWAP", "Swap buttons", MOTION_TAB, IP_RW, ISR_1OFMANY, 0, IPS_IDLE); IUFillSwitch(&SyncCMRS[USE_REGULAR_SYNC], ":CM#", ":CM#", ISS_OFF); IUFillSwitch(&SyncCMRS[USE_CMR_SYNC], ":CMR#", ":CMR#", ISS_ON); IUFillSwitchVector(&SyncCMRSP, SyncCMRS, 2, getDeviceName(), "SYNCCMR", "Sync", MOTION_TAB, IP_RW, ISR_1OFMANY, 0, IPS_IDLE); // guide speed IUFillSwitch(&APGuideSpeedS[0], "0.25", "0.25x", ISS_OFF); IUFillSwitch(&APGuideSpeedS[1], "0.5", "0.50x", ISS_OFF); IUFillSwitch(&APGuideSpeedS[2], "1.0", "1.0x", ISS_ON); IUFillSwitchVector(&APGuideSpeedSP, APGuideSpeedS, 3, getDeviceName(), "Guide Rate", "", GUIDE_TAB, IP_RW, ISR_1OFMANY, 0, IPS_IDLE); // Unpark from? IUFillSwitch(&UnparkFromS[0], "Last", "Last Parked", ISS_ON); IUFillSwitch(&UnparkFromS[1], "Park1", "Park1", ISS_OFF); IUFillSwitch(&UnparkFromS[2], "Park2", "Park2", ISS_OFF); IUFillSwitch(&UnparkFromS[3], "Park3", "Park3", ISS_OFF); IUFillSwitch(&UnparkFromS[4], "Park4", "Park4", ISS_OFF); IUFillSwitchVector(&UnparkFromSP, UnparkFromS, 5, getDeviceName(), "UNPARK_FROM", "Unpark From?", MAIN_CONTROL_TAB, IP_RW, ISR_1OFMANY, 0, IPS_IDLE); // park presets IUFillSwitch(&ParkToS[0], "Custom", "Custom", ISS_OFF); IUFillSwitch(&ParkToS[1], "Park1", "Park1", ISS_OFF); IUFillSwitch(&ParkToS[2], "Park2", "Park2", ISS_OFF); IUFillSwitch(&ParkToS[3], "Park3", "Park3", ISS_ON); IUFillSwitch(&ParkToS[4], "Park4", "Park4", ISS_OFF); IUFillSwitchVector(&ParkToSP, ParkToS, 5, getDeviceName(), "PARK_TO", "Park To?", SITE_TAB, IP_RW, ISR_1OFMANY, 0, IPS_IDLE); IUFillText(&VersionT[0], "Version", "Version", ""); IUFillTextVector(&VersionInfo, VersionT, 1, getDeviceName(), "Firmware", "Firmware", MAIN_CONTROL_TAB, IP_RO, 0, IPS_IDLE); // meridian delay (experimental!) IUFillNumber(&MeridianDelayN[0], "MeridianDelay", "MERIDIAN_DELAY (experimental!)", "%4.2f", 0.0, 3.0, 0.25, 0.0); IUFillNumberVector(&MeridianDelayNP, MeridianDelayN, 1, getDeviceName(), "MERIDIAN_DELAY (experimental!)", "", MAIN_CONTROL_TAB, IP_RW, 60, IPS_OK); SetParkDataType(PARK_AZ_ALT); return true; } void LX200AstroPhysicsExperimental::ISGetProperties(const char *dev) { LX200Generic::ISGetProperties(dev); defineSwitch(&UnparkFromSP); // MSF 2018/04/10 - disable this behavior for now - we want to have // UnparkFromSP to always start out as "Last Parked" for safety // load config to get unpark from position user wants BEFORE we connect to mount // if (!isConnected()) // { // LOG_DEBUG("Loading unpark from location from config file"); // loadConfig(true, UnparkFromSP.name); // } if (isConnected()) { defineText(&VersionInfo); /* Motion group */ defineSwitch(&APSlewSpeedSP); defineSwitch(&SwapSP); defineSwitch(&SyncCMRSP); defineSwitch(&APGuideSpeedSP); defineSwitch(&ParkToSP); } } bool LX200AstroPhysicsExperimental::updateProperties() { LX200Generic::updateProperties(); defineSwitch(&UnparkFromSP); if (isConnected()) { defineText(&VersionInfo); /* Motion group */ defineSwitch(&APSlewSpeedSP); defineSwitch(&SwapSP); defineSwitch(&SyncCMRSP); defineSwitch(&APGuideSpeedSP); defineSwitch(&ParkToSP); defineNumber(&MeridianDelayNP); // load in config value for park to and initialize park position loadConfig(true, ParkToSP.name); ParkPosition parkPos = (ParkPosition)IUFindOnSwitchIndex(&ParkToSP); LOGF_DEBUG("park position = %d", parkPos); // setup location double longitude=-1000, latitude=-1000; // Get value from config file if it exists. IUGetConfigNumber(getDeviceName(), "GEOGRAPHIC_COORD", "LONG", &longitude); IUGetConfigNumber(getDeviceName(), "GEOGRAPHIC_COORD", "LAT", &latitude); if (longitude != -1000 && latitude != -1000) updateLocation(latitude, longitude, 0); // initialize park position if (InitPark()) { SetAxis1ParkDefault(LocationN[LOCATION_LATITUDE].value >= 0 ? 0 : 180); SetAxis2ParkDefault(LocationN[LOCATION_LATITUDE].value); } else { // Otherwise, we set all parking data to default in case no parking data is found. SetAxis1Park(LocationN[LOCATION_LATITUDE].value >= 0 ? 0 : 180); SetAxis1ParkDefault(LocationN[LOCATION_LATITUDE].value); SetAxis1ParkDefault(LocationN[LOCATION_LATITUDE].value >= 0 ? 0 : 180); SetAxis2ParkDefault(LocationN[LOCATION_LATITUDE].value); } // override with predefined position if selected if (parkPos != PARK_CUSTOM) { double parkAz, parkAlt; if (calcParkPosition(parkPos, &parkAz, &parkAlt)) { SetAxis1Park(parkAlt); SetAxis2Park(parkAz); LOGF_DEBUG("Set predefined park position %d to az=%f alt=%f", parkPos, parkAz, parkAlt); } else { LOGF_ERROR("Unable to set predefined park position %d!!", parkPos); } } } else { deleteProperty(VersionInfo.name); deleteProperty(APSlewSpeedSP.name); deleteProperty(SwapSP.name); deleteProperty(SyncCMRSP.name); deleteProperty(APGuideSpeedSP.name); deleteProperty(ParkToSP.name); deleteProperty(MeridianDelayNP.name); } return true; } bool LX200AstroPhysicsExperimental::getFirmwareVersion() { bool success; char rev[8]; char versionString[128]; success = false; if (isSimulation()) strncpy(versionString, "VCP4-P01-01", 128); else getAPVersionNumber(PortFD, versionString); VersionInfo.s = IPS_OK; IUSaveText(&VersionT[0], versionString); IDSetText(&VersionInfo, nullptr); // Check controller version // example "VCP4-P01-01" for CP4 or newer // single or double letter like "T" or "V1" for CP3 and older // CP4 if (strstr(versionString, "VCP4")) { firmwareVersion = MCV_V; servoType = GTOCP4; strcpy(rev, "V"); success = true; } else if (strlen(versionString) == 1 || strlen(versionString) == 2) { // Check earlier versions // FIXME could probably use better range checking in case we get a letter like 'Z' that doesn't map to anything! int typeIndex = VersionT[0].text[0] - 'E'; if (typeIndex >= 0) { firmwareVersion = static_cast(typeIndex); LOGF_DEBUG("Firmware version index: %d", typeIndex); if (firmwareVersion < MCV_G) servoType = GTOCP2; else servoType = GTOCP3; strcpy(rev, versionString); success = true; } } if (success) { LOGF_INFO("Servo Box Controller: GTOCP%d.", servoType); LOGF_INFO("Firmware Version: '%s' - %s", rev, versionString+5); } return success; } bool LX200AstroPhysicsExperimental::initMount() { // Make sure that the mount is setup according to the properties int err=0; if (!IsMountInitialized(&mountInitialized)) { LOG_ERROR("Error determining if mount is initialized!"); return false; } if (!IsMountParked(&mountParked)) { LOG_ERROR("Error determining if mount is parked!"); return false; } if (!mountInitialized) { LOG_DEBUG("Mount is not yet initialized. Initializing it..."); if (isSimulation() == false) { // This is how to init the mount in case RA/DE are missing. // :PO# if (setAPUnPark(PortFD) < 0) { LOG_ERROR("UnParking Failed."); return false; } // Stop :Q# abortSlew(PortFD); } } mountInitialized = true; LOG_DEBUG("Mount is initialized."); // Astrophysics mount is always unparked on startup // In this driver, unpark only sets the tracking ON. // setAPUnPark() is NOT called as this function, despite its name, is only used for initialization purposes. UnPark(); // On most mounts SlewRateS defines the MoveTo AND Slew (GOTO) speeds // lx200ap is different - some of the MoveTo speeds are not VALID // Slew speeds so we have to keep two lists. // // SlewRateS is used as the MoveTo speed if (isSimulation() == false && (err = selectAPMoveToRate(PortFD, IUFindOnSwitchIndex(&SlewRateSP))) < 0) { LOGF_ERROR("Error setting move rate (%d).", err); return false; } SlewRateSP.s = IPS_OK; IDSetSwitch(&SlewRateSP, nullptr); // APSlewSpeedsS defines the Slew (GOTO) speeds valid on the AP mounts if (isSimulation() == false && (err = selectAPSlewRate(PortFD, IUFindOnSwitchIndex(&APSlewSpeedSP))) < 0) { LOGF_ERROR("Error setting slew to rate (%d).", err); return false; } APSlewSpeedSP.s = IPS_OK; IDSetSwitch(&APSlewSpeedSP, nullptr); return true; } /************************************************************************************** ** ***************************************************************************************/ bool LX200AstroPhysicsExperimental::ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) { if (strcmp(getDeviceName(), dev)) return false; if (!strcmp(name, MeridianDelayNP.name)) { if (IUUpdateNumber(&MeridianDelayNP, values, names, n) < 0) return false; float mdelay; int err; mdelay = MeridianDelayN[0].value; LOGF_INFO("lx200ap_experimental: meridian delay request = %f", mdelay); if (!isSimulation() && (err = setAPMeridianDelay(PortFD, mdelay) < 0)) { LOGF_ERROR("lx200ap_experimental: Error setting meridian delay (%d).", err); return false; } MeridianDelayNP.s = IPS_OK; IDSetNumber(&MeridianDelayNP, nullptr); return true; } return LX200Generic::ISNewNumber(dev, name, values, names, n); } bool LX200AstroPhysicsExperimental::ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) { int err = 0; // ignore if not ours // if (strcmp(getDeviceName(), dev)) return false; // ======================================= // Swap Buttons // ======================================= if (!strcmp(name, SwapSP.name)) { int currentSwap; IUResetSwitch(&SwapSP); IUUpdateSwitch(&SwapSP, states, names, n); currentSwap = IUFindOnSwitchIndex(&SwapSP); if ((!isSimulation() && (err = swapAPButtons(PortFD, currentSwap)) < 0)) { LOGF_ERROR("Error swapping buttons (%d).", err); return false; } SwapS[0].s = ISS_OFF; SwapS[1].s = ISS_OFF; SwapSP.s = IPS_OK; IDSetSwitch(&SwapSP, nullptr); return true; } // =========================================================== // GOTO ("slew") Speed. // =========================================================== if (!strcmp(name, APSlewSpeedSP.name)) { IUUpdateSwitch(&APSlewSpeedSP, states, names, n); int slewRate = IUFindOnSwitchIndex(&APSlewSpeedSP); if (!isSimulation() && (err = selectAPSlewRate(PortFD, slewRate) < 0)) { LOGF_ERROR("Error setting move to rate (%d).", err); return false; } APSlewSpeedSP.s = IPS_OK; IDSetSwitch(&APSlewSpeedSP, nullptr); return true; } // =========================================================== // Guide Speed. // =========================================================== if (!strcmp(name, APGuideSpeedSP.name)) { IUUpdateSwitch(&APGuideSpeedSP, states, names, n); int guideRate = IUFindOnSwitchIndex(&APGuideSpeedSP); if (!isSimulation() && (err = selectAPGuideRate(PortFD, guideRate) < 0)) { LOGF_ERROR("Error setting guiding to rate (%d).", err); return false; } APGuideSpeedSP.s = IPS_OK; IDSetSwitch(&APGuideSpeedSP, nullptr); return true; } // ======================================= // Choose the appropriate sync command // ======================================= if (!strcmp(name, SyncCMRSP.name)) { IUResetSwitch(&SyncCMRSP); IUUpdateSwitch(&SyncCMRSP, states, names, n); IUFindOnSwitchIndex(&SyncCMRSP); SyncCMRSP.s = IPS_OK; IDSetSwitch(&SyncCMRSP, nullptr); return true; } // ======================================= // Choose the PEC playback mode // ======================================= if (!strcmp(name, PECStateSP.name)) { IUResetSwitch(&PECStateSP); IUUpdateSwitch(&PECStateSP, states, names, n); IUFindOnSwitchIndex(&PECStateSP); int pecstate = IUFindOnSwitchIndex(&PECStateSP); if (!isSimulation() && (err = selectAPPECState(PortFD, pecstate) < 0)) { LOGF_ERROR("Error setting PEC state (%d).", err); return false; } PECStateSP.s = IPS_OK; IDSetSwitch(&PECStateSP, nullptr); return true; } // =========================================================== // Unpark from positions // =========================================================== if (!strcmp(name, UnparkFromSP.name)) { IUUpdateSwitch(&UnparkFromSP, states, names, n); int unparkPos = IUFindOnSwitchIndex(&UnparkFromSP); LOGF_DEBUG("Unpark from pos set to (%d).", unparkPos); UnparkFromSP.s = IPS_OK; IDSetSwitch(&UnparkFromSP, nullptr); return true; } // =========================================================== // Park To positions // =========================================================== if (!strcmp(name, ParkToSP.name)) { IUUpdateSwitch(&ParkToSP, states, names, n); ParkPosition parkPos = (ParkPosition) IUFindOnSwitchIndex(&ParkToSP); LOGF_DEBUG("Park to pos set to (%d).", parkPos); ParkToSP.s = IPS_OK; IDSetSwitch(&ParkToSP, nullptr); // override with predefined position if selected if (parkPos != PARK_CUSTOM) { double parkAz, parkAlt; if (calcParkPosition(parkPos, &parkAz, &parkAlt)) { SetAxis1Park(parkAlt); SetAxis2Park(parkAz); LOGF_DEBUG("Set predefined park position %d to az=%f alt=%f", parkPos, parkAz, parkAlt); } else { LOGF_ERROR("Unable to set predefined park position %d!!", parkPos); } } return true; } return LX200Generic::ISNewSwitch(dev, name, states, names, n); } bool LX200AstroPhysicsExperimental::ReadScopeStatus() { if (isSimulation()) { mountSim(); return true; } if (getLX200RA(PortFD, ¤tRA) < 0 || getLX200DEC(PortFD, ¤tDEC) < 0) { EqNP.s = IPS_ALERT; IDSetNumber(&EqNP, "Error reading RA/DEC."); return false; } if (TrackState == SCOPE_SLEWING) { double dx = lastRA - currentRA; double dy = lastDE - currentDEC; LOGF_DEBUG("Slewing... currentRA: %g dx: %g currentDE: %g dy: %g", currentRA, dx, currentDEC, dy); // Wait until acknowledged if (dx == 0 && dy == 0) { TrackState = SCOPE_TRACKING; LOG_INFO("Slew is complete. Tracking..."); } // Keep try of last values to determine if the mount settled. lastRA = currentRA; lastDE = currentDEC; } else if (TrackState == SCOPE_PARKING) { // new way char parkStatus; char slewStatus; bool slewcomplete; double PARKTHRES=0.1; // max difference from parked position to consider mount PARKED slewcomplete = false; if (check_lx200ap_status(PortFD, &parkStatus, &slewStatus) == 0) { LOGF_DEBUG("parkStatus: %c slewStatus: %c", parkStatus, slewStatus); if (slewStatus == '0') slewcomplete = true; } // old way if (getLX200Az(PortFD, ¤tAz) < 0 || getLX200Alt(PortFD, ¤tAlt) < 0) { EqNP.s = IPS_ALERT; IDSetNumber(&EqNP, "Error reading Az/Alt."); return false; } double dx = lastAZ - currentAz; double dy = lastAL - currentAlt; LOGF_DEBUG("Parking... currentAz: %g dx: %g currentAlt: %g dy: %g", currentAz, dx, currentAlt, dy); // if for some reason we check slew status BEFORE park motion starts make sure we dont consider park // action complete too early by checking how far from park position we are! if (slewcomplete && (dx > PARKTHRES || dy > PARKTHRES)) { LOG_WARN("Parking... slew status indicates mount stopped by dx/dy too far from mount - continuing!"); slewcomplete = false; } if (slewcomplete) { LOG_DEBUG("Parking slew is complete. Asking astrophysics mount to park..."); if (!isSimulation() && setAPPark(PortFD) < 0) { LOG_ERROR("Parking Failed."); return false; } // Turn off tracking. SetTrackEnabled(false); SetParked(true); LOG_INFO("Please disconnect and power off the mount."); } lastAZ = currentAz; lastAL = currentAlt; } NewRaDec(currentRA, currentDEC); syncSideOfPier(); return true; } // experimental function needs testing!!! bool LX200AstroPhysicsExperimental::IsMountInitialized(bool *initialized) { double ra, dec; bool raZE, deZE, de90; double epscheck = 1e-5; // two doubles this close are considered equal LOG_DEBUG("EXPERIMENTAL: LX200AstroPhysicsExperimental::IsMountInitialized()"); if (getLX200RA(PortFD, &ra) || getLX200DEC(PortFD, &dec)) return false; LOGF_DEBUG("IsMountInitialized: RA: %f - DEC: %f", ra, dec); raZE = (fabs(ra) < epscheck); deZE = (fabs(dec) < epscheck); de90 = (fabs(dec-90) < epscheck); LOGF_DEBUG("IsMountInitialized: raZE: %d - deZE: %d - de90: %d", raZE, deZE, de90); // RA is zero and DEC is zero or 90 // then mount is not initialized and we need to initialized it. if ( (raZE && deZE) || (raZE && de90)) { LOG_WARN("Mount is not yet initialized."); *initialized = false; return true; } // mount is initialized LOG_INFO("Mount is initialized."); *initialized = true; return true; } // experimental function needs testing!!! bool LX200AstroPhysicsExperimental::IsMountParked(bool *isParked) { const struct timespec timeout = {0, 250000000L}; double ra1, ra2; LOG_DEBUG("EXPERIMENTAL: LX200AstroPhysicsExperimental::IsMountParked()"); // try one method if (getMountStatus(isParked)) { return true; } // fallback for older controllers if (getLX200RA(PortFD, &ra1)) return false; // wait 250ms nanosleep(&timeout, NULL); if (getLX200RA(PortFD, &ra2)) return false; // if within an arcsec then assume RA is constant if (fabs(ra1-ra2) < (1.0/(15.0*3600.0))) { *isParked=false; return true; } // can't determine return false; } bool LX200AstroPhysicsExperimental::getMountStatus(bool *isParked) { // check for newer if ((firmwareVersion != MCV_UNKNOWN) && (firmwareVersion >= MCV_T)) { char parkStatus; char slewStatus; if (check_lx200ap_status(PortFD, &parkStatus, &slewStatus) == 0) { LOGF_DEBUG("parkStatus: %c", parkStatus); *isParked = (parkStatus == 'P'); return true; } } return false; } bool LX200AstroPhysicsExperimental::Goto(double r, double d) { const struct timespec timeout = {0, 100000000L}; targetRA = r; targetDEC = d; char RAStr[64], DecStr[64]; fs_sexa(RAStr, targetRA, 2, 3600); fs_sexa(DecStr, targetDEC, 2, 3600); // If moving, let's stop it first. if (EqNP.s == IPS_BUSY) { if (!isSimulation() && abortSlew(PortFD) < 0) { AbortSP.s = IPS_ALERT; IDSetSwitch(&AbortSP, "Abort slew failed."); return false; } AbortSP.s = IPS_OK; EqNP.s = IPS_IDLE; IDSetSwitch(&AbortSP, "Slew aborted."); IDSetNumber(&EqNP, nullptr); if (MovementNSSP.s == IPS_BUSY || MovementWESP.s == IPS_BUSY) { MovementNSSP.s = MovementWESP.s = IPS_IDLE; EqNP.s = IPS_IDLE; IUResetSwitch(&MovementNSSP); IUResetSwitch(&MovementWESP); IDSetSwitch(&MovementNSSP, nullptr); IDSetSwitch(&MovementWESP, nullptr); } // sleep for 100 mseconds nanosleep(&timeout, NULL); } if (!isSimulation()) { if (setAPObjectRA(PortFD, targetRA) < 0 || (setAPObjectDEC(PortFD, targetDEC)) < 0) { EqNP.s = IPS_ALERT; IDSetNumber(&EqNP, "Error setting RA/DEC."); return false; } int err = 0; /* Slew reads the '0', that is not the end of the slew */ if ((err = Slew(PortFD))) { EqNP.s = IPS_ALERT; IDSetNumber(&EqNP, "Error Slewing to JNow RA %s - DEC %s\n", RAStr, DecStr); slewError(err); return false; } motionCommanded = true; lastRA = targetRA; lastDE = targetDEC; } TrackState = SCOPE_SLEWING; EqNP.s = IPS_BUSY; LOGF_INFO("Slewing to RA: %s - DEC: %s", RAStr, DecStr); return true; } int LX200AstroPhysicsExperimental::SendPulseCmd(int direction, int duration_msec) { return APSendPulseCmd(PortFD, direction, duration_msec); } bool LX200AstroPhysicsExperimental::Handshake() { if (isSimulation()) { LOG_INFO("Simulated Astrophysics is online. Retrieving basic data..."); return true; } int err=0; if ((err = setAPClearBuffer(PortFD)) < 0) { LOGF_ERROR("Error clearing the buffer (%d): %s", err, strerror(err)); return false; } if ((err = setAPBackLashCompensation(PortFD, 0, 0, 0)) < 0) { // It seems we need to send it twice before it works! if ((err = setAPBackLashCompensation(PortFD, 0, 0, 0)) < 0) { LOGF_ERROR("Error setting back lash compensation (%d): %s.", err, strerror(err)); return false; } } // get firmware version bool rc=false; rc = getFirmwareVersion(); // see if firmware is 'V' or not if (!rc || firmwareVersion == MCV_UNKNOWN || firmwareVersion < MCV_V) { LOG_ERROR("Firmware version is not 'V' - too old to use the experimental driver!"); return false; } else { LOG_INFO("Firmware level 'V' detected - driver loaded."); } disclaimerMessage(); // Detect and set fomat. It should be LONG. return (checkLX200Format(PortFD) == 0); } bool LX200AstroPhysicsExperimental::Disconnect() { timeUpdated = false; //locationUpdated = false; mountInitialized = false; return LX200Generic::Disconnect(); } bool LX200AstroPhysicsExperimental::Sync(double ra, double dec) { char syncString[256]; int syncType = IUFindOnSwitchIndex(&SyncCMRSP); if (!isSimulation()) { if (setAPObjectRA(PortFD, ra) < 0 || setAPObjectDEC(PortFD, dec) < 0) { EqNP.s = IPS_ALERT; IDSetNumber(&EqNP, "Error setting RA/DEC. Unable to Sync."); return false; } bool syncOK = true; switch (syncType) { case USE_REGULAR_SYNC: if (::Sync(PortFD, syncString) < 0) syncOK = false; break; case USE_CMR_SYNC: if (APSyncCMR(PortFD, syncString) < 0) syncOK = false; break; default: break; } if (syncOK == false) { EqNP.s = IPS_ALERT; IDSetNumber(&EqNP, "Synchronization failed."); return false; } } currentRA = ra; currentDEC = dec; LOGF_DEBUG("%s Synchronization successful %s", (syncType == USE_REGULAR_SYNC ? "CM" : "CMR"), syncString); LOG_INFO("Synchronization successful."); EqNP.s = IPS_OK; NewRaDec(currentRA, currentDEC); return true; } bool LX200AstroPhysicsExperimental::updateTime(ln_date *utc, double utc_offset) { struct ln_zonedate ltm; ln_date_to_zonedate(utc, <m, utc_offset * 3600.0); JD = ln_get_julian_day(utc); LOGF_DEBUG("New JD is %.2f", JD); // Set Local Time if (isSimulation() == false && setLocalTime(PortFD, ltm.hours, ltm.minutes, (int)ltm.seconds) < 0) { LOG_ERROR("Error setting local time."); return false; } LOGF_DEBUG("Set Local Time %02d:%02d:%02d is successful.", ltm.hours, ltm.minutes, (int)ltm.seconds); if (isSimulation() == false && setCalenderDate(PortFD, ltm.days, ltm.months, ltm.years) < 0) { LOG_ERROR("Error setting local date."); return false; } LOGF_DEBUG("Set Local Date %02d/%02d/%02d is successful.", ltm.days, ltm.months, ltm.years); if (isSimulation() == false && setAPUTCOffset(PortFD, fabs(utc_offset)) < 0) { LOG_ERROR("Error setting UTC Offset."); return false; } LOGF_DEBUG("Set UTC Offset %g (always positive for AP) is successful.", fabs(utc_offset)); LOG_INFO("Time updated."); timeUpdated = true; if (locationUpdated && timeUpdated && mountInitialized == false) initMount(); return true; } bool LX200AstroPhysicsExperimental::updateLocation(double latitude, double longitude, double elevation) { INDI_UNUSED(elevation); if (!isSimulation() && setAPSiteLongitude(PortFD, 360.0 - longitude) < 0) { LOG_ERROR("Error setting site longitude coordinates"); return false; } if (!isSimulation() && setAPSiteLatitude(PortFD, latitude) < 0) { LOG_ERROR("Error setting site latitude coordinates"); return false; } char l[32], L[32]; fs_sexa(l, latitude, 3, 3600); fs_sexa(L, longitude, 4, 3600); LOGF_INFO("Site location updated to Lat %.32s - Long %.32s", l, L); locationUpdated = true; if (locationUpdated && timeUpdated && mountInitialized == false) initMount(); return true; } void LX200AstroPhysicsExperimental::debugTriggered(bool enable) { LX200Generic::debugTriggered(enable); // we use routines from legacy AP driver routines and newer experimental driver routines set_lx200ap_name(getDeviceName(), DBG_SCOPE); set_lx200ap_exp_name(getDeviceName(), DBG_SCOPE); } // For most mounts the SetSlewRate() method sets both the MoveTo and Slew (GOTO) speeds. // For AP mounts these two speeds are handled separately - so SetSlewRate() actually sets the MoveTo speed for AP mounts - confusing! // ApSetSlew bool LX200AstroPhysicsExperimental::SetSlewRate(int index) { if (!isSimulation() && selectAPMoveToRate(PortFD, index) < 0) { SlewRateSP.s = IPS_ALERT; IDSetSwitch(&SlewRateSP, "Error setting slew mode."); return false; } SlewRateSP.s = IPS_OK; IDSetSwitch(&SlewRateSP, nullptr); return true; } bool LX200AstroPhysicsExperimental::Park() { double parkAz = GetAxis1Park(); double parkAlt = GetAxis2Park(); char AzStr[16], AltStr[16]; fs_sexa(AzStr, parkAz, 2, 3600); fs_sexa(AltStr, parkAlt, 2, 3600); LOGF_DEBUG("Parking to Az (%s) Alt (%s)...", AzStr, AltStr); if (isSimulation()) { ln_lnlat_posn observer; observer.lat = LocationN[LOCATION_LATITUDE].value; observer.lng = LocationN[LOCATION_LONGITUDE].value; if (observer.lng > 180) observer.lng -= 360; ln_hrz_posn horizontalPos; // Libnova south = 0, west = 90, north = 180, east = 270 horizontalPos.az = parkAz + 180; if (horizontalPos.az > 360) horizontalPos.az -= 360; horizontalPos.alt = parkAlt; ln_equ_posn equatorialPos; ln_get_equ_from_hrz(&horizontalPos, &observer, ln_get_julian_from_sys(), &equatorialPos); Goto(equatorialPos.ra / 15.0, equatorialPos.dec); } else { if (setAPObjectAZ(PortFD, parkAz) < 0 || setAPObjectAlt(PortFD, parkAlt) < 0) { LOG_ERROR("Error setting Az/Alt."); return false; } int err = 0; /* Slew reads the '0', that is not the end of the slew */ if ((err = Slew(PortFD))) { LOGF_ERROR("Error Slewing to Az %s - Alt %s", AzStr, AltStr); slewError(err); return false; } motionCommanded = true; lastAZ = parkAz; lastAL = parkAlt; } EqNP.s = IPS_BUSY; TrackState = SCOPE_PARKING; LOG_INFO("Parking is in progress..."); return true; } bool LX200AstroPhysicsExperimental::calcParkPosition(ParkPosition pos, double *parkAlt, double *parkAz) { switch (pos) { // last unparked case PARK_CUSTOM: LOG_ERROR("Called calcParkPosition with PARK_CUSTOM!"); return false; break; // Park 1 case 1: LOG_DEBUG("Computing PARK1 position..."); *parkAlt = 0; *parkAz = 0; break; // Park 2 case 2: LOG_DEBUG("Computing PARK2 position..."); *parkAlt = 0; *parkAz = 90; break; // Park 3 case 3: LOG_DEBUG("Computing PARK3 position..."); *parkAlt = LocationN[LOCATION_LATITUDE].value; *parkAz = 0; break; // Park 4 case 4: LOG_DEBUG("Computing PARK4 position..."); *parkAlt = 0; *parkAz = 180; break; default: LOG_ERROR("Unknown park position!"); return false; break; } LOGF_DEBUG("calcParkPosition: parkPos=%d parkAlt=%f parkAz=%f", pos, *parkAlt, *parkAz); return true; } bool LX200AstroPhysicsExperimental::UnPark() { // The AP :PO# should only be used during initilization and not here as indicated by email from Preston on 2017-12-12 // check the unpark from position and set mount as appropriate ParkPosition unparkPos; unparkPos = (ParkPosition) IUFindOnSwitchIndex(&UnparkFromSP); LOGF_DEBUG("Unpark() -> unpark position = %d", unparkPos); if (unparkPos == PARK_LAST) { LOG_INFO("Unparking from last parked position..."); } else { double unparkAlt, unparkAz; if (!calcParkPosition(unparkPos, &unparkAlt, &unparkAz)) { LOG_ERROR("Error calculating unpark position!"); return false; } LOGF_DEBUG("unparkPos=%d unparkAlt=%f unparkAz=%f", unparkPos, unparkAlt, unparkAz); if (setAPObjectAZ(PortFD, unparkAz) < 0 || (setAPObjectAlt(PortFD, unparkAlt)) < 0) { LOG_ERROR("Error setting Az/Alt."); return false; } char syncString[256]; if (APSyncCM(PortFD, syncString) < 0) { LOG_WARN("Sync failed."); return false; } } // Enable tracking SetTrackEnabled(true); SetParked(false); return true; } bool LX200AstroPhysicsExperimental::SetCurrentPark() { ln_hrz_posn horizontalPos; // Libnova south = 0, west = 90, north = 180, east = 270 ln_lnlat_posn observer; observer.lat = LocationN[LOCATION_LATITUDE].value; observer.lng = LocationN[LOCATION_LONGITUDE].value; if (observer.lng > 180) observer.lng -= 360; ln_equ_posn equatorialPos; equatorialPos.ra = currentRA * 15; equatorialPos.dec = currentDEC; ln_get_hrz_from_equ(&equatorialPos, &observer, ln_get_julian_from_sys(), &horizontalPos); double parkAZ = horizontalPos.az - 180; if (parkAZ < 0) parkAZ += 360; double parkAlt = horizontalPos.alt; char AzStr[16], AltStr[16]; fs_sexa(AzStr, parkAZ, 2, 3600); fs_sexa(AltStr, parkAlt, 2, 3600); LOGF_DEBUG("Setting current parking position to coordinates Az (%s) Alt (%s)", AzStr, AltStr); SetAxis1Park(parkAZ); SetAxis2Park(parkAlt); return true; } bool LX200AstroPhysicsExperimental::SetDefaultPark() { // Az = 0 for North hemisphere SetAxis1Park(LocationN[LOCATION_LATITUDE].value > 0 ? 0 : 180); // Alt = Latitude SetAxis2Park(LocationN[LOCATION_LATITUDE].value); return true; } void LX200AstroPhysicsExperimental::syncSideOfPier() { const char *cmd = ":pS#"; // Response char response[16] = { 0 }; int rc = 0, nbytes_read = 0, nbytes_written = 0; LOGF_DEBUG("CMD: <%s>", cmd); tcflush(PortFD, TCIOFLUSH); if ((rc = tty_write(PortFD, cmd, strlen(cmd), &nbytes_written)) != TTY_OK) { char errmsg[256]; tty_error_msg(rc, errmsg, 256); LOGF_ERROR("Error writing to device %s (%d)", errmsg, rc); return; } // Read Side if ((rc = tty_read_section(PortFD, response, '#', 3, &nbytes_read)) != TTY_OK) { char errmsg[256]; tty_error_msg(rc, errmsg, 256); LOGF_ERROR("Error reading from device %s (%d)", errmsg, rc); return; } response[nbytes_read - 1] = '\0'; tcflush(PortFD, TCIOFLUSH); LOGF_DEBUG("RES: <%s>", response); if (!strcmp(response, "East")) setPierSide(INDI::Telescope::PIER_EAST); else if (!strcmp(response, "West")) setPierSide(INDI::Telescope::PIER_WEST); else LOGF_ERROR("Invalid pier side response from device-> %s", response); } bool LX200AstroPhysicsExperimental::saveConfigItems(FILE *fp) { LX200Generic::saveConfigItems(fp); IUSaveConfigSwitch(fp, &SyncCMRSP); IUSaveConfigSwitch(fp, &APSlewSpeedSP); IUSaveConfigSwitch(fp, &APGuideSpeedSP); IUSaveConfigSwitch(fp, &ParkToSP); return true; } bool LX200AstroPhysicsExperimental::SetTrackMode(uint8_t mode) { int err=0; if (mode == TRACK_CUSTOM) { if (!isSimulation() && (err = selectAPTrackingMode(PortFD, AP_TRACKING_SIDEREAL)) < 0) { LOGF_ERROR("Error setting tracking mode (%d).", err); return false; } return SetTrackRate(TrackRateN[AXIS_RA].value, TrackRateN[AXIS_DE].value); } if (!isSimulation() && (err = selectAPTrackingMode(PortFD, mode)) < 0) { LOGF_ERROR("Error setting tracking mode (%d).", err); return false; } return true; } bool LX200AstroPhysicsExperimental::SetTrackEnabled(bool enabled) { return SetTrackMode(enabled ? IUFindOnSwitchIndex(&TrackModeSP) : AP_TRACKING_OFF); } bool LX200AstroPhysicsExperimental::SetTrackRate(double raRate, double deRate) { // Convert to arcsecs/s to AP sidereal multiplier /* :RR0.0000# = normal sidereal tracking in RA - similar to :RT2# :RR+1.0000# = 1 + normal sidereal = 2X sidereal :RR+9.0000# = 9 + normal sidereal = 10X sidereal :RR-1.0000# = normal sidereal - 1 = 0 or Stop - similar to :RT9# :RR-11.0000# = normal sidereal - 11 = -10X sidereal (East at 10X) :RD0.0000# = normal zero rate for Dec. :RD5.0000# = 5 + normal zero rate = 5X sidereal clockwise from above - equivalent to South :RD-5.0000# = normal zero rate - 5 = 5X sidereal counter-clockwise from above - equivalent to North */ double APRARate = (raRate - TRACKRATE_SIDEREAL) / TRACKRATE_SIDEREAL; double APDERate = deRate / TRACKRATE_SIDEREAL; if (!isSimulation()) { if (setAPRATrackRate(PortFD, APRARate) < 0 || setAPDETrackRate(PortFD, APDERate) < 0) return false; } return true; } bool LX200AstroPhysicsExperimental::getUTFOffset(double *offset) { return (getAPUTCOffset(PortFD, offset) == 0); } bool LX200AstroPhysicsExperimental::MoveNS(INDI_DIR_NS dir, TelescopeMotionCommand command) { bool rc = LX200Generic::MoveNS(dir, command); if (command == MOTION_START) motionCommanded = true; return rc; } bool LX200AstroPhysicsExperimental::MoveWE(INDI_DIR_WE dir, TelescopeMotionCommand command) { bool rc = LX200Generic::MoveWE(dir, command); if (command == MOTION_START) motionCommanded = true; return rc; } libindi/drivers/telescope/lx200gemini.cpp0000664000175000017500000005722613263645557017711 0ustar jasemjasem/* Losmandy Gemini INDI driver Copyright (C) 2017 Jasem Mutlaq Difference from LX200 Generic: 1. Added Side of Pier 2. Reimplemented isSlewComplete to use :Gv# since it is more reliable 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 */ #include "lx200gemini.h" #include "indicom.h" #include "lx200driver.h" #include "connectionplugins/connectioninterface.h" #include "connectionplugins/connectiontcp.h" #include #include #define MANUAL_SLEWING_SPEED_ID 120 #define GOTO_SLEWING_SPEED_ID 140 #define MOVE_SPEED_ID 145 #define GUIDING_SPEED_ID 150 #define CENTERING_SPEED_ID 170 LX200Gemini::LX200Gemini() { setVersion(1, 3); setLX200Capability(LX200_HAS_SITES | LX200_HAS_FOCUS | LX200_HAS_PULSE_GUIDING); SetTelescopeCapability(TELESCOPE_CAN_PARK | TELESCOPE_CAN_SYNC | TELESCOPE_CAN_GOTO | TELESCOPE_CAN_ABORT | TELESCOPE_HAS_TIME | TELESCOPE_HAS_LOCATION | TELESCOPE_HAS_PIER_SIDE | TELESCOPE_HAS_TRACK_MODE | TELESCOPE_CAN_CONTROL_TRACK, 4); } const char *LX200Gemini::getDefaultName() { return (const char *)"Losmandy Gemini"; } bool LX200Gemini::Connect() { Connection::Interface *activeConnection = getActiveConnection(); if (!activeConnection->name().compare("CONNECTION_TCP")) { tty_set_gemini_udp_format(1); } return LX200Generic::Connect(); } void LX200Gemini::ISGetProperties(const char *dev) { LX200Generic::ISGetProperties(dev); defineSwitch(&StartupModeSP); loadConfig(true, StartupModeSP.name); } bool LX200Gemini::initProperties() { LX200Generic::initProperties(); // Park Option IUFillSwitch(&ParkSettingsS[PARK_HOME], "HOME", "Home", ISS_ON); IUFillSwitch(&ParkSettingsS[PARK_STARTUP], "STARTUP", "Startup", ISS_OFF); IUFillSwitch(&ParkSettingsS[PARK_ZENITH], "ZENITH", "Zenith", ISS_OFF); IUFillSwitchVector(&ParkSettingsSP, ParkSettingsS, 3, getDeviceName(), "PARK_SETTINGS", "Park Settings", MAIN_CONTROL_TAB, IP_RW, ISR_1OFMANY, 60, IPS_IDLE); IUFillSwitch(&StartupModeS[COLD_START], "COLD_START", "Cold", ISS_ON); IUFillSwitch(&StartupModeS[PARK_STARTUP], "WARM_START", "Warm", ISS_OFF); IUFillSwitch(&StartupModeS[PARK_ZENITH], "WARM_RESTART", "Restart", ISS_OFF); IUFillSwitchVector(&StartupModeSP, StartupModeS, 3, getDeviceName(), "STARTUP_MODE", "Startup Mode", MAIN_CONTROL_TAB, IP_RW, ISR_1OFMANY, 60, IPS_IDLE); IUFillNumber(&ManualSlewingSpeedN[0], "MANUAL_SLEWING_SPEED", "Manual Slewing Speed", "%g", 20, 2000., 10., 800); IUFillNumberVector(&ManualSlewingSpeedNP, ManualSlewingSpeedN, 1, getDeviceName(), "MANUAL_SLEWING_SPEED", "Manual Slewing Speed", OPTIONS_TAB, IP_RW, 0, IPS_IDLE); IUFillNumber(&GotoSlewingSpeedN[0], "GOTO_SLEWING_SPEED", "Goto Slewing Speed", "%g", 20, 2000., 10., 800); IUFillNumberVector(&GotoSlewingSpeedNP, GotoSlewingSpeedN, 1, getDeviceName(), "GOTO_SLEWING_SPEED", "Goto Slewing Speed", OPTIONS_TAB, IP_RW, 0, IPS_IDLE); IUFillNumber(&MoveSpeedN[0], "MOVE_SPEED", "Move Speed", "%g", 20, 2000., 10., 10); IUFillNumberVector(&MoveSpeedNP, MoveSpeedN, 1, getDeviceName(), "MOVE_SLEWING_SPEED", "Move Slewing Speed", OPTIONS_TAB, IP_RW, 0, IPS_IDLE); IUFillNumber(&GuidingSpeedN[0], "GUIDING_SPEED", "Guiding Speed", "%g", 0.2, 0.8, 0.1, 0.5); IUFillNumberVector(&GuidingSpeedNP, GuidingSpeedN, 1, getDeviceName(), "GUIDING_SLEWING_SPEED", "Guiding Slewing Speed", OPTIONS_TAB, IP_RW, 0, IPS_IDLE); IUFillNumber(&CenteringSpeedN[0], "CENTERING_SPEED", "Centering Speed", "%g", 20, 2000., 10., 10); IUFillNumberVector(&CenteringSpeedNP, CenteringSpeedN, 1, getDeviceName(), "CENTERING_SLEWING_SPEED", "Centering Slewing Speed", OPTIONS_TAB, IP_RW, 0, IPS_IDLE); IUFillSwitch(&TrackModeS[GEMINI_TRACK_SIDEREAL], "TRACK_SIDEREAL", "Sidereal", ISS_ON); IUFillSwitch(&TrackModeS[GEMINI_TRACK_KING], "TRACK_CUSTOM", "King", ISS_OFF); IUFillSwitch(&TrackModeS[GEMINI_TRACK_LUNAR], "TRACK_LUNAR", "Lunar", ISS_OFF); IUFillSwitch(&TrackModeS[GEMINI_TRACK_SOLAR], "TRACK_SOLAR", "Solar", ISS_OFF); return true; } bool LX200Gemini::updateProperties() { const int MAX_VALUE_LENGTH = 32; char value[MAX_VALUE_LENGTH]; unsigned int speed = 0; float guidingSpeed = 0.0; LX200Generic::updateProperties(); if (isConnected()) { defineSwitch(&ParkSettingsSP); if (getGeminiProperty(MANUAL_SLEWING_SPEED_ID, value)) { sscanf(value, "%d", &speed); ManualSlewingSpeedN[0].value = speed; defineNumber(&ManualSlewingSpeedNP); } if (getGeminiProperty(GOTO_SLEWING_SPEED_ID, value)) { sscanf(value, "%d", &speed); GotoSlewingSpeedN[0].value = speed; defineNumber(&GotoSlewingSpeedNP); } if (getGeminiProperty(MOVE_SPEED_ID, value)) { sscanf(value, "%d", &speed); MoveSpeedN[0].value = speed; defineNumber(&MoveSpeedNP); } if (getGeminiProperty(GUIDING_SPEED_ID, value)) { sscanf(value, "%f", &guidingSpeed); GuidingSpeedN[0].value = guidingSpeed; defineNumber(&GuidingSpeedNP); } if (getGeminiProperty(CENTERING_SPEED_ID, value)) { sscanf(value, "%d", &speed); CenteringSpeedN[0].value = speed; defineNumber(&CenteringSpeedNP); } } else { deleteProperty(ParkSettingsSP.name); deleteProperty(ManualSlewingSpeedNP.name); deleteProperty(GotoSlewingSpeedNP.name); deleteProperty(MoveSpeedNP.name); deleteProperty(GuidingSpeedNP.name); deleteProperty(CenteringSpeedNP.name); } return true; } bool LX200Gemini::ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) { if (dev != nullptr && strcmp(dev, getDeviceName()) == 0) { if (!strcmp(name, StartupModeSP.name)) { IUUpdateSwitch(&StartupModeSP, states, names, n); StartupModeSP.s = IPS_OK; LOG_INFO("Startup mode will take effect on future connections."); IDSetSwitch(&StartupModeSP, nullptr); return true; } if (!strcmp(name, ParkSettingsSP.name)) { IUUpdateSwitch(&ParkSettingsSP, states, names, n); ParkSettingsSP.s = IPS_OK; IDSetSwitch(&ParkSettingsSP, nullptr); return true; } } return LX200Generic::ISNewSwitch(dev, name, states, names, n); } bool LX200Gemini::ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) { char valueString[16] = {0}; if (dev != nullptr && strcmp(dev, getDeviceName()) == 0) { snprintf(valueString, 16, "%2.0f", values[0]); if (!strcmp(name, ManualSlewingSpeedNP.name)) { LOGF_DEBUG("Trying to set manual slewing speed of: %f\n", values[0]); if (!isSimulation() && !setGeminiProperty(MANUAL_SLEWING_SPEED_ID, valueString)) { ManualSlewingSpeedNP.s = IPS_ALERT; IDSetNumber(&ManualSlewingSpeedNP, "Error setting manual slewing speed"); return false; } ManualSlewingSpeedNP.s = IPS_OK; ManualSlewingSpeedN[0].value = values[0]; IDSetNumber(&ManualSlewingSpeedNP, "Manual slewing speed set to %f", values[0]); return true; } if (!strcmp(name, GotoSlewingSpeedNP.name)) { LOGF_DEBUG("Trying to set goto slewing speed of: %f\n", values[0]); if (!isSimulation() && !setGeminiProperty(GOTO_SLEWING_SPEED_ID, valueString)) { GotoSlewingSpeedNP.s = IPS_ALERT; IDSetNumber(&GotoSlewingSpeedNP, "Error setting goto slewing speed"); return false; } GotoSlewingSpeedNP.s = IPS_OK; GotoSlewingSpeedN[0].value = values[0]; IDSetNumber(&GotoSlewingSpeedNP, "Goto slewing speed set to %f", values[0]); return true; } if (!strcmp(name, MoveSpeedNP.name)) { LOGF_DEBUG("Trying to set move speed of: %f\n", values[0]); if (!isSimulation() && !setGeminiProperty(MOVE_SPEED_ID, valueString)) { MoveSpeedNP.s = IPS_ALERT; IDSetNumber(&MoveSpeedNP, "Error setting move speed"); return false; } MoveSpeedNP.s = IPS_OK; MoveSpeedN[0].value = values[0]; IDSetNumber(&MoveSpeedNP, "Move speed set to %f", values[0]); return true; } if (!strcmp(name, GuidingSpeedNP.name)) { LOGF_DEBUG("Trying to set guiding speed of: %f\n", values[0]); // Special formatting for guiding speed snprintf(valueString, 16, "%1.1f", values[0]); if (!isSimulation() && !setGeminiProperty(GUIDING_SPEED_ID, valueString)) { GuidingSpeedNP.s = IPS_ALERT; IDSetNumber(&GuidingSpeedNP, "Error setting guiding speed"); return false; } GuidingSpeedNP.s = IPS_OK; GuidingSpeedN[0].value = values[0]; IDSetNumber(&GuidingSpeedNP, "Guiding speed set to %f", values[0]); return true; } if (!strcmp(name, CenteringSpeedNP.name)) { LOGF_DEBUG("Trying to set centering speed of: %f\n", values[0]); if (!isSimulation() && !setGeminiProperty(CENTERING_SPEED_ID, valueString)) { CenteringSpeedNP.s = IPS_ALERT; IDSetNumber(&CenteringSpeedNP, "Error setting centering speed"); return false; } CenteringSpeedNP.s = IPS_OK; CenteringSpeedN[0].value = values[0]; IDSetNumber(&CenteringSpeedNP, "Centering speed set to %f", values[0]); return true; } } // If we didn't process it, continue up the chain, let somebody else give it a shot return LX200Generic::ISNewNumber(dev, name, values, names, n); } bool LX200Gemini::checkConnection() { if (isSimulation()) return true; // Response char response[8] = { 0 }; int rc = 0, nbytes_read = 0, nbytes_written = 0; LOGF_DEBUG("CMD: <%#02X>", 0x06); tcflush(PortFD, TCIFLUSH); char ack[1] = { 0x06 }; if ((rc = tty_write(PortFD, ack, 1, &nbytes_written)) != TTY_OK) { char errmsg[256]; tty_error_msg(rc, errmsg, 256); LOGF_ERROR("Error writing to device %s (%d)", errmsg, rc); return false; } // Read response if ((rc = tty_read_section(PortFD, response, '#', GEMINI_TIMEOUT, &nbytes_read)) != TTY_OK) { char errmsg[256]; tty_error_msg(rc, errmsg, 256); LOGF_ERROR("Error reading from device %s (%d)", errmsg, rc); return false; } //response[1] = '\0'; tcflush(PortFD, TCIFLUSH); LOGF_DEBUG("RES: <%s>", response); // If waiting for selection of startup mode, let us select it if (response[0] == 'b') { LOG_DEBUG("Mount is waiting for selection of the startup mode."); char cmd[4] = "bC#"; int startupMode = IUFindOnSwitchIndex(&StartupModeSP); if (startupMode == WARM_START) strncpy(cmd, "bW#", 4); else if (startupMode == WARM_RESTART) strncpy(cmd, "bR#", 4); LOGF_DEBUG("CMD: <%s>", cmd); if ((rc = tty_write(PortFD, cmd, 4, &nbytes_written)) != TTY_OK) { char errmsg[256]; tty_error_msg(rc, errmsg, 256); LOGF_ERROR("Error writing to device %s (%d)", errmsg, rc); return false; } tcflush(PortFD, TCIFLUSH); // Send ack again and check response return checkConnection(); } else if (response[0] == 'B') { LOG_DEBUG("Initial startup message is being displayed."); } else if (response[0] == 'S') { LOG_DEBUG("Cold start in progress."); } else if (response[0] == 'G') { updateMovementState(); LOG_DEBUG("Startup complete with equatorial mount selected."); } else if (response[0] == 'A') { LOG_DEBUG("Startup complete with Alt-Az mount selected."); } return true; } bool LX200Gemini::isSlewComplete() { LX200Gemini::MovementState movementState = getMovementState(); if (movementState == TRACKING || movementState == GUIDING || movementState == NO_MOVEMENT) return true; else return false; } bool LX200Gemini::ReadScopeStatus() { if (!isConnected()) return false; if (isSimulation()) return LX200Generic::ReadScopeStatus(); updateMovementState(); if (TrackState == SCOPE_SLEWING) { // Check if LX200 is done slewing if (isSlewComplete()) { // Set slew mode to "Centering" IUResetSwitch(&SlewRateSP); SlewRateS[SLEW_CENTERING].s = ISS_ON; IDSetSwitch(&SlewRateSP, nullptr); TrackState = SCOPE_TRACKING; LOG_INFO("Slew is complete. Tracking..."); } } else if (TrackState == SCOPE_PARKING) { if (isSlewComplete()) { SetParked(true); sleepMount(); } } if (getLX200RA(PortFD, ¤tRA) < 0 || getLX200DEC(PortFD, ¤tDEC) < 0) { EqNP.s = IPS_ALERT; IDSetNumber(&EqNP, "Error reading RA/DEC."); return false; } NewRaDec(currentRA, currentDEC); syncSideOfPier(); return true; } void LX200Gemini::syncSideOfPier() { // Send ':Gm#' const char *cmd = ":Gm#"; // Response char response[8] = { 0 }; int rc = 0, nbytes_read = 0, nbytes_written = 0; LOGF_DEBUG("CMD: <%s>", cmd); tcflush(PortFD, TCIFLUSH); if ((rc = tty_write(PortFD, cmd, 5, &nbytes_written)) != TTY_OK) { char errmsg[256]; tty_error_msg(rc, errmsg, 256); LOGF_ERROR("Error writing to device %s (%d)", errmsg, rc); return; } if ((rc = tty_read_section(PortFD, response, '#', GEMINI_TIMEOUT, &nbytes_read)) != TTY_OK) { char errmsg[256]; tty_error_msg(rc, errmsg, 256); LOGF_ERROR("Error reading from device %s (%d)", errmsg, rc); return; } response[nbytes_read - 1] = '\0'; tcflush(PortFD, TCIFLUSH); LOGF_DEBUG("RES: <%s>", response); setPierSide(response[0] == 'E' ? INDI::Telescope::PIER_EAST : INDI::Telescope::PIER_WEST); } bool LX200Gemini::Park() { char cmd[6] = ":hP#"; int parkSetting = IUFindOnSwitchIndex(&ParkSettingsSP); if (parkSetting == PARK_STARTUP) strncpy(cmd, ":hC#", 5); else if (parkSetting == PARK_ZENITH) strncpy(cmd, ":hZ#", 5); // Response int rc = 0, nbytes_written = 0; LOGF_DEBUG("CMD: <%s>", cmd); tcflush(PortFD, TCIFLUSH); if ((rc = tty_write(PortFD, cmd, 5, &nbytes_written)) != TTY_OK) { char errmsg[256]; tty_error_msg(rc, errmsg, 256); LOGF_ERROR("Error writing to device %s (%d)", errmsg, rc); return false; } tcflush(PortFD, TCIFLUSH); ParkSP.s = IPS_BUSY; TrackState = SCOPE_PARKING; return true; } bool LX200Gemini::UnPark() { wakeupMount(); SetParked(false); TrackState = SCOPE_TRACKING; return true; } bool LX200Gemini::sleepMount() { const char *cmd = ":hN#"; // Response int rc = 0, nbytes_written = 0; LOGF_DEBUG("CMD: <%s>", cmd); tcflush(PortFD, TCIFLUSH); if ((rc = tty_write(PortFD, cmd, 5, &nbytes_written)) != TTY_OK) { char errmsg[256]; tty_error_msg(rc, errmsg, 256); LOGF_ERROR("Error writing to device %s (%d)", errmsg, rc); return false; } tcflush(PortFD, TCIFLUSH); LOG_INFO("Mount is sleeping..."); return true; } bool LX200Gemini::wakeupMount() { const char *cmd = ":hW#"; // Response int rc = 0, nbytes_written = 0; LOGF_DEBUG("CMD: <%s>", cmd); tcflush(PortFD, TCIFLUSH); if ((rc = tty_write(PortFD, cmd, 5, &nbytes_written)) != TTY_OK) { char errmsg[256]; tty_error_msg(rc, errmsg, 256); LOGF_ERROR("Error writing to device %s (%d)", errmsg, rc); return false; } tcflush(PortFD, TCIFLUSH); LOG_INFO("Mount is awake..."); return true; } void LX200Gemini::setTrackState(INDI::Telescope::TelescopeStatus state) { if (TrackState != state) TrackState = state; } void LX200Gemini::updateMovementState() { LX200Gemini::ParkingState parkingState = getParkingState(); if (parkingState != priorParkingState) { if (parkingState == PARKED) SetParked(true); else if (parkingState == NOT_PARKED) SetParked(false); } priorParkingState = parkingState; LX200Gemini::MovementState movementState = getMovementState(); switch (movementState) { case NO_MOVEMENT: if (parkingState == PARKED) setTrackState(SCOPE_PARKED); else setTrackState(SCOPE_IDLE); break; case TRACKING: case GUIDING: setTrackState(SCOPE_TRACKING); break; case CENTERING: case SLEWING: setTrackState(SCOPE_SLEWING); break; case STALLED: setTrackState(SCOPE_IDLE); break; } } LX200Gemini::MovementState LX200Gemini::getMovementState() { const char *cmd = ":Gv#"; char response[2] = { 0 }; int rc = 0, nbytes_read = 0, nbytes_written = 0; LOGF_DEBUG("CMD: <%s>", cmd); tcflush(PortFD, TCIFLUSH); if ((rc = tty_write(PortFD, cmd, 5, &nbytes_written)) != TTY_OK) { char errmsg[256]; tty_error_msg(rc, errmsg, 256); LOGF_ERROR("Error writing to device %s (%d)", errmsg, rc); return LX200Gemini::MovementState::NO_MOVEMENT; } if ((rc = tty_read(PortFD, response, 1, GEMINI_TIMEOUT, &nbytes_read)) != TTY_OK) { char errmsg[256]; tty_error_msg(rc, errmsg, 256); LOGF_ERROR("Error reading from device %s (%d)", errmsg, rc); return LX200Gemini::MovementState::NO_MOVEMENT; } response[1] = '\0'; tcflush(PortFD, TCIFLUSH); LOGF_DEBUG("RES: <%s>", response); switch (response[0]) { case 'N': return LX200Gemini::MovementState::NO_MOVEMENT; case 'T': return LX200Gemini::MovementState::TRACKING; case 'G': return LX200Gemini::MovementState::GUIDING; case 'C': return LX200Gemini::MovementState::CENTERING; case 'S': return LX200Gemini::MovementState::SLEWING; case '!': return LX200Gemini::MovementState::STALLED; default: return LX200Gemini::MovementState::NO_MOVEMENT; } } LX200Gemini::ParkingState LX200Gemini::getParkingState() { const char *cmd = ":h?#"; char response[2] = { 0 }; int rc = 0, nbytes_read = 0, nbytes_written = 0; LOGF_DEBUG("CMD: <%s>", cmd); tcflush(PortFD, TCIFLUSH); if ((rc = tty_write(PortFD, cmd, 5, &nbytes_written)) != TTY_OK) { char errmsg[256]; tty_error_msg(rc, errmsg, 256); LOGF_ERROR("Error writing to device %s (%d)", errmsg, rc); return LX200Gemini::ParkingState::NOT_PARKED; } if ((rc = tty_read(PortFD, response, 1, GEMINI_TIMEOUT, &nbytes_read)) != TTY_OK) { char errmsg[256]; tty_error_msg(rc, errmsg, 256); LOGF_ERROR("Error reading from device %s (%d)", errmsg, rc); return LX200Gemini::ParkingState::NOT_PARKED; } response[1] = '\0'; tcflush(PortFD, TCIFLUSH); LOGF_DEBUG("RES: <%s>", response); switch (response[0]) { case '0': return LX200Gemini::ParkingState::NOT_PARKED; case '1': return LX200Gemini::ParkingState::PARKED; case '2': return LX200Gemini::ParkingState::PARK_IN_PROGRESS; default: return LX200Gemini::ParkingState::NOT_PARKED; } } bool LX200Gemini::saveConfigItems(FILE *fp) { LX200Generic::saveConfigItems(fp); IUSaveConfigSwitch(fp, &StartupModeSP); IUSaveConfigSwitch(fp, &ParkSettingsSP); return true; } bool LX200Gemini::getGeminiProperty(uint8_t propertyNumber, char* value) { int rc = TTY_OK; int nbytes = 0; char prefix[16] = {0}; char cmd[16] = {0}; snprintf(prefix, 16, "<%d:", propertyNumber); uint8_t checksum = calculateChecksum(prefix); snprintf(cmd, 16, "%s%c#", prefix, checksum); LOGF_DEBUG("CMD: <%s>", cmd); if ((rc = tty_write_string(PortFD, cmd, &nbytes)) != TTY_OK) { char errmsg[256]; tty_error_msg(rc, errmsg, 256); LOGF_ERROR("Error writing to device %s (%d)", errmsg, rc); return false; } if ((rc = tty_read_section(PortFD, value, '#', GEMINI_TIMEOUT, &nbytes)) != TTY_OK) { char errmsg[256]; tty_error_msg(rc, errmsg, 256); LOGF_ERROR("Error reading from device %s (%d)", errmsg, rc); return false; } value[nbytes - 1] = '\0'; tcflush(PortFD, TCIFLUSH); LOGF_DEBUG("RES: <%s>", value); return true; } bool LX200Gemini::setGeminiProperty(uint8_t propertyNumber, char* value) { int rc = TTY_OK; int nbytes_written=0; char prefix[16] = {0}; char cmd[16] = {0}; snprintf(prefix, 16, ">%d:%s", propertyNumber, value); uint8_t checksum = calculateChecksum(prefix); snprintf(cmd, 16, "%s%c#", prefix, checksum); LOGF_DEBUG("CMD: <%s>", cmd); if ((rc = tty_write_string(PortFD, cmd, &nbytes_written)) != TTY_OK) { char errmsg[256]; tty_error_msg(rc, errmsg, 256); LOGF_ERROR("Error writing to device %s (%d)", errmsg, rc); return false; } tcflush(PortFD, TCIFLUSH); return true; } bool LX200Gemini::SetTrackMode(uint8_t mode) { int rc = TTY_OK, nbytes_written=0; char prefix[16] = {0}; char cmd[16] = {0}; snprintf(prefix, 16, ">130:%d", mode + 131); uint8_t checksum = calculateChecksum(prefix); snprintf(cmd, 16, "%s%c#", prefix, checksum); LOG_ERROR("Setting track mode"); LOGF_DEBUG("CMD: <%s>", cmd); if ((rc = tty_write_string(PortFD, cmd, &nbytes_written)) != TTY_OK) { char errmsg[256]; tty_error_msg(rc, errmsg, 256); LOGF_ERROR("Error writing to device %s (%d)", errmsg, rc); return false; } tcflush(PortFD, TCIFLUSH); return true; } bool LX200Gemini::SetTrackEnabled(bool enabled) { if (enabled) { return wakeupMount(); } else { return sleepMount(); } } uint8_t LX200Gemini::calculateChecksum(char *cmd) { uint8_t result = cmd[0]; for (size_t i=1; i < strlen(cmd); i++) result = result ^ cmd[i]; result = result % 128; result += 64; return result; } libindi/drivers/telescope/telescope_script.txt0000664000175000017500000000763013263645557021251 0ustar jasemjasemSample scripts for INDI Telescope Scripting Gateway This is python scripts used to test INDI Telescope Scripting Gateway. The default folder for them is /usr/share/indi/scripts (or /usr/local/share/indi/scripts on OSX). You can use any other folder, any other script names and any other script language, just make sure, that all of them have "executable" bit set. All scripts except for status.py are called only when related driver property is set. status.py is called periodically and is supposed to create file with name submitted as parameter containing single line with 3 number: 0/1 for unparked/parked, ra and dec as floats. ---------- connect.py -------------- I #!/usr/bin/python # # Connect script for INDI Telescope Scripting Gateway # # Arguments: none # Exit code: 0 for success, 1 for failure # import sys coordinates = open('/tmp/indi-status', 'w') coordinates.truncate() coordinates.write('1 0 90') coordinates.close() sys.exit(0) ---------- disconnect.py -------------- #!/usr/bin/python # # Connect script for INDI Telescope Scripting Gateway # # Arguments: none # Exit code: 0 for success, 1 for failure # import sys sys.exit(0) ---------- park.py -------------- #!/usr/bin/python # # Park script for INDI Telescope Scripting Gateway # # Arguments: none # Exit code: 0 for success, 1 for failure # import sys coordinates = open('/tmp/indi-status', 'w') coordinates.truncate() coordinates.write('1 0 90') coordinates.close() sys.exit(0) ---------- unpark.py -------------- #!/usr/bin/python # # Park script for INDI Telescope Scripting Gateway # # Arguments: none # Exit code: 0 for success, 1 for failure # import sys coordinates = open('/tmp/indi-status', 'w') coordinates.truncate() coordinates.write('0 0 90') coordinates.close() sys.exit(0) ---------- sync.py -------------- #!/usr/bin/python # # Sync script for INDI Telescope Scripting Gateway # # Arguments: RA Dec # Exit code: 0 for success, 1 for failure # import sys script, ra, dec = sys.argv coordinates = open('/tmp/indi-status', 'w') coordinates.truncate() coordinates.write('0 ' + ra + ' ' + dec) coordinates.close() sys.exit(0) ---------- goto.py -------------- #!/usr/bin/python # # Goto script for INDI Telescope Scripting Gateway # # Arguments: RA Dec # Exit code: 0 for success, 1 for failure # import sys script, ra, dec = sys.argv coordinates = open('/tmp/indi-status', 'w') coordinates.truncate() coordinates.write('0 ' + ra + ' ' + dec) coordinates.close() sys.exit(0) ---------- move_north.py -------------- #!/usr/bin/python # # Move north script for INDI Telescope Scripting Gateway # # Arguments: slew rate (0-3) # Exit code: 0 for success, 1 for failure # import sys sys.exit(0) ---------- move_east.py -------------- #!/usr/bin/python # # Move east script for INDI Telescope Scripting Gateway # # Arguments: slew rate (0-3) # Exit code: 0 for success, 1 for failure # import sys sys.exit(0) ---------- move_south.py -------------- #!/usr/bin/python # # Move south script for INDI Telescope Scripting Gateway # # Arguments: slew rate (0-3) # Exit code: 0 for success, 1 for failure # import sys sys.exit(0) ---------- move_west.py -------------- #!/usr/bin/python # # Move west script for INDI Telescope Scripting Gateway # # Arguments: slew rate (0-3) # Exit code: 0 for success, 1 for failure # import sys sys.exit(0) ---------- abort.py -------------- #!/usr/bin/python # # Abort script for INDI Telescope Scripting Gateway # # Arguments: none # Exit code: 0 for success, 1 for failure # import sys sys.exit(0) ---------- status.py -------------- #!/usr/bin/python # # Status script for INDI Telescope Scripting Gateway # # Arguments: file name to save current state and coordinates (parked ra dec) # Exit code: 0 for success, 1 for failure # import sys script, path = sys.argv coordinates = open('/tmp/indi-status', 'r') status = open(path, 'w') status.truncate() status.write(coordinates.readline()) status.close() sys.exit(0) libindi/drivers/telescope/lx200_10micron.cpp0000664000175000017500000006574713263645557020237 0ustar jasemjasem/* 10micron INDI driver GM1000HPS GM2000QCI GM2000HPS GM3000HPS GM4000QCI GM4000HPS AZ2000 Mount Command Protocol 2.14.11 Copyright (C) 2017 Hans Lambermont 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 */ #include "lx200_10micron.h" #include "indicom.h" #include "lx200driver.h" #include #include #include #include #define PRODUCT_TAB "Product" #define ALIGNMENT_TAB "Alignment" #define LX200_TIMEOUT 5 /* FD timeout in seconds */ LX200_10MICRON::LX200_10MICRON() : LX200Generic() { setLX200Capability( LX200_HAS_TRACKING_FREQ | LX200_HAS_PULSE_GUIDING ); SetTelescopeCapability( TELESCOPE_CAN_GOTO | TELESCOPE_CAN_SYNC | TELESCOPE_CAN_PARK | TELESCOPE_CAN_ABORT | TELESCOPE_HAS_TIME | TELESCOPE_HAS_LOCATION | TELESCOPE_HAS_PIER_SIDE | TELESCOPE_HAS_TRACK_MODE | TELESCOPE_CAN_CONTROL_TRACK | TELESCOPE_HAS_TRACK_RATE ); setVersion(1, 0); } // Called by INDI::DefaultDevice::ISGetProperties // Note that getDriverName calls ::getDefaultName which returns LX200 Generic const char *LX200_10MICRON::getDefaultName() { return (const char *)"10micron"; } // Called by either TCP Connect or Serial Port Connect bool LX200_10MICRON::Handshake() { fd = PortFD; if (isSimulation() == true) { LOG_INFO("Simulate Connect."); return true; } // Set Ultra Precision Mode #:U2# , replies like 15:58:19.49 instead of 15:21.2 LOG_INFO("Setting Ultra Precision Mode."); if (setCommandInt(fd, 2, "#:U") < 0) { LOG_ERROR("Failed to set Ultra Precision Mode."); return false; } return true; } // Called by ISGetProperties to initialize basic properties that are required all the time bool LX200_10MICRON::initProperties() { const bool result = LX200Generic::initProperties(); // TODO initialize properties additional to INDI::Telescope IUFillNumber(&RefractionModelTemperatureN[0], "TEMPERATURE", "Celsius", "%+6.1f", -999.9, 999.9, 0, 0.); IUFillNumberVector(&RefractionModelTemperatureNP, RefractionModelTemperatureN, 1, getDeviceName(), "REFRACTION_MODEL_TEMPERATURE", "Temperature", ALIGNMENT_TAB, IP_RW, 60, IPS_IDLE); IUFillNumber(&RefractionModelPressureN[0], "PRESSURE", "hPa", "%6.1f", 0.0, 9999.9, 0, 0.); IUFillNumberVector(&RefractionModelPressureNP, RefractionModelPressureN, 1, getDeviceName(), "REFRACTION_MODEL_PRESSURE", "Pressure", ALIGNMENT_TAB, IP_RW, 60, IPS_IDLE); IUFillNumber(&ModelCountN[0], "COUNT", "#", "%.0f", 0, 999, 0, 0); IUFillNumberVector(&ModelCountNP, ModelCountN, 1, getDeviceName(), "MODEL_COUNT", "Models", ALIGNMENT_TAB, IP_RO, 60, IPS_IDLE); IUFillNumber(&AlignmentPointsN[0], "COUNT", "#", "%.0f", 0, 100, 0, 0); IUFillNumberVector(&AlignmentPointsNP, AlignmentPointsN, 1, getDeviceName(), "ALIGNMENT_POINTS", "Points", ALIGNMENT_TAB, IP_RO, 60, IPS_IDLE); IUFillSwitch(&AlignmentStateS[ALIGN_IDLE], "Idle", "Idle", ISS_ON); IUFillSwitch(&AlignmentStateS[ALIGN_START], "Start", "Start new model", ISS_OFF); IUFillSwitch(&AlignmentStateS[ALIGN_END], "End", "End new model", ISS_OFF); IUFillSwitch(&AlignmentStateS[ALIGN_DELETE_CURRENT], "Del", "Delete current model", ISS_OFF); IUFillSwitchVector(&AlignmentSP, AlignmentStateS, ALIGN_COUNT, getDeviceName(), "Alignment", "Alignment", ALIGNMENT_TAB, IP_RW, ISR_1OFMANY, 60, IPS_IDLE); IUFillNumber(&MiniNewAlpRON[MALPRO_MRA], "MRA", "Mount RA (hh:mm:ss)", "%010.6m", 0, 24, 0, 0); IUFillNumber(&MiniNewAlpRON[MALPRO_MDEC], "MDEC", "Mount DEC (dd:mm:ss)", "%010.6m", -90, 90, 0, 0); IUFillNumber(&MiniNewAlpRON[MALPRO_MSIDE], "MSIDE", "Pier Side (0=E 1=W)", "%.0f", 0, 1, 0, 0); IUFillNumber(&MiniNewAlpRON[MALPRO_SIDTIME], "SIDTIME", "Sidereal Time (hh:mm:ss)", "%010.6m", 0, 24, 0, 0); IUFillNumberVector(&MiniNewAlpRONP, MiniNewAlpRON, MALPRO_COUNT, getDeviceName(), "MINIMAL_NEW_ALIGNMENT_POINT_RO", "Actual", ALIGNMENT_TAB, IP_RO, 60, IPS_IDLE); IUFillNumber(&MiniNewAlpN[MALP_PRA], "PRA", "Solved RA (hh:mm:ss)", "%010.6m", 0, 24, 0, 0); IUFillNumber(&MiniNewAlpN[MALP_PDEC], "PDEC", "Solved DEC (dd:mm:ss)", "%010.6m", -90, 90, 0, 0); IUFillNumberVector(&MiniNewAlpNP, MiniNewAlpN, MALP_COUNT, getDeviceName(), "MINIMAL_NEW_ALIGNMENT_POINT", "New Point", ALIGNMENT_TAB, IP_RW, 60, IPS_IDLE); IUFillNumber(&NewAlpN[ALP_MRA], "MRA", "Mount RA (hh:mm:ss)", "%010.6m", 0, 24, 0, 0); IUFillNumber(&NewAlpN[ALP_MDEC], "MDEC", "Mount DEC (dd:mm:ss)", "%010.6m", -90, 90, 0, 0); IUFillNumber(&NewAlpN[ALP_MSIDE], "MSIDE", "Pier Side (0=E 1=W)", "%.0f", 0, 1, 0, 0); IUFillNumber(&NewAlpN[ALP_SIDTIME], "SIDTIME", "Sidereal Time (hh:mm:ss)", "%010.6m", 0, 24, 0, 0); IUFillNumber(&NewAlpN[ALP_PRA], "PRA", "Solved RA (hh:mm:ss)", "%010.6m", 0, 24, 0, 0); IUFillNumber(&NewAlpN[ALP_PDEC], "PDEC", "Solved DEC (dd:mm:ss)", "%010.6m", -90, 90, 0, 0); IUFillNumberVector(&NewAlpNP, NewAlpN, ALP_COUNT, getDeviceName(), "NEW_ALIGNMENT_POINT", "New Point", ALIGNMENT_TAB, IP_RW, 60, IPS_IDLE); IUFillNumber(&NewAlignmentPointsN[0], "COUNT", "#", "%.0f", 0, 100, 1, 0); IUFillNumberVector(&NewAlignmentPointsNP, NewAlignmentPointsN, 1, getDeviceName(), "NEW_ALIGNMENT_POINTS", "New Points", ALIGNMENT_TAB, IP_RO, 60, IPS_IDLE); IUFillText(&NewModelNameT[0], "NAME", "Model Name", "newmodel"); IUFillTextVector(&NewModelNameTP, NewModelNameT, 1, getDeviceName(), "NEW_MODEL_NAME", "New Name", ALIGNMENT_TAB, IP_RW, 60, IPS_IDLE); return result; } // this should move to some generic library int LX200_10MICRON::monthToNumber(const char *monthName) { struct entry { const char *name; int id; }; entry month_table[] = { { "Jan", 1 }, { "Feb", 2 }, { "Mar", 3 }, { "Apr", 4 }, { "May", 5 }, { "Jun", 6 }, { "Jul", 7 }, { "Aug", 8 }, { "Sep", 9 }, { "Oct", 10 }, { "Nov", 11 }, { "Dec", 12 }, { nullptr, 0 } }; entry *p = month_table; while (p->name != nullptr) { if (strcasecmp(p->name, monthName) == 0) return p->id; ++p; } return 0; } // Called by INDI::Telescope when connected state changes to add/remove properties bool LX200_10MICRON::updateProperties() { if (isConnected()) { // getMountInfo defines ProductTP defineNumber(&RefractionModelTemperatureNP); defineNumber(&RefractionModelPressureNP); defineNumber(&ModelCountNP); defineNumber(&AlignmentPointsNP); defineSwitch(&AlignmentSP); defineNumber(&MiniNewAlpRONP); defineNumber(&MiniNewAlpNP); defineNumber(&NewAlpNP); defineNumber(&NewAlignmentPointsNP); defineText(&NewModelNameTP); } else { deleteProperty(ProductTP.name); deleteProperty(RefractionModelTemperatureNP.name); deleteProperty(RefractionModelPressureNP.name); deleteProperty(ModelCountNP.name); deleteProperty(AlignmentPointsNP.name); deleteProperty(AlignmentSP.name); deleteProperty(MiniNewAlpRONP.name); deleteProperty(MiniNewAlpNP.name); deleteProperty(NewAlpNP.name); deleteProperty(NewAlignmentPointsNP.name); deleteProperty(NewModelNameTP.name); } bool result = LX200Generic::updateProperties(); return result; } // INDI::Telescope calls ReadScopeStatus() every POLLMS to check the link to the telescope and update its state and position. // The child class should call newRaDec() whenever a new value is read from the telescope. bool LX200_10MICRON::ReadScopeStatus() { if (!isConnected()) { return false; } if (isSimulation()) { mountSim(); return true; } // Read scope status, based loosely on LX200_GENERIC::getCommandString char cmd[] = "#:Ginfo#"; char data[80]; char *term; int error_type; int nbytes_write = 0, nbytes_read = 0; // DEBUGFDEVICE(getDefaultName(), DBG_SCOPE, "CMD <%s>", cmd); if ((error_type = tty_write_string(fd, cmd, &nbytes_write)) != TTY_OK) { return false; } error_type = tty_read_section(fd, data, '#', LX200_TIMEOUT, &nbytes_read); tcflush(fd, TCIFLUSH); if (error_type != TTY_OK) { return false; } term = strchr(data, '#'); if (term) { *(term + 1) = '\0'; } else { return false; } DEBUGFDEVICE(getDefaultName(), DBG_SCOPE, "CMD <%s> RES <%s>", cmd, data); // Now parse the data. This format may consist of more parts some day nbytes_read = sscanf(data, "%g,%g,%c,%g,%g,%g,%d,%d#", &Ginfo.RA_JNOW, &Ginfo.DEC_JNOW, &Ginfo.SideOfPier, &Ginfo.AZ, &Ginfo.ALT, &Ginfo.Jdate, &Ginfo.Gstat, &Ginfo.SlewStatus); if (nbytes_read < 0) { return false; } if (Ginfo.Gstat != OldGstat) { if (OldGstat != GSTAT_UNSET) { LOGF_INFO("Gstat changed from %d to %d", OldGstat, Ginfo.Gstat); } else { LOGF_INFO("Gstat initialized at %d", Ginfo.Gstat); } } switch (Ginfo.Gstat) { case GSTAT_TRACKING: TrackState = SCOPE_TRACKING; break; case GSTAT_STOPPED: TrackState = SCOPE_IDLE; break; case GSTAT_PARKING: TrackState = SCOPE_PARKING; break; case GSTAT_UNPARKING: TrackState = SCOPE_TRACKING; break; case GSTAT_SLEWING_TO_HOME: TrackState = SCOPE_SLEWING; break; case GSTAT_PARKED: TrackState = SCOPE_PARKED; if (!isParked()) SetParked(true); break; case GSTAT_SLEWING_OR_STOPPING: TrackState = SCOPE_SLEWING; break; case GSTAT_NOT_TRACKING_AND_NOT_MOVING: TrackState = SCOPE_IDLE; break; case GSTAT_MOTORS_TOO_COLD: TrackState = SCOPE_IDLE; break; case GSTAT_TRACKING_OUTSIDE_LIMITS: TrackState = SCOPE_TRACKING; break; case GSTAT_FOLLOWING_SATELLITE: TrackState = SCOPE_TRACKING; break; case GSTAT_NEED_USEROK: TrackState = SCOPE_IDLE; break; case GSTAT_UNKNOWN_STATUS: TrackState = SCOPE_IDLE; break; case GSTAT_ERROR: TrackState = SCOPE_IDLE; break; default: return false; } setPierSide(toupper(Ginfo.SideOfPier) ? INDI::Telescope::PIER_EAST : INDI::Telescope::PIER_WEST); OldGstat = Ginfo.Gstat; NewRaDec(Ginfo.RA_JNOW, Ginfo.DEC_JNOW); // Update alignment Mini new alignment point Read-Only fields char LocalSiderealTimeS[80]; getCommandString(fd, LocalSiderealTimeS, "#:GS#"); f_scansexa(LocalSiderealTimeS, &Ginfo.SiderealTime); MiniNewAlpRON[MALPRO_MRA].value = Ginfo.RA_JNOW; MiniNewAlpRON[MALPRO_MDEC].value = Ginfo.DEC_JNOW; MiniNewAlpRON[MALPRO_MSIDE].value = (toupper(Ginfo.SideOfPier) == 'E') ? 0 : 1; MiniNewAlpRON[MALPRO_SIDTIME].value = Ginfo.SiderealTime; IDSetNumber(&MiniNewAlpRONP, nullptr); return true; } // Called by LX200Generic::updateProperties void LX200_10MICRON::getBasicData() { DEBUGFDEVICE(getDefaultName(), DBG_SCOPE, "<%s>", __FUNCTION__); // cannot call LX200Generic::getBasicData(); as getTimeFormat :Gc# (and getSiteName :GM#) are not implemented on 10Micron if (!isSimulation()) { getMountInfo(); getAlignment(); checkLX200Format(fd); timeFormat = LX200_24; if (getTrackFreq(PortFD, &TrackFreqN[0].value) < 0) { LOG_WARN("Failed to get tracking frequency from device."); } else { LOGF_INFO("Tracking frequency is %.1f Hz", TrackFreqN[0].value); IDSetNumber(&TrackingFreqNP, nullptr); } char RefractionModelTemperature[80]; getCommandString(PortFD, RefractionModelTemperature, "#:GRTMP#"); float rmtemp; sscanf(RefractionModelTemperature, "%f#", &rmtemp); RefractionModelTemperatureN[0].value = (double) rmtemp; LOGF_INFO("RefractionModelTemperature is %0+6.1f degrees C", RefractionModelTemperatureN[0].value); IDSetNumber(&RefractionModelTemperatureNP, nullptr); char RefractionModelPressure[80]; getCommandString(PortFD, RefractionModelPressure, "#:GRPRS#"); float rmpres; sscanf(RefractionModelPressure, "%f#", &rmpres); RefractionModelPressureN[0].value = (double) rmpres; LOGF_INFO("RefractionModelPressure is %06.1f hPa", RefractionModelPressureN[0].value); IDSetNumber(&RefractionModelPressureNP, nullptr); int ModelCount; getCommandInt(PortFD, &ModelCount, "#:modelcnt#"); ModelCountN[0].value = (double) ModelCount; LOGF_INFO("%d Alignment Models", (int) ModelCountN[0].value); IDSetNumber(&ModelCountNP, nullptr); int AlignmentPoints; getCommandInt(PortFD, &AlignmentPoints, "#:getalst#"); AlignmentPointsN[0].value = (double) AlignmentPoints; LOGF_INFO("%d Alignment Stars in active model", (int) AlignmentPointsN[0].value); IDSetNumber(&AlignmentPointsNP, nullptr); } sendScopeLocation(); sendScopeTime(); } // Called by getBasicData bool LX200_10MICRON::getMountInfo() { char ProductName[80]; getCommandString(PortFD, ProductName, "#:GVP#"); char ControlBox[80]; getCommandString(PortFD, ControlBox, "#:GVZ#"); char FirmwareVersion[80]; getCommandString(PortFD, FirmwareVersion, "#:GVN#"); char FirmwareDate1[80]; getCommandString(PortFD, FirmwareDate1, "#:GVD#"); char FirmwareDate2[80]; char mon[4]; int dd, yyyy; sscanf(FirmwareDate1, "%s %02d %04d", mon, &dd, &yyyy); getCommandString(PortFD, FirmwareDate2, "#:GVT#"); char FirmwareDate[80]; snprintf(FirmwareDate, 80, "%04d-%02d-%02dT%s", yyyy, monthToNumber(mon), dd, FirmwareDate2); LOGF_INFO("Product:%s Control box:%s Firmware:%s of %s", ProductName, ControlBox, FirmwareVersion, FirmwareDate); IUFillText(&ProductT[PRODUCT_NAME], "NAME", "Product Name", ProductName); IUFillText(&ProductT[PRODUCT_CONTROL_BOX], "CONTROL_BOX", "Control Box", ControlBox); IUFillText(&ProductT[PRODUCT_FIRMWARE_VERSION], "FIRMWARE_VERSION", "Firmware Version", FirmwareVersion); IUFillText(&ProductT[PRODUCT_FIRMWARE_DATE], "FIRMWARE_DATE", "Firmware Date", FirmwareDate); IUFillTextVector(&ProductTP, ProductT, PRODUCT_COUNT, getDeviceName(), "PRODUCT_INFO", "Product", PRODUCT_TAB, IP_RO, 60, IPS_IDLE); defineText(&ProductTP); return true; } // this should move to some generic library int LX200_10MICRON::setStandardProcedureWithoutRead(int fd, const char *data) { int error_type; int nbytes_write = 0; DEBUGFDEVICE(getDefaultName(), DBG_SCOPE, "CMD <%s>", data); if ((error_type = tty_write_string(fd, data, &nbytes_write)) != TTY_OK) { return error_type; } tcflush(fd, TCIFLUSH); return 0; } int LX200_10MICRON::setStandardProcedureAndExpect(int fd, const char *data, const char *expect) { char bool_return[2]; int error_type; int nbytes_write = 0, nbytes_read = 0; DEBUGFDEVICE(getDefaultName(), DBG_SCOPE, "CMD <%s>", data); tcflush(fd, TCIFLUSH); if ((error_type = tty_write_string(fd, data, &nbytes_write)) != TTY_OK) return error_type; error_type = tty_read(fd, bool_return, 1, LX200_TIMEOUT, &nbytes_read); tcflush(fd, TCIFLUSH); if (nbytes_read < 1) return error_type; if (bool_return[0] != expect[0]) { DEBUGFDEVICE(getDefaultName(), DBG_SCOPE, "CMD <%s> failed.", data); return -1; } DEBUGFDEVICE(getDefaultName(), DBG_SCOPE, "CMD <%s> successful.", data); return 0; } int LX200_10MICRON::setStandardProcedureAndReturnResponse(int fd, const char *data, char *response, int max_response_length) { int error_type; int nbytes_write = 0, nbytes_read = 0; DEBUGFDEVICE(getDefaultName(), DBG_SCOPE, "CMD <%s>", data); tcflush(fd, TCIFLUSH); if ((error_type = tty_write_string(fd, data, &nbytes_write)) != TTY_OK) return error_type; error_type = tty_read(fd, response, max_response_length, LX200_TIMEOUT, &nbytes_read); tcflush(fd, TCIFLUSH); if (nbytes_read < 1) return error_type; return 0; } bool LX200_10MICRON::Park() { LOG_INFO("Parking."); if (setStandardProcedureWithoutRead(fd, "#:KA#") < 0) { return false; } return true; } bool LX200_10MICRON::UnPark() { LOG_INFO("Unparking."); if (setStandardProcedureWithoutRead(fd, "#:PO#") < 0) { return false; } SetParked(false); return true; } bool LX200_10MICRON::SyncConfigBehaviour(bool cmcfg) { LOG_INFO("SyncConfig."); if (setCommandInt(fd, cmcfg, "#:CMCFG") < 0) { return false; } return true; } bool LX200_10MICRON::setLocalDate(uint8_t days, uint8_t months, uint16_t years) { DEBUGFDEVICE(getDefaultName(), DBG_SCOPE, "<%s>", __FUNCTION__); char data[64]; snprintf(data, sizeof(data), ":SC%04d-%02d-%02d#", years, months, days); return 0 == setStandardProcedureAndExpect(fd, data, "1"); } int LX200_10MICRON::SetRefractionModelTemperature(double temperature) { char data[16]; snprintf(data, 16, "#:SRTMP%0+6.1f#", temperature); return setStandardProcedure(fd, data); } int LX200_10MICRON::SetRefractionModelPressure(double pressure) { char data[16]; snprintf(data, 16, "#:SRPRS%06.1f#", pressure); return setStandardProcedure(fd, data); } int LX200_10MICRON::AddSyncPoint(double MRa, double MDec, double MSide, double PRa, double PDec, double SidTime) { char MRa_str[32], MDec_str[32]; fs_sexa(MRa_str, MRa, 0, 36000); fs_sexa(MDec_str, MDec, 0, 3600); char MSide_char; ((int)MSide == 0) ? MSide_char = 'E' : MSide_char = 'W'; char PRa_str[32], PDec_str[32]; fs_sexa(PRa_str, PRa, 0, 36000); fs_sexa(PDec_str, PDec, 0, 3600); char SidTime_str[32]; fs_sexa(SidTime_str, SidTime, 0, 36000); char command[80]; snprintf(command, 80, "#:newalpt%s,%s,%c,%s,%s,%s#", MRa_str, MDec_str, MSide_char, PRa_str, PDec_str, SidTime_str); LOGF_INFO("AddSyncPoint %s", command); char response[6]; if (0 != setStandardProcedureAndReturnResponse(fd, command, response, 5) || response[0] == 'E') { LOG_ERROR("AddSyncPoint error"); return 1; } response[4] = 0; int points; int nbytes_read = sscanf(response, "%3d#", &points); if (nbytes_read < 0) { LOGF_ERROR("AddSyncPoint response error %d", nbytes_read); return 1; } LOGF_INFO("AddSyncPoint responded [%4s], there are now %d new alignment points", response, points); NewAlignmentPointsN[0].value = points; IDSetNumber(&NewAlignmentPointsNP, nullptr); return 0; } int LX200_10MICRON::AddSyncPointHere(double PRa, double PDec) { double MSide = (toupper(Ginfo.SideOfPier) == 'E') ? 0 : 1; return AddSyncPoint(Ginfo.RA_JNOW, Ginfo.DEC_JNOW, MSide, PRa, PDec, Ginfo.SiderealTime); } bool LX200_10MICRON::ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) { if (dev != nullptr && strcmp(dev, getDeviceName()) == 0) { if (strcmp(name, "REFRACTION_MODEL_TEMPERATURE") == 0) { IUUpdateNumber(&RefractionModelTemperatureNP, values, names, n); if (0 != SetRefractionModelTemperature(RefractionModelTemperatureN[0].value)) { LOG_ERROR("SetRefractionModelTemperature error"); RefractionModelTemperatureNP.s = IPS_ALERT; IDSetNumber(&RefractionModelTemperatureNP, nullptr); return false; } RefractionModelTemperatureNP.s = IPS_OK; IDSetNumber(&RefractionModelTemperatureNP, nullptr); LOGF_INFO("RefractionModelTemperature set to %0+6.1f degrees C", RefractionModelTemperatureN[0].value); return true; } if (strcmp(name, "REFRACTION_MODEL_PRESSURE") == 0) { IUUpdateNumber(&RefractionModelPressureNP, values, names, n); if (0 != SetRefractionModelPressure(RefractionModelPressureN[0].value)) { LOG_ERROR("SetRefractionModelPressure error"); RefractionModelPressureNP.s = IPS_ALERT; IDSetNumber(&RefractionModelPressureNP, nullptr); return false; } RefractionModelPressureNP.s = IPS_OK; IDSetNumber(&RefractionModelPressureNP, nullptr); LOGF_INFO("RefractionModelPressure set to %06.1f hPa", RefractionModelPressureN[0].value); return true; } if (strcmp(name, "MODEL_COUNT") == 0) { IUUpdateNumber(&ModelCountNP, values, names, n); ModelCountNP.s = IPS_OK; IDSetNumber(&ModelCountNP, nullptr); LOGF_INFO("ModelCount %d", ModelCountN[0].value); return true; } if (strcmp(name, "MINIMAL_NEW_ALIGNMENT_POINT_RO") == 0) { IUUpdateNumber(&MiniNewAlpNP, values, names, n); MiniNewAlpRONP.s = IPS_OK; IDSetNumber(&MiniNewAlpRONP, nullptr); return true; } if (strcmp(name, "MINIMAL_NEW_ALIGNMENT_POINT") == 0) { if (AlignmentState != ALIGN_START) { LOG_ERROR("Cannot add alignment points yet, need to start a new alignment first"); return false; } IUUpdateNumber(&MiniNewAlpNP, values, names, n); if (0 != AddSyncPointHere(MiniNewAlpN[MALP_PRA].value, MiniNewAlpN[MALP_PDEC].value)) { LOG_ERROR("AddSyncPointHere error"); MiniNewAlpNP.s = IPS_ALERT; IDSetNumber(&MiniNewAlpNP, nullptr); return false; } MiniNewAlpNP.s = IPS_OK; IDSetNumber(&MiniNewAlpNP, nullptr); return true; } if (strcmp(name, "NEW_ALIGNMENT_POINT") == 0) { if (AlignmentState != ALIGN_START) { LOG_ERROR("Cannot add alignment points yet, need to start a new alignment first"); return false; } IUUpdateNumber(&NewAlpNP, values, names, n); if (0 != AddSyncPoint(NewAlpN[ALP_MRA].value, NewAlpN[ALP_MDEC].value, NewAlpN[ALP_MSIDE].value, NewAlpN[ALP_PRA].value, NewAlpN[ALP_PDEC].value, NewAlpN[ALP_SIDTIME].value)) { LOG_ERROR("AddSyncPoint error"); NewAlpNP.s = IPS_ALERT; IDSetNumber(&NewAlpNP, nullptr); return false; } NewAlpNP.s = IPS_OK; IDSetNumber(&NewAlpNP, nullptr); return true; } if (strcmp(name, "NEW_ALIGNMENT_POINTS") == 0) { IUUpdateNumber(&NewAlignmentPointsNP, values, names, n); NewAlignmentPointsNP.s = IPS_OK; IDSetNumber(&NewAlignmentPointsNP, nullptr); LOGF_INFO("New unnamed Model now has %d alignment points", NewAlignmentPointsN[0].value); return true; } } // Let INDI::LX200Generic handle any other number properties return LX200Generic::ISNewNumber(dev, name, values, names, n); } bool LX200_10MICRON::ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) { if (dev != nullptr && strcmp(dev, getDeviceName()) == 0) { if (strcmp(AlignmentSP.name, name) == 0) { IUUpdateSwitch(&AlignmentSP, states, names, n); int index = IUFindOnSwitchIndex(&AlignmentSP); switch (index) { case ALIGN_IDLE: AlignmentState = ALIGN_IDLE; LOG_INFO("Alignment state is IDLE"); break; case ALIGN_START: if (0 != setStandardProcedureAndExpect(fd, "#:newalig#", "V")) { LOG_ERROR("New alignment start error"); AlignmentSP.s = IPS_ALERT; IDSetSwitch(&AlignmentSP, nullptr); return false; } LOG_INFO("New Alignment started"); AlignmentState = ALIGN_START; break; case ALIGN_END: if (0 != setStandardProcedureAndExpect(fd, "#:endalig#", "V")) { LOG_ERROR("New alignment end error"); AlignmentSP.s = IPS_ALERT; IDSetSwitch(&AlignmentSP, nullptr); return false; } LOG_INFO("New Alignment ended"); AlignmentState = ALIGN_END; break; case ALIGN_DELETE_CURRENT: if (0 != setStandardProcedureAndExpect(fd, "#:delalig#", "#")) { LOG_ERROR("Delete current alignment error"); AlignmentSP.s = IPS_ALERT; IDSetSwitch(&AlignmentSP, nullptr); return false; } LOG_INFO("Current Alignment deleted"); AlignmentState = ALIGN_DELETE_CURRENT; break; default: AlignmentSP.s = IPS_ALERT; IDSetSwitch(&AlignmentSP, "Unknown alignment index %d", index); AlignmentState = ALIGN_IDLE; return false; } AlignmentSP.s = IPS_OK; IDSetSwitch(&AlignmentSP, nullptr); return true; } } return LX200Generic::ISNewSwitch(dev, name, states, names, n); } bool LX200_10MICRON::ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n) { if (dev != nullptr && strcmp(dev, getDeviceName()) == 0) { if (strcmp(name, "NEW_MODEL_NAME") == 0) { IUUpdateText(&NewModelNameTP, texts, names, n); NewModelNameTP.s = IPS_OK; IDSetText(&NewModelNameTP, nullptr); LOGF_INFO("Model saved with name %s", NewModelNameT[0].text); return true; } } return LX200Generic::ISNewText(dev, name, texts, names, n); } libindi/drivers/telescope/lx200apdriver.cpp0000664000175000017500000005372013263645557020250 0ustar jasemjasem#if 0 LX200 Astro - Physics Driver Copyright (C) 2007 Markus Wildi 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 #include "lx200apdriver.h" #include "indicom.h" #include "indilogger.h" #include "lx200driver.h" #include #include #ifndef _WIN32 #include #endif #define LX200_TIMEOUT 5 /* FD timeout in seconds */ char lx200ap_name[MAXINDIDEVICE]; unsigned int AP_DBG_SCOPE; void set_lx200ap_name(const char *deviceName, unsigned int debug_level) { strncpy(lx200ap_name, deviceName, MAXINDIDEVICE); AP_DBG_SCOPE = debug_level; } int check_lx200ap_connection(int fd) { const struct timespec timeout = {0, 50000000L}; int i = 0; char temp_string[64]; int error_type; int nbytes_write = 0; int nbytes_read = 0; DEBUGDEVICE(lx200ap_name, INDI::Logger::DBG_DEBUG, "Testing telescope's connection using #:GG#..."); if (fd <= 0) { DEBUGDEVICE(lx200ap_name, INDI::Logger::DBG_ERROR, "check_lx200ap_connection: not a valid file descriptor received"); return -1; } for (i = 0; i < 2; i++) { if ((error_type = tty_write_string(fd, "#:GG#", &nbytes_write)) != TTY_OK) { DEBUGFDEVICE(lx200ap_name, INDI::Logger::DBG_ERROR, "check_lx200ap_connection: unsuccessful write to telescope, %d", nbytes_write); return error_type; } tty_read_section(fd, temp_string, '#', LX200_TIMEOUT, &nbytes_read); tcflush(fd, TCIFLUSH); if (nbytes_read > 1) { temp_string[nbytes_read - 1] = '\0'; DEBUGFDEVICE(lx200ap_name, INDI::Logger::DBG_ERROR, "check_lx200ap_connection: received bytes %d, [%s]", nbytes_write, temp_string); return 0; } nanosleep(&timeout, NULL); } DEBUGDEVICE(lx200ap_name, INDI::Logger::DBG_ERROR, "check_lx200ap_connection: wrote, but nothing received."); return -1; } int getAPUTCOffset(int fd, double *value) { int error_type; int nbytes_write = 0; int nbytes_read = 0; char temp_string[16]; DEBUGFDEVICE(lx200ap_name, AP_DBG_SCOPE, "CMD <%s>", "#:GG#"); if ((error_type = tty_write_string(fd, "#:GG#", &nbytes_write)) != TTY_OK) return error_type; if ((error_type = tty_read_section(fd, temp_string, '#', LX200_TIMEOUT, &nbytes_read)) != TTY_OK) { DEBUGFDEVICE(lx200ap_name, INDI::Logger::DBG_ERROR, "getAPUTCOffset: saying good bye %d, %d", error_type, nbytes_read); return error_type; } tcflush(fd, TCIFLUSH); DEBUGFDEVICE(lx200ap_name, AP_DBG_SCOPE, "RES <%s>", temp_string); /* Negative offsets, see AP keypad manual p. 77 */ if ((temp_string[0] == 'A') || ((temp_string[0] == '0') && (temp_string[1] == '0')) || (temp_string[0] == '@')) { int i; for (i = nbytes_read; i > 0; i--) { temp_string[i] = temp_string[i - 1]; } temp_string[0] = '-'; temp_string[nbytes_read + 1] = '\0'; if (temp_string[1] == 'A') { temp_string[1] = '0'; switch (temp_string[2]) { case '5': temp_string[2] = '1'; break; case '4': temp_string[2] = '2'; break; case '3': temp_string[2] = '3'; break; case '2': temp_string[2] = '4'; break; case '1': temp_string[2] = '5'; break; default: DEBUGFDEVICE(lx200ap_name, INDI::Logger::DBG_ERROR, "getAPUTCOffset: string not handled %s", temp_string); return -1; break; } } else if (temp_string[1] == '0') { temp_string[1] = '0'; temp_string[2] = '6'; } else if (temp_string[1] == '@') { temp_string[1] = '0'; switch (temp_string[2]) { case '9': temp_string[2] = '7'; break; case '8': temp_string[2] = '8'; break; case '7': temp_string[2] = '9'; break; case '6': temp_string[2] = '0'; break; case '5': temp_string[1] = '1'; temp_string[2] = '1'; break; case '4': temp_string[1] = '1'; temp_string[2] = '2'; break; default: DEBUGFDEVICE(lx200ap_name, INDI::Logger::DBG_ERROR, "getAPUTCOffset: string not handled %s", temp_string); return -1; break; } } else { DEBUGFDEVICE(lx200ap_name, INDI::Logger::DBG_ERROR, "getAPUTCOffset: string not handled %s", temp_string); } } else { temp_string[nbytes_read - 1] = '\0'; } if (f_scansexa(temp_string, value)) { DEBUGFDEVICE(lx200ap_name, INDI::Logger::DBG_ERROR, "getAPUTCOffset: unable to process %s", temp_string); return -1; } return 0; } int setAPObjectAZ(int fd, double az) { int h, m, s; char temp_string[16]; getSexComponents(az, &h, &m, &s); snprintf(temp_string, sizeof(temp_string), "#:Sz %03d*%02d:%02d#", h, m, s); DEBUGFDEVICE(lx200ap_name, AP_DBG_SCOPE, "CMD <%s>", temp_string); return (setStandardProcedure(fd, temp_string)); } /* wildi Valid set Values are positive, add error condition */ int setAPObjectAlt(int fd, double alt) { int d, m, s; char temp_string[16]; getSexComponents(alt, &d, &m, &s); /* case with negative zero */ if (!d && alt < 0) { snprintf(temp_string, sizeof(temp_string), "#:Sa -%02d*%02d:%02d#", d, m, s); } else { snprintf(temp_string, sizeof(temp_string), "#:Sa %+02d*%02d:%02d#", d, m, s); } DEBUGFDEVICE(lx200ap_name, AP_DBG_SCOPE, "CMD <%s>", temp_string); return (setStandardProcedure(fd, temp_string)); } int setAPUTCOffset(int fd, double hours) { int h, m, s; char temp_string[16]; getSexComponents(hours, &h, &m, &s); snprintf(temp_string, sizeof(temp_string), "#:SG %+03d:%02d:%02d#", h, m, s); DEBUGFDEVICE(lx200ap_name, AP_DBG_SCOPE, "CMD <%s>", temp_string); return (setStandardProcedure(fd, temp_string)); } int APSyncCM(int fd, char *matchedObject) { const struct timespec timeout = {0, 10000000L}; int error_type; int nbytes_write = 0; int nbytes_read = 0; DEBUGFDEVICE(lx200ap_name, AP_DBG_SCOPE, "CMD <%s>", "#:CM#"); if ((error_type = tty_write_string(fd, "#:CM#", &nbytes_write)) != TTY_OK) return error_type; if ((error_type = tty_read_section(fd, matchedObject, '#', LX200_TIMEOUT, &nbytes_read)) != TTY_OK) return error_type; matchedObject[nbytes_read - 1] = '\0'; DEBUGFDEVICE(lx200ap_name, AP_DBG_SCOPE, "RES <%s>", matchedObject); /* Sleep 10ms before flushing. This solves some issues with LX200 compatible devices. */ nanosleep(&timeout, NULL); tcflush(fd, TCIFLUSH); return 0; } int APSyncCMR(int fd, char *matchedObject) { const struct timespec timeout = {0, 10000000L}; int error_type; int nbytes_write = 0; int nbytes_read = 0; DEBUGFDEVICE(lx200ap_name, AP_DBG_SCOPE, "CMD <%s>", "#:CMR#"); if ((error_type = tty_write_string(fd, "#:CMR#", &nbytes_write)) != TTY_OK) return error_type; /* read_ret = portRead(matchedObject, -1, LX200_TIMEOUT); */ if ((error_type = tty_read_section(fd, matchedObject, '#', LX200_TIMEOUT, &nbytes_read)) != TTY_OK) return error_type; matchedObject[nbytes_read - 1] = '\0'; DEBUGFDEVICE(lx200ap_name, AP_DBG_SCOPE, "RES <%s>", matchedObject); /* Sleep 10ms before flushing. This solves some issues with LX200 compatible devices. */ nanosleep(&timeout, NULL); tcflush(fd, TCIFLUSH); return 0; } int selectAPPECState(int fd, int pecstate) { int error_type; int nbytes_write = 0; switch (pecstate) { // PEC OFF case 0: DEBUGDEVICE(lx200ap_name, INDI::Logger::DBG_DEBUG, "selectAPPECState: Setting PEC OFF"); DEBUGFDEVICE(lx200ap_name, AP_DBG_SCOPE, "CMD <%s>", "#:p#"); if ((error_type = tty_write_string(fd, "#:p#", &nbytes_write)) != TTY_OK) return error_type; break; // PEC ON case 1: DEBUGDEVICE(lx200ap_name, INDI::Logger::DBG_DEBUG, "selectAPPECState: Setting PEC ON"); DEBUGFDEVICE(lx200ap_name, AP_DBG_SCOPE, "CMD <%s>", "#:pP#"); if ((error_type = tty_write_string(fd, "#:pP#", &nbytes_write)) != TTY_OK) return error_type; break; default: return -1; break; } return 0; } int selectAPMoveToRate(int fd, int moveToRate) { int error_type; int nbytes_write = 0; switch (moveToRate) { /* 12x*/ case 0: DEBUGDEVICE(lx200ap_name, INDI::Logger::DBG_DEBUG, "selectAPMoveToRate: Setting move to rate to 12x"); DEBUGFDEVICE(lx200ap_name, AP_DBG_SCOPE, "CMD <%s>", "#:RC0#"); if ((error_type = tty_write_string(fd, "#:RC0#", &nbytes_write)) != TTY_OK) return error_type; break; /* 64x */ case 1: DEBUGDEVICE(lx200ap_name, INDI::Logger::DBG_DEBUG, "selectAPMoveToRate: Setting move to rate to 64x"); DEBUGFDEVICE(lx200ap_name, AP_DBG_SCOPE, "CMD <%s>", "#:RC1#"); if ((error_type = tty_write_string(fd, "#:RC1#", &nbytes_write)) != TTY_OK) return error_type; break; /* 600x */ case 2: DEBUGDEVICE(lx200ap_name, INDI::Logger::DBG_DEBUG, "selectAPMoveToRate: Setting move to rate to 600x"); DEBUGFDEVICE(lx200ap_name, AP_DBG_SCOPE, "CMD <%s>", "#:RC2#"); if ((error_type = tty_write_string(fd, "#:RC2#", &nbytes_write)) != TTY_OK) return error_type; break; /* 1200x */ case 3: DEBUGDEVICE(lx200ap_name, INDI::Logger::DBG_DEBUG, "selectAPMoveToRate: Setting move to rate to 1200x"); DEBUGFDEVICE(lx200ap_name, AP_DBG_SCOPE, "CMD <%s>", "#:RC3#"); if ((error_type = tty_write_string(fd, "#:RC3#", &nbytes_write)) != TTY_OK) return error_type; break; default: return -1; break; } return 0; } int selectAPSlewRate(int fd, int slewRate) { int error_type; int nbytes_write = 0; switch (slewRate) { /* 600x */ case 0: DEBUGDEVICE(lx200ap_name, INDI::Logger::DBG_DEBUG, "selectAPSlewRate: Setting slew to rate to 600x"); DEBUGFDEVICE(lx200ap_name, AP_DBG_SCOPE, "CMD <%s>", "#:RS0#"); if ((error_type = tty_write_string(fd, "#:RS0#", &nbytes_write)) != TTY_OK) return error_type; break; /* 900x */ case 1: DEBUGDEVICE(lx200ap_name, INDI::Logger::DBG_DEBUG, "selectAPSlewRate: Setting slew to rate to 900x"); DEBUGFDEVICE(lx200ap_name, AP_DBG_SCOPE, "CMD <%s>", "#:RS1#"); if ((error_type = tty_write_string(fd, "#:RS1#", &nbytes_write)) != TTY_OK) return error_type; break; /* 1200x */ case 2: DEBUGDEVICE(lx200ap_name, INDI::Logger::DBG_DEBUG, "selectAPSlewRate: Setting slew to rate to 1200x"); DEBUGFDEVICE(lx200ap_name, AP_DBG_SCOPE, "CMD <%s>", "#:RS2#"); if ((error_type = tty_write_string(fd, "#:RS2#", &nbytes_write)) != TTY_OK) return error_type; break; default: return -1; break; } return 0; } int selectAPTrackingMode(int fd, int trackMode) { int error_type; int nbytes_write = 0; switch (trackMode) { /* Sidereal */ case AP_TRACKING_SIDEREAL: DEBUGDEVICE(lx200ap_name, INDI::Logger::DBG_DEBUG, "selectAPTrackingMode: Setting tracking mode to sidereal."); DEBUGFDEVICE(lx200ap_name, AP_DBG_SCOPE, "CMD <%s>", "#:RT2#"); if ((error_type = tty_write_string(fd, "#:RT2#", &nbytes_write)) != TTY_OK) return error_type; break; /* Solar */ case AP_TRACKING_SOLAR: DEBUGDEVICE(lx200ap_name, INDI::Logger::DBG_DEBUG, "selectAPTrackingMode: Setting tracking mode to solar."); DEBUGFDEVICE(lx200ap_name, AP_DBG_SCOPE, "CMD <%s>", "#:RT1#"); if ((error_type = tty_write_string(fd, "#:RT1#", &nbytes_write)) != TTY_OK) return error_type; break; /* Lunar */ case AP_TRACKING_LUNAR: DEBUGDEVICE(lx200ap_name, INDI::Logger::DBG_DEBUG, "selectAPTrackingMode: Setting tracking mode to lunar."); DEBUGFDEVICE(lx200ap_name, AP_DBG_SCOPE, "CMD <%s>", "#:RT0#"); if ((error_type = tty_write_string(fd, "#:RT0#", &nbytes_write)) != TTY_OK) return error_type; break; case AP_TRACKING_CUSTOM: DEBUGDEVICE(lx200ap_name, INDI::Logger::DBG_DEBUG, "selectAPTrackingMode: Setting tracking mode to Custom."); break; /* Zero */ case AP_TRACKING_OFF: DEBUGDEVICE(lx200ap_name, INDI::Logger::DBG_DEBUG, "selectAPTrackingMode: Setting tracking mode to Zero."); DEBUGFDEVICE(lx200ap_name, AP_DBG_SCOPE, "CMD <%s>", "#:RT9#"); if ((error_type = tty_write_string(fd, "#:RT9#", &nbytes_write)) != TTY_OK) return error_type; break; default: return -1; break; } return 0; } int selectAPGuideRate(int fd, int guideRate) { int error_type; int nbytes_write = 0; switch (guideRate) { /* 0.25x */ case 0: DEBUGDEVICE(lx200ap_name, INDI::Logger::DBG_DEBUG, "selectAPGuideRate: Setting guide to rate to 0.25x"); DEBUGFDEVICE(lx200ap_name, AP_DBG_SCOPE, "CMD <%s>", "#:RG0#"); if ((error_type = tty_write_string(fd, "#:RG0#", &nbytes_write)) != TTY_OK) return error_type; break; /* 0.50x */ case 1: DEBUGDEVICE(lx200ap_name, INDI::Logger::DBG_DEBUG, "selectAPGuideRate: Setting guide to rate to 0.50x"); DEBUGFDEVICE(lx200ap_name, AP_DBG_SCOPE, "CMD <%s>", "#:RG1#"); if ((error_type = tty_write_string(fd, "#:RG1#", &nbytes_write)) != TTY_OK) return error_type; break; /* 1.00x */ case 2: DEBUGDEVICE(lx200ap_name, INDI::Logger::DBG_DEBUG, "selectAPGuideRate: Setting guide to rate to 1.00x"); DEBUGFDEVICE(lx200ap_name, AP_DBG_SCOPE, "CMD <%s>", "#:RG2#"); if ((error_type = tty_write_string(fd, "#:RG2#", &nbytes_write)) != TTY_OK) return error_type; break; default: return -1; break; } return 0; } int swapAPButtons(int fd, int currentSwap) { int error_type; int nbytes_write = 0; switch (currentSwap) { case 0: DEBUGFDEVICE(lx200ap_name, AP_DBG_SCOPE, "CMD <%s>", "#:NS#"); if ((error_type = tty_write_string(fd, "#:NS#", &nbytes_write)) != TTY_OK) return error_type; break; case 1: DEBUGFDEVICE(lx200ap_name, AP_DBG_SCOPE, "CMD <%s>", "#:EW#"); if ((error_type = tty_write_string(fd, "#:EW#", &nbytes_write)) != TTY_OK) return error_type; break; default: return -1; break; } return 0; } int setAPObjectRA(int fd, double ra) { /*ToDo AP accepts "#:Sr %02d:%02d:%02d.%1d#"*/ int h, m, s; char temp_string[16]; getSexComponents(ra, &h, &m, &s); snprintf(temp_string, sizeof(temp_string), "#:Sr %02d:%02d:%02d#", h, m, s); DEBUGFDEVICE(lx200ap_name, AP_DBG_SCOPE, "CMD <%s>", temp_string); return (setStandardProcedure(fd, temp_string)); } int setAPObjectDEC(int fd, double dec) { int d, m, s; char temp_string[16]; getSexComponents(dec, &d, &m, &s); /* case with negative zero */ if (!d && dec < 0) { snprintf(temp_string, sizeof(temp_string), "#:Sd -%02d*%02d:%02d#", d, m, s); } else { snprintf(temp_string, sizeof(temp_string), "#:Sd %+03d*%02d:%02d#", d, m, s); } DEBUGFDEVICE(lx200ap_name, AP_DBG_SCOPE, "CMD <%s>", temp_string); return (setStandardProcedure(fd, temp_string)); } int setAPSiteLongitude(int fd, double Long) { int d, m, s; char temp_string[32]; getSexComponents(Long, &d, &m, &s); snprintf(temp_string, sizeof(temp_string), "#:Sg %03d*%02d:%02d#", d, m, s); DEBUGFDEVICE(lx200ap_name, AP_DBG_SCOPE, "CMD <%s>", temp_string); return (setStandardProcedure(fd, temp_string)); } int setAPSiteLatitude(int fd, double Lat) { int d, m, s; char temp_string[32]; getSexComponents(Lat, &d, &m, &s); snprintf(temp_string, sizeof(temp_string), "#:St %+03d*%02d:%02d#", d, m, s); DEBUGFDEVICE(lx200ap_name, AP_DBG_SCOPE, "CMD <%s>", temp_string); return (setStandardProcedure(fd, temp_string)); } int setAPRATrackRate(int fd, double rate) { char cmd[16]; char sign; int errcode = 0; char errmsg[MAXRBUF]; char response[8]; int nbytes_read = 0; int nbytes_written = 0; if (rate < 0) sign = '-'; else sign = '+'; snprintf(cmd, 16, ":RR%c%03.4f#", sign, fabs(rate)); DEBUGFDEVICE(lx200ap_name, INDI::Logger::DBG_DEBUG, "CMD (%s)", cmd); tcflush(fd, TCIFLUSH); if ((errcode = tty_write(fd, cmd, strlen(cmd), &nbytes_written)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); DEBUGFDEVICE(lx200ap_name, INDI::Logger::DBG_ERROR, "%s", errmsg); return errcode; } if ((errcode = tty_read(fd, response, 1, LX200_TIMEOUT, &nbytes_read))) { tty_error_msg(errcode, errmsg, MAXRBUF); DEBUGFDEVICE(lx200ap_name, INDI::Logger::DBG_ERROR, "%s", errmsg); return errcode; } if (nbytes_read > 0) { response[nbytes_read] = '\0'; DEBUGFDEVICE(lx200ap_name, INDI::Logger::DBG_DEBUG, "RES (%s)", response); tcflush(fd, TCIFLUSH); return 0; } DEBUGFDEVICE(lx200ap_name, INDI::Logger::DBG_ERROR, "Only received #%d bytes, expected 1.", nbytes_read); return -1; } int setAPDETrackRate(int fd, double rate) { char cmd[16]; char sign; int errcode = 0; char errmsg[MAXRBUF]; char response[8]; int nbytes_read = 0; int nbytes_written = 0; if (rate < 0) sign = '-'; else sign = '+'; snprintf(cmd, 16, ":RD%c%03.4f#", sign, fabs(rate)); DEBUGFDEVICE(lx200ap_name, INDI::Logger::DBG_DEBUG, "CMD (%s)", cmd); tcflush(fd, TCIFLUSH); if ((errcode = tty_write(fd, cmd, strlen(cmd), &nbytes_written)) != TTY_OK) { tty_error_msg(errcode, errmsg, MAXRBUF); DEBUGFDEVICE(lx200ap_name, INDI::Logger::DBG_ERROR, "%s", errmsg); return errcode; } if ((errcode = tty_read(fd, response, 1, LX200_TIMEOUT, &nbytes_read))) { tty_error_msg(errcode, errmsg, MAXRBUF); DEBUGFDEVICE(lx200ap_name, INDI::Logger::DBG_ERROR, "%s", errmsg); return errcode; } if (nbytes_read > 0) { response[nbytes_read] = '\0'; DEBUGFDEVICE(lx200ap_name, INDI::Logger::DBG_DEBUG, "RES (%s)", response); tcflush(fd, TCIFLUSH); return 0; } DEBUGFDEVICE(lx200ap_name, INDI::Logger::DBG_ERROR, "Only received #%d bytes, expected 1.", nbytes_read); return -1; } int APSendPulseCmd(int fd, int direction, int duration_msec) { DEBUGFDEVICE(lx200ap_name, AP_DBG_SCOPE, "<%s>", __FUNCTION__); int nbytes_write = 0; char cmd[20]; switch (direction) { case LX200_NORTH: sprintf(cmd, ":Mn%04d#", duration_msec); break; case LX200_SOUTH: sprintf(cmd, ":Ms%04d#", duration_msec); break; case LX200_EAST: sprintf(cmd, ":Me%04d#", duration_msec); break; case LX200_WEST: sprintf(cmd, ":Mw%04d#", duration_msec); break; default: return 1; } DEBUGFDEVICE(lx200ap_name, AP_DBG_SCOPE, "CMD <%s>", cmd); tty_write_string(fd, cmd, &nbytes_write); tcflush(fd, TCIFLUSH); return 0; } #if 0 // experimental function!!! int check_lx200ap_status(int fd, char *parkStatus, char *slewStatus) { char temp_string[64]; int error_type; int nbytes_write = 0; int nbytes_read = 0; DEBUGDEVICE(lx200ap_name, INDI::Logger::DBG_DEBUG, "EXPERIMENTAL: check status..."); if (fd <= 0) { DEBUGDEVICE(lx200ap_name, INDI::Logger::DBG_ERROR, "check_lx200ap_connection: not a valid file descriptor received"); return -1; } if ((error_type = tty_write_string(fd, "#:GOS#", &nbytes_write)) != TTY_OK) { DEBUGFDEVICE(lx200ap_name, INDI::Logger::DBG_ERROR, "check_lx200ap_connection: unsuccessful write to telescope, %d", nbytes_write); return error_type; } tty_read_section(fd, temp_string, '#', LX200_TIMEOUT, &nbytes_read); tcflush(fd, TCIFLUSH); if (nbytes_read > 1) { temp_string[nbytes_read - 1] = '\0'; DEBUGFDEVICE(lx200ap_name, INDI::Logger::DBG_DEBUG, "check_lx200ap_status: received bytes %d, [%s]", nbytes_write, temp_string); *parkStatus = temp_string[0]; *slewStatus = temp_string[3]; return 0; } DEBUGDEVICE(lx200ap_name, INDI::Logger::DBG_ERROR, "check_lx200ap_status: wrote, but nothing received."); return -1; } #endif libindi/drivers/telescope/telescope_simulator.h0000664000175000017500000000717013263645557021373 0ustar jasemjasem/******************************************************************************* Copyright(c) 2015 Jasem Mutlaq. All rights reserved. This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2 as published by the Free Software Foundation. . 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 Library General Public License for more details. . You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. *******************************************************************************/ #pragma once #include "indiguiderinterface.h" #include "inditelescope.h" /** * @brief The ScopeSim class provides a simple mount simulator of an equatorial mount. * * It supports the following features: * + Sideral and Custom Tracking rates. * + Goto & Sync * + NWSE Hand controller direciton key slew. * + Tracking On/Off. * + Parking & Unparking with custom parking positions. * + Setting Time & Location. * * On startup and by default the mount shall point to the celestial pole. * * @author Jasem Mutlaq */ class ScopeSim : public INDI::Telescope, public INDI::GuiderInterface { public: ScopeSim(); virtual ~ScopeSim() = default; virtual const char *getDefaultName() override; virtual bool Connect() override; virtual bool Disconnect() override; virtual bool ReadScopeStatus() override; virtual bool initProperties() override; virtual void ISGetProperties(const char *dev) override; virtual bool updateProperties() override; virtual bool ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) override; virtual bool ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) override; protected: virtual bool MoveNS(INDI_DIR_NS dir, TelescopeMotionCommand command) override; virtual bool MoveWE(INDI_DIR_WE dir, TelescopeMotionCommand command) override; virtual bool Abort() override; virtual IPState GuideNorth(float ms) override; virtual IPState GuideSouth(float ms) override; virtual IPState GuideEast(float ms) override; virtual IPState GuideWest(float ms) override; virtual bool updateLocation(double latitude, double longitude, double elevation) override; virtual bool SetTrackMode(uint8_t mode) override; virtual bool SetTrackEnabled(bool enabled) override; virtual bool SetTrackRate(double raRate, double deRate) override; virtual bool Goto(double, double) override; virtual bool Park() override; virtual bool UnPark() override; virtual bool Sync(double ra, double dec) override; // Parking virtual bool SetCurrentPark() override; virtual bool SetDefaultPark() override; private: double currentRA { 0 }; double currentDEC { 90 }; double targetRA { 0 }; double targetDEC { 0 }; ln_lnlat_posn lnobserver { 0, 0 }; ln_hrz_posn lnaltaz { 0, 0 }; bool forceMeridianFlip { false }; unsigned int DBG_SCOPE { 0 }; double guiderEWTarget[2]; double guiderNSTarget[2]; INumber GuideRateN[2]; INumberVectorProperty GuideRateNP; #ifdef USE_EQUATORIAL_PE INumberVectorProperty EqPENV; INumber EqPEN[2]; ISwitch PEErrNSS[2]; ISwitchVectorProperty PEErrNSSP; ISwitch PEErrWES[2]; ISwitchVectorProperty PEErrWESP; #endif }; libindi/drivers/telescope/celestrongps.h0000664000175000017500000001040513263645557020014 0ustar jasemjasem/* Celestron GPS Copyright (C) 2003-2017 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 */ /* Version with experimental pulse guide support. GC 04.12.2015 */ #pragma once #include "celestrondriver.h" #include "indiguiderinterface.h" #include "inditelescope.h" class CelestronGPS : public INDI::Telescope, public INDI::GuiderInterface { public: CelestronGPS(); virtual ~CelestronGPS() {} virtual const char *getDefaultName() override; virtual bool Handshake() override; virtual bool ReadScopeStatus() override; virtual void ISGetProperties(const char *dev) override; virtual bool ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) override; virtual bool ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) override; virtual bool initProperties() override; virtual bool updateProperties() override; //GUIDE guideTimeout() funcion void guideTimeout(CELESTRON_DIRECTION calldir); protected: // Goto, Sync, and Motion virtual bool Goto(double ra, double dec) override; //bool GotoAzAlt(double az, double alt); virtual bool Sync(double ra, double dec) override; virtual bool MoveNS(INDI_DIR_NS dir, TelescopeMotionCommand command) override; virtual bool MoveWE(INDI_DIR_WE dir, TelescopeMotionCommand command) override; virtual bool Abort() override; // Time and Location virtual bool updateLocation(double latitude, double longitude, double elevation) override; virtual bool updateTime(ln_date *utc, double utc_offset) override; //GUIDE: guiding functions virtual IPState GuideNorth(float ms) override; virtual IPState GuideSouth(float ms) override; virtual IPState GuideEast(float ms) override; virtual IPState GuideWest(float ms) override; //GUIDE guideTimeoutHelper() function static void guideTimeoutHelperN(void *p); static void guideTimeoutHelperS(void *p); static void guideTimeoutHelperW(void *p); static void guideTimeoutHelperE(void *p); // Tracking virtual bool SetTrackMode(uint8_t mode) override; virtual bool SetTrackEnabled(bool enabled) override; // Parking virtual bool Park() override; virtual bool UnPark() override; virtual bool SetCurrentPark() override; virtual bool SetDefaultPark() override; virtual bool saveConfigItems(FILE *fp) override; virtual void simulationTriggered(bool enable) override; void mountSim(); //GUIDE variables. int GuideNSTID; int GuideWETID; CELESTRON_DIRECTION guide_direction; /* Firmware */ IText FirmwareT[5] {}; ITextVectorProperty FirmwareTP; //INumberVectorProperty HorizontalCoordsNP; //INumber HorizontalCoordsN[2]; //ISwitch TrackS[4]; //ISwitchVectorProperty TrackSP; //GUIDE Pulse guide switch ISwitchVectorProperty UsePulseCmdSP; ISwitch UsePulseCmdS[2]; ISwitchVectorProperty UseHibernateSP; ISwitch UseHibernateS[2]; private: bool setTrackMode(CELESTRON_TRACK_MODE mode); bool checkMinVersion(float minVersion, const char *feature); double currentRA, currentDEC, currentAZ, currentALT; double targetRA, targetDEC, targetAZ, targetALT; CelestronDriver driver; FirmwareInfo fwInfo; bool usePreciseCoords=false; }; libindi/drivers/telescope/synscanmount.h0000664000175000017500000000763513263645557020060 0ustar jasemjasem/******************************************************************************* Copyright(c) 2010 Gerry Rozema. All rights reserved. This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2 as published by the Free Software Foundation. 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 Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. *******************************************************************************/ #pragma once #include "inditelescope.h" class SynscanMount : public INDI::Telescope { public: SynscanMount(); virtual ~SynscanMount() = default; // overrides of base class virtual functions //bool initProperties(); virtual void ISGetProperties(const char *dev) override; virtual bool updateProperties() override; virtual const char *getDefaultName() override; virtual bool Connect() override; virtual bool initProperties() override; virtual bool ReadScopeStatus() override; bool StartTrackMode(); virtual bool Goto(double, double) override; virtual bool Park() override; virtual bool UnPark() override; virtual bool Abort() override; virtual bool SetSlewRate(int index) override; virtual bool MoveNS(INDI_DIR_NS dir, TelescopeMotionCommand command) override; virtual bool MoveWE(INDI_DIR_WE dir, TelescopeMotionCommand command) override; bool ReadTime(); bool ReadLocation(); virtual bool updateLocation(double latitude, double longitude, double elevation) override; virtual bool updateTime(ln_date *utc, double utc_offset) override; virtual bool SetCurrentPark() override; virtual bool SetDefaultPark() override; // methods added for alignment subsystem virtual bool ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) override; virtual bool ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) override; virtual bool ISNewBLOB(const char *dev, const char *name, int sizes[], int blobsizes[], char *blobs[], char *formats[], char *names[], int n) override; virtual bool ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n) override; virtual bool Sync(double ra, double dec) override; private: int PassthruCommand(int cmd, int target, int msgsize, int data, int numReturn); ln_hrz_posn GetAltAzPosition(double ra, double dec); int HexStrToInteger(const std::string &str); bool AnalyzeHandset(); void UpdateMountInformation(bool inform_client); double FirmwareVersion { 0 }; char LastParkRead[20]; int NumPark { 0 }; int StopCount { 0 }; int SlewRate { 5 }; int CustomNSSlewRate { -1 }; int CustomWESlewRate { -1 }; double SlewTargetAlt { -1 }; double SlewTargetAz { -1 }; double CurrentRA { 0 }; double CurrentDEC { 0 }; bool CanSetLocation { false }; bool ReadLatLong { false }; int RecoverTrials { 0 }; // bool HasFailed { true }; bool NewFirmware { false }; const std::string MountInfoPage { "Mount Information" }; enum class MountInfoItems { FwVersion, MountCode, AlignmentStatus, GotoStatus, MountPointingStatus, TrackingMode }; IText BasicMountInfo[6]; ITextVectorProperty BasicMountInfoV; std::string HandsetFwVersion; int MountCode { 0 }; std::string AlignmentStatus; std::string GotoStatus; std::string MountPointingStatus; std::string TrackingMode; }; libindi/drivers/telescope/skywatcherAPI.h0000664000175000017500000003053413263645557020027 0ustar jasemjasem/*! * \file skywatcherAPI.cpp * * \author Roger James * \author Gerry Rozema * \author Jean-Luc Geehalel * \date 13th November 2013 * * This file contains the definitions for a C++ implementatiom of the Skywatcher API. * It is based on work from four sources. * A C++ implementation of the API by Roger James. * The indi_eqmod driver by Jean-Luc Geehalel. * The synscanmount driver by Gerry Rozema. * The C# implementation published by Skywatcher/Synta */ #pragma once #include #define INDI_DEBUG_LOGGING #ifdef INDI_DEBUG_LOGGING #include "indibase/inditelescope.h" #define MYDEBUG(priority, msg) \ INDI::Logger::getInstance().print(pChildTelescope->getDeviceName(), priority, __FILE__, __LINE__, msg) #define MYDEBUGF(priority, msg, ...) \ INDI::Logger::getInstance().print(pChildTelescope->getDeviceName(), priority, __FILE__, __LINE__, msg, __VA_ARGS__) #else #define MYDEBUG(priority, msg) #define MYDEBUGF(priority, msg, ...) #endif struct AXISSTATUS { AXISSTATUS() : FullStop(false), Slewing(false), SlewingTo(false), SlewingForward(false), HighSpeed(false), NotInitialized(true) { } bool FullStop; bool Slewing; bool SlewingTo; bool SlewingForward; bool HighSpeed; bool NotInitialized; void SetFullStop(); void SetSlewing(bool forward, bool highspeed); void SetSlewingTo(bool forward, bool highspeed); }; class SkywatcherAPI { public: enum AXISID { AXIS1 = 0, AXIS2 = 1 }; // These values are in radians per second static constexpr double SIDEREALRATE { (2 * M_PI / 86164.09065) }; static constexpr double MAX_SPEED { 500.0 }; static constexpr double LOW_SPEED_MARGIN { 128.0 * SIDEREALRATE }; SkywatcherAPI(); virtual ~SkywatcherAPI() = default; unsigned long BCDstr2long(std::string &String); unsigned long Highstr2long(std::string &String); bool CheckIfDCMotor(); /// \brief Check if the current mount is a Virtuoso (AltAz) /// \return True if the current mount is Virtuoso otherwise false. bool IsVirtuosoMount() const; /// \brief Check if the current mount is a Merlin (AltAz) /// \return True if the current mount is Merlin otherwise false. bool IsMerlinMount() const; /// \brief Convert a slewing rate in degrees per second into the required /// clock ticks per microstep setting. /// \param[in] Axis - The axis to use. /// \param[in] DegreesPerSecond - Slewing rate in degrees per second /// \return Clock ticks per microstep for the requested rate long DegreesPerSecondToClocksTicksPerMicrostep(AXISID Axis, double DegreesPerSecond); /// \brief Convert angle in degrees to microsteps /// \param[in] Axis - The axis to use. /// \param[in] AngleInRadians - the angle in degrees. /// \return the number of microsteps long DegreesToMicrosteps(AXISID Axis, double AngleInDegrees); /// \brief Set the CurrentEncoders status variable to the current /// encoder value in microsteps for the specified axis. /// \return false failure bool GetEncoder(AXISID Axis); /// \brief Set the HighSpeedRatio status variable to the ratio between /// high and low speed stepping modes. bool GetHighSpeedRatio(AXISID Axis); /// \brief Set the MicrostepsPerRevolution status variable to the number of microsteps /// for a 360 degree revolution of the axis. /// \param[in] Axis - The axis to use. /// \return false failure bool GetMicrostepsPerRevolution(AXISID Axis); /// \brief Set the MicrostepsPermWormRevolution status variable to the number of microsteps /// for a 360 degree revolution of the worm gear. /// \param[in] Axis - The axis to use. /// \return false failure bool GetMicrostepsPerWormRevolution(AXISID Axis); bool GetMotorBoardVersion(AXISID Axis); typedef enum { CLOCKWISE, ANTICLOCKWISE } PositiveRotationSense_t; /// \brief Returns the rotation direction for a positive step on the /// designated axis. /// \param[in] Axis - The axis to use. /// \return The rotation sense clockwise or anticlockwise. /// /// Rotation directions are given looking down the axis towards the motorised pier /// for an altitude or declination axis. Or down the pier towards the mount base /// for an azimuth or right ascension axis PositiveRotationSense_t GetPositiveRotationDirection(AXISID Axis); bool GetStatus(AXISID Axis); /// \brief Set the StepperClockFrequency status variable to fixed PIC timer interrupt /// frequency (ticks per second). /// \return false failure bool GetStepperClockFrequency(AXISID Axis); bool InitializeMC(); /// \brief Initialize the communication to the mount /// \param[in] recover - The connection is recovering /// \return True if successful otherwise false bool InitMount(bool recover); /// \brief Bring the axis to an immediate halt. /// N.B. This command could cause damage to the mount or telescope /// and should not normally be used except for emergency stops. /// \param[in] Axis - The axis to use. /// \return false failure bool InstantStop(AXISID Axis); void Long2BCDstr(long Number, std::string &String); /// \brief Convert microsteps to angle in degrees /// \param[in] Axis - The axis to use. /// \param[in] Microsteps /// \return the angle in degrees double MicrostepsToDegrees(AXISID Axis, long Microsteps); /// \brief Convert microsteps to angle in radians /// \param[in] Axis - The axis to use. /// \param[in] Microsteps /// \return the angle in radians double MicrostepsToRadians(AXISID Axis, long Microsteps); void PrepareForSlewing(AXISID Axis, double Speed); /// \brief Convert a slewing rate in radians per second into the required /// clock ticks per microstep setting. /// \param[in] Axis - The axis to use. /// \param[in] DegreesPerSecond - Slewing rate in degrees per second /// \return Clock ticks per microstep for the requested rate long RadiansPerSecondToClocksTicksPerMicrostep(AXISID Axis, double RadiansPerSecond); /// \brief Convert angle in radians to microsteps /// \param[in] Axis - The axis to use. /// \param[in] AngleInRadians - the angle in radians. /// \return the number of microsteps long RadiansToMicrosteps(AXISID Axis, double AngleInRadians); /// \brief Set axis encoder to the specified value. /// \param[in] Axis - The axis to use. /// \param[in] Microsteps - the value in microsteps. /// \return false failure bool SetEncoder(AXISID Axis, long Microsteps); /// \brief Set the goto target offset per the specified axis /// \param[in] Axis - The axis to use. /// \param[in] OffsetInMicrosteps - the value to use /// \return false failure bool SetGotoTargetOffset(AXISID Axis, long OffsetInMicrosteps); /// \brief Set the motion mode per the specified axis /// \param[in] Axis - The axis to use. /// \param[in] Func - the slewing mode /// - 0 = High speed SlewTo mode /// - 1 = Low speed Slew mode /// - 2 = Low speed SlewTo mode /// - 3 = High Speed Slew mode /// \param[in] Direction - the direction to slew in /// - 0 = Forward /// - 1 = Reverse /// \return false failure bool SetMotionMode(AXISID Axis, char Func, char Direction); /// \brief Set the serail port to be usb for mount communication /// \param[in] port - an open file descriptor for the port to use. void SetSerialPort(int port) { MyPortFD = port; } /// \brief Set the PIC internal divider variable which determines /// how many clock interrupts have to occur between each microstep bool SetClockTicksPerMicrostep(AXISID Axis, long ClockTicksPerMicrostep); /// \brief Set the length of the deccelaration ramp for Slew mode. /// \param[in] Axis - The axis to use. /// \param[in] Microsteps - the length of the decceleration ramp in microsteps. /// \return false failure bool SetSlewModeDeccelerationRampLength(AXISID Axis, long Microsteps); /// \brief Set the length of the deccelaration ramp for SlewTo mode. /// \param[in] Axis - The axis to use. /// \param[in] Microsteps - the length of the decceleration ramp in microsteps. /// \return false failure bool SetSlewToModeDeccelerationRampLength(AXISID Axis, long Microsteps); /// \brief Set the camera control switch to the given state /// \param[in] OnOff - the state requested. bool SetSwitch(bool OnOff); /// \brief Start the axis slewing at the given rate /// \param[in] Axis - The axis to use. /// \param[in] SpeedInRadiansPerSecond - the slewing speed /// \param[in] IgnoreSilentMode - ignore the silent mode even if set void Slew(AXISID Axis, double SpeedInRadiansPerSecond, bool IgnoreSilentMode = true); /// \brief Slew to the given offset and stop /// \param[in] Axis - The axis to use. /// \param[in] OffsetInMicrosteps - The number of microsteps to /// \param[in] verbose - Verbose mode /// slew from the current axis position. void SlewTo(AXISID Axis, long OffsetInMicrosteps, bool verbose = true); /// \brief Bring the axis to slow stop in the distance specified /// by SetSlewModeDeccelerationRampLength /// \param[in] Axis - The axis to use. /// \return false failure bool SlowStop(AXISID Axis); /// \brief Start the axis slewing in the prevously selected mode /// \param[in] Axis - The axis to use. /// \return false failure bool StartMotion(AXISID Axis); bool TalkWithAxis(AXISID Axis, char Command, std::string &cmdDataStr, std::string &responseStr); /// \brief Check if an axis is moving /// \param[in] Axis - The axis to check. /// \return True if the axis is moving otherwise false. bool IsInMotion(AXISID Axis); // Skywatcher mount status variables unsigned long MCVersion { 0 }; // Motor control board firmware version enum MountType { EQ6 = 0x00, HEQ5 = 0x01, EQ5 = 0x02, EQ3 = 0x03, GT = 0x80, MF = 0x81, _114GT = 0x82, DOB = 0x90 }; unsigned long MountCode { 0 }; bool IsDCMotor { false }; bool SilentSlewMode { true }; // Values from mount long MicrostepsPerRevolution[2]; // Number of microsteps for 360 degree revolution long StepperClockFrequency[2]; // The stepper clock timer interrupt frequency in ticks per second long HighSpeedRatio[2]; // The speed multiplier for high speed mode. long MicrostepsPerWormRevolution[2]; // Number of microsteps for one revolution of the worm gear. // Calculated values double RadiansPerMicrostep[2]; double MicrostepsPerRadian[2]; double DegreesPerMicrostep[2]; double MicrostepsPerDegree[2]; long LowSpeedGotoMargin[2]; // SlewTo debugging long LastSlewToTarget[2]; /// Current encoder values (microsteps). long CurrentEncoders[2]; /// Polaris position (initial) encoder values (microsteps). long PolarisPositionEncoders[2]; /// Zero position encoder values (microsteps). long ZeroPositionEncoders[2]; AXISSTATUS AxesStatus[2]; double SlewingSpeed[2]; protected: // Custom debug level unsigned int DBG_SCOPE { 0 }; private: enum TTY_ERROR { TTY_OK = 0, TTY_READ_ERROR = -1, TTY_WRITE_ERROR = -2, TTY_SELECT_ERROR = -3, TTY_TIME_OUT = -4, TTY_PORT_FAILURE = -5, TTY_PARAM_ERROR = -6, TTY_ERRNO = -7 }; virtual int skywatcher_tty_read(int fd, char *buf, int nbytes, int timeout, int *nbytes_read) = 0; // virtual int skywatcher_tty_read_section(int fd, char *buf, char stop_char, int timeout, int *nbytes_read) = 0; virtual int skywatcher_tty_write(int fd, const char *buffer, int nbytes, int *nbytes_written) = 0; // virtual int skywatcher_tty_write_string(int fd, const char * buffer, int *nbytes_written) = 0; // virtual int skywatcher_tty_connect(const char *device, int bit_rate, int word_size, int parity, int stop_bits, int *fd) = 0; // virtual int skywatcher_tty_disconnect(int fd) = 0; // virtual void skywatcher_tty_error_msg(int err_code, char *err_msg, int err_msg_len) = 0; // virtual int skywatcher_tty_timeout(int fd, int timeout) = 0;*/ int MyPortFD { 0 }; #ifdef INDI_DEBUG_LOGGING public: INDI::Telescope *pChildTelescope { nullptr }; #endif }; libindi/drivers/telescope/celestrondriver.h0000664000175000017500000001313613263645557020522 0ustar jasemjasem/* Celestron driver Copyright (C) 2015 Jasem Mutlaq Copyright (C) 2017 Juan Menendez 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 */ /* Version with experimental pulse guide support. GC 04.12.2015 */ #pragma once #include #include "indicom.h" /* Starsense specific constants */ #define ISNEXSTAR 0x11 #define ISSTARSENSE 0x13 #define MINSTSENSVER float(1.18) #define MAX_RESP_SIZE 20 // device IDs #define CELESTRON_DEV_RA 0x10 #define CELESTRON_DEV_DEC 0x11 #define CELESTRON_DEV_GPS 0xb0 typedef enum { GPS_OFF, GPS_ON } CELESTRON_GPS_STATUS; typedef enum { SR_1, SR_2, SR_3, SR_4, SR_5, SR_6, SR_7, SR_8, SR_9 } CELESTRON_SLEW_RATE; typedef enum { TRACKING_OFF, TRACK_ALTAZ, TRACK_EQN, TRACK_EQS } CELESTRON_TRACK_MODE; typedef enum { RA_AXIS, DEC_AXIS } CELESTRON_AXIS; typedef enum { CELESTRON_N, CELESTRON_S, CELESTRON_W, CELESTRON_E } CELESTRON_DIRECTION; typedef enum { FW_MODEL, FW_VERSION, FW_GPS, FW_RA, FW_DEC } CELESTRON_FIRMWARE; typedef struct { std::string Model; std::string Version; std::string GPSFirmware; std::string RAFirmware; std::string DEFirmware; float controllerVersion; char controllerVariant; } FirmwareInfo; typedef struct { double ra; double dec; double az; double alt; CELESTRON_SLEW_RATE slewRate; CELESTRON_TRACK_MODE trackMode; CELESTRON_GPS_STATUS gpsStatus; bool isSlewing; } SimData; /************************************************************************** Utility functions **************************************************************************/ namespace Celestron { double trimDecAngle(double angle); uint16_t dd2nex(double angle); uint32_t dd2pnex(double angle); double nex2dd(uint16_t value); double pnex2dd(uint32_t value); } class CelestronDriver { public: CelestronDriver() {} // Misc. const char *getDeviceName(); void set_port_fd(int port_fd) { fd = port_fd; } void set_simulation(bool enable) { simulation = enable; } void set_device(const char *name); // Simulation void set_sim_slew_rate(CELESTRON_SLEW_RATE val) { sim_data.slewRate = val; } void set_sim_track_mode(CELESTRON_TRACK_MODE val) { sim_data.trackMode = val; } void set_sim_gps_status(CELESTRON_GPS_STATUS val) { sim_data.gpsStatus = val; } void set_sim_slewing(bool isSlewing) { sim_data.isSlewing = isSlewing; }; void set_sim_ra(double ra) { sim_data.ra = ra; } void set_sim_dec(double dec) { sim_data.dec = dec; } void set_sim_az(double az) { sim_data.az = az; } void set_sim_alt(double alt) { sim_data.alt = alt; } double get_sim_ra() { return sim_data.ra; } double get_sim_dec() { return sim_data.dec; } bool echo(); bool check_connection(); // Get info bool get_firmware(FirmwareInfo *info); bool get_version(char *version, int size); bool get_variant(char *variant); bool get_model(char *model, int size); bool get_dev_firmware(int dev, char *version, int size); bool get_radec(double *ra, double *dec, bool precise); bool get_azalt(double *az, double *alt, bool precise); bool get_utc_date_time(double *utc_hours, int *yy, int *mm, int *dd, int *hh, int *minute, int *ss); // Motion bool start_motion(CELESTRON_DIRECTION dir, CELESTRON_SLEW_RATE rate); bool stop_motion(CELESTRON_DIRECTION dir); bool abort(); bool slew_radec(double ra, double dec, bool precise); bool slew_azalt(double az, double alt, bool precise); bool sync(double ra, double dec, bool precise); // Time & Location bool set_location(double longitude, double latitude); bool set_datetime(struct ln_date *utc, double utc_offset); // Track Mode bool get_track_mode(CELESTRON_TRACK_MODE *mode); bool set_track_mode(CELESTRON_TRACK_MODE mode); bool is_slewing(); // Hibernate/Wakup bool hibernate(); bool wakeup(); // Pulse Guide (experimental) int send_pulse(CELESTRON_DIRECTION direction, signed char rate, unsigned char duration_msec); int get_pulse_status(CELESTRON_DIRECTION direction, bool &pulse_state); protected: void set_sim_response(const char *fmt, ...); virtual int serial_write(const char *cmd, int nbytes, int *nbytes_written); virtual int serial_read(int nbytes, int *nbytes_read); virtual int serial_read_section(char stop_char, int *nbytes_read); int send_command(const char *cmd, int cmd_len, char *resp, int resp_len, bool ascii_cmd, bool ascii_resp); int send_passthrough(int dest, int cmd_id, const char *payload, int payload_len, char *response, int response_len); char response[MAX_RESP_SIZE]; bool simulation = false; SimData sim_data; int fd = 0; }; libindi/drivers/telescope/lx200telescope.cpp0000664000175000017500000012707713263645557020426 0ustar jasemjasem/* * Standard LX200 implementation. Copyright (C) 2003 - 2018 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 */ #include "lx200telescope.h" #include "indicom.h" #include "lx200driver.h" #include #include #include #include #include #include /* Simulation Parameters */ #define LX200_GENERIC_SLEWRATE 5 /* slew rate, degrees/s */ #define SIDRATE 0.004178 /* sidereal rate, degrees/s */ void LX200Telescope::debugTriggered(bool enable) { INDI_UNUSED(enable); setLX200Debug(getDeviceName(), DBG_SCOPE); } const char *LX200Telescope::getDriverName() { return getDefaultName(); } const char *LX200Telescope::getDefaultName() { return (const char *)"Standard LX200 telescope"; } bool LX200Telescope::initProperties() { /* Make sure to init parent properties first */ INDI::Telescope::initProperties(); IUFillSwitch(&AlignmentS[0], "Polar", "", ISS_ON); IUFillSwitch(&AlignmentS[1], "AltAz", "", ISS_OFF); IUFillSwitch(&AlignmentS[2], "Land", "", ISS_OFF); IUFillSwitchVector(&AlignmentSP, AlignmentS, 3, getDeviceName(), "Alignment", "", MAIN_CONTROL_TAB, IP_RW, ISR_1OFMANY, 0, IPS_IDLE); #if 0 IUFillSwitch(&SlewRateS[SLEW_GUIDE], "SLEW_GUIDE", "Guide", ISS_OFF); IUFillSwitch(&SlewRateS[SLEW_CENTERING], "SLEW_CENTERING", "Centering", ISS_OFF); IUFillSwitch(&SlewRateS[SLEW_FIND], "SLEW_FIND", "Find", ISS_OFF); IUFillSwitch(&SlewRateS[SLEW_MAX], "SLEW_MAX", "Max", ISS_ON); IUFillSwitchVector(&SlewRateSP, SlewRateS, 4, getDeviceName(), "TELESCOPE_SLEW_RATE", "Slew Rate", MOTION_TAB, IP_RW, ISR_1OFMANY, 0, IPS_IDLE); #endif #if 0 IUFillSwitch(&TrackModeS[0], "TRACK_SIDEREAL", "Sidereal", ISS_ON); IUFillSwitch(&TrackModeS[1], "TRACK_SOLAR", "Solar", ISS_OFF); IUFillSwitch(&TrackModeS[2], "TRACK_LUNAR", "Lunar", ISS_OFF); IUFillSwitch(&TrackModeS[3], "TRACK_CUSTOM", "Custom", ISS_OFF); IUFillSwitchVector(&TrackModeSP, TrackModeS, 4, getDeviceName(), "TELESCOPE_TRACK_MODE", "Track Mode", MOTION_TAB, IP_RW, ISR_1OFMANY, 0, IPS_IDLE); #endif AddTrackMode("TRACK_SIDEREAL", "Sidereal", true); AddTrackMode("TRACK_SOLAR", "Solar"); AddTrackMode("TRACK_LUNAR", "Lunar"); AddTrackMode("TRACK_CUSTOM", "Custom"); IUFillNumber(&TrackFreqN[0], "trackFreq", "Freq", "%g", 56.4, 60.1, 0.1, 60.1); IUFillNumberVector(&TrackingFreqNP, TrackFreqN, 1, getDeviceName(), "Tracking Frequency", "", MOTION_TAB, IP_RW, 0, IPS_IDLE); IUFillSwitch(&UsePulseCmdS[0], "Off", "", ISS_ON); IUFillSwitch(&UsePulseCmdS[1], "On", "", ISS_OFF); IUFillSwitchVector(&UsePulseCmdSP, UsePulseCmdS, 2, getDeviceName(), "Use Pulse Cmd", "", MAIN_CONTROL_TAB, IP_RW, ISR_1OFMANY, 0, IPS_IDLE); IUFillSwitch(&SiteS[0], "Site 1", "", ISS_ON); IUFillSwitch(&SiteS[1], "Site 2", "", ISS_OFF); IUFillSwitch(&SiteS[2], "Site 3", "", ISS_OFF); IUFillSwitch(&SiteS[3], "Site 4", "", ISS_OFF); IUFillSwitchVector(&SiteSP, SiteS, 4, getDeviceName(), "Sites", "", SITE_TAB, IP_RW, ISR_1OFMANY, 0, IPS_IDLE); IUFillText(&SiteNameT[0], "Name", "", ""); IUFillTextVector(&SiteNameTP, SiteNameT, 1, getDeviceName(), "Site Name", "", SITE_TAB, IP_RW, 0, IPS_IDLE); IUFillSwitch(&FocusMotionS[0], "IN", "Focus in", ISS_OFF); IUFillSwitch(&FocusMotionS[1], "OUT", "Focus out", ISS_OFF); IUFillSwitchVector(&FocusMotionSP, FocusMotionS, 2, getDeviceName(), "FOCUS_MOTION", "Motion", FOCUS_TAB, IP_RW, ISR_ATMOST1, 0, IPS_IDLE); IUFillNumber(&FocusTimerN[0], "TIMER", "Timer (ms)", "%g", 0, 10000., 1000., 0); IUFillNumberVector(&FocusTimerNP, FocusTimerN, 1, getDeviceName(), "FOCUS_TIMER", "Focus Timer", FOCUS_TAB, IP_RW, 0, IPS_IDLE); IUFillSwitch(&FocusModeS[0], "FOCUS_HALT", "Halt", ISS_ON); IUFillSwitch(&FocusModeS[1], "FOCUS_SLOW", "Slow", ISS_OFF); IUFillSwitch(&FocusModeS[2], "FOCUS_FAST", "Fast", ISS_OFF); IUFillSwitchVector(&FocusModeSP, FocusModeS, 3, getDeviceName(), "FOCUS_MODE", "Mode", FOCUS_TAB, IP_RW, ISR_1OFMANY, 0, IPS_IDLE); TrackState = SCOPE_IDLE; initGuiderProperties(getDeviceName(), GUIDE_TAB); /* Add debug/simulation/config controls so we may debug driver if necessary */ addAuxControls(); setDriverInterface(getDriverInterface() | GUIDER_INTERFACE); double longitude=0, latitude=90; // Get value from config file if it exists. IUGetConfigNumber(getDeviceName(), "GEOGRAPHIC_COORD", "LONG", &longitude); currentRA = get_local_sidereal_time(longitude); IUGetConfigNumber(getDeviceName(), "GEOGRAPHIC_COORD", "LAT", &latitude); currentDEC = latitude > 0 ? 90 : -90; return true; } void LX200Telescope::ISGetProperties(const char *dev) { if (dev != nullptr && strcmp(dev, getDeviceName()) != 0) return; INDI::Telescope::ISGetProperties(dev); /* if (isConnected()) { if (genericCapability & LX200_HAS_ALIGNMENT_TYPE) defineSwitch(&AlignmentSP); if (genericCapability & LX200_HAS_TRACKING_FREQ) defineNumber(&TrackingFreqNP); if (genericCapability & LX200_HAS_PULSE_GUIDING) defineSwitch(&UsePulseCmdSP); if (genericCapability & LX200_HAS_SITES) { defineSwitch(&SiteSP); defineText(&SiteNameTP); } defineNumber(&GuideNSNP); defineNumber(&GuideWENP); if (genericCapability & LX200_HAS_FOCUS) { defineSwitch(&FocusMotionSP); defineNumber(&FocusTimerNP); defineSwitch(&FocusModeSP); } } */ } bool LX200Telescope::updateProperties() { INDI::Telescope::updateProperties(); if (isConnected()) { if (genericCapability & LX200_HAS_ALIGNMENT_TYPE) defineSwitch(&AlignmentSP); if (genericCapability & LX200_HAS_TRACKING_FREQ) defineNumber(&TrackingFreqNP); if (genericCapability & LX200_HAS_PULSE_GUIDING) defineSwitch(&UsePulseCmdSP); if (genericCapability & LX200_HAS_SITES) { defineSwitch(&SiteSP); defineText(&SiteNameTP); } defineNumber(&GuideNSNP); defineNumber(&GuideWENP); if (genericCapability & LX200_HAS_FOCUS) { defineSwitch(&FocusMotionSP); defineNumber(&FocusTimerNP); defineSwitch(&FocusModeSP); } getBasicData(); } else { if (genericCapability & LX200_HAS_ALIGNMENT_TYPE) deleteProperty(AlignmentSP.name); if (genericCapability & LX200_HAS_TRACKING_FREQ) deleteProperty(TrackingFreqNP.name); if (genericCapability & LX200_HAS_PULSE_GUIDING) deleteProperty(UsePulseCmdSP.name); if (genericCapability & LX200_HAS_SITES) { deleteProperty(SiteSP.name); deleteProperty(SiteNameTP.name); } deleteProperty(GuideNSNP.name); deleteProperty(GuideWENP.name); if (genericCapability & LX200_HAS_FOCUS) { deleteProperty(FocusMotionSP.name); deleteProperty(FocusTimerNP.name); deleteProperty(FocusModeSP.name); } } return true; } bool LX200Telescope::checkConnection() { if (isSimulation()) return true; return (check_lx200_connection(PortFD) == 0); } bool LX200Telescope::Handshake() { return checkConnection(); } bool LX200Telescope::isSlewComplete() { return (::isSlewComplete(PortFD) == 1); } bool LX200Telescope::ReadScopeStatus() { if (!isConnected()) return false; if (isSimulation()) { mountSim(); return true; } //if (check_lx200_connection(PortFD)) //return false; if (TrackState == SCOPE_SLEWING) { // Check if LX200 is done slewing if (isSlewComplete()) { // Set slew mode to "Centering" IUResetSwitch(&SlewRateSP); SlewRateS[SLEW_CENTERING].s = ISS_ON; IDSetSwitch(&SlewRateSP, nullptr); TrackState = SCOPE_TRACKING; LOG_INFO("Slew is complete. Tracking..."); } } else if (TrackState == SCOPE_PARKING) { if (isSlewComplete()) { SetParked(true); } } if (getLX200RA(PortFD, ¤tRA) < 0 || getLX200DEC(PortFD, ¤tDEC) < 0) { EqNP.s = IPS_ALERT; IDSetNumber(&EqNP, "Error reading RA/DEC."); return false; } NewRaDec(currentRA, currentDEC); return true; } bool LX200Telescope::Goto(double ra, double dec) { const struct timespec timeout = {0, 100000000L}; targetRA = ra; targetDEC = dec; char RAStr[64]={0}, DecStr[64]={0}; int fracbase = 0; switch (getLX200Format()) { case LX200_LONGER_FORMAT: fracbase = 360000; break; case LX200_LONG_FORMAT: case LX200_SHORT_FORMAT: default: fracbase = 3600; break; } fs_sexa(RAStr, targetRA, 2, fracbase); fs_sexa(DecStr, targetDEC, 2, fracbase); // If moving, let's stop it first. if (EqNP.s == IPS_BUSY) { if (!isSimulation() && abortSlew(PortFD) < 0) { AbortSP.s = IPS_ALERT; IDSetSwitch(&AbortSP, "Abort slew failed."); return false; } AbortSP.s = IPS_OK; EqNP.s = IPS_IDLE; IDSetSwitch(&AbortSP, "Slew aborted."); IDSetNumber(&EqNP, nullptr); if (MovementNSSP.s == IPS_BUSY || MovementWESP.s == IPS_BUSY) { MovementNSSP.s = MovementWESP.s = IPS_IDLE; EqNP.s = IPS_IDLE; IUResetSwitch(&MovementNSSP); IUResetSwitch(&MovementWESP); IDSetSwitch(&MovementNSSP, nullptr); IDSetSwitch(&MovementWESP, nullptr); } // sleep for 100 mseconds nanosleep(&timeout, NULL); } if (!isSimulation()) { if (setObjectRA(PortFD, targetRA) < 0 || (setObjectDEC(PortFD, targetDEC)) < 0) { EqNP.s = IPS_ALERT; IDSetNumber(&EqNP, "Error setting RA/DEC."); return false; } int err = 0; /* Slew reads the '0', that is not the end of the slew */ if ((err = Slew(PortFD))) { LOGF_ERROR("Error Slewing to JNow RA %s - DEC %s", RAStr, DecStr); slewError(err); return false; } } TrackState = SCOPE_SLEWING; EqNP.s = IPS_BUSY; LOGF_INFO("Slewing to RA: %s - DEC: %s", RAStr, DecStr); return true; } bool LX200Telescope::Sync(double ra, double dec) { char syncString[256]={0}; if (!isSimulation() && (setObjectRA(PortFD, ra) < 0 || (setObjectDEC(PortFD, dec)) < 0)) { EqNP.s = IPS_ALERT; IDSetNumber(&EqNP, "Error setting RA/DEC. Unable to Sync."); return false; } if (!isSimulation() && ::Sync(PortFD, syncString) < 0) { EqNP.s = IPS_ALERT; IDSetNumber(&EqNP, "Synchronization failed."); return false; } currentRA = ra; currentDEC = dec; LOG_INFO("Synchronization successful."); EqNP.s = IPS_OK; NewRaDec(currentRA, currentDEC); return true; } bool LX200Telescope::Park() { const struct timespec timeout = {0, 100000000L}; if (!isSimulation()) { // If scope is moving, let's stop it first. if (EqNP.s == IPS_BUSY) { if (!isSimulation() && abortSlew(PortFD) < 0) { AbortSP.s = IPS_ALERT; IDSetSwitch(&AbortSP, "Abort slew failed."); return false; } AbortSP.s = IPS_OK; EqNP.s = IPS_IDLE; IDSetSwitch(&AbortSP, "Slew aborted."); IDSetNumber(&EqNP, nullptr); if (MovementNSSP.s == IPS_BUSY || MovementWESP.s == IPS_BUSY) { MovementNSSP.s = MovementWESP.s = IPS_IDLE; EqNP.s = IPS_IDLE; IUResetSwitch(&MovementNSSP); IUResetSwitch(&MovementWESP); IDSetSwitch(&MovementNSSP, nullptr); IDSetSwitch(&MovementWESP, nullptr); } // sleep for 100 msec nanosleep(&timeout, NULL); } if (!isSimulation() && slewToPark(PortFD) < 0) { ParkSP.s = IPS_ALERT; IDSetSwitch(&ParkSP, "Parking Failed."); return false; } } ParkSP.s = IPS_BUSY; TrackState = SCOPE_PARKING; LOG_INFO("Parking telescope in progress..."); return true; } bool LX200Telescope::MoveNS(INDI_DIR_NS dir, TelescopeMotionCommand command) { int current_move = (dir == DIRECTION_NORTH) ? LX200_NORTH : LX200_SOUTH; switch (command) { case MOTION_START: if (!isSimulation() && MoveTo(PortFD, current_move) < 0) { LOG_ERROR("Error setting N/S motion direction."); return false; } else LOGF_INFO("Moving toward %s.", (current_move == LX200_NORTH) ? "North" : "South"); break; case MOTION_STOP: if (!isSimulation() && HaltMovement(PortFD, current_move) < 0) { LOG_ERROR("Error stopping N/S motion."); return false; } else LOGF_INFO("Movement toward %s halted.", (current_move == LX200_NORTH) ? "North" : "South"); break; } return true; } bool LX200Telescope::MoveWE(INDI_DIR_WE dir, TelescopeMotionCommand command) { int current_move = (dir == DIRECTION_WEST) ? LX200_WEST : LX200_EAST; switch (command) { case MOTION_START: if (!isSimulation() && MoveTo(PortFD, current_move) < 0) { LOG_ERROR("Error setting W/E motion direction."); return false; } else LOGF_INFO("Moving toward %s.", (current_move == LX200_WEST) ? "West" : "East"); break; case MOTION_STOP: if (!isSimulation() && HaltMovement(PortFD, current_move) < 0) { LOG_ERROR("Error stopping W/E motion."); return false; } else LOGF_INFO("Movement toward %s halted.", (current_move == LX200_WEST) ? "West" : "East"); break; } return true; } bool LX200Telescope::Abort() { if (!isSimulation() && abortSlew(PortFD) < 0) { LOG_ERROR("Failed to abort slew."); return false; } if (GuideNSNP.s == IPS_BUSY || GuideWENP.s == IPS_BUSY) { GuideNSNP.s = GuideWENP.s = IPS_IDLE; GuideNSN[0].value = GuideNSN[1].value = 0.0; GuideWEN[0].value = GuideWEN[1].value = 0.0; if (GuideNSTID) { IERmTimer(GuideNSTID); GuideNSTID = 0; } if (GuideWETID) { IERmTimer(GuideWETID); GuideNSTID = 0; } LOG_INFO("Guide aborted."); IDSetNumber(&GuideNSNP, nullptr); IDSetNumber(&GuideWENP, nullptr); return true; } return true; } bool LX200Telescope::setLocalDate(uint8_t days, uint8_t months, uint16_t years) { return (setCalenderDate(PortFD, days, months, years) == 0); } bool LX200Telescope::setLocalTime24(uint8_t hour, uint8_t minute, uint8_t second) { return (setLocalTime(PortFD, hour, minute, second) == 0); } bool LX200Telescope::setUTCOffset(double offset) { return (::setUTCOffset(PortFD, (offset * -1.0)) == 0); } bool LX200Telescope::updateTime(ln_date *utc, double utc_offset) { struct ln_zonedate ltm; if (isSimulation()) return true; ln_date_to_zonedate(utc, <m, utc_offset * 3600.0); JD = ln_get_julian_day(utc); LOGF_DEBUG("New JD is %.2f", JD); // Meade defines UTC Offset as the offset ADDED to local time to yield UTC, which // is the opposite of the standard definition of UTC offset! if (setUTCOffset(utc_offset) == false) { LOG_ERROR("Error setting UTC Offset."); return false; } // Set Local Time if (setLocalTime24(ltm.hours, ltm.minutes, ltm.seconds) == false) { LOG_ERROR("Error setting local time."); return false; } if (setLocalDate(ltm.days, ltm.months, ltm.years) == false) { LOG_ERROR("Error setting local date."); return false; } LOG_INFO("Time updated, updating planetary data..."); return true; } bool LX200Telescope::updateLocation(double latitude, double longitude, double elevation) { INDI_UNUSED(elevation); if (isSimulation()) return true; if (!isSimulation() && setSiteLongitude(PortFD, 360.0 - longitude) < 0) { LOG_ERROR("Error setting site longitude coordinates"); return false; } if (!isSimulation() && setSiteLatitude(PortFD, latitude) < 0) { LOG_ERROR("Error setting site latitude coordinates"); return false; } char l[32]={0}, L[32]={0}; fs_sexa(l, latitude, 3, 3600); fs_sexa(L, longitude, 4, 3600); LOGF_INFO("Site location updated to Lat %.32s - Long %.32s", l, L); return true; } bool LX200Telescope::ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n) { if (dev != nullptr && strcmp(dev, getDeviceName()) == 0) { if (!strcmp(name, SiteNameTP.name)) { if (!isSimulation() && setSiteName(PortFD, texts[0], currentSiteNum) < 0) { SiteNameTP.s = IPS_ALERT; IDSetText(&SiteNameTP, "Setting site name"); return false; } SiteNameTP.s = IPS_OK; IText *tp = IUFindText(&SiteNameTP, names[0]); IUSaveText(tp, texts[0]); IDSetText(&SiteNameTP, "Site name updated"); return true; } } return INDI::Telescope::ISNewText(dev, name, texts, names, n); } bool LX200Telescope::ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) { if (dev != nullptr && strcmp(dev, getDeviceName()) == 0) { // Update Frequency if (!strcmp(name, TrackingFreqNP.name)) { LOGF_DEBUG("Trying to set track freq of: %04.1f", values[0]); if (!isSimulation() && setTrackFreq(PortFD, values[0]) < 0) { TrackingFreqNP.s = IPS_ALERT; IDSetNumber(&TrackingFreqNP, "Error setting tracking frequency"); return false; } TrackingFreqNP.s = IPS_OK; TrackingFreqNP.np[0].value = values[0]; IDSetNumber(&TrackingFreqNP, "Tracking frequency set to %04.1f", values[0]); if (trackingMode != LX200_TRACK_MANUAL) { trackingMode = LX200_TRACK_MANUAL; TrackModeS[0].s = ISS_OFF; TrackModeS[1].s = ISS_OFF; TrackModeS[2].s = ISS_OFF; TrackModeS[3].s = ISS_ON; TrackModeSP.s = IPS_OK; selectTrackingMode(PortFD, trackingMode); IDSetSwitch(&TrackModeSP, nullptr); } return true; } if (!strcmp(name, FocusTimerNP.name)) { // Don't update if busy if (FocusTimerNP.s == IPS_BUSY) return true; IUUpdateNumber(&FocusTimerNP, values, names, n); FocusTimerNP.s = IPS_OK; IDSetNumber(&FocusTimerNP, nullptr); LOGF_DEBUG("Setting focus timer to %.2f", FocusTimerN[0].value); return true; } processGuiderProperties(name, values, names, n); } // if we didn't process it, continue up the chain, let somebody else // give it a shot return INDI::Telescope::ISNewNumber(dev, name, values, names, n); } bool LX200Telescope::ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) { int index = 0; if (dev != nullptr && strcmp(dev, getDeviceName()) == 0) { // Alignment if (!strcmp(name, AlignmentSP.name)) { if (IUUpdateSwitch(&AlignmentSP, states, names, n) < 0) return false; index = IUFindOnSwitchIndex(&AlignmentSP); if (!isSimulation() && setAlignmentMode(PortFD, index) < 0) { AlignmentSP.s = IPS_ALERT; IDSetSwitch(&AlignmentSP, "Error setting alignment mode."); return false; } AlignmentSP.s = IPS_OK; IDSetSwitch(&AlignmentSP, nullptr); return true; } // Sites if (!strcmp(name, SiteSP.name)) { if (IUUpdateSwitch(&SiteSP, states, names, n) < 0) return false; currentSiteNum = IUFindOnSwitchIndex(&SiteSP) + 1; if (!isSimulation() && selectSite(PortFD, currentSiteNum) < 0) { SiteSP.s = IPS_ALERT; IDSetSwitch(&SiteSP, "Error selecting sites."); return false; } if (isSimulation()) IUSaveText(&SiteNameTP.tp[0], "Sample Site"); else getSiteName(PortFD, SiteNameTP.tp[0].text, currentSiteNum); if (GetTelescopeCapability() & TELESCOPE_HAS_LOCATION) sendScopeLocation(); SiteNameTP.s = SiteSP.s = IPS_OK; IDSetText(&SiteNameTP, nullptr); IDSetSwitch(&SiteSP, nullptr); return false; } // Focus Motion if (!strcmp(name, FocusMotionSP.name)) { // If mode is "halt" if (FocusModeS[0].s == ISS_ON) { FocusMotionSP.s = IPS_IDLE; IDSetSwitch(&FocusMotionSP, "Focus mode is halt. Select slow or fast mode"); return true; } int last_motion = IUFindOnSwitchIndex(&FocusMotionSP); if (IUUpdateSwitch(&FocusMotionSP, states, names, n) < 0) return false; index = IUFindOnSwitchIndex(&FocusMotionSP); // If same direction and we're busy, stop if (last_motion == index && FocusMotionSP.s == IPS_BUSY) { IUResetSwitch(&FocusMotionSP); FocusMotionSP.s = IPS_IDLE; setFocuserSpeedMode(PortFD, 0); IDSetSwitch(&FocusMotionSP, nullptr); return true; } if (!isSimulation() && setFocuserMotion(PortFD, index) < 0) { FocusMotionSP.s = IPS_ALERT; IDSetSwitch(&FocusMotionSP, "Error setting focuser speed."); return false; } // with a timer if (FocusTimerN[0].value > 0) { FocusTimerNP.s = IPS_BUSY; FocusMotionSP.s = IPS_BUSY; IEAddTimer(50, LX200Telescope::updateFocusHelper, this); } FocusMotionSP.s = IPS_OK; IDSetSwitch(&FocusMotionSP, nullptr); return true; } // Focus speed if (!strcmp(name, FocusModeSP.name)) { IUResetSwitch(&FocusModeSP); IUUpdateSwitch(&FocusModeSP, states, names, n); index = IUFindOnSwitchIndex(&FocusModeSP); /* disable timer and motion */ if (index == 0) { IUResetSwitch(&FocusMotionSP); FocusMotionSP.s = IPS_IDLE; FocusTimerNP.s = IPS_IDLE; IDSetSwitch(&FocusMotionSP, nullptr); IDSetNumber(&FocusTimerNP, nullptr); } if (!isSimulation()) setFocuserSpeedMode(PortFD, index); FocusModeSP.s = IPS_OK; IDSetSwitch(&FocusModeSP, nullptr); return true; } // Pulse-Guide command support if (!strcmp(name, UsePulseCmdSP.name)) { IUResetSwitch(&UsePulseCmdSP); IUUpdateSwitch(&UsePulseCmdSP, states, names, n); UsePulseCmdSP.s = IPS_OK; IDSetSwitch(&UsePulseCmdSP, nullptr); return true; } } // Nobody has claimed this, so pass it to the parent return INDI::Telescope::ISNewSwitch(dev, name, states, names, n); } bool LX200Telescope::SetTrackMode(uint8_t mode) { if (isSimulation()) return true; bool rc = (selectTrackingMode(PortFD, mode) == 0); // Only update tracking frequency if it is defined and not deleted by child classes if (rc && (genericCapability & LX200_HAS_TRACKING_FREQ)) { getTrackFreq(PortFD, &TrackFreqN[0].value); IDSetNumber(&TrackingFreqNP, nullptr); } return rc; } bool LX200Telescope::SetSlewRate(int index) { // Convert index to Meade format index = 3 - index; if (!isSimulation() && setSlewMode(PortFD, index) < 0) { SlewRateSP.s = IPS_ALERT; IDSetSwitch(&SlewRateSP, "Error setting slew mode."); return false; } SlewRateSP.s = IPS_OK; IDSetSwitch(&SlewRateSP, nullptr); return true; } void LX200Telescope::updateFocusHelper(void *p) { ((LX200Telescope *)p)->updateFocusTimer(); } void LX200Telescope::updateFocusTimer() { switch (FocusTimerNP.s) { case IPS_IDLE: break; case IPS_BUSY: //if (isDebug()) //IDLog("Focus Timer Value is %g\n", FocusTimerN[0].value); FocusTimerN[0].value -= 50; if (FocusTimerN[0].value <= 0) { //if (isDebug()) //IDLog("Focus Timer Expired\n"); if (!isSimulation() && setFocuserSpeedMode(PortFD, 0) < 0) { FocusModeSP.s = IPS_ALERT; IDSetSwitch(&FocusModeSP, "Error setting focuser mode."); //if (isDebug()) //IDLog("Error setting focuser mode\n"); return; } FocusMotionSP.s = IPS_IDLE; FocusTimerNP.s = IPS_OK; FocusModeSP.s = IPS_OK; IUResetSwitch(&FocusMotionSP); IUResetSwitch(&FocusModeSP); FocusModeS[0].s = ISS_ON; IDSetSwitch(&FocusModeSP, nullptr); IDSetSwitch(&FocusMotionSP, nullptr); } IDSetNumber(&FocusTimerNP, nullptr); if (FocusTimerN[0].value > 0) IEAddTimer(50, LX200Telescope::updateFocusHelper, this); break; case IPS_OK: break; case IPS_ALERT: break; } } void LX200Telescope::mountSim() { static struct timeval ltv; struct timeval tv; double dt=0, da=0, dx=0; int nlocked=0; /* update elapsed time since last poll, don't presume exactly POLLMS */ gettimeofday(&tv, nullptr); if (ltv.tv_sec == 0 && ltv.tv_usec == 0) ltv = tv; dt = tv.tv_sec - ltv.tv_sec + (tv.tv_usec - ltv.tv_usec) / 1e6; ltv = tv; da = LX200_GENERIC_SLEWRATE * dt; /* Process per current state. We check the state of EQUATORIAL_COORDS and act acoordingly */ switch (TrackState) { case SCOPE_IDLE: currentRA += (TRACKRATE_SIDEREAL/3600.0 * dt / 15.); break; case SCOPE_TRACKING: switch (IUFindOnSwitchIndex(&TrackModeSP)) { case TRACK_SIDEREAL: da = 0; dx = 0; break; case TRACK_LUNAR: da = ((TRACKRATE_LUNAR-TRACKRATE_SIDEREAL)/3600.0 * dt / 15.); dx = 0; break; case TRACK_SOLAR: da = ((TRACKRATE_SOLAR-TRACKRATE_SIDEREAL)/3600.0 * dt / 15.); dx = 0; break; case TRACK_CUSTOM: da = ((TrackRateN[AXIS_RA].value-TRACKRATE_SIDEREAL)/3600.0 * dt / 15.); dx = (TrackRateN[AXIS_DE].value/3600.0 * dt); break; } currentRA += da; currentDEC += dx; break; case SCOPE_SLEWING: case SCOPE_PARKING: /* slewing - nail it when both within one pulse @ LX200_GENERIC_SLEWRATE */ nlocked = 0; dx = targetRA - currentRA; if (fabs(dx) <= da) { currentRA = targetRA; nlocked++; } else if (dx > 0) currentRA += da / 15.; else currentRA -= da / 15.; dx = targetDEC - currentDEC; if (fabs(dx) <= da) { currentDEC = targetDEC; nlocked++; } else if (dx > 0) currentDEC += da; else currentDEC -= da; if (nlocked == 2) { if (TrackState == SCOPE_SLEWING) TrackState = SCOPE_TRACKING; else SetParked(true); } break; default: break; } NewRaDec(currentRA, currentDEC); } void LX200Telescope::getBasicData() { if (!isSimulation()) { checkLX200Format(PortFD); if (genericCapability & LX200_HAS_ALIGNMENT_TYPE) getAlignment(); // Only check time format if it is not already initialized by the class if ( (GetTelescopeCapability() & TELESCOPE_HAS_TIME) && timeFormat == -1) { if (getTimeFormat(PortFD, &timeFormat) < 0) LOG_ERROR("Failed to retrieve time format from device."); else { int ret = 0; timeFormat = (timeFormat == 24) ? LX200_24 : LX200_AM; // We always do 24 hours if (timeFormat != LX200_24) ret = toggleTimeFormat(PortFD); } } if (genericCapability & LX200_HAS_SITES) { SiteNameT[0].text = new char[64]; if (getSiteName(PortFD, SiteNameT[0].text, currentSiteNum) < 0) LOG_ERROR("Failed to get site name from device"); else IDSetText(&SiteNameTP, nullptr); } if (genericCapability & LX200_HAS_TRACKING_FREQ) { if (getTrackFreq(PortFD, &TrackFreqN[0].value) < 0) LOG_ERROR("Failed to get tracking frequency from device."); else IDSetNumber(&TrackingFreqNP, nullptr); } } if (sendLocationOnStartup && (GetTelescopeCapability() & TELESCOPE_HAS_LOCATION)) sendScopeLocation(); if (sendTimeOnStartup && (GetTelescopeCapability() & TELESCOPE_HAS_TIME)) sendScopeTime(); } void LX200Telescope::slewError(int slewCode) { if (slewCode == 1) LOG_ERROR("Object below horizon."); else if (slewCode == 2) LOG_ERROR("Object below the minimum elevation limit."); else LOG_ERROR("Slew failed."); EqNP.s = IPS_ALERT; IDSetNumber(&EqNP, nullptr); } void LX200Telescope::getAlignment() { signed char align = ACK(PortFD); if (align < 0) { IDSetSwitch(&AlignmentSP, "Failed to get telescope alignment."); return; } AlignmentS[0].s = ISS_OFF; AlignmentS[1].s = ISS_OFF; AlignmentS[2].s = ISS_OFF; switch (align) { case 'P': AlignmentS[0].s = ISS_ON; break; case 'A': AlignmentS[1].s = ISS_ON; break; case 'L': AlignmentS[2].s = ISS_ON; break; } AlignmentSP.s = IPS_OK; IDSetSwitch(&AlignmentSP, nullptr); } bool LX200Telescope::getLocalTime(char *timeString) { if (isSimulation()) { time_t now = time (NULL); strftime(timeString, 32, "%T", localtime(&now)); } else { double ctime=0; int h, m, s; getLocalTime24(PortFD, &ctime); getSexComponents(ctime, &h, &m, &s); snprintf(timeString, 32, "%02d:%02d:%02d", h, m, s); } return true; } bool LX200Telescope::getLocalDate(char *dateString) { if (isSimulation()) { time_t now = time (NULL); strftime(dateString, 32, "%F", localtime(&now)); } else { getCalendarDate(PortFD, dateString); } return true; } bool LX200Telescope::getUTFOffset(double *offset) { if (isSimulation()) { *offset = 3; return true; } int lx200_utc_offset = 0; getUTCOffset(PortFD, &lx200_utc_offset); // LX200 TimeT Offset is defined at the number of hours added to LOCAL TIME to get TimeT. This is contrary to the normal definition. *offset = lx200_utc_offset * -1; return true; } bool LX200Telescope::sendScopeTime() { char cdate[32]={0}; char ctime[32]={0}; struct tm ltm; struct tm utm; time_t time_epoch; double offset=0; if (getUTFOffset(&offset)) { char utcStr[8]={0}; snprintf(utcStr, 8, "%.2f", offset); IUSaveText(&TimeT[1], utcStr); } else { LOG_WARN("Could not obtain UTC offset from mount!"); return false; } if (getLocalTime(ctime) == false) { LOG_WARN("Could not obtain local time from mount!"); return false; } if (getLocalDate(cdate) == false) { LOG_WARN("Could not obtain local date from mount!"); return false; } // To ISO 8601 format in LOCAL TIME! char datetime[64]; snprintf(datetime, 64, "%sT%s", cdate, ctime); // Now that date+time are combined, let's get tm representation of it. if (strptime(datetime, "%FT%T", <m) == NULL) { LOGF_WARN("Could not process mount date and time: %s", datetime); return false; } // Get local time epoch in UNIX seconds time_epoch = mktime(<m); // LOCAL to UTC by subtracting offset. time_epoch -= static_cast(offset * 3600.0); // Get UTC (we're using localtime_r, but since we shifted time_epoch above by UTCOffset, we should be getting the real UTC time) localtime_r(&time_epoch, &utm); // Format it into the final UTC ISO 8601 strftime(cdate, 32, "%Y-%m-%dT%H:%M:%S", &utm); IUSaveText(&TimeT[0], cdate); LOGF_DEBUG("Mount controller UTC Time: %s", TimeT[0].text); LOGF_DEBUG("Mount controller UTC Offset: %s", TimeT[1].text); // Let's send everything to the client IDSetText(&TimeTP, nullptr); return true; } bool LX200Telescope::sendScopeLocation() { int dd = 0, mm = 0; if (isSimulation()) { LocationNP.np[LOCATION_LATITUDE].value = 29.5; LocationNP.np[LOCATION_LONGITUDE].value = 48.0; LocationNP.np[LOCATION_ELEVATION].value = 10; LocationNP.s = IPS_OK; IDSetNumber(&LocationNP, nullptr); return true; } if (getSiteLatitude(PortFD, &dd, &mm) < 0) { LOG_WARN("Failed to get site latitude from device."); return false; } else { if (dd > 0) LocationNP.np[0].value = dd + mm / 60.0; else LocationNP.np[0].value = dd - mm / 60.0; } if (getSiteLongitude(PortFD, &dd, &mm) < 0) { LOG_WARN("Failed to get site longitude from device."); return false; } else { if (dd > 0) LocationNP.np[1].value = 360.0 - (dd + mm / 60.0); else LocationNP.np[1].value = (dd - mm / 60.0) * -1.0; } LOGF_DEBUG("Mount Controller Latitude: %g Longitude: %g", LocationN[LOCATION_LATITUDE].value, LocationN[LOCATION_LONGITUDE].value); IDSetNumber(&LocationNP, nullptr); return true; } IPState LX200Telescope::GuideNorth(float ms) { int use_pulse_cmd; use_pulse_cmd = IUFindOnSwitchIndex(&UsePulseCmdSP); if (!use_pulse_cmd && (MovementNSSP.s == IPS_BUSY || MovementWESP.s == IPS_BUSY)) { LOG_ERROR("Cannot guide while moving."); return IPS_ALERT; } // If already moving (no pulse command), then stop movement if (MovementNSSP.s == IPS_BUSY) { int dir = IUFindOnSwitchIndex(&MovementNSSP); MoveNS(dir == 0 ? DIRECTION_NORTH : DIRECTION_SOUTH, MOTION_STOP); } if (GuideNSTID) { IERmTimer(GuideNSTID); GuideNSTID = 0; } if (use_pulse_cmd) { SendPulseCmd(LX200_NORTH, ms); } else { if (setSlewMode(PortFD, LX200_SLEW_GUIDE) < 0) { SlewRateSP.s = IPS_ALERT; IDSetSwitch(&SlewRateSP, "Error setting slew mode."); return IPS_ALERT; } MovementNSS[0].s = ISS_ON; MoveNS(DIRECTION_NORTH, MOTION_START); } // Set slew to guiding IUResetSwitch(&SlewRateSP); SlewRateS[SLEW_GUIDE].s = ISS_ON; IDSetSwitch(&SlewRateSP, nullptr); guide_direction = LX200_NORTH; GuideNSTID = IEAddTimer(ms, guideTimeoutHelper, this); return IPS_BUSY; } IPState LX200Telescope::GuideSouth(float ms) { int use_pulse_cmd; use_pulse_cmd = IUFindOnSwitchIndex(&UsePulseCmdSP); if (!use_pulse_cmd && (MovementNSSP.s == IPS_BUSY || MovementWESP.s == IPS_BUSY)) { LOG_ERROR("Cannot guide while moving."); return IPS_ALERT; } // If already moving (no pulse command), then stop movement if (MovementNSSP.s == IPS_BUSY) { int dir = IUFindOnSwitchIndex(&MovementNSSP); MoveNS(dir == 0 ? DIRECTION_NORTH : DIRECTION_SOUTH, MOTION_STOP); } if (GuideNSTID) { IERmTimer(GuideNSTID); GuideNSTID = 0; } if (use_pulse_cmd) { SendPulseCmd(LX200_SOUTH, ms); } else { if (setSlewMode(PortFD, LX200_SLEW_GUIDE) < 0) { SlewRateSP.s = IPS_ALERT; IDSetSwitch(&SlewRateSP, "Error setting slew mode."); return IPS_ALERT; } MovementNSS[1].s = ISS_ON; MoveNS(DIRECTION_SOUTH, MOTION_START); } // Set slew to guiding IUResetSwitch(&SlewRateSP); SlewRateS[SLEW_GUIDE].s = ISS_ON; IDSetSwitch(&SlewRateSP, nullptr); guide_direction = LX200_SOUTH; GuideNSTID = IEAddTimer(ms, guideTimeoutHelper, this); return IPS_BUSY; } IPState LX200Telescope::GuideEast(float ms) { int use_pulse_cmd; use_pulse_cmd = IUFindOnSwitchIndex(&UsePulseCmdSP); if (!use_pulse_cmd && (MovementNSSP.s == IPS_BUSY || MovementWESP.s == IPS_BUSY)) { LOG_ERROR("Cannot guide while moving."); return IPS_ALERT; } // If already moving (no pulse command), then stop movement if (MovementWESP.s == IPS_BUSY) { int dir = IUFindOnSwitchIndex(&MovementWESP); MoveWE(dir == 0 ? DIRECTION_WEST : DIRECTION_EAST, MOTION_STOP); } if (GuideWETID) { IERmTimer(GuideWETID); GuideWETID = 0; } if (use_pulse_cmd) { SendPulseCmd(LX200_EAST, ms); } else { if (setSlewMode(PortFD, LX200_SLEW_GUIDE) < 0) { SlewRateSP.s = IPS_ALERT; IDSetSwitch(&SlewRateSP, "Error setting slew mode."); return IPS_ALERT; } MovementWES[1].s = ISS_ON; MoveWE(DIRECTION_EAST, MOTION_START); } // Set slew to guiding IUResetSwitch(&SlewRateSP); SlewRateS[SLEW_GUIDE].s = ISS_ON; IDSetSwitch(&SlewRateSP, nullptr); guide_direction = LX200_EAST; GuideWETID = IEAddTimer(ms, guideTimeoutHelper, this); return IPS_BUSY; } IPState LX200Telescope::GuideWest(float ms) { int use_pulse_cmd; use_pulse_cmd = IUFindOnSwitchIndex(&UsePulseCmdSP); if (!use_pulse_cmd && (MovementNSSP.s == IPS_BUSY || MovementWESP.s == IPS_BUSY)) { LOG_ERROR("Cannot guide while moving."); return IPS_ALERT; } // If already moving (no pulse command), then stop movement if (MovementWESP.s == IPS_BUSY) { int dir = IUFindOnSwitchIndex(&MovementWESP); MoveWE(dir == 0 ? DIRECTION_WEST : DIRECTION_EAST, MOTION_STOP); } if (GuideWETID) { IERmTimer(GuideWETID); GuideWETID = 0; } if (use_pulse_cmd) { SendPulseCmd(LX200_WEST, ms); } else { if (setSlewMode(PortFD, LX200_SLEW_GUIDE) < 0) { SlewRateSP.s = IPS_ALERT; IDSetSwitch(&SlewRateSP, "Error setting slew mode."); return IPS_ALERT; } MovementWES[0].s = ISS_ON; MoveWE(DIRECTION_WEST, MOTION_START); } // Set slew to guiding IUResetSwitch(&SlewRateSP); SlewRateS[SLEW_GUIDE].s = ISS_ON; IDSetSwitch(&SlewRateSP, nullptr); guide_direction = LX200_WEST; GuideWETID = IEAddTimer(ms, guideTimeoutHelper, this); return IPS_BUSY; } int LX200Telescope::SendPulseCmd(int direction, int duration_msec) { return ::SendPulseCmd(PortFD, direction, duration_msec); } void LX200Telescope::guideTimeoutHelper(void * p) { ((LX200Telescope *)p)->guideTimeout(); } void LX200Telescope::guideTimeout() { int use_pulse_cmd; use_pulse_cmd = IUFindOnSwitchIndex(&UsePulseCmdSP); if (guide_direction == -1) { HaltMovement(PortFD, LX200_NORTH); HaltMovement(PortFD, LX200_SOUTH); HaltMovement(PortFD, LX200_EAST); HaltMovement(PortFD, LX200_WEST); MovementNSSP.s = IPS_IDLE; MovementWESP.s = IPS_IDLE; IUResetSwitch(&MovementNSSP); IUResetSwitch(&MovementWESP); IDSetSwitch(&MovementNSSP, nullptr); IDSetSwitch(&MovementWESP, nullptr); IERmTimer(GuideNSTID); IERmTimer(GuideWETID); } else if (!use_pulse_cmd) { if (guide_direction == LX200_NORTH || guide_direction == LX200_SOUTH) { MoveNS(guide_direction == LX200_NORTH ? DIRECTION_NORTH : DIRECTION_SOUTH, MOTION_STOP); if (guide_direction == LX200_NORTH) GuideNSNP.np[0].value = 0; else GuideNSNP.np[1].value = 0; GuideNSNP.s = IPS_IDLE; IDSetNumber(&GuideNSNP, nullptr); MovementNSSP.s = IPS_IDLE; IUResetSwitch(&MovementNSSP); IDSetSwitch(&MovementNSSP, nullptr); } if (guide_direction == LX200_WEST || guide_direction == LX200_EAST) { MoveWE(guide_direction == LX200_WEST ? DIRECTION_WEST : DIRECTION_EAST, MOTION_STOP); if (guide_direction == LX200_WEST) GuideWENP.np[0].value = 0; else GuideWENP.np[1].value = 0; GuideWENP.s = IPS_IDLE; IDSetNumber(&GuideWENP, nullptr); MovementWESP.s = IPS_IDLE; IUResetSwitch(&MovementWESP); IDSetSwitch(&MovementWESP, nullptr); } } if (guide_direction == LX200_NORTH || guide_direction == LX200_SOUTH || guide_direction == -1) { GuideNSNP.np[0].value = 0; GuideNSNP.np[1].value = 0; GuideNSNP.s = IPS_IDLE; GuideNSTID = 0; IDSetNumber(&GuideNSNP, nullptr); } if (guide_direction == LX200_WEST || guide_direction == LX200_EAST || guide_direction == -1) { GuideWENP.np[0].value = 0; GuideWENP.np[1].value = 0; GuideWENP.s = IPS_IDLE; GuideWETID = 0; IDSetNumber(&GuideWENP, nullptr); } } bool LX200Telescope::saveConfigItems(FILE *fp) { INDI::Telescope::saveConfigItems(fp); if (genericCapability & LX200_HAS_PULSE_GUIDING) IUSaveConfigSwitch(fp, &UsePulseCmdSP); return true; } libindi/drivers/telescope/lx200gps.h0000664000175000017500000000366413263645557016674 0ustar jasemjasem/* LX200 GPS Copyright (C) 2003 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 */ #pragma once #include "lx200autostar.h" class LX200GPS : public LX200Autostar { public: LX200GPS(); ~LX200GPS() {} const char *getDefaultName(); bool initProperties(); bool updateProperties(); void ISGetProperties(const char *dev); bool ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n); virtual bool updateTime(ln_date *utc, double utc_offset); protected: virtual bool UnPark(); ISwitchVectorProperty GPSPowerSP; ISwitch GPSPowerS[2]; ISwitchVectorProperty GPSStatusSP; ISwitch GPSStatusS[3]; ISwitchVectorProperty GPSUpdateSP; ISwitch GPSUpdateS[2]; ISwitchVectorProperty AltDecPecSP; ISwitch AltDecPecS[2]; ISwitchVectorProperty AzRaPecSP; ISwitch AzRaPecS[2]; ISwitchVectorProperty SelenSyncSP; ISwitch SelenSyncS[1]; ISwitchVectorProperty AltDecBacklashSP; ISwitch AltDecBacklashS[1]; ISwitchVectorProperty AzRaBacklashSP; ISwitch AzRaBacklashS[1]; ISwitchVectorProperty OTAUpdateSP; ISwitch OTAUpdateS[1]; INumberVectorProperty OTATempNP; INumber OTATempN[1]; }; libindi/drivers/telescope/lx200basic.cpp0000664000175000017500000003457013263645557017517 0ustar jasemjasem#if 0 LX200 Basic Driver Copyright (C) 2015 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 "lx200basic.h" #include "indicom.h" #include "lx200driver.h" #include #include #include #include #include /* Simulation Parameters */ #define SLEWRATE 1 /* slew rate, degrees/s */ #define SIDRATE 0.004178 /* sidereal rate, degrees/s */ /* Our telescope auto pointer */ std::unique_ptr telescope(new LX200Basic()); /************************************************************************************** ** Send client definitions of all properties. ***************************************************************************************/ /************************************************************************************** ** ***************************************************************************************/ void ISGetProperties(const char *dev) { telescope->ISGetProperties(dev); } /************************************************************************************** ** ***************************************************************************************/ void ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) { telescope->ISNewSwitch(dev, name, states, names, n); } /************************************************************************************** ** ***************************************************************************************/ void ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n) { telescope->ISNewText(dev, name, texts, names, n); } /************************************************************************************** ** ***************************************************************************************/ void ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) { telescope->ISNewNumber(dev, name, values, names, n); } /************************************************************************************** ** ***************************************************************************************/ void ISNewBLOB(const char *dev, const char *name, int sizes[], int blobsizes[], char *blobs[], char *formats[], char *names[], int n) { INDI_UNUSED(dev); INDI_UNUSED(name); INDI_UNUSED(sizes); INDI_UNUSED(blobsizes); INDI_UNUSED(blobs); INDI_UNUSED(formats); INDI_UNUSED(names); INDI_UNUSED(n); } /************************************************************************************** ** ***************************************************************************************/ void ISSnoopDevice(XMLEle *root) { INDI_UNUSED(root); } /************************************************************************************** ** LX200 Basic constructor ***************************************************************************************/ LX200Basic::LX200Basic() { setVersion(2, 0); DBG_SCOPE = INDI::Logger::getInstance().addDebugLevel("Scope Verbose", "SCOPE"); double longitude=0, latitude=90; // Get value from config file if it exists. IUGetConfigNumber(getDeviceName(), "GEOGRAPHIC_COORD", "LONG", &longitude); currentRA = get_local_sidereal_time(longitude); IUGetConfigNumber(getDeviceName(), "GEOGRAPHIC_COORD", "LAT", &latitude); currentDEC = latitude > 0 ? 90 : -90; SetTelescopeCapability(TELESCOPE_CAN_SYNC | TELESCOPE_CAN_GOTO | TELESCOPE_CAN_ABORT); LOG_DEBUG("Initializing from LX200 Basic device..."); } /************************************************************************************** ** ***************************************************************************************/ void LX200Basic::debugTriggered(bool enable) { INDI_UNUSED(enable); setLX200Debug(getDeviceName(), DBG_SCOPE); } /************************************************************************************** ** ***************************************************************************************/ const char *LX200Basic::getDefaultName() { return (const char *)"LX200 Basic"; } /************************************************************************************** ** ***************************************************************************************/ bool LX200Basic::initProperties() { /* Make sure to init parent properties first */ INDI::Telescope::initProperties(); // Slew threshold IUFillNumber(&SlewAccuracyN[0], "SlewRA", "RA (arcmin)", "%10.6m", 0., 60., 1., 3.0); IUFillNumber(&SlewAccuracyN[1], "SlewDEC", "Dec (arcmin)", "%10.6m", 0., 60., 1., 3.0); IUFillNumberVector(&SlewAccuracyNP, SlewAccuracyN, NARRAY(SlewAccuracyN), getDeviceName(), "Slew Accuracy", "", OPTIONS_TAB, IP_RW, 0, IPS_IDLE); addAuxControls(); return true; } /************************************************************************************** ** Define LX200 Basic properties to clients. ***************************************************************************************/ void LX200Basic::ISGetProperties(const char *dev) { if (dev != nullptr && strcmp(dev, getDeviceName()) != 0) return; INDI::Telescope::ISGetProperties(dev); //if (isConnected()) // defineNumber(&SlewAccuracyNP); } /************************************************************************************** ** ***************************************************************************************/ bool LX200Basic::updateProperties() { INDI::Telescope::updateProperties(); if (isConnected()) { defineNumber(&SlewAccuracyNP); // We don't support NSWE controls deleteProperty(MovementNSSP.name); deleteProperty(MovementWESP.name); getBasicData(); } else { deleteProperty(SlewAccuracyNP.name); } return true; } /************************************************************************************** ** ***************************************************************************************/ bool LX200Basic::Handshake() { if (getLX200RA(PortFD, ¤tRA) != 0) { LOG_ERROR("Error communication with telescope."); return false; } return true; } /************************************************************************************** ** ***************************************************************************************/ bool LX200Basic::isSlewComplete() { const double dx = targetRA - currentRA; const double dy = targetDEC - currentDEC; return fabs(dx) <= (SlewAccuracyN[0].value / (900.0)) && fabs(dy) <= (SlewAccuracyN[1].value / 60.0); } /************************************************************************************** ** ***************************************************************************************/ bool LX200Basic::ReadScopeStatus() { if (!isConnected()) return false; if (isSimulation()) { mountSim(); return true; } if (getLX200RA(PortFD, ¤tRA) < 0 || getLX200DEC(PortFD, ¤tDEC) < 0) { EqNP.s = IPS_ALERT; IDSetNumber(&EqNP, "Error reading RA/DEC."); return false; } if (TrackState == SCOPE_SLEWING) { // Check if LX200 is done slewing if (isSlewComplete()) { TrackState = SCOPE_TRACKING; LOG_INFO("Slew is complete. Tracking..."); } } NewRaDec(currentRA, currentDEC); return true; } /************************************************************************************** ** ***************************************************************************************/ bool LX200Basic::Goto(double r, double d) { targetRA = r; targetDEC = d; char RAStr[64]={0}, DecStr[64]={0}; fs_sexa(RAStr, targetRA, 2, 3600); fs_sexa(DecStr, targetDEC, 2, 3600); // If moving, let's stop it first. if (EqNP.s == IPS_BUSY) { if (!isSimulation() && abortSlew(PortFD) < 0) { AbortSP.s = IPS_ALERT; IDSetSwitch(&AbortSP, "Abort slew failed."); return false; } AbortSP.s = IPS_OK; EqNP.s = IPS_IDLE; IDSetSwitch(&AbortSP, "Slew aborted."); IDSetNumber(&EqNP, nullptr); // sleep for 100 mseconds usleep(100000); } if (!isSimulation()) { if (setObjectRA(PortFD, targetRA) < 0 || (setObjectDEC(PortFD, targetDEC)) < 0) { EqNP.s = IPS_ALERT; IDSetNumber(&EqNP, "Error setting RA/DEC."); return false; } int err = 0; /* Slew reads the '0', that is not the end of the slew */ if ((err = Slew(PortFD))) { EqNP.s = IPS_ALERT; IDSetNumber(&EqNP, "Error Slewing to JNow RA %s - DEC %s\n", RAStr, DecStr); slewError(err); return false; } } TrackState = SCOPE_SLEWING; EqNP.s = IPS_BUSY; LOGF_INFO("Slewing to RA: %s - DEC: %s", RAStr, DecStr); return true; } /************************************************************************************** ** ***************************************************************************************/ bool LX200Basic::Sync(double ra, double dec) { char syncString[256]={0}; if (!isSimulation() && (setObjectRA(PortFD, ra) < 0 || (setObjectDEC(PortFD, dec)) < 0)) { EqNP.s = IPS_ALERT; IDSetNumber(&EqNP, "Error setting RA/DEC. Unable to Sync."); return false; } if (!isSimulation() && ::Sync(PortFD, syncString) < 0) { EqNP.s = IPS_ALERT; IDSetNumber(&EqNP, "Synchronization failed."); return false; } currentRA = ra; currentDEC = dec; LOG_INFO("Synchronization successful."); EqNP.s = IPS_OK; NewRaDec(currentRA, currentDEC); return true; } /************************************************************************************** ** ***************************************************************************************/ bool LX200Basic::ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) { if (dev != nullptr && strcmp(dev, getDeviceName()) == 0) { if (!strcmp(name, SlewAccuracyNP.name)) { if (IUUpdateNumber(&SlewAccuracyNP, values, names, n) < 0) return false; SlewAccuracyNP.s = IPS_OK; if (SlewAccuracyN[0].value < 3 || SlewAccuracyN[1].value < 3) IDSetNumber(&SlewAccuracyNP, "Warning: Setting the slew accuracy too low may result in a dead lock"); IDSetNumber(&SlewAccuracyNP, nullptr); return true; } } return INDI::Telescope::ISNewNumber(dev, name, values, names, n); } /************************************************************************************** ** ***************************************************************************************/ bool LX200Basic::Abort() { if (!isSimulation() && abortSlew(PortFD) < 0) { LOG_ERROR("Failed to abort slew."); return false; } EqNP.s = IPS_IDLE; TrackState = SCOPE_IDLE; IDSetNumber(&EqNP, nullptr); LOG_INFO("Slew aborted."); return true; } /************************************************************************************** ** ***************************************************************************************/ void LX200Basic::getBasicData() { // Make sure short checkLX200Format(PortFD); // Get current RA/DEC getLX200RA(PortFD, ¤tRA); getLX200DEC(PortFD, ¤tDEC); IDSetNumber(&EqNP, nullptr); } /************************************************************************************** ** ***************************************************************************************/ void LX200Basic::mountSim() { static struct timeval ltv; struct timeval tv; double dt, da, dx; int nlocked; /* update elapsed time since last poll, don't presume exactly POLLMS */ gettimeofday(&tv, nullptr); if (ltv.tv_sec == 0 && ltv.tv_usec == 0) ltv = tv; dt = tv.tv_sec - ltv.tv_sec + (tv.tv_usec - ltv.tv_usec) / 1e6; ltv = tv; da = SLEWRATE * dt; /* Process per current state. We check the state of EQUATORIAL_COORDS and act acoordingly */ switch (TrackState) { case SCOPE_TRACKING: /* RA moves at sidereal, Dec stands still */ currentRA += (SIDRATE * dt / 15.); break; case SCOPE_SLEWING: /* slewing - nail it when both within one pulse @ SLEWRATE */ nlocked = 0; dx = targetRA - currentRA; if (fabs(dx) <= da) { currentRA = targetRA; nlocked++; } else if (dx > 0) currentRA += da / 15.; else currentRA -= da / 15.; dx = targetDEC - currentDEC; if (fabs(dx) <= da) { currentDEC = targetDEC; nlocked++; } else if (dx > 0) currentDEC += da; else currentDEC -= da; if (nlocked == 2) { TrackState = SCOPE_TRACKING; } break; default: break; } NewRaDec(currentRA, currentDEC); } /************************************************************************************** ** ***************************************************************************************/ void LX200Basic::slewError(int slewCode) { EqNP.s = IPS_ALERT; if (slewCode == 1) IDSetNumber(&EqNP, "Object below horizon."); else if (slewCode == 2) IDSetNumber(&EqNP, "Object below the minimum elevation limit."); else IDSetNumber(&EqNP, "Slew failed."); } libindi/drivers/telescope/skywatcherAPIMount.cpp0000664000175000017500000022727313263645557021415 0ustar jasemjasem/*! * \file skywatcherAPIMount.cpp * * \author Roger James * \author Gerry Rozema * \author Jean-Luc Geehalel * \date 13th November 2013 * * This file contains the implementation in C++ of a INDI telescope driver using the Skywatcher API. * It is based on work from three sources. * A C++ implementation of the API by Roger James. * The indi_eqmod driver by Jean-Luc Geehalel. * The synscanmount driver by Gerry Rozema. */ #include "skywatcherAPIMount.h" #include "indicom.h" #include "alignment/DriverCommon.h" #include "connectionplugins/connectionserial.h" #include #include using namespace INDI::AlignmentSubsystem; // We declare an auto pointer to SkywatcherAPIMount. std::unique_ptr SkywatcherAPIMountPtr(new SkywatcherAPIMount()); /* Preset Slew Speeds */ #define SLEWMODES 9 double SlewSpeeds[SLEWMODES] = { 1.0, 2.0, 4.0, 8.0, 16.0, 32.0, 64.0, 128.0, 600.0 }; void ISPoll(void *p); namespace { bool FileExists(const std::string &name) { std::ifstream File(name.c_str()); return File.good(); } } // namespace void ISGetProperties(const char *dev) { SkywatcherAPIMountPtr->ISGetProperties(dev); } void ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) { SkywatcherAPIMountPtr->ISNewSwitch(dev, name, states, names, n); } void ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n) { SkywatcherAPIMountPtr->ISNewText(dev, name, texts, names, n); } void ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) { SkywatcherAPIMountPtr->ISNewNumber(dev, name, values, names, n); } void ISNewBLOB(const char *dev, const char *name, int sizes[], int blobsizes[], char *blobs[], char *formats[], char *names[], int n) { SkywatcherAPIMountPtr->ISNewBLOB(dev, name, sizes, blobsizes, blobs, formats, names, n); } void ISSnoopDevice(XMLEle *root) { INDI_UNUSED(root); } SkywatcherAPIMount::SkywatcherAPIMount() { // Set up the logging pointer in SkyWatcherAPI pChildTelescope = this; #ifdef USE_INITIAL_JULIAN_DATE InitialJulianDate = ln_get_julian_from_sys(); #endif SetTelescopeCapability(TELESCOPE_CAN_PARK | TELESCOPE_CAN_SYNC | TELESCOPE_CAN_GOTO | TELESCOPE_CAN_ABORT | TELESCOPE_HAS_TIME | TELESCOPE_HAS_LOCATION, SLEWMODES); } bool SkywatcherAPIMount::Abort() { DEBUG(DBG_SCOPE, "SkywatcherAPIMount::Abort"); SlowStop(AXIS1); SlowStop(AXIS2); TrackState = SCOPE_IDLE; if (GuideNSNP.s == IPS_BUSY || GuideWENP.s == IPS_BUSY) { GuideNSNP.s = GuideWENP.s = IPS_IDLE; GuideNSN[0].value = GuideNSN[1].value = 0.0; GuideWEN[0].value = GuideWEN[1].value = 0.0; IDMessage(getDeviceName(), "Guide aborted."); IDSetNumber(&GuideNSNP, nullptr); IDSetNumber(&GuideWENP, nullptr); return true; } return true; } bool SkywatcherAPIMount::Handshake() { DEBUG(DBG_SCOPE, "SkywatcherAPIMount::Handshake"); SetSerialPort(PortFD); bool Result = InitMount(RecoverAfterReconnection); if (getActiveConnection() == serialConnection) { SerialPortName = serialConnection->port(); } else { SerialPortName = ""; } // The default slew mode is silent on Virtuoso mounts. if (Result && !RecoverAfterReconnection && IsVirtuosoMount() && IUFindSwitch(&SlewModesSP, "SLEW_SILENT") != nullptr && IUFindSwitch(&SlewModesSP, "SLEW_NORMAL") != nullptr) { IUFindSwitch(&SlewModesSP, "SLEW_SILENT")->s = ISS_ON; IUFindSwitch(&SlewModesSP, "SLEW_NORMAL")->s = ISS_OFF; } // The SoftPEC is enabled on Virtuoso mounts by default. if (Result && !RecoverAfterReconnection && IsVirtuosoMount() && IUFindSwitch(&SoftPECModesSP, "SOFTPEC_ENABLED") != nullptr && IUFindSwitch(&SoftPECModesSP, "SOFTPEC_DISABLED") != nullptr) { IUFindSwitch(&SoftPECModesSP, "SOFTPEC_ENABLED")->s = ISS_ON; IUFindSwitch(&SoftPECModesSP, "SOFTPEC_DISABLED")->s = ISS_OFF; } // The default position is parking on Virtuoso mounts (the telescope is oriented to polar). if (Result && !RecoverAfterReconnection && IsVirtuosoMount()) { SetParked(true); } // The default mode is Slew out of Track/Slew/Sync if (!RecoverAfterReconnection && IUFindSwitch(&CoordSP, "TRACK") != nullptr && IUFindSwitch(&CoordSP, "SLEW") != nullptr && IUFindSwitch(&CoordSP, "SYNC") != nullptr) { IUFindSwitch(&CoordSP, "TRACK")->s = ISS_OFF; IUFindSwitch(&CoordSP, "SLEW")->s = ISS_ON; IUFindSwitch(&CoordSP, "SYNC")->s = ISS_OFF; } RecoverAfterReconnection = false; DEBUGF(DBG_SCOPE, "SkywatcherAPIMount::Handshake - Result: %d", Result); return Result; } const char *SkywatcherAPIMount::getDefaultName() { //DEBUG(DBG_SCOPE, "SkywatcherAPIMount::getDefaultName\n"); return "Skywatcher Alt-Az"; } bool SkywatcherAPIMount::Goto(double ra, double dec) { DEBUG(INDI::AlignmentSubsystem::DBG_ALIGNMENT, "SkywatcherAPIMount::Goto"); if (TrackState != SCOPE_IDLE) Abort(); DEBUGF(INDI::AlignmentSubsystem::DBG_ALIGNMENT, "RA %lf DEC %lf", ra, dec); if (IUFindSwitch(&CoordSP, "TRACK")->s == ISS_ON || IUFindSwitch(&CoordSP, "SLEW")->s == ISS_ON) { char RAStr[32], DecStr[32]; fs_sexa(RAStr, ra, 2, 3600); fs_sexa(DecStr, dec, 2, 3600); CurrentTrackingTarget.ra = ra; CurrentTrackingTarget.dec = dec; LOGF_INFO("New Tracking target RA %s DEC %s", RAStr, DecStr); } ln_hrz_posn AltAz { 0, 0 }; TelescopeDirectionVector TDV; if (TransformCelestialToTelescope(ra, dec, 0.0, TDV)) { DEBUGF(INDI::AlignmentSubsystem::DBG_ALIGNMENT, "TDV x %lf y %lf z %lf", TDV.x, TDV.y, TDV.z); AltitudeAzimuthFromTelescopeDirectionVector(TDV, AltAz); DEBUG(INDI::AlignmentSubsystem::DBG_ALIGNMENT, "Conversion OK"); } else { // Try a conversion with the stored observatory position if any bool HavePosition = false; ln_lnlat_posn Position { 0, 0 }; if ((nullptr != IUFindNumber(&LocationNP, "LAT")) && (0 != IUFindNumber(&LocationNP, "LAT")->value) && (nullptr != IUFindNumber(&LocationNP, "LONG")) && (0 != IUFindNumber(&LocationNP, "LONG")->value)) { // I assume that being on the equator and exactly on the prime meridian is unlikely Position.lat = IUFindNumber(&LocationNP, "LAT")->value; Position.lng = IUFindNumber(&LocationNP, "LONG")->value; HavePosition = true; } ln_equ_posn EquatorialCoordinates { 0, 0 }; // libnova works in decimal degrees EquatorialCoordinates.ra = ra * 360.0 / 24.0; EquatorialCoordinates.dec = dec; if (HavePosition) { #ifdef USE_INITIAL_JULIAN_DATE ln_get_hrz_from_equ(&EquatorialCoordinates, &Position, InitialJulianDate, &AltAz); #else ln_get_hrz_from_equ(&EquatorialCoordinates, &Position, ln_get_julian_from_sys(), &AltAz); #endif TDV = TelescopeDirectionVectorFromAltitudeAzimuth(AltAz); switch (GetApproximateMountAlignment()) { case ZENITH: break; case NORTH_CELESTIAL_POLE: // Rotate the TDV coordinate system clockwise (negative) around the y axis by 90 minus // the (positive)observatory latitude. The vector itself is rotated anticlockwise TDV.RotateAroundY(Position.lat - 90.0); break; case SOUTH_CELESTIAL_POLE: // Rotate the TDV coordinate system anticlockwise (positive) around the y axis by 90 plus // the (negative)observatory latitude. The vector itself is rotated clockwise TDV.RotateAroundY(Position.lat + 90.0); break; } AltitudeAzimuthFromTelescopeDirectionVector(TDV, AltAz); } else { // The best I can do is just do a direct conversion to Alt/Az TDV = TelescopeDirectionVectorFromEquatorialCoordinates(EquatorialCoordinates); AltitudeAzimuthFromTelescopeDirectionVector(TDV, AltAz); } DEBUGF(INDI::AlignmentSubsystem::DBG_ALIGNMENT, "Conversion Failed - HavePosition %d", HavePosition); } if (IsVirtuosoMount()) { // The initial position of the Virtuoso mount is polar aligned when switched on. // The altitude is corrected by the latitude. if (IUFindNumber(&LocationNP, "LAT") != nullptr) AltAz.alt = AltAz.alt - IUFindNumber(&LocationNP, "LAT")->value; AltAz.az = 180 + AltAz.az; } DEBUGF(INDI::AlignmentSubsystem::DBG_ALIGNMENT, "New Altitude %lf degrees %ld microsteps Azimuth %lf degrees %ld microsteps", AltAz.alt, DegreesToMicrosteps(AXIS2, AltAz.alt), AltAz.az, DegreesToMicrosteps(AXIS1, AltAz.az)); // Update the current encoder positions GetEncoder(AXIS1); GetEncoder(AXIS2); long AltitudeOffsetMicrosteps = DegreesToMicrosteps(AXIS2, AltAz.alt) + ZeroPositionEncoders[AXIS2] - CurrentEncoders[AXIS2]; long AzimuthOffsetMicrosteps = DegreesToMicrosteps(AXIS1, AltAz.az) + ZeroPositionEncoders[AXIS1] - CurrentEncoders[AXIS1]; // Do I need to take out any complete revolutions before I do this test? if (AltitudeOffsetMicrosteps > MicrostepsPerRevolution[AXIS2] / 2) { // Going the long way round - send it the other way AltitudeOffsetMicrosteps -= MicrostepsPerRevolution[AXIS2]; } if (AzimuthOffsetMicrosteps > MicrostepsPerRevolution[AXIS1] / 2) { // Going the long way round - send it the other way AzimuthOffsetMicrosteps -= MicrostepsPerRevolution[AXIS1]; } DEBUGF(INDI::AlignmentSubsystem::DBG_ALIGNMENT, "Initial Axis2 %ld microsteps Axis1 %ld microsteps", ZeroPositionEncoders[AXIS2], ZeroPositionEncoders[AXIS1]); DEBUGF(INDI::AlignmentSubsystem::DBG_ALIGNMENT, "Current Axis2 %ld microsteps Axis1 %ld microsteps", CurrentEncoders[AXIS2], CurrentEncoders[AXIS1]); DEBUGF(INDI::AlignmentSubsystem::DBG_ALIGNMENT, "Altitude offset %ld microsteps Azimuth offset %ld microsteps", AltitudeOffsetMicrosteps, AzimuthOffsetMicrosteps); if (IUFindSwitch(&SlewModesSP, "SLEW_SILENT") != nullptr && IUFindSwitch(&SlewModesSP, "SLEW_SILENT")->s == ISS_ON) { SilentSlewMode = true; } else { SilentSlewMode = false; } SlewTo(AXIS1, AzimuthOffsetMicrosteps); SlewTo(AXIS2, AltitudeOffsetMicrosteps); TrackState = SCOPE_SLEWING; EqNP.s = IPS_BUSY; return true; } bool SkywatcherAPIMount::initProperties() { IDLog("SkywatcherAPIMount::initProperties\n"); // Allow the base class to initialise its visible before connection properties INDI::Telescope::initProperties(); for (int i = 0; i < SlewRateSP.nsp; ++i) { sprintf(SlewRateSP.sp[i].label, "%.fx", SlewSpeeds[i]); SlewRateSP.sp[i].aux = (void *)&SlewSpeeds[i]; } strncpy(SlewRateSP.sp[SlewRateSP.nsp - 1].name, "SLEW_MAX", MAXINDINAME); // Add default properties addDebugControl(); addConfigurationControl(); // Add alignment properties InitAlignmentProperties(this); // Force the alignment system to always be on getSwitch("ALIGNMENT_SUBSYSTEM_ACTIVE")->sp[0].s = ISS_ON; // Set up property variables IUFillText(&BasicMountInfo[MOTOR_CONTROL_FIRMWARE_VERSION], "MOTOR_CONTROL_FIRMWARE_VERSION", "Motor control firmware version", "-"); IUFillText(&BasicMountInfo[MOUNT_CODE], "MOUNT_CODE", "Mount code", "-"); IUFillText(&BasicMountInfo[MOUNT_NAME], "MOUNT_NAME", "Mount name", "-"); IUFillText(&BasicMountInfo[IS_DC_MOTOR], "IS_DC_MOTOR", "Is DC motor", "-"); IUFillTextVector(&BasicMountInfoV, BasicMountInfo, 4, getDeviceName(), "BASIC_MOUNT_INFO", "Basic mount information", DetailedMountInfoPage, IP_RO, 60, IPS_IDLE); IUFillNumber(&AxisOneInfo[MICROSTEPS_PER_REVOLUTION], "MICROSTEPS_PER_REVOLUTION", "Microsteps per revolution", "%.0f", 0, 0xFFFFFF, 1, 0); IUFillNumber(&AxisOneInfo[STEPPER_CLOCK_FREQUENCY], "STEPPER_CLOCK_FREQUENCY", "Stepper clock frequency", "%.0f", 0, 0xFFFFFF, 1, 0); IUFillNumber(&AxisOneInfo[HIGH_SPEED_RATIO], "HIGH_SPEED_RATIO", "High speed ratio", "%.0f", 0, 0xFFFFFF, 1, 0); IUFillNumber(&AxisOneInfo[MICROSTEPS_PER_WORM_REVOLUTION], "MICROSTEPS_PER_WORM_REVOLUTION", "Microsteps per worm revolution", "%.0f", 0, 0xFFFFFF, 1, 0); IUFillNumberVector(&AxisOneInfoV, AxisOneInfo, 4, getDeviceName(), "AXIS_ONE_INFO", "Axis one information", DetailedMountInfoPage, IP_RO, 60, IPS_IDLE); IUFillSwitch(&AxisOneState[FULL_STOP], "FULL_STOP", "FULL_STOP", ISS_OFF); IUFillSwitch(&AxisOneState[SLEWING], "SLEWING", "SLEWING", ISS_OFF); IUFillSwitch(&AxisOneState[SLEWING_TO], "SLEWING_TO", "SLEWING_TO", ISS_OFF); IUFillSwitch(&AxisOneState[SLEWING_FORWARD], "SLEWING_FORWARD", "SLEWING_FORWARD", ISS_OFF); IUFillSwitch(&AxisOneState[HIGH_SPEED], "HIGH_SPEED", "HIGH_SPEED", ISS_OFF); IUFillSwitch(&AxisOneState[NOT_INITIALISED], "NOT_INITIALISED", "NOT_INITIALISED", ISS_ON); IUFillSwitchVector(&AxisOneStateV, AxisOneState, 6, getDeviceName(), "AXIS_ONE_STATE", "Axis one state", DetailedMountInfoPage, IP_RO, ISR_NOFMANY, 60, IPS_IDLE); IUFillNumber(&AxisTwoInfo[MICROSTEPS_PER_REVOLUTION], "MICROSTEPS_PER_REVOLUTION", "Microsteps per revolution", "%.0f", 0, 0xFFFFFF, 1, 0); IUFillNumber(&AxisTwoInfo[STEPPER_CLOCK_FREQUENCY], "STEPPER_CLOCK_FREQUENCY", "Step timer frequency", "%.0f", 0, 0xFFFFFF, 1, 0); IUFillNumber(&AxisTwoInfo[HIGH_SPEED_RATIO], "HIGH_SPEED_RATIO", "High speed ratio", "%.0f", 0, 0xFFFFFF, 1, 0); IUFillNumber(&AxisTwoInfo[MICROSTEPS_PER_WORM_REVOLUTION], "MICROSTEPS_PER_WORM_REVOLUTION", "Mictosteps per worm revolution", "%.0f", 0, 0xFFFFFF, 1, 0); IUFillNumberVector(&AxisTwoInfoV, AxisTwoInfo, 4, getDeviceName(), "AXIS_TWO_INFO", "Axis two information", DetailedMountInfoPage, IP_RO, 60, IPS_IDLE); IUFillSwitch(&AxisTwoState[FULL_STOP], "FULL_STOP", "FULL_STOP", ISS_OFF); IUFillSwitch(&AxisTwoState[SLEWING], "SLEWING", "SLEWING", ISS_OFF); IUFillSwitch(&AxisTwoState[SLEWING_TO], "SLEWING_TO", "SLEWING_TO", ISS_OFF); IUFillSwitch(&AxisTwoState[SLEWING_FORWARD], "SLEWING_FORWARD", "SLEWING_FORWARD", ISS_OFF); IUFillSwitch(&AxisTwoState[HIGH_SPEED], "HIGH_SPEED", "HIGH_SPEED", ISS_OFF); IUFillSwitch(&AxisTwoState[NOT_INITIALISED], "NOT_INITIALISED", "NOT_INITIALISED", ISS_ON); IUFillSwitchVector(&AxisTwoStateV, AxisTwoState, 6, getDeviceName(), "AXIS_TWO_STATE", "Axis two state", DetailedMountInfoPage, IP_RO, ISR_NOFMANY, 60, IPS_IDLE); IUFillNumber(&AxisOneEncoderValues[RAW_MICROSTEPS], "RAW_MICROSTEPS", "Raw Microsteps", "%.0f", 0, 0xFFFFFF, 1, 0); IUFillNumber(&AxisOneEncoderValues[MICROSTEPS_PER_ARCSEC], "MICROSTEPS_PER_ARCSEC", "Microsteps/arcsecond", "%.4f", 0, 0xFFFFFF, 1, 0); IUFillNumber(&AxisOneEncoderValues[OFFSET_FROM_INITIAL], "OFFSET_FROM_INITIAL", "Offset from initial", "%.0f", 0, 0xFFFFFF, 1, 0); IUFillNumber(&AxisOneEncoderValues[DEGREES_FROM_INITIAL], "DEGREES_FROM_INITIAL", "Degrees from initial", "%.2f", -1000.0, 1000.0, 1, 0); IUFillNumberVector(&AxisOneEncoderValuesV, AxisOneEncoderValues, 4, getDeviceName(), "AXIS1_ENCODER_VALUES", "Axis 1 Encoder values", DetailedMountInfoPage, IP_RO, 60, IPS_IDLE); IUFillNumber(&AxisTwoEncoderValues[RAW_MICROSTEPS], "RAW_MICROSTEPS", "Raw Microsteps", "%.0f", 0, 0xFFFFFF, 1, 0); IUFillNumber(&AxisTwoEncoderValues[MICROSTEPS_PER_ARCSEC], "MICROSTEPS_PER_ARCSEC", "Microsteps/arcsecond", "%.4f", 0, 0xFFFFFF, 1, 0); IUFillNumber(&AxisTwoEncoderValues[OFFSET_FROM_INITIAL], "OFFSET_FROM_INITIAL", "Offset from initial", "%.0f", 0, 0xFFFFFF, 1, 0); IUFillNumber(&AxisTwoEncoderValues[DEGREES_FROM_INITIAL], "DEGREES_FROM_INITIAL", "Degrees from initial", "%.2f", -1000.0, 1000.0, 1, 0); IUFillNumberVector(&AxisTwoEncoderValuesV, AxisTwoEncoderValues, 4, getDeviceName(), "AXIS2_ENCODER_VALUES", "Axis 2 Encoder values", DetailedMountInfoPage, IP_RO, 60, IPS_IDLE); // Register any visible before connection properties // Slew modes IUFillSwitch(&SlewModes[SLEW_SILENT], "SLEW_SILENT", "Silent", ISS_OFF); IUFillSwitch(&SlewModes[SLEW_NORMAL], "SLEW_NORMAL", "Normal", ISS_ON); IUFillSwitchVector(&SlewModesSP, SlewModes, 2, getDeviceName(), "TELESCOPE_MOTION_SLEWMODE", "Slew Mode", MOTION_TAB, IP_RW, ISR_ATMOST1, 60, IPS_IDLE); // SoftPEC modes IUFillSwitch(&SoftPECModes[SOFTPEC_ENABLED], "SOFTPEC_ENABLED", "Enable for tracking", ISS_OFF); IUFillSwitch(&SoftPECModes[SOFTPEC_DISABLED], "SOFTPEC_DISABLED", "Disabled", ISS_ON); IUFillSwitchVector(&SoftPECModesSP, SoftPECModes, 2, getDeviceName(), "TELESCOPE_MOTION_SOFTPECMODE", "SoftPEC Mode", MOTION_TAB, IP_RW, ISR_ATMOST1, 60, IPS_IDLE); // SoftPEC value for tracking mode IUFillNumber(&SoftPecN, "SOFTPEC_VALUE", "degree/minute (Alt)", "%1.3f", 0.001, 1.0, 0.001, 0.009); IUFillNumberVector(&SoftPecNP, &SoftPecN, 1, getDeviceName(), "SOFTPEC", "SoftPEC Value", MOTION_TAB, IP_RW, 60, IPS_IDLE); // Guiding rates for RA/DEC axes IUFillNumber(&GuidingRatesN[0], "GUIDERA_RATE", "arcsec/seconds (RA)", "%1.3f", 1.0, 6000.0, 1.0, 120.0); IUFillNumber(&GuidingRatesN[1], "GUIDEDEC_RATE", "arcsec/seconds (Dec)", "%1.3f", 1.0, 6000.0, 1.0, 120.0); IUFillNumberVector(&GuidingRatesNP, GuidingRatesN, 2, getDeviceName(), "GUIDE_RATES", "Guide Rates", MOTION_TAB, IP_RW, 60, IPS_IDLE); // Park movement directions IUFillSwitch(&ParkMovementDirection[PARK_COUNTERCLOCKWISE], "PMD_COUNTERCLOCKWISE", "Counterclockwise", ISS_ON); IUFillSwitch(&ParkMovementDirection[PARK_CLOCKWISE], "PMD_CLOCKWISE", "Clockwise", ISS_OFF); IUFillSwitchVector(&ParkMovementDirectionSP, ParkMovementDirection, 2, getDeviceName(), "PARK_DIRECTION", "Park Direction", MOTION_TAB, IP_RW, ISR_ATMOST1, 60, IPS_IDLE); // Park positions IUFillSwitch(&ParkPosition[PARK_NORTH], "PARK_NORTH", "North", ISS_ON); IUFillSwitch(&ParkPosition[PARK_EAST], "PARK_EAST", "East", ISS_OFF); IUFillSwitch(&ParkPosition[PARK_SOUTH], "PARK_SOUTH", "South", ISS_OFF); IUFillSwitch(&ParkPosition[PARK_WEST], "PARK_WEST", "West", ISS_OFF); IUFillSwitchVector(&ParkPositionSP, ParkPosition, 4, getDeviceName(), "PARK_POSITION", "Park Position", MOTION_TAB, IP_RW, ISR_ATMOST1, 60, IPS_IDLE); // Unpark positions IUFillSwitch(&UnparkPosition[PARK_NORTH], "UNPARK_NORTH", "North", ISS_OFF); IUFillSwitch(&UnparkPosition[PARK_EAST], "UNPARK_EAST", "East", ISS_OFF); IUFillSwitch(&UnparkPosition[PARK_SOUTH], "UNPARK_SOUTH", "South", ISS_ON); IUFillSwitch(&UnparkPosition[PARK_WEST], "UNPARK_WEST", "West", ISS_OFF); IUFillSwitchVector(&UnparkPositionSP, UnparkPosition, 4, getDeviceName(), "UNPARK_POSITION", "Unpark Position", MOTION_TAB, IP_RW, ISR_ATMOST1, 60, IPS_IDLE); // Guiding support // TODO: Hide the auto-guide support now because it is not production-ready // initGuiderProperties(getDeviceName(), GUIDE_TAB); // setDriverInterface(getDriverInterface() | GUIDER_INTERFACE); return true; } void SkywatcherAPIMount::ISGetProperties(const char *dev) { IDLog("SkywatcherAPIMount::ISGetProperties\n"); INDI::Telescope::ISGetProperties(dev); if (isConnected()) { // Fill in any real values now available MCInit should have been called already UpdateDetailedMountInformation(false); // Define our connected only properties to the base driver // e.g. defineNumber(MyNumberVectorPointer); // This will register our properties and send a IDDefXXXX mewssage to any connected clients defineText(&BasicMountInfoV); defineNumber(&AxisOneInfoV); defineSwitch(&AxisOneStateV); defineNumber(&AxisTwoInfoV); defineSwitch(&AxisTwoStateV); defineNumber(&AxisOneEncoderValuesV); defineNumber(&AxisTwoEncoderValuesV); defineSwitch(&SlewModesSP); defineSwitch(&SoftPECModesSP); defineNumber(&SoftPecNP); defineNumber(&GuidingRatesNP); defineSwitch(&ParkMovementDirectionSP); defineSwitch(&ParkPositionSP); defineSwitch(&UnparkPositionSP); defineNumber(&GuideNSNP); defineNumber(&GuideWENP); } } bool SkywatcherAPIMount::ISNewBLOB(const char *dev, const char *name, int sizes[], int blobsizes[], char *blobs[], char *formats[], char *names[], int n) { if (dev != nullptr && strcmp(dev, getDeviceName()) == 0) { // It is for us ProcessAlignmentBLOBProperties(this, name, sizes, blobsizes, blobs, formats, names, n); } // Pass it up the chain return INDI::Telescope::ISNewBLOB(dev, name, sizes, blobsizes, blobs, formats, names, n); } bool SkywatcherAPIMount::ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) { if (dev != nullptr && strcmp(dev, getDeviceName()) == 0) { ProcessAlignmentNumberProperties(this, name, values, names, n); if (strcmp(name, "SOFTPEC") == 0) { SoftPecNP.s = IPS_OK; IUUpdateNumber(&SoftPecNP, values, names, n); IDSetNumber(&SoftPecNP, nullptr); return true; } if (strcmp(name, "GUIDE_RATES") == 0) { ResetGuidePulses(); GuidingRatesNP.s = IPS_OK; IUUpdateNumber(&GuidingRatesNP, values, names, n); IDSetNumber(&GuidingRatesNP, nullptr); return true; } // Let our driver do sync operation in park position if (strcmp(name, "EQUATORIAL_EOD_COORD") == 0) { double ra = -1; double dec = -100; for (int x = 0; x < n; x++) { INumber *eqp = IUFindNumber(&EqNP, names[x]); if (eqp == &EqN[AXIS_RA]) { ra = values[x]; } else if (eqp == &EqN[AXIS_DE]) { dec = values[x]; } } if ((ra >= 0) && (ra <= 24) && (dec >= -90) && (dec <= 90)) { ISwitch *sw = IUFindSwitch(&CoordSP, "SYNC"); if (sw != nullptr && sw->s == ISS_ON && isParked()) { return Sync(ra, dec); } } } processGuiderProperties(name, values, names, n); } // Pass it up the chain return INDI::Telescope::ISNewNumber(dev, name, values, names, n); } bool SkywatcherAPIMount::ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) { IUUpdateSwitch(getSwitch(name), states, names, n); if (dev != nullptr && strcmp(dev, getDeviceName()) == 0) { // It is for us ProcessAlignmentSwitchProperties(this, name, states, names, n); } // Pass it up the chain return INDI::Telescope::ISNewSwitch(dev, name, states, names, n); } bool SkywatcherAPIMount::ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n) { if (dev != nullptr && strcmp(dev, getDeviceName()) == 0) { ProcessAlignmentTextProperties(this, name, texts, names, n); } // Pass it up the chain bool Ret = INDI::Telescope::ISNewText(dev, name, texts, names, n); // The scope config switch must be updated after the config is saved to disk if (dev != nullptr && strcmp(dev, getDeviceName()) == 0) { if (name && std::string(name) == "SCOPE_CONFIG_NAME") { UpdateScopeConfigSwitch(); } } return Ret; } void SkywatcherAPIMount::UpdateScopeConfigSwitch() { if (!CheckFile(ScopeConfigFileName, false)) { LOGF_INFO("Can't open XML file (%s) for read", ScopeConfigFileName.c_str()); return; } LilXML *XmlHandle = newLilXML(); FILE *FilePtr = fopen(ScopeConfigFileName.c_str(), "r"); XMLEle *RootXmlNode = nullptr; XMLEle *CurrentXmlNode = nullptr; XMLAtt *Ap = nullptr; bool DeviceFound = false; char ErrMsg[512]; RootXmlNode = readXMLFile(FilePtr, XmlHandle, ErrMsg); delLilXML(XmlHandle); XmlHandle = nullptr; if (!RootXmlNode) { LOGF_INFO("Failed to parse XML file (%s): %s", ScopeConfigFileName.c_str(), ErrMsg); return; } if (std::string(tagXMLEle(RootXmlNode)) != ScopeConfigRootXmlNode) { LOGF_INFO("Not a scope config XML file (%s)", ScopeConfigFileName.c_str()); delXMLEle(RootXmlNode); return; } CurrentXmlNode = nextXMLEle(RootXmlNode, 1); // Find the current telescope in the config file while (CurrentXmlNode) { if (std::string(tagXMLEle(CurrentXmlNode)) != ScopeConfigDeviceXmlNode) { CurrentXmlNode = nextXMLEle(RootXmlNode, 0); continue; } Ap = findXMLAtt(CurrentXmlNode, ScopeConfigNameXmlNode.c_str()); if (Ap && !strcmp(valuXMLAtt(Ap), getDeviceName())) { DeviceFound = true; break; } CurrentXmlNode = nextXMLEle(RootXmlNode, 0); } if (!DeviceFound) { LOGF_INFO("No a scope config found for %s in the XML file (%s)", getDeviceName(), ScopeConfigFileName.c_str()); delXMLEle(RootXmlNode); return; } // Read the values XMLEle *XmlNode = nullptr; XMLEle *DeviceXmlNode = CurrentXmlNode; std::string ConfigName; for (int i = 1; i < 7; ++i) { bool Found = true; CurrentXmlNode = findXMLEle(DeviceXmlNode, ("config"+std::to_string(i)).c_str()); if (CurrentXmlNode) { XmlNode = findXMLEle(CurrentXmlNode, ScopeConfigLabelApXmlNode.c_str()); if (XmlNode) { ConfigName = pcdataXMLEle(XmlNode); } } else { Found = false; } // Change the switch label ISwitch *configSwitch = IUFindSwitch(&ScopeConfigsSP, ("SCOPE_CONFIG"+std::to_string(i)).c_str()); if (configSwitch != nullptr) { // The config is not used yet if (!Found) { strncpy(configSwitch->label, ("Config #"+std::to_string(i)+" - Not used").c_str(), MAXINDILABEL); continue; } // Empty switch label if (ConfigName.empty()) { strncpy(configSwitch->label, ("Config #"+std::to_string(i)+" - Untitled").c_str(), MAXINDILABEL); continue; } strncpy(configSwitch->label, ("Config #"+std::to_string(i)+" - "+ConfigName).c_str(), MAXINDILABEL); } } delXMLEle(RootXmlNode); // Delete the joystick control to get the telescope config switch to the bottom of the page deleteProperty("USEJOYSTICK"); // Recreate the switch control deleteProperty(ScopeConfigsSP.name); defineSwitch(&ScopeConfigsSP); } double SkywatcherAPIMount::GetSlewRate() { ISwitch *Switch = IUFindOnSwitch(&SlewRateSP); double Rate = *((double *)Switch->aux); return Rate; } bool SkywatcherAPIMount::MoveNS(INDI_DIR_NS dir, TelescopeMotionCommand command) { DEBUG(DBG_SCOPE, "SkywatcherAPIMount::MoveNS"); double speed = (dir == DIRECTION_NORTH) ? GetSlewRate() * LOW_SPEED_MARGIN / 2 : -GetSlewRate() * LOW_SPEED_MARGIN / 2; const char *dirStr = (dir == DIRECTION_NORTH) ? "North" : "South"; switch (command) { case MOTION_START: DEBUGF(DBG_SCOPE, "Starting Slew %s", dirStr); // Ignore the silent mode because MoveNS() is called by the manual motion UI controls. Slew(AXIS2, speed, true); break; case MOTION_STOP: DEBUGF(DBG_SCOPE, "Stopping Slew %s", dirStr); SlowStop(AXIS2); break; } return true; } bool SkywatcherAPIMount::MoveWE(INDI_DIR_WE dir, TelescopeMotionCommand command) { DEBUG(DBG_SCOPE, "SkywatcherAPIMount::MoveWE"); double speed = (dir == DIRECTION_WEST) ? GetSlewRate() * LOW_SPEED_MARGIN / 2 : -GetSlewRate() * LOW_SPEED_MARGIN / 2; const char *dirStr = (dir == DIRECTION_WEST) ? "West" : "East"; if (IsVirtuosoMount()) speed = -speed; switch (command) { case MOTION_START: DEBUGF(DBG_SCOPE, "Starting Slew %s", dirStr); // Ignore the silent mode because MoveNS() is called by the manual motion UI controls. Slew(AXIS1, speed, true); break; case MOTION_STOP: DEBUGF(DBG_SCOPE, "Stopping Slew %s", dirStr); SlowStop(AXIS1); break; } return true; } double SkywatcherAPIMount::GetParkDeltaAz(ParkDirection_t target_direction, ParkPosition_t target_position) { double Result = 0; DEBUGF(INDI::AlignmentSubsystem::DBG_ALIGNMENT, "GetParkDeltaAz: direction %d - position: %d", (int)target_direction, (int)target_position); // Calculate delta degrees (target: NORTH) if (target_position == PARK_NORTH) { if (target_direction == PARK_COUNTERCLOCKWISE) { Result = -CurrentAltAz.az; } else { Result = 360 - CurrentAltAz.az; } } // Calculate delta degrees (target: EAST) if (target_position == PARK_EAST) { if (target_direction == PARK_COUNTERCLOCKWISE) { if (CurrentAltAz.az > 0 && CurrentAltAz.az < 90) Result = -270 - CurrentAltAz.az; else Result = -CurrentAltAz.az + 90; } else { if (CurrentAltAz.az > 0 && CurrentAltAz.az < 90) Result = 90 - CurrentAltAz.az; else Result = 360 - CurrentAltAz.az + 90; } } // Calculate delta degrees (target: SOUTH) if (target_position == PARK_SOUTH) { if (target_direction == PARK_COUNTERCLOCKWISE) { if (CurrentAltAz.az > 0 && CurrentAltAz.az < 180) Result = -180 - CurrentAltAz.az; else Result = -CurrentAltAz.az + 180; } else { if (CurrentAltAz.az > 0 && CurrentAltAz.az < 180) Result = 180 - CurrentAltAz.az; else Result = 360 - CurrentAltAz.az + 180; } } // Calculate delta degrees (target: WEST) if (target_position == PARK_WEST) { if (target_direction == PARK_COUNTERCLOCKWISE) { if (CurrentAltAz.az > 0 && CurrentAltAz.az < 270) Result = -90 - CurrentAltAz.az; else Result = -CurrentAltAz.az + 270; } else { if (CurrentAltAz.az > 0 && CurrentAltAz.az < 270) Result = 270 - CurrentAltAz.az; else Result = 360 - CurrentAltAz.az + 270; } } if (Result >= 360) { Result -= 360; } if (Result <= -360) { Result += 360; } return Result; } bool SkywatcherAPIMount::Park() { DEBUG(DBG_SCOPE, "SkywatcherAPIMount::Park"); ParkPosition_t TargetPosition = PARK_NORTH; ParkDirection_t TargetDirection = PARK_COUNTERCLOCKWISE; double DeltaAlt = 0; double DeltaAz = 0; // Determinate the target position and direction if (IUFindSwitch(&ParkPositionSP, "PARK_NORTH") != nullptr && IUFindSwitch(&ParkPositionSP, "PARK_NORTH")->s == ISS_ON) { TargetPosition = PARK_NORTH; } if (IUFindSwitch(&ParkPositionSP, "PARK_EAST") != nullptr && IUFindSwitch(&ParkPositionSP, "PARK_EAST")->s == ISS_ON) { TargetPosition = PARK_EAST; } if (IUFindSwitch(&ParkPositionSP, "PARK_SOUTH") != nullptr && IUFindSwitch(&ParkPositionSP, "PARK_SOUTH")->s == ISS_ON) { TargetPosition = PARK_SOUTH; } if (IUFindSwitch(&ParkPositionSP, "PARK_WEST") != nullptr && IUFindSwitch(&ParkPositionSP, "PARK_WEST")->s == ISS_ON) { TargetPosition = PARK_WEST; } if (IUFindSwitch(&ParkMovementDirectionSP, "PMD_COUNTERCLOCKWISE") != nullptr && IUFindSwitch(&ParkMovementDirectionSP, "PMD_COUNTERCLOCKWISE")->s == ISS_ON) { TargetDirection = PARK_COUNTERCLOCKWISE; } if (IUFindSwitch(&ParkMovementDirectionSP, "PMD_CLOCKWISE") != nullptr && IUFindSwitch(&ParkMovementDirectionSP, "PMD_CLOCKWISE")->s == ISS_ON) { TargetDirection = PARK_CLOCKWISE; } DeltaAz = GetParkDeltaAz(TargetDirection, TargetPosition); // Altitude 3440 points the telescope downwards DeltaAlt = CurrentAltAz.alt - 3440; // Move the telescope to the desired position long AltitudeOffsetMicrosteps = DegreesToMicrosteps(AXIS2, DeltaAlt); long AzimuthOffsetMicrosteps = DegreesToMicrosteps(AXIS1, DeltaAz); DEBUGF(INDI::AlignmentSubsystem::DBG_ALIGNMENT, "Parking: Delta altitude %1.2f - delta azimuth %1.2f", DeltaAlt, DeltaAz); DEBUGF(INDI::AlignmentSubsystem::DBG_ALIGNMENT, "Parking: Altitude offset %ld microsteps Azimuth offset %ld microsteps", AltitudeOffsetMicrosteps, AzimuthOffsetMicrosteps); if (IUFindSwitch(&SlewModesSP, "SLEW_SILENT") != nullptr && IUFindSwitch(&SlewModesSP, "SLEW_SILENT")->s == ISS_ON) { SilentSlewMode = true; } else { SilentSlewMode = false; } SlewTo(AXIS1, AzimuthOffsetMicrosteps); SlewTo(AXIS2, AltitudeOffsetMicrosteps); TrackState = SCOPE_PARKING; return true; } bool SkywatcherAPIMount::UnPark() { DEBUG(DBG_SCOPE, "SkywatcherAPIMount::UnPark"); ParkPosition_t TargetPosition = PARK_NORTH; ParkDirection_t TargetDirection = PARK_COUNTERCLOCKWISE; double DeltaAlt = 0; double DeltaAz = 0; // Determinate the target position and direction if (IUFindSwitch(&UnparkPositionSP, "UNPARK_NORTH") != nullptr && IUFindSwitch(&UnparkPositionSP, "UNPARK_NORTH")->s == ISS_ON) { TargetPosition = PARK_NORTH; } if (IUFindSwitch(&UnparkPositionSP, "UNPARK_EAST") != nullptr && IUFindSwitch(&UnparkPositionSP, "UNPARK_EAST")->s == ISS_ON) { TargetPosition = PARK_EAST; } if (IUFindSwitch(&UnparkPositionSP, "UNPARK_SOUTH") != nullptr && IUFindSwitch(&UnparkPositionSP, "UNPARK_SOUTH")->s == ISS_ON) { TargetPosition = PARK_SOUTH; } if (IUFindSwitch(&UnparkPositionSP, "UNPARK_WEST") != nullptr && IUFindSwitch(&UnparkPositionSP, "UNPARK_WEST")->s == ISS_ON) { TargetPosition = PARK_WEST; } // Note: The reverse direction is used for unparking. if (IUFindSwitch(&ParkMovementDirectionSP, "PMD_COUNTERCLOCKWISE") != nullptr && IUFindSwitch(&ParkMovementDirectionSP, "PMD_COUNTERCLOCKWISE")->s == ISS_ON) { TargetDirection = PARK_CLOCKWISE; } if (IUFindSwitch(&ParkMovementDirectionSP, "PMD_CLOCKWISE") != nullptr && IUFindSwitch(&ParkMovementDirectionSP, "PMD_CLOCKWISE")->s == ISS_ON) { TargetDirection = PARK_COUNTERCLOCKWISE; } DeltaAz = GetParkDeltaAz(TargetDirection, TargetPosition); // Altitude 3360 points the telescope upwards DeltaAlt = CurrentAltAz.alt - 3360; // Move the telescope to the desired position long AltitudeOffsetMicrosteps = DegreesToMicrosteps(AXIS2, DeltaAlt); long AzimuthOffsetMicrosteps = DegreesToMicrosteps(AXIS1, DeltaAz); DEBUGF(INDI::AlignmentSubsystem::DBG_ALIGNMENT, "Unparking: Delta altitude %1.2f - delta azimuth %1.2f", DeltaAlt, DeltaAz); DEBUGF(INDI::AlignmentSubsystem::DBG_ALIGNMENT, "Unparking: Altitude offset %ld microsteps Azimuth offset %ld microsteps", AltitudeOffsetMicrosteps, AzimuthOffsetMicrosteps); if (IUFindSwitch(&SlewModesSP, "SLEW_SILENT") != nullptr && IUFindSwitch(&SlewModesSP, "SLEW_SILENT")->s == ISS_ON) { SilentSlewMode = true; } else { SilentSlewMode = false; } SlewTo(AXIS1, AzimuthOffsetMicrosteps); SlewTo(AXIS2, AltitudeOffsetMicrosteps); SetParked(false); TrackState = SCOPE_SLEWING; return true; } bool SkywatcherAPIMount::ReadScopeStatus() { DEBUG(INDI::AlignmentSubsystem::DBG_ALIGNMENT, "SkywatcherAPIMount::ReadScopeStatus"); // leave the following stuff in for the time being it is mostly harmless // Quick check of the mount if (!GetMotorBoardVersion(AXIS1)) return false; if (!GetStatus(AXIS1)) return false; if (!GetStatus(AXIS2)) return false; // Update Axis Position if (!GetEncoder(AXIS1)) return false; if (!GetEncoder(AXIS2)) return false; UpdateDetailedMountInformation(true); if (TrackState == SCOPE_PARKING) { if (!IsInMotion(AXIS1) && !IsInMotion(AXIS2)) { SetParked(true); } } // Calculate new RA DEC ln_hrz_posn AltAz { 0, 0 }; AltAz.alt = MicrostepsToDegrees(AXIS2, CurrentEncoders[AXIS2] - ZeroPositionEncoders[AXIS2]); if (IsVirtuosoMount()) { double MountDegree = AltAz.alt; // The initial position of the Virtuoso mount is polar aligned when switched on. // The altitude is corrected by the latitude. if (IUFindNumber(&LocationNP, "LAT") != nullptr) MountDegree += IUFindNumber(&LocationNP, "LAT")->value; // The altitude degrees in the Virtuoso Alt-Az mount are inverted. AltAz.alt = 3420 - MountDegree; // Drift compensation for tracking mode (SoftPEC) if (IUFindSwitch(&SoftPECModesSP, "SOFTPEC_ENABLED") != nullptr && IUFindSwitch(&SoftPECModesSP, "SOFTPEC_ENABLED")->s == ISS_ON && IUFindNumber(&SoftPecNP, "SOFTPEC_VALUE") != nullptr) { AltAz.alt += (IUFindNumber(&SoftPecNP, "SOFTPEC_VALUE")->value / 60) * TrackingMsecs / 1000; } } DEBUGF(INDI::AlignmentSubsystem::DBG_ALIGNMENT, "Axis2 encoder %ld initial %ld alt(degrees) %lf", CurrentEncoders[AXIS2], ZeroPositionEncoders[AXIS2], AltAz.alt); AltAz.az = MicrostepsToDegrees(AXIS1, CurrentEncoders[AXIS1] - ZeroPositionEncoders[AXIS1]); if (IsVirtuosoMount()) { if (AltAz.az < 0) AltAz.az += 360; } CurrentAltAz = AltAz; DEBUGF(INDI::AlignmentSubsystem::DBG_ALIGNMENT, "Axis1 encoder %ld initial %ld az(degrees) %lf", CurrentEncoders[AXIS1], ZeroPositionEncoders[AXIS1], AltAz.az); TelescopeDirectionVector TDV = TelescopeDirectionVectorFromAltitudeAzimuth(AltAz); DEBUGF(INDI::AlignmentSubsystem::DBG_ALIGNMENT, "TDV x %lf y %lf z %lf", TDV.x, TDV.y, TDV.z); double RightAscension, Declination; if (TransformTelescopeToCelestial(TDV, RightAscension, Declination)) DEBUG(INDI::AlignmentSubsystem::DBG_ALIGNMENT, "Conversion OK"); else { bool HavePosition = false; ln_lnlat_posn Position { 0, 0 }; if ((nullptr != IUFindNumber(&LocationNP, "LAT")) && (0 != IUFindNumber(&LocationNP, "LAT")->value) && (nullptr != IUFindNumber(&LocationNP, "LONG")) && (0 != IUFindNumber(&LocationNP, "LONG")->value)) { // I assume that being on the equator and exactly on the prime meridian is unlikely Position.lat = IUFindNumber(&LocationNP, "LAT")->value; Position.lng = IUFindNumber(&LocationNP, "LONG")->value; HavePosition = true; } ln_equ_posn EquatorialCoordinates { 0, 0 }; if (HavePosition) { TelescopeDirectionVector RotatedTDV(TDV); switch (GetApproximateMountAlignment()) { case ZENITH: break; case NORTH_CELESTIAL_POLE: // Rotate the TDV coordinate system anticlockwise (positive) around the y axis by 90 minus // the (positive)observatory latitude. The vector itself is rotated clockwise RotatedTDV.RotateAroundY(90.0 - Position.lat); AltitudeAzimuthFromTelescopeDirectionVector(RotatedTDV, AltAz); break; case SOUTH_CELESTIAL_POLE: // Rotate the TDV coordinate system clockwise (negative) around the y axis by 90 plus // the (negative)observatory latitude. The vector itself is rotated anticlockwise RotatedTDV.RotateAroundY(-90.0 - Position.lat); AltitudeAzimuthFromTelescopeDirectionVector(RotatedTDV, AltAz); break; } #ifdef USE_INITIAL_JULIAN_DATE ln_get_equ_from_hrz(&AltAz, &Position, InitialJulianDate, &EquatorialCoordinates); #else ln_get_equ_from_hrz(&AltAz, &Position, ln_get_julian_from_sys(), &EquatorialCoordinates); #endif } else // The best I can do is just do a direct conversion to RA/DEC EquatorialCoordinatesFromTelescopeDirectionVector(TDV, EquatorialCoordinates); // libnova works in decimal degrees RightAscension = EquatorialCoordinates.ra * 24.0 / 360.0; Declination = EquatorialCoordinates.dec; DEBUGF(INDI::AlignmentSubsystem::DBG_ALIGNMENT, "Conversion Failed - HavePosition %d RA (degrees) %lf DEC (degrees) %lf", HavePosition, EquatorialCoordinates.ra, EquatorialCoordinates.dec); } DEBUGF(INDI::AlignmentSubsystem::DBG_ALIGNMENT, "New RA %lf (hours) DEC %lf (degrees)", RightAscension, Declination); NewRaDec(RightAscension, Declination); return true; } bool SkywatcherAPIMount::saveConfigItems(FILE *fp) { SaveAlignmentConfigProperties(fp); return INDI::Telescope::saveConfigItems(fp); } bool SkywatcherAPIMount::Sync(double ra, double dec) { DEBUG(INDI::AlignmentSubsystem::DBG_ALIGNMENT, "SkywatcherAPIMount::Sync"); // Compute a telescope direction vector from the current encoders if (!GetEncoder(AXIS1)) return false; if (!GetEncoder(AXIS2)) return false; // Syncing is treated specially when the telescope position is known in park position to spare // "a huge-jump point" in the alignment model. if (isParked()) { ln_hrz_posn AltAz { 0, 0 }; TelescopeDirectionVector TDV; double OrigAlt = 0; if (TransformCelestialToTelescope(ra, dec, 0.0, TDV)) { AltitudeAzimuthFromTelescopeDirectionVector(TDV, AltAz); OrigAlt = AltAz.alt; if (IsVirtuosoMount()) { // The initial position of the Virtuoso mount is polar aligned when switched on. // The altitude is corrected by the latitude. if (IUFindNumber(&LocationNP, "LAT") != nullptr) AltAz.alt = AltAz.alt - IUFindNumber(&LocationNP, "LAT")->value; AltAz.az = 180 + AltAz.az; } ZeroPositionEncoders[AXIS1] = PolarisPositionEncoders[AXIS1]-DegreesToMicrosteps(AXIS1, AltAz.az); ZeroPositionEncoders[AXIS2] = PolarisPositionEncoders[AXIS2]-DegreesToMicrosteps(AXIS2, AltAz.alt); LOGF_INFO("Sync (Alt: %lf Az: %lf) in park position", OrigAlt, AltAz.az); GetAlignmentDatabase().clear(); return true; } } // The tracking seconds should be reset to restart the drift compensation ResetTrackingSeconds = true; // Might as well do this UpdateDetailedMountInformation(true); ln_hrz_posn AltAz { 0, 0 }; AltAz.alt = MicrostepsToDegrees(AXIS2, CurrentEncoders[AXIS2] - ZeroPositionEncoders[AXIS2]); if (IsVirtuosoMount()) { double MountDegree = AltAz.alt; // The initial position of the Virtuoso mount is polar aligned when switched on. // The altitude is corrected by the latitude. if (IUFindNumber(&LocationNP, "LAT") != nullptr) MountDegree += IUFindNumber(&LocationNP, "LAT")->value; // The altitude degrees in the Virtuoso Alt-Az mount are inverted. AltAz.alt = 3420 - MountDegree; } DEBUGF(INDI::AlignmentSubsystem::DBG_ALIGNMENT, "Axis2 encoder %ld initial %ld alt(degrees) %lf", CurrentEncoders[AXIS2], ZeroPositionEncoders[AXIS2], AltAz.alt); AltAz.az = MicrostepsToDegrees(AXIS1, CurrentEncoders[AXIS1] - ZeroPositionEncoders[AXIS1]); DEBUGF(INDI::AlignmentSubsystem::DBG_ALIGNMENT, "Axis1 encoder %ld initial %ld az(degrees) %lf", CurrentEncoders[AXIS1], ZeroPositionEncoders[AXIS1], AltAz.az); AlignmentDatabaseEntry NewEntry; #ifdef USE_INITIAL_JULIAN_DATE NewEntry.ObservationJulianDate = InitialJulianDate; #else NewEntry.ObservationJulianDate = ln_get_julian_from_sys(); #endif NewEntry.RightAscension = ra; NewEntry.Declination = dec; NewEntry.TelescopeDirection = TelescopeDirectionVectorFromAltitudeAzimuth(AltAz); NewEntry.PrivateDataSize = 0; DEBUGF(INDI::AlignmentSubsystem::DBG_ALIGNMENT, "New sync point Date %lf RA %lf DEC %lf TDV(x %lf y %lf z %lf)", NewEntry.ObservationJulianDate, NewEntry.RightAscension, NewEntry.Declination, NewEntry.TelescopeDirection.x, NewEntry.TelescopeDirection.y, NewEntry.TelescopeDirection.z); if (!CheckForDuplicateSyncPoint(NewEntry)) { GetAlignmentDatabase().push_back(NewEntry); // Tell the client about size change UpdateSize(); // Tell the math plugin to reinitialise Initialise(this); return true; } return false; } void SkywatcherAPIMount::TimerHit() { static bool Slewing = false; static bool Tracking = false; // By default this method is called every POLLMS milliseconds // Call the base class handler // This normally just calls ReadScopeStatus INDI::Telescope::TimerHit(); // Do my own timer stuff assuming ReadScopeStatus has just been called SetTimer(TimeoutDuration); switch (TrackState) { case SCOPE_SLEWING: if (!Slewing) { LOG_INFO("Slewing started"); } TrackingMsecs = 0; GuideDeltaAlt = 0; GuideDeltaAz = 0; ResetGuidePulses(); TimeoutDuration = 500; Tracking = false; Slewing = true; GuidingPulses.clear(); if ((AxesStatus[AXIS1].FullStop) && (AxesStatus[AXIS2].FullStop)) { if (ISS_ON == IUFindSwitch(&CoordSP, "TRACK")->s) { // Goto has finished start tracking TrackState = SCOPE_TRACKING; // Fall through to tracking case } else { TrackState = SCOPE_IDLE; break; } } break; case SCOPE_TRACKING: { if (!Tracking) { LOG_INFO("Tracking started"); TrackingMsecs = 0; GuideDeltaAlt = 0; GuideDeltaAz = 0; ResetGuidePulses(); TrackedAltAz = CurrentAltAz; } // Restart the drift compensation after syncing if (ResetTrackingSeconds) { ResetTrackingSeconds = false; TrackingMsecs = 0; GuideDeltaAlt = 0; GuideDeltaAz = 0; ResetGuidePulses(); TrackedAltAz = CurrentAltAz; } double trackingDeltaAlt = std::abs(CurrentAltAz.alt-TrackedAltAz.alt); double trackingDeltaAz = std::abs(CurrentAltAz.az-TrackedAltAz.az); if (trackingDeltaAlt+trackingDeltaAz > 50.0) { IDMessage(nullptr, "Abort tracking after too much margin (%1.4f > 10)", trackingDeltaAlt+trackingDeltaAz); Abort(); } TrackingMsecs += TimeoutDuration; if (TrackingMsecs % 60000 == 0) { LOGF_INFO("Tracking in progress (%d seconds elapsed)", TrackingMsecs / 1000); } Tracking = true; Slewing = false; // Continue or start tracking // Calculate where the mount needs to be in POLLMS time // POLLMS is hardcoded to be one second double JulianOffset = 1.0 / (24.0 * 60 * 60); // TODO may need to make this longer to get a meaningful result TelescopeDirectionVector TDV; ln_hrz_posn AltAz { 0, 0 }; if (TransformCelestialToTelescope(CurrentTrackingTarget.ra, CurrentTrackingTarget.dec, #ifdef USE_INITIAL_JULIAN_DATE 0, TDV)) #else JulianOffset, TDV)) #endif { DEBUGF(INDI::AlignmentSubsystem::DBG_ALIGNMENT, "TDV x %lf y %lf z %lf", TDV.x, TDV.y, TDV.z); AltitudeAzimuthFromTelescopeDirectionVector(TDV, AltAz); } else { // Try a conversion with the stored observatory position if any bool HavePosition = false; ln_lnlat_posn Position { 0, 0 }; if ((nullptr != IUFindNumber(&LocationNP, "LAT")) && (0 != IUFindNumber(&LocationNP, "LAT")->value) && (nullptr != IUFindNumber(&LocationNP, "LONG")) && (0 != IUFindNumber(&LocationNP, "LONG")->value)) { // I assume that being on the equator and exactly on the prime meridian is unlikely Position.lat = IUFindNumber(&LocationNP, "LAT")->value; Position.lng = IUFindNumber(&LocationNP, "LONG")->value; HavePosition = true; } ln_equ_posn EquatorialCoordinates { 0, 0 }; // libnova works in decimal degrees EquatorialCoordinates.ra = CurrentTrackingTarget.ra * 360.0 / 24.0; EquatorialCoordinates.dec = CurrentTrackingTarget.dec; if (HavePosition) ln_get_hrz_from_equ(&EquatorialCoordinates, &Position, #ifdef USE_INITIAL_JULIAN_DATE InitialJulianDate, &AltAz); #else ln_get_julian_from_sys() + JulianOffset, &AltAz); #endif else { // No sense in tracking in this case TrackState = SCOPE_IDLE; break; } } if (IsVirtuosoMount()) { // The initial position of the Virtuoso mount is polar aligned when switched on. // The altitude is corrected by the latitude. if (IUFindNumber(&LocationNP, "LAT") != nullptr) { AltAz.alt = AltAz.alt - IUFindNumber(&LocationNP, "LAT")->value; } // Drift compensation for tracking mode (SoftPEC) if (IUFindSwitch(&SoftPECModesSP, "SOFTPEC_ENABLED") != nullptr && IUFindSwitch(&SoftPECModesSP, "SOFTPEC_ENABLED")->s == ISS_ON && IUFindNumber(&SoftPecNP, "SOFTPEC_VALUE") != nullptr) { AltAz.alt += (IUFindNumber(&SoftPecNP, "SOFTPEC_VALUE")->value / 60) * TrackingMsecs / 1000; } AltAz.az = 180 + AltAz.az; } DEBUGF(DBG_SCOPE, "Tracking AXIS1 CurrentEncoder %ld OldTrackingTarget %ld AXIS2 CurrentEncoder %ld OldTrackingTarget " "%ld", CurrentEncoders[AXIS1], OldTrackingTarget[AXIS1], CurrentEncoders[AXIS2], OldTrackingTarget[AXIS2]); DEBUGF(DBG_SCOPE, "New Tracking Target Altitude %lf degrees %ld microsteps Azimuth %lf degrees %ld microsteps", AltAz.alt, DegreesToMicrosteps(AXIS2, AltAz.alt), AltAz.az, DegreesToMicrosteps(AXIS1, AltAz.az)); // Calculate the auto-guiding delta degrees double DeltaAlt = 0; double DeltaAz = 0; for (auto Iter = GuidingPulses.begin(); Iter != GuidingPulses.end(); ) { // We treat the guide calibration specially if (Iter->OriginalDuration == 1000) { DeltaAlt += Iter->DeltaAlt; DeltaAz += Iter->DeltaAz; } else { DeltaAlt += Iter->DeltaAlt / 2; DeltaAz += Iter->DeltaAz / 2; } Iter->Duration -= TimeoutDuration; if (Iter->Duration < TimeoutDuration) { Iter = GuidingPulses.erase(Iter); if (Iter == GuidingPulses.end()) { break; } continue; } ++Iter; } GuideDeltaAlt += DeltaAlt; GuideDeltaAz += DeltaAz; long AltitudeOffsetMicrosteps = DegreesToMicrosteps(AXIS2, AltAz.alt+GuideDeltaAlt) + ZeroPositionEncoders[AXIS2] - CurrentEncoders[AXIS2]; long AzimuthOffsetMicrosteps = DegreesToMicrosteps(AXIS1, AltAz.az+GuideDeltaAz) + ZeroPositionEncoders[AXIS1] - CurrentEncoders[AXIS1]; DEBUGF(DBG_SCOPE, "New Tracking Target AltitudeOffset %ld microsteps AzimuthOffset %ld microsteps", AltitudeOffsetMicrosteps, AzimuthOffsetMicrosteps); if (AzimuthOffsetMicrosteps > MicrostepsPerRevolution[AXIS1] / 2) { DEBUG(DBG_SCOPE, "Tracking AXIS1 going long way round"); // Going the long way round - send it the other way AzimuthOffsetMicrosteps -= MicrostepsPerRevolution[AXIS1]; } if (0 != AzimuthOffsetMicrosteps) { // Calculate the slewing rates needed to reach that position // at the correct time. long AzimuthRate = StepperClockFrequency[AXIS1] / AzimuthOffsetMicrosteps; if (!AxesStatus[AXIS1].FullStop && ((AxesStatus[AXIS1].SlewingForward && (AzimuthRate < 0)) || (!AxesStatus[AXIS1].SlewingForward && (AzimuthRate > 0)))) { // Direction change whilst axis running // Abandon tracking for this clock tick DEBUG(DBG_SCOPE, "Tracking - AXIS1 direction change"); SlowStop(AXIS1); } else { char Direction = AzimuthRate > 0 ? '0' : '1'; AzimuthRate = std::abs(AzimuthRate); SetClockTicksPerMicrostep(AXIS1, AzimuthRate < 1 ? 1 : AzimuthRate); if (AxesStatus[AXIS1].FullStop) { DEBUG(DBG_SCOPE, "Tracking - AXIS1 restart"); SetMotionMode(AXIS1, '1', Direction); StartMotion(AXIS1); } DEBUGF(DBG_SCOPE, "Tracking - AXIS1 offset %ld microsteps rate %ld direction %c", AzimuthOffsetMicrosteps, AzimuthRate, Direction); } } else { // Nothing to do - stop the axis DEBUG(DBG_SCOPE, "Tracking - AXIS1 zero offset"); SlowStop(AXIS1); } // Do I need to take out any complete revolutions before I do this test? if (AltitudeOffsetMicrosteps > MicrostepsPerRevolution[AXIS2] / 2) { DEBUG(DBG_SCOPE, "Tracking AXIS2 going long way round"); // Going the long way round - send it the other way AltitudeOffsetMicrosteps -= MicrostepsPerRevolution[AXIS2]; } if (0 != AltitudeOffsetMicrosteps) { // Calculate the slewing rates needed to reach that position // at the correct time. long AltitudeRate = StepperClockFrequency[AXIS2] / AltitudeOffsetMicrosteps; if (!AxesStatus[AXIS2].FullStop && ((AxesStatus[AXIS2].SlewingForward && (AltitudeRate < 0)) || (!AxesStatus[AXIS2].SlewingForward && (AltitudeRate > 0)))) { // Direction change whilst axis running // Abandon tracking for this clock tick DEBUG(DBG_SCOPE, "Tracking - AXIS2 direction change"); SlowStop(AXIS2); } else { char Direction = AltitudeRate > 0 ? '0' : '1'; AltitudeRate = std::abs(AltitudeRate); SetClockTicksPerMicrostep(AXIS2, AltitudeRate < 1 ? 1 : AltitudeRate); if (AxesStatus[AXIS2].FullStop) { DEBUG(DBG_SCOPE, "Tracking - AXIS2 restart"); SetMotionMode(AXIS2, '1', Direction); StartMotion(AXIS2); } DEBUGF(DBG_SCOPE, "Tracking - AXIS2 offset %ld microsteps rate %ld direction %c", AltitudeOffsetMicrosteps, AltitudeRate, Direction); } } else { // Nothing to do - stop the axis DEBUG(DBG_SCOPE, "Tracking - AXIS2 zero offset"); SlowStop(AXIS2); } DEBUGF(DBG_SCOPE, "Tracking - AXIS1 error %d AXIS2 error %d", OldTrackingTarget[AXIS1] - CurrentEncoders[AXIS1], OldTrackingTarget[AXIS2] - CurrentEncoders[AXIS2]); OldTrackingTarget[AXIS1] = AzimuthOffsetMicrosteps + CurrentEncoders[AXIS1]; OldTrackingTarget[AXIS2] = AltitudeOffsetMicrosteps + CurrentEncoders[AXIS2]; break; } break; default: if (Slewing) { LOG_INFO("Slewing stopped"); } if (Tracking) { LOG_INFO("Tracking stopped"); } TrackingMsecs = 0; GuideDeltaAlt = 0; GuideDeltaAz = 0; ResetGuidePulses(); TimeoutDuration = 500; Tracking = false; Slewing = false; GuidingPulses.clear(); break; } } bool SkywatcherAPIMount::updateLocation(double latitude, double longitude, double elevation) { DEBUG(DBG_SCOPE, "SkywatcherAPIMount::updateLocation"); UpdateLocation(latitude, longitude, elevation); return true; } bool SkywatcherAPIMount::updateProperties() { INDI::Telescope::updateProperties(); if (isConnected()) { // Fill in any real values now available MCInit should have been called already UpdateDetailedMountInformation(false); // Define our connected only properties to the base driver // e.g. defineNumber(MyNumberVectorPointer); // This will register our properties and send a IDDefXXXX message to any connected clients // I have now idea why I have to do this here as well as in ISGetProperties. It makes me // concerned there is a design or implementation flaw somewhere. defineText(&BasicMountInfoV); defineNumber(&AxisOneInfoV); defineSwitch(&AxisOneStateV); defineNumber(&AxisTwoInfoV); defineSwitch(&AxisTwoStateV); defineNumber(&AxisOneEncoderValuesV); defineNumber(&AxisTwoEncoderValuesV); defineSwitch(&SlewModesSP); defineSwitch(&SoftPECModesSP); defineNumber(&SoftPecNP); defineNumber(&GuidingRatesNP); defineSwitch(&ParkMovementDirectionSP); defineSwitch(&ParkPositionSP); defineSwitch(&UnparkPositionSP); defineNumber(&GuideNSNP); defineNumber(&GuideWENP); // Start the timer if we need one // SetTimer(POLLMS); return true; } else { // Delete any connected only properties from the base driver's list // e.g. deleteProperty(MyNumberVector.name); deleteProperty(BasicMountInfoV.name); deleteProperty(AxisOneInfoV.name); deleteProperty(AxisOneStateV.name); deleteProperty(AxisTwoInfoV.name); deleteProperty(AxisTwoStateV.name); deleteProperty(AxisOneEncoderValuesV.name); deleteProperty(AxisTwoEncoderValuesV.name); deleteProperty(SlewModesSP.name); deleteProperty(SoftPECModesSP.name); deleteProperty(SoftPecNP.name); deleteProperty(GuidingRatesNP.name); deleteProperty(ParkMovementDirectionSP.name); deleteProperty(ParkPositionSP.name); deleteProperty(UnparkPositionSP.name); deleteProperty(GuideNSNP.name); deleteProperty(GuideWENP.name); return true; } } IPState SkywatcherAPIMount::GuideNorth(float ms) { GuidingPulse Pulse; TimeoutDuration = 250; CalculateGuidePulses(); Pulse.DeltaAz = NorthPulse.DeltaAz; Pulse.DeltaAlt = NorthPulse.DeltaAlt; Pulse.Duration = (int)ms; Pulse.OriginalDuration = (int)ms; GuidingPulses.push_back(Pulse); // IDMessage(nullptr, "GUIDE NORTH: %1.2f msec - deltaalt: %1.6f deltaaz: %1.6f", ms, // Pulse.DeltaAlt, Pulse.DeltaAz); return IPS_OK; } IPState SkywatcherAPIMount::GuideSouth(float ms) { GuidingPulse Pulse; TimeoutDuration = 250; CalculateGuidePulses(); Pulse.DeltaAz = -NorthPulse.DeltaAz; Pulse.DeltaAlt = -NorthPulse.DeltaAlt; Pulse.Duration = (int)ms; Pulse.OriginalDuration = (int)ms; GuidingPulses.push_back(Pulse); // IDMessage(nullptr, "GUIDE SOUTH: %1.2f msec - deltaalt: %1.6f deltaaz: %1.6f", ms, // Pulse.DeltaAlt, Pulse.DeltaAz); return IPS_OK; } IPState SkywatcherAPIMount::GuideWest(float ms) { GuidingPulse Pulse; TimeoutDuration = 250; CalculateGuidePulses(); Pulse.DeltaAz = WestPulse.DeltaAz; Pulse.DeltaAlt = WestPulse.DeltaAlt; Pulse.Duration = (int)ms; Pulse.OriginalDuration = (int)ms; GuidingPulses.push_back(Pulse); // IDMessage(nullptr, "GUIDE WEST: %1.2f msec - deltaalt: %1.6f deltaaz: %1.6f", ms, // Pulse.DeltaAlt, Pulse.DeltaAz); return IPS_OK; } IPState SkywatcherAPIMount::GuideEast(float ms) { GuidingPulse Pulse; TimeoutDuration = 250; CalculateGuidePulses(); Pulse.DeltaAz = -WestPulse.DeltaAz; Pulse.DeltaAlt = -WestPulse.DeltaAlt; Pulse.Duration = (int)ms; Pulse.OriginalDuration = (int)ms; GuidingPulses.push_back(Pulse); // IDMessage(nullptr, "GUIDE EAST: %1.2f msec - deltaalt: %1.6f deltaaz: %1.6f", ms, // Pulse.DeltaAlt, Pulse.DeltaAz); return IPS_OK; } // Private methods void SkywatcherAPIMount::CalculateGuidePulses() { if (NorthPulse.Duration != 0 || WestPulse.Duration != 0) return; // Calculate the west reference delta // Note: The RA is multiplied by 3.75 (90/24) to be more comparable with DEC values. const double WestRate = IUFindNumber(&GuidingRatesNP, "GUIDERA_RATE")->value / 10*-(double)1 / 60 / 60*3.75 / 100; ConvertGuideCorrection(WestRate, 0, WestPulse.DeltaAlt, WestPulse.DeltaAz); WestPulse.Duration = 1; // Calculate the north reference delta // Note: By some reason, it must be multiplied by 100 to match with the RA values. const double NorthRate = IUFindNumber(&GuidingRatesNP, "GUIDEDEC_RATE")->value / 10*(double)1 / 60 / 60*100 / 100; ConvertGuideCorrection(0, NorthRate, NorthPulse.DeltaAlt, NorthPulse.DeltaAz); NorthPulse.Duration = 1; } void SkywatcherAPIMount::ResetGuidePulses() { NorthPulse.Duration = 0; WestPulse.Duration = 0; } void SkywatcherAPIMount::ConvertGuideCorrection(double delta_ra, double delta_dec, double &delta_alt, double &delta_az) { ln_hrz_posn OldAltAz { 0, 0 }; ln_hrz_posn NewAltAz { 0, 0 }; TelescopeDirectionVector OldTDV; TelescopeDirectionVector NewTDV; TransformCelestialToTelescope(CurrentTrackingTarget.ra, CurrentTrackingTarget.dec, 0.0, OldTDV); AltitudeAzimuthFromTelescopeDirectionVector(OldTDV, OldAltAz); TransformCelestialToTelescope(CurrentTrackingTarget.ra+delta_ra, CurrentTrackingTarget.dec+delta_dec, 0.0, NewTDV); AltitudeAzimuthFromTelescopeDirectionVector(NewTDV, NewAltAz); delta_alt = NewAltAz.alt-OldAltAz.alt; delta_az = NewAltAz.az-OldAltAz.az; } int SkywatcherAPIMount::skywatcher_tty_read(int fd, char *buf, int nbytes, int timeout, int *nbytes_read) { if (!RecoverAfterReconnection && !SerialPortName.empty() && !FileExists(SerialPortName)) { RecoverAfterReconnection = true; serialConnection->Disconnect(); serialConnection->Refresh(); std::this_thread::sleep_for(std::chrono::milliseconds(1000)); if (!serialConnection->Connect()) { RecoverAfterReconnection = true; std::this_thread::sleep_for(std::chrono::milliseconds(1000)); if (!serialConnection->Connect()) { RecoverAfterReconnection = false; return 0; } } SetSerialPort(serialConnection->getPortFD()); SerialPortName = serialConnection->port(); RecoverAfterReconnection = false; } return tty_read(fd, buf, nbytes, timeout, nbytes_read); } int SkywatcherAPIMount::skywatcher_tty_write(int fd, const char *buffer, int nbytes, int *nbytes_written) { if (!RecoverAfterReconnection && !SerialPortName.empty() && !FileExists(SerialPortName)) { RecoverAfterReconnection = true; serialConnection->Disconnect(); serialConnection->Refresh(); std::this_thread::sleep_for(std::chrono::milliseconds(1000)); if (!serialConnection->Connect()) { RecoverAfterReconnection = true; std::this_thread::sleep_for(std::chrono::milliseconds(1000)); if (!serialConnection->Connect()) { RecoverAfterReconnection = false; return 0; } } SetSerialPort(serialConnection->getPortFD()); SerialPortName = serialConnection->port(); RecoverAfterReconnection = false; } return tty_write(fd, buffer, nbytes, nbytes_written); } void SkywatcherAPIMount::SkywatcherMicrostepsFromTelescopeDirectionVector( const TelescopeDirectionVector TelescopeDirectionVector, long &Axis1Microsteps, long &Axis2Microsteps) { // For the time being I assume that all skywathcer mounts share the same encoder conventions double Axis1Angle = 0; double Axis2Angle = 0; SphericalCoordinateFromTelescopeDirectionVector(TelescopeDirectionVector, Axis1Angle, TelescopeDirectionVectorSupportFunctions::CLOCKWISE, Axis1Angle, FROM_AZIMUTHAL_PLANE); Axis1Microsteps = RadiansToMicrosteps(AXIS1, Axis1Angle); Axis2Microsteps = RadiansToMicrosteps(AXIS2, Axis2Angle); } const TelescopeDirectionVector SkywatcherAPIMount::TelescopeDirectionVectorFromSkywatcherMicrosteps(long Axis1Microsteps, long Axis2Microsteps) { // For the time being I assume that all skywathcer mounts share the same encoder conventions double Axis1Angle = MicrostepsToRadians(AXIS1, Axis1Microsteps); double Axis2Angle = MicrostepsToRadians(AXIS2, Axis2Microsteps); return TelescopeDirectionVectorFromSphericalCoordinate( Axis1Angle, TelescopeDirectionVectorSupportFunctions::CLOCKWISE, Axis2Angle, FROM_AZIMUTHAL_PLANE); } void SkywatcherAPIMount::UpdateDetailedMountInformation(bool InformClient) { bool BasicMountInfoHasChanged = false; if (std::string(BasicMountInfo[MOTOR_CONTROL_FIRMWARE_VERSION].text) != std::to_string(MCVersion)) { IUSaveText(&BasicMountInfo[MOTOR_CONTROL_FIRMWARE_VERSION], std::to_string(MCVersion).c_str()); BasicMountInfoHasChanged = true; } if (std::string(BasicMountInfo[MOUNT_CODE].text) != std::to_string(MountCode)) { IUSaveText(&BasicMountInfo[MOUNT_CODE], std::to_string(MountCode).c_str()); SetApproximateMountAlignmentFromMountType(ALTAZ); BasicMountInfoHasChanged = true; } if (std::string(BasicMountInfo[IS_DC_MOTOR].text) != std::to_string(IsDCMotor)) { IUSaveText(&BasicMountInfo[IS_DC_MOTOR], std::to_string(IsDCMotor).c_str()); BasicMountInfoHasChanged = true; } if (BasicMountInfoHasChanged && InformClient) IDSetText(&BasicMountInfoV, nullptr); if (MountCode >= 128 && MountCode <= 143) IUSaveText(&BasicMountInfo[MOUNT_NAME], "Az Goto"); if (MountCode >= 144 && MountCode <= 159) IUSaveText(&BasicMountInfo[MOUNT_NAME], "Dob Goto"); if (MountCode >= 160) IUSaveText(&BasicMountInfo[MOUNT_NAME], "AllView Goto"); bool AxisOneInfoHasChanged = false; if (AxisOneInfo[MICROSTEPS_PER_REVOLUTION].value != MicrostepsPerRevolution[0]) { AxisOneInfo[MICROSTEPS_PER_REVOLUTION].value = MicrostepsPerRevolution[0]; AxisOneInfoHasChanged = true; } if (AxisOneInfo[STEPPER_CLOCK_FREQUENCY].value != StepperClockFrequency[0]) { AxisOneInfo[STEPPER_CLOCK_FREQUENCY].value = StepperClockFrequency[0]; AxisOneInfoHasChanged = true; } if (AxisOneInfo[HIGH_SPEED_RATIO].value != HighSpeedRatio[0]) { AxisOneInfo[HIGH_SPEED_RATIO].value = HighSpeedRatio[0]; AxisOneInfoHasChanged = true; } if (AxisOneInfo[MICROSTEPS_PER_WORM_REVOLUTION].value != MicrostepsPerWormRevolution[0]) { AxisOneInfo[MICROSTEPS_PER_WORM_REVOLUTION].value = MicrostepsPerWormRevolution[0]; AxisOneInfoHasChanged = true; } if (AxisOneInfoHasChanged && InformClient) IDSetNumber(&AxisOneInfoV, nullptr); bool AxisOneStateHasChanged = false; if (AxisOneState[FULL_STOP].s != (AxesStatus[0].FullStop ? ISS_ON : ISS_OFF)) { AxisOneState[FULL_STOP].s = AxesStatus[0].FullStop ? ISS_ON : ISS_OFF; AxisOneStateHasChanged = true; } if (AxisOneState[SLEWING].s != (AxesStatus[0].Slewing ? ISS_ON : ISS_OFF)) { AxisOneState[SLEWING].s = AxesStatus[0].Slewing ? ISS_ON : ISS_OFF; AxisOneStateHasChanged = true; } if (AxisOneState[SLEWING_TO].s != (AxesStatus[0].SlewingTo ? ISS_ON : ISS_OFF)) { AxisOneState[SLEWING_TO].s = AxesStatus[0].SlewingTo ? ISS_ON : ISS_OFF; AxisOneStateHasChanged = true; } if (AxisOneState[SLEWING_FORWARD].s != (AxesStatus[0].SlewingForward ? ISS_ON : ISS_OFF)) { AxisOneState[SLEWING_FORWARD].s = AxesStatus[0].SlewingForward ? ISS_ON : ISS_OFF; AxisOneStateHasChanged = true; } if (AxisOneState[HIGH_SPEED].s != (AxesStatus[0].HighSpeed ? ISS_ON : ISS_OFF)) { AxisOneState[HIGH_SPEED].s = AxesStatus[0].HighSpeed ? ISS_ON : ISS_OFF; AxisOneStateHasChanged = true; } if (AxisOneState[NOT_INITIALISED].s != (AxesStatus[0].NotInitialized ? ISS_ON : ISS_OFF)) { AxisOneState[NOT_INITIALISED].s = AxesStatus[0].NotInitialized ? ISS_ON : ISS_OFF; AxisOneStateHasChanged = true; } if (AxisOneStateHasChanged && InformClient) IDSetSwitch(&AxisOneStateV, nullptr); bool AxisTwoInfoHasChanged = false; if (AxisTwoInfo[MICROSTEPS_PER_REVOLUTION].value != MicrostepsPerRevolution[1]) { AxisTwoInfo[MICROSTEPS_PER_REVOLUTION].value = MicrostepsPerRevolution[1]; AxisTwoInfoHasChanged = true; } if (AxisTwoInfo[STEPPER_CLOCK_FREQUENCY].value != StepperClockFrequency[1]) { AxisTwoInfo[STEPPER_CLOCK_FREQUENCY].value = StepperClockFrequency[1]; AxisTwoInfoHasChanged = true; } if (AxisTwoInfo[HIGH_SPEED_RATIO].value != HighSpeedRatio[1]) { AxisTwoInfo[HIGH_SPEED_RATIO].value = HighSpeedRatio[1]; AxisTwoInfoHasChanged = true; } if (AxisTwoInfo[MICROSTEPS_PER_WORM_REVOLUTION].value != MicrostepsPerWormRevolution[1]) { AxisTwoInfo[MICROSTEPS_PER_WORM_REVOLUTION].value = MicrostepsPerWormRevolution[1]; AxisTwoInfoHasChanged = true; } if (AxisTwoInfoHasChanged && InformClient) IDSetNumber(&AxisTwoInfoV, nullptr); bool AxisTwoStateHasChanged = false; if (AxisTwoState[FULL_STOP].s != (AxesStatus[1].FullStop ? ISS_ON : ISS_OFF)) { AxisTwoState[FULL_STOP].s = AxesStatus[1].FullStop ? ISS_ON : ISS_OFF; AxisTwoStateHasChanged = true; } if (AxisTwoState[SLEWING].s != (AxesStatus[1].Slewing ? ISS_ON : ISS_OFF)) { AxisTwoState[SLEWING].s = AxesStatus[1].Slewing ? ISS_ON : ISS_OFF; AxisTwoStateHasChanged = true; } if (AxisTwoState[SLEWING_TO].s != (AxesStatus[1].SlewingTo ? ISS_ON : ISS_OFF)) { AxisTwoState[SLEWING_TO].s = AxesStatus[1].SlewingTo ? ISS_ON : ISS_OFF; AxisTwoStateHasChanged = true; } if (AxisTwoState[SLEWING_FORWARD].s != (AxesStatus[1].SlewingForward ? ISS_ON : ISS_OFF)) { AxisTwoState[SLEWING_FORWARD].s = AxesStatus[1].SlewingForward ? ISS_ON : ISS_OFF; AxisTwoStateHasChanged = true; } if (AxisTwoState[HIGH_SPEED].s != (AxesStatus[1].HighSpeed ? ISS_ON : ISS_OFF)) { AxisTwoState[HIGH_SPEED].s = AxesStatus[1].HighSpeed ? ISS_ON : ISS_OFF; AxisTwoStateHasChanged = true; } if (AxisTwoState[NOT_INITIALISED].s != (AxesStatus[1].NotInitialized ? ISS_ON : ISS_OFF)) { AxisTwoState[NOT_INITIALISED].s = AxesStatus[1].NotInitialized ? ISS_ON : ISS_OFF; AxisTwoStateHasChanged = true; } if (AxisTwoStateHasChanged && InformClient) IDSetSwitch(&AxisTwoStateV, nullptr); bool AxisOneEncoderValuesHasChanged = false; if ((AxisOneEncoderValues[RAW_MICROSTEPS].value != CurrentEncoders[AXIS1]) || (AxisOneEncoderValues[OFFSET_FROM_INITIAL].value != CurrentEncoders[AXIS1] - ZeroPositionEncoders[AXIS1])) { AxisOneEncoderValues[RAW_MICROSTEPS].value = CurrentEncoders[AXIS1]; AxisOneEncoderValues[MICROSTEPS_PER_ARCSEC].value = MicrostepsPerDegree[AXIS1] / 3600.0; AxisOneEncoderValues[OFFSET_FROM_INITIAL].value = CurrentEncoders[AXIS1] - ZeroPositionEncoders[AXIS1]; AxisOneEncoderValues[DEGREES_FROM_INITIAL].value = MicrostepsToDegrees(AXIS1, CurrentEncoders[AXIS1] - ZeroPositionEncoders[AXIS1]); AxisOneEncoderValuesHasChanged = true; } if (AxisOneEncoderValuesHasChanged && InformClient) IDSetNumber(&AxisOneEncoderValuesV, nullptr); bool AxisTwoEncoderValuesHasChanged = false; if ((AxisTwoEncoderValues[RAW_MICROSTEPS].value != CurrentEncoders[AXIS2]) || (AxisTwoEncoderValues[OFFSET_FROM_INITIAL].value != CurrentEncoders[AXIS2] - ZeroPositionEncoders[AXIS2])) { AxisTwoEncoderValues[RAW_MICROSTEPS].value = CurrentEncoders[AXIS2]; AxisTwoEncoderValues[MICROSTEPS_PER_ARCSEC].value = MicrostepsPerDegree[AXIS2] / 3600.0; AxisTwoEncoderValues[OFFSET_FROM_INITIAL].value = CurrentEncoders[AXIS2] - ZeroPositionEncoders[AXIS2]; AxisTwoEncoderValues[DEGREES_FROM_INITIAL].value = MicrostepsToDegrees(AXIS2, CurrentEncoders[AXIS2] - ZeroPositionEncoders[AXIS2]); AxisTwoEncoderValuesHasChanged = true; } if (AxisTwoEncoderValuesHasChanged && InformClient) IDSetNumber(&AxisTwoEncoderValuesV, nullptr); } libindi/drivers/telescope/lx200generic.h0000664000175000017500000000175413263645557017515 0ustar jasemjasem/* LX200 Generic Copyright (C) 2003-2015 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 */ #pragma once #include "lx200telescope.h" class LX200Generic : public LX200Telescope { public: LX200Generic(); virtual ~LX200Generic() = default; }; libindi/drivers/telescope/magellandriver.h0000664000175000017500000000446113263645557020305 0ustar jasemjasem/* MAGELLAN Driver Copyright (C) 2011 Onno Hommes (ohommes@alumni.cmu.edu) 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 */ #pragma once /* Just use Default tracking for what ever telescope is feeding Magellan I */ enum TFreq { MAGELLAN_TRACK_DEFAULT, MAGELLAN_TRACK_LUNAR, MAGELLAN_TRACK_MANUAL }; /* Time Format */ enum TTimeFormat { MAGELLAN_24, MAGELLAN_AM, MAGELLAN_PM }; #define MAGELLAN_TIMEOUT 5 /* FD timeout in seconds */ #define MAGELLAN_ERROR -1 /* Default Error Code */ #define MAGELLAN_OK 0 /* Default Success Code */ #define MAGELLAN_ACK 'P' /* Default Success Code */ #define CENTURY_THRESHOLD 91 /* When to goto 21st Century */ #define CONNECTION_RETRIES 2 /* Retry Attempt cut-off */ /* GET formatted sexagisemal value from device, return as double */ #define getMAGELLANRA(fd, x) getCommandSexa(fd, x, "#:GR#") #define getMAGELLANDEC(fd, x) getCommandSexa(fd, x, "#:GD#") #ifdef __cplusplus extern "C" { #endif /************************************************************************** Diagnostics **************************************************************************/ char ACK(int fd); int check_magellan_connection(int fd); /************************************************************************** Get Commands: store data in the supplied buffer. Return 0 on success or -1 on failure **************************************************************************/ /* Get Double from Sexagisemal */ int getCommandSexa(int fd, double *value, const char *cmd); /* Get Calender data */ int getCalendarDate(int fd, char *date); #ifdef __cplusplus } #endif libindi/drivers/telescope/magellan1.h0000664000175000017500000000435613263645557017155 0ustar jasemjasem/* MAGELLAN Generic Copyright (C) 2011 Onno Hommes (ohommes@alumni.cmu.edu) 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 */ #pragma once #include "indicom.h" #include "indidevapi.h" /* The device name below eventhough we have a Magellan I should remain set to a KStars registered telescope so It allows the service to be stopped */ #define mydev "Magellan I" /* The device name */ class Magellan1 { public: Magellan1(); virtual ~Magellan1(); virtual void ISGetProperties(const char *dev); virtual void ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n); virtual void ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n); virtual void ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n); virtual void ISSnoopDevice(XMLEle *root); virtual void ISPoll(); virtual void getBasicData(); void handleError(ISwitchVectorProperty *svp, int err, const char *msg); void handleError(INumberVectorProperty *nvp, int err, const char *msg); void handleError(ITextVectorProperty *tvp, int err, const char *msg); bool isTelescopeOn(); void connectTelescope(); void setCurrentDeviceName(const char *devName); void correctFault(); int fd; protected: int timeFormat; int currentSiteNum; int trackingMode; double JD; double lastRA; double lastDEC; bool fault; bool simulation; char thisDevice[64]; int currentSet; int lastSet; double targetRA, targetDEC; }; libindi/drivers/telescope/lx200gemini.h0000664000175000017500000000700513263645557017344 0ustar jasemjasem/* Losmandy Gemini INDI driver Copyright (C) 2017 Jasem Mutlaq Difference from LX200 Generic: 1. Added Side of Pier 2. Reimplemented isSlewComplete to use :Gv# since it is more reliable 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 */ #pragma once #include "lx200generic.h" class LX200Gemini : public LX200Generic { public: LX200Gemini(); ~LX200Gemini() {} virtual void ISGetProperties(const char *dev) override; virtual bool ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) override; virtual bool ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) override; protected: virtual const char *getDefaultName() override; virtual bool Connect() override; virtual bool initProperties() override ; virtual bool updateProperties() override; virtual bool isSlewComplete() override; virtual bool ReadScopeStatus() override; virtual bool Park()override ; virtual bool UnPark() override; virtual bool SetTrackMode(uint8_t mode) override; virtual bool SetTrackEnabled(bool enabled) override; virtual bool checkConnection() override; virtual bool saveConfigItems(FILE *fp) override; private: void syncSideOfPier(); bool sleepMount(); bool wakeupMount(); bool getGeminiProperty(uint8_t propertyNumber, char* value); bool setGeminiProperty(uint8_t propertyNumber, char* value); // Checksum for private commands uint8_t calculateChecksum(char *cmd); INumber ManualSlewingSpeedN[1]; INumberVectorProperty ManualSlewingSpeedNP; INumber GotoSlewingSpeedN[1]; INumberVectorProperty GotoSlewingSpeedNP; INumber MoveSpeedN[1]; INumberVectorProperty MoveSpeedNP; INumber GuidingSpeedN[1]; INumberVectorProperty GuidingSpeedNP; INumber CenteringSpeedN[1]; INumberVectorProperty CenteringSpeedNP; ISwitch ParkSettingsS[3]; ISwitchVectorProperty ParkSettingsSP; enum { PARK_HOME, PARK_STARTUP, PARK_ZENITH }; ISwitch StartupModeS[3]; ISwitchVectorProperty StartupModeSP; enum { COLD_START, WARM_START, WARM_RESTART }; enum { GEMINI_TRACK_SIDEREAL, GEMINI_TRACK_KING, GEMINI_TRACK_LUNAR, GEMINI_TRACK_SOLAR }; enum MovementState { NO_MOVEMENT, TRACKING, GUIDING, CENTERING, SLEWING, STALLED }; enum ParkingState { NOT_PARKED, PARKED, PARK_IN_PROGRESS }; const uint8_t GEMINI_TIMEOUT = 3; void setTrackState(INDI::Telescope::TelescopeStatus state); void updateMovementState(); MovementState getMovementState(); ParkingState getParkingState(); ParkingState priorParkingState = PARK_IN_PROGRESS; }; libindi/drivers/telescope/lx200ap_experimentaldriver.h0000664000175000017500000000216213263645557022464 0ustar jasemjasem/* LX200 AP Driver Copyright (C) 2007 Markus Wildi 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 */ #pragma once #ifdef __cplusplus extern "C" { #endif void set_lx200ap_exp_name(const char *deviceName, unsigned int debug_level); int setAPMeridianDelay(int fd, double mdelay); int getAPMeridianDelay(int fd, double *mdelay); int check_lx200ap_status(int fd, char *parkStatus, char *slewStatus); #ifdef __cplusplus } #endif libindi/drivers/telescope/lx200_OnStep.cpp0000664000175000017500000013333613263645557020005 0ustar jasemjasem/* LX200 LX200_OnStep Based on LX200 classic, azwing (alain@zwingelstein.org) Contributors: James Lan https://github.com/james-lan Ray Wells https://github.com/blueshawk Copyright (C) 2003 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 */ #include "lx200_OnStep.h" #define LIBRARY_TAB "Library" #define FIRMWARE_TAB "Firmware data" #define STATUS_TAB "ONStep Status" #define ONSTEP_TIMEOUT 3 #define RB_MAX_LEN 64 LX200_OnStep::LX200_OnStep() : LX200Generic() { currentCatalog = LX200_STAR_C; currentSubCatalog = 0; setVersion(1, 3); setLX200Capability(LX200_HAS_TRACKING_FREQ |LX200_HAS_SITES | LX200_HAS_ALIGNMENT_TYPE | LX200_HAS_PULSE_GUIDING); SetTelescopeCapability(GetTelescopeCapability() | TELESCOPE_CAN_CONTROL_TRACK | TELESCOPE_HAS_PEC | TELESCOPE_HAS_PIER_SIDE | TELESCOPE_HAS_TRACK_RATE, 4 ); //CAN_ABORT, CAN_GOTO ,CAN_PARK ,CAN_SYNC ,HAS_LOCATION ,HAS_TIME ,HAS_TRACK_MODEAlready inherited from lx200generic, // 4 stands for the number of Slewrate Buttons as defined in Inditelescope.cpp //setLX200Capability(LX200_HAS_FOCUS | LX200_HAS_TRACKING_FREQ | LX200_HAS_ALIGNMENT_TYPE | LX200_HAS_SITES | LX200_HAS_PULSE_GUIDING); // // Get generic capabilities but discard the followng: // LX200_HAS_FOCUS } const char *LX200_OnStep::getDefaultName() { return (const char *)"LX200 OnStep"; } bool LX200_OnStep::initProperties() { LX200Generic::initProperties(); SetParkDataType(PARK_RA_DEC); // ============== MAIN_CONTROL_TAB IUFillSwitch(&ReticS[0], "PLUS", "Light", ISS_OFF); IUFillSwitch(&ReticS[1], "MOINS", "Dark", ISS_OFF); IUFillSwitchVector(&ReticSP, ReticS, 2, getDeviceName(), "RETICULE_BRIGHTNESS", "Reticule +/-", MAIN_CONTROL_TAB, IP_RW, ISR_ATMOST1, 60, IPS_ALERT); IUFillSwitch(&OSAlignS[0], "1", "1 Star", ISS_OFF); IUFillSwitch(&OSAlignS[1], "2", "2 Stars", ISS_OFF); IUFillSwitch(&OSAlignS[2], "3", "3 Stars", ISS_OFF); IUFillSwitch(&OSAlignS[3], "4", "Align", ISS_OFF); IUFillSwitchVector(&OSAlignSP, OSAlignS, 4, getDeviceName(), "AlignStar", "Align using n stars", MAIN_CONTROL_TAB, IP_RW, ISR_ATMOST1, 0, IPS_IDLE); IUFillText(&OSAlignT[0], "OSStarAlign", "Align x Star(s)", "Manual Alignment Process Idle"); IUFillTextVector(&OSAlignTP, OSAlignT, 1, getDeviceName(), "Align Process", "", MAIN_CONTROL_TAB, IP_RO, 0, IPS_IDLE); IUFillNumber(&ElevationLimitN[0], "minAlt", "Elev Min", "%+03f", -90.0, 90.0, 1.0, -30.0); IUFillNumber(&ElevationLimitN[1], "maxAlt", "Elev Max", "%+03f", -90.0, 90.0, 1.0, 89.0); IUFillNumberVector(&ElevationLimitNP, ElevationLimitN, 2, getDeviceName(), "Slew elevation Limit", "", MAIN_CONTROL_TAB, IP_RW, 0, IPS_IDLE); IUFillText(&ObjectInfoT[0], "Info", "", ""); IUFillTextVector(&ObjectInfoTP, ObjectInfoT, 1, getDeviceName(), "Object Info", "", MAIN_CONTROL_TAB, IP_RO, 0, IPS_IDLE); // ============== CONNECTION_TAB // ============== OPTION_TAB IUFillNumber(&BacklashN[0], "Backlash DEC", "DE", "%g", 0, 999, 1, 15); IUFillNumber(&BacklashN[1], "Backlash RA", "RA", "%g", 0, 999, 1, 15); IUFillNumberVector(&BacklashNP, BacklashN, 2, getDeviceName(), "Backlash", "", MOTION_TAB, IP_RW, 0,IPS_IDLE); // ============== MOTION_CONTROL_TAB IUFillNumber(&MaxSlewRateN[0], "maxSlew", "Rate", "%g", 1.0, 9.0, 1.0, 5.0); //2.0, 9.0, 1.0, 9.0 IUFillNumberVector(&MaxSlewRateNP, MaxSlewRateN, 1, getDeviceName(), "Max slew Rate", "", MOTION_TAB, IP_RW, 0,IPS_IDLE); IUFillSwitch(&TrackCompS[0], "1", "Full Compensation", ISS_OFF); IUFillSwitch(&TrackCompS[1], "2", "Refraction", ISS_OFF); IUFillSwitch(&TrackCompS[2], "3", "Off", ISS_OFF); IUFillSwitchVector(&TrackCompSP, TrackCompS, 3, getDeviceName(), "Compensation", "Compensation Tracking", MOTION_TAB, IP_RW, ISR_1OFMANY, 0, IPS_IDLE); // ============== SITE_MANAGEMENT_TAB IUFillSwitch(&SetHomeS[0], "COLD_START", "Cold Start", ISS_OFF); IUFillSwitch(&SetHomeS[1], "WARM_START", "Init Home", ISS_OFF); IUFillSwitchVector(&SetHomeSP, SetHomeS, 2, getDeviceName(), "HOME_INIT", "Homing", SITE_TAB, IP_RW, ISR_ATMOST1, 60, IPS_ALERT); // ============== GUIDE_TAB // ============== FOCUSER_TAB // Focuser 1 //IUFillSwitch(&OSFocus1SelS[0], "Focus1_Sel1", "Foc 1", ISS_OFF); //IUFillSwitch(&OSFocus1SelS[1], "Focus1_Sel2", "Foc 2", ISS_OFF); //IUFillSwitchVector(&OSFocus1SelSP, OSFocus1SelS, 2, getDeviceName(), "Foc1Sel", "Focuser 1", FOCUS_TAB, IP_RW, ISR_ATMOST1, 0, IPS_IDLE); IUFillSwitch(&OSFocus1MotionS[0], "Focus1_In", "In", ISS_OFF); IUFillSwitch(&OSFocus1MotionS[1], "Focus1_Out", "Out", ISS_OFF); IUFillSwitch(&OSFocus1MotionS[2], "Focus1_Stop", "Stop", ISS_OFF); IUFillSwitchVector(&OSFocus1MotionSP, OSFocus1MotionS, 3, getDeviceName(), "Foc1Mot", "Foc 1 Motion", FOCUS_TAB, IP_RW, ISR_ATMOST1, 0, IPS_IDLE); IUFillSwitch(&OSFocus1RateS[0], "Focus1_1", "min", ISS_OFF); IUFillSwitch(&OSFocus1RateS[1], "Focus1_2", "0.01", ISS_OFF); IUFillSwitch(&OSFocus1RateS[2], "Focus1_3", "0.1", ISS_OFF); IUFillSwitch(&OSFocus1RateS[3], "Focus1_4", "1", ISS_OFF); IUFillSwitchVector(&OSFocus1RateSP, OSFocus1RateS, 4, getDeviceName(), "Foc1Rate", "Foc 1 Rates", FOCUS_TAB, IP_RW, ISR_ATMOST1, 0, IPS_IDLE); IUFillNumber(&OSFocus1TargN[0], "FocusTarget1", "Abs Pos", "%g", -25000, 25000, 1, 0); IUFillNumberVector(&OSFocus1TargNP, OSFocus1TargN, 1, getDeviceName(), "Foc1Targ", "Foc 1 Target", FOCUS_TAB, IP_RW, 0,IPS_IDLE); // Focuser 2 //IUFillSwitch(&OSFocus2SelS[0], "Focus2_Sel1", "Foc 1", ISS_OFF); //IUFillSwitch(&OSFocus2SelS[1], "Focus2_Sel2", "Foc 2", ISS_OFF); //IUFillSwitchVector(&OSFocus2SelSP, OSFocus2SelS, 2, getDeviceName(), "Foc2Sel", "Foc 2", FOCUS_TAB, IP_RW, ISR_ATMOST1, 0, IPS_IDLE); IUFillSwitch(&OSFocus2MotionS[0], "Focus2_In", "In", ISS_OFF); IUFillSwitch(&OSFocus2MotionS[1], "Focus2_Out", "Out", ISS_OFF); IUFillSwitch(&OSFocus2MotionS[2], "Focus2_Stop", "Stop", ISS_OFF); IUFillSwitchVector(&OSFocus2MotionSP, OSFocus2MotionS, 3, getDeviceName(), "Foc2Mot", "Foc 2 Motion", FOCUS_TAB, IP_RW, ISR_ATMOST1, 0, IPS_IDLE); IUFillSwitch(&OSFocus2RateS[0], "Focus2_1", "min", ISS_OFF); IUFillSwitch(&OSFocus2RateS[1], "Focus2_2", "0.01", ISS_OFF); IUFillSwitch(&OSFocus2RateS[2], "Focus2_3", "0.1", ISS_OFF); IUFillSwitch(&OSFocus2RateS[3], "Focus2_4", "1", ISS_OFF); IUFillSwitchVector(&OSFocus2RateSP, OSFocus2RateS, 4, getDeviceName(), "Foc2Rate", "Foc 2 Rates", FOCUS_TAB, IP_RW, ISR_ATMOST1, 0, IPS_IDLE); IUFillNumber(&OSFocus2TargN[0], "FocusTarget2", "Abs Pos", "%g", -25000, 25000, 1, 0); IUFillNumberVector(&OSFocus2TargNP, OSFocus2TargN, 1, getDeviceName(), "Foc2Targ", "Foc 2 Target", FOCUS_TAB, IP_RW, 0,IPS_IDLE); // ============== FIRMWARE_TAB IUFillText(&VersionT[0], "Date", "", ""); IUFillText(&VersionT[1], "Time", "", ""); IUFillText(&VersionT[2], "Number", "", ""); IUFillText(&VersionT[3], "Name", "", ""); IUFillTextVector(&VersionTP, VersionT, 4, getDeviceName(), "Firmware Info", "", FIRMWARE_TAB, IP_RO, 0, IPS_IDLE); // ============== LIBRARY_TAB IUFillSwitch(&StarCatalogS[0], "Star", "", ISS_ON); IUFillSwitch(&StarCatalogS[1], "SAO", "", ISS_OFF); IUFillSwitch(&StarCatalogS[2], "GCVS", "", ISS_OFF); IUFillSwitchVector(&StarCatalogSP, StarCatalogS, 3, getDeviceName(), "Star Catalogs", "", LIBRARY_TAB, IP_RW, ISR_1OFMANY, 0, IPS_IDLE); IUFillSwitch(&DeepSkyCatalogS[0], "NGC", "", ISS_ON); IUFillSwitch(&DeepSkyCatalogS[1], "IC", "", ISS_OFF); IUFillSwitch(&DeepSkyCatalogS[2], "UGC", "", ISS_OFF); IUFillSwitch(&DeepSkyCatalogS[3], "Caldwell", "", ISS_OFF); IUFillSwitch(&DeepSkyCatalogS[4], "Arp", "", ISS_OFF); IUFillSwitch(&DeepSkyCatalogS[5], "Abell", "", ISS_OFF); IUFillSwitch(&DeepSkyCatalogS[6], "Messier", "", ISS_OFF); IUFillSwitchVector(&DeepSkyCatalogSP, DeepSkyCatalogS, 7, getDeviceName(), "Deep Sky Catalogs", "", LIBRARY_TAB, IP_RW, ISR_1OFMANY, 0, IPS_IDLE); IUFillSwitch(&SolarS[0], "Select", "Select item", ISS_ON); IUFillSwitch(&SolarS[1], "1", "Mercury", ISS_OFF); IUFillSwitch(&SolarS[2], "2", "Venus", ISS_OFF); IUFillSwitch(&SolarS[3], "3", "Moon", ISS_OFF); IUFillSwitch(&SolarS[4], "4", "Mars", ISS_OFF); IUFillSwitch(&SolarS[5], "5", "Jupiter", ISS_OFF); IUFillSwitch(&SolarS[6], "6", "Saturn", ISS_OFF); IUFillSwitch(&SolarS[7], "7", "Uranus", ISS_OFF); IUFillSwitch(&SolarS[8], "8", "Neptune", ISS_OFF); IUFillSwitch(&SolarS[9], "9", "Pluto", ISS_OFF); IUFillSwitchVector(&SolarSP, SolarS, 10, getDeviceName(), "SOLAR_SYSTEM", "Solar System", LIBRARY_TAB, IP_RW, ISR_1OFMANY, 0, IPS_IDLE); IUFillNumber(&ObjectNoN[0], "ObjectN", "Number", "%+03f", 1.0, 1000.0, 1.0, 0); IUFillNumberVector(&ObjectNoNP, ObjectNoN, 1, getDeviceName(), "Object Number", "", LIBRARY_TAB, IP_RW, 0, IPS_IDLE); // ============== STATUS_TAB IUFillText(&OnstepStat[0], ":GU# return", "", ""); IUFillText(&OnstepStat[1], "Tracking", "", ""); IUFillText(&OnstepStat[2], "Refractoring", "", ""); IUFillText(&OnstepStat[3], "Park", "", ""); IUFillText(&OnstepStat[4], "Pec", "", ""); IUFillText(&OnstepStat[5], "TimeSync", "", ""); IUFillText(&OnstepStat[6], "Mount Type", "", ""); IUFillText(&OnstepStat[7], "Error", "", ""); IUFillTextVector(&OnstepStatTP, OnstepStat, 8, getDeviceName(), "OnStep Status", "", STATUS_TAB, IP_RO, 0, IPS_IDLE); return true; } void LX200_OnStep::ISGetProperties(const char *dev) { if (dev != nullptr && strcmp(dev, getDeviceName()) != 0) return; LX200Generic::ISGetProperties(dev); } bool LX200_OnStep::updateProperties() { LX200Generic::updateProperties(); if (isConnected()) { // Firstinitialize some variables // keep sorted by TABs is easier // Main Control defineSwitch(&ReticSP); defineSwitch(&OSAlignSP); defineText(&OSAlignTP); defineNumber(&ElevationLimitNP); defineText(&ObjectInfoTP); // Connection // Options // Motion Control defineNumber(&MaxSlewRateNP); defineSwitch(&TrackCompSP); defineNumber(&BacklashNP); // Site Management defineSwitch(&ParkOptionSP); defineSwitch(&SetHomeSP); // Guide // Focuser // Focuser 1 if (!sendOnStepCommand(":FA#")) // do we have a Focuser 1 { OSFocuser1 = true; //defineSwitch(&OSFocus1SelSP); defineSwitch(&OSFocus1MotionSP); defineSwitch(&OSFocus1RateSP); defineNumber(&OSFocus1TargNP); } // Focuser 2 if (!sendOnStepCommand(":fA#")) // Do we have a Focuser 2 { OSFocuser2 = true; //defineSwitch(&OSFocus2SelSP); defineSwitch(&OSFocus2MotionSP); defineSwitch(&OSFocus2RateSP); defineNumber(&OSFocus2TargNP); } // Firmware Data defineText(&VersionTP); // Library defineSwitch(&SolarSP); defineSwitch(&StarCatalogSP); defineSwitch(&DeepSkyCatalogSP); defineNumber(&ObjectNoNP); // OnStep Status defineText(&OnstepStatTP); if (InitPark()) { // If loading parking data is successful, we just set the default parking values. SetAxis1ParkDefault(LocationN[LOCATION_LATITUDE].value >= 0 ? 0 : 180); SetAxis2ParkDefault(LocationN[LOCATION_LATITUDE].value); } else { // Otherwise, we set all parking data to default in case no parking data is found. SetAxis1Park(LocationN[LOCATION_LATITUDE].value >= 0 ? 0 : 180); SetAxis1ParkDefault(LocationN[LOCATION_LATITUDE].value); SetAxis1ParkDefault(LocationN[LOCATION_LATITUDE].value >= 0 ? 0 : 180); SetAxis2ParkDefault(LocationN[LOCATION_LATITUDE].value); } double longitude=-1000, latitude=-1000; // Get value from config file if it exists. IUGetConfigNumber(getDeviceName(), "GEOGRAPHIC_COORD", "LONG", &longitude); IUGetConfigNumber(getDeviceName(), "GEOGRAPHIC_COORD", "LAT", &latitude); if (longitude != -1000 && latitude != -1000) { updateLocation(latitude, longitude, 0); } } else { // keep sorted by TABs is easier // Main Control deleteProperty(ReticSP.name); deleteProperty(OSAlignSP.name); deleteProperty(OSAlignTP.name); deleteProperty(ElevationLimitNP.name); // Connection // Options // Motion Control deleteProperty(MaxSlewRateNP.name); deleteProperty(TrackCompSP.name); deleteProperty(BacklashNP.name); // Site Management deleteProperty(ParkOptionSP.name); deleteProperty(SetHomeSP.name); // Guide // Focuser // Focuser 1 //deleteProperty(OSFocus1SelSP.name); deleteProperty(OSFocus1MotionSP.name); deleteProperty(OSFocus1RateSP.name); deleteProperty(OSFocus1TargNP.name); // Focuser 2 //deleteProperty(OSFocus2SelSP.name); deleteProperty(OSFocus2MotionSP.name); deleteProperty(OSFocus2RateSP.name); deleteProperty(OSFocus2TargNP.name); // Firmware Data deleteProperty(VersionTP.name); // Library deleteProperty(ObjectInfoTP.name); deleteProperty(SolarSP.name); deleteProperty(StarCatalogSP.name); deleteProperty(DeepSkyCatalogSP.name); deleteProperty(ObjectNoNP.name); // OnStep Status deleteProperty(OnstepStatTP.name); } return true; } bool LX200_OnStep::ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) { if (dev != nullptr && strcmp(dev, getDeviceName()) == 0) { if (!strcmp(name, ObjectNoNP.name)) { char object_name[256]; if (selectCatalogObject(PortFD, currentCatalog, (int)values[0]) < 0) { ObjectNoNP.s = IPS_ALERT; IDSetNumber(&ObjectNoNP, "Failed to select catalog object."); return false; } getLX200RA(PortFD, &targetRA); getLX200DEC(PortFD, &targetDEC); ObjectNoNP.s = IPS_OK; IDSetNumber(&ObjectNoNP, "Object updated."); if (getObjectInfo(PortFD, object_name) < 0) IDMessage(getDeviceName(), "Getting object info failed."); else { IUSaveText(&ObjectInfoTP.tp[0], object_name); IDSetText(&ObjectInfoTP, nullptr); } Goto(targetRA, targetDEC); return true; } if (!strcmp(name, MaxSlewRateNP.name)) // Tested { int ret; char cmd[4]; snprintf(cmd, 4, ":R%d#", (int)values[0]); ret = sendOnStepCommandBlind(cmd); //if (setMaxSlewRate(PortFD, (int)values[0]) < 0) //(int) MaxSlewRateN[0].value if (ret == -1) { LOGF_DEBUG("Pas OK Return value =%d", ret); LOGF_DEBUG("Setting Max Slew Rate to %f\n", values[0]); MaxSlewRateNP.s = IPS_ALERT; IDSetNumber(&MaxSlewRateNP, "Setting Max Slew Rate Failed"); return false; } LOGF_DEBUG("OK Return value =%d", ret); MaxSlewRateNP.s = IPS_OK; MaxSlewRateNP.np[0].value = values[0]; IDSetNumber(&MaxSlewRateNP, "Slewrate set to %04.1f", values[0]); return true; } if (!strcmp(name, BacklashNP.name)) // tested { char cmd[9]; int i, nset; double bklshdec=0, bklshra=0; for (nset = i = 0; i < n; i++) { INumber *bktp = IUFindNumber(&BacklashNP, names[i]); if (bktp == &BacklashN[0]) { bklshdec = values[i]; //LOGF_INFO("===CMD==> Backlash DEC= %f", bklshdec); nset += bklshdec >= 0 && bklshdec <= 999; //range 0 to 999 } else if (bktp == &BacklashN[1]) { bklshra = values[i]; //LOGF_INFO("===CMD==> Backlash RA= %f", bklshra); nset += bklshra >= 0 && bklshra <= 999; //range 0 to 999 } } if (nset == 2) { snprintf(cmd, 9, ":$BD%d#", (int)bklshdec); if (sendOnStepCommand(cmd)) { BacklashNP.s = IPS_ALERT; IDSetNumber(&BacklashNP, "Error Backlash DEC limit."); } usleep(100000); // time for OnStep to respond to previous cmd snprintf(cmd, 9, ":$BR%d#", (int)bklshra); if (sendOnStepCommand(cmd)) { BacklashNP.s = IPS_ALERT; IDSetNumber(&BacklashNP, "Error Backlash RA limit."); } BacklashNP.np[0].value = bklshdec; BacklashNP.np[1].value = bklshra; BacklashNP.s = IPS_OK; IDSetNumber(&BacklashNP, nullptr); return true; } else { BacklashNP.s = IPS_ALERT; IDSetNumber(&BacklashNP, "Backlash invalid."); return false; } } if (!strcmp(name, ElevationLimitNP.name)) // Tested { // new elevation limits double minAlt = 0, maxAlt = 0; int i, nset; for (nset = i = 0; i < n; i++) { INumber *altp = IUFindNumber(&ElevationLimitNP, names[i]); if (altp == &ElevationLimitN[0]) { minAlt = values[i]; nset += minAlt >= -30.0 && minAlt <= 30.0; //range -30 to 30 } else if (altp == &ElevationLimitN[1]) { maxAlt = values[i]; nset += maxAlt >= 60.0 && maxAlt <= 90.0; //range 60 to 90 } } if (nset == 2) { if (setMinElevationLimit(PortFD, (int)minAlt) < 0) { ElevationLimitNP.s = IPS_ALERT; IDSetNumber(&ElevationLimitNP, "Error setting min elevation limit."); } if (setMaxElevationLimit(PortFD, (int)maxAlt) < 0) { ElevationLimitNP.s = IPS_ALERT; IDSetNumber(&ElevationLimitNP, "Error setting max elevation limit."); return false; } ElevationLimitNP.np[0].value = minAlt; ElevationLimitNP.np[1].value = maxAlt; ElevationLimitNP.s = IPS_OK; IDSetNumber(&ElevationLimitNP, nullptr); return true; } else { ElevationLimitNP.s = IPS_IDLE; IDSetNumber(&ElevationLimitNP, "elevation limit missing or invalid."); return false; } } } // Focuser // Focuser 1 Target if (!strcmp(name, OSFocus1TargNP.name)) { char cmd[32]; if ((values[0] >= -25000) && (values[0] <= 25000)) { snprintf(cmd, 15, ":FR%d#", (int)values[0]); sendOnStepCommandBlind(cmd); OSFocus1TargNP.s = IPS_OK; IDSetNumber(&OSFocus1TargNP, "Slewrate set to %d", (int)values[0]); } else { OSFocus1TargNP.s = IPS_ALERT; IDSetNumber(&OSFocus1TargNP, "Setting Max Slew Rate Failed"); } return true; } // Focuser 2 Target if (!strcmp(name, OSFocus2TargNP.name)) { char cmd[32]; if ((values[0] >= -25000) && (values[0] <= 25000)) { snprintf(cmd, 15, ":fR%d#", (int)values[0]); sendOnStepCommandBlind(cmd); OSFocus2TargNP.s = IPS_OK; IDSetNumber(&OSFocus2TargNP, "Slewrate set to %d", (int)values[0]); } else { OSFocus2TargNP.s = IPS_ALERT; IDSetNumber(&OSFocus2TargNP, "Setting Max Slew Rate Failed"); } return true; } return LX200Generic::ISNewNumber(dev, name, values, names, n); } bool LX200_OnStep::ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) { int index = 0; if (dev != nullptr && strcmp(dev, getDeviceName()) == 0) { // Align Buttons if (!strcmp(name, OSAlignSP.name)) // Tested { if (IUUpdateSwitch(&OSAlignSP, states, names, n) < 0) return false; index = IUFindOnSwitchIndex(&OSAlignSP); if (index == 0) { if(sendOnStepCommand(":A1#")) LOG_DEBUG("1 Star"); OSAlignOn=true; } if (index == 1) { if(sendOnStepCommand(":A2#")) LOG_DEBUG("2 Stars"); OSAlignOn=true; } if (index == 2) { if(sendOnStepCommand(":A3#")) LOG_DEBUG("3 Stars"); OSAlignOn=true; } if (index == 3) { if(sendOnStepCommand(":A+#")) LOG_DEBUG("Align"); OSAlignS[3].s=ISS_OFF; } OSAlignSP.s = IPS_OK; IDSetSwitch(&OSAlignSP, nullptr); } // Reticlue +/- Buttons if (!strcmp(name, ReticSP.name)) // Tested { int ret = 0; IUUpdateSwitch(&ReticSP, states, names, n); ReticSP.s = IPS_OK; if (ReticS[0].s == ISS_ON) { ret = ReticPlus(PortFD); ReticS[0].s=ISS_OFF; IDSetSwitch(&ReticSP, "Bright"); } else { ret = ReticMoins(PortFD); ReticS[1].s=ISS_OFF; IDSetSwitch(&ReticSP, "Dark"); } IUResetSwitch(&ReticSP); IDSetSwitch(&ReticSP, nullptr); return true; } // Homing, Cold and Warm Init if (!strcmp(name, SetHomeSP.name)) // Tested { IUUpdateSwitch(&SetHomeSP, states, names, n); SetHomeSP.s = IPS_OK; if (SetHomeS[0].s == ISS_ON) { if(!sendOnStepCommandBlind(":hC#")) return false; IDSetSwitch(&SetHomeSP, "Cold Start"); SetHomeS[0].s = ISS_OFF; } else { if(!sendOnStepCommandBlind(":hF#")) return false; IDSetSwitch(&SetHomeSP, "Home Init"); SetHomeS[1].s = ISS_OFF; } IUResetSwitch(&ReticSP); SetHomeSP.s = IPS_IDLE; IDSetSwitch(&SetHomeSP, nullptr); return true; } // Tracking Compensation selection if (!strcmp(name, TrackCompSP.name)) // Tested { IUUpdateSwitch(&TrackCompSP, states, names, n); TrackCompSP.s = IPS_OK; if (TrackCompS[0].s == ISS_ON) { if (!sendOnStepCommand(":To#")) { IDSetSwitch(&TrackCompSP, "Full Compensated Tracking On"); return true; } } if (TrackCompS[1].s == ISS_ON) { if (!sendOnStepCommand(":Tr#")) { IDSetSwitch(&TrackCompSP, "Refraction Tracking On"); return true; } } if (TrackCompS[2].s == ISS_ON) { if (!sendOnStepCommand(":Tn#")) { IDSetSwitch(&TrackCompSP, "Refraction Tracking Disabled"); return true; } } IUResetSwitch(&TrackCompSP); TrackCompSP.s = IPS_IDLE; IDSetSwitch(&TrackCompSP, nullptr); return true; } // Focuser // Focuser 1 Rates if (!strcmp(name, OSFocus1RateSP.name)) { char cmd[32]; if (IUUpdateSwitch(&OSFocus1RateSP, states, names, n) < 0) return false; index = IUFindOnSwitchIndex(&OSFocus1RateSP); snprintf(cmd, 5, ":F%d#", index+1); sendOnStepCommandBlind(cmd); OSFocus1RateS[index].s=ISS_OFF; OSFocus1RateSP.s = IPS_OK; IDSetSwitch(&OSFocus1RateSP, nullptr); } // Focuser 1 Motion if (!strcmp(name, OSFocus1MotionSP.name)) { char cmd[32]; if (IUUpdateSwitch(&OSFocus1MotionSP, states, names, n) < 0) return false; index = IUFindOnSwitchIndex(&OSFocus1MotionSP); if (index ==0) { strcpy(cmd, ":F+#"); } if (index ==1) { strcpy(cmd, ":F-#"); } if (index ==2) { strcpy(cmd, ":FQ#"); } sendOnStepCommandBlind(cmd); usleep(100000); // Pulse 0,1 s if(index != 2) { sendOnStepCommandBlind(":FQ#"); } OSFocus1MotionS[index].s=ISS_OFF; OSFocus1MotionSP.s = IPS_OK; IDSetSwitch(&OSFocus1MotionSP, nullptr); } // Focuser 2 Rates if (!strcmp(name, OSFocus2RateSP.name)) { char cmd[32]; if (IUUpdateSwitch(&OSFocus2RateSP, states, names, n) < 0) return false; index = IUFindOnSwitchIndex(&OSFocus2RateSP); snprintf(cmd, 5, ":F%d#", index+1); sendOnStepCommandBlind(cmd); OSFocus2RateS[index].s=ISS_OFF; OSFocus2RateSP.s = IPS_OK; IDSetSwitch(&OSFocus2RateSP, nullptr); } // Focuser 2 Motion if (!strcmp(name, OSFocus2MotionSP.name)) { char cmd[32]; if (IUUpdateSwitch(&OSFocus2MotionSP, states, names, n) < 0) return false; index = IUFindOnSwitchIndex(&OSFocus2MotionSP); if (index ==0) { strcpy(cmd, ":f+#"); } if (index ==1) { strcpy(cmd, ":f-#"); } if (index ==2) { strcpy(cmd, ":fQ#"); } sendOnStepCommandBlind(cmd); usleep(100000); // Pulse 0,1 s if(index != 2) { sendOnStepCommandBlind(":fQ#"); } OSFocus2MotionS[index].s=ISS_OFF; OSFocus2MotionSP.s = IPS_OK; IDSetSwitch(&OSFocus2MotionSP, nullptr); } // Star Catalog if (!strcmp(name, StarCatalogSP.name)) { IUResetSwitch(&StarCatalogSP); IUUpdateSwitch(&StarCatalogSP, states, names, n); index = IUFindOnSwitchIndex(&StarCatalogSP); currentCatalog = LX200_STAR_C; if (selectSubCatalog(PortFD, currentCatalog, index)) { currentSubCatalog = index; StarCatalogSP.s = IPS_OK; IDSetSwitch(&StarCatalogSP, nullptr); return true; } else { StarCatalogSP.s = IPS_IDLE; IDSetSwitch(&StarCatalogSP, "Catalog unavailable."); return false; } } // Deep sky catalog if (!strcmp(name, DeepSkyCatalogSP.name)) { IUResetSwitch(&DeepSkyCatalogSP); IUUpdateSwitch(&DeepSkyCatalogSP, states, names, n); index = IUFindOnSwitchIndex(&DeepSkyCatalogSP); if (index == LX200_MESSIER_C) { currentCatalog = index; DeepSkyCatalogSP.s = IPS_OK; IDSetSwitch(&DeepSkyCatalogSP, nullptr); } else currentCatalog = LX200_DEEPSKY_C; if (selectSubCatalog(PortFD, currentCatalog, index)) { currentSubCatalog = index; DeepSkyCatalogSP.s = IPS_OK; IDSetSwitch(&DeepSkyCatalogSP, nullptr); } else { DeepSkyCatalogSP.s = IPS_IDLE; IDSetSwitch(&DeepSkyCatalogSP, "Catalog unavailable"); return false; } return true; } // Solar system if (!strcmp(name, SolarSP.name)) { if (IUUpdateSwitch(&SolarSP, states, names, n) < 0) return false; index = IUFindOnSwitchIndex(&SolarSP); // We ignore the first option : "Select item" if (index == 0) { SolarSP.s = IPS_IDLE; IDSetSwitch(&SolarSP, nullptr); return true; } selectSubCatalog(PortFD, LX200_STAR_C, LX200_STAR); selectCatalogObject(PortFD, LX200_STAR_C, index + 900); ObjectNoNP.s = IPS_OK; SolarSP.s = IPS_OK; getObjectInfo(PortFD, ObjectInfoTP.tp[0].text); IDSetNumber(&ObjectNoNP, "Object updated."); IDSetSwitch(&SolarSP, nullptr); if (currentCatalog == LX200_STAR_C || currentCatalog == LX200_DEEPSKY_C) selectSubCatalog(PortFD, currentCatalog, currentSubCatalog); getObjectRA(PortFD, &targetRA); getObjectDEC(PortFD, &targetDEC); Goto(targetRA, targetDEC); return true; } } return LX200Generic::ISNewSwitch(dev, name, states, names, n); } void LX200_OnStep::getBasicData() { // process parent LX200Generic::getBasicData(); if (!isSimulation()) { char buffer[128]; getVersionDate(PortFD, buffer); IUSaveText(&VersionT[0], buffer); getVersionTime(PortFD, buffer); IUSaveText(&VersionT[1], buffer); getVersionNumber(PortFD, buffer); IUSaveText(&VersionT[2], buffer); getProductName(PortFD, buffer); IUSaveText(&VersionT[3], buffer); IDSetText(&VersionTP, nullptr); if (InitPark()) { // If loading parking data is successful, we just set the default parking values. LOG_INFO("=============== Parkdata loaded"); //SetAxis1ParkDefault(currentRA); //SetAxis2ParkDefault(currentDEC); } else { // Otherwise, we set all parking data to default in case no parking data is found. LOG_INFO("=============== Parkdata Load Failed"); //SetAxis1Park(currentRA); //SetAxis2Park(currentDEC); //SetAxis1ParkDefault(currentRA); //SetAxis2ParkDefault(currentDEC); } } } //======================== Parking ======================= bool LX200_OnStep::SetCurrentPark() // Tested { char response[32]; if(!getCommandString(PortFD, response, ":hQ#")) { LOGF_WARN("===CMD==> Set Park Pos %s", response); return false; } SetAxis1Park(currentRA); SetAxis2Park(currentDEC); LOG_WARN("Park Value set to current postion"); return true; } bool LX200_OnStep::SetDefaultPark() // Tested { IDMessage(getDeviceName(), "Setting Park Data to Default."); SetAxis1Park(20); SetAxis2Park(80); LOG_WARN("Park Position set to Default value, 20/80"); return true; } bool LX200_OnStep::UnPark() // Tested { char response[32]; if (!isSimulation()) { if(!getCommandString(PortFD, response, ":hR#")) { return false; } } return true; } bool LX200_OnStep::Park() // Tested { if (!isSimulation()) { // If scope is moving, let's stop it first. if (EqNP.s == IPS_BUSY) { if (!isSimulation() && abortSlew(PortFD) < 0) { AbortSP.s = IPS_ALERT; IDSetSwitch(&AbortSP, "Abort slew failed."); return false; } AbortSP.s = IPS_OK; EqNP.s = IPS_IDLE; IDSetSwitch(&AbortSP, "Slew aborted."); IDSetNumber(&EqNP, nullptr); if (MovementNSSP.s == IPS_BUSY || MovementWESP.s == IPS_BUSY) { MovementNSSP.s = MovementWESP.s = IPS_IDLE; EqNP.s = IPS_IDLE; IUResetSwitch(&MovementNSSP); IUResetSwitch(&MovementWESP); IDSetSwitch(&MovementNSSP, nullptr); IDSetSwitch(&MovementWESP, nullptr); } } if (!isSimulation() && slewToPark(PortFD) < 0) { ParkSP.s = IPS_ALERT; IDSetSwitch(&ParkSP, "Parking Failed."); return false; } } ParkSP.s = IPS_BUSY; return true; } // Periodically Polls OnStep Parameter from controller bool LX200_OnStep::ReadScopeStatus() // Tested { char OSbacklashDEC[5]; char OSbacklashRA[5]; Errors Lasterror = ERR_NONE; if (isSimulation()) //if Simulation is selected { mountSim(); return true; } if (getLX200RA(PortFD, ¤tRA) < 0 || getLX200DEC(PortFD, ¤tDEC) < 0) // Update actual position { EqNP.s = IPS_ALERT; IDSetNumber(&EqNP, "Error reading RA/DEC."); return false; } NewRaDec(currentRA, currentDEC); // Update Scope Position getCommandString(PortFD,OSStat,":GU#"); // :GU# returns a string containg controller status if (strcmp(OSStat,OldOSStat) != 0) //if status changed { // ============= Telescope Status strcpy(OldOSStat ,OSStat); IUSaveText(&OnstepStat[0],OSStat); if (strstr(OSStat,"n") && strstr(OSStat,"N")) {IUSaveText(&OnstepStat[1],"Tracking Off"); } if (strstr(OSStat,"n") && !strstr(OSStat,"N")) { IUSaveText(&OnstepStat[1],"Sleewing"); TrackState=SCOPE_SLEWING; } if (strstr(OSStat,"N") && !strstr(OSStat,"n")) { IUSaveText(&OnstepStat[1],"Tracking"); TrackState=SCOPE_TRACKING; } // ============= Refractoring if (strstr(OSStat,"r")) {IUSaveText(&OnstepStat[2],"Refractoring On"); } if (strstr(OSStat,"s")) {IUSaveText(&OnstepStat[2],"Refractoring Off"); } if (strstr(OSStat,"r") && strstr(OSStat,"t")) {IUSaveText(&OnstepStat[2],"Full Comp"); } if (strstr(OSStat,"r") && !strstr(OSStat,"t")) { IUSaveText(&OnstepStat[2],"Refractory Comp"); } // ============= Parkstatus if(FirstRead) // it is the first time I read the status so I need to update { if (strstr(OSStat,"P")) { TrackState=SCOPE_PARKED; SetParked(true); IUSaveText(&OnstepStat[3],"Parked"); } if (strstr(OSStat,"F")) { TrackState=SCOPE_IDLE; SetParked(false); IUSaveText(&OnstepStat[3],"Parking Failed"); } if (strstr(OSStat,"I")) { TrackState=SCOPE_PARKING; SetParked(false); IUSaveText(&OnstepStat[3],"Park in Progress"); } if (strstr(OSStat,"p")) { TrackState=SCOPE_IDLE; SetParked(false); IUSaveText(&OnstepStat[3],"UnParked"); } FirstRead=false; } else { if (!isParked()) { if(strstr(OSStat,"P")) { TrackState=SCOPE_PARKED; SetParked(true); IUSaveText(&OnstepStat[3],"Parked"); //LOG_INFO("OnStep Parking Succeded"); } if (strstr(OSStat,"I")) { TrackState=SCOPE_PARKING; SetParked(false); IUSaveText(&OnstepStat[3],"Park in Progress"); LOG_INFO("OnStep Parking in Progress..."); } } if (isParked()) { if (strstr(OSStat,"F")) { TrackState=SCOPE_IDLE; SetParked(false); IUSaveText(&OnstepStat[3],"Parking Failed"); LOG_ERROR("OnStep Parking failed, need to re Init OnStep at home"); } if (strstr(OSStat,"p")) { //TrackState=SCOPE_IDLE; SetParked(false); IUSaveText(&OnstepStat[3],"UnParked"); //LOG_INFO("OnStep Unparked..."); } } } //if (strstr(OSStat,"H")) { IUSaveText(&OnstepStat[3],"At Home"); } if (strstr(OSStat,"H") && strstr(OSStat,"P")) { IUSaveText(&OnstepStat[3],"At Home and Parked"); } if (strstr(OSStat,"H") && strstr(OSStat,"p")) { IUSaveText(&OnstepStat[3],"At Home and UnParked"); } if (strstr(OSStat,"W")) { IUSaveText(&OnstepStat[3],"Waiting at Home"); } // ============= Pec Status if (!strstr(OSStat,"R") && !strstr(OSStat,"W")) { IUSaveText(&OnstepStat[4],"N/A"); } if (strstr(OSStat,"R")) { IUSaveText(&OnstepStat[4],"Recorded"); } if (strstr(OSStat,"W")) { IUSaveText(&OnstepStat[4],"Autorecord"); } // ============= Time Sync Status if (!strstr(OSStat,"S")) { IUSaveText(&OnstepStat[5],"N/A"); } if (strstr(OSStat,"S")) { IUSaveText(&OnstepStat[5],"PPS / GPS Sync Ok"); } // ============= Mount Types if (strstr(OSStat,"E")) { IUSaveText(&OnstepStat[6],"German Mount"); } if (strstr(OSStat,"K")) { IUSaveText(&OnstepStat[6],"Fork Mount"); } if (strstr(OSStat,"k")) { IUSaveText(&OnstepStat[6],"Fork Alt Mount"); } if (strstr(OSStat,"A")) { IUSaveText(&OnstepStat[6],"AltAZ Mount"); } // ============= Error Code Lasterror=(Errors)(OSStat[strlen(OSStat)-1]-'0'); if (Lasterror==ERR_NONE) { IUSaveText(&OnstepStat[7],"None"); } if (Lasterror==ERR_MOTOR_FAULT) { IUSaveText(&OnstepStat[7],"Motor Fault"); } if (Lasterror==ERR_ALT) { IUSaveText(&OnstepStat[7],"Altitude Min/Max"); } if (Lasterror==ERR_LIMIT_SENSE) { IUSaveText(&OnstepStat[7],"Limit Sense"); } if (Lasterror==ERR_DEC) { IUSaveText(&OnstepStat[7],"Dec Limit Exceeded"); } if (Lasterror==ERR_AZM) { IUSaveText(&OnstepStat[7],"Azm Limit Exceeded"); } if (Lasterror==ERR_UNDER_POLE) { IUSaveText(&OnstepStat[7],"Under Pole Limit Exceeded"); } if (Lasterror==ERR_MERIDIAN) { IUSaveText(&OnstepStat[7],"Meridian Limit (W) Exceeded"); } if (Lasterror==ERR_SYNC) { IUSaveText(&OnstepStat[7],"Sync. ignored >30°"); } } // Get actual Pier Side getCommandString(PortFD,OSPier,":Gm#"); if (strcmp(OSPier, OldOSPier) !=0) // any change ? { strcpy(OldOSPier, OSPier); switch(OSPier[0]) { case 'E': setPierSide(PIER_EAST); break; case 'W': setPierSide(PIER_WEST); break; case 'N': setPierSide(PIER_UNKNOWN); break; case '?': setPierSide(PIER_UNKNOWN); break; } } //========== Get actual Backlash values getCommandString(PortFD,OSbacklashDEC, ":%BD#"); getCommandString(PortFD,OSbacklashRA, ":%BR#"); BacklashNP.np[0].value = atof(OSbacklashDEC); BacklashNP.np[1].value = atof(OSbacklashRA); IDSetNumber(&BacklashNP, nullptr); // Update OnStep Status TAB IDSetText(&OnstepStatTP, "==> Update OnsTep Status"); if (OSAlignOn) //don't Poll if no Aligning { if(!GetAlignStatus()) LOG_WARN("Fail Align Command"); } OSUpdateFocuser(); // Update Focuser Position return true; } bool LX200_OnStep::SetTrackEnabled(bool enabled) //track On/Off events handled by inditelescope Tested { char response[32]; if (enabled) { if(!getCommandString(PortFD, response, ":Te#")) { LOGF_ERROR("===CMD==> Track On %s", response); return false; } } else { if(!getCommandString(PortFD, response, ":Td#")) { LOGF_ERROR("===CMD==> Track Off %s", response); return false; } } return true; } bool LX200_OnStep::setLocalDate(uint8_t days, uint8_t months, uint16_t years) // Tested { years = years % 100; char cmd[32]; snprintf(cmd, 32, ":SC%02d/%02d/%02d#", months, days, years); if (!sendOnStepCommand(cmd)) return true; return false; } bool LX200_OnStep::sendOnStepCommandBlind(const char *cmd) { int error_type; int nbytes_write = 0; DEBUGF(DBG_SCOPE, "CMD <%s>", cmd); tcflush(PortFD, TCIFLUSH); if ((error_type = tty_write_string(PortFD, cmd, &nbytes_write)) != TTY_OK) return error_type; return 1; } bool LX200_OnStep::sendOnStepCommand(const char *cmd) // Tested { char response[1]; int error_type; int nbytes_write = 0, nbytes_read = 0; DEBUGF(DBG_SCOPE, "CMD <%s>", cmd); tcflush(PortFD, TCIFLUSH); if ((error_type = tty_write_string(PortFD, cmd, &nbytes_write)) != TTY_OK) return error_type; error_type = tty_read(PortFD, response, 1, ONSTEP_TIMEOUT, &nbytes_read); tcflush(PortFD, TCIFLUSH); if (nbytes_read < 1) { LOG_ERROR("Unable to parse response."); return error_type; } return (response[0] == '0'); } bool LX200_OnStep::updateLocation(double latitude, double longitude, double elevation) // Tested { INDI_UNUSED(elevation); if (isSimulation()) return true; double onstep_long = 360 - longitude ; if (onstep_long < -180) onstep_long += 360; if (onstep_long > 180) onstep_long -= 360; if (!isSimulation() && setSiteLongitude(PortFD, onstep_long) < 0) { LOG_ERROR("Error setting site longitude coordinates"); return false; } if (!isSimulation() && setSiteLatitude(PortFD, latitude) < 0) { LOG_ERROR("Error setting site latitude coordinates"); return false; } char l[32]={0}, L[32]={0}; fs_sexa(l, latitude, 3, 3600); fs_sexa(L, longitude, 4, 3600); LOGF_INFO("Site location updated to Lat %.32s - Long %.32s", l, L); return true; } int LX200_OnStep::setMaxElevationLimit(int fd, int max) // According to standard command is :SoDD*# Tested { LOGF_INFO("<%s>", __FUNCTION__); char read_buffer[RB_MAX_LEN]={0}; snprintf(read_buffer, sizeof(read_buffer), ":So%02d#", max); return (setStandardProcedure(fd, read_buffer)); } int LX200_OnStep::setSiteLongitude(int fd, double Long) { //DEBUGFDEVICE(lx200Name, DBG_SCOPE, "<%s>", __FUNCTION__); int d, m, s; char read_buffer[32]; getSexComponents(Long, &d, &m, &s); snprintf(read_buffer, sizeof(read_buffer), ":Sg%.03d:%02d#", d, m); return (setStandardProcedure(fd, read_buffer)); } bool LX200_OnStep::GetAlignStatus() { char msg[40]; int mx_stars, act_star, nb_stars; if(getCommandString(PortFD, OSAlignStat, ":A?#")) { LOGF_INFO("Align Status response Error, response = %s>", OSAlignStat); return false; } if(strcmp(OSAlignStat, oldOSAlignStat) != 0) //no change { strcpy(oldOSAlignStat, OSAlignStat); mx_stars = OSAlignStat[0] - '0'; act_star = OSAlignStat[1] - '0'; nb_stars = OSAlignStat[2] - '0'; //LOGF_INFO("Response = %s>", OSAlignStat); if (nb_stars !=0) { if (act_star <= nb_stars) { snprintf(msg, sizeof(msg), "%s Manual Align: Star %d/%d", OSAlignStat, act_star, nb_stars ); IUSaveText(&OSAlignT[0],msg); OSAlignProcess=true; } if (act_star > nb_stars) { snprintf(msg, sizeof(msg), "Manual Align: Completed"); OSAlignOn=false; IUSaveText(&OSAlignT[0],msg); } } else { snprintf(msg, sizeof(msg), "Manual Align: Idle"); OSAlignProcess=false; IUSaveText(&OSAlignT[0],msg); } IDSetText(&OSAlignTP, "Alignment Star reached, apply corrections and validate"); } if (OSAlignProcess && TrackState==SCOPE_SLEWING) OSAlignFlag=true; if (OSAlignFlag && TrackState==SCOPE_TRACKING) { OSAlignFlag=false; OSAlignProcess=false; if(kdedialog("kdialog 'OnStep Align' --title 'OnStep Align' --msgbox 'Align Star reached, apply corections and confirm with Align'")) return true; } return true; } bool LX200_OnStep::kdedialog(const char * commande) { return system(commande); } void LX200_OnStep::OSUpdateFocuser() { char value[10]; if(OSFocuser1) { getCommandString(PortFD, value, ":FG#"); OSFocus1TargNP.np[0].value = atoi(value); IDSetNumber(&OSFocus1TargNP, nullptr); } if(OSFocuser2) { getCommandString(PortFD, value, ":fG#"); OSFocus2TargNP.np[0].value = atoi(value); IDSetNumber(&OSFocus2TargNP, nullptr); } } libindi/drivers/telescope/pmc8.cpp0000664000175000017500000006465013263645557016521 0ustar jasemjasem/* INDI Explore Scientific PMC8 driver Copyright (C) 2017 Michael Fulbright Based on IEQPro driver. 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 */ #include "pmc8.h" #include "indicom.h" #include #include #include #include /* Simulation Parameters */ #define SLEWRATE 3 /* slew rate, degrees/s */ #define MOUNTINFO_TAB "Mount Info" std::unique_ptr scope(new PMC8()); void ISGetProperties(const char *dev) { scope->ISGetProperties(dev); } void ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int num) { scope->ISNewSwitch(dev, name, states, names, num); } void ISNewText(const char *dev, const char *name, char *texts[], char *names[], int num) { scope->ISNewText(dev, name, texts, names, num); } void ISNewNumber(const char *dev, const char *name, double values[], char *names[], int num) { scope->ISNewNumber(dev, name, values, names, num); } void ISNewBLOB(const char *dev, const char *name, int sizes[], int blobsizes[], char *blobs[], char *formats[], char *names[], int n) { INDI_UNUSED(dev); INDI_UNUSED(name); INDI_UNUSED(sizes); INDI_UNUSED(blobsizes); INDI_UNUSED(blobs); INDI_UNUSED(formats); INDI_UNUSED(names); INDI_UNUSED(n); } void ISSnoopDevice(XMLEle *root) { scope->ISSnoopDevice(root); } /* Constructor */ PMC8::PMC8() { set_pmc8_device(getDeviceName()); //ctor currentRA = ln_get_apparent_sidereal_time(ln_get_julian_from_sys()); currentDEC = 90; DBG_SCOPE = INDI::Logger::getInstance().addDebugLevel("Scope Verbose", "SCOPE"); SetTelescopeCapability(TELESCOPE_CAN_PARK | TELESCOPE_CAN_SYNC | TELESCOPE_CAN_GOTO | TELESCOPE_CAN_ABORT | TELESCOPE_HAS_TRACK_MODE | TELESCOPE_CAN_CONTROL_TRACK | TELESCOPE_HAS_TRACK_RATE | TELESCOPE_HAS_LOCATION, 4); setVersion(0, 1); } PMC8::~PMC8() { } const char *PMC8::getDefaultName() { return (const char *)"PMC8"; } bool PMC8::initProperties() { INDI::Telescope::initProperties(); /* Tracking Mode */ AddTrackMode("TRACK_SIDEREAL", "Sidereal", true); AddTrackMode("TRACK_SOLAR", "Solar"); AddTrackMode("TRACK_LUNAR", "Lunar"); AddTrackMode("TRACK_CUSTOM", "Custom"); // Set TrackRate limits within +/- 0.0100 of Sidereal rate // TrackRateN[AXIS_RA].min = TRACKRATE_SIDEREAL - 0.01; // TrackRateN[AXIS_RA].max = TRACKRATE_SIDEREAL + 0.01; // TrackRateN[AXIS_DE].min = -0.01; // TrackRateN[AXIS_DE].max = 0.01; // relabel move speeds strcpy(SlewRateSP.sp[0].label, "4x"); strcpy(SlewRateSP.sp[1].label, "16x"); strcpy(SlewRateSP.sp[2].label, "64x"); strcpy(SlewRateSP.sp[3].label, "256x"); /* How fast do we guide compared to sidereal rate */ IUFillNumber(&GuideRateN[0], "GUIDE_RATE", "x Sidereal", "%g", 0.1, 1.0, 0.1, 0.5); IUFillNumberVector(&GuideRateNP, GuideRateN, 1, getDeviceName(), "GUIDE_RATE", "Guiding Rate", MOTION_TAB, IP_RW, 0, IPS_IDLE); initGuiderProperties(getDeviceName(), MOTION_TAB); TrackState = SCOPE_IDLE; SetParkDataType(PARK_RA_DEC); addAuxControls(); IUFillText(&FirmwareT[0], "Version", "Version", ""); IUFillTextVector(&FirmwareTP, FirmwareT, 1, getDeviceName(), "Firmware", "Firmware", MAIN_CONTROL_TAB, IP_RO, 0, IPS_IDLE); return true; } bool PMC8::updateProperties() { INDI::Telescope::updateProperties(); if (isConnected()) { defineNumber(&GuideNSNP); defineNumber(&GuideWENP); defineNumber(&GuideRateNP); defineText(&FirmwareTP); // do not support part position deleteProperty(ParkPositionNP.name); deleteProperty(ParkOptionSP.name); getStartupData(); } else { deleteProperty(GuideNSNP.name); deleteProperty(GuideWENP.name); deleteProperty(GuideRateNP.name); deleteProperty(FirmwareTP.name); } return true; } void PMC8::getStartupData() { LOG_DEBUG("Getting firmware data..."); if (get_pmc8_firmware(PortFD, &firmwareInfo)) { const char *c; // FIXME - Need to add code to get firmware data FirmwareTP.s = IPS_OK; c = firmwareInfo.MainBoardFirmware.c_str(); LOGF_INFO("firmware = %s.", c); IUSaveText(&FirmwareT[0], c); IDSetText(&FirmwareTP, nullptr); } // PMC8 doesn't store location permanently so read from config and set // Convert to INDI standard longitude (0 to 360 Eastward) double longitude; double latitude; longitude = LocationN[LOCATION_LONGITUDE].value; latitude = LocationN[LOCATION_LATITUDE].value; // must also keep "low level" aware of position to convert motor counts to RA/DEC set_pmc8_location(latitude, longitude); // seems like best place to put a warning that will be seen in log window of EKOS/etc LOG_INFO("NOTICE!!!"); LOG_INFO("The PMC-Eight driver is in BETA development currently."); LOG_INFO("When using this driver please remember it is being tested so stay near your mount"); LOG_INFO("and be prepared to intervene if something unexpected occurs."); LOG_INFO("Please read the instructions at:"); LOG_INFO(" http://indilib.org/devices/telescopes/explore-scientific-g11-pmc-eight/"); LOG_INFO("before using this driver!"); #if 0 // FIXEME - Need to handle southern hemisphere for DEC? double HA = ln_get_apparent_sidereal_time(ln_get_julian_from_sys()); double DEC = 90; // currently only park at motor position (0, 0) if (InitPark()) { // If loading parking data is successful, we just set the default parking values. SetAxis1ParkDefault(HA); SetAxis2ParkDefault(DEC); } else { // Otherwise, we set all parking data to default in case no parking data is found. SetAxis1Park(HA); SetAxis2Park(DEC); SetAxis1ParkDefault(HA); SetAxis2ParkDefault(DEC); } #endif #if 0 // FIXME - Need to implement simulation functionality if (isSimulation()) { if (isParked()) set_sim_system_status(ST_PARKED); else set_sim_system_status(ST_STOPPED); } #endif } bool PMC8::ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) { if (!strcmp(dev, getDeviceName())) { // FIXME - will add setting guide rate when firmware supports // Guiding Rate if (!strcmp(name, GuideRateNP.name)) { IUUpdateNumber(&GuideRateNP, values, names, n); if (set_pmc8_guide_rate(PortFD, GuideRateN[0].value)) GuideRateNP.s = IPS_OK; else GuideRateNP.s = IPS_ALERT; IDSetNumber(&GuideRateNP, nullptr); return true; } if (!strcmp(name, GuideNSNP.name) || !strcmp(name, GuideWENP.name)) { processGuiderProperties(name, values, names, n); return true; } } return INDI::Telescope::ISNewNumber(dev, name, values, names, n); } bool PMC8::ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) { if (!strcmp(getDeviceName(), dev)) { } return INDI::Telescope::ISNewSwitch(dev, name, states, names, n); } bool PMC8::ReadScopeStatus() { bool rc = false; if (isSimulation()) mountSim(); bool slewing=false; switch (TrackState) { case SCOPE_SLEWING: // are we done? // check slew state rc = get_pmc8_is_scope_slewing(PortFD, slewing); if (!rc) { LOG_ERROR("PMC8::ReadScopeStatus() - unable to check slew state"); } else { if (slewing == false) { LOG_INFO("Slew complete, tracking..."); TrackState = SCOPE_TRACKING; if (!SetTrackEnabled(true)) { LOG_ERROR("slew complete - unable to enable tracking"); return false; } if (!SetTrackMode(IUFindOnSwitchIndex(&TrackModeSP))) { LOG_ERROR("slew complete - unable to set track mode"); return false; } } } break; case SCOPE_PARKING: // are we done? // are we done? // check slew state rc = get_pmc8_is_scope_slewing(PortFD, slewing); if (!rc) { LOG_ERROR("PMC8::ReadScopeStatus() - unable to check slew state"); } else { if (slewing == false) { if (stop_pmc8_tracking_motion(PortFD)) LOG_DEBUG("Mount tracking is off."); SetParked(true); saveConfig(true); } } break; default: break; } rc = get_pmc8_coords(PortFD, currentRA, currentDEC); if (rc) NewRaDec(currentRA, currentDEC); return rc; } bool PMC8::Goto(double r, double d) { char RAStr[64]={0}, DecStr[64]={0}; targetRA = r; targetDEC = d; fs_sexa(RAStr, targetRA, 2, 3600); fs_sexa(DecStr, targetDEC, 2, 3600); DEBUGF(INDI::Logger::DBG_SESSION,"Slewing to RA: %s - DEC: %s", RAStr, DecStr); if (slew_pmc8(PortFD, r, d) == false) { LOG_ERROR("Failed to slew."); return false; } TrackState = SCOPE_SLEWING; return true; } bool PMC8::Sync(double ra, double dec) { targetRA = ra; targetDEC = dec; char RAStr[64]={0}, DecStr[64]={0}; fs_sexa(RAStr, targetRA, 2, 3600); fs_sexa(DecStr, targetDEC, 2, 3600); DEBUGF(INDI::Logger::DBG_SESSION,"Syncing to RA: %s - DEC: %s", RAStr, DecStr); if (sync_pmc8(PortFD, ra, dec) == false) { LOG_ERROR("Failed to sync."); } EqNP.s = IPS_OK; currentRA = ra; currentDEC = dec; NewRaDec(currentRA, currentDEC); return true; } bool PMC8::Abort() { //GUIDE Abort guide operations. if (GuideNSNP.s == IPS_BUSY || GuideWENP.s == IPS_BUSY) { GuideNSNP.s = GuideWENP.s = IPS_IDLE; GuideNSN[0].value = GuideNSN[1].value = 0.0; GuideWEN[0].value = GuideWEN[1].value = 0.0; if (GuideNSTID) { IERmTimer(GuideNSTID); GuideNSTID = 0; } if (GuideWETID) { IERmTimer(GuideWETID); GuideNSTID = 0; } LOG_INFO("Guide aborted."); IDSetNumber(&GuideNSNP, nullptr); IDSetNumber(&GuideWENP, nullptr); return true; } return abort_pmc8(PortFD); } bool PMC8::Park() { #if 0 // FIXME - Currently only support parking at motor position (0, 0) targetRA = GetAxis1Park(); targetDEC = GetAxis2Park(); if (set_pmc8_radec(PortFD, r, d) == false) { LOG_ERROR("Error setting RA/DEC."); return false; } #endif if (park_pmc8(PortFD)) { TrackState = SCOPE_PARKING; LOG_INFO("Telescope parking in progress to motor position (0, 0)"); return true; } else { return false; } } bool PMC8::UnPark() { if (unpark_pmc8(PortFD)) { SetParked(false); TrackState = SCOPE_IDLE; return true; } else { return false; } } bool PMC8::Handshake() { if (isSimulation()) { set_pmc8_sim_system_status(ST_STOPPED); set_pmc8_sim_track_rate(PMC8_TRACK_SIDEREAL); set_pmc8_sim_move_rate(PMC8_MOVE_64X); // set_pmc8_sim_hemisphere(HEMI_NORTH); } if (check_pmc8_connection(PortFD) == false) return false; return true; } bool PMC8::updateTime(ln_date *utc, double utc_offset) { // mark unused INDI_UNUSED(utc); INDI_UNUSED(utc_offset); LOG_ERROR("PMC8::updateTime() not implemented!"); return false; } bool PMC8::updateLocation(double latitude, double longitude, double elevation) { INDI_UNUSED(elevation); if (longitude > 180) longitude -= 360; // do not support Southern Hemisphere yet! if (latitude < 0) { LOG_ERROR("Southern Hemisphere not currently supported!"); return false; } // must also keep "low level" aware of position to convert motor counts to RA/DEC set_pmc8_location(latitude, longitude); char l[32]={0}, L[32]={0}; fs_sexa(l, latitude, 3, 3600); fs_sexa(L, longitude, 4, 3600); LOGF_INFO("Site location updated to Lat %.32s - Long %.32s", l, L); return true; } void PMC8::debugTriggered(bool enable) { set_pmc8_debug(enable); } void PMC8::simulationTriggered(bool enable) { set_pmc8_simulation(enable); } bool PMC8::MoveNS(INDI_DIR_NS dir, TelescopeMotionCommand command) { if (TrackState == SCOPE_PARKED) { LOG_ERROR("Please unpark the mount before issuing any motion commands."); return false; } // read desired move rate int currentIndex = IUFindOnSwitchIndex(&SlewRateSP); LOGF_DEBUG("MoveNS at slew index %d", currentIndex); switch (command) { case MOTION_START: if (start_pmc8_motion(PortFD, (dir == DIRECTION_NORTH ? PMC8_N : PMC8_S), currentIndex) == false) { LOG_ERROR("Error setting N/S motion direction."); return false; } else { LOGF_INFO("Moving toward %s.", (dir == DIRECTION_NORTH) ? "North" : "South"); } break; case MOTION_STOP: if (stop_pmc8_motion(PortFD, (dir == DIRECTION_NORTH ? PMC8_N : PMC8_S)) == false) { LOG_ERROR("Error stopping N/S motion."); return false; } else { LOGF_INFO("%s motion stopped.", (dir == DIRECTION_NORTH) ? "North" : "South"); } break; } return true; } bool PMC8::MoveWE(INDI_DIR_WE dir, TelescopeMotionCommand command) { if (TrackState == SCOPE_PARKED) { LOG_ERROR("Please unpark the mount before issuing any motion commands."); return false; } // read desired move rate int currentIndex = IUFindOnSwitchIndex(&SlewRateSP); LOGF_DEBUG("MoveWE at slew index %d", currentIndex); switch (command) { case MOTION_START: if (start_pmc8_motion(PortFD, (dir == DIRECTION_WEST ? PMC8_W : PMC8_E), currentIndex) == false) { LOG_ERROR("Error setting N/S motion direction."); return false; } else { LOGF_INFO("Moving toward %s.", (dir == DIRECTION_WEST) ? "West" : "East"); } break; case MOTION_STOP: if (stop_pmc8_motion(PortFD, (dir == DIRECTION_WEST ? PMC8_W : PMC8_E)) == false) { LOG_ERROR("Error stopping W/E motion."); return false; } else { LOGF_INFO("%s motion stopped.", (dir == DIRECTION_WEST) ? "West" : "East"); // restore tracking if (TrackState == SCOPE_TRACKING) { LOG_INFO("Move E/W complete, tracking..."); if (!SetTrackEnabled(true)) { LOG_ERROR("slew complete - unable to enable tracking"); return false; } if (!SetTrackMode(IUFindOnSwitchIndex(&TrackModeSP))) { LOG_ERROR("slew complete - unable to set track mode"); return false; } } } break; } return true; } IPState PMC8::GuideNorth(float ms) { bool rc; long timetaken_us; int timeremain_ms; // If already moving, then stop movement if (MovementNSSP.s == IPS_BUSY) { int dir = IUFindOnSwitchIndex(&MovementNSSP); MoveNS(dir == 0 ? DIRECTION_NORTH : DIRECTION_SOUTH, MOTION_STOP); } if (GuideNSTID) { IERmTimer(GuideNSTID); GuideNSTID = 0; } rc = start_pmc8_guide(PortFD, PMC8_N, (int)ms, timetaken_us); timeremain_ms = (int)(ms - ((float)timetaken_us)/1000.0); if (timeremain_ms < 0) timeremain_ms = 0; GuideNSTID = IEAddTimer(timeremain_ms, guideTimeoutHelperN, this); return IPS_BUSY; } IPState PMC8::GuideSouth(float ms) { bool rc; long timetaken_us; int timeremain_ms; // If already moving, then stop movement if (MovementNSSP.s == IPS_BUSY) { int dir = IUFindOnSwitchIndex(&MovementNSSP); MoveNS(dir == 0 ? DIRECTION_NORTH : DIRECTION_SOUTH, MOTION_STOP); } if (GuideNSTID) { IERmTimer(GuideNSTID); GuideNSTID = 0; } rc = start_pmc8_guide(PortFD, PMC8_S, (int)ms, timetaken_us); timeremain_ms = (int)(ms - ((float)timetaken_us)/1000.0); if (timeremain_ms < 0) timeremain_ms = 0; GuideNSTID = IEAddTimer(timeremain_ms, guideTimeoutHelperS, this); return IPS_BUSY; } IPState PMC8::GuideEast(float ms) { bool rc; long timetaken_us; int timeremain_ms; // If already moving (no pulse command), then stop movement if (MovementWESP.s == IPS_BUSY) { int dir = IUFindOnSwitchIndex(&MovementWESP); MoveWE(dir == 0 ? DIRECTION_WEST : DIRECTION_EAST, MOTION_STOP); } if (GuideWETID) { IERmTimer(GuideWETID); GuideWETID = 0; } rc = start_pmc8_guide(PortFD, PMC8_E, (int)ms, timetaken_us); timeremain_ms = (int)(ms - ((float)timetaken_us)/1000.0); if (timeremain_ms < 0) timeremain_ms = 0; GuideWETID = IEAddTimer(timeremain_ms, guideTimeoutHelperE, this); return IPS_BUSY; } IPState PMC8::GuideWest(float ms) { bool rc; long timetaken_us; int timeremain_ms; // If already moving (no pulse command), then stop movement if (MovementWESP.s == IPS_BUSY) { int dir = IUFindOnSwitchIndex(&MovementWESP); MoveWE(dir == 0 ? DIRECTION_WEST : DIRECTION_EAST, MOTION_STOP); } if (GuideWETID) { IERmTimer(GuideWETID); GuideWETID = 0; } rc = start_pmc8_guide(PortFD, PMC8_W, (int)ms, timetaken_us); timeremain_ms = (int)(ms - ((float)timetaken_us)/1000.0); if (timeremain_ms < 0) timeremain_ms = 0; GuideWETID = IEAddTimer(timeremain_ms, guideTimeoutHelperW, this); return IPS_BUSY; } void PMC8::guideTimeout(PMC8_DIRECTION calldir) { // end previous pulse command stop_pmc8_guide(PortFD, calldir); if (calldir == PMC8_N || calldir == PMC8_S) { GuideNSNP.np[0].value = 0; GuideNSNP.np[1].value = 0; GuideNSNP.s = IPS_IDLE; GuideNSTID = 0; IDSetNumber(&GuideNSNP, nullptr); } if (calldir == PMC8_W || calldir == PMC8_E) { GuideWENP.np[0].value = 0; GuideWENP.np[1].value = 0; GuideWENP.s = IPS_IDLE; GuideWETID = 0; IDSetNumber(&GuideWENP, nullptr); } LOG_DEBUG("GUIDE CMD COMPLETED"); } //GUIDE The timer helper functions. void PMC8::guideTimeoutHelperN(void *p) { ((PMC8 *)p)->guideTimeout(PMC8_N); } void PMC8::guideTimeoutHelperS(void *p) { ((PMC8 *)p)->guideTimeout(PMC8_S); } void PMC8::guideTimeoutHelperW(void *p) { ((PMC8 *)p)->guideTimeout(PMC8_W); } void PMC8::guideTimeoutHelperE(void *p) { ((PMC8 *)p)->guideTimeout(PMC8_E); } bool PMC8::SetSlewRate(int index) { INDI_UNUSED(index); // slew rate is rate for MoveEW/MOVENE commands - not for GOTOs!!! // just return true - we will check SlewRateSP when we do actually moves return true; } bool PMC8::saveConfigItems(FILE *fp) { INDI::Telescope::saveConfigItems(fp); return true; } void PMC8::mountSim() { static struct timeval ltv; struct timeval tv; double dt, da, dx; int nlocked; /* update elapsed time since last poll, don't presume exactly POLLMS */ gettimeofday(&tv, nullptr); if (ltv.tv_sec == 0 && ltv.tv_usec == 0) ltv = tv; dt = tv.tv_sec - ltv.tv_sec + (tv.tv_usec - ltv.tv_usec) / 1e6; ltv = tv; da = SLEWRATE * dt; /* Process per current state. We check the state of EQUATORIAL_COORDS and act acoordingly */ switch (TrackState) { case SCOPE_IDLE: currentRA += (TrackRateN[AXIS_RA].value/3600.0 * dt) / 15.0; currentRA = range24(currentRA); break; case SCOPE_TRACKING: if (TrackModeS[1].s == ISS_ON) { currentRA += ( ((TRACKRATE_SIDEREAL/3600.0) - (TrackRateN[AXIS_RA].value/3600.0)) * dt) / 15.0; currentDEC += ( (TrackRateN[AXIS_DE].value/3600.0) * dt); } break; case SCOPE_SLEWING: case SCOPE_PARKING: /* slewing - nail it when both within one pulse @ SLEWRATE */ nlocked = 0; dx = targetRA - currentRA; // Take shortest path if (fabs(dx) > 12) dx *= -1; if (fabs(dx) <= da) { currentRA = targetRA; nlocked++; } else if (dx > 0) currentRA += da / 15.; else currentRA -= da / 15.; if (currentRA < 0) currentRA += 24; else if (currentRA > 24) currentRA -= 24; dx = targetDEC - currentDEC; if (fabs(dx) <= da) { currentDEC = targetDEC; nlocked++; } else if (dx > 0) currentDEC += da; else currentDEC -= da; if (nlocked == 2) { if (TrackState == SCOPE_SLEWING) set_pmc8_sim_system_status(ST_TRACKING); else set_pmc8_sim_system_status(ST_PARKED); } break; case SCOPE_PARKED: // setting system status to parked will automatically // set the simulated RA/DEC to park position so reread set_pmc8_sim_system_status(ST_PARKED); get_pmc8_coords(PortFD, currentRA, currentDEC); break; default: break; } set_pmc8_sim_ra(currentRA); set_pmc8_sim_dec(currentDEC); } #if 0 // PMC8 only parks to motor position (0, 0) currently bool PMC8::SetCurrentPark() { SetAxis1Park(currentRA); SetAxis2Park(currentDEC); return true; } bool PMC8::SetDefaultPark() { // By default set RA to HA SetAxis1Park(ln_get_apparent_sidereal_time(ln_get_julian_from_sys())); // Set DEC to 90 or -90 depending on the hemisphere // SetAxis2Park((HemisphereS[HEMI_NORTH].s == ISS_ON) ? 90 : -90); SetAxis2Park(90); return true; } #else bool PMC8::SetCurrentPark() { LOG_ERROR("PPMC8::SetCurrentPark() not implemented!"); return false; } bool PMC8::SetDefaultPark() { LOG_ERROR("PMC8::SetDefaultPark() not implemented!"); return false; } #endif bool PMC8::SetTrackMode(uint8_t mode) { uint pmc8_mode; LOGF_DEBUG("PMC8::SetTrackMode called mode=%d", mode); // FIXME - Need to make sure track modes are handled properly! //PMC8_TRACK_RATE rate = static_cast(mode); switch (mode) { case TRACK_SIDEREAL: pmc8_mode = PMC8_TRACK_SIDEREAL; break; case TRACK_LUNAR: pmc8_mode = PMC8_TRACK_LUNAR; break; case TRACK_SOLAR: pmc8_mode = PMC8_TRACK_LUNAR; break; case TRACK_CUSTOM: pmc8_mode = PMC8_TRACK_CUSTOM; break; default: LOGF_ERROR("PMC8::SetTrackMode mode=%d not supported!", mode); return false; } if (pmc8_mode == PMC8_TRACK_CUSTOM) { if (set_pmc8_custom_ra_track_rate(PortFD, TrackRateN[AXIS_RA].value)) return true; } else { if (set_pmc8_track_mode(PortFD, mode)) return true; } return false; } bool PMC8::SetTrackRate(double raRate, double deRate) { static bool deRateWarning = true; double pmc8RARate; LOGF_DEBUG("PMC8::SetTrackRate called raRate=%f deRate=%f", raRate, deRate); // Convert to arcsecs/s to +/- 0.0100 accepted by //double pmc8RARate = raRate - TRACKRATE_SIDEREAL; // for now just send rate pmc8RARate = raRate; if (deRate != 0 && deRateWarning) { // Only send warning once per session deRateWarning = false; LOG_WARN("Custom Declination tracking rate is not implemented yet."); } if (set_pmc8_custom_ra_track_rate(PortFD, pmc8RARate)) return true; LOG_ERROR("PMC8::SetTrackRate not implemented!"); return false; } bool PMC8::SetTrackEnabled(bool enabled) { LOGF_DEBUG("PMC8::SetTrackEnabled called enabled=%d", enabled); // need to determine current tracking mode and start tracking if (enabled) { if (!SetTrackMode(IUFindOnSwitchIndex(&TrackModeSP))) { LOG_ERROR("PMC8::SetTrackREnabled - unable to enable tracking"); return false; } } else { bool rc; rc=set_pmc8_custom_ra_track_rate(PortFD, 0); if (!rc) { LOG_ERROR("PMC8::SetTrackREnabled - unable to set RA track rate to 0"); return false; } // currently only support tracking rate in RA // rc=set_pmc8_custom_dec_track_rate(PortFD, 0); // if (!rc) // { // LOG_ERROR("PMC8::SetTrackREnabled - unable to set DEC track rate to 0"); // return false; // } } return true; } libindi/drivers/telescope/lx200gotonova.h0000664000175000017500000000604213263645557017730 0ustar jasemjasem/* GotoNova INDI driver Copyright (C) 2017 Jasem Mutlaq 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 */ #pragma once #include "lx200generic.h" class LX200GotoNova : public LX200Generic { public: LX200GotoNova(); ~LX200GotoNova() {} virtual bool updateProperties() override; virtual bool initProperties() override; virtual bool ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) override; protected: virtual const char *getDefaultName() override; virtual void getBasicData() override; virtual bool checkConnection() override; virtual bool isSlewComplete() override; virtual bool ReadScopeStatus() override; virtual bool SetSlewRate(int index) override; virtual bool SetTrackMode(uint8_t mode) override; virtual bool Goto(double, double) override; virtual bool Sync(double ra, double dec) override; virtual bool updateTime(ln_date *utc, double utc_offset) override; virtual bool updateLocation(double latitude, double longitude, double elevation) override; virtual bool saveConfigItems(FILE *fp) override; virtual bool Park() override; virtual bool UnPark() override; private: int setGotoNovaStandardProcedure(int fd, const char *data); void setGuidingEnabled(bool enable); int GotonovaSyncCMR(char *matchedObject); // Settings int setGotoNovaLatitude(double Lat); int setGotoNovaLongitude(double Long); int setGotoNovaUTCOffset(double hours); int setCalenderDate(int fd, int dd, int mm, int yy); // Motion int slewGotoNova(); // Park int setGotoNovaParkPosition(int position); // Track Mode int setGotoNovaTrackMode(int mode); int getGotoNovaTrackMode(int *mode); // Guide Rate int setGotoNovaGuideRate(int rate); int getGotoNovaGuideRate(int *rate); // Pier Side void syncSideOfPier(); // Simulation void mountSim(); // Custom Parking Position ISwitch ParkPositionS[5]; ISwitchVectorProperty ParkPositionSP; enum { PS_NORTH_POLE, PS_LEFT_VERTICAL, PS_LEFT_HORIZON, PS_RIGHT_VERTICAL, PS_RIGHT_HORIZON }; // Sync type ISwitch SyncCMRS[2]; ISwitchVectorProperty SyncCMRSP; enum { USE_REGULAR_SYNC, USE_CMR_SYNC }; /* Guide Rate */ ISwitch GuideRateS[4]; ISwitchVectorProperty GuideRateSP; bool isGuiding=false; }; libindi/drivers/telescope/ioptronv3driver.h0000664000175000017500000001642413263645557020472 0ustar jasemjasem/* INDI IOptron v3 Driver for firmware version 20171001 or later. Copyright (C) 2018 Jasem Mutlaq 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 */ #pragma once #include #include #include "indilogger.h" namespace IOPv3 { typedef enum { GPS_OFF, GPS_ON, GPS_DATA_OK } IOP_GPS_STATUS; typedef enum { ST_STOPPED, ST_TRACKING_PEC_OFF, ST_SLEWING, ST_GUIDING, ST_MERIDIAN_FLIPPING, ST_TRACKING_PEC_ON, ST_PARKED, ST_HOME } IOP_SYSTEM_STATUS; typedef enum { TR_SIDEREAL, TR_LUNAR, TR_SOLAR, TR_KING, TR_CUSTOM } IOP_TRACK_RATE; typedef enum { SR_1=1, SR_2, SR_3, SR_4, SR_5, SR_6, SR_7, SR_8, SR_MAX } IOP_SLEW_RATE; typedef enum { TS_RS232, TS_CONTROLLER, TS_GPS } IOP_TIME_SOURCE; typedef enum { HEMI_SOUTH, HEMI_NORTH } IOP_HEMISPHERE; typedef enum { FW_MODEL, FW_BOARD, FW_CONTROLLER, FW_RA, FW_DEC } IOP_FIRMWARE; typedef enum { RA_AXIS, DEC_AXIS } IOP_AXIS; typedef enum { IOP_N, IOP_S, IOP_W, IOP_E } IOP_DIRECTION; typedef enum { IOP_FIND_HOME, IOP_SET_HOME, IOP_GOTO_HOME } IOP_HOME_OPERATION; typedef enum { IOP_PIER_EAST, IOP_PIER_WEST, IOP_PIER_UNKNOWN } IOP_PIER_STATE; typedef enum { IOP_CW_UP, IOP_CW_NORMAL} IOP_CW_STATE; typedef struct { IOP_GPS_STATUS gpsStatus; IOP_SYSTEM_STATUS systemStatus; IOP_SYSTEM_STATUS rememberSystemStatus; IOP_TRACK_RATE trackRate; IOP_SLEW_RATE slewRate; IOP_TIME_SOURCE timeSource; IOP_HEMISPHERE hemisphere; double longitude; double latitude; } IOPInfo; typedef struct { std::string Model; std::string MainBoardFirmware; std::string ControllerFirmware; std::string RAFirmware; std::string DEFirmware; } FirmwareInfo; class Driver { public: explicit Driver(const char *deviceName); ~Driver() = default; static const std::map models; // Slew speeds. N.B. 1024 is arbitrary as the real max value different from // one mount to another. It is used for simulation purposes only. static const uint16_t IOP_SLEW_RATES[]; /************************************************************************** Communication **************************************************************************/ bool sendCommand(const char *command, int count=1, char *response=nullptr, uint8_t timeout=IOP_TIMEOUT, uint8_t debugLog=INDI::Logger::DBG_DEBUG); bool checkConnection(int fd); /************************************************************************** Get Info **************************************************************************/ /** Get iEQ current status info */ bool getStatus(IOPInfo *info); /** Get All firmware informatin in addition to mount model */ bool getFirmwareInfo(FirmwareInfo *info); /** Get RA/DEC */ bool getCoords(double *ra, double *de, IOP_PIER_STATE *pierState, IOP_CW_STATE *cwState); /** Get UTC JD plus utc offset and whether daylight savings is active or not */ bool getUTCDateTime(double *JD, int *utcOffsetMinutes, bool *dayLightSaving); /************************************************************************** Motion **************************************************************************/ bool startMotion(IOP_DIRECTION dir); bool stopMotion(IOP_DIRECTION dir); bool setSlewRate(IOP_SLEW_RATE rate); bool setCustomRATrackRate(double rate); bool setTrackMode(IOP_TRACK_RATE rate); bool setTrackEnabled(bool enabled); bool abort(); bool slewNormal(); bool slewCWUp(); bool sync(); bool setRA(double ra); bool setDE(double de); /************************************************************************** Home **************************************************************************/ bool findHome(); bool gotoHome(); bool setCurrentHome(); /************************************************************************** Park **************************************************************************/ bool park(); bool unpark(); /************************************************************************** Guide **************************************************************************/ bool setGuideRate(double RARate, double DERate); bool getGuideRate(double *RARate, double *DERate); bool startGuide(IOP_DIRECTION dir, uint32_t ms); /************************************************************************** Time & Location **************************************************************************/ bool setLongitude(double longitude); bool setLatitude(double latitude); bool setUTCDateTime(double JD); bool setUTCOffset(int offsetMinutes); bool setDaylightSaving(bool enabled); /************************************************************************** Misc. **************************************************************************/ void setDebug(bool enable); void setSimulation(bool enable); /************************************************************************** Simulation **************************************************************************/ void setSimGPSstatus(IOP_GPS_STATUS value); void setSimSytemStatus(IOP_SYSTEM_STATUS value); void setSimTrackRate(IOP_TRACK_RATE value); void setSimSlewRate(IOP_SLEW_RATE value); void setSimTimeSource(IOP_TIME_SOURCE value); void setSimHemisphere(IOP_HEMISPHERE value); void setSimRA(double ra); void setSimDE(double de); void setSimLongLat(double longitude, double latitude); void setSimGuideRate(double raRate, double deRate); protected: /************************************************************************** Firmware Info **************************************************************************/ /** Get mainboard and controller firmware only */ bool getMainFirmware(std::string &mainFirmware, std::string &controllerFirmware); /** Get RA and DEC firmware info */ bool getRADEFirmware(std::string &RAFirmware, std::string &DEFirmware); /** Get Mount model */ bool getModel(std::string &model); struct { double ra; double de; double ra_guide_rate; double de_guide_rate; double JD; int utc_offset_minutes; bool day_light_saving; IOP_PIER_STATE pier_state; IOP_CW_STATE cw_state; IOPInfo simInfo; } simData; private: int PortFD = { -1 }; bool m_Debug = {false}; bool m_Simulation = {false}; const char *m_DeviceName; // FD timeout in seconds static const uint8_t IOP_TIMEOUT=5; // Buffer to store mount response static const uint8_t IOP_BUFFER=64; }; } libindi/drivers/dome/0000775000175000017500000000000013263645557014074 5ustar jasemjasemlibindi/drivers/dome/dome_simulator.cpp0000664000175000017500000001735713263645557017640 0ustar jasemjasem/******************************************************************************* Dome Simulator Copyright(c) 2014 Jasem Mutlaq. All rights reserved. This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2 as published by the Free Software Foundation. . 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 Library General Public License for more details. . You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. *******************************************************************************/ #include "dome_simulator.h" #include "indicom.h" #include #include #include // We declare an auto pointer to domeSim. std::unique_ptr domeSim(new DomeSim()); #define DOME_SPEED 10.0 /* 10 degrees per second, constant */ #define SHUTTER_TIMER 5.0 /* Shutter closes/open in 5 seconds */ void ISPoll(void *p); void ISGetProperties(const char *dev) { domeSim->ISGetProperties(dev); } void ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) { domeSim->ISNewSwitch(dev, name, states, names, n); } void ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n) { domeSim->ISNewText(dev, name, texts, names, n); } void ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) { domeSim->ISNewNumber(dev, name, values, names, n); } void ISNewBLOB(const char *dev, const char *name, int sizes[], int blobsizes[], char *blobs[], char *formats[], char *names[], int n) { INDI_UNUSED(dev); INDI_UNUSED(name); INDI_UNUSED(sizes); INDI_UNUSED(blobsizes); INDI_UNUSED(blobs); INDI_UNUSED(formats); INDI_UNUSED(names); INDI_UNUSED(n); } void ISSnoopDevice(XMLEle *root) { domeSim->ISSnoopDevice(root); } DomeSim::DomeSim() { targetAz = 0; shutterTimer = 0; prev_az = 0; prev_alt = 0; TimeSinceUpdate = 0; SetDomeCapability(DOME_CAN_ABORT | DOME_CAN_ABS_MOVE | DOME_CAN_REL_MOVE | DOME_CAN_PARK | DOME_HAS_SHUTTER); } /************************************************************************************ * * ***********************************************************************************/ bool DomeSim::initProperties() { INDI::Dome::initProperties(); SetParkDataType(PARK_AZ); addAuxControls(); return true; } bool DomeSim::SetupParms() { targetAz = 0; shutterTimer = SHUTTER_TIMER; DomeAbsPosN[0].value = 0; DomeParamN[0].value = 5; IDSetNumber(&DomeAbsPosNP, nullptr); IDSetNumber(&DomeParamNP, nullptr); if (InitPark()) { // If loading parking data is successful, we just set the default parking values. SetAxis1ParkDefault(90); } else { // Otherwise, we set all parking data to default in case no parking data is found. SetAxis1Park(90); SetAxis1ParkDefault(90); } return true; } const char *DomeSim::getDefaultName() { return (const char *)"Dome Simulator"; } bool DomeSim::updateProperties() { INDI::Dome::updateProperties(); if (isConnected()) { SetupParms(); } return true; } bool DomeSim::Connect() { SetTimer(1000); // start the timer return true; } bool DomeSim::Disconnect() { return true; } void DomeSim::TimerHit() { int nexttimer = 1000; if (!isConnected()) return; // No need to reset timer if we are not connected anymore if (DomeAbsPosNP.s == IPS_BUSY) { if (targetAz > DomeAbsPosN[0].value) { DomeAbsPosN[0].value += DOME_SPEED; } else if (targetAz < DomeAbsPosN[0].value) { DomeAbsPosN[0].value -= DOME_SPEED; } DomeAbsPosN[0].value = range360(DomeAbsPosN[0].value); if (fabs(targetAz - DomeAbsPosN[0].value) <= DOME_SPEED) { DomeAbsPosN[0].value = targetAz; LOG_INFO("Dome reached requested azimuth angle."); if (getDomeState() == DOME_PARKING) SetParked(true); else if (getDomeState() == DOME_UNPARKING) SetParked(false); else setDomeState(DOME_SYNCED); } IDSetNumber(&DomeAbsPosNP, nullptr); } if (DomeShutterSP.s == IPS_BUSY) { if (shutterTimer-- <= 0) { shutterTimer = 0; DomeShutterSP.s = IPS_OK; LOGF_INFO("Shutter is %s.", (DomeShutterS[0].s == ISS_ON ? "open" : "closed")); IDSetSwitch(&DomeShutterSP, nullptr); if (getDomeState() == DOME_UNPARKING) SetParked(false); } } SetTimer(nexttimer); // Not all mounts update ra/dec constantly if tracking co-ordinates // This is to ensure our alt/az gets updated even if ra/dec isn't being updated // Once every 10 seconds is more than sufficient // with this added, dome simulator will now correctly track telescope simulator // which does not emit new ra/dec co-ords if they are not changing if (!isParked() && TimeSinceUpdate++ > 9) { TimeSinceUpdate = 0; UpdateMountCoords(); } } IPState DomeSim::Move(DomeDirection dir, DomeMotionCommand operation) { if (operation == MOTION_START) { targetAz = (dir == DOME_CW) ? 1e6 : -1e6; DomeAbsPosNP.s = IPS_BUSY; } else { targetAz = 0; DomeAbsPosNP.s = IPS_IDLE; } IDSetNumber(&DomeAbsPosNP, nullptr); return ((operation == MOTION_START) ? IPS_BUSY : IPS_OK); } IPState DomeSim::MoveAbs(double az) { targetAz = az; // Requested position is within one cycle, let's declare it done if (fabs(az - DomeAbsPosN[0].value) < DOME_SPEED) return IPS_OK; // It will take a few cycles to reach final position return IPS_BUSY; } IPState DomeSim::MoveRel(double azDiff) { targetAz = DomeAbsPosN[0].value + azDiff; ; if (targetAz < DomeAbsPosN[0].min) targetAz += DomeAbsPosN[0].max; if (targetAz > DomeAbsPosN[0].max) targetAz -= DomeAbsPosN[0].max; // Requested position is within one cycle, let's declare it done if (fabs(targetAz - DomeAbsPosN[0].value) < DOME_SPEED) return IPS_OK; // It will take a few cycles to reach final position return IPS_BUSY; } IPState DomeSim::Park() { if (INDI::Dome::isLocked()) { DEBUG(INDI::Logger::DBG_SESSION, "Cannot Park Dome when mount is locking. See: Telescope parking policy, in options tab"); return IPS_ALERT; } targetAz = DomeParamN[0].value; Dome::ControlShutter(SHUTTER_CLOSE); Dome::MoveAbs(GetAxis1Park()); return IPS_BUSY; } IPState DomeSim::UnPark() { return Dome::ControlShutter(SHUTTER_OPEN); } IPState DomeSim::ControlShutter(ShutterOperation operation) { INDI_UNUSED(operation); shutterTimer = SHUTTER_TIMER; return IPS_BUSY; } bool DomeSim::Abort() { // If we abort while in the middle of opening/closing shutter, alert. if (DomeShutterSP.s == IPS_BUSY) { DomeShutterSP.s = IPS_ALERT; IDSetSwitch(&DomeShutterSP, "Shutter operation aborted. Status: unknown."); return false; } return true; } bool DomeSim::SetCurrentPark() { SetAxis1Park(DomeAbsPosN[0].value); return true; } bool DomeSim::SetDefaultPark() { // By default set position to 90 SetAxis1Park(90); return true; } libindi/drivers/dome/baader_dome.cpp0000664000175000017500000007434713263645557017041 0ustar jasemjasem/******************************************************************************* Baader Planetarium Dome INDI Driver Copyright(c) 2014 Jasem Mutlaq. All rights reserved. Baader Dome INDI Driver This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2 as published by the Free Software Foundation. . 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 Library General Public License for more details. . You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. *******************************************************************************/ #include "baader_dome.h" #include "indicom.h" #include #include #include #include // We declare an auto pointer to BaaderDome. std::unique_ptr baaderDome(new BaaderDome()); #define DOME_CMD 9 /* Dome command in bytes */ #define DOME_BUF 16 /* Dome command buffer */ #define DOME_TIMEOUT 3 /* 3 seconds comm timeout */ #define SIM_SHUTTER_TIMER 5.0 /* Simulated Shutter closes/open in 5 seconds */ #define SIM_FLAP_TIMER 5.0 /* Simulated Flap closes/open in 3 seconds */ #define SIM_DOME_HI_SPEED 5.0 /* Simulated dome speed 5.0 degrees per second, constant */ #define SIM_DOME_LO_SPEED 0.5 /* Simulated dome speed 0.5 degrees per second, constant */ void ISPoll(void *p); void ISGetProperties(const char *dev) { baaderDome->ISGetProperties(dev); } void ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) { baaderDome->ISNewSwitch(dev, name, states, names, n); } void ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n) { baaderDome->ISNewText(dev, name, texts, names, n); } void ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) { baaderDome->ISNewNumber(dev, name, values, names, n); } void ISNewBLOB(const char *dev, const char *name, int sizes[], int blobsizes[], char *blobs[], char *formats[], char *names[], int n) { INDI_UNUSED(dev); INDI_UNUSED(name); INDI_UNUSED(sizes); INDI_UNUSED(blobsizes); INDI_UNUSED(blobs); INDI_UNUSED(formats); INDI_UNUSED(names); INDI_UNUSED(n); } void ISSnoopDevice(XMLEle *root) { baaderDome->ISSnoopDevice(root); } BaaderDome::BaaderDome() { targetAz = 0; shutterState = SHUTTER_UNKNOWN; flapStatus = FLAP_UNKNOWN; simShutterStatus = SHUTTER_CLOSED; simFlapStatus = FLAP_CLOSED; status = DOME_UNKNOWN; targetShutter = SHUTTER_CLOSE; targetFlap = FLAP_CLOSE; calibrationStage = CALIBRATION_UNKNOWN; SetDomeCapability(DOME_CAN_ABORT | DOME_CAN_ABS_MOVE | DOME_CAN_REL_MOVE | DOME_CAN_PARK | DOME_HAS_SHUTTER | DOME_HAS_VARIABLE_SPEED); } bool BaaderDome::initProperties() { INDI::Dome::initProperties(); IUFillSwitch(&CalibrateS[0], "Start", "", ISS_OFF); IUFillSwitchVector(&CalibrateSP, CalibrateS, 1, getDeviceName(), "Calibrate", "", MAIN_CONTROL_TAB, IP_RW, ISR_ATMOST1, 0, IPS_IDLE); IUFillSwitch(&DomeFlapS[0], "FLAP_OPEN", "Open", ISS_OFF); IUFillSwitch(&DomeFlapS[1], "FLAP_CLOSE", "Close", ISS_ON); IUFillSwitchVector(&DomeFlapSP, DomeFlapS, 2, getDeviceName(), "DOME_FLAP", "Flap", MAIN_CONTROL_TAB, IP_RW, ISR_1OFMANY, 60, IPS_OK); SetParkDataType(PARK_AZ); addAuxControls(); return true; } /************************************************************************************ * * ***********************************************************************************/ bool BaaderDome::SetupParms() { targetAz = 0; if (UpdatePosition()) IDSetNumber(&DomeAbsPosNP, nullptr); if (UpdateShutterStatus()) IDSetSwitch(&DomeShutterSP, nullptr); if (UpdateFlapStatus()) IDSetSwitch(&DomeFlapSP, nullptr); if (InitPark()) { // If loading parking data is successful, we just set the default parking values. SetAxis1ParkDefault(0); } else { // Otherwise, we set all parking data to default in case no parking data is found. SetAxis1Park(0); SetAxis1ParkDefault(0); } return true; } /************************************************************************************ * * ***********************************************************************************/ bool BaaderDome::Handshake() { return Ack(); } /************************************************************************************ * * ***********************************************************************************/ const char *BaaderDome::getDefaultName() { return (const char *)"Baader Dome"; } /************************************************************************************ * * ***********************************************************************************/ bool BaaderDome::updateProperties() { INDI::Dome::updateProperties(); if (isConnected()) { defineSwitch(&DomeFlapSP); defineSwitch(&CalibrateSP); SetupParms(); } else { deleteProperty(DomeFlapSP.name); deleteProperty(CalibrateSP.name); } return true; } /************************************************************************************ * * ***********************************************************************************/ bool BaaderDome::ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) { if (dev != nullptr && strcmp(dev, getDeviceName()) == 0) { if (strcmp(name, CalibrateSP.name) == 0) { IUResetSwitch(&CalibrateSP); if (status == DOME_READY) { CalibrateSP.s = IPS_OK; LOG_INFO("Dome is already calibrated."); IDSetSwitch(&CalibrateSP, nullptr); return true; } if (CalibrateSP.s == IPS_BUSY) { Abort(); LOG_INFO("Calibration aborted."); status = DOME_UNKNOWN; CalibrateSP.s = IPS_IDLE; IDSetSwitch(&CalibrateSP, nullptr); return true; } status = DOME_CALIBRATING; LOG_INFO("Starting calibration procedure..."); calibrationStage = CALIBRATION_STAGE1; calibrationStart = DomeAbsPosN[0].value; // Goal of procedure is to reach south point to hit sensor calibrationTarget1 = calibrationStart + 179; if (calibrationTarget1 > 360) calibrationTarget1 -= 360; if (MoveAbs(calibrationTarget1) == IPS_IDLE) { CalibrateSP.s = IPS_ALERT; LOG_ERROR("Calibration failue due to dome motion failure."); status = DOME_UNKNOWN; IDSetSwitch(&CalibrateSP, nullptr); return false; } DomeAbsPosNP.s = IPS_BUSY; CalibrateSP.s = IPS_BUSY; LOGF_INFO("Calibration is in progress. Moving to position %g.", calibrationTarget1); IDSetSwitch(&CalibrateSP, nullptr); return true; } if (strcmp(name, DomeFlapSP.name) == 0) { int ret = 0; int prevStatus = IUFindOnSwitchIndex(&DomeFlapSP); IUUpdateSwitch(&DomeFlapSP, states, names, n); int FlapDome = IUFindOnSwitchIndex(&DomeFlapSP); // No change of status, let's return if (prevStatus == FlapDome) { DomeFlapSP.s = IPS_OK; IDSetSwitch(&DomeFlapSP, nullptr); } // go back to prev status in case of failure IUResetSwitch(&DomeFlapSP); DomeFlapS[prevStatus].s = ISS_ON; if (FlapDome == 0) ret = ControlDomeFlap(FLAP_OPEN); else ret = ControlDomeFlap(FLAP_CLOSE); if (ret == 0) { DomeFlapSP.s = IPS_OK; IUResetSwitch(&DomeFlapSP); DomeFlapS[FlapDome].s = ISS_ON; IDSetSwitch(&DomeFlapSP, "Flap is %s.", (FlapDome == 0 ? "open" : "closed")); return true; } else if (ret == 1) { DomeFlapSP.s = IPS_BUSY; IUResetSwitch(&DomeFlapSP); DomeFlapS[FlapDome].s = ISS_ON; IDSetSwitch(&DomeFlapSP, "Flap is %s...", (FlapDome == 0 ? "opening" : "closing")); return true; } DomeFlapSP.s = IPS_ALERT; IDSetSwitch(&DomeFlapSP, "Flap failed to %s.", (FlapDome == 0 ? "open" : "close")); return false; } } return INDI::Dome::ISNewSwitch(dev, name, states, names, n); } /************************************************************************************ * * ***********************************************************************************/ bool BaaderDome::Ack() { int nbytes_written = 0, nbytes_read = 0, rc = -1; char errstr[MAXRBUF]; char resp[DOME_BUF]; char status[DOME_BUF]; sim = isSimulation(); tcflush(PortFD, TCIOFLUSH); if (!sim && (rc = tty_write(PortFD, "d#getflap", DOME_CMD, &nbytes_written)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("d#getflap Ack error: %s.", errstr); return false; } LOG_DEBUG("CMD (d#getflap)"); if (sim) { strncpy(resp, "d#flapclo", DOME_BUF); nbytes_read = DOME_CMD; } else if ((rc = tty_read(PortFD, resp, DOME_CMD, DOME_TIMEOUT, &nbytes_read)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("Ack error: %s.", errstr); return false; } resp[nbytes_read] = '\0'; LOGF_DEBUG("RES (%s)", resp); rc = sscanf(resp, "d#%s", status); if (rc > 0) return true; return false; } /************************************************************************************ * * ***********************************************************************************/ bool BaaderDome::UpdateShutterStatus() { int nbytes_written = 0, nbytes_read = 0, rc = -1; char errstr[MAXRBUF]; char resp[DOME_BUF]; char status[DOME_BUF]; tcflush(PortFD, TCIOFLUSH); if (!sim && (rc = tty_write(PortFD, "d#getshut", DOME_CMD, &nbytes_written)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("d#getshut UpdateShutterStatus error: %s.", errstr); return false; } LOG_DEBUG("CMD (d#getshut)"); if (sim) { if (simShutterStatus == SHUTTER_CLOSED) strncpy(resp, "d#shutclo", DOME_CMD); else if (simShutterStatus == SHUTTER_OPENED) strncpy(resp, "d#shutope", DOME_CMD); else if (simShutterStatus == SHUTTER_MOVING) strncpy(resp, "d#shutrun", DOME_CMD); nbytes_read = DOME_CMD; } else if ((rc = tty_read(PortFD, resp, DOME_CMD, DOME_TIMEOUT, &nbytes_read)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("UpdateShutterStatus error: %s.", errstr); return false; } resp[nbytes_read] = '\0'; LOGF_DEBUG("RES (%s)", resp); rc = sscanf(resp, "d#shut%s", status); if (rc > 0) { DomeShutterSP.s = IPS_OK; IUResetSwitch(&DomeShutterSP); if (strcmp(status, "ope") == 0) { if (shutterState == SHUTTER_MOVING && targetShutter == SHUTTER_OPEN) LOGF_INFO("%s", GetShutterStatusString(SHUTTER_OPENED)); shutterState = SHUTTER_OPENED; DomeShutterS[SHUTTER_OPEN].s = ISS_ON; } else if (strcmp(status, "clo") == 0) { if (shutterState == SHUTTER_MOVING && targetShutter == SHUTTER_CLOSE) LOGF_INFO("%s", GetShutterStatusString(SHUTTER_CLOSED)); shutterState = SHUTTER_CLOSED; DomeShutterS[SHUTTER_CLOSE].s = ISS_ON; } else if (strcmp(status, "run") == 0) { shutterState = SHUTTER_MOVING; DomeShutterSP.s = IPS_BUSY; } else { shutterState = SHUTTER_UNKNOWN; DomeShutterSP.s = IPS_ALERT; LOGF_ERROR("Unknown Shutter status: %s.", resp); } return true; } return false; } /************************************************************************************ * * ***********************************************************************************/ bool BaaderDome::UpdatePosition() { int nbytes_written = 0, nbytes_read = 0, rc = -1; char errstr[MAXRBUF]; char resp[DOME_BUF]; unsigned short domeAz = 0; tcflush(PortFD, TCIOFLUSH); if (!sim && (rc = tty_write(PortFD, "d#getazim", DOME_CMD, &nbytes_written)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("d#getazim UpdatePosition error: %s.", errstr); return false; } LOG_DEBUG("CMD (d#getazim)"); if (sim) { if (status == DOME_READY || calibrationStage == CALIBRATION_COMPLETE) snprintf(resp, DOME_BUF, "d#azr%04d", MountAzToDomeAz(DomeAbsPosN[0].value)); else snprintf(resp, DOME_BUF, "d#azi%04d", MountAzToDomeAz(DomeAbsPosN[0].value)); nbytes_read = DOME_CMD; } else if ((rc = tty_read(PortFD, resp, DOME_CMD, DOME_TIMEOUT, &nbytes_read)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("UpdatePosition error: %s.", errstr); return false; } resp[nbytes_read] = '\0'; LOGF_DEBUG("RES (%s)", resp); rc = sscanf(resp, "d#azr%hu", &domeAz); if (rc > 0) { if (calibrationStage == CALIBRATION_UNKNOWN) { status = DOME_READY; calibrationStage = CALIBRATION_COMPLETE; LOG_INFO("Dome is calibrated."); CalibrateSP.s = IPS_OK; IDSetSwitch(&CalibrateSP, nullptr); } else if (status == DOME_CALIBRATING) { status = DOME_READY; calibrationStage = CALIBRATION_COMPLETE; LOG_INFO("Calibration complete."); CalibrateSP.s = IPS_OK; IDSetSwitch(&CalibrateSP, nullptr); } DomeAbsPosN[0].value = DomeAzToMountAz(domeAz); return true; } else { rc = sscanf(resp, "d#azi%hu", &domeAz); if (rc > 0) { DomeAbsPosN[0].value = DomeAzToMountAz(domeAz); return true; } } return false; } /************************************************************************************ * * ***********************************************************************************/ unsigned short BaaderDome::MountAzToDomeAz(double mountAz) { int domeAz = 0; domeAz = (mountAz)*10.0 - 1800; if (mountAz >= 0 && mountAz <= 179.9) domeAz += 3600; if (domeAz > 3599) domeAz = 3599; else if (domeAz < 0) domeAz = 0; return ((unsigned short)(domeAz)); } /************************************************************************************ * * ***********************************************************************************/ double BaaderDome::DomeAzToMountAz(unsigned short domeAz) { double mountAz = 0; mountAz = ((double)(domeAz + 1800)) / 10.0; if (domeAz >= 1800) mountAz -= 360; if (mountAz > 360) mountAz -= 360; else if (mountAz < 0) mountAz += 360; return mountAz; } /************************************************************************************ * * ***********************************************************************************/ void BaaderDome::TimerHit() { if (!isConnected()) return; // No need to reset timer if we are not connected anymore UpdatePosition(); if (DomeAbsPosNP.s == IPS_BUSY) { if (sim) { double speed = 0; if (fabs(targetAz - DomeAbsPosN[0].value) > SIM_DOME_HI_SPEED) speed = SIM_DOME_HI_SPEED; else speed = SIM_DOME_LO_SPEED; if (DomeRelPosNP.s == IPS_BUSY) { // CW if (DomeMotionS[0].s == ISS_ON) DomeAbsPosN[0].value += speed; // CCW else DomeAbsPosN[0].value -= speed; } else { if (targetAz > DomeAbsPosN[0].value) { DomeAbsPosN[0].value += speed; } else if (targetAz < DomeAbsPosN[0].value) { DomeAbsPosN[0].value -= speed; } } if (DomeAbsPosN[0].value < DomeAbsPosN[0].min) DomeAbsPosN[0].value += DomeAbsPosN[0].max; if (DomeAbsPosN[0].value > DomeAbsPosN[0].max) DomeAbsPosN[0].value -= DomeAbsPosN[0].max; } if (fabs(targetAz - DomeAbsPosN[0].value) < DomeParamN[0].value) { DomeAbsPosN[0].value = targetAz; LOG_INFO("Dome reached requested azimuth angle."); if (status != DOME_CALIBRATING) { if (getDomeState() == DOME_PARKING) SetParked(true); else if (getDomeState() == DOME_UNPARKING) SetParked(false); else setDomeState(DOME_SYNCED); } if (status == DOME_CALIBRATING) { if (calibrationStage == CALIBRATION_STAGE1) { LOG_INFO("Calibration stage 1 complete. Starting stage 2..."); calibrationTarget2 = DomeAbsPosN[0].value + 2; calibrationStage = CALIBRATION_STAGE2; MoveAbs(calibrationTarget2); DomeAbsPosNP.s = IPS_BUSY; } else if (calibrationStage == CALIBRATION_STAGE2) { DEBUGF(INDI::Logger::DBG_SESSION, "Calibration stage 2 complete. Returning to initial position %g...", calibrationStart); calibrationStage = CALIBRATION_STAGE3; MoveAbs(calibrationStart); DomeAbsPosNP.s = IPS_BUSY; } else if (calibrationStage == CALIBRATION_STAGE3) { calibrationStage = CALIBRATION_COMPLETE; LOG_INFO("Dome reached initial position."); } } } IDSetNumber(&DomeAbsPosNP, nullptr); } else IDSetNumber(&DomeAbsPosNP, nullptr); UpdateShutterStatus(); if (sim && DomeShutterSP.s == IPS_BUSY) { if (simShutterTimer-- <= 0) { simShutterTimer = 0; simShutterStatus = (targetShutter == SHUTTER_OPEN) ? SHUTTER_OPENED : SHUTTER_CLOSED; } } else IDSetSwitch(&DomeShutterSP, nullptr); UpdateFlapStatus(); if (sim && DomeFlapSP.s == IPS_BUSY) { if (simFlapTimer-- <= 0) { simFlapTimer = 0; simFlapStatus = (targetFlap == FLAP_OPEN) ? FLAP_OPENED : FLAP_CLOSED; } } else IDSetSwitch(&DomeFlapSP, nullptr); SetTimer(POLLMS); } /************************************************************************************ * * ***********************************************************************************/ IPState BaaderDome::MoveAbs(double az) { int nbytes_written = 0, nbytes_read = 0, rc = -1; char errstr[MAXRBUF]; char cmd[DOME_BUF]; char resp[DOME_BUF]; if (status == DOME_UNKNOWN) { LOG_WARN("Dome is not calibrated. Please calibrate dome before issuing any commands."); return IPS_ALERT; } targetAz = az; snprintf(cmd, DOME_BUF, "d#azi%04d", MountAzToDomeAz(targetAz)); tcflush(PortFD, TCIOFLUSH); if (!sim && (rc = tty_write(PortFD, cmd, DOME_CMD, &nbytes_written)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("%s MoveAbsDome error: %s.", cmd, errstr); return IPS_ALERT; } LOGF_DEBUG("CMD (%s)", cmd); if (sim) { strncpy(resp, "d#gotmess", DOME_CMD); nbytes_read = DOME_CMD; } else if ((rc = tty_read(PortFD, resp, DOME_CMD, DOME_TIMEOUT, &nbytes_read)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("MoveAbsDome error: %s.", errstr); return IPS_ALERT; } resp[nbytes_read] = '\0'; LOGF_DEBUG("RES (%s)", resp); if (strcmp(resp, "d#gotmess") == 0) return IPS_BUSY; return IPS_ALERT; } /************************************************************************************ * * ***********************************************************************************/ IPState BaaderDome::MoveRel(double azDiff) { targetAz = DomeAbsPosN[0].value + azDiff; if (targetAz < DomeAbsPosN[0].min) targetAz += DomeAbsPosN[0].max; if (targetAz > DomeAbsPosN[0].max) targetAz -= DomeAbsPosN[0].max; // It will take a few cycles to reach final position return MoveAbs(targetAz); } /************************************************************************************ * * ***********************************************************************************/ IPState BaaderDome::Park() { targetAz = GetAxis1Park(); return MoveAbs(targetAz); } /************************************************************************************ * * ***********************************************************************************/ IPState BaaderDome::UnPark() { return IPS_OK; } /************************************************************************************ * * ***********************************************************************************/ IPState BaaderDome::ControlShutter(ShutterOperation operation) { int nbytes_written = 0, nbytes_read = 0, rc = -1; char errstr[MAXRBUF]; char cmd[DOME_BUF]; char resp[DOME_BUF]; memset(cmd, 0, sizeof(cmd)); if (operation == SHUTTER_OPEN) { targetShutter = operation; strncpy(cmd, "d#opeshut", DOME_CMD); } else { targetShutter = operation; strncpy(cmd, "d#closhut", DOME_CMD); } tcflush(PortFD, TCIOFLUSH); if (!sim && (rc = tty_write(PortFD, cmd, DOME_CMD, &nbytes_written)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("%s ControlDomeShutter error: %s.", cmd, errstr); return IPS_ALERT; } LOGF_DEBUG("CMD (%s)", cmd); if (sim) { simShutterTimer = SIM_SHUTTER_TIMER; strncpy(resp, "d#gotmess", DOME_CMD); nbytes_read = DOME_CMD; } else if ((rc = tty_read(PortFD, resp, DOME_CMD, DOME_TIMEOUT, &nbytes_read)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("ControlDomeShutter error: %s.", errstr); return IPS_ALERT; } resp[nbytes_read] = '\0'; LOGF_DEBUG("RES (%s)", resp); if (strcmp(resp, "d#gotmess") == 0) { shutterState = simShutterStatus = SHUTTER_MOVING; return IPS_BUSY; } return IPS_ALERT; } /************************************************************************************ * * ***********************************************************************************/ bool BaaderDome::Abort() { LOGF_INFO("Attempting to abort dome motion by stopping at %g", DomeAbsPosN[0].value); MoveAbs(DomeAbsPosN[0].value); return true; } /************************************************************************************ * * ***********************************************************************************/ const char *BaaderDome::GetFlapStatusString(FlapStatus status) { switch (status) { case FLAP_OPENED: return "Flap is open."; break; case FLAP_CLOSED: return "Flap is closed."; break; case FLAP_MOVING: return "Flap is in motion."; break; case FLAP_UNKNOWN: default: return "Flap status is unknown."; break; } } /************************************************************************************ * * ***********************************************************************************/ int BaaderDome::ControlDomeFlap(FlapOperation operation) { int nbytes_written = 0, nbytes_read = 0, rc = -1; char errstr[MAXRBUF]; char cmd[DOME_BUF]; char resp[DOME_BUF]; memset(cmd, 0, sizeof(cmd)); if (operation == FLAP_OPEN) { targetFlap = operation; strncpy(cmd, "d#opeflap", DOME_CMD); } else { targetFlap = operation; strncpy(cmd, "d#cloflap", DOME_CMD); } tcflush(PortFD, TCIOFLUSH); if (!sim && (rc = tty_write(PortFD, cmd, DOME_CMD, &nbytes_written)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("%s ControlDomeFlap error: %s.", cmd, errstr); return -1; } LOGF_DEBUG("CMD (%s)", cmd); if (sim) { simFlapTimer = SIM_FLAP_TIMER; strncpy(resp, "d#gotmess", DOME_CMD); nbytes_read = DOME_CMD; } else if ((rc = tty_read(PortFD, resp, DOME_CMD, DOME_TIMEOUT, &nbytes_read)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("ControlDomeFlap error: %s.", errstr); return -1; } resp[nbytes_read] = '\0'; LOGF_DEBUG("RES (%s)", resp); if (strcmp(resp, "d#gotmess") == 0) { flapStatus = simFlapStatus = FLAP_MOVING; return 1; } return -1; } /************************************************************************************ * * ***********************************************************************************/ bool BaaderDome::UpdateFlapStatus() { int nbytes_written = 0, nbytes_read = 0, rc = -1; char errstr[MAXRBUF]; char resp[DOME_BUF]; char status[DOME_BUF]; tcflush(PortFD, TCIOFLUSH); if (!sim && (rc = tty_write(PortFD, "d#getflap", DOME_CMD, &nbytes_written)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("d#getflap UpdateflapStatus error: %s.", errstr); return false; } LOG_DEBUG("CMD (d#getflap)"); if (sim) { if (simFlapStatus == FLAP_CLOSED) strncpy(resp, "d#flapclo", DOME_CMD); else if (simFlapStatus == FLAP_OPENED) strncpy(resp, "d#flapope", DOME_CMD); else if (simFlapStatus == FLAP_MOVING) strncpy(resp, "d#flaprun", DOME_CMD); nbytes_read = DOME_CMD; } else if ((rc = tty_read(PortFD, resp, DOME_CMD, DOME_TIMEOUT, &nbytes_read)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("UpdateflapStatus error: %s.", errstr); return false; } resp[nbytes_read] = '\0'; LOGF_DEBUG("RES (%s)", resp); rc = sscanf(resp, "d#flap%s", status); if (rc > 0) { DomeFlapSP.s = IPS_OK; IUResetSwitch(&DomeFlapSP); if (strcmp(status, "ope") == 0) { if (flapStatus == FLAP_MOVING && targetFlap == FLAP_OPEN) LOGF_INFO("%s", GetFlapStatusString(FLAP_OPENED)); flapStatus = FLAP_OPENED; DomeFlapS[FLAP_OPEN].s = ISS_ON; } else if (strcmp(status, "clo") == 0) { if (flapStatus == FLAP_MOVING && targetFlap == FLAP_CLOSE) LOGF_INFO("%s", GetFlapStatusString(FLAP_CLOSED)); flapStatus = FLAP_CLOSED; DomeFlapS[FLAP_CLOSE].s = ISS_ON; } else if (strcmp(status, "run") == 0) { flapStatus = FLAP_MOVING; DomeFlapSP.s = IPS_BUSY; } else { flapStatus = FLAP_UNKNOWN; DomeFlapSP.s = IPS_ALERT; LOGF_ERROR("Unknown flap status: %s.", resp); } return true; } return false; } /************************************************************************************ * * ***********************************************************************************/ bool BaaderDome::SaveEncoderPosition() { int nbytes_written = 0, nbytes_read = 0, rc = -1; char errstr[MAXRBUF]; char cmd[DOME_BUF]; char resp[DOME_BUF]; strncpy(cmd, "d#encsave", DOME_CMD); tcflush(PortFD, TCIOFLUSH); if (!sim && (rc = tty_write(PortFD, cmd, DOME_CMD, &nbytes_written)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("%s SaveEncoderPosition error: %s.", cmd, errstr); return false; } LOGF_DEBUG("CMD (%s)", cmd); if (sim) { strncpy(resp, "d#gotmess", DOME_CMD); nbytes_read = DOME_CMD; } else if ((rc = tty_read(PortFD, resp, DOME_CMD, DOME_TIMEOUT, &nbytes_read)) != TTY_OK) { tty_error_msg(rc, errstr, MAXRBUF); LOGF_ERROR("SaveEncoderPosition error: %s.", errstr); return false; } resp[nbytes_read] = '\0'; LOGF_DEBUG("RES (%s)", resp); return strcmp(resp, "d#gotmess") == 0; } /************************************************************************************ * * ***********************************************************************************/ bool BaaderDome::saveConfigItems(FILE *fp) { // Only save if calibration is complete if (calibrationStage == CALIBRATION_COMPLETE) SaveEncoderPosition(); return INDI::Dome::saveConfigItems(fp); } /************************************************************************************ * * ***********************************************************************************/ bool BaaderDome::SetCurrentPark() { SetAxis1Park(DomeAbsPosN[0].value); return true; } /************************************************************************************ * * ***********************************************************************************/ bool BaaderDome::SetDefaultPark() { // By default set position to 90 SetAxis1Park(90); return true; } libindi/drivers/dome/baader_dome.h0000664000175000017500000000647713263645557016505 0ustar jasemjasem/******************************************************************************* Copyright(c) 2014 Jasem Mutlaq. All rights reserved. Baader Planetarium Dome INDI Driver This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2 as published by the Free Software Foundation. . 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 Library General Public License for more details. . You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. *******************************************************************************/ #pragma once #include "indibase/indidome.h" class BaaderDome : public INDI::Dome { public: typedef enum { DOME_UNKNOWN, DOME_CALIBRATING, DOME_READY } DomeStatus; typedef enum { CALIBRATION_UNKNOWN, CALIBRATION_STAGE1, CALIBRATION_STAGE2, CALIBRATION_STAGE3, CALIBRATION_COMPLETE } CalibrationStage; typedef enum { FLAP_OPEN, FLAP_CLOSE } FlapOperation; typedef enum { FLAP_OPENED, FLAP_CLOSED, FLAP_MOVING, FLAP_UNKNOWN } FlapStatus; BaaderDome(); virtual ~BaaderDome() = default; virtual const char *getDefaultName() override; virtual bool initProperties() override; virtual bool updateProperties() override; virtual bool saveConfigItems(FILE *fp) override; virtual bool Handshake() override; virtual void TimerHit() override; virtual bool ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) override; virtual IPState MoveRel(double azDiff) override; virtual IPState MoveAbs(double az) override; virtual IPState ControlShutter(ShutterOperation operation) override; virtual bool Abort() override; // Parking virtual IPState Park() override; virtual IPState UnPark() override; virtual bool SetCurrentPark() override; virtual bool SetDefaultPark() override; protected: // Commands bool Ack(); bool UpdatePosition(); bool UpdateShutterStatus(); int ControlDomeFlap(FlapOperation operation); bool UpdateFlapStatus(); bool SaveEncoderPosition(); const char *GetFlapStatusString(FlapStatus status); // Misc unsigned short MountAzToDomeAz(double mountAz); double DomeAzToMountAz(unsigned short domeAz); bool SetupParms(); ISwitch CalibrateS[1]; ISwitchVectorProperty CalibrateSP; ISwitch DomeFlapS[2]; ISwitchVectorProperty DomeFlapSP; DomeStatus status { DOME_UNKNOWN }; FlapStatus flapStatus { FLAP_OPENED }; CalibrationStage calibrationStage { CALIBRATION_UNKNOWN }; double targetAz { 0 }; double calibrationStart { 0 }; double calibrationTarget1 { 0 }; double calibrationTarget2 { 0 }; ShutterOperation targetShutter { SHUTTER_OPEN }; FlapOperation targetFlap { FLAP_OPEN }; bool sim { false }; double simShutterTimer { 0 }; double simFlapTimer { 0 }; ShutterStatus simShutterStatus { SHUTTER_OPENED }; FlapStatus simFlapStatus { FLAP_OPENED }; }; libindi/drivers/dome/roll_off.h0000664000175000017500000000354513263645557016056 0ustar jasemjasem/******************************************************************************* Copyright(c) 2014 Jasem Mutlaq. All rights reserved. This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2 as published by the Free Software Foundation. . 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 Library General Public License for more details. . You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. *******************************************************************************/ #pragma once #include "indidome.h" class RollOff : public INDI::Dome { public: RollOff(); virtual ~RollOff() = default; virtual bool initProperties(); const char *getDefaultName(); bool updateProperties(); virtual bool ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n); virtual bool saveConfigItems(FILE *fp); virtual bool ISSnoopDevice(XMLEle *root); protected: bool Connect(); bool Disconnect(); void TimerHit(); virtual IPState Move(DomeDirection dir, DomeMotionCommand operation); virtual IPState Park(); virtual IPState UnPark(); virtual bool Abort(); virtual bool getFullOpenedLimitSwitch(); virtual bool getFullClosedLimitSwitch(); private: bool SetupParms(); float CalcTimeLeft(timeval); ISState fullOpenLimitSwitch { ISS_ON }; ISState fullClosedLimitSwitch { ISS_OFF }; double MotionRequest { 0 }; struct timeval MotionStart { 0, 0 }; }; libindi/drivers/dome/dome_simulator.h0000664000175000017500000000432113263645557017270 0ustar jasemjasem/******************************************************************************* Copyright(c) 2014 Jasem Mutlaq. All rights reserved. This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2 as published by the Free Software Foundation. . 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 Library General Public License for more details. . You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. *******************************************************************************/ #pragma once #include "indidome.h" /** * @brief The DomeSim class provides an absolute position dome that supports parking, unparking, and slaving. * * The driver can support custom parking positions and includes shutter control. It can be used to simulate dome slaving. * * The dome parameters must be set before slaving is enabled. Furthermore, the dome listens to changes in the TARGET_EOD_COORS of the mount driver * in order to make the decision to move to a new target location. * * All the mathematical models are taken care of in the base INDI::Dome class. */ class DomeSim : public INDI::Dome { public: DomeSim(); virtual ~DomeSim() = default; virtual bool initProperties(); const char *getDefaultName(); bool updateProperties(); protected: bool Connect(); bool Disconnect(); void TimerHit(); virtual IPState Move(DomeDirection dir, DomeMotionCommand operation); virtual IPState MoveRel(double azDiff); virtual IPState MoveAbs(double az); virtual IPState Park(); virtual IPState UnPark(); virtual IPState ControlShutter(ShutterOperation operation); virtual bool Abort(); // Parking virtual bool SetCurrentPark(); virtual bool SetDefaultPark(); private: double targetAz; double shutterTimer; bool SetupParms(); int TimeSinceUpdate; }; libindi/drivers/dome/dome_script.cpp0000664000175000017500000002555013263645557017117 0ustar jasemjasem/******************************************************************************* Copyright(c) 2016 CloudMakers, s. r. o.. All rights reserved. This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2 as published by the Free Software Foundation. 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 Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. *******************************************************************************/ #include "dome_script.h" #include "indicom.h" #include #include #include #include #include #define MAXARGS 20 typedef enum { SCRIPT_CONNECT = 1, SCRIPT_DISCONNECT, SCRIPT_STATUS, SCRIPT_OPEN, SCRIPT_CLOSE, SCRIPT_PARK, SCRIPT_UNPARK, SCRIPT_GOTO, SCRIPT_MOVE_CW, SCRIPT_MOVE_CCW, SCRIPT_ABORT, SCRIPT_COUNT } scripts; std::unique_ptr scope_script(new DomeScript()); void ISGetProperties(const char *dev) { scope_script->ISGetProperties(dev); } void ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) { scope_script->ISNewSwitch(dev, name, states, names, n); } void ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n) { scope_script->ISNewText(dev, name, texts, names, n); } void ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) { scope_script->ISNewNumber(dev, name, values, names, n); } void ISNewBLOB(const char *dev, const char *name, int sizes[], int blobsizes[], char *blobs[], char *formats[], char *names[], int n) { INDI_UNUSED(dev); INDI_UNUSED(name); INDI_UNUSED(sizes); INDI_UNUSED(blobsizes); INDI_UNUSED(blobs); INDI_UNUSED(formats); INDI_UNUSED(names); INDI_UNUSED(n); } void ISSnoopDevice(XMLEle *root) { scope_script->ISSnoopDevice(root); } DomeScript::DomeScript() { SetDomeCapability(DOME_CAN_PARK | DOME_CAN_ABORT | DOME_CAN_ABS_MOVE | DOME_HAS_SHUTTER); } const char *DomeScript::getDefaultName() { return (const char *)"Dome Scripting Gateway"; } bool DomeScript::initProperties() { INDI::Dome::initProperties(); SetParkDataType(PARK_AZ); #if defined(__APPLE__) IUFillText(&ScriptsT[0], "FOLDER", "Folder", "/usr/local/share/indi/scripts"); #else IUFillText(&ScriptsT[0], "FOLDER", "Folder", "/usr/share/indi/scripts"); #endif IUFillText(&ScriptsT[SCRIPT_CONNECT], "SCRIPT_CONNECT", "Connect script", "connect.py"); IUFillText(&ScriptsT[SCRIPT_DISCONNECT], "SCRIPT_DISCONNECT", "Disconnect script", "disconnect.py"); IUFillText(&ScriptsT[SCRIPT_STATUS], "SCRIPT_STATUS", "Get status script", "status.py"); IUFillText(&ScriptsT[SCRIPT_OPEN], "SCRIPT_OPEN", "Open shutter script", "open.py"); IUFillText(&ScriptsT[SCRIPT_CLOSE], "SCRIPT_CLOSE", "Close shutter script", "close.py"); IUFillText(&ScriptsT[SCRIPT_PARK], "SCRIPT_PARK", "Park script", "park.py"); IUFillText(&ScriptsT[SCRIPT_UNPARK], "SCRIPT_UNPARK", "Unpark script", "unpark.py"); IUFillText(&ScriptsT[SCRIPT_GOTO], "SCRIPT_GOTO", "Goto script", "goto.py"); IUFillText(&ScriptsT[SCRIPT_MOVE_CW], "SCRIPT_MOVE_CW", "Move clockwise script", "move_cw.py"); IUFillText(&ScriptsT[SCRIPT_MOVE_CCW], "SCRIPT_MOVE_CCW", "Move counter clockwise script", "move_ccw.py"); IUFillText(&ScriptsT[SCRIPT_ABORT], "SCRIPT_ABORT", "Abort motion script", "abort.py"); IUFillTextVector(&ScriptsTP, ScriptsT, SCRIPT_COUNT, getDefaultName(), "SCRIPTS", "Scripts", OPTIONS_TAB, IP_RW, 60, IPS_IDLE); setDefaultPollingPeriod(2000); return true; } bool DomeScript::saveConfigItems(FILE *fp) { INDI::Dome::saveConfigItems(fp); IUSaveConfigText(fp, &ScriptsTP); return true; } void DomeScript::ISGetProperties(const char *dev) { INDI::Dome::ISGetProperties(dev); defineText(&ScriptsTP); } bool DomeScript::ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n) { if (dev != nullptr && strcmp(dev, getDeviceName()) == 0) { if (strcmp(name, ScriptsTP.name) == 0) { IUUpdateText(&ScriptsTP, texts, names, n); ScriptsTP.s = IPS_OK; IDSetText(&ScriptsTP, nullptr); return true; } } return Dome::ISNewText(dev, name, texts, names, n); } bool DomeScript::RunScript(int script, ...) { char tmp[256]; strncpy(tmp, ScriptsT[script].text, sizeof(tmp)); char **args = (char **)malloc(MAXARGS * sizeof(char *)); int arg = 1; char *p = tmp; args[0] = p; while (arg < MAXARGS) { char *pp = strstr(p, " "); if (pp == nullptr) break; *pp++ = 0; args[arg++] = pp; p = pp; } va_list ap; va_start(ap, script); while (arg < MAXARGS) { char *pp = va_arg(ap, char *); args[arg++] = pp; if (pp == nullptr) break; } va_end(ap); char path[1024]; snprintf(path, sizeof(path), "%s/%s", ScriptsT[0].text, tmp); if (isDebug()) { char dbg[8 * 1024]; snprintf(dbg, sizeof(dbg), "execvp('%s'", path); for (int i = 0; args[i]; i++) { strcat(dbg, ", '"); strcat(dbg, args[i]); strcat(dbg, "'"); } strcat(dbg, ", NULL)"); LOG_DEBUG(dbg); } int pid = fork(); if (pid == -1) { LOG_ERROR("Fork failed"); return false; } else if (pid == 0) { execvp(path, args); LOG_DEBUG("Failed to execute script"); exit(0); } else { int status; waitpid(pid, &status, 0); LOGF_DEBUG("Script %s returned %d", ScriptsT[script].text, status); return status == 0; } } bool DomeScript::updateProperties() { INDI::Dome::updateProperties(); if (isConnected()) { if (InitPark()) { SetAxis1ParkDefault(0); } else { SetAxis1Park(0); SetAxis1ParkDefault(0); } TimerHit(); } return true; } void DomeScript::TimerHit() { if (!isConnected()) return; char name[1024]; char *s = tmpnam(name); INDI_UNUSED(s); bool status = RunScript(SCRIPT_STATUS, name, nullptr); if (status) { int parked = 0, shutter = 0; float az = 0; FILE *file = fopen(name, "r"); int ret = 0; ret = fscanf(file, "%d %d %f", &parked, &shutter, &az); fclose(file); unlink(name); DomeAbsPosN[0].value = az = round(range360(az) * 10) / 10; if (parked != 0) { if (getDomeState() == DOME_PARKING || getDomeState() == DOME_UNPARKED) { SetParked(true); TargetAz = az; LOG_INFO("Park succesfully executed"); } } else { if (getDomeState() == DOME_UNPARKING || getDomeState() == DOME_PARKED) { SetParked(false); TargetAz = az; LOG_INFO("Unpark succesfully executed"); } } if (std::round(az * 10) != std::round(TargetAz * 10)) { LOGF_INFO("Moving %g -> %g %d", std::round(az * 10) / 10, std::round(TargetAz * 10) / 10, getDomeState()); IDSetNumber(&DomeAbsPosNP, nullptr); } else if (getDomeState() == DOME_MOVING) { setDomeState(DOME_SYNCED); IDSetNumber(&DomeAbsPosNP, nullptr); } if (shutterState == SHUTTER_OPENED) { if (shutter == 0) { shutterState = SHUTTER_CLOSED; DomeShutterSP.s = IPS_OK; IDSetSwitch(&DomeShutterSP, nullptr); LOG_INFO("Shutter was succesfully closed"); } } else { if (shutter == 1) { shutterState = SHUTTER_OPENED; DomeShutterSP.s = IPS_OK; IDSetSwitch(&DomeShutterSP, nullptr); LOG_INFO("Shutter was succesfully opened"); } } } else { LOG_ERROR("Failed to read status"); } SetTimer(POLLMS); if (!isParked() && TimeSinceUpdate++ > 4) { TimeSinceUpdate = 0; UpdateMountCoords(); } } bool DomeScript::Connect() { if (isConnected()) return true; bool status = RunScript(SCRIPT_CONNECT, nullptr); if (status) { LOG_INFO("Successfully connected"); } return status; } bool DomeScript::Disconnect() { bool status = RunScript(SCRIPT_DISCONNECT, nullptr); if (status) { LOG_INFO("Successfully disconnected"); } return status; } IPState DomeScript::Park() { bool status = RunScript(SCRIPT_PARK, nullptr); if (status) { return IPS_BUSY; } LOG_ERROR("Failed to park"); return IPS_ALERT; } IPState DomeScript::UnPark() { bool status = RunScript(SCRIPT_UNPARK, nullptr); if (status) { return IPS_BUSY; } LOG_ERROR("Failed to unpark"); return IPS_ALERT; } IPState DomeScript::ControlShutter(ShutterOperation operation) { if (RunScript(operation == SHUTTER_OPEN ? SCRIPT_OPEN : SCRIPT_CLOSE, nullptr)) { return IPS_BUSY; } LOGF_ERROR("Failed to %s shutter", operation == SHUTTER_OPEN ? "open" : "close"); return IPS_ALERT; } IPState DomeScript::MoveAbs(double az) { char _az[16]; snprintf(_az, 16, "%f", round(az * 10) / 10); bool status = RunScript(SCRIPT_GOTO, _az, nullptr); if (status) { TargetAz = az; return IPS_BUSY; } return IPS_ALERT; } IPState DomeScript::Move(DomeDirection dir, DomeMotionCommand operation) { if (operation == MOTION_START) { if (RunScript(dir == DOME_CW ? SCRIPT_MOVE_CW : SCRIPT_MOVE_CCW, nullptr)) { DomeAbsPosNP.s = IPS_BUSY; TargetAz = -1; } else { DomeAbsPosNP.s = IPS_ALERT; } } else { if (RunScript(SCRIPT_ABORT, nullptr)) { DomeAbsPosNP.s = IPS_IDLE; } else { DomeAbsPosNP.s = IPS_ALERT; } } IDSetNumber(&DomeAbsPosNP, nullptr); return ((operation == MOTION_START) ? IPS_BUSY : IPS_OK); } bool DomeScript::Abort() { bool status = RunScript(SCRIPT_ABORT, nullptr); if (status) { LOG_INFO("Successfully aborted"); } return status; } libindi/drivers/dome/roll_off.cpp0000664000175000017500000001712213263645557016405 0ustar jasemjasem/******************************************************************************* Dome Simulator Copyright(c) 2014 Jasem Mutlaq. All rights reserved. This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2 as published by the Free Software Foundation. . 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 Library General Public License for more details. . You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. *******************************************************************************/ #include "roll_off.h" #include "indicom.h" #include #include #include #include // We declare an auto pointer to RollOff. std::unique_ptr rollOff(new RollOff()); #define ROLLOFF_DURATION 10 // 10 seconds until roof is fully opened or closed void ISPoll(void *p); void ISGetProperties(const char *dev) { rollOff->ISGetProperties(dev); } void ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) { rollOff->ISNewSwitch(dev, name, states, names, n); } void ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n) { rollOff->ISNewText(dev, name, texts, names, n); } void ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) { rollOff->ISNewNumber(dev, name, values, names, n); } void ISNewBLOB(const char *dev, const char *name, int sizes[], int blobsizes[], char *blobs[], char *formats[], char *names[], int n) { INDI_UNUSED(dev); INDI_UNUSED(name); INDI_UNUSED(sizes); INDI_UNUSED(blobsizes); INDI_UNUSED(blobs); INDI_UNUSED(formats); INDI_UNUSED(names); INDI_UNUSED(n); } void ISSnoopDevice(XMLEle *root) { rollOff->ISSnoopDevice(root); } RollOff::RollOff() { SetDomeCapability(DOME_CAN_ABORT | DOME_CAN_PARK); } /************************************************************************************ * * ***********************************************************************************/ bool RollOff::initProperties() { INDI::Dome::initProperties(); SetParkDataType(PARK_NONE); addAuxControls(); return true; } bool RollOff::ISSnoopDevice(XMLEle *root) { return INDI::Dome::ISSnoopDevice(root); } bool RollOff::SetupParms() { // If we have parking data if (InitPark()) { if (isParked()) { fullOpenLimitSwitch = ISS_OFF; fullClosedLimitSwitch = ISS_ON; } else { fullOpenLimitSwitch = ISS_ON; fullClosedLimitSwitch = ISS_OFF; } } // If we don't have parking data else { fullOpenLimitSwitch = ISS_OFF; fullClosedLimitSwitch = ISS_OFF; } return true; } bool RollOff::Connect() { SetTimer(1000); // start the timer return true; } bool RollOff::Disconnect() { return true; } const char *RollOff::getDefaultName() { return (const char *)"RollOff Simulator"; } bool RollOff::ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) { return INDI::Dome::ISNewSwitch(dev, name, states, names, n); } bool RollOff::updateProperties() { INDI::Dome::updateProperties(); if (isConnected()) { SetupParms(); } return true; } void RollOff::TimerHit() { if (!isConnected()) return; // No need to reset timer if we are not connected anymore if (DomeMotionSP.s == IPS_BUSY) { // Abort called if (MotionRequest < 0) { LOG_INFO("Roof motion is stopped."); setDomeState(DOME_IDLE); SetTimer(1000); return; } // Roll off is opening if (DomeMotionS[DOME_CW].s == ISS_ON) { if (getFullOpenedLimitSwitch()) { LOG_INFO("Roof is open."); SetParked(false); return; } } // Roll Off is closing else if (DomeMotionS[DOME_CCW].s == ISS_ON) { if (getFullClosedLimitSwitch()) { LOG_INFO("Roof is closed."); SetParked(true); return; } } SetTimer(1000); } //SetTimer(1000); } bool RollOff::saveConfigItems(FILE *fp) { return INDI::Dome::saveConfigItems(fp); } IPState RollOff::Move(DomeDirection dir, DomeMotionCommand operation) { if (operation == MOTION_START) { // DOME_CW --> OPEN. If can we are ask to "open" while we are fully opened as the limit switch indicates, then we simply return false. if (dir == DOME_CW && fullOpenLimitSwitch == ISS_ON) { LOG_WARN("Roof is already fully opened."); return IPS_ALERT; } else if (dir == DOME_CW && getWeatherState() == IPS_ALERT) { LOG_WARN("Weather conditions are in the danger zone. Cannot open roof."); return IPS_ALERT; } else if (dir == DOME_CCW && fullClosedLimitSwitch == ISS_ON) { LOG_WARN("Roof is already fully closed."); return IPS_ALERT; } else if (dir == DOME_CCW && INDI::Dome::isLocked()) { DEBUG(INDI::Logger::DBG_WARNING, "Cannot close dome when mount is locking. See: Telescope parkng policy, in options tab"); return IPS_ALERT; } fullOpenLimitSwitch = ISS_OFF; fullClosedLimitSwitch = ISS_OFF; MotionRequest = ROLLOFF_DURATION; gettimeofday(&MotionStart, nullptr); SetTimer(1000); return IPS_BUSY; } return (Dome::Abort() ? IPS_OK : IPS_ALERT); } IPState RollOff::Park() { IPState rc = INDI::Dome::Move(DOME_CCW, MOTION_START); if (rc == IPS_BUSY) { LOG_INFO("Roll off is parking..."); return IPS_BUSY; } else return IPS_ALERT; } IPState RollOff::UnPark() { IPState rc = INDI::Dome::Move(DOME_CW, MOTION_START); if (rc == IPS_BUSY) { LOG_INFO("Roll off is unparking..."); return IPS_BUSY; } else return IPS_ALERT; } bool RollOff::Abort() { MotionRequest = -1; // If both limit switches are off, then we're neither parked nor unparked. if (fullOpenLimitSwitch == ISS_OFF && fullClosedLimitSwitch == ISS_OFF) { IUResetSwitch(&ParkSP); ParkSP.s = IPS_IDLE; IDSetSwitch(&ParkSP, nullptr); } return true; } float RollOff::CalcTimeLeft(timeval start) { double timesince; double timeleft; struct timeval now { 0, 0 }; gettimeofday(&now, nullptr); timesince = (double)(now.tv_sec * 1000.0 + now.tv_usec / 1000) - (double)(start.tv_sec * 1000.0 + start.tv_usec / 1000); timesince = timesince / 1000; timeleft = MotionRequest - timesince; return timeleft; } bool RollOff::getFullOpenedLimitSwitch() { double timeleft = CalcTimeLeft(MotionStart); if (timeleft <= 0) { fullOpenLimitSwitch = ISS_ON; return true; } else return false; } bool RollOff::getFullClosedLimitSwitch() { double timeleft = CalcTimeLeft(MotionStart); if (timeleft <= 0) { fullClosedLimitSwitch = ISS_ON; return true; } else return false; } libindi/drivers/dome/dome_script.h0000664000175000017500000000354513263645557016564 0ustar jasemjasem/******************************************************************************* Copyright(c) 2016 CloudMakers, s. r. o.. All rights reserved. This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2 as published by the Free Software Foundation. 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 Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. *******************************************************************************/ #pragma once #include "indidome.h" class DomeScript : public INDI::Dome { public: DomeScript(); virtual ~DomeScript() = default; virtual const char *getDefaultName(); virtual bool initProperties(); virtual bool saveConfigItems(FILE *fp); void ISGetProperties(const char *dev); bool ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n); bool updateProperties(); protected: void TimerHit(); virtual bool Connect(); virtual bool Disconnect(); virtual IPState Move(DomeDirection dir, DomeMotionCommand operation); virtual IPState MoveAbs(double az); virtual IPState Park(); virtual IPState UnPark(); virtual IPState ControlShutter(ShutterOperation operation); virtual bool Abort(); private: bool ReadDomeStatus(); bool RunScript(int script, ...); ITextVectorProperty ScriptsTP; IText ScriptsT[15] {}; double TargetAz { 0 }; int TimeSinceUpdate { 0 }; }; libindi/drivers/dome/dome_script.txt0000664000175000017500000001003613263645557017145 0ustar jasemjasemSample scripts for INDI Dome Scripting Gateway This is python scripts used to test INDI Dome Scripting Gateway. The default folder for them is /usr/share/indi/scripts (or /usr/local/share/indi/scripts on OSX). You can use any other folder, any other script names and any other script language, just make sure, that all of them have "executable" bit set. All scripts except for status.py are called only when related driver property is set. status.py is called periodically and is supposed to create file with name submitted as parameter containing single line with 3 number: 0/1 for unparked/parked, 0/1 for closed/open shutter and azimuth as float. ---------- connect.py -------------- #!/usr/bin/python # # Connect script for INDI Dome Scripting Gateway # # Arguments: none # Exit code: 0 for success, 1 for failure # import sys coordinates = open('/tmp/indi-status', 'w') coordinates.truncate() coordinates.write('1 0 0') coordinates.close() sys.exit(0) ---------- disconnect.py -------------- #!/usr/bin/python # # Connect script for INDI Dome Scripting Gateway # # Arguments: none # Exit code: 0 for success, 1 for failure # import sys sys.exit(0) ---------- park.py -------------- #!/usr/bin/python # # Park script for INDI Dome Scripting Gateway # # Arguments: none # Exit code: 0 for success, 1 for failure # import sys coordinates = open('/tmp/indi-status', 'w') coordinates.truncate() coordinates.write('1 0 0') coordinates.close() sys.exit(0) ---------- unpark.py -------------- #!/usr/bin/python # # Park script for INDI Dome Scripting Gateway # # Arguments: none # Exit code: 0 for success, 1 for failure # import sys coordinates = open('/tmp/indi-status', 'w') coordinates.truncate() coordinates.write('0 0 0') coordinates.close() sys.exit(0) ---------- goto.py -------------- #!/usr/bin/python # # Goto azimuth script for INDI Dome Scripting Gateway # # Arguments: Az # Exit code: 0 for success, 1 for failure # import sys script, az = sys.argv coordinates = open('/tmp/indi-status', 'r') str = coordinates.readline() coordinates.close() str = str[0:3] + az coordinates = open('/tmp/indi-status', 'w') coordinates.truncate() coordinates.write(str) coordinates.close() sys.exit(0) ---------- open.py -------------- #!/usr/bin/python # # Open shutter script for INDI Dome Scripting Gateway # # Arguments: none # Exit code: 0 for success, 1 for failure # import sys coordinates = open('/tmp/indi-status', 'r') str = coordinates.readline() coordinates.close() str = str[0] + ' 1 ' + str[4:] coordinates = open('/tmp/indi-status', 'w') coordinates.truncate() coordinates.write(str) coordinates.close() sys.exit(0) ---------- close.py -------------- #!/usr/bin/python # # Close shutter script for INDI Dome Scripting Gateway # # Arguments: none # Exit code: 0 for success, 1 for failure # import sys coordinates = open('/tmp/indi-status', 'r') str = coordinates.readline() coordinates.close() str = str[0] + ' 0 ' + str[4:] coordinates = open('/tmp/indi-status', 'w') coordinates.truncate() coordinates.write(str) coordinates.close() sys.exit(0) ---------- move_cw.py -------------- #!/usr/bin/python # # Move clockwise script for INDI Dome Scripting Gateway # # Arguments: none # Exit code: 0 for success, 1 for failure # import sys sys.exit(0) ---------- move_ccw.py -------------- #!/usr/bin/python # # Move counter clockwise script for INDI Dome Scripting Gateway # # Arguments: none # Exit code: 0 for success, 1 for failure # import sys sys.exit(0) ---------- abort.py -------------- #!/usr/bin/python # # Abort script for INDI Dome Scripting Gateway # # Arguments: none # Exit code: 0 for success, 1 for failure # import sys sys.exit(0) ---------- status.py -------------- #!/usr/bin/python # # Status script for INDI Dome Scripting Gateway # # Arguments: file name to save current state and coordinates (parked ra dec) # Exit code: 0 for success, 1 for failure # import sys script, path = sys.argv coordinates = open('/tmp/indi-status', 'r') status = open(path, 'w') status.truncate() status.write(coordinates.readline()) status.close() sys.exit(0) libindi/COPYING.LGPL0000664000175000017500000006364213263645557013275 0ustar jasemjasem GNU LESSER GENERAL PUBLIC LICENSE Version 2.1, February 1999 Copyright (C) 1991, 1999 Free Software Foundation, Inc. 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. [This is the first released version of the Lesser GPL. It also counts as the successor of the GNU Library Public License, version 2, hence the version number 2.1.] Preamble The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public Licenses are intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This license, the Lesser General Public License, applies to some specially designated software packages--typically libraries--of the Free Software Foundation and other authors who decide to use it. You can use it too, but we suggest you first think carefully about whether this license or the ordinary General Public License is the better strategy to use in any particular case, based on the explanations below. When we speak of free software, we are referring to freedom of use, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish); that you receive source code or can get it if you want it; that you can change the software and use pieces of it in new free programs; and that you are informed that you can do these things. To protect your rights, we need to make restrictions that forbid distributors to deny you these rights or to ask you to surrender these rights. These restrictions translate to certain responsibilities for you if you distribute copies of the library or if you modify it. For example, if you distribute copies of the library, whether gratis or for a fee, you must give the recipients all the rights that we gave you. You must make sure that they, too, receive or can get the source code. If you link other code with the library, you must provide complete object files to the recipients, so that they can relink them with the library after making changes to the library and recompiling it. And you must show them these terms so they know their rights. We protect your rights with a two-step method: (1) we copyright the library, and (2) we offer you this license, which gives you legal permission to copy, distribute and/or modify the library. To protect each distributor, we want to make it very clear that there is no warranty for the free library. Also, if the library is modified by someone else and passed on, the recipients should know that what they have is not the original version, so that the original author's reputation will not be affected by problems that might be introduced by others. Finally, software patents pose a constant threat to the existence of any free program. We wish to make sure that a company cannot effectively restrict the users of a free program by obtaining a restrictive license from a patent holder. Therefore, we insist that any patent license obtained for a version of the library must be consistent with the full freedom of use specified in this license. Most GNU software, including some libraries, is covered by the ordinary GNU General Public License. This license, the GNU Lesser General Public License, applies to certain designated libraries, and is quite different from the ordinary General Public License. We use this license for certain libraries in order to permit linking those libraries into non-free programs. When a program is linked with a library, whether statically or using a shared library, the combination of the two is legally speaking a combined work, a derivative of the original library. The ordinary General Public License therefore permits such linking only if the entire combination fits its criteria of freedom. The Lesser General Public License permits more lax criteria for linking other code with the library. We call this license the "Lesser" General Public License because it does Less to protect the user's freedom than the ordinary General Public License. It also provides other free software developers Less of an advantage over competing non-free programs. These disadvantages are the reason we use the ordinary General Public License for many libraries. However, the Lesser license provides advantages in certain special circumstances. For example, on rare occasions, there may be a special need to encourage the widest possible use of a certain library, so that it becomes a de-facto standard. To achieve this, non-free programs must be allowed to use the library. A more frequent case is that a free library does the same job as widely used non-free libraries. In this case, there is little to gain by limiting the free library to free software only, so we use the Lesser General Public License. In other cases, permission to use a particular library in non-free programs enables a greater number of people to use a large body of free software. For example, permission to use the GNU C Library in non-free programs enables many more people to use the whole GNU operating system, as well as its variant, the GNU/Linux operating system. Although the Lesser General Public License is Less protective of the users' freedom, it does ensure that the user of a program that is linked with the Library has the freedom and the wherewithal to run that program using a modified version of the Library. The precise terms and conditions for copying, distribution and modification follow. Pay close attention to the difference between a "work based on the library" and a "work that uses the library". The former contains code derived from the library, whereas the latter must be combined with the library in order to run. GNU LESSER GENERAL PUBLIC LICENSE TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 0. This License Agreement applies to any software library or other program which contains a notice placed by the copyright holder or other authorized party saying it may be distributed under the terms of this Lesser General Public License (also called "this License"). Each licensee is addressed as "you". A "library" means a collection of software functions and/or data prepared so as to be conveniently linked with application programs (which use some of those functions and data) to form executables. The "Library", below, refers to any such software library or work which has been distributed under these terms. A "work based on the Library" means either the Library or any derivative work under copyright law: that is to say, a work containing the Library or a portion of it, either verbatim or with modifications and/or translated straightforwardly into another language. (Hereinafter, translation is included without limitation in the term "modification".) "Source code" for a work means the preferred form of the work for making modifications to it. For a library, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the library. Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running a program using the Library is not restricted, and output from such a program is covered only if its contents constitute a work based on the Library (independent of the use of the Library in a tool for writing it). Whether that is true depends on what the Library does and what the program that uses the Library does. 1. You may copy and distribute verbatim copies of the Library's complete source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and distribute a copy of this License along with the Library. You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. 2. You may modify your copy or copies of the Library or any portion of it, thus forming a work based on the Library, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions: a) The modified work must itself be a software library. b) You must cause the files modified to carry prominent notices stating that you changed the files and the date of any change. c) You must cause the whole of the work to be licensed at no charge to all third parties under the terms of this License. d) If a facility in the modified Library refers to a function or a table of data to be supplied by an application program that uses the facility, other than as an argument passed when the facility is invoked, then you must make a good faith effort to ensure that, in the event an application does not supply such function or table, the facility still operates, and performs whatever part of its purpose remains meaningful. (For example, a function in a library to compute square roots has a purpose that is entirely well-defined independent of the application. Therefore, Subsection 2d requires that any application-supplied function or table used by this function must be optional: if the application does not supply it, the square root function must still compute square roots.) These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Library, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Library, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Library. In addition, mere aggregation of another work not based on the Library with the Library (or with a work based on the Library) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. 3. You may opt to apply the terms of the ordinary GNU General Public License instead of this License to a given copy of the Library. To do this, you must alter all the notices that refer to this License, so that they refer to the ordinary GNU General Public License, version 2, instead of to this License. (If a newer version than version 2 of the ordinary GNU General Public License has appeared, then you can specify that version instead if you wish.) Do not make any other change in these notices. Once this change is made in a given copy, it is irreversible for that copy, so the ordinary GNU General Public License applies to all subsequent copies and derivative works made from that copy. This option is useful when you wish to copy part of the code of the Library into a program that is not a library. 4. You may copy and distribute the Library (or a portion or derivative of it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange. If distribution of object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place satisfies the requirement to distribute the source code, even though third parties are not compelled to copy the source along with the object code. 5. A program that contains no derivative of any portion of the Library, but is designed to work with the Library by being compiled or linked with it, is called a "work that uses the Library". Such a work, in isolation, is not a derivative work of the Library, and therefore falls outside the scope of this License. However, linking a "work that uses the Library" with the Library creates an executable that is a derivative of the Library (because it contains portions of the Library), rather than a "work that uses the library". The executable is therefore covered by this License. Section 6 states terms for distribution of such executables. When a "work that uses the Library" uses material from a header file that is part of the Library, the object code for the work may be a derivative work of the Library even though the source code is not. Whether this is true is especially significant if the work can be linked without the Library, or if the work is itself a library. The threshold for this to be true is not precisely defined by law. If such an object file uses only numerical parameters, data structure layouts and accessors, and small macros and small inline functions (ten lines or less in length), then the use of the object file is unrestricted, regardless of whether it is legally a derivative work. (Executables containing this object code plus portions of the Library will still fall under Section 6.) Otherwise, if the work is a derivative of the Library, you may distribute the object code for the work under the terms of Section 6. Any executables containing that work also fall under Section 6, whether or not they are linked directly with the Library itself. 6. As an exception to the Sections above, you may also combine or link a "work that uses the Library" with the Library to produce a work containing portions of the Library, and distribute that work under terms of your choice, provided that the terms permit modification of the work for the customer's own use and reverse engineering for debugging such modifications. You must give prominent notice with each copy of the work that the Library is used in it and that the Library and its use are covered by this License. You must supply a copy of this License. If the work during execution displays copyright notices, you must include the copyright notice for the Library among them, as well as a reference directing the user to the copy of this License. Also, you must do one of these things: a) Accompany the work with the complete corresponding machine-readable source code for the Library including whatever changes were used in the work (which must be distributed under Sections 1 and 2 above); and, if the work is an executable linked with the Library, with the complete machine-readable "work that uses the Library", as object code and/or source code, so that the user can modify the Library and then relink to produce a modified executable containing the modified Library. (It is understood that the user who changes the contents of definitions files in the Library will not necessarily be able to recompile the application to use the modified definitions.) b) Use a suitable shared library mechanism for linking with the Library. A suitable mechanism is one that (1) uses at run time a copy of the library already present on the user's computer system, rather than copying library functions into the executable, and (2) will operate properly with a modified version of the library, if the user installs one, as long as the modified version is interface-compatible with the version that the work was made with. c) Accompany the work with a written offer, valid for at least three years, to give the same user the materials specified in Subsection 6a, above, for a charge no more than the cost of performing this distribution. d) If distribution of the work is made by offering access to copy from a designated place, offer equivalent access to copy the above specified materials from the same place. e) Verify that the user has already received a copy of these materials or that you have already sent this user a copy. For an executable, the required form of the "work that uses the Library" must include any data and utility programs needed for reproducing the executable from it. However, as a special exception, the materials to be distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable. It may happen that this requirement contradicts the license restrictions of other proprietary libraries that do not normally accompany the operating system. Such a contradiction means you cannot use both them and the Library together in an executable that you distribute. 7. You may place library facilities that are a work based on the Library side-by-side in a single library together with other library facilities not covered by this License, and distribute such a combined library, provided that the separate distribution of the work based on the Library and of the other library facilities is otherwise permitted, and provided that you do these two things: a) Accompany the combined library with a copy of the same work based on the Library, uncombined with any other library facilities. This must be distributed under the terms of the Sections above. b) Give prominent notice with the combined library of the fact that part of it is a work based on the Library, and explaining where to find the accompanying uncombined form of the same work. 8. You may not copy, modify, sublicense, link with, or distribute the Library except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense, link with, or distribute the Library is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. 9. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Library or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Library (or any work based on the Library), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Library or works based on it. 10. Each time you redistribute the Library (or any work based on the Library), the recipient automatically receives a license from the original licensor to copy, distribute, link with or modify the Library subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties with this License. 11. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Library at all. For example, if a patent license would not permit royalty-free redistribution of the Library by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Library. If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply, and the section as a whole is intended to apply in other circumstances. It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice. This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. 12. If the distribution and/or use of the Library is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Library under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License. 13. The Free Software Foundation may publish revised and/or new versions of the Lesser General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Library specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Library does not specify a license version number, you may choose any version ever published by the Free Software Foundation. 14. If you wish to incorporate parts of the Library into other free programs whose distribution conditions are incompatible with these, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. NO WARRANTY 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Libraries If you develop a new library, and you want it to be of the greatest possible use to the public, we recommend making it free software that everyone can redistribute and change. You can do so by permitting redistribution under these terms (or, alternatively, under the terms of the ordinary General Public License). To apply these terms, attach the following notices to the library. It is safest to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) 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 Also add information on how to contact you by electronic and paper mail. You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the library, if necessary. Here is a sample; alter the names: Yoyodyne, Inc., hereby disclaims all copyright interest in the library `Frob' (a library for tweaking knobs) written by James Random Hacker. , 1 April 1990 Ty Coon, President of Vice That's all there is to it! libindi/base64.h0000664000175000017500000000322713263645557012733 0ustar jasemjasem#if 0 INDI Copyright (C) 2003 Elwood C. Downey 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 #pragma once #ifdef __cplusplus extern "C" { #endif /** * \defgroup base64 Base 64 Functions: Convert from and to base64 */ /*@{*/ /** \brief Convert bytes array to base64. \param out output buffer in base64. The buffer size must be at least (4 * inlen / 3 + 4) bytes long. \param in input binary buffer \param inlen number of bytes to convert \return 0 on success, -1 on failure. */ extern int to64frombits(unsigned char *out, const unsigned char *in, int inlen); /** \brief Convert base64 to bytes array. \param out output buffer in bytes. The buffer size must be at least (3 * size_of_in_buffer / 4) bytes long. \param in input base64 buffer \param inlen base64 buffer lenght \return 0 on success, -1 on failure. */ extern int from64tobits(char *out, const char *in); extern int from64tobits_fast(char *out, const char *in, int inlen); /*@}*/ #ifdef __cplusplus } #endif libindi/libindi.pc.cmake0000664000175000017500000000057413263645557014515 0ustar jasemjasemprefix=@CMAKE_INSTALL_PREFIX@ exec_prefix=@CMAKE_INSTALL_PREFIX@ libdir=@PKG_CONFIG_LIBDIR@ includedir=@INCLUDE_INSTALL_DIR@ Name: libindi Description: Instrument Neutral Distributed Interface URL: http://www.indilib.org/ Version: @CMAKE_INDI_VERSION_STRING@ Libs: -L${libdir} @PKG_CONFIG_LIBS@ Libs.private: -lz -lcfitsio -lnova Cflags: -I${includedir} -I${includedir}/libindi libindi/config.h.cmake0000664000175000017500000000035713263645557014174 0ustar jasemjasem/* Set INDI Library version */ #cmakedefine CMAKE_INDI_VERSION_STRING "@CMAKE_INDI_VERSION_STRING@" /* Define INDI Data Dir */ #cmakedefine DATA_INSTALL_DIR "@DATA_INSTALL_DIR@" /* Set when theora is detected */ #cmakedefine HAVE_THEORA libindi/base64_luts.h0000664000175000017500000163222213263645557014006 0ustar jasemjasem#if 0 INDI Copyright (C) 2016 Rumen G. Bogdanovski 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. #endif #pragma once #include static const char base64digits[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; static const char base64lut[] = "AAABACADAEAFAGAHAIAJAKALAMANAOAPAQARASATAUAVAWAXAYAZAaAbAcAdAeAfAgAhAiAjAkAlAmAnAoApAq" "ArAsAtAuAvAwAxAyAzA0A1A2A3A4A5A6A7A8A9A+A/" "BABBBCBDBEBFBGBHBIBJBKBLBMBNBOBPBQBRBSBTBUBVBWBXBYBZBaBbBcBdBeBfBgBhBiBjBkBlBmBnBoBpBq" "BrBsBtBuBvBwBxByBzB0B1B2B3B4B5B6B7B8B9B+B/" "CACBCCCDCECFCGCHCICJCKCLCMCNCOCPCQCRCSCTCUCVCWCXCYCZCaCbCcCdCeCfCgChCiCjCkClCmCnCoCpCq" "CrCsCtCuCvCwCxCyCzC0C1C2C3C4C5C6C7C8C9C+C/" "DADBDCDDDEDFDGDHDIDJDKDLDMDNDODPDQDRDSDTDUDVDWDXDYDZDaDbDcDdDeDfDgDhDiDjDkDlDmDnDoDpDq" "DrDsDtDuDvDwDxDyDzD0D1D2D3D4D5D6D7D8D9D+D/" "EAEBECEDEEEFEGEHEIEJEKELEMENEOEPEQERESETEUEVEWEXEYEZEaEbEcEdEeEfEgEhEiEjEkElEmEnEoEpEq" "ErEsEtEuEvEwExEyEzE0E1E2E3E4E5E6E7E8E9E+E/" "FAFBFCFDFEFFFGFHFIFJFKFLFMFNFOFPFQFRFSFTFUFVFWFXFYFZFaFbFcFdFeFfFgFhFiFjFkFlFmFnFoFpFq" "FrFsFtFuFvFwFxFyFzF0F1F2F3F4F5F6F7F8F9F+F/" "GAGBGCGDGEGFGGGHGIGJGKGLGMGNGOGPGQGRGSGTGUGVGWGXGYGZGaGbGcGdGeGfGgGhGiGjGkGlGmGnGoGpGq" "GrGsGtGuGvGwGxGyGzG0G1G2G3G4G5G6G7G8G9G+G/" "HAHBHCHDHEHFHGHHHIHJHKHLHMHNHOHPHQHRHSHTHUHVHWHXHYHZHaHbHcHdHeHfHgHhHiHjHkHlHmHnHoHpHq" "HrHsHtHuHvHwHxHyHzH0H1H2H3H4H5H6H7H8H9H+H/" "IAIBICIDIEIFIGIHIIIJIKILIMINIOIPIQIRISITIUIVIWIXIYIZIaIbIcIdIeIfIgIhIiIjIkIlImInIoIpIq" "IrIsItIuIvIwIxIyIzI0I1I2I3I4I5I6I7I8I9I+I/" "JAJBJCJDJEJFJGJHJIJJJKJLJMJNJOJPJQJRJSJTJUJVJWJXJYJZJaJbJcJdJeJfJgJhJiJjJkJlJmJnJoJpJq" "JrJsJtJuJvJwJxJyJzJ0J1J2J3J4J5J6J7J8J9J+J/" "KAKBKCKDKEKFKGKHKIKJKKKLKMKNKOKPKQKRKSKTKUKVKWKXKYKZKaKbKcKdKeKfKgKhKiKjKkKlKmKnKoKpKq" "KrKsKtKuKvKwKxKyKzK0K1K2K3K4K5K6K7K8K9K+K/" "LALBLCLDLELFLGLHLILJLKLLLMLNLOLPLQLRLSLTLULVLWLXLYLZLaLbLcLdLeLfLgLhLiLjLkLlLmLnLoLpLq" "LrLsLtLuLvLwLxLyLzL0L1L2L3L4L5L6L7L8L9L+L/" "MAMBMCMDMEMFMGMHMIMJMKMLMMMNMOMPMQMRMSMTMUMVMWMXMYMZMaMbMcMdMeMfMgMhMiMjMkMlMmMnMoMpMq" "MrMsMtMuMvMwMxMyMzM0M1M2M3M4M5M6M7M8M9M+M/" "NANBNCNDNENFNGNHNINJNKNLNMNNNONPNQNRNSNTNUNVNWNXNYNZNaNbNcNdNeNfNgNhNiNjNkNlNmNnNoNpNq" "NrNsNtNuNvNwNxNyNzN0N1N2N3N4N5N6N7N8N9N+N/" "OAOBOCODOEOFOGOHOIOJOKOLOMONOOOPOQOROSOTOUOVOWOXOYOZOaObOcOdOeOfOgOhOiOjOkOlOmOnOoOpOq" "OrOsOtOuOvOwOxOyOzO0O1O2O3O4O5O6O7O8O9O+O/" "PAPBPCPDPEPFPGPHPIPJPKPLPMPNPOPPPQPRPSPTPUPVPWPXPYPZPaPbPcPdPePfPgPhPiPjPkPlPmPnPoPpPq" "PrPsPtPuPvPwPxPyPzP0P1P2P3P4P5P6P7P8P9P+P/" "QAQBQCQDQEQFQGQHQIQJQKQLQMQNQOQPQQQRQSQTQUQVQWQXQYQZQaQbQcQdQeQfQgQhQiQjQkQlQmQnQoQpQq" "QrQsQtQuQvQwQxQyQzQ0Q1Q2Q3Q4Q5Q6Q7Q8Q9Q+Q/" "RARBRCRDRERFRGRHRIRJRKRLRMRNRORPRQRRRSRTRURVRWRXRYRZRaRbRcRdReRfRgRhRiRjRkRlRmRnRoRpRq" "RrRsRtRuRvRwRxRyRzR0R1R2R3R4R5R6R7R8R9R+R/" "SASBSCSDSESFSGSHSISJSKSLSMSNSOSPSQSRSSSTSUSVSWSXSYSZSaSbScSdSeSfSgShSiSjSkSlSmSnSoSpSq" "SrSsStSuSvSwSxSySzS0S1S2S3S4S5S6S7S8S9S+S/" "TATBTCTDTETFTGTHTITJTKTLTMTNTOTPTQTRTSTTTUTVTWTXTYTZTaTbTcTdTeTfTgThTiTjTkTlTmTnToTpTq" "TrTsTtTuTvTwTxTyTzT0T1T2T3T4T5T6T7T8T9T+T/" "UAUBUCUDUEUFUGUHUIUJUKULUMUNUOUPUQURUSUTUUUVUWUXUYUZUaUbUcUdUeUfUgUhUiUjUkUlUmUnUoUpUq" "UrUsUtUuUvUwUxUyUzU0U1U2U3U4U5U6U7U8U9U+U/" "VAVBVCVDVEVFVGVHVIVJVKVLVMVNVOVPVQVRVSVTVUVVVWVXVYVZVaVbVcVdVeVfVgVhViVjVkVlVmVnVoVpVq" "VrVsVtVuVvVwVxVyVzV0V1V2V3V4V5V6V7V8V9V+V/" "WAWBWCWDWEWFWGWHWIWJWKWLWMWNWOWPWQWRWSWTWUWVWWWXWYWZWaWbWcWdWeWfWgWhWiWjWkWlWmWnWoWpWq" "WrWsWtWuWvWwWxWyWzW0W1W2W3W4W5W6W7W8W9W+W/" "XAXBXCXDXEXFXGXHXIXJXKXLXMXNXOXPXQXRXSXTXUXVXWXXXYXZXaXbXcXdXeXfXgXhXiXjXkXlXmXnXoXpXq" "XrXsXtXuXvXwXxXyXzX0X1X2X3X4X5X6X7X8X9X+X/" "YAYBYCYDYEYFYGYHYIYJYKYLYMYNYOYPYQYRYSYTYUYVYWYXYYYZYaYbYcYdYeYfYgYhYiYjYkYlYmYnYoYpYq" "YrYsYtYuYvYwYxYyYzY0Y1Y2Y3Y4Y5Y6Y7Y8Y9Y+Y/" "ZAZBZCZDZEZFZGZHZIZJZKZLZMZNZOZPZQZRZSZTZUZVZWZXZYZZZaZbZcZdZeZfZgZhZiZjZkZlZmZnZoZpZq" "ZrZsZtZuZvZwZxZyZzZ0Z1Z2Z3Z4Z5Z6Z7Z8Z9Z+Z/" "aAaBaCaDaEaFaGaHaIaJaKaLaMaNaOaPaQaRaSaTaUaVaWaXaYaZaaabacadaeafagahaiajakalamanaoapaq" "arasatauavawaxayaza0a1a2a3a4a5a6a7a8a9a+a/" "bAbBbCbDbEbFbGbHbIbJbKbLbMbNbObPbQbRbSbTbUbVbWbXbYbZbabbbcbdbebfbgbhbibjbkblbmbnbobpbq" "brbsbtbubvbwbxbybzb0b1b2b3b4b5b6b7b8b9b+b/" "cAcBcCcDcEcFcGcHcIcJcKcLcMcNcOcPcQcRcScTcUcVcWcXcYcZcacbcccdcecfcgchcicjckclcmcncocpcq" "crcsctcucvcwcxcyczc0c1c2c3c4c5c6c7c8c9c+c/" "dAdBdCdDdEdFdGdHdIdJdKdLdMdNdOdPdQdRdSdTdUdVdWdXdYdZdadbdcdddedfdgdhdidjdkdldmdndodpdq" "drdsdtdudvdwdxdydzd0d1d2d3d4d5d6d7d8d9d+d/" "eAeBeCeDeEeFeGeHeIeJeKeLeMeNeOePeQeReSeTeUeVeWeXeYeZeaebecedeeefegeheiejekelemeneoepeq" "ereseteuevewexeyeze0e1e2e3e4e5e6e7e8e9e+e/" "fAfBfCfDfEfFfGfHfIfJfKfLfMfNfOfPfQfRfSfTfUfVfWfXfYfZfafbfcfdfefffgfhfifjfkflfmfnfofpfq" "frfsftfufvfwfxfyfzf0f1f2f3f4f5f6f7f8f9f+f/" "gAgBgCgDgEgFgGgHgIgJgKgLgMgNgOgPgQgRgSgTgUgVgWgXgYgZgagbgcgdgegfggghgigjgkglgmgngogpgq" "grgsgtgugvgwgxgygzg0g1g2g3g4g5g6g7g8g9g+g/" "hAhBhChDhEhFhGhHhIhJhKhLhMhNhOhPhQhRhShThUhVhWhXhYhZhahbhchdhehfhghhhihjhkhlhmhnhohphq" "hrhshthuhvhwhxhyhzh0h1h2h3h4h5h6h7h8h9h+h/" "iAiBiCiDiEiFiGiHiIiJiKiLiMiNiOiPiQiRiSiTiUiViWiXiYiZiaibicidieifigihiiijikiliminioipiq" "irisitiuiviwixiyizi0i1i2i3i4i5i6i7i8i9i+i/" "jAjBjCjDjEjFjGjHjIjJjKjLjMjNjOjPjQjRjSjTjUjVjWjXjYjZjajbjcjdjejfjgjhjijjjkjljmjnjojpjq" "jrjsjtjujvjwjxjyjzj0j1j2j3j4j5j6j7j8j9j+j/" "kAkBkCkDkEkFkGkHkIkJkKkLkMkNkOkPkQkRkSkTkUkVkWkXkYkZkakbkckdkekfkgkhkikjkkklkmknkokpkq" "krksktkukvkwkxkykzk0k1k2k3k4k5k6k7k8k9k+k/" "lAlBlClDlElFlGlHlIlJlKlLlMlNlOlPlQlRlSlTlUlVlWlXlYlZlalblcldlelflglhliljlklllmlnlolplq" "lrlsltlulvlwlxlylzl0l1l2l3l4l5l6l7l8l9l+l/" "mAmBmCmDmEmFmGmHmImJmKmLmMmNmOmPmQmRmSmTmUmVmWmXmYmZmambmcmdmemfmgmhmimjmkmlmmmnmompmq" "mrmsmtmumvmwmxmymzm0m1m2m3m4m5m6m7m8m9m+m/" "nAnBnCnDnEnFnGnHnInJnKnLnMnNnOnPnQnRnSnTnUnVnWnXnYnZnanbncndnenfngnhninjnknlnmnnnonpnq" "nrnsntnunvnwnxnynzn0n1n2n3n4n5n6n7n8n9n+n/" "oAoBoCoDoEoFoGoHoIoJoKoLoMoNoOoPoQoRoSoToUoVoWoXoYoZoaobocodoeofogohoiojokolomonooopoq" "orosotouovowoxoyozo0o1o2o3o4o5o6o7o8o9o+o/" "pApBpCpDpEpFpGpHpIpJpKpLpMpNpOpPpQpRpSpTpUpVpWpXpYpZpapbpcpdpepfpgphpipjpkplpmpnpopppq" "prpsptpupvpwpxpypzp0p1p2p3p4p5p6p7p8p9p+p/" "qAqBqCqDqEqFqGqHqIqJqKqLqMqNqOqPqQqRqSqTqUqVqWqXqYqZqaqbqcqdqeqfqgqhqiqjqkqlqmqnqoqpqq" "qrqsqtquqvqwqxqyqzq0q1q2q3q4q5q6q7q8q9q+q/" "rArBrCrDrErFrGrHrIrJrKrLrMrNrOrPrQrRrSrTrUrVrWrXrYrZrarbrcrdrerfrgrhrirjrkrlrmrnrorprq" "rrrsrtrurvrwrxryrzr0r1r2r3r4r5r6r7r8r9r+r/" "sAsBsCsDsEsFsGsHsIsJsKsLsMsNsOsPsQsRsSsTsUsVsWsXsYsZsasbscsdsesfsgshsisjskslsmsnsospsq" "srssstsusvswsxsyszs0s1s2s3s4s5s6s7s8s9s+s/" "tAtBtCtDtEtFtGtHtItJtKtLtMtNtOtPtQtRtStTtUtVtWtXtYtZtatbtctdtetftgthtitjtktltmtntotptq" "trtstttutvtwtxtytzt0t1t2t3t4t5t6t7t8t9t+t/" "uAuBuCuDuEuFuGuHuIuJuKuLuMuNuOuPuQuRuSuTuUuVuWuXuYuZuaubucudueufuguhuiujukulumunuoupuq" "urusutuuuvuwuxuyuzu0u1u2u3u4u5u6u7u8u9u+u/" "vAvBvCvDvEvFvGvHvIvJvKvLvMvNvOvPvQvRvSvTvUvVvWvXvYvZvavbvcvdvevfvgvhvivjvkvlvmvnvovpvq" "vrvsvtvuvvvwvxvyvzv0v1v2v3v4v5v6v7v8v9v+v/" "wAwBwCwDwEwFwGwHwIwJwKwLwMwNwOwPwQwRwSwTwUwVwWwXwYwZwawbwcwdwewfwgwhwiwjwkwlwmwnwowpwq" "wrwswtwuwvwwwxwywzw0w1w2w3w4w5w6w7w8w9w+w/" "xAxBxCxDxExFxGxHxIxJxKxLxMxNxOxPxQxRxSxTxUxVxWxXxYxZxaxbxcxdxexfxgxhxixjxkxlxmxnxoxpxq" "xrxsxtxuxvxwxxxyxzx0x1x2x3x4x5x6x7x8x9x+x/" "yAyByCyDyEyFyGyHyIyJyKyLyMyNyOyPyQyRySyTyUyVyWyXyYyZyaybycydyeyfygyhyiyjykylymynyoypyq" "yrysytyuyvywyxyyyzy0y1y2y3y4y5y6y7y8y9y+y/" "zAzBzCzDzEzFzGzHzIzJzKzLzMzNzOzPzQzRzSzTzUzVzWzXzYzZzazbzczdzezfzgzhzizjzkzlzmznzozpzq" "zrzsztzuzvzwzxzyzzz0z1z2z3z4z5z6z7z8z9z+z/" "0A0B0C0D0E0F0G0H0I0J0K0L0M0N0O0P0Q0R0S0T0U0V0W0X0Y0Z0a0b0c0d0e0f0g0h0i0j0k0l0m0n0o0p0q" "0r0s0t0u0v0w0x0y0z000102030405060708090+0/" "1A1B1C1D1E1F1G1H1I1J1K1L1M1N1O1P1Q1R1S1T1U1V1W1X1Y1Z1a1b1c1d1e1f1g1h1i1j1k1l1m1n1o1p1q" "1r1s1t1u1v1w1x1y1z101112131415161718191+1/" "2A2B2C2D2E2F2G2H2I2J2K2L2M2N2O2P2Q2R2S2T2U2V2W2X2Y2Z2a2b2c2d2e2f2g2h2i2j2k2l2m2n2o2p2q" "2r2s2t2u2v2w2x2y2z202122232425262728292+2/" "3A3B3C3D3E3F3G3H3I3J3K3L3M3N3O3P3Q3R3S3T3U3V3W3X3Y3Z3a3b3c3d3e3f3g3h3i3j3k3l3m3n3o3p3q" "3r3s3t3u3v3w3x3y3z303132333435363738393+3/" "4A4B4C4D4E4F4G4H4I4J4K4L4M4N4O4P4Q4R4S4T4U4V4W4X4Y4Z4a4b4c4d4e4f4g4h4i4j4k4l4m4n4o4p4q" "4r4s4t4u4v4w4x4y4z404142434445464748494+4/" "5A5B5C5D5E5F5G5H5I5J5K5L5M5N5O5P5Q5R5S5T5U5V5W5X5Y5Z5a5b5c5d5e5f5g5h5i5j5k5l5m5n5o5p5q" "5r5s5t5u5v5w5x5y5z505152535455565758595+5/" "6A6B6C6D6E6F6G6H6I6J6K6L6M6N6O6P6Q6R6S6T6U6V6W6X6Y6Z6a6b6c6d6e6f6g6h6i6j6k6l6m6n6o6p6q" "6r6s6t6u6v6w6x6y6z606162636465666768696+6/" "7A7B7C7D7E7F7G7H7I7J7K7L7M7N7O7P7Q7R7S7T7U7V7W7X7Y7Z7a7b7c7d7e7f7g7h7i7j7k7l7m7n7o7p7q" "7r7s7t7u7v7w7x7y7z707172737475767778797+7/" "8A8B8C8D8E8F8G8H8I8J8K8L8M8N8O8P8Q8R8S8T8U8V8W8X8Y8Z8a8b8c8d8e8f8g8h8i8j8k8l8m8n8o8p8q" "8r8s8t8u8v8w8x8y8z808182838485868788898+8/" "9A9B9C9D9E9F9G9H9I9J9K9L9M9N9O9P9Q9R9S9T9U9V9W9X9Y9Z9a9b9c9d9e9f9g9h9i9j9k9l9m9n9o9p9q" "9r9s9t9u9v9w9x9y9z909192939495969798999+9/" "+A+B+C+D+E+F+G+H+I+J+K+L+M+N+O+P+Q+R+S+T+U+V+W+X+Y+Z+a+b+c+d+e+f+g+h+i+j+k+l+m+n+o+p+" "q+r+s+t+u+v+w+x+y+z+0+1+2+3+4+5+6+7+8+9+++/" "/A/B/C/D/E/F/G/H/I/J/K/L/M/N/O/P/Q/R/S/T/U/V/W/X/Y/Z/a/b/c/d/e/f/g/h/i/j/k/l/m/n/o/p/" "q/r/s/t/u/v/w/x/y/z/0/1/2/3/4/5/6/7/8/9/+//"; static const uint16_t rbase64lut[] = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15872, 0, 0, 0, 16128, 13312, 13568, 13824, 14080, 14336, 14592, 14848, 15104, 15360, 15616, 0, 0, 0, 25344, 0, 0, 0, 0, 256, 512, 768, 1024, 1280, 1536, 1792, 2048, 2304, 2560, 2816, 3072, 3328, 3584, 3840, 4096, 4352, 4608, 4864, 5120, 5376, 5632, 5888, 6144, 6400, 0, 0, 0, 0, 0, 0, 6656, 6912, 7168, 7424, 7680, 7936, 8192, 8448, 8704, 8960, 9216, 9472, 9728, 9984, 10240, 10496, 10752, 11008, 11264, 11520, 11776, 12032, 12288, 12544, 12800, 13056, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15872, 0, 0, 0, 16128, 13312, 13568, 13824, 14080, 14336, 14592, 14848, 15104, 15360, 15616, 0, 0, 0, 25344, 0, 0, 0, 0, 256, 512, 768, 1024, 1280, 1536, 1792, 2048, 2304, 2560, 2816, 3072, 3328, 3584, 3840, 4096, 4352, 4608, 4864, 5120, 5376, 5632, 5888, 6144, 6400, 0, 0, 0, 0, 0, 0, 6656, 6912, 7168, 7424, 7680, 7936, 8192, 8448, 8704, 8960, 9216, 9472, 9728, 9984, 10240, 10496, 10752, 11008, 11264, 11520, 11776, 12032, 12288, 12544, 12800, 13056, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15872, 0, 0, 0, 16128, 13312, 13568, 13824, 14080, 14336, 14592, 14848, 15104, 15360, 15616, 0, 0, 0, 25344, 0, 0, 0, 0, 256, 512, 768, 1024, 1280, 1536, 1792, 2048, 2304, 2560, 2816, 3072, 3328, 3584, 3840, 4096, 4352, 4608, 4864, 5120, 5376, 5632, 5888, 6144, 6400, 0, 0, 0, 0, 0, 0, 6656, 6912, 7168, 7424, 7680, 7936, 8192, 8448, 8704, 8960, 9216, 9472, 9728, 9984, 10240, 10496, 10752, 11008, 11264, 11520, 11776, 12032, 12288, 12544, 12800, 13056, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15872, 0, 0, 0, 16128, 13312, 13568, 13824, 14080, 14336, 14592, 14848, 15104, 15360, 15616, 0, 0, 0, 25344, 0, 0, 0, 0, 256, 512, 768, 1024, 1280, 1536, 1792, 2048, 2304, 2560, 2816, 3072, 3328, 3584, 3840, 4096, 4352, 4608, 4864, 5120, 5376, 5632, 5888, 6144, 6400, 0, 0, 0, 0, 0, 0, 6656, 6912, 7168, 7424, 7680, 7936, 8192, 8448, 8704, 8960, 9216, 9472, 9728, 9984, 10240, 10496, 10752, 11008, 11264, 11520, 11776, 12032, 12288, 12544, 12800, 13056, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15872, 0, 0, 0, 16128, 13312, 13568, 13824, 14080, 14336, 14592, 14848, 15104, 15360, 15616, 0, 0, 0, 25344, 0, 0, 0, 0, 256, 512, 768, 1024, 1280, 1536, 1792, 2048, 2304, 2560, 2816, 3072, 3328, 3584, 3840, 4096, 4352, 4608, 4864, 5120, 5376, 5632, 5888, 6144, 6400, 0, 0, 0, 0, 0, 0, 6656, 6912, 7168, 7424, 7680, 7936, 8192, 8448, 8704, 8960, 9216, 9472, 9728, 9984, 10240, 10496, 10752, 11008, 11264, 11520, 11776, 12032, 12288, 12544, 12800, 13056, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15872, 0, 0, 0, 16128, 13312, 13568, 13824, 14080, 14336, 14592, 14848, 15104, 15360, 15616, 0, 0, 0, 25344, 0, 0, 0, 0, 256, 512, 768, 1024, 1280, 1536, 1792, 2048, 2304, 2560, 2816, 3072, 3328, 3584, 3840, 4096, 4352, 4608, 4864, 5120, 5376, 5632, 5888, 6144, 6400, 0, 0, 0, 0, 0, 0, 6656, 6912, 7168, 7424, 7680, 7936, 8192, 8448, 8704, 8960, 9216, 9472, 9728, 9984, 10240, 10496, 10752, 11008, 11264, 11520, 11776, 12032, 12288, 12544, 12800, 13056, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15872, 0, 0, 0, 16128, 13312, 13568, 13824, 14080, 14336, 14592, 14848, 15104, 15360, 15616, 0, 0, 0, 25344, 0, 0, 0, 0, 256, 512, 768, 1024, 1280, 1536, 1792, 2048, 2304, 2560, 2816, 3072, 3328, 3584, 3840, 4096, 4352, 4608, 4864, 5120, 5376, 5632, 5888, 6144, 6400, 0, 0, 0, 0, 0, 0, 6656, 6912, 7168, 7424, 7680, 7936, 8192, 8448, 8704, 8960, 9216, 9472, 9728, 9984, 10240, 10496, 10752, 11008, 11264, 11520, 11776, 12032, 12288, 12544, 12800, 13056, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15872, 0, 0, 0, 16128, 13312, 13568, 13824, 14080, 14336, 14592, 14848, 15104, 15360, 15616, 0, 0, 0, 25344, 0, 0, 0, 0, 256, 512, 768, 1024, 1280, 1536, 1792, 2048, 2304, 2560, 2816, 3072, 3328, 3584, 3840, 4096, 4352, 4608, 4864, 5120, 5376, 5632, 5888, 6144, 6400, 0, 0, 0, 0, 0, 0, 6656, 6912, 7168, 7424, 7680, 7936, 8192, 8448, 8704, 8960, 9216, 9472, 9728, 9984, 10240, 10496, 10752, 11008, 11264, 11520, 11776, 12032, 12288, 12544, 12800, 13056, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15872, 0, 0, 0, 16128, 13312, 13568, 13824, 14080, 14336, 14592, 14848, 15104, 15360, 15616, 0, 0, 0, 25344, 0, 0, 0, 0, 256, 512, 768, 1024, 1280, 1536, 1792, 2048, 2304, 2560, 2816, 3072, 3328, 3584, 3840, 4096, 4352, 4608, 4864, 5120, 5376, 5632, 5888, 6144, 6400, 0, 0, 0, 0, 0, 0, 6656, 6912, 7168, 7424, 7680, 7936, 8192, 8448, 8704, 8960, 9216, 9472, 9728, 9984, 10240, 10496, 10752, 11008, 11264, 11520, 11776, 12032, 12288, 12544, 12800, 13056, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15872, 0, 0, 0, 16128, 13312, 13568, 13824, 14080, 14336, 14592, 14848, 15104, 15360, 15616, 0, 0, 0, 25344, 0, 0, 0, 0, 256, 512, 768, 1024, 1280, 1536, 1792, 2048, 2304, 2560, 2816, 3072, 3328, 3584, 3840, 4096, 4352, 4608, 4864, 5120, 5376, 5632, 5888, 6144, 6400, 0, 0, 0, 0, 0, 0, 6656, 6912, 7168, 7424, 7680, 7936, 8192, 8448, 8704, 8960, 9216, 9472, 9728, 9984, 10240, 10496, 10752, 11008, 11264, 11520, 11776, 12032, 12288, 12544, 12800, 13056, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15872, 0, 0, 0, 16128, 13312, 13568, 13824, 14080, 14336, 14592, 14848, 15104, 15360, 15616, 0, 0, 0, 25344, 0, 0, 0, 0, 256, 512, 768, 1024, 1280, 1536, 1792, 2048, 2304, 2560, 2816, 3072, 3328, 3584, 3840, 4096, 4352, 4608, 4864, 5120, 5376, 5632, 5888, 6144, 6400, 0, 0, 0, 0, 0, 0, 6656, 6912, 7168, 7424, 7680, 7936, 8192, 8448, 8704, 8960, 9216, 9472, 9728, 9984, 10240, 10496, 10752, 11008, 11264, 11520, 11776, 12032, 12288, 12544, 12800, 13056, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15872, 0, 0, 0, 16128, 13312, 13568, 13824, 14080, 14336, 14592, 14848, 15104, 15360, 15616, 0, 0, 0, 25344, 0, 0, 0, 0, 256, 512, 768, 1024, 1280, 1536, 1792, 2048, 2304, 2560, 2816, 3072, 3328, 3584, 3840, 4096, 4352, 4608, 4864, 5120, 5376, 5632, 5888, 6144, 6400, 0, 0, 0, 0, 0, 0, 6656, 6912, 7168, 7424, 7680, 7936, 8192, 8448, 8704, 8960, 9216, 9472, 9728, 9984, 10240, 10496, 10752, 11008, 11264, 11520, 11776, 12032, 12288, 12544, 12800, 13056, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15872, 0, 0, 0, 16128, 13312, 13568, 13824, 14080, 14336, 14592, 14848, 15104, 15360, 15616, 0, 0, 0, 25344, 0, 0, 0, 0, 256, 512, 768, 1024, 1280, 1536, 1792, 2048, 2304, 2560, 2816, 3072, 3328, 3584, 3840, 4096, 4352, 4608, 4864, 5120, 5376, 5632, 5888, 6144, 6400, 0, 0, 0, 0, 0, 0, 6656, 6912, 7168, 7424, 7680, 7936, 8192, 8448, 8704, 8960, 9216, 9472, 9728, 9984, 10240, 10496, 10752, 11008, 11264, 11520, 11776, 12032, 12288, 12544, 12800, 13056, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15872, 0, 0, 0, 16128, 13312, 13568, 13824, 14080, 14336, 14592, 14848, 15104, 15360, 15616, 0, 0, 0, 25344, 0, 0, 0, 0, 256, 512, 768, 1024, 1280, 1536, 1792, 2048, 2304, 2560, 2816, 3072, 3328, 3584, 3840, 4096, 4352, 4608, 4864, 5120, 5376, 5632, 5888, 6144, 6400, 0, 0, 0, 0, 0, 0, 6656, 6912, 7168, 7424, 7680, 7936, 8192, 8448, 8704, 8960, 9216, 9472, 9728, 9984, 10240, 10496, 10752, 11008, 11264, 11520, 11776, 12032, 12288, 12544, 12800, 13056, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15872, 0, 0, 0, 16128, 13312, 13568, 13824, 14080, 14336, 14592, 14848, 15104, 15360, 15616, 0, 0, 0, 25344, 0, 0, 0, 0, 256, 512, 768, 1024, 1280, 1536, 1792, 2048, 2304, 2560, 2816, 3072, 3328, 3584, 3840, 4096, 4352, 4608, 4864, 5120, 5376, 5632, 5888, 6144, 6400, 0, 0, 0, 0, 0, 0, 6656, 6912, 7168, 7424, 7680, 7936, 8192, 8448, 8704, 8960, 9216, 9472, 9728, 9984, 10240, 10496, 10752, 11008, 11264, 11520, 11776, 12032, 12288, 12544, 12800, 13056, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15872, 0, 0, 0, 16128, 13312, 13568, 13824, 14080, 14336, 14592, 14848, 15104, 15360, 15616, 0, 0, 0, 25344, 0, 0, 0, 0, 256, 512, 768, 1024, 1280, 1536, 1792, 2048, 2304, 2560, 2816, 3072, 3328, 3584, 3840, 4096, 4352, 4608, 4864, 5120, 5376, 5632, 5888, 6144, 6400, 0, 0, 0, 0, 0, 0, 6656, 6912, 7168, 7424, 7680, 7936, 8192, 8448, 8704, 8960, 9216, 9472, 9728, 9984, 10240, 10496, 10752, 11008, 11264, 11520, 11776, 12032, 12288, 12544, 12800, 13056, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15872, 0, 0, 0, 16128, 13312, 13568, 13824, 14080, 14336, 14592, 14848, 15104, 15360, 15616, 0, 0, 0, 25344, 0, 0, 0, 0, 256, 512, 768, 1024, 1280, 1536, 1792, 2048, 2304, 2560, 2816, 3072, 3328, 3584, 3840, 4096, 4352, 4608, 4864, 5120, 5376, 5632, 5888, 6144, 6400, 0, 0, 0, 0, 0, 0, 6656, 6912, 7168, 7424, 7680, 7936, 8192, 8448, 8704, 8960, 9216, 9472, 9728, 9984, 10240, 10496, 10752, 11008, 11264, 11520, 11776, 12032, 12288, 12544, 12800, 13056, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15872, 0, 0, 0, 16128, 13312, 13568, 13824, 14080, 14336, 14592, 14848, 15104, 15360, 15616, 0, 0, 0, 25344, 0, 0, 0, 0, 256, 512, 768, 1024, 1280, 1536, 1792, 2048, 2304, 2560, 2816, 3072, 3328, 3584, 3840, 4096, 4352, 4608, 4864, 5120, 5376, 5632, 5888, 6144, 6400, 0, 0, 0, 0, 0, 0, 6656, 6912, 7168, 7424, 7680, 7936, 8192, 8448, 8704, 8960, 9216, 9472, 9728, 9984, 10240, 10496, 10752, 11008, 11264, 11520, 11776, 12032, 12288, 12544, 12800, 13056, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15872, 0, 0, 0, 16128, 13312, 13568, 13824, 14080, 14336, 14592, 14848, 15104, 15360, 15616, 0, 0, 0, 25344, 0, 0, 0, 0, 256, 512, 768, 1024, 1280, 1536, 1792, 2048, 2304, 2560, 2816, 3072, 3328, 3584, 3840, 4096, 4352, 4608, 4864, 5120, 5376, 5632, 5888, 6144, 6400, 0, 0, 0, 0, 0, 0, 6656, 6912, 7168, 7424, 7680, 7936, 8192, 8448, 8704, 8960, 9216, 9472, 9728, 9984, 10240, 10496, 10752, 11008, 11264, 11520, 11776, 12032, 12288, 12544, 12800, 13056, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15872, 0, 0, 0, 16128, 13312, 13568, 13824, 14080, 14336, 14592, 14848, 15104, 15360, 15616, 0, 0, 0, 25344, 0, 0, 0, 0, 256, 512, 768, 1024, 1280, 1536, 1792, 2048, 2304, 2560, 2816, 3072, 3328, 3584, 3840, 4096, 4352, 4608, 4864, 5120, 5376, 5632, 5888, 6144, 6400, 0, 0, 0, 0, 0, 0, 6656, 6912, 7168, 7424, 7680, 7936, 8192, 8448, 8704, 8960, 9216, 9472, 9728, 9984, 10240, 10496, 10752, 11008, 11264, 11520, 11776, 12032, 12288, 12544, 12800, 13056, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15872, 0, 0, 0, 16128, 13312, 13568, 13824, 14080, 14336, 14592, 14848, 15104, 15360, 15616, 0, 0, 0, 25344, 0, 0, 0, 0, 256, 512, 768, 1024, 1280, 1536, 1792, 2048, 2304, 2560, 2816, 3072, 3328, 3584, 3840, 4096, 4352, 4608, 4864, 5120, 5376, 5632, 5888, 6144, 6400, 0, 0, 0, 0, 0, 0, 6656, 6912, 7168, 7424, 7680, 7936, 8192, 8448, 8704, 8960, 9216, 9472, 9728, 9984, 10240, 10496, 10752, 11008, 11264, 11520, 11776, 12032, 12288, 12544, 12800, 13056, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15872, 0, 0, 0, 16128, 13312, 13568, 13824, 14080, 14336, 14592, 14848, 15104, 15360, 15616, 0, 0, 0, 25344, 0, 0, 0, 0, 256, 512, 768, 1024, 1280, 1536, 1792, 2048, 2304, 2560, 2816, 3072, 3328, 3584, 3840, 4096, 4352, 4608, 4864, 5120, 5376, 5632, 5888, 6144, 6400, 0, 0, 0, 0, 0, 0, 6656, 6912, 7168, 7424, 7680, 7936, 8192, 8448, 8704, 8960, 9216, 9472, 9728, 9984, 10240, 10496, 10752, 11008, 11264, 11520, 11776, 12032, 12288, 12544, 12800, 13056, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15872, 0, 0, 0, 16128, 13312, 13568, 13824, 14080, 14336, 14592, 14848, 15104, 15360, 15616, 0, 0, 0, 25344, 0, 0, 0, 0, 256, 512, 768, 1024, 1280, 1536, 1792, 2048, 2304, 2560, 2816, 3072, 3328, 3584, 3840, 4096, 4352, 4608, 4864, 5120, 5376, 5632, 5888, 6144, 6400, 0, 0, 0, 0, 0, 0, 6656, 6912, 7168, 7424, 7680, 7936, 8192, 8448, 8704, 8960, 9216, 9472, 9728, 9984, 10240, 10496, 10752, 11008, 11264, 11520, 11776, 12032, 12288, 12544, 12800, 13056, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15872, 0, 0, 0, 16128, 13312, 13568, 13824, 14080, 14336, 14592, 14848, 15104, 15360, 15616, 0, 0, 0, 25344, 0, 0, 0, 0, 256, 512, 768, 1024, 1280, 1536, 1792, 2048, 2304, 2560, 2816, 3072, 3328, 3584, 3840, 4096, 4352, 4608, 4864, 5120, 5376, 5632, 5888, 6144, 6400, 0, 0, 0, 0, 0, 0, 6656, 6912, 7168, 7424, 7680, 7936, 8192, 8448, 8704, 8960, 9216, 9472, 9728, 9984, 10240, 10496, 10752, 11008, 11264, 11520, 11776, 12032, 12288, 12544, 12800, 13056, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15872, 0, 0, 0, 16128, 13312, 13568, 13824, 14080, 14336, 14592, 14848, 15104, 15360, 15616, 0, 0, 0, 25344, 0, 0, 0, 0, 256, 512, 768, 1024, 1280, 1536, 1792, 2048, 2304, 2560, 2816, 3072, 3328, 3584, 3840, 4096, 4352, 4608, 4864, 5120, 5376, 5632, 5888, 6144, 6400, 0, 0, 0, 0, 0, 0, 6656, 6912, 7168, 7424, 7680, 7936, 8192, 8448, 8704, 8960, 9216, 9472, 9728, 9984, 10240, 10496, 10752, 11008, 11264, 11520, 11776, 12032, 12288, 12544, 12800, 13056, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15872, 0, 0, 0, 16128, 13312, 13568, 13824, 14080, 14336, 14592, 14848, 15104, 15360, 15616, 0, 0, 0, 25344, 0, 0, 0, 0, 256, 512, 768, 1024, 1280, 1536, 1792, 2048, 2304, 2560, 2816, 3072, 3328, 3584, 3840, 4096, 4352, 4608, 4864, 5120, 5376, 5632, 5888, 6144, 6400, 0, 0, 0, 0, 0, 0, 6656, 6912, 7168, 7424, 7680, 7936, 8192, 8448, 8704, 8960, 9216, 9472, 9728, 9984, 10240, 10496, 10752, 11008, 11264, 11520, 11776, 12032, 12288, 12544, 12800, 13056, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15872, 0, 0, 0, 16128, 13312, 13568, 13824, 14080, 14336, 14592, 14848, 15104, 15360, 15616, 0, 0, 0, 25344, 0, 0, 0, 0, 256, 512, 768, 1024, 1280, 1536, 1792, 2048, 2304, 2560, 2816, 3072, 3328, 3584, 3840, 4096, 4352, 4608, 4864, 5120, 5376, 5632, 5888, 6144, 6400, 0, 0, 0, 0, 0, 0, 6656, 6912, 7168, 7424, 7680, 7936, 8192, 8448, 8704, 8960, 9216, 9472, 9728, 9984, 10240, 10496, 10752, 11008, 11264, 11520, 11776, 12032, 12288, 12544, 12800, 13056, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15872, 0, 0, 0, 16128, 13312, 13568, 13824, 14080, 14336, 14592, 14848, 15104, 15360, 15616, 0, 0, 0, 25344, 0, 0, 0, 0, 256, 512, 768, 1024, 1280, 1536, 1792, 2048, 2304, 2560, 2816, 3072, 3328, 3584, 3840, 4096, 4352, 4608, 4864, 5120, 5376, 5632, 5888, 6144, 6400, 0, 0, 0, 0, 0, 0, 6656, 6912, 7168, 7424, 7680, 7936, 8192, 8448, 8704, 8960, 9216, 9472, 9728, 9984, 10240, 10496, 10752, 11008, 11264, 11520, 11776, 12032, 12288, 12544, 12800, 13056, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15872, 0, 0, 0, 16128, 13312, 13568, 13824, 14080, 14336, 14592, 14848, 15104, 15360, 15616, 0, 0, 0, 25344, 0, 0, 0, 0, 256, 512, 768, 1024, 1280, 1536, 1792, 2048, 2304, 2560, 2816, 3072, 3328, 3584, 3840, 4096, 4352, 4608, 4864, 5120, 5376, 5632, 5888, 6144, 6400, 0, 0, 0, 0, 0, 0, 6656, 6912, 7168, 7424, 7680, 7936, 8192, 8448, 8704, 8960, 9216, 9472, 9728, 9984, 10240, 10496, 10752, 11008, 11264, 11520, 11776, 12032, 12288, 12544, 12800, 13056, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15872, 0, 0, 0, 16128, 13312, 13568, 13824, 14080, 14336, 14592, 14848, 15104, 15360, 15616, 0, 0, 0, 25344, 0, 0, 0, 0, 256, 512, 768, 1024, 1280, 1536, 1792, 2048, 2304, 2560, 2816, 3072, 3328, 3584, 3840, 4096, 4352, 4608, 4864, 5120, 5376, 5632, 5888, 6144, 6400, 0, 0, 0, 0, 0, 0, 6656, 6912, 7168, 7424, 7680, 7936, 8192, 8448, 8704, 8960, 9216, 9472, 9728, 9984, 10240, 10496, 10752, 11008, 11264, 11520, 11776, 12032, 12288, 12544, 12800, 13056, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15872, 0, 0, 0, 16128, 13312, 13568, 13824, 14080, 14336, 14592, 14848, 15104, 15360, 15616, 0, 0, 0, 25344, 0, 0, 0, 0, 256, 512, 768, 1024, 1280, 1536, 1792, 2048, 2304, 2560, 2816, 3072, 3328, 3584, 3840, 4096, 4352, 4608, 4864, 5120, 5376, 5632, 5888, 6144, 6400, 0, 0, 0, 0, 0, 0, 6656, 6912, 7168, 7424, 7680, 7936, 8192, 8448, 8704, 8960, 9216, 9472, 9728, 9984, 10240, 10496, 10752, 11008, 11264, 11520, 11776, 12032, 12288, 12544, 12800, 13056, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15872, 0, 0, 0, 16128, 13312, 13568, 13824, 14080, 14336, 14592, 14848, 15104, 15360, 15616, 0, 0, 0, 25344, 0, 0, 0, 0, 256, 512, 768, 1024, 1280, 1536, 1792, 2048, 2304, 2560, 2816, 3072, 3328, 3584, 3840, 4096, 4352, 4608, 4864, 5120, 5376, 5632, 5888, 6144, 6400, 0, 0, 0, 0, 0, 0, 6656, 6912, 7168, 7424, 7680, 7936, 8192, 8448, 8704, 8960, 9216, 9472, 9728, 9984, 10240, 10496, 10752, 11008, 11264, 11520, 11776, 12032, 12288, 12544, 12800, 13056, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15872, 0, 0, 0, 16128, 13312, 13568, 13824, 14080, 14336, 14592, 14848, 15104, 15360, 15616, 0, 0, 0, 25344, 0, 0, 0, 0, 256, 512, 768, 1024, 1280, 1536, 1792, 2048, 2304, 2560, 2816, 3072, 3328, 3584, 3840, 4096, 4352, 4608, 4864, 5120, 5376, 5632, 5888, 6144, 6400, 0, 0, 0, 0, 0, 0, 6656, 6912, 7168, 7424, 7680, 7936, 8192, 8448, 8704, 8960, 9216, 9472, 9728, 9984, 10240, 10496, 10752, 11008, 11264, 11520, 11776, 12032, 12288, 12544, 12800, 13056, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15872, 0, 0, 0, 16128, 13312, 13568, 13824, 14080, 14336, 14592, 14848, 15104, 15360, 15616, 0, 0, 0, 25344, 0, 0, 0, 0, 256, 512, 768, 1024, 1280, 1536, 1792, 2048, 2304, 2560, 2816, 3072, 3328, 3584, 3840, 4096, 4352, 4608, 4864, 5120, 5376, 5632, 5888, 6144, 6400, 0, 0, 0, 0, 0, 0, 6656, 6912, 7168, 7424, 7680, 7936, 8192, 8448, 8704, 8960, 9216, 9472, 9728, 9984, 10240, 10496, 10752, 11008, 11264, 11520, 11776, 12032, 12288, 12544, 12800, 13056, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15872, 0, 0, 0, 16128, 13312, 13568, 13824, 14080, 14336, 14592, 14848, 15104, 15360, 15616, 0, 0, 0, 25344, 0, 0, 0, 0, 256, 512, 768, 1024, 1280, 1536, 1792, 2048, 2304, 2560, 2816, 3072, 3328, 3584, 3840, 4096, 4352, 4608, 4864, 5120, 5376, 5632, 5888, 6144, 6400, 0, 0, 0, 0, 0, 0, 6656, 6912, 7168, 7424, 7680, 7936, 8192, 8448, 8704, 8960, 9216, 9472, 9728, 9984, 10240, 10496, 10752, 11008, 11264, 11520, 11776, 12032, 12288, 12544, 12800, 13056, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15872, 0, 0, 0, 16128, 13312, 13568, 13824, 14080, 14336, 14592, 14848, 15104, 15360, 15616, 0, 0, 0, 25344, 0, 0, 0, 0, 256, 512, 768, 1024, 1280, 1536, 1792, 2048, 2304, 2560, 2816, 3072, 3328, 3584, 3840, 4096, 4352, 4608, 4864, 5120, 5376, 5632, 5888, 6144, 6400, 0, 0, 0, 0, 0, 0, 6656, 6912, 7168, 7424, 7680, 7936, 8192, 8448, 8704, 8960, 9216, 9472, 9728, 9984, 10240, 10496, 10752, 11008, 11264, 11520, 11776, 12032, 12288, 12544, 12800, 13056, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15872, 0, 0, 0, 16128, 13312, 13568, 13824, 14080, 14336, 14592, 14848, 15104, 15360, 15616, 0, 0, 0, 25344, 0, 0, 0, 0, 256, 512, 768, 1024, 1280, 1536, 1792, 2048, 2304, 2560, 2816, 3072, 3328, 3584, 3840, 4096, 4352, 4608, 4864, 5120, 5376, 5632, 5888, 6144, 6400, 0, 0, 0, 0, 0, 0, 6656, 6912, 7168, 7424, 7680, 7936, 8192, 8448, 8704, 8960, 9216, 9472, 9728, 9984, 10240, 10496, 10752, 11008, 11264, 11520, 11776, 12032, 12288, 12544, 12800, 13056, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15872, 0, 0, 0, 16128, 13312, 13568, 13824, 14080, 14336, 14592, 14848, 15104, 15360, 15616, 0, 0, 0, 25344, 0, 0, 0, 0, 256, 512, 768, 1024, 1280, 1536, 1792, 2048, 2304, 2560, 2816, 3072, 3328, 3584, 3840, 4096, 4352, 4608, 4864, 5120, 5376, 5632, 5888, 6144, 6400, 0, 0, 0, 0, 0, 0, 6656, 6912, 7168, 7424, 7680, 7936, 8192, 8448, 8704, 8960, 9216, 9472, 9728, 9984, 10240, 10496, 10752, 11008, 11264, 11520, 11776, 12032, 12288, 12544, 12800, 13056, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15872, 0, 0, 0, 16128, 13312, 13568, 13824, 14080, 14336, 14592, 14848, 15104, 15360, 15616, 0, 0, 0, 25344, 0, 0, 0, 0, 256, 512, 768, 1024, 1280, 1536, 1792, 2048, 2304, 2560, 2816, 3072, 3328, 3584, 3840, 4096, 4352, 4608, 4864, 5120, 5376, 5632, 5888, 6144, 6400, 0, 0, 0, 0, 0, 0, 6656, 6912, 7168, 7424, 7680, 7936, 8192, 8448, 8704, 8960, 9216, 9472, 9728, 9984, 10240, 10496, 10752, 11008, 11264, 11520, 11776, 12032, 12288, 12544, 12800, 13056, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15872, 0, 0, 0, 16128, 13312, 13568, 13824, 14080, 14336, 14592, 14848, 15104, 15360, 15616, 0, 0, 0, 25344, 0, 0, 0, 0, 256, 512, 768, 1024, 1280, 1536, 1792, 2048, 2304, 2560, 2816, 3072, 3328, 3584, 3840, 4096, 4352, 4608, 4864, 5120, 5376, 5632, 5888, 6144, 6400, 0, 0, 0, 0, 0, 0, 6656, 6912, 7168, 7424, 7680, 7936, 8192, 8448, 8704, 8960, 9216, 9472, 9728, 9984, 10240, 10496, 10752, 11008, 11264, 11520, 11776, 12032, 12288, 12544, 12800, 13056, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15872, 0, 0, 0, 16128, 13312, 13568, 13824, 14080, 14336, 14592, 14848, 15104, 15360, 15616, 0, 0, 0, 25344, 0, 0, 0, 0, 256, 512, 768, 1024, 1280, 1536, 1792, 2048, 2304, 2560, 2816, 3072, 3328, 3584, 3840, 4096, 4352, 4608, 4864, 5120, 5376, 5632, 5888, 6144, 6400, 0, 0, 0, 0, 0, 0, 6656, 6912, 7168, 7424, 7680, 7936, 8192, 8448, 8704, 8960, 9216, 9472, 9728, 9984, 10240, 10496, 10752, 11008, 11264, 11520, 11776, 12032, 12288, 12544, 12800, 13056, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15872, 0, 0, 0, 16128, 13312, 13568, 13824, 14080, 14336, 14592, 14848, 15104, 15360, 15616, 0, 0, 0, 25344, 0, 0, 0, 0, 256, 512, 768, 1024, 1280, 1536, 1792, 2048, 2304, 2560, 2816, 3072, 3328, 3584, 3840, 4096, 4352, 4608, 4864, 5120, 5376, 5632, 5888, 6144, 6400, 0, 0, 0, 0, 0, 0, 6656, 6912, 7168, 7424, 7680, 7936, 8192, 8448, 8704, 8960, 9216, 9472, 9728, 9984, 10240, 10496, 10752, 11008, 11264, 11520, 11776, 12032, 12288, 12544, 12800, 13056, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15872, 0, 0, 0, 16128, 13312, 13568, 13824, 14080, 14336, 14592, 14848, 15104, 15360, 15616, 0, 0, 0, 25344, 0, 0, 0, 0, 256, 512, 768, 1024, 1280, 1536, 1792, 2048, 2304, 2560, 2816, 3072, 3328, 3584, 3840, 4096, 4352, 4608, 4864, 5120, 5376, 5632, 5888, 6144, 6400, 0, 0, 0, 0, 0, 0, 6656, 6912, 7168, 7424, 7680, 7936, 8192, 8448, 8704, 8960, 9216, 9472, 9728, 9984, 10240, 10496, 10752, 11008, 11264, 11520, 11776, 12032, 12288, 12544, 12800, 13056, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 16120, 248, 248, 248, 16376, 13560, 13816, 14072, 14328, 14584, 14840, 15096, 15352, 15608, 15864, 248, 248, 248, 25592, 248, 248, 248, 248, 504, 760, 1016, 1272, 1528, 1784, 2040, 2296, 2552, 2808, 3064, 3320, 3576, 3832, 4088, 4344, 4600, 4856, 5112, 5368, 5624, 5880, 6136, 6392, 6648, 248, 248, 248, 248, 248, 248, 6904, 7160, 7416, 7672, 7928, 8184, 8440, 8696, 8952, 9208, 9464, 9720, 9976, 10232, 10488, 10744, 11000, 11256, 11512, 11768, 12024, 12280, 12536, 12792, 13048, 13304, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15872, 0, 0, 0, 16128, 13312, 13568, 13824, 14080, 14336, 14592, 14848, 15104, 15360, 15616, 0, 0, 0, 25344, 0, 0, 0, 0, 256, 512, 768, 1024, 1280, 1536, 1792, 2048, 2304, 2560, 2816, 3072, 3328, 3584, 3840, 4096, 4352, 4608, 4864, 5120, 5376, 5632, 5888, 6144, 6400, 0, 0, 0, 0, 0, 0, 6656, 6912, 7168, 7424, 7680, 7936, 8192, 8448, 8704, 8960, 9216, 9472, 9728, 9984, 10240, 10496, 10752, 11008, 11264, 11520, 11776, 12032, 12288, 12544, 12800, 13056, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15872, 0, 0, 0, 16128, 13312, 13568, 13824, 14080, 14336, 14592, 14848, 15104, 15360, 15616, 0, 0, 0, 25344, 0, 0, 0, 0, 256, 512, 768, 1024, 1280, 1536, 1792, 2048, 2304, 2560, 2816, 3072, 3328, 3584, 3840, 4096, 4352, 4608, 4864, 5120, 5376, 5632, 5888, 6144, 6400, 0, 0, 0, 0, 0, 0, 6656, 6912, 7168, 7424, 7680, 7936, 8192, 8448, 8704, 8960, 9216, 9472, 9728, 9984, 10240, 10496, 10752, 11008, 11264, 11520, 11776, 12032, 12288, 12544, 12800, 13056, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15872, 0, 0, 0, 16128, 13312, 13568, 13824, 14080, 14336, 14592, 14848, 15104, 15360, 15616, 0, 0, 0, 25344, 0, 0, 0, 0, 256, 512, 768, 1024, 1280, 1536, 1792, 2048, 2304, 2560, 2816, 3072, 3328, 3584, 3840, 4096, 4352, 4608, 4864, 5120, 5376, 5632, 5888, 6144, 6400, 0, 0, 0, 0, 0, 0, 6656, 6912, 7168, 7424, 7680, 7936, 8192, 8448, 8704, 8960, 9216, 9472, 9728, 9984, 10240, 10496, 10752, 11008, 11264, 11520, 11776, 12032, 12288, 12544, 12800, 13056, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 252, 252, 252, 252, 252, 252, 252, 252, 252, 252, 252, 252, 252, 252, 252, 252, 252, 252, 252, 252, 252, 252, 252, 252, 252, 252, 252, 252, 252, 252, 252, 252, 252, 252, 252, 252, 252, 252, 252, 252, 252, 252, 252, 16124, 252, 252, 252, 16380, 13564, 13820, 14076, 14332, 14588, 14844, 15100, 15356, 15612, 15868, 252, 252, 252, 25596, 252, 252, 252, 252, 508, 764, 1020, 1276, 1532, 1788, 2044, 2300, 2556, 2812, 3068, 3324, 3580, 3836, 4092, 4348, 4604, 4860, 5116, 5372, 5628, 5884, 6140, 6396, 6652, 252, 252, 252, 252, 252, 252, 6908, 7164, 7420, 7676, 7932, 8188, 8444, 8700, 8956, 9212, 9468, 9724, 9980, 10236, 10492, 10748, 11004, 11260, 11516, 11772, 12028, 12284, 12540, 12796, 13052, 13308, 252, 252, 252, 252, 252, 252, 252, 252, 252, 252, 252, 252, 252, 252, 252, 252, 252, 252, 252, 252, 252, 252, 252, 252, 252, 252, 252, 252, 252, 252, 252, 252, 252, 252, 252, 252, 252, 252, 252, 252, 252, 252, 252, 252, 252, 252, 252, 252, 252, 252, 252, 252, 252, 252, 252, 252, 252, 252, 252, 252, 252, 252, 252, 252, 252, 252, 252, 252, 252, 252, 252, 252, 252, 252, 252, 252, 252, 252, 252, 252, 252, 252, 252, 252, 252, 252, 252, 252, 252, 252, 252, 252, 252, 252, 252, 252, 252, 252, 252, 252, 252, 252, 252, 252, 252, 252, 252, 252, 252, 252, 252, 252, 252, 252, 252, 252, 252, 252, 252, 252, 252, 252, 252, 252, 252, 252, 252, 252, 252, 252, 252, 252, 252, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 16080, 208, 208, 208, 16336, 13520, 13776, 14032, 14288, 14544, 14800, 15056, 15312, 15568, 15824, 208, 208, 208, 25552, 208, 208, 208, 208, 464, 720, 976, 1232, 1488, 1744, 2000, 2256, 2512, 2768, 3024, 3280, 3536, 3792, 4048, 4304, 4560, 4816, 5072, 5328, 5584, 5840, 6096, 6352, 6608, 208, 208, 208, 208, 208, 208, 6864, 7120, 7376, 7632, 7888, 8144, 8400, 8656, 8912, 9168, 9424, 9680, 9936, 10192, 10448, 10704, 10960, 11216, 11472, 11728, 11984, 12240, 12496, 12752, 13008, 13264, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 212, 212, 212, 212, 212, 212, 212, 212, 212, 212, 212, 212, 212, 212, 212, 212, 212, 212, 212, 212, 212, 212, 212, 212, 212, 212, 212, 212, 212, 212, 212, 212, 212, 212, 212, 212, 212, 212, 212, 212, 212, 212, 212, 16084, 212, 212, 212, 16340, 13524, 13780, 14036, 14292, 14548, 14804, 15060, 15316, 15572, 15828, 212, 212, 212, 25556, 212, 212, 212, 212, 468, 724, 980, 1236, 1492, 1748, 2004, 2260, 2516, 2772, 3028, 3284, 3540, 3796, 4052, 4308, 4564, 4820, 5076, 5332, 5588, 5844, 6100, 6356, 6612, 212, 212, 212, 212, 212, 212, 6868, 7124, 7380, 7636, 7892, 8148, 8404, 8660, 8916, 9172, 9428, 9684, 9940, 10196, 10452, 10708, 10964, 11220, 11476, 11732, 11988, 12244, 12500, 12756, 13012, 13268, 212, 212, 212, 212, 212, 212, 212, 212, 212, 212, 212, 212, 212, 212, 212, 212, 212, 212, 212, 212, 212, 212, 212, 212, 212, 212, 212, 212, 212, 212, 212, 212, 212, 212, 212, 212, 212, 212, 212, 212, 212, 212, 212, 212, 212, 212, 212, 212, 212, 212, 212, 212, 212, 212, 212, 212, 212, 212, 212, 212, 212, 212, 212, 212, 212, 212, 212, 212, 212, 212, 212, 212, 212, 212, 212, 212, 212, 212, 212, 212, 212, 212, 212, 212, 212, 212, 212, 212, 212, 212, 212, 212, 212, 212, 212, 212, 212, 212, 212, 212, 212, 212, 212, 212, 212, 212, 212, 212, 212, 212, 212, 212, 212, 212, 212, 212, 212, 212, 212, 212, 212, 212, 212, 212, 212, 212, 212, 212, 212, 212, 212, 212, 212, 216, 216, 216, 216, 216, 216, 216, 216, 216, 216, 216, 216, 216, 216, 216, 216, 216, 216, 216, 216, 216, 216, 216, 216, 216, 216, 216, 216, 216, 216, 216, 216, 216, 216, 216, 216, 216, 216, 216, 216, 216, 216, 216, 16088, 216, 216, 216, 16344, 13528, 13784, 14040, 14296, 14552, 14808, 15064, 15320, 15576, 15832, 216, 216, 216, 25560, 216, 216, 216, 216, 472, 728, 984, 1240, 1496, 1752, 2008, 2264, 2520, 2776, 3032, 3288, 3544, 3800, 4056, 4312, 4568, 4824, 5080, 5336, 5592, 5848, 6104, 6360, 6616, 216, 216, 216, 216, 216, 216, 6872, 7128, 7384, 7640, 7896, 8152, 8408, 8664, 8920, 9176, 9432, 9688, 9944, 10200, 10456, 10712, 10968, 11224, 11480, 11736, 11992, 12248, 12504, 12760, 13016, 13272, 216, 216, 216, 216, 216, 216, 216, 216, 216, 216, 216, 216, 216, 216, 216, 216, 216, 216, 216, 216, 216, 216, 216, 216, 216, 216, 216, 216, 216, 216, 216, 216, 216, 216, 216, 216, 216, 216, 216, 216, 216, 216, 216, 216, 216, 216, 216, 216, 216, 216, 216, 216, 216, 216, 216, 216, 216, 216, 216, 216, 216, 216, 216, 216, 216, 216, 216, 216, 216, 216, 216, 216, 216, 216, 216, 216, 216, 216, 216, 216, 216, 216, 216, 216, 216, 216, 216, 216, 216, 216, 216, 216, 216, 216, 216, 216, 216, 216, 216, 216, 216, 216, 216, 216, 216, 216, 216, 216, 216, 216, 216, 216, 216, 216, 216, 216, 216, 216, 216, 216, 216, 216, 216, 216, 216, 216, 216, 216, 216, 216, 216, 216, 216, 220, 220, 220, 220, 220, 220, 220, 220, 220, 220, 220, 220, 220, 220, 220, 220, 220, 220, 220, 220, 220, 220, 220, 220, 220, 220, 220, 220, 220, 220, 220, 220, 220, 220, 220, 220, 220, 220, 220, 220, 220, 220, 220, 16092, 220, 220, 220, 16348, 13532, 13788, 14044, 14300, 14556, 14812, 15068, 15324, 15580, 15836, 220, 220, 220, 25564, 220, 220, 220, 220, 476, 732, 988, 1244, 1500, 1756, 2012, 2268, 2524, 2780, 3036, 3292, 3548, 3804, 4060, 4316, 4572, 4828, 5084, 5340, 5596, 5852, 6108, 6364, 6620, 220, 220, 220, 220, 220, 220, 6876, 7132, 7388, 7644, 7900, 8156, 8412, 8668, 8924, 9180, 9436, 9692, 9948, 10204, 10460, 10716, 10972, 11228, 11484, 11740, 11996, 12252, 12508, 12764, 13020, 13276, 220, 220, 220, 220, 220, 220, 220, 220, 220, 220, 220, 220, 220, 220, 220, 220, 220, 220, 220, 220, 220, 220, 220, 220, 220, 220, 220, 220, 220, 220, 220, 220, 220, 220, 220, 220, 220, 220, 220, 220, 220, 220, 220, 220, 220, 220, 220, 220, 220, 220, 220, 220, 220, 220, 220, 220, 220, 220, 220, 220, 220, 220, 220, 220, 220, 220, 220, 220, 220, 220, 220, 220, 220, 220, 220, 220, 220, 220, 220, 220, 220, 220, 220, 220, 220, 220, 220, 220, 220, 220, 220, 220, 220, 220, 220, 220, 220, 220, 220, 220, 220, 220, 220, 220, 220, 220, 220, 220, 220, 220, 220, 220, 220, 220, 220, 220, 220, 220, 220, 220, 220, 220, 220, 220, 220, 220, 220, 220, 220, 220, 220, 220, 220, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 16096, 224, 224, 224, 16352, 13536, 13792, 14048, 14304, 14560, 14816, 15072, 15328, 15584, 15840, 224, 224, 224, 25568, 224, 224, 224, 224, 480, 736, 992, 1248, 1504, 1760, 2016, 2272, 2528, 2784, 3040, 3296, 3552, 3808, 4064, 4320, 4576, 4832, 5088, 5344, 5600, 5856, 6112, 6368, 6624, 224, 224, 224, 224, 224, 224, 6880, 7136, 7392, 7648, 7904, 8160, 8416, 8672, 8928, 9184, 9440, 9696, 9952, 10208, 10464, 10720, 10976, 11232, 11488, 11744, 12000, 12256, 12512, 12768, 13024, 13280, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 228, 228, 228, 228, 228, 228, 228, 228, 228, 228, 228, 228, 228, 228, 228, 228, 228, 228, 228, 228, 228, 228, 228, 228, 228, 228, 228, 228, 228, 228, 228, 228, 228, 228, 228, 228, 228, 228, 228, 228, 228, 228, 228, 16100, 228, 228, 228, 16356, 13540, 13796, 14052, 14308, 14564, 14820, 15076, 15332, 15588, 15844, 228, 228, 228, 25572, 228, 228, 228, 228, 484, 740, 996, 1252, 1508, 1764, 2020, 2276, 2532, 2788, 3044, 3300, 3556, 3812, 4068, 4324, 4580, 4836, 5092, 5348, 5604, 5860, 6116, 6372, 6628, 228, 228, 228, 228, 228, 228, 6884, 7140, 7396, 7652, 7908, 8164, 8420, 8676, 8932, 9188, 9444, 9700, 9956, 10212, 10468, 10724, 10980, 11236, 11492, 11748, 12004, 12260, 12516, 12772, 13028, 13284, 228, 228, 228, 228, 228, 228, 228, 228, 228, 228, 228, 228, 228, 228, 228, 228, 228, 228, 228, 228, 228, 228, 228, 228, 228, 228, 228, 228, 228, 228, 228, 228, 228, 228, 228, 228, 228, 228, 228, 228, 228, 228, 228, 228, 228, 228, 228, 228, 228, 228, 228, 228, 228, 228, 228, 228, 228, 228, 228, 228, 228, 228, 228, 228, 228, 228, 228, 228, 228, 228, 228, 228, 228, 228, 228, 228, 228, 228, 228, 228, 228, 228, 228, 228, 228, 228, 228, 228, 228, 228, 228, 228, 228, 228, 228, 228, 228, 228, 228, 228, 228, 228, 228, 228, 228, 228, 228, 228, 228, 228, 228, 228, 228, 228, 228, 228, 228, 228, 228, 228, 228, 228, 228, 228, 228, 228, 228, 228, 228, 228, 228, 228, 228, 232, 232, 232, 232, 232, 232, 232, 232, 232, 232, 232, 232, 232, 232, 232, 232, 232, 232, 232, 232, 232, 232, 232, 232, 232, 232, 232, 232, 232, 232, 232, 232, 232, 232, 232, 232, 232, 232, 232, 232, 232, 232, 232, 16104, 232, 232, 232, 16360, 13544, 13800, 14056, 14312, 14568, 14824, 15080, 15336, 15592, 15848, 232, 232, 232, 25576, 232, 232, 232, 232, 488, 744, 1000, 1256, 1512, 1768, 2024, 2280, 2536, 2792, 3048, 3304, 3560, 3816, 4072, 4328, 4584, 4840, 5096, 5352, 5608, 5864, 6120, 6376, 6632, 232, 232, 232, 232, 232, 232, 6888, 7144, 7400, 7656, 7912, 8168, 8424, 8680, 8936, 9192, 9448, 9704, 9960, 10216, 10472, 10728, 10984, 11240, 11496, 11752, 12008, 12264, 12520, 12776, 13032, 13288, 232, 232, 232, 232, 232, 232, 232, 232, 232, 232, 232, 232, 232, 232, 232, 232, 232, 232, 232, 232, 232, 232, 232, 232, 232, 232, 232, 232, 232, 232, 232, 232, 232, 232, 232, 232, 232, 232, 232, 232, 232, 232, 232, 232, 232, 232, 232, 232, 232, 232, 232, 232, 232, 232, 232, 232, 232, 232, 232, 232, 232, 232, 232, 232, 232, 232, 232, 232, 232, 232, 232, 232, 232, 232, 232, 232, 232, 232, 232, 232, 232, 232, 232, 232, 232, 232, 232, 232, 232, 232, 232, 232, 232, 232, 232, 232, 232, 232, 232, 232, 232, 232, 232, 232, 232, 232, 232, 232, 232, 232, 232, 232, 232, 232, 232, 232, 232, 232, 232, 232, 232, 232, 232, 232, 232, 232, 232, 232, 232, 232, 232, 232, 232, 236, 236, 236, 236, 236, 236, 236, 236, 236, 236, 236, 236, 236, 236, 236, 236, 236, 236, 236, 236, 236, 236, 236, 236, 236, 236, 236, 236, 236, 236, 236, 236, 236, 236, 236, 236, 236, 236, 236, 236, 236, 236, 236, 16108, 236, 236, 236, 16364, 13548, 13804, 14060, 14316, 14572, 14828, 15084, 15340, 15596, 15852, 236, 236, 236, 25580, 236, 236, 236, 236, 492, 748, 1004, 1260, 1516, 1772, 2028, 2284, 2540, 2796, 3052, 3308, 3564, 3820, 4076, 4332, 4588, 4844, 5100, 5356, 5612, 5868, 6124, 6380, 6636, 236, 236, 236, 236, 236, 236, 6892, 7148, 7404, 7660, 7916, 8172, 8428, 8684, 8940, 9196, 9452, 9708, 9964, 10220, 10476, 10732, 10988, 11244, 11500, 11756, 12012, 12268, 12524, 12780, 13036, 13292, 236, 236, 236, 236, 236, 236, 236, 236, 236, 236, 236, 236, 236, 236, 236, 236, 236, 236, 236, 236, 236, 236, 236, 236, 236, 236, 236, 236, 236, 236, 236, 236, 236, 236, 236, 236, 236, 236, 236, 236, 236, 236, 236, 236, 236, 236, 236, 236, 236, 236, 236, 236, 236, 236, 236, 236, 236, 236, 236, 236, 236, 236, 236, 236, 236, 236, 236, 236, 236, 236, 236, 236, 236, 236, 236, 236, 236, 236, 236, 236, 236, 236, 236, 236, 236, 236, 236, 236, 236, 236, 236, 236, 236, 236, 236, 236, 236, 236, 236, 236, 236, 236, 236, 236, 236, 236, 236, 236, 236, 236, 236, 236, 236, 236, 236, 236, 236, 236, 236, 236, 236, 236, 236, 236, 236, 236, 236, 236, 236, 236, 236, 236, 236, 240, 240, 240, 240, 240, 240, 240, 240, 240, 240, 240, 240, 240, 240, 240, 240, 240, 240, 240, 240, 240, 240, 240, 240, 240, 240, 240, 240, 240, 240, 240, 240, 240, 240, 240, 240, 240, 240, 240, 240, 240, 240, 240, 16112, 240, 240, 240, 16368, 13552, 13808, 14064, 14320, 14576, 14832, 15088, 15344, 15600, 15856, 240, 240, 240, 25584, 240, 240, 240, 240, 496, 752, 1008, 1264, 1520, 1776, 2032, 2288, 2544, 2800, 3056, 3312, 3568, 3824, 4080, 4336, 4592, 4848, 5104, 5360, 5616, 5872, 6128, 6384, 6640, 240, 240, 240, 240, 240, 240, 6896, 7152, 7408, 7664, 7920, 8176, 8432, 8688, 8944, 9200, 9456, 9712, 9968, 10224, 10480, 10736, 10992, 11248, 11504, 11760, 12016, 12272, 12528, 12784, 13040, 13296, 240, 240, 240, 240, 240, 240, 240, 240, 240, 240, 240, 240, 240, 240, 240, 240, 240, 240, 240, 240, 240, 240, 240, 240, 240, 240, 240, 240, 240, 240, 240, 240, 240, 240, 240, 240, 240, 240, 240, 240, 240, 240, 240, 240, 240, 240, 240, 240, 240, 240, 240, 240, 240, 240, 240, 240, 240, 240, 240, 240, 240, 240, 240, 240, 240, 240, 240, 240, 240, 240, 240, 240, 240, 240, 240, 240, 240, 240, 240, 240, 240, 240, 240, 240, 240, 240, 240, 240, 240, 240, 240, 240, 240, 240, 240, 240, 240, 240, 240, 240, 240, 240, 240, 240, 240, 240, 240, 240, 240, 240, 240, 240, 240, 240, 240, 240, 240, 240, 240, 240, 240, 240, 240, 240, 240, 240, 240, 240, 240, 240, 240, 240, 240, 244, 244, 244, 244, 244, 244, 244, 244, 244, 244, 244, 244, 244, 244, 244, 244, 244, 244, 244, 244, 244, 244, 244, 244, 244, 244, 244, 244, 244, 244, 244, 244, 244, 244, 244, 244, 244, 244, 244, 244, 244, 244, 244, 16116, 244, 244, 244, 16372, 13556, 13812, 14068, 14324, 14580, 14836, 15092, 15348, 15604, 15860, 244, 244, 244, 25588, 244, 244, 244, 244, 500, 756, 1012, 1268, 1524, 1780, 2036, 2292, 2548, 2804, 3060, 3316, 3572, 3828, 4084, 4340, 4596, 4852, 5108, 5364, 5620, 5876, 6132, 6388, 6644, 244, 244, 244, 244, 244, 244, 6900, 7156, 7412, 7668, 7924, 8180, 8436, 8692, 8948, 9204, 9460, 9716, 9972, 10228, 10484, 10740, 10996, 11252, 11508, 11764, 12020, 12276, 12532, 12788, 13044, 13300, 244, 244, 244, 244, 244, 244, 244, 244, 244, 244, 244, 244, 244, 244, 244, 244, 244, 244, 244, 244, 244, 244, 244, 244, 244, 244, 244, 244, 244, 244, 244, 244, 244, 244, 244, 244, 244, 244, 244, 244, 244, 244, 244, 244, 244, 244, 244, 244, 244, 244, 244, 244, 244, 244, 244, 244, 244, 244, 244, 244, 244, 244, 244, 244, 244, 244, 244, 244, 244, 244, 244, 244, 244, 244, 244, 244, 244, 244, 244, 244, 244, 244, 244, 244, 244, 244, 244, 244, 244, 244, 244, 244, 244, 244, 244, 244, 244, 244, 244, 244, 244, 244, 244, 244, 244, 244, 244, 244, 244, 244, 244, 244, 244, 244, 244, 244, 244, 244, 244, 244, 244, 244, 244, 244, 244, 244, 244, 244, 244, 244, 244, 244, 244, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15872, 0, 0, 0, 16128, 13312, 13568, 13824, 14080, 14336, 14592, 14848, 15104, 15360, 15616, 0, 0, 0, 25344, 0, 0, 0, 0, 256, 512, 768, 1024, 1280, 1536, 1792, 2048, 2304, 2560, 2816, 3072, 3328, 3584, 3840, 4096, 4352, 4608, 4864, 5120, 5376, 5632, 5888, 6144, 6400, 0, 0, 0, 0, 0, 0, 6656, 6912, 7168, 7424, 7680, 7936, 8192, 8448, 8704, 8960, 9216, 9472, 9728, 9984, 10240, 10496, 10752, 11008, 11264, 11520, 11776, 12032, 12288, 12544, 12800, 13056, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15872, 0, 0, 0, 16128, 13312, 13568, 13824, 14080, 14336, 14592, 14848, 15104, 15360, 15616, 0, 0, 0, 25344, 0, 0, 0, 0, 256, 512, 768, 1024, 1280, 1536, 1792, 2048, 2304, 2560, 2816, 3072, 3328, 3584, 3840, 4096, 4352, 4608, 4864, 5120, 5376, 5632, 5888, 6144, 6400, 0, 0, 0, 0, 0, 0, 6656, 6912, 7168, 7424, 7680, 7936, 8192, 8448, 8704, 8960, 9216, 9472, 9728, 9984, 10240, 10496, 10752, 11008, 11264, 11520, 11776, 12032, 12288, 12544, 12800, 13056, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15872, 0, 0, 0, 16128, 13312, 13568, 13824, 14080, 14336, 14592, 14848, 15104, 15360, 15616, 0, 0, 0, 25344, 0, 0, 0, 0, 256, 512, 768, 1024, 1280, 1536, 1792, 2048, 2304, 2560, 2816, 3072, 3328, 3584, 3840, 4096, 4352, 4608, 4864, 5120, 5376, 5632, 5888, 6144, 6400, 0, 0, 0, 0, 0, 0, 6656, 6912, 7168, 7424, 7680, 7936, 8192, 8448, 8704, 8960, 9216, 9472, 9728, 9984, 10240, 10496, 10752, 11008, 11264, 11520, 11776, 12032, 12288, 12544, 12800, 13056, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 396, 396, 396, 396, 396, 396, 396, 396, 396, 396, 396, 396, 396, 396, 396, 396, 396, 396, 396, 396, 396, 396, 396, 396, 396, 396, 396, 396, 396, 396, 396, 396, 396, 396, 396, 396, 396, 396, 396, 396, 396, 396, 396, 16268, 396, 396, 396, 16268, 13708, 13708, 14220, 14220, 14732, 14732, 15244, 15244, 15756, 15756, 396, 396, 396, 25484, 396, 396, 396, 396, 396, 908, 908, 1420, 1420, 1932, 1932, 2444, 2444, 2956, 2956, 3468, 3468, 3980, 3980, 4492, 4492, 5004, 5004, 5516, 5516, 6028, 6028, 6540, 6540, 396, 396, 396, 396, 396, 396, 7052, 7052, 7564, 7564, 8076, 8076, 8588, 8588, 9100, 9100, 9612, 9612, 10124, 10124, 10636, 10636, 11148, 11148, 11660, 11660, 12172, 12172, 12684, 12684, 13196, 13196, 396, 396, 396, 396, 396, 396, 396, 396, 396, 396, 396, 396, 396, 396, 396, 396, 396, 396, 396, 396, 396, 396, 396, 396, 396, 396, 396, 396, 396, 396, 396, 396, 396, 396, 396, 396, 396, 396, 396, 396, 396, 396, 396, 396, 396, 396, 396, 396, 396, 396, 396, 396, 396, 396, 396, 396, 396, 396, 396, 396, 396, 396, 396, 396, 396, 396, 396, 396, 396, 396, 396, 396, 396, 396, 396, 396, 396, 396, 396, 396, 396, 396, 396, 396, 396, 396, 396, 396, 396, 396, 396, 396, 396, 396, 396, 396, 396, 396, 396, 396, 396, 396, 396, 396, 396, 396, 396, 396, 396, 396, 396, 396, 396, 396, 396, 396, 396, 396, 396, 396, 396, 396, 396, 396, 396, 396, 396, 396, 396, 396, 396, 396, 396, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15872, 0, 0, 0, 16128, 13312, 13568, 13824, 14080, 14336, 14592, 14848, 15104, 15360, 15616, 0, 0, 0, 25344, 0, 0, 0, 0, 256, 512, 768, 1024, 1280, 1536, 1792, 2048, 2304, 2560, 2816, 3072, 3328, 3584, 3840, 4096, 4352, 4608, 4864, 5120, 5376, 5632, 5888, 6144, 6400, 0, 0, 0, 0, 0, 0, 6656, 6912, 7168, 7424, 7680, 7936, 8192, 8448, 8704, 8960, 9216, 9472, 9728, 9984, 10240, 10496, 10752, 11008, 11264, 11520, 11776, 12032, 12288, 12544, 12800, 13056, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15872, 0, 0, 0, 16128, 13312, 13568, 13824, 14080, 14336, 14592, 14848, 15104, 15360, 15616, 0, 0, 0, 25344, 0, 0, 0, 0, 256, 512, 768, 1024, 1280, 1536, 1792, 2048, 2304, 2560, 2816, 3072, 3328, 3584, 3840, 4096, 4352, 4608, 4864, 5120, 5376, 5632, 5888, 6144, 6400, 0, 0, 0, 0, 0, 0, 6656, 6912, 7168, 7424, 7680, 7936, 8192, 8448, 8704, 8960, 9216, 9472, 9728, 9984, 10240, 10496, 10752, 11008, 11264, 11520, 11776, 12032, 12288, 12544, 12800, 13056, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15872, 0, 0, 0, 16128, 13312, 13568, 13824, 14080, 14336, 14592, 14848, 15104, 15360, 15616, 0, 0, 0, 25344, 0, 0, 0, 0, 256, 512, 768, 1024, 1280, 1536, 1792, 2048, 2304, 2560, 2816, 3072, 3328, 3584, 3840, 4096, 4352, 4608, 4864, 5120, 5376, 5632, 5888, 6144, 6400, 0, 0, 0, 0, 0, 0, 6656, 6912, 7168, 7424, 7680, 7936, 8192, 8448, 8704, 8960, 9216, 9472, 9728, 9984, 10240, 10496, 10752, 11008, 11264, 11520, 11776, 12032, 12288, 12544, 12800, 13056, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15872, 0, 0, 0, 16128, 13312, 13568, 13824, 14080, 14336, 14592, 14848, 15104, 15360, 15616, 0, 0, 0, 25344, 0, 0, 0, 0, 256, 512, 768, 1024, 1280, 1536, 1792, 2048, 2304, 2560, 2816, 3072, 3328, 3584, 3840, 4096, 4352, 4608, 4864, 5120, 5376, 5632, 5888, 6144, 6400, 0, 0, 0, 0, 0, 0, 6656, 6912, 7168, 7424, 7680, 7936, 8192, 8448, 8704, 8960, 9216, 9472, 9728, 9984, 10240, 10496, 10752, 11008, 11264, 11520, 11776, 12032, 12288, 12544, 12800, 13056, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 15876, 4, 4, 4, 16132, 13316, 13572, 13828, 14084, 14340, 14596, 14852, 15108, 15364, 15620, 4, 4, 4, 25348, 4, 4, 4, 4, 260, 516, 772, 1028, 1284, 1540, 1796, 2052, 2308, 2564, 2820, 3076, 3332, 3588, 3844, 4100, 4356, 4612, 4868, 5124, 5380, 5636, 5892, 6148, 6404, 4, 4, 4, 4, 4, 4, 6660, 6916, 7172, 7428, 7684, 7940, 8196, 8452, 8708, 8964, 9220, 9476, 9732, 9988, 10244, 10500, 10756, 11012, 11268, 11524, 11780, 12036, 12292, 12548, 12804, 13060, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 15880, 8, 8, 8, 16136, 13320, 13576, 13832, 14088, 14344, 14600, 14856, 15112, 15368, 15624, 8, 8, 8, 25352, 8, 8, 8, 8, 264, 520, 776, 1032, 1288, 1544, 1800, 2056, 2312, 2568, 2824, 3080, 3336, 3592, 3848, 4104, 4360, 4616, 4872, 5128, 5384, 5640, 5896, 6152, 6408, 8, 8, 8, 8, 8, 8, 6664, 6920, 7176, 7432, 7688, 7944, 8200, 8456, 8712, 8968, 9224, 9480, 9736, 9992, 10248, 10504, 10760, 11016, 11272, 11528, 11784, 12040, 12296, 12552, 12808, 13064, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 15884, 12, 12, 12, 16140, 13324, 13580, 13836, 14092, 14348, 14604, 14860, 15116, 15372, 15628, 12, 12, 12, 25356, 12, 12, 12, 12, 268, 524, 780, 1036, 1292, 1548, 1804, 2060, 2316, 2572, 2828, 3084, 3340, 3596, 3852, 4108, 4364, 4620, 4876, 5132, 5388, 5644, 5900, 6156, 6412, 12, 12, 12, 12, 12, 12, 6668, 6924, 7180, 7436, 7692, 7948, 8204, 8460, 8716, 8972, 9228, 9484, 9740, 9996, 10252, 10508, 10764, 11020, 11276, 11532, 11788, 12044, 12300, 12556, 12812, 13068, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 15888, 16, 16, 16, 16144, 13328, 13584, 13840, 14096, 14352, 14608, 14864, 15120, 15376, 15632, 16, 16, 16, 25360, 16, 16, 16, 16, 272, 528, 784, 1040, 1296, 1552, 1808, 2064, 2320, 2576, 2832, 3088, 3344, 3600, 3856, 4112, 4368, 4624, 4880, 5136, 5392, 5648, 5904, 6160, 6416, 16, 16, 16, 16, 16, 16, 6672, 6928, 7184, 7440, 7696, 7952, 8208, 8464, 8720, 8976, 9232, 9488, 9744, 10000, 10256, 10512, 10768, 11024, 11280, 11536, 11792, 12048, 12304, 12560, 12816, 13072, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 15892, 20, 20, 20, 16148, 13332, 13588, 13844, 14100, 14356, 14612, 14868, 15124, 15380, 15636, 20, 20, 20, 25364, 20, 20, 20, 20, 276, 532, 788, 1044, 1300, 1556, 1812, 2068, 2324, 2580, 2836, 3092, 3348, 3604, 3860, 4116, 4372, 4628, 4884, 5140, 5396, 5652, 5908, 6164, 6420, 20, 20, 20, 20, 20, 20, 6676, 6932, 7188, 7444, 7700, 7956, 8212, 8468, 8724, 8980, 9236, 9492, 9748, 10004, 10260, 10516, 10772, 11028, 11284, 11540, 11796, 12052, 12308, 12564, 12820, 13076, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 15896, 24, 24, 24, 16152, 13336, 13592, 13848, 14104, 14360, 14616, 14872, 15128, 15384, 15640, 24, 24, 24, 25368, 24, 24, 24, 24, 280, 536, 792, 1048, 1304, 1560, 1816, 2072, 2328, 2584, 2840, 3096, 3352, 3608, 3864, 4120, 4376, 4632, 4888, 5144, 5400, 5656, 5912, 6168, 6424, 24, 24, 24, 24, 24, 24, 6680, 6936, 7192, 7448, 7704, 7960, 8216, 8472, 8728, 8984, 9240, 9496, 9752, 10008, 10264, 10520, 10776, 11032, 11288, 11544, 11800, 12056, 12312, 12568, 12824, 13080, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 15900, 28, 28, 28, 16156, 13340, 13596, 13852, 14108, 14364, 14620, 14876, 15132, 15388, 15644, 28, 28, 28, 25372, 28, 28, 28, 28, 284, 540, 796, 1052, 1308, 1564, 1820, 2076, 2332, 2588, 2844, 3100, 3356, 3612, 3868, 4124, 4380, 4636, 4892, 5148, 5404, 5660, 5916, 6172, 6428, 28, 28, 28, 28, 28, 28, 6684, 6940, 7196, 7452, 7708, 7964, 8220, 8476, 8732, 8988, 9244, 9500, 9756, 10012, 10268, 10524, 10780, 11036, 11292, 11548, 11804, 12060, 12316, 12572, 12828, 13084, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 15904, 32, 32, 32, 16160, 13344, 13600, 13856, 14112, 14368, 14624, 14880, 15136, 15392, 15648, 32, 32, 32, 25376, 32, 32, 32, 32, 288, 544, 800, 1056, 1312, 1568, 1824, 2080, 2336, 2592, 2848, 3104, 3360, 3616, 3872, 4128, 4384, 4640, 4896, 5152, 5408, 5664, 5920, 6176, 6432, 32, 32, 32, 32, 32, 32, 6688, 6944, 7200, 7456, 7712, 7968, 8224, 8480, 8736, 8992, 9248, 9504, 9760, 10016, 10272, 10528, 10784, 11040, 11296, 11552, 11808, 12064, 12320, 12576, 12832, 13088, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 15908, 36, 36, 36, 16164, 13348, 13604, 13860, 14116, 14372, 14628, 14884, 15140, 15396, 15652, 36, 36, 36, 25380, 36, 36, 36, 36, 292, 548, 804, 1060, 1316, 1572, 1828, 2084, 2340, 2596, 2852, 3108, 3364, 3620, 3876, 4132, 4388, 4644, 4900, 5156, 5412, 5668, 5924, 6180, 6436, 36, 36, 36, 36, 36, 36, 6692, 6948, 7204, 7460, 7716, 7972, 8228, 8484, 8740, 8996, 9252, 9508, 9764, 10020, 10276, 10532, 10788, 11044, 11300, 11556, 11812, 12068, 12324, 12580, 12836, 13092, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 15912, 40, 40, 40, 16168, 13352, 13608, 13864, 14120, 14376, 14632, 14888, 15144, 15400, 15656, 40, 40, 40, 25384, 40, 40, 40, 40, 296, 552, 808, 1064, 1320, 1576, 1832, 2088, 2344, 2600, 2856, 3112, 3368, 3624, 3880, 4136, 4392, 4648, 4904, 5160, 5416, 5672, 5928, 6184, 6440, 40, 40, 40, 40, 40, 40, 6696, 6952, 7208, 7464, 7720, 7976, 8232, 8488, 8744, 9000, 9256, 9512, 9768, 10024, 10280, 10536, 10792, 11048, 11304, 11560, 11816, 12072, 12328, 12584, 12840, 13096, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 15916, 44, 44, 44, 16172, 13356, 13612, 13868, 14124, 14380, 14636, 14892, 15148, 15404, 15660, 44, 44, 44, 25388, 44, 44, 44, 44, 300, 556, 812, 1068, 1324, 1580, 1836, 2092, 2348, 2604, 2860, 3116, 3372, 3628, 3884, 4140, 4396, 4652, 4908, 5164, 5420, 5676, 5932, 6188, 6444, 44, 44, 44, 44, 44, 44, 6700, 6956, 7212, 7468, 7724, 7980, 8236, 8492, 8748, 9004, 9260, 9516, 9772, 10028, 10284, 10540, 10796, 11052, 11308, 11564, 11820, 12076, 12332, 12588, 12844, 13100, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 15920, 48, 48, 48, 16176, 13360, 13616, 13872, 14128, 14384, 14640, 14896, 15152, 15408, 15664, 48, 48, 48, 25392, 48, 48, 48, 48, 304, 560, 816, 1072, 1328, 1584, 1840, 2096, 2352, 2608, 2864, 3120, 3376, 3632, 3888, 4144, 4400, 4656, 4912, 5168, 5424, 5680, 5936, 6192, 6448, 48, 48, 48, 48, 48, 48, 6704, 6960, 7216, 7472, 7728, 7984, 8240, 8496, 8752, 9008, 9264, 9520, 9776, 10032, 10288, 10544, 10800, 11056, 11312, 11568, 11824, 12080, 12336, 12592, 12848, 13104, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 15924, 52, 52, 52, 16180, 13364, 13620, 13876, 14132, 14388, 14644, 14900, 15156, 15412, 15668, 52, 52, 52, 25396, 52, 52, 52, 52, 308, 564, 820, 1076, 1332, 1588, 1844, 2100, 2356, 2612, 2868, 3124, 3380, 3636, 3892, 4148, 4404, 4660, 4916, 5172, 5428, 5684, 5940, 6196, 6452, 52, 52, 52, 52, 52, 52, 6708, 6964, 7220, 7476, 7732, 7988, 8244, 8500, 8756, 9012, 9268, 9524, 9780, 10036, 10292, 10548, 10804, 11060, 11316, 11572, 11828, 12084, 12340, 12596, 12852, 13108, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 15928, 56, 56, 56, 16184, 13368, 13624, 13880, 14136, 14392, 14648, 14904, 15160, 15416, 15672, 56, 56, 56, 25400, 56, 56, 56, 56, 312, 568, 824, 1080, 1336, 1592, 1848, 2104, 2360, 2616, 2872, 3128, 3384, 3640, 3896, 4152, 4408, 4664, 4920, 5176, 5432, 5688, 5944, 6200, 6456, 56, 56, 56, 56, 56, 56, 6712, 6968, 7224, 7480, 7736, 7992, 8248, 8504, 8760, 9016, 9272, 9528, 9784, 10040, 10296, 10552, 10808, 11064, 11320, 11576, 11832, 12088, 12344, 12600, 12856, 13112, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 15932, 60, 60, 60, 16188, 13372, 13628, 13884, 14140, 14396, 14652, 14908, 15164, 15420, 15676, 60, 60, 60, 25404, 60, 60, 60, 60, 316, 572, 828, 1084, 1340, 1596, 1852, 2108, 2364, 2620, 2876, 3132, 3388, 3644, 3900, 4156, 4412, 4668, 4924, 5180, 5436, 5692, 5948, 6204, 6460, 60, 60, 60, 60, 60, 60, 6716, 6972, 7228, 7484, 7740, 7996, 8252, 8508, 8764, 9020, 9276, 9532, 9788, 10044, 10300, 10556, 10812, 11068, 11324, 11580, 11836, 12092, 12348, 12604, 12860, 13116, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 15936, 64, 64, 64, 16192, 13376, 13632, 13888, 14144, 14400, 14656, 14912, 15168, 15424, 15680, 64, 64, 64, 25408, 64, 64, 64, 64, 320, 576, 832, 1088, 1344, 1600, 1856, 2112, 2368, 2624, 2880, 3136, 3392, 3648, 3904, 4160, 4416, 4672, 4928, 5184, 5440, 5696, 5952, 6208, 6464, 64, 64, 64, 64, 64, 64, 6720, 6976, 7232, 7488, 7744, 8000, 8256, 8512, 8768, 9024, 9280, 9536, 9792, 10048, 10304, 10560, 10816, 11072, 11328, 11584, 11840, 12096, 12352, 12608, 12864, 13120, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 15940, 68, 68, 68, 16196, 13380, 13636, 13892, 14148, 14404, 14660, 14916, 15172, 15428, 15684, 68, 68, 68, 25412, 68, 68, 68, 68, 324, 580, 836, 1092, 1348, 1604, 1860, 2116, 2372, 2628, 2884, 3140, 3396, 3652, 3908, 4164, 4420, 4676, 4932, 5188, 5444, 5700, 5956, 6212, 6468, 68, 68, 68, 68, 68, 68, 6724, 6980, 7236, 7492, 7748, 8004, 8260, 8516, 8772, 9028, 9284, 9540, 9796, 10052, 10308, 10564, 10820, 11076, 11332, 11588, 11844, 12100, 12356, 12612, 12868, 13124, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 72, 72, 72, 72, 72, 72, 72, 72, 72, 72, 72, 72, 72, 72, 72, 72, 72, 72, 72, 72, 72, 72, 72, 72, 72, 72, 72, 72, 72, 72, 72, 72, 72, 72, 72, 72, 72, 72, 72, 72, 72, 72, 72, 15944, 72, 72, 72, 16200, 13384, 13640, 13896, 14152, 14408, 14664, 14920, 15176, 15432, 15688, 72, 72, 72, 25416, 72, 72, 72, 72, 328, 584, 840, 1096, 1352, 1608, 1864, 2120, 2376, 2632, 2888, 3144, 3400, 3656, 3912, 4168, 4424, 4680, 4936, 5192, 5448, 5704, 5960, 6216, 6472, 72, 72, 72, 72, 72, 72, 6728, 6984, 7240, 7496, 7752, 8008, 8264, 8520, 8776, 9032, 9288, 9544, 9800, 10056, 10312, 10568, 10824, 11080, 11336, 11592, 11848, 12104, 12360, 12616, 12872, 13128, 72, 72, 72, 72, 72, 72, 72, 72, 72, 72, 72, 72, 72, 72, 72, 72, 72, 72, 72, 72, 72, 72, 72, 72, 72, 72, 72, 72, 72, 72, 72, 72, 72, 72, 72, 72, 72, 72, 72, 72, 72, 72, 72, 72, 72, 72, 72, 72, 72, 72, 72, 72, 72, 72, 72, 72, 72, 72, 72, 72, 72, 72, 72, 72, 72, 72, 72, 72, 72, 72, 72, 72, 72, 72, 72, 72, 72, 72, 72, 72, 72, 72, 72, 72, 72, 72, 72, 72, 72, 72, 72, 72, 72, 72, 72, 72, 72, 72, 72, 72, 72, 72, 72, 72, 72, 72, 72, 72, 72, 72, 72, 72, 72, 72, 72, 72, 72, 72, 72, 72, 72, 72, 72, 72, 72, 72, 72, 72, 72, 72, 72, 72, 72, 76, 76, 76, 76, 76, 76, 76, 76, 76, 76, 76, 76, 76, 76, 76, 76, 76, 76, 76, 76, 76, 76, 76, 76, 76, 76, 76, 76, 76, 76, 76, 76, 76, 76, 76, 76, 76, 76, 76, 76, 76, 76, 76, 15948, 76, 76, 76, 16204, 13388, 13644, 13900, 14156, 14412, 14668, 14924, 15180, 15436, 15692, 76, 76, 76, 25420, 76, 76, 76, 76, 332, 588, 844, 1100, 1356, 1612, 1868, 2124, 2380, 2636, 2892, 3148, 3404, 3660, 3916, 4172, 4428, 4684, 4940, 5196, 5452, 5708, 5964, 6220, 6476, 76, 76, 76, 76, 76, 76, 6732, 6988, 7244, 7500, 7756, 8012, 8268, 8524, 8780, 9036, 9292, 9548, 9804, 10060, 10316, 10572, 10828, 11084, 11340, 11596, 11852, 12108, 12364, 12620, 12876, 13132, 76, 76, 76, 76, 76, 76, 76, 76, 76, 76, 76, 76, 76, 76, 76, 76, 76, 76, 76, 76, 76, 76, 76, 76, 76, 76, 76, 76, 76, 76, 76, 76, 76, 76, 76, 76, 76, 76, 76, 76, 76, 76, 76, 76, 76, 76, 76, 76, 76, 76, 76, 76, 76, 76, 76, 76, 76, 76, 76, 76, 76, 76, 76, 76, 76, 76, 76, 76, 76, 76, 76, 76, 76, 76, 76, 76, 76, 76, 76, 76, 76, 76, 76, 76, 76, 76, 76, 76, 76, 76, 76, 76, 76, 76, 76, 76, 76, 76, 76, 76, 76, 76, 76, 76, 76, 76, 76, 76, 76, 76, 76, 76, 76, 76, 76, 76, 76, 76, 76, 76, 76, 76, 76, 76, 76, 76, 76, 76, 76, 76, 76, 76, 76, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 15952, 80, 80, 80, 16208, 13392, 13648, 13904, 14160, 14416, 14672, 14928, 15184, 15440, 15696, 80, 80, 80, 25424, 80, 80, 80, 80, 336, 592, 848, 1104, 1360, 1616, 1872, 2128, 2384, 2640, 2896, 3152, 3408, 3664, 3920, 4176, 4432, 4688, 4944, 5200, 5456, 5712, 5968, 6224, 6480, 80, 80, 80, 80, 80, 80, 6736, 6992, 7248, 7504, 7760, 8016, 8272, 8528, 8784, 9040, 9296, 9552, 9808, 10064, 10320, 10576, 10832, 11088, 11344, 11600, 11856, 12112, 12368, 12624, 12880, 13136, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 15956, 84, 84, 84, 16212, 13396, 13652, 13908, 14164, 14420, 14676, 14932, 15188, 15444, 15700, 84, 84, 84, 25428, 84, 84, 84, 84, 340, 596, 852, 1108, 1364, 1620, 1876, 2132, 2388, 2644, 2900, 3156, 3412, 3668, 3924, 4180, 4436, 4692, 4948, 5204, 5460, 5716, 5972, 6228, 6484, 84, 84, 84, 84, 84, 84, 6740, 6996, 7252, 7508, 7764, 8020, 8276, 8532, 8788, 9044, 9300, 9556, 9812, 10068, 10324, 10580, 10836, 11092, 11348, 11604, 11860, 12116, 12372, 12628, 12884, 13140, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 15960, 88, 88, 88, 16216, 13400, 13656, 13912, 14168, 14424, 14680, 14936, 15192, 15448, 15704, 88, 88, 88, 25432, 88, 88, 88, 88, 344, 600, 856, 1112, 1368, 1624, 1880, 2136, 2392, 2648, 2904, 3160, 3416, 3672, 3928, 4184, 4440, 4696, 4952, 5208, 5464, 5720, 5976, 6232, 6488, 88, 88, 88, 88, 88, 88, 6744, 7000, 7256, 7512, 7768, 8024, 8280, 8536, 8792, 9048, 9304, 9560, 9816, 10072, 10328, 10584, 10840, 11096, 11352, 11608, 11864, 12120, 12376, 12632, 12888, 13144, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 15964, 92, 92, 92, 16220, 13404, 13660, 13916, 14172, 14428, 14684, 14940, 15196, 15452, 15708, 92, 92, 92, 25436, 92, 92, 92, 92, 348, 604, 860, 1116, 1372, 1628, 1884, 2140, 2396, 2652, 2908, 3164, 3420, 3676, 3932, 4188, 4444, 4700, 4956, 5212, 5468, 5724, 5980, 6236, 6492, 92, 92, 92, 92, 92, 92, 6748, 7004, 7260, 7516, 7772, 8028, 8284, 8540, 8796, 9052, 9308, 9564, 9820, 10076, 10332, 10588, 10844, 11100, 11356, 11612, 11868, 12124, 12380, 12636, 12892, 13148, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 15968, 96, 96, 96, 16224, 13408, 13664, 13920, 14176, 14432, 14688, 14944, 15200, 15456, 15712, 96, 96, 96, 25440, 96, 96, 96, 96, 352, 608, 864, 1120, 1376, 1632, 1888, 2144, 2400, 2656, 2912, 3168, 3424, 3680, 3936, 4192, 4448, 4704, 4960, 5216, 5472, 5728, 5984, 6240, 6496, 96, 96, 96, 96, 96, 96, 6752, 7008, 7264, 7520, 7776, 8032, 8288, 8544, 8800, 9056, 9312, 9568, 9824, 10080, 10336, 10592, 10848, 11104, 11360, 11616, 11872, 12128, 12384, 12640, 12896, 13152, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 15972, 100, 100, 100, 16228, 13412, 13668, 13924, 14180, 14436, 14692, 14948, 15204, 15460, 15716, 100, 100, 100, 25444, 100, 100, 100, 100, 356, 612, 868, 1124, 1380, 1636, 1892, 2148, 2404, 2660, 2916, 3172, 3428, 3684, 3940, 4196, 4452, 4708, 4964, 5220, 5476, 5732, 5988, 6244, 6500, 100, 100, 100, 100, 100, 100, 6756, 7012, 7268, 7524, 7780, 8036, 8292, 8548, 8804, 9060, 9316, 9572, 9828, 10084, 10340, 10596, 10852, 11108, 11364, 11620, 11876, 12132, 12388, 12644, 12900, 13156, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15872, 0, 0, 0, 16128, 13312, 13568, 13824, 14080, 14336, 14592, 14848, 15104, 15360, 15616, 0, 0, 0, 25344, 0, 0, 0, 0, 256, 512, 768, 1024, 1280, 1536, 1792, 2048, 2304, 2560, 2816, 3072, 3328, 3584, 3840, 4096, 4352, 4608, 4864, 5120, 5376, 5632, 5888, 6144, 6400, 0, 0, 0, 0, 0, 0, 6656, 6912, 7168, 7424, 7680, 7936, 8192, 8448, 8704, 8960, 9216, 9472, 9728, 9984, 10240, 10496, 10752, 11008, 11264, 11520, 11776, 12032, 12288, 12544, 12800, 13056, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15872, 0, 0, 0, 16128, 13312, 13568, 13824, 14080, 14336, 14592, 14848, 15104, 15360, 15616, 0, 0, 0, 25344, 0, 0, 0, 0, 256, 512, 768, 1024, 1280, 1536, 1792, 2048, 2304, 2560, 2816, 3072, 3328, 3584, 3840, 4096, 4352, 4608, 4864, 5120, 5376, 5632, 5888, 6144, 6400, 0, 0, 0, 0, 0, 0, 6656, 6912, 7168, 7424, 7680, 7936, 8192, 8448, 8704, 8960, 9216, 9472, 9728, 9984, 10240, 10496, 10752, 11008, 11264, 11520, 11776, 12032, 12288, 12544, 12800, 13056, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15872, 0, 0, 0, 16128, 13312, 13568, 13824, 14080, 14336, 14592, 14848, 15104, 15360, 15616, 0, 0, 0, 25344, 0, 0, 0, 0, 256, 512, 768, 1024, 1280, 1536, 1792, 2048, 2304, 2560, 2816, 3072, 3328, 3584, 3840, 4096, 4352, 4608, 4864, 5120, 5376, 5632, 5888, 6144, 6400, 0, 0, 0, 0, 0, 0, 6656, 6912, 7168, 7424, 7680, 7936, 8192, 8448, 8704, 8960, 9216, 9472, 9728, 9984, 10240, 10496, 10752, 11008, 11264, 11520, 11776, 12032, 12288, 12544, 12800, 13056, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15872, 0, 0, 0, 16128, 13312, 13568, 13824, 14080, 14336, 14592, 14848, 15104, 15360, 15616, 0, 0, 0, 25344, 0, 0, 0, 0, 256, 512, 768, 1024, 1280, 1536, 1792, 2048, 2304, 2560, 2816, 3072, 3328, 3584, 3840, 4096, 4352, 4608, 4864, 5120, 5376, 5632, 5888, 6144, 6400, 0, 0, 0, 0, 0, 0, 6656, 6912, 7168, 7424, 7680, 7936, 8192, 8448, 8704, 8960, 9216, 9472, 9728, 9984, 10240, 10496, 10752, 11008, 11264, 11520, 11776, 12032, 12288, 12544, 12800, 13056, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15872, 0, 0, 0, 16128, 13312, 13568, 13824, 14080, 14336, 14592, 14848, 15104, 15360, 15616, 0, 0, 0, 25344, 0, 0, 0, 0, 256, 512, 768, 1024, 1280, 1536, 1792, 2048, 2304, 2560, 2816, 3072, 3328, 3584, 3840, 4096, 4352, 4608, 4864, 5120, 5376, 5632, 5888, 6144, 6400, 0, 0, 0, 0, 0, 0, 6656, 6912, 7168, 7424, 7680, 7936, 8192, 8448, 8704, 8960, 9216, 9472, 9728, 9984, 10240, 10496, 10752, 11008, 11264, 11520, 11776, 12032, 12288, 12544, 12800, 13056, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15872, 0, 0, 0, 16128, 13312, 13568, 13824, 14080, 14336, 14592, 14848, 15104, 15360, 15616, 0, 0, 0, 25344, 0, 0, 0, 0, 256, 512, 768, 1024, 1280, 1536, 1792, 2048, 2304, 2560, 2816, 3072, 3328, 3584, 3840, 4096, 4352, 4608, 4864, 5120, 5376, 5632, 5888, 6144, 6400, 0, 0, 0, 0, 0, 0, 6656, 6912, 7168, 7424, 7680, 7936, 8192, 8448, 8704, 8960, 9216, 9472, 9728, 9984, 10240, 10496, 10752, 11008, 11264, 11520, 11776, 12032, 12288, 12544, 12800, 13056, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 104, 104, 104, 104, 104, 104, 104, 104, 104, 104, 104, 104, 104, 104, 104, 104, 104, 104, 104, 104, 104, 104, 104, 104, 104, 104, 104, 104, 104, 104, 104, 104, 104, 104, 104, 104, 104, 104, 104, 104, 104, 104, 104, 15976, 104, 104, 104, 16232, 13416, 13672, 13928, 14184, 14440, 14696, 14952, 15208, 15464, 15720, 104, 104, 104, 25448, 104, 104, 104, 104, 360, 616, 872, 1128, 1384, 1640, 1896, 2152, 2408, 2664, 2920, 3176, 3432, 3688, 3944, 4200, 4456, 4712, 4968, 5224, 5480, 5736, 5992, 6248, 6504, 104, 104, 104, 104, 104, 104, 6760, 7016, 7272, 7528, 7784, 8040, 8296, 8552, 8808, 9064, 9320, 9576, 9832, 10088, 10344, 10600, 10856, 11112, 11368, 11624, 11880, 12136, 12392, 12648, 12904, 13160, 104, 104, 104, 104, 104, 104, 104, 104, 104, 104, 104, 104, 104, 104, 104, 104, 104, 104, 104, 104, 104, 104, 104, 104, 104, 104, 104, 104, 104, 104, 104, 104, 104, 104, 104, 104, 104, 104, 104, 104, 104, 104, 104, 104, 104, 104, 104, 104, 104, 104, 104, 104, 104, 104, 104, 104, 104, 104, 104, 104, 104, 104, 104, 104, 104, 104, 104, 104, 104, 104, 104, 104, 104, 104, 104, 104, 104, 104, 104, 104, 104, 104, 104, 104, 104, 104, 104, 104, 104, 104, 104, 104, 104, 104, 104, 104, 104, 104, 104, 104, 104, 104, 104, 104, 104, 104, 104, 104, 104, 104, 104, 104, 104, 104, 104, 104, 104, 104, 104, 104, 104, 104, 104, 104, 104, 104, 104, 104, 104, 104, 104, 104, 104, 108, 108, 108, 108, 108, 108, 108, 108, 108, 108, 108, 108, 108, 108, 108, 108, 108, 108, 108, 108, 108, 108, 108, 108, 108, 108, 108, 108, 108, 108, 108, 108, 108, 108, 108, 108, 108, 108, 108, 108, 108, 108, 108, 15980, 108, 108, 108, 16236, 13420, 13676, 13932, 14188, 14444, 14700, 14956, 15212, 15468, 15724, 108, 108, 108, 25452, 108, 108, 108, 108, 364, 620, 876, 1132, 1388, 1644, 1900, 2156, 2412, 2668, 2924, 3180, 3436, 3692, 3948, 4204, 4460, 4716, 4972, 5228, 5484, 5740, 5996, 6252, 6508, 108, 108, 108, 108, 108, 108, 6764, 7020, 7276, 7532, 7788, 8044, 8300, 8556, 8812, 9068, 9324, 9580, 9836, 10092, 10348, 10604, 10860, 11116, 11372, 11628, 11884, 12140, 12396, 12652, 12908, 13164, 108, 108, 108, 108, 108, 108, 108, 108, 108, 108, 108, 108, 108, 108, 108, 108, 108, 108, 108, 108, 108, 108, 108, 108, 108, 108, 108, 108, 108, 108, 108, 108, 108, 108, 108, 108, 108, 108, 108, 108, 108, 108, 108, 108, 108, 108, 108, 108, 108, 108, 108, 108, 108, 108, 108, 108, 108, 108, 108, 108, 108, 108, 108, 108, 108, 108, 108, 108, 108, 108, 108, 108, 108, 108, 108, 108, 108, 108, 108, 108, 108, 108, 108, 108, 108, 108, 108, 108, 108, 108, 108, 108, 108, 108, 108, 108, 108, 108, 108, 108, 108, 108, 108, 108, 108, 108, 108, 108, 108, 108, 108, 108, 108, 108, 108, 108, 108, 108, 108, 108, 108, 108, 108, 108, 108, 108, 108, 108, 108, 108, 108, 108, 108, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 15984, 112, 112, 112, 16240, 13424, 13680, 13936, 14192, 14448, 14704, 14960, 15216, 15472, 15728, 112, 112, 112, 25456, 112, 112, 112, 112, 368, 624, 880, 1136, 1392, 1648, 1904, 2160, 2416, 2672, 2928, 3184, 3440, 3696, 3952, 4208, 4464, 4720, 4976, 5232, 5488, 5744, 6000, 6256, 6512, 112, 112, 112, 112, 112, 112, 6768, 7024, 7280, 7536, 7792, 8048, 8304, 8560, 8816, 9072, 9328, 9584, 9840, 10096, 10352, 10608, 10864, 11120, 11376, 11632, 11888, 12144, 12400, 12656, 12912, 13168, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 116, 116, 116, 116, 116, 116, 116, 116, 116, 116, 116, 116, 116, 116, 116, 116, 116, 116, 116, 116, 116, 116, 116, 116, 116, 116, 116, 116, 116, 116, 116, 116, 116, 116, 116, 116, 116, 116, 116, 116, 116, 116, 116, 15988, 116, 116, 116, 16244, 13428, 13684, 13940, 14196, 14452, 14708, 14964, 15220, 15476, 15732, 116, 116, 116, 25460, 116, 116, 116, 116, 372, 628, 884, 1140, 1396, 1652, 1908, 2164, 2420, 2676, 2932, 3188, 3444, 3700, 3956, 4212, 4468, 4724, 4980, 5236, 5492, 5748, 6004, 6260, 6516, 116, 116, 116, 116, 116, 116, 6772, 7028, 7284, 7540, 7796, 8052, 8308, 8564, 8820, 9076, 9332, 9588, 9844, 10100, 10356, 10612, 10868, 11124, 11380, 11636, 11892, 12148, 12404, 12660, 12916, 13172, 116, 116, 116, 116, 116, 116, 116, 116, 116, 116, 116, 116, 116, 116, 116, 116, 116, 116, 116, 116, 116, 116, 116, 116, 116, 116, 116, 116, 116, 116, 116, 116, 116, 116, 116, 116, 116, 116, 116, 116, 116, 116, 116, 116, 116, 116, 116, 116, 116, 116, 116, 116, 116, 116, 116, 116, 116, 116, 116, 116, 116, 116, 116, 116, 116, 116, 116, 116, 116, 116, 116, 116, 116, 116, 116, 116, 116, 116, 116, 116, 116, 116, 116, 116, 116, 116, 116, 116, 116, 116, 116, 116, 116, 116, 116, 116, 116, 116, 116, 116, 116, 116, 116, 116, 116, 116, 116, 116, 116, 116, 116, 116, 116, 116, 116, 116, 116, 116, 116, 116, 116, 116, 116, 116, 116, 116, 116, 116, 116, 116, 116, 116, 116, 120, 120, 120, 120, 120, 120, 120, 120, 120, 120, 120, 120, 120, 120, 120, 120, 120, 120, 120, 120, 120, 120, 120, 120, 120, 120, 120, 120, 120, 120, 120, 120, 120, 120, 120, 120, 120, 120, 120, 120, 120, 120, 120, 15992, 120, 120, 120, 16248, 13432, 13688, 13944, 14200, 14456, 14712, 14968, 15224, 15480, 15736, 120, 120, 120, 25464, 120, 120, 120, 120, 376, 632, 888, 1144, 1400, 1656, 1912, 2168, 2424, 2680, 2936, 3192, 3448, 3704, 3960, 4216, 4472, 4728, 4984, 5240, 5496, 5752, 6008, 6264, 6520, 120, 120, 120, 120, 120, 120, 6776, 7032, 7288, 7544, 7800, 8056, 8312, 8568, 8824, 9080, 9336, 9592, 9848, 10104, 10360, 10616, 10872, 11128, 11384, 11640, 11896, 12152, 12408, 12664, 12920, 13176, 120, 120, 120, 120, 120, 120, 120, 120, 120, 120, 120, 120, 120, 120, 120, 120, 120, 120, 120, 120, 120, 120, 120, 120, 120, 120, 120, 120, 120, 120, 120, 120, 120, 120, 120, 120, 120, 120, 120, 120, 120, 120, 120, 120, 120, 120, 120, 120, 120, 120, 120, 120, 120, 120, 120, 120, 120, 120, 120, 120, 120, 120, 120, 120, 120, 120, 120, 120, 120, 120, 120, 120, 120, 120, 120, 120, 120, 120, 120, 120, 120, 120, 120, 120, 120, 120, 120, 120, 120, 120, 120, 120, 120, 120, 120, 120, 120, 120, 120, 120, 120, 120, 120, 120, 120, 120, 120, 120, 120, 120, 120, 120, 120, 120, 120, 120, 120, 120, 120, 120, 120, 120, 120, 120, 120, 120, 120, 120, 120, 120, 120, 120, 120, 124, 124, 124, 124, 124, 124, 124, 124, 124, 124, 124, 124, 124, 124, 124, 124, 124, 124, 124, 124, 124, 124, 124, 124, 124, 124, 124, 124, 124, 124, 124, 124, 124, 124, 124, 124, 124, 124, 124, 124, 124, 124, 124, 15996, 124, 124, 124, 16252, 13436, 13692, 13948, 14204, 14460, 14716, 14972, 15228, 15484, 15740, 124, 124, 124, 25468, 124, 124, 124, 124, 380, 636, 892, 1148, 1404, 1660, 1916, 2172, 2428, 2684, 2940, 3196, 3452, 3708, 3964, 4220, 4476, 4732, 4988, 5244, 5500, 5756, 6012, 6268, 6524, 124, 124, 124, 124, 124, 124, 6780, 7036, 7292, 7548, 7804, 8060, 8316, 8572, 8828, 9084, 9340, 9596, 9852, 10108, 10364, 10620, 10876, 11132, 11388, 11644, 11900, 12156, 12412, 12668, 12924, 13180, 124, 124, 124, 124, 124, 124, 124, 124, 124, 124, 124, 124, 124, 124, 124, 124, 124, 124, 124, 124, 124, 124, 124, 124, 124, 124, 124, 124, 124, 124, 124, 124, 124, 124, 124, 124, 124, 124, 124, 124, 124, 124, 124, 124, 124, 124, 124, 124, 124, 124, 124, 124, 124, 124, 124, 124, 124, 124, 124, 124, 124, 124, 124, 124, 124, 124, 124, 124, 124, 124, 124, 124, 124, 124, 124, 124, 124, 124, 124, 124, 124, 124, 124, 124, 124, 124, 124, 124, 124, 124, 124, 124, 124, 124, 124, 124, 124, 124, 124, 124, 124, 124, 124, 124, 124, 124, 124, 124, 124, 124, 124, 124, 124, 124, 124, 124, 124, 124, 124, 124, 124, 124, 124, 124, 124, 124, 124, 124, 124, 124, 124, 124, 124, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 16000, 128, 128, 128, 16256, 13440, 13696, 13952, 14208, 14464, 14720, 14976, 15232, 15488, 15744, 128, 128, 128, 25472, 128, 128, 128, 128, 384, 640, 896, 1152, 1408, 1664, 1920, 2176, 2432, 2688, 2944, 3200, 3456, 3712, 3968, 4224, 4480, 4736, 4992, 5248, 5504, 5760, 6016, 6272, 6528, 128, 128, 128, 128, 128, 128, 6784, 7040, 7296, 7552, 7808, 8064, 8320, 8576, 8832, 9088, 9344, 9600, 9856, 10112, 10368, 10624, 10880, 11136, 11392, 11648, 11904, 12160, 12416, 12672, 12928, 13184, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 132, 132, 132, 132, 132, 132, 132, 132, 132, 132, 132, 132, 132, 132, 132, 132, 132, 132, 132, 132, 132, 132, 132, 132, 132, 132, 132, 132, 132, 132, 132, 132, 132, 132, 132, 132, 132, 132, 132, 132, 132, 132, 132, 16004, 132, 132, 132, 16260, 13444, 13700, 13956, 14212, 14468, 14724, 14980, 15236, 15492, 15748, 132, 132, 132, 25476, 132, 132, 132, 132, 388, 644, 900, 1156, 1412, 1668, 1924, 2180, 2436, 2692, 2948, 3204, 3460, 3716, 3972, 4228, 4484, 4740, 4996, 5252, 5508, 5764, 6020, 6276, 6532, 132, 132, 132, 132, 132, 132, 6788, 7044, 7300, 7556, 7812, 8068, 8324, 8580, 8836, 9092, 9348, 9604, 9860, 10116, 10372, 10628, 10884, 11140, 11396, 11652, 11908, 12164, 12420, 12676, 12932, 13188, 132, 132, 132, 132, 132, 132, 132, 132, 132, 132, 132, 132, 132, 132, 132, 132, 132, 132, 132, 132, 132, 132, 132, 132, 132, 132, 132, 132, 132, 132, 132, 132, 132, 132, 132, 132, 132, 132, 132, 132, 132, 132, 132, 132, 132, 132, 132, 132, 132, 132, 132, 132, 132, 132, 132, 132, 132, 132, 132, 132, 132, 132, 132, 132, 132, 132, 132, 132, 132, 132, 132, 132, 132, 132, 132, 132, 132, 132, 132, 132, 132, 132, 132, 132, 132, 132, 132, 132, 132, 132, 132, 132, 132, 132, 132, 132, 132, 132, 132, 132, 132, 132, 132, 132, 132, 132, 132, 132, 132, 132, 132, 132, 132, 132, 132, 132, 132, 132, 132, 132, 132, 132, 132, 132, 132, 132, 132, 132, 132, 132, 132, 132, 132, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 16008, 136, 136, 136, 16264, 13448, 13704, 13960, 14216, 14472, 14728, 14984, 15240, 15496, 15752, 136, 136, 136, 25480, 136, 136, 136, 136, 392, 648, 904, 1160, 1416, 1672, 1928, 2184, 2440, 2696, 2952, 3208, 3464, 3720, 3976, 4232, 4488, 4744, 5000, 5256, 5512, 5768, 6024, 6280, 6536, 136, 136, 136, 136, 136, 136, 6792, 7048, 7304, 7560, 7816, 8072, 8328, 8584, 8840, 9096, 9352, 9608, 9864, 10120, 10376, 10632, 10888, 11144, 11400, 11656, 11912, 12168, 12424, 12680, 12936, 13192, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 140, 140, 140, 140, 140, 140, 140, 140, 140, 140, 140, 140, 140, 140, 140, 140, 140, 140, 140, 140, 140, 140, 140, 140, 140, 140, 140, 140, 140, 140, 140, 140, 140, 140, 140, 140, 140, 140, 140, 140, 140, 140, 140, 16012, 140, 140, 140, 16268, 13452, 13708, 13964, 14220, 14476, 14732, 14988, 15244, 15500, 15756, 140, 140, 140, 25484, 140, 140, 140, 140, 396, 652, 908, 1164, 1420, 1676, 1932, 2188, 2444, 2700, 2956, 3212, 3468, 3724, 3980, 4236, 4492, 4748, 5004, 5260, 5516, 5772, 6028, 6284, 6540, 140, 140, 140, 140, 140, 140, 6796, 7052, 7308, 7564, 7820, 8076, 8332, 8588, 8844, 9100, 9356, 9612, 9868, 10124, 10380, 10636, 10892, 11148, 11404, 11660, 11916, 12172, 12428, 12684, 12940, 13196, 140, 140, 140, 140, 140, 140, 140, 140, 140, 140, 140, 140, 140, 140, 140, 140, 140, 140, 140, 140, 140, 140, 140, 140, 140, 140, 140, 140, 140, 140, 140, 140, 140, 140, 140, 140, 140, 140, 140, 140, 140, 140, 140, 140, 140, 140, 140, 140, 140, 140, 140, 140, 140, 140, 140, 140, 140, 140, 140, 140, 140, 140, 140, 140, 140, 140, 140, 140, 140, 140, 140, 140, 140, 140, 140, 140, 140, 140, 140, 140, 140, 140, 140, 140, 140, 140, 140, 140, 140, 140, 140, 140, 140, 140, 140, 140, 140, 140, 140, 140, 140, 140, 140, 140, 140, 140, 140, 140, 140, 140, 140, 140, 140, 140, 140, 140, 140, 140, 140, 140, 140, 140, 140, 140, 140, 140, 140, 140, 140, 140, 140, 140, 140, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 16016, 144, 144, 144, 16272, 13456, 13712, 13968, 14224, 14480, 14736, 14992, 15248, 15504, 15760, 144, 144, 144, 25488, 144, 144, 144, 144, 400, 656, 912, 1168, 1424, 1680, 1936, 2192, 2448, 2704, 2960, 3216, 3472, 3728, 3984, 4240, 4496, 4752, 5008, 5264, 5520, 5776, 6032, 6288, 6544, 144, 144, 144, 144, 144, 144, 6800, 7056, 7312, 7568, 7824, 8080, 8336, 8592, 8848, 9104, 9360, 9616, 9872, 10128, 10384, 10640, 10896, 11152, 11408, 11664, 11920, 12176, 12432, 12688, 12944, 13200, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 148, 148, 148, 148, 148, 148, 148, 148, 148, 148, 148, 148, 148, 148, 148, 148, 148, 148, 148, 148, 148, 148, 148, 148, 148, 148, 148, 148, 148, 148, 148, 148, 148, 148, 148, 148, 148, 148, 148, 148, 148, 148, 148, 16020, 148, 148, 148, 16276, 13460, 13716, 13972, 14228, 14484, 14740, 14996, 15252, 15508, 15764, 148, 148, 148, 25492, 148, 148, 148, 148, 404, 660, 916, 1172, 1428, 1684, 1940, 2196, 2452, 2708, 2964, 3220, 3476, 3732, 3988, 4244, 4500, 4756, 5012, 5268, 5524, 5780, 6036, 6292, 6548, 148, 148, 148, 148, 148, 148, 6804, 7060, 7316, 7572, 7828, 8084, 8340, 8596, 8852, 9108, 9364, 9620, 9876, 10132, 10388, 10644, 10900, 11156, 11412, 11668, 11924, 12180, 12436, 12692, 12948, 13204, 148, 148, 148, 148, 148, 148, 148, 148, 148, 148, 148, 148, 148, 148, 148, 148, 148, 148, 148, 148, 148, 148, 148, 148, 148, 148, 148, 148, 148, 148, 148, 148, 148, 148, 148, 148, 148, 148, 148, 148, 148, 148, 148, 148, 148, 148, 148, 148, 148, 148, 148, 148, 148, 148, 148, 148, 148, 148, 148, 148, 148, 148, 148, 148, 148, 148, 148, 148, 148, 148, 148, 148, 148, 148, 148, 148, 148, 148, 148, 148, 148, 148, 148, 148, 148, 148, 148, 148, 148, 148, 148, 148, 148, 148, 148, 148, 148, 148, 148, 148, 148, 148, 148, 148, 148, 148, 148, 148, 148, 148, 148, 148, 148, 148, 148, 148, 148, 148, 148, 148, 148, 148, 148, 148, 148, 148, 148, 148, 148, 148, 148, 148, 148, 152, 152, 152, 152, 152, 152, 152, 152, 152, 152, 152, 152, 152, 152, 152, 152, 152, 152, 152, 152, 152, 152, 152, 152, 152, 152, 152, 152, 152, 152, 152, 152, 152, 152, 152, 152, 152, 152, 152, 152, 152, 152, 152, 16024, 152, 152, 152, 16280, 13464, 13720, 13976, 14232, 14488, 14744, 15000, 15256, 15512, 15768, 152, 152, 152, 25496, 152, 152, 152, 152, 408, 664, 920, 1176, 1432, 1688, 1944, 2200, 2456, 2712, 2968, 3224, 3480, 3736, 3992, 4248, 4504, 4760, 5016, 5272, 5528, 5784, 6040, 6296, 6552, 152, 152, 152, 152, 152, 152, 6808, 7064, 7320, 7576, 7832, 8088, 8344, 8600, 8856, 9112, 9368, 9624, 9880, 10136, 10392, 10648, 10904, 11160, 11416, 11672, 11928, 12184, 12440, 12696, 12952, 13208, 152, 152, 152, 152, 152, 152, 152, 152, 152, 152, 152, 152, 152, 152, 152, 152, 152, 152, 152, 152, 152, 152, 152, 152, 152, 152, 152, 152, 152, 152, 152, 152, 152, 152, 152, 152, 152, 152, 152, 152, 152, 152, 152, 152, 152, 152, 152, 152, 152, 152, 152, 152, 152, 152, 152, 152, 152, 152, 152, 152, 152, 152, 152, 152, 152, 152, 152, 152, 152, 152, 152, 152, 152, 152, 152, 152, 152, 152, 152, 152, 152, 152, 152, 152, 152, 152, 152, 152, 152, 152, 152, 152, 152, 152, 152, 152, 152, 152, 152, 152, 152, 152, 152, 152, 152, 152, 152, 152, 152, 152, 152, 152, 152, 152, 152, 152, 152, 152, 152, 152, 152, 152, 152, 152, 152, 152, 152, 152, 152, 152, 152, 152, 152, 156, 156, 156, 156, 156, 156, 156, 156, 156, 156, 156, 156, 156, 156, 156, 156, 156, 156, 156, 156, 156, 156, 156, 156, 156, 156, 156, 156, 156, 156, 156, 156, 156, 156, 156, 156, 156, 156, 156, 156, 156, 156, 156, 16028, 156, 156, 156, 16284, 13468, 13724, 13980, 14236, 14492, 14748, 15004, 15260, 15516, 15772, 156, 156, 156, 25500, 156, 156, 156, 156, 412, 668, 924, 1180, 1436, 1692, 1948, 2204, 2460, 2716, 2972, 3228, 3484, 3740, 3996, 4252, 4508, 4764, 5020, 5276, 5532, 5788, 6044, 6300, 6556, 156, 156, 156, 156, 156, 156, 6812, 7068, 7324, 7580, 7836, 8092, 8348, 8604, 8860, 9116, 9372, 9628, 9884, 10140, 10396, 10652, 10908, 11164, 11420, 11676, 11932, 12188, 12444, 12700, 12956, 13212, 156, 156, 156, 156, 156, 156, 156, 156, 156, 156, 156, 156, 156, 156, 156, 156, 156, 156, 156, 156, 156, 156, 156, 156, 156, 156, 156, 156, 156, 156, 156, 156, 156, 156, 156, 156, 156, 156, 156, 156, 156, 156, 156, 156, 156, 156, 156, 156, 156, 156, 156, 156, 156, 156, 156, 156, 156, 156, 156, 156, 156, 156, 156, 156, 156, 156, 156, 156, 156, 156, 156, 156, 156, 156, 156, 156, 156, 156, 156, 156, 156, 156, 156, 156, 156, 156, 156, 156, 156, 156, 156, 156, 156, 156, 156, 156, 156, 156, 156, 156, 156, 156, 156, 156, 156, 156, 156, 156, 156, 156, 156, 156, 156, 156, 156, 156, 156, 156, 156, 156, 156, 156, 156, 156, 156, 156, 156, 156, 156, 156, 156, 156, 156, 160, 160, 160, 160, 160, 160, 160, 160, 160, 160, 160, 160, 160, 160, 160, 160, 160, 160, 160, 160, 160, 160, 160, 160, 160, 160, 160, 160, 160, 160, 160, 160, 160, 160, 160, 160, 160, 160, 160, 160, 160, 160, 160, 16032, 160, 160, 160, 16288, 13472, 13728, 13984, 14240, 14496, 14752, 15008, 15264, 15520, 15776, 160, 160, 160, 25504, 160, 160, 160, 160, 416, 672, 928, 1184, 1440, 1696, 1952, 2208, 2464, 2720, 2976, 3232, 3488, 3744, 4000, 4256, 4512, 4768, 5024, 5280, 5536, 5792, 6048, 6304, 6560, 160, 160, 160, 160, 160, 160, 6816, 7072, 7328, 7584, 7840, 8096, 8352, 8608, 8864, 9120, 9376, 9632, 9888, 10144, 10400, 10656, 10912, 11168, 11424, 11680, 11936, 12192, 12448, 12704, 12960, 13216, 160, 160, 160, 160, 160, 160, 160, 160, 160, 160, 160, 160, 160, 160, 160, 160, 160, 160, 160, 160, 160, 160, 160, 160, 160, 160, 160, 160, 160, 160, 160, 160, 160, 160, 160, 160, 160, 160, 160, 160, 160, 160, 160, 160, 160, 160, 160, 160, 160, 160, 160, 160, 160, 160, 160, 160, 160, 160, 160, 160, 160, 160, 160, 160, 160, 160, 160, 160, 160, 160, 160, 160, 160, 160, 160, 160, 160, 160, 160, 160, 160, 160, 160, 160, 160, 160, 160, 160, 160, 160, 160, 160, 160, 160, 160, 160, 160, 160, 160, 160, 160, 160, 160, 160, 160, 160, 160, 160, 160, 160, 160, 160, 160, 160, 160, 160, 160, 160, 160, 160, 160, 160, 160, 160, 160, 160, 160, 160, 160, 160, 160, 160, 160, 164, 164, 164, 164, 164, 164, 164, 164, 164, 164, 164, 164, 164, 164, 164, 164, 164, 164, 164, 164, 164, 164, 164, 164, 164, 164, 164, 164, 164, 164, 164, 164, 164, 164, 164, 164, 164, 164, 164, 164, 164, 164, 164, 16036, 164, 164, 164, 16292, 13476, 13732, 13988, 14244, 14500, 14756, 15012, 15268, 15524, 15780, 164, 164, 164, 25508, 164, 164, 164, 164, 420, 676, 932, 1188, 1444, 1700, 1956, 2212, 2468, 2724, 2980, 3236, 3492, 3748, 4004, 4260, 4516, 4772, 5028, 5284, 5540, 5796, 6052, 6308, 6564, 164, 164, 164, 164, 164, 164, 6820, 7076, 7332, 7588, 7844, 8100, 8356, 8612, 8868, 9124, 9380, 9636, 9892, 10148, 10404, 10660, 10916, 11172, 11428, 11684, 11940, 12196, 12452, 12708, 12964, 13220, 164, 164, 164, 164, 164, 164, 164, 164, 164, 164, 164, 164, 164, 164, 164, 164, 164, 164, 164, 164, 164, 164, 164, 164, 164, 164, 164, 164, 164, 164, 164, 164, 164, 164, 164, 164, 164, 164, 164, 164, 164, 164, 164, 164, 164, 164, 164, 164, 164, 164, 164, 164, 164, 164, 164, 164, 164, 164, 164, 164, 164, 164, 164, 164, 164, 164, 164, 164, 164, 164, 164, 164, 164, 164, 164, 164, 164, 164, 164, 164, 164, 164, 164, 164, 164, 164, 164, 164, 164, 164, 164, 164, 164, 164, 164, 164, 164, 164, 164, 164, 164, 164, 164, 164, 164, 164, 164, 164, 164, 164, 164, 164, 164, 164, 164, 164, 164, 164, 164, 164, 164, 164, 164, 164, 164, 164, 164, 164, 164, 164, 164, 164, 164, 168, 168, 168, 168, 168, 168, 168, 168, 168, 168, 168, 168, 168, 168, 168, 168, 168, 168, 168, 168, 168, 168, 168, 168, 168, 168, 168, 168, 168, 168, 168, 168, 168, 168, 168, 168, 168, 168, 168, 168, 168, 168, 168, 16040, 168, 168, 168, 16296, 13480, 13736, 13992, 14248, 14504, 14760, 15016, 15272, 15528, 15784, 168, 168, 168, 25512, 168, 168, 168, 168, 424, 680, 936, 1192, 1448, 1704, 1960, 2216, 2472, 2728, 2984, 3240, 3496, 3752, 4008, 4264, 4520, 4776, 5032, 5288, 5544, 5800, 6056, 6312, 6568, 168, 168, 168, 168, 168, 168, 6824, 7080, 7336, 7592, 7848, 8104, 8360, 8616, 8872, 9128, 9384, 9640, 9896, 10152, 10408, 10664, 10920, 11176, 11432, 11688, 11944, 12200, 12456, 12712, 12968, 13224, 168, 168, 168, 168, 168, 168, 168, 168, 168, 168, 168, 168, 168, 168, 168, 168, 168, 168, 168, 168, 168, 168, 168, 168, 168, 168, 168, 168, 168, 168, 168, 168, 168, 168, 168, 168, 168, 168, 168, 168, 168, 168, 168, 168, 168, 168, 168, 168, 168, 168, 168, 168, 168, 168, 168, 168, 168, 168, 168, 168, 168, 168, 168, 168, 168, 168, 168, 168, 168, 168, 168, 168, 168, 168, 168, 168, 168, 168, 168, 168, 168, 168, 168, 168, 168, 168, 168, 168, 168, 168, 168, 168, 168, 168, 168, 168, 168, 168, 168, 168, 168, 168, 168, 168, 168, 168, 168, 168, 168, 168, 168, 168, 168, 168, 168, 168, 168, 168, 168, 168, 168, 168, 168, 168, 168, 168, 168, 168, 168, 168, 168, 168, 168, 172, 172, 172, 172, 172, 172, 172, 172, 172, 172, 172, 172, 172, 172, 172, 172, 172, 172, 172, 172, 172, 172, 172, 172, 172, 172, 172, 172, 172, 172, 172, 172, 172, 172, 172, 172, 172, 172, 172, 172, 172, 172, 172, 16044, 172, 172, 172, 16300, 13484, 13740, 13996, 14252, 14508, 14764, 15020, 15276, 15532, 15788, 172, 172, 172, 25516, 172, 172, 172, 172, 428, 684, 940, 1196, 1452, 1708, 1964, 2220, 2476, 2732, 2988, 3244, 3500, 3756, 4012, 4268, 4524, 4780, 5036, 5292, 5548, 5804, 6060, 6316, 6572, 172, 172, 172, 172, 172, 172, 6828, 7084, 7340, 7596, 7852, 8108, 8364, 8620, 8876, 9132, 9388, 9644, 9900, 10156, 10412, 10668, 10924, 11180, 11436, 11692, 11948, 12204, 12460, 12716, 12972, 13228, 172, 172, 172, 172, 172, 172, 172, 172, 172, 172, 172, 172, 172, 172, 172, 172, 172, 172, 172, 172, 172, 172, 172, 172, 172, 172, 172, 172, 172, 172, 172, 172, 172, 172, 172, 172, 172, 172, 172, 172, 172, 172, 172, 172, 172, 172, 172, 172, 172, 172, 172, 172, 172, 172, 172, 172, 172, 172, 172, 172, 172, 172, 172, 172, 172, 172, 172, 172, 172, 172, 172, 172, 172, 172, 172, 172, 172, 172, 172, 172, 172, 172, 172, 172, 172, 172, 172, 172, 172, 172, 172, 172, 172, 172, 172, 172, 172, 172, 172, 172, 172, 172, 172, 172, 172, 172, 172, 172, 172, 172, 172, 172, 172, 172, 172, 172, 172, 172, 172, 172, 172, 172, 172, 172, 172, 172, 172, 172, 172, 172, 172, 172, 172, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 16048, 176, 176, 176, 16304, 13488, 13744, 14000, 14256, 14512, 14768, 15024, 15280, 15536, 15792, 176, 176, 176, 25520, 176, 176, 176, 176, 432, 688, 944, 1200, 1456, 1712, 1968, 2224, 2480, 2736, 2992, 3248, 3504, 3760, 4016, 4272, 4528, 4784, 5040, 5296, 5552, 5808, 6064, 6320, 6576, 176, 176, 176, 176, 176, 176, 6832, 7088, 7344, 7600, 7856, 8112, 8368, 8624, 8880, 9136, 9392, 9648, 9904, 10160, 10416, 10672, 10928, 11184, 11440, 11696, 11952, 12208, 12464, 12720, 12976, 13232, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 180, 180, 180, 180, 180, 180, 180, 180, 180, 180, 180, 180, 180, 180, 180, 180, 180, 180, 180, 180, 180, 180, 180, 180, 180, 180, 180, 180, 180, 180, 180, 180, 180, 180, 180, 180, 180, 180, 180, 180, 180, 180, 180, 16052, 180, 180, 180, 16308, 13492, 13748, 14004, 14260, 14516, 14772, 15028, 15284, 15540, 15796, 180, 180, 180, 25524, 180, 180, 180, 180, 436, 692, 948, 1204, 1460, 1716, 1972, 2228, 2484, 2740, 2996, 3252, 3508, 3764, 4020, 4276, 4532, 4788, 5044, 5300, 5556, 5812, 6068, 6324, 6580, 180, 180, 180, 180, 180, 180, 6836, 7092, 7348, 7604, 7860, 8116, 8372, 8628, 8884, 9140, 9396, 9652, 9908, 10164, 10420, 10676, 10932, 11188, 11444, 11700, 11956, 12212, 12468, 12724, 12980, 13236, 180, 180, 180, 180, 180, 180, 180, 180, 180, 180, 180, 180, 180, 180, 180, 180, 180, 180, 180, 180, 180, 180, 180, 180, 180, 180, 180, 180, 180, 180, 180, 180, 180, 180, 180, 180, 180, 180, 180, 180, 180, 180, 180, 180, 180, 180, 180, 180, 180, 180, 180, 180, 180, 180, 180, 180, 180, 180, 180, 180, 180, 180, 180, 180, 180, 180, 180, 180, 180, 180, 180, 180, 180, 180, 180, 180, 180, 180, 180, 180, 180, 180, 180, 180, 180, 180, 180, 180, 180, 180, 180, 180, 180, 180, 180, 180, 180, 180, 180, 180, 180, 180, 180, 180, 180, 180, 180, 180, 180, 180, 180, 180, 180, 180, 180, 180, 180, 180, 180, 180, 180, 180, 180, 180, 180, 180, 180, 180, 180, 180, 180, 180, 180, 184, 184, 184, 184, 184, 184, 184, 184, 184, 184, 184, 184, 184, 184, 184, 184, 184, 184, 184, 184, 184, 184, 184, 184, 184, 184, 184, 184, 184, 184, 184, 184, 184, 184, 184, 184, 184, 184, 184, 184, 184, 184, 184, 16056, 184, 184, 184, 16312, 13496, 13752, 14008, 14264, 14520, 14776, 15032, 15288, 15544, 15800, 184, 184, 184, 25528, 184, 184, 184, 184, 440, 696, 952, 1208, 1464, 1720, 1976, 2232, 2488, 2744, 3000, 3256, 3512, 3768, 4024, 4280, 4536, 4792, 5048, 5304, 5560, 5816, 6072, 6328, 6584, 184, 184, 184, 184, 184, 184, 6840, 7096, 7352, 7608, 7864, 8120, 8376, 8632, 8888, 9144, 9400, 9656, 9912, 10168, 10424, 10680, 10936, 11192, 11448, 11704, 11960, 12216, 12472, 12728, 12984, 13240, 184, 184, 184, 184, 184, 184, 184, 184, 184, 184, 184, 184, 184, 184, 184, 184, 184, 184, 184, 184, 184, 184, 184, 184, 184, 184, 184, 184, 184, 184, 184, 184, 184, 184, 184, 184, 184, 184, 184, 184, 184, 184, 184, 184, 184, 184, 184, 184, 184, 184, 184, 184, 184, 184, 184, 184, 184, 184, 184, 184, 184, 184, 184, 184, 184, 184, 184, 184, 184, 184, 184, 184, 184, 184, 184, 184, 184, 184, 184, 184, 184, 184, 184, 184, 184, 184, 184, 184, 184, 184, 184, 184, 184, 184, 184, 184, 184, 184, 184, 184, 184, 184, 184, 184, 184, 184, 184, 184, 184, 184, 184, 184, 184, 184, 184, 184, 184, 184, 184, 184, 184, 184, 184, 184, 184, 184, 184, 184, 184, 184, 184, 184, 184, 188, 188, 188, 188, 188, 188, 188, 188, 188, 188, 188, 188, 188, 188, 188, 188, 188, 188, 188, 188, 188, 188, 188, 188, 188, 188, 188, 188, 188, 188, 188, 188, 188, 188, 188, 188, 188, 188, 188, 188, 188, 188, 188, 16060, 188, 188, 188, 16316, 13500, 13756, 14012, 14268, 14524, 14780, 15036, 15292, 15548, 15804, 188, 188, 188, 25532, 188, 188, 188, 188, 444, 700, 956, 1212, 1468, 1724, 1980, 2236, 2492, 2748, 3004, 3260, 3516, 3772, 4028, 4284, 4540, 4796, 5052, 5308, 5564, 5820, 6076, 6332, 6588, 188, 188, 188, 188, 188, 188, 6844, 7100, 7356, 7612, 7868, 8124, 8380, 8636, 8892, 9148, 9404, 9660, 9916, 10172, 10428, 10684, 10940, 11196, 11452, 11708, 11964, 12220, 12476, 12732, 12988, 13244, 188, 188, 188, 188, 188, 188, 188, 188, 188, 188, 188, 188, 188, 188, 188, 188, 188, 188, 188, 188, 188, 188, 188, 188, 188, 188, 188, 188, 188, 188, 188, 188, 188, 188, 188, 188, 188, 188, 188, 188, 188, 188, 188, 188, 188, 188, 188, 188, 188, 188, 188, 188, 188, 188, 188, 188, 188, 188, 188, 188, 188, 188, 188, 188, 188, 188, 188, 188, 188, 188, 188, 188, 188, 188, 188, 188, 188, 188, 188, 188, 188, 188, 188, 188, 188, 188, 188, 188, 188, 188, 188, 188, 188, 188, 188, 188, 188, 188, 188, 188, 188, 188, 188, 188, 188, 188, 188, 188, 188, 188, 188, 188, 188, 188, 188, 188, 188, 188, 188, 188, 188, 188, 188, 188, 188, 188, 188, 188, 188, 188, 188, 188, 188, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 16064, 192, 192, 192, 16320, 13504, 13760, 14016, 14272, 14528, 14784, 15040, 15296, 15552, 15808, 192, 192, 192, 25536, 192, 192, 192, 192, 448, 704, 960, 1216, 1472, 1728, 1984, 2240, 2496, 2752, 3008, 3264, 3520, 3776, 4032, 4288, 4544, 4800, 5056, 5312, 5568, 5824, 6080, 6336, 6592, 192, 192, 192, 192, 192, 192, 6848, 7104, 7360, 7616, 7872, 8128, 8384, 8640, 8896, 9152, 9408, 9664, 9920, 10176, 10432, 10688, 10944, 11200, 11456, 11712, 11968, 12224, 12480, 12736, 12992, 13248, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 196, 196, 196, 196, 196, 196, 196, 196, 196, 196, 196, 196, 196, 196, 196, 196, 196, 196, 196, 196, 196, 196, 196, 196, 196, 196, 196, 196, 196, 196, 196, 196, 196, 196, 196, 196, 196, 196, 196, 196, 196, 196, 196, 16068, 196, 196, 196, 16324, 13508, 13764, 14020, 14276, 14532, 14788, 15044, 15300, 15556, 15812, 196, 196, 196, 25540, 196, 196, 196, 196, 452, 708, 964, 1220, 1476, 1732, 1988, 2244, 2500, 2756, 3012, 3268, 3524, 3780, 4036, 4292, 4548, 4804, 5060, 5316, 5572, 5828, 6084, 6340, 6596, 196, 196, 196, 196, 196, 196, 6852, 7108, 7364, 7620, 7876, 8132, 8388, 8644, 8900, 9156, 9412, 9668, 9924, 10180, 10436, 10692, 10948, 11204, 11460, 11716, 11972, 12228, 12484, 12740, 12996, 13252, 196, 196, 196, 196, 196, 196, 196, 196, 196, 196, 196, 196, 196, 196, 196, 196, 196, 196, 196, 196, 196, 196, 196, 196, 196, 196, 196, 196, 196, 196, 196, 196, 196, 196, 196, 196, 196, 196, 196, 196, 196, 196, 196, 196, 196, 196, 196, 196, 196, 196, 196, 196, 196, 196, 196, 196, 196, 196, 196, 196, 196, 196, 196, 196, 196, 196, 196, 196, 196, 196, 196, 196, 196, 196, 196, 196, 196, 196, 196, 196, 196, 196, 196, 196, 196, 196, 196, 196, 196, 196, 196, 196, 196, 196, 196, 196, 196, 196, 196, 196, 196, 196, 196, 196, 196, 196, 196, 196, 196, 196, 196, 196, 196, 196, 196, 196, 196, 196, 196, 196, 196, 196, 196, 196, 196, 196, 196, 196, 196, 196, 196, 196, 196, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 16072, 200, 200, 200, 16328, 13512, 13768, 14024, 14280, 14536, 14792, 15048, 15304, 15560, 15816, 200, 200, 200, 25544, 200, 200, 200, 200, 456, 712, 968, 1224, 1480, 1736, 1992, 2248, 2504, 2760, 3016, 3272, 3528, 3784, 4040, 4296, 4552, 4808, 5064, 5320, 5576, 5832, 6088, 6344, 6600, 200, 200, 200, 200, 200, 200, 6856, 7112, 7368, 7624, 7880, 8136, 8392, 8648, 8904, 9160, 9416, 9672, 9928, 10184, 10440, 10696, 10952, 11208, 11464, 11720, 11976, 12232, 12488, 12744, 13000, 13256, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 16076, 204, 204, 204, 16332, 13516, 13772, 14028, 14284, 14540, 14796, 15052, 15308, 15564, 15820, 204, 204, 204, 25548, 204, 204, 204, 204, 460, 716, 972, 1228, 1484, 1740, 1996, 2252, 2508, 2764, 3020, 3276, 3532, 3788, 4044, 4300, 4556, 4812, 5068, 5324, 5580, 5836, 6092, 6348, 6604, 204, 204, 204, 204, 204, 204, 6860, 7116, 7372, 7628, 7884, 8140, 8396, 8652, 8908, 9164, 9420, 9676, 9932, 10188, 10444, 10700, 10956, 11212, 11468, 11724, 11980, 12236, 12492, 12748, 13004, 13260, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15872, 0, 0, 0, 16128, 13312, 13568, 13824, 14080, 14336, 14592, 14848, 15104, 15360, 15616, 0, 0, 0, 25344, 0, 0, 0, 0, 256, 512, 768, 1024, 1280, 1536, 1792, 2048, 2304, 2560, 2816, 3072, 3328, 3584, 3840, 4096, 4352, 4608, 4864, 5120, 5376, 5632, 5888, 6144, 6400, 0, 0, 0, 0, 0, 0, 6656, 6912, 7168, 7424, 7680, 7936, 8192, 8448, 8704, 8960, 9216, 9472, 9728, 9984, 10240, 10496, 10752, 11008, 11264, 11520, 11776, 12032, 12288, 12544, 12800, 13056, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15872, 0, 0, 0, 16128, 13312, 13568, 13824, 14080, 14336, 14592, 14848, 15104, 15360, 15616, 0, 0, 0, 25344, 0, 0, 0, 0, 256, 512, 768, 1024, 1280, 1536, 1792, 2048, 2304, 2560, 2816, 3072, 3328, 3584, 3840, 4096, 4352, 4608, 4864, 5120, 5376, 5632, 5888, 6144, 6400, 0, 0, 0, 0, 0, 0, 6656, 6912, 7168, 7424, 7680, 7936, 8192, 8448, 8704, 8960, 9216, 9472, 9728, 9984, 10240, 10496, 10752, 11008, 11264, 11520, 11776, 12032, 12288, 12544, 12800, 13056, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15872, 0, 0, 0, 16128, 13312, 13568, 13824, 14080, 14336, 14592, 14848, 15104, 15360, 15616, 0, 0, 0, 25344, 0, 0, 0, 0, 256, 512, 768, 1024, 1280, 1536, 1792, 2048, 2304, 2560, 2816, 3072, 3328, 3584, 3840, 4096, 4352, 4608, 4864, 5120, 5376, 5632, 5888, 6144, 6400, 0, 0, 0, 0, 0, 0, 6656, 6912, 7168, 7424, 7680, 7936, 8192, 8448, 8704, 8960, 9216, 9472, 9728, 9984, 10240, 10496, 10752, 11008, 11264, 11520, 11776, 12032, 12288, 12544, 12800, 13056, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15872, 0, 0, 0, 16128, 13312, 13568, 13824, 14080, 14336, 14592, 14848, 15104, 15360, 15616, 0, 0, 0, 25344, 0, 0, 0, 0, 256, 512, 768, 1024, 1280, 1536, 1792, 2048, 2304, 2560, 2816, 3072, 3328, 3584, 3840, 4096, 4352, 4608, 4864, 5120, 5376, 5632, 5888, 6144, 6400, 0, 0, 0, 0, 0, 0, 6656, 6912, 7168, 7424, 7680, 7936, 8192, 8448, 8704, 8960, 9216, 9472, 9728, 9984, 10240, 10496, 10752, 11008, 11264, 11520, 11776, 12032, 12288, 12544, 12800, 13056, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15872, 0, 0, 0, 16128, 13312, 13568, 13824, 14080, 14336, 14592, 14848, 15104, 15360, 15616, 0, 0, 0, 25344, 0, 0, 0, 0, 256, 512, 768, 1024, 1280, 1536, 1792, 2048, 2304, 2560, 2816, 3072, 3328, 3584, 3840, 4096, 4352, 4608, 4864, 5120, 5376, 5632, 5888, 6144, 6400, 0, 0, 0, 0, 0, 0, 6656, 6912, 7168, 7424, 7680, 7936, 8192, 8448, 8704, 8960, 9216, 9472, 9728, 9984, 10240, 10496, 10752, 11008, 11264, 11520, 11776, 12032, 12288, 12544, 12800, 13056, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15872, 0, 0, 0, 16128, 13312, 13568, 13824, 14080, 14336, 14592, 14848, 15104, 15360, 15616, 0, 0, 0, 25344, 0, 0, 0, 0, 256, 512, 768, 1024, 1280, 1536, 1792, 2048, 2304, 2560, 2816, 3072, 3328, 3584, 3840, 4096, 4352, 4608, 4864, 5120, 5376, 5632, 5888, 6144, 6400, 0, 0, 0, 0, 0, 0, 6656, 6912, 7168, 7424, 7680, 7936, 8192, 8448, 8704, 8960, 9216, 9472, 9728, 9984, 10240, 10496, 10752, 11008, 11264, 11520, 11776, 12032, 12288, 12544, 12800, 13056, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15872, 0, 0, 0, 16128, 13312, 13568, 13824, 14080, 14336, 14592, 14848, 15104, 15360, 15616, 0, 0, 0, 25344, 0, 0, 0, 0, 256, 512, 768, 1024, 1280, 1536, 1792, 2048, 2304, 2560, 2816, 3072, 3328, 3584, 3840, 4096, 4352, 4608, 4864, 5120, 5376, 5632, 5888, 6144, 6400, 0, 0, 0, 0, 0, 0, 6656, 6912, 7168, 7424, 7680, 7936, 8192, 8448, 8704, 8960, 9216, 9472, 9728, 9984, 10240, 10496, 10752, 11008, 11264, 11520, 11776, 12032, 12288, 12544, 12800, 13056, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15872, 0, 0, 0, 16128, 13312, 13568, 13824, 14080, 14336, 14592, 14848, 15104, 15360, 15616, 0, 0, 0, 25344, 0, 0, 0, 0, 256, 512, 768, 1024, 1280, 1536, 1792, 2048, 2304, 2560, 2816, 3072, 3328, 3584, 3840, 4096, 4352, 4608, 4864, 5120, 5376, 5632, 5888, 6144, 6400, 0, 0, 0, 0, 0, 0, 6656, 6912, 7168, 7424, 7680, 7936, 8192, 8448, 8704, 8960, 9216, 9472, 9728, 9984, 10240, 10496, 10752, 11008, 11264, 11520, 11776, 12032, 12288, 12544, 12800, 13056, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15872, 0, 0, 0, 16128, 13312, 13568, 13824, 14080, 14336, 14592, 14848, 15104, 15360, 15616, 0, 0, 0, 25344, 0, 0, 0, 0, 256, 512, 768, 1024, 1280, 1536, 1792, 2048, 2304, 2560, 2816, 3072, 3328, 3584, 3840, 4096, 4352, 4608, 4864, 5120, 5376, 5632, 5888, 6144, 6400, 0, 0, 0, 0, 0, 0, 6656, 6912, 7168, 7424, 7680, 7936, 8192, 8448, 8704, 8960, 9216, 9472, 9728, 9984, 10240, 10496, 10752, 11008, 11264, 11520, 11776, 12032, 12288, 12544, 12800, 13056, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15872, 0, 0, 0, 16128, 13312, 13568, 13824, 14080, 14336, 14592, 14848, 15104, 15360, 15616, 0, 0, 0, 25344, 0, 0, 0, 0, 256, 512, 768, 1024, 1280, 1536, 1792, 2048, 2304, 2560, 2816, 3072, 3328, 3584, 3840, 4096, 4352, 4608, 4864, 5120, 5376, 5632, 5888, 6144, 6400, 0, 0, 0, 0, 0, 0, 6656, 6912, 7168, 7424, 7680, 7936, 8192, 8448, 8704, 8960, 9216, 9472, 9728, 9984, 10240, 10496, 10752, 11008, 11264, 11520, 11776, 12032, 12288, 12544, 12800, 13056, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15872, 0, 0, 0, 16128, 13312, 13568, 13824, 14080, 14336, 14592, 14848, 15104, 15360, 15616, 0, 0, 0, 25344, 0, 0, 0, 0, 256, 512, 768, 1024, 1280, 1536, 1792, 2048, 2304, 2560, 2816, 3072, 3328, 3584, 3840, 4096, 4352, 4608, 4864, 5120, 5376, 5632, 5888, 6144, 6400, 0, 0, 0, 0, 0, 0, 6656, 6912, 7168, 7424, 7680, 7936, 8192, 8448, 8704, 8960, 9216, 9472, 9728, 9984, 10240, 10496, 10752, 11008, 11264, 11520, 11776, 12032, 12288, 12544, 12800, 13056, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15872, 0, 0, 0, 16128, 13312, 13568, 13824, 14080, 14336, 14592, 14848, 15104, 15360, 15616, 0, 0, 0, 25344, 0, 0, 0, 0, 256, 512, 768, 1024, 1280, 1536, 1792, 2048, 2304, 2560, 2816, 3072, 3328, 3584, 3840, 4096, 4352, 4608, 4864, 5120, 5376, 5632, 5888, 6144, 6400, 0, 0, 0, 0, 0, 0, 6656, 6912, 7168, 7424, 7680, 7936, 8192, 8448, 8704, 8960, 9216, 9472, 9728, 9984, 10240, 10496, 10752, 11008, 11264, 11520, 11776, 12032, 12288, 12544, 12800, 13056, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15872, 0, 0, 0, 16128, 13312, 13568, 13824, 14080, 14336, 14592, 14848, 15104, 15360, 15616, 0, 0, 0, 25344, 0, 0, 0, 0, 256, 512, 768, 1024, 1280, 1536, 1792, 2048, 2304, 2560, 2816, 3072, 3328, 3584, 3840, 4096, 4352, 4608, 4864, 5120, 5376, 5632, 5888, 6144, 6400, 0, 0, 0, 0, 0, 0, 6656, 6912, 7168, 7424, 7680, 7936, 8192, 8448, 8704, 8960, 9216, 9472, 9728, 9984, 10240, 10496, 10752, 11008, 11264, 11520, 11776, 12032, 12288, 12544, 12800, 13056, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15872, 0, 0, 0, 16128, 13312, 13568, 13824, 14080, 14336, 14592, 14848, 15104, 15360, 15616, 0, 0, 0, 25344, 0, 0, 0, 0, 256, 512, 768, 1024, 1280, 1536, 1792, 2048, 2304, 2560, 2816, 3072, 3328, 3584, 3840, 4096, 4352, 4608, 4864, 5120, 5376, 5632, 5888, 6144, 6400, 0, 0, 0, 0, 0, 0, 6656, 6912, 7168, 7424, 7680, 7936, 8192, 8448, 8704, 8960, 9216, 9472, 9728, 9984, 10240, 10496, 10752, 11008, 11264, 11520, 11776, 12032, 12288, 12544, 12800, 13056, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15872, 0, 0, 0, 16128, 13312, 13568, 13824, 14080, 14336, 14592, 14848, 15104, 15360, 15616, 0, 0, 0, 25344, 0, 0, 0, 0, 256, 512, 768, 1024, 1280, 1536, 1792, 2048, 2304, 2560, 2816, 3072, 3328, 3584, 3840, 4096, 4352, 4608, 4864, 5120, 5376, 5632, 5888, 6144, 6400, 0, 0, 0, 0, 0, 0, 6656, 6912, 7168, 7424, 7680, 7936, 8192, 8448, 8704, 8960, 9216, 9472, 9728, 9984, 10240, 10496, 10752, 11008, 11264, 11520, 11776, 12032, 12288, 12544, 12800, 13056, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15872, 0, 0, 0, 16128, 13312, 13568, 13824, 14080, 14336, 14592, 14848, 15104, 15360, 15616, 0, 0, 0, 25344, 0, 0, 0, 0, 256, 512, 768, 1024, 1280, 1536, 1792, 2048, 2304, 2560, 2816, 3072, 3328, 3584, 3840, 4096, 4352, 4608, 4864, 5120, 5376, 5632, 5888, 6144, 6400, 0, 0, 0, 0, 0, 0, 6656, 6912, 7168, 7424, 7680, 7936, 8192, 8448, 8704, 8960, 9216, 9472, 9728, 9984, 10240, 10496, 10752, 11008, 11264, 11520, 11776, 12032, 12288, 12544, 12800, 13056, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15872, 0, 0, 0, 16128, 13312, 13568, 13824, 14080, 14336, 14592, 14848, 15104, 15360, 15616, 0, 0, 0, 25344, 0, 0, 0, 0, 256, 512, 768, 1024, 1280, 1536, 1792, 2048, 2304, 2560, 2816, 3072, 3328, 3584, 3840, 4096, 4352, 4608, 4864, 5120, 5376, 5632, 5888, 6144, 6400, 0, 0, 0, 0, 0, 0, 6656, 6912, 7168, 7424, 7680, 7936, 8192, 8448, 8704, 8960, 9216, 9472, 9728, 9984, 10240, 10496, 10752, 11008, 11264, 11520, 11776, 12032, 12288, 12544, 12800, 13056, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15872, 0, 0, 0, 16128, 13312, 13568, 13824, 14080, 14336, 14592, 14848, 15104, 15360, 15616, 0, 0, 0, 25344, 0, 0, 0, 0, 256, 512, 768, 1024, 1280, 1536, 1792, 2048, 2304, 2560, 2816, 3072, 3328, 3584, 3840, 4096, 4352, 4608, 4864, 5120, 5376, 5632, 5888, 6144, 6400, 0, 0, 0, 0, 0, 0, 6656, 6912, 7168, 7424, 7680, 7936, 8192, 8448, 8704, 8960, 9216, 9472, 9728, 9984, 10240, 10496, 10752, 11008, 11264, 11520, 11776, 12032, 12288, 12544, 12800, 13056, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15872, 0, 0, 0, 16128, 13312, 13568, 13824, 14080, 14336, 14592, 14848, 15104, 15360, 15616, 0, 0, 0, 25344, 0, 0, 0, 0, 256, 512, 768, 1024, 1280, 1536, 1792, 2048, 2304, 2560, 2816, 3072, 3328, 3584, 3840, 4096, 4352, 4608, 4864, 5120, 5376, 5632, 5888, 6144, 6400, 0, 0, 0, 0, 0, 0, 6656, 6912, 7168, 7424, 7680, 7936, 8192, 8448, 8704, 8960, 9216, 9472, 9728, 9984, 10240, 10496, 10752, 11008, 11264, 11520, 11776, 12032, 12288, 12544, 12800, 13056, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15872, 0, 0, 0, 16128, 13312, 13568, 13824, 14080, 14336, 14592, 14848, 15104, 15360, 15616, 0, 0, 0, 25344, 0, 0, 0, 0, 256, 512, 768, 1024, 1280, 1536, 1792, 2048, 2304, 2560, 2816, 3072, 3328, 3584, 3840, 4096, 4352, 4608, 4864, 5120, 5376, 5632, 5888, 6144, 6400, 0, 0, 0, 0, 0, 0, 6656, 6912, 7168, 7424, 7680, 7936, 8192, 8448, 8704, 8960, 9216, 9472, 9728, 9984, 10240, 10496, 10752, 11008, 11264, 11520, 11776, 12032, 12288, 12544, 12800, 13056, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15872, 0, 0, 0, 16128, 13312, 13568, 13824, 14080, 14336, 14592, 14848, 15104, 15360, 15616, 0, 0, 0, 25344, 0, 0, 0, 0, 256, 512, 768, 1024, 1280, 1536, 1792, 2048, 2304, 2560, 2816, 3072, 3328, 3584, 3840, 4096, 4352, 4608, 4864, 5120, 5376, 5632, 5888, 6144, 6400, 0, 0, 0, 0, 0, 0, 6656, 6912, 7168, 7424, 7680, 7936, 8192, 8448, 8704, 8960, 9216, 9472, 9728, 9984, 10240, 10496, 10752, 11008, 11264, 11520, 11776, 12032, 12288, 12544, 12800, 13056, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15872, 0, 0, 0, 16128, 13312, 13568, 13824, 14080, 14336, 14592, 14848, 15104, 15360, 15616, 0, 0, 0, 25344, 0, 0, 0, 0, 256, 512, 768, 1024, 1280, 1536, 1792, 2048, 2304, 2560, 2816, 3072, 3328, 3584, 3840, 4096, 4352, 4608, 4864, 5120, 5376, 5632, 5888, 6144, 6400, 0, 0, 0, 0, 0, 0, 6656, 6912, 7168, 7424, 7680, 7936, 8192, 8448, 8704, 8960, 9216, 9472, 9728, 9984, 10240, 10496, 10752, 11008, 11264, 11520, 11776, 12032, 12288, 12544, 12800, 13056, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15872, 0, 0, 0, 16128, 13312, 13568, 13824, 14080, 14336, 14592, 14848, 15104, 15360, 15616, 0, 0, 0, 25344, 0, 0, 0, 0, 256, 512, 768, 1024, 1280, 1536, 1792, 2048, 2304, 2560, 2816, 3072, 3328, 3584, 3840, 4096, 4352, 4608, 4864, 5120, 5376, 5632, 5888, 6144, 6400, 0, 0, 0, 0, 0, 0, 6656, 6912, 7168, 7424, 7680, 7936, 8192, 8448, 8704, 8960, 9216, 9472, 9728, 9984, 10240, 10496, 10752, 11008, 11264, 11520, 11776, 12032, 12288, 12544, 12800, 13056, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15872, 0, 0, 0, 16128, 13312, 13568, 13824, 14080, 14336, 14592, 14848, 15104, 15360, 15616, 0, 0, 0, 25344, 0, 0, 0, 0, 256, 512, 768, 1024, 1280, 1536, 1792, 2048, 2304, 2560, 2816, 3072, 3328, 3584, 3840, 4096, 4352, 4608, 4864, 5120, 5376, 5632, 5888, 6144, 6400, 0, 0, 0, 0, 0, 0, 6656, 6912, 7168, 7424, 7680, 7936, 8192, 8448, 8704, 8960, 9216, 9472, 9728, 9984, 10240, 10496, 10752, 11008, 11264, 11520, 11776, 12032, 12288, 12544, 12800, 13056, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15872, 0, 0, 0, 16128, 13312, 13568, 13824, 14080, 14336, 14592, 14848, 15104, 15360, 15616, 0, 0, 0, 25344, 0, 0, 0, 0, 256, 512, 768, 1024, 1280, 1536, 1792, 2048, 2304, 2560, 2816, 3072, 3328, 3584, 3840, 4096, 4352, 4608, 4864, 5120, 5376, 5632, 5888, 6144, 6400, 0, 0, 0, 0, 0, 0, 6656, 6912, 7168, 7424, 7680, 7936, 8192, 8448, 8704, 8960, 9216, 9472, 9728, 9984, 10240, 10496, 10752, 11008, 11264, 11520, 11776, 12032, 12288, 12544, 12800, 13056, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15872, 0, 0, 0, 16128, 13312, 13568, 13824, 14080, 14336, 14592, 14848, 15104, 15360, 15616, 0, 0, 0, 25344, 0, 0, 0, 0, 256, 512, 768, 1024, 1280, 1536, 1792, 2048, 2304, 2560, 2816, 3072, 3328, 3584, 3840, 4096, 4352, 4608, 4864, 5120, 5376, 5632, 5888, 6144, 6400, 0, 0, 0, 0, 0, 0, 6656, 6912, 7168, 7424, 7680, 7936, 8192, 8448, 8704, 8960, 9216, 9472, 9728, 9984, 10240, 10496, 10752, 11008, 11264, 11520, 11776, 12032, 12288, 12544, 12800, 13056, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15872, 0, 0, 0, 16128, 13312, 13568, 13824, 14080, 14336, 14592, 14848, 15104, 15360, 15616, 0, 0, 0, 25344, 0, 0, 0, 0, 256, 512, 768, 1024, 1280, 1536, 1792, 2048, 2304, 2560, 2816, 3072, 3328, 3584, 3840, 4096, 4352, 4608, 4864, 5120, 5376, 5632, 5888, 6144, 6400, 0, 0, 0, 0, 0, 0, 6656, 6912, 7168, 7424, 7680, 7936, 8192, 8448, 8704, 8960, 9216, 9472, 9728, 9984, 10240, 10496, 10752, 11008, 11264, 11520, 11776, 12032, 12288, 12544, 12800, 13056, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15872, 0, 0, 0, 16128, 13312, 13568, 13824, 14080, 14336, 14592, 14848, 15104, 15360, 15616, 0, 0, 0, 25344, 0, 0, 0, 0, 256, 512, 768, 1024, 1280, 1536, 1792, 2048, 2304, 2560, 2816, 3072, 3328, 3584, 3840, 4096, 4352, 4608, 4864, 5120, 5376, 5632, 5888, 6144, 6400, 0, 0, 0, 0, 0, 0, 6656, 6912, 7168, 7424, 7680, 7936, 8192, 8448, 8704, 8960, 9216, 9472, 9728, 9984, 10240, 10496, 10752, 11008, 11264, 11520, 11776, 12032, 12288, 12544, 12800, 13056, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15872, 0, 0, 0, 16128, 13312, 13568, 13824, 14080, 14336, 14592, 14848, 15104, 15360, 15616, 0, 0, 0, 25344, 0, 0, 0, 0, 256, 512, 768, 1024, 1280, 1536, 1792, 2048, 2304, 2560, 2816, 3072, 3328, 3584, 3840, 4096, 4352, 4608, 4864, 5120, 5376, 5632, 5888, 6144, 6400, 0, 0, 0, 0, 0, 0, 6656, 6912, 7168, 7424, 7680, 7936, 8192, 8448, 8704, 8960, 9216, 9472, 9728, 9984, 10240, 10496, 10752, 11008, 11264, 11520, 11776, 12032, 12288, 12544, 12800, 13056, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15872, 0, 0, 0, 16128, 13312, 13568, 13824, 14080, 14336, 14592, 14848, 15104, 15360, 15616, 0, 0, 0, 25344, 0, 0, 0, 0, 256, 512, 768, 1024, 1280, 1536, 1792, 2048, 2304, 2560, 2816, 3072, 3328, 3584, 3840, 4096, 4352, 4608, 4864, 5120, 5376, 5632, 5888, 6144, 6400, 0, 0, 0, 0, 0, 0, 6656, 6912, 7168, 7424, 7680, 7936, 8192, 8448, 8704, 8960, 9216, 9472, 9728, 9984, 10240, 10496, 10752, 11008, 11264, 11520, 11776, 12032, 12288, 12544, 12800, 13056, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15872, 0, 0, 0, 16128, 13312, 13568, 13824, 14080, 14336, 14592, 14848, 15104, 15360, 15616, 0, 0, 0, 25344, 0, 0, 0, 0, 256, 512, 768, 1024, 1280, 1536, 1792, 2048, 2304, 2560, 2816, 3072, 3328, 3584, 3840, 4096, 4352, 4608, 4864, 5120, 5376, 5632, 5888, 6144, 6400, 0, 0, 0, 0, 0, 0, 6656, 6912, 7168, 7424, 7680, 7936, 8192, 8448, 8704, 8960, 9216, 9472, 9728, 9984, 10240, 10496, 10752, 11008, 11264, 11520, 11776, 12032, 12288, 12544, 12800, 13056, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15872, 0, 0, 0, 16128, 13312, 13568, 13824, 14080, 14336, 14592, 14848, 15104, 15360, 15616, 0, 0, 0, 25344, 0, 0, 0, 0, 256, 512, 768, 1024, 1280, 1536, 1792, 2048, 2304, 2560, 2816, 3072, 3328, 3584, 3840, 4096, 4352, 4608, 4864, 5120, 5376, 5632, 5888, 6144, 6400, 0, 0, 0, 0, 0, 0, 6656, 6912, 7168, 7424, 7680, 7936, 8192, 8448, 8704, 8960, 9216, 9472, 9728, 9984, 10240, 10496, 10752, 11008, 11264, 11520, 11776, 12032, 12288, 12544, 12800, 13056, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15872, 0, 0, 0, 16128, 13312, 13568, 13824, 14080, 14336, 14592, 14848, 15104, 15360, 15616, 0, 0, 0, 25344, 0, 0, 0, 0, 256, 512, 768, 1024, 1280, 1536, 1792, 2048, 2304, 2560, 2816, 3072, 3328, 3584, 3840, 4096, 4352, 4608, 4864, 5120, 5376, 5632, 5888, 6144, 6400, 0, 0, 0, 0, 0, 0, 6656, 6912, 7168, 7424, 7680, 7936, 8192, 8448, 8704, 8960, 9216, 9472, 9728, 9984, 10240, 10496, 10752, 11008, 11264, 11520, 11776, 12032, 12288, 12544, 12800, 13056, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15872, 0, 0, 0, 16128, 13312, 13568, 13824, 14080, 14336, 14592, 14848, 15104, 15360, 15616, 0, 0, 0, 25344, 0, 0, 0, 0, 256, 512, 768, 1024, 1280, 1536, 1792, 2048, 2304, 2560, 2816, 3072, 3328, 3584, 3840, 4096, 4352, 4608, 4864, 5120, 5376, 5632, 5888, 6144, 6400, 0, 0, 0, 0, 0, 0, 6656, 6912, 7168, 7424, 7680, 7936, 8192, 8448, 8704, 8960, 9216, 9472, 9728, 9984, 10240, 10496, 10752, 11008, 11264, 11520, 11776, 12032, 12288, 12544, 12800, 13056, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15872, 0, 0, 0, 16128, 13312, 13568, 13824, 14080, 14336, 14592, 14848, 15104, 15360, 15616, 0, 0, 0, 25344, 0, 0, 0, 0, 256, 512, 768, 1024, 1280, 1536, 1792, 2048, 2304, 2560, 2816, 3072, 3328, 3584, 3840, 4096, 4352, 4608, 4864, 5120, 5376, 5632, 5888, 6144, 6400, 0, 0, 0, 0, 0, 0, 6656, 6912, 7168, 7424, 7680, 7936, 8192, 8448, 8704, 8960, 9216, 9472, 9728, 9984, 10240, 10496, 10752, 11008, 11264, 11520, 11776, 12032, 12288, 12544, 12800, 13056, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15872, 0, 0, 0, 16128, 13312, 13568, 13824, 14080, 14336, 14592, 14848, 15104, 15360, 15616, 0, 0, 0, 25344, 0, 0, 0, 0, 256, 512, 768, 1024, 1280, 1536, 1792, 2048, 2304, 2560, 2816, 3072, 3328, 3584, 3840, 4096, 4352, 4608, 4864, 5120, 5376, 5632, 5888, 6144, 6400, 0, 0, 0, 0, 0, 0, 6656, 6912, 7168, 7424, 7680, 7936, 8192, 8448, 8704, 8960, 9216, 9472, 9728, 9984, 10240, 10496, 10752, 11008, 11264, 11520, 11776, 12032, 12288, 12544, 12800, 13056, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15872, 0, 0, 0, 16128, 13312, 13568, 13824, 14080, 14336, 14592, 14848, 15104, 15360, 15616, 0, 0, 0, 25344, 0, 0, 0, 0, 256, 512, 768, 1024, 1280, 1536, 1792, 2048, 2304, 2560, 2816, 3072, 3328, 3584, 3840, 4096, 4352, 4608, 4864, 5120, 5376, 5632, 5888, 6144, 6400, 0, 0, 0, 0, 0, 0, 6656, 6912, 7168, 7424, 7680, 7936, 8192, 8448, 8704, 8960, 9216, 9472, 9728, 9984, 10240, 10496, 10752, 11008, 11264, 11520, 11776, 12032, 12288, 12544, 12800, 13056, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15872, 0, 0, 0, 16128, 13312, 13568, 13824, 14080, 14336, 14592, 14848, 15104, 15360, 15616, 0, 0, 0, 25344, 0, 0, 0, 0, 256, 512, 768, 1024, 1280, 1536, 1792, 2048, 2304, 2560, 2816, 3072, 3328, 3584, 3840, 4096, 4352, 4608, 4864, 5120, 5376, 5632, 5888, 6144, 6400, 0, 0, 0, 0, 0, 0, 6656, 6912, 7168, 7424, 7680, 7936, 8192, 8448, 8704, 8960, 9216, 9472, 9728, 9984, 10240, 10496, 10752, 11008, 11264, 11520, 11776, 12032, 12288, 12544, 12800, 13056, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15872, 0, 0, 0, 16128, 13312, 13568, 13824, 14080, 14336, 14592, 14848, 15104, 15360, 15616, 0, 0, 0, 25344, 0, 0, 0, 0, 256, 512, 768, 1024, 1280, 1536, 1792, 2048, 2304, 2560, 2816, 3072, 3328, 3584, 3840, 4096, 4352, 4608, 4864, 5120, 5376, 5632, 5888, 6144, 6400, 0, 0, 0, 0, 0, 0, 6656, 6912, 7168, 7424, 7680, 7936, 8192, 8448, 8704, 8960, 9216, 9472, 9728, 9984, 10240, 10496, 10752, 11008, 11264, 11520, 11776, 12032, 12288, 12544, 12800, 13056, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15872, 0, 0, 0, 16128, 13312, 13568, 13824, 14080, 14336, 14592, 14848, 15104, 15360, 15616, 0, 0, 0, 25344, 0, 0, 0, 0, 256, 512, 768, 1024, 1280, 1536, 1792, 2048, 2304, 2560, 2816, 3072, 3328, 3584, 3840, 4096, 4352, 4608, 4864, 5120, 5376, 5632, 5888, 6144, 6400, 0, 0, 0, 0, 0, 0, 6656, 6912, 7168, 7424, 7680, 7936, 8192, 8448, 8704, 8960, 9216, 9472, 9728, 9984, 10240, 10496, 10752, 11008, 11264, 11520, 11776, 12032, 12288, 12544, 12800, 13056, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15872, 0, 0, 0, 16128, 13312, 13568, 13824, 14080, 14336, 14592, 14848, 15104, 15360, 15616, 0, 0, 0, 25344, 0, 0, 0, 0, 256, 512, 768, 1024, 1280, 1536, 1792, 2048, 2304, 2560, 2816, 3072, 3328, 3584, 3840, 4096, 4352, 4608, 4864, 5120, 5376, 5632, 5888, 6144, 6400, 0, 0, 0, 0, 0, 0, 6656, 6912, 7168, 7424, 7680, 7936, 8192, 8448, 8704, 8960, 9216, 9472, 9728, 9984, 10240, 10496, 10752, 11008, 11264, 11520, 11776, 12032, 12288, 12544, 12800, 13056, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15872, 0, 0, 0, 16128, 13312, 13568, 13824, 14080, 14336, 14592, 14848, 15104, 15360, 15616, 0, 0, 0, 25344, 0, 0, 0, 0, 256, 512, 768, 1024, 1280, 1536, 1792, 2048, 2304, 2560, 2816, 3072, 3328, 3584, 3840, 4096, 4352, 4608, 4864, 5120, 5376, 5632, 5888, 6144, 6400, 0, 0, 0, 0, 0, 0, 6656, 6912, 7168, 7424, 7680, 7936, 8192, 8448, 8704, 8960, 9216, 9472, 9728, 9984, 10240, 10496, 10752, 11008, 11264, 11520, 11776, 12032, 12288, 12544, 12800, 13056, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15872, 0, 0, 0, 16128, 13312, 13568, 13824, 14080, 14336, 14592, 14848, 15104, 15360, 15616, 0, 0, 0, 25344, 0, 0, 0, 0, 256, 512, 768, 1024, 1280, 1536, 1792, 2048, 2304, 2560, 2816, 3072, 3328, 3584, 3840, 4096, 4352, 4608, 4864, 5120, 5376, 5632, 5888, 6144, 6400, 0, 0, 0, 0, 0, 0, 6656, 6912, 7168, 7424, 7680, 7936, 8192, 8448, 8704, 8960, 9216, 9472, 9728, 9984, 10240, 10496, 10752, 11008, 11264, 11520, 11776, 12032, 12288, 12544, 12800, 13056, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15872, 0, 0, 0, 16128, 13312, 13568, 13824, 14080, 14336, 14592, 14848, 15104, 15360, 15616, 0, 0, 0, 25344, 0, 0, 0, 0, 256, 512, 768, 1024, 1280, 1536, 1792, 2048, 2304, 2560, 2816, 3072, 3328, 3584, 3840, 4096, 4352, 4608, 4864, 5120, 5376, 5632, 5888, 6144, 6400, 0, 0, 0, 0, 0, 0, 6656, 6912, 7168, 7424, 7680, 7936, 8192, 8448, 8704, 8960, 9216, 9472, 9728, 9984, 10240, 10496, 10752, 11008, 11264, 11520, 11776, 12032, 12288, 12544, 12800, 13056, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15872, 0, 0, 0, 16128, 13312, 13568, 13824, 14080, 14336, 14592, 14848, 15104, 15360, 15616, 0, 0, 0, 25344, 0, 0, 0, 0, 256, 512, 768, 1024, 1280, 1536, 1792, 2048, 2304, 2560, 2816, 3072, 3328, 3584, 3840, 4096, 4352, 4608, 4864, 5120, 5376, 5632, 5888, 6144, 6400, 0, 0, 0, 0, 0, 0, 6656, 6912, 7168, 7424, 7680, 7936, 8192, 8448, 8704, 8960, 9216, 9472, 9728, 9984, 10240, 10496, 10752, 11008, 11264, 11520, 11776, 12032, 12288, 12544, 12800, 13056, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15872, 0, 0, 0, 16128, 13312, 13568, 13824, 14080, 14336, 14592, 14848, 15104, 15360, 15616, 0, 0, 0, 25344, 0, 0, 0, 0, 256, 512, 768, 1024, 1280, 1536, 1792, 2048, 2304, 2560, 2816, 3072, 3328, 3584, 3840, 4096, 4352, 4608, 4864, 5120, 5376, 5632, 5888, 6144, 6400, 0, 0, 0, 0, 0, 0, 6656, 6912, 7168, 7424, 7680, 7936, 8192, 8448, 8704, 8960, 9216, 9472, 9728, 9984, 10240, 10496, 10752, 11008, 11264, 11520, 11776, 12032, 12288, 12544, 12800, 13056, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15872, 0, 0, 0, 16128, 13312, 13568, 13824, 14080, 14336, 14592, 14848, 15104, 15360, 15616, 0, 0, 0, 25344, 0, 0, 0, 0, 256, 512, 768, 1024, 1280, 1536, 1792, 2048, 2304, 2560, 2816, 3072, 3328, 3584, 3840, 4096, 4352, 4608, 4864, 5120, 5376, 5632, 5888, 6144, 6400, 0, 0, 0, 0, 0, 0, 6656, 6912, 7168, 7424, 7680, 7936, 8192, 8448, 8704, 8960, 9216, 9472, 9728, 9984, 10240, 10496, 10752, 11008, 11264, 11520, 11776, 12032, 12288, 12544, 12800, 13056, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15872, 0, 0, 0, 16128, 13312, 13568, 13824, 14080, 14336, 14592, 14848, 15104, 15360, 15616, 0, 0, 0, 25344, 0, 0, 0, 0, 256, 512, 768, 1024, 1280, 1536, 1792, 2048, 2304, 2560, 2816, 3072, 3328, 3584, 3840, 4096, 4352, 4608, 4864, 5120, 5376, 5632, 5888, 6144, 6400, 0, 0, 0, 0, 0, 0, 6656, 6912, 7168, 7424, 7680, 7936, 8192, 8448, 8704, 8960, 9216, 9472, 9728, 9984, 10240, 10496, 10752, 11008, 11264, 11520, 11776, 12032, 12288, 12544, 12800, 13056, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15872, 0, 0, 0, 16128, 13312, 13568, 13824, 14080, 14336, 14592, 14848, 15104, 15360, 15616, 0, 0, 0, 25344, 0, 0, 0, 0, 256, 512, 768, 1024, 1280, 1536, 1792, 2048, 2304, 2560, 2816, 3072, 3328, 3584, 3840, 4096, 4352, 4608, 4864, 5120, 5376, 5632, 5888, 6144, 6400, 0, 0, 0, 0, 0, 0, 6656, 6912, 7168, 7424, 7680, 7936, 8192, 8448, 8704, 8960, 9216, 9472, 9728, 9984, 10240, 10496, 10752, 11008, 11264, 11520, 11776, 12032, 12288, 12544, 12800, 13056, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15872, 0, 0, 0, 16128, 13312, 13568, 13824, 14080, 14336, 14592, 14848, 15104, 15360, 15616, 0, 0, 0, 25344, 0, 0, 0, 0, 256, 512, 768, 1024, 1280, 1536, 1792, 2048, 2304, 2560, 2816, 3072, 3328, 3584, 3840, 4096, 4352, 4608, 4864, 5120, 5376, 5632, 5888, 6144, 6400, 0, 0, 0, 0, 0, 0, 6656, 6912, 7168, 7424, 7680, 7936, 8192, 8448, 8704, 8960, 9216, 9472, 9728, 9984, 10240, 10496, 10752, 11008, 11264, 11520, 11776, 12032, 12288, 12544, 12800, 13056, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15872, 0, 0, 0, 16128, 13312, 13568, 13824, 14080, 14336, 14592, 14848, 15104, 15360, 15616, 0, 0, 0, 25344, 0, 0, 0, 0, 256, 512, 768, 1024, 1280, 1536, 1792, 2048, 2304, 2560, 2816, 3072, 3328, 3584, 3840, 4096, 4352, 4608, 4864, 5120, 5376, 5632, 5888, 6144, 6400, 0, 0, 0, 0, 0, 0, 6656, 6912, 7168, 7424, 7680, 7936, 8192, 8448, 8704, 8960, 9216, 9472, 9728, 9984, 10240, 10496, 10752, 11008, 11264, 11520, 11776, 12032, 12288, 12544, 12800, 13056, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15872, 0, 0, 0, 16128, 13312, 13568, 13824, 14080, 14336, 14592, 14848, 15104, 15360, 15616, 0, 0, 0, 25344, 0, 0, 0, 0, 256, 512, 768, 1024, 1280, 1536, 1792, 2048, 2304, 2560, 2816, 3072, 3328, 3584, 3840, 4096, 4352, 4608, 4864, 5120, 5376, 5632, 5888, 6144, 6400, 0, 0, 0, 0, 0, 0, 6656, 6912, 7168, 7424, 7680, 7936, 8192, 8448, 8704, 8960, 9216, 9472, 9728, 9984, 10240, 10496, 10752, 11008, 11264, 11520, 11776, 12032, 12288, 12544, 12800, 13056, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15872, 0, 0, 0, 16128, 13312, 13568, 13824, 14080, 14336, 14592, 14848, 15104, 15360, 15616, 0, 0, 0, 25344, 0, 0, 0, 0, 256, 512, 768, 1024, 1280, 1536, 1792, 2048, 2304, 2560, 2816, 3072, 3328, 3584, 3840, 4096, 4352, 4608, 4864, 5120, 5376, 5632, 5888, 6144, 6400, 0, 0, 0, 0, 0, 0, 6656, 6912, 7168, 7424, 7680, 7936, 8192, 8448, 8704, 8960, 9216, 9472, 9728, 9984, 10240, 10496, 10752, 11008, 11264, 11520, 11776, 12032, 12288, 12544, 12800, 13056, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15872, 0, 0, 0, 16128, 13312, 13568, 13824, 14080, 14336, 14592, 14848, 15104, 15360, 15616, 0, 0, 0, 25344, 0, 0, 0, 0, 256, 512, 768, 1024, 1280, 1536, 1792, 2048, 2304, 2560, 2816, 3072, 3328, 3584, 3840, 4096, 4352, 4608, 4864, 5120, 5376, 5632, 5888, 6144, 6400, 0, 0, 0, 0, 0, 0, 6656, 6912, 7168, 7424, 7680, 7936, 8192, 8448, 8704, 8960, 9216, 9472, 9728, 9984, 10240, 10496, 10752, 11008, 11264, 11520, 11776, 12032, 12288, 12544, 12800, 13056, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15872, 0, 0, 0, 16128, 13312, 13568, 13824, 14080, 14336, 14592, 14848, 15104, 15360, 15616, 0, 0, 0, 25344, 0, 0, 0, 0, 256, 512, 768, 1024, 1280, 1536, 1792, 2048, 2304, 2560, 2816, 3072, 3328, 3584, 3840, 4096, 4352, 4608, 4864, 5120, 5376, 5632, 5888, 6144, 6400, 0, 0, 0, 0, 0, 0, 6656, 6912, 7168, 7424, 7680, 7936, 8192, 8448, 8704, 8960, 9216, 9472, 9728, 9984, 10240, 10496, 10752, 11008, 11264, 11520, 11776, 12032, 12288, 12544, 12800, 13056, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15872, 0, 0, 0, 16128, 13312, 13568, 13824, 14080, 14336, 14592, 14848, 15104, 15360, 15616, 0, 0, 0, 25344, 0, 0, 0, 0, 256, 512, 768, 1024, 1280, 1536, 1792, 2048, 2304, 2560, 2816, 3072, 3328, 3584, 3840, 4096, 4352, 4608, 4864, 5120, 5376, 5632, 5888, 6144, 6400, 0, 0, 0, 0, 0, 0, 6656, 6912, 7168, 7424, 7680, 7936, 8192, 8448, 8704, 8960, 9216, 9472, 9728, 9984, 10240, 10496, 10752, 11008, 11264, 11520, 11776, 12032, 12288, 12544, 12800, 13056, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15872, 0, 0, 0, 16128, 13312, 13568, 13824, 14080, 14336, 14592, 14848, 15104, 15360, 15616, 0, 0, 0, 25344, 0, 0, 0, 0, 256, 512, 768, 1024, 1280, 1536, 1792, 2048, 2304, 2560, 2816, 3072, 3328, 3584, 3840, 4096, 4352, 4608, 4864, 5120, 5376, 5632, 5888, 6144, 6400, 0, 0, 0, 0, 0, 0, 6656, 6912, 7168, 7424, 7680, 7936, 8192, 8448, 8704, 8960, 9216, 9472, 9728, 9984, 10240, 10496, 10752, 11008, 11264, 11520, 11776, 12032, 12288, 12544, 12800, 13056, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15872, 0, 0, 0, 16128, 13312, 13568, 13824, 14080, 14336, 14592, 14848, 15104, 15360, 15616, 0, 0, 0, 25344, 0, 0, 0, 0, 256, 512, 768, 1024, 1280, 1536, 1792, 2048, 2304, 2560, 2816, 3072, 3328, 3584, 3840, 4096, 4352, 4608, 4864, 5120, 5376, 5632, 5888, 6144, 6400, 0, 0, 0, 0, 0, 0, 6656, 6912, 7168, 7424, 7680, 7936, 8192, 8448, 8704, 8960, 9216, 9472, 9728, 9984, 10240, 10496, 10752, 11008, 11264, 11520, 11776, 12032, 12288, 12544, 12800, 13056, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15872, 0, 0, 0, 16128, 13312, 13568, 13824, 14080, 14336, 14592, 14848, 15104, 15360, 15616, 0, 0, 0, 25344, 0, 0, 0, 0, 256, 512, 768, 1024, 1280, 1536, 1792, 2048, 2304, 2560, 2816, 3072, 3328, 3584, 3840, 4096, 4352, 4608, 4864, 5120, 5376, 5632, 5888, 6144, 6400, 0, 0, 0, 0, 0, 0, 6656, 6912, 7168, 7424, 7680, 7936, 8192, 8448, 8704, 8960, 9216, 9472, 9728, 9984, 10240, 10496, 10752, 11008, 11264, 11520, 11776, 12032, 12288, 12544, 12800, 13056, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15872, 0, 0, 0, 16128, 13312, 13568, 13824, 14080, 14336, 14592, 14848, 15104, 15360, 15616, 0, 0, 0, 25344, 0, 0, 0, 0, 256, 512, 768, 1024, 1280, 1536, 1792, 2048, 2304, 2560, 2816, 3072, 3328, 3584, 3840, 4096, 4352, 4608, 4864, 5120, 5376, 5632, 5888, 6144, 6400, 0, 0, 0, 0, 0, 0, 6656, 6912, 7168, 7424, 7680, 7936, 8192, 8448, 8704, 8960, 9216, 9472, 9728, 9984, 10240, 10496, 10752, 11008, 11264, 11520, 11776, 12032, 12288, 12544, 12800, 13056, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15872, 0, 0, 0, 16128, 13312, 13568, 13824, 14080, 14336, 14592, 14848, 15104, 15360, 15616, 0, 0, 0, 25344, 0, 0, 0, 0, 256, 512, 768, 1024, 1280, 1536, 1792, 2048, 2304, 2560, 2816, 3072, 3328, 3584, 3840, 4096, 4352, 4608, 4864, 5120, 5376, 5632, 5888, 6144, 6400, 0, 0, 0, 0, 0, 0, 6656, 6912, 7168, 7424, 7680, 7936, 8192, 8448, 8704, 8960, 9216, 9472, 9728, 9984, 10240, 10496, 10752, 11008, 11264, 11520, 11776, 12032, 12288, 12544, 12800, 13056, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15872, 0, 0, 0, 16128, 13312, 13568, 13824, 14080, 14336, 14592, 14848, 15104, 15360, 15616, 0, 0, 0, 25344, 0, 0, 0, 0, 256, 512, 768, 1024, 1280, 1536, 1792, 2048, 2304, 2560, 2816, 3072, 3328, 3584, 3840, 4096, 4352, 4608, 4864, 5120, 5376, 5632, 5888, 6144, 6400, 0, 0, 0, 0, 0, 0, 6656, 6912, 7168, 7424, 7680, 7936, 8192, 8448, 8704, 8960, 9216, 9472, 9728, 9984, 10240, 10496, 10752, 11008, 11264, 11520, 11776, 12032, 12288, 12544, 12800, 13056, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15872, 0, 0, 0, 16128, 13312, 13568, 13824, 14080, 14336, 14592, 14848, 15104, 15360, 15616, 0, 0, 0, 25344, 0, 0, 0, 0, 256, 512, 768, 1024, 1280, 1536, 1792, 2048, 2304, 2560, 2816, 3072, 3328, 3584, 3840, 4096, 4352, 4608, 4864, 5120, 5376, 5632, 5888, 6144, 6400, 0, 0, 0, 0, 0, 0, 6656, 6912, 7168, 7424, 7680, 7936, 8192, 8448, 8704, 8960, 9216, 9472, 9728, 9984, 10240, 10496, 10752, 11008, 11264, 11520, 11776, 12032, 12288, 12544, 12800, 13056, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15872, 0, 0, 0, 16128, 13312, 13568, 13824, 14080, 14336, 14592, 14848, 15104, 15360, 15616, 0, 0, 0, 25344, 0, 0, 0, 0, 256, 512, 768, 1024, 1280, 1536, 1792, 2048, 2304, 2560, 2816, 3072, 3328, 3584, 3840, 4096, 4352, 4608, 4864, 5120, 5376, 5632, 5888, 6144, 6400, 0, 0, 0, 0, 0, 0, 6656, 6912, 7168, 7424, 7680, 7936, 8192, 8448, 8704, 8960, 9216, 9472, 9728, 9984, 10240, 10496, 10752, 11008, 11264, 11520, 11776, 12032, 12288, 12544, 12800, 13056, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15872, 0, 0, 0, 16128, 13312, 13568, 13824, 14080, 14336, 14592, 14848, 15104, 15360, 15616, 0, 0, 0, 25344, 0, 0, 0, 0, 256, 512, 768, 1024, 1280, 1536, 1792, 2048, 2304, 2560, 2816, 3072, 3328, 3584, 3840, 4096, 4352, 4608, 4864, 5120, 5376, 5632, 5888, 6144, 6400, 0, 0, 0, 0, 0, 0, 6656, 6912, 7168, 7424, 7680, 7936, 8192, 8448, 8704, 8960, 9216, 9472, 9728, 9984, 10240, 10496, 10752, 11008, 11264, 11520, 11776, 12032, 12288, 12544, 12800, 13056, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15872, 0, 0, 0, 16128, 13312, 13568, 13824, 14080, 14336, 14592, 14848, 15104, 15360, 15616, 0, 0, 0, 25344, 0, 0, 0, 0, 256, 512, 768, 1024, 1280, 1536, 1792, 2048, 2304, 2560, 2816, 3072, 3328, 3584, 3840, 4096, 4352, 4608, 4864, 5120, 5376, 5632, 5888, 6144, 6400, 0, 0, 0, 0, 0, 0, 6656, 6912, 7168, 7424, 7680, 7936, 8192, 8448, 8704, 8960, 9216, 9472, 9728, 9984, 10240, 10496, 10752, 11008, 11264, 11520, 11776, 12032, 12288, 12544, 12800, 13056, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15872, 0, 0, 0, 16128, 13312, 13568, 13824, 14080, 14336, 14592, 14848, 15104, 15360, 15616, 0, 0, 0, 25344, 0, 0, 0, 0, 256, 512, 768, 1024, 1280, 1536, 1792, 2048, 2304, 2560, 2816, 3072, 3328, 3584, 3840, 4096, 4352, 4608, 4864, 5120, 5376, 5632, 5888, 6144, 6400, 0, 0, 0, 0, 0, 0, 6656, 6912, 7168, 7424, 7680, 7936, 8192, 8448, 8704, 8960, 9216, 9472, 9728, 9984, 10240, 10496, 10752, 11008, 11264, 11520, 11776, 12032, 12288, 12544, 12800, 13056, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15872, 0, 0, 0, 16128, 13312, 13568, 13824, 14080, 14336, 14592, 14848, 15104, 15360, 15616, 0, 0, 0, 25344, 0, 0, 0, 0, 256, 512, 768, 1024, 1280, 1536, 1792, 2048, 2304, 2560, 2816, 3072, 3328, 3584, 3840, 4096, 4352, 4608, 4864, 5120, 5376, 5632, 5888, 6144, 6400, 0, 0, 0, 0, 0, 0, 6656, 6912, 7168, 7424, 7680, 7936, 8192, 8448, 8704, 8960, 9216, 9472, 9728, 9984, 10240, 10496, 10752, 11008, 11264, 11520, 11776, 12032, 12288, 12544, 12800, 13056, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15872, 0, 0, 0, 16128, 13312, 13568, 13824, 14080, 14336, 14592, 14848, 15104, 15360, 15616, 0, 0, 0, 25344, 0, 0, 0, 0, 256, 512, 768, 1024, 1280, 1536, 1792, 2048, 2304, 2560, 2816, 3072, 3328, 3584, 3840, 4096, 4352, 4608, 4864, 5120, 5376, 5632, 5888, 6144, 6400, 0, 0, 0, 0, 0, 0, 6656, 6912, 7168, 7424, 7680, 7936, 8192, 8448, 8704, 8960, 9216, 9472, 9728, 9984, 10240, 10496, 10752, 11008, 11264, 11520, 11776, 12032, 12288, 12544, 12800, 13056, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15872, 0, 0, 0, 16128, 13312, 13568, 13824, 14080, 14336, 14592, 14848, 15104, 15360, 15616, 0, 0, 0, 25344, 0, 0, 0, 0, 256, 512, 768, 1024, 1280, 1536, 1792, 2048, 2304, 2560, 2816, 3072, 3328, 3584, 3840, 4096, 4352, 4608, 4864, 5120, 5376, 5632, 5888, 6144, 6400, 0, 0, 0, 0, 0, 0, 6656, 6912, 7168, 7424, 7680, 7936, 8192, 8448, 8704, 8960, 9216, 9472, 9728, 9984, 10240, 10496, 10752, 11008, 11264, 11520, 11776, 12032, 12288, 12544, 12800, 13056, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15872, 0, 0, 0, 16128, 13312, 13568, 13824, 14080, 14336, 14592, 14848, 15104, 15360, 15616, 0, 0, 0, 25344, 0, 0, 0, 0, 256, 512, 768, 1024, 1280, 1536, 1792, 2048, 2304, 2560, 2816, 3072, 3328, 3584, 3840, 4096, 4352, 4608, 4864, 5120, 5376, 5632, 5888, 6144, 6400, 0, 0, 0, 0, 0, 0, 6656, 6912, 7168, 7424, 7680, 7936, 8192, 8448, 8704, 8960, 9216, 9472, 9728, 9984, 10240, 10496, 10752, 11008, 11264, 11520, 11776, 12032, 12288, 12544, 12800, 13056, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15872, 0, 0, 0, 16128, 13312, 13568, 13824, 14080, 14336, 14592, 14848, 15104, 15360, 15616, 0, 0, 0, 25344, 0, 0, 0, 0, 256, 512, 768, 1024, 1280, 1536, 1792, 2048, 2304, 2560, 2816, 3072, 3328, 3584, 3840, 4096, 4352, 4608, 4864, 5120, 5376, 5632, 5888, 6144, 6400, 0, 0, 0, 0, 0, 0, 6656, 6912, 7168, 7424, 7680, 7936, 8192, 8448, 8704, 8960, 9216, 9472, 9728, 9984, 10240, 10496, 10752, 11008, 11264, 11520, 11776, 12032, 12288, 12544, 12800, 13056, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15872, 0, 0, 0, 16128, 13312, 13568, 13824, 14080, 14336, 14592, 14848, 15104, 15360, 15616, 0, 0, 0, 25344, 0, 0, 0, 0, 256, 512, 768, 1024, 1280, 1536, 1792, 2048, 2304, 2560, 2816, 3072, 3328, 3584, 3840, 4096, 4352, 4608, 4864, 5120, 5376, 5632, 5888, 6144, 6400, 0, 0, 0, 0, 0, 0, 6656, 6912, 7168, 7424, 7680, 7936, 8192, 8448, 8704, 8960, 9216, 9472, 9728, 9984, 10240, 10496, 10752, 11008, 11264, 11520, 11776, 12032, 12288, 12544, 12800, 13056, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15872, 0, 0, 0, 16128, 13312, 13568, 13824, 14080, 14336, 14592, 14848, 15104, 15360, 15616, 0, 0, 0, 25344, 0, 0, 0, 0, 256, 512, 768, 1024, 1280, 1536, 1792, 2048, 2304, 2560, 2816, 3072, 3328, 3584, 3840, 4096, 4352, 4608, 4864, 5120, 5376, 5632, 5888, 6144, 6400, 0, 0, 0, 0, 0, 0, 6656, 6912, 7168, 7424, 7680, 7936, 8192, 8448, 8704, 8960, 9216, 9472, 9728, 9984, 10240, 10496, 10752, 11008, 11264, 11520, 11776, 12032, 12288, 12544, 12800, 13056, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15872, 0, 0, 0, 16128, 13312, 13568, 13824, 14080, 14336, 14592, 14848, 15104, 15360, 15616, 0, 0, 0, 25344, 0, 0, 0, 0, 256, 512, 768, 1024, 1280, 1536, 1792, 2048, 2304, 2560, 2816, 3072, 3328, 3584, 3840, 4096, 4352, 4608, 4864, 5120, 5376, 5632, 5888, 6144, 6400, 0, 0, 0, 0, 0, 0, 6656, 6912, 7168, 7424, 7680, 7936, 8192, 8448, 8704, 8960, 9216, 9472, 9728, 9984, 10240, 10496, 10752, 11008, 11264, 11520, 11776, 12032, 12288, 12544, 12800, 13056, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15872, 0, 0, 0, 16128, 13312, 13568, 13824, 14080, 14336, 14592, 14848, 15104, 15360, 15616, 0, 0, 0, 25344, 0, 0, 0, 0, 256, 512, 768, 1024, 1280, 1536, 1792, 2048, 2304, 2560, 2816, 3072, 3328, 3584, 3840, 4096, 4352, 4608, 4864, 5120, 5376, 5632, 5888, 6144, 6400, 0, 0, 0, 0, 0, 0, 6656, 6912, 7168, 7424, 7680, 7936, 8192, 8448, 8704, 8960, 9216, 9472, 9728, 9984, 10240, 10496, 10752, 11008, 11264, 11520, 11776, 12032, 12288, 12544, 12800, 13056, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15872, 0, 0, 0, 16128, 13312, 13568, 13824, 14080, 14336, 14592, 14848, 15104, 15360, 15616, 0, 0, 0, 25344, 0, 0, 0, 0, 256, 512, 768, 1024, 1280, 1536, 1792, 2048, 2304, 2560, 2816, 3072, 3328, 3584, 3840, 4096, 4352, 4608, 4864, 5120, 5376, 5632, 5888, 6144, 6400, 0, 0, 0, 0, 0, 0, 6656, 6912, 7168, 7424, 7680, 7936, 8192, 8448, 8704, 8960, 9216, 9472, 9728, 9984, 10240, 10496, 10752, 11008, 11264, 11520, 11776, 12032, 12288, 12544, 12800, 13056, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15872, 0, 0, 0, 16128, 13312, 13568, 13824, 14080, 14336, 14592, 14848, 15104, 15360, 15616, 0, 0, 0, 25344, 0, 0, 0, 0, 256, 512, 768, 1024, 1280, 1536, 1792, 2048, 2304, 2560, 2816, 3072, 3328, 3584, 3840, 4096, 4352, 4608, 4864, 5120, 5376, 5632, 5888, 6144, 6400, 0, 0, 0, 0, 0, 0, 6656, 6912, 7168, 7424, 7680, 7936, 8192, 8448, 8704, 8960, 9216, 9472, 9728, 9984, 10240, 10496, 10752, 11008, 11264, 11520, 11776, 12032, 12288, 12544, 12800, 13056, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15872, 0, 0, 0, 16128, 13312, 13568, 13824, 14080, 14336, 14592, 14848, 15104, 15360, 15616, 0, 0, 0, 25344, 0, 0, 0, 0, 256, 512, 768, 1024, 1280, 1536, 1792, 2048, 2304, 2560, 2816, 3072, 3328, 3584, 3840, 4096, 4352, 4608, 4864, 5120, 5376, 5632, 5888, 6144, 6400, 0, 0, 0, 0, 0, 0, 6656, 6912, 7168, 7424, 7680, 7936, 8192, 8448, 8704, 8960, 9216, 9472, 9728, 9984, 10240, 10496, 10752, 11008, 11264, 11520, 11776, 12032, 12288, 12544, 12800, 13056, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15872, 0, 0, 0, 16128, 13312, 13568, 13824, 14080, 14336, 14592, 14848, 15104, 15360, 15616, 0, 0, 0, 25344, 0, 0, 0, 0, 256, 512, 768, 1024, 1280, 1536, 1792, 2048, 2304, 2560, 2816, 3072, 3328, 3584, 3840, 4096, 4352, 4608, 4864, 5120, 5376, 5632, 5888, 6144, 6400, 0, 0, 0, 0, 0, 0, 6656, 6912, 7168, 7424, 7680, 7936, 8192, 8448, 8704, 8960, 9216, 9472, 9728, 9984, 10240, 10496, 10752, 11008, 11264, 11520, 11776, 12032, 12288, 12544, 12800, 13056, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15872, 0, 0, 0, 16128, 13312, 13568, 13824, 14080, 14336, 14592, 14848, 15104, 15360, 15616, 0, 0, 0, 25344, 0, 0, 0, 0, 256, 512, 768, 1024, 1280, 1536, 1792, 2048, 2304, 2560, 2816, 3072, 3328, 3584, 3840, 4096, 4352, 4608, 4864, 5120, 5376, 5632, 5888, 6144, 6400, 0, 0, 0, 0, 0, 0, 6656, 6912, 7168, 7424, 7680, 7936, 8192, 8448, 8704, 8960, 9216, 9472, 9728, 9984, 10240, 10496, 10752, 11008, 11264, 11520, 11776, 12032, 12288, 12544, 12800, 13056, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15872, 0, 0, 0, 16128, 13312, 13568, 13824, 14080, 14336, 14592, 14848, 15104, 15360, 15616, 0, 0, 0, 25344, 0, 0, 0, 0, 256, 512, 768, 1024, 1280, 1536, 1792, 2048, 2304, 2560, 2816, 3072, 3328, 3584, 3840, 4096, 4352, 4608, 4864, 5120, 5376, 5632, 5888, 6144, 6400, 0, 0, 0, 0, 0, 0, 6656, 6912, 7168, 7424, 7680, 7936, 8192, 8448, 8704, 8960, 9216, 9472, 9728, 9984, 10240, 10496, 10752, 11008, 11264, 11520, 11776, 12032, 12288, 12544, 12800, 13056, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15872, 0, 0, 0, 16128, 13312, 13568, 13824, 14080, 14336, 14592, 14848, 15104, 15360, 15616, 0, 0, 0, 25344, 0, 0, 0, 0, 256, 512, 768, 1024, 1280, 1536, 1792, 2048, 2304, 2560, 2816, 3072, 3328, 3584, 3840, 4096, 4352, 4608, 4864, 5120, 5376, 5632, 5888, 6144, 6400, 0, 0, 0, 0, 0, 0, 6656, 6912, 7168, 7424, 7680, 7936, 8192, 8448, 8704, 8960, 9216, 9472, 9728, 9984, 10240, 10496, 10752, 11008, 11264, 11520, 11776, 12032, 12288, 12544, 12800, 13056, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15872, 0, 0, 0, 16128, 13312, 13568, 13824, 14080, 14336, 14592, 14848, 15104, 15360, 15616, 0, 0, 0, 25344, 0, 0, 0, 0, 256, 512, 768, 1024, 1280, 1536, 1792, 2048, 2304, 2560, 2816, 3072, 3328, 3584, 3840, 4096, 4352, 4608, 4864, 5120, 5376, 5632, 5888, 6144, 6400, 0, 0, 0, 0, 0, 0, 6656, 6912, 7168, 7424, 7680, 7936, 8192, 8448, 8704, 8960, 9216, 9472, 9728, 9984, 10240, 10496, 10752, 11008, 11264, 11520, 11776, 12032, 12288, 12544, 12800, 13056, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15872, 0, 0, 0, 16128, 13312, 13568, 13824, 14080, 14336, 14592, 14848, 15104, 15360, 15616, 0, 0, 0, 25344, 0, 0, 0, 0, 256, 512, 768, 1024, 1280, 1536, 1792, 2048, 2304, 2560, 2816, 3072, 3328, 3584, 3840, 4096, 4352, 4608, 4864, 5120, 5376, 5632, 5888, 6144, 6400, 0, 0, 0, 0, 0, 0, 6656, 6912, 7168, 7424, 7680, 7936, 8192, 8448, 8704, 8960, 9216, 9472, 9728, 9984, 10240, 10496, 10752, 11008, 11264, 11520, 11776, 12032, 12288, 12544, 12800, 13056, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15872, 0, 0, 0, 16128, 13312, 13568, 13824, 14080, 14336, 14592, 14848, 15104, 15360, 15616, 0, 0, 0, 25344, 0, 0, 0, 0, 256, 512, 768, 1024, 1280, 1536, 1792, 2048, 2304, 2560, 2816, 3072, 3328, 3584, 3840, 4096, 4352, 4608, 4864, 5120, 5376, 5632, 5888, 6144, 6400, 0, 0, 0, 0, 0, 0, 6656, 6912, 7168, 7424, 7680, 7936, 8192, 8448, 8704, 8960, 9216, 9472, 9728, 9984, 10240, 10496, 10752, 11008, 11264, 11520, 11776, 12032, 12288, 12544, 12800, 13056, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15872, 0, 0, 0, 16128, 13312, 13568, 13824, 14080, 14336, 14592, 14848, 15104, 15360, 15616, 0, 0, 0, 25344, 0, 0, 0, 0, 256, 512, 768, 1024, 1280, 1536, 1792, 2048, 2304, 2560, 2816, 3072, 3328, 3584, 3840, 4096, 4352, 4608, 4864, 5120, 5376, 5632, 5888, 6144, 6400, 0, 0, 0, 0, 0, 0, 6656, 6912, 7168, 7424, 7680, 7936, 8192, 8448, 8704, 8960, 9216, 9472, 9728, 9984, 10240, 10496, 10752, 11008, 11264, 11520, 11776, 12032, 12288, 12544, 12800, 13056, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15872, 0, 0, 0, 16128, 13312, 13568, 13824, 14080, 14336, 14592, 14848, 15104, 15360, 15616, 0, 0, 0, 25344, 0, 0, 0, 0, 256, 512, 768, 1024, 1280, 1536, 1792, 2048, 2304, 2560, 2816, 3072, 3328, 3584, 3840, 4096, 4352, 4608, 4864, 5120, 5376, 5632, 5888, 6144, 6400, 0, 0, 0, 0, 0, 0, 6656, 6912, 7168, 7424, 7680, 7936, 8192, 8448, 8704, 8960, 9216, 9472, 9728, 9984, 10240, 10496, 10752, 11008, 11264, 11520, 11776, 12032, 12288, 12544, 12800, 13056, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15872, 0, 0, 0, 16128, 13312, 13568, 13824, 14080, 14336, 14592, 14848, 15104, 15360, 15616, 0, 0, 0, 25344, 0, 0, 0, 0, 256, 512, 768, 1024, 1280, 1536, 1792, 2048, 2304, 2560, 2816, 3072, 3328, 3584, 3840, 4096, 4352, 4608, 4864, 5120, 5376, 5632, 5888, 6144, 6400, 0, 0, 0, 0, 0, 0, 6656, 6912, 7168, 7424, 7680, 7936, 8192, 8448, 8704, 8960, 9216, 9472, 9728, 9984, 10240, 10496, 10752, 11008, 11264, 11520, 11776, 12032, 12288, 12544, 12800, 13056, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15872, 0, 0, 0, 16128, 13312, 13568, 13824, 14080, 14336, 14592, 14848, 15104, 15360, 15616, 0, 0, 0, 25344, 0, 0, 0, 0, 256, 512, 768, 1024, 1280, 1536, 1792, 2048, 2304, 2560, 2816, 3072, 3328, 3584, 3840, 4096, 4352, 4608, 4864, 5120, 5376, 5632, 5888, 6144, 6400, 0, 0, 0, 0, 0, 0, 6656, 6912, 7168, 7424, 7680, 7936, 8192, 8448, 8704, 8960, 9216, 9472, 9728, 9984, 10240, 10496, 10752, 11008, 11264, 11520, 11776, 12032, 12288, 12544, 12800, 13056, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15872, 0, 0, 0, 16128, 13312, 13568, 13824, 14080, 14336, 14592, 14848, 15104, 15360, 15616, 0, 0, 0, 25344, 0, 0, 0, 0, 256, 512, 768, 1024, 1280, 1536, 1792, 2048, 2304, 2560, 2816, 3072, 3328, 3584, 3840, 4096, 4352, 4608, 4864, 5120, 5376, 5632, 5888, 6144, 6400, 0, 0, 0, 0, 0, 0, 6656, 6912, 7168, 7424, 7680, 7936, 8192, 8448, 8704, 8960, 9216, 9472, 9728, 9984, 10240, 10496, 10752, 11008, 11264, 11520, 11776, 12032, 12288, 12544, 12800, 13056, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15872, 0, 0, 0, 16128, 13312, 13568, 13824, 14080, 14336, 14592, 14848, 15104, 15360, 15616, 0, 0, 0, 25344, 0, 0, 0, 0, 256, 512, 768, 1024, 1280, 1536, 1792, 2048, 2304, 2560, 2816, 3072, 3328, 3584, 3840, 4096, 4352, 4608, 4864, 5120, 5376, 5632, 5888, 6144, 6400, 0, 0, 0, 0, 0, 0, 6656, 6912, 7168, 7424, 7680, 7936, 8192, 8448, 8704, 8960, 9216, 9472, 9728, 9984, 10240, 10496, 10752, 11008, 11264, 11520, 11776, 12032, 12288, 12544, 12800, 13056, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15872, 0, 0, 0, 16128, 13312, 13568, 13824, 14080, 14336, 14592, 14848, 15104, 15360, 15616, 0, 0, 0, 25344, 0, 0, 0, 0, 256, 512, 768, 1024, 1280, 1536, 1792, 2048, 2304, 2560, 2816, 3072, 3328, 3584, 3840, 4096, 4352, 4608, 4864, 5120, 5376, 5632, 5888, 6144, 6400, 0, 0, 0, 0, 0, 0, 6656, 6912, 7168, 7424, 7680, 7936, 8192, 8448, 8704, 8960, 9216, 9472, 9728, 9984, 10240, 10496, 10752, 11008, 11264, 11520, 11776, 12032, 12288, 12544, 12800, 13056, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15872, 0, 0, 0, 16128, 13312, 13568, 13824, 14080, 14336, 14592, 14848, 15104, 15360, 15616, 0, 0, 0, 25344, 0, 0, 0, 0, 256, 512, 768, 1024, 1280, 1536, 1792, 2048, 2304, 2560, 2816, 3072, 3328, 3584, 3840, 4096, 4352, 4608, 4864, 5120, 5376, 5632, 5888, 6144, 6400, 0, 0, 0, 0, 0, 0, 6656, 6912, 7168, 7424, 7680, 7936, 8192, 8448, 8704, 8960, 9216, 9472, 9728, 9984, 10240, 10496, 10752, 11008, 11264, 11520, 11776, 12032, 12288, 12544, 12800, 13056, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15872, 0, 0, 0, 16128, 13312, 13568, 13824, 14080, 14336, 14592, 14848, 15104, 15360, 15616, 0, 0, 0, 25344, 0, 0, 0, 0, 256, 512, 768, 1024, 1280, 1536, 1792, 2048, 2304, 2560, 2816, 3072, 3328, 3584, 3840, 4096, 4352, 4608, 4864, 5120, 5376, 5632, 5888, 6144, 6400, 0, 0, 0, 0, 0, 0, 6656, 6912, 7168, 7424, 7680, 7936, 8192, 8448, 8704, 8960, 9216, 9472, 9728, 9984, 10240, 10496, 10752, 11008, 11264, 11520, 11776, 12032, 12288, 12544, 12800, 13056, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15872, 0, 0, 0, 16128, 13312, 13568, 13824, 14080, 14336, 14592, 14848, 15104, 15360, 15616, 0, 0, 0, 25344, 0, 0, 0, 0, 256, 512, 768, 1024, 1280, 1536, 1792, 2048, 2304, 2560, 2816, 3072, 3328, 3584, 3840, 4096, 4352, 4608, 4864, 5120, 5376, 5632, 5888, 6144, 6400, 0, 0, 0, 0, 0, 0, 6656, 6912, 7168, 7424, 7680, 7936, 8192, 8448, 8704, 8960, 9216, 9472, 9728, 9984, 10240, 10496, 10752, 11008, 11264, 11520, 11776, 12032, 12288, 12544, 12800, 13056, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15872, 0, 0, 0, 16128, 13312, 13568, 13824, 14080, 14336, 14592, 14848, 15104, 15360, 15616, 0, 0, 0, 25344, 0, 0, 0, 0, 256, 512, 768, 1024, 1280, 1536, 1792, 2048, 2304, 2560, 2816, 3072, 3328, 3584, 3840, 4096, 4352, 4608, 4864, 5120, 5376, 5632, 5888, 6144, 6400, 0, 0, 0, 0, 0, 0, 6656, 6912, 7168, 7424, 7680, 7936, 8192, 8448, 8704, 8960, 9216, 9472, 9728, 9984, 10240, 10496, 10752, 11008, 11264, 11520, 11776, 12032, 12288, 12544, 12800, 13056, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15872, 0, 0, 0, 16128, 13312, 13568, 13824, 14080, 14336, 14592, 14848, 15104, 15360, 15616, 0, 0, 0, 25344, 0, 0, 0, 0, 256, 512, 768, 1024, 1280, 1536, 1792, 2048, 2304, 2560, 2816, 3072, 3328, 3584, 3840, 4096, 4352, 4608, 4864, 5120, 5376, 5632, 5888, 6144, 6400, 0, 0, 0, 0, 0, 0, 6656, 6912, 7168, 7424, 7680, 7936, 8192, 8448, 8704, 8960, 9216, 9472, 9728, 9984, 10240, 10496, 10752, 11008, 11264, 11520, 11776, 12032, 12288, 12544, 12800, 13056, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15872, 0, 0, 0, 16128, 13312, 13568, 13824, 14080, 14336, 14592, 14848, 15104, 15360, 15616, 0, 0, 0, 25344, 0, 0, 0, 0, 256, 512, 768, 1024, 1280, 1536, 1792, 2048, 2304, 2560, 2816, 3072, 3328, 3584, 3840, 4096, 4352, 4608, 4864, 5120, 5376, 5632, 5888, 6144, 6400, 0, 0, 0, 0, 0, 0, 6656, 6912, 7168, 7424, 7680, 7936, 8192, 8448, 8704, 8960, 9216, 9472, 9728, 9984, 10240, 10496, 10752, 11008, 11264, 11520, 11776, 12032, 12288, 12544, 12800, 13056, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15872, 0, 0, 0, 16128, 13312, 13568, 13824, 14080, 14336, 14592, 14848, 15104, 15360, 15616, 0, 0, 0, 25344, 0, 0, 0, 0, 256, 512, 768, 1024, 1280, 1536, 1792, 2048, 2304, 2560, 2816, 3072, 3328, 3584, 3840, 4096, 4352, 4608, 4864, 5120, 5376, 5632, 5888, 6144, 6400, 0, 0, 0, 0, 0, 0, 6656, 6912, 7168, 7424, 7680, 7936, 8192, 8448, 8704, 8960, 9216, 9472, 9728, 9984, 10240, 10496, 10752, 11008, 11264, 11520, 11776, 12032, 12288, 12544, 12800, 13056, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15872, 0, 0, 0, 16128, 13312, 13568, 13824, 14080, 14336, 14592, 14848, 15104, 15360, 15616, 0, 0, 0, 25344, 0, 0, 0, 0, 256, 512, 768, 1024, 1280, 1536, 1792, 2048, 2304, 2560, 2816, 3072, 3328, 3584, 3840, 4096, 4352, 4608, 4864, 5120, 5376, 5632, 5888, 6144, 6400, 0, 0, 0, 0, 0, 0, 6656, 6912, 7168, 7424, 7680, 7936, 8192, 8448, 8704, 8960, 9216, 9472, 9728, 9984, 10240, 10496, 10752, 11008, 11264, 11520, 11776, 12032, 12288, 12544, 12800, 13056, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15872, 0, 0, 0, 16128, 13312, 13568, 13824, 14080, 14336, 14592, 14848, 15104, 15360, 15616, 0, 0, 0, 25344, 0, 0, 0, 0, 256, 512, 768, 1024, 1280, 1536, 1792, 2048, 2304, 2560, 2816, 3072, 3328, 3584, 3840, 4096, 4352, 4608, 4864, 5120, 5376, 5632, 5888, 6144, 6400, 0, 0, 0, 0, 0, 0, 6656, 6912, 7168, 7424, 7680, 7936, 8192, 8448, 8704, 8960, 9216, 9472, 9728, 9984, 10240, 10496, 10752, 11008, 11264, 11520, 11776, 12032, 12288, 12544, 12800, 13056, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15872, 0, 0, 0, 16128, 13312, 13568, 13824, 14080, 14336, 14592, 14848, 15104, 15360, 15616, 0, 0, 0, 25344, 0, 0, 0, 0, 256, 512, 768, 1024, 1280, 1536, 1792, 2048, 2304, 2560, 2816, 3072, 3328, 3584, 3840, 4096, 4352, 4608, 4864, 5120, 5376, 5632, 5888, 6144, 6400, 0, 0, 0, 0, 0, 0, 6656, 6912, 7168, 7424, 7680, 7936, 8192, 8448, 8704, 8960, 9216, 9472, 9728, 9984, 10240, 10496, 10752, 11008, 11264, 11520, 11776, 12032, 12288, 12544, 12800, 13056, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15872, 0, 0, 0, 16128, 13312, 13568, 13824, 14080, 14336, 14592, 14848, 15104, 15360, 15616, 0, 0, 0, 25344, 0, 0, 0, 0, 256, 512, 768, 1024, 1280, 1536, 1792, 2048, 2304, 2560, 2816, 3072, 3328, 3584, 3840, 4096, 4352, 4608, 4864, 5120, 5376, 5632, 5888, 6144, 6400, 0, 0, 0, 0, 0, 0, 6656, 6912, 7168, 7424, 7680, 7936, 8192, 8448, 8704, 8960, 9216, 9472, 9728, 9984, 10240, 10496, 10752, 11008, 11264, 11520, 11776, 12032, 12288, 12544, 12800, 13056, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15872, 0, 0, 0, 16128, 13312, 13568, 13824, 14080, 14336, 14592, 14848, 15104, 15360, 15616, 0, 0, 0, 25344, 0, 0, 0, 0, 256, 512, 768, 1024, 1280, 1536, 1792, 2048, 2304, 2560, 2816, 3072, 3328, 3584, 3840, 4096, 4352, 4608, 4864, 5120, 5376, 5632, 5888, 6144, 6400, 0, 0, 0, 0, 0, 0, 6656, 6912, 7168, 7424, 7680, 7936, 8192, 8448, 8704, 8960, 9216, 9472, 9728, 9984, 10240, 10496, 10752, 11008, 11264, 11520, 11776, 12032, 12288, 12544, 12800, 13056, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15872, 0, 0, 0, 16128, 13312, 13568, 13824, 14080, 14336, 14592, 14848, 15104, 15360, 15616, 0, 0, 0, 25344, 0, 0, 0, 0, 256, 512, 768, 1024, 1280, 1536, 1792, 2048, 2304, 2560, 2816, 3072, 3328, 3584, 3840, 4096, 4352, 4608, 4864, 5120, 5376, 5632, 5888, 6144, 6400, 0, 0, 0, 0, 0, 0, 6656, 6912, 7168, 7424, 7680, 7936, 8192, 8448, 8704, 8960, 9216, 9472, 9728, 9984, 10240, 10496, 10752, 11008, 11264, 11520, 11776, 12032, 12288, 12544, 12800, 13056, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15872, 0, 0, 0, 16128, 13312, 13568, 13824, 14080, 14336, 14592, 14848, 15104, 15360, 15616, 0, 0, 0, 25344, 0, 0, 0, 0, 256, 512, 768, 1024, 1280, 1536, 1792, 2048, 2304, 2560, 2816, 3072, 3328, 3584, 3840, 4096, 4352, 4608, 4864, 5120, 5376, 5632, 5888, 6144, 6400, 0, 0, 0, 0, 0, 0, 6656, 6912, 7168, 7424, 7680, 7936, 8192, 8448, 8704, 8960, 9216, 9472, 9728, 9984, 10240, 10496, 10752, 11008, 11264, 11520, 11776, 12032, 12288, 12544, 12800, 13056, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15872, 0, 0, 0, 16128, 13312, 13568, 13824, 14080, 14336, 14592, 14848, 15104, 15360, 15616, 0, 0, 0, 25344, 0, 0, 0, 0, 256, 512, 768, 1024, 1280, 1536, 1792, 2048, 2304, 2560, 2816, 3072, 3328, 3584, 3840, 4096, 4352, 4608, 4864, 5120, 5376, 5632, 5888, 6144, 6400, 0, 0, 0, 0, 0, 0, 6656, 6912, 7168, 7424, 7680, 7936, 8192, 8448, 8704, 8960, 9216, 9472, 9728, 9984, 10240, 10496, 10752, 11008, 11264, 11520, 11776, 12032, 12288, 12544, 12800, 13056, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15872, 0, 0, 0, 16128, 13312, 13568, 13824, 14080, 14336, 14592, 14848, 15104, 15360, 15616, 0, 0, 0, 25344, 0, 0, 0, 0, 256, 512, 768, 1024, 1280, 1536, 1792, 2048, 2304, 2560, 2816, 3072, 3328, 3584, 3840, 4096, 4352, 4608, 4864, 5120, 5376, 5632, 5888, 6144, 6400, 0, 0, 0, 0, 0, 0, 6656, 6912, 7168, 7424, 7680, 7936, 8192, 8448, 8704, 8960, 9216, 9472, 9728, 9984, 10240, 10496, 10752, 11008, 11264, 11520, 11776, 12032, 12288, 12544, 12800, 13056, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15872, 0, 0, 0, 16128, 13312, 13568, 13824, 14080, 14336, 14592, 14848, 15104, 15360, 15616, 0, 0, 0, 25344, 0, 0, 0, 0, 256, 512, 768, 1024, 1280, 1536, 1792, 2048, 2304, 2560, 2816, 3072, 3328, 3584, 3840, 4096, 4352, 4608, 4864, 5120, 5376, 5632, 5888, 6144, 6400, 0, 0, 0, 0, 0, 0, 6656, 6912, 7168, 7424, 7680, 7936, 8192, 8448, 8704, 8960, 9216, 9472, 9728, 9984, 10240, 10496, 10752, 11008, 11264, 11520, 11776, 12032, 12288, 12544, 12800, 13056, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15872, 0, 0, 0, 16128, 13312, 13568, 13824, 14080, 14336, 14592, 14848, 15104, 15360, 15616, 0, 0, 0, 25344, 0, 0, 0, 0, 256, 512, 768, 1024, 1280, 1536, 1792, 2048, 2304, 2560, 2816, 3072, 3328, 3584, 3840, 4096, 4352, 4608, 4864, 5120, 5376, 5632, 5888, 6144, 6400, 0, 0, 0, 0, 0, 0, 6656, 6912, 7168, 7424, 7680, 7936, 8192, 8448, 8704, 8960, 9216, 9472, 9728, 9984, 10240, 10496, 10752, 11008, 11264, 11520, 11776, 12032, 12288, 12544, 12800, 13056, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15872, 0, 0, 0, 16128, 13312, 13568, 13824, 14080, 14336, 14592, 14848, 15104, 15360, 15616, 0, 0, 0, 25344, 0, 0, 0, 0, 256, 512, 768, 1024, 1280, 1536, 1792, 2048, 2304, 2560, 2816, 3072, 3328, 3584, 3840, 4096, 4352, 4608, 4864, 5120, 5376, 5632, 5888, 6144, 6400, 0, 0, 0, 0, 0, 0, 6656, 6912, 7168, 7424, 7680, 7936, 8192, 8448, 8704, 8960, 9216, 9472, 9728, 9984, 10240, 10496, 10752, 11008, 11264, 11520, 11776, 12032, 12288, 12544, 12800, 13056, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15872, 0, 0, 0, 16128, 13312, 13568, 13824, 14080, 14336, 14592, 14848, 15104, 15360, 15616, 0, 0, 0, 25344, 0, 0, 0, 0, 256, 512, 768, 1024, 1280, 1536, 1792, 2048, 2304, 2560, 2816, 3072, 3328, 3584, 3840, 4096, 4352, 4608, 4864, 5120, 5376, 5632, 5888, 6144, 6400, 0, 0, 0, 0, 0, 0, 6656, 6912, 7168, 7424, 7680, 7936, 8192, 8448, 8704, 8960, 9216, 9472, 9728, 9984, 10240, 10496, 10752, 11008, 11264, 11520, 11776, 12032, 12288, 12544, 12800, 13056, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15872, 0, 0, 0, 16128, 13312, 13568, 13824, 14080, 14336, 14592, 14848, 15104, 15360, 15616, 0, 0, 0, 25344, 0, 0, 0, 0, 256, 512, 768, 1024, 1280, 1536, 1792, 2048, 2304, 2560, 2816, 3072, 3328, 3584, 3840, 4096, 4352, 4608, 4864, 5120, 5376, 5632, 5888, 6144, 6400, 0, 0, 0, 0, 0, 0, 6656, 6912, 7168, 7424, 7680, 7936, 8192, 8448, 8704, 8960, 9216, 9472, 9728, 9984, 10240, 10496, 10752, 11008, 11264, 11520, 11776, 12032, 12288, 12544, 12800, 13056, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15872, 0, 0, 0, 16128, 13312, 13568, 13824, 14080, 14336, 14592, 14848, 15104, 15360, 15616, 0, 0, 0, 25344, 0, 0, 0, 0, 256, 512, 768, 1024, 1280, 1536, 1792, 2048, 2304, 2560, 2816, 3072, 3328, 3584, 3840, 4096, 4352, 4608, 4864, 5120, 5376, 5632, 5888, 6144, 6400, 0, 0, 0, 0, 0, 0, 6656, 6912, 7168, 7424, 7680, 7936, 8192, 8448, 8704, 8960, 9216, 9472, 9728, 9984, 10240, 10496, 10752, 11008, 11264, 11520, 11776, 12032, 12288, 12544, 12800, 13056, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15872, 0, 0, 0, 16128, 13312, 13568, 13824, 14080, 14336, 14592, 14848, 15104, 15360, 15616, 0, 0, 0, 25344, 0, 0, 0, 0, 256, 512, 768, 1024, 1280, 1536, 1792, 2048, 2304, 2560, 2816, 3072, 3328, 3584, 3840, 4096, 4352, 4608, 4864, 5120, 5376, 5632, 5888, 6144, 6400, 0, 0, 0, 0, 0, 0, 6656, 6912, 7168, 7424, 7680, 7936, 8192, 8448, 8704, 8960, 9216, 9472, 9728, 9984, 10240, 10496, 10752, 11008, 11264, 11520, 11776, 12032, 12288, 12544, 12800, 13056, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15872, 0, 0, 0, 16128, 13312, 13568, 13824, 14080, 14336, 14592, 14848, 15104, 15360, 15616, 0, 0, 0, 25344, 0, 0, 0, 0, 256, 512, 768, 1024, 1280, 1536, 1792, 2048, 2304, 2560, 2816, 3072, 3328, 3584, 3840, 4096, 4352, 4608, 4864, 5120, 5376, 5632, 5888, 6144, 6400, 0, 0, 0, 0, 0, 0, 6656, 6912, 7168, 7424, 7680, 7936, 8192, 8448, 8704, 8960, 9216, 9472, 9728, 9984, 10240, 10496, 10752, 11008, 11264, 11520, 11776, 12032, 12288, 12544, 12800, 13056, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15872, 0, 0, 0, 16128, 13312, 13568, 13824, 14080, 14336, 14592, 14848, 15104, 15360, 15616, 0, 0, 0, 25344, 0, 0, 0, 0, 256, 512, 768, 1024, 1280, 1536, 1792, 2048, 2304, 2560, 2816, 3072, 3328, 3584, 3840, 4096, 4352, 4608, 4864, 5120, 5376, 5632, 5888, 6144, 6400, 0, 0, 0, 0, 0, 0, 6656, 6912, 7168, 7424, 7680, 7936, 8192, 8448, 8704, 8960, 9216, 9472, 9728, 9984, 10240, 10496, 10752, 11008, 11264, 11520, 11776, 12032, 12288, 12544, 12800, 13056, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15872, 0, 0, 0, 16128, 13312, 13568, 13824, 14080, 14336, 14592, 14848, 15104, 15360, 15616, 0, 0, 0, 25344, 0, 0, 0, 0, 256, 512, 768, 1024, 1280, 1536, 1792, 2048, 2304, 2560, 2816, 3072, 3328, 3584, 3840, 4096, 4352, 4608, 4864, 5120, 5376, 5632, 5888, 6144, 6400, 0, 0, 0, 0, 0, 0, 6656, 6912, 7168, 7424, 7680, 7936, 8192, 8448, 8704, 8960, 9216, 9472, 9728, 9984, 10240, 10496, 10752, 11008, 11264, 11520, 11776, 12032, 12288, 12544, 12800, 13056, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15872, 0, 0, 0, 16128, 13312, 13568, 13824, 14080, 14336, 14592, 14848, 15104, 15360, 15616, 0, 0, 0, 25344, 0, 0, 0, 0, 256, 512, 768, 1024, 1280, 1536, 1792, 2048, 2304, 2560, 2816, 3072, 3328, 3584, 3840, 4096, 4352, 4608, 4864, 5120, 5376, 5632, 5888, 6144, 6400, 0, 0, 0, 0, 0, 0, 6656, 6912, 7168, 7424, 7680, 7936, 8192, 8448, 8704, 8960, 9216, 9472, 9728, 9984, 10240, 10496, 10752, 11008, 11264, 11520, 11776, 12032, 12288, 12544, 12800, 13056, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15872, 0, 0, 0, 16128, 13312, 13568, 13824, 14080, 14336, 14592, 14848, 15104, 15360, 15616, 0, 0, 0, 25344, 0, 0, 0, 0, 256, 512, 768, 1024, 1280, 1536, 1792, 2048, 2304, 2560, 2816, 3072, 3328, 3584, 3840, 4096, 4352, 4608, 4864, 5120, 5376, 5632, 5888, 6144, 6400, 0, 0, 0, 0, 0, 0, 6656, 6912, 7168, 7424, 7680, 7936, 8192, 8448, 8704, 8960, 9216, 9472, 9728, 9984, 10240, 10496, 10752, 11008, 11264, 11520, 11776, 12032, 12288, 12544, 12800, 13056, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15872, 0, 0, 0, 16128, 13312, 13568, 13824, 14080, 14336, 14592, 14848, 15104, 15360, 15616, 0, 0, 0, 25344, 0, 0, 0, 0, 256, 512, 768, 1024, 1280, 1536, 1792, 2048, 2304, 2560, 2816, 3072, 3328, 3584, 3840, 4096, 4352, 4608, 4864, 5120, 5376, 5632, 5888, 6144, 6400, 0, 0, 0, 0, 0, 0, 6656, 6912, 7168, 7424, 7680, 7936, 8192, 8448, 8704, 8960, 9216, 9472, 9728, 9984, 10240, 10496, 10752, 11008, 11264, 11520, 11776, 12032, 12288, 12544, 12800, 13056, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15872, 0, 0, 0, 16128, 13312, 13568, 13824, 14080, 14336, 14592, 14848, 15104, 15360, 15616, 0, 0, 0, 25344, 0, 0, 0, 0, 256, 512, 768, 1024, 1280, 1536, 1792, 2048, 2304, 2560, 2816, 3072, 3328, 3584, 3840, 4096, 4352, 4608, 4864, 5120, 5376, 5632, 5888, 6144, 6400, 0, 0, 0, 0, 0, 0, 6656, 6912, 7168, 7424, 7680, 7936, 8192, 8448, 8704, 8960, 9216, 9472, 9728, 9984, 10240, 10496, 10752, 11008, 11264, 11520, 11776, 12032, 12288, 12544, 12800, 13056, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15872, 0, 0, 0, 16128, 13312, 13568, 13824, 14080, 14336, 14592, 14848, 15104, 15360, 15616, 0, 0, 0, 25344, 0, 0, 0, 0, 256, 512, 768, 1024, 1280, 1536, 1792, 2048, 2304, 2560, 2816, 3072, 3328, 3584, 3840, 4096, 4352, 4608, 4864, 5120, 5376, 5632, 5888, 6144, 6400, 0, 0, 0, 0, 0, 0, 6656, 6912, 7168, 7424, 7680, 7936, 8192, 8448, 8704, 8960, 9216, 9472, 9728, 9984, 10240, 10496, 10752, 11008, 11264, 11520, 11776, 12032, 12288, 12544, 12800, 13056, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15872, 0, 0, 0, 16128, 13312, 13568, 13824, 14080, 14336, 14592, 14848, 15104, 15360, 15616, 0, 0, 0, 25344, 0, 0, 0, 0, 256, 512, 768, 1024, 1280, 1536, 1792, 2048, 2304, 2560, 2816, 3072, 3328, 3584, 3840, 4096, 4352, 4608, 4864, 5120, 5376, 5632, 5888, 6144, 6400, 0, 0, 0, 0, 0, 0, 6656, 6912, 7168, 7424, 7680, 7936, 8192, 8448, 8704, 8960, 9216, 9472, 9728, 9984, 10240, 10496, 10752, 11008, 11264, 11520, 11776, 12032, 12288, 12544, 12800, 13056, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15872, 0, 0, 0, 16128, 13312, 13568, 13824, 14080, 14336, 14592, 14848, 15104, 15360, 15616, 0, 0, 0, 25344, 0, 0, 0, 0, 256, 512, 768, 1024, 1280, 1536, 1792, 2048, 2304, 2560, 2816, 3072, 3328, 3584, 3840, 4096, 4352, 4608, 4864, 5120, 5376, 5632, 5888, 6144, 6400, 0, 0, 0, 0, 0, 0, 6656, 6912, 7168, 7424, 7680, 7936, 8192, 8448, 8704, 8960, 9216, 9472, 9728, 9984, 10240, 10496, 10752, 11008, 11264, 11520, 11776, 12032, 12288, 12544, 12800, 13056, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15872, 0, 0, 0, 16128, 13312, 13568, 13824, 14080, 14336, 14592, 14848, 15104, 15360, 15616, 0, 0, 0, 25344, 0, 0, 0, 0, 256, 512, 768, 1024, 1280, 1536, 1792, 2048, 2304, 2560, 2816, 3072, 3328, 3584, 3840, 4096, 4352, 4608, 4864, 5120, 5376, 5632, 5888, 6144, 6400, 0, 0, 0, 0, 0, 0, 6656, 6912, 7168, 7424, 7680, 7936, 8192, 8448, 8704, 8960, 9216, 9472, 9728, 9984, 10240, 10496, 10752, 11008, 11264, 11520, 11776, 12032, 12288, 12544, 12800, 13056, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15872, 0, 0, 0, 16128, 13312, 13568, 13824, 14080, 14336, 14592, 14848, 15104, 15360, 15616, 0, 0, 0, 25344, 0, 0, 0, 0, 256, 512, 768, 1024, 1280, 1536, 1792, 2048, 2304, 2560, 2816, 3072, 3328, 3584, 3840, 4096, 4352, 4608, 4864, 5120, 5376, 5632, 5888, 6144, 6400, 0, 0, 0, 0, 0, 0, 6656, 6912, 7168, 7424, 7680, 7936, 8192, 8448, 8704, 8960, 9216, 9472, 9728, 9984, 10240, 10496, 10752, 11008, 11264, 11520, 11776, 12032, 12288, 12544, 12800, 13056, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15872, 0, 0, 0, 16128, 13312, 13568, 13824, 14080, 14336, 14592, 14848, 15104, 15360, 15616, 0, 0, 0, 25344, 0, 0, 0, 0, 256, 512, 768, 1024, 1280, 1536, 1792, 2048, 2304, 2560, 2816, 3072, 3328, 3584, 3840, 4096, 4352, 4608, 4864, 5120, 5376, 5632, 5888, 6144, 6400, 0, 0, 0, 0, 0, 0, 6656, 6912, 7168, 7424, 7680, 7936, 8192, 8448, 8704, 8960, 9216, 9472, 9728, 9984, 10240, 10496, 10752, 11008, 11264, 11520, 11776, 12032, 12288, 12544, 12800, 13056, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15872, 0, 0, 0, 16128, 13312, 13568, 13824, 14080, 14336, 14592, 14848, 15104, 15360, 15616, 0, 0, 0, 25344, 0, 0, 0, 0, 256, 512, 768, 1024, 1280, 1536, 1792, 2048, 2304, 2560, 2816, 3072, 3328, 3584, 3840, 4096, 4352, 4608, 4864, 5120, 5376, 5632, 5888, 6144, 6400, 0, 0, 0, 0, 0, 0, 6656, 6912, 7168, 7424, 7680, 7936, 8192, 8448, 8704, 8960, 9216, 9472, 9728, 9984, 10240, 10496, 10752, 11008, 11264, 11520, 11776, 12032, 12288, 12544, 12800, 13056, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15872, 0, 0, 0, 16128, 13312, 13568, 13824, 14080, 14336, 14592, 14848, 15104, 15360, 15616, 0, 0, 0, 25344, 0, 0, 0, 0, 256, 512, 768, 1024, 1280, 1536, 1792, 2048, 2304, 2560, 2816, 3072, 3328, 3584, 3840, 4096, 4352, 4608, 4864, 5120, 5376, 5632, 5888, 6144, 6400, 0, 0, 0, 0, 0, 0, 6656, 6912, 7168, 7424, 7680, 7936, 8192, 8448, 8704, 8960, 9216, 9472, 9728, 9984, 10240, 10496, 10752, 11008, 11264, 11520, 11776, 12032, 12288, 12544, 12800, 13056, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15872, 0, 0, 0, 16128, 13312, 13568, 13824, 14080, 14336, 14592, 14848, 15104, 15360, 15616, 0, 0, 0, 25344, 0, 0, 0, 0, 256, 512, 768, 1024, 1280, 1536, 1792, 2048, 2304, 2560, 2816, 3072, 3328, 3584, 3840, 4096, 4352, 4608, 4864, 5120, 5376, 5632, 5888, 6144, 6400, 0, 0, 0, 0, 0, 0, 6656, 6912, 7168, 7424, 7680, 7936, 8192, 8448, 8704, 8960, 9216, 9472, 9728, 9984, 10240, 10496, 10752, 11008, 11264, 11520, 11776, 12032, 12288, 12544, 12800, 13056, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15872, 0, 0, 0, 16128, 13312, 13568, 13824, 14080, 14336, 14592, 14848, 15104, 15360, 15616, 0, 0, 0, 25344, 0, 0, 0, 0, 256, 512, 768, 1024, 1280, 1536, 1792, 2048, 2304, 2560, 2816, 3072, 3328, 3584, 3840, 4096, 4352, 4608, 4864, 5120, 5376, 5632, 5888, 6144, 6400, 0, 0, 0, 0, 0, 0, 6656, 6912, 7168, 7424, 7680, 7936, 8192, 8448, 8704, 8960, 9216, 9472, 9728, 9984, 10240, 10496, 10752, 11008, 11264, 11520, 11776, 12032, 12288, 12544, 12800, 13056, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }; libindi/obsolete/0000775000175000017500000000000013263645557013306 5ustar jasemjasemlibindi/obsolete/lx200aplib.h0000664000175000017500000000231613263645557015336 0ustar jasemjasem/* LX200 AP Driver Copyright (C) 2007 Markus Wildi 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 */ #pragma once #define ATA 0 #define ATR 1 #define ARTT 2 #define ARTTO 3 /* not yet there, requires a pointing model */ double LDRAtoHA(double RA, double longitude); int LDEqToEqT(double ra_h, double dec_d, double *hxt, double *rat_h, double *dect_d); int LDCartToSph(double *vec, double *ra, double *dec); int LDAppToX(int trans_to, double *star_cat, double tjd, double *loc, double *hxt, double *star_trans); libindi/obsolete/ieq45driver8406.c0000664000175000017500000011305313263645557016142 0ustar jasemjasem#if 0 IEQ45 Driver Copyright (C) 2011 Nacho Mas (mas.ignacio@gmail.com). Only litle changes from lx200basic made it by 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 "ieq45driver.h" #include "indicom.h" #include "indidevapi.h" #ifndef _WIN32 #include #endif #define IEQ45_TIMEOUT 5 /* FD timeout in seconds */ int controller_format; /************************************************************************** Diagnostics **************************************************************************/ int check_IEQ45_connection(int fd); /************************************************************************** Get Commands: store data in the supplied buffer. Return 0 on success or -1 on failure **************************************************************************/ /* Get Double from Sexagisemal */ int getCommandSexa(int fd, double *value, const char *cmd); /* Get String */ int getCommandString(int fd, char *data, const char *cmd); /* Get Int */ int getCommandInt(int fd, int *value, const char *cmd); /* Get tracking frequency */ int getTrackFreq(int fd, double *value); /* Get site Latitude */ int getSiteLatitude(int fd, int *dd, int *mm); /* Get site Longitude */ int getSiteLongitude(int fd, int *ddd, int *mm); /* Get Calender data */ int getCalendarDate(int fd, char *date); /* Get site Name */ int getSiteName(int fd, char *siteName, int siteNum); /* Get Number of Bars */ int getNumberOfBars(int fd, int *value); /* Get Home Search Status */ int getHomeSearchStatus(int fd, int *status); /* Get OTA Temperature */ int getOTATemp(int fd, double *value); /* Get time format: 12 or 24 */ int getTimeFormat(int fd, int *format); /************************************************************************** Set Commands **************************************************************************/ /* Set Int */ int setCommandInt(int fd, int data, const char *cmd); /* Set Sexigesimal */ int setCommandXYZ(int fd, int x, int y, int z, const char *cmd); /* Common routine for Set commands */ int setStandardProcedure(int fd, char *writeData); /* Set Slew Mode */ int setSlewMode(int fd, int slewMode); /* Set Alignment mode */ int setAlignmentMode(int fd, unsigned int alignMode); /* Set Object RA */ int setObjectRA(int fd, double ra); /* set Object DEC */ int setObjectDEC(int fd, double dec); /* Set Calender date */ int setCalenderDate(int fd, int dd, int mm, int yy); /* Set UTC offset */ int setUTCOffset(int fd, double hours); /* Set Track Freq */ int setTrackFreq(int fd, double trackF); /* Set current site longitude */ int setSiteLongitude(int fd, double Long); /* Set current site latitude */ int setSiteLatitude(int fd, double Lat); /* Set Object Azimuth */ int setObjAz(int fd, double az); /* Set Object Altitude */ int setObjAlt(int fd, double alt); /* Set site name */ int setSiteName(int fd, char *siteName, int siteNum); /* Set maximum slew rate */ int setMaxSlewRate(int fd, int slewRate); /* Set focuser motion */ int setFocuserMotion(int fd, int motionType); /* Set focuser speed mode */ int setFocuserSpeedMode(int fd, int speedMode); /* Set minimum elevation limit */ int setMinElevationLimit(int fd, int min); /* Set maximum elevation limit */ int setMaxElevationLimit(int fd, int max); /************************************************************************** Motion Commands **************************************************************************/ /* Slew to the selected coordinates */ int Slew(int fd); /* Synchronize to the selected coordinates and return the matching object if any */ int Sync(int fd, char *matchedObject); /* Abort slew in all axes */ int abortSlew(int fd); /* Move into one direction, two valid directions can be stacked */ int MoveTo(int fd, int direction); /* Half movement in a particular direction */ int HaltMovement(int fd, int direction); /* Select the tracking mode */ int selectTrackingMode(int fd, int trackMode); /* Select Astro-Physics tracking mode */ int selectAPTrackingMode(int fd, int trackMode); /* Send Pulse-Guide command (timed guide move), two valid directions can be stacked */ int SendPulseCmd(int fd, int direction, int duration_msec); /************************************************************************** Other Commands **************************************************************************/ /* Ensures IEQ45 RA/DEC format is long */ int checkIEQ45Format(int fd); /* Select a site from the IEQ45 controller */ int selectSite(int fd, int siteNum); /* Select a catalog object */ int selectCatalogObject(int fd, int catalog, int NNNN); /* Select a sub catalog */ int selectSubCatalog(int fd, int catalog, int subCatalog); int check_IEQ45_connection(int in_fd) { int i = 0; char firmwareDate[14] = ":FirmWareDate#"; char MountAlign[64]; int nbytes_read = 0; #ifdef INDI_DEBUG IDLog("Testing telescope's connection using FirmwareDate command...\n"); #endif if (in_fd <= 0) return -1; for (i = 0; i < 2; i++) { if (write(in_fd, firmwareDate, 14) < 0) return -1; tty_read(in_fd, MountAlign, 1, IEQ45_TIMEOUT, &nbytes_read); if (nbytes_read == 1) return 0; usleep(50000); } return -1; } /********************************************************************** * GET **********************************************************************/ void remove_spaces(char *texto_recibe) { char *texto_sin_espacio; for (texto_sin_espacio = texto_recibe; *texto_recibe; texto_recibe++) { if (isspace(*texto_recibe)) continue; *texto_sin_espacio++ = *texto_recibe; } *texto_sin_espacio = '\0'; } int getCommandSexa(int fd, double *value, const char *cmd) { char temp_string[16]; int error_type; int nbytes_write = 0, nbytes_read = 0; tcflush(fd, TCIFLUSH); if ((error_type = tty_write_string(fd, cmd, &nbytes_write)) != TTY_OK) return error_type; /*if ( (read_ret = portRead(temp_string, -1, IEQ45_TIMEOUT)) < 1) return read_ret;*/ tty_read_section(fd, temp_string, '#', IEQ45_TIMEOUT, &nbytes_read); temp_string[nbytes_read - 1] = '\0'; /*IDLog("getComandSexa: %s\n", temp_string);*/ //IEQ45 sometimes send a malformed RA/DEC (intermediate spaces) //so I clean before: remove_spaces(temp_string); if (f_scansexa(temp_string, value)) { #ifdef INDI_DEBUG IDLog("unable to process [%s]\n", temp_string); #endif return -1; } tcflush(fd, TCIFLUSH); return 0; } int getCommandInt(int fd, int *value, const char *cmd) { char temp_string[16]; float temp_number; int error_type; int nbytes_write = 0, nbytes_read = 0; tcflush(fd, TCIFLUSH); if ((error_type = tty_write_string(fd, cmd, &nbytes_write)) != TTY_OK) return error_type; tty_read_section(fd, temp_string, '#', IEQ45_TIMEOUT, &nbytes_read); temp_string[nbytes_read - 1] = '\0'; /* Float */ if (strchr(temp_string, '.')) { if (sscanf(temp_string, "%f", &temp_number) != 1) return -1; *value = (int)temp_number; } /* Int */ else if (sscanf(temp_string, "%d", value) != 1) return -1; return 0; } int getCommandString(int fd, char *data, const char *cmd) { char *term; int error_type; int nbytes_write = 0, nbytes_read = 0; /*if (portWrite(cmd) < 0) return -1;*/ if ((error_type = tty_write_string(fd, cmd, &nbytes_write)) != TTY_OK) return error_type; /*read_ret = portRead(data, -1, IEQ45_TIMEOUT);*/ error_type = tty_read_section(fd, data, '#', IEQ45_TIMEOUT, &nbytes_read); tcflush(fd, TCIFLUSH); if (error_type != TTY_OK) return error_type; term = strchr(data, '#'); if (term) *term = '\0'; #ifdef INDI_DEBUG /*IDLog("Requested data: %s\n", data);*/ #endif return 0; } int getCalendarDate(int fd, char *date) { int dd, mm, yy; int error_type; int nbytes_read = 0; char mell_prefix[3]; if ((error_type = getCommandString(fd, date, ":GC#"))) return error_type; /* Meade format is MM/DD/YY */ nbytes_read = sscanf(date, "%d%*c%d%*c%d", &mm, &dd, &yy); if (nbytes_read < 3) return -1; /* We consider years 50 or more to be in the last century, anything less in the 21st century.*/ if (yy > 50) strncpy(mell_prefix, "19", 3); else strncpy(mell_prefix, "20", 3); /* We need to have in in YYYY/MM/DD format */ snprintf(date, 16, "%s%02d/%02d/%02d", mell_prefix, yy, mm, dd); return (0); } int getTimeFormat(int fd, int *format) { char temp_string[16]; int error_type; int nbytes_write = 0, nbytes_read = 0; int tMode; /*if (portWrite(":Gc#") < 0) return -1;*/ if ((error_type = tty_write_string(fd, ":Gc#", &nbytes_write)) != TTY_OK) return error_type; /*read_ret = portRead(temp_string, -1, IEQ45_TIMEOUT);*/ if ((error_type = tty_read_section(fd, temp_string, '#', IEQ45_TIMEOUT, &nbytes_read)) != TTY_OK) return error_type; tcflush(fd, TCIFLUSH); if (nbytes_read < 1) return error_type; temp_string[nbytes_read - 1] = '\0'; nbytes_read = sscanf(temp_string, "(%d)", &tMode); if (nbytes_read < 1) return -1; else *format = tMode; return 0; } int getSiteName(int fd, char *siteName, int siteNum) { char *term; int error_type; int nbytes_write = 0, nbytes_read = 0; switch (siteNum) { case 1: /*if (portWrite(":GM#") < 0) return -1;*/ if ((error_type = tty_write_string(fd, ":GM#", &nbytes_write)) != TTY_OK) return error_type; break; case 2: /*if (portWrite(":GN#") < 0) return -1;*/ if ((error_type = tty_write_string(fd, ":GN#", &nbytes_write)) != TTY_OK) return error_type; break; case 3: /*if (portWrite(":GO#") < 0) return -1;*/ if ((error_type = tty_write_string(fd, ":GO#", &nbytes_write)) != TTY_OK) return error_type; break; case 4: /*if (portWrite(":GP#") < 0) return -1;*/ if ((error_type = tty_write_string(fd, ":GP#", &nbytes_write)) != TTY_OK) return error_type; break; default: return -1; } /*read_ret = portRead(siteName, -1, IEQ45_TIMEOUT);*/ error_type = tty_read_section(fd, siteName, '#', IEQ45_TIMEOUT, &nbytes_read); tcflush(fd, TCIFLUSH); if (nbytes_read < 1) return error_type; siteName[nbytes_read - 1] = '\0'; term = strchr(siteName, ' '); if (term) *term = '\0'; term = strchr(siteName, '<'); if (term) strcpy(siteName, "unused site"); #ifdef INDI_DEBUG IDLog("Requested site name: %s\n", siteName); #endif return 0; } int getSiteLatitude(int fd, int *dd, int *mm) { char temp_string[16]; int error_type; int nbytes_write = 0, nbytes_read = 0; /*if (portWrite(":Gt#") < 0) return -1;*/ if ((error_type = tty_write_string(fd, ":Gt#", &nbytes_write)) != TTY_OK) return error_type; /*read_ret = portRead(temp_string, -1, IEQ45_TIMEOUT);*/ error_type = tty_read_section(fd, temp_string, '#', IEQ45_TIMEOUT, &nbytes_read); tcflush(fd, TCIFLUSH); if (nbytes_read < 1) return error_type; temp_string[nbytes_read - 1] = '\0'; if (sscanf(temp_string, "%d%*c%d", dd, mm) < 2) return -1; #ifdef INDI_DEBUG fprintf(stderr, "Requested site latitude in String %s\n", temp_string); fprintf(stderr, "Requested site latitude %d:%d\n", *dd, *mm); #endif return 0; } int getSiteLongitude(int fd, int *ddd, int *mm) { char temp_string[16]; int error_type; int nbytes_write = 0, nbytes_read = 0; if ((error_type = tty_write_string(fd, ":Gg#", &nbytes_write)) != TTY_OK) return error_type; /*if (portWrite(":Gg#") < 0) return -1;*/ error_type = tty_read_section(fd, temp_string, '#', IEQ45_TIMEOUT, &nbytes_read); /*read_ret = portRead(temp_string, -1, IEQ45_TIMEOUT);*/ tcflush(fd, TCIFLUSH); if (nbytes_read < 1) return error_type; temp_string[nbytes_read - 1] = '\0'; if (sscanf(temp_string, "%d%*c%d", ddd, mm) < 2) return -1; #ifdef INDI_DEBUG fprintf(stderr, "Requested site longitude in String %s\n", temp_string); fprintf(stderr, "Requested site longitude %d:%d\n", *ddd, *mm); #endif return 0; } int getTrackFreq(int fd, double *value) { float Freq; char temp_string[16]; int error_type; int nbytes_write = 0, nbytes_read = 0; if ((error_type = tty_write_string(fd, ":GT#", &nbytes_write)) != TTY_OK) return error_type; /*if (portWrite(":GT#") < 0) return -1;*/ /*read_ret = portRead(temp_string, -1, IEQ45_TIMEOUT);*/ error_type = tty_read_section(fd, temp_string, '#', IEQ45_TIMEOUT, &nbytes_read); tcflush(fd, TCIFLUSH); if (nbytes_read < 1) return error_type; temp_string[nbytes_read] = '\0'; /*fprintf(stderr, "Telescope tracking freq str: %s\n", temp_string);*/ if (sscanf(temp_string, "%f#", &Freq) < 1) return -1; *value = (double)Freq; #ifdef INDI_DEBUG fprintf(stderr, "Tracking frequency value is %f\n", Freq); #endif return 0; } int getNumberOfBars(int fd, int *value) { char temp_string[128]; int error_type; int nbytes_write = 0, nbytes_read = 0; if ((error_type = tty_write_string(fd, ":D#", &nbytes_write)) != TTY_OK) return error_type; /*if (portWrite(":D#") < 0) return -1;*/ error_type = tty_read_section(fd, temp_string, '#', IEQ45_TIMEOUT, &nbytes_read); tcflush(fd, TCIFLUSH); if (nbytes_read < 0) return error_type; *value = nbytes_read - 1; return 0; } int getHomeSearchStatus(int fd, int *status) { char temp_string[16]; int error_type; int nbytes_write = 0, nbytes_read = 0; if ((error_type = tty_write_string(fd, ":h?#", &nbytes_write)) != TTY_OK) return error_type; /*if (portWrite(":h?#") < 0) return -1;*/ /*read_ret = portRead(temp_string, 1, IEQ45_TIMEOUT);*/ error_type = tty_read_section(fd, temp_string, '#', IEQ45_TIMEOUT, &nbytes_read); tcflush(fd, TCIFLUSH); if (nbytes_read < 1) return error_type; temp_string[1] = '\0'; if (temp_string[0] == '0') *status = 0; else if (temp_string[0] == '1') *status = 1; else if (temp_string[0] == '2') *status = 1; return 0; } int getOTATemp(int fd, double *value) { char temp_string[16]; int error_type; int nbytes_write = 0, nbytes_read = 0; float temp; if ((error_type = tty_write_string(fd, ":fT#", &nbytes_write)) != TTY_OK) return error_type; error_type = tty_read_section(fd, temp_string, '#', IEQ45_TIMEOUT, &nbytes_read); if (nbytes_read < 1) return error_type; temp_string[nbytes_read - 1] = '\0'; if (sscanf(temp_string, "%f", &temp) < 1) return -1; *value = (double)temp; return 0; } int updateSkyCommanderCoord(int fd, double *ra, double *dec) { char coords[16]; char CR[1] = { (char)0x0D }; float RA = 0.0, DEC = 0.0; int error_type; int nbytes_read = 0; error_type = write(fd, CR, 1); error_type = tty_read(fd, coords, 16, IEQ45_TIMEOUT, &nbytes_read); /*read_ret = portRead(coords, 16, IEQ45_TIMEOUT);*/ tcflush(fd, TCIFLUSH); nbytes_read = sscanf(coords, " %g %g", &RA, &DEC); if (nbytes_read < 2) { #ifdef INDI_DEBUG IDLog("Error in Sky commander number format [%s], exiting.\n", coords); #endif return error_type; } *ra = RA; *dec = DEC; return 0; } int updateIntelliscopeCoord(int fd, double *ra, double *dec) { char coords[16]; char CR[1] = { (char)0x51 }; /* "Q" */ float RA = 0.0, DEC = 0.0; int error_type; int nbytes_read = 0; /*IDLog ("Sending a Q\n");*/ error_type = write(fd, CR, 1); /* We start at 14 bytes in case its a Sky Wizard, but read one more later it if it's a intelliscope */ /*read_ret = portRead (coords, 14, IEQ45_TIMEOUT);*/ error_type = tty_read(fd, coords, 14, IEQ45_TIMEOUT, &nbytes_read); tcflush(fd, TCIFLUSH); /*IDLog ("portRead() = [%s]\n", coords);*/ /* Remove the Q in the response from the Intelliscope but not the Sky Wizard */ if (coords[0] == 'Q') { coords[0] = ' '; /* Read one more byte if Intelliscope to get the "CR" */ error_type = tty_read(fd, coords, 1, IEQ45_TIMEOUT, &nbytes_read); /*read_ret = portRead (coords, 1, IEQ45_TIMEOUT);*/ } nbytes_read = sscanf(coords, " %g %g", &RA, &DEC); /*IDLog ("sscanf() RA = [%f]\n", RA * 0.0390625);*/ /*IDLog ("sscanf() DEC = [%f]\n", DEC * 0.0390625);*/ /*IDLog ("Intelliscope output [%s]", coords);*/ if (nbytes_read < 2) { #ifdef INDI_DEBUG IDLog("Error in Intelliscope number format [%s], exiting.\n", coords); #endif return -1; } *ra = RA * 0.0390625; *dec = DEC * 0.0390625; return 0; } /********************************************************************** * SET **********************************************************************/ int setStandardProcedure(int fd, char *data) { char bool_return[2]; int error_type; int nbytes_write = 0, nbytes_read = 0; if ((error_type = tty_write_string(fd, data, &nbytes_write)) != TTY_OK) return error_type; error_type = tty_read(fd, bool_return, 1, IEQ45_TIMEOUT, &nbytes_read); /*read_ret = portRead(boolRet, 1, IEQ45_TIMEOUT);*/ tcflush(fd, TCIFLUSH); if (nbytes_read < 1) return error_type; if (bool_return[0] == '0') { #ifdef INDI_DEBUG IDLog("%s Failed.\n", data); #endif return -1; } #ifdef INDI_DEBUG IDLog("%s Successful\n", data); #endif return 0; } int setCommandInt(int fd, int data, const char *cmd) { char temp_string[16]; int error_type; int nbytes_write = 0; snprintf(temp_string, sizeof(temp_string), "%s%d#", cmd, data); if ((error_type = tty_write_string(fd, temp_string, &nbytes_write)) != TTY_OK) return error_type; /* if (portWrite(temp_string) < 0) return -1;*/ return 0; } int setMinElevationLimit(int fd, int min) { char temp_string[16]; snprintf(temp_string, sizeof(temp_string), ":Sh%02d#", min); return (setStandardProcedure(fd, temp_string)); } int setMaxElevationLimit(int fd, int max) { char temp_string[16]; snprintf(temp_string, sizeof(temp_string), ":So%02d*#", max); return (setStandardProcedure(fd, temp_string)); } int setMaxSlewRate(int fd, int slewRate) { char temp_string[16]; if (slewRate < 2 || slewRate > 8) return -1; snprintf(temp_string, sizeof(temp_string), ":Sw%d#", slewRate); return (setStandardProcedure(fd, temp_string)); } int setObjectRA(int fd, double ra) { int h, m, s, frac_m; char temp_string[16]; getSexComponents(ra, &h, &m, &s); frac_m = (s / 60.0) * 10.; if (controller_format == IEQ45_LONG_FORMAT) snprintf(temp_string, sizeof(temp_string), ":Sr %02d:%02d:%02d#", h, m, s); else snprintf(temp_string, sizeof(temp_string), ":Sr %02d:%02d.%01d#", h, m, frac_m); /*IDLog("Set Object RA String %s\n", temp_string);*/ return (setStandardProcedure(fd, temp_string)); } int setObjectDEC(int fd, double dec) { int d, m, s; char temp_string[16]; getSexComponents(dec, &d, &m, &s); switch (controller_format) { case IEQ45_SHORT_FORMAT: /* case with negative zero */ if (!d && dec < 0) snprintf(temp_string, sizeof(temp_string), ":Sd -%02d*%02d#", d, m); else snprintf(temp_string, sizeof(temp_string), ":Sd %+03d*%02d#", d, m); break; case IEQ45_LONG_FORMAT: /* case with negative zero */ if (!d && dec < 0) snprintf(temp_string, sizeof(temp_string), ":Sd -%02d:%02d:%02d#", d, m, s); else snprintf(temp_string, sizeof(temp_string), ":Sd %+03d:%02d:%02d#", d, m, s); break; } /*IDLog("Set Object DEC String %s\n", temp_string);*/ return (setStandardProcedure(fd, temp_string)); } int setCommandXYZ(int fd, int x, int y, int z, const char *cmd) { char temp_string[16]; snprintf(temp_string, sizeof(temp_string), "%s %02d:%02d:%02d#", cmd, x, y, z); return (setStandardProcedure(fd, temp_string)); } int setAlignmentMode(int fd, unsigned int alignMode) { /*fprintf(stderr , "Set alignment mode %d\n", alignMode);*/ int error_type; int nbytes_write = 0; switch (alignMode) { case IEQ45_ALIGN_POLAR: if ((error_type = tty_write_string(fd, ":AP#", &nbytes_write)) != TTY_OK) return error_type; /*if (portWrite(":AP#") < 0) return -1;*/ break; case IEQ45_ALIGN_ALTAZ: if ((error_type = tty_write_string(fd, ":AA#", &nbytes_write)) != TTY_OK) return error_type; /*if (portWrite(":AA#") < 0) return -1;*/ break; case IEQ45_ALIGN_LAND: if ((error_type = tty_write_string(fd, ":AL#", &nbytes_write)) != TTY_OK) return error_type; /*if (portWrite(":AL#") < 0) return -1;*/ break; } tcflush(fd, TCIFLUSH); return 0; } int setCalenderDate(int fd, int dd, int mm, int yy) { char temp_string[32]; char dumpPlanetaryUpdateString[64]; char bool_return[2]; int error_type; int nbytes_write = 0, nbytes_read = 0; yy = yy % 100; snprintf(temp_string, sizeof(temp_string), ":SC %02d/%02d/%02d#", mm, dd, yy); if ((error_type = tty_write_string(fd, temp_string, &nbytes_write)) != TTY_OK) return error_type; /*if (portWrite(temp_string) < 0) return -1;*/ /*read_ret = portRead(boolRet, 1, IEQ45_TIMEOUT);*/ error_type = tty_read(fd, bool_return, 1, IEQ45_TIMEOUT, &nbytes_read); tcflush(fd, TCIFLUSH); if (nbytes_read < 1) return error_type; bool_return[1] = '\0'; if (bool_return[0] == '0') return -1; /* Read dumped data */ error_type = tty_read_section(fd, dumpPlanetaryUpdateString, '#', IEQ45_TIMEOUT, &nbytes_read); error_type = tty_read_section(fd, dumpPlanetaryUpdateString, '#', 5, &nbytes_read); return 0; } int setUTCOffset(int fd, double hours) { char temp_string[16]; snprintf(temp_string, sizeof(temp_string), ":SG %+03d#", (int)hours); /*IDLog("UTC string is %s\n", temp_string);*/ return (setStandardProcedure(fd, temp_string)); } int setSiteLongitude(int fd, double Long) { int d, m, s; char temp_string[32]; getSexComponents(Long, &d, &m, &s); snprintf(temp_string, sizeof(temp_string), ":Sg%03d:%02d#", d, m); return (setStandardProcedure(fd, temp_string)); } int setSiteLatitude(int fd, double Lat) { int d, m, s; char temp_string[32]; getSexComponents(Lat, &d, &m, &s); snprintf(temp_string, sizeof(temp_string), ":St%+03d:%02d:%02d#", d, m, s); return (setStandardProcedure(fd, temp_string)); } int setObjAz(int fd, double az) { int d, m, s; char temp_string[16]; getSexComponents(az, &d, &m, &s); snprintf(temp_string, sizeof(temp_string), ":Sz%03d:%02d#", d, m); return (setStandardProcedure(fd, temp_string)); } int setObjAlt(int fd, double alt) { int d, m, s; char temp_string[16]; getSexComponents(alt, &d, &m, &s); snprintf(temp_string, sizeof(temp_string), ":Sa%+02d*%02d#", d, m); return (setStandardProcedure(fd, temp_string)); } int setSiteName(int fd, char *siteName, int siteNum) { char temp_string[16]; switch (siteNum) { case 1: snprintf(temp_string, sizeof(temp_string), ":SM %s#", siteName); break; case 2: snprintf(temp_string, sizeof(temp_string), ":SN %s#", siteName); break; case 3: snprintf(temp_string, sizeof(temp_string), ":SO %s#", siteName); break; case 4: snprintf(temp_string, sizeof(temp_string), ":SP %s#", siteName); break; default: return -1; } return (setStandardProcedure(fd, temp_string)); } int setSlewMode(int fd, int slewMode) { int error_type; int nbytes_write = 0; switch (slewMode) { case IEQ45_SLEW_MAX: if ((error_type = tty_write_string(fd, ":RS#", &nbytes_write)) != TTY_OK) return error_type; /*if (portWrite(":RS#") < 0) return -1;*/ break; case IEQ45_SLEW_FIND: if ((error_type = tty_write_string(fd, ":RM#", &nbytes_write)) != TTY_OK) return error_type; /*if (portWrite(":RM#") < 0) return -1;*/ break; case IEQ45_SLEW_CENTER: if ((error_type = tty_write_string(fd, ":RC#", &nbytes_write)) != TTY_OK) return error_type; /*if (portWrite(":RC#") < 0) return -1;*/ break; case IEQ45_SLEW_GUIDE: if ((error_type = tty_write_string(fd, ":RG#", &nbytes_write)) != TTY_OK) return error_type; /*if (portWrite(":RG#") < 0) return -1;*/ break; default: break; } tcflush(fd, TCIFLUSH); return 0; } int setFocuserMotion(int fd, int motionType) { int error_type; int nbytes_write = 0; switch (motionType) { case IEQ45_FOCUSIN: if ((error_type = tty_write_string(fd, ":F+#", &nbytes_write)) != TTY_OK) return error_type; #ifdef INDI_DEBUG /*IDLog("Focus IN Command\n");*/ #endif /*if (portWrite(":F+#") < 0) return -1;*/ break; case IEQ45_FOCUSOUT: if ((error_type = tty_write_string(fd, ":F-#", &nbytes_write)) != TTY_OK) return error_type; #ifdef INDI_DEBUG /*IDLog("Focus OUT Command\n");*/ #endif /*if (portWrite(":F-#") < 0) return -1;*/ break; } tcflush(fd, TCIFLUSH); return 0; } int setFocuserSpeedMode(int fd, int speedMode) { int error_type; int nbytes_write = 0; switch (speedMode) { case IEQ45_HALTFOCUS: if ((error_type = tty_write_string(fd, ":FQ#", &nbytes_write)) != TTY_OK) return error_type; #ifdef INDI_DEBUG /*IDLog("Halt Focus Command\n");*/ #endif /* if (portWrite(":FQ#") < 0) return -1;*/ break; case IEQ45_FOCUSSLOW: if ((error_type = tty_write_string(fd, ":FS#", &nbytes_write)) != TTY_OK) return error_type; #ifdef INDI_DEBUG /*IDLog("Focus Slow (FS) Command\n");*/ #endif /*if (portWrite(":FS#") < 0) return -1;*/ break; case IEQ45_FOCUSFAST: if ((error_type = tty_write_string(fd, ":FF#", &nbytes_write)) != TTY_OK) return error_type; #ifdef INDI_DEBUG /*IDLog("Focus Fast (FF) Command\n");*/ #endif /*if (portWrite(":FF#") < 0) return -1;*/ break; } tcflush(fd, TCIFLUSH); return 0; } int setGPSFocuserSpeed(int fd, int speed) { char speed_str[8]; int error_type; int nbytes_write = 0; if (speed == 0) { /*if (portWrite(":FQ#") < 0) return -1;*/ if ((error_type = tty_write_string(fd, ":FQ#", &nbytes_write)) != TTY_OK) return error_type; #ifdef INDI_DEBUG /*IDLog("GPS Focus HALT Command (FQ) \n");*/ #endif return 0; } snprintf(speed_str, 8, ":F%d#", speed); if ((error_type = tty_write_string(fd, speed_str, &nbytes_write)) != TTY_OK) return error_type; #ifdef INDI_DEBUG /*IDLog("GPS Focus Speed command %s \n", speed_str);*/ #endif /*if (portWrite(speed_str) < 0) return -1;*/ tcflush(fd, TCIFLUSH); return 0; } int setTrackFreq(int fd, double trackF) { char temp_string[16]; snprintf(temp_string, sizeof(temp_string), ":ST %04.1f#", trackF); return (setStandardProcedure(fd, temp_string)); } /********************************************************************** * Misc *********************************************************************/ int Slew(int fd) { char slewNum[2]; int error_type; int nbytes_write = 0, nbytes_read = 0; if ((error_type = tty_write_string(fd, ":MS#", &nbytes_write)) != TTY_OK) return error_type; error_type = tty_read(fd, slewNum, 1, IEQ45_TIMEOUT, &nbytes_read); if (nbytes_read < 1) return error_type; /* We don't need to read the string message, just return corresponding error code */ tcflush(fd, TCIFLUSH); if (slewNum[0] == '0') return 0; else if (slewNum[0] == '1') return 1; else return 2; } int MoveTo(int fd, int direction) { int nbytes_write = 0; switch (direction) { case IEQ45_NORTH: tty_write_string(fd, ":Mn#", &nbytes_write); /*portWrite(":Mn#");*/ break; case IEQ45_WEST: tty_write_string(fd, ":Mw#", &nbytes_write); /*portWrite(":Mw#");*/ break; case IEQ45_EAST: tty_write_string(fd, ":Me#", &nbytes_write); /*portWrite(":Me#");*/ break; case IEQ45_SOUTH: tty_write_string(fd, ":Ms#", &nbytes_write); /*portWrite(":Ms#");*/ break; default: break; } tcflush(fd, TCIFLUSH); return 0; } int SendPulseCmd(int fd, int direction, int duration_msec) { int nbytes_write = 0; char cmd[20]; switch (direction) { case IEQ45_NORTH: sprintf(cmd, ":Mgn%04d#", duration_msec); break; case IEQ45_SOUTH: sprintf(cmd, ":Mgs%04d#", duration_msec); break; case IEQ45_EAST: sprintf(cmd, ":Mge%04d#", duration_msec); break; case IEQ45_WEST: sprintf(cmd, ":Mgw%04d#", duration_msec); break; default: return 1; } tty_write_string(fd, cmd, &nbytes_write); tcflush(fd, TCIFLUSH); return 0; } int HaltMovement(int fd, int direction) { int error_type; int nbytes_write = 0; switch (direction) { case IEQ45_NORTH: /*if (portWrite(":Qn#") < 0) return -1;*/ if ((error_type = tty_write_string(fd, ":Qn#", &nbytes_write)) != TTY_OK) return error_type; break; case IEQ45_WEST: /*if (portWrite(":Qw#") < 0) return -1;*/ if ((error_type = tty_write_string(fd, ":Qw#", &nbytes_write)) != TTY_OK) return error_type; break; case IEQ45_EAST: /*if (portWrite(":Qe#") < 0) return -1;*/ if ((error_type = tty_write_string(fd, ":Qe#", &nbytes_write)) != TTY_OK) return error_type; break; case IEQ45_SOUTH: if ((error_type = tty_write_string(fd, ":Qs#", &nbytes_write)) != TTY_OK) return error_type; /*if (portWrite(":Qs#") < 0) return -1;*/ break; case IEQ45_ALL: /*if (portWrite(":Q#") < 0) return -1;*/ if ((error_type = tty_write_string(fd, ":Q#", &nbytes_write)) != TTY_OK) return error_type; break; default: return -1; break; } tcflush(fd, TCIFLUSH); return 0; } int abortSlew(int fd) { /*if (portWrite(":Q#") < 0) return -1;*/ int error_type; int nbytes_write = 0; if ((error_type = tty_write_string(fd, ":Q#", &nbytes_write)) != TTY_OK) return error_type; tcflush(fd, TCIFLUSH); return 0; } int Sync(int fd, char *matchedObject) { int error_type; int nbytes_write = 0, nbytes_read = 0; // if ( (error_type = tty_write_string(fd, ":CM#", &nbytes_write)) != TTY_OK) if ((error_type = tty_write_string(fd, ":CMR#", &nbytes_write)) != TTY_OK) return error_type; /*portWrite(":CM#");*/ /*read_ret = portRead(matchedObject, -1, IEQ45_TIMEOUT);*/ error_type = tty_read_section(fd, matchedObject, '#', IEQ45_TIMEOUT, &nbytes_read); if (nbytes_read < 1) return error_type; matchedObject[nbytes_read - 1] = '\0'; /*IDLog("Matched Object: %s\n", matchedObject);*/ /* Sleep 10ms before flushing. This solves some issues with IEQ45 compatible devices. */ usleep(10000); tcflush(fd, TCIFLUSH); return 0; } int selectSite(int fd, int siteNum) { int error_type; int nbytes_write = 0; switch (siteNum) { case 1: if ((error_type = tty_write_string(fd, ":W1#", &nbytes_write)) != TTY_OK) return error_type; /*if (portWrite(":W1#") < 0) return -1;*/ break; case 2: if ((error_type = tty_write_string(fd, ":W2#", &nbytes_write)) != TTY_OK) return error_type; /*if (portWrite(":W2#") < 0) return -1;*/ break; case 3: if ((error_type = tty_write_string(fd, ":W3#", &nbytes_write)) != TTY_OK) return error_type; /*if (portWrite(":W3#") < 0) return -1;*/ break; case 4: if ((error_type = tty_write_string(fd, ":W4#", &nbytes_write)) != TTY_OK) return error_type; /*if (portWrite(":W4#") < 0) return -1;*/ break; default: return -1; break; } tcflush(fd, TCIFLUSH); return 0; } int selectCatalogObject(int fd, int catalog, int NNNN) { char temp_string[16]; int error_type; int nbytes_write = 0; switch (catalog) { case IEQ45_STAR_C: snprintf(temp_string, sizeof(temp_string), ":LS%d#", NNNN); break; case IEQ45_DEEPSKY_C: snprintf(temp_string, sizeof(temp_string), ":LC%d#", NNNN); break; case IEQ45_MESSIER_C: snprintf(temp_string, sizeof(temp_string), ":LM%d#", NNNN); break; default: return -1; } if ((error_type = tty_write_string(fd, temp_string, &nbytes_write)) != TTY_OK) return error_type; /*if (portWrite(temp_string) < 0) return -1;*/ tcflush(fd, TCIFLUSH); return 0; } int selectSubCatalog(int fd, int catalog, int subCatalog) { char temp_string[16]; switch (catalog) { case IEQ45_STAR_C: snprintf(temp_string, sizeof(temp_string), ":LsD%d#", subCatalog); break; case IEQ45_DEEPSKY_C: snprintf(temp_string, sizeof(temp_string), ":LoD%d#", subCatalog); break; case IEQ45_MESSIER_C: return 1; default: return 0; } return (setStandardProcedure(fd, temp_string)); } int checkIEQ45Format(int fd) { char temp_string[16]; controller_format = IEQ45_LONG_FORMAT; int error_type; int nbytes_write = 0, nbytes_read = 0; if ((error_type = tty_write_string(fd, ":GR#", &nbytes_write)) != TTY_OK) return error_type; /*if (portWrite(":GR#") < 0) return -1;*/ /*read_ret = portRead(temp_string, -1, IEQ45_TIMEOUT);*/ error_type = tty_read_section(fd, temp_string, '#', IEQ45_TIMEOUT, &nbytes_read); if (nbytes_read < 1) return error_type; temp_string[nbytes_read - 1] = '\0'; /* Check whether it's short or long */ if (temp_string[5] == '.') { controller_format = IEQ45_SHORT_FORMAT; return 0; } else return 0; } int selectTrackingMode(int fd, int trackMode) { int error_type; int nbytes_write = 0; switch (trackMode) { case IEQ45_TRACK_SIDERAL: #ifdef INDI_DEBUG IDLog("Setting tracking mode to sidereal.\n"); #endif if ((error_type = tty_write_string(fd, ":RT2#", &nbytes_write)) != TTY_OK) return error_type; /*if (portWrite(":TQ#") < 0) return -1;*/ break; case IEQ45_TRACK_LUNAR: #ifdef INDI_DEBUG IDLog("Setting tracking mode to LUNAR.\n"); #endif if ((error_type = tty_write_string(fd, ":RT0#", &nbytes_write)) != TTY_OK) return error_type; /*if (portWrite(":TL#") < 0) return -1;*/ break; case IEQ45_TRACK_SOLAR: #ifdef INDI_DEBUG IDLog("Setting tracking mode to SOLAR.\n"); #endif if ((error_type = tty_write_string(fd, ":RT1#", &nbytes_write)) != TTY_OK) return error_type; /*if (portWrite(":TM#") < 0) return -1;*/ break; case IEQ45_TRACK_ZERO: #ifdef INDI_DEBUG IDLog("Setting tracking mode to ZERO.\n"); #endif if ((error_type = tty_write_string(fd, ":RT9#", &nbytes_write)) != TTY_OK) return error_type; /*if (portWrite(":TM#") < 0) return -1;*/ break; default: return -1; break; } tcflush(fd, TCIFLUSH); return 0; } libindi/obsolete/ieq45driver8407.c0000664000175000017500000011424113263645557016143 0ustar jasemjasem#if 0 IEQ45 Driver Copyright (C) 2011 Nacho Mas (mas.ignacio@gmail.com). Only litle changes from lx200basic made it by 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 NOTES on 9407 vs 8406: .-Diferente init V#, .- Diferent response to :MS# .- Diff RT0,1, .. codification #endif #include "ieq45driver.h" #include "indicom.h" #include "indidevapi.h" #ifndef _WIN32 #include #endif #define IEQ45_TIMEOUT 5 /* FD timeout in seconds */ int controller_format; int is8407ver = 0; /************************************************************************** Diagnostics **************************************************************************/ int check_IEQ45_connection(int fd); /************************************************************************** Get Commands: store data in the supplied buffer. Return 0 on success or -1 on failure **************************************************************************/ /* Get Double from Sexagisemal */ int getCommandSexa(int fd, double *value, const char *cmd); /* Get String */ int getCommandString(int fd, char *data, const char *cmd); /* Get Int */ int getCommandInt(int fd, int *value, const char *cmd); /* Get tracking frequency */ int getTrackFreq(int fd, double *value); /* Get site Latitude */ int getSiteLatitude(int fd, int *dd, int *mm); /* Get site Longitude */ int getSiteLongitude(int fd, int *ddd, int *mm); /* Get Calender data */ int getCalendarDate(int fd, char *date); /* Get site Name */ int getSiteName(int fd, char *siteName, int siteNum); /* Get Number of Bars */ int getNumberOfBars(int fd, int *value); /* Get Home Search Status */ int getHomeSearchStatus(int fd, int *status); /* Get OTA Temperature */ int getOTATemp(int fd, double *value); /* Get time format: 12 or 24 */ int getTimeFormat(int fd, int *format); /************************************************************************** Set Commands **************************************************************************/ /* Set Int */ int setCommandInt(int fd, int data, const char *cmd); /* Set Sexigesimal */ int setCommandXYZ(int fd, int x, int y, int z, const char *cmd); /* Common routine for Set commands */ int setStandardProcedure(int fd, char *writeData); /* Set Slew Mode */ int setSlewMode(int fd, int slewMode); /* Set Alignment mode */ int setAlignmentMode(int fd, unsigned int alignMode); /* Set Object RA */ int setObjectRA(int fd, double ra); /* set Object DEC */ int setObjectDEC(int fd, double dec); /* Set Calender date */ int setCalenderDate(int fd, int dd, int mm, int yy); /* Set UTC offset */ int setUTCOffset(int fd, double hours); /* Set Track Freq */ int setTrackFreq(int fd, double trackF); /* Set current site longitude */ int setSiteLongitude(int fd, double Long); /* Set current site latitude */ int setSiteLatitude(int fd, double Lat); /* Set Object Azimuth */ int setObjAz(int fd, double az); /* Set Object Altitude */ int setObjAlt(int fd, double alt); /* Set site name */ int setSiteName(int fd, char *siteName, int siteNum); /* Set maximum slew rate */ int setMaxSlewRate(int fd, int slewRate); /* Set focuser motion */ int setFocuserMotion(int fd, int motionType); /* Set focuser speed mode */ int setFocuserSpeedMode(int fd, int speedMode); /* Set minimum elevation limit */ int setMinElevationLimit(int fd, int min); /* Set maximum elevation limit */ int setMaxElevationLimit(int fd, int max); /************************************************************************** Motion Commands **************************************************************************/ /* Slew to the selected coordinates */ int Slew(int fd); /* Synchronize to the selected coordinates and return the matching object if any */ int Sync(int fd, char *matchedObject); /* Abort slew in all axes */ int abortSlew(int fd); /* Move into one direction, two valid directions can be stacked */ int MoveTo(int fd, int direction); /* Half movement in a particular direction */ int HaltMovement(int fd, int direction); /* Select the tracking mode */ int selectTrackingMode(int fd, int trackMode); /* Select Astro-Physics tracking mode */ int selectAPTrackingMode(int fd, int trackMode); /* Send Pulse-Guide command (timed guide move), two valid directions can be stacked */ int SendPulseCmd(int fd, int direction, int duration_msec); /************************************************************************** Other Commands **************************************************************************/ /* Ensures IEQ45 RA/DEC format is long */ int checkIEQ45Format(int fd); /* Select a site from the IEQ45 controller */ int selectSite(int fd, int siteNum); /* Select a catalog object */ int selectCatalogObject(int fd, int catalog, int NNNN); /* Select a sub catalog */ int selectSubCatalog(int fd, int catalog, int subCatalog); int check_IEQ45_connection(int in_fd) { int i = 0; char firmwareVersion[] = ":V#"; char MountInfo[] = ":MountInfo#"; char response[64]; int nbytes_read = 0; #ifdef INDI_DEBUG IDLog("Testing telescope's connection using :V# command...\n"); #endif if (in_fd <= 0) return -1; for (i = 0; i < 2; i++) { if (write(in_fd, firmwareVersion, sizeof(firmwareVersion)) < 0) return -1; tty_read(in_fd, response, 1, IEQ45_TIMEOUT, &nbytes_read); if (nbytes_read != 1) return -1; usleep(50000); } #ifdef INDI_DEBUG IDLog("Initializating telescope's using :MountInfo# command...\n"); #endif for (i = 0; i < 2; i++) { if (write(in_fd, MountInfo, sizeof(MountInfo)) < 0) return -1; tty_read(in_fd, response, 1, IEQ45_TIMEOUT, &nbytes_read); if (nbytes_read == 1) return 0; usleep(50000); } return -1; } /********************************************************************** * GET **********************************************************************/ void remove_spaces(char *texto_recibe) { char *texto_sin_espacio; for (texto_sin_espacio = texto_recibe; *texto_recibe; texto_recibe++) { if (isspace(*texto_recibe)) continue; *texto_sin_espacio++ = *texto_recibe; } *texto_sin_espacio = '\0'; } int getCommandSexa(int fd, double *value, const char *cmd) { char temp_string[16]; int error_type; int nbytes_write = 0, nbytes_read = 0; tcflush(fd, TCIFLUSH); if ((error_type = tty_write_string(fd, cmd, &nbytes_write)) != TTY_OK) return error_type; /*if ( (read_ret = portRead(temp_string, -1, IEQ45_TIMEOUT)) < 1) return read_ret;*/ tty_read_section(fd, temp_string, '#', IEQ45_TIMEOUT, &nbytes_read); temp_string[nbytes_read - 1] = '\0'; /*IDLog("getComandSexa: %s\n", temp_string);*/ //IEQ45 sometimes send a malformed RA/DEC (intermediate spaces) //so I clean before: remove_spaces(temp_string); if (f_scansexa(temp_string, value)) { #ifdef INDI_DEBUG IDLog("unable to process [%s]\n", temp_string); #endif return -1; } tcflush(fd, TCIFLUSH); return 0; } int getCommandInt(int fd, int *value, const char *cmd) { char temp_string[16]; float temp_number; int error_type; int nbytes_write = 0, nbytes_read = 0; tcflush(fd, TCIFLUSH); if ((error_type = tty_write_string(fd, cmd, &nbytes_write)) != TTY_OK) return error_type; tty_read_section(fd, temp_string, '#', IEQ45_TIMEOUT, &nbytes_read); temp_string[nbytes_read - 1] = '\0'; /* Float */ if (strchr(temp_string, '.')) { if (sscanf(temp_string, "%f", &temp_number) != 1) return -1; *value = (int)temp_number; } /* Int */ else if (sscanf(temp_string, "%d", value) != 1) return -1; return 0; } int getCommandString(int fd, char *data, const char *cmd) { char *term; int error_type; int nbytes_write = 0, nbytes_read = 0; /*if (portWrite(cmd) < 0) return -1;*/ if ((error_type = tty_write_string(fd, cmd, &nbytes_write)) != TTY_OK) return error_type; /*read_ret = portRead(data, -1, IEQ45_TIMEOUT);*/ error_type = tty_read_section(fd, data, '#', IEQ45_TIMEOUT, &nbytes_read); tcflush(fd, TCIFLUSH); if (error_type != TTY_OK) return error_type; term = strchr(data, '#'); if (term) *term = '\0'; #ifdef INDI_DEBUG /*IDLog("Requested data: %s\n", data);*/ #endif return 0; } int getCalendarDate(int fd, char *date) { int dd, mm, yy; int error_type; int nbytes_read = 0; char mell_prefix[3]; if ((error_type = getCommandString(fd, date, ":GC#"))) return error_type; /* Meade format is MM/DD/YY */ nbytes_read = sscanf(date, "%d%*c%d%*c%d", &mm, &dd, &yy); if (nbytes_read < 3) return -1; /* We consider years 50 or more to be in the last century, anything less in the 21st century.*/ if (yy > 50) strncpy(mell_prefix, "19", 3); else strncpy(mell_prefix, "20", 3); /* We need to have in in YYYY/MM/DD format */ snprintf(date, 16, "%s%02d/%02d/%02d", mell_prefix, yy, mm, dd); return (0); } int getTimeFormat(int fd, int *format) { char temp_string[16]; int error_type; int nbytes_write = 0, nbytes_read = 0; int tMode; /*if (portWrite(":Gc#") < 0) return -1;*/ if ((error_type = tty_write_string(fd, ":Gc#", &nbytes_write)) != TTY_OK) return error_type; /*read_ret = portRead(temp_string, -1, IEQ45_TIMEOUT);*/ if ((error_type = tty_read_section(fd, temp_string, '#', IEQ45_TIMEOUT, &nbytes_read)) != TTY_OK) return error_type; tcflush(fd, TCIFLUSH); if (nbytes_read < 1) return error_type; temp_string[nbytes_read - 1] = '\0'; nbytes_read = sscanf(temp_string, "(%d)", &tMode); if (nbytes_read < 1) return -1; else *format = tMode; return 0; } int getSiteName(int fd, char *siteName, int siteNum) { char *term; int error_type; int nbytes_write = 0, nbytes_read = 0; switch (siteNum) { case 1: /*if (portWrite(":GM#") < 0) return -1;*/ if ((error_type = tty_write_string(fd, ":GM#", &nbytes_write)) != TTY_OK) return error_type; break; case 2: /*if (portWrite(":GN#") < 0) return -1;*/ if ((error_type = tty_write_string(fd, ":GN#", &nbytes_write)) != TTY_OK) return error_type; break; case 3: /*if (portWrite(":GO#") < 0) return -1;*/ if ((error_type = tty_write_string(fd, ":GO#", &nbytes_write)) != TTY_OK) return error_type; break; case 4: /*if (portWrite(":GP#") < 0) return -1;*/ if ((error_type = tty_write_string(fd, ":GP#", &nbytes_write)) != TTY_OK) return error_type; break; default: return -1; } /*read_ret = portRead(siteName, -1, IEQ45_TIMEOUT);*/ error_type = tty_read_section(fd, siteName, '#', IEQ45_TIMEOUT, &nbytes_read); tcflush(fd, TCIFLUSH); if (nbytes_read < 1) return error_type; siteName[nbytes_read - 1] = '\0'; term = strchr(siteName, ' '); if (term) *term = '\0'; term = strchr(siteName, '<'); if (term) strcpy(siteName, "unused site"); #ifdef INDI_DEBUG IDLog("Requested site name: %s\n", siteName); #endif return 0; } int getSiteLatitude(int fd, int *dd, int *mm) { char temp_string[16]; int error_type; int nbytes_write = 0, nbytes_read = 0; /*if (portWrite(":Gt#") < 0) return -1;*/ if ((error_type = tty_write_string(fd, ":Gt#", &nbytes_write)) != TTY_OK) return error_type; /*read_ret = portRead(temp_string, -1, IEQ45_TIMEOUT);*/ error_type = tty_read_section(fd, temp_string, '#', IEQ45_TIMEOUT, &nbytes_read); tcflush(fd, TCIFLUSH); if (nbytes_read < 1) return error_type; temp_string[nbytes_read - 1] = '\0'; if (sscanf(temp_string, "%d%*c%d", dd, mm) < 2) return -1; #ifdef INDI_DEBUG fprintf(stderr, "Requested site latitude in String %s\n", temp_string); fprintf(stderr, "Requested site latitude %d:%d\n", *dd, *mm); #endif return 0; } int getSiteLongitude(int fd, int *ddd, int *mm) { char temp_string[16]; int error_type; int nbytes_write = 0, nbytes_read = 0; if ((error_type = tty_write_string(fd, ":Gg#", &nbytes_write)) != TTY_OK) return error_type; /*if (portWrite(":Gg#") < 0) return -1;*/ error_type = tty_read_section(fd, temp_string, '#', IEQ45_TIMEOUT, &nbytes_read); /*read_ret = portRead(temp_string, -1, IEQ45_TIMEOUT);*/ tcflush(fd, TCIFLUSH); if (nbytes_read < 1) return error_type; temp_string[nbytes_read - 1] = '\0'; if (sscanf(temp_string, "%d%*c%d", ddd, mm) < 2) return -1; #ifdef INDI_DEBUG fprintf(stderr, "Requested site longitude in String %s\n", temp_string); fprintf(stderr, "Requested site longitude %d:%d\n", *ddd, *mm); #endif return 0; } int getTrackFreq(int fd, double *value) { float Freq; char temp_string[16]; int error_type; int nbytes_write = 0, nbytes_read = 0; if ((error_type = tty_write_string(fd, ":GT#", &nbytes_write)) != TTY_OK) return error_type; /*if (portWrite(":GT#") < 0) return -1;*/ /*read_ret = portRead(temp_string, -1, IEQ45_TIMEOUT);*/ error_type = tty_read_section(fd, temp_string, '#', IEQ45_TIMEOUT, &nbytes_read); tcflush(fd, TCIFLUSH); if (nbytes_read < 1) return error_type; temp_string[nbytes_read] = '\0'; /*fprintf(stderr, "Telescope tracking freq str: %s\n", temp_string);*/ if (sscanf(temp_string, "%f#", &Freq) < 1) return -1; *value = (double)Freq; #ifdef INDI_DEBUG fprintf(stderr, "Tracking frequency value is %f\n", Freq); #endif return 0; } int getNumberOfBars(int fd, int *value) { char temp_string[128]; int error_type; int nbytes_write = 0, nbytes_read = 0; if ((error_type = tty_write_string(fd, ":D#", &nbytes_write)) != TTY_OK) return error_type; /*if (portWrite(":D#") < 0) return -1;*/ error_type = tty_read_section(fd, temp_string, '#', IEQ45_TIMEOUT, &nbytes_read); tcflush(fd, TCIFLUSH); if (nbytes_read < 0) return error_type; *value = nbytes_read - 1; return 0; } int getHomeSearchStatus(int fd, int *status) { char temp_string[16]; int error_type; int nbytes_write = 0, nbytes_read = 0; if ((error_type = tty_write_string(fd, ":h?#", &nbytes_write)) != TTY_OK) return error_type; /*if (portWrite(":h?#") < 0) return -1;*/ /*read_ret = portRead(temp_string, 1, IEQ45_TIMEOUT);*/ error_type = tty_read_section(fd, temp_string, '#', IEQ45_TIMEOUT, &nbytes_read); tcflush(fd, TCIFLUSH); if (nbytes_read < 1) return error_type; temp_string[1] = '\0'; if (temp_string[0] == '0') *status = 0; else if (temp_string[0] == '1') *status = 1; else if (temp_string[0] == '2') *status = 1; return 0; } int getOTATemp(int fd, double *value) { char temp_string[16]; int error_type; int nbytes_write = 0, nbytes_read = 0; float temp; if ((error_type = tty_write_string(fd, ":fT#", &nbytes_write)) != TTY_OK) return error_type; error_type = tty_read_section(fd, temp_string, '#', IEQ45_TIMEOUT, &nbytes_read); if (nbytes_read < 1) return error_type; temp_string[nbytes_read - 1] = '\0'; if (sscanf(temp_string, "%f", &temp) < 1) return -1; *value = (double)temp; return 0; } int updateSkyCommanderCoord(int fd, double *ra, double *dec) { char coords[16]; char CR[1] = { (char)0x0D }; float RA = 0.0, DEC = 0.0; int error_type; int nbytes_read = 0; error_type = write(fd, CR, 1); error_type = tty_read(fd, coords, 16, IEQ45_TIMEOUT, &nbytes_read); /*read_ret = portRead(coords, 16, IEQ45_TIMEOUT);*/ tcflush(fd, TCIFLUSH); nbytes_read = sscanf(coords, " %g %g", &RA, &DEC); if (nbytes_read < 2) { #ifdef INDI_DEBUG IDLog("Error in Sky commander number format [%s], exiting.\n", coords); #endif return error_type; } *ra = RA; *dec = DEC; return 0; } int updateIntelliscopeCoord(int fd, double *ra, double *dec) { char coords[16]; char CR[1] = { (char)0x51 }; /* "Q" */ float RA = 0.0, DEC = 0.0; int error_type; int nbytes_read = 0; /*IDLog ("Sending a Q\n");*/ error_type = write(fd, CR, 1); /* We start at 14 bytes in case its a Sky Wizard, but read one more later it if it's a intelliscope */ /*read_ret = portRead (coords, 14, IEQ45_TIMEOUT);*/ error_type = tty_read(fd, coords, 14, IEQ45_TIMEOUT, &nbytes_read); tcflush(fd, TCIFLUSH); /*IDLog ("portRead() = [%s]\n", coords);*/ /* Remove the Q in the response from the Intelliscope but not the Sky Wizard */ if (coords[0] == 'Q') { coords[0] = ' '; /* Read one more byte if Intelliscope to get the "CR" */ error_type = tty_read(fd, coords, 1, IEQ45_TIMEOUT, &nbytes_read); /*read_ret = portRead (coords, 1, IEQ45_TIMEOUT);*/ } nbytes_read = sscanf(coords, " %g %g", &RA, &DEC); /*IDLog ("sscanf() RA = [%f]\n", RA * 0.0390625);*/ /*IDLog ("sscanf() DEC = [%f]\n", DEC * 0.0390625);*/ /*IDLog ("Intelliscope output [%s]", coords);*/ if (nbytes_read < 2) { #ifdef INDI_DEBUG IDLog("Error in Intelliscope number format [%s], exiting.\n", coords); #endif return -1; } *ra = RA * 0.0390625; *dec = DEC * 0.0390625; return 0; } /********************************************************************** * SET **********************************************************************/ int setStandardProcedure(int fd, char *data) { char bool_return[2]; int error_type; int nbytes_write = 0, nbytes_read = 0; if ((error_type = tty_write_string(fd, data, &nbytes_write)) != TTY_OK) return error_type; error_type = tty_read(fd, bool_return, 1, IEQ45_TIMEOUT, &nbytes_read); /*read_ret = portRead(boolRet, 1, IEQ45_TIMEOUT);*/ tcflush(fd, TCIFLUSH); if (nbytes_read < 1) return error_type; if (bool_return[0] == '0') { #ifdef INDI_DEBUG IDLog("%s Failed.\n", data); #endif return -1; } #ifdef INDI_DEBUG IDLog("%s Successful\n", data); #endif return 0; } int setCommandInt(int fd, int data, const char *cmd) { char temp_string[16]; int error_type; int nbytes_write = 0; snprintf(temp_string, sizeof(temp_string), "%s%d#", cmd, data); if ((error_type = tty_write_string(fd, temp_string, &nbytes_write)) != TTY_OK) return error_type; /* if (portWrite(temp_string) < 0) return -1;*/ return 0; } int setMinElevationLimit(int fd, int min) { char temp_string[16]; snprintf(temp_string, sizeof(temp_string), ":Sh%02d#", min); return (setStandardProcedure(fd, temp_string)); } int setMaxElevationLimit(int fd, int max) { char temp_string[16]; snprintf(temp_string, sizeof(temp_string), ":So%02d*#", max); return (setStandardProcedure(fd, temp_string)); } int setMaxSlewRate(int fd, int slewRate) { char temp_string[16]; if (slewRate < 2 || slewRate > 8) return -1; snprintf(temp_string, sizeof(temp_string), ":Sw%d#", slewRate); return (setStandardProcedure(fd, temp_string)); } int setObjectRA(int fd, double ra) { int h, m, s, frac_m; char temp_string[16]; getSexComponents(ra, &h, &m, &s); frac_m = (s / 60.0) * 10.; if (controller_format == IEQ45_LONG_FORMAT) snprintf(temp_string, sizeof(temp_string), ":Sr %02d:%02d:%02d#", h, m, s); else snprintf(temp_string, sizeof(temp_string), ":Sr %02d:%02d.%01d#", h, m, frac_m); /*IDLog("Set Object RA String %s\n", temp_string);*/ return (setStandardProcedure(fd, temp_string)); } int setObjectDEC(int fd, double dec) { int d, m, s; char temp_string[16]; getSexComponents(dec, &d, &m, &s); switch (controller_format) { case IEQ45_SHORT_FORMAT: /* case with negative zero */ if (!d && dec < 0) snprintf(temp_string, sizeof(temp_string), ":Sd -%02d*%02d#", d, m); else snprintf(temp_string, sizeof(temp_string), ":Sd %+03d*%02d#", d, m); break; case IEQ45_LONG_FORMAT: /* case with negative zero */ if (!d && dec < 0) snprintf(temp_string, sizeof(temp_string), ":Sd -%02d:%02d:%02d#", d, m, s); else snprintf(temp_string, sizeof(temp_string), ":Sd %+03d:%02d:%02d#", d, m, s); break; } /*IDLog("Set Object DEC String %s\n", temp_string);*/ return (setStandardProcedure(fd, temp_string)); } int setCommandXYZ(int fd, int x, int y, int z, const char *cmd) { char temp_string[16]; snprintf(temp_string, sizeof(temp_string), "%s %02d:%02d:%02d#", cmd, x, y, z); return (setStandardProcedure(fd, temp_string)); } int setAlignmentMode(int fd, unsigned int alignMode) { /*fprintf(stderr , "Set alignment mode %d\n", alignMode);*/ int error_type; int nbytes_write = 0; switch (alignMode) { case IEQ45_ALIGN_POLAR: if ((error_type = tty_write_string(fd, ":AP#", &nbytes_write)) != TTY_OK) return error_type; /*if (portWrite(":AP#") < 0) return -1;*/ break; case IEQ45_ALIGN_ALTAZ: if ((error_type = tty_write_string(fd, ":AA#", &nbytes_write)) != TTY_OK) return error_type; /*if (portWrite(":AA#") < 0) return -1;*/ break; case IEQ45_ALIGN_LAND: if ((error_type = tty_write_string(fd, ":AL#", &nbytes_write)) != TTY_OK) return error_type; /*if (portWrite(":AL#") < 0) return -1;*/ break; } tcflush(fd, TCIFLUSH); return 0; } int setCalenderDate(int fd, int dd, int mm, int yy) { char temp_string[32]; char dumpPlanetaryUpdateString[64]; char bool_return[2]; int error_type; int nbytes_write = 0, nbytes_read = 0; yy = yy % 100; snprintf(temp_string, sizeof(temp_string), ":SC %02d/%02d/%02d#", mm, dd, yy); if ((error_type = tty_write_string(fd, temp_string, &nbytes_write)) != TTY_OK) return error_type; /*if (portWrite(temp_string) < 0) return -1;*/ /*read_ret = portRead(boolRet, 1, IEQ45_TIMEOUT);*/ error_type = tty_read(fd, bool_return, 1, IEQ45_TIMEOUT, &nbytes_read); tcflush(fd, TCIFLUSH); if (nbytes_read < 1) return error_type; bool_return[1] = '\0'; if (bool_return[0] == '0') return -1; /* Read dumped data */ error_type = tty_read_section(fd, dumpPlanetaryUpdateString, '#', IEQ45_TIMEOUT, &nbytes_read); error_type = tty_read_section(fd, dumpPlanetaryUpdateString, '#', 5, &nbytes_read); return 0; } int setUTCOffset(int fd, double hours) { char temp_string[16]; snprintf(temp_string, sizeof(temp_string), ":SG %+03d#", (int)hours); /*IDLog("UTC string is %s\n", temp_string);*/ return (setStandardProcedure(fd, temp_string)); } int setSiteLongitude(int fd, double Long) { int d, m, s; char temp_string[32]; getSexComponents(Long, &d, &m, &s); snprintf(temp_string, sizeof(temp_string), ":Sg%03d:%02d#", d, m); return (setStandardProcedure(fd, temp_string)); } int setSiteLatitude(int fd, double Lat) { int d, m, s; char temp_string[32]; getSexComponents(Lat, &d, &m, &s); snprintf(temp_string, sizeof(temp_string), ":St%+03d:%02d:%02d#", d, m, s); return (setStandardProcedure(fd, temp_string)); } int setObjAz(int fd, double az) { int d, m, s; char temp_string[16]; getSexComponents(az, &d, &m, &s); snprintf(temp_string, sizeof(temp_string), ":Sz%03d:%02d#", d, m); return (setStandardProcedure(fd, temp_string)); } int setObjAlt(int fd, double alt) { int d, m, s; char temp_string[16]; getSexComponents(alt, &d, &m, &s); snprintf(temp_string, sizeof(temp_string), ":Sa%+02d*%02d#", d, m); return (setStandardProcedure(fd, temp_string)); } int setSiteName(int fd, char *siteName, int siteNum) { char temp_string[16]; switch (siteNum) { case 1: snprintf(temp_string, sizeof(temp_string), ":SM %s#", siteName); break; case 2: snprintf(temp_string, sizeof(temp_string), ":SN %s#", siteName); break; case 3: snprintf(temp_string, sizeof(temp_string), ":SO %s#", siteName); break; case 4: snprintf(temp_string, sizeof(temp_string), ":SP %s#", siteName); break; default: return -1; } return (setStandardProcedure(fd, temp_string)); } int setSlewMode(int fd, int slewMode) { int error_type; int nbytes_write = 0; switch (slewMode) { case IEQ45_SLEW_MAX: if ((error_type = tty_write_string(fd, ":RS#", &nbytes_write)) != TTY_OK) return error_type; /*if (portWrite(":RS#") < 0) return -1;*/ break; case IEQ45_SLEW_FIND: if ((error_type = tty_write_string(fd, ":RM#", &nbytes_write)) != TTY_OK) return error_type; /*if (portWrite(":RM#") < 0) return -1;*/ break; case IEQ45_SLEW_CENTER: if ((error_type = tty_write_string(fd, ":RC#", &nbytes_write)) != TTY_OK) return error_type; /*if (portWrite(":RC#") < 0) return -1;*/ break; case IEQ45_SLEW_GUIDE: if ((error_type = tty_write_string(fd, ":RG#", &nbytes_write)) != TTY_OK) return error_type; /*if (portWrite(":RG#") < 0) return -1;*/ break; default: break; } tcflush(fd, TCIFLUSH); return 0; } int setFocuserMotion(int fd, int motionType) { int error_type; int nbytes_write = 0; switch (motionType) { case IEQ45_FOCUSIN: if ((error_type = tty_write_string(fd, ":F+#", &nbytes_write)) != TTY_OK) return error_type; #ifdef INDI_DEBUG /*IDLog("Focus IN Command\n");*/ #endif /*if (portWrite(":F+#") < 0) return -1;*/ break; case IEQ45_FOCUSOUT: if ((error_type = tty_write_string(fd, ":F-#", &nbytes_write)) != TTY_OK) return error_type; #ifdef INDI_DEBUG /*IDLog("Focus OUT Command\n");*/ #endif /*if (portWrite(":F-#") < 0) return -1;*/ break; } tcflush(fd, TCIFLUSH); return 0; } int setFocuserSpeedMode(int fd, int speedMode) { int error_type; int nbytes_write = 0; switch (speedMode) { case IEQ45_HALTFOCUS: if ((error_type = tty_write_string(fd, ":FQ#", &nbytes_write)) != TTY_OK) return error_type; #ifdef INDI_DEBUG /*IDLog("Halt Focus Command\n");*/ #endif /* if (portWrite(":FQ#") < 0) return -1;*/ break; case IEQ45_FOCUSSLOW: if ((error_type = tty_write_string(fd, ":FS#", &nbytes_write)) != TTY_OK) return error_type; #ifdef INDI_DEBUG /*IDLog("Focus Slow (FS) Command\n");*/ #endif /*if (portWrite(":FS#") < 0) return -1;*/ break; case IEQ45_FOCUSFAST: if ((error_type = tty_write_string(fd, ":FF#", &nbytes_write)) != TTY_OK) return error_type; #ifdef INDI_DEBUG /*IDLog("Focus Fast (FF) Command\n");*/ #endif /*if (portWrite(":FF#") < 0) return -1;*/ break; } tcflush(fd, TCIFLUSH); return 0; } int setGPSFocuserSpeed(int fd, int speed) { char speed_str[8]; int error_type; int nbytes_write = 0; if (speed == 0) { /*if (portWrite(":FQ#") < 0) return -1;*/ if ((error_type = tty_write_string(fd, ":FQ#", &nbytes_write)) != TTY_OK) return error_type; #ifdef INDI_DEBUG /*IDLog("GPS Focus HALT Command (FQ) \n");*/ #endif return 0; } snprintf(speed_str, 8, ":F%d#", speed); if ((error_type = tty_write_string(fd, speed_str, &nbytes_write)) != TTY_OK) return error_type; #ifdef INDI_DEBUG /*IDLog("GPS Focus Speed command %s \n", speed_str);*/ #endif /*if (portWrite(speed_str) < 0) return -1;*/ tcflush(fd, TCIFLUSH); return 0; } int setTrackFreq(int fd, double trackF) { char temp_string[16]; snprintf(temp_string, sizeof(temp_string), ":ST %04.1f#", trackF); return (setStandardProcedure(fd, temp_string)); } /********************************************************************** * Misc *********************************************************************/ int Slew(int fd) { char slewNum[2]; int error_type; int nbytes_write = 0, nbytes_read = 0; if ((error_type = tty_write_string(fd, ":MS#", &nbytes_write)) != TTY_OK) return error_type; error_type = tty_read(fd, slewNum, 1, IEQ45_TIMEOUT, &nbytes_read); #ifdef INDI_DEBUG //IDLog("SLEW _MS# %s %u \n", slewNum, nbytes_read); #endif if (nbytes_read < 1) return error_type; /* We don't need to read the string message, just return corresponding error code */ tcflush(fd, TCIFLUSH); if (slewNum[0] == '1') return 0; else if (slewNum[0] == '0') return 1; else return 2; } int MoveTo(int fd, int direction) { int nbytes_write = 0; switch (direction) { case IEQ45_NORTH: tty_write_string(fd, ":Mn#", &nbytes_write); /*portWrite(":Mn#");*/ break; case IEQ45_WEST: tty_write_string(fd, ":Mw#", &nbytes_write); /*portWrite(":Mw#");*/ break; case IEQ45_EAST: tty_write_string(fd, ":Me#", &nbytes_write); /*portWrite(":Me#");*/ break; case IEQ45_SOUTH: tty_write_string(fd, ":Ms#", &nbytes_write); /*portWrite(":Ms#");*/ break; default: break; } tcflush(fd, TCIFLUSH); return 0; } int SendPulseCmd(int fd, int direction, int duration_msec) { int nbytes_write = 0; char cmd[20]; switch (direction) { case IEQ45_NORTH: sprintf(cmd, ":Mgn%04d#", duration_msec); break; case IEQ45_SOUTH: sprintf(cmd, ":Mgs%04d#", duration_msec); break; case IEQ45_EAST: sprintf(cmd, ":Mge%04d#", duration_msec); break; case IEQ45_WEST: sprintf(cmd, ":Mgw%04d#", duration_msec); break; default: return 1; } tty_write_string(fd, cmd, &nbytes_write); tcflush(fd, TCIFLUSH); return 0; } int HaltMovement(int fd, int direction) { int error_type; int nbytes_write = 0; switch (direction) { case IEQ45_NORTH: /*if (portWrite(":Qn#") < 0) return -1;*/ if ((error_type = tty_write_string(fd, ":Qn#", &nbytes_write)) != TTY_OK) return error_type; break; case IEQ45_WEST: /*if (portWrite(":Qw#") < 0) return -1;*/ if ((error_type = tty_write_string(fd, ":Qw#", &nbytes_write)) != TTY_OK) return error_type; break; case IEQ45_EAST: /*if (portWrite(":Qe#") < 0) return -1;*/ if ((error_type = tty_write_string(fd, ":Qe#", &nbytes_write)) != TTY_OK) return error_type; break; case IEQ45_SOUTH: if ((error_type = tty_write_string(fd, ":Qs#", &nbytes_write)) != TTY_OK) return error_type; /*if (portWrite(":Qs#") < 0) return -1;*/ break; case IEQ45_ALL: /*if (portWrite(":Q#") < 0) return -1;*/ if ((error_type = tty_write_string(fd, ":Q#", &nbytes_write)) != TTY_OK) return error_type; break; default: return -1; break; } tcflush(fd, TCIFLUSH); return 0; } int abortSlew(int fd) { /*if (portWrite(":Q#") < 0) return -1;*/ int error_type; int nbytes_write = 0; if ((error_type = tty_write_string(fd, ":Q#", &nbytes_write)) != TTY_OK) return error_type; tcflush(fd, TCIFLUSH); return 0; } int Sync(int fd, char *matchedObject) { int error_type; int nbytes_write = 0, nbytes_read = 0; // if ( (error_type = tty_write_string(fd, ":CM#", &nbytes_write)) != TTY_OK) if ((error_type = tty_write_string(fd, ":CMR#", &nbytes_write)) != TTY_OK) return error_type; /*portWrite(":CM#");*/ /*read_ret = portRead(matchedObject, -1, IEQ45_TIMEOUT);*/ error_type = tty_read_section(fd, matchedObject, '#', IEQ45_TIMEOUT, &nbytes_read); if (nbytes_read < 1) return error_type; matchedObject[nbytes_read - 1] = '\0'; /*IDLog("Matched Object: %s\n", matchedObject);*/ /* Sleep 10ms before flushing. This solves some issues with IEQ45 compatible devices. */ usleep(10000); tcflush(fd, TCIFLUSH); return 0; } int selectSite(int fd, int siteNum) { int error_type; int nbytes_write = 0; switch (siteNum) { case 1: if ((error_type = tty_write_string(fd, ":W1#", &nbytes_write)) != TTY_OK) return error_type; /*if (portWrite(":W1#") < 0) return -1;*/ break; case 2: if ((error_type = tty_write_string(fd, ":W2#", &nbytes_write)) != TTY_OK) return error_type; /*if (portWrite(":W2#") < 0) return -1;*/ break; case 3: if ((error_type = tty_write_string(fd, ":W3#", &nbytes_write)) != TTY_OK) return error_type; /*if (portWrite(":W3#") < 0) return -1;*/ break; case 4: if ((error_type = tty_write_string(fd, ":W4#", &nbytes_write)) != TTY_OK) return error_type; /*if (portWrite(":W4#") < 0) return -1;*/ break; default: return -1; break; } tcflush(fd, TCIFLUSH); return 0; } int selectCatalogObject(int fd, int catalog, int NNNN) { char temp_string[16]; int error_type; int nbytes_write = 0; switch (catalog) { case IEQ45_STAR_C: snprintf(temp_string, sizeof(temp_string), ":LS%d#", NNNN); break; case IEQ45_DEEPSKY_C: snprintf(temp_string, sizeof(temp_string), ":LC%d#", NNNN); break; case IEQ45_MESSIER_C: snprintf(temp_string, sizeof(temp_string), ":LM%d#", NNNN); break; default: return -1; } if ((error_type = tty_write_string(fd, temp_string, &nbytes_write)) != TTY_OK) return error_type; /*if (portWrite(temp_string) < 0) return -1;*/ tcflush(fd, TCIFLUSH); return 0; } int selectSubCatalog(int fd, int catalog, int subCatalog) { char temp_string[16]; switch (catalog) { case IEQ45_STAR_C: snprintf(temp_string, sizeof(temp_string), ":LsD%d#", subCatalog); break; case IEQ45_DEEPSKY_C: snprintf(temp_string, sizeof(temp_string), ":LoD%d#", subCatalog); break; case IEQ45_MESSIER_C: return 1; default: return 0; } return (setStandardProcedure(fd, temp_string)); } int checkIEQ45Format(int fd) { char temp_string[16]; controller_format = IEQ45_LONG_FORMAT; int error_type; int nbytes_write = 0, nbytes_read = 0; if ((error_type = tty_write_string(fd, ":GR#", &nbytes_write)) != TTY_OK) return error_type; /*if (portWrite(":GR#") < 0) return -1;*/ /*read_ret = portRead(temp_string, -1, IEQ45_TIMEOUT);*/ error_type = tty_read_section(fd, temp_string, '#', IEQ45_TIMEOUT, &nbytes_read); if (nbytes_read < 1) return error_type; temp_string[nbytes_read - 1] = '\0'; /* Check whether it's short or long */ if (temp_string[5] == '.') { controller_format = IEQ45_SHORT_FORMAT; return 0; } else return 0; } int selectTrackingMode(int fd, int trackMode) { int error_type; int nbytes_write = 0; switch (trackMode) { case IEQ45_TRACK_SIDERAL: #ifdef INDI_DEBUG IDLog("Setting tracking mode to sidereal.\n"); #endif if ((error_type = tty_write_string(fd, ":RT0#", &nbytes_write)) != TTY_OK) return error_type; /*if (portWrite(":TQ#") < 0) return -1;*/ break; case IEQ45_TRACK_LUNAR: #ifdef INDI_DEBUG IDLog("Setting tracking mode to LUNAR.\n"); #endif if ((error_type = tty_write_string(fd, ":RT1#", &nbytes_write)) != TTY_OK) return error_type; /*if (portWrite(":TL#") < 0) return -1;*/ break; case IEQ45_TRACK_SOLAR: #ifdef INDI_DEBUG IDLog("Setting tracking mode to SOLAR.\n"); #endif if ((error_type = tty_write_string(fd, ":RT2#", &nbytes_write)) != TTY_OK) return error_type; /*if (portWrite(":TM#") < 0) return -1;*/ break; case IEQ45_TRACK_ZERO: #ifdef INDI_DEBUG IDLog("Setting tracking mode to custom.\n"); #endif if ((error_type = tty_write_string(fd, ":RT4#", &nbytes_write)) != TTY_OK) return error_type; /*if (portWrite(":TM#") < 0) return -1;*/ break; default: return -1; break; } tcflush(fd, TCIFLUSH); return 0; } libindi/obsolete/fli_wheel.c0000664000175000017500000004515513263645557015422 0ustar jasemjasem#if 0 FLI WHEEL INDI Interface for Finger Lakes Instruments Filter Wheels Copyright (C) 2005 Gaetano Vocca (yagvoc-web AT yahoo DOT it) Based on fli_ccd by Jasem Mutlaq (mutlaqja AT ikarustech DOT 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 #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "libfli.h" #include "indidevapi.h" #include "eventloop.h" #include "indicom.h" void ISInit(void); void getBasicData(void); void ISPoll(void *); void handleExposure(void *); void connectFilter(void); int findwheel(flidomain_t domain); int manageDefaults(char errmsg[]); int checkPowerS(ISwitchVectorProperty *sp); int checkPowerN(INumberVectorProperty *np); int checkPowerT(ITextVectorProperty *tp); int getOnSwitch(ISwitchVectorProperty *sp); int isFilterConnected(void); double min(void); double max(void); extern char *me; extern int errno; #define mydev "FLI Wheel" #define MAIN_GROUP "Main Control" #define LAST_FILTER 14 /* Max slot index */ #define FIRST_FILTER 0 /* Min slot index */ #define currentFilter FilterN[0].value #define POLLMS 1000 #define LIBVERSIZ 1024 #define PREFIXSIZ 64 #define PIPEBUFSIZ 8192 #define FRAME_ILEN 64 typedef struct { flidomain_t domain; char *dname; char *name; char *model; long HWRevision; long FWRevision; long current_filter; long filter_count; long home; } cam_t; static flidev_t fli_dev; static cam_t *FLIWheel; static int portSwitchIndex; static int simulation; static int targetFilter; long int Domains[] = { FLIDOMAIN_USB, FLIDOMAIN_SERIAL, FLIDOMAIN_PARALLEL_PORT, FLIDOMAIN_INET }; /*INDI controls */ /* Connect/Disconnect */ static ISwitch PowerS[] = { { "CONNECT", "Connect", ISS_OFF, 0, 0 }, { "DISCONNECT", "Disconnect", ISS_ON, 0, 0 } }; static ISwitchVectorProperty PowerSP = { mydev, "CONNECTION", "Connection", MAIN_GROUP, IP_RW, ISR_1OFMANY, 60, IPS_IDLE, PowerS, NARRAY(PowerS), "", 0 }; /* Types of Ports */ static ISwitch PortS[] = { { "USB", "", ISS_ON, 0, 0 }, { "Serial", "", ISS_OFF, 0, 0 }, { "Parallel", "", ISS_OFF, 0, 0 }, { "INet", "", ISS_OFF, 0, 0 } }; static ISwitchVectorProperty PortSP = { mydev, "Port Type", "", MAIN_GROUP, IP_RW, ISR_1OFMANY, 0, IPS_IDLE, PortS, NARRAY(PortS), "", 0 }; /* Filter control */ static INumber FilterN[] = { { "FILTER_SLOT_VALUE", "Active Filter", "%2.0f", FIRST_FILTER, LAST_FILTER, 1, 0, 0, 0, 0 } }; static INumberVectorProperty FilterNP = { mydev, "FILTER_SLOT", "Filter", MAIN_GROUP, IP_RW, 0, IPS_IDLE, FilterN, NARRAY(FilterN), "", 0 }; /* send client definitions of all properties */ void ISInit() { static int isInit = 0; if (isInit) return; /* USB by default {USB, SERIAL, PARALLEL, INET} */ portSwitchIndex = 0; targetFilter = 0; /* No Simulation by default */ simulation = 0; /* Enable the following for simulation mode */ /*simulation = 1; IDLog("WARNING: Simulation is on\n");*/ IEAddTimer(POLLMS, ISPoll, NULL); isInit = 1; } void ISGetProperties(const char *dev) { ISInit(); if (dev != nullptr && strcmp(mydev, dev)) return; /* Main Control */ IDDefSwitch(&PowerSP, NULL); IDDefSwitch(&PortSP, NULL); IDDefNumber(&FilterNP, NULL); } void ISNewBLOB(const char *dev, const char *name, int sizes[], int blobsizes[], char *blobs[], char *formats[], char *names[], int n) { INDI_UNUSED(dev); INDI_UNUSED(name); INDI_UNUSED(sizes); INDI_UNUSED(blobsizes); INDI_UNUSED(blobs); INDI_UNUSED(formats); INDI_UNUSED(names); INDI_UNUSED(n); } void ISSnoopDevice(XMLEle *root) { INDI_UNUSED(root); } void ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) { /* ignore if not ours */ if (dev != nullptr && strcmp(dev, mydev)) return; ISInit(); /* Port type */ if (!strcmp(name, PortSP.name)) { PortSP.s = IPS_IDLE; IUResetSwitch(&PortSP); IUUpdateSwitch(&PortSP, states, names, n); portSwitchIndex = getOnSwitch(&PortSP); PortSP.s = IPS_OK; IDSetSwitch(&PortSP, NULL); return; } /* Connection */ if (!strcmp(name, PowerSP.name)) { IUResetSwitch(&PowerSP); IUUpdateSwitch(&PowerSP, states, names, n); connectFilter(); return; } } void ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n) { ISInit(); /* ignore if not ours */ if (dev != nullptr && strcmp(mydev, dev)) return; /* suppress warning */ n = n; dev = dev; name = name; names = names; texts = texts; } void ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) { long err; INumber *np; long newFilter; n = n; /* ignore if not ours */ if (dev != nullptr && strcmp(dev, mydev)) return; ISInit(); if (!strcmp(FilterNP.name, name)) { if (simulation) { targetFilter = values[0]; FilterNP.s = IPS_BUSY; IDSetNumber(&FilterNP, "Setting current filter to slot %d", targetFilter); IDLog("Setting current filter to slot %d\n", targetFilter); return; } if (!isFilterConnected()) { IDMessage(mydev, "Device not connected."); FilterNP.s = IPS_IDLE; IDSetNumber(&FilterNP, NULL); return; } targetFilter = values[0]; np = IUFindNumber(&FilterNP, names[0]); if (!np) { FilterNP.s = IPS_ALERT; IDSetNumber(&FilterNP, "Unknown error. %s is not a member of %s property.", names[0], name); return; } if (targetFilter < FIRST_FILTER || targetFilter > FLIWheel->filter_count - 1) { FilterNP.s = IPS_ALERT; IDSetNumber(&FilterNP, "Error: valid range of filter is from %d to %d", FIRST_FILTER, LAST_FILTER); return; } FilterNP.s = IPS_BUSY; IDSetNumber(&FilterNP, "Setting current filter to slot %d", targetFilter); IDLog("Setting current filter to slot %d\n", targetFilter); if ((err = FLISetFilterPos(fli_dev, targetFilter))) { FilterNP.s = IPS_ALERT; IDSetNumber(&FilterNP, "FLISetFilterPos() failed. %s.", strerror((int)-err)); IDLog("FLISetFilterPos() failed. %s.", strerror((int)-err)); return; } /* Check current filter position */ if ((err = FLIGetFilterPos(fli_dev, &newFilter))) { FilterNP.s = IPS_ALERT; IDSetNumber(&FilterNP, "FLIGetFilterPos() failed. %s.", strerror((int)-err)); IDLog("FLIGetFilterPos() failed. %s.\n", strerror((int)-err)); return; } if (newFilter == targetFilter) { FLIWheel->current_filter = targetFilter; FilterN[0].value = FLIWheel->current_filter; FilterNP.s = IPS_OK; IDSetNumber(&FilterNP, "Filter set to slot #%d", targetFilter); return; } return; } } /* Retrieves basic data from the Wheel upon connection like temperature, array size, firmware..etc */ void getBasicData() { char buff[2048]; long err; if ((err = FLIGetModel(fli_dev, buff, 2048))) { IDMessage(mydev, "FLIGetModel() failed. %s.", strerror((int)-err)); IDLog("FLIGetModel() failed. %s.\n", strerror((int)-err)); return; } else { if ((FLIWheel->model = malloc(sizeof(char) * 2048)) == NULL) { IDMessage(mydev, "malloc() failed."); IDLog("malloc() failed."); return; } strcpy(FLIWheel->model, buff); } if ((err = FLIGetHWRevision(fli_dev, &FLIWheel->HWRevision))) { IDMessage(mydev, "FLIGetHWRevision() failed. %s.", strerror((int)-err)); IDLog("FLIGetHWRevision() failed. %s.\n", strerror((int)-err)); return; } if ((err = FLIGetFWRevision(fli_dev, &FLIWheel->FWRevision))) { IDMessage(mydev, "FLIGetFWRevision() failed. %s.", strerror((int)-err)); IDLog("FLIGetFWRevision() failed. %s.\n", strerror((int)-err)); return; } if ((err = FLIGetFilterCount(fli_dev, &FLIWheel->filter_count))) { IDMessage(mydev, "FLIGetFilterCount() failed. %s.", strerror((int)-err)); IDLog("FLIGetFilterCount() failed. %s.\n", strerror((int)-err)); return; } IDLog("The filter count is %ld\n", FLIWheel->filter_count); FilterN[0].max = FLIWheel->filter_count - 1; FilterNP.s = IPS_OK; IUUpdateMinMax(&FilterNP); IDSetNumber(&FilterNP, "Setting basic data"); IDLog("Exiting getBasicData()\n"); } int manageDefaults(char errmsg[]) { long err; /*IDLog("Resetting filter wheel to slot %d\n", 0); FLIWheel->home = 0; if (( err = FLISetFilterPos(fli_dev, 0))) { IDMessage(mydev, "FLISetFilterPos() failed. %s.", strerror((int)-err)); IDLog("FLISetFilterPos() failed. %s.\n", strerror((int)-err)); return (int)-err; }*/ if ((err = FLIGetFilterPos(fli_dev, &FLIWheel->current_filter))) { IDMessage(mydev, "FLIGetFilterPos() failed. %s.", strerror((int)-err)); IDLog("FLIGetFilterPos() failed. %s.\n", strerror((int)-err)); return (int)-err; } IDLog("The current filter is %ld\n", FLIWheel->current_filter); FilterN[0].value = FLIWheel->current_filter; IDSetNumber(&FilterNP, "Storing defaults"); /* Success */ return 0; } void ISPoll(void *p) { static int simMTC = 5; if (!isFilterConnected()) { IEAddTimer(POLLMS, ISPoll, NULL); return; } switch (FilterNP.s) { case IPS_IDLE: case IPS_OK: break; case IPS_BUSY: /* Simulate that it takes 5 seconds to change slot */ if (simulation) { simMTC--; if (simMTC == 0) { simMTC = 5; currentFilter = targetFilter; FilterNP.s = IPS_OK; IDSetNumber(&FilterNP, "Filter set to slot #%2.0f", currentFilter); break; } IDSetNumber(&FilterNP, NULL); break; } /*if (( err = FLIGetFilterPos(fli_dev, ¤tFilter))) { FilterNP.s = IPS_ALERT; IDSetNumber(&FilterNP, "FLIGetFilterPos() failed. %s.", strerror((int)-err)); IDLog("FLIGetFilterPos() failed. %s.\n", strerror((int)-err)); return; } if (targetFilter == currentFilter) { FLIWheel->current_filter = currentFilter; FilterNP.s = IPS_OK; IDSetNumber(&FilterNP, "Filter set to slot #%2.0f", currentFilter); return; } IDSetNumber(&FilterNP, NULL);*/ break; case IPS_ALERT: break; } IEAddTimer(POLLMS, ISPoll, NULL); } int getOnSwitch(ISwitchVectorProperty *sp) { int i = 0; for (i = 0; i < sp->nsp; i++) { /*IDLog("Switch %s is %s\n", sp->sp[i].name, sp->sp[i].s == ISS_ON ? "On" : "Off");*/ if (sp->sp[i].s == ISS_ON) return i; } return -1; } int checkPowerS(ISwitchVectorProperty *sp) { if (simulation) return 0; if (PowerSP.s != IPS_OK) { if (!strcmp(sp->label, "")) IDMessage(mydev, "Cannot change property %s while the wheel is offline.", sp->name); else IDMessage(mydev, "Cannot change property %s while the wheel is offline.", sp->label); sp->s = IPS_IDLE; IDSetSwitch(sp, NULL); return -1; } return 0; } int checkPowerN(INumberVectorProperty *np) { if (simulation) return 0; if (PowerSP.s != IPS_OK) { if (!strcmp(np->label, "")) IDMessage(mydev, "Cannot change property %s while the wheel is offline.", np->name); else IDMessage(mydev, "Cannot change property %s while the wheel is offline.", np->label); np->s = IPS_IDLE; IDSetNumber(np, NULL); return -1; } return 0; } int checkPowerT(ITextVectorProperty *tp) { if (simulation) return 0; if (PowerSP.s != IPS_OK) { if (!strcmp(tp->label, "")) IDMessage(mydev, "Cannot change property %s while the wheel is offline.", tp->name); else IDMessage(mydev, "Cannot change property %s while the wheel is offline.", tp->label); tp->s = IPS_IDLE; IDSetText(tp, NULL); return -1; } return 0; } void connectFilter() { long err; char errmsg[ERRMSG_SIZE]; /* USB by default {USB, SERIAL, PARALLEL, INET} */ switch (PowerS[0].s) { case ISS_ON: if (simulation) { /* Success! */ PowerS[0].s = ISS_ON; PowerS[1].s = ISS_OFF; PowerSP.s = IPS_OK; IDSetSwitch(&PowerSP, "Simulation Wheel is online."); IDLog("Simulation Wheel is online.\n"); return; } IDLog("Current portSwitch is %d\n", portSwitchIndex); IDLog("Attempting to find the device in domain %ld\n", Domains[portSwitchIndex]); if (findwheel(Domains[portSwitchIndex])) { PowerSP.s = IPS_IDLE; PowerS[0].s = ISS_OFF; PowerS[1].s = ISS_ON; IDSetSwitch(&PowerSP, "Error: no wheels were detected."); IDLog("Error: no wheels were detected.\n"); return; } if ((err = FLIOpen(&fli_dev, FLIWheel->name, FLIWheel->domain | FLIDEVICE_FILTERWHEEL))) { PowerSP.s = IPS_IDLE; PowerS[0].s = ISS_OFF; PowerS[1].s = ISS_ON; IDSetSwitch(&PowerSP, "Error: FLIOpen() failed. %s.", strerror((int)-err)); IDLog("Error: FLIOpen() failed. %s.\n", strerror((int)-err)); return; } /* Success! */ PowerS[0].s = ISS_ON; PowerS[1].s = ISS_OFF; PowerSP.s = IPS_OK; IDSetSwitch(&PowerSP, "Wheel is online. Retrieving basic data."); IDLog("Wheel is online. Retrieving basic data.\n"); getBasicData(); if (manageDefaults(errmsg)) { IDMessage(mydev, errmsg, NULL); IDLog("%s", errmsg); return; } break; case ISS_OFF: if (simulation) { PowerS[0].s = ISS_OFF; PowerS[1].s = ISS_ON; PowerSP.s = IPS_IDLE; IDSetSwitch(&PowerSP, "Wheel is offline."); return; } PowerS[0].s = ISS_OFF; PowerS[1].s = ISS_ON; PowerSP.s = IPS_IDLE; if ((err = FLIClose(fli_dev))) { PowerSP.s = IPS_ALERT; IDSetSwitch(&PowerSP, "Error: FLIClose() failed. %s.", strerror((int)-err)); IDLog("Error: FLIClose() failed. %s.\n", strerror((int)-err)); return; } IDSetSwitch(&PowerSP, "Wheel is offline."); break; } } /* isFilterConnected: return 1 if we have a connection, 0 otherwise */ int isFilterConnected(void) { if (simulation) return 1; return ((PowerS[0].s == ISS_ON) ? 1 : 0); } int findwheel(flidomain_t domain) { char **devlist; long err; IDLog("In find Camera, the domain is %ld\n", domain); if ((err = FLIList(domain | FLIDEVICE_FILTERWHEEL, &devlist))) { IDLog("FLIList() failed. %s\n", strerror((int)-err)); return -1; } if (devlist != NULL && devlist[0] != NULL) { int i; IDLog("Trying to allocate memory to FLIWheel\n"); if ((FLIWheel = malloc(sizeof(cam_t))) == NULL) { IDLog("malloc() failed.\n"); return -1; } for (i = 0; devlist[i] != NULL; i++) { int j; for (j = 0; devlist[i][j] != '\0'; j++) if (devlist[i][j] == ';') { devlist[i][j] = '\0'; break; } } FLIWheel->domain = domain; /* Each driver handles _only_ one camera for now */ switch (domain) { case FLIDOMAIN_PARALLEL_PORT: FLIWheel->dname = strdup("parallel port"); break; case FLIDOMAIN_USB: FLIWheel->dname = strdup("USB"); break; case FLIDOMAIN_SERIAL: FLIWheel->dname = strdup("serial"); break; case FLIDOMAIN_INET: FLIWheel->dname = strdup("inet"); break; default: FLIWheel->dname = strdup("Unknown domain"); } IDLog("Domain set OK\n"); FLIWheel->name = strdup(devlist[0]); if ((err = FLIFreeList(devlist))) { IDLog("FLIFreeList() failed. %s.\n", strerror((int)-err)); return -1; } } /* end if */ else { if ((err = FLIFreeList(devlist))) { IDLog("FLIFreeList() failed. %s.\n", strerror((int)-err)); return -1; } return -1; } IDLog("Findcam() finished successfully.\n"); return 0; } libindi/obsolete/intelliscope.c0000664000175000017500000001514413263645557016151 0ustar jasemjasem#if 0 Intelliscope INDI driver Copyright (C) 2005 Douglas Philipson (dougp AT intermind DOT net) Based on code by 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 "config.h" #include "indidevapi.h" #include "indicom.h" #ifndef _WIN32 #include #endif #define mydev "Intelliscope" #define BASIC_GROUP "Main Control" #define POLLMS 1000 #define currentRA eq[0].value #define currentDEC eq[1].value #define INTELLISCOPE_TIMEOUT 5 static void ISPoll(void *); static void ISInit(void); static void connectTelescope(void); int fd; static ISwitch PowerS[] = { { "CONNECT", "Connect", ISS_OFF, 0, 0 }, { "DISCONNECT", "Disconnect", ISS_ON, 0, 0 } }; ISwitchVectorProperty PowerSP = { mydev, "CONNECTION", "Connection", BASIC_GROUP, IP_RW, ISR_1OFMANY, 0, IPS_IDLE, PowerS, NARRAY(PowerS), "", 0 }; static IText PortT[] = { { "PORT", "Port", 0, 0, 0, 0 } }; static ITextVectorProperty PortTP = { mydev, "DEVICE_PORT", "Ports", BASIC_GROUP, IP_RW, 0, IPS_IDLE, PortT, NARRAY(PortT), "", 0 }; /* equatorial position */ INumber eq[] = { { "RA", "RA H:M:S", "%10.6m", 0., 24., 0., 0., 0, 0, 0 }, { "DEC", "Dec D:M:S", "%10.6m", -90., 90., 0., 0., 0, 0, 0 }, }; INumberVectorProperty eqNum = { mydev, "EQUATORIAL_EOD_COORD", "Equatorial JNow", BASIC_GROUP, IP_RO, 0, IPS_IDLE, eq, NARRAY(eq), "", 0 }; void ISInit(void) { static int isInit = 0; if (isInit) return; isInit = 1; fd = -1; IEAddTimer(POLLMS, ISPoll, NULL); } void ISGetProperties(const char *dev) { ISInit(); dev = dev; IDDefSwitch(&PowerSP, NULL); IDDefText(&PortTP, NULL); IDDefNumber(&eqNum, NULL); } void ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) { ISInit(); dev = dev; if (!strcmp(name, PowerSP.name)) { IUResetSwitch(&PowerSP); IUUpdateSwitch(&PowerSP, states, names, n); connectTelescope(); return; } } void ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n) { ISInit(); dev = dev; names = names; n = n; if (!strcmp(name, PortTP.name)) { IUSaveText(&PortT[0], texts[0]); PortTP.s = IPS_OK; IDSetText(&PortTP, NULL); return; } } void ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) { dev = dev; name = name; values = values; names = names; n = n; } void ISNewBLOB(const char *dev, const char *name, int sizes[], int blobsizes[], char *blobs[], char *formats[], char *names[], int n) { INDI_UNUSED(dev); INDI_UNUSED(name); INDI_UNUSED(sizes); INDI_UNUSED(blobsizes); INDI_UNUSED(blobs); INDI_UNUSED(formats); INDI_UNUSED(names); INDI_UNUSED(n); } void ISSnoopDevice(XMLEle *root) { INDI_UNUSED(root); } int updateIntelliscopeCoord(int fd, double *ra, double *dec) { char coords[16]; char CR[1] = { (char)0x51 }; /* "Q" */ float RA = 0.0, DEC = 0.0; int error_type; int nbytes_read = 0; /*IDLog ("Sending a Q\n");*/ error_type = write(fd, CR, 1); /* We start at 14 bytes in case its a Sky Wizard, but read one more later it if it's a intelliscope */ /*read_ret = portRead (coords, 14, LX200_TIMEOUT);*/ error_type = tty_read(fd, coords, 14, INTELLISCOPE_TIMEOUT, &nbytes_read); tcflush(fd, TCIFLUSH); /*IDLog ("portRead() = [%s]\n", coords);*/ /* Remove the Q in the response from the Intelliscope but not the Sky Wizard */ if (coords[0] == 'Q') { coords[0] = ' '; /* Read one more byte if Intelliscope to get the "CR" */ error_type = tty_read(fd, coords, 1, INTELLISCOPE_TIMEOUT, &nbytes_read); /*read_ret = portRead (coords, 1, LX200_TIMEOUT);*/ } nbytes_read = sscanf(coords, " %g %g", &RA, &DEC); /*IDLog ("sscanf() RA = [%f]\n", RA * 0.0390625);*/ /*IDLog ("sscanf() DEC = [%f]\n", DEC * 0.0390625);*/ /*IDLog ("Intelliscope output [%s]", coords);*/ if (nbytes_read < 2) { #ifdef INDI_DEBUG IDLog("Error in Intelliscope number format [%s], exiting.\n", coords); #endif return -1; } *ra = RA * 0.0390625; *dec = DEC * 0.0390625; return 0; } void ISPoll(void *p) { p = p; if (PowerS[0].s == ISS_ON) { switch (eqNum.s) { case IPS_IDLE: case IPS_OK: case IPS_BUSY: if (updateIntelliscopeCoord(fd, ¤tRA, ¤tDEC) < 0) { eqNum.s = IPS_ALERT; IDSetNumber(&eqNum, "Unknown error while reading telescope coordinates"); IDLog("Unknown error while reading telescope coordinates\n"); break; } IDSetNumber(&eqNum, NULL); break; case IPS_ALERT: break; } } IEAddTimer(POLLMS, ISPoll, NULL); } void connectTelescope(void) { switch (PowerS[0].s) { case ISS_ON: if (tty_connect(PortT[0].text, 9600, 8, 0, 1, &fd) != TTY_OK) { PowerSP.s = IPS_ALERT; IUResetSwitch(&PowerSP); IDSetSwitch(&PowerSP, "Error connecting to port %s", PortT[0].text); return; } PowerSP.s = IPS_OK; IDSetSwitch(&PowerSP, "Intelliscope is online."); break; case ISS_OFF: tty_disconnect(fd); IUResetSwitch(&PowerSP); eqNum.s = PortTP.s = PowerSP.s = IPS_IDLE; IDSetSwitch(&PowerSP, "Intelliscope is offline."); IDSetText(&PortTP, NULL); IDSetNumber(&eqNum, NULL); break; } } libindi/obsolete/skycommander.c0000664000175000017500000001345613263645557016157 0ustar jasemjasem#if 0 Sky Commander INDI driver Copyright (C) 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 "lx200driver.h" #include "indidevapi.h" #include "indicom.h" #ifndef _WIN32 #include #endif #define mydev "Sky Commander" #define BASIC_GROUP "Main Control" #define POLLMS 1000 #define currentRA eq[0].value #define currentDEC eq[1].value #define SKYCOMMANDER_TIMEOUT 5 static void ISPoll(void *); static void ISInit(void); static void connectTelescope(void); int fd; static ISwitch PowerS[] = { { "CONNECT", "Connect", ISS_OFF, 0, 0 }, { "DISCONNECT", "Disconnect", ISS_ON, 0, 0 } }; ISwitchVectorProperty PowerSP = { mydev, "CONNECTION", "Connection", BASIC_GROUP, IP_RW, ISR_1OFMANY, 0, IPS_IDLE, PowerS, NARRAY(PowerS), "", 0 }; static IText PortT[] = { { "PORT", "Port", 0, 0, 0, 0 } }; static ITextVectorProperty PortTP = { mydev, "DEVICE_PORT", "Ports", BASIC_GROUP, IP_RW, 0, IPS_IDLE, PortT, NARRAY(PortT), "", 0 }; /* equatorial position */ INumber eq[] = { { "RA", "RA H:M:S", "%10.6m", 0., 24., 0., 0., 0, 0, 0 }, { "DEC", "Dec D:M:S", "%10.6m", -90., 90., 0., 0., 0, 0, 0 }, }; INumberVectorProperty eqNum = { mydev, "EQUATORIAL_EOD_COORD", "Equatorial JNow", BASIC_GROUP, IP_RO, 0, IPS_IDLE, eq, NARRAY(eq), "", 0 }; void ISInit(void) { static int isInit = 0; if (isInit) return; isInit = 1; fd = -1; IEAddTimer(POLLMS, ISPoll, NULL); } void ISGetProperties(const char *dev) { ISInit(); dev = dev; IDDefSwitch(&PowerSP, NULL); IDDefText(&PortTP, NULL); IDDefNumber(&eqNum, NULL); } void ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) { ISInit(); dev = dev; if (!strcmp(name, PowerSP.name)) { IUResetSwitch(&PowerSP); IUUpdateSwitch(&PowerSP, states, names, n); connectTelescope(); return; } } void ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n) { ISInit(); dev = dev; names = names; n = n; if (!strcmp(name, PortTP.name)) { IUSaveText(&PortT[0], texts[0]); PortTP.s = IPS_OK; IDSetText(&PortTP, NULL); return; } } void ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) { dev = dev; name = name; values = values; names = names; n = n; } void ISNewBLOB(const char *dev, const char *name, int sizes[], int blobsizes[], char *blobs[], char *formats[], char *names[], int n) { INDI_UNUSED(dev); INDI_UNUSED(name); INDI_UNUSED(sizes); INDI_UNUSED(blobsizes); INDI_UNUSED(blobs); INDI_UNUSED(formats); INDI_UNUSED(names); INDI_UNUSED(n); } void ISSnoopDevice(XMLEle *root) { INDI_UNUSED(root); } int updateSkyCommanderCoord(int fd, double *ra, double *dec) { char coords[16]; char CR[1] = { (char)0x0D }; float RA = 0.0, DEC = 0.0; int error_type; int nbytes_read = 0; error_type = write(fd, CR, 1); error_type = tty_read(fd, coords, 16, SKYCOMMANDER_TIMEOUT, &nbytes_read); /*read_ret = portRead(coords, 16, LX200_TIMEOUT);*/ tcflush(fd, TCIFLUSH); nbytes_read = sscanf(coords, " %g %g", &RA, &DEC); if (nbytes_read < 2) { IDLog("Error in Sky commander number format [%s], exiting.\n", coords); return error_type; } *ra = RA; *dec = DEC; return 0; } void ISPoll(void *p) { p = p; if (PowerS[0].s == ISS_ON) { switch (eqNum.s) { case IPS_IDLE: case IPS_OK: case IPS_BUSY: if (updateSkyCommanderCoord(fd, ¤tRA, ¤tDEC) < 0) { eqNum.s = IPS_ALERT; IDSetNumber(&eqNum, "Unknown error while reading telescope coordinates"); IDLog("Unknown error while reading telescope coordinates\n"); break; } IDSetNumber(&eqNum, NULL); break; case IPS_ALERT: break; } } IEAddTimer(POLLMS, ISPoll, NULL); } void connectTelescope(void) { switch (PowerS[0].s) { case ISS_ON: if (tty_connect(PortT[0].text, 9600, 8, 0, 1, &fd) != TTY_OK) { PowerSP.s = IPS_ALERT; IUResetSwitch(&PowerSP); IDSetSwitch(&PowerSP, "Error connecting to port %s", PortT[0].text); return; } PowerSP.s = IPS_OK; IDSetSwitch(&PowerSP, "Sky Commander is online."); break; case ISS_OFF: tty_disconnect(fd); IUResetSwitch(&PowerSP); eqNum.s = PortTP.s = PowerSP.s = IPS_IDLE; IDSetSwitch(&PowerSP, "Sky Commander is offline."); IDSetText(&PortTP, NULL); IDSetNumber(&eqNum, NULL); break; } } libindi/obsolete/celestronprotocol.h0000664000175000017500000000746213263645557017250 0ustar jasemjasem/* * Header File for the Telescope Control protocols for the Meade LX200 * Author: John Kielkopf (kielkopf@louisville.edu) * * This file contains header information used in common with xmtel. 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 * * 15 May 2003 -- Version 2.00 * */ #pragma once #include /* These are user defined quantities that set the limits over which it */ /* is safe to operate the telescope. */ /* LOWER is the number of degrees from the zenith that you will allow. */ /* Use 80, for example, to keep the eyepiece end out of the fork arm space */ /* of an LX200 telescope. */ #define LOWER 90. /* HIGHER is the horizon. 0 is an unobstructed horizon in every direction. */ /* Use 10, for example, to limit sighting below 10 degrees above the horizon. */ #define HIGHER 0. /* Set this if a slew to the north sends the telescope south. */ #define REVERSE_NS 0 /* 1 for reverse; 0 for normal. */ /* Set this for maximum slew rate allowed in degree/sec. */ #define MAXSLEWRATE 4 /* 2 for safety; 4 for 16-inch; 8 otherwise. */ /* The following parameters are used internally to set speed and direction. */ /* Do not change these values. */ #define SLEW 0 #define FIND 1 #define CENTER 2 #define GUIDE 3 #if REVERSE_NS > 0 #define NORTH 3 #define SOUTH 0 #else #define NORTH 0 #define SOUTH 3 #endif #define EAST 2 #define WEST 1 /* Slew speed defines */ #define SLEWRATE8 8 /* should be 8 degrees per second (not 16-inch) */ #define SLEWRATE4 4 /* should be 4 degrees per second */ #define SLEWRATE3 3 /* should be 3 degrees per second */ #define SLEWRATE2 2 /* should be 2 degrees per second */ /* Reticle defines */ #define BRIGHTER 16 /* increase */ #define DIMMER 8 /* decrease */ #define BLINK0 0 /* no blinking */ #define BLINK1 1 /* blink rate 1 */ #define BLINK2 2 /* blink rate 2 */ #define BLINK3 4 /* blink rate 3 */ /* Focus defines */ #define FOCUSOUT 8 /* positive voltage output */ #define FOCUSIN 4 /* negative voltage output */ #define FOCUSSTOP 0 /* no output */ #define FOCUSSLOW 1 /* half voltage */ #define FOCUSFAST 2 /* full voltage */ /* Rotator defines */ #define ROTATORON 1 /* image rotator on */ #define ROTATOROFF 0 /* image rotator off */ /* Fan defines */ #define FANON 1 /* cooling fan on */ #define FANOFF 0 /* cooling fan off */ #ifdef __cplusplus extern "C" { #endif int ConnectTel(char *port); void DisconnectTel(); /* 0 if connection is OK, -1 otherwise */ int CheckConnectTel(); void SetRate(int newRate); void SetLimits(double limitLower, double limitHigher); int StartSlew(int direction); int StopSlew(int direction); double GetRA(); double GetDec(); int SlewToCoords(double newRA, double newDec); int SyncToCoords(double newRA, double newDec); int CheckCoords(double desRA, double desDec, double tolRA, double tolDEC); int isScopeSlewing(); int updateLocation(double lng, double lat); int updateTime(struct ln_date *utc, double utc_offset); void StopNSEW(); int SetSlewRate(); int SyncLST(double newTime); int SyncLocalTime(); void Reticle(int reticle); void Focus(int focus); void Derotator(int rotate); void Fan(int fan); #ifdef __cplusplus } #endif libindi/obsolete/ieq45driver.h0000664000175000017500000002515313263645557015630 0ustar jasemjasem/* IEQ45 Driver Copyright (C) 2011 Nacho Mas (mas.ignacio@gmail.com). Only litle changes from lx200basic made it by 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 */ #pragma once /* Slew speeds */ enum TSlew { IEQ45_SLEW_MAX, IEQ45_SLEW_FIND, IEQ45_SLEW_CENTER, IEQ45_SLEW_GUIDE }; /* Alignment modes */ enum TAlign { IEQ45_ALIGN_POLAR, IEQ45_ALIGN_ALTAZ, IEQ45_ALIGN_LAND }; /* Directions */ enum TDirection { IEQ45_NORTH, IEQ45_WEST, IEQ45_EAST, IEQ45_SOUTH, IEQ45_ALL }; /* Formats of Right ascention and Declenation */ enum TFormat { IEQ45_SHORT_FORMAT, IEQ45_LONG_FORMAT }; /* Time Format */ enum TTimeFormat { IEQ45_24, IEQ45_AM, IEQ45_PM }; /* Focus operation */ enum TFocusMotion { IEQ45_FOCUSIN, IEQ45_FOCUSOUT }; enum TFocusSpeed { IEQ45_HALTFOCUS = 0, IEQ45_FOCUSSLOW, IEQ45_FOCUSFAST }; /* Library catalogs */ enum TCatalog { IEQ45_STAR_C, IEQ45_DEEPSKY_C }; /* Frequency mode */ enum StarCatalog { IEQ45_STAR, IEQ45_SAO, IEQ45_GCVS }; /* Deep Sky Catalogs */ enum DeepSkyCatalog { IEQ45_NGC, IEQ45_IC, IEQ45_UGC, IEQ45_CALDWELL, IEQ45_ARP, IEQ45_ABELL, IEQ45_MESSIER_C }; /* Mount tracking frequency, in Hz */ enum TFreq { IEQ45_TRACK_SIDERAL, IEQ45_TRACK_LUNAR, IEQ45_TRACK_SOLAR, IEQ45_TRACK_ZERO }; #define MaxReticleDutyCycle 15 #define MaxFocuserSpeed 4 /* GET formatted sexagisemal value from device, return as double */ #define getIEQ45RA(fd, x) getCommandSexa(fd, x, ":GR#") //OK #define getIEQ45DEC(fd, x) getCommandSexa(fd, x, ":GD#") //OK //#define getObjectRA(fd, x) getCommandSexa(fd, x, ":Gr#") //NO OK //#define getObjectDEC(fd, x) getCommandSexa(fd, x, ":Gd#") //NO OK //#define getLocalTime12(fd, x) getCommandSexa(fd, x, ":Ga#") //NO OK #define getLocalTime24(fd, x) getCommandSexa(fd, x, ":GL#") //OK #define getSDTime(fd, x) getCommandSexa(fd, x, ":GS#") //OK #define getIEQ45Alt(fd, x) getCommandSexa(fd, x, ":GA#") //OK #define getIEQ45Az(fd, x) getCommandSexa(fd, x, ":GZ#") //OK /* GET String from device and store in supplied buffer x */ //#define getObjectInfo(fd, x) getCommandString(fd, x, ":LI#") //NO OK //#define getVersionDate(fd, x) getCommandString(fd, x, ":GVD#") //NO OK //#define getVersionTime(fd, x) getCommandString(fd, x, ":GVT#") //NO OK //#define getFullVersion(fd, x) getCommandString(fd, x, ":GVF#") //NO OK //#define getVersionNumber(fd, x) getCommandString(fd, x, ":GVN#") //NO OK //#define getProductName(fd, x) getCommandString(fd, x, ":GVP#") //NO OK //#define turnGPS_StreamOn(fd) getCommandString(fd, x, ":gps#") //NO OK /* GET Int from device and store in supplied pointer to integer x */ #define getUTCOffset(fd, x) getCommandInt(fd, x, ":GG#") //OK //#define getMaxElevationLimit(fd, x) getCommandInt(fd, x, ":Go#") //NO OK //#define getMinElevationLimit(fd, x) getCommandInt(fd, x, ":Gh#") //NO OK /* Generic set, x is an integer */ //#define setReticleDutyFlashCycle(fd, x) setCommandInt(fd, x, ":BD") #define setReticleFlashRate(fd, x) setCommandInt(fd, x, ":B") #define setFocuserSpeed(fd, x) setCommandInt(fd, x, ":F") #define setSlewSpeed(fd, x) setCommandInt(fd, x, ":Sw") /* Set X:Y:Z */ #define setLocalTime(fd, x, y, z) setCommandXYZ(fd, x, y, z, ":SL") #define setSDTime(fd, x, y, z) setCommandXYZ(fd, x, y, z, ":SS") /* GPS Specefic */ #define turnGPSOn(fd) write(fd, ":g+#", 5) #define turnGPSOff(fd) write(fd, ":g-#", 5) #define alignGPSScope(fd) write(fd, ":Aa#", 5) #define gpsSleep(fd) write(fd, ":hN#", 5) #define gpsWakeUp(fd) write(fd, ":hW#", 5); #define gpsRestart(fd) write(fd, ":I#", 4); #define updateGPS_System(fd) setStandardProcedure(fd, ":gT#") #define enableDecAltPec(fd) write(fd, ":QA+#", 6) #define disableDecAltPec(fd) write(fd, ":QA-#", 6) #define enableRaAzPec(fd) write(fd, ":QZ+#", 6) #define disableRaAzPec(fd) write(fd, ":QZ-#", 6) #define activateAltDecAntiBackSlash(fd) write(fd, "$BAdd#", 7) #define activateAzRaAntiBackSlash(fd) write(fd, "$BZdd#", 7) #define SelenographicSync(fd) write(fd, ":CL#", 5); #define slewToAltAz(fd) setStandardProcedure(fd, ":MA#") #define toggleTimeFormat(fd) write(fd, ":H#", 4) #define increaseReticleBrightness(fd) write(fd, ":B+#", 5) #define decreaseReticleBrightness(fd) write(fd, ":B-#", 5) #define turnFanOn(fd) write(fd, ":f+#", 5) #define turnFanOff(fd) write(fd, ":f-#", 5) #define seekHomeAndSave(fd) write(fd, ":hS#", 5) #define seekHomeAndSet(fd) write(fd, ":hF#", 5) #define turnFieldDeRotatorOn(fd) write(fd, ":r+#", 5) #define turnFieldDeRotatorOff(fd) write(fd, ":r-#", 5) #define slewToPark(fd) write(fd, ":hP#", 5) #ifdef __cplusplus extern "C" { #endif /************************************************************************** Basic I/O - OBSELETE **************************************************************************/ /*int openPort(const char *portID); int portRead(char *buf, int nbytes, int timeout); int portWrite(const char * buf); int IEQ45readOut(int timeout); int Connect(const char* device); void Disconnect();*/ /************************************************************************** Diagnostics **************************************************************************/ char ACK(int fd); /*int testTelescope(); int testAP();*/ int check_IEQ45_connection(int fd); /************************************************************************** Get Commands: store data in the supplied buffer. Return 0 on success or -1 on failure **************************************************************************/ /* Get Double from Sexagisemal */ int getCommandSexa(int fd, double *value, const char *cmd); /* Get String */ int getCommandString(int fd, char *data, const char *cmd); /* Get Int */ int getCommandInt(int fd, int *value, const char *cmd); /* Get tracking frequency */ int getTrackFreq(int fd, double *value); /* Get site Latitude */ int getSiteLatitude(int fd, int *dd, int *mm); /* Get site Longitude */ int getSiteLongitude(int fd, int *ddd, int *mm); /* Get Calender data */ int getCalendarDate(int fd, char *date); /* Get site Name */ int getSiteName(int fd, char *siteName, int siteNum); /* Get Number of Bars */ int getNumberOfBars(int fd, int *value); /* Get Home Search Status */ int getHomeSearchStatus(int fd, int *status); /* Get OTA Temperature */ int getOTATemp(int fd, double *value); /* Get time format: 12 or 24 */ int getTimeFormat(int fd, int *format); /* Get RA, DEC from Sky Commander controller */ int updateSkyCommanderCoord(int fd, double *ra, double *dec); /* Get RA, DEC from Intelliscope/SkyWizard controllers */ int updateIntelliscopeCoord(int fd, double *ra, double *dec); /************************************************************************** Set Commands **************************************************************************/ /* Set Int */ int setCommandInt(int fd, int data, const char *cmd); /* Set Sexigesimal */ int setCommandXYZ(int fd, int x, int y, int z, const char *cmd); /* Common routine for Set commands */ int setStandardProcedure(int fd, char *writeData); /* Set Slew Mode */ int setSlewMode(int fd, int slewMode); /* Set Alignment mode */ int setAlignmentMode(int fd, unsigned int alignMode); /* Set Object RA */ int setObjectRA(int fd, double ra); /* set Object DEC */ int setObjectDEC(int fd, double dec); /* Set Calender date */ int setCalenderDate(int fd, int dd, int mm, int yy); /* Set UTC offset */ int setUTCOffset(int fd, double hours); /* Set Track Freq */ int setTrackFreq(int fd, double trackF); /* Set current site longitude */ int setSiteLongitude(int fd, double Long); /* Set current site latitude */ int setSiteLatitude(int fd, double Lat); /* Set Object Azimuth */ int setObjAz(int fd, double az); /* Set Object Altitude */ int setObjAlt(int fd, double alt); /* Set site name */ int setSiteName(int fd, char *siteName, int siteNum); /* Set maximum slew rate */ int setMaxSlewRate(int fd, int slewRate); /* Set focuser motion */ int setFocuserMotion(int fd, int motionType); /* SET GPS Focuser raneg (1 to 4) */ int setGPSFocuserSpeed(int fd, int speed); /* Set focuser speed mode */ int setFocuserSpeedMode(int fd, int speedMode); /* Set minimum elevation limit */ int setMinElevationLimit(int fd, int min); /* Set maximum elevation limit */ int setMaxElevationLimit(int fd, int max); /************************************************************************** Motion Commands **************************************************************************/ /* Slew to the selected coordinates */ int Slew(int fd); /* Synchronize to the selected coordinates and return the matching object if any */ int Sync(int fd, char *matchedObject); /* Abort slew in all axes */ int abortSlew(int fd); /* Move into one direction, two valid directions can be stacked */ int MoveTo(int fd, int direction); /* Halt movement in a particular direction */ int HaltMovement(int fd, int direction); /* Select the tracking mode */ int selectTrackingMode(int fd, int trackMode); /* Select Astro-Physics tracking mode */ int selectAPTrackingMode(int fd, int trackMode); /* Send Pulse-Guide command (timed guide move), two valid directions can be stacked */ int SendPulseCmd(int fd, int direction, int duration_msec); /************************************************************************** Other Commands **************************************************************************/ /* Ensures IEQ45 RA/DEC format is long */ int checkIEQ45Format(int fd); /* Select a site from the IEQ45 controller */ int selectSite(int fd, int siteNum); /* Select a catalog object */ int selectCatalogObject(int fd, int catalog, int NNNN); /* Select a sub catalog */ int selectSubCatalog(int fd, int catalog, int subCatalog); #ifdef __cplusplus } #endif libindi/obsolete/celestronprotocol.c0000664000175000017500000005043713263645557017243 0ustar jasemjasem/* * Telescope Control Protocol for Celestron NexStar GPS telescopes * * Copyright 2003 John Kielkopf * John Kielkopf (kielkopf@louisville.edu) * 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 * * 15 May 2003 -- Version 2.00 * * * */ #include "celestronprotocol.h" #ifndef _WIN32 #include #endif #define NULL_PTR(x) (x *)0 /* There are two classes of routines defined here: */ /* XmTel commands to allow easy NexStar access. These */ /* include routines that mimic the extensive LX200 command */ /* language and, for the most part, trap calls and */ /* respond with an error message to the console. */ /* NexStar specific commands and data. */ /* The NexStar command set as documented by Celestron */ /* is very limited. This version of xmtel uses ta few */ /* auxilliary commands which permit direct access to the motor */ /* controllers. */ /* XmTel compatibility commands */ int ConnectTel(char *port); void DisconnectTel(void); int CheckConnectTel(void); void SetRate(int newRate); void SetLimits(double limitLower, double limitHigher); double GetRA(void); double GetDec(void); int SlewToCoords(double newRA, double newDec); int SyncToCoords(double newRA, double newDec); int CheckCoords(double desRA, double desDec, double tolRA, double tolDEC); void StopNSEW(void); int SetSlewRate(void); int SyncLST(double newTime); int SyncLocalTime(void); void Reticle(int reticle); void Focus(int focus); void Derotator(int rotate); void Fan(int fan); static int TelPortFD; static int TelConnectFlag = 0; /* NexStar local data */ static double returnRA; /* Last update of RA */ static double returnDec; /* Last update of Dec */ static int updateRA; /* Set if no RA inquiry since last update */ static int updateDec; /* Set if no Dec inquiry since last update */ static int slewRate; /* Rate for slew request in StartSlew */ /* Coordinate reported by NexStar = true coordinate + offset. */ static double offsetRA = 0; /* Correction to RA from NexStar */ static double offsetDec = 0; /* Correction to Dec from NexStar */ /* NexStar local commands */ void GetRAandDec(void); /* Update RA and Dec from NexStar */ /* Serial communication utilities */ typedef fd_set telfds; static int readn(int fd, char *ptr, int nbytes, int sec); static int writen(int fd, char *ptr, int nbytes); static int telstat(int fd, int sec, int usec); int CheckConnectTel(void) { int numRead; char returnStr[128]; //return TelConnectFlag; tcflush(TelPortFD, TCIOFLUSH); /* Test connection */ writen(TelPortFD, "Kx", 2); numRead = readn(TelPortFD, returnStr, 3, 2); returnStr[numRead] = '\0'; if (numRead == 2) { TelConnectFlag = 1; return (0); } else return -1; } int ConnectTel(char *port) { #ifdef _WIN32 return -1; #else struct termios tty; char returnStr[128]; int numRead; fprintf(stderr, "Connecting to port: %s\n", port); if (TelConnectFlag != 0) return 0; /* Make the connection */ TelPortFD = open(port, O_RDWR); if (TelPortFD == -1) return -1; tcgetattr(TelPortFD, &tty); cfsetospeed(&tty, (speed_t)B9600); cfsetispeed(&tty, (speed_t)B9600); tty.c_cflag = (tty.c_cflag & ~CSIZE) | CS8; tty.c_iflag = IGNBRK; tty.c_lflag = 0; tty.c_oflag = 0; tty.c_cflag |= CLOCAL | CREAD; tty.c_cc[VMIN] = 1; tty.c_cc[VTIME] = 5; tty.c_iflag &= ~(IXON | IXOFF | IXANY); tty.c_cflag &= ~(PARENB | PARODD); tcsetattr(TelPortFD, TCSANOW, &tty); /* Flush the input (read) buffer */ tcflush(TelPortFD, TCIOFLUSH); /* Test connection */ writen(TelPortFD, "Kx", 2); numRead = readn(TelPortFD, returnStr, 3, 2); returnStr[numRead] = '\0'; /* Diagnostic tests */ fprintf(stderr, "ConnectTel read %d characters: %s\n", numRead, returnStr); if (numRead == 2) { TelConnectFlag = 1; return (0); } else return -1; #endif } /* Assign and save slewRate for use in StartSlew */ void SetRate(int newRate) { if (newRate == SLEW) { slewRate = 9; } else if (newRate == FIND) { slewRate = 6; } else if (newRate == CENTER) { slewRate = 3; } else if (newRate == GUIDE) { slewRate = 1; } } /* Start a slew in chosen direction at slewRate */ /* Use auxilliary NexStar command set through the hand control computer */ int StartSlew(int direction) { char slewCmd[] = { 0x50, 0x02, 0x11, 0x24, 0x09, 0x00, 0x00, 0x00 }; char inputStr[2048]; if (direction == NORTH) { slewCmd[2] = 0x11; slewCmd[3] = 0x24; slewCmd[4] = slewRate; } else if (direction == EAST) { slewCmd[2] = 0x10; slewCmd[3] = 0x25; slewCmd[4] = slewRate; } else if (direction == SOUTH) { slewCmd[2] = 0x11; slewCmd[3] = 0x25; slewCmd[4] = slewRate; } else if (direction == WEST) { slewCmd[2] = 0x10; slewCmd[3] = 0x24; slewCmd[4] = slewRate; } writen(TelPortFD, slewCmd, 8); /* Look for '#' acknowledgment of request*/ for (;;) { if (readn(TelPortFD, inputStr, 1, 1)) { if (inputStr[0] == '#') return 0; } else { fprintf(stderr, "No acknowledgment from telescope in StartSlew.\n"); return -1; } } } /* Stop the slew in chosen direction */ int StopSlew(int direction) { char slewCmd[] = { 0x50, 0x02, 0x11, 0x24, 0x00, 0x00, 0x00, 0x00 }; char inputStr[2048]; if (direction == NORTH) { slewCmd[2] = 0x11; slewCmd[3] = 0x24; } else if (direction == EAST) { slewCmd[2] = 0x10; slewCmd[3] = 0x25; } else if (direction == SOUTH) { slewCmd[2] = 0x11; slewCmd[3] = 0x25; } else if (direction == WEST) { slewCmd[2] = 0x10; slewCmd[3] = 0x24; } writen(TelPortFD, slewCmd, 8); /* Look for '#' acknowledgment of request*/ for (;;) { if (readn(TelPortFD, inputStr, 1, 1)) { if (inputStr[0] == '#') return 0; } else { fprintf(stderr, "No acknowledgment from telescope in StartSlew.\n"); return -1; } } } void DisconnectTel(void) { /* printf("DisconnectTel\n"); */ if (TelConnectFlag == 1) close(TelPortFD); TelConnectFlag = 0; } /* Test update status and return the telescope right ascension */ /* Set updateRA flag false */ /* Last telescope readout will be returned if no RA inquiry since then */ /* Otherwise force a new readout */ /* Two successive calls to GetRA will always force a new readout */ double GetRA(void) { if (updateRA != 1) GetRAandDec(); updateRA = 0; return returnRA; } /* Test update status and return the telescope declination */ /* Set updateDec flag false */ /* Last telescope readout will returned if no Dec inquiry since then */ /* Otherwise force a new readout */ /* Two successive calls to GetDec will always force a new readout */ double GetDec(void) { if (updateDec != 1) GetRAandDec(); updateDec = 0; return returnDec; } /* Read the telescope right ascension and declination and set update status */ int isScopeSlewing() { int numRead; char returnStr[128]; writen(TelPortFD, "L", 1); numRead = readn(TelPortFD, returnStr, 2, 2); returnStr[numRead] = '\0'; // 0 Slew complete // 1 Slew in progress return (returnStr[0] == '0' ? 0 : 1); } void GetRAandDec(void) { char returnStr[12]; int countRA, countDec; int numRead; writen(TelPortFD, "E", 1); numRead = readn(TelPortFD, returnStr, 10, 1); returnStr[4] = returnStr[9] = '\0'; /* Diagnostic * * printf("GetRAandDec: %d read %x\n",numRead,returnStr); * */ sscanf(returnStr, "%x", &countRA); sscanf(returnStr + 5, "%x:", &countDec); returnRA = (double)countRA; returnRA = returnRA / (3. * 15. * 60. * 65536. / 64800.); returnDec = (double)countDec; returnDec = returnDec / (3. * 60. * 65536. / 64800.); /* Account for the quadrant in declination */ /* 90 to 180 */ if ((returnDec > 90.) && (returnDec <= 180.)) { returnDec = 180. - returnDec; } /* 180 to 270 */ if ((returnDec > 180.) && (returnDec <= 270.)) { returnDec = returnDec - 270.; } /* 270 to 360 */ if ((returnDec > 270.) && (returnDec <= 360.)) { returnDec = returnDec - 360.; } /* Set update flags */ updateRA = 1; updateDec = 1; /* Correct for offsets and return true coordinate */ /* Coordinate reported by NexStar = true coordinate + offset. */ returnRA = returnRA - offsetRA; returnDec = returnDec - offsetDec; } /* Reset telescope coordinates to new coordinates by adjusting offsets*/ /* Coordinate reported by NexStar = true coordinate + offset. */ int SyncToCoords(double newRA, double newDec) { /* offsetRA = 0.; offsetDec = 0.; GetRAandDec(); offsetRA = returnRA - newRA; offsetDec = returnDec - newDec; return (0);*/ /* 2013-10-19 JM: Trying to support sync in Nextstar command set v4.10+ */ char str[20]; int n1, n2; // so, lets format up a sync command n1 = newRA * 0x1000000 / 24; n2 = newDec * 0x1000000 / 360; n1 = n1 << 8; n2 = n2 << 8; sprintf((char *)str, "s%08X,%08X", n1, n2); writen(TelPortFD, str, 18); /* Look for '#' in response */ for (;;) { if (readn(TelPortFD, str, 1, 2)) { if (str[0] == '#') break; } else fprintf(stderr, "No acknowledgment from telescope after SyncToCoords.\n"); return 4; } return 0; } /* 2013-11-06 JM: support location update in Nextstar command set */ int updateLocation(double lng, double lat) { int lat_d, lat_m, lat_s; int long_d, long_m, long_s; char str[10]; char cmd[8]; // Convert from INDI standard to regular east/west -180 to 180 if (lng > 180) lng -= 360; getSexComponents(lat, &lat_d, &lat_m, &lat_s); getSexComponents(lng, &long_d, &long_m, &long_s); cmd[0] = abs(lat_d); cmd[1] = lat_m; cmd[2] = lat_s; cmd[3] = lat_d > 0 ? 0 : 1; cmd[4] = abs(long_d); cmd[5] = long_m; cmd[6] = long_s; cmd[7] = long_d > 0 ? 0 : 1; sprintf((char *)str, "W%c%c%c%c%c%c%c%c", cmd[0], cmd[1], cmd[2], cmd[3], cmd[4], cmd[5], cmd[6], cmd[7]); writen(TelPortFD, str, 9); /* Look for '#' in response */ for (;;) { if (readn(TelPortFD, str, 1, 2)) { if (str[0] == '#') break; } else fprintf(stderr, "No acknowledgment from telescope after updateLocation.\n"); return 4; } return 0; } int updateTime(struct ln_date *utc, double utc_offset) { char str[10]; char cmd[8]; struct ln_zonedate local_date; // Celestron takes local time ln_date_to_zonedate(utc, &local_date, utc_offset * 3600); cmd[0] = local_date.hours; cmd[1] = local_date.minutes; cmd[2] = local_date.seconds; cmd[3] = local_date.months; cmd[4] = local_date.days; cmd[5] = local_date.years - 2000; if (utc_offset < 0) cmd[6] = 256 - ((unsigned int)fabs(utc_offset)); else cmd[6] = ((unsigned int)fabs(utc_offset)); // Always assume standard time cmd[7] = 0; sprintf((char *)str, "H%c%c%c%c%c%c%c%c", cmd[0], cmd[1], cmd[2], cmd[3], cmd[4], cmd[5], cmd[6], cmd[7]); writen(TelPortFD, str, 9); /* Look for '#' in response */ for (;;) { if (readn(TelPortFD, str, 1, 2)) { if (str[0] == '#') break; } else fprintf(stderr, "No acknowledgment from telescope after updateTime.\n"); return 4; } return 0; } /* Slew to new coordinates */ /* Coordinate sent to NexStar = true coordinate + offset. */ int SlewToCoords(double newRA, double newDec) { int countRA, countDec; char r0, r1, r2, r3, d0, d1, d2, d3; double degs, hrs; char outputStr[32], inputStr[2048]; /* Add offsets */ hrs = newRA + offsetRA; degs = newDec + offsetDec; /* Convert float RA to integer count */ hrs = hrs * (3. * 15. * 60. * 65536. / 64800.); countRA = (int)hrs; /* Account for the quadrant in declination */ if ((newDec >= 0.0) && (newDec <= 90.0)) { degs = degs * (3. * 60. * 65536. / 64800.); } else if ((newDec < 0.0) && (newDec >= -90.0)) { degs = (360. + degs) * (3. * 60. * 65536. / 64800.); } else { fprintf(stderr, "Invalid newDec in SlewToCoords.\n"); return 1; } /* Convert float Declination to integer count */ countDec = (int)degs; /* Convert each integer count to four HEX characters */ /* Inline coding just to be fast */ if (countRA < 65536) { r0 = countRA % 16; if (r0 < 10) { r0 = r0 + 48; } else { r0 = r0 + 55; } countRA = countRA / 16; r1 = countRA % 16; if (r1 < 10) { r1 = r1 + 48; } else { r1 = r1 + 55; } countRA = countRA / 16; r2 = countRA % 16; if (r2 < 10) { r2 = r2 + 48; } else { r2 = r2 + 55; } r3 = countRA / 16; if (r3 < 10) { r3 = r3 + 48; } else { r3 = r3 + 55; } } else { printf("RA count overflow in SlewToCoords.\n"); return 2; } if (countDec < 65536) { d0 = countDec % 16; if (d0 < 10) { d0 = d0 + 48; } else { d0 = d0 + 55; } countDec = countDec / 16; d1 = countDec % 16; if (d1 < 10) { d1 = d1 + 48; } else { d1 = d1 + 55; } countDec = countDec / 16; d2 = countDec % 16; if (d2 < 10) { d2 = d2 + 48; } else { d2 = d2 + 55; } d3 = countDec / 16; if (d3 < 10) { d3 = d3 + 48; } else { d3 = d3 + 55; } } else { fprintf(stderr, "Dec count overflow in SlewToCoords.\n"); return 3; } /* Send the command and characters to the NexStar */ sprintf(outputStr, "R%c%c%c%c,%c%c%c%c", r3, r2, r1, r0, d3, d2, d1, d0); writen(TelPortFD, outputStr, 10); /* Look for '#' in response */ for (;;) { if (readn(TelPortFD, inputStr, 1, 2)) { if (inputStr[0] == '#') break; } else fprintf(stderr, "No acknowledgment from telescope after SlewToCoords.\n"); return 4; } return 0; } /* Test whether the destination has been reached */ /* With the NexStar we use the goto in progress query */ /* Return value is */ /* 0 -- goto in progress */ /* 1 -- goto complete within tolerance */ /* 2 -- goto complete but outside tolerance */ int CheckCoords(double desRA, double desDec, double tolRA, double tolDEC) { double errorRA, errorDec, nowRA, nowDec; char inputStr[2048]; writen(TelPortFD, "L", 1); /* Look for '0#' in response indicating goto is not in progress */ for (;;) { if (readn(TelPortFD, inputStr, 2, 2)) { if ((inputStr[0] == '0') && (inputStr[1] == '#')) break; } else return 0; } nowRA = GetRA(); errorRA = nowRA - desRA; nowDec = GetDec(); errorDec = nowDec - desDec; /* For 6 minute of arc precision; change as needed. */ if (fabs(errorRA) > tolRA || fabs(errorDec) > tolDEC) return 1; else return 2; } /* Set lower and upper limits to protect hardware */ void SetLimits(double limitLower, double limitHigher) { limitLower = limitHigher; fprintf(stderr, "NexStar does not support software limits.\n"); } /* Set slew speed limited by MAXSLEWRATE */ int SetSlewRate(void) { fprintf(stderr, "NexStar does not support remote setting of slew rate.\n"); return 0; } /* Stop all slew motion */ void StopNSEW(void) { char inputStr[2048]; writen(TelPortFD, "M", 1); /* Look for '#' */ for (;;) { if (readn(TelPortFD, inputStr, 1, 1)) { if (inputStr[0] == '#') break; } else { fprintf(stderr, "No acknowledgment from telescope in StopNSEW.\n"); } } } /* Control the reticle function using predefined values */ void Reticle(int reticle) { reticle = reticle; fprintf(stderr, "NexStar does not support remote setting of reticle.\n"); } /* Control the focus using predefined values */ void Focus(int focus) { focus = focus; fprintf(stderr, "NexStar does not support remote setting of focus.\n"); } /* Control the derotator using predefined values */ void Derotator(int rotate) { rotate = rotate; fprintf(stderr, "NexStar does not support an image derotator.\n"); } /* Control the fan using predefined values */ void Fan(int fan) { fan = fan; fprintf(stderr, "NexStar does not have a fan.\n"); } /* Time synchronization utilities */ /* Reset the telescope sidereal time */ int SyncLST(double newTime) { newTime = newTime; fprintf(stderr, "NexStar does not support remote setting of sidereal time.\n"); return -1; } /* Reset the telescope local time */ int SyncLocalTime() { fprintf(stderr, "NexStar does not support remote setting of local time.\n"); return -1; } /* Serial port utilities */ static int writen(fd, ptr, nbytes) int fd; char *ptr; int nbytes; { int nleft, nwritten; nleft = nbytes; while (nleft > 0) { nwritten = write(fd, ptr, nleft); if (nwritten <= 0) break; nleft -= nwritten; ptr += nwritten; } return (nbytes - nleft); } static int readn(fd, ptr, nbytes, sec) int fd; char *ptr; int nbytes; int sec; { int status; int nleft, nread; nleft = nbytes; while (nleft > 0) { status = telstat(fd, sec, 0); if (status <= 0) break; nread = read(fd, ptr, nleft); /* Diagnostic */ /* printf("readn: %d read\n", nread); */ if (nread <= 0) break; nleft -= nread; ptr += nread; } return (nbytes - nleft); } /* * Examines the read status of a file descriptor. * The timeout (sec, usec) specifies a maximum interval to * wait for data to be available in the descriptor. * To effect a poll, the timeout (sec, usec) should be 0. * Returns non-negative value on data available. * 0 indicates that the time limit referred by timeout expired. * On failure, it returns -1 and errno is set to indicate the * error. */ static int telstat(fd, sec, usec) register int fd, sec, usec; { int ret; int width; struct timeval timeout; telfds readfds; memset((char *)&readfds, 0, sizeof(readfds)); FD_SET(fd, &readfds); width = fd + 1; timeout.tv_sec = sec; timeout.tv_usec = usec; ret = select(width, &readfds, NULL_PTR(telfds), NULL_PTR(telfds), &timeout); return (ret); } libindi/obsolete/magellan1.cpp0000664000175000017500000003344313263645557015662 0ustar jasemjasem#if 0 MAGELLAN Generic Copyright (C) 2011 Onno Hommes (ohommes@alumni.cmu.edu) 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 "magellan1.h" #include "magellandriver.h" Magellan1 *telescope = nullptr; extern char *me; #define COMM_GROUP "Communication" #define BASIC_GROUP "Position" #define MAGELLAN_TRACK 0 #define MAGELLAN_SYNC 1 /* Handy Macros */ #define currentRA EquatorialCoordsRN[0].value #define currentDEC EquatorialCoordsRN[1].value static void ISPoll(void *); static void retryConnection(void *); /*INDI Propertries */ /**********************************************************************************************/ /************************************ GROUP: Communication ************************************/ /**********************************************************************************************/ /******************************************** Property: Connection *********************************************/ static ISwitch ConnectS[] = { { "CONNECT", "Connect", ISS_OFF, 0, 0 }, { "DISCONNECT", "Disconnect", ISS_ON, 0, 0 } }; ISwitchVectorProperty ConnectSP = { mydev, "CONNECTION", "Connection", COMM_GROUP, IP_RW, ISR_1OFMANY, 0, IPS_IDLE, ConnectS, NARRAY(ConnectS), "", 0 }; /******************************************** Property: Device Port *********************************************/ /*wildi removed static */ static IText PortT[] = { { "PORT", "Port", 0, 0, 0, 0 } }; ITextVectorProperty PortTP = { mydev, "DEVICE_PORT", "Ports", COMM_GROUP, IP_RW, 0, IPS_IDLE, PortT, NARRAY(PortT), "", 0 }; /**********************************************************************************************/ /************************************ GROUP: Position Display**********************************/ /**********************************************************************************************/ /******************************************** Property: Equatorial Coordinates JNow Perm: RO *********************************************/ INumber EquatorialCoordsRN[] = { { "RA", "RA H:M:S", "%10.6m", 0., 24., 0., 0., 0, 0, 0 }, { "DEC", "Dec D:M:S", "%10.6m", -90., 90., 0., 0., 0, 0, 0 } }; INumberVectorProperty EquatorialCoordsRNP = { mydev, "EQUATORIAL_EOD_COORD", "Equatorial JNow", BASIC_GROUP, IP_RO, 120, IPS_IDLE, EquatorialCoordsRN, NARRAY(EquatorialCoordsRN), "", 0 }; /*****************************************************************************************************/ /**************************************** END PROPERTIES *********************************************/ /*****************************************************************************************************/ /* send client definitions of all properties */ void ISInit() { static int isInit = 0; if (isInit) return; if (telescope == nullptr) { IUSaveText(&PortT[0], "/dev/ttyS0"); telescope = new Magellan1(); telescope->setCurrentDeviceName(mydev); } isInit = 1; IEAddTimer(POLLMS, ISPoll, nullptr); } void ISGetProperties(const char *dev) { ISInit(); telescope->ISGetProperties(dev); } void ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) { ISInit(); telescope->ISNewSwitch(dev, name, states, names, n); } void ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n) { ISInit(); telescope->ISNewText(dev, name, texts, names, n); } void ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) { ISInit(); telescope->ISNewNumber(dev, name, values, names, n); } void ISPoll(void *p) { telescope->ISPoll(); IEAddTimer(POLLMS, ISPoll, nullptr); p = p; } void ISNewBLOB(const char *dev, const char *name, int sizes[], int blobsizes[], char *blobs[], char *formats[], char *names[], int n) { INDI_UNUSED(dev); INDI_UNUSED(name); INDI_UNUSED(sizes); INDI_UNUSED(blobsizes); INDI_UNUSED(blobs); INDI_UNUSED(formats); INDI_UNUSED(names); INDI_UNUSED(n); } void ISSnoopDevice(XMLEle *root) { telescope->ISSnoopDevice(root); } /************************************************** *** MAGELLAN1 Implementation ***************************************************/ Magellan1::Magellan1() { currentSiteNum = 1; trackingMode = MAGELLAN_TRACK_DEFAULT; lastSet = -1; fault = false; simulation = false; currentSet = 0; fd = -1; // Children call parent routines, this is the default IDLog("Initializing from MAGELLAN device...\n"); IDLog("Driver Version: 2011-07-28\n"); } Magellan1::~Magellan1() { } void Magellan1::setCurrentDeviceName(const char *devName) { strcpy(thisDevice, devName); } void Magellan1::ISGetProperties(const char *dev) { if (dev != nullptr && strcmp(thisDevice, dev)) return; // COMM_GROUP IDDefSwitch(&ConnectSP, nullptr); IDDefText(&PortTP, nullptr); // POSITION_GROUP IDDefNumber(&EquatorialCoordsRNP, nullptr); /* Send the basic data to the new client if the previous client(s) are already connected. */ if (ConnectSP.s == IPS_OK) getBasicData(); } void Magellan1::ISSnoopDevice(XMLEle *root) { INDI_UNUSED(root); } void Magellan1::ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n) { IText *tp; /* Ignore other devices */ if (strcmp(dev, thisDevice)) return; /* See if the port is updated */ if (!strcmp(name, PortTP.name)) { PortTP.s = IPS_OK; tp = IUFindText(&PortTP, names[0]); if (!tp) return; IUSaveText(&PortTP.tp[0], texts[0]); IDSetText(&PortTP, nullptr); return; } } void Magellan1::ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) { INDI_UNUSED(dev); INDI_UNUSED(name); INDI_UNUSED(values); INDI_UNUSED(names); INDI_UNUSED(n); } void Magellan1::ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) { INDI_UNUSED(names); /* Ignore other devices */ if (strcmp(thisDevice, dev)) return; // FIRST Switch ALWAYS for Connection if (!strcmp(name, ConnectSP.name)) { bool connectionEstablished = (ConnectS[0].s == ISS_ON); if (IUUpdateSwitch(&ConnectSP, states, names, n) < 0) return; if ((connectionEstablished && ConnectS[0].s == ISS_ON) || (!connectionEstablished && ConnectS[1].s == ISS_ON)) { ConnectSP.s = IPS_OK; IDSetSwitch(&ConnectSP, nullptr); return; } connectTelescope(); return; } } void Magellan1::handleError(ISwitchVectorProperty *svp, int err, const char *msg) { svp->s = IPS_ALERT; /* First check to see if the telescope is connected */ if (check_magellan_connection(fd)) { /* The telescope is off locally */ ConnectS[0].s = ISS_OFF; ConnectS[1].s = ISS_ON; ConnectSP.s = IPS_BUSY; IDSetSwitch(&ConnectSP, "Telescope is not responding to commands, will retry in 10 seconds."); IDSetSwitch(svp, nullptr); IEAddTimer(10000, retryConnection, &fd); return; } /* If the error is a time out, then the device doesn't support this property or busy*/ if (err == -2) { svp->s = IPS_ALERT; IDSetSwitch(svp, "Device timed out. Current device may be busy or does not support %s. Will retry again.", msg); } else /* Changing property failed, user should retry. */ IDSetSwitch(svp, "%s failed.", msg); fault = true; } void Magellan1::handleError(INumberVectorProperty *nvp, int err, const char *msg) { nvp->s = IPS_ALERT; /* First check to see if the telescope is connected */ if (check_magellan_connection(fd)) { /* The telescope is off locally */ ConnectS[0].s = ISS_OFF; ConnectS[1].s = ISS_ON; ConnectSP.s = IPS_BUSY; IDSetSwitch(&ConnectSP, "Telescope is not responding to commands, will retry in 10 seconds."); IDSetNumber(nvp, nullptr); IEAddTimer(10000, retryConnection, &fd); return; } /* If the error is a time out, then the device doesn't support this property */ if (err == -2) { nvp->s = IPS_ALERT; IDSetNumber(nvp, "Device timed out. Current device may be busy or does not support %s. Will retry again.", msg); } else /* Changing property failed, user should retry. */ IDSetNumber(nvp, "%s failed.", msg); fault = true; } void Magellan1::handleError(ITextVectorProperty *tvp, int err, const char *msg) { tvp->s = IPS_ALERT; /* First check to see if the telescope is connected */ if (check_magellan_connection(fd)) { /* The telescope is off locally */ ConnectS[0].s = ISS_OFF; ConnectS[1].s = ISS_ON; ConnectSP.s = IPS_BUSY; IDSetSwitch(&ConnectSP, "Telescope is not responding to commands, will retry in 10 seconds."); IDSetText(tvp, nullptr); IEAddTimer(10000, retryConnection, &fd); return; } /* If the error is a time out, then the device doesn't support this property */ if (err == -2) { tvp->s = IPS_ALERT; IDSetText(tvp, "Device timed out. Current device may be busy or does not support %s. Will retry again.", msg); } else { /* Changing property failed, user should retry. */ IDSetText(tvp, "%s failed.", msg); } fault = true; } void Magellan1::correctFault() { fault = false; IDMessage(thisDevice, "Telescope is online."); } bool Magellan1::isTelescopeOn() { return (ConnectSP.sp[0].s == ISS_ON); } static void retryConnection(void *p) { int fd = *((int *)p); if (check_magellan_connection(fd)) { ConnectSP.s = IPS_IDLE; IDSetSwitch(&ConnectSP, "The connection to the telescope is lost."); return; } ConnectS[0].s = ISS_ON; ConnectS[1].s = ISS_OFF; ConnectSP.s = IPS_OK; IDSetSwitch(&ConnectSP, "The connection to the telescope has been resumed."); } void Magellan1::ISPoll() { int err = 0; if (!isTelescopeOn()) return; if ((err = getMAGELLANRA(fd, ¤tRA)) < 0 || (err = getMAGELLANDEC(fd, ¤tDEC)) < 0) { EquatorialCoordsRNP.s = IPS_ALERT; IDSetNumber(&EquatorialCoordsRNP, nullptr); handleError(&EquatorialCoordsRNP, err, "Getting RA/DEC"); return; } if (fault) correctFault(); EquatorialCoordsRNP.s = IPS_OK; lastRA = currentRA; lastDEC = currentDEC; IDSetNumber(&EquatorialCoordsRNP, nullptr); } void Magellan1::getBasicData() { char calendarDate[32]; int err; /* Magellan 1 Get Calendar Date As a Test (always 1/1/96) */ if ((err = getCalendarDate(fd, calendarDate)) < 0) IDMessage(thisDevice, "Failed to retrieve calendar date from device."); else IDMessage(thisDevice, "Successfully retrieved calendar date from device."); /* Only 24 Time format on Magellan and you cant get to the local time which you can set in Magellan I */ timeFormat = MAGELLAN_24; if ((err = getMAGELLANRA(fd, &targetRA)) < 0 || (err = getMAGELLANDEC(fd, &targetDEC)) < 0) { EquatorialCoordsRNP.s = IPS_ALERT; IDSetNumber(&EquatorialCoordsRNP, nullptr); handleError(&EquatorialCoordsRNP, err, "Getting RA/DEC"); return; } if (fault) correctFault(); EquatorialCoordsRNP.np[0].value = targetRA; EquatorialCoordsRNP.np[1].value = targetDEC; EquatorialCoordsRNP.s = IPS_OK; IDSetNumber(&EquatorialCoordsRNP, nullptr); } void Magellan1::connectTelescope() { switch (ConnectSP.sp[0].s) { case ISS_ON: /* Magellan I only has 1200 buad, 8 data bits, 0 parity and 1 stop bit */ if (tty_connect(PortTP.tp[0].text, 1200, 8, 0, 1, &fd) != TTY_OK) { ConnectS[0].s = ISS_OFF; ConnectS[1].s = ISS_ON; IDSetSwitch( &ConnectSP, "Error connecting to port %s. Make sure you have BOTH write and read permission to your port.\n", PortTP.tp[0].text); return; } if (check_magellan_connection(fd)) { ConnectS[0].s = ISS_OFF; ConnectS[1].s = ISS_ON; IDSetSwitch(&ConnectSP, "Error connecting to Telescope. Telescope is offline."); return; } #ifdef INDI_DEBUG IDLog("Telescope test successful.\n"); #endif ConnectSP.s = IPS_OK; IDSetSwitch(&ConnectSP, "Telescope is online. Retrieving basic data..."); getBasicData(); break; case ISS_OFF: ConnectS[0].s = ISS_OFF; ConnectS[1].s = ISS_ON; ConnectSP.s = IPS_IDLE; IDSetSwitch(&ConnectSP, "Telescope is offline."); IDLog("Telescope is offline."); tty_disconnect(fd); break; } } libindi/obsolete/focusmaster.h0000664000175000017500000000525413263645557016020 0ustar jasemjasem/******************************************************************************* Copyright(c) 2017 Jasem Mutlaq. All rights reserved. Televue FocusMaster Driver. This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2 as published by the Free Software Foundation. . 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 Library General Public License for more details. . You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. *******************************************************************************/ #pragma once #include "indifocuser.h" #include "hidapi.h" class FocusMaster : public INDI::Focuser { public: FocusMaster(); virtual ~FocusMaster() = default; virtual bool ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n); virtual bool ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n); const char *getDefaultName(); virtual bool initProperties(); virtual bool updateProperties(); bool Connect(); bool Disconnect(); void TimerHit(); virtual IPState MoveFocuser(FocusDirection dir, int speed, uint16_t duration); virtual IPState MoveAbsFocuser(uint32_t targetTicks); virtual IPState MoveRelFocuser(FocusDirection dir, uint32_t ticks); virtual bool AbortFocuser(); private: /** * @brief sendCommand Send a command to the focuser and optional read a response * @param command 2-byte unsigned char command. * @param response If nullptr (default) is passed, no response is read. If a valid pointer is passed, the response is read into the buffer up until * MAX_FM_BUF bytes. * @return True if sending command is successful, false otherwise. */ bool sendCommand(const uint8_t *command, char *response = nullptr); bool setPosition(uint32_t ticks); bool getPosition(uint32_t *ticks); bool sync(uint32_t ticks); hid_device *handle { nullptr }; //uint32_t targetPosition { 0 }; struct timeval focusMoveStart { 0, 0 }; float focusMoveRequest { 0 }; float CalcTimeLeft(timeval, float); // Sync to a particular position INumber SyncN[1]; INumberVectorProperty SyncNP; // Full Forward / Reverse ISwitch FullMotionS[2]; ISwitchVectorProperty FullMotionSP; }; libindi/obsolete/magellandriver.c0000664000175000017500000001274013263645557016452 0ustar jasemjasem#if 0 MAGELLAN Driver Copyright (C) 2011 Onno Hommes (ohommes@alumni.cmu.edu) 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 "magellandriver.h" #include "indicom.h" #include "indidevapi.h" #ifndef _WIN32 #include #endif /************************************************************************** Connection Diagnostics **************************************************************************/ char ACK(int fd); int check_magellan_connection(int fd); /************************************************************************** Get Commands: store data in the supplied buffer. Return 0 on success or -1 on failure **************************************************************************/ /* Get Double from Sexagisemal */ int getCommandSexa(int fd, double *value, const char *cmd); /* Get String */ static int getCommandString(int fd, char *data, const char *cmd); /* Get Calender data */ int getCalendarDate(int fd, char *date); /************************************************************************** Driver Implementations **************************************************************************/ /* Check the Magellan Connection using any set command Magellan 1 does not support any sey but still sends 'OK' for it */ int check_magellan_connection(int fd) { int i = 0; /* Magellan I ALways Response OK for :S?# */ char ack[4] = ":S?#"; char Response[64]; int nbytes_read = 0; #ifdef INDI_DEBUG IDLog("Testing telescope's connection...\n"); #endif if (fd <= 0) return MAGELLAN_ERROR; for (i = 0; i < CONNECTION_RETRIES; i++) { if (write(fd, ack, 4) < 0) return MAGELLAN_ERROR; tty_read(fd, Response, 2, MAGELLAN_TIMEOUT, &nbytes_read); if (nbytes_read == 2) return MAGELLAN_OK; usleep(50000); } tcflush(fd, TCIFLUSH); return MAGELLAN_ERROR; } /********************************************************************** * GET FUNCTIONS **********************************************************************/ char ACK(int fd) { /* Magellan I ALways Response OK for :S?# (any set will do)*/ char ack[4] = ":S?#"; char Response[64]; int nbytes_read = 0; int i = 0; int result = MAGELLAN_ERROR; /* Check for Comm handle */ if (fd > 0) { for (i = 0; i < CONNECTION_RETRIES; i++) { /* Send ACK string to Magellan */ if (write(fd, ack, 4) >= 0) { /* Read Response */ tty_read(fd, Response, 2, MAGELLAN_TIMEOUT, &nbytes_read); /* If the two byte OK is returned we have an Ack */ if (nbytes_read == 2) { result = MAGELLAN_ACK; break; /* Force quick success return */ } else usleep(50000); } } } tcflush(fd, TCIFLUSH); return result; } int getCommandSexa(int fd, double *value, const char *cmd) { char temp_string[16]; int result = MAGELLAN_ERROR; int nbytes_write = 0, nbytes_read = 0; if ((tty_write_string(fd, cmd, &nbytes_write)) == TTY_OK) { if ((tty_read_section(fd, temp_string, '#', MAGELLAN_TIMEOUT, &nbytes_read)) == TTY_OK) { temp_string[nbytes_read - 1] = '\0'; if (f_scansexa(temp_string, value)) { #ifdef INDI_DEBUG IDLog("unable to process [%s]\n", temp_string); #endif } else { result = MAGELLAN_OK; } } } tcflush(fd, TCIFLUSH); return result; } static int getCommandString(int fd, char *data, const char *cmd) { int nbytes_write = 0, nbytes_read = 0; int result = MAGELLAN_ERROR; if ((tty_write_string(fd, cmd, &nbytes_write)) == TTY_OK) { if ((tty_read_section(fd, data, '#', MAGELLAN_TIMEOUT, &nbytes_read)) == TTY_OK) { data[nbytes_read - 1] = '\0'; result = MAGELLAN_OK; } } tcflush(fd, TCIFLUSH); return result; } int getCalendarDate(int fd, char *date) { int dd, mm, yy; char century[3]; int result; if ((result = getCommandString(fd, date, "#:GC#"))) { /* Magellan format is MM/DD/YY */ if ((sscanf(date, "%d%*c%d%*c%d", &mm, &dd, &yy)) > 2) { /* Consider years 92 or more to be in the last century, anything less in the 21st century.*/ if (yy > CENTURY_THRESHOLD) strncpy(century, "19", 3); else strncpy(century, "20", 3); /* Format must be YYYY/MM/DD format */ snprintf(date, 16, "%s%02d/%02d/%02d", century, yy, mm, dd); } } return result; } libindi/obsolete/trutech_wheel.c0000664000175000017500000003243713263645557016325 0ustar jasemjasem#if 0 True Technology Filter Wheel Copyright (C) 2006 Jasem Mutlaq (mutlaqja AT ikarustech DOT 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 #include #include #include #include #include #include #include #include #include #include #include #include #include "indidevapi.h" #include "eventloop.h" #include "indicom.h" void ISInit(void); void getBasicData(void); void ISPoll(void *); void handleExposure(void *); void connectFilter(void); int manageDefaults(char errmsg[]); int checkPowerS(ISwitchVectorProperty *sp); int checkPowerN(INumberVectorProperty *np); int checkPowerT(ITextVectorProperty *tp); int getOnSwitch(ISwitchVectorProperty *sp); int isFilterConnected(void); static int targetFilter; static int fd; const unsigned char COMM_PRE = 0x01; const unsigned char COMM_INIT = 0xA5; const unsigned char COMM_FILL = 0x20; #define mydev "TruTech Wheel" #define MAIN_GROUP "Main Control" #define currentFilter FilterPositionN[0].value #define POLLMS 3000 #define DEFAULT_FILTER_COUNT 5 #define MAX_FILTER_COUNT 10 #define ERRMSG_SIZE 1024 #define CMD_SIZE 5 #define CMD_JUNK 64 #define CMD_RESP 15 #define FILTER_TIMEOUT 15 /* 15 Seconds before timeout */ #define FIRST_FILTER 1 #define DEBUG_ON 0 #define SIMULATION_ON 0 /*INDI controls */ /* Connect/Disconnect */ static ISwitch PowerS[] = { { "CONNECT", "Connect", ISS_OFF, 0, 0 }, { "DISCONNECT", "Disconnect", ISS_ON, 0, 0 } }; static ISwitchVectorProperty PowerSP = { mydev, "CONNECTION", "Connection", MAIN_GROUP, IP_RW, ISR_1OFMANY, 60, IPS_IDLE, PowerS, NARRAY(PowerS), "", 0 }; /* Connection Port */ static IText PortT[] = { { "PORT", "Port", 0, 0, 0, 0 } }; static ITextVectorProperty PortTP = { mydev, "DEVICE_PORT", "Ports", MAIN_GROUP, IP_RW, 0, IPS_IDLE, PortT, NARRAY(PortT), "", 0 }; /* Home/Learn Swtich */ static ISwitch HomeS[] = { { "Find", "", ISS_OFF, 0, 0 } }; static ISwitchVectorProperty HomeSP = { mydev, "HOME", "", MAIN_GROUP, IP_RW, ISR_1OFMANY, 60, IPS_IDLE, HomeS, NARRAY(HomeS), "", 0 }; /* Filter Count */ static INumber FilterCountN[] = { { "Count", "", "%2.0f", 0, MAX_FILTER_COUNT, 1, DEFAULT_FILTER_COUNT, 0, 0, 0 } }; static INumberVectorProperty FilterCountNP = { mydev, "Filter Count", "", MAIN_GROUP, IP_RW, 0, IPS_IDLE, FilterCountN, NARRAY(FilterCountN), "", 0 }; /* Filter Position */ static INumber FilterPositionN[] = { { "FILTER_SLOT_VALUE", "Active Filter", "%2.0f", 1, DEFAULT_FILTER_COUNT, 1, 1, 0, 0, 0 } }; static INumberVectorProperty FilterPositionNP = { mydev, "FILTER_SLOT", "Filter", MAIN_GROUP, IP_RW, 0, IPS_IDLE, FilterPositionN, NARRAY(FilterPositionN), "", 0 }; /* send client definitions of all properties */ void ISInit() { static int isInit = 0; if (isInit) return; targetFilter = -1; fd = -1; isInit = 1; } void ISGetProperties(const char *dev) { ISInit(); if (dev != nullptr && strcmp(mydev, dev)) return; /* Main Control */ IDDefSwitch(&PowerSP, NULL); IDDefText(&PortTP, NULL); IDDefNumber(&FilterCountNP, NULL); IDDefSwitch(&HomeSP, NULL); IDDefNumber(&FilterPositionNP, NULL); } void ISNewBLOB(const char *dev, const char *name, int sizes[], int blobsizes[], char *blobs[], char *formats[], char *names[], int n) { INDI_UNUSED(dev); INDI_UNUSED(name); INDI_UNUSED(sizes); INDI_UNUSED(blobsizes); INDI_UNUSED(blobs); INDI_UNUSED(formats); INDI_UNUSED(names); INDI_UNUSED(n); } void ISSnoopDevice(XMLEle *root) { INDI_UNUSED(root); } void ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) { /* ignore if not ours */ if (dev != nullptr && strcmp(dev, mydev)) return; ISInit(); /* Connection */ if (!strcmp(name, PowerSP.name)) { IUUpdateSwitch(&PowerSP, states, names, n); connectFilter(); return; } /* Home Search */ if (!strcmp(name, HomeSP.name)) { int err; int nbytes = 0; unsigned char type = 0x03; unsigned char chksum = COMM_INIT + type + COMM_FILL; unsigned char filter_command[CMD_SIZE]; //snprintf(filter_command, CMD_SIZE, "%c%c%c%c%c", COMM_PRE, COMM_INIT, type, COMM_FILL, chksum); snprintf(filter_command, CMD_SIZE, "%c%c%c%c", COMM_INIT, type, COMM_FILL, chksum); if (checkPowerS(&HomeSP)) return; if (!SIMULATION_ON) err = tty_write(fd, filter_command, CMD_SIZE, &nbytes); if (DEBUG_ON) { IDLog("Home Search Command (int): #%d#%d#%d#%d#\n", COMM_INIT, type, COMM_FILL, chksum); IDLog("Home Search Command (char): #%c#%c#%c#%c#\n", COMM_INIT, type, COMM_FILL, chksum); } /* Send Home Command */ if (err != TTY_OK) { char error_message[ERRMSG_SIZE]; tty_error_msg(err, error_message, ERRMSG_SIZE); HomeSP.s = IPS_ALERT; IDSetSwitch(&HomeSP, "Sending command Home to filter failed. %s", error_message); IDLog("Sending command Home to filter failed. %s\n", error_message); return; } FilterPositionN[0].value = 1; FilterPositionNP.s = IPS_OK; HomeSP.s = IPS_OK; IDSetSwitch(&HomeSP, "Filter set to HOME."); IDSetNumber(&FilterPositionNP, NULL); } } void ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n) { ISInit(); /* ignore if not ours */ if (dev != nullptr && strcmp(mydev, dev)) return; if (!strcmp(name, PortTP.name)) { if (IUUpdateText(&PortTP, texts, names, n)) return; PortTP.s = IPS_OK; IDSetText(&PortTP, NULL); } } void ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) { long err; INumber *np; /* ignore if not ours */ if (dev != nullptr && strcmp(dev, mydev)) return; ISInit(); if (!strcmp(FilterPositionNP.name, name)) { if (!isFilterConnected()) { IDMessage(mydev, "Filter is not connected."); FilterPositionNP.s = IPS_IDLE; IDSetNumber(&FilterPositionNP, NULL); return; } np = IUFindNumber(&FilterPositionNP, names[0]); if (np == &FilterPositionN[0]) { targetFilter = values[0]; int nbytes = 0; unsigned char type = 0x01; unsigned char chksum = COMM_INIT + type + (unsigned char)targetFilter; /*char filter_command[5] = { COMM_PRE, COMM_INIT, type, targetFilter, chksum };*/ unsigned char filter_command[CMD_SIZE]; if (targetFilter < FilterPositionN[0].min || targetFilter > FilterPositionN[0].max) { FilterPositionNP.s = IPS_ALERT; IDSetNumber(&FilterPositionNP, "Error: valid range of filter is from %d to %d", (int)FilterPositionN[0].min, (int)FilterPositionN[0].max); return; } IUUpdateNumber(&FilterPositionNP, values, names, n); //snprintf(filter_command, CMD_SIZE, "%c%c%c%c%c", COMM_PRE, COMM_INIT, type, targetFilter, chksum); snprintf(filter_command, CMD_SIZE, "%c%c%c%c", COMM_INIT, type, targetFilter, chksum); if (DEBUG_ON) { IDLog("Filter Position Command (int): #%d#%d#%d#%d#\n", COMM_INIT, type, targetFilter, chksum); IDLog("Filter Position Command (char): #%c#%c#%c#%c#\n", COMM_INIT, type, targetFilter, chksum); } if (!SIMULATION_ON) err = tty_write(fd, filter_command, CMD_SIZE, &nbytes); FilterPositionNP.s = IPS_OK; IDSetNumber(&FilterPositionNP, "Setting current filter to slot %d", targetFilter); } else { FilterPositionNP.s = IPS_IDLE; IDSetNumber(&FilterPositionNP, NULL); } return; } if (!strcmp(FilterCountNP.name, name)) { if (!isFilterConnected()) { IDMessage(mydev, "Filter is not connected."); FilterCountNP.s = IPS_IDLE; IDSetNumber(&FilterCountNP, NULL); return; } np = IUFindNumber(&FilterCountNP, names[0]); if (np == &FilterCountN[0]) { if (IUUpdateNumber(&FilterCountNP, values, names, n) < 0) return; FilterPositionN[0].min = 1; FilterPositionN[0].max = (int)FilterCountN[0].value; IUUpdateMinMax(&FilterPositionNP); FilterCountNP.s = IPS_OK; IDSetNumber(&FilterCountNP, "Updated number of available filters to %d", ((int)FilterCountN[0].value)); } else { FilterCountNP.s = IPS_IDLE; IDSetNumber(&FilterCountNP, NULL); } return; } } int getOnSwitch(ISwitchVectorProperty *sp) { int i = 0; for (i = 0; i < sp->nsp; i++) { if (sp->sp[i].s == ISS_ON) return i; } return -1; } int checkPowerS(ISwitchVectorProperty *sp) { if (PowerSP.s != IPS_OK) { if (!strcmp(sp->label, "")) IDMessage(mydev, "Cannot change property %s while the wheel is offline.", sp->name); else IDMessage(mydev, "Cannot change property %s while the wheel is offline.", sp->label); sp->s = IPS_IDLE; IDSetSwitch(sp, NULL); return -1; } return 0; } int checkPowerN(INumberVectorProperty *np) { if (PowerSP.s != IPS_OK) { if (!strcmp(np->label, "")) IDMessage(mydev, "Cannot change property %s while the wheel is offline.", np->name); else IDMessage(mydev, "Cannot change property %s while the wheel is offline.", np->label); np->s = IPS_IDLE; IDSetNumber(np, NULL); return -1; } return 0; } int checkPowerT(ITextVectorProperty *tp) { if (PowerSP.s != IPS_OK) { if (!strcmp(tp->label, "")) IDMessage(mydev, "Cannot change property %s while the wheel is offline.", tp->name); else IDMessage(mydev, "Cannot change property %s while the wheel is offline.", tp->label); tp->s = IPS_IDLE; IDSetText(tp, NULL); return -1; } return 0; } void connectFilter() { int err; char errmsg[ERRMSG_SIZE]; switch (PowerS[0].s) { case ISS_ON: if (SIMULATION_ON) { PowerSP.s = IPS_OK; IDSetSwitch(&PowerSP, "Simulated Filter Wheel is online."); return; } if ((err = tty_connect(PortT[0].text, 9600, 8, 0, 1, &fd)) != TTY_OK) { PowerSP.s = IPS_ALERT; IDSetSwitch( &PowerSP, "Error: cannot connect to %s. Make sure the filter is connected and you have access to the port.", PortT[0].text); tty_error_msg(err, errmsg, ERRMSG_SIZE); IDLog("Error: %s\n", errmsg); return; } PowerSP.s = IPS_OK; IDSetSwitch(&PowerSP, "Filter Wheel is online. True Technology filter wheel suffers from several bugs. " "Please refer to http://indi.sf.net/profiles/trutech.html for more details."); break; case ISS_OFF: if (SIMULATION_ON) { PowerSP.s = IPS_OK; IDSetSwitch(&PowerSP, "Simulated Filter Wheel is offline."); return; } if ((err = tty_disconnect(fd)) != TTY_OK) { PowerSP.s = IPS_ALERT; IDSetSwitch(&PowerSP, "Error: cannot disconnect."); tty_error_msg(err, errmsg, ERRMSG_SIZE); IDLog("Error: %s\n", errmsg); return; } PowerSP.s = IPS_IDLE; IDSetSwitch(&PowerSP, "Filter Wheel is offline."); break; } } /* isFilterConnected: return 1 if we have a connection, 0 otherwise */ int isFilterConnected(void) { return ((PowerS[0].s == ISS_ON) ? 1 : 0); } libindi/obsolete/ieq45.cpp0000664000175000017500000006111713263645557014747 0ustar jasemjasem#if 0 IEQ45 Basic Driver Copyright (C) 2011 Nacho Mas (mas.ignacio@gmail.com). Only litle changes from lx200basic made it by 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 "ieq45.h" #include "config.h" #include "ieq45driver.h" #include "indicom.h" #include #include #include #include #include /* Our telescope auto pointer */ std::unique_ptr telescope(new IEQ45Basic()); const int POLLMS = 100; // Period of update, 1 second. const char *mydev = "IEQ45"; // Name of our device. const char *BASIC_GROUP = "Main Control"; // Main Group const char *OPTIONS_GROUP = "Options"; // Options Group /* Handy Macros */ #define currentRA EquatorialCoordsRN[0].value #define currentDEC EquatorialCoordsRN[1].value static void ISPoll(void *); static void retry_connection(void *); /************************************************************************************** ** Send client definitions of all properties. ***************************************************************************************/ void ISInit() { static int isInit = 0; if (isInit) return; if (telescope.get() == 0) telescope.reset(new IEQ45Basic()); isInit = 1; IEAddTimer(POLLMS, ISPoll, nullptr); } /************************************************************************************** ** ***************************************************************************************/ void ISGetProperties(const char *dev) { ISInit(); telescope->ISGetProperties(dev); } /************************************************************************************** ** ***************************************************************************************/ void ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) { ISInit(); telescope->ISNewSwitch(dev, name, states, names, n); } /************************************************************************************** ** ***************************************************************************************/ void ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n) { ISInit(); telescope->ISNewText(dev, name, texts, names, n); } /************************************************************************************** ** ***************************************************************************************/ void ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) { ISInit(); telescope->ISNewNumber(dev, name, values, names, n); } /************************************************************************************** ** ***************************************************************************************/ void ISPoll(void *p) { INDI_UNUSED(p); telescope->ISPoll(); IEAddTimer(POLLMS, ISPoll, nullptr); } /************************************************************************************** ** ***************************************************************************************/ void ISNewBLOB(const char *dev, const char *name, int sizes[], int blobsizes[], char *blobs[], char *formats[], char *names[], int n) { INDI_UNUSED(dev); INDI_UNUSED(name); INDI_UNUSED(sizes); INDI_UNUSED(blobsizes); INDI_UNUSED(blobs); INDI_UNUSED(formats); INDI_UNUSED(names); INDI_UNUSED(n); } /************************************************************************************** ** ***************************************************************************************/ void ISSnoopDevice(XMLEle *root) { INDI_UNUSED(root); } /************************************************************************************** ** IEQ45 Basic constructor ***************************************************************************************/ IEQ45Basic::IEQ45Basic() { init_properties(); lastSet = -1; fd = -1; simulation = false; lastRA = 0; lastDEC = 0; currentSet = 0; IDLog("Initializing from IEQ45 device...\n"); IDLog("Driver Version: 0.1 (2011-11-07)\n"); enable_simulation(false); } /************************************************************************************** ** ***************************************************************************************/ IEQ45Basic::~IEQ45Basic() { } /************************************************************************************** ** Initialize all properties & set default values. ***************************************************************************************/ void IEQ45Basic::init_properties() { // Connection#include #include IUFillSwitch(&ConnectS[0], "CONNECT", "Connect", ISS_OFF); IUFillSwitch(&ConnectS[1], "DISCONNECT", "Disconnect", ISS_ON); IUFillSwitchVector(&ConnectSP, ConnectS, NARRAY(ConnectS), mydev, "CONNECTION", "Connection", BASIC_GROUP, IP_RW, ISR_1OFMANY, 60, IPS_IDLE); // Coord Set IUFillSwitch(&OnCoordSetS[0], "SLEW", "Slew", ISS_ON); IUFillSwitch(&OnCoordSetS[1], "SYNC", "Sync", ISS_OFF); IUFillSwitchVector(&OnCoordSetSP, OnCoordSetS, NARRAY(OnCoordSetS), mydev, "ON_COORD_SET", "On Set", BASIC_GROUP, IP_RW, ISR_1OFMANY, 0, IPS_IDLE); //Track MODE IUFillSwitch(&TrackModeS[0], "SIDEREAL", "Sidereal", ISS_ON); IUFillSwitch(&TrackModeS[1], "LUNAR", "Lunar", ISS_OFF); IUFillSwitch(&TrackModeS[2], "SOLAR", "Solar", ISS_OFF); IUFillSwitch(&TrackModeS[3], "ZERO", "Stop", ISS_OFF); IUFillSwitchVector(&TrackModeSP, TrackModeS, NARRAY(TrackModeS), mydev, "TRACK_MODE", "Track Mode", BASIC_GROUP, IP_RW, ISR_1OFMANY, 0, IPS_IDLE); // Abort IUFillSwitch(&AbortSlewS[0], "ABORT", "Abort", ISS_OFF); IUFillSwitchVector(&AbortSlewSP, AbortSlewS, NARRAY(AbortSlewS), mydev, "ABORT_MOTION", "Abort Slew/Track", BASIC_GROUP, IP_RW, ISR_ATMOST1, 0, IPS_IDLE); // Port IUFillText(&PortT[0], "PORT", "Port", "/dev/ttyS0"); IUFillTextVector(&PortTP, PortT, NARRAY(PortT), mydev, "DEVICE_PORT", "Ports", BASIC_GROUP, IP_RW, 0, IPS_IDLE); // Equatorial Coords IUFillNumber(&EquatorialCoordsRN[0], "RA", "RA H:M:S", "%10.6m", 0., 24., 0., 0.); IUFillNumber(&EquatorialCoordsRN[1], "DEC", "Dec D:M:S", "%10.6m", -90., 90., 0., 0.); IUFillNumberVector(&EquatorialCoordsRNP, EquatorialCoordsRN, NARRAY(EquatorialCoordsRN), mydev, "EQUATORIAL_EOD_COORD", "Equatorial JNow", BASIC_GROUP, IP_RW, 0, IPS_IDLE); } /************************************************************************************** ** Define IEQ45 Basic properties to clients. ***************************************************************************************/ void IEQ45Basic::ISGetProperties(const char *dev) { if (dev != nullptr && strcmp(mydev, dev)) return; // Main Control IDDefSwitch(&ConnectSP, nullptr); IDDefText(&PortTP, nullptr); IDDefNumber(&EquatorialCoordsRNP, nullptr); IDDefSwitch(&OnCoordSetSP, nullptr); IDDefSwitch(&TrackModeSP, nullptr); IDDefSwitch(&AbortSlewSP, nullptr); } /************************************************************************************** ** Process Text properties ***************************************************************************************/ void IEQ45Basic::ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n) { // Ignore if not ours if (strcmp(dev, mydev)) return; // =================================== // Port Name // =================================== if (!strcmp(name, PortTP.name)) { if (IUUpdateText(&PortTP, texts, names, n) < 0) return; PortTP.s = IPS_OK; IDSetText(&PortTP, nullptr); return; } if (is_connected() == false) { IDMessage(mydev, "IEQ45 is offline. Please connect before issuing any commands."); reset_all_properties(); return; } } /************************************************************************************** ** ***************************************************************************************/ void IEQ45Basic::ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) { // Ignore if not ours if (strcmp(dev, mydev)) return; if (is_connected() == false) { IDMessage(mydev, "IEQ45 is offline. Please connect before issuing any commands."); reset_all_properties(); return; } // =================================== // Equatorial Coords // =================================== if (!strcmp(name, EquatorialCoordsRNP.name)) { int i = 0, nset = 0, error_code = 0; double newRA = 0, newDEC = 0; for (nset = i = 0; i < n; i++) { INumber *eqp = IUFindNumber(&EquatorialCoordsRNP, names[i]); if (eqp == &EquatorialCoordsRN[0]) { newRA = values[i]; nset += newRA >= 0 && newRA <= 24.0; } else if (eqp == &EquatorialCoordsRN[1]) { newDEC = values[i]; nset += newDEC >= -90.0 && newDEC <= 90.0; } } if (nset == 2) { char RAStr[32], DecStr[32]; fs_sexa(RAStr, newRA, 2, 3600); fs_sexa(DecStr, newDEC, 2, 3600); #ifdef INDI_DEBUG IDLog("We received JNow RA %g - DEC %g\n", newRA, newDEC); IDLog("We received JNow RA %s - DEC %s\n", RAStr, DecStr); #endif if (!simulation && ((error_code = setObjectRA(fd, newRA)) < 0 || (error_code = setObjectDEC(fd, newDEC)) < 0)) { handle_error(&EquatorialCoordsRNP, error_code, "Setting RA/DEC"); return; } targetRA = newRA; targetDEC = newDEC; if (process_coords() == false) { EquatorialCoordsRNP.s = IPS_ALERT; IDSetNumber(&EquatorialCoordsRNP, nullptr); } } // end nset else { EquatorialCoordsRNP.s = IPS_ALERT; IDSetNumber(&EquatorialCoordsRNP, "RA or Dec missing or invalid"); } return; } /* end EquatorialCoordsWNP */ } /************************************************************************************** ** ***************************************************************************************/ void IEQ45Basic::ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) { // ignore if not ours // if (strcmp(mydev, dev)) return; // =================================== // Connect Switch // =================================== if (!strcmp(name, ConnectSP.name)) { if (IUUpdateSwitch(&ConnectSP, states, names, n) < 0) return; connect_telescope(); return; } if (is_connected() == false) { IDMessage(mydev, "IEQ45 is offline. Please connect before issuing any commands."); reset_all_properties(); return; } // =================================== // Coordinate Set // =================================== if (!strcmp(name, OnCoordSetSP.name)) { if (IUUpdateSwitch(&OnCoordSetSP, states, names, n) < 0) return; currentSet = get_switch_index(&OnCoordSetSP); OnCoordSetSP.s = IPS_OK; IDSetSwitch(&OnCoordSetSP, nullptr); } // =================================== // Track Mode Set // =================================== if (!strcmp(name, TrackModeSP.name)) { if (IUUpdateSwitch(&TrackModeSP, states, names, n) < 0) return; int trackMode = get_switch_index(&TrackModeSP); selectTrackingMode(fd, trackMode); TrackModeSP.s = IPS_OK; IDSetSwitch(&TrackModeSP, nullptr); } // =================================== // Abort slew // =================================== if (!strcmp(name, AbortSlewSP.name)) { IUResetSwitch(&AbortSlewSP); abortSlew(fd); if (EquatorialCoordsRNP.s == IPS_BUSY) { AbortSlewSP.s = IPS_OK; EquatorialCoordsRNP.s = IPS_IDLE; IDSetSwitch(&AbortSlewSP, "Slew aborted."); IDSetNumber(&EquatorialCoordsRNP, nullptr); } return; } } /************************************************************************************** ** Retry connecting to the telescope on error. Give up if there is no hope. ***************************************************************************************/ void IEQ45Basic::handle_error(INumberVectorProperty *nvp, int err, const char *msg) { nvp->s = IPS_ALERT; /* First check to see if the telescope is connected */ if (check_IEQ45_connection(fd)) { /* The telescope is off locally */ ConnectS[0].s = ISS_OFF; ConnectS[1].s = ISS_ON; ConnectSP.s = IPS_BUSY; IDSetSwitch(&ConnectSP, "Telescope is not responding to commands, will retry in 10 seconds."); IDSetNumber(nvp, nullptr); IEAddTimer(10000, retry_connection, &fd); return; } /* If the error is a time out, then the device doesn't support this property */ if (err == -2) { nvp->s = IPS_ALERT; IDSetNumber(nvp, "Device timed out. Current device may be busy or does not support %s. Will retry again.", msg); } else /* Changing property failed, user should retry. */ IDSetNumber(nvp, "%s failed.", msg); fault = true; } /************************************************************************************** ** Set all properties to idle and reset most switches to clean state. ***************************************************************************************/ void IEQ45Basic::reset_all_properties() { ConnectSP.s = IPS_IDLE; OnCoordSetSP.s = IPS_IDLE; TrackModeSP.s = IPS_IDLE; AbortSlewSP.s = IPS_IDLE; PortTP.s = IPS_IDLE; EquatorialCoordsRNP.s = IPS_IDLE; IUResetSwitch(&OnCoordSetSP); IUResetSwitch(&TrackModeSP); IUResetSwitch(&AbortSlewSP); OnCoordSetS[0].s = ISS_ON; TrackModeS[0].s = ISS_ON; ConnectS[0].s = ISS_OFF; ConnectS[1].s = ISS_ON; IDSetSwitch(&ConnectSP, nullptr); IDSetSwitch(&OnCoordSetSP, nullptr); IDSetSwitch(&TrackModeSP, nullptr); IDSetSwitch(&AbortSlewSP, nullptr); IDSetText(&PortTP, nullptr); IDSetNumber(&EquatorialCoordsRNP, nullptr); } /************************************************************************************** ** ***************************************************************************************/ void IEQ45Basic::correct_fault() { fault = false; IDMessage(mydev, "Telescope is online."); } /************************************************************************************** ** ***************************************************************************************/ bool IEQ45Basic::is_connected() { if (simulation) return true; return (ConnectSP.sp[0].s == ISS_ON); } /************************************************************************************** ** ***************************************************************************************/ static void retry_connection(void *p) { int fd = *((int *)p); if (check_IEQ45_connection(fd)) telescope->connection_lost(); else telescope->connection_resumed(); } /************************************************************************************** ** ***************************************************************************************/ void IEQ45Basic::ISPoll() { if (is_connected() == false || simulation) return; double dx, dy; int error_code = 0; switch (EquatorialCoordsRNP.s) { case IPS_IDLE: getIEQ45RA(fd, ¤tRA); getIEQ45DEC(fd, ¤tDEC); if (fabs(currentRA - lastRA) > 0.01 || fabs(currentDEC - lastDEC) > 0.01) { lastRA = currentRA; lastDEC = currentDEC; IDSetNumber(&EquatorialCoordsRNP, nullptr); IDLog("IDLE update coord\n"); } break; case IPS_BUSY: getIEQ45RA(fd, ¤tRA); getIEQ45DEC(fd, ¤tDEC); dx = targetRA - currentRA; dy = targetDEC - currentDEC; // Wait until acknowledged or within threshold if (fabs(dx) <= (3 / (900.0)) && fabs(dy) <= (3 / 60.0)) { lastRA = currentRA; lastDEC = currentDEC; IUResetSwitch(&OnCoordSetSP); OnCoordSetSP.s = IPS_OK; EquatorialCoordsRNP.s = IPS_OK; IDSetNumber(&EquatorialCoordsRNP, nullptr); switch (currentSet) { case IEQ45_SLEW: OnCoordSetSP.sp[IEQ45_SLEW].s = ISS_ON; IDSetSwitch(&OnCoordSetSP, "Slew is complete."); break; case IEQ45_SYNC: break; } } else IDSetNumber(&EquatorialCoordsRNP, nullptr); break; case IPS_OK: if ((error_code = getIEQ45RA(fd, ¤tRA)) < 0 || (error_code = getIEQ45DEC(fd, ¤tDEC)) < 0) { handle_error(&EquatorialCoordsRNP, error_code, "Getting RA/DEC"); return; } if (fault == true) correct_fault(); if ((currentRA != lastRA) || (currentDEC != lastDEC)) { lastRA = currentRA; lastDEC = currentDEC; //IDLog("IPS_OK update coords %g %g\n",currentRA,currentDEC); IDSetNumber(&EquatorialCoordsRNP, nullptr); } break; case IPS_ALERT: break; } } /************************************************************************************** ** ***************************************************************************************/ bool IEQ45Basic::process_coords() { int error_code; char syncString[256]; char RAStr[32], DecStr[32]; double dx, dy; switch (currentSet) { // Slew case IEQ45_SLEW: lastSet = IEQ45_SLEW; if (EquatorialCoordsRNP.s == IPS_BUSY) { IDLog("Aborting Slew\n"); abortSlew(fd); // sleep for 100 mseconds usleep(100000); } if (!simulation && (error_code = Slew(fd))) { slew_error(error_code); return false; } EquatorialCoordsRNP.s = IPS_BUSY; fs_sexa(RAStr, targetRA, 2, 3600); fs_sexa(DecStr, targetDEC, 2, 3600); IDSetNumber(&EquatorialCoordsRNP, "Slewing to JNow RA %s - DEC %s", RAStr, DecStr); IDLog("Slewing to JNow RA %s - DEC %s\n", RAStr, DecStr); break; // Sync case IEQ45_SYNC: lastSet = IEQ45_SYNC; EquatorialCoordsRNP.s = IPS_IDLE; if (!simulation && (error_code = Sync(fd, syncString) < 0)) { IDSetNumber(&EquatorialCoordsRNP, "Synchronization failed."); return false; } if (simulation) { EquatorialCoordsRN[0].value = EquatorialCoordsRN[0].value; EquatorialCoordsRN[1].value = EquatorialCoordsRN[1].value; } EquatorialCoordsRNP.s = IPS_OK; IDLog("Synchronization successful %s\n", syncString); IDSetNumber(&EquatorialCoordsRNP, "Synchronization successful."); break; } return true; } /************************************************************************************** ** ***************************************************************************************/ int IEQ45Basic::get_switch_index(ISwitchVectorProperty *sp) { for (int i = 0; i < sp->nsp; i++) if (sp->sp[i].s == ISS_ON) return i; return -1; } /************************************************************************************** ** ***************************************************************************************/ void IEQ45Basic::connect_telescope() { switch (ConnectSP.sp[0].s) { case ISS_ON: if (simulation) { ConnectSP.s = IPS_OK; IDSetSwitch(&ConnectSP, "Simulated telescope is online."); return; } if (tty_connect(PortT[0].text, 9600, 8, 0, 1, &fd) != TTY_OK) { ConnectS[0].s = ISS_OFF; ConnectS[1].s = ISS_ON; IDSetSwitch( &ConnectSP, "Error connecting to port %s. Make sure you have BOTH read and write permission to the port.", PortT[0].text); return; } if (check_IEQ45_connection(fd)) { ConnectS[0].s = ISS_OFF; ConnectS[1].s = ISS_ON; IDSetSwitch(&ConnectSP, "Error connecting to Telescope. Telescope is offline."); return; } ConnectSP.s = IPS_OK; IDSetSwitch(&ConnectSP, "Telescope is online. Retrieving basic data..."); get_initial_data(); break; case ISS_OFF: ConnectS[0].s = ISS_OFF; ConnectS[1].s = ISS_ON; ConnectSP.s = IPS_IDLE; if (simulation) { IDSetSwitch(&ConnectSP, "Simulated Telescope is offline."); return; } IDSetSwitch(&ConnectSP, "Telescope is offline."); IDLog("Telescope is offline."); tty_disconnect(fd); break; } } /************************************************************************************** ** ***************************************************************************************/ void IEQ45Basic::get_initial_data() { // Make sure short checkIEQ45Format(fd); // Get current RA/DEC getIEQ45RA(fd, ¤tRA); getIEQ45DEC(fd, ¤tDEC); IDSetNumber(&EquatorialCoordsRNP, nullptr); } /************************************************************************************** ** ***************************************************************************************/ void IEQ45Basic::slew_error(int slewCode) { OnCoordSetSP.s = IPS_IDLE; IDLog("Aborting Slew\n"); abortSlew(fd); if (slewCode == 1) IDSetSwitch(&OnCoordSetSP, "Object below horizon."); else if (slewCode == 2) IDSetSwitch(&OnCoordSetSP, "Object below the minimum elevation limit."); else IDSetSwitch(&OnCoordSetSP, "Slew failed."); } /************************************************************************************** ** ***************************************************************************************/ void IEQ45Basic::enable_simulation(bool enable) { simulation = enable; if (simulation) IDLog("Warning: Simulation is activated.\n"); else IDLog("Simulation is disabled.\n"); } /************************************************************************************** ** ***************************************************************************************/ void IEQ45Basic::connection_lost() { ConnectSP.s = IPS_IDLE; IDSetSwitch(&ConnectSP, "The connection to the telescope is lost."); return; } /************************************************************************************** ** ***************************************************************************************/ void IEQ45Basic::connection_resumed() { ConnectS[0].s = ISS_ON; ConnectS[1].s = ISS_OFF; ConnectSP.s = IPS_OK; IDSetSwitch(&ConnectSP, "The connection to the telescope has been resumed."); } libindi/obsolete/ieq45.h0000664000175000017500000000644413263645557014416 0ustar jasemjasem/* IEQ45 Basic Driver Copyright (C) 2011 Nacho Mas (mas.ignacio@gmail.com). Only litle changes from lx200basic made it by 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 */ #pragma once #include "indicom.h" #include "indidevapi.h" class IEQ45Basic { public: IEQ45Basic(); ~IEQ45Basic(); void ISGetProperties(const char *dev); void ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n); void ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n); void ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n); void ISPoll(); void connection_lost(); void connection_resumed(); private: enum IEQ45_STATUS { IEQ45_SLEW, IEQ45_SYNC, IEQ45_PARK }; /* Switches */ ISwitch ConnectS[2]; ISwitch OnCoordSetS[2]; ISwitch TrackModeS[4]; ISwitch AbortSlewS[1]; /* Texts */ IText PortT[1]; /* Numbers */ INumber EquatorialCoordsRN[2]; /* Switch Vectors */ ISwitchVectorProperty ConnectSP; ISwitchVectorProperty OnCoordSetSP; ISwitchVectorProperty TrackModeSP; ISwitchVectorProperty AbortSlewSP; /* Number Vectors */ INumberVectorProperty EquatorialCoordsRNP; /* Text Vectors */ ITextVectorProperty PortTP; /*******************************************************/ /* Connection Routines ********************************************************/ void init_properties(); void get_initial_data(); void connect_telescope(); bool is_connected(); /*******************************************************/ /* Misc routines ********************************************************/ bool process_coords(); int get_switch_index(ISwitchVectorProperty *sp); /*******************************************************/ /* Simulation Routines ********************************************************/ void enable_simulation(bool enable); /*******************************************************/ /* Error handling routines ********************************************************/ void slew_error(int slewCode); void reset_all_properties(); void handle_error(INumberVectorProperty *nvp, int err, const char *msg); void correct_fault(); protected: double JD; /* Julian Date */ double lastRA; double lastDEC; bool simulation; bool fault; int fd; /* Telescope tty file descriptor */ int currentSet; int lastSet; double targetRA, targetDEC; }; libindi/obsolete/focusmaster.cpp0000664000175000017500000002214213263645557016346 0ustar jasemjasem/******************************************************************************* Copyright(c) 2017 Jasem Mutlaq. All rights reserved. Televue FocusMaster Driver. This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2 as published by the Free Software Foundation. . 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 Library General Public License for more details. . You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. *******************************************************************************/ #include "focusmaster.h" #include #include #include #include #define POLLMS 1000 /* 1000 ms */ #define FOCUSMASTER_TIMEOUT 1000 /* 1000 ms */ #define MAX_FM_BUF 16 #define FOCUS_SETTINGS_TAB "Settings" // We declare an auto pointer to FocusMaster. std::unique_ptr focusMaster(new FocusMaster()); void ISPoll(void *p); void ISGetProperties(const char *dev) { focusMaster->ISGetProperties(dev); } void ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) { focusMaster->ISNewSwitch(dev, name, states, names, n); } void ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n) { focusMaster->ISNewText(dev, name, texts, names, n); } void ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) { focusMaster->ISNewNumber(dev, name, values, names, n); } void ISNewBLOB(const char *dev, const char *name, int sizes[], int blobsizes[], char *blobs[], char *formats[], char *names[], int n) { INDI_UNUSED(dev); INDI_UNUSED(name); INDI_UNUSED(sizes); INDI_UNUSED(blobsizes); INDI_UNUSED(blobs); INDI_UNUSED(formats); INDI_UNUSED(names); INDI_UNUSED(n); } void ISSnoopDevice(XMLEle *root) { focusMaster->ISSnoopDevice(root); } FocusMaster::FocusMaster() { FI::SetCapability(FOCUSER_CAN_ABORT); setConnection(CONNECTION_NONE); } bool FocusMaster::Connect() { handle = hid_open(0x134A, 0x9030, nullptr); if (handle == nullptr) { LOG_ERROR("No FocusMaster focuser found."); return false; } else { // N.B. Check here if we have the digital readout gadget. // if digital readout //FI::SetCapability(GetFocuserCapability() | FOCUSER_CAN_REL_MOVE | FOCUSER_CAN_ABS_MOVE); SetTimer(POLLMS); } return (handle != nullptr); } bool FocusMaster::Disconnect() { hid_close(handle); hid_exit(); return true; } const char *FocusMaster::getDefaultName() { return (const char *)"FocusMaster"; } bool FocusMaster::initProperties() { INDI::Focuser::initProperties(); // Sync to a particular position IUFillNumber(&SyncN[0], "Ticks", "", "%.f", 0, 100000, 100., 0.); IUFillNumberVector(&SyncNP, SyncN, 1, getDeviceName(), "Sync", "", MAIN_CONTROL_TAB, IP_RW, 0, IPS_IDLE); // Full Forward/Reverse Motion IUFillSwitch(&FullMotionS[FOCUS_INWARD], "FULL_INWARD", "Full Inward", ISS_OFF); IUFillSwitch(&FullMotionS[FOCUS_OUTWARD], "FULL_OUTWARD", "Full Outward", ISS_OFF); IUFillSwitchVector(&FullMotionSP, FullMotionS, 2, getDeviceName(), "FULL_MOTION", "Full Motion", MAIN_CONTROL_TAB, IP_RW, ISR_ATMOST1, 0, IPS_IDLE); FocusAbsPosN[0].min = SyncN[0].min = 0; FocusAbsPosN[0].max = SyncN[0].max; FocusAbsPosN[0].step = SyncN[0].step; FocusAbsPosN[0].value = 0; FocusRelPosN[0].max = (FocusAbsPosN[0].max - FocusAbsPosN[0].min) / 2; FocusRelPosN[0].step = FocusRelPosN[0].max / 100.0; FocusRelPosN[0].value = 100; addSimulationControl(); return true; } bool FocusMaster::updateProperties() { INDI::Focuser::updateProperties(); if (isConnected()) { defineSwitch(&FullMotionSP); //defineNumber(&SyncNP); } else { deleteProperty(FullMotionSP.name); //deleteProperty(SyncNP.name); } return true; } void FocusMaster::TimerHit() { if (!isConnected()) return; //uint32_t currentTicks = 0; if (FocusTimerNP.s == IPS_BUSY) { float remaining = CalcTimeLeft(focusMoveStart, focusMoveRequest); if (remaining <= 0) { FocusTimerNP.s = IPS_OK; FocusTimerN[0].value = 0; AbortFocuser(); } else FocusTimerN[0].value = remaining * 1000.0; IDSetNumber(&FocusTimerNP, nullptr); } #if 0 bool rc = getPosition(¤tTicks); if (rc) FocusAbsPosN[0].value = currentTicks; if (FocusAbsPosNP.s == IPS_BUSY || FocusRelPosNP.s == IPS_BUSY) { if (targetPosition == FocusAbsPosN[0].value) { if (FocusRelPosNP.s == IPS_BUSY) { FocusRelPosNP.s = IPS_OK; IDSetNumber(&FocusRelPosNP, nullptr); } FocusAbsPosNP.s = IPS_OK; LOG_DEBUG("Focuser reached target position."); } } IDSetNumber(&FocusAbsPosNP, nullptr); #endif SetTimer(POLLMS); } bool FocusMaster::ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) { if (dev != nullptr && strcmp(dev, getDeviceName()) == 0) { // Full Motion if (!strcmp(FullMotionSP.name, name)) { IUUpdateSwitch(&FullMotionSP, states, names, n); FocusDirection targetDirection = static_cast(IUFindOnSwitchIndex(&FullMotionSP)); FullMotionSP.s = MoveFocuser(targetDirection, 0, 0); return true; } } return INDI::Focuser::ISNewSwitch(dev, name, states, names, n); } bool FocusMaster::ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) { if (dev != nullptr && strcmp(dev, getDeviceName()) == 0) { // Sync if (strcmp(SyncNP.name, name) == 0) { IUUpdateNumber(&SyncNP, values, names, n); if (!sync(SyncN[0].value)) SyncNP.s = IPS_ALERT; else SyncNP.s = IPS_OK; IDSetNumber(&SyncNP, nullptr); return true; } } return INDI::Focuser::ISNewNumber(dev, name, values, names, n); } IPState FocusMaster::MoveFocuser(FocusDirection dir, int speed, uint16_t duration) { INDI_UNUSED(speed); uint8_t command[MAX_FM_BUF] = {0}; if (dir == FOCUS_INWARD) { command[0] = 0x31; command[1] = 0x21; } else { command[0] = 0x32; command[1] = 0x22; } sendCommand(command); gettimeofday(&focusMoveStart, nullptr); focusMoveRequest = duration / 1000.0; if (duration > 0 && duration <= POLLMS) { usleep(duration * 1000); AbortFocuser(); return IPS_OK; } return IPS_BUSY; } IPState FocusMaster::MoveAbsFocuser(uint32_t targetTicks) { INDI_UNUSED(targetTicks); FocusAbsPosNP.s = IPS_BUSY; return IPS_BUSY; } IPState FocusMaster::MoveRelFocuser(FocusDirection dir, uint32_t ticks) { uint32_t finalTicks = FocusAbsPosN[0].value + (ticks * (dir == FOCUS_INWARD ? -1 : 1)); return MoveAbsFocuser(finalTicks); } bool FocusMaster::sendCommand(const uint8_t *command, char *response) { INDI_UNUSED(response); int rc = hid_write(handle, command, 2); LOGF_DEBUG("CMD <%#02X %#02X>", command[0], command[1]); if (rc < 0) { LOGF_ERROR("<%#02X %#02X>: Error writing to device %s", command[0], command[1], hid_error(handle)); return false; } return true; } bool FocusMaster::setPosition(uint32_t ticks) { INDI_UNUSED(ticks); return false; } bool FocusMaster::getPosition(uint32_t *ticks) { INDI_UNUSED(ticks); return false; } bool FocusMaster::AbortFocuser() { uint8_t command[MAX_FM_BUF] = {0}; command[0] = 0x30; command[1] = 0x30; LOG_DEBUG("Aborting Focuser..."); bool rc = sendCommand(command); if (rc) { if (FullMotionSP.s == IPS_BUSY) { IUResetSwitch(&FullMotionSP); FullMotionSP.s = IPS_IDLE; IDSetSwitch(&FullMotionSP, nullptr); } if (FocusMotionSP.s == IPS_BUSY) { IUResetSwitch(&FocusMotionSP); FocusMotionSP.s = IPS_IDLE; IDSetSwitch(&FocusMotionSP, nullptr); } } return rc; } bool FocusMaster::sync(uint32_t ticks) { INDI_UNUSED(ticks); return false; } float FocusMaster::CalcTimeLeft(timeval start, float req) { double timesince; double timeleft; struct timeval now { 0, 0 }; gettimeofday(&now, nullptr); timesince = (double)(now.tv_sec * 1000.0 + now.tv_usec / 1000) - (double)(start.tv_sec * 1000.0 + start.tv_usec / 1000); timesince = timesince / 1000; timeleft = req - timesince; return timeleft; } libindi/obsolete/stvdriver.c0000664000175000017500000013617713263645557015521 0ustar jasemjasem#if 0 STV Low Level Driver Copyright (C) 2006 Markus Wildi, markus.wildi@datacomm.ch The initial work is based on the program STVremote by Shashikiran Ganesh. email: gshashikiran_AT_linuxmail_dot_org 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 #define _GNU_SOURCE 1 #include #include #include #include #include #include #include #include #include "config.h" #include "stvdriver.h" #include #ifndef _WIN32 #include #endif /* INDI Common Routines/RS232 */ #include "indicom.h" extern int fd; extern char tracking_buf[]; struct termios orig_tty_setting; /* old serial port setting to restore on close */ struct termios tty_setting; /* new serial port setting */ #define FALSE 0 #define TRUE 1 /*int tty_read( int fd, char *buf, int nbytes, int timeout, int *nbytes_read) ; */ /*int tty_write( int fd, const char *buffer, int *nbytes_written) ; */ void ISUpdateDisplay(int buffer, int line); int STV_portWrite(char *buf, int nbytes); int STV_TXDisplay(void); int STV_TerminateTXDisplay(void); int STV_FileStatus(int); int STV_DownloadComplete(void); int STV_RequestImage(int compression, int buffer, int x_offset, int y_offset, int *length, int *lines, int image[][320], IMAGE_INFO *image_info); int STV_RequestImageData(int compression, int *data, int j, int length, int *values); int STV_Download(void); int STV_DownloadAll(void); int STV_RequestAck(void); int STV_CheckHeaderSum(unsigned char *buf); int STV_CheckDataSum(unsigned char *data); int STV_PrintBuffer(unsigned char *buf, int n); int STV_PrintBufferAsText(unsigned char *buf, int n); int STV_CheckAck(unsigned char *buf); int STV_SendPacket(int cmd, int *data, int n); int STV_ReceivePacket(unsigned char *buf, int mode); int STV_DecompressData(unsigned char *data, int *values, int length, int expected_n_values); int STV_BufferStatus(int buffer); void STV_PrintBits(unsigned int x, int n); unsigned int STV_RecombineInt(unsigned char low_byte, unsigned char high_byte); unsigned int STV_GetBits(unsigned int x, int p, int n); int STV_MenueSetup(int delay); int STV_MenueDateTime(int delay); int STV_MenueCCDTemperature(int delay); typedef struct { double ccd_temperature; } DISPLAY_INFO; DISPLAY_INFO di; /* STV Buttons */ int STV_LRRotaryDecrease(void) { int data[] = { LR_ROTARY_DECREASE_PATTERN }; return STV_SendPacket(SEND_KEY_PATTERN, data, 1); } int STV_LRRotaryIncrease(void) { int data[] = { LR_ROTARY_INCREASE_PATTERN }; return STV_SendPacket(SEND_KEY_PATTERN, data, 1); } int STV_UDRotaryDecrease(void) { int data[] = { UD_ROTARY_DECREASE_PATTERN }; return STV_SendPacket(SEND_KEY_PATTERN, data, 1); } int STV_UDRotaryIncrease(void) { int data[] = { UD_ROTARY_INCREASE_PATTERN }; return STV_SendPacket(SEND_KEY_PATTERN, data, 1); } int STV_AKey(void) { /* Parameter button */ int data[] = { A_KEY_PATTERN }; return STV_SendPacket(SEND_KEY_PATTERN, data, 1); } int STV_BKey(void) { /* Value Button */ int data[] = { B_KEY_PATTERN }; return STV_SendPacket(SEND_KEY_PATTERN, data, 1); } int STV_Setup(void) { int data[] = { SETUP_KEY_PATTERN }; return STV_SendPacket(SEND_KEY_PATTERN, data, 1); } int STV_Interrupt(void) { int data[] = { INT_KEY_PATTERN }; return STV_SendPacket(SEND_KEY_PATTERN, data, 1); } int STV_Focus(void) { int data[] = { FOCUS_KEY_PATTERN }; return STV_SendPacket(SEND_KEY_PATTERN, data, 1); } int STV_Image(void) { int data[] = { IMAGE_KEY_PATTERN }; return STV_SendPacket(SEND_KEY_PATTERN, data, 1); } int STV_Monitor(void) { int data[] = { MONITOR_KEY_PATTERN }; return STV_SendPacket(SEND_KEY_PATTERN, data, 1); } int STV_Calibrate(void) { int data[] = { CAL_KEY_PATTERN }; return STV_SendPacket(SEND_KEY_PATTERN, data, 1); } int STV_Track(void) { int data[] = { TRACK_KEY_PATTERN }; return STV_SendPacket(SEND_KEY_PATTERN, data, 1); } int STV_Display(void) { int data[] = { DISPLAY_KEY_PATTERN }; return STV_SendPacket(SEND_KEY_PATTERN, data, 1); } int STV_FileOps(void) { int data[] = { FILEOPS_KEY_PATTERN }; return STV_SendPacket(SEND_KEY_PATTERN, data, 1); } int STV_RequestImageInfo(int buffer, IMAGE_INFO *image_info) { int i; int length; int res; int data[] = { buffer }; unsigned char buf[1024]; unsigned int value[21]; if ((res = STV_BufferStatus(buffer)) != 0) { if (res == 1) { /*fprintf( stderr," STV_RequestImageInfo buffer empty %d\n", buffer) ; */ return res; /* Buffer is empty */ } else { fprintf(stderr, " STV_RequestImageInfo error %d\n", res); return res; } } res = STV_SendPacket(REQUEST_IMAGE_INFO, data, 1); usleep(50000); res = STV_ReceivePacket(buf, 0); if (buf[1] != REQUEST_IMAGE_INFO) { /*length= buf[3] * 0x100 + buf[2] ; */ /*res= STV_PrintBuffer(buf, 6+ length+ 2) ; */ AGAINI: if ((res = STV_ReceivePacket(buf, 0)) < 0) { /* STV answers with a packet 0x03 sometimes, should be 0x04 */ return -1; ; } if (buf[1] != REQUEST_IMAGE_INFO) { /* Second try */ fprintf(stderr, "STV_RequestImageInfo: expected REQUEST_IMAGE_INFO, received %d, try again\n", buf[1]); goto AGAINI; } } length = buf[3] * 0x100 + buf[2]; /* DECODE it */ for (i = 6; i < length; i += 2) { value[(i - 6) / 2] = STV_RecombineInt(buf[i], buf[i + 1]); } image_info->descriptor = value[0]; /* Decode the descriptor */ /*STV_PrintBits(( unsigned int)value[0], 16) ; */ if ((image_info->descriptor & ID_BITS_MASK) == ID_BITS_10) { /* fprintf( stderr, "ADC Resolution: 10 bits\n") ; */ } else if ((image_info->descriptor & ID_BITS_MASK) == ID_BITS_8) { /*fprintf( stderr, "Resolution: 8 bits (focus mode only)\n") ; */ } if ((image_info->descriptor & ID_UNITS_MASK) == ID_UNITS_INCHES) { /*fprintf( stderr, "Units: inch\n") ; */ } else if ((image_info->descriptor & ID_UNITS_MASK) == ID_UNITS_CM) { /*fprintf( stderr, "Unis: cm\n") ; */ } if ((image_info->descriptor & ID_SCOPE_MASK) == ID_SCOPE_REFRACTOR) { /*fprintf( stderr, "Refractor\n") ; */ } else if ((image_info->descriptor & ID_SCOPE_MASK) == ID_SCOPE_REFLECTOR) { /*fprintf( stderr, "Reflector\n") ; */ } if ((image_info->descriptor & ID_DATETIME_MASK) == ID_DATETIME_VALID) { /*fprintf( stderr, "Date and time valid\n") ; */ } else if ((image_info->descriptor & ID_DATETIME_MASK) == ID_DATETIME_INVALID) { /*fprintf( stderr, "Date and time invalid\n") ; */ } if ((image_info->descriptor & ID_BIN_MASK) == ID_BIN_1X1) { image_info->binning = 1; } else if ((image_info->descriptor & ID_BIN_MASK) == ID_BIN_2X2) { image_info->binning = 2; } else if ((image_info->descriptor & ID_BIN_MASK) == ID_BIN_3X3) { image_info->binning = 3; } if ((image_info->descriptor & ID_PM_MASK) == ID_PM_PM) { /*fprintf( stderr, "Time: PM\n") ; */ } else if ((image_info->descriptor & ID_PM_MASK) == ID_PM_AM) { /*fprintf( stderr, "Time: AM\n") ; */ } /* ToDo: Include that to the fits header */ if ((image_info->descriptor & ID_FILTER_MASK) == ID_FILTER_LUNAR) { /*fprintf( stderr, "Filter: Lunar\n") ; */ } else if ((image_info->descriptor & ID_FILTER_MASK) == ID_FILTER_NP) { /*fprintf( stderr, "Filter: clear\n") ; */ } /* ToDo: Include that to the fits header */ if ((image_info->descriptor & ID_DARKSUB_MASK) == ID_DARKSUB_YES) { /*fprintf( stderr, "Drak sub yes\n") ; */ } else if ((image_info->descriptor & ID_DARKSUB_MASK) == ID_DARKSUB_NO) { /*fprintf( stderr, "Dark sub no\n") ; */ } if ((image_info->descriptor & ID_MOSAIC_MASK) == ID_MOSAIC_NONE) { /*fprintf( stderr, "Is NOT a mosaic\n") ; */ } else if ((image_info->descriptor & ID_MOSAIC_MASK) == ID_MOSAIC_SMALL) { /*fprintf( stderr, "Is a small mosaic\n") ; */ } else if ((image_info->descriptor & ID_MOSAIC_MASK) == ID_MOSAIC_LARGE) { /*fprintf( stderr, "Is a large mosaic\n") ; */ } image_info->height = value[1]; image_info->width = value[2]; image_info->top = value[3]; image_info->left = value[4]; /* Exposure time */ if ((value[5] >= 100) && (value[5] <= 60000)) { image_info->exposure = ((float)value[5]) * 0.01; } else if ((value[5] >= 60001) && (value[5] <= 60999)) { image_info->exposure = ((float)value[5] - 60000.) * .001; } else { fprintf(stderr, "Error Exposure time %d\n", value[5]); image_info->exposure = -1.; } image_info->noExposure = value[6]; image_info->analogGain = value[7]; image_info->digitalGain = value[8]; image_info->focalLength = value[9]; image_info->aperture = value[10]; image_info->packedDate = value[11]; image_info->year = STV_GetBits(value[11], 6, 7) + 1999; image_info->day = STV_GetBits(value[11], 11, 5); image_info->month = STV_GetBits(value[11], 15, 4); image_info->packedTime = value[12]; image_info->seconds = STV_GetBits(value[12], 5, 6); image_info->minutes = STV_GetBits(value[12], 11, 6); image_info->hours = STV_GetBits(value[12], 15, 4); if ((image_info->descriptor & ID_PM_PM) > 0) { image_info->hours += 12; } if ((value[13] & 0x8000) == 0x8000) { image_info->ccdTemp = (float)(0xffff - value[13]) / 100.; } else { image_info->ccdTemp = (float)value[13] / 100.; } image_info->siteID = value[14]; image_info->eGain = value[15]; image_info->background = value[16]; image_info->range = value[17]; image_info->pedestal = value[18]; image_info->ccdTop = value[19]; image_info->ccdLeft = value[20]; /* fprintf( stderr, "descriptor:%d 0x%2x 0x%2x\n", image_info->descriptor, buf[6], buf[6+1]) ; */ /* fprintf( stderr, "height:%d\n", image_info->height) ; */ /* fprintf( stderr, "width:%d\n", image_info->width) ; */ /* fprintf( stderr, "top:%d\n", image_info->top) ; */ /* fprintf( stderr, "left:%d\n", image_info->left) ; */ /* fprintf( stderr, "exposure:%f\n", image_info->exposure) ; */ /* fprintf( stderr, "noExposure:%d\n", image_info->noExposure) ; */ /* fprintf( stderr, "analogGain:%d\n", image_info->analogGain) ; */ /* fprintf( stderr, "digitalGain:%d\n", image_info->digitalGain) ; */ /* fprintf( stderr, "focalLength:%d\n", image_info->focalLength) ; */ /* fprintf( stderr, "aperture:%d\n", image_info->aperture) ; */ /* fprintf( stderr, "packedDate:%d\n", image_info->packedDate) ; */ /* fprintf( stderr, "Year:%d\n", image_info->year) ; */ /* fprintf( stderr, "Day:%d\n", image_info->day) ; */ /* fprintf( stderr, "Month:%d\n", image_info->month) ; */ /* fprintf( stderr, "packedTime:%d\n", image_info->packedTime) ; */ /* fprintf( stderr, "Seconds:%d\n", image_info->seconds) ; */ /* fprintf( stderr, "minutes:%d\n", image_info->minutes) ; */ /* fprintf( stderr, "hours:%d\n", image_info->hours) ; */ /* fprintf( stderr, "ccdTemp:%f\n", image_info->ccdTemp) ; */ /* fprintf( stderr, "siteID:%d\n", image_info->siteID) ; */ /* fprintf( stderr, "eGain:%d\n", image_info->eGain) ; */ /* fprintf( stderr, "background:%d\n", image_info->background) ; */ /* fprintf( stderr, "range :%d\n", image_info->range ) ; */ /* fprintf( stderr, "pedestal:%d\n", image_info->pedestal) ; */ /* fprintf( stderr, "ccdTop :%d\n", image_info->ccdTop) ; */ /* fprintf( stderr, "ccdLeft:%d\n", image_info->ccdLeft) ; */ return 0; } int STV_RequestImage(int compression, int buffer, int x_offset, int y_offset, int *length, int *lines, int image[][320], IMAGE_INFO *image_info) { int i, j; int res; int n_values; unsigned char buf[0xffff]; int values[1024]; int data[] = { 0, y_offset, *length, buffer }; /* Offset row, Offset line, length, buffer number */ int XOFF; /* fprintf(stderr, "STV_RequestImage: --------------------------------buffer= %d, %d\n", buffer, lines) ; */ if ((res = STV_RequestImageInfo(buffer, image_info)) != 0) { /* Trigger for download */ /* buffer empty */ return res; } res = STV_BKey(); /* "press" the STV Value button */ usleep(50000); res = STV_ReceivePacket(buf, 0); /* Check the boundaries obtained from the image data versus windowing */ /* Take the smaller boundaries in each direction */ if (x_offset > image_info->height) { x_offset = image_info->height; } XOFF = image_info->top + x_offset; if (y_offset > image_info->width) { y_offset = image_info->width; } data[1] = image_info->left + y_offset; if (*length > image_info->width - y_offset) { *length = image_info->width - y_offset; } data[2] = *length; if (*lines > image_info->height - x_offset) { *lines = image_info->height - x_offset; } /*fprintf(stderr, "STV_RequestImage: DATA 0=%d, 1=%d, 2=%d, 3=%d, length=%d, lines=%d\n", data[0], data[1], data[2], data[3], *length, *lines) ; */ for (j = 0; j < *lines; j++) { data[0] = j + XOFF; /*line */ if ((n_values = STV_RequestImageData(compression, data, j, *length, values)) < 0) { fprintf(stderr, "STV_RequestImage: Calling STV_RequestImageData failed %d\n", n_values); return n_values; } else { for (i = 0; i < n_values; i++) { image[j][i] = values[i]; } ISUpdateDisplay(buffer, j); } } /* Read the 201th line, see mosaic mode */ /* ToDo: analyze/display the data or use them in the fits header(s) */ data[0] = 200; /*line */ if ((res = STV_RequestImageData(1, data, 200, *length, values)) < 0) { return res; } else { for (i = 0; i < n_values; i++) { /* fprintf( stderr, "%d: %d ", i, values[i]) ; */ } /* fprintf( stderr, "\n") ; */ } res = STV_DownloadComplete(); ISUpdateDisplay(buffer, -j); return 0; } int STV_RequestImageData(int compression, int *data, int j, int length, int *values) { int i; int res; int data_length; int n_values; unsigned char buf[0xffff]; data_length = -1; if (compression == ON) { /* compressed download */ if ((res = STV_SendPacket(REQUEST_COMPRESSED_IMAGE_DATA, data, 4)) != 0) { fprintf(stderr, "STV_RequestImageData: could not write\n"); return -1; } AGAINC: if ((res = STV_ReceivePacket(buf, 0)) > 0) { if (buf[1] != REQUEST_COMPRESSED_IMAGE_DATA) { if (buf[1] != DISPLAY_ECHO) { if (buf[1] != ACK) { fprintf(stderr, "STV_RequestImageData: expected REQUEST_COMPRESSED_IMAGE_DATA, received %2x\n", buf[1]); } } goto AGAINC; } data_length = (int)buf[3] * 0x100 + (int)buf[2]; if ((n_values = STV_DecompressData((buf + 6), values, data_length, length)) < 0) { n_values = -n_values; fprintf(stderr, "SEVERE ERROR on Line %d, pixel position=%d\n", j, n_values); _exit(-1); } if (n_values == length) { return n_values; } else { fprintf(stderr, "SEVERE Error: Length not equal, Line: %d, %d != %d, data %2x %2x, values %d, %d\n", j, n_values, length, buf[6 + length - 2], buf[6 + length - 1], values[n_values - 2], values[n_values - 1]); _exit(1); } } else { fprintf(stderr, "Error: waiting for data on the serial port at line %d\n", j); return -1; } } else { /* uncompressed download */ if ((res = STV_SendPacket(REQUEST_IMAGE_DATA, data, 4)) == 0) { AGAINU: if ((res = STV_ReceivePacket(buf, 0)) > 0) { if (buf[1] != REQUEST_IMAGE_DATA) { if (buf[1] != DISPLAY_ECHO) { if (buf[1] != ACK) { fprintf(stderr, "STV_RequestImageData: expected REQUEST_IMAGE_DATA, received %2x\n", buf[1]); } } goto AGAINU; } data_length = (int)buf[3] * 0x100 + (int)buf[2]; for (i = 0; i < data_length; i += 2) { values[i / 2] = STV_RecombineInt(buf[6 + i], buf[7 + i]); } return data_length / 2; /*ISUpdateDisplay( buffer, j) ; */ /* Update the display of the INDI client */ } else { fprintf(stderr, "Error: waiting for data on the serial port at line %d\n", j); return -1; } } else { fprintf(stderr, "STV_RequestImageData: error writing %d\n", res); } } return 0; } int STV_DecompressData(unsigned char *data, int *values, int length, int expected_n_values) { int i, n_values; int value; int base; base = STV_RecombineInt(data[0], data[1]); /*STV Manual says: MSB then LSB! */ values[0] = base; i = 2; n_values = 1; while (i < length) { if (((data[i] & 0xc0)) == 0x80) { /*two bytes, first byte: bit 7 set, 6 cleared, 14 bits data */ if ((data[i] & 0x20) == 0x20) { /* minus sign set? */ value = -((int)(((~data[i] & 0x1f)) * 0x100) + (((int)(~data[i + 1])) & 0xff)) - 1; /* value without sign */ } else { value = (int)((data[i] & 0x1f) * 0x100) + (int)(data[i + 1]); } base = value + base; /* new base value */ values[n_values++] = base; /*pixel data */ i += 2; } else if (((data[i] & 0x80)) == 0) { /* one byte: bit 7 clear (7 bits data) */ if ((data[i] & 0x40) == 0x40) { /* minus sign set? */ value = -(int)((~(data[i])) & 0x3f) - 1; /* value without sign */ } else { value = (int)(data[i] & 0x3f); /*fprintf( stderr, "Value 7P: %d, pixel value: %d, length %d, pix=%d\n", value, value+ base, length, n_values) ; */ } /* Sometimes the STV send a 0 at the end, thats not very likely a pixel to pixel variation */ /* I checked the last decompressed pixel value against the last uncompressed pixel value */ /* with different images - it seems to be ok. */ if ((value == 0) && (n_values == expected_n_values)) { /*fprintf( stderr, "Ignoring byte %d, Zero difference obtained values: %d\n", i, n_values) ; */ } else { base = value + base; values[n_values++] = base; } i++; } else if (((data[i] & 0xc0)) == 0xc0) { /*two bytes, values= pixel_n/4 */ /*STV_PrintBits( data[ i], 8) ; */ /*STV_PrintBits( data[ i+ 1], 8) ; */ value = 4 * ((int)((data[i] & 0x3f) * 0x100) + (int)(data[i + 1])); /*fprintf( stderr, "Value 14P: %d, pixel value: %d, length %d, pix=%d\n", value, value+ base, length, n_values) ; */ base = value; values[n_values++] = value; i += 2; /*return -n_values ; */ } else { fprintf(stderr, "Unknown compression case: %2x, length %d, i=%d\n", data[i], length, i); return -n_values; /* exit */ } } return n_values; } int STV_TXDisplay(void) { int data[] = { TRUE }; return STV_SendPacket(DISPLAY_ECHO, data, 1); } int STV_TerminateTXDisplay(void) { int res; int data[] = { FALSE }; res = STV_SendPacket(DISPLAY_ECHO, data, 1); /* Despite the manual says so, I not always see an ACK packet */ /* So i flush it for the moment */ tcflush(fd, TCIOFLUSH); return res; } int STV_FileStatus(int status) { int data[] = { status }; return STV_SendPacket(FILE_STATUS, data, 1); } int STV_DownloadComplete(void) { return STV_SendPacket(DOWNLOAD_COMPLETE, NULL, 0); } int STV_Download(void) { return STV_SendPacket(REQUEST_DOWNLOAD, NULL, 0); } int STV_DownloadAll(void) { return STV_SendPacket(REQUEST_DOWNLOAD_ALL, NULL, 0); } int STV_RequestAck(void) { int data[] = { 0x06, 0x06, 0x06 }; /* SBIG manual says contains data, which? */ return STV_SendPacket(REQUEST_ACK, data, 3); } int STV_BufferStatus(int buffer) { unsigned char buf[1024]; int buffers; int res; int val; /*fprintf(stderr, "STV_BufferStatus entering\n") ; */ usleep(50000); if ((res = STV_SendPacket(REQUEST_BUFFER_STATUS, NULL, 0)) < 0) { fprintf(stderr, "STV_BufferStatus: Error requesting buffer status: %d\n", buffer); return -1; } else { /*fprintf( stderr, "STV_BufferStatus %2d\n", buffer) ; */ } AGAIN: if ((res = STV_ReceivePacket(buf, 0)) < 0) { fprintf(stderr, "STV_BufferStatus: Error reading: %d\n", res); return -1; ; } if (buf[1] == REQUEST_BUFFER_STATUS) { buffers = STV_RecombineInt(buf[6], buf[7]) * 0x10000 + STV_RecombineInt(buf[8], buf[9]); if ((val = STV_GetBits(buffers, buffer, 1)) == 1) { res = 0; /* image present */ } else { /*fprintf( stderr, "STV_BufferStatus %2d is empty\n", buffer) ; */ res = 1; /* empty */ } } else { /* The SBIG manual does not specify an ACK, but it is there (at least sometimes) */ if ((val = STV_CheckAck(buf)) == 0) { /*fprintf( stderr, "STV_BufferStatus SAW ACK, reading again\n") ; */ goto AGAIN; } /* The SBIG manual does not specify the cmd in the answer */ if (buf[1] != DISPLAY_ECHO) { fprintf(stderr, "STV_BufferStatus: unexpected cmd byte received %d\n", buf[1]); val = STV_RecombineInt(buf[2], buf[3]); res = STV_PrintBuffer(buf, 6 + val + 2); return -1; } else { /* a display packet is silently ignored, there are many of this kind */ fprintf(stderr, "STV_BufferStatus DISPLAY_ECHO received, try again\n"); return -1; } } /* fprintf(stderr, "STV_BufferStatus leaving\n") ; */ return res; } /* Low level communication */ /* STV_ReceivePacket n_bytes read */ int STV_ReceivePacket(unsigned char *buf, int mode) { int i, j, k; int n_bytes = 0; int length = 0; int res; int trb = 0; int pos = 0; char display1[25]; char display2[25]; j = 0; tracking_buf[0] = 0; /*fprintf( stderr,"R") ; */ while (pos < 6) { /* Read the header first, calculate length of data */ /* At higher speeds the data arrives in smaller chunks, assembling the packet */ if ((trb = read(fd, (char *)(buf + pos), 1)) == -1) { fprintf(stderr, "Error, %s\n", strerror(errno)); } if (buf[0] == 0xa5) { if (pos == 5) { pos++; break; } else { pos += trb; /* could be zero */ } } else { /* In tracking mode the STV sends raw data (time, brightnes, centroid x,y). */ /* Under normal operation here appear pieces of a packet. This could happen */ /* on start up or if something goes wrong. */ tracking_buf[j++] = buf[pos]; /*fprintf(stderr, "READ: %d, read %d, pos %d, 0x%2x< %c\n", j, trb, pos, buf[pos], buf[pos]) ; */ } } if (j > 0) { /* Sometimes the packets are corrupt, e.g. at the very beginning */ /* Or it is tracking information */ tracking_buf[j] = 0; if (mode == ON) { /* Tracking mode */ fprintf(stderr, "%s\n", tracking_buf); } else { for (k = 0; k < j; k++) { if (!(tracking_buf[k] > 29 && tracking_buf[k] < 127)) { tracking_buf[k] = 0x20; /* simply */ } } fprintf(stderr, "Not a packet: length: %d >%s<\n", j, tracking_buf); } } n_bytes = pos; if ((buf[0] == 0xa5) && (pos == 6)) { /* Check the sanity of the header part */ if ((res = STV_CheckHeaderSum(buf)) == 0) { length = (int)buf[3] * 0x100 + (int)buf[2]; ; if (length > 0) { trb = 0; while (pos < 6 + length + 2) { /* header, data, check sum */ /* At higher speeds the data arrives in smaller chunks, assembling the packet */ if ((trb = read(fd, (char *)(buf + pos), (6 + length + 2) - pos)) == -1) { fprintf(stderr, "STV_ReceivePacket: Error reading at serial port, %s\n", strerror(errno)); return -1; } pos += trb; } n_bytes += pos; /*fprintf(stderr, "STV_ReceivePacket: LEAVING LOOP length %d, to read %d, read %d, pos %d\n", length, (6 + length + 2)- pos, trb, pos) ; */ } } else { fprintf(stderr, "STV_ReceivePacket: Header check failed\n"); return -1; } } else if (pos > 0) { for (i = 0; i < 6; i++) { if (buf[i] == 0xa5) { fprintf(stderr, "STV_ReceivePacket: pos= %d, saw 0xa5 at %d\n", pos, i); } } return -1; } else { fprintf(stderr, "STV_ReceivePacket: NO 0xa5 until pos= %d\n", pos); return -1; } if (length > 0) { /* Check the sanity of the data */ if ((res = STV_CheckDataSum(buf)) == -1) { return -1; } } /* Analyse the display and retrieve the values */ if (buf[1] == DISPLAY_ECHO) { for (i = 0; i < 24; i++) { display1[i] = buf[i + 6]; if (display1[i] == 0) { display1[i] = 0x32; } display2[i] = buf[i + 30]; if (display2[i] == 0) { display2[i] = 0x32; } } display1[24] = 0; display2[24] = 0; /*fprintf(stderr, "STV_ReceivePacket: DISPLAY1 %s<\n", display1) ; */ /*fprintf(stderr, "STV_ReceivePacket: DISPLAY2 %s<\n", display2) ; */ /* CCD temperature */ if ((res = strncmp("CCD Temp.", display2, 9)) == 0) { float t; res = sscanf(display2, "CCD Temp. %f", &t); di.ccd_temperature = (double)t; /*fprintf(stderr, "STV_ReceivePacket: Read from DISPLAY2 %g<\n", di.ccd_temperature ) ; */ } /* further values can be stored here */ } return n_bytes; } int STV_CheckHeaderSum(unsigned char *buf) { int sum = buf[0] + buf[1] + buf[2] + buf[3]; /* Calculated header sum */ int sumbuf = STV_RecombineInt(buf[4], buf[5]); /* STV packet header sum */ if (buf[0] != 0xa5) { fprintf(stderr, "STV_CheckHeaderSum: Wrong start byte, skipping\n"); return -1; } if (sum != sumbuf) { fprintf(stderr, "STV_CheckHeaderSum: NOK: %d==%d\n", sum, sumbuf); return -1; } return 0; } int STV_CheckDataSum(unsigned char *buf) { /* *buf points to the beginning of the packet */ int j, n; int sum = 0; int sumbuf = 0; n = STV_RecombineInt(buf[2], buf[3]); if (n == 0) { fprintf(stderr, "STV_CheckDataSum: No data present\n"); return 0; } sumbuf = STV_RecombineInt(buf[6 + n], buf[6 + n + 1]); for (j = 0; j < n; j++) { sum += (int)buf[6 + j]; } sum = sum & 0xffff; if (sum != sumbuf) { fprintf(stderr, "DATA SUM NOK: %d !=%d\n", sum, sumbuf); return -1; } return 0; } int STV_PrintBuffer(unsigned char *buf, int n) { /* For debugging purposes only */ int i; fprintf(stderr, "\nHEADER: %d bytes ", n); for (i = 0; i < n; i++) { if (i == 6) { fprintf(stderr, "\nDATA : "); } fprintf(stderr, "%d:0x%2x<>%c< >>", i, (unsigned char)buf[i], (unsigned char)buf[i]); /*STV_PrintBits((unsigned int) buf[i], 8) ; */ } fprintf(stderr, "\n"); return 0; } int STV_PrintBufferAsText(unsigned char *buf, int n) { /* For debugging purposes only */ int i; fprintf(stderr, "\nHEADER: %d bytes ", n); for (i = 0; i < n; i++) { if (i == 6) { fprintf(stderr, "\nDATA : "); } fprintf(stderr, "%c", (unsigned char)buf[i]); } fprintf(stderr, "\n"); return 0; } /* Aggregates a STV command packet */ int STV_SendPacket(int cmd, int *data, int n) { char buf[1024]; int j, l; int res; int sum; /* check sum */ /* Header section */ buf[0] = (unsigned char)0xa5; /* start byte */ buf[1] = (unsigned char)cmd; /* command byte */ buf[2] = (unsigned char)2 * n; /* data length N (low byte) */ buf[3] = (unsigned char)0x00; /* data length N (high byte) */ sum = buf[0] + buf[1] + buf[2] + buf[3]; /* header checksum */ buf[4] = (unsigned char)sum % 0x100; /* header checksum low byte */ buf[5] = (unsigned char)(sum / 0x100); /* header checksum high byte */ /* DATA section */ l = 0; if (n > 0) { l = 2; /* Two bytes per value are sent to STV */ for (j = 0; j < 2 * n; j += 2) { buf[6 + j] = (unsigned char)(data[j / 2] % 0x100); /* data low byte */ buf[7 + j] = (unsigned char)(data[j / 2] / 0x100); /* data high byte */ } sum = 0; for (j = 0; j < 2 * n; j++) { if ((int)buf[6 + j] < 0) { sum = sum + 0xff + (int)buf[6 + j] + 1; } else { sum = sum + (int)buf[6 + j]; } } buf[6 + 2 * n] = (unsigned char)(sum % 0x10000); /* data checksum (low byte) */ buf[7 + 2 * n] = (unsigned char)(sum / 0x100); /* data checksum (high byte) */ } if ((res = STV_CheckHeaderSum((unsigned char *)buf)) != 0) { /* Check outgoing packet as well */ fprintf(stderr, "STV_SendPacket: corrupt header\n"); if (n > 0) { if ((res = STV_CheckDataSum((unsigned char *)buf)) != 0) { fprintf(stderr, "STV_SendPacket: corrupt data\n"); } } } return STV_portWrite(buf, 8 + l * n); } /* Returns 0 or -1 in case of an error */ int STV_portWrite(char *buf, int nbytes) { int bytesWritten = 0; /*fprintf( stderr,"w") ; */ while (nbytes > 0) { if ((bytesWritten = write(fd, buf, nbytes)) == -1) { fprintf(stderr, "STV_portWrite: Error writing at serial port, %s\n", strerror(errno)); /* return -1 ; */ } if (bytesWritten < 0) { fprintf(stderr, "STV_portWrite: Error writing\n"); return -1; } else { buf += bytesWritten; nbytes -= bytesWritten; } } return nbytes; } int STV_CheckAck(unsigned char *buf) { /* Watch out for an ACK, for debugging purposes only */ int i; unsigned char ackseq[] = { 0xa5, 0x06, 0x00, 0x00, 0xab, 0x00 }; for (i = 0; i < 6; i++) { if (buf[i] != ackseq[i]) { return -1; } } return 0; } unsigned int STV_RecombineInt(unsigned char low_byte, unsigned char high_byte) { return (unsigned int)high_byte * (unsigned int)0x100 + (unsigned int)low_byte; } unsigned int STV_GetBits(unsigned int x, int p, int n) { /* from the C book */ return (x >> (p + 1 - n)) & ~(~0 << n); } void STV_PrintBits(unsigned int x, int n) { /* debugging purposes only */ int i; int res; fprintf(stderr, "STV_PrintBits:\n"); if (n > 8) { fprintf(stderr, "54321098 76543210\n"); } else { fprintf(stderr, "76543210\n"); } for (i = n; i > 0; i--) { if ((i == 8) && (n > 8)) { fprintf(stderr, " "); } if ((res = STV_GetBits(x, i - 1, 1)) == 0) { fprintf(stderr, "0"); } else { fprintf(stderr, "1"); } } fprintf(stderr, "\n"); } double STV_SetCCDTemperature(double set_value) { int i; int res; int delay = 40000; unsigned char buf[1024]; /* 1st step */ res = STV_Interrupt(); /* Reset Display */ tcflush(fd, TCIOFLUSH); usleep(100000); STV_MenueCCDTemperature(delay); for (i = 0; i < 100; i++) { /* Change to the highest temperature */ res = STV_LRRotaryIncrease(); tcflush(fd, TCIOFLUSH); usleep(delay); } di.ccd_temperature = 25.2; /* The actual value is set in STV_ReceivePacket, needed to enter the while() loop */ i = 0; while (set_value < di.ccd_temperature) { /*fprintf( stderr, "STV_SetCCDTemperature %g %g\n", set_value, di.ccd_temperature) ; */ res = STV_LRRotaryDecrease(); /* Lower CCD temperature */ /*fprintf(stderr, ":") ; */ usleep(10000); res = STV_TerminateTXDisplay(); usleep(10000); res = STV_TXDisplay(); /* That's the trick, STV sends at least one display */ res = STV_ReceivePacket(buf, 0); /* discard it */ /*STV_PrintBufferAsText( buf, res) ; */ if ((res != 62) || (i++ > 100)) { /* why 56 + 6?, 6 + 48 + 6 */ STV_PrintBuffer(buf, res); /*STV_PrintBufferAsText( buf, res) ; */ return 0.0; } } res = STV_Interrupt(); return di.ccd_temperature; } int STV_MenueCCDTemperature(int delay) { int res; res = STV_MenueSetup(delay); usleep(delay); res = STV_UDRotaryIncrease(); /* Change to CCD Temperature */ /*usleep( delay) ; */ tcflush(fd, TCIOFLUSH); return 0; } int STV_SetDateTime(char *times) { int i; int res; int turn; struct ln_date utm; int delay = 20000; /*fprintf(stderr, "STV_SetTime\n") ; */ if ((times == NULL) || ((res = strlen(times)) == 0)) ln_get_date_from_sys(&utm); else { if (extractISOTime(times, &utm) < 0) { fprintf(stderr, "Bad time string %s\n", times); return -1; } } /* Print out the date and time in the standard format. */ /*fprintf( stderr, "TIME %s\n", asctime (utc)) ; */ /* 1st step */ res = STV_Interrupt(); /* Reset Display */ usleep(delay); tcflush(fd, TCIOFLUSH); res = STV_MenueDateTime(delay); usleep(delay); res = STV_MenueDateTime(delay); /* This not an error */ usleep(delay); for (i = 0; i < 13; i++) { /* Reset Month menu to the lef most position */ res = STV_LRRotaryDecrease(); usleep(delay); } for (i = 0; i < utm.months; i++) { /* Set Month menu */ res = STV_LRRotaryIncrease(); usleep(delay); tcflush(fd, TCIOFLUSH); } res = STV_AKey(); /* Press the Parameter button */ usleep(delay); tcflush(fd, TCIOFLUSH); for (i = 0; i < 32; i++) { /* Reset Day menu to the lef most position */ res = STV_LRRotaryDecrease(); usleep(delay); tcflush(fd, TCIOFLUSH); } for (i = 0; i < utm.days - 1; i++) { /* Set Day menu -1? */ res = STV_LRRotaryIncrease(); usleep(delay); tcflush(fd, TCIOFLUSH); } res = STV_AKey(); /* Press the Parameter button */ usleep(delay); tcflush(fd, TCIOFLUSH); for (i = 0; i < 128; i++) { /* Reset Year menu to the lef most position, ATTENTION */ res = STV_LRRotaryDecrease(); usleep(delay); /* sleep a 1/100 second */ tcflush(fd, TCIOFLUSH); } /* JM: Is this how you want not? Please verify the code! */ int ymenu = utm.years % 100; for (i = 0; i < ymenu; i++) { /* Set Year menu */ res = STV_LRRotaryIncrease(); usleep(delay); tcflush(fd, TCIOFLUSH); } res = STV_AKey(); /* Press the Parameter button */ usleep(delay); tcflush(fd, TCIOFLUSH); for (i = 0; i < 25; i++) { /* Reset Hour menu to the lef most position, ATTENTION */ res = STV_LRRotaryDecrease(); usleep(delay); tcflush(fd, TCIOFLUSH); } for (i = 0; i < utm.hours; i++) { /* Set Hour menu */ res = STV_LRRotaryIncrease(); usleep(delay); tcflush(fd, TCIOFLUSH); } res = STV_AKey(); /* Press the Parameter button */ usleep(delay); tcflush(fd, TCIOFLUSH); for (i = 0; i < 61; i++) { /* Reset Minute menu to the lef most position, ATTENTION */ res = STV_LRRotaryDecrease(); usleep(delay); tcflush(fd, TCIOFLUSH); } for (i = 0; i < utm.minutes; i++) { /* Set Minute menu */ res = STV_LRRotaryIncrease(); usleep(delay); tcflush(fd, TCIOFLUSH); } res = STV_AKey(); /* Press the Parameter button */ tcflush(fd, TCIOFLUSH); for (i = 0; i < 5; i++) { /* Reset Seconds menu to the lef most position, ATTENTION */ res = STV_LRRotaryDecrease(); usleep(delay); tcflush(fd, TCIOFLUSH); } if (utm.seconds < 15) { turn = 0; } else if (utm.seconds < 30) { turn = 1; } else if (utm.seconds < 45) { turn = 2; } else { turn = 3; } for (i = 0; i < turn; i++) { /* Set Seconds menu, steps of 15 seconds */ res = STV_LRRotaryIncrease(); usleep(delay); tcflush(fd, TCIOFLUSH); } res = STV_AKey(); /* Press the Parameter button */ usleep(delay); tcflush(fd, TCIOFLUSH); res = STV_BKey(); /* Press the Parameter button */ usleep(delay); tcflush(fd, TCIOFLUSH); res = STV_Interrupt(); return 0; } int STV_MenueDateTime(int delay) { int i; int res; res = STV_MenueSetup(delay); usleep(delay); res = STV_BKey(); /* Press the Value button */ usleep(delay); for (i = 0; i < 8; i++) { /* Reset Date menu to the lef most position */ res = STV_UDRotaryDecrease(); usleep(delay); tcflush(fd, TCIOFLUSH); } return 0; } int STV_MenueSetup(int delay) { int i; int res; res = STV_Setup(); /* Change to Setup */ usleep(delay); for (i = 0; i < 16; i++) { /* Reset Setup menu to the lef most position */ res = STV_UDRotaryDecrease(); usleep(delay); tcflush(fd, TCIOFLUSH); } return 0; } int STV_Connect(char *device, int baud) { /*fprintf( stderr, "STV_Connect\n") ; */ if ((fd = init_serial(device, baud, 8, 0, 1)) == -1) { fprintf(stderr, "Error on port %s, %s\n", device, strerror(errno)); return -1; } return fd; } /****************************************************************************** * shutdown_serial(..) ****************************************************************************** * Restores terminal settings of open serial port device and close the file. * Arguments: * fd: file descriptor of open serial port device. *****************************************************************************/ void shutdown_serial(int fd) { if (fd > 0) { if (tcsetattr(fd, TCSANOW, &orig_tty_setting) < 0) { perror("shutdown_serial: can't restore serial device's terminal settings."); } close(fd); } } /****************************************************************************** * init_serial(..) ****************************************************************************** * Opens and initializes a serial device and returns it's file descriptor. * Arguments: * device_name : device name string of the device to open (/dev/ttyS0, ...) * bit_rate : bit rate * word_size : number of data bits, 7 or 8, USE 8 DATA BITS with modbus * parity : 0=no parity, 1=parity EVEN, 2=parity ODD * stop_bits : number of stop bits : 1 or 2 * Return: * file descriptor of successfully opened serial device * or -1 in case of error. *****************************************************************************/ int init_serial(char *device_name, int bit_rate, int word_size, int parity, int stop_bits) { int fd; char *msg; /* open serial device */ fd = open(device_name, O_RDWR | O_NOCTTY); if (fd < 0) { if (asprintf(&msg, "init_serial: open %s failed", device_name) < 0) perror(NULL); else perror(msg); return -1; } /* save current tty settings */ if (tcgetattr(fd, &orig_tty_setting) < 0) { perror("init_serial: can't get terminal parameters."); return -1; } /* Control Modes */ /* Set bps rate */ int bps; switch (bit_rate) { case 0: bps = B0; break; case 50: bps = B50; break; case 75: bps = B75; break; case 110: bps = B110; break; case 134: bps = B134; break; case 150: bps = B150; break; case 200: bps = B200; break; case 300: bps = B300; break; case 600: bps = B600; break; case 1200: bps = B1200; break; case 1800: bps = B1800; break; case 2400: bps = B2400; break; case 4800: bps = B4800; break; case 9600: bps = B9600; break; case 19200: bps = B19200; break; case 38400: bps = B38400; break; case 57600: bps = B57600; break; case 115200: bps = B115200; break; case 230400: bps = B230400; break; default: if (asprintf(&msg, "init_serial: %d is not a valid bit rate.", bit_rate) < 0) perror(NULL); else perror(msg); return -1; } if ((cfsetispeed(&tty_setting, bps) < 0) || (cfsetospeed(&tty_setting, bps) < 0)) { perror("init_serial: failed setting bit rate."); return -1; } /* Control Modes */ /* set no flow control word size, parity and stop bits. */ /* Also don't hangup automatically and ignore modem status. */ /* Finally enable receiving characters. */ tty_setting.c_cflag &= ~(CSIZE | CSTOPB | PARENB | PARODD | HUPCL | CRTSCTS); /* #ifdef CBAUDEX */ /*tty_setting.c_cflag &= ~(CBAUDEX); */ /*#endif */ /*#ifdef CBAUDEXT */ /*tty_setting.c_cflag &= ~(CBAUDEXT); */ /*#endif */ tty_setting.c_cflag |= (CLOCAL | CREAD); /* word size */ switch (word_size) { case 5: tty_setting.c_cflag |= CS5; break; case 6: tty_setting.c_cflag |= CS6; break; case 7: tty_setting.c_cflag |= CS7; break; case 8: tty_setting.c_cflag |= CS8; break; default: fprintf(stderr, "Default\n"); if (asprintf(&msg, "init_serial: %d is not a valid data bit count.", word_size) < 0) perror(NULL); else perror(msg); return -1; } /* parity */ switch (parity) { case PARITY_NONE: break; case PARITY_EVEN: tty_setting.c_cflag |= PARENB; break; case PARITY_ODD: tty_setting.c_cflag |= PARENB | PARODD; break; default: fprintf(stderr, "Default1\n"); if (asprintf(&msg, "init_serial: %d is not a valid parity selection value.", parity) < 0) perror(NULL); else perror(msg); return -1; } /* stop_bits */ switch (stop_bits) { case 1: break; case 2: tty_setting.c_cflag |= CSTOPB; break; default: fprintf(stderr, "Default2\n"); if (asprintf(&msg, "init_serial: %d is not a valid stop bit count.", stop_bits) < 0) perror(NULL); else perror(msg); return -1; } /* Control Modes complete */ /* Ignore bytes with parity errors and make terminal raw and dumb. */ tty_setting.c_iflag &= ~(PARMRK | ISTRIP | IGNCR | ICRNL | INLCR | IXOFF | IXON | IXANY); tty_setting.c_iflag |= INPCK | IGNPAR | IGNBRK; /* Raw output. */ tty_setting.c_oflag &= ~(OPOST | ONLCR); /* Local Modes */ /* Don't echo characters. Don't generate signals. */ /* Don't process any characters. */ tty_setting.c_lflag &= ~(ICANON | ECHO | ECHOE | ISIG | IEXTEN | NOFLSH | TOSTOP); tty_setting.c_lflag |= NOFLSH; /* blocking read until 1 char arrives */ tty_setting.c_cc[VMIN] = 1; tty_setting.c_cc[VTIME] = 0; /* now clear input and output buffers and activate the new terminal settings */ tcflush(fd, TCIOFLUSH); if (tcsetattr(fd, TCSANOW, &tty_setting)) { perror("init_serial: failed setting attributes on serial port."); shutdown_serial(fd); return -1; } return fd; } libindi/obsolete/stv.c0000664000175000017500000016703213263645557014277 0ustar jasemjasem#if 0 STV Driver Copyright (C) 2006 Markus Wildi, markus.wildi@datacomm.ch 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 /* Standard headers */ #include #include #include #include #include #ifndef _WIN32 #include #endif /* INDI Core headers */ #include "indidevapi.h" /* INDI Eventloop mechanism */ #include "eventloop.h" /* INDI Common Routines/RS232 */ #include "indicom.h" /* Config parameters */ #include /* Fits */ #include /* STV's definitions */ #include "stvdriver.h" /* Definitions */ #define mydev "STV Guider" /* Device name */ #define CONNECTION_GROUP "Connection" /* Group name */ #define SETTINGS_GROUP "Setings" /* Group name */ #define BUTTONS_GROUP "Buttons and Knobs" /* Button Pannel */ #define IMAGE_GROUP "Download" /* Button Pannel */ #define currentBuffer BufferN[0].value #define currentX WindowingN[0].value #define currentY WindowingN[1].value #define currentLines WindowingN[2].value #define currentLength WindowingN[3].value #define currentCompression CompressionS[0].s static int compression = OFF; static int acquiring = OFF; static int guiding = OFF; static int processing = OFF; /* Fits (fli_ccd project) */ enum STVFrames { LIGHT_FRAME = 0, BIAS_FRAME, DARK_FRAME, FLAT_FRAME }; #define TEMPFILE_LEN 16 /* Image (fli_ccd project)*/ typedef struct { int width; int height; int frameType; int expose; unsigned short *img; } img_t; static img_t *STVImg; /* Function adapted from fli_ccd project */ void addFITSKeywords(fitsfile *fptr, IMAGE_INFO *image_info); int writeFITS(const char *filename, IMAGE_INFO *image_info, char errmsg[]); void uploadFile(const char *filename); /* File descriptor and call back id */ int fd; static int cb = -1; char tracking_buf[1024]; /* Function prototypes */ int ISTerminateTXDisplay(void); int ISRestoreTXDisplay(void); int ISMessageImageInfo(int buffer, IMAGE_INFO *image_info); int ISRequestImageData(int compression, int buffer, int x_offset, int y_offset, int length, int lines); int STV_LRRotaryDecrease(void); int STV_LRRotaryIncrease(void); int STV_UDRotaryDecrease(void); int STV_UDRotaryIncrease(void); int STV_AKey(void); int STV_BKey(void); int STV_Setup(void); int STV_Interrupt(void); int STV_Focus(void); int STV_Image(void); int STV_Monitor(void); int STV_Calibrate(void); int STV_Track(void); int STV_Display(void); int STV_FileOps(void); int STV_RequestImageInfo(int imagebuffer, IMAGE_INFO *image_info); int STV_BufferStatus(int buffer); int STV_RequestImage(int compression, int buffer, int x_offset, int y_offset, int *length, int *lines, int image[][320], IMAGE_INFO *image_info); int STV_Download(void); int STV_TXDisplay(void); int STV_TerminateTXDisplay(void); int STV_RequestAck(void); unsigned int STV_GetBits(unsigned int x, int p, int n); int STV_PrintBuffer(unsigned char *cmdbuf, int n); void handleError(ISwitchVectorProperty *svp, int err, const char *msg); static void ISInit(); void ISCallBack(void); int init_serial(char *device_name, int bit_rate, int word_size, int parity, int stop_bits); int STV_ReceivePacket(unsigned char *buf, int mode); int STV_Connect(char *device, int baud); int STV_SetDateTime(char *times); double STV_SetCCDTemperature(double set_value); static IText StatusT[] = { { "STATUS", "This driver", "is experimental, contact markus.wildi@datacomm.ch", 0, 0, 0 }, }; static ITextVectorProperty StatusTP = { mydev, "STAUS", "Status", CONNECTION_GROUP, IP_RO, ISR_1OFMANY, IPS_IDLE, StatusT, NARRAY(StatusT), "", 0 }; /* RS 232 Connection */ static ISwitch PowerS[] = { { "CONNECT", "Connect", ISS_OFF, 0, 0 }, { "DISCONNECT", "Disconnect", ISS_OFF, 0, 0 }, }; static ISwitchVectorProperty PowerSP = { mydev, "CONNECTION", "Connection", CONNECTION_GROUP, IP_RW, ISR_1OFMANY, 0, IPS_IDLE, PowerS, NARRAY(PowerS), "", 0 }; /* Serial Port */ static IText PortT[] = { { "PORT", "Port", NULL, 0, 0, 0 }, { "SPEED", "Speed", NULL, 0, 0, 0 } }; static ITextVectorProperty PortTP = { mydev, "DEVICE_PORT", "Port", CONNECTION_GROUP, IP_RW, ISR_1OFMANY, IPS_IDLE, PortT, NARRAY(PortT), "", 0 }; static ISwitch TXDisplayS[] = { { "1", "On", ISS_ON, 0, 0 }, { "2", "Off", ISS_OFF, 0, 0 }, }; static ISwitchVectorProperty TXDisplaySP = { mydev, "Update Display", "Update Display", CONNECTION_GROUP, IP_RW, ISR_1OFMANY, 0, IPS_IDLE, TXDisplayS, NARRAY(TXDisplayS), "", 0 }; static IText DisplayCT[] = { { "DISPLAYC1", "Line 1", NULL, 0, 0, 0 }, { "DISPLAYC2", "Line 2", NULL, 0, 0, 0 } }; static ITextVectorProperty DisplayCTP = { mydev, "DISPLAYC", "Display", CONNECTION_GROUP, IP_RO, ISR_1OFMANY, IPS_IDLE, DisplayCT, NARRAY(DisplayCT), "", 0 }; static IText DisplayBT[] = { { "DISPLAYB1", "Line 1", NULL, 0, 0, 0 }, { "DISPLAYB2", "Line 2", NULL, 0, 0, 0 } }; static ITextVectorProperty DisplayBTP = { mydev, "DISPLAYB", "Display", BUTTONS_GROUP, IP_RO, ISR_1OFMANY, IPS_IDLE, DisplayBT, NARRAY(DisplayBT), "", 0 }; static IText DisplayDT[] = { { "DISPLAYD1", "Line 1", NULL, 0, 0, 0 }, { "DISPLAYD2", "Line 2", NULL, 0, 0, 0 } }; static ITextVectorProperty DisplayDTP = { mydev, "DISPLAYD", "Display", IMAGE_GROUP, IP_RO, ISR_1OFMANY, IPS_IDLE, DisplayDT, NARRAY(DisplayDT), "", 0 }; /* Setings */ static IText UTCT[] = { { "UTC", "UTC", NULL, 0, 0, 0 } }; ITextVectorProperty UTCTP = { mydev, "TIME_UTC", "UTC Time", SETTINGS_GROUP, IP_RW, 0, IPS_IDLE, UTCT, NARRAY(UTCT), "", 0 }; static INumber SetCCDTemperatureN[] = { { "TEMPERATURE", "Cel. -55.1, +25.2", "%6.1f", -55.8, 25.2, 0., 16., 0, 0, 0 }, }; static INumberVectorProperty SetCCDTemperatureNP = { mydev, "CCD_TEMPERATURE", "CCD Temperature", SETTINGS_GROUP, IP_RW, ISR_1OFMANY, IPS_IDLE, SetCCDTemperatureN, NARRAY(SetCCDTemperatureN), "", 0 }; /* Buttons */ static ISwitch ControlS[] = { { "1", "Parameter", ISS_OFF, 0, 0 }, { "2", "Increase", ISS_OFF, 0, 0 }, { "3", "Decrease", ISS_OFF, 0, 0 }, }; static ISwitchVectorProperty ControlSP = { mydev, "ParaButtons", "Control", BUTTONS_GROUP, IP_RW, ISR_1OFMANY, 0, IPS_IDLE, ControlS, NARRAY(ControlS), "", 0 }; static ISwitch ValueS[] = { { "1", "Value", ISS_OFF, 0, 0 }, { "2", "Increase", ISS_OFF, 0, 0 }, { "3", "Decrease", ISS_OFF, 0, 0 }, }; static ISwitchVectorProperty ValueSP = { mydev, "ValueButtons", "Control", BUTTONS_GROUP, IP_RW, ISR_1OFMANY, 0, IPS_IDLE, ValueS, NARRAY(ValueS), "", 0 }; static ISwitch AuxiliaryS[] = { { "1", "Setup", ISS_OFF, 0, 0 }, { "2", "Interrupt", ISS_OFF, 0, 0 }, }; static ISwitchVectorProperty AuxiliarySP = { mydev, "Auxilliary", "", BUTTONS_GROUP, IP_RW, ISR_1OFMANY, 0, IPS_IDLE, AuxiliaryS, NARRAY(AuxiliaryS), "", 0 }; static ISwitch AcquireS[] = { { "1", "Focus", ISS_OFF, 0, 0 }, { "2", "Image", ISS_OFF, 0, 0 }, { "3", "Monitor", ISS_OFF, 0, 0 }, }; static ISwitchVectorProperty AcquireSP = { mydev, "Acquire", "", BUTTONS_GROUP, IP_RW, ISR_1OFMANY, 0, IPS_IDLE, AcquireS, NARRAY(AcquireS), "", 0 }; static ISwitch GuideS[] = { { "1", "Calibrate", ISS_OFF, 0, 0 }, { "2", "Track", ISS_OFF, 0, 0 }, }; static ISwitchVectorProperty GuideSP = { mydev, "Guide", "", BUTTONS_GROUP, IP_RW, ISR_1OFMANY, 0, IPS_IDLE, GuideS, NARRAY(GuideS), "", 0 }; static ISwitch ProcessS[] = { { "1", "Display/Crosshairs", ISS_OFF, 0, 0 }, { "2", "File Ops", ISS_OFF, 0, 0 }, }; static ISwitchVectorProperty ProcessSP = { mydev, "Process", "", BUTTONS_GROUP, IP_RW, ISR_1OFMANY, 0, IPS_IDLE, ProcessS, NARRAY(ProcessS), "", 0 }; static ISwitch CompressionS[] = { { "1", "On", ISS_OFF, 0, 0 }, { "2", "Off", ISS_OFF, 0, 0 }, }; static ISwitchVectorProperty CompressionSP = { mydev, "Compression", "", IMAGE_GROUP, IP_RW, ISR_1OFMANY, 0, IPS_IDLE, CompressionS, NARRAY(CompressionS), "", 0 }; static ISwitch BufferStatusS[] = { { "1", "Status", ISS_OFF, 0, 0 }, }; static ISwitchVectorProperty BufferStatusSP = { mydev, "Buffers", "", IMAGE_GROUP, IP_RW, ISR_1OFMANY, 0, IPS_IDLE, BufferStatusS, NARRAY(BufferStatusS), "", 0 }; static INumber BufferN[] = { { "A0", "Number 1 - 32", "%6.0f", 1., 32., 0., 32., 0, 0, 0 }, }; static INumberVectorProperty BufferNP = { mydev, "BUFFER_Number", "Buffer", IMAGE_GROUP, IP_RW, ISR_1OFMANY, IPS_IDLE, BufferN, NARRAY(BufferN), "", 0 }; static INumber WindowingN[] = { { "X", "Offset x", "%6.0f", 0., 199., 0., 0., 0, 0, 0 }, { "Y", "Offset y", "%6.0f", 0., 319., 0., 0., 0, 0, 0 }, { "HEIGHT", "Lines", "%6.0f", 1., 200., 0., 200., 0, 0, 0 }, { "WIDTH", "Length", "%6.0f", 1., 320., 0., 320., 0, 0, 0 }, }; static INumberVectorProperty WindowingNP = { mydev, "CCD_FRAME", "Windowing", IMAGE_GROUP, IP_RW, ISR_1OFMANY, IPS_IDLE, WindowingN, NARRAY(WindowingN), "", 0 }; static ISwitch ImageInfoS[] = { { "1", "One Image", ISS_OFF, 0, 0 }, { "2", "All Images", ISS_OFF, 0, 0 }, }; static ISwitchVectorProperty ImageInfoSP = { mydev, "Information", "", IMAGE_GROUP, IP_RW, ISR_1OFMANY, 0, IPS_IDLE, ImageInfoS, NARRAY(ImageInfoS), "", 0 }; static ISwitch DownloadS[] = { { "1", "One Image", ISS_OFF, 0, 0 }, { "2", "All Images", ISS_OFF, 0, 0 }, }; static ISwitchVectorProperty DownloadSP = { mydev, "Download", "", IMAGE_GROUP, IP_RW, ISR_1OFMANY, 0, IPS_IDLE, DownloadS, NARRAY(DownloadS), "", 0 }; /* BLOB for sending image */ static IBLOB imageB = { "CCD1", "Image", "", 0, 0, 0, 0, 0, 0, 0 }; static IBLOBVectorProperty imageBP = { mydev, "Image", "Image", IMAGE_GROUP, IP_RO, 0, IPS_IDLE, &imageB, 1, "", 0 }; /* Initlization routine */ static void ISInit() { static int isInit = 0; if (isInit) return; IUSaveText(&PortT[0], "/dev/ttyUSB0"); IUSaveText(&PortT[1], "115200"); if ((DisplayCT[0].text = malloc(1024)) == NULL) { fprintf(stderr, "3:Memory allocation error"); return; } if ((DisplayCT[1].text = malloc(1024)) == NULL) { fprintf(stderr, "4:Memory allocation error"); return; } if ((DisplayBT[0].text = malloc(1024)) == NULL) { fprintf(stderr, "5:Memory allocation error"); return; } if ((DisplayBT[1].text = malloc(1024)) == NULL) { fprintf(stderr, "5:Memory allocation error"); return; } if ((DisplayDT[0].text = malloc(1024)) == NULL) { fprintf(stderr, "7:Memory allocation error"); return; } if ((DisplayDT[1].text = malloc(1024)) == NULL) { fprintf(stderr, "8:Memory allocation error"); return; } if ((STVImg = malloc(sizeof(img_t))) == NULL) { fprintf(stderr, "9:Memory allocation error"); return; } isInit = 1; } void ISResetButtons(char *message) { ControlSP.s = IPS_IDLE; IUResetSwitch(&ControlSP); IDSetSwitch(&ControlSP, NULL); ValueSP.s = IPS_IDLE; IUResetSwitch(&ValueSP); IDSetSwitch(&ValueSP, NULL); AuxiliarySP.s = IPS_IDLE; IUResetSwitch(&AuxiliarySP); IDSetSwitch(&AuxiliarySP, NULL); AcquireSP.s = IPS_IDLE; IUResetSwitch(&AcquireSP); IDSetSwitch(&AcquireSP, NULL); GuideSP.s = IPS_IDLE; IUResetSwitch(&GuideSP); IDSetSwitch(&GuideSP, NULL); ProcessSP.s = IPS_IDLE; IUResetSwitch(&ProcessSP); IDSetSwitch(&ProcessSP, NULL); ImageInfoSP.s = IPS_IDLE; IUResetSwitch(&ImageInfoSP); IDSetSwitch(&ImageInfoSP, NULL); BufferStatusSP.s = IPS_IDLE; IUResetSwitch(&BufferStatusSP); IDSetSwitch(&BufferStatusSP, NULL); /* SP.s= IPS_IDLE ; */ /* IUResetSwitch(&SP); */ /* IDSetSwitch(&SP, NULL); */ DownloadSP.s = IPS_IDLE; IUResetSwitch(&DownloadSP); IDSetSwitch(&DownloadSP, "%s", message); return; } /* This function is called when ever the file handle fd provides data */ void ISCallBack() { int res; int k, l, m; unsigned char buf[1024]; IERmCallback(cb); cb = -1; /* fprintf( stderr, "ISCallBack\n") ; */ /* if(( counter++ % 4) ==0){ */ /* fprintf( stderr, ".") ; */ /* } */ if (PowerS[0].s == ISS_ON) { res = STV_ReceivePacket(buf, guiding); /* res= STV_PrintBuffer(buf,res) ; */ DisplayCTP.s = IPS_IDLE; IDSetText(&DisplayCTP, NULL); DisplayBTP.s = IPS_IDLE; IDSetText(&DisplayBTP, NULL); DisplayDTP.s = IPS_IDLE; IDSetText(&DisplayDTP, NULL); switch ((int)buf[1]) { /* STV cmd byte */ case DISPLAY_ECHO: if (res < 0) { DisplayCTP.s = IPS_ALERT; IDSetText(&DisplayCTP, NULL); DisplayBTP.s = IPS_ALERT; IDSetText(&DisplayBTP, NULL); DisplayDTP.s = IPS_ALERT; IDSetText(&DisplayDTP, NULL); IDMessage(mydev, "Error while reading, continuing\n"); } else { l = 0; m = 0; /* replace unprintable characters and format the string */ for (k = 0; k < 24; k++) { if (buf[k + 6] == 0x5e) { /* first line */ DisplayCT[0].text[l - 1] = 0x50; /* P */ DisplayCT[0].text[l++] = 0x6b; /* k */ } else if (buf[k + 6] == 0xd7) { DisplayCT[0].text[l++] = 0x28; /* "(x,y) " */ DisplayCT[0].text[l++] = 0x78; DisplayCT[0].text[l++] = 0x2c; DisplayCT[0].text[l++] = 0x79; DisplayCT[0].text[l++] = 0x29; DisplayCT[0].text[l++] = 0x20; } else if (buf[k + 6] > 29 && buf[k + 6] < 127) { DisplayCT[0].text[l++] = buf[k + 6]; } else { /* fprintf(stderr, "LINE 1%2x, %2x, %2x, %c %c %c\n", buf[k+ 5], buf[k+ 6], buf[k+ 7], buf[k+ 5], buf[k+ 6], buf[k+ 7]) ; */ DisplayCT[0].text[l++] = 0x20; } if (buf[k + 30] == 0xb0) { /* second line */ DisplayCT[1].text[m++] = 0x43; /* Celsius */ } else if (buf[k + 30] > 29 && buf[k + 30] < 127) { DisplayCT[1].text[m++] = buf[k + 30]; } else { /* fprintf(stderr, "LINE 2 %2x, %2x, %2x, %c %c %c\n", buf[k+ 29], buf[k+ 30], buf[k+ 31], buf[k+ 29], buf[k+ 30], buf[k+ 31]) ; */ DisplayCT[1].text[m++] = 0x20; } } DisplayCT[0].text[l] = 0; DisplayCT[1].text[m] = 0; strcpy(DisplayBT[0].text, DisplayCT[0].text); strcpy(DisplayBT[1].text, DisplayCT[1].text); strcpy(DisplayDT[0].text, DisplayCT[0].text); strcpy(DisplayDT[1].text, DisplayCT[1].text); DisplayCTP.s = IPS_OK; IDSetText(&DisplayCTP, NULL); DisplayBTP.s = IPS_OK; IDSetText(&DisplayBTP, NULL); DisplayDTP.s = IPS_OK; IDSetText(&DisplayDTP, NULL); } break; case REQUEST_DOWNLOAD: /* fprintf(stderr, "STV says REQUEST_DOWNLOAD\n") ; */ if (TXDisplayS[0].s == ISS_ON) { res = STV_Download(); imageB.blob = NULL; imageB.bloblen = 0; imageB.size = 0; imageBP.s = IPS_ALERT; IDSetBLOB(&imageBP, NULL); IDMessage(mydev, "Switch off display read out manually first (Update Display: Off\n)"); } else { tcflush(fd, TCIOFLUSH); usleep(100000); res = ISRequestImageData(1, 31, 0, 0, 320, 200); /* Download the on screen image (buffer 32 -1) */ } /*fprintf(stderr, "STV END REQUEST_DOWNLOAD\n") ; */ break; case REQUEST_DOWNLOAD_ALL: IDMessage(mydev, "REQUEST_DOWNLOAD_ALL initiated at the STV not implemented"); break; case ACK: if (cb == -1) { strcpy(DisplayCT[0].text, "Key press acknowledged"); strcpy(DisplayBT[0].text, DisplayCT[0].text); strcpy(DisplayDT[0].text, DisplayCT[0].text); DisplayCTP.s = IPS_OK; IDSetText(&DisplayCTP, NULL); DisplayBTP.s = IPS_OK; IDSetText(&DisplayBTP, NULL); DisplayDTP.s = IPS_OK; IDSetText(&DisplayDTP, NULL); } break; case NACK: /*fprintf(stderr, "STV says NACK!!") ; */ IDMessage(mydev, "STV says NACK!"); break; case REQUEST_BUFFER_STATUS: IDMessage(mydev, "Request Buffer status seen, ignoring\n"); break; default: if (guiding == ON) { /* While STV is tracking, it send time, brightnes, centroid x,y */ IDMessage(mydev, "Tracking: %s", tracking_buf); } else { /*fprintf(stderr, "STV ISCallBack: Unknown response 0x%2x\n", buf[1]) ; */ IDLog("ISCallBack: Unknown response 0x%2x\n", buf[1]); } break; } } cb = IEAddCallback(fd, (IE_CBF *)ISCallBack, NULL); } /* Client retrieves properties */ void ISGetProperties(const char *dev) { /* #1 Let's make sure everything has been initialized properly */ ISInit(); /* #2 Let's make sure that the client is asking for the properties of our device, otherwise ignore */ if (dev != nullptr && strcmp(mydev, dev)) return; /* #3 Tell the client to create new properties */ /* Connection tab */ IDDefText(&DisplayCTP, NULL); IDDefSwitch(&PowerSP, NULL); IDDefText(&PortTP, NULL); IDDefSwitch(&TXDisplaySP, NULL); IDDefText(&StatusTP, NULL); /* Settings Tab */ IDDefText(&UTCTP, NULL); IDDefNumber(&SetCCDTemperatureNP, NULL); /* Buttons tab */ IDDefText(&DisplayBTP, NULL); IDDefSwitch(&ControlSP, NULL); IDDefSwitch(&ValueSP, NULL); IDDefSwitch(&AuxiliarySP, NULL); IDDefSwitch(&AcquireSP, NULL); IDDefSwitch(&GuideSP, NULL); IDDefSwitch(&ProcessSP, NULL); /* Image tab */ IDDefText(&DisplayDTP, NULL); /* "direct" read out does not work well IDDefSwitch(&BufferStatusSP, NULL) ; */ IDDefSwitch(&BufferStatusSP, NULL); IDDefSwitch(&ImageInfoSP, NULL); IDDefNumber(&BufferNP, NULL); IDDefSwitch(&DownloadSP, NULL); IDDefSwitch(&CompressionSP, NULL); IDDefNumber(&WindowingNP, NULL); IDDefBLOB(&imageBP, NULL); } /* Client sets new switch */ void ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) { int i, j; int res = 0; int baud; IMAGE_INFO image_info; ISwitch *sp; int lower_buffer = 0; int upper_buffer = 0; /*fprintf(stderr, "ISNewSwitch\n") ; */ /* #1 Let's make sure everything has been initialized properly */ ISInit(); /* #2 Let's make sure that the client is asking to update the properties of our device, otherwise ignore */ if (dev != nullptr && strcmp(dev, mydev)) return; /* #3 Now let's check if the property the client wants to change is the PowerSP (name: CONNECTION) property*/ if (!strcmp(name, PowerSP.name)) { /* A. We reset all switches (in this case CONNECT and DISCONNECT) to ISS_OFF */ IUResetSwitch(&PowerSP); /* B. We update the switches by sending their names and updated states IUUpdateSwitch function */ IUUpdateSwitch(&PowerSP, states, names, n); /* C. We try to establish a connection to our device or terminate it*/ int res = 0; switch (PowerS[0].s) { case ISS_ON: if ((res = strcmp("9600", PortT[1].text)) == 0) { baud = 9600; } else if ((res = strcmp("19200", PortT[1].text)) == 0) { baud = 19200; } else if ((res = strcmp("38400", PortT[1].text)) == 0) { baud = 38400; } else if ((res = strcmp("57600", PortT[1].text)) == 0) { baud = 57600; } else if ((res = strcmp("115200", PortT[1].text)) == 0) { baud = 115200; } else { IUSaveText(&PortT[1], "9600"); IDSetText(&PortTP, "Wrong RS 232 value: %s, defaulting to 9600 baud", PortT[1].text); return; } if ((fd = STV_Connect(PortT[0].text, baud)) == -1) { PowerSP.s = IPS_ALERT; IUResetSwitch(&PowerSP); IDSetSwitch(&PowerSP, "Error connecting to port %s", PortT[0].text); return; } else { cb = IEAddCallback(fd, (IE_CBF *)ISCallBack, NULL); /* The SBIG manual says one can request an ACK, never saw it, even not on a RS 232 tester */ if ((res = STV_RequestAck()) != 0) { fprintf(stderr, "COULD not write an ACK\n"); } /* Second trial: start reading out the display */ if ((res = STV_TXDisplay()) != 0) { fprintf(stderr, "STV: Could not write %d\n", res); return; } } PowerSP.s = IPS_OK; IDSetSwitch(&PowerSP, "STV is online, port: %s, baud rate: %s", PortT[0].text, PortT[1].text); PortTP.s = IPS_OK; IDSetText(&PortTP, NULL); break; case ISS_OFF: IERmCallback(cb); cb = -1; /* Close the serial port */ tty_disconnect(fd); ISResetButtons(NULL); GuideSP.s = IPS_IDLE; IUResetSwitch(&GuideSP); IDSetSwitch(&GuideSP, NULL); TXDisplaySP.s = IPS_IDLE; IUResetSwitch(&TXDisplaySP); IDSetSwitch(&TXDisplaySP, NULL); DisplayCTP.s = IPS_IDLE; IDSetText(&DisplayCTP, NULL); DisplayBTP.s = IPS_IDLE; IDSetText(&DisplayBTP, NULL); DisplayDTP.s = IPS_IDLE; IDSetText(&DisplayDTP, NULL); PortTP.s = IPS_IDLE; IDSetText(&PortTP, NULL); imageB.blob = NULL; imageB.bloblen = 0; imageB.size = 0; imageBP.s = IPS_IDLE; IDSetBLOB(&imageBP, NULL); PowerSP.s = IPS_IDLE; IUResetSwitch(&PowerSP); IDSetSwitch(&PowerSP, "STV is offline"); break; } return; } else if (!strcmp(name, AuxiliarySP.name)) { /* Setup und interrupt buttons */ ISResetButtons(NULL); IUResetSwitch(&AuxiliarySP); IUUpdateSwitch(&AuxiliarySP, states, names, n); for (i = 0; i < n; i++) { sp = IUFindSwitch(&AuxiliarySP, names[i]); if (sp == &AuxiliaryS[0]) { res = STV_Setup(); } else if (sp == &AuxiliaryS[1]) { res = STV_Interrupt(); } } if (res == 0) { AuxiliarySP.s = IPS_OK; IUResetSwitch(&AuxiliarySP); IDSetSwitch(&AuxiliarySP, NULL); } else { AuxiliarySP.s = IPS_ALERT; IUResetSwitch(&AuxiliarySP); IDSetSwitch(&AuxiliarySP, "Check connection"); } } else if (!strcmp(name, ControlSP.name)) { /* Parameter, value and the rotary knobs */ ISResetButtons(NULL); IUResetSwitch(&ControlSP); IUUpdateSwitch(&ControlSP, states, names, n); acquiring = OFF; guiding = OFF; processing = OFF; for (i = 0; i < n; i++) { sp = IUFindSwitch(&ControlSP, names[i]); /* If the state found is ControlS[0] then process it */ if (sp == &ControlS[0]) { res = STV_AKey(); } else if (sp == &ControlS[1]) { res = STV_UDRotaryIncrease(); } else if (sp == &ControlS[2]) { res = STV_UDRotaryDecrease(); } } if (res == 0) { ControlSP.s = IPS_OK; IUResetSwitch(&ControlSP); IDSetSwitch(&ControlSP, NULL); } else { ControlSP.s = IPS_ALERT; IUResetSwitch(&ControlSP); IDSetSwitch(&ControlSP, "Check connection"); } } else if (!strcmp(name, ValueSP.name)) { /* Button Value, left/right knob */ ISResetButtons(NULL); IUResetSwitch(&ValueSP); IUUpdateSwitch(&ValueSP, states, names, n); acquiring = OFF; guiding = OFF; processing = OFF; for (i = 0; i < n; i++) { sp = IUFindSwitch(&ValueSP, names[i]); if (sp == &ValueS[0]) { res = STV_BKey(); } else if (sp == &ValueS[1]) { res = STV_LRRotaryIncrease(); } else if (sp == &ValueS[2]) { res = STV_LRRotaryDecrease(); } } if (res == 0) { ValueSP.s = IPS_OK; IUResetSwitch(&ValueSP); IDSetSwitch(&ValueSP, NULL); } else { ValueSP.s = IPS_ALERT; IUResetSwitch(&ValueSP); IDSetSwitch(&ValueSP, "Check connection"); } } else if (!strcmp(name, AcquireSP.name)) { /* Focus, Image Monitor buttons */ ISResetButtons(NULL); IUResetSwitch(&AcquireSP); IUUpdateSwitch(&AcquireSP, states, names, n); acquiring = ON; guiding = OFF; processing = OFF; for (i = 0; i < n; i++) { sp = IUFindSwitch(&AcquireSP, names[i]); if (sp == &AcquireS[0]) { res = STV_Focus(); } else if (sp == &AcquireS[1]) { res = STV_Image(); } else if (sp == &AcquireS[2]) { res = STV_Monitor(); } } if (res == 0) { AcquireSP.s = IPS_OK; IUResetSwitch(&AcquireSP); IDSetSwitch(&AcquireSP, NULL); } else { AcquireSP.s = IPS_ALERT; IUResetSwitch(&AcquireSP); IDSetSwitch(&AcquireSP, "Check connection"); } } else if (!strcmp(name, GuideSP.name)) { /* Calibrate, Track buttons */ ISResetButtons(NULL); IUResetSwitch(&GuideSP); IUUpdateSwitch(&GuideSP, states, names, n); acquiring = OFF; guiding = ON; processing = OFF; for (i = 0; i < n; i++) { sp = IUFindSwitch(&GuideSP, names[i]); if (sp == &GuideS[0]) { res = STV_Calibrate(); } else if (sp == &GuideS[1]) { res = STV_Track(); } } if (res == 0) { GuideSP.s = IPS_OK; IUResetSwitch(&GuideSP); IDSetSwitch(&GuideSP, NULL); } else { GuideSP.s = IPS_ALERT; IUResetSwitch(&GuideSP); IDSetSwitch(&GuideSP, "Check connection"); } } else if (!strcmp(name, ProcessSP.name)) { ISResetButtons(NULL); IUResetSwitch(&ProcessSP); IUUpdateSwitch(&ProcessSP, states, names, n); acquiring = OFF; guiding = OFF; processing = ON; for (i = 0; i < n; i++) { sp = IUFindSwitch(&ProcessSP, names[i]); if (sp == &ProcessS[0]) { res = STV_Display(); } else if (sp == &ProcessS[1]) { res = STV_FileOps(); } } if (res == 0) { ProcessSP.s = IPS_OK; IUResetSwitch(&ProcessSP); IDSetSwitch(&ProcessSP, NULL); } else { ProcessSP.s = IPS_ALERT; IUResetSwitch(&ProcessSP); IDSetSwitch(&ProcessSP, "Check connection"); } } else if (!strcmp(name, ImageInfoSP.name)) { acquiring = OFF; guiding = OFF; processing = OFF; /* Read out the image buffer and display a short message if it is empty or not */ res = ISTerminateTXDisplay(); for (i = 0; i < n; i++) { sp = IUFindSwitch(&ImageInfoSP, names[i]); if (sp == &ImageInfoS[0]) { if ((res = STV_RequestImageInfo(currentBuffer - 1, &image_info)) == 0) { ISMessageImageInfo((int)currentBuffer - 1, &image_info); } else { IDMessage(mydev, "Buffer %2d is empty", (int)currentBuffer); } break; } else if (sp == &ImageInfoS[1]) { for (i = 0; i < 32; i++) { if ((res = STV_RequestImageInfo(i, &image_info)) == 0) { ISMessageImageInfo(i, &image_info); } else { IDMessage(mydev, "Buffer %2d is empty", i + 1); } } break; } } if (res == 0) { ImageInfoSP.s = IPS_OK; IUResetSwitch(&ImageInfoSP); IDSetSwitch(&ImageInfoSP, NULL); } else { ImageInfoSP.s = IPS_ALERT; IUResetSwitch(&ImageInfoSP); /*IDSetSwitch( &ImageInfoSP, "Check connection") ; */ IDSetSwitch(&ImageInfoSP, NULL); } res = STV_Interrupt(); /* STV initiates a download that we do not want */ res = ISRestoreTXDisplay(); } else if (!strcmp(name, CompressionSP.name)) { acquiring = OFF; guiding = OFF; processing = OFF; /* Enable or disable compression for image download */ ISResetButtons(NULL); IUResetSwitch(&CompressionSP); IUUpdateSwitch(&CompressionSP, states, names, n); for (i = 0; i < n; i++) { sp = IUFindSwitch(&CompressionSP, names[i]); if (sp == &CompressionS[0]) { CompressionS[0].s = ISS_ON; } else if (sp == &CompressionS[1]) { CompressionS[1].s = ISS_ON; } } CompressionSP.s = IPS_OK; IDSetSwitch(&CompressionSP, NULL); } else if (!strcmp(name, BufferStatusSP.name)) { ISResetButtons(NULL); BufferStatusSP.s = IPS_ALERT; IUResetSwitch(&BufferStatusSP); IDSetSwitch(&BufferStatusSP, "Wait..."); if ((AcquireSP.s != OFF) || (GuideSP.s != OFF) || (ProcessSP.s != OFF)) { acquiring = OFF; guiding = OFF; processing = OFF; ISResetButtons("Interrupting ongoing image acquisition, calibration or tracking\n"); AcquireSP.s = IPS_IDLE; IUResetSwitch(&AcquireSP); IDSetSwitch(&AcquireSP, NULL); GuideSP.s = IPS_IDLE; IUResetSwitch(&GuideSP); IDSetSwitch(&GuideSP, NULL); ProcessSP.s = IPS_IDLE; IUResetSwitch(&ProcessSP); IDSetSwitch(&ProcessSP, NULL); ImageInfoSP.s = IPS_IDLE; IUResetSwitch(&ImageInfoSP); IDSetSwitch(&ImageInfoSP, NULL); res = STV_Interrupt(); usleep(100000); res = STV_Interrupt(); } acquiring = OFF; guiding = OFF; processing = OFF; sp = IUFindSwitch(&BufferStatusSP, names[0]); if ((res = ISTerminateTXDisplay()) != 0) { fprintf(stderr, "STV Buffer can not terminate TX %d\n", res); } if (sp == &BufferStatusS[0]) { for (i = 31; i > -1; i--) { usleep(50000); if ((res = STV_BufferStatus(i)) == 0) { IDMessage(mydev, "Buffer %2d: image present", i + 1); } else { IDMessage(mydev, "Buffer %2d: empty", i + 1); } } } BufferStatusS[0].s = ISS_OFF; if (0 <= res) { BufferStatusSP.s = IPS_OK; IUResetSwitch(&BufferStatusSP); IDSetSwitch(&BufferStatusSP, NULL); } else { BufferStatusSP.s = IPS_ALERT; IUResetSwitch(&BufferStatusSP); IDSetSwitch(&BufferStatusSP, "Check connection"); } res = ISRestoreTXDisplay(); res = STV_Interrupt(); } else if (!strcmp(name, DownloadSP.name)) { /* Download images */ /* Downloading while the STV is occupied is not working */ if ((AcquireSP.s != OFF) || (GuideSP.s != OFF) || (ProcessSP.s != OFF)) { ISResetButtons("Interrupting ongoing image acquisition, calibration or tracking\n"); AcquireSP.s = IPS_IDLE; IUResetSwitch(&AcquireSP); IDSetSwitch(&AcquireSP, NULL); GuideSP.s = IPS_IDLE; IUResetSwitch(&GuideSP); IDSetSwitch(&GuideSP, NULL); ProcessSP.s = IPS_IDLE; IUResetSwitch(&ProcessSP); IDSetSwitch(&ProcessSP, NULL); ImageInfoSP.s = IPS_IDLE; IUResetSwitch(&ImageInfoSP); IDSetSwitch(&ImageInfoSP, NULL); res = STV_Interrupt(); usleep(100000); res = STV_Interrupt(); } acquiring = OFF; guiding = OFF; processing = OFF; if ((res = ISTerminateTXDisplay()) != 0) { fprintf(stderr, "STV Buffer can not terminate TX %d\n", res); } DownloadSP.s = IPS_ALERT; IUResetSwitch(&DownloadSP); IDSetSwitch(&DownloadSP, NULL); compression = OFF; if (CompressionS[0].s == ISS_ON) { compression = ON; } for (i = 0; i < n; i++) { sp = IUFindSwitch(&DownloadSP, names[i]); if (sp == &DownloadS[0]) { lower_buffer = currentBuffer - 2; upper_buffer = currentBuffer - 1; } else if (sp == &DownloadS[1]) { lower_buffer = -1; upper_buffer = 31; } } for (j = upper_buffer; j > lower_buffer; j--) { if ((res = ISRequestImageData(compression, j, currentX, currentY, currentLength, currentLines)) != 0) { if (res == 1) { IDMessage(mydev, "Buffer %2.0f: empty", (double)(j + 1)); } else { break; } } } if (res == 0) { IDMessage(mydev, "STV waits for SYNC TIME Do it! Setting time, PLEASE WAIT!"); if ((res = STV_SetDateTime(NULL)) == 0) { UTCTP.s = IPS_OK; IDSetText(&UTCTP, "Time set to UTC now"); } else { UTCTP.s = IPS_ALERT; IDSetText(&UTCTP, "Error setting time, check connection"); } DownloadSP.s = IPS_OK; IUResetSwitch(&DownloadSP); IDSetSwitch(&DownloadSP, NULL); } else { /* res could be -1 (STV_RequestImageData) */ DownloadSP.s = IPS_ALERT; IUResetSwitch(&DownloadSP); IDSetSwitch(&DownloadSP, "Check connection"); IDSetSwitch(&DownloadSP, NULL); } res = ISRestoreTXDisplay(); res = STV_Interrupt(); IDMessage(mydev, "You may continue NOW"); } else if (!strcmp(name, TXDisplaySP.name)) { acquiring = OFF; guiding = OFF; processing = OFF; ISResetButtons(NULL); IUResetSwitch(&TXDisplaySP); IUUpdateSwitch(&TXDisplaySP, states, names, n); for (i = 0; i < n; i++) { sp = IUFindSwitch(&TXDisplaySP, names[i]); if (sp == &TXDisplayS[0]) { if ((res = STV_TXDisplay()) == 0) { TXDisplaySP.s = IPS_OK; IDSetSwitch(&TXDisplaySP, "Reading out display"); DisplayCTP.s = IPS_OK; IDSetText(&DisplayCTP, NULL); DisplayBTP.s = IPS_OK; IDSetText(&DisplayBTP, NULL); DisplayDTP.s = IPS_OK; IDSetText(&DisplayDTP, NULL); } } else if (sp == &TXDisplayS[1]) { DisplayCTP.s = IPS_IDLE; DisplayBTP.s = IPS_IDLE; DisplayDTP.s = IPS_IDLE; if ((res = STV_TerminateTXDisplay()) == 0) { TXDisplaySP.s = IPS_OK; IDSetSwitch(&TXDisplaySP, "Stopping display read out"); DisplayCTP.s = IPS_IDLE; IUSaveText(&DisplayCT[0], " "); /* reset client's display */ IUSaveText(&DisplayCT[1], " "); IDSetText(&DisplayCTP, NULL); DisplayBTP.s = IPS_IDLE; IUSaveText(&DisplayBT[0], " "); /* reset client's display */ IUSaveText(&DisplayBT[1], " "); IDSetText(&DisplayBTP, NULL); DisplayDTP.s = IPS_IDLE; IUSaveText(&DisplayDT[0], " "); /* reset client's display */ IUSaveText(&DisplayDT[1], " "); IDSetText(&DisplayDTP, NULL); } } } if (res != 0) { TXDisplaySP.s = IPS_ALERT; IUResetSwitch(&TXDisplaySP); IDSetSwitch(&TXDisplaySP, "Check connection"); } } } void ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n) { int res; IText *tp; /*fprintf(stderr, "ISNewText\n") ; */ /* #1 Let's make sure everything has been initialized properly */ ISInit(); /* #2 Let's make sure that the client is asking to update the properties of our device, otherwise ignore */ if (dev != nullptr && strcmp(dev, mydev)) return; if (!strcmp(name, PortTP.name)) { if (IUUpdateText(&PortTP, texts, names, n) < 0) return; PortTP.s = IPS_OK; if (PowerS[0].s == ISS_ON) { PortTP.s = IPS_ALERT; IDSetText(&PortTP, "STV is already online"); } /* JM: Don't forget to send acknowledgment */ IDSetText(&PortTP, NULL); } else if (!strcmp(name, UTCTP.name)) { ISResetButtons(NULL); tp = IUFindText(&UTCTP, names[0]); if ((res = ISTerminateTXDisplay()) != 0) { fprintf(stderr, "STV Buffer can not terminate TX %d\n", res); } if (tp == &UTCT[0]) { if ((res = STV_SetDateTime(NULL)) == 0) { UTCTP.s = IPS_OK; IDSetText(&UTCTP, "Time set to UTC"); } else { UTCTP.s = IPS_ALERT; IDSetText(&UTCTP, "Error setting time, check connection"); } } res = ISRestoreTXDisplay(); } } /* Client sets new number */ void ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) { int res; double ccd_temperature; /*fprintf(stderr, "ISNewNumber\n") ; */ /* #1 Let's make sure everything has been initialized properly */ ISInit(); /* #2 Let's make sure that the client is asking to update the properties of our device, otherwise ignore */ if (dev != nullptr && strcmp(dev, mydev)) return; if (PowerS[0].s != ISS_ON) { PowerSP.s = IPS_ALERT; IDSetSwitch(&PowerSP, NULL); IDMessage("STV is offline", NULL); return; } if (!strcmp(name, BufferNP.name)) { INumber *buffer = IUFindNumber(&BufferNP, names[0]); if (buffer == &BufferN[0]) { currentBuffer = values[0]; /* Check the boundaries, this is incomplete at the moment */ BufferNP.s = IPS_OK; IDSetNumber(&BufferNP, NULL); } } else if (!strcmp(name, WindowingNP.name)) { INumber *buffer = IUFindNumber(&WindowingNP, names[0]); if (buffer == &WindowingN[0]) { currentX = values[0]; currentY = values[1]; currentLines = values[2]; currentLength = values[3]; WindowingNP.s = IPS_OK; IDSetNumber(&WindowingNP, NULL); } } else if (!strcmp(name, SetCCDTemperatureNP.name)) { if ((res = ISTerminateTXDisplay()) != 0) { fprintf(stderr, "STV Buffer can not terminate TX %d\n", res); } INumber *np = IUFindNumber(&SetCCDTemperatureNP, names[0]); if (np == &SetCCDTemperatureN[0]) { if ((ccd_temperature = STV_SetCCDTemperature(values[0])) != 0) { /* STV has no 0 C setting */ SetCCDTemperatureNP.s = IPS_OK; SetCCDTemperatureN[0].value = ccd_temperature; IDSetNumber(&SetCCDTemperatureNP, "CCD Temperature set to %g", SetCCDTemperatureN[0].value); } else { SetCCDTemperatureNP.s = IPS_ALERT; IDSetNumber(&SetCCDTemperatureNP, "Error setting CCD temperature, check connection"); } } res = ISRestoreTXDisplay(); } } void ISNewBLOB(const char *dev, const char *name, int sizes[], int blobsizes[], char *blobs[], char *formats[], char *names[], int n) { INDI_UNUSED(dev); INDI_UNUSED(name); INDI_UNUSED(sizes); INDI_UNUSED(blobsizes); INDI_UNUSED(blobs); INDI_UNUSED(formats); INDI_UNUSED(names); INDI_UNUSED(n); } void ISSnoopDevice(XMLEle *root) { INDI_UNUSED(root); } int writeFITS(const char *filename, IMAGE_INFO *image_info, char errmsg[]) { fitsfile *fptr; /* pointer to the FITS file; defined in fitsio.h */ int status; long fpixel = 1, naxis = 2, nelements; long naxes[2]; char filename_rw[TEMPFILE_LEN + 1]; naxes[0] = STVImg->width; naxes[1] = STVImg->height; /* Append ! to file name to over write it.*/ snprintf(filename_rw, TEMPFILE_LEN + 1, "!%s", filename); status = 0; /* initialize status before calling fitsio routines */ fits_create_file(&fptr, filename_rw, &status); /* create new file */ /* Create the primary array image (16-bit short integer pixels */ fits_create_img(fptr, USHORT_IMG, naxis, naxes, &status); addFITSKeywords(fptr, image_info); nelements = naxes[0] * naxes[1]; /* number of pixels to write */ /* Write the array of integers to the image */ fits_write_img(fptr, TUSHORT, fpixel, nelements, STVImg->img, &status); fits_close_file(fptr, &status); /* close the file */ fits_report_error(stderr, status); /* print out any error messages */ /* Success */ /*ExposeTimeNP.s = IPS_OK; */ /*IDSetNumber(&ExposeTimeNP, NULL); */ uploadFile(filename); return status; } void addFITSKeywords(fitsfile *fptr, IMAGE_INFO *image_info) { int status = 0; char binning_s[32]; char frame_s[32]; char date_obs_s[64]; char tmp[32]; image_info->pixelSize = 7.4; /* microns */ if (image_info->binning == 1) { snprintf(binning_s, 32, "(%1.0f x %1.0f)", 1., 1.); } else if (image_info->binning == 2) { snprintf(binning_s, 32, "(%1.0f x %1.0f)", 2., 2.); } else if (image_info->binning == 3) { snprintf(binning_s, 32, "(%1.0f x %1.0f)", 3., 3.); } else { fprintf(stderr, "Error in binning information: %d\n", image_info->binning); } strcpy(frame_s, "Light"); /* ToDo: assign the frame type */ /* switch (STVImg->frameType) */ /* { */ /* case LIGHT_FRAME: */ /* strcpy(frame_s, "Light"); */ /* break; */ /* case BIAS_FRAME: */ /* strcpy(frame_s, "Bias"); */ /* break; */ /* case FLAT_FRAME: */ /* strcpy(frame_s, "Flat Field"); */ /* break; */ /* case DARK_FRAME: */ /* strcpy(frame_s, "Dark"); */ /* break; */ /* } */ fits_update_key(fptr, TDOUBLE, "CCD-TEMP", &(image_info->ccdTemp), "CCD Temperature (Celcius)", &status); fits_update_key(fptr, TDOUBLE, "EXPOSURE", &(image_info->exposure), "Total Exposure Time (ms)", &status); fits_update_key(fptr, TDOUBLE, "PIX-SIZ", &(image_info->pixelSize), "Pixel Size (microns)", &status); fits_update_key(fptr, TSTRING, "BINNING", binning_s, "Binning HOR x VER", &status); fits_update_key(fptr, TSTRING, "FRAME", frame_s, "Frame Type", &status); fits_update_key(fptr, TDOUBLE, "DATAMIN", &(image_info->minValue), "Minimum value", &status); fits_update_key(fptr, TDOUBLE, "DATAMAX", &(image_info->maxValue), "Maximum value", &status); fits_update_key(fptr, TSTRING, "INSTRUME", "SBIG STV", "CCD Name", &status); sprintf(tmp, "%4d-", image_info->year); strcpy(date_obs_s, tmp); if (image_info->month < 10) { sprintf(tmp, "0%1d-", image_info->month); } else { sprintf(tmp, "%2d-", image_info->month); } strcat(date_obs_s, tmp); if (image_info->day < 10) { sprintf(tmp, "0%1dT", image_info->day); } else { sprintf(tmp, "%2dT", image_info->day); } strcat(date_obs_s, tmp); if (image_info->hours < 10) { sprintf(tmp, "0%1d:", image_info->hours); } else { sprintf(tmp, "%2d:", image_info->hours); } strcat(date_obs_s, tmp); if (image_info->minutes < 10) { sprintf(tmp, "0%1d:", image_info->minutes); } else { sprintf(tmp, "%2d:", image_info->minutes); } strcat(date_obs_s, tmp); if (image_info->seconds < 10) { sprintf(tmp, "0%1d:", image_info->seconds); } else { sprintf(tmp, "%2d:", image_info->seconds); } strcat(date_obs_s, tmp); fits_update_key(fptr, TSTRING, "DATE-OBS", date_obs_s, "Observing date (YYYY-MM-DDThh:mm:ss UT", &status); fits_write_date(fptr, &status); } void uploadFile(const char *filename) { FILE *fitsFile; unsigned char *fitsData, *compressedData; int r = 0; unsigned int i = 0, nr = 0; uLongf compressedBytes = 0; uLong totalBytes; struct stat stat_p; if (-1 == stat(filename, &stat_p)) { IDLog("Error occurred attempting to stat file.\n"); return; } totalBytes = stat_p.st_size; fitsData = (unsigned char *)malloc(sizeof(unsigned char) * totalBytes); compressedData = (unsigned char *)malloc(sizeof(unsigned char) * totalBytes + totalBytes / 64 + 16 + 3); if (fitsData == NULL || compressedData == NULL) { if (fitsData) free(fitsData); if (compressedData) free(compressedData); IDLog("Error! low memory. Unable to initialize fits buffers.\n"); return; } fitsFile = fopen(filename, "r"); if (fitsFile == NULL) { free(fitsData); free(compressedData); return; } /* #1 Read file from disk */ for (i = 0; i < totalBytes; i += nr) { nr = fread(fitsData + i, 1, totalBytes - i, fitsFile); if (nr <= 0) { IDLog("Error reading temporary FITS file.\n"); free(fitsData); fclose(fitsFile); return; } } fclose(fitsFile); compressedBytes = sizeof(char) * totalBytes + totalBytes / 64 + 16 + 3; /* #2 Compress it */ r = compress2(compressedData, &compressedBytes, fitsData, totalBytes, 9); if (r != Z_OK) { /* this should NEVER happen */ IDLog("internal error - compression failed: %d\n", r); return; } /* #3 Send it */ imageB.blob = compressedData; imageB.bloblen = compressedBytes; imageB.size = totalBytes; strcpy(imageB.format, ".fits.z"); imageBP.s = IPS_OK; IDSetBLOB(&imageBP, NULL); free(fitsData); free(compressedData); } int ISTerminateTXDisplay(void) { int res = 0; res = STV_Interrupt(); /* with out it hangs */ usleep(100000); IERmCallback(cb); cb = -1; if (TXDisplayS[0].s == ISS_ON) { TXDisplaySP.s = IPS_BUSY; IDSetSwitch(&TXDisplaySP, "Stopping display read out"); DisplayCTP.s = IPS_IDLE; IUSaveText(&DisplayCT[0], " "); /* reset client's display */ IUSaveText(&DisplayCT[1], " "); IDSetText(&DisplayCTP, NULL); DisplayBTP.s = IPS_IDLE; IUSaveText(&DisplayBT[0], " "); /* reset client's display */ IUSaveText(&DisplayBT[1], " "); IDSetText(&DisplayBTP, NULL); DisplayDTP.s = IPS_IDLE; IUSaveText(&DisplayDT[0], " "); /* reset client's display */ IUSaveText(&DisplayDT[1], " "); IDSetText(&DisplayDTP, NULL); if ((res = STV_TerminateTXDisplay()) != 0) { fprintf(stderr, "STV: error writing TTXD %d\n", res); } } else { res = 0; } usleep(500000); /* make sure that everything is discarded */ tcflush(fd, TCIOFLUSH); return res; } int ISRestoreTXDisplay(void) { int res = 0; cb = IEAddCallback(fd, (IE_CBF *)ISCallBack, NULL); if (TXDisplayS[0].s == ISS_ON) { usleep(500000); /* STV need a little rest */ res = STV_TXDisplay(); TXDisplaySP.s = IPS_OK; IDSetSwitch(&TXDisplaySP, "Starting Display read out"); DisplayCTP.s = IPS_OK; IDSetText(&DisplayCTP, NULL); DisplayBTP.s = IPS_OK; IDSetText(&DisplayBTP, NULL); DisplayDTP.s = IPS_OK; IDSetText(&DisplayDTP, NULL); } return res; } int ISMessageImageInfo(int buffer, IMAGE_INFO *image_info) { buffer++; /* IDMessage( mydev, "B%2d: descriptor:%d\n", buffer, image_info->descriptor) ; */ /* IDMessage( mydev, "B%2d: height:%d\n", buffer, image_info->height) ; */ /* IDMessage( mydev, "B%2d: width:%d\n", buffer, image_info->width) ; */ /* IDMessage( mydev, "B%2d: top:%d\n", buffer, image_info->top) ; */ /* IDMessage( mydev, "B%2d: left:%d\n", buffer, image_info->left) ; */ IDMessage(mydev, "B%2d: Exposure:%6.3f, Height:%2d, Width:%2d, CCD Temperature:%3.1f\n", buffer, image_info->exposure, image_info->height, image_info->width, image_info->ccdTemp); /* IDMessage( mydev, "B%2d: noExposure:%d\n", buffer, image_info->noExposure) ; */ /* IDMessage( mydev, "B%2d: analogGain:%d\n", buffer, image_info->analogGain) ; */ /* IDMessage( mydev, "B%2d: digitalGain:%d\n", buffer, image_info->digitalGain) ; */ /* IDMessage( mydev, "B%2d: focalLength:%d\n", buffer, image_info->focalLength) ; */ /* IDMessage( mydev, "B%2d: aperture:%d\n", buffer, image_info->aperture) ; */ /* IDMessage( mydev, "B%2d: packedDate:%d\n", buffer, image_info->packedDate) ; */ IDMessage(mydev, "B%2d: Year:%4d, Month: %2d, Day:%2d\n", buffer, image_info->year, image_info->month, image_info->day); /* IDMessage( mydev, "B%2d: Day:%d\n", buffer, image_info->day) ; */ /* IDMessage( mydev, "B%2d: Month:%d\n", buffer, image_info->month) ; */ /* IDMessage( mydev, "B%2d: packedTime:%d\n", buffer, image_info->packedTime) ; */ /* IDMessage( mydev, "B%2d: Seconds:%d\n", buffer, image_info->seconds) ; */ /* IDMessage( mydev, "B%2d: minutes:%d\n", buffer, image_info->minutes) ; */ IDMessage(mydev, "B%2d: Hours:%2d, Minutes:%2d, Seconds:%d\n", buffer, image_info->hours, image_info->minutes, image_info->seconds); /* IDMessage( mydev, "B%2d: ccdTemp:%f\n", buffer, image_info->ccdTemp) ; */ /* IDMessage( mydev, "B%2d: siteID:%d\n", buffer, image_info->siteID) ; */ /* IDMessage( mydev, "B%2d: eGain:%d\n", buffer, image_info->eGain) ; */ /* IDMessage( mydev, "B%2d: background:%d\n", buffer, image_info->background) ; */ /* IDMessage( mydev, "B%2d: range :%d\n", buffer, image_info->range ) ; */ /* IDMessage( mydev, "B%2d: pedestal:%d\n", buffer, image_info->pedestal) ; */ /* IDMessage( mydev, "B%2d: ccdTop :%d\n", buffer, image_info->ccdTop) ; */ /* IDMessage( mydev, "B%2d: ccdLeft:%d\n", buffer, image_info->ccdLeft) ; */ return 0; } int ISRequestImageData(int compression, int buffer, int x_offset, int y_offset, int length, int lines) { int res; int i, k; int img_size; char errmsg[1024]; int image[320][320]; IMAGE_INFO image_info; for (i = 0; i < 320; i++) { for (k = 0; k < 320; k++) { image[i][k] = -1; } } res = STV_RequestImage(compression, buffer, x_offset, y_offset, &length, &lines, image, &image_info); if (res == 0) { STVImg->width = length; STVImg->height = lines; img_size = STVImg->width * STVImg->height * sizeof(unsigned short); STVImg->img = malloc(img_size); for (i = 0; i < STVImg->height; i++) { /* x */ for (k = 0; k < STVImg->width; k++) { /* y */ STVImg->img[STVImg->width * i + k] = (unsigned short)image[i][k]; /* Uncomment this line in case of doubts about decompressed values and compare */ /* both sets. */ /*fprintf( stderr, "Line: %d %d %d %d\n", i, k, image[i][k], STVImg->img[ STVImg->width* i + k]) ; */ if (STVImg->img[STVImg->width * i + k] < image_info.minValue) { image_info.minValue = STVImg->img[STVImg->width * i + k]; } if (STVImg->img[STVImg->width * i + k] > image_info.maxValue) { image_info.maxValue = STVImg->img[STVImg->width * i + k]; } } } writeFITS("FITS.fits", &image_info, errmsg); /*fprintf( stderr, "Fits writing message: %s\n", errmsg) ; */ free(STVImg->img); } return res; } void ISUpdateDisplay(int buffer, int line) { if (!((line + 1) % 10)) { sprintf(DisplayCT[0].text, "Buffer %2d line: %3d", buffer + 1, line + 1); strcpy(DisplayBT[0].text, DisplayCT[0].text); strcpy(DisplayDT[0].text, DisplayCT[0].text); DisplayCTP.s = IPS_OK; IDSetText(&DisplayCTP, NULL); DisplayBTP.s = IPS_OK; IDSetText(&DisplayBTP, NULL); DisplayDTP.s = IPS_OK; IDSetText(&DisplayDTP, NULL); } else if ((line + 1) == 1) { /* first time */ IDMessage(mydev, "Image download started"); } else if (line < 0) { /* last line */ line = -line; sprintf(DisplayCT[0].text, "Buffer %2d line: %3d", buffer + 1, line + 1); strcpy(DisplayBT[0].text, DisplayCT[0].text); strcpy(DisplayDT[0].text, DisplayCT[0].text); DisplayCTP.s = IPS_OK; IDSetText(&DisplayCTP, NULL); DisplayBTP.s = IPS_OK; IDSetText(&DisplayBTP, NULL); DisplayDTP.s = IPS_OK; IDSetText(&DisplayDTP, NULL); IDMessage(mydev, "Image download ended, buffer %2d line: %3d", buffer + 1, line); } } libindi/indiapi.h0000664000175000017500000004025213263645557013263 0ustar jasemjasem#if 0 INDI Copyright (C) 2003 Elwood C. Downey 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 #pragma once /** \mainpage Instrument Neutral Distributed Interface INDI * \section Introduction

INDI is a simple XML-like communications protocol described for interactive and automated remote control of diverse instrumentation. INDI is small, easy to parse, and stateless.

A full description of the INDI protocol is detailed in the INDI white paper

Under INDI, any number of clients can connect to any number of drivers running one or more devices. It is a true N-to-N server/client architecture topology allowing for reliable deployments of several INDI server, client, and driver instances distrubted across different systems in different physical and logical locations.

The basic premise of INDI is this: Drivers are responsible for defining their functionality in terms of Properties. Clients are not aware of such properties until they establish connection with the driver and start receiving streams of defined properties. Each property encompases some functionality or information about the device. These include number, text, switch, light, and BLOB properties.

For example, all devices define the CONNECTION vector switch property, which is compromised of two switches:

  1. CONNECT: Connect to the device.
  2. DISCONNECT: Disconnect to the device.

Therefore, a client, whether it is a GUI client that represents such property as buttons, or a Python script that parses the properties, can change the state of the switch to cause the desired action.

Not all properties are equal. A few properties are reserved to ensure interoperality among different clients that want to target a specific functionality. These Standard Properties ensure that different clients agree of a common set of properties with specific meaning since INDI does not impose any specific meaning on the properties themselves.

INDI server acts as a convenient hub to route communications between clients and drivers. While it is not strictly required for controlling driver, it offers many queue and routing capabilities.

\section Audience Intended Audience INDI is intended for developers seeking to add support for their devices in INDI. Any INDI driver can be operated from numerous cross-platform cross-architecture clients. \section Development Developing under INDI

Please refere to the INDI Developers Manual for a complete guide on INDI's driver developemnt framework.

The INDI Library API is divided into the following main sections:

\section Tutorials INDI Library includes a number of tutorials to illustrate development of INDI drivers. Check out the examples provided with INDI library. \section Simulators Simulators provide a great framework to test drivers and equipment alike. INDI Library provides the following simulators:
  • @ref ScopeSim "Telescope Simulator": Offers GOTO capability, motion control, guiding, and ability to set Periodic Error (PE) which is read by the CCD simulator when generating images.
  • @ref CCDSim "CCD Simulator": Offers a very flexible CCD simulator with a primary CCD chip and a guide chip. The simulator generate images based on the RA & DEC coordinates it snoops from the telescope driver using General Star Catalog (GSC). Please note that you must install GSC for the CCD simulator to work properly. Furthermore, The simulator snoops FWHM from the focuser simulator which affects the generated images focus. All images are generated in standard FITS format.
  • @ref GuideSim "Guide Simulator": Simple dedicated Guide Simulator.
  • @ref FilterSim "Filter Wheel Simulator": Offers a simple simulator to change filter wheels and their corresponding designations.
  • @ref FocusSim "Focuser Simulator": Offers a simple simualtor for an absolute position focuser. It generates a simulated FWHM value that may be used by other simulator such as the CCD simulator.
  • @ref DomeSim "Dome Simulator": Offers a simple simulator for an absolute position dome with shutter.
  • @ref GPSSimulator "GPS Simulator": Offers a simple simulator for GPS devices that send time and location data to the client and other drivers.
\section Help You can find information on INDI development in the INDI Library site. Furthermore, you can discuss INDI related issues on the INDI development mailing list. \author Jasem Mutlaq \author Elwood Downey For a full list of contributors, please check Contributors page on Github. */ /** \file indiapi.h \brief Constants and Data structure definitions for the interface to the reference INDI C API implementation. \author Elwood C. Downey */ /******************************************************************************* * INDI wire protocol version implemented by this API. * N.B. this is indepedent of the API itself. */ #define INDIV 1.7 /* INDI Library version */ #define INDI_VERSION_MAJOR 1 #define INDI_VERSION_MINOR 7 #define INDI_VERSION_RELEASE 1 /******************************************************************************* * Manifest constants */ /** * @typedef ISState * @brief Switch state. */ typedef enum { ISS_OFF = 0, /*!< Switch is OFF */ ISS_ON /*!< Switch is ON */ } ISState; /** * @typedef IPState * @brief Property state. */ typedef enum { IPS_IDLE = 0, /*!< State is idle */ IPS_OK, /*!< State is ok */ IPS_BUSY, /*!< State is busy */ IPS_ALERT /*!< State is alert */ } IPState; /** * @typedef ISRule * @brief Switch vector rule hint. */ typedef enum { ISR_1OFMANY, /*!< Only 1 switch of many can be ON (e.g. radio buttons) */ ISR_ATMOST1, /*!< At most one switch can be ON, but all switches can be off. It is similar to ISR_1OFMANY with the exception that all switches can be off. */ ISR_NOFMANY /*!< Any number of switches can be ON (e.g. check boxes) */ } ISRule; /** * @typedef IPerm * @brief Permission hint, with respect to client. */ typedef enum { IP_RO, /*!< Read Only */ IP_WO, /*!< Write Only */ IP_RW /*!< Read & Write */ } IPerm; // The XML strings for these attributes may be any length but implementations // are only obligued to support these lengths for the various string attributes. #define MAXINDINAME 64 #define MAXINDILABEL 64 #define MAXINDIDEVICE 64 #define MAXINDIGROUP 64 #define MAXINDIFORMAT 64 #define MAXINDIBLOBFMT 64 #define MAXINDITSTAMP 64 #define MAXINDIMESSAGE 255 /******************************************************************************* * Typedefs for each INDI Property type. * * INumber.format may be any printf-style appropriate for double * or style "m" to create sexigesimal using the form "%.m" where * is the total field width. * is the width of the fraction. valid values are: * 9 -> :mm:ss.ss * 8 -> :mm:ss.s * 6 -> :mm:ss * 5 -> :mm.m * 3 -> :mm * * examples: * * to produce use * * "-123:45" %7.3m * " 0:01:02" %9.6m */ /** * @struct IText * @brief One text descriptor. */ typedef struct { /** Index name */ char name[MAXINDINAME]; /** Short description */ char label[MAXINDILABEL]; /** Allocated text string */ char *text; /** Pointer to parent */ struct _ITextVectorProperty *tvp; /** Helper info */ void *aux0; /** Helper info */ void *aux1; } IText; /** * @struct _ITextVectorProperty * @brief Text vector property descriptor. */ typedef struct _ITextVectorProperty { /** Device name */ char device[MAXINDIDEVICE]; /** Property name */ char name[MAXINDINAME]; /** Short description */ char label[MAXINDILABEL]; /** GUI grouping hint */ char group[MAXINDIGROUP]; /** Client accessibility hint */ IPerm p; /** Current max time to change, secs */ double timeout; /** Current property state */ IPState s; /** Texts comprising this vector */ IText *tp; /** Dimension of tp[] */ int ntp; /** ISO 8601 timestamp of this event */ char timestamp[MAXINDITSTAMP]; /** Helper info */ void *aux; } ITextVectorProperty; /** * @struct INumber * @brief One number descriptor. */ typedef struct { /** Index name */ char name[MAXINDINAME]; /** Short description */ char label[MAXINDILABEL]; /** GUI display format, see above */ char format[MAXINDIFORMAT]; /** Range min, ignored if min == max */ double min; /** Range max, ignored if min == max */ double max; /** Step size, ignored if step == 0 */ double step; /** Current value */ double value; /** Pointer to parent */ struct _INumberVectorProperty *nvp; /** Helper info */ void *aux0; /** Helper info */ void *aux1; } INumber; /** * @struct _INumberVectorProperty * @brief Number vector property descriptor. * * INumber.format may be any printf-style appropriate for double or style * "m" to create sexigesimal using the form "%\.\m" where:\n * \ is the total field width.\n * \ is the width of the fraction. valid values are:\n * 9 -> \:mm:ss.ss \n * 8 -> \:mm:ss.s \n * 6 -> \:mm:ss \n * 5 -> \:mm.m \n * 3 -> \:mm \n * * examples:\n * * To produce "-123:45", use \%7.3m \n * To produce " 0:01:02", use \%9.6m */ typedef struct _INumberVectorProperty { /** Device name */ char device[MAXINDIDEVICE]; /** Property name */ char name[MAXINDINAME]; /** Short description */ char label[MAXINDILABEL]; /** GUI grouping hint */ char group[MAXINDIGROUP]; /** Client accessibility hint */ IPerm p; /** Current max time to change, secs */ double timeout; /** current property state */ IPState s; /** Numbers comprising this vector */ INumber *np; /** Dimension of np[] */ int nnp; /** ISO 8601 timestamp of this event */ char timestamp[MAXINDITSTAMP]; /** Helper info */ void *aux; } INumberVectorProperty; /** * @struct ISwitch * @brief One switch descriptor. */ typedef struct { /** Index name */ char name[MAXINDINAME]; /** Switch label */ char label[MAXINDILABEL]; /** Switch state */ ISState s; /** Pointer to parent */ struct _ISwitchVectorProperty *svp; /** Helper info */ void *aux; } ISwitch; /** * @struct _ISwitchVectorProperty * @brief Switch vector property descriptor. */ typedef struct _ISwitchVectorProperty { /** Device name */ char device[MAXINDIDEVICE]; /** Property name */ char name[MAXINDINAME]; /** Short description */ char label[MAXINDILABEL]; /** GUI grouping hint */ char group[MAXINDIGROUP]; /** Client accessibility hint */ IPerm p; /** Switch behavior hint */ ISRule r; /** Current max time to change, secs */ double timeout; /** Current property state */ IPState s; /** Switches comprising this vector */ ISwitch *sp; /** Dimension of sp[] */ int nsp; /** ISO 8601 timestamp of this event */ char timestamp[MAXINDITSTAMP]; /** Helper info */ void *aux; } ISwitchVectorProperty; /** * @struct ILight * @brief One light descriptor. */ typedef struct { /** Index name */ char name[MAXINDINAME]; /** Light labels */ char label[MAXINDILABEL]; /** Light state */ IPState s; /** Pointer to parent */ struct _ILightVectorProperty *lvp; /** Helper info */ void *aux; } ILight; /** * @struct _ILightVectorProperty * @brief Light vector property descriptor. */ typedef struct _ILightVectorProperty { /** Device name */ char device[MAXINDIDEVICE]; /** Property name */ char name[MAXINDINAME]; /** Short description */ char label[MAXINDILABEL]; /** GUI grouping hint */ char group[MAXINDIGROUP]; /** Current property state */ IPState s; /** Lights comprising this vector */ ILight *lp; /** Dimension of lp[] */ int nlp; /** ISO 8601 timestamp of this event */ char timestamp[MAXINDITSTAMP]; /** Helper info */ void *aux; } ILightVectorProperty; /** * @struct IBLOB * @brief One Blob (Binary Large Object) descriptor. */ typedef struct /* one BLOB descriptor */ { /** Index name */ char name[MAXINDINAME]; /** Blob label */ char label[MAXINDILABEL]; /** Format attr */ char format[MAXINDIBLOBFMT]; /** Allocated binary large object bytes */ void *blob; /** Blob size in bytes */ int bloblen; /** N uncompressed bytes */ int size; /** Pointer to parent */ struct _IBLOBVectorProperty *bvp; /** Helper info */ void *aux0; /** Helper info */ void *aux1; /** Helper info */ void *aux2; } IBLOB; /** * @struct _IBLOBVectorProperty * @brief BLOB (Binary Large Object) vector property descriptor. */ typedef struct _IBLOBVectorProperty /* BLOB vector property descriptor */ { /** Device name */ char device[MAXINDIDEVICE]; /** Property name */ char name[MAXINDINAME]; /** Short description */ char label[MAXINDILABEL]; /** GUI grouping hint */ char group[MAXINDIGROUP]; /** Client accessibility hint */ IPerm p; /** Current max time to change, secs */ double timeout; /** Current property state */ IPState s; /** BLOBs comprising this vector */ IBLOB *bp; /** Dimension of bp[] */ int nbp; /** ISO 8601 timestamp of this event */ char timestamp[MAXINDITSTAMP]; /** Helper info */ void *aux; } IBLOBVectorProperty; /** * @brief Handy macro to find the number of elements in array a[]. Must be used * with actual array, not pointer. */ #define NARRAY(a) (sizeof(a) / sizeof(a[0])) libindi/indidriver.c0000664000175000017500000017103113263645557014000 0ustar jasemjasem#if 0 INDI Driver Functions Copyright (C) 2003-2015 Jasem Mutlaq Copyright (C) 2003-2006 Elwood C. Downey 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 "indidriver.h" #include "base64.h" #include "eventloop.h" #include "indicom.h" #include "indidevapi.h" #include "locale_compat.h" #include #include #include #include #include #include #include #include #include pthread_mutex_t stdout_mutex = PTHREAD_MUTEX_INITIALIZER; #define MAXRBUF 2048 /*! INDI property type */ enum { INDI_NUMBER, INDI_SWITCH, INDI_TEXT, INDI_LIGHT, INDI_BLOB, INDI_UNKNOWN }; /* Return index of property property if already cached, -1 otherwise */ int isPropDefined(const char *property_name, const char *device_name) { int i = 0; for (i = 0; i < nPropCache; i++) if (!strcmp(property_name, propCache[i].propName) && !strcmp(device_name, propCache[i].devName)) return i; return -1; } /* output a string expanding special characters into xml/html escape sequences */ /* N.B. You must free the returned buffer after use! */ char *escapeXML(const char *s, unsigned int MAX_BUF_SIZE) { char *buf = malloc(sizeof(char) * MAX_BUF_SIZE); char *out = buf; unsigned int i = 0; for (i = 0; i <= strlen(s); i++) { switch (s[i]) { case '&': strncpy(out, "&", 5); out += 5; break; case '\'': strncpy(out, "'", 6); out += 6; break; case '"': strncpy(out, """, 6); out += 6; break; case '<': strncpy(out, "<", 4); out += 4; break; case '>': strncpy(out, ">", 4); out += 4; break; default: strncpy(out++, s + i, 1); break; } } return buf; } /* tell Client to delete the property with given name on given device, or * entire device if !name */ void IDDelete(const char *dev, const char *name, const char *fmt, ...) { pthread_mutex_lock(&stdout_mutex); xmlv1(); printf("\n"); fflush(stdout); pthread_mutex_unlock(&stdout_mutex); } /* tell indiserver we want to snoop on the given device/property. * name ignored if NULL or empty. */ void IDSnoopDevice(const char *snooped_device, const char *snooped_property) { pthread_mutex_lock(&stdout_mutex); xmlv1(); if (snooped_property && snooped_property[0]) printf("\n", INDIV, snooped_device, snooped_property); else printf("\n", INDIV, snooped_device); fflush(stdout); pthread_mutex_unlock(&stdout_mutex); } /* tell indiserver whether we want BLOBs from the given snooped device. * silently ignored if given device is not already registered for snooping. */ void IDSnoopBLOBs(const char *snooped_device, const char *snooped_property, BLOBHandling bh) { const char *how; switch (bh) { case B_NEVER: how = "Never"; break; case B_ALSO: how = "Also"; break; case B_ONLY: how = "Only"; break; default: return; } pthread_mutex_lock(&stdout_mutex); xmlv1(); if (snooped_property && snooped_property[0]) printf("%s\n", snooped_device, snooped_property, how); else printf("%s\n", snooped_device, how); fflush(stdout); pthread_mutex_unlock(&stdout_mutex); } /* "INDI" wrappers to the more generic eventloop facility. */ int IEAddCallback(int readfiledes, IE_CBF *fp, void *p) { return (addCallback(readfiledes, (CBF *)fp, p)); } void IERmCallback(int callbackid) { rmCallback(callbackid); } int IEAddTimer(int millisecs, IE_TCF *fp, void *p) { return (addTimer(millisecs, (TCF *)fp, p)); } void IERmTimer(int timerid) { rmTimer(timerid); } int IEAddWorkProc(IE_WPF *fp, void *p) { return (addWorkProc((WPF *)fp, p)); } void IERmWorkProc(int workprocid) { rmWorkProc(workprocid); } int IEDeferLoop(int maxms, int *flagp) { return (deferLoop(maxms, flagp)); } int IEDeferLoop0(int maxms, int *flagp) { return (deferLoop0(maxms, flagp)); } /* Update property switches in accord with states and names. */ int IUUpdateSwitch(ISwitchVectorProperty *svp, ISState *states, char *names[], int n) { int i = 0; ISwitch *sp; char sn[MAXINDINAME]; /* store On switch name */ if (svp->r == ISR_1OFMANY) { sp = IUFindOnSwitch(svp); if (sp) strncpy(sn, sp->name, MAXINDINAME); IUResetSwitch(svp); } for (i = 0; i < n; i++) { sp = IUFindSwitch(svp, names[i]); if (!sp) { svp->s = IPS_IDLE; IDSetSwitch(svp, "Error: %s is not a member of %s (%s) property.", names[i], svp->label, svp->name); return -1; } sp->s = states[i]; } /* Consistency checks for ISR_1OFMANY after update. */ if (svp->r == ISR_1OFMANY) { int t_count = 0; for (i = 0; i < svp->nsp; i++) { if (svp->sp[i].s == ISS_ON) t_count++; } if (t_count != 1) { IUResetSwitch(svp); sp = IUFindSwitch(svp, sn); if (sp) sp->s = ISS_ON; svp->s = IPS_IDLE; IDSetSwitch(svp, "Error: invalid state switch for property %s (%s). %s.", svp->label, svp->name, t_count == 0 ? "No switch is on" : "Too many switches are on"); return -1; } } return 0; } /* Update property numbers in accord with values and names */ int IUUpdateNumber(INumberVectorProperty *nvp, double values[], char *names[], int n) { int i = 0; INumber *np; for (i = 0; i < n; i++) { np = IUFindNumber(nvp, names[i]); if (!np) { nvp->s = IPS_IDLE; IDSetNumber(nvp, "Error: %s is not a member of %s (%s) property.", names[i], nvp->label, nvp->name); return -1; } if (values[i] < np->min || values[i] > np->max) { nvp->s = IPS_ALERT; IDSetNumber(nvp, "Error: Invalid range for %s (%s). Valid range is from %g to %g. Requested value is %g", np->label, np->name, np->min, np->max, values[i]); return -1; } } /* First loop checks for error, second loop set all values atomically*/ for (i = 0; i < n; i++) { np = IUFindNumber(nvp, names[i]); np->value = values[i]; } return 0; } /* Update property text in accord with texts and names */ int IUUpdateText(ITextVectorProperty *tvp, char *texts[], char *names[], int n) { int i = 0; IText *tp; for (i = 0; i < n; i++) { tp = IUFindText(tvp, names[i]); if (!tp) { tvp->s = IPS_IDLE; IDSetText(tvp, "Error: %s is not a member of %s (%s) property.", names[i], tvp->label, tvp->name); return -1; } } /* First loop checks for error, second loop set all values atomically*/ for (i = 0; i < n; i++) { tp = IUFindText(tvp, names[i]); IUSaveText(tp, texts[i]); } return 0; } /* Update property BLOB in accord with BLOBs and names */ int IUUpdateBLOB(IBLOBVectorProperty *bvp, int sizes[], int blobsizes[], char *blobs[], char *formats[], char *names[], int n) { int i = 0; IBLOB *bp; for (i = 0; i < n; i++) { bp = IUFindBLOB(bvp, names[i]); if (!bp) { bvp->s = IPS_IDLE; IDSetBLOB(bvp, "Error: %s is not a member of %s (%s) property.", names[i], bvp->label, bvp->name); return -1; } } /* First loop checks for error, second loop set all values atomically*/ for (i = 0; i < n; i++) { bp = IUFindBLOB(bvp, names[i]); IUSaveBLOB(bp, sizes[i], blobsizes[i], blobs[i], formats[i]); } return 0; } int IUSaveBLOB(IBLOB *bp, int size, int blobsize, char *blob, char *format) { bp->bloblen = blobsize; bp->size = size; bp->blob = blob; strncpy(bp->format, format, MAXINDIFORMAT); return 0; } void IUFillSwitch(ISwitch *sp, const char *name, const char *label, ISState s) { char *escapedName = escapeXML(name, MAXINDINAME); char *escapedLabel = escapeXML(label, MAXINDILABEL); strncpy(sp->name, escapedName, MAXINDINAME); if (label[0]) strncpy(sp->label, escapedLabel, MAXINDILABEL); else strncpy(sp->label, escapedName, MAXINDILABEL); sp->s = s; sp->svp = NULL; sp->aux = NULL; free(escapedName); free(escapedLabel); } void IUFillLight(ILight *lp, const char *name, const char *label, IPState s) { char *escapedName = escapeXML(name, MAXINDINAME); char *escapedLabel = escapeXML(label, MAXINDILABEL); strncpy(lp->name, escapedName, MAXINDINAME); if (label[0]) strncpy(lp->label, escapedLabel, MAXINDILABEL); else strncpy(lp->label, escapedName, MAXINDILABEL); lp->s = s; lp->lvp = NULL; lp->aux = NULL; free(escapedName); free(escapedLabel); } void IUFillNumber(INumber *np, const char *name, const char *label, const char *format, double min, double max, double step, double value) { char *escapedName = escapeXML(name, MAXINDINAME); char *escapedLabel = escapeXML(label, MAXINDILABEL); strncpy(np->name, escapedName, MAXINDINAME); if (label[0]) strncpy(np->label, escapedLabel, MAXINDILABEL); else strncpy(np->label, escapedName, MAXINDILABEL); strncpy(np->format, format, MAXINDIFORMAT); np->min = min; np->max = max; np->step = step; np->value = value; np->nvp = NULL; np->aux0 = NULL; np->aux1 = NULL; free(escapedName); free(escapedLabel); } void IUFillText(IText *tp, const char *name, const char *label, const char *initialText) { char *escapedName = escapeXML(name, MAXINDINAME); char *escapedLabel = escapeXML(label, MAXINDILABEL); strncpy(tp->name, escapedName, MAXINDINAME); if (label[0]) strncpy(tp->label, escapedLabel, MAXINDILABEL); else strncpy(tp->label, escapedName, MAXINDILABEL); if (tp->text && tp->text[0]) free(tp->text); tp->text = NULL; tp->tvp = NULL; tp->aux0 = NULL; tp->aux1 = NULL; if (initialText && strlen(initialText) > 0) IUSaveText(tp, initialText); free(escapedName); free(escapedLabel); } void IUFillBLOB(IBLOB *bp, const char *name, const char *label, const char *format) { char *escapedName = escapeXML(name, MAXINDINAME); char *escapedLabel = escapeXML(label, MAXINDILABEL); memset(bp, 0, sizeof(IBLOB)); strncpy(bp->name, escapedName, MAXINDINAME); if (label[0]) strncpy(bp->label, escapedLabel, MAXINDILABEL); else strncpy(bp->label, escapedName, MAXINDILABEL); strncpy(bp->format, format, MAXINDIBLOBFMT); bp->blob = 0; bp->bloblen = 0; bp->size = 0; bp->bvp = 0; bp->aux0 = 0; bp->aux1 = 0; bp->aux2 = 0; free(escapedName); free(escapedLabel); } void IUFillSwitchVector(ISwitchVectorProperty *svp, ISwitch *sp, int nsp, const char *dev, const char *name, const char *label, const char *group, IPerm p, ISRule r, double timeout, IPState s) { char *escapedName = escapeXML(name, MAXINDINAME); char *escapedLabel = escapeXML(label, MAXINDILABEL); strncpy(svp->device, dev, MAXINDIDEVICE); strncpy(svp->name, escapedName, MAXINDINAME); if (label[0]) strncpy(svp->label, escapedLabel, MAXINDILABEL); else strncpy(svp->label, escapedName, MAXINDILABEL); strncpy(svp->group, group, MAXINDIGROUP); strcpy(svp->timestamp, ""); svp->p = p; svp->r = r; svp->timeout = timeout; svp->s = s; svp->sp = sp; svp->nsp = nsp; free(escapedName); free(escapedLabel); } void IUFillLightVector(ILightVectorProperty *lvp, ILight *lp, int nlp, const char *dev, const char *name, const char *label, const char *group, IPState s) { char *escapedName = escapeXML(name, MAXINDINAME); char *escapedLabel = escapeXML(label, MAXINDILABEL); strncpy(lvp->device, dev, MAXINDIDEVICE); strncpy(lvp->name, escapedName, MAXINDINAME); if (label[0]) strncpy(lvp->label, escapedLabel, MAXINDILABEL); else strncpy(lvp->label, escapedName, MAXINDILABEL); strncpy(lvp->group, group, MAXINDIGROUP); strcpy(lvp->timestamp, ""); lvp->s = s; lvp->lp = lp; lvp->nlp = nlp; free(escapedName); free(escapedLabel); } void IUFillNumberVector(INumberVectorProperty *nvp, INumber *np, int nnp, const char *dev, const char *name, const char *label, const char *group, IPerm p, double timeout, IPState s) { char *escapedName = escapeXML(name, MAXINDINAME); char *escapedLabel = escapeXML(label, MAXINDILABEL); strncpy(nvp->device, dev, MAXINDIDEVICE); strncpy(nvp->name, escapedName, MAXINDINAME); if (label[0]) strncpy(nvp->label, escapedLabel, MAXINDILABEL); else strncpy(nvp->label, escapedName, MAXINDILABEL); strncpy(nvp->group, group, MAXINDIGROUP); strcpy(nvp->timestamp, ""); nvp->p = p; nvp->timeout = timeout; nvp->s = s; nvp->np = np; nvp->nnp = nnp; free(escapedName); free(escapedLabel); } void IUFillTextVector(ITextVectorProperty *tvp, IText *tp, int ntp, const char *dev, const char *name, const char *label, const char *group, IPerm p, double timeout, IPState s) { char *escapedName = escapeXML(name, MAXINDINAME); char *escapedLabel = escapeXML(label, MAXINDILABEL); strncpy(tvp->device, dev, MAXINDIDEVICE); strncpy(tvp->name, escapedName, MAXINDINAME); if (label[0]) strncpy(tvp->label, escapedLabel, MAXINDILABEL); else strncpy(tvp->label, escapedName, MAXINDILABEL); strncpy(tvp->group, group, MAXINDIGROUP); strcpy(tvp->timestamp, ""); tvp->p = p; tvp->timeout = timeout; tvp->s = s; tvp->tp = tp; tvp->ntp = ntp; free(escapedName); free(escapedLabel); } void IUFillBLOBVector(IBLOBVectorProperty *bvp, IBLOB *bp, int nbp, const char *dev, const char *name, const char *label, const char *group, IPerm p, double timeout, IPState s) { char *escapedName = escapeXML(name, MAXINDINAME); char *escapedLabel = escapeXML(label, MAXINDILABEL); memset(bvp, 0, sizeof(IBLOBVectorProperty)); strncpy(bvp->device, dev, MAXINDIDEVICE); strncpy(bvp->name, escapedName, MAXINDINAME); if (label[0]) strncpy(bvp->label, escapedLabel, MAXINDILABEL); else strncpy(bvp->label, escapedName, MAXINDILABEL); strncpy(bvp->group, group, MAXINDIGROUP); strcpy(bvp->timestamp, ""); bvp->p = p; bvp->timeout = timeout; bvp->s = s; bvp->bp = bp; bvp->nbp = nbp; free(escapedName); free(escapedLabel); } /***************************************************************************** * convenience functions for use in your implementation of ISSnoopDevice(). */ /* crack the snooped driver setNumberVector or defNumberVector message into * the given INumberVectorProperty. * return 0 if type, device and name match and all members are present, else * return -1 */ int IUSnoopNumber(XMLEle *root, INumberVectorProperty *nvp) { char *dev, *name; XMLEle *ep; int i; /* check and crack type, device, name and state */ if (strcmp(tagXMLEle(root) + 3, "NumberVector") || crackDN(root, &dev, &name, NULL) < 0) return (-1); if (strcmp(dev, nvp->device) || strcmp(name, nvp->name)) return (-1); /* not this property */ (void)crackIPState(findXMLAttValu(root, "state"), &nvp->s); /* match each INumber with a oneNumber */ locale_char_t *orig = indi_locale_C_numeric_push(); for (i = 0; i < nvp->nnp; i++) { for (ep = nextXMLEle(root, 1); ep; ep = nextXMLEle(root, 0)) { if (!strcmp(tagXMLEle(ep) + 3, "Number") && !strcmp(nvp->np[i].name, findXMLAttValu(ep, "name"))) { if (f_scansexa(pcdataXMLEle(ep), &nvp->np[i].value) < 0) { indi_locale_C_numeric_pop(orig); return (-1); /* bad number format */ } break; } } if (!ep) { indi_locale_C_numeric_pop(orig); return (-1); /* element not found */ } } indi_locale_C_numeric_pop(orig); /* ok */ return (0); } /* crack the snooped driver setTextVector or defTextVector message into * the given ITextVectorProperty. * return 0 if type, device and name match and all members are present, else * return -1 */ int IUSnoopText(XMLEle *root, ITextVectorProperty *tvp) { char *dev, *name; XMLEle *ep; int i; /* check and crack type, device, name and state */ if (strcmp(tagXMLEle(root) + 3, "TextVector") || crackDN(root, &dev, &name, NULL) < 0) return (-1); if (strcmp(dev, tvp->device) || strcmp(name, tvp->name)) return (-1); /* not this property */ (void)crackIPState(findXMLAttValu(root, "state"), &tvp->s); /* match each IText with a oneText */ for (i = 0; i < tvp->ntp; i++) { for (ep = nextXMLEle(root, 1); ep; ep = nextXMLEle(root, 0)) { if (!strcmp(tagXMLEle(ep) + 3, "Text") && !strcmp(tvp->tp[i].name, findXMLAttValu(ep, "name"))) { IUSaveText(&tvp->tp[i], pcdataXMLEle(ep)); break; } } if (!ep) return (-1); /* element not found */ } /* ok */ return (0); } /* crack the snooped driver setLightVector or defLightVector message into * the given ILightVectorProperty. it is not necessary that all ILight names * be found. * return 0 if type, device and name match, else return -1. */ int IUSnoopLight(XMLEle *root, ILightVectorProperty *lvp) { char *dev, *name; XMLEle *ep; int i; /* check and crack type, device, name and state */ if (strcmp(tagXMLEle(root) + 3, "LightVector") || crackDN(root, &dev, &name, NULL) < 0) return (-1); if (strcmp(dev, lvp->device) || strcmp(name, lvp->name)) return (-1); /* not this property */ (void)crackIPState(findXMLAttValu(root, "state"), &lvp->s); /* match each oneLight with one ILight */ for (ep = nextXMLEle(root, 1); ep; ep = nextXMLEle(root, 0)) { if (!strcmp(tagXMLEle(ep) + 3, "Light")) { const char *name = findXMLAttValu(ep, "name"); for (i = 0; i < lvp->nlp; i++) { if (!strcmp(lvp->lp[i].name, name)) { if (crackIPState(pcdataXMLEle(ep), &lvp->lp[i].s) < 0) { return (-1); /* unrecognized state */ } break; } } } } /* ok */ return (0); } /* crack the snooped driver setSwitchVector or defSwitchVector message into the * given ISwitchVectorProperty. it is not necessary that all ISwitch names be * found. * return 0 if type, device and name match, else return -1. */ int IUSnoopSwitch(XMLEle *root, ISwitchVectorProperty *svp) { char *dev, *name; XMLEle *ep; int i; /* check and crack type, device, name and state */ if (strcmp(tagXMLEle(root) + 3, "SwitchVector") || crackDN(root, &dev, &name, NULL) < 0) return (-1); if (strcmp(dev, svp->device) || strcmp(name, svp->name)) return (-1); /* not this property */ (void)crackIPState(findXMLAttValu(root, "state"), &svp->s); /* match each oneSwitch with one ISwitch */ for (ep = nextXMLEle(root, 1); ep; ep = nextXMLEle(root, 0)) { if (!strcmp(tagXMLEle(ep) + 3, "Switch")) { const char *name = findXMLAttValu(ep, "name"); for (i = 0; i < svp->nsp; i++) { if (!strcmp(svp->sp[i].name, name)) { if (crackISState(pcdataXMLEle(ep), &svp->sp[i].s) < 0) { return (-1); /* unrecognized state */ } break; } } } } /* ok */ return (0); } /* crack the snooped driver setBLOBVector message into the given * IBLOBVectorProperty. it is not necessary that all IBLOB names be found. * return 0 if type, device and name match, else return -1. * N.B. we assume any existing blob in bvp has been malloced, which we free * and replace with a newly malloced blob if found. */ int IUSnoopBLOB(XMLEle *root, IBLOBVectorProperty *bvp) { char *dev, *name; XMLEle *ep; /* check and crack type, device, name and state */ if (strcmp(tagXMLEle(root), "setBLOBVector") || crackDN(root, &dev, &name, NULL) < 0) return (-1); if (strcmp(dev, bvp->device) || strcmp(name, bvp->name)) return (-1); /* not this property */ crackIPState(findXMLAttValu(root, "state"), &bvp->s); for (ep = nextXMLEle(root, 1); ep; ep = nextXMLEle(root, 0)) { if (strcmp(tagXMLEle(ep), "oneBLOB") == 0) { XMLAtt *na = findXMLAtt(ep, "name"); if (na == NULL) return (-1); IBLOB *bp = IUFindBLOB(bvp, valuXMLAtt(na)); if (bp == NULL) return (-1); XMLAtt *fa = findXMLAtt(ep, "format"); XMLAtt *sa = findXMLAtt(ep, "size"); XMLAtt *ec = findXMLAtt(ep, "enclen"); if (fa && sa && ec) { int enclen = atoi(valuXMLAtt(ec)); bp->blob = realloc(bp->blob, 3 * enclen / 4); bp->bloblen = from64tobits_fast(bp->blob, pcdataXMLEle(ep), enclen); strncpy(bp->format, valuXMLAtt(fa), MAXINDIFORMAT); bp->size = atoi(valuXMLAtt(sa)); } } } /* ok */ return (0); } /* callback when INDI client message arrives on stdin. * collect and dispatch when see outter element closure. * exit if OS trouble or see incompatable INDI version. * arg is not used. */ void clientMsgCB(int fd, void *arg) { (void)arg; char buf[MAXRBUF], msg[MAXRBUF], *bp; int nr; /* one read */ nr = read(fd, buf, sizeof(buf)); if (nr < 0) { fprintf(stderr, "%s: %s\n", me, strerror(errno)); exit(1); } if (nr == 0) { fprintf(stderr, "%s: EOF\n", me); exit(1); } /* crack and dispatch when complete */ for (bp = buf; nr-- > 0; bp++) { XMLEle *root = readXMLEle(clixml, *bp, msg); if (root) { if (dispatch(root, msg) < 0) fprintf(stderr, "%s dispatch error: %s\n", me, msg); delXMLEle(root); } else if (msg[0]) fprintf(stderr, "%s XML error: %s\n", me, msg); } } /* crack the given INDI XML element and call driver's IS* entry points as they * are recognized. * return 0 if ok else -1 with reason in msg[]. * N.B. exit if getProperties does not proclaim a compatible version. */ int dispatch(XMLEle *root, char msg[]) { char *rtag = tagXMLEle(root); XMLEle *ep; int n, i = 0; if (verbose) prXMLEle(stderr, root, 0); if (!strcmp(rtag, "getProperties")) { XMLAtt *ap, *name, *dev; double v; /* check version */ ap = findXMLAtt(root, "version"); if (!ap) { fprintf(stderr, "%s: getProperties missing version\n", me); exit(1); } v = atof(valuXMLAtt(ap)); if (v > INDIV) { fprintf(stderr, "%s: client version %g > %g\n", me, v, INDIV); exit(1); } // Get device dev = findXMLAtt(root, "device"); // Get property name name = findXMLAtt(root, "name"); if (name && dev) { int index = isPropDefined(valuXMLAtt(name), valuXMLAtt(dev)); if (index < 0) return 0; ROSC *prop = propCache + index; switch (prop->type) { case INDI_NUMBER: IDSetNumber((INumberVectorProperty *)(prop->ptr), NULL); return 0; break; case INDI_SWITCH: IDSetSwitch((ISwitchVectorProperty *)(prop->ptr), NULL); return 0; break; case INDI_TEXT: IDSetText((ITextVectorProperty *)(prop->ptr), NULL); return 0; break; case INDI_BLOB: IDSetBLOB((IBLOBVectorProperty *)(prop->ptr), NULL); return 0; break; default: return 0; } } ISGetProperties(dev ? valuXMLAtt(dev) : NULL); return (0); } /* other commands might be from a snooped device. * we don't know here which devices are being snooped so we send * all remaining valid messages */ if (!strcmp(rtag, "setNumberVector") || !strcmp(rtag, "setTextVector") || !strcmp(rtag, "setLightVector") || !strcmp(rtag, "setSwitchVector") || !strcmp(rtag, "setBLOBVector") || !strcmp(rtag, "defNumberVector") || !strcmp(rtag, "defTextVector") || !strcmp(rtag, "defLightVector") || !strcmp(rtag, "defSwitchVector") || !strcmp(rtag, "defBLOBVector") || !strcmp(rtag, "message") || !strcmp(rtag, "delProperty")) { ISSnoopDevice(root); return (0); } char *dev, *name; /* pull out device and name */ if (crackDN(root, &dev, &name, msg) < 0) return (-1); if (isPropDefined(name, dev) < 0) { snprintf(msg, MAXRBUF, "Property %s is not defined in %s.", name, dev); return -1; } /* ensure property is not RO */ for (i = 0; i < nPropCache; i++) { if (!strcmp(propCache[i].propName, name) && !strcmp(propCache[i].devName, dev)) { if (propCache[i].perm == IP_RO) { snprintf(msg, MAXRBUF, "Cannot set read-only property %s", name); return -1; } else break; } } /* check tag in surmised decreasing order of likelyhood */ if (!strcmp(rtag, "newNumberVector")) { static double *doubles; static char **names; static int maxn; /* seed for reallocs */ if (!doubles) { doubles = (double *)malloc(1); names = (char **)malloc(1); } // Set locale to C and save previous value locale_char_t *orig = indi_locale_C_numeric_push(); /* pull out each name/value pair */ for (n = 0, ep = nextXMLEle(root, 1); ep; ep = nextXMLEle(root, 0)) { if (strcmp(tagXMLEle(ep), "oneNumber") == 0) { XMLAtt *na = findXMLAtt(ep, "name"); if (na) { if (n >= maxn) { /* grow for this and another */ int newsz = (maxn = n + 1) * sizeof(double); doubles = (double *)realloc(doubles, newsz); newsz = maxn * sizeof(char *); names = (char **)realloc(names, newsz); } if (f_scansexa(pcdataXMLEle(ep), &doubles[n]) < 0) IDMessage(dev, "[ERROR] %s: Bad format %s", name, pcdataXMLEle(ep)); else names[n++] = valuXMLAtt(na); } } } // Reset locale settings to original value indi_locale_C_numeric_pop(orig); /* invoke driver if something to do, but not an error if not */ if (n > 0) ISNewNumber(dev, name, doubles, names, n); else IDMessage(dev, "[ERROR] %s: newNumberVector with no valid members", name); return (0); } if (!strcmp(rtag, "newSwitchVector")) { static ISState *states; static char **names; static int maxn; XMLEle *ep; /* seed for reallocs */ if (!states) { states = (ISState *)malloc(1); names = (char **)malloc(1); } /* pull out each name/state pair */ for (n = 0, ep = nextXMLEle(root, 1); ep; ep = nextXMLEle(root, 0)) { if (strcmp(tagXMLEle(ep), "oneSwitch") == 0) { XMLAtt *na = findXMLAtt(ep, "name"); if (na) { if (n >= maxn) { int newsz = (maxn = n + 1) * sizeof(ISState); states = (ISState *)realloc(states, newsz); newsz = maxn * sizeof(char *); names = (char **)realloc(names, newsz); } if (strncmp(pcdataXMLEle(ep), "On", 2) == 0) { states[n] = ISS_ON; names[n] = valuXMLAtt(na); n++; } else if (strcmp(pcdataXMLEle(ep), "Off") == 0) { states[n] = ISS_OFF; names[n] = valuXMLAtt(na); n++; } else IDMessage(dev, "[ERROR] %s: must be On or Off: %s", name, pcdataXMLEle(ep)); } } } /* invoke driver if something to do, but not an error if not */ if (n > 0) ISNewSwitch(dev, name, states, names, n); else IDMessage(dev, "[ERROR] %s: newSwitchVector with no valid members", name); return (0); } if (!strcmp(rtag, "newTextVector")) { static char **texts; static char **names; static int maxn; /* seed for reallocs */ if (!texts) { texts = (char **)malloc(1); names = (char **)malloc(1); } /* pull out each name/text pair */ for (n = 0, ep = nextXMLEle(root, 1); ep; ep = nextXMLEle(root, 0)) { if (strcmp(tagXMLEle(ep), "oneText") == 0) { XMLAtt *na = findXMLAtt(ep, "name"); if (na) { if (n >= maxn) { int newsz = (maxn = n + 1) * sizeof(char *); texts = (char **)realloc(texts, newsz); names = (char **)realloc(names, newsz); } texts[n] = pcdataXMLEle(ep); names[n] = valuXMLAtt(na); n++; } } } /* invoke driver if something to do, but not an error if not */ if (n > 0) ISNewText(dev, name, texts, names, n); else IDMessage(dev, "[ERROR] %s: set with no valid members", name); return (0); } if (!strcmp(rtag, "newBLOBVector")) { static char **blobs; static char **names; static char **formats; static int *blobsizes; static int *sizes; static int maxn; int i; /* seed for reallocs */ if (!blobs) { blobs = (char **)malloc(1); names = (char **)malloc(1); formats = (char **)malloc(1); blobsizes = (int *)malloc(1); sizes = (int *)malloc(1); } /* pull out each name/BLOB pair, decode */ for (n = 0, ep = nextXMLEle(root, 1); ep; ep = nextXMLEle(root, 0)) { if (strcmp(tagXMLEle(ep), "oneBLOB") == 0) { XMLAtt *na = findXMLAtt(ep, "name"); XMLAtt *fa = findXMLAtt(ep, "format"); XMLAtt *sa = findXMLAtt(ep, "size"); XMLAtt *el = findXMLAtt(ep, "enclen"); if (na && fa && sa) { if (n >= maxn) { int newsz = (maxn = n + 1) * sizeof(char *); blobs = (char **)realloc(blobs, newsz); names = (char **)realloc(names, newsz); formats = (char **)realloc(formats, newsz); newsz = maxn * sizeof(int); sizes = (int *)realloc(sizes, newsz); blobsizes = (int *)realloc(blobsizes, newsz); } int bloblen = pcdatalenXMLEle(ep); // enclen is optional and not required by INDI protocol if (el) bloblen = atoi(valuXMLAtt(el)); blobs[n] = malloc(3 * bloblen / 4); blobsizes[n] = from64tobits_fast(blobs[n], pcdataXMLEle(ep), bloblen); names[n] = valuXMLAtt(na); formats[n] = valuXMLAtt(fa); sizes[n] = atoi(valuXMLAtt(sa)); n++; } } } /* invoke driver if something to do, but not an error if not */ if (n > 0) { ISNewBLOB(dev, name, sizes, blobsizes, blobs, formats, names, n); for (i = 0; i < n; i++) free(blobs[i]); } else IDMessage(dev, "[ERROR] %s: newBLOBVector with no valid members", name); return (0); } sprintf(msg, "Unknown command: %s", rtag); return (1); } int IUReadConfig(const char *filename, const char *dev, const char *property, int silent, char errmsg[]) { char *rname, *rdev; XMLEle *root = NULL, *fproot = NULL; LilXML *lp = newLilXML(); FILE *fp = IUGetConfigFP(filename, dev, "r", errmsg); if (fp == NULL) return -1; fproot = readXMLFile(fp, lp, errmsg); delLilXML(lp); if (fproot == NULL) { snprintf(errmsg, MAXRBUF, "Unable to parse config XML: %s", errmsg); fclose(fp); return -1; } if (nXMLEle(fproot) > 0 && silent != 1) IDMessage(dev, "[INFO] Loading device configuration..."); for (root = nextXMLEle(fproot, 1); root != NULL; root = nextXMLEle(fproot, 0)) { /* pull out device and name */ if (crackDN(root, &rdev, &rname, errmsg) < 0) { fclose(fp); delXMLEle(fproot); return -1; } // It doesn't belong to our device?? if (strcmp(dev, rdev)) continue; if ((property && !strcmp(property, rname)) || property == NULL) dispatch(root, errmsg); } if (nXMLEle(fproot) > 0 && silent != 1) IDMessage(dev, "[INFO] Device configuration applied."); fclose(fp); delXMLEle(fproot); return (0); } void IUSaveDefaultConfig(const char *source_config, const char *dest_config, const char *dev) { char configFileName[MAXRBUF], configDefaultFileName[MAXRBUF]; if (source_config) strncpy(configFileName, source_config, MAXRBUF); else { if (getenv("INDICONFIG")) strncpy(configFileName, getenv("INDICONFIG"), MAXRBUF); else snprintf(configFileName, MAXRBUF, "%s/.indi/%s_config.xml", getenv("HOME"), dev); } if (dest_config) strncpy(configDefaultFileName, dest_config, MAXRBUF); else if (getenv("INDICONFIG")) snprintf(configDefaultFileName, MAXRBUF, "%s.default", getenv("INDICONFIG")); else snprintf(configDefaultFileName, MAXRBUF, "%s/.indi/%s_config.xml.default", getenv("HOME"), dev); // If the default doesn't exist, create it. if (access(configDefaultFileName, F_OK)) { FILE *fpin = fopen(configFileName, "r"); if (fpin != NULL) { FILE *fpout = fopen(configDefaultFileName, "w"); if (fpout != NULL) { int ch = 0; while ((ch = getc(fpin)) != EOF) putc(ch, fpout); fclose(fpin); } fclose(fpout); } } } int IUGetConfigNumber(const char *dev, const char *property, const char *member, double *value) { char *rname, *rdev; XMLEle *root = NULL, *fproot = NULL; char errmsg[MAXRBUF]; LilXML *lp = newLilXML(); int valueFound=0; FILE *fp = IUGetConfigFP(NULL, dev, "r", errmsg); if (fp == NULL) return -1; fproot = readXMLFile(fp, lp, errmsg); if (fproot == NULL) { fclose(fp); return -1; } for (root = nextXMLEle(fproot, 1); root != NULL; root = nextXMLEle(fproot, 0)) { /* pull out device and name */ if (crackDN(root, &rdev, &rname, errmsg) < 0) { fclose(fp); delXMLEle(fproot); return -1; } // It doesn't belong to our device?? if (strcmp(dev, rdev)) continue; if ((property && !strcmp(property, rname)) || property == NULL) { XMLEle *oneNumber = NULL; for (oneNumber = nextXMLEle(root, 1); oneNumber != NULL; oneNumber = nextXMLEle(root, 0)) { if (!strcmp(member, findXMLAttValu(oneNumber, "name"))) { *value = atof(pcdataXMLEle(oneNumber)); valueFound = 1; break; } } break; } } fclose(fp); delXMLEle(fproot); delLilXML(lp); return (valueFound == 1 ? 0 : -1); } /* send client a message for a specific device or at large if !dev */ void IDMessage(const char *dev, const char *fmt, ...) { pthread_mutex_lock(&stdout_mutex); xmlv1(); printf("\n"); fflush(stdout); pthread_mutex_unlock(&stdout_mutex); } FILE *IUGetConfigFP(const char *filename, const char *dev, const char *mode, char errmsg[]) { char configFileName[MAXRBUF]; char configDir[MAXRBUF]; struct stat st; FILE *fp = NULL; snprintf(configDir, MAXRBUF, "%s/.indi/", getenv("HOME")); if (filename) strncpy(configFileName, filename, MAXRBUF); else { if (getenv("INDICONFIG")) strncpy(configFileName, getenv("INDICONFIG"), MAXRBUF); else snprintf(configFileName, MAXRBUF, "%s%s_config.xml", configDir, dev); } if (stat(configDir, &st) != 0) { if (mkdir(configDir, S_IRWXU | S_IRWXG | S_IROTH | S_IXOTH) < 0) { snprintf(errmsg, MAXRBUF, "Unable to create config directory. Error %s: %s\n", configDir, strerror(errno)); return NULL; } } fp = fopen(configFileName, mode); if (fp == NULL) { snprintf(errmsg, MAXRBUF, "Unable to open config file. Error loading file %s: %s\n", configFileName, strerror(errno)); return NULL; } return fp; } void IUSaveConfigTag(FILE *fp, int ctag, const char *dev, int silent) { if (!fp) return; /* Opening tag */ if (ctag == 0) { fprintf(fp, "\n"); if (silent != 1) IDMessage(dev, "[INFO] Saving device configuration..."); } /* Closing tag */ else { fprintf(fp, "\n"); if (silent != 1) IDMessage(dev, "[INFO] Device configuration saved."); } } void IUSaveConfigNumber(FILE *fp, const INumberVectorProperty *nvp) { int i; locale_char_t *orig = indi_locale_C_numeric_push(); fprintf(fp, "\n", nvp->device, nvp->name); for (i = 0; i < nvp->nnp; i++) { INumber *np = &nvp->np[i]; fprintf(fp, " \n", np->name); fprintf(fp, " %.20g\n", np->value); fprintf(fp, " \n"); } fprintf(fp, "\n"); indi_locale_C_numeric_pop(orig); } void IUSaveConfigText(FILE *fp, const ITextVectorProperty *tvp) { int i; fprintf(fp, "\n", tvp->device, tvp->name); for (i = 0; i < tvp->ntp; i++) { IText *tp = &tvp->tp[i]; fprintf(fp, " \n", tp->name); fprintf(fp, " %s\n", tp->text ? tp->text : ""); fprintf(fp, " \n"); } fprintf(fp, "\n"); } void IUSaveConfigSwitch(FILE *fp, const ISwitchVectorProperty *svp) { int i; fprintf(fp, "\n", svp->device, svp->name); for (i = 0; i < svp->nsp; i++) { ISwitch *sp = &svp->sp[i]; fprintf(fp, " \n", sp->name); fprintf(fp, " %s\n", sstateStr(sp->s)); fprintf(fp, " \n"); } fprintf(fp, "\n"); } void IUSaveConfigBLOB(FILE *fp, const IBLOBVectorProperty *bvp) { int i; fprintf(fp, "\n", bvp->device, bvp->name); for (i = 0; i < bvp->nbp; i++) { IBLOB *bp = &bvp->bp[i]; unsigned char *encblob = NULL; int l = 0; fprintf(fp, " name); fprintf(fp, " size='%d'\n", bp->size); fprintf(fp, " format='%s'>\n", bp->format); encblob = malloc(4 * bp->bloblen / 3 + 4); l = to64frombits(encblob, bp->blob, bp->bloblen); size_t written = 0; while ((int)written < l) { size_t towrite = ((l - written) > 72) ? 72 : l - written; size_t wr = fwrite(encblob + written, 1, towrite, fp); fputc('\n', fp); if (wr > 0) written += wr; } free(encblob); fprintf(fp, " \n"); } fprintf(fp, "\n"); } /* tell client to create a text vector property */ void IDDefText(const ITextVectorProperty *tvp, const char *fmt, ...) { int i; ROSC *SC; pthread_mutex_lock(&stdout_mutex); xmlv1(); locale_char_t *orig = indi_locale_C_numeric_push(); printf("device); printf(" name='%s'\n", tvp->name); printf(" label='%s'\n", tvp->label); printf(" group='%s'\n", tvp->group); printf(" state='%s'\n", pstateStr(tvp->s)); printf(" perm='%s'\n", permStr(tvp->p)); printf(" timeout='%g'\n", tvp->timeout); printf(" timestamp='%s'\n", timestamp()); if (fmt) { va_list ap; va_start(ap, fmt); char message[MAXINDIMESSAGE]; printf(" message='"); vsnprintf(message, MAXINDIMESSAGE, fmt, ap); char *escapedMessage = escapeXML(message, MAXINDIMESSAGE); printf("%s'\n", escapedMessage); free(escapedMessage); va_end(ap); } printf(">\n"); for (i = 0; i < tvp->ntp; i++) { IText *tp = &tvp->tp[i]; printf(" name); printf(" label='%s'>\n", tp->label); printf(" %s\n", tp->text ? tp->text : ""); printf(" \n"); } printf("\n"); if (isPropDefined(tvp->name, tvp->device) < 0) { /* Add this property to insure proper sanity check */ propCache = propCache ? (ROSC *)realloc(propCache, sizeof(ROSC) * (nPropCache + 1)) : (ROSC *)malloc(sizeof(ROSC)); SC = &propCache[nPropCache++]; strcpy(SC->propName, tvp->name); strcpy(SC->devName, tvp->device); SC->perm = tvp->p; SC->ptr = tvp; SC->type = INDI_TEXT; } indi_locale_C_numeric_pop(orig); fflush(stdout); pthread_mutex_unlock(&stdout_mutex); } /* tell client to create a new numeric vector property */ void IDDefNumber(const INumberVectorProperty *n, const char *fmt, ...) { int i; ROSC *SC; pthread_mutex_lock(&stdout_mutex); xmlv1(); locale_char_t *orig = indi_locale_C_numeric_push(); printf("device); printf(" name='%s'\n", n->name); printf(" label='%s'\n", n->label); printf(" group='%s'\n", n->group); printf(" state='%s'\n", pstateStr(n->s)); printf(" perm='%s'\n", permStr(n->p)); printf(" timeout='%g'\n", n->timeout); printf(" timestamp='%s'\n", timestamp()); if (fmt) { va_list ap; va_start(ap, fmt); char message[MAXINDIMESSAGE]; printf(" message='"); vsnprintf(message, MAXINDIMESSAGE, fmt, ap); char *escapedMessage = escapeXML(message, MAXINDIMESSAGE); printf("%s'\n", escapedMessage); free(escapedMessage); va_end(ap); } printf(">\n"); for (i = 0; i < n->nnp; i++) { INumber *np = &n->np[i]; printf(" name); printf(" label='%s'\n", np->label); printf(" format='%s'\n", np->format); printf(" min='%.20g'\n", np->min); printf(" max='%.20g'\n", np->max); printf(" step='%.20g'>\n", np->step); printf(" %.20g\n", np->value); printf(" \n"); } printf("\n"); if (isPropDefined(n->name, n->device) < 0) { /* Add this property to insure proper sanity check */ propCache = propCache ? (ROSC *)realloc(propCache, sizeof(ROSC) * (nPropCache + 1)) : (ROSC *)malloc(sizeof(ROSC)); SC = &propCache[nPropCache++]; strcpy(SC->propName, n->name); strcpy(SC->devName, n->device); SC->perm = n->p; SC->ptr = n; SC->type = INDI_NUMBER; } indi_locale_C_numeric_pop(orig); fflush(stdout); pthread_mutex_unlock(&stdout_mutex); } /* tell client to create a new switch vector property */ void IDDefSwitch(const ISwitchVectorProperty *s, const char *fmt, ...) { int i; ROSC *SC; pthread_mutex_lock(&stdout_mutex); xmlv1(); locale_char_t *orig = indi_locale_C_numeric_push(); printf("device); printf(" name='%s'\n", s->name); printf(" label='%s'\n", s->label); printf(" group='%s'\n", s->group); printf(" state='%s'\n", pstateStr(s->s)); printf(" perm='%s'\n", permStr(s->p)); printf(" rule='%s'\n", ruleStr(s->r)); printf(" timeout='%g'\n", s->timeout); printf(" timestamp='%s'\n", timestamp()); if (fmt) { va_list ap; va_start(ap, fmt); char message[MAXINDIMESSAGE]; printf(" message='"); vsnprintf(message, MAXINDIMESSAGE, fmt, ap); char *escapedMessage = escapeXML(message, MAXINDIMESSAGE); printf("%s'\n", escapedMessage); free(escapedMessage); va_end(ap); } printf(">\n"); for (i = 0; i < s->nsp; i++) { ISwitch *sp = &s->sp[i]; printf(" name); printf(" label='%s'>\n", sp->label); printf(" %s\n", sstateStr(sp->s)); printf(" \n"); } printf("\n"); if (isPropDefined(s->name, s->device) < 0) { /* Add this property to insure proper sanity check */ propCache = propCache ? (ROSC *)realloc(propCache, sizeof(ROSC) * (nPropCache + 1)) : (ROSC *)malloc(sizeof(ROSC)); SC = &propCache[nPropCache++]; strcpy(SC->propName, s->name); strcpy(SC->devName, s->device); SC->perm = s->p; SC->ptr = s; SC->type = INDI_SWITCH; } indi_locale_C_numeric_pop(orig); fflush(stdout); pthread_mutex_unlock(&stdout_mutex); } /* tell client to create a new lights vector property */ void IDDefLight(const ILightVectorProperty *lvp, const char *fmt, ...) { int i; pthread_mutex_lock(&stdout_mutex); xmlv1(); printf("device); printf(" name='%s'\n", lvp->name); printf(" label='%s'\n", lvp->label); printf(" group='%s'\n", lvp->group); printf(" state='%s'\n", pstateStr(lvp->s)); printf(" timestamp='%s'\n", timestamp()); if (fmt) { va_list ap; va_start(ap, fmt); char message[MAXINDIMESSAGE]; printf(" message='"); vsnprintf(message, MAXINDIMESSAGE, fmt, ap); char *escapedMessage = escapeXML(message, MAXINDIMESSAGE); printf("%s'\n", escapedMessage); free(escapedMessage); va_end(ap); } printf(">\n"); for (i = 0; i < lvp->nlp; i++) { ILight *lp = &lvp->lp[i]; printf(" name); printf(" label='%s'>\n", lp->label); printf(" %s\n", pstateStr(lp->s)); printf(" \n"); } printf("\n"); fflush(stdout); pthread_mutex_unlock(&stdout_mutex); } /* tell client to create a new BLOB vector property */ void IDDefBLOB(const IBLOBVectorProperty *b, const char *fmt, ...) { int i; ROSC *SC; pthread_mutex_lock(&stdout_mutex); xmlv1(); locale_char_t *orig = indi_locale_C_numeric_push(); printf("device); printf(" name='%s'\n", b->name); printf(" label='%s'\n", b->label); printf(" group='%s'\n", b->group); printf(" state='%s'\n", pstateStr(b->s)); printf(" perm='%s'\n", permStr(b->p)); printf(" timeout='%g'\n", b->timeout); printf(" timestamp='%s'\n", timestamp()); if (fmt) { va_list ap; va_start(ap, fmt); char message[MAXINDIMESSAGE]; printf(" message='"); vsnprintf(message, MAXINDIMESSAGE, fmt, ap); char *escapedMessage = escapeXML(message, MAXINDIMESSAGE); printf("%s'\n", escapedMessage); free(escapedMessage); va_end(ap); } printf(">\n"); for (i = 0; i < b->nbp; i++) { IBLOB *bp = &b->bp[i]; printf(" name); printf(" label='%s'\n", bp->label); printf(" />\n"); } printf("\n"); if (isPropDefined(b->name, b->device) < 0) { /* Add this property to insure proper sanity check */ propCache = propCache ? (ROSC *)realloc(propCache, sizeof(ROSC) * (nPropCache + 1)) : (ROSC *)malloc(sizeof(ROSC)); SC = &propCache[nPropCache++]; strcpy(SC->propName, b->name); strcpy(SC->devName, b->device); SC->perm = b->p; SC->ptr = b; SC->type = INDI_BLOB; } indi_locale_C_numeric_pop(orig); fflush(stdout); pthread_mutex_unlock(&stdout_mutex); } /* tell client to update an existing text vector property */ void IDSetText(const ITextVectorProperty *tvp, const char *fmt, ...) { int i; pthread_mutex_lock(&stdout_mutex); xmlv1(); locale_char_t *orig = indi_locale_C_numeric_push(); printf("device); printf(" name='%s'\n", tvp->name); printf(" state='%s'\n", pstateStr(tvp->s)); printf(" timeout='%g'\n", tvp->timeout); printf(" timestamp='%s'\n", timestamp()); if (fmt) { va_list ap; va_start(ap, fmt); char message[MAXINDIMESSAGE]; printf(" message='"); vsnprintf(message, MAXINDIMESSAGE, fmt, ap); char *escapedMessage = escapeXML(message, MAXINDIMESSAGE); printf("%s'\n", escapedMessage); free(escapedMessage); va_end(ap); } printf(">\n"); for (i = 0; i < tvp->ntp; i++) { IText *tp = &tvp->tp[i]; printf(" \n", tp->name); printf(" %s\n", tp->text ? tp->text : ""); printf(" \n"); } printf("\n"); indi_locale_C_numeric_pop(orig); fflush(stdout); pthread_mutex_unlock(&stdout_mutex); } /* tell client to update an existing numeric vector property */ void IDSetNumber(const INumberVectorProperty *nvp, const char *fmt, ...) { int i; pthread_mutex_lock(&stdout_mutex); xmlv1(); locale_char_t *orig = indi_locale_C_numeric_push(); printf("device); printf(" name='%s'\n", nvp->name); printf(" state='%s'\n", pstateStr(nvp->s)); printf(" timeout='%g'\n", nvp->timeout); printf(" timestamp='%s'\n", timestamp()); if (fmt) { va_list ap; va_start(ap, fmt); char message[MAXINDIMESSAGE]; printf(" message='"); vsnprintf(message, MAXINDIMESSAGE, fmt, ap); char *escapedMessage = escapeXML(message, MAXINDIMESSAGE); printf("%s'\n", escapedMessage); free(escapedMessage); va_end(ap); } printf(">\n"); for (i = 0; i < nvp->nnp; i++) { INumber *np = &nvp->np[i]; printf(" \n", np->name); printf(" %.20g\n", np->value); printf(" \n"); } printf("\n"); indi_locale_C_numeric_pop(orig); fflush(stdout); pthread_mutex_unlock(&stdout_mutex); } /* tell client to update an existing switch vector property */ void IDSetSwitch(const ISwitchVectorProperty *svp, const char *fmt, ...) { int i; pthread_mutex_lock(&stdout_mutex); xmlv1(); locale_char_t *orig = indi_locale_C_numeric_push(); printf("device); printf(" name='%s'\n", svp->name); printf(" state='%s'\n", pstateStr(svp->s)); printf(" timeout='%g'\n", svp->timeout); printf(" timestamp='%s'\n", timestamp()); if (fmt) { va_list ap; va_start(ap, fmt); char message[MAXINDIMESSAGE]; printf(" message='"); vsnprintf(message, MAXINDIMESSAGE, fmt, ap); char *escapedMessage = escapeXML(message, MAXINDIMESSAGE); printf("%s'\n", escapedMessage); free(escapedMessage); va_end(ap); } printf(">\n"); for (i = 0; i < svp->nsp; i++) { ISwitch *sp = &svp->sp[i]; printf(" \n", sp->name); printf(" %s\n", sstateStr(sp->s)); printf(" \n"); } printf("\n"); indi_locale_C_numeric_pop(orig); fflush(stdout); pthread_mutex_unlock(&stdout_mutex); } /* tell client to update an existing lights vector property */ void IDSetLight(const ILightVectorProperty *lvp, const char *fmt, ...) { int i; pthread_mutex_lock(&stdout_mutex); xmlv1(); printf("device); printf(" name='%s'\n", lvp->name); printf(" state='%s'\n", pstateStr(lvp->s)); printf(" timestamp='%s'\n", timestamp()); if (fmt) { va_list ap; va_start(ap, fmt); char message[MAXINDIMESSAGE]; printf(" message='"); vsnprintf(message, MAXINDIMESSAGE, fmt, ap); char *escapedMessage = escapeXML(message, MAXINDIMESSAGE); printf("%s'\n", escapedMessage); free(escapedMessage); va_end(ap); } printf(">\n"); for (i = 0; i < lvp->nlp; i++) { ILight *lp = &lvp->lp[i]; printf(" \n", lp->name); printf(" %s\n", pstateStr(lp->s)); printf(" \n"); } printf("\n"); fflush(stdout); pthread_mutex_unlock(&stdout_mutex); } /* tell client to update an existing BLOB vector property */ void IDSetBLOB(const IBLOBVectorProperty *bvp, const char *fmt, ...) { int i; pthread_mutex_lock(&stdout_mutex); xmlv1(); locale_char_t *orig = indi_locale_C_numeric_push(); printf("device); printf(" name='%s'\n", bvp->name); printf(" state='%s'\n", pstateStr(bvp->s)); printf(" timeout='%g'\n", bvp->timeout); printf(" timestamp='%s'\n", timestamp()); if (fmt) { va_list ap; va_start(ap, fmt); printf(" message='"); vprintf(fmt, ap); printf("'\n"); va_end(ap); } printf(">\n"); for (i = 0; i < bvp->nbp; i++) { IBLOB *bp = &bvp->bp[i]; unsigned char *encblob; int l; printf(" name); printf(" size='%d'\n", bp->size); // If size is zero, we are only sending a state-change if (bp->size == 0) { printf(" enclen='0'\n"); printf(" format='%s'>\n", bp->format); } else { encblob = malloc(4 * bp->bloblen / 3 + 4); l = to64frombits(encblob, bp->blob, bp->bloblen); printf(" enclen='%d'\n", l); printf(" format='%s'>\n", bp->format); size_t written = 0; while ((int)written < l) { size_t towrite = ((l - written) > 72) ? 72 : l - written; size_t wr = fwrite(encblob + written, 1, towrite, stdout); if (wr > 0) written += wr; if ((written % 72) == 0) fputc('\n', stdout); } if ((written % 72) != 0) fputc('\n', stdout); free(encblob); } printf(" \n"); } printf("\n"); indi_locale_C_numeric_pop(orig); fflush(stdout); pthread_mutex_unlock(&stdout_mutex); } /* tell client to update min/max elements of an existing number vector property */ void IUUpdateMinMax(const INumberVectorProperty *nvp) { int i; pthread_mutex_lock(&stdout_mutex); xmlv1(); locale_char_t *orig = indi_locale_C_numeric_push(); printf("device); printf(" name='%s'\n", nvp->name); printf(" state='%s'\n", pstateStr(nvp->s)); printf(" timeout='%g'\n", nvp->timeout); printf(" timestamp='%s'\n", timestamp()); printf(">\n"); for (i = 0; i < nvp->nnp; i++) { INumber *np = &nvp->np[i]; printf(" name); printf(" min='%g'\n", np->min); printf(" max='%g'\n", np->max); printf(" step='%g'\n", np->step); printf(">\n"); printf(" %g\n", np->value); printf(" \n"); } printf("\n"); indi_locale_C_numeric_pop(orig); fflush(stdout); pthread_mutex_unlock(&stdout_mutex); } int IUFindIndex(const char *needle, char **hay, unsigned int n) { int i = 0; for (i = 0; i < (int)n; i++) { if (!strcmp(hay[i], needle)) return i; } return -1; } libindi/COPYING.GPL0000664000175000017500000004325413263645557013156 0ustar jasemjasem GNU GENERAL PUBLIC LICENSE Version 2, June 1991 Copyright (C) 1989, 1991 Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This General Public License applies to most of the Free Software Foundation's software and to any other program whose authors commit to using it. (Some other Free Software Foundation software is covered by the GNU Lesser General Public License instead.) You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs; and that you know you can do these things. To protect your rights, we need to make restrictions that forbid anyone to deny you these rights or to ask you to surrender the rights. These restrictions translate to certain responsibilities for you if you distribute copies of the software, or if you modify it. For example, if you distribute copies of such a program, whether gratis or for a fee, you must give the recipients all the rights that you have. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. We protect your rights with two steps: (1) copyright the software, and (2) offer you this license which gives you legal permission to copy, distribute and/or modify the software. Also, for each author's protection and ours, we want to make certain that everyone understands that there is no warranty for this free software. If the software is modified by someone else and passed on, we want its recipients to know that what they have is not the original, so that any problems introduced by others will not reflect on the original authors' reputations. Finally, any free program is threatened constantly by software patents. We wish to avoid the danger that redistributors of a free program will individually obtain patent licenses, in effect making the program proprietary. To prevent this, we have made it clear that any patent must be licensed for everyone's free use or not licensed at all. The precise terms and conditions for copying, distribution and modification follow. GNU GENERAL PUBLIC LICENSE TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 0. This License applies to any program or other work which contains a notice placed by the copyright holder saying it may be distributed under the terms of this General Public License. The "Program", below, refers to any such program or work, and a "work based on the Program" means either the Program or any derivative work under copyright law: that is to say, a work containing the Program or a portion of it, either verbatim or with modifications and/or translated into another language. (Hereinafter, translation is included without limitation in the term "modification".) Each licensee is addressed as "you". Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running the Program is not restricted, and the output from the Program is covered only if its contents constitute a work based on the Program (independent of having been made by running the Program). Whether that is true depends on what the Program does. 1. You may copy and distribute verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and give any other recipients of the Program a copy of this License along with the Program. You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. 2. You may modify your copy or copies of the Program or any portion of it, thus forming a work based on the Program, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions: a) You must cause the modified files to carry prominent notices stating that you changed the files and the date of any change. b) You must cause any work that you distribute or publish, that in whole or in part contains or is derived from the Program or any part thereof, to be licensed as a whole at no charge to all third parties under the terms of this License. c) If the modified program normally reads commands interactively when run, you must cause it, when started running for such interactive use in the most ordinary way, to print or display an announcement including an appropriate copyright notice and a notice that there is no warranty (or else, saying that you provide a warranty) and that users may redistribute the program under these conditions, and telling the user how to view a copy of this License. (Exception: if the Program itself is interactive but does not normally print such an announcement, your work based on the Program is not required to print an announcement.) These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Program, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Program, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Program. In addition, mere aggregation of another work not based on the Program with the Program (or with a work based on the Program) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. 3. You may copy and distribute the Program (or a work based on it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you also do one of the following: a) Accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, b) Accompany it with a written offer, valid for at least three years, to give any third party, for a charge no more than your cost of physically performing source distribution, a complete machine-readable copy of the corresponding source code, to be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, c) Accompany it with the information you received as to the offer to distribute corresponding source code. (This alternative is allowed only for noncommercial distribution and only if you received the program in object code or executable form with such an offer, in accord with Subsection b above.) The source code for a work means the preferred form of the work for making modifications to it. For an executable work, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the executable. However, as a special exception, the source code distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable. If distribution of executable or object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place counts as distribution of the source code, even though third parties are not compelled to copy the source along with the object code. 4. You may not copy, modify, sublicense, or distribute the Program except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense or distribute the Program is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. 5. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Program or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Program (or any work based on the Program), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Program or works based on it. 6. Each time you redistribute the Program (or any work based on the Program), the recipient automatically receives a license from the original licensor to copy, distribute or modify the Program subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties to this License. 7. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Program at all. For example, if a patent license would not permit royalty-free redistribution of the Program by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Program. If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply and the section as a whole is intended to apply in other circumstances. It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system, which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice. This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. 8. If the distribution and/or use of the Program is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Program under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License. 9. The Free Software Foundation may publish revised and/or new versions of the General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of this License, you may choose any version ever published by the Free Software Foundation. 10. If you wish to incorporate parts of the Program into other free programs whose distribution conditions are different, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. NO WARRANTY 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program 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 General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. Also add information on how to contact you by electronic and paper mail. If the program is interactive, make it output a short notice like this when it starts in an interactive mode: Gnomovision version 69, Copyright (C) year name of author Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, the commands you use may be called something other than `show w' and `show c'; they could even be mouse-clicks or menu items--whatever suits your program. You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the program, if necessary. Here is a sample; alter the names: Yoyodyne, Inc., hereby disclaims all copyright interest in the program `Gnomovision' (which makes passes at compilers) written by James Hacker. , 1 April 1989 Ty Coon, President of Vice This General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. libindi/examples/0000775000175000017500000000000013263645557013310 5ustar jasemjasemlibindi/examples/tutorial_one/0000775000175000017500000000000013263645557016014 5ustar jasemjasemlibindi/examples/tutorial_one/CMakeLists.txt0000664000175000017500000000021013263645557020545 0ustar jasemjasem########### Tutorial one ############## add_executable(tutorial_one simpledevice.cpp) target_link_libraries(tutorial_one indidriver) libindi/examples/tutorial_one/simpledevice.cpp0000664000175000017500000000733513263645557021201 0ustar jasemjasem/* INDI Developers Manual Tutorial #1 "Hello INDI" We construct a most basic (and useless) device driver to illustrate INDI. Refer to README, which contains instruction on how to build this driver, and use it with an INDI-compatible client. */ /** \file simpledevice.cpp \brief Construct a basic INDI device with only one property to connect and disconnect. \author Jasem Mutlaq \example simpledevice.cpp A very minimal device! It also allows you to connect/disconnect and performs no other functions. */ #include "simpledevice.h" #include std::unique_ptr simpleDevice(new SimpleDevice()); /************************************************************************************** ** Return properties of device. ***************************************************************************************/ void ISGetProperties(const char *dev) { simpleDevice->ISGetProperties(dev); } /************************************************************************************** ** Process new switch from client ***************************************************************************************/ void ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) { simpleDevice->ISNewSwitch(dev, name, states, names, n); } /************************************************************************************** ** Process new text from client ***************************************************************************************/ void ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n) { simpleDevice->ISNewText(dev, name, texts, names, n); } /************************************************************************************** ** Process new number from client ***************************************************************************************/ void ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) { simpleDevice->ISNewNumber(dev, name, values, names, n); } /************************************************************************************** ** Process new blob from client ***************************************************************************************/ void ISNewBLOB(const char *dev, const char *name, int sizes[], int blobsizes[], char *blobs[], char *formats[], char *names[], int n) { simpleDevice->ISNewBLOB(dev, name, sizes, blobsizes, blobs, formats, names, n); } /************************************************************************************** ** Process snooped property from another driver ***************************************************************************************/ void ISSnoopDevice(XMLEle *root) { INDI_UNUSED(root); } /************************************************************************************** ** Client is asking us to establish connection to the device ***************************************************************************************/ bool SimpleDevice::Connect() { IDMessage(getDeviceName(), "Simple device connected successfully!"); return true; } /************************************************************************************** ** Client is asking us to terminate connection to the device ***************************************************************************************/ bool SimpleDevice::Disconnect() { IDMessage(getDeviceName(), "Simple device disconnected successfully!"); return true; } /************************************************************************************** ** INDI is asking us for our default device name ***************************************************************************************/ const char *SimpleDevice::getDefaultName() { return "Simple Device"; } libindi/examples/tutorial_one/simpledevice.h0000664000175000017500000000137713263645557020646 0ustar jasemjasem/* INDI Developers Manual Tutorial #1 "Hello INDI" We construct a most basic (and useless) device driver to illustate INDI. Refer to README, which contains instruction on how to build this driver, and use it with an INDI-compatible client. */ /** \file simpledevice.h \brief Construct a basic INDI device with only one property to connect and disconnect. \author Jasem Mutlaq \example simpledevice.h A very minimal device! It also allows you to connect/disconnect and performs no other functions. */ #pragma once #include "defaultdevice.h" class SimpleDevice : public INDI::DefaultDevice { public: SimpleDevice() = default; protected: bool Connect(); bool Disconnect(); const char *getDefaultName(); }; libindi/examples/CMakeLists.txt0000664000175000017500000000040013263645557016042 0ustar jasemjasemadd_subdirectory(tutorial_one) add_subdirectory(tutorial_two) add_subdirectory(tutorial_three) add_subdirectory(tutorial_four) add_subdirectory(tutorial_five) add_subdirectory(tutorial_six) add_subdirectory(tutorial_seven) add_subdirectory(tutorial_eight) libindi/examples/tutorial_seven/0000775000175000017500000000000013263645557016353 5ustar jasemjasemlibindi/examples/tutorial_seven/CMakeLists.txt0000664000175000017500000000025413263645557021114 0ustar jasemjasem########### Tutorial Seven ############## add_executable(tutorial_seven simple_telescope_simulator.cpp) target_link_libraries(tutorial_seven indidriver AlignmentDriver) libindi/examples/tutorial_seven/simple_telescope_simulator.h0000664000175000017500000000610213263645557024156 0ustar jasemjasem #pragma once #include "indiguiderinterface.h" #include "inditelescope.h" #include "alignment/AlignmentSubsystemForDrivers.h" class ScopeSim : public INDI::Telescope, public INDI::AlignmentSubsystem::AlignmentSubsystemForDrivers { public: ScopeSim(); private: virtual bool Abort() override; bool canSync(); virtual bool Connect() override; virtual bool Disconnect() override; virtual const char *getDefaultName() override; virtual bool Goto(double ra, double dec) override; virtual bool initProperties() override; friend void ISNewBLOB(const char *dev, const char *name, int sizes[], int blobsizes[], char *blobs[], char *formats[], char *names[], int n); virtual bool ISNewBLOB(const char *dev, const char *name, int sizes[], int blobsizes[], char *blobs[], char *formats[], char *names[], int n) override; friend void ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n); virtual bool ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) override; friend void ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n); virtual bool ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) override; friend void ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n); virtual bool ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n) override; virtual bool MoveNS(INDI_DIR_NS dir, TelescopeMotionCommand command) override; virtual bool MoveWE(INDI_DIR_WE dir, TelescopeMotionCommand command) override; virtual bool ReadScopeStatus() override; virtual bool Sync(double ra, double dec) override; virtual void TimerHit() override; virtual bool updateLocation(double latitude, double longitude, double elevation) override; static constexpr long MICROSTEPS_PER_REVOLUTION { 1000000 }; static constexpr double MICROSTEPS_PER_DEGREE { MICROSTEPS_PER_REVOLUTION / 360.0 }; static constexpr double DEFAULT_SLEW_RATE { MICROSTEPS_PER_DEGREE * 2.0 }; static constexpr long MAX_DEC { (long)(90.0 * MICROSTEPS_PER_DEGREE) }; static constexpr long MIN_DEC { (long)(-90.0 * MICROSTEPS_PER_DEGREE) }; enum AxisStatus { STOPPED, SLEWING, SLEWING_TO }; enum AxisDirection { FORWARD, REVERSE }; AxisStatus AxisStatusDEC { STOPPED }; AxisDirection AxisDirectionDEC { FORWARD }; double AxisSlewRateDEC { DEFAULT_SLEW_RATE }; long CurrentEncoderMicrostepsDEC { 0 }; long GotoTargetMicrostepsDEC { 0 }; AxisStatus AxisStatusRA { STOPPED }; AxisDirection AxisDirectionRA { FORWARD }; double AxisSlewRateRA { DEFAULT_SLEW_RATE }; long CurrentEncoderMicrostepsRA { 0 }; long GotoTargetMicrostepsRA { 0 }; // Tracking ln_equ_posn CurrentTrackingTarget { 0, 0 }; // Tracing in timer tick int TraceThisTickCount { 0 }; bool TraceThisTick { false }; unsigned int DBG_SIMULATOR { 0 }; }; libindi/examples/tutorial_seven/simple_telescope_simulator.cpp0000664000175000017500000007461513263645557024527 0ustar jasemjasem/* INDI Developers Manual Tutorial #7 "Simple telescope simulator" We construct a most basic (and useless) device driver to illustrate INDI. Refer to README, which contains instruction on how to build this driver, and use it with an INDI-compatible client. */ #include "simple_telescope_simulator.h" #include "indicom.h" #include using namespace INDI::AlignmentSubsystem; // We declare an auto pointer to ScopeSim. std::unique_ptr telescope_sim(new ScopeSim()); void ISGetProperties(const char *dev) { telescope_sim->ISGetProperties(dev); } void ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) { telescope_sim->ISNewSwitch(dev, name, states, names, n); } void ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n) { telescope_sim->ISNewText(dev, name, texts, names, n); } void ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) { telescope_sim->ISNewNumber(dev, name, values, names, n); } void ISNewBLOB(const char *dev, const char *name, int sizes[], int blobsizes[], char *blobs[], char *formats[], char *names[], int n) { telescope_sim->ISNewBLOB(dev, name, sizes, blobsizes, blobs, formats, names, n); } void ISSnoopDevice(XMLEle *root) { INDI_UNUSED(root); } ScopeSim::ScopeSim() : DBG_SIMULATOR(INDI::Logger::getInstance().addDebugLevel("Simulator Verbose", "SIMULATOR")) { } bool ScopeSim::Abort() { if (MovementNSSP.s == IPS_BUSY) { IUResetSwitch(&MovementNSSP); MovementNSSP.s = IPS_IDLE; IDSetSwitch(&MovementNSSP, nullptr); } if (MovementWESP.s == IPS_BUSY) { MovementWESP.s = IPS_IDLE; IUResetSwitch(&MovementWESP); IDSetSwitch(&MovementWESP, nullptr); } if (EqNP.s == IPS_BUSY) { EqNP.s = IPS_IDLE; IDSetNumber(&EqNP, nullptr); } TrackState = SCOPE_IDLE; AxisStatusRA = AxisStatusDEC = STOPPED; // This marvelous inertia free scope can be stopped instantly! AbortSP.s = IPS_OK; IUResetSwitch(&AbortSP); IDSetSwitch(&AbortSP, nullptr); LOG_INFO("Telescope aborted."); return true; } bool ScopeSim::canSync() { return true; } bool ScopeSim::Connect() { SetTimer(POLLMS); return true; } bool ScopeSim::Disconnect() { return true; } const char *ScopeSim::getDefaultName() { return (const char *)"Simple Telescope Simulator"; } bool ScopeSim::Goto(double ra, double dec) { DEBUGF(DBG_SIMULATOR, "Goto - Celestial reference frame target right ascension %lf(%lf) declination %lf", ra * 360.0 / 24.0, ra, dec); if (ISS_ON == IUFindSwitch(&CoordSP, "TRACK")->s) { char RAStr[32], DecStr[32]; fs_sexa(RAStr, ra, 2, 3600); fs_sexa(DecStr, dec, 2, 3600); CurrentTrackingTarget.ra = ra; CurrentTrackingTarget.dec = dec; DEBUG(DBG_SIMULATOR, "Goto - tracking requested"); } // Call the alignment subsystem to translate the celestial reference frame coordinate // into a telescope reference frame coordinate TelescopeDirectionVector TDV; ln_hrz_posn AltAz { 0, 0 }; if (TransformCelestialToTelescope(ra, dec, 0.0, TDV)) { // The alignment subsystem has successfully transformed my coordinate AltitudeAzimuthFromTelescopeDirectionVector(TDV, AltAz); } else { // The alignment subsystem cannot transform the coordinate. // Try some simple rotations using the stored observatory position if any bool HavePosition = false; ln_lnlat_posn Position { 0, 0 }; if ((nullptr != IUFindNumber(&LocationNP, "LAT")) && (0 != IUFindNumber(&LocationNP, "LAT")->value) && (nullptr != IUFindNumber(&LocationNP, "LONG")) && (0 != IUFindNumber(&LocationNP, "LONG")->value)) { // I assume that being on the equator and exactly on the prime meridian is unlikely Position.lat = IUFindNumber(&LocationNP, "LAT")->value; Position.lng = IUFindNumber(&LocationNP, "LONG")->value; HavePosition = true; } ln_equ_posn EquatorialCoordinates { 0, 0 }; // libnova works in decimal degrees EquatorialCoordinates.ra = ra * 360.0 / 24.0; EquatorialCoordinates.dec = dec; if (HavePosition) { ln_get_hrz_from_equ(&EquatorialCoordinates, &Position, ln_get_julian_from_sys(), &AltAz); TDV = TelescopeDirectionVectorFromAltitudeAzimuth(AltAz); switch (GetApproximateMountAlignment()) { case ZENITH: break; case NORTH_CELESTIAL_POLE: // Rotate the TDV coordinate system clockwise (negative) around the y axis by 90 minus // the (positive)observatory latitude. The vector itself is rotated anticlockwise TDV.RotateAroundY(Position.lat - 90.0); break; case SOUTH_CELESTIAL_POLE: // Rotate the TDV coordinate system anticlockwise (positive) around the y axis by 90 plus // the (negative)observatory latitude. The vector itself is rotated clockwise TDV.RotateAroundY(Position.lat + 90.0); break; } AltitudeAzimuthFromTelescopeDirectionVector(TDV, AltAz); } else { // The best I can do is just do a direct conversion to Alt/Az TDV = TelescopeDirectionVectorFromEquatorialCoordinates(EquatorialCoordinates); AltitudeAzimuthFromTelescopeDirectionVector(TDV, AltAz); } } // My altitude encoder runs -90 to +90 if ((AltAz.alt > 90.0) || (AltAz.alt < -90.0)) { DEBUG(DBG_SIMULATOR, "Goto - Altitude out of range"); // This should not happen return false; } // My polar encoder runs 0 to +360 if ((AltAz.az > 360.0) || (AltAz.az < -360.0)) { DEBUG(DBG_SIMULATOR, "Goto - Azimuth out of range"); // This should not happen return false; } if (AltAz.az < 0.0) { DEBUG(DBG_SIMULATOR, "Goto - Azimuth negative"); AltAz.az = 360.0 + AltAz.az; } DEBUGF(DBG_SIMULATOR, "Goto - Scope reference frame target altitude %lf azimuth %lf", AltAz.alt, AltAz.az); GotoTargetMicrostepsDEC = int(AltAz.alt * MICROSTEPS_PER_DEGREE); if (GotoTargetMicrostepsDEC == CurrentEncoderMicrostepsDEC) AxisStatusDEC = STOPPED; else { if (GotoTargetMicrostepsDEC > CurrentEncoderMicrostepsDEC) AxisDirectionDEC = FORWARD; else AxisDirectionDEC = REVERSE; AxisStatusDEC = SLEWING_TO; } GotoTargetMicrostepsRA = int(AltAz.az * MICROSTEPS_PER_DEGREE); if (GotoTargetMicrostepsRA == CurrentEncoderMicrostepsRA) AxisStatusRA = STOPPED; else { if (GotoTargetMicrostepsRA > CurrentEncoderMicrostepsRA) AxisDirectionRA = (GotoTargetMicrostepsRA - CurrentEncoderMicrostepsRA) < MICROSTEPS_PER_REVOLUTION / 2.0 ? FORWARD : REVERSE; else AxisDirectionRA = (CurrentEncoderMicrostepsRA - GotoTargetMicrostepsRA) < MICROSTEPS_PER_REVOLUTION / 2.0 ? REVERSE : FORWARD; AxisStatusRA = SLEWING_TO; } TrackState = SCOPE_SLEWING; EqNP.s = IPS_BUSY; return true; } bool ScopeSim::initProperties() { /* Make sure to init parent properties first */ INDI::Telescope::initProperties(); // Let's simulate it to be an F/10 8" telescope ScopeParametersN[0].value = 203; ScopeParametersN[1].value = 2000; ScopeParametersN[2].value = 203; ScopeParametersN[3].value = 2000; TrackState = SCOPE_IDLE; /* Add debug controls so we may debug driver if necessary */ addDebugControl(); // Add alignment properties InitAlignmentProperties(this); return true; } bool ScopeSim::ISNewBLOB(const char *dev, const char *name, int sizes[], int blobsizes[], char *blobs[], char *formats[], char *names[], int n) { if (dev != nullptr && strcmp(dev, getDeviceName()) == 0) { // Process alignment properties ProcessAlignmentBLOBProperties(this, name, sizes, blobsizes, blobs, formats, names, n); } // Pass it up the chain return INDI::Telescope::ISNewBLOB(dev, name, sizes, blobsizes, blobs, formats, names, n); } bool ScopeSim::ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) { // first check if it's for our device if (dev != nullptr && strcmp(dev, getDeviceName()) == 0) { // Process alignment properties ProcessAlignmentNumberProperties(this, name, values, names, n); } // if we didn't process it, continue up the chain, let somebody else // give it a shot return INDI::Telescope::ISNewNumber(dev, name, values, names, n); } bool ScopeSim::ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) { if (dev != nullptr && strcmp(dev, getDeviceName()) == 0) { // Process alignment properties ProcessAlignmentSwitchProperties(this, name, states, names, n); } // Nobody has claimed this, so, ignore it return INDI::Telescope::ISNewSwitch(dev, name, states, names, n); } bool ScopeSim::ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n) { if (dev != nullptr && strcmp(dev, getDeviceName()) == 0) { // Process alignment properties ProcessAlignmentTextProperties(this, name, texts, names, n); } // Pass it up the chain return INDI::Telescope::ISNewText(dev, name, texts, names, n); } bool ScopeSim::MoveNS(INDI_DIR_NS dir, TelescopeMotionCommand command) { AxisDirection axisDir = (dir == DIRECTION_NORTH) ? FORWARD : REVERSE; AxisStatus axisStat = (command == MOTION_START) ? SLEWING : STOPPED; AxisSlewRateDEC = DEFAULT_SLEW_RATE; AxisDirectionDEC = axisDir; AxisStatusDEC = axisStat; return true; } bool ScopeSim::MoveWE(INDI_DIR_WE dir, TelescopeMotionCommand command) { AxisDirection axisDir = (dir == DIRECTION_WEST) ? FORWARD : REVERSE; AxisStatus axisStat = (command == MOTION_START) ? SLEWING : STOPPED; AxisSlewRateRA = DEFAULT_SLEW_RATE; AxisDirectionRA = axisDir; AxisStatusRA = axisStat; return true; } bool ScopeSim::ReadScopeStatus() { ln_hrz_posn AltAz { 0, 0 }; AltAz.alt = double(CurrentEncoderMicrostepsDEC) / MICROSTEPS_PER_DEGREE; AltAz.az = double(CurrentEncoderMicrostepsRA) / MICROSTEPS_PER_DEGREE; TelescopeDirectionVector TDV = TelescopeDirectionVectorFromAltitudeAzimuth(AltAz); double RightAscension, Declination; if (!TransformTelescopeToCelestial(TDV, RightAscension, Declination)) { if (TraceThisTick) DEBUG(DBG_SIMULATOR, "ReadScopeStatus - TransformTelescopeToCelestial failed"); bool HavePosition = false; ln_lnlat_posn Position { 0, 0 }; if ((nullptr != IUFindNumber(&LocationNP, "LAT")) && (0 != IUFindNumber(&LocationNP, "LAT")->value) && (nullptr != IUFindNumber(&LocationNP, "LONG")) && (0 != IUFindNumber(&LocationNP, "LONG")->value)) { // I assume that being on the equator and exactly on the prime meridian is unlikely Position.lat = IUFindNumber(&LocationNP, "LAT")->value; Position.lng = IUFindNumber(&LocationNP, "LONG")->value; HavePosition = true; } ln_equ_posn EquatorialCoordinates { 0, 0 }; if (HavePosition) { if (TraceThisTick) DEBUG(DBG_SIMULATOR, "ReadScopeStatus - HavePosition true"); TelescopeDirectionVector RotatedTDV(TDV); switch (GetApproximateMountAlignment()) { case ZENITH: if (TraceThisTick) DEBUG(DBG_SIMULATOR, "ReadScopeStatus - ApproximateMountAlignment ZENITH"); break; case NORTH_CELESTIAL_POLE: if (TraceThisTick) DEBUG(DBG_SIMULATOR, "ReadScopeStatus - ApproximateMountAlignment NORTH_CELESTIAL_POLE"); // Rotate the TDV coordinate system anticlockwise (positive) around the y axis by 90 minus // the (positive)observatory latitude. The vector itself is rotated clockwise RotatedTDV.RotateAroundY(90.0 - Position.lat); AltitudeAzimuthFromTelescopeDirectionVector(RotatedTDV, AltAz); break; case SOUTH_CELESTIAL_POLE: if (TraceThisTick) DEBUG(DBG_SIMULATOR, "ReadScopeStatus - ApproximateMountAlignment SOUTH_CELESTIAL_POLE"); // Rotate the TDV coordinate system clockwise (negative) around the y axis by 90 plus // the (negative)observatory latitude. The vector itself is rotated anticlockwise RotatedTDV.RotateAroundY(-90.0 - Position.lat); AltitudeAzimuthFromTelescopeDirectionVector(RotatedTDV, AltAz); break; } ln_get_equ_from_hrz(&AltAz, &Position, ln_get_julian_from_sys(), &EquatorialCoordinates); } else { if (TraceThisTick) DEBUG(DBG_SIMULATOR, "ReadScopeStatus - HavePosition false"); // The best I can do is just do a direct conversion to RA/DEC EquatorialCoordinatesFromTelescopeDirectionVector(TDV, EquatorialCoordinates); } // libnova works in decimal degrees RightAscension = EquatorialCoordinates.ra * 24.0 / 360.0; Declination = EquatorialCoordinates.dec; } if (TraceThisTick) DEBUGF(DBG_SIMULATOR, "ReadScopeStatus - RA %lf hours DEC %lf degrees", RightAscension, Declination); NewRaDec(RightAscension, Declination); return true; } bool ScopeSim::Sync(double ra, double dec) { ln_hrz_posn AltAz { 0, 0 }; AlignmentDatabaseEntry NewEntry; AltAz.alt = double(CurrentEncoderMicrostepsDEC) / MICROSTEPS_PER_DEGREE; AltAz.az = double(CurrentEncoderMicrostepsRA) / MICROSTEPS_PER_DEGREE; NewEntry.ObservationJulianDate = ln_get_julian_from_sys(); NewEntry.RightAscension = ra; NewEntry.Declination = dec; NewEntry.TelescopeDirection = TelescopeDirectionVectorFromAltitudeAzimuth(AltAz); NewEntry.PrivateDataSize = 0; if (!CheckForDuplicateSyncPoint(NewEntry)) { GetAlignmentDatabase().push_back(NewEntry); // Tell the client about size change UpdateSize(); // Tell the math plugin to reinitialise Initialise(this); return true; } return false; } void ScopeSim::TimerHit() { TraceThisTickCount++; if (60 == TraceThisTickCount) { TraceThisTick = true; TraceThisTickCount = 0; } // Simulate mount movement static struct timeval ltv { 0, 0 }; // previous system time struct timeval tv { 0, 0 }; // new system time double dt; // Elapsed time in seconds since last tick gettimeofday(&tv, nullptr); if (ltv.tv_sec == 0 && ltv.tv_usec == 0) ltv = tv; dt = tv.tv_sec - ltv.tv_sec + (tv.tv_usec - ltv.tv_usec) / 1e6; ltv = tv; // RA axis long SlewSteps = dt * AxisSlewRateRA; bool CompleteRevolution = SlewSteps >= MICROSTEPS_PER_REVOLUTION; SlewSteps = SlewSteps % MICROSTEPS_PER_REVOLUTION; // Just in case ;-) switch (AxisStatusRA) { case STOPPED: // Do nothing break; case SLEWING: { DEBUGF(DBG_SIMULATOR, "TimerHit Slewing - RA Current Encoder %ld SlewSteps %ld Direction %d Target %ld Status %d", CurrentEncoderMicrostepsRA, SlewSteps, AxisDirectionRA, GotoTargetMicrostepsRA, AxisStatusRA); // Update the encoder if (FORWARD == AxisDirectionRA) CurrentEncoderMicrostepsRA += SlewSteps; else CurrentEncoderMicrostepsRA -= SlewSteps; if (CurrentEncoderMicrostepsRA < 0) CurrentEncoderMicrostepsRA += MICROSTEPS_PER_REVOLUTION; else if (CurrentEncoderMicrostepsRA >= MICROSTEPS_PER_REVOLUTION) CurrentEncoderMicrostepsRA -= MICROSTEPS_PER_REVOLUTION; DEBUGF(DBG_SIMULATOR, "TimerHit Slewing - RA New Encoder %d New Status %d", CurrentEncoderMicrostepsRA, AxisStatusRA); break; } case SLEWING_TO: { DEBUGF(DBG_SIMULATOR, "TimerHit SlewingTo - RA Current Encoder %ld SlewSteps %ld Direction %d Target %ld Status %d", CurrentEncoderMicrostepsRA, SlewSteps, AxisDirectionRA, GotoTargetMicrostepsRA, AxisStatusRA); long OldEncoder = CurrentEncoderMicrostepsRA; // Update the encoder if (FORWARD == AxisDirectionRA) CurrentEncoderMicrostepsRA += SlewSteps; else CurrentEncoderMicrostepsRA -= SlewSteps; if (CurrentEncoderMicrostepsRA < 0) CurrentEncoderMicrostepsRA += MICROSTEPS_PER_REVOLUTION; else if (CurrentEncoderMicrostepsRA >= MICROSTEPS_PER_REVOLUTION) CurrentEncoderMicrostepsRA -= MICROSTEPS_PER_REVOLUTION; if (CompleteRevolution) { // Must have found the target AxisStatusRA = STOPPED; CurrentEncoderMicrostepsRA = GotoTargetMicrostepsRA; } else { bool FoundTarget = false; if (FORWARD == AxisDirectionRA) { if (CurrentEncoderMicrostepsRA < OldEncoder) { // Two ranges to search if ((GotoTargetMicrostepsRA >= OldEncoder) && (GotoTargetMicrostepsRA <= MICROSTEPS_PER_REVOLUTION)) FoundTarget = true; else if ((GotoTargetMicrostepsRA >= 0) && (GotoTargetMicrostepsRA <= CurrentEncoderMicrostepsRA)) FoundTarget = true; } else if ((GotoTargetMicrostepsRA >= OldEncoder) && (GotoTargetMicrostepsRA <= CurrentEncoderMicrostepsRA)) FoundTarget = true; } else { if (CurrentEncoderMicrostepsRA > OldEncoder) { // Two ranges to search if ((GotoTargetMicrostepsRA >= 0) && (GotoTargetMicrostepsRA <= OldEncoder)) FoundTarget = true; else if ((GotoTargetMicrostepsRA >= CurrentEncoderMicrostepsRA) && (GotoTargetMicrostepsRA <= MICROSTEPS_PER_REVOLUTION)) FoundTarget = true; } else if ((GotoTargetMicrostepsRA >= CurrentEncoderMicrostepsRA) && (GotoTargetMicrostepsRA <= OldEncoder)) FoundTarget = true; } if (FoundTarget) { AxisStatusRA = STOPPED; CurrentEncoderMicrostepsRA = GotoTargetMicrostepsRA; } } DEBUGF(DBG_SIMULATOR, "TimerHit SlewingTo - RA New Encoder %d New Status %d", CurrentEncoderMicrostepsRA, AxisStatusRA); break; } } // DEC axis SlewSteps = dt * AxisSlewRateDEC; switch (AxisStatusDEC) { case STOPPED: // Do nothing break; case SLEWING: { DEBUGF(DBG_SIMULATOR, "TimerHit Slewing - DEC Current Encoder %ld SlewSteps %d Direction %ld Target %ld Status %d", CurrentEncoderMicrostepsDEC, SlewSteps, AxisDirectionDEC, GotoTargetMicrostepsDEC, AxisStatusDEC); // Update the encoder SlewSteps = SlewSteps % MICROSTEPS_PER_REVOLUTION; // Just in case ;-) if (FORWARD == AxisDirectionDEC) CurrentEncoderMicrostepsDEC += SlewSteps; else CurrentEncoderMicrostepsDEC -= SlewSteps; if (CurrentEncoderMicrostepsDEC > MAX_DEC) { CurrentEncoderMicrostepsDEC = MAX_DEC; AxisStatusDEC = STOPPED; // Hit the buffers DEBUG(DBG_SIMULATOR, "TimerHit - DEC axis hit the buffers at MAX_DEC"); } else if (CurrentEncoderMicrostepsDEC < MIN_DEC) { CurrentEncoderMicrostepsDEC = MIN_DEC; AxisStatusDEC = STOPPED; // Hit the buffers DEBUG(DBG_SIMULATOR, "TimerHit - DEC axis hit the buffers at MIN_DEC"); } DEBUGF(DBG_SIMULATOR, "TimerHit Slewing - DEC New Encoder %d New Status %d", CurrentEncoderMicrostepsDEC, AxisStatusDEC); break; } case SLEWING_TO: { DEBUGF(DBG_SIMULATOR, "TimerHit SlewingTo - DEC Current Encoder %ld SlewSteps %d Direction %ld Target %ld Status %d", CurrentEncoderMicrostepsDEC, SlewSteps, AxisDirectionDEC, GotoTargetMicrostepsDEC, AxisStatusDEC); // Calculate steps to target int StepsToTarget; if (FORWARD == AxisDirectionDEC) { if (CurrentEncoderMicrostepsDEC <= GotoTargetMicrostepsDEC) StepsToTarget = GotoTargetMicrostepsDEC - CurrentEncoderMicrostepsDEC; else StepsToTarget = CurrentEncoderMicrostepsDEC - GotoTargetMicrostepsDEC; } else { // Axis in reverse if (CurrentEncoderMicrostepsDEC >= GotoTargetMicrostepsDEC) StepsToTarget = CurrentEncoderMicrostepsDEC - GotoTargetMicrostepsDEC; else StepsToTarget = GotoTargetMicrostepsDEC - CurrentEncoderMicrostepsDEC; } if (StepsToTarget <= SlewSteps) { // Target was hit this tick AxisStatusDEC = STOPPED; CurrentEncoderMicrostepsDEC = GotoTargetMicrostepsDEC; } else { if (FORWARD == AxisDirectionDEC) CurrentEncoderMicrostepsDEC += SlewSteps; else CurrentEncoderMicrostepsDEC -= SlewSteps; if (CurrentEncoderMicrostepsDEC < 0) CurrentEncoderMicrostepsDEC += MICROSTEPS_PER_REVOLUTION; else if (CurrentEncoderMicrostepsDEC >= MICROSTEPS_PER_REVOLUTION) CurrentEncoderMicrostepsDEC -= MICROSTEPS_PER_REVOLUTION; } DEBUGF(DBG_SIMULATOR, "TimerHit SlewingTo - DEC New Encoder %d New Status %d", CurrentEncoderMicrostepsDEC, AxisStatusDEC); break; } } INDI::Telescope::TimerHit(); // This will call ReadScopeStatus // OK I have updated the celestial reference frame RA/DEC in ReadScopeStatus // Now handle the tracking state switch (TrackState) { case SCOPE_SLEWING: if ((STOPPED == AxisStatusRA) && (STOPPED == AxisStatusDEC)) { if (ISS_ON == IUFindSwitch(&CoordSP, "TRACK")->s) { // Goto has finished start tracking DEBUG(DBG_SIMULATOR, "TimerHit - Goto finished start tracking"); TrackState = SCOPE_TRACKING; // Fall through to tracking case } else { TrackState = SCOPE_IDLE; break; } } else break; case SCOPE_TRACKING: { // Continue or start tracking // Calculate where the mount needs to be in POLLMS time // POLLMS is hardcoded to be one second // TODO may need to make this longer to get a meaningful result double JulianOffset = 1.0 / (24.0 * 60 * 60); TelescopeDirectionVector TDV; ln_hrz_posn AltAz { 0, 0 }; if (TransformCelestialToTelescope(CurrentTrackingTarget.ra, CurrentTrackingTarget.dec, JulianOffset, TDV)) AltitudeAzimuthFromTelescopeDirectionVector(TDV, AltAz); else { // Try a conversion with the stored observatory position if any bool HavePosition = false; ln_lnlat_posn Position { 0, 0 }; if ((nullptr != IUFindNumber(&LocationNP, "LAT")) && (0 != IUFindNumber(&LocationNP, "LAT")->value) && (nullptr != IUFindNumber(&LocationNP, "LONG")) && (0 != IUFindNumber(&LocationNP, "LONG")->value)) { // I assume that being on the equator and exactly on the prime meridian is unlikely Position.lat = IUFindNumber(&LocationNP, "LAT")->value; Position.lng = IUFindNumber(&LocationNP, "LONG")->value; HavePosition = true; } ln_equ_posn EquatorialCoordinates { 0, 0 }; // libnova works in decimal degrees EquatorialCoordinates.ra = CurrentTrackingTarget.ra * 360.0 / 24.0; EquatorialCoordinates.dec = CurrentTrackingTarget.dec; if (HavePosition) ln_get_hrz_from_equ(&EquatorialCoordinates, &Position, ln_get_julian_from_sys() + JulianOffset, &AltAz); else { // No sense in tracking in this case TrackState = SCOPE_IDLE; break; } } // My altitude encoder runs -90 to +90 if ((AltAz.alt > 90.0) || (AltAz.alt < -90.0)) { DEBUG(DBG_SIMULATOR, "TimerHit tracking - Altitude out of range"); // This should not happen return; } // My polar encoder runs 0 to +360 if ((AltAz.az > 360.0) || (AltAz.az < -360.0)) { DEBUG(DBG_SIMULATOR, "TimerHit tracking - Azimuth out of range"); // This should not happen return; } if (AltAz.az < 0.0) { DEBUG(DBG_SIMULATOR, "TimerHit tracking - Azimuth negative"); AltAz.az = 360.0 + AltAz.az; } long AltitudeOffsetMicrosteps = int(AltAz.alt * MICROSTEPS_PER_DEGREE - CurrentEncoderMicrostepsDEC); long AzimuthOffsetMicrosteps = int(AltAz.az * MICROSTEPS_PER_DEGREE - CurrentEncoderMicrostepsRA); DEBUGF(DBG_SIMULATOR, "TimerHit - Tracking AltitudeOffsetMicrosteps %d AzimuthOffsetMicrosteps %d", AltitudeOffsetMicrosteps, AzimuthOffsetMicrosteps); if (0 != AzimuthOffsetMicrosteps) { // Calculate the slewing rates needed to reach that position // at the correct time. This is simple as interval is one second if (AzimuthOffsetMicrosteps > 0) { if (AzimuthOffsetMicrosteps < MICROSTEPS_PER_REVOLUTION / 2.0) { // Forward AxisDirectionRA = FORWARD; AxisSlewRateRA = AzimuthOffsetMicrosteps; } else { // Reverse AxisDirectionRA = REVERSE; AxisSlewRateRA = MICROSTEPS_PER_REVOLUTION - AzimuthOffsetMicrosteps; } } else { AzimuthOffsetMicrosteps = std::abs(AzimuthOffsetMicrosteps); if (AzimuthOffsetMicrosteps < MICROSTEPS_PER_REVOLUTION / 2.0) { // Forward AxisDirectionRA = REVERSE; AxisSlewRateRA = AzimuthOffsetMicrosteps; } else { // Reverse AxisDirectionRA = FORWARD; AxisSlewRateRA = MICROSTEPS_PER_REVOLUTION - AzimuthOffsetMicrosteps; } } AxisSlewRateRA = std::abs(AzimuthOffsetMicrosteps); AxisDirectionRA = AzimuthOffsetMicrosteps > 0 ? FORWARD : REVERSE; // !!!! BEWARE INERTIA FREE MOUNT AxisStatusRA = SLEWING; DEBUGF(DBG_SIMULATOR, "TimerHit - Tracking AxisSlewRateRA %lf AxisDirectionRA %d", AxisSlewRateRA, AxisDirectionRA); } else { // Nothing to do - stop the axis AxisStatusRA = STOPPED; // !!!! BEWARE INERTIA FREE MOUNT DEBUG(DBG_SIMULATOR, "TimerHit - Tracking nothing to do stopping RA axis"); } if (0 != AltitudeOffsetMicrosteps) { // Calculate the slewing rates needed to reach that position // at the correct time. AxisSlewRateDEC = std::abs(AltitudeOffsetMicrosteps); AxisDirectionDEC = AltitudeOffsetMicrosteps > 0 ? FORWARD : REVERSE; // !!!! BEWARE INERTIA FREE MOUNT AxisStatusDEC = SLEWING; DEBUGF(DBG_SIMULATOR, "TimerHit - Tracking AxisSlewRateDEC %lf AxisDirectionDEC %d", AxisSlewRateDEC, AxisDirectionDEC); } else { // Nothing to do - stop the axis AxisStatusDEC = STOPPED; // !!!! BEWARE INERTIA FREE MOUNT DEBUG(DBG_SIMULATOR, "TimerHit - Tracking nothing to do stopping DEC axis"); } break; } default: break; } TraceThisTick = false; } bool ScopeSim::updateLocation(double latitude, double longitude, double elevation) { UpdateLocation(latitude, longitude, elevation); return true; } libindi/examples/README0000664000175000017500000001042713263645557014174 0ustar jasemjasem************************************************* * INDI Developers Manual * Chapter 8: Tutorial ************************************************** Introduction -------------- The tutorials included in the INDI Library can be used with any INDI compatible client. The following instructions are provided to test the drivers with KStars. If you are running a different client, refer to your client documentation. Tutorials ---------- Four tutorials are presented to introduce developers to the INDI architecture. tutorial_one: We construct the most basic device. tutorial_two: We create a simple telescope simulator. tutorial_three: We create a simple CCD simulator, and establish a data channel with the client to transmit randomly generated FITS. tutorial_four: We demonstrate use of skeleton files to define properties. tutorial_five: We create two devices (Rain Detector and Dome) to demonstrate inter-driver communication in INDI. tutorial_six: We create a simple client to set the temperature of a CCD simulator (tutorial_three). tuturial_seven: Simple telescope simulator based on INDI Alignment Subsystem which is used to improve pointing performance of mounts. Usage -------- 1. Running cmake in the libindi directory should build the examples binary as well. ------------------------------------------------------------------------------------------ 2. Run KStars (v1.1 or above is required) Under KStars, click on the "device" menu, then click on the "device manager". Click on the "client" tab and then click on the "add" button. A dialog box will be displayed with three fields: name, host, and port. You can enter anything in the name field, for example "my device" or "tutorial". Enter "127.0.0.1" in the host name and "8000" in the port entry (without the quotes). This device will be saved in your client menu. So you only to do #2 once. ------------------------------------------------------------------------------------------ 3. In the libindi/examples/tutorial_xxx directory, run each tutorial $ indiserver -v -p 8000 ./tutorial_NUM where num is the tutorial number (tutorial_one, tutorial_two...etc) ------------------------------------------------------------------------------------------ For tutorial four, a skeleton file needs to be specified first. The tutorial skeleton file is located under examples/tutorial_four/tutorial_four_sk.xml Let's assume for this example that the full path to libindi sources is this: /home/jasem/indi/libindi, and full path to libindi build directory is this: /home/jasem/build/libindi Open a console and go to the build directory then type: $ cd examples/tutorial_four $ export INDISKEL=/home/jasem/indi/libindi/examples/tutorial_four/tutorial_four_sk.xml $ indiserver -v -p 8000 ./tutorial_four Now Tutorial Four shall run and load the Skeleton file. Usually skeleton files for drivers are stored under /usr/share/indi/ and end with _sk.xml ------------------------------------------------------------------------------------------ For the inter-driver communications tutorial where it shows have a dome driver and a rain detector driver share information between each other, type the folliwng: $ indiserver -v -p 8000 ./tutorial_dome ./tutorial_rain ------------------------------------------------------------------------------------------ For tutorial_client. Run tutorial_three (Simple CCD) first: $ indiserver -v ./tutorial_three Then open another console tab, and run the client: $ ./tutorial_client You can connect to tutorial_three via a GUI client (e.g. KStars) before running tutorial_client. This will enable you to watch changes in the driver as they occur from tutorial_client. Make sure to set the port to 7624 in KStars for this tutorial. ------------------------------------------------------------------------------------------ 4. In KStars, go the device menu --> device manager --> client and select the host you added in step #2 and hit connect. ------------------------------------------------------------------------------------------ 5. The connection icon should be changed to connected, otherwise, an error message will be displayed. ------------------------------------------------------------------------------------------ 6. Close the device manager, and open the INDI Control Panel in the device menu. You can control your device from there. libindi/examples/tutorial_five/0000775000175000017500000000000013263645557016164 5ustar jasemjasemlibindi/examples/tutorial_five/raindetector.cpp0000664000175000017500000001221013263645557021347 0ustar jasemjasem/* INDI Developers Manual Tutorial #5 - Snooping Rain Detector Refer to README, which contains instruction on how to build this driver, and use it with an INDI-compatible client. */ /** \file raindetector.cpp \brief Construct a rain detector device that the user may operate to raise a rain alert. This rain light property defined by this driver is \e snooped by the Dome driver then takes whatever appropiate action to protect the dome. \author Jasem Mutlaq */ #include "raindetector.h" #include #include std::unique_ptr rainDetector(new RainDetector()); void ISGetProperties(const char *dev) { rainDetector->ISGetProperties(dev); } void ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) { rainDetector->ISNewSwitch(dev, name, states, names, n); } void ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n) { rainDetector->ISNewText(dev, name, texts, names, n); } void ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) { rainDetector->ISNewNumber(dev, name, values, names, n); } void ISNewBLOB(const char *dev, const char *name, int sizes[], int blobsizes[], char *blobs[], char *formats[], char *names[], int n) { INDI_UNUSED(dev); INDI_UNUSED(name); INDI_UNUSED(sizes); INDI_UNUSED(blobsizes); INDI_UNUSED(blobs); INDI_UNUSED(formats); INDI_UNUSED(names); INDI_UNUSED(n); } void ISSnoopDevice(XMLEle *root) { rainDetector->ISSnoopDevice(root); } /************************************************************************************** ** Client is asking us to establish connection to the device ***************************************************************************************/ bool RainDetector::Connect() { IDMessage(getDeviceName(), "Rain Detector connected successfully!"); return true; } /************************************************************************************** ** Client is asking us to terminate connection to the device ***************************************************************************************/ bool RainDetector::Disconnect() { IDMessage(getDeviceName(), "Rain Detector disconnected successfully!"); return true; } /************************************************************************************** ** INDI is asking us for our default device name ***************************************************************************************/ const char *RainDetector::getDefaultName() { return "Rain Detector"; } /************************************************************************************** ** INDI is asking us to init our properties. ***************************************************************************************/ bool RainDetector::initProperties() { // Must init parent properties first! INDI::DefaultDevice::initProperties(); IUFillLight(&RainL[0], "Status", "", IPS_IDLE); IUFillLightVector(&RainLP, RainL, 1, getDeviceName(), "Rain Alert", "", MAIN_CONTROL_TAB, IPS_IDLE); IUFillSwitch(&RainS[0], "On", "", ISS_OFF); IUFillSwitch(&RainS[1], "Off", "", ISS_OFF); IUFillSwitchVector(&RainSP, RainS, 2, getDeviceName(), "Control Rain", "", MAIN_CONTROL_TAB, IP_RW, ISR_1OFMANY, 0, IPS_IDLE); return true; } /******************************************************************************************** ** INDI is asking us to update the properties because there is a change in CONNECTION status ** This fucntion is called whenever the device is connected or disconnected. *********************************************************************************************/ bool RainDetector::updateProperties() { // Call parent update properties first INDI::DefaultDevice::updateProperties(); if (isConnected()) { defineLight(&RainLP); defineSwitch(&RainSP); } else // We're disconnected { deleteProperty(RainLP.name); deleteProperty(RainSP.name); } return true; } /******************************************************************************************** ** Client is asking us to update a switch *********************************************************************************************/ bool RainDetector::ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) { if (dev != nullptr && strcmp(dev, getDeviceName()) == 0) { if (strcmp(name, RainSP.name) == 0) { IUUpdateSwitch(&RainSP, states, names, n); if (RainS[0].s == ISS_ON) { RainL[0].s = IPS_ALERT; RainLP.s = IPS_ALERT; IDSetLight(&RainLP, "Alert! Alert! Rain detected!"); } else { RainL[0].s = IPS_IDLE; RainLP.s = IPS_OK; IDSetLight(&RainLP, "Rain threat passed. The skies are clear."); } RainSP.s = IPS_OK; IDSetSwitch(&RainSP, nullptr); return true; } } return INDI::DefaultDevice::ISNewSwitch(dev, name, states, names, n); } libindi/examples/tutorial_five/CMakeLists.txt0000664000175000017500000000045113263645557020724 0ustar jasemjasem########### Tutorial five - dome driver ############## add_executable(tutorial_dome dome.cpp) target_link_libraries(tutorial_dome indidriver) ########### Tutorial five - rain driver ############## add_executable(tutorial_rain raindetector.cpp) target_link_libraries(tutorial_rain indidriver) libindi/examples/tutorial_five/raindetector.h0000664000175000017500000000223413263645557021021 0ustar jasemjasem/* INDI Developers Manual Tutorial #5 - Snooping Rain Detector Refer to README, which contains instruction on how to build this driver, and use it with an INDI-compatible client. */ /** \file raindetector.h * \brief Construct a rain detector device that the user may operate to raise a rain alert. This rain light property defined by this driver is \e snooped by the Dome driver * then takes whatever appropiate action to protect the dome. * \author Jasem Mutlaq * * \example raindetector.h * The rain detector emits a signal each time it detects raid. This signal is \e snooped by the dome driver. */ #pragma once #include "defaultdevice.h" class RainDetector : public INDI::DefaultDevice { public: RainDetector() = default; bool ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n); protected: // General device functions bool Connect(); bool Disconnect(); const char *getDefaultName(); bool initProperties(); bool updateProperties(); private: ILight RainL[1]; ILightVectorProperty RainLP; ISwitch RainS[2]; ISwitchVectorProperty RainSP; }; libindi/examples/tutorial_five/dome.cpp0000664000175000017500000002003213263645557017611 0ustar jasemjasem/* INDI Developers Manual Tutorial #5 - Snooping Dome Refer to README, which contains instruction on how to build this driver, and use it with an INDI-compatible client. */ /** \file dome.cpp \brief Construct a dome device that the user may operate to open or close the dome shutter door. This driver is \e snooping on the Rain Detector rain property status. If rain property state is alert, we close the dome shutter door if it is open, and we prevent the user from opening it until the rain threat passes. \author Jasem Mutlaq \example dome.cpp The dome driver \e snoops on the rain detector signal and watches whether rain is detected or not. If it is detector and the dome is closed, it performs no action, but it also prevents you from opening the dome due to rain. If the dome is open, it will automatically starts closing the shutter. In order snooping to work, both drivers must be started by the same indiserver (or chained INDI servers): \code indiserver tutorial_dome tutorial_rain \endcode The dome driver keeps a copy of RainL light property from the rain driver. This makes it easy to parse the property status once an update from the rain driver arrives in the dome driver. Alternatively, you can directly parse the XML root element in ISSnoopDevice(XMLEle *root) to extract the required data. */ #include "dome.h" #include #include #include std::unique_ptr dome(new Dome()); void ISGetProperties(const char *dev) { dome->ISGetProperties(dev); } void ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) { dome->ISNewSwitch(dev, name, states, names, n); } void ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n) { dome->ISNewText(dev, name, texts, names, n); } void ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) { dome->ISNewNumber(dev, name, values, names, n); } void ISNewBLOB(const char *dev, const char *name, int sizes[], int blobsizes[], char *blobs[], char *formats[], char *names[], int n) { INDI_UNUSED(dev); INDI_UNUSED(name); INDI_UNUSED(sizes); INDI_UNUSED(blobsizes); INDI_UNUSED(blobs); INDI_UNUSED(formats); INDI_UNUSED(names); INDI_UNUSED(n); } void ISSnoopDevice(XMLEle *root) { dome->ISSnoopDevice(root); } /************************************************************************************** ** Client is asking us to establish connection to the device ***************************************************************************************/ bool Dome::Connect() { IDMessage(getDeviceName(), "Dome connected successfully!"); return true; } /************************************************************************************** ** Client is asking us to terminate connection to the device ***************************************************************************************/ bool Dome::Disconnect() { IDMessage(getDeviceName(), "Dome disconnected successfully!"); return true; } /************************************************************************************** ** INDI is asking us for our default device name ***************************************************************************************/ const char *Dome::getDefaultName() { return "Dome"; } /************************************************************************************** ** INDI is asking us to init our properties. ***************************************************************************************/ bool Dome::initProperties() { // Must init parent properties first! INDI::DefaultDevice::initProperties(); IUFillSwitch(&ShutterS[0], "Open", "", ISS_ON); IUFillSwitch(&ShutterS[1], "Close", "", ISS_OFF); IUFillSwitchVector(&ShutterSP, ShutterS, 2, getDeviceName(), "Shutter Door", "", MAIN_CONTROL_TAB, IP_RW, ISR_1OFMANY, 0, IPS_IDLE); // We init here the property we wish to "snoop" from the target device IUFillLight(&RainL[0], "Status", "", IPS_IDLE); // Make sure to set the device name to "Rain Detector" since we are snooping on rain detector device. IUFillLightVector(&RainLP, RainL, 1, "Rain Detector", "Rain Alert", "", MAIN_CONTROL_TAB, IPS_IDLE); return true; } /******************************************************************************************** ** INDI is asking us to update the properties because there is a change in CONNECTION status ** This fucntion is called whenever the device is connected or disconnected. *********************************************************************************************/ bool Dome::updateProperties() { // Call parent update properties first INDI::DefaultDevice::updateProperties(); if (isConnected()) { defineSwitch(&ShutterSP); /* Let's listen for Rain Alert property in the device Rain */ IDSnoopDevice("Rain Detector", "Rain Alert"); } else // We're disconnected deleteProperty(ShutterSP.name); return true; } /******************************************************************************************** ** Client is asking us to update a switch *********************************************************************************************/ bool Dome::ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) { if (dev != nullptr && strcmp(dev, getDeviceName()) == 0) { if (strcmp(name, ShutterSP.name) == 0) { IUUpdateSwitch(&ShutterSP, states, names, n); ShutterSP.s = IPS_BUSY; if (ShutterS[0].s == ISS_ON) { if (RainL[0].s == IPS_ALERT) { ShutterSP.s = IPS_ALERT; ShutterS[0].s = ISS_OFF; ShutterS[1].s = ISS_ON; IDSetSwitch(&ShutterSP, "It is raining, cannot open Shutter."); return true; } IDSetSwitch(&ShutterSP, "Shutter is opening."); } else IDSetSwitch(&ShutterSP, "Shutter is closing."); sleep(5); ShutterSP.s = IPS_OK; if (ShutterS[0].s == ISS_ON) IDSetSwitch(&ShutterSP, "Shutter is open."); else IDSetSwitch(&ShutterSP, "Shutter is closed."); return true; } } return INDI::DefaultDevice::ISNewSwitch(dev, name, states, names, n); } /******************************************************************************************** ** We received snooped property update from rain detector device *********************************************************************************************/ bool Dome::ISSnoopDevice(XMLEle *root) { IPState old_state = RainL[0].s; /* If the "Rain Alert" property gets updated in the Rain device, we will receive a notification. We need to process the new values of Rain Alert and update the local version of the property.*/ if (IUSnoopLight(root, &RainLP) == 0) { // If the dome is connected and rain is Alert */ if (RainL[0].s == IPS_ALERT) { // If dome is open, then close it */ if (ShutterS[0].s == ISS_ON) closeShutter(); else IDMessage(getDeviceName(), "Rain Alert Detected! Dome is already closed."); } else if (old_state == IPS_ALERT && RainL[0].s != IPS_ALERT) IDMessage(getDeviceName(), "Rain threat passed. Opening the dome is now safe."); return true; } return false; } /******************************************************************************************** ** Close shutter *********************************************************************************************/ void Dome::closeShutter() { ShutterSP.s = IPS_BUSY; IDSetSwitch(&ShutterSP, "Rain Alert! Shutter is closing..."); sleep(5); ShutterS[0].s = ISS_OFF; ShutterS[1].s = ISS_ON; ShutterSP.s = IPS_OK; IDSetSwitch(&ShutterSP, "Shutter is closed."); } libindi/examples/tutorial_five/dome.h0000664000175000017500000000364413263645557017270 0ustar jasemjasem/* INDI Developers Manual Tutorial #5 - Snooping Dome Refer to README, which contains instruction on how to build this driver, and use it with an INDI-compatible client. */ /** \file dome.h \brief Construct a dome device that the user may operate to open or close the dome shutter door. This driver is \e snooping on the Rain Detector rain property status. If rain property state is alert, we close the dome shutter door if it is open, and we prevent the user from opening it until the rain threat passes. \author Jasem Mutlaq \example dome.h The dome driver \e snoops on the rain detector signal and watches whether rain is detected or not. If it is detector and the dome is closed, it performs no action, but it also prevents you from opening the dome due to rain. If the dome is open, it will automatically starts closing the shutter. In order snooping to work, both drivers must be started by the same indiserver (or chained INDI servers): \code indiserver tutorial_dome tutorial_rain \endcode The dome driver keeps a copy of RainL light property from the rain driver. This makes it easy to parse the property status once an update from the rain driver arrives in the dome driver. Alternatively, you can directly parse the XML root element in ISSnoopDevice(XMLEle *root) to extract the required data. */ #pragma once #include "defaultdevice.h" class Dome : public INDI::DefaultDevice { public: Dome() = default; bool ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n); bool ISSnoopDevice(XMLEle *root); protected: // General device functions bool Connect(); bool Disconnect(); const char *getDefaultName(); bool initProperties(); bool updateProperties(); private: void closeShutter(); ISwitch ShutterS[2]; ISwitchVectorProperty ShutterSP; ILight RainL[1]; ILightVectorProperty RainLP; }; libindi/examples/tutorial_three/0000775000175000017500000000000013263645557016342 5ustar jasemjasemlibindi/examples/tutorial_three/CMakeLists.txt0000664000175000017500000000026313263645557021103 0ustar jasemjasem########### Tutorial three ############## if (CFITSIO_FOUND) add_executable(tutorial_three simpleccd.cpp) target_link_libraries(tutorial_three indidriver) endif (CFITSIO_FOUND) libindi/examples/tutorial_three/simpleccd.cpp0000664000175000017500000002271413263645557021017 0ustar jasemjasem/* INDI Developers Manual Tutorial #3 "Simple CCD Driver" We develop a simple CCD driver. Refer to README, which contains instruction on how to build this driver, and use it with an INDI-compatible client. */ /** \file simpleccd.cpp \brief Construct a basic INDI CCD device that simulates exposure & temperature settings. It also generates a random pattern and uploads it as a FITS file. \author Jasem Mutlaq \example simpleccd.cpp A simple CCD device that can capture images and control temperature. It returns a FITS image to the client. To build drivers for complex CCDs, please refer to the INDI Generic CCD driver template in INDI SVN (under 3rdparty). */ #include "simpleccd.h" #include /* Macro shortcut to CCD temperature value */ #define currentCCDTemperature TemperatureN[0].value std::unique_ptr simpleCCD(new SimpleCCD()); void ISGetProperties(const char *dev) { simpleCCD->ISGetProperties(dev); } void ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) { simpleCCD->ISNewSwitch(dev, name, states, names, n); } void ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n) { simpleCCD->ISNewText(dev, name, texts, names, n); } void ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) { simpleCCD->ISNewNumber(dev, name, values, names, n); } void ISNewBLOB(const char *dev, const char *name, int sizes[], int blobsizes[], char *blobs[], char *formats[], char *names[], int n) { INDI_UNUSED(dev); INDI_UNUSED(name); INDI_UNUSED(sizes); INDI_UNUSED(blobsizes); INDI_UNUSED(blobs); INDI_UNUSED(formats); INDI_UNUSED(names); INDI_UNUSED(n); } void ISSnoopDevice(XMLEle *root) { simpleCCD->ISSnoopDevice(root); } /************************************************************************************** ** Client is asking us to establish connection to the device ***************************************************************************************/ bool SimpleCCD::Connect() { IDMessage(getDeviceName(), "Simple CCD connected successfully!"); // Let's set a timer that checks teleCCDs status every POLLMS milliseconds. SetTimer(POLLMS); return true; } /************************************************************************************** ** Client is asking us to terminate connection to the device ***************************************************************************************/ bool SimpleCCD::Disconnect() { IDMessage(getDeviceName(), "Simple CCD disconnected successfully!"); return true; } /************************************************************************************** ** INDI is asking us for our default device name ***************************************************************************************/ const char *SimpleCCD::getDefaultName() { return "Simple CCD"; } /************************************************************************************** ** INDI is asking us to init our properties. ***************************************************************************************/ bool SimpleCCD::initProperties() { // Must init parent properties first! INDI::CCD::initProperties(); // We set the CCD capabilities uint32_t cap = CCD_CAN_ABORT | CCD_CAN_BIN | CCD_CAN_SUBFRAME | CCD_HAS_COOLER | CCD_HAS_SHUTTER; SetCCDCapability(cap); // Add Debug, Simulator, and Configuration controls addAuxControls(); setDefaultPollingPeriod(500); return true; } /******************************************************************************************** ** INDI is asking us to update the properties because there is a change in CONNECTION status ** This fucntion is called whenever the device is connected or disconnected. *********************************************************************************************/ bool SimpleCCD::updateProperties() { // Call parent update properties first INDI::CCD::updateProperties(); if (isConnected()) { // Let's get parameters now from CCD setupParams(); // Start the timer SetTimer(POLLMS); } return true; } /************************************************************************************** ** Setting up CCD parameters ***************************************************************************************/ void SimpleCCD::setupParams() { // Our CCD is an 8 bit CCD, 1280x1024 resolution, with 5.4um square pixels. SetCCDParams(1280, 1024, 8, 5.4, 5.4); // Let's calculate how much memory we need for the primary CCD buffer int nbuf; nbuf = PrimaryCCD.getXRes() * PrimaryCCD.getYRes() * PrimaryCCD.getBPP() / 8; nbuf += 512; // leave a little extra at the end PrimaryCCD.setFrameBufferSize(nbuf); } /************************************************************************************** ** Client is asking us to start an exposure ***************************************************************************************/ bool SimpleCCD::StartExposure(float duration) { ExposureRequest = duration; // Since we have only have one CCD with one chip, we set the exposure duration of the primary CCD PrimaryCCD.setExposureDuration(duration); gettimeofday(&ExpStart, nullptr); InExposure = true; // We're done return true; } /************************************************************************************** ** Client is asking us to abort an exposure ***************************************************************************************/ bool SimpleCCD::AbortExposure() { InExposure = false; return true; } /************************************************************************************** ** Client is asking us to set a new temperature ***************************************************************************************/ int SimpleCCD::SetTemperature(double temperature) { TemperatureRequest = temperature; // 0 means it will take a while to change the temperature return 0; } /************************************************************************************** ** How much longer until exposure is done? ***************************************************************************************/ float SimpleCCD::CalcTimeLeft() { double timesince; double timeleft; struct timeval now { 0, 0 }; gettimeofday(&now, nullptr); timesince = (double)(now.tv_sec * 1000.0 + now.tv_usec / 1000) - (double)(ExpStart.tv_sec * 1000.0 + ExpStart.tv_usec / 1000); timesince = timesince / 1000; timeleft = ExposureRequest - timesince; return timeleft; } /************************************************************************************** ** Main device loop. We check for exposure and temperature progress here ***************************************************************************************/ void SimpleCCD::TimerHit() { long timeleft; if (!isConnected()) return; // No need to reset timer if we are not connected anymore if (InExposure) { timeleft = CalcTimeLeft(); // Less than a 0.1 second away from exposure completion // This is an over simplified timing method, check CCDSimulator and simpleCCD for better timing checks if (timeleft < 0.1) { /* We're done exposing */ IDMessage(getDeviceName(), "Exposure done, downloading image..."); // Set exposure left to zero PrimaryCCD.setExposureLeft(0); // We're no longer exposing... InExposure = false; /* grab and save image */ grabImage(); } else // Just update time left in client PrimaryCCD.setExposureLeft(timeleft); } // TemperatureNP is defined in INDI::CCD switch (TemperatureNP.s) { case IPS_IDLE: case IPS_OK: break; case IPS_BUSY: /* If target temperature is higher, then increase current CCD temperature */ if (currentCCDTemperature < TemperatureRequest) currentCCDTemperature++; /* If target temperature is lower, then decrese current CCD temperature */ else if (currentCCDTemperature > TemperatureRequest) currentCCDTemperature--; /* If they're equal, stop updating */ else { TemperatureNP.s = IPS_OK; IDSetNumber(&TemperatureNP, "Target temperature reached."); break; } IDSetNumber(&TemperatureNP, nullptr); break; case IPS_ALERT: break; } SetTimer(POLLMS); } /************************************************************************************** ** Create a random image and return it to client ***************************************************************************************/ void SimpleCCD::grabImage() { // Let's get a pointer to the frame buffer uint8_t *image = PrimaryCCD.getFrameBuffer(); // Get width and height int width = PrimaryCCD.getSubW() / PrimaryCCD.getBinX() * PrimaryCCD.getBPP() / 8; int height = PrimaryCCD.getSubH() / PrimaryCCD.getBinY(); // Fill buffer with random pattern for (int i = 0; i < height; i++) for (int j = 0; j < width; j++) image[i * width + j] = rand() % 255; IDMessage(getDeviceName(), "Download complete."); // Let INDI::CCD know we're done filling the image buffer ExposureComplete(&PrimaryCCD); } libindi/examples/tutorial_three/simpleccd.h0000664000175000017500000000266213263645557020464 0ustar jasemjasem/* INDI Developers Manual Tutorial #3 "Simple CCD Driver" We develop a simple CCD driver. Refer to README, which contains instruction on how to build this driver, and use it with an INDI-compatible client. */ /** \file simpleccd.h \brief Construct a basic INDI CCD device that simulates exposure & temperature settings. It also generates a random pattern and uploads it as a FITS file. \author Jasem Mutlaq \example simpleccd.h A simple CCD device that can capture images and control temperature. It returns a FITS image to the client. To build drivers for complex CCDs, please refer to the INDI Generic CCD driver template in INDI SVN (under 3rdparty). */ #pragma once #include "indiccd.h" class SimpleCCD : public INDI::CCD { public: SimpleCCD() = default; protected: // General device functions bool Connect(); bool Disconnect(); const char *getDefaultName(); bool initProperties(); bool updateProperties(); // CCD specific functions bool StartExposure(float duration); bool AbortExposure(); int SetTemperature(double temperature); void TimerHit(); private: // Utility functions float CalcTimeLeft(); void setupParams(); void grabImage(); // Are we exposing? bool InExposure { false }; // Struct to keep timing struct timeval ExpStart { 0, 0 }; float ExposureRequest { 0 }; float TemperatureRequest { 0 }; }; libindi/examples/tutorial_six/0000775000175000017500000000000013263645557016036 5ustar jasemjasemlibindi/examples/tutorial_six/CMakeLists.txt0000664000175000017500000000040713263645557020577 0ustar jasemjasem########### Tutorial Six ############## add_executable(tutorial_client tutorial_client.cpp) target_link_libraries(tutorial_client indiclient ${ZLIB_LIBRARY} ${NOVA_LIBRARIES}) IF (UNIX AND NOT APPLE) target_link_libraries(tutorial_client -lpthread) ENDIF () libindi/examples/tutorial_six/tutorial_client.h0000664000175000017500000000456413263645557021421 0ustar jasemjasem/* Tutorial Client Copyright (C) 2010 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 */ #pragma once /** \file tutorial_client.h \brief Construct a basic INDI client that demonstrates INDI::Client capabilities. This client must be used with tutorial_three device "Simple CCD". \author Jasem Mutlaq \example tutorial_client.h Construct a basic INDI client that demonstrates INDI::Client capabilities. This client must be used with tutorial_three device "Simple CCD". To run the example, you must first run tutorial_three: \code indiserver tutorial_three \endcode Then in another terminal, run the client: \code tutorial_client \endcode The client will connect to the CCD driver and attempts to change the CCD temperature. */ #include "baseclient.h" class MyClient : public INDI::BaseClient { public: MyClient(); ~MyClient() = default; void setTemperature(); void takeExposure(); protected: virtual void newDevice(INDI::BaseDevice *dp); virtual void removeDevice(INDI::BaseDevice */*dp*/) {} virtual void newProperty(INDI::Property *property); virtual void removeProperty(INDI::Property */*property*/) {} virtual void newBLOB(IBLOB *bp); virtual void newSwitch(ISwitchVectorProperty */*svp*/) {} virtual void newNumber(INumberVectorProperty *nvp); virtual void newMessage(INDI::BaseDevice *dp, int messageID); virtual void newText(ITextVectorProperty */*tvp*/) {} virtual void newLight(ILightVectorProperty */*lvp*/) {} virtual void serverConnected() {} virtual void serverDisconnected(int /*exit_code*/) {} private: INDI::BaseDevice *ccd_simulator; }; libindi/examples/tutorial_six/tutorial_client.cpp0000664000175000017500000001400213263645557021740 0ustar jasemjasem#if 0 Simple Client Tutorial Demonstration of libindi v0.7 capabilities. Copyright (C) 2010 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 /** \file tutorial_client.cpp \brief Construct a basic INDI client that demonstrates INDI::Client capabilities. This client must be used with tutorial_three device "Simple CCD". \author Jasem Mutlaq \example tutorial_client.cpp Construct a basic INDI client that demonstrates INDI::Client capabilities. This client must be used with tutorial_three device "Simple CCD". To run the example, you must first run tutorial_three: \code indiserver tutorial_three \endcode Then in another terminal, run the client: \code tutorial_client \endcode The client will connect to the CCD driver and attempts to change the CCD temperature. */ #include "tutorial_client.h" #include "indibase/basedevice.h" #include #include #include #include #define MYCCD "Simple CCD" /* Our client auto pointer */ std::unique_ptr camera_client(new MyClient()); int main(int /*argc*/, char **/*argv*/) { camera_client->setServer("localhost", 7624); camera_client->watchDevice(MYCCD); camera_client->connectServer(); camera_client->setBLOBMode(B_ALSO, MYCCD, nullptr); std::cout << "Press any key to terminate the client.\n"; std::string term; std::cin >> term; } /************************************************************************************** ** ***************************************************************************************/ MyClient::MyClient() { ccd_simulator = nullptr; } /************************************************************************************** ** ***************************************************************************************/ void MyClient::setTemperature() { INumberVectorProperty *ccd_temperature = nullptr; ccd_temperature = ccd_simulator->getNumber("CCD_TEMPERATURE"); if (ccd_temperature == nullptr) { IDLog("Error: unable to find CCD Simulator CCD_TEMPERATURE property...\n"); return; } ccd_temperature->np[0].value = -20; sendNewNumber(ccd_temperature); } /************************************************************************************** ** ***************************************************************************************/ void MyClient::takeExposure() { INumberVectorProperty *ccd_exposure = nullptr; ccd_exposure = ccd_simulator->getNumber("CCD_EXPOSURE"); if (ccd_exposure == nullptr) { IDLog("Error: unable to find CCD Simulator CCD_EXPOSURE property...\n"); return; } // Take a 1 second exposure IDLog("Taking a 1 second exposure.\n"); ccd_exposure->np[0].value = 1; sendNewNumber(ccd_exposure); } /************************************************************************************** ** ***************************************************************************************/ void MyClient::newDevice(INDI::BaseDevice *dp) { if (strcmp(dp->getDeviceName(), MYCCD) == 0) IDLog("Receiving %s Device...\n", dp->getDeviceName()); ccd_simulator = dp; } /************************************************************************************** ** *************************************************************************************/ void MyClient::newProperty(INDI::Property *property) { if (strcmp(property->getDeviceName(), MYCCD) == 0 && strcmp(property->getName(), "CONNECTION") == 0) { connectDevice(MYCCD); return; } if (strcmp(property->getDeviceName(), MYCCD) == 0 && strcmp(property->getName(), "CCD_TEMPERATURE") == 0) { if (ccd_simulator->isConnected()) { IDLog("CCD is connected. Setting temperature to -20 C.\n"); setTemperature(); } return; } } /************************************************************************************** ** ***************************************************************************************/ void MyClient::newNumber(INumberVectorProperty *nvp) { // Let's check if we get any new values for CCD_TEMPERATURE if (strcmp(nvp->name, "CCD_TEMPERATURE") == 0) { IDLog("Receving new CCD Temperature: %g C\n", nvp->np[0].value); if (nvp->np[0].value == -20) { IDLog("CCD temperature reached desired value!\n"); takeExposure(); } } } /************************************************************************************** ** ***************************************************************************************/ void MyClient::newMessage(INDI::BaseDevice *dp, int messageID) { if (strcmp(dp->getDeviceName(), MYCCD) != 0) return; IDLog("Recveing message from Server:\n\n########################\n%s\n########################\n\n", dp->messageQueue(messageID).c_str()); } /************************************************************************************** ** ***************************************************************************************/ void MyClient::newBLOB(IBLOB *bp) { // Save FITS file to disk std::ofstream myfile; myfile.open("ccd_simulator.fits", std::ios::out | std::ios::binary); myfile.write(static_cast(bp->blob), bp->bloblen); myfile.close(); IDLog("Received image, saved as ccd_simulator.fits\n"); } libindi/examples/tutorial_two/0000775000175000017500000000000013263645557016044 5ustar jasemjasemlibindi/examples/tutorial_two/CMakeLists.txt0000664000175000017500000000020613263645557020602 0ustar jasemjasem########## Tutorial two ############## add_executable(tutorial_two simplescope.cpp) target_link_libraries(tutorial_two indidriver) libindi/examples/tutorial_two/simplescope.cpp0000664000175000017500000001760013263645557021077 0ustar jasemjasem/* INDI Developers Manual Tutorial #2 "Simple Telescope Driver" We develop a simple telescope simulator. Refer to README, which contains instruction on how to build this driver, and use it with an INDI-compatible client. */ /** \file simplescope.cpp \brief Construct a basic INDI telescope device that simulates GOTO commands. \author Jasem Mutlaq \example simplescope.cpp A simple GOTO telescope that simulator slewing operation. */ #include "simplescope.h" #include "indicom.h" #include #include //const float SIDE_RATE = 0.004178; /* sidereal rate, degrees/s */ const int SLEW_RATE = 1; /* slew rate, degrees/s */ std::unique_ptr simpleScope(new SimpleScope()); /************************************************************************************** ** Return properties of device. ***************************************************************************************/ void ISGetProperties(const char *dev) { simpleScope->ISGetProperties(dev); } /************************************************************************************** ** Process new switch from client ***************************************************************************************/ void ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) { simpleScope->ISNewSwitch(dev, name, states, names, n); } /************************************************************************************** ** Process new text from client ***************************************************************************************/ void ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n) { simpleScope->ISNewText(dev, name, texts, names, n); } /************************************************************************************** ** Process new number from client ***************************************************************************************/ void ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) { simpleScope->ISNewNumber(dev, name, values, names, n); } /************************************************************************************** ** Process new blob from client ***************************************************************************************/ void ISNewBLOB(const char *dev, const char *name, int sizes[], int blobsizes[], char *blobs[], char *formats[], char *names[], int n) { simpleScope->ISNewBLOB(dev, name, sizes, blobsizes, blobs, formats, names, n); } /************************************************************************************** ** Process snooped property from another driver ***************************************************************************************/ void ISSnoopDevice(XMLEle *root) { INDI_UNUSED(root); } SimpleScope::SimpleScope() { currentRA = 0; currentDEC = 90; // We add an additional debug level so we can log verbose scope status DBG_SCOPE = INDI::Logger::getInstance().addDebugLevel("Scope Verbose", "SCOPE"); SetTelescopeCapability(TELESCOPE_CAN_ABORT); } /************************************************************************************** ** We init our properties here. The only thing we want to init are the Debug controls ***************************************************************************************/ bool SimpleScope::initProperties() { // ALWAYS call initProperties() of parent first INDI::Telescope::initProperties(); addDebugControl(); return true; } /************************************************************************************** ** INDI is asking us to check communication with the device via a handshake ***************************************************************************************/ bool SimpleScope::Handshake() { // When communicating with a real mount, we check here if commands are receieved // and acknolowedged by the mount. For SimpleScope, we simply return true. return true; } /************************************************************************************** ** INDI is asking us for our default device name ***************************************************************************************/ const char *SimpleScope::getDefaultName() { return "Simple Scope"; } /************************************************************************************** ** Client is asking us to slew to a new position ***************************************************************************************/ bool SimpleScope::Goto(double ra, double dec) { targetRA = ra; targetDEC = dec; char RAStr[64]={0}, DecStr[64]={0}; // Parse the RA/DEC into strings fs_sexa(RAStr, targetRA, 2, 3600); fs_sexa(DecStr, targetDEC, 2, 3600); // Mark state as slewing TrackState = SCOPE_SLEWING; // Inform client we are slewing to a new position LOGF_INFO("Slewing to RA: %s - DEC: %s", RAStr, DecStr); // Success! return true; } /************************************************************************************** ** Client is asking us to abort our motion ***************************************************************************************/ bool SimpleScope::Abort() { return true; } /************************************************************************************** ** Client is asking us to report telescope status ***************************************************************************************/ bool SimpleScope::ReadScopeStatus() { static struct timeval ltv { 0, 0 }; struct timeval tv { 0, 0 }; double dt = 0, da_ra = 0, da_dec = 0, dx = 0, dy = 0; int nlocked; /* update elapsed time since last poll, don't presume exactly POLLMS */ gettimeofday(&tv, nullptr); if (ltv.tv_sec == 0 && ltv.tv_usec == 0) ltv = tv; dt = tv.tv_sec - ltv.tv_sec + (tv.tv_usec - ltv.tv_usec) / 1e6; ltv = tv; // Calculate how much we moved since last time da_ra = SLEW_RATE * dt; da_dec = SLEW_RATE * dt; /* Process per current state. We check the state of EQUATORIAL_EOD_COORDS_REQUEST and act acoordingly */ switch (TrackState) { case SCOPE_SLEWING: // Wait until we are "locked" into positon for both RA & DEC axis nlocked = 0; // Calculate diff in RA dx = targetRA - currentRA; // If diff is very small, i.e. smaller than how much we changed since last time, then we reached target RA. if (fabs(dx) * 15. <= da_ra) { currentRA = targetRA; nlocked++; } // Otherwise, increase RA else if (dx > 0) currentRA += da_ra / 15.; // Otherwise, decrease RA else currentRA -= da_ra / 15.; // Calculate diff in DEC dy = targetDEC - currentDEC; // If diff is very small, i.e. smaller than how much we changed since last time, then we reached target DEC. if (fabs(dy) <= da_dec) { currentDEC = targetDEC; nlocked++; } // Otherwise, increase DEC else if (dy > 0) currentDEC += da_dec; // Otherwise, decrease DEC else currentDEC -= da_dec; // Let's check if we recahed position for both RA/DEC if (nlocked == 2) { // Let's set state to TRACKING TrackState = SCOPE_TRACKING; LOG_INFO("Telescope slew is complete. Tracking..."); } break; default: break; } char RAStr[64]={0}, DecStr[64]={0}; // Parse the RA/DEC into strings fs_sexa(RAStr, currentRA, 2, 3600); fs_sexa(DecStr, currentDEC, 2, 3600); DEBUGF(DBG_SCOPE, "Current RA: %s Current DEC: %s", RAStr, DecStr); NewRaDec(currentRA, currentDEC); return true; } libindi/examples/tutorial_two/simplescope.h0000664000175000017500000000164213263645557020543 0ustar jasemjasem/* INDI Developers Manual Tutorial #2 "Simple Telescope Driver" We develop a simple telescope simulator. Refer to README, which contains instruction on how to build this driver, and use it with an INDI-compatible client. */ /** \file simplescope.h \brief Construct a basic INDI telescope device that simulates GOTO commands. \author Jasem Mutlaq \example simplescope.h A simple GOTO telescope that simulator slewing operation. */ #pragma once #include "inditelescope.h" class SimpleScope : public INDI::Telescope { public: SimpleScope(); protected: bool Handshake(); const char *getDefaultName(); bool initProperties(); // Telescope specific functions bool ReadScopeStatus(); bool Goto(double, double); bool Abort(); private: double currentRA; double currentDEC; double targetRA; double targetDEC; unsigned int DBG_SCOPE; }; libindi/examples/tutorial_four/0000775000175000017500000000000013263645557016206 5ustar jasemjasemlibindi/examples/tutorial_four/simpleskeleton.h0000664000175000017500000000431013263645557021413 0ustar jasemjasem/* Tutorial Four Copyright (C) 2010 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 */ #pragma once /** \file simpleskeleton.h \brief Construct a basic INDI CCD device that demonstrates ability to define properties from a skeleton file. \author Jasem Mutlaq \example simpleskeleton.h A skeleton file is an external XML file with the driver properties already defined. This tutorial illustrates how to create a driver from a skeleton file and parse/process the properties. The skeleton file name is tutorial_four_sk.xml \note Please note that if you create your own skeleton file, you must append _sk postfix to your skeleton file name. */ #include "defaultdevice.h" class SimpleSkeleton : public INDI::DefaultDevice { public: SimpleSkeleton() = default; ~SimpleSkeleton() = default; virtual void ISGetProperties(const char *dev); virtual bool ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n); virtual bool ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n); virtual bool ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n); virtual bool ISNewBLOB(const char *dev, const char *name, int sizes[], int blobsizes[], char *blobs[], char *formats[], char *names[], int n); private: const char *getDefaultName(); virtual bool initProperties(); virtual bool Connect(); virtual bool Disconnect(); }; libindi/examples/tutorial_four/CMakeLists.txt0000664000175000017500000000021413263645557020743 0ustar jasemjasem########### Tutorial four ############## add_executable(tutorial_four simpleskeleton.cpp) target_link_libraries(tutorial_four indidriver) libindi/examples/tutorial_four/simpleskeleton.cpp0000664000175000017500000002400413263645557021750 0ustar jasemjasem#if 0 Simple Skeleton - Tutorial Four Demonstration of libindi v0.7 capabilities. Copyright (C) 2010 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 /** \file simpleskeleton.cpp \brief Construct a basic INDI CCD device that demonstrates ability to define properties from a skeleton file. \author Jasem Mutlaq \example simpleskeleton.cpp A skeleton file is an external XML file with the driver properties already defined. This tutorial illustrates how to create a driver from a skeleton file and parse/process the properties. The skeleton file name is tutorial_four_sk.xml \note Please note that if you create your own skeleton file, you must append _sk postfix to your skeleton file name. */ #include "simpleskeleton.h" #include #include #include #include /* Our simpleSkeleton auto pointer */ std::unique_ptr simpleSkeleton(new SimpleSkeleton()); //const int POLLMS = 1000; // Period of update, 1 second. /************************************************************************************** ** ***************************************************************************************/ void ISGetProperties(const char *dev) { simpleSkeleton->ISGetProperties(dev); } /************************************************************************************** ** ***************************************************************************************/ void ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) { simpleSkeleton->ISNewSwitch(dev, name, states, names, n); } /************************************************************************************** ** ***************************************************************************************/ void ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n) { simpleSkeleton->ISNewText(dev, name, texts, names, n); } /************************************************************************************** ** ***************************************************************************************/ void ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) { simpleSkeleton->ISNewNumber(dev, name, values, names, n); } /************************************************************************************** ** ***************************************************************************************/ void ISNewBLOB(const char *dev, const char *name, int sizes[], int blobsizes[], char *blobs[], char *formats[], char *names[], int n) { simpleSkeleton->ISNewBLOB(dev, name, sizes, blobsizes, blobs, formats, names, n); } /************************************************************************************** ** ***************************************************************************************/ void ISSnoopDevice(XMLEle *root) { INDI_UNUSED(root); } /************************************************************************************** ** Initialize all properties & set default values. **************************************************************************************/ bool SimpleSkeleton::initProperties() { DefaultDevice::initProperties(); // This is the default driver skeleton file location // Convention is: drivername_sk_xml // Default location is /usr/share/indi const char *skelFileName = "/usr/share/indi/tutorial_four_sk.xml"; struct stat st; char *skel = getenv("INDISKEL"); if (skel != nullptr) buildSkeleton(skel); else if (stat(skelFileName, &st) == 0) buildSkeleton(skelFileName); else IDLog( "No skeleton file was specified. Set environment variable INDISKEL to the skeleton path and try again.\n"); // Optional: Add aux controls for configuration, debug & simulation that get added in the Options tab // of the driver. addAuxControls(); std::vector *pAll = getProperties(); // Let's print a list of all device properties for (int i = 0; i < (int)pAll->size(); i++) IDLog("Property #%d: %s\n", i, pAll->at(i)->getName()); return true; } /************************************************************************************** ** Define Basic properties to clients. ***************************************************************************************/ void SimpleSkeleton::ISGetProperties(const char *dev) { static int configLoaded = 0; // Ask the default driver first to send properties. INDI::DefaultDevice::ISGetProperties(dev); // If no configuration is load before, then load it now. if (configLoaded == 0) { loadConfig(); configLoaded = 1; } } /************************************************************************************** ** Process Text properties ***************************************************************************************/ bool SimpleSkeleton::ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n) { INDI_UNUSED(name); INDI_UNUSED(texts); INDI_UNUSED(names); INDI_UNUSED(n); // Ignore if not ours if (dev != nullptr && strcmp(dev, getDeviceName()) != 0) return false; return false; } /************************************************************************************** ** ***************************************************************************************/ bool SimpleSkeleton::ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) { // Ignore if not ours if (dev != nullptr && strcmp(dev, getDeviceName()) != 0) return false; INumberVectorProperty *nvp = getNumber(name); if (nvp == nullptr) return false; if (!isConnected()) { nvp->s = IPS_ALERT; IDSetNumber(nvp, "Cannot change property while device is disconnected."); return false; } if (strcmp(nvp->name, "Number Property") != 0) { IUUpdateNumber(nvp, values, names, n); nvp->s = IPS_OK; IDSetNumber(nvp, nullptr); return true; } return false; } /************************************************************************************** ** ***************************************************************************************/ bool SimpleSkeleton::ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) { int lightState = 0; int lightIndex = 0; // ignore if not ours if (dev != nullptr && strcmp(dev, getDeviceName()) != 0) return false; if (INDI::DefaultDevice::ISNewSwitch(dev, name, states, names, n)) return true; ISwitchVectorProperty *svp = getSwitch(name); ILightVectorProperty *lvp = getLight("Light Property"); if (!isConnected()) { svp->s = IPS_ALERT; IDSetSwitch(svp, "Cannot change property while device is disconnected."); return false; } if (svp == nullptr || lvp == nullptr) return false; if (strcmp(svp->name, "Menu") == 0) { IUUpdateSwitch(svp, states, names, n); ISwitch *onSW = IUFindOnSwitch(svp); lightIndex = IUFindOnSwitchIndex(svp); if (lightIndex < 0 || lightIndex > lvp->nlp) return false; if (onSW != nullptr) { lightState = rand() % 4; svp->s = IPS_OK; lvp->s = IPS_OK; lvp->lp[lightIndex].s = (IPState)lightState; IDSetSwitch(svp, "Setting to switch %s is successful. Changing corresponding light property to %s.", onSW->name, pstateStr(lvp->lp[lightIndex].s)); IDSetLight(lvp, nullptr); } return true; } return false; } bool SimpleSkeleton::ISNewBLOB(const char *dev, const char *name, int sizes[], int blobsizes[], char *blobs[], char *formats[], char *names[], int n) { if (dev != nullptr && strcmp(dev, getDeviceName()) != 0) return false; IBLOBVectorProperty *bvp = getBLOB(name); if (bvp == nullptr) return false; if (!isConnected()) { bvp->s = IPS_ALERT; IDSetBLOB(bvp, "Cannot change property while device is disconnected."); return false; } if (strcmp(bvp->name, "BLOB Test") == 0) { IUUpdateBLOB(bvp, sizes, blobsizes, blobs, formats, names, n); IBLOB *bp = IUFindBLOB(bvp, names[0]); if (bp == nullptr) return false; IDLog("Received BLOB with name %s, format %s, and size %d, and bloblen %d\n", bp->name, bp->format, bp->size, bp->bloblen); char *blobBuffer = new char[bp->bloblen + 1]; strncpy(blobBuffer, ((char *)bp->blob), bp->bloblen); blobBuffer[bp->bloblen] = '\0'; IDLog("BLOB Content:\n##################################\n%s\n##################################\n", blobBuffer); delete[] blobBuffer; bp->size = 0; bvp->s = IPS_OK; IDSetBLOB(bvp, nullptr); } return true; } /************************************************************************************** ** ***************************************************************************************/ bool SimpleSkeleton::Connect() { return true; } bool SimpleSkeleton::Disconnect() { return true; } const char *SimpleSkeleton::getDefaultName() { return "Simple Skeleton"; } libindi/examples/tutorial_four/tutorial_four_sk.xml0000664000175000017500000000321413263645557022323 0ustar jasemjasem Off On 3 3 Idle Idle Idle Idle Idle On Off Off Off Off libindi/examples/tutorial_eight/0000775000175000017500000000000013263645557016333 5ustar jasemjasemlibindi/examples/tutorial_eight/CMakeLists.txt0000664000175000017500000000030413263645557021070 0ustar jasemjasem########### Tutorial Eight ############## if (CFITSIO_FOUND) add_executable(tutorial_eight simple_detector_simulator.cpp) target_link_libraries(tutorial_eight indidriver) endif (CFITSIO_FOUND) libindi/examples/tutorial_eight/simple_detector_simulator.cpp0000664000175000017500000002431713263645557024327 0ustar jasemjasem/* INDI Developers Manual Tutorial #3 "Simple Detector Driver" We develop a simple Detector driver. Refer to README, which contains instruction on how to build this driver, and use it with an INDI-compatible client. */ /** \file simpledetector.cpp \brief Construct a basic INDI Detector device that simulates exposure & temperature settings. It also generates a random pattern and uploads it as a FITS file. \author Ilia Platone, clearly taken from SimpleCCD by Jasem Mutlaq \example simpledetector.cpp A simple Detector device that can capture images and control temperature. It returns a FITS image to the client. To build drivers for complex Detectors, please refer to the INDI Generic Detector driver template in INDI SVN (under 3rdparty). */ #include "simple_detector_simulator.h" #include /* Macro shortcut to Detector temperature value */ #define currentDetectorTemperature TemperatureN[0].value std::unique_ptr simpleDetector(new SimpleDetector()); void ISGetProperties(const char *dev) { simpleDetector->ISGetProperties(dev); } void ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) { simpleDetector->ISNewSwitch(dev, name, states, names, n); } void ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n) { simpleDetector->ISNewText(dev, name, texts, names, n); } void ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) { simpleDetector->ISNewNumber(dev, name, values, names, n); } void ISNewBLOB(const char *dev, const char *name, int sizes[], int blobsizes[], char *blobs[], char *formats[], char *names[], int n) { INDI_UNUSED(dev); INDI_UNUSED(name); INDI_UNUSED(sizes); INDI_UNUSED(blobsizes); INDI_UNUSED(blobs); INDI_UNUSED(formats); INDI_UNUSED(names); INDI_UNUSED(n); } void ISSnoopDevice(XMLEle *root) { simpleDetector->ISSnoopDevice(root); } /************************************************************************************** ** Client is asking us to establish connection to the device ***************************************************************************************/ bool SimpleDetector::Connect() { IDMessage(getDeviceName(), "Simple Detector connected successfully!"); // Let's set a timer that checks teleDetectors status every POLLMS milliseconds. SetTimer(POLLMS); return true; } /************************************************************************************** ** Client is asking us to terminate connection to the device ***************************************************************************************/ bool SimpleDetector::Disconnect() { IDMessage(getDeviceName(), "Simple Detector disconnected successfully!"); return true; } /************************************************************************************** ** INDI is asking us for our default device name ***************************************************************************************/ const char *SimpleDetector::getDefaultName() { return "Simple Detector"; } /************************************************************************************** ** INDI is asking us to init our properties. ***************************************************************************************/ bool SimpleDetector::initProperties() { // Must init parent properties first! INDI::Detector::initProperties(); // We set the Detector capabilities uint32_t cap = DETECTOR_CAN_ABORT | DETECTOR_HAS_COOLER | DETECTOR_HAS_SHUTTER | DETECTOR_HAS_CONTINUUM | DETECTOR_HAS_SPECTRUM; SetDetectorCapability(cap); // Add Debug, Simulator, and Configuration controls addAuxControls(); setDefaultPollingPeriod(500); return true; } /******************************************************************************************** ** INDI is asking us to update the properties because there is a change in CONNECTION status ** This fucntion is called whenever the device is connected or disconnected. *********************************************************************************************/ bool SimpleDetector::updateProperties() { // Call parent update properties first INDI::Detector::updateProperties(); if (isConnected()) { // Let's get parameters now from Detector setupParams(); // Start the timer SetTimer(POLLMS); } return true; } /************************************************************************************** ** Client is updating capture settings ***************************************************************************************/ bool SimpleDetector::CaptureParamsUpdated(float sr, float freq, float bps) { INDI_UNUSED(bps); INDI_UNUSED(freq); INDI_UNUSED(sr); return true; } /************************************************************************************** ** Setting up Detector parameters ***************************************************************************************/ void SimpleDetector::setupParams() { // Our Detector is an 8 bit Detector, 100MHz frequency 1MHz samplerate. SetDetectorParams(1000000.0, 100000000.0, 8); } /************************************************************************************** ** Client is asking us to start an exposure ***************************************************************************************/ bool SimpleDetector::StartCapture(float duration) { CaptureRequest = duration; // Since we have only have one Detector with one chip, we set the exposure duration of the primary Detector PrimaryDetector.setCaptureDuration(duration); gettimeofday(&CapStart, nullptr); InCapture = true; // We're done return true; } /************************************************************************************** ** Client is asking us to abort an exposure ***************************************************************************************/ bool SimpleDetector::AbortCapture() { InCapture = false; return true; } /************************************************************************************** ** Client is asking us to set a new temperature ***************************************************************************************/ int SimpleDetector::SetTemperature(double temperature) { TemperatureRequest = temperature; // 0 means it will take a while to change the temperature return 0; } /************************************************************************************** ** How much longer until exposure is done? ***************************************************************************************/ float SimpleDetector::CalcTimeLeft() { double timesince; double timeleft; struct timeval now { 0, 0 }; gettimeofday(&now, nullptr); timesince = (double)(now.tv_sec * 1000.0 + now.tv_usec / 1000) - (double)(CapStart.tv_sec * 1000.0 + CapStart.tv_usec / 1000); timesince = timesince / 1000; timeleft = CaptureRequest - timesince; return timeleft; } /************************************************************************************** ** Main device loop. We check for exposure and temperature progress here ***************************************************************************************/ void SimpleDetector::TimerHit() { long timeleft; if (!isConnected()) return; // No need to reset timer if we are not connected anymore if (InCapture) { timeleft = CalcTimeLeft(); // Less than a 0.1 second away from exposure completion // This is an over simplified timing method, check DetectorSimulator and simpleDetector for better timing checks if (timeleft < 0.1) { /* We're done exposing */ IDMessage(getDeviceName(), "Capture done, downloading image..."); // Set exposure left to zero PrimaryDetector.setCaptureLeft(0); // We're no longer exposing... InCapture = false; /* grab and save image */ grabFrame(); } else // Just update time left in client PrimaryDetector.setCaptureLeft(timeleft); } // TemperatureNP is defined in INDI::Detector switch (TemperatureNP.s) { case IPS_IDLE: case IPS_OK: break; case IPS_BUSY: /* If target temperature is higher, then increase current Detector temperature */ if (currentDetectorTemperature < TemperatureRequest) currentDetectorTemperature++; /* If target temperature is lower, then decrese current Detector temperature */ else if (currentDetectorTemperature > TemperatureRequest) currentDetectorTemperature--; /* If they're equal, stop updating */ else { TemperatureNP.s = IPS_OK; IDSetNumber(&TemperatureNP, "Target temperature reached."); break; } IDSetNumber(&TemperatureNP, nullptr); break; case IPS_ALERT: break; } SetTimer(POLLMS); } /************************************************************************************** ** Create a random image and return it to client ***************************************************************************************/ void SimpleDetector::grabFrame() { // Set length of continuum int len = PrimaryDetector.getSampleRate() * PrimaryDetector.getCaptureDuration() * PrimaryDetector.getBPS() / 8; PrimaryDetector.setContinuumBufferSize(len); // Let's get a pointer to the frame buffer uint8_t *continuum = PrimaryDetector.getContinuumBuffer(); // Fill buffer with random pattern for (int i = 0; i < len; i++) continuum[i] = rand() % 255; // Set length of spectrum len = 1000; PrimaryDetector.setSpectrumBufferSize(len); // Let's get a pointer to the frame buffer double *spectrum = PrimaryDetector.getSpectrumBuffer(); // Fill buffer with random pattern for (int i = 0; i < len; i++) spectrum[i] = rand() % 255; IDMessage(getDeviceName(), "Download complete."); // Let INDI::Detector know we're done filling the image buffer CaptureComplete(&PrimaryDetector); } libindi/examples/tutorial_eight/simple_detector_simulator.h0000664000175000017500000000307113263645557023766 0ustar jasemjasem/* INDI Developers Manual Tutorial #3 "Simple Detector Driver" We develop a simple Detector driver. Refer to README, which contains instruction on how to build this driver, and use it with an INDI-compatible client. */ /** \file simpleDetector.h \brief Construct a basic INDI Detector device that simulates capture & temperature settings. It also generates a random pattern and uploads it as a FITS file. \author Ilia Platone \example simpleDetector.h A simple detector device that can capture stream frames and controls temperature. It returns a FITS image to the client. To build drivers for complex Detectors, please refer to the INDI Generic Detector driver template in INDI github (under 3rdparty). */ #pragma once #include "indidetector.h" class SimpleDetector : public INDI::Detector { public: SimpleDetector() = default; protected: // General device functions bool Connect(); bool Disconnect(); const char *getDefaultName(); bool initProperties(); bool updateProperties(); // Detector specific functions bool StartCapture(float duration); bool CaptureParamsUpdated(float bw, float freq, float bps); bool AbortCapture(); int SetTemperature(double temperature); void TimerHit(); private: // Utility functions float CalcTimeLeft(); void setupParams(); void grabFrame(); // Are we exposing? bool InCapture { false }; // Struct to keep timing struct timeval CapStart { 0, 0 }; float CaptureRequest { 0 }; float TemperatureRequest { 0 }; }; libindi/indidriver.h0000664000175000017500000002217013263645557014004 0ustar jasemjasem#if 0 INDI Driver Functions Copyright (C) 2003 - 2015 Jasem Mutlaq Copyright (C) 2003 - 2006 Elwood C. Downey 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 #pragma once #include "indiapi.h" #include "lilxml.h" #include #ifdef __cplusplus extern "C" { #endif /* insure RO properties are never modified. RO Sanity Check */ typedef struct { char propName[MAXINDINAME]; char devName[MAXINDIDEVICE]; IPerm perm; const void *ptr; int type; } ROSC; extern ROSC *propCache; extern int nPropCache; /* # of elements in roCheck */ extern int verbose; /* chatty */ extern char *me; /* a.out name */ extern LilXML *clixml; /* XML parser context */ extern int dispatch(XMLEle *root, char msg[]); extern void clientMsgCB(int fd, void *arg); /** * \defgroup configFunctions Configuration Functions: Functions drivers call to save and load configuraion options.

Drivers can save properties states and values in an XML configuration file. The following functions take an optional filename parameter which specifies the full path of the configuration file. If the filename is set to NULL, the configuration file is locally stored in ~/.indi. By default, two configuration files may exist for each driver:

  • Last Saved Configuration: ~/.indi/driver_name_config.xml
  • Default Configuration: ~/.indi/driver_name_config.xml.default

libindi stores the configuration parameters enclosed in newXXX commands. Therefore, if a configuration file is loaded, the driver property gets updated as if a client is setting these values. This is important to note since some configuration options may only available when the device is powered up or is in a particular state.

If no filename is supplied, each function will try to create the configuration files in the following order:

  1. INDICONFIG environment variable: The functions checks if the envrionment variable is defined, and if so, it shall be used as the configuration filename
  2. Generate filename: If the device_name is supplied, the function will attempt to set the configuration filename to ~/.indi/device_name_config.xml
\author Jasem Mutlaq \note Drivers subclassing INDI::DefaultDevice do not need to call the configuration functions directly as it is handled internally by the class. \version libindi 1.1+ */ /*@{*/ /** \brief Open a configuration file for writing and return a configuration file FILE pointer. \param filename full path of the configuration file. If set, the function will attempt to open it for writing. If set to NULL, it will attempt to generate the filename as described in the Detailed Description introduction and then open it for writing. \param dev device name. This is used if the filename parameter is NULL, and INDICONFIG environment variable is not set as described in the Detailed Description introduction. \param mode mode to open the file with (e.g. "w" or "r") \param errmsg In case of errors, store the error message in this buffer. The size of the buffer must be at least MAXRBUF. \return pointer to FILE if configuration file is opened successful, otherwise NULL and errmsg is set. */ extern FILE *IUGetConfigFP(const char *filename, const char *dev, const char *mode, char errmsg[]); /** \brief Loads and processes a configuration file. Once a configuration file is successful loaded, the function will iterate over the enclosed newXXX commands, and dispatches each command to the driver. Subsequently, the driver receives the updated property value in the driver's ISNewXXX functions. The driver may call this function at any time. However, it is usually called either on driver startup or on device power up. By default, all the properties are read from the configuration file. To load a specific property, pass the property name, otherwise pass NULL to retrive all properties. \param filename full path of the configuration file. If set, the function will attempt to load the file. If set to NULL, it will attempt to generate the filename as described in the Detailed Description introduction and then load it. \param dev device name. This is used if the filename parameter is NULL, and INDICONFIG environment variable is not set as described in the Detailed Description introduction. \param property Property name to load configuration for. If NULL, all properties within the configuration file will be processed. \param silent If silent is 1, it will suppress any output messages to the driver. \param errmsg In case of errors, store the error message in this buffer. The size of the buffer must be at least MAXRBUF. \return 0 on successful, -1 if there is an error and errmsg is set. */ extern int IUReadConfig(const char *filename, const char *dev, const char *property, int silent, char errmsg[]); /** \brief Copies an existing configuration file into a default configuration file. If no default configuration file for the supplied dev exists, it gets created and its contentes copied from an exiting source configuration file. Usually, when the user saves the configuration file of a driver for the first time, IUSaveDefaultConfig is called to create the default configuration file. If the default configuration file already exists, the function performs no action and returns. \param source_config full path of the source configuration file to read. If set, the function will attempt to load the file. If set to NULL, it will attempt to generate the filename as described in the Detailed Description introduction and then load it. \param dest_config full path of the destination default configuration file to write. If set, the function will attempt to load the file. If set to NULL, it will attempt to generate the filename as described in the Detailed Description introduction and then load it. If the file already exists, the function returns. If the file doesn't exist, it gets created and its contents copied from the source_config file. \param dev device name. This is used if either the source_config or desg_config are NULL, and INDICONFIG environment variable is not set as described in the Detailed Description introduction. */ extern void IUSaveDefaultConfig(const char *source_config, const char *dest_config, const char *dev); /** \brief Add opening or closing tag to a configuration file. A configuration file root XML element is \. This functions add \ or \ as required. \param fp file pointer to a configuration file. \param ctag If 0, \ is appened to the configuration file, otherwise \ is appeneded and the fp is closed. \param dev device name. Used only for sending notification to the driver if silent is set to 1. \param silent If silent is 1, it will suppress any output messages to the driver. */ extern void IUSaveConfigTag(FILE *fp, int ctag, const char *dev, int silent); /** \brief Add a number vector property value to the configuration file \param fp file pointer to a configuration file. \param nvp pointer to a number vector property. */ extern void IUSaveConfigNumber(FILE *fp, const INumberVectorProperty *nvp); /** \brief Add a text vector property value to the configuration file \param fp file pointer to a configuration file. \param tvp pointer to a text vector property. */ extern void IUSaveConfigText(FILE *fp, const ITextVectorProperty *tvp); /** \brief Add a switch vector property value to the configuration file \param fp file pointer to a configuration file. \param svp pointer to a switch vector property. */ extern void IUSaveConfigSwitch(FILE *fp, const ISwitchVectorProperty *svp); /** \brief Add a BLOB vector property value to the configuration file \param fp file pointer to a configuration file. \param bvp pointer to a BLOB vector property. \note If the BLOB size is large, this function will block until the BLOB contents are written to the file. */ extern void IUSaveConfigBLOB(FILE *fp, const IBLOBVectorProperty *bvp); /** * @brief IUGetConfigNumber Opens configuration file and reads number property. * @param dev name of device * @param property name of vector property * @param member name of member property * @param value pointer to save value of property if found. * @return 0 on success, -1 if not found. */ extern int IUGetConfigNumber(const char *dev, const char *property, const char *member, double *value); /*@}*/ #ifdef __cplusplus } #endif libindi/indiversion.h.cmake0000664000175000017500000000022713263645557015254 0ustar jasemjasem/* Set INDI Library version */ #cmakedefine INDI_VERSION @INDI_VERSION@ /* Define INDI Data Dir */ #cmakedefine DATA_INSTALL_DIR "@DATA_INSTALL_DIR@" libindi/indiserver.c0000664000175000017500000020046513263645557014017 0ustar jasemjasem/* INDI Server for protocol version 1.7. * Copyright (C) 2007 Elwood C. Downey 2013 Jasem Mutlaq 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 * argv lists names of Driver programs to run or sockets to connect for Devices. * Drivers are restarted if they exit or connection closes. * Each local Driver's stdin/out are assumed to provide INDI traffic and are * connected here via pipes. Local Drivers' stderr are connected to our * stderr with date stamp and driver name prepended. * We only support Drivers that advertise support for one Device. The problem * with multiple Devices in one Driver is without a way to know what they * _all_ are there is no way to avoid sending all messages to all Drivers. * Outbound messages are limited to Devices and Properties seen inbound. * Messages to Devices on sockets always include Device so the chained * indiserver will only pass back info from that Device. * All newXXX() received from one Client are echoed to all other Clients who * have shown an interest in the same Device and property. * * 2017-01-29 JM: Added option to drop stream blobs if client blob queue is * higher than maxstreamsiz bytes * * Implementation notes: * * We fork each driver and open a server socket listening for INDI clients. * Then forever we listen for new clients and pass traffic between clients and * drivers, subject to optimizations based on sniffing messages for matching * Devices and Properties. Since one message might be destined to more than * one client or device, they are queued and only removed after the last * consumer is finished. XMLEle are converted to linear strings before being * sent to optimize write system calls and avoid blocking to slow clients. * Clients that get more than maxqsiz bytes behind are shut down. */ #include "config.h" #include "fq.h" #include "indiapi.h" #include "indidevapi.h" #include "lilxml.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #define INDIPORT 7624 /* default TCP/IP port to listen */ #define REMOTEDVR (-1234) /* invalid PID to flag remote drivers */ #define MAXSBUF 512 #define MAXRBUF 49152 /* max read buffering here */ #define MAXWSIZ 49152 /* max bytes/write */ #define DEFMAXQSIZ 128 /* default max q behind, MB */ #define DEFMAXSSIZ 5 /* default max stream behind, MB */ #define DEFMAXRESTART 10 /* default max restarts */ #ifdef OSX_EMBEDED_MODE #define LOGNAME "/Users/%s/Library/Logs/indiserver.log" #define FIFONAME "/tmp/indiserverFIFO" #endif /* associate a usage count with queuded client or device message */ typedef struct { int count; /* number of consumers left */ unsigned long cl; /* content length */ char *cp; /* content: buf or malloced */ char buf[MAXWSIZ]; /* local buf for most messages */ } Msg; /* device + property name */ typedef struct { char dev[MAXINDIDEVICE]; char name[MAXINDINAME]; BLOBHandling blob; /* when to snoop BLOBs */ } Property; /* record of each snooped property typedef struct { Property prop; BLOBHandling blob; } Property; */ struct { const char *name; /* Path to FIFO for dynamic startups & shutdowns of drivers */ int fd; //FILE *fs; } fifo; /* info for each connected client */ typedef struct { int active; /* 1 when this record is in use */ Property *props; /* malloced array of props we want */ int nprops; /* n entries in props[] */ int allprops; /* saw getProperties w/o device */ BLOBHandling blob; /* when to send setBLOBs */ int s; /* socket for this client */ LilXML *lp; /* XML parsing context */ FQ *msgq; /* Msg queue */ unsigned int nsent; /* bytes of current Msg sent so far */ } ClInfo; static ClInfo *clinfo; /* malloced pool of clients */ static int nclinfo; /* n total (not active) */ /* info for each connected driver */ typedef struct { char name[MAXINDINAME]; /* persistent name */ char envDev[MAXSBUF]; char envConfig[MAXSBUF]; char envSkel[MAXSBUF]; char envPrefix[MAXSBUF]; char host[MAXSBUF]; int port; //char dev[MAXINDIDEVICE]; /* device served by this driver */ char **dev; /* device served by this driver */ int ndev; /* number of devices served by this driver */ int active; /* 1 when this record is in use */ Property *sprops; /* malloced array of props we snoop */ int nsprops; /* n entries in sprops[] */ int pid; /* process id or REMOTEDVR if remote */ int rfd; /* read pipe fd */ int wfd; /* write pipe fd */ int efd; /* stderr from driver, if local */ int restarts; /* times process has been restarted */ LilXML *lp; /* XML parsing context */ FQ *msgq; /* Msg queue */ unsigned int nsent; /* bytes of current Msg sent so far */ } DvrInfo; static DvrInfo *dvrinfo; /* malloced array of drivers */ static int ndvrinfo; /* n total */ static char *me; /* our name */ static int port = INDIPORT; /* public INDI port */ static int verbose; /* chattiness */ static int lsocket; /* listen socket */ static char *ldir; /* where to log driver messages */ static int maxqsiz = (DEFMAXQSIZ * 1024 * 1024); /* kill if these bytes behind */ static int maxstreamsiz = (DEFMAXSSIZ * 1024 * 1024); /* drop blobs if these bytes behind while streaming*/ static int maxrestarts = DEFMAXRESTART; static int terminateddrv = 0; static void logStartup(int ac, char *av[]); static void usage(void); //static void noZombies(void); static void reapZombies(void); static void noSIGPIPE(void); static void indiFIFO(void); static void indiRun(void); static void indiListen(void); static void newFIFO(void); static void newClient(void); static int newClSocket(void); static void shutdownClient(ClInfo *cp); static int readFromClient(ClInfo *cp); static void startDvr(DvrInfo *dp); static void startLocalDvr(DvrInfo *dp); static void startRemoteDvr(DvrInfo *dp); static int openINDIServer(char host[], int indi_port); static void shutdownDvr(DvrInfo *dp, int restart); static int isDeviceInDriver(const char *dev, DvrInfo *dp); static void q2RDrivers(const char *dev, Msg *mp, XMLEle *root); static void q2SDrivers(DvrInfo *me, int isblob, const char *dev, const char *name, Msg *mp, XMLEle *root); static int q2Clients(ClInfo *notme, int isblob, const char *dev, const char *name, Msg *mp, XMLEle *root); static int q2Servers(DvrInfo *me, Msg *mp, XMLEle *root); static void addSDevice(DvrInfo *dp, const char *dev, const char *name); static Property *findSDevice(DvrInfo *dp, const char *dev, const char *name); static void addClDevice(ClInfo *cp, const char *dev, const char *name, int isblob); static int findClDevice(ClInfo *cp, const char *dev, const char *name); static int readFromDriver(DvrInfo *dp); static int stderrFromDriver(DvrInfo *dp); static int msgQSize(FQ *q); static void setMsgXMLEle(Msg *mp, XMLEle *root); static void setMsgStr(Msg *mp, char *str); static void freeMsg(Msg *mp); static Msg *newMsg(void); static int sendClientMsg(ClInfo *cp); static int sendDriverMsg(DvrInfo *cp); static void crackBLOB(const char *enableBLOB, BLOBHandling *bp); static void crackBLOBHandling(const char *dev, const char *name, const char *enableBLOB, ClInfo *cp); static void traceMsg(XMLEle *root); static char *indi_tstamp(char *s); static void logDMsg(XMLEle *root, const char *dev); static void Bye(void); int main(int ac, char *av[]) { /* log startup */ logStartup(ac, av); /* save our name */ me = av[0]; #ifdef OSX_EMBEDED_MODE char logname[128]; snprintf(logname, 128, LOGNAME, getlogin()); fprintf(stderr, "switching stderr to %s", logname); freopen(logname, "w", stderr); fifo.name = FIFONAME; verbose = 1; ac = 0; #else /* crack args */ while ((--ac > 0) && ((*++av)[0] == '-')) { char *s; for (s = av[0] + 1; *s != '\0'; s++) switch (*s) { case 'l': if (ac < 2) { fprintf(stderr, "-l requires log directory\n"); usage(); } ldir = *++av; ac--; break; case 'm': if (ac < 2) { fprintf(stderr, "-m requires max MB behind\n"); usage(); } maxqsiz = 1024 * 1024 * atoi(*++av); ac--; break; case 'p': if (ac < 2) { fprintf(stderr, "-p requires port value\n"); usage(); } port = atoi(*++av); ac--; break; case 'd': if (ac < 2) { fprintf(stderr, "-d requires max stream MB behind\n"); usage(); } maxstreamsiz = 1024 * 1024 * atoi(*++av); ac--; break; case 'f': if (ac < 2) { fprintf(stderr, "-f requires fifo node\n"); usage(); } fifo.name = *++av; ac--; break; case 'r': if (ac < 2) { fprintf(stderr, "-r requires number of restarts\n"); usage(); } maxrestarts = atoi(*++av); if (maxrestarts < 0) maxrestarts = 0; ac--; break; case 'v': verbose++; break; default: usage(); } } #endif /* at this point there are ac args in av[] to name our drivers */ if (ac == 0 && !fifo.name) usage(); /* take care of some unixisms */ /*noZombies();*/ reapZombies(); noSIGPIPE(); /* realloc seed for client pool */ clinfo = (ClInfo *)malloc(1); nclinfo = 0; /* create driver info array all at once since size never changes */ ndvrinfo = ac; dvrinfo = (DvrInfo *)calloc(ndvrinfo, sizeof(DvrInfo)); /* start each driver */ while (ac-- > 0) { strncpy(dvrinfo[ac].name, *av++, MAXINDINAME); startDvr(&dvrinfo[ac]); } /* announce we are online */ indiListen(); /* Load up FIFO, if available */ indiFIFO(); /* handle new clients and all io */ while (1) indiRun(); /* whoa! */ fprintf(stderr, "unexpected return from main\n"); return (1); } /* record we have started and our args */ static void logStartup(int ac, char *av[]) { int i; fprintf(stderr, "%s: startup: ", indi_tstamp(NULL)); for (i = 0; i < ac; i++) fprintf(stderr, "%s ", av[i]); fprintf(stderr, "\n"); } /* print usage message and exit (2) */ static void usage(void) { fprintf(stderr, "Usage: %s [options] driver [driver ...]\n", me); fprintf(stderr, "Purpose: server for local and remote INDI drivers\n"); fprintf(stderr, "INDI Library: %s\nCode %s. Protocol %g.\n", CMAKE_INDI_VERSION_STRING, "$Rev$", INDIV); fprintf(stderr, "Options:\n"); fprintf(stderr, " -l d : log driver messages to /YYYY-MM-DD.islog\n"); fprintf(stderr, " -m m : kill client if gets more than this many MB behind, default %d\n", DEFMAXQSIZ); fprintf(stderr, " -d m : drop streaming blobs if client gets more than this many MB behind, default %d. 0 to disable\n", DEFMAXSSIZ); fprintf(stderr, " -p p : alternate IP port, default %d\n", INDIPORT); fprintf(stderr, " -r r : maximum driver restarts on error, default %d\n", DEFMAXRESTART); fprintf(stderr, " -f path : Path to fifo for dynamic startup and shutdown of drivers.\n"); fprintf(stderr, " -v : show key events, no traffic\n"); fprintf(stderr, " -vv : -v + key message content\n"); fprintf(stderr, " -vvv : -vv + complete xml\n"); fprintf(stderr, "driver : executable or device@host[:port]\n"); exit(2); } /* arrange for no zombies if drivers die */ //static void noZombies() //{ // struct sigaction sa; // sa.sa_handler = SIG_IGN; // sigemptyset(&sa.sa_mask); //#ifdef SA_NOCLDWAIT // sa.sa_flags = SA_NOCLDWAIT; //#else // sa.sa_flags = 0; //#endif // (void)sigaction(SIGCHLD, &sa, NULL); //} /* reap zombies when drivers die, in order to leave SIGCHLD unmodified for subprocesses */ static void zombieRaised(int signum, siginfo_t *sig, void *data) { INDI_UNUSED(data); switch (signum) { case SIGCHLD: fprintf(stderr, "Child process %d died\n", sig->si_pid); waitpid(sig->si_pid, NULL, WNOHANG); break; default: fprintf(stderr, "Received unexpected signal %d\n", signum); } } /* reap zombies as they die */ static void reapZombies() { struct sigaction sa; sa.sa_sigaction = zombieRaised; sigemptyset(&sa.sa_mask); sa.sa_flags = SA_SIGINFO; (void)sigaction(SIGCHLD, &sa, NULL); } /* turn off SIGPIPE on bad write so we can handle it inline */ static void noSIGPIPE() { struct sigaction sa; sa.sa_handler = SIG_IGN; sigemptyset(&sa.sa_mask); (void)sigaction(SIGPIPE, &sa, NULL); } static DvrInfo *allocDvr() { DvrInfo *dp = NULL; int dvi; /* try to reuse a driver slot, else add one */ for (dvi = 0; dvi < ndvrinfo; dvi++) if (!(dp = &dvrinfo[dvi])->active) break; if (dvi == ndvrinfo) { /* grow dvrinfo */ dvrinfo = (DvrInfo *)realloc(dvrinfo, (ndvrinfo + 1) * sizeof(DvrInfo)); if (!dvrinfo) { fprintf(stderr, "no memory for new drivers\n"); Bye(); } dp = &dvrinfo[ndvrinfo++]; } if (dp == NULL) return NULL; /* rig up new dvrinfo entry */ memset(dp, 0, sizeof(*dp)); dp->active = 1; dp->ndev = 0; return dp; } /* start the given INDI driver process or connection. * exit if trouble. */ static void startDvr(DvrInfo *dp) { if (strchr(dp->name, '@')) startRemoteDvr(dp); else startLocalDvr(dp); } /* start the given local INDI driver process. * exit if trouble. */ static void startLocalDvr(DvrInfo *dp) { Msg *mp; char buf[32]; int rp[2], wp[2], ep[2]; int pid; #ifdef OSX_EMBEDED_MODE fprintf(stderr, "STARTING \"%s\"\n", dp->name); fflush(stderr); #endif /* build three pipes: r, w and error*/ if (pipe(rp) < 0) { fprintf(stderr, "%s: read pipe: %s\n", indi_tstamp(NULL), strerror(errno)); Bye(); } if (pipe(wp) < 0) { fprintf(stderr, "%s: write pipe: %s\n", indi_tstamp(NULL), strerror(errno)); Bye(); } if (pipe(ep) < 0) { fprintf(stderr, "%s: stderr pipe: %s\n", indi_tstamp(NULL), strerror(errno)); Bye(); } /* fork&exec new process */ pid = fork(); if (pid < 0) { fprintf(stderr, "%s: fork: %s\n", indi_tstamp(NULL), strerror(errno)); Bye(); } if (pid == 0) { /* child: exec name */ int fd; /* rig up pipes */ dup2(wp[0], 0); /* driver stdin reads from wp[0] */ dup2(rp[1], 1); /* driver stdout writes to rp[1] */ dup2(ep[1], 2); /* driver stderr writes to e[]1] */ for (fd = 3; fd < 100; fd++) (void)close(fd); if (*dp->envDev) setenv("INDIDEV", dp->envDev, 1); /* Only reset environment variable in case of FIFO */ else if (fifo.fd > 0) unsetenv("INDIDEV"); if (*dp->envConfig) setenv("INDICONFIG", dp->envConfig, 1); else if (fifo.fd > 0) unsetenv("INDICONFIG"); if (*dp->envSkel) setenv("INDISKEL", dp->envSkel, 1); else if (fifo.fd > 0) unsetenv("INDISKEL"); char executable[MAXSBUF]; if (*dp->envPrefix) { setenv("INDIPREFIX", dp->envPrefix, 1); #if defined(OSX_EMBEDED_MODE) snprintf(executable, MAXSBUF, "%s/Contents/MacOS/%s", dp->envPrefix, dp->name); #elif defined(__APPLE__) snprintf(executable, MAXSBUF, "%s/%s", dp->envPrefix, dp->name); #else snprintf(executable, MAXSBUF, "%s/bin/%s", dp->envPrefix, dp->name); #endif fprintf(stderr, "%s\n", executable); execlp(executable, dp->name, NULL); } else { if (fifo.fd > 0) unsetenv("INDIPREFIX"); if (dp->name[0] == '.') { snprintf(executable, MAXSBUF, "%s/%s", dirname(me), dp->name); execlp(executable, dp->name, NULL); } else { execlp(dp->name, dp->name, NULL); } } #ifdef OSX_EMBEDED_MODE fprintf(stderr, "FAILED \"%s\"\n", dp->name); fflush(stderr); #endif fprintf(stderr, "%s: Driver %s: execlp: %s\n", indi_tstamp(NULL), dp->name, strerror(errno)); _exit(1); /* parent will notice EOF shortly */ } /* don't need child's side of pipes */ close(wp[0]); close(rp[1]); close(ep[1]); /* record pid, io channels, init lp and snoop list */ dp->pid = pid; strncpy(dp->host, "localhost", MAXSBUF); dp->port = -1; dp->rfd = rp[0]; dp->wfd = wp[1]; dp->efd = ep[0]; dp->lp = newLilXML(); dp->msgq = newFQ(1); dp->sprops = (Property *)malloc(1); /* seed for realloc */ dp->nsprops = 0; dp->nsent = 0; dp->active = 1; dp->ndev = 0; dp->dev = (char **)malloc(sizeof(char *)); /* first message primes driver to report its properties -- dev known * if restarting */ mp = newMsg(); pushFQ(dp->msgq, mp); snprintf(buf, sizeof(buf), "\n", INDIV); setMsgStr(mp, buf); mp->count++; if (verbose > 0) fprintf(stderr, "%s: Driver %s: pid=%d rfd=%d wfd=%d efd=%d\n", indi_tstamp(NULL), dp->name, dp->pid, dp->rfd, dp->wfd, dp->efd); } /* start the given remote INDI driver connection. * exit if trouble. */ static void startRemoteDvr(DvrInfo *dp) { Msg *mp; char dev[MAXINDIDEVICE]; char host[MAXSBUF]; char buf[MAXSBUF]; int indi_port, sockfd; /* extract host and port */ indi_port = INDIPORT; if (sscanf(dp->name, "%[^@]@%[^:]:%d", dev, host, &indi_port) < 2) { fprintf(stderr, "Bad remote device syntax: %s\n", dp->name); Bye(); } /* connect */ sockfd = openINDIServer(host, indi_port); /* record flag pid, io channels, init lp and snoop list */ dp->pid = REMOTEDVR; strncpy(dp->host, host, MAXSBUF); dp->port = indi_port; dp->rfd = sockfd; dp->wfd = sockfd; dp->lp = newLilXML(); dp->msgq = newFQ(1); dp->sprops = (Property *)malloc(1); /* seed for realloc */ dp->nsprops = 0; dp->nsent = 0; dp->active = 1; dp->ndev = 1; dp->dev = (char **)malloc(sizeof(char *)); /* N.B. storing name now is key to limiting outbound traffic to this * dev. */ dp->dev[0] = (char *)malloc(MAXINDIDEVICE * sizeof(char)); strncpy(dp->dev[0], dev, MAXINDIDEVICE - 1); dp->dev[0][MAXINDIDEVICE - 1] = '\0'; /* Sending getProperties with device lets remote server limit its * outbound (and our inbound) traffic on this socket to this device. */ mp = newMsg(); pushFQ(dp->msgq, mp); sprintf(buf, "\n", dp->dev[0], INDIV); setMsgStr(mp, buf); mp->count++; if (verbose > 0) fprintf(stderr, "%s: Driver %s: socket=%d\n", indi_tstamp(NULL), dp->name, sockfd); } /* open a connection to the given host and port or die. * return socket fd. */ static int openINDIServer(char host[], int indi_port) { struct sockaddr_in serv_addr; struct hostent *hp; int sockfd; /* lookup host address */ hp = gethostbyname(host); if (!hp) { fprintf(stderr, "gethostbyname(%s): %s\n", host, strerror(errno)); Bye(); } /* create a socket to the INDI server */ (void)memset((char *)&serv_addr, 0, sizeof(serv_addr)); serv_addr.sin_family = AF_INET; serv_addr.sin_addr.s_addr = ((struct in_addr *)(hp->h_addr_list[0]))->s_addr; serv_addr.sin_port = htons(indi_port); if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0) { fprintf(stderr, "socket(%s,%d): %s\n", host, indi_port, strerror(errno)); Bye(); } /* connect */ if (connect(sockfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0) { fprintf(stderr, "connect(%s,%d): %s\n", host, indi_port, strerror(errno)); Bye(); } /* ok */ return (sockfd); } /* create the public INDI Driver endpoint lsocket on port. * return server socket else exit. */ static void indiListen() { struct sockaddr_in serv_socket; int sfd; int reuse = 1; /* make socket endpoint */ if ((sfd = socket(AF_INET, SOCK_STREAM, 0)) < 0) { fprintf(stderr, "%s: socket: %s\n", indi_tstamp(NULL), strerror(errno)); Bye(); } /* bind to given port for any IP address */ memset(&serv_socket, 0, sizeof(serv_socket)); serv_socket.sin_family = AF_INET; #ifdef SSH_TUNNEL serv_socket.sin_addr.s_addr = htonl(INADDR_LOOPBACK); #else serv_socket.sin_addr.s_addr = htonl(INADDR_ANY); #endif serv_socket.sin_port = htons((unsigned short)port); if (setsockopt(sfd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse)) < 0) { fprintf(stderr, "%s: setsockopt: %s\n", indi_tstamp(NULL), strerror(errno)); Bye(); } if (bind(sfd, (struct sockaddr *)&serv_socket, sizeof(serv_socket)) < 0) { fprintf(stderr, "%s: bind: %s\n", indi_tstamp(NULL), strerror(errno)); Bye(); } /* willing to accept connections with a backlog of 5 pending */ if (listen(sfd, 5) < 0) { fprintf(stderr, "%s: listen: %s\n", indi_tstamp(NULL), strerror(errno)); Bye(); } /* ok */ lsocket = sfd; if (verbose > 0) fprintf(stderr, "%s: listening to port %d on fd %d\n", indi_tstamp(NULL), port, sfd); } /* Attempt to open up FIFO */ static void indiFIFO(void) { close(fifo.fd); fifo.fd = -1; /* Open up FIFO, if available */ if (fifo.name) { fifo.fd = open(fifo.name, O_RDWR | O_NONBLOCK); if (fifo.fd < 0) { fprintf(stderr, "%s: open(%s): %s.\n", indi_tstamp(NULL), fifo.name, strerror(errno)); Bye(); } } } /* service traffic from clients and drivers */ static void indiRun(void) { fd_set rs, ws; int maxfd = 0; int i, s; /* init with no writers or readers */ FD_ZERO(&ws); FD_ZERO(&rs); if (fifo.name && fifo.fd >= 0) { FD_SET(fifo.fd, &rs); maxfd = fifo.fd; } /* always listen for new clients */ FD_SET(lsocket, &rs); if (lsocket > maxfd) maxfd = lsocket; /* add all client readers and client writers with work to send */ for (i = 0; i < nclinfo; i++) { ClInfo *cp = &clinfo[i]; if (cp->active) { FD_SET(cp->s, &rs); if (nFQ(cp->msgq) > 0) FD_SET(cp->s, &ws); if (cp->s > maxfd) maxfd = cp->s; } } /* add all driver readers and driver writers with work to send */ for (i = 0; i < ndvrinfo; i++) { DvrInfo *dp = &dvrinfo[i]; if (dp->active) { FD_SET(dp->rfd, &rs); if (dp->rfd > maxfd) maxfd = dp->rfd; if (dp->pid != REMOTEDVR) { FD_SET(dp->efd, &rs); if (dp->efd > maxfd) maxfd = dp->efd; } if (nFQ(dp->msgq) > 0) { FD_SET(dp->wfd, &ws); if (dp->wfd > maxfd) maxfd = dp->wfd; } } } /* wait for action */ s = select(maxfd + 1, &rs, &ws, NULL, NULL); if (s < 0) { if(errno==EINTR) return; fprintf(stderr, "%s: select(%d): %s\n", indi_tstamp(NULL), maxfd + 1, strerror(errno)); Bye(); } /* new command from FIFO? */ if (s > 0 && fifo.fd >= 0 && FD_ISSET(fifo.fd, &rs)) { newFIFO(); s--; } /* new client? */ if (s > 0 && FD_ISSET(lsocket, &rs)) { newClient(); s--; } /* message to/from client? */ for (i = 0; s > 0 && i < nclinfo; i++) { ClInfo *cp = &clinfo[i]; if (cp->active) { if (FD_ISSET(cp->s, &rs)) { if (readFromClient(cp) < 0) return; /* fds effected */ s--; } if (s > 0 && FD_ISSET(cp->s, &ws)) { if (sendClientMsg(cp) < 0) return; /* fds effected */ s--; } } } /* message to/from driver? */ for (i = 0; s > 0 && i < ndvrinfo; i++) { DvrInfo *dp = &dvrinfo[i]; if (dp->pid != REMOTEDVR && FD_ISSET(dp->efd, &rs)) { if (stderrFromDriver(dp) < 0) return; /* fds effected */ s--; } if (s > 0 && FD_ISSET(dp->rfd, &rs)) { if (readFromDriver(dp) < 0) return; /* fds effected */ s--; } if (s > 0 && FD_ISSET(dp->wfd, &ws) && nFQ(dp->msgq) > 0) { if (sendDriverMsg(dp) < 0) return; /* fds effected */ s--; } } } int isDeviceInDriver(const char *dev, DvrInfo *dp) { int i = 0; for (i = 0; i < dp->ndev; i++) { if (!strcmp(dev, dp->dev[i])) return 1; } return 0; } /* Read commands from FIFO and process them. Start/stop drivers accordingly */ static void newFIFO(void) { //char line[MAXRBUF], tDriver[MAXRBUF], tConfig[MAXRBUF], tDev[MAXRBUF], tSkel[MAXRBUF], envDev[MAXRBUF], envConfig[MAXRBUF], envSkel[MAXR]; char line[MAXRBUF]; DvrInfo *dp = NULL; int startCmd = 0, i = 0, remoteDriver = 0; while (i < MAXRBUF) { if (read(fifo.fd, line + i, 1) <= 0) { // Reset FIFO now, otherwise select will always return with no data from FIFO. indiFIFO(); return; } if (line[i] == '\n') { line[i] = '\0'; i = 0; } else { i++; continue; } if (verbose) fprintf(stderr, "FIFO: %s\n", line); char cmd[MAXSBUF], arg[4][1], var[4][MAXSBUF], tDriver[MAXSBUF], tName[MAXSBUF], envConfig[MAXSBUF], envSkel[MAXSBUF], envPrefix[MAXSBUF]; memset(&tDriver[0], 0, sizeof(char) * MAXSBUF); memset(&tName[0], 0, sizeof(char) * MAXSBUF); memset(&envConfig[0], 0, sizeof(char) * MAXSBUF); memset(&envSkel[0], 0, sizeof(char) * MAXSBUF); memset(&envPrefix[0], 0, sizeof(char) * MAXSBUF); int n = 0; // If remote driver if (strstr(line, "@")) { n = sscanf(line, "%s %512[^\n]", cmd, tDriver); // Remove quotes if any char *ptr = tDriver; int len = strlen(tDriver); while ((ptr = strstr(tDriver, "\""))) { memmove(ptr, ptr + 1, --len); ptr[len] = '\0'; } //fprintf(stderr, "Remote Driver: %s\n", tDriver); remoteDriver = 1; } // If local driver else { n = sscanf(line, "%s %s -%1c \"%512[^\"]\" -%1c \"%512[^\"]\" -%1c \"%512[^\"]\" -%1c \"%512[^\"]\"", cmd, tDriver, arg[0], var[0], arg[1], var[1], arg[2], var[2], arg[3], var[3]); remoteDriver = 0; } int n_args = (n - 2) / 2; int j = 0; for (j = 0; j < n_args; j++) { //fprintf(stderr, "arg[%d]: %c\n", i, arg[j][0]); //fprintf(stderr, "var[%d]: %s\n", i, var[j]); if (arg[j][0] == 'n') { strncpy(tName, var[j], MAXSBUF - 1); tName[MAXSBUF - 1] = '\0'; if (verbose) fprintf(stderr, "With name: %s\n", tName); } else if (arg[j][0] == 'c') { strncpy(envConfig, var[j], MAXSBUF - 1); envConfig[MAXSBUF - 1] = '\0'; if (verbose) fprintf(stderr, "With config: %s\n", envConfig); } else if (arg[j][0] == 's') { strncpy(envSkel, var[j], MAXSBUF - 1); envSkel[MAXSBUF - 1] = '\0'; if (verbose) fprintf(stderr, "With skeketon: %s\n", envSkel); } else if (arg[j][0] == 'p') { strncpy(envPrefix, var[j], MAXSBUF - 1); envPrefix[MAXSBUF - 1] = '\0'; if (verbose) fprintf(stderr, "With prefix: %s\n", envPrefix); } } if (!strcmp(cmd, "start")) startCmd = 1; else startCmd = 0; if (startCmd) { if (verbose) fprintf(stderr, "FIFO: Starting driver %s\n", tDriver); dp = allocDvr(); strncpy(dp->name, tDriver, MAXINDIDEVICE); if (remoteDriver == 0) { //strncpy(dp->dev, tName, MAXINDIDEVICE); strncpy(dp->envDev, tName, MAXSBUF); strncpy(dp->envConfig, envConfig, MAXSBUF); strncpy(dp->envSkel, envSkel, MAXSBUF); strncpy(dp->envPrefix, envPrefix, MAXSBUF); startDvr(dp); } else startRemoteDvr(dp); } else { for (dp = dvrinfo; dp < &dvrinfo[ndvrinfo]; dp++) { fprintf(stderr, "dp->name: %s - tDriver: %s\n", dp->name, tDriver); if (!strcmp(dp->name, tDriver) && dp->active == 1) { fprintf(stderr, "name: %s - dp->dev[0]: %s\n", tName, dp->dev[0]); /* If device name is given, check against it before shutting down */ //if (tName[0] && strcmp(dp->dev[0], tName)) if (tName[0] && isDeviceInDriver(tName, dp) == 0) continue; if (verbose) fprintf(stderr, "FIFO: Shutting down driver: %s\n", tDriver); for (i = 0; i < dp->ndev; i++) { /* Inform clients that this driver is dead */ XMLEle *root = addXMLEle(NULL, "delProperty"); addXMLAtt(root, "device", dp->dev[i]); prXMLEle(stderr, root, 0); Msg *mp = newMsg(); q2Clients(NULL, 0, dp->dev[i], NULL, mp, root); if (mp->count > 0) setMsgXMLEle(mp, root); else freeMsg(mp); delXMLEle(root); } shutdownDvr(dp, 0); break; } } } } } /* prepare for new client arriving on lsocket. * exit if trouble. */ static void newClient() { ClInfo *cp = NULL; int s, cli; /* assign new socket */ s = newClSocket(); /* try to reuse a clinfo slot, else add one */ for (cli = 0; cli < nclinfo; cli++) if (!(cp = &clinfo[cli])->active) break; if (cli == nclinfo) { /* grow clinfo */ clinfo = (ClInfo *)realloc(clinfo, (nclinfo + 1) * sizeof(ClInfo)); if (!clinfo) { fprintf(stderr, "no memory for new client\n"); Bye(); } cp = &clinfo[nclinfo++]; } if (cp == NULL) return; /* rig up new clinfo entry */ memset(cp, 0, sizeof(*cp)); cp->active = 1; cp->s = s; cp->lp = newLilXML(); cp->msgq = newFQ(1); cp->props = malloc(1); cp->nsent = 0; if (verbose > 0) { struct sockaddr_in addr; socklen_t len = sizeof(addr); getpeername(s, (struct sockaddr *)&addr, &len); fprintf(stderr, "%s: Client %d: new arrival from %s:%d - welcome!\n", indi_tstamp(NULL), cp->s, inet_ntoa(addr.sin_addr), ntohs(addr.sin_port)); } #ifdef OSX_EMBEDED_MODE int active = 0; for (int i = 0; i < nclinfo; i++) if (clinfo[i].active) active++; fprintf(stderr, "CLIENTS %d\n", active); fflush(stderr); #endif } /* read more from the given client, send to each appropriate driver when see * xml closure. also send all newXXX() to all other interested clients. * return -1 if had to shut down anything, else 0. */ static int readFromClient(ClInfo *cp) { char buf[MAXRBUF]; int shutany = 0; ssize_t i, nr; /* read client */ nr = read(cp->s, buf, sizeof(buf)); if (nr <= 0) { if (nr < 0) fprintf(stderr, "%s: Client %d: read: %s\n", indi_tstamp(NULL), cp->s, strerror(errno)); else if (verbose > 0) fprintf(stderr, "%s: Client %d: read EOF\n", indi_tstamp(NULL), cp->s); shutdownClient(cp); return (-1); } /* process XML, sending when find closure */ for (i = 0; i < nr; i++) { char err[1024]; XMLEle *root = readXMLEle(cp->lp, buf[i], err); if (root) { char *roottag = tagXMLEle(root); const char *dev = findXMLAttValu(root, "device"); const char *name = findXMLAttValu(root, "name"); int isblob = !strcmp(tagXMLEle(root), "setBLOBVector"); Msg *mp; if (verbose > 2) { fprintf(stderr, "%s: Client %d: read ", indi_tstamp(NULL), cp->s); traceMsg(root); } else if (verbose > 1) { fprintf(stderr, "%s: Client %d: read <%s device='%s' name='%s'>\n", indi_tstamp(NULL), cp->s, tagXMLEle(root), findXMLAttValu(root, "device"), findXMLAttValu(root, "name")); } /* snag interested properties. * N.B. don't open to alldevs if seen specific dev already, else * remote client connections start returning too much. */ if (dev[0]) addClDevice(cp, dev, name, isblob); else if (!strcmp(roottag, "getProperties") && !cp->nprops) cp->allprops = 1; /* snag enableBLOB -- send to remote drivers too */ if (!strcmp(roottag, "enableBLOB")) crackBLOBHandling(dev, name, pcdataXMLEle(root), cp); /* build a new message -- set content iff anyone cares */ mp = newMsg(); /* send message to driver(s) responsible for dev */ q2RDrivers(dev, mp, root); /* JM 2016-05-18: Upstream client can be a chained INDI server. If any driver locally is snooping * on any remote drivers, we should catch it and forward it to the responsible snooping driver. */ /* send to snooping drivers. */ // JM 2016-05-26: Only forward setXXX messages if (!strncmp(roottag, "set", 3)) q2SDrivers(NULL, isblob, dev, name, mp, root); /* echo new* commands back to other clients */ if (!strncmp(roottag, "new", 3)) { if (q2Clients(cp, isblob, dev, name, mp, root) < 0) shutany++; } /* set message content if anyone cares else forget it */ if (mp->count > 0) setMsgXMLEle(mp, root); else freeMsg(mp); delXMLEle(root); } else if (err[0]) { char *ts = indi_tstamp(NULL); fprintf(stderr, "%s: Client %d: XML error: %s\n", ts, cp->s, err); fprintf(stderr, "%s: Client %d: XML read: %.*s\n", ts, cp->s, (int)nr, buf); shutdownClient(cp); return (-1); } } return (shutany ? -1 : 0); } /* read more from the given driver, send to each interested client when see * xml closure. if driver dies, try restarting. * return 0 if ok else -1 if had to shut down anything. */ static int readFromDriver(DvrInfo *dp) { char buf[MAXRBUF]; int shutany = 0; ssize_t nr; char err[1024]; XMLEle **nodes; XMLEle *root; int inode = 0; /* read driver */ nr = read(dp->rfd, buf, sizeof(buf)); if (nr <= 0) { if (nr < 0) fprintf(stderr, "%s: Driver %s: stdin %s\n", indi_tstamp(NULL), dp->name, strerror(errno)); else fprintf(stderr, "%s: Driver %s: stdin EOF\n", indi_tstamp(NULL), dp->name); shutdownDvr(dp, 1); return (-1); } /* process XML chunk */ nodes = parseXMLChunk(dp->lp, buf, nr, err); if (!nodes) { if (err[0]) { char *ts = indi_tstamp(NULL); fprintf(stderr, "%s: Driver %s: XML error: %s\n", ts, dp->name, err); fprintf(stderr, "%s: Driver %s: XML read: %.*s\n", ts, dp->name, (int)nr, buf); shutdownDvr(dp, 1); return (-1); } return -1; } root = nodes[inode]; while (root) { char *roottag = tagXMLEle(root); const char *dev = findXMLAttValu(root, "device"); const char *name = findXMLAttValu(root, "name"); int isblob = !strcmp(tagXMLEle(root), "setBLOBVector"); Msg *mp; if (verbose > 2) { fprintf(stderr, "%s: Driver %s: read ", indi_tstamp(0), dp->name); traceMsg(root); } else if (verbose > 1) { fprintf(stderr, "%s: Driver %s: read <%s device='%s' name='%s'>\n", indi_tstamp(NULL), dp->name, tagXMLEle(root), findXMLAttValu(root, "device"), findXMLAttValu(root, "name")); } /* that's all if driver is just registering a snoop */ /* JM 2016-05-18: Send getProperties to upstream chained servers as well.*/ if (!strcmp(roottag, "getProperties")) { addSDevice(dp, dev, name); mp = newMsg(); /* send to interested chained servers upstream */ if (q2Servers(dp, mp, root) < 0) shutany++; /* Send to snooped drivers if they exist so that they can echo back the snooped propertly immediately */ q2RDrivers(dev, mp, root); if (mp->count > 0) setMsgXMLEle(mp, root); else freeMsg(mp); delXMLEle(root); inode++; root = nodes[inode]; continue; } /* that's all if driver desires to snoop BLOBs from other drivers */ if (!strcmp(roottag, "enableBLOB")) { Property *sp = findSDevice(dp, dev, name); if (sp) crackBLOB(pcdataXMLEle(root), &sp->blob); delXMLEle(root); inode++; root = nodes[inode]; continue; } /* Found a new device? Let's add it to driver info */ if (dev[0] && isDeviceInDriver(dev, dp) == 0) { dp->dev = (char **)realloc(dp->dev, (dp->ndev + 1) * sizeof(char *)); dp->dev[dp->ndev] = (char *)malloc(MAXINDIDEVICE * sizeof(char)); strncpy(dp->dev[dp->ndev], dev, MAXINDIDEVICE - 1); dp->dev[dp->ndev][MAXINDIDEVICE - 1] = '\0'; #ifdef OSX_EMBEDED_MODE if (!dp->ndev) fprintf(stderr, "STARTED \"%s\"\n", dp->name); fflush(stderr); #endif dp->ndev++; } /* log messages if any and wanted */ if (ldir) logDMsg(root, dev); /* build a new message -- set content iff anyone cares */ mp = newMsg(); /* send to interested clients */ if (q2Clients(NULL, isblob, dev, name, mp, root) < 0) shutany++; /* send to snooping drivers */ q2SDrivers(dp, isblob, dev, name, mp, root); /* set message content if anyone cares else forget it */ if (mp->count > 0) setMsgXMLEle(mp, root); else freeMsg(mp); delXMLEle(root); inode++; root = nodes[inode]; } free(nodes); return (shutany ? -1 : 0); } /* read more from the given driver stderr, add prefix and send to our stderr. * return 0 if ok else -1 if had to restart. */ static int stderrFromDriver(DvrInfo *dp) { static char exbuf[MAXRBUF]; static int nexbuf; ssize_t i, nr; /* read more */ nr = read(dp->efd, exbuf + nexbuf, sizeof(exbuf) - nexbuf); if (nr <= 0) { if (nr < 0) fprintf(stderr, "%s: Driver %s: stderr %s\n", indi_tstamp(NULL), dp->name, strerror(errno)); else fprintf(stderr, "%s: Driver %s: stderr EOF\n", indi_tstamp(NULL), dp->name); shutdownDvr(dp, 1); return (-1); } nexbuf += nr; /* prefix each whole line to our stderr, save extra for next time */ for (i = 0; i < nexbuf; i++) { if (exbuf[i] == '\n') { fprintf(stderr, "%s: Driver %s: %.*s\n", indi_tstamp(NULL), dp->name, (int)i, exbuf); i++; /* count including nl */ nexbuf -= i; /* remove from nexbuf */ memmove(exbuf, exbuf + i, nexbuf); /* slide remaining to front */ i = -1; /* restart for loop scan */ } } return (0); } /* close down the given client */ static void shutdownClient(ClInfo *cp) { Msg *mp; /* close connection */ shutdown(cp->s, SHUT_RDWR); close(cp->s); /* free memory */ delLilXML(cp->lp); free(cp->props); /* decrement and possibly free any unsent messages for this client */ while ((mp = (Msg *)popFQ(cp->msgq)) != NULL) if (--mp->count == 0) freeMsg(mp); delFQ(cp->msgq); /* ok now to recycle */ cp->active = 0; if (verbose > 0) fprintf(stderr, "%s: Client %d: shut down complete - bye!\n", indi_tstamp(NULL), cp->s); #ifdef OSX_EMBEDED_MODE int active = 0; for (int i = 0; i < nclinfo; i++) if (clinfo[i].active) active++; fprintf(stderr, "CLIENTS %d\n", active); fflush(stderr); #endif } /* close down the given driver and restart */ static void shutdownDvr(DvrInfo *dp, int restart) { Msg *mp; /* make sure it's dead, reclaim resources */ if (dp->pid == REMOTEDVR) { /* socket connection */ shutdown(dp->wfd, SHUT_RDWR); close(dp->wfd); /* same as rfd */ } else { /* local pipe connection */ kill(dp->pid, SIGKILL); /* we've insured there are no zombies */ close(dp->wfd); close(dp->rfd); close(dp->efd); } #ifdef OSX_EMBEDED_MODE fprintf(stderr, "STOPPED \"%s\"\n", dp->name); fflush(stderr); #endif /* free memory */ free(dp->sprops); free(dp->dev); delLilXML(dp->lp); /* ok now to recycle */ dp->active = 0; dp->ndev = 0; /* decrement and possibly free any unsent messages for this client */ while ((mp = (Msg *)popFQ(dp->msgq)) != NULL) if (--mp->count == 0) freeMsg(mp); delFQ(dp->msgq); if (restart) { if (dp->restarts >= maxrestarts) { fprintf(stderr, "%s: Driver %s: Terminated after #%d restarts.\n", indi_tstamp(NULL), dp->name, dp->restarts); // If we're not in FIFO mode and we do not have any more drivers, shutdown the server terminateddrv++; if ((ndvrinfo - terminateddrv) <= 0 && !fifo.name) Bye(); } else { fprintf(stderr, "%s: Driver %s: restart #%d\n", indi_tstamp(NULL), dp->name, ++dp->restarts); startDvr(dp); } } } /* put Msg mp on queue of each driver responsible for dev, or all drivers * if dev not specified. */ static void q2RDrivers(const char *dev, Msg *mp, XMLEle *root) { DvrInfo *dp; char *roottag = tagXMLEle(root); char lastRemoteHost[MAXSBUF]; int lastRemotePort = -1; /* queue message to each interested driver. * N.B. don't send generic getProps to more than one remote driver, * otherwise they all fan out and we get multiple responses back. */ for (dp = dvrinfo; dp < &dvrinfo[ndvrinfo]; dp++) { int isRemote = (dp->pid == REMOTEDVR); if (dp->active == 0) continue; /* driver known to not support this dev */ if (dev[0] && isDeviceInDriver(dev, dp) == 0) continue; /* Only send message to each *unique* remote driver at a particular host:port * Since it will be propogated to all other devices there */ if (!dev[0] && isRemote && !strcmp(lastRemoteHost, dp->host) && lastRemotePort == dp->port) continue; /* JM 2016-10-30: Only send enableBLOB to remote drivers */ if (isRemote == 0 && !strcmp(roottag, "enableBLOB")) continue; /* Retain last remote driver data so that we do not send the same info again to a driver * residing on the same host:port */ if (isRemote) { strncpy(lastRemoteHost, dp->host, MAXSBUF); lastRemotePort = dp->port; } /* ok: queue message to this driver */ mp->count++; pushFQ(dp->msgq, mp); if (verbose > 1) { fprintf(stderr, "%s: Driver %s: queuing responsible for <%s device='%s' name='%s'>\n", indi_tstamp(NULL), dp->name, tagXMLEle(root), findXMLAttValu(root, "device"), findXMLAttValu(root, "name")); } } } /* put Msg mp on queue of each driver snooping dev/name. * if BLOB always honor current mode. */ static void q2SDrivers(DvrInfo *me, int isblob, const char *dev, const char *name, Msg *mp, XMLEle *root) { DvrInfo *dp = NULL; for (dp = dvrinfo; dp < &dvrinfo[ndvrinfo]; dp++) { Property *sp = findSDevice(dp, dev, name); /* nothing for dp if not snooping for dev/name or wrong BLOB mode */ if (!sp) continue; if ((isblob && sp->blob == B_NEVER) || (!isblob && sp->blob == B_ONLY)) continue; if (me && me->pid == REMOTEDVR && dp->pid == REMOTEDVR) { // Do not send snoop data to remote drivers at the same host // since they will manage their own snoops remotely if (!strcmp(me->host, dp->host) && me->port == dp->port) continue; } /* ok: queue message to this device */ mp->count++; pushFQ(dp->msgq, mp); if (verbose > 1) { fprintf(stderr, "%s: Driver %s: queuing snooped <%s device='%s' name='%s'>\n", indi_tstamp(NULL), dp->name, tagXMLEle(root), findXMLAttValu(root, "device"), findXMLAttValu(root, "name")); } } } /* add dev/name to dp's snooping list. * init with blob mode set to B_NEVER. */ static void addSDevice(DvrInfo *dp, const char *dev, const char *name) { Property *sp; char *ip; /* no dups */ sp = findSDevice(dp, dev, name); if (sp) return; /* add dev to sdevs list */ dp->sprops = (Property *)realloc(dp->sprops, (dp->nsprops + 1) * sizeof(Property)); sp = &dp->sprops[dp->nsprops++]; ip = sp->dev; strncpy(ip, dev, MAXINDIDEVICE - 1); ip[MAXINDIDEVICE - 1] = '\0'; ip = sp->name; strncpy(ip, name, MAXINDINAME - 1); ip[MAXINDINAME - 1] = '\0'; sp->blob = B_NEVER; if (verbose) fprintf(stderr, "%s: Driver %s: snooping on %s.%s\n", indi_tstamp(NULL), dp->name, dev, name); } /* return Property if dp is snooping dev/name, else NULL. */ static Property *findSDevice(DvrInfo *dp, const char *dev, const char *name) { int i; for (i = 0; i < dp->nsprops; i++) { Property *sp = &dp->sprops[i]; if (!strcmp(sp->dev, dev) && (!sp->name[0] || !strcmp(sp->name, name))) return (sp); } return (NULL); } /* put Msg mp on queue of each client interested in dev/name, except notme. * if BLOB always honor current mode. * return -1 if had to shut down any clients, else 0. */ static int q2Clients(ClInfo *notme, int isblob, const char *dev, const char *name, Msg *mp, XMLEle *root) { int shutany = 0; ClInfo *cp; int ql, i = 0; /* queue message to each interested client */ for (cp = clinfo; cp < &clinfo[nclinfo]; cp++) { /* cp in use? notme? want this dev/name? blob? */ if (!cp->active || cp == notme) continue; if (findClDevice(cp, dev, name) < 0) continue; //if ((isblob && cp->blob==B_NEVER) || (!isblob && cp->blob==B_ONLY)) if (!isblob && cp->blob == B_ONLY) continue; if (isblob) { if (cp->nprops > 0) { Property *pp = NULL; int blob_found = 0; for (i = 0; i < cp->nprops; i++) { pp = &cp->props[i]; if (!strcmp(pp->dev, dev) && (!strcmp(pp->name, name))) { blob_found = 1; break; } } if ((blob_found && pp->blob == B_NEVER) || (blob_found == 0 && cp->blob == B_NEVER)) continue; } else if (cp->blob == B_NEVER) continue; } /* shut down this client if its q is already too large */ ql = msgQSize(cp->msgq); if (isblob && maxstreamsiz > 0 && ql > maxstreamsiz) { // Drop frames for streaming blobs /* pull out each name/BLOB pair, decode */ XMLEle *ep = NULL; int streamFound = 0; for (ep = nextXMLEle(root, 1); ep; ep = nextXMLEle(root, 0)) { if (strcmp(tagXMLEle(ep), "oneBLOB") == 0) { XMLAtt *fa = findXMLAtt(ep, "format"); if (fa && strstr(valuXMLAtt(fa), "stream")) { streamFound = 1; break; } } } if (streamFound) { if (verbose > 1) fprintf(stderr, "%s: Client %d: %d bytes behind. Dropping stream BLOB...\n", indi_tstamp(NULL), cp->s, ql); continue; } } if (ql > maxqsiz) { if (verbose) fprintf(stderr, "%s: Client %d: %d bytes behind, shutting down\n", indi_tstamp(NULL), cp->s, ql); shutdownClient(cp); shutany++; continue; } /* ok: queue message to this client */ mp->count++; pushFQ(cp->msgq, mp); if (verbose > 1) fprintf(stderr, "%s: Client %d: queuing <%s device='%s' name='%s'>\n", indi_tstamp(NULL), cp->s, tagXMLEle(root), findXMLAttValu(root, "device"), findXMLAttValu(root, "name")); } return (shutany ? -1 : 0); } /* put Msg mp on queue of each chained server client, except notme. * return -1 if had to shut down any clients, else 0. */ static int q2Servers(DvrInfo *me, Msg *mp, XMLEle *root) { int shutany = 0, i = 0, devFound = 0; ClInfo *cp; int ql = 0; /* queue message to each interested client */ for (cp = clinfo; cp < &clinfo[nclinfo]; cp++) { /* cp in use? not chained server? */ if (!cp->active || cp->allprops == 1) continue; // Only send the message to the upstream server that is connected specfically to the device in driver dp for (i = 0; i < cp->nprops; i++) { Property *pp = &cp->props[i]; int j = 0; for (j = 0; j < me->ndev; j++) { if (!strcmp(pp->dev, me->dev[j])) break; } if (j != me->ndev) { devFound = 1; break; } } // If no matching device found, continue if (devFound == 0) continue; /* shut down this client if its q is already too large */ ql = msgQSize(cp->msgq); if (ql > maxqsiz) { if (verbose) fprintf(stderr, "%s: Client %d: %d bytes behind, shutting down\n", indi_tstamp(NULL), cp->s, ql); shutdownClient(cp); shutany++; continue; } /* ok: queue message to this client */ mp->count++; pushFQ(cp->msgq, mp); if (verbose > 1) fprintf(stderr, "%s: Client %d: queuing <%s device='%s' name='%s'>\n", indi_tstamp(NULL), cp->s, tagXMLEle(root), findXMLAttValu(root, "device"), findXMLAttValu(root, "name")); } return (shutany ? -1 : 0); } /* return size of all Msqs on the given q */ static int msgQSize(FQ *q) { int i, l = 0; for (i = 0; i < nFQ(q); i++) { Msg *mp = (Msg *)peekiFQ(q, i); l += mp->cl; } return (l); } /* print root as content in Msg mp. */ static void setMsgXMLEle(Msg *mp, XMLEle *root) { /* want cl to only count content, but need room for final \0 */ mp->cl = sprlXMLEle(root, 0); if (mp->cl < sizeof(mp->buf)) mp->cp = mp->buf; else mp->cp = malloc(mp->cl + 1); sprXMLEle(mp->cp, root, 0); } /* save str as content in Msg mp. */ static void setMsgStr(Msg *mp, char *str) { /* want cl to only count content, but need room for final \0 */ mp->cl = strlen(str); if (mp->cl < sizeof(mp->buf)) mp->cp = mp->buf; else mp->cp = malloc(mp->cl + 1); strcpy(mp->cp, str); } /* return pointer to one new nulled Msg */ static Msg *newMsg(void) { return ((Msg *)calloc(1, sizeof(Msg))); } /* free Msg mp and everything it contains */ static void freeMsg(Msg *mp) { if (mp->cp && mp->cp != mp->buf) free(mp->cp); free(mp); } /* write the next chunk of the current message in the queue to the given * client. pop message from queue when complete and free the message if we are * the last one to use it. shut down this client if trouble. * N.B. we assume we will never be called with cp->msgq empty. * return 0 if ok else -1 if had to shut down. */ static int sendClientMsg(ClInfo *cp) { ssize_t nsend, nw; Msg *mp; /* get current message */ mp = (Msg *)peekFQ(cp->msgq); /* send next chunk, never more than MAXWSIZ to reduce blocking */ nsend = mp->cl - cp->nsent; if (nsend > MAXWSIZ) nsend = MAXWSIZ; nw = write(cp->s, &mp->cp[cp->nsent], nsend); /* shut down if trouble */ if (nw <= 0) { if (nw == 0) fprintf(stderr, "%s: Client %d: write returned 0\n", indi_tstamp(NULL), cp->s); else fprintf(stderr, "%s: Client %d: write: %s\n", indi_tstamp(NULL), cp->s, strerror(errno)); shutdownClient(cp); return (-1); } /* trace */ if (verbose > 2) { fprintf(stderr, "%s: Client %d: sending msg copy %d nq %d:\n%.*s\n", indi_tstamp(NULL), cp->s, mp->count, nFQ(cp->msgq), (int)nw, &mp->cp[cp->nsent]); } else if (verbose > 1) { fprintf(stderr, "%s: Client %d: sending %.50s\n", indi_tstamp(NULL), cp->s, &mp->cp[cp->nsent]); } /* update amount sent. when complete: free message if we are the last * to use it and pop from our queue. */ cp->nsent += nw; if (cp->nsent == mp->cl) { if (--mp->count == 0) freeMsg(mp); popFQ(cp->msgq); cp->nsent = 0; } return (0); } /* write the next chunk of the current message in the queue to the given * driver. pop message from queue when complete and free the message if we are * the last one to use it. restart this driver if touble. * N.B. we assume we will never be called with dp->msgq empty. * return 0 if ok else -1 if had to shut down. */ static int sendDriverMsg(DvrInfo *dp) { ssize_t nsend, nw; Msg *mp; /* get current message */ mp = (Msg *)peekFQ(dp->msgq); /* send next chunk, never more than MAXWSIZ to reduce blocking */ nsend = mp->cl - dp->nsent; if (nsend > MAXWSIZ) nsend = MAXWSIZ; nw = write(dp->wfd, &mp->cp[dp->nsent], nsend); /* restart if trouble */ if (nw <= 0) { if (nw == 0) fprintf(stderr, "%s: Driver %s: write returned 0\n", indi_tstamp(NULL), dp->name); else fprintf(stderr, "%s: Driver %s: write: %s\n", indi_tstamp(NULL), dp->name, strerror(errno)); shutdownDvr(dp, 1); return (-1); } /* trace */ if (verbose > 2) { fprintf(stderr, "%s: Driver %s: sending msg copy %d nq %d:\n%.*s\n", indi_tstamp(NULL), dp->name, mp->count, nFQ(dp->msgq), (int)nw, &mp->cp[dp->nsent]); } else if (verbose > 1) { fprintf(stderr, "%s: Driver %s: sending %.50s\n", indi_tstamp(NULL), dp->name, &mp->cp[dp->nsent]); } /* update amount sent. when complete: free message if we are the last * to use it and pop from our queue. */ dp->nsent += nw; if (dp->nsent == mp->cl) { if (--mp->count == 0) freeMsg(mp); popFQ(dp->msgq); dp->nsent = 0; } return (0); } /* return 0 if cp may be interested in dev/name else -1 */ static int findClDevice(ClInfo *cp, const char *dev, const char *name) { int i; if (cp->allprops || !dev[0]) return (0); for (i = 0; i < cp->nprops; i++) { Property *pp = &cp->props[i]; if (!strcmp(pp->dev, dev) && (!pp->name[0] || !strcmp(pp->name, name))) return (0); } return (-1); } /* add the given device and property to the devs[] list of client if new. */ static void addClDevice(ClInfo *cp, const char *dev, const char *name, int isblob) { Property *pp; //char *ip; int i = 0; if (isblob) { for (i = 0; i < cp->nprops; i++) { Property *pp = &cp->props[i]; if (!strcmp(pp->dev, dev) && (name == NULL || !strcmp(pp->name, name))) return; } } /* no dups */ else if (!findClDevice(cp, dev, name)) return; /* add */ cp->props = (Property *)realloc(cp->props, (cp->nprops + 1) * sizeof(Property)); pp = &cp->props[cp->nprops++]; /*ip = pp->dev; strncpy (ip, dev, MAXINDIDEVICE-1); ip[MAXINDIDEVICE-1] = '\0'; ip = pp->name; strncpy (ip, name, MAXINDINAME-1); ip[MAXINDINAME-1] = '\0';*/ strncpy(pp->dev, dev, MAXINDIDEVICE); strncpy(pp->name, name, MAXINDINAME); pp->blob = B_NEVER; } /* block to accept a new client arriving on lsocket. * return private nonblocking socket or exit. */ static int newClSocket() { struct sockaddr_in cli_socket; socklen_t cli_len; int cli_fd; /* get a private connection to new client */ cli_len = sizeof(cli_socket); cli_fd = accept(lsocket, (struct sockaddr *)&cli_socket, &cli_len); if (cli_fd < 0) { fprintf(stderr, "accept: %s\n", strerror(errno)); Bye(); } /* ok */ return (cli_fd); } /* convert the string value of enableBLOB to our B_ state value. * no change if unrecognized */ static void crackBLOB(const char *enableBLOB, BLOBHandling *bp) { if (!strcmp(enableBLOB, "Also")) *bp = B_ALSO; else if (!strcmp(enableBLOB, "Only")) *bp = B_ONLY; else if (!strcmp(enableBLOB, "Never")) *bp = B_NEVER; } /* Update the client property BLOB handling policy */ static void crackBLOBHandling(const char *dev, const char *name, const char *enableBLOB, ClInfo *cp) { int i = 0; /* If we have EnableBLOB with property name, we add it to Client device list */ if (name[0]) addClDevice(cp, dev, name, 1); else /* Otherwise, we set the whole client blob handling to what's passed (enableBLOB) */ crackBLOB(enableBLOB, &cp->blob); /* If whole client blob handling policy was updated, we need to pass that also to all children and if the request was for a specific property, then we apply the policy to it */ for (i = 0; i < cp->nprops; i++) { Property *pp = &cp->props[i]; if (!name[0]) crackBLOB(enableBLOB, &pp->blob); else if (!strcmp(pp->dev, dev) && (!strcmp(pp->name, name))) { crackBLOB(enableBLOB, &pp->blob); return; } } } /* print key attributes and values of the given xml to stderr. */ static void traceMsg(XMLEle *root) { static const char *prtags[] = { "defNumber", "oneNumber", "defText", "oneText", "defSwitch", "oneSwitch", "defLight", "oneLight", }; XMLEle *e; const char *msg, *perm, *pcd; unsigned int i; /* print tag header */ fprintf(stderr, "%s %s %s %s", tagXMLEle(root), findXMLAttValu(root, "device"), findXMLAttValu(root, "name"), findXMLAttValu(root, "state")); pcd = pcdataXMLEle(root); if (pcd[0]) fprintf(stderr, " %s", pcd); perm = findXMLAttValu(root, "perm"); if (perm[0]) fprintf(stderr, " %s", perm); msg = findXMLAttValu(root, "message"); if (msg[0]) fprintf(stderr, " '%s'", msg); /* print each array value */ for (e = nextXMLEle(root, 1); e; e = nextXMLEle(root, 0)) for (i = 0; i < sizeof(prtags) / sizeof(prtags[0]); i++) if (strcmp(prtags[i], tagXMLEle(e)) == 0) fprintf(stderr, "\n %10s='%s'", findXMLAttValu(e, "name"), pcdataXMLEle(e)); fprintf(stderr, "\n"); } /* fill s with current UT string. * if no s, use a static buffer * return s or buffer. * N.B. if use our buffer, be sure to use before calling again */ static char *indi_tstamp(char *s) { static char sbuf[64]; struct tm *tp; time_t t; time(&t); tp = gmtime(&t); if (!s) s = sbuf; strftime(s, sizeof(sbuf), "%Y-%m-%dT%H:%M:%S", tp); return (s); } /* log message in root known to be from device dev to ldir, if any. */ static void logDMsg(XMLEle *root, const char *dev) { char stamp[64]; char logfn[1024]; const char *ts, *ms; FILE *fp; /* get message, if any */ ms = findXMLAttValu(root, "message"); if (!ms[0]) return; /* get timestamp now if not provided */ ts = findXMLAttValu(root, "timestamp"); if (!ts[0]) { indi_tstamp(stamp); ts = stamp; } /* append to log file, name is date portion of time stamp */ sprintf(logfn, "%s/%.10s.islog", ldir, ts); fp = fopen(logfn, "a"); if (!fp) return; /* oh well */ fprintf(fp, "%s: %s: %s\n", ts, dev, ms); fclose(fp); } /* log when then exit */ static void Bye() { fprintf(stderr, "%s: good bye\n", indi_tstamp(NULL)); exit(1); } libindi/INSTALL0000664000175000017500000000100113263645557012513 0ustar jasemjasemINDI Library Setup 1.0.0 ======================== You must have CMake >= 2.8 in order to build this package. 1) $ tar -xzf libindi.tar.gz 2) $ mkdir libindi_build 3) $ cd libindi_build 4) $ cmake -DCMAKE_INSTALL_PREFIX=/usr . ../libindi 5) $ su -c 'make install' or sudo make install Refer to README for instructions on running indiserver and device drivers. Refer to README.drivers for driver-specific information. Dependencies ============ + libusb-1.0-0-dev + libnova-dev + cfitsio-dev + libgsl0-dev libindi/NEWS0000664000175000017500000000035113263645557012170 0ustar jasemjasem------------------------------------------------------------------------- News ------------------------------------------------------------------------- For the latest news on INDI, refer to INDI's website @ http://www.indilib.org libindi/AUTHORS0000664000175000017500000000041613263645557012543 0ustar jasemjasemJasem Mutlaq Elwood C. Downey INDI drivers were written by numerous volunteers across the globe. Please refer to each driver to find the author's contact info. Thank you for making INDI such a great software! libindi/LICENSE0000664000175000017500000006351013263645557012504 0ustar jasemjasem GNU LESSER GENERAL PUBLIC LICENSE Version 2.1, February 1999 Copyright (C) 1991, 1999 Free Software Foundation, Inc. 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. [This is the first released version of the Lesser GPL. It also counts as the successor of the GNU Library Public License, version 2, hence the version number 2.1.] Preamble The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public Licenses are intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This license, the Lesser General Public License, applies to some specially designated software packages--typically libraries--of the Free Software Foundation and other authors who decide to use it. You can use it too, but we suggest you first think carefully about whether this license or the ordinary General Public License is the better strategy to use in any particular case, based on the explanations below. When we speak of free software, we are referring to freedom of use, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish); that you receive source code or can get it if you want it; that you can change the software and use pieces of it in new free programs; and that you are informed that you can do these things. To protect your rights, we need to make restrictions that forbid distributors to deny you these rights or to ask you to surrender these rights. These restrictions translate to certain responsibilities for you if you distribute copies of the library or if you modify it. For example, if you distribute copies of the library, whether gratis or for a fee, you must give the recipients all the rights that we gave you. You must make sure that they, too, receive or can get the source code. If you link other code with the library, you must provide complete object files to the recipients, so that they can relink them with the library after making changes to the library and recompiling it. And you must show them these terms so they know their rights. We protect your rights with a two-step method: (1) we copyright the library, and (2) we offer you this license, which gives you legal permission to copy, distribute and/or modify the library. To protect each distributor, we want to make it very clear that there is no warranty for the free library. Also, if the library is modified by someone else and passed on, the recipients should know that what they have is not the original version, so that the original author's reputation will not be affected by problems that might be introduced by others. Finally, software patents pose a constant threat to the existence of any free program. We wish to make sure that a company cannot effectively restrict the users of a free program by obtaining a restrictive license from a patent holder. Therefore, we insist that any patent license obtained for a version of the library must be consistent with the full freedom of use specified in this license. Most GNU software, including some libraries, is covered by the ordinary GNU General Public License. This license, the GNU Lesser General Public License, applies to certain designated libraries, and is quite different from the ordinary General Public License. We use this license for certain libraries in order to permit linking those libraries into non-free programs. When a program is linked with a library, whether statically or using a shared library, the combination of the two is legally speaking a combined work, a derivative of the original library. The ordinary General Public License therefore permits such linking only if the entire combination fits its criteria of freedom. The Lesser General Public License permits more lax criteria for linking other code with the library. We call this license the "Lesser" General Public License because it does Less to protect the user's freedom than the ordinary General Public License. It also provides other free software developers Less of an advantage over competing non-free programs. These disadvantages are the reason we use the ordinary General Public License for many libraries. However, the Lesser license provides advantages in certain special circumstances. For example, on rare occasions, there may be a special need to encourage the widest possible use of a certain library, so that it becomes a de-facto standard. To achieve this, non-free programs must be allowed to use the library. A more frequent case is that a free library does the same job as widely used non-free libraries. In this case, there is little to gain by limiting the free library to free software only, so we use the Lesser General Public License. In other cases, permission to use a particular library in non-free programs enables a greater number of people to use a large body of free software. For example, permission to use the GNU C Library in non-free programs enables many more people to use the whole GNU operating system, as well as its variant, the GNU/Linux operating system. Although the Lesser General Public License is Less protective of the users' freedom, it does ensure that the user of a program that is linked with the Library has the freedom and the wherewithal to run that program using a modified version of the Library. The precise terms and conditions for copying, distribution and modification follow. Pay close attention to the difference between a "work based on the library" and a "work that uses the library". The former contains code derived from the library, whereas the latter must be combined with the library in order to run. GNU LESSER GENERAL PUBLIC LICENSE TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 0. This License Agreement applies to any software library or other program which contains a notice placed by the copyright holder or other authorized party saying it may be distributed under the terms of this Lesser General Public License (also called "this License"). Each licensee is addressed as "you". A "library" means a collection of software functions and/or data prepared so as to be conveniently linked with application programs (which use some of those functions and data) to form executables. The "Library", below, refers to any such software library or work which has been distributed under these terms. A "work based on the Library" means either the Library or any derivative work under copyright law: that is to say, a work containing the Library or a portion of it, either verbatim or with modifications and/or translated straightforwardly into another language. (Hereinafter, translation is included without limitation in the term "modification".) "Source code" for a work means the preferred form of the work for making modifications to it. For a library, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the library. Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running a program using the Library is not restricted, and output from such a program is covered only if its contents constitute a work based on the Library (independent of the use of the Library in a tool for writing it). Whether that is true depends on what the Library does and what the program that uses the Library does. 1. You may copy and distribute verbatim copies of the Library's complete source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and distribute a copy of this License along with the Library. You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. 2. You may modify your copy or copies of the Library or any portion of it, thus forming a work based on the Library, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions: a) The modified work must itself be a software library. b) You must cause the files modified to carry prominent notices stating that you changed the files and the date of any change. c) You must cause the whole of the work to be licensed at no charge to all third parties under the terms of this License. d) If a facility in the modified Library refers to a function or a table of data to be supplied by an application program that uses the facility, other than as an argument passed when the facility is invoked, then you must make a good faith effort to ensure that, in the event an application does not supply such function or table, the facility still operates, and performs whatever part of its purpose remains meaningful. (For example, a function in a library to compute square roots has a purpose that is entirely well-defined independent of the application. Therefore, Subsection 2d requires that any application-supplied function or table used by this function must be optional: if the application does not supply it, the square root function must still compute square roots.) These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Library, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Library, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Library. In addition, mere aggregation of another work not based on the Library with the Library (or with a work based on the Library) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. 3. You may opt to apply the terms of the ordinary GNU General Public License instead of this License to a given copy of the Library. To do this, you must alter all the notices that refer to this License, so that they refer to the ordinary GNU General Public License, version 2, instead of to this License. (If a newer version than version 2 of the ordinary GNU General Public License has appeared, then you can specify that version instead if you wish.) Do not make any other change in these notices. Once this change is made in a given copy, it is irreversible for that copy, so the ordinary GNU General Public License applies to all subsequent copies and derivative works made from that copy. This option is useful when you wish to copy part of the code of the Library into a program that is not a library. 4. You may copy and distribute the Library (or a portion or derivative of it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange. If distribution of object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place satisfies the requirement to distribute the source code, even though third parties are not compelled to copy the source along with the object code. 5. A program that contains no derivative of any portion of the Library, but is designed to work with the Library by being compiled or linked with it, is called a "work that uses the Library". Such a work, in isolation, is not a derivative work of the Library, and therefore falls outside the scope of this License. However, linking a "work that uses the Library" with the Library creates an executable that is a derivative of the Library (because it contains portions of the Library), rather than a "work that uses the library". The executable is therefore covered by this License. Section 6 states terms for distribution of such executables. When a "work that uses the Library" uses material from a header file that is part of the Library, the object code for the work may be a derivative work of the Library even though the source code is not. Whether this is true is especially significant if the work can be linked without the Library, or if the work is itself a library. The threshold for this to be true is not precisely defined by law. If such an object file uses only numerical parameters, data structure layouts and accessors, and small macros and small inline functions (ten lines or less in length), then the use of the object file is unrestricted, regardless of whether it is legally a derivative work. (Executables containing this object code plus portions of the Library will still fall under Section 6.) Otherwise, if the work is a derivative of the Library, you may distribute the object code for the work under the terms of Section 6. Any executables containing that work also fall under Section 6, whether or not they are linked directly with the Library itself. 6. As an exception to the Sections above, you may also combine or link a "work that uses the Library" with the Library to produce a work containing portions of the Library, and distribute that work under terms of your choice, provided that the terms permit modification of the work for the customer's own use and reverse engineering for debugging such modifications. You must give prominent notice with each copy of the work that the Library is used in it and that the Library and its use are covered by this License. You must supply a copy of this License. If the work during execution displays copyright notices, you must include the copyright notice for the Library among them, as well as a reference directing the user to the copy of this License. Also, you must do one of these things: a) Accompany the work with the complete corresponding machine-readable source code for the Library including whatever changes were used in the work (which must be distributed under Sections 1 and 2 above); and, if the work is an executable linked with the Library, with the complete machine-readable "work that uses the Library", as object code and/or source code, so that the user can modify the Library and then relink to produce a modified executable containing the modified Library. (It is understood that the user who changes the contents of definitions files in the Library will not necessarily be able to recompile the application to use the modified definitions.) b) Use a suitable shared library mechanism for linking with the Library. A suitable mechanism is one that (1) uses at run time a copy of the library already present on the user's computer system, rather than copying library functions into the executable, and (2) will operate properly with a modified version of the library, if the user installs one, as long as the modified version is interface-compatible with the version that the work was made with. c) Accompany the work with a written offer, valid for at least three years, to give the same user the materials specified in Subsection 6a, above, for a charge no more than the cost of performing this distribution. d) If distribution of the work is made by offering access to copy from a designated place, offer equivalent access to copy the above specified materials from the same place. e) Verify that the user has already received a copy of these materials or that you have already sent this user a copy. For an executable, the required form of the "work that uses the Library" must include any data and utility programs needed for reproducing the executable from it. However, as a special exception, the materials to be distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable. It may happen that this requirement contradicts the license restrictions of other proprietary libraries that do not normally accompany the operating system. Such a contradiction means you cannot use both them and the Library together in an executable that you distribute. 7. You may place library facilities that are a work based on the Library side-by-side in a single library together with other library facilities not covered by this License, and distribute such a combined library, provided that the separate distribution of the work based on the Library and of the other library facilities is otherwise permitted, and provided that you do these two things: a) Accompany the combined library with a copy of the same work based on the Library, uncombined with any other library facilities. This must be distributed under the terms of the Sections above. b) Give prominent notice with the combined library of the fact that part of it is a work based on the Library, and explaining where to find the accompanying uncombined form of the same work. 8. You may not copy, modify, sublicense, link with, or distribute the Library except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense, link with, or distribute the Library is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. 9. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Library or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Library (or any work based on the Library), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Library or works based on it. 10. Each time you redistribute the Library (or any work based on the Library), the recipient automatically receives a license from the original licensor to copy, distribute, link with or modify the Library subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties with this License. 11. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Library at all. For example, if a patent license would not permit royalty-free redistribution of the Library by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Library. If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply, and the section as a whole is intended to apply in other circumstances. It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice. This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. 12. If the distribution and/or use of the Library is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Library under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License. 13. The Free Software Foundation may publish revised and/or new versions of the Lesser General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Library specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Library does not specify a license version number, you may choose any version ever published by the Free Software Foundation. 14. If you wish to incorporate parts of the Library into other free programs whose distribution conditions are incompatible with these, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. NO WARRANTY 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Libraries If you develop a new library, and you want it to be of the greatest possible use to the public, we recommend making it free software that everyone can redistribute and change. You can do so by permitting redistribution under these terms (or, alternatively, under the terms of the ordinary General Public License). To apply these terms, attach the following notices to the library. It is safest to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) 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 Also add information on how to contact you by electronic and paper mail. You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the library, if necessary. Here is a sample; alter the names: Yoyodyne, Inc., hereby disclaims all copyright interest in the library `Frob' (a library for tweaking knobs) written by James Random Hacker. , 1 April 1990 Ty Coon, President of Vice That's all there is to it! libindi/libs/0000775000175000017500000000000013263645557012423 5ustar jasemjasemlibindi/libs/indicom.h0000664000175000017500000002225413263645557014223 0ustar jasemjasem/* INDI LIB Common routines used by all drivers Copyright (C) 2003 by Jason Harris (jharris@30doradus.org) Elwood C. Downey Jasem Mutlaq 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 */ /** \file indicom.h \brief Implementations for common driver routines. The INDI Common Routine Library provides formatting and serial routines employed by many INDI drivers. Currently, the library is composed of the following sections:
  • Formatting Functions
  • Conversion Functions
  • TTY Functions
\author Jason Harris \author Elwood C. Downey \author Jasem Mutlaq */ #pragma once #define J2000 2451545.0 #define ERRMSG_SIZE 1024 #define STELLAR_DAY 86164.098903691 #define TRACKRATE_SIDEREAL ((360.0 * 3600.0) / STELLAR_DAY) #define SOLAR_DAY 86400 #define TRACKRATE_SOLAR ((360.0 * 3600.0) / SOLAR_DAY) #define TRACKRATE_LUNAR 14.511415 extern const char *Direction[]; extern const char *SolarSystem[]; struct ln_date; /* TTY Error Codes */ enum TTY_ERROR { TTY_OK = 0, TTY_READ_ERROR = -1, TTY_WRITE_ERROR = -2, TTY_SELECT_ERROR = -3, TTY_TIME_OUT = -4, TTY_PORT_FAILURE = -5, TTY_PARAM_ERROR = -6, TTY_ERRNO = -7, TTY_OVERFLOW = -8 }; #ifdef __cplusplus extern "C" { #endif /** * \defgroup ttyFunctions TTY Functions: Functions to perform common terminal access routines. */ /*@{*/ /** \brief read buffer from terminal \param fd file descriptor \param buf pointer to store data. Must be initilized and big enough to hold data. \param nbytes number of bytes to read. \param timeout number of seconds to wait for terminal before a timeout error is issued. \param nbytes_read the number of bytes read. \return On success, it returns TTY_OK, otherwise, a TTY_ERROR code. */ int tty_read(int fd, char *buf, int nbytes, int timeout, int *nbytes_read); /** \brief read buffer from terminal with a delimiter \param fd file descriptor \param buf pointer to store data. Must be initilized and big enough to hold data. \param stop_char if the function encounters \e stop_char then it stops reading and returns the buffer. \param timeout number of seconds to wait for terminal before a timeout error is issued. \param nbytes_read the number of bytes read. \return On success, it returns TTY_OK, otherwise, a TTY_ERROR code. */ int tty_read_section(int fd, char *buf, char stop_char, int timeout, int *nbytes_read); /** \brief read buffer from terminal with a delimiter \param fd file descriptor \param buf pointer to store data. Must be initilized and big enough to hold data. \param stop_char if the function encounters \e stop_char then it stops reading and returns the buffer. \param nsize size of buf. If stop character is not encountered before nsize, the function aborts. \param timeout number of seconds to wait for terminal before a timeout error is issued. \param nbytes_read the number of bytes read. \return On success, it returns TTY_OK, otherwise, a TTY_ERROR code. */ int tty_nread_section(int fd, char *buf, int nsize, char stop_char, int timeout, int *nbytes_read); /** \brief Writes a buffer to fd. \param fd file descriptor \param buffer a null-terminated buffer to write to fd. \param nbytes number of bytes to write from \e buffer \param nbytes_written the number of bytes written \return On success, it returns TTY_OK, otherwise, a TTY_ERROR code. */ int tty_write(int fd, const char *buffer, int nbytes, int *nbytes_written); /** \brief Writes a null terminated string to fd. \param fd file descriptor \param buffer the buffer to write to fd. \param nbytes_written the number of bytes written \return On success, it returns TTY_OK, otherwise, a TTY_ERROR code. */ int tty_write_string(int fd, const char *buffer, int *nbytes_written); /** \brief Establishes a tty connection to a terminal device. \param device the device node. e.g. /dev/ttyS0 \param bit_rate bit rate \param word_size number of data bits, 7 or 8, USE 8 DATA BITS with modbus \param parity 0=no parity, 1=parity EVEN, 2=parity ODD \param stop_bits number of stop bits : 1 or 2 \param fd \e fd is set to the file descriptor value on success. \return On success, it returns TTY_OK, otherwise, a TTY_ERROR code. \author Wildi Markus */ int tty_connect(const char *device, int bit_rate, int word_size, int parity, int stop_bits, int *fd); /** \brief Closes a tty connection and flushes the bus. \param fd the file descriptor to close. \return On success, it returns TTY_OK, otherwise, a TTY_ERROR code. */ int tty_disconnect(int fd); /** \brief Retrieve the tty error message \param err_code the error code return by any TTY function. \param err_msg an initialized buffer to hold the error message. \param err_msg_len length in bytes of \e err_msg */ void tty_error_msg(int err_code, char *err_msg, int err_msg_len); /** * @brief tty_set_debug Enable or disable debug which prints verbose information. * @param debug 1 to enable, 0 to disable */ void tty_set_debug(int debug); void tty_set_gemini_udp_format(int enabled); int tty_timeout(int fd, int timeout); /*@}*/ /** * \defgroup convertFunctions Formatting Functions: Functions to perform handy formatting and conversion routines. */ /*@{*/ /** \brief Converts a sexagesimal number to a string. sprint the variable a in sexagesimal format into out[]. \param out a pointer to store the sexagesimal number. \param a the sexagesimal number to convert. \param w the number of spaces in the whole part. \param fracbase is the number of pieces a whole is to broken into; valid options:\n \li 360000: \:mm:ss.ss \li 36000: \:mm:ss.s \li 3600: \:mm:ss \li 600: \:mm.m \li 60: \:mm \return number of characters written to out, not counting final null terminator. */ int fs_sexa(char *out, double a, int w, int fracbase); /** \brief convert sexagesimal string str AxBxC to double. x can be anything non-numeric. Any missing A, B or C will be assumed 0. Optional - and + can be anywhere. \param str0 string containing sexagesimal number. \param dp pointer to a double to store the sexagesimal number. \return return 0 if ok, -1 if can't find a thing. */ int f_scansexa(const char *str0, double *dp); /** \brief Extract ISO 8601 time and store it in a tm struct. \param timestr a string containing date and time in ISO 8601 format. \param iso_date a pointer to a \e ln_date structure to store the extracted time and date (libnova). \return 0 on success, -1 on failure. */ #ifndef _WIN32 int extractISOTime(const char *timestr, struct ln_date *iso_date); #endif void getSexComponents(double value, int *d, int *m, int *s); void getSexComponentsIID(double value, int *d, int *m, double *s); /** \brief Fill buffer with properly formatted INumber string. \param buf to store the formatted string. \param format format in sprintf style. \param value the number to format. \return length of string. \note buf must be of length MAXINDIFORMAT at minimum */ int numberFormat(char *buf, const char *format, double value); /** \brief Create an ISO 8601 formatted time stamp. The format is YYYY-MM-DDTHH:MM:SS \return The formatted time stamp. */ const char *timestamp(); /** * @brief rangeHA Limits the hour angle value to be between -12 ---> 12 * @param r current hour angle value * @return Limited value (-12,12) */ double rangeHA(double r); /** * @brief range24 Limits a number to be between 0-24 range. * @param r number to be limited * @return Limited number */ double range24(double r); /** * @brief range360 Limits an angle to be between 0-360 degrees. * @param r angle * @return Limited angle */ double range360(double r); /** * @brief rangeDec Limits declination value to be in -90 to 90 range. * @param r declination angle * @return limited declination */ double rangeDec(double r); /** * @brief get_local_sidereal_time Returns local sideral time given longitude and system clock. * @param longitude Longitude in INDI format (0 to 360) increasing eastward. * @return Local Sidereal Time. */ double get_local_sidereal_time(double longitude); /** * @brief get_local_hour_angle Returns local hour angle of an object * @param local_sideral_time Local Sideral Time * @param ra RA of object * @return Hour angle in hours (-12 to 12) */ double get_local_hour_angle(double local_sideral_time, double ra); /*@}*/ #ifdef __cplusplus } #endif libindi/libs/lilxml.c0000664000175000017500000010501313263645557014070 0ustar jasemjasem#if 0 liblilxml Copyright (C) 2003 Elwood C. Downey 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 /* little DOM-style XML parser. * only handles elements, attributes and pcdata content. * and are silently ignored. * pcdata is collected into one string, sans leading whitespace first line. * * #define MAIN_TST to create standalone test program */ #include #include #include #include #if defined(_MSC_VER) #define snprintf _snprintf #pragma warning(push) ///@todo Introduce plattform indipendent safe functions as macros to fix this #pragma warning(disable : 4996) #endif #include "lilxml.h" /* used to efficiently manage growing malloced string space */ typedef struct { char *s; /* malloced memory for string */ int sl; /* string length, sans trailing \0 */ int sm; /* total malloced bytes */ } String; #define MINMEM 64 /* starting string length */ static int oneXMLchar(LilXML *lp, int c, char ynot[]); static void initParser(LilXML *lp); static void pushXMLEle(LilXML *lp); static void popXMLEle(LilXML *lp); static void resetEndTag(LilXML *lp); static XMLAtt *growAtt(XMLEle *e); static XMLEle *growEle(XMLEle *pe); static void freeAtt(XMLAtt *a); static int isTokenChar(int start, int c); static void growString(String *sp, int c); static void appendString(String *sp, const char *str); static void freeString(String *sp); static void newString(String *sp); static void *moremem(void *old, int n); typedef enum { LOOK4START = 0, /* looking for first element start */ LOOK4TAG, /* looking for element tag */ INTAG, /* reading tag */ LOOK4ATTRN, /* looking for attr name, > or / */ INATTRN, /* reading attr name */ LOOK4ATTRV, /* looking for attr value */ SAWSLASH, /* saw / in element opening */ INATTRV, /* in attr value */ ENTINATTRV, /* in entity in attr value */ LOOK4CON, /* skipping leading content whitespc */ INCON, /* reading content */ ENTINCON, /* in entity in pcdata */ SAWLTINCON, /* saw < in content */ LOOK4CLOSETAG, /* looking for closing tag after < */ INCLOSETAG /* reading closing tag */ } State; /* parsing states */ /* maintain state while parsing */ struct _LilXML { State cs; /* current state */ int ln; /* line number for diags */ XMLEle *ce; /* current element being built */ String endtag; /* to check for match with opening tag*/ String entity; /* collect entity seq */ int delim; /* attribute value delimiter */ int lastc; /* last char (just used wiht skipping)*/ int skipping; /* in comment or declaration */ int inblob; /* in oneBLOB element */ }; /* internal representation of a (possibly nested) XML element */ struct _xml_ele { String tag; /* element tag */ XMLEle *pe; /* parent element, or NULL if root */ XMLAtt **at; /* list of attributes */ int nat; /* number of attributes */ int ait; /* used to iterate over at[] */ XMLEle **el; /* list of child elements */ int nel; /* number of child elements */ int eit; /* used to iterate over el[] */ String pcdata; /* character data in this element */ int pcdata_hasent; /* 1 if pcdata contains an entity char*/ }; /* internal representation of an attribute */ struct _xml_att { String name; /* name */ String valu; /* value */ XMLEle *ce; /* containing element */ }; /* characters that need escaping as "entities" in attr values and pcdata */ static char entities[] = "&<>'\""; /* default memory managers, override with lilxmlMalloc() */ static void *(*mymalloc)(size_t size) = malloc; static void *(*myrealloc)(void *ptr, size_t size) = realloc; static void (*myfree)(void *ptr) = free; /* install new version of malloc/realloc/free. * N.B. don't call after first use of any other lilxml function */ void lilxmlMalloc(void *(*newmalloc)(size_t size), void *(*newrealloc)(void *ptr, size_t size), void (*newfree)(void *ptr)) { mymalloc = newmalloc; myrealloc = newrealloc; myfree = newfree; } /* pass back a fresh handle for use with our other functions */ LilXML *newLilXML() { LilXML *lp = (LilXML *)moremem(NULL, sizeof(LilXML)); memset(lp, 0, sizeof(LilXML)); initParser(lp); return (lp); } /* discard */ void delLilXML(LilXML *lp) { delXMLEle(lp->ce); freeString(&lp->endtag); (*myfree)(lp); } /* delete ep and all its children and remove from parent's list if known */ void delXMLEle(XMLEle *ep) { int i; /* benign if NULL */ if (!ep) return; /* delete all parts of ep */ freeString(&ep->tag); freeString(&ep->pcdata); if (ep->at) { for (i = 0; i < ep->nat; i++) freeAtt(ep->at[i]); (*myfree)(ep->at); } if (ep->el) { for (i = 0; i < ep->nel; i++) { /* forget parent so deleting doesn't modify _this_ el[] */ ep->el[i]->pe = NULL; delXMLEle(ep->el[i]); } (*myfree)(ep->el); } /* remove from parent's list if known */ if (ep->pe) { XMLEle *pe = ep->pe; for (i = 0; i < pe->nel; i++) { if (pe->el[i] == ep) { memmove(&pe->el[i], &pe->el[i + 1], (--pe->nel - i) * sizeof(XMLEle *)); break; } } } /* delete ep itself */ (*myfree)(ep); } //#define WITH_MEMCHR XMLEle **parseXMLChunk(LilXML *lp, char *buf, int size, char ynot[]) { XMLEle **nodes = (XMLEle **)malloc(sizeof(XMLEle *)); int nnodes = 1; *nodes = NULL; char *curr = buf; int s; ynot[0] = '\0'; if (lp->inblob) { #ifdef WITH_ENCLEN if (size < lp->ce->pcdata.sm - lp->ce->pcdata.sl) { memcpy((void *)(lp->ce->pcdata.s + lp->ce->pcdata.sl), (const void *)buf, size); lp->ce->pcdata.sl += size; return nodes; } else lp->inblob = 0; #endif #ifdef WITH_MEMCHR char *ltpos = memchr(buf, '<', size); if (!ltpos) { lp->ce->pcdata.s = (char *)moremem(lp->ce->pcdata.s, lp->ce->pcdata.sm + size); lp->ce->pcdata.sm += size; memcpy((void *)(lp->ce->pcdata.s + lp->ce->pcdata.sl), (const void *)buf, size); lp->ce->pcdata.sl += size; return nodes; } else lp->inblob = 0; #endif } else { if (lp->ce) { char *ctag = tagXMLEle(lp->ce); if (ctag && !(strcmp(ctag, "oneBLOB")) && (lp->cs == INCON)) { #ifdef WITH_ENCLEN XMLAtt *blenatt = findXMLAtt(lp->ce, "enclen"); if (blenatt) { int blen; sscanf(valuXMLAtt(blenatt), "%d", &blen); // if (lp->ce->pcdata.sm < blen) { // always realloc if (blen % 72 != 0) blen += (blen / 72) + 1; // add room for those '\n' else blen += (blen / 72); lp->ce->pcdata.s = (char *)moremem(lp->ce->pcdata.s, blen); lp->ce->pcdata.sm = blen; // or always set sm //} if (size < blen - lp->ce->pcdata.sl) { memcpy((void *)(lp->ce->pcdata.s + lp->ce->pcdata.sl), (const void *)buf, size); lp->ce->pcdata.sl += size; lp->inblob = 1; return nodes; } } #endif #ifdef WITH_MEMCHR char *ltpos = memchr(buf, '<', size); if (!ltpos) { lp->ce->pcdata.s = (char *)moremem(lp->ce->pcdata.s, lp->ce->pcdata.sm + size); lp->ce->pcdata.sm += size; memcpy((void *)(lp->ce->pcdata.s + lp->ce->pcdata.sl), (const void *)buf, size); lp->ce->pcdata.sl += size; lp->inblob = 1; return nodes; } else lp->inblob = 0; #endif } } } while (curr - buf < size) { char newc = *curr; /* EOF? */ if (newc == 0) { sprintf(ynot, "Line %d: early XML EOF", lp->ln); initParser(lp); curr++; continue; } /* new line? */ if (newc == '\n') lp->ln++; /* skip comments and declarations. requires 1 char history */ if (!lp->skipping && lp->lastc == '<' && (newc == '?' || newc == '!')) { lp->skipping = 1; lp->lastc = newc; curr++; continue; } if (lp->skipping) { if (newc == '>') lp->skipping = 0; lp->lastc = newc; curr++; continue; } if (newc == '<') { lp->lastc = '<'; curr++; continue; } /* do a pending '<' first then newc */ if (lp->lastc == '<') { if (oneXMLchar(lp, '<', ynot) < 0) { initParser(lp); curr++; continue; } /* N.B. we assume '<' will never result in closure */ } /* process newc (at last!) */ s = oneXMLchar(lp, newc, ynot); if (s == 0) { lp->lastc = newc; curr++; continue; } if (s < 0) { initParser(lp); curr++; continue; } /* Ok! store ce in nodes and we start over. * N.B. up to caller to call delXMLEle with what we return. */ nodes[nnodes - 1] = lp->ce; nodes = (XMLEle **)realloc(nodes, (nnodes + 1) * sizeof(XMLEle *)); nodes[nnodes] = NULL; nnodes += 1; lp->ce = NULL; initParser(lp); curr++; } /* * N.B. up to caller to free nodes. */ return nodes; } /* process one more character of an XML file. * when find closure with outter element return root of complete tree. * when find error return NULL with reason in ynot[]. * when need more return NULL with ynot[0] = '\0'. * N.B. it is up to the caller to delete any tree returned with delXMLEle(). */ XMLEle *readXMLEle(LilXML *lp, int newc, char ynot[]) { XMLEle *root; int s; /* start optimistic */ ynot[0] = '\0'; /* EOF? */ if (newc == 0) { sprintf(ynot, "Line %d: early XML EOF", lp->ln); initParser(lp); return (NULL); } /* new line? */ if (newc == '\n') lp->ln++; /* skip comments and declarations. requires 1 char history */ if (!lp->skipping && lp->lastc == '<' && (newc == '?' || newc == '!')) { lp->skipping = 1; lp->lastc = newc; return (NULL); } if (lp->skipping) { if (newc == '>') lp->skipping = 0; lp->lastc = newc; return (NULL); } if (newc == '<') { lp->lastc = '<'; return (NULL); } /* do a pending '<' first then newc */ if (lp->lastc == '<') { if (oneXMLchar(lp, '<', ynot) < 0) { initParser(lp); return (NULL); } /* N.B. we assume '<' will never result in closure */ } /* process newc (at last!) */ s = oneXMLchar(lp, newc, ynot); if (s == 0) { lp->lastc = newc; return (NULL); } if (s < 0) { initParser(lp); return (NULL); } /* Ok! return ce and we start over. * N.B. up to caller to call delXMLEle with what we return. */ root = lp->ce; lp->ce = NULL; initParser(lp); return (root); } /* parse the given XML string. * return XMLEle* else NULL with reason why in ynot[] */ XMLEle *parseXML(char buf[], char ynot[]) { LilXML *lp = newLilXML(); XMLEle *root; do { root = readXMLEle(lp, *buf++, ynot); } while (!root && !ynot[0]); delLilXML(lp); return (root); } /* return a deep copy of the given XMLEle * */ XMLEle *cloneXMLEle(XMLEle *ep) { char *buf; char ynot[1024]; XMLEle *newep; buf = (*mymalloc)(sprlXMLEle(ep, 0) + 1); sprXMLEle(buf, ep, 0); newep = parseXML(buf, ynot); (*myfree)(buf); return (newep); } /* search ep for an attribute with given name. * return NULL if not found. */ XMLAtt *findXMLAtt(XMLEle *ep, const char *name) { int i; for (i = 0; i < ep->nat; i++) if (!strcmp(ep->at[i]->name.s, name)) return (ep->at[i]); return (NULL); } /* search ep for an element with given tag. * return NULL if not found. */ XMLEle *findXMLEle(XMLEle *ep, const char *tag) { int tl = strlen(tag); int i; for (i = 0; i < ep->nel; i++) { String *sp = &ep->el[i]->tag; if (sp->sl == tl && !strcmp(sp->s, tag)) return (ep->el[i]); } return (NULL); } /* iterate over each child element of ep. * call first time with first set to 1, then 0 from then on. * returns NULL when no more or err */ XMLEle *nextXMLEle(XMLEle *ep, int init) { int eit; if (init) ep->eit = 0; eit = ep->eit++; if (eit < 0 || eit >= ep->nel) return (NULL); return (ep->el[eit]); } /* iterate over each attribute of ep. * call first time with first set to 1, then 0 from then on. * returns NULL when no more or err */ XMLAtt *nextXMLAtt(XMLEle *ep, int init) { int ait; if (init) ep->ait = 0; ait = ep->ait++; if (ait < 0 || ait >= ep->nat) return (NULL); return (ep->at[ait]); } /* return parent of given XMLEle */ XMLEle *parentXMLEle(XMLEle *ep) { return (ep->pe); } /* return parent element of given XMLAtt */ XMLEle *parentXMLAtt(XMLAtt *ap) { return (ap->ce); } /* access functions */ /* return the tag name of the given element */ char *tagXMLEle(XMLEle *ep) { return (ep->tag.s); } /* return the pcdata portion of the given element */ char *pcdataXMLEle(XMLEle *ep) { return (ep->pcdata.s); } /* return the number of characters in the pcdata portion of the given element */ int pcdatalenXMLEle(XMLEle *ep) { return (ep->pcdata.sl); } /* return the name of the given attribute */ char *nameXMLAtt(XMLAtt *ap) { return (ap->name.s); } /* return the value of the given attribute */ char *valuXMLAtt(XMLAtt *ap) { return (ap->valu.s); } /* return the number of child elements of the given element */ int nXMLEle(XMLEle *ep) { return (ep->nel); } /* return the number of attributes in the given element */ int nXMLAtt(XMLEle *ep) { return (ep->nat); } /* search ep for an attribute with the given name and return its value. * return "" if not found. */ const char *findXMLAttValu(XMLEle *ep, const char *name) { XMLAtt *a = findXMLAtt(ep, name); return (a ? a->valu.s : ""); } /* handy wrapper to read one xml file. * return root element else NULL with report in ynot[] */ XMLEle *readXMLFile(FILE *fp, LilXML *lp, char ynot[]) { int c; while ((c = fgetc(fp)) != EOF) { XMLEle *root = readXMLEle(lp, c, ynot); if (root || ynot[0]) return (root); } return (NULL); } /* add an element with the given tag to the given element. * parent can be NULL to make a new root. */ XMLEle *addXMLEle(XMLEle *parent, const char *tag) { XMLEle *ep = growEle(parent); appendString(&ep->tag, tag); return (ep); } /* append an existing element to the given element. * N.B. be mindful of when these are deleted, this is not a deep copy. */ void appXMLEle(XMLEle *ep, XMLEle *newep) { ep->el = (XMLEle **)moremem(ep->el, (ep->nel + 1) * sizeof(XMLEle *)); ep->el[ep->nel++] = newep; } /* set the pcdata of the given element */ void editXMLEle(XMLEle *ep, const char *pcdata) { freeString(&ep->pcdata); appendString(&ep->pcdata, pcdata); ep->pcdata_hasent = (strpbrk(pcdata, entities) != NULL); } /* add an attribute to the given XML element */ XMLAtt *addXMLAtt(XMLEle *ep, const char *name, const char *valu) { XMLAtt *ap = growAtt(ep); appendString(&ap->name, name); appendString(&ap->valu, valu); return (ap); } /* remove the named attribute from ep, if any */ void rmXMLAtt(XMLEle *ep, const char *name) { int i; for (i = 0; i < ep->nat; i++) { if (strcmp(ep->at[i]->name.s, name) == 0) { freeAtt(ep->at[i]); memmove(&ep->at[i], &ep->at[i + 1], (--ep->nat - i) * sizeof(XMLAtt *)); return; } } } /* change the value of an attribute to str */ void editXMLAtt(XMLAtt *ap, const char *str) { freeString(&ap->valu); appendString(&ap->valu, str); } /* sample print ep to fp * N.B. set level = 0 on first call */ #define PRINDENT 4 /* sample print indent each level */ void prXMLEle(FILE *fp, XMLEle *ep, int level) { int indent = level * PRINDENT; int i; fprintf(fp, "%*s<%s", indent, "", ep->tag.s); for (i = 0; i < ep->nat; i++) fprintf(fp, " %s=\"%s\"", ep->at[i]->name.s, entityXML(ep->at[i]->valu.s)); if (ep->nel > 0) { fprintf(fp, ">\n"); for (i = 0; i < ep->nel; i++) prXMLEle(fp, ep->el[i], level + 1); } if (ep->pcdata.sl > 0) { if (ep->nel == 0) fprintf(fp, ">\n"); if (ep->pcdata_hasent) fprintf(fp, "%s", entityXML(ep->pcdata.s)); else fprintf(fp, "%s", ep->pcdata.s); if (ep->pcdata.s[ep->pcdata.sl - 1] != '\n') fprintf(fp, "\n"); } if (ep->nel > 0 || ep->pcdata.sl > 0) fprintf(fp, "%*s\n", indent, "", ep->tag.s); else fprintf(fp, "/>\n"); } /* sample print ep to string s. * N.B. s must be at least as large as that reported by sprlXMLEle()+1. * N.B. set level = 0 on first call * return length of resulting string (sans trailing \0) */ int sprXMLEle(char *s, XMLEle *ep, int level) { int indent = level * PRINDENT; int sl = 0; int i; sl += sprintf(s + sl, "%*s<%s", indent, "", ep->tag.s); for (i = 0; i < ep->nat; i++) sl += sprintf(s + sl, " %s=\"%s\"", ep->at[i]->name.s, entityXML(ep->at[i]->valu.s)); if (ep->nel > 0) { sl += sprintf(s + sl, ">\n"); for (i = 0; i < ep->nel; i++) sl += sprXMLEle(s + sl, ep->el[i], level + 1); } if (ep->pcdata.sl > 0) { if (ep->nel == 0) sl += sprintf(s + sl, ">\n"); if (ep->pcdata_hasent) sl += sprintf(s + sl, "%s", entityXML(ep->pcdata.s)); else { strcpy(s + sl, ep->pcdata.s); sl += ep->pcdata.sl; } if (ep->pcdata.s[ep->pcdata.sl - 1] != '\n') sl += sprintf(s + sl, "\n"); } if (ep->nel > 0 || ep->pcdata.sl > 0) sl += sprintf(s + sl, "%*s\n", indent, "", ep->tag.s); else sl += sprintf(s + sl, "/>\n"); return (sl); } /* return number of bytes in a string guaranteed able to hold result of * sprXLMEle(ep) (sans trailing \0). * N.B. set level = 0 on first call */ int sprlXMLEle(XMLEle *ep, int level) { int indent = level * PRINDENT; int l = 0; int i; l += indent + 1 + ep->tag.sl; for (i = 0; i < ep->nat; i++) l += ep->at[i]->name.sl + 4 + strlen(entityXML(ep->at[i]->valu.s)); if (ep->nel > 0) { l += 2; for (i = 0; i < ep->nel; i++) l += sprlXMLEle(ep->el[i], level + 1); } if (ep->pcdata.sl > 0) { if (ep->nel == 0) l += 2; if (ep->pcdata_hasent) l += strlen(entityXML(ep->pcdata.s)); else l += ep->pcdata.sl; if (ep->pcdata.s[ep->pcdata.sl - 1] != '\n') l += 1; } if (ep->nel > 0 || ep->pcdata.sl > 0) l += indent + 4 + ep->tag.sl; else l += 3; return (l); } /* return a string with all xml-sensitive characters within the passed string s * replaced with their entity sequence equivalents. * N.B. caller must use the returned string before calling us again. */ char *entityXML(char *s) { static char *malbuf; int nmalbuf = 0; char *sret = NULL; char *ep = NULL; /* scan for each entity, if any */ for (sret = s; (ep = strpbrk(s, entities)) != NULL; s = ep + 1) { /* found another entity, copy preceding to malloced buffer */ int nnew = ep - s; /* all but entity itself */ sret = malbuf = moremem(malbuf, nmalbuf + nnew + 10); memcpy(malbuf + nmalbuf, s, nnew); nmalbuf += nnew; /* replace with entity encoding */ switch (*ep) { case '&': nmalbuf += sprintf(malbuf + nmalbuf, "&"); break; case '<': nmalbuf += sprintf(malbuf + nmalbuf, "<"); break; case '>': nmalbuf += sprintf(malbuf + nmalbuf, ">"); break; case '\'': nmalbuf += sprintf(malbuf + nmalbuf, "'"); break; case '"': nmalbuf += sprintf(malbuf + nmalbuf, """); break; } } /* return s if no entities, else malloc cleaned-up copy */ if (sret == s) { /* using s, so free any alloced memory from last time */ if (malbuf) { free(malbuf); malbuf = NULL; } return s; } else { /* put remaining part of s into malbuf */ int nleft = strlen(s) + 1; /* include \0 */ sret = malbuf = moremem(malbuf, nmalbuf + nleft); memcpy(malbuf + nmalbuf, s, nleft); } return (sret); } /* if ent is a recognized xml entity sequence, set *cp to char and return 1 * else return 0 */ static int decodeEntity(char *ent, int *cp) { static struct { char *ent; char c; } enttable[] = { { "&", '&' }, { "'", '\'' }, { "<", '<' }, { ">", '>' }, { """, '"' }, }; for (int i = 0; i < (int)(sizeof(enttable) / sizeof(enttable[0])); i++) { if (strcmp(ent, enttable[i].ent) == 0) { *cp = enttable[i].c; return (1); } } return (0); } /* process one more char in XML file. * if find final closure, return 1 and tree is in ce. * if need more, return 0. * if real trouble, return -1 and put reason in ynot. */ static int oneXMLchar(LilXML *lp, int c, char ynot[]) { switch (lp->cs) { case LOOK4START: /* looking for first element start */ if (c == '<') { pushXMLEle(lp); lp->cs = LOOK4TAG; } /* silently ignore until resync */ break; case LOOK4TAG: /* looking for element tag */ if (isTokenChar(1, c)) { growString(&lp->ce->tag, c); lp->cs = INTAG; } else if (!isspace(c)) { sprintf(ynot, "Line %d: Bogus tag char %c", lp->ln, c); return (-1); } break; case INTAG: /* reading tag */ if (isTokenChar(0, c)) growString(&lp->ce->tag, c); else if (c == '>') lp->cs = LOOK4CON; else if (c == '/') lp->cs = SAWSLASH; else lp->cs = LOOK4ATTRN; break; case LOOK4ATTRN: /* looking for attr name, > or / */ if (c == '>') lp->cs = LOOK4CON; else if (c == '/') lp->cs = SAWSLASH; else if (isTokenChar(1, c)) { XMLAtt *ap = growAtt(lp->ce); growString(&ap->name, c); lp->cs = INATTRN; } else if (!isspace(c)) { sprintf(ynot, "Line %d: Bogus leading attr name char: %c", lp->ln, c); return (-1); } break; case SAWSLASH: /* saw / in element opening */ if (c == '>') { if (!lp->ce->pe) return (1); /* root has no content */ popXMLEle(lp); lp->cs = LOOK4CON; } else { sprintf(ynot, "Line %d: Bogus char %c before >", lp->ln, c); return (-1); } break; case INATTRN: /* reading attr name */ if (isTokenChar(0, c)) growString(&lp->ce->at[lp->ce->nat - 1]->name, c); else if (isspace(c) || c == '=') lp->cs = LOOK4ATTRV; else { sprintf(ynot, "Line %d: Bogus attr name char: %c", lp->ln, c); return (-1); } break; case LOOK4ATTRV: /* looking for attr value */ if (c == '\'' || c == '"') { lp->delim = c; lp->cs = INATTRV; } else if (!(isspace(c) || c == '=')) { sprintf(ynot, "Line %d: No value for attribute %s", lp->ln, lp->ce->at[lp->ce->nat - 1]->name.s); return (-1); } break; case INATTRV: /* in attr value */ if (c == '&') { newString(&lp->entity); growString(&lp->entity, c); lp->cs = ENTINATTRV; } else if (c == lp->delim) lp->cs = LOOK4ATTRN; else if (!iscntrl(c)) growString(&lp->ce->at[lp->ce->nat - 1]->valu, c); break; case ENTINATTRV: /* working on entity in attr valu */ if (c == ';') { /* if find a recongized esp seq, add equiv char else raw seq */ growString(&lp->entity, c); if (decodeEntity(lp->entity.s, &c)) growString(&lp->ce->at[lp->ce->nat - 1]->valu, c); else appendString(&lp->ce->at[lp->ce->nat - 1]->valu, lp->entity.s); freeString(&lp->entity); lp->cs = INATTRV; } else growString(&lp->entity, c); break; case LOOK4CON: /* skipping leading content whitespace*/ if (c == '<') lp->cs = SAWLTINCON; else if (!isspace(c)) { growString(&lp->ce->pcdata, c); lp->cs = INCON; } break; case INCON: /* reading content */ if (c == '&') { newString(&lp->entity); growString(&lp->entity, c); lp->cs = ENTINCON; } else if (c == '<') { /* chomp trailing whitespace */ while (lp->ce->pcdata.sl > 0 && isspace(lp->ce->pcdata.s[lp->ce->pcdata.sl - 1])) lp->ce->pcdata.s[--(lp->ce->pcdata.sl)] = '\0'; lp->cs = SAWLTINCON; } else { growString(&lp->ce->pcdata, c); } break; case ENTINCON: /* working on entity in content */ if (c == ';') { /* if find a recognized esc seq, add equiv char else raw seq */ growString(&lp->entity, c); if (decodeEntity(lp->entity.s, &c)) growString(&lp->ce->pcdata, c); else { appendString(&lp->ce->pcdata, lp->entity.s); lp->ce->pcdata_hasent = 1; } freeString(&lp->entity); lp->cs = INCON; } else growString(&lp->entity, c); break; case SAWLTINCON: /* saw < in content */ if (c == '/') { resetEndTag(lp); lp->cs = LOOK4CLOSETAG; } else { pushXMLEle(lp); if (isTokenChar(1, c)) { growString(&lp->ce->tag, c); lp->cs = INTAG; } else lp->cs = LOOK4TAG; } break; case LOOK4CLOSETAG: /* looking for closing tag after < */ if (isTokenChar(1, c)) { growString(&lp->endtag, c); lp->cs = INCLOSETAG; } else if (!isspace(c)) { sprintf(ynot, "Line %d: Bogus preend tag char %c", lp->ln, c); return (-1); } break; case INCLOSETAG: /* reading closing tag */ if (isTokenChar(0, c)) growString(&lp->endtag, c); else if (c == '>') { if (strcmp(lp->ce->tag.s, lp->endtag.s)) { sprintf(ynot, "Line %d: closing tag %s does not match %s", lp->ln, lp->endtag.s, lp->ce->tag.s); return (-1); } else if (lp->ce->pe) { popXMLEle(lp); lp->cs = LOOK4CON; /* back to content after nested elem */ } else return (1); /* yes! */ } else if (!isspace(c)) { sprintf(ynot, "Line %d: Bogus end tag char %c", lp->ln, c); return (-1); } break; } return (0); } /* set up for a fresh start again */ static void initParser(LilXML *lp) { delXMLEle(lp->ce); freeString(&lp->endtag); memset(lp, 0, sizeof(*lp)); newString(&lp->endtag); lp->cs = LOOK4START; lp->ln = 1; } /* start a new XMLEle. * point ce to a new XMLEle. * if ce already set up, add to its list of child elements too. * endtag no longer valid. */ static void pushXMLEle(LilXML *lp) { lp->ce = growEle(lp->ce); resetEndTag(lp); } /* point ce to parent of current ce. * endtag no longer valid. */ static void popXMLEle(LilXML *lp) { lp->ce = lp->ce->pe; resetEndTag(lp); } /* return one new XMLEle, added to the given element if given */ static XMLEle *growEle(XMLEle *pe) { XMLEle *newe = (XMLEle *)moremem(NULL, sizeof(XMLEle)); memset(newe, 0, sizeof(XMLEle)); newString(&newe->tag); newString(&newe->pcdata); newe->pe = pe; if (pe) { pe->el = (XMLEle **)moremem(pe->el, (pe->nel + 1) * sizeof(XMLEle *)); pe->el[pe->nel++] = newe; } return (newe); } /* add room for and return one new XMLAtt to the given element */ static XMLAtt *growAtt(XMLEle *ep) { XMLAtt *newa = (XMLAtt *)moremem(NULL, sizeof(XMLAtt)); memset(newa, 0, sizeof(*newa)); newString(&newa->name); newString(&newa->valu); newa->ce = ep; ep->at = (XMLAtt **)moremem(ep->at, (ep->nat + 1) * sizeof(XMLAtt *)); ep->at[ep->nat++] = newa; return (newa); } /* free a and all it holds */ static void freeAtt(XMLAtt *a) { if (!a) return; freeString(&a->name); freeString(&a->valu); (*myfree)(a); } /* reset endtag */ static void resetEndTag(LilXML *lp) { freeString(&lp->endtag); newString(&lp->endtag); } /* 1 if c is a valid token character, else 0. * it can be alpha or '_' or numeric unless start. */ static int isTokenChar(int start, int c) { return (isalpha(c) || c == '_' || (!start && isdigit(c))); } /* grow the String storage at *sp to append c */ static void growString(String *sp, int c) { int l = sp->sl + 2; /* need room for '\0' plus c */ if (l > sp->sm) { if (!sp->s) newString(sp); else sp->s = (char *)moremem(sp->s, sp->sm *= 2); } sp->s[--l] = '\0'; sp->s[--l] = (char)c; sp->sl++; } /* append str to the String storage at *sp */ static void appendString(String *sp, const char *str) { if (!sp || !str) return; int strl = strlen(str); int l = sp->sl + strl + 1; /* need room for '\0' */ if (l > sp->sm) { if (!sp->s) newString(sp); if (l > sp->sm) sp->s = (char *)moremem(sp->s, (sp->sm = l)); } if (sp->s) { strcpy(&sp->s[sp->sl], str); sp->sl += strl; } } /* init a String with a malloced string containing just \0 */ static void newString(String *sp) { if (!sp) return; sp->s = (char *)moremem(NULL, MINMEM); sp->sm = MINMEM; *sp->s = '\0'; sp->sl = 0; } /* free memory used by the given String */ static void freeString(String *sp) { if (sp->s) (*myfree)(sp->s); sp->s = NULL; sp->sl = 0; sp->sm = 0; } /* like malloc but knows to use realloc if already started */ static void *moremem(void *old, int n) { return (old ? (*myrealloc)(old, n) : (*mymalloc)(n)); } #if defined(MAIN_TST) int main(int ac, char *av[]) { LilXML *lp = newLilXML(); char ynot[1024]; XMLEle *root; root = readXMLFile(stdin, lp, ynot); if (root) { char *str; int l; if (ac > 1) { XMLEle *theend = addXMLEle(root, "theend"); editXMLEle(theend, "Added to test editing"); addXMLAtt(theend, "hello", "world"); } fprintf(stderr, "::::::::::::: %s\n", tagXMLEle(root)); prXMLEle(stdout, root, 0); l = sprlXMLEle(root, 0); str = malloc(l + 1); fprintf(stderr, "::::::::::::: %s : %d : %d", tagXMLEle(root), l, sprXMLEle(str, root, 0)); fprintf(stderr, ": %d\n", printf("%s", str)); delXMLEle(root); } else if (ynot[0]) { fprintf(stderr, "Error: %s\n", ynot); } delLilXML(lp); return (0); } #endif #if defined(_MSC_VER) #undef snprintf #pragma warning(pop) #endif libindi/libs/webcam/0000775000175000017500000000000013263645557013661 5ustar jasemjasemlibindi/libs/webcam/v4l2_colorspace.c0000664000175000017500000001252513263645557017033 0ustar jasemjasem #include "v4l2_colorspace.h" #include unsigned char lutrangey8[256]; unsigned short lutrangey10[1024]; unsigned short lutrangey12[4096]; unsigned short lutrangey16[65536]; unsigned char lutrangecbcr8[256]; unsigned short lutrangecbcr10[1024]; unsigned short lutrangecbcr12[4096]; unsigned short lutrangecbcr16[65536]; void initColorSpace() { unsigned int i; for (i = 0; i < 256; i++) { lutrangey8[i] = (unsigned char)((255.0 / 219.0) * (i - 16)); if (i > 235) lutrangey8[i] = 255; lutrangecbcr8[i] = (unsigned char)((255.0 / 224.0) * i); } } void rangeY8(unsigned char *buf, unsigned int len) { unsigned char *s = buf; for (unsigned int i = 0; i < len; i++) { *s = lutrangey8[*s]; s++; } } void linearize(float *buf, unsigned int len, struct v4l2_format *fmt) { unsigned int i; float *src = buf; switch (fmt->fmt.pix.colorspace) { case V4L2_COLORSPACE_SMPTE240M: // Old obsolete HDTV standard. Replaced by REC 709. // This is the transfer function for SMPTE 240M for (i = 0; i < len; i++) { *src = (*src < 0.0913) ? *src / 4.0 : pow((*src + 0.1115) / 1.1115, 1.0 / 0.45); src++; } break; case V4L2_COLORSPACE_SRGB: // This is used for sRGB as specified by the IEC FDIS 61966-2-1 standard for (i = 0; i < len; i++) { *src = (*src < -0.04045) ? -pow((-*src + 0.055) / 1.055, 2.4) : ((*src <= 0.04045) ? *src / 12.92 : pow((*src + 0.055) / 1.055, 2.4)); src++; } break; //case V4L2_COLORSPACE_ADOBERGB: //r = pow(r, 2.19921875); //break; case V4L2_COLORSPACE_REC709: //case V4L2_COLORSPACE_BT2020: default: // All others use the transfer function specified by REC 709 for (i = 0; i < len; i++) { *src = (*src <= -0.081) ? -pow((*src - 0.099) / -1.099, 1.0 / 0.45) : ((*src < 0.081) ? *src / 4.5 : pow((*src + 0.099) / 1.099, 1.0 / 0.45)); src++; } } } const char *getColorSpaceName(struct v4l2_format *fmt) { switch (fmt->fmt.pix.colorspace) { case V4L2_COLORSPACE_SMPTE170M: return "SMPTE170M (SDTV)"; case V4L2_COLORSPACE_SMPTE240M: return "SMPTE240M (early HDTV)"; case V4L2_COLORSPACE_REC709: return "REC709 (HDTV)"; case V4L2_COLORSPACE_BT878: return "BT878"; case V4L2_COLORSPACE_470_SYSTEM_M: return "470 SYSTEM M (old NTSC)"; case V4L2_COLORSPACE_470_SYSTEM_BG: return "470 SYSTEM BG (old PAL/SECAM)"; case V4L2_COLORSPACE_JPEG: return "JPEG"; case V4L2_COLORSPACE_SRGB: return "SRGB"; /* since Kernel 3.19 case V4L2_COLORSPACE_ADOBERGB: return "Adobe RGB"; case V4L2_COLORSPACE_BT2020: return "BT2020 (UHDTV)"; */ default: return "Unknown"; } } unsigned int getYCbCrEncoding(struct v4l2_format *fmt) { switch (fmt->fmt.pix.colorspace) { case V4L2_COLORSPACE_SMPTE170M: case V4L2_COLORSPACE_BT878: case V4L2_COLORSPACE_470_SYSTEM_M: case V4L2_COLORSPACE_470_SYSTEM_BG: case V4L2_COLORSPACE_JPEG: return YCBCR_ENC_601; case V4L2_COLORSPACE_REC709: return YCBCR_ENC_709; case V4L2_COLORSPACE_SRGB: return YCBCR_ENC_SYCC; case V4L2_COLORSPACE_SMPTE240M: return YCBCR_ENC_SMPTE240M; /* since Kernel 3.19 case V4L2_COLORSPACE_ADOBERGB: return return V4L2_YCBCR_ENC_601; case V4L2_COLORSPACE_BT2020: return return V4L2_YCBCR_ENC_BT2020; */ default: return YCBCR_ENC_601; } } const char *getYCbCrEncodingName(struct v4l2_format *fmt) { switch (getYCbCrEncoding(fmt)) { case YCBCR_ENC_601: return "ITU-R 601 -- SDTV"; case YCBCR_ENC_709: return "Rec. 709 -- HDTV"; case YCBCR_ENC_SYCC: return "sYCC (Y'CbCr encoding of sRGB)"; case YCBCR_ENC_SMPTE240M: return "SMPTE 240M -- Obsolete HDTV"; default: return "Unknown"; } } unsigned int getQuantization(struct v4l2_format *fmt) { switch (fmt->fmt.pix.colorspace) { /* since Kernel 3.19 case V4L2_COLORSPACE_ADOBERGB: case V4L2_COLORSPACE_BT2020: */ case V4L2_COLORSPACE_SMPTE170M: case V4L2_COLORSPACE_BT878: case V4L2_COLORSPACE_470_SYSTEM_M: case V4L2_COLORSPACE_470_SYSTEM_BG: case V4L2_COLORSPACE_JPEG: case V4L2_COLORSPACE_REC709: case V4L2_COLORSPACE_SMPTE240M: return QUANTIZATION_LIM_RANGE; case V4L2_COLORSPACE_SRGB: return QUANTIZATION_FULL_RANGE; default: return QUANTIZATION_LIM_RANGE; } } const char *getQuantizationName(struct v4l2_format *fmt) { switch (getQuantization(fmt)) { case QUANTIZATION_FULL_RANGE: return "Full Range"; case QUANTIZATION_LIM_RANGE: return "Limited Range"; default: return "Unknown"; } } libindi/libs/webcam/ccvt_c2.c0000664000175000017500000001607513263645557015361 0ustar jasemjasem/* CCVT_C2: Convert an image from yuv colourspace to rgb Copyright (C) 2001 Tony Hague This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program 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 General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA For questions, remarks, patches, etc. for this program, the author can be reached at nemosoft@smcc.demon.nl. */ #include "ccvt.h" #include "ccvt_types.h" /* by suitable definition of PIXTYPE, can do yuv to rgb or bgr, with or without word alignment */ /* This doesn't exactly earn a prize in a programming beauty contest. */ #define WHOLE_FUNC2RGB(type) \ const unsigned char *y1, *y2, *u, *v; \ PIXTYPE_##type *l1, *l2; \ int r, g, b, cr, cg, cb, yp, j, i; \ \ if ((width & 1) || (height & 1)) \ return; \ \ l1 = (PIXTYPE_##type *)dst; \ l2 = l1 + width; \ y1 = (unsigned char *)src; \ y2 = y1 + width; \ u = (unsigned char *)src + width * height; \ v = u + (width * height) / 4; \ j = height / 2; \ while (j--) \ { \ i = width / 2; \ while (i--) \ { \ /* Since U & V are valid for 4 pixels, repeat code 4 \ times for different Y */ \ cb = ((*u - 128) * 454) >> 8; \ cr = ((*v - 128) * 359) >> 8; \ cg = ((*v - 128) * 183 + (*u - 128) * 88) >> 8; \ \ yp = *(y1++); \ r = yp + cr; \ b = yp + cb; \ g = yp - cg; \ SAT(r); \ SAT(g); \ SAT(b); \ l1->b = b; \ l1->g = g; \ l1->r = r; \ l1++; \ \ yp = *(y1++); \ r = yp + cr; \ b = yp + cb; \ g = yp - cg; \ SAT(r); \ SAT(g); \ SAT(b); \ l1->b = b; \ l1->g = g; \ l1->r = r; \ l1++; \ \ yp = *(y2++); \ r = yp + cr; \ b = yp + cb; \ g = yp - cg; \ SAT(r); \ SAT(g); \ SAT(b); \ l2->b = b; \ l2->g = g; \ l2->r = r; \ l2++; \ \ yp = *(y2++); \ r = yp + cr; \ b = yp + cb; \ g = yp - cg; \ SAT(r); \ SAT(g); \ SAT(b); \ l2->b = b; \ l2->g = g; \ l2->r = r; \ l2++; \ \ u++; \ v++; \ } \ y1 = y2; \ y2 += width; \ l1 = l2; \ l2 += width; \ } void ccvt_420p_bgr32(int width, int height, const void *src, void *dst) { WHOLE_FUNC2RGB(bgr32) } void ccvt_420p_bgr24(int width, int height, const void *src, void *dst) { WHOLE_FUNC2RGB(bgr24) } void ccvt_420p_rgb32(int width, int height, const void *src, void *dst) { WHOLE_FUNC2RGB(rgb32) } void ccvt_420p_rgb24(int width, int height, const void *src, void *dst) { WHOLE_FUNC2RGB(rgb24) } libindi/libs/webcam/v4l2_base.h0000664000175000017500000001513113263645557015614 0ustar jasemjasem/* Copyright (C) 2005 by Jasem Mutlaq Copyright (C) 2013 Geehalel (geehalel@gmail.com) Based on V4L 2 Example http://v4l2spec.bytesex.org/spec-single/v4l2.html#CAPTURE-EXAMPLE 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 */ #pragma once //#include "videodev2.h" #include "eventloop.h" #include "indidevapi.h" // this adds add dependency to indidriver for v4l_legacy, meade_lpi // Can't use logger as legacy drivers don't use defaultdevice //#include "indilogger.h" #include "v4l2_decode/v4l2_decode.h" // for direct recording #include "stream/streammanager.h" #include #include #include #define VIDEO_COMPRESSION_LEVEL 4 class V4L2_Driver; enum { LX_ACTIVE = 0, LX_TRIGGERED, LX_ACCUMULATING }; namespace INDI { class V4L2_Base { public: V4L2_Base(); virtual ~V4L2_Base(); typedef enum { IO_METHOD_READ, IO_METHOD_MMAP, IO_METHOD_USERPTR } io_method; struct buffer { void *start; size_t length; }; /* Connection */ virtual int connectCam(const char *devpath, char *errmsg, int pixelFormat = -1, int width = -1, int height = -1); virtual void disconnectCam(bool stopcapture); char *getDeviceName(); void setDeviceName(const char *name); bool isLXmodCapable(); /* Updates */ void callFrame(void *p); /* Image Format/Size */ int getFormat(); int getWidth(); int getHeight(); int getBpp(); virtual int setSize(int x, int y); virtual void getMaxMinSize(int &x_max, int &y_max, int &x_min, int &y_min); /* Frame rate */ int (V4L2_Base::*setframerate)(struct v4l2_fract frate, char *errmsg); struct v4l2_fract (V4L2_Base::*getframerate)(); unsigned char *getY(); unsigned char *getU(); unsigned char *getV(); // 2017-01-24 JM: Deprecated RGBA (32bit) buffer. Should use RGB24 buffer to save space //unsigned char * getColorBuffer(); unsigned char *getRGBBuffer(); float *getLinearY(); void registerCallback(WPF *fp, void *ud); int start_capturing(char *errmsg); int stop_capturing(char *errmsg); static void newFrame(int fd, void *p); //void setDropFrameCount(unsigned int count) { dropFrameCount = count;} void enumerate_ctrl(); void enumerate_menu(); bool enumerate_ext_ctrl(); int queryINTControls(INumberVectorProperty *nvp); bool queryExtControls(INumberVectorProperty *nvp, unsigned int *nnumber, ISwitchVectorProperty **options, unsigned int *noptions, const char *dev, const char *group); void queryControls(INumberVectorProperty *nvp, unsigned int *nnumber, ISwitchVectorProperty **options, unsigned int *noptions, const char *dev, const char *group); int getControl(unsigned int ctrl_id, double *value, char *errmsg); int setINTControl(unsigned int ctrl_id, double new_value, char *errmsg); int setOPTControl(unsigned int ctrl_id, unsigned int new_value, char *errmsg); int query_ctrl(unsigned int ctrl_id, double &ctrl_min, double &ctrl_max, double &ctrl_step, double &ctrl_value, char *errmsg); void getinputs(ISwitchVectorProperty *inputssp); int setinput(unsigned int inputindex, char *errmsg); void getcaptureformats(ISwitchVectorProperty *captureformatssp); int setcaptureformat(unsigned int captureformatindex, char *errmsg); void getcapturesizes(ISwitchVectorProperty *capturesizessp, INumberVectorProperty *capturesizenp); int setcapturesize(unsigned int w, unsigned int h, char *errmsg); void getframerates(ISwitchVectorProperty *frameratessp, INumberVectorProperty *frameratenp); int setcroprect(int x, int y, int w, int h, char *errmsg); struct v4l2_rect getcroprect(); void setColorProcessing(bool quantization, bool colorconvert, bool linearization); void setlxstate(short s) { IDLog("setlexstate to %d\n", s); lxstate = s; } short getlxstate() { return lxstate; } bool isstreamactive() { return streamactive; } void doDecode(bool); protected: int xioctl(int fd, int request, void *arg, char const *const request_str); int ioctl_set_format(struct v4l2_format new_fmt, char *errmsg); int read_frame(char *errsg); int uninit_device(char *errmsg); int open_device(const char *devpath, char *errmsg); int check_device(char *errmsg); int init_device(char *errmsg); int init_mmap(char *errmsg); int errno_exit(const char *s, char *errmsg); void close_device(); void init_userp(unsigned int buffer_size); void init_read(unsigned int buffer_size); void findMinMax(); int enumeratedInputs; int enumeratedCaptureFormats; /* Frame rate */ int stdsetframerate(struct v4l2_fract frate, char *errmsg); int pwcsetframerate(struct v4l2_fract frate, char *errmsg); struct v4l2_fract stdgetframerate(); struct v4l2_capability cap; struct v4l2_cropcap cropcap; struct v4l2_crop crop; struct v4l2_format fmt; struct v4l2_input input; struct v4l2_buffer buf; bool cancrop; bool cropset; bool cansetrate; bool streamedonce; bool streamactive; short lxstate; struct v4l2_queryctrl queryctrl; struct v4l2_querymenu querymenu; bool has_ext_pix_format; bool is_compressed() const; WPF *callback; void *uptr; char dev_name[64]; const char *path; io_method io; int fd; struct buffer *buffers; unsigned int n_buffers; bool reallocate_buffers; //int dropFrame; //bool dropFrameEnabled; //unsigned int dropFrameCount; struct v4l2_fract frameRate; int xmax, xmin, ymax, ymin; int selectCallBackID; //unsigned char * YBuf,*UBuf,*VBuf, *yuvBuffer, *colorBuffer, *rgb24_buffer, *cropbuf; V4L2_Decode *v4l2_decode; V4L2_Decoder *decoder; bool dodecode; int bpp; friend class ::V4L2_Driver; char deviceName[MAXINDIDEVICE]; }; } libindi/libs/webcam/pwc-ioctl.h0000664000175000017500000002505513263645557015742 0ustar jasemjasem/* (C) 2001-2004 Nemosoft Unv. (C) 2004-2006 Luc Saillard (luc@saillard.org) NOTE: this version of pwc is an unofficial (modified) release of pwc & pcwx driver and thus may have bugs that are not present in the original version. Please send bug reports and support requests to . The decompression routines have been implemented by reverse-engineering the Nemosoft binary pwcx module. Caveat emptor. This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program 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 General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ /* This is pwc-ioctl.h belonging to PWC 10.0.10 It contains structures and defines to communicate from user space directly to the driver. */ /* Changes 2001/08/03 Alvarado Added ioctl constants to access methods for changing white balance and red/blue gains 2002/12/15 G. H. Fernandez-Toribio VIDIOCGREALSIZE 2003/12/13 Nemosft Unv. Some modifications to make interfacing to PWCX easier 2006/01/01 Luc Saillard Add raw format definition */ /* These are private ioctl() commands, specific for the Philips webcams. They contain functions not found in other webcams, and settings not specified in the Video4Linux API. The #define names are built up like follows: VIDIOC VIDeo IOCtl prefix PWC Philps WebCam G optional: Get S optional: Set ... the function */ #pragma once #include /* Enumeration of image sizes */ #define PSZ_SQCIF 0x00 #define PSZ_QSIF 0x01 #define PSZ_QCIF 0x02 #define PSZ_SIF 0x03 #define PSZ_CIF 0x04 #define PSZ_VGA 0x05 #define PSZ_MAX 6 /* The frame rate is encoded in the video_window.flags parameter using the upper 16 bits, since some flags are defined nowadays. The following defines provide a mask and shift to filter out this value. This value can also be passing using the private flag when using v4l2 and VIDIOC_S_FMT ioctl. In 'Snapshot' mode the camera freezes its automatic exposure and colour balance controls. */ #define PWC_FPS_SHIFT 16 #define PWC_FPS_MASK 0x00FF0000 #define PWC_FPS_FRMASK 0x003F0000 #define PWC_FPS_SNAPSHOT 0x00400000 #define PWC_QLT_MASK 0x03000000 #define PWC_QLT_SHIFT 24 /* structure for transferring x & y coordinates */ struct pwc_coord { int x, y; /* guess what */ int size; /* size, or offset */ }; /* Used with VIDIOCPWCPROBE */ struct pwc_probe { char name[32]; int type; }; struct pwc_serial { char serial[30]; /* String with serial number. Contains terminating 0 */ }; /* pwc_whitebalance.mode values */ #define PWC_WB_INDOOR 0 #define PWC_WB_OUTDOOR 1 #define PWC_WB_FL 2 #define PWC_WB_MANUAL 3 #define PWC_WB_AUTO 4 /* Used with VIDIOCPWC[SG]AWB (Auto White Balance). Set mode to one of the PWC_WB_* values above. *red and *blue are the respective gains of these colour components inside the camera; range 0..65535 When 'mode' == PWC_WB_MANUAL, 'manual_red' and 'manual_blue' are set or read; otherwise undefined. 'read_red' and 'read_blue' are read-only. */ struct pwc_whitebalance { int mode; int manual_red, manual_blue; /* R/W */ int read_red, read_blue; /* R/O */ }; /* 'control_speed' and 'control_delay' are used in automatic whitebalance mode, and tell the camera how fast it should react to changes in lighting, and with how much delay. Valid values are 0..65535. */ struct pwc_wb_speed { int control_speed; int control_delay; }; /* Used with VIDIOCPWC[SG]LED */ struct pwc_leds { int led_on; /* Led on-time; range = 0..25000 */ int led_off; /* Led off-time; range = 0..25000 */ }; /* Image size (used with GREALSIZE) */ struct pwc_imagesize { int width; int height; }; /* Defines and structures for Motorized Pan & Tilt */ #define PWC_MPT_PAN 0x01 #define PWC_MPT_TILT 0x02 #define PWC_MPT_TIMEOUT 0x04 /* for status */ /* Set angles; when absolute != 0, the angle is absolute and the driver calculates the relative offset for you. This can only be used with VIDIOCPWCSANGLE; VIDIOCPWCGANGLE always returns absolute angles. */ struct pwc_mpt_angles { int absolute; /* write-only */ int pan; /* degrees * 100 */ int tilt; /* degress * 100 */ }; /* Range of angles of the camera, both horizontally and vertically. */ struct pwc_mpt_range { int pan_min, pan_max; /* degrees * 100 */ int tilt_min, tilt_max; }; struct pwc_mpt_status { int status; int time_pan; int time_tilt; }; /* This is used for out-of-kernel decompression. With it, you can get all the necessary information to initialize and use the decompressor routines in standalone applications. */ struct pwc_video_command { int type; /* camera type (645, 675, 730, etc.) */ int release; /* release number */ int size; /* one of PSZ_* */ int alternate; int command_len; /* length of USB video command */ unsigned char command_buf[13]; /* Actual USB video command */ int bandlength; /* >0 = compressed */ int frame_size; /* Size of one (un)compressed frame */ }; /* Flags for PWCX subroutines. Not all modules honour all flags. */ #define PWCX_FLAG_PLANAR 0x0001 #define PWCX_FLAG_BAYER 0x0008 /* IOCTL definitions */ /* Restore user settings */ #define VIDIOCPWCRUSER _IO('v', 192) /* Save user settings */ #define VIDIOCPWCSUSER _IO('v', 193) /* Restore factory settings */ #define VIDIOCPWCFACTORY _IO('v', 194) /* You can manipulate the compression factor. A compression preference of 0 means use uncompressed modes when available; 1 is low compression, 2 is medium and 3 is high compression preferred. Of course, the higher the compression, the lower the bandwidth used but more chance of artefacts in the image. The driver automatically chooses a higher compression when the preferred mode is not available. */ /* Set preferred compression quality (0 = uncompressed, 3 = highest compression) */ #define VIDIOCPWCSCQUAL _IOW('v', 195, int) /* Get preferred compression quality */ #define VIDIOCPWCGCQUAL _IOR('v', 195, int) /* Retrieve serial number of camera */ #define VIDIOCPWCGSERIAL _IOR('v', 198, struct pwc_serial) /* This is a probe function; since so many devices are supported, it becomes difficult to include all the names in programs that want to check for the enhanced Philips stuff. So in stead, try this PROBE; it returns a structure with the original name, and the corresponding Philips type. To use, fill the structure with zeroes, call PROBE and if that succeeds, compare the name with that returned from VIDIOCGCAP; they should be the same. If so, you can be assured it is a Philips (OEM) cam and the type is valid. */ #define VIDIOCPWCPROBE _IOR('v', 199, struct pwc_probe) /* Set AGC (Automatic Gain Control); int < 0 = auto, 0..65535 = fixed */ #define VIDIOCPWCSAGC _IOW('v', 200, int) /* Get AGC; int < 0 = auto; >= 0 = fixed, range 0..65535 */ #define VIDIOCPWCGAGC _IOR('v', 200, int) /* Set shutter speed; int < 0 = auto; >= 0 = fixed, range 0..65535 */ #define VIDIOCPWCSSHUTTER _IOW('v', 201, int) /* Color compensation (Auto White Balance) */ #define VIDIOCPWCSAWB _IOW('v', 202, struct pwc_whitebalance) #define VIDIOCPWCGAWB _IOR('v', 202, struct pwc_whitebalance) /* Auto WB speed */ #define VIDIOCPWCSAWBSPEED _IOW('v', 203, struct pwc_wb_speed) #define VIDIOCPWCGAWBSPEED _IOR('v', 203, struct pwc_wb_speed) /* LEDs on/off/blink; int range 0..65535 */ #define VIDIOCPWCSLED _IOW('v', 205, struct pwc_leds) #define VIDIOCPWCGLED _IOR('v', 205, struct pwc_leds) /* Contour (sharpness); int < 0 = auto, 0..65536 = fixed */ #define VIDIOCPWCSCONTOUR _IOW('v', 206, int) #define VIDIOCPWCGCONTOUR _IOR('v', 206, int) /* Backlight compensation; 0 = off, otherwise on */ #define VIDIOCPWCSBACKLIGHT _IOW('v', 207, int) #define VIDIOCPWCGBACKLIGHT _IOR('v', 207, int) /* Flickerless mode; = 0 off, otherwise on */ #define VIDIOCPWCSFLICKER _IOW('v', 208, int) #define VIDIOCPWCGFLICKER _IOR('v', 208, int) /* Dynamic noise reduction; 0 off, 3 = high noise reduction */ #define VIDIOCPWCSDYNNOISE _IOW('v', 209, int) #define VIDIOCPWCGDYNNOISE _IOR('v', 209, int) /* Real image size as used by the camera; tells you whether or not there's a gray border around the image */ #define VIDIOCPWCGREALSIZE _IOR('v', 210, struct pwc_imagesize) /* Motorized pan & tilt functions */ #define VIDIOCPWCMPTRESET _IOW('v', 211, int) #define VIDIOCPWCMPTGRANGE _IOR('v', 211, struct pwc_mpt_range) #define VIDIOCPWCMPTSANGLE _IOW('v', 212, struct pwc_mpt_angles) #define VIDIOCPWCMPTGANGLE _IOR('v', 212, struct pwc_mpt_angles) #define VIDIOCPWCMPTSTATUS _IOR('v', 213, struct pwc_mpt_status) /* Get the USB set-video command; needed for initializing libpwcx */ #define VIDIOCPWCGVIDCMD _IOR('v', 215, struct pwc_video_command) struct pwc_table_init_buffer { int len; char *buffer; }; #define VIDIOCPWCGVIDTABLE _IOR('v', 216, struct pwc_table_init_buffer) /* * This is private command used when communicating with v4l2. * In the future all private ioctl will be remove/replace to * use interface offer by v4l2. */ #define V4L2_CID_PRIVATE_SAVE_USER (V4L2_CID_PRIVATE_BASE + 0) #define V4L2_CID_PRIVATE_RESTORE_USER (V4L2_CID_PRIVATE_BASE + 1) #define V4L2_CID_PRIVATE_RESTORE_FACTORY (V4L2_CID_PRIVATE_BASE + 2) #define V4L2_CID_PRIVATE_COLOUR_MODE (V4L2_CID_PRIVATE_BASE + 3) #define V4L2_CID_PRIVATE_AUTOCONTOUR (V4L2_CID_PRIVATE_BASE + 4) #define V4L2_CID_PRIVATE_CONTOUR (V4L2_CID_PRIVATE_BASE + 5) #define V4L2_CID_PRIVATE_BACKLIGHT (V4L2_CID_PRIVATE_BASE + 6) #define V4L2_CID_PRIVATE_FLICKERLESS (V4L2_CID_PRIVATE_BASE + 7) #define V4L2_CID_PRIVATE_NOISE_REDUCTION (V4L2_CID_PRIVATE_BASE + 8) struct pwc_raw_frame { __le16 type; /* type of the webcam */ __le16 vbandlength; /* Size of 4lines compressed (used by the decompressor) */ __u8 cmd[4]; /* the four byte of the command (in case of nala, only the first 3 bytes is filled) */ __u8 rawframe[0]; /* frame_size = H/4*vbandlength */ } __attribute__((packed)); libindi/libs/webcam/ccvt.h0000664000175000017500000002104413263645557014772 0ustar jasemjasem/* CCVT: ColourConVerT: simple library for converting colourspaces Copyright (C) 2002 Nemosoft Unv. Email:athomas@nemsoft.co.uk This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program 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 General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA For questions, remarks, patches, etc. for this program, the author can be reached at nemosoft@smcc.demon.nl. */ /* $Log$ Revision 1.4 2005/04/29 16:51:20 mutlaqja Adding initial support for Video 4 Linux 2 drivers. This mean that KStars can probably control Meade Lunar Planetary Imager (LPI). V4L2 requires a fairly recent kernel (> 2.6.9) and many drivers don't fully support it yet. It will take sometime. KStars still supports V4L1 and will continue so until V4L1 is obselete. Please test KStars video drivers if you can. Any comments welcomed. CCMAIL: kstars-devel@kde.org Revision 1.3 2004/06/26 23:12:03 mutlaqja Hopefully this will fix compile issues on 64bit archs, and FreeBSD, among others. The assembly code is replaced with a more portable, albeit slower C implementation. I imported the videodev.h header after cleaning it for user space. Anyone who has problems compiling this, please report the problem to kstars-devel@kde.org I noticed one odd thing after updating my kdelibs, the LEDs don't change color when state is changed. Try that by starting any INDI device, and hit connect, if the LED turns to yellow and back to grey then it works fine, otherwise, we've got a problem. CCMAIL: kstars-devel@kde.org Revision 1.10 2003/10/24 16:55:18 nemosoft removed erronous log messages Revision 1.9 2002/11/03 22:46:25 nemosoft Adding various RGB to RGB functions. Adding proper copyright header too. Revision 1.8 2002/04/14 01:00:27 nemosoft Finishing touches: adding const, adding libs for 'show' */ #pragma once #ifdef __cplusplus extern "C" { #endif /** * \defgroup colorSpace Color space conversion functions Colour ConVerT: going from one colour space to another. Format descriptions:\n 420i = "4:2:0 interlaced"\n YYYY UU YYYY UU even lines\n YYYY VV YYYY VV odd lines\n U/V data is subsampled by 2 both in horizontal and vertical directions, and intermixed with the Y values.\n\n 420p = "4:2:0 planar"\n YYYYYYYY N lines\n UUUU N/2 lines\n VVVV N/2 lines\n U/V is again subsampled, but all the Ys, Us and Vs are placed together in separate buffers. The buffers may be placed in one piece of contiguous memory though, with Y buffer first, followed by U, followed by V. yuyv = "4:2:2 interlaced"\n YUYV YUYV YUYV ... N lines\n The U/V data is subsampled by 2 in horizontal direction only.\n\n bgr24 = 3 bytes per pixel, in the order Blue Green Red (whoever came up with that idea...)\n rgb24 = 3 bytes per pixel, in the order Red Green Blue (which is sensible)\n rgb32 = 4 bytes per pixel, in the order Red Green Blue Alpha, with Alpha really being a filler byte (0)\n bgr32 = last but not least, 4 bytes per pixel, in the order Blue Green Red Alpha, Alpha again a filler byte (0)\n */ /*@{*/ /** 4:2:0 YUV planar to RGB/BGR */ void ccvt_420p_bgr24(int width, int height, const void *src, void *dst); /** 4:2:0 YUV planar to RGB/BGR */ void ccvt_420p_rgb24(int width, int height, const void *src, void *dst); /** 4:2:0 YUV planar to RGB/BGR */ void ccvt_420p_bgr32(int width, int height, const void *src, void *dst); /** 4:2:0 YUV planar to RGB/BGR */ void ccvt_420p_rgb32(int width, int height, const void *src, void *dst); /** 4:2:2 YUYV interlaced to RGB/BGR */ void ccvt_yuyv_rgb32(int width, int height, const void *src, void *dst); /** 4:2:2 YUYV interlaced to RGB/BGR */ void ccvt_yuyv_bgr32(int width, int height, const void *src, void *dst); /** 4:2:2 YUYV interlaced to BGR24 */ void ccvt_yuyv_bgr24(int width, int height, const void *src, void *dst); /** 4:2:2 YUYV interlaced to RGB24 */ void ccvt_yuyv_rgb24(int width, int height, const void *src, void *dst); /** 4:2:2 YUYV interlaced to 4:2:0 YUV planar */ void ccvt_yuyv_420p(int width, int height, const void *src, void *dsty, void *dstu, void *dstv); /* RGB/BGR to 4:2:0 YUV interlaced */ /** RGB/BGR to 4:2:0 YUV planar */ void ccvt_rgb24_420p(int width, int height, const void *src, void *dsty, void *dstu, void *dstv); /** RGB/BGR to 4:2:0 YUV planar */ void ccvt_bgr24_420p(int width, int height, const void *src, void *dsty, void *dstu, void *dstv); /** RGB/BGR to RGB/BGR */ void ccvt_bgr24_bgr32(int width, int height, const void *const src, void *const dst); /** RGB/BGR to RGB/BGR */ void ccvt_bgr24_rgb32(int width, int height, const void *const src, void *const dst); /** RGB/BGR to RGB/BGR */ void ccvt_bgr32_bgr24(int width, int height, const void *const src, void *const dst); /** RGB/BGR to RGB/BGR */ void ccvt_bgr32_rgb24(int width, int height, const void *const src, void *const dst); /** RGB/BGR to RGB/BGR */ void ccvt_rgb24_bgr32(int width, int height, const void *const src, void *const dst); /** RGB/BGR to RGB/BGR */ void ccvt_rgb24_rgb32(int width, int height, const void *const src, void *const dst); /** RGB/BGR to RGB/BGR */ void ccvt_rgb32_bgr24(int width, int height, const void *const src, void *const dst); /** RGB/BGR to RGB/BGR */ void ccvt_rgb32_rgb24(int width, int height, const void *const src, void *const dst); /** RGB to YUV */ int RGB2YUV(int x_dim, int y_dim, void *bmp, void *y_out, void *u_out, void *v_out, int flip); /** * @short mjpegtoyuv420p MPEG to YUV 420 P * * Return values * -1 on fatal error * 0 on success * 2 if jpeg lib threw a "corrupt jpeg data" warning. * in this case, "a damaged output image is likely." * * Copyright 2000 by Jeroen Vreeken (pe1rxq@amsat.org) * 2006 by Krzysztof Blaszkowski (kb@sysmikro.com.pl) * 2007 by Angel Carpinteo (ack@telefonica.net) */ int mjpegtoyuv420p(unsigned char *map, unsigned char *cap_map, int width, int height, unsigned int size); /* * BAYER2RGB24 ROUTINE TAKEN FROM: * * Sonix SN9C101 based webcam basic I/F routines * Copyright (C) 2004 Takafumi Mizuno * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. */ /** Bayer 8bit to RGB 24 */ void bayer2rgb24(unsigned char *dst, unsigned char *src, long int WIDTH, long int HEIGHT); /** Bayer 16 bit to RGB 24 */ void bayer16_2_rgb24(unsigned short *dst, unsigned short *src, long int WIDTH, long int HEIGHT); /** Bayer RGGB to RGB 24 */ void bayer_rggb_2rgb24(unsigned char *dst, unsigned char *srcc, long int WIDTH, long int HEIGHT); /*@}*/ #ifdef __cplusplus } #endif enum Options { ioNoBlock = (1 << 0), ioUseSelect = (1 << 1), haveBrightness = (1 << 2), haveContrast = (1 << 3), haveHue = (1 << 4), haveColor = (1 << 5), haveWhiteness = (1 << 6) }; libindi/libs/webcam/v4l2_colorspace.h0000664000175000017500000000153613263645557017040 0ustar jasemjasem #pragma once #include #ifdef __cplusplus extern "C" { #endif /* for kernel < 3.19 */ /* ITU-R 601 -- SDTV */ #define YCBCR_ENC_601 1 /* Rec. 709 -- HDTV */ #define YCBCR_ENC_709 2 /* sYCC (Y'CbCr encoding of sRGB) */ #define YCBCR_ENC_SYCC 5 /* SMPTE 240M -- Obsolete HDTV */ #define YCBCR_ENC_SMPTE240M 8 #define QUANTIZATION_FULL_RANGE 1 #define QUANTIZATION_LIM_RANGE 2 void initColorSpace(); const char *getColorSpaceName(struct v4l2_format *fmt); unsigned int getYCbCrEncoding(struct v4l2_format *fmt); const char *getYCbCrEncodingName(struct v4l2_format *fmt); unsigned int getQuantization(struct v4l2_format *fmt); const char *getQuantizationName(struct v4l2_format *fmt); void rangeY8(unsigned char *buf, unsigned int len); void linearize(float *buf, unsigned int len, struct v4l2_format *fmt); #ifdef __cplusplus } #endif libindi/libs/webcam/v4l2_decode/0000775000175000017500000000000013263645557015753 5ustar jasemjasemlibindi/libs/webcam/v4l2_decode/v4l2_decode.cpp0000664000175000017500000000332313263645557020552 0ustar jasemjasem/* Copyright (C) 2005 by Jasem Mutlaq Copyright (C) 2014 by geehalel V4L2 Decode 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 */ #include "v4l2_decode.h" #include "v4l2_builtin_decoder.h" V4L2_Decoder::V4L2_Decoder() { } V4L2_Decoder::~V4L2_Decoder() { } const char *V4L2_Decoder::getName() { return name; } V4L2_Decode::V4L2_Decode() { decoder_list.push_back(new V4L2_Builtin_Decoder()); default_decoder = decoder_list.at(0); } V4L2_Decode::~V4L2_Decode() { std::vector::iterator it; for (it = decoder_list.begin(); it != decoder_list.end(); it++) { delete (*it); } decoder_list.clear(); } std::vector V4L2_Decode::getDecoderList() { return decoder_list; } V4L2_Decoder *V4L2_Decode::getDecoder() { return current_decoder; } V4L2_Decoder *V4L2_Decode::getDefaultDecoder() { return default_decoder; }; void V4L2_Decode::setDecoder(V4L2_Decoder *decoder) { current_decoder = decoder; }; libindi/libs/webcam/v4l2_decode/v4l2_builtin_decoder.cpp0000664000175000017500000012621113263645557022464 0ustar jasemjasem/* Copyright (C) 2005 by Jasem Mutlaq Copyright (C) 2014 by geehalel V4L2 Builtin Decoder As of August 2015 gst-lauch-1.0 and v4l2loopback do not work together well (https://github.com/umlaeute/v4l2loopback/issues/83) Still use v4l2loopback (see below) but with ffmpeg: ffmpeg -f lavfi -re -i "smptebars=size=640x480:rate=30" -pix_fmt yuv420p -f v4l2 /dev/video8 Use the -re flag to really get frame rate otherwise ffmpeg outputs frames as fast as possible (400 to 700fps). With indi-opencv-ccd use /dev/video1 as v4l2loopback device for 16 bits gray ffmpeg -f lavfi -re -i "testsrc=size=640x480:rate=30" -pix_fmt gray16le -f v4l2 /dev/video1 Profiling cmake -DCMAKE_INSTALL_PREFIX=/usr -DCMAKE_CXX_FLAGS=-pg /nfs/devel/sourceforge/indi-code/libindi/ kill indiserver with the kill command, not ctrl-c To test decoders, use gstreamer and the v4l2loopback kernel module Use the experimental git branch of v4l2loopback to get more pixel formats git clone https://github.com/umlaeute/v4l2loopback/ -b experimental cd v4l2loopback; make; sudo make install sudo modprobe v4l2loopback video_nr=8 card_label="Indi V4L2 Test Loopback" # debug=n gst-launch-1.0 -v videotestsrc ! video/x-raw,format=\(string\)UYVY,width=1024,height=576,framerate=\(fraction\)30/1 ! v4l2sink device=/dev/video8 gst-launch-1.0 -v v4l2src device=/dev/video0 ! jpegdec ! videoconvert! video/x-raw,format=\(string\)UYVY,width=1280,height=960,framerate=\(fraction\)5/1 ! v4l2sink device=/dev/video8 For Bayer I used gst-launch-0.10 modprobe v4l2loopback video_nr=8 card_label="Indi Loopback" exclusive_caps=0,0 gst-launch-0.10 -v videotestsrc ! 'video/x-raw-bayer, format=(string)bggr, width=640, height=480, framerate=(fraction)2/1' ! v4l2sink device=/dev/video8 For Gray16 format I use gst-launch with a videotstsrc in GRAY16 format writing in a FIFO and a C program reding the FIFO into the v4l2loopback device mkfifo /tmp/videopipe gst-launch-1.0 -v videotestsrc ! video/x-raw,format=\(string\)GRAY16_LE,width=1024,height=576,framerate=\(fraction\)25/1 ! filesink location =/tmp/videopipe ./gray16_to_v4l2 /dev/video8 < /tmp/videopipe & The C program ./gray16_to_v4l2 is adpated from https://github.com/umlaeute/v4l2loopback/blob/master/examples/yuv4mpeg_to_v4l2.c : process_header/read_header calls are suppressed (copy_frames uses a while (true) loop), frame_width and frame_height are constant and V4L2 pixel format is set to V4L2_PIX_FMT_Y16. To repeat an avi stream ./v4l2loopback-ctl set-caps 'video/x-raw-yuv,width=720,height=576' /dev/video8 ./v4l2loopback-ctl set-fps 25/1 /dev/video8 as normal user while true; do gst-launch-0.10 -vvv filesrc location=chi-aquarius.avi ! avidemux name=demux demux.video_00 ! queue ! decodebin ! videoscale add-borders=true ! v4l2sink device=/dev/video8 ; done 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 */ #include "v4l2_builtin_decoder.h" //#include "indilogger.h" #include "../ccvt.h" #include "../v4l2_colorspace.h" #include // memcpy V4L2_Builtin_Decoder::V4L2_Builtin_Decoder() { unsigned int i; name = "Builtin decoder"; useSoftCrop = false; doCrop = false; doQuantization = false; YBuf = nullptr; UBuf = nullptr; VBuf = nullptr; yuvBuffer = nullptr; yuyvBuffer = nullptr; colorBuffer = nullptr; rgb24_buffer = nullptr; linearBuffer = nullptr; //cropbuf = nullptr; for (i = 0; i < 32; i++) { lut5[i] = (char)(((float)i * 255.0) / 31.0); } for (i = 0; i < 64; i++) { lut6[i] = (char)(((float)i * 255.0) / 63.0); } initColorSpace(); bpp = 8; } V4L2_Builtin_Decoder::~V4L2_Builtin_Decoder() { YBuf = nullptr; UBuf = nullptr; VBuf = nullptr; if (yuvBuffer) delete[](yuvBuffer); yuvBuffer = nullptr; if (yuyvBuffer) delete[](yuyvBuffer); yuyvBuffer = nullptr; if (colorBuffer) delete[](colorBuffer); colorBuffer = nullptr; if (rgb24_buffer) delete[](rgb24_buffer); rgb24_buffer = nullptr; if (linearBuffer) delete[](linearBuffer); linearBuffer = nullptr; }; void V4L2_Builtin_Decoder::init() { init_supported_formats(); }; void V4L2_Builtin_Decoder::decode(unsigned char *frame, struct v4l2_buffer *buf) { //DEBUG(INDI::Logger::DBG_SESSION,"Calling builtin decoder decode"); //IDLog("Decoding buffer at %lx, len %d, bytesused %d, bytesperpixel %d, sequence %d, flag %x, field %x, use soft crop %c, do crop %c\n", frame, buf->length, buf->bytesused, fmt.fmt.pix.bytesperline, buf->sequence, buf->flags, buf->field, (useSoftCrop?'y':'n'), (doCrop?'y':'n')); switch (fmt.fmt.pix.pixelformat) { case V4L2_PIX_FMT_GREY: if (useSoftCrop && doCrop) { unsigned char *src = frame + crop.c.left + (crop.c.top * fmt.fmt.pix.width); unsigned char *dest = YBuf; for (unsigned int i = 0; i < (unsigned int)crop.c.height; i++) { memcpy(dest, src, crop.c.width); src += fmt.fmt.pix.width; dest += crop.c.width; } } else { memcpy(YBuf, frame, bufwidth * bufheight); } break; case V4L2_PIX_FMT_Y16: if (useSoftCrop && doCrop) { unsigned char *src = frame + 2 * (crop.c.left) + (crop.c.top * fmt.fmt.pix.bytesperline); unsigned char *dest = yuyvBuffer; for (unsigned int i = 0; i < (unsigned int)crop.c.height; i++) { memcpy(dest, src, 2 * crop.c.width); src += fmt.fmt.pix.bytesperline; dest += 2 * crop.c.width; } } else { memcpy(yuyvBuffer, frame, 2 * bufwidth * bufheight); } break; case V4L2_PIX_FMT_YUV420: case V4L2_PIX_FMT_YVU420: if (useSoftCrop && doCrop) { unsigned char *src = frame + crop.c.left + (crop.c.top * fmt.fmt.pix.width); unsigned char *dest = YBuf; //IDLog("grabImage: src=%d dest=%d\n", src, dest); for (unsigned int i = 0; i < (unsigned int)crop.c.height; i++) { memcpy(dest, src, crop.c.width); src += fmt.fmt.pix.width; dest += crop.c.width; } dest = UBuf; src = frame + (fmt.fmt.pix.width * fmt.fmt.pix.height) + ((crop.c.left + (crop.c.top * fmt.fmt.pix.width) / 2) / 2); if (fmt.fmt.pix.pixelformat == V4L2_PIX_FMT_YVU420) { dest = VBuf; } for (unsigned int i = 0; i < (unsigned int)crop.c.height / 2; i++) { memcpy(dest, src, crop.c.width / 2); src += fmt.fmt.pix.width / 2; dest += crop.c.width / 2; } dest = VBuf; src = frame + (fmt.fmt.pix.width * fmt.fmt.pix.height) + ((fmt.fmt.pix.width * fmt.fmt.pix.height) / 4) + ((crop.c.left + (crop.c.top * fmt.fmt.pix.width) / 2) / 2); if (fmt.fmt.pix.pixelformat == V4L2_PIX_FMT_YVU420) { dest = UBuf; } for (unsigned int i = 0; i < (unsigned int)crop.c.height / 2; i++) { memcpy(dest, src, crop.c.width / 2); src += fmt.fmt.pix.width / 2; dest += crop.c.width / 2; } } else { memcpy(YBuf, frame, bufwidth * bufheight); if (fmt.fmt.pix.pixelformat == V4L2_PIX_FMT_YUV420) { memcpy(UBuf, frame + bufwidth * bufheight, (bufwidth / 2) * (bufheight / 2)); memcpy(VBuf, frame + bufwidth * bufheight + (bufwidth / 2) * (bufheight / 2), (bufwidth / 2) * (bufheight / 2)); } else { if (fmt.fmt.pix.pixelformat == V4L2_PIX_FMT_YVU420) { memcpy(VBuf, frame + bufwidth * bufheight, (bufwidth / 2) * (bufheight / 2)); memcpy(UBuf, frame + bufwidth * bufheight + (bufwidth / 2) * (bufheight / 2), (bufwidth / 2) * (bufheight / 2)); } } } break; case V4L2_PIX_FMT_NV12: case V4L2_PIX_FMT_NV21: if (useSoftCrop && doCrop) { unsigned char *src = frame + crop.c.left + (crop.c.top * fmt.fmt.pix.bytesperline); unsigned char *dest = YBuf, *destv = nullptr; //IDLog("grabImage: src=%d dest=%d\n", src, dest); for (unsigned int i = 0; i < (unsigned int)crop.c.height; i++) { memcpy(dest, src, crop.c.width); src += fmt.fmt.pix.bytesperline; dest += crop.c.width; } dest = UBuf; destv = VBuf; src = frame + (fmt.fmt.pix.bytesperline * fmt.fmt.pix.height) + ((crop.c.left + (crop.c.top * fmt.fmt.pix.bytesperline) / 2) / 2); if (fmt.fmt.pix.pixelformat == V4L2_PIX_FMT_NV21) { dest = VBuf; destv = UBuf; } for (unsigned int i = 0; i < (unsigned int)crop.c.height / 2; i++) { unsigned char *s = src; for (unsigned int j = 0; j < (unsigned int)crop.c.width; j += 2) { *(dest++) = *(s++); *(destv++) = *(s++); } src += fmt.fmt.pix.bytesperline; } } else { unsigned char *src = frame; unsigned char *dest = YBuf; unsigned char *destv = VBuf; unsigned char *s = nullptr; for (unsigned int i = 0; i < bufheight; i++) { memcpy(dest, src, bufwidth); src += fmt.fmt.pix.bytesperline; dest += bufwidth; } dest = UBuf; src = frame + (fmt.fmt.pix.bytesperline * bufheight); if (fmt.fmt.pix.pixelformat == V4L2_PIX_FMT_NV21) { dest = VBuf; destv = UBuf; } for (unsigned int i = 0; i < bufheight / 2; i++) { s = src; //IDLog("NV12: converting UV line %d at %p, offset %d, pUbuf %p offset %d, pVbuf %p offset %d\n", i, s, s- (frame + (fmt.fmt.pix.bytesperline * bufheight)), dest, dest-UBuf, destv, destv-VBuf); for (unsigned int j = 0; j < bufwidth; j += 2) { *(dest++) = *(s++); *(destv++) = *(s++); } src += fmt.fmt.pix.bytesperline; } } break; case V4L2_PIX_FMT_YUYV: if (useSoftCrop && doCrop) { unsigned char *src = frame + 2 * (crop.c.left) + (crop.c.top * fmt.fmt.pix.bytesperline); unsigned char *dest = yuyvBuffer; for (unsigned int i = 0; i < (unsigned int)crop.c.height; i++) { memcpy(dest, src, 2 * crop.c.width); src += fmt.fmt.pix.bytesperline; dest += 2 * crop.c.width; } } else { memcpy(yuyvBuffer, frame, 2 * bufwidth * bufheight); } break; case V4L2_PIX_FMT_UYVY: case V4L2_PIX_FMT_VYUY: case V4L2_PIX_FMT_YVYU: { unsigned char *src = nullptr; unsigned char *dest = yuyvBuffer; unsigned char *s = nullptr, *s1 = nullptr, *s2 = nullptr, *s3 = nullptr, *s4 = nullptr; if (useSoftCrop && doCrop) { src = frame + 2 * (crop.c.left) + (crop.c.top * fmt.fmt.pix.bytesperline); //IDLog("Decoding UYVY with cropping %dx%d frame at %lx\n", width, height, src); } else { src = frame; //IDLog("Decoding UYVY %dx%d frame at %lx\n", width, height, src); } for (int i = 0; i < (int)bufheight; i++) { s = src; switch (fmt.fmt.pix.pixelformat) { case V4L2_PIX_FMT_UYVY: s1 = (s + 1); s2 = (s); s3 = (s + 3); s4 = (s + 2); break; case V4L2_PIX_FMT_VYUY: s1 = (s + 1); s2 = (s + 2); s3 = (s + 3); s4 = (s); break; case V4L2_PIX_FMT_YVYU: s1 = (s); s2 = (s + 3); s3 = (s + 2); s4 = (s + 1); break; } for (int j = 0; j < (int)(bufwidth / 2); j++) { *(dest++) = *(s1); *(dest++) = *(s2); *(dest++) = *(s3); *(dest++) = *(s4); s1 += 4; s2 += 4; s3 += 4; s4 += 4; } src += fmt.fmt.pix.bytesperline; } } break; case V4L2_PIX_FMT_RGB24: { unsigned char *src = nullptr, *dest = rgb24_buffer; if (useSoftCrop && doCrop) { src = frame + (3 * (crop.c.left)) + (crop.c.top * fmt.fmt.pix.bytesperline); } else { src = frame; } for (unsigned int i = 0; i < bufheight; i++) { memcpy(dest, src, 3 * bufwidth); src += fmt.fmt.pix.bytesperline; dest += 3 * bufwidth; } //memcpy(rgb24_buffer, frame, fmt.fmt.pix.width * fmt.fmt.pix.height * 3); } break; case V4L2_PIX_FMT_RGB555: { unsigned char *src, *dest = rgb24_buffer; if (useSoftCrop && doCrop) { src = frame + (2 * (crop.c.left)) + (crop.c.top * fmt.fmt.pix.bytesperline); } else { src = frame; } for (unsigned int i = 0; i < bufheight; i++) { unsigned char *s = src; for (unsigned int j = 0; j < bufwidth; j++) { *(dest++) = lut5[((*(s + 1) & 0x7C) >> 2)]; // R *(dest++) = lut5[(((*(s + 1) & 0x03) << 3) | ((*(s)&0xE0) >> 5))]; // G *(dest++) = lut5[((*s) & 0x1F)]; // B s += 2; } src += fmt.fmt.pix.bytesperline; } } break; case V4L2_PIX_FMT_RGB565: { unsigned char *src = nullptr, *dest = rgb24_buffer; if (useSoftCrop && doCrop) { src = frame + (2 * (crop.c.left)) + (crop.c.top * fmt.fmt.pix.bytesperline); } else { src = frame; } for (unsigned int i = 0; i < bufheight; i++) { unsigned char *s = src; for (unsigned int j = 0; j < bufwidth; j++) { *(dest++) = lut5[((*(s + 1) & 0xF8) >> 3)]; // R *(dest++) = lut6[(((*(s + 1) & 0x07) << 3) | ((*(s)&0xE0) >> 5))]; // G *(dest++) = lut5[((*s) & 0x1F)]; // B s += 2; } src += fmt.fmt.pix.bytesperline; } } break; case V4L2_PIX_FMT_SBGGR8: bayer2rgb24(rgb24_buffer, frame, fmt.fmt.pix.width, fmt.fmt.pix.height); break; case V4L2_PIX_FMT_SRGGB8: bayer_rggb_2rgb24(rgb24_buffer, frame, fmt.fmt.pix.width, fmt.fmt.pix.height); break; case V4L2_PIX_FMT_SBGGR16: bayer16_2_rgb24((unsigned short *)rgb24_buffer, (unsigned short *)frame, fmt.fmt.pix.width, fmt.fmt.pix.height); break; case V4L2_PIX_FMT_JPEG: case V4L2_PIX_FMT_MJPEG: //mjpegtoyuv420p(yuvBuffer, ((unsigned char *) buffers[buf.index].start), fmt.fmt.pix.width, fmt.fmt.pix.height, buffers[buf.index].length); mjpegtoyuv420p(yuvBuffer, frame, fmt.fmt.pix.width, fmt.fmt.pix.height, buf->bytesused); break; default: { unsigned char *src = YBuf; for (unsigned int i = 0; i < bufheight * bufwidth; i++) { *(src++) = random() % 255; } } } } bool V4L2_Builtin_Decoder::setcrop(struct v4l2_crop c) { crop = c; IDLog("Decoder set crop: %dx%d at (%d, %d)\n", crop.c.width, crop.c.height, crop.c.left, crop.c.top); if (supported_formats[fmt.fmt.pix.pixelformat]->softcrop) { doCrop = true; allocBuffers(); return true; } else { doCrop = false; return false; } } void V4L2_Builtin_Decoder::resetcrop() { IDLog("Decoder reset crop\n"); doCrop = false; allocBuffers(); } void V4L2_Builtin_Decoder::usesoftcrop(bool c) { IDLog("Decoder usesoftcrop %s\n", (c ? "true" : "false")); useSoftCrop = c; } void V4L2_Builtin_Decoder::setformat(struct v4l2_format f, bool use_ext_pix_format) { (void)use_ext_pix_format; fmt = f; if (supported_formats.count(fmt.fmt.pix.pixelformat) == 1) bpp = supported_formats.at(fmt.fmt.pix.pixelformat)->bpp; else bpp = 8; IDLog("Decoder set format: %c%c%c%c size %dx%d bpp %d\n", (fmt.fmt.pix.pixelformat) & 0xFF, (fmt.fmt.pix.pixelformat >> 8) & 0xFF, (fmt.fmt.pix.pixelformat >> 16) & 0xFF, (fmt.fmt.pix.pixelformat >> 24) & 0xFF, f.fmt.pix.width, f.fmt.pix.height, bpp); /* kernel 3.19 if (use_ext_pix_format && fmt.fmt.pix.priv == V4L2_PIX_FMT_PRIV_MAGIC) IDLog("Decoder: Colorspace is %d, YCbCr encoding is %d, Quantization is %d\n", fmt.fmt.pix.colorspace, fmt.fmt.pix.ycbcr_enc, fmt.fmt.pix.quantization); else */ IDLog("Decoder: Colorspace is %d, using default ycbcr encoding and quantization\n", fmt.fmt.pix.colorspace); doCrop = false; allocBuffers(); } void V4L2_Builtin_Decoder::setQuantization(bool doquantization) { doQuantization = doquantization; } void V4L2_Builtin_Decoder::setLinearization(bool dolinearization) { doLinearization = dolinearization; if (doLinearization) bpp = 16; else if (supported_formats.count(fmt.fmt.pix.pixelformat) == 1) bpp = supported_formats.at(fmt.fmt.pix.pixelformat)->bpp; else bpp = 8; } void V4L2_Builtin_Decoder::allocBuffers() { YBuf = nullptr; UBuf = nullptr; VBuf = nullptr; if (yuvBuffer) delete[](yuvBuffer); yuvBuffer = nullptr; if (yuyvBuffer) delete[](yuyvBuffer); yuyvBuffer = nullptr; if (colorBuffer) delete[](colorBuffer); colorBuffer = nullptr; if (rgb24_buffer) delete[](rgb24_buffer); rgb24_buffer = nullptr; if (linearBuffer) delete[](linearBuffer); linearBuffer = nullptr; //if (cropbuf) free(cropbuf); cropbuf=nullptr; if (doCrop) { bufwidth = crop.c.width; bufheight = crop.c.height; } else { bufwidth = fmt.fmt.pix.width; bufheight = fmt.fmt.pix.height; } switch (fmt.fmt.pix.pixelformat) { case V4L2_PIX_FMT_GREY: case V4L2_PIX_FMT_JPEG: case V4L2_PIX_FMT_MJPEG: case V4L2_PIX_FMT_YUV420: case V4L2_PIX_FMT_YVU420: case V4L2_PIX_FMT_NV12: case V4L2_PIX_FMT_NV21: yuvBuffer = new unsigned char[(bufwidth * bufheight) + ((bufwidth * bufheight) / 2)]; YBuf = yuvBuffer; UBuf = yuvBuffer + (bufwidth * bufheight); VBuf = UBuf + ((bufwidth * bufheight) / 4); // bzero(Ubuf, ((bufwidth * bufheight) / 2)); break; case V4L2_PIX_FMT_Y16: case V4L2_PIX_FMT_YUYV: case V4L2_PIX_FMT_UYVY: case V4L2_PIX_FMT_VYUY: case V4L2_PIX_FMT_YVYU: yuyvBuffer = new unsigned char[(bufwidth * bufheight) * 2]; break; case V4L2_PIX_FMT_RGB24: case V4L2_PIX_FMT_RGB555: case V4L2_PIX_FMT_RGB565: case V4L2_PIX_FMT_SBGGR8: case V4L2_PIX_FMT_SRGGB8: case V4L2_PIX_FMT_SBGGR16: rgb24_buffer = new unsigned char[(bufwidth * bufheight) * (bpp / 8) * 3]; break; default: yuvBuffer = new unsigned char[(bufwidth * bufheight) + ((bufwidth * bufheight) / 2)]; YBuf = yuvBuffer; UBuf = yuvBuffer + (bufwidth * bufheight); VBuf = UBuf + ((bufwidth * bufheight) / 4); break; } IDLog("Decoder allocBuffers cropping %s\n", (doCrop ? "true" : "false")); } void V4L2_Builtin_Decoder::makeLinearY() { unsigned char *src = YBuf; float *dest; unsigned int i; if (!linearBuffer) { linearBuffer = new float[(bufwidth * bufheight)]; } dest = linearBuffer; for (i = 0; i < bufwidth * bufheight; i++) *dest++ = (*src++) / 255.0; linearize(linearBuffer, bufwidth * bufheight, &fmt); } void V4L2_Builtin_Decoder::makeY() { if (!yuvBuffer) { yuvBuffer = new unsigned char[(bufwidth * bufheight) + ((bufwidth * bufheight) / 2)]; YBuf = yuvBuffer; UBuf = yuvBuffer + (bufwidth * bufheight); VBuf = UBuf + ((bufwidth * bufheight) / 4); } switch (fmt.fmt.pix.pixelformat) { case V4L2_PIX_FMT_RGB24: case V4L2_PIX_FMT_RGB555: case V4L2_PIX_FMT_RGB565: case V4L2_PIX_FMT_SBGGR8: case V4L2_PIX_FMT_SRGGB8: RGB2YUV(bufwidth, bufheight, rgb24_buffer, YBuf, UBuf, VBuf, 0); break; case V4L2_PIX_FMT_YUYV: case V4L2_PIX_FMT_UYVY: case V4L2_PIX_FMT_VYUY: case V4L2_PIX_FMT_YVYU: // todo handcopy only Ybuf using an int, byfwidth should be even ccvt_yuyv_420p(bufwidth, bufheight, yuyvBuffer, YBuf, UBuf, VBuf); break; } } unsigned char *V4L2_Builtin_Decoder::getY() { if (fmt.fmt.pix.pixelformat == V4L2_PIX_FMT_Y16) return yuyvBuffer; makeY(); if (doQuantization && getQuantization(&fmt) == QUANTIZATION_LIM_RANGE) rangeY8(YBuf, (bufwidth * bufheight)); if (doLinearization) { unsigned int i; float *src; unsigned short *dest; if (!yuyvBuffer) yuyvBuffer = new unsigned char[(bufwidth * bufheight) * 2]; makeLinearY(); src = linearBuffer; dest = (unsigned short *)yuyvBuffer; for (i = 0; i < bufwidth * bufheight; i++) *dest++ = (unsigned short)(*src++ * 65535.0); return yuyvBuffer; } return YBuf; } float *V4L2_Builtin_Decoder::getLinearY() { makeY(); if (doQuantization && getQuantization(&fmt) == QUANTIZATION_LIM_RANGE) rangeY8(YBuf, (bufwidth * bufheight)); makeLinearY(); return linearBuffer; } unsigned char *V4L2_Builtin_Decoder::getU() { return UBuf; } unsigned char *V4L2_Builtin_Decoder::getV() { return VBuf; } /* used for streaming/exposure */ #if 0 unsigned char * V4L2_Builtin_Decoder::geColorBuffer() { //cerr << "in get color buffer " << endl; //IDLog("Decoder geRGBBuffer %s\n", (doCrop?"true":"false")); if (!colorBuffer) colorBuffer = new unsigned char[(bufwidth * bufheight) * (bpp / 8) * 4]; switch (fmt.fmt.pix.pixelformat) { case V4L2_PIX_FMT_GREY: case V4L2_PIX_FMT_JPEG: case V4L2_PIX_FMT_MJPEG: case V4L2_PIX_FMT_YUV420: case V4L2_PIX_FMT_YVU420: case V4L2_PIX_FMT_NV12: case V4L2_PIX_FMT_NV21: ccvt_420p_bgr32(bufwidth, bufheight, (void *)yuvBuffer, (void *)colorBuffer); break; case V4L2_PIX_FMT_YUYV: case V4L2_PIX_FMT_UYVY: case V4L2_PIX_FMT_VYUY: case V4L2_PIX_FMT_YVYU: ccvt_yuyv_bgr32(bufwidth, bufheight, yuyvBuffer, (void *)colorBuffer); break; case V4L2_PIX_FMT_RGB24: case V4L2_PIX_FMT_RGB555: case V4L2_PIX_FMT_RGB565: case V4L2_PIX_FMT_SBGGR8: case V4L2_PIX_FMT_SRGGB8: ccvt_rgb24_bgr32(bufwidth, bufheight, rgb24_buffer, (void *)colorBuffer); break; case V4L2_PIX_FMT_Y16: /* OOOps this is planar ARGB */ /* bzero(colorBuffer, bufwidth * bufheight * 2); memcpy(colorBuffer + (bufwidth * bufheight * 2), yuyvBuffer, bufwidth * bufheight * 2); memcpy(colorBuffer + 2 * (bufwidth * bufheight * 2), yuyvBuffer, bufwidth * bufheight * 2); memcpy(colorBuffer + 3 * (bufwidth * bufheight * 2), yuyvBuffer, bufwidth * bufheight * 2); */ { /* this is bgra , use unsigned short here... */ unsigned int i; unsigned char * src = yuyvBuffer; unsigned char * dest = colorBuffer; for (i = 0; i < bufwidth * bufheight; i += 1) { *dest++ = *src; *dest++ = *(src + 1); *dest++ = *src; *dest++ = *(src + 1); *dest++ = *src; *dest++ = *(src + 1); *dest++ = 0; *dest++ = 0; src += 2; } } break; case V4L2_PIX_FMT_SBGGR16: { /* this is bgra , now I use unsigned short! */ unsigned int i; unsigned short * src = (unsigned short *)rgb24_buffer; unsigned short * dest = (unsigned short *)colorBuffer; for (i = 0; i < bufwidth * bufheight; i += 1) { *dest++ = *(src + 2); *dest++ = *(src + 1); *dest++ = *(src); *dest++ = 0; src += 3; } } default: ccvt_420p_bgr32(bufwidth, bufheight, (void *)yuvBuffer, (void *)colorBuffer); break; } return colorBuffer; } #endif /* used for SER recorder */ unsigned char *V4L2_Builtin_Decoder::getRGBBuffer() { //cerr << "in get color buffer " << endl; //IDLog("Decoder geRGBBuffer %s\n", (doCrop?"true":"false")); if (!rgb24_buffer) rgb24_buffer = new unsigned char[(bufwidth * bufheight) * 3]; switch (fmt.fmt.pix.pixelformat) { case V4L2_PIX_FMT_GREY: case V4L2_PIX_FMT_JPEG: case V4L2_PIX_FMT_MJPEG: case V4L2_PIX_FMT_YUV420: case V4L2_PIX_FMT_YVU420: case V4L2_PIX_FMT_NV12: case V4L2_PIX_FMT_NV21: ccvt_420p_rgb24(bufwidth, bufheight, (void *)yuvBuffer, (void *)rgb24_buffer); break; case V4L2_PIX_FMT_YUYV: case V4L2_PIX_FMT_UYVY: case V4L2_PIX_FMT_VYUY: case V4L2_PIX_FMT_YVYU: //if (!colorBuffer) colorBuffer = new unsigned char[(bufwidth * bufheight) * 4]; //ccvt_yuyv_bgr32(bufwidth, bufheight, yuyvBuffer, rgb24_buffer); //ccvt_bgr32_rgb24(bufwidth, bufheight, colorBuffer, (void*)rgb24_buffer); ccvt_yuyv_rgb24(bufwidth, bufheight, yuyvBuffer, (void *)rgb24_buffer); break; case V4L2_PIX_FMT_RGB24: case V4L2_PIX_FMT_RGB555: case V4L2_PIX_FMT_RGB565: case V4L2_PIX_FMT_SBGGR8: case V4L2_PIX_FMT_SRGGB8: case V4L2_PIX_FMT_SBGGR16: break; default: ccvt_420p_rgb24(bufwidth, bufheight, (void *)yuvBuffer, (void *)rgb24_buffer); break; } return rgb24_buffer; } int V4L2_Builtin_Decoder::getBpp() { return (int)(bpp); } bool V4L2_Builtin_Decoder::issupportedformat(unsigned int format) { //IDLog("Checking support for %c%c%c%c: %c\n", format&0xFF, (format>>8)&0xFF, (format>>16)&0xFF,(format>>24)&0xFF, (supported_formats.count(format) > 0)?'y':'n'); return (supported_formats.count(format) > 0); } const std::vector &V4L2_Builtin_Decoder::getsupportedformats() { return vsuppformats; } void V4L2_Builtin_Decoder::init_supported_formats() { /* RGB formats */ // V4L2_PIX_FMT_RGB332 , // v4l2_fourcc('R', 'G', 'B', '1') /* 8 RGB-3-3-2 */ // V4L2_PIX_FMT_RGB444 , // v4l2_fourcc('R', '4', '4', '4') /* 16 xxxxrrrr ggggbbbb */ // V4L2_PIX_FMT_RGB555 , // v4l2_fourcc('R', 'G', 'B', 'O') /* 16 RGB-5-5-5 */ supported_formats.insert( std::make_pair(V4L2_PIX_FMT_RGB555, new V4L2_Builtin_Decoder::format(V4L2_PIX_FMT_RGB555, 8, true))); // V4L2_PIX_FMT_RGB565 , // v4l2_fourcc('R', 'G', 'B', 'P') /* 16 RGB-5-6-5 */ supported_formats.insert( std::make_pair(V4L2_PIX_FMT_RGB565, new V4L2_Builtin_Decoder::format(V4L2_PIX_FMT_RGB565, 8, true))); // V4L2_PIX_FMT_RGB555X , // v4l2_fourcc('R', 'G', 'B', 'Q') /* 16 RGB-5-5-5 BE */ // V4L2_PIX_FMT_RGB565X , // v4l2_fourcc('R', 'G', 'B', 'R') /* 16 RGB-5-6-5 BE */ // V4L2_PIX_FMT_BGR666 , // v4l2_fourcc('B', 'G', 'R', 'H') /* 18 BGR-6-6-6 */ // V4L2_PIX_FMT_BGR24 , // v4l2_fourcc('B', 'G', 'R', '3') /* 24 BGR-8-8-8 */ // V4L2_PIX_FMT_RGB24 , // v4l2_fourcc('R', 'G', 'B', '3') /* 24 RGB-8-8-8 */ supported_formats.insert( std::make_pair(V4L2_PIX_FMT_RGB24, new V4L2_Builtin_Decoder::format(V4L2_PIX_FMT_RGB24, 8, true))); // V4L2_PIX_FMT_BGR32 , // v4l2_fourcc('B', 'G', 'R', '4') /* 32 BGR-8-8-8-8 */ // V4L2_PIX_FMT_RGB32 , // v4l2_fourcc('R', 'G', 'B', '4') /* 32 RGB-8-8-8-8 */ /* Grey formats */ //V4L2_PIX_FMT_GREY , // v4l2_fourcc('G', 'R', 'E', 'Y') /* 8 Greyscale */ supported_formats.insert( std::make_pair(V4L2_PIX_FMT_GREY, new V4L2_Builtin_Decoder::format(V4L2_PIX_FMT_GREY, 8, true))); // V4L2_PIX_FMT_Y4 , // v4l2_fourcc('Y', '0', '4', ' ') /* 4 Greyscale */ // V4L2_PIX_FMT_Y6 , // v4l2_fourcc('Y', '0', '6', ' ') /* 6 Greyscale */ // V4L2_PIX_FMT_Y10 , // v4l2_fourcc('Y', '1', '0', ' ') /* 10 Greyscale */ // V4L2_PIX_FMT_Y12 , // v4l2_fourcc('Y', '1', '2', ' ') /* 12 Greyscale */ // V4L2_PIX_FMT_Y16 , // v4l2_fourcc('Y', '1', '6', ' ') /* 16 Greyscale */ supported_formats.insert( std::make_pair(V4L2_PIX_FMT_Y16, new V4L2_Builtin_Decoder::format(V4L2_PIX_FMT_Y16, 16, true))); /* Grey bit-packed formats */ // V4L2_PIX_FMT_Y10BPACK , // v4l2_fourcc('Y', '1', '0', 'B') /* 10 Greyscale bit-packed */ /* Palette formats */ // V4L2_PIX_FMT_PAL8 , // v4l2_fourcc('P', 'A', 'L', '8') /* 8 8-bit palette */ /* Chrominance formats */ // V4L2_PIX_FMT_UV8 , // v4l2_fourcc('U', 'V', '8', ' ') /* 8 UV 4:4 */ /* Luminance+Chrominance formats */ // V4L2_PIX_FMT_YVU410 , // v4l2_fourcc('Y', 'V', 'U', '9') /* 9 YVU 4:1:0 */ // V4L2_PIX_FMT_YVU420 , // v4l2_fourcc('Y', 'V', '1', '2') /* 12 YVU 4:2:0 */ supported_formats.insert( std::make_pair(V4L2_PIX_FMT_YVU420, new V4L2_Builtin_Decoder::format(V4L2_PIX_FMT_YVU420, 8, true))); // V4L2_PIX_FMT_YUYV , // v4l2_fourcc('Y', 'U', 'Y', 'V') /* 16 YUV 4:2:2 */ supported_formats.insert( std::make_pair(V4L2_PIX_FMT_YUYV, new V4L2_Builtin_Decoder::format(V4L2_PIX_FMT_YUYV, 8, true))); // V4L2_PIX_FMT_YYUV , // v4l2_fourcc('Y', 'Y', 'U', 'V') /* 16 YUV 4:2:2 */ // V4L2_PIX_FMT_YVYU , // v4l2_fourcc('Y', 'V', 'Y', 'U') /* 16 YVU 4:2:2 */ supported_formats.insert( std::make_pair(V4L2_PIX_FMT_YVYU, new V4L2_Builtin_Decoder::format(V4L2_PIX_FMT_YVYU, 8, true))); // V4L2_PIX_FMT_UYVY , // v4l2_fourcc('U', 'Y', 'V', 'Y') /* 16 YUV 4:2:2 */ supported_formats.insert( std::make_pair(V4L2_PIX_FMT_UYVY, new V4L2_Builtin_Decoder::format(V4L2_PIX_FMT_UYVY, 8, true))); // V4L2_PIX_FMT_VYUY , // v4l2_fourcc('V', 'Y', 'U', 'Y') /* 16 YUV 4:2:2 */ supported_formats.insert( std::make_pair(V4L2_PIX_FMT_VYUY, new V4L2_Builtin_Decoder::format(V4L2_PIX_FMT_VYUY, 8, true))); // V4L2_PIX_FMT_YUV422P , // v4l2_fourcc('4', '2', '2', 'P') /* 16 YVU422 planar */ // V4L2_PIX_FMT_YUV411P , // v4l2_fourcc('4', '1', '1', 'P') /* 16 YVU411 planar */ // V4L2_PIX_FMT_Y41P , // v4l2_fourcc('Y', '4', '1', 'P') /* 12 YUV 4:1:1 */ // V4L2_PIX_FMT_YUV444 , // v4l2_fourcc('Y', '4', '4', '4') /* 16 xxxxyyyy uuuuvvvv */ // V4L2_PIX_FMT_YUV555 , // v4l2_fourcc('Y', 'U', 'V', 'O') /* 16 YUV-5-5-5 */ // V4L2_PIX_FMT_YUV565 , // v4l2_fourcc('Y', 'U', 'V', 'P') /* 16 YUV-5-6-5 */ // V4L2_PIX_FMT_YUV32 , // v4l2_fourcc('Y', 'U', 'V', '4') /* 32 YUV-8-8-8-8 */ // V4L2_PIX_FMT_YUV410 , // v4l2_fourcc('Y', 'U', 'V', '9') /* 9 YUV 4:1:0 */ // V4L2_PIX_FMT_YUV420 , // v4l2_fourcc('Y', 'U', '1', '2') /* 12 YUV 4:2:0 */ supported_formats.insert( std::make_pair(V4L2_PIX_FMT_YUV420, new V4L2_Builtin_Decoder::format(V4L2_PIX_FMT_YUV420, 8, true))); // V4L2_PIX_FMT_HI240 , // v4l2_fourcc('H', 'I', '2', '4') /* 8 8-bit color */ // V4L2_PIX_FMT_HM12 , // v4l2_fourcc('H', 'M', '1', '2') /* 8 YUV 4:2:0 16x16 macroblocks */ // V4L2_PIX_FMT_M420 , // v4l2_fourcc('M', '4', '2', '0') /* 12 YUV 4:2:0 2 lines y, 1 line uv interleaved */ /* two planes -- one Y, one Cr + Cb interleaved */ // V4L2_PIX_FMT_NV12 , // v4l2_fourcc('N', 'V', '1', '2') /* 12 Y/CbCr 4:2:0 */ supported_formats.insert( std::make_pair(V4L2_PIX_FMT_NV12, new V4L2_Builtin_Decoder::format(V4L2_PIX_FMT_NV12, 8, true))); // V4L2_PIX_FMT_NV21 , // v4l2_fourcc('N', 'V', '2', '1') /* 12 Y/CrCb 4:2:0 */ supported_formats.insert( std::make_pair(V4L2_PIX_FMT_NV21, new V4L2_Builtin_Decoder::format(V4L2_PIX_FMT_NV21, 8, true))); // V4L2_PIX_FMT_NV16 , // v4l2_fourcc('N', 'V', '1', '6') /* 16 Y/CbCr 4:2:2 */ // V4L2_PIX_FMT_NV61 , // v4l2_fourcc('N', 'V', '6', '1') /* 16 Y/CrCb 4:2:2 */ // V4L2_PIX_FMT_NV24 , // v4l2_fourcc('N', 'V', '2', '4') /* 24 Y/CbCr 4:4:4 */ // V4L2_PIX_FMT_NV42 , // v4l2_fourcc('N', 'V', '4', '2') /* 24 Y/CrCb 4:4:4 */ /* two non contiguous planes - one Y, one Cr + Cb interleaved */ // V4L2_PIX_FMT_NV12M , // v4l2_fourcc('N', 'M', '1', '2') /* 12 Y/CbCr 4:2:0 */ // V4L2_PIX_FMT_NV21M , // v4l2_fourcc('N', 'M', '2', '1') /* 21 Y/CrCb 4:2:0 */ // V4L2_PIX_FMT_NV16M , // v4l2_fourcc('N', 'M', '1', '6') /* 16 Y/CbCr 4:2:2 */ // V4L2_PIX_FMT_NV61M , // v4l2_fourcc('N', 'M', '6', '1') /* 16 Y/CrCb 4:2:2 */ // V4L2_PIX_FMT_NV12MT , // v4l2_fourcc('T', 'M', '1', '2') /* 12 Y/CbCr 4:2:0 64x32 macroblocks */ // V4L2_PIX_FMT_NV12MT_16X16 , // v4l2_fourcc('V', 'M', '1', '2') /* 12 Y/CbCr 4:2:0 16x16 macroblocks */ /* three non contiguous planes - Y, Cb, Cr */ // V4L2_PIX_FMT_YUV420M , // v4l2_fourcc('Y', 'M', '1', '2') /* 12 YUV420 planar */ // V4L2_PIX_FMT_YVU420M , // v4l2_fourcc('Y', 'M', '2', '1') /* 12 YVU420 planar */ /* Bayer formats - see http://www.siliconimaging.com/RGB%20Bayer.htm */ // V4L2_PIX_FMT_SBGGR8 , // v4l2_fourcc('B', 'A', '8', '1') /* 8 BGBG.. GRGR.. */ supported_formats.insert( std::make_pair(V4L2_PIX_FMT_SBGGR8, new V4L2_Builtin_Decoder::format(V4L2_PIX_FMT_SBGGR8, 8, false))); // V4L2_PIX_FMT_SGBRG8 , // v4l2_fourcc('G', 'B', 'R', 'G') /* 8 GBGB.. RGRG.. */ // V4L2_PIX_FMT_SGRBG8 , // v4l2_fourcc('G', 'R', 'B', 'G') /* 8 GRGR.. BGBG.. */ // V4L2_PIX_FMT_SRGGB8 , // v4l2_fourcc('R', 'G', 'G', 'B') /* 8 RGRG.. GBGB.. */ supported_formats.insert( std::make_pair(V4L2_PIX_FMT_SRGGB8, new V4L2_Builtin_Decoder::format(V4L2_PIX_FMT_SRGGB8, 8, false))); // V4L2_PIX_FMT_SBGGR10 , // v4l2_fourcc('B', 'G', '1', '0') /* 10 BGBG.. GRGR.. */ // V4L2_PIX_FMT_SGBRG10 , // v4l2_fourcc('G', 'B', '1', '0') /* 10 GBGB.. RGRG.. */ // V4L2_PIX_FMT_SGRBG10 , // v4l2_fourcc('B', 'A', '1', '0') /* 10 GRGR.. BGBG.. */ // V4L2_PIX_FMT_SRGGB10 , // v4l2_fourcc('R', 'G', '1', '0') /* 10 RGRG.. GBGB.. */ // V4L2_PIX_FMT_SBGGR12 , // v4l2_fourcc('B', 'G', '1', '2') /* 12 BGBG.. GRGR.. */ // V4L2_PIX_FMT_SGBRG12 , // v4l2_fourcc('G', 'B', '1', '2') /* 12 GBGB.. RGRG.. */ // V4L2_PIX_FMT_SGRBG12 , // v4l2_fourcc('B', 'A', '1', '2') /* 12 GRGR.. BGBG.. */ // V4L2_PIX_FMT_SRGGB12 , // v4l2_fourcc('R', 'G', '1', '2') /* 12 RGRG.. GBGB.. */ /* 10bit raw bayer a-law compressed to 8 bits */ // V4L2_PIX_FMT_SBGGR10ALAW8 , // v4l2_fourcc('a', 'B', 'A', '8') // V4L2_PIX_FMT_SGBRG10ALAW8 , // v4l2_fourcc('a', 'G', 'A', '8') // V4L2_PIX_FMT_SGRBG10ALAW8 , // v4l2_fourcc('a', 'g', 'A', '8') // V4L2_PIX_FMT_SRGGB10ALAW8 , // v4l2_fourcc('a', 'R', 'A', '8') /* 10bit raw bayer DPCM compressed to 8 bits */ // V4L2_PIX_FMT_SBGGR10DPCM8 , // v4l2_fourcc('b', 'B', 'A', '8') // V4L2_PIX_FMT_SGBRG10DPCM8 , // v4l2_fourcc('b', 'G', 'A', '8') // V4L2_PIX_FMT_SGRBG10DPCM8 , // v4l2_fourcc('B', 'D', '1', '0') // V4L2_PIX_FMT_SRGGB10DPCM8 , // v4l2_fourcc('b', 'R', 'A', '8') /* * 10bit raw bayer, expanded to 16 bits * xxxxrrrrrrrrrrxxxxgggggggggg xxxxggggggggggxxxxbbbbbbbbbb... */ // V4L2_PIX_FMT_SBGGR16 , // v4l2_fourcc('B', 'Y', 'R', '2') /* 16 BGBG.. GRGR.. */ supported_formats.insert( std::make_pair(V4L2_PIX_FMT_SBGGR16, new V4L2_Builtin_Decoder::format(V4L2_PIX_FMT_SBGGR16, 16, false))); /* compressed formats */ // V4L2_PIX_FMT_MJPEG , // v4l2_fourcc('M', 'J', 'P', 'G') /* Motion-JPEG */ supported_formats.insert( std::make_pair(V4L2_PIX_FMT_MJPEG, new V4L2_Builtin_Decoder::format(V4L2_PIX_FMT_MJPEG, 8, false))); // V4L2_PIX_FMT_JPEG , // v4l2_fourcc('J', 'P', 'E', 'G') /* JFIF JPEG */ supported_formats.insert( std::make_pair(V4L2_PIX_FMT_JPEG, new V4L2_Builtin_Decoder::format(V4L2_PIX_FMT_JPEG, 8, false))); // V4L2_PIX_FMT_DV , // v4l2_fourcc('d', 'v', 's', 'd') /* 1394 */ // V4L2_PIX_FMT_MPEG , // v4l2_fourcc('M', 'P', 'E', 'G') /* MPEG-1/2/4 Multiplexed */ // V4L2_PIX_FMT_H264 , // v4l2_fourcc('H', '2', '6', '4') /* H264 with start codes */ // V4L2_PIX_FMT_H264_NO_SC , // v4l2_fourcc('A', 'V', 'C', '1') /* H264 without start codes */ // V4L2_PIX_FMT_H264_MVC , // v4l2_fourcc('M', '2', '6', '4') /* H264 MVC */ // V4L2_PIX_FMT_H263 , // v4l2_fourcc('H', '2', '6', '3') /* H263 */ // V4L2_PIX_FMT_MPEG1 , // v4l2_fourcc('M', 'P', 'G', '1') /* MPEG-1 ES */ // V4L2_PIX_FMT_MPEG2 , // v4l2_fourcc('M', 'P', 'G', '2') /* MPEG-2 ES */ // V4L2_PIX_FMT_MPEG4 , // v4l2_fourcc('M', 'P', 'G', '4') /* MPEG-4 part 2 ES */ // V4L2_PIX_FMT_XVID , // v4l2_fourcc('X', 'V', 'I', 'D') /* Xvid */ // V4L2_PIX_FMT_VC1_ANNEX_G , // v4l2_fourcc('V', 'C', '1', 'G') /* SMPTE 421M Annex G compliant stream */ // V4L2_PIX_FMT_VC1_ANNEX_L , // v4l2_fourcc('V', 'C', '1', 'L') /* SMPTE 421M Annex L compliant stream */ // V4L2_PIX_FMT_VP8 , // v4l2_fourcc('V', 'P', '8', '0') /* VP8 */ /* Vendor-specific formats */ // V4L2_PIX_FMT_CPIA1 , // v4l2_fourcc('C', 'P', 'I', 'A') /* cpia1 YUV */ // V4L2_PIX_FMT_WNVA , // v4l2_fourcc('W', 'N', 'V', 'A') /* Winnov hw compress */ // V4L2_PIX_FMT_SN9C10X , // v4l2_fourcc('S', '9', '1', '0') /* SN9C10x compression */ // V4L2_PIX_FMT_SN9C20X_I420 , // v4l2_fourcc('S', '9', '2', '0') /* SN9C20x YUV 4:2:0 */ // V4L2_PIX_FMT_PWC1 , // v4l2_fourcc('P', 'W', 'C', '1') /* pwc older webcam */ // V4L2_PIX_FMT_PWC2 , // v4l2_fourcc('P', 'W', 'C', '2') /* pwc newer webcam */ // V4L2_PIX_FMT_ET61X251 , // v4l2_fourcc('E', '6', '2', '5') /* ET61X251 compression */ // V4L2_PIX_FMT_SPCA501 , // v4l2_fourcc('S', '5', '0', '1') /* YUYV per line */ // V4L2_PIX_FMT_SPCA505 , // v4l2_fourcc('S', '5', '0', '5') /* YYUV per line */ // V4L2_PIX_FMT_SPCA508 , // v4l2_fourcc('S', '5', '0', '8') /* YUVY per line */ // V4L2_PIX_FMT_SPCA561 , // v4l2_fourcc('S', '5', '6', '1') /* compressed GBRG bayer */ // V4L2_PIX_FMT_PAC207 , // v4l2_fourcc('P', '2', '0', '7') /* compressed BGGR bayer */ // V4L2_PIX_FMT_MR97310A , // v4l2_fourcc('M', '3', '1', '0') /* compressed BGGR bayer */ // V4L2_PIX_FMT_JL2005BCD , // v4l2_fourcc('J', 'L', '2', '0') /* compressed RGGB bayer */ // V4L2_PIX_FMT_SN9C2028 , // v4l2_fourcc('S', 'O', 'N', 'X') /* compressed GBRG bayer */ // V4L2_PIX_FMT_SQ905C , // v4l2_fourcc('9', '0', '5', 'C') /* compressed RGGB bayer */ // V4L2_PIX_FMT_PJPG , // v4l2_fourcc('P', 'J', 'P', 'G') /* Pixart 73xx JPEG */ // V4L2_PIX_FMT_OV511 , // v4l2_fourcc('O', '5', '1', '1') /* ov511 JPEG */ // V4L2_PIX_FMT_OV518 , // v4l2_fourcc('O', '5', '1', '8') /* ov518 JPEG */ // V4L2_PIX_FMT_STV0680 , // v4l2_fourcc('S', '6', '8', '0') /* stv0680 bayer */ // V4L2_PIX_FMT_TM6000 , // v4l2_fourcc('T', 'M', '6', '0') /* tm5600/tm60x0 */ // V4L2_PIX_FMT_CIT_YYVYUY , // v4l2_fourcc('C', 'I', 'T', 'V') /* one line of Y then 1 line of VYUY */ // V4L2_PIX_FMT_KONICA420 , // v4l2_fourcc('K', 'O', 'N', 'I') /* YUV420 planar in blocks of 256 pixels */ // V4L2_PIX_FMT_JPGL , // v4l2_fourcc('J', 'P', 'G', 'L') /* JPEG-Lite */ // V4L2_PIX_FMT_SE401 , // v4l2_fourcc('S', '4', '0', '1') /* se401 janggu compressed rgb */ // V4L2_PIX_FMT_S5C_UYVY_JPG // v4l2_fourcc('S', '5', 'C', 'I') /* S5C73M3 interleaved UYVY/JPEG */ for (std::map::iterator it = supported_formats.begin(); it != supported_formats.end(); ++it) vsuppformats.push_back(it->first); } libindi/libs/webcam/v4l2_decode/v4l2_builtin_decoder.h0000664000175000017500000000561213263645557022132 0ustar jasemjasem/* Copyright (C) 2005 by Jasem Mutlaq Copyright (C) 2014 by geehalel V4L2 Builtin Decoder 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 */ #pragma once #include "v4l2_decode.h" #include class V4L2_Builtin_Decoder : public V4L2_Decoder { struct format { unsigned int fourcc; // V4L2 format unsigned char bpp; // bytes per pixel implementation bool softcrop; // softcropping available format(unsigned int f, unsigned char b = 8, bool sc = false) : fourcc(f), bpp(b), softcrop(sc) {} }; public: V4L2_Builtin_Decoder(); virtual ~V4L2_Builtin_Decoder(); virtual void init(); virtual bool setcrop(struct v4l2_crop c); virtual void resetcrop(); virtual void usesoftcrop(bool c); virtual void setformat(struct v4l2_format f, bool use_ext_pix_format); virtual bool issupportedformat(unsigned int format); virtual const std::vector &getsupportedformats(); virtual void decode(unsigned char *frame, struct v4l2_buffer *buf); virtual unsigned char *getY(); virtual unsigned char *getU(); virtual unsigned char *getV(); //virtual unsigned char * getColorBuffer(); virtual unsigned char *getRGBBuffer(); virtual float *getLinearY(); virtual int getBpp(); virtual void setQuantization(bool); virtual void setLinearization(bool); protected: void init_supported_formats(); std::map supported_formats; std::vector vsuppformats; void allocBuffers(); void makeY(); void makeLinearY(); struct v4l2_crop crop; struct v4l2_format fmt; bool useSoftCrop; // uses software cropping bool doCrop; // do software cropping when decoding frames bool doQuantization; bool doLinearization; unsigned char *YBuf; unsigned char *UBuf; unsigned char *VBuf; unsigned char *yuvBuffer; unsigned char *yuyvBuffer; unsigned char *colorBuffer; unsigned char *rgb24_buffer; float *linearBuffer; //unsigned char *cropbuf; unsigned int bufwidth; unsigned int bufheight; char lut5[32]; char lut6[64]; unsigned char bpp; }; libindi/libs/webcam/v4l2_decode/v4l2_decode.h0000664000175000017500000000513013263645557020215 0ustar jasemjasem/* Copyright (C) 2005 by Jasem Mutlaq Copyright (C) 2014 by geehalel V4L2 Decode 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 */ #pragma once #include "indidevapi.h" #include #include #include #include class V4L2_Decoder { public: V4L2_Decoder(); virtual ~V4L2_Decoder(); virtual void init() = 0; virtual const char *getName(); virtual bool setcrop(struct v4l2_crop c) = 0; virtual void resetcrop() = 0; virtual void usesoftcrop(bool c) = 0; virtual void setformat(struct v4l2_format f, bool use_ext_pix_format) = 0; virtual bool issupportedformat(unsigned int format) = 0; virtual const std::vector &getsupportedformats() = 0; virtual void decode(unsigned char *frame, struct v4l2_buffer *buf) = 0; virtual unsigned char *getY() = 0; virtual unsigned char *getU() = 0; virtual unsigned char *getV() = 0; //virtual unsigned char * geColorBuffer()=0; virtual unsigned char *getRGBBuffer() = 0; virtual float *getLinearY() = 0; virtual int getBpp() = 0; virtual void setQuantization(bool) = 0; virtual void setLinearization(bool) = 0; protected: const char *name; }; class V4L2_Decode { public: V4L2_Decode(); ~V4L2_Decode(); std::vector getDecoderList(); V4L2_Decoder *getDecoder(); V4L2_Decoder *getDefaultDecoder(); void setDecoder(V4L2_Decoder *decoder); protected: std::vector decoder_list; V4L2_Decoder *current_decoder; V4L2_Decoder *default_decoder; }; libindi/libs/webcam/ccvt_misc.c0000664000175000017500000006652713263645557016017 0ustar jasemjasem/* CCVT: ColourConVerT: simple library for converting colourspaces Copyright (C) 2002 Nemosoft Unv. Email:athomas@nemsoft.co.uk This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program 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 General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA For questions, remarks, patches, etc. for this program, the author can be reached at nemosoft@smcc.demon.nl. */ /* This file contains CCVT functions that aren't available in assembly yet (or are not worth programming) */ /* * $Log$ * Revision 1.2 2005/04/29 16:51:20 mutlaqja * Adding initial support for Video 4 Linux 2 drivers. This mean that KStars can probably control Meade Lunar Planetary Imager (LPI). V4L2 requires a fairly recent kernel (> 2.6.9) and many drivers don't fully support it yet. It will take sometime. KStars still supports V4L1 and will continue so until V4L1 is obselete. Please test KStars video drivers if you can. Any comments welcomed. * * CCMAIL: kstars-devel@kde.org * * Revision 1.1 2004/06/26 23:12:03 mutlaqja * Hopefully this will fix compile issues on 64bit archs, and FreeBSD, among others. The assembly code is replaced with a more portable, albeit slower C implementation. I imported the videodev.h header after cleaning it for user space. * * Anyone who has problems compiling this, please report the problem to kstars-devel@kde.org * * I noticed one odd thing after updating my kdelibs, the LEDs don't change color when state is changed. Try that by starting any INDI device, and hit connect, if the LED turns to yellow and back to grey then it works fine, otherwise, we've got a problem. * * CCMAIL: kstars-devel@kde.org * * Revision 1.7 2003/01/02 04:10:19 nemosoft * Adding ''upside down" conversion to rgb/bgr routines * * Revision 1.6 2002/12/03 23:29:11 nemosoft * *** empty log message *** * * Revision 1.5 2002/12/03 23:27:41 nemosoft * fixing log messages (gcc 3.2 complaining) * Revision 1.4 2002/12/03 22:29:07 nemosoft Fixing up FTP stuff and some video Revision 1.3 2002/11/03 22:46:25 nemosoft Adding various RGB to RGB functions. Adding proper copyright header too. */ #include "ccvt.h" #include "ccvt_types.h" #include "indidevapi.h" #include "jpegutils.h" #include #include static float RGBYUV02990[256], RGBYUV05870[256], RGBYUV01140[256]; static float RGBYUV01684[256], RGBYUV03316[256]; static float RGBYUV04187[256], RGBYUV00813[256]; void InitLookupTable(void); /* YUYV: two Y's and one U/V */ void ccvt_yuyv_rgb32(int width, int height, const void *src, void *dst) { INDI_UNUSED(width); INDI_UNUSED(height); INDI_UNUSED(src); INDI_UNUSED(dst); } void ccvt_yuyv_bgr32(int width, int height, const void *src, void *dst) { const unsigned char *s; PIXTYPE_bgr32 *d; int l, c; int r, g, b, cr, cg, cb, y1, y2; l = height; s = src; d = dst; while (l--) { c = width >> 1; while (c--) { y1 = *s++; cb = ((*s - 128) * 454) >> 8; cg = (*s++ - 128) * 88; y2 = *s++; cr = ((*s - 128) * 359) >> 8; cg = (cg + (*s++ - 128) * 183) >> 8; r = y1 + cr; b = y1 + cb; g = y1 - cg; SAT(r); SAT(g); SAT(b); d->b = b; d->g = g; d->r = r; d++; r = y2 + cr; b = y2 + cb; g = y2 - cg; SAT(r); SAT(g); SAT(b); d->b = b; d->g = g; d->r = r; d++; } } } void ccvt_yuyv_bgr24(int width, int height, const void *src, void *dst) { const unsigned char *s; PIXTYPE_bgr24 *d; int l, c; int r, g, b, cr, cg, cb, y1, y2; l = height; s = src; d = dst; while (l--) { c = width >> 1; while (c--) { y1 = *s++; cb = ((*s - 128) * 454) >> 8; cg = (*s++ - 128) * 88; y2 = *s++; cr = ((*s - 128) * 359) >> 8; cg = (cg + (*s++ - 128) * 183) >> 8; r = y1 + cr; b = y1 + cb; g = y1 - cg; SAT(r); SAT(g); SAT(b); d->b = b; d->g = g; d->r = r; d++; r = y2 + cr; b = y2 + cb; g = y2 - cg; SAT(r); SAT(g); SAT(b); d->b = b; d->g = g; d->r = r; d++; } } } void ccvt_yuyv_rgb24(int width, int height, const void *src, void *dst) { const unsigned char *s; PIXTYPE_rgb24 *d; int l, c; int r, g, b, cr, cg, cb, y1, y2; l = height; s = src; d = dst; while (l--) { c = width >> 1; while (c--) { y1 = *s++; cb = ((*s - 128) * 454) >> 8; cg = (*s++ - 128) * 88; y2 = *s++; cr = ((*s - 128) * 359) >> 8; cg = (cg + (*s++ - 128) * 183) >> 8; r = y1 + cr; b = y1 + cb; g = y1 - cg; SAT(r); SAT(g); SAT(b); d->r = r; d->g = g; d->b = b; d++; r = y2 + cr; b = y2 + cb; g = y2 - cg; SAT(r); SAT(g); SAT(b); d->r = r; d->g = g; d->b = b; d++; } } } void ccvt_yuyv_420p(int width, int height, const void *src, void *dsty, void *dstu, void *dstv) { int n, l, j; const unsigned char *s1, *s2; unsigned char *dy, *du, *dv; dy = (unsigned char *)dsty; du = (unsigned char *)dstu; dv = (unsigned char *)dstv; s1 = (unsigned char *)src; s2 = s1; /* keep pointer */ n = width * height; for (; n > 0; n--) { *dy = *s1; dy++; s1 += 2; } /* Two options here: average U/V values, or skip every second row */ s1 = s2; /* restore pointer */ s1++; /* point to U */ for (l = 0; l < height; l += 2) { s2 = s1 + width * 2; /* odd line */ for (j = 0; j < width; j += 2) { *du = (*s1 + *s2) / 2; du++; s1 += 2; s2 += 2; *dv = (*s1 + *s2) / 2; dv++; s1 += 2; s2 += 2; } s1 = s2; } } void bayer2rgb24(unsigned char *dst, unsigned char *src, long int WIDTH, long int HEIGHT) { long int i; unsigned char *rawpt, *scanpt; long int size; rawpt = src; scanpt = dst; size = WIDTH * HEIGHT; for (i = 0; i < size; i++) { if ((i / WIDTH) % 2 == 0) { if ((i % 2) == 0) { /* B */ if ((i > WIDTH) && ((i % WIDTH) > 0)) { *scanpt++ = (*(rawpt - WIDTH - 1) + *(rawpt - WIDTH + 1) + *(rawpt + WIDTH - 1) + *(rawpt + WIDTH + 1)) / 4; /* R */ *scanpt++ = (*(rawpt - 1) + *(rawpt + 1) + *(rawpt + WIDTH) + *(rawpt - WIDTH)) / 4; /* G */ *scanpt++ = *rawpt; /* B */ } else { /* first line or left column */ *scanpt++ = *(rawpt + WIDTH + 1); /* R */ *scanpt++ = (*(rawpt + 1) + *(rawpt + WIDTH)) / 2; /* G */ *scanpt++ = *rawpt; /* B */ } } else { /* (B)G */ if ((i > WIDTH) && ((i % WIDTH) < (WIDTH - 1))) { *scanpt++ = (*(rawpt + WIDTH) + *(rawpt - WIDTH)) / 2; /* R */ *scanpt++ = *rawpt; /* G */ *scanpt++ = (*(rawpt - 1) + *(rawpt + 1)) / 2; /* B */ } else { /* first line or right column */ *scanpt++ = *(rawpt + WIDTH); /* R */ *scanpt++ = *rawpt; /* G */ *scanpt++ = *(rawpt - 1); /* B */ } } } else { if ((i % 2) == 0) { /* G(R) */ if ((i < (WIDTH * (HEIGHT - 1))) && ((i % WIDTH) > 0)) { *scanpt++ = (*(rawpt - 1) + *(rawpt + 1)) / 2; /* R */ *scanpt++ = *rawpt; /* G */ *scanpt++ = (*(rawpt + WIDTH) + *(rawpt - WIDTH)) / 2; /* B */ } else { /* bottom line or left column */ *scanpt++ = *(rawpt + 1); /* R */ *scanpt++ = *rawpt; /* G */ *scanpt++ = *(rawpt - WIDTH); /* B */ } } else { /* R */ if (i < (WIDTH * (HEIGHT - 1)) && ((i % WIDTH) < (WIDTH - 1))) { *scanpt++ = *rawpt; /* R */ *scanpt++ = (*(rawpt - 1) + *(rawpt + 1) + *(rawpt - WIDTH) + *(rawpt + WIDTH)) / 4; /* G */ *scanpt++ = (*(rawpt - WIDTH - 1) + *(rawpt - WIDTH + 1) + *(rawpt + WIDTH - 1) + *(rawpt + WIDTH + 1)) / 4; /* B */ } else { /* bottom line or right column */ *scanpt++ = *rawpt; /* R */ *scanpt++ = (*(rawpt - 1) + *(rawpt - WIDTH)) / 2; /* G */ *scanpt++ = *(rawpt - WIDTH - 1); /* B */ } } } rawpt++; } } void bayer16_2_rgb24(unsigned short *dst, unsigned short *src, long int WIDTH, long int HEIGHT) { long int i; unsigned short *rawpt, *scanpt; long int size; rawpt = src; scanpt = dst; size = WIDTH * HEIGHT; for (i = 0; i < size; i++) { if ((i / WIDTH) % 2 == 0) { if ((i % 2) == 0) { /* B */ if ((i > WIDTH) && ((i % WIDTH) > 0)) { *scanpt++ = (*(rawpt - WIDTH - 1) + *(rawpt - WIDTH + 1) + *(rawpt + WIDTH - 1) + *(rawpt + WIDTH + 1)) / 4; /* R */ *scanpt++ = (*(rawpt - 1) + *(rawpt + 1) + *(rawpt + WIDTH) + *(rawpt - WIDTH)) / 4; /* G */ *scanpt++ = *rawpt; /* B */ } else { /* first line or left column */ *scanpt++ = *(rawpt + WIDTH + 1); /* R */ *scanpt++ = (*(rawpt + 1) + *(rawpt + WIDTH)) / 2; /* G */ *scanpt++ = *rawpt; /* B */ } } else { /* (B)G */ if ((i > WIDTH) && ((i % WIDTH) < (WIDTH - 1))) { *scanpt++ = (*(rawpt + WIDTH) + *(rawpt - WIDTH)) / 2; /* R */ *scanpt++ = *rawpt; /* G */ *scanpt++ = (*(rawpt - 1) + *(rawpt + 1)) / 2; /* B */ } else { /* first line or right column */ *scanpt++ = *(rawpt + WIDTH); /* R */ *scanpt++ = *rawpt; /* G */ *scanpt++ = *(rawpt - 1); /* B */ } } } else { if ((i % 2) == 0) { /* G(R) */ if ((i < (WIDTH * (HEIGHT - 1))) && ((i % WIDTH) > 0)) { *scanpt++ = (*(rawpt - 1) + *(rawpt + 1)) / 2; /* R */ *scanpt++ = *rawpt; /* G */ *scanpt++ = (*(rawpt + WIDTH) + *(rawpt - WIDTH)) / 2; /* B */ } else { /* bottom line or left column */ *scanpt++ = *(rawpt + 1); /* R */ *scanpt++ = *rawpt; /* G */ *scanpt++ = *(rawpt - WIDTH); /* B */ } } else { /* R */ if (i < (WIDTH * (HEIGHT - 1)) && ((i % WIDTH) < (WIDTH - 1))) { *scanpt++ = *rawpt; /* R */ *scanpt++ = (*(rawpt - 1) + *(rawpt + 1) + *(rawpt - WIDTH) + *(rawpt + WIDTH)) / 4; /* G */ *scanpt++ = (*(rawpt - WIDTH - 1) + *(rawpt - WIDTH + 1) + *(rawpt + WIDTH - 1) + *(rawpt + WIDTH + 1)) / 4; /* B */ } else { /* bottom line or right column */ *scanpt++ = *rawpt; /* R */ *scanpt++ = (*(rawpt - 1) + *(rawpt - WIDTH)) / 2; /* G */ *scanpt++ = *(rawpt - WIDTH - 1); /* B */ } } } rawpt++; } } void bayer_rggb_2rgb24(unsigned char *dst, unsigned char *src, long int WIDTH, long int HEIGHT) { long int i; unsigned char *rawpt, *scanpt; long int size; rawpt = src; scanpt = dst; size = WIDTH * HEIGHT; for (i = 0; i < size; i++) { if ((i / WIDTH) % 2 == 0) //wenn zeile grade { if ((i % 2) == 0) //spalte gerade { /* B */ if ((i > WIDTH) && ((i % WIDTH) > 0)) // wenn nicht erste zeile oder linke spalte { *scanpt++ = *rawpt; /* R */ *scanpt++ = (*(rawpt - 1) + *(rawpt + 1) + *(rawpt + WIDTH) + *(rawpt - WIDTH)) / 4; /* G */ *scanpt++ = (*(rawpt - WIDTH - 1) + *(rawpt - WIDTH + 1) + *(rawpt + WIDTH - 1) + *(rawpt + WIDTH + 1)) / 4; /* B */ } else { /* first line or left column */ *scanpt++ = *rawpt; /* R */ *scanpt++ = (*(rawpt + 1) + *(rawpt + WIDTH)) / 2; /* G */ *scanpt++ = *(rawpt + WIDTH + 1); /* B */ } } else { /* (B)G */ if ((i > WIDTH) && ((i % WIDTH) < (WIDTH - 1))) { *scanpt++ = (*(rawpt - 1) + *(rawpt + 1)) / 2; /* R */ *scanpt++ = *rawpt; /* G */ *scanpt++ = (*(rawpt + WIDTH) + *(rawpt - WIDTH)) / 2; /* B */ } else { /* first line or right column */ *scanpt++ = *(rawpt - 1); /* R */ *scanpt++ = *rawpt; /* G */ *scanpt++ = *(rawpt + WIDTH); /* B */ } } } else { if ((i % 2) == 0) { /* G(R) */ if ((i < (WIDTH * (HEIGHT - 1))) && ((i % WIDTH) > 0)) { *scanpt++ = (*(rawpt + WIDTH) + *(rawpt - WIDTH)) / 2; /* R */ *scanpt++ = *rawpt; /* G */ *scanpt++ = (*(rawpt - 1) + *(rawpt + 1)) / 2; /* B */ } else { /* bottom line or left column */ *scanpt++ = *(rawpt - WIDTH); /* R */ *scanpt++ = *rawpt; /* G */ *scanpt++ = *(rawpt + 1); /* B */ } } else { /* R */ if (i < (WIDTH * (HEIGHT - 1)) && ((i % WIDTH) < (WIDTH - 1))) { *scanpt++ = (*(rawpt - WIDTH - 1) + *(rawpt - WIDTH + 1) + *(rawpt + WIDTH - 1) + *(rawpt + WIDTH + 1)) / 4; /* R */ *scanpt++ = (*(rawpt - 1) + *(rawpt + 1) + *(rawpt - WIDTH) + *(rawpt + WIDTH)) / 4; /* G */ *scanpt++ = *rawpt; /* B */ } else { /* bottom line or right column */ *scanpt++ = *(rawpt - WIDTH - 1); /* R */ *scanpt++ = (*(rawpt - 1) + *(rawpt - WIDTH)) / 2; /* G */ *scanpt++ = *rawpt; /* B */ } } } rawpt++; } } int mjpegtoyuv420p(unsigned char *map, unsigned char *cap_map, int width, int height, unsigned int size) { unsigned char *yuv[3]; unsigned char *y, *u, *v; int loop, ret; yuv[0] = malloc(width * height * sizeof(yuv[0][0])); yuv[1] = malloc(width * height / 4 * sizeof(yuv[1][0])); yuv[2] = malloc(width * height / 4 * sizeof(yuv[2][0])); ret = decode_jpeg_raw(cap_map, size, 0, 420, width, height, yuv[0], yuv[1], yuv[2]); y = map; u = y + width * height; v = u + (width * height) / 4; memset(y, 0, width * height); memset(u, 0, width * height / 4); memset(v, 0, width * height / 4); for (loop = 0; loop < width * height; loop++) *map++ = yuv[0][loop]; for (loop = 0; loop < width * height / 4; loop++) *map++ = yuv[1][loop]; for (loop = 0; loop < width * height / 4; loop++) *map++ = yuv[2][loop]; free(yuv[0]); free(yuv[1]); free(yuv[2]); return ret; } /************************************************************************ * * int RGB2YUV (int x_dim, int y_dim, void *bmp, YUV *yuv) * * Purpose : It takes a 24-bit RGB bitmap and convert it into * YUV (4:2:0) format * * Input : x_dim the x dimension of the bitmap * y_dim the y dimension of the bitmap * bmp pointer to the buffer of the bitmap * yuv pointer to the YUV structure * * Output : 0 OK * 1 wrong dimension * 2 memory allocation error * * Side Effect : * None * * Date : 09/28/2000 * * Contacts: * * Adam Li * * DivX Advance Research Center * ************************************************************************/ int RGB2YUV(int x_dim, int y_dim, void *bmp, void *y_out, void *u_out, void *v_out, int flip) { static int init_done = 0; long i, j, size; unsigned char *r, *g, *b; unsigned char *y, *u, *v; unsigned char *pu1, *pu2, *pv1, *pv2, *psu, *psv; unsigned char *y_buffer, *u_buffer, *v_buffer; unsigned char *sub_u_buf, *sub_v_buf; if (init_done == 0) { InitLookupTable(); init_done = 1; } /* check to see if x_dim and y_dim are divisible by 2*/ if ((x_dim % 2) || (y_dim % 2)) return 1; size = x_dim * y_dim; /* allocate memory*/ y_buffer = (unsigned char *)y_out; sub_u_buf = (unsigned char *)u_out; sub_v_buf = (unsigned char *)v_out; u_buffer = (unsigned char *)malloc(size * sizeof(unsigned char)); v_buffer = (unsigned char *)malloc(size * sizeof(unsigned char)); if (!(u_buffer && v_buffer)) { if (u_buffer) free(u_buffer); if (v_buffer) free(v_buffer); return 2; } b = (unsigned char *)bmp; y = y_buffer; u = u_buffer; v = v_buffer; /* convert RGB to YUV*/ if (!flip) { for (j = 0; j < y_dim; j++) { y = y_buffer + (y_dim - j - 1) * x_dim; u = u_buffer + (y_dim - j - 1) * x_dim; v = v_buffer + (y_dim - j - 1) * x_dim; for (i = 0; i < x_dim; i++) { g = b + 1; r = b + 2; *y = (unsigned char)(RGBYUV02990[*r] + RGBYUV05870[*g] + RGBYUV01140[*b]); *u = (unsigned char)(-RGBYUV01684[*r] - RGBYUV03316[*g] + (*b) / 2 + 128); *v = (unsigned char)((*r) / 2 - RGBYUV04187[*g] - RGBYUV00813[*b] + 128); b += 3; y++; u++; v++; } } } else { for (i = 0; i < size; i++) { g = b + 1; r = b + 2; *y = (unsigned char)(RGBYUV02990[*r] + RGBYUV05870[*g] + RGBYUV01140[*b]); *u = (unsigned char)(-RGBYUV01684[*r] - RGBYUV03316[*g] + (*b) / 2 + 128); *v = (unsigned char)((*r) / 2 - RGBYUV04187[*g] - RGBYUV00813[*b] + 128); b += 3; y++; u++; v++; } } /* subsample UV*/ for (j = 0; j < y_dim / 2; j++) { psu = sub_u_buf + j * x_dim / 2; psv = sub_v_buf + j * x_dim / 2; pu1 = u_buffer + 2 * j * x_dim; pu2 = u_buffer + (2 * j + 1) * x_dim; pv1 = v_buffer + 2 * j * x_dim; pv2 = v_buffer + (2 * j + 1) * x_dim; for (i = 0; i < x_dim / 2; i++) { *psu = (*pu1 + *(pu1 + 1) + *pu2 + *(pu2 + 1)) / 4; *psv = (*pv1 + *(pv1 + 1) + *pv2 + *(pv2 + 1)) / 4; psu++; psv++; pu1 += 2; pu2 += 2; pv1 += 2; pv2 += 2; } } free(u_buffer); free(v_buffer); return 0; } void InitLookupTable() { int i; for (i = 0; i < 256; i++) RGBYUV02990[i] = (float)0.2990 * i; for (i = 0; i < 256; i++) RGBYUV05870[i] = (float)0.5870 * i; for (i = 0; i < 256; i++) RGBYUV01140[i] = (float)0.1140 * i; for (i = 0; i < 256; i++) RGBYUV01684[i] = (float)0.1684 * i; for (i = 0; i < 256; i++) RGBYUV03316[i] = (float)0.3316 * i; for (i = 0; i < 256; i++) RGBYUV04187[i] = (float)0.4187 * i; for (i = 0; i < 256; i++) RGBYUV00813[i] = (float)0.0813 * i; } /* RGB/BGR to RGB/BGR */ #define RGBBGR_BODY24(TIN, TOUT) \ void ccvt_##TIN##_##TOUT(int width, int height, const void *const src, void *dst) \ { \ const PIXTYPE_##TIN *in = src; \ PIXTYPE_##TOUT *out = dst; \ int l, c, stride = 0; \ \ stride = width; \ out += ((height - 1) * width); \ stride *= 2; \ for (l = 0; l < height; l++) \ { \ for (c = 0; c < width; c++) \ { \ out->r = in->r; \ out->g = in->g; \ out->b = in->b; \ in++; \ out++; \ } \ out -= stride; \ } \ } #define RGBBGR_BODY32(TIN, TOUT) \ void ccvt_##TIN##_##TOUT(int width, int height, const void *const src, void *dst) \ { \ const PIXTYPE_##TIN *in = src; \ PIXTYPE_##TOUT *out = dst; \ int l, c, stride = 0; \ \ stride = width; \ out += ((height - 1) * width); \ stride *= 2; \ for (l = 0; l < height; l++) \ { \ for (c = 0; c < width; c++) \ { \ out->r = in->r; \ out->g = in->g; \ out->b = in->b; \ out->z = 0; \ in++; \ out++; \ } \ out -= stride; \ } \ } RGBBGR_BODY32(bgr24, bgr32) RGBBGR_BODY32(bgr24, rgb32) RGBBGR_BODY32(rgb24, bgr32) RGBBGR_BODY32(rgb24, rgb32) RGBBGR_BODY24(bgr32, bgr24) RGBBGR_BODY24(bgr32, rgb24) RGBBGR_BODY24(rgb32, bgr24) RGBBGR_BODY24(rgb32, rgb24) libindi/libs/webcam/vcvt.h0000664000175000017500000000450413263645557015017 0ustar jasemjasem/* (C) 2001 Nemosoft Unv. This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program 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 General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ /* 'Viewport' conversion routines. These functions convert from one colour space to another, taking into account that the source image has a smaller size than the view, and is placed inside the view: +-------view.x------------+ | | | +---image.x---+ | | | | | | | | | | +-------------+ | | | +-------------------------+ The image should always be smaller than the view. The offset (top-left corner of the image) should be precomputed, so you can place the image anywhere in the view. The functions take these parameters: - width image width (in pixels) - height image height (in pixels) - plus view width (in pixels) *src pointer at start of image *dst pointer at offset (!) in view */ #pragma once #ifdef __cplusplus extern "C" { #endif /* Functions in vcvt_i386.S/vcvt_c.c */ /* 4:2:0 YUV interlaced to RGB/BGR */ void vcvt_420i_bgr24(int width, int height, int plus, void *src, void *dst); void vcvt_420i_rgb24(int width, int height, int plus, void *src, void *dst); void vcvt_420i_bgr32(int width, int height, int plus, void *src, void *dst); void vcvt_420i_rgb32(int width, int height, int plus, void *src, void *dst); /* Go from 420i to other yuv formats */ void vcvt_420i_420p(int width, int height, int plus, void *src, void *dsty, void *dstu, void *dstv); void vcvt_420i_yuyv(int width, int height, int plus, void *src, void *dst); #if 0 #endif #ifdef __cplusplus } #endif libindi/libs/webcam/v4l2_base.cpp0000664000175000017500000033572013263645557016160 0ustar jasemjasem/* Copyright (C) 2005 by Jasem Mutlaq Based on V4L 2 Example http://v4l2spec.bytesex.org/spec-single/v4l2.html#CAPTURE-EXAMPLE 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 */ #include "v4l2_base.h" #include "ccvt.h" #include "eventloop.h" #include "indidevapi.h" #include "indilogger.h" #include "lilxml.h" // PWC framerate support #include "pwc-ioctl.h" #include #include #include #include #include #include #include #include #include #include #include #include /* for videodev2.h */ #include #include #include /* Kernel headers version */ #include #define ERRMSGSIZ 1024 #define CLEAR(x) memset(&(x), 0, sizeof(x)) #define XIOCTL(fd, ioctl, arg) this->xioctl(fd, ioctl, arg, #ioctl) #define DBG_STR_PIX "%c%c%c%c" #define DBG_PIX(pf) ((pf) >> 0) & 0xFF, ((pf) >> 8) & 0xFF, ((pf) >> 16) & 0xFF, ((pf) >> 24) & 0xFF /* TODO: Before 3.17, the only way to determine a format is compressed is to * consolidate a matrix with v4l2_pix_format::pixelformat and v4l2_fourcc * values. After 3.17, field 'flags' in v4l2_pix_format is assumed to be * properly filled. For now we rely on 'flags', but we could just check the * most used pixel formats in a CCD whitelist (YUVx, RGBxxx...). */ #if (LINUX_VERSION_CODE >= KERNEL_VERSION(3, 17, 0)) #define DBG_STR_FLAGS "...%c .%c%c%c %c%c.%c .%c%c%c %c%c%c%c" #define DBG_FLAGS(b) \ /* 0x00010000 */ ((b).flags & V4L2_BUF_FLAG_TSTAMP_SRC_SOE) ? 'S' : 'E', \ /* 0x00004000 */ ((b).flags & V4L2_BUF_FLAG_TIMESTAMP_COPY) ? 'c' : '.', \ /* 0x00002000 */ ((b).flags & V4L2_BUF_FLAG_TIMESTAMP_MONOTONIC) ? 'm' : '.', \ /* 0x00001000 */ ((b).flags & V4L2_BUF_FLAG_NO_CACHE_CLEAN) ? 'C' : '.', \ /* 0x00000800 */ ((b).flags & V4L2_BUF_FLAG_NO_CACHE_INVALIDATE) ? 'I' : '.', \ /* 0x00000400 */ ((b).flags & V4L2_BUF_FLAG_PREPARED) ? 'p' : '.', \ /* 0x00000100 */ ((b).flags & V4L2_BUF_FLAG_TIMECODE) ? 'T' : '.', \ /* 0x00000040 */ ((b).flags & V4L2_BUF_FLAG_ERROR) ? 'E' : '.', \ /* 0x00000020 */ ((b).flags & V4L2_BUF_FLAG_BFRAME) ? 'B' : '.', \ /* 0x00000010 */ ((b).flags & V4L2_BUF_FLAG_PFRAME) ? 'P' : '.', \ /* 0x00000008 */ ((b).flags & V4L2_BUF_FLAG_KEYFRAME) ? 'K' : '.', \ /* 0x00000004 */ ((b).flags & V4L2_BUF_FLAG_DONE) ? 'd' : '.', \ /* 0x00000002 */ ((b).flags & V4L2_BUF_FLAG_QUEUED) ? 'q' : '.', \ /* 0x00000001 */ ((b).flags & V4L2_BUF_FLAG_MAPPED) ? 'm' : '.' #else #define DBG_STR_FLAGS "%s" #define DBG_FLAGS(b) "" #endif #if (LINUX_VERSION_CODE >= KERNEL_VERSION(3, 17, 0)) #define DBG_STR_FMT "%ux%u " DBG_STR_PIX " %scompressed (%ssupported)" #define DBG_FMT(f) \ (f).fmt.pix.width, (f).fmt.pix.height, DBG_PIX((f).fmt.pix.pixelformat), \ ((f).fmt.pix.flags & V4L2_FMT_FLAG_COMPRESSED) ? "" : "un", \ (decoder->issupportedformat((f).fmt.pix.pixelformat) ? "" : "un") #else #define DBG_STR_FMT "%ux%u " DBG_STR_PIX " (%ssupported)" #define DBG_FMT(f) \ (f).fmt.pix.width, (f).fmt.pix.height, DBG_PIX((f).fmt.pix.pixelformat), \ (decoder->issupportedformat((f).fmt.pix.pixelformat) ? "" : "un") #endif #define DBG_STR_BUF "#%d " DBG_STR_FLAGS " % 7d bytes %4.4s seq %d:%d stamp %ld.%06ld" #define DBG_BUF(b) \ (b).index, DBG_FLAGS(b), (b).bytesused, \ ((b).memory == V4L2_MEMORY_MMAP) ? \ "mmap" : \ ((b).memory == V4L2_MEMORY_USERPTR) ? "uptr" : \ ((b).memory == 4 /* kernel 3.8.0: V4L2_MEMORY_DMABUF */) ? \ "dma" : \ ((b).memory == V4L2_MEMORY_OVERLAY) ? "over" : "", \ (b).sequence, (b).field, (b).timestamp.tv_sec, (b).timestamp.tv_usec using namespace std; /*char *entityXML(char *src) { char *s = src; while (*s) { if ((*s == '&') || (*s == '<') || (*s == '>') || (*s == '\'') || (*s == '"')) *s = '_'; s += 1; } return src; }*/ namespace INDI { V4L2_Base::V4L2_Base() { frameRate.numerator = 1; frameRate.denominator = 25; selectCallBackID = -1; //dropFrameCount = 1; //dropFrame = 0; xmax = xmin = 160; ymax = ymin = 120; io = IO_METHOD_MMAP; fd = -1; buffers = nullptr; n_buffers = 0; callback = nullptr; cancrop = true; cansetrate = true; streamedonce = false; v4l2_decode = new V4L2_Decode(); decoder = v4l2_decode->getDefaultDecoder(); decoder->init(); dodecode = true; bpp = 8; has_ext_pix_format = false; const std::vector &vsuppformats = decoder->getsupportedformats(); DEBUGFDEVICE(deviceName, INDI::Logger::DBG_DEBUG, "Using default decoder '%s'\n Supported V4L2 formats are:", decoder->getName()); for (std::vector::const_iterator it = vsuppformats.begin(); it != vsuppformats.end(); ++it) DEBUGFDEVICE(deviceName, INDI::Logger::DBG_DEBUG, "%c%c%c%c ", (*it >> 0), (*it >> 8), (*it >> 16), (*it >> 24)); //DEBUGFDEVICE(deviceName, INDI::Logger::DBG_DEBUG,INDI::Logger::DBG_SESSION,"Default decoder: %s", decoder->getName()); getframerate = nullptr; setframerate = nullptr; reallocate_buffers = false; path = nullptr; uptr = nullptr; lxstate = LX_ACTIVE; streamactive = false; cropset = false; } V4L2_Base::~V4L2_Base() { delete v4l2_decode; } /** @brief Helper indicating whether current pixel format is compressed or not. * * This function is used in read_frame to check for corrupted frames. * * @return true if pixel format is considered compressed by the driver, else * false. * * @warning If kernel headers 3.17 or later are available, this function will * rely on field 'flags', else will compare the current pixel format against an * arbitrary list of known format codes. */ bool V4L2_Base::is_compressed() const { /* See note at top of this file */ #if (LINUX_VERSION_CODE >= KERNEL_VERSION(3, 17, 0)) switch (fmt.fmt.pix.pixelformat) { case V4L2_PIX_FMT_JPEG: case V4L2_PIX_FMT_MJPEG: DEBUGFDEVICE(deviceName, INDI::Logger::DBG_DEBUG, "%s: format %c%c%c%c patched to be considered compressed", __FUNCTION__, fmt.fmt.pix.pixelformat, fmt.fmt.pix.pixelformat >> 8, fmt.fmt.pix.pixelformat >> 16, fmt.fmt.pix.pixelformat >> 24); return true; default: DEBUGFDEVICE(deviceName, INDI::Logger::DBG_DEBUG, "%s: format %c%c%c%c has compressed flag %d", __FUNCTION__, fmt.fmt.pix.pixelformat, fmt.fmt.pix.pixelformat >> 8, fmt.fmt.pix.pixelformat >> 16, fmt.fmt.pix.pixelformat >> 24, fmt.fmt.pix.flags & V4L2_FMT_FLAG_COMPRESSED); return fmt.fmt.pix.flags & V4L2_FMT_FLAG_COMPRESSED; } #else switch (fmt.fmt.pix.pixelformat) { case V4L2_PIX_FMT_GREY: /* case V4L2_PIX_FMT... add other uncompressed and supported formats here */ return false; default: return true; } #endif } /** @internal Helper for ioctl calls, with logging facility. * * This function is called by internal macro XIOCTL. * * @param fd is the file descriptor against which to run the ioctl. * @param request is the name of the ioctl to run. * @param arg is the argument structure to pass to the ioctl. * @param request_str is the stringified name of the request for debug prints. * @return the result of the ioctl. * @note This function takes care of EINTR while running the ioctl. */ int V4L2_Base::xioctl(int fd, int request, void *arg, char const *const request_str) { int r = -1; do { r = ioctl(fd, request, arg); } while (-1 == r && EINTR == errno); if (-1 == r) DEBUGFDEVICE(deviceName, INDI::Logger::DBG_DEBUG, "%s: ioctl 0x%08X/%s received errno %d (%s)", __FUNCTION__, request, request_str, errno, strerror(errno)); return r; } /* @internal Setting a V4L2 format through ioctl VIDIOC_S_FMT * * If the format type is non-zero, this function executes ioctl * VIDIOC_S_FMT on the argument data format, and updates the instance * data format on success. If an error arises, the instance data * format is left unmodified. * * If the format type is zero, this function executes ioctl * VIDIOC_G_FMT on a temporary data format, and updates the instance * data format on success. If an error arises, the instance data * format is left unmodified. * * @warning If the format type is non-zero and the device streamed * at least once before the call, the device is closed and reopened * before updating the format. * * @warning If successful, this function updates the instance data * format 'fmt'. * * @note The frame decoder format is updated with the resulting format, * and the instance depth field 'bpp' is updated with the resulting * frame decoder depth. * * @param new_fmt is the v4l2_format to set, eventually with type set * to zero to refresh the instance format with the current device format. * @return 0 if ioctl is successful, or -1 with error message updated. */ int V4L2_Base::ioctl_set_format(struct v4l2_format new_fmt, char *errmsg) { /* Reopen device if it streamed at least once and we want to update the format*/ if (streamedonce && new_fmt.type) { close_device(); if (open_device(path, errmsg)) { DEBUGFDEVICE(deviceName, INDI::Logger::DBG_DEBUG, "%s: failed reopening device %s (%s)", __FUNCTION__, path, errmsg); return -1; } } /* Trying format with VIDIOC_TRY_FMT has no interesting advantage here */ if (false) { if (-1 == XIOCTL(fd, VIDIOC_TRY_FMT, &new_fmt)) { DEBUGFDEVICE(deviceName, INDI::Logger::DBG_DEBUG, "%s: failed VIDIOC_TRY_FMT with " DBG_STR_FMT, __FUNCTION__, DBG_FMT(new_fmt)); return errno_exit("VIDIOC_TRY_FMT", errmsg); } } if (new_fmt.type) { /* Set format */ if (-1 == XIOCTL(fd, VIDIOC_S_FMT, &new_fmt)) { DEBUGFDEVICE(deviceName, INDI::Logger::DBG_DEBUG, "%s: failed VIDIOC_S_FMT with " DBG_STR_FMT, __FUNCTION__, DBG_FMT(new_fmt)); return errno_exit("VIDIOC_S_FMT", errmsg); } } else { /* Retrieve format */ new_fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; if (-1 == XIOCTL(fd, VIDIOC_G_FMT, &new_fmt)) { DEBUGFDEVICE(deviceName, INDI::Logger::DBG_DEBUG, "%s: failed VIDIOC_G_FMT", __FUNCTION__); return errno_exit("VIDIOC_G_FMT", errmsg); } } DEBUGFDEVICE(deviceName, INDI::Logger::DBG_DEBUG, "%s: current format " DBG_STR_FMT, __FUNCTION__, DBG_FMT(new_fmt)); /* Update internals */ decoder->setformat(new_fmt, has_ext_pix_format); this->bpp = decoder->getBpp(); /* Assign the format as current */ fmt = new_fmt; return 0; } int V4L2_Base::errno_exit(const char *s, char *errmsg) { fprintf(stderr, "%s error %d, %s\n", s, errno, strerror(errno)); snprintf(errmsg, ERRMSGSIZ, "%s error %d, %s\n", s, errno, strerror(errno)); if (streamactive) stop_capturing(errmsg); return -1; } void V4L2_Base::doDecode(bool d) { dodecode = d; } int V4L2_Base::connectCam(const char *devpath, char *errmsg, int pixelFormat, int width, int height) { INDI_UNUSED(pixelFormat); INDI_UNUSED(width); INDI_UNUSED(height); selectCallBackID = -1; cancrop = true; cansetrate = true; streamedonce = false; frameRate.numerator = 1; frameRate.denominator = 25; if (open_device(devpath, errmsg) < 0) return -1; path = devpath; if (check_device(errmsg) < 0) return -1; //cerr << "V4L2 Check: All successful, returning\n"; return fd; } void V4L2_Base::disconnectCam(bool stopcapture) { char errmsg[ERRMSGSIZ]; if (selectCallBackID != -1) rmCallback(selectCallBackID); if (stopcapture) stop_capturing(errmsg); //uninit_device (errmsg); close_device(); //fprintf(stderr, "Disconnect cam\n"); } bool V4L2_Base::isLXmodCapable() { if (!(strcmp((const char *)cap.driver, "pwc"))) return true; else return false; } /* @internal Calculate epoch time shift * * The clock CLOCK_MONOTONIC starts counting from an undefined origin (boot time * for instance). This function computes the offset between the current time returned * by gettimeofday and the monotonic time returned by clock_gettime in milliseconds. * This value can then be used to determine the time and date of frames from their * timestamp as returned by the kernel. * * Code provided at: * http://stackoverflow.com/questions/10266451/where-does-v4l2-buffer-timestamp-value-starts-counting * * @return the milliseconds offset to apply to the timestamp returned by gettimeofday for * it to have the same reference as clock_gettime. */ //static long getEpochTimeShift() //{ // struct timeval epochtime = { 0, 0 }; // struct timespec vsTime = { 0, 0 }; // // gettimeofday(&epochtime, nullptr); // clock_gettime(CLOCK_MONOTONIC, &vsTime); // // long const uptime_ms = vsTime.tv_sec * 1000 + (long)round(vsTime.tv_nsec / 1000000.0); // long const epoch_ms = epochtime.tv_sec * 1000 + (long)round(epochtime.tv_usec / 1000.0); // // long const epoch_shift = epoch_ms - uptime_ms; // //DEBUGFDEVICE(deviceName, INDI::Logger::DBG_DEBUG,"%s: epoch shift is %ld",__FUNCTION__,epoch_shift); // // return epoch_shift; //} /* @brief Reading a frame from the V4L2 driver. * * This function will attempt to read a frame with the adequate * method for the device, and forward the frame read to the configured * decoder and/or recorder. * * With the MMAP method, the first available buffer is dequeued to read * the embedded frame. If the frame is marked erroneous by the driver, or * the frame is known to be uncompressed but its length doesn't match the * expected size, the buffer is re-enqueued immediately. * * Although only the MMAP method is actually supported, two other methods * are also implemented: * - With the READ method, the frame is read directly from the device * descriptor, using the first buffer characteristics are address and * length. But no processing is done actually. * - With the USERPTR method, the first available buffer is dequeued, then * verified against the buffer list. No processing is done actually, and * the buffer is immediately requeued. * * @param errmsg is the error messsage updated in case of error. * @return 0 if frame read is processed, or -1 with error message updated. */ int V4L2_Base::read_frame(char *errmsg) { unsigned int i; //cerr << "in read Frame" << endl; switch (io) { case IO_METHOD_READ: cerr << "in read Frame method read" << endl; if (-1 == read(fd, buffers[0].start, buffers[0].length)) { switch (errno) { case EAGAIN: return 0; case EIO: /* Could ignore EIO, see spec. */ /* fall through */ default: return errno_exit("read", errmsg); } } //process_image (buffers[0].start); break; case IO_METHOD_MMAP: DEBUGFDEVICE(deviceName, INDI::Logger::DBG_DEBUG, "%s: using MMAP to recover frame buffer", __FUNCTION__); CLEAR(buf); buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; buf.memory = V4L2_MEMORY_MMAP; /* For debugging purposes */ if (false) { for (i = 0; i < n_buffers; ++i) { buf.index = i; if (-1 == XIOCTL(fd, VIDIOC_QUERYBUF, &buf)) switch (errno) { case EINVAL: DEBUGFDEVICE(deviceName, INDI::Logger::DBG_DEBUG, "%s: invalid buffer query, doing as if buffer was in output queue", __FUNCTION__); break; default: return errno_exit("ReadFrame IO_METHOD_MMAP: VIDIOC_QUERYBUF", errmsg); } DEBUGFDEVICE(deviceName, INDI::Logger::DBG_DEBUG, "%s: " DBG_STR_BUF, __FUNCTION__, DBG_BUF(buf)); } } if (-1 == XIOCTL(fd, VIDIOC_DQBUF, &buf)) switch (errno) { case EAGAIN: DEBUGFDEVICE(deviceName, INDI::Logger::DBG_DEBUG, "%s: no buffer found with DQBUF ioctl (EAGAIN) - frame not ready or not requested", __FUNCTION__); return 0; case EIO: /* Could ignore EIO, see spec. */ /* Fall through */ DEBUGFDEVICE(deviceName, INDI::Logger::DBG_DEBUG, "%s: transitory internal error with DQBUF ioctl (EIO)", __FUNCTION__); return 0; case EINVAL: case EPIPE: default: return errno_exit("ReadFrame IO_METHOD_MMAP: VIDIOC_DQBUF", errmsg); } DEBUGFDEVICE(deviceName, INDI::Logger::DBG_DEBUG, "%s: buffer #%d dequeued from fd:%d\n", __FUNCTION__, buf.index, fd); if (buf.flags & V4L2_BUF_FLAG_ERROR) { DEBUGFDEVICE(deviceName, INDI::Logger::DBG_DEBUG, "%s: recoverable error with DQBUF ioctl (BUF_FLAG_ERROR) - frame should be dropped", __FUNCTION__); if (-1 == XIOCTL(fd, VIDIOC_QBUF, &buf)) return errno_exit("ReadFrame IO_METHOD_MMAP: VIDIOC_QBUF", errmsg); buf.bytesused = 0; return 0; } if (!is_compressed() && buf.bytesused != fmt.fmt.pix.sizeimage) { DEBUGFDEVICE(deviceName, INDI::Logger::DBG_DEBUG, "%s: frame is %d-byte long, expected %d - frame should be dropped", __FUNCTION__, buf.bytesused, fmt.fmt.pix.sizeimage); if (false) { unsigned char const *b = (unsigned char const *)buffers[buf.index].start; unsigned char const *end = b + buf.bytesused; do DEBUGFDEVICE(deviceName, INDI::Logger::DBG_DEBUG, "%s: [%p] %02X%02X%02X%02X %02X%02X%02X%02X %02X%02X%02X%02X %02X%02X%02X%02X", __FUNCTION__, b, b[0 * 4 + 0], b[0 * 4 + 1], b[0 * 4 + 2], b[0 * 4 + 3], b[1 * 4 + 0], b[1 * 4 + 1], b[1 * 4 + 2], b[1 * 4 + 3], b[2 * 4 + 0], b[2 * 4 + 1], b[2 * 4 + 2], b[2 * 4 + 3], b[3 * 4 + 0], b[3 * 4 + 1], b[3 * 4 + 2], b[3 * 4 + 3]); while ((b += 16) < end); } if (-1 == XIOCTL(fd, VIDIOC_QBUF, &buf)) return errno_exit("ReadFrame IO_METHOD_MMAP: VIDIOC_QBUF", errmsg); buf.bytesused = 0; return 0; } #if (LINUX_VERSION_CODE >= KERNEL_VERSION(3, 15, 0)) /* TODO: the timestamp can be checked against the expected exposure to validate the frame - doesn't work, yet */ switch (buf.flags & V4L2_BUF_FLAG_TIMESTAMP_MASK) { case V4L2_BUF_FLAG_TIMESTAMP_UNKNOWN: /* FIXME: try monotonic clock when timestamp clock type is unknown */ case V4L2_BUF_FLAG_TIMESTAMP_MONOTONIC: { struct timespec uptime = { 0, 0 }; clock_gettime(CLOCK_MONOTONIC, &uptime); struct timeval epochtime = { 0, 0 }; /*gettimeofday(&epochtime, nullptr); uncomment this to get the timestamp from epoch start */ float const secs = (epochtime.tv_sec - uptime.tv_sec + buf.timestamp.tv_sec) + (epochtime.tv_usec - uptime.tv_nsec / 1000.0f + buf.timestamp.tv_usec) / 1000000.0f; if (V4L2_BUF_FLAG_TSTAMP_SRC_SOE == (buf.flags & V4L2_BUF_FLAG_TSTAMP_SRC_MASK)) { DEBUGFDEVICE(deviceName, INDI::Logger::DBG_DEBUG, "%s: frame exposure started %.03f seconds ago", __FUNCTION__, -secs); } else if (V4L2_BUF_FLAG_TSTAMP_SRC_EOF == (buf.flags & V4L2_BUF_FLAG_TSTAMP_SRC_MASK)) { DEBUGFDEVICE(deviceName, INDI::Logger::DBG_DEBUG, "%s: frame finished capturing %.03f seconds ago", __FUNCTION__, -secs); } else DEBUGFDEVICE(deviceName, INDI::Logger::DBG_DEBUG, "%s: unsupported timestamp in frame", __FUNCTION__); break; } case V4L2_BUF_FLAG_TIMESTAMP_COPY: default: DEBUGFDEVICE(deviceName, INDI::Logger::DBG_DEBUG, "%s: no usable timestamp found in frame", __FUNCTION__); } #endif /* TODO: there is probably a better error handling than asserting the buffer index */ assert(buf.index < n_buffers); if (dodecode) { DEBUGFDEVICE(deviceName, INDI::Logger::DBG_DEBUG, "%s: [%p] decoding %d-byte buffer %p cropset %c", __FUNCTION__, decoder, buf.bytesused, buffers[buf.index].start, cropset ? 'Y' : 'N'); decoder->decode((unsigned char *)(buffers[buf.index].start), &buf); } /* if (dorecord) { DEBUGFDEVICE(deviceName, INDI::Logger::DBG_DEBUG, "%s: [%p] recording %d-byte buffer %p", __FUNCTION__, recorder, buf.bytesused, buffers[buf.index].start); recorder->writeFrame((unsigned char *)(buffers[buf.index].start)); } */ //DEBUGFDEVICE(deviceName, INDI::Logger::DBG_DEBUG,"lxstate is %d, dropFrame %c\n", lxstate, (dropFrame?'Y':'N')); /* Requeue buffer */ if (-1 == XIOCTL(fd, VIDIOC_QBUF, &buf)) return errno_exit("ReadFrame IO_METHOD_MMAP: VIDIOC_QBUF", errmsg); if (lxstate == LX_ACTIVE) { /* Call provided callback function if any */ //if (callback && !dorecord) if (callback) (*callback)(uptr); } if (lxstate == LX_TRIGGERED) lxstate = LX_ACTIVE; break; case IO_METHOD_USERPTR: cerr << "in read Frame method userptr" << endl; CLEAR(buf); buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; buf.memory = V4L2_MEMORY_USERPTR; if (-1 == XIOCTL(fd, VIDIOC_DQBUF, &buf)) { switch (errno) { case EAGAIN: return 0; case EIO: /* Could ignore EIO, see spec. */ /* fall through */ default: errno_exit("VIDIOC_DQBUF", errmsg); } } for (i = 0; i < n_buffers; ++i) if (buf.m.userptr == (unsigned long)buffers[i].start && buf.length == buffers[i].length) break; assert(i < n_buffers); //process_image ((void *) buf.m.userptr); if (-1 == XIOCTL(fd, VIDIOC_QBUF, &buf)) errno_exit("ReadFrame IO_METHOD_USERPTR: VIDIOC_QBUF", errmsg); break; } return 0; } int V4L2_Base::stop_capturing(char *errmsg) { enum v4l2_buf_type type; switch (io) { case IO_METHOD_READ: /* Nothing to do. */ break; case IO_METHOD_MMAP: /* Kernel 3.11 problem with streamoff: vb2_is_busy(queue) remains true so we can not change anything without diconnecting */ /* It seems that device should be closed/reopened to change any capture format settings. From V4L2 API Spec. (Data Negotiation) */ /* Switching the logical stream or returning into "panel mode" is possible by closing and reopening the device. */ /* Drivers may support a switch using VIDIOC_S_FMT. */ case IO_METHOD_USERPTR: // N.B. I used this as a hack to solve a problem with capturing a frame // long time ago. I recently tried taking this hack off, and it worked fine! type = V4L2_BUF_TYPE_VIDEO_CAPTURE; if (selectCallBackID != -1) { IERmCallback(selectCallBackID); selectCallBackID = -1; } streamactive = false; if (-1 == XIOCTL(fd, VIDIOC_STREAMOFF, &type)) return errno_exit("VIDIOC_STREAMOFF", errmsg); break; } //uninit_device(errmsg); return 0; } int V4L2_Base::start_capturing(char *errmsg) { unsigned int i; enum v4l2_buf_type type; if (!streamedonce) init_device(errmsg); switch (io) { case IO_METHOD_READ: /* Nothing to do. */ break; case IO_METHOD_MMAP: for (i = 0; i < n_buffers; ++i) { struct v4l2_buffer buf; CLEAR(buf); buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; buf.memory = V4L2_MEMORY_MMAP; buf.index = i; //DEBUGFDEVICE(deviceName, INDI::Logger::DBG_DEBUG,"v4l2_start_capturing: enqueuing buffer %d for fd=%d\n", buf.index, fd); /*if (-1 == XIOCTL(fd, VIDIOC_QBUF, &buf)) return errno_exit ("StartCapturing IO_METHOD_MMAP: VIDIOC_QBUF", errmsg);*/ XIOCTL(fd, VIDIOC_QBUF, &buf); } type = V4L2_BUF_TYPE_VIDEO_CAPTURE; if (-1 == XIOCTL(fd, VIDIOC_STREAMON, &type)) return errno_exit("VIDIOC_STREAMON", errmsg); selectCallBackID = IEAddCallback(fd, newFrame, this); streamactive = true; break; case IO_METHOD_USERPTR: for (i = 0; i < n_buffers; ++i) { struct v4l2_buffer buf; CLEAR(buf); buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; buf.memory = V4L2_MEMORY_USERPTR; buf.m.userptr = (unsigned long)buffers[i].start; buf.length = buffers[i].length; if (-1 == XIOCTL(fd, VIDIOC_QBUF, &buf)) return errno_exit("StartCapturing IO_METHOD_USERPTR: VIDIOC_QBUF", errmsg); } type = V4L2_BUF_TYPE_VIDEO_CAPTURE; if (-1 == XIOCTL(fd, VIDIOC_STREAMON, &type)) return errno_exit("VIDIOC_STREAMON", errmsg); break; } //if (dropFrameEnabled) //dropFrame = dropFrameCount; streamedonce = true; return 0; } void V4L2_Base::newFrame(int /*fd*/, void *p) { char errmsg[ERRMSGSIZ]; ((V4L2_Base *)(p))->read_frame(errmsg); } int V4L2_Base::uninit_device(char *errmsg) { switch (io) { case IO_METHOD_READ: free(buffers[0].start); break; case IO_METHOD_MMAP: for (unsigned int i = 0; i < n_buffers; ++i) if (-1 == munmap(buffers[i].start, buffers[i].length)) return errno_exit("munmap", errmsg); break; case IO_METHOD_USERPTR: for (unsigned int i = 0; i < n_buffers; ++i) free(buffers[i].start); break; } free(buffers); return 0; } void V4L2_Base::init_read(unsigned int buffer_size) { buffers = (buffer *)calloc(1, sizeof(*buffers)); if (!buffers) { fprintf(stderr, "Out of memory\n"); exit(EXIT_FAILURE); } buffers[0].length = buffer_size; buffers[0].start = malloc(buffer_size); if (!buffers[0].start) { fprintf(stderr, "Out of memory\n"); exit(EXIT_FAILURE); } } int V4L2_Base::init_mmap(char *errmsg) { struct v4l2_requestbuffers req; CLEAR(req); req.count = 4; //req.count = 1; req.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; req.memory = V4L2_MEMORY_MMAP; if (-1 == XIOCTL(fd, VIDIOC_REQBUFS, &req)) { if (EINVAL == errno) { fprintf(stderr, "%.*s does not support memory mapping\n", (int)sizeof(dev_name), dev_name); snprintf(errmsg, ERRMSGSIZ, "%.*s does not support memory mapping\n", (int)sizeof(dev_name), dev_name); return -1; } else { return errno_exit("VIDIOC_REQBUFS", errmsg); } } if (req.count < 2) { fprintf(stderr, "Insufficient buffer memory on %.*s\n", (int)sizeof(dev_name), dev_name); snprintf(errmsg, ERRMSGSIZ, "Insufficient buffer memory on %.*s\n", (int)sizeof(dev_name), dev_name); return -1; } buffers = (buffer *)calloc(req.count, sizeof(*buffers)); if (!buffers) { fprintf(stderr, "buffers. Out of memory\n"); strncpy(errmsg, "buffers. Out of memory\n", ERRMSGSIZ); return -1; } for (n_buffers = 0; n_buffers < req.count; n_buffers++) { struct v4l2_buffer buf; CLEAR(buf); buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; buf.memory = V4L2_MEMORY_MMAP; buf.index = n_buffers; if (-1 == XIOCTL(fd, VIDIOC_QUERYBUF, &buf)) return errno_exit("VIDIOC_QUERYBUF", errmsg); buffers[n_buffers].length = buf.length; buffers[n_buffers].start = mmap(nullptr /* start anywhere */, buf.length, PROT_READ | PROT_WRITE /* required */, MAP_SHARED /* recommended */, fd, buf.m.offset); if (MAP_FAILED == buffers[n_buffers].start) return errno_exit("mmap", errmsg); } return 0; } void V4L2_Base::init_userp(unsigned int buffer_size) { struct v4l2_requestbuffers req; char errmsg[ERRMSGSIZ]; CLEAR(req); req.count = 4; req.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; req.memory = V4L2_MEMORY_USERPTR; if (-1 == XIOCTL(fd, VIDIOC_REQBUFS, &req)) { if (EINVAL == errno) { fprintf(stderr, "%.*s does not support user pointer i/o\n", (int)sizeof(dev_name), dev_name); exit(EXIT_FAILURE); } else { errno_exit("VIDIOC_REQBUFS", errmsg); } } buffers = (buffer *)calloc(4, sizeof(*buffers)); if (!buffers) { fprintf(stderr, "Out of memory\n"); exit(EXIT_FAILURE); } for (n_buffers = 0; n_buffers < 4; ++n_buffers) { buffers[n_buffers].length = buffer_size; buffers[n_buffers].start = malloc(buffer_size); if (!buffers[n_buffers].start) { fprintf(stderr, "Out of memory\n"); exit(EXIT_FAILURE); } } } int V4L2_Base::check_device(char *errmsg) { struct v4l2_input input_avail; if (-1 == XIOCTL(fd, VIDIOC_QUERYCAP, &cap)) { if (EINVAL == errno) { fprintf(stderr, "%.*s is no V4L2 device\n", (int)sizeof(dev_name), dev_name); snprintf(errmsg, ERRMSGSIZ, "%.*s is no V4L2 device\n", (int)sizeof(dev_name), dev_name); return -1; } else { return errno_exit("VIDIOC_QUERYCAP", errmsg); } } DEBUGFDEVICE(deviceName, INDI::Logger::DBG_DEBUG, "Driver %.*s (version %u.%u.%u)", (int)sizeof(cap.driver), cap.driver, (cap.version >> 16) & 0xFF, (cap.version >> 8) & 0xFF, (cap.version & 0xFF)); DEBUGFDEVICE(deviceName, INDI::Logger::DBG_DEBUG, " card: %.*s", (int)sizeof(cap.card), cap.card); DEBUGFDEVICE(deviceName, INDI::Logger::DBG_DEBUG, " bus: %.*s", (int)sizeof(cap.bus_info), cap.bus_info); setframerate = &V4L2_Base::stdsetframerate; getframerate = &V4L2_Base::stdgetframerate; // if (!(strncmp((const char *)cap.driver, "pwc", sizeof(cap.driver)))) // { //unsigned int qual = 3; //pwc driver does not allow to get current fps with VIDIOCPWC //frameRate.numerator=1; // using default module load fps //frameRate.denominator=10; //if (ioctl(fd, VIDIOCPWCSLED, &qual)) { // DEBUGFDEVICE(deviceName, INDI::Logger::DBG_DEBUG,"ioctl: can't set pwc video quality to High (uncompressed).\n"); //} //else // DEBUGFDEVICE(deviceName, INDI::Logger::DBG_DEBUG," Setting pwc video quality to High (uncompressed)\n"); //setframerate=&V4L2_Base::pwcsetframerate; // } DEBUGDEVICE(deviceName, INDI::Logger::DBG_DEBUG, "Driver capabilities:"); if (cap.capabilities & V4L2_CAP_VIDEO_CAPTURE) DEBUGDEVICE(deviceName, INDI::Logger::DBG_DEBUG, " V4L2_CAP_VIDEO_CAPTURE"); if (cap.capabilities & V4L2_CAP_VIDEO_OUTPUT) DEBUGDEVICE(deviceName, INDI::Logger::DBG_DEBUG, " V4L2_CAP_VIDEO_OUTPUT"); if (cap.capabilities & V4L2_CAP_VIDEO_OVERLAY) DEBUGDEVICE(deviceName, INDI::Logger::DBG_DEBUG, " V4L2_CAP_VIDEO_OVERLAY"); if (cap.capabilities & V4L2_CAP_VBI_CAPTURE) DEBUGDEVICE(deviceName, INDI::Logger::DBG_DEBUG, " V4L2_CAP_VBI_CAPTURE"); if (cap.capabilities & V4L2_CAP_VBI_OUTPUT) DEBUGDEVICE(deviceName, INDI::Logger::DBG_DEBUG, " V4L2_CAP_VBI_OUTPUT"); if (cap.capabilities & V4L2_CAP_SLICED_VBI_CAPTURE) DEBUGDEVICE(deviceName, INDI::Logger::DBG_DEBUG, " V4L2_CAP_SLICED_VBI_CAPTURE"); if (cap.capabilities & V4L2_CAP_SLICED_VBI_OUTPUT) DEBUGDEVICE(deviceName, INDI::Logger::DBG_DEBUG, " V4L2_CAP_SLICED_VBI_OUTPUT"); if (cap.capabilities & V4L2_CAP_RDS_CAPTURE) DEBUGDEVICE(deviceName, INDI::Logger::DBG_DEBUG, " V4L2_CAP_RDS_CAPTURE"); if (cap.capabilities & V4L2_CAP_VIDEO_OUTPUT_OVERLAY) DEBUGDEVICE(deviceName, INDI::Logger::DBG_DEBUG, " V4L2_CAP_VIDEO_OUTPUT_OVERLAY"); if (cap.capabilities & V4L2_CAP_TUNER) DEBUGDEVICE(deviceName, INDI::Logger::DBG_DEBUG, " V4L2_CAP_TUNER"); if (cap.capabilities & V4L2_CAP_AUDIO) DEBUGDEVICE(deviceName, INDI::Logger::DBG_DEBUG, " V4L2_CAP_AUDIO"); if (cap.capabilities & V4L2_CAP_RADIO) DEBUGDEVICE(deviceName, INDI::Logger::DBG_DEBUG, " V4L2_CAP_RADIO"); if (cap.capabilities & V4L2_CAP_READWRITE) DEBUGDEVICE(deviceName, INDI::Logger::DBG_DEBUG, " V4L2_CAP_READWRITE"); if (cap.capabilities & V4L2_CAP_ASYNCIO) DEBUGDEVICE(deviceName, INDI::Logger::DBG_DEBUG, " V4L2_CAP_ASYNCIO"); if (cap.capabilities & V4L2_CAP_STREAMING) DEBUGDEVICE(deviceName, INDI::Logger::DBG_DEBUG, " V4L2_CAP_STREAMING"); /*if (cap.capabilities & V4L2_CAP_EXT_PIX_FORMAT) { has_ext_pix_format=true; DEBUGFDEVICE(deviceName, INDI::Logger::DBG_DEBUG," V4L2_CAP_EXT_PIX_FORMAT\n"); }*/ if (!(cap.capabilities & V4L2_CAP_VIDEO_CAPTURE)) { fprintf(stderr, "%.*s is no video capture device\n", (int)sizeof(dev_name), dev_name); snprintf(errmsg, ERRMSGSIZ, "%.*s is no video capture device", (int)sizeof(dev_name), dev_name); return -1; } switch (io) { case IO_METHOD_READ: if (!(cap.capabilities & V4L2_CAP_READWRITE)) { fprintf(stderr, "%.*s does not support read i/o", (int)sizeof(dev_name), dev_name); snprintf(errmsg, ERRMSGSIZ, "%.*s does not support read i/o", (int)sizeof(dev_name), dev_name); return -1; } break; case IO_METHOD_MMAP: case IO_METHOD_USERPTR: if (!(cap.capabilities & V4L2_CAP_STREAMING)) { fprintf(stderr, "%.*s does not support streaming i/o", (int)sizeof(dev_name), dev_name); snprintf(errmsg, ERRMSGSIZ, "%.*s does not support streaming i/o", (int)sizeof(dev_name), dev_name); return -1; } break; } /* Select video input, video standard and tune here. */ if (-1 == ioctl(fd, VIDIOC_G_INPUT, &input.index)) { perror("VIDIOC_G_INPUT"); exit(EXIT_FAILURE); } DEBUGDEVICE(deviceName, INDI::Logger::DBG_DEBUG, "Enumerating available Inputs:"); for (input_avail.index = 0; ioctl(fd, VIDIOC_ENUMINPUT, &input_avail) != -1; input_avail.index++) DEBUGFDEVICE(deviceName, INDI::Logger::DBG_DEBUG, "%2d. %.*s (type %s)%s", input_avail.index, (int)sizeof(input_avail.name), input_avail.name, (input_avail.type == V4L2_INPUT_TYPE_TUNER ? "Tuner/RF Demodulator" : "Composite/S-Video"), input.index == input_avail.index ? " current" : ""); if (errno != EINVAL) DEBUGDEVICE(deviceName, INDI::Logger::DBG_DEBUG, "(problem enumerating inputs)"); enumeratedInputs = input_avail.index; /* Cropping */ cropcap.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; cancrop = true; if (-1 == XIOCTL(fd, VIDIOC_CROPCAP, &cropcap)) { perror("VIDIOC_CROPCAP"); crop.c.top = -1; cancrop = false; /* Errors ignored. */ } if (cancrop) { DEBUGFDEVICE(deviceName, INDI::Logger::DBG_DEBUG, " Crop capabilities: bounds = (top=%d, left=%d, width=%d, height=%d)", cropcap.bounds.top, cropcap.bounds.left, cropcap.bounds.width, cropcap.bounds.height); DEBUGFDEVICE(deviceName, INDI::Logger::DBG_DEBUG, " Crop capabilities: defrect = (top=%d, left=%d, width=%d, height=%d)", cropcap.defrect.top, cropcap.defrect.left, cropcap.defrect.width, cropcap.defrect.height); DEBUGFDEVICE(deviceName, INDI::Logger::DBG_DEBUG, " Crop capabilities: pixelaspect = %d / %d", cropcap.pixelaspect.numerator, cropcap.pixelaspect.denominator); DEBUGDEVICE(deviceName, INDI::Logger::DBG_DEBUG, "Explicitely resetting crop area to default..."); crop.c.top = cropcap.defrect.top; crop.c.left = cropcap.defrect.left; crop.c.width = cropcap.defrect.width; crop.c.height = cropcap.defrect.height; if (-1 == XIOCTL(fd, VIDIOC_S_CROP, &crop)) { perror("VIDIOC_S_CROP"); cancrop = false; /* Errors ignored. */ } if (-1 == XIOCTL(fd, VIDIOC_G_CROP, &crop)) { perror("VIDIOC_G_CROP"); crop.c.top = -1; cancrop = false; /* Errors ignored. */ } } decoder->usesoftcrop(!cancrop); // Enumerating capture format { struct v4l2_fmtdesc fmt_avail; fmt_avail.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; //DEBUG(INDI::Logger::DBG_SESSION,"Available Capture Image formats:"); DEBUGDEVICE(deviceName, INDI::Logger::DBG_DEBUG, "Enumerating available Capture Image formats:"); for (fmt_avail.index = 0; ioctl(fd, VIDIOC_ENUM_FMT, &fmt_avail) != -1; fmt_avail.index++) { //DEBUGFDEVICE(deviceName, INDI::Logger::DBG_DEBUG,INDI::Logger::DBG_SESSION,"\t%d. %s (%c%c%c%c) %s\n", fmt_avail.index, fmt_avail.description, (fmt_avail.pixelformat)&0xFF, (fmt_avail.pixelformat >> 8)&0xFF, // (fmt_avail.pixelformat >> 16)&0xFF, (fmt_avail.pixelformat >> 24)&0xFF, (decoder->issupportedformat(fmt_avail.pixelformat)?"supported":"UNSUPPORTED")); DEBUGFDEVICE(deviceName, INDI::Logger::DBG_DEBUG, "%2d. Format %.*s (%c%c%c%c) is %s", fmt_avail.index, (int)sizeof(fmt_avail.description), fmt_avail.description, (fmt_avail.pixelformat) & 0xFF, (fmt_avail.pixelformat >> 8) & 0xFF, (fmt_avail.pixelformat >> 16) & 0xFF, (fmt_avail.pixelformat >> 24) & 0xFF, (decoder->issupportedformat(fmt_avail.pixelformat) ? "supported" : "UNSUPPORTED")); { // Enumerating frame sizes available for this pixel format struct v4l2_frmsizeenum frm_sizeenum; frm_sizeenum.pixel_format = fmt_avail.pixelformat; DEBUGDEVICE(deviceName, INDI::Logger::DBG_DEBUG, " Enumerating available Frame sizes/rates for this format:"); for (frm_sizeenum.index = 0; XIOCTL(fd, VIDIOC_ENUM_FRAMESIZES, &frm_sizeenum) != -1; frm_sizeenum.index++) { switch (frm_sizeenum.type) { case V4L2_FRMSIZE_TYPE_DISCRETE: DEBUGFDEVICE(deviceName, INDI::Logger::DBG_DEBUG, " %2d. (Discrete) width %d x height %d\n", frm_sizeenum.index, frm_sizeenum.discrete.width, frm_sizeenum.discrete.height); break; case V4L2_FRMSIZE_TYPE_STEPWISE: DEBUGFDEVICE(deviceName, INDI::Logger::DBG_DEBUG, " (Stepwise) min. width %d, max. width %d step width %d", frm_sizeenum.stepwise.min_width, frm_sizeenum.stepwise.max_width, frm_sizeenum.stepwise.step_width); DEBUGFDEVICE(deviceName, INDI::Logger::DBG_DEBUG, " (Stepwise) min. height %d, max. height %d step height %d ", frm_sizeenum.stepwise.min_height, frm_sizeenum.stepwise.max_height, frm_sizeenum.stepwise.step_height); break; case V4L2_FRMSIZE_TYPE_CONTINUOUS: DEBUGFDEVICE(deviceName, INDI::Logger::DBG_DEBUG, " (Continuous--step=1) min. width %d, max. width %d", frm_sizeenum.stepwise.min_width, frm_sizeenum.stepwise.max_width); DEBUGFDEVICE(deviceName, INDI::Logger::DBG_DEBUG, " (Continuous--step=1) min. height %d, max. height %d ", frm_sizeenum.stepwise.min_height, frm_sizeenum.stepwise.max_height); break; default: DEBUGFDEVICE(deviceName, INDI::Logger::DBG_DEBUG, " Unknown Frame size type: %d\n", frm_sizeenum.type); break; } { // Enumerating frame intervals available for this frame size and this pixel format struct v4l2_frmivalenum frmi_valenum; frmi_valenum.pixel_format = fmt_avail.pixelformat; if (frm_sizeenum.type == V4L2_FRMSIZE_TYPE_DISCRETE) { frmi_valenum.width = frm_sizeenum.discrete.width; frmi_valenum.height = frm_sizeenum.discrete.height; } else { frmi_valenum.width = frm_sizeenum.stepwise.max_width; frmi_valenum.height = frm_sizeenum.stepwise.max_height; } frmi_valenum.type = 0; frmi_valenum.stepwise.min.numerator = 0; frmi_valenum.stepwise.min.denominator = 0; frmi_valenum.stepwise.max.numerator = 0; frmi_valenum.stepwise.max.denominator = 0; frmi_valenum.stepwise.step.numerator = 0; frmi_valenum.stepwise.step.denominator = 0; DEBUGDEVICE(deviceName, INDI::Logger::DBG_DEBUG, " Frame intervals:"); for (frmi_valenum.index = 0; XIOCTL(fd, VIDIOC_ENUM_FRAMEINTERVALS, &frmi_valenum) != -1; frmi_valenum.index++) { switch (frmi_valenum.type) { case V4L2_FRMIVAL_TYPE_DISCRETE: DEBUGFDEVICE(deviceName, INDI::Logger::DBG_DEBUG, " %d/%d s", frmi_valenum.discrete.numerator, frmi_valenum.discrete.denominator); break; case V4L2_FRMIVAL_TYPE_STEPWISE: DEBUGFDEVICE( deviceName, INDI::Logger::DBG_DEBUG, " (Stepwise) min. %d/%ds, max. %d / %d s, step %d / %d s", frmi_valenum.stepwise.min.numerator, frmi_valenum.stepwise.min.denominator, frmi_valenum.stepwise.max.numerator, frmi_valenum.stepwise.max.denominator, frmi_valenum.stepwise.step.numerator, frmi_valenum.stepwise.step.denominator); break; case V4L2_FRMIVAL_TYPE_CONTINUOUS: DEBUGFDEVICE( deviceName, INDI::Logger::DBG_DEBUG, " (Continuous) min. %d / %d s, max. %d / %d s", frmi_valenum.stepwise.min.numerator, frmi_valenum.stepwise.min.denominator, frmi_valenum.stepwise.max.numerator, frmi_valenum.stepwise.max.denominator); break; default: DEBUGFDEVICE(deviceName, INDI::Logger::DBG_DEBUG, " Unknown Frame rate type: %d", frmi_valenum.type); break; } } if (frmi_valenum.index == 0) { perror("VIDIOC_ENUM_FRAMEINTERVALS"); switch (frmi_valenum.type) { case V4L2_FRMIVAL_TYPE_DISCRETE: DEBUGFDEVICE(deviceName, INDI::Logger::DBG_DEBUG, " %d/%d s", frmi_valenum.discrete.numerator, frmi_valenum.discrete.denominator); break; case V4L2_FRMIVAL_TYPE_STEPWISE: DEBUGFDEVICE( deviceName, INDI::Logger::DBG_DEBUG, " (Stepwise) min. %d/%ds, max. %d / %d s, step %d / %d s", frmi_valenum.stepwise.min.numerator, frmi_valenum.stepwise.min.denominator, frmi_valenum.stepwise.max.numerator, frmi_valenum.stepwise.max.denominator, frmi_valenum.stepwise.step.numerator, frmi_valenum.stepwise.step.denominator); break; case V4L2_FRMIVAL_TYPE_CONTINUOUS: DEBUGFDEVICE( deviceName, INDI::Logger::DBG_DEBUG, " (Continuous) min. %d / %d s, max. %d / %d s", frmi_valenum.stepwise.min.numerator, frmi_valenum.stepwise.min.denominator, frmi_valenum.stepwise.max.numerator, frmi_valenum.stepwise.max.denominator); break; default: DEBUGFDEVICE(deviceName, INDI::Logger::DBG_DEBUG, " Unknown Frame rate type: %d", frmi_valenum.type); break; } } //DEBUGFDEVICE(deviceName, INDI::Logger::DBG_DEBUG,"error %d, %s\n", errno, strerror (errno)); } } } } if (errno != EINVAL) DEBUGDEVICE(deviceName, INDI::Logger::DBG_DEBUG, "Problem enumerating capture formats."); enumeratedCaptureFormats = fmt_avail.index; } // CLEAR (fmt); // fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; // if (-1 == XIOCTL(fd, VIDIOC_G_FMT, &fmt)) // return errno_exit ("VIDIOC_G_FMT", errmsg); // fmt.fmt.pix.width = (width == -1) ? fmt.fmt.pix.width : width; // fmt.fmt.pix.height = (height == -1) ? fmt.fmt.pix.height : height; // fmt.fmt.pix.pixelformat = (pixelFormat == -1) ? fmt.fmt.pix.pixelformat : pixelFormat; // //fmt.fmt.pix.field = V4L2_FIELD_INTERLACED; // if (-1 == XIOCTL(fd, VIDIOC_S_FMT, &fmt)) // return errno_exit ("VIDIOC_S_FMT", errmsg); // /* Note VIDIOC_S_FMT may change width and height. */ // /* Buggy driver paranoia. */ // min = fmt.fmt.pix.width * 2; // if (fmt.fmt.pix.bytesperline < min) // fmt.fmt.pix.bytesperline = min; // min = fmt.fmt.pix.bytesperline * fmt.fmt.pix.height; // if (fmt.fmt.pix.sizeimage < min) // fmt.fmt.pix.sizeimage = min; /* Refresh the instance format with the current device format */ CLEAR(fmt); return ioctl_set_format(fmt, errmsg); } int V4L2_Base::init_device(char *errmsg) { //findMinMax(); // decode allocBuffers(); lxstate = LX_ACTIVE; switch (io) { case IO_METHOD_READ: init_read(fmt.fmt.pix.sizeimage); break; case IO_METHOD_MMAP: return init_mmap(errmsg); break; case IO_METHOD_USERPTR: init_userp(fmt.fmt.pix.sizeimage); break; } return 0; } void V4L2_Base::close_device() { char errmsg[ERRMSGSIZ]; uninit_device(errmsg); if (-1 == close(fd)) errno_exit("close", errmsg); fd = -1; } int V4L2_Base::open_device(const char *devpath, char *errmsg) { struct stat st; strncpy(dev_name, devpath, 64); if (-1 == stat(dev_name, &st)) { fprintf(stderr, "Cannot identify %.*s: %d, %s\n", (int)sizeof(dev_name), dev_name, errno, strerror(errno)); snprintf(errmsg, ERRMSGSIZ, "Cannot identify %.*s: %d, %s\n", (int)sizeof(dev_name), dev_name, errno, strerror(errno)); return -1; } if (!S_ISCHR(st.st_mode)) { fprintf(stderr, "%.*s is no device\n", (int)sizeof(dev_name), dev_name); snprintf(errmsg, ERRMSGSIZ, "%.*s is no device\n", (int)sizeof(dev_name), dev_name); return -1; } fd = open(dev_name, O_RDWR /* required */ | O_NONBLOCK, 0); if (-1 == fd) { fprintf(stderr, "Cannot open %.*s: %d, %s\n", (int)sizeof(dev_name), dev_name, errno, strerror(errno)); snprintf(errmsg, ERRMSGSIZ, "Cannot open %.*s: %d, %s\n", (int)sizeof(dev_name), dev_name, errno, strerror(errno)); return -1; } streamedonce = false; snprintf(errmsg, ERRMSGSIZ, "%s\n", strerror(0)); return 0; } /* @brief Storing inputs in a switch vector property, marking current as selected. * * @param inputssp is the property to store to (nothing is stored if null). */ void V4L2_Base::getinputs(ISwitchVectorProperty *inputssp) { if (!inputssp) return; struct v4l2_input input_avail; /* Allocate inputs from previously enumerated count */ size_t const inputsLen = enumeratedInputs * sizeof(ISwitch); ISwitch *inputs = (ISwitch *)malloc(inputsLen); if (!inputs) exit(EXIT_FAILURE); memset(inputs, 0, inputsLen); /* Ask device about each input */ for (input_avail.index = 0; (int)input_avail.index < enumeratedInputs; input_avail.index++) { /* Enumeration ends with EINVAL */ if (XIOCTL(fd, VIDIOC_ENUMINPUT, &input_avail)) break; /* Store input description */ strncpy(inputs[input_avail.index].name, (const char *)input_avail.name, MAXINDINAME); strncpy(inputs[input_avail.index].label, (const char *)input_avail.name, MAXINDILABEL); } /* Free inputs before replacing */ if (inputssp->sp) free(inputssp->sp); /* Store inputs */ inputssp->sp = inputs; inputssp->nsp = input_avail.index; IUResetSwitch(inputssp); /* And mark current */ inputs[input.index].s = ISS_ON; DEBUGFDEVICE(deviceName, INDI::Logger::DBG_DEBUG, "Current video input is %d. %.*s", input.index, (int)sizeof(inputs[input.index].name), inputs[input.index].name); } int V4L2_Base::setinput(unsigned int inputindex, char *errmsg) { DEBUGFDEVICE(deviceName, INDI::Logger::DBG_DEBUG, "Setting Video input to %d", inputindex); if (streamedonce) { close_device(); if (open_device(path, errmsg)) { DEBUGFDEVICE(deviceName, INDI::Logger::DBG_DEBUG, "%s: failed reopening device %s (%s)", __FUNCTION__, path, errmsg); return -1; } } if (-1 == XIOCTL(fd, VIDIOC_S_INPUT, &inputindex)) { return errno_exit("VIDIOC_S_INPUT", errmsg); } if (-1 == XIOCTL(fd, VIDIOC_G_INPUT, &input.index)) { return errno_exit("VIDIOC_G_INPUT", errmsg); } //decode reallocate_buffers=true; return 0; } /* @brief Storing capture formats in a switch vector property, marking current as selected. * * @param captureformatssp is the property to store to (nothing is stored if null). */ void V4L2_Base::getcaptureformats(ISwitchVectorProperty *captureformatssp) { if (!captureformatssp) return; struct v4l2_fmtdesc fmt_avail; /* Allocate capture formats from preliminary enumerated count */ size_t const formatsLen = enumeratedCaptureFormats * sizeof(ISwitch); ISwitch *formats = (ISwitch *)malloc(formatsLen); if (!formats) exit(EXIT_FAILURE); memset(formats, 0, formatsLen); /* Ask device about each format */ fmt_avail.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; for (fmt_avail.index = 0; (int)fmt_avail.index < enumeratedCaptureFormats; fmt_avail.index++) { /* Enumeration ends with EINVAL */ if (XIOCTL(fd, VIDIOC_ENUM_FMT, &fmt_avail)) break; /* Store format description */ strncpy(formats[fmt_avail.index].name, (const char *)fmt_avail.description, MAXINDINAME); strncpy(formats[fmt_avail.index].label, (const char *)fmt_avail.description, MAXINDILABEL); /* And store pixel format for reference */ /* FIXME: store pixel format as void pointer to avoid that malloc */ formats[fmt_avail.index].aux = (int *)malloc(sizeof(int)); if (!formats[fmt_avail.index].aux) exit(EXIT_FAILURE); *(int *)formats[fmt_avail.index].aux = fmt_avail.pixelformat; } /* Free formats before replacing */ if (captureformatssp->sp) free(captureformatssp->sp); /* Store formats */ captureformatssp->sp = formats; captureformatssp->nsp = fmt_avail.index; IUResetSwitch(captureformatssp); /* And mark current */ for (unsigned int i = 0; i < fmt_avail.index; i++) { if ((int)fmt.fmt.pix.pixelformat == *(int *)formats[i].aux) { formats[i].s = ISS_ON; DEBUGFDEVICE(deviceName, INDI::Logger::DBG_DEBUG, "Current capture format is %d. %c%c%c%c.", i, (fmt.fmt.pix.pixelformat) & 0xFF, (fmt.fmt.pix.pixelformat >> 8) & 0xFF, (fmt.fmt.pix.pixelformat >> 16) & 0xFF, (fmt.fmt.pix.pixelformat >> 24) & 0xFF); } else formats[i].s = ISS_OFF; } } /* @brief Setting the pixel format of the capture. * * @param captureformat is the identifier of the pixel format to set. * @param errmsg is the error message to return in case of failure. * @return 0 if successful, else -1 with error message updated. */ int V4L2_Base::setcaptureformat(unsigned int captureformat, char *errmsg) { struct v4l2_format new_fmt; CLEAR(new_fmt); new_fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; new_fmt.fmt.pix.pixelformat = captureformat; return ioctl_set_format(new_fmt, errmsg); } void V4L2_Base::getcapturesizes(ISwitchVectorProperty *capturesizessp, INumberVectorProperty *capturesizenp) { struct v4l2_frmsizeenum frm_sizeenum; ISwitch *sizes = nullptr; INumber *sizevalue = nullptr; bool sizefound = false; if (capturesizessp->sp) free(capturesizessp->sp); if (capturesizenp->np) free(capturesizenp->np); frm_sizeenum.pixel_format = fmt.fmt.pix.pixelformat; //DEBUGFDEVICE(deviceName, INDI::Logger::DBG_DEBUG,"\t Available Frame sizes/rates for this format:\n"); for (frm_sizeenum.index = 0; XIOCTL(fd, VIDIOC_ENUM_FRAMESIZES, &frm_sizeenum) != -1; frm_sizeenum.index++) { switch (frm_sizeenum.type) { case V4L2_FRMSIZE_TYPE_DISCRETE: sizes = (sizes == nullptr) ? (ISwitch *)malloc(sizeof(ISwitch)) : (ISwitch *)realloc(sizes, (frm_sizeenum.index + 1) * sizeof(ISwitch)); snprintf(sizes[frm_sizeenum.index].name, MAXINDINAME, "%dx%d", frm_sizeenum.discrete.width, frm_sizeenum.discrete.height); snprintf(sizes[frm_sizeenum.index].label, MAXINDINAME, "%dx%d", frm_sizeenum.discrete.width, frm_sizeenum.discrete.height); sizes[frm_sizeenum.index].s = ISS_OFF; if (!sizefound) { if ((fmt.fmt.pix.width == frm_sizeenum.discrete.width) && (fmt.fmt.pix.height == frm_sizeenum.discrete.height)) { sizes[frm_sizeenum.index].s = ISS_ON; sizefound = true; DEBUGFDEVICE(deviceName, INDI::Logger::DBG_DEBUG, "Current capture size is (%d.) %dx%d", frm_sizeenum.index, frm_sizeenum.discrete.width, frm_sizeenum.discrete.height); } } break; case V4L2_FRMSIZE_TYPE_STEPWISE: case V4L2_FRMSIZE_TYPE_CONTINUOUS: sizevalue = (INumber *)malloc(2 * sizeof(INumber)); IUFillNumber(sizevalue, "Width", "Width", "%.0f", frm_sizeenum.stepwise.min_width, frm_sizeenum.stepwise.max_width, frm_sizeenum.stepwise.step_width, fmt.fmt.pix.width); IUFillNumber(sizevalue + 1, "Height", "Height", "%.0f", frm_sizeenum.stepwise.min_height, frm_sizeenum.stepwise.max_height, frm_sizeenum.stepwise.step_height, fmt.fmt.pix.height); DEBUGFDEVICE(deviceName, INDI::Logger::DBG_DEBUG, "Current capture size is %dx%d", fmt.fmt.pix.width, fmt.fmt.pix.height); break; default: DEBUGFDEVICE(deviceName, INDI::Logger::DBG_DEBUG, "Unknown Frame size type: %d", frm_sizeenum.type); break; } } if (sizes != nullptr) { capturesizessp->sp = sizes; capturesizessp->nsp = frm_sizeenum.index; capturesizenp->np = nullptr; } else { capturesizenp->np = sizevalue; capturesizenp->nnp = 2; capturesizessp->sp = nullptr; } } /* @brief Updating the capture dimensions. * * @param w is the updated width of the capture. * @param h is the update height of the capture. * @param errmsg is the returned error message in case of failure. * @return 0 if successful, else -1 with error message updated. */ int V4L2_Base::setcapturesize(unsigned int w, unsigned int h, char *errmsg) { struct v4l2_format new_fmt = fmt; new_fmt.fmt.pix.width = w; new_fmt.fmt.pix.height = h; return ioctl_set_format(new_fmt, errmsg); } void V4L2_Base::getframerates(ISwitchVectorProperty *frameratessp, INumberVectorProperty *frameratenp) { struct v4l2_frmivalenum frmi_valenum; ISwitch *rates = nullptr; INumber *ratevalue = nullptr; struct v4l2_fract frate; if (frameratessp->sp) free(frameratessp->sp); if (frameratenp->np) free(frameratenp->np); frate = (this->*getframerate)(); bzero(&frmi_valenum, sizeof(frmi_valenum)); frmi_valenum.pixel_format = fmt.fmt.pix.pixelformat; frmi_valenum.width = fmt.fmt.pix.width; frmi_valenum.height = fmt.fmt.pix.height; for (frmi_valenum.index = 0; XIOCTL(fd, VIDIOC_ENUM_FRAMEINTERVALS, &frmi_valenum) != -1; frmi_valenum.index++) { switch (frmi_valenum.type) { case V4L2_FRMIVAL_TYPE_DISCRETE: rates = (rates == nullptr) ? (ISwitch *)malloc(sizeof(ISwitch)) : (ISwitch *)realloc(rates, (frmi_valenum.index + 1) * sizeof(ISwitch)); snprintf(rates[frmi_valenum.index].name, MAXINDINAME, "%d/%d", frmi_valenum.discrete.numerator, frmi_valenum.discrete.denominator); snprintf(rates[frmi_valenum.index].label, MAXINDINAME, "%d/%d", frmi_valenum.discrete.numerator, frmi_valenum.discrete.denominator); if ((frate.numerator == frmi_valenum.discrete.numerator) && (frate.denominator == frmi_valenum.discrete.denominator)) { DEBUGFDEVICE(deviceName, INDI::Logger::DBG_DEBUG, "Current frame interval is %d/%d", frmi_valenum.discrete.numerator, frmi_valenum.discrete.denominator); rates[frmi_valenum.index].s = ISS_ON; } else rates[frmi_valenum.index].s = ISS_OFF; break; case V4L2_FRMIVAL_TYPE_STEPWISE: case V4L2_FRMIVAL_TYPE_CONTINUOUS: ratevalue = (INumber *)malloc(sizeof(INumber)); IUFillNumber(ratevalue, "V4L2_FRAME_INTERVAL", "Frame Interval", "%.0f", frmi_valenum.stepwise.min.numerator / (double)frmi_valenum.stepwise.min.denominator, frmi_valenum.stepwise.max.numerator / (double)frmi_valenum.stepwise.max.denominator, frmi_valenum.stepwise.step.numerator / (double)frmi_valenum.stepwise.step.denominator, frate.numerator / (double)frate.denominator); break; default: DEBUGFDEVICE(deviceName, INDI::Logger::DBG_DEBUG, "Unknown Frame rate type: %d", frmi_valenum.type); break; } } frameratessp->sp = nullptr; frameratessp->nsp = 0; frameratenp->np = nullptr; frameratenp->nnp = 0; if (frmi_valenum.index != 0) { if (rates != nullptr) { frameratessp->sp = rates; frameratessp->nsp = frmi_valenum.index; } else { frameratenp->np = ratevalue; frameratenp->nnp = 1; } } } int V4L2_Base::setcroprect(int x, int y, int w, int h, char *errmsg) { bool softcrop = false; crop.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; crop.c.left = x; crop.c.top = y; crop.c.width = w; crop.c.height = h; if ((int)(crop.c.left + crop.c.width) > (int)fmt.fmt.pix.width) { strncpy(errmsg, "crop width exceeds image width", ERRMSGSIZ); return -1; } if ((int)(crop.c.top + crop.c.height) > (int)fmt.fmt.pix.height) { strncpy(errmsg, "crop height exceeds image height", ERRMSGSIZ); return -1; } // we only support pair sizes if ((crop.c.width % 2 != 0) || (crop.c.height % 2 != 0)) { strncpy(errmsg, "crop width/height must be pair", ERRMSGSIZ); return -1; } if ((crop.c.left == 0) && (crop.c.top == 0) && ((int)crop.c.width == (int)fmt.fmt.pix.width) && ((int)crop.c.height == (int)fmt.fmt.pix.height)) { cropset = false; decoder->resetcrop(); } else { if (cancrop) { if (-1 == XIOCTL(fd, VIDIOC_S_CROP, &crop)) { return errno_exit("VIDIOC_S_CROP", errmsg); } if (-1 == XIOCTL(fd, VIDIOC_G_CROP, &crop)) { return errno_exit("VIDIOC_G_CROP", errmsg); } } softcrop = decoder->setcrop(crop); cropset = true; if ((!cancrop) && (!softcrop)) { cropset = false; strncpy(errmsg, "No hardware and software cropping for this format.", ERRMSGSIZ); return -1; } } //decode allocBuffers(); DEBUGFDEVICE(deviceName, INDI::Logger::DBG_DEBUG, "V4L2 base setcroprect %dx%d at (%d, %d)", crop.c.width, crop.c.height, crop.c.left, crop.c.top); return 0; } int V4L2_Base::getWidth() { if (cropset) return crop.c.width; else return fmt.fmt.pix.width; } int V4L2_Base::getHeight() { if (cropset) return crop.c.height; else return fmt.fmt.pix.height; } int V4L2_Base::getBpp() { return bpp; } int V4L2_Base::getFormat() { return fmt.fmt.pix.pixelformat; } struct v4l2_rect V4L2_Base::getcroprect() { return crop.c; } int V4L2_Base::stdsetframerate(struct v4l2_fract frate, char *errmsg) { struct v4l2_streamparm sparm; //if (!cansetrate) {sprintf(errmsg, "Can not set rate"); return -1;} bzero(&sparm, sizeof(struct v4l2_streamparm)); sparm.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; sparm.parm.capture.timeperframe = frate; if (-1 == XIOCTL(fd, VIDIOC_S_PARM, &sparm)) { //cansetrate=false; return errno_exit("VIDIOC_S_PARM", errmsg); } return 0; } /* @brief Setting the framerate for Philips-based PWC devices. * * @param frate is the v4l2_fract structure defining framerate. * @param errmsg is the returned error message in case of error. * @return 0 if successful, else -1 with error message updated. */ int V4L2_Base::pwcsetframerate(struct v4l2_fract frate, char *errmsg) { int const fps = frate.denominator / frate.numerator; struct v4l2_format new_fmt = fmt; new_fmt.fmt.pix.priv |= (fps << PWC_FPS_SHIFT); if (-1 == ioctl_set_format(new_fmt, errmsg)) return errno_exit("pwcsetframerate", errmsg); frameRate = frate; return 0; } struct v4l2_fract V4L2_Base::stdgetframerate() { struct v4l2_streamparm sparm; //if (!cansetrate) return frameRate; bzero(&sparm, sizeof(struct v4l2_streamparm)); sparm.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; if (-1 == XIOCTL(fd, VIDIOC_G_PARM, &sparm)) { perror("VIDIOC_G_PARM"); } else { frameRate = sparm.parm.capture.timeperframe; } //if (!(sparm.parm.capture.capability & V4L2_CAP_TIMEPERFRAME)) cansetrate=false; return frameRate; } char *V4L2_Base::getDeviceName() { return ((char *)cap.card); } void V4L2_Base::getMaxMinSize(int &x_max, int &y_max, int &x_min, int &y_min) { x_max = xmax; y_max = ymax; x_min = xmin; y_min = ymin; } /* @brief Setting the dimensions of the capture frame. * * @param x is the width of the capture frame. * @param y is the height of the capture frame. * @return 0 if successful, else -1. */ int V4L2_Base::setSize(int x, int y) { char errmsg[ERRMSGSIZ]; struct v4l2_format new_fmt = fmt; new_fmt.fmt.pix.width = x; new_fmt.fmt.pix.height = y; if (-1 == ioctl_set_format(new_fmt, errmsg)) return -1; /* PWC bug? It seems that setting the "wrong" width and height will mess something in the driver. Only 160x120, 320x280, and 640x480 are accepted. If I try to set it for example to 300x200, it wii get set to 320x280, which is fine, but then the video information is messed up for some reason. */ // XIOCTL(fd, VIDIOC_S_FMT, &fmt); //allocBuffers(); return 0; } void V4L2_Base::setColorProcessing(bool quantization, bool colorconvert, bool linearization) { INDI_UNUSED(colorconvert); decoder->setQuantization(quantization); decoder->setLinearization(linearization); bpp = decoder->getBpp(); } unsigned char *V4L2_Base::getY() { return decoder->getY(); } unsigned char *V4L2_Base::getU() { return decoder->getU(); } unsigned char *V4L2_Base::getV() { return decoder->getV(); } /*unsigned char * V4L2_Base::getColorBuffer() { return decoder->geColorBuffer(); }*/ unsigned char *V4L2_Base::getRGBBuffer() { return decoder->getRGBBuffer(); } float *V4L2_Base::getLinearY() { return decoder->getLinearY(); } void V4L2_Base::registerCallback(WPF *fp, void *ud) { callback = fp; uptr = ud; } void V4L2_Base::findMinMax() { char errmsg[ERRMSGSIZ]; struct v4l2_format tryfmt; CLEAR(tryfmt); xmin = xmax = fmt.fmt.pix.width; ymin = ymax = fmt.fmt.pix.height; tryfmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; tryfmt.fmt.pix.width = 10; tryfmt.fmt.pix.height = 10; tryfmt.fmt.pix.pixelformat = fmt.fmt.pix.pixelformat; tryfmt.fmt.pix.field = fmt.fmt.pix.field; if (-1 == XIOCTL(fd, VIDIOC_TRY_FMT, &tryfmt)) { errno_exit("VIDIOC_TRY_FMT 1", errmsg); return; } xmin = tryfmt.fmt.pix.width; ymin = tryfmt.fmt.pix.height; tryfmt.fmt.pix.width = 1600; tryfmt.fmt.pix.height = 1200; if (-1 == XIOCTL(fd, VIDIOC_TRY_FMT, &tryfmt)) { errno_exit("VIDIOC_TRY_FMT 2", errmsg); return; } xmax = tryfmt.fmt.pix.width; ymax = tryfmt.fmt.pix.height; cerr << "Min X: " << xmin << " - Max X: " << xmax << " - Min Y: " << ymin << " - Max Y: " << ymax << endl; } void V4L2_Base::enumerate_ctrl() { char errmsg[ERRMSGSIZ]; CLEAR(queryctrl); for (queryctrl.id = V4L2_CID_BASE; queryctrl.id < V4L2_CID_LASTP1; queryctrl.id++) { if (0 == XIOCTL(fd, VIDIOC_QUERYCTRL, &queryctrl)) { if (queryctrl.flags & V4L2_CTRL_FLAG_DISABLED) { cerr << "DISABLED--Control " << queryctrl.name << endl; continue; } cerr << "Control " << queryctrl.name << endl; #if (LINUX_VERSION_CODE >= KERNEL_VERSION(3, 5, 0)) if ((queryctrl.type == V4L2_CTRL_TYPE_MENU) || (queryctrl.type == V4L2_CTRL_TYPE_INTEGER_MENU)) #else if (queryctrl.type == V4L2_CTRL_TYPE_MENU) #endif enumerate_menu(); if (queryctrl.type == V4L2_CTRL_TYPE_BOOLEAN) cerr << " boolean" << endl; if (queryctrl.type == V4L2_CTRL_TYPE_INTEGER) cerr << " integer" << endl; //if (queryctrl.type == V4L2_CTRL_TYPE_BITMASK) //not in < 3.1 // cerr << " bitmask" <= KERNEL_VERSION(3, 5, 0)) if ((queryctrl.type == V4L2_CTRL_TYPE_MENU) || (queryctrl.type == V4L2_CTRL_TYPE_INTEGER_MENU)) #else if (queryctrl.type == V4L2_CTRL_TYPE_MENU) #endif enumerate_menu(); if (queryctrl.type == V4L2_CTRL_TYPE_BOOLEAN) cerr << " boolean" << endl; if (queryctrl.type == V4L2_CTRL_TYPE_INTEGER) cerr << " integer" << endl; //if (queryctrl.type == V4L2_CTRL_TYPE_BITMASK) // cerr << " bitmask" <= KERNEL_VERSION(3, 5, 0)) if (queryctrl.type == V4L2_CTRL_TYPE_MENU) cerr << " Menu items:" << endl; if (queryctrl.type == V4L2_CTRL_TYPE_INTEGER_MENU) cerr << " Integer Menu items:" << endl; #else cerr << " Menu items:" << endl; #endif CLEAR(querymenu); querymenu.id = queryctrl.id; for (querymenu.index = queryctrl.minimum; (int)querymenu.index <= queryctrl.maximum; querymenu.index++) { if (0 == XIOCTL(fd, VIDIOC_QUERYMENU, &querymenu)) { if (queryctrl.type == V4L2_CTRL_TYPE_MENU) cerr << " " << querymenu.name << endl; #if (LINUX_VERSION_CODE >= KERNEL_VERSION(3, 5, 0)) if (queryctrl.type == V4L2_CTRL_TYPE_INTEGER_MENU) { char menuname[19]; menuname[18] = '\0'; snprintf(menuname, 19, "0x%016llX", querymenu.value); cerr << " " << menuname << endl; } #endif } //else //{ // errno_exit("VIDIOC_QUERYMENU", errmsg); // return; //} } } int V4L2_Base::query_ctrl(unsigned int ctrl_id, double &ctrl_min, double &ctrl_max, double &ctrl_step, double &ctrl_value, char *errmsg) { struct v4l2_control control; CLEAR(queryctrl); queryctrl.id = ctrl_id; if (-1 == ioctl(fd, VIDIOC_QUERYCTRL, &queryctrl)) { if (errno != EINVAL) return errno_exit("VIDIOC_QUERYCTRL", errmsg); else { cerr << "#" << ctrl_id << " is not supported" << endl; snprintf(errmsg, ERRMSGSIZ, "# %d is not supported", ctrl_id); return -1; } } else if (queryctrl.flags & V4L2_CTRL_FLAG_DISABLED) { cerr << "#" << ctrl_id << " is disabled" << endl; snprintf(errmsg, ERRMSGSIZ, "# %d is disabled", ctrl_id); return -1; } ctrl_min = queryctrl.minimum; ctrl_max = queryctrl.maximum; ctrl_step = queryctrl.step; ctrl_value = queryctrl.default_value; /* Get current value */ CLEAR(control); control.id = ctrl_id; if (0 == XIOCTL(fd, VIDIOC_G_CTRL, &control)) ctrl_value = control.value; cerr << queryctrl.name << " -- min: " << ctrl_min << " max: " << ctrl_max << " step: " << ctrl_step << " value: " << ctrl_value << endl; return 0; } void V4L2_Base::queryControls(INumberVectorProperty *nvp, unsigned int *nnumber, ISwitchVectorProperty **options, unsigned int *noptions, const char *dev, const char *group) { struct v4l2_control control; INumber *numbers = nullptr; unsigned int *num_ctrls = nullptr; int nnum = 0; ISwitchVectorProperty *opt = nullptr; unsigned int nopt = 0; char optname[] = "OPT000"; char swonname[] = "SET_OPT000"; char swoffname[] = "UNSET_OPT000"; char menuname[] = "MENU000"; char menuoptname[] = "MENU000_OPT000"; *noptions = 0; *nnumber = 0; CLEAR(queryctrl); for (queryctrl.id = V4L2_CID_BASE; queryctrl.id < V4L2_CID_LASTP1; queryctrl.id++) { if (0 == ioctl(fd, VIDIOC_QUERYCTRL, &queryctrl)) { if (queryctrl.flags & V4L2_CTRL_FLAG_DISABLED) { cerr << queryctrl.name << " is disabled." << endl; continue; } if (queryctrl.type == V4L2_CTRL_TYPE_INTEGER) { numbers = (numbers == nullptr) ? (INumber *)malloc(sizeof(INumber)) : (INumber *)realloc(numbers, (nnum + 1) * sizeof(INumber)); num_ctrls = (num_ctrls == nullptr) ? (unsigned int *)malloc(sizeof(unsigned int)) : (unsigned int *)realloc(num_ctrls, (nnum + 1) * sizeof(unsigned int)); strncpy(numbers[nnum].name, (const char *)entityXML((char *)queryctrl.name), MAXINDINAME); strncpy(numbers[nnum].label, (const char *)entityXML((char *)queryctrl.name), MAXINDILABEL); strncpy(numbers[nnum].format, "%0.f", MAXINDIFORMAT); numbers[nnum].min = queryctrl.minimum; numbers[nnum].max = queryctrl.maximum; numbers[nnum].step = queryctrl.step; numbers[nnum].value = queryctrl.default_value; /* Get current value if possible */ CLEAR(control); control.id = queryctrl.id; if (0 == XIOCTL(fd, VIDIOC_G_CTRL, &control)) numbers[nnum].value = control.value; /* Store ID info in INumber. This is the first time ever I make use of aux0!! */ num_ctrls[nnum] = queryctrl.id; cerr << "Adding " << queryctrl.name << " -- min: " << queryctrl.minimum << " max: " << queryctrl.maximum << " step: " << queryctrl.step << " value: " << numbers[nnum].value << endl; nnum++; } if (queryctrl.type == V4L2_CTRL_TYPE_BOOLEAN) { ISwitch *sw = (ISwitch *)malloc(2 * sizeof(ISwitch)); snprintf(optname + 3, 4, "%03d", nopt); snprintf(swonname + 7, 4, "%03d", nopt); snprintf(swoffname + 9, 4, "%03d", nopt); opt = (opt == nullptr) ? (ISwitchVectorProperty *)malloc(sizeof(ISwitchVectorProperty)) : (ISwitchVectorProperty *)realloc(opt, (nopt + 1) * sizeof(ISwitchVectorProperty)); CLEAR(control); control.id = queryctrl.id; XIOCTL(fd, VIDIOC_G_CTRL, &control); IUFillSwitch(sw, swonname, "Off", (control.value ? ISS_OFF : ISS_ON)); IUFillSwitch(sw + 1, swoffname, "On", (control.value ? ISS_ON : ISS_OFF)); queryctrl.name[31] = '\0'; IUFillSwitchVector(&opt[nopt], sw, 2, dev, optname, (const char *)entityXML((char *)queryctrl.name), group, IP_RW, ISR_1OFMANY, 0.0, IPS_IDLE); opt[nopt].aux = malloc(sizeof(unsigned int)); *(unsigned int *)(opt[nopt].aux) = (queryctrl.id); DEBUGFDEVICE(deviceName, INDI::Logger::DBG_DEBUG, "Adding switch %.*s (%s)\n", (int)sizeof(queryctrl.name), queryctrl.name, (control.value ? "On" : "Off")); nopt += 1; } #if (LINUX_VERSION_CODE >= KERNEL_VERSION(3, 5, 0)) if ((queryctrl.type == V4L2_CTRL_TYPE_MENU) || (queryctrl.type == V4L2_CTRL_TYPE_INTEGER_MENU)) #else if (queryctrl.type == V4L2_CTRL_TYPE_MENU) #endif { ISwitch *sw = nullptr; unsigned int nmenuopt = 0; char sname[32]; snprintf(menuname + 4, 4, "%03d", nopt); snprintf(menuoptname + 4, 4, "%03d", nopt); menuoptname[7] = '_'; opt = (opt == nullptr) ? (ISwitchVectorProperty *)malloc(sizeof(ISwitchVectorProperty)) : (ISwitchVectorProperty *)realloc(opt, (nopt + 1) * sizeof(ISwitchVectorProperty)); CLEAR(control); control.id = queryctrl.id; XIOCTL(fd, VIDIOC_G_CTRL, &control); CLEAR(querymenu); querymenu.id = queryctrl.id; for (querymenu.index = queryctrl.minimum; (int)querymenu.index <= queryctrl.maximum; querymenu.index++) { if (0 == XIOCTL(fd, VIDIOC_QUERYMENU, &querymenu)) { sw = (sw == nullptr) ? (ISwitch *)malloc(sizeof(ISwitch)) : (ISwitch *)realloc(sw, (nmenuopt + 1) * sizeof(ISwitch)); snprintf(menuoptname + 11, 4, "%03d", nmenuopt); #if (LINUX_VERSION_CODE >= KERNEL_VERSION(3, 5, 0)) if (queryctrl.type == V4L2_CTRL_TYPE_MENU) { snprintf(sname, 31, "%.*s", (int)sizeof(querymenu.name), querymenu.name); sname[31] = '\0'; } if (queryctrl.type == V4L2_CTRL_TYPE_INTEGER_MENU) { snprintf(sname, 19, "0x%016llX", querymenu.value); sname[31] = '\0'; } #else snprintf(sname, 31, "%.*s", (int)sizeof(querymenu.name), querymenu.name); sname[31] = '\0'; #endif DEBUGFDEVICE(deviceName, INDI::Logger::DBG_DEBUG, "Adding menu item %.*s %.*s item %d", (int)sizeof(sname), sname, (int)sizeof(menuoptname), menuoptname, nmenuopt); //IUFillSwitch(&sw[nmenuopt], menuoptname, (const char *)sname, (control.value==nmenuopt?ISS_ON:ISS_OFF)); IUFillSwitch(&sw[nmenuopt], menuoptname, (const char *)entityXML((char *)sname), (control.value == (int)nmenuopt ? ISS_ON : ISS_OFF)); nmenuopt += 1; } else { //errno_exit("VIDIOC_QUERYMENU", errmsg); //exit(3); } } queryctrl.name[31] = '\0'; IUFillSwitchVector(&opt[nopt], sw, nmenuopt, dev, menuname, (const char *)entityXML((char *)queryctrl.name), group, IP_RW, ISR_1OFMANY, 0.0, IPS_IDLE); opt[nopt].aux = malloc(sizeof(unsigned int)); *(unsigned int *)(opt[nopt].aux) = (queryctrl.id); DEBUGFDEVICE(deviceName, INDI::Logger::DBG_DEBUG, "Adding menu %.*s (item %d set)", (int)sizeof(queryctrl.name), queryctrl.name, control.value); nopt += 1; } } else { if (errno != EINVAL) { if (numbers) free(numbers); if (opt) free(opt); perror("VIDIOC_QUERYCTRL"); return; } } } for (queryctrl.id = V4L2_CID_PRIVATE_BASE;; queryctrl.id++) { if (0 == ioctl(fd, VIDIOC_QUERYCTRL, &queryctrl)) { if (queryctrl.flags & V4L2_CTRL_FLAG_DISABLED) { cerr << queryctrl.name << " is disabled." << endl; continue; } if (queryctrl.type == V4L2_CTRL_TYPE_INTEGER) { numbers = (numbers == nullptr) ? (INumber *)malloc(sizeof(INumber)) : (INumber *)realloc(numbers, (nnum + 1) * sizeof(INumber)); num_ctrls = (num_ctrls == nullptr) ? (unsigned int *)malloc(sizeof(unsigned int)) : (unsigned int *)realloc(num_ctrls, (nnum + 1) * sizeof(unsigned int)); strncpy(numbers[nnum].name, (const char *)entityXML((char *)queryctrl.name), MAXINDINAME); strncpy(numbers[nnum].label, (const char *)entityXML((char *)queryctrl.name), MAXINDILABEL); strncpy(numbers[nnum].format, "%0.f", MAXINDIFORMAT); numbers[nnum].min = queryctrl.minimum; numbers[nnum].max = queryctrl.maximum; numbers[nnum].step = queryctrl.step; numbers[nnum].value = queryctrl.default_value; /* Get current value if possible */ CLEAR(control); control.id = queryctrl.id; if (0 == XIOCTL(fd, VIDIOC_G_CTRL, &control)) numbers[nnum].value = control.value; /* Store ID info in INumber. This is the first time ever I make use of aux0!! */ num_ctrls[nnum] = queryctrl.id; cerr << "Adding ext. " << queryctrl.name << " -- min: " << queryctrl.minimum << " max: " << queryctrl.maximum << " step: " << queryctrl.step << " value: " << numbers[nnum].value << endl; nnum++; } if (queryctrl.type == V4L2_CTRL_TYPE_BOOLEAN) { ISwitch *sw = (ISwitch *)malloc(2 * sizeof(ISwitch)); snprintf(optname + 3, 4, "%03d", nopt); snprintf(swonname + 7, 4, "%03d", nopt); snprintf(swoffname + 9, 4, "%03d", nopt); opt = (opt == nullptr) ? (ISwitchVectorProperty *)malloc(sizeof(ISwitchVectorProperty)) : (ISwitchVectorProperty *)realloc(opt, (nopt + 1) * sizeof(ISwitchVectorProperty)); CLEAR(control); control.id = queryctrl.id; XIOCTL(fd, VIDIOC_G_CTRL, &control); IUFillSwitch(sw, swonname, "On", (control.value ? ISS_ON : ISS_OFF)); IUFillSwitch(sw + 1, swoffname, "Off", (control.value ? ISS_OFF : ISS_ON)); queryctrl.name[31] = '\0'; IUFillSwitchVector(&opt[nopt], sw, 2, dev, optname, (const char *)entityXML((char *)queryctrl.name), group, IP_RW, ISR_1OFMANY, 0.0, IPS_IDLE); opt[nopt].aux = malloc(sizeof(unsigned int)); *(unsigned int *)(opt[nopt].aux) = (queryctrl.id); DEBUGFDEVICE(deviceName, INDI::Logger::DBG_DEBUG, "Adding ext. switch %.*s (%s)\n", (int)sizeof(queryctrl.name), queryctrl.name, (control.value ? "On" : "Off")); nopt += 1; } #if (LINUX_VERSION_CODE >= KERNEL_VERSION(3, 5, 0)) if ((queryctrl.type == V4L2_CTRL_TYPE_MENU) || (queryctrl.type == V4L2_CTRL_TYPE_INTEGER_MENU)) #else if (queryctrl.type == V4L2_CTRL_TYPE_MENU) #endif { ISwitch *sw = nullptr; unsigned int nmenuopt = 0; char sname[32]; snprintf(menuname + 4, 4, "%03d", nopt); snprintf(menuoptname + 4, 4, "%03d", nopt); menuoptname[7] = '_'; opt = (opt == nullptr) ? (ISwitchVectorProperty *)malloc(sizeof(ISwitchVectorProperty)) : (ISwitchVectorProperty *)realloc(opt, (nopt + 1) * sizeof(ISwitchVectorProperty)); CLEAR(control); control.id = queryctrl.id; XIOCTL(fd, VIDIOC_G_CTRL, &control); CLEAR(querymenu); querymenu.id = queryctrl.id; for (querymenu.index = queryctrl.minimum; (int)querymenu.index <= queryctrl.maximum; querymenu.index++) { if (0 == XIOCTL(fd, VIDIOC_QUERYMENU, &querymenu)) { sw = (sw == nullptr) ? (ISwitch *)malloc(sizeof(ISwitch)) : (ISwitch *)realloc(sw, (nmenuopt + 1) * sizeof(ISwitch)); snprintf(menuoptname + 11, 4, "%03d", nmenuopt); #if (LINUX_VERSION_CODE >= KERNEL_VERSION(3, 5, 0)) if (queryctrl.type == V4L2_CTRL_TYPE_MENU) { snprintf(sname, 31, "%.*s", (int)sizeof(querymenu.name), querymenu.name); sname[31] = '\0'; } if (queryctrl.type == V4L2_CTRL_TYPE_INTEGER_MENU) { snprintf(sname, 19, "0x%016llX", querymenu.value); sname[31] = '\0'; } #else snprintf(sname, 31, "%.*s", (int)sizeof(querymenu.name), querymenu.name); sname[31] = '\0'; #endif DEBUGFDEVICE(deviceName, INDI::Logger::DBG_DEBUG, "Adding menu item %.*s %.*s item %d", (int)sizeof(sname), sname, (int)sizeof(menuoptname), menuoptname, nmenuopt); //IUFillSwitch(&sw[nmenuopt], menuoptname, (const char *)sname, (control.value==nmenuopt?ISS_ON:ISS_OFF)); IUFillSwitch(&sw[nmenuopt], menuoptname, (const char *)entityXML((char *)querymenu.name), (control.value == (int)nmenuopt ? ISS_ON : ISS_OFF)); nmenuopt += 1; } else { //errno_exit("VIDIOC_QUERYMENU", errmsg); //exit(3); } } queryctrl.name[31] = '\0'; IUFillSwitchVector(&opt[nopt], sw, nmenuopt, dev, menuname, (const char *)entityXML((char *)queryctrl.name), group, IP_RW, ISR_1OFMANY, 0.0, IPS_IDLE); opt[nopt].aux = malloc(sizeof(unsigned int)); *(unsigned int *)(opt[nopt].aux) = (queryctrl.id); DEBUGFDEVICE(deviceName, INDI::Logger::DBG_DEBUG, "Adding ext. menu %.*s (item %d set)", (int)sizeof(queryctrl.name), queryctrl.name, control.value); nopt += 1; } } else break; } /* Store numbers in aux0 */ for (int i = 0; i < nnum; i++) numbers[i].aux0 = &num_ctrls[i]; nvp->np = numbers; nvp->nnp = nnum; *nnumber = nnum; *options = opt; *noptions = nopt; } int V4L2_Base::queryINTControls(INumberVectorProperty *nvp) { struct v4l2_control control; char errmsg[ERRMSGSIZ]; CLEAR(queryctrl); INumber *numbers = nullptr; unsigned int *num_ctrls = nullptr; int nnum = 0; for (queryctrl.id = V4L2_CID_BASE; queryctrl.id < V4L2_CID_LASTP1; queryctrl.id++) { if (0 == ioctl(fd, VIDIOC_QUERYCTRL, &queryctrl)) { if (queryctrl.flags & V4L2_CTRL_FLAG_DISABLED) { cerr << queryctrl.name << " is disabled." << endl; continue; } if (queryctrl.type == V4L2_CTRL_TYPE_INTEGER) { numbers = (numbers == nullptr) ? (INumber *)malloc(sizeof(INumber)) : (INumber *)realloc(numbers, (nnum + 1) * sizeof(INumber)); num_ctrls = (num_ctrls == nullptr) ? (unsigned int *)malloc(sizeof(unsigned int)) : (unsigned int *)realloc(num_ctrls, (nnum + 1) * sizeof(unsigned int)); strncpy(numbers[nnum].name, ((char *)queryctrl.name), MAXINDINAME); strncpy(numbers[nnum].label, ((char *)queryctrl.name), MAXINDILABEL); strncpy(numbers[nnum].format, "%0.f", MAXINDIFORMAT); numbers[nnum].min = queryctrl.minimum; numbers[nnum].max = queryctrl.maximum; numbers[nnum].step = queryctrl.step; numbers[nnum].value = queryctrl.default_value; /* Get current value if possible */ CLEAR(control); control.id = queryctrl.id; if (0 == XIOCTL(fd, VIDIOC_G_CTRL, &control)) numbers[nnum].value = control.value; /* Store ID info in INumber. This is the first time ever I make use of aux0!! */ num_ctrls[nnum] = queryctrl.id; DEBUGFDEVICE(deviceName, INDI::Logger::DBG_DEBUG, "%.*s -- min: %d max: %d step: %d value: %d", (int)sizeof(queryctrl.name), queryctrl.name, queryctrl.minimum, queryctrl.maximum, queryctrl.step, numbers[nnum].value); nnum++; } } else if (errno != EINVAL) { if (numbers) free(numbers); return errno_exit("VIDIOC_QUERYCTRL", errmsg); } } for (queryctrl.id = V4L2_CID_PRIVATE_BASE;; queryctrl.id++) { if (0 == ioctl(fd, VIDIOC_QUERYCTRL, &queryctrl)) { if (queryctrl.flags & V4L2_CTRL_FLAG_DISABLED) { cerr << queryctrl.name << " is disabled." << endl; continue; } if (queryctrl.type == V4L2_CTRL_TYPE_INTEGER) { numbers = (numbers == nullptr) ? (INumber *)malloc(sizeof(INumber)) : (INumber *)realloc(numbers, (nnum + 1) * sizeof(INumber)); num_ctrls = (num_ctrls == nullptr) ? (unsigned int *)malloc(sizeof(unsigned int)) : (unsigned int *)realloc(num_ctrls, (nnum + 1) * sizeof(unsigned int)); strncpy(numbers[nnum].name, ((char *)queryctrl.name), MAXINDINAME); strncpy(numbers[nnum].label, ((char *)queryctrl.name), MAXINDILABEL); strncpy(numbers[nnum].format, "%0.f", MAXINDIFORMAT); numbers[nnum].min = queryctrl.minimum; numbers[nnum].max = queryctrl.maximum; numbers[nnum].step = queryctrl.step; numbers[nnum].value = queryctrl.default_value; /* Get current value if possible */ CLEAR(control); control.id = queryctrl.id; if (0 == XIOCTL(fd, VIDIOC_G_CTRL, &control)) numbers[nnum].value = control.value; /* Store ID info in INumber. This is the first time ever I make use of aux0!! */ num_ctrls[nnum] = queryctrl.id; nnum++; } } else break; } /* Store numbers in aux0 */ for (int i = 0; i < nnum; i++) numbers[i].aux0 = &num_ctrls[i]; nvp->np = numbers; nvp->nnp = nnum; return nnum; } int V4L2_Base::getControl(unsigned int ctrl_id, double *value, char *errmsg) { struct v4l2_control control; CLEAR(control); control.id = ctrl_id; if (-1 == XIOCTL(fd, VIDIOC_G_CTRL, &control)) return errno_exit("VIDIOC_G_CTRL", errmsg); *value = (double)control.value; return 0; } int V4L2_Base::setINTControl(unsigned int ctrl_id, double new_value, char *errmsg) { struct v4l2_control control; CLEAR(queryctrl); queryctrl.id = ctrl_id; if (-1 == ioctl(fd, VIDIOC_QUERYCTRL, &queryctrl)) return 0; if ((queryctrl.flags & V4L2_CTRL_FLAG_READ_ONLY) || (queryctrl.flags & V4L2_CTRL_FLAG_GRABBED) || (queryctrl.flags & V4L2_CTRL_FLAG_INACTIVE) || (queryctrl.flags & V4L2_CTRL_FLAG_VOLATILE)) { DEBUGFDEVICE(deviceName, INDI::Logger::DBG_WARNING, "Setting INT control %.*s will fail, currently %s%s%s%s", (int)sizeof(queryctrl.name), queryctrl.name, queryctrl.flags&V4L2_CTRL_FLAG_READ_ONLY?"read only ":"", queryctrl.flags&V4L2_CTRL_FLAG_GRABBED?"grabbed ":"", queryctrl.flags&V4L2_CTRL_FLAG_INACTIVE?"inactive ":"", queryctrl.flags&V4L2_CTRL_FLAG_VOLATILE?"volatile":""); return 0; } CLEAR(control); //cerr << "The id is " << ctrl_id << " new value is " << new_value << endl; control.id = ctrl_id; control.value = (int)new_value; if (-1 == XIOCTL(fd, VIDIOC_S_CTRL, &control)) { DEBUGFDEVICE(deviceName, INDI::Logger::DBG_ERROR, "Setting INT control %.*s failed (%s)", (int)sizeof(queryctrl.name), queryctrl.name, errmsg); return errno_exit("VIDIOC_S_CTRL", errmsg); } return 0; } int V4L2_Base::setOPTControl(unsigned int ctrl_id, unsigned int new_value, char *errmsg) { struct v4l2_control control; CLEAR(queryctrl); queryctrl.id = ctrl_id; if (-1 == ioctl(fd, VIDIOC_QUERYCTRL, &queryctrl)) return 0; if ((queryctrl.flags & V4L2_CTRL_FLAG_READ_ONLY) || (queryctrl.flags & V4L2_CTRL_FLAG_GRABBED) || (queryctrl.flags & V4L2_CTRL_FLAG_INACTIVE) || (queryctrl.flags & V4L2_CTRL_FLAG_VOLATILE)) { DEBUGFDEVICE(deviceName, INDI::Logger::DBG_DEBUG, "Setting OPT control %.*s will fail, currently %s%s%s%s", (int)sizeof(queryctrl.name), queryctrl.name, queryctrl.flags&V4L2_CTRL_FLAG_READ_ONLY?"read only ":"", queryctrl.flags&V4L2_CTRL_FLAG_GRABBED?"grabbed ":"", queryctrl.flags&V4L2_CTRL_FLAG_INACTIVE?"inactive ":"", queryctrl.flags&V4L2_CTRL_FLAG_VOLATILE?"volatile":""); return 0; } CLEAR(control); //cerr << "The id is " << ctrl_id << " new value is " << new_value << endl; control.id = ctrl_id; control.value = new_value; if (-1 == XIOCTL(fd, VIDIOC_S_CTRL, &control)) { DEBUGFDEVICE(deviceName, INDI::Logger::DBG_ERROR, "Setting INT control %.*s failed (%s)", (int)sizeof(queryctrl.name), queryctrl.name, errmsg); return errno_exit("VIDIOC_S_CTRL", errmsg); } return 0; } bool V4L2_Base::enumerate_ext_ctrl() { //struct v4l2_queryctrl queryctrl; CLEAR(queryctrl); queryctrl.id = V4L2_CTRL_FLAG_NEXT_CTRL; if (-1 == ioctl(fd, VIDIOC_QUERYCTRL, &queryctrl)) return false; queryctrl.id = V4L2_CTRL_FLAG_NEXT_CTRL; while (0 == XIOCTL(fd, VIDIOC_QUERYCTRL, &queryctrl)) { if (queryctrl.flags & V4L2_CTRL_FLAG_DISABLED) { cerr << "DISABLED--Control " << queryctrl.name << endl; queryctrl.id |= V4L2_CTRL_FLAG_NEXT_CTRL; continue; } if (queryctrl.type == V4L2_CTRL_TYPE_CTRL_CLASS) { cerr << "Control Class " << queryctrl.name << endl; queryctrl.id |= V4L2_CTRL_FLAG_NEXT_CTRL; continue; } cerr << "Control " << queryctrl.name << endl; #if (LINUX_VERSION_CODE >= KERNEL_VERSION(3, 5, 0)) if ((queryctrl.type == V4L2_CTRL_TYPE_MENU) || (queryctrl.type == V4L2_CTRL_TYPE_INTEGER_MENU)) #else if (queryctrl.type == V4L2_CTRL_TYPE_MENU) #endif enumerate_menu(); if (queryctrl.type == V4L2_CTRL_TYPE_BOOLEAN) cerr << " boolean" << endl; if (queryctrl.type == V4L2_CTRL_TYPE_INTEGER) cerr << " integer" << endl; //if (queryctrl.type == V4L2_CTRL_TYPE_BITMASK) //not in < 3.1 // cerr << " bitmask" <aux = nullptr; IUFillSwitch(sw + 1, swoffname, "On", (control.value ? ISS_ON : ISS_OFF)); (sw + 1)->aux = nullptr; queryctrl.name[31] = '\0'; IUFillSwitchVector(&opt[nopt], sw, 2, dev, optname, (const char *)entityXML((char *)queryctrl.name), group, IP_RW, ISR_1OFMANY, 0.0, IPS_IDLE); opt[nopt].aux = malloc(sizeof(unsigned int)); *(unsigned int *)(opt[nopt].aux) = (queryctrl.id); DEBUGFDEVICE(deviceName, INDI::Logger::DBG_DEBUG, "Adding switch %.*s (%s)", (int)sizeof(queryctrl.name), queryctrl.name, (control.value ? "On" : "Off")); nopt += 1; } if (queryctrl.type == V4L2_CTRL_TYPE_BUTTON) { ISwitch *sw = (ISwitch *)malloc(sizeof(ISwitch)); snprintf(optname + 3, 4, "%03d", nopt); snprintf(swonname + 7, 4, "%03d", nopt); //snprintf(swoffname+9, 4, "%03d", nopt); opt = (opt == nullptr) ? (ISwitchVectorProperty *)malloc(sizeof(ISwitchVectorProperty)) : (ISwitchVectorProperty *)realloc(opt, (nopt + 1) * sizeof(ISwitchVectorProperty)); queryctrl.name[31] = '\0'; IUFillSwitch(sw, swonname, (const char *)entityXML((char *)queryctrl.name), ISS_OFF); sw->aux = nullptr; IUFillSwitchVector(&opt[nopt], sw, 1, dev, optname, (const char *)entityXML((char *)queryctrl.name), group, IP_RW, ISR_NOFMANY, 0.0, IPS_IDLE); opt[nopt].aux = malloc(sizeof(unsigned int)); *(unsigned int *)(opt[nopt].aux) = (queryctrl.id); DEBUGFDEVICE(deviceName, INDI::Logger::DBG_DEBUG, "Adding Button %.*s", (int)sizeof(queryctrl.name), queryctrl.name); nopt += 1; } #if (LINUX_VERSION_CODE >= KERNEL_VERSION(3, 5, 0)) if ((queryctrl.type == V4L2_CTRL_TYPE_MENU) || (queryctrl.type == V4L2_CTRL_TYPE_INTEGER_MENU)) #else if (queryctrl.type == V4L2_CTRL_TYPE_MENU) #endif { ISwitch *sw = nullptr; unsigned int nmenuopt = 0; char sname[32]; snprintf(menuname + 4, 4, "%03d", nopt); snprintf(menuoptname + 4, 4, "%03d", nopt); menuoptname[7] = '_'; opt = (opt == nullptr) ? (ISwitchVectorProperty *)malloc(sizeof(ISwitchVectorProperty)) : (ISwitchVectorProperty *)realloc(opt, (nopt + 1) * sizeof(ISwitchVectorProperty)); CLEAR(control); control.id = queryctrl.id; XIOCTL(fd, VIDIOC_G_CTRL, &control); CLEAR(querymenu); querymenu.id = queryctrl.id; for (querymenu.index = queryctrl.minimum; (int)querymenu.index <= queryctrl.maximum; querymenu.index++) { if (0 == XIOCTL(fd, VIDIOC_QUERYMENU, &querymenu)) { sw = (sw == nullptr) ? (ISwitch *)malloc(sizeof(ISwitch)) : (ISwitch *)realloc(sw, (nmenuopt + 1) * sizeof(ISwitch)); snprintf(menuoptname + 11, 4, "%03d", nmenuopt); #if (LINUX_VERSION_CODE >= KERNEL_VERSION(3, 5, 0)) if (queryctrl.type == V4L2_CTRL_TYPE_MENU) { snprintf(sname, 31, "%.*s", (int)sizeof(querymenu.name), querymenu.name); sname[31] = '\0'; } if (queryctrl.type == V4L2_CTRL_TYPE_INTEGER_MENU) { snprintf(sname, 19, "0x%016llX", querymenu.value); sname[31] = '\0'; } #else snprintf(sname, 31, "%.*s", (int)sizeof(querymenu.name), querymenu.name); sname[31] = '\0'; #endif DEBUGFDEVICE(deviceName, INDI::Logger::DBG_DEBUG, "Adding menu item %.*s %.*s item %d index %d", (int)sizeof(sname), sname, (int)sizeof(menuoptname), menuoptname, nmenuopt, querymenu.index); //IUFillSwitch(&sw[nmenuopt], menuoptname, (const char *)sname, (control.value==nmenuopt?ISS_ON:ISS_OFF)); IUFillSwitch(&sw[nmenuopt], menuoptname, (const char *)entityXML((char *)querymenu.name), (control.value == (int)nmenuopt ? ISS_ON : ISS_OFF)); sw[nmenuopt].aux = malloc(sizeof(unsigned int)); *(unsigned int *)(sw[nmenuopt].aux) = (querymenu.index); nmenuopt += 1; } else { //errno_exit("VIDIOC_QUERYMENU", errmsg); //exit(3); } } queryctrl.name[31] = '\0'; IUFillSwitchVector(&opt[nopt], sw, nmenuopt, dev, menuname, (const char *)entityXML((char *)queryctrl.name), group, IP_RW, ISR_1OFMANY, 0.0, IPS_IDLE); opt[nopt].aux = malloc(sizeof(unsigned int)); *(unsigned int *)(opt[nopt].aux) = (queryctrl.id); DEBUGFDEVICE(deviceName, INDI::Logger::DBG_DEBUG, "Adding menu %.*s (item %d set)", (int)sizeof(queryctrl.name), queryctrl.name, control.value); nopt += 1; } //if (queryctrl.type == V4L2_CTRL_TYPE_INTEGER_MENU) // { // DEBUGFDEVICE(deviceName, INDI::Logger::DBG_DEBUG,"Control type not implemented\n"); //} queryctrl.id |= V4L2_CTRL_FLAG_NEXT_CTRL; } /* Store numbers in aux0 */ for (int i = 0; i < nnum; i++) numbers[i].aux0 = &num_ctrls[i]; nvp->np = numbers; nvp->nnp = nnum; *nnumber = nnum; *options = opt; *noptions = nopt; return true; } void V4L2_Base::setDeviceName(const char *name) { strncpy(deviceName, name, MAXINDIDEVICE); } } libindi/libs/webcam/ccvt_types.h0000664000175000017500000000310013263645557016207 0ustar jasemjasem/* CCVT: ColourConVerT: simple library for converting colourspaces Copyright (C) 2002 Nemosoft Unv. Email:athomas@nemsoft.co.uk This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program 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 General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA For questions, remarks, patches, etc. for this program, the author can be reached at nemosoft@smcc.demon.nl. */ #pragma once typedef struct { unsigned char b; unsigned char g; unsigned char r; unsigned char z; } PIXTYPE_bgr32; typedef struct { unsigned char b; unsigned char g; unsigned char r; } PIXTYPE_bgr24; typedef struct { unsigned char r; unsigned char g; unsigned char b; unsigned char z; } PIXTYPE_rgb32; typedef struct { unsigned char r; unsigned char g; unsigned char b; } PIXTYPE_rgb24; #define SAT(c) \ if (c & (~255)) \ { \ if (c < 0) \ c = 0; \ else \ c = 255; \ } libindi/libs/locale_compat.h0000664000175000017500000000575613263645557015413 0ustar jasemjasem/* INDI LIB Utility routines for saving and restoring the current locale, handling platform differences Copyright (C) 2017 Andy Galasso 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 */ #pragma once #include #ifdef __cplusplus extern "C" { #endif // C interface // // usage: // // locale_char_t *save = indi_locale_C_numeric_push(); // ... // indi_locale_C_numeric_pop(save); // #if defined(_MSC_VER) #include #include typedef wchar_t locale_char_t; #define INDI_LOCALE(s) L#s __inline static locale_char_t *indi_setlocale(int category, const locale_char_t *locale) { return _wcsdup(_wsetlocale(category, locale)); } __inline static void indi_restore_locale(int category, locale_char_t *prev) { _wsetlocale(category, prev); free(prev); } # define _INDI_C_INLINE __inline #else // _MSC_VER typedef char locale_char_t; #define INDI_LOCALE(s) s inline static locale_char_t *indi_setlocale(int category, const locale_char_t *locale) { return setlocale(category, locale); } inline static void indi_restore_locale(int category, locale_char_t *prev) { setlocale(category, prev); } # define _INDI_C_INLINE inline #endif // _MSC_VER _INDI_C_INLINE static locale_char_t *indi_locale_C_numeric_push() { return indi_setlocale(LC_NUMERIC, INDI_LOCALE("C")); } _INDI_C_INLINE static void indi_locale_C_numeric_pop(locale_char_t *prev) { indi_restore_locale(LC_NUMERIC, prev); } #undef _INDI_C_INLINE #ifdef __cplusplus } #endif #ifdef __cplusplus // C++ interface // // usage: // // AutoCNumeric locale; // LC_NUMERIC locale set to "C" for object scope // ... // class AutoLocale { int m_category; locale_char_t *m_orig; public: AutoLocale(int category, const locale_char_t *locale) : m_category(category) { m_orig = indi_setlocale(category, locale); } // method Restore can be used to restore the original locale // before the object goes out of scope void Restore() { if (m_orig) { indi_restore_locale(m_category, m_orig); m_orig = nullptr; } } ~AutoLocale() { Restore(); } }; class AutoCNumeric : public AutoLocale { public: AutoCNumeric() : AutoLocale(LC_NUMERIC, INDI_LOCALE("C")) { } }; #endif // __cplusplus libindi/libs/lilxml.h0000664000175000017500000002467613263645557014114 0ustar jasemjasem#if 0 liblilxml Copyright (C) 2003 Elwood C. Downey 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 /** \file lilxml.h \brief A little DOM-style library to handle parsing and processing an XML file. It only handles elements, attributes and pcdata content. and are silently ignored. pcdata is collected into one string, sans leading whitespace first line. \n The following is an example of a cannonical usage for the lilxml library. Initialize a lil xml context and read an XML file in a root element. \code #include LilXML *lp = newLilXML(); char errmsg[1024]; XMLEle *root, *ep; int c; while ((c = fgetc(stdin)) != EOF) { root = readXMLEle (lp, c, errmsg); if (root) break; if (errmsg[0]) error ("Error: %s\n", errmsg); } // print the tag and pcdata content of each child element within the root for (ep = nextXMLEle (root, 1); ep != NULL; ep = nextXMLEle (root, 0)) printf ("%s: %s\n", tagXMLEle(ep), pcdataXMLEle(ep)); // finished with root element and with lil xml context delXMLEle (root); delLilXML (lp); \endcode */ #pragma once #include #ifdef __cplusplus extern "C" { #endif /* opaque handle types */ typedef struct _xml_att XMLAtt; typedef struct _xml_ele XMLEle; typedef struct _LilXML LilXML; /** * \defgroup lilxmlFunctions XML Functions: Functions to parse, process, and search XML. */ /*@{*/ /* creation and destruction functions */ /** \brief Create a new lilxml parser. \return a pointer to the lilxml parser on success. NULL on failure. */ extern LilXML *newLilXML(); /** \brief Delete a lilxml parser. \param lp a pointer to a lilxml parser to be deleted. */ extern void delLilXML(LilXML *lp); /** \brief Delete an XML element. \return a pointer to the XML Element to be deleted. */ extern void delXMLEle(XMLEle *e); /** \brief Process an XML chunk. \param lp a pointer to a lilxml parser. \param buf buffer to process. \param size size of buf \param errmsg a buffer to store error messages if an error in parsing is encountered. \return return a pointer to a NULL terminated array of parsed XML elements. An array of size 1 with on a NULL element means there is nothing to parse or a parsing is still in progress. A NULL pointer may be returned if a parsing error occurs. Check errmsg for errors if NULL is returned. */ extern XMLEle **parseXMLChunk(LilXML *lp, char *buf, int size, char errmsg[]); /** \brief Process an XML one char at a time. \param lp a pointer to a lilxml parser. \param c one character to process. \param errmsg a buffer to store error messages if an error in parsing is encounterd. \return When the function parses a complete valid XML element, it will return a pointer to the XML element. A NULL is returned when parsing the element is still in progress, or if a parsing error occurs. Check errmsg for errors if NULL is returned. */ extern XMLEle *readXMLEle(LilXML *lp, int c, char errmsg[]); /* search functions */ /** \brief Find an XML attribute within an XML element. \param e a pointer to the XML element to search. \param name the attribute name to search for. \return A pointer to the XML attribute if found or NULL on failure. */ extern XMLAtt *findXMLAtt(XMLEle *e, const char *name); /** \brief Find an XML element within an XML element. \param e a pointer to the XML element to search. \param tag the element tag to search for. \return A pointer to the XML element if found or NULL on failure. */ extern XMLEle *findXMLEle(XMLEle *e, const char *tag); /* iteration functions */ /** \brief Iterate an XML element for a list of nesetd XML elements. \param ep a pointer to the XML element to iterate. \param first the index of the starting XML element. Pass 1 to start iteration from the beginning of the XML element. Pass 0 to get the next element thereater. \return On success, a pointer to the next XML element is returned. NULL when there are no more elements. */ extern XMLEle *nextXMLEle(XMLEle *ep, int first); /** \brief Iterate an XML element for a list of XML attributes. \param ep a pointer to the XML element to iterate. \param first the index of the starting XML attribute. Pass 1 to start iteration from the beginning of the XML element. Pass 0 to get the next attribute thereater. \return On success, a pointer to the next XML attribute is returned. NULL when there are no more attributes. */ extern XMLAtt *nextXMLAtt(XMLEle *ep, int first); /* tree functions */ /** \brief Return the parent of an XML element. \return a pointer to the XML element parent. */ extern XMLEle *parentXMLEle(XMLEle *ep); /** \brief Return the parent of an XML attribute. \return a pointer to the XML element parent. */ extern XMLEle *parentXMLAtt(XMLAtt *ap); /* access functions */ /** \brief Return the tag of an XML element. \param ep a pointer to an XML element. \return the tag string. */ extern char *tagXMLEle(XMLEle *ep); /** \brief Return the pcdata of an XML element. \param ep a pointer to an XML element. \return the pcdata string on success. */ extern char *pcdataXMLEle(XMLEle *ep); /** \brief Return the name of an XML attribute. \param ap a pointer to an XML attribute. \return the name string of the attribute. */ extern char *nameXMLAtt(XMLAtt *ap); /** \brief Return the value of an XML attribute. \param ap a pointer to an XML attribute. \return the value string of the attribute. */ extern char *valuXMLAtt(XMLAtt *ap); /** \brief Return the number of characters in pcdata in an XML element. \param ep a pointer to an XML element. \return the length of the pcdata string. */ extern int pcdatalenXMLEle(XMLEle *ep); /** \brief Return the number of nested XML elements in a parent XML element. \param ep a pointer to an XML element. \return the number of nested XML elements. */ extern int nXMLEle(XMLEle *ep); /** \brief Return the number of XML attributes in a parent XML element. \param ep a pointer to an XML element. \return the number of XML attributes within the XML element. */ extern int nXMLAtt(XMLEle *ep); /* editing functions */ /** \brief add an element with the given tag to the given element. parent can be NULL to make a new root. \return if parent is NULL, a new root is returned, otherwise, parent is returned. */ extern XMLEle *addXMLEle(XMLEle *parent, const char *tag); /** \brief set the pcdata of the given element \param ep pointer to an XML element. \param pcdata pcdata to set. */ extern void editXMLEle(XMLEle *ep, const char *pcdata); /** \brief Add an XML attribute to an existing XML element. \param ep pointer to an XML element \param name the name of the XML attribute to add. \param value the value of the XML attribute to add. */ extern XMLAtt *addXMLAtt(XMLEle *ep, const char *name, const char *value); /** \brief Remove an XML attribute from an XML element. \param ep pointer to an XML element. \param name the name of the XML attribute to remove */ extern void rmXMLAtt(XMLEle *ep, const char *name); /** \brief change the value of an attribute to str. * \param ap pointer to XML attribute * \param str new attribute value */ extern void editXMLAtt(XMLAtt *ap, const char *str); /** \brief return a string with all xml-sensitive characters within the passed string replaced with their entity sequence equivalents. * N.B. caller must use the returned string before calling us again. */ extern char *entityXML(char *str); /* convenience functions */ /** \brief Find an XML element's attribute value. \param ep a pointer to an XML element. \param name the name of the XML attribute to retrieve its value. \return the value string of an XML element on success. NULL on failure. */ extern const char *findXMLAttValu(XMLEle *ep, const char *name); /** \brief Handy wrapper to read one xml file. \param fp pointer to FILE to read. \param lp pointer to lilxml parser. \param errmsg a buffer to store error messages on failure. \return root element else NULL with report in errmsg[]. */ extern XMLEle *readXMLFile(FILE *fp, LilXML *lp, char errmsg[]); /** \brief Print an XML element. \param fp a pointer to FILE where the print output is directed. \param e the XML element to print. \param level the printing level, set to 0 to print the whole element. */ extern void prXMLEle(FILE *fp, XMLEle *e, int level); /** \brief sample print ep to string s. * N.B. s must be at least as large as that reported by sprlXMLEle()+1. * N.B. set level = 0 on first call. * \return return length of resulting string (sans trailing @\0@) */ extern int sprXMLEle(char *s, XMLEle *ep, int level); /** \brief return number of bytes in a string guaranteed able to hold result of sprXLMEle(ep) (sans trailing @\0@). * N.B. set level = 0 on first call. */ extern int sprlXMLEle(XMLEle *ep, int level); /* install alternatives to malloc/realloc/free */ extern void indi_xmlMalloc(void *(*newmalloc)(size_t size), void *(*newrealloc)(void *ptr, size_t size), void (*newfree)(void *ptr)); /*@}*/ #ifdef __cplusplus } #endif /* examples. initialize a lil xml context and read an XML file in a root element LilXML *lp = newLilXML(); char errmsg[1024]; XMLEle *root, *ep; int c; while ((c = fgetc(stdin)) != EOF) { root = readXMLEle (lp, c, errmsg); if (root) break; if (errmsg[0]) error ("Error: %s\n", errmsg); } print the tag and pcdata content of each child element within the root for (ep = nextXMLEle (root, 1); ep != NULL; ep = nextXMLEle (root, 0)) printf ("%s: %s\n", tagXMLEle(ep), pcdataXMLEle(ep)); finished with root element and with lil xml context delXMLEle (root); delLilXML (lp); */ /* For RCS Only -- Do Not Edit * @(#) $RCSfile$ $Date: 2007-09-17 16:34:48 +0300 (Mon, 17 Sep 2007) $ $Revision: 713418 $ $Name: $ */ libindi/libs/indibase/0000775000175000017500000000000013263645557014201 5ustar jasemjasemlibindi/libs/indibase/indibasetypes.h0000664000175000017500000000470413263645557017222 0ustar jasemjasem/******************************************************************************* Copyright(c) 2011 Jasem Mutlaq. All rights reserved. This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2 as published by the Free Software Foundation. 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 Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. *******************************************************************************/ #pragma once /*! INDI property type */ typedef enum { INDI_NUMBER, /*!< INumberVectorProperty. */ INDI_SWITCH, /*!< ISwitchVectorProperty. */ INDI_TEXT, /*!< ITextVectorProperty. */ INDI_LIGHT, /*!< ILightVectorProperty. */ INDI_BLOB, /*!< IBLOBVectorProperty. */ INDI_UNKNOWN } INDI_PROPERTY_TYPE; /*! INDI Equatorial Axis type */ typedef enum { AXIS_RA, /*!< Right Ascension Axis. */ AXIS_DE /*!< Declination Axis. */ } INDI_EQ_AXIS; /*! INDI Horizontal Axis type */ typedef enum { AXIS_AZ, /*!< Azimuth Axis. */ AXIS_ALT /*!< Altitude Axis. */ } INDI_HO_AXIS; /*! North/South Direction type */ typedef enum { DIRECTION_NORTH = 0, /*!< North direction */ DIRECTION_SOUTH /*!< South direction */ } INDI_DIR_NS; /*! West/East Direction type */ typedef enum { DIRECTION_WEST = 0, /*!< West direction */ DIRECTION_EAST /*!< East direction */ } INDI_DIR_WE; /*! INDI Error Types */ typedef enum { INDI_DEVICE_NOT_FOUND = -1, /*!< Device not found error */ INDI_PROPERTY_INVALID = -2, /*!< Property invalid error */ INDI_PROPERTY_DUPLICATED = -3, /*!< Property duplicated error */ INDI_DISPATCH_ERROR = -4 /*!< Dispatch error */ } INDI_ERROR_TYPE; typedef enum { INDI_MONO = 0, INDI_BAYER_RGGB = 8, INDI_BAYER_GRBG = 9, INDI_BAYER_GBRG = 10, INDI_BAYER_BGGR = 11, INDI_BAYER_CYYM = 16, INDI_BAYER_YCMY = 17, INDI_BAYER_YMCY = 18, INDI_BAYER_MYYC = 19, INDI_RGB = 100, INDI_BGR = 101, INDI_JPG = 200, } INDI_PIXEL_FORMAT; libindi/libs/indibase/baseclient.h0000664000175000017500000002540513263645557016471 0ustar jasemjasem/******************************************************************************* Copyright(c) 2011 Jasem Mutlaq. All rights reserved. This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2 as published by the Free Software Foundation. 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 Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. *******************************************************************************/ #pragma once #include "indiapi.h" #include "indibase.h" #include #include #include #ifdef _WINDOWS #include #endif #define MAXRBUF 2048 /** * \class INDI::BaseClient \brief Class to provide basic client functionality. BaseClient enables accelerated development of INDI Clients by providing a framework that facilitates communication, device handling, and event notification. By subclassing BaseClient, clients can quickly connect to an INDI server, and query for a set of INDI::BaseDevice devices, and read and write properties seamlessly. Event driven programming is possible due to notifications upon reception of new devices or properties. Upon connecting to an INDI server, it creates a dedicated thread to handle all incoming traffic. The thread is terminated when disconnectServer() is called or when a communication error occurs. \attention All notifications functions defined in INDI::BaseMediator must be implemented in the client class even if they are not used because these are pure virtual functions. \see INDI Client Tutorial for more details. \author Jasem Mutlaq */ class INDI::BaseClient : public INDI::BaseMediator { public: BaseClient(); virtual ~BaseClient(); /** \brief Set the server host name and port \param hostname INDI server host name or IP address. \param port INDI server port. */ void setServer(const char *hostname, unsigned int port); /** \brief Add a device to the watch list. A client may select to receive notifications of only a specific device or a set of devices. If the client encounters any of the devices set via this function, it will create a corresponding INDI::BaseDevice object to handle them. If no devices are watched, then all devices owned by INDI server will be created and handled. */ void watchDevice(const char *deviceName); /** \brief Connect to INDI server. \returns True if the connection is successful, false otherwise. \note This function blocks until connection is either successull or unsuccessful. */ bool connectServer(); /** \brief Disconnect from INDI server. Disconnects from INDI servers. Any devices previously created will be deleted and memory cleared. \return True if disconnection is successful, false otherwise. */ bool disconnectServer(); bool isServerConnected() const; /** \brief Connect to INDI driver \param deviceName Name of the device to connect to. */ void connectDevice(const char *deviceName); /** \brief Disconnect INDI driver \param deviceName Name of the device to disconnect. */ void disconnectDevice(const char *deviceName); /** \param deviceName Name of device to search for in the list of devices owned by INDI server, \returns If \e deviceName exists, it returns an instance of the device. Otherwise, it returns NULL. */ INDI::BaseDevice *getDevice(const char *deviceName); /** \returns Returns a vector of all devices created in the client. */ const std::vector &getDevices() const { return cDevices; } /** * @brief getDevices Returns list of devices that belong to a particular @ref INDI::BaseDevice::DRIVER_INTERFACE "DRIVER_INTERFACE" class. * * For example, to get a list of guide cameras: @code{.cpp} std::vector guideCameras; getDevices(guideCameras, CCD_INTERFACE | GUIDE_INTERFACE); for (INDI::BaseDevice *device : guideCameras) cout << "Guide Camera Name: " << device->getDeviceName(); @endcode * @param deviceList Supply device list to be filled by the function. * @param driverInterface ORed DRIVER_INTERFACE values to select the desired class of devices. * @return True if one or more devices are found for the supplied driverInterface, false if no matching devices found. */ bool getDevices(std::vector &deviceList, uint16_t driverInterface); /** \brief Set Binary Large Object policy mode Set the BLOB handling mode for the client. The client may either receive:
  • Only BLOBS
  • BLOBs mixed with normal messages
  • Normal messages only, no BLOBs
If \e dev and \e prop are supplied, then the BLOB handling policy is set for this particular device and property. if \e prop is NULL, then the BLOB policy applies to the whole device. \param blobH BLOB handling policy \param dev name of device, required. \param prop name of property, optional. */ void setBLOBMode(BLOBHandling blobH, const char *dev, const char *prop = NULL); /** * @brief getBLOBMode Get Binary Large Object policy mode IF set previously by setBLOBMode * @param dev name of device. * @param prop property name, can be NULL to return overall device policy if it exists. * @return BLOB Policy, if not found, it always returns B_ALSO */ BLOBHandling getBLOBMode(const char *dev, const char *prop = NULL); // Update static void *listenHelper(void *context); const char *getHost() { return cServer.c_str(); } int getPort() { return cPort; } /** \brief Send new Text command to server */ void sendNewText(ITextVectorProperty *pp); /** \brief Send new Text command to server */ void sendNewText(const char *deviceName, const char *propertyName, const char *elementName, const char *text); /** \brief Send new Number command to server */ void sendNewNumber(INumberVectorProperty *pp); /** \brief Send new Number command to server */ void sendNewNumber(const char *deviceName, const char *propertyName, const char *elementName, double value); /** \brief Send new Switch command to server */ void sendNewSwitch(ISwitchVectorProperty *pp); /** \brief Send new Switch command to server */ void sendNewSwitch(const char *deviceName, const char *propertyName, const char *elementName); /** \brief Send opening tag for BLOB command to server */ void startBlob(const char *devName, const char *propName, const char *timestamp); /** \brief Send ONE blob content to server. The BLOB data in raw binary format and will be converted to base64 and sent to server */ void sendOneBlob(IBLOB *bp); /** \brief Send ONE blob content to server. The BLOB data in raw binary format and will be converted to base64 and sent to server */ void sendOneBlob(const char *blobName, unsigned int blobSize, const char *blobFormat, void *blobBuffer); /** \brief Send closing tag for BLOB command to server */ void finishBlob(); /** * @brief setVerbose Set verbose mode * @param enable If true, enable FULL verbose output. Any XML message received, including BLOBs, are printed on * standard output. Only use this for debugging purposes. */ void setVerbose(bool enable) { verbose = enable; } /** * @brief isVerbose Is client in verbose mode? * @return Is client in verbose mode? */ bool isVerbose() const { return verbose; } /** * @brief setConnectionTimeout Set connection timeout. By default it is 3 seconds. * @param seconds seconds * @param microseconds microseconds */ void setConnectionTimeout(uint32_t seconds, uint32_t microseconds) { timeout_sec = seconds; timeout_us = microseconds; } protected: /** \brief Dispatch command received from INDI server to respective devices handled by the client */ int dispatchCommand(XMLEle *root, char *errmsg); /** \brief Remove device */ int deleteDevice(const char *devName, char *errmsg); /** \brief Delete property command */ int delPropertyCmd(XMLEle *root, char *errmsg); /** \brief Find and return a particular device */ INDI::BaseDevice *findDev(const char *devName, char *errmsg); /** \brief Add a new device */ INDI::BaseDevice *addDevice(XMLEle *dep, char *errmsg); /** \brief Find a device, and if it doesn't exist, create it if create is set to 1 */ INDI::BaseDevice *findDev(XMLEle *root, int create, char *errmsg); /** Process messages */ int messageCmd(XMLEle *root, char *errmsg); /** * @brief newUniversalMessage Universal messages are sent from INDI server without a specific device. It is addressed to the client overall. * @param message content of message. * @note The default implementation simply logs the message to stderr. Override to handle the message. */ virtual void newUniversalMessage(std::string message); private: typedef struct { std::string device; std::string property; BLOBHandling blobMode; } BLOBMode; BLOBMode *findBLOBMode(const std::string& device, const std::string& property); /** \brief Connect/Disconnect to INDI driver \param status If true, the client will attempt to turn on CONNECTION property within the driver (i.e. turn on the device). Otherwise, CONNECTION will be turned off. \param deviceName Name of the device to connect to. */ void setDriverConnection(bool status, const char *deviceName); /** * @brief clear Clear devices and blob modes */ void clear(); std::thread *listen_thread=nullptr; #ifdef _WINDOWS SOCKET sockfd; #else int sockfd; int m_receiveFd; int m_sendFd; #endif // Listen to INDI server and process incoming messages void listenINDI(); void sendString(const char *fmt, ...); std::vector cDevices; std::vector cDeviceNames; std::vector blobModes; std::string cServer; unsigned int cPort; bool sConnected; bool verbose; // Parse & FILE buffers for IO LilXML *lillp; /* XML parser context */ uint32_t timeout_sec, timeout_us; }; libindi/libs/indibase/indidetector.h0000664000175000017500000004257513263645557017044 0ustar jasemjasem/******************************************************************************* Copyright(c) 2010, 2017 Ilia Platone, Jasem Mutlaq. All rights reserved. This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2 as published by the Free Software Foundation. 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 Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. *******************************************************************************/ #pragma once #include "defaultdevice.h" #include #include #include #include extern const char *CAPTURE_SETTINGS_TAB; extern const char *CAPTURE_INFO_TAB; extern const char *GUIDE_HEAD_TAB; /** * @brief The DetectorDevice class provides functionality of a Detector Device within a Detector. */ class DetectorDevice { public: DetectorDevice(); ~DetectorDevice(); typedef enum { DETECTOR_SAMPLERATE, DETECTOR_FREQUENCY, DETECTOR_BITSPERSAMPLE, } DETECTOR_INFO_INDEX; typedef enum { DETECTOR_BLOB_CONTINUUM, DETECTOR_BLOB_SPECTRUM, } DETECTOR_BLOB_INDEX; /** * @brief getBPS Get Detector depth (bits per sample). * @return bits per sample. */ inline int getBPS() { return BPS; } /** * @brief getContinuumBufferSize Get allocated continuum buffer size to hold the Detector captured stream. * @return allocated continuum buffer size to hold the Detector capture stream. */ inline int getContinuumBufferSize() { return ContinuumBufferSize; } /** * @brief getSpectrumBufferSize Get allocated spectrum buffer size to hold the Detector spectrum. * @return allocated spectrum buffer size (in doubles) to hold the Detector spectrum. */ inline int getSpectrumBufferSize() { return SpectrumBufferSize; } /** * @brief getCaptureLeft Get Capture time left in seconds. * @return Capture time left in seconds. */ inline double getCaptureLeft() { return FramedCaptureN[0].value; } /** * @brief getSampleRate Get requested SampleRate for the Detector device in Hz. * @return requested SampleRate for the Detector device in Hz. */ inline double getSampleRate() { return samplerate; } /** * @brief getSamplingFrequency Get requested Capture frequency for the Detector device in Hz. * @return requested Capture frequency for the Detector device in Hz. */ inline double getFrequency() { return Frequency; } /** * @brief getCaptureDuration Get requested Capture duration for the Detector device in seconds. * @return requested Capture duration for the Detector device in seconds. */ inline double getCaptureDuration() { return captureDuration; } /** * @brief getCaptureStartTime * @return Capture start time in ISO 8601 format. */ const char *getCaptureStartTime(); /** * @brief getContinuumBuffer Get raw buffer of the continuum stream of the Detector device. * @return raw continuum buffer of the Detector device. */ inline uint8_t *getContinuumBuffer() { return ContinuumBuffer; } /** * @brief getSpectrumBuffer Get raw buffer of the spectrum of the Detector device. * @return raw continuum buffer of the Detector device. */ inline double *getSpectrumBuffer() { return SpectrumBuffer; } /** * @brief setContinuumBuffer Set raw frame buffer pointer. * @param buffer pointer to continuum buffer * /note Detector Device allocates the frame buffer internally once SetContinuumBufferSize is called * with allocMem set to true which is the default behavior. If you allocated the memory * yourself (i.e. allocMem is false), then you must call this function to set the pointer * to the raw frame buffer. */ void setContinuumBuffer(uint8_t *buffer) { ContinuumBuffer = buffer; } /** * @brief setSpectrumBuffer Set raw frame buffer pointer. * @param buffer pointer to spectrum buffer * /note Detector Device allocates the frame buffer internally once SetSpectrumBufferSize is called * with allocMem set to true which is the default behavior. If you allocated the memory * yourself (i.e. allocMem is false), then you must call this function to set the pointer * to the raw frame buffer. */ void setSpectrumBuffer(double *buffer) { SpectrumBuffer = buffer; } /** * @brief Return Detector Info Property */ INumberVectorProperty *getDetectorSettings() { return &DetectorSettingsNP; } /** * @brief setMinMaxStep for a number property element * @param property Property name * @param element Element name * @param min Minimum element value * @param max Maximum element value * @param step Element step value * @param sendToClient If true (default), the element limits are updated and is sent to the * client. If false, the element limits are updated without getting sent to the client. */ void setMinMaxStep(const char *property, const char *element, double min, double max, double step, bool sendToClient = true); /** * @brief setContinuumBufferSize Set desired continuum buffer size. The function will allocate memory * accordingly. The frame size depends on the desired capture time, sampling frequency, and * sample depth of the Detector device (bps). You must set the frame size any time any of * the prior parameters gets updated. * @param nbuf size of buffer in bytes. * @param allocMem if True, it will allocate memory of nbut size bytes. */ void setContinuumBufferSize(int nbuf, bool allocMem = true); /** * @brief setSpectrumBufferSize Set desired spectrum buffer size. The function will allocate memory * accordingly. The frame size depends on the size of the spectrum. You must set the frame size any * time the spectrum size changes. * @param nbuf size of buffer in doubles. * @param allocMem if True, it will allocate memory of nbut size doubles. */ void setSpectrumBufferSize(int nbuf, bool allocMem = true); /** * @brief setSampleRate Set depth of Detector device. * @param bpp bits per pixel */ void setSampleRate(float sr); /** * @brief setFrequency Set the frequency observed. * @param capfreq Capture frequency */ void setFrequency(float freq); /** * @brief setBPP Set depth of Detector device. * @param bpp bits per pixel */ void setBPS(int bps); /** * @brief setCaptureDuration Set desired Detector frame Capture duration for next Capture. You must * call this function immediately before starting the actual Capture as it is used to calculate * the timestamp used for the FITS header. * @param duration Capture duration in seconds. */ void setCaptureDuration(double duration); /** * @brief setCaptureLeft Update Capture time left. Inform the client of the new Capture time * left value. * @param duration Capture duration left in seconds. */ void setCaptureLeft(double duration); /** * @brief setCaptureFailed Alert the client that the Capture failed. */ void setCaptureFailed(); /** * @return Get number of FITS axis in capture. By default 2 */ int getNAxis() const; /** * @brief setNAxis Set FITS number of axis * @param value number of axis */ void setNAxis(int value); /** * @brief setCaptureExtension Set capture exntension * @param ext extension (fits, jpeg, raw..etc) */ void setCaptureExtension(const char *ext); /** * @return Return capture extension (fits, jpeg, raw..etc) */ char *getCaptureExtension() { return captureExtention; } /** * @return True if Detector is currently exposing, false otherwise. */ bool isCapturing() { return (FramedCaptureNP.s == IPS_BUSY); } private: /// # of Axis int NAxis; /// Bytes per Sample int BPS; double samplerate; double Frequency; uint8_t *ContinuumBuffer; int ContinuumBufferSize; double *SpectrumBuffer; int SpectrumBufferSize; double captureDuration; timeval startCaptureTime; char captureExtention[MAXINDIBLOBFMT]; INumberVectorProperty FramedCaptureNP; INumber FramedCaptureN[1]; INumberVectorProperty DetectorSettingsNP; INumber DetectorSettingsN[4]; ISwitchVectorProperty AbortCaptureSP; ISwitch AbortCaptureS[1]; IBLOB FitsB[2]; IBLOBVectorProperty FitsBP; friend class INDI::Detector; }; /** * \class INDI::Detector * \brief Class to provide general functionality of Monodimensional Detector. * * The Detector capabilities must be set to select which features are exposed to the clients. * SetDetectorCapability() is typically set in the constructor or initProperties(), but can also be * called after connection is established with the Detector, but must be called /em before returning * true in Connect(). * * Developers need to subclass INDI::Detector to implement any driver for Detectors within INDI. * * \example Detector Simulator * \author Jasem Mutlaq, Ilia Platone * */ namespace INDI { class Detector : public DefaultDevice { public: Detector(); virtual ~Detector(); enum { DETECTOR_CAN_ABORT = 1 << 0, /*!< Can the Detector Capture be aborted? */ DETECTOR_HAS_SHUTTER = 1 << 1, /*!< Does the Detector have a mechanical shutter? */ DETECTOR_HAS_COOLER = 1 << 2, /*!< Does the Detector have a cooler and temperature control? */ DETECTOR_HAS_CONTINUUM = 1 << 3, /*!< Does the Detector support live streaming? */ DETECTOR_HAS_SPECTRUM = 1 << 4, /*!< Does the Detector support spectrum analysis? */ } DetectorCapability; virtual bool initProperties(); virtual bool updateProperties(); virtual void ISGetProperties(const char *dev); virtual bool ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n); virtual bool ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n); virtual bool ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n); virtual bool ISSnoopDevice(XMLEle *root); protected: /** * @brief GetDetectorCapability returns the Detector capabilities. */ uint32_t GetDetectorCapability() const { return capability; } /** * @brief SetDetectorCapability Set the Detector capabilities. Al fields must be initilized. * @param cap pointer to DetectorCapability struct. */ void SetDetectorCapability(uint32_t cap); /** * @return True if Detector can abort Capture. False otherwise. */ bool CanAbort() { return capability & DETECTOR_CAN_ABORT; } /** * @return True if Detector has mechanical or electronic shutter. False otherwise. */ bool HasShutter() { return capability & DETECTOR_HAS_SHUTTER; } /** * @return True if Detector has cooler and temperature can be controlled. False otherwise. */ bool HasCooler() { return capability & DETECTOR_HAS_COOLER; } /** * @return True if the Detector supports live streaming. False otherwise. */ bool HasContinuum() { return capability & DETECTOR_HAS_CONTINUUM; } /** * @return True if the Detector supports live streaming. False otherwise. */ bool HasSpectrum() { return capability & DETECTOR_HAS_SPECTRUM; } /** * @brief Set Detector temperature * @param temperature Detector temperature in degrees celsius. * @return 0 or 1 if setting the temperature call to the hardware is successful. -1 if an * error is encountered. * Return 0 if setting the temperature to the requested value takes time. * Return 1 if setting the temperature to the requested value is complete. * \note Upon returning 0, the property becomes BUSY. Once the temperature reaches the requested * value, change the state to OK. * \note This function is not implemented in Detector, it must be implemented in the child class */ virtual int SetTemperature(double temperature); /** * \brief Start capture from the Detector device * \param duration Duration in seconds * \return true if OK and Capture will take some time to complete, false on error. * \note This function is not implemented in Detector, it must be implemented in the child class */ virtual bool StartCapture(float duration); /** * \brief Set common capture params * \param sr Detector samplerate (in Hz) * \param cfreq Capture frequency of the detector (Hz, observed frequency). * \param sfreq Sampling frequency of the detector (Hz, electronic speed of the detector). * \param bps Bit resolution of a single sample. * \return true if OK and Capture will take some time to complete, false on error. * \note This function is not implemented in Detector, it must be implemented in the child class */ virtual bool CaptureParamsUpdated(float sr, float freq, float bps); /** * \brief Uploads target Device exposed buffer as FITS to the client. Dervied classes should class * this function when an Capture is complete. * @param targetDevice device that contains upload capture data * \note This function is not implemented in Detector, it must be implemented in the child class */ virtual bool CaptureComplete(DetectorDevice *targetDevice); /** * \brief Abort ongoing Capture * \return true is abort is successful, false otherwise. * \note This function is not implemented in Detector, it must be implemented in the child class */ virtual bool AbortCapture(); /** * \brief Setup Detector parameters for the Detector. Child classes call this function to update * Detector parameters * \param samplerate Detector samplerate (in Hz) * \param freq Center frequency of the detector (Hz, observed frequency). * \param bps Bit resolution of a single sample. */ virtual void SetDetectorParams(float samplerate, float freq, float bps); /** * \brief Add FITS keywords to a fits file * \param fptr pointer to a valid FITS file. * \param targetDevice The target device to extract the keywords from. * \param blobIndex The blob index of this FITS (0: continuum, 1: spectrum). * \note In additional to the standard FITS keywords, this function write the following * keywords the FITS file: *
    *
  • EXPTIME: Total Capture Time (s)
  • *
  • DATAMIN: Minimum value
  • *
  • DATAMAX: Maximum value
  • *
  • INSTRUME: Detector Name
  • *
  • DATE-OBS: UTC start date of observation
  • *
* * To add additional information, override this function in the child class and ensure to call * Detector::addFITSKeywords. */ virtual void addFITSKeywords(fitsfile *fptr, DetectorDevice *targetDevice, int blobIndex); /** A function to just remove GCC warnings about deprecated conversion */ void fits_update_key_s(fitsfile *fptr, int type, std::string name, void *p, std::string explanation, int *status); /** * @brief activeDevicesUpdated Inform children that ActiveDevices property was updated so they can * snoop on the updated devices if desired. */ virtual void activeDevicesUpdated() {} /** * @brief saveConfigItems Save configuration items in XML file. * @param fp pointer to file to write to * @return True if successful, false otherwise */ virtual bool saveConfigItems(FILE *fp); double RA, Dec; double primaryAperture; double primaryFocalLength; bool InCapture; bool AutoLoop; bool SendCapture; bool ShowMarker; float CaptureTime; // Sky Quality double MPSAS; std::vector FilterNames; int CurrentFilterSlot; DetectorDevice PrimaryDetector; // We are going to snoop these from a telescope INumberVectorProperty EqNP; INumber EqN[2]; ITextVectorProperty ActiveDeviceTP; IText ActiveDeviceT[4] {}; INumber TemperatureN[1]; INumberVectorProperty TemperatureNP; IText FileNameT[1] {}; ITextVectorProperty FileNameTP; ISwitch DatasetS[1]; ISwitchVectorProperty DatasetSP; ISwitch UploadS[3]; ISwitchVectorProperty UploadSP; IText UploadSettingsT[2] {}; ITextVectorProperty UploadSettingsTP; enum { UPLOAD_DIR, UPLOAD_PREFIX }; ISwitch TelescopeTypeS[2]; ISwitchVectorProperty TelescopeTypeSP; enum { TELESCOPE_PRIMARY }; // FITS Header IText FITSHeaderT[2] {}; ITextVectorProperty FITSHeaderTP; enum { FITS_OBSERVER, FITS_OBJECT }; private: uint32_t capability; bool uploadFile(DetectorDevice *targetDevice, const void *fitsData, size_t totalBytes, bool sendCapture, bool saveCapture, int blobindex); void getMinMax(double *min, double *max, uint8_t *buf, int len, int bpp); int getFileIndex(const char *dir, const char *prefix, const char *ext); }; } libindi/libs/indibase/indidustcapinterface.cpp0000664000175000017500000000444213263645557021101 0ustar jasemjasem/* Dust Cap Interface Copyright (C) 2015 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 */ #include "indidustcapinterface.h" #include namespace INDI { void DustCapInterface::initDustCapProperties(const char *deviceName, const char *groupName) { strncpy(dustCapName, deviceName, MAXINDIDEVICE); // Open/Close cover IUFillSwitch(&ParkCapS[CAP_PARK], "PARK", "Park", ISS_OFF); IUFillSwitch(&ParkCapS[CAP_UNPARK], "UNPARK", "Unpark", ISS_OFF); IUFillSwitchVector(&ParkCapSP, ParkCapS, 2, deviceName, "CAP_PARK", "Dust Cover", groupName, IP_RW, ISR_1OFMANY, 0, IPS_IDLE); } bool DustCapInterface::processDustCapSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) { INDI_UNUSED(dev); // Park/UnPark Dust Cover if (!strcmp(ParkCapSP.name, name)) { int prevSwitch = IUFindOnSwitchIndex(&ParkCapSP); IUUpdateSwitch(&ParkCapSP, states, names, n); if (ParkCapS[CAP_PARK].s == ISS_ON) ParkCapSP.s = ParkCap(); else ParkCapSP.s = UnParkCap(); if (ParkCapSP.s == IPS_ALERT) { IUResetSwitch(&ParkCapSP); ParkCapS[prevSwitch].s = ISS_ON; } IDSetSwitch(&ParkCapSP, nullptr); return true; } return false; } IPState DustCapInterface::ParkCap() { // Must be implemented by child class return IPS_ALERT; } IPState DustCapInterface::UnParkCap() { // Must be implemented by child class return IPS_ALERT; } } libindi/libs/indibase/indidetector.cpp0000664000175000017500000012321713263645557017370 0ustar jasemjasem/******************************************************************************* Copyright(c) 2010, 2017 Ilia Platone, Jasem Mutlaq. All rights reserved. This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2 as published by the Free Software Foundation. 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 Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. *******************************************************************************/ #include "indidetector.h" #include "indicom.h" #include #include #include #include #include #include #include #include #include #include #include const char *CAPTURE_SETTINGS_TAB = "Capture Settings"; const char *CAPTURE_INFO_TAB = "Capture Info"; // Create dir recursively static int _det_mkdir(const char *dir, mode_t mode) { char tmp[PATH_MAX]; char *p = nullptr; size_t len; snprintf(tmp, sizeof(tmp), "%s", dir); len = strlen(tmp); if (tmp[len - 1] == '/') tmp[len - 1] = 0; for (p = tmp + 1; *p; p++) if (*p == '/') { *p = 0; if (mkdir(tmp, mode) == -1 && errno != EEXIST) return -1; *p = '/'; } if (mkdir(tmp, mode) == -1 && errno != EEXIST) return -1; return 0; } DetectorDevice::DetectorDevice() { ContinuumBuffer = (uint8_t *)malloc(sizeof(uint8_t)); // Seed for realloc ContinuumBufferSize = 0; SpectrumBuffer = (double *)malloc(sizeof(double)); // Seed for realloc SpectrumBufferSize = 0; BPS = 8; NAxis = 2; strncpy(captureExtention, "raw", MAXINDIBLOBFMT); } DetectorDevice::~DetectorDevice() { free(ContinuumBuffer); ContinuumBufferSize = 0; ContinuumBuffer = nullptr; free(SpectrumBuffer); SpectrumBufferSize = 0; SpectrumBuffer = nullptr; } void DetectorDevice::setMinMaxStep(const char *property, const char *element, double min, double max, double step, bool sendToClient) { INumberVectorProperty *vp = nullptr; if (!strcmp(property, FramedCaptureNP.name)) vp = &FramedCaptureNP; if (!strcmp(property, DetectorSettingsNP.name)) vp = &DetectorSettingsNP; INumber *np = IUFindNumber(vp, element); if (np) { np->min = min; np->max = max; np->step = step; if (sendToClient) IUUpdateMinMax(vp); } } void DetectorDevice::setSampleRate(float sr) { samplerate = sr; DetectorSettingsN[DetectorDevice::DETECTOR_SAMPLERATE].value = sr; IDSetNumber(&DetectorSettingsNP, nullptr); } void DetectorDevice::setFrequency(float freq) { Frequency = freq; DetectorSettingsN[DetectorDevice::DETECTOR_FREQUENCY].value = freq; IDSetNumber(&DetectorSettingsNP, nullptr); } void DetectorDevice::setBPS(int bbs) { BPS = bbs; DetectorSettingsN[DetectorDevice::DETECTOR_BITSPERSAMPLE].value = BPS; IDSetNumber(&DetectorSettingsNP, nullptr); } void DetectorDevice::setContinuumBufferSize(int nbuf, bool allocMem) { if (nbuf == ContinuumBufferSize) return; ContinuumBufferSize = nbuf; if (allocMem == false) return; ContinuumBuffer = (uint8_t *)realloc(ContinuumBuffer, nbuf * sizeof(uint8_t)); } void DetectorDevice::setSpectrumBufferSize(int nbuf, bool allocMem) { if (nbuf == SpectrumBufferSize) return; SpectrumBufferSize = nbuf; if (allocMem == false) return; SpectrumBuffer = (double *)realloc(SpectrumBuffer, nbuf * sizeof(double)); } void DetectorDevice::setCaptureLeft(double duration) { FramedCaptureN[0].value = duration; IDSetNumber(&FramedCaptureNP, nullptr); } void DetectorDevice::setCaptureDuration(double duration) { captureDuration = duration; gettimeofday(&startCaptureTime, nullptr); } const char *DetectorDevice::getCaptureStartTime() { static char ts[32]; char iso8601[32]; struct tm *tp; time_t t = (time_t)startCaptureTime.tv_sec; int u = startCaptureTime.tv_usec / 1000.0; tp = gmtime(&t); strftime(iso8601, sizeof(iso8601), "%Y-%m-%dT%H:%M:%S", tp); snprintf(ts, 32, "%s.%03d", iso8601, u); return (ts); } void DetectorDevice::setCaptureFailed() { FramedCaptureNP.s = IPS_ALERT; IDSetNumber(&FramedCaptureNP, nullptr); } int DetectorDevice::getNAxis() const { return NAxis; } void DetectorDevice::setNAxis(int value) { NAxis = value; } void DetectorDevice::setCaptureExtension(const char *ext) { strncpy(captureExtention, ext, MAXINDIBLOBFMT); } namespace INDI { Detector::Detector() { //ctor capability = 0; InCapture = false; AutoLoop = false; SendCapture = false; ShowMarker = false; CaptureTime = 0.0; CurrentFilterSlot = -1; RA = -1000; Dec = -1000; MPSAS = -1000; primaryAperture = primaryFocalLength - 1; } Detector::~Detector() { } void Detector::SetDetectorCapability(uint32_t cap) { capability = cap; setDriverInterface(getDriverInterface()); } bool Detector::initProperties() { DefaultDevice::initProperties(); // let the base class flesh in what it wants // PrimaryDetector Temperature IUFillNumber(&TemperatureN[0], "DETECTOR_TEMPERATURE_VALUE", "Temperature (C)", "%5.2f", -50.0, 50.0, 0., 0.); IUFillNumberVector(&TemperatureNP, TemperatureN, 1, getDeviceName(), "DETECTOR_TEMPERATURE", "Temperature", MAIN_CONTROL_TAB, IP_RW, 60, IPS_IDLE); /**********************************************/ /**************** Primary Device ****************/ /**********************************************/ // PrimaryDetector Capture IUFillNumber(&PrimaryDetector.FramedCaptureN[0], "DETECTOR_CAPTURE_VALUE", "Duration (s)", "%5.2f", 0.01, 3600, 1.0, 1.0); IUFillNumberVector(&PrimaryDetector.FramedCaptureNP, PrimaryDetector.FramedCaptureN, 1, getDeviceName(), "DETECTOR_CAPTURE", "Capture", MAIN_CONTROL_TAB, IP_RW, 60, IPS_IDLE); // PrimaryDetector Abort IUFillSwitch(&PrimaryDetector.AbortCaptureS[0], "ABORT", "Abort", ISS_OFF); IUFillSwitchVector(&PrimaryDetector.AbortCaptureSP, PrimaryDetector.AbortCaptureS, 1, getDeviceName(), "DETECTOR_ABORT_CAPTURE", "Capture Abort", MAIN_CONTROL_TAB, IP_RW, ISR_ATMOST1, 60, IPS_IDLE); // PrimaryDetector Info IUFillNumber(&PrimaryDetector.DetectorSettingsN[DetectorDevice::DETECTOR_SAMPLERATE], "DETECTOR_SAMPLERATE", "Sample rate (SPS)", "%16.2f", 0.01, 1.0e+15, 0.01, 1.0e+6); IUFillNumber(&PrimaryDetector.DetectorSettingsN[DetectorDevice::DETECTOR_FREQUENCY], "DETECTOR_FREQUENCY", "Center frequency (Hz)", "%16.2f", 0.01, 1.0e+15, 0.01, 1.42e+9); IUFillNumber(&PrimaryDetector.DetectorSettingsN[DetectorDevice::DETECTOR_BITSPERSAMPLE], "DETECTOR_BITSPERSAMPLE", "Bits per sample", "%3.0f", 1, 64, 1, 8); IUFillNumberVector(&PrimaryDetector.DetectorSettingsNP, PrimaryDetector.DetectorSettingsN, 3, getDeviceName(), "DETECTOR_SETTINGS", "Detector Settings", CAPTURE_SETTINGS_TAB, IP_RW, 60, IPS_IDLE); // PrimaryDetector Device Continuum Blob IUFillBLOB(&PrimaryDetector.FitsB[0], "CONTINUUM", "Continuum", ""); IUFillBLOB(&PrimaryDetector.FitsB[1], "SPECTRUM", "Spectrum", ""); IUFillBLOBVector(&PrimaryDetector.FitsBP, PrimaryDetector.FitsB, 2, getDeviceName(), "DETECTOR", "Capture Data", CAPTURE_INFO_TAB, IP_RO, 60, IPS_IDLE); /**********************************************/ /************** Upload Settings ***************/ /**********************************************/ // Upload Mode IUFillSwitch(&UploadS[0], "UPLOAD_CLIENT", "Client", ISS_ON); IUFillSwitch(&UploadS[1], "UPLOAD_LOCAL", "Local", ISS_OFF); IUFillSwitch(&UploadS[2], "UPLOAD_BOTH", "Both", ISS_OFF); IUFillSwitchVector(&UploadSP, UploadS, 3, getDeviceName(), "UPLOAD_MODE", "Upload", OPTIONS_TAB, IP_RW, ISR_1OFMANY, 0, IPS_IDLE); // Upload Settings IUFillText(&UploadSettingsT[UPLOAD_DIR], "UPLOAD_DIR", "Dir", ""); IUFillText(&UploadSettingsT[UPLOAD_PREFIX], "UPLOAD_PREFIX", "Prefix", "CAPTURE_XXX"); IUFillTextVector(&UploadSettingsTP, UploadSettingsT, 2, getDeviceName(), "UPLOAD_SETTINGS", "Upload Settings", OPTIONS_TAB, IP_RW, 60, IPS_IDLE); // Upload File Path IUFillText(&FileNameT[0], "FILE_PATH", "Path", ""); IUFillTextVector(&FileNameTP, FileNameT, 1, getDeviceName(), "DETECTOR_FILE_PATH", "Filename", OPTIONS_TAB, IP_RO, 60, IPS_IDLE); /**********************************************/ /****************** FITS Header****************/ /**********************************************/ IUFillText(&FITSHeaderT[FITS_OBSERVER], "FITS_OBSERVER", "Observer", "Unknown"); IUFillText(&FITSHeaderT[FITS_OBJECT], "FITS_OBJECT", "Object", "Unknown"); IUFillTextVector(&FITSHeaderTP, FITSHeaderT, 2, getDeviceName(), "FITS_HEADER", "FITS Header", INFO_TAB, IP_RW, 60, IPS_IDLE); /**********************************************/ /**************** Snooping ********************/ /**********************************************/ // Snooped Devices IUFillText(&ActiveDeviceT[0], "ACTIVE_TELESCOPE", "Telescope", "Telescope Simulator"); IUFillText(&ActiveDeviceT[1], "ACTIVE_FOCUSER", "Focuser", "Focuser Simulator"); IUFillText(&ActiveDeviceT[2], "ACTIVE_FILTER", "Filter", "PrimaryDetector Simulator"); IUFillText(&ActiveDeviceT[3], "ACTIVE_SKYQUALITY", "Sky Quality", "SQM"); IUFillTextVector(&ActiveDeviceTP, ActiveDeviceT, 4, getDeviceName(), "ACTIVE_DEVICES", "Snoop devices", OPTIONS_TAB, IP_RW, 60, IPS_IDLE); // Snooped RA/DEC Property IUFillNumber(&EqN[0], "RA", "Ra (hh:mm:ss)", "%010.6m", 0, 24, 0, 0); IUFillNumber(&EqN[1], "DEC", "Dec (dd:mm:ss)", "%010.6m", -90, 90, 0, 0); IUFillNumberVector(&EqNP, EqN, 2, ActiveDeviceT[0].text, "EQUATORIAL_EOD_COORD", "EQ Coord", "Main Control", IP_RW, 60, IPS_IDLE); // Snoop properties of interest IDSnoopDevice(ActiveDeviceT[0].text, "EQUATORIAL_EOD_COORD"); IDSnoopDevice(ActiveDeviceT[0].text, "TELESCOPE_INFO"); IDSnoopDevice(ActiveDeviceT[2].text, "FILTER_SLOT"); IDSnoopDevice(ActiveDeviceT[2].text, "FILTER_NAME"); IDSnoopDevice(ActiveDeviceT[3].text, "SKY_QUALITY"); setDriverInterface(DETECTOR_INTERFACE); return true; } void Detector::ISGetProperties(const char *dev) { DefaultDevice::ISGetProperties(dev); defineText(&ActiveDeviceTP); loadConfig(true, "ACTIVE_DEVICES"); } bool Detector::updateProperties() { //IDLog("PrimaryDetector UpdateProperties isConnected returns %d %d\n",isConnected(),Connected); if (isConnected()) { defineNumber(&PrimaryDetector.FramedCaptureNP); if (CanAbort()) defineSwitch(&PrimaryDetector.AbortCaptureSP); defineText(&FITSHeaderTP); if (HasCooler()) defineNumber(&TemperatureNP); defineNumber(&PrimaryDetector.DetectorSettingsNP); defineBLOB(&PrimaryDetector.FitsBP); defineSwitch(&TelescopeTypeSP); defineSwitch(&UploadSP); if (UploadSettingsT[UPLOAD_DIR].text == nullptr) IUSaveText(&UploadSettingsT[UPLOAD_DIR], getenv("HOME")); defineText(&UploadSettingsTP); } else { deleteProperty(PrimaryDetector.DetectorSettingsNP.name); deleteProperty(PrimaryDetector.FramedCaptureNP.name); if (CanAbort()) deleteProperty(PrimaryDetector.AbortCaptureSP.name); deleteProperty(PrimaryDetector.FitsBP.name); deleteProperty(FITSHeaderTP.name); if (HasCooler()) deleteProperty(TemperatureNP.name); deleteProperty(TelescopeTypeSP.name); deleteProperty(UploadSP.name); deleteProperty(UploadSettingsTP.name); } return true; } bool Detector::ISSnoopDevice(XMLEle *root) { XMLEle *ep = nullptr; const char *propName = findXMLAttValu(root, "name"); if (IUSnoopNumber(root, &EqNP) == 0) { float newra, newdec; newra = EqN[0].value; newdec = EqN[1].value; if ((newra != RA) || (newdec != Dec)) { //IDLog("RA %4.2f Dec %4.2f Snooped RA %4.2f Dec %4.2f\n",RA,Dec,newra,newdec); RA = newra; Dec = newdec; } } else if (!strcmp(propName, "TELESCOPE_INFO")) { for (ep = nextXMLEle(root, 1); ep != nullptr; ep = nextXMLEle(root, 0)) { const char *name = findXMLAttValu(ep, "name"); if (!strcmp(name, "TELESCOPE_APERTURE")) { primaryAperture = atof(pcdataXMLEle(ep)); } else if (!strcmp(name, "TELESCOPE_FOCAL_LENGTH")) { primaryFocalLength = atof(pcdataXMLEle(ep)); } } } else if (!strcmp(propName, "FILTER_NAME")) { FilterNames.clear(); for (ep = nextXMLEle(root, 1); ep != nullptr; ep = nextXMLEle(root, 0)) FilterNames.push_back(pcdataXMLEle(ep)); } else if (!strcmp(propName, "FILTER_SLOT")) { CurrentFilterSlot = -1; for (ep = nextXMLEle(root, 1); ep != nullptr; ep = nextXMLEle(root, 0)) CurrentFilterSlot = atoi(pcdataXMLEle(ep)); } else if (!strcmp(propName, "SKY_QUALITY")) { for (ep = nextXMLEle(root, 1); ep != nullptr; ep = nextXMLEle(root, 0)) { const char *name = findXMLAttValu(ep, "name"); if (!strcmp(name, "SKY_BRIGHTNESS")) { MPSAS = atof(pcdataXMLEle(ep)); break; } } } return DefaultDevice::ISSnoopDevice(root); } bool Detector::ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n) { // first check if it's for our device if (dev != nullptr && strcmp(dev, getDeviceName()) == 0) { // This is for our device // Now lets see if it's something we process here if (!strcmp(name, ActiveDeviceTP.name)) { ActiveDeviceTP.s = IPS_OK; IUUpdateText(&ActiveDeviceTP, texts, names, n); IDSetText(&ActiveDeviceTP, nullptr); // Update the property name! strncpy(EqNP.device, ActiveDeviceT[0].text, MAXINDIDEVICE); IDSnoopDevice(ActiveDeviceT[0].text, "EQUATORIAL_EOD_COORD"); IDSnoopDevice(ActiveDeviceT[0].text, "TELESCOPE_INFO"); IDSnoopDevice(ActiveDeviceT[2].text, "FILTER_SLOT"); IDSnoopDevice(ActiveDeviceT[2].text, "FILTER_NAME"); IDSnoopDevice(ActiveDeviceT[3].text, "SKY_QUALITY"); // Tell children active devices was updated. activeDevicesUpdated(); // We processed this one, so, tell the world we did it return true; } if (!strcmp(name, FITSHeaderTP.name)) { IUUpdateText(&FITSHeaderTP, texts, names, n); FITSHeaderTP.s = IPS_OK; IDSetText(&FITSHeaderTP, nullptr); return true; } if (!strcmp(name, UploadSettingsTP.name)) { IUUpdateText(&UploadSettingsTP, texts, names, n); UploadSettingsTP.s = IPS_OK; IDSetText(&UploadSettingsTP, nullptr); return true; } } return DefaultDevice::ISNewText(dev, name, texts, names, n); } bool Detector::ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) { // first check if it's for our device //IDLog("Detector::ISNewNumber %s\n",name); if (dev != nullptr && strcmp(dev, getDeviceName()) == 0) { if (!strcmp(name, "DETECTOR_CAPTURE")) { if ((values[0] < PrimaryDetector.FramedCaptureN[0].min || values[0] > PrimaryDetector.FramedCaptureN[0].max)) { DEBUGF(Logger::DBG_ERROR, "Requested capture value (%g) seconds out of bounds [%g,%g].", values[0], PrimaryDetector.FramedCaptureN[0].min, PrimaryDetector.FramedCaptureN[0].max); PrimaryDetector.FramedCaptureNP.s = IPS_ALERT; IDSetNumber(&PrimaryDetector.FramedCaptureNP, nullptr); return false; } PrimaryDetector.FramedCaptureN[0].value = CaptureTime = values[0]; if (PrimaryDetector.FramedCaptureNP.s == IPS_BUSY) { if (CanAbort() && AbortCapture() == false) DEBUG(Logger::DBG_WARNING, "Warning: Aborting capture failed."); } if (StartCapture(CaptureTime)) PrimaryDetector.FramedCaptureNP.s = IPS_BUSY; else PrimaryDetector.FramedCaptureNP.s = IPS_ALERT; IDSetNumber(&PrimaryDetector.FramedCaptureNP, nullptr); return true; } // PrimaryDetector TEMPERATURE: if (!strcmp(name, TemperatureNP.name)) { if (values[0] < TemperatureN[0].min || values[0] > TemperatureN[0].max) { TemperatureNP.s = IPS_ALERT; DEBUGF(Logger::DBG_ERROR, "Error: Bad temperature value! Range is [%.1f, %.1f] [C].", TemperatureN[0].min, TemperatureN[0].max); IDSetNumber(&TemperatureNP, nullptr); return false; } int rc = SetTemperature(values[0]); if (rc == 0) TemperatureNP.s = IPS_BUSY; else if (rc == 1) TemperatureNP.s = IPS_OK; else TemperatureNP.s = IPS_ALERT; IDSetNumber(&TemperatureNP, nullptr); return true; } // PrimaryDetector Info if (!strcmp(name, PrimaryDetector.DetectorSettingsNP.name)) { IUUpdateNumber(&PrimaryDetector.DetectorSettingsNP, values, names, n); PrimaryDetector.DetectorSettingsNP.s = IPS_OK; SetDetectorParams(PrimaryDetector.DetectorSettingsNP.np[DetectorDevice::DETECTOR_SAMPLERATE].value, PrimaryDetector.DetectorSettingsNP.np[DetectorDevice::DETECTOR_FREQUENCY].value, PrimaryDetector.DetectorSettingsNP.np[DetectorDevice::DETECTOR_BITSPERSAMPLE].value); IDSetNumber(&PrimaryDetector.DetectorSettingsNP, nullptr); return true; } } return DefaultDevice::ISNewNumber(dev, name, values, names, n); } bool Detector::ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) { if (dev != nullptr && strcmp(dev, getDeviceName()) == 0) { if (!strcmp(name, UploadSP.name)) { int prevMode = IUFindOnSwitchIndex(&UploadSP); IUUpdateSwitch(&UploadSP, states, names, n); UploadSP.s = IPS_OK; IDSetSwitch(&UploadSP, nullptr); if (UploadS[0].s == ISS_ON) { DEBUG(Logger::DBG_SESSION, "Upload settings set to client only."); if (prevMode != 0) deleteProperty(FileNameTP.name); } else if (UploadS[1].s == ISS_ON) { DEBUG(Logger::DBG_SESSION, "Upload settings set to local only."); defineText(&FileNameTP); } else { DEBUG(Logger::DBG_SESSION, "Upload settings set to client and local."); defineText(&FileNameTP); } return true; } if (!strcmp(name, TelescopeTypeSP.name)) { IUUpdateSwitch(&TelescopeTypeSP, states, names, n); TelescopeTypeSP.s = IPS_OK; IDSetSwitch(&TelescopeTypeSP, nullptr); return true; } // Primary Device Abort Expsoure if (strcmp(name, PrimaryDetector.AbortCaptureSP.name) == 0) { IUResetSwitch(&PrimaryDetector.AbortCaptureSP); if (AbortCapture()) { PrimaryDetector.AbortCaptureSP.s = IPS_OK; PrimaryDetector.FramedCaptureNP.s = IPS_IDLE; PrimaryDetector.FramedCaptureN[0].value = 0; } else { PrimaryDetector.AbortCaptureSP.s = IPS_ALERT; PrimaryDetector.FramedCaptureNP.s = IPS_ALERT; } IDSetSwitch(&PrimaryDetector.AbortCaptureSP, nullptr); IDSetNumber(&PrimaryDetector.FramedCaptureNP, nullptr); return true; } } return DefaultDevice::ISNewSwitch(dev, name, states, names, n); } int Detector::SetTemperature(double temperature) { INDI_UNUSED(temperature); DEBUGF(Logger::DBG_WARNING, "Detector::SetTemperature %4.2f - Should never get here", temperature); return -1; } bool Detector::StartCapture(float duration) { INDI_UNUSED(duration); DEBUGF(Logger::DBG_WARNING, "Detector::StartCapture %4.2f - Should never get here", duration); return false; } bool Detector::CaptureParamsUpdated(float sr, float freq, float bps) { INDI_UNUSED(sr); INDI_UNUSED(freq); INDI_UNUSED(bps); DEBUGF(Logger::DBG_WARNING, "Detector::CaptureParamsUpdated %15.0f %15.0f %15.0f - Should never get here", sr, freq, bps); return false; } bool Detector::AbortCapture() { DEBUG(Logger::DBG_WARNING, "Detector::AbortCapture - Should never get here"); return false; } void Detector::addFITSKeywords(fitsfile *fptr, DetectorDevice *targetDevice, int blobIndex) { INDI_UNUSED(blobIndex); int status = 0; char dev_name[32]; char exp_start[32]; double captureDuration; char *orig = setlocale(LC_NUMERIC, "C"); char fitsString[MAXINDIDEVICE]; // DETECTOR strncpy(fitsString, getDeviceName(), MAXINDIDEVICE); fits_update_key_s(fptr, TSTRING, "INSTRUME", fitsString, "PrimaryDetector Name", &status); // Telescope strncpy(fitsString, ActiveDeviceT[0].text, MAXINDIDEVICE); fits_update_key_s(fptr, TSTRING, "TELESCOP", fitsString, "Telescope name", &status); // Observer strncpy(fitsString, FITSHeaderT[FITS_OBSERVER].text, MAXINDIDEVICE); fits_update_key_s(fptr, TSTRING, "OBSERVER", fitsString, "Observer name", &status); // Object strncpy(fitsString, FITSHeaderT[FITS_OBJECT].text, MAXINDIDEVICE); fits_update_key_s(fptr, TSTRING, "OBJECT", fitsString, "Object name", &status); captureDuration = targetDevice->getCaptureDuration(); strncpy(dev_name, getDeviceName(), 32); strncpy(exp_start, targetDevice->getCaptureStartTime(), 32); fits_update_key_s(fptr, TDOUBLE, "EXPTIME", &(captureDuration), "Total Capture Time (s)", &status); if (HasCooler()) fits_update_key_s(fptr, TDOUBLE, "DETECTOR-TEMP", &(TemperatureN[0].value), "PrimaryDetector Temperature (Celsius)", &status); if (CurrentFilterSlot != -1 && CurrentFilterSlot <= (int)FilterNames.size()) { char filter[32]; strncpy(filter, FilterNames.at(CurrentFilterSlot - 1).c_str(), 32); fits_update_key_s(fptr, TSTRING, "FILTER", filter, "Filter", &status); } #ifdef WITH_MINMAX if (targetDevice->getNAxis() == 2) { double min_val, max_val; if(blobIndex == DetectorDevice::DETECTOR_BLOB_CONTINUUM) { getMinMax(&min_val, &max_val, targetDevice->getContinuumBuffer(), targetDevice->getContinuumBufferSize(), targetDevice->getBPS()); } if(blobIndex == DetectorDevice::DETECTOR_BLOB_SPECTRUM) { getMinMax(&min_val, &max_val, targetDevice->getSpectrumBuffer(), targetDevice->getSpectrumBufferSize(), sizeof(double) * 8); } fits_update_key_s(fptr, TDOUBLE, "DATAMIN", &min_val, "Minimum value", &status); fits_update_key_s(fptr, TDOUBLE, "DATAMAX", &max_val, "Maximum value", &status); } #endif if (primaryFocalLength != -1) fits_update_key_s(fptr, TDOUBLE, "FOCALLEN", &primaryFocalLength, "Focal Length (mm)", &status); if (MPSAS != -1000) { fits_update_key_s(fptr, TDOUBLE, "MPSAS", &MPSAS, "Sky Quality (mag per arcsec^2)", &status); } if (RA != -1000 && Dec != -1000) { ln_equ_posn epochPos { 0, 0 }, J2000Pos { 0, 0 }; epochPos.ra = RA * 15.0; epochPos.dec = Dec; // Convert from JNow to J2000 //TODO use exp_start instead of julian from system ln_get_equ_prec2(&epochPos, ln_get_julian_from_sys(), JD2000, &J2000Pos); double raJ2000 = J2000Pos.ra / 15.0; double decJ2000 = J2000Pos.dec; char ra_str[32], de_str[32]; fs_sexa(ra_str, raJ2000, 2, 360000); fs_sexa(de_str, decJ2000, 2, 360000); char *raPtr = ra_str, *dePtr = de_str; while (*raPtr != '\0') { if (*raPtr == ':') *raPtr = ' '; raPtr++; } while (*dePtr != '\0') { if (*dePtr == ':') *dePtr = ' '; dePtr++; } fits_update_key_s(fptr, TSTRING, "OBJCTRA", ra_str, "Object RA", &status); fits_update_key_s(fptr, TSTRING, "OBJCTDEC", de_str, "Object DEC", &status); int epoch = 2000; //fits_update_key_s(fptr, TINT, "EPOCH", &epoch, "Epoch", &status); fits_update_key_s(fptr, TINT, "EQUINOX", &epoch, "Equinox", &status); } fits_update_key_s(fptr, TSTRING, "DATE-OBS", exp_start, "UTC start date of observation", &status); fits_write_comment(fptr, "Generated by INDI", &status); setlocale(LC_NUMERIC, orig); } void Detector::fits_update_key_s(fitsfile *fptr, int type, std::string name, void *p, std::string explanation, int *status) { // this function is for removing warnings about deprecated string conversion to char* (from arg 5) fits_update_key(fptr, type, name.c_str(), p, const_cast(explanation.c_str()), status); } bool Detector::CaptureComplete(DetectorDevice *targetDevice) { bool sendCapture = (UploadS[0].s == ISS_ON || UploadS[2].s == ISS_ON); bool saveCapture = (UploadS[1].s == ISS_ON || UploadS[2].s == ISS_ON); bool autoLoop = false; if (sendCapture || saveCapture) { if(HasContinuum()) { if (!strcmp(targetDevice->getCaptureExtension(), "fits")) { void *memptr; size_t memsize; int img_type = 0; int byte_type = 0; int status = 0; long naxis = 2; long naxes[naxis]; int nelements = 0; std::string bit_depth; char error_status[MAXRBUF]; fitsfile *fptr = nullptr; naxes[0] = targetDevice->getContinuumBufferSize() * 8 / targetDevice->getBPS(); naxes[0] = naxes[0] < 1 ? 1 : naxes[0]; naxes[1] = 1; switch (targetDevice->getBPS()) { case 8: byte_type = TBYTE; img_type = BYTE_IMG; bit_depth = "8 bits per sample"; break; case 16: byte_type = TUSHORT; img_type = USHORT_IMG; bit_depth = "16 bits per sample"; break; case 32: byte_type = TULONG; img_type = ULONG_IMG; bit_depth = "32 bits per sample"; break; default: DEBUGF(Logger::DBG_ERROR, "Unsupported bits per sample value %d", targetDevice->getBPS()); return false; break; } nelements = naxes[0] * naxes[1]; if (naxis == 3) { nelements *= 3; naxes[2] = 3; } // Now we have to send fits format data to the client memsize = 5760; memptr = malloc(memsize); if (!memptr) { DEBUGF(Logger::DBG_ERROR, "Error: failed to allocate memory: %lu", (unsigned long)memsize); } fits_create_memfile(&fptr, &memptr, &memsize, 2880, realloc, &status); if (status) { fits_report_error(stderr, status); /* print out any error messages */ fits_get_errstatus(status, error_status); DEBUGF(Logger::DBG_ERROR, "FITS Error: %s", error_status); return false; } fits_create_img(fptr, img_type, naxis, naxes, &status); if (status) { fits_report_error(stderr, status); /* print out any error messages */ fits_get_errstatus(status, error_status); DEBUGF(Logger::DBG_ERROR, "FITS Error: %s", error_status); return false; } addFITSKeywords(fptr, targetDevice, DetectorDevice::DETECTOR_BLOB_CONTINUUM); fits_write_img(fptr, byte_type, 1, nelements, targetDevice->getContinuumBuffer(), &status); if (status) { fits_report_error(stderr, status); /* print out any error messages */ fits_get_errstatus(status, error_status); DEBUGF(Logger::DBG_ERROR, "FITS Error: %s", error_status); return false; } fits_close_file(fptr, &status); uploadFile(targetDevice, memptr, memsize, sendCapture, saveCapture, DetectorDevice::DETECTOR_BLOB_CONTINUUM); free(memptr); } else { uploadFile(targetDevice, targetDevice->getContinuumBuffer(), targetDevice->getContinuumBufferSize(), sendCapture, saveCapture, DetectorDevice::DETECTOR_BLOB_CONTINUUM); } } if(HasSpectrum()) { if (!strcmp(targetDevice->getCaptureExtension(), "fits")) { void *memptr; size_t memsize; int img_type = 0; int byte_type = 0; int status = 0; long naxis = 2; long naxes[naxis]; int nelements = 0; std::string bit_depth; char error_status[MAXRBUF]; fitsfile *fptr = nullptr; naxes[0] = targetDevice->getSpectrumBufferSize() / sizeof(double); naxes[1] = 1; byte_type = TDOUBLE; img_type = DOUBLE_IMG; bit_depth = "64 bits per sample"; nelements = naxes[0] * naxes[1]; // Now we have to send fits format data to the client memsize = 5760; memptr = malloc(memsize); if (!memptr) { DEBUGF(Logger::DBG_ERROR, "Error: failed to allocate memory: %lu", (unsigned long)memsize); } fits_create_memfile(&fptr, &memptr, &memsize, 2880, realloc, &status); if (status) { fits_report_error(stderr, status); /* print out any error messages */ fits_get_errstatus(status, error_status); DEBUGF(Logger::DBG_ERROR, "FITS Error: %s", error_status); return false; } fits_create_img(fptr, img_type, naxis, naxes, &status); if (status) { fits_report_error(stderr, status); /* print out any error messages */ fits_get_errstatus(status, error_status); DEBUGF(Logger::DBG_ERROR, "FITS Error: %s", error_status); return false; } addFITSKeywords(fptr, targetDevice, DetectorDevice::DETECTOR_BLOB_SPECTRUM); fits_write_img(fptr, byte_type, 1, nelements, targetDevice->getSpectrumBuffer(), &status); if (status) { fits_report_error(stderr, status); /* print out any error messages */ fits_get_errstatus(status, error_status); DEBUGF(Logger::DBG_ERROR, "FITS Error: %s", error_status); return false; } fits_close_file(fptr, &status); uploadFile(targetDevice, memptr, memsize, sendCapture, saveCapture, DetectorDevice::DETECTOR_BLOB_SPECTRUM); free(memptr); } else { uploadFile(targetDevice, targetDevice->getSpectrumBuffer(), targetDevice->getSpectrumBufferSize() * sizeof(double), sendCapture, saveCapture, DetectorDevice::DETECTOR_BLOB_SPECTRUM); } } if (sendCapture) IDSetBLOB(&targetDevice->FitsBP, nullptr); DEBUG(Logger::DBG_DEBUG, "Upload complete"); } targetDevice->FramedCaptureNP.s = IPS_OK; IDSetNumber(&targetDevice->FramedCaptureNP, nullptr); if (autoLoop) { PrimaryDetector.FramedCaptureN[0].value = CaptureTime; PrimaryDetector.FramedCaptureNP.s = IPS_BUSY; if (StartCapture(CaptureTime)) PrimaryDetector.FramedCaptureNP.s = IPS_BUSY; else { DEBUG(Logger::DBG_DEBUG, "Autoloop: PrimaryDetector Capture Error!"); PrimaryDetector.FramedCaptureNP.s = IPS_ALERT; } IDSetNumber(&PrimaryDetector.FramedCaptureNP, nullptr); } return true; } bool Detector::uploadFile(DetectorDevice *targetDevice, const void *fitsData, size_t totalBytes, bool sendCapture, bool saveCapture, int blobIndex) { DEBUGF(Logger::DBG_DEBUG, "Uploading file. Ext: %s, Size: %d, sendCapture? %s, saveCapture? %s", targetDevice->getCaptureExtension(), totalBytes, sendCapture ? "Yes" : "No", saveCapture ? "Yes" : "No"); if (saveCapture) { targetDevice->FitsB[blobIndex].blob = (unsigned char *)fitsData; targetDevice->FitsB[blobIndex].bloblen = totalBytes; snprintf(targetDevice->FitsB[blobIndex].format, MAXINDIBLOBFMT, ".%s", targetDevice->getCaptureExtension()); FILE *fp = nullptr; char captureFileName[MAXRBUF]; std::string prefix = UploadSettingsT[UPLOAD_PREFIX].text; int maxIndex = getFileIndex(UploadSettingsT[UPLOAD_DIR].text, UploadSettingsT[UPLOAD_PREFIX].text, targetDevice->FitsB[blobIndex].format); if (maxIndex < 0) { DEBUGF(Logger::DBG_ERROR, "Error iterating directory %s. %s", UploadSettingsT[0].text, strerror(errno)); return false; } if (maxIndex > 0) { char ts[32]; struct tm *tp; time_t t; time(&t); tp = localtime(&t); strftime(ts, sizeof(ts), "%Y-%m-%dT%H-%M-%S", tp); std::string filets(ts); prefix = std::regex_replace(prefix, std::regex("ISO8601"), filets); char indexString[8]; snprintf(indexString, 8, "%03d", maxIndex); std::string prefixIndex = indexString; //prefix.replace(prefix.find("XXX"), std::string::npos, prefixIndex); prefix = std::regex_replace(prefix, std::regex("XXX"), prefixIndex); } snprintf(captureFileName, MAXRBUF, "%s/%s%s", UploadSettingsT[0].text, prefix.c_str(), targetDevice->FitsB[blobIndex].format); fp = fopen(captureFileName, "w"); if (fp == nullptr) { DEBUGF(Logger::DBG_ERROR, "Unable to save capture file (%s). %s", captureFileName, strerror(errno)); return false; } int n = 0; for (int nr = 0; nr < (int)targetDevice->FitsB[blobIndex].bloblen; nr += n) n = fwrite((static_cast(targetDevice->FitsB[blobIndex].blob) + nr), 1, targetDevice->FitsB[blobIndex].bloblen - nr, fp); fclose(fp); // Save capture file path IUSaveText(&FileNameT[0], captureFileName); DEBUGF(Logger::DBG_SESSION, "Capture saved to %s", captureFileName); FileNameTP.s = IPS_OK; IDSetText(&FileNameTP, nullptr); } targetDevice->FitsB[blobIndex].blob = (unsigned char *)fitsData; targetDevice->FitsB[blobIndex].bloblen = totalBytes; snprintf(targetDevice->FitsB[blobIndex].format, MAXINDIBLOBFMT, ".%s", targetDevice->getCaptureExtension()); targetDevice->FitsB[blobIndex].size = totalBytes; targetDevice->FitsBP.s = IPS_OK; return true; } void Detector::SetDetectorParams(float samplerate, float freq, float bps) { PrimaryDetector.setSampleRate(samplerate); PrimaryDetector.setFrequency(freq); PrimaryDetector.setBPS(bps); CaptureParamsUpdated(samplerate, freq, bps); } bool Detector::saveConfigItems(FILE *fp) { DefaultDevice::saveConfigItems(fp); IUSaveConfigText(fp, &ActiveDeviceTP); IUSaveConfigSwitch(fp, &UploadSP); IUSaveConfigText(fp, &UploadSettingsTP); IUSaveConfigSwitch(fp, &TelescopeTypeSP); return true; } void Detector::getMinMax(double *min, double *max, uint8_t *buf, int len, int bpp) { int ind = 0, i, j; int captureHeight = 1; int captureWidth = abs(len * 8 / bpp); double lmin = 0, lmax = 0; switch (bpp) { case 8: { unsigned char *captureBuffer = (unsigned char *)buf; lmin = lmax = captureBuffer[0]; for (i = 0; i < captureHeight; i++) for (j = 0; j < captureWidth; j++) { ind = (i * captureWidth) + j; if (captureBuffer[ind] < lmin) lmin = captureBuffer[ind]; else if (captureBuffer[ind] > lmax) lmax = captureBuffer[ind]; } } break; case 16: { unsigned short *captureBuffer = (unsigned short *)buf; lmin = lmax = captureBuffer[0]; for (i = 0; i < captureHeight; i++) for (j = 0; j < captureWidth; j++) { ind = (i * captureWidth) + j; if (captureBuffer[ind] < lmin) lmin = captureBuffer[ind]; else if (captureBuffer[ind] > lmax) lmax = captureBuffer[ind]; } } break; case 32: { unsigned int *captureBuffer = (unsigned int *)buf; lmin = lmax = captureBuffer[0]; for (i = 0; i < captureHeight; i++) for (j = 0; j < captureWidth; j++) { ind = (i * captureWidth) + j; if (captureBuffer[ind] < lmin) lmin = captureBuffer[ind]; else if (captureBuffer[ind] > lmax) lmax = captureBuffer[ind]; } } break; case 64: { double *captureBuffer = (double *)buf; lmin = lmax = captureBuffer[0]; for (i = 0; i < captureHeight; i++) for (j = 0; j < captureWidth; j++) { ind = (i * captureWidth) + j; if (captureBuffer[ind] < lmin) lmin = captureBuffer[ind]; else if (captureBuffer[ind] > lmax) lmax = captureBuffer[ind]; } } break; } *min = lmin; *max = lmax; } std::string regex_replace_compat2(const std::string &input, const std::string &pattern, const std::string &replace) { std::stringstream s; std::regex_replace(std::ostreambuf_iterator(s), input.begin(), input.end(), std::regex(pattern), replace); return s.str(); } int Detector::getFileIndex(const char *dir, const char *prefix, const char *ext) { INDI_UNUSED(ext); DIR *dpdf = nullptr; struct dirent *epdf = nullptr; std::vector files = std::vector(); std::string prefixIndex = prefix; prefixIndex = regex_replace_compat2(prefixIndex, "_ISO8601", ""); prefixIndex = regex_replace_compat2(prefixIndex, "_XXX", ""); // Create directory if does not exist struct stat st; if (stat(dir, &st) == -1) { DEBUGF(Logger::DBG_DEBUG, "Creating directory %s...", dir); if (_det_mkdir(dir, 0755) == -1) DEBUGF(Logger::DBG_ERROR, "Error creating directory %s (%s)", dir, strerror(errno)); } dpdf = opendir(dir); if (dpdf != nullptr) { while ((epdf = readdir(dpdf))) { if (strstr(epdf->d_name, prefixIndex.c_str())) files.push_back(epdf->d_name); } closedir(dpdf); } else return -1; int maxIndex = 0; for (int i = 0; i < (int)files.size(); i++) { int index = -1; std::string file = files.at(i); std::size_t start = file.find_last_of("_"); std::size_t end = file.find_last_of("."); if (start != std::string::npos) { index = atoi(file.substr(start + 1, end).c_str()); if (index > maxIndex) maxIndex = index; } } return (maxIndex + 1); } } libindi/libs/indibase/indifocuser.h0000664000175000017500000000737013263645557016673 0ustar jasemjasem/******************************************************************************* Copyright(c) 2013 Jasem Mutlaq. All rights reserved. This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2 as published by the Free Software Foundation. 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 Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. *******************************************************************************/ #pragma once #include "defaultdevice.h" #include "indifocuserinterface.h" namespace Connection { class Serial; class TCP; } /** * \class Focuser \brief Class to provide general functionality of a focuser device. Both relative and absolute focuser supported. Furthermore, if no position feedback is available from the focuser, an open-loop control is possible using timers, speed presets, and direction of motion. Developers need to subclass Focuser to implement any driver for focusers within INDI. \author Jasem Mutlaq \author Gerry Rozema */ namespace INDI { class Focuser : public DefaultDevice, public FocuserInterface { public: Focuser(); virtual ~Focuser(); /** \struct FocuserConnection \brief Holds the connection mode of the Focuser. */ enum { CONNECTION_NONE = 1 << 0, /** Do not use any connection plugin */ CONNECTION_SERIAL = 1 << 1, /** For regular serial and bluetooth connections */ CONNECTION_TCP = 1 << 2 /** For Wired and WiFI connections */ } FocuserConnection; virtual bool initProperties(); virtual void ISGetProperties(const char *dev); virtual bool updateProperties(); virtual bool ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n); virtual bool ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n); virtual bool ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n); virtual bool ISSnoopDevice(XMLEle *root); /** * @brief setConnection Set Focuser connection mode. Child class should call this in the constructor before Focuser registers * any connection interfaces * @param value ORed combination of FocuserConnection values. */ void setConnection(const uint8_t &value); /** * @return Get current Focuser connection mode */ uint8_t getConnection() const { return focuserConnection;} static void buttonHelper(const char *button_n, ISState state, void *context); protected: /** * @brief saveConfigItems Saves the Device Port and Focuser Presets in the configuration file * @param fp pointer to configuration file * @return true if successful, false otherwise. */ virtual bool saveConfigItems(FILE *fp); /** \brief perform handshake with device to check communication */ virtual bool Handshake(); INumber PresetN[3]; INumberVectorProperty PresetNP; ISwitch PresetGotoS[3]; ISwitchVectorProperty PresetGotoSP; void processButton(const char *button_n, ISState state); Controller *controller; Connection::Serial *serialConnection = nullptr; Connection::TCP *tcpConnection = nullptr; int PortFD = -1; private: bool callHandshake(); uint8_t focuserConnection = CONNECTION_SERIAL | CONNECTION_TCP; }; } libindi/libs/indibase/defaultdevice.cpp0000664000175000017500000007544213263645557017525 0ustar jasemjasem/******************************************************************************* Copyright(c) 2011 Jasem Mutlaq. All rights reserved. This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2 as published by the Free Software Foundation. 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 Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. *******************************************************************************/ #include "defaultdevice.h" #include "indicom.h" #include "indistandardproperty.h" #include "connectionplugins/connectionserial.h" #include #include #include const char *COMMUNICATION_TAB = "Communication"; const char *MAIN_CONTROL_TAB = "Main Control"; const char *CONNECTION_TAB = "Connection"; const char *MOTION_TAB = "Motion Control"; const char *DATETIME_TAB = "Date/Time"; const char *SITE_TAB = "Site Management"; const char *OPTIONS_TAB = "Options"; const char *FILTER_TAB = "Filter Wheel"; const char *FOCUS_TAB = "Focuser"; const char *GUIDE_TAB = "Guide"; const char *ALIGNMENT_TAB = "Alignment"; const char *INFO_TAB = "General Info"; void timerfunc(void *t) { //fprintf(stderr,"Got a timer hit with %x\n",t); INDI::DefaultDevice *devPtr = static_cast(t); if (devPtr != nullptr) { // this was for my device // but we dont have a way of telling // WHICH timer was hit :( devPtr->TimerHit(); } return; } namespace INDI { DefaultDevice::DefaultDevice() { pDebug = false; pSimulation = false; isInit = false; majorVersion = 1; minorVersion = 0; interfaceDescriptor = GENERAL_INTERFACE; memset(&ConnectionModeSP, 0, sizeof(ConnectionModeSP)); } DefaultDevice::~DefaultDevice() { } bool DefaultDevice::loadConfig(bool silent, const char *property) { char errmsg[MAXRBUF]; bool pResult = false; pResult = IUReadConfig(nullptr, deviceID, property, silent ? 1 : 0, errmsg) == 0 ? true : false; if (!silent) { if (pResult) { LOG_DEBUG("Configuration successfully loaded."); } else LOGF_ERROR( "Error loading user configuration. %s. To save user configuration, click Save under the " "Configuration property in the Options tab. ", errmsg); } IUSaveDefaultConfig(nullptr, nullptr, deviceID); return pResult; } bool DefaultDevice::saveConfigItems(FILE *fp) { IUSaveConfigSwitch(fp, &DebugSP); IUSaveConfigNumber(fp, &PollPeriodNP); if (ConnectionModeS != nullptr) IUSaveConfigSwitch(fp, &ConnectionModeSP); if (activeConnection) activeConnection->saveConfigItems(fp); return INDI::Logger::saveConfigItems(fp); } bool DefaultDevice::saveAllConfigItems(FILE *fp) { std::vector::iterator orderi; INDI_PROPERTY_TYPE pType; void *pPtr; ISwitchVectorProperty *svp = nullptr; INumberVectorProperty *nvp = nullptr; ITextVectorProperty *tvp = nullptr; IBLOBVectorProperty *bvp = nullptr; for (orderi = pAll.begin(); orderi != pAll.end(); orderi++) { pType = (*orderi)->getType(); pPtr = (*orderi)->getProperty(); switch (pType) { case INDI_NUMBER: nvp = static_cast(pPtr); //IDLog("Trying to save config for number %s\n", nvp->name); IUSaveConfigNumber(fp, nvp); break; case INDI_TEXT: tvp = static_cast(pPtr); IUSaveConfigText(fp, tvp); break; case INDI_SWITCH: svp = static_cast(pPtr); /* Never save CONNECTION property. Don't save switches with no switches on if the rule is one of many */ if (!strcmp(svp->name, INDI::SP::CONNECTION) || (svp->r == ISR_1OFMANY && !IUFindOnSwitch(svp))) continue; IUSaveConfigSwitch(fp, svp); break; case INDI_BLOB: bvp = static_cast(pPtr); IUSaveConfigBLOB(fp, bvp); break; case INDI_LIGHT: case INDI_UNKNOWN: break; } } return true; } bool DefaultDevice::saveConfig(bool silent, const char *property) { //std::vector::iterator orderi; char errmsg[MAXRBUF]; FILE *fp = nullptr; if (property == nullptr) { fp = IUGetConfigFP(nullptr, deviceID, "w", errmsg); if (fp == nullptr) { if (!silent) LOGF_ERROR("Error saving configuration. %s", errmsg); return false; } IUSaveConfigTag(fp, 0, getDeviceName(), silent ? 1 : 0); saveConfigItems(fp); IUSaveConfigTag(fp, 1, getDeviceName(), silent ? 1 : 0); fclose(fp); IUSaveDefaultConfig(nullptr, nullptr, deviceID); LOG_DEBUG("Configuration successfully saved."); } else { fp = IUGetConfigFP(nullptr, deviceID, "r", errmsg); if (fp == nullptr) { //if (!silent) // LOGF_ERROR("Error saving configuration. %s", errmsg); //return false; // If we don't have an existing file pointer, save all properties. return saveConfig(silent); } LilXML *lp = newLilXML(); XMLEle *root = readXMLFile(fp, lp, errmsg); fclose(fp); delLilXML(lp); if (root == nullptr) return false; XMLEle *ep = nullptr; bool propertySaved = false; for (ep = nextXMLEle(root, 1); ep != nullptr; ep = nextXMLEle(root, 0)) { const char *elemName = findXMLAttValu(ep, "name"); const char *tagName = tagXMLEle(ep); if (strcmp(elemName, property)) continue; if (!strcmp(tagName, "newSwitchVector")) { ISwitchVectorProperty *svp = getSwitch(elemName); if (svp == nullptr) { delXMLEle(root); return false; } XMLEle *sw = nullptr; for (sw = nextXMLEle(ep, 1); sw != nullptr; sw = nextXMLEle(ep, 0)) { ISwitch *oneSwitch = IUFindSwitch(svp, findXMLAttValu(sw, "name")); if (oneSwitch == nullptr) { delXMLEle(root); return false; } char formatString[MAXRBUF]; snprintf(formatString, MAXRBUF, " %s\n", sstateStr(oneSwitch->s)); editXMLEle(sw, formatString); } propertySaved = true; break; } else if (!strcmp(tagName, "newNumberVector")) { INumberVectorProperty *nvp = getNumber(elemName); if (nvp == nullptr) { delXMLEle(root); return false; } XMLEle *np = nullptr; for (np = nextXMLEle(ep, 1); np != nullptr; np = nextXMLEle(ep, 0)) { INumber *oneNumber = IUFindNumber(nvp, findXMLAttValu(np, "name")); if (oneNumber == nullptr) return false; char formatString[MAXRBUF]; snprintf(formatString, MAXRBUF, " %.20g\n", oneNumber->value); editXMLEle(np, formatString); } propertySaved = true; break; } else if (!strcmp(tagName, "newTextVector")) { ITextVectorProperty *tvp = getText(elemName); if (tvp == nullptr) { delXMLEle(root); return false; } XMLEle *tp = nullptr; for (tp = nextXMLEle(ep, 1); tp != nullptr; tp = nextXMLEle(ep, 0)) { IText *oneText = IUFindText(tvp, findXMLAttValu(tp, "name")); if (oneText == nullptr) return false; char formatString[MAXRBUF]; snprintf(formatString, MAXRBUF, " %s\n", oneText->text ? oneText->text : ""); editXMLEle(tp, formatString); } propertySaved = true; break; } } if (propertySaved) { fp = IUGetConfigFP(nullptr, deviceID, "w", errmsg); prXMLEle(fp, root, 0); fclose(fp); delXMLEle(root); LOGF_DEBUG("Configuration successfully saved for %s.", property); return true; } else { delXMLEle(root); return false; } } return true; } bool DefaultDevice::loadDefaultConfig() { char configDefaultFileName[MAXRBUF]; char errmsg[MAXRBUF]; bool pResult = false; if (getenv("INDICONFIG")) snprintf(configDefaultFileName, MAXRBUF, "%s.default", getenv("INDICONFIG")); else snprintf(configDefaultFileName, MAXRBUF, "%s/.indi/%s_config.xml.default", getenv("HOME"), deviceID); LOGF_DEBUG("Requesting to load default config with: %s", configDefaultFileName); pResult = IUReadConfig(configDefaultFileName, deviceID, nullptr, 0, errmsg) == 0 ? true : false; if (pResult) LOG_INFO("Default configuration loaded."); else LOGF_INFO("Error loading default configuraiton. %s", errmsg); return pResult; } bool DefaultDevice::ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) { // ignore if not ours // if (strcmp(dev, deviceID)) return false; ISwitchVectorProperty *svp = getSwitch(name); if (!svp) return false; //////////////////////////////////////////////////// // Connection //////////////////////////////////////////////////// if (!strcmp(svp->name, ConnectionSP.name)) { bool rc = false; for (int i = 0; i < n; i++) { if (!strcmp(names[i], "CONNECT") && (states[i] == ISS_ON)) { // If disconnected, try to connect. if (isConnected() == false) { rc = Connect(); if (rc) { // Connection is successful, set it to OK and updateProperties. setConnected(true, IPS_OK); updateProperties(); } else setConnected(false, IPS_ALERT); } else // Already connected, tell client we're connected already. setConnected(true); } else if (!strcmp(names[i], "DISCONNECT") && (states[i] == ISS_ON)) { // If connected, try to disconnect. if (isConnected() == true) { rc = Disconnect(); // Disconnection is successful, set it IDLE and updateProperties. if (rc) { setConnected(false, IPS_IDLE); updateProperties(); } else setConnected(true, IPS_ALERT); } // Already disconnected, tell client we're disconnected already. else setConnected(false, IPS_IDLE); } } return true; } //////////////////////////////////////////////////// // Connection Mode //////////////////////////////////////////////////// if (!strcmp(name, ConnectionModeSP.name)) { IUUpdateSwitch(&ConnectionModeSP, states, names, n); int activeConnectionIndex = IUFindOnSwitchIndex(&ConnectionModeSP); if (activeConnectionIndex >= 0 && activeConnectionIndex < static_cast(connections.size())) { activeConnection = connections[activeConnectionIndex]; activeConnection->Activated(); for (Connection::Interface *oneConnection : connections) { if (oneConnection == activeConnection) continue; oneConnection->Deactivated(); } ConnectionModeSP.s = IPS_OK; } else ConnectionModeSP.s = IPS_ALERT; IDSetSwitch(&ConnectionModeSP, nullptr); return true; } //////////////////////////////////////////////////// // Debug //////////////////////////////////////////////////// if (!strcmp(svp->name, "DEBUG")) { IUUpdateSwitch(svp, states, names, n); ISwitch *sp = IUFindOnSwitch(svp); assert(sp != nullptr); if (!strcmp(sp->name, "ENABLE")) setDebug(true); else setDebug(false); return true; } //////////////////////////////////////////////////// // Simulation //////////////////////////////////////////////////// if (!strcmp(svp->name, "SIMULATION")) { IUUpdateSwitch(svp, states, names, n); ISwitch *sp = IUFindOnSwitch(svp); assert(sp != nullptr); if (!strcmp(sp->name, "ENABLE")) setSimulation(true); else setSimulation(false); return true; } //////////////////////////////////////////////////// // Configuration //////////////////////////////////////////////////// if (!strcmp(svp->name, "CONFIG_PROCESS")) { IUUpdateSwitch(svp, states, names, n); ISwitch *sp = IUFindOnSwitch(svp); IUResetSwitch(svp); bool pResult = false; // Not suppose to happen (all switches off) but let's handle it anyway if (sp == nullptr) { svp->s = IPS_IDLE; IDSetSwitch(svp, nullptr); return true; } if (!strcmp(sp->name, "CONFIG_LOAD")) pResult = loadConfig(); else if (!strcmp(sp->name, "CONFIG_SAVE")) pResult = saveConfig(); else if (!strcmp(sp->name, "CONFIG_DEFAULT")) pResult = loadDefaultConfig(); if (pResult) svp->s = IPS_OK; else svp->s = IPS_ALERT; IDSetSwitch(svp, nullptr); return true; } //////////////////////////////////////////////////// // Debugging and Logging Levels //////////////////////////////////////////////////// if (!strcmp(svp->name, "DEBUG_LEVEL") || !strcmp(svp->name, "LOGGING_LEVEL") || !strcmp(svp->name, "LOG_OUTPUT")) { bool rc = Logger::ISNewSwitch(dev, name, states, names, n); if (!strcmp(svp->name, "LOG_OUTPUT")) { ISwitch *sw = IUFindSwitch(svp, "FILE_DEBUG"); if (sw && sw->s == ISS_ON) DEBUGF(Logger::DBG_SESSION, "Session log file %s", Logger::getLogFile().c_str()); } return rc; } bool rc = false; for (Connection::Interface *oneConnection : connections) rc |= oneConnection->ISNewSwitch(dev, name, states, names, n); return rc; } bool DefaultDevice::ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) { //////////////////////////////////////////////////// // Polling Period //////////////////////////////////////////////////// if (!strcmp(name, PollPeriodNP.name)) { IUUpdateNumber(&PollPeriodNP, values, names, n); PollPeriodNP.s = IPS_OK; POLLMS = static_cast(PollPeriodN[0].value); IDSetNumber(&PollPeriodNP, nullptr); return true; } for (Connection::Interface *oneConnection : connections) oneConnection->ISNewNumber(dev, name, values, names, n); return false; } bool DefaultDevice::ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n) { for (Connection::Interface *oneConnection : connections) oneConnection->ISNewText(dev, name, texts, names, n); return false; } bool DefaultDevice::ISNewBLOB(const char *dev, const char *name, int sizes[], int blobsizes[], char *blobs[], char *formats[], char *names[], int n) { INDI_UNUSED(dev); INDI_UNUSED(name); INDI_UNUSED(sizes); INDI_UNUSED(blobsizes); INDI_UNUSED(blobs); INDI_UNUSED(formats); INDI_UNUSED(names); INDI_UNUSED(n); return false; } bool DefaultDevice::ISSnoopDevice(XMLEle *root) { INDI_UNUSED(root); return false; } void DefaultDevice::addDebugControl() { registerProperty(&DebugSP, INDI_SWITCH); pDebug = false; } void DefaultDevice::addSimulationControl() { registerProperty(&SimulationSP, INDI_SWITCH); pSimulation = false; } void DefaultDevice::addConfigurationControl() { registerProperty(&ConfigProcessSP, INDI_SWITCH); } void DefaultDevice::addPollPeriodControl() { registerProperty(&PollPeriodNP, INDI_NUMBER); } void DefaultDevice::addAuxControls() { addDebugControl(); addSimulationControl(); addConfigurationControl(); addPollPeriodControl(); } void DefaultDevice::setDebug(bool enable) { if (pDebug == enable) { DebugSP.s = IPS_OK; IDSetSwitch(&DebugSP, nullptr); return; } IUResetSwitch(&DebugSP); if (enable) { ISwitch *sp = IUFindSwitch(&DebugSP, "ENABLE"); if (sp) { sp->s = ISS_ON; LOG_INFO("Debug is enabled."); } } else { ISwitch *sp = IUFindSwitch(&DebugSP, "DISABLE"); if (sp) { sp->s = ISS_ON; LOG_INFO("Debug is disabled."); } } pDebug = enable; // Inform logger if (Logger::updateProperties(enable) == false) DEBUG(Logger::DBG_WARNING, "setLogDebug: Logger error"); debugTriggered(enable); DebugSP.s = IPS_OK; IDSetSwitch(&DebugSP, nullptr); } void DefaultDevice::setSimulation(bool enable) { if (pSimulation == enable) { SimulationSP.s = IPS_OK; IDSetSwitch(&SimulationSP, nullptr); return; } IUResetSwitch(&SimulationSP); if (enable) { ISwitch *sp = IUFindSwitch(&SimulationSP, "ENABLE"); if (sp) { LOG_INFO("Simulation is enabled."); sp->s = ISS_ON; } } else { ISwitch *sp = IUFindSwitch(&SimulationSP, "DISABLE"); if (sp) { sp->s = ISS_ON; LOG_INFO("Simulation is disabled."); } } pSimulation = enable; simulationTriggered(enable); SimulationSP.s = IPS_OK; IDSetSwitch(&SimulationSP, nullptr); } bool DefaultDevice::isDebug() { return pDebug; } bool DefaultDevice::isSimulation() { return pSimulation; } void DefaultDevice::debugTriggered(bool enable) { INDI_UNUSED(enable); } void DefaultDevice::simulationTriggered(bool enable) { INDI_UNUSED(enable); } void DefaultDevice::ISGetProperties(const char *dev) { INDI_PROPERTY_TYPE pType; void *pPtr; if (isInit == false) { if (dev != nullptr) setDeviceName(dev); else if (*getDeviceName() == '\0') { char *envDev = getenv("INDIDEV"); if (envDev != nullptr) setDeviceName(envDev); else setDeviceName(getDefaultName()); } strncpy(ConnectionSP.device, getDeviceName(), MAXINDIDEVICE); initProperties(); addConfigurationControl(); // If we have no connections, move Driver Info to General Info tab if (connections.size() == 0) strncpy(DriverInfoTP.group, INFO_TAB, MAXINDINAME); } for (INDI::Property *oneProperty : pAll) { pType = oneProperty->getType(); pPtr = oneProperty->getProperty(); if (defineDynamicProperties == false && oneProperty->isDynamic()) continue; switch (pType) { case INDI_NUMBER: IDDefNumber(static_cast(pPtr), nullptr); break; case INDI_TEXT: IDDefText(static_cast(pPtr), nullptr); break; case INDI_SWITCH: IDDefSwitch(static_cast(pPtr), nullptr); break; case INDI_LIGHT: IDDefLight(static_cast(pPtr), nullptr); break; case INDI_BLOB: IDDefBLOB(static_cast(pPtr), nullptr); break; case INDI_UNKNOWN: break; } } // Remember debug & logging settings if (isInit == false) { loadConfig(true, "DEBUG"); loadConfig(true, "DEBUG_LEVEL"); loadConfig(true, "LOGGING_LEVEL"); loadConfig(true, "POLLING_PERIOD"); loadConfig(true, "LOG_OUTPUT"); } if (ConnectionModeS == nullptr) { if (connections.size() > 0) { ConnectionModeS = static_cast(malloc(connections.size() * sizeof(ISwitch))); ISwitch *sp = ConnectionModeS; for (Connection::Interface *oneConnection : connections) { IUFillSwitch(sp++, oneConnection->name().c_str(), oneConnection->label().c_str(), ISS_OFF); } activeConnection = connections[0]; ConnectionModeS[0].s = ISS_ON; IUFillSwitchVector(&ConnectionModeSP, ConnectionModeS, connections.size(), getDeviceName(), "CONNECTION_MODE", "Connection Mode", CONNECTION_TAB, IP_RW, ISR_1OFMANY, 60, IPS_IDLE); defineSwitch(&ConnectionModeSP); activeConnection->Activated(); loadConfig(true, "CONNECTION_MODE"); } } isInit = true; } void DefaultDevice::resetProperties() { std::vector::iterator orderi; INDI_PROPERTY_TYPE pType; void *pPtr; for (orderi = pAll.begin(); orderi != pAll.end(); orderi++) { pType = (*orderi)->getType(); pPtr = (*orderi)->getProperty(); switch (pType) { case INDI_NUMBER: static_cast(pPtr)->s = IPS_IDLE; IDSetNumber(static_cast(pPtr), nullptr); break; case INDI_TEXT: static_cast(pPtr)->s = IPS_IDLE; IDSetText(static_cast(pPtr), nullptr); break; case INDI_SWITCH: static_cast(pPtr)->s = IPS_IDLE; IDSetSwitch(static_cast(pPtr), nullptr); break; case INDI_LIGHT: static_cast(pPtr)->s = IPS_IDLE; IDSetLight(static_cast(pPtr), nullptr); break; case INDI_BLOB: static_cast(pPtr)->s = IPS_IDLE; IDSetBLOB(static_cast(pPtr), nullptr); break; case INDI_UNKNOWN: break; } } } void DefaultDevice::setConnected(bool status, IPState state, const char *msg) { ISwitch *sp = nullptr; ISwitchVectorProperty *svp = getSwitch(INDI::SP::CONNECTION); if (!svp) return; IUResetSwitch(svp); // Connect if (status) { sp = IUFindSwitch(svp, "CONNECT"); if (!sp) return; sp->s = ISS_ON; } // Disconnect else { sp = IUFindSwitch(svp, "DISCONNECT"); if (!sp) return; sp->s = ISS_ON; } svp->s = state; if (msg == nullptr) IDSetSwitch(svp, nullptr); else IDSetSwitch(svp, "%s", msg); } // This is a helper function // that just encapsulates the Indi way into our clean c++ way of doing things int DefaultDevice::SetTimer(uint32_t ms) { return IEAddTimer(ms, timerfunc, this); } // Just another helper to help encapsulate indi into a clean class void DefaultDevice::RemoveTimer(int id) { IERmTimer(id); return; } // This is just a placeholder // This function should be overriden by child classes if they use timers // So we should never get here void DefaultDevice::TimerHit() { return; } bool DefaultDevice::updateProperties() { // The base device has no properties to update return true; } uint16_t DefaultDevice::getDriverInterface() { return interfaceDescriptor; } void DefaultDevice::setDriverInterface(uint16_t value) { char interfaceStr[16]; interfaceDescriptor = value; snprintf(interfaceStr, 16, "%d", interfaceDescriptor); IUSaveText(&DriverInfoT[3], interfaceStr); } bool DefaultDevice::initProperties() { char versionStr[16]; char interfaceStr[16]; snprintf(versionStr, 16, "%d.%d", majorVersion, minorVersion); snprintf(interfaceStr, 16, "%d", interfaceDescriptor); IUFillSwitch(&ConnectionS[0], "CONNECT", "Connect", ISS_OFF); IUFillSwitch(&ConnectionS[1], "DISCONNECT", "Disconnect", ISS_ON); IUFillSwitchVector(&ConnectionSP, ConnectionS, 2, getDeviceName(), INDI::SP::CONNECTION, "Connection", "Main Control", IP_RW, ISR_1OFMANY, 60, IPS_IDLE); registerProperty(&ConnectionSP, INDI_SWITCH); IUFillText(&DriverInfoT[0], "DRIVER_NAME", "Name", getDriverName()); IUFillText(&DriverInfoT[1], "DRIVER_EXEC", "Exec", getDriverExec()); IUFillText(&DriverInfoT[2], "DRIVER_VERSION", "Version", versionStr); IUFillText(&DriverInfoT[3], "DRIVER_INTERFACE", "Interface", interfaceStr); IUFillTextVector(&DriverInfoTP, DriverInfoT, 4, getDeviceName(), "DRIVER_INFO", "Driver Info", CONNECTION_TAB, IP_RO, 60, IPS_IDLE); registerProperty(&DriverInfoTP, INDI_TEXT); IUFillSwitch(&DebugS[0], "ENABLE", "Enable", ISS_OFF); IUFillSwitch(&DebugS[1], "DISABLE", "Disable", ISS_ON); IUFillSwitchVector(&DebugSP, DebugS, NARRAY(DebugS), getDeviceName(), "DEBUG", "Debug", "Options", IP_RW, ISR_1OFMANY, 0, IPS_IDLE); IUFillSwitch(&SimulationS[0], "ENABLE", "Enable", ISS_OFF); IUFillSwitch(&SimulationS[1], "DISABLE", "Disable", ISS_ON); IUFillSwitchVector(&SimulationSP, SimulationS, NARRAY(SimulationS), getDeviceName(), "SIMULATION", "Simulation", "Options", IP_RW, ISR_1OFMANY, 0, IPS_IDLE); IUFillSwitch(&ConfigProcessS[0], "CONFIG_LOAD", "Load", ISS_OFF); IUFillSwitch(&ConfigProcessS[1], "CONFIG_SAVE", "Save", ISS_OFF); IUFillSwitch(&ConfigProcessS[2], "CONFIG_DEFAULT", "Default", ISS_OFF); IUFillSwitchVector(&ConfigProcessSP, ConfigProcessS, NARRAY(ConfigProcessS), getDeviceName(), "CONFIG_PROCESS", "Configuration", "Options", IP_RW, ISR_ATMOST1, 0, IPS_IDLE); IUFillNumber(&PollPeriodN[0], "PERIOD_MS", "Period (ms)", "%.f", 10, 60000, 1000, POLLMS); IUFillNumberVector(&PollPeriodNP, PollPeriodN, 1, getDeviceName(), "POLLING_PERIOD", "Polling", "Options", IP_RW, 0, IPS_IDLE); INDI::Logger::initProperties(this); // Ready the logger std::string logFile = getDriverExec(); DEBUG_CONF(logFile, Logger::file_off | Logger::screen_on, Logger::defaultlevel, Logger::defaultlevel); return true; } bool DefaultDevice::deleteProperty(const char *propertyName) { char errmsg[MAXRBUF]; if (propertyName == nullptr) { //while(!pAll.empty()) delete bar.back(), bar.pop_back(); IDDelete(getDeviceName(), nullptr, nullptr); return true; } // Keep dynamic properties in existing property list so they can be reused if (deleteDynamicProperties == false) { INDI::Property *prop = getProperty(propertyName); if (prop && prop->isDynamic()) { IDDelete(getDeviceName(), propertyName, nullptr); return true; } } if (removeProperty(propertyName, errmsg) == 0) { IDDelete(getDeviceName(), propertyName, nullptr); return true; } else return false; } void DefaultDevice::defineNumber(INumberVectorProperty *nvp) { registerProperty(nvp, INDI_NUMBER); IDDefNumber(nvp, nullptr); } void DefaultDevice::defineText(ITextVectorProperty *tvp) { registerProperty(tvp, INDI_TEXT); IDDefText(tvp, nullptr); } void DefaultDevice::defineSwitch(ISwitchVectorProperty *svp) { registerProperty(svp, INDI_SWITCH); IDDefSwitch(svp, nullptr); } void DefaultDevice::defineLight(ILightVectorProperty *lvp) { registerProperty(lvp, INDI_LIGHT); IDDefLight(lvp, nullptr); } void DefaultDevice::defineBLOB(IBLOBVectorProperty *bvp) { registerProperty(bvp, INDI_BLOB); IDDefBLOB(bvp, nullptr); } bool DefaultDevice::Connect() { if (isConnected()) return true; if (activeConnection == nullptr) { LOG_ERROR("No active connection defined."); return false; } bool rc = false; rc = activeConnection->Connect(); if (rc) { saveConfig(true, "CONNECTION_MODE"); if (POLLMS > 0) SetTimer(POLLMS); } return rc; } bool DefaultDevice::Disconnect() { if (isSimulation()) { DEBUGF(Logger::DBG_SESSION, "%s is offline.", getDeviceName()); return true; } if (activeConnection) { bool rc = activeConnection->Disconnect(); if (rc) { DEBUGF(Logger::DBG_SESSION, "%s is offline.", getDeviceName()); return true; } else return false; } return false; } void DefaultDevice::registerConnection(Connection::Interface *newConnection) { connections.push_back(newConnection); } bool DefaultDevice::unRegisterConnection(Connection::Interface *existingConnection) { auto i = std::begin(connections); while (i != std::end(connections)) { if (*i == existingConnection) { i = connections.erase(i); return true; } else ++i; } return false; } void DefaultDevice::setDefaultPollingPeriod(uint32_t period) { PollPeriodN[0].value = period; POLLMS = period; } } libindi/libs/indibase/indilogger.cpp0000664000175000017500000003335013263645557017034 0ustar jasemjasem/******************************************************************************* Copyright (C) 2012 Evidence Srl - www.evidence.eu.com Adapted to INDI Library by Jasem Mutlaq & Geehalel. This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2 as published by the Free Software Foundation. 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 Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. *******************************************************************************/ #include "indilogger.h" #include #include #include #include #include #include namespace INDI { char Logger::Tags[Logger::nlevels][MAXINDINAME] = { "ERROR", "WARNING", "INFO", "DEBUG", "DBG_EXTRA_1", "DBG_EXTRA_2", "DBG_EXTRA_3", "DBG_EXTRA_4" }; struct Logger::switchinit Logger::DebugLevelSInit[] = { { "DBG_ERROR", "Errors", ISS_ON, DBG_ERROR }, { "DBG_WARNING", "Warnings", ISS_ON, DBG_WARNING }, { "DBG_SESSION", "Messages", ISS_ON, DBG_SESSION }, { "DBG_DEBUG", "Driver Debug", ISS_OFF, DBG_DEBUG }, { "DBG_EXTRA_1", "Debug Extra 1", ISS_OFF, DBG_EXTRA_1 }, { "DBG_EXTRA_2", "Debug Extra 2", ISS_OFF, DBG_EXTRA_2 }, { "DBG_EXTRA_3", "Debug Extra 3", ISS_OFF, DBG_EXTRA_3 }, { "DBG_EXTRA_4", "Debug Extra 4", ISS_OFF, DBG_EXTRA_4 } }; struct Logger::switchinit Logger::LoggingLevelSInit[] = { { "LOG_ERROR", "Errors", ISS_ON, DBG_ERROR }, { "LOG_WARNING", "Warnings", ISS_ON, DBG_WARNING }, { "LOG_SESSION", "Messages", ISS_ON, DBG_SESSION }, { "LOG_DEBUG", "Driver Debug", ISS_OFF, DBG_DEBUG }, { "LOG_EXTRA_1", "Log Extra 1", ISS_OFF, DBG_EXTRA_1 }, { "LOG_EXTRA_2", "Log Extra 2", ISS_OFF, DBG_EXTRA_2 }, { "LOG_EXTRA_3", "Log Extra 3", ISS_OFF, DBG_EXTRA_3 }, { "LOG_EXTRA_4", "Log Extra 4", ISS_OFF, DBG_EXTRA_4 } }; ISwitch Logger::DebugLevelS[Logger::nlevels]; ISwitchVectorProperty Logger::DebugLevelSP; ISwitch Logger::LoggingLevelS[Logger::nlevels]; ISwitchVectorProperty Logger::LoggingLevelSP; ISwitch Logger::ConfigurationS[2]; ISwitchVectorProperty Logger::ConfigurationSP; INDI::DefaultDevice *Logger::parentDevice = nullptr; unsigned int Logger::fileVerbosityLevel_ = Logger::defaultlevel; unsigned int Logger::screenVerbosityLevel_ = Logger::defaultlevel; unsigned int Logger::rememberscreenlevel_ = Logger::defaultlevel; Logger::loggerConf Logger::configuration_ = Logger::screen_on | Logger::file_off; std::string Logger::logDir_; std::string Logger::logFile_; unsigned int Logger::nDevices = 0; unsigned int Logger::customLevel = 4; // Create dir recursively static int _mkdir(const char *dir, mode_t mode) { char tmp[PATH_MAX]; char *p = nullptr; size_t len; snprintf(tmp, sizeof(tmp), "%s", dir); len = strlen(tmp); if (tmp[len - 1] == '/') tmp[len - 1] = 0; for (p = tmp + 1; *p; p++) if (*p == '/') { *p = 0; if (mkdir(tmp, mode) == -1 && errno != EEXIST) return -1; *p = '/'; } if (mkdir(tmp, mode) == -1 && errno != EEXIST) return -1; return 0; } int Logger::addDebugLevel(const char *debugLevelName, const char *loggingLevelName) { // Cannot create any more levels if (customLevel == nlevels) return -1; strncpy(Tags[customLevel], loggingLevelName, MAXINDINAME); strncpy(DebugLevelSInit[customLevel].label, debugLevelName, MAXINDINAME); strncpy(LoggingLevelSInit[customLevel].label, debugLevelName, MAXINDINAME); return DebugLevelSInit[customLevel++].levelmask; } bool Logger::initProperties(DefaultDevice *device) { nDevices++; for (unsigned int i = 0; i < customLevel; i++) { IUFillSwitch(&DebugLevelS[i], DebugLevelSInit[i].name, DebugLevelSInit[i].label, DebugLevelSInit[i].state); DebugLevelS[i].aux = (void *)&DebugLevelSInit[i].levelmask; IUFillSwitch(&LoggingLevelS[i], LoggingLevelSInit[i].name, LoggingLevelSInit[i].label, LoggingLevelSInit[i].state); LoggingLevelS[i].aux = (void *)&LoggingLevelSInit[i].levelmask; } IUFillSwitchVector(&DebugLevelSP, DebugLevelS, customLevel, device->getDeviceName(), "DEBUG_LEVEL", "Debug Levels", OPTIONS_TAB, IP_RW, ISR_NOFMANY, 0, IPS_IDLE); IUFillSwitchVector(&LoggingLevelSP, LoggingLevelS, customLevel, device->getDeviceName(), "LOGGING_LEVEL", "Logging Levels", OPTIONS_TAB, IP_RW, ISR_NOFMANY, 0, IPS_IDLE); IUFillSwitch(&ConfigurationS[0], "CLIENT_DEBUG", "To Client", ISS_ON); IUFillSwitch(&ConfigurationS[1], "FILE_DEBUG", "To Log File", ISS_OFF); IUFillSwitchVector(&ConfigurationSP, ConfigurationS, 2, device->getDeviceName(), "LOG_OUTPUT", "Log Output", OPTIONS_TAB, IP_RW, ISR_NOFMANY, 0, IPS_IDLE); parentDevice = device; return true; } bool Logger::updateProperties(bool enable) { if (enable) { parentDevice->defineSwitch(&DebugLevelSP); parentDevice->defineSwitch(&LoggingLevelSP); screenVerbosityLevel_ = rememberscreenlevel_; parentDevice->defineSwitch(&ConfigurationSP); } else { parentDevice->deleteProperty(DebugLevelSP.name); parentDevice->deleteProperty(LoggingLevelSP.name); parentDevice->deleteProperty(ConfigurationSP.name); rememberscreenlevel_ = screenVerbosityLevel_; screenVerbosityLevel_ = defaultlevel; } return true; } bool Logger::saveConfigItems(FILE *fp) { IUSaveConfigSwitch(fp, &DebugLevelSP); IUSaveConfigSwitch(fp, &LoggingLevelSP); IUSaveConfigSwitch(fp, &ConfigurationSP); return true; } bool Logger::ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) { int debug_level = 0, log_level = 0, bitmask = 0, verbose_level = 0; if (strcmp(name, "DEBUG_LEVEL") == 0) { ISwitch *sw; IUUpdateSwitch(&DebugLevelSP, states, names, n); sw = IUFindOnSwitch(&DebugLevelSP); if (sw == nullptr) { DebugLevelSP.s = IPS_IDLE; IDSetSwitch(&DebugLevelSP, nullptr); screenVerbosityLevel_ = 0; return true; } for (int i = 0; i < DebugLevelSP.nsp; i++) { sw = &DebugLevelSP.sp[i]; bitmask = *((unsigned int *)sw->aux); if (sw->s == ISS_ON) { debug_level = i; verbose_level |= bitmask; } else verbose_level &= ~bitmask; } screenVerbosityLevel_ = verbose_level; DEBUGFDEVICE(dev, Logger::DBG_DEBUG, "Toggle Debug Level -- %s", DebugLevelSInit[debug_level].label); DebugLevelSP.s = IPS_OK; IDSetSwitch(&DebugLevelSP, nullptr); return true; } if (strcmp(name, "LOGGING_LEVEL") == 0) { ISwitch *sw; IUUpdateSwitch(&LoggingLevelSP, states, names, n); sw = IUFindOnSwitch(&LoggingLevelSP); if (sw == nullptr) { fileVerbosityLevel_ = 0; LoggingLevelSP.s = IPS_IDLE; IDSetSwitch(&LoggingLevelSP, nullptr); return true; } for (int i = 0; i < LoggingLevelSP.nsp; i++) { sw = &LoggingLevelSP.sp[i]; bitmask = *((unsigned int *)sw->aux); if (sw->s == ISS_ON) { log_level = i; fileVerbosityLevel_ |= bitmask; } else fileVerbosityLevel_ &= ~bitmask; } DEBUGFDEVICE(dev, Logger::DBG_DEBUG, "Toggle Logging Level -- %s", LoggingLevelSInit[log_level].label); LoggingLevelSP.s = IPS_OK; IDSetSwitch(&LoggingLevelSP, nullptr); return true; } if (!strcmp(name, "LOG_OUTPUT")) { ISwitch *sw; IUUpdateSwitch(&ConfigurationSP, states, names, n); sw = IUFindOnSwitch(&ConfigurationSP); if (sw == nullptr) { configuration_ = screen_off | file_off; ConfigurationSP.s = IPS_IDLE; IDSetSwitch(&ConfigurationSP, nullptr); return true; } bool wasFileOff = configuration_ & file_off; configuration_ = (loggerConf)0; if (ConfigurationS[1].s == ISS_ON) configuration_ = configuration_ | file_on; else configuration_ = configuration_ | file_off; if (ConfigurationS[0].s == ISS_ON) configuration_ = configuration_ | screen_on; else configuration_ = configuration_ | screen_off; // If file was off, then on again if (wasFileOff && (configuration_ & file_on)) Logger::getInstance().configure(logFile_, configuration_, fileVerbosityLevel_, screenVerbosityLevel_); ConfigurationSP.s = IPS_OK; IDSetSwitch(&ConfigurationSP, nullptr); return true; } return false; } // Definition (and initialization) of static attributes Logger *Logger::m_ = nullptr; #ifdef LOGGER_MULTITHREAD pthread_mutex_t Logger::lock_ = PTHREAD_MUTEX_INITIALIZER; inline void Logger::lock() { pthread_mutex_lock(&lock_); } inline void Logger::unlock() { pthread_mutex_unlock(&lock_); } #else void Logger::lock() { } void Logger::unlock() { } #endif Logger::Logger() : configured_(false) { gettimeofday(&initialTime_, nullptr); } void Logger::configure(const std::string &outputFile, const loggerConf configuration, const int fileVerbosityLevel, const int screenVerbosityLevel) { Logger::lock(); fileVerbosityLevel_ = fileVerbosityLevel; screenVerbosityLevel_ = screenVerbosityLevel; rememberscreenlevel_ = screenVerbosityLevel_; // Close the old stream, if needed if (configuration_ & file_on) out_.close(); // Compute a new file name, if needed if (outputFile != logFile_) { char ts_date[32], ts_time[32]; struct tm *tp; time_t t; time(&t); tp = gmtime(&t); strftime(ts_date, sizeof(ts_date), "%Y-%m-%d", tp); strftime(ts_time, sizeof(ts_time), "%H:%M:%S", tp); char dir[MAXRBUF]; snprintf(dir, MAXRBUF, "%s/.indi/logs/%s/%s", getenv("HOME"), ts_date, outputFile.c_str()); logDir_ = dir; char logFileBuf[MAXRBUF]; snprintf(logFileBuf, MAXRBUF, "%s/%s_%s.log", dir, outputFile.c_str(), ts_time); logFile_ = logFileBuf; } // Open a new stream, if needed if (configuration & file_on) { _mkdir(logDir_.c_str(), 0775); out_.open(logFile_.c_str(), std::ios::app); } configuration_ = configuration; configured_ = true; Logger::unlock(); } Logger::~Logger() { Logger::lock(); if (configuration_ & file_on) out_.close(); m_ = nullptr; Logger::unlock(); } Logger &Logger::getInstance() { Logger::lock(); if (m_ == nullptr) m_ = new Logger; Logger::unlock(); return *m_; } unsigned int Logger::rank(unsigned int l) { switch (l) { case DBG_ERROR: return 0; case DBG_WARNING: return 1; case DBG_SESSION: return 2; case DBG_EXTRA_1: return 4; case DBG_EXTRA_2: return 5; case DBG_EXTRA_3: return 6; case DBG_EXTRA_4: return 7; case DBG_DEBUG: default: return 3; } } void Logger::print(const char *devicename, const unsigned int verbosityLevel, const std::string &file, const int line, //const std::string& message, const char *message, ...) { INDI_UNUSED(file); INDI_UNUSED(line); bool filelog = (verbosityLevel & fileVerbosityLevel_) != 0; bool screenlog = (verbosityLevel & screenVerbosityLevel_) != 0; va_list ap; char msg[257]; char usec[7]; msg[256] = '\0'; va_start(ap, message); vsnprintf(msg, 257, message, ap); va_end(ap); if (!configured_) { //std::cerr << "Warning! Logger not configured!" << std::endl; std::cerr << msg << std::endl; return; } struct timeval currentTime, resTime; usec[6] = '\0'; gettimeofday(¤tTime, nullptr); timersub(¤tTime, &initialTime_, &resTime); #if defined(__APPLE__) snprintf(usec, 7, "%06d", resTime.tv_usec); #else snprintf(usec, 7, "%06ld", resTime.tv_usec); #endif Logger::lock(); if ((configuration_ & file_on) && filelog) { if (nDevices == 1) out_ << Tags[rank(verbosityLevel)] << "\t" << (resTime.tv_sec) << "." << (usec) << " sec" << "\t: " << msg << std::endl; else out_ << Tags[rank(verbosityLevel)] << "\t" << (resTime.tv_sec) << "." << (usec) << " sec" << "\t: [" << devicename << "] " << msg << std::endl; } if ((configuration_ & screen_on) && screenlog) IDMessage(devicename, "[%s] %s", Tags[rank(verbosityLevel)], msg); Logger::unlock(); } } libindi/libs/indibase/indidustcapinterface.h0000664000175000017500000000513613263645557020547 0ustar jasemjasem/* Dust Cap Interface Copyright (C) 2015 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 */ #pragma once #include "indibase.h" /** * \class DustCapInterface \brief Provides interface to implement remotely controlled dust cover \e IMPORTANT: initDustCapProperties() must be called before any other function to initilize the Dust Cap properties. \e IMPORTANT: processDustCapSwitch() must be called in your driver ISNewSwitch function. \author Jasem Mutlaq */ namespace INDI { class DustCapInterface { public: enum { CAP_PARK, CAP_UNPARK }; protected: DustCapInterface() = default; virtual ~DustCapInterface() = default; /** * @brief Park dust cap (close cover). Must be implemented by child. * @return If command completed immediatly, return IPS_OK. If command is in progress, return IPS_BUSY. If there is an error, return IPS_ALERT */ virtual IPState ParkCap(); /** * @brief unPark dust cap (open cover). Must be implemented by child. * @return If command completed immediatly, return IPS_OK. If command is in progress, return IPS_BUSY. If there is an error, return IPS_ALERT */ virtual IPState UnParkCap(); /** \brief Initilize dust cap properties. It is recommended to call this function within initProperties() of your primary device \param deviceName Name of the primary device \param groupName Group or tab name to be used to define focuser properties. */ void initDustCapProperties(const char *deviceName, const char *groupName); /** \brief Process dust cap switch properties */ bool processDustCapSwitch(const char *dev, const char *name, ISState *states, char *names[], int n); // Open/Close cover ISwitchVectorProperty ParkCapSP; ISwitch ParkCapS[2]; private: char dustCapName[MAXINDIDEVICE]; }; } libindi/libs/indibase/indirotatorinterface.cpp0000664000175000017500000002004413263645557021124 0ustar jasemjasem/* Rotator Interface Copyright (C) 2017 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 */ #include "indirotatorinterface.h" #include "defaultdevice.h" #include "indilogger.h" #include namespace INDI { RotatorInterface::RotatorInterface(DefaultDevice *defaultDevice) : m_defaultDevice(defaultDevice) { } void RotatorInterface::initProperties(const char *groupName) { // Rotator Angle IUFillNumber(&GotoRotatorN[0], "ANGLE", "Angle", "%.2f", 0, 360., 10., 0.); IUFillNumberVector(&GotoRotatorNP, GotoRotatorN, 1, m_defaultDevice->getDeviceName(), "ABS_ROTATOR_ANGLE", "Goto", groupName, IP_RW, 0, IPS_IDLE ); // Abort Rotator IUFillSwitch(&AbortRotatorS[0], "ABORT", "Abort", ISS_OFF); IUFillSwitchVector(&AbortRotatorSP, AbortRotatorS, 1, m_defaultDevice->getDeviceName(), "ROTATOR_ABORT_MOTION", "Abort Motion", groupName, IP_RW, ISR_ATMOST1, 0, IPS_IDLE); // Rotator Sync IUFillNumber(&SyncRotatorN[0], "ANGLE", "Angle", "%.2f", 0, 360., 10., 0.); IUFillNumberVector(&SyncRotatorNP, SyncRotatorN, 1, m_defaultDevice->getDeviceName(), "SYNC_ROTATOR_ANGLE", "Sync", groupName, IP_RW, 0, IPS_IDLE ); // Home Rotator IUFillSwitch(&HomeRotatorS[0], "HOME", "Start", ISS_OFF); IUFillSwitchVector(&HomeRotatorSP, HomeRotatorS, 1, m_defaultDevice->getDeviceName(), "ROTATOR_HOME", "Homing", groupName, IP_RW, ISR_ATMOST1, 0, IPS_IDLE); // Reverse Direction IUFillSwitch(&ReverseRotatorS[REVERSE_ENABLED], "REVERSE_ENABLED", "Enable", ISS_OFF); IUFillSwitch(&ReverseRotatorS[REVERSE_DISABLED], "REVERSE_DISABLED", "Disable", ISS_ON); IUFillSwitchVector(&ReverseRotatorSP, ReverseRotatorS, 2, m_defaultDevice->getDeviceName(), "ROTATOR_REVERSE", "Reverse", groupName, IP_RW, ISR_1OFMANY, 0, IPS_IDLE); } bool RotatorInterface::processNumber(const char *dev, const char *name, double values[], char *names[], int n) { INDI_UNUSED(names); INDI_UNUSED(n); if (dev != nullptr && strcmp(dev, m_defaultDevice->getDeviceName()) == 0) { //////////////////////////////////////////// // Move Absolute Angle //////////////////////////////////////////// if (strcmp(name, GotoRotatorNP.name) == 0) { if (values[0] == GotoRotatorN[0].value) { GotoRotatorNP.s = IPS_OK; IDSetNumber(&GotoRotatorNP, nullptr); return true; } GotoRotatorNP.s = MoveRotator(values[0]); IDSetNumber(&GotoRotatorNP, nullptr); if (GotoRotatorNP.s == IPS_BUSY) DEBUGFDEVICE(m_defaultDevice->getDeviceName(), Logger::DBG_SESSION, "Rotator moving to %.2f degrees...", values[0]); return true; } //////////////////////////////////////////// // Sync //////////////////////////////////////////// else if (strcmp(name, SyncRotatorNP.name) == 0) { if (values[0] == GotoRotatorN[0].value) { SyncRotatorNP.s = IPS_OK; IDSetNumber(&SyncRotatorNP, nullptr); return true; } bool rc = SyncRotator(values[0]); SyncRotatorNP.s = rc ? IPS_OK : IPS_ALERT; if (rc) SyncRotatorN[0].value = values[0]; IDSetNumber(&SyncRotatorNP, nullptr); return true; } } return false; } bool RotatorInterface::processSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) { INDI_UNUSED(states); INDI_UNUSED(names); INDI_UNUSED(n); if (dev != nullptr && strcmp(dev, m_defaultDevice->getDeviceName()) == 0) { //////////////////////////////////////////// // Abort //////////////////////////////////////////// if (strcmp(name, AbortRotatorSP.name) == 0) { AbortRotatorSP.s = AbortRotator() ? IPS_OK : IPS_ALERT; IDSetSwitch(&AbortRotatorSP, nullptr); if (AbortRotatorSP.s == IPS_OK) { if (GotoRotatorNP.s != IPS_OK) { GotoRotatorNP.s = IPS_OK; IDSetNumber(&GotoRotatorNP, nullptr); } } return true; } //////////////////////////////////////////// // Home //////////////////////////////////////////// else if (strcmp(name, HomeRotatorSP.name) == 0) { HomeRotatorSP.s = HomeRotator(); IUResetSwitch(&HomeRotatorSP); if (HomeRotatorSP.s == IPS_BUSY) HomeRotatorS[0].s = ISS_ON; IDSetSwitch(&HomeRotatorSP, nullptr); return true; } //////////////////////////////////////////// // Reverse Rotator //////////////////////////////////////////// else if (strcmp(name, ReverseRotatorSP.name) == 0) { bool rc = false; bool enabled = (!strcmp(IUFindOnSwitchName(states, names, n), "ENABLED")); rc = ReverseRotator(enabled); if (rc) { IUUpdateSwitch(&ReverseRotatorSP, states, names, n); ReverseRotatorSP.s = IPS_OK; DEBUGFDEVICE(m_defaultDevice->getDeviceName(), Logger::DBG_SESSION, "Rotator direction is %s.", (enabled ? "reversed" : "normal")); } else { ReverseRotatorSP.s = IPS_ALERT; DEBUGDEVICE(m_defaultDevice->getDeviceName(), Logger::DBG_SESSION, "Rotator reverse direction failed."); } IDSetSwitch(&ReverseRotatorSP, nullptr); return true; } } return false; } bool RotatorInterface::updateProperties() { if (m_defaultDevice->isConnected()) { m_defaultDevice->defineNumber(&GotoRotatorNP); if (CanAbort()) m_defaultDevice->defineSwitch(&AbortRotatorSP); if (CanSync()) m_defaultDevice->defineNumber(&SyncRotatorNP); if (CanHome()) m_defaultDevice->defineSwitch(&HomeRotatorSP); if (CanReverse()) m_defaultDevice->defineSwitch(&ReverseRotatorSP); } else { m_defaultDevice->deleteProperty(GotoRotatorNP.name); if (CanAbort()) m_defaultDevice->deleteProperty(AbortRotatorSP.name); if (CanSync()) m_defaultDevice->deleteProperty(SyncRotatorNP.name); if (CanHome()) m_defaultDevice->deleteProperty(HomeRotatorSP.name); if (CanReverse()) m_defaultDevice->deleteProperty(ReverseRotatorSP.name); } return true; } bool RotatorInterface::SyncRotator(double angle) { INDI_UNUSED(angle); DEBUGDEVICE(m_defaultDevice->getDeviceName(), Logger::DBG_ERROR, "Rotator does not support syncing."); return false; } IPState RotatorInterface::HomeRotator() { DEBUGDEVICE(m_defaultDevice->getDeviceName(), Logger::DBG_ERROR, "Rotator does not support homing."); return IPS_ALERT; } bool RotatorInterface::AbortRotator() { DEBUGDEVICE(m_defaultDevice->getDeviceName(), Logger::DBG_ERROR, "Rotator does not support abort."); return false; } bool RotatorInterface::ReverseRotator(bool enabled) { INDI_UNUSED(enabled); DEBUGDEVICE(m_defaultDevice->getDeviceName(), Logger::DBG_ERROR, "Rotator does not support reverse."); return false; } } libindi/libs/indibase/indifilterwheel.h0000664000175000017500000000666413263645557017544 0ustar jasemjasem/******************************************************************************* Copyright(c) 2010, 2011 Gerry Rozema. All rights reserved. This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2 as published by the Free Software Foundation. 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 Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. *******************************************************************************/ #pragma once #include "defaultdevice.h" #include "indifilterinterface.h" /** * \class FilterWheel * \brief Class to provide general functionality of a filter wheel device. * * Developers need to subclass FilterWheel to implement any driver for filter wheels within INDI. * * \author Gerry Rozema, Jasem Mutlaq * \see FilterInterface */ namespace INDI { class FilterWheel : public DefaultDevice, public FilterInterface { protected: FilterWheel(); virtual ~FilterWheel() = default; public: /** * \struct FilterConnection * \brief Holds the connection mode of the Filter. */ enum { CONNECTION_NONE = 1 << 0, /** Do not use any connection plugin */ CONNECTION_SERIAL = 1 << 1, /** For regular serial and bluetooth connections */ CONNECTION_TCP = 1 << 2 /** For Wired and WiFI connections */ } FilterConnection; virtual bool initProperties(); virtual bool updateProperties(); virtual void ISGetProperties(const char *dev); virtual bool ISSnoopDevice(XMLEle *root); virtual bool ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n); virtual bool ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n); virtual bool ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n); static void joystickHelper(const char *joystick_n, double mag, double angle, void *context); static void buttonHelper(const char *button_n, ISState state, void *context); /** * @brief setFilterConnection Set Filter connection mode. Child class should call this in the constructor before Filter registers * any connection interfaces * @param value ORed combination of FilterConnection values. */ void setFilterConnection(const uint8_t &value); /** * @return Get current Filter connection mode */ uint8_t getFilterConnection() const; protected: virtual bool saveConfigItems(FILE *fp); virtual int QueryFilter(); virtual bool SelectFilter(int); /** \brief perform handshake with device to check communication */ virtual bool Handshake(); void processJoystick(const char *joystick_n, double mag, double angle); void processButton(const char *button_n, ISState state); Controller *controller; Connection::Serial *serialConnection = NULL; Connection::TCP *tcpConnection = NULL; /// For Serial & TCP connections int PortFD = -1; private: bool callHandshake(); uint8_t filterConnection = CONNECTION_NONE; }; } libindi/libs/indibase/hid_win.c0000664000175000017500000007011613263645557015773 0ustar jasemjasem/* HIDAPI - Multi-Platform library for communication with HID devices. Copyright (c) 2009 by Alan Ott, Signal 11 Software (8/22/2009) All Rights Reserved. Changes for use with SX Filter Wheel INDI Driver by CloudMakers - 11/6/2012 This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program 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 General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. The full GNU General Public License is included in this distribution in the file called LICENSE. These files may also be found in the public source code repository located: http://github.com/signal11/hidapi */ #include #ifndef _NTDEF_ typedef LONG NTSTATUS; #endif #ifdef __MINGW32__ #include #include #endif #ifdef __CYGWIN__ #include #define _wcsdup wcsdup #endif //#define HIDAPI_USE_DDK #ifdef __cplusplus extern "C" { #endif #include #include #ifdef HIDAPI_USE_DDK #include #endif // Copied from inc/ddk/hidclass.h, part of the Windows DDK. #define HID_OUT_CTL_CODE(id) CTL_CODE(FILE_DEVICE_KEYBOARD, (id), METHOD_OUT_DIRECT, FILE_ANY_ACCESS) #define IOCTL_HID_GET_FEATURE HID_OUT_CTL_CODE(100) #ifdef __cplusplus } // extern "C" #endif #include #include #include "hidapi.h" #ifdef _MSC_VER // Thanks Microsoft, but I know how to use strncpy(). #pragma warning(disable : 4996) #endif #ifdef __cplusplus extern "C" { #endif #ifndef HIDAPI_USE_DDK // Since we're not building with the DDK, and the HID header // files aren't part of the SDK, we have to define all this // stuff here. In lookup_functions(), the function pointers // defined below are set. typedef struct _HIDD_ATTRIBUTES { ULONG Size; USHORT VendorID; USHORT ProductID; USHORT VersionNumber; } HIDD_ATTRIBUTES, *PHIDD_ATTRIBUTES; typedef USHORT USAGE; typedef struct _HIDP_CAPS { USAGE Usage; USAGE UsagePage; USHORT InputReportByteLength; USHORT OutputReportByteLength; USHORT FeatureReportByteLength; USHORT Reserved[17]; USHORT fields_not_used_by_hidapi[10]; } HIDP_CAPS, *PHIDP_CAPS; typedef void *PHIDP_PREPARSED_DATA; #define HIDP_STATUS_SUCCESS 0x110000 typedef BOOLEAN(__stdcall *HidD_GetAttributes_)(HANDLE device, PHIDD_ATTRIBUTES attrib); typedef BOOLEAN(__stdcall *HidD_GetSerialNumberString_)(HANDLE device, PVOID buffer, ULONG buffer_len); typedef BOOLEAN(__stdcall *HidD_GetManufacturerString_)(HANDLE handle, PVOID buffer, ULONG buffer_len); typedef BOOLEAN(__stdcall *HidD_GetProductString_)(HANDLE handle, PVOID buffer, ULONG buffer_len); typedef BOOLEAN(__stdcall *HidD_SetFeature_)(HANDLE handle, PVOID data, ULONG length); typedef BOOLEAN(__stdcall *HidD_GetFeature_)(HANDLE handle, PVOID data, ULONG length); typedef BOOLEAN(__stdcall *HidD_GetIndexedString_)(HANDLE handle, ULONG string_index, PVOID buffer, ULONG buffer_len); typedef BOOLEAN(__stdcall *HidD_GetPreparsedData_)(HANDLE handle, PHIDP_PREPARSED_DATA *preparsed_data); typedef BOOLEAN(__stdcall *HidD_FreePreparsedData_)(PHIDP_PREPARSED_DATA preparsed_data); typedef NTSTATUS(__stdcall *HidP_GetCaps_)(PHIDP_PREPARSED_DATA preparsed_data, HIDP_CAPS *caps); static HidD_GetAttributes_ HidD_GetAttributes; static HidD_GetSerialNumberString_ HidD_GetSerialNumberString; static HidD_GetManufacturerString_ HidD_GetManufacturerString; static HidD_GetProductString_ HidD_GetProductString; static HidD_SetFeature_ HidD_SetFeature; static HidD_GetFeature_ HidD_GetFeature; static HidD_GetIndexedString_ HidD_GetIndexedString; static HidD_GetPreparsedData_ HidD_GetPreparsedData; static HidD_FreePreparsedData_ HidD_FreePreparsedData; static HidP_GetCaps_ HidP_GetCaps; static HMODULE lib_handle = NULL; static BOOLEAN initialized = FALSE; #endif // HIDAPI_USE_DDK struct hid_device_ { HANDLE device_handle; BOOL blocking; USHORT output_report_length; size_t input_report_length; void *last_error_str; DWORD last_error_num; BOOL read_pending; char *read_buf; OVERLAPPED ol; }; static hid_device *new_hid_device() { hid_device *dev = (hid_device *)calloc(1, sizeof(hid_device)); dev->device_handle = INVALID_HANDLE_VALUE; dev->blocking = TRUE; dev->output_report_length = 0; dev->input_report_length = 0; dev->last_error_str = NULL; dev->last_error_num = 0; dev->read_pending = FALSE; dev->read_buf = NULL; memset(&dev->ol, 0, sizeof(dev->ol)); dev->ol.hEvent = CreateEvent(NULL, FALSE, FALSE /*inital state f=nonsignaled*/, NULL); return dev; } static void register_error(hid_device *device, const char *op) { WCHAR *ptr, *msg; FormatMessageW(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, NULL, GetLastError(), MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPWSTR)&msg, 0 /*sz*/, NULL); // Get rid of the CR and LF that FormatMessage() sticks at the // end of the message. Thanks Microsoft! ptr = msg; while (*ptr) { if (*ptr == '\r') { *ptr = 0x0000; break; } ptr++; } // Store the message off in the Device entry so that // the hid_error() function can pick it up. LocalFree(device->last_error_str); device->last_error_str = msg; } #ifndef HIDAPI_USE_DDK static int lookup_functions() { lib_handle = LoadLibraryA("hid.dll"); if (lib_handle) { #define RESOLVE(x) \ x = (x##_)GetProcAddress(lib_handle, #x); \ if (!x) \ return -1; RESOLVE(HidD_GetAttributes); RESOLVE(HidD_GetSerialNumberString); RESOLVE(HidD_GetManufacturerString); RESOLVE(HidD_GetProductString); RESOLVE(HidD_SetFeature); RESOLVE(HidD_GetFeature); RESOLVE(HidD_GetIndexedString); RESOLVE(HidD_GetPreparsedData); RESOLVE(HidD_FreePreparsedData); RESOLVE(HidP_GetCaps); #undef RESOLVE } else return -1; return 0; } #endif static HANDLE open_device(const char *path, BOOL enumerate) { HANDLE handle; DWORD desired_access = (enumerate) ? 0 : (GENERIC_WRITE | GENERIC_READ); DWORD share_mode = (enumerate) ? FILE_SHARE_READ | FILE_SHARE_WRITE : FILE_SHARE_READ; handle = CreateFileA(path, desired_access, share_mode, NULL, OPEN_EXISTING, FILE_FLAG_OVERLAPPED, //FILE_ATTRIBUTE_NORMAL, 0); return handle; } int HID_API_EXPORT hid_init(void) { #ifndef HIDAPI_USE_DDK if (!initialized) { if (lookup_functions() < 0) { hid_exit(); return -1; } initialized = TRUE; } #endif return 0; } int HID_API_EXPORT hid_exit(void) { #ifndef HIDAPI_USE_DDK if (lib_handle) FreeLibrary(lib_handle); lib_handle = NULL; initialized = FALSE; #endif return 0; } struct hid_device_info HID_API_EXPORT *HID_API_CALL hid_enumerate(unsigned short vendor_id, unsigned short product_id) { BOOL res; struct hid_device_info *root = NULL; // return object struct hid_device_info *cur_dev = NULL; // Windows objects for interacting with the driver. GUID InterfaceClassGuid = { 0x4d1e55b2, 0xf16f, 0x11cf, { 0x88, 0xcb, 0x00, 0x11, 0x11, 0x00, 0x00, 0x30 } }; SP_DEVINFO_DATA devinfo_data; SP_DEVICE_INTERFACE_DATA device_interface_data; SP_DEVICE_INTERFACE_DETAIL_DATA_A *device_interface_detail_data = NULL; HDEVINFO device_info_set = INVALID_HANDLE_VALUE; int device_index = 0; int i; if (hid_init() < 0) return NULL; // Initialize the Windows objects. memset(&devinfo_data, 0x0, sizeof(devinfo_data)); devinfo_data.cbSize = sizeof(SP_DEVINFO_DATA); device_interface_data.cbSize = sizeof(SP_DEVICE_INTERFACE_DATA); // Get information for all the devices belonging to the HID class. device_info_set = SetupDiGetClassDevsA(&InterfaceClassGuid, NULL, NULL, DIGCF_PRESENT | DIGCF_DEVICEINTERFACE); // Iterate over each device in the HID class, looking for the right one. for (;;) { HANDLE write_handle = INVALID_HANDLE_VALUE; DWORD required_size = 0; HIDD_ATTRIBUTES attrib; res = SetupDiEnumDeviceInterfaces(device_info_set, NULL, &InterfaceClassGuid, device_index, &device_interface_data); if (!res) { // A return of FALSE from this function means that // there are no more devices. break; } // Call with 0-sized detail size, and let the function // tell us how long the detail struct needs to be. The // size is put in &required_size. res = SetupDiGetDeviceInterfaceDetailA(device_info_set, &device_interface_data, NULL, 0, &required_size, NULL); // Allocate a long enough structure for device_interface_detail_data. device_interface_detail_data = (SP_DEVICE_INTERFACE_DETAIL_DATA_A *)malloc(required_size); device_interface_detail_data->cbSize = sizeof(SP_DEVICE_INTERFACE_DETAIL_DATA_A); // Get the detailed data for this device. The detail data gives us // the device path for this device, which is then passed into // CreateFile() to get a handle to the device. res = SetupDiGetDeviceInterfaceDetailA(device_info_set, &device_interface_data, device_interface_detail_data, required_size, NULL, NULL); if (!res) { //register_error(dev, "Unable to call SetupDiGetDeviceInterfaceDetail"); // Continue to the next device. goto cont; } // Make sure this device is of Setup Class "HIDClass" and has a // driver bound to it. for (i = 0;; i++) { char driver_name[256]; // Populate devinfo_data. This function will return failure // when there are no more interfaces left. res = SetupDiEnumDeviceInfo(device_info_set, i, &devinfo_data); if (!res) goto cont; res = SetupDiGetDeviceRegistryPropertyA(device_info_set, &devinfo_data, SPDRP_CLASS, NULL, (PBYTE)driver_name, sizeof(driver_name), NULL); if (!res) goto cont; if (strcmp(driver_name, "HIDClass") == 0) { // See if there's a driver bound. res = SetupDiGetDeviceRegistryPropertyA(device_info_set, &devinfo_data, SPDRP_DRIVER, NULL, (PBYTE)driver_name, sizeof(driver_name), NULL); if (res) break; } } //wprintf(L"HandleName: %s\n", device_interface_detail_data->DevicePath); // Open a handle to the device write_handle = open_device(device_interface_detail_data->DevicePath, TRUE); // Check validity of write_handle. if (write_handle == INVALID_HANDLE_VALUE) { // Unable to open the device. //register_error(dev, "CreateFile"); goto cont_close; } // Get the Vendor ID and Product ID for this device. attrib.Size = sizeof(HIDD_ATTRIBUTES); HidD_GetAttributes(write_handle, &attrib); //wprintf(L"Product/Vendor: %x %x\n", attrib.ProductID, attrib.VendorID); // Check the VID/PID to see if we should add this // device to the enumeration list. if ((vendor_id == 0x0 && product_id == 0x0) || (attrib.VendorID == vendor_id && attrib.ProductID == product_id)) { #define WSTR_LEN 512 const char *str; struct hid_device_info *tmp; PHIDP_PREPARSED_DATA pp_data = NULL; HIDP_CAPS caps; BOOLEAN res; NTSTATUS nt_res; wchar_t wstr[WSTR_LEN]; // TODO: Determine Size size_t len; /* VID/PID match. Create the record. */ tmp = (struct hid_device_info *)calloc(1, sizeof(struct hid_device_info)); if (cur_dev) { cur_dev->next = tmp; } else { root = tmp; } cur_dev = tmp; // Get the Usage Page and Usage for this device. res = HidD_GetPreparsedData(write_handle, &pp_data); if (res) { nt_res = HidP_GetCaps(pp_data, &caps); if (nt_res == HIDP_STATUS_SUCCESS) { cur_dev->usage_page = caps.UsagePage; cur_dev->usage = caps.Usage; } HidD_FreePreparsedData(pp_data); } /* Fill out the record */ cur_dev->next = NULL; str = device_interface_detail_data->DevicePath; if (str) { len = strlen(str); cur_dev->path = (char *)calloc(len + 1, sizeof(char)); strncpy(cur_dev->path, str, len + 1); cur_dev->path[len] = '\0'; } else cur_dev->path = NULL; /* Serial Number */ res = HidD_GetSerialNumberString(write_handle, wstr, sizeof(wstr)); wstr[WSTR_LEN - 1] = 0x0000; if (res) { cur_dev->serial_number = _wcsdup(wstr); } /* Manufacturer String */ res = HidD_GetManufacturerString(write_handle, wstr, sizeof(wstr)); wstr[WSTR_LEN - 1] = 0x0000; if (res) { cur_dev->manufacturer_string = _wcsdup(wstr); } /* Product String */ res = HidD_GetProductString(write_handle, wstr, sizeof(wstr)); wstr[WSTR_LEN - 1] = 0x0000; if (res) { cur_dev->product_string = _wcsdup(wstr); } /* VID/PID */ cur_dev->vendor_id = attrib.VendorID; cur_dev->product_id = attrib.ProductID; /* Release Number */ cur_dev->release_number = attrib.VersionNumber; /* Interface Number. It can sometimes be parsed out of the path on Windows if a device has multiple interfaces. See http://msdn.microsoft.com/en-us/windows/hardware/gg487473 or search for "Hardware IDs for HID Devices" at MSDN. If it's not in the path, it's set to -1. */ cur_dev->interface_number = -1; if (cur_dev->path) { char *interface_component = strstr(cur_dev->path, "&mi_"); if (interface_component) { char *hex_str = interface_component + 4; char *endptr = NULL; cur_dev->interface_number = strtol(hex_str, &endptr, 16); if (endptr == hex_str) { /* The parsing failed. Set interface_number to -1. */ cur_dev->interface_number = -1; } } } } cont_close: CloseHandle(write_handle); cont: // We no longer need the detail data. It can be freed free(device_interface_detail_data); device_index++; } // Close the device information handle. SetupDiDestroyDeviceInfoList(device_info_set); return root; } void HID_API_EXPORT HID_API_CALL hid_free_enumeration(struct hid_device_info *devs) { // TODO: Merge this with the Linux version. This function is platform-independent. struct hid_device_info *d = devs; while (d) { struct hid_device_info *next = d->next; free(d->path); free(d->serial_number); free(d->manufacturer_string); free(d->product_string); free(d); d = next; } } HID_API_EXPORT hid_device *HID_API_CALL hid_open(unsigned short vendor_id, unsigned short product_id, const wchar_t *serial_number) { // TODO: Merge this functions with the Linux version. This function should be platform independent. struct hid_device_info *devs, *cur_dev; const char *path_to_open = NULL; hid_device *handle = NULL; devs = hid_enumerate(vendor_id, product_id); cur_dev = devs; while (cur_dev) { if (cur_dev->vendor_id == vendor_id && cur_dev->product_id == product_id) { if (serial_number) { if (wcscmp(serial_number, cur_dev->serial_number) == 0) { path_to_open = cur_dev->path; break; } } else { path_to_open = cur_dev->path; break; } } cur_dev = cur_dev->next; } if (path_to_open) { /* Open the device */ handle = hid_open_path(path_to_open); } hid_free_enumeration(devs); return handle; } HID_API_EXPORT hid_device *HID_API_CALL hid_open_path(const char *path) { hid_device *dev; HIDP_CAPS caps; PHIDP_PREPARSED_DATA pp_data = NULL; BOOLEAN res; NTSTATUS nt_res; if (hid_init() < 0) { return NULL; } dev = new_hid_device(); // Open a handle to the device dev->device_handle = open_device(path, FALSE); // Check validity of write_handle. if (dev->device_handle == INVALID_HANDLE_VALUE) { // Unable to open the device. register_error(dev, "CreateFile"); goto err; } // Get the Input Report length for the device. res = HidD_GetPreparsedData(dev->device_handle, &pp_data); if (!res) { register_error(dev, "HidD_GetPreparsedData"); goto err; } nt_res = HidP_GetCaps(pp_data, &caps); if (nt_res != HIDP_STATUS_SUCCESS) { register_error(dev, "HidP_GetCaps"); goto err_pp_data; } dev->output_report_length = caps.OutputReportByteLength; dev->input_report_length = caps.InputReportByteLength; HidD_FreePreparsedData(pp_data); dev->read_buf = (char *)malloc(dev->input_report_length); return dev; err_pp_data: HidD_FreePreparsedData(pp_data); err: CloseHandle(dev->device_handle); free(dev); return NULL; } int HID_API_EXPORT HID_API_CALL hid_write(hid_device *dev, const unsigned char *data, size_t length) { DWORD bytes_written; BOOL res; OVERLAPPED ol; unsigned char *buf; memset(&ol, 0, sizeof(ol)); /* Make sure the right number of bytes are passed to WriteFile. Windows expects the number of bytes which are in the _longest_ report (plus one for the report number) bytes even if the data is a report which is shorter than that. Windows gives us this value in caps.OutputReportByteLength. If a user passes in fewer bytes than this, create a temporary buffer which is the proper size. */ if (length >= dev->output_report_length) { /* The user passed the right number of bytes. Use the buffer as-is. */ buf = (unsigned char *)data; } else { /* Create a temporary buffer and copy the user's data into it, padding the rest with zeros. */ buf = (unsigned char *)malloc(dev->output_report_length); memcpy(buf, data, length); memset(buf + length, 0, dev->output_report_length - length); length = dev->output_report_length; } res = WriteFile(dev->device_handle, buf, length, NULL, &ol); if (!res) { if (GetLastError() != ERROR_IO_PENDING) { // WriteFile() failed. Return error. register_error(dev, "WriteFile"); bytes_written = -1; goto end_of_function; } } // Wait here until the write is done. This makes // hid_write() synchronous. res = GetOverlappedResult(dev->device_handle, &ol, &bytes_written, TRUE /*wait*/); if (!res) { // The Write operation failed. register_error(dev, "WriteFile"); bytes_written = -1; goto end_of_function; } end_of_function: if (buf != data) free(buf); return bytes_written; } int HID_API_EXPORT HID_API_CALL hid_read_timeout(hid_device *dev, unsigned char *data, size_t length, int milliseconds) { DWORD bytes_read = 0; BOOL res; // Copy the handle for convenience. HANDLE ev = dev->ol.hEvent; if (!dev->read_pending) { // Start an Overlapped I/O read. dev->read_pending = TRUE; memset(dev->read_buf, 0, dev->input_report_length); ResetEvent(ev); res = ReadFile(dev->device_handle, dev->read_buf, dev->input_report_length, &bytes_read, &dev->ol); if (!res) { if (GetLastError() != ERROR_IO_PENDING) { // ReadFile() has failed. // Clean up and return error. CancelIo(dev->device_handle); dev->read_pending = FALSE; goto end_of_function; } } } if (milliseconds >= 0) { // See if there is any data yet. res = WaitForSingleObject(ev, milliseconds); if (res != WAIT_OBJECT_0) { // There was no data this time. Return zero bytes available, // but leave the Overlapped I/O running. return 0; } } // Either WaitForSingleObject() told us that ReadFile has completed, or // we are in non-blocking mode. Get the number of bytes read. The actual // data has been copied to the data[] array which was passed to ReadFile(). res = GetOverlappedResult(dev->device_handle, &dev->ol, &bytes_read, TRUE /*wait*/); // Set pending back to false, even if GetOverlappedResult() returned error. dev->read_pending = FALSE; if (res && bytes_read > 0) { if (dev->read_buf[0] == 0x0) { /* If report numbers aren't being used, but Windows sticks a report number (0x0) on the beginning of the report anyway. To make this work like the other platforms, and to make it work more like the HID spec, we'll skip over this byte. */ size_t copy_len; bytes_read--; copy_len = length > bytes_read ? bytes_read : length; memcpy(data, dev->read_buf + 1, copy_len); } else { /* Copy the whole buffer, report number and all. */ size_t copy_len = length > bytes_read ? bytes_read : length; memcpy(data, dev->read_buf, copy_len); } } end_of_function: if (!res) { register_error(dev, "GetOverlappedResult"); return -1; } return bytes_read; } int HID_API_EXPORT HID_API_CALL hid_read(hid_device *dev, unsigned char *data, size_t length) { return hid_read_timeout(dev, data, length, (dev->blocking) ? -1 : 0); } int HID_API_EXPORT HID_API_CALL hid_set_nonblocking(hid_device *dev, int nonblock) { dev->blocking = !nonblock; return 0; /* Success */ } int HID_API_EXPORT HID_API_CALL hid_send_feature_report(hid_device *dev, const unsigned char *data, size_t length) { BOOL res = HidD_SetFeature(dev->device_handle, (PVOID)data, length); if (!res) { register_error(dev, "HidD_SetFeature"); return -1; } return length; } int HID_API_EXPORT HID_API_CALL hid_get_feature_report(hid_device *dev, unsigned char *data, size_t length) { BOOL res; #if 0 res = HidD_GetFeature(dev->device_handle, data, length); if (!res) { register_error(dev, "HidD_GetFeature"); return -1; } return 0; /* HidD_GetFeature() doesn't give us an actual length, unfortunately */ #else DWORD bytes_returned; OVERLAPPED ol; memset(&ol, 0, sizeof(ol)); res = DeviceIoControl(dev->device_handle, IOCTL_HID_GET_FEATURE, data, length, data, length, &bytes_returned, &ol); if (!res) { if (GetLastError() != ERROR_IO_PENDING) { // DeviceIoControl() failed. Return error. register_error(dev, "Send Feature Report DeviceIoControl"); return -1; } } // Wait here until the write is done. This makes // hid_get_feature_report() synchronous. res = GetOverlappedResult(dev->device_handle, &ol, &bytes_returned, TRUE /*wait*/); if (!res) { // The operation failed. register_error(dev, "Send Feature Report GetOverLappedResult"); return -1; } return bytes_returned; #endif } void HID_API_EXPORT HID_API_CALL hid_close(hid_device *dev) { if (!dev) return; CancelIo(dev->device_handle); CloseHandle(dev->ol.hEvent); CloseHandle(dev->device_handle); LocalFree(dev->last_error_str); free(dev->read_buf); free(dev); } int HID_API_EXPORT_CALL HID_API_CALL hid_get_manufacturer_string(hid_device *dev, wchar_t *string, size_t maxlen) { BOOL res; res = HidD_GetManufacturerString(dev->device_handle, string, 2 * maxlen); if (!res) { register_error(dev, "HidD_GetManufacturerString"); return -1; } return 0; } int HID_API_EXPORT_CALL HID_API_CALL hid_get_product_string(hid_device *dev, wchar_t *string, size_t maxlen) { BOOL res; res = HidD_GetProductString(dev->device_handle, string, 2 * maxlen); if (!res) { register_error(dev, "HidD_GetProductString"); return -1; } return 0; } int HID_API_EXPORT_CALL HID_API_CALL hid_get_serial_number_string(hid_device *dev, wchar_t *string, size_t maxlen) { BOOL res; res = HidD_GetSerialNumberString(dev->device_handle, string, 2 * maxlen); if (!res) { register_error(dev, "HidD_GetSerialNumberString"); return -1; } return 0; } int HID_API_EXPORT_CALL HID_API_CALL hid_get_indexed_string(hid_device *dev, int string_index, wchar_t *string, size_t maxlen) { BOOL res; res = HidD_GetIndexedString(dev->device_handle, string_index, string, 2 * maxlen); if (!res) { register_error(dev, "HidD_GetIndexedString"); return -1; } return 0; } HID_API_EXPORT const wchar_t *HID_API_CALL hid_error(hid_device *dev) { return (wchar_t *)dev->last_error_str; } //#define PICPGM //#define S11 #define P32 #ifdef S11 unsigned short VendorID = 0xa0a0; unsigned short ProductID = 0x0001; #endif #ifdef P32 unsigned short VendorID = 0x04d8; unsigned short ProductID = 0x3f; #endif #ifdef PICPGM unsigned short VendorID = 0x04d8; unsigned short ProductID = 0x0033; #endif #if 0 int __cdecl main(int argc, char * argv[]) { int res; unsigned char buf[65]; UNREFERENCED_PARAMETER(argc); UNREFERENCED_PARAMETER(argv); // Set up the command buffer. memset(buf,0x00,sizeof(buf)); buf[0] = 0; buf[1] = 0x81; // Open the device. int handle = open(VendorID, ProductID, L"12345"); if (handle < 0) printf("unable to open device\n"); // Toggle LED (cmd 0x80) buf[1] = 0x80; res = write(handle, buf, 65); if (res < 0) printf("Unable to write()\n"); // Request state (cmd 0x81) buf[1] = 0x81; write(handle, buf, 65); if (res < 0) printf("Unable to write() (2)\n"); // Read requested state read(handle, buf, 65); if (res < 0) printf("Unable to read()\n"); // Print out the returned buffer. for (int i = 0; i < 4; i++) printf("buf[%d]: %d\n", i, buf[i]); return 0; } #endif #ifdef __cplusplus } // extern "C" #endif libindi/libs/indibase/basedevice.cpp0000664000175000017500000011720413263645557017004 0ustar jasemjasem/******************************************************************************* Copyright(c) 2011 Jasem Mutlaq. All rights reserved. This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2 as published by the Free Software Foundation. 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 Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. *******************************************************************************/ #include "basedevice.h" #include "base64.h" #include "config.h" #include "indicom.h" #include "indistandardproperty.h" #include "locale_compat.h" #include #include #include #include #include #if defined(_MSC_VER) #define snprintf _snprintf #pragma warning(push) ///@todo Introduce platform independent safe functions as macros to fix this #pragma warning(disable : 4996) #endif namespace INDI { BaseDevice::BaseDevice() { mediator = nullptr; lp = newLilXML(); deviceID = new char[MAXINDIDEVICE]; memset(deviceID, 0, MAXINDIDEVICE); char indidev[MAXINDIDEVICE]; strncpy(indidev, "INDIDEV=", MAXINDIDEVICE); if (getenv("INDIDEV") != nullptr) { strncpy(deviceID, getenv("INDIDEV"), MAXINDIDEVICE); putenv(indidev); } } BaseDevice::~BaseDevice() { delLilXML(lp); while (!pAll.empty()) { delete pAll.back(), pAll.pop_back(); } messageLog.clear(); delete[] deviceID; } INumberVectorProperty *BaseDevice::getNumber(const char *name) { INumberVectorProperty *nvp = nullptr; nvp = static_cast(getRawProperty(name, INDI_NUMBER)); return nvp; } ITextVectorProperty *BaseDevice::getText(const char *name) { ITextVectorProperty *tvp = nullptr; tvp = static_cast(getRawProperty(name, INDI_TEXT)); return tvp; } ISwitchVectorProperty *BaseDevice::getSwitch(const char *name) { ISwitchVectorProperty *svp = nullptr; svp = static_cast(getRawProperty(name, INDI_SWITCH)); return svp; } ILightVectorProperty *BaseDevice::getLight(const char *name) { ILightVectorProperty *lvp = nullptr; lvp = static_cast(getRawProperty(name, INDI_LIGHT)); return lvp; } IBLOBVectorProperty *BaseDevice::getBLOB(const char *name) { IBLOBVectorProperty *bvp = nullptr; bvp = static_cast(getRawProperty(name, INDI_BLOB)); return bvp; } IPState BaseDevice::getPropertyState(const char *name) { IPState state = IPS_IDLE; INDI_PROPERTY_TYPE pType; void *pPtr; INumberVectorProperty *nvp; ITextVectorProperty *tvp; ISwitchVectorProperty *svp; ILightVectorProperty *lvp; IBLOBVectorProperty *bvp; std::vector::iterator orderi = pAll.begin(); for (; orderi != pAll.end(); ++orderi) { pType = (*orderi)->getType(); pPtr = (*orderi)->getProperty(); switch (pType) { case INDI_NUMBER: nvp = static_cast(pPtr); if (nvp == nullptr) continue; if (!strcmp(name, nvp->name)) return nvp->s; break; case INDI_SWITCH: svp = static_cast(pPtr); if (svp == nullptr) continue; if (!strcmp(name, svp->name)) return svp->s; break; case INDI_TEXT: tvp = static_cast(pPtr); if (tvp == nullptr) continue; if (!strcmp(name, tvp->name)) return tvp->s; break; case INDI_LIGHT: lvp = static_cast(pPtr); if (lvp == nullptr) continue; if (!strcmp(name, lvp->name)) return lvp->s; break; case INDI_BLOB: bvp = static_cast(pPtr); if (bvp == nullptr) continue; if (!strcmp(name, bvp->name)) return bvp->s; break; default: break; } } return state; } IPerm BaseDevice::getPropertyPermission(const char *name) { IPerm perm = IP_RO; INDI_PROPERTY_TYPE pType; void *pPtr; INumberVectorProperty *nvp; ITextVectorProperty *tvp; ISwitchVectorProperty *svp; IBLOBVectorProperty *bvp; std::vector::iterator orderi = pAll.begin(); for (; orderi != pAll.end(); ++orderi) { pType = (*orderi)->getType(); pPtr = (*orderi)->getProperty(); switch (pType) { case INDI_NUMBER: nvp = static_cast(pPtr); if (nvp == nullptr) continue; if (!strcmp(name, nvp->name)) return nvp->p; break; case INDI_SWITCH: svp = static_cast(pPtr); if (svp == nullptr) continue; if (!strcmp(name, svp->name)) return svp->p; break; case INDI_TEXT: tvp = static_cast(pPtr); if (tvp == nullptr) continue; if (!strcmp(name, tvp->name)) return tvp->p; break; case INDI_BLOB: bvp = static_cast(pPtr); if (bvp == nullptr) continue; if (!strcmp(name, bvp->name)) return bvp->p; break; default: break; } } return perm; } void *BaseDevice::getRawProperty(const char *name, INDI_PROPERTY_TYPE type) { INDI_PROPERTY_TYPE pType; void *pPtr = nullptr; bool pRegistered = false; std::vector::iterator orderi = pAll.begin(); INumberVectorProperty *nvp = nullptr; ITextVectorProperty *tvp = nullptr; ISwitchVectorProperty *svp = nullptr; ILightVectorProperty *lvp = nullptr; IBLOBVectorProperty *bvp = nullptr; for (; orderi != pAll.end(); ++orderi) { pType = (*orderi)->getType(); pPtr = (*orderi)->getProperty(); pRegistered = (*orderi)->getRegistered(); if (type != INDI_UNKNOWN && pType != type) continue; switch (pType) { case INDI_NUMBER: nvp = static_cast(pPtr); if (nvp == nullptr) continue; if (!strcmp(name, nvp->name) && pRegistered) return pPtr; break; case INDI_TEXT: tvp = static_cast(pPtr); if (tvp == nullptr) continue; if (!strcmp(name, tvp->name) && pRegistered) return pPtr; break; case INDI_SWITCH: svp = static_cast(pPtr); if (svp == nullptr) continue; //IDLog("Switch %s and aux value is now %d\n", svp->name, regStatus ); if (!strcmp(name, svp->name) && pRegistered) return pPtr; break; case INDI_LIGHT: lvp = static_cast(pPtr); if (lvp == nullptr) continue; if (!strcmp(name, lvp->name) && pRegistered) return pPtr; break; case INDI_BLOB: bvp = static_cast(pPtr); if (bvp == nullptr) continue; if (!strcmp(name, bvp->name) && pRegistered) return pPtr; case INDI_UNKNOWN: break; } } return nullptr; } INDI::Property *BaseDevice::getProperty(const char *name, INDI_PROPERTY_TYPE type) { INDI_PROPERTY_TYPE pType; void *pPtr; bool pRegistered = false; std::vector::iterator orderi; INumberVectorProperty *nvp; ITextVectorProperty *tvp; ISwitchVectorProperty *svp; ILightVectorProperty *lvp; IBLOBVectorProperty *bvp; for (orderi = pAll.begin(); orderi != pAll.end(); ++orderi) { pType = (*orderi)->getType(); pPtr = (*orderi)->getProperty(); pRegistered = (*orderi)->getRegistered(); if (type != INDI_UNKNOWN && pType != type) continue; switch (pType) { case INDI_NUMBER: nvp = static_cast(pPtr); if (nvp == nullptr) continue; if (!strcmp(name, nvp->name) && pRegistered) return *orderi; break; case INDI_TEXT: tvp = static_cast(pPtr); if (tvp == nullptr) continue; if (!strcmp(name, tvp->name) && pRegistered) return *orderi; break; case INDI_SWITCH: svp = static_cast(pPtr); if (svp == nullptr) continue; //IDLog("Switch %s and aux value is now %d\n", svp->name, regStatus ); if (!strcmp(name, svp->name) && pRegistered) return *orderi; break; case INDI_LIGHT: lvp = static_cast(pPtr); if (lvp == nullptr) continue; if (!strcmp(name, lvp->name) && pRegistered) return *orderi; break; case INDI_BLOB: bvp = static_cast(pPtr); if (bvp == nullptr) continue; if (!strcmp(name, bvp->name) && pRegistered) return *orderi; break; case INDI_UNKNOWN: break; } } return nullptr; } int BaseDevice::removeProperty(const char *name, char *errmsg) { std::vector::iterator orderi; INDI_PROPERTY_TYPE pType; void *pPtr; INumberVectorProperty *nvp; ITextVectorProperty *tvp; ISwitchVectorProperty *svp; ILightVectorProperty *lvp; IBLOBVectorProperty *bvp; for (orderi = pAll.begin(); orderi != pAll.end(); ++orderi) { pType = (*orderi)->getType(); pPtr = (*orderi)->getProperty(); switch (pType) { case INDI_NUMBER: nvp = static_cast(pPtr); if (!strcmp(name, nvp->name)) { (*orderi)->setRegistered(false); delete *orderi; orderi = pAll.erase(orderi); return 0; } break; case INDI_TEXT: tvp = static_cast(pPtr); if (!strcmp(name, tvp->name)) { (*orderi)->setRegistered(false); delete *orderi; orderi = pAll.erase(orderi); return 0; } break; case INDI_SWITCH: svp = static_cast(pPtr); if (!strcmp(name, svp->name)) { (*orderi)->setRegistered(false); delete *orderi; orderi = pAll.erase(orderi); return 0; } break; case INDI_LIGHT: lvp = static_cast(pPtr); if (!strcmp(name, lvp->name)) { (*orderi)->setRegistered(false); delete *orderi; orderi = pAll.erase(orderi); return 0; } break; case INDI_BLOB: bvp = static_cast(pPtr); if (!strcmp(name, bvp->name)) { (*orderi)->setRegistered(false); delete *orderi; orderi = pAll.erase(orderi); return 0; } break; case INDI_UNKNOWN: break; } } snprintf(errmsg, MAXRBUF, "Error: Property %s not found in device %s.", name, deviceID); return INDI_PROPERTY_INVALID; } bool BaseDevice::buildSkeleton(const char *filename) { char errmsg[MAXRBUF]; FILE *fp = nullptr; XMLEle *root = nullptr, *fproot = nullptr; char pathname[MAXRBUF]; struct stat st; const char *indiskel = getenv("INDISKEL"); if (indiskel) { strncpy(pathname, indiskel, MAXRBUF - 1); pathname[MAXRBUF - 1] = 0; IDLog("Using INDISKEL %s\n", pathname); } else { if (stat(filename, &st) == 0) { strncpy(pathname, filename, MAXRBUF - 1); pathname[MAXRBUF - 1] = 0; IDLog("Using %s\n", pathname); } else { const char *slash = strrchr(filename, '/'); if (slash) filename = slash + 1; const char *indiprefix = getenv("INDIPREFIX"); if (indiprefix) { #if defined(OSX_EMBEDED_MODE) snprintf(pathname, MAXRBUF - 1, "%s/Contents/Resources/%s", indiprefix, filename); #elif defined(__APPLE__) snprintf(pathname, MAXRBUF - 1, "%s/Contents/Resources/DriverSupport/%s", indiprefix, filename); #else snprintf(pathname, MAXRBUF - 1, "%s/share/indi/%s", indiprefix, filename); #endif } else { snprintf(pathname, MAXRBUF - 1, "%s/%s", DATA_INSTALL_DIR, filename); } pathname[MAXRBUF - 1] = 0; IDLog("Using prefix %s\n", pathname); } } fp = fopen(pathname, "r"); if (fp == nullptr) { IDLog("Unable to build skeleton. Error loading file %s: %s\n", pathname, strerror(errno)); return false; } fproot = readXMLFile(fp, lp, errmsg); if (fproot == nullptr) { IDLog("Unable to parse skeleton XML: %s", errmsg); return false; } //prXMLEle(stderr, fproot, 0); for (root = nextXMLEle(fproot, 1); root != nullptr; root = nextXMLEle(fproot, 0)) buildProp(root, errmsg); delXMLEle(fproot); return true; /**************************************************************************/ } int BaseDevice::buildProp(XMLEle *root, char *errmsg) { IPerm perm = IP_RO; IPState state = IPS_IDLE; XMLEle *ep = nullptr; char *rtag, *rname, *rdev; double timeout = 0; rtag = tagXMLEle(root); /* pull out device and name */ if (crackDN(root, &rdev, &rname, errmsg) < 0) return -1; if (!deviceID[0]) strncpy(deviceID, rdev, MAXINDINAME); //if (getProperty(rname, type) != nullptr) if (getProperty(rname) != nullptr) return INDI_PROPERTY_DUPLICATED; if (strcmp(rtag, "defLightVector") && crackIPerm(findXMLAttValu(root, "perm"), &perm) < 0) { IDLog("Error extracting %s permission (%s)\n", rname, findXMLAttValu(root, "perm")); return -1; } timeout = atoi(findXMLAttValu(root, "timeout")); if (crackIPState(findXMLAttValu(root, "state"), &state) < 0) { IDLog("Error extracting %s state (%s)\n", rname, findXMLAttValu(root, "state")); return -1; } if (!strcmp(rtag, "defNumberVector")) { AutoCNumeric locale; INDI::Property *indiProp = new INDI::Property(); INumberVectorProperty *nvp = new INumberVectorProperty; INumber *np = nullptr; int n = 0; strncpy(nvp->device, deviceID, MAXINDIDEVICE); strncpy(nvp->name, rname, MAXINDINAME); strncpy(nvp->label, findXMLAttValu(root, "label"), MAXINDILABEL); strncpy(nvp->group, findXMLAttValu(root, "group"), MAXINDIGROUP); nvp->p = perm; nvp->s = state; nvp->timeout = timeout; /* pull out each name/value pair */ for (n = 0, ep = nextXMLEle(root, 1); ep != nullptr; ep = nextXMLEle(root, 0), n++) { if (!strcmp(tagXMLEle(ep), "defNumber")) { np = (INumber *)realloc(np, (n + 1) * sizeof(INumber)); np[n].nvp = nvp; XMLAtt *na = findXMLAtt(ep, "name"); if (na) { if (f_scansexa(pcdataXMLEle(ep), &(np[n].value)) < 0) IDLog("%s: Bad format %s\n", rname, pcdataXMLEle(ep)); else { strncpy(np[n].name, valuXMLAtt(na), MAXINDINAME); na = findXMLAtt(ep, "label"); if (na) strncpy(np[n].label, valuXMLAtt(na), MAXINDILABEL); na = findXMLAtt(ep, "format"); if (na) strncpy(np[n].format, valuXMLAtt(na), MAXINDIFORMAT); na = findXMLAtt(ep, "min"); if (na) np[n].min = atof(valuXMLAtt(na)); na = findXMLAtt(ep, "max"); if (na) np[n].max = atof(valuXMLAtt(na)); na = findXMLAtt(ep, "step"); if (na) np[n].step = atof(valuXMLAtt(na)); } } } } if (n > 0) { nvp->nnp = n; nvp->np = np; indiProp->setBaseDevice(this); indiProp->setProperty(nvp); indiProp->setDynamic(true); indiProp->setType(INDI_NUMBER); pAll.push_back(indiProp); //IDLog("Adding number property %s to list.\n", nvp->name); if (mediator) mediator->newProperty(indiProp); } else { IDLog("%s: newNumberVector with no valid members\n", rname); delete (nvp); delete (indiProp); } } else if (!strcmp(rtag, "defSwitchVector")) { INDI::Property *indiProp = new INDI::Property(); ISwitchVectorProperty *svp = new ISwitchVectorProperty; ISwitch *sp = nullptr; int n = 0; strncpy(svp->device, deviceID, MAXINDIDEVICE); strncpy(svp->name, rname, MAXINDINAME); strncpy(svp->label, findXMLAttValu(root, "label"), MAXINDILABEL); strncpy(svp->group, findXMLAttValu(root, "group"), MAXINDIGROUP); if (crackISRule(findXMLAttValu(root, "rule"), (&svp->r)) < 0) svp->r = ISR_1OFMANY; svp->p = perm; svp->s = state; svp->timeout = timeout; /* pull out each name/value pair */ for (n = 0, ep = nextXMLEle(root, 1); ep != nullptr; ep = nextXMLEle(root, 0), n++) { if (!strcmp(tagXMLEle(ep), "defSwitch")) { sp = (ISwitch *)realloc(sp, (n + 1) * sizeof(ISwitch)); sp[n].svp = svp; XMLAtt *na = findXMLAtt(ep, "name"); if (na) { crackISState(pcdataXMLEle(ep), &(sp[n].s)); strncpy(sp[n].name, valuXMLAtt(na), MAXINDINAME); na = findXMLAtt(ep, "label"); if (na) strncpy(sp[n].label, valuXMLAtt(na), MAXINDILABEL); } } } if (n > 0) { svp->nsp = n; svp->sp = sp; indiProp->setBaseDevice(this); indiProp->setProperty(svp); indiProp->setDynamic(true); indiProp->setType(INDI_SWITCH); pAll.push_back(indiProp); //IDLog("Adding Switch property %s to list.\n", svp->name); if (mediator) mediator->newProperty(indiProp); } else { IDLog("%s: newSwitchVector with no valid members\n", rname); delete (svp); delete (indiProp); } } else if (!strcmp(rtag, "defTextVector")) { INDI::Property *indiProp = new INDI::Property(); ITextVectorProperty *tvp = new ITextVectorProperty; IText *tp = nullptr; int n = 0; strncpy(tvp->device, deviceID, MAXINDIDEVICE); strncpy(tvp->name, rname, MAXINDINAME); strncpy(tvp->label, findXMLAttValu(root, "label"), MAXINDILABEL); strncpy(tvp->group, findXMLAttValu(root, "group"), MAXINDIGROUP); tvp->p = perm; tvp->s = state; tvp->timeout = timeout; // pull out each name/value pair for (n = 0, ep = nextXMLEle(root, 1); ep != nullptr; ep = nextXMLEle(root, 0), n++) { if (!strcmp(tagXMLEle(ep), "defText")) { tp = (IText *)realloc(tp, (n + 1) * sizeof(IText)); tp[n].tvp = tvp; XMLAtt *na = findXMLAtt(ep, "name"); if (na) { tp[n].text = (char *)malloc((pcdatalenXMLEle(ep) * sizeof(char)) + 1); strncpy(tp[n].text, pcdataXMLEle(ep), pcdatalenXMLEle(ep)); tp[n].text[pcdatalenXMLEle(ep)] = '\0'; strncpy(tp[n].name, valuXMLAtt(na), MAXINDINAME); na = findXMLAtt(ep, "label"); if (na) strncpy(tp[n].label, valuXMLAtt(na), MAXINDILABEL); } } } if (n > 0) { tvp->ntp = n; tvp->tp = tp; indiProp->setBaseDevice(this); indiProp->setProperty(tvp); indiProp->setDynamic(true); indiProp->setType(INDI_TEXT); pAll.push_back(indiProp); //IDLog("Adding Text property %s to list with initial value of %s.\n", tvp->name, tvp->tp[0].text); if (mediator) mediator->newProperty(indiProp); } else { IDLog("%s: newTextVector with no valid members\n", rname); delete (tvp); delete (indiProp); } } else if (!strcmp(rtag, "defLightVector")) { INDI::Property *indiProp = new INDI::Property(); ILightVectorProperty *lvp = new ILightVectorProperty; ILight *lp = nullptr; int n = 0; strncpy(lvp->device, deviceID, MAXINDIDEVICE); strncpy(lvp->name, rname, MAXINDINAME); strncpy(lvp->label, findXMLAttValu(root, "label"), MAXINDILABEL); strncpy(lvp->group, findXMLAttValu(root, "group"), MAXINDIGROUP); lvp->s = state; /* pull out each name/value pair */ for (n = 0, ep = nextXMLEle(root, 1); ep != nullptr; ep = nextXMLEle(root, 0), n++) { if (!strcmp(tagXMLEle(ep), "defLight")) { lp = (ILight *)realloc(lp, (n + 1) * sizeof(ILight)); lp[n].lvp = lvp; XMLAtt *na = findXMLAtt(ep, "name"); if (na) { crackIPState(pcdataXMLEle(ep), &(lp[n].s)); strncpy(lp[n].name, valuXMLAtt(na), MAXINDINAME); na = findXMLAtt(ep, "label"); if (na) strncpy(lp[n].label, valuXMLAtt(na), MAXINDILABEL); } } } if (n > 0) { lvp->nlp = n; lvp->lp = lp; indiProp->setBaseDevice(this); indiProp->setProperty(lvp); indiProp->setDynamic(true); indiProp->setType(INDI_LIGHT); pAll.push_back(indiProp); //IDLog("Adding Light property %s to list.\n", lvp->name); if (mediator) mediator->newProperty(indiProp); } else { IDLog("%s: newLightVector with no valid members\n", rname); delete (lvp); delete (indiProp); } } else if (!strcmp(rtag, "defBLOBVector")) { INDI::Property *indiProp = new INDI::Property(); IBLOBVectorProperty *bvp = new IBLOBVectorProperty; IBLOB *bp = nullptr; int n = 0; strncpy(bvp->device, deviceID, MAXINDIDEVICE); strncpy(bvp->name, rname, MAXINDINAME); strncpy(bvp->label, findXMLAttValu(root, "label"), MAXINDILABEL); strncpy(bvp->group, findXMLAttValu(root, "group"), MAXINDIGROUP); bvp->s = state; bvp->p = perm; bvp->timeout = timeout; /* pull out each name/value pair */ for (n = 0, ep = nextXMLEle(root, 1); ep != nullptr; ep = nextXMLEle(root, 0), n++) { if (!strcmp(tagXMLEle(ep), "defBLOB")) { bp = (IBLOB *)realloc(bp, (n + 1) * sizeof(IBLOB)); bp[n].bvp = bvp; XMLAtt *na = findXMLAtt(ep, "name"); if (na) { strncpy(bp[n].name, valuXMLAtt(na), MAXINDINAME); na = findXMLAtt(ep, "label"); if (na) strncpy(bp[n].label, valuXMLAtt(na), MAXINDILABEL); na = findXMLAtt(ep, "format"); if (na) strncpy(bp[n].label, valuXMLAtt(na), MAXINDIBLOBFMT); // Initialize everything to zero bp[n].blob = nullptr; bp[n].size = 0; bp[n].bloblen = 0; } } } if (n > 0) { bvp->nbp = n; bvp->bp = bp; indiProp->setBaseDevice(this); indiProp->setProperty(bvp); indiProp->setDynamic(true); indiProp->setType(INDI_BLOB); pAll.push_back(indiProp); //IDLog("Adding BLOB property %s to list.\n", bvp->name); if (mediator) mediator->newProperty(indiProp); } else { IDLog("%s: newBLOBVector with no valid members\n", rname); delete (bvp); delete (indiProp); } } return (0); } bool BaseDevice::isConnected() { ISwitchVectorProperty *svp = getSwitch(INDI::SP::CONNECTION); if (!svp) return false; ISwitch *sp = IUFindSwitch(svp, "CONNECT"); if (!sp) return false; if (sp->s == ISS_ON && svp->s == IPS_OK) return true; else return false; } /* * return 0 if ok else -1 with reason in errmsg */ int BaseDevice::setValue(XMLEle *root, char *errmsg) { XMLAtt *ap = nullptr; XMLEle *ep = nullptr; char *rtag = nullptr, *name = nullptr; double timeout = 0; IPState state; bool stateSet = false, timeoutSet = false; rtag = tagXMLEle(root); ap = findXMLAtt(root, "name"); if (!ap) { snprintf(errmsg, MAXRBUF, "INDI: <%s> unable to find name attribute", tagXMLEle(root)); return (-1); } name = valuXMLAtt(ap); /* set overall property state, if any */ ap = findXMLAtt(root, "state"); if (ap) { if (crackIPState(valuXMLAtt(ap), &state) != 0) { snprintf(errmsg, MAXRBUF, "INDI: <%s> bogus state %s for %s", tagXMLEle(root), valuXMLAtt(ap), name); return (-1); } stateSet = true; } /* allow changing the timeout */ ap = findXMLAtt(root, "timeout"); if (ap) { AutoCNumeric locale; timeout = atof(valuXMLAtt(ap)); timeoutSet = true; } checkMessage(root); if (!strcmp(rtag, "setNumberVector")) { INumberVectorProperty *nvp = getNumber(name); if (nvp == nullptr) { snprintf(errmsg, MAXRBUF, "INDI: Could not find property %s in %s", name, deviceID); return -1; } if (stateSet) nvp->s = state; if (timeoutSet) nvp->timeout = timeout; AutoCNumeric locale; for (ep = nextXMLEle(root, 1); ep != nullptr; ep = nextXMLEle(root, 0)) { INumber *np = IUFindNumber(nvp, findXMLAttValu(ep, "name")); if (!np) continue; np->value = atof(pcdataXMLEle(ep)); // Permit changing of min/max if (findXMLAtt(ep, "min")) np->min = atof(findXMLAttValu(ep, "min")); if (findXMLAtt(ep, "max")) np->max = atof(findXMLAttValu(ep, "max")); } locale.Restore(); if (mediator) mediator->newNumber(nvp); return 0; } else if (!strcmp(rtag, "setTextVector")) { ITextVectorProperty *tvp = getText(name); if (tvp == nullptr) return -1; if (stateSet) tvp->s = state; if (timeoutSet) tvp->timeout = timeout; for (ep = nextXMLEle(root, 1); ep != nullptr; ep = nextXMLEle(root, 0)) { IText *tp = IUFindText(tvp, findXMLAttValu(ep, "name")); if (!tp) continue; IUSaveText(tp, pcdataXMLEle(ep)); } if (mediator) mediator->newText(tvp); return 0; } else if (!strcmp(rtag, "setSwitchVector")) { ISState swState; ISwitchVectorProperty *svp = getSwitch(name); if (svp == nullptr) return -1; if (stateSet) svp->s = state; if (timeoutSet) svp->timeout = timeout; for (ep = nextXMLEle(root, 1); ep != nullptr; ep = nextXMLEle(root, 0)) { ISwitch *sp = IUFindSwitch(svp, findXMLAttValu(ep, "name")); if (!sp) continue; if (crackISState(pcdataXMLEle(ep), &swState) == 0) sp->s = swState; } if (mediator) mediator->newSwitch(svp); return 0; } else if (!strcmp(rtag, "setLightVector")) { IPState lState; ILightVectorProperty *lvp = getLight(name); if (lvp == nullptr) return -1; if (stateSet) lvp->s = state; for (ep = nextXMLEle(root, 1); ep != nullptr; ep = nextXMLEle(root, 0)) { ILight *lp = IUFindLight(lvp, findXMLAttValu(ep, "name")); if (!lp) continue; if (crackIPState(pcdataXMLEle(ep), &lState) == 0) lp->s = lState; } if (mediator) mediator->newLight(lvp); return 0; } else if (!strcmp(rtag, "setBLOBVector")) { IBLOBVectorProperty *bvp = getBLOB(name); if (bvp == nullptr) return -1; if (stateSet) bvp->s = state; if (timeoutSet) bvp->timeout = timeout; return setBLOB(bvp, root, errmsg); } snprintf(errmsg, MAXRBUF, "INDI: <%s> Unable to process tag", tagXMLEle(root)); return -1; } /* Set BLOB vector. Process incoming data stream * Return 0 if okay, -1 if error */ int BaseDevice::setBLOB(IBLOBVectorProperty *bvp, XMLEle *root, char *errmsg) { IBLOB *blobEL; unsigned char *dataBuffer = nullptr; XMLEle *ep; int r = 0; uLongf dataSize = 0; /* pull out each name/BLOB pair, decode */ for (ep = nextXMLEle(root, 1); ep; ep = nextXMLEle(root, 0)) { if (strcmp(tagXMLEle(ep), "oneBLOB") == 0) { XMLAtt *na = findXMLAtt(ep, "name"); blobEL = IUFindBLOB(bvp, findXMLAttValu(ep, "name")); XMLAtt *fa = findXMLAtt(ep, "format"); XMLAtt *sa = findXMLAtt(ep, "size"); if (na && fa && sa) { int blobSize = atoi(valuXMLAtt(sa)); /* Blob size = 0 when only state changes */ if (blobSize == 0) { if (mediator) mediator->newBLOB(blobEL); continue; } blobEL->size = blobSize; int bloblen = pcdatalenXMLEle(ep); blobEL->blob = (unsigned char *)realloc(blobEL->blob, 3 * bloblen / 4); blobEL->bloblen = from64tobits_fast(static_cast(blobEL->blob), pcdataXMLEle(ep), bloblen); strncpy(blobEL->format, valuXMLAtt(fa), MAXINDIFORMAT); if (strstr(blobEL->format, ".z")) { blobEL->format[strlen(blobEL->format) - 2] = '\0'; dataSize = blobEL->size * sizeof(unsigned char); dataBuffer = (unsigned char *)malloc(dataSize); if (dataBuffer == nullptr) { strncpy(errmsg, "Unable to allocate memory for data buffer", MAXRBUF); return (-1); } r = uncompress(dataBuffer, &dataSize, static_cast(blobEL->blob), (uLong)blobEL->bloblen); if (r != Z_OK) { snprintf(errmsg, MAXRBUF, "INDI: %s.%s.%s compression error: %d", blobEL->bvp->device, blobEL->bvp->name, blobEL->name, r); free(dataBuffer); return -1; } blobEL->size = dataSize; free(blobEL->blob); blobEL->blob = dataBuffer; } if (mediator) mediator->newBLOB(blobEL); } else { snprintf(errmsg, MAXRBUF, "INDI: %s.%s.%s No valid members.", blobEL->bvp->device, blobEL->bvp->name, blobEL->name); return -1; } } } return 0; } void BaseDevice::setDeviceName(const char *dev) { strncpy(deviceID, dev, MAXINDINAME); } const char *BaseDevice::getDeviceName() { return deviceID; } /* add message to queue * N.B. don't put carriage control in msg, we take care of that. */ void BaseDevice::checkMessage(XMLEle *root) { XMLAtt *ap; ap = findXMLAtt(root, "message"); if (ap) doMessage(root); } /* Store msg in queue */ void BaseDevice::doMessage(XMLEle *msg) { XMLAtt *message; XMLAtt *time_stamp; char msgBuffer[MAXRBUF]; /* prefix our timestamp if not with msg */ time_stamp = findXMLAtt(msg, "timestamp"); /* finally! the msg */ message = findXMLAtt(msg, "message"); if (!message) return; if (time_stamp) snprintf(msgBuffer, MAXRBUF, "%s: %s ", valuXMLAtt(time_stamp), valuXMLAtt(message)); else snprintf(msgBuffer, MAXRBUF, "%s: %s ", timestamp(), valuXMLAtt(message)); std::string finalMsg = msgBuffer; // Prepend to the log addMessage(finalMsg); } void BaseDevice::addMessage(const std::string& msg) { messageLog.push_back(msg); if (mediator) mediator->newMessage(this, messageLog.size() - 1); } std::string BaseDevice::messageQueue(int index) const { if (index >= (int)messageLog.size()) return nullptr; return messageLog.at(index); } std::string BaseDevice::lastMessage() { return messageLog.back(); } void BaseDevice::registerProperty(void *p, INDI_PROPERTY_TYPE type) { INDI::Property *pContainer; if (type == INDI_NUMBER) { INumberVectorProperty *nvp = static_cast(p); if ((pContainer = getProperty(nvp->name, INDI_NUMBER)) != nullptr) { pContainer->setRegistered(true); return; } pContainer = new INDI::Property(); pContainer->setProperty(p); pContainer->setType(type); pAll.push_back(pContainer); } else if (type == INDI_TEXT) { ITextVectorProperty *tvp = static_cast(p); if ((pContainer = getProperty(tvp->name, INDI_TEXT)) != nullptr) { pContainer->setRegistered(true); return; } pContainer = new INDI::Property(); pContainer->setProperty(p); pContainer->setType(type); pAll.push_back(pContainer); } else if (type == INDI_SWITCH) { ISwitchVectorProperty *svp = static_cast(p); if ((pContainer = getProperty(svp->name, INDI_SWITCH)) != nullptr) { pContainer->setRegistered(true); return; } pContainer = new INDI::Property(); pContainer->setProperty(p); pContainer->setType(type); pAll.push_back(pContainer); } else if (type == INDI_LIGHT) { ILightVectorProperty *lvp = static_cast(p); if ((pContainer = getProperty(lvp->name, INDI_LIGHT)) != nullptr) { pContainer->setRegistered(true); return; } pContainer = new INDI::Property(); pContainer->setProperty(p); pContainer->setType(type); pAll.push_back(pContainer); } else if (type == INDI_BLOB) { IBLOBVectorProperty *bvp = static_cast(p); if ((pContainer = getProperty(bvp->name, INDI_BLOB)) != nullptr) { pContainer->setRegistered(true); return; } pContainer = new INDI::Property(); pContainer->setProperty(p); pContainer->setType(type); pAll.push_back(pContainer); } } const char *BaseDevice::getDriverName() { ITextVectorProperty *driverInfo = getText("DRIVER_INFO"); if (driverInfo == nullptr) return nullptr; IText *driverName = IUFindText(driverInfo, "DRIVER_NAME"); if (driverName) return driverName->text; return nullptr; } const char *BaseDevice::getDriverExec() { ITextVectorProperty *driverInfo = getText("DRIVER_INFO"); if (driverInfo == nullptr) return nullptr; IText *driverExec = IUFindText(driverInfo, "DRIVER_EXEC"); if (driverExec) return driverExec->text; return nullptr; } const char *BaseDevice::getDriverVersion() { ITextVectorProperty *driverInfo = getText("DRIVER_INFO"); if (driverInfo == nullptr) return nullptr; IText *driverVersion = IUFindText(driverInfo, "DRIVER_VERSION"); if (driverVersion) return driverVersion->text; return nullptr; } uint16_t BaseDevice::getDriverInterface() { ITextVectorProperty *driverInfo = getText("DRIVER_INFO"); if (driverInfo == nullptr) return 0; IText *driverInterface = IUFindText(driverInfo, "DRIVER_INTERFACE"); if (driverInterface) return atoi(driverInterface->text); return 0; } } #if defined(_MSC_VER) #undef snprintf #pragma warning(pop) #endif libindi/libs/indibase/indilightboxinterface.cpp0000664000175000017500000002402313263645557021253 0ustar jasemjasem/* Dust Cap Interface Copyright (C) 2015 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 */ #include "indilightboxinterface.h" #include "indilogger.h" #include namespace INDI { LightBoxInterface::LightBoxInterface(DefaultDevice *device, bool isDimmable) { this->device = device; this->isDimmable = isDimmable; FilterIntensityN = nullptr; currentFilterSlot = 0; } LightBoxInterface::~LightBoxInterface() { } void LightBoxInterface::initLightBoxProperties(const char *deviceName, const char *groupName) { // Turn on/off light IUFillSwitch(&LightS[FLAT_LIGHT_ON], "FLAT_LIGHT_ON", "On", ISS_OFF); IUFillSwitch(&LightS[FLAT_LIGHT_OFF], "FLAT_LIGHT_OFF", "Off", ISS_OFF); IUFillSwitchVector(&LightSP, LightS, 2, deviceName, "FLAT_LIGHT_CONTROL", "Flat Light", groupName, IP_RW, ISR_1OFMANY, 0, IPS_IDLE); // Light Intensity IUFillNumber(&LightIntensityN[0], "FLAT_LIGHT_INTENSITY_VALUE", "Value", "%.f", 0, 255, 10, 0); IUFillNumberVector(&LightIntensityNP, LightIntensityN, 1, deviceName, "FLAT_LIGHT_INTENSITY", "Brightness", groupName, IP_RW, 0, IPS_IDLE); // Active Devices IUFillText(&ActiveDeviceT[0], "ACTIVE_FILTER", "Filter", "Filter Simulator"); IUFillTextVector(&ActiveDeviceTP, ActiveDeviceT, 1, deviceName, "ACTIVE_DEVICES", "Snoop devices", OPTIONS_TAB, IP_RW, 60, IPS_IDLE); // Filter duration IUFillNumberVector(&FilterIntensityNP, nullptr, 0, deviceName, "FLAT_LIGHT_FILTER_INTENSITY", "Filter Intensity", "Preset", IP_RW, 60, IPS_OK); IDSnoopDevice(ActiveDeviceT[0].text, "FILTER_SLOT"); IDSnoopDevice(ActiveDeviceT[0].text, "FILTER_NAME"); } void LightBoxInterface::isGetLightBoxProperties(const char *deviceName) { INDI_UNUSED(deviceName); device->defineText(&ActiveDeviceTP); char errmsg[MAXRBUF]; IUReadConfig(nullptr, device->getDeviceName(), "ACTIVE_DEVICES", 1, errmsg); } bool LightBoxInterface::updateLightBoxProperties() { if (device->isConnected() == false) { if (FilterIntensityN) { device->deleteProperty(FilterIntensityNP.name); FilterIntensityNP.nnp = 0; free (FilterIntensityN); FilterIntensityN = nullptr; } } return true; } bool LightBoxInterface::processLightBoxSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) { if (strcmp(dev, device->getDeviceName()) == 0) { // Light if (!strcmp(LightSP.name, name)) { int prevIndex = IUFindOnSwitchIndex(&LightSP); IUUpdateSwitch(&LightSP, states, names, n); bool rc = EnableLightBox(LightS[FLAT_LIGHT_ON].s == ISS_ON ? true : false); LightSP.s = rc ? IPS_OK : IPS_ALERT; if (!rc) { IUResetSwitch(&LightSP); LightS[prevIndex].s = ISS_ON; } IDSetSwitch(&LightSP, nullptr); return true; } } return false; } bool LightBoxInterface::processLightBoxNumber(const char *dev, const char *name, double values[], char *names[], int n) { if (strcmp(dev, device->getDeviceName()) == 0) { // Light Intensity if (!strcmp(LightIntensityNP.name, name)) { double prevValue = LightIntensityN[0].value; IUUpdateNumber(&LightIntensityNP, values, names, n); bool rc = SetLightBoxBrightness(LightIntensityN[0].value); if (rc) LightIntensityNP.s = IPS_OK; else { LightIntensityN[0].value = prevValue; LightIntensityNP.s = IPS_ALERT; } IDSetNumber(&LightIntensityNP, nullptr); return true; } if (!strcmp(FilterIntensityNP.name, name)) { if (FilterIntensityN == nullptr) { for (int i = 0; i < n; i++) addFilterDuration(names[i], values[i]); device->defineNumber(&FilterIntensityNP); return true; } IUUpdateNumber(&FilterIntensityNP, values, names, n); FilterIntensityNP.s = IPS_OK; IDSetNumber(&FilterIntensityNP, nullptr); return true; } } return false; } bool LightBoxInterface::processLightBoxText(const char *dev, const char *name, char *texts[], char *names[], int n) { if (strcmp(dev, device->getDeviceName()) == 0) { if (!strcmp(name, ActiveDeviceTP.name)) { ActiveDeviceTP.s = IPS_OK; IUUpdateText(&ActiveDeviceTP, texts, names, n); // Update client display IDSetText(&ActiveDeviceTP, nullptr); IDSnoopDevice(ActiveDeviceT[0].text, "FILTER_SLOT"); IDSnoopDevice(ActiveDeviceT[0].text, "FILTER_NAME"); return true; } } return false; } bool LightBoxInterface::EnableLightBox(bool enable) { INDI_UNUSED(enable); // Must be implemented by child class return false; } bool LightBoxInterface::SetLightBoxBrightness(uint16_t value) { INDI_UNUSED(value); // Must be implemented by child class return false; } bool LightBoxInterface::snoopLightBox(XMLEle *root) { if (isDimmable == false) return false; XMLEle *ep = nullptr; const char *propTag = tagXMLEle(root); const char *propName = findXMLAttValu(root, "name"); if (!strcmp(propTag, "delProperty")) return false; if (!strcmp(propName, "FILTER_NAME")) { if (FilterIntensityN != nullptr) { int snoopCounter=0; bool isDifferent=false; for (ep = nextXMLEle(root, 1); ep != nullptr; ep = nextXMLEle(root, 0)) { if (snoopCounter >= FilterIntensityNP.nnp || (strcmp(FilterIntensityN[snoopCounter].label, pcdataXMLEle(ep)))) { isDifferent = true; break; } snoopCounter++; } if (isDifferent == false && snoopCounter != FilterIntensityNP.nnp) isDifferent = true; // Check if we have different FILTER_NAME // If identical, no need to recreate it. if (isDifferent) { device->deleteProperty(FilterIntensityNP.name); FilterIntensityNP.nnp=0; free(FilterIntensityN); FilterIntensityN = nullptr; } else return false; } for (ep = nextXMLEle(root, 1); ep != nullptr; ep = nextXMLEle(root, 0)) addFilterDuration(pcdataXMLEle(ep), 0); device->defineNumber(&FilterIntensityNP); char errmsg[MAXRBUF]; IUReadConfig(nullptr, device->getDeviceName(), "FLAT_LIGHT_FILTER_INTENSITY", 1, errmsg); if (device->isConnected()) { if (currentFilterSlot < FilterIntensityNP.nnp) { double duration = FilterIntensityN[currentFilterSlot].value; if (duration > 0) SetLightBoxBrightness(duration); } } } else if (!strcmp(propName, "FILTER_SLOT")) { // Only accept IPS_OK/IPS_IDLE state if (strcmp(findXMLAttValu(root, "state"), "Ok") && strcmp(findXMLAttValu(root, "state"), "Idle")) return false; for (ep = nextXMLEle(root, 1); ep != nullptr; ep = nextXMLEle(root, 0)) { const char *elemName = findXMLAttValu(ep, "name"); if (!strcmp(elemName, "FILTER_SLOT_VALUE")) { currentFilterSlot = atoi(pcdataXMLEle(ep)) - 1; break; } } if (FilterIntensityN && device->isConnected()) { if (currentFilterSlot < FilterIntensityNP.nnp) { double duration = FilterIntensityN[currentFilterSlot].value; if (duration > 0) SetLightBoxBrightness(duration); } } } return false; } void LightBoxInterface::addFilterDuration(const char *filterName, uint16_t filterDuration) { if (FilterIntensityN == nullptr) { FilterIntensityN = (INumber *)malloc(sizeof(INumber)); DEBUGDEVICE(device->getDeviceName(), Logger::DBG_DEBUG, "Filter intensity preset created."); } else { // Ensure no duplicates for (int i = 0; i < FilterIntensityNP.nnp; i++) { if (!strcmp(filterName, FilterIntensityN[i].name)) return; } FilterIntensityN = (INumber *)realloc(FilterIntensityN, (FilterIntensityNP.nnp + 1) * sizeof(INumber)); } IUFillNumber(&FilterIntensityN[FilterIntensityNP.nnp], filterName, filterName, "%0.f", 0, LightIntensityN[0].max, LightIntensityN[0].step, filterDuration); FilterIntensityNP.nnp++; FilterIntensityNP.np = FilterIntensityN; } bool LightBoxInterface::saveLightBoxConfigItems(FILE *fp) { IUSaveConfigText(fp, &ActiveDeviceTP); if (FilterIntensityN != nullptr) IUSaveConfigNumber(fp, &FilterIntensityNP); return true; } } libindi/libs/indibase/baseclientqt.cpp0000664000175000017500000005232613263645557017373 0ustar jasemjasem/******************************************************************************* Copyright(c) 2016 Jasem Mutlaq. All rights reserved. INDI Qt Client This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2 as published by the Free Software Foundation. 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 Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. *******************************************************************************/ #include "baseclientqt.h" #include "base64.h" #include "basedevice.h" #include "locale_compat.h" #include "indistandardproperty.h" #include #include #include #define MAXINDIBUF 49152 #if defined(_MSC_VER) #define snprintf _snprintf #pragma warning(push) ///@todo Introduce plattform indipendent safe functions as macros to fix this #pragma warning(disable : 4996) #endif INDI::BaseClientQt::BaseClientQt(QObject *parent) : QObject(parent) { cServer = "localhost"; cPort = 7624; sConnected = false; verbose = false; lillp = nullptr; timeout_sec = 3; timeout_us = 0; connect(&client_socket, SIGNAL(readyRead()), this, SLOT(listenINDI())); connect(&client_socket, SIGNAL(error(QAbstractSocket::SocketError)), this, SLOT(processSocketError(QAbstractSocket::SocketError))); } INDI::BaseClientQt::~BaseClientQt() { clear(); } void INDI::BaseClientQt::clear() { while (!cDevices.empty()) delete cDevices.back(), cDevices.pop_back(); cDevices.clear(); while (!blobModes.empty()) delete blobModes.back(), blobModes.pop_back(); blobModes.clear(); } void INDI::BaseClientQt::setServer(const char *hostname, unsigned int port) { cServer = hostname; cPort = port; } void INDI::BaseClientQt::watchDevice(const char *deviceName) { cDeviceNames.push_back(deviceName); } bool INDI::BaseClientQt::connectServer() { client_socket.connectToHost(cServer.c_str(), cPort); if (client_socket.waitForConnected(timeout_sec * 1000) == false) { sConnected = false; return false; } clear(); lillp = newLilXML(); sConnected = true; serverConnected(); AutoCNumeric locale; QString getProp; if (cDeviceNames.empty()) { getProp = QString("\n").arg(QString::number(INDIV)); client_socket.write(getProp.toLatin1()); if (verbose) std::cerr << getProp.toLatin1().constData() << std::endl; } else { for (auto& str : cDeviceNames) { getProp = QString("\n").arg(QString::number(INDIV)).arg(str.c_str()); client_socket.write(getProp.toLatin1()); if (verbose) std::cerr << getProp.toLatin1().constData() << std::endl; } } return true; } bool INDI::BaseClientQt::disconnectServer() { if (sConnected == false) return true; sConnected = false; client_socket.close(); if (lillp) { delLilXML(lillp); lillp = nullptr; } clear(); cDeviceNames.clear(); serverDisconnected(0); return true; } void INDI::BaseClientQt::connectDevice(const char *deviceName) { setDriverConnection(true, deviceName); } void INDI::BaseClientQt::disconnectDevice(const char *deviceName) { setDriverConnection(false, deviceName); } void INDI::BaseClientQt::setDriverConnection(bool status, const char *deviceName) { INDI::BaseDevice *drv = getDevice(deviceName); ISwitchVectorProperty *drv_connection = nullptr; if (drv == nullptr) { IDLog("INDI::BaseClientQt: Error. Unable to find driver %s\n", deviceName); return; } drv_connection = drv->getSwitch(INDI::SP::CONNECTION); if (drv_connection == nullptr) return; // If we need to connect if (status) { // If there is no need to do anything, i.e. already connected. if (drv_connection->sp[0].s == ISS_ON) return; IUResetSwitch(drv_connection); drv_connection->s = IPS_BUSY; drv_connection->sp[0].s = ISS_ON; drv_connection->sp[1].s = ISS_OFF; sendNewSwitch(drv_connection); } else { // If there is no need to do anything, i.e. already disconnected. if (drv_connection->sp[1].s == ISS_ON) return; IUResetSwitch(drv_connection); drv_connection->s = IPS_BUSY; drv_connection->sp[0].s = ISS_OFF; drv_connection->sp[1].s = ISS_ON; sendNewSwitch(drv_connection); } } INDI::BaseDevice *INDI::BaseClientQt::getDevice(const char *deviceName) { for (auto& dev : cDevices) { if (!strcmp(deviceName, dev->getDeviceName())) return dev; } return nullptr; } void *INDI::BaseClientQt::listenHelper(void *context) { (static_cast(context))->listenINDI(); return nullptr; } void INDI::BaseClientQt::listenINDI() { char buffer[MAXINDIBUF]; char errorMsg[MAXRBUF]; int err_code = 0; XMLEle **nodes; XMLEle *root; int inode = 0; if (sConnected == false) return; while (client_socket.bytesAvailable() > 0) { qint64 readBytes = client_socket.read(buffer, MAXINDIBUF - 1); if (readBytes > 0) buffer[readBytes] = '\0'; nodes = parseXMLChunk(lillp, buffer, readBytes, errorMsg); if (!nodes) { if (errorMsg[0]) { fprintf(stderr, "Bad XML from %s/%d: %s\n%s\n", cServer.c_str(), cPort, errorMsg, buffer); return; } return; } root = nodes[inode]; while (root) { if (verbose) prXMLEle(stderr, root, 0); if ((err_code = dispatchCommand(root, errorMsg)) < 0) { // Silenty ignore property duplication errors if (err_code != INDI_PROPERTY_DUPLICATED) { IDLog("Dispatch command error(%d): %s\n", err_code, errorMsg); prXMLEle(stderr, root, 0); } } delXMLEle(root); // not yet, delete and continue inode++; root = nodes[inode]; } free(nodes); inode = 0; } } int INDI::BaseClientQt::dispatchCommand(XMLEle *root, char *errmsg) { if (!strcmp(tagXMLEle(root), "message")) return messageCmd(root, errmsg); else if (!strcmp(tagXMLEle(root), "delProperty")) return delPropertyCmd(root, errmsg); // Just ignore any getProperties we might get else if (!strcmp(tagXMLEle(root), "getProperties")) return INDI_PROPERTY_DUPLICATED; /* Get the device, if not available, create it */ INDI::BaseDevice *dp = findDev(root, 1, errmsg); if (dp == nullptr) { strcpy(errmsg, "No device available and none was created"); return INDI_DEVICE_NOT_FOUND; } // FIXME REMOVE THIS // Ignore echoed newXXX and getProperties if (strstr(tagXMLEle(root), "new") || strstr(tagXMLEle(root), "getProperties")) return 0; if ((!strcmp(tagXMLEle(root), "defTextVector")) || (!strcmp(tagXMLEle(root), "defNumberVector")) || (!strcmp(tagXMLEle(root), "defSwitchVector")) || (!strcmp(tagXMLEle(root), "defLightVector")) || (!strcmp(tagXMLEle(root), "defBLOBVector"))) return dp->buildProp(root, errmsg); else if (!strcmp(tagXMLEle(root), "setTextVector") || !strcmp(tagXMLEle(root), "setNumberVector") || !strcmp(tagXMLEle(root), "setSwitchVector") || !strcmp(tagXMLEle(root), "setLightVector") || !strcmp(tagXMLEle(root), "setBLOBVector")) return dp->setValue(root, errmsg); return INDI_DISPATCH_ERROR; } /* delete the property in the given device, including widgets and data structs. * when last property is deleted, delete the device too. * if no property name attribute at all, delete the whole device regardless. * return 0 if ok, else -1 with reason in errmsg[]. */ int INDI::BaseClientQt::delPropertyCmd(XMLEle *root, char *errmsg) { XMLAtt *ap; INDI::BaseDevice *dp; /* dig out device and optional property name */ dp = findDev(root, 0, errmsg); if (!dp) return INDI_DEVICE_NOT_FOUND; dp->checkMessage(root); ap = findXMLAtt(root, "name"); /* Delete property if it exists, otherwise, delete the whole device */ if (ap) { INDI::Property *rProp = dp->getProperty(valuXMLAtt(ap)); if (rProp == nullptr) { snprintf(errmsg, MAXRBUF, "Cannot delete property %s as it is not defined yet. Check driver.", valuXMLAtt(ap)); return -1; } removeProperty(rProp); int errCode = dp->removeProperty(valuXMLAtt(ap), errmsg); return errCode; } // delete the whole device else return deleteDevice(dp->getDeviceName(), errmsg); } int INDI::BaseClientQt::deleteDevice(const char *devName, char *errmsg) { std::vector::iterator devicei; for (devicei = cDevices.begin(); devicei != cDevices.end();) { if (!strcmp(devName, (*devicei)->getDeviceName())) { removeDevice(*devicei); delete *devicei; devicei = cDevices.erase(devicei); return 0; } else ++devicei; } snprintf(errmsg, MAXRBUF, "Device %s not found", devName); return INDI_DEVICE_NOT_FOUND; } INDI::BaseDevice *INDI::BaseClientQt::findDev(const char *devName, char *errmsg) { std::vector::const_iterator devicei; for (devicei = cDevices.begin(); devicei != cDevices.end(); devicei++) { if (!strcmp(devName, (*devicei)->getDeviceName())) return (*devicei); } snprintf(errmsg, MAXRBUF, "Device %s not found", devName); return nullptr; } /* add new device */ INDI::BaseDevice *INDI::BaseClientQt::addDevice(XMLEle *dep, char *errmsg) { //devicePtr dp(new INDI::BaseDriver()); INDI::BaseDevice *dp = new INDI::BaseDevice(); XMLAtt *ap; char *device_name; /* allocate new INDI::BaseDriver */ ap = findXMLAtt(dep, "device"); if (!ap) { strncpy(errmsg, "Unable to find device attribute in XML element. Cannot add device.", MAXRBUF); return nullptr; } device_name = valuXMLAtt(ap); dp->setMediator(this); dp->setDeviceName(device_name); cDevices.push_back(dp); newDevice(dp); /* ok */ return dp; } INDI::BaseDevice *INDI::BaseClientQt::findDev(XMLEle *root, int create, char *errmsg) { XMLAtt *ap; INDI::BaseDevice *dp; char *dn; /* get device name */ ap = findXMLAtt(root, "device"); if (!ap) { snprintf(errmsg, MAXRBUF, "No device attribute found in element %s", tagXMLEle(root)); return (nullptr); } dn = valuXMLAtt(ap); if (*dn == '\0') { snprintf(errmsg, MAXRBUF, "Device name is empty! %s", tagXMLEle(root)); return (nullptr); } dp = findDev(dn, errmsg); if (dp) return dp; /* not found, create if ok */ if (create) return (addDevice(root, errmsg)); snprintf(errmsg, MAXRBUF, "INDI: <%s> no such device %s", tagXMLEle(root), dn); return nullptr; } /* a general message command received from the device. * return 0 if ok, else -1 with reason in errmsg[]. */ int INDI::BaseClientQt::messageCmd(XMLEle *root, char *errmsg) { INDI::BaseDevice *dp = findDev(root, 0, errmsg); if (dp) dp->checkMessage(root); return (0); } void INDI::BaseClientQt::sendNewText(ITextVectorProperty *tvp) { AutoCNumeric locale; tvp->s = IPS_BUSY; QString prop; prop += QString("device); prop += QString(" name='%1'\n>").arg(tvp->name); for (int i = 0; i < tvp->ntp; i++) { prop += QString(" \n").arg(tvp->tp[i].name); prop += QString(" %1\n").arg(tvp->tp[i].text); prop += QString(" \n"); } prop += QString("\n"); client_socket.write(prop.toLatin1()); } void INDI::BaseClientQt::sendNewText(const char *deviceName, const char *propertyName, const char *elementName, const char *text) { INDI::BaseDevice *drv = getDevice(deviceName); if (drv == nullptr) return; ITextVectorProperty *tvp = drv->getText(propertyName); if (tvp == nullptr) return; IText *tp = IUFindText(tvp, elementName); if (tp == nullptr) return; IUSaveText(tp, text); sendNewText(tvp); } void INDI::BaseClientQt::sendNewNumber(INumberVectorProperty *nvp) { AutoCNumeric locale; nvp->s = IPS_BUSY; QString prop; prop += QString("device); prop += QString(" name='%1'\n>").arg(nvp->name); for (int i = 0; i < nvp->nnp; i++) { prop += QString(" \n").arg(nvp->np[i].name); prop += QString(" %1\n").arg(QString::number(nvp->np[i].value)); prop += QString(" \n"); } prop += QString("\n"); client_socket.write(prop.toLatin1()); } void INDI::BaseClientQt::sendNewNumber(const char *deviceName, const char *propertyName, const char *elementName, double value) { INDI::BaseDevice *drv = getDevice(deviceName); if (drv == nullptr) return; INumberVectorProperty *nvp = drv->getNumber(propertyName); if (nvp == nullptr) return; INumber *np = IUFindNumber(nvp, elementName); if (np == nullptr) return; np->value = value; sendNewNumber(nvp); } void INDI::BaseClientQt::sendNewSwitch(ISwitchVectorProperty *svp) { svp->s = IPS_BUSY; ISwitch *onSwitch = IUFindOnSwitch(svp); QString prop; prop += QString("device); prop += QString(" name='%1'>\n").arg(svp->name); if (svp->r == ISR_1OFMANY && onSwitch) { prop += QString(" \n").arg(onSwitch->name); prop += QString(" %1\n").arg((onSwitch->s == ISS_ON) ? "On" : "Off"); prop += QString(" \n"); } else { for (int i = 0; i < svp->nsp; i++) { prop += QString(" \n").arg(svp->sp[i].name); prop += QString(" %1\n").arg((svp->sp[i].s == ISS_ON) ? "On" : "Off"); prop += QString(" \n"); } } prop += QString("\n"); client_socket.write(prop.toLatin1()); } void INDI::BaseClientQt::sendNewSwitch(const char *deviceName, const char *propertyName, const char *elementName) { INDI::BaseDevice *drv = getDevice(deviceName); if (drv == nullptr) return; ISwitchVectorProperty *svp = drv->getSwitch(propertyName); if (svp == nullptr) return; ISwitch *sp = IUFindSwitch(svp, elementName); if (sp == nullptr) return; sp->s = ISS_ON; sendNewSwitch(svp); } void INDI::BaseClientQt::startBlob(const char *devName, const char *propName, const char *timestamp) { QString prop; prop += QString("\n").arg(timestamp); client_socket.write(prop.toLatin1()); } void INDI::BaseClientQt::sendOneBlob(IBLOB *bp) { QString prop; unsigned char *encblob; int l; encblob = (unsigned char *)malloc(4 * bp->size / 3 + 4); l = to64frombits(encblob, reinterpret_cast(bp->blob), bp->size); prop += QString(" name); prop += QString(" size='%1'\n").arg(QString::number(bp->size)); prop += QString(" enclen='%1'\n").arg(QString::number(l)); prop += QString(" format='%1'>\n").arg(bp->format); client_socket.write(prop.toLatin1()); size_t written = 0; size_t towrite = l; while ((int)written < l) { towrite = ((l - written) > 72) ? 72 : l - written; size_t wr = client_socket.write(reinterpret_cast(encblob + written), towrite); if (wr > 0) written += wr; if ((written % 72) == 0) client_socket.write("\n"); } if ((written % 72) != 0) client_socket.write("\n"); free(encblob); client_socket.write(" \n"); } void INDI::BaseClientQt::sendOneBlob(const char *blobName, unsigned int blobSize, const char *blobFormat, void *blobBuffer) { unsigned char *encblob; int l; encblob = (unsigned char *)malloc(4 * blobSize / 3 + 4); l = to64frombits(encblob, reinterpret_cast(blobBuffer), blobSize); QString prop; prop += QString(" \n").arg(blobFormat); client_socket.write(prop.toLatin1()); size_t written = 0; size_t towrite = l; while ((int)written < l) { towrite = ((l - written) > 72) ? 72 : l - written; size_t wr = client_socket.write(reinterpret_cast(encblob + written), towrite); if (wr > 0) written += wr; if ((written % 72) == 0) client_socket.write("\n"); } if ((written % 72) != 0) client_socket.write("\n"); free(encblob); client_socket.write(" \n"); } void INDI::BaseClientQt::finishBlob() { client_socket.write("\n"); } void INDI::BaseClientQt::setBLOBMode(BLOBHandling blobH, const char *dev, const char *prop) { if (!dev[0]) return; BLOBMode *bMode = findBLOBMode(std::string(dev), prop ? std::string(prop) : std::string()); if (bMode == nullptr) { BLOBMode *newMode = new BLOBMode(); newMode->device = std::string(dev); newMode->property = (prop ? std::string(prop) : std::string()); newMode->blobMode = blobH; blobModes.push_back(newMode); } else { // If nothing changed, nothing to to do if (bMode->blobMode == blobH) return; bMode->blobMode = blobH; } QString blobOpenTag; QString blobEnableTag; if (prop != nullptr) blobOpenTag = QString("").arg(dev).arg(prop); else blobOpenTag = QString("").arg(dev); switch (blobH) { case B_NEVER: blobEnableTag = QString("%1Never\n").arg(blobOpenTag); break; case B_ALSO: blobEnableTag = QString("%1Also\n").arg(blobOpenTag); break; case B_ONLY: blobEnableTag = QString("%1Only\n").arg(blobOpenTag); break; } client_socket.write(blobEnableTag.toLatin1()); } BLOBHandling INDI::BaseClientQt::getBLOBMode(const char *dev, const char *prop) { BLOBHandling bHandle = B_ALSO; BLOBMode *bMode = findBLOBMode(dev, (prop ? std::string(prop) : std::string())); if (bMode) bHandle = bMode->blobMode; return bHandle; } INDI::BaseClientQt::BLOBMode *INDI::BaseClientQt::findBLOBMode(const std::string& device, const std::string& property) { for (auto& blob : blobModes) { if (blob->device == device && blob->property == property) return blob; } return nullptr; } void INDI::BaseClientQt::processSocketError(QAbstractSocket::SocketError socketError) { if (sConnected == false) return; // TODO Handle what happens on socket failure! INDI_UNUSED(socketError); IDLog("Socket Error: %s\n", client_socket.errorString().toLatin1().constData()); fprintf(stderr, "INDI server %s/%d disconnected.\n", cServer.c_str(), cPort); delLilXML(lillp); client_socket.close(); // Let client handle server disconnection serverDisconnected(-1); } bool INDI::BaseClientQt::getDevices(std::vector &deviceList, uint16_t driverInterface ) { for (INDI::BaseDevice *device : cDevices) { if (device->getDriverInterface() | driverInterface) deviceList.push_back(device); } return (deviceList.size() > 0); } bool INDI::BaseClientQt::isServerConnected() const { return sConnected; } #if defined(_MSC_VER) #undef snprintf #pragma warning(pop) #endif libindi/libs/indibase/baseclient.cpp0000664000175000017500000006325613263645557017032 0ustar jasemjasem/******************************************************************************* Copyright(c) 2011 Jasem Mutlaq. All rights reserved. This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2 as published by the Free Software Foundation. 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 Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. *******************************************************************************/ #include "baseclient.h" #include "indistandardproperty.h" #include "base64.h" #include "basedevice.h" #include "locale_compat.h" #include #include #include #include #include #ifdef _WINDOWS #include #include #define net_read(x,y,z) recv(x,y,z,0) #define net_write(x,y,z) send(x,(const char *)(y),z,0) #define net_close closesocket #pragma comment(lib, "Ws2_32.lib") #else #include #include #include #include #include #define net_read read #define net_write write #define net_close close #endif #ifdef _MSC_VER # define snprintf _snprintf #endif #define MAXINDIBUF 49152 INDI::BaseClient::BaseClient() { cServer = "localhost"; cPort = 7624; sConnected = false; verbose = false; timeout_sec = 3; timeout_us = 0; } INDI::BaseClient::~BaseClient() { clear(); } void INDI::BaseClient::clear() { while (!cDevices.empty()) delete cDevices.back(), cDevices.pop_back(); cDevices.clear(); while (!blobModes.empty()) delete blobModes.back(), blobModes.pop_back(); blobModes.clear(); } void INDI::BaseClient::setServer(const char *hostname, unsigned int port) { cServer = hostname; cPort = port; } void INDI::BaseClient::watchDevice(const char *deviceName) { cDeviceNames.emplace_back(std::string(deviceName)); } bool INDI::BaseClient::connectServer() { #ifdef _WINDOWS WSADATA wsaData; int iResult = WSAStartup(MAKEWORD(2,2), &wsaData); if (iResult != NO_ERROR) { IDLog("Error at WSAStartup()\n"); return false; } #endif struct timeval ts; ts.tv_sec = timeout_sec; ts.tv_usec = timeout_us; struct sockaddr_in serv_addr; struct hostent *hp; int ret = 0; /* lookup host address */ hp = gethostbyname(cServer.c_str()); if (!hp) { perror("gethostbyname"); return false; } /* create a socket to the INDI server */ (void)memset((char *)&serv_addr, 0, sizeof(serv_addr)); serv_addr.sin_family = AF_INET; serv_addr.sin_addr.s_addr = ((struct in_addr *)(hp->h_addr_list[0]))->s_addr; serv_addr.sin_port = htons(cPort); #ifdef _WINDOWS if ((sockfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP)) == INVALID_SOCKET) { IDLog("Socket error: %d\n", WSAGetLastError()); WSACleanup(); return false; } #else if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0) { perror("socket"); return false; } #endif /* set the socket in non-blocking */ //set socket nonblocking flag #ifdef _WINDOWS u_long iMode = 0; iResult = ioctlsocket(sockfd, FIONBIO, &iMode); if (iResult != NO_ERROR) { IDLog("ioctlsocket failed with error: %ld\n", iResult); return false; } #else int flags = 0; if ((flags = fcntl(sockfd, F_GETFL, 0)) < 0) return false; if (fcntl(sockfd, F_SETFL, flags | O_NONBLOCK) < 0) return false; #endif //clear out descriptor sets for select //add socket to the descriptor sets fd_set rset, wset; FD_ZERO(&rset); FD_SET(sockfd, &rset); wset = rset; //structure assignment okok /* connect */ if ((ret = ::connect(sockfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr))) < 0) { if (errno != EINPROGRESS) { perror("connect"); net_close(sockfd); return false; } } /* If it is connected, continue, otherwise wait */ if (ret != 0) { //we are waiting for connect to complete now if ((ret = select(sockfd + 1, &rset, &wset, nullptr, &ts)) < 0) return false; //we had a timeout if (ret == 0) { #ifdef _WINDOWS IDLog("select timeout\n"); #else errno = ETIMEDOUT; perror("select timeout"); #endif return false; } } /* we had a positivite return so a descriptor is ready */ #ifndef _WINDOWS int error = 0; socklen_t len = sizeof(error); if (FD_ISSET(sockfd, &rset) || FD_ISSET(sockfd, &wset)) { if (getsockopt(sockfd, SOL_SOCKET, SO_ERROR, &error, &len) < 0) { perror("getsockopt"); return false; } } else return false; /* check if we had a socket error */ if (error) { errno = error; perror("socket"); return false; } #endif #ifndef _WINDOWS int pipefd[2]; ret = socketpair(PF_UNIX, SOCK_STREAM, 0, pipefd); if (ret < 0) { IDLog("notify pipe: %s\n", strerror(errno)); return false; } m_receiveFd = pipefd[0]; m_sendFd = pipefd[1]; #endif sConnected = true; /*int result = pthread_create(&listen_thread, nullptr, &INDI::BaseClient::listenHelper, this); if (result != 0) { sConnected = false; perror("thread"); return false; }*/ listen_thread = new std::thread(listenHelper, this); serverConnected(); return true; } bool INDI::BaseClient::disconnectServer() { //IDLog("Server disconnected called\n"); if (sConnected == false) return true; sConnected = false; #ifdef _WINDOWS net_close(sockfd); WSACleanup(); #else shutdown(sockfd, SHUT_RDWR); while (write(m_sendFd, "1", 1) <= 0) #endif clear(); cDeviceNames.clear(); listen_thread->join(); delete(listen_thread); listen_thread=nullptr; //pthread_join(listen_thread, nullptr); int exit_code = 0; serverDisconnected(exit_code); return true; } bool INDI::BaseClient::isServerConnected() const { return sConnected; } void INDI::BaseClient::connectDevice(const char *deviceName) { setDriverConnection(true, deviceName); } void INDI::BaseClient::disconnectDevice(const char *deviceName) { setDriverConnection(false, deviceName); } void INDI::BaseClient::setDriverConnection(bool status, const char *deviceName) { INDI::BaseDevice *drv = getDevice(deviceName); ISwitchVectorProperty *drv_connection = nullptr; if (drv == nullptr) { IDLog("INDI::BaseClient: Error. Unable to find driver %s\n", deviceName); return; } drv_connection = drv->getSwitch(INDI::SP::CONNECTION); if (drv_connection == nullptr) return; // If we need to connect if (status) { // If there is no need to do anything, i.e. already connected. if (drv_connection->sp[0].s == ISS_ON) return; IUResetSwitch(drv_connection); drv_connection->s = IPS_BUSY; drv_connection->sp[0].s = ISS_ON; drv_connection->sp[1].s = ISS_OFF; sendNewSwitch(drv_connection); } else { // If there is no need to do anything, i.e. already disconnected. if (drv_connection->sp[1].s == ISS_ON) return; IUResetSwitch(drv_connection); drv_connection->s = IPS_BUSY; drv_connection->sp[0].s = ISS_OFF; drv_connection->sp[1].s = ISS_ON; sendNewSwitch(drv_connection); } } INDI::BaseDevice *INDI::BaseClient::getDevice(const char *deviceName) { for (auto& device : cDevices) { if (!strcmp(deviceName, device->getDeviceName())) return device; } return nullptr; } void *INDI::BaseClient::listenHelper(void *context) { (static_cast(context))->listenINDI(); return nullptr; } void INDI::BaseClient::listenINDI() { char buffer[MAXINDIBUF]; char msg[MAXRBUF]; int n = 0, err_code = 0; #ifdef _WINDOWS SOCKET maxfd = 0; #else int maxfd = 0; #endif fd_set rs; XMLEle **nodes = nullptr; XMLEle *root = nullptr; int inode = 0; AutoCNumeric locale; if (cDeviceNames.empty()) { sendString("\n", INDIV); if (verbose) fprintf(stderr, "\n", INDIV); } else { for (auto& str : cDeviceNames) { sendString("\n", INDIV, str.c_str()); if (verbose) IDLog("\n", INDIV, str.c_str()); } } locale.Restore(); FD_ZERO(&rs); FD_SET(sockfd, &rs); if (sockfd > maxfd) maxfd = sockfd; #ifndef _WINDOWS FD_SET(m_receiveFd, &rs); if (m_receiveFd > maxfd) maxfd = m_receiveFd; #endif clear(); lillp = newLilXML(); /* read from server, exit if find all requested properties */ while (sConnected) { n = select(maxfd + 1, &rs, nullptr, nullptr, nullptr); if (n < 0) { IDLog("INDI server %s/%d disconnected.\n", cServer.c_str(), cPort); net_close(sockfd); break; } #ifndef _WINDOWS // Received termination string from main thread if (n > 0 && FD_ISSET(m_receiveFd, &rs)) { sConnected = false; break; } #endif if (n > 0 && FD_ISSET(sockfd, &rs)) { #ifdef _WINDOWS n = recv(sockfd, buffer, MAXINDIBUF, 0); #else n = recv(sockfd, buffer, MAXINDIBUF, MSG_DONTWAIT); #endif if (n <= 0) { if (n == 0) { IDLog("INDI server %s/%d disconnected.\n", cServer.c_str(), cPort); net_close(sockfd); break; } else continue; } nodes = parseXMLChunk(lillp, buffer, n, msg); if (!nodes) { if (msg[0]) { IDLog("Bad XML from %s/%d: %s\n%s\n", cServer.c_str(), cPort, msg, buffer); return; } return; } root = nodes[inode]; while (root) { if (verbose) prXMLEle(stderr, root, 0); if ((err_code = dispatchCommand(root, msg)) < 0) { // Silenty ignore property duplication errors if (err_code != INDI_PROPERTY_DUPLICATED) { IDLog("Dispatch command error(%d): %s\n", err_code, msg); prXMLEle(stderr, root, 0); } } delXMLEle(root); // not yet, delete and continue inode++; root = nodes[inode]; } free(nodes); inode = 0; } } delLilXML(lillp); serverDisconnected((sConnected == false) ? 0 : -1); sConnected = false; //pthread_exit(0); } int INDI::BaseClient::dispatchCommand(XMLEle *root, char *errmsg) { if (!strcmp(tagXMLEle(root), "message")) return messageCmd(root, errmsg); else if (!strcmp(tagXMLEle(root), "delProperty")) return delPropertyCmd(root, errmsg); // Just ignore any getProperties we might get else if (!strcmp(tagXMLEle(root), "getProperties")) return INDI_PROPERTY_DUPLICATED; /* Get the device, if not available, create it */ INDI::BaseDevice *dp = findDev(root, 1, errmsg); if (dp == nullptr) { strcpy(errmsg, "No device available and none was created"); return INDI_DEVICE_NOT_FOUND; } // FIXME REMOVE THIS // Ignore echoed newXXX if (strstr(tagXMLEle(root), "new")) return 0; if ((!strcmp(tagXMLEle(root), "defTextVector")) || (!strcmp(tagXMLEle(root), "defNumberVector")) || (!strcmp(tagXMLEle(root), "defSwitchVector")) || (!strcmp(tagXMLEle(root), "defLightVector")) || (!strcmp(tagXMLEle(root), "defBLOBVector"))) return dp->buildProp(root, errmsg); else if (!strcmp(tagXMLEle(root), "setTextVector") || !strcmp(tagXMLEle(root), "setNumberVector") || !strcmp(tagXMLEle(root), "setSwitchVector") || !strcmp(tagXMLEle(root), "setLightVector") || !strcmp(tagXMLEle(root), "setBLOBVector")) return dp->setValue(root, errmsg); return INDI_DISPATCH_ERROR; } /* delete the property in the given device, including widgets and data structs. * when last property is deleted, delete the device too. * if no property name attribute at all, delete the whole device regardless. * return 0 if ok, else -1 with reason in errmsg[]. */ int INDI::BaseClient::delPropertyCmd(XMLEle *root, char *errmsg) { XMLAtt *ap; INDI::BaseDevice *dp; /* dig out device and optional property name */ dp = findDev(root, 0, errmsg); if (!dp) return INDI_DEVICE_NOT_FOUND; dp->checkMessage(root); ap = findXMLAtt(root, "name"); /* Delete property if it exists, otherwise, delete the whole device */ if (ap) { INDI::Property *rProp = dp->getProperty(valuXMLAtt(ap)); if (rProp == nullptr) { snprintf(errmsg, MAXRBUF, "Cannot delete property %s as it is not defined yet. Check driver.", valuXMLAtt(ap)); return -1; } removeProperty(rProp); int errCode = dp->removeProperty(valuXMLAtt(ap), errmsg); return errCode; } // delete the whole device else return deleteDevice(dp->getDeviceName(), errmsg); } int INDI::BaseClient::deleteDevice(const char *devName, char *errmsg) { std::vector::iterator devicei; for (devicei = cDevices.begin(); devicei != cDevices.end();) { if (!strcmp(devName, (*devicei)->getDeviceName())) { removeDevice(*devicei); delete *devicei; devicei = cDevices.erase(devicei); return 0; } else ++devicei; } snprintf(errmsg, MAXRBUF, "Device %s not found", devName); return INDI_DEVICE_NOT_FOUND; } INDI::BaseDevice *INDI::BaseClient::findDev(const char *devName, char *errmsg) { std::vector::const_iterator devicei; for (devicei = cDevices.begin(); devicei != cDevices.end(); devicei++) { if (!strcmp(devName, (*devicei)->getDeviceName())) return (*devicei); } snprintf(errmsg, MAXRBUF, "Device %s not found", devName); return nullptr; } /* add new device */ INDI::BaseDevice *INDI::BaseClient::addDevice(XMLEle *dep, char *errmsg) { //devicePtr dp(new INDI::BaseDriver()); INDI::BaseDevice *dp = new INDI::BaseDevice(); XMLAtt *ap; char *device_name; /* allocate new INDI::BaseDriver */ ap = findXMLAtt(dep, "device"); if (!ap) { strncpy(errmsg, "Unable to find device attribute in XML element. Cannot add device.", MAXRBUF); return nullptr; } device_name = valuXMLAtt(ap); dp->setMediator(this); dp->setDeviceName(device_name); cDevices.push_back(dp); newDevice(dp); /* ok */ return dp; } INDI::BaseDevice *INDI::BaseClient::findDev(XMLEle *root, int create, char *errmsg) { XMLAtt *ap; INDI::BaseDevice *dp; char *dn; /* get device name */ ap = findXMLAtt(root, "device"); if (!ap) { snprintf(errmsg, MAXRBUF, "No device attribute found in element %s", tagXMLEle(root)); return (nullptr); } dn = valuXMLAtt(ap); if (*dn == '\0') { snprintf(errmsg, MAXRBUF, "Device name is empty! %s", tagXMLEle(root)); return (nullptr); } dp = findDev(dn, errmsg); if (dp) return dp; /* not found, create if ok */ if (create) return (addDevice(root, errmsg)); snprintf(errmsg, MAXRBUF, "INDI: <%s> no such device %s", tagXMLEle(root), dn); return nullptr; } /* a general message command received from the device. * return 0 if ok, else -1 with reason in errmsg[]. */ int INDI::BaseClient::messageCmd(XMLEle *root, char *errmsg) { INDI::BaseDevice *dp = findDev(root, 0, errmsg); if (dp) dp->checkMessage(root); else { XMLAtt *message; XMLAtt *time_stamp; char msgBuffer[MAXRBUF]; /* prefix our timestamp if not with msg */ time_stamp = findXMLAtt(root, "timestamp"); /* finally! the msg */ message = findXMLAtt(root, "message"); if (!message) { strncpy(errmsg, "No message content found.", MAXRBUF); return -1; } if (time_stamp) snprintf(msgBuffer, MAXRBUF, "%s: %s", valuXMLAtt(time_stamp), valuXMLAtt(message)); else { 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(msgBuffer, MAXRBUF, "%s: %s", ts, valuXMLAtt(message)); } std::string finalMsg = msgBuffer; newUniversalMessage(finalMsg); } return (0); } void INDI::BaseClient::newUniversalMessage(std::string message) { IDLog("%s\n", message.c_str()); } void INDI::BaseClient::sendNewText(ITextVectorProperty *tvp) { tvp->s = IPS_BUSY; sendString("device); sendString(" name='%s'\n>", tvp->name); for (int i = 0; i < tvp->ntp; i++) { sendString(" \n", tvp->tp[i].name); sendString(" %s\n", tvp->tp[i].text); sendString(" \n"); } sendString("\n"); } void INDI::BaseClient::sendNewText(const char *deviceName, const char *propertyName, const char *elementName, const char *text) { INDI::BaseDevice *drv = getDevice(deviceName); if (drv == nullptr) return; ITextVectorProperty *tvp = drv->getText(propertyName); if (tvp == nullptr) return; IText *tp = IUFindText(tvp, elementName); if (tp == nullptr) return; IUSaveText(tp, text); sendNewText(tvp); } void INDI::BaseClient::sendNewNumber(INumberVectorProperty *nvp) { AutoCNumeric locale; nvp->s = IPS_BUSY; sendString("device); sendString(" name='%s'\n>", nvp->name); for (int i = 0; i < nvp->nnp; i++) { sendString(" \n", nvp->np[i].name); sendString(" %g\n", nvp->np[i].value); sendString(" \n"); } sendString("\n"); } void INDI::BaseClient::sendNewNumber(const char *deviceName, const char *propertyName, const char *elementName, double value) { INDI::BaseDevice *drv = getDevice(deviceName); if (drv == nullptr) return; INumberVectorProperty *nvp = drv->getNumber(propertyName); if (nvp == nullptr) return; INumber *np = IUFindNumber(nvp, elementName); if (np == nullptr) return; np->value = value; sendNewNumber(nvp); } void INDI::BaseClient::sendNewSwitch(ISwitchVectorProperty *svp) { svp->s = IPS_BUSY; ISwitch *onSwitch = IUFindOnSwitch(svp); sendString("device); sendString(" name='%s'>\n", svp->name); if (svp->r == ISR_1OFMANY && onSwitch) { sendString(" \n", onSwitch->name); sendString(" %s\n", (onSwitch->s == ISS_ON) ? "On" : "Off"); sendString(" \n"); } else { for (int i = 0; i < svp->nsp; i++) { sendString(" \n", svp->sp[i].name); sendString(" %s\n", (svp->sp[i].s == ISS_ON) ? "On" : "Off"); sendString(" \n"); } } sendString("\n"); } void INDI::BaseClient::sendNewSwitch(const char *deviceName, const char *propertyName, const char *elementName) { INDI::BaseDevice *drv = getDevice(deviceName); if (drv == nullptr) return; ISwitchVectorProperty *svp = drv->getSwitch(propertyName); if (svp == nullptr) return; ISwitch *sp = IUFindSwitch(svp, elementName); if (sp == nullptr) return; sp->s = ISS_ON; sendNewSwitch(svp); } void INDI::BaseClient::startBlob(const char *devName, const char *propName, const char *timestamp) { sendString("\n", timestamp); } void INDI::BaseClient::sendOneBlob(IBLOB *bp) { unsigned char *encblob; char nl = '\n'; int l; int ret = 0; encblob = (unsigned char *)malloc(4 * bp->size / 3 + 4); l = to64frombits(encblob, reinterpret_cast(bp->blob), bp->size); sendString(" name); sendString(" size='%ud'\n", bp->size); sendString(" enclen='%d'\n", l); sendString(" format='%s'>\n", bp->format); size_t written = 0; size_t towrite = 0; while ((int)written < l) { towrite = ((l - written) > 72) ? 72 : l - written; size_t wr = net_write(sockfd, encblob + written, towrite); if (wr > 0) written += wr; if ((written % 72) == 0) ret = net_write(sockfd, &nl, 1); } if ((written % 72) != 0) ret = net_write(sockfd, &nl, 1); free(encblob); sendString(" \n"); } void INDI::BaseClient::sendOneBlob(const char *blobName, unsigned int blobSize, const char *blobFormat, void *blobBuffer) { unsigned char *encblob; char nl = '\n'; int l; int ret = 0; encblob = (unsigned char *)malloc(4 * blobSize / 3 + 4); l = to64frombits(encblob, reinterpret_cast(blobBuffer), blobSize); sendString(" \n", blobFormat); size_t written = 0; size_t towrite = 0; while ((int)written < l) { towrite = ((l - written) > 72) ? 72 : l - written; size_t wr = net_write(sockfd, encblob + written, towrite); if (wr > 0) written += wr; if ((written % 72) == 0) ret = net_write(sockfd, &nl, 1); } if ((written % 72) != 0) ret = net_write(sockfd, &nl, 1); free(encblob); sendString(" \n"); } void INDI::BaseClient::finishBlob() { sendString("\n"); } void INDI::BaseClient::setBLOBMode(BLOBHandling blobH, const char *dev, const char *prop) { char blobOpenTag[MAXRBUF]; if (!dev[0]) return; BLOBMode *bMode = findBLOBMode(std::string(dev), (prop ? std::string(prop) : std::string())); if (bMode == nullptr) { BLOBMode *newMode = new BLOBMode(); newMode->device = std::string(dev); newMode->property = (prop ? std::string(prop) : std::string()); newMode->blobMode = blobH; blobModes.push_back(newMode); } else { // If nothing changed, nothing to to do if (bMode->blobMode == blobH) return; bMode->blobMode = blobH; } if (prop != nullptr) snprintf(blobOpenTag, MAXRBUF, "", dev, prop); else snprintf(blobOpenTag, MAXRBUF, "", dev); switch (blobH) { case B_NEVER: sendString("%sNever\n", blobOpenTag); break; case B_ALSO: sendString("%sAlso\n", blobOpenTag); break; case B_ONLY: sendString("%sOnly\n", blobOpenTag); break; } } BLOBHandling INDI::BaseClient::getBLOBMode(const char *dev, const char *prop) { BLOBHandling bHandle = B_ALSO; BLOBMode *bMode = findBLOBMode(dev, (prop ? std::string(prop) : std::string())); if (bMode) bHandle = bMode->blobMode; return bHandle; } INDI::BaseClient::BLOBMode *INDI::BaseClient::findBLOBMode(const std::string& device, const std::string& property) { for (auto& blob : blobModes) { if (blob->device == device && blob->property == property) return blob; } return nullptr; } bool INDI::BaseClient::getDevices(std::vector &deviceList, uint16_t driverInterface ) { for (INDI::BaseDevice *device : cDevices) { if (device->getDriverInterface() | driverInterface) deviceList.push_back(device); } return (deviceList.size() > 0); } void INDI::BaseClient::sendString(const char *fmt, ...) { int ret = 0; char message[MAXRBUF]; va_list ap; va_start(ap, fmt); vsnprintf(message, MAXRBUF, fmt, ap); va_end(ap); ret = net_write(sockfd, message, strlen(message)); } libindi/libs/indibase/indiusbdevice.cpp0000664000175000017500000001563513263645557017534 0ustar jasemjasem/******************************************************************************* Copyright(c) 2011 Gerry Rozema. All rights reserved. Upgrade to libusb 1.0 by CloudMakers, s. r. o. Copyright(c) 2013 CloudMakers, s. r. o. All rights reserved. This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2 as published by the Free Software Foundation. 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 Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. *******************************************************************************/ #include "indiusbdevice.h" #include #ifndef USB1_HAS_LIBUSB_ERROR_NAME const char *LIBUSB_CALL libusb_error_name(int errcode) { static char buffer[30]; sprintf(buffer, "error %d", errcode); return buffer; } #endif static libusb_context *ctx = nullptr; namespace INDI { USBDevice::USBDevice() { dev = nullptr; usb_handle = nullptr; OutputEndpoint = 0; InputEndpoint = 0; if (ctx == nullptr) { int rc = libusb_init(&ctx); if (rc < 0) { fprintf(stderr, "USBDevice: Can't initialize libusb\n"); } } } USBDevice::~USBDevice() { libusb_exit(ctx); } libusb_device *USBDevice::FindDevice(int vendor, int product, int searchindex) { int index = 0; libusb_device **usb_devices; struct libusb_device_descriptor descriptor; ssize_t total = libusb_get_device_list(ctx, &usb_devices); if (total < 0) { fprintf(stderr, "USBDevice: Can't get device list\n"); return 0; } for (int i = 0; i < total; i++) { libusb_device *device = usb_devices[i]; if (!libusb_get_device_descriptor(device, &descriptor)) { if (descriptor.idVendor == vendor && descriptor.idProduct == product) { if (index == searchindex) { libusb_ref_device(device); libusb_free_device_list(usb_devices, 1); fprintf(stderr, "Found device %04x/%04x/%d\n", descriptor.idVendor, descriptor.idProduct, index); return device; } else { fprintf(stderr, "Skipping device %04x/%04x/%d\n", descriptor.idVendor, descriptor.idProduct, index); index++; } } else { fprintf(stderr, "Skipping device %04x/%04x\n", descriptor.idVendor, descriptor.idProduct); } } } libusb_free_device_list(usb_devices, 1); return nullptr; } int USBDevice::Open() { if (dev == nullptr) return -1; int rc = libusb_open(dev, &usb_handle); if (rc >= 0) { if (libusb_kernel_driver_active(usb_handle, 0) == 1) { rc = libusb_detach_kernel_driver(usb_handle, 0); if (rc < 0) { fprintf(stderr, "USBDevice: libusb_detach_kernel_driver -> %s\n", libusb_error_name(rc)); } } if (rc >= 0) { rc = libusb_claim_interface(usb_handle, 0); if (rc < 0) { fprintf(stderr, "USBDevice: libusb_claim_interface -> %s\n", libusb_error_name(rc)); } } return FindEndpoints(); } return rc; } void USBDevice::Close() { libusb_close(usb_handle); } int USBDevice::FindEndpoints() { struct libusb_config_descriptor *config; struct libusb_interface_descriptor *interface; int rc = libusb_get_config_descriptor(dev, 0, &config); if (rc < 0) { fprintf(stderr, "USBDevice: libusb_get_config_descriptor -> %s\n", libusb_error_name(rc)); return rc; } interface = (struct libusb_interface_descriptor *)&(config->interface[0].altsetting[0]); for (int i = 0; i < interface->bNumEndpoints; i++) { fprintf(stderr, "Endpoint %04x %04x\n", interface->endpoint[i].bEndpointAddress, interface->endpoint[i].bmAttributes); int dir = interface->endpoint[i].bEndpointAddress & LIBUSB_ENDPOINT_DIR_MASK; if (dir == LIBUSB_ENDPOINT_IN) { fprintf(stderr, "Got an input endpoint\n"); InputEndpoint = interface->endpoint[i].bEndpointAddress; InputType = interface->endpoint[i].bmAttributes & LIBUSB_TRANSFER_TYPE_MASK; } else if (dir == LIBUSB_ENDPOINT_OUT) { fprintf(stderr, "Got an output endpoint\n"); OutputEndpoint = interface->endpoint[i].bEndpointAddress; OutputType = interface->endpoint[i].bmAttributes & LIBUSB_TRANSFER_TYPE_MASK; } } return 0; } int USBDevice::ReadInterrupt(unsigned char *buf, int count, int timeout) { int transferred; int rc = libusb_interrupt_transfer(usb_handle, InputEndpoint, buf, count, &transferred, timeout); if (rc < 0) { fprintf(stderr, "USBDevice: libusb_interrupt_transfer -> %s\n", libusb_error_name(rc)); } return rc < 0 ? rc : transferred; } int USBDevice::WriteInterrupt(unsigned char *buf, int count, int timeout) { int transferred; int rc = libusb_interrupt_transfer(usb_handle, OutputEndpoint, buf, count, &transferred, timeout); if (rc < 0) { fprintf(stderr, "USBDevice: libusb_interrupt_transfer -> %s\n", libusb_error_name(rc)); } return rc < 0 ? rc : transferred; } int USBDevice::ReadBulk(unsigned char *buf, int count, int timeout) { int transferred; int rc = libusb_bulk_transfer(usb_handle, InputEndpoint, buf, count, &transferred, timeout); if (rc < 0) { fprintf(stderr, "USBDevice: libusb_bulk_transfer -> %s\n", libusb_error_name(rc)); } return rc < 0 ? rc : transferred; } int USBDevice::WriteBulk(unsigned char *buf, int count, int timeout) { int transferred; int rc = libusb_bulk_transfer(usb_handle, OutputEndpoint, buf, count, &transferred, timeout); if (rc < 0) { fprintf(stderr, "USBDevice: libusb_bulk_transfer -> %s\n", libusb_error_name(rc)); } return rc < 0 ? rc : transferred; } int USBDevice::ControlMessage(unsigned char request_type, unsigned char request, unsigned int value, unsigned int index, unsigned char *data, unsigned char len) { int rc = libusb_control_transfer(usb_handle, request_type, request, value, index, data, len, 5000); if (rc < 0) { fprintf(stderr, "USBDevice: libusb_control_transfer -> %s\n", libusb_error_name(rc)); } return rc; } } libindi/libs/indibase/basedevice.h0000664000175000017500000002250313263645557016446 0ustar jasemjasem/******************************************************************************* Copyright(c) 2011 Jasem Mutlaq. All rights reserved. This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2 as published by the Free Software Foundation. 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 Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. *******************************************************************************/ #pragma once #include "indibase.h" #include "indiproperty.h" #include #include #include #define MAXRBUF 2048 /** * \class INDI::BaseDevice \brief Class to provide basic INDI device functionality. INDI::BaseDevice is the base device for all INDI devices and contains a list of all properties defined by the device either explicity or via a skeleton file. You don't need to subclass INDI::BaseDevice class directly, it is inheritied by INDI::DefaultDevice which takes care of building a standard INDI device. Moreover, INDI::BaseClient maintains a list of INDI::BaseDevice objects as they get defined from the INDI server, and those objects may be accessed to retrieve information on the object properties or message log. \author Jasem Mutlaq */ namespace INDI { class BaseDevice { public: BaseDevice(); virtual ~BaseDevice(); /*! INDI error codes. */ enum INDI_ERROR { INDI_DEVICE_NOT_FOUND = -1, /*!< INDI Device was not found. */ INDI_PROPERTY_INVALID = -2, /*!< Property has an invalid syntax or attribute. */ INDI_PROPERTY_DUPLICATED = -3, /*!< INDI Device was not found. */ INDI_DISPATCH_ERROR = -4 /*!< Dispatching command to driver failed. */ }; /** * @brief The DRIVER_INTERFACE enum defines the class of devices the driver implements. A driver may implement one or more interfaces. */ enum DRIVER_INTERFACE { GENERAL_INTERFACE = 0, /**< Default interface for all INDI devices */ TELESCOPE_INTERFACE = (1 << 0), /**< Telescope interface, must subclass INDI::Telescope */ CCD_INTERFACE = (1 << 1), /**< CCD interface, must subclass INDI::CCD */ GUIDER_INTERFACE = (1 << 2), /**< Guider interface, must subclass INDI::GuiderInterface */ FOCUSER_INTERFACE = (1 << 3), /**< Focuser interface, must subclass INDI::FocuserInterface */ FILTER_INTERFACE = (1 << 4), /**< Filter interface, must subclass INDI::FilterInterface */ DOME_INTERFACE = (1 << 5), /**< Dome interface, must subclass INDI::Dome */ GPS_INTERFACE = (1 << 6), /**< GPS interface, must subclass INDI::GPS */ WEATHER_INTERFACE = (1 << 7), /**< Weather interface, must subclass INDI::Weather */ AO_INTERFACE = (1 << 8), /**< Adaptive Optics Interface */ DUSTCAP_INTERFACE = (1 << 9), /**< Dust Cap Interface */ LIGHTBOX_INTERFACE = (1 << 10), /**< Light Box Interface */ DETECTOR_INTERFACE = (1 << 11), /**< Detector interface, must subclass INDI::Detector */ ROTATOR_INTERFACE = (1 << 12), /**< Rotator interface, must subclass INDI::RotatorInterface */ AUX_INTERFACE = (1 << 15), /**< Auxiliary interface */ }; /** \return Return vector number property given its name */ INumberVectorProperty *getNumber(const char *name); /** \return Return vector text property given its name */ ITextVectorProperty *getText(const char *name); /** \return Return vector switch property given its name */ ISwitchVectorProperty *getSwitch(const char *name); /** \return Return vector light property given its name */ ILightVectorProperty *getLight(const char *name); /** \return Return vector BLOB property given its name */ IBLOBVectorProperty *getBLOB(const char *name); /** \return Return property state */ IPState getPropertyState(const char *name); /** \return Return property permission */ IPerm getPropertyPermission(const char *name); void registerProperty(void *p, INDI_PROPERTY_TYPE type); /** \brief Remove a property \param name name of property to be removed. Pass NULL to remove the whole device. \param errmsg buffer to store error message. \return 0 if successul, -1 otherwise. */ int removeProperty(const char *name, char *errmsg); /** \brief Return a property and its type given its name. \param name of property to be found. \param type of property found. \return If property is found, the raw void * pointer to the IXXXVectorProperty is returned. To be used you must use static_cast with given the type of property returned. For example, INumberVectorProperty *num = static_cast getRawProperty("FOO", INDI_NUMBER); \note This is a low-level function and should not be called directly unless necessary. Use getXXX instead where XXX is the property type (Number, Text, Switch..etc). */ void *getRawProperty(const char *name, INDI_PROPERTY_TYPE type = INDI_UNKNOWN); /** \brief Return a property and its type given its name. \param name of property to be found. \param type of property found. \return If property is found, it is returned. To be used you must use static_cast with given the type of property returned. */ INDI::Property *getProperty(const char *name, INDI_PROPERTY_TYPE type = INDI_UNKNOWN); /** \brief Return a list of all properties in the device. */ std::vector *getProperties() { return &pAll; } /** \brief Build driver properties from a skeleton file. \param filename full path name of the file. \return true if successful, false otherwise. A skeloton file defines the properties supported by this driver. It is a list of defXXX elements enclosed by @@ and @@ opening and closing tags. After the properties are created, they can be rerieved, manipulated, and defined to other clients. \see An example skeleton file can be found under examples/tutorial_four_sk.xml */ bool buildSkeleton(const char *filename); /** \return True if the device is connected (CONNECT=ON), False otherwise */ bool isConnected(); /** \brief Set the device name \param dev new device name */ void setDeviceName(const char *dev); /** \return Returns the device name */ const char *getDeviceName(); /** \brief Add message to the driver's message queue. \param msg Message to add. */ void addMessage(const std::string& msg); void checkMessage(XMLEle *root); void doMessage(XMLEle *msg); /** \return Returns a specific message. */ std::string messageQueue(int index) const; /** \return Returns last message message. */ std::string lastMessage(); /** \brief Set the driver's mediator to receive notification of news devices and updated property values. */ void setMediator(INDI::BaseMediator *med) { mediator = med; } /** \returns Get the meditator assigned to this driver */ INDI::BaseMediator *getMediator() { return mediator; } /** \return driver name * \note This can only be valid if DRIVER_INFO is defined by the driver. **/ const char *getDriverName(); /** \return driver executable name * \note This can only be valid if DRIVER_INFO is defined by the driver. **/ const char *getDriverExec(); /** \return driver version * \note This can only be valid if DRIVER_INFO is defined by the driver. **/ const char *getDriverVersion(); /** \brief * \return **/ /** * @brief getDriverInterface returns ORed values of @ref INDI::BaseDevice::DRIVER_INTERFACE "DRIVER_INTERFACE". It presents the device classes supported by the driver. * @return driver device interface descriptor. * @note For example, to know if the driver supports CCD interface, check the retruned value: @code{.cpp} if (device->getDriverInterface() & CCD_INTERFACE) cout << "We received a camera!" << endl; @endcode */ virtual uint16_t getDriverInterface(); protected: /** \brief Build a property given the supplied XML element (defXXX) \param root XML element to parse and build. \param errmsg buffer to store error message in parsing fails. \return 0 if parsing is successful, -1 otherwise and errmsg is set */ int buildProp(XMLEle *root, char *errmsg); /** \brief handle SetXXX commands from client */ int setValue(XMLEle *root, char *errmsg); /** \brief Parse and store BLOB in the respective vector */ int setBLOB(IBLOBVectorProperty *pp, XMLEle *root, char *errmsg); private: char *deviceID; std::vector pAll; LilXML *lp; std::vector messageLog; INDI::BaseMediator *mediator; friend class INDI::BaseClient; friend class INDI::BaseClientQt; friend class INDI::DefaultDevice; }; } libindi/libs/indibase/indibase.h0000664000175000017500000001311713263645557016133 0ustar jasemjasem #pragma once #include "indiapi.h" #include "indidevapi.h" #include "indibasetypes.h" #define MAXRBUF 2048 /** * \namespace INDI \brief Namespace to encapsulate INDI client, drivers, and mediator classes. Developers can subclass the base devices class to implement device specific functionality. This ensures interoperability and consistency among devices within the same family and reduces code overhead.
  • BaseClient: Base class for INDI clients. By subclassing BaseClient, client can easily connect to INDI server and handle device communication, command, and notifcation.
  • BaseClientQt: Qt5 based class for INDI clients. By subclassing BaseClientQt, client can easily connect to INDI server and handle device communication, command, and notifcation.
  • BaseMediator: Abstract class to provide interface for event notifications in INDI::BaseClient.
  • BaseDevice: Base class for all INDI virtual devices as handled and stored in INDI::BaseClient. It is also the parent for all drivers.
  • DefaultDevice: INDI::BaseDevice with extended functionality such as debug, simulation, and configuration support. It is the base class for all drivers and may \e only used by drivers directly, it cannot be used by clients.
  • FilterInterface: Basic interface for filter wheels functions.
  • GuiderInterface: Basic interface for guider (ST4) port functions.
  • RotatorInterface: Basic interface for Rotator functions.
  • DustCapInterface: Basic interface remotely controlled dust covers.
  • LightBoxInterface: Basic interface for remotely controlled light boxes/switches.
  • CCD: Base class for CCD drivers. Provides basic support for single chip CCD and CCDs with a guide head as well.
  • Telescope: Base class for telescope drivers.
  • FilterWheel: Base class for Filter Wheels. It implements the FilterInterface.
  • Focuser: Base class for focusers.
  • Rotator: Base class for rotators.
  • Dome: Base class for domes.
  • GPS: Base class for GPS devices.
  • Weather: Base class for Weather devices.
  • USBDevice: Base class for USB devices for direct read/write/control over USB.
  • Controller: Class to handle controller inputs like joysticks and gamepads.
  • Logger: Class to handle debugging and logging of drivers.
\author Jasem Mutlaq \author Gerry Rozema */ namespace INDI { class BaseMediator; class BaseClient; class BaseClientQt; class BaseDevice; class DefaultDevice; class FilterInterface; class RotatorInterface; class GuiderInterface; class FocuserInterface; class DomeInterface; class DustCapInterface; class LightBoxInterface; class CCD; class Detector; class Telescope; class FilterWheel; class Focuser; class Rotator; class Dome; class GPS; class Weather; class USBDevice; class Property; class Controller; class Logger; } /** * \class INDI::BaseMediator \brief Meditates event notification as generated by driver and passed to clients. */ class INDI::BaseMediator { public: /** \brief Emmited when a new device is created from INDI server. \param dp Pointer to the base device instance */ virtual void newDevice(INDI::BaseDevice *dp) = 0; /** \brief Emmited when a device is deleted from INDI server. \param dp Pointer to the base device instance. */ virtual void removeDevice(INDI::BaseDevice *dp) = 0; /** \brief Emmited when a new property is created for an INDI driver. \param property Pointer to the Property Container */ virtual void newProperty(INDI::Property *property) = 0; /** \brief Emmited when a property is deleted for an INDI driver. \param property Pointer to the Property Container to remove. */ virtual void removeProperty(INDI::Property *property) = 0; /** \brief Emmited when a new BLOB value arrives from INDI server. \param bp Pointer to filled and process BLOB. */ virtual void newBLOB(IBLOB *bp) = 0; /** \brief Emmited when a new switch value arrives from INDI server. \param svp Pointer to a switch vector property. */ virtual void newSwitch(ISwitchVectorProperty *svp) = 0; /** \brief Emmited when a new number value arrives from INDI server. \param nvp Pointer to a number vector property. */ virtual void newNumber(INumberVectorProperty *nvp) = 0; /** \brief Emmited when a new text value arrives from INDI server. \param tvp Pointer to a text vector property. */ virtual void newText(ITextVectorProperty *tvp) = 0; /** \brief Emmited when a new light value arrives from INDI server. \param lvp Pointer to a light vector property. */ virtual void newLight(ILightVectorProperty *lvp) = 0; /** \brief Emmited when a new message arrives from INDI server. \param dp pointer to the INDI device the message is sent to. \param messageID ID of the message that can be used to retrieve the message from the device's messageQueue() function. */ virtual void newMessage(INDI::BaseDevice *dp, int messageID) = 0; /** \brief Emmited when the server is connected. */ virtual void serverConnected() = 0; /** \brief Emmited when the server gets disconnected. \param exit_code 0 if client was requested to disconnect from server. -1 if connection to server is terminated due to remote server disconnection. */ virtual void serverDisconnected(int exit_code) = 0; virtual ~BaseMediator() {} }; libindi/libs/indibase/indiusbdevice.h0000664000175000017500000000413313263645557017170 0ustar jasemjasem/******************************************************************************* Copyright(c) 2011 Gerry Rozema. All rights reserved. Upgrade to libusb 1.0 by CloudMakers, s. r. o. Copyright(c) 2013 CloudMakers, s. r. o. All rights reserved. This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2 as published by the Free Software Foundation. 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 Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. *******************************************************************************/ #pragma once #include "indibase.h" #include /** * \class USBDevice \brief Class to provide general functionality of a generic USB device. Developers need to subclass USBDevice to implement any driver within INDI that requires direct read/write/control over USB. */ namespace INDI { class USBDevice { protected: libusb_device *dev; libusb_device_handle *usb_handle; int ProductId; int VendorId; int OutputType; int OutputEndpoint; int InputType; int InputEndpoint; libusb_device *FindDevice(int, int, int); public: int WriteInterrupt(unsigned char *, int, int); int ReadInterrupt(unsigned char *, int, int); int WriteBulk(unsigned char *buf, int nbytes, int timeout); int ReadBulk(unsigned char *buf, int nbytes, int timeout); int ControlMessage(unsigned char request_type, unsigned char request, unsigned int value, unsigned int index, unsigned char *data, unsigned char len); int FindEndpoints(); int Open(); void Close(); USBDevice(); USBDevice(libusb_device *dev); virtual ~USBDevice(); }; } libindi/libs/indibase/indiproperty.h0000664000175000017500000000415513263645557017107 0ustar jasemjasem/******************************************************************************* Copyright(c) 2011 Jasem Mutlaq. All rights reserved. This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2 as published by the Free Software Foundation. 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 Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. *******************************************************************************/ #pragma once #include "indibase.h" namespace INDI { class BaseDevice; /** * \class INDI::Property \brief Provides generic container for INDI properties \author Jasem Mutlaq */ class Property { public: Property(); ~Property(); void setProperty(void *); void setType(INDI_PROPERTY_TYPE t); void setRegistered(bool r); void setDynamic(bool d); void setBaseDevice(BaseDevice *idp); void *getProperty() { return pPtr; } INDI_PROPERTY_TYPE getType() { return pType; } bool getRegistered() { return pRegistered; } bool isDynamic() { return pDynamic; } BaseDevice *getBaseDevice() { return dp; } // Convenience Functions const char *getName() const; const char *getLabel() const; const char *getGroupName() const; const char *getDeviceName() const; const char *getTimestamp() const; IPState getState() const; IPerm getPermission() const; INumberVectorProperty *getNumber(); ITextVectorProperty *getText(); ISwitchVectorProperty *getSwitch(); ILightVectorProperty *getLight(); IBLOBVectorProperty *getBLOB(); private: void *pPtr; BaseDevice *dp; INDI_PROPERTY_TYPE pType; bool pRegistered; bool pDynamic; }; } // namespace INDI libindi/libs/indibase/indilogger.h0000664000175000017500000002552413263645557016505 0ustar jasemjasem/******************************************************************************* Copyright (C) 2012 Evidence Srl - www.evidence.eu.com Adapted to INDI Library by Jasem Mutlaq & Geehalel. This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2 as published by the Free Software Foundation. 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 Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. *******************************************************************************/ #pragma once #include "indiapi.h" #include "defaultdevice.h" #include #include #include #include #include #include /** * @brief Macro to configure the logger. * Example of configuration of the Logger: * DEBUG_CONF("outputfile", Logger::file_on|Logger::screen_on, DBG_DEBUG, DBG_ERROR); */ #define DEBUG_CONF(outputFile, configuration, fileVerbosityLevel, screenVerbosityLevel) \ { \ Logger::getInstance().configure(outputFile, configuration, fileVerbosityLevel, screenVerbosityLevel); \ } /** * @brief Macro to print log messages. * Example of usage of the Logger: * DEBUG(DBG_DEBUG, "hello " << "world"); */ /* #define DEBUG(priority, msg) { \ std::ostringstream __debug_stream__; \ __debug_stream__ << msg; \ Logger::getInstance().print(priority, __FILE__, __LINE__, \ __debug_stream__.str()); \ } */ #define DEBUG(priority, msg) INDI::Logger::getInstance().print(getDeviceName(), priority, __FILE__, __LINE__, msg) #define DEBUGF(priority, msg, ...) \ INDI::Logger::getInstance().print(getDeviceName(), priority, __FILE__, __LINE__, msg, __VA_ARGS__) #define DEBUGDEVICE(device, priority, msg) INDI::Logger::getInstance().print(device, priority, __FILE__, __LINE__, msg) #define DEBUGFDEVICE(device, priority, msg, ...) \ INDI::Logger::getInstance().print(device, priority, __FILE__, __LINE__, msg, __VA_ARGS__) /** * @brief Shorter logging macros. In order to use these macros, the function * (or method) "getDeviceName()" must be defined in the calling scope. * * Usage examples: * LOG_DEBUG("hello " << "world"); * LOGF_WARN("hello %s", "world"); */ #define LOG_ERROR(txt) DEBUG(INDI::Logger::DBG_ERROR, (txt)) #define LOG_WARN(txt) DEBUG(INDI::Logger::DBG_WARNING, (txt)) #define LOG_INFO(txt) DEBUG(INDI::Logger::DBG_SESSION, (txt)) #define LOG_DEBUG(txt) DEBUG(INDI::Logger::DBG_DEBUG, (txt)) #define LOG_EXTRA1(txt) DEBUG(INDI::Logger::DBG_EXTRA_1, (txt)) #define LOG_EXTRA2(txt) DEBUG(INDI::Logger::DBG_EXTRA_2, (txt)) #define LOG_EXTRA3(txt) DEBUG(INDI::Logger::DBG_EXTRA_3, (txt)) #define LOGF_ERROR(fmt, ...) DEBUGF(INDI::Logger::DBG_ERROR, (fmt), __VA_ARGS__) #define LOGF_WARN(fmt, ...) DEBUGF(INDI::Logger::DBG_WARNING, (fmt), __VA_ARGS__) #define LOGF_INFO(fmt, ...) DEBUGF(INDI::Logger::DBG_SESSION, (fmt), __VA_ARGS__) #define LOGF_DEBUG(fmt, ...) DEBUGF(INDI::Logger::DBG_DEBUG, (fmt), __VA_ARGS__) #define LOGF_EXTRA1(fmt, ...) DEBUGF(INDI::Logger::DBG_EXTRA_1, (fmt), __VA_ARGS__) #define LOGF_EXTRA2(fmt, ...) DEBUGF(INDI::Logger::DBG_EXTRA_2, (fmt), __VA_ARGS__) #define LOGF_EXTRA3(fmt, ...) DEBUGF(INDI::Logger::DBG_EXTRA_3, (fmt), __VA_ARGS__) namespace INDI { /** * @class INDI::Logger * @brief The Logger class is a simple logger to log messages to file and INDI clients. This is the implementation of a simple * logger in C++. It is implemented as a Singleton, so it can be easily called through two DEBUG macros. * It is Pthread-safe. It allows to log on both file and screen, and to specify a verbosity threshold for both of them. * * - By default, the class defines 4 levels of debugging/logging levels: * -# Errors: Use macro DEBUG(INDI::Logger::DBG_ERROR, "My Error Message) * * -# Warnings: Use macro DEBUG(INDI::Logger::DBG_WARNING, "My Warning Message) * * -# Session: Use macro DEBUG(INDI::Logger::DBG_SESSION, "My Message) Session messages are the regular status messages from the driver. * * -# Driver Debug: Use macro DEBUG(INDI::Logger::DBG_DEBUG, "My Driver Debug Message) * * @note Use DEBUGF macro if you have a variable list message. e.g. DEBUGF(INDI::Logger::DBG_SESSION, "Hello %s!", "There") * * The default \e active debug levels are Error, Warning, and Session. Driver Debug can be enabled by the client. * * To add a new debug level, call addDebugLevel(). You can add an additional 4 custom debug/logging levels. * * Check INDI Tutorial two for an example simple implementation. */ class Logger { /** Type used for the configuration */ enum loggerConf_ { L_nofile_ = 1 << 0, L_file_ = 1 << 1, L_noscreen_ = 1 << 2, L_screen_ = 1 << 3 }; #ifdef LOGGER_MULTITHREAD /// Lock for mutual exclusion between different threads static pthread_mutex_t lock_; #endif bool configured_ { false }; /** Pointer to the unique Logger (i.e., Singleton) */ static Logger *m_; /** * @brief Initial part of the name of the file used for Logging. * Date and time are automatically appended. */ static std::string logFile_; /** * @brief Directory where log file is stored. it is created under ~/.indi/logs/[DATE]/[DRIVER_EXEC] */ static std::string logDir_; /** * @brief Current configuration of the logger. * Variable to know if logging on file and on screen are enabled. Note that if the log on * file is enabled, it means that the logger has been already configured, therefore the * stream is already open. */ static loggerConf_ configuration_; /// Stream used when logging on a file std::ofstream out_; /// Initial time (used to print relative times) struct timeval initialTime_; /// Verbosity threshold for files static unsigned int fileVerbosityLevel_; /// Verbosity threshold for screen static unsigned int screenVerbosityLevel_; static unsigned int rememberscreenlevel_; /** * @brief Constructor. * It is a private constructor, called only by getInstance() and only the * first time. It is called inside a lock, so lock inside this method * is not required. * It only initializes the initial time. All configuration is done inside the * configure() method. */ Logger(); /** * @brief Destructor. * It only closes the file, if open, and cleans memory. */ ~Logger(); /** Method to lock in case of multithreading */ inline static void lock(); /** Method to unlock in case of multithreading */ inline static void unlock(); static INDI::DefaultDevice *parentDevice; public: enum VerbosityLevel { DBG_ERROR = 0x1, DBG_WARNING = 0x2, DBG_SESSION = 0x4, DBG_DEBUG = 0x8, DBG_EXTRA_1 = 0x10, DBG_EXTRA_2 = 0X20, DBG_EXTRA_3 = 0x40, DBG_EXTRA_4 = 0x80 }; struct switchinit { char name[MAXINDINAME]; char label[MAXINDILABEL]; ISState state; unsigned int levelmask; }; static const unsigned int defaultlevel = DBG_ERROR | DBG_WARNING | DBG_SESSION; static const unsigned int nlevels = 8; static struct switchinit LoggingLevelSInit[nlevels]; static ISwitch LoggingLevelS[nlevels]; static ISwitchVectorProperty LoggingLevelSP; static ISwitch ConfigurationS[2]; static ISwitchVectorProperty ConfigurationSP; typedef loggerConf_ loggerConf; static const loggerConf file_on = L_nofile_; static const loggerConf file_off = L_file_; static const loggerConf screen_on = L_noscreen_; static const loggerConf screen_off = L_screen_; static unsigned int customLevel; static unsigned int nDevices; static std::string getLogFile() { return logFile_; } static loggerConf_ getConfiguration() { return configuration_; } /** * @brief Method to get a reference to the object (i.e., Singleton) * It is a static method. * @return Reference to the object. */ static Logger &getInstance(); static bool saveConfigItems(FILE *fp); /** * @brief Adds a new debugging level to the driver. * * @param debugLevelName The descriptive debug level defined to the client. e.g. Scope Status * @param LoggingLevelName the short logging level recorded in the logfile. e.g. SCOPE * @return bitmask of the new debugging level to be used for any subsequent calls to DEBUG and DEBUGF to * record events to this debug level. */ int addDebugLevel(const char *debugLevelName, const char *LoggingLevelName); void print(const char *devicename, const unsigned int verbosityLevel, const std::string &sourceFile, const int codeLine, //const std::string& message, const char *message, ...); /** * @brief Method to configure the logger. Called by the DEBUG_CONF() macro. To make implementation * easier, the old stream is always closed. * Then, in case, it is open again in append mode. * @param outputFile of the file used for logging * @param configuration (i.e., log on file and on screen on or off) * @param fileVerbosityLevel threshold for file * @param screenVerbosityLevel threshold for screen */ void configure(const std::string &outputFile, const loggerConf configuration, const int fileVerbosityLevel, const int screenVerbosityLevel); static struct switchinit DebugLevelSInit[nlevels]; static ISwitch DebugLevelS[nlevels]; static ISwitchVectorProperty DebugLevelSP; static bool ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n); static bool initProperties(INDI::DefaultDevice *device); static bool updateProperties(bool enable); static char Tags[nlevels][MAXINDINAME]; /** * @brief Method used to print message called by the DEBUG() macro. * @param i which debugging to query its rank. The lower the rank, the more priority it is. * @return rank of debugging level requested. */ static unsigned int rank(unsigned int l); }; inline Logger::loggerConf operator|(Logger::loggerConf __a, Logger::loggerConf __b) { return Logger::loggerConf(static_cast(__a) | static_cast(__b)); } inline Logger::loggerConf operator&(Logger::loggerConf __a, Logger::loggerConf __b) { return Logger::loggerConf(static_cast(__a) & static_cast(__b)); } } libindi/libs/indibase/indicontroller.h0000664000175000017500000001474513263645557017414 0ustar jasemjasem/******************************************************************************* Copyright (C) 2013 Jasem Mutlaq This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2 as published by the Free Software Foundation. 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 Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. *******************************************************************************/ #pragma once #include "defaultdevice.h" #include // detect std::lib #include namespace INDI { /** * \class INDI::Controller * @brief The Controller class provides functionality to access a controller (e.g. joystick) input and send it to the requesting driver. * * - To use the class in an INDI::DefaultDevice based driver: * * -# Set the callback functions for each of input type requested. * * -# Map the properties you wish to /e listen to from the joystick driver by specifying * the type of input requested and the initial default value. * For example: * \code{.cpp} * mapController("ABORT", "Abort Telescope", INDI::Controller::CONTROLLER_BUTTON, "BUTTON_1"); * \endcode * After mapping all the desired controls, call the class initProperties() function. * If the user enables joystick support in the driver and presses a button on the joystick, the button * callback function will be invoked with the name & state of the button. * * -# Call the Controller's ISGetProperties(), initProperties(), updateProperties(), saveConfigItems(), and * ISNewXXX functions from the same standard functions in your driver. * * The class communicates with INDI joystick driver which in turn enumerates the game pad and provides * three types of constrcuts: *
    *
  • Joysticks: Each joystick displays a normalized magnitude [0 to 1] and an angle. The angle is measured counter clock wise starting from * the right/east direction [0 to 360]. They are defined as JOYSTICK_# where # is the joystick number.
  • *
  • Axes: Each joystick has two or more axes. Each axis has a raw value and angle. The raw value ranges from -32767.0 to 32767.0 They are * defined as AXIS_# where # is the axis number.
  • *
  • Buttons: Buttons are either on or off. They are defined as BUTTON_# where # is the button number.
  • *
* * \note All indexes start from 1. i.e. There is no BUTTON_0 or JOYSTICK_0. * \see See the LX200 Generic & Celestron GPS drivers for an example implementation. * \warning Both the indi_joystick driver and the driver using this class must be running in the same INDI server * (or chained INDI servers) in order for it to work as it depends on snooping among drivers. * \author Jasem Multaq */ class Controller { public: typedef enum { CONTROLLER_JOYSTICK, CONTROLLER_AXIS, CONTROLLER_BUTTON, CONTROLLER_UNKNOWN } ControllerType; /** * @brief joystickFunc Joystick callback function signature. */ typedef std::function joystickFunc; /** * @brief axisFunc Axis callback function signature. */ typedef std::function axisFunc; /** * @brief buttonFunc Button callback function signature. */ typedef std::function buttonFunc; /** * @brief Controller Default ctor * @param cdevice INDI::DefaultDevice device */ Controller(INDI::DefaultDevice *cdevice); virtual ~Controller(); virtual void ISGetProperties(const char *dev); virtual bool initProperties(); virtual bool updateProperties(); virtual bool ISSnoopDevice(XMLEle *root); virtual bool ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n); virtual bool ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n); virtual bool saveConfigItems(FILE *fp); /** * @brief mapController adds a new property to the joystick's settings. * @param propertyName Name * @param propertyLabel Label * @param type The input type of the property. This value cannot be updated. * @param initialValue Initial value for the property. */ void mapController(const char *propertyName, const char *propertyLabel, ControllerType type, const char *initialValue); /** * @brief clearMap clears all properties added previously by mapController() */ void clearMap(); /** * @brief setJoystickCallback Sets the callback function when a new joystick input is detected. * @param joystickCallback the callback function. */ void setJoystickCallback(joystickFunc joystickCallback); /** * @brief setAxisCallback Sets the callback function when a new axis input is detected. * @param axisCallback the callback function. */ void setAxisCallback(axisFunc axisCallback); /** * @brief setButtonCallback Sets the callback function when a new button input is detected. * @param buttonCallback the callback function. */ void setButtonCallback(buttonFunc buttonCallback); ControllerType getControllerType(const char *name); const char *getControllerSetting(const char *name); protected: static void joystickEvent(const char *joystick_n, double mag, double angle, void *context); static void axisEvent(const char *axis_n, int value, void *context); static void buttonEvent(const char *button_n, int value, void *context); void enableJoystick(); void disableJoystick(); joystickFunc joystickCallbackFunc; buttonFunc buttonCallbackFunc; axisFunc axisCallbackFunc; INDI::DefaultDevice *device; private: /* Joystick Support */ ISwitchVectorProperty UseJoystickSP; ISwitch UseJoystickS[2]; ITextVectorProperty JoystickSettingTP; IText *JoystickSettingT = nullptr; }; } libindi/libs/indibase/defaultdevice.h0000664000175000017500000004543713263645557017173 0ustar jasemjasem/******************************************************************************* Copyright(c) 2011 Jasem Mutlaq. All rights reserved. This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2 as published by the Free Software Foundation. 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 Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. *******************************************************************************/ #pragma once #include "basedevice.h" #include "indidriver.h" #include "indilogger.h" #include namespace Connection { class Interface; class Serial; class TCP; } /** * @brief COMMUNICATION_TAB Where all the properties required to connect/disconnect from * a device are located. Usually such properties may include port number, IP address, or * any property necessarily to establish a connection to the device. */ extern const char *COMMUNICATION_TAB; /** * @brief MAIN_CONTROL_TAB Where all the primary controls for the device are located. */ extern const char *MAIN_CONTROL_TAB; /** * @brief CONNECTION_TAB Where all device connection settings (serial, usb, ethernet) are defined and controlled. */ extern const char *CONNECTION_TAB; /** * @brief MOTION_TAB Where all the motion control properties of the device are located. */ extern const char *MOTION_TAB; /** * @brief DATETIME_TAB Where all date and time setting properties are located. */ extern const char *DATETIME_TAB; /** * @brief SITE_TAB Where all site information setting are located. */ extern const char *SITE_TAB; /** * @brief OPTIONS_TAB Where all the driver's options are located. Those may include auxiliary controls, driver * metadata, version information..etc. */ extern const char *OPTIONS_TAB; /** * @brief FILTER_TAB Where all the properties for filter wheels are located. */ extern const char *FILTER_TAB; /** * @brief FOCUS_TAB Where all the properties for focuser are located. */ extern const char *FOCUS_TAB; /** * @brief GUIDE_TAB Where all the properties for guiding are located. */ extern const char *GUIDE_TAB; /** * @brief ALIGNMENT_TAB Where all the properties for guiding are located. */ extern const char *ALIGNMENT_TAB; /** * @brief INFO_TAB Where all the properties for general information are located. */ extern const char *INFO_TAB; /** * \class INDI::DefaultDevice * \brief Class to provide extended functionality for devices in addition * to the functionality provided by INDI::BaseDevice. This class should \e only be subclassed by * drivers directly as it is linked with main(). Virtual drivers cannot employ INDI::DefaultDevice. * * INDI::DefaultDevice provides capability to add Debug, Simulation, and Configuration controls. * These controls (switches) are defined to the client. Configuration options permit saving and * loading of AS-IS property values. * * \see Tutorial Four * \author Jasem Mutlaq */ class INDI::DefaultDevice : public INDI::BaseDevice { public: DefaultDevice(); virtual ~DefaultDevice(); /** \brief Add Debug, Simulation, and Configuration options to the driver */ void addAuxControls(); /** \brief Add Debug control to the driver */ void addDebugControl(); /** \brief Add Simulation control to the driver */ void addSimulationControl(); /** \brief Add Configuration control to the driver */ void addConfigurationControl(); /** \brief Add Polling period control to the driver */ void addPollPeriodControl(); /** \brief Set all properties to IDLE state */ void resetProperties(); /** * \brief Define number vector to client & register it. Alternatively, IDDefNumber can * be used but the property will not get registered and the driver will not be able to * save configuration files. * \param nvp The number vector property to be defined */ void defineNumber(INumberVectorProperty *nvp); /** * \brief Define text vector to client & register it. Alternatively, IDDefText can be * used but the property will not get registered and the driver will not be able to save * configuration files. * \param tvp The text vector property to be defined */ void defineText(ITextVectorProperty *tvp); /** * \brief Define switch vector to client & register it. Alternatively, IDDefswitch can be * used but the property will not get registered and the driver will not be able to save * configuration files. * \param svp The switch vector property to be defined */ void defineSwitch(ISwitchVectorProperty *svp); /** * \brief Define light vector to client & register it. Alternatively, IDDeflight can be * used but the property will not get registered and the driver will not be able to save * configuration files. * \param lvp The light vector property to be defined */ void defineLight(ILightVectorProperty *lvp); /** * \brief Define BLOB vector to client & register it. Alternatively, IDDefBLOB can be * used but the property will not get registered and the driver will not be able to * save configuration files. * \param bvp The BLOB vector property to be defined */ void defineBLOB(IBLOBVectorProperty *bvp); /** * \brief Delete a property and unregister it. It will also be deleted from all clients. * \param propertyName name of property to be deleted. */ virtual bool deleteProperty(const char *propertyName); /** * \brief Set connection switch status in the client. * \param status If true, the driver will attempt to connect to the device (CONNECT=ON). * If false, it will attempt to disconnect the device. * \param status True to set CONNECT on, false to set DISCONNECT on. * \param state State of CONNECTION properti, by default IPS_OK. * \param msg A message to be sent along with connect/disconnect command, by default nullptr. */ virtual void setConnected(bool status, IPState state = IPS_OK, const char *msg = nullptr); /** * \brief Set a timer to call the function TimerHit after ms milliseconds * \param ms timer duration in milliseconds. * \return id of the timer to be used with RemoveTimer */ int SetTimer(uint32_t ms); /** * \brief Remove timer added with SetTimer * \param id ID of the timer as returned from SetTimer */ void RemoveTimer(int id); /** \brief Callback function to be called once SetTimer duration elapses. */ virtual void TimerHit(); /** \return driver executable filename */ virtual const char *getDriverExec() { return me; } /** \return driver name */ virtual const char *getDriverName() { return getDefaultName(); } /** * \brief Set driver version information to be defined in DRIVER_INFO property as vMajor.vMinor * \param vMajor major revision number * \param vMinor minor revision number */ void setVersion(uint16_t vMajor, uint16_t vMinor) { majorVersion = vMajor; minorVersion = vMinor; } /** \return Major driver version number. */ uint16_t getMajorVersion() { return majorVersion; } /** \return Minor driver version number. */ uint16_t getMinorVersion() { return minorVersion; } /** * \brief define the driver's properties to the client. * Usually, only a minimum set of properties are defined to the client in this function * if the device is in disconnected state. Those properties should be enough to enable the * client to establish a connection to the device. In addition to CONNECT/DISCONNECT, such * properties may include port name, IP address, etc. You should check if the device is * already connected, and if this is true, then you must define the remainder of the * the properties to the client in this function. Otherwise, the remainder of the driver's * properties are defined to the client in updateProperties() function which is called when * a client connects/disconnects from a device. * \param dev name of the device * \note This function is called by the INDI framework, do not call it directly. See LX200 * Generic driver for an example implementation */ virtual void ISGetProperties(const char *dev); /** * \brief Process the client newSwitch command * \note This function is called by the INDI framework, do not call it directly. * \returns True if any property was successfully processed, false otherwise. */ virtual bool ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n); /** * \brief Process the client newNumber command * \note This function is called by the INDI framework, do not call it directly. * \returns True if any property was successfully processed, false otherwise. */ virtual bool ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n); /** * \brief Process the client newSwitch command * \note This function is called by the INDI framework, do not call it directly. * \returns True if any property was successfully processed, false otherwise. */ virtual bool ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n); /** * \brief Process the client newBLOB command * \note This function is called by the INDI framework, do not call it directly. * \returns True if any property was successfully processed, false otherwise. */ virtual bool ISNewBLOB(const char *dev, const char *name, int sizes[], int blobsizes[], char *blobs[], char *formats[], char *names[], int n); /** * \brief Process a snoop event from INDI server. This function is called when a snooped property is * updated in a snooped driver. * \note This function is called by the INDI framework, do not call it directly. * \returns True if any property was successfully processed, false otherwise. */ virtual bool ISSnoopDevice(XMLEle *root); /** * @return getInterface Return the interface declared by the driver. */ virtual uint16_t getDriverInterface(); /** * @brief setInterface Set driver interface. By default the driver interface is set to GENERAL_DEVICE. * You may send an ORed list of DeviceInterface values. * @param value ORed list of DeviceInterface values. */ void setDriverInterface(uint16_t value); protected: /** * @brief setDynamicPropertiesBehavior controls handling of dynamic properties. Dyanmic properties * are those generated from an external skeleton XML file. By default all properties, including * dynamic properties, are defined to the client in ISGetProperties(). Furthermore, when * űdeleteProperty(properyName) is called, the dynamic property is deleted by default, and can only * be restored by calling buildSkeleton(filename) again. However, it is sometimes desirable to skip * the definition of the dynamic properties on startup and delegate this task to the child class. * To control this behavior, set enabled to false. * @param defineEnabled True to define all dynamic properties in INDI::DefaultDevice own * ISGetProperties() on startup. False to skip defining dynamic properties. * @param deleteEnabled True to delete dynamic properties from memory in deleteProperty(name). * False to keep dynamic property in the properties list, but delete it from the client. * @note This function has no effect on regular properties initialized directly by the driver. */ void setDynamicPropertiesBehavior(bool defineEnabled, bool deleteEnabled) { defineDynamicProperties = defineEnabled; deleteDynamicProperties = deleteEnabled; } // Configuration /** * \brief Load the last saved configuration file * \param silent if true, don't report any error or notification messages. * \param property Name of property to load configuration for. If nullptr, all properties in the * configuration file are loaded which is the default behavior. * \return True if successful, false otherwise. */ virtual bool loadConfig(bool silent = false, const char *property = nullptr); /** * \brief Save the current properties in a configuration file * \param silent if true, don't report any error or notification messages. * \param property Name of specific property to save while leaving all others properties in the * file as is. * \return True if successful, false otherwise. */ virtual bool saveConfig(bool silent = false, const char *property = nullptr); /** * @brief saveConfigItems Save specific properties in the provide config file handler. Child * class usually override this function to save their own properties and the base class * saveConfigItems(fp) must be explicitly called by each child class. The Default Device * saveConfigItems(fp) only save Debug properties options in the config file. * @param fp Pointer to config file handler * @return True if successful, false otherwise. */ virtual bool saveConfigItems(FILE *fp); /** * @brief saveAllConfigItems Save all the drivers' properties in the configuration file * @param fp pointer to config file handler * @return True if successful, false otherwise. */ virtual bool saveAllConfigItems(FILE *fp); /** * \brief Load the default configuration file * \return True if successful, false otherwise. */ virtual bool loadDefaultConfig(); // Simulatin & Debug /** * \brief Toggle driver debug status * A driver can be more verbose if Debug option is enabled by the client. * \param enable If true, the Debug option is set to ON. */ void setDebug(bool enable); /** * \brief Toggle driver simulation status * A driver can run in simulation mode if Simulation option is enabled by the client. * \param enable If true, the Simulation option is set to ON. */ void setSimulation(bool enable); /** * \brief Inform driver that the debug option was triggered. * This function is called after setDebug is triggered by the client. Reimplement this * function if your driver needs to take specific action after debug is enabled/disabled. * Otherwise, you can use isDebug() to check if simulation is enabled or disabled. * \param enable If true, the debug option is set to ON. */ virtual void debugTriggered(bool enable); /** * \brief Inform driver that the simulation option was triggered. * This function is called after setSimulation is triggered by the client. Reimplement this * function if your driver needs to take specific action after simulation is enabled/disabled. * Otherwise, you can use isSimulation() to check if simulation is enabled or disabled. * \param enable If true, the simulation option is set to ON. */ virtual void simulationTriggered(bool enable); /** \return True if Debug is on, False otherwise. */ bool isDebug(); /** \return True if Simulation is on, False otherwise. */ bool isSimulation(); /** * \brief Initilize properties initial state and value. The child class must implement this function. * \return True if initilization is successful, false otherwise. */ virtual bool initProperties(); /** * \brief updateProperties is called whenever there is a change in the CONNECTION status of * the driver. This will enable the driver to react to changes of switching ON/OFF a device. * For example, a driver may only define a set of properties after a device is connected, but * not before. * \return True if update is successful, false otherwise. */ virtual bool updateProperties(); /** * \brief Connect to the device. INDI::DefaultDevice implementation connects to appropriate * connection interface (Serial or TCP) governed by connectionMode. If connection is successful, * it proceed to call Handshake() function to ensure communication with device is successful. * For other communication interface, override the method in the child class implementation * \return True if connection is successful, false otherwise */ virtual bool Connect(); /** * \brief Disconnect from device * \return True if successful, false otherwise */ virtual bool Disconnect(); /** * @brief registerConnection Add new connection plugin to the existing connection pool. The * connection type shall be defined to the client in ISGetProperties() * @param newConnection Pointer to new connection plugin */ void registerConnection(Connection::Interface *newConnection); /** * @brief unRegisterConnection Remove connection from existing pool * @param existingConnection pointer to connection interface * @return True if connection is removed, false otherwise. */ bool unRegisterConnection(Connection::Interface *existingConnection); /** @return Return actively selected connection plugin */ Connection::Interface *getActiveConnection() { return activeConnection; } void setDefaultPollingPeriod(uint32_t period); uint32_t getPollingPeriod() { return static_cast(PollPeriodN[0].value); } /** \return Default name of the device. */ virtual const char *getDefaultName() = 0; /// Period in milliseconds to call TimerHit(). Default 1000 ms uint32_t POLLMS = 1000; private: bool isInit { false }; bool pDebug { false }; bool pSimulation { false }; uint16_t majorVersion { 1 }; uint16_t minorVersion { 0 }; uint16_t interfaceDescriptor { 0 }; ISwitch DebugS[2]; ISwitch SimulationS[2]; ISwitch ConfigProcessS[3]; ISwitch ConnectionS[2]; INumber PollPeriodN[1]; ISwitchVectorProperty DebugSP; ISwitchVectorProperty SimulationSP; ISwitchVectorProperty ConfigProcessSP; ISwitchVectorProperty ConnectionSP; INumberVectorProperty PollPeriodNP; IText DriverInfoT[4] {}; ITextVectorProperty DriverInfoTP; // Connection modes ISwitch *ConnectionModeS = nullptr; ISwitchVectorProperty ConnectionModeSP; std::vector connections; Connection::Interface *activeConnection = nullptr; // Connection Plugins friend class Connection::Serial; friend class Connection::TCP; friend class FilterInterface; bool defineDynamicProperties = true; bool deleteDynamicProperties = true; }; libindi/libs/indibase/indiweather.cpp0000664000175000017500000004311613263645557017215 0ustar jasemjasem/******************************************************************************* Copyright(c) 2015 Jasem Mutlaq. All rights reserved. INDI Weather Device Class This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program 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 General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. The full GNU General Public License is included in this distribution in the file called LICENSE. *******************************************************************************/ #include "indiweather.h" #include "connectionplugins/connectionserial.h" #include "connectionplugins/connectiontcp.h" #include #define PARAMETERS_TAB "Parameters" namespace INDI { Weather::Weather() { ParametersN = nullptr; critialParametersL = nullptr; updateTimerID = -1; ParametersRangeNP = nullptr; nRanges = 0; } Weather::~Weather() { for (int i = 0; i < ParametersNP.nnp; i++) { free(ParametersN[i].aux0); free(ParametersN[i].aux1); free(ParametersRangeNP[i].np); } free(ParametersN); free(ParametersRangeNP); free(critialParametersL); } bool Weather::initProperties() { DefaultDevice::initProperties(); // Parameters IUFillNumberVector(&ParametersNP, nullptr, 0, getDeviceName(), "WEATHER_PARAMETERS", "Parameters", PARAMETERS_TAB, IP_RO, 60, IPS_OK); // Refresh IUFillSwitch(&RefreshS[0], "REFRESH", "Refresh", ISS_OFF); IUFillSwitchVector(&RefreshSP, RefreshS, 1, getDeviceName(), "WEATHER_REFRESH", "Weather", MAIN_CONTROL_TAB, IP_RW, ISR_ATMOST1, 0, IPS_IDLE); // Weather Status IUFillLightVector(&critialParametersLP, nullptr, 0, getDeviceName(), "WEATHER_STATUS", "Status", MAIN_CONTROL_TAB, IPS_IDLE); // Location IUFillNumber(&LocationN[LOCATION_LATITUDE], "LAT", "Lat (dd:mm:ss)", "%010.6m", -90, 90, 0, 0.0); IUFillNumber(&LocationN[LOCATION_LONGITUDE], "LONG", "Lon (dd:mm:ss)", "%010.6m", 0, 360, 0, 0.0); IUFillNumber(&LocationN[LOCATION_ELEVATION], "ELEV", "Elevation (m)", "%g", -200, 10000, 0, 0); IUFillNumberVector(&LocationNP, LocationN, 3, getDeviceName(), "GEOGRAPHIC_COORD", "Location", SITE_TAB, IP_RW, 60, IPS_OK); // Update Period IUFillNumber(&UpdatePeriodN[0], "PERIOD", "Period (secs)", "%4.2f", 0, 3600, 60, 60); IUFillNumberVector(&UpdatePeriodNP, UpdatePeriodN, 1, getDeviceName(), "WEATHER_UPDATE", "Update", MAIN_CONTROL_TAB, IP_RW, 60, IPS_IDLE); // Active Devices IUFillText(&ActiveDeviceT[0], "ACTIVE_GPS", "GPS", "GPS Simulator"); IUFillTextVector(&ActiveDeviceTP, ActiveDeviceT, 1, getDeviceName(), "ACTIVE_DEVICES", "Snoop devices", OPTIONS_TAB, IP_RW, 60, IPS_IDLE); IDSnoopDevice(ActiveDeviceT[0].text, "GEOGRAPHIC_COORD"); if (weatherConnection & CONNECTION_SERIAL) { serialConnection = new Connection::Serial(this); serialConnection->registerHandshake([&]() { return callHandshake(); }); registerConnection(serialConnection); } if (weatherConnection & CONNECTION_TCP) { tcpConnection = new Connection::TCP(this); tcpConnection->registerHandshake([&]() { return callHandshake(); }); registerConnection(tcpConnection); } setDriverInterface(WEATHER_INTERFACE); return true; } bool Weather::updateProperties() { DefaultDevice::updateProperties(); if (isConnected()) { updateTimerID = -1; if (critialParametersL) defineLight(&critialParametersLP); defineNumber(&UpdatePeriodNP); defineSwitch(&RefreshSP); if (ParametersN) defineNumber(&ParametersNP); if (ParametersRangeNP) { for (int i = 0; i < nRanges; i++) defineNumber(&ParametersRangeNP[i]); } defineNumber(&LocationNP); defineText(&ActiveDeviceTP); DEBUG(Logger::DBG_SESSION, "Weather update is in progress..."); TimerHit(); } else { if (critialParametersL) deleteProperty(critialParametersLP.name); deleteProperty(UpdatePeriodNP.name); deleteProperty(RefreshSP.name); if (ParametersN) deleteProperty(ParametersNP.name); if (ParametersRangeNP) { for (int i = 0; i < nRanges; i++) deleteProperty(ParametersRangeNP[i].name); } deleteProperty(LocationNP.name); deleteProperty(ActiveDeviceTP.name); } return true; } bool Weather::ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) { if (dev != nullptr && strcmp(dev, getDeviceName()) == 0) { if (!strcmp(name, RefreshSP.name)) { RefreshS[0].s = ISS_OFF; RefreshSP.s = IPS_OK; IDSetSwitch(&RefreshSP, nullptr); TimerHit(); } } return DefaultDevice::ISNewSwitch(dev, name, states, names, n); } bool Weather::ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) { // first check if it's for our device if (dev != nullptr && strcmp(dev, getDeviceName()) == 0) { if (strcmp(name, "GEOGRAPHIC_COORD") == 0) { int latindex = IUFindIndex("LAT", names, n); int longindex = IUFindIndex("LONG", names, n); int elevationindex = IUFindIndex("ELEV", names, n); if (latindex == -1 || longindex == -1 || elevationindex == -1) { LocationNP.s = IPS_ALERT; IDSetNumber(&LocationNP, "Location data missing or corrupted."); } double targetLat = values[latindex]; double targetLong = values[longindex]; double targetElev = values[elevationindex]; return processLocationInfo(targetLat, targetLong, targetElev); } // Update period if (strcmp(name, "WEATHER_UPDATE") == 0) { IUUpdateNumber(&UpdatePeriodNP, values, names, n); UpdatePeriodNP.s = IPS_OK; IDSetNumber(&UpdatePeriodNP, nullptr); if (UpdatePeriodN[0].value == 0) DEBUG(Logger::DBG_SESSION, "Periodic updates are disabled."); else { if (updateTimerID > 0) RemoveTimer(updateTimerID); updateTimerID = SetTimer(UpdatePeriodN[0].value * 1000); } return true; } for (int i = 0; i < nRanges; i++) { if (!strcmp(name, ParametersRangeNP[i].name)) { IUUpdateNumber(&ParametersRangeNP[i], values, names, n); ParametersN[i].min = ParametersRangeNP[i].np[0].value; ParametersN[i].max = ParametersRangeNP[i].np[1].value; *((double *)ParametersN[i].aux0) = ParametersRangeNP[i].np[2].value; *((double *)ParametersN[i].aux1) = ParametersRangeNP[i].np[3].value; updateWeatherState(); ParametersRangeNP[i].s = IPS_OK; IDSetNumber(&ParametersRangeNP[i], nullptr); return true; } } } return DefaultDevice::ISNewNumber(dev, name, values, names, n); } bool INDI::Weather::ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n) { // first check if it's for our device if (dev != nullptr && strcmp(dev, getDeviceName()) == 0) { if (!strcmp(name, ActiveDeviceTP.name)) { ActiveDeviceTP.s = IPS_OK; IUUpdateText(&ActiveDeviceTP, texts, names, n); // Update client display IDSetText(&ActiveDeviceTP, nullptr); IDSnoopDevice(ActiveDeviceT[0].text, "GEOGRAPHIC_COORD"); return true; } } return DefaultDevice::ISNewText(dev, name, texts, names, n); } bool INDI::Weather::ISSnoopDevice(XMLEle *root) { XMLEle *ep = nullptr; const char *propName = findXMLAttValu(root, "name"); if (isConnected()) { if (!strcmp(propName, "GEOGRAPHIC_COORD")) { // Only accept IPS_OK state if (strcmp(findXMLAttValu(root, "state"), "Ok")) return false; double longitude = -1, latitude = -1, elevation = -1; for (ep = nextXMLEle(root, 1); ep != nullptr; ep = nextXMLEle(root, 0)) { const char *elemName = findXMLAttValu(ep, "name"); if (!strcmp(elemName, "LAT")) latitude = atof(pcdataXMLEle(ep)); else if (!strcmp(elemName, "LONG")) longitude = atof(pcdataXMLEle(ep)); else if (!strcmp(elemName, "ELEV")) elevation = atof(pcdataXMLEle(ep)); } return processLocationInfo(latitude, longitude, elevation); } } return DefaultDevice::ISSnoopDevice(root); } void Weather::TimerHit() { if (!isConnected()) return; if (updateTimerID > 0) RemoveTimer(updateTimerID); IPState state = updateWeather(); switch (state) { // Ok case IPS_OK: updateWeatherState(); ParametersNP.s = state; IDSetNumber(&ParametersNP, nullptr); // If update period is set, then set up the timer if (UpdatePeriodN[0].value > 0) updateTimerID = SetTimer((int)(UpdatePeriodN[0].value * 1000)); return; // Alert // We retry every 5000 ms until we get OK case IPS_ALERT: ParametersNP.s = state; IDSetNumber(&ParametersNP, nullptr); break; // Weather update is in progress default: break; } updateTimerID = SetTimer(5000); } IPState Weather::updateWeather() { DEBUG(Logger::DBG_ERROR, "updateWeather() must be implemented in Weather device child class to update GEOGRAPHIC_COORD properties."); return IPS_ALERT; } bool Weather::updateLocation(double latitude, double longitude, double elevation) { INDI_UNUSED(latitude); INDI_UNUSED(longitude); INDI_UNUSED(elevation); return true; } bool Weather::processLocationInfo(double latitude, double longitude, double elevation) { // Do not update if not necessary if (latitude == LocationN[LOCATION_LATITUDE].value && longitude == LocationN[LOCATION_LONGITUDE].value && elevation == LocationN[LOCATION_ELEVATION].value) { LocationNP.s = IPS_OK; IDSetNumber(&LocationNP, nullptr); } if (updateLocation(latitude, longitude, elevation)) { LocationNP.s = IPS_OK; LocationN[LOCATION_LATITUDE].value = latitude; LocationN[LOCATION_LONGITUDE].value = longitude; LocationN[LOCATION_ELEVATION].value = elevation; // Update client display IDSetNumber(&LocationNP, nullptr); return true; } else { LocationNP.s = IPS_ALERT; // Update client display IDSetNumber(&LocationNP, nullptr); return false; } } void Weather::addParameter(std::string name, std::string label, double minimumOK, double maximumOK, double minimumWarning, double maximumWarning) { DEBUGF(Logger::DBG_DEBUG, "Parameter %s is added. Ok (%g,%g) Warn (%g,%g)", name.c_str(), minimumOK, maximumOK, minimumWarning, maximumWarning); ParametersN = (ParametersN == nullptr) ? (INumber *)malloc(sizeof(INumber)) : (INumber *)realloc(ParametersN, (ParametersNP.nnp + 1) * sizeof(INumber)); double *minWarn = (double *)malloc(sizeof(double)); double *maxWarn = (double *)malloc(sizeof(double)); *minWarn = minimumWarning; *maxWarn = maximumWarning; IUFillNumber(&ParametersN[ParametersNP.nnp], name.c_str(), label.c_str(), "%4.2f", minimumOK, maximumOK, 0, 0); ParametersN[ParametersNP.nnp].aux0 = minWarn; ParametersN[ParametersNP.nnp].aux1 = maxWarn; ParametersNP.np = ParametersN; ParametersNP.nnp++; createParameterRange(name, label); } void Weather::setParameterValue(std::string name, double value) { for (int i = 0; i < ParametersNP.nnp; i++) { if (!strcmp(ParametersN[i].name, name.c_str())) { ParametersN[i].value = value; return; } } } bool Weather::setCriticalParameter(std::string param) { for (int i = 0; i < ParametersNP.nnp; i++) { if (!strcmp(ParametersN[i].name, param.c_str())) { critialParametersL = (critialParametersL == nullptr) ? (ILight *)malloc(sizeof(ILight)) : (ILight *)realloc(critialParametersL, (critialParametersLP.nlp + 1) * sizeof(ILight)); IUFillLight(&critialParametersL[critialParametersLP.nlp], param.c_str(), ParametersN[i].label, IPS_IDLE); critialParametersLP.lp = critialParametersL; critialParametersLP.nlp++; return true; } } DEBUGF(Logger::DBG_WARNING, "Unable to find parameter %s in list of existing parameters!", param.c_str()); return false; } void Weather::updateWeatherState() { if (critialParametersL == nullptr) return; critialParametersLP.s = IPS_IDLE; for (int i = 0; i < critialParametersLP.nlp; i++) { for (int j = 0; j < ParametersNP.nnp; j++) { if (!strcmp(critialParametersL[i].name, ParametersN[j].name)) { double minWarn = *(static_cast(ParametersN[j].aux0)); double maxWarn = *(static_cast(ParametersN[j].aux1)); if ((ParametersN[j].value >= ParametersN[j].min) && (ParametersN[j].value <= ParametersN[j].max)) critialParametersL[i].s = IPS_OK; else if ((ParametersN[j].value >= minWarn) && (ParametersN[j].value <= maxWarn)) { critialParametersL[i].s = IPS_BUSY; DEBUGF(Logger::DBG_WARNING, "Warning: Parameter %s value (%g) is in the warning zone!", ParametersN[j].label, ParametersN[j].value); } else { critialParametersL[i].s = IPS_ALERT; DEBUGF(Logger::DBG_WARNING, "Caution: Parameter %s value (%g) is in the danger zone!", ParametersN[j].label, ParametersN[j].value); } break; } } // The overall state is the worst individual state. if (critialParametersL[i].s > critialParametersLP.s) critialParametersLP.s = critialParametersL[i].s; } IDSetLight(&critialParametersLP, nullptr); } void Weather::createParameterRange(std::string name, std::string label) { ParametersRangeNP = (ParametersRangeNP == nullptr) ? (INumberVectorProperty *)malloc(sizeof(INumberVectorProperty)) : (INumberVectorProperty *)realloc(ParametersRangeNP, (nRanges + 1) * sizeof(INumberVectorProperty)); INumber *rangesN = (INumber *)malloc(sizeof(INumber) * 4); IUFillNumber(&rangesN[0], "MIN_OK", "Min OK", "%4.2f", -1e6, 1e6, 0, ParametersN[nRanges].min); IUFillNumber(&rangesN[1], "MAX_OK", "Max OK", "%4.2f", -1e6, 1e6, 0, ParametersN[nRanges].max); IUFillNumber(&rangesN[2], "MIN_WARN", "Min Warn", "%4.2f", -1e6, 1e6, 0, *((double *)ParametersN[nRanges].aux0)); IUFillNumber(&rangesN[3], "MAX_WARN", "Max Warn", "%4.2f", -1e6, 1e6, 0, *((double *)ParametersN[nRanges].aux1)); char propName[MAXINDINAME]; char propLabel[MAXINDILABEL]; snprintf(propName, MAXINDINAME, "%s Range", name.c_str()); snprintf(propLabel, MAXINDILABEL, "%s Range", label.c_str()); IUFillNumberVector(&ParametersRangeNP[nRanges], rangesN, 4, getDeviceName(), propName, propLabel, PARAMETERS_TAB, IP_RW, 60, IPS_IDLE); nRanges++; } bool Weather::saveConfigItems(FILE *fp) { DefaultDevice::saveConfigItems(fp); IUSaveConfigText(fp, &ActiveDeviceTP); IUSaveConfigNumber(fp, &LocationNP); IUSaveConfigNumber(fp, &UpdatePeriodNP); for (int i = 0; i < nRanges; i++) IUSaveConfigNumber(fp, &ParametersRangeNP[i]); return true; } bool Weather::Handshake() { return false; } bool Weather::callHandshake() { if (weatherConnection > 0) { if (getActiveConnection() == serialConnection) PortFD = serialConnection->getPortFD(); else if (getActiveConnection() == tcpConnection) PortFD = tcpConnection->getPortFD(); } return Handshake(); } uint8_t Weather::getWeatherConnection() const { return weatherConnection; } void Weather::setWeatherConnection(const uint8_t &value) { uint8_t mask = CONNECTION_SERIAL | CONNECTION_TCP | CONNECTION_NONE; if (value == 0 || (mask & value) == 0) { DEBUGF(Logger::DBG_ERROR, "Invalid connection mode %d", value); return; } weatherConnection = value; } } libindi/libs/indibase/indifilterwheel.cpp0000664000175000017500000001442413263645557020070 0ustar jasemjasem/******************************************************************************* Copyright(c) 2010, 2011 Gerry Rozema. All rights reserved. This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2 as published by the Free Software Foundation. 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 Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. *******************************************************************************/ #include "indifilterwheel.h" #include "indicontroller.h" #include "connectionplugins/connectionserial.h" #include "connectionplugins/connectiontcp.h" #include namespace INDI { FilterWheel::FilterWheel() : FilterInterface(this) { controller = new Controller(this); controller->setJoystickCallback(joystickHelper); controller->setButtonCallback(buttonHelper); } bool FilterWheel::initProperties() { DefaultDevice::initProperties(); FilterInterface::initProperties(FILTER_TAB); controller->mapController("Change Filter", "Change Filter", Controller::CONTROLLER_JOYSTICK, "JOYSTICK_1"); controller->mapController("Reset", "Reset", Controller::CONTROLLER_BUTTON, "BUTTON_1"); controller->initProperties(); setDriverInterface(FILTER_INTERFACE); if (filterConnection & CONNECTION_SERIAL) { serialConnection = new Connection::Serial(this); serialConnection->registerHandshake([&]() { return callHandshake(); }); registerConnection(serialConnection); } if (filterConnection & CONNECTION_TCP) { tcpConnection = new Connection::TCP(this); tcpConnection->registerHandshake([&]() { return callHandshake(); }); registerConnection(tcpConnection); } return true; } void FilterWheel::ISGetProperties(const char *dev) { DefaultDevice::ISGetProperties(dev); controller->ISGetProperties(dev); return; } bool FilterWheel::updateProperties() { // Update default device DefaultDevice::updateProperties(); // Update Filter Interface FilterInterface::updateProperties(); // Update controller controller->updateProperties(); return true; } bool FilterWheel::ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) { controller->ISNewSwitch(dev, name, states, names, n); return DefaultDevice::ISNewSwitch(dev, name, states, names, n); } bool FilterWheel::ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) { if (dev != nullptr && strcmp(dev, getDeviceName()) == 0) { if (strcmp(name, "FILTER_SLOT") == 0) { FilterInterface::processNumber(dev, name, values, names, n); return true; } } return DefaultDevice::ISNewNumber(dev, name, values, names, n); } bool FilterWheel::ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n) { if (dev != nullptr && strcmp(dev, getDeviceName()) == 0) { if (strcmp(name, FilterNameTP->name) == 0) { FilterInterface::processText(dev, name, texts, names, n); return true; } } controller->ISNewText(dev, name, texts, names, n); return DefaultDevice::ISNewText(dev, name, texts, names, n); } bool FilterWheel::saveConfigItems(FILE *fp) { DefaultDevice::saveConfigItems(fp); FilterInterface::saveConfigItems(fp); controller->saveConfigItems(fp); return true; } int FilterWheel::QueryFilter() { return -1; } bool FilterWheel::SelectFilter(int) { return false; } bool FilterWheel::ISSnoopDevice(XMLEle *root) { controller->ISSnoopDevice(root); return DefaultDevice::ISSnoopDevice(root); } void FilterWheel::joystickHelper(const char *joystick_n, double mag, double angle, void *context) { static_cast(context)->processJoystick(joystick_n, mag, angle); } void FilterWheel::buttonHelper(const char *button_n, ISState state, void *context) { static_cast(context)->processButton(button_n, state); } void FilterWheel::processJoystick(const char *joystick_n, double mag, double angle) { if (!strcmp(joystick_n, "Change Filter")) { // Put high threshold if (mag > 0.9) { // North if (angle > 0 && angle < 180) { // Previous switch if (FilterSlotN[0].value == FilterSlotN[0].min) TargetFilter = FilterSlotN[0].max; else TargetFilter = FilterSlotN[0].value - 1; SelectFilter(TargetFilter); } // South if (angle > 180 && angle < 360) { // Next Switch if (FilterSlotN[0].value == FilterSlotN[0].max) TargetFilter = FilterSlotN[0].min; else TargetFilter = FilterSlotN[0].value + 1; SelectFilter(TargetFilter); } } } } void FilterWheel::processButton(const char *button_n, ISState state) { //ignore OFF if (state == ISS_OFF) return; // Reset if (!strcmp(button_n, "Reset")) { TargetFilter = FilterSlotN[0].min; SelectFilter(TargetFilter); } } bool FilterWheel::Handshake() { return false; } bool FilterWheel::callHandshake() { if (filterConnection > 0) { if (getActiveConnection() == serialConnection) PortFD = serialConnection->getPortFD(); else if (getActiveConnection() == tcpConnection) PortFD = tcpConnection->getPortFD(); } return Handshake(); } void FilterWheel::setFilterConnection(const uint8_t &value) { uint8_t mask = CONNECTION_SERIAL | CONNECTION_TCP | CONNECTION_NONE; if (value == 0 || (mask & value) == 0) { DEBUGF(Logger::DBG_ERROR, "Invalid connection mode %d", value); return; } filterConnection = value; } } libindi/libs/indibase/indiproperty.cpp0000664000175000017500000002066313263645557017444 0ustar jasemjasem/******************************************************************************* Copyright(c) 2011 Jasem Mutlaq. All rights reserved. This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2 as published by the Free Software Foundation. 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 Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. *******************************************************************************/ #include "indiproperty.h" #include INDI::Property::Property() { pPtr = nullptr; pRegistered = false; pDynamic = false; pType = INDI_UNKNOWN; } INDI::Property::~Property() { // Only delete properties if they were created dynamically via the buildSkeleton // function. Other drivers are responsible for their own memory allocation. if (pDynamic) { switch (pType) { case INDI_NUMBER: { INumberVectorProperty *p = (INumberVectorProperty *)pPtr; free(p->np); delete p; break; } case INDI_TEXT: { ITextVectorProperty *p = (ITextVectorProperty *)pPtr; for (int i = 0; i < p->ntp; ++i) { free(p->tp[i].text); } free(p->tp); delete p; break; } case INDI_SWITCH: { ISwitchVectorProperty *p = (ISwitchVectorProperty *)pPtr; free(p->sp); delete p; break; } case INDI_LIGHT: { ILightVectorProperty *p = (ILightVectorProperty *)pPtr; free(p->lp); delete p; break; } case INDI_BLOB: { IBLOBVectorProperty *p = (IBLOBVectorProperty *)pPtr; for (int i = 0; i < p->nbp; ++i) { free(p->bp[i].blob); } free(p->bp); delete p; break; } case INDI_UNKNOWN: break; } } } void INDI::Property::setProperty(void *p) { pRegistered = true; pPtr = p; } void INDI::Property::setType(INDI_PROPERTY_TYPE t) { pType = t; } void INDI::Property::setRegistered(bool r) { pRegistered = r; } void INDI::Property::setDynamic(bool d) { pDynamic = d; } void INDI::Property::setBaseDevice(BaseDevice *idp) { dp = idp; } const char *INDI::Property::getName() const { if (pPtr == nullptr) return nullptr; switch (pType) { case INDI_NUMBER: return ((INumberVectorProperty *)pPtr)->name; break; case INDI_TEXT: return ((ITextVectorProperty *)pPtr)->name; break; case INDI_SWITCH: return ((ISwitchVectorProperty *)pPtr)->name; break; case INDI_LIGHT: return ((ILightVectorProperty *)pPtr)->name; break; case INDI_BLOB: return ((IBLOBVectorProperty *)pPtr)->name; break; case INDI_UNKNOWN: break; } return nullptr; } const char *INDI::Property::getLabel() const { if (pPtr == nullptr) return nullptr; switch (pType) { case INDI_NUMBER: return ((INumberVectorProperty *)pPtr)->label; break; case INDI_TEXT: return ((ITextVectorProperty *)pPtr)->label; break; case INDI_SWITCH: return ((ISwitchVectorProperty *)pPtr)->label; break; case INDI_LIGHT: return ((ILightVectorProperty *)pPtr)->label; break; case INDI_BLOB: return ((IBLOBVectorProperty *)pPtr)->label; break; case INDI_UNKNOWN: break; } return nullptr; } const char *INDI::Property::getGroupName() const { if (pPtr == nullptr) return nullptr; switch (pType) { case INDI_NUMBER: return ((INumberVectorProperty *)pPtr)->group; break; case INDI_TEXT: return ((ITextVectorProperty *)pPtr)->group; break; case INDI_SWITCH: return ((ISwitchVectorProperty *)pPtr)->group; break; case INDI_LIGHT: return ((ILightVectorProperty *)pPtr)->group; break; case INDI_BLOB: return ((IBLOBVectorProperty *)pPtr)->group; break; case INDI_UNKNOWN: break; } return nullptr; } const char *INDI::Property::getDeviceName() const { if (pPtr == nullptr) return nullptr; switch (pType) { case INDI_NUMBER: return ((INumberVectorProperty *)pPtr)->device; break; case INDI_TEXT: return ((ITextVectorProperty *)pPtr)->device; break; case INDI_SWITCH: return ((ISwitchVectorProperty *)pPtr)->device; break; case INDI_LIGHT: return ((ILightVectorProperty *)pPtr)->device; break; case INDI_BLOB: return ((IBLOBVectorProperty *)pPtr)->device; break; case INDI_UNKNOWN: break; } return nullptr; } const char *INDI::Property::getTimestamp() const { if (pPtr == nullptr) return nullptr; switch (pType) { case INDI_NUMBER: return ((INumberVectorProperty *)pPtr)->timestamp; break; case INDI_TEXT: return ((ITextVectorProperty *)pPtr)->timestamp; break; case INDI_SWITCH: return ((ISwitchVectorProperty *)pPtr)->timestamp; break; case INDI_LIGHT: return ((ILightVectorProperty *)pPtr)->timestamp; break; case INDI_BLOB: return ((IBLOBVectorProperty *)pPtr)->timestamp; break; case INDI_UNKNOWN: break; } return nullptr; } IPState INDI::Property::getState() const { switch (pType) { case INDI_NUMBER: return ((INumberVectorProperty *)pPtr)->s; break; case INDI_TEXT: return ((ITextVectorProperty *)pPtr)->s; break; case INDI_SWITCH: return ((ISwitchVectorProperty *)pPtr)->s; break; case INDI_LIGHT: return ((ILightVectorProperty *)pPtr)->s; break; case INDI_BLOB: return ((IBLOBVectorProperty *)pPtr)->s; break; default: break; } return IPS_IDLE; } IPerm INDI::Property::getPermission() const { switch (pType) { case INDI_NUMBER: return ((INumberVectorProperty *)pPtr)->p; break; case INDI_TEXT: return ((ITextVectorProperty *)pPtr)->p; break; case INDI_SWITCH: return ((ISwitchVectorProperty *)pPtr)->p; break; case INDI_LIGHT: break; case INDI_BLOB: return ((IBLOBVectorProperty *)pPtr)->p; break; default: break; } return IP_RO; } INumberVectorProperty *INDI::Property::getNumber() { if (pType == INDI_NUMBER) return ((INumberVectorProperty *)pPtr); return nullptr; } ITextVectorProperty *INDI::Property::getText() { if (pType == INDI_TEXT) return ((ITextVectorProperty *)pPtr); return nullptr; } ISwitchVectorProperty *INDI::Property::getSwitch() { if (pType == INDI_SWITCH) return ((ISwitchVectorProperty *)pPtr); return nullptr; } ILightVectorProperty *INDI::Property::getLight() { if (pType == INDI_LIGHT) return ((ILightVectorProperty *)pPtr); return nullptr; } IBLOBVectorProperty *INDI::Property::getBLOB() { if (pType == INDI_BLOB) return ((IBLOBVectorProperty *)pPtr); return nullptr; } libindi/libs/indibase/indiguiderinterface.h0000664000175000017500000001017713263645557020364 0ustar jasemjasem/* Guider Interface Copyright (C) 2011 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 */ #pragma once #include "indibase.h" /** * @class GuiderInterface * @brief Provides interface to implement guider (ST4) port functionality. * * The child class implements GuideXXXX() functions and returns: * IPS_OK if the guide operation is completed in the function, which is usually appropriate for * very short guiding pulses. * IPS_BUSY if the guide operation is in progress and will take time to complete. In this * case, the child class must call GuideComplete() once the guiding pulse is complete. * IPS_ALERT if the guide operation failed. * * \e IMPORTANT: initGuiderProperties() must be called before any other function to initialize * the guider properties. * \e IMPORATNT: processGuiderProperties() must be called in your driver's ISNewNumber(..) * function. processGuiderProperties() will call the guide functions * GuideXXXX functions according to the driver. * * @author Jasem Mutlaq */ namespace INDI { class GuiderInterface { public: /** * @brief Guide north for ms milliseconds. North is defined as DEC+ * @return IPS_OK if operation is completed successfully, IPS_BUSY if operation will take take to * complete, or IPS_ALERT if operation failed. */ virtual IPState GuideNorth(float ms) = 0; /** * @brief Guide south for ms milliseconds. South is defined as DEC- * @return IPS_OK if operation is completed successfully, IPS_BUSY if operation will take take to * complete, or IPS_ALERT if operation failed. */ virtual IPState GuideSouth(float ms) = 0; /** * @brief Guide east for ms milliseconds. East is defined as RA+ * @return IPS_OK if operation is completed successfully, IPS_BUSY if operation will take take to * complete, or IPS_ALERT if operation failed. */ virtual IPState GuideEast(float ms) = 0; /** * @brief Guide west for ms milliseconds. West is defined as RA- * @return IPS_OK if operation is completed successfully, IPS_BUSY if operation will take take to * complete, or IPS_ALERT if operation failed. */ virtual IPState GuideWest(float ms) = 0; /** * @brief Call GuideComplete once the guiding pulse is complete. * @param axis Axis of completed guiding operation. */ virtual void GuideComplete(INDI_EQ_AXIS axis); protected: GuiderInterface(); ~GuiderInterface(); /** * @brief Initilize guider properties. It is recommended to call this function within * initProperties() of your primary device * @param deviceName Name of the primary device * @param groupName Group or tab name to be used to define guider properties. */ void initGuiderProperties(const char *deviceName, const char *groupName); /** * @brief Call this function whenever client updates GuideNSNP or GuideWSP properties in the * primary device. This function then takes care of issuing the corresponding GuideXXXX * function accordingly. * @param name device name * @param values value as passed by the client * @param names names as passed by the client * @param n number of values and names pair to process. */ void processGuiderProperties(const char *name, double values[], char *names[], int n); INumber GuideNSN[2]; INumberVectorProperty GuideNSNP; INumber GuideWEN[2]; INumberVectorProperty GuideWENP; }; } libindi/libs/indibase/indiweather.h0000664000175000017500000002032613263645557016660 0ustar jasemjasem/******************************************************************************* Copyright(c) 2015 Jasem Mutlaq. All rights reserved. INDI Weather Device Class This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program 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 General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. The full GNU General Public License is included in this distribution in the file called LICENSE. *******************************************************************************/ #pragma once #include "defaultdevice.h" #include namespace Connection { class Serial; class TCP; } /** * \class Weather * \brief Class to provide general functionality of a weather device. * * The Weather provides a simple interface for weather devices. Parameters such as temperature, * wind, humidity etc can be added by the child class as supported by the physical device. With each * parameter, the caller specifies the minimum and maximum ranges of OK and WARNING zones. Any value * outside of the warning zone is automatically treated as ALERT. * * The class also specifies the list of critical parameters for observatory operations. When any of * the parameters changes state to WARNING or ALERT, then the overall state of the WEATHER_STATUS * property reflects the worst state of any individual parameter. The WEATHER_STATUS property may be * used by clients to determine whether to proceed with observation tasks or not, and * whether to take any safety measures to protect the observatory from severe weather conditions. * * The child class should start by first adding all the weather parameters via the addParameter() * function, then set all the critial parameters via the setCriticalParameter() function, and finally call * generateParameterRanges() function to generate all the parameter ranges properties. * * Weather update period is controlled by the WEATHER_UPDATE property which stores the update period * in seconds and calls updateWeather() every X seconds as given in the property. * * \e IMPORTANT: GEOGRAPHIC_COORD stores latitude and longitude in INDI specific format, refer to * INDI Standard * Properties for details. * * \author Jasem Mutlaq */ namespace INDI { class Weather : public DefaultDevice { public: enum WeatherLocation { LOCATION_LATITUDE, LOCATION_LONGITUDE, LOCATION_ELEVATION }; /** \struct WeatherConnection * \brief Holds the connection mode of the Weather. */ enum { CONNECTION_NONE = 1 << 0, /** Do not use any connection plugin */ CONNECTION_SERIAL = 1 << 1, /** For regular serial and bluetooth connections */ CONNECTION_TCP = 1 << 2 /** For Wired and WiFI connections */ } WeatherConnection; Weather(); virtual ~Weather(); virtual bool initProperties(); virtual bool updateProperties(); virtual bool ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n); virtual bool ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n); virtual bool ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n); virtual bool ISSnoopDevice(XMLEle *root); protected: /** * @brief updateWeather Update weather conditions from device or service. The function should * not change the state of any property in the device as this is handled by Weather. It * should only update the raw values. * @return Return overall state. The state should be IPS_OK if data is valid. IPS_BUSY if * weather update is in progress. IPS_ALERT is there is an error. The clients will only accept * values with IPS_OK state. */ virtual IPState updateWeather(); /** * @brief TimerHit Keep calling updateWeather() until it is successful, if it fails upon first * connection. */ virtual void TimerHit(); /** \brief Update telescope location settings * \param latitude Site latitude in degrees. * \param longitude Site latitude in degrees increasing eastward from Greenwich (0 to 360). * \param elevation Site elevation in meters. * \return True if successful, false otherwise * \note This function performs no action unless subclassed by the child class if required. */ virtual bool updateLocation(double latitude, double longitude, double elevation); /** * @brief addParameter Add a physical weather measurable parameter to the weather driver. * The weather value has three zones: *
    *
  1. OK: Set minimum and maximum values for acceptable values.
  2. *
  3. Warning: Set minimum and maximum values for values outside of Ok range and in the * dangerous warning zone.
  4. *
  5. Alert: Any value outsize of Ok and Warning zone is marked as Alert.
  6. *
* @param name Name of parameter * @param label Label of paremeter (in GUI) * @param minimumOK Minimum OK value. * @param maximumOK Maximum OK value. * @param minimumWarning Minimum Warning value. * @param maximumWarning Maximum Warning value. */ void addParameter(std::string name, std::string label, double minimumOK, double maximumOK, double minimumWarning, double maximumWarning); /** * @brief setCriticalParameter Set parameter that is considered critical to the operation of the * observatory. The parameter state can affect the overall weather driver state which signals * the client to take appropriate action depending on the severity of the state. * @param param Name of critical parameter. * @return True if critical parameter was set, false if parameter is not found. */ bool setCriticalParameter(std::string param); /** * @brief setParameterValue Update weather parameter value * @param name name of weather parameter * @param value new value of weather parameter; */ void setParameterValue(std::string name, double value); /** * @brief setWeatherConnection Set Weather connection mode. Child class should call this * in the constructor before Weather registers any connection interfaces * @param value ORed combination of WeatherConnection values. */ void setWeatherConnection(const uint8_t &value); /** * @return Get current Weather connection mode */ uint8_t getWeatherConnection() const; virtual bool saveConfigItems(FILE *fp); /** \brief perform handshake with device to check communication */ virtual bool Handshake(); // A number vector that stores lattitude and longitude INumberVectorProperty LocationNP; INumber LocationN[3]; // Refresh data ISwitch RefreshS[1]; ISwitchVectorProperty RefreshSP; // Parameters INumber *ParametersN; INumberVectorProperty ParametersNP; // Parameter Ranges INumberVectorProperty *ParametersRangeNP; uint8_t nRanges; // Weather status ILight *critialParametersL; ILightVectorProperty critialParametersLP; // Active devices to snoop ITextVectorProperty ActiveDeviceTP; IText ActiveDeviceT[1] {}; // Update Period INumber UpdatePeriodN[1]; INumberVectorProperty UpdatePeriodNP; Connection::Serial *serialConnection = NULL; Connection::TCP *tcpConnection = NULL; int PortFD = -1; private: bool processLocationInfo(double latitude, double longitude, double elevation); void createParameterRange(std::string name, std::string label); void updateWeatherState(); int updateTimerID; bool callHandshake(); uint8_t weatherConnection = CONNECTION_SERIAL | CONNECTION_TCP; }; } libindi/libs/indibase/indigps.cpp0000664000175000017500000001414113263645557016343 0ustar jasemjasem/******************************************************************************* Copyright(c) 2015 Jasem Mutlaq. All rights reserved. INDI GPS Device Class This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program 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 General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. The full GNU General Public License is included in this distribution in the file called LICENSE. *******************************************************************************/ #include "indigps.h" #include namespace INDI { bool GPS::initProperties() { DefaultDevice::initProperties(); IUFillNumber(&PeriodN[0], "PERIOD", "Period (s)", "%.f", 0, 3600, 60.0, 0); IUFillNumberVector(&PeriodNP, PeriodN, 1, getDeviceName(), "GPS_REFRESH_PERIOD", "Refresh", MAIN_CONTROL_TAB, IP_RW, 0, IPS_IDLE); IUFillSwitch(&RefreshS[0], "REFRESH", "GPS", ISS_OFF); IUFillSwitchVector(&RefreshSP, RefreshS, 1, getDeviceName(), "GPS_REFRESH", "Refresh", MAIN_CONTROL_TAB, IP_RW, ISR_ATMOST1, 0, IPS_IDLE); IUFillNumber(&LocationN[LOCATION_LATITUDE], "LAT", "Lat (dd:mm:ss)", "%010.6m", -90, 90, 0, 0.0); IUFillNumber(&LocationN[LOCATION_LONGITUDE], "LONG", "Lon (dd:mm:ss)", "%010.6m", 0, 360, 0, 0.0); IUFillNumber(&LocationN[LOCATION_ELEVATION], "ELEV", "Elevation (m)", "%g", -200, 10000, 0, 0); IUFillNumberVector(&LocationNP, LocationN, 3, getDeviceName(), "GEOGRAPHIC_COORD", "Location", MAIN_CONTROL_TAB, IP_RO, 60, IPS_IDLE); IUFillText(&TimeT[0], "UTC", "UTC Time", nullptr); IUFillText(&TimeT[1], "OFFSET", "UTC Offset", nullptr); IUFillTextVector(&TimeTP, TimeT, 2, getDeviceName(), "TIME_UTC", "UTC", MAIN_CONTROL_TAB, IP_RO, 60, IPS_IDLE); setDefaultPollingPeriod(2000); return true; } bool GPS::updateProperties() { DefaultDevice::updateProperties(); if (isConnected()) { // Update GPS and send values to client IPState state = updateGPS(); LocationNP.s = state; defineNumber(&LocationNP); TimeTP.s = state; defineText(&TimeTP); RefreshSP.s = state; defineSwitch(&RefreshSP); defineNumber(&PeriodNP); if (state != IPS_OK) { if (state == IPS_BUSY) DEBUG(Logger::DBG_SESSION, "GPS fix is in progress..."); timerID = SetTimer(POLLMS); } else if (PeriodN[0].value > 0) timerID = SetTimer(PeriodN[0].value); } else { deleteProperty(LocationNP.name); deleteProperty(TimeTP.name); deleteProperty(RefreshSP.name); deleteProperty(PeriodNP.name); if (timerID > 0) { RemoveTimer(timerID); timerID = -1; } } return true; } void GPS::TimerHit() { if (!isConnected()) { timerID = SetTimer(POLLMS); return; } IPState state = updateGPS(); LocationNP.s = state; TimeTP.s = state; RefreshSP.s = state; switch (state) { // Ok case IPS_OK: IDSetNumber(&LocationNP, nullptr); IDSetText(&TimeTP, nullptr); // We got data OK, but if we are required to update once in a while, we'll call it. if (PeriodN[0].value > 0) timerID = SetTimer(PeriodN[0].value*1000); return; break; // GPS fix is in progress or alert case IPS_ALERT: IDSetNumber(&LocationNP, nullptr); IDSetText(&TimeTP, nullptr); break; default: break; } timerID = SetTimer(POLLMS); } IPState GPS::updateGPS() { DEBUG(Logger::DBG_ERROR, "updateGPS() must be implemented in GPS device child class to update TIME_UTC and " "GEOGRAPHIC_COORD properties."); return IPS_ALERT; } bool GPS::ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) { if (dev != nullptr && strcmp(dev, getDeviceName()) == 0) { if (!strcmp(name, RefreshSP.name)) { RefreshS[0].s = ISS_OFF; RefreshSP.s = IPS_OK; IDSetSwitch(&RefreshSP, nullptr); // Manual trigger TimerHit(); } } return DefaultDevice::ISNewSwitch(dev, name, states, names, n); } bool GPS::ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) { if (dev != nullptr && strcmp(dev, getDeviceName()) == 0) { if (strcmp(name, PeriodNP.name) == 0) { double prevPeriod = PeriodN[0].value; IUUpdateNumber(&PeriodNP, values, names, n); // Do not remove timer if GPS update is still in progress if (timerID > 0 && RefreshSP.s != IPS_BUSY) { RemoveTimer(timerID); timerID = -1; } if (PeriodN[0].value == 0) { DEBUG(Logger::DBG_SESSION, "GPS Update Timer disabled."); } else { timerID = SetTimer(PeriodN[0].value*1000); if (prevPeriod == 0) DEBUG(Logger::DBG_SESSION, "GPS Update Timer enabled."); } PeriodNP.s = IPS_OK; IDSetNumber(&PeriodNP, nullptr); return true; } } return DefaultDevice::ISNewNumber(dev, name, values, names, n); } bool GPS::saveConfigItems(FILE *fp) { DefaultDevice::saveConfigItems(fp); IUSaveConfigNumber(fp, &PeriodNP); return true; } } libindi/libs/indibase/indifocuser.cpp0000664000175000017500000002512013263645557017217 0ustar jasemjasem/******************************************************************************* Copyright(c) 2013 Jasem Mutlaq. All rights reserved. This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2 as published by the Free Software Foundation. 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 Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. *******************************************************************************/ #include "indifocuser.h" #include "indicontroller.h" #include "connectionplugins/connectionserial.h" #include "connectionplugins/connectiontcp.h" #include namespace INDI { Focuser::Focuser() : FI(this) { controller = new Controller(this); controller->setButtonCallback(buttonHelper); } Focuser::~Focuser() { delete (controller); } bool Focuser::initProperties() { DefaultDevice::initProperties(); // let the base class flesh in what it wants FI::initProperties(MAIN_CONTROL_TAB); // Presets IUFillNumber(&PresetN[0], "PRESET_1", "Preset 1", "%.f", 0, 100000, 1000, 0); IUFillNumber(&PresetN[1], "PRESET_2", "Preset 2", "%.f", 0, 100000, 1000, 0); IUFillNumber(&PresetN[2], "PRESET_3", "Preset 3", "%.f", 0, 100000, 1000, 0); IUFillNumberVector(&PresetNP, PresetN, 3, getDeviceName(), "Presets", "", "Presets", IP_RW, 0, IPS_IDLE); //Preset GOTO IUFillSwitch(&PresetGotoS[0], "Preset 1", "", ISS_OFF); IUFillSwitch(&PresetGotoS[1], "Preset 2", "", ISS_OFF); IUFillSwitch(&PresetGotoS[2], "Preset 3", "", ISS_OFF); IUFillSwitchVector(&PresetGotoSP, PresetGotoS, 3, getDeviceName(), "Goto", "", "Presets", IP_RW, ISR_1OFMANY, 0, IPS_IDLE); addDebugControl(); addPollPeriodControl(); controller->mapController("Focus In", "Focus In", Controller::CONTROLLER_BUTTON, "BUTTON_1"); controller->mapController("Focus Out", "Focus Out", Controller::CONTROLLER_BUTTON, "BUTTON_2"); controller->mapController("Abort Focus", "Abort Focus", Controller::CONTROLLER_BUTTON, "BUTTON_3"); controller->initProperties(); setDriverInterface(FOCUSER_INTERFACE); if (focuserConnection & CONNECTION_SERIAL) { serialConnection = new Connection::Serial(this); serialConnection->registerHandshake([&]() { return callHandshake(); }); registerConnection(serialConnection); } if (focuserConnection & CONNECTION_TCP) { tcpConnection = new Connection::TCP(this); tcpConnection->registerHandshake([&]() { return callHandshake(); }); registerConnection(tcpConnection); } return true; } void Focuser::ISGetProperties(const char *dev) { // First we let our parent populate DefaultDevice::ISGetProperties(dev); controller->ISGetProperties(dev); return; } bool Focuser::updateProperties() { FI::updateProperties(); if (isConnected()) { if (CanAbsMove()) { defineNumber(&PresetNP); defineSwitch(&PresetGotoSP); } } else { if (CanAbsMove()) { deleteProperty(PresetNP.name); deleteProperty(PresetGotoSP.name); } } controller->updateProperties(); return true; } bool Focuser::ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) { // first check if it's for our device if (dev != nullptr && strcmp(dev, getDeviceName()) == 0) { if (!strcmp(name, PresetNP.name)) { IUUpdateNumber(&PresetNP, values, names, n); PresetNP.s = IPS_OK; IDSetNumber(&PresetNP, nullptr); //saveConfig(); return true; } if (strstr(name, "FOCUS_")) return FI::processNumber(dev, name, values, names, n); } return DefaultDevice::ISNewNumber(dev, name, values, names, n); } bool Focuser::ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) { if (dev != nullptr && strcmp(dev, getDeviceName()) == 0) { if (!strcmp(PresetGotoSP.name, name)) { IUUpdateSwitch(&PresetGotoSP, states, names, n); int index = IUFindOnSwitchIndex(&PresetGotoSP); if (PresetN[index].value < FocusAbsPosN[0].min) { PresetGotoSP.s = IPS_ALERT; IDSetSwitch(&PresetGotoSP, nullptr); DEBUGFDEVICE(dev, Logger::DBG_ERROR, "Requested position out of bound. Focus minimum position is %g", FocusAbsPosN[0].min); return true; } else if (PresetN[index].value > FocusAbsPosN[0].max) { PresetGotoSP.s = IPS_ALERT; IDSetSwitch(&PresetGotoSP, nullptr); DEBUGFDEVICE(dev, Logger::DBG_ERROR, "Requested position out of bound. Focus maximum position is %g", FocusAbsPosN[0].max); return true; } int rc = MoveAbsFocuser(PresetN[index].value); if (rc >= 0) { PresetGotoSP.s = IPS_OK; DEBUGF(Logger::DBG_SESSION, "Moving to Preset %d with position %g.", index + 1, PresetN[index].value); IDSetSwitch(&PresetGotoSP, nullptr); return true; } PresetGotoSP.s = IPS_ALERT; IDSetSwitch(&PresetGotoSP, nullptr); return true; } if (strstr(name, "FOCUS_")) return FI::processSwitch(dev, name, states, names, n); } controller->ISNewSwitch(dev, name, states, names, n); // Nobody has claimed this, so, ignore it return DefaultDevice::ISNewSwitch(dev, name, states, names, n); } bool Focuser::ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n) { controller->ISNewText(dev, name, texts, names, n); return DefaultDevice::ISNewText(dev, name, texts, names, n); } bool Focuser::ISSnoopDevice(XMLEle *root) { controller->ISSnoopDevice(root); return DefaultDevice::ISSnoopDevice(root); } bool Focuser::Handshake() { return false; } bool Focuser::saveConfigItems(FILE *fp) { DefaultDevice::saveConfigItems(fp); IUSaveConfigNumber(fp, &PresetNP); controller->saveConfigItems(fp); return true; } void Focuser::buttonHelper(const char *button_n, ISState state, void *context) { static_cast(context)->processButton(button_n, state); } void Focuser::processButton(const char *button_n, ISState state) { //ignore OFF if (state == ISS_OFF) return; FocusTimerN[0].value = lastTimerValue; IPState rc = IPS_IDLE; // Abort if (!strcmp(button_n, "Abort Focus")) { if (AbortFocuser()) { AbortSP.s = IPS_OK; DEBUG(Logger::DBG_SESSION, "Focuser aborted."); if (CanAbsMove() && FocusAbsPosNP.s != IPS_IDLE) { FocusAbsPosNP.s = IPS_IDLE; IDSetNumber(&FocusAbsPosNP, nullptr); } if (CanRelMove() && FocusRelPosNP.s != IPS_IDLE) { FocusRelPosNP.s = IPS_IDLE; IDSetNumber(&FocusRelPosNP, nullptr); } } else { AbortSP.s = IPS_ALERT; DEBUG(Logger::DBG_ERROR, "Aborting focuser failed."); } IDSetSwitch(&AbortSP, nullptr); } // Focus In else if (!strcmp(button_n, "Focus In")) { if (FocusMotionS[FOCUS_INWARD].s != ISS_ON) { FocusMotionS[FOCUS_INWARD].s = ISS_ON; FocusMotionS[FOCUS_OUTWARD].s = ISS_OFF; IDSetSwitch(&FocusMotionSP, nullptr); } if (HasVariableSpeed()) { rc = MoveFocuser(FOCUS_INWARD, FocusSpeedN[0].value, FocusTimerN[0].value); FocusTimerNP.s = rc; IDSetNumber(&FocusTimerNP, nullptr); } else if (CanRelMove()) { rc = MoveRelFocuser(FOCUS_INWARD, FocusRelPosN[0].value); if (rc == IPS_OK) { FocusRelPosNP.s = IPS_OK; IDSetNumber(&FocusRelPosNP, "Focuser moved %d steps inward", (int)FocusRelPosN[0].value); IDSetNumber(&FocusAbsPosNP, nullptr); } else if (rc == IPS_BUSY) { FocusRelPosNP.s = IPS_BUSY; IDSetNumber(&FocusAbsPosNP, "Focuser is moving %d steps inward...", (int)FocusRelPosN[0].value); } } } else if (!strcmp(button_n, "Focus Out")) { if (FocusMotionS[FOCUS_OUTWARD].s != ISS_ON) { FocusMotionS[FOCUS_INWARD].s = ISS_OFF; FocusMotionS[FOCUS_OUTWARD].s = ISS_ON; IDSetSwitch(&FocusMotionSP, nullptr); } if (HasVariableSpeed()) { rc = MoveFocuser(FOCUS_OUTWARD, FocusSpeedN[0].value, FocusTimerN[0].value); FocusTimerNP.s = rc; IDSetNumber(&FocusTimerNP, nullptr); } else if (CanRelMove()) { rc = MoveRelFocuser(FOCUS_OUTWARD, FocusRelPosN[0].value); if (rc == IPS_OK) { FocusRelPosNP.s = IPS_OK; IDSetNumber(&FocusRelPosNP, "Focuser moved %d steps outward", (int)FocusRelPosN[0].value); IDSetNumber(&FocusAbsPosNP, nullptr); } else if (rc == IPS_BUSY) { FocusRelPosNP.s = IPS_BUSY; IDSetNumber(&FocusAbsPosNP, "Focuser is moving %d steps outward...", (int)FocusRelPosN[0].value); } } } } bool Focuser::callHandshake() { if (focuserConnection > 0) { if (getActiveConnection() == serialConnection) PortFD = serialConnection->getPortFD(); else if (getActiveConnection() == tcpConnection) PortFD = tcpConnection->getPortFD(); } return Handshake(); } void Focuser::setConnection(const uint8_t &value) { uint8_t mask = CONNECTION_SERIAL | CONNECTION_TCP | CONNECTION_NONE; if (value == 0 || (mask & value) == 0) { DEBUGF(Logger::DBG_ERROR, "Invalid connection mode %d", value); return; } focuserConnection = value; } } libindi/libs/indibase/indirotator.h0000664000175000017500000000702513263645557016714 0ustar jasemjasem/******************************************************************************* Copyright(c) 2017 Jasem Mutlaq. All rights reserved. This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2 as published by the Free Software Foundation. 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 Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. *******************************************************************************/ #pragma once #include "defaultdevice.h" #include "indirotatorinterface.h" namespace Connection { class Serial; class TCP; } /** * \class Rotator \brief Class to provide general functionality of a rotator device. Rotators must be able to move to a specific angle. Other capabilities including abort, syncing, homing are optional. The angle is to be interpreted as the raw angle and not necessairly the position angle as this definition should be handled by clients after homing and syncing. This class is designed for pure rotator devices. To utilize Rotator Interface in another type of device, inherit from RotatorInterface. \author Jasem Mutlaq */ namespace INDI { class Rotator : public DefaultDevice, public RotatorInterface { public: Rotator(); virtual ~Rotator(); /** \struct RotatorConnection \brief Holds the connection mode of the Rotator. */ enum { CONNECTION_NONE = 1 << 0, /** Do not use any connection plugin */ CONNECTION_SERIAL = 1 << 1, /** For regular serial and bluetooth connections */ CONNECTION_TCP = 1 << 2 /** For Wired and WiFI connections */ } RotatorConnection; virtual bool initProperties(); virtual void ISGetProperties(const char *dev); virtual bool updateProperties(); virtual bool ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n); virtual bool ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n); /** * @brief setRotatorConnection Set Rotator connection mode. Child class should call this in the constructor before Rotator registers * any connection interfaces * @param value ORed combination of RotatorConnection values. */ void setRotatorConnection(const uint8_t &value); /** * @return Get current Rotator connection mode */ uint8_t getRotatorConnection() const; protected: /** * @brief saveConfigItems Saves the reverse direction property in the configuration file * @param fp pointer to configuration file * @return true if successful, false otherwise. */ virtual bool saveConfigItems(FILE *fp); /** \brief perform handshake with device to check communication */ virtual bool Handshake(); INumber PresetN[3]; INumberVectorProperty PresetNP; ISwitch PresetGotoS[3]; ISwitchVectorProperty PresetGotoSP; Connection::Serial *serialConnection = NULL; Connection::TCP *tcpConnection = NULL; int PortFD = -1; private: bool callHandshake(); uint8_t rotatorConnection = CONNECTION_SERIAL | CONNECTION_TCP; }; } libindi/libs/indibase/hid_libusb.c0000664000175000017500000013656313263645557016467 0ustar jasemjasem/* HIDAPI - Multi-Platform library for communication with HID devices. Copyright (c) 2009 by Alan Ott, Signal 11 Software (8/22/2009) All Rights Reserved. Linux Version - 6/2/2010 Libusb Version - 8/13/2010 FreeBSD Version - 11/1/2011 Changes for use with SX Filter Wheel INDI Driver by CloudMakers - 11/6/2012 This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program 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 General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. The full GNU General Public License is included in this distribution in the file called LICENSE. These files may also be found in the public source code repository located: http://github.com/signal11/hidapi */ #define _GNU_SOURCE // needed for wcsdup() before glibc 2.10 #include "hidapi.h" #include "locale_compat.h" /* GNU / LibUSB */ #include #include "iconv.h" /* C */ #include #include #include #include #include /* Unix */ #include #ifdef __cplusplus extern "C" { #endif #define DEBUG_PRINTF #ifdef DEBUG_PRINTF #define LOG(...) fprintf(stderr, __VA_ARGS__) #else #define LOG(...) \ do \ { \ } while (0) #endif #ifndef __FreeBSD__ #define DETACH_KERNEL_DRIVER #endif /* Uncomment to enable the retrieval of Usage and Usage Page in hid_enumerate(). Warning, on platforms different from FreeBSD this is very invasive as it requires the detach and re-attach of the kernel driver. See comments inside hid_enumerate(). libusb HIDAPI programs are encouraged to use the interface number instead to differentiate between interfaces on a composite HID device. */ // 2018-01-13 JM: It seems that on ARM systems, we need to detach and reattach the kernel driver otherwise we get LIBUSB_ERROR_BUSY // So for now we use INVASIVE_GET_USAGE #ifdef __arm__ #define INVASIVE_GET_USAGE #endif /* Linked List of input reports received from the device. */ struct input_report { uint8_t *data; size_t len; struct input_report *next; }; struct hid_device_ { /* Handle to the actual device. */ libusb_device_handle *device_handle; /* Endpoint information */ int input_endpoint; int output_endpoint; int input_ep_max_packet_size; /* The interface number of the HID */ int interface; /* Indexes of Strings */ int manufacturer_index; int product_index; int serial_index; /* Whether blocking reads are used */ int blocking; /* boolean */ /* Read thread objects */ pthread_t thread; pthread_mutex_t mutex; /* Protects input_reports */ pthread_cond_t condition; pthread_barrier_t barrier; /* Ensures correct startup sequence */ int shutdown_thread; struct libusb_transfer *transfer; /* List of received input reports. */ struct input_report *input_reports; }; static libusb_context *usb_context = NULL; uint16_t get_usb_code_for_current_locale(void); static int return_data(hid_device *dev, unsigned char *data, size_t length); static hid_device *new_hid_device(void) { hid_device *dev = calloc(1, sizeof(hid_device)); dev->blocking = 1; pthread_mutex_init(&dev->mutex, NULL); pthread_cond_init(&dev->condition, NULL); pthread_barrier_init(&dev->barrier, NULL, 2); return dev; } static void free_hid_device(hid_device *dev) { /* Clean up the thread objects */ pthread_barrier_destroy(&dev->barrier); pthread_cond_destroy(&dev->condition); pthread_mutex_destroy(&dev->mutex); /* Free the device itself */ free(dev); } #if 0 //TODO: Implement this funciton on hidapi/libusb.. static void register_error(hid_device * device, const char * op) { } #endif #ifdef INVASIVE_GET_USAGE /* Get bytes from a HID Report Descriptor. Only call with a num_bytes of 0, 1, 2, or 4. */ static uint32_t get_bytes(uint8_t *rpt, size_t len, size_t num_bytes, size_t cur) { /* Return if there aren't enough bytes. */ if (cur + num_bytes >= len) return 0; if (num_bytes == 0) return 0; else if (num_bytes == 1) { return rpt[cur + 1]; } else if (num_bytes == 2) { return (rpt[cur + 2] * 256 + rpt[cur + 1]); } else if (num_bytes == 4) { return (rpt[cur + 4] * 0x01000000 + rpt[cur + 3] * 0x00010000 + rpt[cur + 2] * 0x00000100 + rpt[cur + 1] * 0x00000001); } else return 0; } /* Retrieves the device's Usage Page and Usage from the report descriptor. The algorithm is simple, as it just returns the first Usage and Usage Page that it finds in the descriptor. The return value is 0 on success and -1 on failure. */ static int get_usage(uint8_t *report_descriptor, size_t size, unsigned short *usage_page, unsigned short *usage) { size_t i = 0; int size_code; int data_len, key_size; int usage_found = 0, usage_page_found = 0; while (i < size) { int key = report_descriptor[i]; int key_cmd = key & 0xfc; //printf("key: %02hhx\n", key); if ((key & 0xf0) == 0xf0) { /* This is a Long Item. The next byte contains the length of the data section (value) for this key. See the HID specification, version 1.11, section 6.2.2.3, titled "Long Items." */ if (i + 1 < size) data_len = report_descriptor[i + 1]; else data_len = 0; /* malformed report */ key_size = 3; } else { /* This is a Short Item. The bottom two bits of the key contain the size code for the data section (value) for this key. Refer to the HID specification, version 1.11, section 6.2.2.2, titled "Short Items." */ size_code = key & 0x3; switch (size_code) { case 0: case 1: case 2: data_len = size_code; break; case 3: data_len = 4; break; default: /* Can't ever happen since size_code is & 0x3 */ data_len = 0; break; }; key_size = 1; } if (key_cmd == 0x4) { *usage_page = get_bytes(report_descriptor, size, data_len, i); usage_page_found = 1; //printf("Usage Page: %x\n", (uint32_t)*usage_page); } if (key_cmd == 0x8) { *usage = get_bytes(report_descriptor, size, data_len, i); usage_found = 1; //printf("Usage: %x\n", (uint32_t)*usage); } if (usage_page_found && usage_found) return 0; /* success */ /* Skip over this key and it's associated data */ i += data_len + key_size; } return -1; /* failure */ } #endif // INVASIVE_GET_USAGE #ifdef __FreeBSD__ /* The FreeBSD version of libusb doesn't have this funciton. In mainline libusb, it's inlined in libusb.h. This function will bear a striking resemblence to that one, because there's about one way to code it. Note that the data parameter is Unicode in UTF-16LE encoding. Return value is the number of bytes in data, or LIBUSB_ERROR_*. */ static inline int libusb_get_string_descriptor(libusb_device_handle *dev, uint8_t descriptor_index, uint16_t lang_id, unsigned char *data, int length) { return libusb_control_transfer(dev, LIBUSB_ENDPOINT_IN | 0x0, /* Endpoint 0 IN */ LIBUSB_REQUEST_GET_DESCRIPTOR, (LIBUSB_DT_STRING << 8) | descriptor_index, lang_id, data, (uint16_t)length, 1000); } #endif /* Get the first language the device says it reports. This comes from USB string #0. */ static uint16_t get_first_language(libusb_device_handle *dev) { uint16_t buf[32]; int len; /* Get the string from libusb. */ len = libusb_get_string_descriptor(dev, 0x0, /* String ID */ 0x0, /* Language */ (unsigned char *)buf, sizeof(buf)); if (len < 4) return 0x0; return buf[1]; // First two bytes are len and descriptor type. } static int is_language_supported(libusb_device_handle *dev, uint16_t lang) { uint16_t buf[32]; int len; int i; /* Get the string from libusb. */ len = libusb_get_string_descriptor(dev, 0x0, /* String ID */ 0x0, /* Language */ (unsigned char *)buf, sizeof(buf)); if (len < 4) return 0x0; len /= 2; /* language IDs are two-bytes each. */ /* Start at index 1 because there are two bytes of protocol data. */ for (i = 1; i < len; i++) { if (buf[i] == lang) return 1; } return 0; } /* This function returns a newly allocated wide string containing the USB device string numbered by the index. The returned string must be freed by using free(). */ static wchar_t *get_usb_string(libusb_device_handle *dev, uint8_t idx) { char buf[512]; int len; wchar_t *str = NULL; wchar_t wbuf[256]; /* iconv variables */ iconv_t ic; size_t inbytes; size_t outbytes; size_t res; #ifdef __FreeBSD__ const char *inptr; #else char *inptr; #endif char *outptr; /* Determine which language to use. */ uint16_t lang; lang = get_usb_code_for_current_locale(); if (!is_language_supported(dev, lang)) lang = get_first_language(dev); /* Get the string from libusb. */ len = libusb_get_string_descriptor(dev, idx, lang, (unsigned char *)buf, sizeof(buf)); if (len < 0) return NULL; /* buf does not need to be explicitly NULL-terminated because it is only passed into iconv() which does not need it. */ /* Initialize iconv. */ ic = iconv_open("WCHAR_T", "UTF-16LE"); if (ic == (iconv_t)-1) { LOG("iconv_open() failed\n"); return NULL; } /* Convert to native wchar_t (UTF-32 on glibc/BSD systems). Skip the first character (2-bytes). */ inptr = buf + 2; inbytes = len - 2; outptr = (char *)wbuf; outbytes = sizeof(wbuf); res = iconv(ic, &inptr, &inbytes, &outptr, &outbytes); if (res == (size_t)-1) { LOG("iconv() failed\n"); goto err; } /* Write the terminating NULL. */ wbuf[sizeof(wbuf) / sizeof(wbuf[0]) - 1] = 0x00000000; if (outbytes >= sizeof(wbuf[0])) *((wchar_t *)outptr) = 0x00000000; /* Allocate and copy the string. */ str = wcsdup(wbuf); err: iconv_close(ic); return str; } static char *make_path(libusb_device *dev, int interface_number) { char str[64]; snprintf(str, sizeof(str), "%04x:%04x:%02x", libusb_get_bus_number(dev), libusb_get_device_address(dev), interface_number); str[sizeof(str) - 1] = '\0'; return strdup(str); } int HID_API_EXPORT hid_init(void) { if (!usb_context) { locale_char_t *locale; /* Init Libusb */ if (libusb_init(&usb_context)) return -1; /* Set the locale if it's not set. */ locale = indi_setlocale(LC_CTYPE, NULL); if (!locale) indi_setlocale(LC_CTYPE, INDI_LOCALE("")); } return 0; } int HID_API_EXPORT hid_exit(void) { if (usb_context) { libusb_exit(usb_context); usb_context = NULL; } return 0; } struct hid_device_info HID_API_EXPORT *hid_enumerate(unsigned short vendor_id, unsigned short product_id) { libusb_device **devs; libusb_device *dev; libusb_device_handle *handle; ssize_t num_devs; int i = 0; LOG("Searching for HID Device VID: %#04x PID: %#04x\n", vendor_id, product_id); struct hid_device_info *root = NULL; // return object struct hid_device_info *cur_dev = NULL; hid_init(); num_devs = libusb_get_device_list(usb_context, &devs); if (num_devs < 0) return NULL; while ((dev = devs[i++]) != NULL) { struct libusb_device_descriptor desc; struct libusb_config_descriptor *conf_desc = NULL; int j, k; int interface_num = 0; int res = libusb_get_device_descriptor(dev, &desc); unsigned short dev_vid = desc.idVendor; unsigned short dev_pid = desc.idProduct; /* HID's are defined at the interface level. */ if (desc.bDeviceClass != LIBUSB_CLASS_PER_INTERFACE) continue; res = libusb_get_active_config_descriptor(dev, &conf_desc); if (res < 0) libusb_get_config_descriptor(dev, 0, &conf_desc); if (conf_desc) { for (j = 0; j < conf_desc->bNumInterfaces; j++) { const struct libusb_interface *intf = &conf_desc->interface[j]; for (k = 0; k < intf->num_altsetting; k++) { const struct libusb_interface_descriptor *intf_desc; intf_desc = &intf->altsetting[k]; if (intf_desc->bInterfaceClass == LIBUSB_CLASS_HID) { interface_num = intf_desc->bInterfaceNumber; /* Check the VID/PID against the arguments */ if ((vendor_id == 0x0 && product_id == 0x0) || (vendor_id == dev_vid && product_id == dev_pid)) { struct hid_device_info *tmp; /* VID/PID match. Create the record. */ tmp = calloc(1, sizeof(struct hid_device_info)); if (cur_dev) { cur_dev->next = tmp; } else { root = tmp; } cur_dev = tmp; /* Fill out the record */ cur_dev->next = NULL; cur_dev->path = make_path(dev, interface_num); res = libusb_open(dev, &handle); if (res >= 0) { /* Serial Number */ if (desc.iSerialNumber > 0) cur_dev->serial_number = get_usb_string(handle, desc.iSerialNumber); /* Manufacturer and Product strings */ if (desc.iManufacturer > 0) cur_dev->manufacturer_string = get_usb_string(handle, desc.iManufacturer); if (desc.iProduct > 0) cur_dev->product_string = get_usb_string(handle, desc.iProduct); #ifdef INVASIVE_GET_USAGE /* This section is removed because it is too invasive on the system. Getting a Usage Page and Usage requires parsing the HID Report descriptor. Getting a HID Report descriptor involves claiming the interface. Claiming the interface involves detaching the kernel driver. Detaching the kernel driver is hard on the system because it will unclaim interfaces (if another app has them claimed) and the re-attachment of the driver will sometimes change /dev entry names. It is for these reasons that this section is #if 0. For composite devices, use the interface field in the hid_device_info struct to distinguish between interfaces. */ unsigned char data[256]; #ifdef DETACH_KERNEL_DRIVER int detached = 0; /* Usage Page and Usage */ res = libusb_kernel_driver_active(handle, interface_num); if (res == 1) { res = libusb_detach_kernel_driver(handle, interface_num); if (res < 0) LOG("Couldn't detach kernel driver, even though a kernel driver was attached."); else detached = 1; } #endif res = libusb_claim_interface(handle, interface_num); if (res >= 0) { /* Get the HID Report Descriptor. */ res = libusb_control_transfer( handle, LIBUSB_ENDPOINT_IN | LIBUSB_RECIPIENT_INTERFACE, LIBUSB_REQUEST_GET_DESCRIPTOR, (LIBUSB_DT_REPORT << 8) | interface_num, 0, data, sizeof(data), 5000); if (res >= 0) { unsigned short page = 0, usage = 0; /* Parse the usage and usage page out of the report descriptor. */ get_usage(data, res, &page, &usage); cur_dev->usage_page = page; cur_dev->usage = usage; } else LOG("libusb_control_transfer() for getting the HID report failed with %d\n", res); /* Release the interface */ res = libusb_release_interface(handle, interface_num); if (res < 0) LOG("Can't release the interface.\n"); } else LOG("Can't claim interface %d\n", res); #ifdef DETACH_KERNEL_DRIVER /* Re-attach kernel driver if necessary. */ if (detached) { res = libusb_attach_kernel_driver(handle, interface_num); if (res < 0) LOG("Couldn't re-attach kernel driver.\n"); } #endif #endif // INVASIVE_GET_USAGE libusb_close(handle); } /* VID/PID */ cur_dev->vendor_id = dev_vid; cur_dev->product_id = dev_pid; /* Release Number */ cur_dev->release_number = desc.bcdDevice; /* Interface Number */ cur_dev->interface_number = interface_num; } } } /* altsettings */ } /* interfaces */ libusb_free_config_descriptor(conf_desc); } } libusb_free_device_list(devs, 1); return root; } void HID_API_EXPORT hid_free_enumeration(struct hid_device_info *devs) { struct hid_device_info *d = devs; while (d) { struct hid_device_info *next = d->next; free(d->path); free(d->serial_number); free(d->manufacturer_string); free(d->product_string); free(d); d = next; } } hid_device *hid_open(unsigned short vendor_id, unsigned short product_id, const wchar_t *serial_number) { struct hid_device_info *devs, *cur_dev; const char *path_to_open = NULL; hid_device *handle = NULL; devs = hid_enumerate(vendor_id, product_id); cur_dev = devs; while (cur_dev) { if (cur_dev->vendor_id == vendor_id && cur_dev->product_id == product_id) { if (serial_number) { if (wcscmp(serial_number, cur_dev->serial_number) == 0) { path_to_open = cur_dev->path; break; } } else { path_to_open = cur_dev->path; break; } } cur_dev = cur_dev->next; } if (path_to_open) { /* Open the device */ handle = hid_open_path(path_to_open); } hid_free_enumeration(devs); return handle; } static void read_callback(struct libusb_transfer *transfer) { hid_device *dev = transfer->user_data; int res=0; if (transfer->status == LIBUSB_TRANSFER_COMPLETED) { struct input_report *rpt = malloc(sizeof(*rpt)); rpt->data = malloc(transfer->actual_length); memcpy(rpt->data, transfer->buffer, transfer->actual_length); rpt->len = transfer->actual_length; rpt->next = NULL; pthread_mutex_lock(&dev->mutex); /* Attach the new report object to the end of the list. */ if (dev->input_reports == NULL) { /* The list is empty. Put it at the root. */ dev->input_reports = rpt; pthread_cond_signal(&dev->condition); } else { /* Find the end of the list and attach. */ struct input_report *cur = dev->input_reports; int num_queued = 0; while (cur->next != NULL) { cur = cur->next; num_queued++; } cur->next = rpt; /* Pop one off if we've reached 30 in the queue. This way we don't grow forever if the user never reads anything from the device. */ if (num_queued > 30) { return_data(dev, NULL, 0); } } pthread_mutex_unlock(&dev->mutex); } else if (transfer->status == LIBUSB_TRANSFER_CANCELLED) { dev->shutdown_thread = 1; return; } else if (transfer->status == LIBUSB_TRANSFER_NO_DEVICE) { dev->shutdown_thread = 1; return; } else if (transfer->status == LIBUSB_TRANSFER_TIMED_OUT) { //LOG("Timeout (normal)\n"); } else { LOG("Unknown transfer code: #%d %s\n", transfer->status, libusb_error_name(res)); } /* Re-submit the transfer object. */ res = libusb_submit_transfer(transfer); if (res != 0) { LOG("Unable to submit URB. libusb error code: #%d %s\n", res, libusb_error_name(res)); dev->shutdown_thread = 1; } } static void *read_thread(void *param) { hid_device *dev = param; unsigned char *buf; const size_t length = dev->input_ep_max_packet_size; /* Set up the transfer object. */ buf = malloc(length); dev->transfer = libusb_alloc_transfer(0); libusb_fill_interrupt_transfer(dev->transfer, dev->device_handle, dev->input_endpoint, buf, length, read_callback, dev, 5000 /*timeout*/); /* Make the first submission. Further submissions are made from inside read_callback() */ libusb_submit_transfer(dev->transfer); // Notify the main thread that the read thread is up and running. pthread_barrier_wait(&dev->barrier); /* Handle all the events. */ while (!dev->shutdown_thread) { int res; res = libusb_handle_events(usb_context); if (res < 0) { /* There was an error. */ LOG("read_thread(): libusb reports error #%d %s\n", res, libusb_error_name(res)); /* Break out of this loop only on fatal error.*/ if (res != LIBUSB_ERROR_BUSY && res != LIBUSB_ERROR_TIMEOUT && res != LIBUSB_ERROR_OVERFLOW && res != LIBUSB_ERROR_INTERRUPTED) { break; } } } /* Cancel any transfer that may be pending. This call will fail if no transfers are pending, but that's OK. */ if (libusb_cancel_transfer(dev->transfer) == 0) { /* The transfer was cancelled, so wait for its completion. */ libusb_handle_events(usb_context); } /* Now that the read thread is stopping, Wake any threads which are waiting on data (in hid_read_timeout()). Do this under a mutex to make sure that a thread which is about to go to sleep waiting on the condition acutally will go to sleep before the condition is signaled. */ pthread_mutex_lock(&dev->mutex); pthread_cond_broadcast(&dev->condition); pthread_mutex_unlock(&dev->mutex); /* The dev->transfer->buffer and dev->transfer objects are cleaned up in hid_close(). They are not cleaned up here because this thread could end either due to a disconnect or due to a user call to hid_close(). In both cases the objects can be safely cleaned up after the call to pthread_join() (in hid_close()), but since hid_close() calls libusb_cancel_transfer(), on these objects, they can not be cleaned up here. */ return NULL; } hid_device *HID_API_EXPORT hid_open_path(const char *path) { hid_device *dev = NULL; dev = new_hid_device(); libusb_device **devs; libusb_device *usb_dev; ssize_t num_devs; int res; int d = 0; int good_open = 0; hid_init(); num_devs = libusb_get_device_list(usb_context, &devs); if (num_devs < 0) return NULL; while ((usb_dev = devs[d++]) != NULL) { struct libusb_device_descriptor desc; struct libusb_config_descriptor *conf_desc = NULL; int i, j, k; libusb_get_device_descriptor(usb_dev, &desc); if (libusb_get_active_config_descriptor(usb_dev, &conf_desc) < 0) continue; for (j = 0; j < conf_desc->bNumInterfaces; j++) { const struct libusb_interface *intf = &conf_desc->interface[j]; for (k = 0; k < intf->num_altsetting; k++) { const struct libusb_interface_descriptor *intf_desc; intf_desc = &intf->altsetting[k]; if (intf_desc->bInterfaceClass == LIBUSB_CLASS_HID) { char *dev_path = make_path(usb_dev, intf_desc->bInterfaceNumber); if (!strcmp(dev_path, path)) { /* Matched Paths. Open this device */ // OPEN HERE // res = libusb_open(usb_dev, &dev->device_handle); if (res < 0) { LOG("can't open device: %s\n", libusb_error_name(res)); free(dev_path); break; } good_open = 1; #ifdef DETACH_KERNEL_DRIVER /* Detach the kernel driver, but only if the device is managed by the kernel */ if (libusb_kernel_driver_active(dev->device_handle, intf_desc->bInterfaceNumber) == 1) { res = libusb_detach_kernel_driver(dev->device_handle, intf_desc->bInterfaceNumber); if (res < 0) { libusb_close(dev->device_handle); LOG("Unable to detach Kernel Driver: %s\n", libusb_error_name(res)); free(dev_path); good_open = 0; break; } } #endif res = libusb_claim_interface(dev->device_handle, intf_desc->bInterfaceNumber); if (res < 0) { LOG("can't claim interface %d: %d %s\n", intf_desc->bInterfaceNumber, res, libusb_error_name(res)); free(dev_path); libusb_close(dev->device_handle); good_open = 0; break; } /* Store off the string descriptor indexes */ dev->manufacturer_index = desc.iManufacturer; dev->product_index = desc.iProduct; dev->serial_index = desc.iSerialNumber; /* Store off the interface number */ dev->interface = intf_desc->bInterfaceNumber; /* Find the INPUT and OUTPUT endpoints. An OUTPUT endpoint is not required. */ for (i = 0; i < intf_desc->bNumEndpoints; i++) { const struct libusb_endpoint_descriptor *ep = &intf_desc->endpoint[i]; /* Determine the type and direction of this endpoint. */ int is_interrupt = (ep->bmAttributes & LIBUSB_TRANSFER_TYPE_MASK) == LIBUSB_TRANSFER_TYPE_INTERRUPT; int is_output = (ep->bEndpointAddress & LIBUSB_ENDPOINT_DIR_MASK) == LIBUSB_ENDPOINT_OUT; int is_input = (ep->bEndpointAddress & LIBUSB_ENDPOINT_DIR_MASK) == LIBUSB_ENDPOINT_IN; /* Decide whether to use it for intput or output. */ if (dev->input_endpoint == 0 && is_interrupt && is_input) { /* Use this endpoint for INPUT */ dev->input_endpoint = ep->bEndpointAddress; dev->input_ep_max_packet_size = ep->wMaxPacketSize; } if (dev->output_endpoint == 0 && is_interrupt && is_output) { /* Use this endpoint for OUTPUT */ dev->output_endpoint = ep->bEndpointAddress; } } pthread_create(&dev->thread, NULL, read_thread, dev); // Wait here for the read thread to be initialized. pthread_barrier_wait(&dev->barrier); } free(dev_path); } } } libusb_free_config_descriptor(conf_desc); } libusb_free_device_list(devs, 1); // If we have a good handle, return it. if (good_open) { return dev; } else { // Unable to open any devices. free_hid_device(dev); return NULL; } } int HID_API_EXPORT hid_write(hid_device *dev, const unsigned char *data, size_t length) { int res; int report_number = data[0]; int skipped_report_id = 0; if (report_number == 0x0) { data++; length--; skipped_report_id = 1; } if (dev->output_endpoint <= 0) { /* No interrput out endpoint. Use the Control Endpoint */ res = libusb_control_transfer(dev->device_handle, LIBUSB_REQUEST_TYPE_CLASS | LIBUSB_RECIPIENT_INTERFACE | LIBUSB_ENDPOINT_OUT, 0x09 /*HID Set_Report*/, (2 /*HID output*/ << 8) | report_number, dev->interface, (unsigned char *)data, length, 1000 /*timeout millis*/); if (res < 0) return -1; if (skipped_report_id) length++; return length; } else { /* Use the interrupt out endpoint */ int actual_length; res = libusb_interrupt_transfer(dev->device_handle, dev->output_endpoint, (unsigned char *)data, length, &actual_length, 1000); if (res < 0) return -1; if (skipped_report_id) actual_length++; return actual_length; } } /* Helper function, to simplify hid_read(). This should be called with dev->mutex locked. */ static int return_data(hid_device *dev, unsigned char *data, size_t length) { /* Copy the data out of the linked list item (rpt) into the return buffer (data), and delete the liked list item. */ struct input_report *rpt = dev->input_reports; size_t len = (length < rpt->len) ? length : rpt->len; if (len > 0) memcpy(data, rpt->data, len); dev->input_reports = rpt->next; free(rpt->data); free(rpt); return len; } static void cleanup_mutex(void *param) { hid_device *dev = param; pthread_mutex_unlock(&dev->mutex); } int HID_API_EXPORT hid_read_timeout(hid_device *dev, unsigned char *data, size_t length, int milliseconds) { volatile int bytes_read = 0; #if 0 int transferred; int res = libusb_interrupt_transfer(dev->device_handle, dev->input_endpoint, data, length, &transferred, 5000); LOG("transferred: %d\n", transferred); return transferred; #endif pthread_mutex_lock(&dev->mutex); pthread_cleanup_push(&cleanup_mutex, dev); /* There's an input report queued up. Return it. */ if (dev->input_reports) { /* Return the first one */ bytes_read = return_data(dev, data, length); goto ret; } if (dev->shutdown_thread) { /* This means the device has been disconnected. An error code of -1 should be returned. */ bytes_read = -1; goto ret; } if (milliseconds == -1) { /* Blocking */ while (!dev->input_reports && !dev->shutdown_thread) { pthread_cond_wait(&dev->condition, &dev->mutex); } if (dev->input_reports) { bytes_read = return_data(dev, data, length); } } else if (milliseconds > 0) { /* Non-blocking, but called with timeout. */ int res; struct timespec ts; clock_gettime(CLOCK_REALTIME, &ts); ts.tv_sec += milliseconds / 1000; ts.tv_nsec += (milliseconds % 1000) * 1000000; if (ts.tv_nsec >= 1000000000L) { ts.tv_sec++; ts.tv_nsec -= 1000000000L; } while (!dev->input_reports && !dev->shutdown_thread) { res = pthread_cond_timedwait(&dev->condition, &dev->mutex, &ts); if (res == 0) { if (dev->input_reports) { bytes_read = return_data(dev, data, length); break; } /* If we're here, there was a spurious wake up or the read thread was shutdown. Run the loop again (ie: don't break). */ } else if (res == ETIMEDOUT) { /* Timed out. */ bytes_read = 0; break; } else { /* Error. */ bytes_read = -1; break; } } } else { /* Purely non-blocking */ bytes_read = 0; } ret: pthread_mutex_unlock(&dev->mutex); pthread_cleanup_pop(0); return bytes_read; } int HID_API_EXPORT hid_read(hid_device *dev, unsigned char *data, size_t length) { return hid_read_timeout(dev, data, length, dev->blocking ? -1 : 0); } int HID_API_EXPORT hid_set_nonblocking(hid_device *dev, int nonblock) { dev->blocking = !nonblock; return 0; } int HID_API_EXPORT hid_send_feature_report(hid_device *dev, const unsigned char *data, size_t length) { int res = -1; int skipped_report_id = 0; int report_number = data[0]; if (report_number == 0x0) { data++; length--; skipped_report_id = 1; } res = libusb_control_transfer(dev->device_handle, LIBUSB_REQUEST_TYPE_CLASS | LIBUSB_RECIPIENT_INTERFACE | LIBUSB_ENDPOINT_OUT, 0x09 /*HID set_report*/, (3 /*HID feature*/ << 8) | report_number, dev->interface, (unsigned char *)data, length, 1000 /*timeout millis*/); if (res < 0) return -1; /* Account for the report ID */ if (skipped_report_id) length++; return length; } int HID_API_EXPORT hid_get_feature_report(hid_device *dev, unsigned char *data, size_t length) { int res = -1; int skipped_report_id = 0; int report_number = data[0]; if (report_number == 0x0) { /* Offset the return buffer by 1, so that the report ID will remain in byte 0. */ data++; length--; skipped_report_id = 1; } res = libusb_control_transfer(dev->device_handle, LIBUSB_REQUEST_TYPE_CLASS | LIBUSB_RECIPIENT_INTERFACE | LIBUSB_ENDPOINT_IN, 0x01 /*HID get_report*/, (3 /*HID feature*/ << 8) | report_number, dev->interface, (unsigned char *)data, length, 1000 /*timeout millis*/); if (res < 0) return -1; if (skipped_report_id) res++; return res; } void HID_API_EXPORT hid_close(hid_device *dev) { if (!dev) return; /* Cause read_thread() to stop. */ dev->shutdown_thread = 1; libusb_cancel_transfer(dev->transfer); /* Wait for read_thread() to end. */ pthread_join(dev->thread, NULL); /* Clean up the Transfer objects allocated in read_thread(). */ free(dev->transfer->buffer); libusb_free_transfer(dev->transfer); /* release the interface */ libusb_release_interface(dev->device_handle, dev->interface); /* Close the handle */ libusb_close(dev->device_handle); /* Clear out the queue of received reports. */ pthread_mutex_lock(&dev->mutex); while (dev->input_reports) { return_data(dev, NULL, 0); } pthread_mutex_unlock(&dev->mutex); free_hid_device(dev); } int HID_API_EXPORT_CALL hid_get_manufacturer_string(hid_device *dev, wchar_t *string, size_t maxlen) { return hid_get_indexed_string(dev, dev->manufacturer_index, string, maxlen); } int HID_API_EXPORT_CALL hid_get_product_string(hid_device *dev, wchar_t *string, size_t maxlen) { return hid_get_indexed_string(dev, dev->product_index, string, maxlen); } int HID_API_EXPORT_CALL hid_get_serial_number_string(hid_device *dev, wchar_t *string, size_t maxlen) { return hid_get_indexed_string(dev, dev->serial_index, string, maxlen); } int HID_API_EXPORT_CALL hid_get_indexed_string(hid_device *dev, int string_index, wchar_t *string, size_t maxlen) { wchar_t *str; str = get_usb_string(dev->device_handle, string_index); if (str) { wcsncpy(string, str, maxlen); string[maxlen - 1] = L'\0'; free(str); return 0; } else return -1; } HID_API_EXPORT const wchar_t *HID_API_CALL hid_error(hid_device *dev) { (void)dev; return NULL; } struct lang_map_entry { const char *name; const char *string_code; uint16_t usb_code; }; #define LANG(name, code, usb_code) \ { \ name, code, usb_code \ } static struct lang_map_entry lang_map[] = { LANG("Afrikaans", "af", 0x0436), LANG("Albanian", "sq", 0x041C), LANG("Arabic - United Arab Emirates", "ar_ae", 0x3801), LANG("Arabic - Bahrain", "ar_bh", 0x3C01), LANG("Arabic - Algeria", "ar_dz", 0x1401), LANG("Arabic - Egypt", "ar_eg", 0x0C01), LANG("Arabic - Iraq", "ar_iq", 0x0801), LANG("Arabic - Jordan", "ar_jo", 0x2C01), LANG("Arabic - Kuwait", "ar_kw", 0x3401), LANG("Arabic - Lebanon", "ar_lb", 0x3001), LANG("Arabic - Libya", "ar_ly", 0x1001), LANG("Arabic - Morocco", "ar_ma", 0x1801), LANG("Arabic - Oman", "ar_om", 0x2001), LANG("Arabic - Qatar", "ar_qa", 0x4001), LANG("Arabic - Saudi Arabia", "ar_sa", 0x0401), LANG("Arabic - Syria", "ar_sy", 0x2801), LANG("Arabic - Tunisia", "ar_tn", 0x1C01), LANG("Arabic - Yemen", "ar_ye", 0x2401), LANG("Armenian", "hy", 0x042B), LANG("Azeri - Latin", "az_az", 0x042C), LANG("Azeri - Cyrillic", "az_az", 0x082C), LANG("Basque", "eu", 0x042D), LANG("Belarusian", "be", 0x0423), LANG("Bulgarian", "bg", 0x0402), LANG("Catalan", "ca", 0x0403), LANG("Chinese - China", "zh_cn", 0x0804), LANG("Chinese - Hong Kong SAR", "zh_hk", 0x0C04), LANG("Chinese - Macau SAR", "zh_mo", 0x1404), LANG("Chinese - Singapore", "zh_sg", 0x1004), LANG("Chinese - Taiwan", "zh_tw", 0x0404), LANG("Croatian", "hr", 0x041A), LANG("Czech", "cs", 0x0405), LANG("Danish", "da", 0x0406), LANG("Dutch - Netherlands", "nl_nl", 0x0413), LANG("Dutch - Belgium", "nl_be", 0x0813), LANG("English - Australia", "en_au", 0x0C09), LANG("English - Belize", "en_bz", 0x2809), LANG("English - Canada", "en_ca", 0x1009), LANG("English - Caribbean", "en_cb", 0x2409), LANG("English - Ireland", "en_ie", 0x1809), LANG("English - Jamaica", "en_jm", 0x2009), LANG("English - New Zealand", "en_nz", 0x1409), LANG("English - Phillippines", "en_ph", 0x3409), LANG("English - Southern Africa", "en_za", 0x1C09), LANG("English - Trinidad", "en_tt", 0x2C09), LANG("English - Great Britain", "en_gb", 0x0809), LANG("English - United States", "en_us", 0x0409), LANG("Estonian", "et", 0x0425), LANG("Farsi", "fa", 0x0429), LANG("Finnish", "fi", 0x040B), LANG("Faroese", "fo", 0x0438), LANG("French - France", "fr_fr", 0x040C), LANG("French - Belgium", "fr_be", 0x080C), LANG("French - Canada", "fr_ca", 0x0C0C), LANG("French - Luxembourg", "fr_lu", 0x140C), LANG("French - Switzerland", "fr_ch", 0x100C), LANG("Gaelic - Ireland", "gd_ie", 0x083C), LANG("Gaelic - Scotland", "gd", 0x043C), LANG("German - Germany", "de_de", 0x0407), LANG("German - Austria", "de_at", 0x0C07), LANG("German - Liechtenstein", "de_li", 0x1407), LANG("German - Luxembourg", "de_lu", 0x1007), LANG("German - Switzerland", "de_ch", 0x0807), LANG("Greek", "el", 0x0408), LANG("Hebrew", "he", 0x040D), LANG("Hindi", "hi", 0x0439), LANG("Hungarian", "hu", 0x040E), LANG("Icelandic", "is", 0x040F), LANG("Indonesian", "id", 0x0421), LANG("Italian - Italy", "it_it", 0x0410), LANG("Italian - Switzerland", "it_ch", 0x0810), LANG("Japanese", "ja", 0x0411), LANG("Korean", "ko", 0x0412), LANG("Latvian", "lv", 0x0426), LANG("Lithuanian", "lt", 0x0427), LANG("F.Y.R.O. Macedonia", "mk", 0x042F), LANG("Malay - Malaysia", "ms_my", 0x043E), LANG("Malay - Brunei", "ms_bn", 0x083E), LANG("Maltese", "mt", 0x043A), LANG("Marathi", "mr", 0x044E), LANG("Norwegian - Bokml", "no_no", 0x0414), LANG("Norwegian - Nynorsk", "no_no", 0x0814), LANG("Polish", "pl", 0x0415), LANG("Portuguese - Portugal", "pt_pt", 0x0816), LANG("Portuguese - Brazil", "pt_br", 0x0416), LANG("Raeto-Romance", "rm", 0x0417), LANG("Romanian - Romania", "ro", 0x0418), LANG("Romanian - Republic of Moldova", "ro_mo", 0x0818), LANG("Russian", "ru", 0x0419), LANG("Russian - Republic of Moldova", "ru_mo", 0x0819), LANG("Sanskrit", "sa", 0x044F), LANG("Serbian - Cyrillic", "sr_sp", 0x0C1A), LANG("Serbian - Latin", "sr_sp", 0x081A), LANG("Setsuana", "tn", 0x0432), LANG("Slovenian", "sl", 0x0424), LANG("Slovak", "sk", 0x041B), LANG("Sorbian", "sb", 0x042E), LANG("Spanish - Spain (Traditional)", "es_es", 0x040A), LANG("Spanish - Argentina", "es_ar", 0x2C0A), LANG("Spanish - Bolivia", "es_bo", 0x400A), LANG("Spanish - Chile", "es_cl", 0x340A), LANG("Spanish - Colombia", "es_co", 0x240A), LANG("Spanish - Costa Rica", "es_cr", 0x140A), LANG("Spanish - Dominican Republic", "es_do", 0x1C0A), LANG("Spanish - Ecuador", "es_ec", 0x300A), LANG("Spanish - Guatemala", "es_gt", 0x100A), LANG("Spanish - Honduras", "es_hn", 0x480A), LANG("Spanish - Mexico", "es_mx", 0x080A), LANG("Spanish - Nicaragua", "es_ni", 0x4C0A), LANG("Spanish - Panama", "es_pa", 0x180A), LANG("Spanish - Peru", "es_pe", 0x280A), LANG("Spanish - Puerto Rico", "es_pr", 0x500A), LANG("Spanish - Paraguay", "es_py", 0x3C0A), LANG("Spanish - El Salvador", "es_sv", 0x440A), LANG("Spanish - Uruguay", "es_uy", 0x380A), LANG("Spanish - Venezuela", "es_ve", 0x200A), LANG("Southern Sotho", "st", 0x0430), LANG("Swahili", "sw", 0x0441), LANG("Swedish - Sweden", "sv_se", 0x041D), LANG("Swedish - Finland", "sv_fi", 0x081D), LANG("Tamil", "ta", 0x0449), LANG("Tatar", "tt", 0X0444), LANG("Thai", "th", 0x041E), LANG("Turkish", "tr", 0x041F), LANG("Tsonga", "ts", 0x0431), LANG("Ukrainian", "uk", 0x0422), LANG("Urdu", "ur", 0x0420), LANG("Uzbek - Cyrillic", "uz_uz", 0x0843), LANG("Uzbek - Latin", "uz_uz", 0x0443), LANG("Vietnamese", "vi", 0x042A), LANG("Xhosa", "xh", 0x0434), LANG("Yiddish", "yi", 0x043D), LANG("Zulu", "zu", 0x0435), LANG(NULL, NULL, 0x0), }; uint16_t get_usb_code_for_current_locale(void) { locale_char_t *locale; char search_string[64]; char *ptr; /* Get the current locale. */ locale = indi_setlocale(0, NULL); if (!locale) return 0x0; /* Make a copy of the current locale string. */ strncpy(search_string, locale, sizeof(search_string)); search_string[sizeof(search_string) - 1] = '\0'; /* Chop off the encoding part, and make it lower case. */ ptr = search_string; while (*ptr) { *ptr = tolower(*ptr); if (*ptr == '.') { *ptr = '\0'; break; } ptr++; } /* Find the entry which matches the string code of our locale. */ struct lang_map_entry *lang = lang_map; while (lang->string_code) { if (!strcmp(lang->string_code, search_string)) { return lang->usb_code; } lang++; } /* There was no match. Find with just the language only. */ /* Chop off the variant. Chop it off at the '_'. */ ptr = search_string; while (*ptr) { *ptr = tolower(*ptr); if (*ptr == '_') { *ptr = '\0'; break; } ptr++; } #if 0 // TODO: Do we need this? /* Find the entry which matches the string code of our language. */ lang = lang_map; while (lang->string_code) { if (!strcmp(lang->string_code, search_string)) { return lang->usb_code; } lang++; } #endif /* Found nothing. */ return 0x0; } #ifdef __cplusplus } #endif libindi/libs/indibase/indistandardproperty.cpp0000664000175000017500000000240213263645557021154 0ustar jasemjasem/******************************************************************************* Copyright(c) 2017 Jasem Mutlaq. All rights reserved. List of INDI Stanadrd Properties This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2 as published by the Free Software Foundation. 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 Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. *******************************************************************************/ #include "indistandardproperty.h" namespace INDI { namespace SP { const char *CONNECTION = "CONNECTION"; const char *DEVICE_PORT = "DEVICE_PORT"; const char *DEVICE_AUTO_SEARCH = "DEVICE_AUTO_SEARCH"; const char *DEVICE_BAUD_RATE = "DEVICE_BAUD_RATE"; const char *DEVICE_TCP_ADDRESS = "DEVICE_TCP_ADDRESS"; } // namespace SP } // namespace INDI libindi/libs/indibase/hidtest.cpp0000664000175000017500000001110313263645557016345 0ustar jasemjasem/******************************************************* Windows HID simplification Alan Ott Signal 11 Software 8/22/2009 Copyright 2009 This contents of this file may be used by anyone for any reason without any conditions and may be used as a starting point for your own applications which use HIDAPI. ********************************************************/ #include #include #include #include #include "hidapi.h" // Headers needed for sleeping. #ifdef _WIN32 #include #else #include #endif int main(int argc, char* argv[]) { (void)argc; (void)argv; int res; unsigned char buf[256]; #define MAX_STR 255 wchar_t wstr[MAX_STR]; hid_device *handle; int i; #ifdef WIN32 UNREFERENCED_PARAMETER(argc); UNREFERENCED_PARAMETER(argv); #endif struct hid_device_info *devs, *cur_dev; if (hid_init()) return -1; devs = hid_enumerate(0x0, 0x0); cur_dev = devs; while (cur_dev) { printf("Device Found\n type: %04hx %04hx\n path: %s\n serial_number: %ls", cur_dev->vendor_id, cur_dev->product_id, cur_dev->path, cur_dev->serial_number); printf("\n"); printf(" Manufacturer: %ls\n", cur_dev->manufacturer_string); printf(" Product: %ls\n", cur_dev->product_string); printf(" Release: %hx\n", cur_dev->release_number); printf(" Interface: %d\n", cur_dev->interface_number); printf("\n"); cur_dev = cur_dev->next; } hid_free_enumeration(devs); // Set up the command buffer. memset(buf,0x00,sizeof(buf)); buf[0] = 0x01; buf[1] = 0x81; // Open the device using the VID, PID, // and optionally the Serial number. ////handle = hid_open(0x4d8, 0x3f, L"12345"); handle = hid_open(0x4d8, 0x3f, NULL); if (!handle) { printf("unable to open device\n"); return 1; } // Read the Manufacturer String wstr[0] = 0x0000; res = hid_get_manufacturer_string(handle, wstr, MAX_STR); if (res < 0) printf("Unable to read manufacturer string\n"); printf("Manufacturer String: %ls\n", wstr); // Read the Product String wstr[0] = 0x0000; res = hid_get_product_string(handle, wstr, MAX_STR); if (res < 0) printf("Unable to read product string\n"); printf("Product String: %ls\n", wstr); // Read the Serial Number String wstr[0] = 0x0000; res = hid_get_serial_number_string(handle, wstr, MAX_STR); if (res < 0) printf("Unable to read serial number string\n"); printf("Serial Number String: (%d) %ls", wstr[0], wstr); printf("\n"); // Read Indexed String 1 wstr[0] = 0x0000; res = hid_get_indexed_string(handle, 1, wstr, MAX_STR); if (res < 0) printf("Unable to read indexed string 1\n"); printf("Indexed String 1: %ls\n", wstr); // Set the hid_read() function to be non-blocking. hid_set_nonblocking(handle, 1); // Try to read from the device. There shoud be no // data here, but execution should not block. res = hid_read(handle, buf, 17); // Send a Feature Report to the device buf[0] = 0x2; buf[1] = 0xa0; buf[2] = 0x0a; buf[3] = 0x00; buf[4] = 0x00; res = hid_send_feature_report(handle, buf, 17); if (res < 0) { printf("Unable to send a feature report.\n"); } memset(buf,0,sizeof(buf)); // Read a Feature Report from the device buf[0] = 0x2; res = hid_get_feature_report(handle, buf, sizeof(buf)); if (res < 0) { printf("Unable to get a feature report.\n"); printf("%ls", hid_error(handle)); } else { // Print out the returned buffer. printf("Feature Report\n "); for (i = 0; i < res; i++) printf("%02hhx ", buf[i]); printf("\n"); } memset(buf,0,sizeof(buf)); // Toggle LED (cmd 0x80). The first byte is the report number (0x1). buf[0] = 0x1; buf[1] = 0x80; res = hid_write(handle, buf, 17); if (res < 0) { printf("Unable to write()\n"); printf("Error: %ls\n", hid_error(handle)); } // Request state (cmd 0x81). The first byte is the report number (0x1). buf[0] = 0x1; buf[1] = 0x81; hid_write(handle, buf, 17); if (res < 0) printf("Unable to write() (2)\n"); // Read requested state. hid_read() has been set to be // non-blocking by the call to hid_set_nonblocking() above. // This loop demonstrates the non-blocking nature of hid_read(). res = 0; while (res == 0) { res = hid_read(handle, buf, sizeof(buf)); if (res == 0) printf("waiting...\n"); if (res < 0) printf("Unable to read()\n"); #ifdef WIN32 Sleep(500); #else usleep(500*1000); #endif } printf("Data read:\n "); // Print out the returned buffer. for (i = 0; i < res; i++) printf("%02hhx ", buf[i]); printf("\n"); hid_close(handle); /* Free static HIDAPI objects. */ hid_exit(); #ifdef WIN32 system("pause"); #endif return 0; } libindi/libs/indibase/indidome.h0000664000175000017500000005323513263645557016152 0ustar jasemjasem/******************************************************************************* INDI Dome Base Class Copyright(c) 2014 Jasem Mutlaq. All rights reserved. The code used calculate dome target AZ and ZD is written by Ferran Casarramona, and adapted from code from Markus Wildi. The transformations are based on the paper Matrix Method for Coodinates Transformation written by Toshimi Taki (http://www.asahi-net.or.jp/~zs3t-tk). This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2 as published by the Free Software Foundation. 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 Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. *******************************************************************************/ #pragma once #include "defaultdevice.h" #include #include // Defines a point in a 3 dimension space typedef struct { double x, y, z; } point3D; namespace Connection { class Serial; class TCP; } /** * \class INDI::Dome \brief Class to provide general functionality of a Dome device. Both relative and absolute position domes are supported. Furthermore, if no position feedback is available from the dome, an open-loop control is possible with simple direction commands (Clockwise and counter clockwise). Before using any of the dome functions, you must define the capabilities of the dome by calling SetDomeCapability() function. All positions are represented as degrees of azimuth. Relative motion is specified in degrees as either positive (clock wise direction), or negative (counter clock-wise direction). Slaving is used to synchronizes the dome's azimuth position with that of the mount. The mount's coordinates are snooped from the active mount that has its name specified in ACTIVE_TELESCOPE property in the ACTIVE_DEVICES vector. Dome motion begins when it receives TARGET_EOD_COORD property from the mount driver when the mount starts slewing to the desired target coordinates /em OR when the mount's current tracking position exceeds the AutoSync threshold. Therefore, slaving is performed while slewing and tracking. The user is required to fill in all required parameters before slaving can be used. The AutoSync threshold is the difference in degrees between the dome's azimuth angle and the mount's azimuth angle that should trigger a dome motion. By default, it is set to 0.5 degrees which would trigger dome motion due to any difference between the dome and mount azimuth angles that exceeds 0.5 degrees. For example, if the threshold is set to 5 degrees, the dome will only start moving to sync with the mount's azimuth angle once the difference in azimuth angles is equal or exceeds 5 degrees. Custom parking position is available for absolute/relative position domes. For roll-off observatories, parking state reflects whether the roof is closed or open. Developers need to subclass INDI::Dome to implement any driver for Domes within INDI. \note The code used calculate dome target AZ and ZD is written by Ferran Casarramona, and adapted from code from Markus Wildi. The transformations are based on the paper Matrix Method for Coodinates Transformation written by Toshimi Taki (http://www.asahi-net.or.jp/~zs3t-tk). \author Jasem Mutlaq */ namespace INDI { class Dome : public DefaultDevice { public: /** \typedef DomeMeasurements \brief Measurements necessary for dome-slit synchronization. All values are in meters. The displacements are measured from the true dome centre, and the dome is assumed spherical. \note: The mount centre is the point where RA and Dec. axis crosses, no matter the kind of mount. For example, for a fork mount this displacement is typically 0 if it's perfectly centred with RA axis. */ typedef enum { DM_DOME_RADIUS, /*!< Dome RADIUS */ DM_SHUTTER_WIDTH, /*!< Shutter width */ DM_NORTH_DISPLACEMENT, /*!< Displacement to north of the mount center */ DM_EAST_DISPLACEMENT, /*!< Displacement to east of the mount center */ DM_UP_DISPLACEMENT, /*!< Up Displacement of the mount center */ DM_OTA_OFFSET /*!< Distance from the optical axis to the mount center*/ } DomeMeasurements; enum DomeDirection { DOME_CW, DOME_CCW }; enum DomeMotionCommand { MOTION_START, MOTION_STOP }; /*! Dome Parking data type enum */ enum DomeParkData { PARK_NONE, /*!< Open loop Parking */ PARK_AZ, /*!< Parking via azimuth angle control */ PARK_AZ_ENCODER, /*!< Parking via azimuth encoder control */ }; /** \typedef ShutterOperation \brief Shutter operation command. */ typedef enum { SHUTTER_OPEN, /*!< Open Shutter */ SHUTTER_CLOSE /*!< Close Shutter */ } ShutterOperation; /** \typedef DomeState \brief Dome status */ typedef enum { DOME_IDLE, /*!< Dome is idle */ DOME_MOVING, /*!< Dome is in motion */ DOME_SYNCED, /*!< Dome is synced */ DOME_PARKING, /*!< Dome is parking */ DOME_UNPARKING, /*!< Dome is unparking */ DOME_PARKED, /*!< Dome is parked */ DOME_UNPARKED, /*!< Dome is unparked */ } DomeState; /** \typedef ShutterStatus \brief Shutter Status */ typedef enum { SHUTTER_OPENED, /*!< Shutter is open */ SHUTTER_CLOSED, /*!< Shutter is closed */ SHUTTER_MOVING, /*!< Shutter is in motion */ SHUTTER_UNKNOWN /*!< Shutter status is unknown */ } ShutterStatus; enum { DOME_CAN_ABORT = 1 << 0, /*!< Can the dome motion be aborted? */ DOME_CAN_ABS_MOVE = 1 << 1, /*!< Can the dome move to an absolute azimuth position? */ DOME_CAN_REL_MOVE = 1 << 2, /*!< Can the dome move to a relative position a number of degrees away from current position? Positive degress is Clockwise direction. Negative Degrees is counter clock wise direction */ DOME_CAN_PARK = 1 << 3, /*!< Can the dome park and unpark itself? */ DOME_HAS_SHUTTER = 1 << 4, /*!< Does the dome has a shutter than can be opened and closed electronically? */ DOME_HAS_VARIABLE_SPEED = 1 << 5 /*!< Can the dome move in different configurable speeds? */ }; /** \struct DomeConnection \brief Holds the connection mode of the Dome. */ enum { CONNECTION_NONE = 1 << 0, /** Do not use any connection plugin */ CONNECTION_SERIAL = 1 << 1, /** For regular serial and bluetooth connections */ CONNECTION_TCP = 1 << 2 /** For Wired and WiFI connections */ } DomeConnection; Dome(); virtual ~Dome(); virtual bool initProperties(); virtual void ISGetProperties(const char *dev); virtual bool updateProperties(); virtual bool ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n); virtual bool ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n); virtual bool ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n); virtual bool ISSnoopDevice(XMLEle *root); static void buttonHelper(const char *button_n, ISState state, void *context); /** * @brief setDomeConnection Set Dome connection mode. Child class should call this in the constructor before Dome registers * any connection interfaces * @param value ORed combination of DomeConnection values. */ void setDomeConnection(const uint8_t &value); /** * @return Get current Dome connection mode */ uint8_t getDomeConnection() const; /** * @brief GetDomeCapability returns the capability of the dome */ uint32_t GetDomeCapability() const { return capability; } /** * @brief SetDomeCapability set the dome capabilities. All capabilities must be initialized. * @param cap pointer to dome capability */ void SetDomeCapability(uint32_t cap); /** * @return True if dome support aborting motion */ bool CanAbort() { return capability & DOME_CAN_ABORT; } /** * @return True if dome has absolute postion encoders. */ bool CanAbsMove() { return capability & DOME_CAN_ABS_MOVE; } /** * @return True if dome has relative position encoders. */ bool CanRelMove() { return capability & DOME_CAN_REL_MOVE; } /** * @return True if dome can park. */ bool CanPark() { return capability & DOME_CAN_PARK; } /** * @return True if dome has controllable shutter door */ bool HasShutter() { return capability & DOME_HAS_SHUTTER; } /** * @return True if dome support multiple speeds */ bool HasVariableSpeed() { return capability & DOME_HAS_VARIABLE_SPEED; } /** * @brief isLocked, is the dome currently locked? * @return True if lock status equals true, and TelescopeClosedLockTP is Telescope Locks. */ bool isLocked(); DomeState getDomeState() const; void setDomeState(const DomeState &value); IPState getWeatherState() const; IPState getMountState() const; protected: /** * @brief SetSpeed Set Dome speed. This does not initiate motion, it sets the speed for the next motion command. If motion is in progress, then change speed accordingly. * @param rpm Dome speed (RPM) * @return true if successful, false otherwise */ virtual bool SetSpeed(double rpm); /** \brief Move the Dome in a particular direction. \param dir Direction of Dome, either DOME_CW or DOME_CCW. \return Return IPS_OK if dome operation is complete. IPS_BUSY if operation is in progress. IPS_ALERT on error. */ virtual IPState Move(DomeDirection dir, DomeMotionCommand operation); /** \brief Move the Dome to an absolute azimuth. \param az The new position of the Dome. \return Return IPS_OK if motion is completed and Dome reached requested position. Return IPS_BUSY if Dome started motion to requested position and is in progress. Return IPS_ALERT if there is an error. */ virtual IPState MoveAbs(double az); /** \brief Move the Dome to an relative position. \param azDiff The relative azimuth angle to move. Positive degree is clock-wise direction. Negative degrees is counter clock-wise direction. \return Return IPS_OK if motion is completed and Dome reached requested position. Return IPS_BUSY if Dome started motion to requested position and is in progress. Return IPS_ALERT if there is an error. */ virtual IPState MoveRel(double azDiff); /** * \brief Abort all dome motion * \return True if abort is successful, false otherwise. */ virtual bool Abort(); /** * \brief Goto Park Position. The park position is an absolute azimuth value. * \return Return IPS_OK if motion is completed and Dome reached park position. Return IPS_BUSY if Dome started motion to park requested position and is in progress. Return -IPS_ALERT if there is an error. */ virtual IPState Park(); /** * \brief UnPark dome. The action of the Unpark command is dome specific, but it may include opening the shutter and moving to home position. When UnPark() is successful * The observatory should be in a ready state to utilize the mount to perform observations. * \return Return IPS_OK if motion is completed and Dome is unparked. Return IPS_BUSY if Dome unparking is in progress. Return -IPS_ALERT if there is an error. */ virtual IPState UnPark(); /** * \brief Open or Close shutter * \param operation Either open or close the shutter. * \return Return IPS_OK if shutter operation is complete. Return IPS_BUSY if shutter operation is in progress. Return IPS_ALERT if there is an error. */ virtual IPState ControlShutter(ShutterOperation operation); /** * @brief getShutterStatusString * @param status Status of shutter * @return Returns string representation of the shutter status */ const char *GetShutterStatusString(ShutterStatus status); /** * \brief setParkDataType Sets the type of parking data stored in the park data file and presented to the user. * \param type parking data type. If PARK_NONE then no properties will be presented to the user for custom parking position. */ void SetParkDataType(DomeParkData type); /** * @brief InitPark Loads parking data (stored in ~/.indi/ParkData.xml) that contains parking status * and parking position. InitPark() should be called after successful connection to the dome on startup. * @return True if loading is successful and data is read, false otherwise. On success, you must call * SetAzParkDefault() to set the default parking values. On failure, you must call * SetAzParkDefault() to set the default parking values in addition to SetAzPark() * to set the current parking position. */ bool InitPark(); /** * @brief isParked is dome currently parked? * @return True if parked, false otherwise. */ bool isParked(); /** * @brief SetParked Change the mount parking status. The data park file (stored in ~/.indi/ParkData.xml) is updated in the process. * @param isparked set to true if parked, false otherwise. */ void SetParked(bool isparked); /** * @return Get current AZ parking position. */ double GetAxis1Park(); /** * @return Get default AZ parking position. */ double GetAxis1ParkDefault(); /** * @brief SetRAPark Set current AZ parking position. The data park file (stored in ~/.indi/ParkData.xml) is updated in the process. * @param value current Axis 1 value (AZ either in angles or encoder values as specificed by the DomeParkData type). */ void SetAxis1Park(double value); /** * @brief SetAxis1Park Set default AZ parking position. * @param value Default Axis 1 value (AZ either in angles or encoder values as specificed by the DomeParkData type). */ void SetAxis1ParkDefault(double steps); /** * @brief SetCurrentPark Set current coordinates/encoders value as the desired parking position * \note This function performs no action unless subclassed by the child class if required. */ virtual bool SetCurrentPark(); /** * @brief SetDefaultPark Set default coordinates/encoders value as the desired parking position * \note This function performs no action unless subclassed by the child class if required. */ virtual bool SetDefaultPark(); //Park char *LoadParkData(); bool WriteParkData(); /** * @brief GetTargetAz * @param Az Returns Azimuth required to the dome in order to center the shutter aperture with telescope * @param Alt * @param minAz Returns Minimum azimuth in order to avoid any dome interference to the full aperture of the telescope * @param maxAz Returns Maximum azimuth in order to avoid any dome interference to the full aperture of the telescope * @return Returns false if it can't solve it due bad geometry of the observatory */ bool GetTargetAz(double &Az, double &Alt, double &minAz, double &maxAz); /** * @brief Intersection Calculate the intersection of a ray and a sphere. The line segment is defined from p1 to p2. The sphere is of radius r and centered at (0,0,0). * From http://local.wasp.uwa.edu.au/~pbourke/geometry/sphereline/ * There are potentially two points of intersection given by * p := p1 + mu1 (p2 - p1) * p := p1 + mu2 (p2 - p1) * @param p1 First point * @param p2 Direction of the ray * @param r RADIUS of sphere * @param mu1 First point of potentional intersection. * @param mu2 Second point of potentional intersection. * @return Returns FALSE if the ray doesn't intersect the sphere. */ bool Intersection(point3D p1, point3D p2, double r, double &mu1, double &mu2); /** * @brief OpticalCenter This function calculates the distance from the optical axis to the Dome center * @param MountCenter Distance from the Dome center to the point where mount axis crosses * @param dOpticalAxis Distance from the mount center to the optical axis. * @param Lat Latitude * @param Ah Hour Angle (in hours) * @param OP a 3D point from the optical center to the Dome center. * @return false in case of error. */ bool OpticalCenter(point3D MountCenter, double dOpticalAxis, double Lat, double Ah, point3D &OP); /** * @brief OpticalVector This function calculates a second point for determining the optical axis * @param Az Azimuth * @param Alt Altitude * @param OV a 3D point that determines the optical line. * @return false in case of error. */ bool OpticalVector(double Az, double Alt, point3D &OV); /** * @brief CheckHorizon Returns true if telescope points above horizon. * @param HA Hour angle * @param dec Declination * @param lat observer's latitude * @return True if telescope points above horizon, false otherwise. */ bool CheckHorizon(double HA, double dec, double lat); /** * @brief saveConfigItems Saves the Device Port and Dome Presets in the configuration file * @param fp pointer to configuration file * @return true if successful, false otherwise. */ virtual bool saveConfigItems(FILE *fp); /** * @brief updateCoords updates the horizontal coordinates (Az & Alt) of the mount from the snooped RA, DEC and observer's location. */ void UpdateMountCoords(); /** * @brief UpdateAutoSync This function calculates the target dome azimuth from the mount's target coordinates given the dome parameters. * If the difference between the dome's and mount's azimuth angles exceeds the AutoSync threshold, the dome will be commanded to sync to the mount azimuth position. */ virtual void UpdateAutoSync(); /** \brief perform handshake with device to check communication */ virtual bool Handshake(); double Csc(double x); double Sec(double x); INumberVectorProperty DomeSpeedNP; INumber DomeSpeedN[1]; ISwitchVectorProperty DomeMotionSP; ISwitch DomeMotionS[2]; INumberVectorProperty DomeAbsPosNP; INumber DomeAbsPosN[1]; INumberVectorProperty DomeRelPosNP; INumber DomeRelPosN[1]; ISwitchVectorProperty AbortSP; ISwitch AbortS[1]; INumberVectorProperty DomeParamNP; INumber DomeParamN[1]; ISwitchVectorProperty DomeShutterSP; ISwitch DomeShutterS[2]; ISwitchVectorProperty ParkSP; ISwitch ParkS[2]; INumber ParkPositionN[1]; INumberVectorProperty ParkPositionNP; ISwitch ParkOptionS[3]; ISwitchVectorProperty ParkOptionSP; ISwitch AutoParkS[2]; ISwitchVectorProperty AutoParkSP; uint32_t capability; ShutterStatus shutterState; DomeParkData parkDataType; ITextVectorProperty ActiveDeviceTP; IText ActiveDeviceT[2] {}; // Switch to lock id mount is unparked ISwitchVectorProperty TelescopeClosedLockTP; ISwitch TelescopeClosedLockT[2]; INumber PresetN[3]; INumberVectorProperty PresetNP; ISwitch PresetGotoS[3]; ISwitchVectorProperty PresetGotoSP; INumber DomeMeasurementsN[6]; INumberVectorProperty DomeMeasurementsNP; ISwitchVectorProperty OTASideSP; ISwitch OTASideS[2]; ISwitchVectorProperty DomeAutoSyncSP; ISwitch DomeAutoSyncS[2]; double prev_az, prev_alt, prev_ra, prev_dec; // For Serial and TCP connections int PortFD = -1; Connection::Serial *serialConnection = NULL; Connection::TCP *tcpConnection = NULL; // States DomeState domeState; IPState mountState; IPState weatherState; // Observer geographic coords. Snooped from mount driver. struct ln_lnlat_posn observer; // Do we have valid geographic coords from mount driver? bool HaveLatLong = false; // Mount horizontal and equatorial coords. Snoops from mount driver. struct ln_hrz_posn mountHoriztonalCoords; struct ln_equ_posn mountEquatorialCoords; // Do we have valid coords from mount driver? bool HaveRaDec = false; private: void processButton(const char *button_n, ISState state); void triggerSnoop(const char *driverName, const char *propertyName); Controller *controller = nullptr; bool IsParked = false; bool IsMountParked = false; bool IsLocked = true; const char *ParkDeviceName; const char *Parkdatafile; XMLEle *ParkdataXmlRoot, *ParkdeviceXml, *ParkstatusXml, *ParkpositionXml, *ParkpositionAxis1Xml; double Axis1ParkPosition; double Axis1DefaultParkPosition; bool callHandshake(); uint8_t domeConnection = CONNECTION_SERIAL | CONNECTION_TCP; }; } libindi/libs/indibase/indiccd.cpp0000664000175000017500000032550313263645557016312 0ustar jasemjasem/******************************************************************************* Copyright(c) 2010-2018 Jasem Mutlaq. All rights reserved. Copyright(c) 2010, 2011 Gerry Rozema. All rights reserved. Rapid Guide support added by CloudMakers, s. r. o. Copyright(c) 2013 CloudMakers, s. r. o. All rights reserved. Star detection algorithm is based on PHD Guiding by Craig Stark Copyright (c) 2006-2010 Craig Stark. All rights reserved. This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2 as published by the Free Software Foundation. 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 Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. *******************************************************************************/ #include "indiccd.h" #include "indicom.h" #include "stream/streammanager.h" #include "locale_compat.h" #include #include #include #include #include #include #include #include #include #include #include #include #include const char *IMAGE_SETTINGS_TAB = "Image Settings"; const char *IMAGE_INFO_TAB = "Image Info"; const char *GUIDE_HEAD_TAB = "Guider Head"; const char *GUIDE_CONTROL_TAB = "Guider Control"; const char *RAPIDGUIDE_TAB = "Rapid Guide"; const char *WCS_TAB = "WCS"; // Create dir recursively static int _ccd_mkdir(const char *dir, mode_t mode) { char tmp[PATH_MAX]; char *p = nullptr; size_t len; snprintf(tmp, sizeof(tmp), "%s", dir); len = strlen(tmp); if (tmp[len - 1] == '/') tmp[len - 1] = 0; for (p = tmp + 1; *p; p++) if (*p == '/') { *p = 0; if (mkdir(tmp, mode) == -1 && errno != EEXIST) return -1; *p = '/'; } if (mkdir(tmp, mode) == -1 && errno != EEXIST) return -1; return 0; } namespace INDI { CCDChip::CCDChip() { SendCompressed = false; Interlaced = false; SubX = SubY = 0; SubW = SubH = 1; BPP = 8; BinX = BinY = 1; NAxis = 2; BinFrame = nullptr; strncpy(imageExtention, "fits", MAXINDIBLOBFMT); FrameType = LIGHT_FRAME; lastRapidX = lastRapidY = -1; } CCDChip::~CCDChip() { delete [] RawFrame; delete[] BinFrame; } void CCDChip::setFrameType(CCD_FRAME type) { FrameType = type; } void CCDChip::setResolution(int x, int y) { XRes = x; YRes = y; ImagePixelSizeN[0].value = x; ImagePixelSizeN[1].value = y; IDSetNumber(&ImagePixelSizeNP, nullptr); ImageFrameN[FRAME_X].min = 0; ImageFrameN[FRAME_X].max = x - 1; ImageFrameN[FRAME_Y].min = 0; ImageFrameN[FRAME_Y].max = y - 1; ImageFrameN[FRAME_W].min = 1; ImageFrameN[FRAME_W].max = x; ImageFrameN[FRAME_H].max = 1; ImageFrameN[FRAME_H].max = y; IUUpdateMinMax(&ImageFrameNP); } void CCDChip::setFrame(int subx, int suby, int subw, int subh) { SubX = subx; SubY = suby; SubW = subw; SubH = subh; ImageFrameN[FRAME_X].value = SubX; ImageFrameN[FRAME_Y].value = SubY; ImageFrameN[FRAME_W].value = SubW; ImageFrameN[FRAME_H].value = SubH; IDSetNumber(&ImageFrameNP, nullptr); } void CCDChip::setBin(int hor, int ver) { BinX = hor; BinY = ver; ImageBinN[BIN_W].value = BinX; ImageBinN[BIN_H].value = BinY; IDSetNumber(&ImageBinNP, nullptr); } void CCDChip::setMinMaxStep(const char *property, const char *element, double min, double max, double step, bool sendToClient) { INumberVectorProperty *nvp = nullptr; if (!strcmp(property, ImageExposureNP.name)) nvp = &ImageExposureNP; else if (!strcmp(property, ImageFrameNP.name)) nvp = &ImageFrameNP; else if (!strcmp(property, ImageBinNP.name)) nvp = &ImageBinNP; else if (!strcmp(property, ImagePixelSizeNP.name)) nvp = &ImagePixelSizeNP; else if (!strcmp(property, RapidGuideDataNP.name)) nvp = &RapidGuideDataNP; INumber *np = IUFindNumber(nvp, element); if (np) { np->min = min; np->max = max; np->step = step; if (sendToClient) IUUpdateMinMax(nvp); } } void CCDChip::setPixelSize(float x, float y) { PixelSizex = x; PixelSizey = y; ImagePixelSizeN[2].value = x; ImagePixelSizeN[3].value = x; ImagePixelSizeN[4].value = y; IDSetNumber(&ImagePixelSizeNP, nullptr); } void CCDChip::setBPP(int bbp) { BPP = bbp; ImagePixelSizeN[5].value = BPP; IDSetNumber(&ImagePixelSizeNP, nullptr); } void CCDChip::setFrameBufferSize(int nbuf, bool allocMem) { if (nbuf == RawFrameSize) return; RawFrameSize = nbuf; if (allocMem == false) return; delete [] RawFrame; RawFrame = new uint8_t[nbuf]; if (BinFrame) { delete [] BinFrame; BinFrame = new uint8_t[nbuf]; } } void CCDChip::setExposureLeft(double duration) { ImageExposureNP.s = IPS_BUSY; ImageExposureN[0].value = duration; IDSetNumber(&ImageExposureNP, nullptr); } void CCDChip::setExposureDuration(double duration) { exposureDuration = duration; gettimeofday(&startExposureTime, nullptr); } const char *CCDChip::getFrameTypeName(CCD_FRAME fType) { return FrameTypeS[fType].name; } const char *CCDChip::getExposureStartTime() { static char ts[32]; char iso8601[32]; struct tm *tp; time_t t = (time_t)startExposureTime.tv_sec; int u = startExposureTime.tv_usec / 1000.0; tp = gmtime(&t); strftime(iso8601, sizeof(iso8601), "%Y-%m-%dT%H:%M:%S", tp); snprintf(ts, 32, "%s.%03d", iso8601, u); return (ts); } void CCDChip::setInterlaced(bool intr) { Interlaced = intr; } void CCDChip::setExposureFailed() { ImageExposureNP.s = IPS_ALERT; IDSetNumber(&ImageExposureNP, nullptr); } int CCDChip::getNAxis() const { return NAxis; } void CCDChip::setNAxis(int value) { NAxis = value; } void CCDChip::setImageExtension(const char *ext) { strncpy(imageExtention, ext, MAXINDIBLOBFMT); } void CCDChip::binFrame() { if (BinX == 1) return; // Jasem: Keep full frame shadow in memory to enhance performance and just swap frame pointers after operation is complete if (BinFrame == nullptr) BinFrame = new uint8_t[RawFrameSize]; memset(BinFrame, 0, RawFrameSize); switch (getBPP()) { case 8: { uint8_t *bin_buf = BinFrame; // Try to average pixels since in 8bit they get saturated pretty quickly double factor = (BinX * BinX) / 2; double accumulator = 0; for (int i = 0; i < SubH; i += BinX) for (int j = 0; j < SubW; j += BinX) { accumulator = 0; for (int k = 0; k < BinX; k++) { for (int l = 0; l < BinX; l++) { accumulator += *(RawFrame + j + (i + k) * SubW + l); } } accumulator /= factor; if (accumulator > UINT8_MAX) *bin_buf = UINT8_MAX; else *bin_buf += static_cast(accumulator); bin_buf++; } } break; case 16: { uint16_t *bin_buf = reinterpret_cast(BinFrame); uint16_t *RawFrame16 = reinterpret_cast(RawFrame); uint16_t val; for (int i = 0; i < SubH; i += BinX) for (int j = 0; j < SubW; j += BinX) { for (int k = 0; k < BinX; k++) { for (int l = 0; l < BinX; l++) { val = *(RawFrame16 + j + (i + k) * SubW + l); if (val + *bin_buf > UINT16_MAX) *bin_buf = UINT16_MAX; else *bin_buf += val; } } bin_buf++; } } break; default: return; } // Swap frame pointers uint8_t *rawFramePointer = RawFrame; RawFrame = BinFrame; // We just memset it next time we use it BinFrame = rawFramePointer; } CCD::CCD() { //ctor capability = 0; InExposure = false; InGuideExposure = false; RapidGuideEnabled = false; GuiderRapidGuideEnabled = false; ValidCCDRotation = false; AutoLoop = false; SendImage = false; ShowMarker = false; GuiderAutoLoop = false; GuiderSendImage = false; GuiderShowMarker = false; ExposureTime = 0.0; GuiderExposureTime = 0.0; CurrentFilterSlot = -1; RA = std::numeric_limits::quiet_NaN(); Dec = std::numeric_limits::quiet_NaN(); J2000RA = std::numeric_limits::quiet_NaN(); J2000DE = std::numeric_limits::quiet_NaN(); MPSAS = std::numeric_limits::quiet_NaN(); RotatorAngle = std::numeric_limits::quiet_NaN(); Airmass = std::numeric_limits::quiet_NaN(); Latitude = std::numeric_limits::quiet_NaN(); Longitude = std::numeric_limits::quiet_NaN(); primaryAperture = primaryFocalLength = guiderAperture = guiderFocalLength - 1; } CCD::~CCD() { } void CCD::SetCCDCapability(uint32_t cap) { capability = cap; if (HasST4Port()) setDriverInterface(getDriverInterface() | GUIDER_INTERFACE); else setDriverInterface(getDriverInterface() & ~GUIDER_INTERFACE); if (HasStreaming() && Streamer.get() == nullptr) { Streamer.reset(new StreamManager(this)); Streamer->initProperties(); } } bool CCD::initProperties() { DefaultDevice::initProperties(); // let the base class flesh in what it wants // CCD Temperature IUFillNumber(&TemperatureN[0], "CCD_TEMPERATURE_VALUE", "Temperature (C)", "%5.2f", -50.0, 50.0, 0., 0.); IUFillNumberVector(&TemperatureNP, TemperatureN, 1, getDeviceName(), "CCD_TEMPERATURE", "Temperature", MAIN_CONTROL_TAB, IP_RW, 60, IPS_IDLE); /**********************************************/ /**************** Primary Chip ****************/ /**********************************************/ // Primary CCD Region-Of-Interest (ROI) IUFillNumber(&PrimaryCCD.ImageFrameN[CCDChip::FRAME_X], "X", "Left ", "%4.0f", 0, 0.0, 0, 0); IUFillNumber(&PrimaryCCD.ImageFrameN[CCDChip::FRAME_Y], "Y", "Top", "%4.0f", 0, 0, 0, 0); IUFillNumber(&PrimaryCCD.ImageFrameN[CCDChip::FRAME_W], "WIDTH", "Width", "%4.0f", 0, 0.0, 0, 0.0); IUFillNumber(&PrimaryCCD.ImageFrameN[CCDChip::FRAME_H], "HEIGHT", "Height", "%4.0f", 0, 0, 0, 0.0); IUFillNumberVector(&PrimaryCCD.ImageFrameNP, PrimaryCCD.ImageFrameN, 4, getDeviceName(), "CCD_FRAME", "Frame", IMAGE_SETTINGS_TAB, IP_RW, 60, IPS_IDLE); // Primary CCD Frame Type IUFillSwitch(&PrimaryCCD.FrameTypeS[CCDChip::LIGHT_FRAME], "FRAME_LIGHT", "Light", ISS_ON); IUFillSwitch(&PrimaryCCD.FrameTypeS[CCDChip::BIAS_FRAME], "FRAME_BIAS", "Bias", ISS_OFF); IUFillSwitch(&PrimaryCCD.FrameTypeS[CCDChip::DARK_FRAME], "FRAME_DARK", "Dark", ISS_OFF); IUFillSwitch(&PrimaryCCD.FrameTypeS[CCDChip::FLAT_FRAME], "FRAME_FLAT", "Flat", ISS_OFF); IUFillSwitchVector(&PrimaryCCD.FrameTypeSP, PrimaryCCD.FrameTypeS, 4, getDeviceName(), "CCD_FRAME_TYPE", "Frame Type", IMAGE_SETTINGS_TAB, IP_RW, ISR_1OFMANY, 60, IPS_IDLE); // Primary CCD Exposure IUFillNumber(&PrimaryCCD.ImageExposureN[0], "CCD_EXPOSURE_VALUE", "Duration (s)", "%5.2f", 0.01, 3600, 1.0, 1.0); IUFillNumberVector(&PrimaryCCD.ImageExposureNP, PrimaryCCD.ImageExposureN, 1, getDeviceName(), "CCD_EXPOSURE", "Expose", MAIN_CONTROL_TAB, IP_RW, 60, IPS_IDLE); // Primary CCD Abort IUFillSwitch(&PrimaryCCD.AbortExposureS[0], "ABORT", "Abort", ISS_OFF); IUFillSwitchVector(&PrimaryCCD.AbortExposureSP, PrimaryCCD.AbortExposureS, 1, getDeviceName(), "CCD_ABORT_EXPOSURE", "Expose Abort", MAIN_CONTROL_TAB, IP_RW, ISR_ATMOST1, 60, IPS_IDLE); // Primary CCD Binning IUFillNumber(&PrimaryCCD.ImageBinN[0], "HOR_BIN", "X", "%2.0f", 1, 4, 1, 1); IUFillNumber(&PrimaryCCD.ImageBinN[1], "VER_BIN", "Y", "%2.0f", 1, 4, 1, 1); IUFillNumberVector(&PrimaryCCD.ImageBinNP, PrimaryCCD.ImageBinN, 2, getDeviceName(), "CCD_BINNING", "Binning", IMAGE_SETTINGS_TAB, IP_RW, 60, IPS_IDLE); // Primary CCD Info IUFillNumber(&PrimaryCCD.ImagePixelSizeN[CCDChip::CCD_MAX_X], "CCD_MAX_X", "Max. Width", "%4.0f", 1, 16000, 0, 0); IUFillNumber(&PrimaryCCD.ImagePixelSizeN[CCDChip::CCD_MAX_Y], "CCD_MAX_Y", "Max. Height", "%4.0f", 1, 16000, 0, 0); IUFillNumber(&PrimaryCCD.ImagePixelSizeN[CCDChip::CCD_PIXEL_SIZE], "CCD_PIXEL_SIZE", "Pixel size (um)", "%5.2f", 1, 40, 0, 0); IUFillNumber(&PrimaryCCD.ImagePixelSizeN[CCDChip::CCD_PIXEL_SIZE_X], "CCD_PIXEL_SIZE_X", "Pixel size X", "%5.2f", 1, 40, 0, 0); IUFillNumber(&PrimaryCCD.ImagePixelSizeN[CCDChip::CCD_PIXEL_SIZE_Y], "CCD_PIXEL_SIZE_Y", "Pixel size Y", "%5.2f", 1, 40, 0, 0); IUFillNumber(&PrimaryCCD.ImagePixelSizeN[CCDChip::CCD_BITSPERPIXEL], "CCD_BITSPERPIXEL", "Bits per pixel", "%3.0f", 8, 64, 0, 0); IUFillNumberVector(&PrimaryCCD.ImagePixelSizeNP, PrimaryCCD.ImagePixelSizeN, 6, getDeviceName(), "CCD_INFO", "CCD Information", IMAGE_INFO_TAB, IP_RO, 60, IPS_IDLE); // Primary CCD Compression Options IUFillSwitch(&PrimaryCCD.CompressS[0], "CCD_COMPRESS", "Compress", ISS_OFF); IUFillSwitch(&PrimaryCCD.CompressS[1], "CCD_RAW", "Raw", ISS_ON); IUFillSwitchVector(&PrimaryCCD.CompressSP, PrimaryCCD.CompressS, 2, getDeviceName(), "CCD_COMPRESSION", "Image", IMAGE_SETTINGS_TAB, IP_RW, ISR_1OFMANY, 60, IPS_IDLE); PrimaryCCD.SendCompressed = false; // Primary CCD Chip Data Blob IUFillBLOB(&PrimaryCCD.FitsB, "CCD1", "Image", ""); IUFillBLOBVector(&PrimaryCCD.FitsBP, &PrimaryCCD.FitsB, 1, getDeviceName(), "CCD1", "Image Data", IMAGE_INFO_TAB, IP_RO, 60, IPS_IDLE); // Bayer IUFillText(&BayerT[0], "CFA_OFFSET_X", "X Offset", "0"); IUFillText(&BayerT[1], "CFA_OFFSET_Y", "Y Offset", "0"); IUFillText(&BayerT[2], "CFA_TYPE", "Filter", nullptr); IUFillTextVector(&BayerTP, BayerT, 3, getDeviceName(), "CCD_CFA", "Bayer Info", IMAGE_INFO_TAB, IP_RW, 60, IPS_IDLE); // Reset Frame Settings IUFillSwitch(&PrimaryCCD.ResetS[0], "RESET", "Reset", ISS_OFF); IUFillSwitchVector(&PrimaryCCD.ResetSP, PrimaryCCD.ResetS, 1, getDeviceName(), "CCD_FRAME_RESET", "Frame Values", IMAGE_SETTINGS_TAB, IP_WO, ISR_1OFMANY, 0, IPS_IDLE); /**********************************************/ /********* Primary Chip Rapid Guide **********/ /**********************************************/ IUFillSwitch(&PrimaryCCD.RapidGuideS[0], "ENABLE", "Enable", ISS_OFF); IUFillSwitch(&PrimaryCCD.RapidGuideS[1], "DISABLE", "Disable", ISS_ON); IUFillSwitchVector(&PrimaryCCD.RapidGuideSP, PrimaryCCD.RapidGuideS, 2, getDeviceName(), "CCD_RAPID_GUIDE", "Rapid Guide", OPTIONS_TAB, IP_RW, ISR_1OFMANY, 0, IPS_IDLE); IUFillSwitch(&PrimaryCCD.RapidGuideSetupS[0], "AUTO_LOOP", "Auto loop", ISS_ON); IUFillSwitch(&PrimaryCCD.RapidGuideSetupS[1], "SEND_IMAGE", "Send image", ISS_OFF); IUFillSwitch(&PrimaryCCD.RapidGuideSetupS[2], "SHOW_MARKER", "Show marker", ISS_OFF); IUFillSwitchVector(&PrimaryCCD.RapidGuideSetupSP, PrimaryCCD.RapidGuideSetupS, 3, getDeviceName(), "CCD_RAPID_GUIDE_SETUP", "Rapid Guide Setup", RAPIDGUIDE_TAB, IP_RW, ISR_NOFMANY, 0, IPS_IDLE); IUFillNumber(&PrimaryCCD.RapidGuideDataN[0], "GUIDESTAR_X", "Guide star position X", "%5.2f", 0, 1024, 0, 0); IUFillNumber(&PrimaryCCD.RapidGuideDataN[1], "GUIDESTAR_Y", "Guide star position Y", "%5.2f", 0, 1024, 0, 0); IUFillNumber(&PrimaryCCD.RapidGuideDataN[2], "GUIDESTAR_FIT", "Guide star fit", "%5.2f", 0, 1024, 0, 0); IUFillNumberVector(&PrimaryCCD.RapidGuideDataNP, PrimaryCCD.RapidGuideDataN, 3, getDeviceName(), "CCD_RAPID_GUIDE_DATA", "Rapid Guide Data", RAPIDGUIDE_TAB, IP_RO, 60, IPS_IDLE); /**********************************************/ /***************** Guide Chip *****************/ /**********************************************/ IUFillNumber(&GuideCCD.ImageFrameN[CCDChip::FRAME_X], "X", "Left ", "%4.0f", 0, 0, 0, 0); IUFillNumber(&GuideCCD.ImageFrameN[CCDChip::FRAME_Y], "Y", "Top", "%4.0f", 0, 0, 0, 0); IUFillNumber(&GuideCCD.ImageFrameN[CCDChip::FRAME_W], "WIDTH", "Width", "%4.0f", 0, 0, 0, 0); IUFillNumber(&GuideCCD.ImageFrameN[CCDChip::FRAME_H], "HEIGHT", "Height", "%4.0f", 0, 0, 0, 0); IUFillNumberVector(&GuideCCD.ImageFrameNP, GuideCCD.ImageFrameN, 4, getDeviceName(), "GUIDER_FRAME", "Frame", GUIDE_HEAD_TAB, IP_RW, 60, IPS_IDLE); IUFillNumber(&GuideCCD.ImageBinN[0], "HOR_BIN", "X", "%2.0f", 1, 4, 1, 1); IUFillNumber(&GuideCCD.ImageBinN[1], "VER_BIN", "Y", "%2.0f", 1, 4, 1, 1); IUFillNumberVector(&GuideCCD.ImageBinNP, GuideCCD.ImageBinN, 2, getDeviceName(), "GUIDER_BINNING", "Binning", GUIDE_HEAD_TAB, IP_RW, 60, IPS_IDLE); IUFillNumber(&GuideCCD.ImagePixelSizeN[CCDChip::CCD_MAX_X], "CCD_MAX_X", "Max. Width", "%4.0f", 1, 16000, 0, 0); IUFillNumber(&GuideCCD.ImagePixelSizeN[CCDChip::CCD_MAX_Y], "CCD_MAX_Y", "Max. Height", "%4.0f", 1, 16000, 0, 0); IUFillNumber(&GuideCCD.ImagePixelSizeN[CCDChip::CCD_PIXEL_SIZE], "CCD_PIXEL_SIZE", "Pixel size (um)", "%5.2f", 1, 40, 0, 0); IUFillNumber(&GuideCCD.ImagePixelSizeN[CCDChip::CCD_PIXEL_SIZE_X], "CCD_PIXEL_SIZE_X", "Pixel size X", "%5.2f", 1, 40, 0, 0); IUFillNumber(&GuideCCD.ImagePixelSizeN[CCDChip::CCD_PIXEL_SIZE_Y], "CCD_PIXEL_SIZE_Y", "Pixel size Y", "%5.2f", 1, 40, 0, 0); IUFillNumber(&GuideCCD.ImagePixelSizeN[CCDChip::CCD_BITSPERPIXEL], "CCD_BITSPERPIXEL", "Bits per pixel", "%3.0f", 8, 64, 0, 0); IUFillNumberVector(&GuideCCD.ImagePixelSizeNP, GuideCCD.ImagePixelSizeN, 6, getDeviceName(), "GUIDER_INFO", "Guide Info", IMAGE_INFO_TAB, IP_RO, 60, IPS_IDLE); IUFillSwitch(&GuideCCD.FrameTypeS[0], "FRAME_LIGHT", "Light", ISS_ON); IUFillSwitch(&GuideCCD.FrameTypeS[1], "FRAME_BIAS", "Bias", ISS_OFF); IUFillSwitch(&GuideCCD.FrameTypeS[2], "FRAME_DARK", "Dark", ISS_OFF); IUFillSwitch(&GuideCCD.FrameTypeS[3], "FRAME_FLAT", "Flat", ISS_OFF); IUFillSwitchVector(&GuideCCD.FrameTypeSP, GuideCCD.FrameTypeS, 4, getDeviceName(), "GUIDER_FRAME_TYPE", "Frame Type", GUIDE_HEAD_TAB, IP_RW, ISR_1OFMANY, 60, IPS_IDLE); IUFillNumber(&GuideCCD.ImageExposureN[0], "GUIDER_EXPOSURE_VALUE", "Duration (s)", "%5.2f", 0.01, 3600, 1.0, 1.0); IUFillNumberVector(&GuideCCD.ImageExposureNP, GuideCCD.ImageExposureN, 1, getDeviceName(), "GUIDER_EXPOSURE", "Guide Head", MAIN_CONTROL_TAB, IP_RW, 60, IPS_IDLE); IUFillSwitch(&GuideCCD.AbortExposureS[0], "ABORT", "Abort", ISS_OFF); IUFillSwitchVector(&GuideCCD.AbortExposureSP, GuideCCD.AbortExposureS, 1, getDeviceName(), "GUIDER_ABORT_EXPOSURE", "Guide Abort", MAIN_CONTROL_TAB, IP_RW, ISR_ATMOST1, 60, IPS_IDLE); IUFillSwitch(&GuideCCD.CompressS[0], "GUIDER_COMPRESS", "Compress", ISS_OFF); IUFillSwitch(&GuideCCD.CompressS[1], "GUIDER_RAW", "Raw", ISS_ON); IUFillSwitchVector(&GuideCCD.CompressSP, GuideCCD.CompressS, 2, getDeviceName(), "GUIDER_COMPRESSION", "Image", GUIDE_HEAD_TAB, IP_RW, ISR_1OFMANY, 60, IPS_IDLE); GuideCCD.SendCompressed = false; IUFillBLOB(&GuideCCD.FitsB, "CCD2", "Guider Image", ""); IUFillBLOBVector(&GuideCCD.FitsBP, &GuideCCD.FitsB, 1, getDeviceName(), "CCD2", "Image Data", IMAGE_INFO_TAB, IP_RO, 60, IPS_IDLE); /**********************************************/ /********* Guider Chip Rapid Guide ***********/ /**********************************************/ IUFillSwitch(&GuideCCD.RapidGuideS[0], "ENABLE", "Enable", ISS_OFF); IUFillSwitch(&GuideCCD.RapidGuideS[1], "DISABLE", "Disable", ISS_ON); IUFillSwitchVector(&GuideCCD.RapidGuideSP, GuideCCD.RapidGuideS, 2, getDeviceName(), "GUIDER_RAPID_GUIDE", "Guider Head Rapid Guide", OPTIONS_TAB, IP_RW, ISR_1OFMANY, 0, IPS_IDLE); IUFillSwitch(&GuideCCD.RapidGuideSetupS[0], "AUTO_LOOP", "Auto loop", ISS_ON); IUFillSwitch(&GuideCCD.RapidGuideSetupS[1], "SEND_IMAGE", "Send image", ISS_OFF); IUFillSwitch(&GuideCCD.RapidGuideSetupS[2], "SHOW_MARKER", "Show marker", ISS_OFF); IUFillSwitchVector(&GuideCCD.RapidGuideSetupSP, GuideCCD.RapidGuideSetupS, 3, getDeviceName(), "GUIDER_RAPID_GUIDE_SETUP", "Rapid Guide Setup", RAPIDGUIDE_TAB, IP_RW, ISR_NOFMANY, 0, IPS_IDLE); IUFillNumber(&GuideCCD.RapidGuideDataN[0], "GUIDESTAR_X", "Guide star position X", "%5.2f", 0, 1024, 0, 0); IUFillNumber(&GuideCCD.RapidGuideDataN[1], "GUIDESTAR_Y", "Guide star position Y", "%5.2f", 0, 1024, 0, 0); IUFillNumber(&GuideCCD.RapidGuideDataN[2], "GUIDESTAR_FIT", "Guide star fit", "%5.2f", 0, 1024, 0, 0); IUFillNumberVector(&GuideCCD.RapidGuideDataNP, GuideCCD.RapidGuideDataN, 3, getDeviceName(), "GUIDER_RAPID_GUIDE_DATA", "Rapid Guide Data", RAPIDGUIDE_TAB, IP_RO, 60, IPS_IDLE); /**********************************************/ /******************** WCS *********************/ /**********************************************/ // WCS Enable/Disable IUFillSwitch(&WorldCoordS[0], "WCS_ENABLE", "Enable", ISS_OFF); IUFillSwitch(&WorldCoordS[1], "WCS_DISABLE", "Disable", ISS_ON); IUFillSwitchVector(&WorldCoordSP, WorldCoordS, 2, getDeviceName(), "WCS_CONTROL", "WCS", WCS_TAB, IP_RW, ISR_1OFMANY, 0, IPS_IDLE); IUFillNumber(&CCDRotationN[0], "CCD_ROTATION_VALUE", "Rotation", "%g", -360, 360, 1, 0); IUFillNumberVector(&CCDRotationNP, CCDRotationN, 1, getDeviceName(), "CCD_ROTATION", "CCD FOV", WCS_TAB, IP_RW, 60, IPS_IDLE); IUFillSwitch(&TelescopeTypeS[TELESCOPE_PRIMARY], "TELESCOPE_PRIMARY", "Primary", ISS_ON); IUFillSwitch(&TelescopeTypeS[TELESCOPE_GUIDE], "TELESCOPE_GUIDE", "Guide", ISS_OFF); IUFillSwitchVector(&TelescopeTypeSP, TelescopeTypeS, 2, getDeviceName(), "TELESCOPE_TYPE", "Telescope", OPTIONS_TAB, IP_RW, ISR_1OFMANY, 0, IPS_IDLE); /**********************************************/ /************** Upload Settings ***************/ /**********************************************/ // Upload Mode IUFillSwitch(&UploadS[UPLOAD_CLIENT], "UPLOAD_CLIENT", "Client", ISS_ON); IUFillSwitch(&UploadS[UPLOAD_LOCAL], "UPLOAD_LOCAL", "Local", ISS_OFF); IUFillSwitch(&UploadS[UPLOAD_BOTH], "UPLOAD_BOTH", "Both", ISS_OFF); IUFillSwitchVector(&UploadSP, UploadS, 3, getDeviceName(), "UPLOAD_MODE", "Upload", OPTIONS_TAB, IP_RW, ISR_1OFMANY, 0, IPS_IDLE); // Upload Settings IUFillText(&UploadSettingsT[UPLOAD_DIR], "UPLOAD_DIR", "Dir", ""); IUFillText(&UploadSettingsT[UPLOAD_PREFIX], "UPLOAD_PREFIX", "Prefix", "IMAGE_XXX"); IUFillTextVector(&UploadSettingsTP, UploadSettingsT, 2, getDeviceName(), "UPLOAD_SETTINGS", "Upload Settings", OPTIONS_TAB, IP_RW, 60, IPS_IDLE); // Upload File Path IUFillText(&FileNameT[0], "FILE_PATH", "Path", ""); IUFillTextVector(&FileNameTP, FileNameT, 1, getDeviceName(), "CCD_FILE_PATH", "Filename", IMAGE_INFO_TAB, IP_RO, 60, IPS_IDLE); /**********************************************/ /****************** FITS Header****************/ /**********************************************/ IUFillText(&FITSHeaderT[FITS_OBSERVER], "FITS_OBSERVER", "Observer", "Unknown"); IUFillText(&FITSHeaderT[FITS_OBJECT], "FITS_OBJECT", "Object", "Unknown"); IUFillTextVector(&FITSHeaderTP, FITSHeaderT, 2, getDeviceName(), "FITS_HEADER", "FITS Header", INFO_TAB, IP_RW, 60, IPS_IDLE); /**********************************************/ /****************** Exposure Looping **********/ /***************** Primary CCD Only ***********/ #ifdef WITH_EXPOSURE_LOOPING IUFillSwitch(&ExposureLoopS[EXPOSURE_LOOP_ON], "LOOP_ON", "Enabled", ISS_OFF); IUFillSwitch(&ExposureLoopS[EXPOSURE_LOOP_OFF], "LOOP_OFF", "Disabled", ISS_ON); IUFillSwitchVector(&ExposureLoopSP, ExposureLoopS, 2, getDeviceName(), "CCD_EXPOSURE_LOOP", "Rapid Looping", OPTIONS_TAB, IP_RW, ISR_1OFMANY, 0, IPS_IDLE); // CCD Should loop until the number of frames specified in this property is completed IUFillNumber(&ExposureLoopCountN[0], "FRAMES", "Frames", "%.f", 0, 100000, 1, 1); IUFillNumberVector(&ExposureLoopCountNP, ExposureLoopCountN, 1, getDeviceName(), "CCD_EXPOSURE_LOOP_COUNT", "Rapid Count", OPTIONS_TAB, IP_RW, 0, IPS_IDLE); #endif /**********************************************/ /**************** Snooping ********************/ /**********************************************/ // Snooped Devices IUFillText(&ActiveDeviceT[0], "ACTIVE_TELESCOPE", "Telescope", "Telescope Simulator"); IUFillText(&ActiveDeviceT[1], "ACTIVE_FOCUSER", "Focuser", "Focuser Simulator"); IUFillText(&ActiveDeviceT[2], "ACTIVE_FILTER", "Filter", "CCD Simulator"); IUFillText(&ActiveDeviceT[3], "ACTIVE_SKYQUALITY", "Sky Quality", "SQM"); IUFillTextVector(&ActiveDeviceTP, ActiveDeviceT, 4, getDeviceName(), "ACTIVE_DEVICES", "Snoop devices", OPTIONS_TAB, IP_RW, 60, IPS_IDLE); // Snooped RA/DEC Property IUFillNumber(&EqN[0], "RA", "Ra (hh:mm:ss)", "%010.6m", 0, 24, 0, 0); IUFillNumber(&EqN[1], "DEC", "Dec (dd:mm:ss)", "%010.6m", -90, 90, 0, 0); IUFillNumberVector(&EqNP, EqN, 2, ActiveDeviceT[0].text, "EQUATORIAL_EOD_COORD", "EQ Coord", "Main Control", IP_RW, 60, IPS_IDLE); // Snoop properties of interest // Snoop mount IDSnoopDevice(ActiveDeviceT[SNOOP_MOUNT].text, "EQUATORIAL_EOD_COORD"); IDSnoopDevice(ActiveDeviceT[SNOOP_MOUNT].text, "TELESCOPE_INFO"); IDSnoopDevice(ActiveDeviceT[SNOOP_MOUNT].text, "GEOGRAPHIC_COORD"); // Snoop Rotator IDSnoopDevice(ActiveDeviceT[SNOOP_ROTATOR].text, "ABS_ROTATOR_ANGLE"); // Snoop Filter Wheel IDSnoopDevice(ActiveDeviceT[SNOOP_FILTER_WHEEL].text, "FILTER_SLOT"); IDSnoopDevice(ActiveDeviceT[SNOOP_FILTER_WHEEL].text, "FILTER_NAME"); // Snoop Sky Quality Meter IDSnoopDevice(ActiveDeviceT[SNOOP_SQM].text, "SKY_QUALITY"); // Guider Interface initGuiderProperties(getDeviceName(), GUIDE_CONTROL_TAB); addPollPeriodControl(); setDriverInterface(CCD_INTERFACE | GUIDER_INTERFACE); return true; } void CCD::ISGetProperties(const char *dev) { DefaultDevice::ISGetProperties(dev); defineText(&ActiveDeviceTP); loadConfig(true, "ACTIVE_DEVICES"); if (HasStreaming()) Streamer->ISGetProperties(dev); } bool CCD::updateProperties() { //IDLog("CCD UpdateProperties isConnected returns %d %d\n",isConnected(),Connected); if (isConnected()) { defineNumber(&PrimaryCCD.ImageExposureNP); if (CanAbort()) defineSwitch(&PrimaryCCD.AbortExposureSP); if (CanSubFrame() == false) PrimaryCCD.ImageFrameNP.p = IP_RO; defineNumber(&PrimaryCCD.ImageFrameNP); if (CanBin()) defineNumber(&PrimaryCCD.ImageBinNP); defineText(&FITSHeaderTP); if (HasGuideHead()) { defineNumber(&GuideCCD.ImageExposureNP); if (CanAbort()) defineSwitch(&GuideCCD.AbortExposureSP); if (CanSubFrame() == false) GuideCCD.ImageFrameNP.p = IP_RO; defineNumber(&GuideCCD.ImageFrameNP); } if (HasCooler()) defineNumber(&TemperatureNP); defineNumber(&PrimaryCCD.ImagePixelSizeNP); if (HasGuideHead()) { defineNumber(&GuideCCD.ImagePixelSizeNP); if (CanBin()) defineNumber(&GuideCCD.ImageBinNP); } defineSwitch(&PrimaryCCD.CompressSP); defineBLOB(&PrimaryCCD.FitsBP); if (HasGuideHead()) { defineSwitch(&GuideCCD.CompressSP); defineBLOB(&GuideCCD.FitsBP); } if (HasST4Port()) { defineNumber(&GuideNSNP); defineNumber(&GuideWENP); } defineSwitch(&PrimaryCCD.FrameTypeSP); if (CanBin() || CanSubFrame()) defineSwitch(&PrimaryCCD.ResetSP); if (HasGuideHead()) defineSwitch(&GuideCCD.FrameTypeSP); if (HasBayer()) defineText(&BayerTP); defineSwitch(&PrimaryCCD.RapidGuideSP); if (HasGuideHead()) defineSwitch(&GuideCCD.RapidGuideSP); if (RapidGuideEnabled) { defineSwitch(&PrimaryCCD.RapidGuideSetupSP); defineNumber(&PrimaryCCD.RapidGuideDataNP); } if (GuiderRapidGuideEnabled) { defineSwitch(&GuideCCD.RapidGuideSetupSP); defineNumber(&GuideCCD.RapidGuideDataNP); } defineSwitch(&TelescopeTypeSP); defineSwitch(&WorldCoordSP); defineSwitch(&UploadSP); if (UploadSettingsT[UPLOAD_DIR].text == nullptr) IUSaveText(&UploadSettingsT[UPLOAD_DIR], getenv("HOME")); defineText(&UploadSettingsTP); #ifdef WITH_EXPOSURE_LOOPING defineSwitch(&ExposureLoopSP); defineNumber(&ExposureLoopCountNP); #endif } else { deleteProperty(PrimaryCCD.ImageFrameNP.name); deleteProperty(PrimaryCCD.ImagePixelSizeNP.name); if (CanBin()) deleteProperty(PrimaryCCD.ImageBinNP.name); deleteProperty(PrimaryCCD.ImageExposureNP.name); if (CanAbort()) deleteProperty(PrimaryCCD.AbortExposureSP.name); deleteProperty(PrimaryCCD.FitsBP.name); deleteProperty(PrimaryCCD.CompressSP.name); deleteProperty(PrimaryCCD.RapidGuideSP.name); if (RapidGuideEnabled) { deleteProperty(PrimaryCCD.RapidGuideSetupSP.name); deleteProperty(PrimaryCCD.RapidGuideDataNP.name); } deleteProperty(FITSHeaderTP.name); if (HasGuideHead()) { deleteProperty(GuideCCD.ImageExposureNP.name); if (CanAbort()) deleteProperty(GuideCCD.AbortExposureSP.name); deleteProperty(GuideCCD.ImageFrameNP.name); deleteProperty(GuideCCD.ImagePixelSizeNP.name); deleteProperty(GuideCCD.FitsBP.name); if (CanBin()) deleteProperty(GuideCCD.ImageBinNP.name); deleteProperty(GuideCCD.CompressSP.name); deleteProperty(GuideCCD.FrameTypeSP.name); deleteProperty(GuideCCD.RapidGuideSP.name); if (GuiderRapidGuideEnabled) { deleteProperty(GuideCCD.RapidGuideSetupSP.name); deleteProperty(GuideCCD.RapidGuideDataNP.name); } } if (HasCooler()) deleteProperty(TemperatureNP.name); if (HasST4Port()) { deleteProperty(GuideNSNP.name); deleteProperty(GuideWENP.name); } deleteProperty(PrimaryCCD.FrameTypeSP.name); if (CanBin() || CanSubFrame()) deleteProperty(PrimaryCCD.ResetSP.name); if (HasBayer()) deleteProperty(BayerTP.name); deleteProperty(TelescopeTypeSP.name); if (WorldCoordS[0].s == ISS_ON) { deleteProperty(CCDRotationNP.name); } deleteProperty(WorldCoordSP.name); deleteProperty(UploadSP.name); deleteProperty(UploadSettingsTP.name); #ifdef WITH_EXPOSURE_LOOPING deleteProperty(ExposureLoopSP.name); deleteProperty(ExposureLoopCountNP.name); #endif } // Streamer if (HasStreaming()) Streamer->updateProperties(); return true; } bool CCD::ISSnoopDevice(XMLEle *root) { XMLEle *ep = nullptr; const char *propName = findXMLAttValu(root, "name"); if (IUSnoopNumber(root, &EqNP) == 0) { float newra, newdec; newra = EqN[0].value; newdec = EqN[1].value; if ((newra != RA) || (newdec != Dec)) { //IDLog("RA %4.2f Dec %4.2f Snooped RA %4.2f Dec %4.2f\n",RA,Dec,newra,newdec); RA = newra; Dec = newdec; } } else if (!strcmp(propName, "TELESCOPE_INFO")) { for (ep = nextXMLEle(root, 1); ep != nullptr; ep = nextXMLEle(root, 0)) { const char *name = findXMLAttValu(ep, "name"); if (!strcmp(name, "TELESCOPE_APERTURE")) { primaryAperture = atof(pcdataXMLEle(ep)); } else if (!strcmp(name, "TELESCOPE_FOCAL_LENGTH")) { primaryFocalLength = atof(pcdataXMLEle(ep)); } else if (!strcmp(name, "GUIDER_APERTURE")) { guiderAperture = atof(pcdataXMLEle(ep)); } else if (!strcmp(name, "GUIDER_FOCAL_LENGTH")) { guiderFocalLength = atof(pcdataXMLEle(ep)); } } } else if (!strcmp(propName, "FILTER_NAME")) { FilterNames.clear(); for (ep = nextXMLEle(root, 1); ep != nullptr; ep = nextXMLEle(root, 0)) FilterNames.push_back(pcdataXMLEle(ep)); } else if (!strcmp(propName, "FILTER_SLOT")) { CurrentFilterSlot = -1; for (ep = nextXMLEle(root, 1); ep != nullptr; ep = nextXMLEle(root, 0)) CurrentFilterSlot = atoi(pcdataXMLEle(ep)); } else if (!strcmp(propName, "SKY_QUALITY")) { for (ep = nextXMLEle(root, 1); ep != nullptr; ep = nextXMLEle(root, 0)) { const char *name = findXMLAttValu(ep, "name"); if (!strcmp(name, "SKY_BRIGHTNESS")) { MPSAS = atof(pcdataXMLEle(ep)); break; } } } else if (!strcmp(propName, "ABS_ROTATOR_ANGLE")) { for (ep = nextXMLEle(root, 1); ep != nullptr; ep = nextXMLEle(root, 0)) { const char *name = findXMLAttValu(ep, "name"); if (!strcmp(name, "ANGLE")) { RotatorAngle = atof(pcdataXMLEle(ep)); break; } } } else if (!strcmp(propName, "GEOGRAPHIC_COORD")) { for (ep = nextXMLEle(root, 1); ep != nullptr; ep = nextXMLEle(root, 0)) { const char *name = findXMLAttValu(ep, "name"); if (!strcmp(name, "LONG")) { Longitude = atof(pcdataXMLEle(ep)); if (Longitude > 180) Longitude -= 360; } else if (!strcmp(name, "LAT")) { Latitude = atof(pcdataXMLEle(ep)); } } } return DefaultDevice::ISSnoopDevice(root); } bool CCD::ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n) { // first check if it's for our device if (dev != nullptr && strcmp(dev, getDeviceName()) == 0) { // This is for our device // Now lets see if it's something we process here if (!strcmp(name, ActiveDeviceTP.name)) { ActiveDeviceTP.s = IPS_OK; IUUpdateText(&ActiveDeviceTP, texts, names, n); IDSetText(&ActiveDeviceTP, nullptr); // Update the property name! strncpy(EqNP.device, ActiveDeviceT[SNOOP_MOUNT].text, MAXINDIDEVICE); if (strlen(ActiveDeviceT[SNOOP_MOUNT].text) > 0) { IDSnoopDevice(ActiveDeviceT[SNOOP_MOUNT].text, "EQUATORIAL_EOD_COORD"); IDSnoopDevice(ActiveDeviceT[SNOOP_MOUNT].text, "TELESCOPE_INFO"); IDSnoopDevice(ActiveDeviceT[SNOOP_MOUNT].text, "GEOGRAPHIC_COORD"); } else { RA = std::numeric_limits::quiet_NaN(); Dec = std::numeric_limits::quiet_NaN(); J2000RA = std::numeric_limits::quiet_NaN(); J2000DE = std::numeric_limits::quiet_NaN(); Latitude = std::numeric_limits::quiet_NaN(); Longitude = std::numeric_limits::quiet_NaN(); Airmass = std::numeric_limits::quiet_NaN(); } if (strlen(ActiveDeviceT[SNOOP_ROTATOR].text) > 0) IDSnoopDevice(ActiveDeviceT[SNOOP_ROTATOR].text, "ABS_ROTATOR_ANGLE"); else MPSAS = std::numeric_limits::quiet_NaN(); if (strlen(ActiveDeviceT[SNOOP_FILTER_WHEEL].text) > 0) { IDSnoopDevice(ActiveDeviceT[SNOOP_FILTER_WHEEL].text, "FILTER_SLOT"); IDSnoopDevice(ActiveDeviceT[SNOOP_FILTER_WHEEL].text, "FILTER_NAME"); } else { CurrentFilterSlot = -1; } IDSnoopDevice(ActiveDeviceT[SNOOP_SQM].text, "SKY_QUALITY"); // Tell children active devices was updated. activeDevicesUpdated(); // We processed this one, so, tell the world we did it return true; } if (!strcmp(name, BayerTP.name)) { IUUpdateText(&BayerTP, texts, names, n); BayerTP.s = IPS_OK; IDSetText(&BayerTP, nullptr); return true; } if (!strcmp(name, FITSHeaderTP.name)) { IUUpdateText(&FITSHeaderTP, texts, names, n); FITSHeaderTP.s = IPS_OK; IDSetText(&FITSHeaderTP, nullptr); return true; } if (!strcmp(name, UploadSettingsTP.name)) { IUUpdateText(&UploadSettingsTP, texts, names, n); UploadSettingsTP.s = IPS_OK; IDSetText(&UploadSettingsTP, nullptr); return true; } } // Streamer if (HasStreaming()) Streamer->ISNewText(dev, name, texts, names, n); return DefaultDevice::ISNewText(dev, name, texts, names, n); } bool CCD::ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) { // first check if it's for our device //IDLog("CCD::ISNewNumber %s\n",name); if (dev != nullptr && strcmp(dev, getDeviceName()) == 0) { if (!strcmp(name, "CCD_EXPOSURE")) { if (PrimaryCCD.getFrameType() != CCDChip::BIAS_FRAME && (values[0] < PrimaryCCD.ImageExposureN[0].min || values[0] > PrimaryCCD.ImageExposureN[0].max)) { DEBUGF(Logger::DBG_ERROR, "Requested exposure value (%g) seconds out of bounds [%g,%g].", values[0], PrimaryCCD.ImageExposureN[0].min, PrimaryCCD.ImageExposureN[0].max); PrimaryCCD.ImageExposureNP.s = IPS_ALERT; IDSetNumber(&PrimaryCCD.ImageExposureNP, nullptr); return false; } if (PrimaryCCD.getFrameType() == CCDChip::BIAS_FRAME) PrimaryCCD.ImageExposureN[0].value = ExposureTime = PrimaryCCD.ImageExposureN[0].min; else PrimaryCCD.ImageExposureN[0].value = ExposureTime = values[0]; // Only abort when busy if we are not already in an exposure loops //if (PrimaryCCD.ImageExposureNP.s == IPS_BUSY && ExposureLoopS[EXPOSURE_LOOP_OFF].s == ISS_ON) if (PrimaryCCD.ImageExposureNP.s == IPS_BUSY) { if (CanAbort() && AbortExposure() == false) DEBUG(Logger::DBG_WARNING, "Warning: Aborting exposure failed."); } if (StartExposure(ExposureTime)) { if (PrimaryCCD.getFrameType() == CCDChip::LIGHT_FRAME && !std::isnan(RA) && !std::isnan(Dec)) { ln_equ_posn epochPos { 0, 0 }, J2000Pos { 0, 0 }; epochPos.ra = RA * 15.0; epochPos.dec = Dec; // Convert from JNow to J2000 ln_get_equ_prec2(&epochPos, ln_get_julian_from_sys(), JD2000, &J2000Pos); J2000RA = J2000Pos.ra / 15.0; J2000DE = J2000Pos.dec; if (!std::isnan(Latitude) && !std::isnan(Longitude)) { // Horizontal Coords ln_hrz_posn horizontalPos; ln_lnlat_posn observer; observer.lat = Latitude; observer.lng = Longitude; ln_get_hrz_from_equ(&epochPos, &observer, ln_get_julian_from_sys(), &horizontalPos); Airmass = ln_get_airmass(horizontalPos.alt, 750); } } PrimaryCCD.ImageExposureNP.s = IPS_BUSY; if (ExposureTime*1000 < POLLMS) POLLMS = ExposureTime*950; } else PrimaryCCD.ImageExposureNP.s = IPS_ALERT; IDSetNumber(&PrimaryCCD.ImageExposureNP, nullptr); return true; } if (!strcmp(name, "GUIDER_EXPOSURE")) { if (GuideCCD.getFrameType() != CCDChip::BIAS_FRAME && (values[0] < GuideCCD.ImageExposureN[0].min || values[0] > GuideCCD.ImageExposureN[0].max)) { DEBUGF(Logger::DBG_ERROR, "Requested guide exposure value (%g) seconds out of bounds [%g,%g].", values[0], GuideCCD.ImageExposureN[0].min, GuideCCD.ImageExposureN[0].max); GuideCCD.ImageExposureNP.s = IPS_ALERT; IDSetNumber(&GuideCCD.ImageExposureNP, nullptr); return false; } if (GuideCCD.getFrameType() == CCDChip::BIAS_FRAME) GuideCCD.ImageExposureN[0].value = GuiderExposureTime = GuideCCD.ImageExposureN[0].min; else GuideCCD.ImageExposureN[0].value = GuiderExposureTime = values[0]; GuideCCD.ImageExposureNP.s = IPS_BUSY; if (StartGuideExposure(GuiderExposureTime)) GuideCCD.ImageExposureNP.s = IPS_BUSY; else GuideCCD.ImageExposureNP.s = IPS_ALERT; IDSetNumber(&GuideCCD.ImageExposureNP, nullptr); return true; } if (!strcmp(name, "CCD_BINNING")) { // We are being asked to set camera binning INumber *np = IUFindNumber(&PrimaryCCD.ImageBinNP, names[0]); if (np == nullptr) { PrimaryCCD.ImageBinNP.s = IPS_ALERT; IDSetNumber(&PrimaryCCD.ImageBinNP, nullptr); return false; } int binx, biny; if (!strcmp(np->name, "HOR_BIN")) { binx = values[0]; biny = values[1]; } else { binx = values[1]; biny = values[0]; } if (UpdateCCDBin(binx, biny)) { IUUpdateNumber(&PrimaryCCD.ImageBinNP, values, names, n); PrimaryCCD.ImageBinNP.s = IPS_OK; } else PrimaryCCD.ImageBinNP.s = IPS_ALERT; IDSetNumber(&PrimaryCCD.ImageBinNP, nullptr); return true; } if (!strcmp(name, "GUIDER_BINNING")) { // We are being asked to set camera binning INumber *np = IUFindNumber(&GuideCCD.ImageBinNP, names[0]); if (np == nullptr) { GuideCCD.ImageBinNP.s = IPS_ALERT; IDSetNumber(&GuideCCD.ImageBinNP, nullptr); return false; } int binx, biny; if (!strcmp(np->name, "HOR_BIN")) { binx = values[0]; biny = values[1]; } else { binx = values[1]; biny = values[0]; } if (UpdateGuiderBin(binx, biny)) { IUUpdateNumber(&GuideCCD.ImageBinNP, values, names, n); GuideCCD.ImageBinNP.s = IPS_OK; } else GuideCCD.ImageBinNP.s = IPS_ALERT; IDSetNumber(&GuideCCD.ImageBinNP, nullptr); return true; } if (!strcmp(name, "CCD_FRAME")) { int x=-1,y=-1,w=-1,h=-1; for (int i=0; i < n; i++) { if (!strcmp(names[i], "X")) x = values[i]; else if (!strcmp(names[i], "Y")) y = values[i]; else if (!strcmp(names[i], "WIDTH")) w = values[i]; else if (!strcmp(names[i], "HEIGHT")) h = values[i]; } DEBUGF(Logger::DBG_DEBUG, "Requested CCD Frame is (%d,%d) (%d x %d)", x, y, w, h); if (x < 0 || y < 0 || w < 0 || h < 0) { DEBUGF(Logger::DBG_ERROR, "Invalid frame requested (%d,%d) (%d x %d)", x, y, w, h); PrimaryCCD.ImageFrameNP.s = IPS_ALERT; IDSetNumber(&PrimaryCCD.ImageFrameNP, nullptr); return true; } if (UpdateCCDFrame(x, y, w, h)) { PrimaryCCD.ImageFrameNP.s = IPS_OK; IUUpdateNumber(&PrimaryCCD.ImageFrameNP, values, names, n); } else PrimaryCCD.ImageFrameNP.s = IPS_ALERT; IDSetNumber(&PrimaryCCD.ImageFrameNP, nullptr); return true; } if (!strcmp(name, "GUIDER_FRAME")) { // We are being asked to set guide frame if (IUUpdateNumber(&GuideCCD.ImageFrameNP, values, names, n) < 0) return false; GuideCCD.ImageFrameNP.s = IPS_OK; DEBUGF(Logger::DBG_DEBUG, "Requested Guide Frame is %4.0f,%4.0f %4.0f x %4.0f", values[0], values[1], values[2], values[4]); if (UpdateGuiderFrame(GuideCCD.ImageFrameN[0].value, GuideCCD.ImageFrameN[1].value, GuideCCD.ImageFrameN[2].value, GuideCCD.ImageFrameN[3].value) == false) GuideCCD.ImageFrameNP.s = IPS_ALERT; IDSetNumber(&GuideCCD.ImageFrameNP, nullptr); return true; } if (!strcmp(name, "CCD_GUIDESTAR")) { PrimaryCCD.RapidGuideDataNP.s = IPS_OK; IUUpdateNumber(&PrimaryCCD.RapidGuideDataNP, values, names, n); IDSetNumber(&PrimaryCCD.RapidGuideDataNP, nullptr); return true; } if (!strcmp(name, "GUIDER_GUIDESTAR")) { GuideCCD.RapidGuideDataNP.s = IPS_OK; IUUpdateNumber(&GuideCCD.RapidGuideDataNP, values, names, n); IDSetNumber(&GuideCCD.RapidGuideDataNP, nullptr); return true; } if (!strcmp(name, GuideNSNP.name) || !strcmp(name, GuideWENP.name)) { processGuiderProperties(name, values, names, n); return true; } #ifdef WITH_EXPOSURE_LOOPING if (!strcmp(name, ExposureLoopCountNP.name)) { IUUpdateNumber(&ExposureLoopCountNP, values, names, n); ExposureLoopCountNP.s = IPS_OK; IDSetNumber(&ExposureLoopCountNP, nullptr); return true; } #endif // CCD TEMPERATURE: if (!strcmp(name, TemperatureNP.name)) { if (values[0] < TemperatureN[0].min || values[0] > TemperatureN[0].max) { TemperatureNP.s = IPS_ALERT; DEBUGF(Logger::DBG_ERROR, "Error: Bad temperature value! Range is [%.1f, %.1f] [C].", TemperatureN[0].min, TemperatureN[0].max); IDSetNumber(&TemperatureNP, nullptr); return false; } int rc = SetTemperature(values[0]); if (rc == 0) TemperatureNP.s = IPS_BUSY; else if (rc == 1) TemperatureNP.s = IPS_OK; else TemperatureNP.s = IPS_ALERT; IDSetNumber(&TemperatureNP, nullptr); return true; } // Primary CCD Info if (!strcmp(name, PrimaryCCD.ImagePixelSizeNP.name)) { IUUpdateNumber(&PrimaryCCD.ImagePixelSizeNP, values, names, n); PrimaryCCD.ImagePixelSizeNP.s = IPS_OK; SetCCDParams(PrimaryCCD.ImagePixelSizeNP.np[CCDChip::CCD_MAX_X].value, PrimaryCCD.ImagePixelSizeNP.np[CCDChip::CCD_MAX_Y].value, PrimaryCCD.getBPP(), PrimaryCCD.ImagePixelSizeNP.np[CCDChip::CCD_PIXEL_SIZE_X].value, PrimaryCCD.ImagePixelSizeNP.np[CCDChip::CCD_PIXEL_SIZE_Y].value); IDSetNumber(&PrimaryCCD.ImagePixelSizeNP, nullptr); return true; } // Guide CCD Info if (!strcmp(name, GuideCCD.ImagePixelSizeNP.name)) { IUUpdateNumber(&GuideCCD.ImagePixelSizeNP, values, names, n); GuideCCD.ImagePixelSizeNP.s = IPS_OK; SetGuiderParams(GuideCCD.ImagePixelSizeNP.np[CCDChip::CCD_MAX_X].value, GuideCCD.ImagePixelSizeNP.np[CCDChip::CCD_MAX_Y].value, GuideCCD.getBPP(), GuideCCD.ImagePixelSizeNP.np[CCDChip::CCD_PIXEL_SIZE_X].value, GuideCCD.ImagePixelSizeNP.np[CCDChip::CCD_PIXEL_SIZE_Y].value); IDSetNumber(&GuideCCD.ImagePixelSizeNP, nullptr); return true; } // CCD Rotation if (!strcmp(name, CCDRotationNP.name)) { IUUpdateNumber(&CCDRotationNP, values, names, n); CCDRotationNP.s = IPS_OK; IDSetNumber(&CCDRotationNP, nullptr); ValidCCDRotation = true; DEBUGF(Logger::DBG_SESSION, "CCD FOV rotation updated to %g degrees.", CCDRotationN[0].value); return true; } } // Streamer if (HasStreaming()) Streamer->ISNewNumber(dev, name, values, names, n); return DefaultDevice::ISNewNumber(dev, name, values, names, n); } bool CCD::ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) { if (dev != nullptr && strcmp(dev, getDeviceName()) == 0) { // Upload Mode if (!strcmp(name, UploadSP.name)) { int prevMode = IUFindOnSwitchIndex(&UploadSP); IUUpdateSwitch(&UploadSP, states, names, n); if (UpdateCCDUploadMode(static_cast(IUFindOnSwitchIndex(&UploadSP)))) { if (UploadS[UPLOAD_CLIENT].s == ISS_ON) { DEBUG(Logger::DBG_SESSION, "Upload settings set to client only."); if (prevMode != 0) deleteProperty(FileNameTP.name); } else if (UploadS[UPLOAD_LOCAL].s == ISS_ON) { DEBUG(Logger::DBG_SESSION, "Upload settings set to local only."); defineText(&FileNameTP); } else { DEBUG(Logger::DBG_SESSION, "Upload settings set to client and local."); defineText(&FileNameTP); } UploadSP.s = IPS_OK; } else { IUResetSwitch(&UploadSP); UploadS[prevMode].s = ISS_ON; UploadSP.s = IPS_ALERT; } IDSetSwitch(&UploadSP, nullptr); return true; } if (!strcmp(name, TelescopeTypeSP.name)) { IUUpdateSwitch(&TelescopeTypeSP, states, names, n); TelescopeTypeSP.s = IPS_OK; IDSetSwitch(&TelescopeTypeSP, nullptr); return true; } #ifdef WITH_EXPOSURE_LOOPING // Exposure Looping if (!strcmp(name, ExposureLoopSP.name)) { IUUpdateSwitch(&ExposureLoopSP, states, names, n); ExposureLoopSP.s = IPS_OK; IDSetSwitch(&ExposureLoopSP, nullptr); return true; } #endif // WCS Enable/Disable if (!strcmp(name, WorldCoordSP.name)) { IUUpdateSwitch(&WorldCoordSP, states, names, n); WorldCoordSP.s = IPS_OK; if (WorldCoordS[0].s == ISS_ON) { DEBUG(Logger::DBG_WARNING, "World Coordinate System is enabled. CCD rotation must be set either " "manually or by solving the image before proceeding to capture any " "frames, otherwise the WCS information may be invalid."); defineNumber(&CCDRotationNP); } else { deleteProperty(CCDRotationNP.name); } ValidCCDRotation = false; IDSetSwitch(&WorldCoordSP, nullptr); } // Primary Chip Frame Reset if (strcmp(name, PrimaryCCD.ResetSP.name) == 0) { IUResetSwitch(&PrimaryCCD.ResetSP); PrimaryCCD.ResetSP.s = IPS_OK; if (CanBin()) UpdateCCDBin(1, 1); if (CanSubFrame()) UpdateCCDFrame(0, 0, PrimaryCCD.getXRes(), PrimaryCCD.getYRes()); IDSetSwitch(&PrimaryCCD.ResetSP, nullptr); return true; } // Primary Chip Abort Expsoure if (strcmp(name, PrimaryCCD.AbortExposureSP.name) == 0) { IUResetSwitch(&PrimaryCCD.AbortExposureSP); if (AbortExposure()) { PrimaryCCD.AbortExposureSP.s = IPS_OK; PrimaryCCD.ImageExposureNP.s = IPS_IDLE; PrimaryCCD.ImageExposureN[0].value = 0; } else { PrimaryCCD.AbortExposureSP.s = IPS_ALERT; PrimaryCCD.ImageExposureNP.s = IPS_ALERT; } POLLMS = getPollingPeriod(); if (ExposureLoopCountNP.s == IPS_BUSY) { uploadTime=0; ExposureLoopCountNP.s = IPS_IDLE; ExposureLoopCountN[0].value = 1; IDSetNumber(&ExposureLoopCountNP, nullptr); } IDSetSwitch(&PrimaryCCD.AbortExposureSP, nullptr); IDSetNumber(&PrimaryCCD.ImageExposureNP, nullptr); return true; } // Guide Chip Abort Exposure if (strcmp(name, GuideCCD.AbortExposureSP.name) == 0) { IUResetSwitch(&GuideCCD.AbortExposureSP); if (AbortGuideExposure()) { GuideCCD.AbortExposureSP.s = IPS_OK; GuideCCD.ImageExposureNP.s = IPS_IDLE; GuideCCD.ImageExposureN[0].value = 0; } else { GuideCCD.AbortExposureSP.s = IPS_ALERT; GuideCCD.ImageExposureNP.s = IPS_ALERT; } IDSetSwitch(&GuideCCD.AbortExposureSP, nullptr); IDSetNumber(&GuideCCD.ImageExposureNP, nullptr); return true; } // Primary Chip Compression if (strcmp(name, PrimaryCCD.CompressSP.name) == 0) { IUUpdateSwitch(&PrimaryCCD.CompressSP, states, names, n); PrimaryCCD.CompressSP.s = IPS_OK; IDSetSwitch(&PrimaryCCD.CompressSP, nullptr); if (PrimaryCCD.CompressS[0].s == ISS_ON) { PrimaryCCD.SendCompressed = true; } else { PrimaryCCD.SendCompressed = false; } return true; } // Guide Chip Compression if (strcmp(name, GuideCCD.CompressSP.name) == 0) { IUUpdateSwitch(&GuideCCD.CompressSP, states, names, n); GuideCCD.CompressSP.s = IPS_OK; IDSetSwitch(&GuideCCD.CompressSP, nullptr); if (GuideCCD.CompressS[0].s == ISS_ON) { GuideCCD.SendCompressed = true; } else { GuideCCD.SendCompressed = false; } return true; } // Primary Chip Frame Type if (strcmp(name, PrimaryCCD.FrameTypeSP.name) == 0) { IUUpdateSwitch(&PrimaryCCD.FrameTypeSP, states, names, n); PrimaryCCD.FrameTypeSP.s = IPS_OK; if (PrimaryCCD.FrameTypeS[0].s == ISS_ON) PrimaryCCD.setFrameType(CCDChip::LIGHT_FRAME); else if (PrimaryCCD.FrameTypeS[1].s == ISS_ON) { PrimaryCCD.setFrameType(CCDChip::BIAS_FRAME); if (HasShutter() == false) DEBUG(Logger::DBG_WARNING, "The CCD does not have a shutter. Cover the camera in order to take a bias frame."); } else if (PrimaryCCD.FrameTypeS[2].s == ISS_ON) { PrimaryCCD.setFrameType(CCDChip::DARK_FRAME); if (HasShutter() == false) DEBUG(Logger::DBG_WARNING, "The CCD does not have a shutter. Cover the camera in order to take a dark frame."); } else if (PrimaryCCD.FrameTypeS[3].s == ISS_ON) PrimaryCCD.setFrameType(CCDChip::FLAT_FRAME); if (UpdateCCDFrameType(PrimaryCCD.getFrameType()) == false) PrimaryCCD.FrameTypeSP.s = IPS_ALERT; IDSetSwitch(&PrimaryCCD.FrameTypeSP, nullptr); return true; } // Guide Chip Frame Type if (strcmp(name, GuideCCD.FrameTypeSP.name) == 0) { // Compression Update IUUpdateSwitch(&GuideCCD.FrameTypeSP, states, names, n); GuideCCD.FrameTypeSP.s = IPS_OK; if (GuideCCD.FrameTypeS[0].s == ISS_ON) GuideCCD.setFrameType(CCDChip::LIGHT_FRAME); else if (GuideCCD.FrameTypeS[1].s == ISS_ON) { GuideCCD.setFrameType(CCDChip::BIAS_FRAME); if (HasShutter() == false) DEBUG(Logger::DBG_WARNING, "The CCD does not have a shutter. Cover the camera in order to take a bias frame."); } else if (GuideCCD.FrameTypeS[2].s == ISS_ON) { GuideCCD.setFrameType(CCDChip::DARK_FRAME); if (HasShutter() == false) DEBUG(Logger::DBG_WARNING, "The CCD does not have a shutter. Cover the camera in order to take a dark frame."); } else if (GuideCCD.FrameTypeS[3].s == ISS_ON) GuideCCD.setFrameType(CCDChip::FLAT_FRAME); if (UpdateGuiderFrameType(GuideCCD.getFrameType()) == false) GuideCCD.FrameTypeSP.s = IPS_ALERT; IDSetSwitch(&GuideCCD.FrameTypeSP, nullptr); return true; } // Primary Chip Rapid Guide Enable/Disable if (strcmp(name, PrimaryCCD.RapidGuideSP.name) == 0) { IUUpdateSwitch(&PrimaryCCD.RapidGuideSP, states, names, n); PrimaryCCD.RapidGuideSP.s = IPS_OK; RapidGuideEnabled = (PrimaryCCD.RapidGuideS[0].s == ISS_ON); if (RapidGuideEnabled) { defineSwitch(&PrimaryCCD.RapidGuideSetupSP); defineNumber(&PrimaryCCD.RapidGuideDataNP); } else { deleteProperty(PrimaryCCD.RapidGuideSetupSP.name); deleteProperty(PrimaryCCD.RapidGuideDataNP.name); } IDSetSwitch(&PrimaryCCD.RapidGuideSP, nullptr); return true; } // Guide Chip Rapid Guide Enable/Disable if (strcmp(name, GuideCCD.RapidGuideSP.name) == 0) { IUUpdateSwitch(&GuideCCD.RapidGuideSP, states, names, n); GuideCCD.RapidGuideSP.s = IPS_OK; GuiderRapidGuideEnabled = (GuideCCD.RapidGuideS[0].s == ISS_ON); if (GuiderRapidGuideEnabled) { defineSwitch(&GuideCCD.RapidGuideSetupSP); defineNumber(&GuideCCD.RapidGuideDataNP); } else { deleteProperty(GuideCCD.RapidGuideSetupSP.name); deleteProperty(GuideCCD.RapidGuideDataNP.name); } IDSetSwitch(&GuideCCD.RapidGuideSP, nullptr); return true; } // Primary CCD Rapid Guide Setup if (strcmp(name, PrimaryCCD.RapidGuideSetupSP.name) == 0) { IUUpdateSwitch(&PrimaryCCD.RapidGuideSetupSP, states, names, n); PrimaryCCD.RapidGuideSetupSP.s = IPS_OK; AutoLoop = (PrimaryCCD.RapidGuideSetupS[0].s == ISS_ON); SendImage = (PrimaryCCD.RapidGuideSetupS[1].s == ISS_ON); ShowMarker = (PrimaryCCD.RapidGuideSetupS[2].s == ISS_ON); IDSetSwitch(&PrimaryCCD.RapidGuideSetupSP, nullptr); return true; } // Guide Chip Rapid Guide Setup if (strcmp(name, GuideCCD.RapidGuideSetupSP.name) == 0) { IUUpdateSwitch(&GuideCCD.RapidGuideSetupSP, states, names, n); GuideCCD.RapidGuideSetupSP.s = IPS_OK; GuiderAutoLoop = (GuideCCD.RapidGuideSetupS[0].s == ISS_ON); GuiderSendImage = (GuideCCD.RapidGuideSetupS[1].s == ISS_ON); GuiderShowMarker = (GuideCCD.RapidGuideSetupS[2].s == ISS_ON); IDSetSwitch(&GuideCCD.RapidGuideSetupSP, nullptr); return true; } } if (HasStreaming()) Streamer->ISNewSwitch(dev, name, states, names, n); return DefaultDevice::ISNewSwitch(dev, name, states, names, n); } int CCD::SetTemperature(double temperature) { INDI_UNUSED(temperature); DEBUGF(Logger::DBG_WARNING, "CCD::SetTemperature %4.2f - Should never get here", temperature); return -1; } bool CCD::StartExposure(float duration) { DEBUGF(Logger::DBG_WARNING, "CCD::StartExposure %4.2f - Should never get here", duration); return false; } bool CCD::StartGuideExposure(float duration) { DEBUGF(Logger::DBG_WARNING, "CCD::StartGuide Exposure %4.2f - Should never get here", duration); return false; } bool CCD::AbortExposure() { DEBUG(Logger::DBG_WARNING, "CCD::AbortExposure - Should never get here"); return false; } bool CCD::AbortGuideExposure() { DEBUG(Logger::DBG_WARNING, "CCD::AbortGuideExposure - Should never get here"); return false; } bool CCD::UpdateCCDFrame(int x, int y, int w, int h) { // Just set value, unless HW layer overrides this and performs its own processing PrimaryCCD.setFrame(x, y, w, h); return true; } bool CCD::UpdateGuiderFrame(int x, int y, int w, int h) { GuideCCD.setFrame(x, y, w, h); return true; } bool CCD::UpdateCCDBin(int hor, int ver) { // Just set value, unless HW layer overrides this and performs its own processing PrimaryCCD.setBin(hor, ver); // Reset size if (HasStreaming()) Streamer->setSize(PrimaryCCD.getSubW()/hor, PrimaryCCD.getSubH()/ver); return true; } bool CCD::UpdateGuiderBin(int hor, int ver) { // Just set value, unless HW layer overrides this and performs its own processing GuideCCD.setBin(hor, ver); return true; } bool CCD::UpdateCCDFrameType(CCDChip::CCD_FRAME fType) { INDI_UNUSED(fType); // Child classes can override this return true; } bool CCD::UpdateGuiderFrameType(CCDChip::CCD_FRAME fType) { INDI_UNUSED(fType); // Child classes can override this return true; } void CCD::addFITSKeywords(fitsfile *fptr, CCDChip *targetChip) { int status = 0; char frame_s[32]; char dev_name[32]; char exp_start[32]; double exposureDuration; float pixSize1, pixSize2; unsigned int xbin, ybin; AutoCNumeric locale; xbin = targetChip->getBinX(); ybin = targetChip->getBinY(); char fitsString[MAXINDIDEVICE]; // CCD strncpy(fitsString, getDeviceName(), MAXINDIDEVICE); fits_update_key_s(fptr, TSTRING, "INSTRUME", fitsString, "CCD Name", &status); // Telescope if (strlen(ActiveDeviceT[0].text) > 0) { strncpy(fitsString, ActiveDeviceT[0].text, MAXINDIDEVICE); fits_update_key_s(fptr, TSTRING, "TELESCOP", fitsString, "Telescope name", &status); } // Observer strncpy(fitsString, FITSHeaderT[FITS_OBSERVER].text, MAXINDIDEVICE); fits_update_key_s(fptr, TSTRING, "OBSERVER", fitsString, "Observer name", &status); // Object strncpy(fitsString, FITSHeaderT[FITS_OBJECT].text, MAXINDIDEVICE); fits_update_key_s(fptr, TSTRING, "OBJECT", fitsString, "Object name", &status); switch (targetChip->getFrameType()) { case CCDChip::LIGHT_FRAME: strcpy(frame_s, "Light"); break; case CCDChip::BIAS_FRAME: strcpy(frame_s, "Bias"); break; case CCDChip::FLAT_FRAME: strcpy(frame_s, "Flat Field"); break; case CCDChip::DARK_FRAME: strcpy(frame_s, "Dark"); break; } exposureDuration = targetChip->getExposureDuration(); pixSize1 = targetChip->getPixelSizeX(); pixSize2 = targetChip->getPixelSizeY(); strncpy(dev_name, getDeviceName(), 32); strncpy(exp_start, targetChip->getExposureStartTime(), 32); fits_update_key_s(fptr, TDOUBLE, "EXPTIME", &(exposureDuration), "Total Exposure Time (s)", &status); if (targetChip->getFrameType() == CCDChip::DARK_FRAME) fits_update_key_s(fptr, TDOUBLE, "DARKTIME", &(exposureDuration), "Total Exposure Time (s)", &status); if (HasCooler()) fits_update_key_s(fptr, TDOUBLE, "CCD-TEMP", &(TemperatureN[0].value), "CCD Temperature (Celsius)", &status); fits_update_key_s(fptr, TFLOAT, "PIXSIZE1", &(pixSize1), "Pixel Size 1 (microns)", &status); fits_update_key_s(fptr, TFLOAT, "PIXSIZE2", &(pixSize2), "Pixel Size 2 (microns)", &status); fits_update_key_s(fptr, TUINT, "XBINNING", &(xbin), "Binning factor in width", &status); fits_update_key_s(fptr, TUINT, "YBINNING", &(ybin), "Binning factor in height", &status); fits_update_key_s(fptr, TSTRING, "FRAME", frame_s, "Frame Type", &status); if (CurrentFilterSlot != -1 && CurrentFilterSlot <= (int)FilterNames.size()) { char filter[32]; strncpy(filter, FilterNames.at(CurrentFilterSlot - 1).c_str(), 32); fits_update_key_s(fptr, TSTRING, "FILTER", filter, "Filter", &status); } #ifdef WITH_MINMAX if (targetChip->getNAxis() == 2) { double min_val, max_val; getMinMax(&min_val, &max_val, targetChip); fits_update_key_s(fptr, TDOUBLE, "DATAMIN", &min_val, "Minimum value", &status); fits_update_key_s(fptr, TDOUBLE, "DATAMAX", &max_val, "Maximum value", &status); } #endif if (HasBayer() && targetChip->getNAxis() == 2) { unsigned int bayer_offset_x = atoi(BayerT[0].text); unsigned int bayer_offset_y = atoi(BayerT[1].text); fits_update_key_s(fptr, TUINT, "XBAYROFF", &bayer_offset_x, "X offset of Bayer array", &status); fits_update_key_s(fptr, TUINT, "YBAYROFF", &bayer_offset_y, "Y offset of Bayer array", &status); fits_update_key_s(fptr, TSTRING, "BAYERPAT", BayerT[2].text, "Bayer color pattern", &status); } if (TelescopeTypeS[TELESCOPE_PRIMARY].s == ISS_ON && primaryFocalLength != -1) fits_update_key_s(fptr, TDOUBLE, "FOCALLEN", &primaryFocalLength, "Focal Length (mm)", &status); else if (TelescopeTypeS[TELESCOPE_GUIDE].s == ISS_ON && guiderFocalLength != -1) fits_update_key_s(fptr, TDOUBLE, "FOCALLEN", &guiderFocalLength, "Focal Length (mm)", &status); if (!std::isnan(MPSAS)) { fits_update_key_s(fptr, TDOUBLE, "MPSAS", &MPSAS, "Sky Quality (mag per arcsec^2)", &status); } if (!std::isnan(RotatorAngle)) { fits_update_key_s(fptr, TDOUBLE, "ROTATANG", &MPSAS, "Rotator angle in degrees", &status); } if (targetChip->getFrameType() == CCDChip::LIGHT_FRAME && !std::isnan(J2000RA) && !std::isnan(J2000DE)) { char ra_str[32], de_str[32]; fs_sexa(ra_str, J2000RA, 2, 360000); fs_sexa(de_str, J2000DE, 2, 360000); char *raPtr = ra_str, *dePtr = de_str; while (*raPtr != '\0') { if (*raPtr == ':') *raPtr = ' '; raPtr++; } while (*dePtr != '\0') { if (*dePtr == ':') *dePtr = ' '; dePtr++; } if (!std::isnan(Airmass)) fits_update_key_s(fptr, TDOUBLE, "AIRMASS", &Airmass, "Airmass", &status); fits_update_key_s(fptr, TSTRING, "OBJCTRA", ra_str, "Object RA", &status); fits_update_key_s(fptr, TSTRING, "OBJCTDEC", de_str, "Object DEC", &status); int epoch = 2000; //fits_update_key_s(fptr, TINT, "EPOCH", &epoch, "Epoch", &status); fits_update_key_s(fptr, TINT, "EQUINOX", &epoch, "Equinox", &status); // Add WCS Info if (WorldCoordS[0].s == ISS_ON && ValidCCDRotation && primaryFocalLength != -1) { double J2000RAHours = J2000RA * 15; fits_update_key_s(fptr, TDOUBLE, "CRVAL1", &J2000RAHours, "CRVAL1", &status); fits_update_key_s(fptr, TDOUBLE, "CRVAL2", &J2000DE, "CRVAL1", &status); char radecsys[8] = "FK5"; char ctype1[16] = "RA---TAN"; char ctype2[16] = "DEC--TAN"; fits_update_key_s(fptr, TSTRING, "RADECSYS", radecsys, "RADECSYS", &status); fits_update_key_s(fptr, TSTRING, "CTYPE1", ctype1, "CTYPE1", &status); fits_update_key_s(fptr, TSTRING, "CTYPE2", ctype2, "CTYPE2", &status); double crpix1 = targetChip->getSubW() / targetChip->getBinX() / 2.0; double crpix2 = targetChip->getSubH() / targetChip->getBinY() / 2.0; fits_update_key_s(fptr, TDOUBLE, "CRPIX1", &crpix1, "CRPIX1", &status); fits_update_key_s(fptr, TDOUBLE, "CRPIX2", &crpix2, "CRPIX2", &status); double secpix1 = pixSize1 / primaryFocalLength * 206.3 * targetChip->getBinX(); double secpix2 = pixSize2 / primaryFocalLength * 206.3 * targetChip->getBinY(); //double secpix1 = pixSize1 / FocalLength * 206.3; //double secpix2 = pixSize2 / FocalLength * 206.3; fits_update_key_s(fptr, TDOUBLE, "SECPIX1", &secpix1, "SECPIX1", &status); fits_update_key_s(fptr, TDOUBLE, "SECPIX2", &secpix2, "SECPIX2", &status); double degpix1 = secpix1 / 3600.0; double degpix2 = secpix2 / 3600.0; fits_update_key_s(fptr, TDOUBLE, "CDELT1", °pix1, "CDELT1", &status); fits_update_key_s(fptr, TDOUBLE, "CDELT2", °pix2, "CDELT2", &status); // Rotation is CW, we need to convert it to CCW per CROTA1 definition double rotation = 360 - CCDRotationN[0].value; if (rotation > 360) rotation -= 360; fits_update_key_s(fptr, TDOUBLE, "CROTA1", &rotation, "CROTA1", &status); fits_update_key_s(fptr, TDOUBLE, "CROTA2", &rotation, "CROTA2", &status); /*double cd[4]; cd[0] = degpix1; cd[1] = 0; cd[2] = 0; cd[3] = degpix2; fits_update_key_s(fptr, TDOUBLE, "CD1_1", &cd[0], "CD1_1", &status); fits_update_key_s(fptr, TDOUBLE, "CD1_2", &cd[1], "CD1_2", &status); fits_update_key_s(fptr, TDOUBLE, "CD2_1", &cd[2], "CD2_1", &status); fits_update_key_s(fptr, TDOUBLE, "CD2_2", &cd[3], "CD2_2", &status);*/ } } fits_update_key_s(fptr, TSTRING, "DATE-OBS", exp_start, "UTC start date of observation", &status); fits_write_comment(fptr, "Generated by INDI", &status); } void CCD::fits_update_key_s(fitsfile *fptr, int type, std::string name, void *p, std::string explanation, int *status) { // this function is for removing warnings about deprecated string conversion to char* (from arg 5) fits_update_key(fptr, type, name.c_str(), p, const_cast(explanation.c_str()), status); } bool CCD::ExposureComplete(CCDChip *targetChip) { // Reset POLLMS to default value POLLMS = getPollingPeriod(); #ifdef WITH_EXPOSURE_LOOPING // If looping is on, let's immediately take another capture if (ExposureLoopS[EXPOSURE_LOOP_ON].s == ISS_ON) { double duration = targetChip->getExposureDuration(); if (ExposureLoopCountN[0].value > 1) { if (ExposureLoopCountNP.s != IPS_BUSY) { exposureLoopStartup = std::chrono::system_clock::now(); } else { auto end = std::chrono::system_clock::now(); uploadTime = (std::chrono::duration_cast(end - exposureLoopStartup)).count() / 1000.0 - duration; LOGF_DEBUG("Image download and upload/save took %.3f seconds.", uploadTime); exposureLoopStartup = end; } ExposureLoopCountNP.s = IPS_BUSY; ExposureLoopCountN[0].value--; IDSetNumber(&ExposureLoopCountNP, nullptr); if (uploadTime < duration) { StartExposure(duration); PrimaryCCD.ImageExposureNP.s = IPS_BUSY; IDSetNumber(&PrimaryCCD.ImageExposureNP, nullptr); if (duration*1000 < POLLMS) POLLMS = duration*950; } else { LOGF_ERROR("Rapid exposure not possible since upload time is %.2f seconds while exposure time is %.2f seconds.", uploadTime, duration); PrimaryCCD.ImageExposureNP.s = IPS_ALERT; IDSetNumber(&PrimaryCCD.ImageExposureNP, nullptr); ExposureLoopCountN[0].value=1; ExposureLoopCountNP.s = IPS_IDLE; IDSetNumber(&ExposureLoopCountNP, nullptr); uploadTime = 0; return false; } } else { uploadTime = 0; ExposureLoopCountNP.s = IPS_IDLE; IDSetNumber(&ExposureLoopCountNP, nullptr); } } #endif bool sendImage = (UploadS[0].s == ISS_ON || UploadS[2].s == ISS_ON); bool saveImage = (UploadS[1].s == ISS_ON || UploadS[2].s == ISS_ON); bool showMarker = false; bool autoLoop = false; bool sendData = false; if (RapidGuideEnabled && targetChip == &PrimaryCCD && (PrimaryCCD.getBPP() == 16 || PrimaryCCD.getBPP() == 8)) { autoLoop = AutoLoop; sendImage = SendImage; showMarker = ShowMarker; sendData = true; saveImage = false; } if (GuiderRapidGuideEnabled && targetChip == &GuideCCD && (GuideCCD.getBPP() == 16 || PrimaryCCD.getBPP() == 8)) { autoLoop = GuiderAutoLoop; sendImage = GuiderSendImage; showMarker = GuiderShowMarker; sendData = true; saveImage = false; } if (sendData) { static double P0 = 0.906, P1 = 0.584, P2 = 0.365, P3 = 0.117, P4 = 0.049, P5 = -0.05, P6 = -0.064, P7 = -0.074, P8 = -0.094; targetChip->RapidGuideDataNP.s = IPS_BUSY; int width = targetChip->getSubW() / targetChip->getBinX(); int height = targetChip->getSubH() / targetChip->getBinY(); void *src = (unsigned short *)targetChip->getFrameBuffer(); int i0, i1, i2, i3, i4, i5, i6, i7, i8; int ix = 0, iy = 0; int xM4; double average, fit, bestFit = 0; int minx = 4; int maxx = width - 4; int miny = 4; int maxy = height - 4; if (targetChip->lastRapidX > 0 && targetChip->lastRapidY > 0) { minx = std::max(targetChip->lastRapidX - 20, 4); maxx = std::min(targetChip->lastRapidX + 20, width - 4); miny = std::max(targetChip->lastRapidY - 20, 4); maxy = std::min(targetChip->lastRapidY + 20, height - 4); } if (targetChip->getBPP() == 16) { unsigned short *p; for (int x = minx; x < maxx; x++) for (int y = miny; y < maxy; y++) { i0 = i1 = i2 = i3 = i4 = i5 = i6 = i7 = i8 = 0; xM4 = x - 4; p = (unsigned short *)src + (y - 4) * width + xM4; i8 += *p++; i8 += *p++; i8 += *p++; i8 += *p++; i8 += *p++; i8 += *p++; i8 += *p++; i8 += *p++; i8 += *p++; p = (unsigned short *)src + (y - 3) * width + xM4; i8 += *p++; i8 += *p++; i8 += *p++; i7 += *p++; i6 += *p++; i7 += *p++; i8 += *p++; i8 += *p++; i8 += *p++; p = (unsigned short *)src + (y - 2) * width + xM4; i8 += *p++; i8 += *p++; i5 += *p++; i4 += *p++; i3 += *p++; i4 += *p++; i5 += *p++; i8 += *p++; i8 += *p++; p = (unsigned short *)src + (y - 1) * width + xM4; i8 += *p++; i7 += *p++; i4 += *p++; i2 += *p++; i1 += *p++; i2 += *p++; i4 += *p++; i8 += *p++; i8 += *p++; p = (unsigned short *)src + (y + 0) * width + xM4; i8 += *p++; i6 += *p++; i3 += *p++; i1 += *p++; i0 += *p++; i1 += *p++; i3 += *p++; i6 += *p++; i8 += *p++; p = (unsigned short *)src + (y + 1) * width + xM4; i8 += *p++; i7 += *p++; i4 += *p++; i2 += *p++; i1 += *p++; i2 += *p++; i4 += *p++; i8 += *p++; i8 += *p++; p = (unsigned short *)src + (y + 2) * width + xM4; i8 += *p++; i8 += *p++; i5 += *p++; i4 += *p++; i3 += *p++; i4 += *p++; i5 += *p++; i8 += *p++; i8 += *p++; p = (unsigned short *)src + (y + 3) * width + xM4; i8 += *p++; i8 += *p++; i8 += *p++; i7 += *p++; i6 += *p++; i7 += *p++; i8 += *p++; i8 += *p++; i8 += *p++; p = (unsigned short *)src + (y + 4) * width + xM4; i8 += *p++; i8 += *p++; i8 += *p++; i8 += *p++; i8 += *p++; i8 += *p++; i8 += *p++; i8 += *p++; i8 += *p++; average = (i0 + i1 + i2 + i3 + i4 + i5 + i6 + i7 + i8) / 85.0; fit = P0 * (i0 - average) + P1 * (i1 - 4 * average) + P2 * (i2 - 4 * average) + P3 * (i3 - 4 * average) + P4 * (i4 - 8 * average) + P5 * (i5 - 4 * average) + P6 * (i6 - 4 * average) + P7 * (i7 - 8 * average) + P8 * (i8 - 48 * average); if (bestFit < fit) { bestFit = fit; ix = x; iy = y; } } } else { unsigned char *p; for (int x = minx; x < maxx; x++) for (int y = miny; y < maxy; y++) { i0 = i1 = i2 = i3 = i4 = i5 = i6 = i7 = i8 = 0; xM4 = x - 4; p = (unsigned char *)src + (y - 4) * width + xM4; i8 += *p++; i8 += *p++; i8 += *p++; i8 += *p++; i8 += *p++; i8 += *p++; i8 += *p++; i8 += *p++; i8 += *p++; p = (unsigned char *)src + (y - 3) * width + xM4; i8 += *p++; i8 += *p++; i8 += *p++; i7 += *p++; i6 += *p++; i7 += *p++; i8 += *p++; i8 += *p++; i8 += *p++; p = (unsigned char *)src + (y - 2) * width + xM4; i8 += *p++; i8 += *p++; i5 += *p++; i4 += *p++; i3 += *p++; i4 += *p++; i5 += *p++; i8 += *p++; i8 += *p++; p = (unsigned char *)src + (y - 1) * width + xM4; i8 += *p++; i7 += *p++; i4 += *p++; i2 += *p++; i1 += *p++; i2 += *p++; i4 += *p++; i8 += *p++; i8 += *p++; p = (unsigned char *)src + (y + 0) * width + xM4; i8 += *p++; i6 += *p++; i3 += *p++; i1 += *p++; i0 += *p++; i1 += *p++; i3 += *p++; i6 += *p++; i8 += *p++; p = (unsigned char *)src + (y + 1) * width + xM4; i8 += *p++; i7 += *p++; i4 += *p++; i2 += *p++; i1 += *p++; i2 += *p++; i4 += *p++; i8 += *p++; i8 += *p++; p = (unsigned char *)src + (y + 2) * width + xM4; i8 += *p++; i8 += *p++; i5 += *p++; i4 += *p++; i3 += *p++; i4 += *p++; i5 += *p++; i8 += *p++; i8 += *p++; p = (unsigned char *)src + (y + 3) * width + xM4; i8 += *p++; i8 += *p++; i8 += *p++; i7 += *p++; i6 += *p++; i7 += *p++; i8 += *p++; i8 += *p++; i8 += *p++; p = (unsigned char *)src + (y + 4) * width + xM4; i8 += *p++; i8 += *p++; i8 += *p++; i8 += *p++; i8 += *p++; i8 += *p++; i8 += *p++; i8 += *p++; i8 += *p++; average = (i0 + i1 + i2 + i3 + i4 + i5 + i6 + i7 + i8) / 85.0; fit = P0 * (i0 - average) + P1 * (i1 - 4 * average) + P2 * (i2 - 4 * average) + P3 * (i3 - 4 * average) + P4 * (i4 - 8 * average) + P5 * (i5 - 4 * average) + P6 * (i6 - 4 * average) + P7 * (i7 - 8 * average) + P8 * (i8 - 48 * average); if (bestFit < fit) { bestFit = fit; ix = x; iy = y; } } } targetChip->RapidGuideDataN[0].value = ix; targetChip->RapidGuideDataN[1].value = iy; targetChip->RapidGuideDataN[2].value = bestFit; targetChip->lastRapidX = ix; targetChip->lastRapidY = iy; if (bestFit > 50) { int sumX = 0; int sumY = 0; int total = 0; int max = 0; int noiseThreshold = 0; if (targetChip->getBPP() == 16) { unsigned short *p; for (int y = iy - 4; y <= iy + 4; y++) { p = (unsigned short *)src + y * width + ix - 4; for (int x = ix - 4; x <= ix + 4; x++) { int w = *p++; noiseThreshold += w; if (w > max) max = w; } } noiseThreshold = (noiseThreshold / 81 + max) / 2; // set threshold between peak and average for (int y = iy - 4; y <= iy + 4; y++) { p = (unsigned short *)src + y * width + ix - 4; for (int x = ix - 4; x <= ix + 4; x++) { int w = *p++; if (w < noiseThreshold) w = 0; sumX += x * w; sumY += y * w; total += w; } } } else { unsigned char *p; for (int y = iy - 4; y <= iy + 4; y++) { p = (unsigned char *)src + y * width + ix - 4; for (int x = ix - 4; x <= ix + 4; x++) { int w = *p++; noiseThreshold += w; if (w > max) max = w; } } noiseThreshold = (noiseThreshold / 81 + max) / 2; // set threshold between peak and average for (int y = iy - 4; y <= iy + 4; y++) { p = (unsigned char *)src + y * width + ix - 4; for (int x = ix - 4; x <= ix + 4; x++) { int w = *p++; if (w < noiseThreshold) w = 0; sumX += x * w; sumY += y * w; total += w; } } } if (total > 0) { targetChip->RapidGuideDataN[0].value = ((double)sumX) / total; targetChip->RapidGuideDataN[1].value = ((double)sumY) / total; targetChip->RapidGuideDataNP.s = IPS_OK; DEBUGF(Logger::DBG_DEBUG, "Guide Star X: %g Y: %g FIT: %g", targetChip->RapidGuideDataN[0].value, targetChip->RapidGuideDataN[1].value, targetChip->RapidGuideDataN[2].value); } else { targetChip->RapidGuideDataNP.s = IPS_ALERT; targetChip->lastRapidX = targetChip->lastRapidY = -1; } } else { targetChip->RapidGuideDataNP.s = IPS_ALERT; targetChip->lastRapidX = targetChip->lastRapidY = -1; } IDSetNumber(&targetChip->RapidGuideDataNP, nullptr); if (showMarker) { int xmin = std::max(ix - 10, 0); int xmax = std::min(ix + 10, width - 1); int ymin = std::max(iy - 10, 0); int ymax = std::min(iy + 10, height - 1); //fprintf(stderr, "%d %d %d %d\n", xmin, xmax, ymin, ymax); if (targetChip->getBPP() == 16) { unsigned short *p; if (ymin > 0) { p = (unsigned short *)src + ymin * width + xmin; for (int x = xmin; x <= xmax; x++) *p++ = 50000; } if (xmin > 0) { for (int y = ymin; y <= ymax; y++) { *((unsigned short *)src + y * width + xmin) = 50000; } } if (xmax < width - 1) { for (int y = ymin; y <= ymax; y++) { *((unsigned short *)src + y * width + xmax) = 50000; } } if (ymax < height - 1) { p = (unsigned short *)src + ymax * width + xmin; for (int x = xmin; x <= xmax; x++) *p++ = 50000; } } else { unsigned char *p; if (ymin > 0) { p = (unsigned char *)src + ymin * width + xmin; for (int x = xmin; x <= xmax; x++) *p++ = 255; } if (xmin > 0) { for (int y = ymin; y <= ymax; y++) { *((unsigned char *)src + y * width + xmin) = 255; } } if (xmax < width - 1) { for (int y = ymin; y <= ymax; y++) { *((unsigned char *)src + y * width + xmax) = 255; } } if (ymax < height - 1) { p = (unsigned char *)src + ymax * width + xmin; for (int x = xmin; x <= xmax; x++) *p++ = 255; } } } } if (sendImage || saveImage /* || useSolver*/) { if (!strcmp(targetChip->getImageExtension(), "fits")) { void *memptr; size_t memsize; int img_type = 0; int byte_type = 0; int status = 0; long naxis = targetChip->getNAxis(); long naxes[naxis]; int nelements = 0; std::string bit_depth; char error_status[MAXRBUF]; fitsfile *fptr = nullptr; naxes[0] = targetChip->getSubW() / targetChip->getBinX(); naxes[1] = targetChip->getSubH() / targetChip->getBinY(); switch (targetChip->getBPP()) { case 8: byte_type = TBYTE; img_type = BYTE_IMG; bit_depth = "8 bits per pixel"; break; case 16: byte_type = TUSHORT; img_type = USHORT_IMG; bit_depth = "16 bits per pixel"; break; case 32: byte_type = TULONG; img_type = ULONG_IMG; bit_depth = "32 bits per pixel"; break; default: DEBUGF(Logger::DBG_ERROR, "Unsupported bits per pixel value %d", targetChip->getBPP()); return false; break; } nelements = naxes[0] * naxes[1]; if (naxis == 3) { nelements *= 3; naxes[2] = 3; } /*DEBUGF(Logger::DBG_DEBUG, "Exposure complete. Image Depth: %s. Width: %d Height: %d nelements: %d", bit_depth.c_str(), naxes[0], naxes[1], nelements);*/ // Now we have to send fits format data to the client memsize = 5760; memptr = malloc(memsize); if (!memptr) { DEBUGF(Logger::DBG_ERROR, "Error: failed to allocate memory: %lu", (unsigned long)memsize); } fits_create_memfile(&fptr, &memptr, &memsize, 2880, realloc, &status); if (status) { fits_report_error(stderr, status); /* print out any error messages */ fits_get_errstatus(status, error_status); DEBUGF(Logger::DBG_ERROR, "FITS Error: %s", error_status); return false; } fits_create_img(fptr, img_type, naxis, naxes, &status); if (status) { fits_report_error(stderr, status); /* print out any error messages */ fits_get_errstatus(status, error_status); DEBUGF(Logger::DBG_ERROR, "FITS Error: %s", error_status); return false; } addFITSKeywords(fptr, targetChip); fits_write_img(fptr, byte_type, 1, nelements, targetChip->getFrameBuffer(), &status); if (status) { fits_report_error(stderr, status); /* print out any error messages */ fits_get_errstatus(status, error_status); DEBUGF(Logger::DBG_ERROR, "FITS Error: %s", error_status); return false; } fits_close_file(fptr, &status); bool rc = uploadFile(targetChip, memptr, memsize, sendImage, saveImage /*, useSolver*/); free(memptr); if (rc == false) { targetChip->setExposureFailed(); return false; } } else { bool rc = uploadFile(targetChip, targetChip->getFrameBuffer(), targetChip->getFrameBufferSize(), sendImage, saveImage); if (rc == false) { targetChip->setExposureFailed(); return false; } } } targetChip->ImageExposureNP.s = IPS_OK; IDSetNumber(&targetChip->ImageExposureNP, nullptr); if (autoLoop) { if (targetChip == &PrimaryCCD) { PrimaryCCD.ImageExposureN[0].value = ExposureTime; if (StartExposure(ExposureTime)) { // Record information required later in creation of FITS header if (targetChip->getFrameType() == CCDChip::LIGHT_FRAME && !std::isnan(RA) && !std::isnan(Dec)) { ln_equ_posn epochPos { 0, 0 }, J2000Pos { 0, 0 }; epochPos.ra = RA * 15.0; epochPos.dec = Dec; // Convert from JNow to J2000 ln_get_equ_prec2(&epochPos, ln_get_julian_from_sys(), JD2000, &J2000Pos); J2000RA = J2000Pos.ra / 15.0; J2000DE = J2000Pos.dec; if (!std::isnan(Latitude) && !std::isnan(Longitude)) { // Horizontal Coords ln_hrz_posn horizontalPos; ln_lnlat_posn observer; observer.lat = Latitude; observer.lng = Longitude; ln_get_hrz_from_equ(&epochPos, &observer, ln_get_julian_from_sys(), &horizontalPos); Airmass = ln_get_airmass(horizontalPos.alt, 750); } } PrimaryCCD.ImageExposureNP.s = IPS_BUSY; } else { DEBUG(Logger::DBG_DEBUG, "Autoloop: Primary CCD Exposure Error!"); PrimaryCCD.ImageExposureNP.s = IPS_ALERT; } IDSetNumber(&PrimaryCCD.ImageExposureNP, nullptr); } else { GuideCCD.ImageExposureN[0].value = GuiderExposureTime; GuideCCD.ImageExposureNP.s = IPS_BUSY; if (StartGuideExposure(GuiderExposureTime)) GuideCCD.ImageExposureNP.s = IPS_BUSY; else { DEBUG(Logger::DBG_DEBUG, "Autoloop: Guide CCD Exposure Error!"); GuideCCD.ImageExposureNP.s = IPS_ALERT; } IDSetNumber(&GuideCCD.ImageExposureNP, nullptr); } } return true; } bool CCD::uploadFile(CCDChip *targetChip, const void *fitsData, size_t totalBytes, bool sendImage, bool saveImage /*, bool useSolver*/) { unsigned char *compressedData = nullptr; uLongf compressedBytes = 0; DEBUGF(Logger::DBG_DEBUG, "Uploading file. Ext: %s, Size: %d, sendImage? %s, saveImage? %s", targetChip->getImageExtension(), totalBytes, sendImage ? "Yes" : "No", saveImage ? "Yes" : "No"); if (saveImage) { targetChip->FitsB.blob = (unsigned char *)fitsData; targetChip->FitsB.bloblen = totalBytes; snprintf(targetChip->FitsB.format, MAXINDIBLOBFMT, ".%s", targetChip->getImageExtension()); FILE *fp = nullptr; char imageFileName[MAXRBUF]; std::string prefix = UploadSettingsT[UPLOAD_PREFIX].text; int maxIndex = getFileIndex(UploadSettingsT[UPLOAD_DIR].text, UploadSettingsT[UPLOAD_PREFIX].text, targetChip->FitsB.format); if (maxIndex < 0) { DEBUGF(Logger::DBG_ERROR, "Error iterating directory %s. %s", UploadSettingsT[0].text, strerror(errno)); return false; } if (maxIndex > 0) { char ts[32]; struct tm *tp; time_t t; time(&t); tp = localtime(&t); strftime(ts, sizeof(ts), "%Y-%m-%dT%H-%M-%S", tp); std::string filets(ts); prefix = std::regex_replace(prefix, std::regex("ISO8601"), filets); char indexString[8]; snprintf(indexString, 8, "%03d", maxIndex); std::string prefixIndex = indexString; //prefix.replace(prefix.find("XXX"), std::string::npos, prefixIndex); prefix = std::regex_replace(prefix, std::regex("XXX"), prefixIndex); } snprintf(imageFileName, MAXRBUF, "%s/%s%s", UploadSettingsT[0].text, prefix.c_str(), targetChip->FitsB.format); fp = fopen(imageFileName, "w"); if (fp == nullptr) { DEBUGF(Logger::DBG_ERROR, "Unable to save image file (%s). %s", imageFileName, strerror(errno)); return false; } int n = 0; for (int nr = 0; nr < (int)targetChip->FitsB.bloblen; nr += n) n = fwrite((static_cast(targetChip->FitsB.blob) + nr), 1, targetChip->FitsB.bloblen - nr, fp); fclose(fp); // Save image file path IUSaveText(&FileNameT[0], imageFileName); DEBUGF(Logger::DBG_SESSION, "Image saved to %s", imageFileName); FileNameTP.s = IPS_OK; IDSetText(&FileNameTP, nullptr); } if (targetChip->SendCompressed) { compressedBytes = sizeof(char) * totalBytes + totalBytes / 64 + 16 + 3; compressedData = new uint8_t[compressedBytes]; if (fitsData == nullptr || compressedData == nullptr) { if (compressedData) delete [] compressedData; DEBUG(Logger::DBG_ERROR, "Error: Ran out of memory compressing image"); return false; } int r = compress2(compressedData, &compressedBytes, (const Bytef *)fitsData, totalBytes, 9); if (r != Z_OK) { /* this should NEVER happen */ DEBUG(Logger::DBG_ERROR, "Error: Failed to compress image"); delete [] compressedData; return false; } targetChip->FitsB.blob = compressedData; targetChip->FitsB.bloblen = compressedBytes; snprintf(targetChip->FitsB.format, MAXINDIBLOBFMT, ".%s.z", targetChip->getImageExtension()); } else { targetChip->FitsB.blob = const_cast(fitsData); targetChip->FitsB.bloblen = totalBytes; snprintf(targetChip->FitsB.format, MAXINDIBLOBFMT, ".%s", targetChip->getImageExtension()); } targetChip->FitsB.size = totalBytes; targetChip->FitsBP.s = IPS_OK; if (sendImage) IDSetBLOB(&targetChip->FitsBP, nullptr); if (compressedData) delete [] compressedData; DEBUG(Logger::DBG_DEBUG, "Upload complete"); return true; } void CCD::SetCCDParams(int x, int y, int bpp, float xf, float yf) { PrimaryCCD.setResolution(x, y); PrimaryCCD.setFrame(0, 0, x, y); if (CanBin()) PrimaryCCD.setBin(1, 1); PrimaryCCD.setPixelSize(xf, yf); PrimaryCCD.setBPP(bpp); } void CCD::SetGuiderParams(int x, int y, int bpp, float xf, float yf) { capability |= CCD_HAS_GUIDE_HEAD; GuideCCD.setResolution(x, y); GuideCCD.setFrame(0, 0, x, y); GuideCCD.setPixelSize(xf, yf); GuideCCD.setBPP(bpp); } bool CCD::saveConfigItems(FILE *fp) { DefaultDevice::saveConfigItems(fp); IUSaveConfigText(fp, &ActiveDeviceTP); IUSaveConfigSwitch(fp, &UploadSP); IUSaveConfigText(fp, &UploadSettingsTP); IUSaveConfigSwitch(fp, &TelescopeTypeSP); #ifdef WITH_EXPOSURE_LOOPING IUSaveConfigSwitch(fp, &ExposureLoopSP); #endif IUSaveConfigSwitch(fp, &PrimaryCCD.CompressSP); if (HasGuideHead()) IUSaveConfigSwitch(fp, &GuideCCD.CompressSP); if (CanSubFrame()) IUSaveConfigNumber(fp, &PrimaryCCD.ImageFrameNP); if (CanBin()) IUSaveConfigNumber(fp, &PrimaryCCD.ImageBinNP); if (HasBayer()) IUSaveConfigText(fp, &BayerTP); if (HasStreaming()) Streamer->saveConfigItems(fp); return true; } IPState CCD::GuideNorth(float ms) { INDI_UNUSED(ms); DEBUG(Logger::DBG_ERROR, "The CCD does not support guiding."); return IPS_ALERT; } IPState CCD::GuideSouth(float ms) { INDI_UNUSED(ms); DEBUG(Logger::DBG_ERROR, "The CCD does not support guiding."); return IPS_ALERT; } IPState CCD::GuideEast(float ms) { INDI_UNUSED(ms); DEBUG(Logger::DBG_ERROR, "The CCD does not support guiding."); return IPS_ALERT; } IPState CCD::GuideWest(float ms) { INDI_UNUSED(ms); DEBUG(Logger::DBG_ERROR, "The CCD does not support guiding."); return IPS_ALERT; } void CCD::getMinMax(double *min, double *max, CCDChip *targetChip) { int ind = 0, i, j; int imageHeight = targetChip->getSubH() / targetChip->getBinY(); int imageWidth = targetChip->getSubW() / targetChip->getBinX(); double lmin = 0, lmax = 0; switch (targetChip->getBPP()) { case 8: { unsigned char *imageBuffer = (unsigned char *)targetChip->getFrameBuffer(); lmin = lmax = imageBuffer[0]; for (i = 0; i < imageHeight; i++) for (j = 0; j < imageWidth; j++) { ind = (i * imageWidth) + j; if (imageBuffer[ind] < lmin) lmin = imageBuffer[ind]; else if (imageBuffer[ind] > lmax) lmax = imageBuffer[ind]; } } break; case 16: { unsigned short *imageBuffer = (unsigned short *)targetChip->getFrameBuffer(); lmin = lmax = imageBuffer[0]; for (i = 0; i < imageHeight; i++) for (j = 0; j < imageWidth; j++) { ind = (i * imageWidth) + j; if (imageBuffer[ind] < lmin) lmin = imageBuffer[ind]; else if (imageBuffer[ind] > lmax) lmax = imageBuffer[ind]; } } break; case 32: { unsigned int *imageBuffer = (unsigned int *)targetChip->getFrameBuffer(); lmin = lmax = imageBuffer[0]; for (i = 0; i < imageHeight; i++) for (j = 0; j < imageWidth; j++) { ind = (i * imageWidth) + j; if (imageBuffer[ind] < lmin) lmin = imageBuffer[ind]; else if (imageBuffer[ind] > lmax) lmax = imageBuffer[ind]; } } break; } *min = lmin; *max = lmax; } std::string regex_replace_compat(const std::string &input, const std::string &pattern, const std::string &replace) { std::stringstream s; std::regex_replace(std::ostreambuf_iterator(s), input.begin(), input.end(), std::regex(pattern), replace); return s.str(); } int CCD::getFileIndex(const char *dir, const char *prefix, const char *ext) { INDI_UNUSED(ext); DIR *dpdf = nullptr; struct dirent *epdf = nullptr; std::vector files = std::vector(); std::string prefixIndex = prefix; prefixIndex = regex_replace_compat(prefixIndex, "_ISO8601", ""); prefixIndex = regex_replace_compat(prefixIndex, "_XXX", ""); // Create directory if does not exist struct stat st; if (stat(dir, &st) == -1) { DEBUGF(Logger::DBG_DEBUG, "Creating directory %s...", dir); if (_ccd_mkdir(dir, 0755) == -1) DEBUGF(Logger::DBG_ERROR, "Error creating directory %s (%s)", dir, strerror(errno)); } dpdf = opendir(dir); if (dpdf != nullptr) { while ((epdf = readdir(dpdf))) { if (strstr(epdf->d_name, prefixIndex.c_str())) files.push_back(epdf->d_name); } } else { closedir(dpdf); return -1; } int maxIndex = 0; for (int i = 0; i < (int)files.size(); i++) { int index = -1; std::string file = files.at(i); std::size_t start = file.find_last_of("_"); std::size_t end = file.find_last_of("."); if (start != std::string::npos) { index = atoi(file.substr(start + 1, end).c_str()); if (index > maxIndex) maxIndex = index; } } closedir(dpdf); return (maxIndex + 1); } void CCD::GuideComplete(INDI_EQ_AXIS axis) { GuiderInterface::GuideComplete(axis); } bool CCD::StartStreaming() { DEBUG(Logger::DBG_ERROR, "Streaming is not supported."); return false; } bool CCD::StopStreaming() { DEBUG(Logger::DBG_ERROR, "Streaming is not supported."); return false; } } libindi/libs/indibase/connectionplugins/0000775000175000017500000000000013263645557017742 5ustar jasemjasemlibindi/libs/indibase/connectionplugins/connectiontcp.cpp0000664000175000017500000001354413263645557023323 0ustar jasemjasem/******************************************************************************* Copyright(c) 2017 Jasem Mutlaq. All rights reserved. This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2 as published by the Free Software Foundation. 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 Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. *******************************************************************************/ #include "connectiontcp.h" #include "indilogger.h" #include "indistandardproperty.h" #include #include #include #include namespace Connection { extern const char *CONNECTION_TAB; TCP::TCP(INDI::DefaultDevice *dev) : Interface(dev) { // Address/Port IUFillText(&AddressT[0], "ADDRESS", "Address", ""); IUFillText(&AddressT[1], "PORT", "Port", ""); IUFillTextVector(&AddressTP, AddressT, 2, getDeviceName(), "DEVICE_ADDRESS", "Server", CONNECTION_TAB, IP_RW, 60, IPS_IDLE); IUFillSwitch(&TcpUdpS[0], "TCP", "TCP", ISS_ON); IUFillSwitch(&TcpUdpS[1], "UDP", "UDP", ISS_OFF); IUFillSwitchVector(&TcpUdpSP, TcpUdpS, 2, getDeviceName(), "CONNECTION_TYPE", "Connection Type", CONNECTION_TAB, IP_RW, ISR_1OFMANY, 60, IPS_IDLE); } bool TCP::ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n) { if (!strcmp(dev, device->getDeviceName())) { // TCP Server settings if (!strcmp(name, AddressTP.name)) { IUUpdateText(&AddressTP, texts, names, n); AddressTP.s = IPS_OK; IDSetText(&AddressTP, nullptr); return true; } } return false; } bool TCP::ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) { if (!strcmp(dev, device->getDeviceName())) { if (!strcmp(name, TcpUdpSP.name)) { IUUpdateSwitch(&TcpUdpSP, states, names, n); TcpUdpSP.s = IPS_OK; IDSetSwitch(&TcpUdpSP, nullptr); return true; } } return false; } bool TCP::Connect() { if (AddressT[0].text == nullptr || AddressT[0].text[0] == '\0' || AddressT[1].text == nullptr || AddressT[1].text[0] == '\0') { LOG_ERROR("Error! Server address is missing or invalid."); return false; } const char *hostname = AddressT[0].text; const char *port = AddressT[1].text; LOGF_INFO("Connecting to %s@%s ...", hostname, port); if (device->isSimulation() == false) { struct sockaddr_in serv_addr; struct hostent *hp = nullptr; int ret = 0; struct timeval ts; ts.tv_sec = SOCKET_TIMEOUT; ts.tv_usec = 0; if (sockfd != -1) close(sockfd); // Lookup host name or IPv4 address hp = gethostbyname(hostname); if (!hp) { LOG_ERROR("Failed to lookup IP Address or hostname."); return false; } memset(&serv_addr, 0, sizeof(serv_addr)); serv_addr.sin_family = AF_INET; serv_addr.sin_addr.s_addr = ((struct in_addr *)(hp->h_addr_list[0]))->s_addr; serv_addr.sin_port = htons(atoi(port)); int socketType = 0; if (TcpUdpS[0].s == ISS_ON) { socketType = SOCK_STREAM; } else { socketType = SOCK_DGRAM; } if ((sockfd = socket(AF_INET, socketType, 0)) < 0) { LOG_ERROR("Failed to create socket."); return false; } // Connect to the mount if ((ret = ::connect(sockfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr))) < 0) { LOGF_ERROR("Failed to connect to mount %s@%s: %s.", hostname, port, strerror(errno)); close(sockfd); sockfd = -1; return false; } // Set the socket receiving and sending timeouts setsockopt(sockfd, SOL_SOCKET, SO_RCVTIMEO, (char *)&ts, sizeof(struct timeval)); setsockopt(sockfd, SOL_SOCKET, SO_SNDTIMEO, (char *)&ts, sizeof(struct timeval)); } PortFD = sockfd; LOG_DEBUG("Connection successful, attempting handshake..."); bool rc = Handshake(); if (rc) { LOGF_INFO("%s is online.", getDeviceName()); device->saveConfig(true, "DEVICE_ADDRESS"); device->saveConfig(true, "CONNECTION_TYPE"); } else LOG_DEBUG("Handshake failed."); return rc; } bool TCP::Disconnect() { if (sockfd > 0) { close(sockfd); sockfd = PortFD = -1; } return true; } void TCP::Activated() { device->defineText(&AddressTP); device->defineSwitch(&TcpUdpSP); device->loadConfig(true, "DEVICE_ADDRESS"); device->loadConfig(true, "CONNECTION_TYPE"); } void TCP::Deactivated() { device->deleteProperty(AddressTP.name); device->deleteProperty(TcpUdpSP.name); } bool TCP::saveConfigItems(FILE *fp) { IUSaveConfigText(fp, &AddressTP); IUSaveConfigSwitch(fp, &TcpUdpSP); return true; } void TCP::setDefaultHost(const char *addressHost) { IUSaveText(&AddressT[0], addressHost); } void TCP::setDefaultPort(uint32_t addressPort) { char portStr[8]; snprintf(portStr, 8, "%d", addressPort); IUSaveText(&AddressT[1], portStr); } void TCP::setConnectionType(int type) { IUResetSwitch(&TcpUdpSP); TcpUdpS[type].s = ISS_ON; IDSetSwitch(&TcpUdpSP, nullptr); } } libindi/libs/indibase/connectionplugins/connectiontcp.h0000664000175000017500000000477613263645557022777 0ustar jasemjasem/******************************************************************************* Copyright(c) 2017 Jasem Mutlaq. All rights reserved. Connection Plugin Interface This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2 as published by the Free Software Foundation. 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 Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. *******************************************************************************/ #pragma once #include "connectioninterface.h" #include #include #include namespace Connection { /** * @brief The TCP class manages connection with devices over the network via TCP/IP. * Upon successfull connection, reads & writes from and to the device are performed via the returned file descriptor * using standard UNIX read/write functions. */ class TCP : public Interface { public: enum ConnectionType { TYPE_TCP = 0, TYPE_UDP }; TCP(INDI::DefaultDevice *dev); virtual ~TCP() = default; virtual bool Connect(); virtual bool Disconnect(); virtual void Activated(); virtual void Deactivated(); virtual std::string name() { return "CONNECTION_TCP"; } virtual std::string label() { return "Ethernet"; } virtual const char *host() { return AddressT[0].text; } virtual uint32_t port() { return atoi(AddressT[0].text); } virtual bool ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n); virtual bool ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n); virtual bool saveConfigItems(FILE *fp); int getPortFD() const { return PortFD; } void setDefaultHost(const char *addressHost); void setDefaultPort(uint32_t addressPort); void setConnectionType(int type); protected: // IP Address/Port ITextVectorProperty AddressTP; IText AddressT[2] {}; ISwitch TcpUdpS[2]; ISwitchVectorProperty TcpUdpSP; int sockfd = -1; const uint8_t SOCKET_TIMEOUT = 5; int PortFD = -1; }; } libindi/libs/indibase/connectionplugins/connectionserial.cpp0000664000175000017500000002403713263645557024013 0ustar jasemjasem/******************************************************************************* Copyright(c) 2017 Jasem Mutlaq. All rights reserved. This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2 as published by the Free Software Foundation. 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 Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. *******************************************************************************/ #include "connectionserial.h" #include "indistandardproperty.h" #include "indicom.h" #include "indilogger.h" #include #include #include namespace Connection { extern const char *CONNECTION_TAB; Serial::Serial(INDI::DefaultDevice *dev) : Interface(dev) { #ifdef __APPLE__ IUFillText(&PortT[0], "PORT", "Port", "/dev/cu.usbserial"); #else IUFillText(&PortT[0], "PORT", "Port", "/dev/ttyUSB0"); #endif IUFillTextVector(&PortTP, PortT, 1, dev->getDeviceName(), INDI::SP::DEVICE_PORT, "Ports", CONNECTION_TAB, IP_RW, 60, IPS_IDLE); IUFillSwitch(&AutoSearchS[0], "ENABLED", "Enabled", ISS_ON); IUFillSwitch(&AutoSearchS[1], "DISABLED", "Disabled", ISS_OFF); IUFillSwitchVector(&AutoSearchSP, AutoSearchS, 2, dev->getDeviceName(), INDI::SP::DEVICE_AUTO_SEARCH, "Auto Search", CONNECTION_TAB, IP_RW, ISR_1OFMANY, 60, IPS_IDLE); IUFillSwitch(&RefreshS[0], "Scan Ports", "Scan Ports", ISS_OFF); IUFillSwitchVector(&RefreshSP, RefreshS, 1, dev->getDeviceName(), "DEVICE_PORT_SCAN", "Refresh", CONNECTION_TAB, IP_RW, ISR_1OFMANY, 60, IPS_IDLE); IUFillSwitch(&BaudRateS[0], "9600", "", ISS_ON); IUFillSwitch(&BaudRateS[1], "19200", "", ISS_OFF); IUFillSwitch(&BaudRateS[2], "38400", "", ISS_OFF); IUFillSwitch(&BaudRateS[3], "57600", "", ISS_OFF); IUFillSwitch(&BaudRateS[4], "115200", "", ISS_OFF); IUFillSwitch(&BaudRateS[5], "230400", "", ISS_OFF); IUFillSwitchVector(&BaudRateSP, BaudRateS, 6, dev->getDeviceName(), INDI::SP::DEVICE_BAUD_RATE, "Baud Rate", CONNECTION_TAB, IP_RW, ISR_1OFMANY, 60, IPS_IDLE); } Serial::~Serial() { delete[] SystemPortS; } bool Serial::ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n) { if (!strcmp(dev, device->getDeviceName())) { // Serial Port if (!strcmp(name, PortTP.name)) { IUUpdateText(&PortTP, texts, names, n); PortTP.s = IPS_OK; IDSetText(&PortTP, nullptr); return true; } } return false; } bool Serial::ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) { if (!strcmp(dev, device->getDeviceName())) { if (!strcmp(name, BaudRateSP.name)) { IUUpdateSwitch(&BaudRateSP, states, names, n); BaudRateSP.s = IPS_OK; IDSetSwitch(&BaudRateSP, nullptr); return true; } if (!strcmp(name, AutoSearchSP.name)) { bool wasEnabled = (AutoSearchS[0].s == ISS_ON); IUUpdateSwitch(&AutoSearchSP, states, names, n); AutoSearchSP.s = IPS_OK; // Only display message if there is an actual change if (wasEnabled == false && AutoSearchS[0].s == ISS_ON) LOG_INFO("Auto search is enabled. When connecting, the driver shall attempt to " "communicate with all available system ports until a connection is " "established."); else if (wasEnabled && AutoSearchS[1].s == ISS_ON) LOG_INFO("Auo search is disabled."); IDSetSwitch(&AutoSearchSP, nullptr); return true; } if (!strcmp(name, RefreshSP.name)) { RefreshSP.s = Refresh() ? IPS_OK : IPS_ALERT; IDSetSwitch(&RefreshSP, nullptr); return true; } if (!strcmp(name, SystemPortSP.name)) { IUUpdateSwitch(&SystemPortSP, states, names, n); ISwitch *sp = IUFindOnSwitch(&SystemPortSP); if (sp) { IUSaveText(&PortT[0], sp->name); IDSetText(&PortTP, nullptr); } SystemPortSP.s = IPS_OK; IDSetSwitch(&SystemPortSP, nullptr); return true; } } return false; } bool Serial::Connect() { uint32_t baud = atoi(IUFindOnSwitch(&BaudRateSP)->name); bool rc = Connect(PortT[0].text, baud); if (rc) rc = processHandshake(); // Start auto-search if option was selected and IF we have system ports to try connecting to if (rc == false && AutoSearchS[0].s == ISS_ON && SystemPortS != nullptr) { LOGF_WARN("Communication with %s @ %d failed. Starting Auto Search...", PortT[0].text, baud); for (int i = 0; i < SystemPortSP.nsp; i++) { LOGF_DEBUG("Trying connection to %s @ %d ...", SystemPortS[i].name, baud); if (Connect(SystemPortS[i].name, baud)) { IUSaveText(&PortT[0], SystemPortS[i].name); IDSetText(&PortTP, nullptr); rc = processHandshake(); if (rc) return true; } } return false; } return rc; } bool Serial::processHandshake() { LOG_DEBUG("Connection successful, attempting handshake..."); bool rc = Handshake(); if (rc) { LOGF_INFO("%s is online.", getDeviceName()); device->saveConfig(true, INDI::SP::DEVICE_PORT); device->saveConfig(true, INDI::SP::DEVICE_BAUD_RATE); } else LOG_DEBUG("Handshake failed."); return rc; } bool Serial::Connect(const char *port, uint32_t baud) { if (device->isSimulation()) return true; int connectrc = 0; char errorMsg[MAXRBUF]; LOGF_DEBUG("Connecting to %s", port); if ((connectrc = tty_connect(port, baud, wordSize, parity, stopBits, &PortFD)) != TTY_OK) { tty_error_msg(connectrc, errorMsg, MAXRBUF); LOGF_ERROR("Failed to connect to port (%s). Error: %s", port, errorMsg); return false; } LOGF_DEBUG("Port FD %d", PortFD); return true; } bool Serial::Disconnect() { if (PortFD > 0) { tty_disconnect(PortFD); PortFD = -1; } return true; } void Serial::Activated() { device->defineText(&PortTP); device->loadConfig(true, INDI::SP::DEVICE_PORT); device->defineSwitch(&BaudRateSP); device->loadConfig(true, INDI::SP::DEVICE_BAUD_RATE); device->defineSwitch(&AutoSearchSP); device->loadConfig(true, INDI::SP::DEVICE_AUTO_SEARCH); device->defineSwitch(&RefreshSP); Refresh(true); } void Serial::Deactivated() { device->deleteProperty(PortTP.name); device->deleteProperty(BaudRateSP.name); device->deleteProperty(AutoSearchSP.name); device->deleteProperty(RefreshSP.name); device->deleteProperty(SystemPortSP.name); delete[] SystemPortS; SystemPortS = nullptr; } bool Serial::saveConfigItems(FILE *fp) { IUSaveConfigText(fp, &PortTP); IUSaveConfigSwitch(fp, &BaudRateSP); IUSaveConfigSwitch(fp, &AutoSearchSP); return true; } void Serial::setDefaultPort(const char *defaultPort) { IUSaveText(&PortT[0], defaultPort); } void Serial::setDefaultBaudRate(BaudRate newRate) { IUResetSwitch(&BaudRateSP); BaudRateS[newRate].s = ISS_ON; } uint32_t Serial::baud() { return atoi(IUFindOnSwitch(&BaudRateSP)->name); } int dev_file_select(const dirent *entry) { #if defined(__APPLE__) static const char *filter_names[] = { "cu.", nullptr }; #else static const char *filter_names[] = { "ttyUSB", "ttyACM", "rfcomm", nullptr }; #endif const char **filter; for (filter = filter_names; *filter; ++filter) { if (strstr(entry->d_name, *filter) != nullptr) { return (true); } } return (false); } bool Serial::Refresh(bool silent) { if (SystemPortS) device->deleteProperty(SystemPortSP.name); delete[] SystemPortS; SystemPortS = nullptr; std::vector m_Ports; struct dirent **namelist; int devCount = scandir("/dev", &namelist, dev_file_select, alphasort); if (devCount < 0) { if (!silent) LOGF_ERROR("Failed to scan directory /dev. Error: %s", strerror(errno)); } else { while (devCount--) { if (m_Ports.size() < 10) { std::string s(namelist[devCount]->d_name); s.erase(s.find_last_not_of(" \n\r\t") + 1); m_Ports.push_back("/dev/" + s); } else { LOGF_DEBUG("Ignoring devices over %d : %s", m_Ports.size(), namelist[devCount]->d_name); } free(namelist[devCount]); } free(namelist); } int pCount = m_Ports.size(); if (pCount == 0) { if (!silent) LOG_WARN("No candidate ports found on the system."); return false; } else { if (!silent) LOGF_INFO("Scan complete. Found %d port(s).", pCount); } SystemPortS = new ISwitch[pCount]; ISwitch *sp = SystemPortS; for (int i = pCount - 1; i >= 0; i--) { IUFillSwitch(sp++, m_Ports[i].c_str(), m_Ports[i].c_str(), ISS_OFF); } IUFillSwitchVector(&SystemPortSP, SystemPortS, pCount, device->getDeviceName(), "SYSTEM_PORTS", "System Ports", CONNECTION_TAB, IP_RW, ISR_ATMOST1, 60, IPS_IDLE); device->defineSwitch(&SystemPortSP); return true; } } libindi/libs/indibase/connectionplugins/connectioninterface.cpp0000664000175000017500000000417213263645557024472 0ustar jasemjasem/******************************************************************************* Copyright(c) 2017 Jasem Mutlaq. All rights reserved. This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2 as published by the Free Software Foundation. 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 Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. *******************************************************************************/ #include "connectioninterface.h" #include "defaultdevice.h" namespace Connection { const char *CONNECTION_TAB = "Connection"; Interface::Interface(INDI::DefaultDevice *dev) : device(dev) { // Default handshake registerHandshake([]() { return true; }); } Interface::~Interface() { } const char *Interface::getDeviceName() { return device->getDeviceName(); } bool Interface::ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) { INDI_UNUSED(dev); INDI_UNUSED(name); INDI_UNUSED(states); INDI_UNUSED(names); INDI_UNUSED(n); return false; } bool Interface::ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) { INDI_UNUSED(dev); INDI_UNUSED(name); INDI_UNUSED(values); INDI_UNUSED(names); INDI_UNUSED(n); return false; } bool Interface::ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n) { INDI_UNUSED(dev); INDI_UNUSED(name); INDI_UNUSED(texts); INDI_UNUSED(names); INDI_UNUSED(n); return false; } bool Interface::saveConfigItems(FILE *fp) { INDI_UNUSED(fp); return true; } void Interface::registerHandshake(std::function callback) { Handshake = callback; } } libindi/libs/indibase/connectionplugins/connectioninterface.h0000664000175000017500000000741413263645557024141 0ustar jasemjasem/******************************************************************************* Copyright(c) 2017 Jasem Mutlaq. All rights reserved. Connection Plugin Interface This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2 as published by the Free Software Foundation. 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 Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. *******************************************************************************/ #pragma once #include "indidevapi.h" #include #include namespace INDI { class DefaultDevice; } /** * Namespace to encapsulate all INDI Connection Plugins. Each INDI connection plugin is responsible of managing communications * with a specific physical or logical medium (e.g. serial or ethernet). */ namespace Connection { /** * @brief The Interface class class is the base class for all INDI connection plugins. * * Each plugin implements the connection details specific to a particular medium (e.g. serial). After the connection to the medium is successful, a handshake is * initialted to make sure the device is online and responding to commands. The child class employing the plugin must register * the handshake to perform the actual low-level communication with the device. * * @see Connection::Serial * @see Connection::TCP * @see INDI::Telescope utilizes both serial and tcp plugins to communicate with mounts. */ class Interface { public: /** * @brief Connect Connect to device via the implemented communication medium. Do not perform any handshakes. * @return True if successful, false otherwise. */ virtual bool Connect() = 0; /** * @brief Disconnect Disconnect from device. * @return True if successful, false otherwise. */ virtual bool Disconnect() = 0; /** * @brief Activated Function called by the framework when the plugin is activated (i.e. selected by the user). It is usually used to define properties * pertaining to the specific plugin functionalities. */ virtual void Activated() = 0; /** * @brief Deactivated Function called by the framework when the plugin is deactivated. It is usually used to delete properties by were defined * previously since the plugin is no longer active. */ virtual void Deactivated() = 0; /** * @return Plugin name */ virtual std::string name() = 0; /** * @return Plugin friendly label presented to the client/user. */ virtual std::string label() = 0; virtual bool ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n); virtual bool ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n); virtual bool ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n); virtual bool saveConfigItems(FILE *fp); /** * @brief registerHandshake Register a handshake function to be called once the intial connection to the device is established. * @param callback Handshake function callback * @see INDI::Telescope */ void registerHandshake(std::function callback); protected: Interface(INDI::DefaultDevice *dev); virtual ~Interface(); const char *getDeviceName(); std::function Handshake; INDI::DefaultDevice *device = NULL; }; } libindi/libs/indibase/connectionplugins/connectionserial.h0000664000175000017500000001222613263645557023455 0ustar jasemjasem/******************************************************************************* Copyright(c) 2017 Jasem Mutlaq. All rights reserved. Connection Plugin Interface This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2 as published by the Free Software Foundation. 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 Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. *******************************************************************************/ #pragma once #include "connectioninterface.h" #include namespace Connection { /** * @brief The Serial class manages connection with serial devices including Bluetooth. Serial communication is still the predominat * method to communicate with astronomical devices such as mounts, focusers, filter wheels..etc. The default connection * parameters are 9600 8N1 (9600 Baud Rate, 8 data bits, no parity, 1 stop bit). All the parameters can be updated and read via * the getters and setters of the class. * The default port is /dev/ttyUSB0 under Linux and /dev/cu.usbserial under MacOS. After serial connection is established * successfully, */ class Serial : public Interface { public: /** * \typedef BaudRate * \brief Supported baud rates * \note: Default baud rate is 9600. To change default baud rate, use setDefaultBaudrate(..) function. */ typedef enum { B_9600, B_19200, B_38400, B_57600, B_115200, B_230400 } BaudRate; Serial(INDI::DefaultDevice *dev); virtual ~Serial(); virtual bool Connect(); virtual bool Disconnect(); virtual void Activated(); virtual void Deactivated(); virtual std::string name() { return "CONNECTION_SERIAL"; } virtual std::string label() { return "Serial"; } /** * @return Currently active device port */ virtual const char *port() { return PortT[0].text; } /** * @return Currently active baud rate raw value (e.g. 9600, 19200..etc) */ virtual uint32_t baud(); /** * @brief setDefaultPort Set default port. Call this function in initProperties() of your driver * if you want to change default port. * @param defaultPort Name of desired default port */ void setDefaultPort(const char *defaultPort); /** * @brief setDefaultBaudRate Set default baud rate. The default baud rate is 9600 unless * otherwise changed by this function. Call this function in initProperties() of your driver. * @param newRate Desired new rate */ void setDefaultBaudRate(BaudRate newRate); /** * @return Return port file descriptor. If connection is successful, PortFD is a positive * integer otherwise it is set to -1 */ int getPortFD() const { return PortFD; } virtual bool ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n); virtual bool ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n); virtual bool saveConfigItems(FILE *fp); /** * Refresh the list of system ports */ bool Refresh(bool silent = false); uint8_t getWordSize() const { return wordSize; } /** * @brief setWordSize Set word size to be used in the serial connection. Default 8 */ void setWordSize(const uint8_t &value) { wordSize = value; } uint8_t getParity() const { return parity ; } /** * @brief setParity Set parity to be used in the serial connection. Default 0 (NONE) * @param value 0 for NONE, 1 for EVEN, 2 for ODD */ void setParity(const uint8_t &value) { parity = value; } uint8_t getStopBits() const { return stopBits; } /** * @brief setStopBits Set stop bits to be used in the serial connection. Default 0 */ void setStopBits(const uint8_t &value) { stopBits = value ; } protected: /** * \brief Connect to serial port device. Default parameters are 8 bits, 1 stop bit, no parity. * Override if different from default. * \param port Port to connect to. * \param baud Baud rate * \return True if connection is successful, false otherwise * \warning Do not call this function directly, it is called by Connection::Serial Connect() function. */ virtual bool Connect(const char *port, uint32_t baud); virtual bool processHandshake(); // Device physical port ITextVectorProperty PortTP; IText PortT[1] {}; ISwitch BaudRateS[6]; ISwitchVectorProperty BaudRateSP; ISwitch AutoSearchS[2]; ISwitchVectorProperty AutoSearchSP; ISwitch *SystemPortS = nullptr; ISwitchVectorProperty SystemPortSP; ISwitch RefreshS[1]; ISwitchVectorProperty RefreshSP; int PortFD = -1; // Default 8N1 parameters uint8_t wordSize=8; uint8_t parity=0; uint8_t stopBits=1; }; } libindi/libs/indibase/indidome.cpp0000664000175000017500000017243613263645557016512 0ustar jasemjasem/******************************************************************************* INDI Dome Base Class Copyright(c) 2014 Jasem Mutlaq. All rights reserved. The code used calculate dome target AZ and ZD is written by Ferran Casarramona, and adapted from code from Markus Wildi. The transformations are based on the paper Matrix Method for Coodinates Transformation written by Toshimi Taki (http://www.asahi-net.or.jp/~zs3t-tk). This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2 as published by the Free Software Foundation. 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 Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. *******************************************************************************/ #include "indidome.h" #include "indicom.h" #include "indicontroller.h" #include "connectionplugins/connectionserial.h" #include "connectionplugins/connectiontcp.h" #include #include #include #include #include #include #include #define DOME_SLAVING_TAB "Slaving" #define DOME_COORD_THRESHOLD \ 0.1 /* Only send debug messages if the differences between old and new values of Az/Alt excceds this value */ namespace INDI { Dome::Dome() { controller = new Controller(this); controller->setButtonCallback(buttonHelper); prev_az = prev_alt = prev_ra = prev_dec = 0; mountEquatorialCoords.dec = mountEquatorialCoords.ra = -1; mountState = IPS_ALERT; weatherState = IPS_IDLE; capability = 0; shutterState = SHUTTER_UNKNOWN; domeState = DOME_IDLE; parkDataType = PARK_NONE; Parkdatafile = "~/.indi/ParkData.xml"; ParkdataXmlRoot = nullptr; } Dome::~Dome() { if (ParkdataXmlRoot) delXMLEle(ParkdataXmlRoot); delete controller; delete serialConnection; delete tcpConnection; } bool Dome::initProperties() { DefaultDevice::initProperties(); // let the base class flesh in what it wants // Presets IUFillNumber(&PresetN[0], "Preset 1", "", "%6.2f", 0, 360.0, 1.0, 0); IUFillNumber(&PresetN[1], "Preset 2", "", "%6.2f", 0, 360.0, 1.0, 0); IUFillNumber(&PresetN[2], "Preset 3", "", "%6.2f", 0, 360.0, 1.0, 0); IUFillNumberVector(&PresetNP, PresetN, 3, getDeviceName(), "Presets", "", "Presets", IP_RW, 0, IPS_IDLE); //Preset GOTO IUFillSwitch(&PresetGotoS[0], "Preset 1", "", ISS_OFF); IUFillSwitch(&PresetGotoS[1], "Preset 2", "", ISS_OFF); IUFillSwitch(&PresetGotoS[2], "Preset 3", "", ISS_OFF); IUFillSwitchVector(&PresetGotoSP, PresetGotoS, 3, getDeviceName(), "Goto", "", "Presets", IP_RW, ISR_1OFMANY, 0, IPS_IDLE); IUFillSwitch(&AutoParkS[0], "Enable", "", ISS_OFF); IUFillSwitch(&AutoParkS[1], "Disable", "", ISS_ON); IUFillSwitchVector(&AutoParkSP, AutoParkS, 2, getDeviceName(), "DOME_AUTOPARK", "Auto Park", OPTIONS_TAB, IP_RW, ISR_1OFMANY, 0, IPS_IDLE); // Active Devices IUFillText(&ActiveDeviceT[0], "ACTIVE_TELESCOPE", "Telescope", "Telescope Simulator"); IUFillText(&ActiveDeviceT[1], "ACTIVE_WEATHER", "Weather", "WunderGround"); IUFillTextVector(&ActiveDeviceTP, ActiveDeviceT, 2, getDeviceName(), "ACTIVE_DEVICES", "Snoop devices", OPTIONS_TAB, IP_RW, 60, IPS_IDLE); // Use locking if telescope is unparked IUFillSwitch(&TelescopeClosedLockT[0], "NO_ACTION", "Ignore Telescope", ISS_ON); IUFillSwitch(&TelescopeClosedLockT[1], "LOCK_PARKING", "Telescope locks", ISS_OFF); IUFillSwitchVector(&TelescopeClosedLockTP, TelescopeClosedLockT, 2, getDeviceName(), "TELESCOPE_POLICY", "Telescope parking policy", OPTIONS_TAB, IP_RW, ISR_1OFMANY, 60, IPS_IDLE); // Measurements IUFillNumber(&DomeMeasurementsN[DM_DOME_RADIUS], "DM_DOME_RADIUS", "Radius (m)", "%6.2f", 0.0, 50.0, 1.0, 0.0); IUFillNumber(&DomeMeasurementsN[DM_SHUTTER_WIDTH], "DM_SHUTTER_WIDTH", "Shutter width (m)", "%6.2f", 0.0, 10.0, 1.0, 0.0); IUFillNumber(&DomeMeasurementsN[DM_NORTH_DISPLACEMENT], "DM_NORTH_DISPLACEMENT", "N displacement (m)", "%6.2f", -10.0, 10.0, 1.0, 0.0); IUFillNumber(&DomeMeasurementsN[DM_EAST_DISPLACEMENT], "DM_EAST_DISPLACEMENT", "E displacement (m)", "%6.2f", -10.0, 10.0, 1.0, 0.0); IUFillNumber(&DomeMeasurementsN[DM_UP_DISPLACEMENT], "DM_UP_DISPLACEMENT", "Up displacement (m)", "%6.2f", -10, 10.0, 1.0, 0.0); IUFillNumber(&DomeMeasurementsN[DM_OTA_OFFSET], "DM_OTA_OFFSET", "OTA offset (m)", "%6.2f", -10.0, 10.0, 1.0, 0.0); IUFillNumberVector(&DomeMeasurementsNP, DomeMeasurementsN, 6, getDeviceName(), "DOME_MEASUREMENTS", "Measurements", DOME_SLAVING_TAB, IP_RW, 60, IPS_OK); IUFillSwitch(&OTASideS[0], "DM_OTA_SIDE_EAST", "East", ISS_OFF); IUFillSwitch(&OTASideS[1], "DM_OTA_SIDE_WEST", "West", ISS_OFF); IUFillSwitchVector(&OTASideSP, OTASideS, 2, getDeviceName(), "DM_OTA_SIDE", "Meridian side", DOME_SLAVING_TAB, IP_RW, ISR_ATMOST1, 60, IPS_OK); IUFillSwitch(&DomeAutoSyncS[0], "DOME_AUTOSYNC_ENABLE", "Enable", ISS_OFF); IUFillSwitch(&DomeAutoSyncS[1], "DOME_AUTOSYNC_DISABLE", "Disable", ISS_ON); IUFillSwitchVector(&DomeAutoSyncSP, DomeAutoSyncS, 2, getDeviceName(), "DOME_AUTOSYNC", "Slaving", DOME_SLAVING_TAB, IP_RW, ISR_1OFMANY, 60, IPS_OK); IUFillNumber(&DomeSpeedN[0], "DOME_SPEED_VALUE", "RPM", "%6.2f", 0.0, 10, 0.1, 1.0); IUFillNumberVector(&DomeSpeedNP, DomeSpeedN, 1, getDeviceName(), "DOME_SPEED", "Speed", MAIN_CONTROL_TAB, IP_RW, 60, IPS_OK); IUFillSwitch(&DomeMotionS[0], "DOME_CW", "Dome CW", ISS_OFF); IUFillSwitch(&DomeMotionS[1], "DOME_CCW", "Dome CCW", ISS_OFF); IUFillSwitchVector(&DomeMotionSP, DomeMotionS, 2, getDeviceName(), "DOME_MOTION", "Motion", MAIN_CONTROL_TAB, IP_RW, ISR_ATMOST1, 60, IPS_OK); // Driver can define those to clients if there is support IUFillNumber(&DomeAbsPosN[0], "DOME_ABSOLUTE_POSITION", "Degrees", "%6.2f", 0.0, 360.0, 1.0, 0.0); IUFillNumberVector(&DomeAbsPosNP, DomeAbsPosN, 1, getDeviceName(), "ABS_DOME_POSITION", "Absolute Position", MAIN_CONTROL_TAB, IP_RW, 60, IPS_OK); IUFillNumber(&DomeRelPosN[0], "DOME_RELATIVE_POSITION", "Degrees", "%6.2f", -180, 180.0, 10.0, 0.0); IUFillNumberVector(&DomeRelPosNP, DomeRelPosN, 1, getDeviceName(), "REL_DOME_POSITION", "Relative Position", MAIN_CONTROL_TAB, IP_RW, 60, IPS_OK); IUFillSwitch(&AbortS[0], "ABORT", "Abort", ISS_OFF); IUFillSwitchVector(&AbortSP, AbortS, 1, getDeviceName(), "DOME_ABORT_MOTION", "Abort Motion", MAIN_CONTROL_TAB, IP_RW, ISR_ATMOST1, 60, IPS_IDLE); IUFillNumber(&DomeParamN[0], "AUTOSYNC_THRESHOLD", "Autosync threshold (deg)", "%6.2f", 0.0, 360.0, 1.0, 0.5); IUFillNumberVector(&DomeParamNP, DomeParamN, 1, getDeviceName(), "DOME_PARAMS", "Params", DOME_SLAVING_TAB, IP_RW, 60, IPS_OK); IUFillSwitch(&ParkS[0], "PARK", "Park", ISS_OFF); IUFillSwitch(&ParkS[1], "UNPARK", "UnPark", ISS_OFF); IUFillSwitchVector(&ParkSP, ParkS, 2, getDeviceName(), "DOME_PARK", "Parking", MAIN_CONTROL_TAB, IP_RW, ISR_1OFMANY, 60, IPS_OK); IUFillSwitch(&DomeShutterS[0], "SHUTTER_OPEN", "Open", ISS_OFF); IUFillSwitch(&DomeShutterS[1], "SHUTTER_CLOSE", "Close", ISS_ON); IUFillSwitchVector(&DomeShutterSP, DomeShutterS, 2, getDeviceName(), "DOME_SHUTTER", "Shutter", MAIN_CONTROL_TAB, IP_RW, ISR_ATMOST1, 60, IPS_OK); IUFillSwitch(&ParkOptionS[0], "PARK_CURRENT", "Current", ISS_OFF); IUFillSwitch(&ParkOptionS[1], "PARK_DEFAULT", "Default", ISS_OFF); IUFillSwitch(&ParkOptionS[2], "PARK_WRITE_DATA", "Write Data", ISS_OFF); IUFillSwitchVector(&ParkOptionSP, ParkOptionS, 3, getDeviceName(), "DOME_PARK_OPTION", "Park Options", SITE_TAB, IP_RW, ISR_ATMOST1, 60, IPS_IDLE); addDebugControl(); controller->mapController("Dome CW", "CW/Open", Controller::CONTROLLER_BUTTON, "BUTTON_1"); controller->mapController("Dome CCW", "CCW/Close", Controller::CONTROLLER_BUTTON, "BUTTON_2"); controller->initProperties(); IDSnoopDevice(ActiveDeviceT[0].text, "EQUATORIAL_EOD_COORD"); IDSnoopDevice(ActiveDeviceT[0].text, "GEOGRAPHIC_COORD"); IDSnoopDevice(ActiveDeviceT[0].text, "TELESCOPE_PARK"); if (CanAbsMove()) IDSnoopDevice(ActiveDeviceT[0].text, "TELESCOPE_PIER_SIDE"); IDSnoopDevice(ActiveDeviceT[1].text, "WEATHER_STATUS"); setDriverInterface(DOME_INTERFACE); if (domeConnection & CONNECTION_SERIAL) { serialConnection = new Connection::Serial(this); serialConnection->registerHandshake([&]() { return callHandshake(); }); registerConnection(serialConnection); } if (domeConnection & CONNECTION_TCP) { tcpConnection = new Connection::TCP(this); tcpConnection->registerHandshake([&]() { return callHandshake(); }); registerConnection(tcpConnection); } return true; } void Dome::ISGetProperties(const char *dev) { // First we let our parent populate DefaultDevice::ISGetProperties(dev); defineText(&ActiveDeviceTP); loadConfig(true, "ACTIVE_DEVICES"); defineSwitch(&TelescopeClosedLockTP); loadConfig(true, "TELESCOPE_POLICY"); controller->ISGetProperties(dev); return; } bool Dome::updateProperties() { if (isConnected()) { if (HasShutter()) defineSwitch(&DomeShutterSP); // Now we add our Dome specific stuff defineSwitch(&DomeMotionSP); if (HasVariableSpeed()) { defineNumber(&DomeSpeedNP); //defineNumber(&DomeTimerNP); } if (CanRelMove()) defineNumber(&DomeRelPosNP); if (CanAbsMove()) defineNumber(&DomeAbsPosNP); if (CanAbort()) defineSwitch(&AbortSP); if (CanAbsMove()) { defineNumber(&PresetNP); defineSwitch(&PresetGotoSP); defineSwitch(&DomeAutoSyncSP); defineSwitch(&OTASideSP); defineNumber(&DomeParamNP); defineNumber(&DomeMeasurementsNP); } if (CanPark()) { defineSwitch(&ParkSP); if (parkDataType != PARK_NONE) { defineNumber(&ParkPositionNP); defineSwitch(&ParkOptionSP); } } defineSwitch(&AutoParkSP); } else { if (HasShutter()) deleteProperty(DomeShutterSP.name); deleteProperty(DomeMotionSP.name); if (HasVariableSpeed()) { deleteProperty(DomeSpeedNP.name); //deleteProperty(DomeTimerNP.name); } if (CanRelMove()) deleteProperty(DomeRelPosNP.name); if (CanAbsMove()) deleteProperty(DomeAbsPosNP.name); if (CanAbort()) deleteProperty(AbortSP.name); if (CanAbsMove()) { deleteProperty(PresetNP.name); deleteProperty(PresetGotoSP.name); deleteProperty(DomeAutoSyncSP.name); deleteProperty(OTASideSP.name); deleteProperty(DomeParamNP.name); deleteProperty(DomeMeasurementsNP.name); } if (CanPark()) { deleteProperty(ParkSP.name); if (parkDataType != PARK_NONE) { deleteProperty(ParkPositionNP.name); deleteProperty(ParkOptionSP.name); } } deleteProperty(AutoParkSP.name); } controller->updateProperties(); return true; } bool Dome::ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) { // first check if it's for our device if (dev != nullptr && strcmp(dev, getDeviceName()) == 0) { if (!strcmp(name, PresetNP.name)) { IUUpdateNumber(&PresetNP, values, names, n); PresetNP.s = IPS_OK; IDSetNumber(&PresetNP, nullptr); return true; } if (!strcmp(name, DomeParamNP.name)) { IUUpdateNumber(&DomeParamNP, values, names, n); DomeParamNP.s = IPS_OK; IDSetNumber(&DomeParamNP, nullptr); return true; } if (!strcmp(name, DomeSpeedNP.name)) { double newSpeed = values[0]; Dome::SetSpeed(newSpeed); return true; } if (!strcmp(name, DomeAbsPosNP.name)) { double newPos = values[0]; Dome::MoveAbs(newPos); return true; } if (!strcmp(name, DomeRelPosNP.name)) { double newPos = values[0]; Dome::MoveRel(newPos); return true; } if (!strcmp(name, DomeMeasurementsNP.name)) { IUUpdateNumber(&DomeMeasurementsNP, values, names, n); DomeMeasurementsNP.s = IPS_OK; IDSetNumber(&DomeMeasurementsNP, nullptr); return true; } if (strcmp(name, ParkPositionNP.name) == 0) { IUUpdateNumber(&ParkPositionNP, values, names, n); ParkPositionNP.s = IPS_OK; Axis1ParkPosition = ParkPositionN[AXIS_RA].value; IDSetNumber(&ParkPositionNP, nullptr); return true; } } return DefaultDevice::ISNewNumber(dev, name, values, names, n); } bool Dome::ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) { if (dev != nullptr && strcmp(dev, getDeviceName()) == 0) { if (!strcmp(PresetGotoSP.name, name)) { if (domeState == DOME_PARKED) { DEBUGDEVICE(getDeviceName(), Logger::DBG_ERROR, "Please unpark before issuing any motion commands."); PresetGotoSP.s = IPS_ALERT; IDSetSwitch(&PresetGotoSP, nullptr); return false; } IUUpdateSwitch(&PresetGotoSP, states, names, n); int index = IUFindOnSwitchIndex(&PresetGotoSP); IPState rc = Dome::MoveAbs(PresetN[index].value); if (rc == IPS_OK || rc == IPS_BUSY) { PresetGotoSP.s = IPS_OK; DEBUGF(Logger::DBG_SESSION, "Moving to Preset %d (%g degrees).", index + 1, PresetN[index].value); IDSetSwitch(&PresetGotoSP, nullptr); return true; } PresetGotoSP.s = IPS_ALERT; IDSetSwitch(&PresetGotoSP, nullptr); return false; } if (!strcmp(name, DomeAutoSyncSP.name)) { IUUpdateSwitch(&DomeAutoSyncSP, states, names, n); DomeAutoSyncSP.s = IPS_OK; if (DomeAutoSyncS[0].s == ISS_ON) { IDSetSwitch(&DomeAutoSyncSP, "Dome will now be synced to mount azimuth position."); UpdateAutoSync(); } else { IDSetSwitch(&DomeAutoSyncSP, "Dome is no longer synced to mount azimuth position."); if (DomeAbsPosNP.s == IPS_BUSY || DomeRelPosNP.s == IPS_BUSY /* || DomeTimerNP.s == IPS_BUSY*/) Dome::Abort(); } return true; } if (!strcmp(name, OTASideSP.name)) { IUUpdateSwitch(&OTASideSP, states, names, n); OTASideSP.s = IPS_OK; if (OTASideS[0].s == ISS_ON) { IDSetSwitch(&OTASideSP, "Dome will be synced for telescope been at east of meridian"); UpdateAutoSync(); } else { IDSetSwitch(&OTASideSP, "Dome will be synced for telescope been at west of meridian"); UpdateAutoSync(); } return true; } if (!strcmp(name, DomeMotionSP.name)) { // Check if any switch is ON for (int i = 0; i < n; i++) { if (states[i] == ISS_ON) { if (!strcmp(DomeMotionS[DOME_CW].name, names[i])) Dome::Move(DOME_CW, MOTION_START); else Dome::Move(DOME_CCW, MOTION_START); return true; } } // All switches are off, so let's turn off last motion int current_direction = IUFindOnSwitchIndex(&DomeMotionSP); // Shouldn't happen if (current_direction < 0) { DomeMotionSP.s = IPS_IDLE; IDSetSwitch(&DomeMotionSP, nullptr); return false; } Dome::Move((DomeDirection)current_direction, MOTION_STOP); return true; } if (!strcmp(name, AbortSP.name)) { Dome::Abort(); return true; } // Dome Shutter if (!strcmp(name, DomeShutterSP.name)) { // Check if any switch is ON for (int i = 0; i < n; i++) { if (states[i] == ISS_ON) { // Open if (!strcmp(DomeShutterS[0].name, names[i])) { return (Dome::ControlShutter(SHUTTER_OPEN) != IPS_ALERT); } else { return (Dome::ControlShutter(SHUTTER_CLOSE) != IPS_ALERT); } } } } if (!strcmp(name, ParkSP.name)) { // Check if any switch is ON for (int i = 0; i < n; i++) { if (states[i] == ISS_ON) { if (!strcmp(ParkS[0].name, names[i])) { if (domeState == DOME_PARKING) return false; return (Dome::Park() != IPS_ALERT); } else { if (domeState == DOME_UNPARKING) return false; return (Dome::UnPark() != IPS_ALERT); } } } } if (!strcmp(name, ParkOptionSP.name)) { IUUpdateSwitch(&ParkOptionSP, states, names, n); ISwitch *sp = IUFindOnSwitch(&ParkOptionSP); if (!sp) return false; bool rc = false; IUResetSwitch(&ParkOptionSP); if (!strcmp(sp->name, "PARK_CURRENT")) { rc = SetCurrentPark(); } else if (!strcmp(sp->name, "PARK_DEFAULT")) { rc = SetDefaultPark(); } else if (!strcmp(sp->name, "PARK_WRITE_DATA")) { rc = WriteParkData(); if (rc) DEBUG(Logger::DBG_SESSION, "Saved Park Status/Position."); else DEBUG(Logger::DBG_WARNING, "Can not save Park Status/Position."); } ParkOptionSP.s = rc ? IPS_OK : IPS_ALERT; IDSetSwitch(&ParkOptionSP, nullptr); return true; } if (!strcmp(name, AutoParkSP.name)) { IUUpdateSwitch(&AutoParkSP, states, names, n); AutoParkSP.s = IPS_OK; if (AutoParkS[0].s == ISS_ON) DEBUG(Logger::DBG_WARNING, "Warning: Auto park is enabled. If weather conditions are in the " "danger zone, the dome will be automatically parked. Only enable this " "option is parking the dome at any time will not cause damange to any " "equipment."); else DEBUG(Logger::DBG_SESSION, "Auto park is disabled."); IDSetSwitch(&AutoParkSP, nullptr); return true; } // Telescope parking policy if (!strcmp(name, TelescopeClosedLockTP.name)) { if (n == 1) { if (!strcmp(names[0], TelescopeClosedLockT[0].name)) DEBUG(Logger::DBG_SESSION, "Telescope parking policy set to: Ignore Telescope"); else if (!strcmp(names[0], TelescopeClosedLockT[1].name)) DEBUG(Logger::DBG_SESSION, "Warning: Telescope parking policy set to: Telescope locks. This " "disallows the dome from parking when telescope is unparked, and " "can lead to damage to hardware if it rains!"); } IUUpdateSwitch(&TelescopeClosedLockTP, states, names, n); TelescopeClosedLockTP.s = IPS_OK; IDSetSwitch(&TelescopeClosedLockTP, nullptr); triggerSnoop(ActiveDeviceT[0].text, "TELESCOPE_PARK"); return true; } } controller->ISNewSwitch(dev, name, states, names, n); // Nobody has claimed this, so, ignore it return DefaultDevice::ISNewSwitch(dev, name, states, names, n); } bool Dome::ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n) { if (dev != nullptr && strcmp(dev, getDeviceName()) == 0) { if (strcmp(name, ActiveDeviceTP.name) == 0) { ActiveDeviceTP.s = IPS_OK; IUUpdateText(&ActiveDeviceTP, texts, names, n); IDSetText(&ActiveDeviceTP, nullptr); IDSnoopDevice(ActiveDeviceT[0].text, "EQUATORIAL_EOD_COORD"); IDSnoopDevice(ActiveDeviceT[0].text, "TARGET_EOD_COORD"); IDSnoopDevice(ActiveDeviceT[0].text, "GEOGRAPHIC_COORD"); IDSnoopDevice(ActiveDeviceT[0].text, "TELESCOPE_PARK"); if (CanAbsMove()) IDSnoopDevice(ActiveDeviceT[0].text, "TELESCOPE_PIER_SIDE"); IDSnoopDevice(ActiveDeviceT[1].text, "WEATHER_STATUS"); return true; } } controller->ISNewText(dev, name, texts, names, n); return DefaultDevice::ISNewText(dev, name, texts, names, n); } bool Dome::ISSnoopDevice(XMLEle *root) { XMLEle *ep = nullptr; const char *propName = findXMLAttValu(root, "name"); // Check TARGET if (!strcmp("TARGET_EOD_COORD", propName)) { int rc_ra = -1, rc_de = -1; double ra = 0, de = 0; for (ep = nextXMLEle(root, 1); ep != nullptr; ep = nextXMLEle(root, 0)) { const char *elemName = findXMLAttValu(ep, "name"); DEBUGF(Logger::DBG_DEBUG, "Snooped Target RA-DEC: %s", pcdataXMLEle(ep)); if (!strcmp(elemName, "RA")) rc_ra = f_scansexa(pcdataXMLEle(ep), &ra); else if (!strcmp(elemName, "DEC")) rc_de = f_scansexa(pcdataXMLEle(ep), &de); } // Dont start moving the dome till the mount has initialized all the variables if (HaveRaDec && CanAbsMove()) { if (rc_ra == 0 && rc_de == 0) { // everything parsed ok, so lets start the dome to moving // If this slew involves a meridan flip, then the slaving calcs will end up using // the wrong OTA side. Lets set things up so our slaving code will calculate the side // for the target slew instead of using mount pier side info OTASideSP.s=IPS_IDLE; IDSetSwitch(&OTASideSP,nullptr); // and see if we can get there at the same time as the mount mountEquatorialCoords.ra = ra * 15.0; mountEquatorialCoords.dec = de; DEBUGF(Logger::DBG_DEBUG, "Calling Update mount to anticipate goto target: %g - DEC: %g", mountEquatorialCoords.ra, mountEquatorialCoords.dec); UpdateMountCoords(); } } return true; } // Check EOD if (!strcmp("EQUATORIAL_EOD_COORD", propName)) { int rc_ra = -1, rc_de = -1; double ra = 0, de = 0; for (ep = nextXMLEle(root, 1); ep != nullptr; ep = nextXMLEle(root, 0)) { const char *elemName = findXMLAttValu(ep, "name"); DEBUGF(Logger::DBG_DEBUG, "Snooped RA-DEC: %s", pcdataXMLEle(ep)); if (!strcmp(elemName, "RA")) rc_ra = f_scansexa(pcdataXMLEle(ep), &ra); else if (!strcmp(elemName, "DEC")) rc_de = f_scansexa(pcdataXMLEle(ep), &de); } if (rc_ra == 0 && rc_de == 0) { mountEquatorialCoords.ra = ra * 15.0; mountEquatorialCoords.dec = de; } mountState = IPS_ALERT; crackIPState(findXMLAttValu(root, "state"), &mountState); // If the diff > 0.1 then the mount is in motion, so let's wait until it settles before moving the doom if (fabs(mountEquatorialCoords.ra - prev_ra) > DOME_COORD_THRESHOLD || fabs(mountEquatorialCoords.dec - prev_dec) > DOME_COORD_THRESHOLD) { prev_ra = mountEquatorialCoords.ra; prev_dec = mountEquatorialCoords.dec; DEBUGF(Logger::DBG_DEBUG, "Snooped RA: %g - DEC: %g", mountEquatorialCoords.ra, mountEquatorialCoords.dec); // a mount still intializing will emit 0 and 0 on the first go // we dont want to process 0/0 if ((mountEquatorialCoords.ra != 0) || (mountEquatorialCoords.dec != 0)) HaveRaDec = true; } // else mount stable, i.e. tracking, so let's update mount coords and check if we need to move else if (mountState == IPS_OK || mountState == IPS_IDLE) UpdateMountCoords(); return true; } // Check Geographic coords if (!strcmp("GEOGRAPHIC_COORD", propName)) { for (ep = nextXMLEle(root, 1); ep != nullptr; ep = nextXMLEle(root, 0)) { const char *elemName = findXMLAttValu(ep, "name"); if (!strcmp(elemName, "LONG")) { double indiLong; f_scansexa(pcdataXMLEle(ep), &indiLong); if (indiLong > 180) indiLong -= 360; observer.lng = indiLong; HaveLatLong = true; } else if (!strcmp(elemName, "LAT")) f_scansexa(pcdataXMLEle(ep), &(observer.lat)); } DEBUGF(Logger::DBG_DEBUG, "Snooped LONG: %g - LAT: %g", observer.lng, observer.lat); UpdateMountCoords(); return true; } // Check Telescope Park status if (!strcmp("TELESCOPE_PARK", propName)) { if (!strcmp(findXMLAttValu(root, "state"), "Ok")) { bool prevState = IsLocked; for (ep = nextXMLEle(root, 1); ep != nullptr; ep = nextXMLEle(root, 0)) { const char *elemName = findXMLAttValu(ep, "name"); if ((!strcmp(elemName, "PARK") && !strcmp(pcdataXMLEle(ep), "On"))) IsMountParked = true; else if ((!strcmp(elemName, "UNPARK") && !strcmp(pcdataXMLEle(ep), "On"))) IsMountParked = false; if (IsLocked && !strcmp(elemName, "PARK") && !strcmp(pcdataXMLEle(ep), "On")) IsLocked = false; else if (!IsLocked && !strcmp(elemName, "UNPARK") && !strcmp(pcdataXMLEle(ep), "On")) IsLocked = true; } if (prevState != IsLocked && TelescopeClosedLockT[1].s == ISS_ON) DEBUGF(Logger::DBG_SESSION, "Telescope status changed. Lock is set to: %s", IsLocked ? "locked" : "unlocked"); } return true; } // Weather Status if (!strcmp("WEATHER_STATUS", propName)) { weatherState = IPS_ALERT; crackIPState(findXMLAttValu(root, "state"), &weatherState); if (weatherState == IPS_ALERT) { if (CanPark() && AutoParkS[0].s == ISS_ON) { if (!isParked()) { DEBUG(Logger::DBG_WARNING, "Weather conditions in the danger zone! Parking dome..."); Dome::Park(); } } else DEBUG(Logger::DBG_WARNING, "Weather conditions in the danger zone! Close the dome immediately!"); return true; } } if (!strcmp("TELESCOPE_PIER_SIDE", propName)) { // set defaults to say we have no valid information from mount bool isEast=false; bool isWest=false; OTASideS[0].s=ISS_OFF; OTASideS[1].s=ISS_OFF; OTASideSP.s=IPS_IDLE; // crack the message for (ep = nextXMLEle(root, 1); ep != nullptr; ep = nextXMLEle(root, 0)) { const char *elemName = findXMLAttValu(ep, "name"); if (!strcmp(elemName, "PIER_EAST") && !strcmp(pcdataXMLEle(ep), "On")) isEast = true; else if (!strcmp(elemName, "PIER_WEST") && !strcmp(pcdataXMLEle(ep), "On")) isWest = true; } // update the switch if(isEast) OTASideS[0].s=ISS_ON; if(isWest) OTASideS[1].s=ISS_ON; if(isWest || isEast) OTASideSP.s=IPS_OK; // and set it. If we didn't get valid info, it'll be set to idle and neither 'button' pressed in the ui IDSetSwitch(&OTASideSP,nullptr); return true; } controller->ISSnoopDevice(root); return DefaultDevice::ISSnoopDevice(root); } bool Dome::saveConfigItems(FILE *fp) { DefaultDevice::saveConfigItems(fp); IUSaveConfigText(fp, &ActiveDeviceTP); IUSaveConfigSwitch(fp, &TelescopeClosedLockTP); IUSaveConfigNumber(fp, &PresetNP); IUSaveConfigNumber(fp, &DomeParamNP); IUSaveConfigNumber(fp, &DomeMeasurementsNP); IUSaveConfigSwitch(fp, &AutoParkSP); IUSaveConfigSwitch(fp, &DomeAutoSyncSP); controller->saveConfigItems(fp); return true; } void Dome::triggerSnoop(const char *driverName, const char *snoopedProp) { DEBUGF(Logger::DBG_DEBUG, "Active Snoop, driver: %s, property: %s", driverName, snoopedProp); IDSnoopDevice(driverName, snoopedProp); } bool Dome::isLocked() { return TelescopeClosedLockT[1].s == ISS_ON && IsLocked; } void Dome::buttonHelper(const char *button_n, ISState state, void *context) { static_cast(context)->processButton(button_n, state); } void Dome::processButton(const char *button_n, ISState state) { //ignore OFF if (state == ISS_OFF) return; // Dome In if (!strcmp(button_n, "Dome CW")) { if (DomeMotionSP.s != IPS_BUSY) Dome::Move(DOME_CW, MOTION_START); else Dome::Move(DOME_CW, MOTION_STOP); } else if (!strcmp(button_n, "Dome CCW")) { if (DomeMotionSP.s != IPS_BUSY) Dome::Move(DOME_CCW, MOTION_START); else Dome::Move(DOME_CCW, MOTION_STOP); } else if (!strcmp(button_n, "Dome Abort")) { Dome::Abort(); } } IPState Dome::getMountState() const { return mountState; } IPState Dome::getWeatherState() const { return weatherState; } Dome::DomeState Dome::Dome::getDomeState() const { return domeState; } void Dome::setDomeState(const Dome::DomeState &value) { switch (value) { case DOME_IDLE: if (DomeMotionSP.s == IPS_BUSY) { IUResetSwitch(&DomeMotionSP); DomeMotionSP.s = IPS_IDLE; IDSetSwitch(&DomeMotionSP, nullptr); } if (DomeAbsPosNP.s == IPS_BUSY) { DomeAbsPosNP.s = IPS_IDLE; IDSetNumber(&DomeAbsPosNP, nullptr); } if (DomeRelPosNP.s == IPS_BUSY) { DomeRelPosNP.s = IPS_IDLE; IDSetNumber(&DomeRelPosNP, nullptr); } break; case DOME_SYNCED: if (DomeMotionSP.s == IPS_BUSY) { IUResetSwitch(&DomeMotionSP); DomeMotionSP.s = IPS_OK; IDSetSwitch(&DomeMotionSP, nullptr); } if (DomeAbsPosNP.s == IPS_BUSY) { DomeAbsPosNP.s = IPS_OK; IDSetNumber(&DomeAbsPosNP, nullptr); } if (DomeRelPosNP.s == IPS_BUSY) { DomeRelPosNP.s = IPS_OK; IDSetNumber(&DomeRelPosNP, nullptr); } break; case DOME_PARKED: IUResetSwitch(&ParkSP); ParkSP.s = IPS_OK; ParkS[0].s = ISS_ON; IDSetSwitch(&ParkSP, nullptr); IsParked = true; break; case DOME_PARKING: IUResetSwitch(&ParkSP); ParkSP.s = IPS_BUSY; ParkS[0].s = ISS_ON; IDSetSwitch(&ParkSP, nullptr); break; case DOME_UNPARKING: IUResetSwitch(&ParkSP); ParkSP.s = IPS_BUSY; ParkS[1].s = ISS_ON; IDSetSwitch(&ParkSP, nullptr); break; case DOME_UNPARKED: IUResetSwitch(&ParkSP); ParkSP.s = IPS_OK; ParkS[1].s = ISS_ON; IDSetSwitch(&ParkSP, nullptr); IsParked = false; break; case DOME_MOVING: break; } domeState = value; } /* The problem to get a dome azimuth given a telescope azimuth, altitude and geometry (telescope placement, mount geometry) can be seen as solve the intersection between the optical axis with the dome, that is, the intersection between a line and a sphere. To do that we need to calculate the optical axis line taking the centre of the dome as origin of coordinates. */ // Returns false if it can't solve it due bad geometry of the observatory // Returns: // Az : Azimuth required to the dome in order to center the shutter aperture with telescope // minAz: Minimum azimuth in order to avoid any dome interference to the full aperture of the telescope // maxAz: Maximum azimuth in order to avoid any dome interference to the full aperture of the telescope bool Dome::GetTargetAz(double &Az, double &Alt, double &minAz, double &maxAz) { point3D MountCenter, OptCenter, OptVector, DomeIntersect; double hourAngle; double mu1, mu2; double yx; double HalfApertureChordAngle; double RadiusAtAlt; int OTASide = 1; /* Side of the telescope with respect of the mount, 1: east, -1: west*/ if (HaveLatLong == false) { triggerSnoop(ActiveDeviceT[0].text, "GEOGRAPHIC_COORD"); DEBUG(Logger::DBG_WARNING, "Geographic coordinates are not yet defined, triggering snoop..."); return false; } double JD = ln_get_julian_from_sys(); double MSD = ln_get_mean_sidereal_time(JD); DEBUGF(Logger::DBG_DEBUG, "JD: %g - MSD: %g", JD, MSD); MountCenter.x = DomeMeasurementsN[DM_EAST_DISPLACEMENT].value; // Positive to East MountCenter.y = DomeMeasurementsN[DM_NORTH_DISPLACEMENT].value; // Positive to North MountCenter.z = DomeMeasurementsN[DM_UP_DISPLACEMENT].value; // Positive Up DEBUGF(Logger::DBG_DEBUG, "MC.x: %g - MC.y: %g MC.z: %g", MountCenter.x, MountCenter.y, MountCenter.z); // Get hour angle in hours hourAngle = rangeHA( MSD + observer.lng / 15.0 - mountEquatorialCoords.ra / 15.0); DEBUGF(Logger::DBG_DEBUG, "HA: %g Lng: %g RA: %g", hourAngle, observer.lng, mountEquatorialCoords.ra); // this will have state OK if the mount sent us information // and it will be IDLE if not if(CanAbsMove() && OTASideSP.s==IPS_OK) { // process info from the mount if(OTASideS[0].s==ISS_ON) OTASide=-1; else OTASide=1; } else { // figure out the pier side without help from the mount if(hourAngle > 0) OTASide=-1; else OTASide=1; // if we got here because we turned off the PIER_SIDE switches in a target goto // lets try get it back on if (CanAbsMove()) triggerSnoop(ActiveDeviceT[0].text, "TELESCOPE_PIER_SIDE"); } OpticalCenter(MountCenter, OTASide * DomeMeasurementsN[DM_OTA_OFFSET].value, observer.lat, hourAngle, OptCenter); DEBUGF(Logger::DBG_DEBUG, "OTA_SIDE: %d", OTASide); DEBUGF(Logger::DBG_DEBUG, "OTA_OFFSET: %g Lat: %g", DomeMeasurementsN[DM_OTA_OFFSET].value, observer.lat); DEBUGF(Logger::DBG_DEBUG, "OC.x: %g - OC.y: %g OC.z: %g", OptCenter.x, OptCenter.y, OptCenter.z); // To be sure mountHoriztonalCoords is up to date. ln_get_hrz_from_equ(&mountEquatorialCoords, &observer, JD, &mountHoriztonalCoords); mountHoriztonalCoords.az += 180; if (mountHoriztonalCoords.az >= 360) mountHoriztonalCoords.az -= 360; if (mountHoriztonalCoords.az < 0) mountHoriztonalCoords.az += 360; // Get optical axis point. This and the previous form the optical axis line OpticalVector(mountHoriztonalCoords.az, mountHoriztonalCoords.alt, OptVector); DEBUGF(Logger::DBG_DEBUG, "Mount Az: %g Alt: %g", mountHoriztonalCoords.az, mountHoriztonalCoords.alt); DEBUGF(Logger::DBG_DEBUG, "OV.x: %g - OV.y: %g OV.z: %g", OptVector.x, OptVector.y, OptVector.z); if (Intersection(OptCenter, OptVector, DomeMeasurementsN[DM_DOME_RADIUS].value, mu1, mu2)) { // If telescope is pointing over the horizon, the solution is mu1, else is mu2 if (mu1 < 0) mu1 = mu2; DomeIntersect.x = OptCenter.x + mu1 * (OptVector.x ); DomeIntersect.y = OptCenter.y + mu1 * (OptVector.y ); DomeIntersect.z = OptCenter.z + mu1 * (OptVector.z ); if (fabs(DomeIntersect.x) > 0.00001) { yx = DomeIntersect.y / DomeIntersect.x; Az = 90 - 180 * atan(yx) / M_PI; if (DomeIntersect.x < 0) { Az = Az + 180; } if (Az >= 360) Az -= 360; else if (Az < 0) Az += 360; } else { // Dome East-West line or zenith if (DomeIntersect.y > 0) Az = 90; else Az = 270; } if ((fabs(DomeIntersect.x) > 0.00001) || (fabs(DomeIntersect.y) > 0.00001)) Alt = 180 * atan(DomeIntersect.z / sqrt((DomeIntersect.x * DomeIntersect.x) + (DomeIntersect.y * DomeIntersect.y))) / M_PI; else Alt = 90; // Dome Zenith // Calculate the Azimuth range in the given Altitude of the dome RadiusAtAlt = DomeMeasurementsN[DM_DOME_RADIUS].value * cos(M_PI * Alt / 180); // Radius alt the given altitude if (DomeMeasurementsN[DM_SHUTTER_WIDTH].value < (2 * RadiusAtAlt)) { HalfApertureChordAngle = 180 * asin(DomeMeasurementsN[DM_SHUTTER_WIDTH].value / (2 * RadiusAtAlt)) / M_PI; // Angle of a chord of half aperture length minAz = Az - HalfApertureChordAngle; if (minAz < 0) minAz = minAz + 360; maxAz = Az + HalfApertureChordAngle; if (maxAz >= 360) maxAz = maxAz - 360; } else { minAz = 0; maxAz = 360; } return true; } return false; } bool Dome::Intersection(point3D p1, point3D dp, double r, double &mu1, double &mu2) { double a, b, c; double bb4ac; a = dp.x * dp.x + dp.y * dp.y + dp.z * dp.z; b = 2 * (dp.x * p1.x + dp.y * p1.y + dp.z * p1.z); c = 0.0; c = c + p1.x * p1.x + p1.y * p1.y + p1.z * p1.z; c = c - r * r; bb4ac = b * b - 4 * a * c; if ((fabs(a) < 0.0000001) || (bb4ac < 0)) { mu1 = 0; mu2 = 0; return false; } mu1 = (-b + sqrt(bb4ac)) / (2 * a); mu2 = (-b - sqrt(bb4ac)) / (2 * a); return true; } bool Dome::OpticalCenter(point3D MountCenter, double dOpticalAxis, double Lat, double Ah, point3D &OP) { double q, f; double cosf, sinf, cosq, sinq; // Note: this transformation is a circle rotated around X axis -(90 - Lat) degrees q = M_PI * (90 - Lat) / 180; f = -M_PI * (180 + Ah * 15) / 180; cosf = cos(f); sinf = sin(f); cosq = cos(q); sinq = sin(q); OP.x = (dOpticalAxis * cosf + MountCenter.x); // The sign of dOpticalAxis determines de side of the tube OP.y = (dOpticalAxis * sinf * cosq + MountCenter.y); OP.z = (dOpticalAxis * sinf * sinq + MountCenter.z); return true; } bool Dome::OpticalVector(double Az, double Alt, point3D &OV) { double q, f; q = M_PI * Alt / 180; f = M_PI * Az / 180; OV.x = cos(q) * sin(f); OV.y = cos(q) * cos(f); OV.z = sin(q); return true; } double Dome::Csc(double x) { return 1.0 / sin(x); } double Dome::Sec(double x) { return 1.0 / cos(x); } bool Dome::CheckHorizon(double HA, double dec, double lat) { double sinh_value; sinh_value = cos(lat) * cos(HA) * cos(dec) + sin(lat) * sin(dec); if (sinh_value >= 0.0) return true; return false; } void Dome::UpdateMountCoords() { // If not initialized yet, return. if (mountEquatorialCoords.ra == -1) return; // Dont do this if we haven't had co-ordinates from the mount yet if (!HaveLatLong) return; if (!HaveRaDec) return; ln_get_hrz_from_equ(&mountEquatorialCoords, &observer, ln_get_julian_from_sys(), &mountHoriztonalCoords); mountHoriztonalCoords.az += 180; if (mountHoriztonalCoords.az > 360) mountHoriztonalCoords.az -= 360; if (mountHoriztonalCoords.az < 0) mountHoriztonalCoords.az += 360; // Control debug flooding if (fabs(mountHoriztonalCoords.az - prev_az) > DOME_COORD_THRESHOLD || fabs(mountHoriztonalCoords.alt - prev_alt) > DOME_COORD_THRESHOLD) { prev_az = mountHoriztonalCoords.az; prev_alt = mountHoriztonalCoords.alt; DEBUGF(Logger::DBG_DEBUG, "Updated telescope Az: %g - Alt: %g", prev_az, prev_alt); } // Check if we need to move if mount is unparked. if (IsMountParked == false) UpdateAutoSync(); } void Dome::UpdateAutoSync() { if ((mountState == IPS_OK || mountState == IPS_IDLE) && DomeAbsPosNP.s != IPS_BUSY && DomeAutoSyncS[0].s == ISS_ON) { if (CanPark()) { if (isParked() == true) { DEBUG(Logger::DBG_WARNING, "Cannot perform autosync with dome parked. Please unpark to enable autosync operation."); return; } } double targetAz = 0, targetAlt = 0, minAz = 0, maxAz = 0; bool res; res = GetTargetAz(targetAz, targetAlt, minAz, maxAz); if (!res) { DEBUGF(Logger::DBG_DEBUG, "GetTargetAz failed %g", targetAz); return; } DEBUGF(Logger::DBG_DEBUG, "Calculated target azimuth is %g. MinAz: %g, MaxAz: %g", targetAz, minAz, maxAz); if (fabs(targetAz - DomeAbsPosN[0].value) > DomeParamN[0].value) { IPState ret = Dome::MoveAbs(targetAz); if (ret == IPS_OK) DEBUGF(Logger::DBG_SESSION, "Dome synced to position %g degrees.", targetAz); else if (ret == IPS_BUSY) DEBUGF(Logger::DBG_SESSION, "Dome is syncing to position %g degrees...", targetAz); else DEBUG(Logger::DBG_SESSION, "Dome failed to sync to new requested position."); DomeAbsPosNP.s = ret; IDSetNumber(&DomeAbsPosNP, nullptr); } } } void Dome::SetDomeCapability(uint32_t cap) { capability = cap; if (CanAbort()) controller->mapController("Dome Abort", "Dome Abort", Controller::CONTROLLER_BUTTON, "BUTTON_3"); } const char *Dome::GetShutterStatusString(ShutterStatus status) { switch (status) { case SHUTTER_OPENED: return "Shutter is open."; break; case SHUTTER_CLOSED: return "Shutter is closed."; break; case SHUTTER_MOVING: return "Shutter is in motion."; break; case SHUTTER_UNKNOWN: default: return "Shutter status is unknown."; break; } } void Dome::SetParkDataType(Dome::DomeParkData type) { parkDataType = type; if (parkDataType != PARK_NONE) { switch (parkDataType) { case PARK_AZ: IUFillNumber(&ParkPositionN[AXIS_AZ], "PARK_AZ", "AZ D:M:S", "%10.6m", 0.0, 360.0, 0.0, 0); break; case PARK_AZ_ENCODER: IUFillNumber(&ParkPositionN[AXIS_AZ], "PARK_AZ", "AZ Encoder", "%.0f", 0, 16777215, 1, 0); break; default: break; } IUFillNumberVector(&ParkPositionNP, ParkPositionN, 1, getDeviceName(), "DOME_PARK_POSITION", "Park Position", SITE_TAB, IP_RW, 60, IPS_IDLE); } else { strncpy(DomeMotionS[DOME_CW].label, "Open", MAXINDILABEL); strncpy(DomeMotionS[DOME_CCW].label, "Close", MAXINDILABEL); } } void Dome::SetParked(bool isparked) { IsParked = isparked; setDomeState(DOME_IDLE); if (IsParked) { setDomeState(DOME_PARKED); DEBUG(Logger::DBG_SESSION, "Dome is parked."); } else { setDomeState(DOME_UNPARKED); DEBUG(Logger::DBG_SESSION, "Dome is unparked."); } WriteParkData(); } bool Dome::isParked() { return IsParked; } bool Dome::InitPark() { char *loadres; loadres = LoadParkData(); if (loadres) { DEBUGF(Logger::DBG_SESSION, "InitPark: No Park data in file %s: %s", Parkdatafile, loadres); SetParked(false); return false; } if (parkDataType != PARK_NONE) { ParkPositionN[AXIS_AZ].value = Axis1ParkPosition; IDSetNumber(&ParkPositionNP, nullptr); // If parked, store the position as current azimuth angle or encoder ticks if (isParked() && CanAbsMove()) { DomeAbsPosN[0].value = ParkPositionN[AXIS_AZ].value; IDSetNumber(&DomeAbsPosNP, nullptr); } } return true; } char *Dome::LoadParkData() { wordexp_t wexp; FILE *fp; LilXML *lp; static char errmsg[512]={0}; XMLEle *parkxml; XMLAtt *ap; bool devicefound = false; ParkDeviceName = getDeviceName(); ParkstatusXml = nullptr; ParkdeviceXml = nullptr; ParkpositionXml = nullptr; ParkpositionAxis1Xml = nullptr; if (wordexp(Parkdatafile, &wexp, 0)) { wordfree(&wexp); return (char *)("Badly formed filename."); } if (!(fp = fopen(wexp.we_wordv[0], "r"))) { wordfree(&wexp); return strerror(errno); } wordfree(&wexp); lp = newLilXML(); if (ParkdataXmlRoot) delXMLEle(ParkdataXmlRoot); ParkdataXmlRoot = readXMLFile(fp, lp, errmsg); delLilXML(lp); if (!ParkdataXmlRoot) return errmsg; if (!strcmp(tagXMLEle(nextXMLEle(ParkdataXmlRoot, 1)), "parkdata")) return (char *)("Not a park data file"); parkxml = nextXMLEle(ParkdataXmlRoot, 1); while (parkxml) { if (strcmp(tagXMLEle(parkxml), "device")) { parkxml = nextXMLEle(ParkdataXmlRoot, 0); continue; } ap = findXMLAtt(parkxml, "name"); if (ap && (!strcmp(valuXMLAtt(ap), ParkDeviceName))) { devicefound = true; break; } parkxml = nextXMLEle(ParkdataXmlRoot, 0); } if (!devicefound) return (char *)"No park data found for this device"; ParkdeviceXml = parkxml; ParkstatusXml = findXMLEle(parkxml, "parkstatus"); if (ParkstatusXml == nullptr) { return (char *)("Park data invalid or missing."); } if (parkDataType != PARK_NONE) { ParkpositionXml = findXMLEle(parkxml, "parkposition"); ParkpositionAxis1Xml = findXMLEle(ParkpositionXml, "axis1position"); if (ParkpositionAxis1Xml == nullptr) { return (char *)("Park data invalid or missing."); } } if (parkDataType != PARK_NONE) sscanf(pcdataXMLEle(ParkpositionAxis1Xml), "%lf", &Axis1ParkPosition); if (!strcmp(pcdataXMLEle(ParkstatusXml), "true")) SetParked(true); else SetParked(false); return nullptr; } bool Dome::WriteParkData() { wordexp_t wexp; FILE *fp; char pcdata[30]={0}; if (wordexp(Parkdatafile, &wexp, 0)) { wordfree(&wexp); DEBUGF(Logger::DBG_SESSION, "WriteParkData: can not write file %s: Badly formed filename.", Parkdatafile); return false; } if (!(fp = fopen(wexp.we_wordv[0], "w"))) { wordfree(&wexp); DEBUGF(Logger::DBG_SESSION, "WriteParkData: can not write file %s: %s", Parkdatafile, strerror(errno)); return false; } if (!ParkdataXmlRoot) ParkdataXmlRoot = addXMLEle(nullptr, "parkdata"); if (!ParkdeviceXml) { ParkdeviceXml = addXMLEle(ParkdataXmlRoot, "device"); addXMLAtt(ParkdeviceXml, "name", ParkDeviceName == nullptr ? getDeviceName() : ParkDeviceName); } if (!ParkstatusXml) ParkstatusXml = addXMLEle(ParkdeviceXml, "parkstatus"); if (parkDataType != PARK_NONE) { if (!ParkpositionXml) ParkpositionXml = addXMLEle(ParkdeviceXml, "parkposition"); if (!ParkpositionAxis1Xml) ParkpositionAxis1Xml = addXMLEle(ParkpositionXml, "axis1position"); } editXMLEle(ParkstatusXml, (IsParked ? "true" : "false")); if (parkDataType != PARK_NONE) { snprintf(pcdata, sizeof(pcdata), "%f", Axis1ParkPosition); editXMLEle(ParkpositionAxis1Xml, pcdata); } prXMLEle(fp, ParkdataXmlRoot, 0); fclose(fp); return true; } double Dome::GetAxis1Park() { return Axis1ParkPosition; } double Dome::GetAxis1ParkDefault() { return Axis1DefaultParkPosition; } void Dome::SetAxis1Park(double value) { Axis1ParkPosition = value; ParkPositionN[AXIS_RA].value = value; IDSetNumber(&ParkPositionNP, nullptr); } void Dome::SetAxis1ParkDefault(double value) { Axis1DefaultParkPosition = value; } IPState Dome::Move(DomeDirection dir, DomeMotionCommand operation) { // Check if it is already parked. if (CanPark()) { if (parkDataType != PARK_NONE && isParked()) { DEBUG(Logger::DBG_WARNING, "Please unpark the dome before issuing any motion commands."); return IPS_ALERT; } } if ((DomeMotionSP.s != IPS_BUSY && (DomeAbsPosNP.s == IPS_BUSY || DomeRelPosNP.s == IPS_BUSY)) || (domeState == DOME_PARKING)) { DEBUG(Logger::DBG_WARNING, "Please stop dome before issuing any further motion commands."); return IPS_ALERT; } int current_direction = IUFindOnSwitchIndex(&DomeMotionSP); // if same move requested, return if (DomeMotionSP.s == IPS_BUSY && current_direction == dir && operation == MOTION_START) return IPS_BUSY; DomeMotionSP.s = Move(dir, operation); if (DomeMotionSP.s == IPS_BUSY || DomeMotionSP.s == IPS_OK) { domeState = (operation == MOTION_START) ? DOME_MOVING : DOME_IDLE; IUResetSwitch(&DomeMotionSP); if (operation == MOTION_START) DomeMotionS[dir].s = ISS_ON; } IDSetSwitch(&DomeMotionSP, nullptr); return DomeMotionSP.s; } IPState Dome::MoveRel(double azDiff) { if (CanRelMove() == false) { DEBUG(Logger::DBG_ERROR, "Dome does not support relative motion."); return IPS_ALERT; } if (domeState == DOME_PARKED) { DEBUG(Logger::DBG_ERROR, "Please unpark before issuing any motion commands."); DomeRelPosNP.s = IPS_ALERT; IDSetNumber(&DomeRelPosNP, nullptr); return IPS_ALERT; } if ((DomeRelPosNP.s != IPS_BUSY && DomeMotionSP.s == IPS_BUSY) || (domeState == DOME_PARKING)) { DEBUG(Logger::DBG_WARNING, "Please stop dome before issuing any further motion commands."); DomeRelPosNP.s = IPS_IDLE; IDSetNumber(&DomeRelPosNP, nullptr); return IPS_ALERT; } IPState rc; if ((rc = MoveRel(azDiff)) == IPS_OK) { domeState = DOME_IDLE; DomeRelPosNP.s = IPS_OK; DomeRelPosN[0].value = azDiff; IDSetNumber(&DomeRelPosNP, "Dome moved %g degrees %s.", azDiff, (azDiff > 0) ? "clockwise" : "counter clockwise"); if (CanAbsMove()) { DomeAbsPosNP.s = IPS_OK; IDSetNumber(&DomeAbsPosNP, nullptr); } return IPS_OK; } else if (rc == IPS_BUSY) { domeState = DOME_MOVING; DomeRelPosN[0].value = azDiff; DomeRelPosNP.s = IPS_BUSY; IDSetNumber(&DomeRelPosNP, "Dome is moving %g degrees %s...", azDiff, (azDiff > 0) ? "clockwise" : "counter clockwise"); if (CanAbsMove()) { DomeAbsPosNP.s = IPS_BUSY; IDSetNumber(&DomeAbsPosNP, nullptr); } DomeMotionSP.s = IPS_BUSY; IUResetSwitch(&DomeMotionSP); DomeMotionS[DOME_CW].s = (azDiff > 0) ? ISS_ON : ISS_OFF; DomeMotionS[DOME_CCW].s = (azDiff < 0) ? ISS_ON : ISS_OFF; IDSetSwitch(&DomeMotionSP, nullptr); return IPS_BUSY; } domeState = DOME_IDLE; DomeRelPosNP.s = IPS_ALERT; IDSetNumber(&DomeRelPosNP, "Dome failed to move to new requested position."); return IPS_ALERT; } IPState Dome::MoveAbs(double az) { if (CanAbsMove() == false) { DEBUG(Logger::DBG_ERROR, "Dome does not support MoveAbs(). MoveAbs() must be implemented in the child class."); return IPS_ALERT; } if (domeState == DOME_PARKED) { DEBUG(Logger::DBG_ERROR, "Please unpark before issuing any motion commands."); DomeAbsPosNP.s = IPS_ALERT; IDSetNumber(&DomeAbsPosNP, nullptr); return IPS_ALERT; } if ((DomeRelPosNP.s != IPS_BUSY && DomeMotionSP.s == IPS_BUSY) || (domeState == DOME_PARKING)) { DEBUG(Logger::DBG_WARNING, "Please stop dome before issuing any further motion commands."); return IPS_ALERT; } IPState rc; if (az < DomeAbsPosN[0].min || az > DomeAbsPosN[0].max) { DEBUGF(Logger::DBG_ERROR, "Error: requested azimuth angle %g is out of range.", az); DomeAbsPosNP.s = IPS_ALERT; IDSetNumber(&DomeAbsPosNP, nullptr); return IPS_ALERT; } if ((rc = MoveAbs(az)) == IPS_OK) { domeState = DOME_IDLE; DomeAbsPosNP.s = IPS_OK; DomeAbsPosN[0].value = az; DEBUGF(Logger::DBG_SESSION, "Dome moved to position %g degrees.", az); IDSetNumber(&DomeAbsPosNP, nullptr); return IPS_OK; } else if (rc == IPS_BUSY) { domeState = DOME_MOVING; DomeAbsPosNP.s = IPS_BUSY; DEBUGF(Logger::DBG_SESSION, "Dome is moving to position %g degrees...", az); IDSetNumber(&DomeAbsPosNP, nullptr); DomeMotionSP.s = IPS_BUSY; IUResetSwitch(&DomeMotionSP); DomeMotionS[DOME_CW].s = (az > DomeAbsPosN[0].value) ? ISS_ON : ISS_OFF; DomeMotionS[DOME_CCW].s = (az < DomeAbsPosN[0].value) ? ISS_ON : ISS_OFF; IDSetSwitch(&DomeMotionSP, nullptr); return IPS_BUSY; } domeState = DOME_IDLE; DomeAbsPosNP.s = IPS_ALERT; IDSetNumber(&DomeAbsPosNP, "Dome failed to move to new requested position."); return IPS_ALERT; } bool Dome::Abort() { if (CanAbort() == false) { DEBUG(Logger::DBG_ERROR, "Dome does not support abort."); return false; } IUResetSwitch(&AbortSP); if (Abort()) { AbortSP.s = IPS_OK; if (domeState == DOME_PARKING || domeState == DOME_UNPARKING) { IUResetSwitch(&ParkSP); if (domeState == DOME_PARKING) { DEBUG(Logger::DBG_SESSION, "Parking aborted."); // If parking was aborted then it was UNPARKED before ParkS[1].s = ISS_ON; } else { DEBUG(Logger::DBG_SESSION, "UnParking aborted."); // If unparking aborted then it was PARKED before ParkS[0].s = ISS_ON; } ParkSP.s = IPS_ALERT; IDSetSwitch(&ParkSP, nullptr); } setDomeState(DOME_IDLE); } else { AbortSP.s = IPS_ALERT; // If alert was aborted during parking or unparking, the parking state is unknown if (domeState == DOME_PARKING || domeState == DOME_UNPARKING) { IUResetSwitch(&ParkSP); ParkSP.s = IPS_IDLE; IDSetSwitch(&ParkSP, nullptr); } } IDSetSwitch(&AbortSP, nullptr); return (AbortSP.s == IPS_OK); } bool Dome::SetSpeed(double speed) { if (HasVariableSpeed() == false) { DEBUG(Logger::DBG_ERROR, "Dome does not support variable speed."); return false; } if (SetSpeed(speed)) { DomeSpeedNP.s = IPS_OK; DomeSpeedN[0].value = speed; } else DomeSpeedNP.s = IPS_ALERT; IDSetNumber(&DomeSpeedNP, nullptr); return (DomeSpeedNP.s == IPS_OK); } IPState Dome::ControlShutter(ShutterOperation operation) { if (HasShutter() == false) { DEBUG(Logger::DBG_ERROR, "Dome does not have shutter control."); return IPS_ALERT; } if (weatherState == IPS_ALERT && operation == SHUTTER_OPEN) { DEBUG(Logger::DBG_WARNING, "Weather is in the danger zone! Cannot open shutter."); return IPS_ALERT; } int currentStatus = IUFindOnSwitchIndex(&DomeShutterSP); // No change of status, let's return if (DomeShutterSP.s == IPS_BUSY && currentStatus == operation) { IDSetSwitch(&DomeShutterSP, nullptr); return DomeShutterSP.s; } DomeShutterSP.s = ControlShutter(operation); if (DomeShutterSP.s == IPS_OK) { IUResetSwitch(&DomeShutterSP); DomeShutterS[operation].s = ISS_ON; IDSetSwitch(&DomeShutterSP, "Shutter is %s.", (operation == SHUTTER_OPEN ? "open" : "closed")); return DomeShutterSP.s; } else if (DomeShutterSP.s == IPS_BUSY) { IUResetSwitch(&DomeShutterSP); DomeShutterS[operation].s = ISS_ON; IDSetSwitch(&DomeShutterSP, "Shutter is %s...", (operation == 0 ? "opening" : "closing")); return DomeShutterSP.s; } IDSetSwitch(&DomeShutterSP, "Shutter failed to %s.", (operation == 0 ? "open" : "close")); return IPS_ALERT; } IPState Dome::Park() { if (CanPark() == false) { DEBUG(Logger::DBG_ERROR, "Dome does not support parking."); return IPS_ALERT; } if (domeState == DOME_PARKED) { IUResetSwitch(&ParkSP); ParkS[0].s = ISS_ON; DEBUG(Logger::DBG_SESSION, "Dome already parked."); IDSetSwitch(&ParkSP, nullptr); return IPS_OK; } ParkSP.s = Park(); if (ParkSP.s == IPS_OK) SetParked(true); else if (ParkSP.s == IPS_BUSY) { domeState = DOME_PARKING; if (CanAbsMove()) DomeAbsPosNP.s = IPS_BUSY; IUResetSwitch(&ParkSP); ParkS[0].s = ISS_ON; } else IDSetSwitch(&ParkSP, nullptr); return ParkSP.s; } IPState Dome::UnPark() { if (CanPark() == false) { DEBUG(Logger::DBG_ERROR, "Dome does not support parking."); return IPS_ALERT; } if (domeState != DOME_PARKED) { IUResetSwitch(&ParkSP); ParkS[1].s = ISS_ON; DEBUG(Logger::DBG_SESSION, "Dome already unparked."); IDSetSwitch(&ParkSP, nullptr); return IPS_OK; } if (weatherState == IPS_ALERT) { DEBUG(Logger::DBG_WARNING, "Weather is in the danger zone! Cannot unpark dome."); ParkSP.s = IPS_OK; IDSetSwitch(&ParkSP, nullptr); return IPS_ALERT; } ParkSP.s = UnPark(); if (ParkSP.s == IPS_OK) SetParked(false); else if (ParkSP.s == IPS_BUSY) setDomeState(DOME_UNPARKING); else IDSetSwitch(&ParkSP, nullptr); return ParkSP.s; } bool Dome::SetCurrentPark() { DEBUG(Logger::DBG_WARNING, "Parking is not supported."); return false; } bool Dome::SetDefaultPark() { DEBUG(Logger::DBG_WARNING, "Parking is not supported."); return false; } bool Dome::Handshake() { return false; } bool Dome::callHandshake() { if (domeConnection > 0) { if (getActiveConnection() == serialConnection) PortFD = serialConnection->getPortFD(); else if (getActiveConnection() == tcpConnection) PortFD = tcpConnection->getPortFD(); } return Handshake(); } uint8_t Dome::getDomeConnection() const { return domeConnection; } void Dome::setDomeConnection(const uint8_t &value) { uint8_t mask = CONNECTION_SERIAL | CONNECTION_TCP | CONNECTION_NONE; if (value == 0 || (mask & value) == 0) { DEBUGF(Logger::DBG_ERROR, "Invalid connection mode %d", value); return; } domeConnection = value; } } libindi/libs/indibase/indirotator.cpp0000664000175000017500000001330013263645557017240 0ustar jasemjasem/******************************************************************************* Copyright(c) 2017 Jasem Mutlaq. All rights reserved. This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2 as published by the Free Software Foundation. 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 Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. *******************************************************************************/ #include "indirotator.h" #include "connectionplugins/connectionserial.h" #include "connectionplugins/connectiontcp.h" #include namespace INDI { Rotator::Rotator() : RotatorInterface(this) { } Rotator::~Rotator() { } bool Rotator::initProperties() { DefaultDevice::initProperties(); RotatorInterface::initProperties(MAIN_CONTROL_TAB); // Presets IUFillNumber(&PresetN[0], "PRESET_1", "Preset 1", "%.f", 0, 360, 10, 0); IUFillNumber(&PresetN[1], "PRESET_2", "Preset 2", "%.f", 0, 360, 10, 0); IUFillNumber(&PresetN[2], "PRESET_3", "Preset 3", "%.f", 0, 360, 10, 0); IUFillNumberVector(&PresetNP, PresetN, 3, getDeviceName(), "Presets", "", "Presets", IP_RW, 0, IPS_IDLE); //Preset GOTO IUFillSwitch(&PresetGotoS[0], "Preset 1", "", ISS_OFF); IUFillSwitch(&PresetGotoS[1], "Preset 2", "", ISS_OFF); IUFillSwitch(&PresetGotoS[2], "Preset 3", "", ISS_OFF); IUFillSwitchVector(&PresetGotoSP, PresetGotoS, 3, getDeviceName(), "Goto", "", "Presets", IP_RW, ISR_1OFMANY, 0, IPS_IDLE); addDebugControl(); setDriverInterface(ROTATOR_INTERFACE); if (rotatorConnection & CONNECTION_SERIAL) { serialConnection = new Connection::Serial(this); serialConnection->registerHandshake([&]() { return callHandshake(); }); registerConnection(serialConnection); } if (rotatorConnection & CONNECTION_TCP) { tcpConnection = new Connection::TCP(this); tcpConnection->registerHandshake([&]() { return callHandshake(); }); registerConnection(tcpConnection); } return true; } void Rotator::ISGetProperties(const char *dev) { DefaultDevice::ISGetProperties(dev); // If connected, let's define properties. //if (isConnected()) // RotatorInterface::updateProperties(); return; } bool Rotator::updateProperties() { // #1 Update base device properties. DefaultDevice::updateProperties(); // #2 Update rotator interface properties RotatorInterface::updateProperties(); if (isConnected()) { defineNumber(&PresetNP); defineSwitch(&PresetGotoSP); } else { deleteProperty(PresetNP.name); deleteProperty(PresetGotoSP.name); } return true; } bool Rotator::ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) { if (dev != nullptr && strcmp(dev, getDeviceName()) == 0) { if (!strcmp(name, PresetNP.name)) { IUUpdateNumber(&PresetNP, values, names, n); PresetNP.s = IPS_OK; IDSetNumber(&PresetNP, nullptr); return true; } if (strstr(name, "ROTATOR")) { if (RotatorInterface::processNumber(dev, name, values, names, n)) return true; } } return DefaultDevice::ISNewNumber(dev, name, values, names, n); } bool Rotator::ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) { if (dev != nullptr && strcmp(dev, getDeviceName()) == 0) { if (!strcmp(PresetGotoSP.name, name)) { IUUpdateSwitch(&PresetGotoSP, states, names, n); int index = IUFindOnSwitchIndex(&PresetGotoSP); if (MoveRotator(PresetN[index].value) != IPS_ALERT) { PresetGotoSP.s = IPS_OK; DEBUGF(Logger::DBG_SESSION, "Moving to Preset %d with angle %g degrees.", index + 1, PresetN[index].value); IDSetSwitch(&PresetGotoSP, nullptr); return true; } PresetGotoSP.s = IPS_ALERT; IDSetSwitch(&PresetGotoSP, nullptr); return false; } if (strstr(name, "ROTATOR")) { if (RotatorInterface::processSwitch(dev, name, states, names, n)) return true; } } return DefaultDevice::ISNewSwitch(dev, name, states, names, n); } bool Rotator::Handshake() { return false; } bool Rotator::saveConfigItems(FILE *fp) { DefaultDevice::saveConfigItems(fp); IUSaveConfigNumber(fp, &PresetNP); IUSaveConfigSwitch(fp, &ReverseRotatorSP); return true; } bool Rotator::callHandshake() { if (rotatorConnection > 0) { if (getActiveConnection() == serialConnection) PortFD = serialConnection->getPortFD(); else if (getActiveConnection() == tcpConnection) PortFD = tcpConnection->getPortFD(); } return Handshake(); } uint8_t Rotator::getRotatorConnection() const { return rotatorConnection; } void Rotator::setRotatorConnection(const uint8_t &value) { uint8_t mask = CONNECTION_SERIAL | CONNECTION_TCP | CONNECTION_NONE; if (value == 0 || (mask & value) == 0) { DEBUGF(Logger::DBG_ERROR, "Invalid connection mode %d", value); return; } rotatorConnection = value; } } libindi/libs/indibase/hid_mac.c0000664000175000017500000007736213263645557015750 0ustar jasemjasem/* HIDAPI - Multi-Platform library for communication with HID devices. Copyright (c) 2009 by Alan Ott, Signal 11 Software (7/3/2010) All Rights Reserved. Changes for use with SX Filter Wheel INDI Driver by CloudMakers - 11/6/2012 This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program 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 General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. The full GNU General Public License is included in this distribution in the file called LICENSE. These files may also be found in the public source code repository located: http://github.com/signal11/hidapi */ /* See Apple Technical Note TN2187 for details on IOHidManager. */ #include "hidapi.h" #include "indidevapi.h" #include #include #include #include #include #include #include /* Barrier implementation because Mac OSX doesn't have pthread_barrier. It also doesn't have clock_gettime(). So much for POSIX and SUSv2. This implementation came from Brent Priddy and was posted on StackOverflow. It is used with his permission. */ typedef int pthread_barrierattr_t; typedef struct pthread_barrier { pthread_mutex_t mutex; pthread_cond_t cond; int count; int trip_count; } pthread_barrier_t; static int pthread_barrier_init(pthread_barrier_t *barrier, const pthread_barrierattr_t *attr, unsigned int count) { INDI_UNUSED(attr); if (count == 0) { errno = EINVAL; return -1; } if (pthread_mutex_init(&barrier->mutex, 0) < 0) { return -1; } if (pthread_cond_init(&barrier->cond, 0) < 0) { pthread_mutex_destroy(&barrier->mutex); return -1; } barrier->trip_count = count; barrier->count = 0; return 0; } static int pthread_barrier_destroy(pthread_barrier_t *barrier) { pthread_cond_destroy(&barrier->cond); pthread_mutex_destroy(&barrier->mutex); return 0; } static int pthread_barrier_wait(pthread_barrier_t *barrier) { pthread_mutex_lock(&barrier->mutex); ++(barrier->count); if (barrier->count >= barrier->trip_count) { barrier->count = 0; pthread_cond_broadcast(&barrier->cond); pthread_mutex_unlock(&barrier->mutex); return 1; } else { pthread_cond_wait(&barrier->cond, &(barrier->mutex)); pthread_mutex_unlock(&barrier->mutex); return 0; } } static int return_data(hid_device *dev, unsigned char *data, size_t length); /* Linked List of input reports received from the device. */ struct input_report { uint8_t *data; size_t len; struct input_report *next; }; struct hid_device_ { IOHIDDeviceRef device_handle; int blocking; int uses_numbered_reports; int disconnected; CFStringRef run_loop_mode; CFRunLoopRef run_loop; CFRunLoopSourceRef source; uint8_t *input_report_buf; CFIndex max_input_report_len; struct input_report *input_reports; pthread_t thread; pthread_mutex_t mutex; /* Protects input_reports */ pthread_cond_t condition; pthread_barrier_t barrier; /* Ensures correct startup sequence */ pthread_barrier_t shutdown_barrier; /* Ensures correct shutdown sequence */ int shutdown_thread; }; static hid_device *new_hid_device(void) { hid_device *dev = calloc(1, sizeof(hid_device)); dev->device_handle = NULL; dev->blocking = 1; dev->uses_numbered_reports = 0; dev->disconnected = 0; dev->run_loop_mode = NULL; dev->run_loop = NULL; dev->source = NULL; dev->input_report_buf = NULL; dev->input_reports = NULL; dev->shutdown_thread = 0; /* Thread objects */ pthread_mutex_init(&dev->mutex, NULL); pthread_cond_init(&dev->condition, NULL); pthread_barrier_init(&dev->barrier, NULL, 2); pthread_barrier_init(&dev->shutdown_barrier, NULL, 2); return dev; } static void free_hid_device(hid_device *dev) { if (!dev) return; /* Delete any input reports still left over. */ struct input_report *rpt = dev->input_reports; while (rpt) { struct input_report *next = rpt->next; free(rpt->data); free(rpt); rpt = next; } /* Free the string and the report buffer. The check for NULL is necessary here as CFRelease() doesn't handle NULL like free() and others do. */ if (dev->run_loop_mode) CFRelease(dev->run_loop_mode); if (dev->source) CFRelease(dev->source); free(dev->input_report_buf); /* Clean up the thread objects */ pthread_barrier_destroy(&dev->shutdown_barrier); pthread_barrier_destroy(&dev->barrier); pthread_cond_destroy(&dev->condition); pthread_mutex_destroy(&dev->mutex); /* Free the structure itself. */ free(dev); } static IOHIDManagerRef hid_mgr = 0x0; #if 0 static void register_error(hid_device * device, const char * op) { } #endif static int32_t get_int_property(IOHIDDeviceRef device, CFStringRef key) { CFTypeRef ref; int32_t value; ref = IOHIDDeviceGetProperty(device, key); if (ref) { if (CFGetTypeID(ref) == CFNumberGetTypeID()) { CFNumberGetValue((CFNumberRef)ref, kCFNumberSInt32Type, &value); return value; } } return 0; } static unsigned short get_vendor_id(IOHIDDeviceRef device) { return get_int_property(device, CFSTR(kIOHIDVendorIDKey)); } static unsigned short get_product_id(IOHIDDeviceRef device) { return get_int_property(device, CFSTR(kIOHIDProductIDKey)); } static int32_t get_max_report_length(IOHIDDeviceRef device) { return get_int_property(device, CFSTR(kIOHIDMaxInputReportSizeKey)); } static int get_string_property(IOHIDDeviceRef device, CFStringRef prop, wchar_t *buf, size_t len) { CFStringRef str; if (!len) return 0; str = IOHIDDeviceGetProperty(device, prop); buf[0] = 0; if (str) { len--; CFIndex str_len = CFStringGetLength(str); CFRange range; range.location = 0; range.length = (str_len > (long)len) ? len : str_len; CFIndex used_buf_len; CFIndex chars_copied; chars_copied = CFStringGetBytes(str, range, kCFStringEncodingUTF32LE, (char)'?', FALSE, (UInt8 *)buf, len, &used_buf_len); buf[chars_copied] = 0; return 0; } else return -1; } static int get_string_property_utf8(IOHIDDeviceRef device, CFStringRef prop, char *buf, size_t len) { CFStringRef str; if (!len) return 0; str = IOHIDDeviceGetProperty(device, prop); buf[0] = 0; if (str) { len--; CFIndex str_len = CFStringGetLength(str); CFRange range; range.location = 0; range.length = (str_len > (long)len) ? len : str_len; CFIndex used_buf_len; CFIndex chars_copied; chars_copied = CFStringGetBytes(str, range, kCFStringEncodingUTF8, (char)'?', FALSE, (UInt8 *)buf, len, &used_buf_len); buf[chars_copied] = 0; return used_buf_len; } else return 0; } static int get_serial_number(IOHIDDeviceRef device, wchar_t *buf, size_t len) { return get_string_property(device, CFSTR(kIOHIDSerialNumberKey), buf, len); } static int get_manufacturer_string(IOHIDDeviceRef device, wchar_t *buf, size_t len) { return get_string_property(device, CFSTR(kIOHIDManufacturerKey), buf, len); } static int get_product_string(IOHIDDeviceRef device, wchar_t *buf, size_t len) { return get_string_property(device, CFSTR(kIOHIDProductKey), buf, len); } /* Implementation of wcsdup() for Mac. */ static wchar_t *dup_wcs(const wchar_t *s) { size_t len = wcslen(s); wchar_t *ret = malloc((len + 1) * sizeof(wchar_t)); wcscpy(ret, s); return ret; } static int make_path(IOHIDDeviceRef device, char *buf, size_t len) { int res; unsigned short vid, pid; char transport[32]; buf[0] = '\0'; res = get_string_property_utf8(device, CFSTR(kIOHIDTransportKey), transport, sizeof(transport)); if (!res) return -1; vid = get_vendor_id(device); pid = get_product_id(device); res = snprintf(buf, len, "%s_%04hx_%04hx_%p", transport, vid, pid, device); buf[len - 1] = '\0'; return res + 1; } /* Initialize the IOHIDManager. Return 0 for success and -1 for failure. */ static int init_hid_manager(void) { /* Initialize all the HID Manager Objects */ hid_mgr = IOHIDManagerCreate(kCFAllocatorDefault, kIOHIDOptionsTypeNone); if (hid_mgr) { IOHIDManagerSetDeviceMatching(hid_mgr, NULL); IOHIDManagerScheduleWithRunLoop(hid_mgr, CFRunLoopGetCurrent(), kCFRunLoopDefaultMode); return 0; } return -1; } /* Initialize the IOHIDManager if necessary. This is the public function, and it is safe to call this function repeatedly. Return 0 for success and -1 for failure. */ int HID_API_EXPORT hid_init(void) { if (!hid_mgr) { return init_hid_manager(); } /* Already initialized. */ return 0; } int HID_API_EXPORT hid_exit(void) { if (hid_mgr) { /* Close the HID manager. */ IOHIDManagerClose(hid_mgr, kIOHIDOptionsTypeNone); CFRelease(hid_mgr); hid_mgr = NULL; } return 0; } static void process_pending_events(void) { SInt32 res; do { res = CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0.001, FALSE); } while (res != kCFRunLoopRunFinished && res != kCFRunLoopRunTimedOut); } struct hid_device_info HID_API_EXPORT *hid_enumerate(unsigned short vendor_id, unsigned short product_id) { struct hid_device_info *root = NULL; // return object struct hid_device_info *cur_dev = NULL; CFIndex num_devices; int i; /* Set up the HID Manager if it hasn't been done */ if (hid_init() < 0) return NULL; /* give the IOHIDManager a chance to update itself */ process_pending_events(); /* Get a list of the Devices */ CFSetRef device_set = IOHIDManagerCopyDevices(hid_mgr); /* Convert the list into a C array so we can iterate easily. */ num_devices = CFSetGetCount(device_set); IOHIDDeviceRef *device_array = calloc(num_devices, sizeof(IOHIDDeviceRef)); CFSetGetValues(device_set, (const void **)device_array); /* Iterate over each device, making an entry for it. */ for (i = 0; i < num_devices; i++) { unsigned short dev_vid; unsigned short dev_pid; #define BUF_LEN 256 wchar_t buf[BUF_LEN]; char cbuf[BUF_LEN]; IOHIDDeviceRef dev = device_array[i]; if (!dev) { continue; } dev_vid = get_vendor_id(dev); dev_pid = get_product_id(dev); /* Check the VID/PID against the arguments */ if ((vendor_id == 0x0 && product_id == 0x0) || (vendor_id == dev_vid && product_id == dev_pid)) { struct hid_device_info *tmp; size_t len; /* VID/PID match. Create the record. */ tmp = malloc(sizeof(struct hid_device_info)); if (cur_dev) { cur_dev->next = tmp; } else { root = tmp; } cur_dev = tmp; // Get the Usage Page and Usage for this device. cur_dev->usage_page = get_int_property(dev, CFSTR(kIOHIDPrimaryUsagePageKey)); cur_dev->usage = get_int_property(dev, CFSTR(kIOHIDPrimaryUsageKey)); /* Fill out the record */ cur_dev->next = NULL; len = make_path(dev, cbuf, sizeof(cbuf)); cur_dev->path = strdup(cbuf); /* Serial Number */ get_serial_number(dev, buf, BUF_LEN); cur_dev->serial_number = dup_wcs(buf); /* Manufacturer and Product strings */ get_manufacturer_string(dev, buf, BUF_LEN); cur_dev->manufacturer_string = dup_wcs(buf); get_product_string(dev, buf, BUF_LEN); cur_dev->product_string = dup_wcs(buf); /* VID/PID */ cur_dev->vendor_id = dev_vid; cur_dev->product_id = dev_pid; /* Release Number */ cur_dev->release_number = get_int_property(dev, CFSTR(kIOHIDVersionNumberKey)); /* Interface Number (Unsupported on Mac)*/ cur_dev->interface_number = -1; } } free(device_array); CFRelease(device_set); return root; } void HID_API_EXPORT hid_free_enumeration(struct hid_device_info *devs) { /* This function is identical to the Linux version. Platform independent. */ struct hid_device_info *d = devs; while (d) { struct hid_device_info *next = d->next; free(d->path); free(d->serial_number); free(d->manufacturer_string); free(d->product_string); free(d); d = next; } } hid_device *HID_API_EXPORT hid_open(unsigned short vendor_id, unsigned short product_id, const wchar_t *serial_number) { /* This function is identical to the Linux version. Platform independent. */ struct hid_device_info *devs, *cur_dev; const char *path_to_open = NULL; hid_device *handle = NULL; devs = hid_enumerate(vendor_id, product_id); cur_dev = devs; while (cur_dev) { if (cur_dev->vendor_id == vendor_id && cur_dev->product_id == product_id) { if (serial_number) { if (wcscmp(serial_number, cur_dev->serial_number) == 0) { path_to_open = cur_dev->path; break; } } else { path_to_open = cur_dev->path; break; } } cur_dev = cur_dev->next; } if (path_to_open) { /* Open the device */ handle = hid_open_path(path_to_open); } hid_free_enumeration(devs); return handle; } static void hid_device_removal_callback(void *context, IOReturn result, void *sender) { INDI_UNUSED(result); INDI_UNUSED(sender); /* Stop the Run Loop for this device. */ hid_device *d = context; d->disconnected = 1; CFRunLoopStop(d->run_loop); } /* The Run Loop calls this function for each input report received. This function puts the data into a linked list to be picked up by hid_read(). */ static void hid_report_callback(void *context, IOReturn result, void *sender, IOHIDReportType report_type, uint32_t report_id, uint8_t *report, CFIndex report_length) { INDI_UNUSED(result); INDI_UNUSED(sender); INDI_UNUSED(report_type); INDI_UNUSED(report_id); struct input_report *rpt; hid_device *dev = context; /* Make a new Input Report object */ rpt = calloc(1, sizeof(struct input_report)); rpt->data = calloc(1, report_length); memcpy(rpt->data, report, report_length); rpt->len = report_length; rpt->next = NULL; /* Lock this section */ pthread_mutex_lock(&dev->mutex); /* Attach the new report object to the end of the list. */ if (dev->input_reports == NULL) { /* The list is empty. Put it at the root. */ dev->input_reports = rpt; } else { /* Find the end of the list and attach. */ struct input_report *cur = dev->input_reports; int num_queued = 0; while (cur->next != NULL) { cur = cur->next; num_queued++; } cur->next = rpt; /* Pop one off if we've reached 30 in the queue. This way we don't grow forever if the user never reads anything from the device. */ if (num_queued > 30) { return_data(dev, NULL, 0); } } /* Signal a waiting thread that there is data. */ pthread_cond_signal(&dev->condition); /* Unlock */ pthread_mutex_unlock(&dev->mutex); } /* This gets called when the read_thred's run loop gets signaled by hid_close(), and serves to stop the read_thread's run loop. */ static void perform_signal_callback(void *context) { hid_device *dev = context; CFRunLoopStop(dev->run_loop); //TODO: CFRunLoopGetCurrent() } static void *read_thread(void *param) { hid_device *dev = param; /* Move the device's run loop to this thread. */ IOHIDDeviceScheduleWithRunLoop(dev->device_handle, CFRunLoopGetCurrent(), dev->run_loop_mode); /* Create the RunLoopSource which is used to signal the event loop to stop when hid_close() is called. */ CFRunLoopSourceContext ctx; memset(&ctx, 0, sizeof(ctx)); ctx.version = 0; ctx.info = dev; ctx.perform = &perform_signal_callback; dev->source = CFRunLoopSourceCreate(kCFAllocatorDefault, 0 /*order*/, &ctx); CFRunLoopAddSource(CFRunLoopGetCurrent(), dev->source, dev->run_loop_mode); /* Store off the Run Loop so it can be stopped from hid_close() and on device disconnection. */ dev->run_loop = CFRunLoopGetCurrent(); /* Notify the main thread that the read thread is up and running. */ pthread_barrier_wait(&dev->barrier); /* Run the Event Loop. CFRunLoopRunInMode() will dispatch HID input reports into the hid_report_callback(). */ SInt32 code; while (!dev->shutdown_thread && !dev->disconnected) { code = CFRunLoopRunInMode(dev->run_loop_mode, 1000 /*sec*/, FALSE); /* Return if the device has been disconnected */ if (code == kCFRunLoopRunFinished) { dev->disconnected = 1; break; } /* Break if The Run Loop returns Finished or Stopped. */ if (code != kCFRunLoopRunTimedOut && code != kCFRunLoopRunHandledSource) { /* There was some kind of error. Setting shutdown seems to make sense, but there may be something else more appropriate */ dev->shutdown_thread = 1; break; } } /* Now that the read thread is stopping, Wake any threads which are waiting on data (in hid_read_timeout()). Do this under a mutex to make sure that a thread which is about to go to sleep waiting on the condition acutally will go to sleep before the condition is signaled. */ pthread_mutex_lock(&dev->mutex); pthread_cond_broadcast(&dev->condition); pthread_mutex_unlock(&dev->mutex); /* Wait here until hid_close() is called and makes it past the call to CFRunLoopWakeUp(). This thread still needs to be valid when that function is called on the other thread. */ pthread_barrier_wait(&dev->shutdown_barrier); return NULL; } hid_device *HID_API_EXPORT hid_open_path(const char *path) { int i; hid_device *dev = NULL; CFIndex num_devices; dev = new_hid_device(); /* Set up the HID Manager if it hasn't been done */ if (hid_init() < 0) return NULL; /* give the IOHIDManager a chance to update itself */ process_pending_events(); CFSetRef device_set = IOHIDManagerCopyDevices(hid_mgr); num_devices = CFSetGetCount(device_set); IOHIDDeviceRef *device_array = calloc(num_devices, sizeof(IOHIDDeviceRef)); CFSetGetValues(device_set, (const void **)device_array); for (i = 0; i < num_devices; i++) { char cbuf[BUF_LEN]; size_t len; IOHIDDeviceRef os_dev = device_array[i]; len = make_path(os_dev, cbuf, sizeof(cbuf)); if (!strcmp(cbuf, path)) { // Matched Paths. Open this Device. IOReturn ret = IOHIDDeviceOpen(os_dev, kIOHIDOptionsTypeNone); if (ret == kIOReturnSuccess) { char str[32]; free(device_array); CFRetain(os_dev); CFRelease(device_set); dev->device_handle = os_dev; /* Create the buffers for receiving data */ dev->max_input_report_len = (CFIndex)get_max_report_length(os_dev); dev->input_report_buf = calloc(dev->max_input_report_len, sizeof(uint8_t)); /* Create the Run Loop Mode for this device. printing the reference seems to work. */ sprintf(str, "HIDAPI_%p", os_dev); dev->run_loop_mode = CFStringCreateWithCString(NULL, str, kCFStringEncodingASCII); /* Attach the device to a Run Loop */ IOHIDDeviceRegisterInputReportCallback(os_dev, dev->input_report_buf, dev->max_input_report_len, &hid_report_callback, dev); IOHIDDeviceRegisterRemovalCallback(dev->device_handle, hid_device_removal_callback, dev); /* Start the read thread */ pthread_create(&dev->thread, NULL, read_thread, dev); /* Wait here for the read thread to be initialized. */ pthread_barrier_wait(&dev->barrier); return dev; } else { goto return_error; } } } return_error: free(device_array); CFRelease(device_set); free_hid_device(dev); return NULL; } static int set_report(hid_device *dev, IOHIDReportType type, const unsigned char *data, size_t length) { const unsigned char *data_to_send; size_t length_to_send; IOReturn res; /* Return if the device has been disconnected. */ if (dev->disconnected) return -1; if (data[0] == 0x0) { /* Not using numbered Reports. Don't send the report number. */ data_to_send = data + 1; length_to_send = length - 1; } else { /* Using numbered Reports. Send the Report Number */ data_to_send = data; length_to_send = length; } if (!dev->disconnected) { res = IOHIDDeviceSetReport(dev->device_handle, type, data[0], /* Report ID*/ data_to_send, length_to_send); if (res == kIOReturnSuccess) { return length; } else return -1; } return -1; } int HID_API_EXPORT hid_write(hid_device *dev, const unsigned char *data, size_t length) { return set_report(dev, kIOHIDReportTypeOutput, data, length); } /* Helper function, so that this isn't duplicated in hid_read(). */ static int return_data(hid_device *dev, unsigned char *data, size_t length) { /* Copy the data out of the linked list item (rpt) into the return buffer (data), and delete the liked list item. */ struct input_report *rpt = dev->input_reports; size_t len = (length < rpt->len) ? length : rpt->len; memcpy(data, rpt->data, len); dev->input_reports = rpt->next; free(rpt->data); free(rpt); return len; } static int cond_wait(const hid_device *dev, pthread_cond_t *cond, pthread_mutex_t *mutex) { while (!dev->input_reports) { int res = pthread_cond_wait(cond, mutex); if (res != 0) return res; /* A res of 0 means we may have been signaled or it may be a spurious wakeup. Check to see that there's acutally data in the queue before returning, and if not, go back to sleep. See the pthread_cond_timedwait() man page for details. */ if (dev->shutdown_thread || dev->disconnected) return -1; } return 0; } static int cond_timedwait(const hid_device *dev, pthread_cond_t *cond, pthread_mutex_t *mutex, const struct timespec *abstime) { while (!dev->input_reports) { int res = pthread_cond_timedwait(cond, mutex, abstime); if (res != 0) return res; /* A res of 0 means we may have been signaled or it may be a spurious wakeup. Check to see that there's acutally data in the queue before returning, and if not, go back to sleep. See the pthread_cond_timedwait() man page for details. */ if (dev->shutdown_thread || dev->disconnected) return -1; } return 0; } int HID_API_EXPORT hid_read_timeout(hid_device *dev, unsigned char *data, size_t length, int milliseconds) { int bytes_read = -1; /* Lock the access to the report list. */ pthread_mutex_lock(&dev->mutex); /* There's an input report queued up. Return it. */ if (dev->input_reports) { /* Return the first one */ bytes_read = return_data(dev, data, length); goto ret; } /* Return if the device has been disconnected. */ if (dev->disconnected) { bytes_read = -1; goto ret; } if (dev->shutdown_thread) { /* This means the device has been closed (or there has been an error. An error code of -1 should be returned. */ bytes_read = -1; goto ret; } /* There is no data. Go to sleep and wait for data. */ if (milliseconds == -1) { /* Blocking */ int res; res = cond_wait(dev, &dev->condition, &dev->mutex); if (res == 0) bytes_read = return_data(dev, data, length); else { /* There was an error, or a device disconnection. */ bytes_read = -1; } } else if (milliseconds > 0) { /* Non-blocking, but called with timeout. */ int res; struct timespec ts; struct timeval tv; gettimeofday(&tv, NULL); TIMEVAL_TO_TIMESPEC(&tv, &ts); ts.tv_sec += milliseconds / 1000; ts.tv_nsec += (milliseconds % 1000) * 1000000; if (ts.tv_nsec >= 1000000000L) { ts.tv_sec++; ts.tv_nsec -= 1000000000L; } res = cond_timedwait(dev, &dev->condition, &dev->mutex, &ts); if (res == 0) bytes_read = return_data(dev, data, length); else if (res == ETIMEDOUT) bytes_read = 0; else bytes_read = -1; } else { /* Purely non-blocking */ bytes_read = 0; } ret: /* Unlock */ pthread_mutex_unlock(&dev->mutex); return bytes_read; } int HID_API_EXPORT hid_read(hid_device *dev, unsigned char *data, size_t length) { return hid_read_timeout(dev, data, length, (dev->blocking) ? -1 : 0); } int HID_API_EXPORT hid_set_nonblocking(hid_device *dev, int nonblock) { /* All Nonblocking operation is handled by the library. */ dev->blocking = !nonblock; return 0; } int HID_API_EXPORT hid_send_feature_report(hid_device *dev, const unsigned char *data, size_t length) { return set_report(dev, kIOHIDReportTypeFeature, data, length); } int HID_API_EXPORT hid_get_feature_report(hid_device *dev, unsigned char *data, size_t length) { CFIndex len = length; IOReturn res; /* Return if the device has been unplugged. */ if (dev->disconnected) return -1; res = IOHIDDeviceGetReport(dev->device_handle, kIOHIDReportTypeFeature, data[0], /* Report ID */ data, &len); if (res == kIOReturnSuccess) return len; else return -1; } void HID_API_EXPORT hid_close(hid_device *dev) { if (!dev) return; /* Disconnect the report callback before close. */ if (!dev->disconnected) { IOHIDDeviceRegisterInputReportCallback(dev->device_handle, dev->input_report_buf, dev->max_input_report_len, NULL, dev); IOHIDManagerRegisterDeviceRemovalCallback(hid_mgr, NULL, dev); IOHIDDeviceUnscheduleFromRunLoop(dev->device_handle, dev->run_loop, dev->run_loop_mode); IOHIDDeviceScheduleWithRunLoop(dev->device_handle, CFRunLoopGetMain(), kCFRunLoopDefaultMode); } /* Cause read_thread() to stop. */ dev->shutdown_thread = 1; /* Wake up the run thread's event loop so that the thread can exit. */ CFRunLoopSourceSignal(dev->source); CFRunLoopWakeUp(dev->run_loop); /* Notify the read thread that it can shut down now. */ pthread_barrier_wait(&dev->shutdown_barrier); /* Wait for read_thread() to end. */ pthread_join(dev->thread, NULL); /* Close the OS handle to the device, but only if it's not been unplugged. If it's been unplugged, then calling IOHIDDeviceClose() will crash. */ if (!dev->disconnected) { IOHIDDeviceClose(dev->device_handle, kIOHIDOptionsTypeNone); } /* Clear out the queue of received reports. */ pthread_mutex_lock(&dev->mutex); while (dev->input_reports) { return_data(dev, NULL, 0); } pthread_mutex_unlock(&dev->mutex); CFRelease(dev->device_handle); free_hid_device(dev); } int HID_API_EXPORT_CALL hid_get_manufacturer_string(hid_device *dev, wchar_t *string, size_t maxlen) { return get_manufacturer_string(dev->device_handle, string, maxlen); } int HID_API_EXPORT_CALL hid_get_product_string(hid_device *dev, wchar_t *string, size_t maxlen) { return get_product_string(dev->device_handle, string, maxlen); } int HID_API_EXPORT_CALL hid_get_serial_number_string(hid_device *dev, wchar_t *string, size_t maxlen) { return get_serial_number(dev->device_handle, string, maxlen); } int HID_API_EXPORT_CALL hid_get_indexed_string(hid_device *dev, int string_index, wchar_t *string, size_t maxlen) { // TODO: INDI_UNUSED(dev); INDI_UNUSED(string_index); INDI_UNUSED(string); INDI_UNUSED(maxlen); return 0; } HID_API_EXPORT const wchar_t *HID_API_CALL hid_error(hid_device *dev) { // TODO: INDI_UNUSED(dev); return NULL; } #if 0 static int32_t get_location_id(IOHIDDeviceRef device) { return get_int_property(device, CFSTR(kIOHIDLocationIDKey)); } static int32_t get_usage(IOHIDDeviceRef device) { int32_t res; res = get_int_property(device, CFSTR(kIOHIDDeviceUsageKey)); if (!res) res = get_int_property(device, CFSTR(kIOHIDPrimaryUsageKey)); return res; } static int32_t get_usage_page(IOHIDDeviceRef device) { int32_t res; res = get_int_property(device, CFSTR(kIOHIDDeviceUsagePageKey)); if (!res) res = get_int_property(device, CFSTR(kIOHIDPrimaryUsagePageKey)); return res; } static int get_transport(IOHIDDeviceRef device, wchar_t * buf, size_t len) { return get_string_property(device, CFSTR(kIOHIDTransportKey), buf, len); } int main(void) { IOHIDManagerRef mgr; int i; mgr = IOHIDManagerCreate(kCFAllocatorDefault, kIOHIDOptionsTypeNone); IOHIDManagerSetDeviceMatching(mgr, NULL); IOHIDManagerOpen(mgr, kIOHIDOptionsTypeNone); CFSetRef device_set = IOHIDManagerCopyDevices(mgr); CFIndex num_devices = CFSetGetCount(device_set); IOHIDDeviceRef * device_array = calloc(num_devices, sizeof(IOHIDDeviceRef)); CFSetGetValues(device_set, (const void **) device_array); for (i = 0; i < num_devices; i++) { IOHIDDeviceRef dev = device_array[i]; printf("Device: %p\n", dev); printf(" %04hx %04hx\n", get_vendor_id(dev), get_product_id(dev)); wchar_t serial[256], buf[256]; char cbuf[256]; get_serial_number(dev, serial, 256); printf(" Serial: %ls\n", serial); printf(" Loc: %ld\n", get_location_id(dev)); get_transport(dev, buf, 256); printf(" Trans: %ls\n", buf); make_path(dev, cbuf, 256); printf(" Path: %s\n", cbuf); } return 0; } #endif libindi/libs/indibase/indiccd.h0000664000175000017500000007145213263645557015760 0ustar jasemjasem/******************************************************************************* Copyright(c) 2010-2018 Jasem Mutlaq. All rights reserved. Copyright(c) 2010, 2011 Gerry Rozema. All rights reserved. Rapid Guide support added by CloudMakers, s. r. o. Copyright(c) 2013 CloudMakers, s. r. o. All rights reserved. Star detection algorithm is based on PHD Guiding by Craig Stark Copyright (c) 2006-2010 Craig Stark. All rights reserved. This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2 as published by the Free Software Foundation. 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 Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. *******************************************************************************/ #pragma once #include "defaultdevice.h" #include "indiguiderinterface.h" #include #include #include #include #include #define WITH_EXPOSURE_LOOPING extern const char *IMAGE_SETTINGS_TAB; extern const char *IMAGE_INFO_TAB; extern const char *GUIDE_HEAD_TAB; extern const char *RAPIDGUIDE_TAB; namespace INDI { class StreamManager; /** * @brief The CCDChip class provides functionality of a CCD Chip within a CCD. */ class CCDChip { public: CCDChip(); ~CCDChip(); typedef enum { LIGHT_FRAME = 0, BIAS_FRAME, DARK_FRAME, FLAT_FRAME } CCD_FRAME; typedef enum { FRAME_X, FRAME_Y, FRAME_W, FRAME_H } CCD_FRAME_INDEX; typedef enum { BIN_W, BIN_H } CCD_BIN_INDEX; typedef enum { CCD_MAX_X, CCD_MAX_Y, CCD_PIXEL_SIZE, CCD_PIXEL_SIZE_X, CCD_PIXEL_SIZE_Y, CCD_BITSPERPIXEL } CCD_INFO_INDEX; /** * @brief getXRes Get the horizontal resolution in pixels of the CCD Chip. * @return the horizontal resolution of the CCD Chip. */ inline int getXRes() { return XRes; } /** * @brief Get the vertical resolution in pixels of the CCD Chip. * @return the horizontal resolution of the CCD Chip. */ inline int getYRes() { return YRes; } /** * @brief getSubX Get the starting left coordinates (X) of the frame. * @return the starting left coordinates (X) of the image. */ inline int getSubX() { return SubX; } /** * @brief getSubY Get the starting top coordinates (Y) of the frame. * @return the starting top coordinates (Y) of the image. */ inline int getSubY() { return SubY; } /** * @brief getSubW Get the width of the frame * @return unbinned width of the frame */ inline int getSubW() { return SubW; } /** * @brief getSubH Get the height of the frame * @return unbinned height of the frame */ inline int getSubH() { return SubH; } /** * @brief getBinX Get horizontal binning of the CCD chip. * @return horizontal binning of the CCD chip. */ inline int getBinX() { return BinX; } /** * @brief getBinY Get vertical binning of the CCD chip. * @return vertical binning of the CCD chip. */ inline int getBinY() { return BinY; } /** * @brief getPixelSizeX Get horizontal pixel size in microns. * @return horizontal pixel size in microns. */ inline float getPixelSizeX() { return PixelSizex; } /** * @brief getPixelSizeY Get vertical pixel size in microns. * @return vertical pixel size in microns. */ inline float getPixelSizeY() { return PixelSizey; } /** * @brief getBPP Get CCD Chip depth (bits per pixel). * @return bits per pixel. */ inline int getBPP() { return BPP; } /** * @brief getFrameBufferSize Get allocated frame buffer size to hold the CCD image frame. * @return allocated frame buffer size to hold the CCD image frame. */ inline int getFrameBufferSize() { return RawFrameSize; } /** * @brief getExposureLeft Get exposure time left in seconds. * @return exposure time left in seconds. */ inline double getExposureLeft() { return ImageExposureN[0].value; } /** * @brief getExposureDuration Get requested exposure duration for the CCD chip in seconds. * @return requested exposure duration for the CCD chip in seconds. */ inline double getExposureDuration() { return exposureDuration; } /** * @brief getExposureStartTime * @return exposure start time in ISO 8601 format. */ const char *getExposureStartTime(); /** * @brief getFrameBuffer Get raw frame buffer of the CCD chip. * @return raw frame buffer of the CCD chip. */ inline uint8_t *getFrameBuffer() { return RawFrame; } /** * @brief setFrameBuffer Set raw frame buffer pointer. * @param buffer pointer to frame buffer * /note CCD Chip allocates the frame buffer internally once SetFrameBufferSize is called * with allocMem set to true which is the default behavior. If you allocated the memory * yourself (i.e. allocMem is false), then you must call this function to set the pointer * to the raw frame buffer. */ void setFrameBuffer(uint8_t *buffer) { RawFrame = buffer; } /** * @brief isCompressed * @return True if frame is compressed, false otherwise. */ inline bool isCompressed() { return SendCompressed; } /** * @brief isInterlaced * @return True if CCD chip is Interlaced, false otherwise. */ inline bool isInterlaced() { return Interlaced; } /** * @brief getFrameType * @return CCD Frame type */ inline CCD_FRAME getFrameType() { return FrameType; } /** * @brief getFrameTypeName returns CCD Frame type name * @param fType type of frame * @return CCD Frame type name */ const char *getFrameTypeName(CCD_FRAME fType); /** * @brief Return CCD Info Property */ INumberVectorProperty *getCCDInfo() { return &ImagePixelSizeNP; } /** * @brief setResolution set CCD Chip resolution * @param x width * @param y height */ void setResolution(int x, int y); /** * @brief setFrame Set desired frame resolutoin for an exposure. * @param subx Left position. * @param suby Top position. * @param subw unbinned width of the frame. * @param subh unbinned height of the frame. */ void setFrame(int subx, int suby, int subw, int subh); /** * @brief setBin Set CCD Chip binnig * @param hor Horizontal binning. * @param ver Vertical binning. */ void setBin(int hor, int ver); /** * @brief setMinMaxStep for a number property element * @param property Property name * @param element Element name * @param min Minimum element value * @param max Maximum element value * @param step Element step value * @param sendToClient If true (default), the element limits are updated and is sent to the * client. If false, the element limits are updated without getting sent to the client. */ void setMinMaxStep(const char *property, const char *element, double min, double max, double step, bool sendToClient = true); /** * @brief setPixelSize Set CCD Chip pixel size * @param x Horziontal pixel size in microns. * @param y Vertical pixel size in microns. */ void setPixelSize(float x, float y); /** * @brief setCompressed Set whether a frame is compressed after exposure? * @param cmp If true, compress frame. */ void setCompressed(bool cmp); /** * @brief setInterlaced Set whether the CCD chip is interlaced or not? * @param intr If true, the CCD chip is interlaced. */ void setInterlaced(bool intr); /** * @brief setFrameBufferSize Set desired frame buffer size. The function will allocate memory * accordingly. The frame size depends on the desired frame resolution (Left, Top, Width, Height), * depth of the CCD chip (bpp), and binning settings. You must set the frame size any time any of * the prior parameters gets updated. * @param nbuf size of buffer in bytes. * @param allocMem if True, it will allocate memory of nbut size bytes. */ void setFrameBufferSize(int nbuf, bool allocMem = true); /** * @brief setBPP Set depth of CCD chip. * @param bpp bits per pixel */ void setBPP(int bpp); /** * @brief setFrameType Set desired frame type for next exposure. * @param type desired CCD frame type. */ void setFrameType(CCD_FRAME type); /** * @brief setExposureDuration Set desired CCD frame exposure duration for next exposure. You must * call this function immediately before starting the actual exposure as it is used to calculate * the timestamp used for the FITS header. * @param duration exposure duration in seconds. */ void setExposureDuration(double duration); /** * @brief setExposureLeft Update exposure time left. Inform the client of the new exposure time * left value. * @param duration exposure duration left in seconds. */ void setExposureLeft(double duration); /** * @brief setExposureFailed Alert the client that the exposure failed. */ void setExposureFailed(); /** * @return Get number of FITS axis in image. By default 2 */ int getNAxis() const; /** * @brief setNAxis Set FITS number of axis * @param value number of axis */ void setNAxis(int value); /** * @brief setImageExtension Set image exntension * @param ext extension (fits, jpeg, raw..etc) */ void setImageExtension(const char *ext); /** * @return Return image extension (fits, jpeg, raw..etc) */ char *getImageExtension() { return imageExtention; } /** * @return True if CCD is currently exposing, false otherwise. */ bool isExposing() { return (ImageExposureNP.s == IPS_BUSY); } /** * @brief binFrame Perform softwre binning on the CCD frame. Only use this function if hardware * binning is not supported. */ void binFrame(); private: /// Native x resolution of the ccd int XRes; /// Native y resolution of the ccd int YRes; /// Left side of the subframe we are requesting int SubX; /// Top of the subframe requested int SubY; /// UNBINNED width of the subframe int SubW; /// UNBINNED height of the subframe int SubH; /// Binning requested in the x direction int BinX; /// Binning requested in the y direction int BinY; /// # of Axis int NAxis; /// Pixel size in microns, x direction float PixelSizex; /// Pixel size in microns, y direction float PixelSizey; /// Bytes per Pixel int BPP; bool Interlaced = false; uint8_t *RawFrame = nullptr; uint8_t *BinFrame = nullptr; int RawFrameSize = 0; bool SendCompressed = false; CCD_FRAME FrameType; double exposureDuration; timeval startExposureTime; int lastRapidX; int lastRapidY; char imageExtention[MAXINDIBLOBFMT]; INumberVectorProperty ImageExposureNP; INumber ImageExposureN[1]; ISwitchVectorProperty AbortExposureSP; ISwitch AbortExposureS[1]; INumberVectorProperty ImageFrameNP; INumber ImageFrameN[4]; INumberVectorProperty ImageBinNP; INumber ImageBinN[2]; INumberVectorProperty ImagePixelSizeNP; INumber ImagePixelSizeN[6]; ISwitch FrameTypeS[5]; ISwitchVectorProperty FrameTypeSP; ISwitch CompressS[2]; ISwitchVectorProperty CompressSP; IBLOB FitsB; IBLOBVectorProperty FitsBP; ISwitch RapidGuideS[2]; ISwitchVectorProperty RapidGuideSP; ISwitch RapidGuideSetupS[3]; ISwitchVectorProperty RapidGuideSetupSP; INumber RapidGuideDataN[3]; INumberVectorProperty RapidGuideDataNP; ISwitch ResetS[1]; ISwitchVectorProperty ResetSP; friend class CCD; friend class StreamRecoder; }; /** * \class CCD * \brief Class to provide general functionality of CCD cameras with a single CCD sensor, or a * primary CCD sensor in addition to a secondary CCD guide head. * * The CCD capabilities must be set to select which features are exposed to the clients. * SetCCDCapability() is typically set in the constructor or initProperties(), but can also be * called after connection is established with the CCD, but must be called /em before returning * true in Connect(). * * It also implements the interface to perform guiding. The class enable the ability to \e snoop * on telescope equatorial coordinates and record them in the FITS file before upload. It also * snoops Sky-Quality-Meter devices to record sky quality in units of Magnitudes-Per-Arcsecond-Squared * (MPASS) in the FITS header. * * Support for streaming is available (Linux only) and is handled by the StreamRecorder class. * * Developers need to subclass INDI::CCD to implement any driver for CCD cameras within INDI. * * \example CCD Simulator * \author Jasem Mutlaq, Gerry Rozema * */ class CCD : public DefaultDevice, GuiderInterface { public: CCD(); virtual ~CCD(); enum { CCD_CAN_BIN = 1 << 0, /*!< Does the CCD support binning? */ CCD_CAN_SUBFRAME = 1 << 1, /*!< Does the CCD support setting ROI? */ CCD_CAN_ABORT = 1 << 2, /*!< Can the CCD exposure be aborted? */ CCD_HAS_GUIDE_HEAD = 1 << 3, /*!< Does the CCD have a guide head? */ CCD_HAS_ST4_PORT = 1 << 4, /*!< Does the CCD have an ST4 port? */ CCD_HAS_SHUTTER = 1 << 5, /*!< Does the CCD have a mechanical shutter? */ CCD_HAS_COOLER = 1 << 6, /*!< Does the CCD have a cooler and temperature control? */ CCD_HAS_BAYER = 1 << 7, /*!< Does the CCD send color data in bayer format? */ CCD_HAS_STREAMING = 1 << 8 /*!< Does the CCD support live video streaming? */ } CCDCapability; typedef enum { UPLOAD_CLIENT, UPLOAD_LOCAL, UPLOAD_BOTH } CCD_UPLOAD_MODE; virtual bool initProperties(); virtual bool updateProperties(); virtual void ISGetProperties(const char *dev); virtual bool ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n); virtual bool ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n); virtual bool ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n); virtual bool ISSnoopDevice(XMLEle *root); protected: /** * @brief GetCCDCapability returns the CCD capabilities. */ uint32_t GetCCDCapability() const { return capability; } /** * @brief SetCCDCapability Set the CCD capabilities. Al fields must be initilized. * @param cap pointer to CCDCapability struct. */ void SetCCDCapability(uint32_t cap); /** * @return True if CCD can abort exposure. False otherwise. */ bool CanAbort() { return capability & CCD_CAN_ABORT; } /** * @return True if CCD supports binning. False otherwise. */ bool CanBin() { return capability & CCD_CAN_BIN; } /** * @return True if CCD supports subframing. False otherwise. */ bool CanSubFrame() { return capability & CCD_CAN_SUBFRAME; } /** * @return True if CCD has guide head. False otherwise. */ bool HasGuideHead() { return capability & CCD_HAS_GUIDE_HEAD; } /** * @return True if CCD has mechanical or electronic shutter. False otherwise. */ bool HasShutter() { return capability & CCD_HAS_SHUTTER; } /** * @return True if CCD has ST4 port for guiding. False otherwise. */ bool HasST4Port() { return capability & CCD_HAS_ST4_PORT; } /** * @return True if CCD has cooler and temperature can be controlled. False otherwise. */ bool HasCooler() { return capability & CCD_HAS_COOLER; } /** * @return True if CCD sends image data in bayer format. False otherwise. */ bool HasBayer() { return capability & CCD_HAS_BAYER; } /** * @return True if the CCD supports live video streaming. False otherwise. */ bool HasStreaming() { return capability & CCD_HAS_STREAMING; } /** * @brief Set CCD temperature * @param temperature CCD temperature in degrees celcius. * @return 0 or 1 if setting the temperature call to the hardware is successful. -1 if an * error is encountered. * Return 0 if setting the temperature to the requested value takes time. * Return 1 if setting the temperature to the requested value is complete. * \note Upon returning 0, the property becomes BUSY. Once the temperature reaches the requested * value, change the state to OK. * \note This function is not implemented in CCD, it must be implemented in the child class */ virtual int SetTemperature(double temperature); /** * \brief Start exposing primary CCD chip * \param duration Duration in seconds * \return true if OK and exposure will take some time to complete, false on error. * \note This function is not implemented in CCD, it must be implemented in the child class */ virtual bool StartExposure(float duration); /** * \brief Uploads target Chip exposed buffer as FITS to the client. Dervied classes should class * this function when an exposure is complete. * @param targetChip chip that contains upload image data * \note This function is not implemented in CCD, it must be implemented in the child class */ virtual bool ExposureComplete(CCDChip *targetChip); /** * \brief Abort ongoing exposure * \return true is abort is successful, false otherwise. * \note This function is not implemented in CCD, it must be implemented in the child class */ virtual bool AbortExposure(); /** * \brief Start exposing guide CCD chip * \param duration Duration in seconds * \return true if OK and exposure will take some time to complete, false on error. * \note This function is not implemented in CCD, it must be implemented in the child class */ virtual bool StartGuideExposure(float duration); /** * \brief Abort ongoing exposure * \return true is abort is successful, false otherwise. * \note This function is not implemented in CCD, it must be implemented in the child class */ virtual bool AbortGuideExposure(); /** * \brief CCD calls this function when CCD Frame dimension needs to be updated in the * hardware. Derived classes should implement this function * \param x Subframe X coordinate in pixels. * \param y Subframe Y coordinate in pixels. * \param w Subframe width in pixels. * \param h Subframe height in pixels. * \note (0,0) is defined as most left, top pixel in the subframe. * \return true is CCD chip update is successful, false otherwise. * \note This function is not implemented in CCD, it must be implemented in the child class */ virtual bool UpdateCCDFrame(int x, int y, int w, int h); /** * \brief CCD calls this function when Guide head frame dimension is updated by the * client. Derived classes should implement this function * \param x Subframe X coordinate in pixels. * \param y Subframe Y coordinate in pixels. * \param w Subframe width in pixels. * \param h Subframe height in pixels. * \note (0,0) is defined as most left, top pixel in the subframe. * \return true is CCD chip update is successful, false otherwise. * \note This function is not implemented in CCD, it must be implemented in the child class */ virtual bool UpdateGuiderFrame(int x, int y, int w, int h); /** * \brief CCD calls this function when CCD Binning needs to be updated in the hardware. * Derived classes should implement this function * \param hor Horizontal binning. * \param ver Vertical binning. * \return true is CCD chip update is successful, false otherwise. * \note This function is not implemented in CCD, it must be implemented in the child class */ virtual bool UpdateCCDBin(int hor, int ver); /** * \brief CCD calls this function when Guide head binning is updated by the client. * Derived classes should implement this function * \param hor Horizontal binning. * \param ver Vertical binning. * \return true is CCD chip update is successful, false otherwise. * \note This function is not implemented in CCD, it must be implemented in the child class */ virtual bool UpdateGuiderBin(int hor, int ver); /** * \brief CCD calls this function when CCD frame type needs to be updated in the hardware. * \param fType Frame type * \return true is CCD chip update is successful, false otherwise. * \note It is \e not mandatory to implement this function in the child class. The CCD hardware * layer may either set the frame type when this function is called, or (optionally) before an * exposure is started. */ virtual bool UpdateCCDFrameType(CCDChip::CCD_FRAME fType); /** * \brief CCD calls this function when client upload mode switch is updated. * \param mode upload mode. UPLOAD_CLIENT only sends the upload the client application. UPLOAD_BOTH saves the frame and uploads it to the client. UPLOAD_LOCAL only saves * the frame locally. * \return true if mode is changed successfully, false otherwise. * \note By default this function is implemented in the base class and returns true. Override if necessary. */ virtual bool UpdateCCDUploadMode(CCD_UPLOAD_MODE mode) { INDI_UNUSED(mode); return true; } /** * \brief CCD calls this function when Guide frame type is updated by the client. * \param fType Frame type * \return true is CCD chip update is successful, false otherwise. * \note It is \e not mandatory to implement this function in the child class. The CCD hardware * layer may either set the frame type when this function is called, or (optionally) before an * exposure is started. */ virtual bool UpdateGuiderFrameType(CCDChip::CCD_FRAME fType); /** * \brief Setup CCD paramters for primary CCD. Child classes call this function to update * CCD parameters * \param x Frame X coordinates in pixels. * \param y Frame Y coordinates in pixels. * \param bpp Bits Per Pixels. * \param xf X pixel size in microns. * \param yf Y pixel size in microns. */ virtual void SetCCDParams(int x, int y, int bpp, float xf, float yf); /** * \brief Setup CCD paramters for guide head CCD. Child classes call this function to update * CCD parameters * \param x Frame X coordinates in pixels. * \param y Frame Y coordinates in pixels. * \param bpp Bits Per Pixels. * \param xf X pixel size in microns. * \param yf Y pixel size in microns. */ virtual void SetGuiderParams(int x, int y, int bpp, float xf, float yf); /** * \brief Guide northward for ms milliseconds * \param ms Duration in milliseconds. * \note This function is not implemented in CCD, it must be implemented in the child class * \return True if successful, false otherwise. */ virtual IPState GuideNorth(float ms); /** * \brief Guide southward for ms milliseconds * \param ms Duration in milliseconds. * \note This function is not implemented in CCD, it must be implemented in the child class * \return 0 if successful, -1 otherwise. */ virtual IPState GuideSouth(float ms); /** * \brief Guide easward for ms milliseconds * \param ms Duration in milliseconds. * \note This function is not implemented in CCD, it must be implemented in the child class * \return 0 if successful, -1 otherwise. */ virtual IPState GuideEast(float ms); /** * \brief Guide westward for ms milliseconds * \param ms Duration in milliseconds. * \note This function is not implemented in CCD, it must be implemented in the child class * \return 0 if successful, -1 otherwise. */ virtual IPState GuideWest(float ms); /** * @brief StartStreaming Start live video streaming * @return True if successful, false otherwise. */ virtual bool StartStreaming(); /** * @brief StopStreaming Stop live video streaming * @return True if successful, false otherwise. */ virtual bool StopStreaming(); /** * \brief Add FITS keywords to a fits file * \param fptr pointer to a valid FITS file. * \param targetChip The target chip to extract the keywords from. * \note In additional to the standard FITS keywords, this function write the following * keywords the FITS file: *
    *
  • EXPTIME: Total Exposure Time (s)
  • *
  • DARKTIME (if applicable): Total Exposure Time (s)
  • *
  • PIXSIZE1: Pixel Size 1 (microns)
  • *
  • PIXSIZE2: Pixel Size 2 (microns)
  • *
  • BINNING: Binning HOR x VER
  • *
  • FRAME: Frame Type
  • *
  • DATAMIN: Minimum value
  • *
  • DATAMAX: Maximum value
  • *
  • INSTRUME: CCD Name
  • *
  • DATE-OBS: UTC start date of observation
  • *
* * To add additional information, override this function in the child class and ensure to call * CCD::addFITSKeywords. */ virtual void addFITSKeywords(fitsfile *fptr, CCDChip *targetChip); /** A function to just remove GCC warnings about deprecated conversion */ void fits_update_key_s(fitsfile *fptr, int type, std::string name, void *p, std::string explanation, int *status); /** * @brief activeDevicesUpdated Inform children that ActiveDevices property was updated so they can * snoop on the updated devices if desired. */ virtual void activeDevicesUpdated() {} /** * @brief saveConfigItems Save configuration items in XML file. * @param fp pointer to file to write to * @return True if successful, false otherwise */ virtual bool saveConfigItems(FILE *fp); void GuideComplete(INDI_EQ_AXIS axis); // Epoch Position double RA, Dec; // J2000 Position double J2000RA; double J2000DE; double primaryFocalLength, primaryAperture, guiderFocalLength, guiderAperture; bool InExposure; bool InGuideExposure; bool RapidGuideEnabled; bool GuiderRapidGuideEnabled; bool AutoLoop; bool GuiderAutoLoop; bool SendImage; bool GuiderSendImage; bool ShowMarker; bool GuiderShowMarker; float ExposureTime; float GuiderExposureTime; // Sky Quality double MPSAS; // Rotator Angle double RotatorAngle; // Airmas double Airmass; double Latitude; double Longitude; std::vector FilterNames; int CurrentFilterSlot; std::unique_ptr Streamer; CCDChip PrimaryCCD; CCDChip GuideCCD; // We are going to snoop these from a telescope INumberVectorProperty EqNP; INumber EqN[2]; ITextVectorProperty ActiveDeviceTP; IText ActiveDeviceT[4] {}; enum { SNOOP_MOUNT, SNOOP_ROTATOR, SNOOP_FILTER_WHEEL, SNOOP_SQM }; INumber TemperatureN[1]; INumberVectorProperty TemperatureNP; IText BayerT[3] {}; ITextVectorProperty BayerTP; IText FileNameT[1] {}; ITextVectorProperty FileNameTP; ISwitch UploadS[3]; ISwitchVectorProperty UploadSP; IText UploadSettingsT[2] {}; ITextVectorProperty UploadSettingsTP; enum { UPLOAD_DIR, UPLOAD_PREFIX }; ISwitch TelescopeTypeS[2]; ISwitchVectorProperty TelescopeTypeSP; enum { TELESCOPE_PRIMARY, TELESCOPE_GUIDE }; // WCS ISwitch WorldCoordS[2]; ISwitchVectorProperty WorldCoordSP; // WCS CCD Rotation INumber CCDRotationN[1]; INumberVectorProperty CCDRotationNP; #ifdef WITH_EXPOSURE_LOOPING // Exposure Looping ISwitch ExposureLoopS[2]; ISwitchVectorProperty ExposureLoopSP; enum { EXPOSURE_LOOP_ON, EXPOSURE_LOOP_OFF }; // Exposure Looping Count INumber ExposureLoopCountN[1]; INumberVectorProperty ExposureLoopCountNP; double uploadTime = { 0 }; std::chrono::system_clock::time_point exposureLoopStartup; #endif // FITS Header IText FITSHeaderT[2] {}; ITextVectorProperty FITSHeaderTP; enum { FITS_OBSERVER, FITS_OBJECT }; private: uint32_t capability; bool ValidCCDRotation; bool uploadFile(CCDChip *targetChip, const void *fitsData, size_t totalBytes, bool sendImage, bool saveImage); void getMinMax(double *min, double *max, CCDChip *targetChip); int getFileIndex(const char *dir, const char *prefix, const char *ext); friend class StreamManager; }; } libindi/libs/indibase/inditelescope.h0000664000175000017500000006661713263645557017221 0ustar jasemjasem/******************************************************************************* Copyright(c) 2011 Gerry Rozema, Jasem Mutlaq. All rights reserved. This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2 as published by the Free Software Foundation. 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 Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. *******************************************************************************/ #pragma once #include "defaultdevice.h" #include #include /** * \class Telescope * \brief Class to provide general functionality of a telescope device. * * Developers need to subclass Telescope to implement any driver for telescopes within INDI. * * Implementing a basic telescope driver involves the child class performing the following steps: *
    *
  • The child class should define the telescope capabilities via the TelescopeCapability structure * and sets in the default constructor.
  • *
  • If the telescope has additional properties, the child class should override initProperties and * initialize the respective additional properties.
  • *
  • The child class can optionally set the connection mode in initProperties(). By default the driver * provide controls for both serial and TCP/IP connections.
  • *
  • Once the parent class calls Connect(), the child class attempts to connect to the telescope and * return either success of failure
  • *
  • Telescope calls updateProperties() to enable the child class to define which properties to * send to the client upon connection
  • *
  • Telescope calls ReadScopeStatus() to check the link to the telescope and update its state * and position. The child class should call newRaDec() whenever * a new value is read from the telescope.
  • *
  • The child class should implement Goto() and Sync(), and Park()/UnPark() if applicable.
  • *
  • Telescope calls disconnect() when the client request a disconnection. The child class * should remove any additional properties it defined in updateProperties() if applicable
  • *
* * TrackState is used to monitor changes in Tracking state. There are three main tracking properties: * + TrackMode: Changes tracking mode or rate. Common modes are TRACK_SIDEREAL, TRACK_LUNAR, TRACK_SOLAR, and TRACK_CUSTOM * + TrackRate: If the mount supports custom tracking rates, it should set the capability flag TELESCOPE_HAS_TRACK_RATE. If the user * changes the custom tracking rates while the mount is tracking, it it sent to the child class via SetTrackRate(...) function. * The base class will reject any track rates that switch from positive to negative (reverse) tracking rates as the mount must be stopped before * such change takes place. * + TrackState: Engages or Disengages tracking. When engaging tracking, the child class should take the necessary steps to set the appropiate TrackMode and TrackRate * properties before or after engaging tracking as governed by the mount protocol. * * Ideally, the child class should avoid changing property states directly within a function call from the base class as such state changes take place in the base class * after checking the return values of such functions. * \author Jasem Mutlaq, Gerry Rozema * \see TelescopeSimulator and SynScan drivers for examples of implementations of Telescope. */ namespace INDI { class Telescope : public DefaultDevice { public: enum TelescopeStatus { SCOPE_IDLE, SCOPE_SLEWING, SCOPE_TRACKING, SCOPE_PARKING, SCOPE_PARKED }; enum TelescopeMotionCommand { MOTION_START = 0, MOTION_STOP }; enum TelescopeSlewRate { SLEW_GUIDE, SLEW_CENTERING, SLEW_FIND, SLEW_MAX }; enum TelescopeTrackMode { TRACK_SIDEREAL, TRACK_SOLAR, TRACK_LUNAR, TRACK_CUSTOM }; enum TelescopeTrackState { TRACK_ON, TRACK_OFF, TRACK_UNKNOWN }; enum TelescopeParkData { PARK_NONE, PARK_RA_DEC, PARK_HA_DEC, PARK_AZ_ALT, PARK_RA_DEC_ENCODER, PARK_AZ_ALT_ENCODER }; enum TelescopeLocation { LOCATION_LATITUDE, LOCATION_LONGITUDE, LOCATION_ELEVATION }; enum TelescopePierSide { PIER_UNKNOWN = -1, PIER_WEST = 0, PIER_EAST = 1 }; enum TelescopePECState { PEC_UNKNOWN = -1, PEC_OFF = 0, PEC_ON = 1 }; /** * \struct TelescopeConnection * \brief Holds the connection mode of the telescope. */ enum { CONNECTION_NONE = 1 << 0, /** Do not use any connection plugin */ CONNECTION_SERIAL = 1 << 1, /** For regular serial and bluetooth connections */ CONNECTION_TCP = 1 << 2 /** For Wired and WiFI connections */ } TelescopeConnection; /** * \struct TelescopeCapability * \brief Holds the capabilities of a telescope. */ enum { TELESCOPE_CAN_GOTO = 1 << 0, /** Can the telescope go to to specific coordinates? */ TELESCOPE_CAN_SYNC = 1 << 1, /** Can the telescope sync to specific coordinates? */ TELESCOPE_CAN_PARK = 1 << 2, /** Can the telescope park? */ TELESCOPE_CAN_ABORT = 1 << 3, /** Can the telescope abort motion? */ TELESCOPE_HAS_TIME = 1 << 4, /** Does the telescope have configurable date and time settings? */ TELESCOPE_HAS_LOCATION = 1 << 5, /** Does the telescope have configuration location settings? */ TELESCOPE_HAS_PIER_SIDE = 1 << 6, /** Does the telescope have pier side property? */ TELESCOPE_HAS_PEC = 1 << 7, /** Does the telescope have PEC playback? */ TELESCOPE_HAS_TRACK_MODE = 1 << 8, /** Does the telescope have track modes (sidereal, lunar, solar..etc)? */ TELESCOPE_CAN_CONTROL_TRACK = 1 << 9, /** Can the telescope engage and disengage tracking? */ TELESCOPE_HAS_TRACK_RATE = 1 << 10, /** Does the telescope have custom track rates? */ } TelescopeCapability; Telescope(); virtual ~Telescope(); virtual bool ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n); virtual bool ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n); virtual bool ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n); virtual void ISGetProperties(const char *dev); virtual bool ISSnoopDevice(XMLEle *root); /** * @brief GetTelescopeCapability returns the capability of the Telescope */ uint32_t GetTelescopeCapability() const { return capability; } /** * @brief SetTelescopeCapability sets the Telescope capabilities. All capabilities must be initialized. * @param cap ORed list of telescope capabilities. * @param slewRateCount Number of slew rates supported by the telescope. If < 4 (default is 0), * no slew rate properties will be defined to the client. If >=4, the driver will construct the default * slew rate property TELESCOPE_SLEW_RATE with SLEW_GUIDE, SLEW_CENTERING, SLEW_FIND, and SLEW_MAX * members where SLEW_GUIDE is the at the lowest setting and SLEW_MAX is at the highest. */ void SetTelescopeCapability(uint32_t cap, uint8_t slewRateCount = 0); /** * @return True if telescope support goto operations */ bool CanGOTO() { return capability & TELESCOPE_CAN_GOTO; } /** * @return True if telescope support sync operations */ bool CanSync() { return capability & TELESCOPE_CAN_SYNC; } /** * @return True if telescope can abort motion. */ bool CanAbort() { return capability & TELESCOPE_CAN_ABORT; } /** * @return True if telescope can park. */ bool CanPark() { return capability & TELESCOPE_CAN_PARK; } /** * @return True if telescope can enagle and disengage tracking. */ bool CanControlTrack() { return capability & TELESCOPE_CAN_CONTROL_TRACK; } /** * @return True if telescope time can be updated. */ bool HasTime() { return capability & TELESCOPE_HAS_TIME; } /** * @return True if telescope location can be updated. */ bool HasLocation() { return capability & TELESCOPE_HAS_LOCATION; } /** * @return True if telescope supports pier side property */ bool HasPierSide() { return capability & TELESCOPE_HAS_PIER_SIDE; } /** * @return True if telescope supports PEC playback property */ bool HasPECState() { return capability & TELESCOPE_HAS_PEC; } /** * @return True if telescope supports track modes */ bool HasTrackMode() { return capability & TELESCOPE_HAS_TRACK_MODE; } /** * @return True if telescope supports custom tracking rates. */ bool HasTrackRate() { return capability & TELESCOPE_HAS_TRACK_RATE; } /** \brief Called to initialize basic properties required all the time */ virtual bool initProperties(); /** \brief Called when connected state changes, to add/remove properties */ virtual bool updateProperties(); /** \brief perform handshake with device to check communication */ virtual bool Handshake(); /** \brief Called when setTimer() time is up */ virtual void TimerHit(); /** * \brief setParkDataType Sets the type of parking data stored in the park data file and * presented to the user. * \param type parking data type. If PARK_NONE then no properties will be presented to the * user for custom parking position. */ void SetParkDataType(TelescopeParkData type); /** * @brief InitPark Loads parking data (stored in ~/.indi/ParkData.xml) that contains parking status * and parking position. * @return True if loading is successful and data is read, false otherwise. On success, you must call * SetAxis1ParkDefault() and SetAxis2ParkDefault() to set the default parking values. On failure, * you must call SetAxis1ParkDefault() and SetAxis2ParkDefault() to set the default parking values * in addition to SetAxis1Park() and SetAxis2Park() to set the current parking position. */ bool InitPark(); /** * @brief isParked is mount currently parked? * @return True if parked, false otherwise. */ bool isParked(); /** * @brief SetParked Change the mount parking status. The data park file (stored in * ~/.indi/ParkData.xml) is updated in the process. * @param isparked set to true if parked, false otherwise. */ void SetParked(bool isparked); /** * @return Get current RA/AZ parking position. */ double GetAxis1Park() const; /** * @return Get default RA/AZ parking position. */ double GetAxis1ParkDefault() const; /** * @return Get current DEC/ALT parking position. */ double GetAxis2Park() const; /** * @return Get defailt DEC/ALT parking position. */ double GetAxis2ParkDefault() const; /** * @brief SetRAPark Set current RA/AZ parking position. The data park file (stored in * ~/.indi/ParkData.xml) is updated in the process. * @param value current Axis 1 value (RA or AZ either in angles or encoder values as specified * by the TelescopeParkData type). */ void SetAxis1Park(double value); /** * @brief SetRAPark Set default RA/AZ parking position. * @param value Default Axis 1 value (RA or AZ either in angles or encoder values as specified * by the TelescopeParkData type). */ void SetAxis1ParkDefault(double steps); /** * @brief SetDEPark Set current DEC/ALT parking position. The data park file (stored in * ~/.indi/ParkData.xml) is updated in the process. * @param value current Axis 1 value (DEC or ALT either in angles or encoder values as specified * by the TelescopeParkData type). */ void SetAxis2Park(double steps); /** * @brief SetDEParkDefault Set default DEC/ALT parking position. * @param value Default Axis 2 value (DEC or ALT either in angles or encoder values as specified * by the TelescopeParkData type). */ void SetAxis2ParkDefault(double steps); /** * @brief isLocked is mount currently locked? * @return true if lock status equals true and DomeClosedLockTP is Dome Locks or Dome Locks and * Dome Parks (both). */ bool isLocked() const; // Joystick helpers static void joystickHelper(const char *joystick_n, double mag, double angle, void *context); static void buttonHelper(const char *button_n, ISState state, void *context); /** * @brief setTelescopeConnection Set telescope connection mode. Child class should call this * in the constructor before Telescope registers any connection interfaces * @param value ORed combination of TelescopeConnection values. */ void setTelescopeConnection(const uint8_t &value); /** * @return Get current telescope connection mode */ uint8_t getTelescopeConnection() const; void setPierSide(TelescopePierSide side); TelescopePierSide getPierSide() { return currentPierSide; } void setPECState(TelescopePECState state); TelescopePECState getPECState() { return currentPECState; } protected: virtual bool saveConfigItems(FILE *fp); /** \brief The child class calls this function when it has updates */ void NewRaDec(double ra, double dec); /** * \brief Read telescope status. * * This function checks the following: *
    *
  1. Check if the link to the telescope is alive.
  2. *
  3. Update telescope status: Idle, Slewing, Parking..etc.
  4. *
  5. Read coordinates
  6. *
* \return True if reading scope status is OK, false if an error is encounterd. * \note This function is not implemented in Telescope, it must be implemented in the * child class */ virtual bool ReadScopeStatus() = 0; /** * \brief Move the scope to the supplied RA and DEC coordinates * \return True if successful, false otherwise * \note If not implemented by the child class, this function by default returns false with a * warning message. */ virtual bool Goto(double ra, double dec); /** * \brief Set the telescope current RA and DEC coordinates to the supplied RA and DEC coordinates * \return True if successful, false otherwise * \note If not implemented by the child class, this function by default returns false with a * warning message. */ virtual bool Sync(double ra, double dec); /** * \brief Start or Stop the telescope motion in the direction dir. * \param dir direction of motion * \param command Start or Stop command * \return True if successful, false otherwise * \note If not implemented by the child class, this function by default returns false with a * warning message. */ virtual bool MoveNS(INDI_DIR_NS dir, TelescopeMotionCommand command); /** * \brief Move the telescope in the direction dir. * \param dir direction of motion * \param command Start or Stop command * \return True if successful, false otherwise * \note If not implemented by the child class, this function by default returns false with a * warning message. */ virtual bool MoveWE(INDI_DIR_WE dir, TelescopeMotionCommand command); /** * \brief Park the telescope to its home position. * \return True if successful, false otherwise * \note If not implemented by the child class, this function by default returns false with a * warning message. */ virtual bool Park(); /** * \brief Unpark the telescope if already parked. * \return True if successful, false otherwise * \note If not implemented by the child class, this function by default returns false with a * warning message. */ virtual bool UnPark(); /** * \brief Abort any telescope motion including tracking if possible. * \return True if successful, false otherwise * \note If not implemented by the child class, this function by default returns false with a * warning message. */ virtual bool Abort(); /** * @brief SetTrackMode Set active tracking mode. Do not change track state. * @param mode Index of track mode. * @return True if successful, false otherwise * @note If not implemented by the child class, this function by default returns false with a * warning message. */ virtual bool SetTrackMode(uint8_t mode); /** * @brief SetTrackRate Set custom tracking rates. * @param raRate RA tracking rate in arcsecs/s * @param deRate DEC tracking rate in arcsecs/s * @return True if successful, false otherwise * @note If not implemented by the child class, this function by default returns false with a * warning message. */ virtual bool SetTrackRate(double raRate, double deRate); /** * @brief AddTrackMode * @param name Name of track mode. It is recommended to use standard properties names such as TRACK_SIDEREAL..etc. * @param label Label of track mode that appears at the client side. * @param isDefault Set to true to mark the track mode as the default. Only one mode should be marked as default. * @return Index of added track mode * @note Child class should add all track modes be */ virtual int AddTrackMode(const char *name, const char *label, bool isDefault=false); /** * @brief SetTrackEnabled Engages or disengages mount tracking. If there are no tracking modes available, it is assumed sidereal. Otherwise, * whatever tracking mode should be activated or deactivated accordingly. * @param enabled True to engage tracking, false to stop tracking completely. * @return True if successful, false otherwise * @note If not implemented by the child class, this function by default returns false with a * warning message. */ virtual bool SetTrackEnabled(bool enabled); /** * \brief Update telescope time, date, and UTC offset. * \param utc UTC time. * \param utc_offset UTC offset in hours. * \return True if successful, false otherwise * \note If not implemented by the child class, this function by default returns false with a * warning message. */ virtual bool updateTime(ln_date *utc, double utc_offset); /** * \brief Update telescope location settings * \param latitude Site latitude in degrees. * \param longitude Site latitude in degrees increasing eastward from Greenwich (0 to 360). * \param elevation Site elevation in meters. * \return True if successful, false otherwise * \note If not implemented by the child class, this function by default returns false with a * warning message. */ virtual bool updateLocation(double latitude, double longitude, double elevation); /** * \brief SetParkPosition Set desired parking position to the supplied value. This ONLY sets the * desired park position value and does not perform parking. * \param Axis1Value First axis value * \param Axis2Value Second axis value * \return True if desired parking position is accepted and set. False otherwise. * \note If not implemented by the child class, this function by default returns false with a * warning message. */ virtual bool SetParkPosition(double Axis1Value, double Axis2Value); /** * @brief SetCurrentPark Set current coordinates/encoders value as the desired parking position * @return True if current mount coordinates are set as parking position, false on error. * \note If not implemented by the child class, this function by default returns false with a * warning message. */ virtual bool SetCurrentPark(); /** * @brief SetDefaultPark Set default coordinates/encoders value as the desired parking position * @return True if default park coordinates are set as parking position, false on error. * \note If not implemented by the child class, this function by default returns false with a * warning message. */ virtual bool SetDefaultPark(); /** * @brief SetSlewRate Set desired slew rate index. * @param index Index of slew rate where 0 is slowest rate and capability.nSlewRate-1 is maximum rate. * @return True is operation successful, false otherwise. * * \note This function as implemented in Telescope performs no function and always return * true. Only reimplement it if you need to issue a command to change the slew rate at the hardware * level. Most telescope drivers only utilize slew rate when issuing a motion command. */ virtual bool SetSlewRate(int index); /** * @brief callHandshake Helper function that sets the port file descriptor before calling the * actual Handshake function implenented in drivers * @return Result of actual device Handshake() */ bool callHandshake(); // Joystick void processNSWE(double mag, double angle); void processJoystick(const char *joystick_n, double mag, double angle); void processSlewPresets(double mag, double angle); void processButton(const char *button_n, ISState state); /** * @brief Load scope settings from XML files. * @return True if all config values were loaded otherwise false. */ bool LoadScopeConfig(); /** * @brief Load scope settings from XML files. * @return True if Config #1 exists otherwise false. */ bool HasDefaultScopeConfig(); /** * \brief Save scope settings to XML files. */ bool UpdateScopeConfig(); /** * @brief Validate a file name * @param file_name File name * @return True if the file name is valid otherwise false. */ std::string GetHomeDirectory() const; /** * @brief Get the scope config index * @return The scope config index */ int GetScopeConfigIndex() const; /** * @brief Check if a file exists and it is readable * @param file_name File name * @param writable Additional check if the file is writable * @return True if the checks are successful otherwise false. */ bool CheckFile(const std::string &file_name, bool writable) const; /** * This is a variable filled in by the ReadStatus telescope * low level code, used to report current state * are we slewing, tracking, or parked. */ TelescopeStatus TrackState; /** * @brief RememberTrackState Remember last state of Track State to fall back to in case of errors or aborts. */ TelescopeStatus RememberTrackState; // All telescopes should produce equatorial co-ordinates INumberVectorProperty EqNP; INumber EqN[2]; // When a goto is issued, domes will snoop the target property // to start moving the dome when a telescope moves INumberVectorProperty TargetNP; INumber TargetN[2]; // Abort motion ISwitchVectorProperty AbortSP; ISwitch AbortS[1]; // On a coord_set message, sync, or slew ISwitchVectorProperty CoordSP; ISwitch CoordS[3]; // A number vector that stores lattitude and longitude INumberVectorProperty LocationNP; INumber LocationN[3]; // A Switch in the client interface to park the scope ISwitchVectorProperty ParkSP; ISwitch ParkS[2]; // Custom parking position INumber ParkPositionN[2]; INumberVectorProperty ParkPositionNP; // Custom parking options ISwitch ParkOptionS[3]; ISwitchVectorProperty ParkOptionSP; // A switch for North/South motion ISwitch MovementNSS[2]; ISwitchVectorProperty MovementNSSP; // A switch for West/East motion ISwitch MovementWES[2]; ISwitchVectorProperty MovementWESP; // Slew Rate ISwitchVectorProperty SlewRateSP; ISwitch *SlewRateS; // Telescope & guider aperture and focal length INumber ScopeParametersN[4]; INumberVectorProperty ScopeParametersNP; // UTC and UTC Offset IText TimeT[2] {}; ITextVectorProperty TimeTP; void sendTimeFromSystem(); // Active GPS/Dome device to snoop ITextVectorProperty ActiveDeviceTP; IText ActiveDeviceT[2] {}; // Switch to lock if dome is closed, and or force parking if dome parks ISwitchVectorProperty DomeClosedLockTP; ISwitch DomeClosedLockT[4]; // Lock Joystick Axis to one direciton only ISwitch LockAxisS[2]; ISwitchVectorProperty LockAxisSP; // Pier Side ISwitch PierSideS[2]; ISwitchVectorProperty PierSideSP; // Pier Side TelescopePierSide lastPierSide, currentPierSide; // PEC State ISwitch PECStateS[2]; ISwitchVectorProperty PECStateSP; // Track Mode ISwitchVectorProperty TrackModeSP; ISwitch *TrackModeS { nullptr }; // Track State ISwitchVectorProperty TrackStateSP; ISwitch TrackStateS[2]; // Track Rate INumberVectorProperty TrackRateNP; INumber TrackRateN[2]; // PEC State TelescopePECState lastPECState, currentPECState; uint32_t capability; int last_we_motion, last_ns_motion; //Park char *LoadParkData(); bool WriteParkData(); int PortFD = -1; Connection::Serial *serialConnection = NULL; Connection::TCP *tcpConnection = NULL; // XML node names for scope config const std::string ScopeConfigRootXmlNode { "scopeconfig" }; const std::string ScopeConfigDeviceXmlNode { "device" }; const std::string ScopeConfigNameXmlNode { "name" }; const std::string ScopeConfigScopeFocXmlNode { "scopefoc" }; const std::string ScopeConfigScopeApXmlNode { "scopeap" }; const std::string ScopeConfigGScopeFocXmlNode { "gscopefoc" }; const std::string ScopeConfigGScopeApXmlNode { "gscopeap" }; const std::string ScopeConfigLabelApXmlNode { "label" }; // A switch to apply custom aperture/focal length config enum { SCOPE_CONFIG1, SCOPE_CONFIG2, SCOPE_CONFIG3, SCOPE_CONFIG4, SCOPE_CONFIG5, SCOPE_CONFIG6 }; ISwitch ScopeConfigs[6]; ISwitchVectorProperty ScopeConfigsSP; // Scope config name ITextVectorProperty ScopeConfigNameTP; IText ScopeConfigNameT[1] {}; /// The telescope/guide scope configuration file name const std::string ScopeConfigFileName; private: bool processTimeInfo(const char *utc, const char *offset); bool processLocationInfo(double latitude, double longitude, double elevation); void triggerSnoop(const char *driverName, const char *propertyName); TelescopeParkData parkDataType; bool IsLocked; bool IsParked; const char *ParkDeviceName; const std::string ParkDataFileName; XMLEle *ParkdataXmlRoot, *ParkdeviceXml, *ParkstatusXml, *ParkpositionXml, *ParkpositionAxis1Xml, *ParkpositionAxis2Xml; double Axis1ParkPosition; double Axis1DefaultParkPosition; double Axis2ParkPosition; double Axis2DefaultParkPosition; uint8_t nSlewRate; IPState lastEqState; uint8_t telescopeConnection = CONNECTION_SERIAL | CONNECTION_TCP; Controller *controller; }; } libindi/libs/indibase/indicontroller.cpp0000664000175000017500000002323213263645557017736 0ustar jasemjasem/******************************************************************************* Copyright (C) 2013 Jasem Mutlaq This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2 as published by the Free Software Foundation. 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 Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. *******************************************************************************/ #include "indicontroller.h" #include namespace INDI { Controller::Controller(DefaultDevice *cdevice) { device = cdevice; JoystickSettingT = nullptr; JoystickSettingTP.ntp = 0; joystickCallbackFunc = joystickEvent; axisCallbackFunc = axisEvent; buttonCallbackFunc = buttonEvent; } Controller::~Controller() { for (int i = 0; i < JoystickSettingTP.ntp; i++) free(JoystickSettingT[i].aux0); free(JoystickSettingT); } void Controller::mapController(const char *propertyName, const char *propertyLabel, ControllerType type, const char *initialValue) { if (JoystickSettingT == nullptr) JoystickSettingT = (IText *)malloc(sizeof(IText)); // Ignore duplicates for (int i = 0; i < JoystickSettingTP.ntp; i++) { if (!strcmp(propertyName, JoystickSettingT[i].name)) return; } JoystickSettingT = (IText *)realloc(JoystickSettingT, (JoystickSettingTP.ntp + 1) * sizeof(IText)); ControllerType *ctype = (ControllerType *)malloc(sizeof(ControllerType)); *ctype = type; memset(JoystickSettingT+JoystickSettingTP.ntp, 0, sizeof(IText)); IUFillText(&JoystickSettingT[JoystickSettingTP.ntp], propertyName, propertyLabel, initialValue); JoystickSettingT[JoystickSettingTP.ntp++].aux0 = ctype; IUFillTextVector(&JoystickSettingTP, JoystickSettingT, JoystickSettingTP.ntp, device->getDeviceName(), "JOYSTICKSETTINGS", "Settings", "Joystick", IP_RW, 0, IPS_IDLE); } void Controller::clearMap() { for (int i = 0; i < JoystickSettingTP.ntp; i++) { free(JoystickSettingT[i].aux0); free(JoystickSettingT[i].text); } JoystickSettingTP.ntp = 0; free(JoystickSettingT); JoystickSettingT = nullptr; } bool Controller::initProperties() { IUFillSwitch(&UseJoystickS[0], "ENABLE", "Enable", ISS_OFF); IUFillSwitch(&UseJoystickS[1], "DISABLE", "Disable", ISS_ON); IUFillSwitchVector(&UseJoystickSP, UseJoystickS, 2, device->getDeviceName(), "USEJOYSTICK", "Joystick", OPTIONS_TAB, IP_RW, ISR_1OFMANY, 0, IPS_IDLE); return true; } void Controller::ISGetProperties(const char *dev) { if (dev != nullptr && strcmp(dev, device->getDeviceName())) return; if (device->isConnected()) { device->defineSwitch(&UseJoystickSP); if (JoystickSettingT && UseJoystickS[0].s == ISS_ON) device->defineText(&JoystickSettingTP); } } bool Controller::updateProperties() { if (device->isConnected()) { device->defineSwitch(&UseJoystickSP); if (JoystickSettingT && UseJoystickS[0].s == ISS_ON) device->defineText(&JoystickSettingTP); } else { device->deleteProperty(UseJoystickSP.name); device->deleteProperty(JoystickSettingTP.name); } return true; } bool Controller::ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) { if (strcmp(dev, device->getDeviceName()) == 0) { // Enable joystick support if (!strcmp(name, UseJoystickSP.name)) { IUUpdateSwitch(&UseJoystickSP, states, names, n); UseJoystickSP.s = IPS_OK; if (UseJoystickSP.sp[0].s == ISS_ON) enableJoystick(); else disableJoystick(); IDSetSwitch(&UseJoystickSP, nullptr); return true; } } return false; } bool Controller::ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n) { if (strcmp(dev, device->getDeviceName()) == 0) { if (!strcmp(name, "JOYSTICKSETTINGS") && n <= JoystickSettingTP.ntp) { for (int i = 0; i < JoystickSettingTP.ntp; i++) { IText *tp = IUFindText(&JoystickSettingTP, names[i]); if (tp) { ControllerType cType = getControllerType(texts[i]); ControllerType myType = *((ControllerType *)JoystickSettingT[i].aux0); if (cType != myType) { JoystickSettingTP.s = IPS_ALERT; IDSetText(&JoystickSettingTP, nullptr); DEBUGFDEVICE(dev, INDI::Logger::DBG_ERROR, "Cannot change controller type to %s.", texts[i]); return false; } } } IUUpdateText(&JoystickSettingTP, texts, names, n); for (int i = 0; i < n; i++) { if (strstr(JoystickSettingT[i].text, "JOYSTICK_")) IDSnoopDevice("Joystick", JoystickSettingT[i].text); } JoystickSettingTP.s = IPS_OK; IDSetText(&JoystickSettingTP, nullptr); return true; } } return false; } bool Controller::ISSnoopDevice(XMLEle *root) { XMLEle *ep = nullptr; double mag = 0, angle = 0; // If joystick is disabled, do not process anything. if (UseJoystickSP.sp[0].s == ISS_OFF) return false; const char *propName = findXMLAttValu(root, "name"); // Check axis if (!strcmp("JOYSTICK_AXIS", propName)) { for (ep = nextXMLEle(root, 1); ep != nullptr; ep = nextXMLEle(root, 0)) { const char *elemName = findXMLAttValu(ep, "name"); const char *setting = getControllerSetting(elemName); if (setting == nullptr) return false; mag = atof(pcdataXMLEle(ep)); axisCallbackFunc(setting, mag, device); } } // Check buttons else if (!strcmp("JOYSTICK_BUTTONS", propName)) { for (ep = nextXMLEle(root, 1); ep != nullptr; ep = nextXMLEle(root, 0)) { const char *elemName = findXMLAttValu(ep, "name"); const char *setting = getControllerSetting(elemName); if (setting == nullptr) continue; buttonCallbackFunc(setting, strcmp(pcdataXMLEle(ep), "Off") ? ISS_ON : ISS_OFF, device); } } // Check joysticks else if (strstr(propName, "JOYSTICK_")) { const char *setting = getControllerSetting(propName); // We don't have this here, so let's not process it if (setting == nullptr) return false; for (ep = nextXMLEle(root, 1); ep != nullptr; ep = nextXMLEle(root, 0)) { if (!strcmp("JOYSTICK_MAGNITUDE", findXMLAttValu(ep, "name"))) mag = atof(pcdataXMLEle(ep)); else if (!strcmp("JOYSTICK_ANGLE", findXMLAttValu(ep, "name"))) angle = atof(pcdataXMLEle(ep)); } joystickCallbackFunc(setting, mag, angle, device); } return false; } bool Controller::saveConfigItems(FILE *fp) { IUSaveConfigSwitch(fp, &UseJoystickSP); IUSaveConfigText(fp, &JoystickSettingTP); return true; } void Controller::enableJoystick() { device->defineText(&JoystickSettingTP); for (int i = 0; i < JoystickSettingTP.ntp; i++) { if (strstr(JoystickSettingTP.tp[i].text, "JOYSTICK_")) IDSnoopDevice("Joystick", JoystickSettingTP.tp[i].text); } IDSnoopDevice("Joystick", "JOYSTICK_AXIS"); IDSnoopDevice("Joystick", "JOYSTICK_BUTTONS"); } void Controller::disableJoystick() { device->deleteProperty(JoystickSettingTP.name); } void Controller::setJoystickCallback(joystickFunc JoystickCallback) { joystickCallbackFunc = JoystickCallback; } void Controller::setAxisCallback(axisFunc AxisCallback) { axisCallbackFunc = AxisCallback; } void Controller::setButtonCallback(buttonFunc buttonCallback) { buttonCallbackFunc = buttonCallback; } void Controller::joystickEvent(const char *joystick_n, double mag, double angle, void *context) { INDI_UNUSED(joystick_n); INDI_UNUSED(mag); INDI_UNUSED(angle); INDI_UNUSED(context); } void Controller::axisEvent(const char *axis_n, int value, void *context) { INDI_UNUSED(axis_n); INDI_UNUSED(value); INDI_UNUSED(context); } void Controller::buttonEvent(const char *button_n, int button_value, void *context) { INDI_UNUSED(button_n); INDI_UNUSED(button_value); INDI_UNUSED(context); } Controller::ControllerType Controller::getControllerType(const char *name) { ControllerType targetType = CONTROLLER_UNKNOWN; if (strstr(name, "JOYSTICK_")) targetType = CONTROLLER_JOYSTICK; else if (strstr(name, "AXIS_")) targetType = CONTROLLER_AXIS; else if (strstr(name, "BUTTON_")) targetType = CONTROLLER_BUTTON; return targetType; } const char *Controller::getControllerSetting(const char *name) { for (int i = 0; i < JoystickSettingTP.ntp; i++) if (!strcmp(JoystickSettingT[i].text, name)) return JoystickSettingT[i].name; return nullptr; } } libindi/libs/indibase/baseclientqt.h0000664000175000017500000002433413263645557017036 0ustar jasemjasem/******************************************************************************* Copyright(c) 2016 Jasem Mutlaq. All rights reserved. INDI Qt Client This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2 as published by the Free Software Foundation. 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 Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. *******************************************************************************/ #pragma once #include "indiapi.h" #include "indidevapi.h" #include "indibase.h" #include #include #include #define MAXRBUF 2048 /** * \class INDI::BaseClientQt \brief Class to provide basic client functionality based on Qt5 toolkit and is therefore suitable for cross-platform development. BaseClientQt enables accelerated development of INDI Clients by providing a framework that facilitates communication, device handling, and event notification. By subclassing BaseClientQt, clients can quickly connect to an INDI server, and query for a set of INDI::BaseDevice devices, and read and write properties seamlessly. Event driven programming is possible due to notifications upon reception of new devices or properties. \attention All notifications functions defined in INDI::BaseMediator must be implemented in the client class even if they are not used because these are pure virtual functions. \see INDI Client Tutorial for more details. \author Jasem Mutlaq */ class INDI::BaseClientQt : public QObject, public INDI::BaseMediator { Q_OBJECT public: BaseClientQt(QObject *parent = Q_NULLPTR); virtual ~BaseClientQt(); /** \brief Set the server host name and port \param hostname INDI server host name or IP address. \param port INDI server port. */ void setServer(const char *hostname, unsigned int port); /** \brief Add a device to the watch list. A client may select to receive notifications of only a specific device or a set of devices. If the client encounters any of the devices set via this function, it will create a corresponding INDI::BaseDevice object to handle them. If no devices are watched, then all devices owned by INDI server will be created and handled. */ void watchDevice(const char *deviceName); /** \brief Connect to INDI server. \returns True if the connection is successful, false otherwise. \note This function blocks until connection is either successull or unsuccessful. */ bool connectServer(); /** \brief Disconnect from INDI server. Disconnects from INDI servers. Any devices previously created will be deleted and memory cleared. \return True if disconnection is successful, false otherwise. */ bool disconnectServer(); bool isServerConnected() const; /** \brief Connect to INDI driver \param deviceName Name of the device to connect to. */ void connectDevice(const char *deviceName); /** \brief Disconnect INDI driver \param deviceName Name of the device to disconnect. */ void disconnectDevice(const char *deviceName); /** \param deviceName Name of device to search for in the list of devices owned by INDI server, \returns If \e deviceName exists, it returns an instance of the device. Otherwise, it returns NULL. */ INDI::BaseDevice *getDevice(const char *deviceName); /** \returns Returns a vector of all devices created in the client. */ const std::vector &getDevices() const { return cDevices; } /** * @brief getDevices Returns list of devices that belong to a particular @ref INDI::BaseDevice::DRIVER_INTERFACE "DRIVER_INTERFACE" class. * * For example, to get a list of guide cameras: @code{.cpp} std::vector guideCameras; getDevices(guideCameras, CCD_INTERFACE | GUIDE_INTERFACE); for (INDI::BaseDevice *device : guideCameras) cout << "Guide Camera Name: " << device->getDeviceName(); @endcode * @param deviceList Supply device list to be filled by the function. * @param driverInterface ORed DRIVER_INTERFACE values to select the desired class of devices. * @return True if one or more devices are found for the supplied driverInterface, false if no matching devices found. */ bool getDevices(std::vector &deviceList, uint16_t driverInterface); /** \brief Set Binary Large Object policy mode Set the BLOB handling mode for the client. The client may either receive:
  • Only BLOBS
  • BLOBs mixed with normal messages
  • Normal messages only, no BLOBs
If \e dev and \e prop are supplied, then the BLOB handling policy is set for this particular device and property. if \e prop is NULL, then the BLOB policy applies to the whole device. \param blobH BLOB handling policy \param dev name of device, required. \param prop name of property, optional. */ void setBLOBMode(BLOBHandling blobH, const char *dev, const char *prop = NULL); /** * @brief getBLOBMode Get Binary Large Object policy mode IF set previously by setBLOBMode * @param dev name of device. * @param prop property name, can be NULL to return overall device policy if it exists. * @return BLOB Policy, if not found, it always returns B_ALSO */ BLOBHandling getBLOBMode(const char *dev, const char *prop = NULL); // Update static void *listenHelper(void *context); const char *getHost() { return cServer.c_str(); } int getPort() { return cPort; } /** \brief Send new Text command to server */ void sendNewText(ITextVectorProperty *pp); /** \brief Send new Text command to server */ void sendNewText(const char *deviceName, const char *propertyName, const char *elementName, const char *text); /** \brief Send new Number command to server */ void sendNewNumber(INumberVectorProperty *pp); /** \brief Send new Number command to server */ void sendNewNumber(const char *deviceName, const char *propertyName, const char *elementName, double value); /** \brief Send new Switch command to server */ void sendNewSwitch(ISwitchVectorProperty *pp); /** \brief Send new Switch command to server */ void sendNewSwitch(const char *deviceName, const char *propertyName, const char *elementName); /** \brief Send opening tag for BLOB command to server */ void startBlob(const char *devName, const char *propName, const char *timestamp); /** \brief Send ONE blob content to server. The BLOB data in raw binary format and will be converted to base64 and sent to server */ void sendOneBlob(IBLOB *bp); /** \brief Send ONE blob content to server. The BLOB data in raw binary format and will be converted to base64 and sent to server */ void sendOneBlob(const char *blobName, unsigned int blobSize, const char *blobFormat, void *blobBuffer); /** \brief Send closing tag for BLOB command to server */ void finishBlob(); /** * @brief setVerbose Set verbose mode * @param enable If true, enable FULL verbose output. Any XML message received, including BLOBs, are printed on * standard output. Only use this for debugging purposes. */ void setVerbose(bool enable) { verbose = enable; } /** * @brief isVerbose Is client in verbose mode? * @return Is client in verbose mode? */ bool isVerbose() const { return verbose; } /** * @brief setConnectionTimeout Set connection timeout. By default it is 3 seconds. * @param seconds seconds * @param microseconds microseconds */ void setConnectionTimeout(uint32_t seconds, uint32_t microseconds) { timeout_sec = seconds; timeout_us = microseconds; } protected: /** \brief Dispatch command received from INDI server to respective devices handled by the client */ int dispatchCommand(XMLEle *root, char *errmsg); /** \brief Remove device */ int deleteDevice(const char *devName, char *errmsg); /** \brief Delete property command */ int delPropertyCmd(XMLEle *root, char *errmsg); /** \brief Find and return a particular device */ INDI::BaseDevice *findDev(const char *devName, char *errmsg); /** \brief Add a new device */ INDI::BaseDevice *addDevice(XMLEle *dep, char *errmsg); /** \brief Find a device, and if it doesn't exist, create it if create is set to 1 */ INDI::BaseDevice *findDev(XMLEle *root, int create, char *errmsg); /** Process messages */ int messageCmd(XMLEle *root, char *errmsg); private: typedef struct { std::string device; std::string property; BLOBHandling blobMode; } BLOBMode; BLOBMode *findBLOBMode(const std::string& device, const std::string& property); /** \brief Connect/Disconnect to INDI driver \param status If true, the client will attempt to turn on CONNECTION property within the driver (i.e. turn on the device). Otherwise, CONNECTION will be turned off. \param deviceName Name of the device to connect to. */ void setDriverConnection(bool status, const char *deviceName); /** * @brief clear Clear devices and blob modes */ void clear(); QTcpSocket client_socket; std::vector cDevices; std::vector cDeviceNames; std::vector blobModes; std::string cServer; unsigned int cPort; bool sConnected; bool verbose; // Parse & FILE buffers for IO LilXML *lillp; /* XML parser context */ uint32_t timeout_sec, timeout_us; private slots: void listenINDI(); void processSocketError(QAbstractSocket::SocketError socketError); }; libindi/libs/indibase/indirotatorinterface.h0000664000175000017500000001324013263645557020571 0ustar jasemjasem/* Rotator Interface Copyright (C) 2017 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 */ #pragma once #include "indibase.h" #include using RI = INDI::RotatorInterface; /** * \class RotatorInterface \brief Provides interface to implement Rotator functionality. A Rotator can be an independent device, or an embedded Rotator within another device (usually a rotating focuser). Child class must implement all the pure virtual functions. Only absolute position Rotators are supported. Angle is ranged from 0 to 360 increasing clockwise when looking at the back of the camera. \e IMPORTANT: initRotatorProperties() must be called before any other function to initilize the Rotator properties. \e IMPORTANT: processRotatorNumber() must be called in your driver's ISNewNumber() function. Similary, processRotatorSwitch() must be called in ISNewSwitch() \author Jasem Mutlaq */ namespace INDI { class RotatorInterface { public: /** * \struct RotatorCapability * \brief Holds the capabilities of a Rotator. */ enum { ROTATOR_CAN_ABORT = 1 << 0, /** Can the Rotator abort motion once started? */ ROTATOR_CAN_HOME = 1 << 1, /** Can the Rotator go to home position? */ ROTATOR_CAN_SYNC = 1 << 2, /** Can the Rotator sync to specific tick? */ ROTATOR_CAN_REVERSE = 1 << 3, /** Can the Rotator reverse direction? */ } RotatorCapability; /** * @brief GetRotatorCapability returns the capability of the Rotator */ uint32_t GetCapability() const { return rotatorCapability; } /** * @brief SetRotatorCapability sets the Rotator capabilities. All capabilities must be initialized. * @param cap pointer to Rotator capability struct. */ void SetCapability(uint32_t cap) { rotatorCapability = cap; } /** * @return Whether Rotator can abort. */ bool CanAbort() { return rotatorCapability & ROTATOR_CAN_ABORT;} /** * @return Whether Rotator can go to home position. */ bool CanHome() { return rotatorCapability & ROTATOR_CAN_HOME;} /** * @return Whether Rotator can sync ticks position to a new one. */ bool CanSync() { return rotatorCapability & ROTATOR_CAN_SYNC;} /** * @return Whether Rotator can reverse direction. */ bool CanReverse() { return rotatorCapability & ROTATOR_CAN_REVERSE;} protected: explicit RotatorInterface(DefaultDevice *defaultDevice); /** * \brief Initilize Rotator properties. It is recommended to call this function within * initProperties() of your primary device * \param groupName Group or tab name to be used to define Rotator properties. */ void initProperties(const char *groupName); /** * @brief updateProperties Define or Delete Rotator properties based on the connection status of the base device * @return True if successful, false otherwise. */ bool updateProperties(); /** \brief Process Rotator number properties */ bool processNumber(const char *dev, const char *name, double values[], char *names[], int n); /** \brief Process Rotator switch properties */ bool processSwitch(const char *dev, const char *name, ISState *states, char *names[], int n); /** * @brief MoveRotator Go to specific angle * @param angle Target angle in degrees. * @return State of operation: IPS_OK is motion is completed, IPS_BUSY if motion in progress, IPS_ALERT on error. */ virtual IPState MoveRotator(double angle) = 0; /** * @brief SyncRotator Set current angle as the supplied angle without moving the rotator. * @param ticks Desired new angle. * @return True if successful, false otherwise. */ virtual bool SyncRotator(double angle); /** * @brief HomeRotator Go to home position. * @return State of operation: IPS_OK is motion is completed, IPS_BUSY if motion in progress, IPS_ALERT on error. */ virtual IPState HomeRotator(); /** * @brief ReverseRotator Reverse the direction of the rotator. CW is usually the normal direction, and CCW is the reversed direction. * @param enable if True, reverse direction. If false, revert to normal direction. * @return True if successful, false otherwise. */ virtual bool ReverseRotator(bool enabled); /** * @brief AbortRotator Abort all motion * @return True if successful, false otherwise. */ virtual bool AbortRotator(); INumber GotoRotatorN[1]; INumberVectorProperty GotoRotatorNP; INumber SyncRotatorN[1]; INumberVectorProperty SyncRotatorNP; ISwitch AbortRotatorS[1]; ISwitchVectorProperty AbortRotatorSP; ISwitch HomeRotatorS[1]; ISwitchVectorProperty HomeRotatorSP; ISwitch ReverseRotatorS[2]; ISwitchVectorProperty ReverseRotatorSP; enum { REVERSE_ENABLED, REVERSE_DISABLED }; uint32_t rotatorCapability = 0; DefaultDevice *m_defaultDevice { nullptr }; }; } libindi/libs/indibase/indigps.h0000664000175000017500000000735613263645557016022 0ustar jasemjasem/******************************************************************************* Copyright(c) 2015 Jasem Mutlaq. All rights reserved. INDI GPS Device Class This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program 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 General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. The full GNU General Public License is included in this distribution in the file called LICENSE. *******************************************************************************/ #pragma once #include "defaultdevice.h" /** * \class GPS \brief Class to provide general functionality of a GPS device. The GPS provides a simple interface for GPS devices. It reports time in INDI standard property TIME_UTC. Location is reported in INDI standard property GEOGRAPHIC_COORD Only one function is called by the INDI framework to update GPS data (updateGPS()). If the data is valid, it is sent to the client. If GPS data is not ready yet, updateGPS will be called every second until the data becomes available and then INDI sends the data to the client. updateGPS() is called upon successful connection and whenever the client requests a data refresh. \example GPS Simulator is available under Auxiliary drivers as a sample implementation of GPS \e IMPORTANT: GEOGRAPHIC_COORD stores latitude and longitude in INDI specific format, refer to INDI Standard Properties for details. \author Jasem Mutlaq */ namespace INDI { class GPS : public DefaultDevice { public: enum GPSLocation { LOCATION_LATITUDE, LOCATION_LONGITUDE, LOCATION_ELEVATION }; GPS() = default; virtual ~GPS() = default; virtual bool initProperties(); virtual bool updateProperties(); virtual bool ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n); virtual bool ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n); protected: /** * @brief updateGPS Retrieve Location & Time from GPS. Update LocationNP & TimeTP properties (value and state) without sending them to the client (i.e. IDSetXXX). * @return Return overall state. The state should be IPS_OK if data is valid. IPS_BUSY if GPS fix is in progress. IPS_ALERT is there is an error. The clients will only accept values with IPS_OK state. */ virtual IPState updateGPS(); /** * @brief TimerHit Keep calling updateGPS() until it is successfull, if it fails upon first connection. */ virtual void TimerHit(); /** * @brief saveConfigItems Save refresh period * @param fp pointer to config file * @return True if all is OK */ virtual bool saveConfigItems(FILE *fp); // A number vector that stores lattitude and longitude INumberVectorProperty LocationNP; INumber LocationN[3]; // UTC and UTC Offset IText TimeT[2] {}; ITextVectorProperty TimeTP; // Refresh data ISwitch RefreshS[1]; ISwitchVectorProperty RefreshSP; // Refresh Period INumber PeriodN[1]; INumberVectorProperty PeriodNP; int timerID = -1; }; } libindi/libs/indibase/indifilterinterface.cpp0000664000175000017500000001632113263645557020722 0ustar jasemjasem/* Filter Interface Copyright (C) 2011 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 */ #include "indifilterinterface.h" #include #include "indilogger.h" namespace INDI { FilterInterface::FilterInterface(DefaultDevice *defaultDevice) : m_defaultDevice(defaultDevice) { FilterNameTP = new ITextVectorProperty; FilterNameT = nullptr; } FilterInterface::~FilterInterface() { delete FilterNameTP; } void FilterInterface::initProperties(const char *groupName) { IUFillNumber(&FilterSlotN[0], "FILTER_SLOT_VALUE", "Filter", "%3.0f", 1.0, 12.0, 1.0, 1.0); IUFillNumberVector(&FilterSlotNP, FilterSlotN, 1, m_defaultDevice->getDeviceName(), "FILTER_SLOT", "Filter Slot", groupName, IP_RW, 60, IPS_IDLE); } bool FilterInterface::updateProperties() { if (m_defaultDevice->isConnected()) { // Define the Filter Slot and name properties m_defaultDevice->defineNumber(&FilterSlotNP); if (FilterNameT == nullptr) { if (GetFilterNames() == true) m_defaultDevice->defineText(FilterNameTP); } else m_defaultDevice->defineText(FilterNameTP); } else { m_defaultDevice->deleteProperty(FilterSlotNP.name); m_defaultDevice->deleteProperty(FilterNameTP->name); } return true; } bool FilterInterface::processNumber(const char *dev, const char *name, double values[], char *names[], int n) { INDI_UNUSED(n); if (dev && !strcmp(dev, m_defaultDevice->getDeviceName()) && !strcmp(name, FilterSlotNP.name)) { TargetFilter = values[0]; INumber *np = IUFindNumber(&FilterSlotNP, names[0]); if (!np) { FilterSlotNP.s = IPS_ALERT; DEBUGFDEVICE(m_defaultDevice->getDeviceName(), Logger::DBG_ERROR, "Unknown error. %s is not a member of %s property.", names[0], FilterSlotNP.name); IDSetNumber(&FilterSlotNP, nullptr); return false; } if (TargetFilter < FilterSlotN[0].min || TargetFilter > FilterSlotN[0].max) { FilterSlotNP.s = IPS_ALERT; DEBUGFDEVICE(m_defaultDevice->getDeviceName(), Logger::DBG_ERROR, "Error: valid range of filter is from %g to %g", FilterSlotN[0].min, FilterSlotN[0].max); IDSetNumber(&FilterSlotNP, nullptr); return false; } FilterSlotNP.s = IPS_BUSY; DEBUGFDEVICE(m_defaultDevice->getDeviceName(), Logger::DBG_SESSION, "Setting current filter to slot %d", TargetFilter); if (SelectFilter(TargetFilter) == false) { FilterSlotNP.s = IPS_ALERT; } IDSetNumber(&FilterSlotNP, nullptr); return true; } return false; } bool FilterInterface::processText(const char *dev, const char *name, char *texts[], char *names[], int n) { if (dev && !strcmp(dev, m_defaultDevice->getDeviceName()) && !strcmp(name, "FILTER_NAME")) { // If this call due to config loading, let's delete existing dummy property and define the full one if (loadingFromConfig) { loadingFromConfig = false; m_defaultDevice->deleteProperty("FILTER_NAME"); char filterName[MAXINDINAME]; char filterLabel[MAXINDILABEL]; if (FilterNameT != nullptr) { for (int i=0; i < FilterNameTP->ntp; i++) free(FilterNameT[i].text); delete [] FilterNameT; } FilterNameT = new IText[n]; memset(FilterNameT, 0, sizeof(IText) * n); for (int i = 0; i < n; i++) { snprintf(filterName, MAXINDINAME, "FILTER_SLOT_NAME_%d", i + 1); snprintf(filterLabel, MAXINDILABEL, "Filter#%d", i + 1); IUFillText(&FilterNameT[i], filterName, filterLabel, texts[i]); } IUFillTextVector(FilterNameTP, FilterNameT, n, m_defaultDevice->getDeviceName(), "FILTER_NAME", "Filter", FilterSlotNP.group, IP_RW, 0, IPS_IDLE); m_defaultDevice->defineText(FilterNameTP); return true; } IUUpdateText(FilterNameTP, texts, names, n); FilterNameTP->s = IPS_OK; if (SetFilterNames() == true) { IDSetText(FilterNameTP, nullptr); return true; } else { FilterNameTP->s = IPS_ALERT; DEBUGDEVICE(m_defaultDevice->getDeviceName(), Logger::DBG_ERROR, "Error updating names of filters."); IDSetText(FilterNameTP, nullptr); return false; } } return false; } bool FilterInterface::saveConfigItems(FILE *fp) { IUSaveConfigNumber(fp, &FilterSlotNP); if (FilterNameTP) IUSaveConfigText(fp, FilterNameTP); return true; } void FilterInterface::SelectFilterDone(int f) { // The hardware has finished changing // filters FilterSlotN[0].value = f; FilterSlotNP.s = IPS_OK; // Tell the clients we are done, and // filter is now useable IDSetNumber(&FilterSlotNP, nullptr); } void FilterInterface::generateSampleFilters() { char filterName[MAXINDINAME]; char filterLabel[MAXINDILABEL]; int MaxFilter = FilterSlotN[0].max; const char *filterDesignation[8] = { "Red", "Green", "Blue", "H_Alpha", "SII", "OIII", "LPR", "Luminance" }; if (FilterNameT != nullptr) { for (int i=0; i < FilterNameTP->ntp; i++) free(FilterNameT[i].text); delete [] FilterNameT; } FilterNameT = new IText[MaxFilter]; memset(FilterNameT, 0, sizeof(IText) * MaxFilter); for (int i = 0; i < MaxFilter; i++) { snprintf(filterName, MAXINDINAME, "FILTER_SLOT_NAME_%d", i + 1); snprintf(filterLabel, MAXINDILABEL, "Filter#%d", i + 1); IUFillText(&FilterNameT[i], filterName, filterLabel, i < 8 ? filterDesignation[i] : filterLabel); } IUFillTextVector(FilterNameTP, FilterNameT, MaxFilter, m_defaultDevice->getDeviceName(), "FILTER_NAME", "Filter", FilterSlotNP.group, IP_RW, 0, IPS_IDLE); } bool FilterInterface::GetFilterNames() { // Load from config if (FilterNameT == nullptr) { generateSampleFilters(); // If property is found, let's define it once loaded to the client and delete // the generate sample filters above if (m_defaultDevice->loadConfig(true, "FILTER_NAME")) loadingFromConfig = true; } return true; } bool FilterInterface::SetFilterNames() { return m_defaultDevice->saveConfig(true, "FILTER_NAME"); } } libindi/libs/indibase/indifocuserinterface.cpp0000664000175000017500000003036713263645557021111 0ustar jasemjasem/* Focuser Interface Copyright (C) 2011 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 */ #include "indifocuserinterface.h" #include "indilogger.h" #include namespace INDI { FocuserInterface::FocuserInterface(DefaultDevice *defaultDevice) : m_defaultDevice(defaultDevice) { } void FocuserInterface::initProperties(const char *groupName) { IUFillNumber(&FocusSpeedN[0], "FOCUS_SPEED_VALUE", "Focus Speed", "%3.0f", 0.0, 255.0, 1.0, 255.0); IUFillNumberVector(&FocusSpeedNP, FocusSpeedN, 1, m_defaultDevice->getDeviceName(), "FOCUS_SPEED", "Speed", groupName, IP_RW, 60, IPS_OK); IUFillNumber(&FocusTimerN[0], "FOCUS_TIMER_VALUE", "Focus Timer (ms)", "%4.0f", 0.0, 5000.0, 50.0, 1000.0); IUFillNumberVector(&FocusTimerNP, FocusTimerN, 1, m_defaultDevice->getDeviceName(), "FOCUS_TIMER", "Timer", groupName, IP_RW, 60, IPS_OK); lastTimerValue = 1000.0; IUFillSwitch(&FocusMotionS[0], "FOCUS_INWARD", "Focus In", ISS_ON); IUFillSwitch(&FocusMotionS[1], "FOCUS_OUTWARD", "Focus Out", ISS_OFF); IUFillSwitchVector(&FocusMotionSP, FocusMotionS, 2 ,m_defaultDevice->getDeviceName(), "FOCUS_MOTION", "Direction", groupName, IP_RW, ISR_1OFMANY, 60, IPS_OK); // Driver can define those to clients if there is support IUFillNumber(&FocusAbsPosN[0], "FOCUS_ABSOLUTE_POSITION", "Ticks", "%4.0f", 0.0, 100000.0, 1000.0, 0); IUFillNumberVector(&FocusAbsPosNP, FocusAbsPosN, 1, m_defaultDevice->getDeviceName(), "ABS_FOCUS_POSITION", "Absolute Position", groupName, IP_RW, 60, IPS_OK); IUFillNumber(&FocusRelPosN[0], "FOCUS_RELATIVE_POSITION", "Ticks", "%4.0f", 0.0, 100000.0, 1000.0, 0); IUFillNumberVector(&FocusRelPosNP, FocusRelPosN, 1, m_defaultDevice->getDeviceName(), "REL_FOCUS_POSITION", "Relative Position", groupName, IP_RW, 60, IPS_OK); IUFillSwitch(&AbortS[0], "ABORT", "Abort", ISS_OFF); IUFillSwitchVector(&AbortSP, AbortS, 1, m_defaultDevice->getDeviceName(), "FOCUS_ABORT_MOTION", "Abort Motion", groupName, IP_RW, ISR_ATMOST1, 60, IPS_IDLE); } bool FocuserInterface::updateProperties() { if (m_defaultDevice->isConnected()) { // Now we add our focusser specific stuff m_defaultDevice->defineSwitch(&FocusMotionSP); if (HasVariableSpeed()) { m_defaultDevice->defineNumber(&FocusSpeedNP); m_defaultDevice->defineNumber(&FocusTimerNP); } if (CanRelMove()) m_defaultDevice->defineNumber(&FocusRelPosNP); if (CanAbsMove()) m_defaultDevice->defineNumber(&FocusAbsPosNP); if (CanAbort()) m_defaultDevice->defineSwitch(&AbortSP); } else { m_defaultDevice->deleteProperty(FocusMotionSP.name); if (HasVariableSpeed()) { m_defaultDevice->deleteProperty(FocusSpeedNP.name); m_defaultDevice->deleteProperty(FocusTimerNP.name); } if (CanRelMove()) m_defaultDevice->deleteProperty(FocusRelPosNP.name); if (CanAbsMove()) m_defaultDevice->deleteProperty(FocusAbsPosNP.name); if (CanAbort()) m_defaultDevice->deleteProperty(AbortSP.name); } return true; } bool FocuserInterface::processNumber(const char *dev, const char *name, double values[], char *names[], int n) { // This is for our device // Now lets see if it's something we process here if (strcmp(name, "FOCUS_TIMER") == 0) { FocusDirection dir; int speed; int t; // first we get all the numbers just sent to us IUUpdateNumber(&FocusTimerNP, values, names, n); // Now lets find what we need for this move speed = FocusSpeedN[0].value; if (FocusMotionS[0].s == ISS_ON) dir = FOCUS_INWARD; else dir = FOCUS_OUTWARD; t = FocusTimerN[0].value; lastTimerValue = t; FocusTimerNP.s = MoveFocuser(dir, speed, t); IDSetNumber(&FocusTimerNP, nullptr); return true; } if (strcmp(name, "FOCUS_SPEED") == 0) { FocusSpeedNP.s = IPS_OK; int current_speed = FocusSpeedN[0].value; IUUpdateNumber(&FocusSpeedNP, values, names, n); if (SetFocuserSpeed(FocusSpeedN[0].value) == false) { FocusSpeedN[0].value = current_speed; FocusSpeedNP.s = IPS_ALERT; } // Update client display IDSetNumber(&FocusSpeedNP, nullptr); return true; } if (strcmp(name, "ABS_FOCUS_POSITION") == 0) { int newPos = (int)values[0]; if (newPos < FocusAbsPosN[0].min) { FocusAbsPosNP.s = IPS_ALERT; IDSetNumber(&FocusAbsPosNP, nullptr); DEBUGFDEVICE(dev, Logger::DBG_ERROR, "Requested position out of bound. Focus minimum position is %g", FocusAbsPosN[0].min); return false; } else if (newPos > FocusAbsPosN[0].max) { FocusAbsPosNP.s = IPS_ALERT; IDSetNumber(&FocusAbsPosNP, nullptr); DEBUGFDEVICE(dev, Logger::DBG_ERROR, "Requested position out of bound. Focus maximum position is %g", FocusAbsPosN[0].max); return false; } IPState ret; if ((ret = MoveAbsFocuser(newPos)) == IPS_OK) { FocusAbsPosNP.s = IPS_OK; IUUpdateNumber(&FocusAbsPosNP, values, names, n); DEBUGFDEVICE(dev, Logger::DBG_SESSION, "Focuser moved to position %d", newPos); IDSetNumber(&FocusAbsPosNP, nullptr); return true; } else if (ret == IPS_BUSY) { FocusAbsPosNP.s = IPS_BUSY; DEBUGFDEVICE(dev, Logger::DBG_SESSION, "Focuser is moving to position %d", newPos); IDSetNumber(&FocusAbsPosNP, nullptr); return true; } FocusAbsPosNP.s = IPS_ALERT; DEBUGDEVICE(dev, Logger::DBG_ERROR, "Focuser failed to move to new requested position."); IDSetNumber(&FocusAbsPosNP, nullptr); return false; } if (strcmp(name, "REL_FOCUS_POSITION") == 0) { int newPos = (int)values[0]; if (newPos <= 0) { DEBUGDEVICE(dev, Logger::DBG_ERROR, "Relative ticks value must be greater than zero."); FocusRelPosNP.s = IPS_ALERT; IDSetNumber(&FocusRelPosNP, nullptr); return false; } IPState ret; if (CanAbsMove()) { if (FocusMotionS[0].s == ISS_ON) { if (FocusAbsPosN[0].value - newPos < FocusAbsPosN[0].min) { FocusRelPosNP.s = IPS_ALERT; IDSetNumber(&FocusRelPosNP, nullptr); DEBUGFDEVICE(dev, Logger::DBG_ERROR, "Requested position out of bound. Focus minimum position is %g", FocusAbsPosN[0].min); return false; } } else { if (FocusAbsPosN[0].value + newPos > FocusAbsPosN[0].max) { FocusRelPosNP.s = IPS_ALERT; IDSetNumber(&FocusRelPosNP, nullptr); DEBUGFDEVICE(dev, Logger::DBG_ERROR, "Requested position out of bound. Focus maximum position is %g", FocusAbsPosN[0].max); return false; } } } if ((ret = MoveRelFocuser((FocusMotionS[0].s == ISS_ON ? FOCUS_INWARD : FOCUS_OUTWARD), newPos)) == IPS_OK) { FocusRelPosNP.s = FocusAbsPosNP.s = IPS_OK; IUUpdateNumber(&FocusRelPosNP, values, names, n); IDSetNumber(&FocusRelPosNP, "Focuser moved %d steps %s", newPos, FocusMotionS[0].s == ISS_ON ? "inward" : "outward"); IDSetNumber(&FocusAbsPosNP, nullptr); return true; } else if (ret == IPS_BUSY) { IUUpdateNumber(&FocusRelPosNP, values, names, n); FocusRelPosNP.s = FocusAbsPosNP.s = IPS_BUSY; IDSetNumber(&FocusAbsPosNP, "Focuser is moving %d steps %s...", newPos, FocusMotionS[0].s == ISS_ON ? "inward" : "outward"); IDSetNumber(&FocusAbsPosNP, nullptr); return true; } FocusRelPosNP.s = IPS_ALERT; DEBUGDEVICE(dev, Logger::DBG_ERROR, "Focuser failed to move to new requested position."); IDSetNumber(&FocusRelPosNP, nullptr); return false; } return false; } bool FocuserInterface::processSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) { INDI_UNUSED(dev); // This one is for us if (strcmp(name, "FOCUS_MOTION") == 0) { // Record last direction and state. FocusDirection prevDirection = FocusMotionS[FOCUS_INWARD].s == ISS_ON ? FOCUS_INWARD : FOCUS_OUTWARD; IPState prevState = FocusMotionSP.s; IUUpdateSwitch(&FocusMotionSP, states, names, n); FocusDirection targetDirection = FocusMotionS[FOCUS_INWARD].s == ISS_ON ? FOCUS_INWARD : FOCUS_OUTWARD; if (CanRelMove() || CanAbsMove() || HasVariableSpeed()) { FocusMotionSP.s = IPS_OK; } // If we are dealing with a simple dumb DC focuser, we move in a specific direction in an open-loop fashion until stopped. else { // If we are reversing direction let's issue abort first. if (prevDirection != targetDirection && prevState == IPS_BUSY) AbortFocuser(); FocusMotionSP.s = MoveFocuser(targetDirection, 0, 0); } IDSetSwitch(&FocusMotionSP, nullptr); return true; } if (strcmp(name, "FOCUS_ABORT_MOTION") == 0) { IUResetSwitch(&AbortSP); if (AbortFocuser()) { AbortSP.s = IPS_OK; if (CanAbsMove() && FocusAbsPosNP.s != IPS_IDLE) { FocusAbsPosNP.s = IPS_IDLE; IDSetNumber(&FocusAbsPosNP, nullptr); } if (CanRelMove() && FocusRelPosNP.s != IPS_IDLE) { FocusRelPosNP.s = IPS_IDLE; IDSetNumber(&FocusRelPosNP, nullptr); } } else AbortSP.s = IPS_ALERT; IDSetSwitch(&AbortSP, nullptr); return true; } return false; } IPState FocuserInterface::MoveFocuser(FocusDirection dir, int speed, uint16_t duration) { INDI_UNUSED(dir); INDI_UNUSED(speed); INDI_UNUSED(duration); // Must be implemented by child class return IPS_ALERT; } IPState FocuserInterface::MoveRelFocuser(FocusDirection dir, uint32_t ticks) { INDI_UNUSED(dir); INDI_UNUSED(ticks); // Must be implemented by child class return IPS_ALERT; } IPState FocuserInterface::MoveAbsFocuser(uint32_t ticks) { INDI_UNUSED(ticks); // Must be implemented by child class return IPS_ALERT; } bool FocuserInterface::AbortFocuser() { // This should be a virtual function, because the low level hardware class // must override this DEBUGDEVICE(m_defaultDevice->getDeviceName(), Logger::DBG_ERROR, "Focuser does not support abort motion."); return false; } bool FocuserInterface::SetFocuserSpeed(int speed) { INDI_UNUSED(speed); // This should be a virtual function, because the low level hardware class // must override this DEBUGDEVICE(m_defaultDevice->getDeviceName(), Logger::DBG_ERROR, "Focuser does not support variable speed."); return false; } } libindi/libs/indibase/indilightboxinterface.h0000664000175000017500000001051113263645557020715 0ustar jasemjasem/* Light Box / Switch Interface Copyright (C) 2015 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 */ #pragma once #include "indibase.h" #include /** * \class LightBoxInterface \brief Provides interface to implement controllable light box/switch device. Filter durations preset can be defined if the active filter name is set. Once the filter names are retrieved, the duration in seconds can be set for each filter. When the filter wheel changes to a new filter, the duration is set accordingly. The child class is expected to call the following functions from the INDI frameworks standard functions: \e IMPORTANT: initLightBoxProperties() must be called before any other function to initilize the Light device properties. \e IMPORTANT: isGetLightBoxProperties() must be called in your driver ISGetProperties function \e IMPORTANT: processLightBoxSwitch() must be called in your driver ISNewSwitch function. \e IMPORTANT: processLightBoxNumber() must be called in your driver ISNewNumber function. \e IMPORTANT: processLightBoxText() must be called in your driver ISNewText function. \author Jasem Mutlaq */ namespace INDI { class LightBoxInterface { public: enum { FLAT_LIGHT_ON, FLAT_LIGHT_OFF }; protected: LightBoxInterface(DefaultDevice *device, bool isDimmable); virtual ~LightBoxInterface(); /** \brief Initilize light box properties. It is recommended to call this function within initProperties() of your primary device \param deviceName Name of the primary device \param groupName Group or tab name to be used to define light box properties. */ void initLightBoxProperties(const char *deviceName, const char *groupNam); /** * @brief isGetLightBoxProperties Get light box properties * @param deviceName parent device name */ void isGetLightBoxProperties(const char *deviceName); /** \brief Process light box switch properties */ bool processLightBoxSwitch(const char *dev, const char *name, ISState *states, char *names[], int n); /** \brief Process light box number properties */ bool processLightBoxNumber(const char *dev, const char *name, double values[], char *names[], int n); /** \brief Process light box text properties */ bool processLightBoxText(const char *dev, const char *name, char *texts[], char *names[], int n); bool updateLightBoxProperties(); bool saveLightBoxConfigItems(FILE *fp); bool snoopLightBox(XMLEle *root); /** * @brief setBrightness Set light level. Must be impelemented in the child class, if supported. * @param value level of light box * @return True if successful, false otherwise. */ virtual bool SetLightBoxBrightness(uint16_t value); /** * @brief EnableLightBox Turn on/off on a light box. Must be impelemented in the child class. * @param enable If true, turn on the light, otherwise turn off the light. * @return True if successful, false otherwise. */ virtual bool EnableLightBox(bool enable); // Turn on/off light ISwitchVectorProperty LightSP; ISwitch LightS[2]; // Light Intensity INumberVectorProperty LightIntensityNP; INumber LightIntensityN[1]; // Active devices to snoop ITextVectorProperty ActiveDeviceTP; IText ActiveDeviceT[1] {}; INumberVectorProperty FilterIntensityNP; INumber *FilterIntensityN; private: void addFilterDuration(const char *filterName, uint16_t filterDuration); DefaultDevice *device; uint8_t currentFilterSlot; bool isDimmable; }; } libindi/libs/indibase/indifocuserinterface.h0000664000175000017500000001600013263645557020542 0ustar jasemjasem/* Filter Interface Copyright (C) 2011 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 */ #pragma once #include "indibase.h" #include // Alias using FI = INDI::FocuserInterface; namespace INDI { /** * \class FocuserInterface \brief Provides interface to implement focuser functionality. A focuser can be an independent device, or an embedded focuser within another device (e.g. Camera or mount). When developing a driver for a fully indepdent focuser device, use INDI::Focuser directly. To add focus functionality to an existing mount or camera driver, subclass INDI::FocuserInterface. In your driver, then call the necessary focuser interface functions.
FunctionWhere to call it from your driver
FI::SetCapabilityConstructor
FI::initPropertiesinitProperties()
FI::updatePropertiesupdateProperties()
FI::processNumberISNewNumber(...) Check if the property name contains FOCUS_* and then call FI::processNumber(..) for such properties
FI::processSwitchISNewSwitch(...)
Implement and overwrite the rest of the virtual functions as needed. INDI GPhoto driver is a good example to check for an actual implementation of a focuser interface within a CCD driver. \author Jasem Mutlaq */ class FocuserInterface { public: enum FocusDirection { FOCUS_INWARD, FOCUS_OUTWARD }; enum { FOCUSER_CAN_ABS_MOVE = 1 << 0, /*!< Can the focuser move by absolute position? */ FOCUSER_CAN_REL_MOVE = 1 << 1, /*!< Can the focuser move by relative position? */ FOCUSER_CAN_ABORT = 1 << 2, /*!< Is it possible to abort focuser motion? */ FOCUSER_HAS_VARIABLE_SPEED = 1 << 3 /*!< Can the focuser move in different configurable speeds? */ } FocuserCapability; /** * @brief GetFocuserCapability returns the capability of the focuser */ uint32_t GetCapability() const { return capability; } /** * @brief FI::SetCapability sets the focuser capabilities. All capabilities must be initialized. * @param cap pointer to focuser capability struct. */ void SetCapability(uint32_t cap) { capability = cap; } /** * @return True if the focuser has absolute position encoders. */ bool CanAbsMove() { return capability & FOCUSER_CAN_ABS_MOVE; } /** * @return True if the focuser has relative position encoders. */ bool CanRelMove() { return capability & FOCUSER_CAN_REL_MOVE; } /** * @return True if the focuser motion can be aborted. */ bool CanAbort() { return capability & FOCUSER_CAN_ABORT; } /** * @return True if the focuser has multiple speeds. */ bool HasVariableSpeed() { return capability & FOCUSER_HAS_VARIABLE_SPEED; } protected: explicit FocuserInterface(DefaultDevice *defaultDevice); virtual ~FocuserInterface() = default; /** * \brief Initilize focuser properties. It is recommended to call this function within * initProperties() of your primary device * \param groupName Group or tab name to be used to define focuser properties. */ void initProperties(const char *groupName); /** * @brief updateProperties Define or Delete Rotator properties based on the connection status of the base device * @return True if successful, false otherwise. */ bool updateProperties(); /** \brief Process focus number properties */ bool processNumber(const char *dev, const char *name, double values[], char *names[], int n); /** \brief Process focus switch properties */ bool processSwitch(const char *dev, const char *name, ISState *states, char *names[], int n); /** * @brief SetFocuserSpeed Set Focuser speed * @param speed focuser speed * @return true if successful, false otherwise */ virtual bool SetFocuserSpeed(int speed); /** * \brief MoveFocuser the focuser in a particular direction with a specific speed for a * finite duration. * \param dir Direction of focuser, either FOCUS_INWARD or FOCUS_OUTWARD. * \param speed Speed of focuser if supported by the focuser. * \param duration The timeout in milliseconds before the focus motion halts. Pass 0 to move indefinitely. * \return Return IPS_OK if motion is completed and focuser reached requested position. * Return IPS_BUSY if focuser started motion to requested position and is in progress. * Return IPS_ALERT if there is an error. */ virtual IPState MoveFocuser(FocusDirection dir, int speed, uint16_t duration); /** * \brief MoveFocuser the focuser to an absolute position. * \param ticks The new position of the focuser. * \return Return IPS_OK if motion is completed and focuser reached requested position. Return * IPS_BUSY if focuser started motion to requested position and is in progress. * Return IPS_ALERT if there is an error. */ virtual IPState MoveAbsFocuser(uint32_t targetTicks); /** * \brief MoveFocuser the focuser to an relative position. * \param dir Direction of focuser, either FOCUS_INWARD or FOCUS_OUTWARD. * \param ticks The relative ticks to move. * \return Return IPS_OK if motion is completed and focuser reached requested position. Return * IPS_BUSY if focuser started motion to requested position and is in progress. * Return IPS_ALERT if there is an error. */ virtual IPState MoveRelFocuser(FocusDirection dir, uint32_t ticks); /** * @brief AbortFocuser all focus motion * @return True if abort is successful, false otherwise. */ virtual bool AbortFocuser(); INumberVectorProperty FocusSpeedNP; INumber FocusSpeedN[1]; ISwitchVectorProperty FocusMotionSP; // A Switch in the client interface to park the scope ISwitch FocusMotionS[2]; INumberVectorProperty FocusTimerNP; INumber FocusTimerN[1]; INumberVectorProperty FocusAbsPosNP; INumber FocusAbsPosN[1]; INumberVectorProperty FocusRelPosNP; INumber FocusRelPosN[1]; ISwitchVectorProperty AbortSP; ISwitch AbortS[1]; uint32_t capability; double lastTimerValue = { 0 }; DefaultDevice *m_defaultDevice { nullptr }; }; } libindi/libs/indibase/indiguiderinterface.cpp0000664000175000017500000000653213263645557020717 0ustar jasemjasem/* Guider Interface Copyright (C) 2011 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 */ #include "indiguiderinterface.h" #include namespace INDI { GuiderInterface::GuiderInterface() { } GuiderInterface::~GuiderInterface() { } void GuiderInterface::initGuiderProperties(const char *deviceName, const char *groupName) { IUFillNumber(&GuideNSN[DIRECTION_NORTH], "TIMED_GUIDE_N", "North (ms)", "%.f", 0, 60000, 100, 0); IUFillNumber(&GuideNSN[DIRECTION_SOUTH], "TIMED_GUIDE_S", "South (ms)", "%.f", 0, 60000, 100, 0); IUFillNumberVector(&GuideNSNP, GuideNSN, 2, deviceName, "TELESCOPE_TIMED_GUIDE_NS", "Guide N/S", groupName, IP_RW, 60, IPS_IDLE); IUFillNumber(&GuideWEN[DIRECTION_WEST], "TIMED_GUIDE_W", "West (ms)", "%.f", 0, 60000, 100, 0); IUFillNumber(&GuideWEN[DIRECTION_EAST], "TIMED_GUIDE_E", "East (ms)", "%.f", 0, 60000, 100, 0); IUFillNumberVector(&GuideWENP, GuideWEN, 2, deviceName, "TELESCOPE_TIMED_GUIDE_WE", "Guide E/W", groupName, IP_RW, 60, IPS_IDLE); } void GuiderInterface::processGuiderProperties(const char *name, double values[], char *names[], int n) { if (strcmp(name, GuideNSNP.name) == 0) { // We are being asked to send a guide pulse north/south on the st4 port IUUpdateNumber(&GuideNSNP, values, names, n); if (GuideNSN[DIRECTION_NORTH].value != 0) { GuideNSN[DIRECTION_SOUTH].value = 0; GuideNSNP.s = GuideNorth(GuideNSN[DIRECTION_NORTH].value); } else if (GuideNSN[DIRECTION_SOUTH].value != 0) GuideNSNP.s = GuideSouth(GuideNSN[DIRECTION_SOUTH].value); IDSetNumber(&GuideNSNP, nullptr); return; } if (strcmp(name, GuideWENP.name) == 0) { // We are being asked to send a guide pulse north/south on the st4 port IUUpdateNumber(&GuideWENP, values, names, n); if (GuideWEN[DIRECTION_WEST].value != 0) { GuideWEN[DIRECTION_EAST].value = 0; GuideWENP.s = GuideWest(GuideWEN[DIRECTION_WEST].value); } else if (GuideWEN[DIRECTION_EAST].value != 0) GuideWENP.s = GuideEast(GuideWEN[DIRECTION_EAST].value); IDSetNumber(&GuideWENP, nullptr); return; } } void GuiderInterface::GuideComplete(INDI_EQ_AXIS axis) { switch (axis) { case AXIS_DE: GuideNSNP.s = IPS_IDLE; IDSetNumber(&GuideNSNP, nullptr); break; case AXIS_RA: GuideWENP.s = IPS_IDLE; IDSetNumber(&GuideWENP, nullptr); break; } } } libindi/libs/indibase/inditelescope.cpp0000664000175000017500000025620513263645557017546 0ustar jasemjasem/******************************************************************************* Copyright(c) 2011 Gerry Rozema, Jasem Mutlaq. All rights reserved. This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2 as published by the Free Software Foundation. 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 Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. *******************************************************************************/ #include "inditelescope.h" #include "indicom.h" #include "indicontroller.h" #include "connectionplugins/connectionserial.h" #include "connectionplugins/connectiontcp.h" #include #include #include #include #include #include #include #include #include namespace INDI { Telescope::Telescope() : DefaultDevice(), ScopeConfigFileName(GetHomeDirectory() + "/.indi/ScopeConfig.xml"), ParkDataFileName(GetHomeDirectory() + "/.indi/ParkData.xml") { capability = 0; last_we_motion = last_ns_motion = -1; parkDataType = PARK_NONE; ParkdataXmlRoot = nullptr; IsParked = false; IsLocked = true; nSlewRate = 0; SlewRateS = nullptr; controller = new Controller(this); controller->setJoystickCallback(joystickHelper); controller->setButtonCallback(buttonHelper); currentPierSide = PIER_EAST; lastPierSide = PIER_UNKNOWN; currentPECState = PEC_OFF; lastPECState = PEC_UNKNOWN; } Telescope::~Telescope() { if (ParkdataXmlRoot) delXMLEle(ParkdataXmlRoot); delete (controller); } bool Telescope::initProperties() { DefaultDevice::initProperties(); // Active Devices IUFillText(&ActiveDeviceT[0], "ACTIVE_GPS", "GPS", "GPS Simulator"); IUFillText(&ActiveDeviceT[1], "ACTIVE_DOME", "DOME", "Dome Simulator"); IUFillTextVector(&ActiveDeviceTP, ActiveDeviceT, 2, getDeviceName(), "ACTIVE_DEVICES", "Snoop devices", OPTIONS_TAB, IP_RW, 60, IPS_IDLE); // Use locking if dome is closed (and or) park scope if dome is closing IUFillSwitch(&DomeClosedLockT[0], "NO_ACTION", "Ignore dome", ISS_ON); IUFillSwitch(&DomeClosedLockT[1], "LOCK_PARKING", "Dome locks", ISS_OFF); IUFillSwitch(&DomeClosedLockT[2], "FORCE_CLOSE", "Dome parks", ISS_OFF); IUFillSwitch(&DomeClosedLockT[3], "LOCK_AND_FORCE", "Both", ISS_OFF); IUFillSwitchVector(&DomeClosedLockTP, DomeClosedLockT, 4, getDeviceName(), "DOME_POLICY", "Dome parking policy", OPTIONS_TAB, IP_RW, ISR_1OFMANY, 60, IPS_IDLE); IUFillNumber(&EqN[AXIS_RA], "RA", "RA (hh:mm:ss)", "%010.6m", 0, 24, 0, 0); IUFillNumber(&EqN[AXIS_DE], "DEC", "DEC (dd:mm:ss)", "%010.6m", -90, 90, 0, 0); IUFillNumberVector(&EqNP, EqN, 2, getDeviceName(), "EQUATORIAL_EOD_COORD", "Eq. Coordinates", MAIN_CONTROL_TAB, IP_RW, 60, IPS_IDLE); lastEqState = IPS_IDLE; IUFillNumber(&TargetN[AXIS_RA], "RA", "RA (hh:mm:ss)", "%010.6m", 0, 24, 0, 0); IUFillNumber(&TargetN[AXIS_DE], "DEC", "DEC (dd:mm:ss)", "%010.6m", -90, 90, 0, 0); IUFillNumberVector(&TargetNP, TargetN, 2, getDeviceName(), "TARGET_EOD_COORD", "Slew Target", MOTION_TAB, IP_RO, 60, IPS_IDLE); IUFillSwitch(&ParkOptionS[0], "PARK_CURRENT", "Current", ISS_OFF); IUFillSwitch(&ParkOptionS[1], "PARK_DEFAULT", "Default", ISS_OFF); IUFillSwitch(&ParkOptionS[2], "PARK_WRITE_DATA", "Write Data", ISS_OFF); IUFillSwitchVector(&ParkOptionSP, ParkOptionS, 3, getDeviceName(), "TELESCOPE_PARK_OPTION", "Park Options", SITE_TAB, IP_RW, ISR_ATMOST1, 60, IPS_IDLE); IUFillText(&TimeT[0], "UTC", "UTC Time", nullptr); IUFillText(&TimeT[1], "OFFSET", "UTC Offset", nullptr); IUFillTextVector(&TimeTP, TimeT, 2, getDeviceName(), "TIME_UTC", "UTC", SITE_TAB, IP_RW, 60, IPS_IDLE); IUFillNumber(&LocationN[LOCATION_LATITUDE], "LAT", "Lat (dd:mm:ss)", "%010.6m", -90, 90, 0, 0.0); IUFillNumber(&LocationN[LOCATION_LONGITUDE], "LONG", "Lon (dd:mm:ss)", "%010.6m", 0, 360, 0, 0.0); IUFillNumber(&LocationN[LOCATION_ELEVATION], "ELEV", "Elevation (m)", "%g", -200, 10000, 0, 0); IUFillNumberVector(&LocationNP, LocationN, 3, getDeviceName(), "GEOGRAPHIC_COORD", "Scope Location", SITE_TAB, IP_RW, 60, IPS_IDLE); // Pier Side IUFillSwitch(&PierSideS[PIER_WEST], "PIER_WEST", "West (pointing east)", ISS_OFF); IUFillSwitch(&PierSideS[PIER_EAST], "PIER_EAST", "East (pointing west)", ISS_ON); IUFillSwitchVector(&PierSideSP, PierSideS, 2, getDeviceName(), "TELESCOPE_PIER_SIDE", "Pier Side", MAIN_CONTROL_TAB, IP_RO, ISR_1OFMANY, 60, IPS_IDLE); // PEC State IUFillSwitch(&PECStateS[PEC_OFF], "PEC OFF", "PEC OFF", ISS_OFF); IUFillSwitch(&PECStateS[PEC_ON], "PEC ON", "PEC ON", ISS_ON); IUFillSwitchVector(&PECStateSP, PECStateS, 2, getDeviceName(), "PEC", "PEC Playback", MOTION_TAB, IP_RW, ISR_1OFMANY, 0, IPS_IDLE); // Track Mode. Child class must call AddTrackMode to add members IUFillSwitchVector(&TrackModeSP, TrackModeS, 0, getDeviceName(), "TELESCOPE_TRACK_MODE", "Track Mode", MAIN_CONTROL_TAB, IP_RW, ISR_1OFMANY, 0, IPS_IDLE); // Track State IUFillSwitch(&TrackStateS[TRACK_ON], "TRACK_ON", "On", ISS_OFF); IUFillSwitch(&TrackStateS[TRACK_OFF], "TRACK_OFF", "Off", ISS_ON); IUFillSwitchVector(&TrackStateSP, TrackStateS, 2, getDeviceName(), "TELESCOPE_TRACK_STATE", "Tracking", MAIN_CONTROL_TAB, IP_RW, ISR_1OFMANY, 0, IPS_IDLE); // Track Rate IUFillNumber(&TrackRateN[AXIS_RA], "TRACK_RATE_RA", "RA (arcsecs/s)", "%.6f", -16384.0, 16384.0, 0.000001, TRACKRATE_SIDEREAL); IUFillNumber(&TrackRateN[AXIS_DE], "TRACK_RATE_DE", "DE (arcsecs/s)", "%.6f", -16384.0, 16384.0, 0.000001, 0.0); IUFillNumberVector(&TrackRateNP, TrackRateN, 2, getDeviceName(), "TELESCOPE_TRACK_RATE", "Track Rates", MAIN_CONTROL_TAB, IP_RW, 60, IPS_IDLE); // On Coord Set actions IUFillSwitch(&CoordS[0], "TRACK", "Track", ISS_ON); IUFillSwitch(&CoordS[1], "SLEW", "Slew", ISS_OFF); IUFillSwitch(&CoordS[2], "SYNC", "Sync", ISS_OFF); // If both GOTO and SYNC are supported if (CanGOTO() && CanSync()) IUFillSwitchVector(&CoordSP, CoordS, 3, getDeviceName(), "ON_COORD_SET", "On Set", MAIN_CONTROL_TAB, IP_RW, ISR_1OFMANY, 60, IPS_IDLE); // If ONLY GOTO is supported else if (CanGOTO()) IUFillSwitchVector(&CoordSP, CoordS, 2, getDeviceName(), "ON_COORD_SET", "On Set", MAIN_CONTROL_TAB, IP_RW, ISR_1OFMANY, 60, IPS_IDLE); // If ONLY SYNC is supported else if (CanSync()) { IUFillSwitch(&CoordS[0], "SYNC", "Sync", ISS_ON); IUFillSwitchVector(&CoordSP, CoordS, 1, getDeviceName(), "ON_COORD_SET", "On Set", MAIN_CONTROL_TAB, IP_RW, ISR_1OFMANY, 60, IPS_IDLE); } if (nSlewRate >= 4) IUFillSwitchVector(&SlewRateSP, SlewRateS, nSlewRate, getDeviceName(), "TELESCOPE_SLEW_RATE", "Slew Rate", MOTION_TAB, IP_RW, ISR_1OFMANY, 0, IPS_IDLE); IUFillSwitch(&ParkS[0], "PARK", "Park", ISS_OFF); IUFillSwitch(&ParkS[1], "UNPARK", "UnPark", ISS_OFF); IUFillSwitchVector(&ParkSP, ParkS, 2, getDeviceName(), "TELESCOPE_PARK", "Parking", MAIN_CONTROL_TAB, IP_RW, ISR_1OFMANY, 60, IPS_IDLE); IUFillSwitch(&AbortS[0], "ABORT", "Abort", ISS_OFF); IUFillSwitchVector(&AbortSP, AbortS, 1, getDeviceName(), "TELESCOPE_ABORT_MOTION", "Abort Motion", MAIN_CONTROL_TAB, IP_RW, ISR_ATMOST1, 60, IPS_IDLE); IUFillSwitch(&MovementNSS[DIRECTION_NORTH], "MOTION_NORTH", "North", ISS_OFF); IUFillSwitch(&MovementNSS[DIRECTION_SOUTH], "MOTION_SOUTH", "South", ISS_OFF); IUFillSwitchVector(&MovementNSSP, MovementNSS, 2, getDeviceName(), "TELESCOPE_MOTION_NS", "Motion N/S", MOTION_TAB, IP_RW, ISR_ATMOST1, 60, IPS_IDLE); IUFillSwitch(&MovementWES[DIRECTION_WEST], "MOTION_WEST", "West", ISS_OFF); IUFillSwitch(&MovementWES[DIRECTION_EAST], "MOTION_EAST", "East", ISS_OFF); IUFillSwitchVector(&MovementWESP, MovementWES, 2, getDeviceName(), "TELESCOPE_MOTION_WE", "Motion W/E", MOTION_TAB, IP_RW, ISR_ATMOST1, 60, IPS_IDLE); IUFillNumber(&ScopeParametersN[0], "TELESCOPE_APERTURE", "Aperture (mm)", "%g", 10, 5000, 0, 0.0); IUFillNumber(&ScopeParametersN[1], "TELESCOPE_FOCAL_LENGTH", "Focal Length (mm)", "%g", 10, 10000, 0, 0.0); IUFillNumber(&ScopeParametersN[2], "GUIDER_APERTURE", "Guider Aperture (mm)", "%g", 10, 5000, 0, 0.0); IUFillNumber(&ScopeParametersN[3], "GUIDER_FOCAL_LENGTH", "Guider Focal Length (mm)", "%g", 10, 10000, 0, 0.0); IUFillNumberVector(&ScopeParametersNP, ScopeParametersN, 4, getDeviceName(), "TELESCOPE_INFO", "Scope Properties", OPTIONS_TAB, IP_RW, 60, IPS_OK); // Scope config name IUFillText(&ScopeConfigNameT[0], "SCOPE_CONFIG_NAME", "Config Name", ""); IUFillTextVector(&ScopeConfigNameTP, ScopeConfigNameT, 1, getDeviceName(), "SCOPE_CONFIG_NAME", "Scope Name", OPTIONS_TAB, IP_RW, 60, IPS_OK); // Switch for aperture/focal length configs IUFillSwitch(&ScopeConfigs[SCOPE_CONFIG1], "SCOPE_CONFIG1", "Config #1", ISS_ON); IUFillSwitch(&ScopeConfigs[SCOPE_CONFIG2], "SCOPE_CONFIG2", "Config #2", ISS_OFF); IUFillSwitch(&ScopeConfigs[SCOPE_CONFIG3], "SCOPE_CONFIG3", "Config #3", ISS_OFF); IUFillSwitch(&ScopeConfigs[SCOPE_CONFIG4], "SCOPE_CONFIG4", "Config #4", ISS_OFF); IUFillSwitch(&ScopeConfigs[SCOPE_CONFIG5], "SCOPE_CONFIG5", "Config #5", ISS_OFF); IUFillSwitch(&ScopeConfigs[SCOPE_CONFIG6], "SCOPE_CONFIG6", "Config #6", ISS_OFF); IUFillSwitchVector(&ScopeConfigsSP, ScopeConfigs, 6, getDeviceName(), "APPLY_SCOPE_CONFIG", "Scope Configs", OPTIONS_TAB, IP_RW, ISR_1OFMANY, 60, IPS_OK); // Lock Axis IUFillSwitch(&LockAxisS[0], "LOCK_AXIS_1", "West/East", ISS_OFF); IUFillSwitch(&LockAxisS[1], "LOCK_AXIS_2", "North/South", ISS_OFF); IUFillSwitchVector(&LockAxisSP, LockAxisS, 2, getDeviceName(), "JOYSTICK_LOCK_AXIS", "Lock Axis", "Joystick", IP_RW, ISR_ATMOST1, 60, IPS_IDLE); controller->initProperties(); TrackState = SCOPE_IDLE; setDriverInterface(TELESCOPE_INTERFACE); if (telescopeConnection & CONNECTION_SERIAL) { serialConnection = new Connection::Serial(this); serialConnection->registerHandshake([&]() { return callHandshake(); }); registerConnection(serialConnection); } if (telescopeConnection & CONNECTION_TCP) { tcpConnection = new Connection::TCP(this); tcpConnection->registerHandshake([&]() { return callHandshake(); }); registerConnection(tcpConnection); } IDSnoopDevice(ActiveDeviceT[0].text, "GEOGRAPHIC_COORD"); IDSnoopDevice(ActiveDeviceT[0].text, "TIME_UTC"); IDSnoopDevice(ActiveDeviceT[1].text, "DOME_PARK"); IDSnoopDevice(ActiveDeviceT[1].text, "DOME_SHUTTER"); addPollPeriodControl(); return true; } void Telescope::ISGetProperties(const char *dev) { // First we let our parent populate DefaultDevice::ISGetProperties(dev); if (CanGOTO()) { defineText(&ActiveDeviceTP); loadConfig(true, "ACTIVE_DEVICES"); defineSwitch(&DomeClosedLockTP); loadConfig(true, "DOME_POLICY"); } defineNumber(&ScopeParametersNP); defineText(&ScopeConfigNameTP); if (HasDefaultScopeConfig()) { LoadScopeConfig(); } else { loadConfig(true, "TELESCOPE_INFO"); loadConfig(true, "SCOPE_CONFIG_NAME"); } /* if (isConnected()) { // Now we add our telescope specific stuff if (CanGOTO()) defineSwitch(&CoordSP); defineNumber(&EqNP); if (CanAbort()) defineSwitch(&AbortSP); if (HasTrackMode() && TrackModeS != nullptr) defineSwitch(&TrackModeSP); if (CanControlTrack()) defineSwitch(&TrackStateSP); if (HasTrackRate()) defineNumber(&TrackRateNP); if (HasTime()) defineText(&TimeTP); if (HasLocation()) defineNumber(&LocationNP); if (CanPark()) { defineSwitch(&ParkSP); if (parkDataType != PARK_NONE) { defineNumber(&ParkPositionNP); defineSwitch(&ParkOptionSP); } } if (CanGOTO()) { defineSwitch(&MovementNSSP); defineSwitch(&MovementWESP); if (nSlewRate >= 4) defineSwitch(&SlewRateSP); defineNumber(&TargetNP); } if (HasPierSide()) defineSwitch(&PierSideSP); if (HasPECState()) defineSwitch(&PECStateSP); defineSwitch(&ScopeConfigsSP); } */ if (CanGOTO()) controller->ISGetProperties(dev); } bool Telescope::updateProperties() { if (isConnected()) { controller->mapController("MOTIONDIR", "N/S/W/E Control", Controller::CONTROLLER_JOYSTICK, "JOYSTICK_1"); if (nSlewRate >= 4) { controller->mapController("SLEWPRESET", "Slew Rate", Controller::CONTROLLER_JOYSTICK, "JOYSTICK_2"); controller->mapController("SLEWPRESETUP", "Slew Rate Up", Controller::CONTROLLER_BUTTON, "BUTTON_5"); controller->mapController("SLEWPRESETDOWN", "Slew Rate Down", Controller::CONTROLLER_BUTTON, "BUTTON_6"); } if (CanAbort()) controller->mapController("ABORTBUTTON", "Abort", Controller::CONTROLLER_BUTTON, "BUTTON_1"); if (CanPark()) { controller->mapController("PARKBUTTON", "Park", Controller::CONTROLLER_BUTTON, "BUTTON_2"); controller->mapController("UNPARKBUTTON", "UnPark", Controller::CONTROLLER_BUTTON, "BUTTON_3"); } // Now we add our telescope specific stuff if (CanGOTO() || CanSync()) defineSwitch(&CoordSP); defineNumber(&EqNP); if (CanAbort()) defineSwitch(&AbortSP); if (HasTrackMode() && TrackModeS != nullptr) defineSwitch(&TrackModeSP); if (CanControlTrack()) defineSwitch(&TrackStateSP); if (HasTrackRate()) defineNumber(&TrackRateNP); if (CanGOTO()) { defineSwitch(&MovementNSSP); defineSwitch(&MovementWESP); if (nSlewRate >= 4) defineSwitch(&SlewRateSP); defineNumber(&TargetNP); } if (HasTime()) defineText(&TimeTP); if (HasLocation()) defineNumber(&LocationNP); if (CanPark()) { defineSwitch(&ParkSP); if (parkDataType != PARK_NONE) { defineNumber(&ParkPositionNP); defineSwitch(&ParkOptionSP); } } if (HasPierSide()) defineSwitch(&PierSideSP); if (HasPECState()) defineSwitch(&PECStateSP); defineText(&ScopeConfigNameTP); defineSwitch(&ScopeConfigsSP); } else { if (CanGOTO() || CanSync()) deleteProperty(CoordSP.name); deleteProperty(EqNP.name); if (CanAbort()) deleteProperty(AbortSP.name); if (HasTrackMode() && TrackModeS != nullptr) deleteProperty(TrackModeSP.name); if (HasTrackRate()) deleteProperty(TrackRateNP.name); if (CanControlTrack()) deleteProperty(TrackStateSP.name); if (CanGOTO()) { deleteProperty(MovementNSSP.name); deleteProperty(MovementWESP.name); if (nSlewRate >= 4) deleteProperty(SlewRateSP.name); deleteProperty(TargetNP.name); } if (HasTime()) deleteProperty(TimeTP.name); if (HasLocation()) deleteProperty(LocationNP.name); if (CanPark()) { deleteProperty(ParkSP.name); if (parkDataType != PARK_NONE) { deleteProperty(ParkPositionNP.name); deleteProperty(ParkOptionSP.name); } } if (HasPierSide()) deleteProperty(PierSideSP.name); if (HasPECState()) deleteProperty(PECStateSP.name); deleteProperty(ScopeConfigNameTP.name); deleteProperty(ScopeConfigsSP.name); } if (CanGOTO()) { controller->updateProperties(); ISwitchVectorProperty *useJoystick = getSwitch("USEJOYSTICK"); if (useJoystick) { if (isConnected()) { if (useJoystick->sp[0].s == ISS_ON) { defineSwitch(&LockAxisSP); loadConfig(true, "LOCK_AXIS"); } else deleteProperty(LockAxisSP.name); } else deleteProperty(LockAxisSP.name); } } return true; } bool Telescope::ISSnoopDevice(XMLEle *root) { controller->ISSnoopDevice(root); XMLEle *ep = nullptr; const char *propName = findXMLAttValu(root, "name"); if (isConnected()) { if (HasLocation() && !strcmp(propName, "GEOGRAPHIC_COORD")) { // Only accept IPS_OK state if (strcmp(findXMLAttValu(root, "state"), "Ok")) return false; double longitude = -1, latitude = -1, elevation = -1; for (ep = nextXMLEle(root, 1); ep != nullptr; ep = nextXMLEle(root, 0)) { const char *elemName = findXMLAttValu(ep, "name"); if (!strcmp(elemName, "LAT")) latitude = atof(pcdataXMLEle(ep)); else if (!strcmp(elemName, "LONG")) longitude = atof(pcdataXMLEle(ep)); else if (!strcmp(elemName, "ELEV")) elevation = atof(pcdataXMLEle(ep)); } return processLocationInfo(latitude, longitude, elevation); } else if (HasTime() && !strcmp(propName, "TIME_UTC")) { // Only accept IPS_OK state if (strcmp(findXMLAttValu(root, "state"), "Ok")) return false; char utc[MAXINDITSTAMP], offset[MAXINDITSTAMP]; for (ep = nextXMLEle(root, 1); ep != nullptr; ep = nextXMLEle(root, 0)) { const char *elemName = findXMLAttValu(ep, "name"); if (!strcmp(elemName, "UTC")) strncpy(utc, pcdataXMLEle(ep), MAXINDITSTAMP); else if (!strcmp(elemName, "OFFSET")) strncpy(offset, pcdataXMLEle(ep), MAXINDITSTAMP); } return processTimeInfo(utc, offset); } else if (!strcmp(propName, "DOME_PARK") || !strcmp(propName, "DOME_SHUTTER")) { if (strcmp(findXMLAttValu(root, "state"), "Ok")) { // Dome options is dome parks or both and dome is parking. if ((DomeClosedLockT[2].s == ISS_ON || DomeClosedLockT[3].s == ISS_ON) && !IsLocked && !IsParked) { RememberTrackState = TrackState; Park(); DEBUG(Logger::DBG_SESSION, "Dome is closing, parking mount..."); } } // Dome is changing state and Dome options is lock or both. d else if (!strcmp(findXMLAttValu(root, "state"), "Ok")) { bool prevState = IsLocked; for (ep = nextXMLEle(root, 1); ep != nullptr; ep = nextXMLEle(root, 0)) { const char *elemName = findXMLAttValu(ep, "name"); if (!IsLocked && (!strcmp(elemName, "PARK")) && !strcmp(pcdataXMLEle(ep), "On")) IsLocked = true; else if (IsLocked && (!strcmp(elemName, "UNPARK")) && !strcmp(pcdataXMLEle(ep), "On")) IsLocked = false; } if (prevState != IsLocked && (DomeClosedLockT[1].s == ISS_ON || DomeClosedLockT[3].s == ISS_ON)) DEBUGF(Logger::DBG_SESSION, "Dome status changed. Lock is set to: %s", IsLocked ? "locked" : "unlock"); } return true; } } return DefaultDevice::ISSnoopDevice(root); } void Telescope::triggerSnoop(const char *driverName, const char *snoopedProp) { DEBUGF(Logger::DBG_DEBUG, "Active Snoop, driver: %s, property: %s", driverName, snoopedProp); IDSnoopDevice(driverName, snoopedProp); } uint8_t Telescope::getTelescopeConnection() const { return telescopeConnection; } void Telescope::setTelescopeConnection(const uint8_t &value) { uint8_t mask = CONNECTION_SERIAL | CONNECTION_TCP | CONNECTION_NONE; if (value == 0 || (mask & value) == 0) { DEBUGF(Logger::DBG_ERROR, "Invalid connection mode %d", value); return; } telescopeConnection = value; } bool Telescope::saveConfigItems(FILE *fp) { DefaultDevice::saveConfigItems(fp); IUSaveConfigText(fp, &ActiveDeviceTP); IUSaveConfigSwitch(fp, &DomeClosedLockTP); if (HasLocation()) IUSaveConfigNumber(fp, &LocationNP); if (!HasDefaultScopeConfig()) { if (ScopeParametersNP.s == IPS_OK) IUSaveConfigNumber(fp, &ScopeParametersNP); if (ScopeConfigNameTP.s == IPS_OK) IUSaveConfigText(fp, &ScopeConfigNameTP); } if (SlewRateS != nullptr) IUSaveConfigSwitch(fp, &SlewRateSP); if (HasPECState()) IUSaveConfigSwitch(fp, &PECStateSP); if (HasTrackMode()) IUSaveConfigSwitch(fp, &TrackModeSP); if (HasTrackRate()) IUSaveConfigNumber(fp, &TrackRateNP); controller->saveConfigItems(fp); IUSaveConfigSwitch(fp, &LockAxisSP); return true; } void Telescope::NewRaDec(double ra, double dec) { switch (TrackState) { case SCOPE_PARKED: case SCOPE_IDLE: EqNP.s = IPS_IDLE; break; case SCOPE_SLEWING: case SCOPE_PARKING: EqNP.s = IPS_BUSY; break; case SCOPE_TRACKING: EqNP.s = IPS_OK; break; default: break; } if (TrackState != SCOPE_TRACKING && CanControlTrack() && TrackStateS[TRACK_ON].s == ISS_ON) { TrackStateSP.s = IPS_IDLE; TrackStateS[TRACK_ON].s = ISS_OFF; TrackStateS[TRACK_OFF].s = ISS_ON; IDSetSwitch(&TrackStateSP, nullptr); } else if (TrackState == SCOPE_TRACKING && CanControlTrack() && TrackStateS[TRACK_OFF].s == ISS_ON) { TrackStateSP.s = IPS_BUSY; TrackStateS[TRACK_ON].s = ISS_ON; TrackStateS[TRACK_OFF].s = ISS_OFF; IDSetSwitch(&TrackStateSP, nullptr); } if (EqN[AXIS_RA].value != ra || EqN[AXIS_DE].value != dec || EqNP.s != lastEqState) { EqN[AXIS_RA].value = ra; EqN[AXIS_DE].value = dec; lastEqState = EqNP.s; IDSetNumber(&EqNP, nullptr); } } bool Telescope::Sync(double ra, double dec) { INDI_UNUSED(ra); INDI_UNUSED(dec); // if we get here, our mount doesn't support sync DEBUG(Logger::DBG_ERROR, "Telescope does not support Sync."); return false; } bool Telescope::MoveNS(INDI_DIR_NS dir, TelescopeMotionCommand command) { INDI_UNUSED(dir); INDI_UNUSED(command); DEBUG(Logger::DBG_ERROR, "Telescope does not support North/South motion."); IUResetSwitch(&MovementNSSP); MovementNSSP.s = IPS_IDLE; IDSetSwitch(&MovementNSSP, nullptr); return false; } bool Telescope::MoveWE(INDI_DIR_WE dir, TelescopeMotionCommand command) { INDI_UNUSED(dir); INDI_UNUSED(command); DEBUG(Logger::DBG_ERROR, "Telescope does not support West/East motion."); IUResetSwitch(&MovementWESP); MovementWESP.s = IPS_IDLE; IDSetSwitch(&MovementWESP, nullptr); return false; } /************************************************************************************** ** Process Text properties ***************************************************************************************/ bool Telescope::ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n) { // first check if it's for our device if (dev != nullptr && strcmp(dev, getDeviceName()) == 0) { if (!strcmp(name, TimeTP.name)) { int utcindex = IUFindIndex("UTC", names, n); int offsetindex = IUFindIndex("OFFSET", names, n); return processTimeInfo(texts[utcindex], texts[offsetindex]); } if (!strcmp(name, ActiveDeviceTP.name)) { ActiveDeviceTP.s = IPS_OK; IUUpdateText(&ActiveDeviceTP, texts, names, n); // Update client display IDSetText(&ActiveDeviceTP, nullptr); IDSnoopDevice(ActiveDeviceT[0].text, "GEOGRAPHIC_COORD"); IDSnoopDevice(ActiveDeviceT[0].text, "TIME_UTC"); IDSnoopDevice(ActiveDeviceT[1].text, "DOME_PARK"); IDSnoopDevice(ActiveDeviceT[1].text, "DOME_SHUTTER"); return true; } if (name && std::string(name) == "SCOPE_CONFIG_NAME") { ScopeConfigNameTP.s = IPS_OK; IUUpdateText(&ScopeConfigNameTP, texts, names, n); IDSetText(&ScopeConfigNameTP, nullptr); UpdateScopeConfig(); return true; } } controller->ISNewText(dev, name, texts, names, n); return DefaultDevice::ISNewText(dev, name, texts, names, n); } /************************************************************************************** ** ***************************************************************************************/ bool Telescope::ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) { // first check if it's for our device if (dev != nullptr && strcmp(dev, getDeviceName()) == 0) { /////////////////////////////////// // Goto & Sync for Equatorial Coords /////////////////////////////////// if (strcmp(name, "EQUATORIAL_EOD_COORD") == 0) { // this is for us, and it is a goto bool rc = false; double ra = -1; double dec = -100; for (int x = 0; x < n; x++) { INumber *eqp = IUFindNumber(&EqNP, names[x]); if (eqp == &EqN[AXIS_RA]) { ra = values[x]; } else if (eqp == &EqN[AXIS_DE]) { dec = values[x]; } } if ((ra >= 0) && (ra <= 24) && (dec >= -90) && (dec <= 90)) { // Check if it is already parked. if (CanPark()) { if (isParked()) { DEBUG(Logger::DBG_WARNING, "Please unpark the mount before issuing any motion/sync commands."); EqNP.s = lastEqState = IPS_IDLE; IDSetNumber(&EqNP, nullptr); return false; } } // Check if it can sync if (CanSync()) { ISwitch *sw; sw = IUFindSwitch(&CoordSP, "SYNC"); if ((sw != nullptr) && (sw->s == ISS_ON)) { rc = Sync(ra, dec); if (rc) EqNP.s = lastEqState = IPS_OK; else EqNP.s = lastEqState = IPS_ALERT; IDSetNumber(&EqNP, nullptr); return rc; } } // Remember Track State RememberTrackState = TrackState; // Issue GOTO rc = Goto(ra, dec); if (rc) { EqNP.s = lastEqState = IPS_BUSY; // Now fill in target co-ords, so domes can start turning TargetN[AXIS_RA].value = ra; TargetN[AXIS_DE].value = dec; IDSetNumber(&TargetNP, nullptr); } else { EqNP.s = lastEqState = IPS_ALERT; } IDSetNumber(&EqNP, nullptr); } return rc; } /////////////////////////////////// // Geographic Coords /////////////////////////////////// if (strcmp(name, "GEOGRAPHIC_COORD") == 0) { int latindex = IUFindIndex("LAT", names, n); int longindex = IUFindIndex("LONG", names, n); int elevationindex = IUFindIndex("ELEV", names, n); if (latindex == -1 || longindex == -1 || elevationindex == -1) { LocationNP.s = IPS_ALERT; IDSetNumber(&LocationNP, "Location data missing or corrupted."); } double targetLat = values[latindex]; double targetLong = values[longindex]; double targetElev = values[elevationindex]; return processLocationInfo(targetLat, targetLong, targetElev); } /////////////////////////////////// // Telescope Info /////////////////////////////////// if (strcmp(name, "TELESCOPE_INFO") == 0) { ScopeParametersNP.s = IPS_OK; IUUpdateNumber(&ScopeParametersNP, values, names, n); IDSetNumber(&ScopeParametersNP, nullptr); UpdateScopeConfig(); return true; } /////////////////////////////////// // Park Position /////////////////////////////////// if (strcmp(name, ParkPositionNP.name) == 0) { double axis1 = std::numeric_limits::quiet_NaN(), axis2 = std::numeric_limits::quiet_NaN(); for (int x = 0; x < n; x++) { INumber *parkPosAxis = IUFindNumber(&ParkPositionNP, names[x]); if (parkPosAxis == &ParkPositionN[AXIS_RA]) { axis1 = values[x]; } else if (parkPosAxis == &ParkPositionN[AXIS_DE]) { axis2 = values[x]; } } if (std::isnan(axis1) == false && std::isnan(axis2) == false) { bool rc = false; rc = SetParkPosition(axis1, axis2); if (rc) { IUUpdateNumber(&ParkPositionNP, values, names, n); Axis1ParkPosition = ParkPositionN[AXIS_RA].value; Axis2ParkPosition = ParkPositionN[AXIS_DE].value; } ParkPositionNP.s = rc ? IPS_OK : IPS_ALERT; } else ParkPositionNP.s = IPS_ALERT; IDSetNumber(&ParkPositionNP, nullptr); return true; } /////////////////////////////////// // Track Rate /////////////////////////////////// if (strcmp(name, TrackRateNP.name) == 0) { double preAxis1 = TrackRateN[AXIS_RA].value, preAxis2 = TrackRateN[AXIS_DE].value; bool rc = (IUUpdateNumber(&TrackRateNP, values, names, n) == 0); if (!rc) { TrackRateNP.s = IPS_ALERT; IDSetNumber(&TrackRateNP, nullptr); return false; } if (TrackState == SCOPE_TRACKING && !strcmp(IUFindOnSwitch(&TrackModeSP)->name, "TRACK_CUSTOM")) { // Check that we do not abruplty change positive tracking rates to negative ones. // tracking must be stopped first. // Give warning is tracking sign would cause a reverse in direction if ( (preAxis1 * TrackRateN[AXIS_RA].value < 0) || (preAxis2 * TrackRateN[AXIS_DE].value < 0) ) { DEBUG(Logger::DBG_ERROR, "Cannot reverse tracking while tracking is engaged. Disengage tracking then try again."); return false; } // All is fine, ask mount to change tracking rate rc = SetTrackRate(TrackRateN[AXIS_RA].value, TrackRateN[AXIS_DE].value); if (!rc) { TrackRateN[AXIS_RA].value = preAxis1; TrackRateN[AXIS_DE].value = preAxis2; } } // If we are already tracking but tracking mode is NOT custom // We just inform the user that it must be set to custom for these values to take // effect. if (TrackState == SCOPE_TRACKING && strcmp(IUFindOnSwitch(&TrackModeSP)->name, "TRACK_CUSTOM")) { DEBUG(Logger::DBG_SESSION, "Custom tracking rates set. Tracking mode must be set to Custom for these rates to take effect."); } // If mount is NOT tracking, we simply accept whatever valid values for use when mount tracking is engaged. TrackRateNP.s = rc ? IPS_OK : IPS_ALERT; IDSetNumber(&TrackRateNP, nullptr); return true; } } return DefaultDevice::ISNewNumber(dev, name, values, names, n); } /************************************************************************************** ** ***************************************************************************************/ bool Telescope::ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) { if (dev != nullptr && strcmp(dev, getDeviceName()) == 0) { // This one is for us if (!strcmp(name, CoordSP.name)) { // client is telling us what to do with co-ordinate requests CoordSP.s = IPS_OK; IUUpdateSwitch(&CoordSP, states, names, n); // Update client display IDSetSwitch(&CoordSP, nullptr); return true; } /////////////////////////////////// // Slew Rate /////////////////////////////////// if (!strcmp(name, SlewRateSP.name)) { int preIndex = IUFindOnSwitchIndex(&SlewRateSP); IUUpdateSwitch(&SlewRateSP, states, names, n); int nowIndex = IUFindOnSwitchIndex(&SlewRateSP); if (SetSlewRate(nowIndex) == false) { IUResetSwitch(&SlewRateSP); SlewRateS[preIndex].s = ISS_ON; SlewRateSP.s = IPS_ALERT; } else SlewRateSP.s = IPS_OK; IDSetSwitch(&SlewRateSP, nullptr); return true; } /////////////////////////////////// // Parking /////////////////////////////////// if (!strcmp(name, ParkSP.name)) { if (TrackState == SCOPE_PARKING) { IUResetSwitch(&ParkSP); ParkSP.s = IPS_ALERT; Abort(); DEBUG(Logger::DBG_SESSION, "Parking/Unparking aborted."); IDSetSwitch(&ParkSP, nullptr); return true; } int preIndex = IUFindOnSwitchIndex(&ParkSP); IUUpdateSwitch(&ParkSP, states, names, n); bool toPark = (ParkS[0].s == ISS_ON); if (toPark == false && TrackState != SCOPE_PARKED) { IUResetSwitch(&ParkSP); ParkS[1].s = ISS_ON; ParkSP.s = IPS_IDLE; LOG_INFO("Telescope already unparked."); IsParked = false; IDSetSwitch(&ParkSP, nullptr); return true; } if (toPark == false && isLocked()) { IUResetSwitch(&ParkSP); ParkS[0].s = ISS_ON; ParkSP.s = IPS_IDLE; LOG_WARN("Cannot unpark mount when dome is locking. See: Dome parking policy, in options tab."); IsParked = true; IDSetSwitch(&ParkSP, nullptr); return true; } if (toPark && TrackState == SCOPE_PARKED) { IUResetSwitch(&ParkSP); ParkS[0].s = ISS_ON; ParkSP.s = IPS_IDLE; DEBUG(Logger::DBG_SESSION, "Telescope already parked."); IDSetSwitch(&ParkSP, nullptr); return true; } RememberTrackState = TrackState; IUResetSwitch(&ParkSP); bool rc = toPark ? Park() : UnPark(); if (rc) { if (TrackState == SCOPE_PARKING) { ParkS[0].s = toPark ? ISS_ON : ISS_OFF; ParkS[1].s = toPark ? ISS_OFF : ISS_ON; ParkSP.s = IPS_BUSY; } else { ParkS[0].s = toPark ? ISS_ON : ISS_OFF; ParkS[1].s = toPark ? ISS_OFF : ISS_ON; ParkSP.s = IPS_OK; } } else { ParkS[preIndex].s = ISS_ON; ParkSP.s = IPS_ALERT; } IDSetSwitch(&ParkSP, nullptr); return true; } /////////////////////////////////// // NS Motion /////////////////////////////////// if (!strcmp(name, MovementNSSP.name)) { // Check if it is already parked. if (CanPark()) { if (isParked()) { DEBUG(Logger::DBG_WARNING, "Please unpark the mount before issuing any motion/sync commands."); MovementNSSP.s = IPS_IDLE; IDSetSwitch(&MovementNSSP, nullptr); return false; } } IUUpdateSwitch(&MovementNSSP, states, names, n); int current_motion = IUFindOnSwitchIndex(&MovementNSSP); // if same move requested, return if (MovementNSSP.s == IPS_BUSY && current_motion == last_ns_motion) return true; // Time to stop motion if (current_motion == -1 || (last_ns_motion != -1 && current_motion != last_ns_motion)) { if (MoveNS(last_ns_motion == 0 ? DIRECTION_NORTH : DIRECTION_SOUTH, MOTION_STOP)) { IUResetSwitch(&MovementNSSP); MovementNSSP.s = IPS_IDLE; last_ns_motion = -1; } else MovementNSSP.s = IPS_ALERT; } else { if (TrackState != SCOPE_SLEWING && TrackState != SCOPE_PARKING) RememberTrackState = TrackState; if (MoveNS(current_motion == 0 ? DIRECTION_NORTH : DIRECTION_SOUTH, MOTION_START)) { MovementNSSP.s = IPS_BUSY; last_ns_motion = current_motion; } else { IUResetSwitch(&MovementNSSP); MovementNSSP.s = IPS_ALERT; last_ns_motion = -1; } } IDSetSwitch(&MovementNSSP, nullptr); return true; } /////////////////////////////////// // WE Motion /////////////////////////////////// if (!strcmp(name, MovementWESP.name)) { // Check if it is already parked. if (CanPark()) { if (isParked()) { DEBUG(Logger::DBG_WARNING, "Please unpark the mount before issuing any motion/sync commands."); MovementWESP.s = IPS_IDLE; IDSetSwitch(&MovementWESP, nullptr); return false; } } IUUpdateSwitch(&MovementWESP, states, names, n); int current_motion = IUFindOnSwitchIndex(&MovementWESP); // if same move requested, return if (MovementWESP.s == IPS_BUSY && current_motion == last_we_motion) return true; // Time to stop motion if (current_motion == -1 || (last_we_motion != -1 && current_motion != last_we_motion)) { if (MoveWE(last_we_motion == 0 ? DIRECTION_WEST : DIRECTION_EAST, MOTION_STOP)) { IUResetSwitch(&MovementWESP); MovementWESP.s = IPS_IDLE; last_we_motion = -1; } else MovementWESP.s = IPS_ALERT; } else { if (TrackState != SCOPE_SLEWING && TrackState != SCOPE_PARKING) RememberTrackState = TrackState; if (MoveWE(current_motion == 0 ? DIRECTION_WEST : DIRECTION_EAST, MOTION_START)) { MovementWESP.s = IPS_BUSY; last_we_motion = current_motion; } else { IUResetSwitch(&MovementWESP); MovementWESP.s = IPS_ALERT; last_we_motion = -1; } } IDSetSwitch(&MovementWESP, nullptr); return true; } /////////////////////////////////// // Abort Motion /////////////////////////////////// if (!strcmp(name, AbortSP.name)) { IUResetSwitch(&AbortSP); if (Abort()) { AbortSP.s = IPS_OK; if (ParkSP.s == IPS_BUSY) { IUResetSwitch(&ParkSP); ParkSP.s = IPS_ALERT; IDSetSwitch(&ParkSP, nullptr); DEBUG(Logger::DBG_SESSION, "Parking aborted."); } if (EqNP.s == IPS_BUSY) { EqNP.s = lastEqState = IPS_IDLE; IDSetNumber(&EqNP, nullptr); DEBUG(Logger::DBG_SESSION, "Slew/Track aborted."); } if (MovementWESP.s == IPS_BUSY) { IUResetSwitch(&MovementWESP); MovementWESP.s = IPS_IDLE; IDSetSwitch(&MovementWESP, nullptr); } if (MovementNSSP.s == IPS_BUSY) { IUResetSwitch(&MovementNSSP); MovementNSSP.s = IPS_IDLE; IDSetSwitch(&MovementNSSP, nullptr); } last_ns_motion = last_we_motion = -1; // JM 2017-07-28: Abort shouldn't affect tracking state. It should affect motion and that's it. //if (TrackState != SCOPE_PARKED) //TrackState = SCOPE_IDLE; // For Idle, Tracking, Parked state, we do not change its status, it should remain as is. // For Slewing & Parking, state should go back to last rememberd state. if (TrackState == SCOPE_SLEWING || TrackState == SCOPE_PARKING) { TrackState = RememberTrackState; } } else AbortSP.s = IPS_ALERT; IDSetSwitch(&AbortSP, nullptr); return true; } /////////////////////////////////// // Track Mode /////////////////////////////////// if (!strcmp(name, TrackModeSP.name)) { int prevIndex = IUFindOnSwitchIndex(&TrackModeSP); IUUpdateSwitch(&TrackModeSP, states, names, n); int currIndex = IUFindOnSwitchIndex(&TrackModeSP); // If same as previous index, or if scope is already idle, then just update switch and return. No commands are sent to the mount. if (prevIndex == currIndex || TrackState == SCOPE_IDLE) { TrackModeSP.s = IPS_OK; IDSetSwitch(&TrackModeSP, nullptr); return true; } if (TrackState == SCOPE_PARKED) { DEBUG(Logger::DBG_WARNING, "Telescope is Parked, Unpark before changing track mode."); return false; } bool rc = SetTrackMode(currIndex); if (rc) TrackModeSP.s = IPS_OK; else { IUResetSwitch(&TrackModeSP); TrackModeS[prevIndex].s = ISS_ON; TrackModeSP.s = IPS_ALERT; } IDSetSwitch(&TrackModeSP, nullptr); return false; } /////////////////////////////////// // Track State /////////////////////////////////// if (!strcmp(name, TrackStateSP.name)) { int previousState = IUFindOnSwitchIndex(&TrackStateSP); IUUpdateSwitch(&TrackStateSP, states, names, n); int targetState = IUFindOnSwitchIndex(&TrackStateSP); if (previousState == targetState) { IDSetSwitch(&TrackStateSP, NULL); return true; } if (TrackState == SCOPE_PARKED) { DEBUG(Logger::DBG_WARNING, "Telescope is Parked, Unpark before tracking."); return false; } bool rc = SetTrackEnabled((targetState == TRACK_ON) ? true : false); if (rc) { TrackState = (targetState == TRACK_ON) ? SCOPE_TRACKING : SCOPE_IDLE; TrackStateSP.s = (targetState == TRACK_ON) ? IPS_BUSY : IPS_IDLE; TrackStateS[TRACK_ON].s = (targetState == TRACK_ON) ? ISS_ON : ISS_OFF; TrackStateS[TRACK_OFF].s = (targetState == TRACK_ON) ? ISS_OFF : ISS_ON; } else { TrackStateSP.s = IPS_ALERT; IUResetSwitch(&TrackStateSP); TrackStateS[previousState].s = ISS_ON; } IDSetSwitch(&TrackStateSP, NULL); return true; } /////////////////////////////////// // Park Options /////////////////////////////////// if (!strcmp(name, ParkOptionSP.name)) { IUUpdateSwitch(&ParkOptionSP, states, names, n); ISwitch *sp = IUFindOnSwitch(&ParkOptionSP); if (!sp) return false; IUResetSwitch(&ParkOptionSP); bool rc = false; if ((TrackState != SCOPE_IDLE && TrackState != SCOPE_TRACKING) || MovementNSSP.s == IPS_BUSY || MovementWESP.s == IPS_BUSY) { DEBUG(Logger::DBG_SESSION, "Can not change park position while slewing or already parked..."); ParkOptionSP.s = IPS_ALERT; IDSetSwitch(&ParkOptionSP, nullptr); return false; } if (!strcmp(sp->name, "PARK_CURRENT")) { rc = SetCurrentPark(); } else if (!strcmp(sp->name, "PARK_DEFAULT")) { rc = SetDefaultPark(); } else if (!strcmp(sp->name, "PARK_WRITE_DATA")) { rc = WriteParkData(); if (rc) DEBUG(Logger::DBG_SESSION, "Saved Park Status/Position."); else DEBUG(Logger::DBG_WARNING, "Can not save Park Status/Position."); } ParkOptionSP.s = rc ? IPS_OK : IPS_ALERT; IDSetSwitch(&ParkOptionSP, nullptr); return true; } /////////////////////////////////// // Parking Dome Policy /////////////////////////////////// if (!strcmp(name, DomeClosedLockTP.name)) { if (n == 1) { if (!strcmp(names[0], DomeClosedLockT[0].name)) DEBUG(Logger::DBG_SESSION, "Dome parking policy set to: Ignore dome"); else if (!strcmp(names[0], DomeClosedLockT[1].name)) DEBUG(Logger::DBG_SESSION, "Warning: Dome parking policy set to: Dome locks. This disallows " "the scope from unparking when dome is parked"); else if (!strcmp(names[0], DomeClosedLockT[2].name)) DEBUG(Logger::DBG_SESSION, "Warning: Dome parking policy set to: Dome parks. This tells " "scope to park if dome is parking. This will disable the locking " "for dome parking, EVEN IF MOUNT PARKING FAILS"); else if (!strcmp(names[0], DomeClosedLockT[3].name)) DEBUG(Logger::DBG_SESSION, "Warning: Dome parking policy set to: Both. This disallows the " "scope from unparking when dome is parked, and tells scope to " "park if dome is parking. This will disable the locking for dome " "parking, EVEN IF MOUNT PARKING FAILS."); } IUUpdateSwitch(&DomeClosedLockTP, states, names, n); DomeClosedLockTP.s = IPS_OK; IDSetSwitch(&DomeClosedLockTP, nullptr); triggerSnoop(ActiveDeviceT[1].text, "DOME_PARK"); return true; } /////////////////////////////////// // Joystick Lock Axis /////////////////////////////////// if (!strcmp(name, LockAxisSP.name)) { IUUpdateSwitch(&LockAxisSP, states, names, n); LockAxisSP.s = IPS_OK; IDSetSwitch(&LockAxisSP, nullptr); if (LockAxisS[AXIS_RA].s == ISS_ON) DEBUG(Logger::DBG_SESSION, "Joystick motion is locked to West/East axis only."); else if (LockAxisS[AXIS_DE].s == ISS_ON) DEBUG(Logger::DBG_SESSION, "Joystick motion is locked to North/South axis only."); else DEBUG(Logger::DBG_SESSION, "Joystick motion is unlocked."); return true; } /////////////////////////////////// // Scope Apply Config /////////////////////////////////// if (name && std::string(name) == "APPLY_SCOPE_CONFIG") { IUUpdateSwitch(&ScopeConfigsSP, states, names, n); bool rc = LoadScopeConfig(); ScopeConfigsSP.s = (rc ? IPS_OK : IPS_ALERT); IDSetSwitch(&ScopeConfigsSP, nullptr); return true; } } bool rc = controller->ISNewSwitch(dev, name, states, names, n); if (rc) { ISwitchVectorProperty *useJoystick = getSwitch("USEJOYSTICK"); if (useJoystick && useJoystick->sp[0].s == ISS_ON) defineSwitch(&LockAxisSP); else deleteProperty(LockAxisSP.name); } // Nobody has claimed this, so, ignore it return DefaultDevice::ISNewSwitch(dev, name, states, names, n); } bool Telescope::callHandshake() { if (telescopeConnection > 0) { if (getActiveConnection() == serialConnection) PortFD = serialConnection->getPortFD(); else if (getActiveConnection() == tcpConnection) PortFD = tcpConnection->getPortFD(); } return Handshake(); } bool Telescope::Handshake() { /* Test connection */ return ReadScopeStatus(); } void Telescope::TimerHit() { if (isConnected()) { bool rc; rc = ReadScopeStatus(); if (!rc) { // read was not good EqNP.s = lastEqState = IPS_ALERT; IDSetNumber(&EqNP, nullptr); } SetTimer(POLLMS); } } bool Telescope::Goto(double ra, double dec) { INDI_UNUSED(ra); INDI_UNUSED(dec); DEBUG(Logger::DBG_WARNING, "GOTO is not supported."); return false; } bool Telescope::Abort() { DEBUG(Logger::DBG_WARNING, "Abort is not supported."); return false; } bool Telescope::Park() { DEBUG(Logger::DBG_WARNING, "Parking is not supported."); return false; } bool Telescope::UnPark() { DEBUG(Logger::DBG_WARNING, "UnParking is not supported."); return false; } bool Telescope::SetTrackMode(uint8_t mode) { INDI_UNUSED(mode); DEBUG(Logger::DBG_WARNING, "Tracking mode is not supported."); return false; } bool Telescope::SetTrackRate(double raRate, double deRate) { INDI_UNUSED(raRate); INDI_UNUSED(deRate); DEBUG(Logger::DBG_WARNING, "Custom tracking rates is not supported."); return false; } bool Telescope::SetTrackEnabled(bool enabled) { INDI_UNUSED(enabled); DEBUG(Logger::DBG_WARNING, "Tracking state is not supported."); return false; } int Telescope::AddTrackMode(const char *name, const char *label, bool isDefault) { TrackModeS = (TrackModeS == nullptr) ? (ISwitch *)malloc(sizeof(ISwitch)) : (ISwitch *)realloc(TrackModeS, (TrackModeSP.nsp + 1) * sizeof(ISwitch)); IUFillSwitch(&TrackModeS[TrackModeSP.nsp], name, label, isDefault ? ISS_ON : ISS_OFF); TrackModeSP.sp = TrackModeS; TrackModeSP.nsp++; return (TrackModeSP.nsp-1); } bool Telescope::SetCurrentPark() { DEBUG(Logger::DBG_WARNING, "Parking is not supported."); return false; } bool Telescope::SetDefaultPark() { DEBUG(Logger::DBG_WARNING, "Parking is not supported."); return false; } bool Telescope::processTimeInfo(const char *utc, const char *offset) { struct ln_date utc_date; double utc_offset = 0; if (extractISOTime(utc, &utc_date) == -1) { TimeTP.s = IPS_ALERT; IDSetText(&TimeTP, "Date/Time is invalid: %s.", utc); return false; } utc_offset = atof(offset); if (updateTime(&utc_date, utc_offset)) { IUSaveText(&TimeT[0], utc); IUSaveText(&TimeT[1], offset); TimeTP.s = IPS_OK; IDSetText(&TimeTP, nullptr); return true; } else { TimeTP.s = IPS_ALERT; IDSetText(&TimeTP, nullptr); return false; } } bool Telescope::processLocationInfo(double latitude, double longitude, double elevation) { // Do not update if not necessary if (latitude == LocationN[LOCATION_LATITUDE].value && longitude == LocationN[LOCATION_LONGITUDE].value && elevation == LocationN[LOCATION_ELEVATION].value) { LocationNP.s = IPS_OK; IDSetNumber(&LocationNP, nullptr); } if (updateLocation(latitude, longitude, elevation)) { LocationNP.s = IPS_OK; LocationN[LOCATION_LATITUDE].value = latitude; LocationN[LOCATION_LONGITUDE].value = longitude; LocationN[LOCATION_ELEVATION].value = elevation; // Update client display IDSetNumber(&LocationNP, nullptr); // Always save geographic coord config immediately. saveConfig(true, "GEOGRAPHIC_COORD"); return true; } else { LocationNP.s = IPS_ALERT; // Update client display IDSetNumber(&LocationNP, nullptr); return false; } } bool Telescope::updateTime(ln_date *utc, double utc_offset) { INDI_UNUSED(utc); INDI_UNUSED(utc_offset); return true; } bool Telescope::updateLocation(double latitude, double longitude, double elevation) { INDI_UNUSED(latitude); INDI_UNUSED(longitude); INDI_UNUSED(elevation); return true; } bool Telescope::SetParkPosition(double Axis1Value, double Axis2Value) { INDI_UNUSED(Axis1Value); INDI_UNUSED(Axis2Value); return true; } void Telescope::SetTelescopeCapability(uint32_t cap, uint8_t slewRateCount) { capability = cap; nSlewRate = slewRateCount; // If both GOTO and SYNC are supported if (CanGOTO() && CanSync()) IUFillSwitchVector(&CoordSP, CoordS, 3, getDeviceName(), "ON_COORD_SET", "On Set", MAIN_CONTROL_TAB, IP_RW, ISR_1OFMANY, 60, IPS_IDLE); // If ONLY GOTO is supported else if (CanGOTO()) IUFillSwitchVector(&CoordSP, CoordS, 2, getDeviceName(), "ON_COORD_SET", "On Set", MAIN_CONTROL_TAB, IP_RW, ISR_1OFMANY, 60, IPS_IDLE); // If ONLY SYNC is supported else if (CanSync()) { IUFillSwitch(&CoordS[0], "SYNC", "Sync", ISS_ON); IUFillSwitchVector(&CoordSP, CoordS, 1, getDeviceName(), "ON_COORD_SET", "On Set", MAIN_CONTROL_TAB, IP_RW, ISR_1OFMANY, 60, IPS_IDLE); } if (nSlewRate >= 4) { free(SlewRateS); SlewRateS = (ISwitch *)malloc(sizeof(ISwitch) * nSlewRate); int step = nSlewRate / 4; for (int i = 0; i < nSlewRate; i++) { char name[4]; snprintf(name, 4, "%dx", i + 1); IUFillSwitch(SlewRateS + i, name, name, ISS_OFF); } strncpy((SlewRateS + (step * 0))->name, "SLEW_GUIDE", MAXINDINAME); strncpy((SlewRateS + (step * 1))->name, "SLEW_CENTERING", MAXINDINAME); strncpy((SlewRateS + (step * 2))->name, "SLEW_FIND", MAXINDINAME); strncpy((SlewRateS + (nSlewRate - 1))->name, "SLEW_MAX", MAXINDINAME); // By Default we set current Slew Rate to 0.5 of max (SlewRateS + (nSlewRate / 2))->s = ISS_ON; IUFillSwitchVector(&SlewRateSP, SlewRateS, nSlewRate, getDeviceName(), "TELESCOPE_SLEW_RATE", "Slew Rate", MOTION_TAB, IP_RW, ISR_1OFMANY, 0, IPS_IDLE); } } void Telescope::SetParkDataType(TelescopeParkData type) { parkDataType = type; if (parkDataType != PARK_NONE) { switch (parkDataType) { case PARK_RA_DEC: IUFillNumber(&ParkPositionN[AXIS_RA], "PARK_RA", "RA (hh:mm:ss)", "%010.6m", 0, 24, 0, 0); IUFillNumber(&ParkPositionN[AXIS_DE], "PARK_DEC", "DEC (dd:mm:ss)", "%010.6m", -90, 90, 0, 0); break; case PARK_HA_DEC: IUFillNumber(&ParkPositionN[AXIS_RA], "PARK_HA", "HA (hh:mm:ss)", "%010.6m", -12, 12, 0, 0); IUFillNumber(&ParkPositionN[AXIS_DE], "PARK_DEC", "DEC (dd:mm:ss)", "%010.6m", -90, 90, 0, 0); break; case PARK_AZ_ALT: IUFillNumber(&ParkPositionN[AXIS_AZ], "PARK_AZ", "AZ D:M:S", "%10.6m", 0.0, 360.0, 0.0, 0); IUFillNumber(&ParkPositionN[AXIS_ALT], "PARK_ALT", "Alt D:M:S", "%10.6m", -90., 90.0, 0.0, 0); break; case PARK_RA_DEC_ENCODER: IUFillNumber(&ParkPositionN[AXIS_RA], "PARK_RA", "RA Encoder", "%.0f", 0, 16777215, 1, 0); IUFillNumber(&ParkPositionN[AXIS_DE], "PARK_DEC", "DEC Encoder", "%.0f", 0, 16777215, 1, 0); break; case PARK_AZ_ALT_ENCODER: IUFillNumber(&ParkPositionN[AXIS_RA], "PARK_AZ", "AZ Encoder", "%.0f", 0, 16777215, 1, 0); IUFillNumber(&ParkPositionN[AXIS_DE], "PARK_ALT", "ALT Encoder", "%.0f", 0, 16777215, 1, 0); break; default: break; } IUFillNumberVector(&ParkPositionNP, ParkPositionN, 2, getDeviceName(), "TELESCOPE_PARK_POSITION", "Park Position", SITE_TAB, IP_RW, 60, IPS_IDLE); } } void Telescope::SetParked(bool isparked) { IsParked = isparked; IUResetSwitch(&ParkSP); if (IsParked) { ParkS[0].s = ISS_ON; TrackState = SCOPE_PARKED; ParkSP.s = IPS_OK; DEBUG(Logger::DBG_SESSION, "Mount is parked."); } else { ParkS[1].s = ISS_ON; TrackState = SCOPE_IDLE; ParkSP.s = IPS_IDLE; DEBUG(Logger::DBG_SESSION, "Mount is unparked."); } IDSetSwitch(&ParkSP, nullptr); if (parkDataType != PARK_NONE) WriteParkData(); } bool Telescope::isParked() { return IsParked; } bool Telescope::InitPark() { char *loadres; loadres = LoadParkData(); if (loadres) { DEBUGF(Logger::DBG_SESSION, "InitPark: No Park data in file %s: %s", ParkDataFileName.c_str(), loadres); SetParked(false); return false; } SetParked(isParked()); DEBUGF(Logger::DBG_DEBUG, "InitPark Axis1 %g Axis2 %g", Axis1ParkPosition, Axis2ParkPosition); ParkPositionN[AXIS_RA].value = Axis1ParkPosition; ParkPositionN[AXIS_DE].value = Axis2ParkPosition; IDSetNumber(&ParkPositionNP, nullptr); return true; } char *Telescope::LoadParkData() { wordexp_t wexp; FILE *fp; LilXML *lp; static char errmsg[512]; XMLEle *parkxml; XMLAtt *ap; bool devicefound = false; ParkDeviceName = getDeviceName(); ParkstatusXml = nullptr; ParkdeviceXml = nullptr; ParkpositionXml = nullptr; ParkpositionAxis1Xml = nullptr; ParkpositionAxis2Xml = nullptr; if (wordexp(ParkDataFileName.c_str(), &wexp, 0)) { wordfree(&wexp); return (char *)("Badly formed filename."); } if (!(fp = fopen(wexp.we_wordv[0], "r"))) { wordfree(&wexp); return strerror(errno); } wordfree(&wexp); lp = newLilXML(); if (ParkdataXmlRoot) delXMLEle(ParkdataXmlRoot); ParkdataXmlRoot = readXMLFile(fp, lp, errmsg); delLilXML(lp); if (!ParkdataXmlRoot) return errmsg; if (!strcmp(tagXMLEle(nextXMLEle(ParkdataXmlRoot, 1)), "parkdata")) return (char *)("Not a park data file"); parkxml = nextXMLEle(ParkdataXmlRoot, 1); while (parkxml) { if (strcmp(tagXMLEle(parkxml), "device")) { parkxml = nextXMLEle(ParkdataXmlRoot, 0); continue; } ap = findXMLAtt(parkxml, "name"); if (ap && (!strcmp(valuXMLAtt(ap), ParkDeviceName))) { devicefound = true; break; } parkxml = nextXMLEle(ParkdataXmlRoot, 0); } if (!devicefound) return (char *)"No park data found for this device"; ParkdeviceXml = parkxml; ParkstatusXml = findXMLEle(parkxml, "parkstatus"); ParkpositionXml = findXMLEle(parkxml, "parkposition"); ParkpositionAxis1Xml = findXMLEle(ParkpositionXml, "axis1position"); ParkpositionAxis2Xml = findXMLEle(ParkpositionXml, "axis2position"); IsParked = false; if (ParkstatusXml == nullptr || ParkpositionAxis1Xml == nullptr || ParkpositionAxis2Xml == nullptr) { return (char *)("Park data invalid or missing."); } if (!strcmp(pcdataXMLEle(ParkstatusXml), "true")) IsParked = true; int rc = 0; double axis1Pos = std::numeric_limits::quiet_NaN(); double axis2Pos = std::numeric_limits::quiet_NaN(); rc = sscanf(pcdataXMLEle(ParkpositionAxis1Xml), "%lf", &axis1Pos); if (rc != 1) { return (char *)("Unable to parse Park Position Axis 1."); } rc = sscanf(pcdataXMLEle(ParkpositionAxis2Xml), "%lf", &axis2Pos); if (rc != 1) { return (char *)("Unable to parse Park Position Axis 2."); } if (std::isnan(axis1Pos) == false && std::isnan(axis2Pos) == false) { Axis1ParkPosition = axis1Pos; Axis2ParkPosition = axis2Pos; return nullptr; } return (char *)("Failed to parse Park Position."); } bool Telescope::WriteParkData() { wordexp_t wexp; FILE *fp; char pcdata[30]; ParkDeviceName = getDeviceName(); if (wordexp(ParkDataFileName.c_str(), &wexp, 0)) { wordfree(&wexp); DEBUGF(Logger::DBG_SESSION, "WriteParkData: can not write file %s: Badly formed filename.", ParkDataFileName.c_str()); return false; } if (!(fp = fopen(wexp.we_wordv[0], "w"))) { wordfree(&wexp); DEBUGF(Logger::DBG_SESSION, "WriteParkData: can not write file %s: %s", ParkDataFileName.c_str(), strerror(errno)); return false; } if (!ParkdataXmlRoot) ParkdataXmlRoot = addXMLEle(nullptr, "parkdata"); if (!ParkdeviceXml) { ParkdeviceXml = addXMLEle(ParkdataXmlRoot, "device"); addXMLAtt(ParkdeviceXml, "name", ParkDeviceName); } if (!ParkstatusXml) ParkstatusXml = addXMLEle(ParkdeviceXml, "parkstatus"); if (!ParkpositionXml) ParkpositionXml = addXMLEle(ParkdeviceXml, "parkposition"); if (!ParkpositionAxis1Xml) ParkpositionAxis1Xml = addXMLEle(ParkpositionXml, "axis1position"); if (!ParkpositionAxis2Xml) ParkpositionAxis2Xml = addXMLEle(ParkpositionXml, "axis2position"); editXMLEle(ParkstatusXml, (IsParked ? "true" : "false")); snprintf(pcdata, sizeof(pcdata), "%lf", Axis1ParkPosition); editXMLEle(ParkpositionAxis1Xml, pcdata); snprintf(pcdata, sizeof(pcdata), "%lf", Axis2ParkPosition); editXMLEle(ParkpositionAxis2Xml, pcdata); prXMLEle(fp, ParkdataXmlRoot, 0); fclose(fp); wordfree(&wexp); return true; } double Telescope::GetAxis1Park() const { return Axis1ParkPosition; } double Telescope::GetAxis1ParkDefault() const { return Axis1DefaultParkPosition; } double Telescope::GetAxis2Park() const { return Axis2ParkPosition; } double Telescope::GetAxis2ParkDefault() const { return Axis2DefaultParkPosition; } void Telescope::SetAxis1Park(double value) { Axis1ParkPosition = value; ParkPositionN[AXIS_RA].value = value; IDSetNumber(&ParkPositionNP, nullptr); } void Telescope::SetAxis1ParkDefault(double value) { Axis1DefaultParkPosition = value; } void Telescope::SetAxis2Park(double value) { Axis2ParkPosition = value; ParkPositionN[AXIS_DE].value = value; IDSetNumber(&ParkPositionNP, nullptr); } void Telescope::SetAxis2ParkDefault(double value) { Axis2DefaultParkPosition = value; } bool Telescope::isLocked() const { return (DomeClosedLockT[1].s == ISS_ON || DomeClosedLockT[3].s == ISS_ON) && IsLocked; } bool Telescope::SetSlewRate(int index) { INDI_UNUSED(index); return true; } void Telescope::processButton(const char *button_n, ISState state) { //ignore OFF if (state == ISS_OFF) return; if (!strcmp(button_n, "ABORTBUTTON")) { ISwitchVectorProperty *trackSW = getSwitch("TELESCOPE_TRACK_MODE"); // Only abort if we have some sort of motion going on if (ParkSP.s == IPS_BUSY || MovementNSSP.s == IPS_BUSY || MovementWESP.s == IPS_BUSY || EqNP.s == IPS_BUSY || (trackSW && trackSW->s == IPS_BUSY)) { // Invoke parent processing so that Telescope takes care of abort cross-check ISState states[1] = { ISS_ON }; char *names[1] = { AbortS[0].name }; ISNewSwitch(getDeviceName(), AbortSP.name, states, names, 1); } } else if (!strcmp(button_n, "PARKBUTTON")) { ISState states[2] = { ISS_ON, ISS_OFF }; char *names[2] = { ParkS[0].name, ParkS[1].name }; ISNewSwitch(getDeviceName(), ParkSP.name, states, names, 2); } else if (!strcmp(button_n, "UNPARKBUTTON")) { ISState states[2] = { ISS_OFF, ISS_ON }; char *names[2] = { ParkS[0].name, ParkS[1].name }; ISNewSwitch(getDeviceName(), ParkSP.name, states, names, 2); } else if (!strcmp(button_n, "SLEWPRESETUP")) { processSlewPresets(1, 270); } else if (!strcmp(button_n, "SLEWPRESETDOWN")) { processSlewPresets(1, 90); } } void Telescope::processJoystick(const char *joystick_n, double mag, double angle) { if (!strcmp(joystick_n, "MOTIONDIR")) { if ((TrackState == SCOPE_PARKING) || (TrackState == SCOPE_PARKED)) { DEBUG(Logger::DBG_WARNING, "Can not slew while mount is parking/parked."); return; } processNSWE(mag, angle); } else if (!strcmp(joystick_n, "SLEWPRESET")) processSlewPresets(mag, angle); } void Telescope::processNSWE(double mag, double angle) { if (mag < 0.5) { // Moving in the same direction will make it stop if (MovementNSSP.s == IPS_BUSY) { if (MoveNS(MovementNSSP.sp[0].s == ISS_ON ? DIRECTION_NORTH : DIRECTION_SOUTH, MOTION_STOP)) { IUResetSwitch(&MovementNSSP); MovementNSSP.s = IPS_IDLE; IDSetSwitch(&MovementNSSP, nullptr); } else { MovementNSSP.s = IPS_ALERT; IDSetSwitch(&MovementNSSP, nullptr); } } if (MovementWESP.s == IPS_BUSY) { if (MoveWE(MovementWESP.sp[0].s == ISS_ON ? DIRECTION_WEST : DIRECTION_EAST, MOTION_STOP)) { IUResetSwitch(&MovementWESP); MovementWESP.s = IPS_IDLE; IDSetSwitch(&MovementWESP, nullptr); } else { MovementWESP.s = IPS_ALERT; IDSetSwitch(&MovementWESP, nullptr); } } } // Put high threshold else if (mag > 0.9) { // Only one axis can move at a time if (LockAxisS[AXIS_RA].s == ISS_ON) { // West if (angle >= 90 && angle <= 270) angle = 180; // East else angle = 0; } else if (LockAxisS[AXIS_DE].s == ISS_ON) { // North if (angle >= 0 && angle <= 180) angle = 90; // South else angle = 270; } // North if (angle > 0 && angle < 180) { // Don't try to move if you're busy and moving in the same direction if (MovementNSSP.s != IPS_BUSY || MovementNSS[0].s != ISS_ON) MoveNS(DIRECTION_NORTH, MOTION_START); // If angle is close to 90, make it exactly 90 to reduce noise that could trigger east/west motion as well if (angle > 80 && angle < 110) angle = 90; MovementNSSP.s = IPS_BUSY; MovementNSSP.sp[DIRECTION_NORTH].s = ISS_ON; MovementNSSP.sp[DIRECTION_SOUTH].s = ISS_OFF; IDSetSwitch(&MovementNSSP, nullptr); } // South if (angle > 180 && angle < 360) { // Don't try to move if you're busy and moving in the same direction if (MovementNSSP.s != IPS_BUSY || MovementNSS[1].s != ISS_ON) MoveNS(DIRECTION_SOUTH, MOTION_START); // If angle is close to 270, make it exactly 270 to reduce noise that could trigger east/west motion as well if (angle > 260 && angle < 280) angle = 270; MovementNSSP.s = IPS_BUSY; MovementNSSP.sp[DIRECTION_NORTH].s = ISS_OFF; MovementNSSP.sp[DIRECTION_SOUTH].s = ISS_ON; IDSetSwitch(&MovementNSSP, nullptr); } // East if (angle < 90 || angle > 270) { // Don't try to move if you're busy and moving in the same direction if (MovementWESP.s != IPS_BUSY || MovementWES[1].s != ISS_ON) MoveWE(DIRECTION_EAST, MOTION_START); MovementWESP.s = IPS_BUSY; MovementWESP.sp[DIRECTION_WEST].s = ISS_OFF; MovementWESP.sp[DIRECTION_EAST].s = ISS_ON; IDSetSwitch(&MovementWESP, nullptr); } // West if (angle > 90 && angle < 270) { // Don't try to move if you're busy and moving in the same direction if (MovementWESP.s != IPS_BUSY || MovementWES[0].s != ISS_ON) MoveWE(DIRECTION_WEST, MOTION_START); MovementWESP.s = IPS_BUSY; MovementWESP.sp[DIRECTION_WEST].s = ISS_ON; MovementWESP.sp[DIRECTION_EAST].s = ISS_OFF; IDSetSwitch(&MovementWESP, nullptr); } } } void Telescope::processSlewPresets(double mag, double angle) { // high threshold, only 1 is accepted if (mag != 1) return; int currentIndex = IUFindOnSwitchIndex(&SlewRateSP); // Up if (angle > 0 && angle < 180) { if (currentIndex <= 0) return; IUResetSwitch(&SlewRateSP); SlewRateS[currentIndex - 1].s = ISS_ON; SetSlewRate(currentIndex - 1); } // Down else { if (currentIndex >= SlewRateSP.nsp - 1) return; IUResetSwitch(&SlewRateSP); SlewRateS[currentIndex + 1].s = ISS_ON; SetSlewRate(currentIndex - 1); } IDSetSwitch(&SlewRateSP, nullptr); } void Telescope::joystickHelper(const char *joystick_n, double mag, double angle, void *context) { static_cast(context)->processJoystick(joystick_n, mag, angle); } void Telescope::buttonHelper(const char *button_n, ISState state, void *context) { static_cast(context)->processButton(button_n, state); } void Telescope::setPierSide(TelescopePierSide side) { currentPierSide = side; if (currentPierSide != lastPierSide) { PierSideS[PIER_WEST].s = (side == PIER_WEST) ? ISS_ON : ISS_OFF; PierSideS[PIER_EAST].s = (side == PIER_EAST) ? ISS_ON : ISS_OFF; PierSideSP.s = IPS_OK; IDSetSwitch(&PierSideSP, nullptr); lastPierSide = currentPierSide; } } void Telescope::setPECState(TelescopePECState state) { currentPECState = state; if (currentPECState != lastPECState) { PECStateS[PEC_OFF].s = (state == PEC_ON) ? ISS_OFF : ISS_ON; PECStateS[PEC_ON].s = (state == PEC_ON) ? ISS_ON : ISS_OFF; PECStateSP.s = IPS_OK; IDSetSwitch(&PECStateSP, nullptr); lastPECState = currentPECState; } } bool Telescope::LoadScopeConfig() { if (!CheckFile(ScopeConfigFileName, false)) { DEBUGF(Logger::DBG_SESSION, "Can't open XML file (%s) for read", ScopeConfigFileName.c_str()); return false; } LilXML *XmlHandle = newLilXML(); FILE *FilePtr = fopen(ScopeConfigFileName.c_str(), "r"); XMLEle *RootXmlNode = nullptr; XMLEle *CurrentXmlNode = nullptr; XMLAtt *Ap = nullptr; bool DeviceFound = false; char ErrMsg[512]; RootXmlNode = readXMLFile(FilePtr, XmlHandle, ErrMsg); delLilXML(XmlHandle); XmlHandle = nullptr; if (!RootXmlNode) { DEBUGF(Logger::DBG_SESSION, "Failed to parse XML file (%s): %s", ScopeConfigFileName.c_str(), ErrMsg); return false; } if (std::string(tagXMLEle(RootXmlNode)) != ScopeConfigRootXmlNode) { DEBUGF(Logger::DBG_SESSION, "Not a scope config XML file (%s)", ScopeConfigFileName.c_str()); delXMLEle(RootXmlNode); return false; } CurrentXmlNode = nextXMLEle(RootXmlNode, 1); // Find the current telescope in the config file while (CurrentXmlNode) { if (std::string(tagXMLEle(CurrentXmlNode)) != ScopeConfigDeviceXmlNode) { CurrentXmlNode = nextXMLEle(RootXmlNode, 0); continue; } Ap = findXMLAtt(CurrentXmlNode, ScopeConfigNameXmlNode.c_str()); if (Ap && !strcmp(valuXMLAtt(Ap), getDeviceName())) { DeviceFound = true; break; } CurrentXmlNode = nextXMLEle(RootXmlNode, 0); } if (!DeviceFound) { DEBUGF(Logger::DBG_SESSION, "No a scope config found for %s in the XML file (%s)", getDeviceName(), ScopeConfigFileName.c_str()); delXMLEle(RootXmlNode); return false; } // Read the values XMLEle *XmlNode = nullptr; const int ConfigIndex = GetScopeConfigIndex(); double ScopeFoc = 0, ScopeAp = 0; double GScopeFoc = 0, GScopeAp = 0; std::string ConfigName; CurrentXmlNode = findXMLEle(CurrentXmlNode, ("config" + std::to_string(ConfigIndex)).c_str()); if (!CurrentXmlNode) { DEBUGF(Logger::DBG_SESSION, "Config %d is not found in the XML file (%s). To save a new config, update and set scope properties and " "config name.", ConfigIndex, ScopeConfigFileName.c_str()); delXMLEle(RootXmlNode); return false; } XmlNode = findXMLEle(CurrentXmlNode, ScopeConfigScopeFocXmlNode.c_str()); if (!XmlNode || sscanf(pcdataXMLEle(XmlNode), "%lf", &ScopeFoc) != 1) { DEBUGF(Logger::DBG_SESSION, "Can't read the telescope focal length from the XML file (%s)", ScopeConfigFileName.c_str()); delXMLEle(RootXmlNode); return false; } XmlNode = findXMLEle(CurrentXmlNode, ScopeConfigScopeApXmlNode.c_str()); if (!XmlNode || sscanf(pcdataXMLEle(XmlNode), "%lf", &ScopeAp) != 1) { DEBUGF(Logger::DBG_SESSION, "Can't read the telescope aperture from the XML file (%s)", ScopeConfigFileName.c_str()); delXMLEle(RootXmlNode); return false; } XmlNode = findXMLEle(CurrentXmlNode, ScopeConfigGScopeFocXmlNode.c_str()); if (!XmlNode || sscanf(pcdataXMLEle(XmlNode), "%lf", &GScopeFoc) != 1) { DEBUGF(Logger::DBG_SESSION, "Can't read the guide scope focal length from the XML file (%s)", ScopeConfigFileName.c_str()); delXMLEle(RootXmlNode); return false; } XmlNode = findXMLEle(CurrentXmlNode, ScopeConfigGScopeApXmlNode.c_str()); if (!XmlNode || sscanf(pcdataXMLEle(XmlNode), "%lf", &GScopeAp) != 1) { DEBUGF(Logger::DBG_SESSION, "Can't read the guide scope aperture from the XML file (%s)", ScopeConfigFileName.c_str()); delXMLEle(RootXmlNode); return false; } XmlNode = findXMLEle(CurrentXmlNode, ScopeConfigLabelApXmlNode.c_str()); if (!XmlNode) { DEBUGF(Logger::DBG_SESSION, "Can't read the telescope config name from the XML file (%s)", ScopeConfigFileName.c_str()); delXMLEle(RootXmlNode); return false; } ConfigName = pcdataXMLEle(XmlNode); // Store the loaded values if (IUFindNumber(&ScopeParametersNP, "TELESCOPE_FOCAL_LENGTH")) { IUFindNumber(&ScopeParametersNP, "TELESCOPE_FOCAL_LENGTH")->value = ScopeFoc; } if (IUFindNumber(&ScopeParametersNP, "TELESCOPE_APERTURE")) { IUFindNumber(&ScopeParametersNP, "TELESCOPE_APERTURE")->value = ScopeAp; } if (IUFindNumber(&ScopeParametersNP, "GUIDER_FOCAL_LENGTH")) { IUFindNumber(&ScopeParametersNP, "GUIDER_FOCAL_LENGTH")->value = GScopeFoc; } if (IUFindNumber(&ScopeParametersNP, "GUIDER_APERTURE")) { IUFindNumber(&ScopeParametersNP, "GUIDER_APERTURE")->value = GScopeAp; } if (IUFindText(&ScopeConfigNameTP, "SCOPE_CONFIG_NAME")) { IUSaveText(IUFindText(&ScopeConfigNameTP, "SCOPE_CONFIG_NAME"), ConfigName.c_str()); } ScopeParametersNP.s = IPS_OK; IDSetNumber(&ScopeParametersNP, nullptr); ScopeConfigNameTP.s = IPS_OK; IDSetText(&ScopeConfigNameTP, nullptr); delXMLEle(RootXmlNode); return true; } bool Telescope::HasDefaultScopeConfig() { if (!CheckFile(ScopeConfigFileName, false)) { return false; } LilXML *XmlHandle = newLilXML(); FILE *FilePtr = fopen(ScopeConfigFileName.c_str(), "r"); XMLEle *RootXmlNode = nullptr; XMLEle *CurrentXmlNode = nullptr; XMLAtt *Ap = nullptr; bool DeviceFound = false; char ErrMsg[512]; RootXmlNode = readXMLFile(FilePtr, XmlHandle, ErrMsg); delLilXML(XmlHandle); XmlHandle = nullptr; if (!RootXmlNode) { return false; } if (std::string(tagXMLEle(RootXmlNode)) != ScopeConfigRootXmlNode) { delXMLEle(RootXmlNode); return false; } CurrentXmlNode = nextXMLEle(RootXmlNode, 1); // Find the current telescope in the config file while (CurrentXmlNode) { if (std::string(tagXMLEle(CurrentXmlNode)) != ScopeConfigDeviceXmlNode) { CurrentXmlNode = nextXMLEle(RootXmlNode, 0); continue; } Ap = findXMLAtt(CurrentXmlNode, ScopeConfigNameXmlNode.c_str()); if (Ap && !strcmp(valuXMLAtt(Ap), getDeviceName())) { DeviceFound = true; break; } CurrentXmlNode = nextXMLEle(RootXmlNode, 0); } if (!DeviceFound) { delXMLEle(RootXmlNode); return false; } // Check the existence of Config #1 node CurrentXmlNode = findXMLEle(CurrentXmlNode, "config1"); if (!CurrentXmlNode) { delXMLEle(RootXmlNode); return false; } return true; } bool Telescope::UpdateScopeConfig() { // Get the config values from the UI const int ConfigIndex = GetScopeConfigIndex(); double ScopeFoc = 0, ScopeAp = 0; double GScopeFoc = 0, GScopeAp = 0; std::string ConfigName; if (IUFindNumber(&ScopeParametersNP, "TELESCOPE_FOCAL_LENGTH")) { ScopeFoc = IUFindNumber(&ScopeParametersNP, "TELESCOPE_FOCAL_LENGTH")->value; } if (IUFindNumber(&ScopeParametersNP, "TELESCOPE_APERTURE")) { ScopeAp = IUFindNumber(&ScopeParametersNP, "TELESCOPE_APERTURE")->value; } if (IUFindNumber(&ScopeParametersNP, "GUIDER_FOCAL_LENGTH")) { GScopeFoc = IUFindNumber(&ScopeParametersNP, "GUIDER_FOCAL_LENGTH")->value; } if (IUFindNumber(&ScopeParametersNP, "GUIDER_APERTURE")) { GScopeAp = IUFindNumber(&ScopeParametersNP, "GUIDER_APERTURE")->value; } if (IUFindText(&ScopeConfigNameTP, "SCOPE_CONFIG_NAME") && IUFindText(&ScopeConfigNameTP, "SCOPE_CONFIG_NAME")->text) { ConfigName = IUFindText(&ScopeConfigNameTP, "SCOPE_CONFIG_NAME")->text; } // Save the values to the actual XML file if (!CheckFile(ScopeConfigFileName, true)) { DEBUGF(Logger::DBG_SESSION, "Can't open XML file (%s) for write", ScopeConfigFileName.c_str()); return false; } // Open the existing XML file for write LilXML *XmlHandle = newLilXML(); FILE *FilePtr = fopen(ScopeConfigFileName.c_str(), "r"); XMLEle *RootXmlNode = nullptr; XMLAtt *Ap = nullptr; bool DeviceFound = false; char ErrMsg[512]; RootXmlNode = readXMLFile(FilePtr, XmlHandle, ErrMsg); delLilXML(XmlHandle); XmlHandle = nullptr; fclose(FilePtr); XMLEle *CurrentXmlNode = nullptr; XMLEle *XmlNode = nullptr; if (!RootXmlNode || std::string(tagXMLEle(RootXmlNode)) != ScopeConfigRootXmlNode) { RootXmlNode = addXMLEle(nullptr, ScopeConfigRootXmlNode.c_str()); } CurrentXmlNode = nextXMLEle(RootXmlNode, 1); // Find the current telescope in the config file while (CurrentXmlNode) { if (std::string(tagXMLEle(CurrentXmlNode)) != ScopeConfigDeviceXmlNode) { CurrentXmlNode = nextXMLEle(RootXmlNode, 0); continue; } Ap = findXMLAtt(CurrentXmlNode, ScopeConfigNameXmlNode.c_str()); if (Ap && !strcmp(valuXMLAtt(Ap), getDeviceName())) { DeviceFound = true; break; } CurrentXmlNode = nextXMLEle(RootXmlNode, 0); } if (!DeviceFound) { CurrentXmlNode = addXMLEle(RootXmlNode, ScopeConfigDeviceXmlNode.c_str()); addXMLAtt(CurrentXmlNode, ScopeConfigNameXmlNode.c_str(), getDeviceName()); } // Add or update the config node XmlNode = findXMLEle(CurrentXmlNode, ("config" + std::to_string(ConfigIndex)).c_str()); if (!XmlNode) { CurrentXmlNode = addXMLEle(CurrentXmlNode, ("config" + std::to_string(ConfigIndex)).c_str()); } else { CurrentXmlNode = XmlNode; } // Add or update the telescope focal length XmlNode = findXMLEle(CurrentXmlNode, ScopeConfigScopeFocXmlNode.c_str()); if (!XmlNode) { XmlNode = addXMLEle(CurrentXmlNode, ScopeConfigScopeFocXmlNode.c_str()); } editXMLEle(XmlNode, std::to_string(ScopeFoc).c_str()); // Add or update the telescope focal aperture XmlNode = findXMLEle(CurrentXmlNode, ScopeConfigScopeApXmlNode.c_str()); if (!XmlNode) { XmlNode = addXMLEle(CurrentXmlNode, ScopeConfigScopeApXmlNode.c_str()); } editXMLEle(XmlNode, std::to_string(ScopeAp).c_str()); // Add or update the guide scope focal length XmlNode = findXMLEle(CurrentXmlNode, ScopeConfigGScopeFocXmlNode.c_str()); if (!XmlNode) { XmlNode = addXMLEle(CurrentXmlNode, ScopeConfigGScopeFocXmlNode.c_str()); } editXMLEle(XmlNode, std::to_string(GScopeFoc).c_str()); // Add or update the guide scope focal aperture XmlNode = findXMLEle(CurrentXmlNode, ScopeConfigGScopeApXmlNode.c_str()); if (!XmlNode) { XmlNode = addXMLEle(CurrentXmlNode, ScopeConfigGScopeApXmlNode.c_str()); } editXMLEle(XmlNode, std::to_string(GScopeAp).c_str()); // Add or update the config name XmlNode = findXMLEle(CurrentXmlNode, ScopeConfigLabelApXmlNode.c_str()); if (!XmlNode) { XmlNode = addXMLEle(CurrentXmlNode, ScopeConfigLabelApXmlNode.c_str()); } editXMLEle(XmlNode, ConfigName.c_str()); // Save the final content FilePtr = fopen(ScopeConfigFileName.c_str(), "w"); prXMLEle(FilePtr, RootXmlNode, 0); fclose(FilePtr); delXMLEle(RootXmlNode); return true; } std::string Telescope::GetHomeDirectory() const { // Check first the HOME environmental variable const char *HomeDir = getenv("HOME"); // ...otherwise get the home directory of the current user. if (!HomeDir) { HomeDir = getpwuid(getuid())->pw_dir; } return (HomeDir ? std::string(HomeDir) : ""); } int Telescope::GetScopeConfigIndex() const { if (IUFindSwitch(&ScopeConfigsSP, "SCOPE_CONFIG1") && IUFindSwitch(&ScopeConfigsSP, "SCOPE_CONFIG1")->s == ISS_ON) { return 1; } if (IUFindSwitch(&ScopeConfigsSP, "SCOPE_CONFIG2") && IUFindSwitch(&ScopeConfigsSP, "SCOPE_CONFIG2")->s == ISS_ON) { return 2; } if (IUFindSwitch(&ScopeConfigsSP, "SCOPE_CONFIG3") && IUFindSwitch(&ScopeConfigsSP, "SCOPE_CONFIG3")->s == ISS_ON) { return 3; } if (IUFindSwitch(&ScopeConfigsSP, "SCOPE_CONFIG4") && IUFindSwitch(&ScopeConfigsSP, "SCOPE_CONFIG4")->s == ISS_ON) { return 4; } if (IUFindSwitch(&ScopeConfigsSP, "SCOPE_CONFIG5") && IUFindSwitch(&ScopeConfigsSP, "SCOPE_CONFIG5")->s == ISS_ON) { return 5; } if (IUFindSwitch(&ScopeConfigsSP, "SCOPE_CONFIG6") && IUFindSwitch(&ScopeConfigsSP, "SCOPE_CONFIG6")->s == ISS_ON) { return 6; } return 0; } bool Telescope::CheckFile(const std::string &file_name, bool writable) const { FILE *FilePtr = fopen(file_name.c_str(), (writable ? "a" : "r")); if (FilePtr) { fclose(FilePtr); return true; } return false; } void Telescope::sendTimeFromSystem() { char ts[32]={0}; std::time_t t = std::time(nullptr); struct std::tm *utctimeinfo = std::gmtime(&t); strftime(ts, sizeof(ts), "%Y-%m-%dT%H:%M:%S", utctimeinfo); IUSaveText(&TimeT[0], ts); struct std::tm *localtimeinfo = std::localtime(&t); snprintf(ts, sizeof(ts), "%4.2f", (localtimeinfo->tm_gmtoff / 3600.0)); IUSaveText(&TimeT[1], ts); TimeTP.s = IPS_OK; IDSetText(&TimeTP, nullptr); } } libindi/libs/indibase/hidapi.h0000664000175000017500000003173213263645557015616 0ustar jasemjasem/* HIDAPI - Multi-Platform library for communication with HID devices. Copyright (c) 2009 by Alan Ott, Signal 11 Software (8/22/2009) All Rights Reserved. Changes for use with SX Filter Wheel INDI Driver by CloudMakers - 11/6/2012 This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program 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 General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. The full GNU General Public License is included in this distribution in the file called LICENSE. These files may also be found in the public source code repository located: http://github.com/signal11/hidapi */ /** @file * @defgroup API hidapi API */ #pragma once #include #ifdef _WIN32 #define HID_API_EXPORT __declspec(dllexport) #define HID_API_CALL #else #define HID_API_EXPORT /**< API export macro */ #define HID_API_CALL /**< API call macro */ #endif #define HID_API_EXPORT_CALL HID_API_EXPORT HID_API_CALL /**< API export and call macro*/ #ifdef __cplusplus extern "C" { #endif struct hid_device_; typedef struct hid_device_ hid_device; /**< opaque hidapi structure */ /** hidapi info structure */ struct hid_device_info { /** Platform-specific device path */ char *path; /** Device Vendor ID */ unsigned short vendor_id; /** Device Product ID */ unsigned short product_id; /** Serial Number */ wchar_t *serial_number; /** Device Release Number in binary-coded decimal, also known as Device Version Number */ unsigned short release_number; /** Manufacturer String */ wchar_t *manufacturer_string; /** Product string */ wchar_t *product_string; /** Usage Page for this Device/Interface (Windows/Mac only). */ unsigned short usage_page; /** Usage for this Device/Interface (Windows/Mac only).*/ unsigned short usage; /** The USB interface which this logical device represents. Valid on both Linux implementations in all cases, and valid on the Windows implementation only if the device contains more than one interface. */ int interface_number; /** Pointer to the next device */ struct hid_device_info *next; }; /** @brief Initialize the HIDAPI library. This function initializes the HIDAPI library. Calling it is not strictly necessary, as it will be called automatically by hid_enumerate() and any of the hid_open_*() functions if it is needed. This function should be called at the beginning of execution however, if there is a chance of HIDAPI handles being opened by different threads simultaneously. @ingroup API @returns This function returns 0 on success and -1 on error. */ int HID_API_EXPORT HID_API_CALL hid_init(); /** @brief Finalize the HIDAPI library. This function frees all of the static data associated with HIDAPI. It should be called at the end of execution to avoid memory leaks. @ingroup API @returns This function returns 0 on success and -1 on error. */ int HID_API_EXPORT HID_API_CALL hid_exit(); /** @brief Enumerate the HID Devices. This function returns a linked list of all the HID devices attached to the system which match vendor_id and product_id. If @p vendor_id and @p product_id are both set to 0, then all HID devices will be returned. @ingroup API @param vendor_id The Vendor ID (VID) of the types of device to open. @param product_id The Product ID (PID) of the types of device to open. @returns This function returns a pointer to a linked list of type struct #hid_device, containing information about the HID devices attached to the system, or NULL in the case of failure. Free this linked list by calling hid_free_enumeration(). */ struct hid_device_info HID_API_EXPORT *HID_API_CALL hid_enumerate(unsigned short vendor_id, unsigned short product_id); /** @brief Free an enumeration Linked List This function frees a linked list created by hid_enumerate(). @ingroup API @param devs Pointer to a list of struct_device returned from hid_enumerate(). */ void HID_API_EXPORT HID_API_CALL hid_free_enumeration(struct hid_device_info *devs); /** @brief Open a HID device using a Vendor ID (VID), Product ID (PID) and optionally a serial number. If @p serial_number is NULL, the first device with the specified VID and PID is opened. @ingroup API @param vendor_id The Vendor ID (VID) of the device to open. @param product_id The Product ID (PID) of the device to open. @param serial_number The Serial Number of the device to open (Optionally NULL). @returns This function returns a pointer to a #hid_device object on success or NULL on failure. */ HID_API_EXPORT hid_device *HID_API_CALL hid_open(unsigned short vendor_id, unsigned short product_id, const wchar_t *serial_number); /** @brief Open a HID device by its path name. The path name be determined by calling hid_enumerate(), or a platform-specific path name can be used (eg: /dev/hidraw0 on Linux). @ingroup API @param path The path name of the device to open @returns This function returns a pointer to a #hid_device object on success or NULL on failure. */ HID_API_EXPORT hid_device *HID_API_CALL hid_open_path(const char *path); /** @brief Write an Output report to a HID device. The first byte of @p data[] must contain the Report ID. For devices which only support a single report, this must be set to 0x0. The remaining bytes contain the report data. Since the Report ID is mandatory, calls to hid_write() will always contain one more byte than the report contains. For example, if a hid report is 16 bytes long, 17 bytes must be passed to hid_write(), the Report ID (or 0x0, for devices with a single report), followed by the report data (16 bytes). In this example, the length passed in would be 17. hid_write() will send the data on the first OUT endpoint, if one exists. If it does not, it will send the data through the Control Endpoint (Endpoint 0). @ingroup API @param device A device handle returned from hid_open(). @param data The data to send, including the report number as the first byte. @param length The length in bytes of the data to send. @returns This function returns the actual number of bytes written and -1 on error. */ int HID_API_EXPORT HID_API_CALL hid_write(hid_device *device, const unsigned char *data, size_t length); /** @brief Read an Input report from a HID device with timeout. Input reports are returned to the host through the INTERRUPT IN endpoint. The first byte will contain the Report number if the device uses numbered reports. @ingroup API @param device A device handle returned from hid_open(). @param data A buffer to put the read data into. @param length The number of bytes to read. For devices with multiple reports, make sure to read an extra byte for the report number. @param milliseconds timeout in milliseconds or -1 for blocking wait. @returns This function returns the actual number of bytes read and -1 on error. */ int HID_API_EXPORT HID_API_CALL hid_read_timeout(hid_device *dev, unsigned char *data, size_t length, int milliseconds); /** @brief Read an Input report from a HID device. Input reports are returned to the host through the INTERRUPT IN endpoint. The first byte will contain the Report number if the device uses numbered reports. @ingroup API @param device A device handle returned from hid_open(). @param data A buffer to put the read data into. @param length The number of bytes to read. For devices with multiple reports, make sure to read an extra byte for the report number. @returns This function returns the actual number of bytes read and -1 on error. */ int HID_API_EXPORT HID_API_CALL hid_read(hid_device *device, unsigned char *data, size_t length); /** @brief Set the device handle to be non-blocking. In non-blocking mode calls to hid_read() will return immediately with a value of 0 if there is no data to be read. In blocking mode, hid_read() will wait (block) until there is data to read before returning. Nonblocking can be turned on and off at any time. @ingroup API @param device A device handle returned from hid_open(). @param nonblock enable or not the nonblocking reads - 1 to enable nonblocking - 0 to disable nonblocking. @returns This function returns 0 on success and -1 on error. */ int HID_API_EXPORT HID_API_CALL hid_set_nonblocking(hid_device *device, int nonblock); /** @brief Send a Feature report to the device. Feature reports are sent over the Control endpoint as a Set_Report transfer. The first byte of @p data[] must contain the Report ID. For devices which only support a single report, this must be set to 0x0. The remaining bytes contain the report data. Since the Report ID is mandatory, calls to hid_send_feature_report() will always contain one more byte than the report contains. For example, if a hid report is 16 bytes long, 17 bytes must be passed to hid_send_feature_report(): the Report ID (or 0x0, for devices which do not use numbered reports), followed by the report data (16 bytes). In this example, the length passed in would be 17. @ingroup API @param device A device handle returned from hid_open(). @param data The data to send, including the report number as the first byte. @param length The length in bytes of the data to send, including the report number. @returns This function returns the actual number of bytes written and -1 on error. */ int HID_API_EXPORT HID_API_CALL hid_send_feature_report(hid_device *device, const unsigned char *data, size_t length); /** @brief Get a feature report from a HID device. Make sure to set the first byte of @p data[] to the Report ID of the report to be read. Make sure to allow space for this extra byte in @p data[]. @ingroup API @param device A device handle returned from hid_open(). @param data A buffer to put the read data into, including the Report ID. Set the first byte of @p data[] to the Report ID of the report to be read. @param length The number of bytes to read, including an extra byte for the report ID. The buffer can be longer than the actual report. @returns This function returns the number of bytes read and -1 on error. */ int HID_API_EXPORT HID_API_CALL hid_get_feature_report(hid_device *device, unsigned char *data, size_t length); /** @brief Close a HID device. @ingroup API @param device A device handle returned from hid_open(). */ void HID_API_EXPORT HID_API_CALL hid_close(hid_device *device); /** @brief Get The Manufacturer String from a HID device. @ingroup API @param device A device handle returned from hid_open(). @param string A wide string buffer to put the data into. @param maxlen The length of the buffer in multiples of wchar_t. @returns This function returns 0 on success and -1 on error. */ int HID_API_EXPORT_CALL hid_get_manufacturer_string(hid_device *device, wchar_t *string, size_t maxlen); /** @brief Get The Product String from a HID device. @ingroup API @param device A device handle returned from hid_open(). @param string A wide string buffer to put the data into. @param maxlen The length of the buffer in multiples of wchar_t. @returns This function returns 0 on success and -1 on error. */ int HID_API_EXPORT_CALL hid_get_product_string(hid_device *device, wchar_t *string, size_t maxlen); /** @brief Get The Serial Number String from a HID device. @ingroup API @param device A device handle returned from hid_open(). @param string A wide string buffer to put the data into. @param maxlen The length of the buffer in multiples of wchar_t. @returns This function returns 0 on success and -1 on error. */ int HID_API_EXPORT_CALL hid_get_serial_number_string(hid_device *device, wchar_t *string, size_t maxlen); /** @brief Get a string from a HID device, based on its string index. @ingroup API @param device A device handle returned from hid_open(). @param string_index The index of the string to get. @param string A wide string buffer to put the data into. @param maxlen The length of the buffer in multiples of wchar_t. @returns This function returns 0 on success and -1 on error. */ int HID_API_EXPORT_CALL hid_get_indexed_string(hid_device *device, int string_index, wchar_t *string, size_t maxlen); /** @brief Get a string describing the last error which occurred. @ingroup API @param device A device handle returned from hid_open(). @returns This function returns a string containing the last error which occurred or NULL if none has occurred. */ HID_API_EXPORT const wchar_t *HID_API_CALL hid_error(hid_device *device); #ifdef __cplusplus } #endif libindi/libs/indibase/alignment/0000775000175000017500000000000013263645557016157 5ustar jasemjasemlibindi/libs/indibase/alignment/BasicMathPlugin.h0000664000175000017500000001171713263645557021351 0ustar jasemjasem/// \file BasicMathPlugin.h /// /// \author Roger James /// \date 13th November 2013 /// /// This file provides the common functionality for the built in /// and SVD math plugins #pragma once #include "AlignmentSubsystemForMathPlugins.h" #include "ConvexHull.h" #include namespace INDI { namespace AlignmentSubsystem { /// \class BasicMathPlugin /// \brief This class implements the common functionality for the built in /// and SVD math plugins class BasicMathPlugin : public AlignmentSubsystemForMathPlugins { public: /// \brief Default constructor BasicMathPlugin(); /// \brief Virtual destructor virtual ~BasicMathPlugin(); /// \brief Override for the base class virtual function virtual bool Initialise(InMemoryDatabase *pInMemoryDatabase); /// \brief Override for the base class virtual function virtual bool TransformCelestialToTelescope(const double RightAscension, const double Declination, double JulianOffset, TelescopeDirectionVector &ApparentTelescopeDirectionVector); /// \brief Override for the base class virtual function virtual bool TransformTelescopeToCelestial(const TelescopeDirectionVector &ApparentTelescopeDirectionVector, double &RightAscension, double &Declination); protected: /// \brief Calculate tranformation matrices from the supplied vectors /// \param[in] Alpha1 Pointer to the first coordinate in the alpha reference frame /// \param[in] Alpha2 Pointer to the second coordinate in the alpha reference frame /// \param[in] Alpha3 Pointer to the third coordinate in the alpha reference frame /// \param[in] Beta1 Pointer to the first coordinate in the beta reference frame /// \param[in] Beta2 Pointer to the second coordinate in the beta reference frame /// \param[in] Beta3 Pointer to the third coordinate in the beta reference frame /// \param[in] pAlphaToBeta Pointer to a matrix to receive the Alpha to Beta transformation matrix /// \param[in] pBetaToAlpha Pointer to a matrix to receive the Beta to Alpha transformation matrix virtual void CalculateTransformMatrices(const TelescopeDirectionVector &Alpha1, const TelescopeDirectionVector &Alpha2, const TelescopeDirectionVector &Alpha3, const TelescopeDirectionVector &Beta1, const TelescopeDirectionVector &Beta2, const TelescopeDirectionVector &Beta3, gsl_matrix *pAlphaToBeta, gsl_matrix *pBetaToAlpha) = 0; /// \brief Print out a 3 vector to debug /// \param[in] Label A label to identify the vector /// \param[in] pVector The vector to print void Dump3(const char *Label, gsl_vector *pVector); /// \brief Print out a 3x3 matrix to debug /// \param[in] Label A label to identify the matrix /// \param[in] pMatrix The matrix to print void Dump3x3(const char *Label, gsl_matrix *pMatrix); /// \brief Caluclate the determinant of the supplied matrix /// \param[in] pMatrix Pointer to the 3x3 matrix /// \return The determinant double Matrix3x3Determinant(gsl_matrix *pMatrix); /// \brief Calculate the inverse of the supplied matrix /// \param[in] pInput Pointer to the input matrix /// \param[in] pInversion Pointer to a matrix to receive the inversion /// \return False if input matrix is singular (not invertable) otherwise true bool MatrixInvert3x3(gsl_matrix *pInput, gsl_matrix *pInversion); /// \brief Multiply matrix A by matrix B and put the result in C void MatrixMatrixMultiply(gsl_matrix *pA, gsl_matrix *pB, gsl_matrix *pC); /// \brief Multiply matrix A by vector B and put the result in vector C void MatrixVectorMultiply(gsl_matrix *pA, gsl_vector *pB, gsl_vector *pC); /// \brief Test if a ray intersects a triangle in 3d space /// \param[in] Ray The ray vector /// \param[in] TriangleVertex1 The first vertex of the triangle /// \param[in] TriangleVertex2 The second vertex of the triangle /// \param[in] TriangleVertex3 The third vertex of the triangle /// \note The order of the vertices determine whether the triangle is facing away from or towards the origin. /// Intersection with triangles facing the origin will be ignored. bool RayTriangleIntersection(TelescopeDirectionVector &Ray, TelescopeDirectionVector &TriangleVertex1, TelescopeDirectionVector &TriangleVertex2, TelescopeDirectionVector &TriangleVertex3); // Transformation matrixes for 1, 2 and 2 sync points case gsl_matrix *pActualToApparentTransform; gsl_matrix *pApparentToActualTransform; // Convex hulls for 4+ sync points case ConvexHull ActualConvexHull; ConvexHull ApparentConvexHull; // Actual direction cosines for the 4+ case std::vector ActualDirectionCosines; }; } // namespace AlignmentSubsystem } // namespace INDI libindi/libs/indibase/alignment/CMakeLists.txt0000664000175000017500000001750613263645557020730 0ustar jasemjasemset(INDI_MATH_PLUGINS_DIRECTORY "${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_LIBDIR}/indi/MathPlugins") set_property(DIRECTORY APPEND PROPERTY COMPILE_DEFINITIONS INDI_MATH_PLUGINS_DIRECTORY="${INDI_MATH_PLUGINS_DIRECTORY}") option(ALIGNMENT_CONVEX_HULL_DEBUGGING "Alignment subsystem - additional debugging output" OFF) if(ALIGNMENT_CONVEX_HULL_DEBUGGING) set_property(DIRECTORY APPEND PROPERTY COMPILE_DEFINITIONS CONVEX_HULL_DEBUGGING) endif(ALIGNMENT_CONVEX_HULL_DEBUGGING) option(SKYWATCHER_API_USE_INITIAL_JULIAN_DATE "Skywatcher API - use initial Julian date only" OFF) if(SKYWATCHER_API_USE_INITIAL_JULIAN_DATE) set_property(DIRECTORY APPEND PROPERTY COMPILE_DEFINITIONS USE_INITIAL_JULIAN_DATE) endif(SKYWATCHER_API_USE_INITIAL_JULIAN_DATE) ################################################## ####### INDI AlignmentDriver shared library ###### ################################################## SET(AlignmentDriver_SRC ${CMAKE_SOURCE_DIR}/libs/indibase/alignment/AlignmentSubsystemForDrivers.cpp ; ${CMAKE_SOURCE_DIR}/libs/indibase/alignment/AlignmentSubsystemForMathPlugins.h ; ${CMAKE_SOURCE_DIR}/libs/indibase/alignment/BasicMathPlugin.cpp ; ${CMAKE_SOURCE_DIR}/libs/indibase/alignment/BuiltInMathPlugin.cpp ; ${CMAKE_SOURCE_DIR}/libs/indibase/alignment/ConvexHull.cpp ; ${CMAKE_SOURCE_DIR}/libs/indibase/alignment/DriverCommon.cpp ; ${CMAKE_SOURCE_DIR}/libs/indibase/alignment/InMemoryDatabase.cpp ; ${CMAKE_SOURCE_DIR}/libs/indibase/alignment/MapPropertiesToInMemoryDatabase.cpp ; ${CMAKE_SOURCE_DIR}/libs/indibase/alignment/MathPlugin.cpp ; ${CMAKE_SOURCE_DIR}/libs/indibase/alignment/MathPluginManagement.cpp ; ${CMAKE_SOURCE_DIR}/libs/indibase/alignment/TelescopeDirectionVectorSupportFunctions.cpp ; ${CMAKE_SOURCE_DIR}/libs/indibase/alignment/Common.cpp) IF (UNITY_BUILD) ENABLE_UNITY_BUILD(AlignmentDriver AlignmentDriver_SRC 15 cpp) ENDIF () IF (CYGWIN) SET(AlignmentDriver_SRC ${AlignmentDriver_SRC} ${CMAKE_SOURCE_DIR}/libs/lilxml.c ${CMAKE_SOURCE_DIR}/libs/indicom.c) add_library(AlignmentDriver STATIC ${AlignmentDriver_SRC}) target_link_libraries(AlignmentDriver dl ${GSL_LIBRARIES} ${NOVA_LIBRARIES}) ELSE () add_library(AlignmentDriver SHARED ${AlignmentDriver_SRC}) set_target_properties(AlignmentDriver PROPERTIES COMPILE_FLAGS "-fPIC") IF (APPLE) target_link_libraries(AlignmentDriver dl -L/usr/local/lib ${GSL_LIBRARIES}) ELSE () # Force linking all referenced libraries because of libgsl is not linked against cblas library on Linux SET(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -Wl,--no-as-needed") target_link_libraries(AlignmentDriver dl ${GSL_LIBRARIES}) ENDIF () ENDIF () if (INDI_BUILD_QT5) qt5_use_modules(AlignmentDriver Network) endif(INDI_BUILD_QT5) set_target_properties(AlignmentDriver PROPERTIES VERSION ${CMAKE_INDI_VERSION_STRING} SOVERSION ${INDI_SOVERSION} OUTPUT_NAME indiAlignmentDriver) install(TARGETS AlignmentDriver ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR} LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}) install(FILES AlignmentSubsystemForMathPlugins.h AlignmentSubsystemForDrivers.h BasicMathPlugin.h BuiltInMathPlugin.h ClientAPIForAlignmentDatabase.h ClientAPIForMathPluginManagement.h Common.h ConvexHull.h DriverCommon.h InMemoryDatabase.h MathPlugin.h MathPluginManagement.h SVDMathPlugin.h TelescopeDirectionVectorSupportFunctions.h MapPropertiesToInMemoryDatabase.h DESTINATION ${INCLUDE_INSTALL_DIR}/libindi/alignment COMPONENT Devel) ################################################## ####### INDI AlignmentClient static library ###### ################################################## set (AlignmentClient_SRCS ${CMAKE_SOURCE_DIR}/libs/indibase/alignment/AlignmentSubsystemForClients.cpp ${CMAKE_SOURCE_DIR}/libs/indibase/alignment/ClientAPIForAlignmentDatabase.cpp ${CMAKE_SOURCE_DIR}/libs/indibase/alignment/ClientAPIForMathPluginManagement.cpp ${CMAKE_SOURCE_DIR}/libs/indibase/alignment/Common.h ) add_library(AlignmentClient STATIC ${AlignmentClient_SRCS}) if (INDI_BUILD_QT5) qt5_use_modules(AlignmentClient Network) endif(INDI_BUILD_QT5) if (NOT CYGWIN AND NOT WIN32) SET_TARGET_PROPERTIES(AlignmentClient PROPERTIES COMPILE_FLAGS "-fPIC") endif(NOT CYGWIN AND NOT WIN32) set_target_properties(AlignmentClient PROPERTIES OUTPUT_NAME indiAlignmentClient) install(TARGETS AlignmentClient ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}) ################################################## ############ LoaderCLient test program ########### ################################################## #set(LoaderClient_SRCS # ${CMAKE_SOURCE_DIR}/libs/indibase/alignment/LoaderClient.cpp # ${CMAKE_SOURCE_DIR}/libs/indibase/alignment/LoaderMain.cpp # ) #add_executable(LoaderClient ${LoaderClient_SRCS}) #target_link_libraries(LoaderClient indiclient AlignmentClient) #install(TARGETS LoaderClient RUNTIME DESTINATION bin) ################################################## ######### MathPluginManager test program ######### ################################################## #set(MathPluginManagerClient_SRCS # ${CMAKE_SOURCE_DIR}/libs/indibase/alignment/MathPluginManagerClient.cpp # ${CMAKE_SOURCE_DIR}/libs/indibase/alignment/MathPluginManagerMain.cpp # ) #add_executable(MathPluginManagerClient ${MathPluginManagerClient_SRCS}) #target_link_libraries(MathPluginManagerClient indiclient AlignmentClient) #install(TARGETS MathPluginManagerClient RUNTIME DESTINATION bin) ################################################## ########### Dummy math plugin example ############ ################################################## #set(DummyMathPlugin_SRCS # ${CMAKE_SOURCE_DIR}/libs/indibase/alignment/DummyMathPlugin.cpp # ) #add_library(indi_Dummy_MathPlugin SHARED ${DummyMathPlugin_SRCS}) #target_link_libraries(indi_Dummy_MathPlugin indidriver) #install(TARGETS indi_Dummy_MathPlugin LIBRARY DESTINATION ${INDI_MATH_PLUGINS_DIRECTORY}) ################################################## ################ SVD math plugin ################# ################################################## set(SVDMathPlugin_SRCS ${CMAKE_SOURCE_DIR}/libs/indibase/alignment/SVDMathPlugin.cpp ) if (CYGWIN) add_library(indi_SVD_MathPlugin STATIC ${SVDMathPlugin_SRCS}) target_link_libraries(indi_SVD_MathPlugin ${GSL_LIBRARIES} ${NOVA_LIBRARIES}) else(CYGWIN) add_library(indi_SVD_MathPlugin SHARED ${SVDMathPlugin_SRCS}) set_property(TARGET indi_SVD_MathPlugin APPEND PROPERTY COMPILE_DEFINITIONS SVD_TRANSFORM_MATRIX) target_link_libraries(indi_SVD_MathPlugin indidriver) endif(CYGWIN) install(TARGETS indi_SVD_MathPlugin ARCHIVE DESTINATION ${INDI_MATH_PLUGINS_DIRECTORY} LIBRARY DESTINATION ${INDI_MATH_PLUGINS_DIRECTORY} RUNTIME DESTINATION ${INDI_MATH_PLUGINS_DIRECTORY}) ################################################## ########### Skywatcher Alt-Az Mount ############## ################################################## set(skywatcherAltAzMount_SRCS ${CMAKE_SOURCE_DIR}/drivers/telescope/skywatcherAPIMount.cpp ${CMAKE_SOURCE_DIR}/drivers/telescope/skywatcherAPI.cpp ) add_executable(indi_skywatcherAltAzMount ${skywatcherAltAzMount_SRCS}) target_link_libraries(indi_skywatcherAltAzMount indidriver AlignmentDriver) install(TARGETS indi_skywatcherAltAzMount RUNTIME DESTINATION bin) ######################################################### ########### Skywatcher Alt-Az Simple Mount ############## ######################################################### set(skywatcherAltAzSimple_SRCS ${CMAKE_SOURCE_DIR}/drivers/telescope/skywatcherAltAzSimple.cpp ${CMAKE_SOURCE_DIR}/drivers/telescope/skywatcherAPI.cpp ) add_executable(indi_skywatcherAltAzSimple ${skywatcherAltAzSimple_SRCS}) target_link_libraries(indi_skywatcherAltAzSimple indidriver) install(TARGETS indi_skywatcherAltAzSimple RUNTIME DESTINATION bin) libindi/libs/indibase/alignment/AlignmentSubsystemForClients.h0000664000175000017500000000572013263645557024162 0ustar jasemjasem/*! * \file AlignmentSubsystemForClients.h * * \author Roger James * \date 13th November 2013 * * This file provides a shorthand way for clients to include all the * functionality they need to use the INDI Alignment Subsystem * Clients should inherit this class alongside INDI::BaseClient */ #pragma once #include "ClientAPIForAlignmentDatabase.h" #include "ClientAPIForMathPluginManagement.h" #include "TelescopeDirectionVectorSupportFunctions.h" #include "basedevice.h" namespace INDI { namespace AlignmentSubsystem { /*! * \class AlignmentSubsystemForClients * \brief This class encapsulates all the alignment subsystem classes that are useful to client implementations. * Clients should inherit from this class. */ class AlignmentSubsystemForClients : public ClientAPIForMathPluginManagement, public ClientAPIForAlignmentDatabase, public TelescopeDirectionVectorSupportFunctions { public: /// \brief Virtual destructor virtual ~AlignmentSubsystemForClients() {} /** \brief This routine should be called before any connections to devices are made. \param[in] DeviceName The device name of INDI driver instance to be used. \param[in] BaseClient A pointer to the child INDI::BaseClient class */ void Initialise(const char *DeviceName, INDI::BaseClient *BaseClient); /** \brief Process new BLOB message from driver. This routine should be called from within the newBLOB handler in the client. \param[in] BLOBPointer A pointer to the INDI::IBLOB. */ void ProcessNewBLOB(IBLOB *BLOBPointer); /** \brief Process new device message from driver. This routine should be called from within the newDevice handler in the client. \param[in] DevicePointer A pointer to the INDI::BaseDevice object. */ void ProcessNewDevice(INDI::BaseDevice *DevicePointer); /** \brief Process new property message from driver. This routine should be called from within the newProperty handler in the client. \param[in] PropertyPointer A pointer to the INDI::Property object. */ void ProcessNewProperty(INDI::Property *PropertyPointer); /** \brief Process new number message from driver. This routine should be called from within the newNumber handler in the client. \param[in] NumberVectorPropertyPointer A pointer to the INDI::INumberVectorProperty. */ void ProcessNewNumber(INumberVectorProperty *NumberVectorPropertyPointer); /** \brief Process new switch message from driver. This routine should be called from within the newSwitch handler in the client. \param[in] SwitchVectorPropertyPointer A pointer to the INDI::ISwitchVectorProperty. */ void ProcessNewSwitch(ISwitchVectorProperty *SwitchVectorPropertyPointer); private: std::string DeviceName; }; } // namespace AlignmentSubsystem } // namespace INDI libindi/libs/indibase/alignment/CMakeModules/0000775000175000017500000000000013263645557020470 5ustar jasemjasemlibindi/libs/indibase/alignment/CMakeModules/FindGSL.cmake0000664000175000017500000001044013263645557022717 0ustar jasemjasem# Try to find gnu scientific library GSL # See # http://www.gnu.org/software/gsl/ and # http://gnuwin32.sourceforge.net/packages/gsl.htm # # Based on a script of Felix Woelk and Jan Woetzel # (www.mip.informatik.uni-kiel.de) # # It defines the following variables: # GSL_FOUND - system has GSL lib # GSL_INCLUDE_DIRS - where to find headers # GSL_LIBRARIES - full path to the libraries # GSL_LIBRARY_DIRS, the directory where the PLplot library is found. # CMAKE_GSL_CXX_FLAGS = Unix compiler flags for GSL, essentially "`gsl-config --cxxflags`" # GSL_LINK_DIRECTORIES = link directories, useful for rpath on Unix # GSL_EXE_LINKER_FLAGS = rpath on Unix INCLUDE(FindPkgConfig) PKG_CHECK_MODULES(GSL "gsl >= 1.10") IF(NOT GSL_FOUND) set( GSL_FOUND OFF ) set( GSL_CBLAS_FOUND OFF ) # Windows, but not for Cygwin and MSys where gsl-config is available if( WIN32 AND NOT CYGWIN AND NOT MSYS ) # look for headers find_path( GSL_INCLUDE_DIR NAMES gsl/gsl_cdf.h gsl/gsl_randist.h ) if( GSL_INCLUDE_DIR ) # look for gsl library find_library( GSL_LIBRARY NAMES gsl ) if( GSL_LIBRARY ) set( GSL_INCLUDE_DIRS ${GSL_INCLUDE_DIR} ) get_filename_component( GSL_LIBRARY_DIRS ${GSL_LIBRARY} PATH ) set( GSL_FOUND ON ) endif( GSL_LIBRARY ) # look for gsl cblas library find_library( GSL_CBLAS_LIBRARY NAMES gslcblas ) if( GSL_CBLAS_LIBRARY ) set( GSL_CBLAS_FOUND ON ) endif( GSL_CBLAS_LIBRARY ) set( GSL_LIBRARIES ${GSL_LIBRARY} ${GSL_CBLAS_LIBRARY} ) endif( GSL_INCLUDE_DIR ) #mark_as_advanced( # GSL_INCLUDE_DIR # GSL_LIBRARY # GSL_CBLAS_LIBRARY #) else( WIN32 AND NOT CYGWIN AND NOT MSYS ) if( UNIX OR MSYS ) find_program( GSL_CONFIG_EXECUTABLE gsl-config /usr/bin/ /usr/local/bin ) if( GSL_CONFIG_EXECUTABLE ) set( GSL_FOUND ON ) # run the gsl-config program to get cxxflags execute_process( COMMAND sh "${GSL_CONFIG_EXECUTABLE}" --cflags OUTPUT_VARIABLE GSL_CFLAGS RESULT_VARIABLE RET ERROR_QUIET ) if( RET EQUAL 0 ) string( STRIP "${GSL_CFLAGS}" GSL_CFLAGS ) separate_arguments( GSL_CFLAGS ) # parse definitions from cflags; drop -D* from CFLAGS string( REGEX MATCHALL "-D[^;]+" GSL_DEFINITIONS "${GSL_CFLAGS}" ) string( REGEX REPLACE "-D[^;]+;" "" GSL_CFLAGS "${GSL_CFLAGS}" ) # parse include dirs from cflags; drop -I prefix string( REGEX MATCHALL "-I[^;]+" GSL_INCLUDE_DIRS "${GSL_CFLAGS}" ) string( REPLACE "-I" "" GSL_INCLUDE_DIRS "${GSL_INCLUDE_DIRS}") string( REGEX REPLACE "-I[^;]+;" "" GSL_CFLAGS "${GSL_CFLAGS}") message("GSL_DEFINITIONS=${GSL_DEFINITIONS}") message("GSL_INCLUDE_DIRS=${GSL_INCLUDE_DIRS}") message("GSL_CFLAGS=${GSL_CFLAGS}") else( RET EQUAL 0 ) set( GSL_FOUND FALSE ) endif( RET EQUAL 0 ) # run the gsl-config program to get the libs execute_process( COMMAND sh "${GSL_CONFIG_EXECUTABLE}" --libs OUTPUT_VARIABLE GSL_LIBRARIES RESULT_VARIABLE RET ERROR_QUIET ) if( RET EQUAL 0 ) string(STRIP "${GSL_LIBRARIES}" GSL_LIBRARIES ) separate_arguments( GSL_LIBRARIES ) # extract linkdirs (-L) for rpath (i.e., LINK_DIRECTORIES) string( REGEX MATCHALL "-L[^;]+" GSL_LIBRARY_DIRS "${GSL_LIBRARIES}" ) string( REPLACE "-L" "" GSL_LIBRARY_DIRS "${GSL_LIBRARY_DIRS}" ) else( RET EQUAL 0 ) set( GSL_FOUND FALSE ) endif( RET EQUAL 0 ) MARK_AS_ADVANCED( GSL_CFLAGS ) message( STATUS "Using GSL from ${GSL_PREFIX}" ) else( GSL_CONFIG_EXECUTABLE ) message( STATUS "FindGSL: gsl-config not found.") endif( GSL_CONFIG_EXECUTABLE ) endif( UNIX OR MSYS ) endif( WIN32 AND NOT CYGWIN AND NOT MSYS ) if( GSL_FOUND ) if( NOT GSL_FIND_QUIETLY ) message( STATUS "FindGSL: Found both GSL headers and library" ) endif( NOT GSL_FIND_QUIETLY ) else( GSL_FOUND ) if( GSL_FIND_REQUIRED ) message( FATAL_ERROR "FindGSL: Could not find GSL headers or library" ) endif( GSL_FIND_REQUIRED ) endif( GSL_FOUND ) INCLUDE(FindPackageHandleStandardArgs) FIND_PACKAGE_HANDLE_STANDARD_ARGS(GSL DEFAULT_MSG GSL_LIBRARIES GSL_INCLUDE_DIRS) ENDIF(NOT GSL_FOUND) libindi/libs/indibase/alignment/BasicMathPlugin.cpp0000664000175000017500000012422513263645557021703 0ustar jasemjasem/// \file BasicMathPlugin.cpp /// \author Roger James /// \date 13th November 2013 #include "BasicMathPlugin.h" #include "DriverCommon.h" #include #include #include #include #include #include #include namespace INDI { namespace AlignmentSubsystem { BasicMathPlugin::BasicMathPlugin() { pActualToApparentTransform = gsl_matrix_alloc(3, 3); pApparentToActualTransform = gsl_matrix_alloc(3, 3); } // Destructor BasicMathPlugin::~BasicMathPlugin() { gsl_matrix_free(pActualToApparentTransform); gsl_matrix_free(pApparentToActualTransform); } // Public methods bool BasicMathPlugin::Initialise(InMemoryDatabase *pInMemoryDatabase) { MathPlugin::Initialise(pInMemoryDatabase); InMemoryDatabase::AlignmentDatabaseType &SyncPoints = pInMemoryDatabase->GetAlignmentDatabase(); /// See how many entries there are in the in memory database. /// - If just one use a hint to mounts approximate alignment, this can either be ZENITH, /// NORTH_CELESTIAL_POLE or SOUTH_CELESTIAL_POLE. The hint is used to make a dummy second /// entry. A dummy third entry is computed from the cross product of the first two. A transform /// matrix is then computed. /// - If two make the dummy third entry and compute a transform matrix. /// - If three compute a transform matrix. /// - If four or more compute a convex hull, then matrices for each /// triangular facet of the hull. switch (SyncPoints.size()) { case 0: // Not sure whether to return false or true here return true; case 1: { AlignmentDatabaseEntry &Entry1 = SyncPoints[0]; ln_equ_posn RaDec; ln_hrz_posn ActualSyncPoint1; ln_lnlat_posn Position; if (!pInMemoryDatabase->GetDatabaseReferencePosition(Position)) return false; RaDec.dec = Entry1.Declination; // libnova works in decimal degrees so conversion is needed here RaDec.ra = Entry1.RightAscension * 360.0 / 24.0; ln_get_hrz_from_equ(&RaDec, &Position, Entry1.ObservationJulianDate, &ActualSyncPoint1); // Now express this coordinate as a normalised direction vector (a.k.a direction cosines) TelescopeDirectionVector ActualDirectionCosine1 = TelescopeDirectionVectorFromAltitudeAzimuth(ActualSyncPoint1); TelescopeDirectionVector DummyActualDirectionCosine2; TelescopeDirectionVector DummyApparentDirectionCosine2; TelescopeDirectionVector DummyActualDirectionCosine3; TelescopeDirectionVector DummyApparentDirectionCosine3; switch (ApproximateMountAlignment) { case ZENITH: DummyActualDirectionCosine2.x = 0.0; DummyActualDirectionCosine2.y = 0.0; DummyActualDirectionCosine2.z = 1.0; DummyApparentDirectionCosine2 = DummyActualDirectionCosine2; break; case NORTH_CELESTIAL_POLE: { ln_equ_posn DummyRaDec; ln_hrz_posn DummyAltAz; DummyRaDec.ra = 0.0; DummyRaDec.dec = 90.0; ln_get_hrz_from_equ(&DummyRaDec, &Position, ln_get_julian_from_sys(), &DummyAltAz); DummyActualDirectionCosine2 = TelescopeDirectionVectorFromAltitudeAzimuth(DummyAltAz); DummyApparentDirectionCosine2 = DummyActualDirectionCosine2; break; } case SOUTH_CELESTIAL_POLE: { ln_equ_posn DummyRaDec; ln_hrz_posn DummyAltAz; DummyRaDec.ra = 0.0; DummyRaDec.dec = -90.0; ln_get_hrz_from_equ(&DummyRaDec, &Position, ln_get_julian_from_sys(), &DummyAltAz); DummyActualDirectionCosine2 = TelescopeDirectionVectorFromAltitudeAzimuth(DummyAltAz); DummyApparentDirectionCosine2 = DummyActualDirectionCosine2; break; } } DummyActualDirectionCosine3 = ActualDirectionCosine1 * DummyActualDirectionCosine2; DummyActualDirectionCosine3.Normalise(); DummyApparentDirectionCosine3 = Entry1.TelescopeDirection * DummyApparentDirectionCosine2; DummyApparentDirectionCosine3.Normalise(); CalculateTransformMatrices(ActualDirectionCosine1, DummyActualDirectionCosine2, DummyActualDirectionCosine3, Entry1.TelescopeDirection, DummyApparentDirectionCosine2, DummyApparentDirectionCosine3, pActualToApparentTransform, pApparentToActualTransform); return true; } case 2: { // First compute local horizontal coordinates for the two sync points AlignmentDatabaseEntry &Entry1 = SyncPoints[0]; AlignmentDatabaseEntry &Entry2 = SyncPoints[1]; ln_hrz_posn ActualSyncPoint1; ln_hrz_posn ActualSyncPoint2; ln_equ_posn RaDec1; ln_equ_posn RaDec2; RaDec1.dec = Entry1.Declination; // libnova works in decimal degrees so conversion is needed here RaDec1.ra = Entry1.RightAscension * 360.0 / 24.0; RaDec2.dec = Entry2.Declination; // libnova works in decimal degrees so conversion is needed here RaDec2.ra = Entry2.RightAscension * 360.0 / 24.0; ln_lnlat_posn Position { 0, 0 }; if (!pInMemoryDatabase->GetDatabaseReferencePosition(Position)) return false; ln_get_hrz_from_equ(&RaDec1, &Position, Entry1.ObservationJulianDate, &ActualSyncPoint1); ln_get_hrz_from_equ(&RaDec2, &Position, Entry2.ObservationJulianDate, &ActualSyncPoint2); // Now express these coordinates as normalised direction vectors (a.k.a direction cosines) TelescopeDirectionVector ActualDirectionCosine1 = TelescopeDirectionVectorFromAltitudeAzimuth(ActualSyncPoint1); TelescopeDirectionVector ActualDirectionCosine2 = TelescopeDirectionVectorFromAltitudeAzimuth(ActualSyncPoint2); TelescopeDirectionVector DummyActualDirectionCosine3; TelescopeDirectionVector DummyApparentDirectionCosine3; DummyActualDirectionCosine3 = ActualDirectionCosine1 * ActualDirectionCosine2; DummyActualDirectionCosine3.Normalise(); DummyApparentDirectionCosine3 = Entry1.TelescopeDirection * Entry2.TelescopeDirection; DummyApparentDirectionCosine3.Normalise(); // The third direction vectors is generated by taking the cross product of the first two CalculateTransformMatrices(ActualDirectionCosine1, ActualDirectionCosine2, DummyActualDirectionCosine3, Entry1.TelescopeDirection, Entry2.TelescopeDirection, DummyApparentDirectionCosine3, pActualToApparentTransform, pApparentToActualTransform); return true; } case 3: { // First compute local horizontal coordinates for the three sync points AlignmentDatabaseEntry &Entry1 = SyncPoints[0]; AlignmentDatabaseEntry &Entry2 = SyncPoints[1]; AlignmentDatabaseEntry &Entry3 = SyncPoints[2]; ln_hrz_posn ActualSyncPoint1; ln_hrz_posn ActualSyncPoint2; ln_hrz_posn ActualSyncPoint3; ln_equ_posn RaDec1; ln_equ_posn RaDec2; ln_equ_posn RaDec3; RaDec1.dec = Entry1.Declination; // libnova works in decimal degrees so conversion is needed here RaDec1.ra = Entry1.RightAscension * 360.0 / 24.0; RaDec2.dec = Entry2.Declination; // libnova works in decimal degrees so conversion is needed here RaDec2.ra = Entry2.RightAscension * 360.0 / 24.0; RaDec3.dec = Entry3.Declination; // libnova works in decimal degrees so conversion is needed here RaDec3.ra = Entry3.RightAscension * 360.0 / 24.0; ln_lnlat_posn Position { 0, 0 }; if (!pInMemoryDatabase->GetDatabaseReferencePosition(Position)) return false; ln_get_hrz_from_equ(&RaDec1, &Position, Entry1.ObservationJulianDate, &ActualSyncPoint1); ln_get_hrz_from_equ(&RaDec2, &Position, Entry2.ObservationJulianDate, &ActualSyncPoint2); ln_get_hrz_from_equ(&RaDec3, &Position, Entry3.ObservationJulianDate, &ActualSyncPoint3); // Now express these coordinates as normalised direction vectors (a.k.a direction cosines) TelescopeDirectionVector ActualDirectionCosine1 = TelescopeDirectionVectorFromAltitudeAzimuth(ActualSyncPoint1); TelescopeDirectionVector ActualDirectionCosine2 = TelescopeDirectionVectorFromAltitudeAzimuth(ActualSyncPoint2); TelescopeDirectionVector ActualDirectionCosine3 = TelescopeDirectionVectorFromAltitudeAzimuth(ActualSyncPoint3); CalculateTransformMatrices(ActualDirectionCosine1, ActualDirectionCosine2, ActualDirectionCosine3, Entry1.TelescopeDirection, Entry2.TelescopeDirection, Entry3.TelescopeDirection, pActualToApparentTransform, pApparentToActualTransform); return true; } default: { ln_lnlat_posn Position { 0, 0 }; if (!pInMemoryDatabase->GetDatabaseReferencePosition(Position)) return false; // Compute Hulls etc. ActualConvexHull.Reset(); ApparentConvexHull.Reset(); ActualDirectionCosines.clear(); // Add a dummy point at the nadir ActualConvexHull.MakeNewVertex(0.0, 0.0, -1.0, 0); ApparentConvexHull.MakeNewVertex(0.0, 0.0, -1.0, 0); int VertexNumber = 1; // Add the rest of the vertices for (InMemoryDatabase::AlignmentDatabaseType::const_iterator Itr = SyncPoints.begin(); Itr != SyncPoints.end(); Itr++) { ln_equ_posn RaDec; ln_hrz_posn ActualSyncPoint; RaDec.dec = (*Itr).Declination; // libnova works in decimal degrees so conversion is needed here RaDec.ra = (*Itr).RightAscension * 360.0 / 24.0; ln_get_hrz_from_equ(&RaDec, &Position, (*Itr).ObservationJulianDate, &ActualSyncPoint); // Now express this coordinate as normalised direction vectors (a.k.a direction cosines) TelescopeDirectionVector ActualDirectionCosine = TelescopeDirectionVectorFromAltitudeAzimuth(ActualSyncPoint); ActualDirectionCosines.push_back(ActualDirectionCosine); ActualConvexHull.MakeNewVertex(ActualDirectionCosine.x, ActualDirectionCosine.y, ActualDirectionCosine.z, VertexNumber); ApparentConvexHull.MakeNewVertex((*Itr).TelescopeDirection.x, (*Itr).TelescopeDirection.y, (*Itr).TelescopeDirection.z, VertexNumber); VertexNumber++; } // I should only need to do this once but it is easier to do it twice ActualConvexHull.DoubleTriangle(); ActualConvexHull.ConstructHull(); ActualConvexHull.EdgeOrderOnFaces(); ApparentConvexHull.DoubleTriangle(); ApparentConvexHull.ConstructHull(); ApparentConvexHull.EdgeOrderOnFaces(); // Make the matrices ConvexHull::tFace CurrentFace = ActualConvexHull.faces; #ifdef CONVEX_HULL_DEBUGGING int ActualFaces = 0; #endif if (nullptr != CurrentFace) { do { #ifdef CONVEX_HULL_DEBUGGING ActualFaces++; #endif if ((0 == CurrentFace->vertex[0]->vnum) || (0 == CurrentFace->vertex[1]->vnum) || (0 == CurrentFace->vertex[2]->vnum)) { #ifdef CONVEX_HULL_DEBUGGING ASSDEBUGF("Initialise - Ignoring actual face %d", ActualFaces); #endif } else { #ifdef CONVEX_HULL_DEBUGGING ASSDEBUGF("Initialise - Processing actual face %d v1 %d v2 %d v3 %d", ActualFaces, CurrentFace->vertex[0]->vnum, CurrentFace->vertex[1]->vnum, CurrentFace->vertex[2]->vnum); #endif CalculateTransformMatrices(ActualDirectionCosines[CurrentFace->vertex[0]->vnum - 1], ActualDirectionCosines[CurrentFace->vertex[1]->vnum - 1], ActualDirectionCosines[CurrentFace->vertex[2]->vnum - 1], SyncPoints[CurrentFace->vertex[0]->vnum - 1].TelescopeDirection, SyncPoints[CurrentFace->vertex[1]->vnum - 1].TelescopeDirection, SyncPoints[CurrentFace->vertex[2]->vnum - 1].TelescopeDirection, CurrentFace->pMatrix, nullptr); } CurrentFace = CurrentFace->next; } while (CurrentFace != ActualConvexHull.faces); } // One of these days I will optimise this CurrentFace = ApparentConvexHull.faces; #ifdef CONVEX_HULL_DEBUGGING int ApparentFaces = 0; #endif if (nullptr != CurrentFace) { do { #ifdef CONVEX_HULL_DEBUGGING ApparentFaces++; #endif if ((0 == CurrentFace->vertex[0]->vnum) || (0 == CurrentFace->vertex[1]->vnum) || (0 == CurrentFace->vertex[2]->vnum)) { #ifdef CONVEX_HULL_DEBUGGING ASSDEBUGF("Initialise - Ignoring apparent face %d", ApparentFaces); #endif } else { #ifdef CONVEX_HULL_DEBUGGING ASSDEBUGF("Initialise - Processing apparent face %d v1 %d v2 %d v3 %d", ApparentFaces, CurrentFace->vertex[0]->vnum, CurrentFace->vertex[1]->vnum, CurrentFace->vertex[2]->vnum); #endif CalculateTransformMatrices(SyncPoints[CurrentFace->vertex[0]->vnum - 1].TelescopeDirection, SyncPoints[CurrentFace->vertex[1]->vnum - 1].TelescopeDirection, SyncPoints[CurrentFace->vertex[2]->vnum - 1].TelescopeDirection, ActualDirectionCosines[CurrentFace->vertex[0]->vnum - 1], ActualDirectionCosines[CurrentFace->vertex[1]->vnum - 1], ActualDirectionCosines[CurrentFace->vertex[2]->vnum - 1], CurrentFace->pMatrix, nullptr); } CurrentFace = CurrentFace->next; } while (CurrentFace != ApparentConvexHull.faces); } #ifdef CONVEX_HULL_DEBUGGING ASSDEBUGF("Initialise - ActualFaces %d ApparentFaces %d", ActualFaces, ApparentFaces); ActualConvexHull.PrintObj("ActualHull.obj"); ActualConvexHull.PrintOut("ActualHull.log", ActualConvexHull.vertices); ApparentConvexHull.PrintObj("ApparentHull.obj"); ActualConvexHull.PrintOut("ApparentHull.log", ApparentConvexHull.vertices); #endif return true; } } } bool BasicMathPlugin::TransformCelestialToTelescope(const double RightAscension, const double Declination, double JulianOffset, TelescopeDirectionVector &ApparentTelescopeDirectionVector) { ln_equ_posn ActualRaDec; ln_hrz_posn ActualAltAz; // libnova works in decimal degrees so conversion is needed here ActualRaDec.ra = RightAscension * 360.0 / 24.0; ActualRaDec.dec = Declination; ln_lnlat_posn Position { 0, 0 }; if ((nullptr == pInMemoryDatabase) || !pInMemoryDatabase->GetDatabaseReferencePosition( Position)) // Should check that this the same as the current observing position return false; ln_get_hrz_from_equ(&ActualRaDec, &Position, ln_get_julian_from_sys() + JulianOffset, &ActualAltAz); ASSDEBUGF("Celestial to telescope - Actual Alt %lf Az %lf", ActualAltAz.alt, ActualAltAz.az); TelescopeDirectionVector ActualVector = TelescopeDirectionVectorFromAltitudeAzimuth(ActualAltAz); InMemoryDatabase::AlignmentDatabaseType &SyncPoints = pInMemoryDatabase->GetAlignmentDatabase(); switch (SyncPoints.size()) { case 0: { // 0 sync points ApparentTelescopeDirectionVector = ActualVector; switch (ApproximateMountAlignment) { case ZENITH: break; case NORTH_CELESTIAL_POLE: // Rotate the TDV coordinate system clockwise (negative) around the y axis by 90 minus // the (positive)observatory latitude. The vector itself is rotated anticlockwise ApparentTelescopeDirectionVector.RotateAroundY(Position.lat - 90.0); break; case SOUTH_CELESTIAL_POLE: // Rotate the TDV coordinate system anticlockwise (positive) around the y axis by 90 plus // the (negative)observatory latitude. The vector itself is rotated clockwise ApparentTelescopeDirectionVector.RotateAroundY(Position.lat + 90.0); break; } break; } case 1: case 2: case 3: { gsl_vector *pGSLActualVector = gsl_vector_alloc(3); gsl_vector_set(pGSLActualVector, 0, ActualVector.x); gsl_vector_set(pGSLActualVector, 1, ActualVector.y); gsl_vector_set(pGSLActualVector, 2, ActualVector.z); gsl_vector *pGSLApparentVector = gsl_vector_alloc(3); MatrixVectorMultiply(pActualToApparentTransform, pGSLActualVector, pGSLApparentVector); ApparentTelescopeDirectionVector.x = gsl_vector_get(pGSLApparentVector, 0); ApparentTelescopeDirectionVector.y = gsl_vector_get(pGSLApparentVector, 1); ApparentTelescopeDirectionVector.z = gsl_vector_get(pGSLApparentVector, 2); ApparentTelescopeDirectionVector.Normalise(); gsl_vector_free(pGSLActualVector); gsl_vector_free(pGSLApparentVector); break; } default: { gsl_matrix *pTransform; gsl_matrix *pComputedTransform = nullptr; // Scale the actual telescope direction vector to make sure it traverses the unit sphere. TelescopeDirectionVector ScaledActualVector = ActualVector * 2.0; // Shoot the scaled vector in the into the list of actual facets // and use the conversuion matrix from the one it intersects ConvexHull::tFace CurrentFace = ActualConvexHull.faces; #ifdef CONVEX_HULL_DEBUGGING int ActualFaces = 0; #endif if (nullptr != CurrentFace) { do { #ifdef CONVEX_HULL_DEBUGGING ActualFaces++; #endif // Ignore faces containg vertex 0 (nadir). if ((0 == CurrentFace->vertex[0]->vnum) || (0 == CurrentFace->vertex[1]->vnum) || (0 == CurrentFace->vertex[2]->vnum)) { #ifdef CONVEX_HULL_DEBUGGING ASSDEBUGF("Celestial to telescope - Ignoring actual face %d", ActualFaces); #endif } else { #ifdef CONVEX_HULL_DEBUGGING ASSDEBUGF("Celestial to telescope - Processing actual face %d v1 %d v2 %d v3 %d", ActualFaces, CurrentFace->vertex[0]->vnum, CurrentFace->vertex[1]->vnum, CurrentFace->vertex[2]->vnum); #endif if (RayTriangleIntersection(ScaledActualVector, ActualDirectionCosines[CurrentFace->vertex[0]->vnum - 1], ActualDirectionCosines[CurrentFace->vertex[1]->vnum - 1], ActualDirectionCosines[CurrentFace->vertex[2]->vnum - 1])) break; } CurrentFace = CurrentFace->next; } while (CurrentFace != ActualConvexHull.faces); if (CurrentFace == ActualConvexHull.faces) { // Find the three nearest points and build a transform std::map NearestMap; for (InMemoryDatabase::AlignmentDatabaseType::const_iterator Itr = SyncPoints.begin(); Itr != SyncPoints.end(); Itr++) { ln_equ_posn RaDec; ln_hrz_posn ActualPoint; RaDec.ra = (*Itr).RightAscension * 360.0 / 24.0; RaDec.dec = (*Itr).Declination; ln_get_hrz_from_equ(&RaDec, &Position, (*Itr).ObservationJulianDate, &ActualPoint); TelescopeDirectionVector ActualDirectionCosine = TelescopeDirectionVectorFromAltitudeAzimuth(ActualPoint); NearestMap[(ActualDirectionCosine - ActualVector).Length()] = &(*Itr); } // First compute local horizontal coordinates for the three sync points std::map::const_iterator Nearest = NearestMap.begin(); const AlignmentDatabaseEntry *pEntry1 = (*Nearest).second; Nearest++; const AlignmentDatabaseEntry *pEntry2 = (*Nearest).second; Nearest++; const AlignmentDatabaseEntry *pEntry3 = (*Nearest).second; ln_hrz_posn ActualSyncPoint1; ln_hrz_posn ActualSyncPoint2; ln_hrz_posn ActualSyncPoint3; ln_equ_posn RaDec1; ln_equ_posn RaDec2; ln_equ_posn RaDec3; RaDec1.dec = pEntry1->Declination; // libnova works in decimal degrees so conversion is needed here RaDec1.ra = pEntry1->RightAscension * 360.0 / 24.0; RaDec2.dec = pEntry2->Declination; // libnova works in decimal degrees so conversion is needed here RaDec2.ra = pEntry2->RightAscension * 360.0 / 24.0; RaDec3.dec = pEntry3->Declination; // libnova works in decimal degrees so conversion is needed here RaDec3.ra = pEntry3->RightAscension * 360.0 / 24.0; ln_get_hrz_from_equ(&RaDec1, &Position, pEntry1->ObservationJulianDate, &ActualSyncPoint1); ln_get_hrz_from_equ(&RaDec2, &Position, pEntry2->ObservationJulianDate, &ActualSyncPoint2); ln_get_hrz_from_equ(&RaDec3, &Position, pEntry3->ObservationJulianDate, &ActualSyncPoint3); // Now express these coordinates as normalised direction vectors (a.k.a direction cosines) TelescopeDirectionVector ActualDirectionCosine1 = TelescopeDirectionVectorFromAltitudeAzimuth(ActualSyncPoint1); TelescopeDirectionVector ActualDirectionCosine2 = TelescopeDirectionVectorFromAltitudeAzimuth(ActualSyncPoint2); TelescopeDirectionVector ActualDirectionCosine3 = TelescopeDirectionVectorFromAltitudeAzimuth(ActualSyncPoint3); pComputedTransform = gsl_matrix_alloc(3, 3); CalculateTransformMatrices(ActualDirectionCosine1, ActualDirectionCosine2, ActualDirectionCosine3, pEntry1->TelescopeDirection, pEntry2->TelescopeDirection, pEntry3->TelescopeDirection, pComputedTransform, nullptr); pTransform = pComputedTransform; } else pTransform = CurrentFace->pMatrix; } else return false; // OK - got an intersection - CurrentFace is pointing at the face gsl_vector *pGSLActualVector = gsl_vector_alloc(3); gsl_vector_set(pGSLActualVector, 0, ActualVector.x); gsl_vector_set(pGSLActualVector, 1, ActualVector.y); gsl_vector_set(pGSLActualVector, 2, ActualVector.z); gsl_vector *pGSLApparentVector = gsl_vector_alloc(3); MatrixVectorMultiply(pTransform, pGSLActualVector, pGSLApparentVector); ApparentTelescopeDirectionVector.x = gsl_vector_get(pGSLApparentVector, 0); ApparentTelescopeDirectionVector.y = gsl_vector_get(pGSLApparentVector, 1); ApparentTelescopeDirectionVector.z = gsl_vector_get(pGSLApparentVector, 2); ApparentTelescopeDirectionVector.Normalise(); gsl_vector_free(pGSLActualVector); gsl_vector_free(pGSLApparentVector); if (nullptr != pComputedTransform) gsl_matrix_free(pComputedTransform); break; } } ln_hrz_posn ApparentAltAz; AltitudeAzimuthFromTelescopeDirectionVector(ApparentTelescopeDirectionVector, ApparentAltAz); ASSDEBUGF("Celestial to telescope - Apparent Alt %lf Az %lf", ApparentAltAz.alt, ApparentAltAz.az); return true; } bool BasicMathPlugin::TransformTelescopeToCelestial(const TelescopeDirectionVector &ApparentTelescopeDirectionVector, double &RightAscension, double &Declination) { ln_lnlat_posn Position; ln_hrz_posn ApparentAltAz; ln_hrz_posn ActualAltAz; ln_equ_posn ActualRaDec; AltitudeAzimuthFromTelescopeDirectionVector(ApparentTelescopeDirectionVector, ApparentAltAz); ASSDEBUGF("Telescope to celestial - Apparent Alt %lf Az %lf", ApparentAltAz.alt, ApparentAltAz.az); if ((nullptr == pInMemoryDatabase) || !pInMemoryDatabase->GetDatabaseReferencePosition(Position)) { // Should check that this the same as the current observing position ASSDEBUG("No database or no position in database"); return false; } InMemoryDatabase::AlignmentDatabaseType &SyncPoints = pInMemoryDatabase->GetAlignmentDatabase(); switch (SyncPoints.size()) { case 0: { // 0 sync points TelescopeDirectionVector RotatedTDV(ApparentTelescopeDirectionVector); switch (ApproximateMountAlignment) { case ZENITH: break; case NORTH_CELESTIAL_POLE: // Rotate the TDV coordinate system anticlockwise (positive) around the y axis by 90 minus // the (positive)observatory latitude. The vector itself is rotated clockwise RotatedTDV.RotateAroundY(90.0 - Position.lat); break; case SOUTH_CELESTIAL_POLE: // Rotate the TDV coordinate system clockwise (negative) around the y axis by 90 plus // the (negative)observatory latitude. The vector itself is rotated anticlockwise RotatedTDV.RotateAroundY(-90.0 - Position.lat); break; } ASSDEBUGF("ApparentVector x %lf y %lf z %lf", ApparentTelescopeDirectionVector.x, ApparentTelescopeDirectionVector.y, ApparentTelescopeDirectionVector.z); ASSDEBUGF("ActualVector x %lf y %lf z %lf", RotatedTDV.x, RotatedTDV.y, RotatedTDV.z); AltitudeAzimuthFromTelescopeDirectionVector(RotatedTDV, ActualAltAz); ln_get_equ_from_hrz(&ActualAltAz, &Position, ln_get_julian_from_sys(), &ActualRaDec); // libnova works in decimal degrees so conversion is needed here RightAscension = ActualRaDec.ra * 24.0 / 360.0; Declination = ActualRaDec.dec; break; } case 1: case 2: case 3: { gsl_vector *pGSLApparentVector = gsl_vector_alloc(3); gsl_vector_set(pGSLApparentVector, 0, ApparentTelescopeDirectionVector.x); gsl_vector_set(pGSLApparentVector, 1, ApparentTelescopeDirectionVector.y); gsl_vector_set(pGSLApparentVector, 2, ApparentTelescopeDirectionVector.z); gsl_vector *pGSLActualVector = gsl_vector_alloc(3); MatrixVectorMultiply(pApparentToActualTransform, pGSLApparentVector, pGSLActualVector); Dump3("ApparentVector", pGSLApparentVector); Dump3("ActualVector", pGSLActualVector); TelescopeDirectionVector ActualTelescopeDirectionVector; ActualTelescopeDirectionVector.x = gsl_vector_get(pGSLActualVector, 0); ActualTelescopeDirectionVector.y = gsl_vector_get(pGSLActualVector, 1); ActualTelescopeDirectionVector.z = gsl_vector_get(pGSLActualVector, 2); ActualTelescopeDirectionVector.Normalise(); AltitudeAzimuthFromTelescopeDirectionVector(ActualTelescopeDirectionVector, ActualAltAz); ln_get_equ_from_hrz(&ActualAltAz, &Position, ln_get_julian_from_sys(), &ActualRaDec); // libnova works in decimal degrees so conversion is needed here RightAscension = ActualRaDec.ra * 24.0 / 360.0; Declination = ActualRaDec.dec; gsl_vector_free(pGSLActualVector); gsl_vector_free(pGSLApparentVector); break; } default: { gsl_matrix *pTransform; gsl_matrix *pComputedTransform = nullptr; // Scale the apparent telescope direction vector to make sure it traverses the unit sphere. TelescopeDirectionVector ScaledApparentVector = ApparentTelescopeDirectionVector * 2.0; // Shoot the scaled vector in the into the list of apparent facets // and use the conversuion matrix from the one it intersects ConvexHull::tFace CurrentFace = ApparentConvexHull.faces; #ifdef CONVEX_HULL_DEBUGGING int ApparentFaces = 0; #endif if (nullptr != CurrentFace) { do { #ifdef CONVEX_HULL_DEBUGGING ApparentFaces++; #endif // Ignore faces containg vertex 0 (nadir). if ((0 == CurrentFace->vertex[0]->vnum) || (0 == CurrentFace->vertex[1]->vnum) || (0 == CurrentFace->vertex[2]->vnum)) { #ifdef CONVEX_HULL_DEBUGGING ASSDEBUGF("Celestial to telescope - Ignoring apparent face %d", ApparentFaces); #endif } else { #ifdef CONVEX_HULL_DEBUGGING ASSDEBUGF("TelescopeToCelestial - Processing apparent face %d v1 %d v2 %d v3 %d", ApparentFaces, CurrentFace->vertex[0]->vnum, CurrentFace->vertex[1]->vnum, CurrentFace->vertex[2]->vnum); #endif if (RayTriangleIntersection(ScaledApparentVector, SyncPoints[CurrentFace->vertex[0]->vnum - 1].TelescopeDirection, SyncPoints[CurrentFace->vertex[1]->vnum - 1].TelescopeDirection, SyncPoints[CurrentFace->vertex[2]->vnum - 1].TelescopeDirection)) break; } CurrentFace = CurrentFace->next; } while (CurrentFace != ApparentConvexHull.faces); if (CurrentFace == ApparentConvexHull.faces) { // Find the three nearest points and build a transform std::map NearestMap; for (InMemoryDatabase::AlignmentDatabaseType::const_iterator Itr = SyncPoints.begin(); Itr != SyncPoints.end(); Itr++) { NearestMap[((*Itr).TelescopeDirection - ApparentTelescopeDirectionVector).Length()] = &(*Itr); } // First compute local horizontal coordinates for the three sync points std::map::const_iterator Nearest = NearestMap.begin(); const AlignmentDatabaseEntry *pEntry1 = (*Nearest).second; Nearest++; const AlignmentDatabaseEntry *pEntry2 = (*Nearest).second; Nearest++; const AlignmentDatabaseEntry *pEntry3 = (*Nearest).second; ln_hrz_posn ActualSyncPoint1; ln_hrz_posn ActualSyncPoint2; ln_hrz_posn ActualSyncPoint3; ln_equ_posn RaDec1; ln_equ_posn RaDec2; ln_equ_posn RaDec3; RaDec1.dec = pEntry1->Declination; // libnova works in decimal degrees so conversion is needed here RaDec1.ra = pEntry1->RightAscension * 360.0 / 24.0; RaDec2.dec = pEntry2->Declination; // libnova works in decimal degrees so conversion is needed here RaDec2.ra = pEntry2->RightAscension * 360.0 / 24.0; RaDec3.dec = pEntry3->Declination; // libnova works in decimal degrees so conversion is needed here RaDec3.ra = pEntry3->RightAscension * 360.0 / 24.0; ln_get_hrz_from_equ(&RaDec1, &Position, pEntry1->ObservationJulianDate, &ActualSyncPoint1); ln_get_hrz_from_equ(&RaDec2, &Position, pEntry2->ObservationJulianDate, &ActualSyncPoint2); ln_get_hrz_from_equ(&RaDec3, &Position, pEntry3->ObservationJulianDate, &ActualSyncPoint3); // Now express these coordinates as normalised direction vectors (a.k.a direction cosines) TelescopeDirectionVector ActualDirectionCosine1 = TelescopeDirectionVectorFromAltitudeAzimuth(ActualSyncPoint1); TelescopeDirectionVector ActualDirectionCosine2 = TelescopeDirectionVectorFromAltitudeAzimuth(ActualSyncPoint2); TelescopeDirectionVector ActualDirectionCosine3 = TelescopeDirectionVectorFromAltitudeAzimuth(ActualSyncPoint3); pComputedTransform = gsl_matrix_alloc(3, 3); CalculateTransformMatrices(pEntry1->TelescopeDirection, pEntry2->TelescopeDirection, pEntry3->TelescopeDirection, ActualDirectionCosine1, ActualDirectionCosine2, ActualDirectionCosine3, pComputedTransform, nullptr); pTransform = pComputedTransform; } else pTransform = CurrentFace->pMatrix; } else return false; // OK - got an intersection - CurrentFace is pointing at the face gsl_vector *pGSLApparentVector = gsl_vector_alloc(3); gsl_vector_set(pGSLApparentVector, 0, ApparentTelescopeDirectionVector.x); gsl_vector_set(pGSLApparentVector, 1, ApparentTelescopeDirectionVector.y); gsl_vector_set(pGSLApparentVector, 2, ApparentTelescopeDirectionVector.z); gsl_vector *pGSLActualVector = gsl_vector_alloc(3); MatrixVectorMultiply(pTransform, pGSLApparentVector, pGSLActualVector); TelescopeDirectionVector ActualTelescopeDirectionVector; ActualTelescopeDirectionVector.x = gsl_vector_get(pGSLActualVector, 0); ActualTelescopeDirectionVector.y = gsl_vector_get(pGSLActualVector, 1); ActualTelescopeDirectionVector.z = gsl_vector_get(pGSLActualVector, 2); ActualTelescopeDirectionVector.Normalise(); AltitudeAzimuthFromTelescopeDirectionVector(ActualTelescopeDirectionVector, ActualAltAz); ln_get_equ_from_hrz(&ActualAltAz, &Position, ln_get_julian_from_sys(), &ActualRaDec); // libnova works in decimal degrees so conversion is needed here RightAscension = ActualRaDec.ra * 24.0 / 360.0; Declination = ActualRaDec.dec; gsl_vector_free(pGSLActualVector); gsl_vector_free(pGSLApparentVector); if (nullptr != pComputedTransform) gsl_matrix_free(pComputedTransform); break; } } ASSDEBUGF("Telescope to Celestial - Actual Alt %lf Az %lf", ActualAltAz.alt, ActualAltAz.az); return true; } // Private methods void BasicMathPlugin::Dump3(const char *Label, gsl_vector *pVector) { ASSDEBUGF("Vector dump - %s", Label); ASSDEBUGF("%lf %lf %lf", gsl_vector_get(pVector, 0), gsl_vector_get(pVector, 1), gsl_vector_get(pVector, 2)); } void BasicMathPlugin::Dump3x3(const char *Label, gsl_matrix *pMatrix) { ASSDEBUGF("Matrix dump - %s", Label); ASSDEBUGF("Row 0 %lf %lf %lf", gsl_matrix_get(pMatrix, 0, 0), gsl_matrix_get(pMatrix, 0, 1), gsl_matrix_get(pMatrix, 0, 2)); ASSDEBUGF("Row 1 %lf %lf %lf", gsl_matrix_get(pMatrix, 1, 0), gsl_matrix_get(pMatrix, 1, 1), gsl_matrix_get(pMatrix, 1, 2)); ASSDEBUGF("Row 2 %lf %lf %lf", gsl_matrix_get(pMatrix, 2, 0), gsl_matrix_get(pMatrix, 2, 1), gsl_matrix_get(pMatrix, 2, 2)); } /// Use gsl to compute the determinant of a 3x3 matrix double BasicMathPlugin::Matrix3x3Determinant(gsl_matrix *pMatrix) { gsl_permutation *pPermutation = gsl_permutation_alloc(3); gsl_matrix *pDecomp = gsl_matrix_alloc(3, 3); int Signum; double Determinant; gsl_matrix_memcpy(pDecomp, pMatrix); gsl_linalg_LU_decomp(pDecomp, pPermutation, &Signum); Determinant = gsl_linalg_LU_det(pDecomp, Signum); gsl_matrix_free(pDecomp); gsl_permutation_free(pPermutation); return Determinant; } /// Use gsl to compute the inverse of a 3x3 matrix bool BasicMathPlugin::MatrixInvert3x3(gsl_matrix *pInput, gsl_matrix *pInversion) { bool Retcode = true; gsl_permutation *pPermutation = gsl_permutation_alloc(3); gsl_matrix *pDecomp = gsl_matrix_alloc(3, 3); int Signum; gsl_matrix_memcpy(pDecomp, pInput); gsl_linalg_LU_decomp(pDecomp, pPermutation, &Signum); // Test for singularity if (0 == gsl_linalg_LU_det(pDecomp, Signum)) { Retcode = false; } else gsl_linalg_LU_invert(pDecomp, pPermutation, pInversion); gsl_matrix_free(pDecomp); gsl_permutation_free(pPermutation); return Retcode; } /// Use gsl blas support to multiply two matrices together and put the result in a third. /// For our purposes all the matrices should be 3 by 3. void BasicMathPlugin::MatrixMatrixMultiply(gsl_matrix *pA, gsl_matrix *pB, gsl_matrix *pC) { // Zeroise the output matrix gsl_matrix_set_zero(pC); gsl_blas_dgemm(CblasNoTrans, CblasNoTrans, 1.0, pA, pB, 0.0, pC); } /// Use gsl blas support to multiply a matrix by a vector and put the result in another vector /// For our purposes the the matrix should be 3x3 and vector 3. void BasicMathPlugin::MatrixVectorMultiply(gsl_matrix *pA, gsl_vector *pB, gsl_vector *pC) { // Zeroise the output vector gsl_vector_set_zero(pC); gsl_blas_dgemv(CblasNoTrans, 1.0, pA, pB, 0.0, pC); } bool BasicMathPlugin::RayTriangleIntersection(TelescopeDirectionVector &Ray, TelescopeDirectionVector &TriangleVertex1, TelescopeDirectionVector &TriangleVertex2, TelescopeDirectionVector &TriangleVertex3) { // Use Möller-Trumbore //Find vectors for two edges sharing V1 TelescopeDirectionVector Edge1 = TriangleVertex2 - TriangleVertex1; TelescopeDirectionVector Edge2 = TriangleVertex3 - TriangleVertex1; TelescopeDirectionVector P = Ray * Edge2; // cross product double Determinant = Edge1 ^ P; // dot product double InverseDeterminant = 1.0 / Determinant; // If the determinant is negative the triangle is backfacing // If the determinant is close to 0, the ray misses the triangle if ((Determinant > -std::numeric_limits::epsilon()) && (Determinant < std::numeric_limits::epsilon())) return false; // I use zero as ray origin so TelescopeDirectionVector T(-TriangleVertex1.x, -TriangleVertex1.y, -TriangleVertex1.z); // Calculate the u parameter double u = (T ^ P) * InverseDeterminant; if (u < 0.0 || u > 1.0) //The intersection lies outside of the triangle return false; //Prepare to test v parameter TelescopeDirectionVector Q = T * Edge1; //Calculate v parameter and test bound double v = (Ray ^ Q) * InverseDeterminant; if (v < 0.0 || u + v > 1.0) //The intersection lies outside of the triangle return false; double t = (Edge2 ^ Q) * InverseDeterminant; if (t > std::numeric_limits::epsilon()) { //ray intersection return true; } // No hit, no win return false; } } // namespace AlignmentSubsystem } // namespace INDI libindi/libs/indibase/alignment/ConvexHull.h0000664000175000017500000003106513263645557020424 0ustar jasemjasem/*! * \file ConvexHull.h * * \author Roger James * \date December 2013 * */ #pragma once // This C++ code is based on the c code described below // it was ported to c++ by Roger James in December 2013 // !!!!!!!!!!!!!!!!!!!!!!! IMPORTANT !!!!!!!!!!!!!!!!!! // This must code must use integer coordinates. A naive conversion // to floating point WILL NOT work. For the reasons behind this // have a look at at section 4.3.5 of the O'Rourke book. For more // information try http://www.mpi-inf.mpg.de/departments/d1/ClassroomExamples/ // For INDI alignment purposes we need to scale floating point coordinates // into the integer space before using this algorithm. /* This code is described in "Computational Geometry in C" (Second Edition), Chapter 4. It is not written to be comprehensible without the explanation in that book. Input: 3n integer coordinates for the points. Output: the 3D convex hull, in postscript with embedded comments showing the vertices and faces. Compile: gcc -o chull chull.c (or simply: make) Written by Joseph O'Rourke, with contributions by Kristy Anderson, John Kutcher, Catherine Schevon, Susan Weller. Last modified: May 2000 Questions to orourke@cs.smith.edu. -------------------------------------------------------------------- This code is Copyright 2000 by Joseph O'Rourke. It may be freely redistributed in its entirety provided that this copyright notice is not removed. -------------------------------------------------------------------- */ #include #include #include #include #include namespace INDI { namespace AlignmentSubsystem { /*! * \class ConvexHull * \brief This class computes the convex hull of a set of 3d points. */ class ConvexHull { public: ConvexHull(); virtual ~ConvexHull() {} enum { X = 0, Y = 1, Z = 2 }; template static void add(Type &head, Type p) { if (NULL != head) { p->next = head; p->prev = head->prev; head->prev = p; p->prev->next = p; } else { head = p; head->next = head->prev = p; } }; template static void remove(Type &head, Type p) { if (NULL != head) { if (head == head->next) head = NULL; else if (p == head) head = head->next; p->next->prev = p->prev; p->prev->next = p->next; delete p; } }; template static void swap(Type &t, Type &x, Type &y) { t = x; x = y; y = t; }; // Explicit forward declarations struct tVertexStructure; struct tFaceStructure; struct tEdgeStructure; /* Define structures for vertices, edges and faces */ typedef struct tVertexStructure tsVertex; typedef tsVertex *tVertex; typedef struct tEdgeStructure tsEdge; typedef tsEdge *tEdge; typedef struct tFaceStructure tsFace; typedef tsFace *tFace; struct tVertexStructure { int v[3]; int vnum; tEdge duplicate; // pointer to incident cone edge (or NULL) bool onhull; // True iff point on hull. bool mark; // True iff point already processed. tVertex next, prev; }; struct tEdgeStructure { tFace adjface[2]; tVertex endpts[2]; tFace newface; // pointer to incident cone face. bool delete_it; // True iff edge should be delete. tEdge next, prev; }; struct tFaceStructure { tFaceStructure() { pMatrix = gsl_matrix_alloc(3, 3); } ~tFaceStructure() { gsl_matrix_free(pMatrix); } tEdge edge[3]; tVertex vertex[3]; bool visible; // True iff face visible from new point. tFace next, prev; gsl_matrix *pMatrix; }; /* Define flags */ static const bool ONHULL = true; static const bool REMOVED = true; static const bool VISIBLE = true; static const bool PROCESSED = true; static const int SAFE = 1000000; /* Range of safe coord values. */ tVertex vertices; tEdge edges; tFace faces; /** \brief AddOne is passed a vertex. It first determines all faces visible from that point. If none are visible then the point is marked as not onhull. Next is a loop over edges. If both faces adjacent to an edge are visible, then the edge is marked for deletion. If just one of the adjacent faces is visible then a new face is constructed. */ bool AddOne(tVertex p); /** \brief Checks that, for each face, for each i={0,1,2}, the [i]th vertex of that face is either the [0]th or [1]st endpoint of the [ith] edge of the face. */ void CheckEndpts(); /** \brief CheckEuler checks Euler's relation, as well as its implications when all faces are known to be triangles. Only prints positive information when debug is true, but always prints negative information. */ void CheckEuler(int V, int E, int F); /** \brief Checks the consistency of the hull and prints the results to the standard error output. */ void Checks(); /** \brief CleanEdges runs through the edge list and cleans up the structure. If there is a newface then it will put that face in place of the visible face and NULL out newface. It also deletes so marked edges. */ void CleanEdges(); /** \brief CleanFaces runs through the face list and deletes any face marked visible. */ void CleanFaces(); /** \brief CleanUp goes through each data structure list and clears all flags and NULLs out some pointers. The order of processing (edges, faces, vertices) is important. */ void CleanUp(tVertex *pvnext); /** \brief CleanVertices runs through the vertex list and deletes the vertices that are marked as processed but are not incident to any undeleted edges. The pointer to vnext, pvnext, is used to alter vnext in ConstructHull() if we are about to delete vnext. */ void CleanVertices(tVertex *pvnext); /** \brief Collinear checks to see if the three points given are collinear, by checking to see if each element of the cross product is zero. */ bool Collinear(tVertex a, tVertex b, tVertex c); /** \brief Consistency runs through the edge list and checks that all adjacent faces have their endpoints in opposite order. This verifies that the vertices are in counterclockwise order. */ void Consistency(); /** \brief ConstructHull adds the vertices to the hull one at a time. The hull vertices are those in the list marked as onhull. */ void ConstructHull(); /** \brief Convexity checks that the volume between every face and every point is negative. This shows that each point is inside every face and therefore the hull is convex. */ void Convexity(); /** \brief DoubleTriangle builds the initial double triangle. It first finds 3 noncollinear points and makes two faces out of them, in opposite order. It then finds a fourth point that is not coplanar with that face. The vertices are stored in the face structure in counterclockwise order so that the volume between the face and the point is negative. Lastly, the 3 newfaces to the fourth point are constructed and the data structures are cleaned up. */ void DoubleTriangle(); /** \brief EdgeOrderOnFaces: puts e0 between v0 and v1, e1 between v1 and v2, e2 between v2 and v0 on each face. This should be unnecessary, alas. Not used in code, but useful for other purposes. */ void EdgeOrderOnFaces(); /** \brief Set the floating point to integer scaling factor */ int GetScaleFactor() const { return ScaleFactor; } /** \brief MakeCcw puts the vertices in the face structure in counterclock wise order. We want to store the vertices in the same order as in the visible face. The third vertex is always p. Although no specific ordering of the edges of a face are used by the code, the following condition is maintained for each face f: one of the two endpoints of f->edge[i] matches f->vertex[i]. But note that this does not imply that f->edge[i] is between f->vertex[i] and f->vertex[(i+1)%3]. (Thanks to Bob Williamson.) */ void MakeCcw(tFace f, tEdge e, tVertex p); /** \brief MakeConeFace makes a new face and two new edges between the edge and the point that are passed to it. It returns a pointer to the new face. */ tFace MakeConeFace(tEdge e, tVertex p); /** \brief MakeFace creates a new face structure from three vertices (in ccw order). It returns a pointer to the face. */ tFace MakeFace(tVertex v0, tVertex v1, tVertex v2, tFace f); /** \brief Makes a vertex from the supplied information and adds it to the vertices list. */ void MakeNewVertex(double x, double y, double z, int VertexId); /** \brief MakeNullEdge creates a new cell and initializes all pointers to NULL and sets all flags to off. It returns a pointer to the empty cell. */ tEdge MakeNullEdge(); /** \brief MakeNullFace creates a new face structure and initializes all of its flags to NULL and sets all the flags to off. It returns a pointer to the empty cell. */ tFace MakeNullFace(); /** \brief MakeNullVertex: Makes a vertex, nulls out fields. */ tVertex MakeNullVertex(); /** \brief Print: Prints out the vertices and the faces. Uses the vnum indices corresponding to the order in which the vertices were input. Output is in PostScript format. This code ignores the Z component of all vertices and does not scale the output to fit the page. It use on 3D hulls is not recommended. */ void Print(); /** \brief Prints the edges Ofile */ void PrintEdges(std::ofstream &Ofile); /** \brief Prints the faces to Ofile */ void PrintFaces(std::ofstream &Ofile); /** \brief Outputs the faces in Lightwave obj format for 3d viewing. The files chull.obj and chull.mtl are written to the current working directory. */ void PrintObj(const char *FileName = "chull.obj"); /** \brief Prints vertices, edges and faces to the standard error output */ void PrintOut(const char *FileName, tVertex v); /** \brief Prints a single vertex to the standard output. */ void PrintPoint(tVertex p); /** \brief Prints vertices to Ofile. */ void PrintVertices(std::ofstream &Ofile); /** \brief ReadVertices: Reads in the vertices, and links them into a circular list with MakeNullVertex. There is no need for the # of vertices to be the first line: the function looks for EOF instead. Sets the global variable vertices via the add<> template function. */ void ReadVertices(); /** \brief Frees the vertices edges and faces lists and resets the debug and check flags. */ void Reset(); /** \brief Set the floating point to integer scaling factor. If you want to tweak this a good value to start from may well be a little bit more than the resolution of the mounts encoders. Whatever is used must not exceed the default value which is set to the constant SAFE. */ void SetScaleFactor(const int NewScaleFactor) { ScaleFactor = NewScaleFactor; } /** \brief SubVec: Computes a - b and puts it into c. */ void SubVec(int a[3], int b[3], int c[3]); /** \brief Volumei returns the volume of the tetrahedron determined by f and p. */ int Volumei(tFace f, tVertex p); /** \brief VolumeSign returns the sign of the volume of the tetrahedron determined by f and p. VolumeSign is +1 iff p is on the negative side of f, where the positive side is determined by the rh-rule. So the volume is positive if the ccw normal to f points outside the tetrahedron. The final fewer-multiplications form is due to Bob Williamson. */ int VolumeSign(tFace f, tVertex p); private: bool debug; bool check; int ScaleFactor; // Scale factor to be used when converting from floating point to integers and vice versa }; } // namespace AlignmentSubsystem } // namespace INDI libindi/libs/indibase/alignment/MathPlugin.cpp0000664000175000017500000000054713263645557020741 0ustar jasemjasem/*! * \file MathPlugin.cpp * * \author Roger James * \date 13th November 2013 * */ #include "MathPlugin.h" namespace INDI { namespace AlignmentSubsystem { bool MathPlugin::Initialise(InMemoryDatabase *pInMemoryDatabase) { MathPlugin::pInMemoryDatabase = pInMemoryDatabase; return true; } } // namespace AlignmentSubsystem } // namespace INDI libindi/libs/indibase/alignment/LoaderClient.cpp0000664000175000017500000000353013263645557021231 0ustar jasemjasem#include "LoaderClient.h" #include #include using namespace INDI::AlignmentSubsystem; LoaderClient::LoaderClient() : DeviceName("skywatcherAPIMount") { //ctor } LoaderClient::~LoaderClient() { //dtor } // Public methods void LoaderClient::Initialise(int argc, char *argv[]) { std::string HostName("localhost"); int Port = 7624; if (argc > 1) DeviceName = argv[1]; if (argc > 2) HostName = argv[2]; if (argc > 3) { std::istringstream Parameter(argv[3]); Parameter >> Port; } AlignmentSubsystemForClients::Initialise(DeviceName.c_str(), this); setServer(HostName.c_str(), Port); watchDevice(DeviceName.c_str()); connectServer(); setBLOBMode(B_ALSO, DeviceName.c_str(), nullptr); } void LoaderClient::Load() { AlignmentDatabaseEntry CurrentValues; AppendSyncPoint(CurrentValues); AppendSyncPoint(CurrentValues); AppendSyncPoint(CurrentValues); CurrentValues.ObservationJulianDate = 128; EditSyncPoint(2, CurrentValues); CurrentValues.ObservationJulianDate = 256; InsertSyncPoint(2, CurrentValues); DeleteSyncPoint(0); CurrentValues.PrivateData.reset(new unsigned char[50]); strcpy((char *)CurrentValues.PrivateData.get(), "This is a test BLOB"); CurrentValues.PrivateDataSize = strlen((char *)CurrentValues.PrivateData.get()) + 1; AppendSyncPoint(CurrentValues); } // Protected methods void LoaderClient::newBLOB(IBLOB *bp) { ProcessNewBLOB(bp); } void LoaderClient::newDevice(INDI::BaseDevice *dp) { ProcessNewDevice(dp); } void LoaderClient::newNumber(INumberVectorProperty *nvp) { ProcessNewNumber(nvp); } void LoaderClient::newProperty(INDI::Property *property) { ProcessNewProperty(property); } void LoaderClient::newSwitch(ISwitchVectorProperty *svp) { ProcessNewSwitch(svp); } libindi/libs/indibase/alignment/controlpanel2.png0000664000175000017500000007433013263645557021456 0ustar jasemjasem‰PNG  IHDRf’¹¡ÈEsBIT|dˆtEXtSoftwaregnome-screenshotï¿> IDATxœìÝw|eÀñßÌlO6=¤PBï½R”¢â vQ9{ïíìžåìž§žgOEADD¥ˆˆ EÞk ¤gûÌÜ» IH'Ôçûùì%Ù}çyßyg¹Ï<¾e”Û·™>ŸŸÏßïÃëõòàƒ÷#„B!„¢þíY}ÅEQQL«jÅâõzñú|x=î½÷.ÒÓRÃG(5 \³bJ Ö°Êr…+?ªÆñª,¨Ô.V +üV×xUG¨äÓÚ½]}ìz‰WúÀ£¬s¬ VŽ-Z5§^‡ØJmެù?¤Ú·¤ò yŒ=FéF{¬RQkðÿ5ŽUi˜cèÉZ}¡ºŸf5ßÐcü‡uÔáǯ–Öç—¥˜Yò?eß:¦xåÿ<†ˆf…¿V^a ãÕ쨔ªU¼Ä7±ÿËÅ6ë'Ø‘ˆf™¿ê)V…ŸÖc¼ÚÅ>¶S¬âßÒ1^ ³â\uŒUÑ›uXq¼jKÔ¨ºÖ7©†ÿ/V‹î3+ø­~âUòI½Ä*_¨ÿ–jQpÿ/‡QÝб,ÜMÇd¸ÿ¾»ˆÀëóÕ4¬B!„BˆZ(λè»We¢bÁ²lS“_øqq±˜õùŸ™„B!„BT*..ºÃP/îãnèö!„B!ÄŸ—ÊÀ}ÍØØ˜:oš&†i`áyÀµq3MEQJŽ ÿ4Ã-‹üT" HŠË)¬=ª0Ži‚¢ (  ¢i*Š¢ ªj¥q„B!„â·–——¨SBe˜¦n  é:ºn–$<š¦`Ñ4¬K…›8ÇP"?MÓ Ô1tÃ00L5’D©š†Õª¢†×€—KªL3²6Ü4jÇ‚ªihj8I“äìDbô „ 4§‡ÖÐí©Êï©­B!„â÷ÀRÛLÓÄÐM‚¡ ~ŸŸ1­Ó9©qÍãܤD;É,ô²#·€e{³™±e‡‹ÅrT "#\¡` @óæÍiÜ8ÄÄ$âããÈÉÉåðá,öîÝÇŽ;°ÛlX¬–ðÈGFÐPÂñB¡~€ôôtZ·nE\|<îèh ÉÍÍcëÖ­ìÙ³»Í6+š¾›–ä¬L]7AÓÐê¥û ‚~i]O¢c37‡ÅÊ"ë šðüžÚ*„B!~/”úšnwt ›¦‰aA’m*Ϝ҉ôh;¦¡ƒa`Šª‚ª¢¨û ýÜ»p‡ü:V›­$FñÏ@0ˆÕjeÈàÁ$$Ä¡ëºn`!TÕ‚¦…§ fgçòÝ‚ƒAlVk¹3“`0ˆ¦© 0””FFÅq²²³hÑbÁ › Í¢Õ>13uŠr (ÒA‹r“àÒPJ½gqEe)ÙáÙð’•ÅF\¢ ž¼ ƒ¥F)M³`wØq9,¨JuUÑ.ŸÇ‡×"¨‡c+š›ÝN´ËZO  ûÉ-²’˜–€¥(©{l3HnQKîšx=-ùÌ{ø&tà¶– lVÜw‹»ÓË®—µëÔV!„B!j¨  °vS Ó  ÑØ®ñÏ!qšA‚¹•–O±9ø÷©¸qþZê!,–ðˆ—a˜„t»ÍÎé§G³(†×©Åm  ¨ QÑNFœqsç~EHb±XQÕȈ[HGÓ4N?}86»•¢¢pÌâó:ÇÍ駟Η_Î% bS4M­y™:E…ñœóÒ+œÛöM{€»¿ÌEÓ#ï™ìúì<<}v·üEºÜÃÇ·´ÏR¹ñm¶‘ÄÐûŸ`|G7%ƒ,ÁBìÜÄŠ…ó˜ùÝVnv*ª«·«‚äC÷“hD¿1g1²'Z';Q0(ÊÜÆªïç2mίxmõœAòôî<üÞ5´'“©w=ÌEN\Ç:ZTÑcmL³ìãELoQ4Cx†+Ú;޼ÈgÏæ_X4ç3æ¬+ÂU*)>.jÒV!„B!j©VSMBÁ÷k¥(¿ß[õEùX.íј«¿ß†%2}à 0ø”˜f¯'¹¹=ò*^¦( ¡P«ÕFÿþ'ñÍ7ßaQÃkÎ Ã à÷sêiCPUðz¼ÕÄ aµZ8°_IœZ%fÕRhvöíÜzàažÿÁKôQ½kbb’Ž$e† ª5šÔÖ=ݺ'#NÍ£OÍbŸÕZ³*Jg®}æF'•þ@%*¥5úýÂG3×bÚ E¾ !UÃîtåÔPLo¡oÐ@$ÈÅe¢ &¦¢I(1(ÊÉ£HsoG3 |/Þ€N¨dÔÎFlœ«RMÝÕ21L 1 G’2ÃÕC“N¸¨S:M|,Ì&Ò 7_A³ZqE9pX”œß‘ó x|yƒ„ÌH—“hG#–B!„Bƒ˜™&„BAF¦FÑÈôÌÍà '@–7HÇĨ’²›rtøðÓŽŠóí· ƒ(J¸®&ÓÙ¿oín¨Í g²•yOÏ÷£Å¤ó—;.dýÝï±¹šš3šX‡‡-_NäÅÆÏðØ©n !cÊ»l+_™Y® F¿»7g÷°°cò‹üûÛ|¢]¬x9¼u™h8¬ B“–ãä¡a %±¬ ­rùßhó÷Ï<@H‰¥sï–¤[3€ß°aOhÉà+îDÙw/on3ÊŒ± r)AàC5ñëIœ<¬­-Aü!+5ü 6—U]·Y~(Ê,÷½¬ ï5gvïV-=Àù-ÓÁH”;‰Dk€‚<GlÚáú{ì¼ó#vTw~Û ìA“—þ†'! óu¢Û0ôªû‰÷ßÏ ?ûŽþ”o«B!„µ¹Í®þ®RAÁÐuš:Œ"¦aйQwŸ5˜çf}ÇØæq|º#[G ¢‹’Q€aè4‰rbèzx7EC':*Š@À®½Ü]mùç’Aø8]år…7LC'**Š@ P&†×ë-§˜®‡"?u\‘859÷ÚØûÅk|Ýé&þÚe·_ó+-«î‡ÃÇŽå»ÐOí„FªÃdëQ3E‹ŸóùËÐQ“ZЀƒ,ÿ%«ÓAxv¦‚Eµ„/°À;ˆñÃ/?ýû^\˜GÊÈ»xá²649ó\º}ñ ?• 4æ3ï‘»ys[£† -£èuJSÔ Û0J³L>yìA>Þ§£jvb¢5_©ã¾›77Ñ6´¸AÜZãºKŸkÕ ¥IOF I¿‘·ƒ^{u+ѱn¢zsûÐ*±ã>f{^õççÄøá‰À>>¼÷1¦ïÔi4ì>þuUkzŽîEì È>jMdeB!„âØÔ|óÈf.3X’P¹‡hç2î¤.¼»x—öïJ'#›P~ö‘c‡,3"ÛÜO9T…%K~¬´êþýû–kwh¦‰ ‘Ä+ –Ä)>¥ª…ÿ¶Û#qj3ÔQ“©ŒÁL¾þ×´~þNé3ì…¥ W4|nªR²†©âV™åέ‚²lDa†B¨Í;ÓÀ»–/–eãp[9°x>[.kC;k3:§h,Û[ú(›’ÇÆ9Ð2ž¨Äh4älþ¤âˆvàÔS/×V»; —áÅ—^ÛºÃçVæÚ”;©ôóŸdÊù¥ßñòÓghzÑýü}dkÜeJÛpÙÊ÷iç§ëÞ…fá¸ð™7¹°ô!ñ‰SM²)먶 !„BQK5žÊ¨šái…^?v=ü0çbvíã“ y\|rWf,ÿ•æíbé{du•¢ªz}ái‰j8Ž×ãÃŒÜÌ+ŠÊ!§”¬++V¼ÞÌëõb˜áQ2¯'‡HYŸÏ¢˜€‚ªIÌ*Úß0 #<êä÷û#£rµë°Jf–åû…·ÿ9ŸŽFË® Uo‚¡ëø¼N:õoÞ#7û}G·ß,?eNQ1²w“ÄЈžâùø›|¬ U Tê†J™} Ã]W³1Å$ÐKê*›4*¨J©6U2ÇÓ¬kÝL¬è¸`Q»7ýÂÂ/f³Ä1žWG¶Æ¦ï替f½¿ gON³H½ÕžŸb‚ªD6#9Ì_þDf©LÔ(ØLªVø%¼L!„B‹OeD1QUÍ>:i†¥ÚRâ¥MÜ<ì$:Û´q2ÏÍY­m£iÞYPU¬l-ô£FveTU•œœ\Üî(LÓ@UU<ªzdÅ’Ñ5Ã(y)ŠJAAA$Nxc¼¼üHUU ï|hD~œ]$s…ôH…ÂÂü’ò5Wòš6O畹ÝyüŒ„*‹>/Eަô¹àRn<9¼³Äï¾e§YÑn…å2 UEË]ÂsYshyÙ­\œÌgËw“²›Ú–ö²X±òú¾_ØM7Z::3²O/.Ê#uäPZ„v³î`¨f; )Ò-Ž–éü{òñ[¸6Q4µ†u›è€‹äD;¡A°¨eb•V¼ùGHQÐ,VE%qT£ðf+»¾å£Ù‹8lï@¯‹‡Ó¬Ì¦%UQÑ37±Ÿ®4!gæR¦ÏÝI‘¡`I"Î,ÀgQQ‚ÕµU!„BˆÚ©ù®Œ†ÍjeÑá":%‡§Ç¸œ.nÖžƒ¦•;Ž»Îè‡ýàvÌ'|°fãÛ¬6« ‹ÅBVÖ!¢¢œèºŽ¢¨ª¦©G%fáE‡_‹…ƒbÕ,Æ ?Û –.ý©Âs…‚†YÇb±Ör Zu[yËâÐÙ2í-¾é{7Cã*Ž–~þ“|x~Ù÷eµ_E-—[Ý5 –Жm5èÑÖF¯Û_f²'ˆÕ»ˆïœÂöòσÆÄT4lÕÖ­a5³ùôil¡û­OrÛ3÷ó¯M:.Kå“;U‡ƒ8{¸Ñf(@Ζ’Ft‹ËxþùAìõ8i^ERvôù©X²3eÉÜÝßM×Ëå½ËCM V%À²§oã¥-&š^][…B!„¨Oe é&V›•oó5FÅi¢¨¡éN<ûJÖ”é¹Y´Òu°žªÕÆŠB“…ÑÑá4‹ÅFvv‰IIØmÖð(–ª éZ$1;²nG7tL#<–“KnN.W4(GÇ é&Ý»wCÓÔÈ´ÆðÄ9ây¼^TE¡ÈãápÖaœQ. ݨü¤+PáŽ|–Ópùàíåœ|GoŠwYW” ¹™¹„ÅÙàПǞ­ëùñ»¹|¾d†Ó†ƒ`Eu•¯Lµ`Éü’ÇîÜËèq#Ò³iQ`âÍÚɯ+ÖSdÑДßý;Oç\Ê%§w§™[AÏÛÉÒÙSyçË=hv•@ÅO(EÁj+`ñkoÐôº Þ!‡ËJa¦U ?çîèö*hŽjêvX°è‡ùêÕ÷izíú7’DA+s¾UYQ6}À?Þ׸ò¬^´HoA;>û¶¯c—¯’éŒåÎÏâð³êÍÇxzß…\0´-ã­XÂÌí0,hf›µú¶ !„BQÊÀ}M›­f3Ö4 †n,à±”"bÌ`ɶùQ-Vò+÷pã³Ga<4Ù0L‚ºaЪUó’²Òƒ.óhUÅÐM¶nÝ‚f±`³ÚQU¥$Žaš´lÙE 앎®Ï?MUÀTضm  jJø ýø PíÜv¥Ì{šÃA´­Ô:¹`€¯Ž©hDEÛ°(&o¨T½‘çµYmìVµTQQ]3 €?D dD®^Û§Y4v š`òñttPT¬6+N{¤NS§¨ @[TxSÝï£Ðo‚f#&*ò€eCÇë (yˆ´—ËŠ…£/ÕªëL=„×"¨ƒ=Êã¨Ý«ë=ÄëÔQÒ·g8ÁªÑù•ok¤/m[øAÕ5j«B!„5ÉYqÂTV›`Ð=àãê˜|zÙý`Dv;Œ<ZQP5VøíLÌÁosb·‡ŸµU<Ï0Âpè¡ð³È\.W¥Ï1óx<ìßMS±;e6ö0 “€ß®¤§§ât:KÖ”¡D¶Û×Ù³g/†a`µZ#Ó(eú™B!„¢áƒ‘ÄÌb±T_º»ÝN(Âëõq’µÁN/­,AܪA¡²5de×ɲ`4.§K¹Ä¯$93M‚Á~ŸŸèèh¢ÝQ8EÅ4 |>…Eb·9°Ú¬%‰Té眕ŽCTtT84PLE…Eäååc³Û°¨A]‡JvoB!„BˆßJ(*NÌj?Ëbµ ªBÁ Á`ÃÐ1 U O³Z­X­ÖJŸâ¤Ê4ÃÓ¡ z0^SI˜4UC³Z°Y¬(ªJq¨ò[ê—Ä1uÁ¡`(2}1¼“£ªªX,V›C7Ð IÊ„B!„'†PH¯ùæå!EGÓ,8ÎR›mÔLñˆ—ª*˜ŠŠ]µcZ‹Ÿ}f–Ä*Þ ¤ô:´Êↆݦa·ZK&2†g5è¥×ÃÕþfB!„Bq<Ôx»üŠ„Ÿ3 XùþB!„B!ªQóL !„B!„8.ê<•Q!„B!Dý8¦©ŒB!„B!Žà‹/æ6t;„B!„âOiÔ¨3PºB!„Bñg'‰™B!„B40IÌ„B!„¢Ib&„B!„ L3!„B!„h`’˜ !„B!D“ÄL!„B!˜$fB!„BÑÀ$1B!„Bˆf©í9ÙYìÙ½‹¢ÂB Ã8m€èh7M3šŸØÐMB!„⸪Ub–“Å–MéÜ­'‰IɨªŠr¼Z&þÔtÃàpÖ!Ö®þ™ÖmÛŸÔÐMB!„⸩Ub¶kÇ:wëIr£LÓ ¿ŽWËÄŸš¢($7J¡S×lݼ^3!„Bñ‡V«Ä¬¨¨ÄÄ$L™Â(~¦i’””Ìš•˺)B!„BWµ^c¦jš¬-¿EUåû&„B!þðj˜ašá—¿E‘UŒB!„â¯Ö‰™ ÈÊ2ñ[‘ošB!„ø3¨ýˆÈݲB!„BÔ£:$ffFÌÌ‚5LúçLÔóïa|[{í«m@Áý ˜6;›>ŸC[—L­ûmÉB!„|jm(^bVÛ—áÛÏšŸ×³ÏcÔ9F}¼ —/gS^ÍÛØý-Ÿ|ñ‚ ×î?Ê+˜³Ÿ\ÇáPÍB!„â®Ö‰P÷»òc9¶ž^¡½sxñ©7XY›,ëϻڗAáÏ/rÙØ±Üð¿íË}Ø:™[ÏËØ±‘×yã¹îÞg™¼`;^#\Æ¿þU®{ ïnõƒibúŠ{ÆŽåºw6â« >ÏÚ7¸jìX\œù›õˆìï_㉗>g·_²2!„B!ŠývSñøúr¤î𷣏Üqk¹~ˆÅ-Åã²P4çcVu½ÝG¦L<òô(ú^sg6Uñçg±uÙ,¦½rks^ä‰1Ë•I(y@Öìwøfäß•¢©/´¹ïÌ#ˆÎ*@'ßBÝz_!„Bˆ?º:l—O÷Êùk?åßÿýŒŸöz@q’>èfþ~SÜ¥1Mü;fðèߦ<ûï<2ð¼ý#’îúöu£fÁÝMн¢CEè&áºôl~þx"“æ,gOD5éÃè˯fl÷x4 ¸ã}n¹{!½Ÿ~•+[Û±¾˜ /kÜûÞ£ôqÙûå«üsÖv,$h±­tÁõ\5,Gq®XÆßÇŸþ=élž{e<-Šû±¢ï›B!„põ»]¾o=“^ø /çþÚãæ@ —b–u e-â_ŸBö€;yú¼ÖD=§0ÙN¼};áü{f[(…Ó;·¦óf³·¬#3؉fVœëÈæk·å£7O@ ìcÕV?w$^3Q“zqÎuˆ‰sÜ¿”ß˜Ê “;òú "ÉN4ýoþçfXA±“¨b÷ðÙ“39³3¹ö^º$B^¶BSgqnG×s.g@cž­_óÞ”—y.1ƒœÓ‹éá×÷æ¹…ÉœsÍ#œ”XÀÊþÍ»Oý‡´Wo¥§=MKWr í|n¿¾1†3#Ó,bݬùäwº‚áûÐmp,ΚǮ¡ÓÜz¤Ï)é½È_Ч]…—€i–+cÈÏ#`mϸ+¬¼õÆ,û8#iÚË×ü€zÊ­œ›õ o.$„‰Õô²ñý‡yês8þ®È€ß¾Ïä'Á÷Ô3Œoã8jT®t{¿éänYÍ6W&Ü}-…ìXô!ïÿçi\-^fBkKøl´Ž\ýØÚ9@±Å‘f©|ôLò2!„BñgP¿Ûå 8ì˜V]éÒ¦)VZÑ&RÞˆcä®äíÿ¼ÆÚ¶×òôÕ'¯j:ýzÇ3kévû:ÑÖ"kÝ b:Ó½Q4û¶Åºx%›òÆÑ,¡ˆ-Ë÷ö,ÛŽ÷ÔYëø%;†®=S±š@L[NêiSëfX×~ÇC×s8ØF&€•˜´fd4³•4Ý·þS>ÙÃéßÁÅíeNËc$Óc@?zÆ(Ð¥%Êš¼¼äWòÎjLBÁÏLû:‡N×?ÉÅâQ€–×d³ìæ)|³ÅGÏŽá8®f=èÝ­%µš`äüÌÌe&½îêEœâÀ=b8é_Íáó á¦ÎÎRým¢‚}:¾üL6ÿø)ï¬Ñ‰=õ$[K]0 Š{0MHé5‚QÉ÷0cö6N½¢5úú™ÌÚÓŒsîìJ£©v‚‡rñàÈ_É´9™¤_ð"7œÕ+ЭC:¾mw1cê*Æü­Îr£re¾ ¥ßs·¤GÏn¤iЭ½›m+ž`ÅòL.mÕ8\F‹"¥i¥»Y20!„Bñ'V¿kÌ¢:1î¬æ<>å.n^}*#FdXïfD©Å£l~–¿ö2¸ñÀCH±G²‘1¤ s—²p×xÚ´ö°ie&®NhfG»´ÖÞdéæBNë¶¥»’qA:‹gýÈNwR7,c_TW®hfÃÄÀ»ó[þ7és–mÞOnЊSñC‚Ÿ Yºí¥qxÓVЬ-éá8êüJ ™Šät7¬É¦P7‰:¸½ºNî«×rþ«å:8Û‹QQ t.ýœõQýy´ƒ 0ÑÒNaT›™<û.ëÔ·RÜwÙÌyhBdú%@-‡\ÍuWtÀ‰¿ÜÈ•7Ïv7{N?¯ ŸOü˜c®Ä÷Ñ÷„úÜÆT+ÜvØ™ƒÇ0qü•]z'uMÂRÜFk#ºvŒá£Ÿ%3Ø—Œ£úÎ,w^ŒíYh¿d¡Ë3!„B!*T·³Ê(Qt¸ä)&\μÏfðÉów3½Ë<}ßHÒPÉØÏÂE¼ñnžº®/ñ‘…T¶fC”<‡ï¾ÝÁ%©ùü°ÃB‡³[â”˜Ž j©óÞ¢ÍdÇýÄFKîèß…ÓßcÙîlÚ.ÞŽ­óXZ:ï&=ñ& Å5wŸLswÍ=ËvVÝt³V;*¨L#2˜ebâ䤛æÂ–¶2åì 1(T&¸…_nGÏÙÎCãç•ýLÍtûâ IDATŠœ^ I(Þ8ÓÍÉ×ÝÆ¨¦Nì®X’S‰²TöL5ožìQØT•ø>ãüþ£|8i2 ñ ÿG7Ü äDÛ1ýùøŒšž¶†‚NP¯E_)*š †! –B!„•©CbVÝ–5¢3úò—›Oâ´¾/rËó³ùvÏ0.vXItWöÄßž{‰§’žàñó[ãTkS†ŽÌà³ésXÑ!Ä¥7µ צÆÓã´–üwÒ\¾qìFïpM3°ySç}ÃŽMVºÝÑ çocsž•®7Žep'ÄHu@$1S¬N¬ø)ð—ÎF,Ä·j†#¸‘å;½t/7•±:¶ä¶¤+óر[¡Ñ)M°•/¬ø¸À®ù|³?ž¡·ßÉðR»&šEkyë‰ÿ1çÇC ™R\ ÍÛÒ®åQÑ+`à/ðÍ…M­}fS¾šü#jûk9#à èØ\6ðà3ÀÖ¨Mµ¯X·æÁ6ᩌ²æ×|,Í:ÐÈ ZtÑä³gŸ³½ÚoÂÑT,.+‹ð†Lªÿ.QÃ2B!„Bü¾Õí9f• îaÁçóYþë¶n^Ǫuðá"ÞUªE%¾Ï•üm|öNIk "“Õ4RœM×à¼ñÖrètí£‹oÊUâ{ §]`/òÑyH+œj]NkAö·Ÿ°ÎÒ“aíÂIœ“AFTµŸ~ÊÂ5›Øºm+;²ü%Õ[ZÓÂåá§?eñê5ü´`>«ë¸Úÿ…ÑMrøêÙç™Ë¿gýÈš5Kùìõgùh*#ÏïŽ[5® ƒ[)lœü:S¿[Îê5«ùySn-¦Ý´‰æz>šö-+W¯`Ѽɬ$qB!„âÏ¢Ö#fJ¦ç ›}À7“ 0kb[N»î†5Òàpé’6šŽº…+WÞΛ¯}@ÿ—®¡K”‚ß›qCâyè«Gv¢Ô£¼Pã{1ª»õ{1¬MxD+®ÛpÚkØ7pí\‘‚ÎŽ\q×øÞúœWŸ˜n³=–ÆÝáPg'.½~‡þ;“WžœÓ†1÷  [bsÆ=òÎw'3ë혥ƒ5±+¶êGÓj;%Š.}œ{âÞåïþËsŸê ¹iÖÿ úžÖ†¨Šúªh#_ýX@Ò°4)¿U¼G—aíÑþ5ŸïöœÅ¸êê?*¸O´Z¤•莌»±c©B*V— BEMp:iwé#Üçx‹ÉÓ_d±¢šôæ¼û¯âÜ6Žð¸•–Âð;ï ëõI|þús|¨ÎDšvë@B „fmþn»‹×?{“§ç‚«Ù0nëw)ÖŠ¿XU}ß„B!„ø£Pèk~ñÅÜ^²è;†M 8NÍ1Èúænû¤ ½x­ìÇ©ñ»a³ÙøúËÙô4¤¡›"„B!Äq1jÔÇcYí黨vPÇ8¸”÷'í¥Ë5wÐB’2È3!„BñgP¿»2Ö‰IáÚx⥕xéœtÞý\ß?¾ž¿ !„B!ĉ«^טœþ¾?mÚÔÊ"{ò}¼wrm["þ d™B!„ø3¨÷©Œ•'`BÔ…dfB!„âOf !„B!DS5­†ûœ !„B!„¨wš¦Õ>1“5?â·$ß7!„BñGW§ÄLÖüˆß–|ß„B!Ä›¦iX,2•Qü,Yô]C7A!„BˆjEGEÓ¼u[“5:Æ¢iXj?•QA‘Q ññû|œ1zLC7C!„Bˆjäçs8ëÖ­¡}§.ÄÄÄÖè8MÓ°h–º=cúðáCìܾ¢Â èS Q?¢£Ý4oÕ†ÄĤ†nÊqáózº B!„BTËb±˜´cǶ-tíÞ«FÇiKíGÌ œ”müu-»õ$1)UUe ­è†Áá¬C¬]ý3í:v&11¹¡›$„B!ÄŸ’¢(ØlVÜî®Îk̶oÙLçn=In”‚išáW­£ˆú ( ÉRèÔµ[7oÄL!„Bˆ¥ ªj­fÖi@QQ!‰‰I˜2…ñ„`š&IIɬY¹œüü<ìÛƒÏçÃ4ÿÜé²Ãá$­qé“zRܧ5/-„B!ª§Õ51P5MÖ–@”HV¾kÇ6š6kNl\<ªªþiw›7tƒ¼Üvnߊ¢(Ò'õ ¸OwíØF³æ-%9B!„¨'Ç”˜ašá—8!(‘'17išAl|<¡`ðO=:¤( ±ññX÷î!5½±ôI=(îSÃ4Ø¿w$fB!„õ䘞cf²²ìÄQ|%bbãƒúÑLÓ4 ƒø~é“zRܧ±±qìÞ¹£¡›#„Bñ‡a9–íò$/;ñ¨šJ(lèfœŠ1é“úc‹UF…B!êQ·Ë3eÄì7àÛ2צåsÆmè쪪däZDvÉ¥HŸÔ3éK!„BˆútLkÌ~ë%fÁ=ÓyàžÏIºé%îîw‚îß`P´k-ksRèÙ-ë1Ç3ñïÿ™ï—+œ¬WÝßfI^&kÿÊ«·>ѳX:åV¥ÇUCÓ8†±æß5Ir…B!ê—¦i¨u]cÉÎ*x‡¾âž±c¹îøŽúÜÀ³ö ®;–ç¹q®â¥ZHkÒ˜”Kµe+}éùlž÷OÞþW.;–±cÇqùMòʧëÈ×ë³ÌËÃÚ‰ñì‡ñõϬQ_—.gþÆ/#ÍæU+ÙR`”y?¸w]u//ËÃh€v¿êµOô6ÿ´‚õýás leÒ]WrëÛð4à9¯kX]¿ !„BˆúQç瘅U=•1”¿< kö;|3òïŒJ)UOhsß™GU€N4ÕµBm4˜Ûž\ªîZÒ³ùáµûyaaûŽæÒ1­I¶ø8¸}-òtT¥>&fÇ0«éšÇ+¹êxfÉêG4LW1ç“/X´j ½&JT í{ ᬿ §sBÍÇ‚‚ûçñÚ‹Kèñȳ´Œ¶ùÀGjã4âÜZðԨO"ôÌ~ä>¦fvåÆço§ol©±Y³øê†ã™ŠƒÄ´Æ¤%:QÇY¯ôVæ÷z¢B!„'¨cÜ.Ÿ*oÐB™à$Õµ™éÓ×3äºÎ¸”ð«¦òéŽhRì…*B7A#ľ/žá±ÿ­âp°'ÓåŒ Ütq50²¾âÞë'Ñ衉ÜÝÕÂÞ/_埳ְû`!A@‹mÅ  ®çªa8Žšçh’»ì¿¼¶°€Ž^àÁQ)¹ýì*gÿnØ¿d §ÌcM¦-¶%ýÏù+WŽnG´èY|?ñe>\¾ƒýy~@!ºY_ιúZÆ´>2½rÓ+\yÞ+8<ÂÄÛÚ“ûÝ[¼1ã'6îÍ#Ä÷¿Ÿ—n뉫p=Ÿ¿õ3~ØN¾a'µëp.¾ú"ú§ÚŽôs úûÈgFµ½[gò'g²=¦ÃÏþ+mYñîû•…s¦òì²_ÿèm O«íD̲ Ô’pý£Ê7®T×'Gø¶|Á—»-¸”5ÌX°^g§W0e1r®–4FÞñ(#KÞû½«éx˜ìn)„BQŸÂ‰™åxl—oÈÏ#`mϸ+¬¼õÆ,û8#iÚË×ü€zÊ­œ›õ o.$„‰•˜Žg0áî¿epxõ Þœú2o¶yûúÅ”‹ÒÉݲšmþ®L¸û4[ Ù±èCÞÿÏÓ¸Z¼Ì„ÖörÍÉáçÏ—ãKÅøaéX+l¹IþŠ7xàå%$ ¿‚{û¥àÝ0‡wß{˜'ŒxüìÆXBvþ²‘ì¦rÇM­pøöñÓôI¼ÿf¯ÝB¨HÔfñÈÍ=q+`‰NÅFˆÃk`m~g.¿ëtZ¸‚xm-p„v3ó‰Gøßáž\póÅ´µà‡©ïòâƒØ_ºžîâ>VªìíâëP<ƒ²R]Ìzc&Û†sߣÑ!*’NöêËÀyý¡7xâBºým(ÉZˆýß¼ÅĹkÙy°àHnÇÉ£/â‚Á8•âºr™÷صÌ )—<÷(ÃÔ~& ×ÂØbÓhÑë\®¿¤7ñTŽ3x忋ز?¯8Òé=b(‡–²pÅfùTbZ ä‚k/e`j$ye±âÓILýæ2}*îf}8ëòñœÞ* …û¿žÈs×±çP!@‹iÎÉù+ã‡4ÅQÅ5<=Y­´?…B!DýÑ,Çð3 Šÿ¸nPt؃élBJ¯ŒJ¾‡³·qê­Ñ×ÏdÖžfœsgWMµ<”‹ß§¢•Ñ‹¾‘1ìúîNæ­É$Ð7kE#Gî–ôèÙ4 ºµw³mŬXžÉ¥­š•ÝxßŖL-£3éÖJÚ­dñÔE¶»–g®J’ tiGbÎ-<òé 6¿‘Ž‘®r5íF¯n­°ÑNÉùù¾,Ù G‡H,GM›eP2 Îô‡ƶ¦W¯.¤Eâx×Nfƶ8NüVƵwÝ蔡²ë¦‰L]|ÝF$×|ĬXxž]¥v~˃¯M{WÙ²Z|/ΕÆò¾áǃ§pfªNþ¶_ØêëÈ%7 $ÍâaÏÏ_2ýÝ'Øø;÷ O‰ÅI×ÜÁÙM, Ú‰W ·xje8‹Ñ/âÕ'ÞeKó\~[gbóáóÿ½Ã“ÙOßÚŸ£ˆ=¿n!·ñ¹ÜxU þýüüÙT¦½¤ÑôÙkèê*då;ÿ拃ý˜p×µdؽdÖhd5«ï—jú¤˜ž¹„ÙÜ ~°;íÜ^Ú|9…ÙkÆÑþ¤˜ðh¨Yêb”YÛ9Çì%¼þøY—:„ oìIºÃæÏþËŒµ{𽈧šsŒRг7±vÊiWÝNïxƒÌŸ>⽓ùµÓ(.¼ú,B;øzò'ü÷?´àT’/>ø¯,IäÌËï¡gB!kf¾Ã/L"õÙké"û:vø;ré̓HÓŠØùÃ'L{ïe\Ížâ’–¶Ê¯ae}&‰™B!D½Š¬1«ëÞrU­zÒñæyÀîÆaoÂéçuáó‰³bÌ•ø>úžPŸÛ’jå€Û;sð&±Z€?|Ä»Ÿ,fýžl|š [,`¹[eë-ùËš@³xø%»³Üô33|©TÑêÀ>Öì…´qíˆU‹Ë8ÈèÝË×[Ù”¢CRñ=é‘Yâ3HÀC¶'Te+7o£ÈÚ’Þö’wÕøötO…™k÷ã‘T‹5k×\¾Œ's7ù$Ó¾©“£GáTâZµ ŠålË b¦*áÑtíÞ™ :wíHšÿ~^š5‡m§\NsÀÄBLJ:MšÚJê1Ê´'ÀîùŸ±VéÁ-7£g´´§…+“»^Á7{û0.-\ÚÙ¸#ݺ´ÀFGÚ%e±ú±ïùq¯Ÿ.­ýäæÑâ[Ò¹C UhÞš Ρâó®¾T€ó¿foêiÜØÔŠªödd×ù×?r¨÷0ƒGeû¸ìU ²wþ V¹î¶Ké§~¬?9˜‘W¶|¥çØÖ)G›®èàVèÐBaÍ’—9Ðq z¦ Ñžä쥬š¾Š=þ!$Wóé‚\ÚÿõÆö‹E2®ÈaÅ=Óùn»—®Õ’kØ¥['R4èÔ6šk^`åªLÎoÙ$’ó} +ï1ÉÌ„B!ꓦYPë¾ùGU ¼y>°GaSUâûŒc°s5NšÌ´ ñ ?·nEÁmÇôçã3 ¸óSž~i™­ÇrûãÏðì#W30±U**š FEó¬¬ñ4O}ïFÕ÷s†5 óšÞeÖhK½Èr¿J·Û3)÷^ÉïÑ´>©9ZÁ6¶çeïÑ+Še†‡=C“ž´r)‘Ï¢[ö ,6ìñ„çruYãš—œ"”xzŸ=ˆÄMïqïý/ñþ¼µdújv®5êÏV¾^’OËá}i¤†Ï³ã}qïœÏ÷ûB÷Eé÷ »7dA“>´Q*î‡êα¢¾ÔbHŽO¶ÝLèd7JÈCQÀ$xp3ûu ï`„+¹b•\yÏv 'Û_a¿b‰§i,xsŠcVq +ìÓæË.„Bñ‡pl›Tù$1l.l àhÍè3›òÕäQÛ_ËV@Çæ²¯Ÿ¾}¿²ŸVÜxÁPºÇ) ;iUǦ•§ÆÓcx{´7çòáøë”ä£w´¥Ñ9Vþ¼Ü1ÃSñ²sù6BQÝh_“~R±:­U€ß€ª·š´Ðº9®à¯,ßé£{ûðB)=g=+@úˆTl@(RÚ(ò«Tø3àÊãæ6ìö0:Õ].¢NÎÖ‘@óx ÓÔÜÓ›f©™má‘4Ã0Jê2Ë=ĹÌhRɱ‘ßKÅ,.\ò~©ßMEEÁÄ0MLS!¦ûå<ùâ –Íÿ’ÙÓ^âëÏ{sÍC×prBÅk¡ŠU×'`R¸~?†¼{7}·ì§ ¾ÛÅÈ‹[`+i»Yr¾GÚl €ª¡”.Wºÿ¨î‹û°tŸ©X50t£äE³ Ä0ŠÛà¤×UwqnFé ¼*öx7˜Á ®¡‚ª‚i˜e®[ùkXU !„Bˆú£iǺƬ2fGk$1C#õÔ+8×B¬§ŸL¢  `sYÁ1s¤¶%™Ï™õñ7Ä jA¬šÅo}5H#é”k¿ø~Þ}õnZw6C»go ’·o r;pñ%=tá@>yæ-ž}Kçü~ðnø’I_ÐêÒsÂLT;Úf#µcLþœ©ó’œ ³° § J©°´³ý¹Œi±„)/ü“¸ËGÐΑÉSßesÌ)Ü?0 ‹;;kYüý&: mK¼µêGk†Qų¥É)ôO\ÄÜé³ÙØá<ÚºŽÄ3ræ“/öAóó铤bÁ’„Ê0 ÀÏî5»1\hêV0ƒ¬(ôêeê5ŒHâa¦ƒô6 °`%[ úÒ#:¼™IÁ–•ì#‘¡i6L#2ùÑ ·ß œ8Pê=ÕÝœ~c®£ÏÀ…<÷·÷™³â\ú M¢ªÔ¬º>ÁÈeÕWkÑ[ÇݵÅYòNæü×ysé7lóW:X‰©è†VºÍ¦FMܰb5;ŠúÐ٥Ƒ„Ô00¨þüi` TÐ/áS ÇÔZª|ÇÎ=& }S)¿Ù}8y.w ‹¯~OÑ*¾†Uõ§B!„¨?Çô3¥ªüÀôã €ã@‹”S¢;2îÆŽ¥ ©X]6Q4±6?—;'dóæ'oóôW:6w íÒ£ª¼é®1kcFýíRf}ÈŒùŸñŸo½˜€=¾û·"`*$÷¼–'ná¿SÞçÙy~Ô˜æœ|Ù£üuTcj¶y¼Fê°ë¹lÃ+L}û9a'­ÿUôè_qb†µ c|ËÛï0ã_O1Í´“Òe·Ýy!=ÜáŽs¶ÇE}vðÁûÿcMßÇ_q¨’ëa˜f7ÎÖ&œyÕH~}vÏ©£†ÑÂf ÅeÐÌéeåÌÙ, µÅ–Ÿƒ­}?:·Á40MM†Œ¦ã·“ùïëÓ¹hDGÜùë˜óájè|9CÒµ# Gê-óÓ(bû¢%ìÏ Í­P´s+‡ ·%\GU—¥š>1²WòÍf…¶Wõ¡m³˜2£ˆ©Cz¿ä{æo, ]g;q.ÈY÷#¿ìK¦Gré6[hvÚ`Ò}Îĉ)\8´-1žm,üµ,fä\ª9G³ô¤i–ހà !„BÔ¯ÈTFÙ.ÿdÃÆM Ý„Nn¾ôÉñ°`þW Ý!„BˆFtT4Í[·%11©ÖÇjš…:geŠ¢ È¨Ù ÃïóqÆè1 Ý !„B!þt òó9œuˆ ëÖоS\NW­cÓpÙŠŸ~8–Ã…B!„â£U›vìØ¶…ŽºÖúØcžÇص{¯c !„B!„¿kkV­ÀíŽÁSTT§ãÕznB!„Bü)©ªZç¬%1B!„Bˆ&‰™B!„B40IÌ„B!„¢Ib&„B!„ L3!„B!„h`’˜ !„B!D“ÄL!„B!˜$fB!„BÑÀ$1B!„Bˆ&‰™B!„B40IÌ„B!„¢Ib&„B!„ L3!„B!„h`’˜ !„B!D“ÄL!„B!˜$fB!„BÑÀ$1B!„Bˆ&‰™B!„B40IÌ„B!„¢YºBTÄ4Mróò(,,B×uLÓlè&ý.X,îh7111¨ªR£cLÓ$óÀ>gú?{÷gWYàü{ïôt¤PBB¨!4QQ{];ÖE]Å.þ, ®®ÝÕµ»«€u] ®®("àêÂ*½…^CI#™L¹çüþ˜’I„òx¿_¯Kfn=÷™ ¯ûÉsÎs200`¬D™:m«l½ÍÌ þùÂŒÍN]×¹ñ¦›ÓÕÕ•9s椻»;†±÷¥®ë¬^½:7ÝtSn¾ùæÌœ9ã>Ç­®ë\zÉEéhkfæ¬íÒÕÕ•ÆZÁ0öëzÌ÷âíž5’‘¸3\uêô÷õç¶Å·fÙÒ‹²ó®»û½F 36;K–.M{{{vÜqÇÒ›ò°Òh4ÒÓÓ“¹sçæÊ+¯Ì’¥K3u‹-îõ17ßtcšdæìíÒÖÖžf[3$Fstæ¬1ôä©S§q·P{4[;ªêºN£:Ãßål#©«:?~Ç\sõ¢ÜróM™1sV‰ 6Cš0;ÿ¯ç–ÞÆØs¯}ïñ¶%K–fþüùáÖ<òÌœ93W\~ù}†Ùâ[oÎvÛÏI[[{ÚÚ;ÒÞÞ–dM\ •Æð Ðpt¬ 4a–¬ ²ÔCC5zýÐChU4ëÔÍFR'³fo—Ë/»X˜£5a–Ü{ ðй¯HHOOÏC´5L===¼Ïû­^½zt÷Åöf3f3ÍFc82ÆÎ‘ Ï5Fçƒbí ‘0m¤‘Fªº ³º®ÓÌðfÒ¨‡Æ¬®S7’qãÆ¥¯¯¯Ø–›ŸÍ>̳øÖÅY²dIzW¯N’ôtwgÊÔ-²õV[¥½}³ l¤ªª{sÙœ• IDAT?5´Z­û¼_] OóTuZuŽ:©‡‡~ä0©5ù±vŠ<Ú ù;c–4†¯Ú•±>Þ¬®×DÙÈýªVUjÓ€ÍÐf]5Ë–-Ë¢EWù€9ô/ô«z{³êÆÞÜzË­™;wÇLž<¹ÜFò€«*X2ŽU] C’¤N«®Ò¬“¡¿kUFvjé·‘jšK{t«Fµ^ªÍ4†¯¯‡Ç­N£ÑH=<–C6ô ªö{¬±Ù†Ù²eËrùåW$i¤½£;&LJGGg’d` ?wݵ<ƒ}¹âŠ+3§y™ô(³Áe×ä¢+z³ÝÞ»f‹¶á+ë»ò_~w>Ý“óá^í;JnáÆ{t‡Y••מŸóïÜ&ûî½M:ïÏ3mÀ8ÖU5eC“:u†Z­ÊH˜¥ÑH]Õi4ê4kv_|tÏ—­14c6]U2´ëbªÔÃ!6”½kfÌõðõêßs`]›e˜ fÑ¢«’4ÒÙ5.“&MÉ„ ãÒÓÓ$éí]ööŽ,_¾4ý}«råUWgÏ{lÒnõêrÎOœ_œy~®^ÒŸ¤;[Î]'>ûè~⩹v«#òî÷½(;{þn„º®ÓH••wޔŽ[d»ÙÒL†–I޶ÆðnŸ#»ŽÖu=4öÃ6Ë0[|ëâ¡Ýíé韞žîŒ?.“&MH’4›Í´ZUƧ¿¿/­ÁÁ,^¼83gÎܨשïº8ßûÈÇrÚÓ²çÓž›×Íß:=KrÝ¥—dÙ`óừV£'ó_øþœXz;6Ѧ»Tgõ•?ÊÞõÝ\6n·ö¼7gá¶S-½!—\pcV?|¢›dCÆqtwºáÝïR'UkÌ,Ú𘵖ü)_øð7rÞ’5 Š´õL˶ó÷Ìãžúô²çVÃYeÅUçåŠÞ$×þ_.]òÜÌïÞ,ÿ7ó€©ë¾,úÞ»ó¡ÿ^š¶=ßž¯¼cÏô ¯^9v6­‘Fê†]€õÛ,?1-[¶,F#míikkK[[3Ífsô_˜›ÍfÚÚšikkK{{gZƒ­,_¶|ã¬îÍ¥?øçœvã¶y·?˜çÏëýØþØ'¶æ~­%ùë|;'ýê¼Ü¸*?kßþÒWæÙ ·H[’ú® rÒç¿›³¯º9KW×Iº³õÂÃòŠcž—½¶hKZwäœo)?:÷Úܲ¼?I#fï—g¼æ59jþ„Ñ…µW-úMþõ;?ÍÙW-K«szv?ä%yÝ “éÃ<õ]WäWßýn~vÎUYÚjdÜì'åÍxef'Iÿ¹ùÄk^6tÇiGæcŸ~nVó-ùÈUÏÈg>~T¶i{ÞÇCh“gÌú¯Í©ÿôÝ\6ùð|øóoÊ>“Ö„Ø“óüË/ÊO¿úµœúÇ«²¬êÎŒ½ÏËßttž0chÇÁ¾+OÎÇ?ÿ›\zýíYÙJÒ¹eö8ìUyË«ʌΤ^ñùæ'¾–3/¿1w®®“ôdƾÏÈëÞò’ì7ux¬êÕ¹þôoçË'ýw.¼­?íSwÎS^ö–sÈvó˜uV_óßùÖW¿Ÿß^¸8ýéΌǿ!'wÀÐã/ýt^öÌO'IÆtbþõ{¦{#‡cƒwe¬ë¡²ªN•VšÍ¡]ÇžPº\™;F£lhÑüVï¹æo¿Ë5û]Nò[ò¾£÷Êäf2i¿Wä5·œ‘›¶|\ž4½™Ö#|—½º®Ò]Èch,[õÐøÕÃǘ ÍÂÇØ8›e˜ VUÚÚÚGW=«ª*éíú~```ôCçPÀudpV [Kïeùå—¥ç€×çȹ=ëŸK©WçòS>šOž–ø’·äèí’ëÎ8%'òÄô}ø„¼d^wê¾[sñÅ7gò3ߘ×í6!ƒ·_˜_~ï?ó™¯ÌÈçÞóÄL­Væº ¯È’m_·¹cºúnι?9)§|ª=Û~îMÙk|#­Å§ç3'|7·?æeyûÑsÓvýïòo}>ŸžøÉœðÌ™i¸)¿øä 9yñnyÖkޑݧÖY¶´™m{C3m»æÕ8:ó»“FÇälÓž,z ßÇC¸2ú¦†Yßտ̯nlf·ÿ]öžt³c×çß?øÞüëûå¥oEvé¾9üÞ7òï\‘î/¿5ûMjdðŽ‹rÞ5Í<ý-Ì[ÖYrɯò­S>•MÛ1Ÿ}þì4Wßœ ο1SžÿŽ»çÄ .þkþã?̉Ÿž™o|ô©™Ö¬²ôœÏç]Ÿ»0»ýŽ|bá„,>óÛùÂþ_º¶ûJ^·swZ·ÿ.ÿøÎ/äÂퟑ׼ÿÌl_‘ÛûçdÊÈßȎΉÿ°&5“¶ Û¤ëAÇj$Àê*UÕJ[£m(ÖªzôôeC—‘皘'¿ÿóʹé_rUÎ>õùÎÙ·åÖß=ßÛóãyÓ^2pãéùÑ/ÿåí·g×Ǿ5{ö æŽóþ#ßþáïsáâÕI£+“·Ý?¯ø‡£³ÏäfRÝ•+wj~ø_Îå·÷%í“3ï7æ/œ—žº?‹ÏûyNþÉïó·›V¥îšž]d^ò¼ÇgÛžFR÷æŠÿøz¾÷§«rÓmwe Iº¶Ì.f^þÂÇefg#ƒ·•ûæå‚ëoÉ’Þ*I3·Ý;‡½øe9r׉ÃK Õé½îùá)¿ÈY—ÝžþÆøÌZxp^ø²£²pêðå¶óí/Øvô€»ÖùŸÍ^™$ÝÙÿ¸Êwí»á…SªºJÛðqzveÆÚ,ì³£#4Ò×_epp0ýýi6›Н‘ëÓjUéêlOGÇÆ½•Á¥×åæþdƳïñ8¬zÅ_óã_/ÎŒçÿc^ä¬t$Ùs—é»ú½ùÏSÏÏQïÚ?C;Wvd«ÝöÉ^ º“ìžm{ÏÏ?|ÿì\ÝûÄL^½aÜìÙ{áŽéÌ‚ì6ý¶üßÎÌŸnèÏ^;7ríý4O8<}íÓ2·3ÉüÙyõ¥ÉÇþxnn;rf¶¸ò?óÓEsÈßší)»fŸÉ¿ÿ톬~ú–ú¼\§®[©ªF’Îl5{Rò×Û³b •)ÕÈB­áçhdÂìééh-É’U©rC.¸9Yuí‡òwg®½m7/Kky®¾pq²Ý‹³Óø*k÷Sk4’ªª•j‹»ûyß8cV®ب£KçK®NU =÷Ø¡¯ëjÍs·MËn;ÏÏn[™ÁÛoȲ…™6fùýª®Ò¿ì†Ü^'Éù»w—ƒ§·§n &mUoÿŸ|ÿ¿nIÒ‘]^ôîû”Ùéªû²ªÕ™fÿ ùÍ©CQ6å oÎñ/Û#×üg>ö‰Óró?ËÏ=.¯š·&·~Öóá#§æÆŸ|$=íöÜ~îÿæægm›­G¶§}¼õÓŸÝûþ’/¼ï›¹pàÒüñʕٟö\þ³ŸæÒ¤gŸ×å„×í“I«/ËIúl~¿øÌüfÑyõ–¾çí¬úGǧm×ä„·,HOÖ,¨ÒÞ}qt|‡ÎGð(_}X׿f“&&+îJWWGúú†N*Ýj ¦Ù:~§ªZéïH_ßêtvv¤««+&Þ=-îMÛ¤ÙÙª=YtÙÍé;x‹>~çÞ4šmiŸ¹h½Öº}øCã¬çæýÇ>6SÆöOû„LïHn+´ËÓ}¾ÉH€­û纷¯«k«Ù™˜?ä’ëîJkö¤õw5z©¡YŸ‘««11T­yݪ³:f3©[©ª*U] ¯ß°&PêF[š©2Xó™t¿ð°ì±Mgúî\‘É —9³çgjý«üûÏH×¾“r×âVv:hÏuÞêCñ>8kŽ1[ó!väû{Õ5?/|ã!9ïc?Éû»>Ï8âÀì¼ÍødÕm¹ö’kÒ}è«òŒŸ›gïxVNþÔg3啇gçî[sö÷¿Ë'=)ï}–iŽž8Ãç÷JFV×[³MÃ7½~ìcš[ç çïŸÿÜ·sÂçWçÍË”æÊÜzýêìrøÁÙ¶³#;ñœìúëoæs'~=/}æc2{Ü`–.iÏîO\­wŸ™üÛæ”_OÏ“·êË­wm›ƒÚn­@Ö‹‘²±»4n讌£Û_7Fßk=\UÖìÞ8úÚU+U«?+o½4gþä¤üni’ŒÏ¾Ož“®ª•jÍXTU+ÕÀª,ˬ<þyoÈãº.¿üä§óËŷ漿ݚóc¦çâÜÒwq~{îÍ™sÀVéÈ`z{ët÷ÌÌNS“«ï\šÿ=ó’ú¢ÝÒqÝ99ûæ$—ív˜˜fÕ·föqd÷ÏѱÙuížÑº®•ª91ÛÏìJ÷¥oEGæ?éˆì±E{UoΔéí©Úï¾Vz{ëtõ$ã‡~:Õí×äöþÝ2«­J_ÚÒÞHÒý…I£mÍÏήŒÀX›e˜%IGGG& /ø±ë.;¥ÙÖ–¾¾¡™­®®ÎT­VÒÑÞ–fÛ¦-çÞ6íñ9öSó›ÿ$¿ùïïæœ»ZI:2iæNÙ÷é­TîÌÉòΞïääŸ|>g­JÆÍÚ'Ï{׫òìyÝCð?ïµmëÃò®µå¤ïý_ýÅ7òÉß$ã¶}JŽ}삵Wñ{ˆÞÇån3 ëovo6aákò‰ÍÍxZÎøîòŸƒIÚÆgë¹{ç°þþ´ÚfåïûPšßúv~ú…óƒº+[/8'ûý|çÇ?Í—Î^•:™2÷)yÓ“ʬöFÓÍ;?ÚÈ÷¾ó“œô™_§?m™4ïȼû±{fîSÞ£/ùB~øÍÌ™éÊŒ_›…›-×ók>ö+ÆFÙϘÕk‡X=üu5´’àÈŠŒÕhœ­È?}\þ¸Ö³´e»C_çÎ_ÿê¦ý×ÿ<ŸøôYé05[ô 採k2m› 霾ŽXøÛ|ëo½¹ðäó¶ïw¤­H5çeùØ?ì›'=cAÎü× ²ìì¯å=g¯ùMmnhŸÓ•uºÙ4=Ùéð§föß~™n?3ÿòÁ3ÓìhK5ÐJ¶yFþß{ÉV÷¸/͉oÛ/SwÞ)O»-+nûUN<îŒtWƒ™ùÒæíûONcÌϦ®†w‹ÍüCð¨²Ù†Y’ás˜ }íhoϸqã’$ýý¨«tumì‚ëy)»æð×îšÃ_{Ow˜š½ŸÿöìýüõßÜœvH>vÒ!k]×½àmùöI#ßm—}î{yјÛ“̇N:pì5·ã!9惇ä˜{ÚŒI»æ¨74G½yÝ[:²û Þ“/¼`«ßðÕœü€¾‡ÎšØ©îfòavÜœ'çåï~r^¾žÛêªJÆÏÏ‘Ç~OGfxtÞ{àÑënÁè NÏOÍ1zê:?ó*uvÈoÿLŽXçúõuÖØk4£—¶¶¶˜13¾Ã9ôõȹ¸’4ÆeË©¹áÎÑaÓ>~z¶·{öÒÁyÜÜIi««TuÖž±ªZlõdëiݹòŽ;³ø®¤9aFö|Â3ò‚…R'Ùëè·ç5Óÿ#§ýéÒÜt×@Zîl=¥•¾ÁdÚÞ/Ëq9-?úåŸrém«“Ž)™³ï!yÞ³Ìôæ˜HÞøµŽ~koO5ºb²æ8Á¶‡ä-oŸŸýìwùß+nKï@+霜Ù3º‡ž£mâ=lg•¾ÁdâGå˜g­Ì)¿9?7¯êÍêŽi™0ЗÁªJG£‘*Uši-™_U¼j&ðè±Y‡ÙXýXÿ±`<²ÜÛŒÙæ2Ë088˜~ô£yç;ß™qãÆmü®´² 1«ªjh»ŒæØÐ«:á…_uRO\˜WpáЦu»Mù¹tnÿ‚|ìscþ¥`‡#ò–ãÈÝ  ÕhŸ–½ŽzMö:j=·§#[íý̼yïg®ïM&™˜þá39`Ìums_’O~î%kžc­í©“LÉAïüLZgÛ{¶= /|ÓyáFng]×i¤';üмïà « NÒHuºÑÚ›±1ü<Ã-Ì€±6aÆ£Ë}EYé@ëïïÏg?ûÙ,[¶,Ç|¦L™òÆÙúVc\÷ë £ªª’¶511ôE=´>d=´šàèÕ±ùF’á]@G‚uätÕI³Ò13Íá0Ǭþ <ÊÂìü¿ž[zØíííY²dI&ÜËJ›ë†I’¼ô¥/½Ïç>é¤fßَ̑_ýêWikkËûÞ÷¾‡,Îî~βµÕu%K–¤³ã¾—ÛìèìÌ]+WfÒĉÃndxÁüÜmZ¬1ôŸºQ§‘ [Žÿ‘lôwphÈÒHs4\Æš(«Cc7æwö®åËÒÝÕµþ'•5a¶ç^û–Þ6ÐÌ3r饗fÿý÷s¾¬õÉØëN9唇dû’ŒØþð‡<îq˸qãrÜqÇeÒ¤IÅvk‹f³™‹.º(³·}ŸÙaû9¹öꫲǂ…i4׌esd®‘¤næïßú¶ Ú†/}þ³›´íGk~õ†ƒk¨¾†£¬½­1r—1+x.ºòŠÌ™·ÓCº½ÀæíQf<|Ì›77çüérÎ9çd×]wÍ[lqŸ³Dµ¡E0vÜqÇœqÆyž &ä­o}k&NœX$ÎFfÊ.¹ä’ô÷÷gÇ9sîó1»ì¾G~÷Ûßäo=7ÛÏ™›)“'ZcøX©º‘|é Ÿ{°7ÿagì|acäÀ± Ÿz 1|7›Íì¶û‚ì¶û‚Ò›ð¨%Ìà~hµZiµZiooÏþûïŸ_üâ9òÈ#ÓÝÕ½è…?~üÃ"Î(K˜Áý088˜ööö´·ýUzžSO=5Ï{ÞóÒÕÝ•ç<ûÙ7nœ8à^ 3¸Z­ÖèîŒ#qvÈ!‡ää“OÎK_úÒtuvåˆ#ž.θWeÖõ†GˆÁÁÁÑÝÇ:òÈ#óõ¯=ï8~úéY½zõ£þ¼_Ü3a÷ÃÈŒÙÈe¬¼àù¾cßòÖœñ‡?¤¯¯Oœ°^ve„ûad¶läÒl6³páÂ\uÕU£÷éííÍ^ÿ†|ÿßÏ~ûî;ºË#Œð ãËî¸ãŽœuÖYyå+_™×¾öµùÊ—¿œcŽ9&“&OJ[³-ééî],Æò î‡V«•o¼1Ï{Þó²téÒ<ëYÏÊ1Ç“|ä#iooÏ“:htᎎŽtvv–Þd6CŽ1ƒûá†nȳžõ¬ì¾Ûn™¿ÓNù⿘®®®{ì±ùÑ”º®3qâÄLš4)===i6ý•àî|J„ûáÅ/~qöÛo¿¼þõ¯Ï;Ž{G¾øÅ/fÙ²eyÛÛÞ– .¸0ýÛßÒ××Wz3ØÌ 3¸žô¤ƒòºc^›…{-ÌAO|b,X/}éK™8qböÚk¯œsΟÒÛk©|îcÌ`4›Í¼ï½ïÍ¡‡š9sædʔɩë:ï<îyéËŽÎgœ‘‹.º(Gy„KpŸ„l‚ÎÎμéMoL[[ÛZÇŽ=ö±Í׿þµüþw¿Ï³ŸõÌxà7®Gœp¯„l‚f³™ &¤Ñh¬]]]]yÊÁç±û?6F#ãÇKGGGÁ-àá@˜Á&X7ÈÆ^ßÕÕ•®®®[ÀÕÅ? f… 3€Â„@a  0aP˜0(L˜&Ì f… 3€Â„@a  0aP˜0(L˜&Ì f… 3€Â„@a  0aP˜0(L˜&Ì f… 3€Â„@a  0aP˜0(L˜&Ì f… 3€Â„@a  0aP˜0(L˜&Ì f… 3€Â„@a  0aP˜0(L˜&Ì f… 3€Â„@a  0aP˜0(L˜”ÛU¸FIDAT&Ì f… 3€Â„@a  0aP˜0(L˜&Ì f… 3€Â„@a  0aP˜0(L˜&Ì f… 3€Â„@a  0aP˜0(L˜&Ì f… 3€Â„@a  0aP˜0(L˜&Ì f… 3€Â„@a  0aP˜0(L˜&Ì f… 3€Â„@a  0aP˜0(L˜&Ì f… 3€Â„@a  0aP˜0(L˜&Ì f… 3€Â„@a  0aP˜0(L˜&Ì f… 3€Â„@a  0aP˜0(L˜&Ì f… 3€Â„@a  0aP˜0(L˜&Ì f… 3€Â„@a  0aP˜0(L˜&Ì f… 3€Â„@a  0aP˜0(L˜&Ì f… 3€Â„@a  0aP˜0(L˜&Ì f… 3€Â„@a  0aP˜0(L˜&Ì f… 3€Â„@a  0aP˜0(L˜&Ì f… 3€Â„@a  0aP˜0(L˜&Ì f… 3€Â„@a  0aP˜0(L˜&Ì f… 3€Â„@a  0aP˜0(L˜&Ì f… 3€Â„@a  0aP˜0(L˜&Ì f… 3€Â„@a  0aP˜0(L˜&Ì f… 3€Â„@a  0aP˜0(L˜&Ì f… 3€Â„@a  0aP˜0(L˜&Ì f… 3€Â„@a  0aP˜0(L˜&Ì f… 3€Â„@a  0aP˜0(L˜&Ì f… 3€Â„@a  0aP˜0(L˜&Ì f… 3€Â„@a  0aP˜0(L˜&Ì f… 3€Â„@a  0aP˜0(L˜&Ì f… 3€Â„@a  0aP˜0(L˜&Ì f… 3€Â„@a  0aP˜0(L˜&Ì f… 3€Â„@a  0aP˜0(L˜&Ì f… 3€Â„@a  0aP˜0(L˜&Ì f… 3€Â„@a  0aP˜0(L˜&Ì f… 3€Â„@a  0aP˜0(L˜&Ì f… 3€Â„@a  0aP˜0(L˜&Ì f… 3€Â„@a  0aP˜0(L˜&Ì f… 3€Â„@a  0aP˜0(L˜&Ì f… 3€Â„@a  0aP˜0(L˜&Ì f… 3€Â„@a›fÍf3+–/ ·àa«ªªM~lû¦<¨gÜøÜyÛâ$Éù=w“_à‘bÅŠ™0aÂ&=v“ÂlÎÜy¹ô¢ 3oÞüL˜4)ͦ="€G§ªª²bÅò,ºâ²ì²û‚MzŽM ³iÓ¦g—ÝäêEWdå]+6é… šÍfÆŸ]vß#Ó¦m™ÞU+7ú96)Ì’dÚ´-3mÚ–›úp†Ù€‡¥Ë.¹0í÷ú5<\3€Â„@a  0aÀÃÒλî‘Á{ý.6iUÆsÿrν»í¾çF?f“—ËüAOÙÔ‡<"õ‡Ó7éqve(L˜&Ì f… 3€Â„@a  0aP˜0(L˜&Ì f… 3€Â„@a  0aP˜0(L˜&Ì f… 3€Â„@a  0aP˜0(L˜&Ì f… 3€Â„@a  0aP˜0(L˜&Ì f… 3€Â„@a  0aP˜0(L˜&Ì f… 3€Â„@a  0aP˜0(L˜&Ì f… 3€Â„@a  0aP˜0(L˜&Ì f… 3€Â„@a  0aP˜0(L˜&Ì f… 3€Â„@a  0aP˜0(L˜&Ì Û¨0k6›Y±|ùƒµ-{UUmôcÚ7æÎ=ãÆçÎÛ'IÎúÃéýbt+V¬È„ 6ê1fsæÎË¥]˜yóæg¤Ii6í Í”­X±<‹®¸,»ì¾`£»Qa6mÚôì²û‚\½èЬ¼kÅF½À#Y³Ù̸ñ²Ëî{dÚ´-30пÁݨ0èϤI“²pï}7z#-6&Ê«2'Ì f… 3€Â„@a  0aP˜0(L˜&Ì k/½°®º®³äÎ;²|Ùò´Zƒ©ëºô&=,´··gҤə2uZšÍÆ&=‡±ß4÷wì…›•º®sýõצ«³+sçÏOOϸ4›&u]§·wUn¼áúÜpý5Ùv»6zÜŒý¦y Æ^˜°Y¹óŽÛÓÑÖž¹;휺®G/Fc­Ù›‘ïÇ~~$Íî¬ûÁ~ä½­ïú‘±èé—y;íœË/½8Kî¼#S§m¹Q¯i쇔{ǘ°Y¹óÎ;2sÛíF¿ù0<öÃñ=}p~$‰£$w ¤õES’ÌÚv»ÜyûíýZÆ~måØ 36+ý}}7n|’»ÏÌŒ|¿î%Iªª*¶Í†ªªÖzO÷ö^×¥qãÆ§ £_ÓØ)1öve`³R™‰XwVb}0vÆâ‘ëÎV­{ÛÈõc£aìí­M c?¤ÄØ›1`³RùPÛù¾®“ºÎºË)ÔÃ×½ý¹ äÆÓ¾”åì,©¨çܸKÖyoë­ZÏe46!Ê}ùq/5ö €ÍÊZ»%I£‘zøëjÌLEUUC’‡o{`/ƒ¹ý‚³òçËîHý`<ÿÆ_FÞ÷ØYšõÝod¶gSf°Êýæ7îÕØÛ•€ÍJ5f–&IêªZsœS2:[1úõ&ª{¯ËO=%?ýýÿeÑýIz2}§½òäç¿:/z̸1w¬×|:/èn« &kf±FÆ¡Ñýz“ÃìÁûjE.ÿíòƒ_œ•ó¯[–Á42aÆ®Ùï¿Ë«žµãšûm&ãž<4c/ÌØ¬ÔUµÖLÅȬÍèícWÆÛÔ׸ë‚|û}ÿ/?¿aËìuÄ‹òæ]·IÏÀ’\{ñ…Y6¸öNekmKIëŒC†g¬c®odÌ Ï&îÊø Ž}ëΜó¥wåSg,ˬž‘—?{^¶ìXÅW]K—U{^æÍfÜ“‡dì…›•jìL̘ٛµŒ\7f‘Š V÷æâï~6?¿aû¼à'æÅ;õŒÎ~ø¤§Þgè5ê537u_®?ãßòÕS~›‹oHÛósðKÞ”W¼mºIÿ¢“òÁþ3—/L2.³ö{f^ÿæçeIͤµ$9éË9ùÌ‹rí}I&dá›?jäë‘Ýé6aFëÁû:Kÿçkùâ+²û«?“ãœÎ‘›<8ÏL’¬oÜûsóY'ç«'ÿ:çßÚ—¶É;æÀç¼6ǵs&4’Ô½Yô«¯åË?<3W/¯“®­ò˜W}(ï:të´¥ÎÊ+NË7¿qjμriZ][eÁa¯È›_z@¶êØèáYãA{aÀfeÝãœÖýˆ»î¬McÝÙŒû²ê’üìK3îÀcóŒy=£Ï³®zÌŸuª,ýŸ/åý_¼8»¼äm9aÏ YüÇË—ÿù„tÍþB^=¿+Í-÷ËsÞ¸G&nÑ“Á›þ”“¿òýüÓwwÏWÞ¼[º«å¹üœórËŒçíoÞ5“ªUÉvSÒ¼‡×ÞkÍÐŒùs$“ˆcÌб¯—äÜŸý%«§™—:;ëyþdÝq¯³üܯ佟=+Ó{uÞó¸­Ó{É/óíï| ©>—ž5+¹ö'ù§oüO¶|áq9a¿i©—ÜœåÛ íà­¿Î'>ø­Ü~À«òîWï”ök“¯õSùø¤Ïç“ϽÉ1ô`Œ½0`³2ò¡¶®«áɈ»/I¾öõ1[–dpÉU¹±/™±ç¬t§Zï¤P2´KßЬP•zð–œñƒ³S=þý9ö9{e|#Ùeû×åš?½;gžqM^ºÓN霴Só˜á‡ÏÛ6/¿àô|àÒ‹sûÀ.™U=ßøíf¿…óFgŠêzS—˜›kŽª‡g›¶ûfÊØ÷-Ε·ÖiÛaÌèØÐq¿58å¹k—×瓯=$[6“,˜ŸiKŽÍÿýÔ\~ØßgîÊÛrW&dŸ ²óŽ=idîðv®ÎÕ¿<5M<2ŸxÃÓ3¯3ÉüYyÝÅÿ“ŸñçÜúÌ™™¹IÓ•ÎØ 36+õÈŠ뮂·ž@¨ëjãvcÌP|Œ~ø¯ª¬ÿ#ôš0©ª*UÿM¹èædÕu'æg­}϶[—g jeàÚßå¤ûyþ|ÅÍY:БžF_2uuú[Uª ¯`8¼¢áý;ãW#F}·°©ë:ÍæÐíõð{ÛXêØWuª±cºþ;­3î7æü“Ïß9“Reè-ue»}wLûoå²;²ËœgäÙ»ý%'ÿæ\ñħåéG–çNN[}W®»ìŽäŽŸç=/ùùÚ/3é–,믲M׆oþoì…›•ª™µ©SUÃ;´Ýc Œ\ŸlèN‰³3½=YtÉYuð”ô¬+†Ãa8Þ†cnê¡ïÈ»ž¶ÍZ¢›ã¦§såÅùúG¿š3·>*Ç÷Æl?a Wžú©|ýÚ‘ø‰ÁjÂlxUÄ5Ë®#³5F#UU¥9T£ã¸1Ô±o›œí¦%­/Í­}{eûõãµî¸¯yí±ã6ºtU¥ê˜gÿÏÙ÷¯¿Ë/~úÓ|ñ½?ÉÏŸûá|ðùÓ†BpÖósü[›ÉcÖti´OÈôö‘л7ÝØ;›•ÑsdUUêºJÕŽ™Vkèƒx«u·K] ݶ!—ôì”C÷éÉ]gý ¿½nõ=ÜoøxÝJ«ªR·OÏÎ[%w^¹8Ý[Ï̬™k.3¦t¤µôª\±¼# žû싎|jöÙqZº«»²øê+rË6GæeOœ’î-Æ%çý9g]¸Ož¾û–9ðÙûæ§ÿüÝ|üŸ{óœçfJse߸:ó9(³Çÿÿöî7´ª:Žãøçwî6mÙ„¶î–³­‰˜P¶ÕR*dk0°2¨ìI‘ГzDú ˆ„ðYdPùg3‰W´ÌIZ6CzâÊ"zQéJlºÝs~=8çÞÝ{·Ùîånç6߯Áw»÷žs¾çÉïË÷÷ûþªU3wD'º»uDw©z®§_~¿"ÉÊõ\¹©Ju庮ÜIÄášÓƒ®€6Õ 08ZYÇ“_qÊ¿­ÈTǾ¢e£6Û¬Îw^Ñ–3zhyB7ÆFô÷¯?êì_whýcKÇÄýÞu-:°m‡Þú`Dëš*5t¶O{_RÝãkTsuõü õŸ’5 4+ù‡Nþ|Y*›¯ÙŠ(ÑÖ¡†þ.mßúž.¬Y¡Ä ž.ÿMΫռpâT(ŒØ“˜ ¤øÕkœ<ÚÚëÆì«5Éñ°sS‹6½Q¡Cû?Qÿ¡. \v%EU~K½înOʵq%ÚÖkå™ÝêîÐ=¯¯UUÓóÚâîSg÷çz÷ø?²Ši~Ýznå*U—/Ñ“/>ª+»jûV=“™5OUËnÖ,etÏèÿ_‚t(«½F:²VVÆŸb—T÷Â+fSûh•Ú^~S•÷é³#=zÿ謤xEBKšiØ'îËŸÕæʵóã½ÚvxXNy­š7¼ª§ÚªµÒ•ÁŸôMw¯:/&%E4¯v…žØ´V5QÉ,lÕK¯E´÷Ãí{û ]•4{A£^ô š*'N…ˆ½ÙÿÑÛÚÞ‘÷€©Ðß׫޵hxxXÖó䦹9•É_kä–¥ÂÏ—:?=°òï5«–“ñÚ#ljÈqŒbñ¸z>= Õ­íy‰ØçšžØ÷õöP1@iÉ®Ú%ˆtÓ…Ìæ ©ÿ™Ñie3‘õF§Ïy^pŸ&èèO¡K¿Õz²Ö)RÅŒØOgìIÌPZ‚it~•ft]Söú¥ŒõNÁÑÌТMêÞ<®éJw´VŽ1’ñ«:©8Ù ±Eþ'#ö™¦3ö$f()ÑX\ƒ5gîœ`d,Éú›úúbÿ,ëY™àGm]^,O?³1ëõ®;ŠöÝ“‘n@aL£ËH œàžmp”I/ˆò¬ÕÅ *Ï{“®’‰}±ä>Ã\=ÓPbŸ÷'€)T]}«¾ÿî´ššïÓˆµ~5ÆH&ñ§G²’q ìÊ89»wå{ 5Þ}eýÍø¿rrÄ¢19}J‰ÚÚ¼ÏY*±/–BŸa±'1@I¹½¾A_¥cGµdé2UT,;Pž±Í&Æ—®Ø¤»Ž]Óe­Õàà}{ꤒɤê×ç}b?ÖtÅžÄ %ʼnDÔrÿ*;÷ƒŽûRCCCa_Òÿ‚1F³ËÊtÛ¢:-^\í½¸&@ì SŒØ“˜ äÇQ}C£êþ”뱇öÀõŽÄ BFb!#1€‘˜@ÈHÌ d$f23‰„ŒÄ B•¤¾Þž°¯®[ÿöyäƒ=„IEND®B`‚libindi/libs/indibase/alignment/BuiltInMathPlugin.cpp0000664000175000017500000000733513263645557022232 0ustar jasemjasem/// \file BuiltInMathPlugin.cpp /// \author Roger James /// \date 13th November 2013 #include "BuiltInMathPlugin.h" #include "DriverCommon.h" namespace INDI { namespace AlignmentSubsystem { // Private methods void BuiltInMathPlugin::CalculateTransformMatrices(const TelescopeDirectionVector &Alpha1, const TelescopeDirectionVector &Alpha2, const TelescopeDirectionVector &Alpha3, const TelescopeDirectionVector &Beta1, const TelescopeDirectionVector &Beta2, const TelescopeDirectionVector &Beta3, gsl_matrix *pAlphaToBeta, gsl_matrix *pBetaToAlpha) { // Derive the Actual to Apparent transformation matrix gsl_matrix *pAlphaMatrix = gsl_matrix_alloc(3, 3); gsl_matrix_set(pAlphaMatrix, 0, 0, Alpha1.x); gsl_matrix_set(pAlphaMatrix, 1, 0, Alpha1.y); gsl_matrix_set(pAlphaMatrix, 2, 0, Alpha1.z); gsl_matrix_set(pAlphaMatrix, 0, 1, Alpha2.x); gsl_matrix_set(pAlphaMatrix, 1, 1, Alpha2.y); gsl_matrix_set(pAlphaMatrix, 2, 1, Alpha2.z); gsl_matrix_set(pAlphaMatrix, 0, 2, Alpha3.x); gsl_matrix_set(pAlphaMatrix, 1, 2, Alpha3.y); gsl_matrix_set(pAlphaMatrix, 2, 2, Alpha3.z); Dump3x3("AlphaMatrix", pAlphaMatrix); gsl_matrix *pBetaMatrix = gsl_matrix_alloc(3, 3); gsl_matrix_set(pBetaMatrix, 0, 0, Beta1.x); gsl_matrix_set(pBetaMatrix, 1, 0, Beta1.y); gsl_matrix_set(pBetaMatrix, 2, 0, Beta1.z); gsl_matrix_set(pBetaMatrix, 0, 1, Beta2.x); gsl_matrix_set(pBetaMatrix, 1, 1, Beta2.y); gsl_matrix_set(pBetaMatrix, 2, 1, Beta2.z); gsl_matrix_set(pBetaMatrix, 0, 2, Beta3.x); gsl_matrix_set(pBetaMatrix, 1, 2, Beta3.y); gsl_matrix_set(pBetaMatrix, 2, 2, Beta3.z); Dump3x3("BetaMatrix", pBetaMatrix); // Use the quick and dirty method // This can result in matrices which are not true transforms gsl_matrix *pInvertedAlphaMatrix = gsl_matrix_alloc(3, 3); if (!MatrixInvert3x3(pAlphaMatrix, pInvertedAlphaMatrix)) { // pAlphaMatrix is singular and therefore is not a true transform // and cannot be inverted. This probably means it contains at least // one row or column that contains only zeroes gsl_matrix_set_identity(pInvertedAlphaMatrix); ASSDEBUG("CalculateTransformMatrices - Alpha matrix is singular!"); IDMessage(nullptr, "Alpha matrix is singular and cannot be inverted."); } else { MatrixMatrixMultiply(pBetaMatrix, pInvertedAlphaMatrix, pAlphaToBeta); Dump3x3("AlphaToBeta", pAlphaToBeta); if (nullptr != pBetaToAlpha) { // Invert the matrix to get the Apparent to Actual transform if (!MatrixInvert3x3(pAlphaToBeta, pBetaToAlpha)) { // pAlphaToBeta is singular and therefore is not a true transform // and cannot be inverted. This probably means it contains at least // one row or column that contains only zeroes gsl_matrix_set_identity(pBetaToAlpha); ASSDEBUG("CalculateTransformMatrices - AlphaToBeta matrix is singular!"); IDMessage( nullptr, "Calculated Celestial to Telescope transformation matrix is singular (not a true transform)."); } Dump3x3("BetaToAlpha", pBetaToAlpha); } } // Clean up gsl_matrix_free(pInvertedAlphaMatrix); gsl_matrix_free(pBetaMatrix); gsl_matrix_free(pAlphaMatrix); } } // namespace AlignmentSubsystem } // namespace INDI libindi/libs/indibase/alignment/AlignmentSubsystemForDrivers.cpp0000664000175000017500000000520013263645557024523 0ustar jasemjasem/*! * \file AlignmentSubsystemForDrivers.cpp * * \author Roger James * \date 13th November 2013 * */ #include "AlignmentSubsystemForDrivers.h" namespace INDI { namespace AlignmentSubsystem { AlignmentSubsystemForDrivers::AlignmentSubsystemForDrivers() { // Set up the in memory database pointer for math plugins SetCurrentInMemoryDatabase(this); // Tell the built in math plugin about it Initialise(this); // Fix up the database load callback SetLoadDatabaseCallback(&MyDatabaseLoadCallback, this); } // Public methods void AlignmentSubsystemForDrivers::InitAlignmentProperties(Telescope *pTelescope) { MapPropertiesToInMemoryDatabase::InitProperties(pTelescope); MathPluginManagement::InitProperties(pTelescope); } void AlignmentSubsystemForDrivers::ProcessAlignmentBLOBProperties(Telescope *pTelescope, const char *name, int sizes[], int blobsizes[], char *blobs[], char *formats[], char *names[], int n) { MapPropertiesToInMemoryDatabase::ProcessBlobProperties(pTelescope, name, sizes, blobsizes, blobs, formats, names, n); } void AlignmentSubsystemForDrivers::ProcessAlignmentNumberProperties(Telescope *pTelescope, const char *name, double values[], char *names[], int n) { MapPropertiesToInMemoryDatabase::ProcessNumberProperties(pTelescope, name, values, names, n); } void AlignmentSubsystemForDrivers::ProcessAlignmentSwitchProperties(Telescope *pTelescope, const char *name, ISState *states, char *names[], int n) { MapPropertiesToInMemoryDatabase::ProcessSwitchProperties(pTelescope, name, states, names, n); MathPluginManagement::ProcessSwitchProperties(pTelescope, name, states, names, n); } void AlignmentSubsystemForDrivers::ProcessAlignmentTextProperties(Telescope *pTelescope, const char *name, char *texts[], char *names[], int n) { MathPluginManagement::ProcessTextProperties(pTelescope, name, texts, names, n); } void AlignmentSubsystemForDrivers::SaveAlignmentConfigProperties(FILE *fp) { MathPluginManagement::SaveConfigProperties(fp); } // Private methods void AlignmentSubsystemForDrivers::MyDatabaseLoadCallback(void *ThisPointer) { ((AlignmentSubsystemForDrivers *)ThisPointer)->Initialise((AlignmentSubsystemForDrivers *)ThisPointer); } } // namespace AlignmentSubsystem } // namespace INDI libindi/libs/indibase/alignment/controlpanel3.png0000664000175000017500000016630213263645557021460 0ustar jasemjasem‰PNG  IHDRf’¹¡ÈEsBIT|dˆtEXtSoftwaregnome-screenshotï¿> IDATxœìÝwxUÛÀáßÌlM#•PBo¡  ¨HQÀˆŠ¢Ø±÷úÚ{Wì¾"ØQªˆŠH©Ò{ïÞ·ÌÎÌ÷ÇnBRyÕç¾®%a3óÌ)3³söœ9£ìܱÝòz½x½>|>/‡G}!„B!„ÇßÞUé((ŠŠb)ØU;6ǃÇëÅSTăÞ@rR½àJõWo1¥Z Vs“å®|­jÇ;ê‚JÍbUca¥‚ßjïè*ùkÍÞ®:öq‰WzÅ#׬u¬ VVŽ-ZY¯El¥&kVÿ@ªyJ*¯Èc,1J'êØc•ŠZó@µcUæJ²Z«V½Pí³YÅzŒÖ«C¼ Sz¶,åX:ƺ°*>¸j«¢7k°âxU.QýXµûÃÑ·Zí$Uó,Vƒâ³*øíøÄ«ä/Ç%Vù…jp,Õ`Ák2QÃ4°ÍÛ˜EÛxø¡ûˆ‰‰ÀãõV7¬B!„Bˆ(nwÑö¬<„Š Û’ÍŒõ¢£ë`ϯ™„B!„BT*:ºt‚=+ÒQ‡Ÿy²Ó#„B!„ÿ^ (½zv·êÔ‰ªÕú–eaZ&–\Ó7˲P¥d½àO+˜²ÐO%tIñrJ÷UDz@QPPPÑ4EQPUµÒ8B!„BñWËÍÍÃÔªAeZ&–a¢ë:ÃÀ0¬’¦)Ø4 »ÍVá$Å1”ÐOË2ÑuÓ00MÓ²PC(UÓ°Û5PÔà=àåU–º7Ü2«džªihj°‘&³ÿ%&ºWÇ0ÑÜn\ÚÉNÏÑüÒ*„B!þl5]Á²,LÃBèø¼>.hžL·ú±4ŽŽ$1ÂÍ¡;sòY²/‹É[÷ãr9±ÙlGÄ ÔÃÐøü~7nLýúÉÄÅÅMvv™™ìÛ·Ÿ;wât8°ÙmÁž1÷ ¡ã|>?ÉÉÉ4oÞŒè˜"#"((( ''—mÛ¶±wï^œ8ìhZðjZgµ`™†š†v\ŠÏD÷9Hêж"I_ð+ íÿ£ ž¿SZ…B!Äß…Ò«gw+22¢Z [–…išøý: •ÏL%9‰e`šX¦‰¢ª ª(ªÆþÎ[GºÏÀîp”Ä(þé×uìv;½Ï:‹ØØh ÃÄ0LL3€ªÚдàĬ¬~›;]×qØíåzÌ,t]GÓTzöìEbb]L³â8™ÌŸ¿¿®ãr8ÐlZÍf–Aav>…há‘Ći(¥Þ³…En+™áÙô‘ÅAt\LŠró)ÐKõR* šfÃér没*GÙÖQÒå-òâñÐ`lE³áp:‰³§`øÈ)´—‹­0‹"K=öØ–NNaSîûè:Ûòøùñ—æ"Ò^.°UqÙÙlvœnaNõ„̬]«´ !„BQMùù5ÊhZ&º ¾SãÍÞmq[:zN~¥Ë':\¼wv*·þº–4#€Íìñ2M‹€aàt89çœþh6…‚‚‚à}jfqZü(ª‚‚Bx„›çžË¬Y?0tl6;ªêq hšÆ9çôÇá´SXŒƒUœ¯Ãq"£"8çœsøñÇYø:EAÓÔê—˜ePXÃ…¯¿ÅÅuaÿ7ÿáþsÐŒ¸Ð{»§¾Àã“ö⌴ƒ¯ûøöŽP´ˆ'ný/Û‰§ïÃÏ2¢m$%,zwmfù¼Ÿ™òÛ6ü‘a8©h[ùD†UÐø0|äúërÚƒØ#•æ nL mgåï³øfæz<ŽãÐ83urN<þÙ´æî{œ Ý„koQEµ±¬²±L<…ôþÏ‹Œlí:ü¾?½[Ö0æTf®+$¬T£ø„¨NZ…B!„¨¡ e´Lè~?­9¶Â,|>ÏÑW(ÌÃî ãÉSêsÃïÛ±…†bšè~?gÙ Ë à) „.n¿ŠïS…@@ÇnwУG7fÏþ ›¼çÌ4Mü>g÷骂§ÈSEœv»^½N+‰S£†Y•7w|œWþðqDéZ˜¦¨øÃ2ÓÕA½æ9¯ygœ=ƒ'Ÿ›Æ~»½z›4ýä+í¸éÅ[9+¾ôT›Óó´5Lœ²Ë`â/òRèÕ ˜ ¨N·›p·† `x GÄ™3gº®£(Ám5¨ŸÌýjvAmU8’­Ì{Fž-*™‹î¹Œ ÷Æ–*Bjî긊ØúãG¼VÿEž:;’Øž½Iùj,ÛËoÌ*—SÇÙ•óOq°sük¼7'ˆ0v¯ uÏ%ëˆ{ê¤U&„B!ŽMõ'ÿM¶fé% *3'V‘:C»µgì‚•\Ù£©f¼¬Ãë* .S/é3CÓÜ9T…… Wºé=º—¬ëti˜–…˜¡†—®ë%qÊç§ôïÁ^µàÿÎPœštuTg(£~ˆ_Þ~Ÿæ¯ÜÁ™§^Ã-Î‚Ò W4˜7U)¹‡©âTYåòVÁ²LDa¨ÛÑÀ³––dኴspÁ¯l½ª­ìh—¨±d_éµJ.›6eCÓÂã"ÐL‹²í'W„ ·XF¹´*8#à 3=x“kºí`ÞÊÔM¹L%ÍWÃJ¿ãaéÔ4¼üažØœÈ2K;s”/Ó ògÜžFÁ-pÙ‹pYéUbê­ZdQÖiB!„¢†ª=”Qµ‚à ó=>œFðaÎÅ6îÞÏws~z&/[OãVuhSçðÝUŠªRàñ‡%ªÁ8ž"/Vèb^QTz÷>³ä¾²bÅ÷›y<L+ØKæ) Æ!´¬×ëCQ,@AU7Ì*šß4ML3ØëäóùB½r5+°JF–å]Ãßü•¶õ¡i‡Ø£¯oix=nR{4N„‘·‡Þ#Óo•2§¨˜Y{È¢¨KçÔ¾‡Ý¥¡*ÁžJÃT)3Oa°èª×Ç£XüFɶÊ6T¥Tš*ãiÕvÛ ¬h=½0ƒ=›×0ï‡,tàÍq{˜ýõ/lð5àükúÓ(´Ý*ó§X *¡ÉH2YüãR•j‰šù[ÈWÕ wi— !„BˆcQí¡Œ(ªª±%ßKªfbÁ^ª­ù^ßœÏíýºÑÎé§Õ€ÓyyæBîlAë¨àÌ‚ªbg[54+£ªªdgçŽe™¨ªJQQªzxÅ’Þ5Ó,y)ŠJ~~~(NpbÜܼP UU Î|h†~å.ÔrŒP…‚‚¼’嫯šËjvØ2‰·fuâéscº¨éõPèjÈ©—^É­§g–8øÛvYÍVX®¤ªh9«™»s8W5†¦WÝÉ úx¦.ÛCvÀNz-i]7ƒå+bì_Ã:ÒÔÕŽ§ÆòÚü\ê ìKs€ÀÖ¥ª7ã ¿€BТišì·7ŸÍE¤»¢…-M­æ¶-  Œ„8']:ØÔ2±J+žü# (h6 »¢7¨np²•Ýs˜8c>™Î6tÞŸFe&-9ãÐfÐDà>´ˆI³vQh*أ≶òñÚT½ª´ !„BQ3ÕŸ•Ñ4qØíÌÏ,$5!8< ÌÆýZÓÆŸ†žžI³Èhî;÷4œi;°EÁ•5s28ìa¨¨Øl622Ò wcŠb¢ª&š¦Ñ0 >(:ø²Ùl¤¥¥a×lÆ >Û -ZZaÓ´JâØlöA«jêÃoÙ\[¿ù„ÙÝï§otÅÑ’‡æëaeß+Zû ¯NÞÍ©ð•_£üÐK‡#›ŸßŸÄéÏ¡…-™¾×?HßëK-âYÆs·À†œÅ|ñëÿñXŸhN½å%¾º™’n¤3¿g•OE-×¶8²hl½,ÙfrJK]î~ƒñE:vÏ|½÷+v”4–¢á¨rÛv+“-épj}îÍ]/>ÌÛ› Âl•îT].¢ÁD[?Ù[wR@M®â•WÎ`_‘›ÆGi”™?[ƾZx.÷÷ˆ¤ÃÕOòÙÕtˆ]ñ³äù»x}«…fT•V!„B!j¦ÚC†…ÝagNžÆ (ŠŠð“ì6 hÉ=eFNÍ &fQÕî`yÅ܈ˆ`šÍæ ++›¸øxœ{°KUÐ -Ô0;|ߎaXf°7,+;‡œìlÂÂ"@92NÀ°èÔ©#𦆆5Ιf°­ÈãAU ‹ŠÈÌÈĆi˜•gºÎÈWár.s_þw§ßÓ•âYÖE'çPºÑ‡'8ôå²wÛÿ6‹é ÷bºØ0Ñ+ÚVù©6l‡~ä©{÷qÞÐôîÜŒ¤p °ðdìbýò Ú44%À¦±Ïð|ö•\qN'E*¹»X4cŸþ¸Í©â¯ø)¥(Øù,ó> G]Fÿ6±¸Âì2±kÁçÜ™^ÍUŶ]6lF&?½ó9 oº€u²òt´2ù=êlˆšeó—¼ð¹Æuƒ»Ð$¹ ­|yìß±ŽÝÞJ†3–ËŸÍåcåOñüþ˸´oG𯨱+‡vpд¡YöªÓ*„B!DM(½zv·Žê=ÌXÓl˜†I´žÏS‰…DYzÉ´ùQmvò;ŒÄë Çzh²iZè~?†iÒ¬Yã’²Òƒ.óhUÅ4,¶mÛŠf³á°;QU¥$ŽiY4mÚE ö앎Üž|6šª€¥°}ûv@AÕ”ZÜdâ-ðá3Auºˆt*eÞÓ\."¥î“Óýä{ ,E#<ÂM±ðùðJm7ô¼6»Ã†Ó®–j@T´­ŠY¦ßÀ0CWÞÛ§Ù4\Nš`ðéxý†(*v‡·3´MË 0ßOGxpRÃç¥Àgæ *<ô€eÓÀãÑñ—­¡²t¸ÁUW+­B!„BT߯fÅ ¦ê°;躧ßË QytqúÀ Ívz2´¢( j,÷9ù(/ ŸÃÓ|ÖVñP<Ó NÀa‚Ï" «ô9fEEE8pMSqº\e&ö0M ¿Ï‡a˜$'×Ãív—ÜSv˜šnß`ïÞ}˜¦‰Ýn £”ágB!„Bˆ“G×C 3›ÍVõÒ¥8N—nöÎr{hfÓ‰TMòM•m;s=n–脹]ØÊ5üJg–…®ûñy}DDDŽËåBQT,ËÄëõR_HaaN‡ »Ã^Ò*ýœ³ÒqêÔ‰"<"<Ø´P,ü~?……äææáp:°©ºa@%³7 !„B!Ä_%7Ìj>Ëf·¡ª6ºŽ®ë˜¦iZ¨jpè˜ÝnÇn·WÚð)nTYVpø¡? cèà=e¡“¦jhv›EU)U~Jý’8–_Сá‹Á™UUÅfÓ°;옆‰aJ£L!„Bñ¿!0ª?ùGyº?€¢hš ·Û]j²ê)îñRUKQqªN,{ñ³Ï¬’XÅ“”¾­²8¦©áth8íö’ŒÁQ&† º¿ôýp5†™B!„BœÕž.¿"ÁçŒùÑ+ŸÿC!„B!Dªÿ€i!„B!„'D­‡2 !„B!„8>Ži(£B!„Bˆcgøá‡Y';B!„Bñ¯4hй¨';B!„Bño' 3!„B!„8ɤa&„B!„'™4Ì„B!„â$“†™B!„BœdÒ0B!„Bˆ“LfB!„Bq’IÃL!„B!N2i˜ !„B!ÄIf«é ÙYìݳ›Â‚LÓ<i€ˆˆH¦4&&6îd'E!„BˆªF ³ì¬ ¶nÞD»Ž‰‹O@UU”•2ñ¯f˜&™é¬]õ'Í[¶"&6þd'I!„Bˆ¦F ³Ý;wÒ®cgê&bYVðu¢R&þÕE!¡n"©NaÛ– Ò0B!„ÿh5j˜%CÅ_À²,âãX½bÙÉNŠB!„'Tï1S5Mî-EUeB!„ÿx5n˜aYÁ—E‘»…B!Ä?_f w–‰¿ŠìiB!„âß æ=f WËB!„BqÕ¢afÕªÇÌÊ_͸7§ {€-5ßìI¤˜Ë73²8uø…´ “¡u-ù@!„Büó©5]¡ø³š¾LïVÿ¹ýEf­c—éË`Ó²elέ~:ü{æðÝK9¨Ÿ¼tÿS^zövþ\¼ŽÌ@õ×B!„âŸ®Æ 3 öWåDzîqzöÍäµçÞg´²Ž1ßU¾L þ|«† á–/v —û»Ûxî6„!CB¯KF0êÁ—?w3¸ŒoÃ;Œr#c·ùÀ²0Óâ!Cõé&¼l¯híû\?d.ÈÃúËÊ?@ÖïcxöõéìñI«L!„BˆbÝPÆc\ÿx9¼íê§£x¹–r#Qf£pæ·¬|]#™4ý¹äát¿ñþ¯¡Š//ƒmK¦ñÍ[°6û5ž½ ~¹\Yòö“ dÌø”ÙŸaP¢vx{ýÌúôg²ˆŒ| "Ðø+Ô®ô…B!„ø§«ÅtùåZÙ$oí÷¼÷ñT–î+ÅMò·óÌm§YzË·s2O>2ýügx¢×½{"ñ÷}È£Ý#Q+1ÏÞø ù#¦ÍÔç™Óæ >¼³.À¿íSn{h)g¾ü&W6¶EüùÒ<—>œ1/ "fç—<ñÜ4¶ä€0êwÌ ·\L»H5”ölf>t3Háš1/r^] «`33>ý”É¿o%ÇPoÔ‡;ž¼‰6lá³{¯æÍlØãhÛw·\Õ“zöPžôtó!ŸÏZÉJdãÓrà œ×2ÅÈfÙ—ïñÕ‚õìÊòát¼ùéøwþÈä­‰ }lÛ^ÃÄéœ2 îáÆ’à"¡YkÚ4uЩkbŽb̬yìx9õ¬RËZÈ?D>nê…maÒ¤ ôÕŽàíqù+'ðýΦbX·edñç·1næ2öBxƒS9ïêÒ) Ðw~Î÷Ï£ëóïp]sG0ÖïsÍ~ö$§†éìûñÞœ¶š=iè€V§g\z3×÷KÁUÜÖô/á™—?Ÿ—ßA“âr¬hB!„âîøN—ïÝÀ¸W¿fcÛ«yø–ÖDú39èO!L±Êô:2æóö3_‘Õó^ž¿¤9¦F縯øuÉ.<ÝSq¾}²=È9íšÓnK3¶®ãžJ#»Aö¦ud‘ÎÚíycQýûY¹ÍGݳÚ£Y¨ñ]¸pT*QÑnô‹øúý ¼:¾-ïÞÒ&ÔØ‰ ÇípqŠ'±q*–¾—©£gü¡v\tÓƒ´ƒÜ,…†îâ¶A4.¼šžõ]mû…Ͼzƒ—ãRxáÂúج"Öö8/ÏKàŸ [\>+&¾ÇØç>$é;éìÌeó¢LÆÝ7·!Ê,ÂJ©ƒŠ…e²nگ䥎¤ÛSéxVö3»û§±ýp™SRz¡ÿ)6ÜNü–Un^.~{k†Ž´óÉû_2wÈÓ ¨«A`¿|ùê™wrqÆ[|’Y@ »åaÓçóÜtè5âF¦À®9Ÿ3~ôxŸ{‘-\GôÊ•NOð7ƒœ­«ØîëÀ5÷÷¡¾­€ó¿æóŸ'¬É\ÓÜÌÖ–žº†V.PÑ$Ù*ï=“v™B!„ø78¾ÓåëùdAT³´oÑ;ÍhZÞ ­cæ¬à¿ŽamË›xþ†nĨ€šÌi]c˜¶h!{¼©´tÈX·‘ü¨vtªAýî-±/XÁæÜ¡4Š-dë²ý`‡½Kvà9;WÆ:ÖdEÑ¡s=ìÕ’n]CijÞûÚßxlÓ2õ6ÔµìD%5"¥‘£$éÞ ßóÝÖ(Îyú†·v—ÉV‘À)=O£s”훢¬^Î ד;¸>±ùòÍ/Ù¤Þ<šá=cP€¦7f±äö¯˜½ÕKç¶Á8aN¡kÇf”lÕ3ûO¦,±èr_¢‘ú“üÓL¦o¼ˆÛÚ¹K•·…á×ѽÞ¼ClYü=Ÿ®6¨sv7êÛKÕ‰X&…™EXî$vÀ „˜&OXÉœ†»\¯\™}¡ô{‘M9¥sG’4èØ:’íËŸeù²C\Ù¬~p-œÄ†)¤”.fi !„Bˆ±ã{Yx*C7æé¯îãöUg3`Ð@úumD¸ZÜËæcÙ˜7 ì þskoíÅ‘¤ô>ØY‹˜·{-š±yÅ!ÂR¯¡‘\­zÑ\û€E[ èÓq‹vÇ3àÒdL[Ì._'êm\ÂþðŒläÀÂijk_Œ›Î’-ÈÑí¸ÄúЭÒi/ý{€ÌÍÛ(´7¥kŠëˆü•î²' É‘°:‹Ã"vMhø%@8M{ßÀ¨‘mpaá+×såÉ-g$.gι¤=Ó?ú–å\‡wâïN½‹ÞõìŒt®lŠL WÚzvÑtë­8öºthÅÄ?×sHïNÊeg•ËW}{öXÅÀš¬B ¹ÇL!„Bˆ ծǬ2J8m®xŽz-ãç©“ùî•û™Ô~$Ï?4$TRzu¦hÞ|Þ{*ÏêNLèF*G£Þœ‘0“ßæìäŠzyü±ÓF›ó›â”¨¶œÑÔà³ù[ÈŠ^Ê&[{îéÑžƒ“>cÉž,Z.أݚºÏFÆ=ûóêæÆûO§q¤Î–‰/ñᮣ'ÝªÑ € ªMË ufYX¸évÛã\ÖÔQf9gl ù‡Ñ÷3ïÇÙ;xl@(ŒË IDATÄÏeÿ¦Î`yvzÇOœÉé£îbPC7ΰ:$$Æn«ì™j&ž\/8Ãq¨*1§å¬ÏŸäëqãñoŒ¡ÿ ‰T ;‰åËÃkV7Û ºQƒ²RT4LSXB!„BT¦ ³ª°¬‘Ò‹nïFŸî¯qÇ+3˜³·Ã#ì$žq ×uÇ#/¿ÎsñÏòô°æ¸ÀÞ¾S˜:i&ËÛب¤r[ëðàÖÔNéÓ”ÇÍb¶kF›[h[Ÿ^ó™ðólvn¶Óñž„FÞv¶äÚépëÎjïtÌz.5Ì»;>ò}¥[#6bš5Â¥obÙ.Ê e¬Š#¡%ÉÊÏìÜ£P÷Ì8Ê/ W¼ž÷¯Ì>C߻數Y­Âµ|òìÌ\œÎ‹·Blã–´jzDô ˜øò½àá®æœ÷ ùiübÔÖ7qnŠ0p„9À›×GÝ64Ô~bÝêtôÁ¡Œèi¬^Ÿ‡­QêÚA‹ˆ'‚<öî/Âjå¬rO8’Š-Ìz!ž€EÕûÕ\F!„Bˆ¿·Ú=Ǭ2ú^æNÿ•eë·²mË:V®;ˆ—0bÂJmFQ‰9õ:Ñ‚}“^eÜêüÐ`5ÄžçÓAÿƒ÷?Y©}hQ|Q®sJZùWòí|/íz7íFÓ¾O²æ|Ç:[gúµ 6â´¨RÂuÖ~ÿ=óVofÛömìÌð•lÞÛœ&aE,ýú{¬ZÍÒ¹¿²*Ó ¬õEœ× ›Ÿ^z…ñ?/aÕšU,ž;ŸMyUw')Ñ]Ú'–´)/òêW¿²tõZV-þ•?o¡°ÒŽ"/[šOVüœÓµM›6-y5kwZ«ìøéwjQV€¢"졆õÎɰ³Îæò§§(8Âìà ö˜)Q§0l@"û&¼Ä{Ó³zõ"¦¾ûÔcà°ND* F·ç¬f ›Æ¿Ë„ß–±jõ*þÜœSƒ„)D4lEœµ‰ßÌaŪåÌÿy1‡*i¸ !„BñoQã3å(VQ›çÉìqù˜€=®%}FÝH¿ºd–^ÒAÃAwpÝŠ»ù`Ì—ôxýFÚ‡+(1]Ú;†Ç~òÓk`*¥å…Ó…AœlØÔ…~-‚=ZÑûÓZÛÈþ^hZÐÝ–‘÷]Š÷“é¼óì”`šu¨ß©..p§råÍHÿx ožŒÕ‚ êIÇ¸Æ }â ÜcÇ3í¿¯2Í{\.kv «,”pÚ_û4DåëŸ>æåï Ð"iÔc$Ýû´ ¼¢²*ÜÄO‹ó‰ïדå§ŠW¢i߯5ÚÛ¿òÛÞÁ ­jûG÷Qä-Ê…*C%¢-Com[j!{˜…ê¸Ý´ºò r}ÂøI¯± Âtå’‡¯çâ®`¿•–Hÿ{ï!ãÝqL÷e¾Tw ;¶!¶šB³7¾ˆ[‡ìæÝ©ðü,kÔ»NëF¢½âëhû›B!„ÿJ¯žÝ­~˜U­…Îÿ~ÎÃï÷Ÿ ä˜dÌ~‚»¾kÁS¯]E3ç ÚŒøÛp8üòã zœÑûd'E!„BˆbРsOÄ=f5gäïf{š™¶ˆÏÇí£ý÷ÐDe{Ì„B!Ä¿Áñ•±V, Ö~ɳ¯¯ÀãJ¦Û%ss˜ã|ó›B!„Büï:®÷˜ vi…ïóÍ„Ê"Rçô‡øìôš¦DüÈ=fB!„âßà¸e¬¼&DmHËL!„BüóɈA!„B!„8ÉTM«æ<çB!„B!Ž;MÓjÞ0“{~Ä_Iö7!„BñOW«†™Üó#þZ²¿ !„Bˆ6MÓ°Ùd(£øX8ÿ·“!„B!ªAãæ-‰‹‹G×ýÕZǦiØj>”QA‘^ ññy½œ{Þ';B!„BT)?/ÌŒt6®[MëÔöDEÕ©Özš¦aÓlÿϘâ(¼ÏÉN‚ÿ*™™éìڱ‚|LÓ<ÙÉ'IDD$›µ ..þd'E!þ6l6±qñ@+vnßJ‡N]ªµžf³Õ¼ÇL!Ä?Wff:›Ö¯¥]ÇÎÄÅ' ªªŒ‘ø2L“ÌŒtÖ®ú“VmÛ—p²“$„ Š¢àp؉ŒŒ¢¨°°ÚëÉ=fB!ÊØ±u í:v&¡n"–e_';Qâ/§( uIíp Û¶l”†™BÔˆ‚ªª5uR«{ÌŠååårpÿ^¼^/–%Û'›Ëå&©~©—)“CÊõŸ©¸^ ˆ‹‹Ç’!Œÿz–eŸÀêËä3_ˆ£(>V÷^"!*¢Õ¶a–——ËîÛiب1u¢cPUUf5?‰LÃ$7'›];¶¡(ŠÔ R&'Š”ë?Sq½îÞ¹UÓäÞ2€úÆW>ó…¨Xéóg£ÆM¥q&j­Ö ³ûöÒ a ubbèº|{v’)ŠB˜ìûöR/¹¾Ô R&'Š”ë?Sq½šVð¾",+øÿzŠlÉg¾+}þ<°o¯4ÌD­Õú3¯×CThº.ߪþ°,‹€®ãóû¤^B¤LN )צâz­S':ø@î,@É^ Ç»+}þܳkçÉNŽø³Ëtùª¦èÇ9I¢¶Š?,¥^“291¤\ÿ™LÓÄf³~CÚe¢9Þ…¨\ñùSz“ű8¶éòC³u‰ÿ1R/G’291¤\ÿ¬’ŸÒc&‚BûïBTAŽqlj}»nå„Ï»}OΧïÍÃiã®zy©—#·212XôÕ—¬Lº„ëû&ño4»ìkVþZ&Žý Ç๰±ãèˆêó˜ªÖ·™Oîyœ%íãQ©¸ülY³¥EgZD©µ *Ž—@ÎvÖlöÒ%•Øj|ü[%í²¿×ñ^“ý¾RF«gýȶäœß)y¸8ùâB+MÓPkû3ë/~éû¦ñØõ·ñÆ’\Ì“°ýê½L ÷®céÚtüÇ%ž…?m‹Vî$Ϭzù“Q/¦?‹-+W°5ßüŸ¬¯ãZ&F>[–.gCš/˜'ÿ6ÆÝwwþw#E'1'ªÿ²r­áKÏÝɪåÉ2J½o°â½;¸ö‘IìÔO~™÷üÃË(ÚÃò¥kØ[T½ú-Q|!^ÁËÈ^ŤWîçºË†0dȆ\q÷¿4‘ &(nê7 ~|šeØ7“מ{Ÿ¹‡ô£Æ¬êe¦ÿÄC†0êÓMxø»IÑÚ÷¹~È]w¸!¯r¯Y¿áÙ×§³ÇWÍuNÂñ^«c Ü9 ¦û}…/#¥3fòû®¢ÿáëŽò¯ã}røemgæ˜GuõÕ\yÕÕÜòÖrò¬“ß*òýN q,Žé9fXõÛ3kOÞýç>ÆsÛá*·rÑÆq<ôüêÞò6ÿéYõÌ»ŽhêÕO":R«ý·fÛæMæûŸ—°~oÂ[Ð鬋>°5‘Çüe®‡õã^âÍÀM¼Ó6ž¨cžNØ }.Z‡‘Tk•j”…?m%3¿ûù+·’æ±PÂiÝ¥7ƒ/êO»Øê÷é~fÌk 9剗hQê[ÉãQ_ÇKµÊ$Ä8ÈŒ'b¡ÜúÊÝt¯Sª‹{ƳqIõIŠs£Zß_•ÖaUªQ®E+^áÖ7vÐížÕ1¢Üq®³û»ÇytŠÆå/=ÅÀÄ꜋ ²ÌËß$ðÀÛ­ˆ.>±X*añI$bpRÍc¥V ²×ÿÂ÷Sç°tÓA M°E&ÓªK?.»ülR\ÇzÐW’¿cp¸¬CѬÿTºGùáùgørwSú_~’Üø³w³yW°ìõüðK ÅÑKâÛðÈ@Þ~rŒŸ2{à3 *½¿ö3ëÓŸÉ"2ò1ˆÞ Yµ¨ «äÇQ÷+ů?̘U…¡7œ1 hѾ}õ§K’«š3ì×ò8âPƒý¾Ò˜¡Ü[?hýøŸßއê]‡ø6ÂÝ£×Ñë©^­E?[¿}“/Ö4dÈ-Ãi ~GÜ'ô|[•äû¯üœþŸ(ñwvLC)ùþ¨bFÞ!òÌŸ¾`~ÿGèŸPúCô ¿~1‡ <³³¢Z|On~²gèµØûl–~ü o/Ì#©ë9\:¨ q6»6°9ÏDSŽÇe•úy<ã•ÿýhŽ^/`áÙ6…FOaGT*ýÏ¿––uíxö¯gÞÌ ¼´d #ž¼‹þIö£Ä¨,­‡·{Ìõu\UU&‡y·þÀ{l„)«™s¯8ŸFÑ*…‡¶³ag–z¼·}"òQULóðb•-êÙÅÒíõ†Üµƒ¬ÒnœQüwÿ.¾¼ë>~ëò<ï\Ûœà©5›™]ÁLR¸fÌ‹œWWÃÈ]ÃäÇ2mÑn #¹Ëy\7j£üDä"7õ¶0iÒzjG˜LlþÊ |¿3‚Dg…é…hØÿË<õÅJ2ý€3öç^ÃmÃO%N+Ÿ½ö)¿oÝO¶×ÜÔë4koFç ªXÀ*ÜŸeò‚Ídê ¸ciØòL®»{8©á X>öÎχ~e}†ŽÓ’Þ—âÚ³âTtöýøoN]Íîô€½n' nOÑ’_X¸ö…V8z ã¶Qiê]iêé,þæC>Ÿµ’•ÈÆ§3ä†8¯e8 ¡˜ÓV³'­Ðê4ãŒKoæú~)”|oà_Â3#. þ>/¿5‚&•òKöƒ*Žw+@Qn!ÔÌ]WµÅ¥{È9°•å¿Má­‡~£×­p]·Ø6˜kp ”;µSáz'úüV[Õ½©a_‘ÉÆõ9Ô9íVvkÎá¦ÜÿÊg_%ùþK?§eÆRql‚ 3[íf¦ÅQ¿% ¤“‹ºîíL™²™ž×¶Áú-Xó=Ów‡“à,¤0³€*üô&/}»–,?àˆ§mßáÜ0ôb503çðä½_ÿ›Ü‘jãÀ/ñþ¬uìM/$hQ9ý¢kÑ»!G~im‘»|<-̧õOsoÿäÃ'•n½8·x)ÓÏ¡%“øìÛ¹¬K÷¡E¥ÐmЕŒ8·9á `d²hÜû|·b7‡òü€Bxƒ.œwõHµG!tØoýÛF~€»û¼qs ò|Áø“­ûóÐènwñÜÍqlfÖç_2cé.òM'uS{sÉUÓ-ÑJy(mU”wuëÿn¦½?…±ýyèÉËi*¬.ÝéÕ³-ï>ö>Ÿ4Žô%A p`ö'|4k-»Ò‚®„Vœ~Þå\zV n¥x[9üüÔMü @C®xùIú©sKÕ— °ðìžÇ×㦰`k6{,-z^ÀU—A#—R­²5²Vðí'_ñ˺tü¨„§ô宇‡Ó²ŠoT«,“bV.NY„¿óõÜ‘4‰šÉæ~×¾·¯äÛÓP}öðíÃ3¿ãã¼|eXx÷-äÛϧ0oC:>ÀQ'‰&].ææ+ºCÕyôïœÌ[Ïgë,<àJ¦ë€¾¤¤/bÞò-¤{U¢šôâÒ›®¤W½ÐU@ ƒåßcÂì5òªD6:•ÁWàœfá(ª¶ÍøŽõöº¸9d˜%±Ìüjþ?yé¦k‚+Ä àé.Ä;ö^žÛ1ˆ—žD¢Ù¬š:ž ?¯`_„'ŸÂ9—_ÅàöÁ{E¬‚µ|5æKï8HŽ×\$¶ïÇ×]DÇè#ón¤ÏãÃq«QºÞÄó7ŸVêþœîœU¼‹XUìçÕ© ów)ɇ~âã±³Y»#´Ä¾<òÌ•4·ý¼UºŽª:ÌÃ#•÷§ØãH‰…uÌaí9WÐ)¦ügH©žå’ô¸ý.N±ƒâ$6NÅòïdÒÓÏòڟ뻉úF¦½?žÞŒçíÇÎ&N-ÓŸ—‹ßÞš¡#í|òþ—Ìò4êjØÇ/_þzæ\œñŸdÀÂŽJTÛs¹æþ‹ˆ 7É\5™&¼Á-Þå¡Ó¢°|Y·v?u.¼Qí" ¤¯fúgßñÒ[IŒyì,bÕ£¯¯èû˜úìcŒßߊ󯽟Žumä¬ÇÛÓ×pÀgÒ6r—Œá?cÖÓæò;xº}é¿ç½÷Fãlð×´„œ­«ØîïÀuö£–ÅÊI1å“u40‚Qƒ“PÌcì§Ÿòzr;^½´!v«ˆõŸ=ÎËó¸ðÆ'è—ÏŠ‰ï1ö¹IzçN:GÁ˜¾\sêÛ Ø9ÿk>ÿðyš¼Á5ÍmÁšÑÚrÃS×ÐÊŠ#š$[å= ÅïWy¼ÿ-¼-Z¶&BÚÂégŸÅÜ·ç¿~Bëf÷rF¬ XmŸÍçã§³hG.†#ž¶}.庡]‰·U~ 4Ò×3vôæí+ÂD%²Q7.¸î*ú¥¸Qð±é£Ãç€ø ö{#o=3Æ}ÅËöRˆ›¤Nçpå5çÓ®Npg³üX8á3&ÍÛD¦®Ò[ØB1ÊfÿDœßL2~y’{¿Iàþ·n§ À˺wî⥴Kxõ‰¾ÄWãs¤âë6eF.§§øÚâˆÏ{,­ÎÆõ—v§®0u<:äþ:šë ®[ïâÑŒ>? ½Šs^Z×?£GhL}·ôù7œ”žƒ83v' ¬dG¶Ž-¶ý®¼‰ac‚ç좣Õ%ù¾!‚ée>§«þŒ©²,*aVq~¢*š­–Ï1ŠÇsUúG=/ÝÖ‚ ‡Ûÿé·ü>øú%¨8Ào—¢õ¸‰Á™òyf!¦e*uZ÷åŠÛþ/ø!¸æÆN~Ÿ±M_æî®‘åÎ"òv¬c§¯-WÞ~IZ!»þøŽo>{ƒ°FÏqEÓrÝòV«g­ÄÓËÎLÂQaº-òW~Ê3ï-%¦÷åÜujÞÍ¿ðÅ×Ïó²ù LÂf²wýVrê_Ì­×7Áå;ÀŸS'ðÍë _º‘á¡3wƒ‹y膎D( EÔÅiÈÞ°” y­¹ü¶³iìàq¤à ìã‡W^`bv.ºa-œi,ùþ+ÆŒ.À9úZ:FP®ev¬õþ]s˜—¦ÑvÔy´+»¬Ó…K%±lâl§ÉÿÕ3ÈÛ¾†mÞ¶\q[/’lEìýóG&}–½þgx¨bhýpºÝxç7°ê$&Fœ²g}#s>ï<;–­põ]í¨“·†é_|Êè,çïìAlUeVÀŠOßㇴӸ澛HqzÈÈÔ¨k¯F¹TQ&ÅŒC ™±1’³íD«H-~üŠ«‡Òº[Tðö$†EÙ–r(Y y÷éYW¯7—ÝÚ™dW›§~Ìäµ{)2»CUû‚‘µ™µ{Uú\7]cL-Èg“dz>u—Ý0˜ØÀN~ÿ˜BëÿœM¼âaã—/ðÖÂ8þïêè[Àê)Ÿòå«ã¨÷ÒMt ¯Æ±RYVgØÛQ—1(Ì.Bi0”«ÍàÝI³ÙÛé"ÚÁÌ^ÊĹ…´>’°o>"3χe9Qû«8&Bõ­µâꇆÓÂ Š½‰šÅö’tY`zÙ2áy^û N»ô†7€Ý ¾a«/à{ìI.mêÄò¦±qÃA¢Î»k[GÈ\Ǭ¯¦óÖ‰¼r_ObÊ4 ¤ýñ3[Íú\6´+±jÅû]•û¹ZU}Ø*Í_ m%‹·œ5òNº'Úðû#IÒÌ*Ï[ eÅ*ë´¢ßK³7eØ=WrèµÏ}ão4éÞ‡sœË™mp(İìD%5"¥Ñás³gíwLÛ]ŸËßIß$ hNÒÈ?¹ñÍ_Y—Û›3£KÃfR˜Y„ån@b— Jx€É3¶söÈæ¦0mo#.¼·u'8ÑÓsð™àVÂSºÐ=%"%ŠÝ¿ÝËÏ«áï…ÝpP·]:wpíHñ¬ä¶/°­è,bþ¾µy2ßo £÷£÷se‡àq‘þÚô‚`žiÌûæ¬pÛ…W MÊMìZü óçíæŠ‚q#›Ò±S{’4h¾…¹ÿYJ‹3ûpZstlŒÑÞ\±…üK“÷'ßü’MêÍ£Þ3hzcKnÿŠÙ[½tÄ<¥sG’4èØ:’íËŸeù²C\Ù¬~hÿ '±a )¥'”:ÖóhÉgt¹å´z;—Nå§eôèŸé¿ñæ _’Ùå2îÞmï<Æ}6†7"Fóäy‰•ŠRî^O÷è(ìÞ=,øjŸ‰§õè‹ih+ß+ßòØÃ´—^ešÚ›«î»šäÀV~üt¯¿ÇË÷õ"V)`íg/ðáB7§_2Š ldmšËw»ÀVaÞOàù­$Ýå+Æ‚Z_‡TRwşѡϞ³¯½“.1¹[çòõ”÷y+¦!OJÂZ?¢ûÜ{^}ì(8¢ £ês^E×?Nߟ¡óïMÜoÏ|>ÿj"ãcºpñ›YĆã˜þþg´|õºD((Ž*ê¿Â|§.;Ë‚*ë@©º,*=*ûƒÕºÇ¬¶sËm„zð„e¹“IèÔ—þqO2ãçœ1¼ Æ–øñ@ÝÖ–„ïè¹x- §î†èÒ0¡i£ö,|œßÖ§áï­T7uÉ7±)´ï˜J¢©-#عúUV¬<ݦ Ê8z&ÛÓ-´†mHtT’j3…ß/¢°ÅÕ<9âÌà·µm›“û0ÏϘÁ–>×Ñ:Ô†u×oKÇöMpЖVñ¬zêwïóѾe(¶+Žä† JíöÓÕ„NÚP<ÔÜ»a3vEÑç‘›¸ …H¥U#…=÷ç»Åç“Ú7.”[娥]ýz±(:´‡<hÝÐ]R–‡©D7kB8ËØž¡cÕSJʹC§v$jЮC[’|óú´™l?ój6¢“iÐÐQ²³L}ùÙóëTÖ*§pÇíCé¡­ivˆûÞ™Ìì}§24©Š²mî#'OG‹iJ»6MˆS¡qs*ÈCMˤ˜Ÿ]¿þ¾z}¸µ¡UíÌÀ_óö‹IïÚ„2ÃÔ¬r-,töý:™Uf;FÝu%§E+€ûR“sË._ùþã -M‹©´‰ThÓDaõÂ78ض=;'¢Ñš„¬E¬œ´’½¾ÞÄê«ø~n­¯ýCN ~[›22›åLâ·:´S«A*†ÖÛÓ&2tr1Óù©ŠóV‹êÖWh™ê,Öb0ŽéËþ5¿óëϳûÔT¾ì8‚Gï=&Úá>³â:92f€Ìm»ñ°ñw^Îø2Ñ“8TÀŠ.}7ðä3—³ç\Òžé}Ëò ®Ã;ñw§ÞEïzvF:aW6E¦EÍÏÁ?&2ö»lØ›…W Ãá›_/wÿ®Y?»‘CŽ×À eý™›·R`oÎiÍàLNCÑýXwŠv?ÇÈ…eËOKË'Pî bZT"‘’QìóCq熹xL‹°´ì3 rÞ¹‰aï”iËò`vDLì±4Š5Y…äºjÕ]Ã*›ÿÒùkAC'lØ™‰Nû™ÆÆðþ<1²M@ód®Þ´œ—þIú€ÁÏï ŽK‹¦M×èÐÿ›gÕ)°~º{+˜`bo˜¶¤Ô‘†röü6éó—±~WVC0¦r0eÚ}n~yÎ~•¿£i k JpèøfîÅŠâ?ÏÖ=¬Ý ‰ÿkO¤ç®o­»·ÅøÃ6ïwÐ1®Ò|ST+¢)¥ X«š§Jyðf§`ãVJLmé‘jñ 7DdÑ5¬ÝƒmhlÍiV«¦íR‘†â'£U·ž #³o[ Ë6óßA¶Õé}§ÓJر.ÒN'#Ľ ÂÒ{Ÿü»£=‰š×­MïÓñÕ+pÇ]+<ü$N”M¢%ÇÉkY'¥›øfi!éãú‘ :—³óÈ~„?ù?ïÌ驯ªëÂ{ýh%lÿw¤ýŽJ•eñ÷·Úòã=Cñ¡°y‰ë¹añá(öŠËulyØíppð­›¹ø-ßE2î/C׃kÝWTÛ°Vµ¬WÍJa)Xâ,˜Yöæ IDATbûqFŸOyáÓe䵉åóï hÖI´*g¬­hz ûDŒÿ}ÂëwÛÞõìpDÒ«sžé†r:†óéªõä•÷&´J:“ã0:PhÕ ¤ÒÙ×yñ¿ïè”ó4K­Û£Ö}Þû÷Ž[íÕZÒ«’x€ÔRºÄ„nÃó×ûÜùÈ^ÿ®ŸàôºJ—>z%ÙÞ57ЉÈ*%h”´‚%³ªÝg'N}™N¡üßhNz²á „YÐË ±j`Ûñ O¼ð9ê°Ë¸éŠL¢ÙÅÂç_âךòd0¢¢¡é`ÛZÓô:ºCň¡–CPìI·qÇ)I>ç$58žêZ`+3Ft¯&Q F³tWS8]G'˜¾×ÞÏÿù´ Q°ÄD T\…z R1¨ Õ»U`ç–*ŸÝ“zŽ•¥ìÚT_óЕ_ûŽžG‘¢üìZÑFÍüˆoWo#¯HÃìÀA+ÊmºŸó•û³›wPÊnfÝy³|fšH^‘[é&rõHúdEc¨|¼®n™Žæñ­¦ëêpâ­¦ã¨ë8—kPâÀ˜UÉÏaóª1ÑØQ@‰" ί0Ê8du.W­Û¿ÆåÚqµ¯ ¿û|}÷1q¬«WçšF =9(-´‚9 ‘ÝOcÀ¬§™3k¶ ‘ ¹?›0„˜'Q‡Žm×^˜üê ¸ú‚t¢ØÍ·¯½ÉïTî I÷iÕ§{z+TPUеjzO2DÒ*»×“gëB«ê* ½»öšÞÓ³“w>ôŠqtEEAGó鵩šÐ^kÞ.”J8¿ðïöF%Uî ÓAÁ¦-CÛhºî¨&ßà®ÁÓ4ÍoÏW¾ÛËk™«Y·ÞëÓÿºUˆè~!=?ˆåß-dÁìøf~o®¸ï Žóû* u:Eÿ,â·";åï߯%ïû]üÃ6N9·æÊÛ×'Ï P (Þãy¯?(?®»ëLÅdÍ¡y¦Q Flhž²L¯Ënu>¿ã¡b‰ÝVÍ6¬´¯fïeµ®WG)­`5Át8u8 ÷ÏgƬPVúrsŸ(T 5딲:×AûDõe²êºvÿê½U´ˆ®|Lq}S ¨^Û£‚‘¨ÖѰj+[:ÈŒ«¦ÌÕ¥œSÃö¨vùÜéèUÖ÷pÇ­ŠôjÞ¾šçÙõºô.©žÙ‡Ìà…ü½µ»W­ €b ÆD‡Ê¼Œ7ÞŠ }-­ÑŒp=SCÎ(;ä<§˜ (“Q£[ñõ”e¨¯ddàÀbë!¬Xw­e7L?ŒîQ 8‚I ­q&>jžÞ@TÛdLå›ùsWÝ2,U0'Ð1VlÜKproªvÊWxfÜIÆg‘¢,bËv…„Ò¨Ú—^5™cˆ lÅ”ÚuÛÎÎqjÝßý”{Þ¿l+‡˜6ÑÝã%ŸÆíWõ&Ò{W2†gÔ)ô$é]ÖñÇ;Ï3cC{N¿àFº%Q²f:ÏÏ.¯2ÏêŽkš®ƒ’Á„»/¤“wT¬˜ˆˆWÑ·9Ïlîãªsbïe«t]Ñ@Ç7T4;­b}V¬âú\‡TÞ\•®mª_tTgDïÉ{åùþ1¯ê5è¨&èç6PÕ„ ‡®¡k…nÿJË]e}pŽ©m]øY©šôý!êÉ`¨Ç3fš¦¡ù-…J•)ƒ®¡™ÚpÒˆ¾ÿhjæ I1 iŒA&(;D©]£d×:riË¥§ $;RÍLrˆsçqÎËuÒ44Mñì,š¦¡)€{¸{|ŸüDÐåÄLÔ¿çãe'0ñ¸Øª=Cã阫W¯ãÀÈWUµ•­+·béL»Åˆèšgºçiyg¾Œ#auh„yN8ZÕü¢Ù¶5Á¶u¬Ü^Jv¦óÄ®¬cõ^HQÓ(wMgw8дÚO¢5o0¦Àñ±Køêã¬ëtY!ijþ`î» íÙô‰SÑ4[5ù.cûêíh!h® Û‚0QNQ©Ãg¾>ÛK"¥} ,^ÉÆCýèæ¼µhãJv˰d3z­ëÖ™¶Þ–þc®¢ÏÀyæî©|¹âLú ‹«±¿«ÚÖ ÚþüúogqÛ9YTܼw°ç»×xã×oÙ0æ:dÝ¡áÐ4 ÞyÖ-$¤…ÊUl)îCNˆ‚÷v×5 Ú—±â«³Ï?ÿ‹Þt!¼Êî`9¯q{ø[>ª– ì¸åjJ«iµoß@¶¿^´šßú…àÎÙ´KˆÀdßϦŸçòK©…^Ý“1sÐg|cL&íBJømæ'ü4.Ë}˜sN¤[§385usŸ{šÐsN¥{J0¶ý;ÉêÏIÝ¢}Ѻ’˜\’†\ÄÙÛ~Ä4â8WG! æX5fAIYÄ3ŸÏç|KÄ vDªûÈ-­uñ|òÒ<öŠ•qƒÚ­“»ÃJÇ‘ƒiuïVyŸ/Û–Ìú†u3ñᣯù䇎+'¯8™þýãªæ°džÌ)­cîä7‰8{™–<~›7›Íaý¹¡O4Š®c ÇÂ:–-ßHûAéDk9óÖ´]LiŒ¾ìÖ>ý%O=”ËIÃûÐ>ÞLiî?ü´ðG6زÑ âU¯õ¸û'>ý"˜^m")ßò#-.$éÔá´3k¢ÚÐ:¸”•óð«= saæŽýé䳽̤ Eçï§ðöksÎÉ /\×3WAÎ… N1TœXü­[­˜ÿ–,ewt’Ê·n"_3ntΣëDÛ¿’o7(d]Ö‡¬Ö>'ì¤Á½‰^ú3ß­;D‡ Q!P°f튧G¼wž´z")KæóÖ[‰üß°,"J6óãÚ"0º¶;µ•ïHg9÷º¥W1®çÞžá]=0Šç¾Âkêiœ˜‹ÉšÏ®¢ jGˆ¢Õº¯øÛ†ÙUzÛ«ÛzÅa¥¸ŒA®m¤DÐcì ú¶˜Þƒâ1è: ‹ŠVrˆrM#2€}"8%hý;>›÷æîáïsq|gŸå$4›Ó‡Äñħ“x×t&Ru¶ý<—ÏöÆ3ò’΄Rù˜¢¹Ö»s 㵟º’†pñ©+xâ‹Wx`ß`N:®)aÊìbãf½ÏMFmå\·×²=ü-_Â*— 5¦Öã–b‰ T)cÓ²•lIëCWj®uÎVß~Gqˆ2çòýìÅÌ9äTB“;1ü²8¯$ŠÝ70#8› WŸLÞÛóxù±OQ"Ú3æÎt‹mÇYÞOðûSùâÃçùÂJH2=Ïêʰ*Y%å`ˆò4TÂ:3nbg¯‘TL!f°SlÓ1µ=“[.ÞÏsß副§›ÃéPÇåµM¯wàüû¯C}kŸ¼ø(Åj4mZ9ó¡*  }üu<¦Íä½9ŸóêÒtLDe eâà+0C ¥Ë%s{ÔûÌüúmžùĆpZý†¶'4€Ó¹©íL»×>{ƒ'¾‚Öù±_MÕotOY¨mw+ÚÆ¿kƒ rX9˜»™•K¾genÇ]q!ý¢A× ´:‚Ìųy÷å)Ù´P¢½{Q³O¤w¼±ú}ภÚ&ùú÷|Ýq8₱í<è|VO÷:_¹ö­*å¾ýÉ Oþƒ“'rÆ0r-Øä²?¢ƒ³#Qò9}hë‘ÃÄœàÊ æ³Mk£Ff3fb6cü`jÍ9“fqŽç±ý.æÑ~Wՙ͘žðŸ–g¦qœôÔ,j{|-ü¸‡˜}œû[0é§\ÇS§\WýÈq#xjöŸŸ‚»Þ”ÙN‚%e —<0KÐÈÿö®~/Š(÷;[ ©ƒ.äÞAV3½…Nße¶×/j•ã`Ч÷øÛè=¾º•«¯B÷$ Šë8 Äȵ·©Ì™ó5óÞú‰2ÀÓža­Ð3Võ³w¥Çù—1òÃ9|ñÆ |`"2µ;½Ê¾UµÜ§ñ¿›o$hö'|3û ¾±ƒœ@×ÑØ)#fÚq 7‡Îáãïf0i¡0ŸA¯$‹ó:Ä{yèøFXw.œ8š§ÔלýÒ["’èÐ'“^ŸëdL^w$Ü5Šº¿sš«ÆÊ}MUu¾Àáó šã¯k<Ý«ÆÌU«†¦¡éaµl?ËÝ=Í7ß\wÕº.¤ÆL4ƒÑ€òÊ‹Ïè^|y@,]ò#GaÙÒ%d´Ï¢¼¬îmäEÃÙ¾mëÞ.åløàžÝ:‚Gî9‰„Ãí'¦ùuRz1«^»‡É%ÿÇS·ö§–8§YiÔõ*ŒÙbaÅò_~òhÊËeÛÖÈžÇo‹VP—B\¸ëî?™ÿÁ<þëv/ߨ»š¦®Í“Ùlá›…óéÕ·¿ìïBÔÀl±°iÃzºõè]ûÈâ˜PZRÌò_æøAƒÿƒ÷Þ:üîò©™Gß‘Ý.wÑ|žhfŽVYÕ ×±dE‰±„›íìßðsÿÖi^{BÝ5Ç-ˆZÙžu`+`ý¯óùfCÅv 8‘ίàþ ½ZLPæMöw!j&û‡¨/ƒÁØPˆÆr¤3Ÿ¦Íôbãh•U[á6þúñ[Öï-Ƙ£ÚÐýŒkÛ7²j3˜@Ž-{{ÖôŒ™p:ûÂû|(ÝÃÚEorÇ¢7˜={V5S5?Чé™ìïBÔDöQ_õìü£æ®—¯½þÆ:%7éå/Â×Ü.“^~‘Œ ã|©ÿ.b›¼ZÖ ¶^j+£Æä¡\}ïÐê2Ð|×]MX¯-É‘(#Mžg{JdV›–xÕÎUޱý½¥ó>ž5ûãVS!û‡¨§z¾Ç¬æ»g/¿ø|Óõw$·KKÙ&Üé d½´”õq¤kwÐ…2ÒÜó/α¶¿·tÞÇ3Ù®G†¬GQ_õ Ìôš»ÎB¶KU²N†¬×–GkiBŠ#Föw!j&ÇOQ_ÎL׫»|)„Ml—ªd4 Y¯-EjÍPfµÊù^!p¨°ü}yü»f5³»Rç4/0sùùÇïê3¹B!„B´í;°eóF:gw­ó´õ ̺vïUß$„B!„¢Y[ýç ÂÃ#().>¬éÕ#œ!„B!„8&©ªzؽtJ`&„B!„L3!„B!„hd˜ !„B!D#“ÀL!„B!™fB!„BÑÈ$0B!„BˆF&™B!„B42 Ì„B!„¢‘I`&„B!„L3!„B!„hd˜ !„B!D#“ÀL!„B!™fB!„BÑÈ$0B!„BˆF&™B!„B42 Ì„B!„¢‘I`&„B!„L3!„B!„hdÆÆÎ€B´tº®c·Ûp8 ëèºÞØY¢ EQ0MM¦ZÇ•2-šƒº”i!š Ì„¢麎­¼Œü¼}lÝúÅÅEhšÖØÙ¢Š°°0Ú¥g’”ŒªúoP#eZ4–i7]×)ØŸOáÁB{“¾á`4‰ˆˆ$*&UU;;â‘ÀL!Ãa'?/×ýCN—nÄÄÆt ÄѤiûó÷±æ¯U˜-âµaÕ2-šƒº”ipe;¶oÃl±‘•EppŠÒ4]×±––²sçvvîØJZ«6M6¯¢n$0Bˆd·Ùزe39]º“žÙ¾±³#„_QÑ1‡„°fõ*“Rü^ÄJ™ÍE e `>&“‰ŒÌ,Ÿ@Ç»æLQt]÷ ?’µjÞizÏÇû¯[pH™YlÚ°Ž‚‚ýÄÄı|ˆÆ#™hRÎ=oBcgAˆÃ2}ÚÔj×u¢¢"bbå¤)š¾Ä¤–-ý j¸ù.eZ4'”i€dfu¤"󸜟Á‘³Ù®;Hª¨Õ‡;m÷|œiê( ^ß½ç É)ilZÿf-„f¢Éñw+DSÈ iê%šUU~^LÊ´h-Ó6›àª«Óug°¤iz55fîÿ‡_sVÔU¤ J¥¼(ÕÎ'8$›ÝM h^$0B!„Ç4ÿµ^îß+whGêù.ïZ¸@)Š‚¦Kç;-…ÜîB!„Ç6¯š¨šj¿«§Æê‚6O^špï‘¢n$0B`Ûù÷\| /,;ˆâ…BK*wðá÷°#Ý¢¿Ú74D$¢qISF!Ž¢’•Oså³ûÿücŒN4T (ZÎÃW¾LÙ%/ñH¿My­/áù›Î ­ fé¬=ë >øz''žÓ CÑJ^rû]ǃýfóàgÕÍÉAÞ·sÏ‚n||³nþ¸†\ˆr· qïAjÞb.˜¾šÝåþ.€mì\ð4¯üœÄYßÁêGŸ¢À{°uŸ|¹›„±osÕˆX€¬{±lìÓLÿ%Ÿ§¶¥k¯¶hy¹„¦ŒîôêYQÖ´<~|ñY¾POå®{ ˜üèŸu˃}'ŸÜr)¯ì?Éo_å¹à´m›ËýüNïûÀüòüî_gÄS1\5aPާ³¶œ3Ÿùƒ5’ìl|çJ®œ•ï¼ÈÙ­Lh%”béÔ¥¢*í%Jmé5§¤\J¹lŠå².*šº»§÷ ©n죓©¹ó HSƤyî=BëJ×ñÅO î£2‚$èqüËêí*.¿{î¾…K(ü>÷}Èâô«oçŽëÆ‘±÷Þx}1y@/áŸiò¢rú^zß-#ÂþdêSïñg‘œ4ŽƒûâÀQÌž¦”vÄ€²í,_g#¶O_Üñ²ÓÚÀÖ_7R¬ƒÞûç|̤›F‘V}é±ïúœGŸ[ωÜň¤:ÜÃÓmÜø½›¨#H¯®&ZŸó&ŸM}” ú%`®œ G J!46ÔÓ$WÈ SœÎÎ5{|küQãùÜÝaÇîp ¹tï¥#Ö¼½,¯Ú³Zmé5R.¥\6År¸ŠŽ4ðj5¨W|¯ô_Añ;¬Aÿ{2\ñ]³–CjÌ„h†ì¶±»’sÒjЪ¨(²ºu!;B!»Êª%ϰ;çDNè„ló—²röJv” 'Îö'w€ÎW<Äøã£P€v—ðÛM³ø~³•]ƒ2£Ç¨2¶~ú$¯onÏ…oHŒ 8²ûD&…{.1D­¢ì¥È‘F…_AåÈå˧Þ`׈'y¼g8ÊÎJƒ­Å”Ú\…Š‘àÐ`Œ Ø·OçÒs'³ ù4žº~ 3Oš’+×»yTK¡fÕÿM… ¶ôËTxqî,–ž8‘É&J÷ì  ìeö€×¢Ö²ƒÔ”c*c'}ÃXÏålžù3ôÿãµ YùÐïKIà IDAT)^;…ÇgÐëÆËé `¦ýSX|EÅ86«Jdп$D3 M°QZ®×)\+/¢ 0˜f i$aӺݔ ‰Æo«œ#ʀɺC¯¸Sg0¡Rîlþ¢ëèÓçê»9»÷¥‚%:BNG’nå¿ïâšWv1äáÉÜrb¼O-DR8lÍ=„×AÞQÈ®jTaµ^9–²ö“9xÀÁ£¿õòß#ù÷ö™X~)/Á=/u¢XP NI èäcnu*÷NÉMù{9` "dï\2q!z$ÖØåCÃÐØÿÛcÒ´Çxîài ÌŒ |ûï|>w ZûóÙ6ÀpF §û„‹èûÏd¦Þ{?›F §wzAZ{þ[OnÒ(Î?!ñ¨6«Q"{pæàhÿüY^TÇ1¬S<&k; Ó8qh ^" ŽÝ|õÚ<öw¾ŠS’ö³qÝ~ç徭ØÖmˆ³DÑï‚QÄ_÷.½ÍeÇYX;ýEþ´ äÁ‘Ô¨'´¦­×/vc ÅHTJk’ª«ÚÐðû´Ø”ЉvqìùY<ýM6[úrw÷ÈÃîeÊ~p;·ìbǺå|õÑl~-ìÉ5“.¡ÃÑ©"®Úû]Xm¼†—Y‰4)XbÒhŒJ9çLf±v·ŸÊî ëØ €Jpb[ZGé¾½ß%íeÑ”E·é@Z”Ê¡-Ëøø­¥”µ»„ÓL@9›kLïè×Ïø%åRÊeS,—uМƒ›æœwáK3!Ž&5šã&>„>g*Ÿ|ÿ!“æëOÇ!—0ñœ!$×a4Äàº'cX4ç}3…_Š€‰ˆ”öô:ÅocÉ£@ %ç¸%r ³¿}çæ9ÀN«ã&Ðwp&¡ñðEKdÝÎoÿéØË'sÓåÞâ9ûí\×ÁBh·‰¼x<1ùYîøØAXú0®ñV†Ä6Ðmm­”ý»ÿdî´)ì*ÖÁI›ž§qÇ=WxÞt‰R°ä®xj=á©é1ø&Ÿ{29ÑGù´U— ­ˆ+wCñnž¾ögŸAé×Îäññ8k\ijVömþ‘¦¿C^”@§Wðüµçn $½ÀšÞR.hÖk%åRˆIùxÖ4ý¤“G4òÒ%?0rÔ¾Z0‘£Æðóßѵ{¯΢8–œ{Þ¦O›ÚØÙ¢Nj*·¥%Åüôã y*‘QG9gBÔÝÜÙÓ9õ´3°•Wßù»”iÑÜÔV¦V®øAƒ‡¢ëºç%΀Ïg·†}¹tMO^EEQ@Q–üð=zõi„üˆÊVÿ¹‚ÎÙ]YþëÏôí?€å¿þÌñƒ4í¢… Ž›«ÿ\ÑØY^$ B!DS¡{¿³ j~Ь±ZVÄe>Ÿ¥)cËqÌf Á@S!A²B!š]o®qYÝš¶Š&í˜ ÌDópîy; B1Š¢ ª*š¦Õ>²,r*eZ4'–ÓæÚ4ç¼ _M>0³jVÖ”¬á¿²ÍØmŒ¢mP;r‚sRV7HâhçËDK£( !!¡äçï#*:¦±³#DöîÉ%<"¢Æ+=)Ó¢9 ¤LC忀ÞugÐtê̼ç­ãî0_š2¶MúÍÛʶ1=:¿ÿN®¶‡½Ayì Ê#WÛËŠ¢ÌÈŸÁöòíM!„ðË`0Ò®]ÿü½ŠÝ»vJ-ƒh’4M#w×N~_¶”³Ñ4‡ßq¥L‹æ .eêÜØ¶Ìmç\ÌSKõ°M³–£ÉÖ˜m+ÛÆ—¾Äj´²*q5E¡Å„+áÒ^F·}]ù²àKN>…4s«ºÍÀ¶ƒï¸“ùmîdò 9TÔ»ÙøoÚ­Üócgîñ þ{èf¦øýÚqù¤‡­:»]<—¹ åï…ØQMÊ¢çqL8µáM:B4ƒÑHbr2æ` ¯^É/?-nì, Q…ªª„‡‡Óµ[Oâ±Ûl~Ç•2-šƒº”i'Ý÷£×Wëêç¹ôÁŸ)õ3eİÇxu\,)i©DG¶B­j…Ò˜±åh’™U³òMá7XV~Hû‘SýèG[׫%·([X¾šïƒ3xlj,:ø çÆž‹EµÙŒ(Ñô»ânZ•h€íó'1eSM<‰#`%-LGËÞ¼Ÿ—~*$¥ÏÉœ3:8Sy[ֲA^¬+Ä1KqögL||"‰‰)^'R!š4ÍQ묔iÑlX¦¡æþ3ÌégsïƒÃ±é ®àÍæc8ó6.ï(˜ãZ’Б›ž|IJ^RaÖr4ÉÀlMÉlšå‰¿c0H'Ît¦Îw4„BEüeú‹ßã~g`îþ.ý›^¡G¸×EÅDLzgœ-èË0ÿb†qdvÎ!ÝìIçàò÷yã§":]ðwŒLÁ3¨ÿ œzds$„h¦GÍMi„hN¤L‹–Ä»9`å'Ì”Ð4:vI@Û¿—PÀÔ¦39]#<÷%´}_qû•pÿ;ÜÞ-˜²M³xæÕïX·c% 8þÿ;…v{âÛ_ÿa¯U%2sÜp9ƒSLÎDl{Y6ëM¦,\ÉîR•ˆvÇsæ—3:+Ì3Ÿê*̤)cËÑ$³måÛ°«vö„î!žxÂÂ…2ʰ`!Œ0L˜È ÛƒCu°£lû‘Ì¡àÏ/ÿÀ3’s‡xeB!„¢yнCž@:Í÷7Ü9½cÿ?ü¹Ueäu÷Ð?Vc÷Ò)¼9û-þêvÜ0–Xûf¾9I/§Óù±‘$¨%¬yÿ>ž^œÀW>D¿¸Bþ˜õï?ª“òÚô S½òV©ã| ÌZŒ&˜•ée”[e RJÉ'ŸMl Ÿ|J)EGGWtJƒ­XËË'³¶|6íÕ1´Î&Y¢2!„BˆfGoç´¢éØ³;]"ºd¨¬\ü»ºaHß t!)o ¿Oÿíe#‰/_Á¬EdO|‚óE£éWïgÙ5Óùf£•^ÝCŽrÞEch’Yˆ!„`C0vì”SÎ^öL0…p€ìe/å”cÇN°!ˆƒÿÛàt@‘FöB!„Í‘»ÒI×tOÓ@Ÿæîß4Ý3ç3 køüî=¾®†Â`C^1vMGE%<1Å^LQ™FÙžØéppàåË÷²oÞLûJÑõ`ÐùP×üP RaÖ’4ÉÀ,ÞA3bÇN)¥ìa„ãê•‘Cìc¥”b×í´ÑÚk¬ã»TT#f#ØKËñíèW£¼¸ fŒÄZÆhZÇ€c×:òlÝicª[6„B!DãÒuÝTáj$¨ëÕfºW`æg¸ó?^㺊ٚCCs̓•r®@N'˜¾×>À9éÞM°,±ys'쮨”Ѽ5ÉNÜSÌ)D«Ñt.ëL1ÅRÈnv³Ùõo7»)¤bŠÉ)Ë!Z&ÅœR·™"h“„ã¿el,ö*Ðe;øýï"”¤Lb ²Ô(ºë€!o-Û‡<-„BÑ̸ƒ%Wh¦£û|ªøL¥ß½ÇÀgZ* ­î7wj¦ø,R”R¶l‡ø´TR=ÿSˆQ«ä¯OReÖr4ɳTc*ôƒ\Tz!š¢P-¤Œ2Œ®ìÚ±SFáZ8Z/$ÄBª©Ž!t8m)Ë?ã¹Ç ŒÙdCÿ|ÿ ó£|y¢jh nड़÷ó½|8ù.\;š¡ÝZe²Q˜»™uNO"›d,„8t]Çn·9{°“»›¢‰R£Ñ„ÑTû]I)Ó¢9¨S™v‡IzE­—ó»o­XÅ÷Ê5f¾5m¾5høt.¢{j¿*Ò%²'c‡Æðè¼'yNÏðÎñ˜¬yì(LcÈð,B½®IÝyt÷Ë(ϘµM203*F2Léäê{y¾øyÞ ~‡¥Æ¥ž‚§¢r¼ýx.+½3’‰cQ,íÆrß½¡L±ßø"Z÷àÌ[ÎgL—°À_ÍbJáä;ž"qÁæý0Ÿ·—¢–èÖt꟎Mö!ŽYº®c+/#?o[·þGqqš¦Õ>¡GYXXíÒ3IHJFUýßM”2-š‹@Ë4àpyµgô­òD_îÏ•†QÃ4Õ×½¦ ¡ËEq[äÌZô6Ï|âC8­¿þCÚjðέâêß jð(š·&˜„)a¤)F •C<ЉµŽk!ëòÉL÷7ØOÓ¯¦Ç釙!D‹äpØÉÏËãßuÿÓ¥1±qµ^ q´išÆþü}¬ùkf‹…ø„D¿ï(“2-šƒº”iÀ'ó4¬ò(уylú‰Î­ëuUJìpžœ1Ü5žNpÏ{˜1ÏwŒ­ÿÒtÆ»Òàîw2ÍgœXzu3½ÎòÊ—¢  ûÄzî^ËÝÕm¾Äe-F“ Ì‚¢Y°+vLª™xºP¦[±iå„k¡(×k !ÄQg·Ùزe39]º“žÙ¾±³#„_QÑ1‡„°fõ*“Rü^ÄJ™ÍE eÜPÑìЧЧÁŽvÓ]ï7–¹¿)î|(îí;Öî+s[Î|èÆ¥óçÇoòÁW«ÙcCòHxü|2͇9—ƒ[X³¡”Ö=:m¨}|!„âhѽºÉ×kì™±i¼·Ï·Â¬ñó#Ž Ì„he¬ëF\u<>>é^ÁŒuÍ$®y|3'?ù(}~¼‹{¾È÷“†®·NæÎ!€NùÞ•,øès~øcyVÀM›ì~Œš0žIU;ÃÑK·³ô³¹,üi›ö—ƒ!‚´œ~Œ8ý †dEàŽ´ý?óÚ«_QxÂÅÜ> »Ft¢ÂoóÒ¼ô¾àf®hŠÃNêa÷¹ã à×7yjf*dÈU—q\´ Šˆ6A€Žõ¿Ïxü¡ØÜ!ÿ»„ì”0ôÂ]¬_›‹µš®¦´ÂÕ|ðàÓ,ÊK¢×©ã¹:=õÐ6V~»€wZÎÚ‰3ñøX @ùŽ?ØlOç’±CèåN«ŒWn¤,u4gïI²R¢Ñ•³iÊ­Ü5­˜a×<Ìñ[ùôÅw¸õîp¦¾2–T9›52Ù>¢™ózq´wçþzf<Ú\¯M«àó²ë£žÑ@äP)D£QJÌ$;Ñù­Ä;ÉYÙä$zEB¶m|þêGlŒÊ]L×ðŠ@lÀðj’ÕKX;óuímËÙÞǘ¶WèÖ—' ë©»yÿíè—}ý"4{)66ñúÄóyÀØÛ_¿£Õ;grË3év¸Ž7ïíJÞ7ïóÖÜ¥l9¤ƒ9ž^ÜÅC0 S²i¼?¥›â0Ç“=ü\®8»ñîš¶òI!„¢Ñy7 ¬ü_GÅ»K}¯çͼßú|¸¼2\sEqÏGA©T'¦»NðÔoö¢éÀLˆ¡`0ÀVJy¥¦VV‚ fC –BpB*a,eýÎô´ˆZã-cd+’L°áï]XE¹šU(Ï]Ë. âÚÆ`ì/˜[ó¿{&Ñ{Õ÷,øìs^½o_œ~?÷už4RÏäžëúåÝ ›1Œx¬Ë|„¨ÄžH‡È-tx~ÓËòÉ-†ˆä9™52Ù>¢¹ó~™în%èéÄ9Ä]Cv¤Ÿ7sŸ×«K¥¢Ó¯ú2Å«SyƬE‘>l…h&bÓQŠþáå?ëEløe3K*m£»T±´ÌqQvþšó%K8ø†t`d¿0Š–ÎdѶrß½-—g~Ï~s7Fv‹ ¸·Dr÷S¸ì¾g¹ÿ¤þûz!›ËCHM†½9žBjjjÅÿÄHÌŠŠ)ĶJírugJêAv¸•5?ov5´Õ)Z»˜ Z<=²c¤™\#“í#š»Š÷˜yê¡<èhšæÛÜQwþv$‚"wèN¯"ôî€D¯¨ÕÓ+šXzò.Z¹‰%DƒPˆìqƒ£ždþOQ>v(Ù1:¹«¾âã¥E$Ÿ6†N!&Ôž±W åϧ?çÁ»w2êä~d%„Bé>¶®ÛJаóÕÚëEiJ(9ç]ÅÐuÏ2óÞ{ùoôúfÄ nåoæ³d‹…>W]L¿¨ºß—±çýÆ7«¡U›X‚lyüµ­‚£ 6˜i}òh²¾ÂϼÃÑ}h¦Q˜»C·áôK0š–EŒ¾¹scéAÑ^íOèC¢ôÊ(’Ã9gµã«wáÙV×2"~Ÿ>ÿÖœÛ^zükt²}DsçÓ¹‡ó¿âÕë¡¢(žŽ@t¯çÌê9o×]RÕÕ¤Ñ9?wºŠç³¢8ç‹ûåÒž¼J`ÖRH`&DQÂr¸è¡›‰ž2‡o¦Oæk˜¢3è{Î=L8¥_ª(Dt»ˆÇIgX2}2óí€!”ÄŒî /·£cö©ýR#ºséãÒáÓ¹|±x¯Ì³!Œ”Ž¸ð¾± ïyXw°í6óë' ù°ÀˆhÛ‡ó®C[(‰#¸ýAÓ¦Îç£I?PÅvâäŒ!ôK0bjsWŸ¾7¼ÍÓ‹ ¤ÕP®ë×›DÓaÕÛ‰cŽ™ŒóŸå ëã¼4ùn¾*"íøKxöŽ3H“3Y ÛG4oÞ~T¼¾Ù+æòîüƒ*ê9s÷÷ùPñtòá EGs>êVz:#©DÓ ‡K!)®'ãnêÉ¸Æ éq;S¦ùª–>˜ nÌÎ[ iË sofйµÌ·çÝL«2ß rnx‡é•m?ž'÷7GBÒ‡sùýù¼ºÁj8ÙgÝÉËg’{!ªaL`àÕ/2ðêÆÎˆ¨–lÑŒUnèéõP÷ ½ÝB(^AReÖ‚H`&„B!Žyºæ pEqÖF)º‚®èÎgº÷ÛÄ|£zθRl§ë:ŠªzÒV]ŸŸ€ÌÕÐñu@"š ÌD“sîy; B1Š¢ ªª³G/!š¸@Ê©”iÑœZNÍf3â)ãºîîÑŒéîN7WǺ®¿>Úár¥çy4 ”ŠA€¢*^£ëì/ ÈTÿy‹&A3ѤLŸ6µ±³ Ä¥( !!¡äçï#*:¦±³#DöîÉ%<"¢Æ)Ó¢9 ¤L¤µjÃÚ5Ó¯ÿñØíÎ7|*ž^5@õ`•±z¿Ç¬RPç|Ƭb˜+,Ä]K¦8`4™Y³æWZ·kW¯ù‹¦C3!„h@ƒ‘ví2øçïU„„„’˜”ìi–"DS¡i{swóûò_èÚ½šæð;®”iÑÔ¥Ldfuà—Ÿ—ðËÏ?Ò)§+1ѱU&¯èNq=ûåýýpÕ5]]×9p`?k–ýŠÝn##3ë°ç-š Ì„¢ŒF““1[ø{õJ~ùiqcgIˆ*TU%<<œ®ÝzŸ˜ˆÝfó;®”iÑÔ¥L  <׳ìç%”––¥œÖ¢(Ó¶]:íL3JÑ$H`&„ HQPâãILLiÐ.–…¨4ÍQ묔iÑlX¦ÝU¥}VGÚgulàŒ Q= Ì„â(p8857¥¢9‘2-„G–4 B!„BˆF&™B!„B42 Ì„B!„¢‘I`&„B!„L3!„B!„hdÒ+£B40]×±ÛmÎìt]×kŸHˆ£LQŒFF“©Öq¥L‹æ .eZˆ¦@3!„h@º®c+/#?o[·þGqqš¦5v¶„¨",,Œvé™$$%£ªþÔH™ÍE eÚM×u öçSx°‡ÃÞ¤o8F"""‰Š‰EUåe‚-…fBÑ€;ùyyü»îrºt#&6.  !Ž&MÓØŸ¿5­Âl±Ÿè÷eR¦EsP—2 Πlçöm˜-2³² q¾L½ Òuki)»vng׎­¤¶jÓdó*êF3!„h@v›-[6“Ó¥;é™í;;BøCpHkV¯"1)ÅïE¬”iÑ\Z¦ öçc4™HÏÌð t¼kÎEA×uÏð#Y«æ¦÷|¼ÿº‡„ž™Å¦ ë((ØOLLìˇh<˜‰&åÜó&4v„8,Ó§M­öw]×)**"&VNš¢éKLJaÙÒŸ †›ïR¦EsH™8x €Œök¬yòŽÜ+jõá/MÁ ¢(¤¤¶bãú$0k!$0MŽ¿ \!šª@n(HS/ѨªðóbR¦Esh™¶Ùì„„†ú^] –·úÔœy`•«ü½ò|‚CB°Ûý׊æE3!„BqŒó­ «2Ô«¦Ìûó‘¬)ó® «m\ßߤó–Bnw Ñ9òcÖÇËÙc«n¨½¿Îeêüõ”ø»Aç(`ÕüÌ]Y@³¼¦[Ù±d6ŸüUˆœn„B44ïÓiMµ_ÕScuA›' k”‰† ™G…FÉŽ¿ùí¯½Tkysäóó[¯³`u>Zµ7Ílìúq_,Ï¥ÜoûømÁ~ÞRÚ_ªÆª IDAT<ØŠ‚m÷oÌyu«‹šå!„hN*uðáOMÍ ëKQ”jÓô×ÙˆgÜ&Ü­¿¨ Ì„”¶ŸïîÀ¹/£î±‚•5ï?É o ´–i­ç1ó¯HN¹xÉÇlcc mG_Ä ÊR¦|½³ö`V49Å?ßĉƒ1ÈëÿI÷þNI•1mä~}?§ Ä„iÛ±ûIO;°’)wOàäAƒ4l7¼ú#¹^CÛû)—Všß 3_æß²jsìgŌǘ8~¤s¼“ÆqÍÓóÙ\eçÔ)ßû¿p;—ŽɉîáË}R‡{8¤<ˆ¦.г£¡òük %,k9ŽÙË>!š$ý}þ…™8©µ¹±sÓ¨”Œ>)‘Û}ÅæQ—ÒÁÒØ9j™ÜÏKø»S{x4Ê•¢DãÞ'Ï¡­ @ÁÙŠ Jãí_ú<×?µÜí/€c7ŸÝ{ onùöî;¾©² àøïf4Iw Ý¥´…2 ”)"ÈŠ‚ÊÐWÜŠnPQYÊ%‚²—ìM¡Ð½2îûGWZZ,BIÒž¯ŸJzsÇÓäIî=÷<£ Žz”è‹+™øÙHžs›Ì䡵0ÖÌT²5!ôó:]µh U(ñc¤hQÓ 4ì÷"ƒÂ¤ìY—“ßçEÏÚÄ?ƒ[^ÙRþþŠg_\€åæ{èñh/j„…é#÷3¯žÔá®2»^ýËJrÕû•ŒY…!™׉š±›ÉoL`õÉ lhðªÞ’{~.‘¦ÂQzÿù‚aƒ¾Àtó+L|¼^Ñ “ÌC¬Ým&jP#ü5;æðªiLùq#‡’­¸Ô$"Û vÇÎ9ͺ™ß2wõ>.˜µøEF£KÍÿ€›96o4£~2ÑÌËÜ¢rzÅ$&.Û͉óiX}@Cºv¯O柿±iÏYÒUwªµìŰ¡]ˆ2*¨i;‰Ÿ0 ‡Ï”¥žD¶éN;ÿ£¬]û‡/™Ñû×¥óù·©?ÚâÇ 6sdåT¾þaGSUp  éý/ótû@HÜÊì¯f°r×y²ÑàÙ™¯ÝGm£Ž ­žÿ ëOÜGíš•;P-/V«•×^{‘#Gâîî~FÚ³‘q)_-êשMx)góñõÖš½6·O^dKþ–Sü8b0Ÿ^¼‹/¾FÔñ…Äo×q뻯ñ`kšpf;ÃæÍ`û€7iá¶ŒKdh«P·A]jû»¨)¶¿Úš yŽfùÏ7‹Á¼áwÞßuœ 57l—~çƒ×~"ü¥é¼Ú!@N–×Dêƒp~ö¡«MÖ,aYÅ!ß-B\'Š!„–½†q³¯ú¬c¬1…ï>  îû}ˆÐç­T­/¯ÃS­gÅ“@æs»9šãÏMÑÞy팭\\?‘·¦î!´ã@žn„zv;?Ï9X¸‘šÆŽoÇðÅ:wnéÿ8UÓqqßoÌ;j÷·Y±Z­yiØH9¼‹#Ù±<0¢aÚKìXð-‹§î¥Fç éŒæÌ:¦OŸÎ§¡õy¿W8ÚìsìÙsŸ1,Ö“ìk˜6}6SýšÓ§ßôõÊ`Ï¢oYôÙjò,ͽŠÓ|b1¾ÛBÕ{ždtcÔ¤³¤ù¢USØ<ù3–œkÅ—:iÌäü-Ay¯—®j]j¸ÿÀþƒIXk¢-—w®r3›Í¼÷Þ{\ºt‰·ß~??¿ëœYI;Ÿ†Uc#éü%ªùa,¾ËìƒÄþ†´ÞŸðdK_}Rôi›Õ‚ÕjCÅFò¾­œ¥CëyçÝä0ݦ!îów³åt-bܰ¦'ŽŽ¬ó $»àã¦)eÅ‹šÎñMóYpÈDÓ'ëᩘ9¹l2k’!òëÁt{=¥j=:Üÿ Oö¨…‡$I®’Ôá®2ëäTÁ›dÌ* Ì„¸^´þĶðÏû¥áý·°fì.ަ÷&Â7o±± áx—ò}nI>MÞ„ùæ}4­ ¬[´KÝa<÷@kü4@£Z¶ÿ¾´ÜUÔäm,X—Bµþ£vGpnðRׇë·³=Õû¾ÏŒ¾ùGɧÑ+Іê¬…šù}ôVj¶iËM5Ü a9›70qÛARï Ç7o?õâhXßõÂÉÜ´‘IJ3:´n‚u¼°îµl;c¦¹—[‘cfïM$ âbc©mB¡F^QRHJ6£õ¡a½TÕBtŒÝ ¢ó!ÔvžLÆ‚få!~ŸÅ‹£ÓéxóÍ7ñõõ½ÆàL%Ûì‰×™oÞ÷KÀŸØ»†óÒc]©nT€ÿ6³Ôþ|>¨FöÝ\F¯ÏVÑ 3‡Î¦€©þÆÂŽÎ76p6Åè1gið1îãÇzó:‚›÷ã©Ó:H_l…e¼¸â î~k;6À·íHÞº=$÷¤hKeϺ£ÚÃo#6XOÂú¯yg쓤ûÍâͶ~ÒAûªH}®èJÁŽ#¡üc;Q`(® Ì„¸NliX>ã{Vn;JBª ƒÉŠ•êäXËþnÍÉŠÆüOfNû ¨u ¼K9óçœÿ‡³ª-êøÿçÀEç„é$fäudW øU1Á±T²KêÛ®ñ÷7ÂÉ‹dØÀG Z¯ ¼È"%ëò 5ºÓ³îVf½ù,‡néL·nhíVëO‹{ÚñÓÇßðìó›éصÝÚ5 ÈwÂQô˜ô“i–¦ålíÚµÜ|ó͘L&Fމ··÷5gF˜Æ’`Ë8ËÎUßñáØ·yJ &~DÆ„ŒŸ•Æíõ£†(i@†«¢àÓúMæ,,)ÿk9ß¼÷)/Ð3õÛÁÔ(±¬‚ï-#™<é §öýFü¤·ò†žïÞìHUk2§“TÜëtåŽ[cêÖx‘„5ýøì‡m¤ÞÚ¹&º R„ó»lÀŠË=X‰¸~ä&׃šÂÖ/ßgÆ_FÚ<ðcÞ}gê@I«7 ÅLvÁPd  ÚÔÒÏJÞ:×ð½¬èôhQ±Ú ¿ñunZ°ÙJÙïåÏ+:7´¨ØJÚÀ-‚;G~ÆØç{PýÂ*&¾ö£æ"KÕàÛtN|¡7Ø5ëžyæ3Ö'æeõT3Yfp3éäÞ`9‹ŽŽfÍš5|ûí·Œ7ŽÔÔÔ‚lڵиÓ¨ÇS¼t‡‰«æp¶‹.äïô³ÌÞ%w¼NC™{^娤t{~#éEö Å;Ø2/p1«°nY’O“‚7ÁÞÅî/꼉hÑ—çG´ÂplkN•>¦§Æ#„šõ›Ð¶×3¼÷BcÒÖLç׳VPt´•”V8"¨Ö‡ðª:lÉÈøþ3©ÂU¨Nþ#*&ɘ q=XÙ,Ÿ–÷нeM @Vª?zÎæ­  7ê!1-7UJĦõ Ç͜J¶€ŸÜ‚¨oÝÅÅ;C ,a;·€Ú„iW±sÛrbªã´Cd(FBâncH£¶´þîEÞ\±œÃw>F=#è|kÒ¦ÏS´j÷+cžý–%[úÓ²kZK2§’Á'ÌG¾¬Ê™Õj¥nݺüú믴kדÉÄ£>Ч§çuĦ’w‡Wƒ»·˜›Uxq‘s˜é#F³£Ë¼ß/S‘-5øÔiJ0?²v_*[y£Å‘u;Hw¥YhI5^-¼aPÖ* VÌVtþÄDš°mÛÈጶĹæsì9iÁT³:ÞÒ¦öšI}ÎÆ•³N®\vQ”\ëqµR²k‡ÉîbAƒgD5Bõ,ûc!ËëßFý9'“íæàq#¸^Ä/cÎ/ܘMBZ8­[W+2ˆ[`=ª»-gß‘Tl‘þh´ÜÒ·5 ?šÆÛŸeЧm ~šö&ZÉßPñnLß®AŒ^ð>ÛúÒ¹~ úŒœÎÌßkñQËýºì˜u³jT«^£ù<;§ƒÉ“&ƒC¿¬åt•(B½5¤ù‡D›–`o74€%q?‡3¼©WÓOú—•3‹Å‚V«¥Q£FüüóÏtîÜ£ÑÈàÁƒñôô¼ªŽî¶‹›ˆÿñÁu"ñ×grzÛ&/¾DÀ]݈6€ÖLuO» ²³ðÑ+üÉ4¡)>j^TO6šÃØ÷ÞeÆ3wuqçž'â¾A4ô̧X9}%éÕkî«!õèÌÿzÙQÑ6\ù(|¶½Ì™þµkì©’|tó¾ú [­a´Ö:ôïNКy¼7¡>/ÜÉ¥ß&*˜»_m€Çõ~ñ+8©µ¨¥<.m-ÿ®†ô5«ˆ$0âj^Ì',.²¨æð‰Œú8w|ÏÂOÞa6€Ö„oDsüô  !¨ýPíŸÈÜï>f=n·|ˆF7WÃ`q¸× m==Ÿ®ÝARÛvøk¼æ­gü™6w9_|80ø„›SŒÄôÅ+^3˜õóTÆ.ÊíüîT››ÂM…í•Õ+4‡,/yÇ´$fÓË™vÉ hñŽlÎÀ'z©Mç÷½¿2mãé܉fÔïö(C›ù `!aËÎx7ah5§ÍVVkî(š:Ž-Z°téRºwïŽÑhdРAxxx”98³f&rrËfL=K ÷¯ÉM÷½Í4£¬×öw€u!ôó)ïeú¨dêiÒo /?X+wº [¯eîÌɜόÔmý0?~/ÑzÀRt¶Ì$Îïý‰i3‘l}´7ÕóF5Ågüh•÷'ç©e9èÒsô8‰-š¿ÿNêƒpY'ŒË$cVq(ógÇ«»u/ÓÊ~_M×î=ùyéBºvïÉúµ¿Ò0®i9QT&bfü Gá²öNæ™1{hÿÎ{ôÍ¿*¨„Ô̽|3âöu|‡÷zUÙ_‰+ÕÛÌŒtÖ­]M§®·ãíã[â:Ž”žžŽ§§'IIIF †ÂîªU«èÕ«&L OŸ>¸»»;×Ñ¢\ü0g&·÷¸sNÉÓ<;{¢¸«Ó»vl£å-· rJ xÊsré“ìüã+ŠÂ¦õk‰mçòˆ¢vlÛJ½ú Ù¼i=-ZÞÂæMëiÕ¦]™¶]¹|© þ!„³1Öº‹~õ/±tꯜµüûúSÇ–Meµµ%»„;uPVQX­V, Ka¥ëÔ©3gÎäé§ŸfÑ¢EdddÈY!D…äÊßm®\vQ”4eÂÙh«Ð摇9½Jµï!_¹¨64Uâ¸ëÑۉ󪄿X,t:]AsÆ|Ý»wç믿æá‡Æh4Ò­[7ŒF£d΄Ø¿µgtt $ýË** Ì„Ó0p£‹à4–,šèè"8ØR~tt®‘¢(h4šë2ô|yÊϘiµZ´Zm‘à¬OŸ>dee1dȦM›FÇŽ1 œU@e©§®R§…€²Õi(–ur±¸L2f‡f©Töþe¢âQww/àëçïèâ”*ðüFC£F8|øpÁ:™™™Üÿý,^¼˜-Z ÞDÅpî,^ÞÞW¼ðt•:-”­N á,ä¬*„åH«ÕUƒ½»¶ãîîAPpÈu™ìzËÏ–%&&²~ýzxà† ¤I“xüñÇñññA«Õâææ†Éd*hú(*›ÍFÂÙ3lÙ¼‘†qM±Ù¬¥®ë*uZTnWS§‹s±„™¨@ä¬*„åH«Ó‚›ÉÀ®³qÝG©ˆ¬¬, 7cvêÔ)î¾ûn’““éÙ³'C‡å7^gç¶¿hÖ´ £kv:ûvoçÐþÝr1^h4¼¼¼hب AAXÌæR×uö:-\]×nèÊeEI`&„åHQP‚ uº[œéééœýû÷§E‹<ôàƒdfeòè£ñÔSOñôÓO3nÜ8¶lÙBûví0™dB]‘ËYë´ÿ…+g\¹ì¢¨J˜íضÕÑEvdbr!œKÛ¶·rß AÔÅM¯§Aƒ|öÙgŒ9’¸¸86nÜDË›ZÊPùBˆ L¥°וzš9*R(ÚÃL²Š¦Òf Á€³ Yç¡Ñhxåå—éܹ3QQQøúú ª*Ï?7‚ƒîcÍš5ìÞ½›îÝo—€LQa¹rÖÉ•Ë.ŠªT™Bˆ¢ÜÜÜxôÑáhµZL&SA²›nº‰¯¿þŠÕ¿­æ®ž=hÕªîî& ΄—$Ì„ƒI`&„•˜F£ÁÓÓEQŠ]ƒíÛsS‹›Pwôz½K*„åÇ•³N®\vQ”fBQ‰Èì—  ƒJ%„7ž$Ì„£UâÀL%ýàJ¦Ç/å‰dzßpê·îÇÃ}ã«utù„…ªªX,æÜìTUîn §¤( :]2£R§…+¸Ú:íª\¹ì¢¨J˜Ù’61ñíil¯ÒŠ>7£š§JÊ©8¬zc” Lq¨ªŠ9'›Äó8vìééiØl6GKˆËxzz]“Àà+ÏW'uZ¸Š²Öi!œE¥ ÌrÎüÍ/ÚÌ]µóšê4½‰övë¨iÿ°|útoÛš*Zɇ¶s03–ûŸiK¨6[—2gòëœ0¿Ç«]ƒÑågÁ»ï³PÓ_L˜åK¿™ÉØÏ«òñK·REÎqBT³™£GÛ Žèš1Ž.Ž¥òõóÇäîÎîÛ -õ"Vê´pe­Ó—³ïmÎÓËÌþØ*¹=ÍDERi3mpžrœ§|ÊÓ„×¶#]º´¥QˆÈ:¸ˆ…‡¼è4ê)úÕ6ÚmiæèŠÙ¡iʳÏô§™—Ô£†ûYž?ŸU§n¢_DÞª^QÄ5nH°Æ5 4{.XÂávC¨v`!ËN„Òwì}´Ö5¾o}ö{RÚÐÆ·r~Ø äè"ñŸÌŒŸQârUUIKKÿJ•\"!®^Pp(lXwÅë=©Ó•”¥NàÊY'W.»(¢Òf(n„µ”Zõaÿkøeå|°b5{>Ç }bHýç0éúhšF‹n§fpbßïMMÏüO¹‚Wͦ„ó{Of¢F”0Š™âI­–Qh7âPr6ž‡OÉifŽø3‹¬Ìùt+øVÞ·¦´ \!œUYn(HS/á 4M™û‹I® ¬uZ-þ‹ %Ì$,«8œþê?Ë–ÅîŒÝÉ>Ì%K~:_"QÄšb1jŒÿ²‡+S Ô¹µ7uÚÜÁm ÞäÕy_²¨Å»´µýÇj~…Ír‡¤VQÕ¼öÀJ ¼>„ú&ûµôøI3!„BˆÆ•³N®\vQ„Sßî:ž}œ™‰3Ù’¾…³¶s$Ï“`<ÏY[[Ó¶2+q'rN\Ÿƒ)FªÅÕ§ 9vIÅ/ºFóa¶Ï*¶ž;ᵫÀÉ-J+¼m‘zp+'  N5S)Ùò,Nl;ŽÕ½:‘>ü¢Â1ªg8”åGhXa?xê*g3FQœJÊ®…Ì^sœ¬’¾s­—ؾd?ü}‰ÒZÍ«iÿ°âû9¬;g)Ï‚–óyþœ?‡?ËÚ/@!„¸z®Ú¸rÙEQN˜Ï>ÎOI?‘ªIåͬ^ÇájG8\ík£gsðŸ¤jRùéÒOœüÁYö¡øtÒl–ÿ¾…m;vð×úeLùz%‰JMB ¸×éÁmaI¬úøcfþ²…»wðçïë9¢%¢ËÝ4°maâøïY³m7¯™Å¸IA£{èf7WÆé5Ì]¸šÍÛþæ÷ùŸ1þ—dB»ÜFM#¸×íI·°4~Ÿ0–é+ÿdû®lýýgVì(ù"Û|r¯=ø#œÄ €JæÞ©<}ÿ0>\w1o+×åáûŸåÛ½™òAuqjÚvf|6?ΚK^Áz?—.eýÑÒßkkò.–/^;ÊZ±’¸ó'&}½‰Í„B”µÈ£ò£frfÏŸl=ží?l_RÙ…ksʦŒY¶,V¥¬"K—Åêðµxë½¹‰›ˆ$€£ÊQvxíà7ãÚlËÊäU ¨2ƒ¦„¾]¥°*Þ¸'­bÑäÅ$™ŒÄÜĽ¯  SˆäžWGbš>“¥ßg‰ôþ±ô‰nA­°¶<=ÚÆŒ©?2ùÃ¥XôþÔj=„×µ¦Š}+D½– ›f1~~:ƒ‰»ë9†Ü‰€[$½^}ÓôYü?åfP܃iÜ«íúQÖÆŒ*Iaç™*ÐëË̉y/ñâO‘¼2ñ b¯­õl™yòçxÖ+mxùÎhŒ•5‰ª ¦Ã·±|ällÁCµoÈ‹/ÊÌÌÙß§ñùô¥lØ{žl<oÖ¡Ï£ChþMª,¶¿Ó‹ÇJ)²eƒ×—0±£—~~”žcv—°ï<9g2}BJù6T³9±ü]žxoݧLghtñ dm¤˜Ç¨''’ÀkƒƒÑžÿ‹yãýZµ¿ìK˜ÀJVJ†&Ï2öÑzó¶óóBAƒwË™ôy9]ëÖŒ{9´¦a‰W562Ž­cÎ×_0eÍIl„^¶†5ù¿Ìü’I37s¨u…¿ ëàl&Ì9MÃ~Ï10³k§0ñÛ—È ›ÃØ.Uí.Úm\Üð1O¾¿™œÿöbU"R/„+²UC-¶¬äǶKë˜0z2Wmýµ ÂS%ùÔÙ¼1iË-•tÌÒ(vëå?¦Øcáêœ20;žs‹ÆÂ9s€'ž0M6 xâ‰=g=ÏaÕX9™}âª3!®'[ê~–}7%%Åf ¨Aú=؇–Andùñ_®áŸÓ‰dX}ê´ïÏ#n&¨ø ] ûÄzþN ¢c‹ ‚©šsšu3¿eîê}\0kñ‹ŒF—ZôClIÜʼÉ3Y±ýYŠÕjz“˜ÿ¤õ+ß{•éÉxcL?¢´‰lœòs¶ã\J Å·v{îl¡g×êõì<‘‚ÅDÜC~W]¼4%üÆPšßÞ…ÈóYýç~ÎgiðŽ¾•þ[Cô—3’3¬›ñ߯ù‡KÐûÖåî_à®-){ñõ”el=Љ[†1zXS¼5~ĵdò´µÌlJœ{y½‹âêi hÿ#ÚçÿÞ˜°ók¸æÎää]€«fÒR¬xÕªCÚµ)Þ®AçEýù¿ÙHüe$KŽFòДû©mÛÅÕŒ~àuö´ú©/6ÇKMdíøX¦¹—_½Äc¶Û£™SK?àÓõÁôyóEvŒyŸKöO[NñãˆÁ|zñ.¾øfµëgÊ zm^Ä߬*‡7<ÎÚ?N’Ó¥*ù9ZóñõÖš½6·O^dËõz +$©Âõü—¬SÎé­ìËñ¢ý£ÃèU7¿·¢“Ý:Ö¤,øz ‹6' wB›ÝÁá½hä[xƒáäŒ'é›7 u½ç¿ã­–å^v᜜20ËV³É4e**™d’H"‡8@"‰d’‰ŠŠª¨dš²ÈÊÉvp©E¥e>Å’÷Æðý¥8zïK-Ã96Í›Á'¯§aüàa⼬—ö±ã„†Ž ™¿Jò߈ÿa"ãü«3æÎÐbD+Iÿ!ÙIªyQ›šÆŽoÇðÅ:wnéÿ8UÓqqßoÌ;j÷!Î>Äœ·Ç±$» ½†"Æ=ƒë0ÏnϪՊÕf˽·fKçø®HªÖ—gzÔÄv€åßÎcúPn½÷>ž‰0pië\&Ïÿ”ïëchmCÁßÑiØ ´ð·röïùö‡©ìnp'‡÷Äßr”Sæðå¤HêŽêD@‘cZ¹°æs&ýf¥Û##iª%5á^þ:ÈÚÇÌ ó8Pg Ï?\/s"ç²#ÈMjðŠ®K•œìN0YB$+O5“|h5óWœÁ÷–áDç_¹ZÓHH³¡Ó$s.1°*¥7×ÉÜÍwŸ­C×õczG¹å-´aµX±æ”«  ëØ…tÓ(dïϤËv¢'âÞ¯X4@ƒ’±™WJȰڬ¬Ö¼Ïôv²e&r!‚cªÚ}¶?úÒz“-m|õÉÕ½4•šÔ á2®>ºÑûU'€ßÙöËVÎÕ¸™ ·b+çóß|‹ùš”mm IDATÎ 5Œjæ},úbïŽà³Qí©š·Z@÷y¾c :L¦ËŽSeÎÉ)3w­;&­ rÈ!L˜H!·-zI$@9X°`Òq×:Ómtu†}Yl~2QQeXÄ¢£>tõ÷äõª[]Ãñ§§0wã=4è·¦/µÇÑÐ[Øhö>ÁÄM{IîJ•"¬$ŸJŸæÓÙ©ÉÛX°.…jýG1ìŽàÜ ˜º>^½½y[eì[ÂÊsU¸ýÍÇéU#÷â¥^ÐÖý±2wm]F}K—üÃä)âK£ØhܨEÕsض ”Vo¦¡¨©gçïï³w÷y,µÃ þŽZPß[¡~”†í¿șضÜÚ,-õ JÜÀßsþædv'ÜíiæØÅ4TcuÖ¡†·Ñy»LKãbxG7 6&=QÔ´{Et>¡ø’Ä©$ ™³±œ˜Éà_p ¤ï?Ùÿü:mË"Ë×ÔÏ3p.àC—ÁÏóìÝuñ(Rïm\Xó K.ÖåÑÿ5.h²ªñïÀ;Ë:9ž¢ù—ö¬Š¦ô¹duaôúl½JüCøuÜÇl©Ò› w„å s8üýÛÌRûóù ZÙwåc‹R/„+ÉÏ:©ªš×_Ÿ‚K{¬ îÆ3ÃŽòÁ×óè†0wèÂm·µ§qhîÝ™{æ³èX8>}Ž!Z &?øCǯbWr[Ús÷cð £zDHÁÍ µ„˜¢P0ÝRþcûå¢bpÊÀ,@€Ö¦Ã‚…L29Ç9¬Xñ €TR¹À2ÉÄ¢Z¨n‹¤ŠÎßÁ¥•“…‹Ql2r­oâ‚`ñ 3;ЉÀP/Øu‰ +Å3æÌÐÑç]Eäœÿ‡³ª-êø—rWÙÂ¥#'Év«NÃP·×øw:¼ƒ¼ '™ÔŒ è¼ ô„ý)Ù”8®£>÷ùƒ‰éX-Z¼‚¼P,¤›íf¿Ì]™°öwÑ|Í×|üÌ+4ïÔ•ÛºÜBí*zúÜs{uÞžó2ÏìlK×n]騤îy¯‹¢7¡ÃJVŽ ÍèŒtÁÝyûÛ†œ9¾åS¾æÅG4ŒÿöšziÀ­&~±€±‘u~?¿ÏÏ{ãŸ&³ê,Æ´õ/ì«c9Í/ßÿ…¶åÛtuÀ©É|†ï>ÆÛ[cynÒpâò"빌Ÿ•Æíõ£†Æe&õB¸µðŸ²u1 „w|Š ­°w㯬üyïü4“Z÷¼ÌËýk“rð™œbÚý™VäX!$¤Z)hÃ{5ã¶•¸žDf…Sf¡n¡¨¨—]}†Ü»PfÌœç<@AÀ–N:±Ù±øiüÕ]ÞÑWg¦Ñi ¿YaÑgЛÜÀ’…¹ o¯’Û½÷ß&0W)9€*k™Ü´(X°ìD‹›T›ZÊ×¾½TkáóŠV†Jš£]Ø–§Ç×gﺟY´p2o.YL§^çÁF^Ô¹÷M¾¸å/~Yºˆã^æÇú÷1æù®„êA5gaA‹‡^&_wJzÂc|‰¥IûÎw›Ó¤£¯]h®ÁP—μ̩ ÷ñÝâ}d¶mE~O Ë™5,9d¢Å#Mð¾Ñ¼˜Ï²bÌ0ÆüË “FsGµÂærÿ\Èßégù{x—"Í‚™4€nÛ>`á‡7su½A*©•äsm#©ØŸùJ{ €¡*uÛõ¥nÛ;é>/ÍžÈ-?¢*(µ<æbív)z|‚4¨–üã©ÅŽV\ávgÛ‚K\Va8e`¦ #IMæÌÿñºþ R4)d“.¯¸,d“—Í‹ÿeýw;aú˜ÙÈ8¹‡Ý—‰kXبJMãïÏ_dÂñv¼1¦Õ¥µU%¡Ã¿fuÜÍ{Ùz<‹FyM­IûØvB»£çjo¦jñ õ†MgH¶@¸Üj¦]ÅÎmgȉ©Îå91þ5#q7ïbóá Õw/½ÉŽƒ)nU©×a õZ·eÁÈ—˜»d/}¶ÀSÑâÑœžÃ›Ñ¾ù'<3î'VŸêÀ€H=–äÓ$ãK-_§üÊö°aÉë«sy=T±©y«°qaëoÕ7`H=\w³øgú‹¼óG-žûf4w„Ûº4ø·{‹±Y…×=9‡™>b4;º|Àûýâø/½A*%©ÂÉ©N^h¦ÚÿN±Ç¥EBF"7 ÊìeM´áU £º‡ƒY¾t)¡[t¸ë!;5 ›ª–ÞÏÒ~àÅür(…åºRH'\‹S^åè5ôÑœUø8ýc&›&³A·¡ âiÐÐÊÒŠ!™ƒqÃ@ˆ.í ûS²Ø=õ=ÆY†3)6° ©hñ%ÌR¥òÎ9UÑ™/rh׬vA·Þ/š:µzrgä&fO˜ˆï ÎÔ2&°iîtzµæ…VUÿü$Z|kÆà¹—ý‰ê‡éP¼Ó·k£¼ÏǶ¾t®ˆ>ã§3 ·2ÖêIÏèMÌúøC ýo§q¨ ó‰c¤å¯P|TÆQO‹³7~ûesZ0‘A¨I{9”¦Ht–S¬]ùQÕðÓgqjÏY²pÇפl¤ÝË·êÔ”;NÅ–Ä–ø¹ ¬KTU–㬙ù‡ -x%Î *黾'~§/ukb4'°gå4¦žôàÖáuí.^38²ñT{˜šÅæC¸lô½k­·ÅFߋɨÀ¤øÃ„÷y„˜ô#ìߟ»š¢÷¡Zd0&Ï`ª{ÚmŸ…^ÁàND ©ä9°*;©R/\Q±ÄSñÖ…%=Î>8/~6S+¶!¾l)'øsñr•hz„0ùÞÍíaðÃØð¸÷vâBM˜/ž"Ñ·%ù¡u ¤v¸–¿ÎfIÌmDªHòkNÛÚ^E‚¸Ëâ2ŠäË$cV8e`à©x®èHQRyÃüØà”îa–0°B–’‰·Æ #N0é¬b¢Vß‘¼íèrˆòcý‡Ùã>(²Hßä&hÈ/½Šö»é,þüæ©c»ðÄÓ}ˆûW †ˆ[hìõþL GX(:ÅHLÿQ¼â5ƒY?Oeì¢ÜA0¼‚jsSxÞE€>Œ;^!>ž%3'ðs u' :Žh¯¼ËGô.8¦´ãÛX:;ç³Ѱ¸»yr` ™;9¸~6¿Æ§bôþ1´:˜ŽZ°]dçš#¸Õ‚šÎ4Æ[&Ïlã‡øéœNWAëCõ&=xqäÃt ÔV2“γ{ÑtâO¦bCƒOõfôù6CÛø^¼šùçd†ˆ(|K¸“¡ªêõ½ð°ûäœþ›ƒ9p1þEŽ·[ÇÔžñ Þ¤©Ô¹«'õB¸ Õ>3¦ªö£Ø­d¿LŪxãqi ¾Z@’ÀD@­›øÚ ºj€HúŒ~ Ówñ,›ö1ËÌ ¸Ó¤O:6ôE‹-‡ aǸÌûèüito ·Ôò¼ò0WùeÌ Ï$cVq(ógÇ«»u/ÓÊ~_M×î=ùyéBºvïÉúµ¿Ò0®|çSQ±h-èunô¹X¶9 ³%U‡RÆÆ ;¶m½¼¬Ö$v,šJüò­œHSÑz×à¶g_f@L6LÃ7›Î‘èüjrk¿aÜß&72øsÌÃŒÛ[¸Óͯ0ññô$o¾“ó'•¶^bÛ‚)Ä/ÿ‹SàÖ”nà®F~h¹ú¹­*Šß‹<bfüŒ\"gdæÄ¼—yñ—:¼:v0õ*ñ$Óæãóxñåߨ÷ÚX†Ôq‚›0%¸R½ÍÌHgÝÚÕtêz;Þ>¾7¸dB\½æÌäöwcÎ)yÊd©ÓÂÕü[X¿v5o»«ÕŠj³]qdÆÜ‘oÜy¹¤ci4­VËÊŸ–pË­ínXyDévlÛJ½ú Ù¼i=-ZÞÂæMëiÕ¦]™¶]¹|©ófÌò)(è­z°ªdg¶ÛÒ_ëÙjçŒá½%Zôν1^X.¥à †·öã‰N~xiR9°b*3¾ü‚êuFÑ9€½j}yuxž h=ƒ.›5‹³Þ⃟ Õ€'¹/ޝ™ÅÌÞ&û1 ¨i¼Ê¹­Då¢'¼ëZ­Ç·K;òvŸ¨ËëXe`=ÇoS—r©þÿ¸+Æ9ƒ2!„®O-áQé od†Ê¾!£ý²ÂrH¾¬â¨´×þjêvæ-?Khï÷xì®ðbaž– F-Êû-: ‰õ›¦³ítNa`f¬BxD„Ý(QE‡wPS·1oE!½ßç‘îaè†uBÈ>ò2‹æïàŽZä½øeÛJT6ŠW÷?v7K5¹#VƤ™ ¼ëtahû6T•…B”µ°)cA8¦^¡%ãRJ\¦R8þ‡LdVqTÚÀ,'a/Ç->4³Y1ŸõÛNgîšÝœ¸îÌX¨o-{ÅÏIØÇI«Íî_DƒºÞÌß¶óæ„ßèŠs[UrtœÌ,ùÆÑep°…KùÌÑeøEA£Ñ`³]ËDBÜe©§R§…+)k=-Ì=•ôS|Mg„òïØJ³Š¤Òf¨¥Ud+çý„ç'rÓ G¸¿nUôI0ñÃE7¤X¥ÏmU9Hÿ2QÑ(Š‚»»‰‰ðõówtq„¸¢„sgñòö¾âu§ÔiáJÊR§‚áñU»Á?TUÍKFÙ5Ì[~£M˜åþ¦ä—CÉOïUÖ«ÆŠ§’ædÀ- †M2»w&`.òŒ™sûN` éH¯ÎM¨ATÍêx<¯ 7ê!;ì+Ü„q ¬M¸6™=»ÎîßœÀν)èÂkP÷BÒjuDEÕ`ï®íœ9}J² Â)Ùl6Ξ>Å–?6P»N}l6k©ëJ®àjê48Gì¿r岋¢*mÆLñiB¯öþ¼=ï>±õ¦]-´É iJTL0lZÍ«Âéã>ãi€n× øeÌù%€[³IH §uëÀ¢û÷jLïμ>÷c¾rëÏ­ÕTŽ­™Åü³AÜ1¼^ d–P.!DÅ¢Õé ÁÍd`׎¿Ù¸n£‹$Äe4 ^^^4lÔ„€  ,fs©ëJ®àjêt.5¯‘¢š—0ˢA‘Œ”zƒ3fE(…eQUP ZIhVQTÚÀ Åú÷æ9¯ïø~Å׌ýAC7=Xæcøé¯™ýý8Ö›t¸ûGë©´µÊ ý™ûÝǬÇà–ÑèæÀbû7RkÀ«ÜÚÐë…¹«¦±RŽã De¤( ( A…VÎA\„kPÁf³þë¬Ôiá2ÊX§ó×uY®\vQDå ÌtUhÒçYšô¹ü©6¢ÍC%o¦˜¢¸ý™¸½ØòªÃ¾d¦ý­?{?KãÞ%ïǽÉ+ÄÛO\‰5îÿ”™÷—ù/B¸«ÕŠÕzå¦4B¸©Ó¢"±Ÿ`:7k¦–:—™£2fv ³¼2ÊÓMåÌ„B!„(䣤¹Ë®ô»£Ø•ÕŠ#® Ì„B!D¥V¤·–Z00cî²bs™9rDÕîA^ÂL2fH¥ ÌvlÛêè"!„BgSdê2ûlT ³L;ÃðôWšjM¸¬J˜5Œkêè"!„B'dŸu*ï8kCF¥à±3”H\•&0B!„¢DEÚR˜»,cÆ Ì˜ÙO/]tªé"!¢Äe†fBQÎTUÅb1çŽ`çÈ9p„¸EQÐéôèôú]Wê´pWU§ æ1s¹¡?$cVH`&„åHUUÌ9Ù$ž¿À±cGHOOÃf³9ºXB\ÆÓÓ“¨èš‡ ÑhJ]Oê´pe­Ó€]»Åb}ËÔüQ‹õ3»®˜0³Qâ² C3!„(GV«…ÄóçÙ·/± á_¥ê¿_ qƒÙl6.&^`÷Îí¸ •:G™Ôiá ®¦Nvù2ûì™Zð»ÿ;>CUØ» » ×'™B”#‹ÙÌÑ£‡‰mGtÍGGˆRùúùcrwg÷Ží‡–z+uZ¸Š²Öi hö©¤‹·gt4g+¸.$0NeÀÀAŽ.‚ÿÉÌø%.WU•´´4ü«T¹Á%âê‡òdžu…7äK uZ¸’²Ôé\…Ù²üKʘٯSÞ.oÉh?£}¦L"³ŠB3átJ»ÀÂY•冂4õ®@£Ñ”¹¿˜Ôiá ÊZ§ º–QØ}ëJ“L;”b×íÍI¦UׇfB!„¢’»š±!r¶òˆëAnw Qž¬Ø8m<_¬:ƒÅÑe)oj‡–Ïbù4®v|6kÒ.ÏZÍ©œr)™BqE¡—Ý ŒÅ§2sø])¾LT˜ Qž¬©ü³y {Îe—ÿ§šÅ¹}±ídFÑcåä»gäÉoö’YއÏ:4ŸO¦¯ãXVšò£¨©øe >]ñX!„Î'¿-£}DVüwgû)˜`ZB³ŠBš2 q ²÷Åo®%­øº8^˜ôqÚY˜#Ìû1G{|Dƒpw ­˜¨FhU»e×›õ<ëâW’ÒèúÅz^u`¦ñkÁ ÞËxnÞl¶µ}šfÞW»Q)Xγî›wùdÞŸœÉ6Q­ÕF¼t?Mýä£S÷G¸°Âa4.üŠ I¯”ÿ Š¢\vŒÂ¡?”bƒû‹ŠB3!®™'-zŒ®¡v'­7pŠô>ŒîÏ¡{9Â|f-KxpË¨Æøþ§k0-­ï&nîýy‘&«H:_“áéÏñr|:}“§ޱ`üdž{Å‹Ÿö"LÎf&ïpqöíí~”bÉ©ÜuopÙåòcÚ—I2f†|U qÍܨ]›ºQne[Ý|ž?çOaÖÊœÍÒàUý&z>ô·ÕTØ>î >8Ö•w?ìKu=€ +Gñd¼Ï~öM5{˜üÆVŸÌÀ†¯ê-¹çáéi*¸“vêûç¸ïûÜÇužú’Q™ýü+¬mò&ãîÆ °¥îgÙwÓYòÇQRl‚t ßƒ}hä˜9½b—íæäù4Ì€Ö'Š[z å/KfYHز‰s^i@MÛIü„él8|†¤,ð$²MwÚùeíÚ¿8|Ɍ޿.νMýÑŠG-ÚÕÕ2nÝn’;ÜŠŸ$Í„½Œ]Ìš{˜*w}΋÷6À„J¬Ç!ú¾ϼî੺G—°r“÷G¸¸¢qWÉy³¢9*…òŠÐJÊ–/mÑ ±EE!™7’šÁÞø1Œû=€ƒGÒ¬J*ÛçÍŒ÷§2îQj܃vË6ö&õ¢z€Ô4þyMtWjz((¶ZöÆÍ¾>賎±vƾû$€ºï÷!"ïU»>Ë3íª¢Eƒ)Àty̧XòÞ¾¿Gïá}©e8Ǧy3øäõ4Œ§¿” bi_KÃWkw“rkkÜ­g%šs›ûÙÒ°|Æ÷¬Üv”„T“+Õɱ–õkÙÂŃGÈÐGÓ4¢0%¥õ­C\,Þ}s~`V¤pþDøÂ®K—…¯ZÈ2ƒÞ¨/}Ðň¿¿N^$Ã>ZÐzáE)Y…{TôFt\ Ól¹ŒBq£åËûœÙ€h—¡º ûãÚí—üàLUó¤äuzSŠfÊ$cVaH`&Ä5ÓãV¨²ô1SUTL4þ }‹¬¯`ðóFQ44èXÍ¤ßØ‘‡Ïê˜cÒØOj [¿|ŸûkÓû¡hj$}ç4ÞŸy&ÿR4h5 ÚJøòWtõ`É6_á䤠sÓ‚ÍVpþPtnhQ±ÙPTsô˜ô2ô‡(Jë„7©œM±,S³9›Þ!Þr2s0y„«+éüU“)ö-/ïÿ¥^k`¤”v[S)òHU Áâ-.EÅ W?Bܪ-÷kÜ- †%“£'ÂÂ+ø ¥ª»PðiØ•8ý?¬X³žŸ·[‰íÖ? `Idÿ±l|ZÞC÷–uˆŠˆ¤f¤a³B“²Ó²®0Á³ÿšÕq7fëñ¬‚¥Ö¤}l;¡õ‚‹6S, ­7¡¾ ©g/‘sMg+iç.b6PÕ$ÍEQúàÆÔ÷Êb÷úÃäÖ\•´=køÇ@ãúþ’_u0y„ËSíäfË”üéÕܦ…¨ª]vKEµÙò–qM?ùY¸üýå"ÿØùÇSŠÏ¯–_^‰Ì* ¹‰%Ä5ËæüÁ=ìJ·û8)Z¼ª×¦ºÉ„¿\Ü¹í§‚iÚ˜{ÚùñÎâ¯éMǺè³Îs2%œ¶jâ¡€âYÛZzòæÜéày /Çzå~qëü©ªgÙ Y^ÿ6ê˜È9™\8"¿>ÚaZV­žÏO5ºPD’}›Ñ&ªhiµzrgä&fO˜ˆï ÎÔ2&°iîtzµæ…VUÑr•8ňúA¨+wqÆ|^eœòrYœÜy%¼3¡ÿy¢ÂråÞ>Qüüí[|TíqºgÁÇ¿’û½bdÄ?‡“÷G¸¼üþcŠ]–¬p¾°‚ë š}(Åú€]»üý©ªZГ¬`™’Æ)JaHg×Þ亖C8ŽfB\³tþœúY¦PÄ—ŒlHëþ]ÙüåJ¦­hAÃcˆýßhFøLgÎ/S»Ð Z/ªÝ<ˆíjâ¡0R£[‚V/ÀÚî6ê¸ç}ñ+>´ú8w|ÏÂOÞa6€Ö„oDsüôJîó=ÄŽOâù~Âß ó§a¿n.˜¡çΗ^EûÝtþóT±]xâé>Äyý—“ŒŽÀæ- š¿šõDz©õ/ÂÔ´X½×Bô½±øH._\Æ÷}Ä»Yï0á‹Wø9ÇHx«‡øèÅ» —3™÷G¸¶ÂD”Z0wYaX–7@~IM¯ãàÿ(Ja#Ƽ‘ùÀhòÓlå‘.f‡|] q µæ«ø‡¯¸ŽoãûxsÒ}… ôUiÚçšö)}}µÞŒ‹ï}Ùrm•¦ x¹)JÛ.¤-ÃßmËðbËûŸA?»ß5^µ¹ãñ1ÜñxI{1PgØ—Ì,rà`îü`w–vÜжÜ^s1³nãî7á[¥ïÄwºâ>5~íß.ï7+6þÈ_jO4/yà!ÐÒzøxZ¯àÂ9Èû#\š}t£äµ(TJ ¹òÇF¼~ŠîKUÕÜŒ\±ç%ﮥª Ú@Õ(×)0ÎBîK !®¶*mvÂóïéÌÛ~Õ§[ÒfÌ9BX~4ö–þeB!n,µh\–›¥*øQìþUÐ( ÉNŠF¹ÆŸ¼ýäíS«ÉmȨ(yÏç=Î-W^Y4…ãAJƬ⌙â:P0ÆôâÉ ûõj~Ë‹2S1yëÿès[ØÕ>"„B\£ÜQé #’›5+ùd–Ûœ°Ün#ª Ñhòª(Mn†Ná²~mªjC2f‡fÂé 8ÈÑE×d9ñÿq˹˧\×’8EQÐh4Øl¥•)„³(K=•:-\IYë©››¤¤K¸»»ç? JÞÓùªyÑ”¼A8 é¸l2èÿFÍÛ7vA¢FÉkܟ׼Q£(En~^¼x £ÁtÍÇÎA3áTfÆÏpt„¸®EÁÝ݃ÄÄ øúù;º8B\Q¹³xy{_ñ¼ÔiáJÊR§ªETgïžÜÔòÌf³]à•$iòW GMÌw­ó˜]–³+l~¿²üc(vëëôzöìÚNõèâ£| W%™B”#­VGTT öîÚŽ»»AÁ!MT„p6›„³gزy# ãšb³YK]Wê´pWS§j֪͆õ¿³aýZêÅ6Ä߯ʕ¦bM_Ë` W»_UUIJºÈ–Í`¶X¨Q³Ö>¶p.˜ !D9Òêt…„àf2°kÇßl\·ÆÑEâ2///6jB@P³¹Ôu¥N Wp5u@«ÕÒºõ­¢NmbÓJ»Òû?œ”»§lŸþE3zµU›~k””ç˜)Ú³ü =yGkÅÄĨݽý4aÅA¥IRü7꣘Â^÷¿¯=é9GPÒö…zþ–Vê±øXã…¶¡ð‚(e÷Bõm£˜§¾Tœ“½ŸMú¿Ñ}tOûÌvÜrÿsšøÍa¥¶›ôÝšÞ5û»kþŒsÛ˜¸I3o¥˜›^Ôú3h^±êó÷bù–W•+£^ÖK–Ñ–xªL¥(EEVŸÙ†\6Jü€éüœÄ_5sügJ¾¡»^ìQQÎî/4uÆ ЕZ8¤™-GI&©ïkKx{?½~½¯6Í«ñýR2ïuÝê’œTùq±Þ›2]ŸïH•‚êr°$mœþœ†~‘(—¼Îת ÓÕ¢ÔŠ7ê©×žVˆû€ÖΤ™ƒF(òã7uSH!¹5ý¤6~6K“§~¢ñ’ëêÜ mÅÿð–žµJÕyE“¯/«C_MШaÏÊ6G}ë5Vßñoë´[RÆ}6r´V„?©×¯%I.ÿUò”ܧ·kż©š2o½ŽKªY¬6.ãÈçzµß4íÌÓ›”®ÿž Y[+«Ë³¯©F@œþ;¢ ¬òó§é¾ˆ~MÜÉŠOq©Z¯±z±Y`æ{.?]qE¾uSvká ýµàXÿP´úüyV¼U/O¿Õt3$+@õ{×{¦Ûp¹h—¸V™f4¡\^™Íi¤€k5hã/:’ÑL5[W»>ÕÛo½¤‚?Ôô‡¢ÎÛï ðwp‰…¥jßâQš²;ZOo¥—$÷iN‚+–QN|óRx9—ì¸cJtKÁž–\çñæ>¢ÏFOÕ¡ö£4¢IYó-NIRrº-G’,Où©(Ç””ºUãºõÒÇÇ$¹¢õЄj`I²•štFiYéÆòð•¿¿—\²ä²$*\ÁªÙ4\š;O n¬G›„Jñu Á-e¤~ÏV~–ë÷§ òVt¯9ú¶WöÏŽ’6ÌЈ/*êÉ™ᕦ#çÙ³}j½¦ŒüBÖ¯ª[tf0 h2Pÿ÷ÝÀœu2’åjë»aݵB’oTŒzîŸz°I9¹$Ùqk5nÌÏjÜ–Z—÷PB¾¶çß_QÙqkõþ§'uÕ SÕ³]¨\’j 8®U]§éÓßÏèºfùª–qH_¾ÿµìßÒK6W %5ª1@»¾}\_~¹[OÖÑÉØ4yT¨¯kšÔUE©vÖZçÔ÷zÿãŠz|¶^î^5Ox8óóÍÝ^NwL¦GúIj¦«j¸´»ë½÷ŸGÕì®JYk†ªaËëÕ¼¬%5o¡«ÃâÔuÄ -Ýwƒ ¾Ð¹^ˆôÓ:–(•­}®®_UÞª­º­ç>¢¯ÞûRñ hÆs·ë —¤¦ tÅÉûÕçõùŽÁjœõÀª×¨Eó:òQ3]UéÖöø\+w§êÚFEûl ßÞÒÎE³õKÐÝš6àÕñ‘T¿ªúÿö­ú}ñߥȿéðT€lÿrÈIÑîÿ¢gÆT›W'ëÁ,>[§×½«©»›kЫ `Iy§™HÒO¯tÒó«³§‰»J#Ý[´]{W×CoÏP›C{ôß%34ó™>ò˜6EGíÖ¤nOè“ì!ÑÿÐGÓïTEóíÌKÕxU}ö ÖÄ~]4G’dÉCŽTó wÛK$}Ÿ]®€¦ë¶Jž’ ŸJÏŽ]§ñÏÒçzjÒÀTØmvž•:k̢Β“®¸Ý?hÉ»#4ý¹Á*3o¼º„§kˬwôCÕ'4«eˆ\§ÇI;¼Aû22;üµžw™÷ñdÙÊ7¬2õ ~= %íî¯[Wä;‡C§•áQ[­먅ƒF«Ûƒß¨Ó]÷èîŽÍák)õÐ/Ú™¢®©”¯G']Ç7oS¢wmµ¬î—ó®Ghc].Íýy¿Rs‚Yn.…6i£ª£Ÿ÷žQ· :—€Âgç ¼ZÞ­~ÓÖ}ë;è®{îQç–ÕÎí1KÙ¯ŸöIUi˜ë³ôW–µäµôwm9‘¡ÆÏݽWù S¢N$º¥L5’g{;U{7—Ž/T¯›æ]±ì!Ågè’ý‰à¯ââ^9)Ú³hžžpHm^¬ç[…å驪XFÚw$Aîì»ãuè”-WÙ ¼``IÖ–OVéô)·^è˜÷ {O¯›õû€iTϱzç®ôÌà¬è€d­.Ê1-o…DÖRHd-5¼êJéÖSÍߢnCkéÞ×Ç醔Ìù]Q…•ܬ€Zê:ò#Ý™xBÇN; ôÞ®7¤ß›ÔQY(H?°RŸíNÑi©í´ÜK^S§.¿hÒ¿ª¡¯dÇ­Ó;½_ÐaOhâ¨ûU³(SîY^*W½¥º¿Ð[kï¥Ï~ŽSç#ZþŸ£J:=Zw·gõço~PΚ©Çª•pðš#Iºá¥qêY+wè·äVN.埡ӑãHawŒÔè»"ò,W@EùÉ¥€–5gIG­\4_sÞí¯EsÚhÈô—uƒ} æÛÏî µ%ç‚çr®@5zjº–´_­%óæhÎà‡5ë꾚þæÝŠºã-O¹ä¨iÛg~ŠzLã^m“ç¿!Ë3H/ÖßnþÂ.b0s”øË$õŸ°G׿2]ýs‡2Iò®¢f5½´ü¿ÿÕ‘ô:Šò’ìØßôÝ^)ªW \0ø©ñÀÙšsæì¤ñî#Ëõâ??SaoëÉæa*XQWW˵‰W‚cZ²,É–!Ç ˆúMQ¢zXò Seÿxýª©ð;×.ψ»4æÃ¶¹žë•¬MãzktÜãšôÚ­ªå#)ã – ¢eA=4etCY¹&ó÷©©žS>T׌œÜ”°î5==­Œþ9y ZU)ù¯w¥zªâZ¬{,UºùJ~MogÞ_箆•¥µ[Ë¿oKU.äÐ^!õtsÏáºñö¥êÛõMÍ_õ”Ú´©¯*®ÅúùÇÃJ¯—{(£—ÂêF+ í­Þ•¬æ 3{ÍÜ'Õ÷‡¤È»«ÈGÒ¹Ozp”°mþPEu‰ô“OHQÏ¥0 ªÞJ ‰QÇVCuÿàZ¾·“žŽÎõ[æ¡&‘Òºuû@TæPFÑÎÕÛ”^æÕ)_œÏÂV‰²ª+PWÖ)/}ºI±eV“¢üuàoæâ3÷a}ñîÅÖ}R·VŒÕÎmYãÿ\> ŒRyŸ²jÞ½ƒÂžy_¯L*§×ùh˼wô«OK »¹râ’_…H]™ë ÏùXž*©Šu¹¹.tÌ ùúCý;¶ŠjG–•Wê1mþâýëh°Ú?_K¾%,…“«}»þС}›µvÙ-ùÍR«AoéÎÿ!OþYƒœkùð•¦*!ò±¤3?ÐŒßÊèæaMäü±]Û²ÖtTVõˆ%ç™E1H{ÏÖÏšªY)@vì­š?][½©Ó¹,…DD)älEtz—¯,UŒ W°—£¤_Ç<+ã¸B®×#Âô¹4ÄÕC®ª$ï3‡µ÷TUÝz{=•qy( Bií*­ø¥•º6ÐÍÄhΰñê?Ù¤2µª©¼W²öþ²_É Tyÿ|yVRûí5û…±ô¶[µª¤3iâÒSªóôCªï']øÆÇü5-WÄO+›¢ï~@ –ŒÓˆþoêä­T=ÈÖ©äѼ‹Ú„;Î" ðwrñ’BÊ~ý¸ÇQFÚdý£gîaºwÆ|=SËGzëÁÒÈÉoià"·«Ý¨gßé¯6¡—ê/èÖŽ™¡”“{µnþ\Í<–"ÉK!Õ›ëWžÖ#×—øéÛ—hp¯÷u¸\”ê6í¤¡ïß§£/âÓ¼‹Â‘ãµ{#]Ç7mU¼NhÙ°'µ,÷¢†¯hÙÄ6òÌ}Ÿ˜“®„“;ôÕ’yš—!)@ánV¿ Oéöó߀—§}%b•QÓ¼«‘!ã4}ñ[4Ç-y«ÚϪUÇz*#Õ¸ç ÝòÓÛš5á+µ~¯»¢ÚÕT÷t›9O¯¯H’#o…Ôé¨Á·Ý¢ª ÚûËR[±/óÝ~ájrïP ¼!D.KjÒw²F•}GSÒ ™¶ä¡ÖÏ7QëêUõÀØñò|{œæ^ïÙ¾ oz§† ï¥ë‚sÒ^ÒžO5öÓJ¶‚U½ÍSzç¹.Ê|4Ü…Î¥pvÒamùrŠ–N<-[’wX}Ý6ðŸêžÿ?i—ʶ )ÃÊê­©“4hqŠ\e£Õ¶ÏDõ»§¨³!æ¯iïÝÌÅ3âN½ñ®§&Mœ«Ö)E’ßuOíNjîRñ~_.?Ö¢sv·t(ÒÊk¿ûF7wè¬5«VêúÚ^â¦mI«ûêÖAiòé»jW¶øhà¯gͪ•ª[¯¡Ö¿F-bZi›¯>_þ'wâÎA0Ãxzp‰´§Uß™nþ è1Ãf`Á #˜€a30Œ`†ÌÀ0‚F0Ãf`Á #˜€a30Œ`†ÌÀ0‚F0Ãf`Á #˜€a30Œ`†ÌÀ0‚F0Ãf`Á #˜€a30Œ`†ÌÀ0‚F0Ãf`Á #˜€a30Œ`†ÌÀ0‚F0Ãf`Á #˜€a30Œ`†ÌÀ0‚F0Ãf`Á #˜€a30Œ`†+˜¹\.%ÄÇ_ª¶À_žmÛÅÞÆ³8+ûù(öø1IÒšU+‹}0¸Ü%$$(00°XÛ+˜U­^C¿oÞ¤5j*0(H.#!@Êì)KHˆ×®ÛT»^ƒbm[¬`¦ÚõhÏ®JJL(Öàrær¹ä¨Úõê+4´¼ÒÓÓŠ¼m±‚Yzzš‚‚‚Ô誫‹ÝHø»(N(“˜•Œ#˜€a30Œ`†ÌÀ0‚F0Ãf`Á #˜€až¦äç8ŽâbO*þt¼Üî 9ŽcºI žžž VÙP¹\V‰öAíKæ­=Á ¥Šã8Ú¿Ÿ|¼}T½fMùùù˲J2þNÇQrò<°_öïU•È+‹]7j_2£ö3”*±'OÈËÃSÕ£kÉqœœ—eYyzo²Î}|9õîä¿°Ï>·‚ÞÏ®…ŸŸ¿jD×Òöß·(.ö¤BBËë˜Ô>“‰ÚsJ•ØØ“ ¯™ósöÅpî‹ãÂ.œ/GÙáHÒ9© Ð$I•«D*öĉb‹ÚçõgÖž`€R%-5Uþþj´‚^ IDAT’Îí™Éþ9ÿK’lÛ6ÖæKÁ¶í<çt¾sÍ–üý”–žVìcRûL&jÏPF”*v®žˆü½€Ü=—S@Èß[•Yöû¹CCîåîÔ‚Úg2Q{zÌPª8¹.j­ìŸGråŸNÁÉz?÷ò‹óJ×ÁÏ&jä䵊³/Ö>‹÷R¾s+°·ª€WNh(A80_{óu7U{‚J•{Ÿ“”Ó[‘ó} 9Éhõ¢ùZòÍ/Ú›&ÉOaÑÕúîÇÔõÿ\+:g¯Î :gDél/Vv,+çû³K]{;AÛW,Ђåk´áÓÊ¥ÀJuÔô¦ûõhçjg×+%u—þœÚÌPª8¶§§"»×&gyî™ñJzŒÄšùâËZv ¼ßÖU½ëT”_zœömÙ¤Óy•åi‹Iùê ¬++×û–rõð”p(ã%­½;Vë&ЛߞVåkoW÷;j¨¼WŠŽíÞ¨ßOÛÊý\æRSwéO©=Á ¥Š»'&WïMÙï嚤¢Èœdm™ó¶–ˆÒ=£^×}Ñ~9½-ZÝš³Næ1œ³=7Nªö;[Sç¯Ð–éò(WSmº=­ÇÚT‘¯%¥íš«¡Ã—j{|†$UnÚIOô¾Kõƒ\’;N?Î}Wó¾Û¬}±©’Õ¨÷½Ô¶¼<Š]¡,–•÷~¨ìﳇӕ GëÒÖÞÑ©¦i· ª÷ØX é!ïìE-Ú¨“$© º§éðšyš:ïKm8š*àjjÑ¥‡zv¬¥@K’“¬]ŸOÓ» ¿ÓžxGò© k¦í®‡%íøLïÍX¤ïvž’Û§‚´X½¸V¼Š]ž³.Aí f(Uòßç”ÿ7¯•¿7ãBÎlÕ§«NÉ¿Å3º½†_Î~òsr}udëÔ5xÂÕîÖOÃêØêÙzwÒpùDŒ×c5}ä*ßT]žª¯2åü”qè{Í›ü/½5§ž&÷®+_;^Û×ý¬#•îÓs½ë(È>#E–•«cEžš\_³cÒŸÇì¢ÖÞ‰ÓOŸþ¨”ÐêÞ.B^ì_Ê_wGñ?MÖ ·×(´ýczáº+”¼õßšùÁKzÕ~GÃ;W–ö}¢·fü ò÷ö×ð¦¡râ+¾bfm3Ž~©QCß׉kÕÀÇ¢å¹ï+MŸú¦FÓwF”8 ]ŠÚÌPªd_Ô:ŽÕqî”äyß/Fo™¤Œ¸Ý:˜*UjXY¾² ì’2‡ôeö Ùr2ŽèÛke_?XÏti¬KªÕK{¿¨ï¾Ý«¢£å­k®ÉÚ¼Fu߸R/ý¾E'Òk«²“¹¿€¨FjÚ¨FNO‘ã”tŠùÜÑàì=PNVo“eyüOÁì’Ô>õ˜vuäqe}Uò*jÝjÕüUJ¬ý„Þèq“Ê»$5¨©Ð¸g4ôÿi{û>ªžt\‰ T“ T«šŸ,UÏjgŠöü{‘6—é QOÞªÞ’jVV¯-?è•o×ëh§p…—¨»òÒÔž`€RÅÉžñ/ÿ,xDZ‹7ŒQ™á#çâß¶Uð%ôÙ`bÛ¶ì´CÚ|X:óÇëzxMÞ5=ŽÆ+Ýv+}ßך;{™Öï8¬Sé^ò³R¥¥¹mÙÊšÁ0kFÃÿí‰_–,Ë9'Ø8Ž#—+s¹“unÅuIko;²s×´à•òÕý 6”*Ý]KA²•yJ>мºš<¿Ú¥m'ÓU»êíº£îš;¤·vÄܬ[ok¯Õƒåá$êm'¥“ËôB·eytD§ÓlUô)zó3]ºÚÌPªØNv¯#ÛÎÐVh@È~_*ê @«L„Â<¥][êL›²ò+¸YÁ!+¼e…¹vÏkÀÍó\D»üÃä´EÓ_›ªï®è¨žýŸRT`ºv.zSÓ÷e‡¿ì0h— ˜eÍŠxvÀ<Á »·Æ²,Ù¶-WfBÈ©cq\ÒÚ{+2Trü]GS+ªÀ{¼ò×ýì±s×-gê~Û–í¡Û‡LÒÕ¿~­åK–h O´ìÎW4ôîÐÌ Xùn éÛ\Á¹æt±<æ™ôÎçÏ«=Ï1@©’óŒ,Û–ãØ²ÝYaÆíμw»Ïy9væ²¢¼ä­vMü”¸fVü‘RÈzYàŽ[nÛ–ã¦Z¤ØÇä{E¸*‡Ÿ}U*ë%÷©ÝÚï¥wÞ¡˜ºUYUU+øåôÊe>¨Yy~.ú+óü >ï¬zd‡GÇÉj»S⡌—¬ö Vã¶µäqü -X{LEª{Õ —Žü¼UqÙëœÑÞ÷(#àJU¶²Úê£JnQ!c4´}ö|þoíJñUåjå¤cÛu*°RžÏ,¼B¼œÒU{zÌPª8Y=5vÎ…îÙ‡ç<_+ëû‚f¼ðóµüÕ Ûúfë}øâíêp£šT •¯¨c{vèHÅz0¦¬|ËùK?¯×šMMtk½òjqÇÕZ2iŽFNJV—ÕUÖ•¤cSTó¦PY‘éúqñb}«&ª`kÿñIŽÜ¶[îìž Ç-·Û-wêpÞa‚Y³:Ù3f}udÉqÙÊìq*þ´"—ºöåZ<ªnk†jÎäôòæjÓ¨ŠÊz¥+þðnm;]K÷v­wNݯ½«…>;So½Ÿ®»š…)yÛWš»2AÕîë¨j^n¥ùQ+6JU"Cå“qBö%I~Áò•‡ª´ï š+>Ô”Ñ3Û±©ªÚJ8rT®†7ªy…£‰ÚÌPªdö>(ë';sØZ!÷;ó\­"^»BZ¨÷ˆrúÏ¢%ZñŸµ.É-ÉSe*Eëê[2äv¼U¥ý½j¹y¶¸N×¼ÖIáÍžÔËî5gñ¿5õû3rä¥àj­ôDËU.SG<{·Rf¦)£3ïg²|‚Þ ¼|”kÖù\³À_HVÊ3½FNr9²2‡Øå«Gö î%ï1»„µ÷ Wûþ£öÙÇZöír½·*YŽ$ïrUT§yU¥ÙÔ½Ñcút}ðÑ|]™&W™(5ï6XÝÛ‡ËÓ‘RNíÑ‹?ל¸ I Šjªû{wR¤§dUh§ç‡xhþ¼åúxÒ7J•äZ[7Wm­fa…G!µ·-˜ë´»¥C±7.…_}®º(--MŽmË}‘›¯çFʼ×(óbY*ùÄó¥]f IDATxœìw|E×€ŸÝ½5½B AšéŠŠŠ "Ÿ(öþо`Ç^A}Qi‚ˆ¢( ‚‚T‘"½×éõÞ½»óýqoB*$ažü6÷ÞÝ™ÙÝٳ眙9«´mÓZ˜ÂD˜&¦o @"‘HΙY¨ŠŠª*¨ªŠªªXLax…aà@LL4JÉ=•ã¿Ê _«”[_ôµÌ^UH+Wb¹õ%Ò+M”r%”ɯ‚T¥Â#*Ê®|&¥«Q.]©°'¨[ñ>•äXYZ%u«xsåi¾óRñ©©¨°Òáý¨0 DE)¢äöâi¥ÓEå…T\>¢’}|ÛW’x|u5åÖœ°nå÷)•RIZÅ–®V%g©Ì—J¥ôu©bZ©kVþ²•øQñÕÍÈÌB!QB…††PPPˆD"‘œ)Šd @zF&šXLSàOHH0¢²G†D"‘œBB‚¯@RMÓ<ËÕ‘H$:¦a` 88¨FZ‘¯óÛkSV7!Š¢ï'„ðÚ–Š‚¢€‚Ц©(Š×ÉU´½D"9ÀÔHˆ˜ÂD&º®ã1 C MS°hV‹¥RG§>g¨0ÂD× LÃçLÕ'€TMÃjµ jšêLç´@2 Ün¡ápZPO9CÇ­£{ªÝŽ]; u”HÎA,ÕÝAitŽ«ÐÅUÉ1´ #>$¨'©¹ìÎÌaÅt¾Ù~‡ÃŽÅb)—Š÷Óãñàr¹‰‰‰!99‰ÐPÈÍÍ%33‹;v°ÿ~ì6جhš÷n<íIxÈÉÈ!ß°€¿¥ù›n%„ºM©Æš?v£;NE <ºB`\c’ꆻn[ ¬åÒé¨{¹¢M C€¦¡«²ÿßPGI•©–f$„À4MÜn›Êè+Z`G˜˜D^QšJT„?í£‚¸®Amžüm#G]n¬6[qEŸº®£i*=zô **Ó41 “ÂÂl6;µkGñci,^¼„B—‡Í†f©z ò2rÈ3*JÔp5à¦W¦oØN&z‡ù9vüªyS w>îF÷òì áÈ÷¬ÿm…6­båPæ’_èÁc @A³X°û9ð·k>­Q'§0‘ÿ{ê^ZY²™÷Üo¬ËÕ°©%2rÝu¸áë^ ÃEfž•ðÚaXòÒÉ7ÕÓs³—¸š a~ =/—Œ|PqhS†N~^!nïéQ°Xm8ý8-Ê™«£ä¬Q-ad ]÷k×x·kcœBGÏÌ©tû(›ƒ±Ýšpß‚ 1ö?ä9ñ;Uó°Ì588ýŸ›…Í£PçúçùøÊ •^z™i» ÎFô»÷*z^O˜ гÙû÷*~üò–§rê(9«TËL&xt7ϵOÆ’—ŽËUpâò²±:üyQ,wÿ¾‹ÏÄ2M·ËE·î]QU(È/ðÝdÇ—"ߢ(x<¬V+:µç—_~Å¢jÕF';®üBÜ0f¢“N!û˜·Æ…¿U£ '—ÝÄ0½w¿¢jØNœÚñÁ  ·€|— øŸ¬0ÓMŽÒ„¡£‡ÓµV‰õŠƒˆä‹¹æþ‹¹dÞÛŒ˜¼Å¿ì£^P˜•M!*V‡͆#ã×Òu·Ùw~!y:á«»Ÿ“G‰º#Ð ÉË×ÑMoÇÕn‡ÊñûÚ$/#‹<ÍAX¨Óy¿ Ý ì²‡yöÊ ‡e¼Î¤í45žA£¤wX‰­AÔmÑ‚¨©Óñ "”ÕQ çå‘]èÓªPЬVüü8, “Âü ÜÃwm5Á!NT½Ü<7në­ª+AN짯¹I*¡Êš‘àñèô‰ö'Rä£gfp$ßͱÆáÇoíùX5bì9D„ЧvóÓÜX, Ý ::šòó½máÂEx<ž ËîÕ«º®ãtúÑ#G°ˆjÈÑ2Çwpú3<òõAŠKS4,ŽÆþ3Wî§0 Î׿ÎáD÷Êí«žâ£%mÊ :>ð:]kaõd°åׯ;e%¹v3‰%êþYªVh£Á•·sëå­¨¨àNÛÆâY“ùbÉQìþL sbºÞ +ÛÑ(Âz&[æŽå•9EeFqÃsÀÞ©Ü7â7ܶš ¤rmÌBdÇ¡ŒºµVÜlüâ5Ƭ* À¦“Ó‰aéÌ}yÿ[— þQ4lJZº‡f¢gW¶Ž‹È×l$ÜðîíRZ~PÈ‘Í0óÓü‘¥ A\|Ã]\Ö*™„H4 pÿ/¼ôÜtÒš\Í“·t£E´„‹Ì=Ëùøµ)l2ìØ¤@:£ø„QÕ6Ö=—F8ñäecztR ÞZ½G[Ö¦q˜[3 xýσ oU—h‡wO^6]#œü˜Z€¦Yð11Ñ`Fqù½zu/%EaáÂßÐuE!Lâbc8tðP•ë œ`j€Ín+7óÁÁ4½8‘ ܸLö°D.½ãQ”ƒO2n—ÕíGÇÇŸàŽ¦Vïù)´VæV-Y¶©ã ì@ÿVvvMz——dàoAËÛÉϽGpÒ(®‰ôç’+Søôµõ–m~µ‚Ð]ØCiØó.FY3yðÓmˆ27‰Y HôžéxÈÍ6¯O!Oêzš7ÿÔqxLb¯{–—úEûêéÁ°†¬ç£—ÈKÏÉ$Çî£ù˜ªRúx„‡Üì‚¡4V¬J…× ¸Õ F%'€‡mÓG3z^Ž+¢0› `´hHìŽ5ìw§³ssN‡µÌظ²u¦ŽÇ¯áV79Y&Ž`"Sº2ìI7{Án¥z¶ Ù ãòXq¨Yä¶çGú¢€‘u„#n'µÂlä˜à¨ú}"©¾¦~ò³¬ `u f^>Â× šF†ðx¿Kyý»_¹6>„Y»³xðŠÎ4S²1s20Mƒ8'¦a Â4ð÷÷Çív—>^-©ìx"Ãðø> üüü|e×¼eÄ\ÿÓ®?þûð×#xä»Ê¶ÎfÞgÜÎú>?š;ýiÝ¥ê¦èÝéßÔ d³pôÆ®Í%°ãødx²o_QªžÂ4Pk%àu[¥²ò¯ lN»×ñj±àçJeÕ_Ù\Ó+-*žpu-û‹÷Îå×ççƒMbz=ÎۃܩÍ&mbE…R·gP¯pà ÓžÅÌ=‘=Ÿâý!É´êÛšàå‹È êÈWFn6|ö"¯ÎÝG¡j'ÈOÁÔêùrJåëQ#øê ªÙ ´¢ÏŸ2ÈuÇs÷èÁ4uTtÞ Y÷ÑË|²G=.JàŸì=GùŒá•o÷c ¶{óÖ¬Øÿ̧¿\ÌS="Iì{/oõ-dÿŸ¿1ç›X´G'ÐQòÚ—¯£“=?Ìõã¬âv1¿xIáMhò»²J\ÛçgÜVÍnÅ{‘ ÀN>}î~>âÍIP «"%Ñ™¦êlÕëxö:n·«xµ™y”†:×µmÆÄ%k¹õ’æ41Óñd§ßWQp˜z±Ébú„®ë¾doc-Y’ß½Ú“÷·Ý®aŠj°<á¶n ÝNÝ,‚MÉbË– H Å?<Í4 *™h€ü¿Y´¹§ŠG/Ý]WÒLóÖ¹¤ÇF”rHŸ¸&ºG`÷×8ºj{7 Q‹$)\cyj©±Ä4£.1Ü8z7–L%D5ȉmI¼¸Ö3ë׃X‚SÂ(ÛݨâpàÔð:ä‹‹11„ˆ¸pjQ‚»ð]+*½~íoçîµ/óÁ²üü¼þ,«ÓæϞåžßÛsY¯KéÚ>‘¸‹zóu¡ý§#yý·L(%KÖÑÄUà éÍqOŸdK•fÃÏVö<+ØýñW ò¯feZ/. Oäîwߥ×ò…|;ëG–56Ú§Êfš*¼ÎäœvÃ;P±ˆÍ{òõæ,nîМoVýM|Ã`ÛŠÓU%· Ð+t¯ð),t¡(>'¡z\U4~ÈÚÄ«e¸\._\õ´äæe}FšÃŽª„Ÿ8EàqETÜ]/4+‚Š&Ö”º 3m7ÇèD Q´iʬ_²±95á¡@DѺ¹w4ªqd/馊Rö,VŸßFøÎKTÅ7¶)åsW’ZBs2s¶‘£”ØF˜Å¾ÊqÁé­0ªâ-²üù¶hÝÄ 7ÞIÅž>°øûhU*¶‡fļº·q[Ë`:Üó®¼ç¿AÇßî‹&`Q)ܳœ™c–0u|—=ô4wµpÐb@OâMcw%uº ¥ÙÝ<Ø'›±_¦Íg“+Žþwö¢nżǮh8-»øü?/³g@ú÷lJ|»¾<Ø®5ß<3ŠGüäØ3Н©‹“/Š@U5¶å"01 ÓÐÙšYÀ[›2¹¿GúE[yüò¼³9“¿3ò‹·ÂdG® UÓ𠕬¬l ÃÄãñàñxo,³Tšé3Ç<ǃa˜dee£ªjÕê\j)sà'¡!þ„†ød¯I#S1mæ €½ý;F –Õ*ÊÔAÕ°f­aÎzïí›xÛ îƒÕcbø%ÐóÿîçÚ(€Vüø7…åÌ+ßJýÞÝ©P¸‡Mif¹A•zêV€3õfNù’Ï'Mgêìü´àor5 fÑ6Žfôm;;—Œ¬BtUw®o{“û‡Œbú!€h:´®ºé £!—3³T§7Í4±Y­,NË£I_Ï—ŸÓz¦ÐÈ}ýhI!8–ÞÞ¥w™œÓ~û„ÿ­wãp({“ƒèõßwé$8½W]ß~Ëß­Œö¤ÀÑ%L]z_HóÛGòÙítaÁª¸YñÊC¼½]Åvìwf¬¸‚‡ÛúÑúÿ^aÊÝ:†jEÝ:»_ØÌŠ&5°Ñúáwø"_ÇZ°˜Nã°ÍVÃÁ…ãÃüøÖ{Ôý(½Â¹å±kÙüô4vF]ÅÐὩHWÍ]»œ}††•ý•Ôq ¶ï&—Ú$ÜÆotæ@¾“ø ýZE5˜7îÚ×ñÖ ÝðÏNãX¾Fd4€Á‘ƒ¹˜* NfÎKN¯¦.N¾x Õfea¶Æ^(ŠŠ0<ÄØ æÄ“žê€—yŒ¤œ½ÄÙL„áAQUVç åjX,VPÀb±‘žžAn^¾wÌ‘î¡eË´m{1íÛ·¡}û¶´o߆¶m/æâ‹[áÖ=˜¦I^~>iÇÒ°Ú,Uªs©¥ì‘Ÿ,ý¤(XnÖ}ü<£¿^Íî,ØØAaæA6þµ—U-_ŽbÁY°’w{‘q?þÅî ·/?i;ÿdöG£xôãõ¸42Ù°z7©9:h^ATº™y^dÔ÷G°Ù”ruWm.ÖŽÅ+_­bg†X°*¹©»8lZЄ‚Å^Èê1£xóÛµìÍ6@µ¢‰¦)8Ô,ø?lJGGÁág¥ ÛÄj³ºçý„çXÃilæó·~ðj˜‘½xð††ø»w³xÅNŽä7|Ý{YõýG<3a¦Ý‚Í–Ã’ ë¨bn«“V°+Ë &†ÉÑØ]Ùܼ‘½…˜j„P±ªùì?R€%(œÚÑ!½Ÿ•3ßフù8,JÍ[.']”NÛ ›ÍZ¥ÛOÓ,˜†IˆžÃ¨¨<‚„^ÜÅ_ªÅJ¶båÉÃÚý±Z½å˜¦@w»1… 11Eaš¥:z·3U¡°sçN@AÕ”ãGPeL s]¸LPíËšf /Ç›¿§Zæ·†«\—ÍFÑàAîÒq¹ Œ¢“ªxÙ9ZåóÒ„‰ÛåÁ­ßOÕ4¬6 v«Zâ†1qè¸=&Eî!EU±Ú¬8làÊsã ïÉkïÝL=Ž1ëÉÿðÕ1 Màqé–¬—ªbsؼÿ„@w—®»jµáïÔPMƒ‚Ý7øÍ‚ŸŸ•S›öVñ5†NnžPmvümw¡Ý8~Ì(*«§]£x6L¥un×ñãBQPU ‡ÓŠU)m‹ê‘_àÁc/Ój³à°Ÿà:JN n·îFEB¢*Xm6tÝ…Ý]ÈÝAÙ´¶»Àôõv á ÿ¡€ª±Úeçãì \6'v»wlM‘idš·Ë…a˜ÄÄDãt:}i%…Œ7/Ã0Ø¿ÿ¦ibµZ1 ãÜž¹ÿ`ârGsí¨Géˆ g #œÈNÕvŠBC"ùgÑu½ú!DÜ.v»]Ñx3S£­5—K$YtU“Se‡ÇÊ¢'+ôüœìVk±ŒQðP›ÝŽ®»Ù·o?ÁÁAøø{…–P@¸ÝnòróÈÊÊÆf·a³ZÑ £ZÑyˆ–@¢B½‚¨ààfŸÎVÓ‚]­<γDr®¢têØNXj0 Þbµ ª<ºŽ®ë˜¦i _´ «ÕŠÕj­Tƒ9>{„0pë<>ß&Šâ{c€EÃj³b&†éDR+ò!L ò‹æž©Øìl²ûYò/Äã1ª7¤$ºÛƒ¢hš§Óé+Tõ¡hö¾ª*˜¦†Ý¦a·Z‹4¯Åfb˜ »Kú¥ª?ÆèüEÅág/µFžÉ¿•Ez,Â;È^¹["‘HªD•ç¦I$É™¤ÆfšD"‘œN,……''/‘H$ÿÊÌ/'‹^—÷=ÛõH$0óæÎ‘K%ɹF‰äœ@ #‰DrN …‘D"9'ÂH"‘œHa$‘HÎ ¤0’H$çRI$’s)Œ$É9F‰äœ /¬—ü›Bàñè¾È˜5 #‘TEQ°X¬Xªƺ$R‡!ÐÝ.ÒŽcÏž]äååúÞA'‘œ9HHL&2º¶ï݆ÕC £óÃðvô(›·l¢i³„…תQãHªŠiš¤§cãú¿°ÙíDDFyµòj …ÑyˆG×Ù½{'M›µ$1¹þÙ®Žä!$4 §Ÿ×ýETtLµ…‘|\ž‡!ÈÍÍ%,¼¢w²J$gލèrssª¿)ŒÎc¤i&ù§QUµÆþIÙZ%É9ÁF…;góþ“ø»àL—zêRfLšÃöÂ3_ÖùJÁ¦/ùäûü™w¶k"¹Ð8ílýÀlFŽ˜K­{^á¡vAè©kYö§B[Ÿ/Ë“µ›Û ¨{Q#B«ÿîÈ“”ý+ßü¨Óÿ ’òe†ÕGà>¸Œ_–ªt­žïQrs8õ0Ï=û,›·lªt›6mÛñæëožR9ÕFf: ž{€ »Ê'% }Ÿÿ6¥v\mBƒ*ÊÚ ãñŒžÁScO¿0’T óè\¾á%Ö$?Ä—c®%Fö©J*aë¶­Üwß½DEFÓçò¾äääàñxðx<8N2331M“_æÏ?å²jÞ cðèñ+^¡âˆ%¸3Ã_èì['Gýž{èìžýkÌ@ÔM“ørs_nê8Û•’œƒ,_¾œa÷#1!‘ž={2pà@ÆŒÃ_ýE£Fx衇˜;w.S§N%;;ç”Ë«¹0 ˆ£aJ#ÊXCfÚ|F<0…ˆ§Æðp3{ÅûºWóê]·z¿‡÷åå7o"ÞââÀ’©|2c›Ót´dº\?„ÛºÄaW@ßÿ#M˜Ïú©ä E÷â¹Wn§¾­læ˜ûöc|²5•B%zí®äö;û ñÛó §sÇ»ÏÓ«–×]æÚ<Žû_ØB¯WG3°Îñaì"w=“ßý‚¥;‘Y(Q-zsûÝ×ÒÒ§Ò¹wNçÅ×`{¶ð#¦Õ ÚŸÆ*i,ûßL_½‡Ôl7 Ò°ýÚZÙðëï¬ß—ÇEË+‡0ìêFª‚üóølâ·,Ý™…a‹ IÏ›z}"¬‚‚Myê•åÔ½ç¾$´F?‘·–ɳRiúÀ›t›û¶Œ;Fw#´’Ì\['òì«sX¿ó0¹(õhÛ÷N¸«;u¥)|ÞòÝw³¹ï¾ûhÖ¬96›6mÚ`³Ù6lóæÍ£wïÞ8ºtéÂçŸNNvö)—Ysa$L Ӡص ¨hj§ÖˆÁ#ÑÀŠ5˜h‹IÖªñŒüh3 ¯¿—çšøstÙT>ÿ:öØ×¸-ÙŽ~äOþØfÐmÈ£tˆ¶âvP»ÂÚëˆØ®ÜÑ·ά Ì™2•—Þ´òƈÞÔjØŽ:LeåŽ\zÖ BAçèÆ­ä¤Ð,²ô|áJåï¿ÜCà9¶&Íæ­±µyç©Î„© ÕºˆþCì@?¼’é¾âÝ©)¼;4‡™ÇÞ ÛȬs=÷OÆž»•¹Ÿ~Å[cèrÓ ®k'cõ >™ù>Óš¼ÍÝ íGðÖ‹_p¬Í­<2( mßB&~ú.o¾Æ‹ýk#„a˜Ô|f‡Iúï“ù®¼Ô» #;ñÉ3“ùåp®‹©Øföù“?¶)\õä+tŠ€Œ-óùlÂH†€I/ö –ì=ïøä“OxôÑGiÕªŠ¢žžÎÈ‘#yíµ×ˆ‹‹£_¿~(ŠBnn.C‡%++ë,kF[?dØmÿz/¾5ˆøªì«ùY·.u‹¬#•%3—#Ú?Á½ý›ã§@J½»Ø·rK–ìçÆä¤¢BhÔº%M‚N$ôâésý•t R€¤åÁ·`ÁþnÜÛœ1“øfÉvò۶Ÿ\v¬;‚­þ-ĕӰ¬D6nEËf  u Öñð´¥ì*èL˜?hAõ¹¸µoÓä:Xÿ^Ĩ­[H÷¤ã[í×”M±Ñ€Z©KYûM —ôè@sleýâÑlÚxOÃHöþô-\Î C.#É4ˆcðæ•¼¼d5Gûö£vã!|0yHUÎpÅxòó”µöK‹ûÅ7Ò;ä^¦·“þÿWŸ Oá4ïÔ‘v! ´»„ÖÜðòfïéÂà„šMŠ”œ»<÷Üs$&&bš&†Aff&³gÏføðá€w`íĉY·nèº~ÊåÖ\ÕÈSCšûŒ[±5m—îT6†ü}¯1äÒIÚ‘jÞ±£àŸÐ‚Ú¬fÛ¡BD½(Ú^Ç—³³-ÿ"Z*{X½_#ipþ'Uê4‚b#°™dšà¯P¸wS&ýÀª‡ÉÔ­8„ºðTè*³î,rÜ X‚ˆ €-Ù.LQÀíéþ#ÏÞùcé]ƒŽc@íStø»w}Ï×»bé?2;€#…ýãøæÛYl¾í š;«’‹Jx«n$ð&kvç38!øÔ*%9ç9r$ÇÇf³¡( š¦Ñ¦Mú÷ïOVVn·›ÍÆ€X°`ßÿ=Šrê&{Í…‘_4‰IIå}F5ÊL €°óøeÑ¥*¥:káj>L©H2(€FdÛn$L›ÎüÍy¤¬`‹^R«4z]Q5T„÷ ·ðÅ+XÙ—!´£n€ÎޝßdžÊ÷Wm Œâ“¤aÓ@˜á;Ä^Ã3÷·#¤¤ùc â”6ÏúÃfŸ êÎ'¥Ò2˜±vÍ:T-+Eõž/Ó{v¥çèüâî»ï&44”Ûn» ǃ‚Q£FáïïϦ͛xïÝw¹ï¾á¤¤¤0êùQ¬[·Žýû÷Ÿr¹ÿX§®) bõ³‚žO§D3¶E’ î8†#º5Q§­Ëß$óïì'Š~qv¯8Šhǧ1~îrÖ×^OAüšWæ½=FÖ.¶g[ivÏÕtnâtÌH'œ@ÅØÄPX¸¬À4 9½·¸È]ÇÌÔôÏ\vÜùm¦±à…'™ñÕ 2ÚuÇ{ꆨLÌr¶,c/Ñ ¨ë”‚è<åºë®#""‚ iÏý÷Y¸ÿ&|2œœl>ó>wÞy3¿úаðP8pÊežaa¤` ÆÎß,]¶”nõ ˆk@˜˜Ë×_-ÂÞ:ˆÜ#õ»´¦Ó€Ö|ûÁ¼:¦k:%ªæ‘ºßEÃ^]*ñçTF&[×®#8²w-eÖ—±´¾î1>ÕB æ¢~m±¿:‰·˜ÄßÑ‚°8aµ ºÔó×Yùíl«­ˆ 0Ù—æª~FÅØˆ¿üJüòã^ÿ„Ì+ÛP7À$ûp*Z‹ž´‹ÔN¡7Mµz&¿ç%3´o[Å–”öü®L`ʸ™,MëF÷ 0¬aþü ´è_ IDAT߯嶕éã§b\’H@æj¦½¿½éCôK¨Ö…‘ü˸ôÒKùõ×_¹âŠ+Ø·wKPP‘QجVf}3“ܼbccظáïS.ïŒkFކ×pCë½L›:õmŸ¥K½þ »zãæLàµyàW§;÷·»˜íïåysŸ}=‡þÈG`%$±+÷téLœ­jÏ_Kh Íã±xüëÌ€_mš_~ l_Bà(ø5êÇå1¿óÕ‘&\Ö*¬fsbœôð@ ÿ÷c_ýΛ³=ˆ˜æ‘8Tjd¯jQ½yb¤ÆäIß3ãƒ_qŽðF\žÔv‘5·e¦³bæJ\ÉÃè]Ví´P»KÇŽáëE‡éså`†vÚθ±cYÙõº`…]ßñÖwû)P‚Iê6Œw@œ,yÞÓ¼ys~ÿýwÈ‚_~­t»Çü”ËRf~9Yôº¼ï)gô¯Bä°úGoÎ[÷6¯‚óúßEA~K~û•ž—]APpÈ)å•·äAú<íæÙïÆÐë4›Ž’󓯧OáŠþÐÝî*ï3oîœ )¸š ÿÐN¹™ëf1~},W¿Üø¼DÉ¿• H¹Øýí[¼¸8 kDSú>ô—E_@‡/‘œã\@w£ƒÆ÷|È”{Îv=Î<Š¢œR«’øwz—ߟ†JI.N¥ÍÉÁüç!Š¢àççOZÚ±³]ÉÆ‘ÔÃÕ¨¯E £óM³Ä¦ qèàùš"ÉÇ4M<ÀªåKi˜ÒÓ¬þ¼‰ ÈL»pÐ,¢j×Ææ´³aÝŸ,[²èlWIrž£ª*4oÑŠˆ¨(<5˜«&…Ñyˆ¢( (DDD#çkHþ˜¦Q#AR׆QíwWI$g é3’H$çRI$’s)Œ$É9F‰äœ@ #‰DrN …‘D"9']û’R!ÈHO#;+Ãð†•HN†Åb!((˜°pÔª¾%¨l§¹N’1BöíÛƒÝf'©AœN¿Óh]r~#„   Ÿû÷±ßnêÔ¯Q»‘ÂHRLzÚ1¬š…¤ú B/Š¢”ÒŠ~—lpRƒ:(+HŠ®mEë‹Ú‚ÓéGrý†lÝü7éi„…תv¹Òg$)&==˜:u‹5¾’±²†*9ÿ(zåH=¤bëÔ%ýXÍ¢EHa$)Æíráçç”×~Š~—]àÔbØHÎ=LÓ,uMOt­Ë>œüüüqëU7[i¦IŠ1K<íÊ>ù*8%ŸŠR ?”Ո˦­/)¤J¦5l g\3*Ü9›÷ߘÄß5 c•ÑS—2cÒ¶žù²ÎGD‰F¤ý„(7ñ_øÖ—L—Ëù±PæÚV¨W° ©sEéfóÌ÷òöò,=u-ËþÜEŽoò¸'k7­ÚDƘL®ø•o~\EªûÔýÂÆö?×°#ûÂyâ—RÍáûn–xš¦ém”¾4¹œßKÑu/© U´]‘FUS-¹úfš™Î‚ç`®òIICßç¿C©W›Ð Š²6Èøc<£§EðÔØF„ž¶7Çž~<‡æñÞKhõü[$]/+4KhBÂ4ûŒãØFÞóëã‡0öå>È÷\”ôºÝnlvûqM¹ÈLS”âïÿœ0*"vÞÙ¿â*~1X‚;3ü…ÎE5­qö’aš¥ž†Eš‘½?Í`½@Ýþ5³·wçîÇÙ¨¦äŸÆ×¶oßÎØ±c6lÉÉÉ^ÿQÑ&”ТþqaGÔF”q&˜ióñÀ"žÃÃÍìïë^Í«wÝêýÞ——ß¼‰x‹‹K¦òÉŒElNÓÑB’érýnë‡]}ÿ|4a>ëw¦’k€Ý‹ç^¹úå”–Ì}û1>ÙšJ¡H½vWrû}H PÈøíy†Ó¹ãÝçéUËk¡º6ãþ¶ÐëÕÑ ¬c-“W&??7˜Ÿ¨Ëm¯ÝÁ¡—ŸgY³gxïžF8ÌcÌö!>zˆ1&°áó±ÌXµ“CYnÀJXR[úÜ| }R‚Šmb#k#ßMœÄ+ö‘‹µ/ºœÛ‡\Eóõ^a}ê™b@) @äodÖÜc¤Üù,—ü:’É3V3ð?ñ¶uíø’×>\À–ýÇÈ7k-÷ÄðÛ;mÍcÍè»yq÷•¼ýÞÍÔ³èì™òÿÜ„Ç §q%MErn°}Ç>ýôS"""øôÓO¹kð`’““·!¼ÚeÚQ5¨¹0&†iPìúQT´ª×1xÄ 8@±m1ÉZ5ž‘m¦áõ÷ò\Ž.›ÊÇã_Çû·%ÛÑüÉÛ º y”ÑVÜ®jWX{Û•;úÖÁ™µ9S¦òÒ›VÞÑ›Z ÛQ‡©¬Ü‘KÏZA(èݸ•Ü€šE–D´ö8W×µ‚j',:˜Ýý˜·y©z#ïMU°—5 N÷üD{Öm&=n  MÀáJeÝÏ3˜üÂnò^|ëlàÞË7¯Œæ[µ;w>u±ž­Ì™0…7ÇÔâ­§:ã†ar6:§ÊúŒŽ7)“Œ•³XN{žèÒˆÄð6L}}K޶£o”×Öö¤obí•Þ÷=M»pÈØ<ϧ¿Íèð^CbÇúh+Ö°!ãêFj`f³å¯#X“ï ŽMêÐç2;vì`ìØ±=z€1> ))) ðùŒ|ÛÿófÚÖvۇLJ^Æ‹o "¾*ûj~DÖ­KÝ"-ßHeÉÌåˆöOpoÿæø)Rï.ö­Á’%û¹19©¨µnI“  ½xú\%ƒ )ÁGyðíX°¿7Å6§CÌ$¾Y²ü¶­ð'—ëŽ`« qº…,Õ®CݺE‰‚„KãXº’5G®¥^¬×Á¿Ø¥GÓ³a0*yøÅ5£UËDl@Ë–‰¨ÿ—f®çŠG[£mù–öÅpý›ƒè­IDZË},äïìÎtn<„&©ÊY<í5"!LßÏç°4±è›ty‰F~ ¶æWÒ9èY¾Ÿ·‡^·ÄcŸc3””ÖÑ=/à¥ÅËIíÓ…M«R lÑ–XPÙKT|>Ÿ¡Ô玑Châ,¹‘•ਲ਼۽hŠ"ÍH`šÞ'lŸû+GE_>|3_–Ú#‹9o¦þE~Ç»þ‹…‘@ÑT†wD¯¢yï&hïÏguZ;ê¯þ‹¬¨Î´Sä€És_Oêñ®³R‚¨ä(}Ó4Q½©¸U—ì1ìm¬*V?+èùxJX™¶HR"áÏÇpD·æôÝ“&™¯`?Qô‹óvGjí¸¢ñ4ÆÏ]ÎúÚë)ˆ@óÐònbÅâÄŠ›\WÙë¤þe]¨µx!¿¬d×>ZÝZJý¯ž£ü½5-&™p«KB±‰…¡ôJö«²ü'(?Tü)0ó6ñãÒ,b¯~‚amŽ;á…‘ÁcßdîkÉjÞ«ï4 ÓD˜ ™^Eù™Øì Ú:G3wÑz2–#ê’6DªÞ4ɹKE´Rb ZÑëÔ…)ª8 fZ•P°cço–.ÛFJ·úÄ5 LÌåë¯aoDîƒú]ZÓi@k¾ýà ^SÈ5’UóHÝï¢a¯.•øs*#“­k×Ù»–2ëËXZßG÷Ÿ ©sQ¿¶Ø_ÄÇ[LâïhAX]V–°$âýòY5c6¿h„=+ [“Î4Ó°ÕíÁåñs™2n ¦_GnŒ/-Š2V~옎4¨%Ø¿d:_ ¡ûÍT@it—Ç®ä›wßÄÿúËiQÛ'ã iÁméÑ<÷YìM>mÈô "! ²×ÏeuA]nèÒ„Ä(ÍÛ0…@ˆXì—ÆòÝ—?±:íbÚú|¦i` `›n†i`€=™>]Ã1s<‡Œú·@5ŒS0Ã%g‚†ÿPo7~Qï™ïS  T¯©þO÷¦UGÃk¸¡õ^¦MÆú¶ÏÒ¥^†]½qs&ðÚ<ð«ÓûÛ]L‹ö÷ò¼9ƒÏ¾žÃGä#°’Ø•{ºt&ÎV5ýÁšBó¸C,ÿ:óàW›æ—ßÇ#Û—8 ~úqyÌï|u¤ —µ «ø†w6â–¡½9ö¿ïøpôl” dú=Öæah‘t¾¦_½µŠ€+zXF-R­Ù¬ýúC¾Í2±ÕjÄå÷ÝÅM|Z-žkGüçSùqò»ÌÕAñ‹æ¢k›Ñ­yg³_É«¶51ÍLþúi=îº7rQ¸Zfò¤JDëÎÄM›ÆÏ+Ò*¼h}éÞ^ï—¢ï6âzö£ÁÿckÝ~\e‘Žësákƒ%ïºâB ŠœØefñ]ûšjFÊÌ/'‹^—÷=…ªÿ 9¬~çQÆÛ†óֽͫè¼.çÀ7<ùÌ º½òWÖöÙ•ú^¾|ü?üÖêyÞ¾ÍÛ›öoâ—yséÛn·ašÇ'<ú¦~”l|fqã„j ЭL|ò-R¯…Ç;þ³šŸ¤ºxoŒ]»w1qâgåRßy >3MCU¬6sfÏ¢G¯Ë«UÒ¼¹s.¤Yû‚üC;9äd®›Åøõ±\ýrãê "#‹];ÓÁ³…gãêøˆ¯{þü ´fT¤Î9-K:/‹Ò”ãêú‰.Ží>@.ùlŸ;‘Å~=Ñ&¥ø,9'&( ñõ¸ãöAL™ú%äääpËÍ7_b¿‰ê)iF0r±ûÛ·xqqÖˆ¦ô}è!.«æä*ãØL|ñ ¶y¨×yOÝÞ¤FZÕ9‹Ï<ójB%&D–²¥D‰ÿ>æÉ$Š~˜…ã^àûC*ÁÉÝúÈÕÄ[©–B%ùç)º¶&‚ø„n¾ùF&Nüœ;î¸zñõ¼)^³®¨aÖ¸kÿÂ4Ó$²ð—y´ëÐ ??ß<5ŸÀ7aÖ,àæsP–ìÚ•ÏJÆ3*ZØcx°Z­ 8ùSEñÜùy¹¬üc]{ôªV™˜™&9±±qlÞ´¶í:  áÕxP4Í;°MQA€ª©å*”Õ $ÿV*‹u  išÏ•T> ­Õbeã†õÔ©W¯FåJa$)&¹~–-]²ß£Q“f„††—o˜Ò¶º P|Nìã=låýB23ÓY½~‡Ä¤ú5*K #I1ª¦qIÇÎlß¾•?~_LAÁ?žSò¯GQN'ñ ‰$%Õ¯ñë­¤0’”BQUê7H¡~ƒ”³]Ɇæ!‘HÎ ¤0’H$çRI$’s)Œ$É9F‰äœ@ #‰DrNPí®ýukWŸ‰zH$’sŒæ-[—['„àèÑ£¤ÚOa÷ÕÍ«…ÿ@"¢"  ûgÇuìÒ½F…I$’¿ÿ¶ Ü:ÇÃæëÉ/ȧ^Bµ"±Ùí¸Ýn23Ò9´ÿYYYÔ©_‚¶:H3M"‘œ!›6®Ç4MÚwèL\\]슢`·Û‰Š®M“a&÷ï­Ñyf2¿îÂu1¿=ÃsäOþئpÕ“¯Ð)2¶Ìç³ #z&½ØƒZEB,°1ºš”`7»æãƒ7ž  Ádhä@qÔ¥û]#èŠ-;?¼÷ïÙ&…‘DRUÂÃÃéÝ«zq®OÆéFžƒü#÷®ïùzW,ýû'{ß9ïHa@ÿ8Í™Åæ*;ÕUÂ[u#ý¬Ù_ñ&¶Ã!ïh`f­gúK÷rã•=¹´sw®}q%t\ú êzh#ûÍ0Z´ˆ”a.%’sˆÓp?°yÖ63ødPw>)•–ÁŒµÃhÖ! jY)ª7Ü·YÉ+µ ‹ ¦ ˜™,~åÞ_ׂ»{“õüÈ]ñ69‰ãÜ玪éûÀ%əᔅ‘È]ÇÌÔôÏ\Zâ½õf ^x’_­ £]w¼ž#á{i9‘³e{‰f@]'ÊÉFüxRY¿­°wrc÷&8€‚¬Hlì=án¶Ú-‰·|ÃÊeûp5­ïÕä$I•Ù¹s;›ÿÞHvv6†a`Ñ4‚CBhÔ¸ õâkœï) #AÖê™üž—Ìоmi[ÒYíÁïʦŒ›ÉÒ´nt ÃÁæÏß@‹þMq°•éã§b\’H@æj¦½¿½éCôK°'Ñp,4ªgåË…Ÿ3£Õ ´®íkg'°ÐPC;p÷µq ûüQþce@ël¹ëØ[‰e(‘H¼x<Ìÿ‰œœš5oAÝøDœ¹¹¹ìÚ¹ƒË—³cû.íV³‰ô§&ŒÌtVÌ\‰+y˽sÞBí.}H;†¯¦Ï•ƒÚi;ãÆŽee×è€v}Ç[ßí§@ &©Û0Þyd@±³ù„¨a\úÔóÜôÊû|ñ܃Œ°øžÔ…Z•{ÌAñ£é°y'ä=ÆÎx‹§'é€•à¸ætKð§â¾?‰D²`ÞO ª\sÝõ¨ª†axðxtlV õë×')9‰_.ä·_P«Vxµó¯öë­×­]}ZBˆä-y>O»yö»1ô 9Ÿ^X/‘üûùý·¥âýôãwê\wÃÞW›!L„a`˜&†a`Š¢ðÃ÷ßQ§N­Û´«ryóæÎ‘³ö%ÉÉ),tÑâ¢VhZ ³E Â4†‰0MLÓ¤aJCÒÓÒª]†F‰ä¤èn:uë"Ì¢Ž%QêSàydx<Ô«›@NNnµË8kCmü;½Ëo‹ÏV鉤:˜B„᛫( Â7WMQ,œþþxŒêÏ”—š‘D"©¥<»Â+‚á]Pµš‰9Y"‘T EEALð-&³H72E%#–OŽÔŒ$IÕ¯)†8î3RÅ+„ﺚ ÃÎJ$’*S§HQŽ $áý'ŠÓk6TG†•H$UFTô]˜!PŠU£s@3’ag%’óÅç3*F”èÚ?¾Uò–ag%IÕ(vÖç?òuñ§Ô Ül§M;û÷5Jç‡Ï–‘qóʵu"O HŸ®éܹ3]®¸•Ç>ü…½…ef÷îïw);w¦ëUwóò·;((ÞÄ$wÓL^z];w¦s¯<øÁ"—˜-kfo`ú C¹ª[g:w¾”>·fY–  vÎáÍáéÙ¹3;÷æ¦gä°ÇÎöõÛúpigï>W~ž™[ójÚI ‘œ”t\ q|Ø£Þ^¶šw¤ÿò°³žƒ³ùÏýï‘zép^y 1ÚŽïxû<<‰ÕêïáËdž3æ`k=>šÖ‚ôc* ~*Æ‘¹ü÷žWù3ñZ†¿Ü‘8K6GÜÉ„Z8I8ÛÓsÆ$’+‚暥ð— Ú_þÅagcÙ1ósþ ºŽñO ¤‘hšÀc-⡟sè¦zDlø‚/6…põ‡/0´¹_‰rÝlûú–Ñ™—G?@çà2 â ÃÙÊÑ’ ”‚ÆÛ&Š×)EcDÍ{›N‹0*;;²tØÙo¾ÅæÛž ¹³*¹…}“5»óœPAØ3_ØÙUGs0ÌË×˶r(ËÄáïÁCò ÃÙJ$ç=eýA%µ Q2½fÆÚ¿7ì,Â{ìõóîóÝ(©°(– ¢íp¸RgšoߊTÊš†³•H.ÊøŽŠWs–}Fg-ì¬@|£ZðÝÒCn§UæS­”Dœîõ,Ù‘O»’fš@|ãZ0ç76æô.m¦Õ0œ­D"95þ½ag±Sÿº[höí»¼üØë¤Ýr)IA&™û÷£µ@·+þÍoã†øÁL|úiœC¯åâ8G3 kß“F×ÝJ‹oßaÔãosÏMˆ÷×IO³qQÏä…³•H.4¾ûæëâïÝ{ô>åüþ½agKÜ5¼6ÆÂ‡LfÂÈï)œQ-˜ÒŸn1VpÔçŽ÷ÞÃï½™öÎ3Lõ€-¢ CRºÑ,~¯ŽÑøð½/øè¹op¡ÒèzFwk[³p¶ÉÆ 7*þ~45õ”ó“ag%I9ʆ>m wÞ5ǃÞˆŽ0<<Ý[“À `¦O›Âõ7Þ\åòæÍ#CˆH$’šqn™i‰ä‚åt›i2ì¬D"©ùùyÅfÚé FÂèØƒù¼îUܶ÷[ù)?åçyøÉµ£N*N·™V#öò•kšõÛ).‘HÎ=Æw¡]›V'u`;ýü‹Ó¦¦žöÐÌE5ÙM"‘ü š¹ˆ¿huÒíÎ 3m|È¥ ÍøµZû(±×cïŒ>ýc WÕª¦¤\‹-fî_Wq óïþµ_˜…S› -‘T‡ñ¡]©ú»`O5š‚~wÆBß»ªú"üb, “A­ê>J“X›Ö¥z¥ÒŸâ@‰o‹å÷Ï•YÉŸG˜Œ\¸™\݃!ÌSÎOþÉ¿ªüݱðôJ™*R#aôqh·ã“åJ.Ýñ5‡À{ú¡ªeÓ*ØçD 5ØçTK2¶[ŸÃÞ øŸ-·‚E÷˜¼½l'#~ÙBF¾Ó4Ïzärþ/‡v;m¦:ÔÈL’ö‹W¶”ÂŠÚæF4%âbù‰Â½%æ—í ¢üÎ JìRÕ}NÅ Ä?VhžW Ï=P€¶x;ÿéœDˆÃŠ*M6ÉdHÚ/¬?‘ÏÈ»!JE¾8ÕVY#a4!¼CŽýRz¥£öö‘ß@oõ önmp}¶¸b!"€˜›ñ»î2´¨H(؇gÕd çý†©‹ãÛE^‡ÿ3w¢8È݆>ÿm Wìòå« Ô釣ÿXãBAOų|…?-ÁôŽ&ØþÖÄdT‡î­¸Æ<†+« Ž{F`‹òLÄ¡E¸¾ú÷Áüb¡©^6 Ë¼ßÉÉ/¸Ÿ€!]Êœðlôq·S°û …ñÕå·ß—Ò¡][œ–]<Ú!‘ »E $ÉcB­ûŒÊ¶9ßÍ­ýS*ÐRªL„Ñ]Çæ—)SEi4 KÈÿs=FÖØoˆ%ôwÜéEï&¾ÿ^G n†¥¶‰{æHôlPc»aïõþá‚ÜÉ‹0M_ ›qÍžƒ‘oCky'Ž«GaBá>„]ßÝ÷ü?{çÕцá{ KY"(VT@E°aEY{ìÝXcì=*±k¬±÷ÏŠ½"v£&–Øc‹Æn¬XP±ƒ4i Û¾ ˆŠŠŠqîëâÒÝ=gΜsö<ûÎ;3Ï ½¶„˜]7Á±æMGbӃ迂À¬òb®ŽÏ &à(äèÃ5tÑœ&*ƒi! úaÖîÚ9kÐ%ÖSÿ÷8bÏ>7Ö8<½aÑs7/¼I~íF Ð"á¹:E —¹$•[¨P!Ž?w%/ÌåRú”sÂJ’àÑõÅA®¦¥ÓRx­“¼ÒìûóAb´Â¾]C¼zCæˆBUÃÅèâtnoE3ÓrN$üy÷ͧPÏ0´×O£1@À?huå– IDAT#­±jÙ…ý ÔÏ7Šº‚æßÓèu ½…¬ØTLŠæBô™W[ä±Û‰Þ¶¸Ÿ¸•Q–®ˆôØôþÖI´wâRœAÚ+!‰ÿ¿ZV E‡ÒÈÌÖ ‹Ošt/AžÍ°@šß¬›c9¡ùëŒqD"I‘g2¼‘ÿ±pÇ´a7L‹º UÊ0¨eÀ]¾ÒJCš9# çnXTÍŽfó0ÔÏ34FáƒI:¼V«E&“Q²dIö8Èw5k`*“оx,2Ñí/ÈTºïçZÆ¥ZžèÃ×o>PŒVæ¨M—çû_™#«X©Ä³{0{mKL .C{3iMkCʧ=Åë$¥J±ÜlJ!HÞGgo$IñyðZ¢×{=‚ÔFóNÉ妨^bIË)˜¼‚ú·h‚ãî‹eÓ7êb UXjá‰y›†H®L îBðG]ø ‘"2ÒétÈårÊ—/Ïî½Ò ^]LeRZsDi"Iy¬ÌYç­9£¤qoúägê•)ÿÇÄK=ÿóÕ swLKÚ ;<’Ø«á¯Þ—Ø¡h3Sï2HŽb|r%‰rúZh”ü™$Oy¤<'!$ã¨CÛ&ý7Ý£P(_ iŒ QéeÊß8ž,òÜf.¯%þò ã‘•!ÈkÜÆ€A ¥)¯]Z‰%òºCPȳýø«œÖ'Åx ­V‹\.G.7Þ.ooo¶mßA‹¦M0“IhR$'&R!H‚L¡Ëó?ÓŽŒ’šf‰¹¡¤åŠ’–,2$þ€è·ðÃ"£œuéüìOŒÍ–¦ÈÍîwæÚБ r gîcZ¿)&VÇHˆ Ç@)%‹¡ýçzâcæŠiÝVnÜÃ`Y³F•Ü_€úYƒÙëi¦”š ñhoD[á',º @ò× t±R$öy `' ¡Ú7¶OüöÚ` ¦%~@q{+Ú°XÈi—l4ÏÐ>×bZ®Š;ÐI¼<‰Fó=³£;¼e¤–z áAèÕŸ&o”TgN—ÜTK¤Zµj±~Óf~hÝ ™”zÎB™Â*ÇºéæŒŒÑ$Y|’„‰QœŒË£~Øõ‰Qç§{kf‡Iå2Hž,EöúÃtWö¡kÐSÄÿ³†¸kΘ7èŽüߟ1öÞkÀ±>ò 1¼D÷ïR¢¶íD¯MTž×šr¤nB…l'zóÆm0û¡¾QP".´ R«`%aód­{cöãLãÅÔÅ¢z½FúP4[çСæ*‚.Ížt&UŒ#DkL&[ £KíºæD]Šü˘^Œ’šjI4hЀe«VÓ£K'ÌdRj°ÃL.Iðqt~º—ëéåŒÞHeè“¢$R;úŒ½i«rÕ£ÓÓ=`xAü¢Ú¤›Æ}±‰ÈÁ›’_ªWµO¶Ù7V÷>q«ú“–’Æ¡ÙØ„°”oé7£¯úÅ î$fáNbRíDì$rðÎ4Þ?Aì’ĦSmCÈ¢gíyãÝ„Lg‡ODÒ/LÊÈ(etвeKÔqqøôíƒo-¨’ßS™$Á‡³:Wý·÷¦I$ÆõÂ’V••$'a»ö?cdÔéÉî:Øk¤µ^çvÒŸT*¥d‰ÞMÞ4NOŸ?XßÄOG+äR±ò­àÃèôd77ÒˆŒ †×—²–J@—˜;H%H >ðñƒÄhuît|¼ëÃŽ˜DRèÍnwA2I×%)* åäÉ“têÔ‰nݺá;åWº¸Û“ÍTŽL"A!“`*“ ÑIÄ5|kò4LI$R¢c¢173.2&‘JÐë ! ¯D(::¹ìý¥åƒ~>;ídMž†tºþwuÄa^øôf©uƒ*ç¿þ/#£ÇS§VM @xx8Ý{ôàE¼žsv%På¶"¤R[ªæÉÆï1AÿÅë-þýzÿí”:µ¡PÈ º‰Tšb˜Œ4¹™–Ô³&“Ëxøð–V\E:_l©"Á»‰‰‰ÁÒÒ’¿þú‹víÚáééIpp0õêÕcôèÑLœ8‘dz~ýzråÊ…T4͙ěKíßûêx-;o…L&5.U¤7:>êt:t-:©TÆŸ{w‘'o>Ê”+Ÿáãøs÷‡EF‚ÏK›6mðòò¢ÿþŒ9’y󿉗.]âÂ… ÄÇÚÑà‚o¹\Ž-‡ìC§Ó!‘HØÆ|‘T.C®PpâÄQr:æB©´xG‰©b”Å‘H$ÔªU‹Ÿ~ú OOOªW¯N©R¥X°`VVV”*UŠ'N'ro‚OŠ‹«+2¹Œ[·pó:jur9­–»wî°û(•JŠºû ò?ÛREq7ü˜¶:‚&cúQZùîíAKð‰õlºåʼ°ûPÙÔsèÓ8œ·/ãÚÄä‹ùH¥RÆG½zõpvvÆÆÆƒÁÀ¨Q£h֬ǎãÚµk´hÑBtå >9‰„Â… ó2ò%ƒpåòEâãP˜š’ÝÎŽ’¥KcgkgŒš>€Ï$FžœâÐßRªéÞ½µ-OndóÍö4ë–éx¾{ç%ÐcåšçIçTôQÜ=s†ër5_›¶B¡àçŸF&“annžœªT©þþþ 5™(îÿ¾† z+¤7Ö±éfx˜½{·FŠ­× y}ÂC|aÒûµ‘H$˜ššbjjúj%|2-¤0Ä\Âûs<úOâ'·0ö¬9Eø[ÚEñ·V3´KKêUS¡R©¨R¿=ƒ"HýFöþzú5ªŠJ¥¢Z“îLÞy—¸ÄMbNøPEÕ›@Ãã½3ðiß„š*c™MÇžâåÅé£.2¿]U¾ë¿• ™uöàcɤÈHOØIŽQIµKR$‡7+FúsèY¾Ï-KsmðENß–Ðdؼ <à k–£ÇcX7±&öI2iUŒf?7¥¨u÷,aÁÌ¡Xö§¿Û›Q—ŽŽp!“¾›RØRC¬i”’W¶&†øûl5”­²ÖÌÔ'E朽@ øx2GŒ´OØ¿þVµQÒRŠiÙ6Ô¶éÃæ?iÜÓ•ôŸùì”ð®L T¨D‡pZO^ÎïªÐ¥`Ò&¥¨Q£yåPÞÃ’'pòäz»J»HÛbT®\–¼Ig¦I#í3LžÃ‚G5˜°´'¥¬þ›y&àk%SžÈ„{»øí^7vÁÀ¬(ÍçåéîíÜŒ{×Þ¯ª’ݳ:yÄ…ûȩ́W8P(;Ä„D‘áN¹D^ü1†é‡´ž4€*ÙÓŽÖÁ—#"£8nnßÃ3}8+~¬ÁŠ×> gË¥Þ÷Êà<•¤¹.út&þJdÈ¥F÷‚÷EY¢6EîígÓäåTœßOk Y‰#Cô¿l;ŽÓÓYÕîU¨¥åð„alÙz†ð 50Æ"t†$ç“T%pŠ i–ßI& 2wþžIÃ=™Ô}*CGçbå¬ä½áA–á#Ñç·q2Æ…Æ ÊãV¤E’þÜÊÓ°aA4ç¶ñw¨“lv˜qƒ¯ò"i‘Fn±yéþG,R¬Êt§_…~[¸Ÿgïk¿ øÏ’‰bô¦íìW>ÕB¢Ä£Ç‡cŠÒüç1Løu$:ÔÂI^–øõÑFð(¸½ßãaoLqVsÓÇc!.4‚ µ£r‡:X]ÙÈž 4ÔKðM’iãŒ^ÙÎ΢úŸ?³lÍ):M«Žm:rk5£§îæJà3¢u ±r¢|ƒÎôïZƒüf)B‘ûëé×h /¢õÈìŠR»Ëp4vNŒ$ôÄÜÞÍÂÙ«øójEŠ×íÌÀŸàb!4<Ø6‘ ›Îøô%HU†!>ˆ} ¦°d÷U^hÀ${):ÍšEg=—'5§ïÍölXÕŽ¼’çš9Žå'ïð(\ HÈV¨ ? J›âÙñ7—ÒÈF®Gh%N•Û0pD‡4íJÔwðwD^WÍ›¸|’†Ç»§3ÿ¤#-Çã߉Óm‡¶ï}JŽËéU»¦@á‘QüÓb:ëO…R¹¾fn½Yõ›Yâõ+kOàß}9öÏ#jÛc{• [ÉÞÔ—am‹cŽå]Z õgëí†ø¸½î©­{º‹I³OcPfíøÚäLñmišòÞ«CyÖŽð÷Ò­Üõîkâ\g]ð!ïÁ6‡ áQωÒÌ\ëPÉf'‡Ž?áÇ‚N_ÕR‚OC&EF¯lg;×.IÞHÏøsè-1x’íl­ÁS˜9s ¿´qáñ¦qô˜)Z­ŠÑlÀfÍœ@ïrÑì9”e‰Q—îù^Æô™Î!Y-MŸÃô5˜FŸqûÖ舸y–uúLžÅ¬écéáù2E:žížÀ”ßÕÔ>¥K}™Ü¯9åÒÐh}4wÏ_åEÁ?s3&øP]z’ECgs&ÊÃÈsV¢ý°i,\²˜9£cyvc|¯:3¢#ôúÂ, SÊ1é14!Û¥ü¾n"*ä@ñfÓPKD(³+Iš’,ÍæŒ›½ÇמcLkI_   åE8ºÚ#4Ï/q-Ê wU!Ì`U¬®’.] {ðNã[¹ªwâ‡^5^¢TgB´ÄUÏîÞÊò¿Ã£#5Wp1û÷ lá„ô¥QŒ0ÍO…BR½#Ù ³Ä(Ùv¶%%-¥X—mCm›6ÿÈÛs¿‰¶³¼©Ûa óFx’p|9¿§L<$ÚΖ¯PÖQSÌÉ“OÐ@ào«9#Q1jR/x•Á«Ao&ôÆpj%;î¥(ú(½ÊSÞ«í¦FrzbB"Лç¥Tù’¸¹§bÍê¸eKÿ²X,G¥ å©X­ý†7Ç!ú,‡KKËl=Py—£D1wÊÔéBŸZÖD^½Lpª–ˆ†°{a`ç„]Ê É\.-Ì PÁE½ß6ñ÷5zƒŽ˜çOm|m,m0‡ÿ7›sٿǧaä€îå3^b…c¶WÊ"1µÇÑ"Ÿ¾äõRxvýX»S2ÇÛh]T1 rªC:æœ^µ›‡ЇdÕ/)Ó¥ %s)!ú9QI‘˜bŸÏý‹‡D|õùEAfðõÚÎ꣹{ùª‚{²Ÿµ”lîÞä)ïE§í©Ès²u­ N:QÅô/~iÝ™Q‹vq98!íýÒÀÄÞ¢y­ ÄÞÙŬŸÛÓ¤vUTÕ1ô@$hÔ¤~Ö hâ@aIF“ã²\4=œ:¦»Ùú;ªV©FÝ&s(²9fã5ÍSöOîÅ„ó žÕ›R––7 i ¤!òÃ7bðAc~HN æX[XáñC;œlfÕH‚v¯æŒe}ºVuÀTi…\ó’È„¤«+ÅÔÂbH‘‘€ÿ²ílz_ð7Ê0ÉÝ€‰›ËpißÖùM§ïzšÎ\Ì€ ï^“L"“#Å€ÎÄþËüÓØŸ»-C¦TÇ%[×W gÆ4÷ÄÄ\ qhÞãATä«Ï¨uuL„Æ ‹à5tùéOÜJç|•sÑ #ãóClÃN³nî ¨ø Í2T†ŽÐ³;ùëe^Šä¶Bz‰á`îjõÞ¿Ö2g\¬8æçÇ>©7NÙô>Oïør²»»cs‘Ÿkð,±¾$mäCîÜ£€3ìÛ²™Ó/=é³  E—Ó‡ÿÍbÿ@ò¶ì‰kÌ=ŒïKL¬ÉWÀs Ú¶,Ⱦ•˜™¯/µ‚Ø1û0jZ¸¦ŽM ´dD‡£ôY;„NϾ§]½28ÙÈP?¹@`g<*ê—jP¼Jº›º¶¢»pÎæjOykã›RSKL‰#"N˜@üCÎÞÓ‘§¹ J øX1J¶í¾íì"_~;úŒz »ÐÃûK-âlµT’mgÿxDœÄçê½™30ã¶³2ÇúLðÕ³`öjfÝ€ÆÄºÃXÔ·.92´‘Ž—wN±iåižª‰N^߯¼çøeiúOêNÜìLè€Ô̧ ¹Ó4iæ\/ë?8xì ?ÈH×¶žðãè1íVyŠRºÚ0µ«‹Gа"áÉEî$@˜ÿ0zø§ØÕ¼:svŒ§Œ…çg2E=™¹‹~a_‚y+uaæ°t®¹D‰{7_ÖòcùÆ?Y6a«QÞMmÈSÔ›ŠùÍ‘` >:L-Q$ÅÙ2ªô•xHÆÈ("12Rß=À‰°¼4RåÝú@ØÎ~A4®è@Ç¥˜»a(žßRx ãÈ/mÙ› š’êwLðŶ³ß&ø¾/µô{˜½þÖ;”ÿ D_ZÅü¿­höSr !$"R‡_©µýÇtdÃs):=߈¡‹I>ª÷ªC÷ tT¾„íìEŠmù.ôùÒÕø¬H±.ÞŠ~Å¿t=Yoâ·X d}„ ‚,#@%Èb¤8ËÒÑ£Y}+þKW%SÐe銿xü‰"¿(š§]¹”#ÏÅìVAæ’5Ä(ú'þ:Ãýè/ov­¹ïG·ï1òÈ›&aDÌþiÙx&ݲ«HGðÙLžv ѦE È2­7MÈa¿lÜ÷!ñ€Žnå¨ÓÁ‡ÎÞ|-ÃI$föä+à„½Éu;Ç][Ëâ3v´ZÑ„ü)†ë#/²rÈ ü-&°kNe’f“aPsoÏ<¦-ÛõPf¹ÊиçzÖ̃qf™õƒ,žµŒ]Ÿ/ÉF¡*­ñÔÏ$ç:m'–OaÞÖ³<7'_¥v Þ2é9Ûa þÉßl\¾Ž]'¯ò,0µÇÕ³mú÷¢¶íYÖÆÙTjœ—®~ki;° [»,ÆïZ5•0ÿ€«$¤&SÄHÿò ûø°ùANÊ·èÂðây°Ð¼ ðòE´oñèÉ‚Èë1vY½ÛÙÉYÿ}D¸÷£™Kâ\/M(Wö®aÑ’í\y Ò2)wÐóòŸ™ô›zŒB~eQež˜ÏÔqýÑ9øáSÂIì¿,ð™Àþüå«Â!ò,~Ó3xœ=ÿWŸÒ$+ÙjöÏÏØ1gƒ±bÝüäIu‡ Äø1 ÷2®)KаÝÊȆ>üW/<$¥yKΦ£Q3Ç«‰9¹å˜*šÒÞc=óÖŸ£[qÖ_Ó dY>^Œ 1ü»äW6?(DÇ%¾t-öj¶|õº-ÿ§!hËp~^r†xÀÌÏf>ŒêáMš¦Š‘çX¯ß#$Ö2\¼›Ñ£ÿxå0¢9Ö¯#µ£ùÝ·6¶ þ&óÚvçxUø÷tÁ$ú_ÖLžÇî ·y£37º/CÕ ÓÞjC«¾ƒnß/$×ìL*g!ú<3ûŒb×½hôH±v­Içáƒh^8 W€˜ëì½O‘¾qH JÔwü™¼üÅ;N¤ÚÁQ,|íÚEsyË!" õfX§ªä‘‡ó(žœúÿ —éV Å㓜u á”öTs3œéÿä­—ýÍu}rð~V²$²þ×e\³m¬•)ŸÂ·v’—lârRʼî”,•'/‰=fæÜ=\Q‘Q‡àm|¼Åü˦ýa(kŒ¤[z¶2ìJ·ÀgRì­ôŸYÇŒã˜á¾•©UmRo®~Ĺs÷°i?Š!¥­Ñ_f׊U íõ‚ÿ­LY«wWËȱã(ÛŽbFE¤±Z%„ß“’_þ’;û—±hæP, ûÓßÍ,U9³üÔè:ŠšÙmQÄÞaϼ™Ì“‹R~Ýq~c†gÂãóÜR;P½¨Mr2ÎÌ­þ;%H gÿ‘7+™@T””v(“v0É{!3Ô„j¼ÈgSGYgNÜ%¶h1, ܸð E‘ä7MPzV²­dÝr½ÖDVßÞÁ凜ݙ²ixs¿V=½N—|O%)R©b]¤49Ô¹ð$¯ÂŠ·#dˆ#Mèª!_Ù‚X¤®K±t©DUã+wWkÿìÈïç“PÕ&J˜’ËÓ¯r@¼Š[Ò½ýb–éˆgceš{¤ÆŒ<åTTôLòRMîmhóÊ¡| kN ääÉ'ôv+”:·%w LU‡ÄÅ(Ðë({GœãVTWœí^˜5aÃŽv)ÏH‚TBÚfoRk —Í þëÙ|¡=³ÃËÇ<ŠÒ6 u¨É°Á§ð™Ö“VçªQÁâGWeÔ‚zä”ú-V²çŸ¾DKJ1Òûð6a䦱K¶wö^ÜóýZ¾)®fÕÿ±sbY,¹]ìã~¨b$øx2!gd0>goÍÄóèÈ æ­9È¿Bˆ“)QăIñŒw›ä®@¥Ü‹Øvá1ñ dShC{>$ ¤#}ä¶.XÄo§nñ4R™R‹âÓpÑÅÇ¡Ey*7ýô0Áù‡ñô½7’?7à 2 P8'Ùd€>Ž ÇÄåôæûš.DüsÍÓ“ìú«^-ßÿ|Íí$¨¢c‹‰Œ©Ôù ÁÄ:?I±£Ta ZââE—š søh1’Û"— ܸüuCÒò6ÔÜõcè˜MH äסÅÈnâ·Ñãøë½Ž$A&ƒÞ€‰±¹ Ó-_?&ú6+[}ǧ dþ¿%é:x^NDŸùƒ}Óžc/35CÎKâ2^)‰²­§l¡yô ‚# X*n1£ýnzºa#…¸Ë‹»AM›µãéXÐZµ¡™o:-œÁþª¾Ô{/+Y)¹ `Í®ÞÁPÐæ­µ´Èå‚[±´rF ×Ä¢AŽ•é×ÒO*Èê|ô8#‰eqšVRòòÀ2þ¸—ö ÅØ K<¤(?tkD…b®¸u£@ª¼>}¯W}ØUÎ=…¼%raŠ +Çlðü&Ï>åÀBís®ÜVcW³3mj”¤ˆ³+ÅŠäH·Q"·s–0¼·—¬Kòä2ãîÆ…U¥eãB(Ðóò^&¹ g}’ IDATqqHJP™’dzvú`„kÞÓJÌ 7¤Fv gWlâÚG¬¤ Mj’ k4AæðñÍ4©5^ýRõÒtíÁvÍP͉¹.’ÇWx”¯=‹áÈ&6®Ú…mmWl¥Á~c|X W’ݹ öi=ƒ–ŨïiÊØ=ÿZ¿ar÷þÛÐ>ÞÉÈ+yjëD±²³² 5]-“ÏÁĹ#3'i™½t9C{DcÀŒœ%ê2tNßDwÈ÷´’EŠMÅA¬XêÆª•[Øç; @fE7/š&hß½T“>”³{PxþŠ»èÖd_Ìvöm¼9Öçk"öÒtÚö»HÕkèîúߌî,§cç?(½`CK~]÷G1„íì‹âéQæfÿΣ÷ôôÿ*Ð>f×ÿ6ð³;=„ 2a;›ÙÈrRgÄpìôÛÕ—уMÉ k\WøW 2•,)FÒœMYy¼é»7Ì¢ÈsÖ¤O/]‹O„"5z|[F¹‚σh¦ ‚,#@%b$²_‘ix°} #füÅ‹ŒZ0ê#¸¸yKNò(o`ÙŒ^&¬eî÷yI2·P¬ÃO?”&»á 'WÏcÑÐÙÚ<–ŠVä9+Ñ~Xi¬í,Ð<<ʲé+ã[š-#J"Ö½>™'FWçÒïJ N]W1þB`‚m¾B8;§ÜÒ”¼«‘7ñUÑ\¡ì?<‡ÓÔ4sLgZ…u¼*U0ZÈV,‡“º-C×øs³á0Š'NWÈV¤ 5¼Ý0Jåxĉn{9OÅ’fÈl=Py'–UÌ“ {øéêe‚5%qV=Ág#óÄÈ¡4^¦sbíTVºM§g…ÔÆ^ïDû‚ÖÍeùîóÜ} Js4hñÌhž[jGÍ¢È^çF˜Žâ©7‘;8cO4/¢u€Ø;»Y´`#Ç®?$,ARª{5b½Tàó’y‰Çï¶r-ü_°apOf áýÜ‘u<ýc4CW\#OË_˜·r‹Æ~Ó{VC’˜‘2Òîÿ—HåH1íjcÿeþÀiìQW¢÷”ŬZ6“>…A@ð%ÈÔ¶ÄÔ‰†c—c­ìÅ/£~B6} ½lS)žD¡DšÈ¸”£xr)}þèÜÌÛØDÊùÔ éIß6ŽÀÓwÐY–ÅÕîݧ¦ àz¸‚r#;R·ŒH@ŸÇîdì|Aæ‘ù]F2{Tç1Âû%;GŽdËýÔù&ön¸*c8¾ÜgÎp|ï.΄HÈéž‚þ`íö\¾À›÷yùª`”9¬àá1]|Š:I‚v³|Ý.Žžú›}+Ç2zgNÍÛàžì³ÌÆ«Îûù±ïìUnÜäÎó´ÍöÁ§åÓXˆ˜ä¦î/“ èìÂÑ~”ö­öúçJO~Ñ‚ç³ý?È©;í¦×¢\“qŒ|8•¥‹G°_`‚¥CQÊÚÈS\Zö¤îùÿ±fþª­he,K!çùA_F­ˆ‹|xu˜ÎήH ÊÒôŸÔ¸Ù™8Щ™-Nrcþ³!²:YÒv6c¨¹<©9}o¶gêvéø= ‚AØÎ ‚o!F Kð7nÌ(9rÇ¿t5A¦ "#@%b$²BŒA– ‹ŠÑXÌ ‚¯š,+Fïm1+¾j>RŒ4ÜYÚ UµŸ9ùnÕxyl ÕUíñ(æÄ ‚×ùH12ÁÑ£&º þ}–bZü-Vôý‘¾Ëx5Ó+gW U¦˜ýW<¢@ |>º™f–¿4y áZ`tò ñ÷ö±ûò}.ïÙ˽øÄ7õÑÜ» ù=Ék †èóÌèPª**UUv϶[1¤_iÚ>žžmRC¥B¥RQ»U?fþ~‹˜Äƒ&Ü^DKUSæÜH: ÈC}P©ús"ÆXÆã½3ðiß„š‰e4{Š—ÐG]gÛ”Þ4¯a|¿Jæt´˜‹QÆÚÄß\JÏF5P©T¨Tui?|5"E2K ÈL>:D‘Û»SÌŽž "¾^vÌIàÁ¡£„Ú¹bzŒ}½ps3ƒ„‡œ ‚œ Š`-‰Y~jtEÍì¶(bï°gÞLæŒÉE)¿î8§rX|·Åì»ÑñâÂ.DxÒwbS [jˆ5-‚Rû€MƒúàTœ¶¦P.—œðÓ ˜°é<ÕJ kZà³ðñí%S'*8KÙ}ó ÁšÒ8éï²÷PE:Ž¢Ö.üwß¡—›ò«Ü|iFѲ¹&ýrÊTM²b,F^GÙ;â·¢ºâl—αÞb1[$£õµ-FåÊe“'ÖÆ]Zĺ–Ô›=…Þå,‘1š-È6¥0/Ö´Á'çãÅH¢Äµ¢,>C@T{r>ÚÅáÈ¢ôP¹ãipeáª\ï]ŒB·ÏðPâL›‚ÆXBy…­ ñÛ©[<Ôc¦Ô¢Å…ø´˜Í°½†–«7xiRŒêÅ,IÛ5DXÓ ŸƒLèÚ—“£lEõwø;0”+;þ"¶Ds¼²›SÕ”bÑÇØv9„û§ï Ë¯¢„­ôŸ2ù'-¨;pKW/arG÷VÆ”³© :tï3À€A«©YzþEšV ø,dÊ8#E¾ÊxZÇpq÷vüOê©Ð²<¶RÚWâû2:NûÿÆžó/ÉQ±,9å€ö9Wn«±«Ù™65JRÄÙ•bEr›o¯‘‹Y\íäȬr’îE¿ÇRÖrì\ó¡ˆàŸi;<&[ÓvîHÝ2n¸¸ºQ4E† 2Fæô±›¹P»¼%{øfÛ¥³c©-ZUÆdðzöbGÓêNÉù"7'6YËÏ֔ɥ$>0”W-´D‹Ù¿qèbUZ—µ5¾´›åë²Q½ˆê;ð݆S£Å¬Ô¤,uݤÌ]8…’f”´—v%Èù–ŠK°,ÕŽú}Ùy>›&`- U:P¨|Iìß²f’DY‚ŸæŒF6k~ã%ÉŽK!cÝ¥„5­@ð™øJlg?§Å¬ž?zÒ|®=3vN¡¢òSK Èš| ÛY1ZóŒã;O›39­Ä=:ÍÆ…7±¬4%E„'>5BŒ4/¸ö×F~¿þŒ(  ÌM©ZC™×»ÖYt±@ð_ä+£Oh1káA¯[èõ)ÊFüö ‚,#@%b$²Ÿ\Œânø1nØ|.Ædt-Á'Ö2å)²¸K‡>ò*¿-^ÊþÇP÷%1 ÜÃÒ…ÛŒ÷ÖÁçæ‹‘„'§8ôw‘ºŒî£åéÑl>rØ,.FºðslñßÅåð ŸÜÄ@ôømû‡`1ÃWù†šijnÌoJÕÉÒ3q_ŠoFŒôáÿ°fW0ff‘\y„,u ß™0ÎHCЖáü¼ä !ñ€™#žÍ|ÕÇ4J¿µšÑSws%ðÑ:X9Q¾Agúw­A~³“½î¯§_£%¼ˆÖ#³+Jí.ÃÐØs‰Ñ²vfŸQìº)Ö®5é<|Í +Óñ$ÒòdÿN›7aÊ “‡û±#°6=\’|4<Ø6‘ ›Îøô%HuLÍ󬜱mÿ<"NbM!w[‚S\‡Ç{ç0ÝÿW„‘d¯1µã¼È¦ áÔšÙøn=Áýh°*àM˾ƒø±‚=r@y†…cæqèF¡q ®uÛÑÐ!€½žäfH&¥h>p ½¼Œ7Í æþŸ¾Ì\±›ËÏgw§n÷aøÔ/ˆñ2ꉸàÏ̹8Ê|xä l2|ßtaçñŸ=MGy‰’ü•Zá3¬#åíÞ2ÙO ø@2AŒdØ•nϤØ[é >³Ž+Æ1Ã}+S«Z§ÚZ|‘Ó·%46o8ÈšåãèñÖM¬‰}R¬fUŒf?7¥¨u÷,aÁÌ¡Xö§¿›Ù{ZÖñwرå[§\y--œö°uóU~øÅãÔ37Ï .‹Ïä†ä—¿äÎþe,JqLÔ7Xæ3‚ êÊtÕwËH®ï[Ê䃤ckK,Wõeèf¨Õgýœáî_|÷#nÉJú3õ.^ÂîDZüRÖõ]Ì¿˜ÿ9T¥kñt·Žæ¢ÿLÖý:“[§RÅÚ@رÉô™|™=ư ŒÏù2mê`Ì øããn†&hC,åaɶ ›R–ìê»]¿Œ«ºo6Hãï²vÀ@üdM8{(4WØ8m!Ãudãÿê“㛉©Ÿ‹L#)–.•¨êb|åîjMàŸùýÜcÒ##Ù)á]™ 6¨P‰2á´ž¼œßT¡KÁ¤MJQ£F%£Å¬‡%7N àäÉ'ôv+„É[-kß|J D_ÞÈžðâôÿ.7r¨×ÚƒÕsý9Ý«5Sno]”Š^åÇ,aMÀ©ÉÇL¸ìÏöÇ9hµtÝŒî¥óÜz€ÇõÛ“?­¦]ÊcH$`0¼‡‹äÇ An*ƒ.ÙéR"3E޽Á0À¡é¦µÈKÊêK•Ž˜‹Dô V"í÷Í`©|‡á™ÒFE¢À6÷W2¥QðUñÑߪؠK<¤(¿tkD;)è”°zs+ºôž Dœ"Gšå7Gò®Çý5ËZwÌ€¸È(JkcžÚÆ5 o†Mï„kŠ +>`ƒgìdÏýVôr}×YšàP¬0– çø+ † žé%ÊS£È]Š‚ò\<÷ »±™†æ g.F`R¨$¹R{í¾ÓÜ”È_Š…7yRÝE)…Jä„íG¸Qçõ¦h"o¿o&Ø)ˆ¹þ7âìiæžÞbAæñÑbdž§Žlbãª]ØÖvÅVÌãØ¤O%˜d³ÃŒ M8·èB…íS˜0`*!jRØFÏ“«¡$õ¦½ý¾²dZ:eí¨áXõlI…|æh^ñÜ®MËÛ§Ý´>‚#…kG&þÊô5³²Ã8ÙÔ:7ù-‘fÅ»ÐÃûK-âlµ[J&pïfÿaì"w®Þ›9›eÌÁñ–µ¯ˆ¿»›=-©4¤(© -=hXQÉ‘Ã;¸Þkð».“´½ó… X¿hÛÔ€ÌG7/ŠÚÈ€tFaK,ðè½€é³X¸zbÀ²€7g ¢ƒ»9>@Œb_c Kt˘»j=“Å`@[CFÖ¯KAS)²œõ˜°TÆÒy«Y=n/1€ÔÂçòű—¿û¾aV˜.ó碜·ˆÍóG±Ee^*u)Gc!F‚OÀg·9áC½ Œþ×ïlDð/dE¾„í¬èY!F KðÙûh•Þs9öIücÁ׌ˆŒA–@ˆ‘@ È1Ya;û‘¼ÿù ‚´¶³Ä̵S'¹‘²RožŸŽç»R·N_~{,¼]‚÷áj¦}œí¬&hã†Î`ßÓ·™ïKPØå¡ S^²›~C—V Ⱦ™éׯlgáàÊ#t+Õð„I±õÄ"¯Ì.W øï“ £† -ƒh^K…J¥Bõ]K||O’N+%þÖj†viI½jÆí«ÔoÏà…‡R¿«Ü_O¿FUQ©TTkÒÉ;ï—¸‰!ú<3:Ô£ªJ…JU•†]ƳíÖÛ¢W¶³ãÇÕGyÙ ¯ÃƒmcéÖª5Tª4i$”m=jÏSÕ‰-OS·=cNøPEÕ›Æão.¥g£‰ûÔ¥ýðÕ\ˆLÑÔÓ<ãØâÁ´­“t.ãØ|-ê3Y•Ya; dÈv€lÔ=“Î ˜bïðîé¢òœ•h?¬4ÖvheÙôŒñ-Í–%17Äpi~_Fîs¤ýày¨rDrzÕTæžE¾McñÊ&æî ¾„ílmg˜`›¯ÎΦÉeG¾ãêÈl=Py'UÕ“ {øéêe‚5%Éõ7+ÿ¥ôð%tÿ.;R ÈÐ޵^Ê7cñ*ŸÊk@ øÏ"lg3d;û¡íY±wv³hÁFŽ]HX‚¥T öj´@ÂÓy Õ6±)U'¾qø8ô(¿¥Á7Ž°Í¨íì‡û/óNcî¶ ™R—l \_5œw?7(©2j.Ý‹¤a f¶BˆßÂv6C¶³ Q(Q &2.­ú¥}~ºð®‡+(7²#uË(ôy, QŒ¹ÜÉ'ÝÁí{rÕ)@1¡@ðÍ lg3h;kb2†ãËý8Щf¡Á˜–­‹GšçgDf㌋UÇüüØ'õÆ)›žÀç¯ÎOjW™N à?”ÑÒn4. EìSîG¤^#w¬Dh$ø†¶³µUzòÓˆ<ŸíÏøA~HmÜi7½åÓ<¿¤}JÓRwâfodâ@cõÍlqªÛ¸J­Äв|™b7—e;f2ÂO2k ÕìOÕ†î¤ 0‚ÿ0ÂvV ¤BØÎ ‚o!F K lgA–@DF K ÄH d „ ‚,°ý¤PîaéÂmÆéºYa;ûI1}c~Ûþ!X "9õkju\Æí„wî,|S|CÍ´³ÍäXårÂ)ãl@Œ°ýœH”xô˜ÇŠÏ|Xàk ÄHCЖáü¼ä !ñ€™#žÍ|ÕÇ4J¿µšÑSws%ðÑ:X9Q¾Agúw­A~³áÂýõôk´„ÑzdvE©Ýe8;c.1ÚÎÎì3Š]÷¢Ñ#ÅÚµ&‡¢yaeš¾)mg§ Ñ0y¸;kÓÃ%i2›† íS˜´é ·G¢Ìs•¢vû~ônXe’hiC8µf6¾[Op?¬ xÓ²ï ~¬`Ÿx!õD\ðgæÜ  Œe><ò„6‰¨¹<©9}o¶gêv䕯°}6ÓVäv¤Ìñî?— r;¿A"Âvá×Os=¶ý'ÕÇIEàñ ,›Ñ‹À„µÌý>/&†X®.êËÐÍP«Ïú9ÃÝ=¾øîGÜ’•ô)fŽ&hC,åaɶ ›R–ìê»]¿Œ«é\9M ?cæ#gçñ,¬”CèC"òÚ !|“ÛÙ×lg‹àU©‚ñ˜Ëá¤nËÐ5þÜl8 ÷¸S¬øí ù»úñKÛ˜åKç'. #ëVüC»™^<ým7-j2cRo*ZI€²¸Æa÷¬´¯‚.ú9/±Â«LYŠU"ÁíCn€@🠲&ñ<:âËÐNÍ©[]EÕz}Øø4ê÷µ}Ä…û±io’h;“h«¼ÂæI}hÓ°UU5h1ñ,Z4ï°m•h;+5ÚÎJγåxévòI­ñ¨YYÄun„éHxr‰@­žåI¾LrS¾´ šÀK<æþPpò¢°eƲÓfEÛñc©¶õmMÏ +Ù#±ô£à[EØÎ¦g; H3PCFúÞ$H$€AOº¦–obæL»¹Ûñþg›ü×1¡ÇZ6u\Äü®nXˆÞ6Á7†°M×v6ŽÀÓwÐY–ÅÕNŽÂ¢å;¸xîwc3 ÍÎ\ŒÀ¤PIr™Z"+‘¶áRD×›oCjA~¯V ©Ø€Ús~¤ïÖÍÜüa,žæïÞU ø/!lgSÚÎífùºlT/b‡úÆ|w†áÔ¡ îæ 5ó¢[óÜô^>œ)¦½©çlàÎn_V=ÊKÛQ±–(°jÑ… Û§0aÀTB:Ô¤°ž'WCyÕ›öFõžeÇ?àìšsÍ3ÎÝK{,ß½›@ðŸCØÎ¦´UÈy~ЗQ+¢À"^¦3¤³«Ñ(_bGïL·˜ÅÂÕ£8–¼é:îº%Œ!I»ÐV­$<þþ?îÚ¶™£æ.$è›ý[ŸÇ…=Ûc“VRs\/_"iÊ4€3*'ã§w˜óþJ¶Ì¥Ôè‡g9xt:UÙÙ—ùû@ ã[yUßE—ýŸ.;k<¾•Ïæ½Îë÷p上·Ÿ ÉõÈÎNÓegÑdgçLãÓ_ré{e$ƲÃüuâoèI¤Û”ì[Áëó–²vÇaò+<ñ3”Ax™”’•H\@ÊÎÎÑ$æ IDATºŽx@P Jíj±Š£¬fkÍu,ÙÂÜ^à‡è«y蹋I¬`Ç{òâ>W ‘H¤ìì1ÜÞª%c`ß;(¼6Þ‘·rÆ@T ü¼çe"*M±ìfG'=ÇÀsý€ Ô_ÎH"q );»|7>ØžמÇÂg^ä±Ù¥\×;õðï|¶Õñúþ±\tq ?x‰™ÜÁ vA¨{°¥”ŒÁI$T°vñb¾7\HB JzN=m‘H$v¤ììêe츽#Ý<ÉÜ’Y¼¼p¡â•ZefΓäq/ð؉çXðî“ülLDµéNûøÃÄgn¥ô•¥<ýÀ­ªÞ!$œ­-`”³fIHÙÙZ)fýCCxøøC,{ca`¤Dr¶²³‰¤Ù"‘D"iHÙÙZñã‚’R´ÉYBFF‰¤Q ‘D"iHg$‘HRvöLaÉfÕìÇyzÙ¡šdJ$’JHÙÙ†¢ªd¬µíkÖ²1³L®w”H\  Óδ쬔Œ•HN);ÛP(~•%c˰l‰¤ eg휮ìl}’±õÛÉšÿ‹¦Œc°.[{í#ï²!ßåñ­DòŸFÊÎ %;뾂1”ïgÑý°Øx%¼ò0‰æ­,}á5}2Š¥¯j? ‘œI¤ìlÊκo§`¬[ñqz"·~4‘!±& •I×3ìɯÙX8˪µK"iZHÙÙ“­ºìdæØ®ýsùW_ÌüJß‹åÈ +Hg$iâHÙÙ†’­‡zí$:rÿüGèæ<&T< ‰n6ó ’fŒ”m ÙÙÚªŠJë´“ám[á£nbgi8Ã;ø×î%’&Š”m ÙÙjý‰ð‡Ü +ùã`=ë´øu¹Ñ kX4åQnÍyq>˜"'´/Ãz†c¬Û*É);ÛP²³T™‚7EsÙí£øùÙ/˜ýY_–à­ëþH"q);ë¼H*½{w¯¤P{Û|8ðù"6Žâ͇GÓÞ èØŠI›×pß÷¿päêâ¥&šDâ2RvöÊÈ:ä9êh›ZÌí¹û úRùkÁYœ°ÐŒ~J"9}¤ìì ëˆ¯e˜ìFÛ¼Hu-¾šÍ³“^$ïÚ>$ªfd` ?ƒÏ÷Ôí8dggL„ßæÅ,K¯¨Ô†Ìo_äÞ뮤_šV¯aÓÖs¢b?oŽIcø¬”ë{ZóÿbÑ”q NK#-m ×>ò.òÇ y|>¡¿f‹´ùôˆ5gãÓ.åñ?µ§l-G×1ÿþ1\ª×ðø9l)=%£K$M); Ô.;[P¥œý,ºÿ¯äW&Ѽ•¥/¼Æ£OF±ôÕAÈ%S_â†$OP¼0BžS¢_gNã£ÌKyø•¡¤ø“c"¦ÊC¾IsCÊÎÖ%;k®ìŒŠ·,âãôDnýh"CbM@*“&®gØ“_³±p àAH\k’’âI•’Öjyù[t¤G·T¢ŒÐÎõç %’&‹”­SvÖ3Çví§˜ƒÌ¿úbæWú[,GN¸8]hlAßñCødò \sÝÏ 9šQCzë-SÛ’æ”­Cv¶B€¡#÷Ï„nÎIŠ'!Ñ&8àJ!B/|„Å_ aõç±xþ$>_|1Sߚƀ®¹E‰¤)"egë­Œám[á£nbgi8Ã;øWkÙÓOÊ8^Zÿj"Ð\vëÓô»b9÷^õ"­½ƒ~£Zº¥I$M);[‡ìlUüºÜÀè„5,šò(·æ¼8ÌǑڗa=ÃñoOŠ_1¿¼½˜oìŠwÞQ¼º¤»s!¢˜ËWp°E[‚œØ½ÕDlˆ—\ƒ$iÖHÙÙ:dg«á݆ñsgã7çu>™;…OÍ øÅÒk|†ö Çè×»&$ç•%Ìxp1†à\3³?ÝCʰžäÀÆåÌ^uP[ÕíM·1OðÈE¡ÍiªDR );+‘Hª!eg%I³E:#‰DÒ(²³‰¤Q ##‰DÒ(ÎH"‘4 ¤3’H$);+‘HRvötÇYÿä•ô÷{+êß]"‘ÔN3¦¹!;[/&Z&ß¹~S"9-¤ììé øÑqÂÞiJ$Í); @kïI#íŽ(°R¾‹9#Ò½`€(ÚÂÂÇnaôÀ>z;'°èŸ“l~fi×H†­½Ö|þ·xãõº§õ½œÑã'³xo¹kueøöîÕ´´4.v;/¬ø‡ªæ‘HšRvÖÅVŠâtÖþ²¿«§ðâùJ,DE™¨¤)JÙ¾à.îÿ¨”Þ7Mæ–Îa¨™+yå¥ïÙtÌÂõ)^õÔ]%í³Üùìf:Ox‚yç½j>/ä™UÅ< m?„Ç ¤•W3Z£*it!P”úsB¸·EÊΞaÌéïpøåtãc”Só’ÿ5ÉζiÛO/¯zeg­V•œì¬S–•Y‰¥‚ƒßÁ߆XâZb8¹ŸŸß_BF‹¡<ÞZ:"É›“E' óª?WPZZRï>5!QC¢‘±u5‹Wî!·Ø †@zŒ`ú·ÒAN…IþãäË#$$£±ötPUŠNw«|);ÛBé=éMzOú·+"‘4<‘QQdf&6.£±úêD¶íHç.Ý8ï¢A\ÿðTƶìýý å¶],¹¬g2×_®ÕkÐõ“yïc87C=¾™¦çŠ>i¤¥]ÊØæ±:£¼ò¡¶=ÅЋ´2Nÿ›RÊ*KÓªÅìþü)Æéc·×䯳pUýI"i Ü6a^^^DDDàååÅm&œv™ÍCvÖŽÀZšOú¯_òÓ©cñ%l{ýnþúßù÷$Áþÿ›ÏüI÷Pºà]îLõó>|`" ŽÏÍSn§£O«ßžË´;Oà³øQ.°5µõfOéEL1ÕÄÐÌéKxbÖZ"ošÁk½Z òSÚôCTÉçe“’’¸qÜ8¼ù&·M˜@RRR¥ýÜ¡ÈÎdóáø~|è´Å»Ë̃ P ×óÎYÄß¼˜Ç®NÄèyN<¥»ÇñÁ;pÍK}ñܺ˜%{BöÚtnììôäœdéW½Ì;+o¢çH}}…o$­’“±ç‡*K¢X‹r8AœÛNíüPhJÖ–Hþ F#e¥¥øøúÚ·%%%ñôSOáéYY.Ã\QÁpꃮ¦/; @(—Ož«=P+N’³{KߘÏm(¼7g,‘Y›H·„rQ(GþÈ#šžçóÞúM©èïöÝy¶ãÂ$ÇCfư®œ Kþ>LùÈT—ZëÝî®ïºŽ×ハF0jÔp.i"##I£&À?€ÜcG‰O¬´½ª#8~¢/ïSó´sF6ÙÙ¬Ôq<ùÚ»¼3û!.¨ÿ{•pGvöW_>ð2o.\À³ã:Ös3{ŸDRR2)íÏáÂaw1ã.X¶}ÅêŒZ=Ø™Á;‰kfÉ’™7’³Œ§& ç¶·wR"ŠHÒˆ‰ŽçŸ}{±Xê2¬V+¹ÙY„†…žò1NÛÙdg¯½å ÎKM!¹]û:dgkÂ!;Û!Þ§~ùéJÒ­]h›”BjÛT÷Ïu!(?Y‚À„—‡‚gtWZ™òÙø¿l‡ê¢9‹  ñhÝ…–žD¤¦àW±‹uûKí¥Xó6ñ{Äw‹Ã O_(=AY}ë£ ¾Ä_0†‡æ~ļ¡ìùìvÕ#p)‘ü›DµŒ& (ÍÿY«C²Z­¤ïÛƒ·þþ§|Œ¦-;k§”#;7±ñ¤òâ|oþ‘¥ïÅÔù>.Š2a0^À-#¢¹ãíGyÎë.Oì[1Ÿ÷2b¹zÊù) t¾kÛ¬æÍ©Ó »{$|2Yýö¶ dfÿHŒ¨Äž¯-åíeQ Œ.'ëd+.]©&æ#kXö$¥´ÀÇœÍÿöŸÿpül½“DÒð(ŠBj‡ÎìÚ¾•ß[K«¤ÂÂÂñôôÂ\QAaa>ÙY™øï’"dµc¸£ô(‘Hš>ÎJ6„Ë=Jvve¥%(BÁèiÂß/€ˆ‘‡¸åˆÜRz¬©‚‰¤y ( -"‰hÙàeK=#‰DÒ(ÎH"‘4 ¤3’H$éŒ$I£@:#‰DÒ(ÎH"‘4 Ü^ô(„  ?ÇO`µZÜ~R·¹a2™ "84 ƒÁµõÒÖîá–­U•œ]›ÈKß…¥´XÚÚE<|} KlG‹vç`0¹·‚×-g$„àðáƒxyz‘Ô¦ >>¾n-tjn!(--!3ã0‡ŸX¯Ý¤­ÝÃ-[«*{V-ÃÓ\L‡Øhüª=ž¤(`÷O µ>¾$§´eÏ®äç^ç1¤­5Ά­³wnÄTVDÇ–!ˆÜPs…æŒlǶºho}[Ó±rel–Š@A©îˆPÁWQèØ"’-Ù…äìÚLTê9§|<·rFùùyDÇ9~¨ÍvA8_ µ]+ºv¥4ï-úö%ùæ›I;–ˆÞ½)ÉÊâë®]I_´è_·½Û¶¶eÝ[Ï3ëÛ ÌgÄÖaµ¢ ª@µ „U Xõ€Èf{UÕöUU„m»*Në•QPÌÜŸöq¢¤â´Ë:Ý—Ö.[µª@XU„UØÛ«¨š}UUQ…¾êžgv/2r:±ZrK±{PçÞÛÞC5ðpÁR˜ÎÖÝ¥$tï@¨«³ˆå{xû¾)lèô³îìˆWñ?løm#ñÕM@VK*Û¶;ícëå]‰]µõþE‹øë¡I¾þz¼|}9°úgŠ33ñññ"09‰ÐÎði•ÀïAI7Üp:Íüw°g×￳íÂÑz§m­yTm"T0ú¶ @´¡šA‹¦@ÑÆknr0¿”A‹Óé7dýßøœ•·w&Àû_"ÕÞ  ´ö"´¡©c|†*T¡‚°jvt÷‘­÷Aël']Ëm9znûS¶Ùw?ÁÏù¶R<ŠI¡K¯KzEIþ®iVòËŒE‘L{/•PW¥vo"bb‰‰ðÅ$Nw´Ó\íéQm6 §€ÞÙ1×1̪Š+¶.;z”?&O&~ð J²°å—_é~ßý´3†Šœ6_=–|¼"cˆ8€?&O&fà@¼k²‰ÒC¬ûü#¾úy#ûó+"RºÒwÔx®êÆ¿¦g7½wn`[ U€ÕªÉT-?"T´Üˆb–((E ,¶á‰ûmÆñR†/=ÌU㮦s×ö”[¬\¿dËÆu¨¹Žæ~Ù‘Á×ÿ°¯Dƒñ‘a êKÿpýܨÚ²™{wùñäð6tÖ…ÝKò2˜öãaŠ’Úñ|÷‚\¨¶íÎPÀª[m–E÷úS¶ZÏnd$TµòxÒ©W²WÒöÞöF-£ ÂÜÇ݆ –'{ÿ_¬úf6“¾û•Ûž}ˆËb\û9EçÐåf{Ä1ôñ—ª´:}¿Á"£*vÐN\å^Û©Cqy˜VŸ­×=ô¾A&¶¯û‰«7ü‰o‹”=Êö‰÷àqN' xpüÈaŒ^øD³î¡‡è·paõãmå½Ç¦ñMF8]]Å]í£ð1ppÇ6Ž[´£!#I·9¶Ö{víeóe*ŠUE„^¸v/ëaB7ö›ë1{³…¡q‚ç;~Ò'ëDÃ?ʤßÐADF…‘“}”o#ž ªK»ªåÇyûÇ=|[äMÏv±L õÄX^ÂÿöeóÆylí•Êý ž¯laeÙ<·ê0… É<ß-€ ,µŸH'*¶Ï z´¨;wF¡ êSþ´¤°êÑâ©ãvÛQñZ" Û6[âUÿìB‡QšïÞ›KœÏ¼_`ÁË_Óñ…áĘQÎá5‹XðÑ*v3c iÃÅ×ÜÉø‹ãðVô¨¦âf\;B;Fø•¼<ïb-ቧ—³ç„ð%¦ûPn»k$ `>È’‰ðS÷˜s²~ÂõY¬Y8Ví&ß!=u*£Nã÷fm3,Î6Ò{“jv¬Wl¿{7¾a¡ÞüçÞ=߈ÊrsùßȦ&Qž™Giù1¶ÿþ'-âŽ'÷îêe‰Rv,~•o2ýü3ŒMqh’÷ês¹¾™ÌÏ1ýƒMäU^-è4ð&&^Û“0#ˆ“›YøÒ;¬Û—EA™|hyÎ n¾ó*ºÙÆÔÖ6}ñﳃEcP W<2Úzc-ÜÊ—o½ÇòßR„/1݇pó£èltªo vh[ U{ÎÃ6s¤ß›*Ú4·-Bµ¿ÕoÆZxë÷ü•ÎÐQç²iã[±›g&‘STेèÖ·/AA¾Í9JNvß}ý_Žj‰°V½¡­lݘηE¾\Ó¿-#Cô± ˆ´„Úþ²“·7à‚ðdzykk@‹ KórxnuY1­xþÜ B…Šp!ÇìðI¶a¨b®*Bhö@ „6å/TŪºäøk¢ArFUOsÕÈH©Ú›;}ÇÒkÆ&³fÁ÷¬:<„ë[)ücÏÝA»kîãéÎþ]·ˆù¯=WìÆ·1iß5v`ÂS7ÓÎð"Ú†ðî ¿£#!>X²~çÃ×—òÒâ¼~WªöcµÔY`åèªYÌýÑÊà{fmäDN>a¦ÓŠ*õÌÎöÐß»“3ªÉÖ…ééxtkOiÞ1Ú\{-eÇŽñçCIl‰º#a´²móv®|ùUÖ?9…#eENš«·­d'_¯-Ä·×=\‘ìS©ÞŒu¸œñ$Ô_plÓ,Xú*¯·YÀcç"ʳٶ-‹àá÷rg§,G7³|áç@âJ7q÷kÙWrÝ­±tÅbƾ½#âpŽ;K6Æòƒ±\3÷Fúµ4IL¸é/nµ’mÇûÐÇ×V_µÊ5Ô0¶Öf¬ú5¡EFzbNû»‚æ¤lïmc}¨ÕõŽPùîï­¤$Åãë@Jûü¶1—¸.I„Ga6[æR~^ù3k¤ÂZ=l±—e…–x[­Õ: O??b•\2òʨˆóÑÚn9ΜŸ ±DÄ0§[ aÂêRDä8ñNmÒ³ÖÂæâUÍ9 }Ì*ƒª TëÙŒ„}-†Ó´*Ôè„ЮªÐsª¶ Ïi?U8ý­,‹íG äÐ3Œûµòq9'0«ªý"SUG»¥bÉ¢oذ÷…f|”r-£Âª¢b;†ö=ÅvQ Uõ$ú’‘ô\ý3ïšÄy.gÐÀ4Ú…y¸™šTPQ}$ÚÚ<}¶ÑÕ¤[{·jÿlÅ ùkôH8‘Cà±20ñëI4ÿM®þ ð1 |M ¢U«j7¨ªç§l âškWAöúOYøÅ:vfäSfôųLmÍúùp8Oí½B@t8ÖB J,”åoã %ˆÂ1V:†…cûRJ&‹îË¢JÇlIÎ 3ªwås¨6´­õý„Þaê¦ÀžGòÖþ%Û›š¯”§/K Ù^þÜ[DëäŽxx RRÓP0QXPL~A&{¶ÿÅ¢ÁÉôˆ­ÞATª›^¡÷±oŽŽKñ¤K„ÊG³y}“ÚøâçÊ]“3ÂæŒlC4´{Ú çÒh?nènîfÎÈv¢ª}|]›%š/ IDATCÒ·«6cVuFV ÷0ÒÂLö"ôÒyø²¨J4øF੪zç¤ß0¶sPº“÷ŸZÀ/‘C¸uÒ$ø›Ù÷ù‹¼uÐvc©•_Å9"úpÿÜìüå[¾úòM¦}õ—NžÁø.Õ–¬Ž>»åÖ;›ÁyÅ´ójaW¦@]±µÛ¶¨ÿl¥âdå>¹+C ô`U…`Ðko0f4%99ä¼>ka _#>mÛVsFJ@,&Ø¿3“’‹ƒ©i¢Ò|è ž{õk ýnæ¾[“ ™üðê\~×í¨V±+€j0b@ÅjUQ­ú´¹“ãÒ[ªµOiÃø·’ê|pÅ“ Jeg)n¡Ál­ªÚlšÐ"/ûL™-¥çŒ÷ªmÈBËWæmÍ-Ÿïáï=hסž!M&ŠK÷³sóï,œÂùq~PÇbA“§'-°ûh eq~ÕÎMʼn"2„ðPµûúÓ'Úˆ0Eжäï;Šwd41ÑŽWË`PÁäíæbJÌVG9…éì=áA§ÃHKmE||+Zµð±÷òÚ‚5ŸmÑ•ÕQL¡´¿øZ}éyÆDg³ò«íY+×·æ—¡Vos%{è7Ÿf7aïÝÂÖ—>ó4y¦`|B=±d–`Œðâ» •^S§Ñ«{æÍcË ~˜²3!Ä“l`.}æéjíÀ'…K»ùPôëǬ:TVc[Ë2wp„$†ŽìKçÖqÄ'¶&ÆÏÉ®ºÃ¬ºxN;õ*á)D Ù¾%‡ŠJeNŒÅ[d±¯4˜h§óÓ2ƒ#ºV Ö3`kmIŠEó;¶ïÚ¯Ûb?U_O㈠„ª]Gu½Þ–L*eÜ¿†ˆ¨<½+øsÝ7,КÞñõ~_5x1°¥‘¢Œ£|[`±;vÕª¢šËX½½€|£—‡´…‡öÑ´nÃó]ý9¶÷3vSTϱì‹…j!´vÛÚk³­ÝqÛ?[Ïndd»àTá\iG TêÁ[² (ÉÜÅ–-Ùˆ²ä¤ofíë9`êÊMwö'R±b%œ^ÃÎå«×óÜk¥ ï•D°¡˜£™e´é±žàL¨øžÏ>ù S·@Šs-$÷ˆ!ÞÏÌŸË–±†nÄø©Î-VÕŠ=¬V¬V+@ü”2ö®ÛÀþØž\ßEQ$¶ðE-Üžà“àb>Â÷3§òÁñ~L›1š¨ô÷yìù ÄMx†û{ñÙtžøÎ›±OOf`”“9õÙáÔ£jÓÑ Â ­]qe=†+¶ö £ÏÓO±úž»híÈ7s‘Ÿ‘‚Yϲ}Þ‹„UBóKÈ ò`Gž™Kæ<…gHÖj¹ _:]3Ž;ßàƒÇ¦²p?ºµÃ[-âè?{ÉŽÌUqI„óß|¾ ÿ^ ò8R U³«Ía¨V{ùZ´$PU+ª†÷ åÙO_`¶e$}Ú„b,É£¤eOÎo3„Ë¢7ðÕ¬—ð=€.-}0d‘Ôƒ~C1*ÞûBþ–ߨ”ɹÑúl_CÙZh³ivG£:¯•s¤Åm÷švl-4ªœµª™÷G¶áêOv°võûd:Àký’¹¤u Ó«.:µ¤~®=HzR0ç›0T”ó÷ÁÖ7гk Î÷²­KÓêª-‡¨ØH¦•[ylgóüx0Öäúz1=W$ìÑŸÝ¢hy30€¢h‘åÙÚWU=rÕnUckª9Va{[z“™?i‡h™D§Ënçæç“ègÐO²àž·3Íú‹—ý ~/AàAPë>Üva1 ¦ø+¸íÊü¹â^Z ¾qsW¸vâ(Ê}Ë/|£2èNáxá4 Ö#nº2fX*ó¾^Ì=;2&cÿ÷årËÅè.øûšvx‰<§v9ÝÑr]ª^ ê”ÓQlá<Šý gg{Ø¢~×#£úmÝi̬B°rê´8A´§B OÅL>pØßÄnKýç°EûζǗö×Må€Å|üÃÛ¼ò¥¯HzŽK¥Gl"یϒ¥|·d.ß[@ñ¢ëðŽôí$P \8fÞú‘Eß÷ ã¸$¼ÔÖzÏ®Úò2NSäz„*´‰$ôÑ™Ðr)®$à£QíxoãQRÛ¦p~œ¿½“vƒ‡·^KÛ}¬8œËì}#1¡þÜt~(B UÕfhí¥ªö‰¹„ÄHî)<Ä+›ðcp4}k®tÕ”‘Ї¢ŠÐæð Nó”BÂ_E² «žN8uNùeVýøƒ‡§¢¢¡ªŽ‡«ôØ€ýAOûÚ$±-ÓÚZé;}VƒÁˆÁ àáéÉŠå_ÒïÒu–|ª¶.ÍÏgÅÔ'ÈݱƒâÌL F#~-[Ö¾=ƒg<‰oXXC5ú_âÌÙú»YO0,5µà¨6샡¨N‘pZV l.y£ÿ Žœ‘Ý/Ùòb±]ÑBÚäF F#†àH–í8ÄÀûfœÒÝúEY¨Ú[Ûº@[øêœPv·j¡›"Bu ì)hm6œiÕ†Y7#£úmí¨ysjµµø¯wgÐÖZnH›wµÛÑ‘S‡*ögµôåÙ ÝÒ§½MPM½Ýös Nmkì‘åY¦¡´^‘#ªœ¸rÊéÿoJçË[ÛTϦÙgt„À Ø¦>SÓB¸¸RUÚºgÒÖBQ‹þ´€sžH_D`‹ðm@TÚ§éâü´ö*Åž®°YÁöp±°œåE&O ñõóÅQ#­ZÚE¡ê+Vµ“¦ 0îÆ›\*ûý…ï¹S¥ûC±}ÈàtsèðŠ>=ª8fBšÀ¾§§W½ÇpÇÖ¶ºê¬†+çèß:?gÃÖžÞ¾ä—Vd¥`_´Q鳊í(NÕ3ËØï¸¼ïÒ‰§u,‡©û-JTmÞnÛõ¨Ïä—•ãíëïÖqÝrF11±ìÚ¹žç]€Y8ÆÍŠÑ¨…ËZüŠÁh°÷Z,v,csUsã¢6ýeÇÚ?Uú&¶oÛJ\BB½ÇpÇÖUëä*‹½ïò¾g›³aëØŽç²k÷_\ê…ZVª Qìa~k*Žã*BØÑ™N@|<(©þÇÓ.NëèÙl§Imoƒ§ÛŽ•N/·Žç–3JNiÃúßÖ±þ×µ´ïЉ°êK3_±÷ÔöÓWýòBPX˜Ï_[·`±Xh”Ro¹ÒÖÕ9S¶N¹àb~MßÍ/¹ÇéêG˜§ §»­Z-šÇÍ!bÛB»«B¥ Ü妆 *|ƒIîÙǽc¹3›Z²oß¾=ü'ÒÒR·ÞÜPo[µ&))ÅåŸs‘¶>uܵµªªìûãgüõ%'ŸáZ6 ƒŸ€ Ϲ€äóúTÒ#w•¿[á¾3’H$’†âÇïVÈŸ·–H$éŒ$I£@:#‰DÒ(ÎH"‘4 ¤3’H$éŒ$I£@:#‰DÒ(ÎH"‘4 ¤3’H$éŒ$I£@:#‰DÒ(0ö\ˆD"‘ü›ü?[5˜€¥%ÄÙIEND®B`‚libindi/libs/indibase/alignment/controlpanel4.png0000664000175000017500000024541413263645557021463 0ustar jasemjasem‰PNG  IHDRf’¹¡ÈEsBIT|dˆtEXtSoftwaregnome-screenshotï¿> IDATxœìÝw|eþÀñÏÌlKïB„Þ« RðgÑC°œ½÷zöÞõ<õlw*Ø‘¢ˆ9ªé5@(!¤—-³3óûc7! ›JË÷íkͲ;ó´y¦<û<󌒾{—åñxðx¼x½Ün7=ôB!„B!^ƺ, EE±ìª›ÛíÆíñà.)á¾ûî ¥IãÀJí®ÝbJ­¬e”•®z­Z‡Wí‚JݪÅÂJˆwõ ¯úªø¶n×vƒ„W~Åc׬wX!VVŽ/´²^°•º¬Yû©î)©zCg‰Q>QÇV¹Pkq¨uXUs%Y«Uk^¨þÙ¬¡†çŽuÌêÇ^È”6de)e•ý¯âGÇ^åGˆVÈ·UGXËðj·V-–ªSxµß:Îò¯¶Õ0 Ѫð¯ +ä· ^ÝÂ>¾,V³/ç¶°Bï\õ +Ô‡õ0tx5.Qû°ê÷Eõ±Ö:Iµ<ŠÕ¡ø¬ï&¼*¾i°*/T‡}© ü50LÛ¢-9tJ‚î¿›¸¸XÜOmƒB!„BQ¥í.ºÂ¾µ™¨Ø°­Øv„É/?Gll VCþÌ$„B!„¢J±±1Ðö­ÉBß7êd§G!„B!þºP ìoÅÄD×k}˲0-Ë Œ®k›eY(ŠR¶^à¯HY𯼤t9%ĽG!ñ,PT4MEQTU­2!„B!„ø­åç`êÕ 2-Ë0Ñu¿a`VYƒGÓlš†Ýf 9‰CiJð¯e™èºi˜¦‰iY¨ÁF”ªiØí(jàðJ*Ë Þn™µdžªihj ‘&³ßÝ£ãó›haa¸´“žêü‘Ò*„B!þlu]Á²,LÃB÷ëx=^Îk›B¿¦ñ´Š"92ŒÌ"7éy…¬ØŸÃôp¹œØl¶c ØÃå×ýx}>ZµjEÓ¦)$$$KnnÙÙGØ¿ÿééé8lv[ gŒ£=h(ðü~?^¯””Ú¶mCl\Q‘‘‘——ÏÎ;ÉÈÈÀép€Ãަ®¦¥qV–‰aX ih R|&º×A“nýèÔ"Ь%óXSlÿ6xþHiB!„Ê ý­¨¨ÈZ-lY¦iâóé$9Tž?½3)‘N,ÓÓÄ2MUUEQ5y¹oÑF²¼v‡£,ŒÒ¿>]Çn·3dð`âãc1 Ã01M?ªjCÓCsròøiáBt]Ça·Wê1³ÐuMS8pÉÉ0ÍÐá9’ÍâÅKðé:.‡ͦսafçRl€E|¸†Rî3[x$q¶²žMoG ü 8ˆMÇII~!Ez¹^JEAÓl8]NÂ]6T¥š¸ªI—§ÄƒÛëG7a+š ‡ÓId¸½P€á%¯ØNB“xlÅ9”Xêñ‡méä§r÷{wÒËVÀ÷ÜˤÃ.¢ì•¶B—ÍfÇæ"Ü©ž™µë•V!„B!j©°°¨nCMËD×ý4ujüsH'Â,=¯°Êå“.þ}Fgnúq‡ ?6[ ÇË4-ü†ÓáäÌ3G ÙŠŠŠ÷©™¥iñ¡¨ ‘aŒ<ë,æÎ‡ßбÙì¨j°ÇÍo igž9‡ÓNqq ¬Ò| '*:’3Ï<“9sæâóë8MSk_b–AqQç¿ú:6‚_üƒ{æä¡ ÁÏ,öÎxŽG¾ÊÀeo1¾®÷òå­iP²ŒGoú/»HdØO1±Se,z‡ölcõ¢ïùæ§ø¢Âq*®B¢ÂC4> /ù¾FœrÞ9ŒЙ¶Ia(˜gîbíÏsùbö&ÜŽhœ™:ùFùèZ:É”»á»â0··(Ôcm,«âãE,wq$Cþñ|›U‡¶i¨r:æ3=¯½±Oóå>pTÜ/þÁ_À K¢Ë°¿qÛ„žÄ´>›;Æ®åöO3B% Ò¶²ð;é}ûÕÁFÙa~0‰oVï%×ï"©U{Ú([ÉwÚpbPRh§í¨«¹tToRc4ôœü3 ¼hÑ)\pç%l¾ç#¶×¤IŒ«„sÞ㕦ÏóøQÄBËÏ>dWåȬJi0u¼Q}8·§€ôɯðïD†Û°ã&{çj2ÑpÙ|E©âááñeaÙãÛ0äòiý8|s¿C—>©¤ØË‡×tàŒOeðw¡¸wv™úXõÂ< ýàË*ÁT-¼F"§ïN[€Ž×oÇ¥æS ;H»¬ú¸­Ê]QV¥z¢ìµ°œî,^vˆq©)•@DT" v…ù&®˜u ÷ùØs×TÒkÊßn§nÑzƒücD২À 2!aW?@œ÷^þÅsl¨œV!„B!ê(x™]óU¥‚‚i4w)˜Å%X¦ @—F±ÜsÎ`^œùcZÅ2-=ŸÛFŸFW¥³0Ó4h†iÙMƒÈˆ|>o öJWµ•ŸKõ ÃODxx`’À2 """ðù|Âp»ÝÂ)eþà_ƒð`8µÉ{]ìÿîM~è|3ïzw\»‰‡WÔ´†ŠŠËå!}Õ^Œ3:£E6¡±Ëbç1#EKŸóü—i &¶¦‡Yõkö0Ñ™ 6ÕØÀ¦oÌiL¸YùïGyeQ>É£îæåËÒhöÒý»×YYÖÑXÀ÷ÞÃ;»b9û‰ç¹25‚Þ§7Gݲ ³¬a–É×?Ä— TÍIt¤†â)·þ#÷ðÎ6Íå@‹=Ûjwù¼VÏôê(Íz1zHJàƒütþ÷æ[|i؉Œ‰""¾wG £-€ûk7ã ¯ˆbÐbIMqáÍ(Àksja ESk·…a„“”àÄ¿G›Z!¬òJ'ÿð+ šMî¨$Œn˜leï¦ÎZL¶³#½Ç E…IKª£bdnã ÝhF$a™ËøjîŠM{t"±V!›Š¢×”V!„B!ê¦ö³2š&»ÅÙÅtN  çÖáèè;Œž•M›¨Xî>뜇wcùK+kñã°‡£¢b³Ù8r$‹ˆˆ0 Ã@QLTÕDÓÔcfE^6›Ã‡c×l!à <Û –-[2~¿ŽiZeáØlö:A«iê£Ù\;¾øóûßðØÐ¡¥Œ{šÏÇUü¬dü<ý6§†ß[yÊC/Ž\¾û+N}j i¶†]}î.·ˆ{ÏÜò›ó–óÉÿÇÃCcé{ã |veÝHgOcWE­Ô¶8¶hlþ Vì4éÙÎAï;^cr‰ŽÝ½˜‡îúŒÝ•Ÿ…¥h8jŒ[Ãne³= ú6µÑã¶§¹ýùø×6ƒp[Õƒ;U—‹Xg Ñ–ßGîŽtŠhBdëËxé¥ÓØ_F«jeÇæOÅvd Ÿ-=‹{DÑíòÇøèr?ºeîøXñìí¼ºÃB3jJ«B!„uS롌~ÃÂî°³ @ct´N3EÅôûH 3 ä@Ù=eFÞÚ8LÌ?ªÝÁê"‹…E‘‘4›ÍANN. ‰‰8ö@/–ª Z°avô¾Ã4°Ì@oXNny¹¹„‡G‚rl8~âGîhšÖ8gš^´·UQ(.)!ûH6aᘆYu¦C9#_Èå4\æV>ýï*N½³¥³¬+ŠN^fþF±G'8ô擱s3˚˷K30ÃØ0ÑCÅU92Õ†-sßµŸ³ÇŽdH¯64‰Ð ÷‘=lZ½™b›†¦øÙúá“<›;KÏìA‹(#ËfMáƒ9hN_è§ ”£`w²äÍ·i~ý%Œè+ÜNQ¦‰] <çîØô*h®âvÙ°ÙÌ{ãcš_wZéäè(hò[ílˆšeÛ§<÷±ÆUçô¦uJkÚx 8°{#{=U g¬”?›ËËÚwçÙ—pñ°î¤ÆÙ±+E™»9dÚÐ,?{ÍiB!„¢.”Aû[Gíf¬i6LÃ$V/äñäb¢-½lÚüPT›ÅÎ}‡¢ð8#°šlšºÏ‡aš´iÓª¬‡¬üà+<$ZU1 ‹;w Ùl8ìNTU) Ç´,RS[£¨ž½òáâ3ÏFS°víÚ(¨šR›ƒLZQP5V{¼W׆ÓxÖVéP<Ó LÀaøÏ" ¯ò9f%%%¿Ž¡û÷”Lšª¡Ùm8lvU¥4¨ÊSê—…cøt?~ݾ˜ÉQUUl6 »ÃŽi˜¦4Ê„B!„¿~¿QûÉ?*Ó}~Å@Ól„……•›l£vJ{¼TUÁRTœªË^úì3«,¬ÒÉ@Ê߇VU8¦©áth8íö²ŒQ&† º¯üýpu†™B!„Bœµž.?”ÀsÆ|èUÏÿ!„B!„¢µÀ´B!„Bˆ¢ÞC…B!„B4ŒãÊ(„B!„âøÙ¾ûnîÉN‡B!„Bü%}êÉN„B!„BüÕIÃL!„B!N2i˜ !„B!ÄI& 3!„B!„8ɤa&„B!„'™4Ì„B!„â$“†™B!„BœdÒ0B!„Bˆ“LfB!„Bq’ÙêºBnÎ2öí¥¸¨Ó4ODš„ 22Šæ-[Ÿp²“"„B!Ä U§†YnÎvlÛJ—î½HHLBUU”•2ñ—f˜&ÙG²Ø°îÚ¶kO\|âÉN’B!„'Lf{ÓÓéÒ½I’±,+ð:Q)iŠ¢Ô(™ÎÝz²sûfi˜ !„Bˆ?µ:5ÌŠ‹‹HHHÄ’!Œâ7`Y‰‰I¬_³êd'E!„BˆªÎ÷˜©š&÷–‰ßŒ¢ªRß„B!ÄŸ^fXVà%Äo@Qä.F!„BñçW熙Èeâ·"5M!„BüÔ½Ç äjY!„B!P=fV½z̬ÂõLúç7¨ãîeb;gÝ£=‰ôƒ ùbV}ÇŸO»pZ÷Û’_„B!ÄŸŸZ×Jo1«ëËôdý/›9PbÖ;Œ†x™Þ#l]µŠmùµO‡oß¾þn%‡ô“—î?ËKÏÝÅ/Ë7’í¯ý:B!„BüÙÕ¹aÔÿªüxÖm —ÿl^yæmfÖ¥•uœù®ñeRôË+\6f 7~²½Ò÷¾“¹mÜÆŒ ¾.šÈõ÷½Àä…»q›e¼›ßàŠ1×òáN/XfÖ<î3†ë?ØŠ'D|%Þæê1cxhIÖoVþ~r~~“§^ý–}^i• !„BQê·Êxœë7”£q×>¥Ë°”Y,™ºŒ’pų¿dí9wÒ'êèIÓ—O¾Aÿkïäÿš«x ްsÅL¾xý^6ä¾ÂSç5­”+ Áò#³>`þ¨'¬Ï€¹|O.y¤ƒH4~ õ+}!„B!þìê1]>Õ\+›l˜Æ¿ßŸÁÊý% „‘rÚ-Ê»·uÁøv~ÀÍ÷¯äôÿÉ„Vv „_^¸–g²Æóæs£‰Kÿ”GŸ™Éö?NÓ>çpÍÒ%J ¦=—Ù÷_ÊlZrå›Ïsv# «h³>ø€é?ï ÏPˆh1”[»ŽŽl磻.矹n°'ÐiØDn¼l íÁ<éY,ÿâ]>ž»–ƒn•¨V§2æšk8»]Š‘ËªOÿÍgK6±'Ç DÐý†—xph"àKŸÃôÉŒ}x ;Ÿ{“©K²è9²ÑÑÆ’à"©M:¦:èѧñ‡®ç͹‹Ø?êo4¶Ê-k¿0“BÂh¾¯¾ÚÌ뻸=΢pí¦¥G’ì,¢8«Ã"—‘Ã/_¾Ç¤Ù«È(†ˆf}9ûòkÓ# ÐÓ?æÖ{ÑçÙ7¸ª­#ÖÏpåk÷}ô}ÃuöÏyƒÎ\ϾÃEè€Ó†Ó.¾«‡·ÄUÚÖô­àɉÞ'žË‹¯O¤ui9†ªoB!„BüÉ5ìtùžÍLzùs¶tºœnì@”/›C¾–„+V…^'ÿ‘ÅüëÉÏÈxÏ^Ô–HS£WÂgü¸bîþ ¼ûa—?™3»´¥ËöpfíØH¦Þ™vƒÜ­É!‹ » 0ZÅ£ú°v§—Fƒ;§Y¨‰½9ÿúÎDdž¡\ÆçoOáåÉxëÆŽÁÆN$ny [ÚAqŸ béÌxú&gvá‚ëî£käç(4+mÄÒíüËØÔEÉÎøè³×x1¡%Ïß›U¦áÅEIœí£ôK(dÍÔóá3ïÒäÛèåÌgÛ²5j2Ž;nèH´Y‚Õ2 Ë*fãÌ)è|#:õ¥ûàšù={‡§•ýh™SVzÁ)6œ*øÝø,«Ò2&¾‚||öŒ½ÂÎÞþ”…cž`d# üûùáÓÿ¡ž~yÿdáÇÂn¹Ùúñ#<ó- šx'W´„= >fòÓâyæy&¦¹Žé•+ŸžÀ;ƒ¼ëØåíÆ•÷ ¥©­ˆôÅŸóñ»ÏÞú5®lk äFëÄ5_I{(ŽXšØªî=“v™B!„ø+hØéòõB²K ºM7º¦5ÇNÒ‚Ë›Áu̼5ü÷Ý7ÙÐî:ž½¦q* ¦pJŸ8f.[Ê>OgÚ9ýÙ¸…Âè.ôhIÓþí°/Yöü±´ˆ/fǪ`‡Œ»qŸëÈF~͉¦[¯ÆØ- ºýúÓÔ¶ö ?ñðÖÍdëid؉nÒ‚–-eI÷lžÆ×;¢9ó‰;ß!¬B¶J,€$z<…^Ñ tMEY¿š×–n"ÿœ¦ÄþÂ?äÒù†§?0H½6‡·|Æüzu „Þ¢'}º·¡,V ÌÜ_øf…Eï»{«¸ˆ9‚”y³ùvËÜÜ%¬\y[>Ýcà)Èdûòi|°Þ æŒ~4µ—Û&`™g—`…5#¹÷HF'ÝËôY»8㊶›¿afF 﫦8ѳòðšà*Xó3I¹øn<§v {Ç<»îfú”µœ÷à)„Uê•«PÊ•JÏ^Ýi¢A÷QìZý«We2¡MÓÀ2ZÉÍ[Ò²|1K L!„Bü…5ì=f{N+žøìnnYw#GbxŸD¨¥½l^V½ù„ŸÆ?nB²½4$-‡œBüÜe,Ú;‘´¶%l[“Ixç+iáWûA´ÕÞaÙö"†vßͲ½‰Œ¼8…%3—³ÇÛƒÆ[Vp ¢W´p`aâÞ³€O&}ËŠíÉÓí„)^ˆ÷¢[åÓ^þ½Ÿìm;)¶§Ò§¥ë˜ü•ï²'I)Q°>‡"Ã"âðöyo\Ǹ7*pŽ3T^ö-›#ðXÇpÀBkr:£Ó¾dò¬_¹¬s_¢”Ò²ËaöÃW‡_D:䮿¢#.,¼•z®Üù%àŒÂålÆ™uåÛ÷¾dõyWá™ú3þ¾·3¤±CQNØ“K‰iá:¼‰½F,ýº%b+M£½Ý:E3õ—MdêýiyLÙY•ò¢oÏO‹8ø5§Cî1B!„"¤úõ˜UE‰ ã¥ÏðÞ U|?c:_¿t_u½‚gïETZêEɢżýa_ž¹¾?qÁ©-†pZÒl~ZÎ¥ ø_ºŽç¦â”èNœ–jðÑâíäÄ®d«­+wèÊ¡¯>bžÚ-Ù£ËRÃ÷&=õ‹õ÷œJ«(íS_àÝ=Õ'ÝªÓ € ªMË vfYX„ÑïæG¸$ÕQa9g|4 …¡ƒÑ°hÎnŒÞòvÁ IDATÜÝ<<ñûŠß©³XÛ›!ñ¥gFqêõ·3ºyÎð’’ˆ°UõL5w¾œ8T•¸¾cüñc|>i2¾-qŒx®;Q äF:±¼xÌÚf[CÁ@7êPVŠŠ¦‚iJK!„BˆªÔ£aVÓ–5"[öç‚[ú1´ÿ+ÜúÒ,d g|€äÓnäªþ“xðÅWy&ñ)ž×–0°7gب–Ìøj6«;úÙ¢tææØÔ8zMåýIs™ïÚ‡ÑñFšÇ7eP«B¦|?Ÿômvºß™F8`ìb{¾n7ap×0@Çlì‚`ÃL±‡aÇK¡·|kÄF\›¸ô­¬Úã¦G¥¡Œ5q$µ#Eùžô} No†£òzèõ|{dþÁ8†Ýq#ÊÍšhoà?O}ÂìåYœ6*¹4â[µ£}ê1¡‡`â-ô€#‡¸Úröÿ5gÞä娮㬖vÀÀîO!:Ò\›ÇÆõYèi¡Œè‡Y¿©[‹Ž4²ƒ™H$d(Ájבּ&KÅn½·ß¢æºD-—B!„â­~Ï1«ŠžÁÂodÕ¦ìܾ‘µá!œ¸ðrÑ(*q}¯âÁ‰iìÿêe&­/ VÓHx.ÝôÿñöVAç¡tˆ,½(W‰ë9‚ö¾µ|¹ØC—!mScé:´59 ¾f£­ÃÛqZtKZFèl˜6Eë·±s×NÒxË¢·Å·¥ux +?ŸÆ’uëY¹ðGÖe„w¸€³›å2ï…—˜üý ÖýºŽå ³µ æî$%¶7c‡Æsø›çyù³Y¹~ë–ÿȬï·S\eG‘‡ó““xgöI#55µìÕ¦ËiŒì ²{ÞÏò×c;X~JJ °fh4>ã Æ >ƒ¿M<•@ÁnO ÇL‰îɸ‘ÉìŸòÿž¹œõë—1ã­˜z°1£Æõ J5¶+ƒÛ(lüS~Zźõëøe[^¦Ù¼= Öf¦~±€5ëV³øûådVÑpB!„⯢Î=fJ5VÉa¶-þ”ù“ 1{B;†^-Ãi]~IÍGßÊUkîà7?eÀ«×Ò5BA‰ëÃØ!q<<ÏÇ Q)÷(/Ô¸ÞŒîádóÖÞ O ôhÅvAm ¤}xpÁ°N\q÷Åxþó-o<õM ÍΚöh„KÂ:3ᆑd½ÿ ¯?=%:óîH÷„VŒ}ôQÂ>œÌÌÿ¾ÌLì ݸ¤Í)4¯±P"èú÷'¸7öC>Ÿ÷>/N3@‹¢Å€+è?4ˆPeU¼•yË I>f•§ŠWbé:¼Ú¿~ä§Œs[SüÇî¥ÄZ´ -X†Jd'ÆÞÔ©ÜB*öpø‹)Ö- £ý„G¹ßõ&õ KJ ¢Y.zàj.Lsú­´dFÜu'Gޚķo½È×€–@óåƒÐì­.à¦1{ykÆ;<;Â[ çöSú‘l]±ª«oB!„BüY(ƒö·¾ûnn­^ºø'†<ŸÏw‚’crdþ£Üþu¿rmœ'(ñ‡áp8øaÎ,œ6äd'E!„Bˆbôè³NÄ=fugîe×aóð2>ž´Ÿ®×ÞIki” @î1B!„ ;+c½Xmø”§^]ƒÛ•B¿‹à†q |ó›B!„Bü~5è=fãÆ]òó/¾˜RUˆÄœz?Z×”ˆ¿¹ÇL!„Bü4øPƪ`BÔ‡´Ì„B!ÄŸŸŒB!„Bˆ“LÕ´ZÎs.„B!„¢ÁišV÷†™Üó#~KRß„B!ÄŸ]½frÏømI}B!„nš¦a³ÉPFñ°tñO'; B!„BÔ(2"’VmÛ‘ˆ®ûjµŽMÓ°Õ}(£‚"½â7âõx8ëìóNv2„B!„¨QaAÙG²Ø²q=:w%::¦Vëiš†M³ýž1-Tvv{v¨Ó4OvrNŠÈÈ(ZµI#!!ñd'E!„BüEØl6âö¤ïÚA·½kµžf³Õ½ÇLü¾egg±uÓºtïEBbªªþåú7 Ó$ûHÖýBûN]HHH:ÙIB!„Š¢àpØ‰ŠŠ¦¤¸¸ÖëÉ=fB»wl§K÷^$5JƲ¬Àëd'ê7¦( I’éÜ­';·o‘†™B!„ø )¨ªZ§‘kõºÇ¬TAA>‡dàñx°¬¿Ú¥ÿïËF“¦Í(.."!!ë/:„±”eY$&&±~Í*©« ¬´®R®"²]E(R/„¨Yé~RÛ{‰„E«oì  Ÿ½é»hÞ¢1±q¨ª*³šŸD¦a’Ÿ—ËÞô]¨šö—½·¬<%øK…ÔÕ†SZ×öìÞ‰¢(R®²]E(R/„¨Yùk°­R¥q&ê­Þ ³ƒû3hÖ¼%1qqøu]~=;ÉE!&.Ó Ü[…e^qJðéÔRWNi]³ïÏ qJS)×? Ù®"©BÔ¬ü5ØÁýÒ0õVï{Ì<7Ñ1±øu]zf~,˯ëÄÄÄþ üõî,;Vi H]m8¥uÍëóJ¹þ‰Èv¡H½¢få¯ÁöíI?ÙÉ`¶ã™._ÕTü~½“$êË4Ml6ûѤ]VFêjÃ*½0“rýs‘í*B‘z!DÍJ¯Á¤7Yã›.?8ãŸø=±ÊþJ”•‡ÔÕCÊõÏI¶«Eê…5ýCŸzßc®[¹éÄóìšÉûÓ vÃx:†U¿léIó·Ü4–ïÛMGIëEZ´Zö¹žñÿ¸÷[o~•{Äž”ûÄ­²vYˆq„eŸ}ÊÚ&qõ°&üÕÍ.Ç€?«pS?ü Ç9×r~+GÍËËv!üÑêE]ë}HFëçÎagÊHÎí‹<\HTG~¸ÇKÓ4Ôú>ÇÌú_úþ™<|õͼ¶"ó$Ä_»—IqÆFVnÈÂ× áYø¯cÙÚt ÌÚ­stYÕ¼L|WðåkÿàÆ c3f c/»™GÞü†uG|5¬[ñåß?›Wžy›…™z…ÏU{ì ÔAßN&Ý}·ýw %'½îÕÿeúrؾv ; ÍZ¯Ó åZÇ—žŸÎºÕ[È1Ê}n±æß·ò÷¿"]?ùeÚàù;Ž—Q²Õ+%£¤æíÛ ÛµÒ~QŸú%¯÷ªOýjzq‚ÓXùP—z_åËÈbå¬Ùü¼§äw|ÝQùÕÐ×!G_fÉ.f¿ù×_~9.»œ__Mu²ó[C¾ãó´Ç㸞c†Eµ¿˜9 xìŽ(:ëažßW¥•K¶LâþgÐèÆñþQ5÷¨8biÜ´ ±QZý•0‹Ø¹h:Ó¾_Á¦Œü(D$§Ñcð…ŒÕ(µæ ªçfÓ¤ø§ÿ:Þè”HôqwYÁö…E`I‹]¯ÊC„…{ûT{h*;cº1jì tLvP²=?ΘÄK×ð÷ç`tS{ë“ÂqªsûóƒYê·e•ý©u11ëÑû™’Ù›^ºƒþ1å6bi$ð,ÅEB“¦4ICý?È[?ø=o¾²”ž¾@jd~Y®E¹–¬y‰›^ÛM¿;Ÿçúî‘•ös½_?ÂCßhüí…Ç•\›c‘Aîò÷yñ‹$îýW{bK,–JxbRüq8©Å¾Ro¹›~`ÚŒ¬ÜzˆblQ)´ï=œKþv-]Ç»ÓW‘¿ãp´'½–CÑj±]üÌúø ¾ÿeù~ÀO«ÎC˜pÕ9´sVÜ/ê]¿*iðsÊ_ÒqÔ¯šê…UÀòWàÍuÅÁœqÍHëÚ¡£G뉫–Û¤ži<æPÇz2ÌàY$FåPþøÖjwâÝöîxz#ƒŽñµêQô±ãËòɯÍsãx:Å‚ÏÑŒ°z¼­‹*òý[ž§å þÈŽk(#e¿…fdRdÏû„Å#dDR¹xü‡øñ“äÙŘDÖ8D@KÈ þ«µßÈeåûOò¯¥4és&nM‚ÍË‘=›ÙV`¢) ±GYåþ6dx•ßWŬ9z_:_¿6•‰£yìù+èQzôȃ»ñê]¯ñÁóéýäH’5sßâ_3×±;³?àjÔ‰Ó.¸œ‰ÃR WJãÉeöý—2€–\ùæóŒRçsß “hôð{ÜÓ- °p§Ïç£÷¿dáÖlt{_ÄÕ— ¥U˜Æ~~ï5>_•ÎÁ|/ Ù¢?ç_sçuœðŒì•|úÖ‡ÌY/*‘­GrÿWÒ¡ªwYT_WËóìøŽ9ûl„+뙾ð½ÏM 1d1XÀ¶&Œºó1FUŒì¬®õ¶¦r5):œA1ÿûd.gwºåÚûfÎ2>uˆ#»ÄÔõ—‘rq+.ÒÆÞÇ•?oP~-ø}¸³Õ©œué¹´ˆU)ÎÜÅæô",µ¡ã>ù¨íq¤šåŒÃüðêK|™Ñ’3Æ^Mׯ.ôÜýìȈ ÒÉ1ûÅÑé"Žï¸ØÐçQ×mQC½°ü”äCÓs¸ý²N¸t7yw°ú§oxýþŸtÓƒ\Õ/¾ŽÛ¥i¬t 0êNÈõBÕÝ}|«¯Ú^‡Ô±¯ÇÈf˦x›‡r4^»w0 f{~ÝJNóK¸óæ6¸<XùÕ$>~N£Å›·Ò3¼o¿Ê7‡qÝ÷ÑÚå&+K£‘½º¾Á£åR«_ô¬|~ùf¾^Wsk“¯x~Þl¶ ¿êè½}e¿ž·‡_>ð‹»?‹ZãÀ³)_~ü ‹6gá1MhÝûBn¸´qÔ\|éÓyýýÅì8˜ƒÛ\)ô9Œ–YËX´z;Y•èÖƒ¸øº j¼ ðaõ´IL™ÿ+™•¨}9çò‰œÙ&ûJ lòøþñëø€æ\úâcœ™Tý…DÍåê§èp!8ˆÊœÇÔ_†qG¿˜à¯Ê^vÎúšMöF„ùóÈ.2ËÂ2 k±Oø~á…ë® ¬0’'ž;χwñÌîѼðäh’5ÀÈeÝŒÉLù~ ûK "¥'gþí2Îé¸WÄ*ÚÀgo~Êò݇ÈóX€‹ä®Ã¹ôª è{lÞ¬E¼;i=JŸëxö†Sˆ/;tö§¬oزpï]Ä瓾aÉŽ\üöxÒžÇe—œF —µÙ!ów1)™óxÿÃùlØ}˜"´äa<øäÚÚ«?n•ßFµÙjÜ®î}¬N·H>÷j&Œlüá¢7§”Æ¡WÜ/”jê—Q°‰Y“>cΪ Š £I3™på¹t‰9¶üúœRóö¯~}«xó?ÿŒYÿÛAŽWMÛžÊÄÆÒ!BËËþŸ¿à£i‹Ùš­£Å¶aИ+¹tPSœJ°.ÌÙHÆ‘@]°'uaÄÈN¸W/bÅæC[á4ëw>×ü}8­JOlÇ»¿WQ¿ZÖ0H¢ÆzQú]D3ÒÚu RºôäÔ3³ðõGøï»ÿ¡C›»8-^,JvÍçãÉß²lw>†#‘NC/檱}H´UÆú&>|úMí/ÁD%ªE?λê2†· CÁËÖ÷ŽCÔûšê›å;ÈÒ)ñÕ¢­dë±-[c+[0ŒŠÙ?Ç7“#?<Æ]_$qÏë·ÐÅàaã·óÂá‹xùÑa$Öâ<ú:¤c…^æ£÷`^Çœ{ìñ´<Ž«/îO#;`ê¸uÈÿñi®þ1°nã Ÿæés› ×pÌ;âúçé‰3Þ*¿ÿEÐràhNOgÉ’µìÎձŷgø„ë×+.pÌ.©nûW‘ïk"ù¶ÂyºæsLeQ³ÇW!ª£Ùêù3 tQ•Ž"~ vo$ÝÛ‰ ·œF­˜=ÿûš/>zðÏpij¥ny+õs×â‰Î%§7Á2Ý…k?àɯ$nÈ߸½ožm?ðÉçÏò¢ù$j‚Í,&cÓòš^ÈMW·Æå=È/3¦ðÅ«Í_¸–nÁ#w³ ¹ÿšîD* E6ÂiùÉݼ’ÍøÛÍgÐ*ÌÛÑ—?ß½ôSs»qÁ5cHsfÅ´Ïxóé"œOÿî‘Tj™Õb›„z_îC÷=ä“Lç–ÛI¨ß®-‘,cÇaš/Ž¢RéÙ«M4èѳM=·òìW3Ø1ô:ÚXv¢›´ e‹£å^v€²Kgïœ/Y§ôåž»'Ð/JºÐ6ü 7¾<•¹û0>%°xxóîôîÞÝéœt˜_î_ÈÒ=>z¶ó’—¯£%´£{§4’4h“V]^+•E-®HÌ¥ÌÚÅà‡zÐ>ÊMڜϘµ~,úEN¶V¹LUh)Þ9Kyë‰÷ÙØx—ÜÔ‹W Ûf¼Ïô ”˜½‰£¦ú£`älcC†ÊЫï OœIæÊ©|4}2›:æ’kÎ!ޟΓ¿æýw[Òág¨¸Ùòés¼¾4ÿ»ü^zűþ›øôåI4~á:ºGÔb_±'Å~×ÞɹÍl :‰‹Sj.³ËÕ 8·¥ÙX.k1‹·¾šOF hn3w%SÓuü„ñÙ^,ˉâ?PÃ>Ü´ö\~ÿxÒœ ØcHÖ,v•¥ËÓÃö)ÏòÊ<8åâß ö.ù‚)/?‡÷áǸ8Õ‰å9̖͇ˆ>ûþÞ!öFæ~ö-¯¿“ÌKw$®BÛÀÏáÿ}ϳ)—ŒíC¼z4²óÆS²£ÕH.¿½ 1¿òí'ðtŽÆ³· ^­i{ت̟ÿðZ–ï0|ÅmôO¶áóEÑD3kþl*“ãzsá˜UÂæY“øöíh÷ò­ôŽTP5lÿù>|´ì, jÜJÍeQå>PÕBÔNð³úÎ-WÝ}L––BRaŒHxŒYßïæ´ñ­1¶ÇœƒÍ}s'’¦9Ðäã±,œ „5ïFïæR[D²oé#ü´é0¾>‘ØÊuS—õE¶¤k÷Î$kй]$éë_fÍÚLÆ¥6«¸ãèÙìʲКw$ÙQEªÍ,–N[FqÚå<6ñtT S[âòàÙY³Ø>ô*:¯?šv¢{×Ö8èDûÄ#¬{üg–ï÷Òµ]0lW)Í›•Ûí ¤7º5=zt¤t¨¹gófí‰fèƒ×q^šèLû ûî™Ì×ËÏ¥ó°„`n•jK»ü6©~Û”ÿ<ô2¿µŽ]S‰¤Ý€6h?ogGžŸÔ*«UÄžMYÐ|i‘G¿‹lׇf¬bÓ¾b̔ҸÆl‹kI<%䔸±´xúÊ·/¾Ã­w,gĨQŒÚÆÎêîZ¨©<Êó±çÇØßx(75·£ª½ÕísþõÝr²ú '©Â0µP¥£³ÿÇé¬3»pýí8%V¼ØWº˜ž_qùªë3¸L,iÝ:Ó1J¡ck…õK_ãP§A 알F’r–±ö«µdx‡¯¯cÚÂ<:üýŒ9%ðkmË+rY}ïWü´ÛM·.jûJ`²œB³æŽryªI åjù(,ð¡F$Ñqô9¤.ùš™›Gp}7ûæÏ`kÔéÓ˜å3aÏ‘"üDáßþm ûD| F-œ¤fÍhVö°·Ü‘ÁÂ,\Ç´ùY4¾àI®ŽÚ¹C2ÞôGùvÚzFÝÙ‡,ÀNRÇîtëì:Ð̳{§.c—{½"*Ö¬]ÙÙŸÔXµŠ|ëìûq”žÜzËXzE*@Z‡gr÷Ó™¿¿/c›[5l”*òW:$0޽ºÒ1*XïÍ,æÕpÜJãhµ»Ã¢†å-8ÿæ‹8üÆT^ºc -{ŸÆÐáC9µ}Ž »ãÑpBÕ¯’-ß2'£ cž»„Ó“5 5—_Çš·±¹pÊßßyÂÎ)ÕoÿêÖ7wÎbæ®pÝ}3ã:‡£n=mnQ çÆ–N_‰Ùï®9» á ´k~ûV?Áÿþ—ÁEmš–Õ….]ç…6;YòÔR ¢Oª¬føV-çíõ;)8/…ØÂãÝß«®_ǽ¿—ÛÚ•—ÔÒhî„ÍéÙèDsà‡™l‰Á£W §µh›Âå[WóÂÒ_È92pþ•F-–Ž}bƒÿnMÊØÕ,~}éÅçÓ,¦4ÖÐg/÷öêëÛ©Ö:f,- ÙE÷sõ¨äÀËöQìZô+[Cäé„ß*ÄwôoùíT÷ëŠé¯\>¥çžv=ºÒ9JέQ·ÜÅÛ+¶?²1ñÁ¥mQhÚ¼YpԑΞ©µ<æUºþ1³K÷¿®tîè‚)”¬\ÎûJ/† èN´iQéüüÄrÖôÑ+Í5nÿùö[ªß‰5—E•ƒI¤e&ަÏs̨î6O¾œ‘¸) =¿#s>šÉºQðL_¿çu jdçp¤öåã6 Fó‘¹ò>ýv9ÛäâѰûÀÞÖ_áÇ/¬r¯òÿ¶ÅÑ<6å–`XTl˜•þh«(U§Ù“ɦƒ|N1e}â.Zôh…í§tvåtH¬/`mNnr‹ÍcÓT) ¿ó“»c%öVôlê,û^‹nG·F0kS&úЄêÃ^‡ÊÓoy|«îú‡:\‡”WÝ><Ž$6‰„My”fǤ§žÇ¼ñ»øöî;<Šâàøûzzï !BKUzèÒDš‚¨`Á ?l(|mŠJWŠ U¤‰"J‘^¥‰Ò{ IH¹äîv\’PCàózž<ÊÝ–Ù™ÙÙýÌìÎyyšàÌRm঳Ÿ.¤sÕl?®BËÿ†Ç© | Ï‹ûƒoê„â:·5ù‡¢pƒ™Žl¤%™Áè„ îÕ:Ð`ÞHÌ›‡å°;M?ŒÄ NFÔô$Ì6˹å|=ñw´zñÒ3eñà<«&LbygCRs=Õ§fÏV¨A«UÉgö$;¥<Ávþ±–*”Êo0çôê9ÖÏžÙ)g:Ôk˨-T”\³6哿ëÒ›£?,猋y¶‘µ~QfeT²ß;-xÉö(}W³lÎi<ÿa IDATD?M%çkËÚ®lgÞâÓÞ›úzòÌÌœÞu ›seÜuh¬ŽHçjú ^zÕ8ZÑþÜÎáäqµßA^=´ƒ3øÒ&Ô ‰7>¸LzuHƒæò¿W'³t[Oê·ñ+à…rû±Ý¸®¨$ÿ÷Û“­dLÿ?úNÏýí_kOѦGŒyË7W}P°)€V‡&çrY{PsôJÞ¨þdÞ1^«Z :PlJö: ”ìºîHÍçÙßñ˦Åäé ª¥ðsåg/+4_mi$šÁèl©Ð¶~þÂÜyÎìÕÕæ­G<Ðrg£JúU³=ŠpN\<ùçuÖ§9Ï£kODçmS2ÿ¥Íê È›zkù\éq£Jãrhg¬aáÖF¼RÏûúy½/`ïÞƒ$´ò˪6sr×I¬N•)ã¦AQ”Ì\ÉÞ‡š=ã†=]z“R’1Û\²/8ÊõéE‹{X(Ž–ƒì:Fd9“}É+Ù{ ›ù¢W22׳Úl(Ê'.¸Ö,ýƒÿ,•éýr rfÐÙ5Ì_äBÝpÒüÁÌ?~â1"@ïUŽ2N©lÿqºDbJ¸Œ1ª1UríÔ@hëÎD¯üŽq_ΡÏãUqOØÅâi; úË´ 1>™‘šÊ‘?×rÖ§,ÁnZ’â²¢#ÐÍTè\W7®«€’Àî•ÿ` ’ÿë^žk¿ãmãâê |·e‡;ö¥’ÞÞ «6û“.g}PMø…¸ÂÎ=œHy„(' 9Ë]U «?ʵ \2ë9ùÔ9û¡Ø·©ó*C€f-'ϨxÕ ï¤ÇŠ’OÝËs®htÈ 9ÍV´:TÔ|µš¹šF( Z¿ú´«ø S6'øxÊ™TU½HâåœPÑ™ô`I%5Æ’ýì\îãÔyG¤[Ëý±d„e¾`½È¾WÑ—Ã[—·MQ2IÍql9F‹_­ú.[²eû¨õL\¯;Šð‚¿vqäjª»dv@ÙÅ9¼ihD-´< :>®¯P´v+óQZE)ZùZ®ùp*]•²«øïÔ2‡\uV›oýÒâ„I=ȱ47š„9åéNR³ËÂîn_S®/ÿ¯¯Á-ÄCÆIö3S9sºñ¬sSQ½7~°çX,ÆnÑøå}OI¿®.d¥ EÍLSžz}Ûç{Áõ«0…·£YmUÎë`¹Èúy«¯Hßjž Úó€õGIpnG÷$Oq©×þ'ßkYO«|ýºå6kuýÚù—·üÈ|W43…•?ùw®ü*Ú}WzayQÀip³í¦yÝæï˜)¨j•PÍ ÍlƒTUƒoý®t<³}“xhì½z'ûEÔ¬(}ËàÍŸü¾l.uJᦽÂE3ØOÖûRíÛËÛ+‰š5„Ÿ¹|ž$yÖéE×­Ÿñã÷ÿcøÁV4Š Æ]oáê¥Nˆà‰ÎU¨Ó¡6¿ŒŸÃ˜9V:ÖðÁ|d óÖ%Ö¹5e ª5»%ËÞG®ÿªF|+øÃO+Y´Ö‹ú>ĦR·®ÏõéLåZÓ&t;?Oœ„[צ”3Ų}É|޹ÔåõG<Ѩ*:'WLdë¶#DÄ”ÅCƒ ifinx­ÕàTá)>þ¦ Kæ-eí¼q,7ƒÆÑŸ tçÝní¨î“gÚ!£ŽØ³5?©þÄÛô2Ì~árŒ¤×K­‰²„1Ÿ.FãAÇwPÅ#÷&t¾Mø‘ÂŒ© ™<|)Vƒ7õç“gá­£ðÀLIáì¿2mãYRü‰j÷ýkç}3Ç‘f}q£º (ñ»XuXCùç¡|¨[®í4©…禬>x• Q&<œàÊþ­ì;çKußœõAOh³Æ­ÿ…É“ýéÖ¼A†ëÏÍìtd×O%ûlG½ç{ÁõË·°×Í ;ß³¾K>Åq°™I¼pŒ]ë×°ë‚õúõ¦Ž'¨ªŽRÍZRî¯ù|?f& ­ª⬒|éÚÈÆÔòÕçŸÆzá„èY¹c9++¶ ’#–³‰X³Ï×ÜÇ~]½/¬¾¹Dòx3_†/ÿ†q¶ÇiZÉCê.˜íùÝ}Å]ißÊÔ#Ò8…åÓàÒ: _C‡.g\KƒZØu¤ û`L9‹+W{’ϵ'û¢\ðõ t„ÜJ›Çµº~­^å®ë×:÷BÊ_—ÿq×"Wº ¿ï*B^8 )™¸=·7]þzÏ{ƒ¥u1 Q3{bËÑ®w9ÀÞ ¡z“¬©¤¤ÛзæÅ§˜ùë\Fÿeß®ÁÙ‡p?ûð±ríDQmî‘ ×÷Ö]w´~4ð>.cÅÆß™±ÑŒ =‚)_3”t›ŠwTOÞyÞ…Y‹2î¯ ´®¥¨ÕåMº7óC›«§*«éúž*Ÿ½xòð4–ÎÀŒøÕêNd-ïëÓ    åko¢ýñ'VLÃÕˆo¥Æ¼Ð¿#‘NöÞMcÙv<^õ ?/ø™ýÕÞ¢^ÞÞÅ^CHSžî}m}½ƒâí£®ºÂÏ ]p+žm{–é+g2z-87¤_õŠs§‘2ÞâUÓ\üò[ÓÀ)0šÇt§m˜!3s¶)™#&¹zJó‘°Ç2,èw–ü±…¥ÓÖ’h¼­T—*é6ð¬Ë‹ÿgcþÜ_™9v%V½áu{òv—Úxõ8ÚËÃïñES6oÝÈä\H»¥:GÒ©]&­ø‘¥Õ£x%Ê1ïÝT¹*V-nÆKl\¶…eÉ6@‹“_9uïËÕ]Pm‰¹ëyõ+„ÇÞz‡ù‹øsþwüi£Ñí+Ò°’kî÷„ïú5åúò/l}ÕX†.oöA3çW~´ƒT;!Á`?á3GÕjôáݾKùñ—•|¿= naõy¶nt××…ìQÂì2Èì©Ï:Wµ·¾T¿¼oó|W5˜\áä &~³£Gá•ÛòÒ‹M©`B“Ùh|3àÿ´,X°’%“7˜¼"hÚ€ÞÚÎñhª?ý<­f,à×ï¾f€Î÷àj¸éÔëέëë}aõÍH™NyËy WÏeÜ + ÇÅ7œš&{™ä<Þ»Ô¾áRÞ¯´gÆœ?™9Á>/½É-€ ø`Poç>$CŽÞެEµ kJæˆUÖ=Õõûn­Íƒ|οÌåÔ#fÙ#È ŠêRHùpÜÕBr§»÷]…æ…Œ˜‰»D§×¡ûÍjïg_(Ò ›Ö¯¥U»ŽlÝ´žðˆòd¤§ßå$Š›a4™Ø¹m -Z·'#ãN”M:ÿ‰¡GgtÙýËh4ñçŠ_¨Y»nñÔU5…=Þgbj7F ªK!qN‰rúÔIi@R®·Cáʆ/xg¾¯ŽìŸùT©BÎh2qôð!ªV¯UøÂâ¡–šÂ¶-©Ó¤HËÿ0mò­O—+ï!ˆ»KÊ#÷ª®*IY¿ó nþÞ¸­ÄÞÀÏÿ¨DôŒÀ9«—ÿ"mÀƒIʵˆ¬ñìÙ°³§?^.zÒ/ýËŸ?ŸÂ±JkB ÷ª—TR/„¸19?ÄíÒéôwkòQ²ÊãFï˜uíúT‘·7þ<*½ò=óo7aÅD“ýÈĽ©«–¤Sì[·ŠC—R°FÒTëô2k»_ÿÌ@Ú€“”kY®pìïÕl8Oª pð&¢f7^¼2NÒ#ÄCGÎq»nsòSÊæcÀkoÜÔæÆùæÖÒ!®É.‚#³ùóçøÝƒ'3 ©«P´úZXÕ6ã¥!Íòùææ¦¥.1Н’;QGJ„‡¬\oÕ€AyÊÚÇá?òùƤ.ä$õâ’³={àêjq‘óCܦÛü³÷žùæ«›Þž¸=’‡ù+JOoQê«äon[úÃRG¶r½U…Õ‡-¥^˜¤\E~¤^qcr&n—ý¦okº|©„÷“¢¼cö0ÉýŽ™ÔÕ;MòõÁ$å*ò#õBˆ“3q»ôúÛ1SUU*á}åZo¦Dfvö|ºzwH¾>˜¤\E~¤^qc2¢,nW棌7?]~BR2 I‡îB’„¸ó”ºz7Hð`’rù‘z!DÑüµzeq'A#gÂÊ•ÇÛÛç¦×ÕéôÜÚ˜Ù2hÕ®ã-­*î Œš‘n6K=B!„wÝÕ¤$â.Çr`ÿ^*FVÁÉÑ馷qkY¦ëVßÎêB!„BñÀ¨À‰cG¨}ÓëÞV`]­æínB!„B!J´½»wâêêFjJÊ-­¯½ÃéB!„Bˆ‡’V«½åY:%0B!„Bˆb&™B!„B3 Ì„B!„¢˜I`&„B!„ÅL3!„B!„(f˜ !„B!D1“ÀL!„B!Š™fB!„BQÌ$0B!„Bˆb&™B!„B3 Ì„B!„¢˜I`&„B!„ÅL3!„B!„(f˜ !„B!D1“ÀL!„B!Š™fB!„BQÌ$0B!„Bˆb&™B!„B3}q'@!tªªbµZ°Ùl ª¨ªZÜIBˆûžF£A¯7 7Š;)Bܘ !Ä]¤ª*–Œtâb/sòäqRR’Q¥¸“%„÷=Ê”-‡_@ Z­<ä%|˜ !Ä]d³Y‰‹åÀÁÿˆªR/o¹ÁBˆB(ŠB|ÜeöïÛƒÑdÂ×ÏßþÔ0 Ì„â.²Z,œ8qŒ¨*Õ([.¢¸“#„%†‡§ŽNNìß»ÿ€ ÌÄO3q_éѳWq'Aˆ[2gö¬|?WU•ääd¼¼½ïqŠ„¢äóbë¦  )î”q÷I`&î;Ýà q¿*J‡‚<¾(„7O«ÕÊ{¹â¡!w B!„BQÌ$0BñP3œÎ;¯dëÕö3J»æã»?Îa)î´!„¸m˜ !„x¨Ùbw±i×q’Šy^Kü!¶®ßÍekW°Å±}ÁóœPâùc@cš¾±6{tÈrr=cZóÑß© ÄòçÐn´Ì]jÖéE¾Xqšôœ»O_ÏÀV™#P]&p8 ƒsk'0°{+ûç-žà­9dzƒ¤Ë+Þ§s³bbÓ®÷æìËÙN($ÿ·Oûu¤Iæq¿>î/.d®¬&ïeú{Ïódë¬|éÇŒãY£]ñlüáOÒcÞçÓžAœ˜?›})ùä­{YªDW¥Z­z4ï<€áŸuÂ;þæïIÎga+‰çÁ€û™Ÿ˜´!ç•™gOæoc0Îy3%qs‡õ屯1ÄÄh›/çnÏN~Ûƒ¦™å3௫ù¤_!î™._ˆÀ»^z†æÓ±ã);üÿháý©«1R·ó‹ÔópÇ`>ɺYÓøaŒ/•F¦\µ‰t ÈL‚¾ǾAGÐ= 1($nù’—>X‰{›—ùèÍpœÒ.‘à‹Ž‹˴╞ÕñVϱqú&þвó‡R×UƒõÜRÞ{u `øk•Ñ]Æ×_á÷YL~º4º”c¬[çîCø¢®/ÚT+ööÅzîwfïr§ýĆTóL¥Ê£™³åEª5÷ºa¯®ÎÁ#6ÒÓm\?Èo#96MÙy+b&Ã&ÿÌñ†Ïn%v “–]¥ökƒqø ̨8¡±œ`Î[¯ñÝåº<7äE¢ϰzÊX†¾œ„ãÌw¨ç^„ `>Ç®¿OáÕëÞ®îŽõâvæÉû—bÞ×mðÍ\, Ëp>iˆ NÎEذBÜ9˜ Qh¨Ðõ-zyŸÆüJù¡ðÏ»ŒÎ‹¨Ú^™ÿ'¤Ûþõ'Rºê‘ù±k¢«F ƒrÎGX?t'åbS'ÜÑ¡dlÛÄøÝG¸úDŽ—ðëé ºŽzš¦: œ€§wóʸ5ü›CŒ‡üÚ§(9lÉI•z5kQ¥¢3*åYB%yß÷ ºßþÖ.àÝŠÊÚÏùcg<Úû¢M=¦ãZʽVw¿YX²ñæ†UОÙÂþ4$ïfÇ QaY"PÉU˜©Û„Ì=V ŒcåêoØrÒL§û‡:W‚ʆðó,›ü Qƒ™ôöcøër$ÙbÌÜ*4¢YÃJ˜€j~gØðüo¬>–NݪŽ,œÁ.·.Lü$•L@Tíù‹7~_Ïùî¥3Óâ@ð#1Ô­á”cãéY´€ã¡ù¸œ ®=êŽåý9«¹Ð¤ A9Ó¡XÉÈHÇlI&öØN–ŒŸÇymežªìІ+y²9ƒÄ+ftnAÔì՛ʿMfæÎ® ­çÀÑEÓÙíÑïš„²z†Êá W±â…eßLfòäññÃèíÔ¦z9-ÇžÅÔ?Ÿ¥vg×"Ö5Pï' eS·ÑíÛ?9˜ÒßÌ»!“OiÂÃKÉÍ‘¢XHÛ#D ¡1Ñr@_ö žÄè…•ù°fîï•äC¬˜õ#ì>Á¥« &G6J“aËÿÅ4½«?®¤—šù ‘Æ„§·#œ¼JºbÅ|ì4iœcÎÀÞÌɵf±)6ðæC”{ðtµ Lðÿ¶|‚.]:Ѭ’gŽ‹à~Ƽ·Í#0¿[Ž™ýZïÚt¨¢eøo;ˆkÛד›ø7=ŒîÕ½ÐÜiXß—6qÆR ‡ÛH®ú=ã§±awÏı~¯™²=kÚG˜­—Ù:k4S–ïäè…dpvÄ‚•7š¿Ç|š' ¤w4^º,—IïŽÉ\N¶’Ήý±;Ÿ~-æç^ÐãI7š[#e? ¿Bå[¬p§f·æ¸XÈŠ“é[ÖpmÙ>æ‰_KC@žùüm:ë¹îÙk%•øptÄð(}›Nåíip®B 3ÅRåå®”wNg‡#¤Ä§¢`!vÿA’iw5êÁì¿O“Þ¹rás}NáQ:ƒ5Žø4ŠÛ !Ä]$wVB” :ïôë¿›Á£Çó“k¹k_¨Iìün³V KßÁTr eß FÌ)xz7Þ€›’¸iÐu (¨*¨ª šú {žHÇœkp÷/¢÷‡pzŒ^Dí¿0oö,>î7ƒy½'2ö¹¬‘³@š4wdͪoøâ—Ê|Ò!€Ö‹Új¢ÿh [.7%rËv‚[QÇ_è)Ó¬.ž Ö±ét{<×\ ´cSšŸ]ÆÂU{¹Té([Jѱ®:lœ_öƒ§^¤é«ïñzõ@ —Wóñà™…$\AQASÄjV{ŒŠª¥û2ú£¦xåxþP£w#ÀTÐVTÿžÏÚD æi<"÷·¿,=B7*áõAÙç9¨îF'Ü| òr,øQG%•„4pp3¡Ñ¸Pýé.÷žÁ¸ îlÑ·àËf~è´±¸9¨˜ÒŠ8C¢­V6‹ý¸‹šW:=ºÌüBˆû¼%"D‰¢Åã‘Þ¼ØÀÌê¹›IÊúØÇÁ“é¸×}‚vu+R&4Œra^n´©ÒãY&õûpÑ3?æQ_çhºµ÷ç¹iŸsÚH¯f¥®uz8DЮ©ËÎDSnøé0y·¤‘óf,ƒr¨0à «—1cQY:TñÅxõI€§}縖ÂWYÀ´©Ë15ôäêy+‘mchÕ§13‡ŽbàgWéÝ¢ž$pÑR‰–õ ;j]zReÉh>ôq=ÈÁ(| IDATî¦pæ º:h”×íÒz~Þ£¡êЖT«{¢°NÍñýíîN¢z¡ûχb&É F'£}`KëC£—_¤ýOI4íjÏW'GÊÕ$ÒðŽ~†žåW3éƒaxèLdz¬ž2†ýî­ÙÂʵªû¯Kùü›Pžo†SÆ!Nä7)dAŒAT Ó³xùæGv&B½D¼O ­¢Ü¥[qÏH`&D ¤q«ÎÓ}j°kÜ©ÌÜ©ýÂÚOšÍ’1Ÿ1@çˆGè#xnqtËFç!ïá8s.¿ÍÍ hœ¨Þ¹ M%0%Œ5îk~˜Ç˜Ë@gDc^ö 化sA}­ÞÂÖ^ïòŨµÔø¬9ÞZ៦ʼ/ØWîiZ–ÊÐ8P¾ãc„ü<§öìA˜¾,­›ú²pA]še^hõwÆû§?gÒ·ï²Ò`ÀÅ·"µ<ìg“1â†ô>ÆgsF0x8—íÀÿšÄP§é¾µNæ›i3øhe*h\(Ûn( ëøR}ÈŒœ gü¸ÙLö fÀÑ¿OVìP@`fåüšÅü§fXuë‚SÙ–4õý‰E?oçJè"æ~ª™äŒk#få»òöû9Òbt1Á¥ÒÀP†ž_Aÿõhf2©ŠAµž`Ø'ý¨çnߊsµ×ùâ5 #ËËUÀˆ{p ˺-°ÒzÑdà ¶8Žï†lƒ/µûEÑB3!Ä=¤Y8o¶úhëvEZxÓúµ´jבߗ/¡U»Žl\·šèj5 _Qˆ"êѳsfÏ*îdqSnToÓRSذn--ZµÅÍÝ#ßeD ¶—Q=qºÿl¾jí+7ëBÜC?ÏŸCÛ°dÜ곪BÜ{wï¤rd4Û¶l¤vÝlÛ²‘ú1MŠ´î+–ˈ™B‘/ÕÌ…ÇH"™}óGò«s&4‘ L!ÄÝ!™B‘Ë)–|ÚŸY'µxEuâý/Ÿ¥‚Cá« !„·B3qßéѳWq'Aˆ;F£Ñ ÕjQ™ë­Ä1–§ÿ¬õô/îtñ“¶S Ì„â.²Ù¬ÄÅÆrààDU©Š—·Ü`!D!E!>î2û÷íÁh2áëç/¿)x˜ !Ä]dµX8qâQUªQ¶\Dq'G!J O/œØ¿wþA˜‰ÞC˜íݽ³¸“ rˆ®V3ßÏ{ôìuS"Ä1gö¬|?WU•ääd¼¼½ïqŠ„¢äóbë¦  )î”q÷=4 ˆ{«° ¹ \!îWEéPÇ…âæiµZy/W<4îûÀÌjµréâ%®\¹BšÙ €£ƒ^žøûù¡×ß÷‡ „B!„7t_wá&&&²wï>Ξ;GjZªªAU5¤¦¥qîì9öîÝGbbbq'S!DI`»ÌÖ¹˜¾é2wìMÛ%V}9÷<ŽåNmóQS±dÂX~>–QÜIBÁ}˜%&&rèÐal6½ÁO?|ý‚ðõ ÂÃÓ½Á›Máðá#$Gp–q„Þz–צüGÚ½ß{Y¹¼mŸ¼Þ—={ÑãÙ!,8UÒn„0ïç›nÍè2r)·º Ëþš3—•‡RPˉ™<ÿèc¼¿&ž[~PJ¹ÊÑmÛø÷‚ùÖ·q'(©œÙ³-ÇRŠœ5ù_–Ì]ÊÎ8ë]MÚÃ@M¿ÄþÍù/A¹Bܺûò9@«ÕÊÑ£Ç F“nn¸¸8áèè@Zš½Þ@RRé©9vœè*Q7÷X£šÈæQƒ»+ë¯ÁäYŠòÑuiѾ%µ‚nüž©ÆŸ `‚|œÐÝâqÞ §¼ÃÛ¿…ñÞøW‰r(| %~#ÆÿNR£gÜ «‚§ÿ}Y䢨”xVøSŽ_ÿU`—‘ŒètžÔBÜ.3¡a”p¹cu\ãàC©°ÒøxJþ¼éùþíw9ôô¦—u¾oz]•ؼùÔ§ü]î æMèLPÎÂKû›Oz¼Îï—³>ÐãR™:­ºÑ·[CB4˜÷§Ë+Ûi5e.¯V0åSžîÍNý™õ]/JçùÝa%~-CºÀŽÚ_³øãZ8ݣ㴜ZȰÁ¿Ó`Ò<*y˜îÑ^…šûòîÒÅKö)Q5zqttÀÙÙ 77Àþ"¨Í¦`±8“‘‘ŽÍjåÒ¥K}'ª´„~œÏFâ`IåʹÃìX½ˆ¯ÿo51¯~@¿ºÞ]†`Ú ú„v·}´wOÆ™¿9f-KßÎM©æQâo;DNÁølå7Zü|îQ'ÅÀF·‘ßÓínRІ¡“ÛÜÁ-ŠÜ,œXú+®hÿ›Å¼íx3g¯¢b&>ü:|Àû-ýQS¯pöŸ•Ìšú}÷¿Ç¬ÏÛà–w“ÖÎ$g`Âú¶ oæ•#5s`ö8Ö§q d¨à$—>!D r_f‰‰‰h4tz#:N›9+ Ø3N‹N§C¯7b³ÚHJLº¹À,‹K)*T¬„‹ˆ®IÃæMYóõûLþv2‘åÓÈKáâúL^¶“Ãg±u1²¿Ë¿Çºñu7•ÙoeSÔûŒ}©j2ÛGàëägó¿føØbÙ¾psÿØË³×ÒuèØ·mÊ9£Ášÿ>T³§+_έü–ñ¿îçLl2@ç^†_ O³P4 XÓ°p”o_yšoôÕüí ª9dpaËO|?5ÿ\JGçFÝÇzÓ»MÄ ö'î+.!×êm¶‹ðéÛ3Héð)?ŠQ5shÖ»üoKeÞñ<Ñ.l‰ûY6}¿n;M2NVoMïç;í‘ÖÙØ»t:³Wìät²ŠÎ-œ6o½Kˆ" Ó q·XŽ2©g~«?‰ߨ„zð{†|¶œN^"Åý¨úØË¼ûrs‚öUTóIVNü‚É¿ìáb†ŸˆŠè!kE¹¸˜ç»Œ'ð«%|úÈÇV¬—60iøí8-n>þ5¢ó\E•«»ßÿ –ú¼Ê”O+³°WVÕË‚÷«á \`ñ O2ÆãC†:bÈ¿ø~ÎËD”í|Ðé-Îöÿ‰ÉСpù·Wxb„‘a‹¿¡©þo¾|y¿OFA‹{Dsž}g O”wÎñ;ùmš~kÿÿªŸüʸƮ`cÛ¬¯ÿÓŽ%)è=+óägßð¢/€™-Ÿu§ÅåxÒq"¤Ng^{ç9êùè²2‘+&ðåÔå치Þ;’Ö/¼ÍëmËà ±pö·o9{ÿœŒ'ðn6’Ãêá¦5e7³]$êµQ4]ñ“ØLŸMñÌ3œçItÕ`ô@Íz ¨êð,½&-`cl+ò†Íª9Ž‹)à›&-àhÃ~ö¼l—VñíâX<ý \¹z‘«6ðÐj:gVObÔ¤¥ì8gFçYžf=ßä'£pÓi»ø¨ÓköÑÆž¥ÐÖ3séÛ}cæƒjzN.ü„çíàØù$ûõÖ«"-û¾Ã›ÂqÌn‡ãXد çµùSy2PºË„Ew_fVEA§Ó£ÑØ[;EQ°X,¤¥Ùÿm±X²§Nµp¬wêG þ4ìц¥o/â·í—iÐÊ+û·òoRez½Ñœ0'+fC59Þk3S'Ò‰?ìá‚¥a ýÛŽX ~¬žšTþ›ý _¯÷¥ÃsïSËû*{NfÖˆi~ý Õ]lìãF UH:öÇÓ£xæÍÆéR8¹é'æ~? §°/x&<óJE(O y‘ê΀Î?•¤]S6n žÍŸf`m?Ìgæì¡|·íƒ0Üh·â¾¦óoJÿ[<ã[~«=ŒVæELü=ƒÆƒ»SÅE§X<|K´Íxöç¶bù”9ŒšàÃWï4Â[cæÈüOøü+µŸ|‰î®X¯$áì+µBÜ_l±{ØvLKÇwFã«ÿÏR&LÆû¾å˜Ü³4%‘m£^å“ßiùÒ0-kàÒîeL=ÌÍ·qjGeîÙGüU"S¸pQos¼R¤¦Ÿ`áÁ,Ð=ÅèOŸ ´«™Æ5œY´{ g2ªe_GØtʾZ…*®ЭÙÍD…_-çvp â·#µs®¤q|ó1Ô²ý©ìªA£†Òì¹!4÷öĘz„_Ç|É7Rmæ „g¦! Ëp>iˆ NΠ¦±ÒÎµÒø…!¼éŽ%.× #Y³•6zŽ~õÑÇídÞ˜™¼ÿq)æ}Ý_­BüºÏxù³=D÷ûq5]¹°j#>„CØl^„˯áï„ øäqÊ»XH5UÀY ¿q6ëh§-«RÁ¯!Sߟͪ èt£`E‡ÉÅX0[¯_Ë–K²Æ‹˜þ=9ùÙ¦lz’áM<ÑbæàSÙåÝ…ÿ›¡óìz…„M#yiØj|;¾Æð&A¤î]ÀØq¯0H™Áøî¥‹Pl$ØÎAs-^ÿ¬=¡ú$ެœÌÄ/ãR~6¯UÊê´r£Ù_Ú¯¿>¾” !nÎ}˜ 4hHÏP°Z­ddXÐjµX­öà+ë3«ÕŠÍ¦`2ê1îÜ¡|*PÚûÅaÅÝþ¡[85jDÕÎZrN8b"¬A$w²ëR‚õ¤ŸÞÂ?©þ4«îƒ6i; W'P¹ßÿxª¾ ÌóWØþæ<Ö3S=Z›ÿ>ŠÂµ ÕªG ƒèŠ®ßõ9ÿ}‘îá¥²Óæ\ŠP·Ì(Ïv‰µ 6’\þ9>éÓ-Uï„|´l)GZ¼H%¹ÿK¿^c¯ýÛ¡N滇zü›õ£Çæ·™9f2Ç2¶’Þh=ª¸ Ò.á×ÓAtõ4Mt@8Oïæ•qkø7)††Ú=,Xq .ŸóÊã!¤‹ûœÑ êQÛCµ*¡ÛÕ‰VïâJ÷Òø\ÙÌŒ•W(ûÒxÞín¡ª~ÙÂî›Ýb&.>_Ô¨L€*Fe}—ù_ëþøìÆiÆÇ“úSÍU 8R¾eMÿ\ÇÆóÏQÚ€ùäVf„ðxUo<›Rޱ¬?˜L{_gâölç’F‡r`'Íõ‰ÒœbÓ?©„m9§ô¢ÔÕL[–DÍݨj:ɹjôøcêJ’ª fÊ[á¯jUÁ?®;fÍbÿãïS­¨õÀ½"uëÕ&Dµ£Ý9¸ù-6n<ÇK•2óž¥Ê.ï˜ !nÍ}˜¹8;“ž‘AJj"èt:Tt:{÷žÍ¦‘‘AFF6›''wLFc![½9ªz3KkpŠhJU‡‘lÞu™Á^œÝ²‹Dÿ&Ô 0qòçl6& çÄÜk®˜QïÔëÉ/B=àŸ+©ÏÊ•qÎA@§ xd?Nâ@h2èWçÈ•äÑ‹û_hWÞëWç¬ë]̺ÐûӼߓü5h.Ûjòf·*™ÄùDg+VÊ‘nóOzú¹]±xÑè‘À"œÓ:›Bݱ/SÃÝ~«®q‰âñw^ÿm gŸlÃîugð¨û6a&ê6 aÂâU«Ãª£>4y£n›g°hÛ9Zë×pÚ«!MC $°~ø[ŒÝ[•ç¢^i'’·}Í  yC—N÷xtÊèOd \Ø}k?·bæÔßDZ:—!ÜSFËJ:[ìz&ýp€2}>ჶ®ìœò=›¯Ø=žeBpPÏsÔìIP®ºè‡‹^c¯«ÚDöï»Tâ~,WˆœŒÕÓdz}óiÒïÐ6 ^‘´zá¦Ïù?¢Ö0wÝ¥ìªv ï§߽CýËóüÁ"®ýd¤3‘]Úàz)Kvlæ£n4haŸ$ !MZ¿žEË–°ß¥õB¨Ù,ˆ³¿-céòãx7nEY`½È¾Ãf¼š?K·fU©Aå ~×âg˜sÿžš10ŠRÚxþÞ~þæÏiSÑÁûïyœB »öêëPà „š¼—…«¯Púé‘Lš2…)Y“FУT lãJŽD:U$2ª2Ë•ºaP æÄ4­#N hÝyäéÇð<ôÛ¬ èÓÒþs!“ FÒHLSÀBP8½y/ñÙûLåȆƒX\+PÉG:WÜàò¡ó˜o1Ž×1b¶ïS!nÑ}9b¦Õjñôp'5ÍL… áœ9sžÄīٓ¨ªŠ‡‡ÁÁ8;: ÕÞbŒyõ$ÿþ㌃5„ó‡Ù±f%;ιój?xëà&F³LaÍiêÿ'‹¾¿ˆ¡ÊËÔΜÕJã^'šxòÙ²/ùFÛ…æ•|1˜c9“Bãfå2_”¾Gt~4x²‹GMgÔt]jûa>¸’™«¯Þ½2ñ^ ‘|Šÿö;çê±Ö:ø^FǶi³8Pê)¾hV?Ë‹´Ùþ?¦ý°“È×jãQ©#­ƒ·³xô(œ»¶¦j Ö+çˆs¯MóhOtî5èÜÔ‹OŒdŒÒ…&å½Ð¥Æ“X›º¡…ü¶Ÿ÷­g=^èÂK3òžÒN5ƒ0&ïåTê-lLMá¿¥Ë9éWÒ:’þÃEEOˆ§ ×6hnË‘§éÿÒ7¼?½2“ž¯„£Láé1ŸñŸCqmIÿò×ZCH3Ú–þžIÓâðïö¥z´ [8ñ;~ÄŸnï†ÛÔûR©´ykfðS§¨èLú±¸kÁ–1ˆ*az/ŸÂüÈÎD¨—ˆ÷‰¡Udú>æËëSò¡òíªø¢O¾DJhšÖŸ©¡UŸfàOÒèÓ²2ÞÚ«œ=žFôm)“ï«T*‰;²1¥ýÚÕ¦RpÎÎ>+NíË0ç»…lŠkJû›~Š_Áœd£ÆÌÆÈÑ•×z\a{`/j»Û?Ôš\0‘FBš ô´|¾%3ÞùŠw¿¶Ñ·q ©{2ni•^~š(G€5 fú¬/9ë%ÚVtG9sˆÄÓq=ƒO%"œSX?e&ô©†CÜ%LµZSÛ÷¾¼ÍBܧîÛÃ`0à’9áG¥Šhu:ÒÓ30™Œ(6‹ƒ^‡Vw £<ŽîN°{)ß|¾Ô¾]ÏRDD?Á›oá¦óMtMÚ†³xÚE궯†{Ö4ÎDõÊ@÷™Ì_5QKl s¥T½^ÔnRç{:H¥Á­úsüo€+ßϛèUhݨÛs}ZËŒŒ%ÆÙ%|=|IîÏ|;0ôÙfïñ Ý§Mñ׺rtê]u_ÎdÑáhúT£ó÷pœ9—ßff…4NTï\…¦Ñžè4ND>3”A®?ðãÊÉŒúY“?už­LíPù4Qrhœˆzi<ßxŒaâO_ñî, `À=$š¦eœo®.Û®rb×RF¯:I €c5º~ÈÛ¼Ðr%çNqŽêËGý¶Ò{Ò'Ìl<•~å@D«¾ ™úî:\»=NÅœ`†íR‰I£®ðh«2ö° &´-ûliŸõø»Ö‹Æï|D÷ác™ùáëLÐ;ãÞ£ýû&±ýÃq|7d|©Ý/ŠQaÔx}"Ÿ{|Ãw ?çÝi 8„Ðd` U/ìÀµø4ûïl“=mŸ®JAňW¥ö¼ß¶uþ™϶…ÛI/÷ ®›ÉJO`£6”8Ÿÿº@››þ 9•ôät0¹`Ìê‹ÕùÒè¥!4Ê™j£}Ä,!M´xÔÌ·Ã<øò»ñ¼»ØŒÖ#‚fÆñÆ“Y32)×{ï% ç»ïÿÇZ+ w% |-*yê"Ìúì\ƒWÞíÌůfóÑÀ™h="é1²…fBˆ›¢Y8o¶úhë¢ýLò¦õkiÕ®#¿/_B«vÙ¸n5ÑÕjÞå$Ú F ™|X22°X2njý½»wÞ³´Š»QYôèÙ‹9³gÝã q{nToÓRSذn--ZµÅÍÝã§LÜ/,'¦Ó繿h?}2ÝKÉͺ7ãçùshÛ¡–Œ›»÷â^Û»{'•#£Ù¶e#µë6`Û–ÔiR¤uÿX±üþ1ËËb¹ù`L!„(”šAü©“Äšó{?H‹Ñ«a¾·ð…5žƒbÁr”_¾žEZËá´)1—]!„÷˜\!„B<ܬgYüN_¦ÉÿkŸÎ“øñJÜì¯SY/¬æ›×Fóň֯3êõZ¸Ê‹šB! ðPf{wï,î$ˆ"èѳWq'Aˆ;F£Ñ ÕjQd^íû—¡ }ç®§ïÞ¬>¤ Ww¹Ã[âá"m§x˜<4™¼_V2ÈûeâA£Ñhprr&.î2ž^Å!„(Q.]¼€«Ûÿ³wßñQTkÇ3Û²›dS „’Ð{Dì ¶+Øëµ]½Š¯è½6T¼Ø ‚ DQ¬ˆ¢ÒTÒ«zHBHÛÝ™óþ±%›H¨ ð|ýä&»;;sfrÂ=Ï>ç<ãFn²)Nuò>fBq¢°X¬4nœÉò¥‹Ùº%G>ýBˆ0M“m[rø}Á\š·hiÖ :¦ǹ“&c&„µÁbµ’”’‚Ýé`iöŸÌûevm7I!ê<]×‰ŽŽ¦]ûN$$%áóô-Ò…8îH`&„G‘¦i i$$$‘””ŠÜ¥[!jHi”‰“†fBq †aÈT!„BTMÖ˜ !„B!D-“ÀL!„B!j™fB!„BQË$0B!„BˆZ&™B!„BÔ2©Ê(„G™R ŸÏë¯Ê¨J©Ún’BÔyš¦aµÚ°ÚlµÝ!Ž Ì„â(RJáõ”‘»s7®§¨h/¦iÖv³„¢Î‹ŠŠ¢q“,“SÐu™ä%N|˜ !ÄQd>rwîdÅÊå´iÛžøzõe€!„Õ0M“ݹ»økÉbì ‰Ir/Hq“ÀL!Ž"Ÿ×ˆ ëhÓ¶M²šÖvs„â¸Óåâ¯ìÅ$%§J`&Nx˜‰:åŠaÃk» B’ɓޫòy¥{÷î%¾^½cÜ"!„8þ%%§²`î/ ÕvK„8ú$0uÎþ¸BÔU5ù@A¦/ !ÄÁ¢*WÓ IDATÓu]Ö劓†Œ„B!„¢–I`&„B!„µL3!NZ»f?ɈëçÛí² Z!„¢6I`&DàÝð>·ΨY¹ì3“ÞÌeÖ#WrÅ=Ÿ’ã;’GÕ±»“IKK"Ö.«ª…8q™­úˆ»û÷aħ;öý7&|Ëü?™øàpú÷îMï3/⎗~b›÷˜5T!Nj˜ QØÒzÑ; Ö|ÿ¹•’WÆî?ùa"£o’h¹ wÇkxtôHºÅÉ?BœˆŒ‚U|ûʽ\yÝ8~-2QÜx+Óº›×³“¹dÔSqg^ÉÝÝ)]ù '=ÆS擌”Š¥l;Ë–m%fðM\ß* ß®¥|ùÞtž{%…±ÿêM¼$Þ„¨Cld\þ:Ó¯ÐÑŠåÁÎX6)X±md2²•;p˨šôn‡kê_ü¾ÅC·¦öcÒj!„8YÉ0Jˆ:ÂR¿+Z[Éùq[k:|Ûeö&t fï"¦Îʧյ·ré©-ÉjÞ!#."£ø~XWZ¾#w&:µ¡UëtjE¹ì%’mÚЬI&-:õ¤kZ,c's§Ìao³k¸ûš~tnÓŽžÞÆ}£YûùtÖ„a#±U':´mG—3.å† à[1—õ%Gû !š¦×ð¾¼{¶íg}â#ÊßaM%†¶í9¢ \…BTA2fBÔZ ít$bÌÌÞ4ˆáM4r~þœ¨®\Ó* oÎ*¶ù¯ÜʰW*¾Õ–WŠÂUån™9¿åBÞýOÖö<‹þýûѽ‰{ߌ™gK·@òÐæÄ†>²‰ £Sc¬³Ö³&Ï ù>ñœwZ6#Ÿ‚R"å³!„BˆC!™u†FT«s9-öQ~ž¹– ¯²ðýO;©ú4s‚© ']ozKÛ+¼ÏçFc?ŸhÛ38ïß/Òeñ̘þ9/=ü_ÅÃe™VëtÔ+½ !ê: îd7”ìbw©—?kæ+ØÂÜ$»e¸ „G›|¼-D]âhÌYg¥R8ïsæÌû‚_ Ò¿o:6ÀžÐ”­„ ›5ÒÒH }¥RßUMá -‚”ñðFåfý·_³®r™5{­S`Û¢•䇢¬Rþþc=¾ÈÆdJq!N`:1-:“Ì:~ZQ¨ÞXÊú_²)rµ¢Kª¬/Bˆ£M3!ê+)}ÓFeóö› : ¦W¢? Òb:rÁéqìü| c?ú‘…Kþ"û·ùòû5 ¶oço|ýýoüµfkW,aÉßEàŒÃY9β$ÒóâžD®z‡gß™ÉÂ¥ÙÌ™ú"cg’9x0M#ŽÞY !j/‡iwô§ï•¯²² ìÏgX{/³Ÿ|‚÷~ø•_¦>Çcï$ãÂá´“ŠŒBqÔÉÜ!ê=® C{ºYú£…>çwÀ\‡¯EÒæêG¸;f"}?žg?3ÀMzát;=‹Èý$´|ùë˜?ík&äy îF]vÛù4²AÅzîŽ×ñè­Ñ¼ýádžýÞƒînD÷aqMÿTl Ó…8Ѩ°Ou¬) ~| {žz–‰£î¦Ä–H§Kçk›!ŸË!ÄÑ'™uN-G¾Ìä‘U¼d«Oç‹ï¢óÅU½ÏA‹_crå½5½”ÿ{ñÒ*äêô “&…=¡ÙIî1œ{ ¯r{½^?þ;©_Åý·½“ñ“ªÜ\QW¸ºñß™?W|ΚÆÐÿ}Ãа§ôØN\õÄ$®:¦B2•Q!„B!jfB!„BQËd*£¨s®Võ4:!ŽGš¦¡ë:¦)+ô„â`É¿âd"™¨S&Oz¯¶› Ä¥i.W$¹¹»ˆ‹¯íæ!ÄqeÇömD»Ýp€êÃBœ(d*£BE‹•Æ3Y¾t1[·äȧ¿BQ¦i²mK¿/˜Kó­1M£¶›$ÄQ'3!„8Š,V+I))Ø–fÿɼ_f×v“„¢ÎÓuèèhÚµïDBR>¯·¶›$ÄQ'™BE𦦑DRR*hÕ¿G! À4 ÊÄIC3!„8 ÃÀ0d*ŽB!ª&kÌ„B!„¢–I`&„B!„µL3!„B!„¨e˜ !„B!D-“ÀL!„B!j™fB!„BQˤ\¾¨“”RäíÎeOÁ ÇRª¶›t\°Z­¸Ý1ÄÆ×C×kvÃ,¥Û·m!w×N¼^¯\ë²ÛmÄ×K$)9µÆ×Z!„b$0uŽRŠM›6â°;ÈlÖ §Óå¿I¯8 ¥%%ÅälÞÄæMHÏhTíuSJ±bù_Ø,:©i8´ AFøÏ*ìñɼi TÃN]¡ð”yعc;ùѼeké£B!„8,˜‰:gwî.l+™M›£” }išV!›|> >‘²=•úÁs«êùàµp:]d5mΪËÈÛK|½ú<ÆÖ-9è¤6ÈÀb±¢[t4@ÓôòãùŠB¡í¨(ª¸ÖóUúCS ”©°ÙlDF6aÃúµlÛº…”Ô´Úh¸B!N'M`–½ham7A„iסó~_Û½;—f-Z…ƒŽý&À>Ú‰ä@ÁhU+@Zz«—/«60Û±}+ c±X±XmX­ÿqÊÃ@$R¨”h'^`¦Â"0ÿ%<ïÑŸ@3è ¥k  ­A«V.“ÀL!„‡å¤ ÌàÀÁ€8vª ’=ee¸\‘@Å@¤ª $ügÓ4O¨àÌ4M€ AXøkº^^»§òuq¹"ñx=Õ£´´44}ѪëhºŽ®iÀ$;¶ï //’ÒRœÄÆÇ‘”˜ˆÕZçOA$3,«Œƒ•p•–EUYøkÁçó‰á¯5¸Ê ¤†L…¡6*ƒwY²T _NÄi£á3ÐÏû§2ªÀz3¥Êƒ²àv¦qâô;!„BÔŽ:ý‘wAAÙÙKÈÙ²…â’”ÒPJ£¸¤„-9[ÈÎ^BAAAm7ïö¹|üÞ Ö”ÖvKN *, Ð‚ƒáÊù°à 9üõcûe°{á'¼ûñŸ˜UoÞÅLzæ>^ç9¨}SéÜT_•·!,HS5ÌLå¿¶w`(¥ ü9!… ˜˜ÊÄÄD)ÿ—© ¼v"|™Ê|ÏÏÿÝ0϶ñ_“ÀõQá×C3!„Bž:›n*((`ÕªÕ€†ÕAT”›Í€×ëaïÞ=ø¼e¬^½†fM³pÇÄÔZ[½9?òéW^RŸKVĉ3•®¶„g½ü -”© Ï …26µ:}ÑËÖ9ÓøtÍœ}AGªê…FÑz~û'ýÍÃ^•µÏz³àóaÛ³?5É*Ó eþ¸NáÕ‚A‹™2š¦Ð´òÏrj7_¦ðî^Ç’eëØj4ãÌӲ—]Kç±`Ež¬ î‹¥º½*Q)HЦ&*!ô‡°å3Mž?2µB!„¨u20óù|¬]»а;\¸Ý±DE¹p:#())Åjµ±gO>ž²bÖ¬[O»¶mnZ£*`Þ³÷1îÏ¢ÀޏtšµëN¿AgÓ%5bŸìŒ86̰ øƒ‡`°¡Qž% ý| Å¿óWý—ÅûŒ›c8û‰W¹©¹ã0[«‚•!m®zÿ÷òs:¤#U®@IX-Áð õÓ>+3Ͱlœi¢4Pz`•ò¦*?7¥z¢Š–óÞãcø&/“Kÿ}/ç¥Ûy_àcû¯0fê6ÈA^éØ÷I§²ô£×™´’/ìÍÀvîjÿ¦+¯gDë¬BA›ê—aAœyâLëB!Dí¨“ÙŽí;0 4+Ng$Ng‘‘.Üî(t]Ç0L¼ÞH<ž2 Ÿ;vššZóƒ(ƒ’ü"HÂÝ×¶&Â[LÞ–Õü>kÏß;‹Þ·=ÌõÝëUû »8ò‚YœÐP7,3ƒ±š‡ëŸs'·ö û}jvêeØhÖ§B›«x~¯×X¥ëœ‚ž9 _V㩌ÁF‚LÓË¢…U+ «"ë?úO~¹‘âPãìĤ4¢eçÞôï*™Ñþ«îÛ½œÅ›½À ®/d`Zìa|øa–ǸaAe*|3,;¸á³òL¬˜ºXþš††Ò¸R2•Q!„‡­Nfhš†ÅjÇb±`±èèºúTZ×u,‹Å‚ÕjÇðì)ØspYPT:Í[´$JÚu¦×™gðÃóÿæWß u³û8-^UFÎ/ïóÖdzY‘ëÅ›Åi—ŒàªÓà søúù{xkÕvJµhž2ˆ«¯@‹((æ·Ç¯çyßM¼úHOÜàYÏ„>ÌÂÞÿeÌ¥ØP”nžÃ‡>áÇ¿vP8bSiÜåbn½ª+ñ'Q„h†g•ö—e U¦Pûd’ªÚΕҔ֭’÷ ´};ùåõçxÿ· l-(4¢2Naè 71¤E”?Ð)ZÂkÿ~†™›Š0Ñq7îÉ%7ßÀ€&ÎòÀbÇlþw×4VmÞ‹r¥ÒùÜk¹é’Nß[y[ƒí1ò—0íñLŸ¿‘½¸Hë2ˆënºˆ±ñ‹TH¬p ³ªëVùò˜¦?㣚©01ÐuÿÔÅJ9¹ ׳œAIþ®PP¦éÊôP°uó¿XÅü™s¹ìÁÛ9'Ý–Ô— +a~A:gtŠ>ÌB-&F¨) Ã41ö]€ˆö³aš5ΘAØý¤•ÿZ_ógMÿõ÷§me*£B![ Ì|¦‰Åb­°VÆëõRRâìõz+”·Xmø ãÈÜ–D¯+0ýþi|õÛ.zžSŸ½¿¿Îÿ½º‚æ—Ų֑̈ìœ÷>o¼þ Ž´§¹*+8΋J;k¦ã,XÊŒÉïóŸgmŒyèl’j0Ö6rç0î‘WYšÜ—+îèBZD1+§½ÆÔ%›(6O²À¬Ò³Ê¡@匙V9“´Uf¬Ì"6f¯dwÆÜ}~¥9ü6å&þ×JÆËwÒ)JƒˆTN½ìvN‹Á^²ƿƛÏ&Òjì4´öéñ‘ÐëJgÆRºò&Lù0†1—7® ú·ÝȔѣ™ªŸÍÈQ7’î]ÁôWÞ剱 ¼8ª/õkX’§Bv,üzO­¦SQ LLÓÀ¢YüÁš©B³"ÓGûÀfy°ÖøZž¨'±f[–|DŽ׾`Eér>xíÚ=2ˆTïZ¾øð²}јí;qM¦²-sùàÝϘ³z7tœqésÃ\Ö̉oÛ,^÷9K·â,Ñ hÆ…\5¸-±ºY~Ö¦ñØ­ãÉ-V8S:0`ØÕ j®Ì°_¸ Á¬–‘Ç’ïóÑÌ?Ù´WaϢǠ˹¬OC"‚+¸ÁPM©@…¥a*K`ÍLeB!Ä᪓™ÝfCC£Ìcâóùðx¼èºŽÏ羂Ïù|> ÃÄa·b³¹S±ÕoNCüµ.Ÿ¡øeêT÷û¸yp;\´hx›~{ˆ_~ÙÌeY™w5bÀ%ƒèéÖ€ö´ˆÙÉÏɬÍgpyÃêŽèeóÌ©üi´ãÖ{¯åÔX (þ ‚©µ_tò˜ *4mßrðŸ?@$ð¾¿ß¹…KÞ {¾Å]ŒìT¢•z[dz[:wÈÂN[Z'ngá}?2÷ïR:¶t€G›SâoÌ$mØ|f=•ͺ½—’ëÏ8‘Ö—K‡ö%ÅtlEÒžÛxè«ÏX9ävZ†²Yþ ~%˦0}c®w g¦X€L®¿v!#ÇÎdiAúÄÖd‚_ÅüE57•6ýk£Ͻ‡;N+_c¦»’p*3𺿀ƒ‰ Xb2ˆ§˜Ü½^LÓ†Y¸’¯&Lâë?Ö³£Ð$Âi`ÐRVÕÐ4ML ÀNZ‡ ,ßn`]ž—æ¡ö›˜¦‡]k6RBn»Œ ›Âö=^Lwu–šÿæÏ•b¥þ¸©æ•ÍàµF¡)-´nͳn…iï™Xq¶Ï>MÊ“Eá×\ Û‘Â6’ÇÆÝÌøò@I)ӓǦ<í®y€[»Ä ›>Œ`P×›_ê…Y²—‚â"6~þ/Ï+dí¢JOiZ~ÜŒËyêÁ>Ä—¬`Ò£cù1=3ΡÏPw…jž¦iâËÏG?îR2ê6°°kösü{ÒZ²¿YȮΧ“` +þ€ƒA®˜¾ºVzÍ ­!„BHÝ ÌÜÑP¸‡ÃFY™ÿæ`†áC×ýÃjÓ4ðx¼”••b·Ûp8DEG±ã{w,g£ê7©‡•­( þÌ»¸÷œä LwÖ'(©r/áå@×50|û ä† èÖº}c¹c$XH¡¼ÚÝ‚³àóPei@àä¨×€F’*®13ÍòÀ*˜Q”¦£Ö-™F>¿¾üÞ]Ñ‚‹Gü‹ŽiNгßá‰÷ÊüY3˜¨@¶(¸kU~  þÛ› ´fücôHZ9ÃÚ£Ù‰IÔªä&Ò•—`¬”…W4M=ÕªIA 3ìº(tU^Ðÿ UU+Ψ˜)Ã0Bk½LÃZ㥙†˜†5…®­,ü£„ì×ïçžïºqÆ98³]"MQºñÞyë3þÜé«pDOQ>ÃÀX· ™fDCº¶ŒäÇyEä®ÛI©YÞ6ÓÀ0 Êr–²€­|:úv> ßq~yeñ ªéZù:3ÿEòkZ ` VÀ<Œj›B!„PG³è¨(ÊÊ<¤$'²ñïJJJðù¼3¯×˜¤¦$ƒ¦u„3ï6~šü5;l­¹±K=,vƒ‰ðçÚ]D$w®Ñz10É_ö+›Iâ¼4L"ëGÂâõìòö!®rrOs‘’ᆋØP|*í#Oî¬Y(‹úWœ°ð™ï¯*Ÿi†*ë…ªA†Uʳ“•ʈéqOÉ”Äag[`-V`ºexe@µ—UóÖcDw¡‰[CåûƒKÓ0Q¦Nl£D¨e¬)‰¥_¦«R°£Ê_IUÏ×××Úéºî¿^ºªQ'xýMLCÍ/34ÿôEÃLô¥iþíÓ&Ûkš*œšä-ÿƒ-Ä‘k%¼9þ‚#Q´¿êAnoô%_ΚϚõ¿òÙ«¿“}Á¿ùgƒoޚʟ;!ªõ9 êTŸ’ìÏølñ^Q°¥måÇ5ñyƒkO ô›ÐýÛ„ú@NïD} ¡íµÈ,b-„ue–_[=0¥SS€˜À6åQŠ!„âpÕÉÀL×uâbc(.)¥yóL6oÞJAAah®”"6ÖMZZ2‘ΈP–à ndÙÒH"|%äo]Íï?|Ëï[bè}Ûõô¬g’è5´3Ÿ½8‘'_.å‚^™ÄéElß\Fó³N£A(ÈÊgÕ¢lbêÁžõs™öá_X;ßBßT ÈèÙ‰èÙßóú;)\tJNïzrB©6;ÏéGƒ>áå—R¸ªKÜÅk˜µ¤°Žþ†Ž®`E@3”)3”± gŸ« Ì*dÕYšâœdgoËHjØãÑ4ÑŸ%Cù3*„h†C£+3æOãË–hàÄów>^f c* p‹þÈ&ÍVÌæ…3ø`N ͆ŸK#«²¹‰ÔJYý˯¬mÐFÍqNê¯|6v ΋Ϧ}ŠoÞvÇt¥oÛ¸PV\Û¤Êï]¦ùï<¦…1öà… ­çóߤŒ`v2x¯8“°û¢Aè^å×Ú¨P’Þ4 |Þ<6þñï¿¿Ð2zѹ>˜Þ°mMÓ(¥°ÈFÖ—qWŸóXñÑ“Œ›³‡õ¿­¤à”x6åDҦߙôβ²kï,`†ÿ8f¥ã–íúï–øÿ¨’š'` 0(Ú™G™‘€½^&‰,c+E”ÖëDÿÓàÔFQ…¶xb00`Æ,˜=ôOaôG¿Á@4Ky_“©ŒB!„8\uvØo³Ùˆ ühÙ¢)ºÅBY™‡ÃŽix½^lV ºåJjœ1.X4±ON÷ï7.¦í.à®»Âo0­×ýfF›óî'3xu~1 ±MNçÆÓzÓÀ®akA»[ùùõgøN®Úõ¿…^ÜøÀšW«áÜw¥—7§½Ç ³`#:©)Ó#ÑKú`þu·7?˜ÉkO}‚•JC; ëh|~"2Í`A P¼ÁqUÓ÷¹§YåX$ð8÷‡×yú‡Š/Eöú7/üÃÚ.8 4@C×·2ðÉL÷XœÄ¦w!Ϊ¡”…è&mh¼b Ÿ_€ 8ê7§çµpÙéÉXÝK†´âÅÏ'òI·ŽÜÕ®C|ç¤øzÒ8¾ñæJ¦ÃÐ6œÞ&.<ó=á¿þòûï1,×þz°˜GÍŠT Ä‚÷S¦¿ú`pšªÔ¨…U©cnœÌƒ·O®øœ»=îéE¢ðV:xéj&?úÙš›z1vÊvìÀ™XŸG=š%Á’ÍEÌñ¿lΈCÛ½µê“Ø8™Gî‚7ðïQ9¯{},šAb7¬ßÃÞ/ñ„ã.º°ƒ:Ïâ…E¬˜ú ÷LµbÕ|ø”ö7fd þŠ‹þudþòÿ&š® _{æÿ!X ¥Ú !„BT£Îf@àfþ ËfµârùÑ^¯2q8£à‡C{_§G¶uÚs8ô^åËöÆpÿST³™ýoá‰þ·ìg+ñ.á¾N—øª½,sÏÅy•ʇàÚ¨ò€!`TÊšù· L±Ûß3g;îÿÎwјw¸üÓ×¢NááwO ÒDëÀ¥÷uàÒ}Þ©PJ'åÌ›xôÌ*Ï$ÐÔš¹†OÃDnι7>¹7VÞex%Áà7-x´PPFù3ûnÁõv5Zcf–_ëÀáCV¡õ}¡Ák° þ/[L}\ú߇”9ˆMiD‹Ž=èÛ§)ŽÀº93¬ÝÊÄç1ˆMű!ŸÜ€-††íú0ôÂLšNÏk¯"÷Ã/™·j›×çœñÉ4in*"R[Ð,¹Œ¿wPZæK4 ڜʹCúÑ6LÓBƒWq^þÇÌ\²{¬(Ú]q77$~Æ—sÿbSŸÒqÕO§¾f`˜ kþi û‹Ç¨ÀµPÊ?¥ÝÀ™&š&3!„B¾:˜…óz=x½žÚnÆQcæÿÅ÷ós‰II &ÂË®eß3y´Ñšè“:c¦BA@y‰üà–Á×*Þwꄼ™1ÁL–¿Z ¿ £ Ü€À´O½Æ33°Æ®¼ø~yP¦ÂM•Å×Ðí îÁJúÀ;yjàÚø…9Zpã³Ï…½ÐŠ‹ïÅÅûy­^.¼¹Vù:ØZá¶ÖCxLÝÕ„³¯»Ÿ³ÃŸ·ÄÒæœ«isNUoSå×B™¡õtà¿JÓëÌüéU-°±fB!„8\ÇM`v¢3 ×óçÌ, ÜLןI×aÿâÊ>õ8Éfi‹f CV.Tœ.FT\þs¢ ž›‰ª°Î2x1=0¿0xµ‚S@kZÂϰàªà•?SZ /G…‹|‚^êÀtÎàš½À4E@WÁË–MÔý×ÚPB!„8 'U`–½ham7áRè?|ý+<·—õÙu¹ÍG‡Õf'?/W¤«ì¿<¼z( ôÐô¾ðë@PxëwÕè|^|áùmWÊÏ+¸žÏ¡išÊÚ à ®(k׬¦qVÓcÚ^!„BœxNšÀL?²š6cÞÜ_˜7ç'Z¶nK\\½}ƒ”w2]•BÙ²ðµ^•(¥ÈÏßÍÂ%Ùø|>šdV,´h݆¾ÿŽÅ‹Ò°q&±11åÙ¸àýÊÊ—YU¬øöâÿÆÆ™Õ á½I .#p½5B挢”IážBÖ­[ƒ©Í[´:öB!Ä E3Qçè §öìÍš5«˜?çgJJJª“@Ó4"œN5nBffÓß-Àb±Ð·ßÙ¬XþK³Q\\| ZzüÓ4 —+’ÌfÍhÑ¢U®µB!ÄH`&ê$M×iÚ¬M›µ¨í¦œðt]§Uë¶´jݶ¶›"„BqÒÒ«ßD!„B!ÄÑ$™B!„BÔ2 Ì„B!„¢–I`&„B!„µL3!„B!„¨e˜ !„B!D-“ÀL!„B!j™ÜÇLÔIJ)òvç²§`†áC)UÛM:.X­VÜîbãë¡ë5»é±RŠü‚öî-Â0 ¹Ö5dµZˆŽŠÆívÔµ–~}ð¥_ !„Ç ÌD£”bÓ¦8ì2›5Ãét¡i2«ŽRŠ’’br6obó¦ ¤g4ªöº)¥ÈÙ²‡ÃAãÆ‰ˆˆk]J)JKKÙ²e [·n%55¥F×ZúõÁ;”~-„B$0uÎîÜ]Ø,V2›6G)úÒ4­B†!ø8|v"e *>ƒçVÕóÁkátºÈjÚœU+–‘·;—øzõxŒ¼ü|¬V+Mš49²?Áiš†Óé$33“5kÖ—ŸO|\Üß#ýÚïXôk!„âxtÒfÙ‹ÖvD˜v:ï÷µÝ»siÖ¢Uèqpp¶¿°Ï@öDr A{U{€´ô V/_V}`–—O³fÍŽ|£O"©©©¬^µªúÀLúuG³_ !„Ç£“&0ƒâØ©.Hö”•árElU ÖÂ6Mó„Äš¦ Pa°þš®—×î©|]\®H<^OµÇðz½8Î#Õä“’ÓéÄëóU»ôk¿cѯ…BˆãQÌ|>;¶ï //’ÒRœÄÆÇ‘”˜ˆÕZçOA$3l ZyÐÔ…«<°;QT5p -ø|xÖ%üu£×âDô×MÓ0 £Úí¤_û‹~-„BêtTSPPÀÚµëÂ=þOR‹KJ(Î)aû¶ídf6!&&¦ö)Ž86ðÒƒƒ9 ª)¥+êtˆaìæÏ?guƒó¸¨K<–궯4(­j•Qè|ó-¸5 ÌÄá«ÉuØ1Ø5ûIF\÷8ßn¯>p¤…r€¦¡?›aŸ¦›¦éÿT=ðZÿòí`Þ´O™½®óÞ<ïðLBUÛû5 $0;2öZ×…~m¯fú³wqåC2t(W?5Ÿu8ûT”m[È/¿¯¥Àô?çÍ_Ç þ"×8¶ýZ!„8ÕÉŒ™ÏçcíÚu€†ÝáÂíŽ%*Ê…Ó@II)V«={òñ”³fÝzÚµmsHÓUéfæ}6…?g³>ÏDP?³-½‡\ÉjÕ¨–‡µebv:ß:œÖñVl ÉØ<ñ¤¦¦’Y»¿ßÖïøß˜_è4ú92ÝöÀ³:vw2ii^bíÇþ³zÓ4ýŸ”i•3 ×B?ÒArùöþ‘¼b¿› ÷$úXœf¨­ŠÊYƒš½½R¥>ªÈ,hZè纘)ö®ü‚7ߚƜå;)ìq iÛ÷jn¿²+ñ‡ÿ‡V«j|­«_lýâaîz{egtW[v¦ÏÀ!ôoŸˆ­Æý¸ŒÕï=ÉÛfpù?¥]NØ÷ÃÙÏ¡û Yé:ȪhaχO‰«É”¯cU†ÝÜý3c|?ú0üö4t+ 6­`µéÆuœeP³ëxøýÚÄS˜O™¥%W?p9™V{wýÍ_s¿aüèo˜yÞ#Œ¾ºîšÌƒ0v±dÉnb{ÝËàžÍÃþ]:2SÁŸkô7vúµBq<ª“YAAš¦a±Ú±X,X,:º®cšþÿkÖu‹EÇb±`µÚ1|{ ö\`¦JXñáK|•“ÎÐGGqQ–3ˆÒûìòíŒ<}:žI_ÿAN1D¦u¦ÿ°kÒ>  ö.aÒ ™»n+ù¥ ˆ ©ýÙ\=òB:ÄYÀôRâ‚ïåÚïý»L¹èiþ{êŸ<üÏi¤?ø"·µŽ¥›çðá„Oøñ¯”ŽØTw¹˜[¯êJ¼w9/Ýö6 Óç%cŒí3xàŸŸÑè¡ÿqsK+ÛžÀŸ/duN^ ¶û=<}k ¿yŽÿ|˜Ín`O õYùéÒÎaYŠ|¾õ¾ ƒ«Æ>Æi›žbä³^n~e=Ý(ÛæÌÛÍbéŽ2,îFt?ïj®Д( 0r™7þE>^¸‘m{<€FTƒ.œwÝu juPA®>È Ë0Tüý©Ð÷C*)^ùUµcÏr>ë-¦Í]ÏÓArû³výôLög=k'1êñé¬Úã\¤uÌ ·\H›ÀÈX•åðÓÄW™4s»¼âga-ëþÎé`išÿº†ïO)vJ×q?ŽUöÁ“ó++°«¨7qÿàtlhØc°”T<”‘;‡q¼ÊÒä¾\qGÒ"ŠY9í5¦.ÙD±Ù5´Ÿý3ÈûkËö´bøgÒÈå£ÔÖ§fAouWß9˜¸HEnöç¼9õ%ÞÌ˽ÝÜÁÖÑý¦{’aÝA|¼6U¸ìùó-þïÅùÄy%wwK¤tå7LœôO™O2jP*6³ˆ¿—®&/ýbîØGÙVN›ÄûÏXI{3"k8U^‹SyV9³ UþÄý Uùi¾w3Ÿ>ö0ïíîÌe·§¹c+s?ϳ1öV:¹5ôú]zS¢ãœø¶Ìgò+0fbk^¹¥ª?_˜f»8íÊÒ;ÃJî_3ù`½ÿîHdé*d¾¯t]šÊhoL³ùãÛßÙ–Õ“äÊSdõhšö»šûÖíïaÅç¯ðæØçhÜæiÎiӛƼͼU{971 ƒÝK~c{Dz6ŠÏ>zxéý¹é±[I÷.ãÓqoóؘD^¬õÁJÚCYcv(ýºrF ÍA£WqÆô‡ùqúþÑñTÜÞL=š©úÙŒu#éÞLå]ž›À‹£ú¼ Xt¯;ø÷ ±ö¸$,NN½ìvN‹Á^²ƿƛÏ&Òjì4´UÛßs¡Ç–Ö\ÿØu´ˆl1¤ZÜ÷D¿B!ŽGu20³Ûlhh”yL|>]×ñùüÁWð9ŸÏ‡a˜8ìVl¶ƒ;_þßlõ@J›Dì'fP…‹˜òíR.zЦaÚµH¡lýLŸšÍ ûºáŸ\i#±U':´Z“^’Í]Ìe}Ioâ˶lîÒ32®â2*f^6ϜʟF;n½÷ZNÕ€2ì "˜z°uMÜ™têÔ†ä°D+£#]3ü?g5Œæï_þŬ¥;ðtsª¿Xq§¤“‘a½§8|ŸÆNæN™ÃÞf×ñø5gøºmšQ/ÿnF>5ýn¤eàx®m騾 vÚÒ*a'>ô3ó7{èÐ<|²Ô^J™Ì÷-›]ñùC˜tÜ·2ýÇ©ôrÉò)L[Ë9ßÁE-"€v´j¤±ñæ7xÿ—‹i7 ‹»)]»Þ•ÎUKfñЊeìò¶ µðw¦Ì. cøcÜ28Å¿V±¥›5ßÿÁ2T•Ç<8ëøU,¿®Ð4ËA G; aIĽ·¬ç¿¯>ÅÈ9 è|æ:‹ÎiÁEì$wêIr`û¬„ÝüøËëü¾©Œí;Ñ+ýu>üq{OíJ”ÊcñìØÛ\F–ŠȧÒ¹òÕ‘œ•jšqÓˆßY0æ[²óÏ䌸ƒïá%í«ËÆjÚÁf‡Þ¯ÍÐï·|öTÚ¥Â÷ÛבëíŽuÙ¦olÀã®áÌ Éõ×.d䨙,-èCŸHÿ³¹“ÉÈH§ü/?Ž6§o”IÚ°ùÌz*›u{/%#6ø7Ö•Ö¶°À3øw¥K$Iééd„Ö˜™èoG¦_ !„Ç£:˜EEFRæñPT\€ÇãÁb± X,^ ÃÄãñàñx0 /.W »½š½V¦ÿ÷¯íwXïÙ±‚ÍF ]Ú&šycK¢mK7S­`§7˜…³àNKÀfäSPjBMš¥ŠÙ´|'d ¡eÌ‘®Báaû‚©Løt.+rò(µ¸°{ÀÚ¬úâ–ïbK·@òÐæÄ†²dtjŒuÖzÖä´¬¿ïÛlqÄSL^ÑÁe3U°*]åJmU b•:Äûp™ÁÀÈ_Ϭ° »V­£ÈÖ„ÎéöòìlL :$ÃgK¶PzN=Øø“&|Á¯«·’ïµáÔÊ ¾aRº}%[U,Ý[Ä¡™&&€¨´§Lÿ1¾Õš¦öÜ*¥ðß›7p¯¬ UÝ/êȯ=³’vÖ]¼Ø{ËæÎ䛯>`ôMi~ÑC<4¬ Ñænþ˜ò&“gf³qg¸"ðâ£ÇÀÔèÞ7ƒ ÏÊ¢Nt([̬µ6ÚÝÛŠHUFΪ ³™ñ7e|…c¦²m3æàÿ™óù|Œ5Š|—˵ÏM+Òj\.ÿðúuy@gš&©LÌ`Pgzصf#%ä0á¶Ë˜Pa)lßãÅtû¢ªÐÍ•|5a_ÿ±ž…&Nƒ†”z _!ìøa}:Ø&:×`ߪ²½U:rýZ!„8ÕÍÀÌ …{q8l”•ùo*m>tÝŸ–1MÇKYY)v» ‡ÃATô¾!ÒXÜ H´ÂÚ•[);#Žˆ#Ø~M· £bà­0L@·îÿþš†®á5*Óâý{:OÿïKô3®åöë2‰e ß½ð b‡ì ¯ƒŸ©‚™èh|jr ©˜…e&”fy–Í,ZÆÄÇ^ãç¤AŒ¼ç&FyY3õÞØx …iåƒöà÷ƒÌ•ûÊKÕU¼3 šæ‚A„©j,Tº®G=@³×§åé—ѲÏùœ7åßÜûþ Líñç¬|ŠÇßßEkoãm±åÍaìãSA¬FB·¾4šð!ß.+ ½ð[VÚ:p_KÊ4ü}DkÁÈ'n ¬» ÐìÄ&Õ,hª¬¬¬Œ§Ÿ~𼼕öU­ÇÒ#©¿nÜI™JÃZÃ$QÙÖål¥ 7^tíc50œ¤E†ÚêƇ½eìØ“h‹­$ÿ¼ÔÀšRþþc=¾È¶dá ¡,Nè»òç7«ÄRáÿABA¦‰ª° Ø&py—ñû†bÚ5÷‡íÆîå,Ú©ç$¡çÿÎê=6ÚÞ4„Þ­œ€#Ñ üí¶ÕoNšå;²ÿØBY– Oø 6X¡ï`Tµ}pRpM’®ëþ륫O¯«.ƒ³¿çƒÛRÿý¬ßµ—mKÿÆLÂ%çt&Í ÔkˆÛß@LÓDKèÁ€æyû‹i\´ W÷‡iéR˜¦Nlãt"Ô_¬*Žåì¬ÈJð`tp|>FyÆŒX,yäbccCÁAxÐÌUçðûuøH³|¡*eý—™]Å©ç¶"JéX5 B-cMI,ý2]û\å Üh,ØïNVn(#¦Ç <% PZ‡mþmLÈú‘°x-;KO#ÖA؇ åç`Êk„ ¼E{ ”£ê¿Ó£Ñ¯…BˆãQ Ìt]'.6†â’Rš7Ïdóæ­†*J)bcݤ¥ùW¤D:#*L5ª-šï¡ÛòWxï¡Q¬Ø.MêaîeûúUlKÈ•½;rÑY‰üßÇÏñºý2NKWlœý>S·%1è¦öDk‡_ÀÁÏNãsúÑà‡Oxù¥®êßwñf-),ÿ Ù’évJÓ¦çÍÏ/ã´ÆÑ¨m(¬fÏŽ¤,êó3¦ý€»g#bô\¶‡­o³ÆgÒÈUÌïOgÎЖ8 r±·îMVøN,‰ô¼¸'Ÿ>ûϾcpQ·DJW~ËÄY…d^>˜¦€÷ˆ\T ›`¯ªü†»¡Alà窰5 "Lß%(\Oö"åI Q ›Ó¤éy j4ÆŽÃ}ÅY4‹ØÁ‚)ï±:º÷œ‡ÅšFF¤—ß>ý”Ùt"-ÒdÓÎRüÙO3²Ä£Ÿ>ɳÆÅôk€­h5[JüÙÃ0ª]wÀ€3P¹N«Õ¾+4”î¿}µªA@R¾î©úàìp”­ù˜—¿öмmS’c옅›ømúWìÒ2œIýfÉ0w&S¾N§_ózØ‹6³ˆ f•´8º lÏÛÏŽg2 ‘ED O8[åÜ´ù|2æI"¯8—Ž)N¼y9äÆôà¬q‡t/ÂàùÿôÓOôèуˆˆî¿ÿ~Ünw`juÅ~W³©Œ‡Û¯MEBïnÖ,ÉÆ°y)Ú½™ ¾ç»%»iÐÿ^†·vbŽfƒ8'õW>;çÅgÓ>ʼn7o »cºÒ·m–P–ÌÀ0 -†F)VfÌŸÆ—-Ð:Á‰çï|¼(LÓÀ0,¤Ú‘èÙßóê;I\Ô5 §w›Šƒû1Ñ]18XÆ/sVÒôŒ,¢R³ˆWß0声vrS´ÓGÓÓº‘´¿µŽP¿B!ŽGu20°ÙlD ~´lÑÝb¡¬Ì€ÃaÇ4 ¼^/6«ÝrhK½žÜöd<ßM™Æw3'2o¯Øp§6¥óS‹ Ùq¯ó&O{9ÅàJëÄ…÷]ˬˆJËÔó|Óó¯»}¼ùÁL^{ꌨTÚ]Lõ±‘1ônnÜû:Ly‰`‰¤~£64q[`?ãl ‡p×Uù¼ùé»<=Ó¿ÖË•H³—Ú¤³%î?›]ã?祧¦£¹³8ïž34ܯãÑ[£yûÃÉ<û½Ý݈îÃâšþ©©ÄaÈf©@‘3P6»ª aŸ{?Õôü8~ËŒ{æ‹ /eÝ0ŽQ=ÓxïƒX&¾Ç¯>ÃTå ±õYÜrûE´ÒP´dØíQ:á+^}Êÿ~Íá&µm}€"‚¬Kâ_Ñ“øð›wyîs`%:©]Óœhªú¦–¯‚ {.8`W …æï•®GàrRƬòz³# )¼DãÊû–i¯}J¾ÀIB³ {øJú%XÑϼ[r^åƒIOó³ÀJd½&´*ŸŠÕn0=cþ`VÜú¥[ËÏÏÖˆ‹…óÝ÷øòÝgùÒ š+…N·åŒv1‡t?Âྛ4iÂìÙ³éÕ«.—‹Ûn»èèèдF(__U“}^¿Ö°DÆ`7VóáóO 9ëѰy†Ý73[×Ǽ³­C|ç¤øzÒ8¾ñæJ¦ÃÐ6œÞ&]•ï\…úb ]GÜÊÀ7&3}Ü|`q›Þ…8«†RÎøg˜·>›Ìÿf+ÀFtbD¢)p4ÂÅ6òѰ¤ËCôÎ8ÎßÄë3ÞbÌLp¥ŸÁ-ݺ`ÕŽj¿B!ŽGÚÔ'©³ú¬ÑÆsþ‘sžÏ73>ãœç3ç§Y´ëÐù(7ÑÏf³c øðz¥_WvôúµBQ²-¤Uëvü:ݺ÷ä×ùs8µ÷é5zïw_Ϩ»³Ê¼ÞƒÆŽ'fþ_|??—˜”b"¼ìZö=“AË­‰>‚²š¨˜Y®Á /|M+Ÿút"Rfù/Ó œ§¨VçŸæÚT™(¥‘ŒÙɘÏÝçóa±Xhß¾=ß|ó gu‡ƒ«¯¾šèèhÿÚ§CʘI¿>šýZ!„87Ù‰Î(\ÏŸ3g°lk!ÀŸI×aÿâÊ>õiÌq-0ÕËŸI(_{S1P[“ø® qDðÜLT…u–š¦a*…®i ù3Áë¤Fj´ÿj‚²“1@ /baV«•nݺ1cÆ ˆÃáàòË/'::zŸéŸØ©ôë0G»_ !„Ç›“*0Ë^´°¶›p)ô>‚Štö²>».·ùè°ÚìäçåãŠtFoà/›´ùïñ¤L…øj^¶üx*þ Lõ ¼ês”“ÔÐB‹ÑL¥ÈÛ‹Ý^ýM½­V+yyyD ªiø ¹¶ 6l¿¯Mš4é¨×çóaµZ±ZýÿTöêÕ‹©S§rá…ât:¹à‚ 0M»­ú•–u­__}͵¿ûÎø#~Œª‹~-„BNšÀì¸Z_v’KKkÀŠåKévJ¼Jù3hjxº¦ƒÝ¢zUÆã@UçUá9Íÿ?•³[6«¿–.!½aÃj‘š’Š+èÖ­†Q~#ðª*BÒm Ž÷ßÿ˜/XåÕ0ŒÐtÆ`pÖ¯_?&OžÌ°aȈˆ !!´iÕî³®õë‰Þ=¢û«©cѯ…BˆãÑI˜‰ãGVÓfÌ›û óæüDËÖm‰‹«·ï`î„-ˆPµPV!T©qß I)E~þn.ÉÆçóÑ$³iµûÍÊÊdÞüÌ›7–-[W«X]áõúïÿ̘§3 87Þxƒ#FpÇwpËÍ7UL…“~½¯£Õ¯…Bˆã‘f¢ÎÑ-NíÙ›5kV1ÎÏ”””Tÿ&¦iD84jÜ„Ì̦5 °t]çÔÝY³v-óæÎ¥¤´ô´´î+ \‡ðŒYxÖ àâ‹/¦´´”;3¶mÛpzŸ>8Žý^wéׇæPúµBq<’ÀLÔIš®Ó´Y š6kQÛM9áišFÓ¬,šfeU¿ñI¢¨¨(/þüÒuöíÛ³nݺж%%%ÜxÃ|ðátéܹBðV™ôk!„BìfB±ÁlYnn.sæÌášk®aĈ¼òòËŒ9wŒ‹nÁn·áŒˆM}B!„8X2‚Bˆý0 ƒœœ.¼ðBòóó9ÿüó9r$£GÆjµÒç´Óp¹\¡MÛíöÚn²B!ŽSzm7@!êªÍ›7sþùçÓºU+š5mʸqãp8ÜvÛm|<åc”RDGGãv»q:¡jŽB!„KFB±—ý?{÷Eµ6`ü™íÙdSI%–BGª EAÅŠ`Åò©WôªW½¶‹½ Ø‚€ŠH )§ý IDATEŠ‚ô"½÷„$¤n™ùþØ$¤BBB²IÞŸ†dgfgÎfNvçsÎ{n¼‘Ž;rÏ=÷ðèØGyï½÷HOOçá‡fӦͬ߰¼¼¼Ú.¦B!ê Ì„¢½{÷bôÝwÑ6¹-½zö¤M›6L˜0›ÍFrr2+W®"''·ÞÌ'„BˆÚ#cÌ„¢NÇSO>Iÿþý‰ŽŽÆßßMÓxlì£ÜtéÜEQðö¶b<Ͼ¦i8ìy¤œ<Åþý{ÉÊÊDUÕ‹ý„¨4¢câ ?g"©Ó¢®¨h. i§SSÈHÏÀårzô ƒÁ€¯¯þAètÒs£¾ÀL!J(]n6›1›ÍÞ—Ëå$åäI¶ÿ³Ä6m j$Ù…ÇQU•Ô”SlÙ´“ÙLpH¨»5¬ R§E]P™: î ìàÁý˜Mfb›7ÇËËê±]Õ5M#''›Ã‡rèà>7iæ±e•#™B\DN‡ƒ}ûöØ&™˜¸øÚ.ŽåòÄËjeËÆ „†E”{+uZÔ­Ó©)§0ê ÄÆ·@Ëïž«iŠ¢k9+x\4ªjËZÉ ª`e-/8¾——•¸øìؾ•Ó©)5ªR„gh0ÙÆõkk»¢ˆ¤äe.qóÈ.‰ÕcꔯË\®i™™™Õp‰„¨¼Ð°V¯Xç¸ù.uZÔ%©Ó©©)4oÙºðqAT^” Ъê\`YA"@dã&ìܶU³z¢ÁfP~0 jÖù‚äò.p…ðT¹¡ ]½D] Óé*<^Lê´¨ *Z§íyyX­Þ@ñ ¨¬€¨èϪªV98+(_Ñ ¬èº¢k%ËbµzcwØ«t|á9<>0s:œ8~‚Ó§O““› €—Å‚`¡!! ÿ„B!„S‹`%ƒ±²»’ÁSU”]W°¼h ^Ñõ.I¾Soxôí®ôôt6nÜÄá#GÈÎÉAÓ4M!;'‡#‡°qã&ÒÓÓk»˜BQ¨¤o˜ÁS—sÜYrFîžùäýìÉ«æÃj9ìš÷%@îýÖuR‡DÝ£ n”‚ÇššVª¤–¿¼èúª|QbZ_%·):M…&Y½á±Yzz:;vìÄåR1-ø„ApHþ!Œ\.•;w‘áiÁ™–Ëñí³þP6ž›hUˆ2HÝj Ë>ziÁ¢/¹R#sÛ,&ÏX͉RÜU£ù›ÏÇOde®‰Ü=kXúç‘×*™e.Gê¨ƒŠ¶ziŠ‚–ÿ³Z¤ÅJUUw°”¿®J_ZG¶üÉÚÙ¨E–«h YYÏ/hi“é*êìèt:Ù½{ `2[ñõõÇÇÇŠ——€œœ\ #iØó²Ùµg/Im+×­QKgå›óÞº¬ÂEFßZ´¿”¡× 1  ¿š¼½|ÿæ[ìúm¢¬”ú\¢*r·òáØq,;]°@Ohm{ âÚ+ÛjªB_w©» žzrs·é8. ß˾¬‘±akì­y¨«•õ/>œÏ0§Sg«s6¿]ÖòsËZù(ƒ?É=Ӿন"ïë¿óÀàgÈytŸ Ag?Ê’/ßáó¹«Ù›î­»qýc1<ÆÀá™sß»‘ZLýˆjÙ™ËGfÄ%aÌf§¦­cÊko2eÙ~²L¡´¿æÿxrt/ÂÐü¶R‡¤ÕEj‘2p·B> g[É voEæŽ|9q+¶Ÿ"0ú7¡MŸ‘<0¢#çûÍÝÅ7ã^fÏð ´mìUø™[*ë#Er—[Q –À¬þðÈÀìÄñŠ//o¼¼,x{[ñõõÜ9].‡Ã»=—Óɉ'ˆˆˆ¨øA49iY9ŒGïhƒÕ™CÚ‘-ü6s*ãÖàñWï!Ù&sB¤æ‘–A}Çpo ´œ NìüƒÙ3ÞæÉ]÷ðú£= ’ˆJ\'VÌc»¹/'ÙΗĬúh¬Ÿ³GÂ#ti¤c[M·Ÿ?ÌÓSÏÐþú•Š)û8;7ÁßÛ¨ä¥!Uמß¼“Æ\ΜÜËê™_ðÙã[Èút÷µ4ƒë(sž~”Oöµçögï#&u1ïOøcMŸóùÝÍ©øìwu™Ô!©Cu“¦ªÅZ¬ ZÌ ×Mþ‘ÿ]=½œ·ŸýŒuÁ=¹éþ®4µ©¤ú‡]š/=Åž_îq‹|/ïØä·Ð)E–+EŸ+Y½á‘Yzz:Š¢ 7˜Ðëõèõºü¬:î*¨ÓéÐëuèõz .§‹ŒôŒÊf|Ó¢E | 1™öÑyî7~üg$É}PÏüÃ_MfÞê}d¨fBÛôå†Û¯£k¨ Ç¡|ôÙÏlÚsœLèÃúóìs8üÍXF}ã>Lˇ>æÙN:ö.žÈ§?¬`ß LÁt¸åIî"-¢Ò¬áq´lê®;ÉíiizбßýÄß§»Ó¿‘®zëngïÚy‘¢f¹NðǼ°t¾ƒ6>  ’ö÷Þ?%{΀wc#Sÿ³ÏÑrÙ·ðÞø|>ŽÛ1%pùÝÿæ¡AMÉ\ø×ŒËåáéŸrM˜û]αûn¾mí'|Ïmݽ Ô´µÌùÛIÂØÎt°ßü"C{½€÷eã™ù\ó2–¿Â“öçxz×uLœ2šX#€ƒÝŸÜÌms’™ðýSÄUèu§²éÏ#Ðòqž¾Áùeè=°`ƒü‹cñm’hëЙnmÍl¹öMÖlOCmŠsÏl¦l0Ðëåg¸½‡ í >º{¿ÿš #^ AüI’:TG©E¯¢-gÅ,Ëïbh?¼†ív_úŽõ­òÃæNÝèWd[WÚ&f~ú%sVí'+‘¯äÎ1×’ì6r;4ù®›ìþ¹õc_ñRWŸâÇU”âcÑ ~.èÊXÅyÔ„çðÈÀÌ©ªèõ†b}g99îLJ£XjQ½Áˆó“V†Îlň »]EsfÞ+/ñÍéd®s=ÍÍÇYõý×¼û\&–×F“lSpœXǪ.úÜõ(—„±çù®Ï ÑÀñȥУÃ+Ø ÇÁïÿÕ]óü·] ZÚ1΄úKP&ª“Õ8ÉsjPÍuW4 ®c˘·ÓJ—Ñm°)à8ð-=ò ÛÞÄ¿_îHPîn–Lý”Í…ÏPI]:ŽûÆm iô³Lè`ãØ/ðê+c±4›ÂÉ}‰ã}–n=ÃÕaþ(¸8µn%G½’¹4ÖR¸´¿ç²NMäñNAèÈv/ŽÍø§»á§ƒo$fœe,ÀgckôËV±þÔĆëAMc뚣[>@tE›ô67õ†¥?²pkonNô=Ïl•¼´ƒ¬™5}4áÆt¨¤o_Ë1b¹»µo~K‘…˜žIXglaÍ;ãM•8u“Ô!©CuUÉ1f%C’-fŠ¢`hF0ËX÷ËZŽÅ^Rz(}?ß¿ð3t¸ûÙ{iìØÎœ¿âåw‚™ðl_ f ü]‚¯k©ck+Z†2Ê.ê6 ÌLF# yv§Ó‰Ýî@§Óátºƒ¯‚eN§—KÅl2`4^àKÑœØvòœÙ¤ÜÂ/SpR‰eP¬7y;¦0gŸýž½ŸkZ¸?Z5Õqàá/™¾òÚ ÎßI­:$“PЙ>×݉ÂA“&a…W^V ™x“œ˜Hó/b/ð7$¨.{y¹éݾœéÓ÷AèPÚéÉÝ1§Zë®hœ]6Ÿ]Ö®ŒIôòØõ÷l·^ÆëÿCW›t$>ï7æ¿Yð”#,úâ7ÔËÞàé‘]ðQ mÜãì^r'‹íáÞ;Ó·Ùx¾Z¼•Ì>ݰi©¬ýyæäÛhUpç_McÍœ hmž S`‘KYk(Ñq±^çd–¹\MHkÝ+,^›ÊÕW£ËÞÅŠ½:âþ¯%> äTèµ{Óñ¡uê>3˜‰rõp®îÓšÀ¢ãz²óY #vÄxnŠ52Že€Ws-g/Î þø±‚cN ¾_TK’:Tw7š¦æ7J•NM_|¹‚.l Ü»—×>}“ûVDҮ﮸¢í"¼P€œ­ß3g#Þ»ËÂõ@,£o_ËÝïüÌæôÞô¶¸iˆ i“ð"cÌŠZEò’Mk(Š^³zÄ#3ooòìv²²Ó±Ûíèõz4 ôz.—ŠÝnÇn·ãr9°Zý0›.ðÍjLJ³„(O£þp_÷`Lfoüh-ñ/BÝõžãðïü¸×‡îµÆÝ¡¸‡/Ü=.ó=Ê}Á<ìe^Eцw^ï5˜¸¾`ΦtbÒç²ÅØ™—ÚtÓRIùs.›•$žéàaó·èé|u /ÌfÕ©>$¬ú‹´Èt u¼)z3ìdÙ‹_Ù¨yäsÑA~q=¸þá\sóo¼tû³¼ñVOº¾Ñ˽ZïKdL,±^ñ´l›Dð¡kxtþ\vÝöaa¾sŠÔ\ ¬îWèL?B¾„ùzäÇmµ’:R‡ê.U+h1Óòsœ+8+X 1ˆ½®¥EÏÁ šù³º¼Æ¥ªJsîxánZ ˜ð QPî„#fùY Ϧ`,”´”)Š‚ªªèÜÑYaÙEÝç‘ó˜Ù||0šL„‡… ª.rrrÈÎÎ.ö•““ƒªºˆÁh2aó¹ÀÀÌ;‚¸øxâ¢Z”ŒkŠÕ±‡µr —ºÒ¶³þ8D´£Ü̵:^FÈËÌ¥ÔŸŠb!<ù îzæ žíïËÞE «’MÑ x…DKL“°"A\´º+ê1‡[À~[wôÓù“ {~c}Z9µÁAR$œÜzkT3š5;ûÕ$Øý~jˆèËðD;«§/bÁ[ñ¾ôZ’ ›:RX=w3ºvCiï_ðî«`²!'ƒÜb‡-o¹Ž€®7ÒÛ{+ß/\ÃÒߎѯ7Qù•Üž@8‡Yñ×ñ"·,TRÖþÊ‚hÙÔ»ÌBC£öôlŽ#ûH+o³šÇ™,Ì~-;Æ–n?“ßñ(—½Ë7’emMLjúÞMêPIR‡ê–ÂùÉTÕÝ‚årKªË…–ÿ½ä—¦º×~if¢Ú&D*ûR\ø7‹Â¢aWŽ?D|…7ÂG§¡iz÷gnF.UÍß_éã+ƒª)«V8·š¨<òö‹N§#Àßìœ\Z´ˆåС£¤§Ÿ)L¢iþþ¾DF†àíe)¼kP,ͯbH³U|;þ}üGö§¹å«¦Of—­wkT~Dc-"õüüû Ä ))¤ûwä’€mü²7 Ââ8ɦYà€—ôeÕ¬ºënÏæ>5—öZÔ<Ç!~ýé ~=£¥µ`¡‰ØáwÐeæË¼øÈ+œ¼å2šû«ÙœBaF=CoëÉäçÞeìK9Ü6 5Aº3Þ›CÒ5ƒÜ‰tAô¸¡ãŸ~—Oã¦G[Sp÷|WÚ=ßžÂkjÌDµkïÃg³Â¸<"#g¢8 I9Ëc°x'qã•¡Üùå+t†3²o㛆ÈÜÚc2ÏOx'SF10!€Ü½K˜öåZÔĹ6Þ êI~yã=6„u¤ml(6}.Ç7Îç³ à?¨¡F8à8ÅŽ ëÀh'ëô¶-Á”?4½u01f0E_ÅÍm¿ãÍW^æëG®&:õgÞŸ~’&£F’Tß³éI’:TÇiù­djaÀsv"é¹Íò.hµÊÛ=“O;ˆOŒ!Ôׄzæÿ¸ˆ%šÁaÌþW20âOf¿ó^×  m¸ŽÓGHõëDß6èõAÄGêùù÷ï™ÛŸ¦Z*éè_ä37?#£V…1ÿ»†‚¦sOK­©’•±¾ðÈÀ Àh4ⓟð£UËxtz=yyvÌfªË…ÃáÀhУÓ_¤ÈÆÅ'žFÿÕdæ~ðßkfBðàÃ×{Ž3ÅÎwÜÁÆw§ðÍøu`$é†xÚÅïaÕÌ…L:íôø6ëÄÍ^E3™4RT·j®»—4÷)¿•MÔyŽƒ?óÓ!?º?Ѳð‚@z/~¢ç“w'2ñ¹d:k0±ÛÐÈ £QßgùØõ)㿜Êÿ~ÉBÃD`«+ùϠˉ6ëþo¦_ÀJæ5ºŽ«b Òܹç»ÚflÏ‹íüŠþ"‡<Á_à³wžd¢.K×ËbÊ]nÑ›‰>Š6߾Φ¸Q h\¤¶ê‚¹ì¿£}ö._ÍÏóÓTð §í•cyf̧†)vÌû€ÙG³Pƒ4ox†îj‡7.̺þbÂcëò÷ëMh\[†=ú·wÏ/egèKoñê›L~öQrŒ!´¿á%ž¼½9gG{ÖOR‡¤ÕuîV(òÇ—©îôåŒ5+H¼áTlXÓ~aÎçsIwXh×™Ÿ¸‰¾Áz4šqõSOâ5åNyŸœ XÃH¾:‘KÐáG§[ogÓ„©|›ÿ™ÛæºXºÆyc(˜dZÓÐ »/CAö|i1«?”ßNÑú_>¸B¯Xö;_ÅOóg3pðUü±ôW’’;\ä"º&Œù >v;‡½RÏ߸~m•UœÛ¹Îň›G2uÊ×5\"!ªæ\õ6';‹åK§ßÀAøúù—¹Mír°ëãÜ>¯#¾ûwþüJ5Àu”£¯çàqÌxµ'~Um’ÍÙÈ›#Ærðž)¼uy°göÓ¯#~øn*ƒ†^Ã^öçlé:-uHx¶óÕi€_/dðЫ±ÛíhªŠ« Ø)ÑjîyÃÜATléŠÊï†{ÿŪt‘ÇŠ¢ ÓéÑéŒ&óçÌä²þ—Wc9Ä…Ú¸~-­’øsÕtîÚ?WýA·ž—V蹋Î÷ܳ’ŽÊcB!ÎþE‹Ðk-jpÊ:×±eÌÛa¡ãËI…Ùõ*MËåØö=dɦï^ãGïkùàR¹ ®qR‡D=P¼Å¬`"ç‚äE“p¬SÎv/¬.šz¶ë¢ªæï[ÉÏÂèî¾X¸©¦¢i:i1«gêL`&„¢úÙ÷.äçãô¾"¾»JåÏweéÈ]I¾>~Ñq€Ùÿ»‡¯÷ëL¼šÿ¼q;-¤¿W“:$ê…ü.Œî²³cÊŠgd,2Ö,ÿ»R fûSó[Ìàì¼eª¦¡SPÜ-jeÓ4UÒå×# *0Û¸~mmATÀˆ›GÖv„¨6î.':½£ijñ ?,{°†j êÆ¯XvcwcjÎ=_/ãžj)“€ŠÝy/Y§¥ OVÑ÷^ƒÑDÚé4¬ÞÖü ÐÜ“;»#÷Üešª¡äÿ™ìùÂÜzÛí¥–Múj"”éò£åG)œzBÕ4N§¦`2™KíCÔM &0“ñeuƒŒ/õ¢(X­Þ¤¤œÂ? °¶‹#Ä98~ ›¯ï9‡ÍHuIEê4@ddÛ·m¦s—Kphš»%LE¯ww%Tt N¯+lÅ*êBƒ³É“¾*s_Å–)îJ& 1ŒlÙ¼‰ÆM›^б…çi0™BÔ½Þ@tt,Û6oÀjõ&4,ü¢Lï!DU¨ªÊ‰cGYóçJ’’; ªåM¼%uZÔ •©ÓqñÍY¹b9+ÿXJ«„6•˜ª5ÑGÙ”" @Š>.VM#--•µ›6ât:‰‰¿èå5C3!„¸ˆô¡áᘼÌlÞ¸Ž•Ë—Ôv‘„(E§Óa³ÙHjÛžàÐPœG¹ÛJuAeê4€N¯§[÷žìÚµƒU,#''§†JZyŠ¢`ñò¢Yt ±±ñ¥HQwI`&„‘¢( (‡Á…g)â"Ó@U]ç½€•:-êŒ ÖéŠNG|ó–Ä7oy‘ &DÙ$0Bˆàr¹p¹ÎÝ•FˆºDê´BT/é.„B!„µL3!„B!„¨e˜ !„B!D-“ÀL!„B!j™fB!„BQË$+£B\dš¦át:Üì4 M»ø“” QYŠ¢`01çÝVê´¨ *S§…ð˜ !ÄE¤i{)'O±ÿ^²²2QUµ¶‹%D)>>>DÇÄŽNW~‡š‚:½i% 'b?²ÍY±y¢„¨I¦ÈXü‡ŒÆ–Üóœuº€¦i¤¦¦––ŠÓî@Ë¿éPÖWÁöå®/ø™¢Ë)½¾ÌíJ®§Øvf³™¨¨&ÄÆ·@¯—põ…fBq¹\NRNždû?ÛHlÓ–À Fº@¢&©ªJjÊ)¶lÚ€Él&8$´Ü9Ê\.'Ù›V:é%š ¹ßf÷¡èõ5\b!ÎMs¹ÈØ·}ŸÇtÏÿðIêqÎy÷4MãÐýL&ââ[bµz»'S¯ØÑ þ?ÏfZ‘mJ>§èc­È¢â;.àÒOŸfÛÖͬüc)Ý{ôB‘Ï•zA3!„¸ˆœûöí!±M21qñµ]!Ê列ÕÊ– ‹(÷"Öép>ÿsš ¹¿áÿWÃ¥¢bÀ¯Ãši.ÏüßäÞç ÌNŸNÅd¶ß¼ü ¬Š­8G`–j©ùÏ-hõ*h +|nÁwÕý==œýî~ºû»—ÕJç®ÝYùÇvíÚA|ó–çü}ˆºA3áQFÜ<²¶‹ Ä™:åë2—kšFff&AA5\"!*/4,‚Õ+–»¯jË¡iöûñm&‚ÂóùF'°gÖ—ç¬ÓiiÄ””iš†¢(…Ý Q”shP¼µëìc­à9šZ$ ÓJü\"hÓJmg÷§( N§ƒV‰mY½|©fõ„fÂã”w+„§ªÈ é¾(êNWá1Ò}QÔŠ^_¡1.—«°ûbÑd6ź3–Ûµ±¼nˆ%7+2f¬h0V¬u¬HàV¬•ÍÝêŠN¡ ±z 9¹9ç}}¢nÀL!„B4h-cå¬,þ°r{.Üa«Y© ¬HV"áGaë\©îŒîòÊØ²úEΦ¢âGø}Ú6œ.¿Ÿ~õë$ÍøŽÕ)5p,!„â|Ê˨X‘Ä%öQêy¥–—uœ"­k2EE½$™u‚Jö¡Íüµéµ—”ÚÉ‘Ÿ>àó_vp¦&>)›ðѧ ±™BˆšP2%~ŸuÎ ªü]•ÓrVlÌ… CÜídEþÕdê•úF3!*ËuŒ¹OŽdÄí¯³2½¦îXå²eâ+¼=c'9µt“LËXÏ73ÒäÚQ\XcK aô½í ü6}Ëì]¹ÿxÂó9O²ü£q}¿žôì9€ÿžÈÚÓraâ1äüˆz¤â©òÏ'?*xkó¢cÎ@SÕÂର‰­°[cu•Qx Ì„¨¤Üóøñ€«c?üvgm¨F¸Hùk.km¹¦G55äßÔä2†Æg²lÞ2¥×Fgg÷ä±<9å­ï|Wÿ;‚¨-Ÿ3ö©™n„NΨ?J&©Vîþ¨å'T ›Ü þ-6u%G¼ Ï&É?„¨ -µ³V`ïxDLgÜÂyì8šÖ^ù«373í½¯Y±ë0©¹è}iÚ¾×J» ‚?7ìÝ‹ùjâlVìIÇe &¡ßF_߉`#hÙÿ0óã¯ù}ë>Nek`Žáú%`ç‡Ü;òC¼.yŠ÷hƦ—Fó¶s ý·;¾ `ßˤ=ÃÚžãxã†&ÊÜß3 ‹T9¼|ŸO_Âözÿ8z]·ôŠÂ\ò&œšÁæ¥{зz˜Þî•y{à—°óH Ù.ÀA§Ahvr%¿ÿõ'suøÆôbÄ}·Ò+Ü88²è#Þÿq OfâŒÁI œ@Î_¿±jë1²4+»çÞ»mQ@@rïf|>i)»r:l½Ø'Xx¬ìÍL›¾‡ aðï›Úà…F¢÷n®| ßï¼’‡Z™k»„ ›œQO(Š‚ªª…-fš¦]¤v)­AÚÙ°«t†ÇêlÕžBÞD+ IDAT3!*Áu|9s·Úèó\ZùåÒ|þdæn¸V]ýP-ï›7Æ6ô^îleÙ²ßgüÀëÿ=ÍS¯ÞA¢·‚ëį¼õÒdNuÉ¿FÅ¢?ø¿Ï›¶×xihºœƒ¬Y³ëà{y¼m º<'Á h|=OIÆG½O(fÎ?øJ+s:Ò×|Äsm§Åõ÷ñl‚7'WNãÓO^Çù·Ä•¸ˆ²aã!°k£ðÊÿpÞÎÆƒ:úÝû8][ý _ü0‘-m†pó˜«tîcÑ—ßññGÍhõl?‚õ*{6³7/‘ÛíK¤þ4g}Á܉ۈí?‚»‡¡;ºœÉ“'ó^D¯ˆ[L+‚ì+ÙrÂAr3cuŸRQG8ޝgË =cpßQ°µ¾”xåwÖoIÅÕ*¼ÆZrEir~D}QÐRV´Å¬øX¯"ßÏnqŽtù%²2Và«X×ÇÂò¸÷¥)UOÔ Ì„¨0;û^Ä¡ðþ<ÔÔ„^׉!ÉSxkî*NvHHáU‡‘Öhׯ$Ñ®…•§û–ïV£u_ûšÍVŸËyñ®Äš€æQܱý/Æ-_ËÉÁ„`"´MG’,ùûÌvf– ¢š4q·Œå/¯˜ûsgþŒÕh]羡IXhÙôNþõ4Ë—âÆ¸XLEž­åœäd®žÀ0[‰‹+š·mC‚¯BB´Ž Ë^çhbozu CO¡)+X÷Ý:åõ#¸ µËMRÛÂô罋eÿ]K\ÏÞt‰5ARì®àýõ»8sM ü"ð'ÃiN@³†Ê•qŒ l„ùžýØRÌóµG3p"þµIΨ/ ž²Z£ ¡ j©RJýP Ž•R|9(ùÿ+îTùŠR°¥¨'$0¢¢rvñÓòtân¸„P=€ ƒºb{q1ËŽöexTÙAƒ!8‰v!ßðÓ–ãØû8¼+RðÌí Šoè{‚3.ò³‹Ì~œmÇ ûàkܵªø*ý‰3¥ÚáTG.vŒXŒç–jô%Äv¥dáôè±…ÚPœÙd9ʾ«g°…b#‹”ìüŠ™€ /؆<Ѓbô€‹\»¤fBQs ±üÇ…ÙÅn©*ÑZWlbëüø¬°%MƘÕ+˜ Q!g¶,äÏ3Nò>{ˆ‘Ÿ_ûÛ¯¸ò–ØrÚst ùo´ùo¡‘×ðŸ»à_4Î1ø| B:.g:5å.GàeðØÀ°bo:¯FXJl­3Z0á ×q® kzŒzÐ\g?&½vÔr>7ƒ=®Â  &=¨êÙŒTŽ\œèñ6Êýö†Lo Å—3Ë8[Óµ¼Žeo¸¯|˜Õ29?¢¾(ÚZVcX™å ° e]Æ—ÕKò^)DE¨i¬_¸gó<}K«ü1NŽ-z‡÷þXÌ®ëbiUÖS3v²ù„ ƨX‰Œ €ßv‘n»šÿÒo¬e‡> F‹R2 [’Üôx7ò† {9åèM€©Ì'—f ¡e¬Û} KX‡üÀò)^Á4²¸8z<Öí’äL?B:þ4÷—·«†ÌÖŽÛDÖü±‡ÜΉXÐÈܺ„j0W$J7¹Z&çGÔ'Uê²X}¥8;†¬ŒÀ°òs­‰º@®t„¨WÊZmWhuwZEûëÑÙ¿+K—°h[&-›ØÙ:g: Ï$fHaͬ¯ÙiîÀƒ]¡GG³Ë¯¤ù/“ùøõÏI»²M|T2ŽGß¶]BÊû“4Ö:¦üÈw¿Ó+$™QôèE“îí±-ù…O&†sm—H¼{9œsž¤¥ÇÕ˜=a2¯|Ë5=b ÐeqüP-ú÷"ªd€g ')JaÝæCä Á»Æ>«TÎìÛÆ)SSBd|YƒfMä¦ë¢ùé‹y£ñ >À¬·~%7ñ!†ÇKÆ¿Z'çGÔ#u!±Fa«žŒ1«W$0â¼\œüóvë[ð`kßRo¦¨t ü‰Ÿm&ã.÷2£}'?~ò§züc»sç³·Ð5¿ß¢>t?§gÊ×ó˜>áwòKP+.ísŽÀLOhŸ»ùÏûLÿê-þÀDX×;h{IcµÉã£|6ókÆ/Ñ#¶Ðx:4ö>ÇD…:ºÞÇ êt¾úa>­ÊFÈÌ¥ÜÛ«'Q¦¯RçGbhœß,eGV;ÚùÔКƦ%{1%Å#mJŽÔ* `éÇÝÏöãî2Öê‚ú1nJ¿ÒÏòŠfÐ#o0¨Ô+±—ßÏË—ß_öáÊÙŠ™ˆî#y²ûÈrÊY”žF]†Ðî› ü°â$IB°¶Š)SŠlbl ï|Í EKÖîq&nc¦å½3µÈúÒ¯ÕLó»?,ÜÆqàWfÿãC÷g±ÉMAa¡Ç˜wè1¦¶ "Ê$çG!ªä)Ö„â,Å7™¯Šbßô)üyú\I@ª‰ë8¿MœÏé„ë_^+„BQ?H‹™¢‚ŒD^~·§ÿ‰¥&ÒóªàÛrw÷éI#É „BˆzN3áqFÜ\‘®užmß+w1¢¶ q1-œQsÇš=Ÿ 5w´j§( :U­VF!ª¨"õTQƒÍå’´Âãi.™SÔ˜ 2uÊ×µ]!ª•¢(X­Þ¤¤œÂ? °¶‹#Ä98~ ›¯/çjWcXS2ölůÀš+œ cßvÌãÏY§…ð˜ !ÄE¤×ˆŽŽeÛæ X­Þ„†…£ÓÉð^áYTUåı£¬ùs%IÉPÕò[ôzþƒïbÿ¤h øÆ´FÑKcáY4—‹Œ½ÛØ?á÷¼rÎ:-„§ÀL!."½Á@hx8&/3›7®cåò%µ]$!JÑétØl6’Ú¶'84§ÃQî¶zƒßv½0yãÐï“7ãã,©£Œ˜#ã=Ÿ¶ÝÏY§…ð˜ !ÄE¤( ( ÁÁ¡„†F ƒr„ÇÒ@U]ç½€-¨Ó>mºãÛ¶—Ôiá¹*X§…ð˜ !D p¹\¸dº¨G¤N !Dõ’B!„BQË$0B!„BˆZ&™B!„BÔ2 Ì„B!„¢–I`&„B!„µL²2 !ÄE¦iN§ÃÁNÓÐ4­¶‹$D)Š¢`01çÝVê´¨ *S§…ð˜ !ÄE¤i{)'O±ÿ^²²2QUµ¶‹%D)>>>DÇÄŽNW~‡©Ó¢®¨hÂSH`&„‘Ëå$åäI¶ÿ³Ä6m j$Â㨪JjÊ)¶lÚ€Él&8$´Ü9ʤN‹º 2uZO!™B\DN‡ƒ}ûöØ&™˜¸øÚ.ŽåòÄËjeËÆ „†E”{+uZÔ­ÓBx Ì„GqóÈÚ.‚dꔯË\®i™™™Õp‰„¨¼Ð°V¯XJùÛHuIEê´žB3áqÊ»ÀÂSU䆂tõuN§«ðx1©Ó¢.¨L¢¶É»ªB!„BÔ2 Ì„¨aZÆf¾ÿ.3÷Ùk»(•â8¶œo'ÍeWNeÒbkdlžÍ·K{ѳikdíZÌ´»©T…B!<€fBÔ05g?kÿÜÈ¡,ÏìZ¡ÙSصîovg/ŸãÈRæüô7Ç•ØW澞ð=«UâIU‘³ŸeS&0cwnÍO!„¢šH`&¦ìu¼=’/¬&³–Z}œGóî_°ìdUý4…?”žÜ1$ËEx­àx£’2X4e§$ùVýã<ÉòþÅõýzÒ³çFü{"kO{æ ŽIÎBT‰$ÿ Z£ËÆ0ú’@ô tB‚õçzЍ¨Ü=,øù(aý¢¥µ†Òa)~´ÖïçdÉÑž 2ÖÌqE °³{òXžœ’Åe÷½ÀÃÁû™õÎçŒ}ÊÆ×ï 'R>Íj™œ!„¨*y« šWX­Z…R:ÓÈÙÿ;S&ÎdÙŽTÆ š÷¼šÛoîMÓ¦'þüž/¾ý…ÇrÁH›á3öÊ0N-|‹ÿ}»‘T;` &¡ÿHÆÜÐÀjˆù\é[˜;ñk~üó ™X ow9·ÞuIþz´ÌML?™{Ž’–«BÛàÖ»‡“Ppp'×Ìdâw?³îp6`ÀÚŒ¶WÞËè¾aùÛ¤±èÙ;X@nyçEz°›)OÝÍû§sÁDË>7rψK-#þÉ;øë2B¹¬s¨ûÆ•ÂÊ/'ðÝÚýϰzü[ôaHg#›ÿƒM3pšCI¾ò.Æ k…MG¯Ç‡f=sià>–.ý›=§[Ñÿ¶1ÜÔÁ`››ö ½ï¯¬Xs’«¢"äM®¾ÈÞÌ´é{öÿ¾© ^h$zïæúǧðýÎ+y¨•¹¶KذÉùBˆ*k0×,ׯ­í"ˆ"’’;ÔvÐ4µØ„“ŠN‡NQp¥,eüsŸ³#z0·MÂ/cs&}Æó©^ÿW‚ôg6|ÁǯÀÖëFº­ –œT²ƒ1 Ç¯un}x(Þ)çòÙŒ÷ù,îëì[µÛ0ëåW™­ëËíOÜI¤só?›Ê›4â­'zwœ­[â7t £[ûà<µ™¿žÃ[†óÎ= ÔiœÙð%ÿ}{–n7ðàÍÍð¶ïgÑ§ÓØ°;Wß°üþÍ>tóÚAg&0PühsåH.‰4“µû7¦LŸ·›òÒ’‹´];I·4£e£ü¨MÍâÀæ¤5¾žG†ÆaÎÜÁÂ/¾gòŽzÝ4ŠGš˜9½v:ŸÏxoÞæîf´Â×s?÷&úwp “&ËÄ€N\wÃ\oËfëœ/˜3áKZ¼û/:Ù0EÔXÇ’ÍȾ*_™»¦^p_Ï–3zÆà€‚­õ¥Ä+¿³~K*®VáeÜ`5EÎBT]ƒ ÌÀs‚†Î“‚äCSã¶©g›;?Á‡µàØ¢™lÔuà_ÜHG›´&ÖzŒ‡Þ™Áχ»pCd+¾[Æ™æwòâ]}hTâŠÃÚ¤š¸ŽkjãÀò'øuó ì}«ôG—óÏl~<ÁõoŽ¢O˜ˆ%lÔzîŸðÒ#!­Û“ÜÆ$Ð8g#|³‚½9= ´œbåô¥¤5ÅÛ÷$T8üÙé3ÅŽdÀ7¼1Mš˜ —dЈ¤nIöU 1Ýæu¼·jéƒ#*6bÕEúátð넉lJ¤mb &šÓèø ÖÏŠ Ûe—dâŒlZö*Û¶œÄÙ"*?H4Ò:™¤ ´Ž"gÕJ>R:Ò·G{ühé»—åϬdýQl&PL†ÙP·åŒ |Ô»\ýåÊ8F6ŠœPÅ܈0X{4'rá_›äü!DÕÉ%‹hЂ>ÄýÝ ³àl˜µln?Q×çSÐÜ¢`‹ë@³íPZðQ6†Ða-ð/uµaçøêLšµ‚í‡O“«·b²ƒ¡yU“i89½ç 9aꣷ2µØº0Nf¹ÀRò9z|#ƒ1ºÒHÏUA”M‡!|H"AU½JR,GØ`ói²]”ÌT9v0Z0–ÛbeÀ7ÔötÎØ5°(`ð%ÄþÉÈC¥ŒìDŠ…À@ J%[?=èm¡ØÈ%#· É€‚ÑËŽì’6_!„u„f¢A³4jBl\‰1fÚyæÓÜÿ¨€RFÐá80‡×Þý]ŸÛù¿;cñç‹Ç¿Ïêj(¯¦i ÄsÛsw‘àUt¿P=¤—~Ž¢Ó£Ë//š†KAGuôðS :PUJÇ?:Œ^&pæâ8Gp¤3éQpâ*LܦǤMÕÊØ'€‚Á¤wS+(ƒ =jÁ4œ9Îó…¢®ÑÛBñå Ç2Îv=ÖòR8–¾áUk‰U'çG!ªNÒå Q’b%ªEZÃî̳ûgv­åÁ´lì…b ¥e(]¿´iÙóŽnã(1 ¹¶mãšÒ4:–HïÒ‡Q+Ýšc : ‹v”ݹDDFYø‚¡Qˆ±qàØ¦Ýd”“ÅZ1xaÄNf^UÒ\ëñ‹ð…ô£¤×ôìšÔc™(øJß©zÃÖŽ[.[þ؃{–:Ì­KØ©Ó.!PºÉÕ29?BQu ø&–FÖ®ÅLž2ŸÕ;RÈŒþQ$ô¸Ñ×·+£{šh8Œ4p5mÆûï|íC’ðMßÀœICÛ{èi}=¯îÄì÷¾ä•O²¸ú’hü´3œrÅÐ94ŽF,`þÌßðíÞ ?] ÇsÎî]gñÇGÉeçŠ5ìmz Ѷ2*Û™}lÞèÅÙF1>M[Ûê*.ü‹YãßÄûúËinÁyú)~¹,)àü­`†0zjɬ/¾`ü4'Wµƹ«O­ò7 Œ¥™5›5ÓçðÇÕ­0§§`JèI\¥~‡züãâñÍÙÆ?)Nj2W¶ã(›¹ЄšÊÒ/j€5‘›®‹æ§/^äÆ0 ø³Þú•Üć/ÿjœ!„¨²˜©i«xÿ“ØÔëFw¤±FÆáìÑ|±HPÖàéõæáÿª|=q&Ÿ¿>§1æ=î⹑=òÇféðï2†\ß1qÆlÞ_ž Š•Æ—ÞOÇ;†ñÈ-i|6ë+^ûÙÝœfò ¡y¸ ø¶ãÆkZóîìIüÐ¥=¶µ–.À‘¹¼ûÚÜb‹âƼÏó=š1üé§ðš<SƳЊ5ŒvÃÛÐ') w¥õ÷yˆ'³&2yÑ$^ÿÑŽWh3üt èò£¯VÜ+y.ŠqóH¦Nùº†KÔÀåýÃ'¿ÈŽË_ã•«ª3€rpðû'ù÷/-yúÍ;i]ÍWZ:«ßz” gnä­gúQSs…Ÿ«Þædg±|éïô8_?ÿš)UðÃwS4ôjö²ÇÛJuÍùê4À®ÿЮcg÷XnÜcº•‚äZþ¸ç’ß iÿSðoÉÇšª¡iš¦æ/ï«ÈzõlYŠRÅ=½N‡ÕêÍŒï¦2dØð ÿ‰j³qýZZ'$ñçª?èܵ;®úƒn=/­Ðs/œßpo(ý›Ä 6þ¾žÑ 1•¸htaþk/1õDk®ºóQ5ÒÓt4öRp¥,9ÏW*é»7°+'‘[éM„>‹ƒkçóÝçÏqÐñ O Ãpžù¨‚dôŸ¸(4²÷üÎïû¬„‡úcÕÒØµl:K2s]µ·0‰8‚n‹ßæ‹ù—ñ¿ë¢¹¸š4²·þÀäuÞô¶G©) „B!ª’³ò6 #nYÛEh@œ¤ø‹¯ÿÅŒ‹zœ˜÷ÙE=@1?N«¹c¢(èt:Tµ*Y.…¨©§R§E]"õTÔ%õŸ«æ²%{ {óöpÚ™@€ÁŸf–h½±èJͨ[)Š9˜–½®¥eÏ+¹bÖ <ýýÇÌéü2½+ŸËÜíOs÷UÖд ÌGÕ@Éø2Qß(Š‚ÕêMJÊ)ük»8BœÓ‰ãǰùúž÷³Lê´¨+*R§…ð=’é@Þ¦¦LeMÖŽ©Ç9a9É ËIŽ©'X›¹–i)Ó8h?X=S,4NN ˆTöŸÖˆiŒÅ±‡µrKlW9®Ê<@.×ÀemJ3?sÕç£BÔ z½èèX¶mÞÀÑ#‡åî­ðHªªrìÈaÖ¬^A‹– ¨ª«Üm¥N‹º 2uºÖh¹ßþ7eKÜ(n1;w€i È5ä²!t#™ÞYØg´3Ø2}h{*‰§0(à ¢L+µÿ¼Ý?ðÉbñ ±„ù™PÏbÍüŤ(Ñ\aÆ0”+"Ÿfæ[oa¹~‰a&òRÏà×öbÏ7ÇyîƒYÂôÙ>t‰ñ#o÷/Lý%ˆaWgÓyæ£*Ùfæ84‡ž™…zÕ <7, #9Û¾âÉ—W9zÿꈩËßá‰OÓõÉÿq{«ò‚D!DMÑ „†‡cò2³yã:V._RÛE¢N‡Íf#©m{‚CCq:ån+uZÔ•©ÓN#sÇ|ùå,Vl?E.îärmúÜÌý7u<ÿœ¸y{ùþÍ·Ø7äuÆEzyvk‰¨˜åª¹üœñ3¹†\~ZŠ¯Ñ—.t¡Íاìc£m#¿Y–pé¡Þ,Nÿ™A#0ë*žóÍ¥øbMû™9ŸÏ%Í`!8¾ 7=5‚~¡z ×<ý¼&OeþWï0ÏÆÀD®‹éLóÈóÍq•ϨçÔªi¼3# ,a$Ë]×4Ã`ªê|Tni[ kÒ^/„çP…ààPBC#.r!ª@Uu÷Vê´¨3*X§/”zz9o?óë‚{2âþ®4±©¤ú‡]š^ wTЍ ̶doÁ¡:ø3t z£žbhMk:Ñ +V2Éd“qk­¡Ç±îlÎÙLïŠÏ©féÇOôãÎsl£÷mÅ•÷¿È•÷—^çÕ¬/w?×—»ÏuàŒyùJÂÊùãÔû¶fÈý/2¤å5F åÅ/‡Y¢àÕê6ÆOº­è ê9–OzV`‡Bˆår¹p¹<°+Hê´hèì‡×°ÝîKßûÇpmK‹û¶xçnôƒÂyÌ\i›˜õé—ÌYu€L¬D´Äí£‡ÑÆïlûØáoãÖoÝ?·xpOu°Öü‹Á#³ö8uNŽ{'˜`|ðÁŒ™¼ü.‚fÌøàƒ#Ç|ŽãÒ¹8”w°R™B!„ÊØŒ`–²þçµéVzN\û~f¼ð"3tý¹û™{ˆ´ogîÇ“yí½ Þyª7©s |„‡z7B¯9È«ÔqDÃá‘ÝYó´ÃúýxlÚ{̼ämú Ò‚»Æ!Ñ¢¡iš¦¢aÄ7D''‰¨Ã<20‹0E 9¡u^k¶›·àÀÁINlYd‘˜—H€.€CDmY!„B4dŠMÚ·¡Ñ´ùìMÑÓ‹¶…]¹ Œ³BA`¦åiN^FÈÍÌCÅCljå‘Y¤!’4-Ûrnå9ãódè2È#C~q8É#›jãÖÜ[±¬Dk&0Óì)ìÞ²%6™Xß‚?!§–¼Î“œ\?îI„JŽT!ÄYš¦át:Üìò?…ð4Š¢`01çÝVê´¨ *S§/DîÎoymâ ÷7ãÊ8À_³pJ‰eX”kÀpG­bÆë¯à=bÉá쩇9åÛ™~IþèLÁ´ˆÔóó’,ŒíOíi~íèç}QÊ+<ŸGfÅ@¬1†cÚ ÞÊz‹Ï½>g…aZþpJ:º9»qWΘ0nE_C/Åyt1ï¾±œö/¼E¬¯)©“o‘‘üKfäB4hš¦á°ç‘ròû÷ï%++UUk»XB”âããCtL!aáètåß»—:-ꊊÖé £¡*~x§.`Ƈ?äωëEH‹nŒzþV†énxá9¬_LfÞWo2?ÎÚö×&Ò7ÉâOç;î`ã»SøöÝu`$ñÚXºÆyWx>[Q¿xd`à£ø¥ÈPÎð¼ãyPá°á0‘ÎHpA®’ƒ¯Î†K-—VÁ·Ým<ß®–‹!„ð8.—“”“'ÙþÏ6Û´%0¨ÑE¸@¢jTU%5å[6mÀd6ZîeR§E]P™:}a¬q—sïs—soþ¤eîæƒüÖãüÇz¿D†=ò*W©EƘteŒá½¹w\/îÑJ¬¯Æ’ŠºÃc3 ̧ºX IDATª§âĨ3L2yZ.ÕŽMõF¡ -TŽ“ü5ãK¦-Þȱ\¶¦]¸êŽÛ¸"ÎÅ•Æßß~Êw+¶sàtàM›Ñãx¤@‹ž½ƒE4á–w^äÿÙ»ïø(ª…ã¿Ùͦ÷BJè5t,tPPTTÄØÁ+¼zE+– Ø)ª‚\ä*TA)‚€té„$¤—Íî¼$$$B™žï‡|HvÚÙ™Ivž9gÎi¿÷5¼ådÐ{/pu fû—ÏaÚ¼ŸØïÄ\‡ö·÷çžöUñ2À¿†O'ÏäÛqdaÿÆ5<ùüÝÔ·:cŠÈ9•ãt²{÷N7iN­:u­.ŽH‰‚CBññõeÓ†õDFU.ñ"Vç´\,J{N‹”å:˜8\p™deeÝÁY¶6ÓÙ<ëUÞþ9‚›|–ËÂRX? 3_ûè·ÿ^Él[½žÃQ½xô¡úº30«aOðçŠOqsŒl^„†ÚaoÁ•»Iú}2#&m¡þíƒx!Ö¸•s˜2ù ¼ª¼Î=µ³X3m_¾ŠþOw¦†wqGíDžŸ&Е>}ûY]‘2™=kf±¯›¦Ijj*¡aa¸D"g.2ª2¿þ²œSÝóÔ9-“ÒœÓ"åE¹fç‹™¼ŽùKÑè¡—¸ãª`  fÿD~ü)?ì̤EÃÜù|cšÑªi-òŸ&s¦x]˜ÏãëK/¸rWËçÿŠyÅPÝÔ_T½¿=Çòåû¸³fÇ’œØCëÒ´QmÂíPK7+éW¤¼*Í 5õ’‹Íf+õób:§åbp&ç´ˆÕ.Ù`–÷7\.޽÷}ß+<Í‘˜yvm{³³ù¤ï}þ« O²IÁe¯Ië[;òÕ˜©<ñÔjºtëN÷ŽMˆôÒí‘KÑ%Ì0ML|¸|à3Ü^Ó³À¯@ RÎfå˜@h—Á<Õ-ªÐN¶ù„ã ŸVýycbGV~»˜…s^gÉ‚6üß+ƒ¸:Lýð”9YµoŽ6æÖõðUž>wœøñ³ß éÞƒf!çùwÁÇo Àݱmô{'"""»dÛ!xFÔ%ÚÈ`÷>ƒˆ*U¨rü«2á¾%_¤>8È&5ëÕâž•hP vÅ;ªàº«êu¼™³GpÚõ~ŒÑo<@ý”_ùò÷ôXªÕ\ýi4ý|•o—t4œX¶€ÿ­>Dö-Ûùä&}ßF~ûóNËÊÃ%ï2íû¿I¹ÝQ.âÿüŠISV¯_<±Ø%[cfµàÖŽ!Œ\ô&cm·Ñ¥aŽÌ8ö%W¥Cç:”4´ŸGhmjø¦óû¼…¬¸¥!^Iñxƶ£NÁ™ì‘´½¥ &Ì`ô»™ÜÚ¶6!¶4ïË¢þ5í©êHgÇÒe«Iå@©»¶ï¶èyé&åsÅÊöŸ>ç¿_¯dã¾dr¯Ð4jÝ~w¶%ú´¬X?&;q9£ŸÄ¦˜{;âZ*]ÊœL6}4š·s2©q%¼u3yŸ|¾—˜ÞreèxÓQt¾ï:¾~öSloÍêµüˉcùÔQ¼óÙoÌò¡ÚU}xòé{h¢¿œå‚ŽˆÈY¹dƒ†ï}‘'ƒf0÷ûyk ìT»²­;–ÌðiH߇®å臋˜øÚBŒÀ:Ü8äÊÂÁ !W âe÷<>þïb&­JÇÄAp­Ž<ܾUíiìß¼”é+ävâ]‰ØîƒpY: :®D~üã–'SåòëèsCMBliÄÜÍÖ¸쥺6°zL:'û¿ÿ‚M¦¶‹øßÎÜW×˪Â\@.â[Äf3·­tÁÖôŒéÂMu3ãËMÜ^¯þú,DzÙ1cÃg¥ÑeÐË<±‡/ÆNcÈ3Ìß‹*—î§Y9¡ã#"r¶.í?•ŽpZõL«ÞÅL³ÇpÇØ™ÜqòB/¿‡—.¿§ÈëÏ0kV /*_ÝáW×[[íyöœEÙ¥“¤5ñþòêß=’áÝ«PðÉÁëó¿ÉÜÌÄGÿÃî›ÞdôQØ×áÅ b5ž{‡A ½Iÿcd‘1éÒÙùÝt>ü|%;’\xFÔ!&Ë'ÖïJÚÄ¢fò¿Õ{IÅ—èݹ·OšÛ¡„1ñ†v;)€˜é[Yøm<õîJ›e£˜ûù:n}ª yÁLÝÈœñ3ùeû~2M°R½eWn¿û&Z„åÿ:›¤ïø–?ZÀ/;“pyFÛµÝ~9pîûŠIS¿ãχIu=ê^•÷K°í=î—ÛŽÏ•Ï0ñ‘üùêC¹5i/^[Žì]LâyÖ´É›wÄ@±ë»—ºŽSåWˆ;™Ëvboø8õýr'fíú/cßÿ‰mâIwÞ•¹üúk©·’ÛJ\¦ÀZíé3è^ÚG;'¾™ÄÄÿmbo\*9€#¢)ÝzÄ’ñÛ¬úëi¦/Õ®èÅ·¶šw¨Á´éËØžÑŠæ¾e=ÿä¼KßÈœy; »ù]†ÝÕLûíàö¡³ølÛ <ÖðR¸QŽéøˆˆœµK;˜IÅacÝWÜ•¾ ‡²³ã"aÅD^ùè/*wéËã­"1­gÉÜí'fÉþ‡/F½Æ[gîúAªäüÍ⩳yëÝpÆ<Ýž0w câ´-7I,âw.gðÕõ©Ú’yc¿ä—¸Ëèž×žÑÌ:ÄÆû ¸éal@Nü~œÿ_Þx1‘g^{€Æ~®#Kóê Ž^Þ'î®}ï|ôÁ8Þ xWoªŒóÈZVmsÑ©ÿ“\å ;ËŸh8PívžØìþ‘x•â©Çâ×wš±üê¹HË>À†}&Q·UÅ'/´¹·°a¯®¥u¨‹C¿~ÂÿýˆMMn¤ïÀž„æìæ›çòþ¤4|¡+v7É;7²+«1÷=Ù™*öD6|ñ‹>ÚLíkúпG¶ƒË™1cã+ÇòZ¯ª8°P«!aÙ+ÙtÄIóL°¼r^ǦobÛÕƒ€F©küȺM ¸F_°šV9™ŽˆÈÙS0“ŠÁÏŽ#&öjM¨|îR¸Ž°|ázr>ÌûÚbšÕÃký÷lIÍ%cëþ··2·¿u7¢ì@m¢î^ÇÿMø¿’ÛÑ.¯]lÑ1ñŠÝÖâ-øµ}¾6<_OÛÀ—ùê‡}t¹£z!ÕTjÔŠM¼¦´¨ïËsO}ÊÜ_o¦Qçö,YÀ_þÝy¥7j{õªòÀ–ß¹| q=*@ [5'6¿*.$>ï0ªÆÄ¯¡+2Bß)YŸë0‹O5–_Ú…öƒ™G\¦Ð¨€"oÁÔkÖ„Ø@ƒØš6Öÿüw ýeQ؉%2þÖÎ]˾¬®Dä×vÔ¤i³X¢ìPÇo;?¿¸†:í:Ц¶'4!{õ/L\·”[«j€GPe‚9Æþc9p¶×ËyãJ>D2DžøØ2¼Â‰ò‡5“ÉAþVÒñ9{—T0Û°nÕEóÆ(fÀÓÌÍS6îÝ^|–ëCÎpµÙGØz"ÛÖ&°ØgÔrHܹ— 0ûÉ{™]hZqi.J~`±0羟øvo%ºaɦÃdwò`ÿöHøŠçïÿªðŒGHq‘ÌγÓåWdv·3“lx;Nñ  #Jþ°=> `ÇN@dFN:iÎâ»qôˆ$€4âÓózQ5¼ ó=)d¹;Ú†à‚9Çß䔦8Øl¸rÎp(‡ÓåWdoùl"¿º»á5S+°¸eØâA^Êôy_óÞŸa^A•iÜ<"÷Y0Ïôzî|fÌá«YãøÚ †o-z5¡Si‚™™Ä†oþ$;æ.Z†ÛN¥ËÚQmξùí(W_–÷V³·ñ¿ÉK8ê´\ûj|á®Èk·h¼–¡#ìÌšù%ó&üHàÖîµ;"˜Ù‰ì4€~['2ïã1¬À“¨+ Ù•ÕoÔ¡w;™úùLÆýd"ëÒªšß)D?ÍX~Eï¶ѸmMr>YÆßi-hq¡sãÏŸváû(uÔU~9çIí»ßdTæHƽ÷ K²½©zÕ¼9ìªê“¬Ðñ9[ÆüOg™×tïQª™ùùGºõèÉ’Å èÖ£'+–-Õ³[rNõéÛÙ³fZ]ŒrËÿÏý{6O¿Ëà&%ôr‘2“cÌà »ã F\{a™vþóÆÿ@£çߢƒ²ïÏS·éi,_ö#]»]O`Pp™·!r¡üwîl®¿éœÙÙÅN×9-›ÓÓÛÿÞJ‹ËZç>7Nîó㆑?ˆhÞ³ÍEÿ?Î<ÞÙñË‹ülºMLÓÄ4Ýyÿ—ôU`ºûDY 2 ÃfÃf³áëëÇü¹³¹ñæ^eßArÎlX·†F±MY½j­¯¸šÕ«VpU»Ž¥ZöÛ¯Ÿâ†·ˆÈd6çΞUÙ=o«OÕ È9â:Ì-&1¶77×­X!WDDD.>j` "儃*ÝqÒj¼KÙgæYqC`ƒkЩ'µ ¹À̤ÜéÓ·ŸÕE(÷vîO« q>}=ÿÂmkÁb&œÇÕ†ÍfÃí¾µ€"g©4ç©Îi¹˜è<•‹‰‚™”+z¾L*Ã0ðõõ#>þ(Á!¡VG䔎>D@`à)Ç_Ô9-“ÒœÓ"å…ž19ìvjÖ¬Íæë9x`¿îÞJ¹äv»9t`?¿ÿú õÄâv—<¤¼Îi¹œÉ9-R^¨ÆLDä<²{x§7¬eåòŸ¬.’ÈIl64mÖ’ˆÈHrœÎçÕ9-ƒ39§EÊ 3‘óÈ0 0 """‰Œ¬ lðl‘3d‚Ûí:í¬Îi¹h”òœ)/ÌDD.—Ë…Ë¥¦4Rqèœ9·ôŒ™ˆˆˆˆˆˆÅÌDDDDDD,¦`&"""""b13‹)˜‰ˆˆˆˆˆXL½2Šˆœg¦i’“ãÌíÁÎ41MÓê"‰œÄ0 <<x8§Wç´\ Îäœ)ÌDDÎ#Ó4qfgw”={v‘––ŠÛí¶ºX"'ñ÷÷§f­:TŠŠÆf+¹AÎi¹X”öœ)/ÌDDÎ#—+‡ø¸8¶lÝLã&Í ×‚”;n·›„ø£lús=ž^^DTŠ,qŒ2Ór18“sZ¤¼P09rœNvïÞIã&Í©U§®ÕÅ)QpH(>¾¾lÚ°žÈ¨Ê%^Ä꜖‹EiÏi‘òB·¸DDÎ#Ó4IMM%4,ÌꢈœVdTeRSSÀ(yÓr1)Í9-R^¨ÆLÊ•>}ûY]‘2™=kæ)§«©—\ l6[©ŸÓ9-ƒ39§E¬¦`&åÎé.pEÊÝP‘³¥Û]"gÀ™Ä±ÌòçÍÌ:Êîqd–³¬Mg*ñDz(M±LgêùÙ×fIIÙ¥*ƒˆˆˆÈ…¢`&Rs7Ÿ ȨŸ9)¸Ž°dÄ`Æ®M+÷õ}À+ïýB‚•Ï9›.²³  ›3†óÜ'»È>íÂYìüìe^˜¾…Œs\¬¬syñÅ™lN/ïGQDDD.%jÊ(”IúΥ̞ù%+¶Æ‘…ÀªÍè>` =ëø”ü °=”Æí»UÃ÷<='œÃþÏŸæ©Ïz5¨ËÆ>P¯ó²Í²¶}Ààדxxü`šzŸÏ-eò×»O25|¯ÝÙíéŠ_ɬ½¹îåúøœã’yÕºž›#žeÖ×ór(ìçxýråı|ê(Þùì7fùPíª><ùô=´ Ñ=ÆrAÇG*ðº·ÃÈíl'ç0 Ìr«X΄‚™THY{ðêˆÏq¶½›Á÷ÆN2wìÄà8uà²Òø†ÛÎ{ù5îå•!—hؼüÏ{(»ÐÜ9®2~\äpxÅ×ì«Û‹Á•ÎCl²…Ñú¦&|úáì½ö.jœij‹d³cƆÏJ£Ë —y{!‡?Ä[ý¯&÷fm4•«×Ï›!“ãgZÔ3¼~G 3•U¯<Æ‚6£yµ‹“EO?Ϧ>ãy¶¥/®Äµ|:ñC¾Úœ€;0Šðt!ùÛɉç·OÞgæ—FÓžyä–øelå“1“ønsxÓ¨ÿh†w /\3ãð%888/˜/8™{¾gÚ¤y¬ø' ïèVÜ<`7Ö÷ÇÀ$sï|4i.Ëv§b6åþOÒ.y6Ï^ÂþLðŒlEïGr}ͲT…™¤ïü†iS°zO2^Õ;pïc÷Ñ.ÒAö®Ïx}Ü7l‰Kǃˆf=ô=©ïgNâ~ý„÷f|Ç–Dx†P»ãÃ<}OmÀÉá…ÏpïB ¸+/¿Ýp“´j,—§’iQ¿û»³9ÁwŽ+‘?M æ†úøÙ¾7Õ®¸†ÆÎuüüÇ^RQ´é;˜]«àéJ`åä7™¾ú’²ÁÒˆ.íÂÙ»b%[âÔ»žÿ|'MmøÖ¾š:i³ùãHojèŠñ⾑9óvvó» »« >˜4öÛÁíCgñÙ¶x¬aE»µq‘Ññ‘ Â0 ÜnwnmT^-•ugù•dÇkÌŠ*¦M.~j_ OÖ^Vo7hxM Ϊ;ïM`©ýZž~ómF?Ò•jǯ-\úvï® á¶ÞaÜÐv¤,šÈ¼íY}„ÍÛí\ûÜXÞ›ðjZªærfÆf¾1‹½âµqo0øêT¾xk*¿'›˜™[™õÚ v5¸Ÿ‘cÇñæÐÛifÇQåþ=r"“ß̓5ÿæÓV‘X†þ2ÌôMLs.q­ñæ„‘Ü[e¼¿‚x7¸’÷°+§5O¼5žwF ¢åáÏywÑ?8çÞE¼þîjBz=ÏØ ãq£{wÄãÀA¥^áƒé3cü½Ôñ°á[¿7Ͻýcž¾×wï3gkfáÂ8ó×ÑêVÉmrš»ý+2v"ï¼Ô› ?ñ[è­<;v,¯ö‹âÏÙ³XŸb‚™Éá ï7šw'¾ÁàËâøvÉQZ Åø7Ÿ¢]Ê>\²/·l^Ñ4 Idó"Û–rËyx›R¼‰mW+¯y«A@£ŽÔ5âX·) k-‘ó£Ä&•y¯ ­BQ0“ ÇÌN!ÉéCD`ÁšŽþ±”e'ŸÜ©GIëIÙÌÒmA\Ó·±ÑTkÐ’ØÐ¼ˆå:Êï?î%ªçm\U=”ˆØîÜ\/ƒâÈ0¼ «JPH$•|Oþ5sn{‡ûõ£Oß~ôy`KޏÈÚù«sZsO¯VT ¦É wÓÕ÷O–ü•BæŽoùÕÕ†{{·¡FDÑ5kâ6ß(ªEáT•Ë»6Å7ñsÎ|ŸeíXÊïî˹³Gc"Cb¸âÆÿóÿäåÃ3ð°ÂcZÒ½K4I›÷’f:Ù·lGëÞÉÝëP)$”ˆ0ïB†ÍŽÝnÇn;ñª#¤ U"B‰jÔ…ëjf±c{"‹lf#ÑéK„ÿ‰8kxúDx¶\SÇ Gpª„‡SëÊÎÔ1³ûXþlx‡„MÓNWa÷¢J(¢ÑáÊp’¶Í fv?Âýܤħë‚ñ"áJ>D2Dø½6¼Â‰ò‡¤ƒÉ”á´—sHÇG* Ó4‡!Ã0Žo^Àª“ÂXÞ¶skÏÀÄÄÈûYϘU,jÃ#ŽáéG€G& ©/¹ìûn6ŸU©Å•õ¢€Óÿ‘u§Ç“jR9°˜_W*q©Nö|4˜»?:ñrpTF©.ô=ª÷åÅÇZ`†ƒ€PY'ЂàüÍyS5ÐÍÚ£idä$å_`Zn!H\û“f|Ï_‡ÓÁ.¿®eøm’•”@zÊv^}pù‰—º¤d]› Ÿ` g9¦‹”CiøTŽÄ§,7ì þž¸2œ…Ëœßt¤Ø»„v¼ýä¤åuwoóÆßÓMV1W}6/˜òÏ.I¯j\ÿÌóôêך±ÓŸcàGž„ÄöÂǯZ×Ò¥ÒÛL™ö'‡¶Ä¯ÀvÜ™ywèc'^¨r'cFõà§ncÚÔ) _ F zgxOßS–Ù»Á]<~Ûd&OÆ’t""r Ä ¢:ßIÇßÞcÔ‹°G¶ç©WîåÔkËc§Ys¾Zµ›ŒVÏËGNÖÞ_ÙêјG£ÕWþE÷1wõ®É’^áÍjpmÄ?|1f)™£W]õøg9© Škâ^bψgêx2óÿz^3ï ²Í(MLlyÍMÉ*cþ§³Ìkº÷(ÕÌ¿üü#ÝzôdÉâtëѓ˖Ҵy«ó\D¹”ôéÛÙ³fZ] )%әġøü|0Ò÷±âñ|Wk8£n«vƃJ”sðKži#=F ¥í¹œÖLa͸aÌŠ~ºLƒ_çTçmFzË—ýH×n×|¶v Ë9Âò)#÷Ùe{Sõª><9ì. U}̹ôß¹³¹þ¦[pfg;½ÄsZÇGÊ©ÓÓÛÿÞJ‹ËZzÆ,ÿY.3÷‡ÂÿW0h™_*0‹™ÛY™ÎÌ' IDAT‡éÎû¿¸/€ÓÝ'¶wbÍùM- ìv>>~ÌŸ;›oîUæý#çΆukhÛ”Õ«VÐúŠ«Y½jWµëXªe¿ýz±jÌD¤ìrŽ,烑óØt,lÔ¸ò¹¡êY‡¨ŽÜÕ| S¿ØÆe÷×§,#³•${÷bfo­Ë÷}9åó¨DÛci;Ðê‚H±t|¤(úŒY±~œôZÁgÄNZcÞz‹›Vt¹üpfä·v$¿×·io¶X¸£T$ f"RfŽ*=xvbéjÜψáO“{†38=œsÝʳj7îAõ`=b+""Å;>¸tÞ@ÓÅõ\à…SÔ˜èêþ¤õ`æÕ¤ªYc^H+” :(‘ AÁLÊ>}ûY]¹Ä䇱‚Ï™å?_fy½ôòÇD;¾ÂÜQÈJ¬a³q<¦y‰Ì4ÁvòÜùeRH«8̤\ÑóeRцÍfÃí.íÐæ"Ö)ÍyªsZ.&¥=O=<<ÈÌÈÀÇ·änªŽ?kVÜ”BÝÝsr™›¼=È«!+X[Vð{w^Eš‰iä×Äå¯Ã<ðò;9v,/ou°SQ¨-ˆÈyd¾¾~Äǵº("§uäð!OÙ]œÎi¹˜”æœ bÿ¾½'ÕŠ[KV¨«û¢ÝÝçulÒ<ù_6 ›Í°aØlØ }oÃf³çþo·a·Ùsÿ·Û±ÙìØ=ì¶Üy ÃÀÃáɦ?×S³Vs°§¤¾¾ÇÃÔqFYF ËkÊh§HoŽE»Ì?ÍsjÇ_ÊíÙ11!MWáÌvR»N½3.•”O f""ç‘ÝÃÈèh<}¼Ø¸a-+—ÿdu‘DNb³Ù i³–DDF’ãt–8¯Îi¹œÉ9 ¹5ÁUcbHLŒgÛ¶-¸œNÜî’»¶JœfšùMͼ u¢;ü“¦;_Ñéœ4Ÿ··ÕkÔ¢V­:zƬQ09 #·IKDD$‘‘•Õ»±”_&¸Ý®R]À꜖‹B)Ïé|†aNhhøy.˜HñÌDD.—Ë…Ëuê¦4"Ó""ç–…‹ˆˆˆˆˆXLÁLDDDDDÄb f""""""S0±˜‚™ˆˆˆˆˆˆÅÌDDDDDD,¦`&"""""b13‹)˜‰ˆˆˆˆˆXLÁLDDDDDÄb f""""""S0±˜‚™ˆˆˆˆˆˆÅÌDDDDDD,¦`&"""""b13‹)˜‰ˆˆˆˆˆXLÁLDDDDDÄb f""""""S0±˜‚™ˆˆˆˆˆˆÅÌDDDDDD,¦`&"""""b13‹)˜‰ˆˆˆˆˆXLÁLDDDDDÄb f""""""S0±˜‚™ˆˆˆˆˆˆÅÌDDDDDD,¦`&"""""b13‹)˜‰ˆˆˆˆˆXLÁLDDDDDÄb f""""""S0±˜‚™ˆˆˆˆˆˆÅÌDDDDDD,¦`&"""""b13‹)˜‰ˆˆˆˆˆXLÁLDDDDDÄb f""""""S0±˜‚™ˆˆˆˆˆˆÅÌDDDDDD,¦`&"""""b13‹)˜‰ˆˆˆˆˆXLÁLDDDDDÄb f""""""S0±˜‚™ˆˆˆˆˆˆÅÌDDDDDD,¦`&"""""b13‹)˜‰ˆˆˆˆˆXLÁLDDDDDÄb f""""""S0±˜‚™ˆˆˆˆˆˆÅÌDDDDDD,¦`&"""""b13‹)˜‰ˆˆˆˆˆXLÁLDDDDDÄb f""""""S0±˜‚™ˆˆˆˆˆˆÅÌDDDDDD,¦`&"""""b13‹)˜‰ˆˆˆˆˆX¬LÁÌf³‘’œ|®Ë""""""rÑr»Ýe^Ö£, ùøú‘w€ ëÖ”yã""""""EJJ þþþeZ¶LÁ¬fí:lÙ´‘:uêáˆÍ¦‘"""""rir»Ý¤¤$³cÛVÄ6)Ó:ÊÌÂÂ"hÛ„];¶‘–šR¦ ‹ˆˆˆˆˆT6› _?Ä6&,,œŒô´3^G™‚@XX8aaáe]\DDDDDDò¨ ¢ˆˆ\”¶nÞˆ‡ÃqÊïEDD. f""""""S0±˜‚™ˆˆˆˆˆˆÅÌDDä¢T¿acrœÎS~/""r±(S¯Œk~[y®Ë!"""""R!4ŠmzÆË”¹»ü«Ûw.ë¢""""""ÒŠeKË´œš2ŠˆˆˆˆˆXLÁLDDDDDÄb f""""""S0±˜‚™ˆˆˆˆˆˆÅ̤‚q²çóQ ãGŽº­.KE¡}*"""r¾)˜Iã$nÍR–onêÔŽví:pÃ=Ãùà×x\¥xËL}¨3_AZ¡mf²kñë<|sGÚµkÇ5·?Îøï÷“ ü#´kG»’¾îú€]Îã[ íï¹<Ù½ý¿8RìÅx‰e(y‡¹s.]ÓŽv¿!ÑÌ_ÏFþûÚ#ô¾6·Ýïz‚ ?ÄYÒjœ;™rGÑòßÜ}9'—1u#Sì@»®Ï°:ý4Å;£ýsi1¼Ã©V£:•ƒ––ă€èêT©„µ©0Ê<ÀtQfê:>|ç+2ÚßÃ3ý£0w.áý©ãJ æ>ßÃ$mÃD{e!þ7>ήöfã¬1¼óx&¡³ÿC—0˜Yúí ¦MšÂ×Û² °q KãÏ)OðÂ’Tl8NUªÓnÓnË$+ª _D¨k¿ÌšÈ‡ÃGóÙt -!·:ãùó«yïýÏù3l­ Nt“üë›<:zµî{‰÷®æÀ·ã=â߸"fðXls{çm’\@Î!¾õßW~˜ÿ™í…j“œìûßx>Þ\…[þý uüù}Î>}þYÂçLæÎªÅœ&® ’3mÔzh Ï´öÏ}ÍæCdd‘y3w2÷é!|z¤”÷üJ·.EQ×ñâ”ë¬.~4~è¦Y]‘ äœ]â­>g.6‡=÷n~»føýù Ãÿ\Ë¡œÖÔñHbõô/‰‹y€1ƒo¡†ÚÖ3ù«÷(>þf?=u-“FÊÑv2¢Í\F,,nK.â¾ɳ‹+óøÈ.|úÄü’ ež~›Ž˜›xbpþ­iº¥ƒ~å¯#Nº†z»ÚÌí³9uMî}•Žß=ÇÄBÛLeý¼ïIª5a÷u Š×~Ž+ïfÖœõôu%u[\–;¯s|ÁVŸ—]†ïñ•8ùgñëŒ_Eï—‡±áÕ×H<“2Íöiÿâ_Ÿúò¯ic¹½Znx536óÑÓãØÃH†îy–çòçwsÇD>»ÃG^~jS=U÷~ÌÊ]™ÜYÕ´uc¸ïñ¨óì‡üçšlît3½©Ú !õëûR,w«Çãcîçõ‡䱷邏ח?É|úýã<IJioòÞ翲/ÝFPÝNÜóä“ÜP|°™Éî¯ßåÍi‹Y8°XºÆc××ÄÛ€œ#Ë™<ê>ÿý ™Ø¬Û‹QÿMS 'žÕ3Ç0qÞrv&»ñiDï‘cÔØwÒz>}{³ØÆ1·7U.ïÉCO  sÕÜó&ëïx~ôbþÜyˆTÕiÝã~þý`gb¼3~/îÃ_Ðÿ¶‰DYÀ.÷æØ3xmÌ–ïIØkŸcÂ3m )º ™Å¾¥“ykòB~?‰=¤ûæñÞ ´®Ã|ÿ榮ØÎ¾ÄLÀ °V{úÊM‹Ù§™¬ÿÏ­<²¥s>ìCU£4Ë»IÝü9ãޞη›pyGѲç# ÿW¢NuoEDDDäqë ìû‰]iNtâ¨\“P;µ—Õ[„umMtÞ…˜-´9í«ÃÄUÛI»³Amxá³ùØl.öΚWìVr,âÕ·þ¦Ã«pmä—|zª"•f›Ç›b™dÝÌ’y¿݉Õi:ìAOÀÌ`ÓäGxrN<ÇÀØ œñǨì ÎÝÌ~âß¼ô |îaûìcéÔñ¼8(ŸOseœ#kYµÍ ç°Q´€Ä­ßññÔ<´f¾Ú…pãtïåí36ðîóSù³ù£¼þtS‚²°/«þ'¥(7Ç~y#–ÑóߌêX™ô Ÿ1~Âÿ1Ä=‰wUÇáNeÇš­9€—Ÿm€OÆ^–<÷†Ž¡Öܹ"à4Çä´ËäXÈ3¾Ãá0êß°ïXÄÛo>ÇÓA3™rwõSÖ{‹ˆˆˆ\ ÎS£°,ö|1šI;ërÚWS (*€ãñÍHåîÄ#¤º ÈÃÀvªo®C|õÚû¸v4#[`ì/293 §ÀðÀ‡ÒlÈÚ̸>ñÙÀV—»Ç÷§‰Ÿ¸ÉJK';/Ývo|}Ø0°@q¡ÂD½Ë*ìÙÌý£9÷· ƒäýìKqANvÉÏleØNóÑ)Ê€'ušÁOåÿl’¶a*#—Dñð‡=©êÈæÐ)Öì>¶šI£–`ty™>usƒ™_Ëaü÷çaÇçÉÉHÅ'ÌÍÏ#îá{À»z;î~â)úµ Á¸aÜ[Ð|ÈÇt ·“R¤ìE×WZîÄ_ø`Q<-ž~ŸׄaêcÙ“Y´%+[Ùk9øæƒpwy“çúµÁ߀fu†²ã§ùæ›<Ü ”ø„lì•syËFDÙ¡A^ ZóØ*>øl՜΋÷Ô,Òÿ˜Á¬¿C¸yâîkê´¦E;ïx‹ißÝOë^Ñys†Ñ´íÕ´ 6 ÍU´ŠH䎑SY¸§=÷î½ø•¼#œII…à—ÓªqM˜7ç/ú¼PŸÛÿ3Žö™¹ý!Úü«—T 2üêsǨyÜšz”#I&þžóF¿áliÙ` (pî[ÊW;3Ù7ùn:O.8ånºe-?FSop'®dìÿ=Í’ˆ1aô]Ô+M—{†ƒÚm¹çéÿã—[GóÕ‰ô =Äâï“–ô·u|­ÐìOvëÇýÈµÊØxÍð£ýsãP¿`è7ðŽÁFÑ:ML"nÅk½ª X6¿(|°á×v3ÜÀÒùs˜ñîæÏèÄóS^¤½û<ô·Ÿ_êó´ïålþ48…×.gÁìÌxö^>nõSÞ¸êç ] a÷À†yæMM‹]>÷PýƽܩÐïáHÔ¹ºw#"""r;‡ÁÌ$uíD†ŒßÅÕ/MaHÁPàYÖõ,þýw9RÝî„õü¼ª?T¿Ófš›ÎŒôÆ»-æ™§¾¢áˆ·y¸MÁþQ´ªU`wb¶i`àÊÎÁ´ùQµqKª–i8ü#¨â›Ì'òSf¹©%?¹vþxTíÅ[3;×+ƒãþ×dâ+×Qß ÈÙÏ‚çŸçËÀþLz­”¡¬ù{ÕcÀ¤™Ü‘sü7RV¾Â É<õÞ0:T+ûiçK5ÛlÛeÝ­%_Ó»sŸ¯óªLÓ*ðË_ñ}¬-UJØ´#4–n^¥Ë yìŽ7˜³l :5¦ší þøí ÎØ‚MD4ª‹_öZ–ïÈ MÓÜZ3Wü:V€˜Ûªáœ<ÒƒIÊÖ•üC·ÄøàZÚ÷R;µ;p÷óí¸¡Ã Üõì§,Þ}ƒê8˼ªÒ2V®Ü@Bßê¹MIgûò­8.§aø™ 7eʪ6j4 ‡EI¾—–¥¹»!"""r‰9wÁÌu%ï. ¡ÑÃ\•Àö­yíÿl^„ÅT'Ü+˜6÷ô âÑxibý¯ôâ¯ÙcYçÕ–ݪ”¢ 6|*ÅP£À+9¡xWŽ!ª¸*7Û鶙áfò¿„j4ˆ Æ‘u„MK>â“ÃA\ûd}¼Ë¸+Ììöìø‡{6ñË—Ÿ²`½A‡áorëY’3S´WÆ *W*0=£~vŒÌªU ÅË€ô??bêúºh‰ùÏßlÍ›ÓæW…ÚUýÈ(Ô‹b »¿˜Î zÔ‹öðes¦°ÙÑŒ!—…b3ì„V­Nè‰=BÒo »Q1• r˜¤­{«ø^OÃz5÷õˆ`𬡣ᠻiì§ð±è> )åÑÊçEÝÛúÒdÁ8Fyƒø¾¨èæØ¾}ØÛÜB§Êf±½ˆŠˆˆˆ\JÎ]RÈÜËo»Lr²ßcð€‚"¸}ê­ï…_³ÿcì³0ê½76ß…­.ü{ì:…¯;èÆi¶™CfünVΙŇG2¡µÛÐ÷¥AÜwEP™GßÎÙ¿€gú€ƒ!ÕitÙM¼ðÁt©ëGó. Ó,mõ†“¸›Iæ(_Žx˜/ Njú_Nè„GÁçÄL')ñÛøvÁl&'æ~TnÖÇÇäÆS?€W¨|ebpÙàw:Ž)_¼Éð.°Q«Ë¿épC,xQ§÷¿è¾æm>ÿ-§ÝCõÎ/ð¾k ã>œÍ¾OÃēІ7ðìõÝ©iOa÷Ú…Œû~OîÝ>•iyû kŠÍ€–½Çèà±¼?4Ã?tƒwU:>Ù’ŽµkÒwÌ;x¼=ŽY¯>É4·7•/»•¯>Ä•A´v-bÌ¢}dAÔî4±OÜBîÐp§{/%s§ä¯o&±pBnÀ3¢1×{Š›*ý•¶|ÕP&æÍ÷'2ü‹LlÁuéüÈï]ÚÞ‹îÓR>»Y€GÕ[yý]&N˜ÅÔ_’ øD6§wƒ›èTÙÆ™¯""""1ÿÓYæ5Ý{”jæ_~þ‘n=z²bÙR®nßù<Mäâ–¶ü1®žÍó‹Þåšà3ï@CDDDD.>+–-¥QlSV¯ZÁUí:–j™o¿^|+qDDDDDDä$ f""""""ÓèA"ç‰_Ûq,ûÙêRˆˆˆˆÈÅ@5f""""""S0±˜‚™ˆˆˆˆˆˆÅÌDDDDDD,¦`&"""""b13‹)˜‰ˆˆˆˆˆXLÁLDDDDDÄb f""""""S0±˜‚™ˆˆˆˆˆˆÅÌDDDDDD,¦`&"""""b13‹)˜‰ˆˆˆˆˆXLÁLDDDDDÄb f""""""S0±˜‚™ˆˆˆˆˆˆÅÌDDDDDD,¦`&"""""b13‹)˜‰ˆˆˆˆˆXLÁLDDDDDÄb f""""""S0±˜‚™ˆˆˆˆˆˆÅÌDDDDDD,¦`&"""""b13‹)˜‰ˆˆˆˆˆXLÁLDDDDDÄb f""""""S0±˜‚™ˆˆˆˆˆˆÅÌDDDDDD,¦`&"""""b13‹)˜‰ˆˆˆˆˆXLÁLDDDDDÄb f""""""S0±˜‚™ˆˆˆˆˆˆÅÌDDDDDD,¦`&"""""b13‹)˜‰ˆˆˆˆˆXLÁLDDDDDÄb f""""""S0±˜‚™ˆˆˆˆˆˆÅÌDDDDDD,¦`&"""""b13‹Q0³Ùl¤$'Ÿ¯²ˆˆˆˆˆˆ\ôÜn÷/ãq&3ûøú‘w€Ë–žñÆDDDDDD*º””üýýÏh™3 f5k×a˦ԩSÿÀ@l6µ„Üš²””dvlÛJƒØ&g´ì³°°Ä6a׎m¤¥¦œÑ†DDDDDD*2›Í†¯Ÿ? bŽÓ™]êeÏ(˜9ÙÒ¬E«3.¤ˆˆˆˆˆÈ¥âLB¨WFË)˜‰ˆˆˆˆˆXLÁLDDDDDÄb f""""""S0±˜‚™ˆˆˆˆˆˆÅÌDDDDDD,¦`&"""""b13‹)˜‰ˆˆˆˆˆXÌÃꈈˆˆˆeš&‰ ñ$'%ãrå`š¦ÕEº(xxxDph6›Q¦uhß—ÍÙî{3)WLÓdïÞ=xyzQ»^=|||1Œ²…ŒK‰išdd¤³ß^öíÝMµ˜g¼ß´ïËæ\ì{3)Wââ°{P»n}LÓ<þeF¡Ú›üŸ ^W¤Ú¢öùï­¸×ó÷…/uêÖçï-‘˜OhXømSû>—û^Ϙ‰ˆˆˆH¹’Oåj1Çο.xq\Ò…sE”Ž€“Rq¡  JµŽ=ãmý{wU}çqü}gòœˆHB$"ÂA›–m©µQ*ʪÅ-[ÛfÝݳõX[·[ºÚjín‘j«²++´ ´[ÀˆÈÑîy¡¥<$ Çùýö{ç1A“i&3«Ÿç2w2÷ÞÏüs¿çû»¿«ì£õfö*ÌDDDD$¥´¶´““ tìÌ_Ç.Ƙ¤ís"c¢ŽéÃŽ5¶XÊÉÉ¥µ­µÛÛTö®dd¯¡Œ""""’RLD'"¶+ÑYÙ±ø8±ÝªØuÁ÷#‹†Èõ8²Pö®dd¯Ž™ˆˆˆˆ¤qRë_[ Ö;‚õÞ\ß3K‡^~‚‡æ¿Fƒé©ïìÞḆuÚ­êd  qÉÏ>ù¹'+{f""""’R¢†8Ö{n":Æ÷$Ù[׳K;Ç·ÕóÆ®´ÚD|÷—àqGvi:û\°ÛO+ùÙ§^½†2ŠˆˆˆHJ1]kLø:'u+BÏãd›òêò¥¬üãŸØw²Èf@åX>7}·Lȉø  Ÿ'Q‡Y!ÜÅ æà8¡çqf‰ÎÞ|ÀîuËX¶ºž­OÑŽC^ñHÆ_õ%f~±"ü¹Éz'{f""""’R¬1QŠ`×&´>rf¼x·qfÏ~÷AVý¥?c¿p u#’ÝÖÀosª=zPYÔ¾$SLxŠZÇ( &IDAT+'â}‡ˆOœCš}à$›ž¸—GÖŸ¢ôŠë¹ý†aôOoæØ»Ûxç”!ò¾Ì)“;ôJö*ÌDDDD$¥˜ÈNLD÷&Jð½ˆI*ºÌ6±cÑOXõ—rþþápkev¨ûQýÙkCŸq·aÃÛŸ×/dÁÒuì8Þ†¿ ŠÉ3¾É¬Éed9кo1sæþšÝ§ÛJÇO宺›¸$ß¶,~’%·sàd ǘºóÀçûãïvBlj¾*ø<8œ.ŽŽVb³·4nþ¯ÿ€Q³cví 2‚«ª'3€Îroåpý,ù[¶àï[Aõ´;ùêuÃÉsÛľ5?ãÉÿÚÈþÓ2‹˜0ó_¹wÊ…ø±œÝó2?f9÷6È,btÍÔÝvEéÝŽ',Ù«0‘”{Sì)nlׯ‰íf|”s;ù͆FrªïæúaÙ¡ï‰e#-†ÆÍOpÿã;1㟙{iÇ^]È“óæ’9è§ÌªÊÄ×<Ó¾q } ²iÿëë,™ÿ+]4Šùu“eN³{Ó[)¾•©I¾9ƒûá;϶»"ªCñ,“zâ³ÍÞ6ðæo¶Ð\XËíS‘ÞÉ÷Clî–ÓoÎç;?©§°f÷}êBšv¾Ä³Ï=À¿™ÿ`îKáÀ‹<úÌfúß|sÇbsz ›mûÑßñðœ_püŠ™|{V%iÖòô‚Gx(ÿ?ùуâ.†‘½ 3I)Á“Zk׌è8%yôûÝè–í ïr¨Š/-% ÓiSÜ!}nWÈ`Û°~Ùk˜OßÏÝÓÆ’ëÀˆò¯ñÞëßfãú÷¸­²’ŒüJ&Lðþ|X·o{…ÞÙÁñ¶”Z÷ûrËÇ0~̰P§ÈÚx§˜, Â×@Y¯Ûä8þ¿©0KHö-ÇØ{Ôâ¿èŠÓ»šûQ6,ÝÀ™wñ£;¯¢¿]EaÃÝÌùŸåì®ù†ž}Ÿ3ä1nôh†Wdã0ÔÛÏfö¿´œí}jyøë×2,¨*åk;6ó½õoptj %qµ+“½ 3I)68ã_ì,xÖšî cÄ->B'ÿÆÐù)t¸01Æ`ZÿÊöÃpîร>ú“þ£§i3ÚüÅ WñÆžÃ4¶¥“í´ÀÍ´ oCoFÿíŽ_Žc;6ÖZ|>w½õŽ­»š½±˜ÈL;ÿPLî‡ØzЧ'ƒ{H™ ¾¬‚´µûØu¢C®ç†‹·°xv{&]͵_¨¡zh_üö w€«¸oƪèÍäáT«a`f×wß•¸ìU˜‰ˆˆˆHJ16ص±ã h;o|º:(Ðé3ˆi°oç!ÎMîGvç{á^ñæsLù÷^=0ê$Ú—3€Œ³;xúû Øxáu|õžoPž×ÆÞåðô`ñ,M…™7+bxÀ¨Â Ø­qc >·BåØ ÍÞß—Á…8ôG[ÆRÞé5^±¹‡·™[hê~c0郸~ö<.ûß?°zåJÿ΋¬ºñ{Ì™^è‚¥Ó™ýOé1§‹“–Ç€´`¡÷az/{ÝÇLDDDDRJèYÆ`­Á¼b&pOÄ‹5,dW2e\6gê—±î`óy>ç€Ûc°i^'÷#ëÂJKÂKq¿tï²çt:£o¼Iaðà! )ÊuåÜ5õºë‹{|·—G°x´ÖÛw÷PÆ„eO_Æ~~8þ÷˲׎Ñޥ܋UGÞÚIC{ð3çxoË~Ús/bh_ÇÛ×LŠÇ\óÌœš|ö¯y‰}ÍY”VÀ±Ý4æGýf%Eù¤ÛÔÊ^3I)ÖëԘЉnøfÆ¡ûkyÏ;›ð£ï¯•Ãèw0açS<ÿÝÙ쫽’q…d™3Û¿‡#kùò¤~däÀ[oPÿö8®ÕŸê.cå¼E<4¯‰iÕCéç;˱CÍT]õw Ê-epn¬`=ã(Í5üùýfÀ0ÁN t!‡&èÍ hƒ3zë3¸§îO+’èì ªg2£~‹æßǃÛk™<¦Œ~émœ>ü.»N çæ[FuÈýŠ›ªyñ±gyômÜtùšv­eñ+PqëuT¤h9²…uÛ lp!™íÇÙzà,d÷% ?e5µT­{ž§~ø '¯OYžáƒ#Gñ]z%‹Î_ %#{f""""’RÜîÞ5Nƶvžë:ÜW«‹çþ ª©û÷~¿|%ë~ÿ<›Î€4úWrÙ5íle57ó™í Yñü&&|*%—/°hÅK,xý–túV|–»>3‰Ò>#¹í§Ó¼ðežú¡{=““™OÉèþd1ë|Ä,ðÅ+‡¢¦×CÖbqÜ!v1ygp¿c–ÀìÓJ¨¹ça¼ü«Ö¯æçš°@FA#'¡Õt’û˜YÌùfžûï¥<öJ+¾>åLœq?·×”f¡¹q?›W¬aQC;à'¿|<_ª›Êà4pЦð­Ù~–.YÍ óþH U8‚«‡|ŽËœ¿JFöÎòe‹í”kj»ý‡"""""‰°níj§N£µµk àInLçÜkÜ“eˆâùTç–÷X£z9¯ÇÁçóãó9¤gd°ú×/rå”kºµ%e«w²_»fµ:f""""’Z¢»6^ "4éBää ÁuNxXÙÇ‘5áásÆxÇéx3ºCèBµk}=Ô1Sö½™½ 3I-Þ0:·K¾®)úú¥ˆë¼GçcÚ´ ›!|MWh&@kñ98nW'˜“õ&¶èþÆ”}¤ÞÌ^…™ˆˆˆˆ¤”´ô ÉÉÍñÎŒëÞÔ×=!vïŸeÅñþAxêòžrÇWfF½þåsÏöØwwEh ‹7Œ.¢0ðyÇl½GœÐQÆZNž #£Û7éJ™ì{Jìoë|¿iR²ïö_ˆˆˆˆˆ$Pié ÞÙù6—OümֺݿßBæøÀ‚Ïï‹sVÆ®Y´ð—=ò=ñê츢ÞsÜÿb'äHOKgûÛÛ(+/ïö6S%ûžïo˜ŒìU˜‰ˆˆˆHJVYŦ×^eSýFŽMAAaÇåíd ulB³v¼¦ËZKcãIÞܶ•ööv*†Vv{;ʾ£ÞÊ^…™ˆˆˆˆ¤ŸßOõ§'±wïn^¯ßHSSS²wéÿÇqÈÊÎæ¢! Zùá÷â:eŸžÈ^…™ˆˆˆˆ¤Ç磲j•U#’½+Ÿ8Ê>9|ÉÞ‘O:f""""""I¦ÂLDDDDD$ÉT˜‰ˆˆˆˆˆ$™ 3‘$Sa&"""""’d*ÌDDDDDD’L…™ˆˆˆˆˆH’©0I2f""""""I–°vÍêdˆˆˆˆÈ'ÖÿÛƒ“i´|IEND®B`‚libindi/libs/indibase/alignment/MathPlugin.h0000664000175000017500000000715513263645557020410 0ustar jasemjasem/*! * \file MathPlugin.h * * \author Roger James * \date 13th November 2013 * */ #pragma once #include "InMemoryDatabase.h" namespace INDI { namespace AlignmentSubsystem { /*! * \class MathPlugin * \brief Provides alignment subsystem functions to INDI alignment math plugins * * \note This class is intended to be implemented within a dynamic shared object. If the * implementation of this class uses a standard 3 by 3 transformation matrix to convert between coordinate systems * then it will not normally need to know the handedness of either the celestial or telescope coordinate systems, as the * necessary rotations and scaling will be handled in the derivation of the matrix coefficients. This will normally * be done using the three reference (sync) points method. Knowledge of the handedness of the coordinate systems is needed * when only two reference points are available and a third reference point has to artificially generated in order to * derive the matrix coefficients. */ class MathPlugin { public: /// \brief Default constructor MathPlugin(MountAlignment_t ApproximateAlignment = ZENITH) : ApproximateMountAlignment(ApproximateAlignment), pInMemoryDatabase(NULL) { } /// \brief Virtual destructor virtual ~MathPlugin() {} // Public methods /// \brief Get the approximate alognment of the mount /// \return the approximate alignment virtual MountAlignment_t GetApproximateMountAlignment() { return ApproximateMountAlignment; } /// \brief Initialise or re-initialise the math plugin. Re-reading the in memory database as necessary. /// \return True if successful virtual bool Initialise(InMemoryDatabase *pInMemoryDatabase); /// \brief Set the approximate alognment of the mount /// \param[in] ApproximateAlignment - the approximate alignment of the mount virtual void SetApproximateMountAlignment(MountAlignment_t ApproximateAlignment) { ApproximateMountAlignment = ApproximateAlignment; } /// \brief Get the alignment corrected telescope pointing direction for the supplied celestial coordinates /// \param[in] RightAscension Right Ascension (Decimal Hours). /// \param[in] Declination Declination (Decimal Degrees). /// \param[in] JulianOffset to be applied to the current julian date. /// \param[out] ApparentTelescopeDirectionVector Parameter to receive the corrected telescope direction /// \return True if successful virtual bool TransformCelestialToTelescope(const double RightAscension, const double Declination, double JulianOffset, TelescopeDirectionVector &ApparentTelescopeDirectionVector) = 0; /// \brief Get the true celestial coordinates for the supplied telescope pointing direction /// \param[in] ApparentTelescopeDirectionVector the telescope direction /// \param[out] RightAscension Parameter to receive the Right Ascension (Decimal Hours). /// \param[out] Declination Parameter to receive the Declination (Decimal Degrees). /// \return True if successful virtual bool TransformTelescopeToCelestial(const TelescopeDirectionVector &ApparentTelescopeDirectionVector, double &RightAscension, double &Declination) = 0; protected: // Protected properties /// \brief Describe the approximate alignment of the mount. This information is normally used in a one star alignment /// calculation. MountAlignment_t ApproximateMountAlignment; InMemoryDatabase *pInMemoryDatabase; }; } // namespace AlignmentSubsystem } // namespace INDI libindi/libs/indibase/alignment/alignment_white_paper.md0000664000175000017500000010343213263645557023051 0ustar jasemjasem# INDI Alignment Subsystem ## Introduction The INDI alignment subsystem is a collection of classes that together provide support for telescope alignment using a database of stored sync points. Support is also provided for "Math Plugin Modules". One of these runtime loadable modules is active at any one time. The currently loaded module uses the sync point database to provide conversion functions to and from coordinates in the celestial reference frame and the telescope mount's local reference frame. During observing runs the sync point database is held in memory within the INDI device driver. It can also be loaded and saved to and from a file on the system the driver is running. The database can be edited via INDI properties (for details of the properties see the class MapPropertiesToInMemoryDatabase), by an API class for use in INDI drivers(InMemoryDatabase), or an API class for use in INDI clients(ClientAPIForAlignmentDatabase). The current math plugin module can be selected and initialised via INDI properties (for details of the properties see the class MathPluginManagement), by and API class for use in INDI drivers(MathPluginManagement), or by an API class for use in INDI clients(ClientAPIForMathPluginManagement). ## Math Plugins The following math plugins are included in the first release. ### Built in math plugin This is the default plugin which is used if no other plugin has been loaded. The plugin is normally initialised or re-initialised when the database has been loaded or when a new sync point has been added. The initialisation process scans the current database and builds a number of transformation matrices depending on how many sync points are present. Before the matrices are computed all celestial reference frame Right Ascension and Declination coordinates are transformed to horizontal Altitude Azimuth coordinates using the julian date stored in the sync point entry. This means that all transformations using the computed transformation matrices will be to and from a zenith aligned celestial reference frame and the telescope mounts local reference frame. This has the advantage of incorporating in the transformation any systematic alignment errors which occur due to the direction the mount is pointing relative to the zenith. Examples of such errors include those due to atmospheric refraction, and those due the effect of gravity on the telescope and mount. All transformation matrices are computed using the simple method proposed by [Toshimi Taki](http://www.geocities.jp/toshimi_taki/matrix/matrix_method_rev_e.pdf). This is quick and dirty but can result in matrices that are not true transforms. These will be reported as errors and an identity matrix will be substituted. #### No sync points present No action is taken. #### One sync point present A transformation matrix is computed using a hint to mounts approximate alignment supplied by the driver, this can either be ZENITH, NORTH_CELESTIAL_POLE or SOUTH_CELESTIAL_POLE. The hint is used to make a dummy second sync point entry. A dummy third entry is computed from the cross product of the first two. A single transformation matrix and its inverse is computed from these three points. #### Two sync points present A transformation matrix is computed using the two sync points and a dummy third sync point computed from the cross product of the first two. A single transformation matrix and its inverse is computed from these three points. #### Three sync points present A single transformation matrix and its inverse is computed from the three sync points. #### Four or more sync points present Two convex hulls are computed. One from the zenith aligned celestial reference frame sync point coordinates plus a dummy nadir, and the other from the mounts local reference frame sync point coordinates plus a dummy nadir. These convex hulls are made up of triangular facets. Forward and inverse transformation matrices are then computed for each corresponding pair of facets and stored alongside the facet in the relevant convex hull. #### Coordinate conversion If when the plugin is asked to translate a coordinate it only has a single conversion matrix (the one, two and three sync points case) this will be used. Otherwise (the four or more sync points case) a ray will shot from the origin of the requested source reference frame in the requested direction into the relevant convex hull and the transformation matrix from the facet it intersects will be used for the conversion. ### SVD math plugin This plugin works in an identical manner to the built in math plugin. The only difference being that [Markley's Singular Value Decomposition algorithm](http://www.control.auc.dk/~tb/best/aug23-Bak-svdalg.pdf) is used to calculate the transformation matrices. This is a highly robust method and forms the basis of the pointing system used in many professional telescope installations. ## Using the Alignment Subsystem from KStars The easiest way to use a telescope driver that supports the Alignment Subsystem is via an INDI aware client such as KStars. The following example uses the indi_SkywatcherAltAzMount driver and a Synscan 114GT mount. If you are using a different driver then the name of that driver will appear in KStars not "skywatcherAPIMount". 1. Firstly connect the mount to the computer that is to run the driver. I use a readily available PL2303 chip based serial to USB converter cable. 2. From the handset utility menu select PC direct mode. As it is the computer that will be driving the mount not the handset, you can enter whatever values you want to get through the handset initialisation process. 3. Start indiserver and the indi_SkyWatcherAPIMount driver. Using the following command in a terminal: indiserver indi_skywatcherAPIMount 4. Start KStars and from the tools menu select "Devices" and then "Device Manager". ![Tools menu](toolsmenu.png) 5. In the device manager window select the "Client" tab, and in the client tab select the host that indiserver is running on. Click on connect. ![Device Manager](devicemanager.png) 6. An INDI Control Panel window should open with a skywatcherAPIMount tab. Select the "Options" subtab (I think I have invented this word!). Ensure that the port property is set to the correct serial device. My PL2303 usb cable always appears as /dev/ttyUSB0. ![INDI Control Panel](controlpanel1.png) 7. Select the "Main Control" tab and click on connect. ![INDI Control Panel](controlpanel2.png) 8. After a few seconds pause (whilst the driver determines what type of motor board is in use) a number of extra tabs should appear. One of these should be the "Site Management" tab. Select this and ensure that you have correct values entered for "Scope Location", you can safely ignore elevation at this time. ![INDI Control Panel](controlpanel3.png) 9. At this point it is probably wise to save the configuration. Return to the "Options" tab and click on "Configuration" "Save". ![INDI Control Panel](controlpanel4.png) 10. Check that the "Alignment" tab is present and select it. Using the controls on this tab you can view and manipulate the entries in the alignment database, and select which math plugin you want to use. It probably best to ignore this tab for the time being and use KStars to create sync points to align your mount. ![INDI Control Panel](controlpanel5.png) 11. To create a sync point using KStars. First ensure your target star is visible in the KStars display. I usually do this using the "Pointing...Find Object" tool. ![Find Object Tool](findobject.png) 12. Once you have the target in the KStars window right click on it and then hover your mouse over the "Sync" option in the "skywatcherAltAzMount" sub-menu. Do not left click on the "Sync" option yet. N.B. The "Centre and Track" item in the main popup menu is nothing to do with your mount. It merely tells KStars to keep this object centered in the display window. ![Object popup menu](objectpopup.png) 13. Go back to your scope and centre the target in the eyepiece. Quickly get back to your computer and left click the mouse (be careful not to move it off the Sync menu item or you will have to right click to bring it up again). If you have been successful you should see the KStars telescope crosshairs displayed around the target. ![Crosshair Display](crosshair.png). 14. The Alignment Subsystem is now in "one star" alignment mode. You can try this out by right clicking on your target star or a nearby star and selecting "Track" from the "skywatcherAltAzMount" sub-menu. The further away the object you track is from the sync point star the less accurate the initial slew will be and the more quickly the tracked star will drift off centre. To correct this you need to add more sync points. 15. To add another sync point you can select a new target star in KStars and use the slew command from the "skywatcherAltAzMount" sub-menu to approximately slew your scope onto the target. The procedure for adding the sync point is the same as before. With the default math plugin one achieves maximum accuracy for a particular triangular patch of sky when it is surrounded by three sync points. If more than three sync points are defined then more triangular patches will be added to the mesh. 16. If would be very useful if you could collect information on how well the alignment mechanism holds a star centred, measured in degrees of drift per second. Please share these on the indi-devel list. ## Adding Alignment Subsystem support to an INDI driver The Alignment Subsystem provides two API classes and a support function class for use in drivers. These are MapPropertiesToInMemoryDatabase, MathPluginManagement, and TelescopeDirectionVectorSupportFunctions. Driver developers can use these classes individually, however, the easiest way to use them is via the AlignmentSubsystemForDrivers class. To use this class simply ensure that is a parent of your driver class. class ScopeSim : public INDI::Telescope, public INDI::GuiderInterface, public INDI::AlignmentSubsystem::AlignmentSubsystemForDrivers Somewhere in your drivers initProperties function add a call to AlignmentSubsystemForDrivers::InitAlignmentProperties. bool ScopeSim::initProperties() { /* Make sure to init parent properties first */ INDI::Telescope::initProperties(); ... /* Add debug controls so we may debug driver if necessary */ addDebugControl(); // Add alignment properties InitAlignmentProperties(this); return true; } Hook the alignment subsystem into your drivers processing of properties by putting calls to AlignmentSubsystemForDrivers::ProcessNumberProperties, AlignmentSubsystemForDrivers::ProcessSwitchProperties, AlignmentSubsystemForDrivers::ProcessBLOBProperties AlignmentSubsystemForDrivers::ProcessTextProperties, in the relevant routines. bool ScopeSim::ISNewNumber (const char *dev, const char *name, double values[], char *names[], int n) { // first check if it's for our device if(strcmp(dev,getDeviceName())==0) { ... // Process alignment properties ProcessNumberProperties(this, name, values, names, n); } // if we didn't process it, continue up the chain, let somebody else // give it a shot return INDI::Telescope::ISNewNumber(dev,name,values,names,n); } bool ScopeSim::ISNewSwitch (const char *dev, const char *name, ISState *states, char *names[], int n) { if(strcmp(dev,getDeviceName())==0) { ... // Process alignment properties ProcessSwitchProperties(this, name, states, names, n); } // Nobody has claimed this, so, ignore it return INDI::Telescope::ISNewSwitch(dev,name,states,names,n); } bool ScopeSim::ISNewBLOB (const char *dev, const char *name, int sizes[], int blobsizes[], char *blobs[], char *formats[], char *names[], int n) { if(strcmp(dev,getDeviceName())==0) { // Process alignment properties ProcessBlobProperties(this, name, sizes, blobsizes, blobs, formats, names, n); } // Pass it up the chain return INDI::Telescope::ISNewBLOB(dev, name, sizes, blobsizes, blobs, formats, names, n); } bool ScopeSim::ISNewText (const char *dev, const char *name, char *texts[], char *names[], int n) { if(strcmp(dev,getDeviceName())==0) { // Process alignment properties ProcessTextProperties(this, name, texts, names, n); } // Pass it up the chain return INDI::Telescope::ISNewText(dev, name, texts, names, n); } Then make sure you override the INDI::Telescope::updateLocation function and call the AligmmentSubsystemForDrivers::UpdateLocation function. bool ScopeSim::updateLocation(double latitude, double longitude, double elevation) { UpdateLocation(latitude, longitude, elevation); return true; } The next step is to add the handling of sync points into your drivers Sync function. bool ScopeSim::Sync(double ra, double dec) { struct ln_hrz_posn AltAz; AltAz.alt = double(CurrentEncoderMicrostepsDEC) / MICROSTEPS_PER_DEGREE; AltAz.az = double(CurrentEncoderMicrostepsRA) / MICROSTEPS_PER_DEGREE; AlignmentDatabaseEntry NewEntry; NewEntry.ObservationJulianDate = ln_get_julian_from_sys(); NewEntry.RightAscension = ra; NewEntry.Declination = dec; NewEntry.TelescopeDirection = TelescopeDirectionVectorFromAltitudeAzimuth(AltAz); NewEntry.PrivateDataSize = 0; if (!CheckForDuplicateSyncPoint(NewEntry)) { GetAlignmentDatabase().push_back(NewEntry); // Tell the client about size change UpdateSize(); // Tell the math plugin to reinitialise Initialise(this); return true; } return false; } The final step is to add coordinate conversion to ReadScopeStatus, TimerHit (for tracking), and Goto. bool ScopeSim::ReadScopeStatus() { struct ln_hrz_posn AltAz; AltAz.alt = double(CurrentEncoderMicrostepsDEC) / MICROSTEPS_PER_DEGREE; AltAz.az = double(CurrentEncoderMicrostepsRA) / MICROSTEPS_PER_DEGREE; TelescopeDirectionVector TDV = TelescopeDirectionVectorFromAltitudeAzimuth(AltAz); double RightAscension, Declination; if (!TransformTelescopeToCelestial( TDV, RightAscension, Declination)) { if (TraceThisTick) DEBUG(DBG_SIMULATOR, "ReadScopeStatus - TransformTelescopeToCelestial failed"); bool HavePosition = false; ln_lnlat_posn Position; if ((NULL != IUFindNumber(&LocationNP, "LAT")) && ( 0 != IUFindNumber(&LocationNP, "LAT")->value) && (NULL != IUFindNumber(&LocationNP, "LONG")) && ( 0 != IUFindNumber(&LocationNP, "LONG")->value)) { // I assume that being on the equator and exactly on the prime meridian is unlikely Position.lat = IUFindNumber(&LocationNP, "LAT")->value; Position.lng = IUFindNumber(&LocationNP, "LONG")->value; HavePosition = true; } struct ln_equ_posn EquatorialCoordinates; if (HavePosition) { if (TraceThisTick) DEBUG(DBG_SIMULATOR, "ReadScopeStatus - HavePosition true"); TelescopeDirectionVector RotatedTDV(TDV); switch (GetApproximateMountAlignment()) { case ZENITH: if (TraceThisTick) DEBUG(DBG_SIMULATOR, "ReadScopeStatus - ApproximateMountAlignment ZENITH"); break; case NORTH_CELESTIAL_POLE: if (TraceThisTick) DEBUG(DBG_SIMULATOR, "ReadScopeStatus - ApproximateMountAlignment NORTH_CELESTIAL_POLE"); // Rotate the TDV coordinate system anticlockwise (positive) around the y axis by 90 minus // the (positive)observatory latitude. The vector itself is rotated clockwise RotatedTDV.RotateAroundY(90.0 - Position.lat); AltitudeAzimuthFromTelescopeDirectionVector(RotatedTDV, AltAz); break; case SOUTH_CELESTIAL_POLE: if (TraceThisTick) DEBUG(DBG_SIMULATOR, "ReadScopeStatus - ApproximateMountAlignment SOUTH_CELESTIAL_POLE"); // Rotate the TDV coordinate system clockwise (negative) around the y axis by 90 plus // the (negative)observatory latitude. The vector itself is rotated anticlockwise RotatedTDV.RotateAroundY(-90.0 - Position.lat); AltitudeAzimuthFromTelescopeDirectionVector(RotatedTDV, AltAz); break; } ln_get_equ_from_hrz(&AltAz, &Position, ln_get_julian_from_sys(), &EquatorialCoordinates); } else { if (TraceThisTick) DEBUG(DBG_SIMULATOR, "ReadScopeStatus - HavePosition false"); // The best I can do is just do a direct conversion to RA/DEC EquatorialCoordinatesFromTelescopeDirectionVector(TDV, EquatorialCoordinates); } // libnova works in decimal degrees RightAscension = EquatorialCoordinates.ra * 24.0 / 360.0; Declination = EquatorialCoordinates.dec; } if (TraceThisTick) DEBUGF(DBG_SIMULATOR, "ReadScopeStatus - RA %lf hours DEC %lf degrees", RightAscension, Declination); NewRaDec(RightAscension, Declination); return true; } bool ScopeSim::Sync(double ra, double dec) { struct ln_hrz_posn AltAz; AltAz.alt = double(CurrentEncoderMicrostepsDEC) / MICROSTEPS_PER_DEGREE; AltAz.az = double(CurrentEncoderMicrostepsRA) / MICROSTEPS_PER_DEGREE; AlignmentDatabaseEntry NewEntry; NewEntry.ObservationJulianDate = ln_get_julian_from_sys(); NewEntry.RightAscension = ra; NewEntry.Declination = dec; NewEntry.TelescopeDirection = TelescopeDirectionVectorFromAltitudeAzimuth(AltAz); NewEntry.PrivateDataSize = 0; if (!CheckForDuplicateSyncPoint(NewEntry)) { GetAlignmentDatabase().push_back(NewEntry); // Tell the client about size change UpdateSize(); // Tell the math plugin to reinitialise Initialise(this); return true; } return false; } void ScopeSim::TimerHit() { // Simulate mount movement ... INDI::Telescope::TimerHit(); // This will call ReadScopeStatus // OK I have updated the celestial reference frame RA/DEC in ReadScopeStatus // Now handle the tracking state switch(TrackState) { case SCOPE_SLEWING: if ((STOPPED == AxisStatusRA) && (STOPPED == AxisStatusDEC)) { if (ISS_ON == IUFindSwitch(&CoordSP,"TRACK")->s) { // Goto has finished start tracking DEBUG(DBG_SIMULATOR, "TimerHit - Goto finished start tracking"); TrackState = SCOPE_TRACKING; // Fall through to tracking case } else { TrackState = SCOPE_IDLE; break; } } else break; case SCOPE_TRACKING: { // Continue or start tracking // Calculate where the mount needs to be in POLLMS time // POLLMS is hardcoded to be one second double JulianOffset = 1.0 / (24.0 * 60 * 60); // TODO may need to make this longer to get a meaningful result TelescopeDirectionVector TDV; ln_hrz_posn AltAz; if (TransformCelestialToTelescope(CurrentTrackingTarget.ra, CurrentTrackingTarget.dec, JulianOffset, TDV)) AltitudeAzimuthFromTelescopeDirectionVector(TDV, AltAz); else { // Try a conversion with the stored observatory position if any bool HavePosition = false; ln_lnlat_posn Position; if ((NULL != IUFindNumber(&LocationNP, "LAT")) && ( 0 != IUFindNumber(&LocationNP, "LAT")->value) && (NULL != IUFindNumber(&LocationNP, "LONG")) && ( 0 != IUFindNumber(&LocationNP, "LONG")->value)) { // I assume that being on the equator and exactly on the prime meridian is unlikely Position.lat = IUFindNumber(&LocationNP, "LAT")->value; Position.lng = IUFindNumber(&LocationNP, "LONG")->value; HavePosition = true; } struct ln_equ_posn EquatorialCoordinates; // libnova works in decimal degrees EquatorialCoordinates.ra = CurrentTrackingTarget.ra * 360.0 / 24.0; EquatorialCoordinates.dec = CurrentTrackingTarget.dec; if (HavePosition) ln_get_hrz_from_equ(&EquatorialCoordinates, &Position, ln_get_julian_from_sys() + JulianOffset, &AltAz); else { // No sense in tracking in this case TrackState = SCOPE_IDLE; break; } } // My altitude encoder runs -90 to +90 if ((AltAz.alt > 90.0) || (AltAz.alt < -90.0)) { DEBUG(DBG_SIMULATOR, "TimerHit tracking - Altitude out of range"); // This should not happen return; } // My polar encoder runs 0 to +360 if ((AltAz.az > 360.0) || (AltAz.az < -360.0)) { DEBUG(DBG_SIMULATOR, "TimerHit tracking - Azimuth out of range"); // This should not happen return; } if (AltAz.az < 0.0) { DEBUG(DBG_SIMULATOR, "TimerHit tracking - Azimuth negative"); AltAz.az = 360.0 + AltAz.az; } long AltitudeOffsetMicrosteps = int(AltAz.alt * MICROSTEPS_PER_DEGREE - CurrentEncoderMicrostepsDEC); long AzimuthOffsetMicrosteps = int(AltAz.az * MICROSTEPS_PER_DEGREE - CurrentEncoderMicrostepsRA); DEBUGF(DBG_SIMULATOR, "TimerHit - Tracking AltitudeOffsetMicrosteps %d AzimuthOffsetMicrosteps %d", AltitudeOffsetMicrosteps, AzimuthOffsetMicrosteps); if (0 != AzimuthOffsetMicrosteps) { // Calculate the slewing rates needed to reach that position // at the correct time. This is simple as interval is one second if (AzimuthOffsetMicrosteps > 0) { if (AzimuthOffsetMicrosteps < MICROSTEPS_PER_REVOLUTION / 2.0) { // Foward AxisDirectionRA = FORWARD; AxisSlewRateRA = AzimuthOffsetMicrosteps; } else { // Reverse AxisDirectionRA = REVERSE; AxisSlewRateRA = MICROSTEPS_PER_REVOLUTION - AzimuthOffsetMicrosteps; } } else { AzimuthOffsetMicrosteps = abs(AzimuthOffsetMicrosteps); if (AzimuthOffsetMicrosteps < MICROSTEPS_PER_REVOLUTION / 2.0) { // Foward AxisDirectionRA = REVERSE; AxisSlewRateRA = AzimuthOffsetMicrosteps; } else { // Reverse AxisDirectionRA = FORWARD; AxisSlewRateRA = MICROSTEPS_PER_REVOLUTION - AzimuthOffsetMicrosteps; } } AxisSlewRateRA = abs(AzimuthOffsetMicrosteps); AxisDirectionRA = AzimuthOffsetMicrosteps > 0 ? FORWARD : REVERSE; // !!!! BEWARE INERTIA FREE MOUNT AxisStatusRA = SLEWING; DEBUGF(DBG_SIMULATOR, "TimerHit - Tracking AxisSlewRateRA %lf AxisDirectionRA %d", AxisSlewRateRA, AxisDirectionRA); } else { // Nothing to do - stop the axis AxisStatusRA = STOPPED; // !!!! BEWARE INERTIA FREE MOUNT DEBUG(DBG_SIMULATOR, "TimerHit - Tracking nothing to do stopping RA axis"); } if (0 != AltitudeOffsetMicrosteps) { // Calculate the slewing rates needed to reach that position // at the correct time. AxisSlewRateDEC = abs(AltitudeOffsetMicrosteps); AxisDirectionDEC = AltitudeOffsetMicrosteps > 0 ? FORWARD : REVERSE; // !!!! BEWARE INERTIA FREE MOUNT AxisStatusDEC = SLEWING; DEBUGF(DBG_SIMULATOR, "TimerHit - Tracking AxisSlewRateDEC %lf AxisDirectionDEC %d", AxisSlewRateDEC, AxisDirectionDEC); } else { // Nothing to do - stop the axis AxisStatusDEC = STOPPED; // !!!! BEWARE INERTIA FREE MOUNT DEBUG(DBG_SIMULATOR, "TimerHit - Tracking nothing to do stopping DEC axis"); } break; } default: break; } bool ScopeSim::Goto(double ra,double dec) { DEBUGF(DBG_SIMULATOR, "Goto - Celestial reference frame target right ascension %lf(%lf) declination %lf", ra * 360.0 / 24.0, ra, dec); if (ISS_ON == IUFindSwitch(&CoordSP,"TRACK")->s) { char RAStr[32], DecStr[32]; fs_sexa(RAStr, ra, 2, 3600); fs_sexa(DecStr, dec, 2, 3600); CurrentTrackingTarget.ra = ra; CurrentTrackingTarget.dec = dec; DEBUG(DBG_SIMULATOR, "Goto - tracking requested"); } // Call the alignment subsystem to translate the celestial reference frame coordinate // into a telescope reference frame coordinate TelescopeDirectionVector TDV; ln_hrz_posn AltAz; if (TransformCelestialToTelescope(ra, dec, 0.0, TDV)) { // The alignment subsystem has successfully transformed my coordinate AltitudeAzimuthFromTelescopeDirectionVector(TDV, AltAz); } else { // The alignment subsystem cannot transform the coordinate. // Try some simple rotations using the stored observatory position if any bool HavePosition = false; ln_lnlat_posn Position; if ((NULL != IUFindNumber(&LocationNP, "LAT")) && ( 0 != IUFindNumber(&LocationNP, "LAT")->value) && (NULL != IUFindNumber(&LocationNP, "LONG")) && ( 0 != IUFindNumber(&LocationNP, "LONG")->value)) { // I assume that being on the equator and exactly on the prime meridian is unlikely Position.lat = IUFindNumber(&LocationNP, "LAT")->value; Position.lng = IUFindNumber(&LocationNP, "LONG")->value; HavePosition = true; } struct ln_equ_posn EquatorialCoordinates; // libnova works in decimal degrees EquatorialCoordinates.ra = ra * 360.0 / 24.0; EquatorialCoordinates.dec = dec; if (HavePosition) { ln_get_hrz_from_equ(&EquatorialCoordinates, &Position, ln_get_julian_from_sys(), &AltAz); TDV = TelescopeDirectionVectorFromAltitudeAzimuth(AltAz); switch (GetApproximateMountAlignment()) { case ZENITH: break; case NORTH_CELESTIAL_POLE: // Rotate the TDV coordinate system clockwise (negative) around the y axis by 90 minus // the (positive)observatory latitude. The vector itself is rotated anticlockwise TDV.RotateAroundY(Position.lat - 90.0); break; case SOUTH_CELESTIAL_POLE: // Rotate the TDV coordinate system anticlockwise (positive) around the y axis by 90 plus // the (negative)observatory latitude. The vector itself is rotated clockwise TDV.RotateAroundY(Position.lat + 90.0); break; } AltitudeAzimuthFromTelescopeDirectionVector(TDV, AltAz); } else { // The best I can do is just do a direct conversion to Alt/Az TDV = TelescopeDirectionVectorFromEquatorialCoordinates(EquatorialCoordinates); AltitudeAzimuthFromTelescopeDirectionVector(TDV, AltAz); } } // My altitude encoder runs -90 to +90 if ((AltAz.alt > 90.0) || (AltAz.alt < -90.0)) { DEBUG(DBG_SIMULATOR, "Goto - Altitude out of range"); // This should not happen return false; } // My polar encoder runs 0 to +360 if ((AltAz.az > 360.0) || (AltAz.az < -360.0)) { DEBUG(DBG_SIMULATOR, "Goto - Azimuth out of range"); // This should not happen return false; } if (AltAz.az < 0.0) { DEBUG(DBG_SIMULATOR, "Goto - Azimuth negative"); AltAz.az = 360.0 + AltAz.az; } DEBUGF(DBG_SIMULATOR, "Goto - Scope reference frame target altitude %lf azimuth %lf", AltAz.alt, AltAz.az); GotoTargetMicrostepsDEC = int(AltAz.alt * MICROSTEPS_PER_DEGREE); if (GotoTargetMicrostepsDEC == CurrentEncoderMicrostepsDEC) AxisStatusDEC = STOPPED; else { if (GotoTargetMicrostepsDEC > CurrentEncoderMicrostepsDEC) AxisDirectionDEC = FORWARD; else AxisDirectionDEC = REVERSE; AxisStatusDEC = SLEWING_TO; } GotoTargetMicrostepsRA = int(AltAz.az * MICROSTEPS_PER_DEGREE); if (GotoTargetMicrostepsRA == CurrentEncoderMicrostepsRA) AxisStatusRA = STOPPED; else { if (GotoTargetMicrostepsRA > CurrentEncoderMicrostepsRA) AxisDirectionRA = (GotoTargetMicrostepsRA - CurrentEncoderMicrostepsRA) < MICROSTEPS_PER_REVOLUTION / 2.0 ? FORWARD : REVERSE; else AxisDirectionRA = (CurrentEncoderMicrostepsRA - GotoTargetMicrostepsRA) < MICROSTEPS_PER_REVOLUTION / 2.0 ? REVERSE : FORWARD; AxisStatusRA = SLEWING_TO; } TrackState = SCOPE_SLEWING; EqNP.s = IPS_BUSY; return true; } ## Developing Alignment Subsystem clients The Alignment Subsystem provides two API classes for use in clients. These are ClientAPIForAlignmentDatabase and ClientAPIForMathPluginManagement. Client developers can use these classes individually, however, the easiest way to use them is via the AlignmentSubsystemForClients class. To use this class simply ensure that is a parent of your client class. class LoaderClient : public INDI::BaseClient, INDI::AlignmentSubsystem::AlignmentSubsystemForClients Somewhere in the initialisation of your client make a call to the Initalise method of the AlignmentSubsystemForClients class for example: void LoaderClient::Initialise(int argc, char* argv[]) { std::string HostName("localhost"); int Port = 7624; if (argc > 1) DeviceName = argv[1]; if (argc > 2) HostName = argv[2]; if (argc > 3) { std::istringstream Parameter(argv[3]); Parameter >> Port; } AlignmentSubsystemForClients::Initialise(DeviceName.c_str(), this); setServer(HostName.c_str(), Port); watchDevice(DeviceName.c_str()); connectServer(); setBLOBMode(B_ALSO, DeviceName.c_str(), NULL); } To hook the Alignment Subsystem into the clients property handling you must ensure that the following virtual functions are overriden. virtual void newBLOB(IBLOB *bp); virtual void newDevice(INDI::BaseDevice *dp); virtual void newNumber(INumberVectorProperty *nvp); virtual void newProperty(INDI::Property *property); virtual void newSwitch(ISwitchVectorProperty *svp); A call to the Alignment Subsystems property handling functions must then be placed in the body of these functions. void LoaderClient::newBLOB(IBLOB *bp) { ProcessNewBLOB(bp); } void LoaderClient::newDevice(INDI::BaseDevice *dp) { ProcessNewDevice(dp); } void LoaderClient::newNumber(INumberVectorProperty *nvp) { ProcessNewNumber(nvp); } void LoaderClient::newProperty(INDI::Property *property) { ProcessNewProperty(property); } void LoaderClient::newSwitch(ISwitchVectorProperty *svp) { ProcessNewSwitch(svp); } See the documentation for the ClientAPIForAlignmentDatabase and ClientAPIForMathPluginManagement to see what other functionality is available. libindi/libs/indibase/alignment/devicemanager.png0000664000175000017500000011607613263645557021472 0ustar jasemjasem‰PNG  IHDRªb–܆²sBIT|dˆtEXtSoftwaregnome-screenshotï¿> IDATxœìw|ÅÚ€ŸÝ=5'½’„Bè½H"ÕŽ€ˆz¹^»`¹ú©X®Á† T6Š¢ˆ(vº éBzNÛïs’œô„`žüÎ/gw§¼3³gÞ)ïÌ(±1ÑB  M›6B"‘H$’3ÍÆM[QEQ@QPP0YmV„† ¹~]jó¨” I)u§´›Â›JÅnJ=PªäN)/D¥¢ËRT —RÖ¿òQÊ•¨Œ + ­Tœå¸WÊzRƪ UyQ* +0Ϊ„F@¤”B鰊¯$ªÂ‡UqW䦊 „ï_¥î ÜTàRPÂE9nEyODݬRÙ!ªâª ;*v[ÜI–ùH”¾*7ˆ9^IDé(?öJò£tœ¥Ý‹*\”òUNž”VÉÛ¢Šî®ÊpW¶•¤ؾcŠ¢¢( Jݺu„0 êÕ«MDDxÙÒH$‰Dr9~<ƒ;÷ **Jrr’Hˆ#<<ìlË%‘H$I!™ìÝwÕb6ŸmY$‰D")“fÂIXXh•Ç•B`aøF>«†EQ ý á3æ@ñÍ…(¨hšoŒRUÕB÷‰D"9ÿ ÀUŸü, PA鯮£ë¢P‘hš‚IÓ0›LåN6 áŸBx<:†®c†¨~å¤jf³ UÓÐTÿÄšTV‰DrÁ`ª®!†.ðx=¸œ.®HI cb$Iá!ÄÛ9’“ÏîŒl~?ÎÜí±Ù¬˜L¦Ra øþ{½^\.7 ¤¤4 <"‚à`rrrÈÈÈdÇŽìß¿«Å3š¦U³*;«/ÙdzÉÓMGã0Õpy%ÕGèºMC“Å+‘œ6”îÝ:‰à*9ö™±¸Ýb,*Ï÷hNB°aè`Ã@QUPUUã`Ž‹‡~ÛÈQ—ŽÙb) £à¿ÇãAÓTºuëN\\,†a ë†áEUMhšoè/-íK–,Åíñ`³XÐLZõ•0ÈËÌ&ÇÐ{T4Í„Õf%ÈfB=U•ð’ã®ÃðÇïcpäN>yêU~ȶtŠ••áÌ&-[4!8ÛÂKvzù¨ö¢‚µ*˜ŸŸçÜãÙäê 9Bˆ ÒPxrs8ž§*ö°B, B÷—ë$ß­c@Q0™-Ø6ì&t¹f¢â#1妓'T©¬$’Ó@vvNõ†þ aàñxI´j¼Ö«váÁ“‘]®û8‹·.iÎ]?m U÷b2™ 礼^MÓ¸ôÒ~X¬frssü Ž äq£¨¾Å^!¡Á\zé¥,Zôn¯‹¢ ijÕS* òó‚éõÈxnh‚Vpß“Ãá=ÛXûÛb¾þeî ¬§ ²º=´íê˜Æt¬ocÁ:{ud®Õ‹Þö¾¸³pY>Å·9v t²ój1rÊ£ Š„ãß=Ø©_ÈÊJèäæDpå+¯su,œý(.ÊÄâU¨3ìi¦ IްàÙ ÌÜåDØ›rÙWзm‘À“ÅÞMkX8k.«Žé8iÃÓo¥ G˜õÀ,ȵ¤U&„D"9ª5ô' ðzÜ<Ñ9Sn:.W~År³0Û‚x²m"·,Û‰É?lgn—‹Kz÷BU!?/ßîEŸ‚¹(EQðz½˜ÍfºwïÌ?þ‚IÕª§¨†‰Ðè"%ePÍÁÔJiÇà”v ¸ä[žœð -6, €;ÏIn¾¯EÕ°Ù ¶i(83³Éö€bubB¼yÙÏ3@³âþ…7?´Ó=|‹×¹pX,€Àãt’›çÁcøŒFÌ6a_˜åÇWëžX°¤ ˇµà»É[0BÍàrÜy(ý#}OÃ#0釨xssÉrú{ (hf3A6“B'?'Ÿ|îsà“Ãn'Ø^ ‡ÀSQþÅï$×éÁkøÜ¨šŠÙj'$¨ œJÒ+ œyùä»u¼º_ÍBX¸ó)Ҷ£Ùÿ>’d³bÊ‹|²Ý‹¦&qÃS÷pid€cs(u[·&nÆl¼¨E-jð`{<“\ÍFd„£Â<.?]ªÇIN®·.|yf2jÇzêÚ7É9I•{TB€×ëa`-±"OF©ynÒò=4‹rºÝv<`³FB°=?›ZÁá Œæ‡cnL&ºG§V­Z;äåù”ÝÏ?ÿŠ×ë-3î~ýúàñx°ÛƒHHL 55“¨†Ž-‘¾ƒ³åþ/bØchÑç:îÕ–°úƒ¹oèŸÜ;ã&‡‚3ÇB£!71j@;ê…(¸ýÃ’¯>åã¥GA·ÐáÑ÷¹»‰BÞò¹}ên,&/!ƒ&0mhìÉÝÏäªQýigÊB]·†éGtLN…„^×sÃN4±‚'ƒ­‹Þâ¹/÷¡v—ŸÕa*¡¬†®W4dk¿èJ.‰z†Åy †7¡W7.¬H•°‚• äæjÔ>Ž;{Ô#:Hœ¤nYÉœ÷?ge†z}¾M‰³¢žô,Ÿ÷ þzK†7ê•F¦†]3È˳Ñtð ï×–äp™G²õ·OxkÑ,öŠó×êPq»Bè0üfú·K¡~¬ pîÿ‘gÿ7›ƒV[õ•U©wÜDl·[yjT#Ì¸Ùøñ ¼¹&Ÿ`‹‡¼„ît‰HgÑ„§øàï,pÄѸqÇÒMØ4ƒ¢ä8†¿4á{gp׸åÄ*/T=´ìt=1›cͯä¡ë/¡u-{V1í…ÏØ¬[±He%¹€QÁ÷®ìàñêôŒ±ãÍÍÂðz0¼Ž8u^\» ©™^[Ò²x~Í^ޏôB7ÞÜ,zÅØñxuŸÂÓujátæãõzðx<ôë×›¾}/)üôëדÉä³,ôzp:󩘀î§ZŸ ×ìÁDØòؾh/ÿ쾌ìÖ‹z¸qæ êÇ£×¶§^ˆNN– KTCúüçÆ^dÅ­æ²aÉ.‚š¶#Añàò†Ò¼}Wüͱ:×È7H:Ž £{ø””ð¢›Ã óäáö¨$W_ŽK”J.Ì„FØÐ³u — JB亰4Bÿ ßí‹Ü‰C†oP4Qf7Ù™¹x°Û¤w Ú;†®û*CÇápàv» ìÍåççû”Å×Kéº×ÿ_'((Èwå2WŒEÅfs²{Í^ôKš£ÇSËf°ÝÜ™úE™ùÐSÌÙ£Û÷a&ÿ'…vƒÛ¾êW2þ^Æ’iÑŠŽµf1+«1Ý’RYº: èâ±…vä_Cjn6LÏs‹öáT­„ˆè^a|a«–à¶XŠ 5J0ÑþNì®ù_cŒ¸šF=‡Ðö«O ¹ª=v2øyÎfÚŽêB¸#‡j YÍì™vÃÞ6‚#²÷Nƒ¨æ4 ÿ‚]™ág±øòöÎp?ý<£“´ïQuË^LÁZa|κ0òâP —å/?Êk«ó¨3b/]Q^Ü!=*Iﯤk²<ñ ooó Ùì„YÐ trÜIÜòü¿ia+«|ü=uïíQ .£ûåHI oå›Lüz?–0+*43–Ãßóþx¸O,ɃïäåÁNöÿñßÎ]À¯{<„Øß½#|ùÔc|qPGÕ¬„†T”?%ò¸ ]V3–Ä¡Ä*;yÿ‰‰|ŸêÍNhˆ sIÍ(‘\`TݘBõA n·«ð¶‘q”Æ!†vlɇKÿdT×V47Òñf¥ùUl†!|"~ETГ*PJr~×u½ðÚjÕ0D5—éVʃZ´©@`Jh‰{^F<ÿ6#½E$® ²3þâ‡í7Ð %†ÎícùþH7)ÀÁ%,KU+—)± I àZÏW¿ÄL¤"0Ü.´Jã38lˆ"«D!0T;A^2÷®àûUy¤s+†]ç&(E½‹™¿ÉFSkUàηÑzä½Ü>0…bya!ÈRRõ+X”L¶n=É8¢‚Ñ W^0-*ÃÕ”ç&~XŸ‹­ä‹0P«¿éÅ<)XC>C|½$„.¬ÄÔŽ*Ñ$(Êó«ð¿+”ó@P相åÏ LY‘MþÌl÷²yúãܾ¬3ýûõ¤Wçdj·½”ÛÚö óûOòâoPL9ªØ‚mØ5;ÏVaþ”Ìckˆ‡ª“wx-«õ£T2·¼öýVýÌ×_-dÕQ`9î'¹Àñ+ªÊªÂgØïªûé°eïA¾Ü’ÉÈ.­˜»fIÃhf)|®¨*9ùNÿ¶í>ÅätºP(¨j‘¢*ËìÜ0 C—Ëå·¬^BK: ]Ç™o§y×$ßhÖ>9¨Šïšc¬Z´š#ÃxFö?dk&Ljkoå–”Æ$\Ü“Þ Ñ€=?ýNªZ†DA˜Â(ÜÅY ø,+O+î*ÞPl„Y\d;³Y?ï7Ò:÷§öÅ/}½ŒC9ñ5)l„ÚÀ\ÿEßÇ3`³«6—îGÝòvNW^·îÿ®¢àEiYY~¥/ cÊ(‰ÊÒ«ªe%v¿6bÞÌ3#FSöÌ&˜BÌEïJ`‡æMeqݹ±M]nWîÓ¼³ÁƒÃoö©˜Tœ{V1çÍ¥Ìx§6ýï}„›[Ûh}U_jÿ:“Ý…á)¨Šÿ}r»PZÞR½<  »i›Àž«.çò¾-Hê4˜{:µgî£OñyªB´}—\ÀTyèE ªÿd;i®º¯7´=ÛË+Û²Ó·#-¬nè‹ —sO£`š„úöT3;r\¨š†O1©dffâ@ªª€o–ï»_*_S¯×·c…¢(äädº¯:¥ÝÎ|rmu¸hø(îêÀá_~faÂ8²C´¢6ÁجdÎw{È5̡ф‹lœ& »î;ÖºsQ|?†ÅúVæ/OÇb6á.Q{z ´µdpç&ýr˜L8B-UŒ/0 ¡X µxÈó€±ï'¾ÙÙŸÑÉ@Ö ¾ü#Ý’‡o–ÊF¨ÍDhb,€½?óù·K8fmJû‘ý¨[æÐYYÙ¨V†‚7õŽÐ’{ vŠaòJ1‘Ö€@TôJÓ«¢x*/CpGéûeú+î_8÷±è•çàáǹ±i½îÇ_`Aº†‰`š´K cËVvçrHÏò6°8|Vxîru@ '9Á†k.ÍFbµóØ÷ŽëŠ`c‹§ObñÌ$®ø?†Å×¢Kûh>Ÿw ¡©îÒÉOÕ­þ ‹ÙÌ’c¹4á·Ð ²1¶ošºSñ=Fƒpèßkê.„7ÏçY³ðsš‹9“ÉDZÚQ;º®£ª¾VïÊ•«ËŒÛëõ`“Éä³ø3™«7ôW¢’Jö,3‡w‘·a6“æÄd3!Ò–2cyìB«›ždúM^<„YqóûÄ{ye»Ã¤bqmfÞÒ,.êãÛ*ÍBÖdk˜ì¯ PG—ñù¯cío›Èg·xÐU3ê¶w¹åéªÄPM av`×<8=šé8Kg-¦ýÐzd/YÈÄâuá«ï­„XéÛw“C<Áõo䥗.æ@ž¤ ”T©,V ŽWÆÑ¥ÌùccÚÑñމ||GÉPUL•åï?FIOþ½$Ë—µrÊò|˜…/¿Nüóÿ¥_d2×?p [™Éθ+¸õîK‰*ÃGΟ«Ø§k˜ÙÏï; Ú6²Ðþ¾Wù8σ9 Ͼ_Í+_{ ×Ëè×:‘=‹LGSÚÖ1!¼ L•å/®ÒÃy¢jïiE”ö®a×·ðÑË h>~ ±ý¸gøŒû~7K~ßI׿IÄ:|=|÷ñ½ü½lÏÙŒa5aÙ,}c*unA¿¦‘Ø‚Ìä1ðnù´zy,@ bVóØŸšOóØ(âCAÏÚÏêÅŸóîê…“Ê˱¿—±zë1Ü#›5/ß̈{?c§ûÔˆk¸Òغf Û2*û‘H$8Á•ðjz6ÿ¬^ËæÎ—cPBF#ƒµ³>c~ÈÝôTCå¯&ÅÒPYÍndóÏOŸ3{Á2þÞ›‰…àø&´ïsÿº¼~QB€ÐŽM¤¶qJ2Ë{`!/Oø•ϽAÃËÉ(‘H.NpèÊúó²ÿËqŒû®½vÍm'.\õåò÷0ðÉ(¡‘þ'?ï4Ñbl#‚D>û–~Á'ó–±ùH>ø†t¸ìfnèsE_*úÓÓYñÆ#Lú-“ÄNƒuE 1&'©»6°%SGUDñADÅF£‘OòBáS!j±ªäC"‘Hà„w¦(Õ_ @”óýLR²Ïgþç¯ì´4ç¾F6ò7¿ÏÄiË0·¿®I&LÍ%uײÃlhçTé·º¬°‹+Èøý]Þø-›f£'ñØ D û3]/á2òƒp±iÊm<±ã*&¿tñà9ʪÙïðÉwr(_%$© ×Ür ƒ9Pðp`Ñ^ûæoö¥æà´°\<üþÓ·6¥@¾ã,|øzPÑo<ÏàX­<±%‰8AEeT2‡ ŠÏe»Óó×¼™µøä#¡-—^w#—µ Gó{tî_ʬ¿féÖc¸±wÑ(þïöî„îý‚ç^þŽÙ^ÀN|›Üt󚆨ŽRŒ`ÆwŒu¿îÁÚü*Rì:G×o![kɽ·\CÛ ¿›Ý‹dF·óG>ùx>+we¢[¢iÖ{87í@´ <û¿çÝdîTrtÐâz3fà>^ûÐͨ—ž o´oêÏ»o?þ MǽÄͬèY›øö£,Z³Ÿ\ìÄ·¹”Q£/§E˜ŠÈû‡¯ßýŒ%›÷–/ÀZŸkžÇå‰å‘! E.¿—"޳nþœQƒ¸¡oæ2]Ü×"MÓŸàÅßb¸òÖÿÑ1*›?>‹'¼Cü”{h¬“±ý/vºZ1úÁÞ$šrؽd&Ÿ¼3‘ ú¯2:Åê/˜®cÆqu=3(V"£Ôrå–zJ"‘pÂCåk*QÜMIgÂÉ?³&òò÷ÐyøŒ¬ {—ÎfÖ¤çp=þ$Ó­èéËyó™÷ÙR»/×iMœ)—twBUÙ†Á£›fÃ{x-_|øoÌjÌK77ÂVކÔÓþà·½6Z]Ó*žÄ4};¿®ÚGÓžµ}-þô£¿ðÚsŸq¬ý¶ÿ7>šþ¯?Ë“ƒãñ¦þɪí:=ÿuâL¸Ý!4¨µ:ÌbíŽúD… `p|ó_µ6æÆD ¸öòÍ “øFíÅÜD‚w;‹>˜Å+S£xñî„çícÝºÝØü‡ÿ¶ŠDqz‰ŽR+i”ó=WÛ´z-H0Wà. ëJ†+2×1û‡ã4¿ãYFv‹@’oMç÷13øq»“v­ý69!É´mךx Z7 açÚñ¬]s„Q êúÃ4_—zu-¥ã•H$’r8Á¡¿ŠæJ¶ÎK<Íþ‹¯~›ô!+¢ûsÓ˜‹¨ëð°cÞd¦ï+ðºÿ»÷Èj–¢ÍuIØJ„g «OÇAõ騝??¿ö4½ÿ9½p­/þøËù¿Û;¸ÒÌä Ú$Ð…(Š£0@•èvÝ©óÅ<~ý'›Ú9¿±ÝÔ‚»Ð1„¥£ÆÝDÓ@KHÅLhŒŠÈ È¡*."*ÚÁª‚}ñÔÚök‚ööwÌ\Ñ—zÄPŽù£LÕg‰iD‚²˜Ýûb{Ô¦´qyŠªŠÙŽÙ®ªn»%÷ú“H$>NPQìógø'ùÂ0(éL‹jH‚ö [6ÅïÀ{„õ[²1%¦e²!ê‡ÃÒulÏîDÛ€¡?#s7;³Ì4=˜.m€oŒ öùãò+¡膆—ë—s0¨#ëYË—Y‹¦iËØ|ˆÃyê&…Ã’d8Ó8¬D…) <JÕ00Œ¢çJd{ú4˜Ãg‹WP/o'öö÷ÐÐ.0 •°º XÅVvæ‡Ò+)¨D5,0 Ÿæm‰œ®’;è·rÃÒGøpʃ<¾ñrú´©G„ÅCæÁílÉhÊÈë[å€u¿³lcG†´ +‚Þž¡½#yæë癤£oóX,ù©ìϪC¯¾ qTA§˜"S¨”Çê™_±ths¬iXZô¤uT¹jS"‘H€6¦0¢|E%àÍ`ç¦õx ›ß ¦°z4JlΕ—D3qîÞ7_M·DÁÞe_2/5†þÿn† {ï4üu&ï¼ö)×ôoM¼]'+ÓDãV Ôò°nÁ·,WZï08pÌ…¯b +áAp|ã*ÖŒ¡mL:«W!¸õõ$YýráåðÏ3ïh"ÍÄa‡¼#›ùiÞˆ@}‡™„Þ—’òëlÞýc2ú·¡¶C“šŠÚ¼'bL;BPK+¡´îÛŒSg1‡Hú_Ÿ„ÕßK±5@ßøu|ûÖ‚®êC‹8+ތ䇶§Wó0(ÈÏ ó¶t9TºÑ¸9‘Aã&÷ÍLæþ4w~ÎGÖˆº4ëÚ·°PoÐuô\ÿ>_N_Jçç÷¯8hùï§ù¿ð™ùý»¼ø•Zu»þ‹N½⨊®±7gÔ8úî×¼þì\”І\ñp·r•Ü<]"‘pzzTú¾|kr±'æVw3éÎfÔ¿ê~ÆXgðÅü·Y•Añ­¸ìîë”dFJÔÅÜõ Â³ñÅÛ¿áA%$©w·½‚a·_ŽsƼ÷êw(Öj5‹Ä, 5’ÎW]ÂÚé¿0ó§64걩Á´¾¡Ã/:†ÍkÛÏÌøá8.su[ âΡHÔ ˆîÉݪ|ñÅ÷|=m).ÀÙ>u»Ñ.J-Ñc,^£:šöã¢õ, ïMø€s¢Lµ¹ìþ{±ÍþŠf¿Í^Pì±´Ò„îMCP"åwj{T™Cû«ÇÐþêržÇöà®I=¸Ë™x×ûÌ.á¿Ãðé0¼,ÏVš–t¯ÅsÅ˳¸¢èQF3¾ÓèªË,‘H$€2gÖ§¢ß€Á•»ô³iÃß4hØ·ËuÅ:xÙ?÷ižYÖ˜Æ_OCkå>Î5,V+k_IßCp»kzyT‹ÅÊ‹æÓõâ^g[‰DrY¼èÛÓÑ£ª!x²rõ1BÚt¢¶¹ô\Ùù@/‰D"9œ·ŠÊsp«ÓChÓ1sá°ßùEAœŽùœaÃŠÆøfÏžUËÓƒœ£’H$œñó¨î{o…ϧ¼þê …[Sí+ÿú•@E¦ôç8…é:õµúÙPNÅ‘šJ"‘ø8ã=ª×_}¹Ò°%UCæ•D"¹8AE%ªnB-9mÈ]$É…ÀIô¨d%y¶9sTg›ó1M‰äÄ8±~…ÃNg¢^íùX«Ÿi’H$'BµUFVYÛN‡,I)–/ùål‹ ‘HNÁŽ`’RÇã®–ßê÷¨t7ý_Q¹;ÉE9Ïz .§S¾gÉyBvVÇÒŽ²eãß4iÞ’ÐаÊ=p‚Ç|€3?¿rGÉI ß1‰äüÀd2 4f÷Îí´jÓ¾ZþÕÊH$‰Drâ(Š‚Åb&$$”¼ÜÜjû—ŠJ"‘H$gUUOhý§TT‰D"©ÑHE%‘H$’TT‰D"©ÑHE%‘H$’TT‰D"©ÑHE%‘H$’Í /ø­ kW¯8ÁK$’Jû‹ºœm$ç§UQtëÑûtG!©"ÿlÙHÃ&Í϶?çky,ûí§³-‚ä<ã´+ªóötÝsY5 YIåÈ9*‰D"‘ÔhN{ Ùb¬YÈò¨YÈòH*Eý]`Èò¨YÈò8<9i<ê%&)ž óë$œ ©¨.0dyÔ,.¸òðìaÆ}òe*Ä…—¯IôUB"›åÿ»™W¶€½ÛÿxçžæØN2ªü¿^â?Ï®ÆÚ‡ñoÜFcë)_rV8ýŠêtG ©þ Q–G A–ÇéG2ÏNÿU5_áNãŸõ»P¶§ahUm= r÷n`}zíÛÄa®¾ç5Š¢”‚üéžmÎZyˆ|mÚÀ¡æ´­tNœ mdoã»O>æë¥[9æÑ«ß‘×ÝÄm"1"w; ß—¯–ï$C“#Žæ—?ÀCWÕ+ $ëG»áG¢.{žÉ7$aìÿ•Þ™Í/[Òp£aj@ß±rcSûÙI¨¤BÎÀÐ_©;äl[ȇÎeù–4œ€9¼.-/Å]#;¼!/ÿ…‹ž‹”KcÉgý;Oò¼÷^>hGعð <ƒ/•*ãß?t SÝW2þÙhyyëÿ¯ñ¤ÿk*/ŠE;3âž÷TX"ƒeÆ2imNá-sXmš^Ô—«G ¢UäIüd;˜1a"»®™B«:A5¿<];™ýäcÌÙç»TÐÉܵ‚™¶’öØKÜÚÊ̶Ϟçƒ%™€èø0ôãÇȲÚJ˜3+8Âñ(&j…šÀ»Ÿo&½É÷K$ñQ Yi™(¶3Ðn—œg¼Ge_Ê«O¼Ë1sÝ]¨"ÈÜ¿•í"»&Ü‹R~+£:~. òB)ã^‰ë}sÿr“Æ]J¼©¤[™¯§†*”‡ð’›‘µ‡òðímpxóÈØ¿žÅŸÈ“kvóØ«wÓ.ôd[b5£<ͺëf•÷TµnóöZcnzþ1'd±ä•˜¼:f,ãêf]HÛ— @xŸxéÖ–8Ðñjq%Ú›G&ÌQå¯gO€•öwMäÿºD ^tE*ªšÊ0O/~é>°†-îPzßqC›úßœ‹ºÒ×ïÖ#޳à¡ëX@=þýÖ‹ vlâíG_ä‡}¹¨„ÖïÆ°;oc`²½èg¿õ5F_óAÝŸâÝ;Þºå v]=™W®J@ôÃ_sÿ]sH~úîinCOûOßø€…§âB%¸þ@ÿošžìLîYÆ—íeThåè)¢Z»ñ&~VçF5.n!å¯×Dîú ÊÀÃ…¯ó꼿ٓšƒ0Ƕaðå­È[õË7$G8¨Ûu8cïD²Ýž±ž¯¦}À¼•{È!ˆÄC¸ùŽ¡´ ¯ñíýjQíò©GÓ&M Q€–íiŸìá®q‹™·i4í:cdmæ›÷Þã«å»È2¬Ôj})×ß:’nµ|£ž½ß0yê"þÚ~˜l´øŒÖ·­ÑþÇpíǾhš=8ñƒO}‚O7‡7ìÆÐh =ëXQ”.º´ÖÕ«píý‹ž¾4¼¸9æ-Éøñnþ»9— ºŠ¡—¶"ª¢×ÇV—îm¬\™ËÚWnãÖù3ðÊ¡ é_ó{™(gÀ˜¢ø/ÑQ–ðÇk8Ü  q¥„{€`ºÞóCë™A±¥ñt1†®aXòwóóïðî¤Xš½zõÌþXêä©1í QA ®……íRŠ÷×BdòûÔ—™{øbn¢ɶIoLj±×ÓØz˜å3?`Ò¸ll¯ÞE»P÷‘µ,ÛªÓ÷öGè–`Âå %Áœ@Ìà‡x°O,&ì±ö2d83”gõWu4bû>ÌKaóùü«,ݱ‘¦odÙ†1¼òÀÅ8+iT¡„ÑiÌ‹<Öè æÌÿ™Íÿ,aæ‹ËYwã$ž’p&†™$ÕäŒ/øÕj àþÛ÷ðü´—¹sy"m{_ÊÀ—Ð6Á×3òýhÌ„ÅסnÝ‚9* ’"ý×)$^¿ŠŸžÿ›9é.|ÑX£©]¯nÑ•3p.À¯¨D@E ;9žáA‹jDëæ)ÄhÜÈÿìœÑTY‰‚Ë „/ùLݨÖáfºz;ã&O¦eò8ú„èÏ?µ e’Lë6-ˆ× ¡c+¿Œ[MÞ½éœb6õp­XÊküCÖ°:Ø7ÍaÞžÚŒœ<š>ñ­£×qË«?°!³'=ÃÏÑÉÆSQ†·Û…ӓ˱}ëY<ýR•† Iq¿ùC¾ÚNÿñ÷2´‰ hM³$…=wNcÆÒki50Ög$Í:¶¥EÁP¡s#ÖˆDêÕ-ê=ÔLy µZ$a^üžm ùu_{'d±æû¿qÔnI‚EàÊÌ'²Ã5ÜÓñJF-{™û^[Mþ¿²Ëu1-m¡ØOö^ödé4‰QѽMó’™c§ÅÛi=èzþ~÷žùá8Û–l$sPQçbé<ç´+*Ã(ù#0ßûn^í>‚Í+~bñw_0aág4ºúaÑ[À;ðddoeÁôOX´n©Ù6»ŽN.Q\ùˆ€öa€r*RTÏÔH:]Û›o^˜Ê˜{VÒoà õiM-ë9ZAVRåaþ¯)Cÿ˨õÿÇ´W¾¥ñCµ Ÿ!Ð+)àVÐBkJ.i9^„0"¢ì°+“|ÝCÞö=äs€ÆŒà£bBÅ“šíE„ÿmÛrËcëdn9¹ð¶Ó†«¾Kã /ÛI®9™u­…ï¾Þ”¶µ`îúƒ¸Äÿ ¾óÿ=Ô\BÛ`Hâ_|u`+Ó¸(èo†ÑkDwbÔ ~}ñ6ÞØDdlZÎ!ò"êaKbšXfµkïÞõ/>³zPZ=Ì[w ¦ŽÈ%œØ+ù‡Ž”‡]*©É™ú+ Á?Ro‰¦YÏa4ëqC¾üÏz“¹^a„R¨©š¤Y¬~c<Ó·4eø­ãh—`'÷ï÷yöcWqwþ QÐ0<Þ"eø_(„w¸WßéÍòEóøò“ ,ü²3cŸËÅrŸ»”[øóÑ”Èà±7±î¿òÚü8ÀwßȬ^PT3Ý(z®Y4:¢àžÒˆ›ÇßF‹@«`ÅLXœZ–ÉèyG¹åQçZ¹µ!f!‘±Ä†û-Ù„7ÀsAo˜2ó¿°Lmb¸_Ó±6`ÄSOüáÇ|³bºBpv\zÝ¿Ú6EO…Zõ ß½‹ô#y€˜¦Ý¸zô0’- XÚqÛ×àyï[þ<ì$Ïe§¶ÃM¾K#º^¶mÇH=˜#Ié8˜ÿÝ¢h¸PR£8#Ƹ¾ lÔiÓ‚¨Y Ùî†8;f\d;õ"åâ9Ê–Ý.º^ÃÎ ±άH,ò·L63¤eãÔ¡:F ":VîJÅi$âPŠÇ„¯…¥pñðûéÚûGž;¯W¤ËÀóÔ»dyÖiE­l5¶7wþûwîŸú). QiUŽ=ªb-ù‚È oiDÔ¯ƒMlb{~8—¦8J™ÔÌ!©SLyåáH ¥aCŸ1ö†@#"%‰ ÏFVïΧuŸÕž¾‰u‡!a@f!p?ckØÍàÊv¢ qvv¤6×ãº)³¹®ä}%„®OϦk‰Ûjh3.;‘ËÇ––K¯1ÏÓkLy‘i„µΣ¯/õäæñí¹¹ÚÂKÎgÆ<= QçÚþS¿÷аE ñaŒì}¬™ÿÇ”d.O°¢…¦”ÇêYsYru3¬ǰ´hEƒ3óW~Å·ÍÓ2ÖŽ{_& óâšÅÃ'ó™¹8†ž±.RsjsñÅñtìÇœ¯ßåí¹#éÙ qhYbylÿñF%“ª’³si†F­P+ЬVÏ'Š—Gà÷¢Æ·FôÅ·rÓ’ûyg£ÇçF‹¬¤ J„p/pÈ5ð¿½É• J\Å—/¿ˆcÄ@ZÇÛñ?HZX'úµŽ8? ¥¨JyÇÖè*®¨¿œ/¿NØýib;ŠYÓù'¤wA-£,0ÇÒ¤¶ÆâŸf3¿Á’8FFDz6 9'ÿJ.\NÿU±V±À«†t|1_Oûš €˜FùØ(úÆ©š2êöþ¤½÷5“'ÎE mÈåÿ×…a·å²©3÷Õg˜  Ù ¯Û‘ TâúÜÆ [§0ûƒYŠ•Z]n¦u×:Ô½úAîÈy›ŸOf¥h¢ë· A˜Š0²Ø¿éG¦/ÿˆ<[,ÍÝÍ­…0(s^a”ê¥ôrÓ¬EÓó–ëùåþq!J+,Q"¤À%îø¿[“¸æa›þ >~…P‚jÑnh z·¿ Π©rybNä²qO }0¯§Läsa%¶eî¹8mBÊ. ”0:Þüz¼ú13^Y¦HZH¡k£`¹›‹¤F£Ì™õ©è7`p•=,_ò ý_3?¿R·kW¯ uÛ>ó\ÉYçàþ½Ä'Ö‘åQC8_ËãÏu«åQô’2ÉÏËå÷•Ëèzq¯*ûY¼èÛ3³dà‚˜k8‡åQ³å!‘TŒ<æãC–GÍB–‡DR9RQ]`Èò¨YÈòH*§Æó!9ÝÈò¨YÈòH*ã´+*«UžïR“åQ³å!‘TÎiWT ¾ùêtG!©Öÿ}¶Ep>–Gl\ÜÙAržqÚÕU×–Zƒ.‘HÎc–ýöÓ‰GÁ±£©dddàõzä|ß)Âd2ATt ªZ3V3žÿ»~J$’ó!»vmÇj±’ܰ!v{Ðy·íl „ ??ƒö³k×v’“Öˆ|•ŠJ"‘œs¤¥Á¬™HNiT´¼ÿì¯ÀžUÁu`e{!õ¼J*™‚´—u¿ ¯ìö ¤4⟭›9v4•èØ³?”[3úuI5ñìÿŽw^ý„¹'^éxö/æ½É3Ø”w “œަ¥_»NáuAÅX—WI_ˆnÂ]R™—¥àj×áèÑÔ3*gyHE%©#ßÞÏ€þwóåoåÎ+À“¾UKþ$íä‚)Ľ{ŸÌYÆAÏÉ„1Ÿé³ã€û­ÀÎUÜ.AA t¯©àºäÀ0ŒrÃ<1 £Xš+Ê‹’Š=(ÈÛí:ýBVó~èï¯Õ?Q·nqÉg[”sKd"õëyˆ²žL›FçØÏyà­x&ÍkCôyÿÖž½3úúwØSƳÖã0¥gˆÿJàNýƒo>É‚¥±=£ÏšÂÀh#ss§NeÆq8µ/bÈmr[¯øÒÏé¬xåþo®‹kޙŽM­§7§# P²GP–2*v뤬Jö4K>+¸_ò Ú‚çz É«óºÊøiþG¤äM&'ü%©¨N•ˆ.ÿå-¹¿èÁTk O¾Õ†üÂúÂÃþo&0á‡Zôj`óß3Èúãîh.Þ.Wsù×Ð 1ž¤0Õç~Ád¦oN䪱Ïâ8ΚS˜õø£DÏx‡µ~î"—õÓîç‰ïrPÏÁ½ÓE@ªø¯ {T_F-„(vŒÉÙ5 8÷üÄ?»¸xä êYÊrãåȲ9|›ÚšáW6Áq2—P>e8ˆ_‘IEuz0 ƒ}ûö±gË*e=‹»Éc$7ìy¶Åª™è©üôÊÓ¼·d+{Ó€™˜¦—0ì®1 󳑻ô>âæñoÞ¤_¸‚kË;Œ}p&›2<€ƒzÝFpÿ#7Ò.L#—­_½ÌóüÀ?™ØjÑ}ìk]ʆ=鸨Þ/ðÑ“]Õ²bú˼ùÅRvç@HRw®½û¿ÜÐ):à¥ÝÄ”Ñx:-,±´¾ìN¹³‰ ÿž¾j,ÛnøŒ¯¯ƒ ðîŸÁ¿¯ûˆ†“¿äñ6 mwóù##yqÃ~òÕ0R.¹ž{ïNë°3?*®X¢Ii]xí=0‡78N›{¦pUmŸ21Ž/á…ÇRûáy¬wL‰°™ºÃßà‹á&Ì~ñ;ÕKgåMÓY±ËɈÚÁ~w:GœÀ£ß&pï„>̺ΙHÞ)¥Øp€¢œÁU¼·…ÿyM!wû"¾ø6Œ¦ÃQ·LEåfßOŸ3/;š+¯lrÊO.elRp?ÀMÒ¯)½ÏóJQ†ÁÖ­[©_¿>Vkr=Kƒ¶Cøtò=4KÔi{õ”³,e ÂÈfûï‘Zÿžz¸ vç~VÏ™Æwo#ûÝiÜÒØVÊ‹)®+£jKXdž}¿2í…÷xâͶ|þHkL;?å‰W#nôÓ¼Ñ5qlµ#‹^2Sþ;ù^ZÚA±DPÛ¬³yÝϬËhÇÝ㯤Q°‡é?L8ˆ¸ Oü,: »Ü“1ožbÙKY@RFJ): ]*ªSŒalܸ‘¸¸8EÁb± Çv!++‹yïÝGóàÍÔéôÎÙ³Fœtݺ4ÅJGºti‚2ê6f½¿†áÏu/uʮт‹»û/š5À¼nwmø‹TOkârŽE]Úw e Mýu¿ç’Р°‰èôý‹hF·n(™2ŽÿÈ{_¤îÍ3îº$Ì@ǶuÉßzŸ¼·Š‘/õòWÆqtê׋.á \ÔuÍržüéŽ_WØ*§¾ÃnI¿pèLëÈC\;n&óv]Æ Ëlîž¼òîo:]þ7Œ”‚©##›MKwCÂ.¿c -j™I]6 “Æ’1ƒ§{F³Ž22~gêÄïPú<ÍÈ‚´è‡YøüÛ¼ô9&´ A9p†vІQxz4P¬GÅ•ÓéUSö~÷9ëE0êö/ùz{ï2xö`*“­*nªE‰|Bˆ¢a¾'2JEuê0 ƒõëד@XX»ví¢víÚx½^Ž?N³‰Mß%:±ÙÙµæcK¦GÛPf¯û‹Ãžî${(ÈÛþ-oM™Éo›ö‘î¶àPíÄ ØšŒä†6Kyëîálºôj†½ŠÞM#ªý’¹þÉNo$=.ªUÔ;0'бm8¬ø“Cî^Ô-éI±S«^8¬>FŽN5U±@iÜ™º,aãÞ|DCËYšÏp³cÞìïÇ}]#‹”žÉÁ AP“þ éÑ;дÁC¤þ:œ)_þIvKó l¤¯àõ±°(öÞx¨‘*€AæŠ7y{g'yº%N‘æ§ØÉÈ=«bÜ 0¸8Õˆ¼|µ(&£§ë/Oòéçk6®ka9€AÖ†¹L}ïkVîÍ šÔÊ|§ˆû4Q;½ÇÛ_,a{†Ž%¶INâÊIשDQ|ïx` Q8TZúê³Ãyaž¾{÷nâââPU•ƒ’””ÄÞ½{±Z­X­VuÿKlm©¤ªŒª€0JÿFòþfòýϳÀÙ•;&Nåƒi/qgçà¢ç¶Œ|í+>}áF™Ë3·^Åmïn&ïŒ¼ë ª¦‚¡ûç,T4t^½©0üþO‡ŒUĵƒ…?¤Õk÷¬ULX5pfäPh•¯…Q;Ú„‘™Fž¿ñk_Á«w=ÌÂÈ[™òâõ4²$&ÿÿÙ;ïø¨ªô?÷NŸô!BЫté6°¬ŠXu×¶úS×®kß]{Y; *ꊽ`£éUzM! )“ÉÌ=ç÷ÇÌL ‚ÈÎÃç2·Ÿ÷žó÷=ï9—µÏ£¨ðþqÆ  À‹^b«ÌgƤ‘\ûy‘ñûùðT±=…Gôêvüò1?чó¶cÀ¹=Ñý„ù9FèßžÏyèwX3Œë»®NSYf·AÞ¼§¹çõ…ÈÞ—sÛ=wsÙ­±ø~çÙêp9TµÕs}rBxTM›6eéÒ¥´lÙ·ÛÍÎ;ÉÈÈ`ûöíÄÄÄ`µžy|ðíeùª"¬-:R-òel`mžw_ƨQ@"Í ›ÃNÒݤ÷½€ÛúŒaÄ3¸þÃX?þ^Z¸íà+¡ÌW%*^+ö¦]iiý„å¿ìÃ×!ú÷‡Ÿ—bËìBê‘Dä,14‰…9÷R.3°‘ðò—Ïa+ÍŸá®7­òíZÀâ¼XzmI• ’5‘6.į”ëè IDAT‹ÙR6ˆ®nÀ·Ÿµ»ü¸Z· Öøwóé½÷òEìD^~â¢0‘pÑõŽ·˜VVÙøû¾ä®Û¾¦ÝOsmï¤óËÕl@eèUÍôêªûÁ·iìeÞ'k‰ø0íÜ`ï|bïå‹ï¶sÚø lT°õ«ÏùÍÝŸ»oO·( èD†w³^%ð£È¿—¹-Ãßázî¸jPÀóíšcÙ7¬+–ç«{˃Tͬšæ/Ñ4‹ªºÄétÒªU«P"…)V.—‹²²2Ú´iSß&F4yó¦25cR$[¿y•7v&qæÿõ"NƒðI,ñ­hSÁ¼iÓøF?•±‚-ûËCÇ}{çòÉOЪMc\¾}üò[1D'mщÉìH#ñ!“ßøÇ© ïõÓatïZíÑãû2q\S®{ý<渎Ó[I6ù“w5ã¢{úÔ°«VlÍ8$)oÿ‡½}£ÛÆ!vm¤¨Æ‰ù¬Zü‰¡`ÃwLye)öSï笌úJÙ®ÿ…=z&“2ª÷uDÑéÂ1¤ÌýÇŸíÀíçdP0ûYÞÙÝ„±÷t" ([5…×WÄ0òîÈÙ¼RJ£U³h\ÓÉ»£ßšˆC³ß4&Ñ¿›QH!jÐ[›XI)ŽIØÏ·ý¾Þ‘Âð›Ò± °µdİ&|ûýL6D[G1;7@Ó®d8%BÀ!¢|ë÷C“­ˆAÐSœ‚ç“_Lš&kDM¤”æ¡ fM*¡ª;|>ùùù$$$°eËš7oŽÇCÛ¶mëÛ¼ˆG·²xòƒL; p4éÆy÷ßεݣkþêÆ\ç©÷xø–w×:hÑ»). üùë™=õ}žËóVÚ âo\Jk;èm.åžË¶ðè»OpûÇ•yîI­ÝΚ›Ž×½À¿ÜOòâ”{ø®¢3NåŠßÊ¥\GøÛØNë˞ஃñÊ›2ÇXch’u íÿì­ºÒ«åf>vK :^çßÏ£‡Ñ¨Þ\‹ rÖïƒøA¤ºj>©«ãuçöç“§ÞâáçK9o`kôƒ¬É5À)0 ãOÕï t0ãOšY~ÁO‰†Ô¯/2²þ¼PA@¬bbbˆ‰‰9üÉ …¢Áð¨‚Ù}RÒ¨ÑWUcLÕŸm{e+¿[MEú…tK´Tëç±Ü}ÍÞ{ï–äqêˆÜð€…ÿ½ý ¿8 9iÞ)‹x¤Ôˆéz%Ü”ÄÛ3¾áåÿÌ@ö¸T:tmŒMþ9sƒÒ]%\n)‘hp`µú2³Õ•G¥¨l­˜ôÁ|&Õ· Å$ÐwR™HjP«yUsƒsýÕYU,ýþñýw¯‘ §ŒâÑ©£Bö8šöå’ÛûrIͧ^k!¹û8nî>®–²j&òGæGprZ*EŠÊ=5„ÌìÏ;iú¨ònp¬‹PüAÞJ?›Kw|ª>#äó„ãÜyU=*³oª²­lÜÍcZeˆëdDŠÊPŸÁzЂY~áÓö¨zDyTÚŒ÷ß‘§sÄ,š?‡‘cΦÜã9ì¹K—,¦Ë÷ÏüûuÌ«q™T4¯¾ÍPœÀ¬~3=zÛ)÷¿úücÆœ=_EBŠP2EÕ>•Àzø+A´zÉ]˜~“éQ™Ñ¾@"…D×ôP|P×-蚆Ínç‹Of0æ¬Ú<½£ÃSVÊÏ?.¤ß€ÁG|Íw3¿<¡¿™‚C`RáÜú6AäÕøAêû8J¬6;……¸£ÜÁt5¤ÇS¬ƒc§¤4ÐZ(=»æ›l†Ë.?|¶èÔ)“ÿt9–P2EP”ÌzR¢éÁ:‘æd¾ZH¬„”ÈÇîøýy J¨N2^M̤‚9 Kc¬£®Æš;•òŸwÛ‚«—¥Çaés>–Ò/¨X±ïØ–¡„¾Å¦yz Ö¯[M¯Þ}ñI‰+Íb „³4$èýdýÁ´·¦þ©ëµ=w•}Zà¯êÉ'6«5«W‘‘Ñòøz޹PÉ:é¼TÔWÌ®üFôXô}±¬œ~ì¿§êeé XúžƒmÃ\¼+öÖ}yÑ­°6wcl\…4êþöuÁk C¸º`v}›Ñ ÉÊjË‚ùsX¼pí:t"!!©f£¬Úž*ha áÛáH)),<ÀÒU+1 ƒÖY‘1a ïQ­9XB3·•8kd¸°õÍk‰C¹úÀ¬ÀFõ¨%‡šíúP3_ÿ),èoÆ5r?žÇVâÐ)ÂC߃IòDýýòZçÛ3Þ½€²5%Á- â:c8k»NX¢¶RþämøJp·Ã6ò ì;¢ÛAæ/Ç÷íóxW×âµêñXÏ| W/;ÿ½ﮊº~Äc†Åjeà ¡lܰŽ-ÀSvØIµG€¦i¸Ün23[Ó&«í1™zêh8öU= Õ‚¼:ô+ tk3b}ê×ÀÄü*g†F֜гΩ^–™F|LÊ–a:Xs>³Háõ¤aLÌÿ¡rGÁ÷x^^6èņÞó\sðíõ„&WÕ2/Ç}éØð9¾¯?§<¢D ¥½û$ÍöPñÅ#Þx¬®Æ~Ñ=ˆ'o¢"?L±µ(,ÃÁÙ5 ðUƒÔPÐtìvÈnס¾MQcŽƒGuÌK¨‚@#·ÓH¶¯Y@§ž¿rpS*ÍEý½/Òx=yó‚cõyþ,IX‡\³t'ÈœÅT|ù"ÞùÁ4´&§á8k<ö–bõ){ï;DêåD]~.–(+PŠX÷å3ÞÃ_*j–ÜÖºßGL¯$4‹Dî_ˆ÷ãç¨Ø^\YVêHgÇÞ"ü¹ËÞ¥ü«o0¼47–>×ã6K”¾ýø?ÿeKr—Ûúâ¾ÿëÀzá JŸ|#‚¼«Ð÷`âËÇØž_¹tÎ.ñŸßNE^Ððè~8/Ž˜q ž•yÕîèGÌÿ?ŠøC³ðø÷'`½ù"¬•÷À‚ÖùÜ=öQ>mŽ+Ï ìnX:¥8‰8¡ú¨{ÆÜJ«Sú3äL¿f¢‰äãÜ}´O‡lO“ãfO$rUÞ÷U¾‘0?4–QÿÆ}*ø¾zÏ>°ô¸ çOÀK×S¾ÓqÃp_÷w,û>Å3í'„ƒnÛ‚0$²ðG¼­@—Aò©8ÇMÀuúJŠ?\¦MÁ²L+ö}OùÜ•­ ¶a×á¼ÜÀø×ãø=âGà¾î&,»?Ä3õDtOgÞ„;ÎGÉ[ß#RÎÇuV_ÄPº>bÒÐò„JÀX…çµ—0¼€¿Ȭ–øäá\•÷}íµ8l£.ÅRô %ËökËŠÞc¶(§=OÌ…qpp=¾9/RþÓæàP"_ÁÑ¢¡‘‡QTQù1q$îsZá{ûoTŽ"ð3N×ÿ« Åá„é£hìu3©Y¯¢w8ˆ;ÿiÊÞ{ˆ™kgÓ¹­Ÿ¤¼4°œÜÿßhtWå~ØŽ”ÕgßTÄwWS6oþ-;ÑÒ^Æ1¼ÞÉ?¡÷€•E”Mý/¾ÒjuY¼ßšàúŽ­”gŽ ªE4}eXXIVöKI»à[» ø ›b½a¶;þ­KŸñXå"ʦ½,ëW OSb.¹{ã9xÐ(Al^Šg°>X†%ÐX‹bÄÞ-¡Š@BßCm$†£ƒÿô0*L÷3kût8ð=_‹¿ÐÞî2\gÿ ­ø ÊVV½GÔ)8Ï+»Ç¨=û¸+З?@ÙoÅšº Þû“ŠCqBôQ 4¶¼‰fº×x½çü»è´$‡øÅ¹ÄÛÜ ._×\™ómÕ>*³’:¡[àÛ¸¯²ž|»ñm)ÂѶ#šu5zËØ÷þÒêS»ÂtÎ3ÎÃ–Þ ÝZ”N(rê‡ +ËÜÄœ] h ’(t‡ŽÄ…Þ²1ì›V–Dn_„ /–ÆnÄšÿQ¾¥®k¦`YþÞŸS±«p?>’û¨Þl<‚+s¾­åˆK¯³±”΢dÝJû-1hQr×÷xW¯<ãž§Ñ:LÅÕ·¬žWynt/\×<ˆ­h2%.DÁþ­ì‰8›,¥ìU×cTéˋЊRœôœÕÞæ=Hmß­Ê«è·lÙBÓ¦MB•qIKîS¿7SFråþo52ñÂ×Ã= ª/«žàèˆkâ-ØüÏ”a”Ù±œöOÜ©æu‡Èú {5†?8•K-ÇÃû¸Ì•Šßð¾r!¾ìÓq¹÷ ãøáfJ¾Ý®‚Û÷úªck‰½k#Ν}q&Â4@‹Š «³BÄA?Ä&æDêºb/z“’)„ÝÃ…µo´( Q÷ ªR¤åúO°Ì˜@ÉO¹uþœ ÅŸå8¼ëBó¥Ñ¾5l^³¯×[åUô{÷îÅ0 äê¹ÇÅŽ†°\¹fµ}`ÎØ)¬D‰XÛ¤T·6ÁÚ*ö­BøJ0väC“þXÜÕ¾Ûè6X¢+ð7ŠÍë1ö¬ÇÈ÷²¬ªÊ#kî“Å[s ¥_XY ¥÷Egþ}¥ÁóJë?¤ìåË(YT€¥ÿX,6ʽ`F³ÔVFd,o¦Œ¬ýXrl±ÅøWlC†ï7ò1ö{ I/,óûiŒ%ÙŠÌÛ˜>È’Šã’ûp”M¡dòû^vï2üN¤øÉ«*—7ÿ‡à ïþ•²•ùuôl EÝrBǺ°U”ѼdW 2Û¶¯ò*ú3§Ñlý÷ê¿OÉMFqž™ £QzÖP,váËý Ï¢½ÄŒ|·ï5*öjXzN™¼›ò÷~F/bÁtü}¯Ç}åxæ,B”ÛÐb+ð¯Ý‚á±cz16±£LGŒ]“¤¨VÖJ›ÂÀ•û*ð/œ†¯ß­¸'\MÙì%Ș^8Ïîëÿ…w_2aŽl0ö䀥 ÖÔ(ÏG±o-B‡sÄ(äšB´D+þ_æ#"(ëïŠ}3kùw©¡§uG—[ñí+¯öÿ§ ÿ¼™ˆNcqŸµ–ÒÅÛÑ»ü gÒ~¼Ó× $h-&àÌ,Áûörd£6•¿DË÷`ä• vRešQRú‘y»0Ê"td´â¤ç„ýù¥FQLâ-~[»Šô6mBPðÍ4Zlœ}ÌËoH\±÷ëÊ ÿ*¾üÛ_ÎÁÝ.?Z‹ÿ‹¿SR~®á÷í¹å¯?ƒg[p@eÞ'”¼àÇuöx\ÎDÃ@îø’__§lÊ›D;Ÿ¨«/ œ[Qˆ±aoà—¾Q­¬ÏÊçÔ6à×ÜwàkJ_Ðq›@ÔU€?ÿÒ')þì[„!Ñ¢³°?k¬ ð#w/À3í Ÿ„ÝïPú}K¢ßJt?{¿ tÅ|D§›œzzÕï;zó&P2á5¹õUŠß¨1×ÓÓE«ñ¾s[ðû±¢·ÈF# Ç%ÏW}cóÖ‡(|iNÍÿŽ2Ìj`¡q)%ù¹9â÷ûT[aµÚˆOH )¹Qļ`ö˜ÏžÞÿŒ}‡Å/$s¼ñtM´“ì+¢B³²¶ó8’ ¶Ñ|Ë‚cZvCdJêh.ßûU}›¡8YwÖ=Ç|öt)%[·nÆawšÖ —Ë1³(4d¤”x#äóD[Ž^/nwP³á ­ð"çµÇ !D•gþ½º¨.ânw‘1Zþ˜÷Qõ8ôÏØ§P( çÍ:æ}TË–þLßþ«„üL-9™¢)µy‡ò*«{£š¦±xá<º÷èUgömUd¤t( Å@†yš¹Ì­Þ ›³õ‡?®‹q€åŸLåƒ_ ŽsÙT{öZ=ÍZS´d„x J¨ Eƒ£J8 @ÓBÃ…ÙÈJ‰"4EW½-þü s·”!€Šrë…—ñø¢Bñýä,~“{&ý…sƎ圿ÜÊ»Û|Çij^ÂC€µgzU‘*=öY …BQLj0 ¿üC!+*½¨Ðz­”³ö…k¸{~K®ñ^†%[ª6öóõ½×ójîéüûÅ+i}´o ª6˜nO¤i³f$ÆX@JDþž}ê ‡^Ã=ÓqúI)–c’1]#LJØëÏÌò4-´®„J¡P(Ž)DÕ ›‚UèxX#Èæ^ú(Ø_¾•Lû`}®ëˆ»²Õ¦dÅ»¼»Þg%ÆïÜçØ XáïÿÚW¾s ›üm¸æ/§Ñ=¡ªˆÔ9Õꉠ·©…í×ÂÊŽ”П*…BÑàáÞFm³›˜ûƒŸµ¦¦Åì/’X(™ý³Ç>ʘ&A¯Ê¿‹oÞZ€7)í@E^Î@^¶õ{¦¾ö?ælÈÇgK¦íàó™tÙ02\ÁtoïnæM{™w¾_KžÏBbËÖX‹Á´Sä}Ãm×L¥ñ}opG²Âƒ<Õ¹<`íÂ…]ÖóÞÎ3xú¹‹iað±ýÝùû·xø•ëiï nдª}Yæºú‹ÄÕG¥P(Õû¨j,Õú¥Âû°B‹QJ~™FÊÐ+9§ÉoÌøx=e€DR¼â}>ÙÕ’ó/@2%ä–<8Þlþ}÷Ë,ÐOeÒ]pפ~hóÿË]OÍ%×)‹Yþê½<;³€öÝÂÝ÷ÜÊ]£0_)VÅ ¬²Ý‚ñÿ|’§žzЧž¼ŽAÛ`ÉYÆê#pŽ8Ȇ9ØZ÷¤¹½Žû¬±¿z=×'J¨ EƒÃl@¥Á0Ÿ ®–ÀvÕ}5 ù¥w£6Œº°Þ¹ï³8Ïôïcî?¡õ»ˆa-“pQBnqRzÙþõ¬ÐzqÃíãÒ½݇\Âm7œ‚\ö>ßìð" ~áùE¤_r';«/ݺödøyã8%–0MÕaÛN’›5'=½9éiI$µH}óWÀY¶•¥;5ZôÎÄÅï<ÓZjÖOx½…×s}£„J¡P48¤™ÍlTÍì>s€k`[ DÕ}U–Šz5œÑvâºËðøµ|8s;E›¿à“ßš0f\{¢Q8ðPPêGÅl[“ Í{ÒÚmÞWÕúš‘Ú%xöo`¯Œ§]Û´PYAïNŠ*pCÛ@"Ãl#®3òu6Ï^Á¿À³k)›*šÑ·},êyþÐ"«Ø^•"&U•B¡P-Bš½ ¼©˜°tëjý*•û!<°%JÊ|F‹%+Wñë²Õìñ„(pÏ 7(ŒàvðYÌíà"­9}p;g¼Ê'»ѯW#tÃÀ¨ØË7OdÂmÓÙì1ðø‘§¯½”ë_YÁA9[?¸“K¯|/w{?ü'+ÅH˜¢ÉP•ˆŒ¬?úS( Ž€QÙGU}êŸðqTÕÇT7…·vÜV=³’2`ׄ®4.x–cH°' äÆ{ïNý„7ÿó~k"mN½’û.îO¢'­/¸‡ļÃûßLå©Ïý€•˜”,z¦¹Ð‚Yà¦ճ«F-í4~&Y_Ofcú™ôK±Ì©Î4ë°õÀR™^ž˜JÕ—‰V«/ó¾‘âQ)¡R( Q%C-,DLKox…9ÿ_µ>*{ÖD^šj^vˆÙšÎEOOá"„îgú ®¼wWV9±R$Ñch?æZ¬m®o) qM*×Õív¦N­v º;•f±Nl£z’¬Ä=™aÿx™a§[nxáÍÐ5îqñæ8³¸ ÷”*I¥HQ¹§†™ýy‘ÒG¥„J¡P48ªzT¦+RÙÀV¶÷æ±àû–Âû¨¤—¼m»)¡ŒÍ3§0ß=Œ{zÆ£!~¶ )Bƒ{¥ÁzÔ‰®k„OåÈúÓ•G¥P( s¬”¡æÛèvRØßO-2º\~ß>f¿ò_ìÕ‰k=„I·œC† Ž^¥*Ÿ]Pó…‰BÊÀϵ€0ì÷‹”P)ЇÕf§° w”›Ð̪20ÅQ ñ LR+E °¥…Ò³+ßd{ÙåWü¡2§N™\·q(l-8ÿ±Éœ_· M%ëf=H)Ñô`Hs2_-Ô™%¤¤à@>v‡³¬øó(¡R( Žæé-X¿n5½z÷Å'eÀSÒ@³Xá,M ºE¯LCJÉ´·¦ÖñǑڞ»Ê>-ðWõä›ÕÆšÕ«ÈÈhy| = J¨ Eƒ#+«- æÏañÂy´ëЉ„„¤šò±™¼Á¢…%T„o‡#¥¤°ðKW­Ä0 Zgµ=®6 %T …¢Áa±Z8h(7¬ãÇE ð”•Õ·I'š¦ár»ÉÌlM›¬¶‡|mýñF •B¡hhºNv»d·ëPߦ(Ž1jf …B¡PD4J¨ …BÑ(¡R( ED£„J¡P(*…B¡PD4J¨ …BÑ(¡R( ED£ÆQ)Љ”’üÜ ñû}5^‘¡8:¬Vñ $%7 ½’¾¾QB¥P(RJ¶nÝŒÃî ³M\.wÄ̢БRâñ”±g÷.¶nÝLff›ˆ¨W%T …¢Á‘—³›ÅJf묰7ÚÊ*³£CåléáíÉäyÕ6)í¡ö›uår¹iÕ:‹MÖ‘Ÿ›Crã”ãf ¿N¡P(þ¹y9¤6kÚÇ’¹}¨Fúd$ô‚I¨!æµ <@ÓfÍÉÍÍ9®v %T …¢ÁQáõâvG5½&s»ú‘óÆÚã…¢Ê3ÿ^]Tv·;ŠŠ ï±7òP¡?…BÑàa^@u 61 ÷N&±ªîiV?fî°ðãF„Ô•*…BÑàEºÜyTT}k»”²Ê›—ê?5à8RM|j ~†ê#<HPÈ”P) ÅÑQ%œ i¡FXVó¶W¨‘lbî;ÇýHñ>•P)Ї"à˜¡«êUðXhýìýâ^þþæzÌÞÍ•DËŽýý— šuBy`52 ©Å£Ò´Ðº*…B¡8J¤H¼€0ϪŠÓïçú *Š ñZÚqéãɲWPš¿%ŸOç…ÛÖRøÌ£Œkn«kóëjõ„”•a>ó*ëL…þ …â(á^R˜gUs_XÂE-'>l d¶mG{@NéœÀ¾IÏ2sÖNμ´%6$¥›¾æ×g0s!†£1F\ÆßÆ÷¡± dñ ¦üç lÞCA¹¢i9ø†'maÖì%üvÀ‡-©§O¼‰ ½’°È ö.|—WÞý–•û½Xâ2é7v"WŸ‘M´Vʲ]ÍÃ[ÇðÔsãɰrgÞÁ5Sâ¹ãµ»è]ÁιoñÊôX›çÃ’Å‹ÿÊ•Cšãÿ7}¾  ¼ûX½zñ£ÿÎ}ÜÏíWt¥xÎÛ¼6[ÐsüÿqßÝ7rFãÍ|öÌ«,9(‘HŠ–þ—;ŸþŠâ.—òûïáú‘ñ¬˜rÿül7¸iuj6–œe¬.0‚ÏTÂÆ·£·îO›hIÁO/p÷ó?âq3?þ ëo0çŇyg“÷wŸµúsÿ^}DJèO •B¡hp˜ ¨”"æ“ÁõÀØ®ºïÐKåõÂïáàÞuÌž6…Ÿ¼1tëÓ›,gëW3X3†¿_{:ݳZÓù´Ë™4 Šísf¿ß¼‡ÆºÒ¹cGz¾” ³€Æ½>°;» àüˆ‘èÝÈÒÝåHÿ>æMŸGIÛ‰Ü1q§tê€ næÿN‹aóG3Øè‘Dg Kß΂UR J×3o£ å©íˆ1ö1÷ýEˆþㆱ½hÛº=ÇObtr‹ænÃ{„Ï]}_x½…×s}£B …¢Á!…ÙÐVÐ[=q"чûˆ@Ä«|1N8¿r·3ƒÁ“î劎.¤QÈŽ ùÿÿ¸ø‹ª—ÇBÐ8L8»ƒ„$'ìÈ£Ô/ˆµ€“B åôøå»Y¹RÏË&Aàré=2±~÷ò}´mÒ‰¡Ù:/Ï^IÁ 86Îeµ¯—tK@+_Éš½P¶ã.[XÕ$Ëþƒø„8L㮡i²FÄTJI`Ú`Ö¤*…B¡8:„4…A"„éJ¬ÌýP3µB $`ëÀ•w§í ¿¾õ$ïïM£{÷4R dðœ´ó¸÷¦ÞÄ…Å¡4k4¬!‚¡ÆPIt›¤Ã ¤fÂÄ"h³i¿À”³OH ±tÞý¥ø5¿+ñ?¬Ä—5nñ ¼d’ÄÓnåö‘Mª4亻ö°{- ü]™âWE¤Âg÷B„fM7ë¹¾QB¥P(Âô¨BŸ~j«C'R@È£²D“šÞ‚ '¤ßôW¶ßþ,/¾Ô–· "Ùâ$-3fo¤0úÚÇW»—!±D˜^Hð¾¦",ƒN¤µ1šÂòeë(8£ É:@9Û–lÅÕ‰VqRHb;¤«íi¾™=ŸØ_ :^Ó•xÒÚˆìÆ°lsÎK»“b©QAµf;Ö:à7¬ÿNÓ4t]Ô§.#&ô§ú¨ EƒC…A˜"%EÐ ‘!3×k[ Ã."èÅHDpŸŒéÎeמŠcå^™›C…a¡ùˆ1d‰•¼üÄë|±p9+V,eÁ7_±h¯7p3ô'ŒÐ½EЃ •%8‡$úœÛ¨“ùϛ߲dåræýï9ž™ULæ™gi ÞÕÍÈÞÑlùß[üjíÁ¨öî€$Ó·Nã±?fî/+Y±l1ß}>›íž0ñü"XW!.$¨"TŸ ƒõ\ÿ(J¡P48bdÎQ›CôUÕSU£_¦ò3°®ÝéB®è¹Œgß}ÝþJ·Æ§q뽦¿û%¾8/àLjËÈ–ƒéÕȸ6xïÐý¨zߤԈér%÷ý5†)ÿ›ÎS³*ÐcZÐû⻹tDS¬æýpÒjÄRæ~Š1pY.-xø^×r¿ñ!Ó>ùŠW~,Cb#.sל:€4[pŽ¿à]Â}À‡)%-¬V_¦½‘âQi3ÞGž6jÌ_°hþFŽ9›rç°ç.]²˜þ‡þû Ecá¼YôèÙ÷˜–1ó«ÏsÖX***BTNžLKoxE¨a†Ã ÿ=q©.Í;:T¹ýYÐu ›ÝΗŸ}̨ÑgÕ™ž²R~þq!ý >âk¾›ù¥ò¨ EãªGeº+f‚@x¢€y,8SøÉ:çŸ!A’BëA fùi„OÛHU×#Ê£RB¥P(æX)Cá-s oØIa“NR‡Ê|vAebI(ËOJtM-àq…Lûý"%T …¢ÁaµÙ),(Äå&4³ª ÌRh|Eð5-øj¾Ýà²Ë¯8lyS§L®û‡8„²eð­Ç„‰”¬iNæ«…:³„”ÈÇîpw›kC •B¡hp4OoÁúu«éÕ»/>)ž’šÅgi:HÐ-z­ééáb5í­©ÇÙúãGmÏ]eŸø«zò‰ÍjcÍêUdd´<>†%T …¢Á‘•Õ–óç°xá<ÚuèDBBRÍFù¤Mœ¨-,¡"|;)%……Xºj%†aÐ:«íqµñP(¡R( ‹ÕÊÀACÙ¸a?.Z€§¬¬¾M:!Ð4 —ÛMffkÚdµýÒÇ%T …¢A¢é:Ùí:Ý®C}›¢8ƨ™) …BÑ(¡R( ED£„J¡P(*…B¡PD4J¨ …BÑ(¡R( ED£„J¡P(*…B¡PD4J¨ …BÑ(¡R( ED£„J¡P(*…B¡PD4J¨ …BÑ(¡R( ED£„J¡P(*…B¡PD4J¨ …BÑ(¡R( ED£„J¡P(*…B¡PD4J¨ …BÑ(¡R( ED£„J¡P(ŽiPš¿“ßvc„ï.]Ãä[ÇsþðÉ_½™w¢`­o Åɇ,ÛÆüO>bæÂlÎõ щJiC—SÏà’s{Ó¨A´Llž|wÎ,ÀÒíN&ßÙ Wðˆq`-Ëwú€µ,ÙZÂÙé hõij§AüsP('FÁOü÷î'™›¾WPº‹wåüs{×—i‰0D­G¬i£¸î  [0¢W¼©?‰*…BqüüøÊ A‘jÂÀ+'rö)$X=än]Ç­©V‰gÇ\¦OÁìUû)×¢Hë:œ¿\~>}Ríh²Œõ<ÏäÙ¹¿€£í_À¤K’æÐÀ·ƒÏ_x…ïÖî`_‘ Ø[Óï쫸rT+ÜZ°œm³x{ÊGÌ]›‹W‹¦y‘\rÕ¹tO 6¢˜ß¾ÃÛŸ/d]®l dŸ~ w_’z,cùc\z€‹~÷½Âß[mæ“i_°Ì‡èчk² +Øÿó &¿ÿ-Ëv•")t4ŽË.L†ëHí=9Q}T …â¸!üÂç˼d\z'Õ™ôäXbâSÈì6˜á]“°þ½3yüΗøzÕ~Êd)»—ÊSw>Ç¢xÙ»b)[öãÂá7—µß¼È#l¥ÀŸÏê%›Ø[äEjðØÌÜÉðÆÊR$`ìÿ–'î~…o׿âuÅ¥•°ó—<öà{löÒÆwïåî7gDJ³añPdsUû•ï&>!„¤&$:jSAÁâç¹ãÉYº«‰åûYý͹ó±™ìõ™½'+J¨ Åq×»™R8¥s2–ZÏò°á£ÿ±Ö¤ÃC“ßçý×ïbxPö3Ógî$<=!õ/ÿaÊ´·xbl ¹?.fO•ü…8†?ô:oO{Ž+2JX:o^ÊÙ0ãÖøÀÝû^›ü:“߸Ÿ À¾øjs9FÞ¦~¾°Óñò3í½wxoú[<>®9¶°,Ýnâ¹W^áÕÿ>Áemµ<øN¾z÷'J„awðÚô÷xë‘q¤þõòѦò#°÷äE •B¡8~„9‡ôüX¿©€ôaÃh¥¡Ç¶gXŸö¯ÞNY‹¤vÈÀP’G‰Qý8`M$«m"¥y%øýù¬ßX @ÙOOqõ…pÁòm@){sÛöÃ×}ƒÅfÁð6žçþs6©©ƒ¹ ÷g<ýS)Ë^¾™ñ¯Ú°"ëo¼úÐ wîH܇û(Ú7ƒÛ.ÿ—ðÑâÚùg¯jEÙšqúE=ùö¹%üð=zëqŒËrrRÇöƒò¨ ÅqDÃÑòxúv.ÐŽÔ(Kh¿+9“î; XSGs×#W3¼m2v@â¢I§ÑÜðèߘTWÁB°·ÇNbDÇT¢4"åH £…; #z}oxŒ[ÏéAz¬„CsÓ4IP!À™=žÛ'ô¡y”¾2<–Db}^j:UûßÈc7AçT ÁžDö«xäî3H³Õ¸@†6ãýwäi£Æñ‹æÏa䘳)÷x{îÒ%‹é?p蟱O¡P40ΛEž}ëÛ Eâ)+åçÒoÀà#¾æ»™_*J¡P(‘*…B¡PD4J¨ …BÑ(¡R( ED£„J¡P(*…B¡PD4J¨ …BÑ(¡R( ED£„J¡P(šëO¡P4H¤”8p€ƒÅÅøý~díS¦+þ V«•¸¸8âãÑõÈðe”P)Ї”’í;vàp8ÈÈÈÀét¢i'é;0ê)%åååìÝ»—;wÒ"=="ê52äR¡P(þùùùX­VZ¶l‰Ë劈ÆôD@Ó4\.™™™Øl6òóóëÛ$@ •B¡h€äåçÓ´iÓú6ã„&55•üêÛ @ •B¡h€ø|>\.W}›qBãr¹ðù|õm „J¡P4@„*ÜwŒÑ4 Ã0êÛ @ •B¡h€QËûÞuN¤Ô³*…BÑàˆ”42”nÿ•ÅË÷QQ×wŽzVéé …¢ÁQ· ¨¤bß>~ûC¾ýy9ÀžHf—œ3ñ2†4ô÷Ä{Xùò½<ê¿•·»6Á^‡Q%T …Bq”ÔÝà^IùæÿqÏíÓØànψsÿF—æ1ˆÂ]¬[µ›rNî~°HD­„J¡P48êì—~ÅvfügâFñà³¥{l¥0 VÞÁ5|úÊ«ÌX°…"á$µë(.ýëNMµàÝü.=ûëwæQâ—`O¦ãˆ+¸éÊA¤ÚA/çÇ_eþÆÝ(—€‹Ôg2鯋9%Ñ(D–³sÖd^zç{VçV`MÌfè%7rõðtœ€¤|Û÷¼ùÊ{ü°:‡ œ¤ö¿–‡ÿ¯OàúõOrÉYOàøSoëŒóOVò¨ …â(©«Ô»õ+fîÖéxËEt‹=„÷äÛÉG÷ÝÉÔüSËe´uîeÁÛ¯óÄmÅ8_º‰Sb5üùkX¶Mçôïã”xO=t[?}œG[òÌéèå{Yµr7ñçÝÊ cðçüÊ'¯À#O6åõ‡†‘¤ ?ËíϬ¦ý„[y¼K49ó'óÜs÷ãHÿ/“²y³yâ¶çXÝâL®º»M­ÅäU´$ÞlÅ3&ðÈß{«ƒ%º Ž:¨%T …Bq”˜ 蟋LIJwm¡FµŒ µÝγö>ü-Ó¿¿´ø(3t¶]ýïÌ»ˆ®cR‚g&Ñ®ç)t°—óÞç 8«[K6ý°˜ü±ÍH–vR:÷¢GWЙe˘4u.›J‡‘èÚÏìw <ÀßÏëA”í3®g뢛™3{;—eµd×ïð }¸ëÞ«é.ª²4`·³1éÄ™‡ñªÃ‹ÕïâÈâ‚놳ìѹûÿvræè~d7‰‚²\¶¯Û†ó´+83{çd.äÝ?Müå£ÈvîgÑ{“Ù;ˆ;OMF—iö…ÙbíÚµcö¬Y <ˆvMš‘\VŒa8Ñu½ê5z ÏëÅGÏLæágË9`kâõRöï,§í¨!4·ÛÈ=–vß¾Á3¼Æø³zÒÌí§°ÀJ‡HéÐÞúŒéß6bpc/ûKš3p`úï&TT¯«p‘ª^Ïõ*…BÑà¨îQUï¯ú#‚Ýå*´~ð5s§=Çg~ÀEJ«nŒ¨¨À°¤qæ] ¿9™OŸ{„÷¥ƒ”N£¸é– éèçµxT~¿‹ÅB—.]øæ›o:t(qŸ½Mq‡‰DGGW½FèÄõ¹ž‡ox)~Ê ‹Ê؈o5”¿HšUCkt·=¤ñö”yç©o©ÀBlë1ÜÑ»3­†^Ë„uÏñÁO0©ý&Ò¥o3’À» ÷¬ÂEJyT …Bñ'¨ ŸYðp¸[æÒ;si-Ǥ•ŘcÌ µÛáì~7ïN”–ö†aX­VzõêÅÌ™33f Ó3¢¸ð ‰êp3S¦ka÷±‘Úowö›PÝ‚PÒ‡+cW?0Œ««Z$ƒÑ·<ÅèjûDgÂÅIÓ´Ðb±X”P) ÅÑò{U}'˜åûý~¬V+Vk ™=õÔS™1cçž{.‡ƒ±cÇâv»#vxåQ) ÅŸäp"U_‚ÞGe†ÿL±>|8ï¾û.ãÇÇn·3zôèz«Ú²ýª¯×·è›(¡R( «ÕJAAÑÑч<§¶¬¶cÅøñãCë~¿?ôiµZCá?“1cÆðÚk¯1iÒ$œN'ÇÇétW±:\YRJ °ÛíÇÉ¢ßG •B¡hp4kÖŒõë×Ó«W¯*/÷«­>0}úôÐzii)Mš4©âQ…{UçŸ>åååÜxã¼òÊ+ <‡ÃQïa@³|]×Y³f -ÒÓëÕ%T …¢Á‘Õ¦ .dñâÅ´k׎„„„zoäMÌ×·›Éæ¢ë:]ºtaË–-¡s=×\s }ô={ö¬"fõéI­[·ŸÏGëÖ­ëÕ%T …¢Áa±X8`7ndñâÅx<žú6)Dyy9P™žžŸŸÏÂ… ¹üòË™8q"O>ù$C‡Åív£ë:V«•… ²uëÖzµiš†Ëå"³eKÚ´i1⯄J¡P4H4M#;;›ìììú6¥ ¥¥¥\EÀ£Ú½{7çž{.………œ}öÙ\}õÕüóŸÿä”==z4QQQhš†ÍfÃáp ëê¥ëµ¡jE¡P(Ž»víâì³Ï¦cÇŽdeeñüóÏãp8¸á†xÿƒRCll,.—K‰Ôï jF¡P(Ž^x!½zõâ¯×]Ç·ßÎóÏ?OQQ7ß|3+W®ä×_ÅëõÖ·™ %T …Bq 2d×^s ]»veРAtéÜ™^x˜˜ºvíÊ¢E‹ðx<3V)’Q}T …BQ‡èºÎ=÷ÜÃÈ#ÈÌÌ$>>)%wÜq¹ðBæÍ›Çš5k8ëÌ3#&Y!ÒQB¥P(uˆÝnç†ë¯Çb±Té{êÓ§“ß|“Y³g3nìXú÷ïÑS(EJ¨ …¢ÑuèèèÐä®&‡ƒaÆѧO4M#** ›ÍV–6”P) ER] Â÷;Žß{K”¢6”P)Љ”’Ò’bNx´j+f&¼T&‰D ¥!‚oùMjÔ˜¼œJKЉމ=¾FׂG¥P(%¥ÅÄ%&"„4…H“$R‚ “¤ÔûHëœ$‹D ,"øüæ~kM Ô‘R L 7 ƒ¸„JJþ™¯©ÎP•B¡hp~?»¿Ïæ ÿ4 +B†ÄJÿ2_‘¬ãFø‹t ÐBTºVèú’"T_»=ôn­úF •B¡hpH)ƒÞBÀƒÒ4-à&h=,0½ƒÀ¡J‘'KÜʈ§ÅüÂöë¦HÂ¥¦n™{ÞgdT–*…BÑà¨ùêùÊ>(-ØÀ†$hZÐÓ’aý6'Uõ)ÔGzÍ|¨O  «ŠS¤•ê£R( iv¶hZ ôg*&RþGŽÏ1Ï [Œ<~ùü}>]’ƒ¯ú±¿žY"1ʰgWe"Ìó «@݉@C‚‘"TÊ£R( ʨšÆgî×4ðXÁ·ß­§Èlku;1‰Ik™E»–p†~¦[‰Š#ÆmEâÍ”x¶ÿÄÜQ Û·̨0=(3$<×üPB¥P(GI¨ K±B`vUHËGÚiÖ£7Ù±:þŠ2 ömcã’YlÚÚ‰aƒÚ“hôX²ŽÀ|ýbd4ÍuCmaN)ýQuiví…RÓµ°+•P) ÅQRÙ€Ê2…ò'*;bа•؈F $Ð$­­š¯àû9«X¸ª£º$bE¬šù[S‡1¦[ZÙVý²‚MûK1аǷfÀ.$[ Jv­eéªßØWâ‹‹&0 ;]xØ»n9¿nÚÃAØc›’Õ¥횸У`- —l#ï`> XÜ4jÙ‰^›md;–þÄê=…{3mØãÒh×½ÙÉö·ã;ðË–¯gûr¤%Š”VéÙ)¨ ‡è/ÚÆ¯Ëײ-· +Ñͺ1¸O‹@Ý»™3ãƒÀ‰îlFžÞ™x`_•ò²Ðª÷Ö/J¨ EÃ#ØG%¥D 3tU9¿‚¢r¬”U¢„öFm霺……[7s C/Wé©÷²géO¬/iNϽ‰·ú(-Ó‰²nàÝ·ŒïÀ‘Ñ™~Ýã°ú<ø¢\høÉ[5‡y!½sºÅCáÖU¬˜?ÿ°átI´"ªÀ‡€ ÷¸ÂGan)Øšc¯žn álÚƒ‘g´dçoY¿r›×7£÷Ð^¤Û$ám] éaÙµ×ôÀ°æpï0¶=ƒz§á ·C·eâOÖ_g*µ˜Yè¦ï¤UFM¢e‰¬>*5ŽJ¡P48B~©œÉÜFœÃ.€† z ‰7g+÷¸3Z‘` ÷¶dh ¬æH ½}o†ìF²wëw—"ô(’£¡do.Qy®”ÍL¬^NÎþ s¿Q¾\/zl2n=,Tv]¥‡W»U½=+1 N(ͧÜMLLLåå@ÇJl¢î!ß+B÷3ÝfáÇ'µà,S'­…B}áÒ)B¥<*…BÑਚž !„ ¥§K)‚ŽŸ’üýì¯ÐñWx(ÌÙÁoÛòñ'µg`Û,ab˜Ž©‚ü­Û)v%ãÐðæS&u¢¤f'½mSÖÿü+óœªBþ IDAT© ]³xTP&HošBûÌ(æ¬Y̽#q’Âm«Y[EÖ)±KÈô Æ+¥)PaÏ ÍQU´BÇ¥N\«,’¶®dÉÂåxþ¿½;޳<ì=þ{ß™‘FIÖjK¶dÙÖbÙÂØ0Á¾`LÔàÜ—8$7ÐÜS’4¡§m¸!%é’–æNK{nÚœBÎIXÚÛ¤!HBÆnÁ6^_¼Û²å}•dmI3ó>÷Y4#Ë–%û±ýýdF³h^ͽßyÞ÷™w'j\ŽQOœ ÓTrUT7]å{6jÝêtUc•ÆŒúú\UÔLPNa©òL‹¶nÙ+·*W‘ÓžJk'ªÀ—˜žî%E•>ÅŸPÀؤŽL‘13->0µ‚wst":²q­ŽH’P¨¸B“¯¹Y“ËâoøMrHR,¢î­Úxp³"’ä©¢þ:Í­Ê•c¤à¤ë´èºmú`û‡Z¿?*) ¢)×kbeHeÍ7iÿmÚ±N+#R °J3æÏQS‰?ó>ÒOýþ\ÿ7’š¦7»Ú¼y§¶­oUT’?¿\õÅSU/¹ùS5ÿfG›7¨-ëö*&G¹%õš?i¼‚ã¦ëúézo×­Þ#Š¦è†‰U %6?¦>ìÃ$§§Ûsd çç?ý7sÛâ%ç}ƒwVþ—~gÉ]ê ‡G¼îûï®Ñ‚›oý(Ëà³úíºöú³zÛ·nÒÌY³5Ðß'Ïœ>aRC¬Ì)é©ÙëW8ã9n²ÐIƒG¢MÜ×u]åƒÚ¶y£f4_ý±Ýøt¯Ö¯]­ù7ÝrÞ·Yöú¯Q¸ô¤fý ž?æŸQÆäƒäuœsÏY¸"8ŽÒâí$Žè?ÃI|DJü`¾Nâ#?ØôIê;Òàˆ í(ɉé©+uP•|T™Ô‘äÔÜ~¥N›ää~oèdŽ‹‹P¸ä¸®O}á°_â0ážIìWIŽ<½ôÒ+çõ³î^ú™ì-èÅ–ú|Ä·‰Q§3øaŠÉ™Ž‘cDúôiùýv$ÂŽ¥€Q()-ÕÑ£‡US;EÑHD&q0Õø&>#G®îþÝ¥‰1D|sV&;F Ù—8Äl*FñqÕà¦ÐÄçv¥MSw$ùý:ТÒÒò‹°Ìg"T.9ã+µg÷NíÛÛ¢ • \Q1?6iïêMƒÆÿõŒQ¸·W‡÷ñŒÊÇO¸ÀË8}ÖÛc‘Õ•£øvï¡b±˜Ö­_¯ÂÂÂa/Ë F%Ï“‰ï‘Êø©ß#/}?û¬Æ$µð§qÎ2ÂÑàfkGRLŽŒ#9Š%.w”ŸR$¹h*† Šc¶mQQ‘ž~æ=þ7£œœø,¿øß»ËËGVCå ø‰ (77˜y¾Ï§?ñ ½ûÞ{gÞÆçSwOrø+ÔÄf#É1’§äf@I&qYb›¿-à—¢hZÎvç'ã¥TâÅSÆ~*GƒÏagpBP¸¿O@Àšçq4SNðìÓÌ»ººô¥‡JEJ’äÏ \ˆÅÃ$«¡*êTG‡&TVqYII‰®¿îº3Î/..Ö¾½{T_ßßtó›UÌ`¨«‚ÔÌ?)µƒ:µÀy¹õ“·ëıcçíkmUYY™5¡ê÷ª¤¸ô¬—?øÀû¨$©óT» òCÙ^4\a²ªòñãuôÐ!•WŒ—Ïç;ãò’avϜ٬uk×jZ]b1/cŸSü©Qâ?92ƒô©NâzÀ…—þ¢)y†“ö"ʘÄf@7þ‚ÊQbDå:Ú¾u‹fÏ™cE¨ŒñÔÝÝ­º†¦a/ò{ß;ã¼X,ªc‡©ªš‰øxe5TÅ%¥êêìÔžÝ;5­¾qØX 5yÊ4íÞ½[ÿµb….ºUž1ŠÅbñ?üøF~Å'P%6£HñýTŠÿÁ;fpS!pQ¥=7•xq•|ŽÆ/sLürŸß'×õkåÛo©²r‚JKÏ>‚¹P<ϨíÄI•–—«`˜ýÈÉÅbÚ³{—B……ç5ìN¦pÕÔNÑ¡ƒûõá¶Mª¨œ¨qEÅ ääœóUãÂEŸÔ[o­Ð¯ùªšf4irÍå¤^…¦6înø‚LábsÎøÎ¤?oÔÓÛ­ÖÖVíܱ]••Ujš1SŽsq¿i/ÃáÓêîêTqI©&UOñ6‘uvÒñ#‡•*ÐÄê+F„¸¼dýX'®ëªº¦V==]jok×ñ£GãSÌG˜•WQV¦ü`Pû÷íÓ¶m[°S——@À¯‚Pª«k åëÈáƒ{‘R:ÚÛÔÑÞ6âõ\ÇQn^¾ªªkâ‡9#RÈ‚¬†*ýÀ”¥¥*-­ÈæÝ.C|ÌÀj„ `5B°¡XP¬F¨Vu¨\×UwW—xS-à|cä%2>Z£U^~Hí'Ž+2ñM»HFÑHDÝÝÝ*((õ­Gý†ß©uõÚ±u‹ê%ÉuÙz8;ÏóÔÝÝ¥–];ÕÔ*A{{›&ÖLN}Ÿ\1§¯¨Ï¶¿%C-éŒXpIšT3Yí'OŽéþQÀúû•Ÿ’”9jn¥œ~Úó¼TÀú»ºtpÅ uîØ!*4y²&-Z¤Âšš ûË|Ééêäeé)ú¸äç‡40†Ã'I„ Fä¥ihœ†ûŒ¥ôt_w·Ö?þ¸v>ý´&45©¢¹Yn  ý+Wjõ×¾¦ÊÅ‹µàÉ'U4yò?Ç6Ã*ý²äùé#®ôËccü<*B#0i+X'ñ}r¥í(ócd1Jnì=~\¯.Y¢Ü¶6Ýúío«`òd¥o lþìgµõ‡?Ô‹óæéÎW_UÕ¼yc[ÀØ ­úñô^õçõµÅ“²·bŸá6n¦~¿ôQ¦!»PœWšôQ“‘$Ç‘IœöÒFžçÅG’¢Ñ¨^»÷^÷íÓœÿ‡|ê×ÀÁÝê?ТþÖ…[ZÔäˆ>õ)ÕŽ¯×?óu:”ú¹£úŠujÇÚµÚr´_ÞXn?Ưäï>‚îzɨ_°Oø€+çyñBreœö½£Ä¦°äH*qzë³Ï*¼j•jo¸Fa·G•ŸPÝêÑ@×.E:w«íØM¸ÿ~õåæª¢¹Yíízç±Ç2~vú—9½O+_xBüÏ{µtéR-]z¿úÆ“ú¿ëO*–ªÂ™·Ëæ×Ðe5Ã]/í1!T%Æó2Fé#ªxLÆ÷ÑXLÛŸ~Z%>Wâ>EÊJ”7±F _~LÇbýÚr¯¦ýÑ#*¬¯WpÖLu< ÒÂBxñEõ¶µ1"ñz6ëGþ±žz©E…Ÿ¸W_ýúÿÒ#ô9ÝRkÔÍ\_¨Ñ”9Ëãà y,2–‰}TÉÍ{’2G éÒF½'OªgãFUÜ“ï+¼r»Vk@ þ®®ýæÔ}ì°JêghÇo_ÑÎïüʇëêQž']µJuwÝ•ösÃÚöÂ?èWkuÏ«ûòRûæ/¼#~b -yåÁ刵kÃÏžÑs¯½«½RAõ<-yðô{×”Ê'É Ö[Ïþ“þuù‡jJ’fÝóç®ß« H¦_Þz^Oÿûrm;‘¯¤Q‹îÿC}qQ‚çz¿®ãdŒ*SKrÓß§ì*ÁÐ}TCW·ÓÓ%õ>¬ 1òçJŽÓ¯òü~Yÿ¿õ›§rtÇŸþ•‚E%ÚòŸohÓ÷hVyDýýÒ@HÊí–N'öS¥œÞ®_¾}JùóÖ¯ÏKÝÇP£Ö‡/|[ÿRúo_xD¿?Ej]ñ¼žü/Ô÷Äßé _þúþ²˜–<ü׺i¢O]ÇÚUTæ—‘§SëþIßúþ65ÝÿÇzüê_õ¼~ðÏ+·úÿ苹g}œœ!Ë‘ü²mcÝôG¨`ɬ1^b pæôëŒó}®r$91É‹Jјt*Zª¹·:uý©s®ÓÎi³d”Ó'ùŽHnÉç÷ɘÁz´cõKUWORPÞ°ƒ9¥®ïÉO^çûúÉkÇ4ñ¾ÐW?]­€¤«gT©¯åëzù't×·®Uo[—L°N3g7©®È‘êË=ª·~ú޼ßÒÃKç(äHMµ©uí£ZùV«>×Рœa¥Ìù™ÓøÇG¨ [Lr6ßÐnÃÄÊOy“&)'R¸WáSÒ¡`©nøûeš0c®ö¯}I'[ÖkîçžÐO-ÓêoÞ¦’Î ŠDâ«úPccÆ ÝKìKN^vUï™Á}Dž§¾£[µ?V¬y³ÊäKÞÆW¡Y3‹ô¶êhÿ”|1iÂ~E¹¹9™#¼Ý67OWÿTË?õ˜–T³á-¹i2“çy TLWµo™¶n:®ºI HRä˜6ní’¿fº*|^|k¡¿T3}N3,ÔË=ªŸ½²UŸýf¦—6ì>®àæj‚ïÌûº*“6Ðq¹®¼Ü±OO÷Ý{ÏÝUWß8¦À• uÏÕ7NW,‹¯tÍà{3Þèš¶Y08a¼º"r׬U_¯Ô¶ñ}™¨§HŸÔß+ tI'VoRÇ;} vK‹nQíw¾£@NÎÍŒUÔ—éЪ7ôúoß×á£Hø”ŽÜ¥V½©•'«Ô\ãÓÁ·—éÝc¹š2£N•¥•ª<½N¿zõË)V°ï Ö¿øŒþcG¡îøò}º¦DêØü¦ÞnéU´¿[Ç÷mѺ5[Ô9ùÝ9¯VUôÖ¯kýWy¾ˆ:îÑæ÷v+P[«Bל±4ùX(ùÄOH’9rÉõ¹Úýᇚ2uÚ¨û=»w1¢€‘ÄGTÉyñiÖgÙW•<››«)ú'j5F9?øºÚ"êéºr%¹’‘‚R™$ÿwjÒSO)/?4ìd ·t¾¾úݽùóW´üÍÕšÞ˜$¿ «tíâ¨bn¥Ü{»ÖÿðM½ðÆ<Íz ^õ÷>¦¯ç>§åûzç´”?i®–>ò€îª J&¢ž}èµ—7éD¿$'¤‰³?£¯Ýߤ\ã*wÞ—õ—±õÂ/^ÓÓkOË( qÓêK7ݤ‰'ã0P©51F&% y<’³ÕÇ:¢r~þÓ3·-^2¦À•`ù²×µäÓK500 ãyƒW2z`›¸@ápXÖ¬QøG?–·n¼övIþü|ùæÌVîç?¯ª;ïTAAÁ%ôaŒ‰ƒÓ*þ»f,uÚ÷ñM>¹®£@NŽ~ýêËúäm‹GuOË^ÿ5#*Iæˆ*¹o*9 }"Aò²Ä´lÇQ0/¨º[*|ÃÃý^ç9ñ†N. øÚºe³jjkÇt¿„ FPßШ5ï¬ÒšÕokFó,•””¹Ò¾Ô¶ß}DNÚ„ŠôïÓctêT»Þß¼IÑhTÓêÆt_„ Fàú|š¿à&íÞ½SkW¯T8¾Ø‹tIpGÁ¼ #include namespace INDI { namespace AlignmentSubsystem { void TelescopeDirectionVector::RotateAroundY(double Angle) { Angle = Angle * M_PI / 180.0; gsl_vector *pGSLInputVector = gsl_vector_alloc(3); gsl_vector_set(pGSLInputVector, 0, x); gsl_vector_set(pGSLInputVector, 1, y); gsl_vector_set(pGSLInputVector, 2, z); gsl_matrix *pRotationMatrix = gsl_matrix_alloc(3, 3); gsl_matrix_set(pRotationMatrix, 0, 0, cos(Angle)); gsl_matrix_set(pRotationMatrix, 0, 1, 0.0); gsl_matrix_set(pRotationMatrix, 0, 2, sin(Angle)); gsl_matrix_set(pRotationMatrix, 1, 0, 0.0); gsl_matrix_set(pRotationMatrix, 1, 1, 1.0); gsl_matrix_set(pRotationMatrix, 1, 2, 0.0); gsl_matrix_set(pRotationMatrix, 2, 0, -sin(Angle)); gsl_matrix_set(pRotationMatrix, 2, 1, 0.0); gsl_matrix_set(pRotationMatrix, 2, 2, cos(Angle)); gsl_vector *pGSLOutputVector = gsl_vector_alloc(3); gsl_vector_set_zero(pGSLOutputVector); gsl_blas_dgemv(CblasNoTrans, 1.0, pRotationMatrix, pGSLInputVector, 0.0, pGSLOutputVector); x = gsl_vector_get(pGSLOutputVector, 0); y = gsl_vector_get(pGSLOutputVector, 1); z = gsl_vector_get(pGSLOutputVector, 2); gsl_vector_free(pGSLInputVector); gsl_vector_free(pGSLOutputVector); gsl_matrix_free(pRotationMatrix); } } // namespace AlignmentSubsystem } // namespace INDI libindi/libs/indibase/alignment/Common.h0000664000175000017500000001424513263645557017566 0ustar jasemjasem/*! * \file Common.h * * \author Roger James * \date 13th November 2013 * */ #pragma once #include #include #include /// \defgroup AlignmentSubsystem INDI Alignment Subsystem namespace INDI { /// \namespace INDI::AlignmentSubsystem /// \brief Namespace to encapsulate the INDI Alignment Subsystem classes. /// For more information see "INDI Alignment Subsystem" in "Related Pages" accessible via the banner at the /// top of this page. /// \ingroup AlignmentSubsystem namespace AlignmentSubsystem { /** \enum MountAlignment \brief Describe the alignment of a telescope axis. This is normally used to differentiate between equatorial mounts in differnet hemispheres and altaz or dobsonian mounts. */ typedef enum MountAlignment { ZENITH, NORTH_CELESTIAL_POLE, SOUTH_CELESTIAL_POLE } MountAlignment_t; /// \enum AlignmentDatabaseActions /// \brief Action to perform on Alignment Database enum AlignmentDatabaseActions { APPEND, INSERT, EDIT, DELETE, CLEAR, READ, READ_INCREMENT, LOAD_DATABASE, SAVE_DATABASE }; /// \enum AlignmentPointSetEnum /// \brief The offsets to the fields in the alignment point set property /// \note This must match the definitions given to INDI enum AlignmentPointSetEnum { ENTRY_OBSERVATION_JULIAN_DATE, ENTRY_RA, ENTRY_DEC, ENTRY_VECTOR_X, ENTRY_VECTOR_Y, ENTRY_VECTOR_Z }; /*! * \struct TelescopeDirectionVector * \brief Holds a nomalised direction vector (direction cosines) * * The x y,z fields of this class should normally represent a normalised (unit length) * vector in a right handed rectangular coordinate space. However, for convenience a number * a number of standard 3d vector methods are also supported. */ struct TelescopeDirectionVector { /// \brief Default constructor TelescopeDirectionVector() : x(0), y(0), z(0) {} /// \brief Copy constructor TelescopeDirectionVector(double X, double Y, double Z) : x(X), y(Y), z(Z) {} double x; double y; double z; /// \brief Override the * operator to return a cross product inline const TelescopeDirectionVector operator*(const TelescopeDirectionVector &RHS) const { TelescopeDirectionVector Result; Result.x = y * RHS.z - z * RHS.y; Result.y = z * RHS.x - x * RHS.z; Result.z = x * RHS.y - y * RHS.x; return Result; } /// \brief Override the * operator to return a scalar product inline const TelescopeDirectionVector operator*(const double &RHS) const { TelescopeDirectionVector Result; Result.x = x * RHS; Result.y = y * RHS; Result.z = z * RHS; return Result; } /// \brief Override the *= operator to return a unary scalar product inline const TelescopeDirectionVector &operator*=(const double &RHS) { x *= RHS; y *= RHS; z *= RHS; return *this; } /// \brief Override the - operator to return a binary vector subtract inline const TelescopeDirectionVector operator-(const TelescopeDirectionVector &RHS) const { return TelescopeDirectionVector(x - RHS.x, y - RHS.y, z - RHS.z); } /// \brief Override the ^ operator to return a dot product inline double operator^(const TelescopeDirectionVector &RHS) const { return x * RHS.x + y * RHS.y + z * RHS.z; } /// \brief Return the length of the vector /// \return Length of the vector inline double Length() const { return sqrt(x * x + y * y + z * z); } /// \brief Normalise the vector inline void Normalise() { double length = sqrt(x * x + y * y + z * z); x /= length; y /= length; z /= length; } /// \brief Rotate the reference frame around the Y axis. This has the affect of rotating the vector /// itself in the opposite direction /// \param[in] Angle The angle to rotate the reference frame by. Positive values give an anti-clockwise /// rotation from the perspective of looking down the positive axis towards the origin. void RotateAroundY(double Angle); }; /*! * \struct AlignmentDatabaseEntry * \brief Entry in the in memory alignment database * */ struct AlignmentDatabaseEntry { /// \brief Default constructor AlignmentDatabaseEntry() : ObservationJulianDate(0), RightAscension(0), Declination(0), PrivateDataSize(0) {} /// \brief Copy constructor AlignmentDatabaseEntry(const AlignmentDatabaseEntry &Source) : ObservationJulianDate(Source.ObservationJulianDate), RightAscension(Source.RightAscension), Declination(Source.Declination), TelescopeDirection(Source.TelescopeDirection), PrivateDataSize(Source.PrivateDataSize) { if (0 != PrivateDataSize) { PrivateData.reset(new unsigned char[PrivateDataSize]); memcpy(PrivateData.get(), Source.PrivateData.get(), PrivateDataSize); } } /// Override the assignment operator to provide a const version inline const AlignmentDatabaseEntry &operator=(const AlignmentDatabaseEntry &RHS) { ObservationJulianDate = RHS.ObservationJulianDate; RightAscension = RHS.RightAscension; Declination = RHS.Declination; TelescopeDirection = RHS.TelescopeDirection; PrivateDataSize = RHS.PrivateDataSize; if (0 != PrivateDataSize) { PrivateData.reset(new unsigned char[PrivateDataSize]); memcpy(PrivateData.get(), RHS.PrivateData.get(), PrivateDataSize); } return *this; } double ObservationJulianDate; /// \brief Right ascension in decimal hours. N.B. libnova works in decimal degrees /// so conversion is always needed! double RightAscension; /// \brief Declination in decimal degrees double Declination; /// \brief Normalised vector giving telescope pointing direction. /// This is referred to elsewhere as the "apparent" direction. TelescopeDirectionVector TelescopeDirection; /// \brief Private data associated with this sync point std::unique_ptr PrivateData; /// \brief This size in bytes of any private data int PrivateDataSize; }; } // namespace AlignmentSubsystem } // namespace INDI libindi/libs/indibase/alignment/AlignmentSubsystemForMathPlugins.h0000664000175000017500000000115613263645557025013 0ustar jasemjasem #pragma once #include "MathPlugin.h" #include "TelescopeDirectionVectorSupportFunctions.h" namespace INDI { namespace AlignmentSubsystem { /*! * \class AlignmentSubsystemForMathPlugins * \brief This class encapsulates all the alignment subsystem classes that are useful to math plugin implementations. * Math plugins should inherit from this class. */ class AlignmentSubsystemForMathPlugins : public MathPlugin, public TelescopeDirectionVectorSupportFunctions { public: /// \brief Virtual destructor virtual ~AlignmentSubsystemForMathPlugins() {} }; } // namespace AlignmentSubsystem } // namespace INDI libindi/libs/indibase/alignment/InMemoryDatabase.cpp0000664000175000017500000002011113263645557022042 0ustar jasemjasem/*! * \file InMemoryDatabase.cpp * * \author Roger James * \date 13th November 2013 * */ #include "InMemoryDatabase.h" #include "basedevice.h" #include "indicom.h" #include #include #include #include namespace INDI { namespace AlignmentSubsystem { InMemoryDatabase::InMemoryDatabase() : DatabaseReferencePositionIsValid(false), LoadDatabaseCallback(nullptr), LoadDatabaseCallbackThisPointer(nullptr) { } bool InMemoryDatabase::CheckForDuplicateSyncPoint(const AlignmentDatabaseEntry &CandidateEntry, double Tolerance) const { for (AlignmentDatabaseType::const_iterator iTr = MySyncPoints.begin(); iTr != MySyncPoints.end(); iTr++) { if (((std::abs((*iTr).RightAscension - CandidateEntry.RightAscension) < 24.0 * Tolerance / 100.0) && (std::abs((*iTr).Declination - CandidateEntry.Declination) < 180.0 * Tolerance / 100.0)) || ((std::abs((*iTr).TelescopeDirection.x - CandidateEntry.TelescopeDirection.x) < Tolerance / 100.0) && (std::abs((*iTr).TelescopeDirection.y - CandidateEntry.TelescopeDirection.y) < Tolerance / 100.0) && (std::abs((*iTr).TelescopeDirection.z - CandidateEntry.TelescopeDirection.z) < Tolerance / 100.0))) return true; } return false; } bool InMemoryDatabase::GetDatabaseReferencePosition(ln_lnlat_posn &Position) { if (DatabaseReferencePositionIsValid) { Position = DatabaseReferencePosition; return true; } else return false; } bool InMemoryDatabase::LoadDatabase(const char *DeviceName) { char DatabaseFileName[MAXRBUF]; char Errmsg[MAXRBUF]; XMLEle *FileRoot = nullptr; XMLEle *EntriesRoot = nullptr; XMLEle *EntryRoot = nullptr; XMLEle *Element = nullptr; XMLAtt *Attribute = nullptr; LilXML *Parser = newLilXML(); FILE *fp = nullptr; snprintf(DatabaseFileName, MAXRBUF, "%s/.indi/%s_alignment_database.xml", getenv("HOME"), DeviceName); fp = fopen(DatabaseFileName, "r"); if (fp == nullptr) { snprintf(Errmsg, MAXRBUF, "Unable to read alignment database file. Error loading file %s: %s\n", DatabaseFileName, strerror(errno)); return false; } if (nullptr == (FileRoot = readXMLFile(fp, Parser, Errmsg))) { snprintf(Errmsg, MAXRBUF, "Unable to parse database XML: %s", Errmsg); return false; } if (strcmp(tagXMLEle(FileRoot), "INDIAlignmentDatabase") != 0) { return false; } if (nullptr == (EntriesRoot = findXMLEle(FileRoot, "DatabaseEntries"))) { snprintf(Errmsg, MAXRBUF, "Cannot find DatabaseEntries element"); return false; } if (nullptr != (Element = findXMLEle(FileRoot, "DatabaseReferenceLocation"))) { if (nullptr == (Attribute = findXMLAtt(Element, "latitude"))) { snprintf(Errmsg, MAXRBUF, "Cannot find latitude attribute"); return false; } sscanf(valuXMLAtt(Attribute), "%lf", &DatabaseReferencePosition.lat); if (nullptr == (Attribute = findXMLAtt(Element, "longitude"))) { snprintf(Errmsg, MAXRBUF, "Cannot find latitude attribute"); return false; } sscanf(valuXMLAtt(Attribute), "%lf", &DatabaseReferencePosition.lng); DatabaseReferencePositionIsValid = true; } MySyncPoints.clear(); for (EntryRoot = nextXMLEle(EntriesRoot, 1); EntryRoot != nullptr; EntryRoot = nextXMLEle(EntriesRoot, 0)) { AlignmentDatabaseEntry CurrentValues; if (strcmp(tagXMLEle(EntryRoot), "DatabaseEntry") != 0) { return false; } for (Element = nextXMLEle(EntryRoot, 1); Element != nullptr; Element = nextXMLEle(EntryRoot, 0)) { if (strcmp(tagXMLEle(Element), "ObservationJulianDate") == 0) { sscanf(pcdataXMLEle(Element), "%lf", &CurrentValues.ObservationJulianDate); } else if (strcmp(tagXMLEle(Element), "RightAscension") == 0) { f_scansexa(pcdataXMLEle(Element), &CurrentValues.RightAscension); } else if (strcmp(tagXMLEle(Element), "Declination") == 0) { f_scansexa(pcdataXMLEle(Element), &CurrentValues.Declination); } else if (strcmp(tagXMLEle(Element), "TelescopeDirectionVectorX") == 0) { sscanf(pcdataXMLEle(Element), "%lf", &CurrentValues.TelescopeDirection.x); } else if (strcmp(tagXMLEle(Element), "TelescopeDirectionVectorY") == 0) { sscanf(pcdataXMLEle(Element), "%lf", &CurrentValues.TelescopeDirection.y); } else if (strcmp(tagXMLEle(Element), "TelescopeDirectionVectorZ") == 0) { sscanf(pcdataXMLEle(Element), "%lf", &CurrentValues.TelescopeDirection.z); } else return false; } MySyncPoints.push_back(CurrentValues); } fclose(fp); delXMLEle(FileRoot); delLilXML(Parser); if (nullptr != LoadDatabaseCallback) (*LoadDatabaseCallback)(LoadDatabaseCallbackThisPointer); return true; } bool InMemoryDatabase::SaveDatabase(const char *DeviceName) { char ConfigDir[MAXRBUF]; char DatabaseFileName[MAXRBUF]; char Errmsg[MAXRBUF]; struct stat Status; FILE *fp; snprintf(ConfigDir, MAXRBUF, "%s/.indi/", getenv("HOME")); snprintf(DatabaseFileName, MAXRBUF, "%s%s_alignment_database.xml", ConfigDir, DeviceName); if (stat(ConfigDir, &Status) != 0) { if (mkdir(ConfigDir, S_IRWXU | S_IRWXG | S_IROTH | S_IXOTH) < 0) { snprintf(Errmsg, MAXRBUF, "Unable to create config directory. Error %s: %s\n", ConfigDir, strerror(errno)); return false; } } fp = fopen(DatabaseFileName, "w"); if (fp == nullptr) { snprintf(Errmsg, MAXRBUF, "Unable to open database file. Error opening file %s: %s\n", DatabaseFileName, strerror(errno)); return false; } fprintf(fp, "\n"); if (DatabaseReferencePositionIsValid) fprintf(fp, " \n", DatabaseReferencePosition.lat, DatabaseReferencePosition.lng); fprintf(fp, " \n"); for (AlignmentDatabaseType::const_iterator Itr = MySyncPoints.begin(); Itr != MySyncPoints.end(); Itr++) { char SexaString[12]; // Long enough to hold xx:xx:xx.xx fprintf(fp, " \n"); fprintf(fp, " %lf\n", (*Itr).ObservationJulianDate); fs_sexa(SexaString, (*Itr).RightAscension, 2, 3600); fprintf(fp, " %s\n", SexaString); fs_sexa(SexaString, (*Itr).Declination, 2, 3600); fprintf(fp, " %s\n", SexaString); fprintf(fp, " %lf\n", (*Itr).TelescopeDirection.x); fprintf(fp, " %lf\n", (*Itr).TelescopeDirection.y); fprintf(fp, " %lf\n", (*Itr).TelescopeDirection.z); fprintf(fp, " \n"); } fprintf(fp, " \n"); fprintf(fp, "\n"); fclose(fp); return true; } void InMemoryDatabase::SetDatabaseReferencePosition(double Latitude, double Longitude) { DatabaseReferencePosition.lat = Latitude; DatabaseReferencePosition.lng = Longitude; DatabaseReferencePositionIsValid = true; } void InMemoryDatabase::SetLoadDatabaseCallback(LoadDatabaseCallbackPointer_t CallbackPointer, void *ThisPointer) { LoadDatabaseCallback = CallbackPointer; LoadDatabaseCallbackThisPointer = ThisPointer; } } // namespace AlignmentSubsystem } // namespace INDI libindi/libs/indibase/alignment/LoaderClient.h0000664000175000017500000000210313263645557020671 0ustar jasemjasem #pragma once #include "indibase/baseclient.h" #include "indibase/basedevice.h" #include "indibase/alignment/AlignmentSubsystemForClients.h" class LoaderClient : public INDI::BaseClient, INDI::AlignmentSubsystem::AlignmentSubsystemForClients { public: LoaderClient(); virtual ~LoaderClient(); // Public methods void Initialise(int argc, char *argv[]); void Load(); protected: // Protected methods virtual void newBLOB(IBLOB *bp); virtual void newDevice(INDI::BaseDevice *dp); virtual void newLight(ILightVectorProperty *lvp) {} virtual void newMessage(INDI::BaseDevice *dp, int messageID) {} virtual void newNumber(INumberVectorProperty *nvp); virtual void newProperty(INDI::Property *property); virtual void newSwitch(ISwitchVectorProperty *svp); virtual void newText(ITextVectorProperty *tvp) {} virtual void removeProperty(INDI::Property *property) {} virtual void serverConnected() {} virtual void serverDisconnected(int exit_code) {} private: INDI::BaseDevice *Device; std::string DeviceName; }; libindi/libs/indibase/alignment/SVDMathPlugin.h0000664000175000017500000000325113263645557020756 0ustar jasemjasem/// \file SVDMathPlugin.h /// /// \author Roger James /// \date 13th November 2013 /// /// This file provides the Singular Value Decomposition (Markley) math plugin functionality #pragma once #include "BasicMathPlugin.h" namespace INDI { namespace AlignmentSubsystem { /*! * \class SVDMathPlugin * \brief This class implements the SVD math plugin. */ class SVDMathPlugin : public BasicMathPlugin { private: /// \brief Calculate tranformation matrices from the supplied vectors /// \param[in] Alpha1 Pointer to the first coordinate in the alpha reference frame /// \param[in] Alpha2 Pointer to the second coordinate in the alpha reference frame /// \param[in] Alpha3 Pointer to the third coordinate in the alpha reference frame /// \param[in] Beta1 Pointer to the first coordinate in the beta reference frame /// \param[in] Beta2 Pointer to the second coordinate in the beta reference frame /// \param[in] Beta3 Pointer to the third coordinate in the beta reference frame /// \param[in] pAlphaToBeta Pointer to a matrix to receive the Alpha to Beta transformation matrix /// \param[in] pBetaToAlpha Pointer to a matrix to receive the Beta to Alpha transformation matrix void CalculateTransformMatrices(const TelescopeDirectionVector &Alpha1, const TelescopeDirectionVector &Alpha2, const TelescopeDirectionVector &Alpha3, const TelescopeDirectionVector &Beta1, const TelescopeDirectionVector &Beta2, const TelescopeDirectionVector &Beta3, gsl_matrix *pAlphaToBeta, gsl_matrix *pBetaToAlpha); }; } // namespace AlignmentSubsystem } // namespace INDI libindi/libs/indibase/alignment/ClientAPIForMathPluginManagement.cpp0000664000175000017500000001237413263645557025077 0ustar jasemjasem/*! * \file ClientAPIForMathPluginManagement.cpp * * \author Roger James * \date 13th November 2013 * */ #include "ClientAPIForMathPluginManagement.h" #include namespace INDI { namespace AlignmentSubsystem { // Public methods bool ClientAPIForMathPluginManagement::EnumerateMathPlugins(MathPluginsList &AvailableMathPlugins) { // Wait for driver to initialise if neccessary WaitForDriverCompletion(); AvailableMathPlugins.clear(); ISwitchVectorProperty *pPlugins = MathPlugins->getSwitch(); for (int i = 0; i < pPlugins->nsp; i++) AvailableMathPlugins.emplace_back(std::string(pPlugins->sp[i].label)); return true; } void ClientAPIForMathPluginManagement::Initialise(INDI::BaseClient *BaseClient) { ClientAPIForMathPluginManagement::BaseClient = BaseClient; } void ClientAPIForMathPluginManagement::ProcessNewDevice(INDI::BaseDevice *DevicePointer) { Device = DevicePointer; } void ClientAPIForMathPluginManagement::ProcessNewProperty(INDI::Property *PropertyPointer) { bool GotOneOfMine = true; if (strcmp(PropertyPointer->getName(), "ALIGNMENT_SUBSYSTEM_MATH_PLUGINS") == 0) MathPlugins = PropertyPointer; else if (strcmp(PropertyPointer->getName(), "ALIGNMENT_SUBSYSTEM_MATH_PLUGIN_INITIALISE") == 0) PluginInitialise = PropertyPointer; else GotOneOfMine = false; // Tell the client when all the database proeprties have been set up if (GotOneOfMine && (nullptr != MathPlugins) && (nullptr != PluginInitialise)) { // The DriverActionComplete state variable is initialised to false // So I need to call this to set it to true and signal anyone // waiting for the driver to initialise etc. SignalDriverCompletion(); } } void ClientAPIForMathPluginManagement::ProcessNewSwitch(ISwitchVectorProperty *SwitchVectorProperty) { if (strcmp(SwitchVectorProperty->name, "ALIGNMENT_SUBSYSTEM_MATH_PLUGINS") == 0) { if (IPS_BUSY != SwitchVectorProperty->s) SignalDriverCompletion(); } else if (strcmp(SwitchVectorProperty->name, "ALIGNMENT_SUBSYSTEM_MATH_PLUGIN_INITIALISE") == 0) { if (IPS_BUSY != SwitchVectorProperty->s) SignalDriverCompletion(); } } bool ClientAPIForMathPluginManagement::SelectMathPlugin(const std::string &MathPluginName) { // Wait for driver to initialise if neccessary WaitForDriverCompletion(); ISwitchVectorProperty *pPlugins = MathPlugins->getSwitch(); int i; for (i = 0; i < pPlugins->nsp; i++) { if (0 == strcmp(MathPluginName.c_str(), pPlugins->sp[i].label)) break; } if (i >= pPlugins->nsp) return false; IUResetSwitch(pPlugins); pPlugins->sp[i].s = ISS_ON; SetDriverBusy(); BaseClient->sendNewSwitch(pPlugins); WaitForDriverCompletion(); if (IPS_OK != pPlugins->s) { IDLog("SelectMathPlugin - Bad MathPlugins switch state %s\n", pstateStr(pPlugins->s)); return false; } return true; } bool ClientAPIForMathPluginManagement::ReInitialiseMathPlugin() { // Wait for driver to initialise if neccessary WaitForDriverCompletion(); ISwitchVectorProperty *pPluginInitialise = PluginInitialise->getSwitch(); IUResetSwitch(pPluginInitialise); pPluginInitialise->sp[0].s = ISS_ON; SetDriverBusy(); BaseClient->sendNewSwitch(pPluginInitialise); WaitForDriverCompletion(); if (IPS_OK != pPluginInitialise->s) { IDLog("ReInitialiseMathPlugin - Bad PluginInitialise switch state %s\n", pstateStr(pPluginInitialise->s)); return false; } return true; } // Private methods bool ClientAPIForMathPluginManagement::SetDriverBusy() { int ReturnCode = pthread_mutex_lock(&DriverActionCompleteMutex); if (ReturnCode != 0) return false; DriverActionComplete = false; IDLog("SetDriverBusy\n"); ReturnCode = pthread_mutex_unlock(&DriverActionCompleteMutex); return ReturnCode == 0; } bool ClientAPIForMathPluginManagement::SignalDriverCompletion() { int ReturnCode = pthread_mutex_lock(&DriverActionCompleteMutex); if (ReturnCode != 0) return false; DriverActionComplete = true; ReturnCode = pthread_cond_signal(&DriverActionCompleteCondition); if (ReturnCode != 0) { ReturnCode = pthread_mutex_unlock(&DriverActionCompleteMutex); return false; } IDLog("SignalDriverCompletion\n"); ReturnCode = pthread_mutex_unlock(&DriverActionCompleteMutex); return ReturnCode == 0; } bool ClientAPIForMathPluginManagement::WaitForDriverCompletion() { int ReturnCode = pthread_mutex_lock(&DriverActionCompleteMutex); while (!DriverActionComplete) { IDLog("WaitForDriverCompletion - Waiting\n"); ReturnCode = pthread_cond_wait(&DriverActionCompleteCondition, &DriverActionCompleteMutex); IDLog("WaitForDriverCompletion - Back from wait ReturnCode = %d\n", ReturnCode); if (ReturnCode != 0) { ReturnCode = pthread_mutex_unlock(&DriverActionCompleteMutex); return false; } } IDLog("WaitForDriverCompletion - Finished waiting\n"); ReturnCode = pthread_mutex_unlock(&DriverActionCompleteMutex); return ReturnCode == 0; } } // namespace AlignmentSubsystem } // namespace INDI libindi/libs/indibase/alignment/MathPluginManagerClient.h0000664000175000017500000000215113263645557023031 0ustar jasemjasem #pragma once #include "indibase/baseclient.h" #include "indibase/basedevice.h" #include "indibase/alignment/AlignmentSubsystemForClients.h" class MathPluginManagerClient : public INDI::BaseClient, INDI::AlignmentSubsystem::AlignmentSubsystemForClients { public: MathPluginManagerClient(); virtual ~MathPluginManagerClient(); // Public methods void Initialise(int argc, char *argv[]); void Test(); protected: // Protected methods virtual void newBLOB(IBLOB *bp) {} virtual void newDevice(INDI::BaseDevice *dp); virtual void newMessage(INDI::BaseDevice *dp, int messageID) {} virtual void newNumber(INumberVectorProperty *nvp) {} virtual void newProperty(INDI::Property *property); virtual void newSwitch(ISwitchVectorProperty *svp); virtual void newText(ITextVectorProperty *tvp) {} virtual void newLight(ILightVectorProperty *lvp) {} virtual void removeProperty(INDI::Property *property) {} virtual void serverConnected() {} virtual void serverDisconnected(int exit_code) {} private: INDI::BaseDevice *Device; std::string DeviceName; }; libindi/libs/indibase/alignment/MathPluginManagement.h0000664000175000017500000001561413263645557022404 0ustar jasemjasem/*! * \file MathPluginManagement.h * * \author Roger James * \date 13th November 2013 * */ #pragma once #include "BuiltInMathPlugin.h" #include "inditelescope.h" #include namespace INDI { namespace AlignmentSubsystem { /*! * \class MathPluginManagement * \brief The following INDI properties are used to manage math plugins * - ALIGNMENT_SUBSYSTEM_MATH_PLUGINS\n * A list of available plugins (switch). This also allows the client to select a plugin. * - ALIGNMENT_SUBSYSTEM_MATH_PLUGIN_INITIALISE\n * Initialise or re-initialise the current math plugin. * - ALIGNMENT_SUBSYSTEM_CURRENT_MATH_PLUGIN\n * This is not communicated to the client and only used to save the currently selected plugin name * to persistent storage. * * This class also provides function links to the currently selected math plugin */ class MathPluginManagement : private MathPlugin // Derive from MathPluign to force the function signatures to match { public: /** \enum MountType \brief Describes the basic type of the mount. */ typedef enum MountType { EQUATORIAL, ALTAZ } MountType_t; /// \brief Default constructor MathPluginManagement(); /// \brief Virtual destructor virtual ~MathPluginManagement() {} /** \brief Initialize alignment math plugin properties. It is recommended to call this function within initProperties() of your primary device * \param[in] pTelescope Pointer to the child INDI::Telecope class */ void InitProperties(Telescope *pTelescope); /** \brief Call this function from within the ISNewSwitch processing path. The function will * handle any math plugin switch properties. * \param[in] pTelescope Pointer to the child INDI::Telecope class * \param[in] name vector property name * \param[in] states states as passed by the client * \param[in] names names as passed by the client * \param[in] n number of values and names pair to process. */ void ProcessSwitchProperties(Telescope *pTelescope, const char *name, ISState *states, char *names[], int n); /** \brief Call this function from within the ISNewText processing path. The function will * handle any math plugin text properties. This text property is at the moment only contained in the * config file so this will normally only have work to do when the config file is loaded. * \param[in] pTelescope Pointer to the child INDI::Telecope class * \param[in] name vector property name * \param[in] texts texts as passed by the client * \param[in] names names as passed by the client * \param[in] n number of values and names pair to process. */ void ProcessTextProperties(Telescope *pTelescope, const char *name, char *texts[], char *names[], int n); /** \brief Call this function to save persistent math plugin properties. * This function should be called from within the saveConfigItems function of your driver. * \param[in] fp File pointer passed into saveConfigItems */ void SaveConfigProperties(FILE *fp); /** \brief Call this function to set the ApproximateMountAlignment property of the current Math Plugin. The alignment database should be initialised before this function is called so that it can use the DatabaseReferencePosition to determine which hemisphere the current observing site is in. For equatorial the ApproximateMountAlignment property will set to NORTH_CELESTIAL_POLE for sites in the northern hemisphere and SOUTH_CELESTIAL_POLE for sites in the southern hemisphere. For altaz mounts the ApproximateMountAlignment will be set to ZENITH. \param[in] Type the mount type either EQUATORIAL or ALTAZ */ void SetApproximateMountAlignmentFromMountType(MountType_t Type); /// \brief Set the current in memory database /// \param[in] pDatabase A pointer to the current in memory database void SetCurrentInMemoryDatabase(InMemoryDatabase *pDatabase) { CurrentInMemoryDatabase = pDatabase; } /** * @brief SetAlignmentSubsystemActive Enable or Disable alignment subsystem * @param enable True to activate Alignment Subsystem. False to deactivate Alignment subsystem. */ void SetAlignmentSubsystemActive(bool enable); /// \brief Return status of alignment subsystem /// \return True if active bool IsAlignmentSubsystemActive() const { return AlignmentSubsystemActive.s == ISS_ON ? true : false; } // These must match the function signatures in MathPlugin MountAlignment_t GetApproximateMountAlignment(); bool Initialise(InMemoryDatabase *pInMemoryDatabase); void SetApproximateMountAlignment(MountAlignment_t ApproximateAlignment); bool TransformCelestialToTelescope(const double RightAscension, const double Declination, double JulianOffset, TelescopeDirectionVector &ApparentTelescopeDirectionVector); bool TransformTelescopeToCelestial(const TelescopeDirectionVector &ApparentTelescopeDirectionVector, double &RightAscension, double &Declination); private: void EnumeratePlugins(); std::vector MathPluginFiles; std::vector MathPluginDisplayNames; std::unique_ptr AlignmentSubsystemMathPlugins; ISwitchVectorProperty AlignmentSubsystemMathPluginsV; ISwitch AlignmentSubsystemMathPluginInitialise; ISwitchVectorProperty AlignmentSubsystemMathPluginInitialiseV; ISwitch AlignmentSubsystemActive; ISwitchVectorProperty AlignmentSubsystemActiveV; InMemoryDatabase *CurrentInMemoryDatabase; // The following property is used for configuration purposes only and is not propagated to the client IText AlignmentSubsystemCurrentMathPlugin; ITextVectorProperty AlignmentSubsystemCurrentMathPluginV; // The following hold links to the current loaded math plugin // These must match the function signatures in MathPlugin MountAlignment_t (MathPlugin::*pGetApproximateMountAlignment)(); bool (MathPlugin::*pInitialise)(InMemoryDatabase *pInMemoryDatabase); void (MathPlugin::*pSetApproximateMountAlignment)(MountAlignment_t ApproximateAlignment); bool (MathPlugin::*pTransformCelestialToTelescope)(const double RightAscension, const double Declination, double JulianOffset, TelescopeDirectionVector &TelescopeDirectionVector); bool (MathPlugin::*pTransformTelescopeToCelestial)(const TelescopeDirectionVector &TelescopeDirectionVector, double &RightAscension, double &Declination); MathPlugin *pLoadedMathPlugin; void *LoadedMathPluginHandle; BuiltInMathPlugin BuiltInPlugin; }; } // namespace AlignmentSubsystem } // namespace INDI libindi/libs/indibase/alignment/MathPluginManagerClient.cpp0000664000175000017500000000373513263645557023375 0ustar jasemjasem#include "MathPluginManagerClient.h" #include #include #include using namespace INDI::AlignmentSubsystem; MathPluginManagerClient::MathPluginManagerClient() : DeviceName("skywatcherAPIMount") { //ctor } MathPluginManagerClient::~MathPluginManagerClient() { //dtor } // Public methods void MathPluginManagerClient::Initialise(int argc, char *argv[]) { std::string HostName("localhost"); int Port = 7624; if (argc > 1) DeviceName = argv[1]; if (argc > 2) HostName = argv[2]; if (argc > 3) { std::istringstream Parameter(argv[3]); Parameter >> Port; } AlignmentSubsystemForClients::Initialise(DeviceName.c_str(), this); setServer(HostName.c_str(), Port); watchDevice(DeviceName.c_str()); connectServer(); setBLOBMode(B_ALSO, DeviceName.c_str(), nullptr); } void MathPluginManagerClient::Test() { MathPluginsList AvailableMathPlugins; cout << "Testing Enumerate available plugins\n"; if (EnumerateMathPlugins(AvailableMathPlugins)) { cout << "Success - List of plugins follows\n"; for (MathPluginsList::const_iterator iTr = AvailableMathPlugins.begin(); iTr != AvailableMathPlugins.end(); iTr++) cout << *iTr << '\n'; } else cout << "Failure\n"; cout << "Testing select plugin\n"; if (SelectMathPlugin(AvailableMathPlugins[AvailableMathPlugins.size() - 1])) cout << "Success\n"; else cout << "Failure\n"; cout << "Testing reinitilialise plugin\n"; if (ReInitialiseMathPlugin()) cout << "Success\n"; else cout << "Failure\n"; } // Protected methods void MathPluginManagerClient::newDevice(INDI::BaseDevice *dp) { ProcessNewDevice(dp); } void MathPluginManagerClient::newProperty(INDI::Property *property) { ProcessNewProperty(property); } void MathPluginManagerClient::newSwitch(ISwitchVectorProperty *svp) { ProcessNewSwitch(svp); } libindi/libs/indibase/alignment/TelescopeDirectionVectorSupportFunctions.cpp0000664000175000017500000000530413263645557027122 0ustar jasemjasem/*! * \file TelescopeDirectionVectorSupportFunctions.cpp * * \author Roger James * \date 13th November 2013 * */ #include "TelescopeDirectionVectorSupportFunctions.h" namespace INDI { namespace AlignmentSubsystem { void TelescopeDirectionVectorSupportFunctions::SphericalCoordinateFromTelescopeDirectionVector( const TelescopeDirectionVector TelescopeDirectionVector, double &AzimuthAngle, AzimuthAngleDirection AzimuthAngleDirection, double &PolarAngle, PolarAngleDirection PolarAngleDirection) { if (ANTI_CLOCKWISE == AzimuthAngleDirection) { if (FROM_AZIMUTHAL_PLANE == PolarAngleDirection) { AzimuthAngle = atan2(TelescopeDirectionVector.y, TelescopeDirectionVector.x); PolarAngle = asin(TelescopeDirectionVector.z); } else { AzimuthAngle = atan2(TelescopeDirectionVector.y, TelescopeDirectionVector.x); PolarAngle = acos(TelescopeDirectionVector.z); } } else { if (FROM_AZIMUTHAL_PLANE == PolarAngleDirection) { AzimuthAngle = atan2(-TelescopeDirectionVector.y, TelescopeDirectionVector.x); PolarAngle = asin(TelescopeDirectionVector.z); } else { AzimuthAngle = atan2(-TelescopeDirectionVector.y, TelescopeDirectionVector.x); PolarAngle = acos(TelescopeDirectionVector.z); } } } const TelescopeDirectionVector TelescopeDirectionVectorSupportFunctions::TelescopeDirectionVectorFromSphericalCoordinate( const double AzimuthAngle, AzimuthAngleDirection AzimuthAngleDirection, const double PolarAngle, PolarAngleDirection PolarAngleDirection) { TelescopeDirectionVector Vector; if (ANTI_CLOCKWISE == AzimuthAngleDirection) { if (FROM_AZIMUTHAL_PLANE == PolarAngleDirection) { Vector.x = cos(PolarAngle) * cos(AzimuthAngle); Vector.y = cos(PolarAngle) * sin(AzimuthAngle); Vector.z = sin(PolarAngle); } else { Vector.x = sin(PolarAngle) * sin(AzimuthAngle); Vector.y = sin(PolarAngle) * cos(AzimuthAngle); Vector.z = cos(PolarAngle); } } else { if (FROM_AZIMUTHAL_PLANE == PolarAngleDirection) { Vector.x = cos(PolarAngle) * cos(-AzimuthAngle); Vector.y = cos(PolarAngle) * sin(-AzimuthAngle); Vector.z = sin(PolarAngle); } else { Vector.x = sin(PolarAngle) * sin(-AzimuthAngle); Vector.y = sin(PolarAngle) * cos(-AzimuthAngle); Vector.z = cos(PolarAngle); } } return Vector; } } // namespace AlignmentSubsystem } // namespace INDI libindi/libs/indibase/alignment/controlpanel5.png0000664000175000017500000031211113263645557021451 0ustar jasemjasem‰PNG  IHDR¿’¥lÆsBIT|dˆtEXtSoftwaregnome-screenshotï¿> IDATxœìÝwœÔÔÚÀñ_’iÛ; »Ô¥³t”¢HQÀk‚½÷Þ^»bïWîµ^T,("¢"RDzïeiK[Ø^¦d’¼Ìle;‹ >ßÏgXfæäœ““3™‡g2þ|ºŽËá@³iu~-ƒÂì| ÐÂ"ˆ ÕPʼf '&ÌVrÇ Ó[À῎CQÐ4µö-fÄpÞkorA#Ø7åÿ¸ïÇ4#.øšÅîïžç±¯ÒqFØÁ[ˆ¯Ëý|y{[(ZÄã·ü4âòÐ3ŒëAÉ`¡^À][X>ïg¾ým;¾ˆPœTVV>¡•x†—\_#N>÷lFôK¥MB &…ÓXõû,¦ÌÜ€ÛѰ©“kt籯§ùâÞÇø¡0„Уõ¬ìÖˆ–UþÖ_–‰»0œAÿ÷Wvp•¾îË#}ëZæÏüŽ™ë -óÃÃ1Q›º !„B!N8ušöl™à×}'WQü~»ÝA¿~}˜=û7ljà`Ó4ñy½œ>xª î"w ùø±Ûí prI>u ~k¤Ðüœ»¸ãÀc¼ü‡›ð#Z×Â4mDÆ—¾¦ª=œÆmzrV›ž ?}O<;}v{íŠ4}ä+¹á…[8-¾ì*a‰mèòZ¦~»Ë`â+òPèÑñ› ¨ÎÂB4TËÀ]àÆ­›Á!ŠÓ„‡h(XXŠZ´cR˜K¡æ"6Ɖf™xŠÜ¸}þ’ÑgQÑ!ؕʮ‘…iÙˆŒ- |M TG$MSûsiêI¤¾÷ÏÏËÂï7T_A³Û sá²)µX¿Òõòy(tëø­`šÐÂ]ÕŒ¼ !„B!Nhµùµ,ðûuF4£‘U„ž“@F‘ÃnNqa%i·dn×H wb¸ói͈&áü’éCÓl膟ää$\.EEn^)Ú4Í’Q4E¡äêÌŠ¢ ë~BBBIJNâÐÁ šC7hܸ1áaaÁ|`Μ¹øýþJ×aذ!èº^’OFF6«ñeítÄk.z\'£ÓÇóåpT“ݾ)ÿÇÝ_ïà I óK¹cl¢ZÅ]£Wqçgé•U Â¶²ð:éuçµÁÀ7ƒ¹NäÛå»Éö»HhÙžÖÊfr6œåÛi3âZ.Ñ‹”( =k;¿Oÿ‚‰¿îÁfÃëeÈMW0´c2M¢œ(€ž•ÆÂï>᣹q¸ÊÖ%‘‹_~‹vOæ–GæRàˆ¥ÏÅ×pfÏ6´j†xÒg3þñ/Øá§}•eWþVù-ßÎÅmGt{.¸ÿ~.JqÐõì¡ mÕ†súµ >TeÒ‚C8C+Û’2ò+„B!ĉ.üÖ.±î78-!a¦_à ÇàÕå{¸§{:ņ²%ÇÍK+÷qkÏæ4vÒø ó”ẪnTÕ†á7i’Ô·Ûaê`–ŒÖ+µ G¦i’”Ô„ýûöc·Ào$%5Æã äS¼è°aƒÈgΜy躎¢ÊjšœÄþ}ûë´X•Îz-÷š‘çE‹Lâü»/aã}³µ†,µp¢\Elûñ=^M~'O ¶ÿ ZLþˆ´Š…Yê`êx#zsN';'½ÊçäjÃŽ›ÌíË9ˆ†Ë®à+°H÷-ÉËÛšAW¼¦gl §]yʾx'Í,7W@ÏÏ!ß¾CE˜ª…׈甡ÝhcÐñúí¸Ô\òtm/¯¾l«âªU¡_VÒöZHN÷væ/:ÀE)IGXDΫórIq/¯\Þ–¦ÿº€n?¼ÉÒ’ó<~~ü>ÞI‹æ¬§^ઔ0zÚ uSfIð{¯Ÿ|„/÷¨š“Èp ÅSfùÇîã-:šË=;j]vÙu­žéÕQšödä ¤À ¹;ùcÂøÒ°AXloîzæbZÇ¥Ò)úKväÖ¼~Þ¨Œìãóžä«]†>È¿¯mCϳzµx.YGœã,‘¯B!„'ºÚ_ð*x©PK/ ZÌœC´Ðݧ -XÅØ~]I5³ðçe•.«(¸L½dd× Þ¢¨xz²¢(,\¸¸Ê¢ûõë[²¬Ó¥aZ`ƒ[]×Kò©¸>eÿFUq…»Ñ˨PWgD¡¦OR]ˬ[¹mSa¥’.Ïä‹Ê¾âféw›hvéC<=¢ åR;uTlÓJÖÏ0 © Í%pÉ ïpIÙEb’‰V-²( !„B!N8µžö¬Z)Èùn/NÃIJ̒÷6íÞÇ×›rsJW¦-Û@ËöQtŒ*=ÛUQU ÜžÀf5»Èƒ ˜EeРSKÎó-V|þ¯ÛíÆ´£½î¢@>Óz<^ÅTµ4ø­ìF¦ibšÑS¯×]®[ƒU1¹<ÏZþ÷Ưtzt0)]c«_ÞÓ0ð¸CHí×2pñ§¼=ì÷Y«âôZEÅÌÚC6I#z¦Æðåì<ì. U Œ¸¦J¹ëš®vc•Š…ßg””U>0WP•2uªb>¸Uß²+™J\ÙrzáaölY˼f°Ð5Ž·F´Áaìaöç¿°ÑÛ”s®Fó`¹5®Ÿbª/À•Éâ—r°L´oæo%_U+íû !„Bqb«õ´g UÕØšï!U31Àhë¶|?¯mÉç¶¡}èìôÑ~ø)¼4s!w´ §CdàŠÅªbg{5xµgUUÉÎÎ!"" Ë2QU•¢¢"TµôÊÌ%£Ä¦YòP•üüü`>‹aåææó±PU•À•ÍàÿƒkŒÎü~#˜BAA^IúÚ«eZÍ[¿âÍYÝyêÌØj“š7…®fœtñXn9%p5¥¿Ía—UÙU+D™ªŠ–³†¹;ÇpyKH¹ü®Ó'ñݲ=dûíD5nG‡F‡Y¾òƾµì¡)®ÎŒ8)–WççÒxÄÚø÷°>Ã_»+û (4-š”$Þô<¼6!•%¶P4µ–e[&@( qNü»t°©åò*«ø‚W~EA³iØ•¸‘Û=‡©3æ“éìH¯1Ãh^îB]ÕQ1na?]iJ8!ñÕ¬]š öÈx¢­|<6E¯©®B!„BˆMí¯ölš8ìvæg’š˜J ÊíC;ÐÑ—~(“ÖÑÜ{æÉ83v`ù‹ kæöã°‡¢¢b³Ù8|øaa!†¢˜¨ª‰¦©G¿†QüÚl6222°k¶Jó Üû-ZZé:øý:¦i•äc³Ùë8]µ¦Ë]•¾dsl›ò³ûÞÇèÊsKºh<Ÿ_Tþµ¢uSxeÚ>lN ¿·â§i+8ÙüüöWœòÌ(ÚÚ’rí ¹¶L÷2ž½í6æ,æÓ_ÿÅ£ƒ£9éæ™|%áûg~Ãj¯ŠZ!~;²ilþt–l7éÑÎA¯»^gR‘ŽÝ=ŸGî™ÌŽ Ñ³……¥h8j,[Ãne²õœ”l£ûã¹ó…‡ø÷ƒP[ÕÁU—‹hg Ò–ßGö¶ЄðV—óòËÙ[BËjß#×OÅvx“žÉ}ý"èzÅ||…ݲaW|,yîN^Ûf¡5ÕU!„Bq¢©õ´g¿aawØ™“§12R§©¢bú}$…P´¯ä_#ç0­ &f‘Õî`yÅÜððÀH°Íæ ++›¸øxœ{`4VUÐ -ü–žGi˜–ÕÍÊÎ!';›ÐÐpPŽÌÇoXtïÞ MSƒS “lM30\äv£* …EEdÎ$$,Ó0«^éJTz¥ßJÓi¸ÌÍ|ö¿eœrwoŠï£(:9sð7Š.½p²7—ôíYüÛ,¾_˜ŽâÀ†‰^YY SmØþÈ“÷ìå¬ÑÃÔ³5MÂ4ÀÂ}x–o¤Ð¦¡)~6ô4Ïeå²3ºÓO&ié%·<ªŒj³“§Øyà@gv{ Ó´Ð}> Ó¤uë–%#½¥·6*ùµ, EU1 ‹íÛ·¡Ùl8ìNTU)ÉÇ´,RRZ¡¨ê²ùÊ3÷V°ÒÒÒUSêq²¦‰§À‹×Õé"©”{Ms¹w”9oY÷‘ï6°°p6ÅÂWäÅí/Snð~Æv‡ §]-¤UVVå,ÓÀçõãó›˜Á¬UE³i¸œ64ÀÂïÕñø PTì;!Î`™–Aa¾? ްÀ…¬ ¯‡¯šƒÈ0-Î4p»u|Fðì_ÍFh¨G._¦†Õ— X†·Çn€3̅눫*×Ô†OÇí –QÒ¶®@[«õ«X×`[:\\ÁÑÝšë*„B!„8Qø|z ø-JkÃîp ë^œ>×EæÑËé3xeË‚â SÕXîuò^^$^GNgà^´ÅÓvM3pÑ)øWohhh•÷ù-**bÿþhšŠÓå*w1+Ó´ðy½†IRRcBBBJÎñ-¥o•dž¾Ó4±ÛíÁ)×2UU!„B!þÎt=üÚl¶šS—át:ñûý¸ÝúØ 8-ÄMk›N„j’oªl÷Û™ëa‰Nhˆ […àº$¶,t݇×ã%<<œðˆ0\.Š¢bY&‡‚üB p:\Øö’`µì}€ËæIXxX ضP,|>……äææáp:°©ºa@W…B!„Bñ÷á÷û‹ƒßºÏÙ´Ùm¨ª ¿®£ë:¦i`šª˜fj·Û±ÛíU—Å«e¦*ûü:†îœã J5UC³ÛpØì(ªJqVo‡T’eàÓýøupªsà Ѫªb³iØvLÃÄ0%ðB!„Bˆ ¿ß¨ý¯*Ò}~Å@Ól„„„”¹ÀTíܪª‚¥¨8U'–½øÞÀVI^ÅÀ*{^pUù˜¦†Ó¡á´ÛK&=f@›&è¾²ç'×ý¿B!„B!þšj}«£ÊîÃëC¯úšWB!„B!Äq<ÙW†@…B!„Bü}Õ{Ú³B!„BñWqTÓž…B!„Bˆ¿À?Ì:ÞõB!„B!މ‘#ÏD=Þ•B!„B!Ž5 ~…B!„BüíIð+„B!„âoO‚_!„B!„{ü !„B!„øÛ“àW!„B!Äßž¿B!„B!þö$øB!„Bñ·'Á¯B!„Bˆ¿=[]ÈÎ:LúžÝ`𿱍“„‡GЬEKbbãŽwU„B!„qu ~³³³mËf:wëI\|ªª¢«š‰4Ã4É<|ˆu«WЦ]{bbãw•„B!„au ~wïÜIçn=Ih”ˆeYDZª™øGS…„F‰¤víÁö­%øB!„B•:¿……ÄÅÅcÉtgñ'°,‹øøÖ¬\v¼«"„B!„ø‹«ó9¿ª¦É¹¾âO£¨ªô7!„B!ÄQ«sð‹eBü EÎ*B!„B½:¿ gúŠ?‹ô4!„B!DC¨ûÈ/HD"„B!„â/¥Á¯U¯‘_+ ßøõ¢û×ÎY÷b#}ÿ\¦ÌÈâ¤1çÑ.T¦áþ¹ä—!„B!ÄÑSëº@ñ)¿u}˜žý¬Y±‘}Ef½óhˆ‡é=ÌæeËØ’[ûzøöÌáë–r@?~õþ»<ôì4V,^O¦¿öË!„B!ÄѪsð Ô?ò9šeèáß;“WŸ}›¹ëÉåz×ø0)Xñ*—ÅÍŸî@¯ð¾oû$î¸h£FŽãÆ^dÒܸÍ@ïÆ·¸rÔõ|´Ý –…yè'î5Š?ÜŒ§’òŠÖ½Íµ£FñÈ‚<¬?­ýýdý>g^ûž=^‰|…B!„ž?oÚóQ.ßPJË®}=ŠÓ³š‡X0uE¡6 g~ɪ³ï¦wDéôjÓ—K®Fßëïæ_ÍT¼y‡Ù¾d:SÞ¼ŸuÙ¯ò̹ÉÖÊŸ·\àðŒ™=âiF&j¥åù÷1ëßÉÂçcŽÆŸ¡~­/„B!„G«·:¢šxÄ$oÝ7ü÷ýïXº·”’ÞÆÓ·žDDÙe, ïÎi<ñðèç<ÍãÖòÈ]S‰¿÷]éXù‹yæú—É÷¿{Ž9çÝ;:ã|Û?äÖ—rêKo0¶¥(bÅ‹×óì¡1Lx~$1;?ãñg§³5Ï„’Üûl®»ù:G¨Áºg3óÁ˘ @ ®šðg5Ò° ¶0ãÙöû6r …°æƒ¹ý‰èÀV>¾ç ÞÈvƒ=ŽNCÆqóåýil®“~ˆÅSÞå“Y«ØïV‰hy £®»Ž³Ú…¡Ù,ûì¿L^°]Y^ Œn7½ÌÃãÑßΙ¶-‘ÑŽbûó˜ºà=†7* H- ­;Ð1Å@÷Þ]‰=p#fÍcïˆKil•Ik?ÿ ù„Ð8t+_}µ‘A7v&pº²Eþª/øfg8‰Î bXÊ2²Xñå{Lœ¹ŒôBkzg]q£ºÇ úÎO¸ý¾yô~î-®iãäõûc\õºÆ?ÁI¡:{|‹7¦¯aOF: EµfàÅ7qíиŠãyßžwaàÿñçðÒ›ãhUÜŽ•õ7!„B!„8J {«#ÏF&¾ò9›:]ÁC7w —É_ B«Üè©ÿð|þýôd²úßÃs¶!ÜÔè7™_—ìÂÝ7•À»wiþDÎè܆Î[C™±m=õTšÛ ²7¯'‹C¬KËÃh‹êÛǪí^Ö‰ÍBïÅy7¦‚¾Ÿ¿ý¯LêÄnî (ÃéwÛÃ\Њ“Ø8KOç»ñ1é`gοáºÄAn–B³âø+š®ç]AÿdEÛáãɯóR\ ž?/›UĆã¥y œwýãô‰ËgåÔÿòѳïÒä­;èéÌeË¢•hrwÝÔ‘H³«E*–UÈúé¿’—z%Ã:D·Ó¢xdúÏì2†–öÒ6§¤õ‚Ï!Nün|–U!‰//Ÿ½£¯´óÁÛŸ1wÔS o¤/¿|öê©wpÁá7ù ³?vËÍæOãÙïaÀ¸»¹²ìšó “Æ?ŽçÙ×ÖuÄèrÙúþg³m5iÞ®\uß`’mìœÿ9Ÿ¼û¡­^çª6¶ÀÚh¸îÉ«hïÅM[Õ£Àû !„B!BÃÞêHÏ'³"[w¥KÛfØiMÛ`z3¸Œ™³’ÿ½;uínà¹ëú£j'÷Žaú¢…ìñ¤ÒÎéçðúMäGv¦{£p’û¶Ã¾`%[rGÓ<¶mËöÒ—ìÀ}z,®ÃëY›Iמ±[@d;úôÖ©Msìë~ãÑÍÉÔ;ÒȰ٤9-š;JªîÙø _o‹äŒ§îfL‡r«Ud$УÿÉôŒT K Êšå¼¾p¹g'›¿‚)¿d“zÓxÆôAR®ÏbÉm“™½ÍCÏN|B›÷ w·Ö””j™½‚o—Xôº·ÑŠ‹ˆáÃHúi&ßo:Ÿ[;‡”io ç£{ \cuz’íe¶‰X&…™EX!MIì5œ‘ ÷3mF§_Ùcã·LOoÎy÷t¥ÑNôC9xMpå­dỄ$]ü*7ŸÝ;Эcž´{™öÅ*Î}ødB*Œ.—ë e_‹H¡GÏn4Ñ [‡Ò–?ÃòeÛ:9F #±Y Z”mf‰r…B!„ÇPÞó–Êè³[òÔä{¹mõé 9‚¡½›¦{Y6áuÈÿÝ2ˆD{qNZ :™ØY‹˜·{mÛ±eåABS¯¢¹\íÐF{‡E[ Üm‹vÇ3üâ$L_Ì.owoZ¾°®\ÙÜ…‰{×>ø=K¶î'G·¢x!Ö‹n•­{ÙÿûÉܲB{ ½[¸ŽX¿²c›€â$!)ÖdQ`X„elb¯aóÖ \ôV…ÎrcV–‹¾gcX?žè XhMNedÛ/™4c-—§žD„RÜvYÌ|ôªàTm€0R]ÇWvÄ……·Â¬;·œ¸œM9ãÂ.|ÿÞ—,?÷fÉž,Ú-Ø£ó(RB÷&&>óóÍãG´ö IDATõ÷BË­S_äÝ]ÕWݪӕ…T› –”µ°¡Ï­qIŠ£\:gl$ ù•g£ïcÞ;0²wð踟˿§Î`yv/Å_;‚Sn¼“‘ÍBp†F‘G˜­ª{›¸s=à áªÄœ4šÓ>y‚Ï'N·)†aÏw#Bìp'–7YÛÕÖP0Ð:´•¢¢©`šÄ !„B!ŽŸz¿U\Å4Â[ôåüÛú0¸ï«Üþò æ¤eL€Ä7sM߉<üÒk<ÿ O]Ô†°7cȈ|÷ÕL–wô³IIåÖaÒÔz Náý‰³˜íÚƒÑñfšÅ&3 e>_ü<›[ìt»»-¡€‘—ÆÖ\;]oÅi]B³± ‚Á¯bÁŽ—|oÙˆÏFLëæ¸ôÍ,Ûå¦{…iÏ5q$´#Iù™{ÚGÅzåËùvÿÊìý1 ¹ë†•¹³U¸Žžù”™‹1pDbq)ĶlGû”#r¯„‰7ߎP àjÃYÿjÆO“£v¸3[ØG¨<ùxLp4êH3í'Ö¯9„Þ60í=ƒ5ò°5ïH#;háñ„“Gú¾"¬öÎ{‘Tl¡vÐ qû-jîKÔ2B!„BT¯~÷ù­ŠžÎÜïeÙ†mlߺžUëà!”˜Ð2Å(*1']ÃÃãÚ²÷«W˜¸&?8±U#±ÿ9tÕÿàí–Aê`:„>*1=†ÑÞ·Š/ç{è<¨5!j4]·"kÎ׬·õdhû@ ¬E¶ E˜Îºo¾aÞš-lOÛÎÎÃÞ’âm±mhZÄÒÏ¿aÁê5,û+«3 B;œÏYM³ùéÅ—™ôóV¯]Íâ¹óÙœWó°¨݋уcÉøö^™ü+K׬cõâ_™ñóV «ðô°í§ùdÅäŒÞmIII)y´î<áTvüô;üõØ–Ÿ¢"ìÁàƧ_ÉE§Î¥ãN!NPp„ÚÁùU"{pÑðDö~ñ"ÿ¾˜5kñÝ^dêþÆŒ¸¨; ¨Ñ]8­µÂæIÿá‹ß–±zÍjVlÉ©CÅ›µ'ÎÚÈÔ)sX¹z9ó^ÌÁ*~B!„Bˆ†Rç‘_¥š8«(ƒ-ó?cöÄ|LÀ׎Á7^ÏÐFd–Mé ÙÈÛ¹få]¼3á3ú½v=]”˜ÞŒã?ù0"•2·ºEéÅÈîN6nîÅж‘ÙènÃè mb߀á´ & éÄ•÷^Œçƒïyë™ouvF‘ܽ.IeìMÃ9ôþ·¼9~Jd[Î}°?ÝâZ2úñÇ ùhÓÿ÷ Ó °Çuå’Ö'Ó¬ÆF £ËÕOqôG|þÓû¼ôZÍû]IßÁm «¬­ 7óÓâ|â‡ö§iÅÛü(ÑtÚíß¿ò[úÙŒ®©ü#2÷Rä-Ò…lC%¼£oéT&‘Š=ÔþB u BBh?öqt}À¤¯^eA„5íÍ…]Ëm]ñW-‘a÷ÜÍáÿLäûÿ¼Ä×€G³n‰­å‚í-Ïç–Q»ùÏwïðÜ,m>”;OîC¢½òŽU]B!„BˆÚRôïkýðìZ%^8ÿ7†? ŸÏwŒªcrxöãÜùu[ž|õrZ;Q1â/ÃápðË3è7pÐñ®ŠB!„â/jäÈ3Å9¿ugäï&-ÃÀÌXÄ'÷Òåú»i%¯äœ_!„B!DChØ«=׋EÁºÏx浕¸]Iô¹ð!nêÓÀ'# !„B!„ø'kÐs~.ºèâJ_Ÿ2勪r$ê”ùø”ºÖDüÈ9¿B!„Bˆ†ÐàÓž«r…¨‰~…B!„GOf !„B!„øÛS5­–÷¨B!„B!þ‚4M«{ð+ç`Š?“ô7!„B!ÄѪWð+ç`Š?—ô7!„B!ÄÑÑ4 ›M¦=‹¿€…ó;ÞUB!„BœÂÃÂiÙ¦qqñ躯VËØ4 [ݧ=+(2'þ$^‡3Ï:÷xWC!„BqÈÏË#óð!6­_C‡Ô.DFFÕj9MÓ°i¶zÜíHˆ?‘Çí>ê<23±kG…ù˜¦Ùµúg  eë¶ÄÅÅïª!„Bˆ ›ÍFl\<ОiÛèÚ½W­–Ól¶ºü ñW“™yˆÍÖѹ[OââPUUæ.Ôƒašd>ĺÕ+hß©3qq Ç»JB!„âFQ;‘Öz99çWü#ìØ¶•ÎÝz’Ð(˲ã]©¿ EQHh”Hj×lߺI‚_!„Bqœ(¨ªZ§õ:ç·X^^.ö¥ãñx°, %Ž7—+„&ÉMd»·IaaqqñX2Ýù¨Y–E||kV.“}ÀߌìCDe¤_Q³âÏImÏ;BZ}ƒß¼¼\vïL£Yó–DEÇ ªªÜ‘æ82 “ÜœlvíØŽ¢(²](m“Ý;ÓP5MÎõm JðW6Ùü}È>DTFú…5+{¼Ñ¼eŠÀBœÀêüîß›NÓf-ˆŠ‰Á¯ëò+ðq¦( Q11Ø÷¦Ó8)Y¶ ¥mbZóT±¬ÀC5E õÊ>àïCö!¢2Ò/„¨YÙãý{Ó%øâVïs~=7‘QÑøu]FÓN–eá×u¼>¯l— â6‰ŠŠ<äL߆QÜŠÒ×þ>d"*#ýBˆš•=ÞØ³kçñ®Ž¢¶£¹Õ‘ª©øýzWIÔWñ‰l—R¦ib³ÙK_Ø·AI_û{‘}ˆ¨Œô !jV|¼!³"„8±Ý­Ž‚WÍ'Ù.X%zä×8Äï}Ȳä±Ü2<‰î²ƒí(}íïI¶«¨Œô !j Ÿ!Ntõ>çÓ<äÊcÏ“6÷§å3ä¦1t ©9½l—òŠÖ¤Yüylúc1ëŒÆ´Àòn჻cI—GyýÆTj±yNH–ï0[×îDiÛ“¶‘jÍéKb_ékVþ:¦~ô޳¯ç¼–ŽšÓËv•ø«õ‹ºöûJ9¬™õ#Û“†sN÷h䯢:òã'>MÓPë{Ÿ_ëO~è{§óèµ·òú’\ÌãP~í&…éëYºî¾ÉÏ—±šE«v’gÖœþxlÓ—ÅÖU+Ù–ož°Û«DñÁ[uÿ^¦Ý=ŠQ—>ÃïÙæ‘ï·²eBBrS’ãCÑj“÷ úðïɫϾÍ܃zí–9N}­ø¡çîdõòMde^· XùßÛ¹úá¯Ø©¿º³õ;ЇQ´‡åK×’^dÖ˜¶A¶«o;f;þ·‰"ªÞGÈãø<êÓ¿¤_ã:VÜÔ¥ßWù0±tÆL~ßUtÜ¿ÇjÿhèãÒ‡Y”ÆÌ pãW0öò+¸ùÍåäYÇ{}kXï û£c]!ĉí¨îó‹Eµ¿r™Ysxâ®)8óQžÓW……‹6MäÁçæÐèæó}#j¾k‚#šÆÉMˆŽÐêÿëšYÀöyÓøæç%lHÏÃBXb[ºŸvcFt ¢æA¯¸Ù0ñEÞðßÀ[â‰<ê[AXÁXÃ"0å¬V‹Ô¢},|«˜ùõÌ_µ ·…–H‡^ƒ8ûüatŽ­ý„^}ÿÏLxu!=‘”ð2¿®7ÄöjVéj3íÙ½ù[¦ï²ª¬dÊì½ô•\fz³Uòײ'söC/rvÉkMV…õªÍÅjÚ¶E+_æ–×wÐç[x…ϹÎî¯ã‘o5.}ñIF$Öf_d½ø}^š’ÀýÿnOtñŽÅR oB’?'µü¬Ô‹Aö†_øæ»9,Ý|€BlI´ï5”K.=®£ýÐW±~G¡töC-§­Öb»¹ë™ñÉ~^±‹\?àŠ¥eê Æ^s6íœ.âš$Ó$.Õ²ªÞGÔQƒ§ü#Eÿª©_Xy,~í!&¬. ¾ àŒiJÛ.}à®ñëðäóŒ©Õȸm_¾Á§k›1êæ1tŠŸ£)!Çt[U¬·R~tL«zB´ƒ¢:G5í™’ßA+gä$ÈüéSæ{˜a eÊñà×Oç„eb^ãt"-¾?7=Ñ?ø¬{#›¥ï?Í¿æÑ¤÷\<²q6/‡wmdKž‰¦4Ä^Ë*ó·!ó«øÿêT¿]À½ý[žÿ-;"SvÎÕ´kdǽoóf~Á‹KÖ2î‰;ÖÄ^MUÕµ´Ü£Þ^ Æ,­B¿ ä°ìËxûÜÊýI“yòûilq ‹ç3Wܼú.>»ó^~ëõo]Ýž=óøìS™½î ^ÀLë>—pçÕ'Ëa~ïu>_¶“ý¹^@!¼y_λîÎí8pò¦Máå ¿²yo&E’Lß UÆ~]¼‰ JT›AŒ»ýZo#ý‹§¼Ë'³V±ß­ÑòF]wgµ CAgïoñÆô5ìÉ(@´¨Ö ¼ø&®Ú—R¼^ÙÌ|ð2fЂ«&¼ÀYªøT–´cM}ͤ #ƒBþøtguº€æeº•™µˆÏfìbÈ,òuýõ©LÙŠ‹¶£੊¯7(?æü›'>ZƒÙòμìšG«LcãÎ,µ¡Ë>ëQ›øny˜kúÄÖq»Ô¡ŽöF}ó©t¹Êúî±Þ¿ÕWmCê8fid²iCQ'߈>m( —O”ˆ¯Šõ®°?:¶äJèBœèÁ¯­~‡f`0²Jþ‚Cäã¢QHß~»…þWw$D°(Xû ßï#ÁYHaf!~ Tüìÿé ^ürY>ÀO§!c¸ntb503çðÄ=ŸßÜžjcÿ/ïñö¬õ¤*Äh‘-9åü«7¨G¾Xä.ŸÄ{ óépÙSÜ3,©tÇÝgg§2}\ò9—õ‡¼h‘-è3r,ãÎlC˜™,šø6_¯ÜÍÁ< Ö´g]q%#Û†QGl{—[¯|€¾÷óúMmÉ[ð)ÿûaÛöå¡Ñ}îäÙ›ºR°…YŸ|ÆŒ¥»È74JÄ…—_@ŸDG°æÁºÕÐÞµÝ.øv3ýíoÙ;ŒŸ¸”ŽaÁÆêÕ—ý;ñŸGßæ“÷æÑíá!$h~öÏþ€÷f­cWF~À•ОSκ”‹OkAˆR\V??y?ÐŒË^z‚¡êÜ2ÛËX¸wÏãó‰ß²`[6~{,mûŸËå— ¤¹K©UÛY+ùòƒÉü²þ>TÂZ á·ÆÐ®š‘³t²ÆQMãÀ<¦­`ÈøÞtŒrÓaú‡L[yúEÑ·Ê*”Ž’?÷gÎåÕ‡'°6iãîéC²«M_M`êšÝ˜}‰¡€]k7“Õìî¾µ5.Ï>–~5‘Ož×h>ávz„)ø³6²j·Æ·<ÌÉqûÿø”÷¦¾Ïº®ç1î¶QÄùÓøñýÉLøw ž>ƒµˆ ?ÆKó8ïúÇé—ÏÊ©ÿå£gߥÉ[wÐ3Ü gÛjÒ¼]¹ê¾Á$Û Ø9ÿs>y÷9B[½ÎUmœÁµ§ßmsA ;(NbãÔ*Û«øÕû~ 2òÁGÄÁŸ˜ºbwõ)nK/Ûg|Í{#Bü9d˜%y™ùµøLøVðâ WˆÎSÏŸ‡ç£{xvÇH^|z$‰`d³ú»I|ñóJöAXRθôrÎî8wÏ*XÇä Ÿ±xÇr<à"±ËP.»æ|ºEy jšÇ»× ô¾çn:™Ø’]g_N+n«†~Ž¿Æ}Wåëw1Iâýf³nGh‰Cxøé±´±W¿ß*»dâÞÃò‰ç\ËØáÅ3#zqrqú¾|è1æw{Œ—ƶB©bqF‚Š‘·'óã²t ¡I÷3{Õ9tŽ:²ýú;¥æí_ýòVa³?ŸÌŒ?¶‘åÅCr›SwÓh:„)`yÙûû>þf>›3u´èÖ u— HÆ©ûÂëI?è ö„Î Þ ÷òy,Ùx€B+”¦}Î㺫‡Ò²ø‹Í˜åßLä‹Ùk9èQ‰h~g_1Ž3Z‡¡EÿjQÃï5ö‹â÷šҶ] sN9ý4æ¾ùÿ{÷:´¾‡±*`Q”6›O&}Ï¢¹Žx: ¾˜kF÷&ÞVu›ëøhüæí-ÂD%¢yνær†¶AÁËæ÷J÷ñ•ôûšú›åÛÏÂ/>æ«y›ÉÔ5¢[´ÂV¶`åWÿXìßLÿò÷LIà¾7o£³ ÀÃú·îäÅŒ yåñ!ÄSßãŽåfK”^»!ððíœÆ›ïÏgÛþ,Ü`¥ýiqíÅ}idL·¹¿ŽçÚ_Ë6¾`<ãÏi‚^Ã>/£’ãŸñã4¾ûOÙÏ_-úäÔØ,X°ŠÙ:¶Øö {õŒ 쳋ªÛþU¬÷uá|_fä¨qÔ¢-ª`Öbÿ*„8¾4[=ïó ø«ú¤[èy¹è¶¶œ7ÆÎ¤¿ä÷³fh‚ þýü6u)Z¿8;ó]>É,Ä´,@%ªÃ.»õ_Ä„™d®ý¦½ÍG)/qWïˆ {j?y;Ö³ÓÛ‰±· ¤‰VÈ®?¾fÊǯÚüY.K©0…ÇÊaͬUxb†rÉ©MpTZo‹üUòô—3èRî<)Ï–_øôóçxÉ|šGF4Áf’¾a9Ép˵­py÷³â»/˜òšF³¯§kXðÛ±é­Ó‹ G6aÙÔÙ,Î8•56ÈK[ËvO'.»uMlE¤¯ø‘¯>z†tßÓ<8,1¸|}®¿›sšÚ@u£@NùoV#s>o=óÛZçŠ;;•·–ï?ýñYÏÝÑØšÚ6´€•þ—2Næª{o …ÓÍáLFöÚ¥Öƒç>vÎú=Iù§…MíÃ9=?æ¥i Éè3"LUÌ«Üs=³¦²ÂèÆ\Ë€ðâøÃÅÔœòiC›u£W·Ö8èFjB+œËÂ]>zttÓÅСG7:G*tNQY5ï9öu9A'5A£3‰‡°|òröxÎ ^_Á”_²I½iƒÿ|5›ôîçÓÌföR¦Î-¤Ë˜+ ò™y^,ˉâßWÃg"¸½µö\ñàÚ:A±G‘¨Y¤•ÔËÓÃÖ/žãÕŸàä‹ofLSؽ` _¼ò<ÞGŸàâ'–'ƒMyÖu\Ý!æzfMþž7ßIäå{ûS.þò“ñÇÏl3“¹dtobÕÊû]ý\­iße«rýü«X¼Íà´+ï o¢ Ÿ/‚&šYã~K¡üg±F5mW[,Í£aÓÒùl4š.ѾC*ž_Õ>»›é/¾Âtu—ß{Iþmüøá¼öv/Ý;€Øríßðß)5oÿê—Wüûùñ•ñ|¾¿-#ÆÞFçyk>çY8è5é ¹Ë?`üû›i7êzîÆáÅSøß¯âh2ž±­ ô_'ÆÝqIjë¦OdƤͤ ¹ˆ«ÎlŒzðw>ûì3þÓ¤#Ïž—ŒÍr³é³çysaÿºâ~zưæÛù앉4~ñº…Õ¿õç½ä;ºB:-þÉŒG¾ã§e‡é7,ýÆÏFf¯K¸cL Zú<&~<×ÃÇóÄY‰UÖQQÓ÷¼ké‰Ý³‡“'òÉ„x:Œ¿€f¶ŠÑnÅènOõýM)`ÝÇÏóîÂN¹ðFú5µ‘µy._ï[¥ë~ ÷o%õ*~ñÔû8¤ŠmWüµ…ué*§_}½b,r·ÍåóoßæÍ˜f<1² ¶àòá}¯çž³’±£àˆN€Ã5ïó*;þqzW?7p]j8Þ=óùdòT&Åôâ‚Q71:¢ˆ3&òýÛÓî•Ûé® 8jØþ•®wFiÛYÔ¸ ”šÛ¢ÊÏ@Uo!NÁs~ë{ÖêÎ |)X!I$t°¸'˜ñóŽi…±õ~Üß”‘·v"áúá\<–…Sf]éÕ,CJópö,|Œß6dà뎭̔–’Q¼ðté–J¢©íÂÙ¹æV®:ÈE)MËïœôLÒYhÍ:’è¨¢Öæ!~³ˆÂ¶WðĸS‰SNmˆÉ}ˆçfÌ`ëàkè<Æ IîD·.­pЉöñ‡Yýäï,Þë¥K»`Þ®8’š5-s®7PßÈVtïÞ‘âS<¿`Æ®H?|ç¶u©´o®°ç¾I|½øR‡Ä×V©¶µk¿],Šî!:4 )iËR*Ñ­[Æ2ÒëX•’vîÚ½3‰tîÚ‰&Þ‡xmúLÒN½‚–€…ÈÄ$š6s””c–Û^>öüúë”Ü~Ûhz†+@Z…äÞ·¦1{ïIŒnRCÛ¶ñ’“§£Å¤Ð¹c+âThÙ†JÖáÈu®¹]÷ffÎË¥í˜þ4Ò,,Âéü¯~D>þ#s÷ áÂfeî-÷µŠŸ[…ì^Ÿ-. S4åÞ­4}ð¹-¦±‘UäÇÂqÄûØ#I‡­™ø±PQ‰HŒ@ñR ›x36±×0Èyë.z«ü*ٲܘ„V¨5`¥y ¬Í*ÄÀB­mÕµM-ùy>Ô°:Ž<›”_3}ã0nìê`ÏìïØ1ˆÇz7fñtØu¸?ø·~_Ãg"6P¢JBÓ¦4-Êð–Ù3X˜ù«ùfö!Ÿÿ4×oK•Ú!ïÎÇùþ›5Œ¸»7aX€„ŽÝèšê:ÐÔ³Žû§."ÍÝžaeWÆÇ¡´LïKJtU£âzÍý¼™Uþ+©Šõ+ž>C‡ž]èܹ˜‡ø©†ýVÛ2Û­¶Û·ÚtŽæœwë…d¼5•—ïZ@‹^<ðý]Yµh:öŽ>oEÒèåÌs; Ï£iTé^¶|ùgî­Õ÷·S¬Õ|·0¦>ȵ#Ó³ÛG6o-›+Y§cº+W^Å9Fu?)_ÿŠíxM»î]HP µê¦{x{É&r‡7&6˜ÚшäfMƒ³çtvM­å>¯Âñ™YüùëBjGtH¢hébÞWz2¨_7"h±“ߟZÌêý>z¶u@Û¿’õöÙªßñ5·E•3Ø%úâD§iGsŸ_ªû¥ØÄ“ëg8.GƒÏëÈOgõˆ±x¦-Áßã4²“î„=¹¸ ˆÒ|\ú-Ÿ}¿˜-û²ñh!Ø}`oã/÷#nIQñ¹-†fQ°!»â|ð[<ø (U×Ùs û!ñì¶D•ÌŸqѼ{Kl¿í$-Ë C|…r{t3bp“]hY§ u(ÿžŸìm»(²·¤G²³ä}-²]ÁŒ ÑÇUŸg¥jø?øžR¶NU%«¬ §MŸ–h‹ÓØ‘kÒ²òoÓòË™E¤o΄¦çÑ:´x(„§ô ‰UlJ/ÂjLõm«ÄÐûœÌú÷Ç<ðÐr ư©$:kº„Jíöó×ýÀ¢|?Þwnæ’wÊ¿;û—]œsUœÕæaâ7ÕV·3»4 «šéR6˜Fé‹b³£â ,cYX„ÐçÖǸ¤ÜŒgl$Jé7™·T4Ì£ž£UC_3=ä¹ÁïÄ×—óOšÆkÓs¨EÓçdÓöÂa4wùXï„ÿgï¾ã£¨ÖŽÿfK6½w  ôÞ$A@P‹¢Ѝ׮ˆ]¯åª¯Š J±  E@ì PQ)^¤ˆ ôÞé=ÙÝ™yÿØ”M²i„ö|5ŸÝÙ™3sž™gΙ39©9hzEö‰ÀÒ÷ §×mçpJõ£{»` „Œ¡thã×;oíW‰ùñ‰Ƥ¦–£g±štDJßwô ÄycKÉò;v•»Ï;¿^ãVŒ¡œù¹Z@™Ó)xFç¡7·ç~ûuË^[ͪãxô¾¡4uî%QZa'åÈ)²9ËŠ'ïdE‘ù‡Ÿ¡‚¯óQü|§”¨{ŸWI9t”LSszF{RbÈÈ=Ͼs}ê-îÝRômãù —umôŇ,’²ò¿O,øºÃñtrU°?ÈYU%õý‡™ü~Ñyš’rÑuªÇW©*öÝRâßùÍMËæÌádH^Ë w­-:O<vð/¥ŒZÆ!~\þ)?ï:A|††ÅCE¥ V›^Ê÷Uþ¿Ë7[öaât?z¶ ÀXì»Èå:]Êã[Yç?Tâ<ÄYYûhÞq$8Âö¤¥âH~K”çy.—ïN`€N%“¥‚¯Ñ±?x“KzŽc½Êfî¹ô IDAT­ÿ2רP—¿-J=í¨êwª¢ºUiÀ+M£ŒU²ÓrÀÍ3 ~]®åò¯³jÅ lýô\{¼QHñtCÏM#GÕ±ùŽ·ÞYƒ¡ÿDî¹¥9þœåçyï±â£,êEzë£ +  k.Fe4úÑ$Ô³ˆ·u¤‰«ÆnçG¸8}¾`ÄHçrè…Ó芭Èh.Ê\¢¼N×uGr.6üÏWt´ç²ëÜCáÃÿØw2‹‘áÅGDUI>|ŒLi`D×Uå†ü–hMÓJQ³h}9­³‹më¼=Kß¶ ¾]&ñò›±lY·šïV¾ÅOßöàÎgï¤Oé—aÑ ÆŸ(#IÖRøó»¿°·¾™ç'·sz^¯³«g0kÃÞØ’ù»Šæ¢ýCñ$²©/lþ“£YýèâU^R~q¸…´"Rù‘c'Bû7¦ä˜.’ßb³frIÏ­è`Žu+/ÖP³IÍ7/7ÀƒÖ#†úÜ·|²Â‹]Æ^<ÜÓÉx¹éä¦ç8ê¹û„ë˜,özá'ŠìG…wO?¦äýeÈ¿¡[7þQ°ó8ÇSUZ»ˆ¹ÊÄy‘ò»>v]¿üùè%¶óû¥· çWþA¤Üzͧ¸Ö~ãÚ÷gÄÞåüwúJ>\ß•gÝ—q¹|Ç6FiÁħ'ÑÖùfDÅŒoˆ¡Xªû;¥dýÛN–õy Õ®L_"®tÇ4ºïç¡Á¡E.È<‚°¸ªk£#jÁ±UÇ`6‚®¡ćÝïxÔq~á±ø€n»àø*O¹qQJüØã÷q M0åOq-ß݃"·w›¼6餹*£žÎŸ Þ䓃1Œ¾å!:G¸“µ{o®´–X¦s¬åÿ»Üx;áøfËßÎŽ;¯[±ã~5ß0( ÙQµÂíY¸‰«rR¼ºŠÛ”8¾èWI Ê^|¹~Ì+yŽP몣 €!oÐ5t-­‚õ_l½Kl¯ |Ç”·-JÙ¨šŒw%D­W¥çüjšVÆJvz.˜Ý1êš¹)WäÜÛIi1‚A‘F4MÇän†Üt²íYgöG3®º¶í›5¢QTS"<(Ç<óòyËÈ? .3ï}]/Y|é8 %†„_øìxl®Êl ¡M8œÛµŸ{þëYßq»gѾŠc¹w"R¸\ ^“Źä¨Eç_²¼üšEáa;ÆŽ“Ù¯Û“÷³ëFð‹ŠÄ¢Çq$Û—°ðpÂó‚ð4?ŽWÿwJñú/ûó ¾Ã0[ó÷™§ÏçÕ­®¡™‚ˆ …ä#ñ¸…8­_x8¡~æ2c­ðµ"qM¸’ÍñS:ÎÛ,<” ¥³+_!.ò¢"¯çžeÊŸˆ7µáÊ. »ÞÌ“âVd»„û`ÒK)£5'sñíy5ú·$*²1ÑM0;•­èº;Ç}ùñf Œ!Ò˜ÆîgÉ)¶Nºîb}«åø?ŒöxŽ%[‹îûzñí\Úweéç!®Ž'±Vâø¢œè¥-÷‚y®÷¿Óåg™š^ú/ãü« Ü•9ï*{[”µ¯!j¯*>çWC×KÙÑu+Ù9*˜Ý1£¡ë !}Ç1êÔfL»á¯8®j›<'*9š†[H4AüÄšoÖã}Y| ÉœËÐò® kNËUJ\]GÏïî“7}±"\6‘q¼Âò_àÕýÃèß¡~&éçq0%†íÈe×öâÛ·—1{™QÝ‚É9ô +ÖgÐlìpš»ièö‚o‹‚eù­»Ò: >]Ë¿Ò7ØJ|f½{—,/`i9œ«¢¶òù;ïá;n--ñlýj%G¼{3¥gŠ®côôÁÂ~þØrˆ˜Øæø›ÊiQ,«^̹úŽ«Øóú¼öBWéILˆÙq{Ù¸z=m­¸áÖXB NÛñìF¾üÞƒîMý°[ϧ¿¥>bÑnFÿ¦Dyd³ã«ïØlo…[Z2nmzÓ¶H}¹ÑxàHÚý²˜æ}ÆMÃÛá“¶›–ï„“iD×ÊÙ¶Z&G7üÎÙ€¦Dø(d?L¢f$ÌÇäXFÛ=Þ]S9¿ùGÛ0¥ƒo‰öa·&±ô úžµ«w‘Ö­7ýÇ_ÅóVóÑ—ÑyRÑa¦ÍQ£yú ;ï-]Ë;/¯BõiD37@18–_‰V– S¼èxÛ‹<î¿åk?à/T0úÕ÷V.»"¯ŠìÞí™xÏpâ?øŠÙ/‰âè'/§sëlËòbMÍ!Ó &÷¼:R|é:v ±?gÒ#6£®¡£`±вұj~Ø'<"› ¯ãë¯6âÖŇÌ•}ÛÙÇðjÏèAÁ¼úå\>4ÿ‹ËéœØô9_ŸaØmíð¢ø1Åñïü+þ8í§ùŒáƒ˜Žù”vŒh3œ!òÝ;sñ3˜aì)q$ùvg`{¿¢·TûwJÉú/ûó:î­†r¹ÿtÖÎ_ˆïu—ížÊÞuÇÐvÌÓLï‘]øîý¼µ ‡k.k†Ÿ!‹ø3¹Ä êK¤Y+ å(ˆÏüc›£Løtäê~þ¼¹zó ×2 UæœDÎdDryl4žJÉyV4¾BÊþ£¼ý=ÿ½ŒìÛ㻚CjÜvlø…q¾ô¹s—€®irÅPZþ¶’g/&eX{édœ?¡ýz„˜\—±O š…›X»í;Ö¶BÛ`l§S±ì¯E×½DÜ—oÞí}E¯~7“¹êhµ Æœuˆ¸Çö/q^Q-Ç7 ÷è>´wû€ï>Z…÷ð„˜³9`-,ƒ~¡ç!ŠÜº£9žhÅþV(|lOþº—\.ËÿnwuÌ£0Ö ãªh¬—ØŒ~åÔ¿Ñõz÷ H¹Ë?ïªÀ¶(µ9]’_!j»ª=ꨬ«\šãKÁàmFÑó®¢y´d䤖­|&‹ìYdæª çîRXüý'ÌúÍ1_³W0-BÝ]M´Âƒ‘¦ ®Ꚇ¦Å¯:—XÛP®¸ÿY‚ú†Õ›Öðñ¦tÀÍ¿­ºG‘«êu˜À“wx³äËϘû›ƒOz\7•›®ÅPìʶž· ½ÈW…àË'rýÁøú“ylÆÐ7ѾGPÉòÂúàT Ë?eõ‚Ù|¥»Òvÿ¾kí=WéÝšdtçS|¾êsvwy˜>~e'¿¹úh‰¾šÇÿÛ˜5ßüÈï_}Ĺ ¸Ó¢ó(5˜Ƽyäuï1IØò9ï|î¡t¼ê^nÙÈq…Ô-†±· $á“5¼?k5ŠO4ÃîëNk_çúÒ  7w?¦²ò“ïYÆò÷ c£aLqš…k3ëWðhÔ;»¶Á­È>æFô˜‡yÀò «¾}—?²Á3¢×Ü#š™ó®Þ‹Q(vÅ¿ø ¹ÑìšGøoä¾úq3_ô+V@q$ªmo:檈s½Üc—Ùåúu¢y‘–‡Âã€W9Ç-Ý«=cF¶æ½ÕËùºkîëàQ|Å*U¯šÝ€¯Ûy6}³™o2TÀ€ghKúßtÿêꮦÝ—K9F´õkÌ5?„ûÊ/øiå»üdÅ#”NW·¡_[Ÿ¢ã6TûwJÉú/ïóº[4×M½eÙ÷|ÿÞ6²?7PòêRÁ·Û­“ްµ¤×ÍÏpë ú“ø:“c@ý$õZAö$vnü›œ€0½MäžßÃOŸŸÀ£ãp¢Ì¥·ÕUB”Mö!j?£Ñ„©ª^‰Úåb'¿Eº!Õáä·ô{~ÆsÙg¸ˆ•+W”ù¾9êZžšym…ËVW)ÝÖäPI½V-™#®cã±$²TÀ=ˆ˜î72et;<ó»f×#B”Mö!j¿*xåüØŠ’îð¡JÍnîì™VQÔE¬—¹³gÒbâ+Ìu̸ôAj³‚B—ý–—Ø gyÛ²œX«o*²ïÔ‹ãX«× uÿ£Åê:'‘ƒ›–3mÓr žÄ‚3‰‹zÅùxVïbµ¦Èþ!D­WÅçü–}xöÌ7+=?Qu³^êCÔ‡u¨­ZKPEöú°=Z½^¨ò⡾mC‰‹úÅ9~¥^/ÙŽBÔ~UL~õ²{ j„ÔKQZ}ë{X‹H¬ÕOR¯Â‰ !Ê&çBÔ~F£“©J:’½¶‘z)ª¢÷üŠŠ+zϯÄZ}#õ*\‘¸¢lÒò+Díg2U¡åW×5tÙÑk©—¢ [*$û½xÛRb­~’z®H\Q6é!Dí—×í¹ò:JIË %í@5IT•Ô‹¸Töí—X«ä"\‘¸¢b~[·¶¦‹ D½æíåM³–­ ®ôgFö_Õʰ‘£.è£BÔEZ/ŠÜœÙÿ…B!Ä%•ž–FbB<ûvï¢MûŽxzxVz–üæÙ´~]U>.„B!„¢éÞ³OÁ¿m6+º®Ϲ³§ÈÉÎÀd6áíåCHX(~þ(ŠBPp0КcGÑ®}§J/»JÉ/@§.Ý«: !„B!„õÜ®¿¶—xÍn·³o÷ßdegÑ4º!Á¡¸Y,X­VR’“8{ê4©©©4‰j†Á`ÀÇÇ—¬ÌÌ Z¾¡ª+ „B!„BT–®ëìÝý7š¦Ñ»O,GaqwGQ, aá´ïÜMÕ8s꺮c0.x„uI~…B!„B\rçâΞžFçn=1š\wJ6´ˆiMvV&iUZž$¿B!„B!.¹Ó'OÝ"S)‰o>£ÑHHx$I‰IUZž$¿B!„B!.¹ŒôtBCÂ*4­Ÿ¯?¹9ÙUZž$¿B!„B!.9US±¸»—xÝn·—xÍìæ†®ëUZž$¿B!„B!j„¢(EþNLLdí?’œœ\b:I~…B!„BÔy‰‰‰lÿóO|||غm[‰¸ªªüœ_!„B!„¢ª‚‚‚zå•Õ6iùB!„BQïIò+„B!„¢Þ“äW!„B!D½'÷ü !„B!„¨QGŽbߞݤ¥¥¡ª*&£?Ú¶kOÓfÍ/Ê2$ùB!„BQ#ìv;ë~ZCzz:;u&ªYs<ÜÝÉÈÈàè‘Ãlùã:Ì€AWTyY’ü !„B!„¨ë~\ÿºnƒUµc·Ûp3›ˆ‰‰¡EËüúË/¬ÿuÁÁAUZ–Üó+„B!„â’³Ùí¤¥§3tøUŒF4]E]G4]GU5úHBB™™™UZž$¿B!„B!.¹œœ\:wí†ÑèÔ!Y×ÑQÐ5 ]ÕÐ5 MÓhݦ5I‰‰UZž$¿B!„B!.9›ÕF“¨(tMË{E/ò[t]GµÛiMzzF•–'ɯB!„BˆKNÓu||| þVÅÑ홼Ľ v÷ôĮګ´º-¿r™A!ª‘®ëdddTÓE¢\aá‘dd¤—yq]bZÔ%‰iqaÒRRlTâõ"Ý™+” çuuÍ›¾èOÞ[”ÒjëôYןwõ£¡k:š¦£åÿ÷š®éhºã¹²Žg˪hš†¦:ž3«jj±×TTMEUUÇߪóߎߪjÏ{Ý1ü›ÍJ»9~äHÕ*¢žPòîù-G8×½´üŠzfü„‰5]!.Ȳ¥KÊ|_º…ŠºÀ`0Tøþ]‰iQT&¦Eå¨ªŠ§§W^«o¡ü¿‹¾^Ñ–ÞâÊjù-ùbéóÏOŽïË-|Š'íùŸÊO°)¸g·hËoáûÎóÕÑQt§ÖJ§u7  8¶O Ù9Ù•Ý(õO±XQMQÐâ‹4°¢$¿¢Ö)/‰¢¶‘‹6B!]×K$¾•øô&Õ˜?ÎÝž‹/ϹÛse’ßâƒcv‘.Hx &×ÏéÉïÿìÚü.к.÷úæsè*€0ò·¡‚¢Ãź„%[\gºŒ” lÕw4®TÒOæxú%¸wJÍ"%%·¿…BQ³.´Ûó…-§bË(Ú¥º"Ý©‹N§•=½œÙ¸¤“×ýYQ ïâJþE‰ ¿ÐR”$¿B8ÉÞ5›‡^üž³öš.I>»ÕVä`œºy:w?¼˜ÃÖ*’í,kßz•G¬@{æ?À½sw‘uÑã6Ž.ÿ/:Hn™ÓßFB!„¸TŠ$€îT6 uNh/î2ÊN’KLKÉ丠X/liéö\Ðܹû¼¢ \¤3=I~Eƒ¢¦üÃWsžåÞI?awÿg_þLíÿQ'cË«Üý¯$PÁ£Ée Ø™ ãÅ^^6½~÷|„ŠçÕfB»fpÜjd0WÛHÔ[öx6ΘqCb‰Êø'²=Yîå«5¤~„hÐ.VËÜÅ‘wçm‰„´êó,3IvžVw ªUÐyº !.láyŠ_pÞ6ºóûUO€%ù †ž±›EÏ¿Æ7©˜ðô4Þ|íi&vHåÛ×^`ÑîÌZÙr¨«vÔbskËõ×vÂÿ¢'¿ÂHpÏÑŒíRc¸ÚF¢>²rxñ£<µôín‘מOãÝ xôé/8]kzj4dR?B4dÎÝSk¯ÒZh/â½ÇNóGW|`,ÇŸÒýÙ¥b÷þ¼ÌÅ«#ðJ4*q¿.ámÏ>:ŽVîŽ+Já7>BPöc¼²äW®ú¿‘øjÂf=ôgS4üb†0é¾ñô1£çã‡wæòé¶8r_:M~LJ ¤íæ‹÷òÃŽ³dûµâª;à¦n²ö³üÍùü´7žlÜi;ºñ?œã¦™OÑÇWì˜=õ}üƒÔ<3m §sÀ-¬;×?p#¢-ä_ă·,"¹aú+ =?‹5á¹×Æe¶·i)ï,ý™ƒ©&‚;ç¶»®£K€ëÑU¼>k-ûâ³Ð0Òy÷Þ7ŠÖ^¹Ò˜Ã?³bAøÓ¼~Cf=ƒÍ/Má«Ë¦ñW8OgåЇ0Ãz3ïn»Xwl-‹í›ÏcñºÃ¤™ˆðNCÍ{Œh®Ëy»ÞF£BRÙºü]–ü²‡x{FÝÃýcÚà-Y뮬øäÓ#žÇ7uÄ^‡÷øRV¼š)m-5]†MêGˆ+?ñuü®ê@X—Za²åêM…¿ ¦®ð|uòF¶*¹LŠ p,.I~EàgphûYüzý›fîNGŃý{âóë6¦ '€)œØ?Fßðv­˜É;³BiöÂp<ÿ\ʧǻòÈܱ´4¤¨`ÔRø}þ,V3–'fõ@Ùö>ÓæLû7¤‹ý<{úÌL†‡«ä’Àgëæ°ýd.}Ú»c=»ƒCJ+îŒtì]Ƀ¯\K€{:;>~™÷n¦Ïó0–¨›™öâ‚ £œó…Å·øŠéïï$掹?ÆÆŽ¥o1kN¯ÿgÞiÇ9jïÅÃ3Æe=Äw³æ2ï›®L¿±)æjÚÌæF®Öc *‹3?ÌâÝíÜúü½´w;ÃÆ…sø¡Ìy»ÚF:q?ÌbÞæn{n6m2Ö1súÛ|Úq:“cä¼®²û‹Ýéî´mŽ >í£üÊ_»“PÛFP+:C4PR?B4\.GIv•ðm¹ào§Ñ’swõËå{Eÿ锼æu7Æé^Ü‚…]&ºÅ_ËÙÙÕhÏ:º’¿8WËPPô‚µtÜ·ªêHKyý$ÝžEàe“š^A^%NÀŒÞÁx“Ij¶ãÞ4£ ÝÚGÞŠAãGyj%ªÜ=0džâD‚³_8|Mèé{øi¯ƒo¼‚˜à ZMwãA¶ž±9f®¸ˆ_@¡Ñômû¶œ";ñ;ÿ&§ešYÀàN“0?¼ýÓsH'<“OœßUP1`41 ÅímãÔ† $µ¾‘›.&$¬ƒoMø‘ulË»Vqó%8(€à¨n AêÞ“dVã±¶¬õ(·,jÛ7ž¥É˜ñ Œ #¬iºµô+8HUx© lûõ$ᣮ£oÓ@BÚgt«lþÙô¾¬»Ô´8Òð!Ü·ðš­b &ÜRϦIÝÖ0©!.ççúÿqž¦Þþ€Óù™‹÷òlÊß&ï9þ—–´üІÁà¿§BfB&*I€ÕŒx2/ü=J^ 2z‡hÊ$!SÇ»óíL¹E¯ÞÇ0á®›égL ÍÏ×OMæë‚O¹Ñ-ÛÕèK´ìß뢭œÉñaÏ–tbƶÂKQIÞ±Šù‹fϹ,0ƒê5¤]kTÒÏeã„Eq*¯1ó%CdÀÃßÅ–=¿NTîªdE×£”²¨™$d™ñtqU®ÛHÍ >ÃÆñ…S¹yaáËþáÙ¨ÈAO!„¨.ÅKS˜7”V΢'YEº€ë +<ã×ñRCÙ.µ‡œІAñ¦eÏR¿ßÀÑqÑ´Îïú¬gsxý6Ò#GãS2#TÓÎ`÷¢“·Œ~t=…×GÄñû‡/3NïòçT IDAT8­žÄÛ-’^y…QEw'=¥D!ðl=˜ö¹‹ø}—»S[1¶Jî>}ÿgó ï^Ñö½Ç£ï:¦WŒP­¥ŒFmÄ'ԃ쓉äê-±( fÄ‘¨zÓÁÇÅ–îtðuI·’™­br7¡ `ö0’“’]ñ‡Šç*e=JrY£7¡^vöŸMGíìQ´…¾Ôy»ØFF/‚½Ühuã›<ë/×Të £O¾¤—V¸7蹉Äe‚o„¯|™Õ0©!®Â¯.Ýë[}t§ü·ÈùŽó]wrvRc¤Û³h Œ„ ˜À ãO¼1}%›öàô‰½l\>éëL\1qayÙ–nM#!)…¤Ó»øfÁœk>˜nFl‰8p:•Í“ÈÆþ¬™Ø¼ÚqE«$¾[¼†¿O'‘’ÇÑ£ ä–’`*ž­¸²Kk|AR»!´ÍðIÓ4l6šÓ!ÑìÇùml=OÂ飜ÍrNEÍ4îKÀþå,ßtŒøóùyñ—œ‹L >IMþ“¯¾ßÊ¡Óg8¼å¾>ìF‹¶Á˜p#¼Cr·Ã/ûÏ‘””@RNÞJÌXŒVÎ#»Xf\ÚzTˆ1˜žCš÷Ù‡|û÷’RIH³äÇÞF¹tàØÊ¥¬ÛGRJ"§#U…T§™Ã»ÒÞ'‡Ý›Ž€NÆžß8¨…е} ÜOZä~„hتï9¿uQþ3 ïõ-2^±.Û«&ÉÅXÑ`(^í™ôÂ-^Î’W¾!U5àÛ´'×<ñ×´÷BŒ¾Mijøéÿ Xë2‚‡ïHˆQ#iÿjæÎßB‚ Æ€Ö »m(-4¹ï!âß[È[BF‚ºM湇ì²î´Úßõ¿ÓuX+<ÀÒ‚±{1óãg¸g!€íÇâ¡€¥ùÕŒëö:½0•e–¦\ýÌóŒqš›9jß™ÁÛ‹ŸeJš‘à#˜òà BŒ]m¢å¤rö«Xµ4ÕD‡«ïç–öž(€_ÏÉÜö÷,½øfß(.±`0zÑkxWÖ.y‡ïº?O—ü™•±c$ôЇx8õ}>zëqVä– Ú_ãƒÁÒ¨RÛhüUS¹7í=–¼ú( làÞž¿‹î¾r¥µÎòìÀM×G³æÃ—˜Þä~††œàË7בÓa ce ³š'õ#Dƒ&8UNA·pi¾ä”ÏV,Õ¯>²Bÿ¾áW†Åšï¾bØÈQlZ¿ŽN]ºWsEC2~ÂD–-]RÓÅ¢RÊŠÛì¬L6®ÿ•!ÃFàëç‰KVÏØÏ³ñýW˜µj;qVw÷Ï#OÜB@iW¼˜>_¹Œ׎Áfµº|¿Ô˜–úµTy1-.Ì¡ûéÚ£—ÓcŽ4JT¤öŒ«eºVø<£ü®áŠ‚¢0  <½¼ùlå2®=¶”íVÿìúk;Ý{ö)ø{éâ…L¾ýNìv;º®¡i: ÚíØívì6;v» ]×ðñõcåòeükìõlÙ¼‰^½/gËæMôX¡eÿ¸ú;iùBQG˜BéwÏLúÝSÓ.Iý!„¨åäž_!„B!„õž´üŠZgü„‰5]!„B!D=#ɯ¨Uä~_Qß(Š‚Á`@Ó*üÐ(!jLEâTbZÔ%§BgÒíY!ª‘¢(xzz‘˜˜PÓE¢\çÏÅáãë[úóÀ‘˜uKEbZÑpHò+„ÕÈh4Ý‚½ÿìäì™ÓÒ !j%MÓˆ;sšmüNë6íÑ´ÒŒ-1-ê‚ÊÄ´¢ánÏBQŒ&a¸yXøg×þ·ñ·š.’% |||èÔ¹!aaØm¶R§•˜uAebZÑpHò+„Õ(ÿ¹~!!a„…E–þD!jš𦖛$HL‹:£‚1-„h8$ùBˆK@UUTUºÝ‰úCbZ!D]#÷ü !„B!„¨÷$ùB!„BQïIò+„B!„¢Þ“äW!„B!D½'ɯB!„BˆzOF{Bˆj¦ë:v»Í12®®£ëzMIˆEÁd2c2›ËVbZÔ•‰i!Dà ɯBT#]×±YsIŒOàøñ£dff iZMKˆ¼½½‰nÞ’Ðð †Ò;†IL‹º¢¢1-„h8$ùBˆj¤ªvããÙ·/:v&0(XNÂD­£iI‰ ìþ{'n !¡a¥>ÃWbZÔ•‰i!DÃ!ɯBT#»ÍƱcGèб Í[ÆÔtq„(•@ žžìÞµ“°ðÈR‰iQWT4¦… ‡\ªBˆj¤ë:ÕtQ„(WXx$é ”>Ä´¨K*ÓBˆ†CZ~E­2~ÂÄš.‚dÙÒ%e¾/ÝBE]`0*|ÿ®Ä´¨ *ÓBˆúO’_Që”—DQÛÈE!„BˆÚO.Û ‘O·’–”†MžØQ&5+™,•2·—nË 1%—:±)õ\RS­u£¬B!„â‚Iò+ê9ÔÍÓ¹ûáŶ–=eö®9<ü«9kw=»ÕV§$õÜž¿ëY¾9ãr….Œý,«_{Ž··¦’Uêö²rxñS<³ü(ålòZ!÷ðJž~ {³êRí !„BˆÊ’äWÔ}z¼~ã'LtüÜzOÎXÁ–ó6@Á£Ée Ø™ ã/€Œ-¯r÷ ¿’Xæ@‘VŽ,™ÂøÛÞb{zÍ'RŸ.ÒÖ~¼âÅèdìZÉ7ý¸®›_½;ÄÒ|£C¶°ô—sÈ8 —€=žófÜXbc‡2þ‰…lO.ý~<-e‹ŸžÈðØXb_Ç”·×gsš@ÏåäÚÜ3f±±ý9ù>Ý›A©s,où•Ÿ¨<‰!„5D’_Qhäd¨„þ/óÞ}›Y/ÞE¬ò³¦}Áq¸5Šåúk;á_…PWí¨åä³zÖ>¾ÛŒ[îN¾ÙšTã'JŠgs†^?ŒV^)MÕRøóÛ½„^5˜(·‹3ËZÁD¯k;’ôó/œ´•?¹¨ +‡?ÊSKOÐîöyíùñ4Þ½€GŸþ‚Ó®:(¨gùú™GxoW8ãž{—îìÈ™åÿáÑ…È@'s×ÛLyék2û<ÈËÓžb¤ÇFf?ô2¿$ºÚË[~eç'*Ob@!DÍ‘¯D½at÷ÆÇÛ£w'†þ«?kžÛÍÑ ÐoòÀ¢&<÷Ú8¢Ì:¹§~ã£ù+Øp4w‚Ûgêã×ØÏ­åÿîüš «™Î£¸÷¾Q´örÌ?÷ø"¼eÉ Ó_aT„ó¶ëþrÌ¿¯ÜÍ»«ç\ÿkˆ0z{>ŸË¼¯þ!I5ÔûA^¾¿;¾Z ;?¿ßE¼ÕÈ!ñ“Û YË‚¾bËñ4,M0iʭĆ™±]Åë³Ö²/> çò)ØâÖ³`ÖÖŸÈÏ\÷ì3ŒñÝÌóS×Ðú‹ RÈ9þ3 æʦ™¸Gtgô¿ÿÍ5­½QÔ$þ÷Þt>Þr‚T+àÝŒ7?Èä~¡˜×0óOÒ§Sù×Jß^©›grÏÆ r?Zÿ7Snì‚_îßÌ|‰c_á©þ¶8-×ÐŽÁ±ÁœÜô?ö%Úði5‚û¦ÞH'_³kf3ýó]œÍ°ƒ9ŒnWöÀ}ßFþ8’ ¹nÊ\ÛÒÛ‘e<õòn.ῌ >ÍïÌåÓmqä*¾tšü]áÁ~WuÑârZf.ãÏó×Ó¬‘«MÖ?|òé‚FÏ㉛:âN¯ÃŒ{|)«^Í”˜¾xävæ$æî&úÄW,Ýi¢ÿ«Ï2¹Ÿ Ý9»“»W-açøéå™Ê–¿%>ê6Þœ:†ffè×JgÏõ¯²híiÜƱwq× OîZ0“qAå,¿Mv9ók"_šU%1 „¢I˯¨wtk{~ßA²OS"=‹µzæ`é´…lq /ÏšÃ[õC?vŒÔ¼þ®¿îÜúÂ,æ¼þ=ã¿äoNßh‰º™·.bñÇÓ¸6¢Øé–ÄŸköã;˜î±ƒˆŒû™_O9>©ž_Ï¢oÓ¹òùy|ôþLž¼®5ÞŠJü/s˜¹ÎÄUOgÎ̹wXSÌY»ùxúJâ»ßËô¹¯0©Ñ_|øî&5PÓŽsÔÞ‹‡gÌaö«÷ÒíÜÌûæ6rØ¿r;ßÎÌðÞËw1 ¬hùôì},yc)';ÜÉk³Þ`êå|9ã¶¥é çpîðY‚Æ¿ÂÜ93xnŒ/[>üˆíiE›ºmñˆs‹"Ú©uéÛË€gëëyæ­Ù¼ù䕨?½Ë'ûsPLþ4mÛ–˜Pw yË ž8yo¿ÁÔñü¸&î÷¼Êœé›¾†ÖœÂ†Fæé£¤·º‹oÏaÚÍ9øýôÁ3cö4îm’ÏýN‚ Fï&´i׊Fž é.åÓã]ydî|øösÜÜ3\Ö`‰ m@2{ÏäT)öDÙlçþbwº;íc›ã€‚O»Ä(ñüµ; ÐT;ªª¡£‘ºo;q´`@;ß¼nöî4í„gÖ^¶±BîI¶ì·Ô³yWj ]èߎo>D¦îèµaWU4½˯ÀüDÕH !„¨I’üŠzÂÆéåqëÍ™0ù!^ßÉMSo$ÆRtªœÃ?±ÙÚI7ô¡YpA!~¸9åÇ÷ GÔ¨3W 'eïɓŀÑ`Äh4”¸ßUMØÆGè{YîÁÝèß(‘ ¿#PLXôNžJAu Q„75­?!ü_7seÛp‚BÓ<ÒÛáulÓzrãÈ„DÑûšøŸØÊ‰¼œLqó%8(€à¨n AêÞ“dêÜ<äÄä\–¯ÐH‚,EK˜{ä'¶Ø{qËØî4 Ž ãÕ73ÄóoÖìIÏÄË€GP0a´8‚ŽæSì=_´°–O¶{0^NG²¶—9 B o7˜«¢s9|(»[FÜ7•Ú{çmCîøûGÐiPoBŒ5 '(¢ú“z(¡àâƒÑ+€@ÿ¢z\A'_#^šÔ˜nƒ:ãx„D;Ccù÷#“¸,ЈÁÝCæ)N$X1û…ÓÈ×äº.3'ØK#=1Kîû­FjZiøî[xqF±î ©gÓ°›1vîOü¶ì^ÚXTÒâÒÀ#˜@÷Âx6ùGâG*qivPS9›~á>½0ú`@K9O†êFÌ‹ùíçùÜe.ùåίÚ7Q½'1 „¢&Iò+ê 3a×<ÍŒ™¯ñ`__L^1tmæY,IÕÉMŽ'×7ŠàrïY5àáïbËÆ^î•~•ó[~ád@Oz…›ÁL·~HÝòGsÀÔîëOæÏsç/³dK<65ƒó&‚B½ O°ÐÉMM"+}#ÿw»cð®IO}͹œ,Ò­Å á\>7bnšÊv2{Ê]<>çižÐÉMN"×§þùç{&ûj¤&d•¼7Ùà¿E#»Ø3Œt]GW (.o!.c{)f¼}ÜP³Ë-Û`ñÆÍžE®ê˜Ÿ»·ÝfsQ> >f;™V­àsf-·Ør¼;ßÎÔ‘îüòê}Üýìüz*ÅU]äM¯(ŽuB!„õ“ܺ"ê “WAAáO˜Ä¦ÇÞgÑÿzóxl Ó f_ŒYñŽ«÷åD¿’Ÿåé ŠÑªÕu« ýüv 5þÏÜþƒã5UE'™_O¤M{wBzÞÀ=®áØOóø¿yïÒtÆíyÚ9p> ¼«P n¾þx áÉ7o¥e±=ûxiåÅ«%Ãî~‰+®ÿ‹åÓÞbæg­™qmáz[üqK?MŠ"M€=…S© ~ÁžÈ(¶Bù nÑDÐàˆ{N2Y¥Œùâ\žïUä2›‚ë¤K̯ØTŠâúsF?:ŒžÂë#âøý×™?'œÖ¯\MDñºhõb}²HÎRð ð”+‚ÕÈè†/éÄ¥îIzn"q™àá[l—4âî Ù $åèw ƒ=õ iø:Z„ûÀñ¸t vi53)ÿP¼‹ rWîò¹•šŸ¨<‰!„5IÎóD½cðïÁ„1áì]ñ5‹ÝÂéK;ëï,ùöoÎ¥&q|ßQÒ*ÐÍìÇùml=OÂ飜uÊíq›Y6„OÏ`æ[o:~f>Çu³øó×CdåÄqàð9Òsü7Â[Ï![ ¤GÿFœZµˆvŸ%)ùÇ'cl>ˆ.ö|üåvŽ'¤|þGÎf•ó|a•”Ãû9™œ…j ¡Iˆ[fn‘ÏXZ ¡—q ¶S qüóÝb~ÊêÈÐv>~d‘[H ÁÙÇ8•^…O­'ù~Λ|²;£Zž™¬ž_Ï»ÓòG’Š-ñN§’£yÙØƒ5“Ü,uaÓÀÏd_Z†»×›G8ÕFæð®´÷Éa÷¦#8vMŒ=¿qP ¡kû@ŠæüÚt'œ#¬ß—ß=?‡£w‘éÙŽ‘nàÖ„^­Ì$lÛVðè-i'ŽAÓËZR| ór—_Éù‰Ê“BQ“¤åWÔCFÂÞÀe_¿Å§[F1Õ¯ðÅ·;w<0ŒyÍdêçV¼CüÉ1¶ÄTÎ ¥ùÕŒëö:½0•e–¦\ýÌóŒoîØ8»y‰QC¹²M˜Ó³„tu¾ZüûÏ´à‡éKù'M·:º‹¾Á¼‡OåÁÔwùøÇXjSðïv'/>ÔÛ»Ž¼ÏS_e€âK›ëŸâ©Qž¥NÏåÔ/ïóÆ/qØPðiÖŸ‰“Zb!¡p½=ÚpóãX0ÿ]ÿ> KDwÆ<úozú*àêñ".(>­è¾„ÿíOgP_¿ò?નöŽïÛGb‡l´Ö4‹2©§8°÷!YvbN¬fîü-ްZ3ì¶¡DdüÁŒua$÷Ðì7uàsù γ7]Íš_bz“ûr‚/ß\GN‡)Œ±€ýt‘‘~[GbBç•̘ö*K¦Ž!:é'Þþ4ž¨›'ÒÉ ÀŸËnIÈòÂÛÜÑÇže3ùËÒÿk„ +‡œGúmRÎò nåÌOT™Ä€Bˆ¤|¶b©~åð‘šø÷ ¿2lä(Ö|÷ÃFŽbÓúutêÒ½š‹(’ñ&²lé’ê[€žCB\*oÜõtÿ8¹Çòò³C–îlåÐHÚ0'WwæÅG^_¶—žÎöYO°4âI^»!Š IËŠÛì¬L6®ÿ•!ÃFàëç_µ²Ööól|ÿf­ÚNœÕÆ}ÇóÈ·Ð#ÐèH|¾9Éc‰´”?YòÚ –lÊØ¶Þ°rhÁܹܓ»?œÅ¸&æ²—_îü†ÏW.cĵc°Y­.߯rLK ˆK¬¼˜æÐýtíÑ ]×Q]×(å&@ÏÿßÅß…o”즻úåò½¢ÿÌ›³ž¿tÝùߎ Æûpþ]ô5ÇçŸÉûw±×J.Ð5 JÉÛF( ŠbÀh0`0ðôò法˸fôØR¶[ý³ë¯ítïÙ§à屢2ùö;±Ûí躆¦iè€j·c·Û±ÛìØí6t]ÃÇו˗ñ¯±×³eó&zõ¾œ-›7Ñ7v`…–ýãêïä"¦h`¬§X3w:?Ë@ÃL`«þL¼{€$¾b °× üjËwÄ2¥‡o½è"l=öËöÇpí/(ñ•d ¥ß=3éw«÷1föÆ8½dðïÆ-¯.å–Òæ§¸5üÞþˆ‹7ÝhyûBÖÝ^Áå—;?qQH !„¨!’üІÅÒ’ /ÏgBM—£®²D3öñGHò©ø½Âµ[ãaÜÿ”‰¦þÒ¦#„BQŸIò+jñ&Öt„B!„õŒ$¿¢V©Öû}…¨Š¢`0д*Œ’-Ä%R‘8•˜u‰Ä©™ôóBˆj¤( žž^$&&”?±5ìü¹8||}]>¯;ŸÄ´¨K*ÓBˆ†C’_!„¨FF£‰èèìýg'gÏœ–VQ+išFÜ™ÓlûãwZ·i¦•þt‰iQT&¦… ‡t{Bˆjd4™‹ˆÀÍÃÂ?»vð¿¿Õt‘„(Á`0àããC§ÎÝ Ãn³•:­Ä´¨ *ÓBˆ†C’_!„¨FùÏõ #,,²ôG QÓtÐ4µÜ$AbZÔŒi!DÃ!ɯB\ªª¢ªÒíNÔÓB!ê¹çW!„B!D½'ɯB!„BˆzO’_!„B!„õž$¿B!„B!ê=I~…B!„BÔ{2Ú³BT3]×±ÛmŽ‘qu]×kºHB” ( &““Ù\î´Ó¢.¨LL !I~…¢麎͚Kb|Ç%33MÓjºXB”àííMt󖄆G`0”Þ1LbZÔi!DÃ!ɯBT#Uµ“Ͼý{éб3AÁr&jMÓHJL`÷ß;q³X +õ¾Ó¢.¨LL !I~…¢Ùm6Ž;B‡Ž]hÞ2¦¦‹#D©üñðôd÷®„…G–š(HL‹º¢¢1-„h8äR­BT#]×ÉÈÈ 0(¨¦‹"D¹ÂÂ#ÉÈH¥ôi$¦E]R‘˜B4Òò+j•ñ&Öt„¸ Ë–.)ó}é*êƒÁPáûw%¦E]P™˜BÔ’üŠZ§¼$BˆÚF.Ú!„BÔ~rÙVˆ‹B%ýôaާױû‰ô\RÓ¬”|H‰Ž5=…L»ëÇ—h9©¤äÔÒ+éz.©©®ÖI!„B4d’ü q1ØÎ²ö­×XyÄ ä°gþÜ;wYœéØ­6§îb̳ä22þ^ȳ/}É [±·2¶2íÁiü’è"ÁUϳæ¿S™¹#³V&˜¹‡WòüóKØ{ñ6”B!„¨$ù ʹ5ÿå– ?a"ã'ÞÉC/Ìãóíç°^”|ÉLh÷Á îÛ °¡“±åUî~áW ’«:OìgøqéNJóEšg-`i>‚Ñ![XúË9êX;|Ã`gãü‡7$–ØØ¡Œb!Û“KïE ¥ì`ñÓKìàë˜òözâœ/Ö蹜\;ƒ{Æ "6¶?#'¿À§{3(uŽå-¿²ó•'1 „¢†Hò+ {VJóÉÌxw>óf<Å-=a㬧ymÍŠ7‚Vž‘àž£Û;ä‚oª×U;j‘D¼êó,.÷ØOü’Ùk»ùׯ!ˆ^×v$éç_8YõÊ••Ëå©¥'hwû‹¼öüxï^À£OÁi»‹ÉÕ³|ýÌ#¼·+œqϽÆKwväÌòÿðèÂä “¹ëm¦¼ô5™}äåiO1Òc#³zÙu¯…r—_Ùù‰Ê“BQsdÀ+ÑpÝñòòÆ×Û›n#Ïë<òÑR¶÷y”Þ~ud- >øŠ-ÇÓ°4À¤)·flœßü ï,ù™ýÉ*ž-þųOõrš±•C> ë}̼»%Ikf3ýó]œÍ°ƒÁÖÃÿÍ”»ào„܃‹yfÚNç€[Xw®àFD[È=¾ˆoYDrÃôÿÒþ‡'òæÙ‹šÌŽ•óùhõn4?Z žÈ=úf¶s¶Œå²·uÖ6·ÑÌ@'cÿ7Ì÷KþØÐÓüðÎ\>ÝG®âK§É/ðØà<[\NËÌeüyþzš5’Ã\­‘õŸ|z„ Ñóx⦎x ÓÁë0ã_ʪƒW3%&/¹9I£y烻‰>ñKwšèÿê³Lîç‡B7BÎîäîUKØ9þEzy¦²åão‰º7§Ž¡™úµÒÙsý«,Z{š7…qlÁ]ܵ“»Ìd\P9Ëo“]ÎüšÈ—fUI !„¨AõªÁGˆ g °Û•´Ñ÷óÇÉ\ô¬Ý|<}%ñÝïeúÜW˜Ôè/>|w‰ØN|Íóÿ$ìúç˜9{ÏNŽ%ÜXÚ|52O%-æß¼1gÓ¿ûOïòÉþÌ®äÁWÞæ½yÓ¸=ú+n&¿÷%êfÞZ¸ˆÅOãÚçÓ-•óëf3{½;Wÿg³^¼‰ðï1ýë“ØÊY^=›SR jŽ gìä£7¿ ù²ûy}æë\“wÁADvʈè\JÆ<Ãi懷_czé„|€Î IDATgò ’ó»þ)Œ#F£"³WØöó1ÂÇLdp«0B¢/güM­H\¿S¶²—W@Ë$!SÁ'ÈðÿìÝw|ÕÚÀñßlÉ&›Í¦÷P‰D ½*E4\@¥ˆRE°"¢(¶kÁJPî¤X €(о6,ˆEŠ ÐIïÉ&[çý#$&ñ‚iÏ÷ƒfËÙ™33Ï–gΙsŠ®g·¦+wÝÚžˆÀ0ZthKPi¾­æïcÝ!onÕ—–¡4ŠiOK¿Òlß­ýœÌ-M¥uÔàîë‡O(­oèJ Ö@xÓüC¯áúkÈ=œq®[¹ÖÓ?_w¼‘Öf-žáò ý m0e&‘éÐàÓ~S&ÞH¨Nƒ›QKqÊIR-<ƒÂð7(hÜ=ОâD† ½wáfÝÙ…àé"?Ó"×ýÖ"μòð"Ä|þdŽb ĹÉy8tá û?/€ƒ“¼”<ðÀÏýü;@ç†7¹¤ä9À™Kr>x‡xqîü“ÖL˜¯WNN7¢Ç/åçç3¼±¾òõWº¼+¾‹ê=‰!„5IzïQJµå‘Y¤ÇۨŚ“…%ÿ0ÓÇn:_@‰&ßæ@›jÁ#Øïï <¥èñôrÃYdGÅIöÎO˜¿ôGö¦Z@NÏÞ• ì, ­@‡°©ôÇ™‚Á?wKùN0V¸¾²«¢ª%¯[nvS3Ìå|"¸,™¨fÂÊ{ò/h &ܬN î&ªÝ~é 1^z¹6×¹×é]\8Ë’Ñ#&3|é¼3ék|º c½ýˆj3–ÉÞçƒ×ä³ðë5á.®0   ( ªÒL#„B!JHò+N2·¯e¿¦¹ãæòÁèÛ›§Þº‡(·²ålñ7P”œM¥ÒßòhÎö·°æã…?ÂàgYpc#Øÿ./PP´pÚÊoµÔšòtp ¥g´¨X3S(26ÅKË%¯Ñ”׿CkÄÏè¢0¯ >þèóN‘e‡°‹>4ž˜•Næ8À§ÃB+Pîî¹$½¨”¢”û:Å3о_æÆa»XùúÛÌZÝ‚·F7#vÐ$ÞìŸÂ/K^aþœZ¼:P,d[<}Ò½¥Ñzc&Ÿ”¼óQªZ3I)s¨ù¢/$-æ3eU¬‚±$*¹gÈÃ\Òr§5âÇSòqRú…æÌãLŽ O¦‹.G¨týZkµ–'ªOb@!DM’ß…¢á²’““MúéCüöù\^Zt„ÈÛFÑÁ[Á½Ù ´ulâÃ5;8ž‘CvÚ ’’-¨¸Ñ£+^{—±týAR³³H>v‚J²¥ÌBµtŒkJòše¬;˜Jú±Í¬Xqˆ€݉¨jnª ‹4‘u `hÞ›®úßX’°‰#é9dedRTš¤*^-éÛÊÂw‹W³íD&9YäœÊv’¯ç¼ÅŠÄ‚+0篋œß—2cÞ$;œä9ÀÉl NC  Ø ­Ø3rðt.Å.#a>hl…Ø\€=ƒÙf¢BÜËOÀEЇ´£¥W1‰›“(¹[¥`ïÏrÒ®¥æ¼c:Böç—ÆW1G7í¡Ðx ÃÜÀ­¯Ò“±}û¹©o\Y»Ùx št‰:ÝzU×_Íå‰ê“BQ“¤åW4@ :£ õ؇<õЇ€;ÍÛÐsÒ«ÜÜ1=€©5÷>q‹-dÚç ˜‰6i·1DãÉû¬Ì_1É‹\è»óØôqtî׎ï–Íã«ÿ¦mUªahÎÐ;;3ëÃg¹ÿ}7|[ÅCC³ÜÞþMÞ{q2Ë Møì4Î'­%(î&eÍgÉô)¼çò&º÷x¿µ1zlUÜnDtŠA]¸3¶DzÄpçScøpÁrž4-¦FÝ6( xÓyÂd,^Ì‚i_bô>͈óÕ#‡ãû÷“[„«EµB¸(J9̾ýNòV2ZÈŒŸR°£àÕ´'wÞÝ ëÌ¿• 'h}[Ð÷Þ>DèÁzì7èby8´M`\c1,’µK^&¾ÑCô <Áš·ÖQ;‰¡Ñpœ¾`¤ß‘·2ªÍ*f¾þË&&2ëþóq:ﺓ֞>t=€À‡—ðâ|w­½Ëg±ËÐú†£ÃÆá²#ý6ªdý·J–'þgB!j²ú£õ¦~ªTø—ëé;àVÖ~õ9}ÜÊæ ëhݶî¢hHFŽº“å Ëjº ƒíK§¾MÞ½oð@kÏúÓBªæ³cö“$„>Åw4æŸHÿ*n‹,…lÚ°žÞ}ûcööùjSË9ÒØ´ðUf²ƒ›;×dÊ“£éè§-I|»—9ÙƒK¸r~gÙ3Y¶éEú Úy„i®'äìU‹9±ö?¼6ïÿø3ˉ©Y÷>õ8C¯6¡ÁÆáÅã¿ÒÈÄ%³¹½‘þ¯×_éò†OW-§ÿ-ƒ±ÛÊ?™ö?Ç´Ä€ø‡UÓâï9|ðí:vFUUEAU]TpÁ žýWÎýóO\Ú‹L-ïO¹Ï]x³tÉêÙõ¨çÆ;9w»¤À¹ñAÊþ½ð±ÒqRTõìí‹»t€ê¥t”î#EÑ ÕhÐh4=M¬^µœ› ­`¿Õ?{ví C§kÏÝOXú>cÆŽÇáp ª.\.*àt8p88ì;ªêÂËìͪ•Ë2t[·l¦s×nlݲ™ëzôªÒº¿ÿö+9‰)DƒåÉ€ÛótÂZN]=„Fõ¤‘Ôvì+–ˆæŽ{"þ‘ÄWT“.ˆî÷Ï¢ûýå=ÎàwÖ2¸ÌCŸöŒ~-Ñ-Oq§q¿)Ìë7¥œ'݈û>ëÆVqý•.O\B!jHƒI~÷ìÚQÓUeHÚ@ƒßµ™ÖH¥>õv‹èËCÓt4ñ‘6!„Bq^ƒI~A®Ú¢²#GÝùÕD!„BÑP4¨äWÔ~r½¯¨oEA£ÑàrUcp!jHUâTbZÔ%§Bˆ²j}¿@‡ÃÁ™ÓgHü3‘íÛw°}ûÿLäô™38Žš®ÞÿL-8Äw+W±)µvmKqÒ̉_ÆÞ¢ŠJ¨XOnࣄï9)ãGQ!EQ0=ÉĮ̀éªQ©´Ô¼ÌæòF9GbZÔ%U‰i!DÃQ«“ßÜÜ\öìùƒÓgÎ`)*BUTUÁRTÄ™ÓgسçrsskºšÿgîŸ|ûëÙŸw%ÏL:ÉøùuÆÎw©Î*”W±§îâ×GÉÿ‹â–¤øü»ÝdVe‘B4PZ­ŽÈÈæìûs7ÉgNK+„¨•\.)gN³ý·_hÓ—«âv‰iQT'¦EÃãÈ=ÊîmûÈ–°hpjm·çÜÜ\<(èôî˜Lfôz7ìvy8ìV:ÌUÑQ˜½½«½Ëïo0aæQº>ÏíL Înçø'Ï0í3 £Þz…ÁÚ˱Y5Dƒ›9„ðp;>nõfB!ê­NGph(nþܳ“_7ý\ÓUâ///Z·iO`p0»½Â²Ó¢.¨NL‹:Ȟʖ³âûœÌw‚Æ“àè œ0‘þM •¼ØIö¯ xmy OÏÁÇý©±¨%jeòëp88r$ Pp31›}0™Œxx”DgQQ1:ž¼¼lV ‡“ŽÒºU,:]u6ÇIAZ.N ؼìÆ£I™o]Y¿²ìË3€/™…N .'¿ æv÷ðb»š®‡ ÏÙyýƒ «x D!jš .—³Ò$AbZÔUŒiQרIZùo~VHìÍ÷pÛ5è‹Ò9z oÏZ™ÚˆZ¤VFHZjN§žxx¸ãéiÄl6%góœNv»'6›§ÃAZZaaaÕX‹“ü´|p À+e-«vôáñ®Þ¥ßáVŽ|ù ‰º`Œöl2 K»t¹²ùå?ÓY´%•b@çEÏ;&2ºGnØ9óÝ|þóu"§Ò °ZïHº ½{nlŒ{éGæ>Y¼œïv§R¬xÑ(ÊLf™ZÙ’V1}Æ×ÎsFÂÚ÷çÞñ·p×ÿÖCÝòû«Ü7ÓÎóž§›YÕÊéM+XüñÏìÏ´£õ‰¢çíãÝ3C¹?d\äíý’Å|ŶS…` ᪠\À\æù/XøÞ×ì8cŃÐnù÷Ę凑8Î’Ï5!ê ‰i!Dpf³g*4ŸÈ¤»o¿ôwf×ëË\Ú­Z9õó‡,X±Ž½v´¾Ñô>‘{®à\'HÛv^½wdÉ퀼?‚¦õhêGQ¾Z™üæææ¢( ZZ­­VS:²dIHk4´Z Z­Î §ÃI^n^5“_–ÌB”FÃÓä æ|ü=';ÜFc=¸²~ã£u´¾kž+ç‘‘kEÅEãIóžwðpo_¼4ùüî}–-˜G“˜ç¹)ÐE^ÒŸµÆ2zòõ„i 9þËǬX2cÓŒnîÖ#¬zåm¾´¶gèýwm,àð¦5|R¦VÚ€vÜ2ݱ§lcÕ¢O˜½"†Ùãc¸|½2\än—æï§Åíð|KOÒ]ÁÂwg`“ÑQn—¼Â‘ü o¾ºŠ”˜Ü÷XK|¬'ÙöåÇ<[ øËgÂÁ˜Q<1>/{&©ÖÆ%ñB!„—‹ÖDX¸¶þÄúC×2ø*ÓEƒ¹Èùm.ÏÌÙGÌÈI¼ëIÚæ¥Ì›÷*náosOtioNíÕŒ}þn®2¨(zo‚keV$.·Zy˜.Z­®¤k%ƒØívŠŠJîÛíösl”$ÉzÕ=û¬ÚÈϳ¡1{Ë šoø˜ÏûñP['¿_Ã>s/u eËçp,£'Þèp#¸Mg‚KÑ,0‡Í[–²ëŒ›Kßv^‘´mך-´ŽñâèÎ×ùý÷TF4o„}ÿ—|ŸêOÿ—bhó’óšà 6ýöý¹jiÍÑt<;qT#ô{æÅƒÈrÄv¹Ž–3M«Cí:•niQ˜&c9¹íY6m:Åð¨f½ÀÆñï¾!ÉãZ¦NN[OhESûo¬_RZÄQ@–ÌÍZžH¢.Su…B!„(a¤õ¸Çšý6 Óîæë=¹©_ú]·p¦±aÕ¯¸º=ÃÃÛâ Ä4ÀñßždãÆ“ŒŒnZ²­'ÁÓØ½¤qMUeHð† V&¿nz= V› ‡ÃÍfG£Ñàp”$¸gs88. n:ôújnŠ«˜Ü"0Ð\Çm]?aÆê_I‹ `ÍÙ\5²M=lüi€¢Ü"\Îlv¾”Näd†Œì8hé¬àÍ¢÷£±ü™mÁ…ƒì£§°º5¡uØ¥-«%TŠOüÌòe_³ýH 9v=Š|­8.çûіʾ°œ|“q[.|J›–Ï%§T §“r ¬ ‘5åz¶dHÿ&¼²j“ÿ¸ž¾ýú×¾ÆZ=ž¸B!„¨k4Þm5}17ÛÁÏßÇ·sŸbUB7|ñanôN&ñ XŽ¿ÂÝ›/|6µœß¹¢A©•ɯÉÓ«ÍF¡%›Í†V«EUA«-°Àéta³Ù°Ùl8vŒFo n%”p“W OŠbäš[úüä–-÷b—®+OvñC«da2¨Xó¬¸p’¾îf¬Î¤Ë}uúœßøÏŒ/*^‡¢A«µ´»¶¢(%ƒ/TT¾øK_[Ħ Œ{¬ MvŽ|:“EÇ«·i•SQ¿¸É<Ñ7ä‚ Ðxà\¸!¥×B«O“§‰ñóºýÎ_}Áš·§ñYË»˜þD_Âäú !„BqYi17íÌÍã:ÓÈ/Ìzl& ÞíBûÇ=ðïû$Óþ‚N-ý«ª(îåüÎ I­l—3™½0  z¬ÖbŠŠŠ(*²`±a±”Ü.**Âj-ÆÍMÁ`ÀdöªÞJT+ùVp3êQ]èõ º¦›NÒw 1F7Œz°æãÂNêþ“¸BãzS{®jژȨ&ç†{ªœ¿¨¦í‡Ùšd)7‰tæåpžžVCÑ£e3š4iFdGõ¶«*Ü‚ˆ ‚¬#¸‡„~þ¿P?ÃwºT#-üáÄoìËý«&h-¦Æ¸õþ—x{r'”ÄoXZFXB!„WŽÖ7–®àH=Iž&„˜`È<”†{hçÿ ó7  Aç¡»…¢ŠzoŠz«V¶üz™LX­6BC‚8~â4EEE8v4š’ Ô].'v»pŠ‚—ÉT½•¸¬XAïáVr@ãK§‘wp÷…t¹1´dÇ(ZÜݵ¸ °©z£C`Ëz>û!‚¸h?ô–ÓUaØýª[¹µÙV¼5Ãðþ´ óÀ~òø¹³OZscšxÚÙöùlÔ´'Üäâd¦µÌœdü<“i‹Séþì+Ü}•;`çøêxþ+7†M†a:P ؽàiâ÷DóÈkÑÉû¢®ÊÚ`ºîÀçs—òú‹Ò½9¾šBROYiqSO"Üô&o ìå—_sC4û¥Íïòß×’5¨+M½\¤*3Ú³ý4¾?„gd#|õŜޛB1F|0ßK {×ȩ̈o¿ÇôwŠÖ# M©'‹iѧn ¦FWá§~Ã'«~Æ­ƒé¢zt"XB¥Þ«•ɯF£Á×ÇKQ1-Z4çÔ©drsóÏ €¥ª*>>fÂÃCðôpG£©f’å²a±où04ý÷M¼ &èzẸ̀إ%øÆ¹ÿÌB>Zù6›í:Œ~‘Äšª8°>œOýCB_.ŸÍZ 5ج-ͼ4àq5wMFñ{_3ïõÿ@1˜ k„{™Í»ô'†Z΃*jy—n—o×xÉõ1|úó·XPÑãÓ¬{ö ÂMÁ½Åîèp‚•+VòGççèé߃I/iXµô3>³"@q÷£që«ðÕ‚Z”Îáͱ.! ÷‹¦×}c‰ ªËó# ñ¿SU»ÍJfzÇ¥°°àÜ€}BÔ&&“‰ÈfQ…„þåwªÄ´¨+ªÓ¢ŽQUô>ô㇬M-éM©57¢ÍÍ“¸{DKwáÍ2#8—¹æW-{»¤À¹ž+eÿ^øXÉëJ^Szû¢Ç.] º ´áŽÒ}„¢ (´š’i\ž&V¯ZÎ̓†V°ßêŸ=»vСӵçî',}Ÿ1cÇãp8PU.— p:8v‡UuáeöfÕÊå :Œ­[6Ó¹k7¶nÙÌu=zUiÝßûUílù=«dŽß’ÄV¯Óa4°ÛlØUC5¹Bˆ˜ÃnçØ±$b[µ¥YTtMWGˆ ùøúáa4’¸g7Á!a'¿ӢލjL !Ž:sªÖn·a),ÀRX€Ýn«éê!D•¨ªJAA~þþ5]!*FAA~Å7HL‹º¥*1-„h8juËïå¶g×Žš®‚¨ÄÈQwÖt„ø[–',ûËç¥[¨¨ 4M•¯ß•˜uAubZ4w qÉc•}‹ú¡Á$¿riÝ!>¢®‘“6B!DÝñÑÊ^ó+ê79m+ÕšÁ±¤tŠëð盫8—œb9‹-„B!DuHò+”¢½KxyÞ/dÕÕ1/œi¬}a2³v¢Ú±rêý¼¶1›Ë– »lXËLøîL]Ë¿'<Çÿq\®5\6ªÝŠ£ŸÄB!„ÿ¬ÓíYÔ7V.|”Öç_ú”ÿ^9‚¦õ}¢r­±=ãij¼<ãxرâ©x²ÇÅóÀÕîh¼¢éÖšz×®ùšikyþé yçi:kº6âãHgÓ¢×xç“m$[=htÝH¦<5š¾r·Vã#„¢–“äWÔQn4ùs†8¡ø‹ÿ½ ωÏ1<Ò EkÄ»¾'¾3±o»|ËSUì5¥*ÆfôÖìò­ãrQØ]ÒìÛ°Ø8²ôq¦%÷ÀK<xœ5³óøÓ^,›3”pù6«ar|„BÔ~òu$ê(§þž@±7­ƒ·?þþn€Š%i-‹}ÎÖãyš\ÏÝ“î¡GðűŠ%é»rËÙS6°xö26œ°€±9·=÷,Cë(>ùïÏ_ņc(æÖŒya ½òعj>ï}›H†Ë›«âîäþQ׬۱OysÖ·ìK· âNhûŒw ±ÞZPmœÞø! Vmæp¶†ÐηóÈ„>4q¿°×™½“þóßìËÂe!ÀâÄÀq†5O=GâÈ9<ÓÖÆŽfñÞ†ÃdÙÀ«ÓcÌœÔÍÑò·ì¤mYÁ¼e?r Û‰±ùž{²8sÙ4}›}û©Ì›Ëë“×Ò3þ%zû+ÿ‘Åó?fó‰BÜC;0è¾û¸¹… řůïÆóáÖäÚSSzÝõcºqn¯'2wÒxdµt5—ͯLá‹¶Óyu€)í g»?}—%_ï!ÝæFXï'x±?`ÝÇÌûJšŠºÿ?¼ØÝŒµ‚úa9ÀÊ·æóþtŠpçšq¯3톴€õØ*žyùzMÿ7CNx†øÔÛxmòµHƒU-bù“'á?è¿<9¢¨Äzáö© |rh “®6Ôt 69>B!êI~E½£Zù0~éq“‰ÂÌÁåo°dÁf®y¶U*וäUËÙ1–YϷŘŸ‰Õ[‡Z|€„7–r´ËD^}4 CA>îþ¶îÞÙ`fÄ33i§?ÄêYïïÁ«CáÌ=ÊQGg¦Ä¦‘ý8ëßÿ/ñ³½™ñÌxŸüŒø%i÷Ð+<žÌ3æ2÷ÛX^~þéÊb󼹬Óæ©ø.˜³vðÑì\¼Ñ. 'ã=|:/w5cs1%ònûÁ|ê fÌÿæcžçþXEyBt 5Óíéx&ÆP4”ÜMç÷WÑ~–ÍHàäµñÆ”0r6.â홋ŸD'c1©G’ñù*¯t0¶õâ—¼GëÖSéj.Mæ éé aW ö–MÑ`û)/Zßéóø§ì‹²šÃ¬užÜ15žNrí~¸‘†«™üÎS´÷E£…¢}Ö¯£+}‡µôyvýBœX ~œíÈmhr ã{ýÊ›ïm¢ýˆ,mòçöé%ñ­eì©»HÌw§ef¥ïc¯kz­¬gWbΫC©]ó9>B!êùy'êë‘ulwubø€X‚}Óõæëñ9±ÅU-§Áͨ¥8å$© žAaø¬G¾ç7gîÖ…¦þ„F6ÅWÉ`ûÇ|'qWÙ‘#®"sÃFNÙKÖ£¸™ ð'°q{†ŒLPÒìÌ*âÔÆ_ÈÆàaø…¶c`ÿp2v"¯ÌèUjþ>Öòæ¦Q}iH£˜ö´ô«è'¤c`¾Þ~û»c«pûìœÚ¸‘̘‘ŒêÙœ ÿ`šDbP­V‹F¹°Úšô[=´Bi5ð.zÿ`íÞü’éÐàဟ_01½úÓJŠ}iöó PL\Õ­ »÷îë©mÒ·¢S˜Zñ¾°g°í‡$B†ÜÅMW‡àA³0ã¹.F[Z×*ÔOqÇ?Èoß`‚Œe>úw¢‡Œ¥ó™yñÕop»õ^zÈÏôÚÆ™—B^„˜ÏŸ³U „˜ 79Ú7$[Ã"ÇG!D] -¿¢žQ±æfaÉ?Ìô±ç[-Q¢É·©U,§§íˆÉ _úïLúŸ.Øpo_³³°šÚáSö]ã, ­@‡°©´UCÁàŒ»%|'„\T;­wºÒ „¤åSüÇl&”"6<—"çNK¹,™¨fÂÌÕ}«þÕö9ЦZðöíÊ#e©X³³°z•Ù~f;3,—Ž6­ñÀÇà¢Ð^vŸ+x·ìIã÷¿cwVb¶þ­&ÒÄÍÅŠö…½tÿ™*i5ªfý.¢xFÑ£­;ë2rSKù`B!„¨‡ä7ž¨gÜÌ>}{óÔ[÷åvá³–*–ƒ(úN|™‡íbåëo3ku ^ië>ÿ 9;ûÎÑšòtp ¥g´¨X3S(26Å«œlÍYF–ÓXOž&<;Žeöä6T4`±Æ3³’ÃÉøTg¯¿Ú>Gü %gcS)mñ­âÄVî4P ?ÜòOŸß~G§r¼Œh.鈭¡¤áøÂA©4¾­‰‹\η[÷þ»‹v›b@©x_8Sð7:8˜Vˆ ó]U-œØœ* T¡~Ívü+Þÿ=œ~ݳøfñt{î_„ʧc­¢õ ÆL>)yçTµf’RæP³|™Õ09>B!ê<ˆZ IDATéö,ê÷f7ÐÖ±‰×ìàxFÙi'HJ¶ ½%ï'sí*,ç$çÈNf[pihÀ^hÅÐüFÚ±…VýÊÑôl2NãŒÕŽqMI^³ŒuSI?¶™+У;¥¹ª3÷0;÷"=õ0?¯\Ùðn´õ÷ ¢{gŒ»øhsi99dœJâdÁ…™§âÕ’¾­,|·x5ÛNd’“•AŽ­j£W¼܈èѯ½ËXºþ ©ÙY$;AÞ„{[9üëœÎLå豜 º*š÷¦³v+®ÞÁ©Œþüj)?XZÑ篪Oµ¤øÐæ¦(R>YÌO®ÎÜÐÔ¸U¼/´tìΩO>à›Äd²²S9~<—)å4¿l?NFÚ Ž¦[qû»õs$óÝ¢ïñ4Ž‘÷Œ§GÎg,Ù˜A] º¾Ò‡´£¥W1‰›“(¹‚A¥`ïÏrÒ®¥Ÿ\OZÃäø!„¨ äd¬¨wSkî}â6/ZÈ´Ï @13lÓn5bhÖ‡¸ ·Y¸øb§¶+¿Ü-þœúi!3~JÁŽ‚WÓžÜywîž:ÆLÁ’…ïóÌÚB04¢ÿÓ/rgÜ#LʚϒéSxÏåMtïñ<~kcôP’<Ú“Ù°à>ÊqâÕ,Žñô!T 4ÆÔ1Å,Xþ"æ8Á£1ý¦þ›ÑW•ù™¨xÓyÂd,^Ì‚i_bô>͈óÕUšpþå~ˆÆ“÷Y™¿b:“¹Ðvç±éãé4ü_¬Ÿ=›'Ö+øuœÈ‹w—YžG wMÅâù ˜úµCh?~Ì U¿ OÁ«U?ÚéßàX¯h\Ú"m¨p_í7™GrðáŒ'H°+ø´ÏK“:sû(f,y–G\&®õB!êeõG êMýT©ð/×ÓwÀ­¬ýêsú¸•ÍÖѺm‡+\EÑŒu'Ë–Õt5.›¢Ý3yøƒF<ÿÆí4ns7P·E–B6mXOï¾ý1{ûüÃ5«gilZø*³?ÙAŠÍˆëF2åÉÑt¬p8ñw|ºj9ýoŒÝf+÷ù cZލ¥*‹iñ÷>x€v;£ª*Š¢ ª.¨ðÔ¼zö_9÷Ï?qiß6µ¼?å>wáÍÒ%«g×£‚ª¢–½]R ¤ÌÙ²¥/|¬äu%¯)½}Ñc—®P]pvðÐÒ}„¢ (´ £§‰Õ«–só ¡ì·úgÏ®tètí¹û KßgÌØñ8TÕ…ËåBœ‡‡ÝÃaGU]x™½Yµr9C†cë–ÍtîÚ­[6s]^UZ÷÷ß~%-¿B!ê]ÝïŸE÷ûkº"¢\r|„BÔr’ü qy´™Â¢·jºB!„BI~E­3rÔ•B!„BˆjäWÔ*õéz_!EA£ÑàrU6Û°5¯*q*1-ê‰S!DY2Õ‘B\AŠ¢`4z’™™QÓU¢Ri©)x™Íå:sŽÄ´¨KªÓBˆ†C’_!„¸‚´Z‘‘ÍÙ÷çn’Ïœ–VQ+¹\.RΜfûo¿Ð"¦%.WÅ3]KL‹º :1-„h8¤Û³B\AZŽàÐPÜ< ü¹g'¿nú¹¦«$Ä%4 ^^^´nÓžÀà`v{…e%¦E]P˜B4’ü !Ätv^¿ÀÀ`‚ƒÃ*žQˆš¦‚Ëå¬4I˜uFcZÑpHò+„ÿ§Ó‰Ó)ÝîDý!1-„¢®‘k~…B!„BÔ{’ü !„B!„¨÷$ùB!„BQïIò+„B!„¢Þ“äW!„B!D½'£= !Ħª*‡½dd\UEUÕš®’—PNN¯¯´¬Ä´¨ ªÓBˆ†A’_!„¸‚TUÅn³’™žÁñãG),,ÀårÕtµ„¸„Éd"²YA!¡h4w “˜uEUcZÑpHò+„WÓé 3=ýöÛª ~þò#LÔ:.—‹¬Ì ÿØ›Á@`Pp…søJL‹º :1-„h8$ùBˆ+Èa·sìX±­ÚÒ,*º¦«#D…||ýð0Iܳ›à° ‰iQWT5¦… ‡œªBˆ+HUU ðó÷¯éªQ©à0 òA©¸ŒÄ´¨KªÓBˆ†CZ~E­2rÔ5]!þ–å Ëþòyé*êFSåëw%¦E]P˜BÔ’üŠZ§²$BˆÚFNÚ!„BÔ~rÚV4(ª5ƒcIé×áY9\ŹäËYl!„B!ªC’_Ñ í]ÂËó~!«®ŽyáLcí “™µ³•böΘæîÁr¹’y— «³ÌÂìÇX9õ~^Û˜M­K·/®«B!„Aº=‹:ÊÊÁ…òÂúüKŸòÀ«3GдÞÏi¯'¨CqÎPÜ.Ç@öc¬x*žìqñø{ÅÞ´ Þþøû»*–¤µ,^ô9[çahr=wOº‡ÁgÄ*–¤ïÊ-gOÙÀâÙËØpÂÆæÜöܳ i¬G-8ÄW‹ñÙÖÓiý¹öá—x¨“‘ÔÍ ÌKø‘C¹:ZõãÞ ·ÑÖW‹íا¼9ë[ö¥[Pq'´ýÆŒ»…Xo-¨6Noü«6s8[ChçÛydBš¸_ØÆêÌÞÉGÿyoöeá2‡`qâ €ÃK¦0Óö ³&F‘³þ]f­ÚÆñ\;ÆñÂ÷qºüí;i[V0oÙÈvbl>„çžlÎ\6MÇ&@ß~*ó'°ö©çH9‡gÚQ ðÅ» Y³#«gzÜ>1qqweñë»ñ|¸õ¹6ÀÔ”^w=˜îAœÛëj¿¾:™‘Ï1sdSô8IýúY¦íÈ[Ï^‡ÛÑ êêÌa÷§ï²äë=¤ÛÜëýÓG¹_Z×)­PŽÿÈâù³ùD!î¡tß}ÜÜ–¬|k>?ìK§w®÷:Ón@ دäñŽpǬ§¹Î[AÍý…—[C›é¯qk¨öÊ…°¨ËŸ¬ø8 ÿAÿåÉ­ð@%Öó·OMà“C™tµ¡¦kذÉñBQHò+êՒȇñ«H›Lüf.ƒ% 6sͳ½ð¨R¹®$¯ZÎÌz¾-ÆüL¬Þ:Póض0žOóûòÈסäañ2á8ññ w=î%ж³3ámfÏñçÍgzcÊ=ÊQGg¦Ä¦‘ý8ëßÿ/ñ³½™ñÌxŸüŒø%i÷Ð+<žÌ3æ2÷ÛX^~þéÊb󼹬Óæ©ø.˜³vðÑì\²Õ.ò“ö‘y/oÞ×w› “#‘÷+ØæS_0cþï4ó<÷Çš(ÊÓ¢+­™nOÇ31Æ€¢Ñ q&—Ùa¹l}w&Ÿöç±ønøœþ޹sg1ƒ±QŤIÆ䫼ÒÁ@ÚÖˆ_ò­[O¥«¹4™W<‰êJÞºýd;›¤)àÐŽt‚;Ec*JdQ¹uíë§9ÌZçÉSãéà ×—ÔU)ÚÏâ œ¼ö!Þ˜FÎÆE¼=s¡ñ“èèJcßa-}žE¿'VƒgÓZ}hZ×±%ɵí<±žÚÉÏ–Œô—Ä·6±§î"1ß–=𕾼®éE´²ž]‰Y8¯EŽXÍ‘ã#„¢. qD½c=²Ží®N K°ocºÞ|=>'¶q¢¸ªå4¸µ§œ$Õ¢Á3( ƒ‚šŸÈw{Œô¹çfÚFøIc³‹S7’Õb8#ºE|qw "$iÛ3KFÕRÜÌøظ=CÆ&(éGvfqjã/äÆ cp‡0üBÛ1°8;‘Wfd)5ëysÓ¨¾´ ¤QL{ZúUüRïH 7A¾TÑöÙ9µq#™1#Õ³9AþÁ4‰ Ä (( Z­ra ´š¿—ïÿðä¦ÑhH£ŽCÝÙÁo?$Q²k5xøàçLL¯þ´ÒŸb_š½Ì´øÅv!(õ7ö婨–$¶œò£s+? KÛ~H"dÈ]Ütuþ4 3–~p]XWkÒlutfôÐ4 ¥ÕÀ»èmüƒµ{óQwüƒüðö &ÈXæ£Ï­1=;8°é(EØIý3 媄»U/îÄ•åÌK!/BÌçÏÙ*†BL›œ‡£ë&äø!„¨¤åWÔ3*ÖÜ,,ù‡™>vÓù‡•hòmjËéi;b2×~À;“¾Æ§Ë0&ÜÛÈ rUoÂÍeß6NòS‹ðñ/MAk ÁO›OZþ¥CJk½#ÔVà $-Ÿâ?f3¡ì±á¹¹8wZÊeɤ@5f®î[õ¯¶Ï6Õ‚G°_µÊr•n„÷Ùºèð óĺ;›êsaa>…ö ¯ËÕv຀/ؼ?ަÍñêÌAZ¬G+¨kQ>i:üƒL•´©X³³°zµÃç\õ|ˆ0»Ø™a©d¤jM{uÆðæ:ærf—…fÚ"Ãh !„BÔ/’üŠzFÁÍìƒÑ·7O½uQµÞYªX¢è;ñen¶‹•¯¿Í¬Õ-˜y‹/žär&Ï>g¯dÕâäAÑÉL¬jœ)d:MÄzi¹¸²³ ,‡'±ž:<LxvËìÉm0V°5ÏÌJ'sÊ®óÝ6Žø(JÎÆ¦r.iGQÐ*NlLUR—\Nåž­‹ƒìÓü|ÊI¢5”4_4(•.˜.=øúçßÙ幯nƒ Õ+Ø+ª«3…\£ƒƒi…¸ð8ßUå’º*|üpË?MŽÂt€#‡S¹ ÞÆJ»¸¸5Ž#Îû%¾Þ°™¢œæÜíY»F·h½‚1“OJÞùU­™¤‚9Ô,_f5LŽBˆº@º=‹zǽ٠´ulâÃ5;8ž‘CvÚ ’’-¨€Fo@É;ÆÉ\;† Ë9É9r€“Ùœ†@°ZÁÜ’¸˜|¾}ïs~?™IvúIŽ¥9 ïÙß+Y¹ùéi‡øqéR#ãèXzͨ3÷0;÷"=õ0?¯\Ùðn´õ÷ ¢{gŒ»øhsi99dœJâdÁ…™§âÕ’¾­,|·x5ÛNd’“•AŽ­j£W¼܈èѯ½ËXºþ ©ÙY$;AÞ„{[9üëœÎLå豜 º**^×Ð'¶ï—~͟ɜھš¥ÛôtîIÕ‡²ÑÔåF‚,eñNoz^‚î¯êª  cÏpN}òß$&“•ÊñãÙØµ—ÖUÛ¼7µ[ùpõNe¤ðçWKùÁÒŠ>×xUžÈêBè1° ‡V|Ê™¨ˆ1Iê[ÛèCÚÑÒ«˜ÄÍg»Ù«ìý™C®@Úµô“ëIk˜!„uœŒõŽbjͽOÜÆâE ™öy(fb†McÚ­F Íúô6 ÿAìÔv嗻şS?-dÆO)ØQðjÚ“;ïŽÂ ÑÓíÇÈxw sžZƒ1#žcÚÀ[™:¾€ÿ,}ŽIyZbû3é‘ÔB€=™ žá£'^ÍâÿHBµ@³aLSÌ‚å/òhŽ<Óoê¿}U™Ÿ‰Š7'žNÃÿÅúÙ³yb½‚_lj¼ô@äEu™BÊ» ™ùøÇXéq×ÜãâÌ®òñÑt¢_ôræÛoâÚ m%umDh¿É<’»€g:™È©¬Vü:  ­é$ú¾Wã%¹oícŒeİHÖ.y™øFÑ'ðkÞZGqì$†FËHÂ5NŽBˆ:@YýQ‚zS¿U*üËÆõôp+k¿úœ¾neó†u´nÛá WQ4$#GÝÉò„e5]˦h÷Lþ Ï¿q;ÄÜÃuj·Pä´“üóBÞÞÍ´n%¼šÇê¯â¶ÈRȦ ëéÝ·?foŸrˈ*r¤±iá«Ìþd)6w"®É”'GÓñ/Õ÷éªåô¿e0v›­Üç+Œi9>¢–ª,¦ÅßsøàÚu쌪ª(Š‚ªº ÂSóêÙåÜ?ÿÄ¥}ÛÔòþ”ûÜ…7K—¬ž] ªŠZövI’2gË–þ½ð±’ו¼¦ôöE]º@uÁÙÁCK÷Š‚¢hÐj4h4Œž&V¯ZÎ̓†V°ßêŸ=»vСӵçî',}Ÿ1cÇãp8PU.— p:8v‡UuáeöfÕÊå :Œ­[6Ó¹k7¶nÙÌu=zUiÝßû•´ü !2•¼s˜4ç4zpÏ£ý«øŠ.ˆî÷Ï¢ûý5]Q.9>B!j9I~…¸‚<ÚLaÑ[5] Q1ï®Oò~ך®‡B!„¸Ò$ùµÎÈQwV^H!„B!ªA’_Q«Ô§ë}…PFƒËõ׳ QT%N%¦E]"q*„(K¦:Bˆ+HQŒFO233jº*BT*-5/³¹¼QgΑ˜uIUbZÑpHò+„WV«#2²9ûþÜMò™ÓÒ !j%—ËEÊ™ÓlÿíZÄ´ÄårVXVbZÔÕ‰i!DÃ!Ýž…â Òêt‡†âæaàÏ=;ùuÓÏ5]%!.¡Ñhðòò¢u›öã°Û+,+1-ê‚êÄ´¢áäW!® ³óúVñˆBÔ4\.g¥I‚Ä´¨3ªÓBˆ†C’_!„ø8NœNév'ê‰i!„u$¿B!„B!j…ÿ[óé¹Û7Æõ¹¬Ë–äW!„B!D­pÇÈ»ÎÝNOM½¬Ë–äW!„B!D­`±ât8p8—}Ù’ü !„B!„¨¤Û³B!„BˆzOº= !D¦ª*‡½dd\UEUÕš®’—PNN¯¯´¬Ä´¨ ªÓBˆÚCº= !D¥ª*v›•Ìô Ž?Jaa.—«¦«%Ä%L&‘Í¢ E£ÑTXNbZÔUi!Dí"Ýž…¢Žr:d¦§³ÿÀ>b[µÁÏ?@~„‰ZÇår‘•™Aâ»q3 ®p_‰iQT'¦…µ‹t{Bˆ:Êa·sìX±­ÚÒ,*º¦«#D…||ýð0Iܳ›à° ‰iQWT5¦…µË•ìö,§j…â RU•‚‚üüýkº*BT*8$Œ‚‚|P*.#1-꒪Ĵ¢á–_Q«ŒugMWAˆ¿ey²¿|^º…Šº@£ÑTùú]‰iQT'¦…õŸ$¿¢Ö©,‰¢¶‘“6B!„µŸœ¶ ‹ËBjÒ1²í5]‘ÿÓBNŽ™XD!„Bˆª“äW4(®Ü­Ì™þÖÕÔÑÎÑ•Ï0õƒCX)fïü‡y`î,—ksT'6[™AìÇX9õ~^Û˜M­ë4æ²auÖÕã(„B!þiÒíYÔY–o2!~—ŽÝؘѳ^¦_ ¶jõOÒÔ!Ž8g(n—e böþw ‹žä;£ÐúÛ3ަÆÚ5Vˆý+žŠ'{\<\í^ÓµÿG:›½Æ;Ÿl#ÙêA£ëF2å©Ñtð•󸵂!4EQPåœ4  (* PòPQ87êÚÙ?Š‚*ýøþq’üŠ:Ë#ö~f¿cC¥ßg=Ï—1Syº_0:Eɧ¾'¾Z: bèe\¢Ëá¼ðcXc&vàm—q —‰ªbwÈFÃbãÈÒÇ™–PHÜ/ñhàqÖÌZÌãO{±lÎPÂåÛ¬†ÉñBQûÉב¨³½~þ€jÀK¯AgôÁßßàÊKdõ»ïóÍÎdм¯â_ãfD{ßKZ/]y‰|VN9Mñ1¾™7—·§`UÌ´ó"OÄ¢)8ÄW‹ñÙÖÓiý¹öá—x¨“‘ÔÍ ÌKø‘C¹:ZõãÞ ·ÑÖW‹jÙËÒ7æóÓá,¬(˜šteð½cèeDAÅ’ô‹}ÎÖãyš\ÏÝ“î¡G°þÂJÚSÙ¼ô¿,]w„ùïÏ_ņc(æÖŒyáAB°“úÅÓÜýàÓ›—fôáÏçŸ#qäžioD-8Àï.dÍŽ¬žMèqûÆÄ5Æ]q¼öâ?ÝCr4Þ´èw“†·åÜy5__ÌŠÈç˜9²)zœ¤~ý,Óv ä­g¯ÃíhEûÂNÚ–Ì[ö#²›á¹'Û3—MÓDZ зŸÊü)­q¯¨~j;>˜Å{“e¯N1sR{L `=ÀüG^fCÁù]n¼îiæ>x Ò¦\‹XþdÅÇIøú/OŽh…*±žG¸}jŸȤ« 5]ÆMŽ VI‹¯Š¢”L…vö±†JQ8×P²_”K¦Ú’ßš#ɯ¨\9l™?›oÊ“³;¢l_Èëó?¤å[ЦJå¦ùž>>ÞŽ)s‡¥É!SñE«æ±ua<Ÿæ÷å‘7®'BÉÃâeÂqâ3âî&zÜK<mggÂÛÌžãÏ›Ïô&À–ÉáãnôîmnôÍgÿÚ…,xc!3¡ƒ.‘ãW‘7™ø'Ì\þKlæšg{á®— ƒ3ßÌfÁŽPîù÷´t;æ÷çðÍ¥M~Ò>Ò#ïåÍûZãnsá¥ËaËìò÷C[ÝÞXÊÑ.yõÑ( ù¸ûk9ž ÏòúíÑ+´®dþ<» 5—­ïÎäóÂþ<ß ŸÓß1wî "f06FCáé£äEßÇŒ{cPN¯gÞ[ XÑîm4…T<‰êJÞºýd;›¤)àÐŽt‚;Ec*JdQûÂ|ê fÌÿæcžçþXEyBt 5Óíéx&ÆP44j.¿UT¿( 'ã=|:/w5csñ<ûEdhÁ„ypŸ Ö¤yþµÝtùW3ä§zíbOÝEb¾;-{4ïkz­¬gWbΫCiý=j+9>Bq©ŠN(¥Ù°R».*käBQï¨ù{ùaŸ'qÃo$:ÀŸ¨ÑA{ˆmgìU.§q÷@SxŠ6ôÞ!„›u¨ù‰|·ÇHŸ{n¦m„?á‘46»8µq#Y-†3¢[$ÁWw× B’Ö±=³ôjdÅß@üƒ›Ñmø}ôöØÃwû (>²Ží®N K°ocºÞ|=>'¶q¢¸L%ìØ”L£Á#éLp“XÚGyWøÆÕ{èãM@/noŸõÈ÷üæìÂÝúÐ4ПÐȦø–ž S4Z´Z-ZÍ…Èjþ^¾ÿÓ›F Uh :etg¿ýÄÙ*ëLþøùÛ‡þ‘VŽÎÆqn Züb»”úûòTTK[Nùѹ•Ž ÷…S7’3’Q=›äL“È@ €‚¢Ñ ÕjÑ(Jê§Á„¯·Áþî|Ý(-ûQÖÌÿ÷Á2¨™»|Õ2μòð"Ä|þœ­b ĹÉyeâLÔ9>B4\çZ7)IöÎÞVØÀe·½Ì£çöƒZzå/ª JÉ}iþçI˯¨w\– òlé|1m _œ{ÔöEÎ*–saj3–ÉÞçƒ×ä³ðë5á.ºk3ÈU½ 7—}Û8ÉO-Â#Ä¿4!­)?m>iùNð¿¨rz?šú¨$fRä•…%ÿ0ÓÇn:ÿ¼M¾Mc霅dXtø«}¦ªâís`ÍÏÂjj‡O5>\…%Ûá}öE:|Â<±îÎÁvñg·¢ÇÓË g‘ý‚uíÿ³w×R”ÿÇß3[·{w{Å5ÍÍÑ**  (*`â»°»ãkw¢¢„H¨_Ä/(%¡”t7Çq}3óûc÷’Kâ>/]vwòÙ™goç3OEv¤GïY¼)NA‹ÙÜ…«¢L¸v–v,¼˜gc/·S¯²ÓZΧËeû¬ùÍq/ ¨‹¥œ¥…BáS¸Úsñéç„üÏérýÁ­o^¡Ò]ÿqRUõœ»1P“Hð+Î:ª=œ kW½ôCc‹fq=µbËÖÃîáÕ‡XòÅ‹|ü^ M#4¤{!4/<2e'go .#›Zæ!R´ Z—PÉÏ›ÎtsàÅv¼y ÖR>Œ)ˆ¨@/›f µµWªÚ`YŸ/gm–Œ÷B\¡YŠª ¹µïCªup*iìKËûü^R÷gb -10UKŠÖÍÑtíS‡ŸþÍêÀM÷¼ŒX‹‚ÇYÚ±p³=ÂFÎÁTÜù7PLŠFáQ™*›¾Â<{aüo }¶qùÖH¦àhœdp(½à¤®e3Ö)?fÕLÎ箂’ß³5 SJz*Ä(ôoá^œ‹VjöUZ“J·ÕM΀8ë(ΖômzŒ¹eÝþcO=ÄÎGq ˜lXôTvìË@/c9OʶìO#WwW7Õ…'°6Ïà—/çð÷ÞR`4Æ IDATìeW²F|ŸÞ„mžÊÔÅ»8’¼•ùgs¸Ñ…tŠð‡ªZ ÿþ³…G±á×)ü–Ñ”ó›ao|í¼‹øzö*v=NjòvÌ.úÓaªCç‹rhæü¸îÇŽ§p4Ý]¡Ÿ—²ŽC@“¾´g_M[ÊÎ#©Ý¿‹Ù*¡ñA_»œÍ‡S8¸k/…‚K%¸%ýZg1oâO¬?x”}+g2q…….5ªDûXQ]û½y"ŸÿBŸî1˜€R…•º½»¼alápê1îÚC:!㏨¶tûS³s×q´“MŸ–Ââ¯~$»Ï(úFÇ㑞¤k KL{Zçòïâ¼j왲U¤}«piOZÍäüqîª%¼þ¡‡üU•Ë~ø w VTþÖýëùßP8r6 PÕ÷FVUNnÆŠ³N¯;îåȧxë¡oÈÅDD‡yêÞó‰ jÁ ^|øñ·t{ãÿJY®¦Í¿ðþÇË9ª)¬ýoêG][õnÇÑO¿à½Gfã"ˆæ×<É£ƒ‡òÐØL>˜ø$÷¤›¨Óz ÷Ü}‘&ÿ]@#—?¿Ãƒ_f †µ`àwÐ=LE!‰›¼‚ÏÇÆ£s2AqÒüÊGyt¨£Pµ[Q}ïe\Úg|ùÖC|ël´º4¸ü;We‡À6ÜøÐ5|ñÙÿ5 lõøØ³\Ó÷jÎ_ñ/ûSt|zPÁö”ºÜr?‡>ýŒ7˜ŽËQŸÞ×>ÈèævÜ>=¦:8…=Ó=ÊwI¬•~,l®äáÿsññ7/pßxsd/ƽ0–ÎW_‚wÞáÁ ánå¹»{–ž>-µÔôé뙿1—£_ä–ÿú'&ÞÆÇO÷Ä)¿I5‡£5×\Ùˆ_¿xž×ëÝI¿È=Ì~ówr[ßÃðD鞬ÚÉùâœU¸ÍoYÓª—’÷¿Ÿ‘ÿtznw·¾2ðür` 7ЊIÊ«“2óÛÉÆÅ•¿$°äÏô4”_çΡÿ ¡,þãw’Úu<ÃIç’‘£F3eò¤êNÆicÿ“§ïû•>¯?ÇEòÇîlUV¾ÍÉÎbÑ ¸¨ÿ@œ!åµ=eò&³è³—xgÆ*¹¨Ûc$÷?|Â¥\ñtúnÚ¹ »ä›Z¥æi9?¢†*/O‹“³mËfÚwêPh¨£Ò_£X Yø½Q8=q½ŸJœWêú†?5 _úò^û^Ð!U¡ç§ù¶eä§Õ·-ßvJÚäU~ÎK†’ßY§‚‚‚j2áp2sÚ.6ü„”Ÿ­Ö®^EÇÎÝóßOž8ÇŒÅëõb:º®cš×‹×ëÅëñâõz0 `gÓ¦NáòáW²|ÙbºtëÉòe‹éÑûü í{Þ/s¥äW!D-aŽ¢×moÓë¶êNˆ(‘œ!ÎIÅÛüžÞßòÚÛ–š*ÚNÇ>J ÀO\¸XÙ7ùm~ 0Ð µ÷U8¡w0Qe$øâ RB{óÜ—½«;B!„g”á/õÌëý¹ØÜ2J~ËÜjIO%Î+ú²PÉm~©liûËÛNAÀ|êU¡‹ß (|EAU¤µou‘àWÔ8#G®î$!„Bˆr¯\üué ·Á-«ÚóÉÊw¸ð´ÂpÑ6º•Ý®¯sÁ“oJÁ ¥PÐEû~6(D°ÓYæ©äiQ›T$O‹“ãt†p`ÿÞJz+Vò[XÞXB•fH)²nņ-R|C © ªª **Šê{¨%N+ü0ùžM¾‡IU1©&ßt“ÿ¡š0™ Þ›òª ³Ùœ¿-EU±X­¬_·†FNúˆÊ“’_!„8ƒL&35aãú58DÇÄ¢ªrßQÔ,º®“|è +—/%©]Gt]+uYÉÓ¢6¨Lž'',"‚}{÷°}ëfâêÖÃn·“צµ|ÅK‚O·âÕžK+i.­ÊuimO,f>aÞ ‹`:)ÇRظ~-—& MËÿ8â´‘àW!Î “ÙLtl,V»õkÿa颅Õ$!N ª*ÁÁÁ$µí@dt4^§Ôe%O‹Ú 2yZœEQ¨[¯>©©Çضe^o¨š’†*ï9Ø¡¼ç“ØFÁ³QP5ùdžO¾èdŸKø\þg›=€†šÐ¤q‚´ù­bü !Ä”×¾'22šèè¸3y›[ˆSc€®kå ’§E­QÁ<-Nž¢(„‡GQÝI¢B$øBˆ* iš&ÕîÄÙCò´BˆÚFé!„B!„8ëIð+„B!„â¬'Á¯B!„Bˆ³ž¿B!„B!Îzü !„B!„8ëIoÏBq††×ëñõŒ[hüB!jEQ0›-˜-–r—•<-jƒÊäi!ĹA‚_!„8ƒ ÃÀãv‘rä(»wï$++]׫;YBœ ((ˆFˆŠ‰EUK¯&yZÔÍÓy à õX éiéhš·FßÔ1›Í8!„†G ª2Ø6Èù#Á¯BœAšæ%åÈ6mÞHë6m ¨S¡‹0!ª’®ëK9Ê¿ëÖ`µÙˆŒŠ.u _ÉÓ¢6¨Lž_à´wïnlVMš6Ånw (53(1 ƒœœlöïÛ˾½»¨W¿aMkU‘ó'*J‚_!„8ƒ¼»ví u›v4NH¬îäQªÐ°pìÿ®]CtL\©‚äiQ[T4OK9ŠÅd¦Ib3 U~Ã0P¥H bÞûÂÁÊ©–0|ò¶WÒô¼ýÛí›±eÓR¥Qç”ÒPÛÉùuοkW¯ªî$ˆB’Úu,qúÈQ£«8%BœS&O*qºadffQÅ)¢ò¢câøkÉ"(£Bò´¨M*’§ŽK¡ió–ùïó‚”Òà„ êT•¤•ÈÄ׫ÏÖÎùàIΟ¨¨s&ø…Ò.QµÊ»QZ!DMU‘›6R-TÔªªV¸ý®äiQT4O»].Ž@ hàRRÐRøµ®ë§@奯p Tx^áïZñ´8¸=îJíKóz¡¢¥Š‚Él®ñß÷såü‰SWãƒ_¯×KòádRSSÉÉÍÀ@hxÑQQ˜Í5þ#!„BˆL/$˜J ž‹8§¢¤ ©ð¼¼é…K2 Ï×*¸MÓÈ=¼‡´/žÆµFÖ ¨×”Ð1Ïc‰ˆ!õà~¢4®Ð¾ªÚ¹pþÄéQ£#Ç´´4¶oßQ¨†ïÎIvNÙûs8|è0Mš4&$$¤úY«xI^ö=ÿ=ÚšË5Å!mëk=Ïá%Ìž—Jû+‘PÝ©9Y’/…BT¯Â âŸÔ(@áÅ0Œ"µ¨Oùg«X@TR™lþ> —`â®*üz<¤Œ’ U§É°PÊ)Í54ƒÿ®"õ›×ù3;€ôÃû^ͬw:ÏŸ‘¾†ÉŸüëð{ÑØZŸÄùÓŽ±êûïÙVu%ø­R5¶CZZ[¶lEÓtÌ–B∌Š#2*ŽÐ°(Ì–4MgëÖm¤§¥UwrÁÈåð¦¿Y½/»H¦÷ìÿžÇo¼·þJ+ñËPµ<øc?-?DE*XxÓv±fåFRKïâÜbdòÏwpÃÃÓÙí9Õm•œ_*˳³^Éaw)[qoã«q7r÷øäœÂ~Î,É—B!ªWáÒ?@Q0ü¯õB%wº®ûJðüóÎÄ#o_…K KZ./¸«hÉ¥æõ’±w;±I]ðîX‹gË*<[ý-y•x¶¬Â»e%ÚÁDºy«7°gí*ÞñP¹ûðh¹/™.7é9¹¤df“œžErz©Y9däºÈv{py¼xOcÐw:ÏŸ–µ“åKþaO–~æÎŸ7™e³g³pG–”üV±YòëõzÙ¾} `µ9p:C r`·ûжrrr1›-¤§ÇíÊfÛŽ$µi}RU œ½,ùþ;~Y´†íÇÜ`rR·uWú »Œ š:1UtC®ÌxãMv y6uùë©Ö0bëÆ欑‡º ©Ë>å?S#y䣄Uø@Ô¹øè—ø35o‚‰ èÚöȃ;m-í®‰ÀÈ8⽜êmÞRòËi§Ø©O\3¸*uçË“¡áû±—óšíY~|¿/!ÙK7èav÷™ÏîEx¡Û›¹ë_cÄm˹ä‹ÉÜ–hŵù#n¾e »òn"˜ÔmÞž^—\É5Ûn.eEò¾—}ÓïæÆwב럢ƘÔ~WŒdXçNü:d,{–ÎÇyíL›ˆÈùû†Ý3ŸìR>jÈà™ñpJY¿DF;~ù„w¾þ…öea`&¤~οîÆF~Ê•åíïÁx?2šg–fäO·…7¢mï¡\7fmÃLÇgó‰Û¨7f_ÝÐKþ”\6¾7бÓ\\òη<Ú!°H‰ƒž<‡±Ã_'mÄÇ|}W+ìÅŽ[Ö?o0êî9Ä?ó#ï_rê¥M§“äEÉ‹µœ®ë¾R¹¼`¥xÉ¡^þë3è„Þˆ)¡äPQò_W4øÕ ª×ƒîqa` ª&´Â5åíˈѼ3S'ÍÁL7Þxu›'•¹ý¯Wmâ­sQÌ“ ÅdUEQT_zýé7 tCÓ¸oÈ…ŒîЬBé/ó³yŽðÍ-C˜ÕðY¾|¸-ùçÏÃŽ ·sÿü6¼øé]´ ¨Àù+ˆ`+Þ6ºðê9y3ŒS¯v-*§FFdɇ“}U3v{ v{œÎ À×y¦éx<¸Ý.4¯—äädâââ*µ=}-_=ó*óŽÄÐqàUÜÖ8 5cÿÌŸËÏ.gÃÏqGˆS L‘½¹óùÞ§°qÚé.ާADßÛ¸µWFN:É[3gæ[<ºí^»¿7%tÅNÓób•'øXâôÀ ªîtˆªc@òOópÜxÞ».±Ô5†ë8©Z0çÝÿ Ãë+ä¦bëŠ_™ùÚÌúe,ï½6šå]Ò¸Ó‘kNâö—n¦™ÅEÆ‘¬þ}ïß?‡¹#ÞàÝ;:R¸Ž‘v˜ß&,$;ÐBæw_±âªçé¢`kz¯½=·zÚRÞ|fæk_àÞ€Š-ª±/°(eý’Ò–õ÷[ÜýÒÏX{^Ëý7·$”ÁþëI ÄQ¡ýe“u,\ÏËv&È“MÊîåÌÿ6w®8À‡_ÞI›¼æ oàåq ÊOŠ‚=.¦È¬žú_ý˜L@üöÅÿ¸¹Ý`¢ ïñ}Ïx—Ÿ†Àð¸Bk{ö2ûÝ9¤Îä 4Bjæxa’óÓ&y±æ3t½HÉ]^ÉaþübÕϨbûÆ_R©š^¸*oE«=ºŽ×Õƒas`JhwÓrðæìÏìA-{ð휹lÙ²•F!vZö¹¨ÜíL9Fý˜hT‹“Ù‚b6£šL(Š©Xð«¡k†×ËšM[NKðkèFÑcVürbél©ÛªÄ²%ªàùËß—¿UªFþ½JKKCQLf+&“ “Iõ÷ÖçË2ªªb2©˜L&Ìf+šW#=-½rÁ¯‘͆©3/¹!#^x’¡ mþŒØ…žçõ¤écÂø¯èÚê>º†x98ï3>úy»gà"[ÐsÈ(®¹ a‘6Šû§>ÀµS}¯›ßó O4YÊwO!ò‘¹¯M`³{“'ÌâÏ-ÇðX"hÚû2nu N¹8ñÄϸ㷯ùrÖR¶§iX#¨ïÒ Ò?_OeÉ/0~ÙarsX}®º•ëzÇܽv¯â•1þžl#ñÒ×·w/¼öÛÒ½€ƒ¸¹iìZרZô%rÄ&мY´ïæF»4·>ÆÓ~åïÔž\äØÊ¬O&±`Ã.Žf`k̈ç¢ù÷óÜŽKyóå¾z÷.^ÝÝŸ—_A €ÎÑyOq÷äƽÿIÉÓË=NÅóËS]Ás„3¿ä›yk9”«Ü +CoºKK¹»¾Ÿ_Þz€Ï·&W ¦A×Á\ã%4RÁ³‡o|Œ?:<Ç[×5Æ’¹ŽÉïLdÉŽƒÏ5€¢ÛöãúÿN;1ª{Géç×ÈÞ|Âq¹â®ö,y}uî}Ÿ‡;ù~3WðŸ;Þ!ãºwxþˆ¢í+$_žqaI ìÿo7ý‚‡º‡•Ѿ%€èmißÌ@÷ qY¿{×§<õEG&ÞÕ’ 5%·Dдm{:8ºqÞ€Ë8ùnnþäiÞé4…'º‡ä§Áµý;¦lŒãº·®góÃ/2á·Ct‹)¨!I 9D0`iÒŽŽŠ–*•ºþ ‰òp`ù?¤™»òòcéäŸ|þ%þ!åïO÷—Å…4¦MR[_Ic§nt­—Ê÷Ïå»M7Ó¦Yg#Ú´kG‰±^ü÷+–Ù‡òòƒ^zd"³wôclBAY¡7íi8ˆwl૯×2àáøb>´Ÿ3i›“¸€t2g QCÀ‹‘¼’k½H‰\)%~yÓ uªT%Å— §Ë0òJ½‚¥“ºaà5 UÅÓ¬ W®æ¼Î=Ñ×,ÀpùêL(Ž`hÝ‹oçüÄ–­Ûhܤ M•ô í#''‹Æ±1˜­VL+&‹Õd.q¼[]ó¢yyü5~Û›…ŽŠ³QOFÜ~ —4¶û>¿k? ¾þˆÉ¿mà¨ÇDxã̾ïNEÏŸ8=jäß+¯®c*ôeÑuÇCNŽï½Çã)Ò­¸ÉlÁ[ÆÀå%ÊÙÌ/KÓ ìqýó_?K,ç]}sžžÇÏkÓ¥·ƒ´íkØ–Óšëî;8S{WÍeÚçϰ×ó Oô/¸£Z§ÿ8î;¿&Tì‘vŠ×cÒRþàg>gK£AÜø@!ékøþëñ<{ÌÌkãz•\âxR4Ž-þ€ç'l îÂQÜÛ1ãÐ~¶­`5&}®â®‹ÂV3Øòß Lúä#4Š‹óS nzâZš€b !Æ jö ¹¹%Á!x­`Úø¼óMsÞÛ¼b'5’ŠÕa¼¸¼FÎ^V®Ü‰cЭ<Ô6Õå%²Ž™‚ÖåvwOÄ´r5§A¤ ŒL¶­Ø‹Ú¸? ¦ §ò‹‘ÍÆÉ/ðÖŸ‘ ó8"2X3ó3&ýçKbߺƒöA%ýÐz0âÏç†Aõ°§­gî”oxñ ¯?ÑèbK®ÃlØp!·1¶eÞ£ëùiÒ÷¼ùQ,o?Ò›p•2Óm-é¸$“1“+÷àêì»@uXÃNo45u»Ø•|Y"/zœ;Z<νO=Kó ¯1,ÞRþJ¨·½ŽÛºÏáñŸf°áæ§è`;‰($¿ƒßÞÉÏß®âîn} õÝaÍ7s9Þþn†¶ëÍ¡aÜöí÷ì¸ô+ЧHåÖ7Ö8“w=?ÎßAÇ!±Ÿ–ëTKp(v\dº*q·ÞµÙÓwÒèªçèÜÅËð?1cÚzF=ÖßàîãÇpY’¸án+o¿ú ¿^ÿ!—ÇšÀ³—>Y€©ÿS\{ø9ÞöµäE¼X;o3zB©aIMUIÊŠ•òNƒÿuE«Íº¯Ú³b `Õúü±d ‡Ž$s倾«“ÚôæÛ~fËÖm„‡‡qÕðá,›üA¹¥“ÙnÑÑDcÂb Àb³a1›IÏus$Ë…ªšˆ ¶lSq»½¾ôéÁvÒr\„ØOæ ^ ¬ógùW'uËßlÍiËMõ%ޔŞs˜òñ£ìq¿Í³ƒb‹Ä€g³Ÿ’IÇ:rõÝ£if;È’©_òÆc¼}'œ ÄÑãê»é‚5gÿûòÆ¿EË·GÒÐ’Á?Ÿ<Á; ô¹v½ë›9¶á7¦îð¿Rò[¥jd‘ˆÕbÁb± i:^¯·ÛCn®‹ìì²³sÈÍuáv{ðz½hšŽÅlÆb©èª÷ø^¹!¶u|‰ƶ¸VÄ«Gv#¿o£àF´kŸDRûî ó÷ö°±eöìpZ/,ŽúõëS¿~]"íů‡½ÿÅZµ#·ßw5ç·oEûó®á¾[;`¬žÉoûOµ¥B´d}¿o‹1õÒß¼Ú¡yǦÈùË›“w,‚Ó¾c[ÚvìÁ¥·<Á½llš1‹­¹zþ±Á¿~öÆÌÚJÿîáŠ^I´éÜŸ›¹‰¦é øfÑa¼†Ž¡†ÑºkZ7mBÓ¶2zT[L‡Ö²#SGO]ÉŒ…iÔ¿öQîÒí»pñ•WÐÉ™—ö³äKTKÔÈ’ß À@\n7YÙi¸ÝnL&†&“ïb^ÓtÜn7n·Móàp„`³VäVmaþ¶å7#*™DÓn0-ÙÎö4¦ÎŠì2›½›ŽBÝ+HÈ/ÁSNèH]þf㾌ú–ÓÓq„;™ÍÉÝ« ÎÒnqh©¬™3‘é ÿeïÑlpØðॕVÖ½LƒÜ= ™2é'Vn?Äq»â‚0ÞZVkcÿÔG¸ijÁûÐfý¸ë–a4°@Eþ )¡­¹ ©Ê§üKzŸ^X·/fƒ·1#Û‡c:Éãä>²…šÆñîdÔGEçYRs1°—“?µ%–Ul=˜‹QnKÎøH,ÚqÒruTN"ÝVê÷éJø¼¿X¼g4‰M²Ø¾&GËë©WüF®äË*£X0üéXyÝË<ùEÞïU™µOÏA+Z“KãЂoYtïµõU·ÔÀ•-¿äƒisgûÞeTÓ<Éõ-1\0n<=¯ÛÈŸ?ÿÀìïžà†ñ öü[ÜÛ»NÅûsXû4ƒû<ÿÖTïŒá «¦Ê³‚¢'ÔÀ6 ß(Eþqm+Zò‹¯ä×È8FÇ6Ø´e ûö`˶íÌð/³eÛvP† Hœ#èwøÖ)çÊè`z6‡3÷P/j{wD×tšFf±Í00¼Ì{6z»³g*û•,·›Ísqó•>2'|¶ücà p zxÎ{ퟮçO_ÏϺHÓîM0-ÚÊÖcš^Wwst˲,éXÏZPó4¤9íb`κäö¯ƒ-c3?=™_þÞIr†N€]C£!.Fî±M0BéÖ<Õ0Ð µPµlió[µjfðë †ŒLl6 .M󢪾Ÿ]×p»=¸\¹X­l6AÁAemòæzÄX`ëúäö-Ö“!¸mà€u†SZ™²ï߉”NÚi½HWü㜥lVãÈïïòÚÌºŽ¾…ëZÔÁrü/>xíû²7›»™‰/gQÔ nוúA¶÷ãOO“*Uçâ;¹½g$V[ ¡u" wT²Î¹J› ›£~ü?ÖoGÈ‚uxGÑ>L…ÜM'wœ ;o{Œ ßÐQ°…9+xc¤xw å| Õ„š÷³v’ç×Z¯=#eáÂ]\“Á²]fš nXB­ É—UÉÝŸG[ʵO<Ëç¡-+ö'ÆÈdûªx BNáŠ6g+ö‚)!:fÀ³›_fnÁ›²…Û.ž]tYõ[–íÉ%‘eìï¤×W°ÖiÉ…×¶ä«®aöƒ7ñÆËqAÇ'ýíB+ ÉXÞ~´+Ùÿ2á±7Y€ÛÂ$ IDATÓ…óZ„­:OBÓf%MÚ‘?™¾"—,ãQ÷)6sÆ<öM}‹Nvj8 PT"zßÈ€ïäÓÞǽ.‚a㻢*¤8í¹ÇÉ©e×J’Aòbͦy%¿†¿±¬8o:œÞ‹7ïÒ]¹ÆT%¿­±®ë¨þqzóÒ^C7𢠥§ ¬[ÈÈaC™2kûøà<ôêIBD î¿ça tâEÁÐËþœ-ê8±Ùb9’q˜OÒŠK%3+¯xÓ Àl#ÒP@EU°8œß4XÇ©ß=Ñ« <Ù¹xu#ÿüiäf¸ÁdEÕ —º®ùëçÄyC!zþÐHyAj~œªë¾scèèÚq–ø"_mjΕ7?J‡ºv²×|ÉK“\þí’·m/ÔEªiWðü‰Ó£F¿ÁAA¸\nbc¢Ø½g?999x½ž"Á¯Çãtâbc@Qª\ð‹£ý»ñÏ’©Ìô—Ö·„ žCü1õ³¶edÛJª>šËÞÕ{ЭibՌݮÌ\t(ù.®â n³˜¿’í™=éì ž3¶­b‘ô¯W^©^%X£i ÿ¬ZϱKãˆ:!AoÚ‹{)Ã/î@œÈ»ö)DöŠ/}ØŒRéß°œ}Dsi][¥óÓIŸ_K]úö«Ï³~åïæ^6«-¸­Y tI¾¬b*á½ÇñX¿Ñ<òá| ¦œåuÒÿùŠÿr=â ZÚ©X5ˆâŒ6OŸ_Òœ\xUGœ ¸¶ýÈûê0øÙ_ðÓcd®âÍq1}á!ú]Wê÷ñT×ÀOçžñð÷nöeét¨èW`< M›¢$ðø‹»¹éž7xzZ>Õ¨ßQ/æÏä_G/~õ Õ†pmþœ^›ÃO»Fpk¢FNZرª€½W]ݘÙ,DMz˜Ëšøú$°Z!'­’‹¼Xãäÿšÿì  (!>Ó]•f*…Æ­UUU}iTJµùõ`hZÚaÔ0ò²!L™õ=û e³fôlÛ ×_?‚¦ahšoröhaÎÄ9dd¤£ªÿ¢ªj)ÇÈ×ÇGÞ˜»G‚‚èqË-JYtl4¬€¶~ [3Û“d÷×ñÌÝÍŠõ(1M7ù{™öE¶º†¡ä²÷ï=hŽV4V0Ò|GZ×t ]%´qCž ¬Ü•MR3ßm}íØFV†¸þÑXÜGؼËEH÷ËÔ5{< +‡04 sD3âMóX»j?¹M`SÔ¼~¸ŒŠŸ?qzÔÈàWUUÂBCÈÎÉ¥Y³&ìÛw´´Œü/‘a„†:‰÷ýxÚòï~U˜HëQ·ÒwóëL}â vîG—&á¨é»ùû·ùs—ηÞH×ÐBÛ=°és‚èÚ8×öùL™ŸFܰKHŒ(šÅ›ømÁL~nÒ¤Ú‰ž…wj¡~¿Ëh3o<¼=•ë/M™¶†ï¿þÚÞÂÅþŽ@<û¾ç¹'g£}Žg†ÕÅ‚AÎÆ¯xôåeÄ}‰q½Â1¡qlÑÛ<òé~º=ú"7¶(8›¢è9¢s^ÿšßÏæÊó SÓÙ˜¢Í—–ÈÄX¶€Y¿ÕåÂÄp,ÙûÉB|ˆÀºM 7~á» ±ut’™¬‘ع> =¬˜ó=ªˆÒÙ›R™F\g%¨%—t â¹é!¨'¶FLÎrŽ“¥äüÒ;±=—ŸÆK?¼ÎÛê\Ø"Kîö¥×å¼¾ ”<ÚÇq¶¬^KH¤ï\¬oÿÅÜñúÆY¨lO$妻ô5‰ê1˜6Ó?ä³ JË{i^Rb%_V=5”îw>Â…Ëe~zñ™¹ø÷oV¦«ä¦dÛŠyÌœ»–œ6ÿÇ{7µ¨xGaž£lú{%†ÕMFòVÿo6ß-?Bã¯sO×TrØ8û¿‰Èå½ZíH¯Ãð6ŸðÂìyìz½¿çôâNf}»g¾Â„ tjÓ€H‡BÆÞÌúbÔ½–'U’¨Üþž½æ/nùäy¦õü„Ñõý³Ò¶³z•ÓßaiG<-røõûõ¼‡ ’š™Oüpº}ð?¿…ÆÕ#;Ë Ö@l €™¸w3fǯX†]à†FÅlƒœãäÔÆ^†$/J^¬Á i¯žøæµ·…‚Øÿº¤à· 8ö°gæ³<ýKW=ÿýìÿðþc²½ýý¼4¦êæ¯xì•åÔû"÷öðõ€^f íïéÙÈëÝÙÿl `¨þö¿å”Êæ§¯¢¢y<(€–šŒº~!#‡]Êßÿn SÛ6¸WÍÃp墨*^Q~érdd$O=ùD…Òq&º…–ÿt&¯>«rÅ€vĘRÙøûwür4”óon‡3¿d8°€i³ti‚{ÇïL™œ¸¡hlÑ0,N•\¶.ZÎöºÝh˜x)ƒ.eÚÛïáy1M’ùkÆ$¶÷â®a(ª™F±fæ.›ÍO-Ð*ÊŽgÏq<øo ·cDÿhžžõ2oh#è×: [Î6ä@eΟ8=jdð `±XRU¼^ÍQM&\._‡76›]ÓðxSЫ¶b %TÍ`Ñ[±ÀL|³ xð®˜Dx…~¬ÁaØ¼ëøøÑû|û Œ&!©w½1’ac°*`d®cöÂ4¢‡\LƒâÅSj†´Åôüü´ën+¡«Ý“[_G %gõ Þ™–L€­M»ÝÀËw^W±}KüÈvZ\ÿÃæÝÉoþÊÅotõMßý5OÜ÷uÑec¯åÓ§\ü´?ˆ6/lµfp·@þ÷ûl6Œý?2\` µcöwTg{nx¬}á‹5О 2<…ûz­=$/J^¬©t5Ö¼N‘tÿ07%U}>aÌßÚäúäo“B¯ :`Êë™Ø7E)¶ Åì(¾ªÐÅÒ·ýŠ–šLfœñ Ys0™¤Ø0,й™([–Ò-ÜwÁjl^†âÍÁdw ÿ8†³^cÌæÊu,[Õt]ÇÚøjž~:€¯'ýÌŒâÁ„³~{.7šK[9|U•ó޶ÅÄÑ¥SyÇMßvÈ8Æ k€ÅÀ¬kÉû?Lä».í¹/)žA>†iâ$~üø5f6¢Z]Ìw_AÛ 'n¾“AŸMáû÷^f€ÉNh½N„š tÃFÂUOòXð$¾ùe¯ï,Ç4§G=Æ)ù­JÊÌo'T¡…—ü¹€þƒ†òëÜ9ô4”ÅüNR»Žg8‰>‹‹¿S+ÛÇã.g¢Ö®^u iu±éã»ýã»&æ´ Gtn*ë\Œ5š)“'UqŠÄé¡sôÏóÀìž|mMNö‚®*+ßædg±è\Ô ÎÐ*N™•÷Ý´) rwÉ¿³’§EmS^ž˜?ï ¹ ·Û¡ëhye±Ò_ðËê LáL´ù5ðm¿È-…Bï}ÕžM¨ª‚Åjeî÷³¸ðâån=7'‡CÛ6³iÒ›¤ìÛY~j•ðº h>j±‰Í±ÔÜ+~þ\lüä>^Ú5ˆWŸDŒ©öœ¿³ÅÚÕ«èØ¹{þûÉ'p㘱x½Þ‚vÖ€æõâõzñz¼x½ C'Ø´©S¸|ø•,_¶˜.Ýz²|Ùbzô>¿Bûž÷ËÜš[ò[œÇSù€Wqæi{ØyDGOþ‹)“ÐzÌÝ4:‡_!„µ_Ñ’ß¼¶¾=ľy󔂪ȧ‹¡TsÖuÿ¶ïξªÎù‹:†¡Vªä7Àn'&¡Ž{_Áí.¿i¢(X,Vœ¡a5:ð…Êœ¿‚^¡1´"Çô”áó'NZü !j"ƒ¬ ÓxéÝÕäÄÒùò¹¥{XÍ@\!„(MÞ¸¹F~ÅX¿…K µýõ?+§±à0o{:F‘~nEñ £øzJÎKaþ¸µ•žìvìÅÇ89 œäù;÷UqþÄ©;§‚ßµ«WüÊÝÆòH7H^·ŠäÓ—$Q‚‘£FWwÄÉÈ=ÈŠ)ϰbJu'¤fÉë•SîìŠÚ "ùTò´¨M*šOÍ+ÇSãtø£ðuœ¼ø†¨1tÅÿ ?t*ò;¼2ðW—-8©þýþg”üÆÁºaz,«ÕvÂ6Ï5f‹•+®¸²BË~5áK¾.4l”œ¿sË9üVUÛdqj¤½¯8Û(Š‚ÃHJÊQBë;9B”)ùð!‚Î2KC$O‹Ú¤"y >¾.›6®§K×îx ÃW¢«€b2ùª­** šÔrz{®¼’¶Udšâû§x§[³…ׯ£^ƒ'½ï³E||]wïüy½m})ÔA¬Qt¬äÂäü;ΙàW!ªƒÉd¦Q£&l\¿‡#è˜ØÊÍ&Ħë:ɇ²rùR’ÚuD×Kï*^ò´¨ *“§›²tÉ"–.þƒ­ÚqbPsZ;·*Y~©a~Ð'¶I5 ƒãDZjÝZ¼^/›$žñtÕtrþDEIð+„gÉl&:6«ÝÆúµÿ°tÑÂêN’'PU•àà`’Úv 2:¯ÇSê²’§EmP™<  šLôèÙ›mÛ¶°lñŸäääTQJ+OQìv6jL“&‰e|Žó'*J‚_!„8ƒEE!22šè踳a8Lq¶2@×µrƒÉÓ¢Ö¨`žÎ£¨*‰M›“Ø´ùN˜8äü‰ŠàW!ª€¦ihZÙÕM$O !„¨m¤‘ŽB!„Bˆ³ž¿B!„B!Îzü !„B!„8ëIð+„B!„â¬'Á¯B!„Bˆ³žôö,„g˜ax½_ϸ†aÕ$!N ( f³³ÅRÝIB!Î ~…â 2 ÛEÊ‘£ìÞ½“¬¬Lt]¯îd q‚   5N *&U•ŠaB!Î>ü !Ĥi^RŽaÓæ´nÓ–ðˆ:XˆG×uŽ¥åßuk°ÚlDFE˾B!Î:ü !Ääõxصk­Û´£qBbu'GˆR…†…cw8øwí¢câ$øBqÖ9g‚ßµ«WUwD!Ií:–8}ä¨ÑUœ!N)“'•8Ý0 233 ˆ¨â QyÑ1qüµd(Õ!„âô;g‚_(=àU«¼¥BÔT¹i#UEm ªª´IBqÖªñÁ¯×ë%ùp2©©©ääæ` 4<Œè¨(Ìæÿ„B!„BT³9¦¥¥±}ûŽBíŽ|%'Ù99dïÏáð¡Ã4iÒ˜êK¤B!„Bˆ¯ÆÖÃKKKcË–­hšŽÙ@hX‘QqDFÅ…Ù€¦élݺô´´êNnÑÉÞ·žë’ñœÁ½xÓv±fåFR¥¯“Ú˽¯ÆÝÈÝã7’SÝiB!„¢¨‘%¿^¯—íÛw V›§3”  v{99¹˜ÍÒÓãve³mÇN’Ú´>©*ÐFî>–ΙÁÜ?ײ3Õ P§Iz»–Ë;„c:½íåòï„WxË{·ŽÂRZ‡$ž#¬øîk¦ÿ¾–}™¨¢š´ç’1cè_ÏZÎ>4R—}ʦFòÈG-«Y@ä3ÈÚ6‰“çò×–\€%´.­z]ÅØí UìÔ‰‹'®Ž£†åa!N÷‹Æ¿Ì»3VpÐe§^‘ÜÿÈut «±÷q…BQƒÔÈà7ùp²¯ª³bÆnÄn 0ÐÓø:äÐ4'·Û…æõ’œœL\\\¥öcdn`Òs/ñóþ’ú_ÎØ¦ÑØ=©ìÙ´‘4¯ZK;»ô°sÆË¼õc-/ÅÍë`Í=Ê®-É8-ôãËøàůYу+Çv¢^Aúþ­ì0œ˜S<ƒxAÕP!N7Û'>À£“³¸ðöç¸7r7³ßþœ fÒ{ɯ‘¿fB!„¨IjäåBZZŠ¢`2[1™L˜Lª¿Jð¿&“ŠÉdÂl¶¢y5ÒÓÒ+ü9lúö~Þ_Ëž}Š+ìùÁn×Þý|/r7òÁ]/²kÈë¼ri &@;<—GÇÍ¡áïr{‹<û~æãñ¿±nÇa250Å\ÌS/%ã»Ï˜¶d{R]@ mƾÄCçE@Ú¿ü0a?-ßK&bÛàú›‡’j-…¥_¾ÏôU»9”î‚êvâÒ1cÜ4¨ ßú·Žþ{÷ÇøàΖäÍÓŽ³em24Ãí#/ Ü_ Ò¹WáÏîbÿ¢oø|úB6¥x0…&ÐgÄÍ\×§.¶¼¸Wñʶƒxékhh©øág–ûà?lqsÞÿaX3›obÇ®\·€gß>øtxŽ·F1ÿñqLÜ[l#nâýçú®doŸÇWæ°dGš5’VdìˆÎDÊ95Eöz¾™¾ƒˆaòð5m°cÐ:p;#šÌŒ­ƒ¹§…­ºS(„Bˆ®F¿^]Çd2£(¾HL×u<99¾÷'(_lÁ«U²jÎf~Z”†½Û- jb?éR^Oò?,ÛªqÁÍ÷Ó=Æ‚ÛD¬šÎßË×p8f8wm†SÏÁ¨‚ɽ‡™/ÿ‡9j_n|d ñÞ-Ì?…7>¬Ã›ô!BÏbÏú­¤Ö»’{5Ææ:ȪY“ùæ53õÞ¾vþÖÁ·µ#HSP4E.ùLÄÆ9`Õü¹½3—&kØ­“¶òSžùxÍFÜÎS­9²ô>ûô5lñ¯r]‚?K˜ZpÓ×Ò4K152§œ»,¡õ‰` k¬&¹Q¢¬eä`S8½ï{…V.ÐI_3™×§î ù…­qª %ÿΛ/LähçÑŒ»¶ ¦½ÿcÂïðFð«¼0$®fþ‘çÏáÕü›@«Þ± Üò|•¬þ÷Z‹X©â/„Bˆ2ÕÈëZ«Å‚‚‚Ë­ãõzq»=¨ªŠ×ë pó¦y½^4MÇf5c±Tî£xïá b[×%à”ë7‡Ñ¢c;Z9ýòìÀQ¿-““×Ê6gÝ~ÚLj7®å‚Є˜kWsÇûÿcCzozûƒ[GÝ6´oÛ+mhy„žø“eûÜ´kæßP@uë×ÇYbº´¾þ.†ÿ€oŸ¹•_{Ò÷¢~\Ü­ !f@;¢™at{ˆÛ‡$áP yƒ1ì]ñ‹íãꄆ¾Í˜DÕ¯Oý€’ö!ª›)¦wß¼‡7¿|{ÿŠ£ÝyÒ¯ßy´ (áFŽ™ èºZÊ"¾½‘Àóàöó£0ãfǯsØ4€çoîO+д.7mZÁK‹VqdP±Qˆ@K?D:ÁÄ8 þÖ+¶:ÄÁªƒéx‘àW!„e«‘ÁoP` .·›¬ì4Ün7&“ ÓÉ×DZ¦é¸ÝnÜn7šæÁáÁf-¯#§â |•¨•*jÛë%uÇ^r8À”û¯gJ‘y1ÉÒ ðĵ,aõ '›Ô¬Š—l«Î6\õä ܳš?ÿ7ŸyŸ>ÍwÓºqËã·pžó0AöÞW¹yYÑõLÉHϵ„b%þ‚Ûy½Ç•lþk!óçÍâÕÿN#aè:„²š Q•,1íiœË¿‹wà»%j¹a![õHÚ·ªiÃÒ !„¢&ª‘7ËUU%,4„ìœ\š5k¾}IKËÈïË0 BCÄÇÇh@U+Ç+Á´}]6~Ĥ'žbû ‹èÔ¸z&‡wnáPÌ ®íC—®ÑÌúþKÆÿp5}cÚEÆI~.G‹¡ ˆ_ÁìwÞ pÄÚÆàM=@JH.L «ÀÅ›•˜–±0ù'¦Í¤O”‹ä̺ôêU¯ Ó+ýK¿˜Ì¦:­h^¿&G7/dÆfpž×’:Ñôº¬#sÞŸÈ+æry¯&„©YÞç¢ÙÅ}¨kU¬Û”pã¾›±[G'™É‰}:-¥€5†kûw|:ÏCb«&Ä„XÑ3ö±rîyísŽîLý ôC‡1µ½ˆ®Q5òO„89ZsÍ•øõ‹çy½Þô‹ÜÃì7'·õ= O”žž…BQ¾{ek±XòwrÕ¢y"ªÉ„Ëå+¡µÙ¬èš†ÇãÁb6¡šNîž¿)¢'w½μ³˜÷ÛD–fj€g\"/ÑбPÿ²û¹5óS¦Îø€¿4ÀH†­iì<‰}Z2ü‰Ç°Oü†Ÿ'¿Ã/P1´Þ† *üšˆ¾àÿ½ù¦õ&‹±Óí&Úv¯‡-oeÃÀ;Lå·#Ùøªx·x£®h{·ÛyNŸÎWßÍåãeÙXm|>·öéM]«‚¥Án¶—OæŽçÕyà¨×—»ºv"Ú"Å€5ƒ¦8qÿï?ÿã€"»rÍc#¹(Ú… ]»X´ÎÞ•|úìÊ‚éaýyáÍki݇ž11yÒL. ¢š\ Á¯¨A¬4¹öu^Î}‰w>zŒ_ÝÔíq¯?|u%› !„¢”™ßN6.0¨B /ùsý å×¹sè?h(‹ÿø¤vÏp},+§V·§”ªÊ¥X»zU•¥U”­¬s1rÔh¦LžTÅ)âÔ”•os²³XôÇ.ê?gHh§LˆÊûnÚ¹ »r¿³B!DyÖ®^EÇÎÝóßOž8ÇŒÅëõb:º®cš×‹×ëÅëñâõz0 `gÓ¦NáòáW²|ÙbºtëÉòe‹éÑûü í{Þ/sknÉoqOå^!„B!„jh‡WB!„B!ÄéTkJ~O‡µ«WUwDŒ5ºº“ Äi£( ªª¢ë•°Lˆª&ùT!ÄÙìœ ~¥½oí í}ÅÙFQŽ@RRŽ^ÝÉ¢Lɇìt–<î¼BQËIµg!„8ƒL&35aãú5<°_JÖD¤ë:‡ìgå_KhÖ¼º®Uw’„BˆÓîœ)ùBˆê`2›‰ŽÅj·±~í?,]´°º“$Ä TU%88˜¤¶ˆŒŽÆëñ”¿’BQËHð+„g¢( (DFF2\¶¨© ÐuM_!„g- ~…¢ hš†¦IUR!„Bˆê"m~…B!„Bœõ$øB!„BqÖ“àW!„B!ÄYïÿÙ»ïø&êÿ㯻KÒ4Ý-݃QZvÙCŠŠ (Š[”/~Qœ_Çω~Up‹ã«ˆ[Á ˆ ˆ¢ÈŠì½(£ƒÒ‘äî~$iÓ(mißO‘äîšûÜåÓôÞ÷þ ~…B!„BÔyü !„B!„¨ód´g!„8ÍLÓÄívyF{6MLÓ¬é" QŽ¢(X,V,Vëq·•:-Î'R§…õƒ¿Bq™¦‰ËYÄÁýؾ}+GŽäaFMKˆr‚ƒƒiܤ)1qñ¨êцIgŠÊÖiqòLÓ$ëÐArsrÑu·Ü«$‹ÅBhhá‘Q¨ªRÓÅ©W$øBˆÓH×ÝÜ¿Ÿuë×ÒºM["£ÈE˜¨u ÃàÐÁ¬^ù7¶€¢cb:/µÔiq&8ÉÍNµ IDAT‘:-NŽišìܹ[©éé:P äŽÇ4M òÙ½k'»vn#9¥‘œ·j$Á¯BœFn—‹m۶кM;š4M«éâqTá‘:¬^ñ7±q G ¤N‹3Eeë´89‡ÀªYHMk†éíþ`š&Š¢”Êû^ûxu)C\6põ[EË}ç"0ÐAÓ´flX·†¬C‰ŒjPmå­ïêMð»bù²š.‚ð“Ñ®c…ËÝpc5—Dˆª1yÒ'.7M“¼¼<"£¢ª¹DBœ¸Ø¸~]²Ž‘„:-Î$•©Óâä:tôæ-‹_û»£@¹ ¸.9VÀ_ÑM€Ää6®]#Áo5ª7Á/=àÕëx7"ŽDQ[Uæ¦4 gUU+ÝWê´8œH'ÆYT„Ôö* ôüŸ†Q§`_ýòtý×ùW–=/GN—³šJ*à ~Ýn7™û2ÉÊÊ¢ °€@»ðÈbcb°Xjý!!„BQ§~AnÙ€·¢eƒÂº¢¢ ×o¹Vܽ^‡ÎÅ™ VGŽ999l޼ů†çÎI~Aù» Ø·w©©M «¹B !„BQϘ~A›â}í À?Ì3M³TËóº“÷Ê´õf.>^ÿl8ÞàX‚ßjUkÛ,åää°aÃFtÝÀbµCtLÑ1 „GÄ`±ÚÑuƒ7‘›“SÓÅ­&ù»VñûÊL\§q/îœmüýÇZ²d\ˆ3™Ç_ãngèƒSØ^©JR=uJ!„u‹öÖPLïsÃ/Ûi†'ëé]W®ì-üùëjêUÿÞ¾ãöÏôV´ïFA]Ê‚Ÿ jeæ×ív³yó@Áà 44œà`v ±X¬äæfã,ÊgÓ–­d´i}RM ÍÂ]ü2c*³®`k–°Ó µ =/Ì"ѪöÐNQ!«'<Ç+Õ:ëÑn›¹öóûW3eþ våé :ˆImÏEÇÓ7Ùvœ}èd-}‡ç?‹æ¡ñ-ˆ¨]' n+\ÃøûŸaa–oFplSÚö¸˜«.é@¬­2÷I5‚¢HtGa¯ÔmÕJÖ)!j÷~½÷,ÿ›ú;{ŠI>{÷=ô/:FÔÚû¸õ‹|>BÔ+†ax2™¾¯læ×»®øyUrícéçïó鼿ØqX5ˆØ´Ž\zëHú7 8ÎëZü:O}Ë?lId`Õ­ÜÈÖTùU”âçüV¯ZüfîËô4uV,h'(ÈAhh0à¼@× \® œÎ"t·›ÌÌLNh?fÞ>yê¾ÝEFß+‘K +‹ëÖ’ãVÏÐ&.¶N}–W¾9BË‹nàöæ °`Û†LBÉÖjFÙ9uÞHní…YKæÆÅÌøòÞt /Þד¨ã}„J é×<ÊÓÕR`!ª““ÍïçáIG8ÿ¶§øOôv¦¿ú>÷?Â'¯_Ib­ükVŸÈç#D}cF©l§/ó[¼¾LߪãbËgOðü´#´¾ô&îiµp?[×í%,ÈR©}™~ÿVù¤KeÎÞ¬·â·Ü¿Y¸4{®^µòÏQNNŠ¢ Ylhš†¦©ÞÑú<ÕDUU4MEÓ4,º['7'÷Ä‚_³€uŸãÛÝÉ |òq®jXìvíy¡çIáZÆÝù4Û¼Äs—Æ¡ú¾Y<|ï ú·µ°ãÚõ-o½÷=+·ì#O-®?}‡¿z—/–¬cGVD›ÏðÀ9Q³š™>aöo;ÉÃA|û~ ¹ù22Â5Ðòˇo0eÙvöæ:…à¤N\:|8—¤—ãÇsëã<ëÆÝÑ»ožÍ†™Ðd8· êM¤÷†{çþÇ^ÄîEŸòþ”¬;èB oJ¯knæ_½’ðíĹŒç†{G°êÏ3c¯§‘µò§Wœ  ¶ÿĤ ÓX¸á.ké=rÓ çÐЮE¬{ë.žÚr)/?{ qœB:N=©°î?;„´ã5.âdä¯âÓ)[ˆºüM¼¾ ˜´ÚÌ5LbêÆK¸»Åñîô‹ÓJ>!êÃ?›ë—.Å·ÌoP¬S¦bÝ_û õVîþW¢¼×ºÝzù—¥ˆ >æíO`ÍZD:½ÝưÞÉØÓy:å©®ðüLƒËûÆ¿h|:®uÅs½åŽLÓ“ý¥Ìy§]­ ~݆¦YJµ…w¹\x^»\®RÊk+`=³åØíú§žt–וùK7êô¾ù>Ί³â, &^ÍåÏßþf_Ü•Ü9¢¡Ffršs_>û<3Ôó¸é¡á$º70ë½ÉŒ}³/?Ô‹(ã;Vm$+ùjîîß„€¢=,›6‰O_´üêm´ òî4ùFlG°Zp,¥.)´ â°ìgnîÌ¥iÁe:väüñO¼µŽf×ÜÆã­‚Øÿ˧¼û΋$¾À¿šz«„Ö‚a£“nÅF\­¬)õŠÍaܹMôƒ yí‰÷Ùи?7ÝŸAXîß|ýñ{é:u¼zPqÝ—z"N×¾å¬>l§UÏ&xZ¨)„´<—4å'–¯>„Þ"¾–uS©_äó¢þ)Ûç·lW6ó«”͈ž,-˜øDüö#?n<‹ÍÊ_ëfÿú¾¾†æƒþØŒ`2}Ì›ãÆô?†¥{³ÃZ+FŒNs;` #Ár²À”Éòúýë‹=¤Ùsõª•—ª6«…"§ÛíÆét¡ª*n·'Àõ-s»ÝèºA€Í‚Õzb‡âÎÞÁ'Ä·NªdßÈc‰ EÇv´ õ¾‘kŽ”¶tÌh‚/V°r³w&pÍØÁôŽÓ€Tâ/çö7~dMnOzzGRÚ·m‚6´ŒÞÏ_£²t—“vͼod")%…Ð Ëí õ;¹,{Ÿ?q+sÓºsÞÒ§[*a@ßÏ¢/Åìö· ÈÀ¡@ó†ÃÙùû(-ÚÅuMyÞFs“’Bн¢}ˆÓÉÐ]¸œEæ°gÝ"¦LÙ±he°sê4V¨¹÷žëè¢-IuìåîW¿äûÝ]¹6¥â÷<á:¥ï;N=IõnX¦î qšè¹{É%„¸Ð’ïz% qÁ°lO.n$¸ªIòùQÿø‚6Ó4¼ ÍòSù”^^U× vÚ »—+²^cò#Cø6½'ô»ˆ¾g§nô½,ø| F÷G¹s`;‚hÞpÛ–>ÈÂÛ¸!­‰§\Z±Éɤ÷ù5*L^ŸšÒã^—žÊDQ4 ~«Y­ ~ƒƒ‚(r:9’ŸƒÓéDÓ4L4Í3­®8NœN'ºîÂá#Àv¢m-MoUTª©o¯›¬-;)à&ß7„É¥ÖűÿˆAåÊ‘B$ùd©|f[ mõãâËYøãÌ{ç¿|õE7nyôÎ ÝÇÚ½¿ón^Zúç´ÌÃÈÏ5o÷g1ì³’×áÍ.äÎ[.§¡%Ÿ…ë@ÒU4 öÕZ…¦IâOÖî*ÀL9~ÓÂJÕ)§Ô!„BéŹìÈÆÀ¦iTM“gŸ6\÷ÄÛôßþ?ÿ0¹ãaÊä³ùømôû‡Õ{ ÇÓ Y\úÇ´}¹¸¼åÏHÔ§/öTP³\@mš&ªêYoš¦ôù­fµ3ø ÃyX)**@×ݨªç¾±aè8.ŠŠ ±Ù¬|BûÐB“ˆ±Àæõ{(êA… NEAU@wéUÒ Â4MPÒúÄÍ´*5²œ•°X­â¶ª†ŠÉ‰ÿZh„¤täâ!é;àWÞ|èuÞÿ°íî Ä"Ï¿‡ÿëWª¨ °y'¼/Q•ô¹ƒÛºGc "¼A4‘¾ÊÌ‚cÿ`e+i¥ê”yÜzrœÒQ¥´XB9ÌÞÜ’[/fÑAöÐøÐÚùǬ‘ÏGˆúÇ0}™_Ó;.ϱ`ßr¨ºÆÅ Aɸhh.°”7|wßíD»{¼×º}îã²×0Žhl†áËHWiðëéºdhçR¯/ã«( †a z"àâó(ªG­œ $8«ÍF|\ †¡SPP@~~~©GAA†¡“ƒÕf#$øÄ‚_%( :’·d ów9+ÞH ¢A0dmßOÑ)ÿžZˆhœ„ÝÜÃæÂI,~Äl©ÌÝ0«Ý EyÀï‰Þ’N‰àÎÜE®Có8´ùö8ÿ2$€‚ŠÕaW>né€_c“––J“”¸’À@qÔ, výÁæ¼’^#‡7-cÑ4O>™¾ëG©S¶ãÕ!ª—5®=­B Y½x ž[¢&yk°Ñˆ¦}«Ú6-]ý#ŸõOñü½†iºáɤêº'¨ÔõrÓð¬«ê‡ւΉàÎÜIÑ4‹C›2±Ç&˜Pòˆ·‚»\GÈwUey<ÇWñq{χaø7³xdQ}jåÍXUU‰#¿ fÍRÙµk99‡‹›K˜¦Ixx(‰‰qÚ‹ïžTšB»‡Òeíx>õ8›û_@§& °yìÛº½qýÜ+Ž.]c™öõ‡¼7ó:z5ÁÜ»Ã'y\Ž—Ñ/ñw¦¿6– kúÑ6ÞŽ;ë†uáüŒˆJ\؈k“fóÅÑôŠ)"3/‰=’K½2ñË“X× ÍS¤q`ý¦®‡ÐsZÒÀK™ñÆDž{³+z¤¡aß®"šõéE’M!()Hs_M]@@ÇPò2uÒzu&VF{®aVR.H›yï1îÕÏri¡9óõÇBÛ[è“hŠNð=V§SONÇñ q ŽÖ\ucæ~0š—’ïàÂèLy>…­ïæÊ4I¸ÆÉç#D½cz³½Fq ç ê dî_ïóŠFz>é¹Cü2a2ë£ZÑ<%Š µˆ~fê:íÕœkξ¼#3ÆMäÙq <;•põ™» I¿Às cOhJ¤9—©_üˆ¥C(Gö»iÚ«3±'³9·w¤gÓ7º³÷_S5ðdŸ%ÙTjeð `µZ örÕ¢yª¦QTäÉÐØ0t—Ë…Õ¢¡j'wOY‹êÎÏE2oê4æ}?‘_òtÀJhB/Ò1°’2ð>nÍ{‡Ï¦ŽãWЂhШ5MBObŸ¶F\9ê'~Ê·“^cŽ Gí¯lCïJ¿±½ÿÍëÇ1壗YŒ¸nÃh{V2¾6M¬a°õ§Ïø~>&ž&Þm.É Wµ …Àn·ñ”1…¾šÅ[Kó1±Þä\níÕ“$›‚µáF^¾“·g½Ç óÀ‘|wvíD¬Uò}5Mkpÿù¯Á'¦ñþ‹³p[#Iïq3OÜXÁHÏ•{Ç£Ö©Ç©'BT/©ƒ_âÙÂgxmü#ÌuÚI:{/=8¤Zû—¬>‘ÏGˆúÆ“ÁÄÛß×ðLÙs”¾¿åæü=•xÏk(l[ð9?ø®uCiÝïV]Ù»©`ïr+ÿÕ§2qúlÞö^Ä59‡[zô$Ѫ`I¹”[.ÛÉ;³Þç¥ïÁ‘Ü›Û;w"&øÄ¯oJFò[æ öMÅÓºÌùðÍ|$™ßê¥|ùù$³O¿þ•ÚxÉŸèÛÿ2æÎšAßþ—±øçùd´ëxš‹èaµÚ°zµr9¸\Giª|+–/«¶²Šc;Ög1膙<é“j.‘§æXõ¶ ÿ‹~þ‰ ú^LhXx5—Lˆ÷Õ“¹xÀ@\ΊÿÎJgšãÕiqr~˜7‡þât:1 ÝÄ•Éþ‚g.[O0§gB¡ÚÀÛBϱ– £ý^+Š‚ªj¨ª‚ÕfcÖ×Ó8¿O¿ê.lY±|;ŸUüzÒÄ Ü4|n·ÛsÅ00ÝíÆívãv¹q»]˜¦AHh_|6™+®¼šß–.¦K·îü¶t1g÷<·Rûž7gVíÍü–årxÀ+„B!„¨z¥3¿Þ4fñ Oþƒ=ùÖ)%Íë"Ó(iælÞãT¼£;{š:oj˜¦*™ßpÆ¿B!„BˆZÂÛÜÙ“é-éã[º/¯_ß_ï¿JMüúŽÍÀ,5N‘¢(¦‰ª( x2þódš†LuTÍêUð»bù²š.‚¨„A7ÜXÓE¢Êxš7©rgWœ*SO¥N‹3‰ÔÓÓÇbµ‘•#ÈáüS|Ÿgn_Ó0Q¼ÿAÉ”?uEñ€W&ÞæÎ~¯ê=fÓû/Jqç`Ã4É:t›M¬Nõ&ø•þ¾géï+êEQp8‚8xðá‘5]!Ž)sß^BBCÙ%Oê´8“T¦N‹““˜˜Äºµ«èÒõ,\¦éÉè* h𧩝¢‚ ª¦VíhϵLEÇUj™âù_ÙÀ¬+«W­$¹aÃê)¨êQð+„5AÓ,4nœÊÚUãpâS³ qš†AæÞ=üñÛ/d´ëˆaèGÝVê´8œH'§iZ:¿,YÄ/‹¦E«6DDD•ëÙ]ÅoÐ+ÿ×þLÓ$;ûËV®ÀívÓ$5­ZËXßIð+„§‘f±-0€U+þâ—E jºHB”£ª*!!!d´í@tl,n—ë¨ÛJg‚©Óâ䨚ÆÙÝ{²iÓ–.^HAAAM錠( öÀ@5nBjjÚ±ç UN‚_!„8EE!::–ØØ*¸ ,Dí`‚aèÇ ¤N‹3F%ë´8yŠª’–Þœ´ôæ5]!*E‚_!„¨º®£ëÒìNÔR§…Bœi¤“ŽB!„Bˆ:O‚_!„B!„už¿B!„B!ê< ~…B!„BÔyü !„B!„¨óêÍhÏ+–/«é"?í:Öt„B!„õH½ ~A®ÚBnD!„B!ª›4{B!„BQçIð+„B!„¢Î“àW!„B!DW¯úüŠ3‡išd:HnN.ºîÆ4Íš.’BÔ‹…ÐÐ0Â#£PU¥¦‹#„BT ~E­cš&;wn'À@jz:E.΄¢*˜¦IAA>»wíd×Îm$§4’ïX!„õ‚¿¢Ö9tðVÍBjZ3LÓ,~(ŠR*ì{íÑ&b!D}S6põ}V´Ü÷½è iZ36¬[CÖ¡ƒDF5¨¶ò !„5EúüŠZçСƒ$$§¿ö]Àù_ÐíbO!ê+ßB ÜÍŠn $&§pèÀj-§BQS$øµŽ³¨‡#(ŸÝõ½.û0 £ÆÊ,„5Å0ŒRßÇú^,{ãÐáÂéržþB !„µ€4{µŽá—¡(›­¨(ÀõÏdH,„¨oʶŽ)»Î·Ü?(ö_¯Ë÷¦Bˆz¢Ö¿n·›Ì}™deeQPX@ ÝNxd±11X,µþêý ¿|ôKãnàŽ‹“°ž†]˜~bŠ÷uqæð¿t3Mÿ8…í®jÜï 3ÈßµŠßWfrBÅ4ްcÅJ6pVxUJ5ßPLïsÃ/ƒa†'“á]w´Ç‘Ms˜:ëO¸¶“ó§ðõ/»):Î{ÌÃp`ý²ßÙkTb{Ìž`РQÌÞ«WyYª¦ŒUûpg¯`æWóXs²Çë&ó—5âZ.8˯½ÉÛ\UT>ƒ¼óËò}8«ù¼ÈC'ûÊu ©h;ßMEi1#„¢¾¨µÁoNN6lD× ,V;á1DÇ$“@xD «]7ظq¹§Y‹xzÈ zì;2+Pêd-}‡çÇÍag©èQ#(:Ä„(ì§%ƒx´ýž¨BVOxŽW¾ÜHyü­«“a`šø2¦ßkÐÌçñ«rųÙç6K¶=êà8Ûø÷½NüáÞ5›—ƌ秽®Jl¯` ‹'9)Žpëé)Ï©—ñ4ýC&Íù“Ýù”Ø‘~7 åò¶h€™·’I¯MdÉ–=dš€Ø¶2äßWÒ.B«¸àåö{=,Nö.Â_ÌgUfZh#º]:„!¥|´,ôÆñÜzãxÏz„qw´DÝòc^œÍ¦\7à ¡ÃÅ 1€–!å ˜GÖ2é¿ÏðCøŒy°/‰V“üÍóøh –lÉA·EÓê‚AŒ¸¦3Ñ•ì ìkÚìÙ_–0óW3mΚßôgÿô“¦,ãêGÎ"\õ Aîªé¼õþ –îÈGÍã²PÀôF0ùlžó>oO]Ȧl[L: uˆ-½¯’±sÁǼýé¬9àB‹H§÷ ÛÖ;»E›?ç…qóY¿ëù:`m@Ë sÇÄù²·d1ûÁë˜ @C†‘þA˜òƇü°j ûóMhÊ çÇpñÞ§üœ“ÿ|ø4=Cpeòëçïðñœ¿ØS Úøl®ño.IFÁ wÕW¼ùÎ ~ÛŠƒÄ^w2æÎ.„•ùÌ›'ñø˜¯Ùàý\; à–Û¯¤u¨zÔ2^S¦þ³,.vû?^ýz;2ópZx*½®½÷iXÜÂ}à7>{ë#fÿµ—B%””ôPTðYׇܵÌ|ÿ}¦-ÙJ®@\Û ¹aÄ ºÇy:p›Î\làõáWò:€¥£&ø€èèh>øà†FÓ¦MKg}}Íž+úÞB!ê Züæää ( šÅ†¦ihšŠªª†ç´ªªhšŠ¦iX,6t·NnNîI¿9Îä:sO÷f4ŽìÀ”W¿aÉþNôó^ôëó¿'ÞemÒ…ÜxOâ,yt¦¦A€Ö‚a£“nÅFœ6ûïÂ,dç£yá[8{Ð] N >eò OSôä5µcícÍš=„ Ɉ–Á¸¬bö'_óòøx^}¨'‘5N/·_“Ü¿Þç‰7–qþ`îëCáú¹Lœ4šççxü’„ЧJ¾†Q#Û¬€K`4hÏ€›[fǵ÷w¾xo*¯}Úœ×F4/uÁo:w3÷Õ—˜£^Ì£÷\H¢ôÌù¼óñoÞCGËZ&¼ø)ëZ å‘Û[ê:Àž¢F8”òYJµA'ŽlMHD î–2yüg¼4±ãoo‰v”2–zã•%Ø {Ó 6e0ìÁóIÒòØúód&¾ý ŽÆÿcXZmdÒŸgFQ'®½ë&Òƒòظ` ŸUxî×.¦~ŒOu亻n¤YÀ–|ö!c9ŒýÕ;èê OrÃSwÑ1X-ˆX»ß{™¹üöÖËLßÛ“[¿&ö|2÷[ˆµVâü{ß§á ž¼³!*hÁqØ**«µÄæÍ›?ÞsSsÿþý¼éͧ¦¦žú[|ËP2¿B!ê‰Züº M³”ŒÃårQPàyír¹ŠÿX{‚d+ný$FÒ3Y4kA=§¹CÅÖúbz„>Å·?îâükbÅÅ®y_²œŽÜ{ÿ`:…ø§Ò¼ûÓĤ¤R•Ú…yx9S¿Ë$þªç¹¥"V £yžšù5›.¸Õ“å*ËERJŠ_¦´Ð4:y›ÝÒ4ëš<¹a=‡ÜÍ)¾½àÞÏâ·?æ“}»ŒïÑ IDAT]¹û©kh¤N¶ÏÁšà~Œ¾¹/©6 =‰aë~ç™EËØß?ø£$²ýù>[Ó4¼I ï -úL_Mp¯§iáP°e\BÏÐÇøfÞvúÜÐ+N¶ÎžÉfGw}àÚ)@-aþ;€i`º÷°à«?q·ºƒ‡Ÿã¹±Ð®ÎeíaÓ³OÿÂè{YðùŒîrçÀv)мá¶-}… ¶qCZš§lDмc{2Bh“Šºj¯-YIÖ€xÂ0+añɤ¤ø†›6¼Ù–â2:Ѿµ/Mi’ï͘¦‘ýŸÏË¢ÕmÏ0¨G ÐøÖƒüzûd¾ß˜O‡¦¹̇ÐÔ Ú¤'c¥1Mñk™óª†¦Ñ¹sÉçú¯•óµn \͉9JK%áW–vªç\„4¡}‡ â5ÈhÌæe£ùã=ÜØ4×êiÌÙÅ%Ïý‡«›zº´ŠÍdÁ’9Þý•¾/X;•i[Âé;æn®jn2hÙHaûmïò颫ɸ(Æ{þí4HJ&%T)_v½€¬lZT:m[¥­A“tïgpÜcò¾O@’&—dÓ+8¿BÔMš4æÅ_ð¾RðŸÚ×4 ¿éLE“àW!D½Q+ƒ_›ÕŠ‚B‘ÓÀívãtºPU·Ûpú–¹ÝntÝ ÀfÁj=ñCqíZÀ¼1œGCléÓ;–ïøž-— §Y@>;Ö+hzÔvÃÇæÌ\Ç.=ŒNm¢K2¯ÖXÚ´åËåëØïò¿þ4B£±êÙäT‰qÉœ{YõÄ lFxñævR:4Æ2+›²tZT&òĤpÇ&2›?6ï%Ûe%P)‚ˆ"Ü~WûYó_ç=3šKF¥s¸÷}Ívo:‡¾å±›¾-ý¶¡™Ö©TðkúFq6KVêÜþ=ßîˆå‚ÿ¤`5 kúœÇwßÏaãÀ48ÌÎMYÐŽFvÓÛRÀ,¾È3 £p/ëöA\ÏTB0ð\óùï6þuá?¬Þù;žfÈâÒåÔöåâ2ütÃÀ0ÀFtb¬8DžË ÄÛbÁ³¾ä"Ó?È/µÜïý ÷­e·®“ýú®~½ôþ-óÑÛ´äªKñÔäû¸cyoú]|tJ© Ê˜lÿ‘IÃo÷”|®‘…8u£¸UEÙ2ú+:^Y Gùó¨E+æá6œܼ“"[cÚÆ[‹÷S|ÊíÛÍ [8bmBÇd[É ¯°æ´‹ƒ+ÿ¡°oƒ ÎJ]®:™/¾Åw/¥ÏEqÑym‰ P*qL¶£× !j=E1Ëõ&0MUõ¬7}d !„õ@­ ~ƒƒ‚(r:9’ŸƒÓéDÓ4L4Í3¼±®8NœN'ºîÂá#ÀVá®ÇPÈ–y Øoæ2å¡L)µ.—9ë®#½­é½ð­þ+^EÕP1©öK’ÂõL|ö=Åôçæ{»’ìbóWcyo{éÍ›w§É®ÅÌz{ mD«`Ïð)&@âî1ykLuCˆZÇ;ºß˜þ¯/ã«( †a z"àâï\!„¢®«ÁohÎ# ÀJQQ!ºîFU=iCÃÐq:]b³Y  8¤|þôXÌü Ì]šKâe÷skç°’9ŸŒl–ŽËœ9+ÉmÛÄÔXð;›òº—iö¬buXÁ•OÛ¿÷Ti¶˜f$iß³fÕ~\M=Ížqe²rm.–¤f•»_[,­âaùòõd_šàiöL!;þÜŠ;¨ ©åÎR°Ú­p0"¼>Ñs¶²)×J›[/§g«@À…e‚_{Ê…Üóï–Œì]^|5šg¼x«ƒÄ&ðã&rBÒ*üä"…âù{‹ÿ51ެåÛ%9$^þ#;‡^¦žåù¼¾]NNFG’šEÁ¼_X“ճ½¹_pdÚbh•þ±‚ƒ—ÄãéÚí½ðóíÓ¿Ø–hšÅÀŸ›2±ÿ«±eO£aà»n4 ÓP(nrë˨hvl89\èÆ4J~åLo`Q:óâÿ~ÖM‰W¾gû“èî ”½Åãù9… ¤N\6²#½;½Æ^™Å;{3¨QIåÒ³·°1×J›‘—Ó³¥çsÕcaÛ±Ëè¯2eñ?nSñœŠ—©„7i„ÃµŠ¥›òÈhåðŒ8ë7Oé ”oû5ü±-ŸŒfžvûú¡µ,ß }c±†§þ–:ÿÓBSéqõÝœuî|Fßó>3½Ž®ÝŽwL&–+ä¦ÀmR™ÆBÔ°Ššä+~ó¥+Š‚ªª˜†‰©ÊTGB!êZü†STä$>.†í;vSPP€Ûí*üº\nÀ !>…à ~¯úŽ? R¸öœ6¤–Šhtìç&1ó³ïø+« =û ùñÆ ¸î’Ž$9Üdg[hyv!IéDšsøjê:†’—©“Ö+£Ô~”ö\Õ'†'¦¼Ì;¶ëè•l²}Á§|¹7–KF¶%¤‚‰ŽM!¨Âýv¢ûÕÝ™>vc'è\Õ%†Âõß1qþaR¯@Z¹þ¾6âZÆÃ¤Ù|ñC4½bŠÈÌK¢Gǹø}Æ×,T;l°ó`Q…å°Äöâ¶û÷òø“óòô¦Œ¾ª ú]BúyûÅ÷ɾ¤3)Á¹{÷¡µ½€®1•«n¾ Ð𾦩“»rË R¸¶W+šÄzb2ML3‘€s™ùù\–ìDÏ>W1ï]Æ=û6t¥QˆAæ†l C×щ¢Û•Ý™þòÇŒyýWõjJ„šËêý:Ø t]G/?5àìË;2cÜDžWÀÀ³S W¹»ô z‘dÝW^CG×@/΀ꆎÖ˜†Ž|þøb: /oA@öA¬­zÐÚ—á6tÏèæ^þïg·åòs#xn拼¢\Iï °`wn"½z7%Ƚ›…ßo"¨q–Bv¯ÞK!BÌRïIP")A.~Ÿ>t 1È`çþÂã–1#ÒïwãxeQôâ º®ëž^ñºî¹µ`z–¤]Ê¥—òùذ]{íqîØF^çÀ–v)—4ú…/^}ÐA}H·gòëÔOØÒƒû»F€®WpþËV¨|6ÿ¸=QˆQ9²m= ¸­ǤÝ<>Åó"éã$3/‘îÝ“9… Ñ„¨RŠrŒŠRr“IQŠGy6Q0UOË éÁ.„¢~¨•Á¯ªªD„‡‘_PH³f©ìÚµ‡œœÃÅàMÓ$<<”ÄÄ8‚íÅÍ·*kÅw+q¦\O‡eS91z’üé§|÷ûzö½€ûÿ«2iâ×|ö¿p¢šÚû»fÙp#/ßÉÛ³Þã…yàH>;»¶)}Q¬ØI4Šÿ œÀäi¯±8‰¸ò›¸¼©½Ô\‹•e­p¿hÛ~8OÞŸOfìNÔÐFt»aCûU4Ò³FlïsãúqLùèec#®Û0Úžu6ƒï¹šÂg3þ¹™žC%!#» ¥Ûa+¦_Á]×ü̓_ŒgFç§¹¶Ñ…<ð„Æ¤O¾aÊ?QØ£ZÐ/µw¥ƒ_ÛAôô÷50Œlþž»gÊu´RKmªDwìIÒgŸñÝïûéÞ§w>¡2å“éL·@±G’Ü&pLS!¤Ý0ž¸;ŠO¾œË[/}‰ ØÂâiÕ.«YöóP ïr+ÿÕ§2qúlÞ^š‰•°&çpKž$Z•R3‡ø?÷<ÓÞ‚ënîÃþ¾áÍf¢„4å’ûºÑ*Ì»¯23ü”z?‚h5ø1î ý„©ó'ðÊ×:h!$w½Î½š˜¿ŸK¾à§É‡1kdSÎ>ŒÞ ´Òýüì-¸á®«(üø[Þzþ›’ϵM8zÛ”j1pì²8´’sW|.ÊÎZ¥%Òÿǰ:™o>ýß9ÍAƒ&mi¤–ŸéHK¤ÿÿ=‚6ñ¾yëE¾4ˆiÕ‡Ûﺊ¶ÁJ¹s^áŒ-z>»×þÈ'Kÿ!ÀC˾#Þ1 à8ǤsîÍ Ú0ž/?~Õó{Òõ&2º&c“,°¨%*ê ãkÞŒibú½òŸ6Î4‹g$ó+„¢VP”R7j‹Wå.¾ü|’Ù§_ÿJm¼dáOôísgÍ oÿËXüó|2Úu<þž$]×q»u¬V+ª¦QTä À†¡ë¸\.¬ U;þUèŠåËNkYEåï³øaÞúˆÓéÄ4<ýCïE¥.àŒâ <Ég„õ›çò`ë¶­L˜ðQ¹µÃnJãÆ½Íž5TUÁj³1ëëiœß§_uV!D=´bù2:v>«øõ¤‰¸éæ[p»\˜ÞF}×ÿºË…ËíFw»1 àÐ0¾øl2W\y5¿-]L—nÝùmébÎîyn¥ö=oάڙùõñÌñë l­ ‡—Ó‰Ë48ÑA®Ä™ tæ·$…X2í‘oKß:¥¤IŸBÔW¦ŠBãF :d0“?ýœ>Ì ƒ®§Q£F%c ˜¦©JæW!DÍ«°éž‹BO+¦ªÈ×êàןËåÄårÖt1Duð6wödz}Íø ŒT¼Äï—P$ñ+„¨Ç|߃&7fРë˜0ác†BÃF =)žfÒ¾ïTÓ4dª#!„µ‡¯¥çirÆ¿¢þ°Xmdgeãrx¯æÀ3t°wŠÓðŒ\j˜(Þÿ d!„¨Oм2=Ï}߉6bÔ¨G°Z­Åw ”âç†i’uè 6› ß&„¢ö(™±Ï,YPE—øüŠZ'11‰ukWÑ¥ëY¸LÓ“ÑU@Ñ4LÓDUT0AÕÔ’A]üH,„¨O*úô-Ó4Í{Qúæ išX-VV¯ZIrÆÕXZ!„¢ ïß,¼‰.³ôrE¡ªòÁüŠZ§iZ:¿,YÄ/‹¦E«6DDD•¿¸“Á­„¢_Ö·dèòý£LÓ$;ûËV®ÀívÓ$5­ZË(„B”â7¨­g†¥¤‡¯ß@U1º¿¢ÖQ5³»÷dÓ¦ ,]¼‚‚‚š.’BÔŠ¢` ¤Qã&¤¦¦{ž`!„¢(Þ9è}Á®/ðEñÃÞ™éOy?üŠZIQUÒÒ›“–Þ¼¦‹"„B!„8Šgxñ<7|Y`ü§ý=õ›µê)¿ƒB!„Bq*¥$Û Þ |!oÕŒz%Á¯B!„BˆQv[Õùú–)ª÷y ù#Á¯B!„Bˆj§(*yGòŠg$PTÏìÞYN= Ÿ½Ao^^íÔzíJð+„B!„¢ÚÙlvlÛ†¢ª~óúªÅÍž}#?k;·|Jû«W^­X¾¬¦‹ „B!„´°âï夦¥£i*¦aÁÅS) ª¢±qý:“’Oiõ&øÍh×±¦‹ „B!„ÂËb±Âüys9¯Ï…¨ª†©˜ÅóÔ« ESùù§ùÄÆÅä8¥ýI³g!„B!„5¢iZšEcúÔ)¬_·†ÂÂ,V .·›Í›61kæ ‚‚‚hÞ¢å)ï«Þd~…B!„BÔ.Š¢žžNnN.;wlgåßQTäÄ@Td$mÛ·'2"E9õ¼­¿B!„B!jTXx8áž¾¾eUEš= !„B!„¨ó$øB!„BQçIð+„B!„¢Î“>¿¢V2M“¬CÉÍÉE×ݘ¾I¯Å1Y,BCÃŒBU«®„B!„g: ~E­cš&;wn'À@jz:Ž*íè^W™¦IAA>»wíd×Îm$§4’ó&„B!„—¿¢Ö9tðVÍBjZ3LÓ,~(ŠR*ì{íàÕ¥ qÙÀÕwl-÷‹À@MÓš±aݲ$2ªAµ•W!„BˆÚ¬Þ¿+–/«é"?í:uÝ¡CIo^2‰µ/°;Zð” ‚ë’cüÝHLNaãÚ5ü !„BáUo‚_8vÀ%ªÏñnD8‹Šp8‚€ÒÁ^EžÿsÃ0êTl@©@ת–ŒWWö¼8A8]Îj*©B!„µ_­~Ýn7™û2ÉÊÊ¢ °€@»ðÈbcb°Xjý!ˆdø¹e^_@è¯lPXWTôú¯ó-÷ÏŠû¯×ëйB!„âTÕêÈ1''‡Í›· ëºw‰'Ó•_P@þîöíÝGjjÂÂÂj®¢Ê™~A›â}í À?Ì3Mÿ\oÝÉûeÚŠz3¯6op,Á¯B!„Åjí<¿999lذ]7°Xí„GÄ“@tLá1X¬vtÝ`ãÆMäæäÔtqÏîœmüýÇZ²tÿ¥:<ÇÍÃÇðÝ>ýh?Z­ü³·&€¢`zŸ~ÙNÃ0›¶l%£MëkmbþãwñÞÖò«â¯zç&ÔΓsÒt²–¾ÃóŸEóÐøDh¾å*¶Ð8]„ÛjGÞÔ0 O&Óà•Íüz×?? Ìüí,œö3ú“M‹@ %%£_} 4Åwú ×¾ÁˆGÿ¦÷‹ã¸)Õæ)îr>xx4s¯âé§®£™£jÎk¹‘­© ó«(ÅÏ%øB!„¢D­Œï2÷ezš:+ƒ ´ä 44UUÑu—+§³Ýí&33“„„„ßYâ@%Žâ*ö˜ÅÁMݧÚ~(O¶¯ér”0 £T¶Ó—ù-^_¦‰oU3rÿ⽇G3g_<]ÜÀ]M£Ðr·ñÇÜé¼õÈVÞóÿéé©#ÅYß#o5“ž|šo-—òøãבîPª®ŒeÎÞ¬·â·Ü¿Y¸4{B!„¢D­ ~srrPÍbCÓ44MEUU ÃsY¯ª*š¦¢i‹ Ý­“›“{rÁopÍš· ¸LrNß7§ü˜#žfô)ØÌB6|ò0O.mɃÏßLF°‚ž³š™>aöo;ÉÃA|û~ ¹ù22½¡³žÍН'0iÎ2væ™h¡©\tïÃ\m›Î},¢ÃS/34Õ˜þe4·¼¡qß»ÒÑQ®”'ÄÌÛÈœ‰™ù˲uGÒ9Ü>j(IÎe<7üFφQýyfìõĬ|†uqÛøÇ骀édïÒ)|ðÅ|Ve¡…6¢Û¥CrQÁ ˜y+™ôÚD–lÙCv¡ ؉m{!Cþ}%í"Ný¶áŸÍõË—>H³øß*æÈ<ª_cÎÞ& zq W6ðfW»Ñó¼^4}/ï½ùgµ~ˆ³ÃÿöȘ‡W1ùɧ˜îêǨуÉ¡â²WEñ”Íÿ™¦'ûK™ó(„B!D=W+ƒ_·a i–R}].ž×.—«Ô40šÅŠ[¯ÚþªZlon´”>~‹o»Ô“ÈSìI^¶ÏoÙ®læW)›=ùkùfQ.Á=ïåâÆÅûÀšÀyƒûðÕCß2syÝÎ,)sîJ>~óyfº/bÔè!´ UOKVÚ_©,¯ß¿¾ÛÒìY!*Çvº4ežBˆÒjeðk³ZQP(r¸ÝnœNªªâv{\ß2·Û®Ø,X­'y(ë_gį—¼¶wå‘qwÒÚn!ö¼ úåA&þï]¶8¥¨×ý jŒ¬ŸÁì \3v0½ã4 •¸Á˹ýY“Û“êßL³—„«žãöË“°úíÒuƒßÂM_3cs<~7×6³û­ÑÙ 9ˆII!ÅoU©àWßÏ’©‹ÉKΘ¡½i ­Ó‰Ê¾§f~ͦ n¥VbZv ];Њä‚ÜóÙ¶ô$2èÔŽÁ´™¦áMh–ŸÊ§ôòª»PqgmcâÛ$`åX[|k’ÔÙìÚ|ç9áxäû矡ÈÖû_ód|Mótž¥Ç½.=%”‰¢hü !D%éºÎè1cø¿ûïÇáp”šG]!DÝQ+ƒßà  ŠœNŽäçàt:Ñ4 ÓMó„iºnàt:q:èº ‡#Œ›íäv–r ŒhCq¼f &>À÷<–óG\Í‚û?åw{Gî¹®·y´›¬-;)à&ß7„É¥Þ0ŽýGtœkÙá£S›˜Rïéå&kãŽX›ÐÑ?º=ν¬úâ6#¼øo¿”±Ìßʦ,fåNµFhb4V=›œB‚Ní¢Áôâ\vdã `Ó4ªô.½aø^Ã0ʇՆwtióÞ4<;vHa÷¯KyÿÝ½)öÓ™9PP³\‹jÓ4ñ\¯yçH–àW!*ÅårñÊ+¯’““Ãc=FDx¸ÀBQÕÎà74ç`¥¨¨]w£ªžþ¤†¡ãtº(**Äf³@pHðÉí,0–F—ëóë¡“½a5{Q p-?¯Î¦ãY¨x1%¡OÜL«@ÿŸ±«ÁVóèÍ^÷i˜YÈ×/ºº)ª†ŠIU„[†éËüšÞã9Vì[U1ü•šB¬6®ØE~Ï0ˬwþ³ŠÝD7Š@3 œ€ƒ¦—=Ìý}¦óÜsïóès<ñ—Ð8°ª`ïH×%C;— |}_EQ0 £ø‚Í8íÙg!„¨|-eæÌ™‹¦i<úÈ£„‡‡I,„uL­üV Æj³ƒa蟟_êQPP€aè$ÄÇ`µÙ >Éà÷ôý yç£u4:†Ç.aÙ{ðK–Xˆhœ„ÝÜÃæÂI,~ÄlQ°E§¯æ°zef™>µ 5 ˜\ví9RÅýB-D4IÆîÚ²…eÖ©XVpåSà>Æ^m±´Š‡½Ëד];²ãÏ­¸ƒ“ZZOñü½†iº'Ëjèº'ãªëå¦áYwÊ{ýº“·x2s·z³»ÞGÑ?ü4ùÙÚÒ7#üæÆ„–WóØèA4Ù8™'^ú–…UPÃð[ùc.u>|™hÓÄ4Ìây…BTÞÏ?ÿÌôé3xù•—ÉÍÍ•ïQ!„¨cjeæWUU"ÂÃÈ/(¤Y³TvíÚCNÎáâæ­¦iJbbAö“¿;›·ƒµ«ƒJeøT{,©5~ûðÖ%_Ë‹ç5$Æu+ýþ$~´ŒVwu!¼ÅeôKüé¯%èš~´·ãÎú‡ƒa]8?#-¬WöŽäé©/ð?ã*ÎMDË?DA|º%¶¢g…'¿Ã¥Í#4r6fQÅepíúš§›ŽqÙSvc'è\Õ%†Âõß1qþaR¯@š8rr§º²Lo¶×(äüƒLo¦×û¼¢‘žOmî_;-®Áyë^æ³Q²¥:§F æìà¯ù³X´-€N· ¡Sˆ‰®ëèÞL»©ë躆5©/÷<˜Ã“c&ò̇1<=¼-¡'™>fsnïHϦotgï¿& ¦j¦÷< !„¨¬&Mš°`Ázôè#ÐÁí·ßFHHˆd€…¢Ž¨•Á/€Õj%Ø;ÈU‹æi¨šFQ‘€€†®ãr¹°Z4Tí²‘»gðʳ3J/‹ÀoÊfÒßáôº7± 5eà³øù¥‰LÛ˜ÁÐf¸rÔ#Nü”o'½Æ(Ž8Ú_Ù†ÞhŠƒVÿú/÷‡|Ägß½ËØ¯Lˆ¥ëM-é’Ëÿ¹›ƒoOböÛ/3P#IÎhFÄq>ÿÐó´ÌxȶF\1êQ'NfÖG¯òÖÈÖ\ݤ é 0òò¼=ë=^˜Žäó¸³k§ÒÁ/ ¡í‡óä!|ðùdÆþàD mD·F1´_V¨’¦ÍÇâÉ`úFß4|3IÆB!NŒ®ë´hÑ‚ùóçsî¹çb·Û¹ùæáK,„u€òåç“Ì>ýúWjã% ¢oÿ˘ûÿìw|EÇ¿»×Ò+)„!ôŽÒ»€ ÒUTT°öŠ"v±G€ª· IDATaÁ†X * "** (H“WÒ‘ ½_Ù÷½»Ü…„ŒRœïû9Ém™yfwnßýÍóÌ3 pi߬\¾”f-ZÿÃ&X,V,î¤VN‡§ÓqZçoÚ°î_³Urj*º?-ù¾ýáp8ºŽæq¥¼¿`¬ekˆA¨å{NâŽxÀ½ ‡ß®’ ª&TUÁbµ²ðëù\Òë²ÛX‰D"9ï((( zB ŠŠŠ°Ùl(ŠÂÚµkéÕ«ãÆ=Â7Ü@HHˆ\I"‘Hþ&›6¬£õÅí½ß}·€ÆM›»—šS½KΕýQ9|è/ê¦Ögíš•´iבµkVÒ¡s·Jս䇅ç®ç·4Nçé ^Éù‰¿ç×íÆô&yòMöäÙ§”„ÿ^ˆ½$ÌY×ÝíTÜÙPgï¡BGUz~%‰ä Ð4 MÓ0›Í´iÓ†… Ò·o_l z5ÁÁÁRK$ÉyÌy#~%ÿ!ÜáΆ§·dޝÿ\^Ÿ¹¿î• Ôñëi›Žð›÷®( º¨ŠŠáö\'!t¹Ô‘D"‘œ&.— ³ÙŒÙl¼uêÔ‰yóæ1dÈl6 HPPÀ‰DržòŸ¿›6¬;Û&H*Ùb%;+› à ·ò„xŸ±¶¯ÐŠûP²äÏ¿Éð7UxÌô?:£²½/Ww¸³ðUÝmîQ¼“ƒu!ÈÊÌÀjµT¦D"‘HÊGÓ4\.&“É+€{öìɧŸ~ʰaðYmôés¹À‰DržòŸ¿r¾ïùC‰ìؾ…6mÛãÂðè* ˜LF¨¯¢‚Õ¤þÙžOŸLÿÇÊ.«]~Ûã?¥€Y̶nÙLÍääÌ6‰D"¹ñx~=¡ÏúöíËÔ©S5j6zöìI@@€À‰DržñŸ¿’󇺩õX½j«W.§aã¦DFFŸ,/ØäVe£ø$½òýî‹‚ììLÖmÞ„Ëå¢NJê¿j£D"‘œïøz~}½¿W]uÅÅÅÜ}Ͻ¼÷Þ»tëÚÕ›K"‘H$çRüJÎ9T“‰;³{÷NÖ¬ü•¢¢¢³mÒy¢(R«vRRRå ™D"‘œ&ž„Wžªª4oÞœ½{÷z)**â¶Ñ·ñùìϹ¨uk?,‘H$’sùÄ–œ“(ªJj½¤Ökp¶M‘H$É×7##ƒ•+W2bÄn½õVÞ}çFŽIXx&Õ„Õj!0 À&-‘H$’óùÄ–H$‰D"Áðü>|˜!C†Í€9r$ãÇÇl6ÓµKo²+‹Å‚Õj=Û&K$‰ä4P϶‰D"‘H$ç‡bÀ€4nÔˆz©©Lš4 ›ÍÆÝwßÍÜ/æ"„ 44”°°0QUù%‘H$çò©-‘H$‰D\sÍ5\tÑEŒ=šÇ<ȤI“ÈÉÉá¾ûîcóæ-lظ»Ý~¶Í”H$É"ůD"‘H$ еkF¼•æ-šÓ¥sgš6mÊÛo¿Mhh(-Z´`õê5ÿëkÊK$‰¤js~%‰D"‘ü§QU•GÇ£W¯^Ô®]›ˆˆp„<4æA†]Ë–-cëÖ­ôíÛGfÒ—H$’ó)~%‰D"‘ü§±Z­ÜqÇí˜L&¿¹¼mÛ¶eêÔ)üòó/ П:(°D"‘œ§Hñ+‘H$‰ä?ªª„„„ (ŠŸ°µÙlôèÞ¶mÚ¢( ÁÁAX,–³h©D"‘HþRüJÎI„K;BFú œN§œ_%9m¬V QѱÄÅ' ª•óÒ!ÈÊÌ 7'MsÉ~wc6› '"*ºÒ}DraRZôún·ÙlØl¶³`•D"‘Hª)~%çBvlߊŤ’P# ›Í†â÷bêû·ðù.…Ê<Õ§»ƒÇ‘“½•ú W¦(„ààÁج6RêÕ#00H†6^À!(**äð¡ƒ:¸ŸšIµäý–H$‰äGŠ_É9ÇÑ#‡QHHLÂd2£šT@QT¯'NPå$1,¹ðð%BPpß{û«1¢€Ð‹…àà:ìß·‡´£G¨žPã”5df¤c1™II­ÂûQÅÏìùî+”¤‡øìSZ¸zŸel÷ÜÃÀÀ ê¦ÖgçŽmdef]í_³W"‘H$É¿ÏFünÚ°îl› ñ¡Y‹Öåî;~ì(Iɵ1™Ì˜ÌÌfP"t Õãöø¹P‰–"äÂÄ0ÂGåšÆ½ÝØi8‚uª@¨ ¨‘˜ÄÎ?·U,~33¨× QInTžˆòØ#½…秨(k0 FÍ$vmß&ůD"‘H$8ÿñ §\’Š"Š‹‹½¡ÎfUEQUTEq __¯[t(^ÿréê XQPPÐ…ðŠ_!*îègEE}A„AAAØíö kpØíµøˆ¦²“ïߺ®K| ë:€ŸÐõÝçÉÞë9ÆwPP0§ã_²T"‘H$ÉÙ✿.—‹ãÇŽ“••EQq1DDE‹Ù|Î7AršÝíÖÓšX·¶ð¼¯–H!Y$ÃO/Lü÷¦{ÔaÏÂ=ÿWˆáë9N×ô ëÐ}DniÁëV¾”W’³KY¢×wŸg»¯7ßw¿&ï¡D"‘H$<ç´rÌÉÉaÏž½hšæÞbŒÜQx¸ˆciÇHI©CxxøÙ3²B42ÖËwû’é7¨Uà˜¹[˜óÑR¬ncP-ëß/ðCº!^hBG`ÜO´G#{”±á–\ˆèžAQ2È¡¢¸· wp Vw1D°q’.*6ÂGü(îïAU: ^ÿ¾vaõ;ôµóY°·6ƒ¯nMdUSh'Xñчüžx=w]VãŸù?žR‚¶¬a0ï}òõâãÇRüJ$‰DrÁsÎÆˆæää°sç.4MÇl "2–˜ØbbˆˆŒÅl @ÓtvíÚMnNÎÙ6Ð)<´…ÿm>ŽÓo»‹k¿ãûßQ\ENI½èëÖnâPAÕ½¬9Íc7ÝÁë¿åœõY³B×½Â×pâ „.Ð5ÝûѼß5ãx]7Ž9åÇEöÞ üºø;–lËÁUáñ•ü8³Ùñë|1k.¿uVM™ÿÈÇEÎþͬúé~Ú~~µ_x?º÷cÜ{c›ç]ÓKŽs‹_£ÿTÂóësŒá0V íŒ!¬Á(Kw—íÙw>œÙ{YÿÛV24ßíNÒÖ,à›UQ$ª¨.-‡kÖ°%ÍŽ^E¶;~Áƒ× gªì2ËôÜ/_OoYåx8¤÷^"‘H$’³Ce#7«"Âóœôüº\.öìÙ (XmA„…ED``EEŘÍrs³qØ Ù½wÍš69íèÂõ/1zâ>Úy•;Z†”òÞ89ðÅcŒ›¯2ìµçégª ´b¶~<×]·ó^“X,gê 9¬žø0“þ(ðn²„%P¿U7ú_Ù›&‘ÿÌ-S­‘TO¬NdØÙïºî[u‹¡€PÝó<}¼|xÄRÉ—V‘Ãú_aʈҔÎõBÎèà8°€ŸŸÇØ>Œ}t(õ•}|5e\aôh}ícT…™G9VARbõžªEàÌÏàhº‹jÉñy—ÊaÝ´ îö7¦cꙵÿ$ŠËh ¶3ó¹WY”•ÂÐÇ¢_MËIópš!`p EAñwh€!X+~XêºîîSžóKy~…½ˆƒ¿ÌdÚÜ_Ø’VˆÀDX†´¿òN†×ü{Ç, ò¶)¼Ø»šÿÈ¢žÎ’GGóNÁ5¼õú`Ì?<ÁýîÀ3Y Š¡vÃÖtí;Ëš—ÿü(øßx†¿¸›ŽMæ¾Ö¡'=³öö÷Ï51bòDT¯¨×id®œÄøOâxê£FDz¯¼[=–üÆN‰ÈfÅ ÷0q]¾w“%<‘†÷dð5}heöQ›%×÷ï¢Z£HHL$*ÔTf™'eä¦ Ï¯'qRüJ$‰Dr¶p9X+±žzI4ð™sö•N?vÜhœb&00˜ÀÀ‚ƒƒ @UU4MÇé Æá°£¹\?~œ„„„Ó¨E#ÿxù¬œù=W4¹ŠdKÉ^=s53¿=D’Q Á¿%_„FQvÔȃ77%ÈUDö‘­ü<ÿS^øã/~i4MÿjM1¹ëÙÎÿ@ɧ7DÕûŽáÑso(à:ÈÜGžàë?ø^PqgzÉ;²Ð}<̧ey{׳«8ð;²S/¢¤Ã+YÌžóôÙ˜š=À»6#°¼â{øx̳ü”ÝÃÄ»Zê}C×ÈXþ c>Ø‹ºÜöÖct ¯xT¥xË[ÜùÊz¡]yòµ›HõDÆŸAû‹·Máá—W‘å=P%¨Z2 Zw¥_¿ÎÔ 5^ºýU#"\™ÛÙxÈ ì`ݾ<úÖˆ0„Šgþ¦WØÙ¾…Ï>¡¯àªlس¯×Ðãùõî:…›§òĤŸ±\4„ÑÃR‰PóIÛ½“¼ð@l5»Ñ-~s¯åø%}ð/Ó2~gñNAòˆNT7ëÎËÆnjÈðq×’bvŸþ[W-â£ñ‹ø±ßSŒÞŒ°“Æ4òŽe¡‘Ïò¿a`³ë¨åûÌÊø•¿: Dq¢@CTâ™%|þ-«/”·Ýÿ ‚ì|H¼ŠGnkN°«ˆìC›Y2÷cžþ}?¿q7­N³ÌJ ÆvçþWºû•ëG©ûç™J¡øl÷ g—aωD"‘œòò󈮄ø-**üÛu“â7''EQ0™­˜L&L&UU½ÞUU1™TL&f³Í¥‘›“{Úâ7ïxX«š¶ˆ9ëz3¦]¸Û3`gÏ·_°ÕG3‹tOx±žÅªÉÏ1mÍ1Šsd]º ½;Çãy»ë]n»þ]Û?Êä»RŒí‡òìm³É*˜ÂkÓqÈHFôH" <-R“úõë¢MZЪ¶“ûŸþ™ïþ¼ž¦5ýuøœ]A«ñ¯1"Å òV?Ëè·M<8õ1Z89ñû|>žó#.Ì„ÆÕ¢ù·1ªG¸€'FÌ$³@`ŠH¡ËÐ;Ù+ùäg–ç:…&ѰACcð¦i+Z×qpç£KøzÛM´jé#³…€â­¼>òIö žÄëƒ0ZÚ¸suÆOáÞÆ€ øà2f~0‡¥›g®-¢)m¯ã[Ú‘µˆ‡FO'öÉÛ<ûžÙ¼C•°¤XLÚŸü¸ê/šöJ&Àï3q.#eÖ4~\}ŒË¹£´ã¬þi¦Æ÷pq´ ÿo¸ÄÓl£Öå7Òýë'øåëÍÜܲa~·D#?£%ùzFÖú’×>ýž_K²ôŒUÌ\’O‹›o'xÆ›¤çØÑ DQC¨Ûýz¸,Š05ßMáã·ß"¹Ñó\ë®ÛÔ˜QÏÞBƒÀN‚v„¤rÙ-½©êä¯_gòñû/Tû-nN-dÖ÷º•<³4ïœÜ²®íIótÝ-}™Ä¦„ÞÜøP[ Ø>ws6 _oGx©2\™ÛÙp@¥÷ãh Y;–ðÉœ×y)º6/ª¥Tž=—X†=K$‰Drvˆ‹çð¡ƒ$ÖLÆd:9rM‚Ì'ˆ"+3ãoÕuN&¼²Z,X,4MÇåráp8).¶SXXDaaÅÅv'.— MÓ±˜ÍX,§éqòr¨!±4é?”ô%,ØšÀÉÁ%_±=ì®j[¨@ÈO/p{î¬Ä5oCˆ©Ô­ßŠË† ¤¶ø‹ G|Ö‡ ˆ&1)‰¤¤$jDÙJ<7 h×®%Íš·¡Ïˆ›h˜ÁúõÇJ%ÇòµÏ…ÃéÀ^˜ÍÑ?W2ûƒï9¡¤Ð>%øô½•Z:«ç.'»ÖõŒ½í Ú7oB³Í©RщÔoÙ‚fM[Ñyð­ÜÐPå¯5ÛÉÑu 3óñ4lÖ€”:õhÑ®-)!U“óV¸ÃsަkFr#!Ð4WØ×ó«£ë. þÚÄÖƒ†ð5Û, IÛüoMú‘#NÝðzÎÉˤÀlEÁÁ‰m‹˜ôÊ|ö»'…º)ù #Ù‘ï;²'Ù’ÿw½ÄÃJ áDDÅnñœ_òqæg`Ìý†µ.tÇ–»'&‚U;Ù¹4]GGAËÌ ØLhˆ œÙì[3‡×gn!_Ó)1G!8<‚ˆˆhbCLþaκ ³UW»—Mçµy{).e[™mâšwp‡R;ÉÊ(ÀuÊö»¿ ï°®»8ñë;<ýþl<œ)"–Ø0(ÎIc×úd8ʺ®Åݲ‰'òq¢b³*`OçÏŸ?âõù(ÒKúˆ¦ká®WCW€GüýÏãMÖ½Pˆìp÷]Ïæ÷`øOñΗ«ØŸçô£VkÃMÍúq9‡Æ6çÑ•,=`£Õ-ˆPôrËBGXh–Ú±½d8KíÓí俨gÖ !¤žøžy›rÑ…?Ìe[x/®iWè È;ž‡Kèa&®e[Z5J¥nƒVô~%uÄ~Ö*öþÎ0W³&II5Iª†·} iß¡ÍZ´åŠ[GÒ10ß?Š£´ÍžrtG1ÅùÞþ ³Þû†ãJ*êOîþPr=)u „Ï1vþZ4›uZ îw+—·mFÓæ­hV#Àç÷Ö÷üH´nI³f­èzÕmŒh¤r`Õf²´“¯µ'ZÀc‹¿‰D"‘œ4jJDDûöî&;; §Ó‰—ÓI^N6G$84”‰I»®sÒóŒÝá  0‡ÃÉdB0™ ©¨i:‡‡Ã¦9 Çf=Í%ôbrŠÀkÃR­W¶û‚Wæ­æxíj|õcõ®»ŒZ¶Ø (§Èx™×²Ø¸`s—må`z!Ùp⢱vš¾XK5#`sV!å¾ní|—»nz×ûÕÓŒcFÒ3ÎÇN¯:œGÙ|ª÷kBô™N]V‰M…-YjjtÈÅ˦òÚýrqÏK¹¼wGêG[ªdÉ—’lºE(Þ¹zº®£(]ÏtNÀ›×«•jßÈK´Ç²ý{c5yýÆ–ÌÄF•ˆ¿˜>3~` ô yþùo9rüWïéÃ-Õ}¼B÷/×ýÝw&]èè>ž<ãœç«©É-ôuÿ#­"5­F¨³·ß¨•Ofä ·?)C°o8¬)š¶·¼HËA»ø}ÙR?‘?O¤×sóÅ‘˜£yßÖNXÊOûû3ÈáC!tÅf%""’º©  ­’ºÎMñ yùØlìöb4Í…ª¯\º®áp8±Û‹±Z-Øl6BB+tcú£“[ ¶`ŠD£þ—7ö+f~Ês;ƶ¤dbØsíèhœXú¯ÌË íõ£¹±a5,Ù¿1ù•¯O¿ŠŠIÅ=‡µ‡ðð-M±CLD€×MR€·¢¢ ã*/ò[4T³ú·Ä©jV½/ºæØ®Ü÷Fc¶¯XÄ× >`ü·ßÐóá§¹©yéyЧï²2U”dó5> ŠŸSCÓ44Ïõ:š¦\£!±¬&ÒóhášWȪ&]Ó1Ç7§eÌ·9QÈáC98cKÊÕ5 M󿚦!4A¨ih¾"MÓÐ4Ð|¼RÆ1e!päçâ°ESï’n$¬þš#?Ïã‹í»É#‹ú\LÂO âìœZzþŸ,üð¾Ýží_œ£€b§†î±Oãºx*%íG×Ñ4ÕRk`ãö‚ òœZ©Xï|a×Þ¼çvÿñݸ,Åjxã+j¿æ3` k¸òþbW@­Ú&aÓ5ìzE×µd.3ºŽ¦›‰©WÛ÷éØ 2Éwé³âNtdÔgòdò­Ä|NOR¬’ìÐå`À‘Bûui×ç –Lx„Þ™I»·ï¤q 5ìKçˆ'Y¾h'WŽ0±ø—Të~?©6À,éS' Ü¢ƒl<¦ädÂÕRû\…äƒ5Ø‚4pqÍçãOBØ`jϸ¶‘("“`› 8§—î$sÉD&ÌÉ í·3¼Q æ¬5¼ýÒWåFï•ðßî#WUã7áïÙ<Ñ5¯dì­ÍµC¬ç™åAá­W * 9œÞ¹ç† îã5œ:  Õt¥ »J hïsCw) šŒÌë¾âÞ7[¸®ë¨ªêwÿ%‰D"‘œÂ#" ˆüGë8'ÞCCB°X­TE×5ŠŠŠ(,,ôû¡ë Õc±X­„†œ¦øvòì` 2¼•æê]ب€u+é4R@±d{^1:NŽí8ˆ^ý†ôjE½ZIÔ®›L˜·@K€ìùØ«â*8º©©Ô­]“8áë‹çÚ\r9t´ ìùÀ–jÔ­i›÷[…ïwеz ã‘W^äê„cüôív ª gŒÿ­žu}—]Ýí©Ò|çüz<±>uëº@(f·‡JGÓ„OH3n¤!²q¨(èÂ3@àÂîôÔé9<¶ ÿÐ`w¹B(Þ¶hå®_«S˜ë^ìÆL@L[.­o‚¢¬Ü炨ŽôªŒ-ØpÝçãÒíìþòCCøF·¦ÿµ70ìÒT÷Ürá w'öáäºK·_(£o ]+ÛN?T+aq)\tépÓϘozRû1’cºÃé]OØïšyŰ'ÙçùîÛ)|¼Éî¶ ]G¨f¯ýž¶jz‰WÓû©LÂ+Ï9ºîíwº^²–´®µ[¾Ó IDATi'}„©M[ÆAÁaޏŒs-Éôî]ƒ¼U_³bÅ׬ÈIæò‰˜õ’5Š=L!|¶i…ìýnËòBh×§!¾ûtá*"·¬fÐuLñ]Ô8Ÿu¿Ϭú!ÌšÁžW„¦Û9¶Íxf]Ù³%©I‰ÔNI2žYºŽÐÁ`g…NÍ¿.CUúm£Œm~ÇU§nÝRjÕ .Ìj Pùž1ˆaá ª…@Ö¾ckž2< :BP=) ­g_¾VŽ]îÙÁÞõŸ=ýKC¸§Jèîã½÷Ës=‚\¸×v{¤%‰D"‘\Øœ“ž_UU‰Œ§°¨˜úõS8tè(99yÞXB""¨Q#€àÀïè}¥ÑíäÛÁh5^žÕH.¾n(Ý( mêÆ…QL˜Ð òq 1©ñ°ææÿ˜È%©QX “îÄ+VâU‡Yß1秺ÄÚ9žŸH§N±UuY¼¨„(ÅìZõ;û’ÛS;¢1ë(|òéæ*½hi"gW6mœ`ާsŸ|õᇼù™‹­bpXÅo'€†gbFö楬ͧV\0"{;{r!°V0æ*ˆ{ö†3"@‹‘xÃP…@GñSBBC¨ø…'{^²=Çh~!̆·ÈIÖ¶Ÿù- œ”Ä@s0á6ÀžÅÎ=Y8B½U 4t]ç£ÅðbA…hÉXB Áª§ï#ÝÞf—P1ûuSE¹Fdf+&%˜Æ½Züçÿ(’/é@‚Iç@€(6VÀ‰£ÆzªÕ.êÉ%²pÑ.ìn{°cœù‡9”ë¤N”‚® Tµtû5¿°f㕺žÝ¦FÜ1a4ý2<¹½i¢tûƒŒ ri[ø« 1u}çxê" –¤P8—Åï¿n£ëT´ôۼݠԾ2Ÿ6"!sÓJþ8Kë„Z·‰åËo>dÊ‚¡t©Šž¶< ñªi’z^BâÒùL~;žë/m@xÁ~Þœfc€Ã“U\¸#?<c%aÏ>aÒî¾â]"Ëý¯@A¨îù¿•HŠ&‘H$‰äü朿‹…UÅåÒhØ ÕdÂn7KÙlVtMÃétb1›PËÈ V!ºƒBG‰çÀVërFÞæ{Š%ȹë&âzÜÉíG¦2ûó×Yé0U›&!&ÀD\÷‘\ÿçdæN•X‰ow3ÍÛW½øUÂZrÍàF¼µà¾lÛŠ›ÇÑó¾{Éxß½ÿ_j`5›Õ'Ò `"¦û½Œ+ø˜‹?á•ïÆÕ"\E=µª“ÿ×ÎÛÈ ; S£Å îÖ TöÛ3ÃoÎ¥¸¨†÷È“íÙ7ìÙ»fmå_^/|™±?ê»Ýô–ú—Ó#Ñj"íòÇú"öÏÏ lèvû© 3GR+6¥¹ØþÑ‹|`{œëë×#ôûtòN,â…‡— »Hö$´ ó ×q}Z±X1+ )½èÓ2ƒß‹’¸â¢(T\˜mÆÏT+*ÂE )Q°?“ôEoðì¶B]Çð €¶Ä5¡Žu ›1û©±|muAƒQ¥¶W»íý’2Lñô{y&ýÊ*D¢Çs3éq*³” ê ~”·—l2G_İG/bXy稡4ì7/ôw·ÿÉ”ûžegµ`L€Ý“fõôÔêQfÍò-ÀJÊ“øôF÷×¾ñfßSyæ/¡>¢Ãp+¹ÿöÌÅô}É~/¿¾s½[t]7S­~Cjä -«b;(Aq4lÓ‹A}Z©èè"†Wfˆ:%$×nÅJhLuêÕ Eõ™[X2'²o‘ϱþ™ð`Öš}5 €O—lâhaÅ–hBŸôŸdAÅù†°6[l̺C—wÓÅ]¾®ƒÉêþ™:ò)ÖL$\z ×Ìå‡uûI?¸tÀ\„úI£#‚rÍ-½qÍ]ƶt;EñŠ]'ø¾ïŸ”| NnkYú Œö«±]usŸ³š'Š)v‚)(’ø¤Ä[º®Óõ6 ZÂ’ÿíæD±…°Àl¶þ™&3Šð½ž°TŸtt·È)¹ÇF22Õø[×Q”Ê Ý/°O¬×ûk!¾ÃµÜßáZc jE)Q_à/αRÿæ·øäfÊØ§×û1¦õ.×’“…/@` Æ~ü±»Àò–<³ÒèŽ÷øÄ]j‡?FÇá'i, LÃÁñúàR;GNö–! ÆÒç…g[麕º=ó1ÝNe;Öü:žzû:¯ýXâé4âq:(Ç6T"[ æÁnãDë_¿7 " Ru”¨î<;½»÷øÀ3}ºq¨Bo¦ö o2ãðÞW¿œñ‹;yD"‘H$’³ËÃcÇž´íå—^ª²ò•y³g‰^—UNŬúõ.í;€E pi߬\¾”f-ZW™1ÿ$›6¬;ol­z…{á—ýAT‹ Hd³û×¹|ºÂÂU/g`Ó\&êoRѽøjÞºv모n§R‰ðUèÙž=¯àŠÇ\5+-Iþ%ô‚t²Õ(¢UÅì_øL‡êýxrLwbÊèTEAG·Û#@£O ª(¸…©¢`r'ÉûéÇE rõ)íY²è;®0‡Ãî]²É3°Rò¯çoáMœ„";Þ?ž³ŸË$¼z5¬.2v,åó/7óîM¹ñ>î{b8y=‘ƽRUão0_©ªŠªªX¬6¾]0^—–º”H$‰DR•lÚ°ŽÖ·÷~_ôÝ.í3Àû}Ïž=|<}:aaaäææ2bøpRRR¼û7o\OÝÔú¬]³’6í:²vÍJ:tîV©º—ü°ðÜöüJª ‚ƒÛX>o=³íÌ„'µdðC7pÅ¿,|+‹®ë`*6žØJÝ=XxÄÀ­zÊ ·”œ£¸8ºì}&,ÎÀ„M+¤Ð=• ^×D™Ê¹ŸB ¼s6Ý!Í€*<]Å'B@5Äæ“L锸à OoIŸò?׿¯ùt?I£åîgãOß³=-`ªÃE×<Äu¢1âš{ðË¡(Š;Ë´bLŸ dù%Oâ1‰D"‘H$ÿ>%ƒÕ’’ˆáÃyÊFå'|+õ>Wÿ)ñ»iú³mÂÙ#¼×lWjã>¶mØwVÌ9«•ü‚Â<ëy ·ç¦D —`¸ÿŠÐX? É¿@XâIÏ'-§B«Õ¡e·~ôo‰Zê6zEŒÛq§ zU§¢(%ÂWÂÑ“Ÿ›C€ÍV¡Ef‹•ì¬l‚‚ƒJ]Ü × á¤uéF_S|<ˆÿõ~7|ÄM§Ü?ýãN»LKb˜PyO¬§ Œ¿=÷GáÍm ¸((ÞG‰.Y™X­÷‰D"‘H$U‹j2Q\TD {z+ø¹gŸÅjõŸâêt8N?Éq)þ3â÷¿ò|þQ+¹6öí¥IÓæ~ ¹To˜)à^’HPuzç=÷Ÿ²Ü·ß|ý³YrºX©ÙûV-wÞ«?%‘ÅnAc(\·ðÞ}ŠçÏ|q{vï¢vÝÔ ë¨Q#‘۷Цm{œBÚZÅd2Bf݉–T“ê7Béá¿,€g|2ýl›PæýðÛæ(+•ðÊb¶°uËfj&'ÿ;†J$‰D"ñʉôã$%ÕòÛ^Zøääfc ü[õýgįäü¡Aã&üüÓ6nXGrí"ÂÃK<7î9žBñ=…·ßzã,Y,ù§ñ••† )·Þù¾å+ŒHy¹yìÝ»]ê7hTauSë±zÕ V¯\NÃÆM‰ŒŒ>YPÉàúó¯××7/@)„dgg²nó&\.uR* ‘H$‰DRµ$$&±o÷.1›Ë—¦š¦q"íñ5ÿV}RüJÎ9L&=zöfÇö­lٴ³m’ä"‘H$‰¤j‰¯ž@zúq6®ÿÍ[]\¦Ö4½»wLHHšËuÆõ‘øUU•¼ÜÜ3®T"©UUiÔ¸)7=Û¦HþC(ªJj½¤Ökp¶M‘H$‰D"¹àQ…F›±cëfÖ¬ZNí”T¢£«aµÚp:dgg’vä0!¡aÔ¨™däaùI*ÏHü“yâ8ðO"%‘H$‰D"‘H$’3Æd2ѨisÒO'-íöíF &«™àPkéÒÊËË#$$äŒê:#ñ[;¥.;¶n¡nÝz„„…ýí¬[‰D"‘H$‰D"¹ð)*,(s{dd‘‘QeîÓ\.t]'//—=»vÒà £CÏHüFGÇРqSöíÙEA~ÞU,‘H$‰D"‘H$IePU• à4nBttµrEô©8ã„WÑÑÕˆŽ®v¦§K$‰D"‘H$‰Dò¯!ã•%‰D"ùøsûÌË)ÿ–H$‰DòÏ!ůD"‘H$‰D"‘H.x¤ø•H$‰D"‘H$É¿‰D"‘H$‰D"¹à‘âW"‘H$’ú ›àr:Où·D"‘H$’Ž3Êö¼î««Ú‰D"‘H$‰D"‘H*E£ÆÍNûœ3^ê¨c—gzªD"‘H$‰D"‘H$gÄÊåKÏè<ö,‘H$‰D"‘H$’ )~%‰D"‘H$‰DrÁ#ůD"‘H$‰D"‘H.x¤ø•H$‰D"‘H$É¿‰D"‘H$‰D"¹à‘âWráäÀü÷Ê/¤ëgÛ– yM%‰D"‘H$ç?RüJ.0œœX·”[ŽãgÛ–ªÅ¹·öêÇc?gòïjÐR×Tä°ú™ô>•]ŽÕ‰D"‘H$‰äŒ©:ñ+ì^>qÃûÑ­sg:÷À­ãç°%O÷;æàâ‰Ü>¨;;w¡ïMÏ0w{þÉ/òά˜xÝ®xÅåÔ§g²zâP:wÈÛí§´«ü:Åû3yìúwïLçÎ]¹âÆq|ø[Z%š¬çüÁ´Q=è~ßJ üê,fß—¹m`7:wîL¯«ïcÒO‡qäþÂ];Ó¹¼Ïµ²Ïé­‚sxð²®ÜúÕñ2O¹6”A(Þ;‡{{u¦óí‹Éžr¶ðåKwqUoÃŽË®}€·9г¼bœ{™:´´ý7òÙ!×É6æoaÚ-]éÜóQÖV`Þi]ŸÿJ@5jÖJ&!‚rV-1Z=™ä¤XÏ®!‰D"‘H$I¥1WUA"½õ=E]näÑ[ã{ñþ´Iû5!ýîãùŽl™õoÝWLÔ§ÏsI´ ÂNÚÿ¾âƒ÷¦òÃ.;„5)§²6O}€'å£b9•UÖiR‹±Ç_ÂíÏÞA”vˆU³&óѸHúâzF•36àÌ`ó÷Óy÷ýùlεµïNÜß^åî Ë©3âÞíÁ‘%“˜ðô=h13¸·q î}ëur4À•Æ÷/¾ÄO ·ñü-õ±jP"ÕÍ åìä§Oßç½O×r¨wZ6”+íÆß7…Ý~^Q'‡¾›Äôí5tÏ³Ô Îâ÷ÏÞföQí³)\“XF7ÑŠÈ-V©3ê5mblS‰‹+ulñ^æ<2†ÙÇ+9Î\¹ëó_Ä9OM½ül›J0MF½ÅgÛ‰D"‘H$‰ä4¨2¡„¶aÜgsP-&Ã+Õ¹9Á›W1nó¤¹ÚPלÃÚO¾åDÒͼvÿ jY S=Á¶«^dúâÃt½¶&¦ü?xoÂlÒ;ßÍÓmçðô×eÕ¤qâ§xla÷½p ³˜W¾Q¢â:-Iýyà~Ï mhµ‹¥wüƶãNzFÙÊ,¶x÷,^˜¶•¦ÃŸ£Û3Ù¯Î|6Îý‰œ:·3vDWj˜ IÊãY}³>ÛÈ­/¶'µåEƱÎ}l Kt}Z^tAÞBœüµðe&­ŒçªñcÙôÜKdŽ 8ØýÁhFÏbôopuMc€@mçãGÞäð/ððÇx"Ós¼…¤¡“ùb¨‹[£¶MÎdÍðé¬ÞWÌ5‰Álx÷ýLÝÇ>âù^1¨z!YÅ$6hHýúA”‰žÃÚIc™ÎM¼|Û/Üûº÷"\žg—)¢âëãLcù¯òîüß8T¨žÚ|+‡–Ê ŠÙÿÃ;¼úÁB6s`ŽnÌe#ÇroŸÚ(à:¾‚)/¾ÅüßRŒJXê^œ|ÍWkg¾Æä¹+Ø›«cŽlÄU/¼ÁMÑs62ûõ7ùôç]dëÔ¸x£ID£ßØw~̲yoù(¡É´é{÷ÜÒƒ¤å´Û¢ûŠ[¯œLõ×ðüÅd¯ŸÁK¯}ÆŠ “ÔûqÞ~´‘¥Ov-ÂÄ)_óû‘bL‘õè1ì~ a* ã§WŸfÚÊÝÊ*Âêtaؘ‡¹¦iX×´˜Ïæ®×óÙGב¨Tæ|üíóyóõOX²=- žVîbÜè®ÄŸjüJ"‘H$‰D"©"ªÐ‡¦`²˜J¾jËrbI¨M” °díŸN¢{¶¡ºûeWjA—d˜¼f7×Ô$<´-O~1UÕ88kn™µ¸Ž|ÃswÒõ¹é÷-³OeReêô†m ìéÛY4w¹Õ»Óµ¦µÜbÞͬ *Y,þ¹ÔNá /ÏÁQ{Tƒ%–Æu(Þû'Îö„Tø²o!éÚ)|}ŠR¸–GË-=¥ €Ð\¸4 Ýãáùlxï)æ…ŽdêMMH{¦Ô ªÙχîÈ>F>1tŠu_Ý…¦¹Ð„§™¹dÚÍÄæãD~"ÕBJ‡âêdþúÏ-¯ÏC ¡ÎÆeþõ•*¯Òˆ6Lº‹ÇÅsý˜·è›Ãš&0iÌDjÎ~Šöa¥OÐÉ\þw¼°‘f£žäíÖ¡¤ýô/MC@­YÜÛ¨˜•/?Åg‡{ñðkýI , 혙V@±uÊ]<ø™‹®#çöÆá83² M°‚s?Ÿ>pï§·ã–Ço£Ià!–N›ÄSwä8ãÚ‡«¸ŽÿÁš] ƾH§ÈúóG¦O{šQ‡aæs—PM©¨-§ˆ).ÚÄ;OLcs‹»yù‘f„;ŽsÈ^—“”ªNöª—¹ýé¥Ä ¸‡»%P¸é &½}'côO˜|m2=Ÿ=ë¶^{$ãk@`ÑAVL›w~:sž¢]h÷¤Âó\G¾æÑ»ßâX×»xñžF˜ö|Ãë¯>Î#á3™zCò)ã7$‰D"‘H$’ªà µsà« ¼·7•áS»¥ZGó <>¯D6…‘©¢g'_ƒp³‚zªèX-ï_zŸ#½'ðB«P”Ã¥vPäÔŠ™@*S'`ßΛ×â‹ã€šÊ “n¥i°èØ q¸¤b (È‚Š‚ªe 75œz%À¬O™³¾7µŠ†ÜÃÊÓÀå(miµ‚y§°+©£f°l”ç» `Ó4^XÏm Ñâ í%ëÙkyïÅE(—ŒçºTCü·Ë—¿Žõã*Ê'0Zç×§oä' ¹37<ð×·ŠDô¬U¼9q=-ÆL§[5y¥l/]^eѳVñá7´|ä}FöŠFê?|‚åC§ðÍŽBÚ·)uÕ\GXüáÏè—¼Êã×·%Dæufϲ[X¼x/·5ˆ"#Ó)¶ ·jD¼ ¸£íEö>üâÉ·|ÂS7Ööh…ëg0kg$'?͈f@ZÖUÙ;t"üxm†TwM³Ni¡@Û´ŽÉbè Óøú@F„WÔ–àò/„3‡ãùÑàbZ7©•4*ë8-%,&·éÃL{ q*pQSâ2®å®™3Ù:ð1Z¸Ÿ!µ/¦CÛ†ØhCËêGXuë,Ýk§]óÊÝ›òÏWØ=ïþ»’)_ECФ6c6.ã¾E¿rôÚd’þ£¡ì‰D"‘H$’ªåÅìýò î~ë0ÝÇ¿ËõuË>}trV¿Ãû{Û2n|S‚ðO­TÀºgúóà OúÙ–¼8ïêÊmMá†×§ÑýÈ>~_0î¾ Ó”÷¸%y/“¯Í|OxpêýÌ:˜xÓ© ³2lÕÍ@ù)zõÌÕ¼uÏ8~ˆÉä±](oÚ³¹ú&ÎÂIÖÞßXðÎ L}à1B?}‹A N¶Mƒßjfz§(Ô²úá8º‰.™Ï ¤ësþû¬'ŠÐ)‚m?̆CP°w —ÿTª Grp™Ðíæ+˜3î%®»þú¹Š+¯hCb€‚ýÈìvFÑåâê¥<“NNlý“|k:¥z·š¢[Ð.f­?ˆÝ+~}Q‰nÕÚLdýþB®‹­¨-Áåg¤ iÍMפrß”á\³¶/C®ºŠêœìù->ȺPsD3Ÿ{DÝNõ±|½ƒmé.ZÄŸ\¼¥Z]bÈ'=_ƒ3H¯åw¾ngÿÖpb£zÎñ?0â¹.þ±a8‰D"‘H$‰ÄCÕ¾rŠböÍÇ“ŽÐ}ü»<Ø5ÆÏã ÒòÐ<k¹ÉÖQ#b ©P±mþrr²5¹Â_Åìu);þœ #_ã!NCj™ÂI .bEeêT¬D%Õ'*©>ÍZÖ¢ðº‘Ìýl×=YŸ«Ÿ“.ÅFže5$¹\1è‹\Ÿ¡/Îep~:Çs!Ö¼rý8v´jHÄYX\Êyh)ßï-æÐ”è1Åwϳôô“?K³Ð³Vóư(f4oO¸–z•Iå«XˆLéÄÜɪÁø~}¢ÒXøã1 r^âÊn/ùþà¥×sÓô¸¹Îº €`º<þ&#ëû¬(ÄD¢R:ó·@ˆø"/ Iô±jp<¨wËŒW°tÞgÌxg óft物OÑEÿÖJòxôu¶å¨!4¿}* z¯`Á§3˜ñØp¦·¾—©¯\IrÄ+&3*âôÃÒË<߸$ßÌ›ã»ûý†sñU5>&‘H$‰D"‘œ‚*¿‚ü?&3fÒ>:>3•1¾ÂÀZ“6õ,,üýwÒœ I¶€ž¹‘_÷Cò¨ºW¨³i1öf–,ø£¥-äч¾§áÓ¯s[Û"Bâi]Çç=ë êTPÐ.„Lb“V$žÑõP°„ÄP#(—õ“'³¬¸wõ¯Cù3‰ÿ9̉C˜8³‡Ïº·ElyóN^ʺ…ÉÏ^N}à:Ì‚'žàÛ°[yï¥J _?|b²Õcä{3êòN8&oõ³Ü1%”‡ÞKךgÞí¬ÕSSýŠ]ûª_Z‹òu“nÌw¶%Ь¬Úv” {;Q£œª-Q¹täs\Òïkîú Ÿ-¿îÝ›PSýŠõÿ;г±oس…˜F©;þ`Åž"Ú63¼¿ZÆÖ¤+kbN^¥K÷çjþ"žAIØ¢*Û–ò0–Ò•žèÌ]ŸäÚÇf³pîHõée¶DZ%Áê՛Ȗl„=SÈîâ ½˜†ÕNç^èœÑx€B­†Õà›-dF §UeF$‰D"‘H$’*¦êįv”Eï, ³Ñm\ŸÉî?ݱªè¤dªÙ"h{c_bîþg&Grk{Û>}ƒ ¶N<}iJ¢›D-Ÿ-.s6ÅLDBñe¹ŽÕŠêt‘öóL¾Ë¬Iƒ¤,öãl]ô1Ÿ §÷ƒõ 8ÃK!™ØóGleÕ·³Y°Q¡ë¸Wü7DßéQ:Ûs8 Éá>û I6¡ÇP31 ›…›?fÚÆP.}ºâ¯üé>R ®AJb0E~Ù™ÃØÿÕ'¬4×£^õ`ôÌ],ÿl*Û-ÍsQªb"*1™¨’+BΞS0ñI „[&–í¹Ô¨ŽŒèÃý³æ õVúNäÔKIDAT·¬Žµð(û³ksy¿Æ„ª&‚cCaÕr~ú£+C/JäÒ™ñô[Œy®ˆ½­æqx_Í÷¡¶µ€í_/ä@l}’#Läþ¹…cº™ÄH¦¨ŽÜÜ/†{?x'õ‘ômƒ9ÿ8IÝèÑìF†Õ[Ê”'ž&ú®!4 <ÌÒio±5ü2^îç3ð³“9S>CëP‡ìu|>iÎ&÷ѯöÿÛ»óè¨Ê<ãÏ­ld! PI‹€0¢qDAÅ¥aF[Ž;íÒ­£Òê4¶´Ò(-‹¸¡ 0ÌÈ ÊHC Q9êöU@ Y„$$@B’ºïüQK*•©2!xëûñ”•ª{«î½¿Ò?žó{ß÷ÆÊu¦k9ÝO¼KŸ}´^ ÛgªIL±vý°GÅJR“„ E7Wÿ{ûkæ35êU·F\Ý\'ÖÎ×ä ÔñáSçxéÌуkÚ¨†¿–Oœ²‡W—…“ôç'_Ñ‘áW++ÙVÁÞ½Šê5D}ÒMµ«“µ©öÒXÉ­ÙiT^:M¿¿/pCSÝþö\=Ò>N‰]GêµÑÒKÓ&èéùn%eöÕ£¯=©>iuÕ ²ÎpÌr•Ù¥ÕsgkÆ¡I1jœÕKÃ_xX¿½4¥Æ,Xù¾…}ÿ»Úߨµ.ì1Xcß½S}³“Âþ¾ðSÓ6]™~^¿I…:¬EÏ?¨E›.zA‹&÷Qtà¼]S¦¢#Û´tá½™_.)Qé]¯Óã¯?¤O?!ºÒù…Åj¨¿Ÿª—OÒ[ &hÔ,·•¢Ì¾êêAÔPqj{ÛðÝ«zÿõ¥êýÎ]j}ÍXMw¿¥I3æèÅeÇe«Æiô Ô&ªH»~øX“–íÖqIŠOW÷ÛÇêé«ËeIÝ›¦ñ©¯iúüñ5Ö´Pï'º«wV Ÿø7E¿:I³Ç=¡wìJïq‹žw¿.K ü¥c¤Ÿhâ'{Ul¥(«ÏCzíCä¹uò™®åÔìãûµñó7ôñ䣲%Å6í¬žþw Nþ_Ú¥Ôœ§ôÆó©š0}ŠF-(‘+5[×ün²¿­¦«,×´†séD·¸E/O֔ɳõöó‹T")þ¼nº­Ã`õIw)´ÿ^€ÐYóçÍ6ý ¬ÑÎ_­úB× ¼I¹+—ëò«®©ãS~ÝŽù˜®Uª1ŸLU¿ÔÐPUîÊ庰ÓEúöë\å\Ù»FŸYºdñYnFP¿Çãîš@I¼b’V®ªï³ ÑùDÂ/Àñ¿Ç#üð p<Â/Àñ¿Ç#üð p<Â/Àñ¿Ç#üð p<Â/Àñ¿Ç#üð p<Â/Àñ¿Ç#üð p<Â/Àñ¿Ç#üð p<Â/Àñ¿Ç#üð p<Â/Àñ¿Ç#üð p<Â/Àñ¿Ç#üð p<Â/Àñ¿Ç#üð p<Â/Àñ¿Ç#üð p<Â/Àñ¿Ç#üð p<Â/Àñ¿Ç#üð p<Â/ÀñB ¿.—KE……uu.œ‘mÛ!&:”ã•÷ó!IRîÊå! €_ª¨¨HIII!}&¤ðÛ&«­6oX¯¶mÛ))9Y.£¦g‡mÛ***ÔŽm[Ô¡S—>RøMKkªºhçŽm:~¬(¤ðK¸\.%$&©C§ÎJKk¢²²Ò6¤ð[VVªääduý—‹C>IjK(ÁWbµg@ üð p<Â/Àñ¿Ç#üð p<Â/Àñ¿Ç#ü/º¾OÀÙgŒQ~Þ-”Û].cL}ŸÒ¯Btt´’“S”Ú8M.—ÖwPûðüÒÚ~€cŒÑž=»§¬víŸ Ë /ÈEcŒŠ‹OhßÞ=Ú»g—Z¶º äºQûðÔFí ¿@„É;rX1QÑÊÊn/cŒÿaYV¥.¤ïu`ÈpR—28<ù®­º÷}µˆOPÛìöÚºy£òóލqZ“ŽIí=ê£öÌù"L^Þ¥·låí äTáĉ|TR•Z]0•¤Œ–­”wøpÈÇ¢ö•ÍÚ~€Szò¤%Uí0ú^?$ɶíz;çº`Ûv¥k:ݵÒ„„D•–•†|LjïQµgØ3a쀎Zpw­ºØysR îºoó½Ì·»Ã¨µ÷¨ÚÓù"Œ –ïµ1’1 ^BÈxßÜ^;2íûl²^šö•òíÚúÎÐ º¶j»®Õ<üÁ,ŒVÿµ¯ÿº×Wí ¿@„©4ÜT’,KÆû·Ðq³mÛD¼Ûj÷Q®Ãërõí–#*5uñý¡?|רm¬n?_×2œNlý×þÜ«ûÙª=Þ€ct%ÉØvżSÉßuóÿ&Sü“¾œ?W ¿øA;òJ%Å«iv7õ:BwôLØÑT$ zTeue©¢뫃eùÿ;üÖuíí"m]6OóçjíOGU.KIÍ;ªÇµ¿Ñ=7eVìwŽÔ]:;µ'üÆØv¥Ž›¯ûè߸ân¸Ç8¶N3þøœím¢n7Ü¡‘ÏW|Y¾vo\¯£å• V:—úTy;¯VÀû–:•a{®ÓÚ»ó´zòSzeÅQe\z£îº¹­šÄ”èÐë´ù¨-WÀØês¦îÒY©=áˆ0v`G1  Y‰ï½€…™jÌkã¬WµhokÝ6þEÝ™ïïâå\}½Ï1LEҜԞ35}î2m<\¦¨FíÔgØÃѧ¥XRéŽÙ;îcm-,—” ŒƒõÀÈ[Õ9Ù%¹óµföTÍYµA»óNJJRבÕ³×4QTÈò²¬ÊóS}û†Þ†Ñ™­ÛÚ|ó¦^_Q¤N#&jÌÀŠõmÊé£Á’¤êê^ªý¹s4}ÎçZ{ð¤¢R2•3ä^Ý7¨½’,I¦X;–¼©©ÿµJ; ×L=ïy^Oõ;OQ2:¾í3½óö|­Ú^ w\3ué·F¿TÍbB.O…:¨=áˆ0ÁóNƒcDp÷Ñ îÊɉMúderÑmãýßÌ<Ù*øf²F¿¾Q†=®q%éЗ35uÊ8ŵø›F´‹“«I y¨³6ŠWù?¿Öœiÿ© ³:iÚÈ ÕÀ.ÔÖÕßë@ó;õ‡‘•lŸZ¥ÊuŠc×D¥NcÀ³/ŠÖÆœßZ­½É×wŸ¬QIÚ@ÝÕ¯…bªù~)¸îF…ßMÓ¨Ws•Ö„ž¹ì<oúT3Þ{Vÿa¿¦q7eH»?Ò„·¿Q“ÛŸÔ¸i2ùûUx¾§¶å?×ø±ïêð¥÷èéÙŠÞ½ToME/%OÒË·´;pÖEí ¿@„ñcloS­êíd*¿B×WRyþÚwRj~Q†È®¶¹)y†ÿzº›¶Lù­˜÷•ìËGë‘!Ý”hIZ߯]_?­U+vixv¶b“³Õ³§÷ãm[ê®uËõìæ:\ÖAÆó}‰­»ªG×¶þާ1áÞ(0~UÌI5Þ®©eEý¢ð['µ?yHÛE]ÐYÍcjZ÷ƒZ9w¥Žux@/ß{­š¸$ui§´üG4öækkÿß)ëøÏ:¦$uïÒEí3ãe)Ë{ž%Úùé|mh8Pã¼^mc%µËÐý¿Ñ +¾ÕÁÁéJ«í^7µ'üÆøV^]·šfŒÚgyž?`Ù¶ª)á϶mÙ¥ÿÔ†ýÒ‰Ÿ^Ôݹ•÷Œ:X¨2Û­²ÝÿÐ왋ôí¶ý*(‹Q¼uRj\¢R·-[Þ•‘½+%ÿ²;âZ²,S%<cäry¶ïµ…ªNkoÙ5­~§ ºïÓÚ}Ró¡í•,[žKŠS«‹3½t‡¶)S‡67êæ ×hö˜‘Úvåuºþ†þÊÉJQ”9¦Ÿ¶‘Ž,Ò3ÃU>Lò-µu~\ÍOߣîjOø"Œm|ÝG#Ûö~=eó½/Õt±Õ°…šFK;6íÓ‰>©Š¯þ,¼á̽¹q¿'ôÔuçW *®„¦Š=¾QoýiºV7H÷=ùZ'•iûüWôÖn_Àön;Œðë]m¹byáJáË×u´,K¶mËåIaþ:†¢Nk•¢Vi’{ßf<ÙM­«s\÷ŠcÖÍÛ%Û–ÓB7Ž™¢‹ÿïZ¼p¡^õ‘Ýò‚ÆMó„팡óX/¥¬cfE'©i´/LŸÎÙ«=÷ù"Œÿ²¶-clÙno`t»=aÇí®ò0¶g[MŠÏV¿îñ:–;OË~*9Å~ÞcÜrÛ¶LtSµo&åm?¤ç¥+#½âÑ<5F­0F]n¹YW^ØF­ZµQ›fñþî²±mï$VSãó¬xx®¯úëöÖÃÐñž» {ØsÕ^)êvM{Eýü¿š÷Õ!•רîÍÔ)]:ðý&å—ûö9¡]kvª<ñe¥XÞsSó®tjlÿdí\ò©v”4PFf#éÐV$5¯ô›¥7KVŒ9·jOçˆ0ÆÛq´ýaÂ,$ÿʺóB«®6|æûÏ&¨Ë°»ÕsÓúàc´c`_uÏLSû˜íܦçÔ¿^™ª¤ï¿UîúSåÜ|±N™¥—¦kHN–R]Çuh_‰Ú]{•Z$f¨Ub™Ö,X ꮌD[{~.‘dä¶Ýrû:šÆ-·Û-w êpÚ!ÅÞÕ†o…aﳑ%ã²å霆¾”V]×¾QÎ=–;V³¦=£ç6 TŸ®-•S¦Âý?jËÑöºýŽNUê~é­9úhâ Mx·L·^ÒTÅ[–jöò"eÞ9H™1n<°FËÖI-[¥)®ü°Öî>.ŧ¨¢Ô²ÿ@µ[öÞøËÛÊÔC-“l8(×E}Õ«Ù©ãf}Ôžð DOMÞ9§¶gˆë)æŸV¹ïl 3‡«qŽFþ¹‘þ>¡–ýý­>î–­†Í³uñ€r¹M¬Zö¿]Wl˜©¬VÏ? Vú%ê9÷‡šµàSMÿú„Œb”’yµ¸âJe4ì¨áUÉÌÏôÆ_<óK­¸d¥wi¢8Ü1(à>g✕–”òNcddy†ãÕÃw÷ð;¿uXûètõr¼š~ö¡­X¬wVËHŠmÔR{µQ©]MÝ»ŽÐ؇ê½ÿž«‰ËKåjØZ½†Ö]ýÓm¤’‚úfÁÍÊ/—¥äÖ=ô›‘ƒÕ*Z²šõÓc¢4wÎb}8å ”Ô ­ƒ®kÓ[—4=uܬÚ[óçÍ6ý ùƒ~–-]¢ƒ‡¨´´Tƶåö‰ ¤ä™ûé $Rø7 :×y"˜‘çZ+õ$^[–%—+J.—¥˜ØX-þø#õí7 ¤#Qû`g§öK—,¦ó DšÊÝGo+Í¿ÐPà‚C¾mVÅT'2vÅP[Ûö^§å]aØ3ÜÖ¿«±eŒ«–:¿ÔþlÖžð Dï[O·±bžiåù¤óO½Ï–C›¾k³U1Çֿ°1rY–dyº“¾:ïbN¡ŒÚ:›µ'ü&:&VùJHLð¦IÆ’ä žûËÛÈòþ#UÜv¦¶ÜýÛ{*½~ÿ½µöÝ5á_tÉÈ;ä6 |¹¼×l¼Ï²üTmc”ŸwD±±!ßÄöœ©}m þ ƒê7­—Ú‡ü ¿j-´yÓz]Òë2•ãé*Z’ånj¹$#¹¢\a®ö\3³f¾_+߮ꮫÒ{–ç_Á‹PÅDÇhÃúujÙºuÈÇ #include #include namespace INDI { namespace AlignmentSubsystem { /*! \class TelescopeDirectionVectorSupportFunctions * \brief These functions are used to convert different coordinate systems to and from the * telescope direction vectors (normalised vector/direction cosines) used for telescope coordinates in the * alignment susbsystem. */ class TelescopeDirectionVectorSupportFunctions { public: /// \brief Virtual destructor virtual ~TelescopeDirectionVectorSupportFunctions() {} /*! * \enum AzimuthAngleDirection * The direction of measurement of an azimuth angle. * The following are the conventions for some coordinate systems. * - Right Ascension is measured ANTI_CLOCKWISE from the vernal equinox. * - Local Hour Angle is measured CLOCKWISE from the observer's meridian. * - Greenwich Hour Angle is measured CLOCKWISE from the Greenwich meridian. * - Azimuth (as in Altitude Azimuth coordinate systems ) is often measured CLOCKWISE\n * from north. But ESO FITS (Clockwise from South) and SDSS FITS(Anticlockwise from South)\n * have different conventions. Horizontal coordinates in libnova are measured clockwise from south. */ typedef enum AzimuthAngleDirection { CLOCKWISE, /*!< Angle is measured clockwise */ ANTI_CLOCKWISE /*!< Angle is measured anti clockwise */ } AzimuthAngleDirection_t; /*! * \enum PolarAngleDirection * The direction of measurement of a polar angle. * The following are conventions for some coordinate systems * - Declination is measured FROM_AZIMUTHAL_PLANE. * - Altitude is measured FROM_AZIMUTHAL_PLANE. * - Altitude in libnova horizontal coordinates is measured FROM_AZIMUTHAL_PLANE. */ typedef enum PolarAngleDirection { FROM_POLAR_AXIS, /*!< Angle is measured down from the polar axis */ FROM_AZIMUTHAL_PLANE /*!< Angle is measured upwards from the azimuthal plane */ } PolarAngleDirection_t; /*! \brief Calculates an altitude and azimuth from the supplied normalised direction vector * and declination. * \param[in] TelescopeDirectionVector * \param[out] HorizontalCoordinates Altitude and Azimuth in decimal degrees * \note This assumes a right handed coordinate system for the telescope direction vector with XY being the azimuthal plane, * and azimuth being measured in a clockwise direction. */ void AltitudeAzimuthFromTelescopeDirectionVector(const TelescopeDirectionVector TelescopeDirectionVector, ln_hrz_posn &HorizontalCoordinates) { double AzimuthAngle; double AltitudeAngle; SphericalCoordinateFromTelescopeDirectionVector(TelescopeDirectionVector, AzimuthAngle, CLOCKWISE, AltitudeAngle, FROM_AZIMUTHAL_PLANE); HorizontalCoordinates.az = ln_rad_to_deg(AzimuthAngle); HorizontalCoordinates.alt = ln_rad_to_deg(AltitudeAngle); }; /*! \brief Calculates an altitude and azimuth from the supplied normalised direction vector * and declination. * \param[in] TelescopeDirectionVector * \param[out] HorizontalCoordinates Altitude and Azimuth in degrees minutes seconds * \note This assumes a right handed coordinate system for the telescope direction vector with XY being the azimuthal plane, * and azimuth being measured in a clockwise direction. */ void AltitudeAzimuthFromTelescopeDirectionVector(const TelescopeDirectionVector TelescopeDirectionVector, lnh_hrz_posn &HorizontalCoordinates) { double AzimuthAngle; double AltitudeAngle; SphericalCoordinateFromTelescopeDirectionVector(TelescopeDirectionVector, AzimuthAngle, CLOCKWISE, AltitudeAngle, FROM_AZIMUTHAL_PLANE); ln_rad_to_dms(AzimuthAngle, &HorizontalCoordinates.az); ln_rad_to_dms(AltitudeAngle, &HorizontalCoordinates.alt); }; /*! \brief Calculates equatorial coordinates from the supplied telescope direction vector * and declination. * \param[in] TelescopeDirectionVector * \param[out] EquatorialCoordinates The equatorial coordinates in decimal degrees * \note This assumes a right handed coordinate system for the direction vector with the right ascension being in the XY plane. */ void EquatorialCoordinatesFromTelescopeDirectionVector(const TelescopeDirectionVector TelescopeDirectionVector, struct ln_equ_posn &EquatorialCoordinates) { double AzimuthAngle; double PolarAngle; SphericalCoordinateFromTelescopeDirectionVector(TelescopeDirectionVector, AzimuthAngle, ANTI_CLOCKWISE, PolarAngle, FROM_AZIMUTHAL_PLANE); EquatorialCoordinates.ra = ln_rad_to_deg(AzimuthAngle); EquatorialCoordinates.dec = ln_rad_to_deg(PolarAngle); }; /*! \brief Calculates equatorial coordinates from the supplied telescope direction vector * and declination. * \param[in] TelescopeDirectionVector * \param[out] EquatorialCoordinates The equatorial coordinates in hours minutes seconds and degrees minutes seconds * \note This assumes a right handed coordinate system for the direction vector with the right ascension being in the XY plane. */ void EquatorialCoordinatesFromTelescopeDirectionVector(const TelescopeDirectionVector TelescopeDirectionVector, struct lnh_equ_posn &EquatorialCoordinates) { double AzimuthAngle; double PolarAngle; SphericalCoordinateFromTelescopeDirectionVector(TelescopeDirectionVector, AzimuthAngle, ANTI_CLOCKWISE, PolarAngle, FROM_AZIMUTHAL_PLANE); ln_rad_to_hms(AzimuthAngle, &EquatorialCoordinates.ra); ln_rad_to_dms(PolarAngle, &EquatorialCoordinates.dec); }; /*! \brief Calculates a local hour angle and declination from the supplied telescope direction vector * and declination. * \param[in] TelescopeDirectionVector * \param[out] EquatorialCoordinates The local hour angle and declination in decimal degrees * \note This assumes a right handed coordinate system for the direction vector with the hour angle being in the XY plane. */ void LocalHourAngleDeclinationFromTelescopeDirectionVector(const TelescopeDirectionVector TelescopeDirectionVector, struct ln_equ_posn &EquatorialCoordinates) { double AzimuthAngle; double PolarAngle; SphericalCoordinateFromTelescopeDirectionVector(TelescopeDirectionVector, AzimuthAngle, CLOCKWISE, PolarAngle, FROM_AZIMUTHAL_PLANE); EquatorialCoordinates.ra = ln_rad_to_deg(AzimuthAngle); EquatorialCoordinates.dec = ln_rad_to_deg(PolarAngle); }; /*! \brief Calculates a local hour angle and declination from the supplied telescope direction vector * and declination. * \param[in] TelescopeDirectionVector * \param[out] EquatorialCoordinates The local hour angle and declination in hours minutes seconds and degrees minutes seconds * \note This assumes a right handed coordinate system for the direction vector with the hour angle being in the XY plane. */ void LocalHourAngleDeclinationFromTelescopeDirectionVector(const TelescopeDirectionVector TelescopeDirectionVector, struct lnh_equ_posn &EquatorialCoordinates) { double AzimuthAngle; double PolarAngle; SphericalCoordinateFromTelescopeDirectionVector(TelescopeDirectionVector, AzimuthAngle, CLOCKWISE, PolarAngle, FROM_AZIMUTHAL_PLANE); ln_rad_to_hms(AzimuthAngle, &EquatorialCoordinates.ra); ln_rad_to_dms(PolarAngle, &EquatorialCoordinates.dec); }; /*! \brief Calculates a spherical coordinate from the supplied telescope direction vector * \param[in] TelescopeDirectionVector * \param[out] AzimuthAngle The azimuth angle in radians * \param[in] AzimuthAngleDirection The direction the azimuth angle has been measured either CLOCKWISE or ANTI_CLOCKWISE * \param[out] PolarAngle The polar angle in radians * \param[in] PolarAngleDirection The direction the polar angle has been measured either FROM_POLAR_AXIS or FROM_AZIMUTHAL_PLANE * \note TelescopeDirectionVectors are always normalised and right handed. */ void SphericalCoordinateFromTelescopeDirectionVector(const TelescopeDirectionVector TelescopeDirectionVector, double &AzimuthAngle, AzimuthAngleDirection_t AzimuthAngleDirection, double &PolarAngle, PolarAngleDirection_t PolarAngleDirection); /*! \brief Calculates a normalised direction vector from the supplied altitude and azimuth. * \param[in] HorizontalCoordinates Altitude and Azimuth in decimal degrees * \return A TelescopeDirectionVector * \note This assumes a right handed coordinate system for the telescope direction vector with XY being the azimuthal plane, * and azimuth being measured in a clockwise direction. */ const TelescopeDirectionVector TelescopeDirectionVectorFromAltitudeAzimuth(ln_hrz_posn HorizontalCoordinates) { return TelescopeDirectionVectorFromSphericalCoordinate(ln_deg_to_rad(HorizontalCoordinates.az), CLOCKWISE, ln_deg_to_rad(HorizontalCoordinates.alt), FROM_AZIMUTHAL_PLANE); }; /*! \brief Calculates a normalised direction vector from the supplied altitude and azimuth. * \param[in] HorizontalCoordinates Altitude and Azimuth in degrees minutes seconds * \return A TelescopeDirectionVector * \note This assumes a right handed coordinate system for the telescope direction vector with XY being the azimuthal plane, * and azimuth being measured in a clockwise direction. */ const TelescopeDirectionVector TelescopeDirectionVectorFromAltitudeAzimuth(lnh_hrz_posn HorizontalCoordinates) { return TelescopeDirectionVectorFromSphericalCoordinate(ln_dms_to_rad(&HorizontalCoordinates.az), CLOCKWISE, ln_dms_to_rad(&HorizontalCoordinates.alt), FROM_AZIMUTHAL_PLANE); }; /*! \brief Calculates a telescope direction vector from the supplied equatorial coordinates. * \param[in] EquatorialCoordinates The equatorial coordinates in decimal degrees * \return A TelescopeDirectionVector * \note This assumes a right handed coordinate system for the direction vector with the right ascension being in the XY plane. */ const TelescopeDirectionVector TelescopeDirectionVectorFromEquatorialCoordinates(struct ln_equ_posn EquatorialCoordinates) { return TelescopeDirectionVectorFromSphericalCoordinate(ln_deg_to_rad(EquatorialCoordinates.ra), ANTI_CLOCKWISE, ln_deg_to_rad(EquatorialCoordinates.dec), FROM_AZIMUTHAL_PLANE); }; /*! \brief Calculates a telescope direction vector from the supplied equatorial coordinates. * \param[in] EquatorialCoordinates The equatorial coordinates in hours minutes seconds and degrees minutes seconds * \return A TelescopeDirectionVector * \note This assumes a right handed coordinate system for the direction vector with the right ascension being in the XY plane. */ const TelescopeDirectionVector TelescopeDirectionVectorFromEquatorialCoordinates(struct lnh_equ_posn EquatorialCoordinates) { return TelescopeDirectionVectorFromSphericalCoordinate(ln_hms_to_rad(&EquatorialCoordinates.ra), ANTI_CLOCKWISE, ln_dms_to_rad(&EquatorialCoordinates.dec), FROM_AZIMUTHAL_PLANE); }; /*! \brief Calculates a telescope direction vector from the supplied local hour angle and declination. * \param[in] EquatorialCoordinates The local hour angle and declination in decimal degrees * \return A TelescopeDirectionVector * \note This assumes a right handed coordinate system for the direction vector with the hour angle being in the XY plane. */ const TelescopeDirectionVector TelescopeDirectionVectorFromLocalHourAngleDeclination(struct ln_equ_posn EquatorialCoordinates) { return TelescopeDirectionVectorFromSphericalCoordinate(ln_deg_to_rad(EquatorialCoordinates.ra), CLOCKWISE, ln_deg_to_rad(EquatorialCoordinates.dec), FROM_AZIMUTHAL_PLANE); }; /*! \brief Calculates a telescope direction vector from the supplied spherical coordinate information * \param[in] AzimuthAngle The azimuth angle in radians * \param[in] AzimuthAngleDirection The direction the azimuth angle has been measured either CLOCKWISE or ANTI_CLOCKWISE * \param[in] PolarAngle The polar angle in radians * \param[in] PolarAngleDirection The direction the polar angle has been measured either FROM_POLAR_AXIS or FROM_AZIMUTHAL_PLANE * \return A TelescopeDirectionVector * \note TelescopeDirectionVectors are always assumed to be normalised and right handed. */ const TelescopeDirectionVector TelescopeDirectionVectorFromSphericalCoordinate(const double AzimuthAngle, AzimuthAngleDirection_t AzimuthAngleDirection, const double PolarAngle, PolarAngleDirection_t PolarAngleDirection); }; } // namespace AlignmentSubsystem } // namespace INDI libindi/libs/indibase/alignment/ClientAPIForMathPluginManagement.h0000664000175000017500000000712113263645557024536 0ustar jasemjasem/*! * \file ClientAPIForMathPluginManagement.h * * \author Roger James * \date 13th November 2013 * */ #pragma once #include "basedevice.h" #include "baseclient.h" #include #include namespace INDI { namespace AlignmentSubsystem { /*! * \class ClientAPIForMathPluginManagement * \brief This class provides the client API for driver side math plugin management. It communicates * with the driver via the INDI properties interface. */ class ClientAPIForMathPluginManagement { public: /// \brief Virtual destructor virtual ~ClientAPIForMathPluginManagement() {} typedef std::vector MathPluginsList; // Public methods /*! * \brief Return a list of the names of the available math plugins. * \param[out] AvailableMathPlugins Reference to a list of the names of the available math plugins. * \return False on failure */ bool EnumerateMathPlugins(MathPluginsList &AvailableMathPlugins); /// \brief Intialise the API /// \param[in] BaseClient Pointer to the INDI:BaseClient class void Initialise(INDI::BaseClient *BaseClient); /** \brief Process new device message from driver. This routine should be called from within the newDevice handler in the client. This routine is not normally called directly but is called by the ProcessNewDevice function in INDI::Alignment::AlignmentSubsystemForClients which filters out calls from unwanted devices. TODO maybe hide this function. \param[in] DevicePointer A pointer to the INDI::BaseDevice object. */ void ProcessNewDevice(INDI::BaseDevice *DevicePointer); /** \brief Process new property message from driver. This routine should be called from within the newProperty handler in the client. This routine is not normally called directly but is called by the ProcessNewProperty function in INDI::Alignment::AlignmentSubsystemForClients which filters out calls from unwanted devices. TODO maybe hide this function. \param[in] PropertyPointer A pointer to the INDI::Property object. */ void ProcessNewProperty(INDI::Property *PropertyPointer); /** \brief Process new switch message from driver. This routine should be called from within the newSwitch handler in the client. This routine is not normally called directly but is called by the ProcessNewSwitch function in INDI::Alignment::AlignmentSubsystemForClients which filters out calls from unwanted devices. TODO maybe hide this function. \param[in] SwitchVectorProperty A pointer to the INDI::ISwitchVectorProperty. */ void ProcessNewSwitch(ISwitchVectorProperty *SwitchVectorProperty); /*! * \brief Selects, loads and initialises the named math plugin. * \param[in] MathPluginName The name of the required math plugin. * \return False on failure. */ bool SelectMathPlugin(const std::string &MathPluginName); /*! * \brief Re-initialises the current math plugin. * \return False on failure. */ bool ReInitialiseMathPlugin(); private: // Private methods bool SetDriverBusy(); bool SignalDriverCompletion(); bool WaitForDriverCompletion(); // Private properties INDI::BaseClient *BaseClient; pthread_cond_t DriverActionCompleteCondition; pthread_mutex_t DriverActionCompleteMutex; bool DriverActionComplete; INDI::BaseDevice *Device; INDI::Property *MathPlugins; INDI::Property *PluginInitialise; }; } // namespace AlignmentSubsystem } // namespace INDI libindi/libs/indibase/alignment/ClientAPIForAlignmentDatabase.cpp0000664000175000017500000005227413263645557024400 0ustar jasemjasem/*! * \file ClientAPIForAlignmentDatabase.cpp * * \author Roger James * \date 13th November 2013 * */ #include "ClientAPIForAlignmentDatabase.h" #include "indicom.h" namespace INDI { namespace AlignmentSubsystem { ClientAPIForAlignmentDatabase::ClientAPIForAlignmentDatabase() { pthread_cond_init(&DriverActionCompleteCondition, nullptr); pthread_mutex_init(&DriverActionCompleteMutex, nullptr); } ClientAPIForAlignmentDatabase::~ClientAPIForAlignmentDatabase() { pthread_cond_destroy(&DriverActionCompleteCondition); pthread_mutex_destroy(&DriverActionCompleteMutex); } bool ClientAPIForAlignmentDatabase::AppendSyncPoint(const AlignmentDatabaseEntry &CurrentValues) { // Wait for driver to initialise if neccessary WaitForDriverCompletion(); ISwitchVectorProperty *pAction = Action->getSwitch(); ISwitchVectorProperty *pCommit = Commit->getSwitch(); if (APPEND != IUFindOnSwitchIndex(pAction)) { // Request Append mode IUResetSwitch(pAction); pAction->sp[APPEND].s = ISS_ON; SetDriverBusy(); BaseClient->sendNewSwitch(pAction); WaitForDriverCompletion(); if (IPS_OK != pAction->s) { IDLog("AppendSyncPoint - Bad Action switch state %s\n", pstateStr(pAction->s)); return false; } } if (!SendEntryData(CurrentValues)) return false; // Commit the entry to the database IUResetSwitch(pCommit); pCommit->sp[0].s = ISS_ON; SetDriverBusy(); BaseClient->sendNewSwitch(pCommit); WaitForDriverCompletion(); if (IPS_OK != pCommit->s) { IDLog("AppendSyncPoint - Bad Commit switch state %s\n", pstateStr(pCommit->s)); return false; } return true; } bool ClientAPIForAlignmentDatabase::ClearSyncPoints() { // Wait for driver to initialise if neccessary WaitForDriverCompletion(); ISwitchVectorProperty *pAction = Action->getSwitch(); ISwitchVectorProperty *pCommit = Commit->getSwitch(); // Select the required action if (CLEAR != IUFindOnSwitchIndex(pAction)) { // Request Clear mode IUResetSwitch(pAction); pAction->sp[CLEAR].s = ISS_ON; SetDriverBusy(); BaseClient->sendNewSwitch(pAction); WaitForDriverCompletion(); if (IPS_OK != pAction->s) { IDLog("ClearSyncPoints - Bad Action switch state %s\n", pstateStr(pAction->s)); return false; } } IUResetSwitch(pCommit); pCommit->sp[0].s = ISS_ON; SetDriverBusy(); BaseClient->sendNewSwitch(pCommit); WaitForDriverCompletion(); if (IPS_OK != pCommit->s) { IDLog("ClearSyncPoints - Bad Commit switch state %s\n", pstateStr(pCommit->s)); return false; } return true; } bool ClientAPIForAlignmentDatabase::DeleteSyncPoint(unsigned int Offset) { // Wait for driver to initialise if neccessary WaitForDriverCompletion(); ISwitchVectorProperty *pAction = Action->getSwitch(); INumberVectorProperty *pCurrentEntry = CurrentEntry->getNumber(); ISwitchVectorProperty *pCommit = Commit->getSwitch(); // Select the required action if (DELETE != IUFindOnSwitchIndex(pAction)) { // Request Delete mode IUResetSwitch(pAction); pAction->sp[DELETE].s = ISS_ON; SetDriverBusy(); BaseClient->sendNewSwitch(pAction); WaitForDriverCompletion(); if (IPS_OK != pAction->s) { IDLog("DeleteSyncPoint - Bad Action switch state %s\n", pstateStr(pAction->s)); return false; } } // Send the offset pCurrentEntry->np[0].value = Offset; SetDriverBusy(); BaseClient->sendNewNumber(pCurrentEntry); WaitForDriverCompletion(); if (IPS_OK != pCurrentEntry->s) { IDLog("DeleteSyncPoint - Bad Current Entry state %s\n", pstateStr(pCurrentEntry->s)); return false; } // Commit the entry to the database IUResetSwitch(pCommit); pCommit->sp[0].s = ISS_ON; SetDriverBusy(); BaseClient->sendNewSwitch(pCommit); WaitForDriverCompletion(); if (IPS_OK != pCommit->s) { IDLog("DeleteSyncPoint - Bad Commit switch state %s\n", pstateStr(pCommit->s)); return false; } return true; } bool ClientAPIForAlignmentDatabase::EditSyncPoint(unsigned int Offset, const AlignmentDatabaseEntry &CurrentValues) { // Wait for driver to initialise if neccessary WaitForDriverCompletion(); ISwitchVectorProperty *pAction = Action->getSwitch(); INumberVectorProperty *pCurrentEntry = CurrentEntry->getNumber(); ISwitchVectorProperty *pCommit = Commit->getSwitch(); // Select the required action if (EDIT != IUFindOnSwitchIndex(pAction)) { // Request Edit mode IUResetSwitch(pAction); pAction->sp[EDIT].s = ISS_ON; SetDriverBusy(); BaseClient->sendNewSwitch(pAction); WaitForDriverCompletion(); if (IPS_OK != pAction->s) { IDLog("EditSyncPoint - Bad Action switch state %s\n", pstateStr(pAction->s)); return false; } } // Send the offset pCurrentEntry->np[0].value = Offset; SetDriverBusy(); BaseClient->sendNewNumber(pCurrentEntry); WaitForDriverCompletion(); if (IPS_OK != pCurrentEntry->s) { IDLog("EditSyncPoint - Bad Current Entry state %s\n", pstateStr(pCurrentEntry->s)); return false; } if (!SendEntryData(CurrentValues)) return false; // Commit the entry to the database IUResetSwitch(pCommit); pCommit->sp[0].s = ISS_ON; SetDriverBusy(); BaseClient->sendNewSwitch(pCommit); WaitForDriverCompletion(); if (IPS_OK != pCommit->s) { IDLog("EditSyncPoint - Bad Commit switch state %s\n", pstateStr(pCommit->s)); return false; } return true; } int ClientAPIForAlignmentDatabase::GetDatabaseSize() { return 0; } void ClientAPIForAlignmentDatabase::Initialise(INDI::BaseClient *BaseClient) { ClientAPIForAlignmentDatabase::BaseClient = BaseClient; } bool ClientAPIForAlignmentDatabase::InsertSyncPoint(unsigned int Offset, const AlignmentDatabaseEntry &CurrentValues) { // Wait for driver to initialise if neccessary WaitForDriverCompletion(); ISwitchVectorProperty *pAction = Action->getSwitch(); INumberVectorProperty *pCurrentEntry = CurrentEntry->getNumber(); ISwitchVectorProperty *pCommit = Commit->getSwitch(); // Select the required action if (INSERT != IUFindOnSwitchIndex(pAction)) { // Request Insert mode IUResetSwitch(pAction); pAction->sp[INSERT].s = ISS_ON; SetDriverBusy(); BaseClient->sendNewSwitch(pAction); WaitForDriverCompletion(); if (IPS_OK != pAction->s) { IDLog("InsertSyncPoint - Bad Action switch state %s\n", pstateStr(pAction->s)); return false; } } // Send the offset pCurrentEntry->np[0].value = Offset; SetDriverBusy(); BaseClient->sendNewNumber(pCurrentEntry); WaitForDriverCompletion(); if (IPS_OK != pCurrentEntry->s) { IDLog("InsertSyncPoint - Bad Current Entry state %s\n", pstateStr(pCurrentEntry->s)); return false; } if (!SendEntryData(CurrentValues)) return false; // Commit the entry to the database IUResetSwitch(pCommit); pCommit->sp[0].s = ISS_ON; SetDriverBusy(); BaseClient->sendNewSwitch(pCommit); WaitForDriverCompletion(); if (IPS_OK != pCommit->s) { IDLog("InsertSyncPoint - Bad Commit switch state %s\n", pstateStr(pCommit->s)); return false; } return true; } bool ClientAPIForAlignmentDatabase::LoadDatabase() { // Wait for driver to initialise if neccessary WaitForDriverCompletion(); ISwitchVectorProperty *pAction = Action->getSwitch(); ISwitchVectorProperty *pCommit = Commit->getSwitch(); // Select the required action if (LOAD_DATABASE != IUFindOnSwitchIndex(pAction)) { // Request Load Database mode IUResetSwitch(pAction); pAction->sp[LOAD_DATABASE].s = ISS_ON; SetDriverBusy(); BaseClient->sendNewSwitch(pAction); WaitForDriverCompletion(); if (IPS_OK != pAction->s) { IDLog("LoadDatabase - Bad Action switch state %s\n", pstateStr(pAction->s)); return false; } } // Commit the Load Database IUResetSwitch(pCommit); pCommit->sp[0].s = ISS_ON; SetDriverBusy(); BaseClient->sendNewSwitch(pCommit); WaitForDriverCompletion(); if (IPS_OK != pCommit->s) { IDLog("LoadDatabase - Bad Commit state %s\n", pstateStr(pCommit->s)); return false; } return true; } void ClientAPIForAlignmentDatabase::ProcessNewBLOB(IBLOB *BLOBPointer) { if (strcmp(BLOBPointer->bvp->name, "ALIGNMENT_POINT_OPTIONAL_BINARY_BLOB") == 0) { if (IPS_BUSY != BLOBPointer->bvp->s) { ISwitchVectorProperty *pAction = Action->getSwitch(); int Index = IUFindOnSwitchIndex(pAction); if ((READ != Index) && (READ_INCREMENT != Index)) SignalDriverCompletion(); } } } void ClientAPIForAlignmentDatabase::ProcessNewDevice(INDI::BaseDevice *DevicePointer) { Device = DevicePointer; } void ClientAPIForAlignmentDatabase::ProcessNewNumber(INumberVectorProperty *NumberVectorProperty) { if (strcmp(NumberVectorProperty->name, "ALIGNMENT_POINT_MANDATORY_NUMBERS") == 0) { if (IPS_BUSY != NumberVectorProperty->s) { ISwitchVectorProperty *pAction = Action->getSwitch(); int Index = IUFindOnSwitchIndex(pAction); if ((READ != Index) && (READ_INCREMENT != Index)) SignalDriverCompletion(); } } else if (strcmp(NumberVectorProperty->name, "ALIGNMENT_POINTSET_CURRENT_ENTRY") == 0) { if (IPS_BUSY != NumberVectorProperty->s) { ISwitchVectorProperty *pAction = Action->getSwitch(); int Index = IUFindOnSwitchIndex(pAction); if (READ_INCREMENT != Index) SignalDriverCompletion(); } } } void ClientAPIForAlignmentDatabase::ProcessNewProperty(INDI::Property *PropertyPointer) { bool GotOneOfMine = true; if (strcmp(PropertyPointer->getName(), "ALIGNMENT_POINT_MANDATORY_NUMBERS") == 0) MandatoryNumbers = PropertyPointer; else if (strcmp(PropertyPointer->getName(), "ALIGNMENT_POINT_OPTIONAL_BINARY_BLOB") == 0) { OptionalBinaryBlob = PropertyPointer; // Make sure the format string is set up strncpy(OptionalBinaryBlob->getBLOB()->bp->format, "alignmentPrivateData", MAXINDIBLOBFMT); } else if (strcmp(PropertyPointer->getName(), "ALIGNMENT_POINTSET_SIZE") == 0) PointsetSize = PropertyPointer; else if (strcmp(PropertyPointer->getName(), "ALIGNMENT_POINTSET_CURRENT_ENTRY") == 0) CurrentEntry = PropertyPointer; else if (strcmp(PropertyPointer->getName(), "ALIGNMENT_POINTSET_ACTION") == 0) Action = PropertyPointer; else if (strcmp(PropertyPointer->getName(), "ALIGNMENT_POINTSET_COMMIT") == 0) Commit = PropertyPointer; else GotOneOfMine = false; // Tell the client when all the database proeprties have been set up if (GotOneOfMine && (nullptr != MandatoryNumbers) && (nullptr != OptionalBinaryBlob) && (nullptr != PointsetSize) && (nullptr != CurrentEntry) && (nullptr != Action) && (nullptr != Commit)) { // The DriverActionComplete state variable is initialised to false // So I need to call this to set it to true and signal anyone // waiting for the driver to initialise etc. SignalDriverCompletion(); } } void ClientAPIForAlignmentDatabase::ProcessNewSwitch(ISwitchVectorProperty *SwitchVectorProperty) { if (strcmp(SwitchVectorProperty->name, "ALIGNMENT_POINTSET_ACTION") == 0) { if (IPS_BUSY != SwitchVectorProperty->s) SignalDriverCompletion(); } else if (strcmp(SwitchVectorProperty->name, "ALIGNMENT_POINTSET_COMMIT") == 0) { if (IPS_BUSY != SwitchVectorProperty->s) SignalDriverCompletion(); } } bool ClientAPIForAlignmentDatabase::ReadIncrementSyncPoint(AlignmentDatabaseEntry &CurrentValues) { // Wait for driver to initialise if neccessary WaitForDriverCompletion(); ISwitchVectorProperty *pAction = Action->getSwitch(); INumberVectorProperty *pMandatoryNumbers = MandatoryNumbers->getNumber(); IBLOBVectorProperty *pBLOB = OptionalBinaryBlob->getBLOB(); INumberVectorProperty *pCurrentEntry = CurrentEntry->getNumber(); ISwitchVectorProperty *pCommit = Commit->getSwitch(); // Select the required action if (READ_INCREMENT != IUFindOnSwitchIndex(pAction)) { // Request Read Increment mode IUResetSwitch(pAction); pAction->sp[READ_INCREMENT].s = ISS_ON; SetDriverBusy(); BaseClient->sendNewSwitch(pAction); WaitForDriverCompletion(); if (IPS_OK != pAction->s) { IDLog("ReadIncrementSyncPoint - Bad Action switch state %s\n", pstateStr(pAction->s)); return false; } } // Commit the read increment IUResetSwitch(pCommit); pCommit->sp[0].s = ISS_ON; SetDriverBusy(); BaseClient->sendNewSwitch(pCommit); WaitForDriverCompletion(); if ((IPS_OK != pCommit->s) || (IPS_OK != pMandatoryNumbers->s) || (IPS_OK != pBLOB->s) || (IPS_OK != pCurrentEntry->s)) { IDLog("ReadIncrementSyncPoint - Bad Commit/Mandatory numbers/Blob/Current entry state %s %s %s %s\n", pstateStr(pCommit->s), pstateStr(pMandatoryNumbers->s), pstateStr(pBLOB->s), pstateStr(pCurrentEntry->s)); return false; } // Read the entry data CurrentValues.ObservationJulianDate = pMandatoryNumbers->np[ENTRY_OBSERVATION_JULIAN_DATE].value; CurrentValues.RightAscension = pMandatoryNumbers->np[ENTRY_RA].value; CurrentValues.Declination = pMandatoryNumbers->np[ENTRY_DEC].value; CurrentValues.TelescopeDirection.x = pMandatoryNumbers->np[ENTRY_VECTOR_X].value; CurrentValues.TelescopeDirection.y = pMandatoryNumbers->np[ENTRY_VECTOR_Y].value; CurrentValues.TelescopeDirection.z = pMandatoryNumbers->np[ENTRY_VECTOR_Z].value; return true; } bool ClientAPIForAlignmentDatabase::ReadSyncPoint(unsigned int Offset, AlignmentDatabaseEntry &CurrentValues) { // Wait for driver to initialise if neccessary WaitForDriverCompletion(); ISwitchVectorProperty *pAction = Action->getSwitch(); INumberVectorProperty *pMandatoryNumbers = MandatoryNumbers->getNumber(); IBLOBVectorProperty *pBLOB = OptionalBinaryBlob->getBLOB(); INumberVectorProperty *pCurrentEntry = CurrentEntry->getNumber(); ISwitchVectorProperty *pCommit = Commit->getSwitch(); // Select the required action if (READ != IUFindOnSwitchIndex(pAction)) { // Request Read mode IUResetSwitch(pAction); pAction->sp[READ].s = ISS_ON; SetDriverBusy(); BaseClient->sendNewSwitch(pAction); WaitForDriverCompletion(); if (IPS_OK != pAction->s) { IDLog("ReadSyncPoint - Bad Action switch state %s\n", pstateStr(pAction->s)); return false; } } // Send the offset pCurrentEntry->np[0].value = Offset; SetDriverBusy(); BaseClient->sendNewNumber(pCurrentEntry); WaitForDriverCompletion(); if (IPS_OK != pCurrentEntry->s) { IDLog("ReadSyncPoint - Bad Current Entry state %s\n", pstateStr(pCurrentEntry->s)); return false; } // Commit the read IUResetSwitch(pCommit); pCommit->sp[0].s = ISS_ON; SetDriverBusy(); BaseClient->sendNewSwitch(pCommit); WaitForDriverCompletion(); if ((IPS_OK != pCommit->s) || (IPS_OK != pMandatoryNumbers->s) || (IPS_OK != pBLOB->s)) { IDLog("ReadSyncPoint - Bad Commit/Mandatory numbers/Blob state %s %s %s\n", pstateStr(pCommit->s), pstateStr(pMandatoryNumbers->s), pstateStr(pBLOB->s)); return false; } // Read the entry data CurrentValues.ObservationJulianDate = pMandatoryNumbers->np[ENTRY_OBSERVATION_JULIAN_DATE].value; CurrentValues.RightAscension = pMandatoryNumbers->np[ENTRY_RA].value; CurrentValues.Declination = pMandatoryNumbers->np[ENTRY_DEC].value; CurrentValues.TelescopeDirection.x = pMandatoryNumbers->np[ENTRY_VECTOR_X].value; CurrentValues.TelescopeDirection.y = pMandatoryNumbers->np[ENTRY_VECTOR_Y].value; CurrentValues.TelescopeDirection.z = pMandatoryNumbers->np[ENTRY_VECTOR_Z].value; return true; } bool ClientAPIForAlignmentDatabase::SaveDatabase() { // Wait for driver to initialise if neccessary WaitForDriverCompletion(); ISwitchVectorProperty *pAction = Action->getSwitch(); ISwitchVectorProperty *pCommit = Commit->getSwitch(); // Select the required action if (SAVE_DATABASE != IUFindOnSwitchIndex(pAction)) { // Request Load Database mode IUResetSwitch(pAction); pAction->sp[SAVE_DATABASE].s = ISS_ON; SetDriverBusy(); BaseClient->sendNewSwitch(pAction); WaitForDriverCompletion(); if (IPS_OK != pAction->s) { IDLog("SaveDatabase - Bad Action switch state %s\n", pstateStr(pAction->s)); return false; } } // Commit the Save Database IUResetSwitch(pCommit); pCommit->sp[0].s = ISS_ON; SetDriverBusy(); BaseClient->sendNewSwitch(pCommit); WaitForDriverCompletion(); if (IPS_OK != pCommit->s) { IDLog("Save Database - Bad Commit state %s\n", pstateStr(pCommit->s)); return false; } return true; } // Private methods bool ClientAPIForAlignmentDatabase::SendEntryData(const AlignmentDatabaseEntry &CurrentValues) { INumberVectorProperty *pMandatoryNumbers = MandatoryNumbers->getNumber(); IBLOBVectorProperty *pBLOB = OptionalBinaryBlob->getBLOB(); // Send the entry data pMandatoryNumbers->np[ENTRY_OBSERVATION_JULIAN_DATE].value = CurrentValues.ObservationJulianDate; pMandatoryNumbers->np[ENTRY_RA].value = CurrentValues.RightAscension; pMandatoryNumbers->np[ENTRY_DEC].value = CurrentValues.Declination; pMandatoryNumbers->np[ENTRY_VECTOR_X].value = CurrentValues.TelescopeDirection.x; pMandatoryNumbers->np[ENTRY_VECTOR_Y].value = CurrentValues.TelescopeDirection.y; pMandatoryNumbers->np[ENTRY_VECTOR_Z].value = CurrentValues.TelescopeDirection.z; SetDriverBusy(); BaseClient->sendNewNumber(pMandatoryNumbers); WaitForDriverCompletion(); if (IPS_OK != pMandatoryNumbers->s) { IDLog("SendEntryData - Bad mandatory numbers state %s\n", pstateStr(pMandatoryNumbers->s)); return false; } if ((0 != CurrentValues.PrivateDataSize) && (nullptr != CurrentValues.PrivateData.get())) { // I have a BLOB to send SetDriverBusy(); BaseClient->startBlob(Device->getDeviceName(), pBLOB->name, timestamp()); BaseClient->sendOneBlob(pBLOB->bp->name, CurrentValues.PrivateDataSize, pBLOB->bp->format, CurrentValues.PrivateData.get()); BaseClient->finishBlob(); WaitForDriverCompletion(); if (IPS_OK != pBLOB->s) { IDLog("SendEntryData - Bad BLOB state %s\n", pstateStr(pBLOB->s)); return false; } } return true; } bool ClientAPIForAlignmentDatabase::SetDriverBusy() { int ReturnCode = pthread_mutex_lock(&DriverActionCompleteMutex); if (ReturnCode != 0) return false; DriverActionComplete = false; IDLog("SetDriverBusy\n"); ReturnCode = pthread_mutex_unlock(&DriverActionCompleteMutex); return ReturnCode == 0; } bool ClientAPIForAlignmentDatabase::SignalDriverCompletion() { int ReturnCode = pthread_mutex_lock(&DriverActionCompleteMutex); if (ReturnCode != 0) return false; DriverActionComplete = true; ReturnCode = pthread_cond_signal(&DriverActionCompleteCondition); if (ReturnCode != 0) { ReturnCode = pthread_mutex_unlock(&DriverActionCompleteMutex); return false; } IDLog("SignalDriverCompletion\n"); ReturnCode = pthread_mutex_unlock(&DriverActionCompleteMutex); return ReturnCode == 0; } bool ClientAPIForAlignmentDatabase::WaitForDriverCompletion() { int ReturnCode = pthread_mutex_lock(&DriverActionCompleteMutex); while (!DriverActionComplete) { IDLog("WaitForDriverCompletion - Waiting\n"); ReturnCode = pthread_cond_wait(&DriverActionCompleteCondition, &DriverActionCompleteMutex); IDLog("WaitForDriverCompletion - Back from wait ReturnCode = %d\n", ReturnCode); if (ReturnCode != 0) { ReturnCode = pthread_mutex_unlock(&DriverActionCompleteMutex); return false; } } IDLog("WaitForDriverCompletion - Finished waiting\n"); ReturnCode = pthread_mutex_unlock(&DriverActionCompleteMutex); return ReturnCode == 0; } } // namespace AlignmentSubsystem } // namespace INDI libindi/libs/indibase/alignment/MapPropertiesToInMemoryDatabase.h0000664000175000017500000001276713263645557024547 0ustar jasemjasem/*! * \file MapPropertiesToInMemoryDatabase.h * * \author Roger James * \date 13th November 2013 * */ #pragma once #include "InMemoryDatabase.h" #include "inditelescope.h" namespace INDI { namespace AlignmentSubsystem { /*! * \class MapPropertiesToInMemoryDatabase * \brief An entry in the sync point database is defined by the following INDI properties * - ALIGNMENT_POINT_ENTRY_OBSERVATION_JULIAN_DATE\n * The Julian date of the sync point observation (number) * - ALIGNMENT_POINT_ENTRY_OBSERVATION_LOCAL_SIDEREAL_TIME\n * The local sidereal time of the sync point observation (number) * - ALIGNMENT_POINT_ENTRY_RA\n * The right ascension of the sync point (number) * - ALIGNMENT_POINT_ENTRY_DEC\n * The declination of the sync point (number) * - ALIGNMENT_POINT_ENTRY_VECTOR_X\n * The x component of the telescope direction vector of the sync point (number) * - ALIGNMENT_POINT_ENTRY_VECTOR_Y\n * The y component of the telescope direction vector of the sync point (number) * - ALIGNMENT_POINT_ENTRY_VECTOR_Z\n * The z component of the telescope direction vector of the sync point (number) * - ALIGNMENT_POINT_ENTRY_PRIVATE\n * An optional binary blob for communication between the client and the math plugin * . * The database is accessed using the following properties * - ALIGNMENT_POINTSET_SIZE\n * The count of the number of sync points in the set (number) * - ALIGNMENT_POINTSET_CURRENT_ENTRY\n * A zero based number that sets/shows the current entry (number) * Only valid if ALIGNMENT_POINTSET_SIZE is greater than zero * - ALIGNMENT_POINTSET_ACTION\n * Determines the action to take when the COMMIT property is written * - APPEND\n * Append a new entry to the set. * - INSERT\n * Insert a new entry at the pointer. * - EDIT\n * Overwrites the entry at the pointer. * - DELETE\n * Delete the entry at the pointer. * - CLEAR\n * Delete all entries. * - READ\n * Read the entry at the pointer. * - READ INCREMENT\n * Increment the pointer before reading the entry. * - LOAD DATABASE\n * Load the databse from local storage. * - SAVE DATABASE\n * Save the database to local storage. * - ALIGNMENT_POINTSET_COMMIT\n * When written take the action defined above. * - COMMIT * */ class MapPropertiesToInMemoryDatabase : public InMemoryDatabase { public: /// \brief Virtual destructor virtual ~MapPropertiesToInMemoryDatabase() {} // Public methods /// \brief Initialize alignment database properties. It is recommended to call this function within initProperties() /// of your primary device /// \param[in] pTelescope Pointer to the child INDI::Telecope class void InitProperties(Telescope *pTelescope); /// \brief Call this function from within the ISNewBLOB processing path. The function will /// handle any alignment database related properties. /// \param[in] pTelescope Pointer to the child INDI::Telecope class /// \param[in] name vector property name /// \param[in] sizes /// \param[in] blobsizes /// \param[in] blobs /// \param[in] formats /// \param[in] names /// \param[in] n void ProcessBlobProperties(Telescope *pTelescope, const char *name, int sizes[], int blobsizes[], char *blobs[], char *formats[], char *names[], int n); /// \brief Call this function from within the ISNewNumber processing path. The function will /// handle any alignment database related properties. /// \param[in] pTelescope Pointer to the child INDI::Telecope class /// \param[in] name vector property name /// \param[in] values value as passed by the client /// \param[in] names names as passed by the client /// \param[in] n number of values and names pair to process. void ProcessNumberProperties(Telescope *pTelescope, const char *name, double values[], char *names[], int n); /// \brief Call this function from within the ISNewSwitch processing path. The function will /// handle any alignment database related properties. /// \param[in] pTelescope Pointer to the child INDI::Telecope class /// \param[in] name vector property name /// \param[in] states states as passed by the client /// \param[in] names names as passed by the client /// \param[in] n number of values and names pair to process. void ProcessSwitchProperties(Telescope *pTelescope, const char *name, ISState *states, char *names[], int n); /// \brief Call this function from within the updateLocation processing path /// \param[in] latitude Site latitude in degrees. /// \param[in] longitude Site latitude in degrees increasing eastward from Greenwich (0 to 360). /// \param[in] elevation Site elevation in meters. void UpdateLocation(double latitude, double longitude, double elevation); /// \brief Call this function when the number of entries in the database changes void UpdateSize(); private: INumber AlignmentPointSetEntry[6]; INumberVectorProperty AlignmentPointSetEntryV; IBLOB AlignmentPointSetPrivateBinaryData; IBLOBVectorProperty AlignmentPointSetPrivateBinaryDataV; INumber AlignmentPointSetSize; INumberVectorProperty AlignmentPointSetSizeV; INumber AlignmentPointSetPointer; INumberVectorProperty AlignmentPointSetPointerV; ISwitch AlignmentPointSetAction[9]; ISwitchVectorProperty AlignmentPointSetActionV; ISwitch AlignmentPointSetCommit; ISwitchVectorProperty AlignmentPointSetCommitV; }; } // namespace AlignmentSubsystem } // namespace INDI libindi/libs/indibase/alignment/AlignmentSubsystemForClients.cpp0000664000175000017500000000454413263645557024520 0ustar jasemjasem/*! * \file AlignmentSubsystemForClients.cpp * * \author Roger James * \date 13th November 2013 * */ #include "AlignmentSubsystemForClients.h" namespace INDI { namespace AlignmentSubsystem { void AlignmentSubsystemForClients::Initialise(const char *DeviceName, INDI::BaseClient *BaseClient) { AlignmentSubsystemForClients::DeviceName = DeviceName; ClientAPIForAlignmentDatabase::Initialise(BaseClient); ClientAPIForMathPluginManagement::Initialise(BaseClient); } void AlignmentSubsystemForClients::ProcessNewBLOB(IBLOB *BLOBPointer) { if (strcmp(BLOBPointer->bvp->device, DeviceName.c_str()) == 0) { IDLog("newBLOB %s\n", BLOBPointer->bvp->name); ClientAPIForAlignmentDatabase::ProcessNewBLOB(BLOBPointer); } } void AlignmentSubsystemForClients::ProcessNewDevice(INDI::BaseDevice *DevicePointer) { if (strcmp(DevicePointer->getDeviceName(), DeviceName.c_str()) == 0) { IDLog("Receiving %s Device...\n", DevicePointer->getDeviceName()); ClientAPIForAlignmentDatabase::ProcessNewDevice(DevicePointer); ClientAPIForMathPluginManagement::ProcessNewDevice(DevicePointer); } } void AlignmentSubsystemForClients::ProcessNewNumber(INumberVectorProperty *NumberVectorPropertyPointer) { if (strcmp(NumberVectorPropertyPointer->device, DeviceName.c_str()) == 0) { IDLog("newNumber %s\n", NumberVectorPropertyPointer->name); ClientAPIForAlignmentDatabase::ProcessNewNumber(NumberVectorPropertyPointer); } } void AlignmentSubsystemForClients::ProcessNewProperty(INDI::Property *PropertyPointer) { if (strcmp(PropertyPointer->getDeviceName(), DeviceName.c_str()) == 0) { IDLog("newProperty %s\n", PropertyPointer->getName()); ClientAPIForAlignmentDatabase::ProcessNewProperty(PropertyPointer); ClientAPIForMathPluginManagement::ProcessNewProperty(PropertyPointer); } } void AlignmentSubsystemForClients::ProcessNewSwitch(ISwitchVectorProperty *SwitchVectorPropertyPointer) { if (strcmp(SwitchVectorPropertyPointer->device, DeviceName.c_str()) == 0) { IDLog("newSwitch %s\n", SwitchVectorPropertyPointer->name); ClientAPIForAlignmentDatabase::ProcessNewSwitch(SwitchVectorPropertyPointer); ClientAPIForMathPluginManagement::ProcessNewSwitch(SwitchVectorPropertyPointer); } } } // namespace AlignmentSubsystem } // namespace INDI libindi/libs/indibase/alignment/BuiltInMathPlugin.h0000664000175000017500000000323313263645557021670 0ustar jasemjasem/// \file BuiltInMathPlugin.h /// /// \author Roger James /// \date 13th November 2013 /// /// This file provides the built in math plugin functionality #pragma once #include "BasicMathPlugin.h" namespace INDI { namespace AlignmentSubsystem { /*! * \class BuiltInMathPlugin * \brief This class implements the default math plugin. */ class BuiltInMathPlugin : public BasicMathPlugin { private: /// \brief Calculate tranformation matrices from the supplied vectors /// \param[in] Alpha1 Pointer to the first coordinate in the alpha reference frame /// \param[in] Alpha2 Pointer to the second coordinate in the alpha reference frame /// \param[in] Alpha3 Pointer to the third coordinate in the alpha reference frame /// \param[in] Beta1 Pointer to the first coordinate in the beta reference frame /// \param[in] Beta2 Pointer to the second coordinate in the beta reference frame /// \param[in] Beta3 Pointer to the third coordinate in the beta reference frame /// \param[in] pAlphaToBeta Pointer to a matrix to receive the Alpha to Beta transformation matrix /// \param[in] pBetaToAlpha Pointer to a matrix to receive the Beta to Alpha transformation matrix void CalculateTransformMatrices(const TelescopeDirectionVector &Alpha1, const TelescopeDirectionVector &Alpha2, const TelescopeDirectionVector &Alpha3, const TelescopeDirectionVector &Beta1, const TelescopeDirectionVector &Beta2, const TelescopeDirectionVector &Beta3, gsl_matrix *pAlphaToBeta, gsl_matrix *pBetaToAlpha); }; } // namespace AlignmentSubsystem } // namespace INDI libindi/libs/indibase/alignment/MathPluginManagerMain.cpp0000664000175000017500000000052113263645557023031 0ustar jasemjasem#include #include "MathPluginManagerClient.h" using namespace std; static MathPluginManagerClient Client; int main(int argc, char *argv[]) { Client.Initialise(argc, argv); Client.Test(); cout << "Press any key followed by return to terminate the client.\n"; string term; cin >> term; return 0; } libindi/libs/indibase/alignment/objectpopup.png0000664000175000017500000027542513263645557021236 0ustar jasemjasem‰PNG  IHDR5okµêJsBIT|dˆtEXtSoftwaregnome-screenshotï¿> IDATxœìwœÕùÿßÓnß»l¡wPŠX° XP{o‰XÓLŒ[bMÑËWc’Ÿ‰5š{Á‚  * `‘²ÀÂöv÷ö)ç÷Çܽ» »°Ë.“ó~½†ËΜyÎgÎÌÞyæyž£|óÍ7B×uêëëÑ4úúzn½åWH$‰D"‘H$‰D"‘ì)ÜxÓmx½^¼^/~¿ŸÏ‡²jÕ*‰DH¥R\wíU”hÛKéyG=ÛEéq=–´Ý;¶° ‡Ü”]·Ýí>¶mÒ÷»ó&;iÑ»ÍÝÛ« #½ûí mo­ÏìwaLéË:µß͆»ÐG¿zØ´w#³ƒ½ûÄþNúìùW`Ï{êâší[û]ní½ý™ê~Ÿ=üÓÐã>ûÎ~çÖ;5·[¿&vóź-"óOÇU}j¿³U}Ø‹ØáÝÚÒ“>zf¥›­Å®ØîaŸ¢Ïm'}ŠÝ×A'öû¶3±ÝvØj7Ùßõ>ûnh:ßy»µ}ú+ÜåEÙïbmõÑýKf×:ìÝ%¹ãÝRÔ‹kv÷Øß…žv±^ûصaíþ^»<¬é·VV®s#\§Fss3¿¸ú òòrwµ ‰D"‘H$‰D"‘H$’ÝNcc×ßp¡På£>×üâçäææ|Ûº$‰D"‘H$‰D"‘HvJSS3×ßpj<ÿ¶µH$‰D"‘H$‰D"‘ôˆh4ŠrÈÁˆœœì]2 „ÀÂqsÎD…(Š’ÙÏý¸I»î§¢¤kQ¤Ûµþ¼S;B€¢ (  ¢i*Š¢ ªj—v$‰D"‘H$‰D"‘|7hn£»äŒp„ƒ°LÓIJml[dœš¦ k†®CÎ!„ëº!LÓÆ±mÇÁ5í€P5 ÃÐ@QÝúdÛ8$„H×-N7ì訚†¦ºéØH$‰D"‘H$‰ä»‹ÞÓ„8¶À´L’‰$'(aÿÒ|†äfQòS‰SÞÔÂÒ- ¼´v+>Ÿ]×·³A:²Â2-’©C† ¡´´„‚‚~äååÒØØD}}[¶l¥¼¼¯ÇƒnènDm‘(®=˲H&S”””0bÄpróòÈ …ˆD"455³nÝ:***ðz<à1Ð4 Òv¾U„ƒm Ð44éc‘H$‰D"‘H$‰¤Û(‡|€ÈÊ u«±ÇqH¥Lú{Tñ4Míþˆ ›h$“ïú5§ÂÖg~Å5o4㱞y¿>¾¨æõßÝÎS’ÿXN8ÿ$Žš<„|`†ÙôÕ'Ì{ú%–ÔÛ$˜ÈͽŒ1Tóô/næõ¨Ÿ€Ö}9‰D"‘H$‰D"‘ü¯‰D{–~"°Ì7Om ™ÜÉÌ)Ñ0†/À­“K¹ôƒõèé”3•â°CA‹xÌJ×Öh[Zk^(Š‚e™†‡ƒÚŸ·ß~]ukl8ŽC*™äð#¦£ªÅwbÇÂ0 9djÆNœ‰i“Ì•Üt| ÐÂGº‹¬µÐÔ!|ï¶+8:¿]c#›A'Rô¯g°PŠJ›Ã!ÚØLTó‘ŸçEC`F£„é(4à ôáÓ‰XœxÊÆ²ÓÎÍCN®ÕL‰¦HÙn±UU7eûñöîP%‰D"‘H$‰D"Ù£èv¡P!À²LfR(b˜MMÔÄRÔÅMÆ´E{¬iŒ24JB^ìx B¹Ì,ñV} MÓ1m‹ÒÒ|>±XÒ3§8Ž“vF¸õE[g+QÓ´ðû””–P[]ƒGó`›6  ¦íÀÂ…ïbYV§Ç0cÆ‘˜¦™±SSSƒ.zà×Ùnœt ¾ŒÛÎ…AŠ/Ÿüù$NÈc+9„óxãöÛxì‹0‹=:úŸæ`fÌqÖÝqÀ¦ñ“ß%¦yzÖ/ùñ¡ƒéЀ5«óü£Ï²¸YAµ³Ù÷¬‹9fŸ - ¢‰Š·ùÝÍÏP?þd®;ïp&ðHÒ´q ýa«l/éØH$‰D"‘H$É i§F÷›–ÍaýýXÑ0ŽeP°¹wÙf®žT̸ükšâÜõéV.ßg|n+fz?óªã¨ªŽm9—DZmWƒ“‰®h¥-ÊÂ}w‡’’b*·Vb°l›’’$®Ö]gÌ8b;; ¾‡iš(ŠÛWYi •[+»}ì®Hw¢ÙVröù·A‹ož¹“;߬Ç2À¶‰0q ‹FMMéºåT¤X¿º¿OGuœŽcÛÒD‹©ÚŽª +Ð#EK³ƒ/'Há˜éü躯~–r¥5‘:€IÒ2ð©ÍD²¦ò³«f2F»¹†š”Ÿ~ùbq|Ý?׉D"‘H$‰D"‘ìé¤Ãvþ¤« àØ6} N4†H?”ïU˜Ë5'Æ]¯¼ÃiCry±¼™+Ž›ÆÞJ§¥DZ) úql۵ı ƒ¤RI·÷mž´[ÓFÚÖ»ûÙ¶E0p ’± ƒ¤R©6âñx;­Ø¶•þ´ ¤ítçØ»"8b±Å᎗+ðäxQ xªðèÛûrý‘… ›õcî• âÓ÷xí¥×yw£I–¯}¿Õ¼pÛ<·ÕFÕ¼dgxqØøÐ•œùWƒPNÁü}¹ò·g1¼`<ãrŸcCsë¾aÞ¼ùþºÆDóxJO§PXÏ£7ßÁ‚ 4?ÙY> Ez4$‰D"‘H$‰DòßC·ÓOHæ 3ãŒpšjerúþ{óø¢Ï8ÿ  Œw°Â mû* >ÇÌDb8é©X[ÓDEá×tÙõAÙ×ëÓp„@NÚiašfÆÎ¶ÇÓþÿn4‡û³×›¶Ó“Ð….Ú¦^À¥ŸÝΟ>j!ÐPÃo±êï7ñæřØ>ue“æ“eꣷr×{MàkoEÅòá×áŒûØëÜŸóÙ#ÈêЛ‡€g[WŒ‚7+HPµ‰U-ããúS0ŒK￟Kòò‹óXRk’¹'‰D"‘H$‰D"ù/¢Ûé'ªpSAZâI¼¶ƒmé«7må…ÕÍœ{à^úä+†ŒÎalŽ'³]QU"ñ„›J¢ºvâ±B¸©'Š¢2}ú¡™:­´Ö׈Çã8ÂΈÇ\;¤Û&I%]Smsjt6U«ã88ŽÉd2 Ò³kß¼r9èû|Rþðz’Ñ_ó·•&A¯Û·¢«$6.áù¿,â_+㘟ßÀÅ}L<å(ÊÞ}ŠòŒ=Uqσ Ì$ÊÞ—rÅÌxìͼýÔ[¬J–qâ…3äz§Â„¢á×7ðÄ/ogã)'râQ{1ä€Y\qÀ^úÕm<[£sÆJ$‰D"‘H$‰ä¿„ô«{±óE¨ªÆ7- ŽmâØ&kšâÜ»ª‰Ÿ¹' 0¸æØ¹ou_5Æ2m„pXI¢¦g?QU•ÆÆ&lÛÁ²,lÛ"‹ljÇã$‰Ìÿc±(¶ma[¶íÐÔÔœ¶ãmngìX– ´/8ÚV€–e§ûshn£ªj÷޽ÃÒ†Hlæÿû=O¬2<¦_ùSfõ·ˆY)+Èè}ÆPäW1-aEh»%x‚îL$©Q —a%>’‘MQ ËVÉ.-İi!Ͼö>ï¾ÿ[’ì8ØŠŸ³7ÿ~?½ä6ž©ÀSúé¸ÓÝÊE.r‘‹\ä"¹ÈE.r‘‹\äò_z2û‰ãà1 Þ¯2¾?ˆtêHÀàgGalª³¶žáY¹ü☩xk6 ¬˜»³æaa…Ç ¢¢ë:uuµƒ~lÛFQTÕAÓÔLqÐV‡„m;é ]ש©©ÁÐôNí¨ªy±xñǃe™8ŽÈØÑu£gé'tÖ¶Šy÷þ‘â;¯fFþ0ÎûÅi¬¾á)ÖÄe—MA'{D>[Âf[à‚¥ë&ò0åÊûx2fbÄßçÆ«ç°em9Š ý>wß=-1?C|Ë(8VŠTñéÜû›Ã †ë©‹i°©ÙÁQtêŽD"‘H$‰D"‘H$ßuº~bÙÃc°0¬q\¶I™¢âX)Jü6ĶfjhØMu ·mð881 Õð°,"x7¢ n§º‡††F úõÃë1pEUÐl-íÔ Š!°áTU¥¡±‰¦ÆF(ÛÛ±lÁ¤IÑ45Š¢Çq£5bñ8ª¢Ũ¯«Ç àØN×Ý Û•†ß^Í÷¾ÎøßGIá ®8ëS~¹ œ÷—®ç ñC( º1©ÆM|ñÁë<ùü*¯ŽG´°èÏ2ð‡g3cl>¾€A¤ÚÁÐUœ5søý?4.>a CK†2 fë†/Ù”è$E€@ÅPcTÔÄ_X@q6Øá >~óYþ8†Ï«ËÙO$‰D"‘H$‰Dò_ƒrÈÁÇèVcMÓql‡\³…ÛŠ¢d 33µkg¨ºAX1¸®*‹„7ˆa¸ý8ŽÀL¥°‡áÇd"3Ú¦pU:¤(ªŠc Ö­[‹¦ëx /ªªdì8B0lØPÕ(ioÇíÏAPT„ÂúõëUSºçÑé€C"’$é€êõ‘•®Ÿ!l“HÔÂT— GJX˜¶ƒÓÚ…¢¢:~¯†Úê•plâq“”í6R4@À@WvÊ$ž´±3û+¨ª†Ïo`(6Ñ– ž`ºÀhZG,na9m}ŸWC– •H$‰D"‘H$É ©”é:5Z ÝÁðx0Í$ÞT‚K³ÃLñ&ÁIÏ*"´:TeI/…³Izüx½^ -ÕÅqÜb¶eSR2Àbu›ÙHZ±XŒÊÊ*4MÅëóu(ê8‚T2‰m;”” Àï÷§m´wV(é)am**¶à8†a¤S_dáL‰D"‘H$‰D"‘H¾k˜fÚ©¡ëzvôz½X–E<ž`#Âaþ8Ãu“,Õ¡ÅQYg¼÷³Ô ðûзqšdB`š)’‰$¡PˆPVŸÏ‡¢¨áH$ˆ´D‰F#x=> ‘qB´Fslk'''›`(è:Q„Š •JDinãñzÐU Ó¶¡‹YR$‰D"‘H$‰D"‘ìÙX–ÕêÔÐz¼³n訪Žeš˜¦‰ãØ8Ž@UÝ Ã00 £K§A«CB7e$e™Ø¦åÖÐH;4UC3t<º¢ª´šÚvÚ׌a“2-,ÓJ§œ8(ŠŠªªèº†á1plÛ‘ ‰D"‘H$‰D"‘H¾ËX–ÝýB¡Ûb¦,ÅFÓtü~»ÂœÝ£5ÒBU„¢âU½ÓÞ*2¶Z ‡¶¯»Ñ•ÇÑðz4¼†‘I>q3QlÌTûúŠ,š)‘H$‰D"‘H$Éw˜nOéÚB'…Ùu­P‰D"‘H$‰D"‘H$’ÝBº˜† YH$‰D"‘H$‰DòÝb—ÓO$‰D"‘H$‰D"‘H¾Mz•~"‘H$‰D"‘H$‰Dòm¡¼þúüo[‡D"‘H$‰D"‘H$I·9î¸cP¿m‰D"‘H$‰D"‘H$»‚tjH$‰D"‘H$‰D"ùN¢Û$‰ä¿!ÕU[©¯«Å4ÍÝZ«Èã1È/(¤h@ ªªH;б§°§Œ‡Ô±gêtDž‰D"‘ô僲¦†D"‘ì:B¾^õ%º¦RÐ^¯EUÚfËVZo”Ûߘo»Né|{ëÍ|z³@J¦¨«©Æ¶-F’¶/utÔ±§°§Œ‡Ô±gêtDž‰D"‘ô„ãŽ;F:5$’ÿB,ËĶmBÎ:Ô EEÕTtÝ@U;fÑ !hl¨'Üƶ­^³®ëdgç›_°Ã7x[·VÐ3pà 4MGÓµ¶[ìvÝ+ŠûóŽnš…ínàÅöSn TUaݺudegS\\"ut¢cOaO©cÏÔ!éˆ*‘Q[[¢ªŒ=6Ó¶}¤F{§ÄC=À¥—^šiÛ~»‚@0ÈÈÑcYýÕJêêjèß¿¨SM©D¯ß¢ª¨ªŠ¦éî=³¢ ¢¢t,"Zõd¡ۅG»wêîMx;MŠpª{c @ˆT2%ut¡cOaO©cÏÔ!鈔6c™ŸEë›ÅôzÑ.|ºu¿ö?R;€ÊÎÉÝáöVFŽÉúõëÝt±nnnêrÛž2RÇž©£3²ö ¬}>¸‡LøAéþ°÷ÙP¶¿ÛS¤ðÚÏ úóN¬(pú?A÷ÁS§b‡×i§:¼Ö¼V»{ÃoáGÃË—BíWîºóæB¼^¸ÀýÙ›çÎ…¦ðâìŽFUpc]—}îÉçå{çž‹êóÅÑvsÑæ Q € – Ým§ë®mÕtH$“D"MDã 1>übýû•H$ÉŽÙe§†i¦p$1jëšø ]'''o·?x¶Ç¶-tC'™0©ohÀë1PU³0 ã?¦ã»ÈÚµëx󭷇ئ‰eYX–ûÇxÓ¦Í躎adgg3㨣1b¸Ô±‹:„D#ró v‹öÿfN<ñDæÎÛé¶’’2Ö­]MÿÂ"lâ±(ÁP¨ÝÍmû›V÷ó¡‡båÊ•üæ7¿& òç?ÿ…ÇŒK.¹dg†’q„C!âñx—á  á *ŽpÈÊÊéÑh£¥%ŒÒo¥ŽÎtô†µkײ×^{±bÅŠ^ÛÚSÆcOס(* à>+ 7½ 5µ õw³õ±T¤diË$™ôƒÖ‡M‘ù·¯Ç#Ìfêa³È+€?Âá „0SIšjغyŸ.y›–憎G§4n€±§¸G÷ÁÝîQlYê.¹C`òl>Ãm{è ðå³ðÍë`¶ûž*Ùr»N‰¶ÇùžqÀOaÊ¥°êEøò9pR0d:4®oshЉùd ”¿ëjì7ê¾oŒ=ƶÃ.÷”ë´3LE@2‰WÕðz\'¹Gpï7CºÀë1ðaã÷ºÑ˜!¿ëÝ…‚x(ÞT"Í-^*ãŠÙ»—S‰D"ÙE§F<cɲ¯hŠ$Ée‘•›ÍÚ­Í46Ö!RaöŸ8š#FöµÖíŽÍû‹?£º±…ܬrós©mˆPW×€!Z8xßñ 2l·ëø®‘H$Xðæ[|ùå—D£Q²³³)..¦¤¤!•••Ô×ׇiiiáÙçžcüøñ=ã(|>ŸÔ±‹:dÊIçl›&ÒÙöÎðX–y-gZŠ¢t©!<üðì\¹’_þò—ä§L?øÁxøá‡yðÁ¿r饗¢ªJ;‡H[¨rÊ4w¬O(ÂÙÅ ¬ÌKee’8N:¯\8Ʀ/ut‡=]GoBðõ×_3qâD>ÿ¼³·à=³µ»Æ£  €úúúëöôóÒ•!¨ 8NºfB[û¶ßå´¥]G¢ÍÙ!Dë¾îï°‚ÒV‡¡Æcø˜Isò…x½þí¶éº?¢¸lûL=’Úª êk· 7õì:÷s˜ù®@¥Måm΋u  d tì{™ëÀøòYh©„ÑÇ»m6}Эãê”ÍÂCaÒ÷aïs ~ h¨\¾\Hì òcíש1åRˆVÈ™ {wÚåžrv…!4LT ×gà5ªbáõxÈ2TüŠM‘GÔ’$½Ùh>ƒlJ––"϶ð%£„u‡:R¦M0°ýu$‘H$’žÑc§FKK3/¿µŒiDQ¿ö«V­æ¦›n$//Çq½>Ÿ—‹/¾ˆGy”Gy˜ïï{hšŽ[‹Î}°R%µÓ©vÇ}€rïÀBt/e¡3þú׋X¸ð#î¾û³.Û(龢à ·uœv*”•v-bο ¶ëðð>ÓÑK  ¾¾k½%•JQ^^ÎÞ{ïÝ«ˆÝ5ûî»/'žx"7ß|s‡õßöyÙU­N‹¬ì®S„ŽÇà18䄳‰EÂ,_øz‡6ÑpŸ¾ûáæF„H—Wè£ñ2b<³N¿ÌýŽØÙñ+*…Ń(,Dc}UÏ®S3 ó®tcNXt—«wÀ$5 ÖÁÂ_ƒªÃð£ÜÔ”½Î‚ñg@Å(ÝÏM©X²S­]òÖ/!T#ƒQÇAáxwý¸3Ü%¦Mà ‚ªÁá·BV‰»øóܶÓ÷‚‘j× ³vܳ¼Ë.÷”ë´3Zº®“ç‡Â†_±† Y†BPÓæOáq4‚^‚äym 4Å£‘j4ð¦¿Maȇ×+_vH$Ioé‘S£±©‰w—®æˆ#ŽÀç5HÚ€—SÏûÏüãÏ4G,²³²˜~ôñ,ýà-† ­¦°°óôÞ‰FxsÑJŽy4^‡X*…ªjÅ}ˆI ÈÎÊ⨙§°øýù ZOÁnû·m›ßÿþN¼ù&ÅÅÅÜrËÍŒ3€U«WsÛm¿¦²²’£gÌàúë¯CӴݪ§+æ½1Ÿêêj’É$Çs {ï½7¹¹¹†®ë(Š’yÓmÛ6ÙÙÙœuÖY¬X±‚ùóÝ}ç½1ŸSN>IêèCÿó¤o^ÛóÒ‹/pò)§dþ¿m›mVU…ææ&‚ÁPÛ ªMÍÍ|õÕ*®½ær²sŽh c¼/³/¸€‡y„æp˜œœ·Ò>î¼pcºÖõgk®¶@àâ@gÕìBŠÂYC‡ò\y9Í]än_vÙ#TUí¸Xí8¨´F t|óÜ”•ÂK/Cc'oYÏ:¼;›Ú':v‚¸æ8å`àÀ®uôÍÍÍÔÕÕ1a¾øâ‹]²±;ÆÃ0 fÏžÏçc¿ýöãã?Îlû6ÎKgôT‡"p§ßܧÿô&ú—ij‚Ù¹L;éÜÌvMÓ5y*LîçÚn¤F_Œ‡¢¨yüùÝrhôd<:Åró®„™÷˜ôߘÏÿGýÞ¹ 7g„oæ¹KÉ×±1è`·}* ÃŽr ö.ÁŒTçÁæàäG nµ[+£p< ¡pœÛN÷µ¥Ä Üh3ùÃaÙÃðéãt' fO¹N;Cõy aâó(ø½ C²†úLj¯°È÷; ð¸Nð|_‚,=…/ËÞ?d$Žb:8-I AÂðà ~;÷ƒ‰DòßDþ2WW×àÏ.ÁQ4TtÀaÂÄÐ4 :š¦¢e ¥bKÅnqjÔÔ×áÉ)Æq4l4CEѺêæâTò5‡P]Y¹Û/¾ø"Ë–/礓NbÕªU\yåUÌ{ý5®¼ò*ÈþûïO<çÅ_äôÓOß­z:cõê¯Y¾|9Ìš5‹}÷Ý—ììl<º®£ëz‡ôÛ¶ñz½hšÆ Aƒ˜5kÏ>û,Ë—/gì˜1Œ3ú?ªÃãñ°Ï>û ëúnÑ1vìXúõë‡×ëýê¤sÞw²½;닊ŠY½j%Sö›ŠmÙî›_GÍõ×_G¿‚„p¶ó(ŠB àç’K.Æçó!„À!]ÔMUYõå (-ëRŸ“ÎÇN—âpoÄ;QýòUWqÄ]w1ûW¿bÚwtjkË–î=x´¾ÌܶF@wtÐpxûõ;ˆHÙ-:ºIVüëÉ3. IDATèGpñÅ0|8hüå/;ÖÑcÆŒaõêÕ=껲²’™3g2zôhž}öÙkßãqòÉ'³iÓ&.\È9çœÃ²eË:9üO—Ñ#Š‚ØA¡FÃëcìþӸ뇧ñÍgG³rùï¯ÚÒVÜ>z £ FŸBvNïî%º{2úXñ¯¶ˆcÓŽ¡‡ƒ7Þ¾ÑÔØ–­ËÜåŒ93 Ÿ[scÿêz¥=“Îòñƒ°¥Í‰†îƒ³žT æ]á:RRQ÷@ƒ…pöón}OëV7{ÊuÚJ"Š®ë5A–á:#†æÀÀ˜›ºÒø o(„’Š¢k1̬,”âZãM4+ŽBÅÂLxØn<2‰D"ùß Û1o |¾¶ŠAƒJq‡@H#T ©`á³r|—­â1T”•0ÿÝÏhìa¥íÑØÔÄ'+73|è`,B¹yA•|¯BŽyè烢lO§tp)sß\BSëhÅqÞy÷]îÿã!xüñÇ™7oååå\xáE\0{6åååÌ›7Ç˲¸ÿðλïî´Êv_’L&™ûÊ+D"FŒÁ”)SÈÊÊBQlÛF×u4Më°èºN}}=>ø ëÖ­c„ Œ7ŽH$ÂÜW^!™Lö‰Žœœ¼^¯›:à88Ž“ÑL&‰ÅbضM  ++k·éˆF£¼÷Þ{D£ÑŒ3§ý¢( Éd!~¿¿OttÄ!ºæ®>ö0.y©†ÿÜÕ±‡Ð­±Íòâ /t¹m[†N´%ÊG‹Þ§%Ü‚®éx ^¯’â</ïö‹áñ`x<ädçàõx1 š¦Ñnæ£Þ#Oî°>Ok „@Ø6Û̓k‡ä—–‚¢_VÆŽßGïÇNÛï¬FÀNtô(»_ǘAðàÏá½»aáàÛáG'Â%Ãå—ÃOÀèѰy3TTÀ 7ìXGgX–Åé§ŸÎÙgŸÍȑݯýôØc1hÐ ~þóŸ÷¸u_Ÿ—ÂÂBfΜÉã?ÎÇL4eúôé™íÿÑëcôT‡c[;ÔsüÅWÓTSź/>î²Mñ°Q4ÕÕ¤ë28Çî³ñ˜ràÑ=:þméÉuʸSÝ´p£>ü?°’®C£þØðN×û–9ƒaÝ[0ç$× a§`Âù».^÷ºQ-•°å“ŽÛ¬8¶ÛG¤Ú0iuBDkÜô—âɽƒ”·vì)×iW¸Q*ù~ƒP@ô5Šû¥šÈÖàM&Ñ14GÁòiè9T¯f9x’q”h /qÃ0d= ‰D"é º©Fˆ¦Tª[L&•esÞù?bü¤}ql‹’S¸æöGp•Õ+–1ç_Æãñ³ujª«ÈëæzÝ!Ò’€ª†û 2sæ9LÞ÷"ý(pXñɇ̟ÿ<aK¥¦ºšÜ>Ô‰D8ãÌ3‰ÇœþùÔ××SRR /¼À´iÓ˜0ÁÍûõz}¼úê«L›6rþùçsë­·á÷ûxö™g…B}ª«36nÜHmm-Gy$¹¹¹”——óÉ'ŸÐÔÔÄùçŸOiiÛ ‡mÛ¼úê«<÷Üs$ ÆŒC0äÔSOåÎ;虜¶–72jÔ¨^ëðz½¤R)ž~úijkk™8q"'žx"¯¿þ:o½õÍÍÍÜÿýdggã8ÎnÑÑÔÔDMM sæÌaß}÷åCé°Ï¦M›¸ï¾û¨¨¨àÜsÏåˆ#ŽèµŽVìæ5¼=ç¯<8g)µÀ®YùnÓïáTMcêA³víðîg,ÙŠ¢àóû2tÇÌLØÂq ‘fЦÓ^:´®¼ë.Î^¼˜ç?ü°×Ç›¹ïߦFÀÎtôˆc ¬nÚÕ¥ì+?=®<²ýAð†Ptá8ì3¼… µN¾EPU\ƒìYM:ÓÑk×®eíÚµ¨ªÊþûïÏ‘G‰eY,\¸uë¶ó­( G}4£GÂ6Mpýu×ñÇ ¹¹yÇÕ‹ñØ\póæÍ£¦¦€9sæpÅW°hÑ"R)7Ò§·çEQ<•d²{õ >¸!àÃ+;¬ï™ÇÞþáUQUfœ÷cNºìjn=慨5y*¾ü´­P{?J‡RT2¸Ë~[šÐ þÀŽÿžw÷:%І kçÃài0ý&Ð ˆTAÁH˜v¼'Û}sûÃô›]Ȳ‡ÜYH>Òúr(°‹ÑCwSbV<µ}Ÿ;ãëWܺ£fÁ'Ûió>ÿëÝ=/L”F¶a1̉âÚhyÙø‡ „ª-²)„ÐòûGCmlÄiIàÔÖAÒÂÒ lÍMó³]žˆP"‘H$iºýMj™ŽíÐw¨mŒ3gÎ1ø„Ãïÿñ1w^?›”¢ pá”MÐïN͆èÛ¿DfÊB8‚æ„EU}’ù¯=!ÜBOJúïžP@á4[¡€†æníS·ßqeeÙgŸ}ðù|LŸ>>ú¿ßÏÇÌÆ¨©©Áï÷3lØ0&MšÄŠ+8묳X¾|9·ßq·ÿîw}®m[6nÚD2™¤ÿþaªª‹Åhiiép“¸uëVþö·¿±bÅ "‘–eaš&^¯—`0Hqq1lÜ´©ç΄Nt躎ã8¤R)Âáp‡QEQhnn¦¾¾UU1 c·ép‡X,F8æÕW_eéÒ¥œ{î¹”””d´„Ãaêëëûl<\L¶¼öø`güú:¾øí4î µU³ˆ¿ÝñG^ü¤’*Ù#OãŽ?ÿŒ ~À¬â½Gîæÿ½¸„Š˜JÎÈÃùþÕWsúø¬tXVŠ­ï<Ì=}™¥1ðög¿‹îáÎs‡ò­N€ÜEäE·öÛEU9j #GéaÝ•áéqÇMO¯ëÈ;••¼óÌ3½ïÏ ŠtŸmýtWG·˜œ\üxÜ›^çôŽŸœלœBÔœ|”`ŠáÇA$⌠6ò­\ÿ´ÍŸþ?ü!Ì›·óñØŽã°xñb/^ŒßïgÆŒœwÞy444ðꫯR^^NVV÷Þ}7ûí¿?Ñh”úº:ª««Ù´q#3=–§ž~º[}õåy™4iƒ â¾ûîˬ ‡Ãø|> ¨¬¬ìÕy).rÑEãÈËó²re=ãÇç3þ&Þ~{s—¿¢Š¢ðãOÀq}T™i×SBTUÁÊ"á „Øë #8þ¢+)PÊo/œEù—]˜T…;…ÏýpÓPÒ¹'½¼N”í²ß¯¿ü˜yÏ?Œ‚þ²ßÁÇ2jü¾Û9B{tÚI˜|!Œ8ƼH4Ã[WAÍJ8æ^7=Þÿ='ƒªÃ¿qg%Yògoi³çX°þß;î³3öÿ |õ¼›z"œôô°]ЕãwÓot‹.{x§Q}ú=Ö zúýá÷ªø¡8['h rõ(Ã0¼ÆBhÍ=%g IœêµÐЀÒG©­3…0l4GG1"%ÓO$‰¤·tÛ©áñzÑD„”e“HÚÄàhîìk-U_cÛû`¡¸ÑBA Mµ).>¥o{ |j ÓrˆÆM"I êˆ (B Ò%þTškË).>µOu|öÙg,Xð&#GŽäµ×^# ’H$¨««ãðÃçí·ßfË÷†" rä‘Gò /°xñbTU%’••ŧŸ~Ê™gœÁ¤I“úTß¶lÞ¼™T*EÿþýI&“ضišÄãñŒSÃq-ZÄ /¼@ee%MMM„ÃáÌ̆aàñxdg{xá…Nj8Bn¹e1Nú {f}u(8ŽÍCU¸EšUUÑø÷sçwG2ÙáøLš~£'Oåþ«¾ŸÖå´½èèåuZп¸Ë~¿^±4“¶P[µ™×Ÿˆ7_y‚‚þ%è†ËLq×-—vÿ:µ’3rÊ\§Àw»©ó¯‚cîq ð^Ú±±ÿO ho·çʧvl¿»Œ?ÃUEQ¡zE›†mÙÑñ8¬{Ÿ ¿çjÜ{Ê÷ig„tA¾ø ßn&ÏlD³hVz?o±ÿ…¨ÁáÅ‹’ªBõ͇M/cÕ”£FM”ì\¼9ˆ„ŸTC‚˜½›ÃP$‰ä€n;5Ñ?`QÝØ„S¢5P’)!šUQ¨k “e$ÉÎÎîSÁ ‹èpØÐÐÈȲŽªê:4T!P7dQ  )*u Md{’dgõŽ––~rùåŒ9’©S§f Knܸ‘¹sç²dɦOŸÎ¢E‹8äCX²d š¦1fÌŒeY™ ?¹ürÞ˜7¬¬¬>Ó¸-›6mÂ4MJJJð¦g6¨¬¬dÑ¢E477S]]ÍüùóÙ¼y3‘H„ææfjjjhllDÓ4LÓÌÔ–9r$ ,`Ó¦M}¢£uv‘uëÖ±víZ&Nœ˜ioÛ6ÔÔÔ¤ßâ©»Mdž x7]ëÄq‚Á 999<ÿüó|ñÅ~øá´´´PSSƒeY}¢#ƒ¢v/–ÈIPßB+Ü‹ýöÇ Æì•ÞÔð!¾RÏäëÿÊ¥3 PÑ×ÖòÞYã•Õ1œæ­‡æÑ´×µüíº(ú¶=èþöŽìSᵆKãÖ·C¡ãì*Ýeܱg ûü|ñÒ;nh·Ám?Ee·uœq:¤:)J:tˆû9h-Ä™ŒLÜÚàݾÑqÒA0$OG 墿 ¡yû£h!ާ=«ÿ€ZÖ} ñÀ¤Ct¡cW¨­­åÁ䥗^âå—^" ѯ_?<^/‰xœps3@€ÜÜ\ÆŒÍâ¥Kwj³¯®O<‘ªª*>ù¤­¦Á…^ÈÖ­[Y°`A[Ã]ɤMEE„qãòñûuxàó^éP€ÙSúãõ0SIνæw¤ñŒCCUÕNÓOY9\tÓÿQþõ—ÜþÜüåºKÝ>ÝÐÍ^_§­Î‰ÎH&Û­3SIª¶l踲'ש7}ϲu9,¸vãqxã*8ö•.Þ¹e)ìu&$›aá­;†è6+Ÿ‚½ÏqE{ì?Á†»³Ÿ4o‚hmºaÇã/€ì°Òß5û^¶Ó.ûê÷¥Ûôà¼x= Ù•Ej‘ÔÁ|”ü Na&œŠêŸˆ¥ÐÞlœÀ×0hzƒ¹RTj£ 5MQê"&ÑxÏŠ3K$‰d{ºíÔP…A‡P„…¦¨´Äþ,…”€ßÜøHÅPMUiŽÀÒùOqÖgö¹`EQ(-D‘®àÑtZâþl%Sá\I‡›jšJ}TeéÿàÌ3ÏîS ÷Ýw?@Ò¢Bþþÿ ®©™¬ ãO<…)S¦ðöÛo³råÊLM•+WÒÒÒ‘GI*ÒÌÿÝq-Ñýrs8âØYÔÕÕqß}÷sÓM7ö©Îö´á2Ó”TUUñå—_‰D¨ªªbýúõÔÔÔP]]Mmm-uuuäçço·oë§}¥C‘©ñqÀdÚÇãqš››·Ë]ß:Ç¡²²’ššèׯ………Ñ¿ÿŒ#*÷Ùxô­éÏ37Üɹç¿Ã‰§ÁéÇïO™O!Uù-‹†ßžÌa¿í¸›§6Ž“ØÌ'åPvÁò÷(‡†{þw¥pn_Oß¹«´:dDúí¢#TèA©×½NüP‰X¯“ŠœÛààNAˆè,œ¾:ž~|¾Î/jja ðÜH»pÓOÖW+úH0¬Tà >?j „êÉEó@5ú!œ_a?!±uÉõµÀö¡Ú]éè ¥%%475áñx¨­­ÅãñPœ.ôZSSƒ×ç#??¿[¶z{}”••qöÙg³ï¾û²xñb<©TŠiÓ¦±×^{qÝu×uh¿«çeÔ¨<6lh+VbÛ‚††%%Arr<„Ã)N9e8€N  3yr.¼ð-Î9g¥¥¡Nžëp_­T +íp[ýɇwÁåäsÚå7²ï᳸ùœÃ©­hs¨ºÎå÷šR âÉ [:5$‰¤·tÛ©¡ª*cÆŒaùòe,]´AC/Ó)©¨V[Õ@U©ÀkÏ=!š™0aâÎ ÷UU5j+V|Îâ•ßP6a:º®QTQ¨B ¨õ1ŸúšÝÀ„½ûVDzåË)..fþKO3eŸ‰ ÊÏ"n Í™½:!¥¥¥™4ŠÒÒRjjj0TX4ÿeŽ›¶?~]aSC ó_zšÉ‡Ͳå]ç ÷eeelÙ²•-[¶Ð~.v]×ÉÎΦ¨¨ˆ_ýêW¼üòË<òÈ#hš†adeeaFçúuëP…²²®§¸ì©MÓhZÛ·¢(èºN ÐñAowèPe»ãöz½\pÁœsÎ9”——£iÙÙÙ™”™Þêè9*ù‡\Ç“/Ï¿ŸÿOþå<ÿäáÜôÐ-*‚zãý\:ºÝ$ ¾þy¨8n„ÕéÚ—N ! õ„›ÃضÕ+LJûû‘Cn~ªºƒB¡ékÈÁMÍŠèVnÌäS.  c&¤<žGhdú#óærÛÙv¦càÚª*TàsàOt¨ûÙÆSÀuÀMíÖMøsïu´òƒ@CÕ4Ð4Å@Q}¨žÏ¡ê¯bF&ªÎ'aÖvj£+½¡µžÏç#/?3•bíš5´D"(¸‹¼ÝœeW¯¢¢"ÎH§'Î;—Gy„‹.ºˆÛo¿§žzŠÙ³gsûí·‹utôôö¼Äb²4ͽöS)‡¦&7ÂðÓOkY¹²ž²²MMIš›S”n3¹EÏu´=T–Ž˨I0xôÞŒÝ÷þ¼p Ëþ=/—¾Ç•÷Ïáæs§c%“xü~ò‡Ç)8”›Ï: ǶxÿÅ'ùâý¬þbYz*P¥××i_Уë4Þèåt°ñá½Û·±°ám0Ñ­§Qõ9Ô~ÕµÍÂñ=]vòÝB£Kþ¡"·¿Â½ÝÈÜ!P®%eøa|»éê­¤[¤v•»ør`ê`쬘êž{^¼–æ¥AÓÉózñ+4Ö'‚RU²ùCžîä¹Q,á5ˆ­ËQ7oÄÙºËò#r#Ô§tb‚ƸIÊÚÿ(K$ÉwŒ•\öxÉú~|µt--QÊËË9bªªòõš5´´´°néöÒ#FçÒ\³…õ_¬@wb”¯úœ˜Ø½eÈÇBUUU¦†„‚@ @¿~ý2©§žz*“&Mâ¶ÛncÙ²eø|¾L1OÛ¶±m›õë×£ª*ì­Z¼^/yyy™tp‚Á yyy""v‡UU äää˜0awÞy'#FŒÒ³aø|ôë×¯ÏÆcW1òÇsÌ¥¿åÈærÅYwñ¯÷~Äá‡g úßlP(>fÞmw²J™X‹?üœ†sïQé'Âî´z»°_‡Ÿ…`Sù<^/ÃFŒ$ f¦-®¯¯§°°p‡ö¢Ñ(~¿UUB‹FÙR±‰Íåë4tX—3 dò°E:ùÍGìüxŒ@ºíðåÜ®ßZv†n®¹Ò¾ïnêxø)Ð  |¼ ÜT·o8(þ ø€«?ð+Ü™Pz©#ÓÍR˜}d’,ËÓD8 »M<ª/ÁŠïG²i8ÂÚL$ÖùÍ®tô†òòr2éw­ÑÑHÓ4ÝkªqG%}ÛèÉxhšÆäÉ“9âˆ#;v,o¼ñ?ûÙÏ2Ž‹{™3grå•W2gΜNglÙÕóòÕWõ”–0 À—_º®.G£ ÀG$b²ys ƒµ=VWǨ®îºÈaOu(íÒÉŽ>çRöšz8+¿Ã_®¿„åïÌ£¹®Ãëç7ϾϹ¿ø=ï¼ð8—ÿáQ†ŸÄ“¸‘h¸í|4×Uã86**¨¢×שªvý…ÙÝè _§•Ë¡amÛ¬!©v5EŒr­;ͪs§Z-™‡þÞ»}ûô“`˜qG·tv`ä,÷óëWÝÏH5¬]à.-~8}Ž[;cÁu®ÎTÄM“iïÐ}0åR·ÈØÕïÓ]¥'çE8I ¾OI`8*V =•B4ı¾Y‰Þ\…’¥"¶”#-3 ÕÛØŒ©Ù4& " ›XB²LéÔH$’^Óãy¤ü~ÇwûLšÂ¢Þç½gJ¥³3f,7^û ¾úê+rss9è ™7oyù}þÛï÷qÜÌã™4q>øà}Þf>É”IV0Ĩ1c¹éúkÓ:ò8tÚ¡¼òÊ+äçæõɃç AƒH¶4Ò/¹‰Â!#X³¥ í«%$šÊÊÆÑä±8næL~rùOP•x€×çÍ£¬P'Y÷Ë–SL˜vöûoÎɧ_V^ŒJ× 4UU©ªª¢¥¥…ÜÜ\lÛ&;;;S¢•aÆñÈ#ðÄOðè£àõz3…E7lØ€ªª 4¨Ot´¼ …Bà÷wœ·=''MÓ2š»KGk†×ëåüóÏgöìÙèzÇ_‘V}5=FDY5÷56Žfp®Føë•T;:ey^´üƒ™=«?WþóZnR/áÄÉÅxb•”7 eæ ãÉÒK9föaUA§q:üU'N‡ P}Ù¹ºÃ)¸2Ž¢3 æå£”ƒ3TÀaŠ  ÈÅ ¹6ȵ:0Û\”¨çÆØPBq>«×äØþþþtêÔ©J òEqD»v,Yò=wßsÆYXüÍb¦OÞàƒ†Ö&Ç7‹¿á¡G¦¨¨ˆ¯/æö3Î8ÇØ±ÿä›o¾eаá¸3°äÚ,˜¸®1l*éÌ…ýxöÙg*ÖöÙgÈÍÍÅårwÜ#û vYp%¯§ÏÀAü™ì`ìØSß¹8SQQQ4oÞœ¬¬,~úé'¦L™Â Aƒˆ‹‹C£Ñ^e}NÇm·ÝÆðáÃyçwðóóÃápðÍ7ß`±Xhݺ5QQQ ’# ½^Ïo¼P¥¨qõÕW3zôhTU¿…„$ IDAT¥yóæ8ÎFË¡ÓéèÕ«wÜqGµÛìСï¿ÿ>¥¥¥6HŽ:s“²ý¼³òV}[úýë)¹ØˆF÷¿ÏKÆw˜óÃë<¶À Úætq/—ü³ÍÐ`úÇ“|èšÃÛsçóìr(Át¾rC‡FÐ܇߫¬¨q,›î={UÌ”SÞ­¨Y³fôìٓ矞gžy“ÉTåµV«•9sæFPPPE–òuÚµïHâ®55ʦ£¬ü¼>ï§¶Êï,–ÿ\—Ÿàml1³ìß¼½IæàmÅñp_å˜Í Ÿ#0fφ{ï…E¡ðÕ´ïXоM6ª+Û±á¸íÍPm¸òÓÙ¸-ƒ¯WWÁQSŽú ªøÿˆu6ÐÂd¢]»v-ÈJKK)*,ä÷+ÈÍ«¶ãNõùjq~\{íµüöÛo,Y²ä´Û¬© Q±¿z|.……¥Ìœ¹†›nŠá¾ûúP\\J«Vž}ö/¶oÏ!8Ø®]ÃØ³'nÝŒ8PÀ‘#Åôèá0411±c;ñ¿ÿ%×+‡·`rú‹ýc)I—`­=n‚Qt.ð×ã.°¡uXÐ:À}̉ÆYŒÚ̈Ãb-·#~-8lõpÌî¡À¡‘VBÑ@êUÔ(W¹€PYÛ6m6tŸ~ú So›ŠÍVÂgó>ãÞÿ»·b,‚†tªC‡æ½÷fs×wa³Ù™;ïsîù¿»Ï(ÇM&ðË/ËØrLeXÜd.jB^ üºaIII|>÷³“^óÐC2åÖÛðD_ÀåW䇄Ô"Öþ•@PP7M˜Pï<µÈM&0û½÷ˆ§OŸ> 8fÍš¡×ë+ºvT>.‡Ž;òâ‹/’––Fbb"+W®¬ÈXÓ@ƒõÈ¡Ñh ©2~x/0ôz}••#::š#FŒÛíFQ”*ÇC£ÑT´,i¨'1\È‹+ÖÖ¼\ך+ž^ÈOװܯ5C§½ÄÐi5,Wé8êÞuÏmXÕ5¦ÜzÛIëø»ubQ£Äf%(8¸Òx-ÞQôµZ 3fLgΜ9Ìšõ?þxE‹¢¢"æÌù„àà`¦N½­l»ÞA Ë #AÁÁ”””œ2ÙOeÿSËæ¬?“¡ ÷L~ «ÕÆ; ï¤ÔSPy‡¨Š÷kø‰cœ.ÇEÀ³ÀS•ž/&óð޳qwmBÖ3‡F À¢E—%%p¨¹“®=3±§éq$wDÕ Q‘Ÿ•ÁÊ¿ ™ñÆ)šŸ×ãtüüü*º¶•ÿÝq8U ïΞͰ¡CéÓ»7þþþ$§¤°nýzÒʦì®Ú|.íÚµ£k׮̞]¿Ö;'ì°ÞçGQQ)~¸«ÚÍZ,N>øàäe{öä³gO5³ÀÔ1‡Ç£ÖzÌŸÌäUþ½é—¯iÙ…GçüÈWÄRR\ŠŠŠEUê}<‚‚C}Õd¨a~ªüÜ,\ÎjfªN}ÎSÕ iy ÿZ?°åÂÆ· yuÕu]vX~Ba£àDòvcùýq¼¿åuàvÀáµÈYË÷“XÖš£æâÊi?—–e†R‡ÏÅåv£ó”¢XŠ!X‡?´Az>É‘˜˜Øh9n¾ùfZ´h¢(g5‡ð<žÆ~øtÎIë¸Î‰wÕœ.WE®rK o‘¦M›ÎG}Ä /¼È“O>IPP~ø¡¡¡ÜvÛmÞ;»šòæó*ªzüB¦ÔYs{gOYåø÷a¥Ößñk22n*³Þ÷ÞÑÜ‘°…ßw(ÐSé;þ‰cœ.‡ ˜€w¨Œð¶Ú(çæxaãM¼œJ}s¼ð\w,[>=GÂÅC§€†Äî#H¯zLå½=¤çÖ/GuÚ´iSQôr»ÝäææÖØªÆd21eÊú÷ïÏ’%KX³f ÙÙÙ§Sm¾Ó.ãÇgùòå§,žÕ~õ??R}rœª…SPH» W©ã¤e†0zANúÜe¿«åTMýŽG¨±%×Ü2“æ¡-jÌt`ÏÖ—¨.ç)¡½…‰ FCPÙ¼¢À¶O`÷×à´Vÿ:—Ã;ìÈWÊ jY•k~Í©è ÞV¾€Ô§X±'’F ‘—‚_Ð)W;WÎÓê¸6t%*?'ªÃ6@‡ÇH©ÕŽÇªRâÖ Ñùcð "ÝD‘FOv‰†<›ÝGi)æ"yVÕÛJCñGë'­5„âL5JQ¼­'† Â/¿,cÆ 0[‰Åß|Ý(S½ž*ǰaÃXºô'Ö®]K\ÜEXmÖ3ÎÈã?x/vkj-RY‡xëÍ7ëôš†6ñ¦ $%%‘͇~H\\7Þx#ÁÁÁøûû£Õj+.Ýn7%%%|ýõ׬\¹½^OË–-™xÓ™·*‘¢²úv?9ñ5n·OÙCE­ô=[­øqÊ”ÉÌ›7Y³fU#'N¼©¢`¢zÊn:*eãm¨Þÿº\5O»§zGuNÕm£²Ö­[3nÜ8V­ZÅܹsÉϯ]ƒjÓæs §ÿþÜ{ï½õÞÇ {<£ó£áÔ-‡÷÷N-%ýÿq%¡-Zf¤uÇ èÜ£/ ^}Œ¥sÞ8¾ E¡ïð+™öô;$ïMàÝ™)µ—TÙ¿‚¦ÎÇ#º[?.{ úš/À=7{wžêB¿vÇ£Z×-:þsþA(µAë^±íôÅ W©w&”ˆ€yIÇ»¨ÔU©ùø4®æÃÞ‚JÎ^(JõPNG£óΘÒi¸· MÐé›Xœ+çiuÜv'8Ø‹Uì.7N—?º;V·†b%|«ÕŠÁ``ðàÁL¼iÂIÓ«JŽÓç(ïÊÒ˜ã,œÏê[Ô(±ÛðóÓU/4…‚‚‚QQ½ƒ¾)”]Ø(ÿ½uÊ­ Î`ò¤ÉÞý»ËÆÑ(Û˜FÑàÁƒ‚B‘9¶æ.k•FTÕãû<•»ïDÇŽFzhYµËw¥}ψqpºí8ÕÂ*ËÊ»Š©TÝO]r¸[€©Àj`%ÞPŽ–-û ||Š÷PŸ×\°~=|ô‘·µÆ™þZÔ”£>ÂÂÂ8p ©©©Ì™3猷§ÿ\ÆÇêÕ«).®Ý€“§Óç‡/rxï˜{—­\ü)9iGÐhµÞ³•‹?£]— yKç¼F£¡÷°Ë;õ~:v‹eÁËñç÷óª\”z<Þê¤RÇc®FLÏ Oûþvüõæê§®Ëñ¨ÖÁåþ—÷a˃°H¸v!ô¼Ž%Ôü:¿ þ$t¼,Ù`ËöN {É“ðçóµÎZaѵÐqÄ\ °G˨ޙP S¡ð¨w¿ª úL‚fЬ 4kë-h(e;]ï!—Á¿Ö¸Ëså<­v]‡—MC¾ZŠÛ­Pê]¿@, Å. N…}ÖRòm¥”x ,*…vN— §K¡Øíýúíòh1øû7Ú{Bˆ¿‹F-j€÷¢qüø«øbÑ"F¾œØØÞËÊjìÝV›ãšk®fÁÂ…Œ5ýú --õ¬ç8 ¦O›Jÿ~ý˜¿`f³™ýû÷“˜˜È?üP1Å«V«¥eË–Lºåúõë+9ê™CQô† Ìyy„†6îL7ç£ú5õdË",Ì„§ì.^«VmØ·w7ýÆávyÇEQ=Þ.(žŠ&ÞÿLž< ðÞm­è¢ÂñÙ—ôôt¶lÙÒ`ûóîãÌϳ£òzGìáè=U¶•š”È-¼ÄcŸþDd·Þ¸\N–Í7þïz¬ÅœHU=(ŠPê”#¦çéov”Ø,lú³îhÖú<]ýLÕ›“½ŽN{ ÅÕ´8 íä¶µyÈÝ¿= ¥Å0êo7–Æ9u`'$¯ò>‚ZBû‹ U,´ì ÍÛCpkˆ¨t¼TêÚ«z¼­;²wCÖ.8²¶V]`Εó´:«l‹ÃM~` àĦÕáT\Øñ'ÏfÃîq£8ÝdÛ ±;ÁêòÃéraqzpy´xʾz{üõøË}!„h^Ô7üë_lß±ƒ¶mÚÒ·oÆhÞ¼9n¼±"GïØ^>Éq®èׯ/Ý»w#))‰ä”RÊ:u¢S§NDvêDtttà ‚ù7ΡÕêèСû÷íÁ`0Þªu£ š{¾RU•fÍj7+‘ªª8ìv²³3QUhÞ ·ÛÛ5$²s›7n`㺵tíÞ£Ñè-RPéëü©¾×«Çÿ£ª Ìùìݳ—ÓE§¾k~™ÇSvQ¦)Û—ç´=ÌwìÈeÇŽÓ qŠœå]cNjN_Çàgãs¼ÝQ.Zog9G½Õ£6éÙ³'V«•uëÖ5JkªÓ_­ù®uývxn.5æðx»JÚ”÷DQQ½W›e¿Ë© ºø¼SÝŽzx:š‡§ƒ¢Á;ljwJÙßW¥¬C]rx<*Í© Wÿ‡ýäévës+³fþ½€€f 7B@(ŒxÖû‡sÝËPj§Š2Žg¨ƒså<­N~a ‡Õ²5ø—àTtØU6›»§KC©³»ÝA©¢ÃéR(Å[ÕâQµèýq¹Ë¦E.Û¦Nßxßk„âïâ¬5À;àÚe#ÎðÿP›PŽ3Þ²GÝ¿,œ(00ØØXbcc •䨉V§£e«V¸'˦õ ºýóYÛvíI:°¯Öë+Š‚V§Ãf¢EË–Þñ3Êî®i´ZâáàÁlZÿç º¨( z=";]eVž•7[ö¨*”ÚNãPjYp·ÇSe`»3Íáê8B@£ä¨«šrT§¨ðä;úí\9çz޲å»§W…TÊTQË ªŠªàíjFùlŠw”:äp:hµ:ïöð.«—/6$5p!„ á¬5„ø;SS‹pÂ[¶®W+প^ƒæªÞ~Ðn—ë¤æÂŠFCt—®DwéÚ€)O- ‹¥˜f!!eM˜½—UɇoýQ)§÷޳ZåZ Ê.æ”òKºòg¼w-TP4w°‹‹ ÑWj5$9ªæ8Wœ+Çã\ÏQqë­FTl[Q¼S²VÜQ/£T¶ìuêñ±ðêÍ¡Ô9ÇCÓ¯8íñxþáI v½cŸNصÇ×Y„8(èc&r×ô8ò·Æ“Ö{øÓñÜ>ÜÅ®í锞íÝ‹3æv»q8deeqìØ1´Z-ááátíÚ•fÍš¡Õj)--ýÛ]`ûJII ùùù 8ÂÂBäõ,Òƒ¢`/kÑ$^‰¥ˆ¬£}C!„h}zÇ¢‘–BÔ•BpìmL›4’V>g7€ÈkþÍ-—_@ Œ?yÞs:dee‘˜˜ÈÚµkÉÌ̤Y³fôíÛ—¾}ûÒ¡C‚ƒƒ}³É³ÛílذÈÈHÚ¶mëë8B!„¢´Z-Sã,Ó¶åºïÍ$|y5¡e¤†‹>ec’™„k^„þtÛUéxͧ¬ÜÏ=1Uišõ冗gå^3 IÙ¬úî&ôjަºíøµæ¢sX´>“„$3;¶lä{.¥EåÓD ¤ãUo0}6 Iù¬]ú kÚ is+‹O|_ë^¢G`Ù þݹgÕ‰ï{#S:úî]§mÏ ?šIøß4Úžö”6ÐÿÕd6¼JÏÀÓ­û÷¤ ÊÝß#~ÞåT¹œV D]÷6 7ädæ¯?~à¡+# ¨a;ÍF|Çö¤Ã¼<ÜXÍù@ÌÌ¿HHÚÀäº|Ö§Ë:_ø…_wzÏ¥ +–ðàåñ¯¼Žq(ÓÞÿ‹ Ifvïâ“GÿIÛ“"hîqnÏå« 5œß ]îä“3 __±ŠLùùù$%%±yóf’’’P…èèh Ä\@XXØ™ïDÔhçÎèõz¢££}EˆÞ†þÃÇú:†BÑ(¼cjÈ”®>gßó*wM^@ 1Žâ‰·îÄõád^ÚXxpdî¥æÆÐ‚¢ÆpËOsǨ(4¤œ°8œ¼ü#Oô[ÛwdsQ{Fþ{6Íõ'ýÒi¬)T«¬ØíÿxtJ'âç>ÀgÉÚŽ|”ß»ýÑ>Üñc‚<Ï'¯OƲøî_UBï¯óÈ|y#oáל“›ËkõaºðÅ=ÓXšéí«á±g‘\þ¦´šzHzóþ³¶ÐûœÛJf†³öÑËþ]¹ðÏÁ´ü„ «÷i¿NÓxëý›Èzþj^ØPˆ  mI·î¡x®'MZ™Wå׊¾×>Ä̧ҷ9x6T^¨¡ùÅo2÷¥’ôÞT&­Ì¥ÝØxæí¥èŽÅñòV UÏ&ÍÛ·BKcž¸‹¹Ÿg¥ã­i}-M2iÝ\ Ôáó®‘?®{‘Û{¥°øù)ì³´à¢iÏ3é¿ È9‚y)NÐväºÙßqoôZ>xà? ¿ž?¶€Ãù×[;qÚÐÞ\>ã?Ü?}-½5ìMq#/ÏŠ˜Fj1S>Ðè‘#Gð÷÷Çd2A=*ÆàÈÏÏÇélˆc'Ê:tˆÖ­[Ó§Ovì1'ÄùÍœ“ɶÕK}C!„hZî¼}ª Èãl=´mÕë¾7« _^­†*'/×´š¤~™dVçýÓ¤* ‚¢_ø¦úë¾Cê»cÛ¨š“¶×F;ì³YêØñ³ÕUIÛÕ{bŽ/7 S_ÙnV¿Ÿ¥êÊž èñœú[R¢úH¯@]¤z£ê¶e³ÔUý´Êñ×ë/RŸýˬ®}ˆª¥…:jî15aùƒj”_yæ‰êÜ}fõûi¨:Ô˜™kÕ-;—©7Gú{÷×ë%uEâoê£Rý1 ú‡úæŽTõ­!ÁgplÕ4n™º3é/uzgÿ²çüÔS׫ IfuÓìËÔfåëPßN0«?ÝÓUõÅ F]óº:÷ÏL5!ɬÆoø]}úº®ª¾âóѪaq÷«oþœ¬&$™Õ„ø-êüŽª ‰_«CƒQѶS/á7õ§MÞåIùêÚŸç©·ö«ôyiÔf½f¨/|·_ÝždVvªŸ<6Nmëï]®F«cŸýM]™hV’ÌêÖ ?©Ó»–}ŽšµÇͨ‹6æ¨ IfuË¿« 7›Õ„ï&ªáï9pÅ;ñê¦$ïk·­ÿ]}êê ÔPý:? þ˜tLýp”±ì|BUBǪ&šÕ¯&´;é| ìõ’úÓ¦ÕêóSƪ·|gVwÌ»\ ._®„©—ÎÍV~ºKm¯-{Î/F½c¹Yý냑ÇoÅ#PíùŸ½j®uMRš:{LËJû3¨½ÿ³GMدnHÊPß\q>×ô^5 Ë]ê«_oS×ìõ.ÿýUµ·þ„ýjüT?íñûwy\ý9)_ýddsï6ºÍRMJSßQ~L j¯ÿ$ª ;檃›¡‚¿ÚiúꟿÎS§_1Iý`·Y]z`ƒ{ø“³îÒÊn{r׳ò D^Ò“`~Zåg–.¸-Aª}«BN~J@¦@'ÎÐv´ ñ§~7½UŠ÷­!•H.ìÒÌ» ]{†ïBѾtßLïï–ýÛ ¦‹¾ˆÝ[Rq¢Å4ò=æ½2ËW3˜rÝ8fý¬cìKßroo~‘wñþ¼§ˆ3/àé;¯ãî§æ“¬©ÔáBÛœ.ƒ/$üÀ<8õZîºçQ–{®àþ_gpsï>uí'óö/Ñ7å-î»a$w¼¸†Ö·ÍãÝÛºà‡Ž¶×}Äsÿ2ðÛcc¹éÚË™ùÂ'lÌt:Zû”Ïf]æ÷'x`êõ<17Wåþn3{–<ÃÃG1qÂÞÞÚ–ë^ùñ:œé+Y“éOÏQ=0”­5’î~ɬ‹ÏÁsÂQ´'<苆óä¼MäŸØèF $Ä,ǰ”¿Ð™ÎÎ$Q}?©û†Žf­›£˜Í _Yvÿt.(Ë­i=ž{ocÃ+/²®8“ÉàýÌNñ^üÚft_?V>}#Ón¾†û_XHʉ#¥zœ8+e÷7µ£ìË´ZB{]B[ö°r§¹ì÷ÅÆÁ•±õ'®}PJÊ'—1üòÉÌY“†ã¤_*@i΀‡>cBÑsÜÿî_Xª[§©ªJ^^`Ó¦M:t­VKLL $**Jº©4€ââbÖ¯_O·nÝ÷u!ê%$¬^v¯c!„B«Õ¢“15Îu*–M÷12ú¾š×ðœâŠÊy¯zŠó^âûß#y—¡ IDATÿÉoÛüˆ¡gÑ÷³Æ¬É|9¡ _V÷Z]£g½F\ÎGLûæ0.‘ˆæP˜n¦âºÑm&=σ¦EÍ´ö½GŸ7Ë*ø<”ôã©ÿíbN2Ö¿Ç+¿ÄÞ«Q­¡9%ÙþñöF.ì‡fÎÓ÷ó鯓/ºO¥4m»‹fÐàH~ÍÃÕ~,W_p”/o{•^¿ÁõCY¿²˜!D¨‰¼¨tøç½ãÑü|¸’bâ÷Í$zÔj®¼ª;oïØMô-ÿGËîú¿Y¬+T?Ø8ž«Ÿ­ºKÒü¹&«Ø’Ö‘‹¿ŸÀèh=ë¶zˆ™ô  >ä¦'>d·ˆßÇsŽeÎø1D|ò­MhlÛÙºv»ó=PþDñÏÛ/ÃoóÜóÌWä¸5›)¹øVúU\³Ú9úç-ûמ´Ö\9æe†FéYœ~€ßWcò諹@¿–%þ´GhþZÖ­®ïŠG…j+K3{×§Àí÷qËEëùpã1hI‡-øéñ;ñ5J ¡&=î‚6ô»®}’i½Ï#«mt™øòç2ñ׃Œ¾K¡kDstdã<å{).{6“-¿/ç¯üÓW4ÆÌ|éÔŸ¦ðù^@ó¶a`ÛInÉñ×»òS(`4mCý¨î“ „ÇßÁý繫Ry{Üçuàëa%‹‹‹)..&%%…ÀÀ@ŒF#tïÞ½J7—ë¬OÕsÞSU•m۶ѵkW‚‚‚HIIñu$!ê¤ÈœË_+–ø:†BÑ(tZ­Ì~Òôi ŠèB„&‰_¾ZÆ‘Rn}F]?²š+ñëÈ•¯.ç¥Á[xî¶Yl-:õdÍKU VÜÊÚÒ»[gÆÝú4»£fòÎÜéRv×Þ•6—;.îÄÀ.­>v"Ÿe áž¹ ¸¾}´ïgÍ^¦ÑRçODZ·Ðéè"–ůäÛ¿4 ž8œ0žÎ—tCIYAb#Ð ‚¯ü–õÊ)ݽš)m!¬}~šætîÕ /gïiŽAeÎc»É&„–!:ЄйO[h}‹v•„šÊœ±Í!¬¡~/~•Ž«x{ÕZÞ|xýÛxë éÓR×mÃ\ÓìžºÖ ¹ûs¾\ÌÖ¤|þúå9ºãÞOJØ¿ä²[ŒáÊ=hŒÄ^‰eÓ$•Ôíð‚ƒßÊë„1mÞ^¶Ègë–?y4. S)<1Ÿ&S0”˜­8Ò¿åý_<\vÏu´k1œé7µaÇ{°×b¥°‚Z„xâ<å{©M‹‘<¼è+Æe>ÇOüDnCÍŽêß… Oߌe΃ü˜vîea·ÛÉÈÈ`÷îÝlܸ‘œœÂÂÂ8p ½zõ¢]»v †ÓoHT±oß>Ün7=zôðu”¦EUA‘iœ„BQ?Z­5š6ñ þóÚ ˜_ìÇ“‹Ópó>ó¾yŽo¿|“G~^ν+òO.HøµçÊ×—óâ ¿xæúi,I®tGße&£:G„¡oë míŒ<ùéŸêÂÑe&eÝû<;ëb.ùèZFtxI•ú¨ò÷ýÂÇþ‡‹×ý—«. ç›ÔŒÚ·Öðä“°<I÷Vëè~M’>_ÂQGù‹Vãž=ƒKÚ¢]ŸrV¯&Ý ( À±/'rÏüCU†ªt[R±ŒªŠ©ª7U¨n'n4hÅûýÐËL»ïò*#Õi&ÝÎÔ…<ð?0þ¦Þñ6s§ßÇ×SGñb¼ *(¥†n9:"n˜Ï{÷E°üÅ»xeóQJ[^ÍKs¨Xñÿ ~J¿ñ×öâ플îVʶ9;°Öö¸V~_–Ì¿£_†´¡u¨B±£Oý¶ˆ›âO.ºhƒ1½°ZÈ–?$õçyè‘|†º–pç/é¸Õ¶ÚôÆ ´µx/µ¥1ä±/¿äŸÇžcÊíï²×VþÁ¹(Ì0ƒ¡ &½Vïóº°N4ÇLFÁé‹~¯æªb6±µJ´Y½~(“GÜÇö:Œ‡Çã!77—ÜÜ\š7oŽÑh¤[·n(ŠRÑŠ£°°ÐÇIÏ©©©F D||¼ Ð*Î A!¡ôºhûÚ×Q„Bˆ§ÕjÑhuRÔhÊt-ûp¡ˆÃó˺‹¨XöýÁÞ=C9ùÓ×ÓõÎÅ„ÿîÌuÓ‡â8Ìædè8b8­ª=,´»°šäÏø`á2v$î!qû.Ì•WqìcÉüÝ„Ž¹K.ž@ußo-¨m}¦*΢ RÓlt™ö<—é·óÅâÄ“gèÑ3€½ØŽ 8“0gS(ÃÇwåè¼wˆ/VÁc§Ø†° ´µy/µ¡‹äúÿÎçê¹mÆ»$Z+¿S7 ’AwFĆ–ŠôDˆ#غ•M©§ŸÇudw\ÇÕW”?Fðôì{†É_ Ñ^×ÀgOaa!ÉÉÉlÛ¶={öàp8èØ±#ƒ&&&†ððp¤Ð}jùùùlß¾þýûêë8Bœ–µ¨@ B!š,­NÆÔðc/ +ÂRñ„›Â½H«‰^^âR} ŸoÊÀ¥‹ä†¹òp‹O¹åêgØt93gtãè¼gØÜî=½ÛQùI:ŠÍ“Ëú÷rì‹Gyå±\Þ[m#vú« °/ãá’q@ÌÌ,¼ÕÂ;ã¯baZcàC;8jvuîM@Òˬ8R rÁM2Ü•ÀÞ´"´¦X.ö=xn}vÞ+@iÊO¬Èx)w]ýIüq¬¬ù€e3_þpŒE“ïÛϼµ§¬‚ë0Kßû™io½Ä¯ñÑÛÈñ„Ò>:ˆø…_pÈîàÀ‚—Y?q6/Íý/­>XÂ^³–v}[yµLegß¼·Ù>áežûä-Âç,å€YKX§Î¸×|ÊòTáC¦pYØaö-@ B¬ l‰8GøùÍÅLûøæ¼Â{ßm!ÛÝ‚mý  ”Ìí‡aÌ$fLÜË·ñ8Bbh^%“´¥ï°éþ9<ý´ϦIÄÖ¯¤¡´¤sL4í¢rñõwqý@•^ÇWÉÕܱÖh®‡R‹Ýû9º³Xùê3,™ÆòŽ­bT'6› Ms#šÚ¼—Ó êÿ0w,à§™kP:÷¦{Ùóžâd)Ä‘ô9Ÿm¹‹'_z©Ï|Ê¡×òïÉmIùàmâ‹O¹iÔÒ<ÒUNLx±UŸÎ‘”ìê=•””žžNzz:Z­“É„Éd"::šâââŠVvû9\¥ñ‘ÒÒR6mÚDll,AAA¤§§û:’B!Äß’ÎÛýDçëOgòÚ§3«<•ðP S66ð~¬xmÒ]8g=Âcó§£,)°p惼¿Ç:ªôgöo?”.ÐbÆW,šQi;¶˜>øV6[U,[ždúà Ï=ôÿ½EGñþ%¼2é–ç¸)Û ŠRÖ]Bk e—+¹yúc´ JÒÙ½òYnq6I¥€@󖽸òÆ{¹×ä“¶åk^ºiߥ×cPCÇ~~ý-ƒ)·XñùŸT\`ÚÙÿÕ\R&?N›Í‹H¨ÃMÎÏ3˜¨y’Gï¹—ç¯ AÁNnÂBžüîKÙ=¸3ñïk]ÜóäÃÜþöMkÖn$§–]Gæp÷ NþýØLþï­Iè[Æz¾Ø5å©nBºfÒ½—¡Ô¯~•_\Ö?îcâ]é<2óNžŸóÀž›ÄæÕ)ØU'¹_Mã‰ÎÿåÞ‡¾àJ€Rг¶³É|<œ'g-9Ƽ›ùeþÚJÇ¥ntnå­ï¥m^»6ÌãÑq³ùuoaõÅ'žfÞ–åËKö¼ÏÓW^É£ØmZ ×8I­Å{95ZöëOsZsÍÛ¿Se¼ÿ­·qñMßSà:·w_Góß`Ú»ßbp¦ó×g“xê¿;ù»^¾»Ýn²³³ÉÎÎ 44£ÑHll,§¢ÀQTTäã¤ç–]»vѹsgbbbØ¿¿¯ãQ­@C0þ1Žu?/òu!„¢Áiµ:”çž~RýÏÓÏû:‹¢QiisÃ/üïέÜzÅì>GÆ|ç>ƒÁ€ÑhÄd2a0*fRÉËËCUÏ“f)¬eË–´k׎øøx_G9ï„¶l‰F«%?3Ó×Q„Bqzîé'e P!š2mXz´Ó¡m?žûë¦''“( Q6› ›ÍFZZ:“ÉDxx8]»v¥   ¢Àápœ~<’¦*;;«ÕʰaÈÇj­Ï0¼B!„¢®´ZSCˆ&LCó¸Y|ôîH‚lI¬üïõ<õKVÇ)¢œËåâØ±c;v €°°0ŒF#½{÷ÆårU8Š‹k18IcµZY»v-ýúõ#--­¢+¾æçÈà+þÅŸ?Î÷u!„¢ÁédJW!š2ùËþÅEѾÎ!š*³ÙŒÙlæÐ¡Ca4‰ŠŠ"00°J7•¿“øøxbbbæðáþŽ#ÎR»4„B4YZ­LéÚxÂÂ4´mHF†³Yî !š.«ÕŠÕj%55???L&­Zµ¢{÷îäççW8JKK}µÑíß¿ŸˆˆbccÙµk—¯ã!„B4YZ´ÔhaaÆ£~ÄøñC Óø:’BœN§“¬¬,Y»v-™™™Ñ·o_úöíK‡öuÌF•žžNjj*qqqøûûû:ŽøÓhµ\zÍT_ÇB!…V¦tmõu !„¢Qhµ:(´±ddØYµêcV­ú˜ôt» Ñ´õ>œ«Wû:†8 ‹Å‚ÅbáÈ‘#øûûc2™hÓ¦ME7•ò"‡ÓéôuÔát:Ù¼y3=zô ((ˆÔÔT_GB!„h2d ÐFd6{øá‡õlÙ²ôtSCˆÆ&óOii)™™™dff¢( F£“ÉDdd$v»½¢ÀѦHݳg:u¢k×®ìÛ·Ï×qÄßÌe×Ï`Å7û:ÆYÕgØpv¬]íë¢ Ò)a`z>.ÕÇ„ø›ÓJQ£q™Íér"„µ ª*yyy³¥4kÖ “ÉDLL Z­¶¢‹ŠÙlöqÒúKII!<<œÇ#ÅnqvüÝ €4D£‰‰0òÄ ¯ð²'Mº áKRÔBqN*..¦¸¸˜””1DDDœÔMÅårù:jäää`±X¸è¢‹HHH ¸¸Ø×‘„B!Î[Z­N¦tBq³Ûíddd‘‘F£©è¦…Íf«(pØlçG˸’’Ö¯_OŸ>}ÈÊÊ"++ËבD÷«oeÍÒ…¸]Mc¬!|iz>/<ñ`ÅÏBßÒ餥†BˆóˆÇã!77—ÜÜ\BBB0™LtëÖ EQ*Zqú8ééíØ±ƒèèh‚ƒƒ9xð ¯ãˆ&ìïçú:‚M†KEºœq‘)]…Bœ×ŠŠŠ(**"99½^Ñh¤cÇŽWé¦âv»}µZIII´iÓ†Þ½{³sçN_ÇB!„8¯hµ:4¾!„B4„’’ÒÓÓIHH`óæÍäååa2™4h±±±DDDèë˜'ÉÌÌ$99™ÁƒŸ“ùÄùïâ±·à¨÷u !šŒáƒúø:‚¢i¦!„¢Éq»Ýäää““@hh(F£‘ØØX<OE+Ž¢¢"'õ***bóæÍôë×äääŠî5Mªª(ŠâëMÞš¥ |Aˆ&eõ澎 „¨DŠB!𼂂 8|ø0ƒ£ÑHdd$ƒ¡¢À‘——‡ªª>Ëèv»Ù²e ݺu#((ˆ#GŽø,‹B!ÄùBºŸ!„ø[±Ùl¤¥¥±sçN¶lÙ‚Ùl&<<œ!C†Ð³gOÚ¶mK@@€ÏòíÝ»UUéÞ½»Ï2ˆ¦eȘÑ…ø:†BÑ(¤¥†Bˆ¿-—ËEvv6ÙÙÙ„……a4éÝ»7.—«¢GqqñYÍuôèQL&$>>þœèTœÖÿò•¯#!„FŠB!D³ÙŒÙlæÐ¡Ca4‰ŠŠ"00°b&•¼¼¼³’%//«ÕÊ AƒØ³gÏy1M­B!ÄÙ&E !„¢V««ÕJjj*~~~˜L&ZµjE÷îÝÉÏϯ(p”––6Z»ÝΆ èÕ«¹¹¹ddd4Ú¾DÓ7ê:vo^…¥0ß×Q„Bˆ'E !„â4œN'YYYdee`41tèÐÒÒÒŠV‹¥QöŸ@TTÑÑÑ$%%5Ê>DÓµiù·¾Ž „B4)j!„uTÞRãàÁƒc2™ˆŽŽÆßß¿¢À‘Ÿß°wÅ:DëÖ­éÛ·/Û·ooÐm !„Bœ¯¤¨!„Bœ‹Å‚ÅbáÈ‘#øûûc2™hÓ¦ =zô¨˜*6??§ÓyÆûÊÊÊÂb±0tèPâãã±Ùl ðDS7pÄxlß@a~¶¯£!„ NŠB!D)--%33“ÌÌLL&&“‰ÈÈHìv{EÃjµÖ{‹…õë×Ó¯_?Ž=JNNNCÅMÔ–•?ø:‚BÑh¤¨!„B4’ò–Íš5Ãd2ƒN§«(p˜Íæ:oWUU¶mÛF×®] &99¹¡£ !„Bœ4¾ „Bü“’’B||<;w¤„ˆˆ† B·nÝhÙ²%:]Ýî5ìÛ·§ÓIÏž=)µh ú]òOŒ-Ûú:†BÑ(¤¥†Bq–9222ÈÈÈ@£Ñ`41™LDEEa³Ù*ZqÔfÌŒ´´4¬V+ƒ ">>¾AÆîMKüŸ?ù:‚BÑh¤¨!„BøÇã!77—ÜÜ\BBB0™LtëÖ EQÈÏÏ'//·a6›Ù¾};ýû÷gÿþýõêÒ"„Bq>’¢†Bq)**¢¨¨ˆäädôz=F£‘Ž;\QàÈÏÏÇívWy]ii)›6m¢gÏž‘––æ£w Î5½"3e?9G|E!„hpRÔB!ÎQ%%%¤§§“žžŽV«­è¦MqqqE‘Ãn·W¼f÷îÝDFFÃþýû<“bˆ¤Çè>x6.%1ËU‹Wh0DÇÝúû×®VZ¤ª¨ªZã+EÃHذÜׄBˆF#… !„ç·ÛMNNûöícÆ ¤¦¦@ll,ýû÷'22’’““1›Íôë×Ïûb?šæF4Í ó?£J@ Z]Б ÿÚ~…Ðbˆ@Ô&tJÕ%[ÐPð ïHÛN¡hq/B!„ð-i©!„Bœ‡ (((àðáà ŒF#‘‘‘ †ŠIII y9 KèЮªª²ÿÀ~¬Gâ±ÔªbOú–×Þý…ìDc= Êé_v•$ÎçåÙû |=/éAèy0ÒjCgv\Ä“ÏüFnè?yõ6z[Ÿ‘Bˆã¤¥†BÑT¹J±¤`í–x>ÿíϳ¶[OþžŸt37M¼™iï%`=ïou—rhþL¦Ýû(³¤ôL6å>ÆŠÏ!h}éÄœQACŲc6Ó&ÞÌM§ðâ:3'Ç*ê# ÓFv þà³_Rqú:BˆIQCQÅð>}|AÑ€<Å…ïO`çŸ+ÎÒ¤þþ‰e]l¿cCîyÒ‡áTwÔ ‡å—@׃kG¶ÇïL6æÎfÝw›°àb÷’•¤ÉÕwÃеaøµý²–/eo­ûI !„8Û¤û‰¢ŠÕ;vø:‚¢¡¹JñŸQû‚ZS­‰ü¸"¦c¸•#9‡Xºü(—LŒÄ¿ÆÙ8°äCæ­?HFv´!íé}ÉUL¼fmü+·f(bÕ SYåTцtⱓ¹íŠhÊ{xŠ÷óëÂEü¼éf—ŽÐÈŒ™x cº… Qíù}!_®ÞÅ£yØUP‚£¹îÉÇ«[Í»o}OBz¥x÷ßç²¹m|oÂ*uepï|©¹èñÙL-}—Û_O8yÊØðq¼üÚ¿èP¥jQJêÚ-~=FÒ»yYh·™]?/âëå[9dv‚6­‰<‰ÿ»¶ 55æp¤,ç§Ã€[Ú’AƱßYºo wÅPÊëw0oýAÒ²‹½­ Âévñ5ÜvÓP"ÊŽ«jIbÙ¼yüos EîŒÆªc}¸Íñ|;)íK!³È ø>øž¿«Í\›BžÍ 3˜ñSnbx‡@ïñq›Ùùã|¾üm+G-*þ¦. ¹j/í„AQhÖc ñ¬·mgõ!;½bõÕB!|JZj!„¢x0oû‰-6 ÓU̘rA@îê_ØwÊ;Ý¥dîŒ'ùXUK€¿‚»(•ø¥³yü­?É9±bàÔ wQ ¿xÏvZ¼ƒ9–¦°ä…çX¸îf·½‹‚ä,záu–eº;)ëV“p$»ªàï¯Aµª˜‚uhüòYP‚BÑ{÷¿mÉ[|°ñÄ.BÃÂ3µ&,@ƒ&ÐH›°0 9^¶Ñ[`8ñ[–Û̾ý…´ØÙ[¬P-ìú|/-Þè-hø¢sÛ):–Âö„¬š»º¨Vöþ¼†| Å¨é̸ÌXÙ´4ž‚ŠÀ¥d&l'9»'ÞãŠ#‡½¿īߤx·íÎåÏw_dᆊÜàè"ÿ„X]¹;øã¯$oAC€Nœúü±²{ÞÓ¼üõf’ Ý €3ÿ k<Ë3ß%ãÜ…IlMLó4üüQ(!sÇ2ÿ´…#EÞ»k¥¹{ùuölþÈöî×}l%oÌú„{sp†¤XH‹ÿ×^ø†C¥µËŒ¢EcË!ß@pH0~ØÉÙ¿Š9¯ËáR@µ‘8ÿi^ùn G-Z‚C(Í;ÀŸ=ÇìõeŸy`'vÖvî>&]P„â%E !„B4 ×1Öÿ¼7Zº]Çÿ³wŸñUT[ÇsNNz%„„’„z E ÒÄk+*V°ãU¬(6Ć‚¢‚€ˆŠˆ^TŠATz½BIïíÔy^0 M’àÿûÆÀÌì½æÄά³öÚ±-ûÓ=(]Ê·+ Nh‰ºWæƒÉS÷Ðyö”Y|—ZùÕ>˜ÞϼÏäɯss#€RV/Þ…“ÂäÏù6 ëËSï¿Ç„÷Çó@'?0·37iÏ!/¥u¯ÃÄÉ1}Ê“t5°Ôîè)SxÿÍѼ0úeê ¸ØºbÏ!‰k»a¼>îmÆ¿õ76öÆ·Åí¼2îmÆ¿ý2÷t ©8)¨ÃîëEíÛUºòHË ~TÀµ˜Ï~ʼi3ä¦|øc®­wÜÏÉ“¿’o—— è—ØˆFç÷¡>à^÷‹3ÿºÜ§âsÄ‹—Ö kÙRö9Á•±˜Ùëœ@½y‡É“¦0aX“#Oê•À£&1uúGüßàx¼²óé9€?ïËÔiòêMM±{æÌdu‘yèµïN⃧z @W™ÄÔw7`nçm%˜”³ù_°Þ þ]îçÞáý÷ž oñ3?l+?Á˜}h~ç8>žô6¯¿ø<¯ŒºŽF9kÙ˜çÆ½˜Oäõ¹zôû¼÷îû¼9¤ ì$·Œ7`øSm^Z¾’""Õ”–ŸˆˆˆÈ)áÜÄüÝ€w$„bñäüžÌû&‹uß/%ëœþÔ9¡])¬Ôj7€žá¿2;'M;ŠðT~Ï7kMš†Á:rfØ IDATögêŽ]Ìÿe Š9B¿oê¶ŒÅç›LìÅÙ”¸Á™¹™ ÿ–ôhŒ°ÚŽ÷K²`ó‚²½)ûcoÃ#°PïܾÄOÛÌfçNÖe8iwÈuþÑm¨küD¾i§Øab6¤EXµÛ¤´ «˜M[Š(]úÃnª|})é™å8ì'³;‡åÓÇñÁÜ-rÀA©Ãƒ#7…4ö0óñ!̬|JÞnòÝa5𠨨¾q—•á6©øwODDª%5DDDä(gÛ‚Ed8’{÷M‡NÇ¢}½ØàDÇóÂ{ÿSŠéö¡ÊÃÀzð˜àñì?+° }{FãSé\ßØÈ£?ô8w3{Üt–e@pÂ¥ ìZ‡Ò3˜±¼ˆƒx™õ˜‰Åù3ã',£ÚÝþ—7ò=ò»¯aÃ×8”9÷âñT$M¼lx¼è8oÎÎ},š—Zñó¶)Ü?xÊ!‡³~YÀöˆÐÌç/W‚—wÅv´æþy1ö÷—páúÛ½0Oâ ßâUqŸ¦Iůˊ÷þ|„é1OEòðmÒ“ó›V*+ö"*Öò³IѪɌŸ»‡5†ó¯éO3Ÿ4æLKš¹ÿWzðþÃé|A×C’m– æTìlâ,«¨Ï°øø`UBCD¤ZRRCä4Hìš@Ò5Ü‘³t3óþ(À!Ô{ÿ«¨é¦4¿;é$%¥qÉ1X¬û¥“ï„ÐJ%n— ûže,Í :&ˆãx؈hVãçBÌÒ"‚:\Ì•-‚±â¦8+Kx6ò|©+¹$\r)ýšÛÈ(þ¡"©QqGøû¸3·‘ålO¬——iÅ˓μw>b½ÏÊÝ=#Ž«WõB€Òböe•c6óÁ+¢µI!£x-‹6Ò´]Æ·çâL ]‡~>Ž´$’2*~ö Ã÷À˶YN^~üÁ¼-×Ò¬õq?4¼£ZP—dÒì«™óÛ>š÷®{ü‹\[¯- XÅŽò5Ì]–E³óBÈüm>Û¼bhYÇ'<\¯Z4Žñ…ôrÊ }h5àjÚ‡Û0<¥dgy‹ô…ôãÅì¦hOfŲ¡è>\ñŸDj;ֱ곹¤•ï=²9Q¬fÅ”Õ9—+ú7Äßbâ,Ì¢Ð;‚p+€“Ü==PBê…ê¡YD¤šÒŸEN%4DäߍdýV•–fÜ5æIz†ø~ÝÁÖ)Ã9?Ÿœß~&õê!4ˆ©‡/{)Ïü†§Fùðò3½Ž”ù¿'òƒ/žòòŠoÔëöáÂx_þº½Èá B;^Eÿˆ™›•ÊW/ ã+/—MúÎHzåRïHZÖ‡•©Å,=‚qµ0²wW:ÁFDëV„|™AAÆÿxüŽyøzœÄÜñX¦òÙ–ŠàJR&óÄýS°ÖîÅð'®¢aå-_¼jÑ,Þö•’¶*òóBð«Ûƒ š|ÃG[òùù•aüægþ¿:€ŒÿñôèZ¼þdïJý9ììøé7rêâ…Ñ—P÷À1×>¾yâ>ÛSÊÊù)nÕèxÖÈ\Ó}¯/.d͇pËÇ~ø8ËŽ{]Å=žÃ ó¿áåŸóX1á!n~ƒ•-Q®¢]ñ÷“øÓ⊋‰]ö;3æóÚýó±Ú¬¸n¨?ˆ×G_BÝãÆìEXã8ØKIêdžñ õüËØY^ù¾{rÍ9ß1ö÷"ÖN{ŠÛ§Ù°Nœ¦7FŒgx[?pìeÕŽŠÝ^b[ÕùgÛT#gÛ°j*"""ÿŒYÈšù)ØK³óiZùñ›˜žçP  `)?n-ÿÍÍÜ{qkêø‚Å'‚àÊ_±‡ãç(ÇÄ›ˆÖ1üñ+‰;ê^°‡2ü[pã¨'¹¡Gjû.'ÃÚàí>Fg k$ýî¿—­"ñuå°sËRó¬ø×Ž%¡y^€oÓA<|}à,¥ÜF°³”Â}™Hš%äåå‘——GvFÁv.ñ¥áyíðÊVÿĦ¬‘ôþWwj@`/3ˆhÕ—«/ïFl[Pm*—~”meÁÒŠŠ˜=;ڣī]zU¬ï±¯žÏš¢XObÓñŽQ<|E'bC¼ÀQ†Ý4ð ­G‹v <Ö“¢HÛ[žå‘+;h€ Ö†œsí—@L¶}ùJÿvwN9ìÛfóù:øvàÆ+šœÖ„FUJNJRBCDj¼³ö‹‘êÇ ¸ý`º®6ûZü‡–þg㢆šÏ§ñ5ŒÀŠ ~t Ów€""Õ™’"""RµŒPz=÷1½ŽæÙÁ¤ÅÅ×Тªã£3üˆ9ï*bª:9.¥žEDDDDDD¤FRRCDDDDDDDj$%5DDDDª1£ôÔT²íj**Ø5¡ªC‘jDI ‘jÊ,XÌs·cø“ãX˜í®êpDª…¤%ÉU‚ˆT#JjˆˆˆˆTW¦OUÇ ""Ri÷‘*å"{ùL&~ò#)å`ø{CBǃç¤óÅ£·ð@ô¼ùâjeÎç­±_‘²§` Ž&¡ïµ ¹¼aVpç­ä‹¾eéÆTö:çÞÇ Ã:¤dEDä,¡J ‘*äÉù•qoÎ!%£kH$Qµ½)ɵãç{ècš-(”°°0"#°›AnF1F@0Á~îÂ4V|9–wÏø²“ùyé–Š„†—6œ8ý‚ñVBCDDÎ"ªÔ©B®‚4²M€x?ó4ý"½0Ý.°åΊⲧ_æÊú•Ýj÷aÔ”ÞxÊŠ(()fǬ»¨­+öà8/Œƒ¹ ¯ð_ü<8]¶3xo"""§›’""""UÈV·3Âç² g“‡ã§.}¹øò‹97öXi&eÛ`¸,ËprÄ^êÀCENäPlzò‘³Œ–ŸˆˆˆˆT!ï97¿0’!´!Ò«”K¿aüÏðÙv;¬·0ñ˜•¶tuîfö¸é,Ëpœp)·Þu;ƒ:8UDDä_CI ‘ªä*¦ÐGŸÁ#xã½çX`KVdâò À ÇŽ½e˜€évcºòØ™ HÂ%—Ò¯gº5 ©Â›‘ê.!1‘„ÄĪã”S¢ˆˆÈ–˜@RrrU‡!Õ„=u&O>ó#%Áµ ÷s‘•`%¢^^>~tŽ·°z³ƒUÿw·ùÛpúõâ™1hYV¦³pôvÆÕÂÈÞ]Õ·"""rÆ©RCDDä SBCþdâñP¯Ž?®ÂlÒ3ò!8šŽW>ÄÐn¡ÖÚôvš‡cä¼Ô‰_‘ô»ÿ^´ŠÄוÃÎ-[Hͳâ_;–„æaúÖJDDþ"9)‰ä¤¤ªã”3f͘ntCUÇ!"""‡iغ5.— *÷R¨ôó_Z'˜m¦`š&¦iâq»™5um:žž`E䤥$¯à¼^}ª: ‘gÖŒéJ䋈ÈéÕ=1ÅI5³2¡:Äž–†Çíþó/ ãÏ?ÙøËßT0Í£©Á”Ô‘Óªª“ÿD•Çn8ÊÊðxaîêtÊ­ÁÄu»Œ[oîOã€Ê;Ô˜”í^Îüy‹X¶f;3Ý´yämi]À¼7^â“Õ¹8ð§A‹ú8¶o!ÓA—[FpOï(lÇ ·`9^šÌò}”íßTÇâNt³Žô8ÄF1;3YúÅfü”¾Rð iHÇ ®åæ‹[j=eŸžˆˆÈ?¦J ‘jJ=5ª;“ÒõñÜÛß°,µà$oûoLù=–üõ·g–oæÓÞàëÕé”c€»‹§ñüØŸÉr<‰s^áÿÇg?®d[f)^ÁµÀƒi–“½;¯N·óð úìÛ°‡è†scó–~9Ÿ]ŽãDìÊ'm÷þ„†—¾>V“y™€g?…É£¯"pnø’9;€IáÊIŒþd ÅÖhÎòoMþˆß}…a ¨å°…4 YB ja%¨A3Ú·® eù”ðî¿u¸ü… |øáT¦ÎyA€g¿®+À]²žÙ¿t{h<ïOxqÃðöüð-›Êþù''r¢’’kî¶à"rf(©!"""r²¬µèzÓ.na`ñ²ìO<tø*ßrv-ߎðmÓ‡v¡V|êwã¼úù¬Û˜‹Ûµ?ûbÀ§A(¹KÿÇä>æ»UØËo†YQÍcprL%ù¹;|¯åƒá,¥Ô àKx-_,X k}Ñå;Ù’ã:ÉÉDDDN=õÔ9Ìâ Ì|óvÝÑ»ÞaYf9¹¹Õµ±Xƒˆ 6`IQfŽ‚,ß[qº}çV°–U¿-àk_äé§*ÚLþ÷Ø­ü¯Òßø¶ÌàvXÌftmÛRs˜3ò!REàUšÎŽŠ¨(qœp9ˆˆˆÈi§J ©6HLÔºI©~ºWÅ›´ô¤Fqç­dʨùf—‰­é5Œ¸½¡GzÊ:ίÔ]˜Aq ym"MÏÈ 1p³õÛoÙânÈõo}Ì»C}5oNŸÀ]ͨ{É+|2é>ZûžxÌQÑÄÆ4 2¸¢ógùêy󛨽¢¸ð¿rU—†„z¶m+»ó¬9ñ'ÌOBåO‰]õì&"UK•"""DZ8é̯éV“К÷”÷Ÿ~‹EyØþfž¾¯/Ñ>GXbøæ 8(Î)Á ØÜEdVü¦ƒêaóòÙÿpfbx{ãåíK“sºRoV*{J2ȱ›à{²kM*«C¿Ÿãšh˜ålýôqFÎÉbǼùì¼èvš†µåÊÚr%ålýx#¿Ï xšÔRRCþ”´D=/D¤j©RCª¤¤d’ªàÅADD䤹³YøÎ»,Êß6·0ê~‡$4Lûn’¦¾ÉkïǦbo¢;6ÄÊS~bM¾ÇÞ¥üº ”VÍkáÞ˜o€],\ž‰ÙkW‘XˆS’Ð8”é*"'oSSÃrH{wé^VÎz“1ßçP¿ßâ}Ny"RC©ÊZªUjˆˆˆˆœ$wÖ¾_ï |Ë,FÝÿåþ#Â{<Ä£mf3uÞrì,§ Ù9Œê|½¿x‰s—0öž¥¨ÇñjvÆyƒµ—õdùì ¶|ô_îüÒ{q9`Ðð¢ÿœ@B¡”å/ÞÅëM¼;>Ê„ám9òŠ”Lþ7òæ{¹)+µs KFýÄó‰ñ†ò“xê_Ù[âçñϯjbÌ""rÖSRCDD¤†KHL$!1±ªÃ¿Ãµ—ùãÆ2á£Où¥¦$jbÌ""rÖSRCDDDäX\{ùfÄ\Ã\ÿt¶9ª: 9@I ‘.9)I½§N“ÒM³™³{ÿs~âËäBm·+""RMÔ˜ÝODDDþU´¥kõàÉgùW‹)B†’ŸšÏª¯~%£ãˆ²VœâÊ\ȇïÍ!eg:¹enÀJpLÜx+—¶ Þÿ ’“Ì¥3™øÙ¬Í°c†P\i³œó?æÓ¤5lÞ•C¹ F`®zê ®ˆö¢õ]~ËóP¾ù3^ø¿ïX›a‹¶âÍ”“úk);s(7 ¼½-˜%&á^¸ÒçóÊÓï3w]åf){’góO¿ÃyÀÁ¾Õ+Ù–QˆÝ´âãî¼­,ür?oÈÂîe³˜´%ŸòÚôÇXÿѳŒ™µŒ]ÅVƒ}pälæçŸgÜâ<<'3Xl¹ÅÁû¸ ÓXñåXÞý½b ‘ÓMI ‘JT¾-"Õ†*5ª;Ìc7à×áBb¸°½/CÒw)=ü×ã•À£ïNbòø{hç 8×±pK)&¥lœý3Ù€¥ñ ¼:iS&?ËaGžµîUc˜8ù#¦Oy’î¡v6ÿïK6:ú—ðÌÓøøÝGé”.gÆüÝ8¹v4|8–!+ª1lí‡óÁ”‰Œ¾"€¢5+Ùçwöb>YÔçêÑïóÞ»ïóæ&X°“üÝ2rÜ'³¥vFM™ÂûoŽæ…Ñ/óP`ÀÅÖ{Pë9´üD¤šèž˜Àâ¤äªCDD(ÝÌÜ_ó€`ºöoF€áK‹þ \¶ˆâå?RÔ†nÁÆ_.³ÅѪ¬Þí¡0· ËÁö=¯øÑçv"Êpza;ÎWK†— «+‹M[+ê#¢iìo`¡9çw ã§¹yd®ÛEéU­½ÐJãfµ`kÎ;¼‰lƒØËò(uƒco iìaæãC˜Yùú¼ÝäÛóN f“²í?0aÜ –e¸ Á^êP¥†ˆˆœJjˆTJhˆˆT'&Eë粬 ¤ço'©òaç¾_žK—ÞáG¸Ö ïýý$LÏþWûý¹—Ã}º>„ÕÛë@˘¬ÞXL³âϦgÒ!œÎt¥N¥þ– æÔ²ž@ÌÎÝÌ7eœp)»Ö¡tÅ f,/ÒÚ)9c´üDDD¤RO*füC v¯@Bà#,,”@/“-ó–y"9 ¯0Çø°çÇùl(ú5 ^µhÖ8€´¤_ØVjâ)ÜÈÏKóˆh½¿©çßãÙœ(Š)«s.W\w=7Üp×\Ò—þýÛîs1»òØ™ HÂ%—Ò¯gº5 ùûÁˆˆˆüªÔ©ŽÔS£JyrW2ƒð¥ëð7¸¿ÿþ“’ä·¸ïÕe”§ýÄoéý¹è¸£ùÓâÊK‰[ú9;²æòÒÝ ðõ5(/?‘H|izÙå4ýíc6ïù†gïøæÏ„—oWõk€¢¿}ÖÈž\sÎwŒý½ˆµÓžâöi6l†§éM§ãÞöböޤe}X™ZÌÂÑ#ØW #{÷Qç9T©!"""r7YË~d+€o;›Hh4íM{€t~Y´×QF©ÌÖàžxîVz·ˆÄ7åå.°Ѱ=­jû;&¯ºðè¨!ônŽ7`âKd« öÜ}œ~’û¦ZBè|×ó<2° qa6À‰Ó´O”Õy"1[#éwÿ½ h‰¯+‡[¶šgÅ¿v, ÍÃôÍ™ˆˆœƬÓ̓n¨ê8DDD¤›Qqq¤mÜxÊÆüõ—i›Ðñ”'"§FJò ÎëÕ§ªÃ©qf͘®J ‘jIËODDDDŽKI ‘jHBEDDά„‰U‚œ%5DDDDDDä_/yQRU‡ 'AI ©‘”Ô©ŽÔSCDDD临ۖˆˆH5tºzjdefœ†QEDDDª†’"""ÿ" ¢c«:©Än/gßÞÝU†ˆH¥¤†ˆˆHutš–ŸØíå§|L‘ª¢ž"""""""R#)©!"""""""5ÒIjtOL8Óˆˆˆœ5 ÃÀÔî'""""ÇtF’‹“’ÏÄ4""""§‡}ŸŒ¸•[ŸþŠ]ÎÓ1›ìEop÷àû·¬Ïé˜BDDä,¤F¡""""î,’Þ|©+s°àKíøv$^q=—´Çˑˮ 'vkù.ˆ±ýƒ¹ÌbVOÍ„å~\;ú z…Y%{Ó(tç‘–^ŠIÈ)¹­£ñ¤0sü$f¯ËðV\|û0®n…˜óñgÌ]º\§•Fݹú®›9¿Æ¡7AéÖï˜ðî,–§; n܇›ï½‘s"¼ l5¯Þý*«\ž”8’·îhŠÏi½+ù7RRCDD¤::M»ŸÈQ˜Nò÷å`Ç VÃF„Ø3رm _¼¶ÒçÇpc£N<ðò³ìµ4 ‘ß?ÌEþ®ÅUªÈ°{峌n_@X|]¬ÿtŠcqç°püX¾^ç$¼Q#ÌíëøúwˆzýQzÚJÙš²ktKZ³‡µÛ2ñ­Hš½tõ+=5šÅÉLzåS–—Û8ŒÝ[ðöáÄ< õ\¥”¸€ðtnŒ !MƒOï=‰ˆÈ¿–’"""Õ(¥Qb¹|ø3ô w²õãGù}+Vermý¦>ó< ½úòÜØ[hT¾†™ã&2g].Ãz=îá™;òä‘ ˜ÞϾÁ»^åñSiÄu<‡îçu§Yðadž"¶¯ßE®=„øVñ„§òûÜEl³ÅÑ,ÊJá–¹¼÷ÙfÊ9úœe¥{ٺυ#m å¦Èœ™›ÈBÇlµÒ(Ž` csN ¼Œ ~ž>‰ñ¯c~DôèC#y[wPì)`ÛŽ\2·ìÃ-#±D7­ d³5ÝŽ§8›b û÷©¼5á~HÉ¡ ˆˆœ&ªÔ©Ž´ü¤Š¤2ù[˜|àá=¸úœ¬äW:ÇÄítB¢Ûsá•]ˆ ´‚3E v`Z›sëCÃèâ›Ê'O¾ÌÏ+þ`Ï͈÷>|®zô½u(çïï©‘³öá„vææÿ^Ÿ gäüZßô_nð;Oßÿ!;R·S`<êœûn¼žáÏ„²ÃÚ„„Ð?Ó2G  8À»âÛ-›6 ¤ÔQQäÊbù¼ŸIvXñ58Lš ~ŠG;äÙ6†üÉÀ›@o ``ó³&öR'¦5”¦-âÙ¾ƒ‹6±bÑ’oÃ#çGh ŠˆˆœrJjˆˆˆˆäMÄ&”$¯#‹Z ¸ÿ:…ZØß=ôà9qß@5X4w<ÏýŒŽƒæÞžE¤î|ðß»ùààé¹”ü£íL¬Eù”;—¡¯&péªï™üÎW¬øøc’;_GxPÞ€‹ï½¶Aû«#l¡ÄR¥aÁÛ PNA™ÂN`5ðþ¡*ŠwŒƒÆpŒ9änJf›µ)‡¬’°ÕiJ–“¶eî8ؾ" a“l¦‰i†ßðF´Œ bÖŽrJ<˜¥»INÉ%*¡5µGa™»´õ8Û†‘¶9¨Mã¨Êû›X ªG¸YÎ2œP©†ˆˆœjJjˆˆˆˆTfxQ·ÏÍ\øÃ3|·ýK>Kî΃m*Ÿ`góG/1uO-ê;Éw†‹w}ÎëY9_íföÄ©lН…¥¤ÿ¾1¼Yåñýˆj›÷1ëõ1lŽð¡Á•÷pÁÉÄj;úœ„ÿÂKÏMa‡Ñœ{Æ=I÷ýKP¬µ;Ó¯é >Üåså°rÖd¦/Hf_)Xƒжß`†^Ù’@ãh¹Éþõ]Æ|¼¨¡¯ñp;¿¿œáÜóc_ÿ–ô6Ã}k|Ó‘Ë® 'vkù.ˆ±®›2qd,çËi3Y°j/¥øS¯Ó%ÜyçÅ4 àÇ»aW|<ž) ¶Rh‹¢ãÀ»¸û¢&à)HaæøIÌ^—á­¸øöa\Ý6Dß ‰œ%»&´D}X¤úÑÿgDDDDr“ñãÿñú7Éìs„ß:æ>R lØŽšÐ0±g¥²§¨§çÈé(Oi:Û3Š(qýyÜêÄ/?Ëó£ï¤Í_ó §ŽYÆÆ/'ñͪLâ[ZÊÞå3xë«TœÇ:vÈ .ö|7–·æo¥´Ncb½ÓYñékLN.Ætç°püX¾^—Ch£F„ä¬ãë7ÞaQŽû4Þ”ˆœIJhHu¥J ‘jÈÇÏo_ߪã_ÈÁ¾5i˜Xisß‹<Þ)L¥N>8ÉøíÞÿ|²Ç'rÃÐéQ÷À÷DnR^»“ëºW½Â˜+êýåa«(é9nMâïä'"ùì™çYèÕ—çÆÞLÔ¦é¼1ùW¶dãÆFD›$xoá÷•i{GÑùš{zAC| “ò]?3eâWü¾-3¼%n¹›ABÈ^ðOOÝE‹»žãþó±þ´¾i]A놸S?åá'ç½= »wÔcå¥eLù I¶ yꉮ,]° ¼Z3ôÙtÊÁÃOÎfÉÜõÜT7—Ö9!ö&F>ÛsþHœ¶Ž¹+r9¯DE """§*5DDDª!‡ÃÓn¯ê0þ…lÔi ¸YóÞ«¼?{9»J¬ø{W”iØ·Ídôøùl(Ž¢}ûÛ~âݱ³I«TÒج ÝÏíI׆þG~Ð oE×sÏ%±CÞ‡0±gndcF1–ú-iY²ÖüÄü…ÔiG°=eÓ&’”åÆ,^Ãä—>dá67qÚR§`=³ßÏ/Ùvò¶î ØSÀ¶…T®“°ÆÑ¶a œ¤§¬%¨Ûº>Ç8f+ÝËÖ}.i[ÈÈßÃæ ¢±þ¶:Í©o÷Þm¤§o"iO°ÕJH£8‚ŒÍY¨UˆˆˆœNªÔ©Ž<<OUGñ/äE½24k€³‡i%¨v E¹ÓCIV. wÞ‹ ›÷ç5%¹¥˜þ HèÒà#zÈ_ñ!£ÆÿF~pgî~ìZZøÇ9f£VÓNÔÌB¿Š$‰½‡ð8)søøøã 8Jxœe8ŸoŽÙŠDDDäRRCDDD¤Oy>¥^¡ÅÒå²Á¯XÁÄ9ìÊ7ˆó\4¿ú®hì[ñÂnøÑÀ6ynJ‹˜øÿåeÞ°ÚðŠ K9±ˆQyœJú…bþ3¬_½ýÕüê†c”îfUJ.Q m¨ëûçEî¬$ƽ½ß¶ 9Œžul'pÌIî¦d¶Y›Ò!¦kÁæ¬ ì,½”Ú¹ØãkÝx"£B¨ÃrÒ¶l£ÀÛ·S4lÁiÛÐEDD%5DDDD*q°ý³§ù“›¨úáø93Ù±°FÓ<"€F}»úÇÏlœ9‘iÍb 6‹) È“-½°ÆFãÇn¶Ny™Ñ¿‡áßòzî¹8úàK½-<ž(+¬_ù/‰#0¸·Ýy1µèGß ¬X>²›ÓÀÏI‘#Ž[žŒgóäxuQ1µ<Ï7Åí_‚âdïÂ9¬w‚5ÜÃÚ™XXü›qùÍçÃQŽ]z±…÷Ÿ›Â£9÷ŒA¯Äz|÷åZÞ9’z¥;ÈÆŸný[QN¿¦3øpó4F\„™º ¼ZÒ¿c-5 ‘ÓJBEDDD0ÝXë¶¢i¸‹ô];ر¯”Àúí¹ü¡¡t³â×ü&žÚµìÞ´ŽõÛrÁæÄi‚_‹AÜ~~CÊw³võFvgZ‘a„tâ–›»QÏ·ˆí))lIÏ¡ô$Û¦!úäÍôŒ¡(uk7ì¤À°àv[Åß&®ap¥„‚›‚½?¥¯eé’%,Y²„߯#Ï}ôcÞõˆ´b«O¤¯èK‡3ôü8ü²v挤àÿ2¤}†5‚óš‡RšJQXK.yh=•Ò‘Ó˘5cº9pÐ U‡ˆˆˆTŽ·ŸÙ»wŸ²1ýåGÚ&t×ßÁuÝê`;ø™³ñ›73s#q¼ÇÈ.§ù~DDäl¥¤†ˆˆHu¤ž§”% š–M³)ô8Èݶ‹\B£ã©íc!06èô–®Z#è3âeâ²hëgk:Û3Š0[ù÷ë)=ÂñÃÆ8LgÉs¦3õ‹ådš@øelür߬*'"¾‘9ر|oE´fìq&,0)ذŽ,ÿ8ZG;ؾn?Œ{˜¦O’X J¶/äó©Ó™¿µì´Þ‡ˆˆü;(©!"""g=ñßg.w6óž~);ëqÁOsY]/<ù+ùôå ,Ú°‡Bø4ºšçkǯ½Ê·› pa!$>‘﹉î‘&Û¿xƒñ?m%³ 7¾Ôíp)wÞu Í <…k˜9n"sÖåà2ü¨×ãž¹³)[Þzœ×ÖÕeÐk/1`LEIÏqk'ïŒêI¨qḣïÄöƒc¼HŸì¼1ùW¶dãÆFD›$xoá÷•i{GÑùš{zAC| 0‹71{ÒTæ¬ØE¡Wí/½»/mOÚ—]š†O³Ž´ JgÕÌ×™¸²{ÖVvÚÝdnÝM‰§Òðþ͹ñéWxÿ•›iyX`–À8Ú6 Ä‚“ô”µdu[×ÃÇ]Èέ¹xJSÙšçÆâå"õûxÿíWyw™#¶=êÛ+‰÷3æ­q<Ö/ò¤g"""¨RCDD¤2 SËOÎ([Ëky°Roÿ<ü4ÿ1Ý8‹×1aø+ü¾w;¹®6ûÖ¥÷ùØ2Ÿ'†O'mãŠ<õq;]€…èö\xeb­ðg­Ã!‚Ú äî;šþ¥§Æ‘yŒØKïáñÖkxþž l®ÛŸa  ðÃxî—}lÌtÒߺ’RPÿº¯µ²àÙ§¿"eÑN¼îʨ‡7bM ò^„ÆÔƒòœ£Dæ"û·=c'D^Àíêc³Áe=Aý´jé ä³û÷y$m«¸Â×ÇÄî2ÁÛÀâE´¿›}G]DDäïPRCDDDä0žüd>7‰ï7äqp„ Ïay&kPÎ2œ¦7qß@5X4w<ÏýŒŽƒæÞþµOk¬ßPB}g)ÓJPí@ ‹r§‰»0ƒB€=ŸóÄÐÏÿ¼¨ ‡%ˆ¸ÿælòW|Ȩñ¿‘Ü™»»–þ¥(>‘-ér°ø"”^ÏM£{i&)_Œáµ¹_1ñ§îŒ¾8ŠÓÝâTDDþ]”Ô9„“´ï§0{C /Ê5-Jùñ©¬8⹯ôÙkísúj—®úžÉï|ÅŠ?&ùÜ*5ÑÃjà (.,åH%Žwüˆ1TÞøàrK@8@Nx"wßÖZû3 –À=…l_µ‘ò†íiQÛÆÑVÉTæÎJbÜÛ ÉñmË‘ÃèYçÀ™Ø3Ö±jOmbð7 ÃÀË¿MZÔÅ67“’ÜŠûQRCDDN%%5DDä´JHL 9)©Jã¨q´ûI•2¬à!{ûzÖš.²ípÔu"ÙÙüÑKLÝS‹zÁNò€aÁrX¶ÀO”Ö¯|—ÆÄÜÛîìI„õXÇ;Ô}XÃ;лé§LٜĤwÑ$Ò{‘A‡¡³êMž›‚«Áu¼ñÒED7ÛàdïÂ9¬w‚5ÜÃÚ™XXü›qù Íùýå—ù_¦]¼ ó£édEESË«ˆÔ”m8 ¤Uû¨C’;"""§‚…ŠˆˆTC•¿x—3ÍFƒ nå²6upnø…ï¾[ÌNkÑMêp¬''Ó‰Ãã&{ý2ÿ–LF`½nL»ÀC³FH'n¹¹õ|‹Øž’–ôJ='~üo±Ö¡ÏC#Ô%Ÿœí¬_·‘Ý¥¦¼k7"ÚÛBíøãÜ×An öTü”¾–¥K–°dÉ~_¼Ž<3ˆ˜Fa¾ ‰‚°Úéë“Y™²âð&$Þ:‚[ZûŸP5ˆˆˆÈßaÌš1Ý8膪ŽCDDD* ‹Œà /=ý”ùë/?Ò6¡ã)ODN”äœ×«OU‡!"RãÌš1]•"""Õ’–Ÿˆˆˆˆ—’Õ\÷Ä„ªADDDDDD¤ZRR£š[œ”\Õ!ˆˆˆˆˆˆˆTKJjˆˆˆTC†a`jù‰ˆˆˆÈ1)©!"""""""5’WU """gNVfFU‡ """rÊ(©!""ò/Ò :¶ªC‘JìöröíÝ]ÕaˆˆÔXJjˆˆˆTC§«§†Ý^~ÊÇ©*ê©!"""""""5’’"""""""R#)©!""RhKW‘cRRCDDDDDŽ(±kBU‡ "rLJjˆˆˆTC : ©jIK’«:‘cRRCDDDDDDDj$%5DDDª#õÔcr²wþ»¼ôίd»«:‘ª£¤†ˆˆˆÔH®‚TV/ß@Þ¿ò¥ÞCá¶dÖ¦æãªêPDDDªWU ""òO%&V4²KJ:{Ö~Ÿ =5ÌòÝüþõÌY”ÂŽ<àKíø6ô¸ü&®ìP ë?ÝMÞï3æ³{·aÿl°£LQÀêYï2黵d;}©×ùJ†Þy!ñþÆ_Ï-^Êsw½ÅÆ£y%¯Œ¹’¶Ó§ˆˆÈ¿˜’"""ÕQ _~b¯çãç^âû=á´½àJîl‰Ÿ3]7Pà²p„´@5cRœ2…·¾Íçü^¤wÈVf¼6™wæµcÌåõÿúåל›ŸzŒ"7àÎæ—÷&òGä ‡7`ñ$BO]"""§œþ÷*""5ÞÙT¡qV0ËØ8c<ßï‰æŠQ#¹ª±ßÁ$F×ýÿ<ϙŲY“ùt~ éå‚b»rÙ[¸°qNöΛÀøïÖ±;«'` ‰£ûÀ;¸¥w ¾t¬àåÛn¬ø9ü"^zý:êg|Ï„‰ X³=ƒb7X£ú1rôÍ41Ž5ßá<¥íÁÑ…^íb©çF×úÓØ’_Ž›#<@Yƒ‰mÑzÿ}íf“/x…ÆÑ²uk|Üù¬üôU>ÿm#»òì@mî|‰G{XX2þ&þ‘A9àÖ˜žƒîfp(¼+>LÊÓòÉ”¯X¸1¾DvÌ÷v=,Ü|–½÷$c“›rÏK÷Ò=üt”®ˆˆˆT?JjˆˆˆÈ©U¶‰ï~-À¯Û]\ïwäª ³” Ó_`ì¢.½íI:…±zÖ|O![–®&#j ÷ÝÙŒ`OftV‹‡øžƒ¸¯oA–"6Ï›ÂÇï½Kló‘ô‹°âÎYÌ[Ï~À†ý¹ñ¡Dy“ãˆ!Ä 9Ç.cëÿ^å­¥u¸îÙ»9W ùQRCDDDN)Wþ.ö9 nëVTÆ,LfÖOù´¼sƒÎ ÅânÏcÙC3øy{9íÛîïeGBû¶DY¡mó v¬z™•+3¸.¾^Åq«?ubbˆñýsl'a´è˜@«àŠÌ‚ߎ3Ÿß_bô‰»+›ÿÌÄ—îe^Hn|ì1.ŒñþGŸL;:¶mDåQ"Ûu!rÿÏ"òYüÇ4’÷:èáÅîù³H¦#Ãÿ{‚*˜öýÿt6ïMÞýÖÍ…?Ìű>5`iˆÔd‰‰ ª”jEI ‘ÿgï¾££ªÖ7ŽÏL&=! )¤$”ÐQ@‚()*(*È»X¸^õ¢"WEÀ†E¥7‘Ž4ƒ"Ò ½÷ J !m23¿?¸  $ç³k‘™3û¼sÆ%sžìýn‘rÈ0 ¶§†£°É©Qì vÞÑíüe³‘6âº8÷9KjÜÏ‘Åpؘš…ý+ºøùÎQâÈÝÇìßgž{^쵇ï¿ßÍ¡ +v¬žó cNÝÇ #ÿÙ¬³ÙRY7}4“—lâÀ±,pwÁJ>q68²Ø¿å(„u$ú¼™$…Ngè8Ý>¤kmO"rÅ)ÐòF¡†ˆˆˆ”*³wN°kÛ!rÛøâZÔAÜhòäkt‰<{Þ‚‹¯7FQ•&Ì&pØÿAØsÑóÍÆ±Ä‘LØK¿á·SÏÝF„eokïGñ]¼‰ì»åò72±qtñÇ|0õ8ͺÿÔöÇ’¶‚Ï>˜qºè3áP±|êÓ¿Oú’Y±ÿ¢C5W""r]1•u"""rm1^Df¼£nçåf5ÏÉä]Ÿ‡Ÿ½Æ1bq¹X5k2ÛÏŒaãÄïŸñÎw› iÛá8¼ŽŸ'í¼ð©nãåç[ãoÃÕ/ãàß0S)öVz¼Ð_Ç×ÏdäÔÏ=ŒWšzcXÿböûw(†»zô¥n€éëÆòùœÉë¬PCDD¤Q¨!""R9ìvv{Y—ñÏÙRX:{+-ûSËÝ„s;ié=€¹¿$Ó¶kòØ7.»Ýn _߈÷0€ºT³® ñ۳Ƙ±ŽüÚ}xùÑ–øš€ú1¸¬[ÄÖÌ œÛÙ—ðpªœ^wr^GQ÷ð4 /ø):‹ýK_eñÆòšzãØ9ƒ»Ü¹éÕ¾"""R*jˆˆˆ”C½§†5y Òö™œ\"¹µM -d÷==©é’ÅÁÝiRŸH÷bf>䥰-‚ZFá]ª]Àò8²b*?ü´Œ­SÉ1»ãœN1ù€Ô»É´DÑ,°;P‡ IDATª БòK¡†ˆÈE$4‹'qERY—!Rä°{ÁŽ:2˜ÜïQ&Ÿó\ó¶>@L}£00pP|tSpŒÃ~¡c.uÿ <S›Çx®g>üł៱¢°»Í†³Ú©‹ˆˆ”{ 5Š 9›þ riYÛùyy¡÷¼LŸ&•þ·Õš=å#†0oÞ2ê7"¬feX°‚-é-¹Á§ˆ9ÎAÄßk6râîRض É=´…CT§O§6Ô÷1ÀæF¨ÇécÌøDcÉÛËú¿ò¨WÝùrO*"""W~QÝÀˆˆH™3 ¨ËOœÜ8ŸµÙáÜܺ.Q‘‘DžþU„„0¬çógª™ðÛî§¾‘Ä烾föòõlÚ´Ž¤éÿÊH‹.-ñÚýÿýô'–&mbÓú$¶·]ZI† ^.pbÛVfÚp ŠÆŸ=Ìžö IÛw³gÏ>ŽdŸ9÷ÚwÑÚ÷8s†ŬÙ¼n)³ìæôYíi+þdžýj#§²·ŒâùGú0dY*vÛü÷ é7‘=çõò‘Ò¦™"""Rz鬟¿¼ðièÿ÷©f·¢êøñÌ_uŒV·µâù&&žÆŸüJ6`¸ú^/_3€wƒž¼Ó×&ÏcÄSp.•B¨@‰çPXB¹¹S+Ö~7†¡Ó¢ù û½ô}$‘?}Ïà…Q…³g 1Áî˜Ã-†n¯?‰éÛiLÿd§L>D„˜0¹hæoáS… £DDD*&cêıŽû»v+ë:DD® ñ$&jF˜\XXL )—}ñƒKhé’EÔ‹oTjã]?ìœøåMžùÁ—~Ÿ¿H¼[Y×#ךõIkhÙºmY—!"RáL8V35DD®6R"vùÉ5 ÿ(«ýIvå`ü½œÈ9¼ž9ãöàÞà>¢\˺89›B ‘³å§±cÅ\ï:Ê©|À-Ú7öäà¥=^EDDÊ…"""RªêÖý¢ÇŒ;æ*Tò¹ÖàÁþCy°¬ë‘‹R¨!""Rœ×~²Â(×…ˆˆˆ\S´¥«ˆˆHy¤ž""WDB³ø².ADJ‘B ¹n$®PÃn‘k‰B ©jˆT-4MR亣å'""""¥PC¤ø=QÓ$E®7¹Q¨ˆˆˆÈÕ¢PCDDDDDDD*$mé*"""×.Û1þ;†¤®ÝGàvÃk|öL,¦Ý“øÁvfäî„4¼“Ç{w ÖË„#sc‡fÙîC¤å8W‚ê·£Ç÷ï{zþ€•”•Søvâ"ÖÎg?êÞß—ï Ãb=ʪ©£¿`=‡sLxE4ãžÇåŽhΞ,br¥f\(öÔcxNᵈ«ãuæ8ûÉmÌþ~4³Vì%ÃîBPÝ›éúXgš)î‘+K¡†ˆˆˆ\vÒ×Îd5MèÛ¢&‘~ ™¾Ã{×Â9÷›7¢R‡'éëIþ±Ì3ƒF3ìÕVø™œ\÷-o_†×Mðü£á¸fŸ +À'G[Ædèotèù:+ŸdÝÔ¯óþ(‚‡>MÏKXc=ȬA™O§'»ãr„åSÆðñ[™¸îM¼W9[O#""×…"""Rúl),½–ý©ån¹δôÀÜ_’iÛ5Ë™}¨Ù žzÞԉ´åY>[¾…ôö!øe®gÊχ è0ç3`=Æ´o¿ŒÂ ÜÃФpvDt„û—¾Êâ)ä5õ.<Æ—Úâ‰ó¾Ä›qêµáÏ¿8˜i'¶ÒÙOÚÉ:‘‰Ã5’Úõjåm@õ¿½Þµ2aááœ}Z³w 7*ü!º*–ÍKx{û6Nä×¢ c_׈£jözúNXÆžìVø¹cÙ¤ß8Ó“wzµÁÿ¬Å‘¾Œ©‹Óˆíý6]oôÁ"{¥²ªïD~ÙCƒzn%~Û9Ûg0co%néÿ4kºP;ÂÄþF1ùŽÔm§Fš""rå(Ô)‡ ÃÀQ—ŸX“—°à@ mŸ‰(è·àÉ­m‚X¸h!»ïéIá½ï¹ 7C¼`c*Y6ðJÙÂ~«7 „”bφ<ެ˜Ê?-cëÁTrÌî8çSL~é _ìgf!´Í½4Yò5õ}&·ÜÆíZP³²…â£9û—0nÌVï:LšÕ‚›‘ ¾¹äy3Þ¡Xli¤çØÁtˆ!èÞ³š—Ê;º¿l6ÒFfn:ŒU¡†ˆˆ\A 5DDD¤”å°{ÁŽ:2˜ÜïQ&Ÿó\ó¶>@L¼G‘¯49™ÀnÇ86ì˜p*vzÃl{>öæ?Öý3üñLm㹞Qøð †ÆŠ¿· pœbÿæpoB¨çùE;¶æ…aqlYú33¦ÀY3¹¥ß[s9²s33Mäfe߆eÌ_²œ˜Îüçþꜿq­´ ‹Y™Y…jA8Ò¶°+ܪyàd8S%6ÆÎaÒ¢n Ì%%3Œ–‰ð°²jú ~35$ÔÓÎã¹%/ÑH«ûš0ý“Q úê÷ÝI%ÇIŽÙªÓ¢Q:&øòîÌfêDÛÚXrŽ’œF뛣ñ¸„©®1÷pwµåLþ>Ýo%Æ5…å“G³Ó«%ýnôÇŒ•}SÞ¤ÿ\7øons[Ë'ÿþŒ ^aÐq˜¶~Ç«ï­ ¼Ï{ô½Ñ—b'戈ˆA¡†ˆH9§@ã:UQ·tu¤³~þò¤¡ÿßçV˜ lÜŠªãÇ3Õ1\d,Ûƽ_ãq×ï™1a(‹sœñ¯˜ ×T>Mù¿—;ñÕ¨Ù|3ôg\|Bˆm„‹ 0iùÀm¬ür?ÌoʇÞKßGÒùÓ÷ ^hÀÙ3˜`wL€­oÑpö¦’ék¾û5f‚"ciÕó?´o}~ÿŠv2÷'1{ê:Žæ†¡ñ÷ñ\·Z¸b&¨Ítßö“¿ÿˆßq¦JóÇ©Ã<Ü·39£æ0bÐÌ‚s»xR/×Ýù›ðiö$l“ønêt>[šSÐÌ4ái7ªOoòR¥ÑLZ4Š!Óm`ö¢ê ÝišÇ¥4Á°„q÷«o`þ~43?̇ uÚñì ÏÝùÄQ0[à+øIDDäŸ2¦N븿k·²®CDDDÎY·.{7mÂa·—Ú˜K—,¢^|£‹XŽY“§Òï_‹©ûöP.½ö¡"%fÝ˸—ú³îöx÷ΠRi‚º>i -[·-…‘DD®/S'ŽÕL )¯¬üµt›LU®ì)ó+~šÅ‘Ê <¦@CÊ’ƒüì RÓ\qqóÂËE‹fDDÊŠB ‘ò¨¢.?)Mö,o_Îôeû8‘m“'!uoáÙ§:S£¨-aE®‡~|›ç~„Ð>dÐÝE5²‘«A¡†ˆˆˆ”‰‡ºu¿àóãÆŽ¡áãhøøU*H¤$,Õxèã1B¦ ÌUn¥ÿ{=¨a¹øùÿõ)“ÖìãHF`Ƨfînjacâïl8A¾KñwõâÉ{kãUÌâá _ƒ\.Ï7“—°õ¸³O47uéÅ#7…áb€5y._U»q”USG1~Ázç˜ðŠhÆ=?ÊÑ=1åüÏõöÙûûÝ4~Û~«¥21­îã±n­‰p½Î§·ˆˆÈeS¨!""RÞTðå'Ÿp*³Œõ‰I¤D6%й¨WOš?ù ÷†[À䂟Ÿƒ`šß߇|*aÉÙǯcFñýÇÔ~¿3á©›V°9#–î/´¥š{>9–HÜŒô‚sVmI·»kãc?ÊÚi£ÿÁ÷Töññç7¼õér|Û>ÌKMÉÙö3£Ç¾ÃûöAô¿+‹ýûÖoåDXgžë‰kîÖÏŸÌØwörjà;t‰,z)…ç Oò¯U±`àì€}çŸ,ßa£M¯—¸¡Š…¼\O‚%:ÿþ;H«Ú…¾¢qÉÜμo§0z{7=ø0}Ã]H]3™o¦~„¸¡ÜüX]¼.ç®ûÎŘƒhy_#¦:šAŸçбe¾¦SIÎ¥æ­7VtTŒJ è˜àË»3?d˜©mk`É9JrF­oŽÆão×ÅɯˆÏ5®qíî£î‚‘|6l=wú:fü°êÿ·†ZÀv„ƒÞ`tú-¼=° Uv}Ç«ï­ ¼Ï{ô½Ñ“SÞ¤ÿ\7øoîÖWW9—þe‘RäÀfxãž¶ßÌ$Í àJ@f<øÚCÜdjÓ­w;ŽšÉgïÏÀðŽæî—o óÏp×Wc™þñ»L0»áÞ_Ëå¤ Þ zòö3^|;qCåaò®Fónoðèí!çÌ1Y2Húñ3¦§Ûqö¯ÍíO÷ä8÷ËœIPòó_9&|›?Åûd¾ÿq6_,ÏŸê ô¹©aEîNÔéñ&/UͤE£2Ýf/ªÞЦ Ñxü}²†[ÑŸk½¨Ö¼ð¦1ßMã›f“oñ#¦e/Þê^¸ó‰¿íöSÄšG —݈ˆÈuǘ:q¬ãþ®Ýʺ)d¶X«Qƒ}›7—ê¸K—,¢^|£Róš`ÝÏÄW^ã׆úHuŠ™¸ rŬOZCËÖm˺ ‘ gêıê©!""RÞ¨¢ˆˆˆHI(Ô‘ I=5DDDÊÃø[Šå¡nÝ‹}nÜØ1W±’²„Óuغ–u"""rÉjˆˆˆH©*—Á…ˆ\u-âù=1©¬Ë‘kœ–Ÿˆˆˆ”3ê©!"×"r5(Ô)o*øò‘«E¡†H’Ð,¾¬K)7jˆT ‰+4SDDD®}úEŽˆ””B )Wô‹))…"""匓³3f‹¥¬Ë)÷jˆˆˆ”3v› ›ÕZÖe\3™8üc¦íÍ+ëRDDD¤”)Ô)gv;í~@~ú^Ö­ÞBªí^”·“ï_|ŒçFn!°gïcÍÊõ$Ÿ²_©2EDD¤Œ8•u"""ò7}KWG: éÇ'ž*|ÀÀÅ·*1õšsË]íhâŠQ¢l¤.ÿŠ÷'ðêˆÚøšKx~à ÿPBüÝ)éKDDD¤bR¨!""RÎ@Ž4Àa#;í„ÞËKÅájÍ"õ¯¬^<¡¯,¦Õ³ÿ¡wóÊW.p°„Òþå´/üñR&yˆˆˆHÅ¢PCDDD® ϪԬUO¨×ˆ–mÛðËÐ×ùú‹¯‰‹éÇM~&pärpéx¾™¼„­Ç­˜}¢¹©K/¹) —ÓÓ9òÖ0¨g÷‚¿WnÏ»C$º‰oÞNbrvLxE4§cïÇhWÍ ÃºŸ‰¯¼Æ¯ 0ô‘êç…'޼C,ó–ì 5,>µ¹ï_ý¸7\ÍYEDD*…"""åME_~RK-ºƒÿšÆÜUÇhq›?™«¿â­/¶R³ËSôóàèãùú«p Ì#Ñ…_S̵yü‡‰qÃR‰*N`˜‚i~nð©„%g¿ŽÅ÷Pûý΄_°Ç–|οظýÿ^çÆ3'SRñòÓW"‘ŠHÿ‚‹ˆˆÈUcñ¯I„ lÚ}œ|›ƒ¥SWàhÞ§:ÔÃÝ€Z=9°ê –.Mæèj/2»N¸ëY™ý¨ÓÔ¯ð‡(ÂXÍ’!Ù{ªáªÀNÖ‰L®‘Ô®W‹(oª_‘·*"""WB ¹ªÎLBÉ;–Ãu`0½–Ÿ{Œ9åä{aØ3·3oÌ$í%å¤76"ȳ]l†‹…Ð6÷ÒdÉ×|Ô÷5šÜrw´kAÍÊ–6/‘òD¡†ˆˆˆ\5Ö”-ìËÿê•qâÀ¯m_^¹­Ê9_JLnþ¸™E âÈ`Í—ï3f[M:=Þ!®œÚðïË+Q N­yaX[–þÌŒéß0`ÖLné÷Õ÷R°!""RÁ(Ô)g ÃÀq‰=5šÆ¸2éJ”T:¬‡ùuÜM;?Ƀ5¼ÈOÍÀ#À…Ë^ßO—8¥mböè|4"˜a¯¶Âïç­šýСW,^•\±^Ť‘S>¾Ã{×¹ð«%š»Ÿ¸‡`K +§gä€L܇gç)Nžê©!""r‰jˆˆˆ”3½§†#k;?/Ï ôž—éÓ¤Òÿ¶Z³§±|ÄæÍÛ@FýfXÜ-`Í";ÿÌ?`*úñüãlÛ—K¥æiß< ç¤àPƒ`Ó"6mHÁV‚ÙÿŒ-};3,Ôís/­âÜ+ö@7¸@¨‘l¦@è]Up6Ü ­î ¿ì$Ýë>â|Îï qzç[{Eþ@DDä*Q¨!""RÞTèå'NnœÏÚìpº¶®KTÐÙ»‹ØpMcæ„ùü™Ú”a1ø9æñã”%¸4ò&3ÅF›šàWÔã­bˆ ±0gÅtæÅÝA\€yÉéäŽlTjÈýmüøï”Á|lïDBŒæ¬d7¥é%öÌ(Ë‘íY—ñ¿¯J†k5ª†áaeÕôüfjH¨§Çÿ¾U­•”ͲÞäíÄ6~™ü#û|Ûðïf~˜0¨vû]Ä,Í—|CÚ]M÷´“qøæú·Ð,Ð “«žF;–­fOÄ Dz]ú-"""× …"""Rz鬟¿¼ðièÿ÷›q3[Quüxæ¯:F«[;ðä½øröH/÷ª7ól³ÆEýxÓ'žá®¯Æ2ýãw™`vÃ'¼ ¾ wây“—½¾gÂü¯ò£\‚höX,cÿÉ9Åš>bÍÙùÜ€¡ðpßÎ䌚ÈA30\¼ ©ˆ« —*ÄÆVaé¼Ï40yÞð>^yäêzÌÊ0µ£ß[fÆŽ™ÅäOÉ\+׿ö¨64 tÂðnÀcùxúüج!/Õwÿ'o@DDäº`L8Öq×ne]‡ˆˆˆòòóÃÕã”ê¸K—,¢^|£‹("WÕú¤5´lݶ¬Ë)šÅ“¸¢d½§NË%îÌ."""""""re”4Ð8M¡†ˆˆH9cŽ ÛSCDDDäêQO ‘³<Ô­{±Ï;æ*V""""£PCäZ$Äó{â¥M‘ŠMÁ…ˆˆHÅ¡å'" @CDÊD…ÞÒUDDDäêQ¨!"""""""’B ‘rÆ4OCDDDäâjˆˆˆˆˆˆˆH…¤PCDD¤¼QO ¹Jâʺ‘ËrÅCø„ýÏRDDDD¤JJL,ëDD.‹fjˆˆˆH9á ÷À¯L»€yÅ“OÊò3k;Yåd2‹õÈ2&™ÍΜ²®äÚàÈÞË¢ñc™Ÿl-ëRDD¤¸â¡FRb¢`‘K`ŽëtùIÖî…LŸ¿Žã¶âްòׯә³ò0ççv²’7²jC WóvØz0‘Ÿæ®æH^9ùÌòvòý‹ñÜÈ-d—u-gØ8¶d½zdþ‘b?\Y;Y4k›R/|\q¬gðúcO1tEú…î:r8²u-IÉYjÌ+"Ri¦†ˆˆˆ\1öÔ¥ü·GwúÏ|RþÙ=ê%ÈaÓwƒ:uÙ¥q—jÏdç/£üï§éÑ­;u{Œ§_Êø¥ûËÍ,‘"nø‡„âÔ·²ob_zä=Vf^ü"d®ÄÃÝú1ógï*„†áãl\v ¦¼ÄC=?ac³cLξ‡èítáar÷0eÈGŒý3ûeV$""eç"ÿ·ù§¬\ô›˜vÏdÎîÖsyC_æ›íGÈ1¼ˆhv=»ƒZž¦Â÷9ƒ¯GÍaÍ_Y`¸Ü¢oöi„·¶ôMÌün sV w‚ÜN^÷PÏÇ ¶4ÖNüšI˶²?5ð N—6œ˜4—ÊÏÊ«M½0r¶òÅóÙvë ßcçÇW^ã׆úHu,™;|4Ëv"-ǸT¿=ž¸Ÿxß¹¶t6ÎþqsW±7Ãfwü«Ö¢mïg¸'Âùœ«ã\› ’Ø~ G¤3—¼”%[râȯ$ßI”sÁg—¼ó8ÇRŲ־ËC¬<5¢?-¼Å^OòHúò%MM'¯¨z/À~|!o<7Ž€W?§o]WlÇ×0ñ«1,Øx”\LxV»•—þó05Or^æá ¯õü—ôoêqÑsˆ\ÏhHy£PCä¤@C¤‚»&¶tµ“¾v&«iBß5‰ôkÈäa³Xv´1·ܘæšËàw'q¸V{žx1ŸÜ¬š5™ígưqâ÷Ïxç»Í„´íÆ ‚p^ÇÏ“v^øÔU»ðÆ“ñx`ö Â~Ã[Ÿ.Ç·íüÔ4œm?3zì;¼oDÿ»B΄¥§“4/‰\ÿö<Ø*èÜç WªßÞ•¦s>äŸwÐ=¶~áV¡ <Ú¾*né™=n<ÿbáÃ7ÚdÝÆ¸áSØ^«¯ô®…—õ8GrÃq7€¼ýüôÞûL7ÝÌc¯ö$4;³GŽcÈçþ|ôêMT¶g°cå:ŽT¹Ÿg{×ÄÛž£Š/ÏbÑêä6ŰIbs¦ báð9—Ñ{„Í›Q©Ã“ôŽõ$ÿØF挙ÁG#‚öj+üŒvNz‡wgåаco:×ôÁ~d£¾ý-'òÏ 5̾ÑD{ÁʇȽÉW¬üci>x§­f鉊r†¼Clü üªáeâÜ>'9¸&…ßO—8¥mböè³ê½”ÅÓŽ Ö|ó)³ŽÜH¯Wo¦šk6G™ ²p¦ ÿÛ^¤o‚?fL¸¸]Âà""R(Ô)g ¨ø m),½–ý©ån¹δôÀÜ_’iÛ5 yì›?—Ýn7ЯïÄ{@]ªYWøíYcÌXG~í>¼ühK|M@ý\Ö-bkæÎíZ™°ððÿͱ¥°lÊïdÆôdà£mð7ub¨œöfÎ`ç-}¨ízÖëó³ûX"c n~ÿÆp«J*°òÐÒmõ©@5îèr-¼  >µ*åù¡sXœÜ†+gr" ¼«×¥N0,D]8Vö¶éÌ9B—!Ó¦Šˆ¢ÊÃI<ýé/lÎhE«ÂIîáõiT¯:ñBn }™¹bòb©áœOÊŸ«9æUŸæ¡ÎçÕ[ÀB`lCâëºqTÍ^Oß ËØ“Ý _Ûz¦ü|˜€yîþj8öÐcLûö÷¢‡r¡~¸‰ÄÝÛ9‘_›û~[žNä½}¸!ñ=f.ÙÇQ5p:±ƒÝ™.DÆCç„ùÅ_“‚þ΄4º‘&u]Ú„d%Ñw|A½~—2‘žGZº³_ êÅFáo†ê5 Ÿ+,ÈÅ7„ðð*W ÿˆˆˆ\ j*"""¥Îš¼„i{sDÁ¸K$·¶ âhâBvçŽ,îNƒúDºÓ˜"/…m)T/ ïËùÆ’w˜A•øšøœÇ•ð†‘8eîagQ»l”(U*î ÈúsŒ‡rpxÄÑñÎOú7}ŽdæêdÙòIÝ}€l’÷RêÖ‡º=LŸO6csœäè©â:«:Sµe|Ó×òGrØŽñçGðn|‘®Å¼äf¼C°Ø2IϱcMÙÂ~«7u„P\$rîÛs§Zý8¼ž=§äîIdùÉê´mM“Öœ\±ˆÙNíÛÀa£* ªQT±×¤˜zCüÏÔ{IÌ~4혀ÿ¶‘¼øÊ`¾ÿy=Gr+|d(""gÑL ‘ò¦Â/?Éa÷‚%ud0¹ß£L>ç¹ æm}€˜úFA/È ŽqØ/tÌàäG¤?,LÞJе>›­áÈ9Ȧ#à\»*•ŠýõþéŠ 0Ü©õàF´XË¢Ù3øiè¿™÷0_i[°u¯QƒGßêEÜ9+,T 2››8‡ßÄ•ç³ì·trÛ¯+Ñì‰Ó :/Î0™1áÀ86ì˜p*qpdÆ/®>þŽÅ¬=Š÷’•d×ìA_ ^Ú5f ó·væö¤ýØBÚS³¨nªÅ^“Û¨r‘z/ ŸF½øà³þX0›ãóóôf<ýÎS´Pë ¹L ññ$&iÙwYÓL )Uެíü¼<ƒÐ{^æïðßÓ¼ÄÝÁÙ¬· Ü «Yö¯`KzqwîAÄÃá59Q¢í` ,®ÈÍ$÷ì»ßÓã$m#íÌã9ì_»‡|H¢þÞ|ÒäC|»ºXŽÎgÂï)äŸóærØûóDVf{Ñü¶ŠîÀ`'móJ’ "6Ì¥0¼1ãÞ„{žÀоM06Í%ñ ßÈ0\‡Ø•ãKHh(¡gþâét­U,a$ÜBêòù,û%‘¿n¢mD‰æYœ?TåêÆ–m'(é®»–à†Äyf³eÉBf®µSÿöºx`òkÀí±v’f.`ÉæL*ׯƒ±ÁOQ×ÄZÜÁ—ÅÉ'šVŸgÐSóä f­>Íä„›r3s´¥«ˆü# 4ÊÍÔ‘ ->!¤ÄIJ.CDÎpprã|Öf‡Óµu]¢‚ξ£µášÆÌ óù3µ)­n»Ÿú ¿âóA_sâÞæT󲓲ã¬ÝOÌ´èÒ’éþÀ?Í¢sëøš2ØrÜFÑSœ© cç0iQ7æ’’FË–a´èÜ‚Ÿ†|ÇïltjHζùŒ^|’¨;Pã¼Õ&|oxœ^«ßdÄׯñÖÖ»hÛ oû v®ü™Y+áóË<ëÎÿb‡4¶'­§ReÈØ³Œi7áÔèin±€õ ¿.ØGdU|-9Ü|˜Üñq3ár·‡®â§áCðèr;õƒ]ÉOý‹ã•šÒ¶žïú<8Q¥E;¢¦|Ç·³!¢[kB‹èÿQ&߆thêÎÐ Ãå܉C ¯YÂA8³CÈy\ÂiQÏ%Ëfæ@ÿÚž×Âð¦þ ° žÅ¯Tâ–f¡YÖ®I‰YO°kãzlgÀâ[˜³g`8²ØµøWþªIˆ·‰Ì=;8n7SÅÛ“%š¡f&NenT;"8NºOcZÅü?{÷_E™5pü7·å¦“„$¤ „’P¥£Ô‘± âZÐ]ĺtWTWWåUD)ŠJUDA¤ˆCo¡„–„^n™™÷{S@PpSá|?Ÿ˜˜™;sfroÈsîyÎãC}\©W!ÄùIRCÑ IBC\ŽEAÓè{Çz>i+·c¾‰”ß¼Eo$¤cO¢>þ˜•[NÓs`O|ÎÀ§s³è?ßS (Ö@¢ãpO(ø%ßÁóòÑg_óöËŸ£þá´K >Oÿ#¡}ïâæ½oñÙ‡ÓÙˆ…&]ÇÓþª('ßÁ³÷û2kÁ|^]mÇà׌®cŸä¶Aáçtƒé9ñß„¬YÄâUß0k}!*fü£Û1p½\ß«yÅJ¦€Ö$FždýÌ—Y¥^a$ºI£ºh½(›°f^!`lIŸ»îàš#(ÍñäãxÎù˜ófðµ¯&$H ïï&5Àظ3Ã’çóÚöV\Û-äÏ7ºTüè8áqÆ[?dÙ'¯±¦ÌBãf!€‚A¹ÐðÞ“˜^‰xnÚŒw¯~ÄV$†¼Z¢[£ùVK¡GÔù«GôÒß¹'g.2nu? ^›vÖ·Ì)“ùïmU¾¡s|÷>úá%ÖâÝË]ýQè<~%Eq¿âT uÐ.T/;ÆK?gùú4åÚ+cè9|×§R¿ó*¹›gòÒ'ÁLy[’BIj!„õM-骃¦R>@×+º–Øut4\m4÷›ÊîÆ†€^Yòžšæê×Q'”ÊëÖ´‹«ÔÐÜ;隫ڥ&¢ŠŽ)Ô‰®§`Nǹ.£r£Á>>½€²/RÁè‰Ò8SŸÛñˆkJÙ›3PËÊ{¥¸¯K7B£F€¦×â80ÍYå„~Ý1wrÀC©±ëúŠÆzí³6”W=¹çn逩Y;ÔCÛÑt£âî¡Q>·KWДڽ~½hsŸ{‘ǃHx=âBñtärtÏnòY¾YÑ IRC!„¸Bè(èšîJ&”'1t Ý]±áJr(î}u÷2¯®ï]R>CñÂØå^¬×ôÁी#çòÇ)Ýr Es'TM5bèô^ãp~øÎNoâ¶”â×?r¾Íú¿‹wç”®Ás„¶WÄž«‚Òós°ª/QôÁ×x2x4Þúþ­”öÂ|ÃKx$„¹j{p¬œNÙ/Ç0h€¹+Ö'–¹bÎ[Dñkï£9-Ú݌ǀ¿` òÇiœkž¦t“{ŘäÇñê„bÔÑ37a_úìGÊ+ ”È¡x\;sd82QzŸ²•]×cÇ2bæ˜X Vìû°½={–”̽zîW);¯«þвñ5Ôò*]u]cÉ!œéi®¯÷ýˆã˜ï»aŽš‰c{44UE×ù# =èZÌqË(Ýžë> c÷±Õ“è¦@ÐTw¥†W<–kïÆÒ.ÅP†v`¶eá̱Þ˜îüOã+ÍüÎu>sK<&½Žé—û)^y¼Rð¸énÌ‘((EÛ· Ûây8 TPUW£ÌþoãÝßuJuþhJv]“¬þª’Äã¶©”Îz== M1 ¡c(_õD©å$‚^Êžo±âx}öiF¶ð¬8—ž*÷SsI]òó¾þ™ã%àÑAcocxûŒ€^´y3æ°)ý$ye:`%´ýn½kIFPsøáƒ7ùlÛNØŸÈŽ\{Ç ó¡ü7LÉÁU|8{)›ÒóQ-ÁÄ÷Ä:lv‡[´Ÿ¯çÌá‹ÒÉS¼"{sß“· `߯¿ï¸ÙµcÐ^|õ&š™kå.^V’úô!uíÚºC4P}º$±öÇÔº¤†BQïÔÔB®âÕ½Ò‰šŽ®«(îªE+/—/@?;‹…ÀzíUè«_ dO6øF äœvU‰¸“h:JË x^×uÑDÊöœBñØ m;bòþÈ•¸0`l ÇÞE=‚ÆC,S–HŒM àLÂ`X‡êPPÂR0¨{°,§uÛ,Ê~8®ûc¼ê~ G®û¨Û)û¿ÿºÎ<4‡†w/^7]¾í}J—¦ƒG0äf캶Sßb[—†®4ÁtÍ=xÜâD›öoœ¥:ÁóλQv¾Ké—{ É`<†?ŽgÑŠ×K4æ¶-`ýË”î= Ú›ëž4y>Žw6¢YQ{܃¥åû”¦•'!Ü ]sÝÇòb[:F×_tÚo÷Qü¼áÔ,ÊNŽÆ³ß ;?t%Xü»áÑÉ痯àÉ(^fЊÀåö—ððû Ûg±û™ IDAT³Píá˜ûßçJ_ŸŽ³¸¼² JUªXÐT0…bЉ@_;²ôh”‚eèh¥'Z1Gwì'7j‰ÁÃv’m‹çññË&¢^¿—$o5k Ó§Îát§›™4.cÆwÌž5ƒW}§1uX8&Ç –O›Êü¬¶\wÇÃÄêäçˆòtW»Û0þÉqÄYA1ûÓDF4Š$4Äÿ¢¾$4@’B!DýScÓOM£²[†V±Š®»¡Ô5×r”zåTwP}ÅŒBÎÛpf”{ÎÝšÞŒçMýп~ˆÒ-'\q¤‹ªMÂÛÛ–Ó`iŽ)TGýbZÞ gÌXÚDîèA1Zœ`MÀØÈˆ3Ë‚±m,œxµDC×m¨{¾/Oà<€)ñ^L-ØÏØÜ= ÑN¦WVCš`îßåÈë/\qöÛÔØãñ ØwíÀ‘ŽqâL¡‡tŒWÝ„±d1Å £:€Ã‡Ñ›uÇ+¹+Ê÷GÐtú¾8”V9¸Æ.×a8½ û º¶ÛÞ xõê…²}‰+ŽòŸƒbB7ZÀ䚌eÈ_1è{(;Zˆ®7ªøI뺎Ž|< ôεóQ;܆%f!¥{m»ŒÁX¼œâ´ Ì}ÁÔÈ]?ƒ}#–ð\ìïü ÛaWŒÎ*†É`Iœ‹cSQE]¯œ¶Tõ¼®çŽýà8ö—©¨ðÔƒe%NÕ½~ΓÇ.úyUïU,×zvg]­à4žwþ›¢GwÚÝË$+µ>ÕÙw”“vk‰õ'× Sù|ea#_âî!˜ÄÖaØ=Ʋ…i ÜWW3!mSHJ°ñD•¦ñÐ'›8TÚ“@‹ëX^‘ $·ÁBmƒ³ùåÉõl>f'©•‘o–²ËgÏß9X Éø=[xqÃ6²‡„p`KúÒïéÝÊZ%B•l£!ÑÑD[BIj!„W ½¼‡{à©iz•¾åa¥âk ¢jCWÎ©Úø½óù[úUXïžñ—eØ6,Ñ‘çÚ¨´Á:® ìÿ7…ëöW&ò·`?¬áÙ! eËJô ÎMG°íÏA·çáØuøÎ(†ÝÓ%}66ßq˜šP–ˆ©™uíϨN ŒA˜úÞ‹µS Æo(+L8 åý&\vVO K8ÆPÐV§¹ŽQ•V¾<§îN yéèx£x(èºCtcð¿ïç¯?û±ÅM€Ê"®)@UŽom…99uÅjT‡äáü~-ú߆anüe§Tô=i:ßç'W>6w ¶¦c˶ƒA?ûº+Šè9%è9«)ûõ¼ûõ¡,#Kׯ8¿Zˆ³Ô£ OÐ ÕŹljâÊû’—†#©!s½„BˆêázS]=«A¨¦kèŠ{Éɪ«žè¸æÿSÙgã¢ÙPúÎhì­þ‚ÇÕ7á5q ê·P¸r{Xu {j)–¤û°vÚEÑæãîOcßô3Ö1C0ú~‡³u”œUØsíh:è¿þˆÞ£;Æ å艡¨›×b ú –ömáh,&ï l»3ÑtCç§ñŒý‹—(9x Ý·ÞwŒ44]«lê¨k•+ËVô¯tÝ—³ož;‘á~¼ :ÜI M×]ùš¬)œ»îìæ£j!šãìUg*¯ Ä^ÙÛ„2r>þ#Ï>­¹KsJ—ìq÷>Nͦhá6tg)Zþ)´Â*åû¸ã× V°€^RЦ ®Y„õá1X‡`Ö¾£05MkŒfWÀÛM×\?oôsîKùµëhºêJ†™M•ûœU©qþëTT§«§‹®¡¡•¯·óÛû|ÙP*îCñ‡Oã8ð+ƒâžuâšæUÛ³OŒ~‘„˜ààÞ“ØúPéÅ`Ä€û5p>gmwÿމ¸ž'&v¡QÕ¼ŽÉ‡`3d×årÏBˆ§öבºD’ÐB!ª‡†ûÝs½üC]s-=é~×ÜõN»æœjš¦W|¿¼ÉèE}¨Å8w}Fñ[ã(ؘ‹±ÇŒ&Ý=ØÏÁ¾ä! ׿`ñ2^-ý*ÞÕWwŠÃÖ”Ì ¡¨?Óá:¦vììE‘Xº ÆrûLÔí›Ñ#†béÒCÖ7ØO;Ð53†æÍ ë J×oÄ‘qçá®´¦£k*z™LÞ` 2fÛq§ÁØ:Áµ ç¹×î ½â>R~Lµõh¶E)8‚óäáʬ4õœý+Ž€¹G'”ÃïPøÚß(¨ø¸Ÿ¢-g0$Çh©rþÒ£8íÄq45¿äü1–ÇŽŠ(µ»~Ž™Ë)=à‡¹C3Ôõã(ÑÐUš/+ºæ@=|݇9ÔZyLŸÌA î?†¦9ÑÎB£8W³ÔóÝ›ó]§æŽK×ÑU;ºO««¯Ëeó¡ó¡SøÎÃ8üRQ•¢ë®×”Rñ¬½Á»âG¿OŠ6}Æšcöóîc iE¤1Ÿ];²q”Ó‘Åöݘ"[U4ñüßñ""&²ïNDDDåG¨?ÅD@LVG:ÛŽ–ó`f/38J(uJâCáRï+5„Bˆ+¢(52Ø)Ÿ*PÞ´¼_hECPM©ì·QÞc½ÊŠ(%°ž­Áy< ÝÔS˜/”žFuê•«yjØ–OàYøÞò0ö—žÁ–¯BévJ6gÑhÀ?ð4ž¤$5­ü^ØöQšz†F=ÆÀ‰ÿR˜ëDÍ_…­ìz<»‚sé:N°ã<|’þ‚µ[:e‡³Ñ=£]kÊ8'v )#ð8mGJ  ç– ”­\ç-Ç÷FJ~Þ‡¦4ÂhÜmw• –*ýF*+#ʰ?ÇUñ¾kÊšõ8‹ ‚#`Ïl9Îʾ«º^yMþÝñˆÇÜ•Ø2r«ÞE”ßáÕé/xÄøTž×cÏûôÐÏÙW¥FY™ë|êil_ÌÄÔËû¦£®ïé4› ^~èèhés(9Öï[ŸD]º‡-Á÷c*^IÁÏ'ÑtplÛ„Öy(ÞÃR’vÍÔ£µüÞêî>-g_§RQ££ÙObÏtâÑå6,G¡*!ò7Pv¸àâž_õÔ¹E :Ω¸^9îæ ŠëC×p5 ­Íq¹âKÒÍ·Ñy÷ÛÌ}òiéGǘÆXµ"2íãT“!Œë™ÌÈþ!<óÙtfZn¤W”Αu³ðT(Cïi¯R![h6h(q«çðîËï“7´Ñ>§21¶ïG—^­‡18âIOŸŽõ†´kbÁv¦ÿöWÑ<2Ž@ýk}¾~e©´ìÕ‰PYýDˆ+–$5„Bˆ+„®ãЗÎuW¥†ëk÷g÷’“•BË_W+¯ø¶ÂÒÞþfÀ‰vü{Š?šƒÓ¡ãî$à:®ó¶O^ÀòØ øŒì…cÖ4­ çú¹8z?ŒùÄ¿ŒñË›ÉßrºO,DõP*ƒŠ«2C×1(®2 ƒ¢ ŠÑ€A1²sÈ£$&u¨ãˆÅå(-u=z_S×aQ£.˜'•5©Oç$Öþtikøöé’T¯ÖýBqyÐMu'0Êÿ«¸ÖBÐu”Ê)Šâ^¨D×ݸ”šMl(VŒQ1(ø`êõe Éß~ºtÿ­JåNÅ—Š»hÅkʽMQj?Y(„—)IjÔC’ÐBˆ+œ¢ü¶l¿Tô(¯À]©\ÊÕ½¼¤âÞOQ×âîÂ`lŠuÌÛx†hh‡–R4óCœ6ñ‰†G©’tõÒЃ+?¨¸²Š®¸zçÔi¤Bqy¤F ºÔ !„¨¶yëçÒQÐ4t¥¢JCw—Ä+îw’+Zg¸›ƒ*Š^Q¬Q1Mÿî‚Ûr&õ¥¤Êÿ+µ¹Þ¥ÿ£òg«^¥§†¡“ê:ŒK&I !„B!„B4HÒ(T!„µJ/=Ìš%Q{ÞÀ€Hs]‡#„B`íO©uŸ"•B!„¨UzÉV¹š¹j]‡"„8—^Ä/oÝÇm~ÆG]Sst{~ù™ƒZ]‡"„øI¥†BQÏ(Š‚Þ {jèXÅœyËùq_6ÀÜ(’ø£™pC2~už#›-‹>â³5i+RÁàEHl2ƒï¸ƒQFN}ýÍهͽ»Á«1M[%ÑcÀú%cVÜô226,à£ÅÙ•Y‚Žß°8:¿‹[{„`úÝóXÎŽ©äþu÷«äÝô*/þ%cmÞqeÐóùáÕÉüç—b÷7<¢ˆKìJ¿¡ènÅõÔ6âN„3«ráÃ5tΓ«xã• ¤<7X?Ë?@QoIRC!„ÕJËÛÌ[/|įAÝ5¡#Q>:Ç÷“®ûa­óѺƒCŸÿ‹×¾,¦íà±Ü׺1–²ÓÞ—…Ÿ—Ðpåc3¶b̤‘ĘìŸ9ÆîÍß2÷¥Õ¬ü(OŽi‡¯A§dׇL}g=æ”aŒ¿!–F†b2 ÐßãžGˆZ¦«”æCÄp¾=«£„ÜûÙºf1¯ýc ='>Å„®AOânx‚ê:^!„¸H’ÔB!Dµ²Ÿü…}v_zßuÃ[y¸¾Ù¡ }ÝÛ]ÅÞvRß}˜Ûró±c%´ýn½kIKê’˜÷õÏ/ïˆ {ÃÛ`´bîÃO±¶õã¼y_[¬€=}“žú™î/N㦦f „Ôé÷3-{4¯OHHyAÍc_ZÄÜÁ½cú螈۩gE‡¹ÍZ·¡­ =zö£÷/òÄ‚7ù¨Ý+Ü›äAvÚn ‰<|Ï tðr?¼s/÷y²ÿà<¿O/ÚμsØ”~’¼2ð¡YÏ!ô <Ì÷ßÿLz®s`úßv7uÄØÓ?eêË_q À xžòÆOF[_÷ÉÕ|v,ÿˆù+¶p¸@££ZsÍ„û¹®©Ð)9¸Šg/eSz>ª%˜ø~c˜pC'‚¥õÉåÃ'ŠV­ÛࣉèqM_¾{í þïÿ#>n2½ìyçžK¿–éÿJC)‡VÍæÿmâp¡–`:ÜòŒï™?{1ßï9íz-w¾…ÇïïE°Q§ôÈZæÍ^Ìú}gp˜ƒˆëùWnÛ›¦VÔ~øàM>Ûv„Sv@Á'²#×ÞqCã|PÛ¡E¼þî:öŸÈ¡DÌA´î{#w¹ŠP÷sRÍßɳçòÕOáEXò n½ó:•ÿ.É#mÙlæ}½Œ"£_,ƒ'=Æ( @+ŸÏJ¢¹åõç,IG!Ij!„õM_ÒÕÜ(š 6‘¶6•¬æ ±œ¿†=¤ãnHÁ”·“ås–1ýí0^ŸÒ“@¥Œ}?Ï´ÐmÌŒ‹†£ë>fþ´°=;•1-ÂHníÅWw“íhK”Y%ÿnÎÍ®CEhM0Ø3IK·ܳ UÇ(Fo½`Û÷¬?؉k[ú\\ƒ1Ѧn¢×Š©¬ÿj'ãÚwÂ/*£z€ï~È þꨳKõÿìyÜt[&»vÄØ}ü­¶Œu|4g³:1jôDnð-aײY,{óZ½1‰N¾ ÆÆÉ »³-¾þV§¶ðé{Ÿ3ããÖ̘Ы^ÆOŸçÅ/ËH¹~£Z5BËÜij6²ûŒ“ëšZP³Ö0}êNwº™Iãb1f|ÇìY3xÕwS‡…Ë—+s(=Æ fÙ£‹Y±å4ÝúŸµÙ‘ñ3>ÜJãëàŸÉèy§( m„Ps6òÆ3ÿÇîÈÜüP MLE䨣ñ7‚šó=3žyŸ}͇pû#‰øüʲÞãÙ3&^žÔƒ ­˜£;ö“5ЇÄàa;ɶÅóøøeQ¯ßK’·‚𻇴 ×Lx˜Ž:ùû¾cÞ¢·x-°)S¯ Çd?Ê’½ÄRÃÕÜ>å"œûXþÞ|^ýoc¦OéERÆO§òï/tu7µôÅ™[€w°ò|èzÏ?mƒ’Т!’Ÿ„BQ­ŒMðÀG™þÁøûá$õ¾†zÓ>¬|Î>€…ðÝè”`Ú^’ÊCoâPiOÔT>_™EØÈ—¸{Hf ±u¶C±laC'w"ºs Ì›~eÁ_‰ ,æà¶“`†ã[QÚ'ë™]ìÈõ#!¹ gxÑîÖ‰\—÷ žùß´ìÎÕýпk,þôW‘G8 M`mÖr]hÚõnî?ð:ïÌzŒ _´¥ÇÕýtu Ñ>Æÿí<Ì„´M"1Þ m#)Ýüï(¹ºG þ ´ö;Ƨ~ õ¤ƒN¾Œ~-éØÁýÐQ˜w­ãÙ}{9ãlMXIŸsŠàaSy`D3,€qšÅ³6º`çÈ7KÙå3ˆçïH¬ˆ‹düž-¼¸aÙC “ñÞeËܸM=`gzNÎNjhÅ9áMR»vÄÅx¢ëÞâàØª…¤ÒIŒ££oÕ¬žƒÃ+“fèÀ¤‡ntokK¬×)|}!ßïÂè0מ^‘ $·ÁBmƒ³ùåÉõl>f'©¼Ê‹F´JN"ÑOv±vOä­Í»ÉŽ×Þ¥|•Î ¯Ž£o#K“q©Ü÷æwì*èIï|þõ)ÂGþ›û†Gžõ»À‘`Â/,Šèhé©!DC&I !„¢žQ€†[§("úÞË+ÝF±÷Çu¬^µ˜i+?¥Åu0yT¼~ó#~á1«yä—iØs÷pLõ§cBpå ÄJB?¦î!ÛÑ™˜–݉5¾ÇO‹éëu”Ÿ2‚02œMË·’aK"tïVNz·ã–ó V ~ Œ~ê-þr4•õß­fÕ̲èÓ®ÜýÄÝôýýK;ëçbjL—Ûž'åºt¶®ÿŽoWÎ`Êçô{` ·v Àø»ç¹ÄùŠ•À@+;C‰þF0ú†âKe Svtóç~ÅÖƒ§Ès˜ñTl`éƒ#k7G~¤$‡sÞá›^ÊñgàÌ žº}ÅÙÛü²(T‘¤ÆeîBÅa±C¸®Í6>~n»÷gР~tñè—ptw6D^O Ÿsª±ô2öœ†È‘U¶)ø¶è@$?³ûX)zØoÏeˆ&r‹/°2’âIH¸/ìÈ¥DubKÏ ”ÌøV柵c²‹U쥻9êô§cB2ƒJˆË—$5„BQ#`Z÷IëžC¼ä9žüü]–u~™}ϳ¯Áˆ‹]\QñkMæ*s6$×+ûÌíxðªx2—Ìe˱\Zn:Œ%~81Ö Áˆotþrkû‘ÿNùïБ¤G;\è`;Îö“`ŒnJ@Å_P æ€X®ËUƒ‡ðíËO0kæ'tiwñÖß;OWü/ie “ÅšV1øTLŒèhºe{™ó¯÷Ø2„;'u!ÚÇÁÁE¯òÞ×¾º®¢aÀtÁ90º+aq=OLìB£ªû™|¤§ÆeΑµ›#vhôÛÁ%škŸx“Ž¿~Çòe_ðÖSKùjøÓ<5"ÈäûK¤\(k{¿ &ƒëu®U¢”–ÜöÌÄ{VÝËŒ¨é ;A,„¸(—2½S!„µ¡÷Ôø ÅJTRßúÐõ/ñøžog!!9ŠOYòÛaÙEçORóq ÀLÂõÃéCÓ¦14©¼zsP Áä±{ïÎû¸âEDLd ß7œˆˆˆÊP.ÐE\§ø~þ×d™ãÜ1èüË +VÂ’sçS¯ðt?­üšt»±pl ŠÎù}¥xÙ*Žmå`Å6ÂÛ8F0­£<ÿL*ä&šGbÕOr°,€ðªÏÙˆ|L –à–„òÙ¹=‹só(&OÌØ)²]l*UQ_I¥†B!ª•íà"f®rÐ2>–&þ´Âcl]¾Š¥9CÃ=þðñŠo2#û‡ðÌgÓ™i¹‘^Q:GÖ}ÌÂS¡ ½§½;¡` éâ>˜É¢^t›ƒÕ`¥]ŸæÌ|g §½»ñXK¯ßœ´3ü0k{ÇÓ:º1ÞF§÷®ãó½à×»-ÍpÀ‘Ë¡];Àä¨XÒuåö3D ~”[}Pppbå{,ÊŠ¦]\8ž Å'·³ja„#Ö;ÞûýóT'£_4M½lYºŒõ†"|42rlÛ ) ëìÅkŸÌàËHºE(œÚ¶Žã@+,44”¸Õsx÷å÷ÉÚ‰h‚S™Û÷£KˆüÉxÙ(<®ÞX¥äÜÏÖïV²õ„?='N {8;ãæÌÞ·iÕ4«#›íG‹Á3O£…èÃh½úCÞœ6›‡v ÒËI^ž‰¶Ý‰ðWV½Ç[¯­×&â—ÿ+Ë>úÚßMÿ3]–õ;¼Ú\Ç ˆ-,™ñ*Þ7 ¢}˜gî rü;sMbFÿFô ä…ϧñ†6’>qKÎPÖ™®Á±4ó*aëgËØø×6xäç`‰ïI¢4 ¢Á‘¡„BQtTů¼oYöþä9¬·ìÂM¡_¨rþàŠ•¸1OòÏÙÌ_<ƒ%à‘ˆɷ3¼Ee³QC£d'Zس?…«[ºª%\M+ã^Nt@Üo›w€®cö‡Ck?áÛìtÀèIÂ_îaìÈ6x¢aöñâîããWÿí Ç3ˆf­“7e(ýÚ5Ƭ¸®Sñò£l÷J>Z‘C€%€æí‡óðØëhj* ûwÏSÍ<Û0î¡Q”}ðoÿû WÜ~„'†`5Š'<Îxë‡,ûä5Ö”YhÜ,P0(®;j ÀägŒÌ›û%Ÿ½¹` jàؾ’Ô¸(F<ý½ u¯ÿ{Q´L¼ž‡@Çpëy«'œyél^ü5å:#~Í:1vâu43ƒÚGþi`Þœe|òÆjìñ‹Ä#] lÜ›¿ÿScîìżÿòrœæ@âzÜÉ37÷ ÈHµ$5°4cÄ“ã9çcVÌ›Á×P¼š<"¾‰/âoù'ø~È'+ÿWéàJ—ÛÛÒ9º c' àô_ðÖKËPüZpí#WIRCˆHY¸`ž>bôغŽC!„nQ­ZqêÈeeÕzÜ ëV“˜ô;=#ÄÅql!“]C³¯1¾…¬þPWÒR·ÉëRÔˆ´Ômôè}M]‡!DZ¸`žTj!„õÎåÖSCÔNlXÅNC‚¼1eðã’/É êÃ=‘’ÐBÑpIRC!„âr§•pjßf–n:™R >„'ôcâ½£hyÁb„BˆúO’B!„¨UcÆÞü»ÛçÏ›[K‘\A þ¤ŒŽ”ñuˆ¨+ôº+'¯?!DC#I !„¢¾¹Ì§ŸÈ IˆÚ'¯;!Äåó{§ IDATÊP×!„âl pù¦4„B!ª$5„B!„BÑ IRC!„B!„ ’$5„Bˆúæ2ï©!„BQ]¤Q¨B!„u(;+³®CBˆK’B!D=#B…¸²DF5­ëÄeÆf+ãä‰cu†µB’B!D}s™O?ÑK³fÉFÔž70 Ò\×áQçl¶²ºA!,é©!„BˆZ¥—`õ—«Ù™«Öu(B!„hà¤RC!„ÕL§øÀ*æÌ[Îûr°æF‘Ä÷Í„’ñ«ãèì‡>á‰~ÉñòœŠÑ‡Ð˜6tì5ˆ!½[ÑÈX¾çï_G#CðÑâìÊ,ALjoX‡ßÅ­=BÎþ#K;Ú§à=ËDf>Õ¥6¯X!„¸|IRC!„ÕJËÛÌ[/|įAÝ5¡#Q>:Ç÷“®ûa5þñãkšn/ _õ¦ÓøûfÀV˜Í‘íùæýçYµþžš|-±žÊ\‡NÉΙúÎzÌ)ÃC, Åd<@¡¿'õà2…Bˆ+‚$5„BˆzFQôÜSÃ~òöÙ}é}× oåáúf‡.ôuo×\{‘úîÃÜ–›+¡ípë]#H p§Ô\R—|À¼¯æx xGt`ÐØÛÞ>£ýs~е­çÍûÚbìés˜ôÔÏtq755%¤N¿ŸiÙ£y}ê@B~“eð 8¦5m›[HîÒ‡þÝçóÔÔO™±0ž—oŽEÿÝëpp$m7…ÆD¾ç:x¹Û¹×EÝ#½h;ófÌaSúIòÊtÀ‡f=‡Ð'ð0ßÿ3é¹ÌmèÛ=ÜÔ!# —ìäýgg°öX |›våú ·3 ™' €šÏŽå1Åh`ô¢qTk®™p?×5µ:%Wñáì¥lJÏGµßo nèD°´6BÑ@IO !„¸Œ%õéS×!ˆ?Á`0`04ܢ͢ ¢´µ©dÙ/œœ é8‚û&Oáú´oÓßÞÄ ÐËØ÷ñóL[tŒ¦Ã`Ê”¸.ú( §½À‚e` #¹µ%w“íPÉß¿›3d³ëP‘+ibÏ$-ÝNpR.ªlBÁ»õ0nJ¶pzÝ7,û£ë0âŒQ=Àw?dPv‰9(Ý–É®]'ñpSŸÂCã(\¿€Ùßkt=‘)ÿøƒƒòÕ›ðs¡ëàŠG]GüÇþù,ÏÏ‹ öØoÿxl ßÚíð¯ì>ã@ÍZÃô©sØ6ŒIÿ|†)7·%竼ºâÎK _!„¨7î_LB!þPêÚµu‚ø4]GkÀ•Æ&xàÎnxlüÿÛ£LûðRO–³L­…ðÝèÔ¾ɽGr׈(œ{6q¨ôÂT>_™EØÈ‡¹{Hg:3ôîIŒhrŠo¦Q {ݹæÌ_Ù_ ^ÌÁm'Á Ç·¢PÏìbG® ÉM¸è"Å‹¨6!Pr‚ãEÚ\‡€®wsÿvÎzŒ ½ÈÌe[8Zt)ÍOÍ„´M"1¾ŽeTK ¸#W÷H!1©×ßÜ—@ûARO:Ü76vSˆ‹%.ñjÆÜ˜€1s‡‹uôÂ4>ÿæÁÃá=Hn׎ä”8UœËΑo–²ËgïHr\ ¯Çøn^ݰléÙ*„¢’é'B!D}£ë {IWÅBDß{y¥Û(öþ¸ŽÕ«3må§´¸î&jƒ×o`Ä/¼1f5ü2 {þtL®LH˜CIhãÇÂÔ=d;:Ó²;±Æ÷øé`1}½ŽòSFF†³iùV2lI„îÝÊIïvÜm¹´Ø«Þ÷?¸ScºÜö<)×¥³uýw|»rS> ßS¸µcÀ¥õÕP¬ZáØJ4ð7‚Ñ7_Ê((sMØÑŠöñõÜOX•z˜¬B O•¦ØUÇéÝuø‘’Îy¯X/åø3pfOݾâìm~Yª&@DµqpbÕ{ÌÞŸÀ„»{ÐXž[Bˆ$I !„¢¾Q”†ÔpS<‚iÝk$­{eð’çxòówYÖùenô=Ͼ#tw¿‹8¶_kz4W™³ñ ¹þ[ÙgnǃWÅ“¹d.[ŽåÒrÓa,ñɱ^BÀz1GweW'"|*‹Y/tcš™s@,W ‹åªÁCøöå'˜5óº´»‡øK97 &‹4­âG¯˜,qWíèl{÷%æîmÅÈñ“I·R¼ý#^šow…®«h0]°WwU˜D\ϻШê~&é©!ª™FÁÁTvŽ’©MBˆ'I !„¢žQ€†ŸÒ¨B±•OÐçßp$×çIjTe iE¤ñ[víÈÆÑ"ÂU­áÈbûîL‘­\p%€ö}š3kî*Öxdàlu7QtkZÈg«¿ãÈ~3‰¶Äó¢ƒÔ)Úµ”ù¿:h›ÎLËôŠÒ9²îcž eè=íñU $]CÜ3Y´Ñ‹nSb°¬´ëÓœ™ï,á´w7kéÅ…‡î62ìbG‘[Q6G¶ob庽”Åâ©1xüáu9±òmeEÓ..œ@O…â“ÛYµ0B‡ë[̓CS ±áf¾úq)_Ç&>Øû±üŠwÁ ) ëìÅkŸÌàËHºE(œÚ¶Žã@+,44”¸Õsx÷å÷ÉÚ‰h‚S™Û÷£KˆüIxY3ÐeÂãD•h€ƒŒ/ßdÎÁvÜv_ÂM€Ñ›HIh!&ùL!„¨oôôUñÃ+ï[–½ÿy+Á-»pÓãcèj„œ?8„b%nÌ“üÃs6óÏ`c xE¤0bòí oa­HT%38Ñžý)\ÝÒU“Ñ(ájZ÷r¢Ûâ~Û¼Ãux‹þ†b¶Í~…mFoB›·¥çO1¤w+/æ:²¼ü(Û½’VäP`  yûá<<ö:šV÷tÅŸÎwÝÏЙóXúÆ‹,0zÒ(ºf?:NxœñÖYöÉk¬)³Ð¸Y `P\wÌ:€ÉÏ™7÷K>{s-6ÀÔ†A±}%©q¹SÌÆ´%–,p¬1-Ú¶#Æà$sý,¦~±ýÇóqº>´{›’öß©¼·9“2ÀЂ^£ÿÆ-=›¸{·è”e|ÏüÙ‹ù~Ïi×òÌoáñû»œ}~--ï>Ák©qÜ÷âýt’&Bˆê£,\0O1zl]Ç!„B·æ‰‰Þ¾½šÖ­&1©CµSÔ_Žc ™üèž}ñ-.±aª¨5i©Ûjùuicßÿýg~íÆÔéãÜI {Þy€çRÛró×ÐÌËI™¹9íã<9ýk*',ø Ù·r6s àöמ¦°5g¯N~‡Ý‘{m MLE䨣¹ªséï<Àsé×2ý…k(Z2•g¿²pÃ3SÚÔãw*¨DuJKÝFÞ×ÔuBÔ¨… æI¥†B!DÃçàĆUì44!,ÈCQ?.ù’Ì >Ü) q‘übIIiG“*…¡í;êþ:&8›çzÂNÿ`ÇV-$•Lzd}«¦*lîÏv2V¾ÎÛ_¨ ~ìaIh!j„$5„B·>]’Xûcj]‡!Ä¥ÓJ8µo3K7áL© Âú1ñÞQ´¼¤UX„¨BÍå×¥sølÝN2N—€—œÄ«:è%Ý ‘×ÓÂ穊ãKym¾JÓ±¯0º$4„5B’B!„[}Ih(ŠRíSODÝ3öfæÏ›[ý7ø“2þ9RÆWÿ¡Å•J%{ͼ¼0‡.7ßÍ-mcÎû‘·^^æÞî^&ø÷RÚÓ½ñN6~ú._¶}”aͬ’ØJ÷>Il\[?þõ´9B!„¨óçÍ­™„†5ÂAæž ´°kÑ?…¸fÑ4oÑ¿òÍŠ±pl Š.„õŽãúÉOsSÌaük&?œQk+xq™‘„†ø=R©!„B\AÒR·ÕuBˆÁLpË&°y-‹¿äš–˜KŽSø`¡ÙÀa´^ý!oN›ÍC;éå$/ÏDÛn­*Ž¢X›2äïròñWxçÍV4l aÕ½:âŠ&I !„â !]ð…¨6¬[]×!\€‘«ïãžÿÇ‚O^c£À„W`sÚù¸:‰CûñÈ? Ì›³ŒOÞX#~±ƒx¤K«³Ždðkϸ‰ÙñÜ|Þ^“Ä?†"‹º !ª‹,é*„BÔ'ŠBLBéiiu‰¢ÈRË¢¦È’®âJ°pÁ<é©!„BÔ' -BEMKèÚÿÆ¡¼£BQÏÉô!„Bˆ+ÌöÍßÖuB!Dµ¤†B!„—¹ò%…ÏGVåB4d’Ô5¢OŸ$ÖÊÒKB!D½Ô¾û@ާïæôÉ£uЍ%’¸B\®$©!j„$4„âORÐ¥«†¨Y¿nü¦®CB!ª…4 B!„B!Dƒ$I !„¢‘ÕODmH¼ª!‘1u†Bñ?“¤†B\AútIªëÄ‘é'¢¤ýð-YÇÒë: !„â&I !„¸‚¬ýQú݈†«OIÊ !„âl’ÔB!Dƒ M¨«O|ç>„5«ë0„Bˆÿ™$5„B!®0;ZËÉ#ûê: Qè;X0ã ¶×u(BqI$©!„BÔ#Š¢ KO !D-ÓJ°í§4ŽkuŠB\S] „B!jWë”åŸáØÁ]uЍ5:ÅV1gÞr~Ü—ƒ 07Š$¾Çh&ÜŒo]‡'„’$5„B!®0{~ÞP×!ˆZ¦åmæ­>â× nŒšÐ‘(‚ãûI×ý°ë::!„øó$©!„B!ÄeÎ~òöÙ}é}× oåáúf‡.ôuoWÏó5'_ÌžËW?eP„aɃ¸õÎëHô:Ê܇ŸbmëÇyó¾¶X{ú&=õ3Ý_œÆMMÍ@ ©ÓïgZöh^Ÿ:Iœ!jˆôÔB!êEé©!jX\û«ˆŽK¨ë0D-27Š&ˆBÒÖ¦’e¿ˆß1ö£,ù×K,ÊlÅMSžá™G†yh¯þw#9¦0’[{Qrp7Ù•üý»9C6»¡Ø3IK·œÔ†Ih!j$5„BˆzD$¥!jÚ¾_àè¾íu†¨EÆ&xàÎnxlüÿÛ£LûðRO–]ð÷MéÞ¥|•Î Œ£oB â’sǸ¶h»¾cW…èÎ-0gþÊþ ôbn; f8¾õ¥€zf;rýHHn‚¹6/TqÅ‘é'B!„B\î }ïå•n£Øûã:V¯ZÌ´•ŸÒâºG˜<ª žgíì$7=ƒRN0ÿá[™Ö¶&dkø´ìN¬ñ=~:XL_¯£ü”Ä€‘álZ¾• [¡{·rÒ»·D[jó*…W Ij!„õ‰L?µ ¶]'4ÕɡݿÔu(¢–)Á´î5’Ö=‡2xÉs<ùù»,ëü2£ÏÎj¸––VZrÛ3wÖ63þ¡FCkz4W™³ñ ¹þ[ÙgnǃWÅ“¹d.[ŽåÒrÓa,ñÉ±ÖæÕ !®D’ÔB!„¸Âܱ¥®CuM±•OÐçßp$×Ay©†¦˜h‰UßÍÁ²ú·ðBùÍhß§9³æ®bGÎVwA·¦…|¶ú;Žì7“ø`Ës*@„¢úIRãÿÙ»ïø(Ê­ã¿Ù’ÞC5ôÞ¡‚4) *J“¢¢W¬ˆÀkA Š Š "E)‚ô"B‡„N€@€ô²mæýcwÓ)YBÎ÷~r³;3;{fw‰;gÎs!„B!îq¦ã‹™¶ÆBµ:U(åKäŠ5\Q*ѵŒ;:|”Lb¶Fr²Bs*ÕzŒÎew²dÒ¼{u¦Ai¬ ç¹âß”öõÑ£#0¼=ÕgLcñ/Z¼YuÛTbÚwK¸ìÝ‚·ªå— BˆÛK’B!Ä]D…ŠÂP©VCtzƒTl6ůĿX:}‰Bª5£ÏÛ}y0T¢5äÉ'j3ù÷Y,nÖˆ7T¤û¨·ñœ=?çLb¥¯R4ì^¶õѺ€† €âüÌh šc£}Ù®­]«…D’B!ÄÝDzjˆBPºB5‚BËqðŸ¿]J±§i ª6Ô„‹öþ€¢Ùkއ󗢦hYëí'´¢8Ð4 Eq$.4Åñ±°/³YÔÐP²z (¡òŸQlHRC!îmÚ„³~}TÖýûÛ„³%Ç}Q|HO Q.œŽáÂéW‡!pžªhšêÌ]8NF{¥†¦9ÏYÑ?Î2 MÎZ‹ÇÛ­jjV¥Ž½jþBÑ4TGå†^³ÉKD±!I !„¸ ¬Ï“À„†BšÍ–+y‘]®a/ÉpžœfȪöñJÖélѧiZÖ©@É–õž;*1r-s¼^ªæÜtÎÏ„M•¤—(6$©!„BQÌ”,W™Rå+³oÛ_®¥ØÓ4P4Õñ[sä1ìCLrf4T'³Î jîžEÕ¦2fÝ!þïšxõèt’Ø€ì„VVbËñžÛ‡-^!W³Õ‘rVþQHRC!„¸›HO Q.ÅžàRì W‡!pf¨ª=K3±á8qÅÙ?CCÉÕÔÙT£Ðýc,V6%)ÓÌè¶µô0Jb#EQÙ-{CQ54’Ý=VÓ@Ñ9¶“¤†(>$©!„B!„‹hö¬FŽ¡ª#¯i‡¢jšc˜IŽþ@ö )}&¥ÜÃ;uÃP>ľ(õ ¶½³0¯tæT\ƦÚøãLú¿0ªm <ŒèŠùP”¬…âhê蛡`¯êAuIQŸ:t Ù$?.Š Ij!„B3%J‡Q®Jm¢6¯tu(Åž†ªÍ~Ks4ÿÔ´¬ûöD†Š½†â¸(ïh h=é÷}÷g¡OÚ€yÉvÔt%¤&z%Íêú«úš#©±qËVš7kЇ!š-«áçn(Þ‰ gÕŽcìQöì8ö„—^qôÐpŽMÒTE› Íå徭C’B!Ä]DQiî&î¸ËÎpùÂW‡! MÕìÍ?I MEsTlØ“Ž> ŽÿÙ‡(ÿ¡J”ÀÆèŒÉXOÆt:Ó±tƒý—±oLÄpìMÒî·Ÿ ëJb2´I} ý£ÿ‡{­Êè¼Ýí‘\ÜŠù÷¯0ŸNqìË ]Ýþ¸w|C°'X.c]÷O(>Õf råÊlØ´™–-šã©×ñRÓŠø÷ÄΞŽa'Y}34TE‡Šæx}ÃO°7“Õî¥ñIBü Ij!„Bá"öj ›c¦T M³¡8ª5Õ9Üçü¯¹ç}.ày«–t6""ÐÅnBµä؉é$–i+6BÑE¡Y÷ J‚my4šæ¾J-”¸ŸÈØ nå0´„ûÓVÔñŸ`ͥƋxõi‡¶k:¿Ÿ÷H¸ä¨B)Çv6›Zµj±nýÚ<Ð = l†›¾x&6rM}ãL\(Yë4jo*«(ŽéOëeø‰(F$©!„BQ̆”¡rFìZ¿ÜÕ¡{8š:æî©¡aojOf¨ö«îYÓ¾æäµ.ÿNúâÊxu{ïzg±F®À¼u%ÖË@¶=Qh Z`ü Ó%+J‰&è ç0Ÿ¼‚¦yÙŸòb$–£Ñ@$–«¡è_é€!Ô ËéŒ: œþ’´E¢ÞDg…šÕjE¯×Ó AV­ù‹íÛá®×ñTý²ø¸é‹ßt¯YÓµæí ›£§†¢8¦ùÍÑNVf?ň$5„B!Š™„øóìZÞÕaì'óöÙOì½4TÕÑTUÁ‘ØÐWèµ½4ì#PòTmü« lÛ?"e÷t õ;ãÖ¼/^Ã`[7Š´U{QO®ÀšùÆÚ¡dÆ]@©\%i–ËæìÙXóˆjIÇÑè†â® Ê uí>lÖ›ë8ª©öç°ÙlØl6 M›6eÅŸ+éòPgÜõ ½j—ÂÛX £bCCÓ”¬$‡ý# Ú_ÍÑ,TÑ¡ÙT©Ôņ$5„Bˆ»‰Lé*D±bo£aËÕ TÕT4Å1egÎYO4ìýÈî³ñŸ™.`Þ9sä|ôí¿Ä¯ÓÜö ãÜ~2&ãñÊÆUèë–A;²«YE38¯ú«öجG²ÅÞÄ=nrçã¬V+ƒƒÁ~šÒ²eKý¶„îÝÃM¯ðXõ’xuÅ3±£ç’£"CÓ4Ð)ŽQ'öaJY£O4i*² 4g[T V««Ã¹ít®@!„Ù#¦…¸£üƒJÒ¤}7W‡!GSGÍù£‚¦Ú§îÔT4ÕþƒªÚˆª*ªªe-w6ýÏ?¶4l‡v£‚Þ×€¦¦aݸ5¤ nÕšáV*óÎÇse{Éz¼³zCÕÐLç°\}Ízö©Fo&-»RÃjµbÍqâõàƒ2wþÞÙtš?Å“f¶¡ÚnáØ‹ÔšçDZÌñ9qÞVUût¯YŸ#UÃųôŠ»HóðpÆÏœIóðpW‡rGH¥†B!D1“tõ;×.quGq†c¨‰£({Ñ‚ch‰ªd_qwöØÀ>Áq«@Ï£„=ƒOK7¬ÑG°¥f‚w%ÜÚ<ŽN;JF\&ª¦Áù¥dœë÷“C c i±ö*Œœ•"9úz8«Ösd®Þ„çÓCñ}Ò‡ôÝѨJzýaLûÏ(B5GO ƒÁ5Å©K—.|?c&ƒŸ€›^¡]Å <ô÷~ÅFÞ£Sœ½5œÃ’œ­6ÇçFçœEGzjˆlÛ¢¢9`Û¢¢\Ê!I !„B!\DÓìC/´¬™M4{ÍyÛñÛ1egv£Pg?‚œÔë@M@óí†g¯¾è éØNýMÚ7ßqÙbßÌzÓÊ-x=ß mýR¬¦¬.•9f^ɱÌyC³¡F#Q7ŸÎýñð-ÛŽ±˜öŸ+XùYžJ ½^^¯Ï•ØèÙ³'™¼öò‹|ÝNáò¸ë•{:±‘•´Êšý${MÎõЦ³ßÓt9ª| 3Rq7K°Zù#2ÒÕaÜ1’ÔB!î&ÒSCÿ ê6kÇöÕ ]J±§¡¡hÎF¡jVAu$.4ÕÙ&Tµ÷ÑpTkhY“aäï… íÌR¿½QuŽ 5>ÕRšÌ-dz†„`9FúØÖ¤çÜ4e IC×ä8 l‘_‘ùU<Iœ?:ŽõësâÄñ¬M32M¼¼:šŸ»Ö¢Q)? Åa@}ÖL®ZÖ{¯iª}š[MÅ>ÜDq&žT©ÔŇ$5„B!Š™Ô¤«wmB£cÌ÷ú{´L:/ PmŽ™Nœÿ¯€’Õ2{臢Ø{p iŽÎxÊšÕõºôAʇ€¡2OôCÙý6™W­…Û}Ïñ\Î*+W®°eË ÀÀùæã1/½‚AŸŒuçW$-Þ¦r¢ÀÑ#Äf³qîÜ9ºw{ŒÄ¤${ì1 ÌïÆ èhU./£0èÜtº›Ÿ ¦Qr$°4œ}:{~K±g9Í>'WÏQ ’Ð(Ú$©!„Âåt%Ê”!#-ÍQ‚í¸bé¸mtwÇfµbsvÃ×rŽ+'»D:çíëm“ó‹^žmrn—sû¼Ï!DQçåãGx«‡Ùúç/®åÅ¥BÃICAUm )YUöþ ŠszNpd<ígµŠ¢ekÜ–¿LWcпøah2œÀ&Ãs¯Ööv<Ë¿röň¥oï^Ôóшǯ&Oæ½Ñ£y嵡,úé:U)¯£Z£8p¥–£§†Î9LQ!w?Çmûð$~"ŠIj!„p9ƒ^—¿¿}ÚBç'_Ö£»;ªÍ†j³ÙàXŸõ.Ç—º¬Û×ÛÆ¹<Ÿmrn—sû¼ÏqÃÄIžu¹'ÿ’¥'k¥ IDATTÑ4 ^¢+Ä…+¥§&ß• âHµ7B@§wþ»×]“l²²öÑZŽÛ·GòÛ]wÑÃã6=Ëõuö¤õ“={Ò4XϳáåÈ´ØxqÒ—¼6t(C_/&Nà`‚‰Üú;ÓÝÀù)ÈJWdç3r®Ègņ$5„B¸œªi¨V+ñ±±®¥@n˜8ɳ.Wâä_’*Š¢Ø“Åä ¤ÂÞ)aã±8W‡ár™V{Òº–·J³R¤gš1è*øùú«¯xgÔ(ê׫ǂ'Q­V|Üäo¥ÂN’B!Ä”ïP!ŠwO/š¶‚MËvu(Å^ÐKŸS?<ÂÕa¸\FFoWŸL‡¨T©þhš†×æÍôëÿ6näpt ŒÁý½Ÿ$ À_’7°/jÌoïê0„¸ã$©!„ÂåE‘žB"SFº$4Ä]ÅÍÍ_‚^¯ÇÓÓc^³fÍøþûi¬ÿ{=Ý{”-Zàåå) !DIj!„(¶Ú„‡»¦„Bq7ÒétøøøØgwÉ‘°pww§]Û¶4kÚ EQðööÂh4º0R!Šžð6mˆZ¿ÞÕaÜ1’ÔBár®ªÔ„†(® F7ZvéËú%3]Šâ/]tuEFR¢ÉÕ!QäÜË ¤†B!D±cµ˜%¡q)W¾‚«C÷“)“ ç‹Fóm!n•$5„B¸œôÔBg&S¦«CBˆ"KwãM„Bˆ;LQ@’BNO»îÏ»:Œ»Bx›6®A!Ä-¤†B!D1£ª6Ö-šîê0î ÷úXs!„¸×IRC!„ËÉð!„Bq3$©!„Âõdø‰…®}A¹¦ÎB!Š"Ij!\îþ6á®A¸˜HJCˆÂµvá÷R!%„¢È“¤†Â嶬ruÂÕ¤RC!„.ÔF.²Y’ÔBárR©!Dákûø³è FW‡!î2–‹[ùõç“YfE1³^.²Y’ÔBázR©!D¡ûû·ج–B}Nnx÷³œ[Ï’?#¹h¾ý“­I§Øy˜Ûmßµ¢“¤†B—“ÙO„(d¸áÝAMØÌ¸gúÓ÷ÝÕ\*´ƒ„íÓøtÊJÎn.Mq“¤†B!D1ôÀ£Oãæîáê0D¡³pníjÞèN,ã&W$„·Äàê„B~"DáÛ¸t–«C. ¥eéš+Tj$Í6~̂ߢxbD3ürÍî{Ž•_ gzôE2_*4ëÊ3Ï>DMÇõPÍLÜö_ùqÁ:\2¡÷«È}<Ã3UÃGl‰ìžÿ= ¶áL‚ ð¦ÞàyÀ¼‹OžïoßOp>šÐ‡ŠÒÚEq $©!„Âå¤Q¨B•¤Ýˈ¤ ¯ß_ƒJAøõËåloLç’úÛYÐʶa@—òx&`ÅÜyŒ›`äóQ Õk$ï™Îû_o'°ýS¼Ñ´$™GW1{ÎX>U?á½®e0ªÉÄü³—‹¥ºóÊàø©håýÐEúZ<7ê)ª{€bô§”œ!n‘ ?BqÇ„·iCx›67ÞP*5„(t-»ôÅÃËÇÕaˆÂd»ÄæGðnÙ‰š^:|ë>LK¿“üùw,¹Û\Tä¡^]y q8MÚ÷ã!£ÿ`]¬lñl]¸…ÔêxcÀƒDÔ­ÏýÝ_ah;_Ž/[škÖ¯°DÔ¯Mðê;²z/J†…FùÒþsUˆ!Š»›i(-I !„.'B…(|›WÌ%3=ÕÕaˆBd‰ÝÀš³%iß®nî•èÐ6”øõqâºS¸*xWj@i.s!ÍÇóP*¼Yg„5ª„!õ$Çdj!Ä-¸™†ÒRð%„⎉Z¿ÞÕ!!„ “k6¯%óëÈüšk]2+~£¡W#DÑq*6ÎÕ!ˆë0$^¼ˆj³çeuz}ÖÁhÌu?×N—+Éa+H2$Ïr¹*'„¤Q¨®³o‡«C®T›Ã3øü×ò|ðd5<•ü6ÒÈ8½ž93cSôU,Æ`ª·zœgûµ¦‚‡X¹¸iß/ÛE̹$,@À}Ã÷@"«ß{ŽÕ„ñô—cé¨05õ $$aƃÐyfPwÂõ…tàBˆ{• ùÊ•›zpÎ$‡>OÂ#+)âáqÍrçš–+Ùa+@"DªEŠ7]P[^ó ¦ï^`êÁLW‡s{èKÐäé¡´¸üß,;…´Ðº³tMé5¨©ó?eù³«Ã¹®¢çm#•BQ(”Ò: ˆ±'òC•y©iÀ5ãÑmW62éýéDWê³Ãë㟼—¥³~`ÌUŸ kI°ÞFÂÁJ®Mÿ¡í©èe%ÓX OåàÃ}CFÐ-Ì:w‚‚ôhßoÉÆÝéÕ°$†Äƒ¬˜½”‰ß–æË7[$â…·à–ŠÎœÉ…›¥8“Žßú|*Bnwµˆ¦ihªŠ93S*EŠ o]ÚvnOÌÜ"ÕW‚x`â6&TŸÍSO|È‹a­KÚÂÖèd²RrúPš?ý®[ÌwËN¹0àܲԻ¯ê¾5¸ZȽ<óæòå´ÝÖƒî£6“vcÑ—hCÿÁOñÏ_Ÿ³üÌ­Ç–zKÑ\ß-ÇYÄH¥†…¯rÆ ©œ8´ÛÕ¡ˆB¥Ã¿Ñ†=ú£¿ûŽº•FÐÖ'çz gWÿÆ>]Ã^’ƾ P›*^q¼öå"þ:׌ÞaŽMýªÐ¨Q]J9¾–YNð+]ž°0·¬=Ú¿ó¸Q&¢Mêyµ(“Åëó¶r2£AÞwü …÷0—ޤÓT›ªÞRC¢ÿZ-¢70º»çŠ!WÄñ[Ë™ɱN»Î¶’ ¹]tø…dÄ[¯Ð±Q9<Süav.Ãè «Hpuxÿ‰…ä³Ñœ0œ#]<Âyqê\j~ׄ9“7÷SwþBÈøFôúñ$Ùí¸¼iúÅa~ÿžÆ} øÝ* bü÷O²¡{x >m²éû²LzðfžÎQsâÿ3#gâù^]úÌ;wãc·¥páÄQNž·—´ÞL,7%¿×ÕP–Îã×1¾ýa>íч¹§sÇ&n©Ô¢Ð8éê„«(T}â5úz‡_¯¢úe²×iéœ=rÊõ ªslŠ‚oÕʱ›Ã±haîùî¶àôø•)Ñ–HR¦ ÞRª!„¸yE¾=ЭT‹(ŠrMµˆó·’c¨LVbD§C¹Î¶hÚ “]^œéBç“ÙŸÒâÂB¦¼µœ˜DAUšPGw…´¢öÒh)DM|„ÞÎûFWSĘòÓ Öüä¸{«_nš>”¶cV0¾ÃI&÷íÏܘ 4rÇ&n™ýD! ™±,½ØŸ¨7g3åÏ^¸Xâ6ý©Vtzth·v‘G!(æSºjŽžV³sf&™ii¤§¤š˜HÊÕ«$ÅǓǕóç‰?{–‹§Owâç#öèQÎ>Ì©8¹o§$öèQâNžäòÙ³\½x‘ä+WHOIÁœ‘j³¡S îîxx{ãH@H%Ê–¥T… ”©V°Zµ¨Ü ëÕ£BíÚ”¯Y“²ÕªQºJB+V$¤|yJ”-KPéÒ”,‰_p0>xùùáé・§'Fwwôƒ=ÑRyTîL¸Ç~û5¾_ø;ëÿúÅSßfì·‘¤çÝØ³1ïlM`Óøûñr.Ó—§×’výØ•C9:[ÅòíçÙ“À¾˜«lZñÏFfð/ª<ñ936\`_L»·®áý5¯Ó4Ë—û&a߆1Ôtžeû´eÂÞ<†½òRGÈ«Ù{äw:y1þ$ûþ|°éÃÊ#v²;&}1 Ìèµ<䉟Ys(}1W®óV¸…Ñ~ÄB–G%°/æ2—Nç©pçøY=÷ gÒªÓö×(ú K?{8ÇØÖPú,ºàxý6Ó¯ÜíɃêÚ1bö?¬Ý{űï³,Ÿþ.­J:öïV‡WÿN`í»r$4òE_¢5ƒ¾Þ¦˜öÅœfé´7i’§±Ìü6}ÛcØ}Œù=AhA‚Ô‡Òöý•|ùXz²JV®ë†q*„uÇ´uçØ“Àžíëù书ø;ß+½ãó¾ÃùųvÞú?3ޝ–G³+&}ûvóÍË- (£·„w^ÅšáT­×ÔÕaÒ‡¶å…µ¸°teŸŠåjCl$ÇS ”c»ˆ%„šå=É÷k <1b&Õ$©Š»Å·_MbèÃå‹þ•l!þ…|¾o“¬¡4Ö[Ÿ—ûz!Y$Ρ5nn×V–äø­(Šý$LÓ°åR£ªöÛ9çY~Ýew¸šÄ|éèÁý=;QîÀb3ÿår@ÆþÚ‘Lï¦æ¾…#&À§.­«B̸(’uþToÑ”èþÃÒ½ªÒö¥y}š•˜vÙœ¤#¸Ã~ú´{&fÀÖDÊvý€÷?^HƱ¦|•7’ÊÑÕ{°=Ü’:zŽÄÙpkM/nU ïÙgHÆ›ªmj¡Ãþ$Òù„}~V_†ýz+*i±)YËMÑó™0m3ñº0Ú¼ô ¯OûÜçõ_ÅàŽ»»;Ùç¨õ9¾j(~4~{_t;ÃôwaÝ…`Z¾ò#¦Oàt»l´ÜÇ_¿CÃïðò›ÛHp/K˜ÇRTgÆ3UÃ{òýÑLP3¸w›æ÷ªH“ûªreê`Þßž€±lž~{_~~Œ‡žÇ¥|”O,u¤/ÃÈOö’ì^>}@Ëú%0ür‹çâÔØv2³¿èÆÅ_ÞâÕ?OáÝøF¾õßêš3à‡ÌzÇçýÈX†M$=°)ýǼÃÈF1,ýì-^?’Fȃïòîk3º½ïG^“&t-~"D¡;u$ÊÕ!—ÓS¢åó<µe$?tª4Öñqê­ù)_þÂ3ÔÇ/i/Kgí†ÿ£CY#`Êwo† *TôJ'ò×¥ly¼îIWp«ÓŠº…v<"¯ïZ4ªˆá³Ü¦oqBÜu$©qRUn±×ŠbOr8ª6r&>œÉ‘¬ßΤ‰Ñ˜õ%϶Y·õú¬*—¼É‘ü–å—L±˜Lhª šfoÞêø±žšÆÈQuùrÌüñÐ[l^ø#ófÿÌ–“©ù”'¦qø÷¤wíJërsä¸Ê¨í~œ‘—Pñ 5æo6l܉u쌭À¿õ¡S5O6G•¢ë«ÝЭèÁÛß­%EƒÝG†R­ãzºDeÏUDiÕx ‘gæo&ÞF¾IÌ‹1D9–ýG­äËX¹v7&`×…*´ù­/ªy²%JÁÇÛh¿*¢©˜Ó’qÎûReÄ6¶ÈçIbí¿tÁÒ;”o=ÈWË.¢‡F•¦ýº÷x¢¾÷êW¬cÇž#˜ØÃ~Ç.ìf®žKÍj±KŽ^g¬•¤#Kùaør¯žÉò)/ñxÕ™|eJÁŒ'^¯81‹$–.üj´o€1éo\¼›’!„BQx†O ¦ÍÃùg[© ÒΦ¸ñ¬ÿŒéïÁÞm»8s9]`-ÚD)u3g ¦Ç‘¤ySÿÑ.Ô:¼ˆÃ VLGfðó¡!¼ñÉ8tÉóùòൠÉÕæ¸Zn³b͈fÉ×+xö‹±LþЩ¿ï"^  |5ovÿ<‡ã×k#±w‹¿Æ«Ï—äÂôÑì‹=í·9ÄMob™5r —¯šQo2-¨fLé阵cì>fáñdžÒ{ï ¢ue)¿Š?èPEoÄÍÃMQÐy{ ôÞþø貆ç8,šOFô¾%ð NÆâHiš'n èÜñô÷Çü…Kâøjð&é?ã·g1y•£RàQ–-ÚE²¡]zGrð0—ÌÞTn^/’¹’¡`»Å‘?Ú½þ½™Ì²dnÏ¡F¿³qR;<ÀÌS¹O¦­gæ3í¯a|úÖr&‡L`ÙžËxV„¯´F¿ç溹ޖ‹»í± }ƒ‡§8cù…­;'2çø£ úzɾcóÉTÜB«Qêòï,ØGZäçLßÿ8C¿û•ŒÏ¾fãñÜkÔï€Ï«&maÒ³O¸xO?IlÏØk¾ñãrœe?neÐ_ðùH7¦¯;ƒ[Ít,ücßä†qZO³â‹_<õ3&ÖóÍÊÓx7þ#ž æÀ§Ù›NVo–¢J*5„(|e+×Â/°‡wmru(¢ôí׿@ÛÍóóŽD¦¦%®ŽBˆ;ë®Kj4mÎäñ3yuäÖýéêpD¡Ò¡×âI)9çÇ¥„;@*ç÷,á‹gÞeþ ªn5“¾ÚÈgCÆ3äÏ?yuC2XO±ì«?yñÛ®$OŸÁ¡·O°¿b0ýt£xó•Wù°‹ ™\Þ÷3£Íãxæu¦#³œäY»xuL,9b¯~8»ŒßŽf˜a‹Žd Ú¬ØT@³a1™0[ϰrÔ0šNþ—&tËy¶NØÄï;Ó°j ™RIMLÄ(Š£¢$ù I—“s?¿¢ d$aÕÀ–žDzr2VE±Wv(`µi Ù°™ÍØ,WØñI7†_ù€!=?æ³ç `»ÊñU°aù~ÌU ä]aŸ>Ö|i+>ÅÊ4_üÜòýdz)?ü5>ùñuÔĽÌ±™K¡^ ¥’b À/ØþeW‘d°éƒ'}ñ]žëõ)ŸÔAúYöüö£¿œÃE£îFÐy¸¡:£;Fw š¦a0èÞ€Nï¨ÈPì'»¤nb›Ó(=ÆËÕHf ZÄÖ¸½|ÓïQRG¡ÿÛ?ÑÏ ´”ãlœü7‹6Åa3eæ³IóCž9‹¾ž€-‰óûÖp0¡`CÔø?øèŸPû÷7™øëéöáå‚~À¬œ[ð4ÿóûœ‘Oçëçk dò¬®ŸTù—„KÖííÜﭢД®X 2ܹÞÕ¡!îÖ~'Yºõ‚$4Ä=í®KjQP†²óÖœOh`LàÈooò¿±ëI–s¢;Ëz’¹½j°€BLh@VƒÔ»ùíÍ[•’½ÌH`é:êLÕ^(X¸zt5?¼ø.3Û‡‘7á¢((:š¦åJ¾äÜÆq'÷ºü¶û·uùl‡¦P²d®¦½9›Ô^S£åib [æØ‡ó¶N¯G§ÓîçJˆbî©h.œŠvuBˆ;hÉ'ï°ôŒ T÷6Ijˆ"Ëzz*OÕžêê0ŠÕ*Ùþ|h×­x°rzÁót_Pø1Ý EQHŒ¿îp¢ü† ýëð¢ë-sìC§(àHÜh96B!„¸e‹wÿ×^`B=€èÃ]‡B!„÷<›Í†ÕjÅd6“ž–wu¢(êæÏ›ƒ¯¯/>Þ>xy{áîæ†Á`@ïlÊ.D·_”=©Q©juWÇ"„B!Ä=Ïf³a±XÈÌÌ$55•sgd¾Mqç”)†¿¿¿=±áデ‡F£Q’âž‘•ÔÈHOsu,B!„BÜóœI “ÉDffç¡/Ò´ŒS¬[²[«^t,gtu8ÅŠ)3ƒL7#F£ƒ^‡¦Ú°JRCÜct®@!„BQôiæ+Û³›ãÉjîåéÇX»|-¤´âö“F¡B!„BÜó4ÒŽ­aöœ숾‚ 0”£NËÞ îÕ€ÛpáÞza “?ßL£&RÅÏíÖw(„ I !„B!îqjâv¦Œ›ÅÞàôܘò>Éçb8¡ùá!#„E˜$5„B!„¸Ç™/ì!ÚìKëAÏÓ­†»}aD3ÚæØFM9Ê?ÍfùŽS$«î„ÖkGïg{r_¨ÎÎó…uß¾?0ŸdÖ°wÙÕê#>ïæØK"«ß{ŽÕ„ñô—cé¨05õ $$aƃÐyfPwÂ%£r/³Ämfñê"zv¥ª§LÝ.îŒ÷Ô°&boäad(œB!„E‹1 Œ`RØ·>ŠKæ|¦‘µœcù'2÷P ‡ŒäíaOR'i5“ߟATÊ™vÖ‡û†Œá“?â“O_£ePvÒ¢dãî¼4òMF î@pôR&~»•«ê¿ìJy–óYºj7-®Ž¤˜Ó29ùÇD^þžþß;LÛz™ÛZŸÊöúÓwÔº;òïÚrr/÷{©G“84e}‡.àŒãsUÀJ  Û§ñé/!¼ùm-$¡*„B!ŠkûÿœÇ‚U;9ž`AçSކúñücõ²úPhé'ùëç™ü¶ù8‰6=•›ÓíÙ§éPÙ‹ìkÓ–+‡ùë÷lØuˆ‰exò©ZH‡¡/Õ‘Wža⌯º£ á­ÛÓ±ck”ö@2£—²ô”?¾÷OÔð Vg†Îà×mOP¯£wŸÉ€_éò„…e÷Ô°Ÿß¸Q&¢Mêyµ(“Åëó¶r2£Aݵ(|¶«ìúu³V ÞìAé†]xvðcÔõs\×Ì\Ø2—©¿üMt‚ŠOØ}<1èY:åúìÿ7ÜŸ•Ë»óãÜ5D]ÈRÔïЇ="(q›Æ ¤ïþ„Áó¼Ü½é›|ûZ] –òæðœÏçq5_›Ê{MïЇY3sq×R~^¸Ž¨³ÉØ þTjÚ•:SÍÛñJ[âùgÁ÷ÌYsˆx‹Àj-éþ\Ú†y@ÒNf/Ëà‘ yò"FMẔˆÁ8‹¶ò>×…MÓ;í$m>þ„^åóÎX¤‘qj%“>œGJŸ/Û>ØQ)¡ÃhP@uÇpÍ›oåräæü¾‘='®bƓкmèý|/î+i4Lç¶²pÞ 6í;C²MÁ§\C:õ}Žn Ðèè1ànС7êÁàŽÞñ<2üD!„B[¶Æ­A3ªûe_Á3ù“ŸV^¢æCÏñhiñ;c΢‰˜JNäÍ–è´d"§~ÄŒ˜Úô}} u¼/óÏüiÌüØJÈ/ÑÐG4R/àãÏÿÂZ¿=ônKw?ü}2¯Òñ)n”mû"Ÿ·èÉÑX»æ7Ư^@Õdž3²g5’$ÝX™ˆ0¬‡èj ËÆaéXå6£Ç¯L Œ¶D’2Uð– ïNÎü>ž Ë2hÞ÷žgͬE|ú…7ŸêH¨^#ãè\Æ}»¯¶Oóz#7b–ÍdÖ8þ†Ò<࿾¯7ÞŸåìR>ðÆÎõR¬ÇV1}æ|æ5žq–¹ '·æ´Lßæ ö0öˆ >¥p(ÑŠWÞ¯‰)«xÉJÜßSùn[ š†å—!ø¯,\ܽSÁiZ!;1¤¥añ¬M˜š<Æ =J Ænfþ¯sO9&½X/ÅÄññå*/:EË’)ìY8Æfà7áe"PPP±ZUlV+ª¢C¹&ñ ‘ynüú ‹wÆ¡Ròšèl©§Ø¾ló–ïã*P)×Z=ÞA¾x™üq»fß :› ßðn¼Ô£º„ìš³ŒÉ“‚©ôAgBõ : s‰ûèûÚ“øÛ.²gÙ\}1•Ò_ޤE€‚Þ;£%|Œxû`ôÆÛñ§:÷û®eprÍL¾_¼•S)¸…ñô[ ml_oÞÅ'Ï÷·ßîÂGúPæì>üìŽ%[/Ê4z˜ç?Jm_ØÙ=ÿ{l=™àM½Á1²u0Rì!„B!î–XVÏšƒßÈÆ¹’ä“É ç%ÁºAœËÎýqXZânŠeÇÁ Ê=Þ‡‡–BOÊ=uœ-ïlcÿ% }ÜÐ’#ù~ÒFJ ú”ÿ5ñGµX0™L¤¦¥‘Yȇ©¸‡PóÔlÕ•‡–|À¨…SYÚôcÚà±:6ëm)[WtzthÈè“»Xf +VÆðà{ êR4ªyžåµÏ—±êtž®”ɾ%ë¹Zú Þð e Q ޽6ß6_¢i×RŽgÎÛƒ˜’ Æ`j¶}’ÿõmNè5)7Ø_‰‡"9çÞ·z¶¤¶Pñ)únßʤ=§I¤ ¾iû™3i6[O\ 1S|¨Øª m‚N±qãnN$X0բÀ!ô‰ÊçœT%39ůÕªT¢TÞ ŒT¨˜u×vq5s¶&SëéQt(eT’-åû°ë|:(ž”¾ÿF¿aïCs#ZÇ–Î`qƒªDTðÊ:YW¼ë1xÂDtF½=ÑѸ^Ñ{˜sˆxk=*¨'Y³1žàNcy²e%Ü€Š/¤±÷Õé,ß“H£šðL·]Lýsåyà™×¨’w‚"5‘³~dƒ®5ÿ’ļoäÙÀÊ¥õÓ™½§_ÈÑo 9×zAU*RÙœOrIOP³~<×Ìy¿6¡Ww2rùQâ- Õƒ±t;žýˆš§Ù>f/Ç®Zhà†Î»,•*T¤¢Ÿ;•+Q)½žùUjXÎ.cÒO‘”xâUF7 BKŒ#%4 ûÍÖ×â¹QOQÝ£?¥  +ÑGÖÆ×ßKÜNü°Iój2ipM<ÔdbþÙËÅRÝyep üÔ ´òþ’ÐB!„E„CŽ/¯š)‘„L©hÿ"m ,vDî%®c)Ê­\=M¢WUê„+q±3Êþ:ŠA_%£ùW¢Q§^<Ò8ØEÇ(”¯CðÂUœNЪZ/Ëavɤcø‰-ñQ¡LÇRÑã]Âöžä²¥5ùÌØª<1b&Õ$©Š{åò¢ÓܨQû'BÁ»jS**ÿp8& [Ùöž²м!޳J_Mš”Ÿ÷ž&£K)ǰêuíOó²î¤ÿ›9¿Ná‹  |øHžÊ sÜ öŠw©Rx˜bØBÝú¾~Š}q e,§šé"‡]ÀÿÑ—x¡®¦³˜5{>3›Ð³÷+ôòMçÐÒYúõ jLFß¼™éWÓPuÉW“ öÃýz'Z2Q¿,$&¨3ãZ…ØÏq32wÒB¢köcÄàšøZ®pц×-÷HUìC.²ÂÌàr’CÉrø€ÌL’3ÁÓß3ë\[çFÕ­Ç.ci]°N¯1¡Ó¿<….–oN¡•¢`>þó®ÙÀ@é.cø¶«%s?¯9&=%;ŒäíŠf%åì?¬Ú_Ã')Íß s 6¯ÜCjHSš–vd¿ÊUkMÊ8{…5 ¢~ed¶j!„BQdÙ®°mæLötbT›PûɃ¡4‡ôáÐG³ùF$÷Õ°r`—™NÞ£¡¯ZÇvƒ’-ißçª鸴s>ßÍ™@’ñ-/s£'½=LÇ3m…juªPÊß 5%–Èk¸¢T¢kw<‚㑊ۙ?i ý;PÝãÛÍ1ß–ŒlQ=v#|7¬eÚÌÒôhVOËIÎed?‡!¨ ½Ò‰üu)[¯…{ÒÜê´¢nᢸÍÔÔxÒð¡„OöÉ´â@ /8ŸŠÍ–ÊåTð ñÉq܇’~ ZÒUÒTð õ[4%ÜOº•ÑØÃWÛ“Ô¥ Á97ÜŸBhݧÒþc¾üôuŽ5Š øÂNŽWÀè.å0âìßb¤dípê×ñ€ÚåÈØ¾ï”Æ´kÙjúdó»Ûˆº`¡‰oÞ3T ³Åïø…¼?t>àOõû0¸ßý”É3¦Â·‰_wÚhørg*:wcMåj:øU®GÝjå0RéôÍ1sî¯ïùålEÿ°  àV–ð0…™ýÉî¦ýh¢'ãJI°™­Þ³r혔<èn®WŠƒíÂrÞþ gBÚ1â©Føçü ˜3køhV^txlTOj`Öœ\I ÷*]x¬Ö.æ}0Œã÷w s繯²ß¿TVhdžÙÀÜŸÿ òx‰#žŠ MXÿK“d!„B! ›–ȆÑ/3õxŽeï?Ç:jñÆ÷ïáåXngËÔ±|s°Ï¿ÿ$µ².½ª¤ÇŸ'^+C‹v(y1’C¦3ìX¿—5[RRKáR²†Gå–´i\ £ÍFùRÏÒmÇpæn:Æã½KqçiØ?¼ÿbéôe$Z<©ÖŒ>o÷åÁP=PŽGÞ…þ§Ù,ûf< 5wJÖíÈ+C{íU»?#Ÿ²ðÃo?3iƒñ ­FDyoûyÏZôÜ‘Ë3–1åÓ¥(~UydxsêÂ!Š¢Añ ¤Œ/H ÝFî¤Fh–Dâ.¤à[»TW9|Q#yÿF6ŸlFù4éT< ò€Ø«¤«à¯½o(¾d’œ™_E‘;5Ÿû„©ÏšOôÖß™þãw|¨3áÙZdŸ_[8³n5g|[0 Ü?ûDß»O<\q Þâõý­éÔ¹í•Çë_ÓʹßÞdĸìE1#yz!€-GMæÅZŽªÍÄÙ5“;û"Í^}ŸG yõ!´}qÇ'þȯ¯uìÄ~Z§àSžÞaú6 û¨—ÎaÓo ùl´ÂÛ  ®³Ù©1ŒGßK³Kç8°öWýÝØÑô¬ôïýJrwq ã‘w¾¦ñÞ¿Y±tSÞý?º½Ç»=*æÿèÌ£Ìþø6—ìÂÀaÍó±p|ñ~8}ŽX!„Bˆ;Iñ%âÅqŒËPÁtŒÙŸ.ÄgÀ<f¡Îêdëe¶|û>ߪƠ÷_¢MéìfZòn~ün3¾ý'òBÛ`ôt¦K›¹¼õÁÌjÎáÜ ü{wÕypügc†a‘}EƒØxI¨ÖPs\¢5±MmpÉBlâ!´j›cbŠZǤÕOÚcLLNÝ‚Á6n€5&k›h¢V£¶u¡ @df˜¹ý4â1*¢y~ßæÞ;ïrg¾Üç>ïóâ¬k¤Ðhü Ô¢VÕ]ÔPðMJöKCÉîà*£^˜Ç¨®ÕŒ/=‡çðêðœk\ %8m¿M›tÙñ¡ÌÏÚîˆ1e*Ëò¿áðÅm¡1‡àG=Õ _?ü«®ZªÁjF«ub†Šªz<´f.yê±_PQ,Á׬ÿªè4àõrÅ;p­_'í¹)/^LÁ™tf½žE’FŽÆŽ3XúÎxu,W&>)è Ú–þÔ¶þ hQñª¿…×Cé“9‘É¥û˜³g7eúÒ»-#ÃušOþQC`úl—®bP|éóÄ\–dìcû¦B6,z™õý&2ïÅaD]^Cä뉖9ùœ¨jÞ^Àö¤¦>ŠVÑááÓvó)ߺˆ9«ì¤ÿr6ÙiAí’ô‘ƒ™² ƒ§ç¨köÁT³ž—çî¦gr×캫éÌ„[ ·&’œ ãÔô÷ÙpèÇô»ßÒRôFÚŒ´‘Ô7š‹¿Î£¤ø£s“1vÐì•5ÅHä€<›·Y[(ÝZÂI§½¯Ü\¼$ÃS[Êñ:=)ceP¿¬Öla¦›>w!„B!n>-æ+6› [|fKT|ËgkxëÛUÿݸ·Æ“=»}@ ÙQJY“/QÑm)ó ¾q)Ä\TmÀ« ÀmD-;@Y[UÐæjNÚ=ø„…wél…¸ú¾ôösqì‹2œ¨4žØÃ)5ˆäÄ´†HúÇë8ø0Õ­+¼uÿæó ˆNµò V ´×i{nìÿ9‹Oh[pALR¿8_ŽãfT°½ U…Ë×\¸+÷±ÿ¼™”ôh®Ì!ÐbŽKcÌ”¹,š–†rx3;*Üô  ˆ"ÞfÃf‹#̤Á'(¦ås|,!Æ–”®æw«Êø‹<²Ó®Vä´¥oS`(aæs|üÞGœ{ˆ‘‰Ýôù\Q/Í,ñPosç?l»L檽|xb­=0º«8tºLA˜´ ~1½ VKX÷ÁN|Z¨·{HL‹ÃêçfïÆBviî%Ú쥬Æy£ÓB!„¢[PëöSPTNøðqX/–SZÚzBg&2&cX )…ì\^@ÒøÁÄ/plû*>w…1*9-zø!{¶°te/žÉŒäÜî•”Ø{ðà @Wíé*Äu2&òÈðv­]Ÿ"Ç“t†W|†3q"ìPô¤ŽBð¼µü1ßÂO8^¼‚£ú{Éý~Øõo¡øwÒžJ\Z/´Ëÿ²bF§öÀ[¹õ…gðIzŒÐpcSö:þIÑöJBlÑè›°ÙÁ×Ùv ?[,Aú&*ŽTÒ„/& ž³›™9=SΛÌ~Ðr}µ)¢ú‡aÔ€ìÑ$„B!îÆ¦¿³øŠÃ.ûQN»ÁQô:yE—^ŸÎÌŹÜcìęϡ[¾Žåó·áL÷0"÷i~ÒZ=ÐØûg¼’£²´`%ów¸ÐôbðäiŒ´ùPkïšéu&kü„ϯί‹F"º=qc^äWÎ¥¬x»Ü>D|o,3&%\  `ê“Å+Ï«¼U°Œ[=øÆ>À¤™Osà·©âÐy{¡™S™Ñ°‚üÍ`Îè°Ý—ÅKO¦¨¹ñÇO¯ÓAå¡6®«¢ ÐÄ‘:f*KjWOã܉jH#ÔØ>4¡^¬âøî5|”/ NdÈäl~¦E=ã¤#AõŠ…Œ9Ëȸü¸«’Cå*wóó.=ÌÈy ™`ÓS»÷MòÞ=…_¸äûžeÎèAí¶§¾­¼Nö±µ¨{£ Z3QÉ™üü¹ÇÔC ¸q:*Ø¿©˜µ5N@O@\ £s³›jî4¤¬]“¯><ü‘.˜‰B!„ßm·ÛÓ餾¡ûWô0°ó/ qø‚&—‹Å‚¿Å‚ÙÏôz=Zm7yØý¹xè rÔñÔ¿apÐ ïñ*Zm+ÙÔm ¡ !„B!„w!7gT`HÆ€@ hÜlºÎ/B!„B!Ä·£'~Ük,ö(h%¦qÓIPC!„B!„¸¥4ȪŸ[C–Ÿ!„B!„âŽ$™B!„BÜFUö³·{BqÇÒAKÅP!„B!Ä­åõzñx<¸\nšššHî—LL¬õvKÜeœÎ&¾:SΞOÿŽŸ¯/&“ £ÑˆÁвó‰F# ûâîñ?0ñëíJ1ñSIEND®B`‚libindi/libs/indibase/alignment/controlpanel1.png0000664000175000017500000015525513263645557021463 0ustar jasemjasem‰PNG  IHDRf’¹¡ÈEsBIT|dˆtEXtSoftwaregnome-screenshotï¿> IDATxœìÝwœEÚÀñ_wOÜœa—¼d–œ”¬„3‚˜Àp§b:s¼3çœN=ÃÝ©`T‚"` */QÉ9§…Í»zºûýcf#›YXÃóå3îìLuUuMŸ~¶B+»wí´¼^/^¯ŸÏ‹ÇãáÁ@!„B!DÝÛ¿6ÅEQQ,»jÇæñxðx½x ¸ï¾»HIn­¸ÀjæW½£ª‘ªFùU#ë$Û¿LÞVÝdVœ£Uê·:Ê«Üwë0¿šå}r§XÉwé$? «ü/W-ó*ïÅÚgX~~U¦¨~^µ{£òR«]¥jþ_¬Íg•ó¬nò«à:É«l¢|—jðЯÇQÃ4°-ÞœA‡Dxàþ»‰ÀãõV7[!„B!„5PwÑ öýr¶[1ù¥g‰‰‰ÆªË?3 !„B!„¨PLL4t…}kÒQ/ïYßõB!„Bˆ?/”ýûXÑÑQµ:Þ²,LËÄ2ƒó€k:âfYŠ¢üikú©„¦SÊY{Tn>–Š‚¢€‚Ц©(Š‚ªªæ#„B!„§[vv6 V•i™X†‰®ë ðŠMS°iv›­ÜM óPB?-ËD× LÃÀ4MLËB Qª¦a·k ¨Á5àe‚*Ë ­ ·ÌjäcCÕ445¤Ipö[b¢{uüÍíÆ¥Õw}*ó{ª«B!„ø=°Õô˲0 = ãóú8¿U ½ÅÑ<&’nŽäyؕˊÌØ~—ˉÍf;!B#\=€Ïï§yóæ4j”B||±±1dffqüø18ÈîÝ»q:Øì¶àÈÅ#h(Áü>ŸŸ””ZµjILl,‘äåå‘••ÍŽ;Ø¿?N‡v4-x5-ÁY-X&†a¦¡ÕIó™è>É{Ó¡i$é?~Ëš|ûo4àù=ÕU!„Bü^(ú÷±"##ª•ز,LÓÄï×It¨ &Ù¹äé%F)M³át9 sÙP•Jʪ¤^Þ/_Ýæ­h6N'aö:  ÃGV¾øä8lùXêÉçmédå§r÷{wÒÝ–Ãwßˤ£."íe2¶Êo;›ÍŽÓí"Ì©ž’µkUW!„B!ª)77¯fSMËD×4rj¼6¨nKGÏÊ­0}‡‹Nãæùë9j°Ù‚#^¦i0 œ'gŸ=ͦ——\§fÖÅ¢*((„G¸qÎ9Ì›÷-CÇf³£ª¡·€¦iœ}öpN;ùùÁ|° Ï«8ŸÈ¨Î>ûlæÎ‡? ãP4M­~‹Yùy±\ðÊë\”§þ“{æf¡ñ¡×,öÎz–‡¿Ø3Ò¾|üîåó[[CÁ2¹ùì$¡<É„‘ ²èyÞ³•Õ‹¿cæÂø#ÃpR^Y¹D†•|>²ýIô=ÿ\FöK£U¢“ü#;ùå§yL³£‚3S'ÛèÊÃ^O;Ž0åî‡ù&ßMØÉŽ•w[Ë*}{ËÄ“Á >ÇÕí\ůûsØ¿íW–̙Ŝ ù„•ŠO‰êÔU!„BˆªÑTFË„€îçá¾­°ågàóy*? ?»+ŒG»5⺟vb MÄ4Ñý~ž5Ë à)„.n‹…kÀE!бÛôë×›~XˆM ®93M¿ÏÇà!ƒPUðxªÈ'€ÝngÀ€¾EùÔ(0«’BÓóîà¶Ãóâÿyˆ8¡u-LÓFTBqPfZ Ú#hت;£[ugÄàÙ<úôW´Û«W¤é'WéÈÄçnf`BÉ7T´¢ß_™6s=–ÀÄ_à%ß«0AQ5œn7án À2ðäyðè&F(@.LáÖP°°µ8 Ä$?3›|ÍE\¬Í2ñxðø E£v¢cÜØ•*Ê®’…iÙˆŠ+ÊL TGÓúsYZ/ÒÞ{gg«¯ Ùí„…»pÙ”jœ_ñyù ¼ä{tV(M˜›W%#–B!„Bœ„j˜Y:#†“d gep´ÀÏ1N‡øð¢´[3 ˆ°k¤D81<¹4Œˆadrß÷£i6t#@£F)¸\. <ÚÑÑ4Í¢ÑE¡hEEQÐõnw)RH?r‡æÀÐ 6lHDxx(X°`@ Üs>|(º®åsôèQlV bÓòÚé„×\t»þvÆîŠÏ÷£’ìNý'w~yÓHÇ¡—qÛønD·ÍcáöOö—W2Ÿ•…/ßIÛ¯ eGYôþ$f®ÞKfÀEbó¶´T¶í´áÄ  ×N«‘×rÅȤFkè;øé«)Lš¿[¸ Ÿ/Ž¡7^ŰöHŽv¢zÆN–ÎúˆÁá*Y—\òâ{\°÷Sn~pyŽ8z_ò7ÎéÞŠIáh€wÿ<õÈv"h[aÙå…feGÌJ·saÛÓ–‹î½—q©:Ÿ;Œa-Zq^¿f$„i€—£›—ñÅÿ¦±,K#¾òó ³¡`âÉsÐæ/W1~DwšE*øocÉô™üc:ΰò>I1B!„''˜U/±0˜è&ŸƒÐ8â5xyõ>îêšL‡¸0¶fyxaÍAnéÞ”†®`š@~ƒÝÌ9âAUm“ä”d<†a¬ƒY4ÊU¨x´+xánš&))É:x»à %¥!^o0ŸÂC‡rB> ,F×u%XVãF):x¨fÔV¹3ÙJ½fäøÐ¢R¸ðÎKÙtχl«"KÍA´«€ísßãåFÏñØàHâú¢Ù§°³laV™:˜:¾Èžœ×Í ÀîÉ/óï9D„Ù°ãáøŽÕAÃeWðçY¤Nx‡†ÅåekÉ «þA«¨Çx`æaJ4{¦’b,?>Ó3.•Wß…rð>ÞÙi–cÕs³È €?½Sµð œ1¬ ­l:¾€—šMŽî õ••—m•вÊôËrÚ^s‡ãôì`ɲÌKMÈxÂ#ˆ·ûÉÍ6qE‡“Ôn7ÞçgÏ]ÓØ]Õùí2qê-ÆÿƒäåDÄ·fèµë{€—~öžØÊÖU!„Bˆ ]fW}U© `M\ f~–iÐ1)†{ÎÈ _-dLó¦ïÎæ¶QgÒIÉÁÌÍÀ4 ‡»1 #¸›¢iŽßï –^檶ì}É xœa n2X¦Axx8~¿¿T§T>… #úiʧ:ç^¾y“ïÓná¯ÎäŽë7òЊªŽ°@Qq¹¼ì^µcpZD2 ];N˜)ZxŸ·Ðo¦šÐ‚$޲ê× ìnÁÙ™ 6Õü€M?¾è3™0,ð°òßðòâlŒ¼›—®lMã¿\D—o^geÑ@cß=rïìŒaôãÏqMj8=Îj‚ºy'fQ`v„/{Ï¨š“¨ Å[âø‡ïá­:šËs&·U»ì’çZ9Ó§£4îΨA)Á²wóo¾Å熈èHÂãzrÇ“—Ð2>1Ÿ³+»êóóEŸÉ„áñÀA>»ï1¾Øc4ì~þum+ºîAôòEdœ°¦N¢2!„Bqrª¿ùGh³0K/ ¨Ì¬tÚFêŒí݉~ü…ñý:“ffÈÉ(>VQp™zш˜Úæ¾pÊ¡¢(,]º¼Â¢ûõëSt¬Ó¥aZ`†/]׋ò){>%ŸGÕ‚¿;¡|j2ÔQ©Œú¾ÿ×Û´zñVÎêu 79óJ&./Ó๨JѦòke•9·rÒ–³… 6ïHÏz¾Y‘+ÒÎáç³ýÊÖ´µ7¥cJ¥àP²Ù²%Rc @3-JÇO*®n °Œ2uUpF†fzð¦Ô´ì๕úlÊœTʸ§øt\ÉW<¬œµ™&—=À#[Y*µƒ0GÙ6-çü R:Ñ4X—>÷—–<$¶1ªE¥PW!„B!j¨ÚSU+8­0×ãÃioæ\hóÞƒ|¹9›ËÏèÌŒUiÞ6šöÑÅ««U%Ïã NKTƒùx ¼X¡‹yEQ4謢ue… ×›y<L+8Jæ)æC(­×ëCQ,@AU‹³ò¶Á7MÓ Ž:ù|¾Ð¨\ͬ‚Ù…¥yå¯Í§ÃCCHíWùñ˜†×ã&­_óàF9û8ä=±þVÙ)sŠŠ™±L Š$º§Åòù9Ø]ª©4L•Rû›®zc<ŠEÀo•U:hTP•uª`ާU۲˙XÞqzþ1ömý•ÅßÌf©koŒl…ÃØÇŸ}Ï&_cλf8MCåVy~ŠªÚŒä8Ëç®äH‰HÔÌÝF®ª–Û $.B!„'£ÚSQ,TUc[®—4ÍÄ4‚£TÛs¼²5—¿ëMG§Ÿ¶#Îà…9K¹­Mí¢‚; ªŠy>ÔЮŒªª’™™Edd8–e¢ª*¨jñŠE£k¦YôP•ÜÜÜP>ÁA²³sBùX¨ªJpçC3ôi‰Áéqaî0nÖŽöþ£èéÇiÃÝçôÅytV  x°æ`Á±{**6›cÇÒ wcŠb¢ª&š¦ž˜o|Øl6Ž=Š]³•›OðÞf°lÙÊrÏ!Ð1M«(›Í^Ã)hUmýQü’Íe°}êù¡Ï= )?·”qOñÙ¸Ò¯¬ŸÊK3bsj|e(;õRÁáÈä»·¿àŒ'ÇÐÚ–ÂÐkïcèµ%’xVñôßßaSÖr>žÿC¯›žçÓ)F:4g:k}*j™ØâĦQ°ö³b‡I·6zÜñ*“ tìž%ÁòÌà½ÑT,…;w ª¦Ôbq‰7χÏÕé"Ò©”zMs¹ˆp”X'§ûÉõXŠFx„›bá/ðá ”(7t¿6»Ã†Ó®– Ê+«|–ià÷ðÌÐÍ•ƒkû4›†ËiCS,>¯ßÀ°EÅî°ãv†Ê´ òsýPp„7õ0|^ò|h¢ÂC7X6 <ÑM¤m„…Ù±qâñ%jXyÙ€eðxè8Ã]¸NØý°ªö°0ü:_¨Œ¢¶Õp¹ƒVµÎ¯l]Cmép9‚7ª®V]…B!„¨¿_f…SuØt݇Óï庨z8}`†v; ÝZQP5Vûœ¼—…ÏáÆé Þk«p*ži7à0Á{‘………Ux³‚‚:Œ¦©8]®R{˜¦…ßçÃ0LRRâv»‹Ö”SBÛíìßÓ4±Ûí¡i”2ýL!„BQt=˜Ùl¶ªS—àt: x<^zÛóèöÐÒ¦©šäš*;vyܬÐ#s»°• üŠ‚3ËB×ýø¼>"""ˆˆ Çår¡(*–eâõzÉËÍ'??§Ã…Ýa/ ¤JÞç¬d>ÑÑQ„G„AKÅÂï÷“Ÿ—Ovv§›ª¡T°{£B!„Bœ.@ 00«ù<,›Ý†ªÚè:º®cš¦i¡ªÁ©cv»»Ý^aàSTYVpú¡? cèàš²PÀ¤©šÝ†ÃfGQU ³*»¥~Q>–_Сé‹ÁUUÅfÓ°;옆‰aJP&„B!„ømŒêoþQ–î (šfÃív—Øl£z G¼TUÁRTœªË^xï3«(¯ÂÍ@J®C«(ÓÔp:4œv{ÑDÆà¬FÃÝ_r=\Íïa&„B!„§Bµ·Ë/Oð>c~ôŠ÷ÿB!„BQ…êß`Z!„B!Ä)Q멌B!„B!êÆIMeB!„Bqòlß|3¯¾ë!„B!„J£FƒZß•B!„Bˆ?; Ì„B!„¢žI`&„B!„õL3!„B!„¨g˜ !„B!D=“ÀL!„B!ê™fB!„BQÏ$0B!„Bˆz&™B!„BÔ3[MÈÌ8Æþ}{ÉÏËÃ4ÍSQ'!ˆˆˆ¤I³æÄÆÅ×wU„B!„8¥j˜efcûÖ-tìÒø„DTUE9U5j†irüX:ë×þL«6m‰K¨ï* !„BqÊÔ(0Û»{7»t'1©–e§ªfâOMQ“Ö¹;¶m’ÀL!„Bü¡Õ(0ËÏÏ#>>K¦0ŠÓÀ²,Y·fU}WE!„BˆSªÆkÌTM“µeâ´QTUú›B!„øÃ«q`†eBœŠ"«…B!Ä_3 •eât‘ž&„B!þ j>brµ,„B!„u¨™U«3+w“^›‰:î^&´qÖ¼Øz¤ZÄÔÙôºüÚ„ÉÔºÓKþ „B!þøÔšP¸Ä¬¦Ó{ˆu?oâ`Yë<êâaúޱeÕ*¶fW¿þ} øò›•Öë¯Þ”‡ž¹“Ÿ—oàx úÇ!„BñGWãÀ ¨ýUùÉ[GÀ9¼üôÛ,:R“(ë$ϻʇIÞÏ/så˜1Üôñ.ô2ïûwLæ¶qc3&ô¸x7Ü÷<“íÂcÓø6½ÁÕc®çƒ>°,Ìôo¹wÌnx ÞrÊ+Xÿ6׎Ã?æ`¶öñÓ›<ùÊ×ìóIT&„B!D¡Ó7•ñ$¯+ÅeW¿…éNYÍt~œ¶Œ‚0ùs>ç—sï¤gdñ”IÓŸM¶NŸëïä/MT|9ÇØ±â+¦¾~/ë3_æÉó•9+‹@ÎA²c³ß燑O0ªV\^à óÞÿŽL âX.hœµk}!„B!þèj±]>•\+›ä¬ŸÎ¿ÿ3‹• @q“ræßyâ–^D–<ƲðížÁ£ÿ˜‚~Þ<2àW¼c w¿Ëƒ}"Q+w9O^ÿ"¹ ý¬gXÐþÞ½­#.À¿ã}n¹%g½ðã›Û~~þzžN¿œ7ŸEìîOxä靨–ÂhÔó\®»é":Fª¡ºg2çþ+˜@3®yó9F'iXy[™ýþûÌøi;Y†BxÓ!ÜúèDÚ°ﺊ×2=`§ÃÐ ÜteÚC礧³|ê»|4ïyT"›ŸÁ˜ë®ct›p#“UŸü›OÜÈž N—_äCÐÿî¹ÌØÞ€±adzo2íÇtºH*–,‰-ÛÑ>Õ@מ‰;|oÎ[Ì‘—ÑÐ*‘Ö‚@îrqÓ0l_|±‰A7t$¸<Î"÷—)LßAgùéùÁ²Œ ~þü=&ÍYÅþ|oÜ‹ÑW]ǘ®±h€¾û#n½g1=Ÿyƒ¿µróúéa®yUã¾¥W˜Î¹oðÚWëØw4Т[ræ%7rí°f¸ cMÿ ž˜pqðyÂy¼ðúZ¶cyýM!„Bˆ?¸ºÝ.ß»‰I/}ÆæWñÀMíˆôç°¿aŠUjÔ)pl ÿzâS2úßÅ3·"ÂÔèÿ)óWìÁÓ' 7à;ð3; 8»c+:n cöö ÑÓhj7Èܲ ÒY¿3£yªÿ ¿ìð‘4°±š…šÐƒ nH#*Æ~hŸ½=…—&wà­›Ú‡‚úýý\ÔÌŠ“¸xKßϬ§fò‘Ž\8ñ>:ÅCv†BwalCç ®¢#;¾çÃO_å…øf<{A#lV?|˜'rÁõÐ;>—5ÓþÍO¿Kò·ÑÝ™ÍÖek8œ<Ž;nlO”Y€Õ, ËÊgÃWóÉI»šázÑe`4~õ{‡^Ns{q›SÔz¡ßn§ ~Ë*“ÆÄŸ“ßÞŽ±WÛùïÛŸ°hÌãŒHÒ p€ï?ù?Ô³nã¢c¯óßãy°°[¶|ô0O &ÜÉÕÍ`Ï‚˜üÔ#xŸ~Ž ­]'ŒÊ•¬Oð™AÖöµìôuæš{†ÐÈ–Çî%ŸñÑ»ÏÖâU®ie žÖ뻆¶.P1$Û*=“¸L!„BüÔívùz.Ç ªeg:µn‚–´¥7CǘYkøß»o²¾ÍDž¹®7±* ¦Ð·g,_-[Ê>omœŽmØLnTGº&EШOì?®aköXšÆå³}ÕA°Ãþ»ð ŽÃul¿fDѹ{CìÕ†Þ=CujÕûú…<´eÇõö$Yv¢’›Ò¬©£¨êÞMÓùr{g?~'—·s—:­ ‘nýûÒ=JN©(ëVóêÒdŸÛˆ¸ÜŸ™ú}&i7>ÅåýcQ€Ôë3Xñ÷Oùa»—î‚ù„5íFÏ.-)*Õ3ógf®°èqwb‘#†“òí¾Þ|!·tt—ho ï£{ ¼9Gض|:ï¯3ˆÜ›FöŸ‰X&ùÇ °ÜiÐc£ïeÆì ¾ºƦ™|µ¿)ÜÕ™¤)Nôô,|&¸rÖ0uÎR.y™›ÎmŒèÒ>ïλ™1åÎÿG_ÜeFåJõ…’¯E¦Ò­{’5èÒ.’«Ÿdõª#ŒoÙ(˜F §A“f4+ÙÌ !„Bˆ?±º]cžÆØs›óø§wó÷µƒ1j$Ãz6%\-eó±êÍW!ìLþyó Ø srÐlP_âæ-cñÞ ´nUÀÖ5GK»†¦Npµ@+í–mËcH—],Û›ÀˆKRøñ«åìñu¥áæ ïÌÕMX˜xö,àãI_³bÛ!²t;nÅq>t«dÝK>p|ëòí©ôlæ:áüJŽ YГĔHX—Aža~t3 ƒ¬7&2î2 œáÁ,/ Ž.ûšMáýx´}`¡%ŸÅ¨ÖŸ3yö¯\™Ö‹H¥°í2˜óÐ5¡é—á¤ºŽ®n _™‘+Ov8#q9söÅøú½ÏY}þßðNû‰@¯ÛÔÐÎáH'ìɤÀ´pÝÈ^#†Þ°ÖÑžDçQLûy#Gô>4;¡í¬2çUÎØž=ަ±ðkF>†¬1B!„¢\µ1«ˆNû+žæ½«ønÖ ¾|ñ¾èt5ÏÜ?’dTš èNÁâ%¼ýA/ž¾¡±¡…Tަƒ83q ìæŠ†9üßníÏKÅ(Q83ÕàÃ%ÛȈYÉ['îì׉Ã_|ÈŠ}´ùqŽŽcHužÍLzò'Ëõ÷œAóHmÓžçÝ=•WݪÑ€ ªMË fYX¸é}ËÃ\šê(•Î…BnùÙèY›4ÿæX†?Û…H2#œX¾¼fuO[CÁ@7jÐVŠŠ¦‚iJ€%„B!DEj˜Uuƒeˆf}¸ðï½Òçen}q6 öãòH; μ‰¿õ™Ä?^x…§žäñq­p+€½ CG6cÖsXÝ>Àf%[Ú…KScé6$•ÿLšÇ®}ío¢I\#4ÏeÊw?°{«.w¶& 0rv²-ÛNç›Ç0°“Ð1º ˜)v7v|äúJF#6b[6Å¥oaÕ]ËLe¬Š#± )ÊwìÞ§tVceèåçß;ŸÅ2ôŽ»^b×D+=ÿ}òcæ,OçÌ‘ K!®yÚ¦ž{9L|¹^p„áPW+Fÿ¥ ßN^ŽÚn"ç4³Ž0xsñšàHjOí[6¬KGoœÊˆ~”us°5mO’´ˆ"ÈaÿÁ¬¶Î*{‰TlavÐóñ,ªîKT3B!„¿oµ»YEôý,úz>«6ngǶ ü²á0^ˆ +QŒ¢ÛëoücBk|ñ“Öå†&«i4èõÿãíÿ®‚´!´‹(¼(W‰í6œ¶þ_ø|‰—ŽƒZâVcè4¤ ¾dƒ­;ÃÚƒ8-ªÍÂuÖOŸÎâu[Ù±s»ùŠŠ·Åµ¢EX+?›Îk×±rÑ|Ö7kw!£gòíó/2ù»¬ýu-Ë-aKNÕÃIJLƉãèÌçxéÓù¬\·žµËç3û»mäW8Päeû·KÈH8“³{¶&55µèѲ㙌h§²ëÛŸ8¨Åç`((0À ÌÐh8øjÆ ÌeÎ ^Pp„ÙÁ1S¢º1nDLyžµœuë–1ë­ç™v¨!#Çu%R5¦[*l™üS®bíºµü¼5«SˆhÒ–xkÓ¦.`ÍÚÕ,ùn9G*\…B!„ø³¨ñˆ™RɆUp”­K>á‡I¹˜€=¾ Cn¸žaI/™ÒA“Q·ò·5wðΛŸÐï•ëé® Äödì XúÖÏ€‘i”¸•jlFuu²iK†µŽhÅtN;m3Œ mX(¡»Wß} Þÿ~ÍOÎ ÖÙM£®I¸TÀÆøGþŸ™¼þÔ ”¨ÖœºÄ7gì#àþ`2_ýï%¾2Àß™K[ö¥I•N§¿>ν1ðÙ·ÿá…éh‘4íw5}†´&¼¼¶Êß·ËsIÖŸÆe·ŠWbè4¬Ú¿æ³pÿ¹Œ­ªü2÷Qà-Ê…jC%¢coîP"‘Š=Ì|òu ÜnÚŽ„û]ÿeò/óc„7îÉÅ\ËE­]Áq+­ÃﺓcoMâë·^àK@uÇÓ¤K{âªy#4{ó ¹yÌ^ÞšõÏ̃°¦Ã¸½ooØËïX•õ7!„B!þ(”ýûXß|3¯Z‰—.YȰ£ñûý§¨:&Ç~x„Û¿lÍc/_IKç)*Fün8¾Ÿ;›~gªïª!„BqJŒuΩXcVsFî^v50.ã£Iètý´ L²ÆL!„BüÔí®Œµb‘·þž|e W ½/~€ûÅÖñâ7!„B!„øíªÓ5fãÆ]RîëS§N©(G¢Ï¸ŸϨiMÄŸ¬1B!„u>•±âLˆÚÈL!„BüñÉŒA!„B!„¨gª¦UsŸs!„B!„uNÓ´šf²æGœNÒß„B!Ä]­3Yó#N/éoB!„âMÓ4l6™Ê(~–.YXßUB!„¢Já4oÕ†øøtÝ_­clš†­æSŧ‰ÏëåœÑç×w5„B!„¨RnNÇ¥³yÃ:Ú¥u"**ºZÇiš†M³ýî1-êÔñãéìÙµ“ü¼\LÓ¬ïêÔ‹ˆˆHš·lM||B}WE!„BüIØl6â‶ìÞ¹Î]{Të8Íf«ùˆ™øm;~<-×Ó±KwâQUõO7¾i˜&Ç¥³~íÏ´íБøøÄú®’B!„øP‡ÃNddùùÕ>NÖ˜ýíÚ¾Ž]º“˜Ô˲‚ú®Ôi¦( ‰I HëÜÛ6K`&„B!N#UUk4s­VkÌ åädsøà~¼^/–õg»ôÿíq¹Ü$7jL~~ññ XÒ)Œ…,Ë"!!‘ukVI_­C…ý¬ºó¥…B!DÕ´Úf99Ùìݽ“&M›‹ªª²«y=2 “ì¬LöîÞ €ªiÚµe%)¡¿TH_­%ûYÓæ©œ !„BÔ‘Zf‡ì§q“fDÇÆÐu…¨gŠ¢‹i×VaYÁÇŸœº;µôÕºQ²Ÿ:°_3!„Bˆ:Rë5f^¯‡¨èº.#3¿–eÐu¢£c‚¿¾•e'*lé«u£d?Û·gw}WG!„âÃv2Û嫚J  ×q•Dm™¦‰Íf/~Aâ²"ÒWëNa?“‘G!„BˆºsrÛå‡vü¿%VÑO1ƒ¢ö¾ZǤ-…B!ê’¦i¨µ ̬ÂuLò8¥ïŽY¼ñÒÇl*¨:maðq:«húޱeÕ*¶f›¥^÷ïû‚{.»†ç~ʬÇ&¬Ó¾HgÙä×xûûƒ~}£¾ä !„BÔ-MÓPk{3ë4?ô_ñе·ðêŠlÌz(¿z“üýX¹>ägá?º–e¿ì&ǬÞ1ÅPe×&þC+øüÕrÓø±Œ3†±WÞÂÃoÎdí1.ÒæðòÓo³èˆ^êuÕGrãF4ˆ²Õ:8éG]÷U#—m+W³é¨/Øý;˜t÷߸í›)¨÷¾Wû‡éÏ`Û/kØžkVû!„BQwNê>fXTú—s3cÞñ!yç<ÄÓ—·ÄUæà‚Í“¸ÿ™$Ýô/þÙ'²êÌ14l”LL¤Vû¿Ø›yìX<ƒéß­`ãþ(„7hM×qùÈvDªµË¶˜‡“žçµÀDÞè@ÔIoËn…â‹` Qåi[ÅO*žÊháÙ6GœÆŽèÎŒ{#í8(8°Žù³&ñøÒ5üõ¹ÕÈ^Áñ'Ô°Ü2Õ¤ÜþÜÀR^VÑj÷ã0³¹Ÿ)G:só‹wÐ'ºÄ‡ÊÃ"˜Ÿ¥¸ˆOnDr¼ÕúýNÕ}Ç›//¥Û#Ï“á¨ú€ßë‰ !„BüFÕz»ü Âq«ò9GÈŽû1K†ÿƒá‰%Ê fþÇ ÈÂçcAUµÐúsã£ýC¿ÕâÊÐÈdåžà_KsHîy6—ŒjA¼ÍDZ=›Øšc¢)uqµi•øY—ù•}^³êâý»ùòÕiìHÅ£Ï]MÇðÂÀ£?ƒvæ•»^åý7~ Ç#h éœ÷ÿúj-»Žä\I8ó«˜04•0¥°œLæÜshÆ5o>ÇHõî»qI½Ç=Ý€…g÷|øŸÏY´å8º=žv/æÚ+‡ÐÜ­€qŒŸÞ{•ÏVíæP¶PˆhÚ‡ ®›Èùí"PãøJ>yëæ®;Š•ˆ#¸ÿñkhç*çÀL‹qã=‰¥êþãß=ƒ×ÿ³„í‡2ð€+…ž#†Ò,}‹Wo#Ý«Õb—LÏ€†¡QÍÀ1VOŸÄ”~åˆW%²i/νjg· G!Påw%Ø6Y|÷ØD¾  W¼ð(g'–?„lþbP!„BˆßÍVËû˜…ó¹*|SÏÉF·µæ‚ËíL~ÿs~:÷ KT!pˆ…ÓV¢õ›È¹Çßå£ãù˜–¨D·Ê·ü…Øp“ã¿~Ã3ÞæƒÔ¸£gdqYÁ+brvm`·¯ãÿ~&ÉZ>{þïK¦~ø*aMŸæŠÔ2Ó±¬,ÖÍûoì0.=+G¹õ¶Èýå}žø÷Jb]Æí½ñnýž?{†Ì'xpd263Ÿý·“Õè"n¾¶.ß!~ž5…©¯h4yþz:‡‡®Ü_Äý×u!B-" § sÓJ6å´ã²[ÓÜÀãh†+p€o^|–i™¹ðº1´veÅôOyó©<œOý•.”‰Ìªñ™”÷¼Ä‹žƒ{ȦiÍÂ9qP#®M+"XÆö£:$‡.Ì#SéÖ½+ÉtíÖ™FÞ[yæ‹Yl2‘–€¨ä¦4kZÜîEï`éìû9k•^Üs÷xzG*@GZ…⦗¦1o_?.O &kÒ…]Zâ  i‰GùùþE,Ýã§[YÙ:Z|ºthM¢-[Wv®eÚ¢‘™qd)³7G2ðÁ®´ôÐzî§Ì^7–v½£‚Sm­'U*R>72–òÖãÿaCÃA\zswR\lõf¬ßOÙƒXªê? FÆVÖïWríôŒ59²rÎ˜ÌÆ´Q\zݹÄvóýä/ùÏ»Íh÷ÏÁ$(6ò,¯/ç/WÝK÷¸<ÖÍ|ŸO^šDÃç'Ò%¼ßËÂé}ýœ×ت“ØX¥â6“ÀL!„¢NÙ‚»2Öî>fÅc!åý3ÈÏ,ÀrÇ’Øõ|†Çïböw»ðaR°íæj̨s;í@ÏÎÆZŸãnÒ™Z“šÚ–^£Ç14ÉÏ®Gñ—Z¿T\ ÍèÔ%´.½uÕ•ôvg°æ—#èeë£ggº…Ö¨= ÔÙLgéôeä·ÏmÓµCGú^p· Š`×ìÙló×Àݨ]:u SÏ¡\öסĬgù_q-]ñ¤4iLã&IŽuÕ™¨tíÚž¶:ѵUþm_3{OCn™Èù}ÓèÐm0n»œ–¹?ñåòcJœwe­]Ôå´Qùé*KSúÝ>m%‚6ýZ¢ålc{V ÒüŠ^·òس1šô¦uDñj´ˆ6=iÌQ6îË/1Ù°øx[l3â( £ €¥ÅÑgì6½Ã­w<Íÿæ¬áϬ“öþó±gþ÷h8˜AMì¨qÝÙYaý7ËI7«Ó::æÏ`­Ù‘koϰžitèØ™´d× é«ì?Äкsí;tfиqts@L‡ôïžFÇÞ#¸lt ÖÞ_Øï³0s×2}Qí&ÜÀ˜¾mImÓƒs¯¾€&¿°p—§¸äJ¿+`a#ªA ›4¦q£Dµ괫B!„¨ šv2÷1£²o¶œ¸) ¹ =s?üе#Ç㱂@·‰ H²s4 û²ñ­ù9²r&Ÿ|½œ­3ñjnì~°· ÿp_vùVÙßm±4‰†™¥×n(JÅuöaã!hpnk¢‹æ$ºhÚµ9¶…»Ù™aÐ.¡L¹€=¦ ±xÈÌ7+_bvÂ{2·ï¡ÀÞœnœEïkQm蜳7A_ÃekU%Pp7lJ‹Ù¸7Ÿ E—ÙtÅ cÛNòˆ§e‚0*ʆ:ÝУ¢Œ4 +4ú¦Ók"¯¼;„¥sg1ý£g˜óe_þþì­œ_U®Æh£gß/Í!ul’T€:œÓ‡ÈgçóÓÁA\ÐÈV~,š/YÀ¾ÍÇ ñ¹´‹RNè'ý,·ÿ”,C‹"1vfûµ†FDb$J €|¿…ž¾C†Aö{wrÍ{¥OÉ–áòÜU~WÔ*GZË’ÀL!„¢.Ôæ¦I%;Ýxr¼àÃŽBt×óè?åy>Ÿ2}[4ƒN#…¬0–/¯a¡œÍ+ÿž‡zÖxn¼2•ñÃ[ï² +¸^QÞV©Y}VÑn… ª –Y6= EÓ$ŒC[I×;Ѥ¼AÂ’Û«—8¾èÞ`%ëa§± Ó*Yn9u>¡¾Åã#ůëQ2Â㫳+£Y´'CÅÛA:›g`â|¾úd&›;O }xqZ#s%Sf샖Wѯ¡ò3/ûÖìÅïH‹h %àÆŽ\_%B(á4m—߯d[Þ™ôŠ F¾¹[W±ŸDF6 C!»ò“ ±Å´æ¬Kï¢ÿÐïyìïï1kÅô™TÁæ1Ás«¼¯XämúŽ•yüÜÃ_?(ý{yy e?ßRýÁÄ0UC)™®°«Ä¨Yeý'ôWˆâ>¡b×À4Ì¢c͆‚ŽYÔ×Ýô¸öîà¿"*ÎØH°ôª¿+VÉ~XuÐeÊÞB!„uJÓNb™iš˜^¡xr}`w¡Y&¦½ÃÏNaÁ´Õ¨­®dpІiØ\vðåâ ˜ÜÂašó·ó­€é 9,x±,+t!kš˜¦Rܘ¦‰©…ï¦/UŸ(: l…:i_,?‹›Ïˆ?ñBÞ–H»†°nݲÎI"Nð²gÍah¥`šfèÂÚ,*Ã*Úq#X/›Óùyx “ˆ¢½Ìë‹Jtó¦¸õ-¬Ùç!­•3˜2s ëŽBòDl¦‰?t\À00ÍÊ÷߯øó(ÁÙ‚1·^ȯOç‘{0zô€Ðvù¿²`ÖwlÒ;pÕMÃhX²,`êôú¶ŒÁ·ý;&—E£‹Î¥µ lq­hVÀÊϦóãØ4œYÇptH§R…Úi:b ¿}‡7^ü„«/èBtÖf¼¿ ºÝÄ9íUoôg°ýû…HH¥Q”JÞέ35’£œTu—ƒÊû*`fñË·ë1Z^Ì=—µÁ]ô†Á‘ùoñβØvþ_io ýQÀ01L­d°œ$5Ž„ÕkÙß‹Ža %?wË41©ªÿ˜Å¸„ú9åô¹à©óÔâZÐPYÈžýq}Rv³{Ó,§ï•ù®(š ;~ò¸“évÑ}L¼¸y0p§1þƤÿg&¯?5%ª5çßߟN1¥³Ðs×ã&“þûï=3‹€=ž¶gMäÉ+Ï"^£êÀÌÌçÀÆïyÿ§¸Ðqô­Lì]v:f‰3-|£²¾ ˜køa›B›k{ѦiT©üêIìÒŸ˜¿%—¶Ä„Aæ†åüz0‘n‰%ûƒ¦C’²äkÞ{¯—mCTÁNoÌ[ès§ªþc–½ ö󒌥-3!²ÃËsÿÅ[êy lÝ{œƒy)ô?³aŠYåwE‹iFS·‡53g³,ÐGN&Žv}I‹­àÿ •´¥B!„¨¹“Û.¿²QÓK¾Ô;Šp·bôU­€àh€Øœvï3Ðà†K²˜üͧ¼¶(˜¯=<–I.˜Að‚Õ4ÕÒ# %FÊ1´$†Üò ßÅÜŸæ1é'/àˆiD›Mññ¯àþk#øhƼ±ÈÙ„žcïà²!I¨¡ó-ºÈ­`Ä,¡ÿx.Þö>³>}‹e8Hêyi=ãO¬/€Ú³o½õ³iÌýïëÌ´$¶ÈuÏ'-ÌÂ4-©£¹ Ë~¾üüK6t½“3¢+Žºª?’¡àLîøÛû0®:ÉÏâæÎ#¹Ü®¢ßçžìsM™×Ïæ¹©g—*ÓÝbŸÆÄò²±7å²7¦pYÉ#¢ðÄÔ¡ßÂxÛË ¼­:.­ªÑÝ£«±[kÉu­Ã‹úf![r/zÄÎgá‚dwìFß ³úÃ…|6¿+.v–ê¶ä³¹í¦M_ȯ}Þ&@¡ÌˆYEý§pÄ̪öˆ™i¹hwÉÝÜ9•‹?æoLP#hÔk,Ýú5Ã¥ZUW­så Ž}:÷^›‹Ù‚snîAû úšŒ˜ !„BÔ-ͦ¡üëÕ¬«®¹®Z,]²sFŸÏò¥KhÙº ~ŸïWQÔ„ÃédõŠe ñüþºøl|lzóFÙq¯U˜ýv9N¾Ÿû5=z÷­Ÿ¾jå³ö­òï‚Kyîî¾TSÿ®8œNvlÛJ—n=ë»*B!„¿Iž‚|V,û‰~gªVúß[m·Ë¯rÝŽ8íäó(ßéê«fΖ¬Î$ªA<‘ŽÛ~äËõ­¯hM¸eþanÌ,ýL!„¢nišíTmþ!êCáçQÙ³qã.©v~S§N¡ýÍÿcêÉV¬ž¶Ãéê«zÎ^~]ü[棎˜ft½ð&ÆôŽNÇ=å58=ä{/„BQ·NróÊ·Ö¾åÖÛk”ݯ¿Z»zˆbEŸGÅ‘ÙÔ©SNO]~BíPmà«Ó_«ê£¶ä!ÜøàrÞ©úV¿+¨“B!„¨'y³ÊG!^õåç'NŽ´aùª3bVþ*í$í „BQ·N20³*Ý‚\œ~æeS“¾Z·¤Ÿ !„BÔ­à ¦Oj»|¹@û-©Î³?“Òk̤¯Ö1B!„¨[6ÛIŒ˜Y–‰%h¿)Å£B™ÛAújÝ’ÑG!„BˆºšÊXóíò³ròÈÊÙz ª$DÝÛ¼Eúê©°hþ·õ]!„BˆßŒˆðš·jC||BÕ4µ»‰™áçœÑç×êPqz(2j†Ïë•~*„B!N¹ÜœŽKgó†u´KëD˜;¬ÆyÔ.0 ùiñü“9\!„B!þ0Z¶nËîÛéÖ¹ÆÇžT`йk“ÍB!„B!~×Öý²šÈÈ( òóku¼ZÇõB!„Bˆ?%UUk½ƒµfB!„BQÏ$0B!„Bˆz&™B!„BÔ3 Ì„B!„¢žI`&„B!„õL3!„B!„¨g˜ !„B!D=“ÀL!„B!ê™fB!„BQÏ$0B!„Bˆz&™B!„BÔ3 Ì„B!„¢žI`&„B!„õL3!„B!„¨g˜ !„B!D=“ÀL!„B!ê™fB!„BQÏ$0B!„Bˆzf«ï !ÄeY:†a€eaYV}WI!~óEÁf³c³Ûë»*Bœ˜ !Ä)dYºßÇñôcìÙ³‹üüýI¢ÚĦ_¦iT_–œËûN¨Iü2¨)Í_ûíRíõÔ\zDµåƒ?2AMà×ÝhW»Ô¢Ó |òÓi² n>{#ƒÛäÕ@uù‚¿srˆýí ?Ý&wz«'xcÁ‰K!éÂOÃèÜ"Ѝ¨¦<Úg8 |úV¼å²Ž(E¬1Ѽ÷ÙüŸù€©ù¡%ž&%Ä F*)Û&ñúøCÔ|s¯<Hzz5ô·±÷BÂõ ¦ðç®s« ¢zäyÊ3Ç3éè Ù'Ùz\ÅfÜÁéìf¸;¨\<²ƒ3Æê sž*í^àýN>¸ëR9¸t“GHÅZÓèWC-O~Ž ˜< 1ª¤nÏÀw×àÞîE>x= §¬xÒ|ÑsSù6¼Ô£6ÞZ,›çLbÚÐO©°x \l±+yçåIœo:ˆ1¯TC|Çç-÷ùÌèU}F 6Áùéá|ÒÀ]¦€€Üã‹-ög¢÷¸Ó~ZcjyfR㟳`Û ÔjéuëºzLØÉζsm%¿ô„t” /ðFø†¿¾@‘­¹ÁÌ­rZ4®Š¨åw†MÏþȺ˜lÔT8¶t.{ܺ0}hWªšˆò Ù÷;¯ý¼‘sO—Í+‹ÁDÑ ŽS•gslÙN„væÃŠfôú&to0™a Ö׬ AË¡ÚÈÉÉÆbM'!f7+¦.✮OUsE!ùªÝœCj²½[u{ö¡Ú3˜·ûIF4tàø²9ìõèÀWÍBY7Wã︋ØðÂz`ÑG=y|êHúF:õ©]QGÌS˜õë3ÔïìZÄO™À:hø€P‡ ™;èöå¯Éh‡oÞÙÙ§,aaeääHQ,äØ#D)¡ƒh=¨†Nçó¥Õx¯î•óÕô£ü4ÿü²÷$ñUÌŽvì”%Ç^xÇ4ƒ«?®d˜™×H1ãéí§.’­Ú°Äœ&‹X îÂ+^@B†<äð!J‡*ÝéUkÓ=ÅŸ­Ÿ K—N´¨êYàGð“Þ9„òÀ»,îŽcÞuw}:ÔÐ1æÇ]$>Ò×S[ø3»O×öBgt§ñC¾ÌÝ´…3Öª8ìÞAzÍþôHšÍ¦½‰ôHdã~ zÔÍ­a¶]`ûüÏ™¹z7ÇãÒÁÙ+6êÜhüËiv„>‘xéo°\ƒo>¤s!Ýj6'%@Âb´Z|傱¤ÝhlŒC,ý9™j/´"ØàNÝn-q´”ŸNu¤_ãåe~È­>¼\†€éýñ›t 6pMÛk5“¤ ptÄð0ýšÏâÍÙ¿[9¹Ë¨ñâ“TrÎf—#d$e¢b%áÐÒMUhæxi5zïZ4‚è?N“ݹÚÍw̵{ ²m‰$e©PÔl'„wœY QŠè½1àù½ ý|*ߺV¼lýäu&íÈÊ]‡gƒn4uþ“%?íbÃú8‚Z5%$ïËåÞŽÆž§ùyåj¾?D‹Zþ„6nˆëÑ•¬Z±Ž„²íhdrˆÝƒú8ÏtjLDx•«‡ãq©p F'XÓÉ´H æ`j†Àé-ûHºÕqwt.”«êç’äJ¹rå.=ʆxa¾Þ×XMdëâíØ"^âó™3™yé1‘í¼Høe)Ä%”jT«\…2»…”,0»˜ÑÆÐöô®}‘M?Ÿ ¸Kw"P̸˜ +%;F|«…ãœs˜Mdz.¯&q/Ûb!´NÌèq pƒó‡‰û··PŒ8›Á’j‘áò…ÅFjÌ„(m7j÷|–†û&²5Ð3ƒaAF~ؾ‚Ÿª·£º¯#9gR q®èœªv¤mðN–>ç'ÛR3Ð[r,‰îõié‰Ô™‰ÒÄzîw–o‡°p?­qì:~\|p)øAÖ¹Q{À8Þþ§/£ßKä¼yØWâI·öþôŸý1§môlQæòE‡pmîŪ¥óP*â]?=fïÖ4q^ÂÜUPqPS Fü«úUÌ]V5|1]…š#ZS«ò•}”ëÔß¿géÞ4jßtû…P-¤YÀädÊ­ØÒùÐäÅhÿmÍ;„æîWÅ€“£õbÙ*xGö¦G¥uLw$Þƒ:SÃñ,ëfNâ{[ƵòGBÅ6áþÃJ>þ,”g›•Ã)ç(' òzLAÔ(g`ùê™,®Þ™p-ž$Ÿ(ÚD¸Ël!Ä]#ÁLˆRHq«M¯¾uØ3埼 îÔní§G³bÒhèñ}O㿬Ý2•£óðwpœ·£?ç'+(NÔî\ƒæÌD)cK<Ìúo1é‚0àÞ”—Fö¦¢ ² .h Í›ÃÙÞóm>™ðuF·Ä[g&¬s/j,ú„{ѺLÁ@ã@¥ŽòÝœÚ7É a† ´mîËÒ%Ati”÷Ck ¸ãH†þ˜é_¾Í+€ß*ÔóÈý6™Â{3¼O £Œeè2p®Ð÷›Eñ`óá|i›Ág³çòÁšLP\¨ðè?èËÍBž`ܦN‰fæÈﱎþµèZ¥Ãu‚™së—ó—!’‘µ=® %æ ­iîû-˾ÛIòƒ‘EÜûhÒs.ט8Tz’7‡\H‡ÉÅ ñ)d©€±<=>„aâçDÌ,Õ zO0rÔºç®Å¹Ö«|òŠ•qßLæÝÕ`Â=8‚Æ\‹¬t^4<„ïMá«á[ÀèKý´’`&„¸‹”¥‹¢µ‡Û>Z¤…·lü6väçÕ+hóhG6oXGd­º7¡EÔ½GODÏ/îbqKnô¹ÍÊÌ`Ó†ßhÕæÜÜ= ]F”Yû™Ð}§ŸæÓ¶¾r².Ä]ôÝâ<Ò¡ÖœÛVUˆ»cÿÞÝT«ÉŽm›©ß ;¶m桨fEzí/?­–3!„¢Pš…¸Ã1¤‘ÎÅãøÁ¹ _4“P&„âÎ`&„BÆú+>zžù§txEtbØøg¨ìpó— !„ÿ†3Qâtïѳ¸‹ Äm£( :U•±ÞJS%žŸ¿‘ç‹»BÜÇäØ)î'ÌD‰"ýËĽFQœœœIL¼€‡§WqG!J•øóq¸º¹ÜoNܤ©¼BÜAz½òåÃøëà>ÎÅž•«¿BQªª{–]Û·P¹JuTõVoä'Dé#5fBqé ü19š9¸[7ý^ÜEBˆO§ÓáêêJdÍ:øúûc³Z‹»HBÜqÌ„âR__üýƒà_ÞVN!î;¨ª]B™¸oH0Bˆ»Àn·c·KS!„BNú˜ !„B!D1“`&„B!„ÅL‚™B!„B3 fB!„BQÌ$˜ !„B!D1“Q…âÓ4 ›Íš;*£¦¡iZqI!JLf3¾~þr/HqÏ“`&„wÍjåäÉ"jÔ¢BÅðâ.ŽB”ž^8:9qhÿ>ü‚$˜‰{Þ}ÌöïÝ]ÜEDÖª[èôî=zÞå’q{,ˆž_ètMÓHOOÇËÛû.—H!J?ÿ€ ¶oÙJq—Dˆ;ï¾ fpý0 î®›…äëà QRå‚‚4_Bˆ[§Óé¤_®¸o”ø`f³Ùˆ?Orr2Y Žxxyâïç‡ÁPâß‚B!„BÜP‰¾„›ššÊþý8KfVš¦ i ™YYÄžeÿþ¤¦¦w1…B”ö l_øs¶\à¶õT±Ç³vü`†ýïÖ۵λDË8ÊŠ/&ó]LNqE!%8˜¥¦¦rôèߨí*£ž~øúá넇§£v»Êß#­8ÂYÎ1¾yã^™ùYwëEdãÂŽùŒzµÝ{ô¤û3ÃYòOi;uBÀrˆÏºµ Ë¸=düÛuXãø}ÁBÖÍ@¬'çñìÃ1l}ÿº¡”z‘ã;vðgœå߯ãvP39³oÛb2Š\-ýOV,\ÉîDÛ-Úý@ËŽçÐÖÍü•"Mî„ÿ^‰lh³Ù8~<P0™psóÀÅÅ GG²², FÒÒRÈÉÎäXÌ "kDÜZ³F-•­†2yOþO¼‚Ù³ •"Ъ}kê9ܸŸ©âˆOP0A>Nèÿåû¼5VN/y‹7,Ç;S_&Âáæ¯P“6óÅÔŸIkò C…à`Sñô/‘rQTjëÞ{…™'®Øec;•Ì/µÿ•Þ™€Ðr” p¹mŸqÅÁ‡2åÊâãa,ýã dáë7ßæh¯Ì©à\b®ºª ?ñúSñGÅ×XôEg‚ þñ²þ`T÷WùùBþn!Õx°M7úukLˆƒ‚eÿº¼´“63òre3Xc˜Ù«ß8=Ïü¯zRöªû«I¿1üéwÙU"Ë?¬‡Ó]zŸÖ–2rèÏ4š¾ˆªæ»´U!Ľ¦DžÃÅŸÏU1àè茣£ÎÎN¸¹¹¹Aív«Õ™œœlì6ñññ}#𬔠~œÁÏTÇÁšIrìßìZ·Œ‰ÿ·Ž¨—ße@ïë‡.c0Å£ÿùÝÞ99gþ ÆV~›SË£ÔŸvˆ‚‚;1ø™jN:t8øùÜ¥‹Bc9ºûšn·q•†€vŒ˜Ñî6®Q\ÉÊÉ•ßð‡êŠî¯ù,:ü(¯¼ª¨ZHJ¿ï2¬µ?Zf2g®aþ¬wèwèæÜ·«WiKáL pö¾ØøcZx¡GOac&˜BŽNòÓ'„(EJd0KMMEQôz½½^—7*ä3½^‡^¯Ç`0a·ÙIKM»µ`–Ï¥ •«TÅE"ëÒ¸esÖOÆŒ/gP½ÒPšx©œß8—«vó÷ÙT¬€Gƒ!Œ{Þ‹ÕCßaC˜ØM#úõl‰ÆäUqÐÒÙ9nÓ{3éýøØعt6 ÙOœE‡kÙ騝/í*:£`+|ƒjå–«PVb×|ÉÔq&!+ w/O£ÎÏÑ·E( ¨¶,¬çË—zñ%€¡C¿B-‡â¶}Ë׋×q0>½[9<Ö‡>íÂo°=Q¢¸„\þÜ`?ÿ ½9—Œñᡘ4 Gç¿ÍûÛªñæØg‰tQ°§bÕœùü°ã4é8X»-}žíH¤G^¬³§°å¢ÚÍét ½[íÞx›îáE¨¦âN±gz¾üøÐtþ÷ZU´#_3|ôjžŠ'Øü¨ùØ‹¼ýbK‚M¹/Ñ,§X3íf|¿ó9|«`H…üJõürží2•ÀOWðÑ7®[±Åobú˜I,Ûu :ÜÂ;3fê+D^õ+ª^ÜÃÔç_c¥ÏËÌü¨K{>ÏÚú“Y2¬ŽjËŸëÊ$÷á<ávàë/n2vòn§78ûü·Ì耕 ?¾ÄcMŒ\þÍ 0þÅá|"îá-yæ­ÁƒwuÚ>÷&¯>RÅÊÙ?c\ô&žJ"ðn1޹#⦀–±—èeç‰xeÍzßl¥ïØæx^UçZÈšÁ€º QÓázN_Âæ„6\›5K"ç3À=¶L_ÂñÆr÷`_Ë—Ëðô3’|ñ<íàa´lά›Î„é+ÙkAïY‰=^çµ®¸é€¬=|Ðé•ÜÚÆe0¶3 é÷ô\Â'Ç»µ œZ:Ší"æ\Zîï­WZ÷{‹×;„áxé8œÈÒ­X @¯,žE×@¹\&„(ºÌlªŠ^o@QrvªªbµZÉÊÊ}nµZ/ šàŒØn×Mþ4îÞŽ•o.ãÇhÔÆäCÛù3­=_kI9'cy•ýÚLÁf,+t-xæ­þÛŽ²zæ&|áço5Á[±plñ(>þÞFý®y:Ü[rξò©%‹=a;btt|k,Q¾IWòÅì‘ ó­ÈŒe1ª©ì˜ð2£~v¦õÀ‘<\ÁHüÞUÌú›[?Æi)l7‚…gfè§wÌ î¼!7èR¤eŸdéð¡,Ñ?Åç=AYW Më8³lï6ÎäÔÊ _ÇØr*¼\ƒ®•ѯßËáT•p_9±»8œI;bÈì€+YœØƒVáyª¹*(Z(-ú§¥·'¦Ìcü0i<Ÿ½H­yÏ–W†€.cÕ> NΠeqhú /´Ñô¹á ¬îŽ51× ù£•6éÏ€‡1$îfѤy û° ‹&¶ÃW§’´a4/ŽÞGä€÷˜Rו¸µ_0öã!8”‹æÕêpáõü‘R‡A£§’‹•Lseœ•¤ÍÑl µ®Ie¿ÆÌÍÚ¸&t ºQXÑcv1V,¶kûkÙ3HW¼ˆz¾§FÏdæ–®Œiæ‰ Gþ7‹=Þ]øðñ?±(7˜aPIÙ2Ž#×áÛñÆ4 "sÿ&Oy‰!ê\¦>]¶Ÿ;)‡wrÄRWG·'ÔÆ±53˜6~(.•¢y¥jþE+7Z¼;>÷÷W1ãã+¡LqkJd03((dç¨Øl6rr¬èt:l¶Üð•?Íf³a·«˜MŒÆÛ÷VŒ>•)k†C1‰ØpÏèF:äg­1S®Qu6ïfO|ÊÈ>½ƒ™þ´¨íƒ.m'K×¥PmÀû<õ PþÙdv¾¾ˆõ1jGê ßFQ¸–§VíHôYÅ•{>æ?ÎótX™Keó .C¨[^ʳÇóےͤWêϨ¾ÍñÑ•ðNÌ«Vr¬Õ T•Š‘’ïÈdôœ|ù¹Ãƒy} ø·@÷­o2oÒ br¶“ÝdÝk¸ YGVðÃé žœÐ‹æz Œ€^{yiÊzþL‹¢±nK~Š#¨ËǼôxˆ„tQÂùÙ¨!õ=¨WýžN|°nÉO—Å'y+s×$SaàTÞ~:·„š^þ~{ou3ª…Ĥô~éç¹h qü2k i5†2óÇð×õjàŸø4ƒæÏçÐãèUÔÏ{4¬OˆêGºsdëlÞËÀªyû#že*&}Ì„ÿN‰ f.ÎÎdçä‘™JNNz½M½>÷òžÝ®’““CNNv»''wÌ&ÓMÖzk4íV–Vp oNM‡qlÝsÁ^œÝ¶‡Tÿf40’sê(±v;)ÓÑcÚ•¯4&[ÐnW÷d£¡p09óú£råÄq0:UÆãRsBë”ǰîÇ’íT•¦%_蓼3 ÎùÏ .æŸ üi9 +¿YÈN‡º¼Þ­F^“GÉ1§É"–ƒû°àŠa''ë/þ±¹S¯†Ÿ„2Qºèœ,ç;I·ƒÛ¹CœQ½hZÓï¿ÿÐéýhÖ¯=‹ßK÷ž¿Ñ¡sWº´¯OˆÃåf V½Ç85€§g¼Nïüc¨‚K4p¯béÑ+ˆó{öìû ƒŒèôõhY>‡ù¿Ç`y(”ÎÖ§æÿýÀÖX+•õ»Ø™èOTã`Œ€šz€%S¦ñÝÖ£œKUqp¶a£"ÙVÀ±ð¢gÇîá˜Õ‹&á;­Ç£lF["IY*èβ÷ dÄ ¡ÝÚ+—4Ħr£±sN|Ïw'‚é0²"f‡*têÂòË8Ü{(‘Ê{òËÞ´ùòòs¯šùVÂM`¹j½jV28âîäJDî„õŒfáÇèvh;\aJS_Ì\1XSHÍÑ€Óì>eúFâué7ωŠ+c\y˜?/بåsÓs-“aÞ°;á"vJð×BˆR¥d37W¸˜ŽÙl$;;÷°l·ÛÐérìTÕNNŽ•ìl &“³ÙŒ‹«ËmÛ¾5þ/Nå€Oï¢ï §Š´¨ãĘ ;‰oQMÛSjÕ@#Ø5 GøO–/ Ìžn(7üy»Š½4õ–R¥(ý)W®üuúÚI9zˆ8°üņC)Ômè‰Ð4 ”púŽ|–êWœY¬¤Ö IDATÈq÷×à ùôˆÒIA¯×jÏý ç}7nÏñP‡Wã7™·¢=ë–.dÞCX:¯9ïÎAkÏÜ%œ#[SùÄžIƒÉ/RÇ=÷T]q‰àñ(w^ýq=g»¶cï†3x4x“rf4á‹åk9žø kûÐ쵸m˲±´5¬ç´Wcš‡š@Maã˜7˜¼¿&ý‡L aY'ÒwLdÈWG—«ÜâûWtô¨y/ÓÐ4ð}| c;_Yƒ®s¸^²8¼ìâÔdfõjÁ¬+æ%óíÞÔhèz©_\À#þp fW¼ñu¾þ/¯=3 fÜ nmé×h6#¦N%!î á}?¦ª#ØÌ®8jçHÉR¡(×ló~;íV{ÑŠƒT_q•È‹<®..M&üPU;YYYdff^ñÈÊÊBUíúa4™pu¹MÁÌdž?o¬N»z7•ñNTjýgcíÖ_Ù–VŽÖü1&ßp•,NžQð &øÒ#§»\;eò§z Äí=ÂåÛ­XøçØœËæ)µe¥=a#Ó¿9Lù¾£x÷WvÏüš­ÉvÀ€gù´s·xtÅgу’ûYÕ¥rè@|©»Y®™kQÎÄέ§É¾Më4zU§Ís£˜³àÿˆLYÏ ñ—nTíÖ…¾z‹‡.,bè»Ë¸|ËHgªwi‡ÿ镬ص•_Ž»Ñ¨mxî Q iÖš¤,[µ‚C. i@ÝAœýq+WŸÀ»i*˜ÛyümÁ«å3tkQ“ÊaáT«ìw9w(FœÍ`I½ò~j¦ÀÊè’øcç¹[ÿN›ƒˆ †„?ÏáRŽrå.?B}®{¡¥ïgéºdÊöÇô™3™™ÿ˜>–îe2ؼdÉ éT…êÕ¨R±Ì C¨XR³PuŽ8йó@¯Çð<ú#;lèÛ:÷v!ŠÙY¤f©`¡N(œÞºŸ¤KÛÌ䨦#X]+SÕÇzWÜàÂÑsXþeŽWLΘ°änS!þ¥Yc¦Óéðôp'3ËBåÊaœ9sŽÔÔ‹—Ñ4 7‚ƒpvt@§û—óâ)þ<范-‹”s³kývźõòyëáj³ÌåZÒÜÿW–}}c©Ÿ7ª•â^›'šy2zÕx>Óu¡eU_Œ–Τ…дEżŽÒw‰ÞF]±|Â&̱ӥ¾–#k˜·î"aOw@Þ+%Òÿá¯CÎW\±Ö9øV^ÏŽÙó9\æ)>iQ?ë ´Ûù>³¿ÙMõWêãQµ#mƒw²üó 8?Ù–šØ’cIt¯OËHOôîuèÜÜ‹–Œc’Ú…f•¼Ðg&‘XŸ¡7¹·Ÿ%ˆÎ³!ÏuaàÜÁ¼£ SÝ Léûù'ó_¬LË௕«9åW™²zÒŽä¼j ÄÓŒÂåƒaø¸ÓUƒ ^ì³áÔ¾< ¾ZÊ–Äæ´¿åVü*–4 ˜ü0åŒÌáOòJ÷dvö¤¾{îDÙ3Y¤dÙÁHëg[3÷­Oy{¢~MÉÜ¿”)+S¨úb/"BhÒ<˜9óÇ3nþ@©âŽzæ(©×-ǵŒ>U wÎ`ãÌyüÒ·‰ñ˜ëµ¥¾o‰<ÍB”P%öˆa4qÉð£j•ptz=ÙÙ9˜Í&T»«ÕŠÑ G§ÿµ<ŠGw'Ø»’Ï>^™»^Ï2„G>Áë¯áÓ…:ˆf„±|öy´¯…{þ g"úŒ`°û<¯Í„vлR¦aOê7«ˆó]­¤Rp«ÝŸ÷¹òõ¢LX›ƒÎ­ z §o[‘±Ô8»‚‰cV\9Í·#žI!zŸ~Ô= ¯H§> Ù0~ËþޤoårtþŽóòcôçüdÅ)€ÚkÐ<Ò½âDõÞ#âú ÿ[3ƒ ßi`öçÁgªQ?ÔAî“&JʼnˆSùÌcÓ¾ý”·ç[#î!‘4/ï|kŸeûENîYÉçkO‘àD'ßãÍ&^èH.¸Qœ#úñÁ€íô™>ŠyMg1 ’‚hÓ¯1³ÞÞ€k·Ç©Rð˜1„‡»Teú„dnS>·,¨TøŠ/ìiŸßü]çEÓ·>àé1“™÷Þ«L08ãÖSîüfƒ‡°ó½)|5| }©? ‚Vå¨óê4>öøŒ¯–~ÌÛ³Up¡Ùà:4©}³7®Ã§Å{|eŸÁç³ðÑÚ 4LxUmϰGÚÌÔ$v,ÝIvÅ4ºf$+MÚQaÚ|÷{ínùrÙéÙ`vÁ”-VïK“ÃiR°Ô¦Ü³”,ÐáñÐP¾éÁø¯¦òör :pZ šÂk]óGd4Q±ÏXÞIÃW_¿Ïo6ÀàJ@¥zTõ4EõÙ¹/½Ý™óŸFóÁàyè<ªÓ}\+ fBˆ[¢,]­=ܶh·IÞ²ñ7Ú<Ú‘ŸW¯ Í£Ù¼a‘µêÞá"æ2Móø°æä`µæÜÒë÷ïÝ}×Ê*nìF‹î=z² zþ].‘ÿÍ>·Y™lÚð­Ú<‚›»Ç].™()¬'çзÿï´Ÿ3ƒ§ËÈɺ·â»Å x¤C'¬9·vî'ÄݶïnªUdǶÍÔoЈÛ6óPT³"½ö—ŸV—ܳ«Y­·Æ„Bˆ›ÒrHúç –Âúé0y•¡œï¿hEaKâÈá°çû‰óÉj=†ö!¥ægW!Ä]&¿B!îo¶³,«³Ï>Û§ótþ÷ZUnõîT¶¸u|öÊç´ºÞöU&¼ZWé¨)„â:î«`¶ïîâ.‚(‚î=zw„¸mEA§Ó¡Ê¸Ú%—±<ýn¤ßm^­!¤ ÓÖu¹Íkâþ"ÇNq?¹o‚™ô/+¤™¸×(Š‚““3‰‰ððô*îâ!D©>W77ä&›â~P"ïc&„÷ ½Þ@ùòaüupçbÏÊÕ_!„(UU‰‹=Ë®í[¨\¥:ªZ„Ñ1…(åî›3!„(zƒÿÀ@LŽfîßÃÖM¿w‘„¢ÄÓét¸ººY³¾þþج·|‹t!J fBq)ŠŠ‚¯¯?þþAÈ]º…¢ˆ4PU»„2qß`&„wÝnÇn—¦8B!„(œô1B!„Bˆb&ÁL!„B!Š™3!„B!„(fÌ„B!„¢˜I0B!„Bˆb&£2 !Ħi6›5wTFMCÓ´â.’B”xŠ¢`01Å]!î fBqiš†5'›Ä„ œ:u‚ŒŒtTU-îb !D‰çââBù ñ D§“F^âÞ'ÁL!î »ÝFbB‡üEDšxyûÈ †BÜ„ªª$%^àÐ}˜Ìf|ýüå^âž'ÁL!î ›ÕÊÉ“1DÔ¨E…ŠáÅ]!„(5<<½ptrâÐþ}øI0÷< f¢DéÞ£gqAˆeAôüB§kšFzz:^ÞÞw¹DBQúù±}Ë&PŠ»$BÜyÌD‰s½\!Jª¢\Pæ‹Bqët:ôË÷ 9SB!„Bˆb&ÁL!„B!Š™3!î[v.üþ1ÏöÅšóÒ¡Z!„¢8I0¢°ž\È+=zòÞºD®iI¯&²nD/ºYÎYÛíܪ“[ÁÁþx˜¤Wµ÷.•Œ£‹ܶ)Ï.¿öSpÉ”=Ì{§'m£¢ˆjÙ…W§n Îz× *„÷5 fB”ÆàÆDùñµxUå•=iëi„¶hHÀm®GÁ­v_Þÿà9ê{Ê¡@ˆ{‘=õ(k¦ý½úOfG†ŠvÅϱrø`¦ïàÉ÷Æòá€ÄþoCæ%ûnX!îc2*£%1FÍ‚X¶h=;/´à}Þ ;‰»ã8èWß½5Kg³ð—ýÄYt¸–}ŽýúÒ®¢3 6ÎoœËŒU»ùûl*VÀ£ÁƽT™„_ç0ã»-œ¼¨É—º½ßæµæ~dÿ1šç&XyqÚ{4rS@Ë!nÛ·|½xã³Ñ»•£Ác}èÓ.´ôD>-1çH±h€þ5[Óç¹ÎÔòÔßà !î>+gWcòæº~ð&ûG%ùKçĬ zŸ&cÞ噯î(ÔÁ÷Ü>^X2Ÿ}Ý? ¾ó]+¸BÜ—$˜ Q"ðoø0}Ãúíñ´î˜ûå´'°cýIôU_ ®g6ÍÅľtè?ŒzÞÙ·tóÇÎ&pâKÔv±“|h;¦U£çk-)çdÃb,ñÌ*>ÿf>O¼ÂˆÚ^h)q\ô÷àÚ¥‘¶g#§lóe/×÷ÃrägæEÈXõcÞk„>û<þy÷PÍÛ…ƒü0%ŸN ä³·¢ð’Š7!J#¡OOgewJæÞ¹a‹e•Ôû‰#Œçª¹åÝ2Ê Q‘8-=Ä®ØꇛîJ©…â~%§QB”zŸhWÝÀÙß¶r.¯O‡íü~?m¦V»Z¸§ïe麪=3ˆ§ªJÅÊõyüÙ.„fþÁúËå¹…Q§NժעN%wÈH$gB""¨T!Œ*uñ@p!'Xö¶,ÙLz¥¾ îÛŠº‘4êü2¯µpåøª•»´ #~ÕêP«F$õš?ÅóC°Þ‰¬;½‡„·LÑñ¾¼vÒâÒÀÑ/‡Ë¯0xáN*qi·µƒ«BˆBH™%…âNÍvµqÿ¿ŸnOÏ g7®ç¬Ëô­æ‚õìQbívR¦ ¢Ç´+_jL¶ áTèjÍaÒ±ên~ðÇ=LÛ¶­hPÁíÚ³œ8ÆB@§Êx\ºdã@hòÖàX²Ê×ä9=nÁ¾í)¤ZTp–k=B!„ÿ†3!J —jÐÄã}6þzœÎ½õ¬Ý€O³W©äª¦¡áÈßáÉò¦+^götCá:W´M¡<6l õö­gõÊUL}w?<þïv »=¥ÖéÑ¡Ýp¤7!DI§Ç-À ².dÑÀ)·ÖÌ–Kn¸Éé‚BÜiry[ˆ’Ä\ž‡ââÖUlÞú=›RËÒ¶EŒ€É7œ@%‹“g|ƒƒ ¾ôÂÇé&o(ÖjdzïŽç½‡Ý8±æ'b®fÍäOõ@ˆÛ{„”K)ËÂ?œÀæ\ž0ÜCˆ{˜÷*u † ‡/æÞháĦýd8U£^ô/Bˆ;M‚™%ŠÀ¦ˆÐöóõÌ?Ðju ±_n RÜkóD3OVç³Å¿±ûÀ!öïüÖ#ãc`ÛvòÓÚ:ÃñÃ8ðO8zâxuÎÒûѨk#œÎaœ_Ù}p?›—Ná³u ëÐp‡;÷®…ÅÀv–e¯¶¥E¯/9’ ¦òéQÓÊïaþúlZú)~›@hçžDʈŒBqÇIÛ!Jg=:5rãàozšv¬…[~?|Å™ˆ>#ì>Åkg3a…ô®”iØ“úÍ*â| -[J Û–ýÄÜd+ Ç­Üôx¹#åŒpåx nµûóþ W¾^´€ ksй•£Aáôm„¤¹¢÷­ÀUC F'mìæ½7˜,£užÅÛÏTB®Ë!Ä'ÁLˆǪÏ}Á‚ç ™eô¡n×ש۵°×™©òÂW,¸zmáO1rÊS…nÉ©Î;DG˜ ˜hØ“wö,tyw+FG·ºrý5^cvt¡‹ !J §úŒþuã•Ó Átšô3 LÒyÔ¡÷˜hzßÕ !„iÊ(„B!„ÅN‚™B!„B3iÊ(Jœî= oF'Di¤( :U•zBq«äØ)î'ÌD‰² z~qAˆÛJQœœœIL¼€‡§WqG!J•øóq¸º¹Á Fâ^!M…âÒë ”/Æ_÷q.ö¬\ýBˆ"PU•¸Ø³ìÚ¾…ÊUª£ªöâ.’wœÔ˜ !Ĥ7ð Äähæàþ=lÝô{qI!JwB!„(%ú’wjj*û÷àll,™YYhš‚¦)dfe{6–ýûššZÜÅÄz~ ßÎ_Í1Kq—äÞ Jþó¼“á«ëÃòO’ ο»;I»¿ã›o÷ª¾ iûˆþä¾É¹¥usÕ{Ó y\½ BšV„`¦j¹û6ïØ5M³“['¤¢¡*ª¦¢¢¢i¹U³CÞ¼{á¡jö¼GþûËý×®æMË[&wŸäí­àþ`&„Bˆÿ¦ÄV7¥¦¦rôè߀‚Á耋‹F£ «5‡ôô4lÖlþþû•Â+âæî^leµžýå?Z êðî¦tÅ¥`­Wn…„r©¦¦` Ú¥›bm¾håÜæe,?ö­Ÿ¨MaŸB{Æ vlÙCh[õ?÷ʺ¦¿YþôËä×þ¥öPSÕK¡,7×iäfµüÐ’ÛÇLS5ECQ._Ë)Þú2 kR þŒáœ½-›”åÚ¯ž• ·²ýðYr*¶£C-ô7[«–[ƒ¨i¹‘E¨hy5„¹ör™¢åM¿‡jj…BQ€h1Ôx~Nã©aÿÇceŒÿz]`ãüoÓ¿4ž¥aã2˜®©N½ÈÁÅÓ‰>£x4Òí¦ßé«û3¢Ùóö³v)´)—>—Bœzï4ëB!Dñ(‘Á,þ|O» Ž—ƒEüïLz}GϤ£9Q÷‘gød¼¿Û岿—Çžr€e3f³rÛ)Òq"¸^{úìB-[øCçxÅ>*(ÔÂöÛÕ»GUsk|4 EÕP±£Óå6]¼ªNîŠýy™¬” —B™¢SÐÔRÏeÛ÷GÙö뺽ó mʘQü[ЯGÛRËмŽë¨EÅ~©(vUÅ~mDÔÿ·«j‘kÌ Àý¤µÜ}‘?/·QÍÝÿ¹Õ¶Ò”Q!„ÿY‰ f6UE¯7\ÑWÆjµ’••ûÜjµ^1”¸Þ`Äf·ßžýiܽ+ß\Æ;/Ш黦3òËÃT~òEÞ«îLÂօ̘þ æàqô®˜ßΊÜŒ¾–Á1õ «,ä£ FÆoε퉛™<âKþ?{çEµþágfû¦‡ô„H!´Ð;ˆ€ rAáZÀ^AP±]lxE¯íŠ¢¢^+MDATD,þ¤zïôºef~ÌîfH óðYv÷L9gÞœÝ=ßyßóž˜^Üô`;â­Ål™û>sÖí£X½Ä„Y…9f¥@E™TÑ“t*õX©EìÉÜBvâM<ò¬¥ø¿ÙS˜þ¢‘Äw¢M Ö8ºÜð]ÂB0—ìféä÷ùhBM'ÞD’ÉsN§›Èn7309”Ò- ™6û?<Ëk¼vcÃrbPßw³ÇgŽÜ‡»ÇÝG}×fæ½7•—&F2i\/"ª˜’§œwÌßÞK«j(#h*ªª` ºXS5_T¤¿óèD¬–‰µ†·óÆÓ] U‹8¸nÓÞÿ–Í¥›˜õþB2ž@œkß~¾LwjË6Ü–lÂqp9³¦~ÃïÛ²q"c kH{â†4îÃ?ñîÛóY¨'`J eÏë¸e` BeµìªwÏåùQ“É*ְŶâªa·2 i²¦úýÁUðzµ”Ö-øŒ/¯a_¡†9<…Înä†IX½‹®Wªiš'Ë&¡j*Ïœ;Ê(à\©•ÂÌl2!!ápª¸ÝnœN²,ãvëâË[æv»Q‹ÙˆÉT}—bŠhL’6ìÌ­hü6g%Z§Ç90»éIw²ïÿžæ·ßösCJ²ç¨\õÏt –€–¤‡ãÁ7¾ã§ý=¹1ét5ºØ¿xk” F=v;]B%Ày¥•9>éäyÇ+(4_(Ú‰éàË—ŸÂâ9nï”ûùç¿òô1L~¾ AšÞP¿m[¥`¦Í¢Ž°êñŸY¾·”ÖM, ‡Ñ¼c˜çÀd⇭à§W2ÙYx=‰¡ºÇ‰ø^\?¸± uS¢óGóô÷ß°eÐ4ñy³ô ~%g3oO7½}½c @2÷ܾл'.f}^z„V%À¯üÏå—Ð$CÕ’xl¬¦¢¨zàH¨ª~NUÓ@õd¼”+ªFÿð@=\P•lÄd\Ã}wìáñ÷Öá<ð;ËöáÚz~íUUT羟4™¥‡s(Ña2Ùù`’õ¶ ûX’=ˆ ­‚‚ý¬ž÷¥‘/òpçÀ2çšCV©J­å« y˜_xŒ>Q~mÓTTUEÕŠÙ<ã%&üœ  2P”½eÓ^%Çü<£;†`¨dÁn$]ˆÉ’T&Â$P½Km‹PF@ çH­f8œNŠŠóp: 4 Š¢ât:q:(Š »=‹Ù\­mðËœGØtŠ÷½Ê]+Êïc8Z@å~:‰€†-‰eÛ•¢%ÆÝ¥³oÓ1HD“‘rÄ^W.-<'&úÐÅi•Võ¹QQW?ʃ—•Í1“íÑØ4Õ³]Oà ¢†DÂ)&«Ð…ªšP ¶ðý´ü°zG T¬6…$J ª_VCUUQ%3ñ­1ü¸Ïí† IDAT›9.ûÚ¯¢ªNŽoßC ˜6ú¦•kl,Gò]¨Á§ûXJúâÏ´ƒ¦i躩ê™U¯­Ñ4É7oM_¬[CU½k¦yfœpN•2g‘¿ Àž”A,ëØC{²¨áeBIÓTTgûr,dÜö£Ú… «n¯¨ ëΓïtC-)$¯¸ˆ=ó_çÝ? رö¥SËêM¼‘WžìAxÉff<7‘Ÿsw±ø×ô\.›§ªª¸³VðÅÏÙ@,ƒÆ=AÿÇ—½ÎS3v¹pÇÛ^N¤Á/ù‡G{E®â _ôÙJ®z¢@ ‚SQ;…Ypb±˜p8ôÅÁÅ,ëÃjUUp:]8¥˜Í&, AÕV¿ëè&ö8!¢Q=ŒBÂ{á±¾1å &Û"°%•žÅ?ȲŠû$BNCQÙX»–;Ox)”e»;•8ó–C¥©5<ÂÉR/ ¢ËÏ1SÕ2aåõ¨š$#yæ-©J.¾û¦nNgè]cio£8s /}êн>ªW˜ho‘÷ÔZYšWüyöW5Ò¸cüÝ4µùµG2%U2È÷Ò•¥`,'Êü³ªªŠìñjU%!…êgW Y+Ë@¨?¼"P«lÆå„™¦¢(Šo®—ª¸}s¼$UAQü„™¢ cißÌÆªÕ%d~ð/]Ôž}¯¢wFI£tÏR¦|ü k޹ËÕè,ràVoŲIUP­I´oÀÏ‘µó¥J@YÛTEQpXÏ~ñõøøÚÿĹÈq(„[t!*ÉRÙ<3ÝHºX“<É@ƒ7æ9dÛ@ €Z*Ì‚q8œÄÆD±gïJJJp»]å„™ËåTâbc@’ ¬&aæ:Ì/3਩÷µ«‡Á¬kvÇÓ¶JóÅ@%wãŸì'šk,H¨DÀß»8îêAXEçžd'61V®ewqZ\Ú^3ŸÇ÷ìW"ÎN™øðÌO–•OU}™õ|Ù ýD•æ<Æ–ÝB:_KÿŽ)X€Ò¼0ÌöÌÅò„[úgÔ ÙúÇ.” v4 –Ðruq©**š*Ú «¶‘í%¡\‘l¯ v´²ú+PY©w~w®,˺½d­J^¯õ„Œ*ª"¤¯e†¤‡/*Š'0³|YpK†ÝÖ(àªPyé6f>÷>™R0õBÌ8Žæ`‹ŠÀj©GZ4¬Û_ÄŠI/²?1 )ûPå±g&Ï>6—çûÀ¶\Ó)ƒ¤• »ò)\ù/YÆðôuÐö'>\UÄæ9ÿåÑ9FŒ’·f¦åÈñÜnAϸ¨Ï#ÓÓÿ«H²šÿÜ3ý‚7Êi:@ 8 µV˜ž5ÌtÑe2±ÛõA´ËéÄ¥©X,çðC ¡ócйJûZˆë:œ'º¯t³¹áµüë•kOs;Éýîç¥~÷Ÿd#ámþÉãmþ©¿Õ YõÚh^/ 'àJ•Þ¹Qe‚Á'0*xÍô}=!v'›cfËàñÉSNYßצ0ôð5€ÀŽ<3µ£§J9¬×?ÞŠëO8RCÓdb{à¹Þ•^‰§©VR=Λƒ¼—¡"5æêûžåêû*žÒ?“ ÷IòÖæe”•œ ܼóíª4ÇL-³µ§z_Åšo~ŸoƒG¼zÛ ?L!Øå½û&”Ym@zëÎôê‘A¬Å3oNõk·¦âv*„Æ…bÙKÖQÀBRF_—ŒE’ézû-d}þl=Îþ]¹€[x !«Ö¸tÒbì=šG©Ã † šwáêAWÐ"TÕ@ÂU·pMî—,^ws¨ ‰@2nz„{£¾á»åØ—çÆ­ÉØ#ê!)(*È’Ö ²žP–"ß»§w[ùu§.:¼‹ãõdéÙõ,Œ’O¸ž°O¹Ê3Õ3Ç®,ù~™(“<òDÒÊ’âKHžöxÏ`¤~ÿ‡x¥ÿ©ÚïùƒYÒ¹oÂë~š2ô¡q =É1¦z­¸nd+®«t;˜š bt³A§¬S¶7¢Ïÿ¢¹!”æ}o¥yßÊÓÊl¡©¾ùt ÛA“$Ï<3ݽ*yvÂL Á¹Rg„ÙÅŽR°‹5‹°Ñ³˜®%<™öÃÆrsz\b3OØ¢êñ•É…òábþ2¢üôŸ‹ ﵩhåæYzד=ñ…^kyC@«šB_3Ì;‡Ê[£¦‹1Mòøå(gä‹ÔÔžpNïœ=O˜" k^óøyeÝÖŠ7Š@ Á9pI ³Ìµ«.tNA,ý†ßE¿re…ìʬÍm®Œ&3¹9¹ØìebÁ“˜Bò,ô+yç+c˜>mj5¶¾ú¨ìºÊ•y…R…ä&£‰ ë×Q?)é´u4HjÈž];iÞ¢¥Oìž^4ÙÞç_½æ…£S¥ë™ôæUÚïBPv]Þù|ºB“$ÉçµÅ+ý®hhìØ¾†)©çµ½@ .>.a&¨;¤¤¦ñÇòßøã÷_hÒ¬aaõN)o0]¥ø¼eþs½* i¹¹Ù¬Z—‰Ûí¦QòéÅBz³æ,]²ˆ¿×®"©a2¡!!eÞ8ïzeeÓ¬Êç‰ôs-ÅÅÅç¡¥uI’°ÛHNK#=½i•l-@p*„0ÔJ$Y&5-Ô´ô Ý”‹Y–iÚ¬M›µ¸ÐM@ ¸d‘O¿‹@ @ j!Ì@ à#„™@ @ \`„0@ ‚ Œf@ @pÂL @ .0B˜ @ ÁF¬c&¨•hšFNvùyù(ŠMÓ.t“êF£‘ààBÃë!ËU[ôXØúì¶>œ­@ ¨ka&¨uhšÆ¾}{°˜-$§¥a³Ù‘$1;š¦QRRÌýûØ¿o7õœÖnÂÖg‡°õùãll-A]D3A­#;ë8&ƒ‘äÔÆhšæ{H’TÎÃà}ï?H»˜<ŸÞk«¬Ük ›ÍNJjc¶nÞHNváõ"NY‡°µŽ°õùã|ØZ ‚ºÈ%#Ì2×®ºÐMø‘ѪíI·egg‘–ÞÔ÷Þ;8;Ù8a {1qªA{e{€øú‰lÛ´ñôbAغÂÖçš´µ@ u‘KF˜Á©Å€àüq:‘ìt8°Û€ò¶Êkþ¯UU½¨±ªª”¬úo“å²Ü=íb·àt9O[‡°µŽ°õùã|ØZ ‚ºH­fn·›£GŽ’““CIi)6«•Ðð0¢£¢0ký%ÎÕo ZqÐêÔùSq`w±PÙÀÕ›·Üßëâ¿]©‚-„­u„­ÏçÃÖ@ ÔEjµªÉËËcÇŽ(Šâ)Ñ珞”P| „#‡œÜˆ ×HAµ£ù ¼$Ï{ï`Nü‡jš¦áïK¨Õ~%›Õóç³-ᆴ Çpºý+ J+›eä»^o žn°­­Ïaëj¡tûl&}žO¿1wÐÜ~’΃­@ ¨‹ÔÚuÌòòòغuŠ¢b4Y ‹"2*ŽÈ¨8Bâ0š¬(ŠÊ¶mÛÉÏË»ÐÍ=%Zñ¾ûIî>œ›† ç¡/÷Q¼w6ÿºý~&­)¨t`rÞÚæÌbûšÕìÈ÷ì(_ö2wÝù?QNzlMáïÐ$ ÍóZõ»›®ªª~Wݳ­Ö?ÜGùcî×,ÛYŒzÇ{¯Ûß“PÙ~ÞÁ~U¼,­­Ïñq)ØZ-ÞÆ¼ c¸ùÚÁ <˜[_YAžv.çÔp^Åoí OÕË\¹;Y½rYÊùµµ@ u‘Zé1s»ÝìØ±0[ì‡hÇf³PRRŠÑh"??§£˜í;w‘Ñ¢ùY…5j¥ûùã›Ù,ø5“]9NÀJDr ºº™kÛTÁ«qZœìøb"Ó3ë3tÔpš…1EÆ`r†GdÀ…ý¸-â­×~£Íø×I6{JeÌÁ1ÄÇ»5Ÿÿ{õªª‚¦á½[~‚gÁ³Í÷ú¬*ÉâÇÝÍ{æG˜öBW‚ÎÇeúÚªQÑkPµÃ+dê£Ï‚$ù^WY,Ô´­=­-Üú=“§|ÍòÍÇ)L¡‰´è9œQ7µ#ìÜ?hÕJí´µÂ¡oŸaÌ'›qxJd{$ ›´¥GÿAôk…©ÊýØÁ¶O_æ“5‰Üøðsd„ƒÓœDZ™Z:cüŽÕ4в›ñÓ¢yvrSÂm'9ªl-A]¤V ³£GŽêá‹’›-›ÍJ@€àà@dYFQT\®œNŠÛÍÑ£G‰‹‹;£z´Â|:þE¾?PŒ¾×rOZ46W{7o"Ï-WOø’ÅÆ9„t~€«;¥bñmèŃÿéU5ÔÁ­oã¹Ö¦vMUËÝM÷z|Û+„7u=~Ïçrž³­ûŒë¬`<^ɯÜ?$®ªáuçÃÖjÎo¼1î#ÖDvçÆû;‘¤’· Ûµ`¬†óoÿÓR+m­â,ÈÅah­OÜH²ÑIáñ½lX¾Éã²øšgkÁU‰ƒP޳n]6¡Ýc`ׯ~ßKÕó™ò¾®Òg¬l-A]¤V ³¼¼<$IÂ`4c00ddYFUõŸfY–1d F£Å­Ÿ—fÂL+aóçïðýú ~nCRl>!Ö±{Ÿ²ý”Ö~=™?¬æ@1Ä·¥ß°ÛÔ2  ®cÆ›ÓY¾ó¹¥`%ºen½û:Z…@uQℼ%Ïqûý”±C^åÅ.kxæá¹Ôr£›YÒý¿óù´¯øyÃQ€%4ކí†2ê–ö„»6ñÎèÿ°{àk¼|M @9²€'þ†O¿ÅÈ&FŽü:ç¯bÛ<\@h§GyuTs ¾Î>Ï$Û ˜#ivåpF\ß–pŸ—"—ÇÝÁ$rËÄç¹lß+Ü=ÁÅÈ÷ÆÑ5XÍÉá_òÉ?±þ¨Cp:]s+·^•J (Yü1y_®ÚÃá|' ˜ÐŽkig$rUoŸ‡¡üßOó=ŸUJñŠuTÖŽüMÌÿøcæ.ßE¾j!¦e†Ýs]ctÏ¢sÇ Æ½0­ùnÀN|»Ü{ÿu4÷ŒŒ5Ç~™þ?f,ÞÈq—ð†) Àx²k:S$I·«ÿù4M÷.PÁŽ'á¼Øpø‹ÍÎ`zÁ&о WxϯfóëÏðÞòÔÆð4z Í—ÇR¸ôiîžäà®÷^áª(½ÓºöÌä‡Ñü…ÿq Jî:æ~8™y+öPˆøv¸sÄZ…V“+®VØÚ³ÍFÃôf4·´¦Óåýèõõ³<öék|œ1‰‡Ú!Á©m¢èßK¹ ŸàÆ…úiãnx“7®Îáã§þËâ}E¨È7ìÊ?GÞËUlH±rÜ-¼â~Éÿ¹Œ pîà“ûçÏx{x’§‰ZÙõi€s%ã‡]«Wñ&Lº…†¦šµµ@ u‘Z)ÌܪŠÁ`,7§ÀårQR¢¿w¹\åR.Œ&ÜÊÎ…*ÙÂw¿åaët/ý“m• ­”­Ÿ=Ï«ßC—›àæDØ»ì3f¾úϽÀM)V4Ç6nšó¥Lä±ÁÞÖÑiÄc J4l!<ÜûÊ‚ü5óïI+ë}3tˆ¢tËB¦ÏxžWÔ—7 “ZÄÞõÛÈ©?”û7Ââ8ι3øì¿FêOI«€ªæ+ÎÅ©8 «èY*Þq?C*½›ïÚÏ×Ï?çÙm¹áá4¶bù¬ÉLx²ëÄQ´ –#Ú1xDs‚Âl¸®`æ{³xmz3Þ»¿)V­€5<ÛËì\vóÃtO4’µa1³véºêðÒ•ó"ø={-}6óžjÊÖÆ°Dò+k–¬âprg¢+†ÈʤôÎÃý – ØüÝL™ôIMÿÕͺЀ©¬Ø^Dߨ`$²×¯âˆµ)w%ZМ{˜=~Ü=î>ê»63ゥ¼41’IãzQ 3ik‹­+z¤ôÆYhpÕ-ôœ÷ ?Ï[Ç­»ì:M<‡u{§®M ˜Ã¢1Xmt¹áº„…`.ÙÍÒÉïóÑ„(šN¼‰$Så°“•ùÞšqÏów’nL!ÄOÝ÷«ÃÖ@ ÔEj¥03›LHH8œ*n·§Ó…,˸ݺøò–¹ÝnEÅb6b2Ù¥¸s÷rÈ ±Í°žD3hk™ýãQb‡¼Â½ýã1é±8v=Á¼9™ x¼zp¥‰¨¦mhÕ 4£~I&cf-gWIwÂ=Ó¶LÁ±ÔOLÄ;‹K)'Ì\ì_<‡5J£».¡àÀ¼ÒÊœ3ÍkœL›6͉ñsØ[Ó>Q’ÄÞ߯òÓú£8;{²¿ Ž­Ob¢ÙwL±ÿ9•c,Ÿý;…iwòÂm=õnó4êå>ÂøùóØ~Å}4ñÔgOhAë–0Ó‚¦‘ÇXóô¯¬Øï¤Ucÿ`©Sãxišê¹a~bÚìòågtê=·¦êõTØ\²i6sw†Ò÷…’n2hÚ@bÏÈùì·¡d\…!8•öí=¤Ôç–u?ñôæw¥Wð³—å‘8üyî«ÏUlÌö%«ÙˆVigFù<~åÓ¯kH’áŒÄBÚcú2æ¾]¼úáF.§u¯>\uUOZÇyoЉnÝ‘hÏþ"‡ðËòOX½¿”¾-é’ 0û—-ujK€–Mæo{17BŠM¥8s6óö$pÓÛ·Ñ;Ö$sÏí«¸{âbÖçõ Gè¹%×[«¾:ËÎáÁGF,9²“,W'ŒOc“½ÿ™‚cHL¬OÙ'?ŒæÃ<¯“‰¶‚Ÿ^Édgáõ$†z?3~ýWSýÚæ'<½Ÿ+ 0]¿>‰¾9fÚ^Žê±µ@ u‘Z)Ìp8çát:1 h .EÅétât:Qv{³ù4g­ˆæùù—N:ÔtÝÌ~%„v-"ñEÞ˜¢iÑ$˜9k7sÌåfþŽÄ¤ä’WªBUš¥³oÓ1HD“êÎBáäÈÊ9Lûz9›äPj°cv‚1Í}§8Ìúƒ3¸1¡>Ä6 1þ´‹í9 M"N<Ì–H8Åä™7Sóf¥«˜©­’A¬¦å⻪WéYðÔr§ps|ëNŠLh[ß\æ I§U |³î ¥}ëÁž¥Ì˜ö-n;D®Ë„Mr@x)NE¥ôÈi¡tJCRUTÕ“iOSõ:ϼÕ$$I;ap«iúÚ¼žµ²ª:辶m €‰¸^£y£ë lþc)‹ÎæÅïg’:x,cohJšÃÚ¹Sø|é:ö+»nš»T)’=ê3㫟ÙZÔš Ç:–í4ÑbL:vÍÉÁí{(áÓFßÀ´ruÆr$ß…|._sµÉÖe‚NUUÊU©©¨^Q§:9~:›Ø¼}Q+×Õ‚-|?m?¬ÞÅÑ«MA!‰R§¢g‹Ä¯~ ¿>ím“æ»VovÉJÛ[)Õgk@ ê"µS˜AA!‹ ‡C_TZQÜȲî–QU§Ó…ÃQŠÙlÂb±t¢D:†à¢Œ°cË!=ðVcû%Ù€Œvo EdãÉ×/$d —rFž×Þy¼úÖwÈ=oç;“ å ‹Þ|‡•gp޳æŒí £j^Ï‚æèjë-‡3 TµS3?Ï„ÿ€R-ó²©E™þüûü=€»AR ‹ísþˇ{<Çh ¡¡*JÙ~ïó 3Oæ¾²Tuå¯^‚$I¨ªŠ¬b}v<µ΃­ý1Õ£ñeChܽ?WÏ}Ž'¿x‡¯;þ—+·Nàå/²èxËnm‰1g“^ùÚ#b%"Ú÷ iæoÎ'¡à'¶š2x¸±MUôvKiÜ1þnšúgþ“Ì„DIgèa©Í¶öŠ/Ê÷K€’}ü} II„È*U°‰×ãåë‹Z>¾û¦nNgè]cio£8s /}êðÔç™ç¥8qû„Y…ó”kŸ¿X¬L˜Õœ­@ ¨‹ÔJaˆÃá$6&Š={PRR‚Ûí*'Ì\.7 ’DPà™ 3) +ÚØø{ù—ü4à ®N8ѵeŽjL‚a1×Õ¢‡2â:ʺMùyª ìgÔ;±‰Á°r-»‹»Ð²²ùXrðçžc8´xŒUt\8mâ¸oHOZ†J Øˆð«ÚhÄ“BÇ);æhšÅÂÚµ[Ƚ&Î3g§”½«wáhAr5ç:÷­ãä{Ötÿf%ƒØ³MFá/²PU´r§ mÔ»k#í.&£±.Û•ìM¬=q}£‘sÿb[¾‰#ѽ© p¡DÙ`·ÞnSDcâ ‹È\}GŠ>‡ÇëÍÐw&T¶¿w’wN’,˺½d­Êáu5nëJ±Ð²õ¾øÝYEÙ¸5ö†\Ñš8#žH°Þ@4UEŽèHßÔϘúÝ/,+Þ†½ÃXÒíš*Ú «¶‘í%¡\‘l¯àׯéL©¶öTË¢µRv}7et¹º)šŒát6qyó¶ÀuŒ-»„t¾–þS°¥ya˜9¬ï£JDÀß;8Vz¡ün6”]€¢(h*­&pQìRÐ,•÷š°µ@ u‘Z)ÌdY&,4„â’R7NfÿþCäåø*š¦L|| 6«ïnj•‘‚h5ü6:lzOŸÇŽþWЮQVµ#»¶r8¦?7woÍ+£ø÷—¯óù.«¯±gÙgÌ9Í€- ’ÎÚoP3 û^AÂÒ¯x÷Xnéׄàâíü´® ì/dŠ¡HÀ—÷ IDATCÇhæÎ›ÌGóoಆAh‡wSpš3[¢Sˆà{Ì]JpׄÈYñ›ßf O¦½˜¿¾œÇ`ÉËÂܬ;)þ'1DÑuhW¾ž0… S†tˆ¢tËLÿ©€ä’j\ÕbÏ@ZCõ^µ²w}ƒXÏëʰUZoKUt/AÁ.2×Z(s*LjL£ÔkÐ྘ø6Á7]Išõ(+gʶ n<Ú1 ƒ1žÄÿ÷õ×,£ ñ*ûŽ•¢{?Ô€ †ô‰æ¹¯_f‚2”+šEb*ÚÆÁÝ› (ÊigkRy2×iÞlužg MÖ—¯®Š 9/¶;æòáb©Í’‰1£ìgõw?’%5¤Œ•ðähX±”¯ÆÓ35sñ> `UÑ—Î ˜VýZ0õ­éÌ"‚þ·6À¬((€%m}ãþ䛉¯aÚ‡–±6\9ÉiO¯aUZ‹°nØZÕ3º²Ù¾.Åä¢({?›W.aѺlú=Æðf6TE9½M|^2ݾ €BƒX# VÌå»&WÑ,Ò†so..4TUAQ ÄwiMв%üoJ4CÚÇcsíf_±÷<*²= ùí÷-¤öL!0.…pm!³¿XбM0EÇܤ^Öè“ÝÔª&[ @P©• Àd2èIøÑ$=Ù`Àáp`±˜Q—Ë…Éh@6œÇÆP¯+£_gÑì¹,Z&?Â}…0kö;¬TC šÓ(Ø' Œ3% bÌ-¹|ôõT^]¬Ïõ2F‘k×Ã&mMvOŽOžÏ;¯ÌC NášG;—fH·¾“çFñÉç3™°Ä‰Ü€NÞæ¶~qT—ãЋêñfiž$ªþ¦Ò°¯Ö~ªêÄ{;þà·¼ýßoËmJ¹÷mÆu§ÿcOb˜þ)ßþï¿ÌÑ,D5»’ûBË@ & {`¥Ó¾ç¯èÇK–`âZD`4¬¤üóiÆÍàó…Sy}¾0Fûx’vú¦–Í‚ô+óØ5 Iïìá1×xqjØÖh¸¥`ì¹K˜÷ɷ乬D¤tà†±7Ò+ÒŒÜs$÷úˆ/?ŸÈïn#öð†4 4x.O"¨y:¯åç°>ôJ0—]¶©ƒŸ|ÛŒYü0ãmºA²ÇÐjps.ovòðàr-¬ ¶–0„`V¶ñù¯èíµÕ#©q+†=ÞŸÞÍ"0y×a>M´²“k¾¾Bû»FÑÿÙÌ{û%¾0حߎ0£„¦IØÒ‡ñè073“·–i€‰ ¨Ú$ i`IÄÐ6{øbÖ,Öµ{šî‰×pï?öñÁ‚ym1Øë÷äþíˆ4J5jk@ ê"ÒœÏghWöë_¥—ÿú3}ûÿƒ… ¾¡oÿðû/?‘Ѫm 7QÇd2cò$øp9¸\Î3:>síªóÖÖjA+dÕk£y½èvÞyö2Î9±\-ât‹%‹~ ÿÀÁ8NÝ»ä.åyö¢útP}2¹¶áñ£_k¹®à÷^ù2 Ë&³™óæÒûÊ~§<³°uE„­Ï5gk@ .™kWÑ´Y®øºòçŠßéÒýò*»è‡µ×cV—ëÌÅX]BÍÝÀ’Y„ÄFbuq|ãf®…&w5#è"eU¡¼gÁ;Ç›À?A€w›Tút1¢©e!^ªê¹NýšeYóòíª©hš|–^akaëóH ÚZ ‚ºHf;JÁ.Ö,^ÀÆC8Kx2í‡åæõª4Gæ¢Âê¥{ÊæÞ”ŸÏä7'Çó,]¤Žﵩ”Í;òe«Ó4dII÷ý¢ÞuÅÖ·ÞvûI·M2ùœÏ)Úº¢M«ÃŽUá|ØZ ‚ºÈ%#ÌêÔü²Kœøø6oZO‡Žqišî1@2ô0'I dƒ|N™k;•]W¹2Iÿ¯bÒ“ÑĆõ먟”tÚ:ꊭ§O›Z£ç¿m]Ó6=çÃÖ@ ÔE.a&¨;¤¤¦ñÇòßøã÷_hÒ¬aaõNÌ]´ *ÇçUðe;„­Ïgck@ ê"B˜ j%’,“š–NjZú…nÊE°õùCØZ ÁɨÊÚ«@ @ ¨A„0@ ‚ Œf@ @pÂL @ .0B˜ @ ÁF3@ @ ¸Àˆtù@PÃhš†ÛíBQÐ44íÒZHZP7$ £Ñ„Ñd:í¾¢O êgÒ§‚Ú€f@Pƒhš†Ëé ëØqöìÙEQQ!ªª^èf 'HÃF)DÅÄ"Ë'¨}ZPW¨jŸö¢i9ÙYäçå£(îZ}ÃÁh4Bhx=dYºÐÍTB˜ A ¢(n²Žcó–M4oÑ’ðzU çUUÉÎ:Άuc¶XˆŒŠÖ½a• ú´ .p&}tQ¶oß,f ÉiiØlv$©v MÓ())æÀþ}ìß·›ú‰ jm[g†f@Pƒ¸].vïÞIó­h”’z¡›#œ”аplv;2ÿ&:&Xѧu…ªöi€ì¬ã˜ F’S£yÂs5MC’¤rž3ï{!t®žµŠ¢Ê{¾ÊʽõÛlvRR³uóFr²³¯qNmÔ.a–¹vÕ…n‚ÀŒVm+-¿iØðóÜ z˜9ãÓJË5M£°°ðzõÎs‹‚3':&Ž•ËƒSÜ|}ZP—¨JŸÈÎÎ"-½©ï½WL$'´såT°2‘_?‘m›6 av‘pÉ38¹œ_N'’O6Àj+U¹¡ B½5ÿ]ýê ʲ\åùb¢O êUíÓN‡»=(/Š*Dþ¯UU=çÏž·}þ"Ì›ÿg­b[ìöœ.ç9Õ/¨=Ôzaæv»9zä(999””–`³Z #:* £±Ö_‚@ 5‚¢(<óÌ3<õÔSØív!–‚³Dõ`ÅXe®¢x:*dþÛ¼åþ<ÿíŠH¾sÑP«¿ÁóòòÈÌ\ǃ).)AÓ$4M¢¸¤„ƒ’™¹Ž¼¼¼ ÝL@ ¸ˆPÉû{ŸÌü#îŠÛ4Jw~ÇïÌa§£š«ÕJØþíd¦ü¸qï·ê¸\.^~ùe}ôQrrrjIvDчuÍï³#yßkhÚ Qš§Üû¹<¨p>­’GÅ}ü—©ÐjÅç^PÔZa–——ÇÖ­ÛP£ÉJhX‘QqDFÅ…ÑdEQT¶mÛN~mgZ)G6¯fíþbjo¢U DߨYüú¿·ù,¬†Š5 7}Íô9+9z€ûÜÐ Vóñ›Sø£ÔLéοøåσ×*…•–_Úx…ØüùóyöÙgÉÍͽðâLô!AÄÿs£Hšçµêç±RUUKžmçôÐJ8¸áOVí-Fõ+÷Öåï!«ìx¯§í‚æÕF­Œt»ÝìØ±0[ì‡hÇf³PRRŠÑh"??§£˜í;w‘Ñ¢ù™…5jyü1áqÞ^Sä+2ÇѸÍå Ò‡æaç`Ç.fOxÝ_£E‚~—‚s¡t#ï=ú"¿æx F§Ð²ÛÕ Іhó9ĺ‹¾{É£[Îü&Ú½˜Aðy˾¬‘ÿ÷<þr6åÁNvÖ>?†§ÜÏ0¯}eݹ˜ÕoTV~jŠþx„þãÞÏ>áÆ¿ïõüŸÕÿJ™Ã‡ƒ¢‡X6y"Ï_É®<ä@âšváŸ=ÆuŒ˜û8#ßú?²½bÂBBzúÝ|7uŽÁ»|­š»†¯N`Ư{(2GÓæÚxâžËˆ©áõmùå:wîŒÍfã©§ž"88ø‚…5Š>T7ûÐ¥Žêç!Ý å>e^2ßk}/ ·~Ïä)_³|óqJSh"-zgÔMí;Ýhévf½ø;¯›DËú6ßoî YñË]â­[’|¯…0»x¨•Âìè‘£zJSɈÍ€Íf% ÀNpp  OäT—+§ÓâvsôèQâââª^‰¦P’[ñƒxäŽØÝ%äÜÀÒ¹3yqÍ^å^Z‰5!µÕAnÔë5‚ûºÕC+Éçè¶ßùfÎ<±ý^þûHwê E%8+Ž.ÿ–Í–ö¼”tº$fÕ‡–ÏÚy«p5CÇ™Mç«^N¶~üOÏ, Í?Gss«hÌÅGضþ ¡&@Å‘}l¹ £'ÜIcS)Çv±rî'|ôøŠ>œÆÈt (‡˜÷ô#|°» ·I£ìE¼3é)5ÌÇw§a©Á+hԨ˖-£[·nØív~øa‚‚‚.€8}¨®ö¡KMUËy¬¼3ßvÿäžg5ç7Þ÷k"»sãýH RÉÛ¿…íZ0VåŽ?i½~Ï'«‡Nò+—üÂ좡V ³¼¼<$IÂ`4c00dOV½ ʲŒÁ c00Í(n…ü¼ü3f^ëÓ¸qc% y+Ú4t1æßKùnËpZµ D-ØÂwS§óíÊÝä«¢[ôâúÛ‡Ò)Ú €kÿ÷üï£Å¬Ûy„B 1W2îß80ëQnž¥W“þàûŒk/³kÑ>üj9» 40GÒö–'x¨g”ðLÎ{l é£õ¾Óª éæ'yô‹…¬ÎéÊ•rõöÝæ"çå(¿»k‡;h(*¹«gðÚ›Ÿ±lgÔ§y|6ZvŒVÊîÞåµð÷'ÆzÍèw÷¿xðê$ ŵ/–òЗrmŒþ-çÚñÃn›G›I³ÛR‚PsW1oµ›fv žWG¬ž—=@@ï7™ûï´JÊ_æ ç¿yzûP¦Ì¸‡d€‹ ã¶y­˜4ûIRªtÝÙ¬ûó ¤?ÎÓ÷_C¤§ =úzwð zLõHm‘AK@º´´°aÈþÚœ‹š{ç7ÌøÛÈe/=ÃíÝBhCä¡¿¹oö§ü}Óxjòc¤( Mš4á§Ÿ~âòË/Çf³1räHϯ8}¨Îö¡KÕOxù{ÎÊá-ó„:üÅfg0½FŒ`Hlnß…+üöUr×1÷ÃÉÌ[±‡BìÄ·À#†Ð*´L¹íŸ>Š¡Óõ×M›Ê Ë×+Iåç¢y_{CÏq5Aí¡V 3·ªb0ËÅκ\.JJô÷.—«\jQƒÑ„û‹ž ²ÅŽ §SEsàÛ—_`VN+†Œø'i–#¬˜ý)oý»ë«÷Ð*HÂut +¶)ô¼ë:ǘp:‰5”Ñ÷aÆ\[¤ ×¾Ù¼9õ/"®}€g[‡£å¦ :Tˆ2A5 c¶›7·ÕÜw—Êá_ùv›Ž÷´ H×ÞÏylÌìky#ÿz©õJw°l懬÷¡’ýË‹Œ|ño2îǤ¶A^ò.¯¼ü(Ö3Õª)¼Ã/ Š„Âñ5pÈÖŠË“­¾s䮞ϵ9·¯‡L±^ÜèÞ|º !2ƒã±à®¤<ŽÀ̦~]ÁÚãw’k5—”>ІUu/‚¨Ÿ¿|Ç{0¬yði&`«8r÷ñ××ß²›DnhŠŒJÞæU&™»›{gtÒ!Õ|3Ãívc0hÙ²% .äÊ+¯ÄjµrçwX­k- чênºÔ©8Ǭ¢Ô©è1“$ cX"ù•5KVq8¹ó‰S œ{˜=~Ü=î>ê»63ゥ¼41’Iãzá]y,²ÿXë… [”ý„ºËyÇüÛPIÛu›Z)ÌÌ&§ŠÛíÆét!Ë2n·.¾¼en·EQ±˜˜Lgy)š§Ë‰Ã]Lö¾ ,™ù=Ǥd®NÀ±uóv‡pŸû¹¶±þÐ$IfïC“ùòkiÑ'Òs’0š´mE3o0}©Da ‹#11Æ'¼EY@«æÍIkdC"ù,-$€ª¸p98Jó8´ù7¾ür7D¤E=¥[çUkß\ ¸9ôë¶Û;1¢y à`ûWŸ³ÙÞ›ÿþg‚$ ©Ž¥,˜à=ä ?~²µ÷k<=¼#´LyœËîäÇwrßèôjð&Sm¤°g‚´lV-Þ‰¥Õm4ñÞùWsùkÞßh-ÆÒ>Üo(k¦aJ2a¾qNa¥åjF_šÊ/³hU6ƒD"ogù.™”Ò ” ¤J×@»Çsóññ|0¢?sš÷ášÁ×1¸gSÂýçõ-â>‹ü d’oz““-€‹üÃù`K#ÜZ683†ÆÂr绚T+Š‚¢(F:tèÀ‚ èß¿?V«•áÇpÄ™èCu¹]êxŦ©§Ô‰©éË—KÈ1}sß.^ýp#—ÇÓºW®ºª'­ãlH@ÉÆÙÌÛ“ÀMoßFïXÌ=·¯âYŸ×ƒV½NKXI‰±~sÌü…–¿,«¸Ð´†$„0»ˆ¨•Â,0 ‡ÓIQqN§ƒÁ€¦Áà@QTœN'N§Eqa·‡`1Ÿå—ÕÖ÷uû{¾·†È =z7WDkY±‹bS#Ú&Z˶‡¦Ó*æo8ŒË7¸­–äþü£É*>ÿ0;º^I¿~WЩQ°ü Ί³ÆrǬ²÷¡û0úÞA$™ÜÜ^½}Wp à>Ä/ßí$ Ó(šj!»7eARgÒO2 w`í~(Úù(W-)¿Éx0·1W5äýi?°©¸ íJW³p«™¶Ïµò%…P³ÿd^&d<ÙŽ°³ˆ¸“ëu`` ™—¾ÿ‹¬«¯"hÏr6:pcëð3J;,‡uàžI_sýö,üökæ¾t/S>èÍŸâêÏNÖŽükâšXT\EÇÙ±r.LÍé}>¼/õÌ_¸ÝnŒF£/ V·nݘ3g×]wV«•¡C‡b·ÛkVœ‰>T§ûÐ¥ŽæÍ¶X1b%âLÓ¼‰ALÄõÍ]o`óKY´p6/~?“ÔÁc{CùÛ÷P¦¾iåÎË‘|ªÙ“ÝQSõ…ªOh•„$i'DUjš†¡ìYwM³‹†Ú)Ì‚ƒ  ‹Å„á/*­(ndY—0ªªàtºp8J1›MX,ƒOuÊ““pßÙœ@“• °H"C­ž/âjÎã `Näš§&Ñîï¥,˜7Ÿwžù†ïã™!ÉXÏO”‰à""âÊQŒì‰Ù@hD$áv¯Ä¯¾+¸èqø™ïvÒõÁ¦èŽIŸ¾à½{\éw”>`ˆô¯\—€¿c@ˆÁ†‘ØËú“ò¿O˜·.FyóÙ`êÀ -½aZ*YÎg½”Á3mCÏný9œƒÛbÿ +Ž÷¤ÙŠÿ#7¾/£õŸ7É`Áˆ“"gù‘êȧ0Zü„¤tãŸuãÚaKyáöq¼özw:½v™¾ÙL|£d’m©¤·Ì rÿµ<²`>Ûo{Œ˜˜`(9Nv©vý ÝyÉ'˜˜àšý¹UÅÎègW\q3gÎdذaX,X£âLô!¨Ë}èRGÕ¼3Í“ÓàTâÌ[ ©/Bãîý¹zîs<ùÅ;|ÝñU.W5Ò¸cüÝ4õŸ ™ ‰’PÝzÂ|ÂÌ“²,c9Qæõ”I’„ªª¾ù£ª&„ÙÅB­\Ç,(0“ÙLlLªªPRRBqqq¹GII ªª…Él&(ð,…Y@)©©¤4¬O´O” OIÂîÚɪ½¥¾R%w3k@\ÓNš¹V6b3£°”>*’•ØVWq×3¯1îÊ`výøCõ/²)¸$°E5$55™F‰1~¢ j¬ï .b\Xú={‚ºÒß&Ò(#v.emîIzƒ%ŽŒx8¶ñö„4hPöHŒÔ¿Oq½¸®¹“•_þÈ÷_m$àò!´ò¹:²X9=rë´ õ~ûJ˜í&(ɧ´\µ'+— ët=62û‡¿øeéaâ®èA‚§“›c›Ë–ÿß¿[*Y«~b'õHO ¨ô‡Ðцî Àup7¹'›Â¬:((rÑ‚Q’ IoK ;ùes'ð¨”]¿eRdoJ»¸š AÓCûõpFú÷ïχ~ÈÈ‘#ùñÇ)--=aY=ˆ>T‘ºÖ‡.u|ë“©ªîÁRt±¤* šç¹âCSõm¾‡f!¡e3ê‘Íî,…Ð Xµƒl/ %..Žxï#6‚@YCÓ úon~ ŠªzÎwb=åÚ ª~mÕ|k« .jåíY– ¡¸¤”Ɠٿÿyy¾»|š¦L|| 6kd²¦ýƒk¬àó7ß!tø•¤Y²âËélêÆã]"N‚hŠ¢q¼Å?Ïáûä>$‘E^h;:‡mbI&ÔOª‡ÕuŒu{‹À†MÄ2 ª™êî»ÝÓÏ_ÚkÁùǵŸŸî#¤Ûc¤Û½…f’¯»ƒŽs_âù1/sì–Þ¤…ª\Ÿ…/£ž1¾·ugú¿ßâÑJ¸­OSêÉØUBƵWë‰äzt»¾=o>ýÃ4Å[…¾Þ•‘ÖϵÁ7¦ÆBBëDxg}C¿8 Ò·OâIÊa Èà†ÑÜ9ùeö¹cÞ«¾ïæƒ1¾·v›Îs“FóDÖÍômFé®e|6yjóÑ Iµ€zŒ%¯½Íß1íh™M¡”#™ øèo½º5Ñ&8à:ÎÖ¿×€ÉIQÎA6ý2‡ºHºµ?,`nø†µü‚ /¿Ä§cÓ0{1ï|yŒÄ›‡“QÃÙôü=fþ^3€¡C‡RZZÊ]wÝÅ´iÓèÝ»7‹¥z=g¢Õù>t©£y¼dªOð”-$í[ÛÌóÚëµrì˜Ë‹\¤6oDt°µ`?«¿û‘,©!ýcŒXBÐ7îO¾™ø¶¡}hkÕsìöôj†ÁPÔx‹žÍ‚ä+IÒ²É kK÷T¿ß\OFFÍ›…Ñó¬!¡Éú²Ôš*²2^,ÔJa`2™ô$üh’žŠl0àp8°XÌ¨Š‚ËåÂd4 jHÙ˜¸fìÓ¦Ngþ»¯2[³Õ¼£zê5Τ:Üq™oÍ`Ö›kÀNÆõ©´NÝÉŠ¹?0-ÇnОa£ÿA±h¤ º©æ¾Û9-ðä^6Aǵo1 ÷‡ÐulºoÀ `ˆ¾Šç?0ðÁ[S˜òïï)d{$ÉZa‰è5Ž÷•ysòLþ³¤ 3áMðÔÕýhh‘™Ðø"ì¾Ê?yÓÜéë]m2µáùÖ!~ÂßHü5c•9ž&>ÁXIèý(z7:i¹Õ`!ùº›iñùY—r3}êûõV9’ÞϾöÑ[LýöMžûL[,-<Ê3#®¡¾ pk˜Ãaë·ïòÍ¡"TÀÚv×?è»Z€‚%<Žpåÿ˜ôØÏyˆNiÉ GÆrûÀT}})c,_xüW&0}Ü#”˜¢hsý ·ë^\››nøAŽyëÛ2eê.ÃÞ§¬ž<øÅÓsÖU‡fé×ÿ u}¥m ÷É\ñþwçóÓÿýÃyìœKsÑñ3Çæqú[ÁÚµk3iÒ¤<òÈ#7n\žþù,[¶,gžyf–.]š/|á ùÀ>©S§¦­­-™={v<ðÀŒ?ü®þ¿|ý²¼í¤SÒÓ=üg7þLû 1¶ý¢Ït’\wí59ñ¤SÒÝݪÑHo_ì ™5Kš× kFS²y—‘Þ\­#ÃÒ|þAéß×jµÔëm©×kéèìÌÕßúFÞrìñ[p;­;~r{^»pqn½eYѹõ–e9â¨7mÖc¯½æê±;c6TOÏÈc €_ û§ùÞµOeÚ¯¼-lÃKÖõ>uc®º|ýäâþÕõF¬z)OÝûpVç…,ÿú_æ;ߕϽiçÜ¡îííÍO<‘SO=5ð¦Ãó÷IDAT«V­ÊÉ'Ÿœ÷½ï}ùøÇ?žööö}ôÑýKæwtt¤s´+ÇgˆÀà³¾ 9÷-¾1p޾ßÕ6^¸¥T ‡.6­ç®µVal¾Øת‘ªª›1ÛÁøwÀN¬û‘kòoOïš7ž0o*ÕºÞÕøCóŽÅSFþbÏÏr埓ÿúþÿ‘‹Ÿúåœÿé³rÀNz¼×ã?ž“O>9‹/΂ ²téÒŒ7.çw^.»ì²TU•É“'gÊ”)éêêÚ¢çeû ±ChÂØœ!ÛpNYß!ƒéÿɆ PWC¿y•·¾çk ¾þUû6¢ÖœQÛ°m Ëåï@¶›³-᎟Ü^zØ §¿ç7Kol1ÍCNêcöÿÑì<àƒù—?¸_µ={Ÿö•ÜxÚ«|šÎù9ç«7æœ-²MÛ·ÓN;-GydÎ9眬[·.gžyfÎ;ï¼|èCÊE]”ÿøÇ9æ˜cÒÕµé)­ÍùœýLû 1–mî¿{Û;:³jåªL˜8¡YHµ$UóâÎÍ0j^»¬jT©µþ—lXÂ~´Þ{æYýìâ¯|9I+Êê­×©Z¦Öé‰FUeåŠçÒÙ9n£ç`û´Ó„™ó˶Î/cGS«Õ2aÂÄ<÷ܳÙeÚ®¥7‡P­VË1Ç“³Ï>;‹/Nggg–,Y’¥K—æüóÏÏ’%KrÓM7åÈ#Ìøñã7¹ã3O?•ÉS¦lò´Ÿi¶'›ó™N’Y³öν÷Ü™Ã_ÿK驪ÔZqVkkkJX«'URo«÷Ïb 4Ú8»ä⯠û\ƒ~Vkþcè$í¹ëÎåÙg¿ýFõÚŒ=;M˜”ÐÖÖžý÷Ÿ“{îü™0abvßcÏ­ryvNõz=ûØÇr 'dΜ9Ùe—]RUU>úÑæ”SNÉ 7Ü»îº+§žzê&ƒ¬Ñhä™§žÌÿ½õæ,^rHWºð–Ï4Û‡‘|¦“dî¼ù¹ù‡7åæe7äÀ…‹2mÚôƒi‹.ô1¼Ú€@~?h;ª*«V­ÈíËïÈúõë3{μ­¾]l `+jkoÏî{î™Î®q¹óŽÿÈÍ7]_z“Øô6™½ï^yàžåyô¡ûú©»»;çþöûrÇò;ók§¾3éíÎ÷¾ó­tt á‹z½žÉ“'gñÁ¯ËÌÝwÏúžžW|MŸi¶#ùL'I½­-GyT|ðþÜ²ìÆ¬[·nméÈÕjµŒïêÊköŸ9sæmÙkR”0ØŠjµZR«eæÌݳûî{eô«ÀÆúV«ÕjƒvΪªÊ ïxgÖ®]›Z­–‰'¦££cÓ;pUÒhôþÂXŸi¶›ù™îS«×3oþ‚Ì›¿`+o O˜l}þ…m¥^«eò¤IýßoîÎéæò™زP˜0(L˜&Ì f… 3€Â„@a  0aP˜0(L˜&Ì f… 3€Â„@a  0aP˜0(L˜&Ì f… 3€Â„@a  0aP˜0(L˜&Ì f… 3€Â„@a  0aP˜0(L˜&Ì f… 3€Â„@a  0aP˜0(L˜&Ì f… 3€Â„@a  0aP˜0(L˜&Ì f… 3€Â„@a  0aP˜0(L˜&Ì f… 3€Â„@a  0aP˜0(L˜&Ì f… 3€Â„@a  0aP˜0(L˜&Ì f… 3€Â„@a  0aP˜0(L˜&Ì f… 3€Â„@a  0aP˜0(L˜&Ì f… 3€Â„@a  0aP˜0(L˜&Ì f… 3€Â„@a  0aP˜0(L˜&Ì f… 3€Â„@a  0aP˜0(L˜&Ì f… 3€Â„@a  0aP˜0(L˜&Ì f… 3€Â„@a  0aP˜0(L˜&Ì f… 3€Â„@a  0aP˜0(L˜&Ì f… 3€Â„@a  0aP˜0(L˜&Ì f… 3€Â„@a  0aP˜0(L˜&Ì f… 3€Â„@a  0aP˜0(L˜&Ì f… 3€Â„@a  0aP˜0(L˜&Ì f… 3€Â„@a  0aP˜0(L˜&Ì f… 3€Â„@a  0aP˜0(L˜&Ì f… 3€Â„@a  0aP˜0(L˜&Ì f… 3€Â„@a  0aP˜0(L˜&Ì f…*ÌêõzÖ¬^½¥·`»Õh4FýØöÑ<¨kÂĬøù3I’;~rû¨_`G±fÍšLš4iTU˜í?gnî½ëÎÌ;?“¦LI½îˆH`çÔh4²fÍê<ôÀ}Y°pѨžcTa6}úÌ,X¸(<ô@Ö¾°fT/ °#¨×ë™0qR,<(Ó§ÏȺ׎ø9FfI2}úŒLŸ>c´ Å1ˆl—î»çδwtlòkØ^3€Â„@a  0aÀvé€ÊúžžM~ Û‹Q­Êxûm7oéíØ!¼váâ?fÔËåù+Gö¡;¤e7|Ts(#@a  0aP˜0(L˜&Ì f… 3€Â„@a  0aP˜0(L˜&Ì f… 3€Â„@a  0aP˜0(L˜&Ì f… 3€Â„@a  0aP˜0(L˜&Ì f… 3€Â„@a  0aP˜0(L˜&Ì f… 3€Â„@a  0aP˜0(L˜&Ì f… 3€Â„@a  0aP˜0(L˜&Ì f… 3€Â„@a  0aP˜0(L˜&Ì f… 3€Â„@a  0aP˜0(L˜&Ì f… 3€Â„@a  0aP؈¬^¯gÍêÕ[k[¶{FcÄiÉ»&LÌŠŸ?“$YvÃ÷Güb;º5kÖdÒ¤I#z̈Âlÿ9ssï]wfîÜù™4eJêuGB$Í™²5kVç¡îË‚…‹FôØ…Ùôé3³`á¢<òÐYûš½Àެ^¯gÂÄIY°ð LŸ>#==Ý›ýØ…YOOw¦L™’ƒÿË!#ÞH€ÅH¢,±*#@q  0aP˜0(L˜&Ì f… 3€Â„@a  °öÒCUU••+žËêçW§·w}ªª*½IÛ…öööL™25»ì:=õzmTÏaìGçÕŽ½0`L©ª*=öhÆuŽËœùóÓÕ5!µÚè"cgRUUÖ­{1O<þXì§Ùgß׌xÜŒýèl‰±fŒ)+ž{6mí™3ï€TUÕ«Õjƒfoú¾¸¼#Íî ݱï{oÃý¼o,ºº&dî¼rÿ½wgåŠç²ëô#zMcßTbìcÀ˜²bÅsÙkŸ}û¿ïÛ¸süJ;Î;¢¾8J²Q MI2kŸ}³âÙgGüZÆ~°m9ö €1¥ûå—3aÂÄ$ÏÌô}?ô–$F£Ø6o FcÐ{ÚÔ{K&LLwO÷ˆ_ÓØ7•{‡20¦4ÌD •.ÎXìH0t¶jèïú~>0þ¾wcaì›JŒ½3Æ”jÀNm­ïûªJª*C—S¨Z?øû-sëÉß]šO~þ‡YÙØRÏ9²[†¼·ag«†¹õGÃ(â üØ—÷Rc/ÌSB–$µZªÖ×3F£¹“ÜúÝ–½­Ï³Ë—åÖûžKwµ5žä·¾÷=p–f¸ûõÍöŒf«üؽqßVcïPFƔƀYš$© ç9%ý³ý_Rµîg¹éНåÊüGZѤ+3ç-É›Þuv~ý° îXmØ;/h£U“ ³X}ãP«õ=ê0ÛÚcßX“û¯»<—_½,wüìù¬O-“ö<0‡ó9ëäÙî7FÆ=Ù6c/ÌSªFcÐLE߬Mÿﮌ7Ú×xay¾ôGœ«Ÿ‘%oûõœ{àéêY™Gï¾3ϯ|PÙ m)iÈ8¤5cUðóZÌðŒòPÆ­:ö½+róÒßÏ_]ÿ|f½á9ãs3£ã¥<óðòÜû|#¯Ëõg9m^WÿìÇo<¡ÿ>Íר6ÌÜT/ç±ë/οv]î~¶'mÓæçͧÿnÎ~ó>_Kºº4~â[¹õú$2ëГrι§æ )õ¤wen»ôs¹ìÆ»ò芗“LÊÁçþu>zôŒ´x„ZjµÁçCõ}Ýw8Ý(f´¶îØWYõ£¿Íg®_“…g_” NÜ;}¿:âÍ9)I2ܸwçÉe—å‹—}/w<ýrÚ¦ÎΧüVÞ÷ö2©–¤Z—‡®ùÛ|îë7æ‘ÕU2n·vÖÇòûÇîž¶TYûÀwóEn|pUzÇí–Eǽ7ç¾ç Ù­cÄóÁV{aÀ˜2ô<§¡»¸CgmjCg3~‘ïÉ·oX• G|0ï˜ÛÕÿqèôT+ŸÌê=šc»þéïåSþcž}ÃYùƒ³ç¥ýÑków_ü«|rÊÿÊ_þêÞ£Ž¡­1ö €1¥o§¶ª­Éˆ—$üóÌ–%Y¿òá<ñr²çâYŸÆ°“BI󾿬P#Õú§rýå?LãÈóóÁS–db-Y°ßûóÓ[þ 7^ÿÓ¼gÞ¼tN™—Ãk=|î>9cù÷óÑ{ïγ= 2«j>ßÄýΡÏíŸ)ªªÑ.1?0 6œUµf›jµ¶Wf[eì_~&>]¥í5eÏŽÍ÷§sÃ×nÈ ÎÉ_þÖ1™QO²h~¦¯ü`.ü—+rÿqÈœµ?Ï ™”×-Z”fw¥–9­í|)|çŠÜ5ùÄ|ê·OÈÜÎ$ógåýwÿ(rý­yú¤½²×¨¦+·ÎØ 3Æ”ªoÅ¿¡«à UÕÙaŒiÆGÿΣ‘áw¡7„I£ÑH£û?sדɋ?û³¼wÙà{¶=½:=Þô<úï¹ôâ«rëOfUOGºj/'»¾”îÞFi­`ØZÑðÕ]ñ«–Z­Ú(lªªJ½Þü}Õzo#µUǾQ¥1pL‡¿Óq"w<‘ìù®2%4ßÒ¸ì{Èì´_ûPî{®' öGÞùÚÛréçæ£ÞšÞv\Ž˜35mÕ ùÙ}Ï%Ï]•œ~Õà—™òTžïndq›¿ùM[oì…cJ£ê›µ©Òh´h{Å@èûy²¹Ö&ï™íÉC÷<‘ß¼Kº†ßŠV8´â­s»û{ùý·î1h'º>af:×Þ¿ûÓ/æÆÝßž÷}øw²ß¤žqož~yIöö¯¡ã¾áµŽ[ÿÒýF{ç|6‡üäßsõ•Wæ3ø\õ«’ ß5½‚³Þ• >ôúL°¦K­}Rf¶÷…Þ¦l»±w3Æ”þkd5©ªF½­˜éímîˆ÷önt«ÍßmÎ-]órìëºò²ËsÝÏ^z…ûµvÀ«Þô6©Úgæ€Ý’>“ñ»ï•Y{m¸í¹KGzW=œVwdѯ¾3G½vÿì»ïþÙ·®þY¹æ…š3èûÍ¿5ßßðï»5}ñXU­m¯F}(ãVûLÍ’£HÛÏÿ5—ÿð™¬ß¬qß- ÷Jžúñ=Y¹¾ï>/æ§·=’õ_“9Sk­m—=>>¿uÁ_çÂã¦ä‘k¾“‡^ŸY³§%ÏÜŸU“öôw¶×nSÒQ­±7cÀ˜Rµfjý;º.fÜ}­Ö×í ø‹¯¯5!‹No»ç ùê]‡N|K^7{zÆ7^È3<§ö81¿yÔ.?mBòã[³ìÎ×å„…3rÄ;É•Ÿ½$Ÿü캜rÄœìR_›gžx)óù•ì=qVöؓ۾ùÍ\Ÿ×eÖÄFûùKIªô6zÓÛ7Tõ¦··7½›1›íí™ÝÑ›—Ÿº-×-OöÙwzÆ­6w<º6éššñiË>ǘù×}5_ø‹¿ÏŠ·š}&5²æ©§S_ü–¼~·WN¡c/ÌSš³iãÔh¶ö ç;mt]­ÍÜ®ïzDÎýóiù·+®ÌuÿöÕܼ¶7I{&ï9/‡¿>½Ugö9îÝùå».Î7¿zsûÓ“²×á¿?îýç\òÍïä‹·¼˜*™:û9ç—ʬÉæ=ç½+/]üÝ|á/šç3ÕÆMÉ^‹fd\¬:?`ø_¤•Cƒ–×衪J•Zó»!ãÑ·‚ûègÌ¶âØ·ï•ã>ü©Ìüî?çªë¯Î?ܰ.U’ÎiûäÀ×ïŸîÆ0ã~ðÙ¹ðw'çËÿçk¹èûÝ©OÞ/¯?ýüœqÜ^i¯’—V=’}óš\²r}’¶LÙïÐüƹ'eßö¤¶Û±ù½ ÚòµË®Î?öy9Éøé òÖýß”Ãg¾r •ûÚ—_Z{ü‰#~ l ×]{MN<é”tww§j4ÒÛ·“;dæ&ižkÔÜYNF¿ðüX×̃*Í÷:h.gÀ÷µZ-õz[êõZ::;sõ·¾‘·{üˆ^ÉØµmÆþÚk®6cÀØ2xÖ¦5Ñ¿èÂÀÅú~WÛpXÙŽ¨jl8|®Ñh½ÏZk%Àæ!týw­©ªúš13öÛrì…cKë0ºæ,͆󚟿4à|§ÖŸµtÒ¦ï½5²áœ®þ•«*õZ-©5guúÆ©j-l1ò3öm˱fŒ)íYµrU&LœÐÚ3NR5/êÛÜ!n^?«jT©µþ—lXº|Kyï™g úþ+_þÒ{îÍÑ¿E•Öat ÞzÏUëÏÔúOˆjTUV®x.#¾Hטû-eèßáP¯ôwZdìGüØŠfÍÚ;÷Þsgý/¥§ªš³1µ¤ÖÖÖ<„¬VOª¤ÞV媌›ç’‹¿²Ežg´†{_ƒ~Vkþcè‚í¹ëÎåÙg¿ýFüšceì·”Ñþ–{aÀ˜2wÞüÜüÛró²ràÂE™6múÆ;Ê;ìbÃ럱é_-pãsºªªÊªU+rûò;²~ýúÌž3oįcì7¶­Æ^˜0¦ÔÛÚrÄ‘GåÁïÏ-Ën̺uëJoÒv¡V«e|WW^³ÿìÌ™3oÓ×âzÆ~t¶ÄØ 3ÆœZ½žyódÞü¥7e§cì˨—Þ€0(L˜&Ì f… 3€Â„@a  0aP˜0(¬=I®½æêÒÛ°Óúÿ.Õ˜›ÜøÈ@IEND®B`‚libindi/libs/indibase/alignment/DriverCommon.h0000664000175000017500000000100713263645557020732 0ustar jasemjasem/*! * \file DriverCommon.h * * \author Roger James * \date 28th January 2014 * */ #pragma once #include "indilogger.h" namespace INDI { namespace AlignmentSubsystem { #define ASSDEBUG(msg) INDI::Logger::getInstance().print("alignmentSubsystem", DBG_ALIGNMENT, __FILE__, __LINE__, msg) #define ASSDEBUGF(msg, ...) \ INDI::Logger::getInstance().print("AlignmentSubsystem", DBG_ALIGNMENT, __FILE__, __LINE__, msg, __VA_ARGS__) extern int DBG_ALIGNMENT; } // namespace AlignmentSubsystem } // namespace INDI libindi/libs/indibase/alignment/SVDMathPlugin.cpp0000664000175000017500000001117413263645557021314 0ustar jasemjasem/// \file SVDMathPlugin.cpp /// \author Roger James /// \date 13th November 2013 #include "SVDMathPlugin.h" #include "DriverCommon.h" #include namespace INDI { namespace AlignmentSubsystem { // Standard functions required for all plugins extern "C" { SVDMathPlugin *Create() { return new SVDMathPlugin; } void Destroy(SVDMathPlugin *pPlugin) { delete pPlugin; } const char *GetDisplayName() { return "SVD Math Plugin"; } } void SVDMathPlugin::CalculateTransformMatrices(const TelescopeDirectionVector &Alpha1, const TelescopeDirectionVector &Alpha2, const TelescopeDirectionVector &Alpha3, const TelescopeDirectionVector &Beta1, const TelescopeDirectionVector &Beta2, const TelescopeDirectionVector &Beta3, gsl_matrix *pAlphaToBeta, gsl_matrix *pBetaToAlpha) { int GslRetcode; // Set up the column vectors gsl_matrix *pAlphaMatrix = gsl_matrix_alloc(3, 3); gsl_matrix_set(pAlphaMatrix, 0, 0, Alpha1.x); gsl_matrix_set(pAlphaMatrix, 1, 0, Alpha1.y); gsl_matrix_set(pAlphaMatrix, 2, 0, Alpha1.z); gsl_matrix_set(pAlphaMatrix, 0, 1, Alpha2.x); gsl_matrix_set(pAlphaMatrix, 1, 1, Alpha2.y); gsl_matrix_set(pAlphaMatrix, 2, 1, Alpha2.z); gsl_matrix_set(pAlphaMatrix, 0, 2, Alpha3.x); gsl_matrix_set(pAlphaMatrix, 1, 2, Alpha3.y); gsl_matrix_set(pAlphaMatrix, 2, 2, Alpha3.z); Dump3x3("AlphaMatrix", pAlphaMatrix); gsl_matrix *pBetaMatrix = gsl_matrix_alloc(3, 3); gsl_matrix_set(pBetaMatrix, 0, 0, Beta1.x); gsl_matrix_set(pBetaMatrix, 1, 0, Beta1.y); gsl_matrix_set(pBetaMatrix, 2, 0, Beta1.z); gsl_matrix_set(pBetaMatrix, 0, 1, Beta2.x); gsl_matrix_set(pBetaMatrix, 1, 1, Beta2.y); gsl_matrix_set(pBetaMatrix, 2, 1, Beta2.z); gsl_matrix_set(pBetaMatrix, 0, 2, Beta3.x); gsl_matrix_set(pBetaMatrix, 1, 2, Beta3.y); gsl_matrix_set(pBetaMatrix, 2, 2, Beta3.z); Dump3x3("BetaMatrix", pBetaMatrix); // Use Markley's singular value decomposition (SVD) method // A detailed description can be found here // http://www.control.auc.dk/~tb/best/aug23-Bak-svdalg.pdf // 1. Transpose the alpha matrix GslRetcode = gsl_matrix_transpose(pAlphaMatrix); // 2. Compute the first intermediate matrix gsl_matrix *pIntermediateMatrix1 = gsl_matrix_alloc(3, 3); MatrixMatrixMultiply(pBetaMatrix, pAlphaMatrix, pIntermediateMatrix1); // 3. Compute the singular value decomoposition of the intermediate matrix gsl_matrix *pV = gsl_matrix_alloc(3, 3); gsl_vector *pS = gsl_vector_alloc(3); gsl_vector *pWork = gsl_vector_alloc(3); GslRetcode = gsl_linalg_SV_decomp(pIntermediateMatrix1, pV, pS, pWork); // The intermediate matrix now contains the U matrix // The V matrix is untransposed // 4. Compute the diagonal matrix gsl_matrix *pDiagonal = gsl_matrix_calloc(3, 3); gsl_matrix_set(pDiagonal, 0, 0, 1); gsl_matrix_set(pDiagonal, 1, 1, 1); gsl_matrix_set(pDiagonal, 2, 2, Matrix3x3Determinant(pIntermediateMatrix1) * Matrix3x3Determinant(pV)); // 5. Compute the transform gsl_matrix_transpose(pV); gsl_matrix *pIntermediateMatrix2 = gsl_matrix_alloc(3, 3); MatrixMatrixMultiply(pIntermediateMatrix1, pDiagonal, pIntermediateMatrix2); MatrixMatrixMultiply(pIntermediateMatrix2, pV, pAlphaToBeta); Dump3x3("AlphaToBeta", pAlphaToBeta); if (nullptr != pBetaToAlpha) { // Invert the matrix to get the Apparent to Actual transform if (!MatrixInvert3x3(pAlphaToBeta, pBetaToAlpha)) { // pAlphaToBeta is singular and therefore is not a true transform // and cannot be inverted. This probably means it contains at least // one row or column that contains only zeroes gsl_matrix_set_identity(pBetaToAlpha); ASSDEBUG("CalculateTransformMatrices - AlphaToBeta matrix is singular!"); IDMessage(nullptr, "Calculated Celestial to Telescope transformation matrix is singular (not a true transform)."); } Dump3x3("BetaToAlpha", pBetaToAlpha); } // Clean up gsl_matrix_free(pIntermediateMatrix1); gsl_matrix_free(pV); gsl_vector_free(pS); gsl_vector_free(pWork); gsl_matrix_free(pDiagonal); gsl_matrix_free(pIntermediateMatrix2); gsl_matrix_free(pBetaMatrix); gsl_matrix_free(pAlphaMatrix); } } // namespace AlignmentSubsystem } // namespace INDI libindi/libs/indibase/alignment/InMemoryDatabase.h0000664000175000017500000000526013263645557021517 0ustar jasemjasem/*! * \file InMemoryDatabase.h * * \author Roger James * \date 13th November 2013 * */ #pragma once #include "Common.h" #include #include namespace INDI { namespace AlignmentSubsystem { /// \class InMemoryDatabase /// \brief This class provides the driver side API to the in memory alignment database. class InMemoryDatabase { public: /// \brief Default constructor InMemoryDatabase(); /// \brief Virtual destructor virtual ~InMemoryDatabase() {} typedef std::vector AlignmentDatabaseType; // Public methods /// \brief Check if a entry already exists in the database /// \param[in] CandidateEntry The candidate entry to check /// \param[in] Tolerance The % tolerance used in the checking process (default 0.1%) /// \return True if an entry already exists within the required tolerance bool CheckForDuplicateSyncPoint(const AlignmentDatabaseEntry &CandidateEntry, double Tolerance = 0.1) const; /// \brief Get a reference to the in memory database. /// \return A reference to the in memory database. AlignmentDatabaseType &GetAlignmentDatabase() { return MySyncPoints; } /// \brief Get the database reference position /// \param[in] Position A pointer to a ln_lnlat_posn object to retunr the current position in /// \return True if successful bool GetDatabaseReferencePosition(ln_lnlat_posn &Position); /// \brief Load the database from persistent storage /// \param[in] DeviceName The name of the current device. /// \return True if successful bool LoadDatabase(const char *DeviceName); /// \brief Save the database to persistent storage /// \param[in] DeviceName The name of the current device. /// \return True if successful bool SaveDatabase(const char *DeviceName); /// \brief Set the database reference position /// \param[in] Latitude /// \param[in] Longitude void SetDatabaseReferencePosition(double Latitude, double Longitude); typedef void (*LoadDatabaseCallbackPointer_t)(void *); /// \brief Set the function to be called when the database is loaded or reloaded /// \param[in] CallbackPointer A pointer to the class function to call /// \param[in] ThisPointer A pointer to the class object of the callback function void SetLoadDatabaseCallback(LoadDatabaseCallbackPointer_t CallbackPointer, void *ThisPointer); private: AlignmentDatabaseType MySyncPoints; ln_lnlat_posn DatabaseReferencePosition; bool DatabaseReferencePositionIsValid; LoadDatabaseCallbackPointer_t LoadDatabaseCallback; void *LoadDatabaseCallbackThisPointer; }; } // namespace AlignmentSubsystem } // namespace INDI libindi/libs/indibase/alignment/toolsmenu.png0000664000175000017500000031336513263645557020725 0ustar jasemjasem‰PNG  IHDR5okµêJsBIT|dˆtEXtSoftwaregnome-screenshotï¿> IDATxœìÝy|TÕùøñÏÝfÉLVBV¾#‹@-ŠHÝwQj[í¢­­Uë·ÖZ[mkkí¦µµ­K­u-(ZWÀÖ*¸ È"{!ûžÉì÷Þs~Lž@ ¢çý2’ÉÜ{ι“ÌÌç>ç9ÚÐ`ŽR"„Dj!%C&AQ>ˤ”_®ºÑ×CRå0s]]×д¿EQEQ”OŸ•¬ACC©36 ÓDÇE ëE“FPTX°c¯8·ëÙ.ZûèñvÛaß-Ðéì~wÒºüÿucgm·z¿ýØâàîîÞ^{i¤ãÇÁ !$B¸¬Û°…‚üÜêYQ”#“’ÚúFŽ3Ó41 ÃÔÑuý{Þ¶‡o==¤í¹¹^ìc÷¦zþ~}PdçÿvþQ¯¶¿§õb/rŸ7»uOOúèY+ÝÜZHÛ=ìSöòïv}ÊC×ÁÚïÝÎänßìs«CÔþ÷Ù{ÍžwÞí§½úÞë E/µ¿—ŸöRÝÿ“9°îOrßtkDñ7{hÚ?€ž°=z=ìãÀÖîïuÀkǎǤþYõá:tMCJ‰ihºf;a(ÙÙYÄâñíJQ>õv jtH&í>•¢(‡“ešTV×aš†a ëÆNYŠ¢(Š¢(ʧKGÌbÂưvÙt]Çô›icKÈÊÊDê0¶¢| ì˜z"Böþ…˜¦—Q£G£ëF— Kj,R4]£¬¬ŒD<¢>\)Ê!ä æîdH©÷õEQEQ”ýÈÊÊdÜÔQlX¹Ý«›}=Eé3}ÇkljæÞßüŽôôL<^þ@ôÁŒtС¼r;••UÜ~û´´„ÿåsD ÑXìë‘(Š¢(Š¢(=¥KÓ]HffÆeiH)R EjÎYOÛR¶_“·é,ù‘ú·ã*uÇv{ºj½Çv¤MCÓ@CÇ0Ró¤u]ßk;ÊçC×é'RþL ×q±,‹P(ÄœkÎÁ]Ó ÇBÔÕ×2EŒaÙ×à¸Îa_'#‡c.¾˜ 5/ðØÿj9¨‘ôf[Êg—æ§xÜXòÃëY¹5Âáxv¦^:2·t4ME7EQEQŽ™™Œ8z&p@Á!Òض㺸®ì ††i©9Ë{ H)S¡‹ö“IÛv®‹!%z{B7 ,ËMOÕ'Û% !e{Ý2)ºÑމnTNù¬èºòI_]µm›òòr~{矉D#„Ãal;UÓ#@Ó´ÎÛ}FOgØ”IŒþൃ¯ ØÓ¶4y#F’ÙÄšÊØ!, §RšI §d#õmÎþº‡ìÂúUn>£väS±ðžçEQEQ”¾Õã¹'RJ„+±›D<Á¹Ã‹8¦8‡ÁYéäýÔ†clmiãƒÊ&^Ø\…ÏçÅ4ÍÝÚ =³Â±É$ƒ¦¸¸ˆ~ýrÉÎ΢¹¹…ÆÆ*+«Øºu+^Ó2SìÈÜ@Kµç8‰D’¢¢"†FVv6éÁ áp˜––V¶lÙBEE^<†‘Z³ÏRຠCÅX>7rrrøáˆiš!v `hš†eY”””tõkÍWÄ1gœÍìãÆ18Ëâ4–­ãÝWæòâÇ͇åJøñ ä¼ë¯cЫ?áŽÊîþ÷Púˆf¦S4b$Ãä‘éÓ‡XKÛ7®eCc€Ñ_˜JææÿñfÛ¡ËÏÙçjã*(¦(Š¢(ŠòÖ£LŽb†É¤MÎ=gL¡(èE „ƒŒ´’oèä÷0-?ƒ‹Fòƒ·×RŸHbyîH)¹í¶Ûxá…8ï¼óp]—'Ÿ|’«®º €yóæTZ`—þà{Ì*lbÍ_æï›ë‰›YA¦ù© g(GÍÊeÜ Ç24=N}ÙFV6EptýúáÕ{ó¹¥aeäSèo£²6²Së Ç°ËëÊÔPEQE9²ô(¨!¤À¶н÷Í‹_ÚØ-m{Ý>ßããÁ“Çñíÿ®¡Îu0ÍT¦…Çuñz¼œvÚ, S#§Ò€EÇX’hº††F èçK³g³`ÁBׯ4-t½=ÓÃq1 ƒÓN›…Çk‰¤ÚAv׎vÒ3‚œvÚi¼þú’ŽGÓ0ŒTº—.‘p6çýþ~.ȃªýˆ[^oÅãh”\rUÔòê/îæ™²8Ò?†³¿u.§=˜`‡(ÿd¯=ûK]âLâŽ|ƒÑÔòì÷ïàÕˆŸ´ˆ³(=ÓùáåêÀôÇq˜?>–e‘H$xöÙg;ï›7o‡øÁ,­¬ùyÁÕÌ*¬ä¥»à e;®VøÞ›íß™̼Ž[.K¶H6²î͹<<ÿcš÷–ad2îôK™sêD4ܶ2þé>æV â÷ޘʬx½0òfñÓ_|‰ò{o㡲]ê^ß…ÞÉC¦¾ßøàÍüjyŒ,&œy—Ìœ@QDª>æ¹OóòšÖÔ‡]=“I\Éùǧ$Ë DYû_ðû%MŸÞÌ”#’IΘ£šÞÆÆ%ï²¾yG&FUÅÖöM‚ÇÌàì1©5.[È;5:c&2º8‡ ŸØÔ¼˜÷+÷Þ›7o K©«‹àv>m»1†ýØ) Ñ‡¯ Š¢(Š¢(ÊéÑô)À±“Ü1m8f¤‰D"¶ï"!,_?=º˜¯¿SŠÙ>å!°“I¾xÒt¤tˆEÝ®”uÔ¼Ð4 DZ±,Ç ÿùÏÿ0õT !ÉD‚“O™®C,ÛO;–e1}ú´ÎvzÔØÓcb»ä̾‘ŸU´ñÞ÷òÄfCÌ•wÞÀi9]6¶28q"ùOÿ ©éìˆa"Í­D 9Ù^ $v$B(Þžå†aY¤|øL ¤ Kº8ígøšá!3ËnÇ G’$ÝT±UÝ´føñª• ?5ÇáüóÏà™gžaΜ9!xì±Ç¸æškxúé§¼ÿpN›–AlÙ?X°uoé÷.¡ÿã©_§%¢‘sÔl®:ç¾\z÷-ï¾æeØù7qól“^ø;sKÃX™éD¤öG÷únüσüiI#’xC4Ã/¼™ïÍ‚¥sâéJ(9þ|.¹á&£FeÓØdâ3vgsùÌùÍCÌ(šoßþQÃÃ9·ñ­“‘›fqêÖ¿ÏsÎåýV ÝÍ`êœk˜=y8Cò@¼â?üâŽÑ8î<~ðÿNfbd‚–mKyè×O±ÎõâQ)æ}˜jîº.óæÍÃ4M"‘O>ùdç}ÿú׿0M“Xl?AÃ}03PàÚOª‰ïõ%Ñí«ùh{êÖ–íaŠû13ÆöÇZ&¹ËÖZpçÍÌ£úÅ»øë+Õ;¯`âíé»×w²¥†íuÓ ´ô£8ï”\j^¼‹G¦Æ°vC-ÞA·sæ¹ãxý¾åtäŽE+×°rí¶ÝŽCéº7ƒ áú¶ý®fãÆÃ´†";eí¯Cv¨žšú–ÓIz˜©Ö“1ìÍNÓNÔôEQEQ”#N{P£{ÛŽËûûq"!„“º:[wùÝGÛ¹yR!csÒØØãÞU\?y ¾Ô6N$ÄŒþ~^«¡ë&®#(,*$‹áº©ÓY)»ÎgÞ‘e‘:BPTTHuU5–Çu)** OµÓ±ë¬Y§ìÖΛo¾mÛhZª¯ÅETWU÷lõ ¹sÝÆÌÉWrçðáqØô¯{¸gQ#¾ ®‹Œ‡ˆéd2râ(Š·,§"ÙDéú6ü>]ì|ÝØnk¡Íd}¡kHaã¤åÒÏJÒÖ*ðeÈ=ƒë~dÛÍsÙªårÜ©nØ$ ŸÞJ8}ß½étFkà¶ÖQ—ô“›ã!àëþïú³¬cÅߎ¯¾àº.]t?þ8W^y%By䮸â þùÏDZû*#rE-ò¦žÍeg~‘…YøÜ¶l¹ÇJ¬Ü‘ 0C¬ø¤±–díYß{õA±by×18õ¬ÝæÜñÃÉ5—Ó¦þÆ?3ŒìñÌš>°KþÆN;kÑÍ‹ùßöƒï£c!ñ¾~MPEQEQL{šÂþÏâ44„ëRâÓ‘(²ýCùQyYÜrö¹÷¥ÿqáà,æomå†3Nd¼B´5 „Ë€€ẩ"ŸÂ%L&R½ïrÙ1mdÇÏSû¹®C - )Dj9XáH&“;µÑq…»£®ë´ÿë’ÖÞÎÁ,/>€èûæ—/VàÉô¢#Á°ðÔ,äÑÿLåÖ™y =ó[üîÌ8+Þæ•^å­m6é¾®ýÖòü·3¯ÊE7¼d¤[xl{èF.ù«E03@ÎTnüù†õÇØ¬y”µvìbÑ·ð×6†×ÂS|y@)ÞñKÖ9`øÉH÷aiêl=Eîòuø !xöÙgI&“¸®Ë?þñ €{úé§1 ã 25ܶ*ê:"ïâöTÃp:ßûæ©ÈÅOó—ÇËhÑ 8åÚk˜º·FõT dϘDHÐ-£[K¶ö¸oåSG$Úˆ ÈÎ `”Ç{!е;7´‰wÞ.Ç@Ã?p2SûU°te-I)q··ÆÐ÷¯ Š¢(Š¢(Êé~¡ÐöœiÒî Fˆ–zF¥Û\tÌx[²’+ŽŸÀ8Ñ„jÚ±¯¦ávg&†h_еcšˆ¦i¼ûîÒ½v}üñÇvîëõ™ºú,ÚƒËav0ºO×ïSÙ©Û^o{;=¹,·—mÓ¦}™¯¯¼›Þk#--õ¡Îò;¬ûǹöiÌžõEfLÊ€£Oã›GŸÄ´Gʽo·€¯k+:¾ ¿HA"æã¨Ë¿Çµ§'}§Þ<¤yv=íÖð¦è.Ñšø°q³û åë÷ÝǬ¥oòâü×XZïTsO€Ý ö×u™3g<ò×\s Bþú׿rõÕWðÐCpû2º…7WÅì¹|ñõ?° j÷º¾‚°•G^\ÂêV zœªèîmiíÁ »¡”qcÆöÃ,Ýeú‰¥!SJúáÕªqöò°v´åÝ_ßÒ!nƒ'èE‡Îé vý&*Ý“=¦ËÌ\Æ âTn¦Á¡ÇS”ä4³µÖ!oÀ(mYÊ–¶=T—•[€á1öÛ¦‘ ޹qÚZ〆s‘nŒPkëŽ)UZ7ư?²½°4jõEQEQ”#Q·§Ÿè25¤-–Àë ¤Ü1}b}yϯoåòã&ð²O<*“1™;†5]'‹§¦’è©vbÑ8R¦N@5MgÆŒ“:ëht訯‹Å2•‹¦Ú¡}Ûx<¦µÄÔw5º¶ÓA©+q‰D¢=¤gX×Í«ÿý ¼Š«&erܵ·’ˆÜÅßÖØ¼©¾5S'¾m)Ïýy Oÿm³¿÷C®™ècâù§2à­gØÚÙž†®uI¶hã¿Î §Çãnç?ϼÁºÄÎùê,vœìïa`R3ð›e<~ÛÝl;ÿÎ9õ({&7;…~t'së4ÒÔš±;§™÷Ñç!?ü0¶m#„àÁD×ë%‰ ë©B¸L†YõìÓ|4ò«Ìùñ­ ^ð+¶6‘Ѓä F~íBæU”ÑÈLfŸu¡¥Ûi9äw ´‰ÍQÈ;•£ kYQ³Š7s˹ßá:ý%onÆMËÆ_³‚+êX¾¬žsN¿œ/Ÿ>Ÿ%[Ãèy%;r»´µ¦n?}Û l¬r™1ýlf•¾Év-‡Œ–•¼[º†Þlà¶ó®ãj{>ïTh”œp>çæ×óú£k «Ï£‡LR»v Õý&1î¤ÈÚ¼šÖŽæ!™M ²…5QšÚ$ƒKF1¬y+­øðÅkÙÞº—6E*˜åë_L^0BMØÙ÷S´;cØÝg]_ ÔôEQEQ”#O·§Ÿ ItÝ`S[œq†@¸©+¿›Û~¿±ïœz Gy“ŒúÒqÜûÚ»Ü02Èè ]³ØN ·¯~¢ë:ÍÍ-¤§R ë:Ñh]ß±RIgV‡_š¦ÓÖÖÖÞNªˆhkk¨½‰®ë@jŸÔ÷íG×~Æê8n{;áp¨sûîÛešL|;¯ÿþWpë¹jL63nü•·þšW› L‚Œž\DËú l $ašB6àO µI2LÄŒ,†ùHT„H˜>†Nfq^jyù›Ì}e1Þ1L¹|}»jÇØ$®æ'(ÊXôß²è™Á\üËŸpIaÇMÉeî¿‘†Þ­éŸm}Ÿj.„è\åäoû×^{-RJþò—¿tfTPM𗟶pʹgpòŒ9Ї¶š-¬Xdàn•?=ɗϺŒg¤ž/Ép›k¢©•BÜÞ{îM¦~u—Ÿ²‚5O–²î™ßpxžr%ß=[ƒD=ËžÜÀ²Š&¶¿üg \Å…ç\ÍTp£4–¯§¬ÍÝ­­ÛžÚOß2ÄGO>Ż߼‹®N3kç—²tK5›çýŽ?Ä/eÎYß`š¢U«xñ¾§x¹4¡&f2VɲÿÅ2jƒ£Ø£‚d¸™šRMF©Z½šþSÆ2fjˆ8õ뛩Ø[PCFÙ¾n+…“sÔjêV7·géH›ßâåÍ0ö÷,ßõõ@ý)Š¢(Š¢I´é'+½Þý/£gè:Ž#8ÁáÚþ6nûr®Õ¤ÑÖ c’õ¸­èéYlN+Æ[WF±Lå“Þ4¨³Xê¤a‰dœôô ƒ Äm¯³¡ë:†¡ïÔpÝA Ó4)+ÛF4ÅãñîÖŽ®k¬^ýIgñÑ]Mš4!$¦iR^¾ÖÖ6L³'S2ñx.¸÷..ȃªýˆï¿RôŒâê{nfVP·ˆÿðJóçpÿݧÑo­„ßùßýë:lÒ™~û¯ùæÈTp%µ±b‹¹ý槨þ5þ|ûqpU•Q?ƒ‡फ़çþïG<[?š[º‰ÉfˆEwü¯2ð“$Qx9üìd¡F¢yY¸¬~àûܽ666òÕ¯\InnΞšQå X¦Im}#–i`š&¦i`FçŠ[{ÊöSEQEQ>]‰d÷§Ÿ8®ÄòX¼28#Ãf€¦#œ$E~¢U54Ü–†¹.x"ê [> KÞ ƒ©Ì ÓôÐÔÔL¿Ü\¼+•=¡k®Ñ~2Ù‘,q…‹©,Œ¦æZš›IK ‚¶{;Ž+™4i"†¡·OEI]£"õ6‹¡k‘h”ƆFü4„Û³«á»?T~w=ÿîUÆýü ŠòfqÜܶp+‹?(åøqƒÉ ´_‰n.gÕ;¯òÏçÖ!¼&ÙÆ’?ý…’k/eÖ˜|iáZeêˆOñ«' ®9{ CІ0 ¢ªl-åñ=LA‘ ѱô(u1Æåõ£0ÜP.šËÃFñyM•ZM߯~’““Åoû ØsPÃ4M¤””––’yø¨(Ÿ#»¾¨×HEQEQ”#‹6ý„c¥ÇcukcÃ0® ËnãÎüÒî\ÚuOtÓ"¤Yü &¸7€e¥úBb'“¸B0lØàÎÌŒK¸îÈÔR¢é:•lÙ²Ã4ñX^t]ëlGHÉСCÐtR+£ti'ÕŸ@ÊT‘B¤Fii) ¡ÚœÁ âá º×Gz{ý éÚ„#Ð=^I2î`»ÑYÔNÇ´Lü^½#*!\b1›¤Û^]Ã0IK³05‰›´‰%\ÜÎý5tÝÀç·°4—H[ O ½Àhû8¢1GìèÓò˜ø¼ªLhÊ®™áHì°fj(ŠÒ÷:25LÃhÏÒÐU¦†¢tÛŽç‡zª(Š¢(})™´SAŽ`CwX¶À›ŒóõŒS¼ í«ŠH 'ƒºÁG /…2Hxüx½^`Çj$B¤ŠuºŽKQQAj‰Õ]*ÏwœXF£Qª«k0 ¯Ï·Óɦ’d"ë ŠŠ ðûý54vÐÚ—„u©¨¨DeYS_”Ï—]ƒ‘¨ j(ÊçMGPC×uLC5¥ç´. M7EQ”>aÛv–tm—L$ðz½ØšÁo[ ޱÂ|Ñc˜i“® Ú„ÎÇâ­˜Ÿì i~^ËêŒ1h¤²0tMKµ£'©¨¨$ LàóùÐ4)ñxœp[„H$Œ×ãÃòXhh éÌæÐ5 ׋m'Ù¾½‚ÌÌ Á@*ˆ"5Ð$Éd’H8Bkk׃Dz°]WåNí´¤«úP”Ï5)nçÊ[b§¬AEQöDëø/u!‹Žs2ÔóFQEéÝ_ý¤‹D"Ži™Óƒ¬²½|±ÂE‰®§¦HX–EºÏÚãÜN Ë‹eX$m›úÚ†T ö“JC70,“@ZM×;¯t´¹[;¦I,ž ­-Ò>å$µbŠ®ë˜¦AZ{ Ûu sÙWõ©öóG­t (JŠ.h ‰@J•©¡(û¤¥.PiÁ@-UÃLÓ4${Yv^QEQ¡n Ý•tÐ4Ã0ñûý] svOg@BךŽW÷"­ŽUXv¼-víZwcoíaàõx-«ó£jj&ŠÀ`ï4½@SWé?§v- xÇP€ôõÈE9\î©ôæ÷÷õ0åÈ#SgiŽã¶f×éˆm¤â*¬¡(Š¢^=ž~ÒUª.A[•"PŽ ;O?IýíŸyÌø>•¢(‡Ó]Ϭ¦ ?·¯‡¡(G$)$µõx=Vû…¥TpCQEQúÂM?Q”#ÛîÓO¤èÙÒ¾Š¢|6¨ÁŠÒs–™:}B¤¦£tÖŠW EQåð;àé'Šr¤ÚuúIêg*¨¡(Êa¦é×A×u2‚^,3Ic‹zCVŽ®+º ¥³¶š¢(Š¢N5ýDQŽD]W>‘™ê9 (Ê¡4$a7õaÏu]D2Áò×P\2Aòøëµ1‚ "Vˆµž]4„[º¿Üº¢ô!ºÐš†:©ì UhWQE9œL€W_]Ð×ãP”ÃÆu]lÛ&‘HŽDøä¶K>;™"κ Û© sBIZÇü2EQúØÄLÁ;M#´F2d˜pf6¿ü®ºúkÜúƒ›¸ö$9ÁÒ.™UË}?mâÛ?΢t«÷ð ÐÈᘋ/fBÍ <ö¿ZœÃÓ«²'z&ãgÍdHõxyU+Ÿæw§ßþæ÷dddž‘A0ÀëõbY†aôõÐEQ”ω3Θ­>ó( ®š.5[·±*´£/Ãò•žÎ€þ™ä{{a.²9š!÷Ígü¦;H,Ú ªå(ʧĈÜ*Š=‚U 6§O +;Ÿ¦  3h ‡h{ !ÈOîdzGUqÜÑ!J·ö?<ÔÓ6e£?xM-ÉÙÁ3„ËzG¯ÿ#w<¾‘Øáê×ìÇäÙ³õßwyõSÔPEQ”OÔPWMtü7,æ‚£3:zE´”Zù ëŸ{–ºXã2̓û@![*0âq4)TPCùÜÑó!ê¶õõ0:iüòK[(­ òø†þüt¬ËÇ ‡ÅááH˜ÓO?#†í´Ï׿ÔBÐòÎ…ÝïHÏeÖ?ã²â½m°Çnù5o·|Þ>"›”œÿîüRºù~> ïûU1pôwù÷²™ûÏyµ1Ncu55QÜÃ4ZEQEQzN5àpÔÔÐ ô¬ ¨|‚ð‚uèFzÿ1§^Ç´SþÛoPS/)´éPW×À¦¦Q–/À¢\ú¡q{9+â™LžMz{$ÞTÉ⃉#„î8—ª`Ó ½èÒ¥±¡QÂ.h¦—A%ŒHÓ÷ÑŽ† ÔÔÈ'uaÚ\@7é_PÈÄìƒ ¼(Ê!øöýÄçß½ü¾ W¿…±SFpëϺa°°%ª˜CUC%­­!ôd„êê úgpÕ„ÎøB„´Ì8?z`[+ÝïH¶²ì±ßSé׋âÙ×pÙõ<ñðÿ¨q¥2üy h84n©ÁÑ‹ÑÏä£pûj7Ö@ÎûÞW½é1~÷B9ÉÔÉ^€ÛLi‹ N5 þx7j‚îá'¥¤¶¦ŠÆ†zlÛ>¤µß<‹œ~yä©"§Š¢(G(ÔPW¦FûIYx ɲ1%°é=’¼†yéý üYŽ IDATîXû“»Zê5êÏø'éx¼>³m!Ÿüí·489äÞ´” òŸaËýó¢’ð1OpÁå!B¿ý;Áû_gÌêïx³ ‰¤µ!IÅÉ çÌ БmëhûçíȪÍ{íÇKŽc³íèÛ™vé¤5HÖù÷pWÖª åSÍ;û+Ã&Ÿ?ÒNôéXÒ r1ŒzFõgc•ËGõ’¶¸M<gP¶—4ËËcß?Þ X”ÍÂO¼ÜÿÁH\ÑÃWÒ¦yëFšð`cCq#¥ë׳µcÕZ³“/¼Œ‹gŒ#ß'o_ÎËO<Í¢Ò螟Óf?&Ÿ»—í5ƒO¾Œ¯œý5H6²ò™?ð§Å ©¬#“q§_ÊœS'2  á¶•±ðO÷1wKŒ,&œy—Ìœ@QDª>æ¹OóòšÖÔ¾z6_¸ü«œô 2<€CsÙrΛˢáÓ1ö5¾.‡‘¨ÙH-G1¬8 m[+ðˉ# ÉÎ;ŽâWÊ)³ÍOñ°l¨Ù@­ ˜Å\ð³Û™¾êWÜúô6´Ñ×ðû›§âÛéAаäžÛxt³ `æuÜrÁX²=@²‘uoÎåáùÓìz&“.¸’óNI–ˆ²ö¿à÷KšžŽ»èrΟ>‚~–KKù6œ ŸÛº&RJ6¬[‹ièÄëõ¢éZç[(EH»þ–wý™¶çû;‚#íwK$ÉD’†ºZ6µ¬eä˜qªÈ©¢(ÊH5…ÃTSCßчbÇRÊÉJì׿»ñ*JŽÊÁù¸™¦ófÚŒâ/ÞJ¸5óä˜p¬ýù}ÈUŸ ¯8ט‹p$šðŸ<Yþ{ˆv¬ç"RÇ$$õ§?Ìôs}$ý†Hy-= £5‰mkûìGä]Ƥ¯‚óú/oiDK/Bkj:LõGåÀ¹uÛ1Ž!pýDŸ¸ ·¶ï¦£<òr6£JâÜ÷Õâ› ìr '½#éÃI­µyº*—§ñß÷šÝ@4?£æÜÄõÓyõ‰ß³¼)Èø³¯äÒ.£æ¶GY•ìÙöŸäÌæºË&ÒøÒCürU3Zféu¡TPBó2ìü›¸y¶ÉG/ü¹¥a¬Ìt" 6h>†_x3ß›Kç>ÄÓ•Prüù\rÃMxî¾›¹e ÐÓ(7‚ìÊóàcÛHxû3îäs˜sK ŸÿŠç·%÷<‘hÝÖ2JÃ0el>žw[I`Qø…Id¶VÊœÄqæSV–O>c  qI9aìRæ(Yö&º¿ãYßÑi²†5‚ #ÇÜHµVÄ _È`Û+±tú÷8ã„1¶­®…‹Æ2(íušò¦ó…à6ž_YÆ:­‚KÎ>‘¡óʨ(K¬bA徦,iFŸÏu_ÊäãGïcQUGøÃ"oêÙ\væY˜…Ïa{Àö컑•;„|-IJͪi»d<×ïGÓut]Ç0R¡Ô4tt´=ü}HÚ36:§—t™n’Š|¤‚]²:4)z*PiiA’‰]S–EQ”# j¡êëjP2ˆD"¾ÿ÷!™LR]]MMm-µµµ444››K~~>ùù²«‡Ÿ–qžL.ÓOºfjH‡kƒ½àV7 :aj¾ o«è’-­©éréJä¥ç‘dñéÇ£—ý·ÕAj;²A¤h¦†$³Ñ¥Ø)›z¿ý$ë‰ýõ2’£NÇwò¥|á×_§eÞ5´,­$C¥k(ŸbFÞ@ìMˈÌý]Ÿ×ÔØMÓ°,“D"e™øý> Óâ‚›c³¾¿ÎßË‚Fœåþžç·uÍC$šÛdõlûd+¯ý懬8êfŸ>›oüètf¿r/÷¼¸¡k»U4èдÎϨû>ž®\š×¯¡Q;‘‰ÅY´p4þÍÏòq‹Cxå۔͹ˆ™#_bÑøè5 ٴǨJŠÍ%ל„oùC<¶´¹³¾‡9àt¾÷ÍS‘‹Ÿæ/—Ñ¢pʵ×0uÇÔž\p¤¨ä€»r]Ó2IÄm›šðz,tt,Ë:lã8mÞ¼…Eo¼A(¶mÇÁqRoÆååÛ1M˲ÈÈÈ`Ö©§2|ø°ý´xäŽãðÔ‰è8{rh‰ÄÑ„$aÛ4ÆòH¿å ÎrÚÖÖ Å:\yéýB|ôn9ù~]¸D]“‚ p7Ÿo?=Yã€ÃÔ©«GéChê_‡mI0™±§]Jna‰Çï!ÑdƒöÉ÷È<ÿÆÈ?²yu%Â?€~ý+«7§ÎÃ+I®h%û²[!¼– Í©é&ZÇqÈÔ1µ¾Kòýz2/{QÆÙ²®,"'ò¢ußýhÓñKRµ¥Ç*¢x`Ä›ñ8WÕ U>¥šï½·º¬¯‡Ñm†aàóyÈÊΤ¶®žÿn ±¥cY/’¡Uü{I ßÿÒ·ø–x‰·64bûúQ”^Å;o—1š£=v*GÖ²¢fßÛ'úÍÉã b{q³c¤A¬•˜Hõõââfn9÷;\§¿ÄâÍ͸iÙøkVðaå^x³ÛÎ»Ž«íù¼S¡QrÂùœ›_Ïë®%ÜåÃqÖÑgpvõljÔ(žv.çå·òÖŸ–Ý8žÝ>dWðÞÚ(Óý™mïð« ‘öú m¬ycÎ §q!Þ\V³Çzž’Óøò3©|õ]3  ‰7ÖÐZWF#3™}Ö „–n§Mäß·(Ù¶†çÿ[ÏÎü.×ë/òßu ØiÃ(첯ž9™kr C?~€Ÿ<¾ŽXƾoïvì}eóÂTPcÊ×!R ÃOÓ»ßݤ쨡!;¦RBB´×é¢[KÇZRÃÀF²|^K¢k^‡tKǯ¹ä{$#A›á³Èðè¤I²]_"BÈ4 ‘´]i½œ•¥(Šò9Ôã F[[+/¾ñ'žp<ù¹Ù¸ÒüBá8RT×7ñÆÑÐØÀ´c;cÀ¶<ùÂÛœ2ã$NÈËÁ‘)ÀM÷©nh敯1m|=S¦{ÈÆq¤‰Çã<üÈ£Ô×× …:t('žx"¹¹¹øý~L3õ'áº.ñxœP(Ä¢E‹(--%S^^Î×®¹ú  Ÿ–q$â©ÔôîœÈ4ህaÈ×8ö'_KõÛZнþ„{ŽD],µ ‘|þÛ´¶}‡ìÙwp܈Vœå¤uÍ&Rñ—îâ—p» >˜‡íÊ 2 ðxTj®l#ùüõ´†¿KÎYw‘w‰ÉJóÖú°vŸýÈ~rÉŌ̴Qñ6á>cZΘewNÕ‘—ÆmY™éi4441°¤˜^_URFùä©{¹?t |ñr¾s†"LÅsùhq·÷ž{“©_Á姬`Í“¥ûÜÞÉÄÔ³frY– ¸„ÊWðìß^£Üˆ±î™ßpxžr%ß=[ƒD=ËžÜÀ²Š&6Ïûˆ_Êœ³¾Á4?D«Vñâ}Oñrib§ þÂIgÂÙWsf†N²q#o<ô$óÖÇÚƒû9žÝ^¦âl}ïbÇN%úî[”uÎJ’D7ý—¥­S˜¡¯âýʽåJ˜ô4ùg\ÏOÎØqϦ¿Ý¯–½ÊŸžÎäËg]Æ3RWè“á6×DÙg XÆÙ2ï7ÜÛv1ϼ”ïžnáºÍ,«ŠuîÛeÖE·n6§ÿâ­`¥'_¼2¤¾üÙ©mJÚÏõ°ea*ØñÛå{m2Ä©©#R"å¡­6¢µ÷%Ûë]íKG@Ã4M²ý4ðk.Òò0 ÝÒCýI< )à%`@¶×¥Ÿá¢y ’ÍÞdéwÉ úðzõ}ö©(Š¢ìŸ6ý„c嫯.èÖÆÍ--¼õÁzŽ9v>¯…a@Zš—K®üÿzâO´†q‡¶h’Þyƒ/8‰¼¼ü^t8fÑ’5wÂqx=tM ëFªž€€¤;)ElÞ_¼€³N™J¿œ~½>Ž®\×åW¿º‡…‹QXXÈO~rcF`ÝúõÜyç]TWWsÚ¬YÜzë0Œƒ+µx 55æ¿ð"Ë—/'‘HpÊ)§0~üx²²²°, Ó4Ñ4 MÓBàº.‰D‚h4ÊêÕ«Y°`^¯—É“'sþyçÔøûr®ëbÛ6‰D‚yÏ=ÏÀ·azIöA¢(G–ÓÞj¦ ?—dro×å/)%7•RUUÃø£Æ››Ó×Cê;f1üìv¦¯ú·>½íÈ’ñf™&µõ|<àÚTðÂJK}ÐÍT¡Î¶°£3 >zV-_ιçžËºuë¸ñÆ›xíÕW¸ñÆ›())á˜cŽ!‹1þ|.ºè¢C:ž=Y¿~Ë—/§¹¹™3Ï<“©S§’‘‘ÇãÁ4MLÓD×w\1p]¯×‹a 83Ï<“¹sç²|ùrÆŒÍèѶàÜŽÃãñ0yòdLÓì•qlܸ‰5kÖ0ÃT(TQe/4Mc`I1 \—êчÜë}wÜùs***txвg¯\†ÎybMðÆ!…dȃKŸƒ¢É°âïÝjR´×·Ð@HÙ¥lè¡Ñ‘²¿šZ<‚iš Iº•º85$J¢©)´A3Ž_sðƒhɦÅNOG+Ìì\ÝÆpbh$мAì„…×êFñEQeŸºÔhjnæãÍ5L8ú„¤ |¦Ä/ààs†\¿NK ±`á³ :Œì̬ý¶ß]Í--,[³ SNÄÑ 'Ë €ÄíïHíÿwý:Í1âAÅü{á³ @V/Ž£ƒ‚·/æ¾ûÿHnn.=öMMMƒA¾úÕ«R°uëVÖ¬YÃÒ¥K¹ð SÛöïÏI'ž¸Ó‡÷C)‘Hðï—^"3|øp¦L™Bzz:š¦áº.>ŸÃ0v¦iÔÔÔðØc1fÌŽ=öXÖ®]˦M›ø÷K/1dÈ`¼ÞýÏÝß8233Û‹å9躎¢³Èk4%™L¢ë:iiièºÎ„ zÉd’×^½³–Çá©©¡(вw~¿Ž?æ€÷¿úë×õâh¥›ÜÂpÝÎ÷Eê b)”LƒŒbí?°&åŽé'ÒuÑŒCÐp]tMoŸê²ÿ¾| ¿W#ÇoL“Ø®AQ®…¨ªÇ›!¬f\4Ÿ™™†ôZh‘z"F"Ò†ÄKÌ2‘–PA EQ”ƒÕí F$&’Ô©m³™4 ƒÿwÅuŒ›4á: ŸÂ-w?‚ÐtÖ¯þˆ§žþ—¨kRW[Ó«AX,B[jš’|¡$Àé§_ÆÑS§ƒ”ísO%BÀêeï²`Áx<>BŽN]mm¯5Âá0_r ±Xœ+®¸‚ÆÆFŠŠŠxþùç9ñÄ™0a^¯—_~™O<‘’’®¸â ~úÓ;ñû}Ìý׿ƒ½:®=Ù¶mõõõÌœ9“¬¬,¶nÝʲeËhiiáŠ+® ¸xGq:×uyùå—™7oñxœÑ£G¸à‚ ¸çž{¨¯¯gÛ¶mŒ9ò ÇáõzI&“<ûì³Ô××3qâDÎ9ç^}õUÞxã Z[[¹ï¾ûÈÈÈ@qÐã(//§±±±óöa©©¡(Š¢ìŸSÉó?¼ŽçûzJïØðRª®ÆÈ3aÙßö»¹©¥V;ÖòJÁ>´C쌣짦†Ç¤3K#Ãr*"ø#.Fvþ¡%PS I™t¡ ˆ‘“ ½¹ÑGÔ7@ÂÁ1,\#u1Ƽ¡¢(ŠÒ®Û¯¤Ží \A[LPßã©§îÇ’à“‚_=ñ!÷Üú’š†Æ— %]~=õ${÷ÈN:H!i;Ô4&XðÊãX2UèIkß“h|“VGL30R÷öê8îþå/0 „É“'ãóù˜1cï½÷~¿Ÿ?ümÛ¶PWW‡ßïgèСLš4‰Õ«W3gΖ/_ÎÝ¿ü%wÿâ½>¶]m+/'‘Hпòóó±, ]׉F£´µµí´6{UUûÛßX½z5ápÇq°m¯×K  °°ŠŠ ¶•—÷<¨±‡q˜¦‰‚d2I("‹un¯i­­­466¢ë:–eõÊ8Ê·o'‘H••µ*SCQEQÊÞªÛ–¿±fqFª¶Æ~¦x¤¦ƒ¶6„Ã<¤SD¥ Ëö>÷ßß«ÓÏ…&G’ªGÛ‚aE0n$Òh…Œh™h$µ›¡© ­)†Vßvi¹ÂD³%2©25EQV·ƒ¯C†I:.ñ„K,Â]ƒ¶š ¸îd´T¶„ÔФFKý6 ÏïÝ{,|zÛDb6á„GOˆ©4 —Ôš:­õ[),¼ WDZråJ.\Ĉ#xå•WÄãq8ùä“ùÏþCee*Í20sæLžþyÞÿ}t]'‰žžÎŠ+¸äâ‹™4iR¯ŽoWÛ·o'™LÒ¿‰Dg±ÌX,ÖÔB°dÉžþyª««iii! u®@bY‡AƒQZZÊöíÛ{eS^:V9I&w.  …hjjJ-»¦i½4Ž ’É$ýúõƒZx¿VT(Š¢(ÊÙW¶£p`ók0þr˜x%äßgS¢s:±h¿ åî»ýƒ$5ÖYScßýMIŽ |ã¶’m7c8q §}?oò˜¯¢†!5/Z²Ý·Ê_ĩۊ±Ñ2²ðföCÆý$›âDݾX¶FQå³¥ÛA‚ü|ú§9Ô6· Šƒtdj@"i#¥ÄÐ@¢¡k -!Ò­½:à‚¼|ú§ Êšš1 ˆ ë©€†.%º–JY”hšNCS žé½7޶¶6¾}ýõŒ1‚iÓ¦áõz1M“mÛ¶ñïÿ›¥K—2cÆ –,YÀôéÓYºt)†a0zôh „ã8$©5å¾}ýõ¼þÚk¤§§÷ÚwU^^ŽmÛuÖŸ¨®®fÉ’%´¶¶R[[Ë‚ ؾ};áp˜ÖÖVêêêhnnÆ0 lÛÆ0 Ã`Ĉ,\¸òòò^GÇ*'[¶laóæÍLœ8±s{×uinn¦®®)%ºþÿÙ;ïø(Êü¿gf{6=$@½š4AD¤‰à‰ g/§žíüwž§g¹ßéyv=Ëï=±"ˆ ( R”Þ[(!„RHO6»Ù>3¿?6 -”„€<ïûbwçyæù<³³›y¾ó-r£èÈÉ9D0$!!çr^UAMÏßÚ;€kn%ÁJ8\·½æÅ ŒÖXˆh Áê›îip·5á'„ò…"U_Õ5jSqÃ%]Í&‰“L¤)H¬$"Ì€ƒ†½§ [S l4Ìh¶thÓC™Qà—dŠ«$Š*ª(q¨ò›nn@p‘pÊFPfö¢H2NŽ5\¯ÃߟüøÝH€"Ë8\°aÉ—ÜpýÔF,I‰ImH0H˜N†5"TŠ )ä©$¡(2¥U2ÊÔ©ÓUÃ믿ÍFbB<¿û%ÂÃ,L¸f2ýû÷géÒ¥¤¥¥ÕæÔHKKÃét2jÔ(ü.¯½ð,Î*/qQ‘Œ¼z<%%%¼þú<õÔ“ª³.ªªÖ†˜Ô”K(((`×®]¸\. 8pàEEER\\LII 111Çô­)µÚX:t]¯Íñ1hРÚö‡ÃÃᨷŸ³ÕQã™".^Æ<ùc@pb¾Û°Æ¿¾J0‡ z0”42ì­ÀP'‰wÄêON¸ßš½Ú[CÓe éþ^khÈHÕ‰B'Ü(-û±¼hÁ*,Rݘ°Á`ÅŠ®©HŽƒÈÎJT§tZ¥—9†*ÝB…ß‹ÇçÇ« £†@ œ-§lÔe™®]»²eËf6¬Ê¢MïË1$ˆµËÈA/ª¬€,Sì‚ïæÌ¨;èÝ;µáŸ&²,Ó¹sgvîÜκ´ ’zÀ`Pˆ “‘БuIV(u+|óå§(j½{5®ŽÍ[¶ÐªU+–Ì›Eÿ~©´‰ ÇÔYµd>=‡ŽB×ukÃ()**Â(ê%ó7| VƒÄ¡2'KæÍ¢ïeW±yË–FÕx4IIIäåå“——‡^'÷Á` ""‚„„þú׿2þ|f̘¢(FÂÃÃ1õŒ™™™H’DRRR£éP›ÍªI_$I l6[½ýœ­ŽÄÄDrrr)((8í¾à×È¥#œ-2D¥€ÁFtŸ²$‚>(Jƒâ=¡‡%? Æ“'G¯¹6Ð¥ºÐ%ý„Ž A½¤¤ „Ÿ˜M&‚Š™2Å@´ÙŒU By ˜\H+.‘Ò IDAT¹H9k C -ªŠ¡rzþ䜃hùùƒVô(¥~enrOP„ŸÁÙrZ)—M&©©}É/øŽï?z†}ý¯¢[TZDôUq`_:ë–K¤ÍÂÕcÇòñ'ŸÐ§O*©½z7ªh“ÉD÷î½ÈÉýžÅ=IFß«éÖ» -" z]ìß·—õK¿!Òfâºë§2ó³OHíÝx:rss±I š›`îNº·é…-*_i»7ü€ÓYEvv6:vD–eÒ÷íÃét’¹áú¥Ä1²KŽ¢<ìØ‰As“½g;nÝØ(ÚNDrr27n¢   6—…®ëØl6âââjC:¦L™BŸ>}xöÙgÙ¼y3‹¥6™§ªª¨ªÊe™äääFÑQ£Ål6]¯<«,Ë„……]Ï3ãlu$%%!Ëk+±‚‹¦L>x2–÷¹™–­éÚ­7l k·nìÙ½‹A«ßm=ÁóÝïCñÿÅËž…@ø]ðPÏa°@ÿ»¡Ë„“î²6¯…^L¬¦Ÿ¾Gæ©¢ë¡Ü çÔÐ5`«¾NñbÔd‚¶p ~?z™‡`FGR¸Œž—îu¢‘Íq@E¥ÜkÄåUq{uüA‰€0jÁYsÚu¤¬V ÆM¤_Ÿþ¬Z½’_fÿ€ßï',,œ®]»ñäŸþÈîÝ»‰ŠŠbèÐ!,Z´ˆØè˜3º›ÞŽqc'Ð'µ«W¯dåWKðù„‡ÙéܵO=þ§jÑ\6ü2,X@LTô-€¦M›6øœåÄùŸÒ‘}y(»×ã­ÐHJêN…)ȸ±cùý¿G–dÞzë-¾_´ˆ¤x¾’ÝlYžMa•Nïá“PWþDed qáÑpTNLÛ6me™‚‚œN'QQQ¨ªJDDÁ`°žwDûöí™1c3gÎäÃ?Àl6×&ÍÊÊB–eÚ¶iÓ(:P. »ÝNll,V«µ^ŸÈÈHEA–et]om’“‘$‰¢¢¢Óî+~4?ßTXíx½^t]'--®Ýºa4››M@pV½¡d èP‘uòv™?A×kNº»ÚïÁÑ^[€µÀÙ¦À2ÀÞ5Q1!_Kºê90é²FX”5Ty0È&˜ƒÍŒ^Z†äw‚)ðÅ&RQéÀ7”Tù(wñèòYNF gTÛd2‘’’RÏ@PwQœ˜”ÄܹßðÀƒ2|¸‹Y³gq÷w7zÒÐSÑ1{Ölû󟨬¬äËY³øÝ=÷œµŽ‰'0{ö zx®Š¢Âí îÚ…užö lkä¹çž­mÿÜsÏRRRB0`pG%Ó±]³VÓç’Aüœåcâē߹8[:tè@dd$,\¸Ûn»Aƒ1xð`dY¦E‹õÚ î¸ãFŒÁo¼ÑhÄçó1{öl\.-[¶¤C‡¢Ãl6cµZyå•Wê5&OžÌ˜1cÐuÈÈH@£èhß¾=‘‘‘µjÎ ähÞx3‹fóÞ‡iÖ([%†×_Oï‚yüwEaÓh‘#é5zí/eáGFL ǧ¹<5Á@m)ë*YFGGÓ´fÓsZh^ö¤çPdOäÒdÛ™]D.^ö-hب¡k¡”: MÓ ;p9pP ”e€ ðU@%à$tu„6ÀD1@4ТúyåOš±O»âÊŠ9ÚŽd‚ÉŠZáFñ¹P| Nôð|.7˜\ªƒÆ8Tiz5*|²ðÒ‚F⬮GêêÒºU+†»”3>àÎ;îÄíöðáÇòÐïª-ßÙ˜œLǰaCyûísÿ}÷ãv{ùèãÿòàï8+7NŸÎ÷ß/bc¡ÎðÁ·2$9‚R,^³“ŒŒ þûчÇôyì±?rÛíw uêÈÕ“®!Ö ;r*Y¹aaaaÜ8}úë9, 7NŸÎ¿ß~›-[¶Ð§O.¹äÂÃñZ­µ¡u‹¦i´mۖ矞ÜÜ\vïÞÍÒ¥KkõZ,–FÓ!Ë2õòw@¨®Õj­ç¡Ñ:Ìf3¿¹î:Þ}ï½ÓîÛdÈ6’zt£½j¢ÙïÛÈátè߇®Ñd—\†XúM—ekø^5Í@såÔ˜&4MÃZ$PÓôFУR}•Gö£MD…‡“A¼YU-“Îâ[mèJ»7¾¡WÆßðý˜Þ¨©‚•ù,ÏöÑ2¥ =#”£~{tœ…9¬-„Î]’I1‹ÅàùÅ)ž E»¡üÐãÄ{ª Ñ«ÿé„j®V_ýh,ª ( çÔª*Íär‚Ý€#J˜Írñê¤@½ Ÿ9 gÐD•)†@À…îtPåÓÉu™È¯Ò)wû©R%4ýø×°@ 8ušä&‹$IôêÕ‹²²2æ|=‡k®¹¯×Ãf¼Ï½wÿ®)†<©Ž’’>ÿâs®»î:¼^7ÌxŸ{ÎB‡Ùlæ“™óꫯ±dÅ*¾vU!Ë2}ûöá“™Óæ8áíڵ㓙óÚë¯óï™óÐ4 »=Œa—ãÑGÿP/DSÑ·oÌêÕ«™5kF£‘>}ú if³£ÑXÏ@¤ªj­gDQQï¼ó‹…ÁƒÓ·oŸfѱ{÷îFÓÑ»w/úõëGNNöïãôk?‚¦^É%b0G>{×ÎãÃovâh¨ûéŽfiÍÀq3¤)QFÀKiÖÖ|7›ùÛË…ApÑÓlž@m2D5¨‚ÞXžáX^É”¾5Þˆ:ZÅ*·}GÚì/yni!ÿ¼²+aFùL š{E.Š×‹¤khÔÐ tyƒk_íFî‹7ᬒ¯#QøÈ9‡)׫伸º[ü‚_œÂ™ +Ðn$O^GY«þn±/H4`k8+´:6“†rj¨7ŽÍ@÷ùPÌ4¯•­JÇ£ÊÈ6cyj•²•"L©[Æà5âóû)¯ôQZ¥‡¼4$ŠQèàli2ÏQEQ¸ôÒKùþûE¬Y³†.Áíq3köWMRêõd:†΂ Y¹r%ƒ¡Ê]uÖ:, O<ñ ´è>‘·H]Ú´iÃk¯¾zZ}››nœNFFEEE¼÷Þ{ <˜iÓ¦a·Û1™L(Š‚$…J䪪ŠÇã᫯¾béÒ¥X­Vâãã¹éƳ÷*9_tL¾vóçÏ=ëýœ räîùŸ©ô,ÛÈ7M®K"¢U{R$^ Ó=C ë´??ÂèVe¤-[ÈGû‹ñ¢Hì܉HƒX @h5Ó ƒAÔê²Ò²,‡ WjÚÙë‘ä¨Èû×’=ÈŠ ¹E7ìî£ÿËøÄÖ€ªñÂ]ˆ¶H”—’QæÁ­ÑF»Öq´±BiÎ!¶z#Ü1ºÖ°à-ËceBjg•›D¾½5ÃZ™C?[ºJiI ûJݸT fÚ&·¤“M=HQQÉqÆ‘òÄÑ"cˆ$ù–Il~yv[·†Ne`m¯m¡˜$pé€NUi![ =xu@6Kx Ѓ."ÓéÇ [£9Œ”Öq´µ…*¦´õÜÊJKÙWR…S$‹ÙBRb<í,ÒÉç§«³ßáÅЙØÖ‰ô64Üù†l{¤Œ€¿°†],BK:è¡L:4©òH%¶†¼¥To|>¼NoP%4a(öP¥Ê8¥0а*I×Ì8üAJ=%.Ÿ„Ï/QäÔpz¡J •“ ÊÂSC Ζ& ‡5›ÍŒs³gÏ!::šý°bÅr6lÚÈÀ—4åÐÇè7n,³f}ELL ƒ æ§Ÿ~dã¦\Ò:ÎÄ8Ñ ›ÍÆ3OÿÏ>ÿ‚5kÖ°qãFÒÒÒHII¡C‡téÒI’Ø¿?™™™8p€ªª*l6C‡å¦§S^õBÖa9*)iSblÙ‹Ž&«>þ”…ûCå~Ù¶™_NÐ^²uæ†'aDÅlž}c-î~G’—ó·§æ“04ùiž»,ƒ>>“}¾šŽ:O¹ƒÑ­òXðüËÌËòÖÞCÛ¸vyõ3-GÝÇcSºmü¥ìY>›¾ÙNù‰’Ìbé7i:×èA‚EÕ³……Ÿ~ÁÜèr {'“û$‘ntªò¶ñý§Ÿ±x: ™Z2ä772yX'b*‡´S'_GšäHúL¹™Éƒ:’eÜìúø¼¶ªLxΈæòÔ‚µUŸôP=I‚Á`#è©þ¦»2ñgmÇ kñoX„ïšø9ßÍ?~ÙËÝ;Q5éU®7 ³E#xðvÿçJ‚1Ä=ºž) _’ùæìÕ)]?eÊ•T¾òö7Ómçƒø–g¡£ã(ñ“{ù \zÍÂÂdt真<‰žï¡¼X¢xÜë\qõУÆ1WïÏ Œ þBÔV7‘Øf®" »z HÑ÷ÓOÍG5Æ`4Õ|v¬cÞæªq—`0þÔ,ú'™+öÑ5LA’b0Þô<ÃtÅat‚9KÙûŸQ4«4ÐG±œËžcÈ5Wb—Asâ9¸‰‚9o ;ü'ŸŸ!&þ…ÁÃmœxæùôS®½v2ýú 77çœë8°ÙlÜ}×ôï×™Ÿ|Byy9éééìÞ½›yóæÔ–xU…øøxn¹ùfúõëû«Ôq®VäRÊ%ôÞ‹·P8ÉÅ“±WÞw?Wª?ñÒ;+8€ªÙh}{Ò%j¹%Z(ïE÷8‚Ù_“ç¯Ó×Ú‘«GàÙô1K²½'¸xV©Ü·‚Ïß]LE•DLÏ1ÜrÍÜzà ÞØâ:¶d¥Ë òÀàR¾ÿô5¶”Ùé5ñf¦=<‚'>d‡ÏJR·öDå}Ë»K²ñš[Òwüu\ÿ Jî_>d§ÇFÏß>Ê]ƒ=¬ûz3ó‚ÄtƵmê5Ð$Ûiß¿ … yï¿8e+äU ƒ†àŒi®œ>ŸE–«CNôÚœAg­G>Ò_×´#.ûþ<?|ÀÊÕk2p þ¶xc¨„úÍŸq9#1\ñ0½Ÿ€]ÿûúŽÝ迊ªÌF êHšo¿Nè‡^·^ýû …ôj:Åc?`Ø$ þ_¦êP%RxŠÃO Q6ù(Á;ÿq\Žˆúãhuý$ÔèpÈywñ´œ:m/Ï%Ì,áÔ‘<:ï‚Wq_ý8F«Uó"@:ôþÏfãõÈÈ~Cô¤Ñ.÷FÔ<І¥g¬yQ57LIFÜEÏǃìùÇ›èZý5’q36)œÀÒר:XQÃ0L¹ŠÈˆ£•40?"± FXÑÇTÍÛr”5Ûy×$´è&;m¡Gÿ»BÞ’ò®(?EiP°® •{m€ÚÐ)tœ´µ¡I§¡éÒ)O\UnŠ\4ŸJ™Åp+R/&JÝn¼šŠP)ò:ð *h$ â h5­úÒ[3Y1ýŠN@ hNÎIâòÈÈHn˜:•­Û¶ÑºUkúö9ó<g«cú´iµ:R{õnç ýúõ¥{÷nddd•Mvõ %%…””Ú¥¤Ð©S§3JÆy¡éhjÔ¢¼73‰~{/ö/`çê•,]¶†…G”X†Üq7Äoáçç‘îmue¬'S»™Á]#Y¾ªÍ’Hjkœ/⩳CD-MP¸ûpÈ¥ú¸è¸sv²¹Ú®—™ã"qÈSŒèÞãþ£ZKá=™4<’½ÿ‹¯×;ÐìO¢è÷ü$.K1³#=ÔΓ¿›íiñ³‡ô’XRŸÌ%‰FÒŠz1ap8y_¿ÌŒ%E¨û*IÖƒ.§©É—ƶ]Ñ(œ.Íå©á0šLÕ†‡Qܽžz µzë@½" U~úyÕ*† Füm‹H5Z±d”­ #ß}˜øÄwÑr–áÑþ‡¸$ þ,72í‰î,ãýn'šV“ÿ)”D5ö'y|[<‹îÁ÷Ë¡zrTÓ ÚŒnAñ¿obÕÏÅ!­ºãäùê„aÈèá4w>_>Ç÷è­Ä%ÌÇ] xÈÝôu}GeZ.êå¦(~½wúö•zpøUÔŸ3èÞókRº·¤*7³öxò7àOÏBbâxx Ññï úNÚ_5ö#i\[|?ýïÒÌÐü°Ô)WªiÀÉçWP=~î:üû²~!'Ëžs$ ÿ3ø=°þ ¸CÆ‹ÊüP×ÓD×4Ðu$)ä=!ј¹[Ž7`uò\½aCg™ÃÃÝLEÆlò xu ·À­zeü?^¯¿d ”ðãCÕ4]Áj1TCgBMÄ©Ázá^×ÁùÂ9«ÆË•£F«áÎ{gK‹ø|¾Ó¿X8‹ÅB¯^½èÕ«W#¨ºðu4)ºŸü•ò× óéÔ(#®Ç##¯!óûwxcÞ>\ÕÍ¢.»›[¥RÿãK¶8Ž\`iŽÝü¼_玡]‰\³–ª–½hg<Ìâô£½¤Ú8ô_‰0‘éã/¡s«(,ª‡€ ¦ãÇzã:ÒJQˆ¼ãŸÌ¸£þ¶`´ù¸}¹”c%Ú¦`ŒkG‚Tɦ}åœ(ºåt5 g‹ÞL95$I…›~¿ŸÅ‹a²ZAOÍw^G×ôzF Õò‰RU•nݺ±lÙ2FŒÁŸÿü;î¿ÿ~ìv;22ø0\¹[ ЉÖ“ýûÖbHC²1wz1º–X3THot?" ¥8öÆx´þ˜TìŠóÃ˘üpýMø0\9u º’¬ –y¡l¾m·8eß, qL<ÞEóP½4"¬ø4Õç'#åºÿy±­[ kUhFP3 ¡ù×dÔë|ÖŽýxù &«Œê9y¢ûa,Ñv¨vnµv'Mox~ùÇÿWÆOßÌ$""‚ðˆìaa˜Í#Iö}ºÔ„hÕ¡YÕ%{Bn}Å$¼-ô­Ñu=”ç¦OÏÖd4²@ 4¢Ä¼@pŽÑ}¥ì[³€}kä‡ñÿÃß&ÝÊØÍÏ0;?´Ý“±ìÖsûDv¾4—=®š ãJÒVìE½k8½£6“Ù«EëI+«o&Pù¡}§xÌ++8žéË4–G~w%úÊ/xofRKFÞ{'N$Z /[>|¹ugƒ¯Ü‰ŽýØ>šŠ†TH0tm*Ë'6Oœ¶&à,i¶ê'ÁU.¿c €$ËÕgë©Q'ü¤®§†¤Pé„rw(ŠBjj*K–,aô¨‘èkçqóÀÎØÍ T†Â#ôõÛЧ]‹ŸMx‡ EÎzÕ4ZÙÚLF…‡ò*‡þgœ®ÂüŽ_ xØ•UÀÚB/SxÝ\çè©<šŠ¶)íC!¨ºÈ¡ÐYFª~.Ë2²¬ Ë ’¬ Érè=¥þCRäñBRªÛËÈ’Œ,…ª«IÒ‘K`8°?í:un²y  éžÁ9ÂÜn<·_a$sO6…•~d{kúŽAŒ~E‡ëfˆÐ ¯åý7xòñ©<0៟ºg?KV–óôÄ›i©”ðݦ"‚G¤»Ø1ë 6w¾žzœ”%?³5» Ÿl'¾m `Nn¥ŒbÌ„K©\ŸƒS‹!¡î *ÍC¹¢» g«B¶ìàÛUüñêû¹_[ÀÏé¥,±´Ïgõ/Y4”úMw¦1wY1ÿÈóY¶§„€­­êŒé+j@“@ÐÈ4WÂÆ;òAþ"~É#}•<¦qÌæ3 äM@x;ÊZ0ر%õ£ûUÓ°F„’ÕƒA CèÏÿ°aÃøzÞ|¦L¾c§dâýA" 2’kþ­¢§?®¨H/M¤šcZøãXƒ]1‘Óߥ‹ò™{ Ñì­‰©Ú€æX‹] ‘“ÿM7ý-öïÌC³&Û"mçþúóÕÍ(&м^4MCÒŠ |÷>^g8þu«?+?øT$k8ª¦a¨ØK©´?ší+öâÓZ‘bIÒC†‘£µ†Qõÿ4Ô¿b5þí÷yóË´¼ÅÁC:Qƒ¯!pèzCó;Îøºýrº}ðaÛÿˆsþ¶†_ÿ:£VNJ·=YþÓlߺ‰¶í;òÆ] 7ÕNG|ÿŽõ”j½4ªcX$B^ruûPy*NئëtíÚ½‰g'‚¦@5‚s‚„¬;qG^θ›ÇiðR’¹…Ù¯|Íòbõ¨o£Ž's!ï|Ó¿O¾ñ[žgî!? gÙ"2FßD§œ%¬=ÚM£­lï=SÁÈIã¸bÄ “ ΂L¶þ¨ æ|ÏÛ_Drë„éüaDèn•ßUÂþwèš_-aí×Ëpûn¹•´Ï°ûó—x³r*S.¿‘ÇÉ ¹ÈÝ0›Í+6j {Éœó2/9¯çúQÓxh¬â*ÚϦ| 5¤I hdš²LäÉpè²f4Ù€[±°Õg£s¸‚ÍWyv;ÖTÂ<.hwƒž¾ Ýq€ÀÞ)]< yjÔ„ Ô6®¼òJ¾ør7Ý8ØaÉ\Õ.›Á+ ¹6Ì!è©IÖ¨©€É¬ é:ºîÄ?÷®‡ˆ™ðñSeðçᛳ—Ê…øçþ‡óA¢Çü!Sз¼…#-ƒz6%9Œ(3ÈzòpÐs¾ÂùY½ bU}`oEÖQs?Á97–ð«žäÒ«B9ôª|åž¶£µÈÂÌP±¿þz9¾/Åå{˜Ä›ß¤Ù‡š›KÈx¤£k•'ŸßñÆ— „+F‹B•®£7ôú"4j(ŠÂÈÑcØ»gi;¶áv7Ëž$IØlatèÜ™®]»WB@p¡! »tþý÷Kš[‡@pÎPU•@ €ÏçÃUUÅ´iSi™‡ßh¸óù€¹#·üïÄÏ}†W×Vˆ¿@p<í`Xrô9ÏÕ"RçP†7 UŠ ¿fÀ,Бhn!XÊ‚X±t‰E»O{ŒU9å'Üæ jÜ´`7………X,Ìf3f³¹^›Ù³gsÏ·ñêåmÙ6‹"_ä‹<O• IÁªHHšJYäíôxézÔW§ã;xöɺ/dVå”yÿ«G% 5Ÿu¢P@ N‡qãÆ`x‚ ÉD\ÛD°ÒqôM\êYÎ ›…AC 8ÎeòÆÃÛ×R²{7š®sX·RboUoû Öáx÷ï Ù_rFÚ¦ÌÝyÂmUUU`·×óÔ¨ë­pýõ×ãõxxø÷÷ñæž…Y‘.bÆ‘à€É(ÞlŠJèöδzöò”v_”^@ œo(Š"ŒÁ¡%#îüãZj82áƒ7¾#Ûßp7@pbÎeN¾:ø ޼áÞ^¿AQý—g²^.x`è ·¹¡ä 5‰Bk²,“Ú»7dÖ¶õx}<´t?íBß;†“T-úU#‡Ós,:£XÐ* îýšŠ·þƒÿ$ @ hzj‘QÍ­¥ÙHhgáÆ[&óùÌo(̺¸]I/VRS{6·„† bÎS÷1§¹u¿"š+§FsP3×/ÒÒRV¯^Ím·ÝÆ]wÝÅÛ/<Ãm]¢‰0)Ȳ„I–0+UC‘.ÒBij)Þ/îÀûEs 9¹rôUÍ-á¬iÛ%•ƒéÛn(8?¹öZˆ¯æÌº9XÚµƒ+®EíÛaÆæÓ)šŒÑWŽÀ`¸È=5n¼e2/üå#^{V\¹ÁÅBsU?¨üF IDATij檪*yyyL¹v‡ƒI“&q÷=÷ðÜ3O£C[‡c3„rid £¤_TÇIpña2‹2[4Û¶Áe—Á¡Cõ ‚Ó  ìÚÕ<úÁi3bDV¬ØvÊí "ü>Ÿù _Vÿ/‚‹ƒu…MWUá|à &rss™vÃT’6«Â[o¾Éßž~š~„Oÿó&1af¢-ä‹6†àb õÒ1l_J’Ÿ±c}3«œ6²Lmù¤ìl¨¬„²²cÛiÌž &ø«cvTõœI§Ïé4 ~"Mºfœ>ÿÛï›H’@pþ“šÚóª~"Κ²rÇEõ½WU•_V®#..Y†V-Ð4•ìƒydgg#Ë2II‰´o×–èèHQ½BpRŒ…Å¥lßžÖÜRΈ­ÛRœ°¹eΣ&Oye,\xz}ÍæPßòrX"*? ¿&]3Y\¸@pq`0È´n•@xxQQ‘X,fÞzë-ÂÃÃéÝ;‡£MÓ.ª|#‚‹ƒË'ÝZû\4.`úö…¨(hÑ:u:½¾BD´jmÛ6>@pÎ UsF @ ~õ¤¤$Õj©-åšØ:_|‘eË–²sçZ¶ŒDè‰à×AXDU•¬Yôe3«4 ›6…ƒ:‘qz}׬˜(, åÛ¿ DN @ .dY&9©5’$!Ë¡D tìÂÌý$¶nIddŠrd»@p!Ó{ÈU¬]ò¿¯™ÕMƒï¾ƒ3ñ&SUøöÛ3ë+Î[Eá'@ \ (ŠRÏ  Ë11Q¤¤$Ó¢EV«E44‰íºÖ>_»ä+.ѧÕš„³1Jƒ†@ð«C5Á™!Ûé:ú:~3(Csk9[”NûwHhº¹È‘ô3…kzG"7ÕÁI$©öqôû²,c41 Ç=‚ ˆØøz¯WŸf}@ \X(Š‚,rjœc”Öüæ›rv|1™¨êëFÛ¬Í(gÇ ¿òJ†¶»ðér8ú÷¡k sÓe0ÄÒoÌh·±^øÇK Î3z\2¢öùžM¿4Ÿ@ œsB95 ü}Ö ï®qÿ­Ÿ`䘫øëk÷|ïV^X[ høïáÄ‘ 2aÆqó£ÏpïUÉ>¦…•ÊÕ÷<ÅîE<°ç$Z´€N˜¼›¯Ÿ{ý•atÿ8øË—¨{Syb³^[Câ4þ9óot9¥• Bì/1ã#1Õ{_Â>àùàå[qÍú3Xæ!õž—ùóL¥£ofqñ)ÖWKHßY†’lù€üªÐÛÆ”»xí)øßÉüc@‰§k×(´ë9ì‡v§6B£`K}ˆ7hÆ·žæýUÔw‚4’<é ž 1ë¯Ï³¤¨™ê¨K |àiîím«}+à, cûj¾Ÿ¿‚ÝÁæÑu$KkŽ›È˜!=H‰2^J³ö°æ»ÙÌß^ŽÖÜ@Ðd”ç7·Á¹¤sçP¢ÐÂÂÓïÛ£G¨_IIãëÍ‚b0ˆð“ó­2­kV°nÍ ÖmÜK%P‘¾šõkV°nÍJ2Z<Ã÷{3ysb«cïò* ŒüÛ«L².à©Ç>ãØŸhÉ׿ΟFV1롇Y}´uÄÐŽ>9ÄæEOÓݸ·ðÑSeæ·KX³b.ÿ}îIV¢éÖ½E=×|ÉÖ{ß{‘äÙ7ñì/î:‹s3]YÉÆí‹øm»#æ S»»xùå¬ûãÝ,(¬³”—bzÿÍ$dý“?>;ƒ¥Ë>çõ‡c³e,¿›Ôî4¼älØÖ“Þ 5ãi5êv.ëÔ— 7^‚½¦©µ=ý’!KîsZ)Ö"™0OE’ñ¨­Ñ˜vuKÀF¬­ïçK ÖH^Ä[/½Â‹¯¾ÍŒ…iz|ê&zÛÏ/×t)¬ ÓžxŠßmƒkËB>úÏx÷ÃoYa΂_#CÇN«}~8{_3*œS:w†áÃaìØPY×Ó¡W/2Æ •…¿ QýäAVPòñÖ¹êaÞÞ…šŽ¹÷¿xä˜~²?¸’ï«èa#yý8‹xÙ`DQ”c\ïe[L¿>ÒNf®Ê£öþ¼É€Ç>dzåß™þæZ¿V¿Ÿ$1*jÏ,KOîxó¯„Ϝȿ~–yèÉ:-ÚÃDñÂåäBoi%«YºþxyOì3öSqJ†çÞ_ÈáQv烥è†dF\ۙʽy„ ý-©KYU©cJJ'k%[·ää\žÿ övð—áJɔԼ¹©²Ú d¢ÝÕé(Æcˆ$&LªˆEïñÓ™:ª7­mP•¿ŸfÁÂ4µ¾ µ‘cøÛ;™Ü'‰„p S•·ï?ýŒÅûö©Æ•GFÆ~\:°'í¼ðøp®ê4‡;CM¢†ÜËË—Ea7½?-GÝÇcSºmü¥ìY>›¾ÙN¹ HR®˜Îm/¡])Û¾|·W–„ôbé7i:×èA‚EÕ³……Ÿ~ÁÜõµJ:O¹ƒÑ­òXðüËÌËòÖn߸vyõ³´“ ÇR2µdÈondò°NÄU*$hçÈw¨!Mr$}¦ÜÌäAIŽ2nv}ü^[U&¼NÁE‹ÉbÅïõ°må÷ͬFÐ,8¡*&Á ¸\§×·¢"ÔOUÁín}àœ£( ‘Sã|GǵîaFwzøÄ-´VýºzüE+@0‹/¦·â‹zoÚòïÝüߘpÀ͆çGóßô {ÿ'øû¤^¿æ¿ ˜i]¯¯½¯¦Ï«5¯Ítºã]nå-ný¿íxè[|C ‰‘àÈ+?²@WËÉ+Õã WàT£ü¹«Hsþ‰ACÛa^\J0y"“;â‹;þEïÿ¼Âõ—D±z©ƒˆÎCi­ïáÝl/’Nmç‚‚-ÚŠž7OrÆpïµ#HÜþ-¹£ûsÝð0Òf}ŠûºÛ‰ 7!áE—,t¼îxd4¬Ÿý>_äAòÐÉL}øQLÏ?Ïì,œJÙJR·öDå}Ë»K²ñš[Òwüu\ÿ Jî_>dç)¸¬¨~AdŒ&¹ÖÈ_ϬEû¨ãè;áz®pZõþT*÷­àówSQ%Ós ·\s'·x‚7¶¸P’ÆpßôTJ¼Ï ;Ê‘"ã /ª ’•.7<ʃKùþÓרRf§×Ä›™öðt žøUu´Z;rÕà<›>fI¶÷çùɵÓ§¡ñ} Kž¿}”»{X÷õ fæ‰é<ŒkÛÔ5j4 I¶Ó¾ òÞ3pÊVÈ« @pQ3xôoøeÁ'¸]•ͬFÐ,†Jºº\àñœ^ßœX²ÊÊÀïo}àœcž‚ããfÛ?®âÆ™)t¹â^~b)o‡qï'™L™þÌoq½?’ù¹àä™6•ÖSyüîHæÝùû¼€åÔœQTˆ7_öhŒ0„xÃŒo&åÐç,Ú²”}dž¿iÑË Ýe]‘¾AºCs âLF;}$öpZU1{-âÀÐk˜Ðu)ÿ·ÓOÒˆqtq®ä72p´‰µ¡P‰jïɵ#ã(˜ÿ3~8LØ•^ˆ¹í“ŒŸÔƒÅolÁu mj²¡xòw³=í ~ö^K꓃¹$ÑÈÎŒãüq—Œ#f£èÖ]¸|ê(âô,–p£@ÕÁ­lÜ~?°¯¬}žR»?wÎN6ç„v•™ã"qÈSŒèÞã’5†0ÜìØ³‡ýÙ^ô:¹`¤ðžLÉÞÿÅ×ëCyP²?‰¢ßó“¸,ÅÌŽ]ÞÚ¶†ˆ$Zš p÷a¼'|ît ‚s‚?œ ÏὤÞKÚ†5n¹“÷_=Åþ6“™ÔÅF›.ëØôhÝ>ÿaÅêaÜ:êa¶ÖÎeb‡ÝÎ%öd.™•ËMu›?¶‘µoàŠËÉw@ûÄhªïd+Ñ$ÅÈhey8O'W¦VÆŽvÃ_FÓ=aݧ´!ã¿s9ä+¥ìó¨ÿ¾‡Ë“2IìAɲ_8Lç0?„d! ¾Ò ÌÛ4‘G®¹„K™0"Šýs–qÐc¤»,d@nщD¥’-»KÜå³kŸ‹I½:gØ‚ïÚ8³àTäRŽ•hÛ ~:ÜÎ+ïÜ^ûR-ÝÅ·>eE±Êñ’+ò¨¨ÝŸ‘ø™>þ:·ŠÂ¢z˜ `2 ¾ìøn_o®ÿÓßi¿þg~üég6f;Qc\GZ) ‘wü“w5F´9äÁrä V{è'1„\Ë1­ÿXêKc\;¤J6í«ã}t–š‚‹]Åípà1Dc?¿绾_ -Ûv¬5jÇ0~<ÄÇèQðÍ7õCRÚ·‡>}À`§Ö¯o> É5 ¨!8::Ì(¾Ï½Wƒ¹öê-ŒÔ§¾ç™˜rëCŸ³Û[·¯FéâÛ˜´¥NKswîüðCú}{|¸·ÏÆšÝ~&½œÖÆÍd@ŽÂÈNõJZ(ŸÃ)¤hÝbr•û¸vêƒt‰ÛÄ›‹D§býÿ±´j.·Ü:-±’­Ë2Be|¹@ØY£SD¶a•]÷¾hEÏŒeÚoªè¥nâÕMåhzU> sxhñܤyL5 éÄ%Fóòú§{p|¸*J(qøNþPg†¤±<ò»+ÑW~Á{3³¨Z2òÞ;PÓÖŸÇ¢—ÿÂÖž—2fìîùëXÆ|÷/ÎÏF“@ÂË–_cîÁ@t|åÎzÇDuæS„öâ1¯¬ ÞéWMƒZަÁñíÇö©{,õÐç&Ë'^✶&à¼G'èóMX”èX²Ð*µ-Ý™ì+ö5íïá™p¾ë»€éœ:„}Û×°cÍͬæüeÄ >¬X¿­¹e4/7ÂÈ‘™ylެ¬QÃj…íÛ›Gß)réˆ>¬^q‘–Á¢DNæ#¦7ƒ†WräçWűg »i(aø sfNbßcÃxdÁáÆ«7´ã†~æOq3¸yò³d&ÞÊÝc$öíÊ¡E<èV˜IÞG_‘éRr3KëìÀN §ŠnÍã`v>ÝL—G~âÓÛ]¼qí$>Í:DVݰW‹ G@Ç[|€ìÃUh¸YýΧ~ö8/þ¥„·W¸éu÷¿à]ÄŸæeqºÅCýÙ ù)ÿÜvÿd¼Ëoayaõ½r×z¾˜WÈç·Þ‹æZÀk{ªk¾ÝÎüø² »üž ,Xͽc¹{H‡ç½O†G)€;f»ðg§^F×n±„BK0ÄÑ£³`Þ~J‚8…6g”Õ]À̧iX aiÙ‰–d3cþ*v:t½ä“K÷R°s)§­aíô¿ñøÈ‘¤,úô’, ôËhÓJÖ>iè…îÎdù/½Mâòů³$?pL›SÒHrÈë#ÐÐø ”ä ”d¯^F÷^ñ˜2s«ÿT5 ~ ¨¸ )“¢IŠ·"×¼Öí$ÄG`®»þ×|ç—¢F%`WÐp—Qâ9ò«/)FÌ+öð0lFéc=@U¥g•¿ª“™°ˆH"¬Ç&Š>}4tc8áJ ÞHË{ÙD‹v)´>”;â£7çØÈÌV‘aX”sà7qR}‚³Áë9Í)½A /ïXtæÎE %= à̩ΩqêE3HûGxiFýZ%;ëÂmk›A‹Tsñ'aŒH Ë¸;¹íñ˜oÁV¼>•Wÿ³‰SOÇ$!IR½‹ä”¤–dç§­Žkã“Üý'‰¿?ö oÝlÀ™>—oy”ŠÏà/ÅKò¹ív?ý÷ç:•S¼¤ùÙ·>AÂú/ØYY³áþ‘“MØMðøCwô4[æÌã—+ÃØôKaÈ¡«x}²- £nWó–—ðĵ÷qGàVçJ$_:™I Å,þp.ôShs®ñeQÊ(ÆL¸”Êõ98µêäS1Äõ力S†×K÷$xx4Ð+wðíª þxõýܯ-àçôR–XZ‡ç³ú—,êæ Ew±cÖlî|;7<õ8)K~fkv>ÙN|Û$þÀœÜ“kAóPî†èîèÙª­ ŒßÀÜugs—ó×ññ€<Ÿe{JØ:ЪΘ Á¯ÉBÇkîâu=‹6•¬y-ï`Þâ}#lj~, \u÷d¬Û–²æ°$I#oâ†ä#¹‹‚ž Šrö³}Ë^ ‘ÄØ¤£Ç8Zƒæ£¢\#¡×p®ì’Dœ-ìWU’Ofz:‡*gïe DÑgôpbö,ceîñ|¦™ãÕ[Iaö6mɵ+c NŸ£®eÃÒyÚ·³™Õ.(ª‚rž4ÁÙ¡(‘S㜣æ3gr4sNØ`&Ó;ͬóZǵáQ®îúè {ÔàÛñ'Fu:Iƒªe<Ò+ºþ{Á,fý6™Y5¯·þ‹'ü«Á±Žàb݉¤QAúëÃðzýVµ ïV^S£î!û›G¹ù›†çØ0^v?߃ÞÏ»%°ÿ%®éôR#Œq†H&¬&x‚µ ß¡¥ü÷ú4?D‡c‘Á¡zÙ?çU^÷Nㆠ÷0Ø îüÌãs¨vwÖO¡Í9&ó=oÉ­¦ó‡¡•…ßUÂþ7 D¶eÀ„QL2*•‡¶2ë?‹8p³ûó—x³r*S.¿‘ÇÉ ¹ÈÝ0›Í+2jZÙÞ{¦‚‘“Æqň&Aœ™lýQAm@ j k¿^΀ÛGpãÈ­¤}vàäã74yÝK朗yÉy=ךÆCc @WÑ~6å{Э!M‚_?1½?¼’¯W• G˜N&a²š¡b/ë÷:Ðd{4-“û1vZWÒúínëIïMèø=zOKj´‡âì ¶—» ÊÂcbˆ´Ê¡\ÉºŠ§ÒA…ËK@Ùh!<*’K‡Š»¬œ O€`uÅ-Ùh!":Šˆ:î&º§”Ü`ŽkI‚UÇSQA…ÛO@ÕKL Z„)H Žy2ê›=¨²³=–6qMÛ~øi> à,£Èá#䜢`±Gi!äÈq sÒUÜŽ *\>‚:ÈF+‘1‘„›„Ť1d] ýêíݺ¦™ÕàBDQ¤çÿþ´þÄSÏ6· ÙHMíIË„8üþcCÁ¯“²rǹùÞKFZ]ÇS£úµµ˜JK Ô ù.C'Ò¦€Eß1CëxjÕ·fŸ²6ý†‘WÈÏßmÅa0Óêxí´ JÇÑŒë`ßêu¤·F¶Ž¯Ò…¥ó†õjC” üŽCl[µŽ & 0Õ1•Þm㈴„n†øÊ³Ø¼ryRc8=F§½éÈ^K6ýÈÚB {›îôîЊX›ðS¸mó]ަ{'+þœí8®APƒ˜{ŒcBo…¼=[Øë„ˆ¤® ?ãw+8„Y2Õ:[Å6r¢ì$tèÊÐ1:?þ°§6I¯kß¶†<Ä‚î HâR’‰tîesš¿d§ýTÆ<Õc©ë躆ð㸠"c2‡<¶9ìÞœ…7 amÑŽ^©#âø‘íNš“ªaMËØ.>öï\ϯ‘{1l,õæ,8=¢[´¦¼8TzS4@p¶DIW@ š_á.6íÂ¥W !”£N¹·æ)¥,­b­ÈÅ'Îx$[¢ˆ2€£ÐyâĈ8úö DZçg¶¨BŠK]`¿ŒÞ©±äm«D¯Žñ;Š(,®B£„R—‘—µ!%<½Õ+ü Ç…£²Îr¿ºŸ¯¢‚âª#áU§2æ©ÝCQA~ÅLXtkzíI„^ÎÎÊ  á)ØÏŽJ¾ Š¦ä¡·œB÷–aÈNW­–ÎÉMßnÿÏÞ}ÇGuŸ‰þÿLÑhÔ+*¨Ò‘ˆÞD3ãØ7¼NY'›Äyeknv³Ùl²{³›x¯óÛd7ëëܵ“õ&v¯[ÜÆôŽ„@ z—F£™‘¦—ß‚3#Ш!izÞ¯öƒæÌ9ßs4žçÛ"iÚÿŸTöÄP×8I~|9sâ.qnȹhb 3,åÌþæP7C!ÄmB¶tB!BƱê,’Ö±acov{ ƒÏ}umõàãTê«ëS¸Ÿ;ïœCz\ZŸ <ÁWì¼'uä4âÕj"7?Î77_×þ8*)j ÛŒ¼%ÔTœàÌþ÷BÜ!„·VFj!„¡ãí¡¦ä©VpÇŒÓë øÜØïðÊ2£½æ Œ8^aÂíuã´[±:<ÊÛT‘s¹{ëBÔuç)¾ÐH2/%kÓÞ“J£A…“š£Ç¨uõßÑËmwâjg¡PËN{B!ƉF£A-[º !„¡ã³6PrÞ@ü’•d8ðƒÝîÀn³Ók1ÓÑé$uY±ÞNjÚmƒçþnõm.ô³3ÝÙE§©«ÍŽÍfÅbêÆØëÆc7añFïÂd±a·Û0›Ü$¦ëñö˜± k7D^hÕzmìV+½Žà{ùx‡uÍ«…¯»+È]:-FŒ& ½ €°øtâé øô%êÚ tv´ct ç^®¶Ña¦×§#)ÂD}S;í::Úiní¢×í»±}MÍ4u9®î¬4ÄïosKÖߣÄWÊO…°%B!ngµ,*„B„’[c9ç;¯¯\ED¸žð¨X¦å.d˃»Ø”颱¤Œ&ÛÃ|NÚ+ÎÓâN¡h÷6ÌŸNBtQ1ÓÈY°‚•¹Qàì ²ÎJŠ-¬É#<<ž™kîdE\/ÕU8‡s ^;æ^/1ó’«GŸÎôØà&¾á\ÓçÃãèéӉь|oÓ‚•i,ÊK%R Ÿ½®-’ÏÑAUƒØ%[¹#/•èðpô1iÌȉ»:’¤ûTáÜñ…'ybM a0äïowõU7·3ŠB1²P¨B1®4„‡jÝÕÝ<®þž°þë[øl4”_"{c>aájúf_¨Py\TÈÝÛ ðØ-tuTqª¤–ÖOkôç³5s栃ܹ³É]¸–Ya*À‹³ÇHkM j+ŽsÒ]@þò;™­WO•'Ë©ì¾z •½4áZ»Õ:ÂN >-åå4.ÍcÍ]YàµÓqé$'kU7¾÷Ð×ôYiºTËôÂ|6,²ðy¹Ùgð{ðš.sú¼žÂÙElÏﻺÇiÅèô ïžpÑyþ§ùÌ[°Ž{—¨ÀçÄÜ|“]6l×µo•Žp MxßÎ-^Íà¿Ö˜[ÌÆ_äÀ;¿ÀÐÚâÖ!„˜ 4 ª_ø¥ïË_ýF¨Û"DÈ–šŒÓé uS„¤Ëh’Ÿ{!F)L«¥­Ã@ii9‘1qX-&4Ú0sB!„bX4} …Ê–®BL*êf-[Ìü“ö-^§Ò“2g.©½U”7lߨ›ÁãÿðW,¹ø ~ð›JlÁŽ mK·meÞ¾£|PfšüÛ^ÿ,BÝ*-Q‰ID9 tXÜCÕ:ÒÓHjº<­SètSa !Æ–Ï{SÃ!„cF£Ñ†®#Xˆ©L¿šoÿäKä5þóôçt ¶ ¾.›¾õ9ü4Ùü+æ{íZZh5Xû¾ì¸[:™­?ø{2‚PÇ‹ßûmÿg!&„JÃô9s™™Bœ^ ¸±uwÒPyžK†(æ¯XNÜåý|nqo;ô™¬ÙRH²ù<{×bF¾®Ãl1³:‰ IDATî×v q; ×éBÝ!„@ŠBL<-Ó7ÞCžÚŠ7wÛrðÒçÈOãnáã_ü˜Ç¾“‹ÏÄéFS„#cÛWØ3ã"/½°ŸV7àµÒdlÂ4žÅ$¢ KfAÑ*fÆØé¨©¤¤«·ZOlRáê±ìÅU›Jz„…¦¶ÞŠVjbrf“¬rዟͬøÎ‡.mÅÇÆ(»7ÜR¦“”’Éåò“¾^XXHYY>ßÄö°ÏZ°cg]mÍzÝÁ¤fÍ$\E}Õ¹QŸCN¯G…ÕlÆj6a 'µwÉšB!&=)j1ÁTs¸wS"—ÿ N­ýsvo/à/Æëʞ滯Ôá vÜùL¾öÌ_öàø¨ IÙÊ?üóÝÔ?ó=ž¯t Ò¥±æ¡ÇypÝ’Âìö›>N¡Š¢à‰¿â««mãWü¦ÉMâÜu<PÔPE0ïÑ¿â[« |ðÒÏ(îŠfáŽ/ðØŸï¡õ{¿¦¬÷Z’d§öd ž%Ì‹—†N/¨"™¹4oíKÔ'ÜÅwöbx÷y~RfD—BL»yì§Œ¨#ÈÌ›I\ãÛ<ûa Žè™l}b'{f·räõ×ø& ‹wòÅÂCÇ‹—#¸GAX"³2u¸›K¸b<‘³ÕœæT½ />ÜV7¨bHHŸFTo%gJºp¨ÂPYøÍPvú´Ù¤ÓÊ©¦.ºímÌ_>‹ŒˆVª‡3eŠ)..fÅŠœ>}zÂGlÜ®ºZZHHM%57—ކ\G¨›tÓ¯»›“{ßÀa·†¸5B!ÄÐd‘y!&’&™5ÛæÒ{|•V/={9nÉæ®õÓj¹Bgw+ M466Ói^îqרb²}u Mo<˯>>͹ò¼÷%–€cb ¸}÷+Þ8QIuU1ïþö]"±!7<àl>z.ŸàŠ7ƒUócQªˆ™¬™¥¦þd%½úD¢°ÒTQÁåÚZ*KOr¦eSo†ÉÖRÁ¹ŠK”Ÿú”ßÜ ®VŽðXèè…Ù1zÔ€z÷(ÆŸ&a![×eŒßXÄ]Û`½|ˆ}fˆÉ"7ÆJmñÕÑ>µuVrsrˆ¯:G×í1`D®”Ÿò˜cÇŽQTTÄ‘#Gƽ=ÏLžé vk'>}c\Îmîì$!-˜øxLø\c<¥däÒÞTêf!„£"E !&L8¹w¬!YÃÿø ì÷Z [ç¾Ååsã±vƒ¯Ôaš¾-bo| Vøj¨°SüëŸñf+ࣥ›}fάÄûä: âÊ1¯Ë#ìÊë”v{ÁÛć?ý[αížm|íïîaÛûÏð/o×bçŠÇåÁ‡͵.|Ÿ§Ô×~?’{œâ¼ V/$$F¡©·9Zc4<æ*ެGƒŠˆì¥,OjäDINŸ£/Zs³ˆT阿éæ÷{÷tf%^¤«ÝäìâÈ‘#¬[·ŽÃ‡'OÑáV×\UEæÜ¹·LQcÞâµ\*9 @ùÉÏCÜ!„bô¤¨!ÄQEÎæÎå1´|ð,/›ýI²:Ž_ù&[·äS~škkè«Ôª¾"„ÏݺèpÔ0È‚šAŽóXéì…eYI„«Zp_—»:«höl a º+ 4ÄÕYC«oÙÓ¡óXË€Ç4sùç”9¿Î–¢•˜ ÔT¼XF÷µ>;­ç>ã¿ËrlÏøîæÍä~øk.†x*úÈîqŠs©ms“’9œ+'¸bàSéóâò‚F×WLnQH¥¢ïsï±c1Ùn›ŸÇ†Ùdò¿Â¦13CGOÕ)Š[><ªp¦/YÁÌ™Óïhžr †^³bóýT–Çdh zÌáÇY¿~=‡·vä-Û€ÙØNSõÅq»Æp¬¾ë!ÊOì£ÇÔ5n×p9´ÖÖ’9g-55“~áPk)ÔMB!ÆÄTv,ÄS=…ì?|šºzj¯ýª¹À¡Ã-hó7Q¯¯ £ò—S®GåꤲÙCÒºl]šÇÂeEÍŠºqÔE°ãÜíŸî |Éã|éž,ÊÏca~1Wßæ³”óæ¾Rîû3¾µ«ˆÅyóX°p>ézÿ©}æ2Þ9ÜMòÝßä›±$>K‹¸kã ¢àáë­ä“Ó½ä>ð‹Ü¥|r¡ M^ÂÖKÈ›™ÃŒ9yägF‚Í„Í ê¸¥|ó_Ÿå§_Êðœãm¤÷8¥ùœ´/§Å•À‚ E,›—CFZ ©é™Ìœ¿…Y‘¨¼Vº,>"²æ1+})éYd'è-àí+Êé§e­ ~ÜUºä\Ò´fj:è6™ü¿º;¨oèAœKjøÔýÆÚ÷ö k:ÄúõëÇ­g†¼ pü“×ǵ qÍb¡»³“ô™3‰IL÷ëÔª­»”¸áòù¶D!„;2RCˆ‰ Š¥`sº†7)5\¿(¢‡Žâc4íÞÅ–¥‰ù¬“co|Îò?ÞÄã›ÏRþr5g^þG¿¾›‡žZn#çߪæDÝu§ñ™>îJ ïý_~õEvï|’åÀcÅP‘‹|n®¼þSž±<ÌÃ[ãÏîÑnzÚ/sºÙ†ÀgåÂïžáßͰkããüé½jðöÐxò5ΪáÆAìTï=Dûº{ÑùŒJ[ßš¸–oßžx-àÁ\–WÿóCê]}½ó*®ý'F|S›ÏÖÄéývfÌ›Cnî2t*À‹³ÇHkµ•ÏJó¹sL[–OÞòðÚé¸h¤1Xç°ÏJCE-é‹s)˜ÑBû¹kk¼øè¹|€÷.« 'eÆ4Ôæ ÚnØåÄGoK–¼ü5ö¾öŸÃ:v< ó–¬Åj1Óp94ÏyVÁr¼/5Å~mµFC΂Ô”•MøµmÞõ$ûÞüuHÛ „BŒ—7^}Y¦Ÿ!„·«á4`|¦¢\:{4d èÛ & Ÿ×‹­§‡´3†>xŒ%¦f*±4„BÜ!„B`üר˜J|>æÎNÔ :½~è7Œ¡Üy‹&ôzâÖ‘ W³ 5’½¤BˆÛ‡ü‰&„BÜÆâ’RX±ùa?–…9‹V‘3¯pLÎ5±‰ÓX¹åÁ¡gV³|>âSSÇýZ3,Sâ⃌ûõÄ­'A¯æ-E|÷§ÿ¶IaCqÛP¨Õò‡šBq;2Ú9µï#zÏX6ªÊNPw©ô¦Ï3Ræ®N~öÖ„_w ÍW®AxDÄø^HVÄC˜§góc_ã‰'ž`óc_##nbG !ÄxÑd/X€ÏëÅåp øËç“¿)…Bˆ©äðáì[·ŽÃ‡‡º)·¼öº:2æÎ¥ºtl W.„º"{î"ô‘T–Öñ>Ÿ#GŽÜTacÖ‚åx}^j.LÜbv<ÁñOßÄiíVª6¤dgÓÑØˆÏ{ýÖÞ£S[Q2&çSƒÑîåŸáÔé34™ìícó9BˆPÓ^ %ŽŒ‰ÇjéakÆÏ¢5wÒÚPM{cõ°ß3ÚÂÆŒ¼%¨5Z®”ŸMSG$oÙzÌÆNšª+ÆýZ7£»£ƒ¬ädb“’0 ÃzÏüe8¹÷M¼Ïx6O!„¸åÜtQc0× =Fc¿¯«Tª~ÓYôQQÄ$&ŽJ­îWäp;8ÜN§<„BŒkI‰ÝÅÑþ'„­?eÇöŽê}£)lÔTœÕµF£âÌ¡ »ÖÍj­®fúœ9ƒ5²æÐPU 4„Bq£q-jãóùpÚí8í7®º¬V«û%…¨¸8tz=*•*Ô·$ÄÍQÇ‘¿ùNÖΌݬJOÊÜE,̈ ßOCø|žúçxúþé¡©nNÝ ÿñ/xæ‹s‰ÎñC=ï`ÏSô§Ò•”JJŒvxÏI­#!=¤ÈÉ÷iÔ†éBÝ6-^̦ŋÇì|ë·ÿáQ#~ŸÏçãèÑ£ ylöÜEÌ-\=šæ [rz6‹×Ý=®×íõõDÆÄ Ó땯-\s§_*9Šf !„·œÉ÷¯ÇAx=V+ë«6Ë–´brÓ’õàùÇ»;yöÛÿΙžÁ?wQKþŒŸ3·¾ÿO|0¼)×Áé²yà[O‘óÁùA“ ÿ˜&.xnBöS Nfë~ÄžŒ`Ôñâwþ»o¢ßÞkÇÐÒB«ÁʘŒç ú<§•6†ésæ2;3…8½pcëò<— QÌ_±œ¸ËûùÜ2ã]Ô1ÌÛ´Ùæ||:pDš¸ü;ؘÕÉ‘½¥ÆàtùÜI%^±ùNíûÃÍŸt8ôÞË£~¯×ë娱cqäÈ‘ ÇfÇ•‘êl©§³¥~ܯ3ºZZHÉΦ±²€ö†á¯s"„Bˆ>·TQc0²%­˜ÜÜ®´âVg0'IË™ž«ÛH†eóÀ_|™ùU/ò¯¨§ïSFòì44¶ËTwcÊì±ÒÕëbZ—5tCý}&N¿ø3š"Ô@۾žyé…ý´º¯•¦ž›l»…ñc>‹ö …*,™E«˜c§£¦’’®^Üj=±II„«Ç²L¦",6•ô Mm½!/"U–ú{ÏuúHœ¸5âþ’É5f¸… œ­§‡´³‰ŒÅj6Ó6‚Å[…BÑç¶)j æf¶¤è—Ûå Á]ˆ[£µ’6 ˜•‰ªÎ„Ðe¬býÜtRÖñ~=5.úÖ˜•­—hs¡L‹_ó ~º!žh­Þ¦>xée>ºÜ‹-i[žâ;»òIÐNŸ¿Æ o•b ÈÓwÿ#Ïïî‹+Ÿû6OŸ5QÛÐ@X»£o¤†JOî{øòŽdG«Ài ä÷?çÙCýÉð¹|홿ì©ðQ;@“²•øç»©æ{<9’ÿ1.É!-V¸1ÖóÉë¯ñieOÿŠÏ…±¶’¾¥„u¸Vº Ã@õÅ‹Ô^û1ÓijhçÙ²ˆé‘ÐÛ\ÊÞ×^á½rS_»Ô‰¬|â+<¸8“ÔpÝóÑf°ëGßg]ÙÓ|÷•º¾Â‘:–ü»á‘;—£¯Cãe¼øïµ õ¼ƒ<Ïâ‰KnCOKbÞfÆX¨<|”‹Fÿ8‰æÆÚ«‡D·‰y}_2œþ„#­jÒò ™Ÿ‘H¬^¸è(=Äñ¦àW O™Ç¢ôjÚÛ{ñ„xpÉЮī·îâà»/…°57OÍò;vrøýßêýC62gå—ÈÅâñYcËC¾7^Àw‹ºŒˆŠÁÖké[L½»‹ˆ˜¬fs¨›%„BÜ’¦DQc0CmI{í—²%mx8­V¶¤#æ1ÕPÝËòSÑ5á Œô‹‰35bŽ[̷̚¨©q‚.•ü40®§Ç‹RÔp5ŸàÕ+éV'³dûÃ<ü§Ñø·¿æœÕƒ¹r?¿{î#º{U$lã‹;¿Â—ª¿Ç¿÷(I¸á³çxö°7>ìvðy)~îН¾®ÍÜÆS{ 1¼û¨ÞJΔtáP…¡²8ð19Ö­‰À‚Fæ¬|¯\akFÇníuAãšÁ ãýL>{ýùq=ÿxYºq;G>x€®æ&RgÌÀÐ4HeO!„AMù¢Æ`®*®7Ü-iÝN'*•Šžîn\‡ìÐ2Õ9[)oô²>g‰ÚJZTÓ)ZKÝû/rbÝ_poQ&oÔT㎟An”ƒÚŠÇõÖåTißHƒÊ®i,þþVd„q®Ê‰µágúŽ»ÒÐCÆš¿gSþ4Š{¸–Ó;»[ihlZ¤ÐD$…•²Š .×ÚñQ{S·kk¹@é¹¾ö–«Ç÷¿ÿš»væññ”Ò;ÌNUUtlN¦õíÿͯ>iÁ œ¿ÔFxÎ÷¹ïþ|ôoÅ\+GÚš/PZ^‡“ .u&QøýÕ}ϧæús.àþ-)t~ø~ùNß”uK"¦'Võ;.èó¾:u¨çy;S‡Ç­žË»Ãxì=˜Ìþ.J‘ÎÜAkG·ÿùiÆ¥©&:.1ÔM)™Š2´ôœ9´ÔU( »Õн·—„ÔTŒmm¡jžBqË’¢Æ( wKZ^OD\ú¨(ÂÂÂŽððzC½¡w>uçÚà¡|r"?¢+e+¢ëx³¤† U#ìXÏÌ×khÌÊ'Í×ÌÇM7Ô®qw7ÑM ‘ Œ”å;Øsß æ¦Ç£÷ØpéÀ¥æ®W9j?áýÊE<ü×?bæ‰|º÷§j-c“´;š(¾ÔË]óf“¬-¥w˜3¸Â¦Í!Cc¦ø‚ÁŸ<»;8_ÙÃý g“¬-Æ2@ÄÕ݈Qy>×3y.™Z ¥¥­ wÌHÿç-&‚&a![×eŒßXÄ]Û`½|ˆ}—&×tƒÀéKÖßËÙC„°5#—š5‹”Œ\ÎÿlÔ稰1}Æ|â“R¸púàX5€Ek®’ö¦Ú1=ïxJLÍTŠ×ëhh § @ŠB!Ä(HQcŒ]Û’Öa³ÐÕÚª¼¦ÖhúðˆŒ‹SbŸÇÓ7f€‚Ç­6WXãÁx±ƒj=…ñXŠ–qùUJ»Ýô”¤æÑ‡Ø2÷]>]˜‰ºõªÊÖ¯ñzð¢B h3ïá/¾~'¾C¯ðËßÔЭJcó7¾Âò‘6ÏÙć?ý[αížm|íïîaÛûÏð/o×bï×^¨Ã4#ÛÒÔ ¨T³ jÀó¹Zƒ/ï~®;ßäuX°z!!1 M½}ÈÑ£á1Wqä`=TDd/eyR#'JÚpú|x½x}z¼^Pk¯ÿªÐè4àó†lÜú*ÿŽjæ–¥×Öp…¶†+7}¯×ËñãÇY»v-G¥¹æ"Í5Ç …ý•ýdÌÏ9æ/]§¼ÎŸü<èq^‹Á@rF2 E!„ù7úº¶%mш±µ•öº:š*+©=wŽÆª*ºZZ°÷ö¢ÖhˆNH %'‡Ü… ÉÎË#}æL’32ˆKN&òêb¦âÖãj+£¢GÏüu¸§Pʽ°øÀk<ǧÞ½‘uó£è*¿8ì­(õisH£–Þ>̹šFjki\³ÒçÆî]tøÐ?ð>;­ç>㿟ù!On&gófr¯_êÀc¥³â³’n…B›ÄüÙQxZk0Œ vuTÑä‰e~^’¿«MfÁÜhÜM—éE6íîªÅ@sç$ŒnÆÃHžçíÊm¤¶ÍMX榪¸tÂN ‰!=9 zØBTÕ0´6*ñ¦û¿šF„Çãáĉ¬]»6ÔM ¹S×°íll$69•zÊþÉ"„BŒŠŒÔ˜$<.—kà-iuºþ[ÒÆÄ(ñ€ÓYœNܲ%íäähäØy+ëVÝMœåO_ººÖ€ÏBùÞ2Ü~E˜ùüt+ÃÝcÇÑ^ƒ-lÛ^„ùDo"©ú€\T6{Ø´n[«?§A•Hlw G¯¬sh“—pÇhlè®M"?3l¦Cw;ŧ;ØyÏã|éž·8\Ûƒ:%‹˜ë‹_r/;ZNReP‘±ú~H5qॠôŒ`€„¯§œ?|ÞÉ÷xŠ']oq¤QEVуܟÚÁG¿>?¢s]ãí.ãýbßÚý5žp½Ë‰f©‹×2¨Î †ù'mçËiIZÌ‚ EÄ_®£ÕdíÒ—@TïÊ­tY|äfÍc–±zôö6LAÎéí+é§eÝKk{ˆçéÆPUKOú,V®ñq©¦¯ž¤œ9ÌŒpP_Ú†c|Cö½ù+%NÍš5&£!ÆÓÜÅk±÷š©¯*¿©ó\+l­ÃäÖQ~bߘ´ozî\â§Mç©ýcr¾ñ°ú®‡8þÉëÀÈJ5utœ‘AGCÃx4M!„¸-IQãà¾Z¤ö-:š°0Ù¡eR²S{ì¶U˱=@²l†kÕ>N˜–±I]Æñ¦á¥\ ðì+q|iûþrS_Ÿ³§“Ë­Ö¾á÷>3g^þG¿¾›‡žZn#çߪæÄ•Þ~Ó4q9,ß¾…=ñZÀƒ¹þ,¯þç‡ÔßP]qÑðÞÿåWQ_d÷Î'Y® gÿëçüvϣܻëlÔ»0ÔwàËw8S½†ù[§÷Û™1o¹¹ ÈЩ/Î#­ÕjT>+ÍçÎ1mY>yËSÀk§ã¢‘Æ`E Ÿ•†ŠZÒçR0£…ösÆ«ë¹øè¹|€÷.ßøéGŽºXŸË¼Å™¨ñá4·Qyò•®IWdJÉÈôEÊ’£cv.ÇÃñãÇXµjÕÐSsm%͵Ã*?N(m˜·«ïÏí›)àš›™QXˆ¡©IÖÚ ±M+°ÿdIˆ["„b(ª7^}Ù·ûÑ? u;Ä»~‡–À_;´Ü°`é-0÷{¬–šŒÓ9ܱbPÚ výèû¬+{šï¾R7ìÅ8CI›±ú‡õœÿÉßóÛê[¡Åâf…iµ´u(-½¹ £µpõœ;¾7$מh†U«VqôèØL&›M|™ýxqLΕ4}:jFFk„˜5„âÖðÆ«/ËHÛÕpwh '2&†°ää¾K}¾~ÓXú-X*½Fâ¶FúšäyÛií²âÊ`ù}ÛHé:Ì #!#ÄÍhoªuµrË\<{sWûM'%siY³9qbŸ²xèh„éÂY{Ïcxû¿oª=“LJ7 IDATc)9=›Î–¾=žÇª }£5fÒÙØ( …‡3„âÖ!E)èúZi´ZÁC§#:`‡¯ÇƒÓáÀípôýßéT~/ÿð· u©³–sßÊ,"Ôàí¥åÂ~~ùÂ;\ ¾‹®cª­Á_ÔØ¼û«ì{ã…¶æF'?ûØœ§½±†öÆeñÐÑ6\NǤ*hdÎÊWŠcÍb08}:Ù E!„’L?æ CŽ6<¼ïÿ:ò{ËuÃTµZÃfÃ5É-•é'BL=¡ž~H­V+ë'$¥eah½}§hµZV¬XÁ±cÇBÝ”Q™U°‚+å§Æý:­–ìü|jÊʆ>X!„˜Âdú‰Ë…Í傞ž^»a‡–ØXôQQx=´ƒ,ZêvI!A1µ.™=§`Ò5T*›w•Ï^~ÔçHNÏ&sV>%‡?ÀívsêÔ)Ö¬Y3ìÂÆò;vrùÜIº;[GÝŽ±âõLÌrÀ·›ÅB|J Ýí77H!„¸ÝIQCŒ‰Ávh »´„‡£Ñh‚®á!»´!¦š³‡>Tâ¼eë©8s(dmñù|7UÐèl©¿aŠÆH §?ç¦Úp³–mÚÁ™ýïPSqv®ÛÝÞNjn®5„Bˆ!HQCLˆk…Šë©-ÕéÐGE“˜Ø·K‹J|—Y´Tq›3;CÝ„q3š¡R}þtH®kïíÅãvOowwHÚ „BÜ ¤¨!BÊçõâ´Ùp°h©Z£é¿KKÀ¢¥>¯7hÁC-BÜšª+”xÃÎ/pð߆¤ñÉiÌ^¸rT#&S2ÈÍ[Lñ÷oxm¨ÂFμBtázªÊNŒªÝ7cËC¢ŒR1v´Løõ¯±tuŸœ,E !„bRÔ“–×ãÁaµâ°Zox­ß.-ááD'$(ñ@‹–^›Ú"„·¢ã¿®ÄqI©˜ mvíîÎÖQOéjo¢«=øƒ6ê.•Žê𣗔‚ÉÐ7Õc²ìFcêè 1- V+S2…Bˆ ¤¨!nI·Û½·÷†×Z´4ìê×®9œv{Ð)1B1Ù8þÑls WsjßÛ!lÍØš,SQæ®åÔ¾¾­l'Óˆ?sWÉ™™´ÕÖ†º)B!Ĥ¤u„k×,5wvbhj¢µºš†‹©.-¥µ¦³Á€ÇåB§×›œêæ !Ĉ4æ,Z5a×Í™W8âëÅ'§±üŽCXØˆŠ‰gÍÝŒ¶™#’3w‘_+hL6Ýmm„GF¢Š uS„BˆIIFjˆ)e Ñéñ!jÍ0©£™¿eæüáD'2YÜTd,È'µç"%µ½ÈÒ¿£ãtØ'ìZ£™2’©+n·›Ó§O³lÙ2Ž}ô?#¾Öhè""'ä:7Ããvcêì$>%…ÖššP7G!„˜td¤†“:ŽüÍw²vfÔè~`UzRæ.baFªÀ¯‡Ïç©ÿxާïŸ>¾ÕÍ`×…`mÑÍàñÿ‚g¾8—ˆPµíV§Ò•”JJŒvxßgµŽ„ô4’"¥¶~3 kïy4„-.—‹3gΰzõêq»FáÚ»”¸ªôø¸]g,™;:ˆŒ u3„BˆIIŠBL-Yþˆ_ÿ¿?gYôÐ)_Ô’?ãùçȽ©š›¿´.›¾õ-Œ¹îރˇ›ÍW'°ñûÏñë¿^Æ0neë‡@°¶xíZZh5Xñ„ªm·•6†Œ¼elÜz;wÜÇÎÛØº~óSõ¨4qÌ_±œ‚Ôðq¸²šØù›Ù¹}5麡K[ÅŽ™ò2Ú„*9ô‘GÅ&ŒÛu ×ÞÅ´ŒÜa›0•w>8ìs¯½ûQÂô‘ãZØh®«—óŽ'ŸÏ‡Íb!&))ÔMB!&çBL n WZq«S™“Ð3–Íßùß} ®Fòì44¶ª»Ç1ÅöXéêuÑÓe•¡ÿî>þÅùÿÞoÄê¶LRª°d¬ÛÀ²Ùñ8[*)9s†Óg/Ñ`†põX.ª¨",6ìÔ(ü%=/6£¯*Фˆ€BŸ&Žùk7²n~\À±j"£Q¹Ì“g±Ç‰`í1)ñ¢5wŽÛuJ~BGSí°Ž5;8¹÷­aŸûèG¯bµ˜Æ|ÄÆ†_Tâá¶}²±õô?mZ¨›!„BL:2îWˆ âh­¤feD¢ª3át«X?7„”5d¼_O‹¾µf%@ë%Ú\(¥Çø5ßà§â‰Öúèm*თ^æ£Ë½øÐ’¶å)¾³+Ÿà4Pñùk¼ðV)Æ€šHúîäùÝ}qåsßæé³&jkwôÔPéɽc_Þ±‚ìh8 ”üþç<{¨sðÑ êDV>ñ\œIjŒ¸¾}A®_lmKïßÃÛª÷ÒÓPÌ{/½Â§ÕV|ê8ïú®šMV|8`åüÿ3?;Ü…w°÷ ã>nhKY»~ô}Ö•=Íw_©Ã9œ{RÇ’÷#FãßþšsVæÊýüî¹èîU‘X°/îü _ªþÿVÜ£ Ÿ=dz‡ ¸ñaï´ƒÏKñsÏP|õumæ6žÚSˆáÝçùI™U\ 1íæ¡§c¨#ÈÌ›I|Ó;<÷q-öð4–Ü·›‡ÿÔÓ×>Oë«"˜÷è_ñ­Õ>xégwE³pÇxìÏ÷Ðú½_Sæˆf沤¶½Ç/_¬Â¢Ž€&3Þ!Þw!qèû¸¡-#½'›ŽY»¾ÍÿÚNÉ»¿å­*ª”|á‰UÌMÐÂíVÔKdV¦ws WŒƒ/Uk«9Í©z^|¸­nPÅ>¨ÞJΔtáP…¡²8ð1Œy$|v#F'¤'G£ipàFMôôtÂfœú42c.bìö€:Šäh°5˜pN­A©5ãóWý†_àø§¯ã´Û‚—HÁªÍÿäõAÏ5gÑjœ+u—Êú}ýf ‹‹îæèG¯» êxr9$¦¥ÑÕÚê¦!„“†5„˜(ÎVʽ¬Ï™E¢¶’ÕtŠVÄR÷þ‹œX÷Ü[”É5Õ¸ãgå ¶¢ƒÀÔ¸·î,§Jëp•]ÓXüý5¬Èã\•kÃ9Î4ôw¥¡‡Œ5Ϧüi„÷(Ó)œÝ­44¶-Rh"‰ÂJYE—kíø¨ÑíÙš/PZ^‡“ .u&QøýÕ}í«øúªØî_ÇÅÿþ?¼q¢oäJíoãYúãûÙNÙ¥¾ã¬M唜¯SîC»dÐ÷]r }7<‹ »§òÖÜ¿%…ÎÂ/ß©Ç ¨[1=1qÛkN$ux,Ñèé° ¹ûŽÇÞƒÉì¡£åÌ´vtûŸùH—‹ñôÐnö‘Ÿ€^m GKöôpL•%4e¯bvv,ºxõ Äë—ËEqq1«V­âĉA‹Ÿ–¦5nG* :!öúúP7E!„˜4¤¨!Ä„ñ`¼XŽAµžÂŒx,EKˆ¸ü*¥ÝnzJRóèCl™û.Ÿ.ÌDÝú U–AÆÎ{=xQ¡´™÷ð_¿ß¡WøåojèV¥±ù_aùH›çlâßþ-g ŠØvÏ6¾öw÷°íýgø—·k±t@û‚R ;Å¿þoÖŽIñá0Zð=º÷9MÁïc„·ôžÔÔxñx§Æü¯Ã‚Õ ‰QhêíCŽÖ ¹Š#ëÑ ""{)Ë“9QÒ†ÓçÃãèÅ‹[g;6U6©1á8²ÓÑv•Óæðâl«§» IzªSbQY®Ð5Å .sW‡¯Üò‡Þ{ù¦ÎÃÒÛ9òÁ+¾Ëâõ÷rôÃßøz|r³®äôçï y-§ÓÉÙ³go(lä/ßÀ…Ó¸pêÀ(îâÖÑk6“ä‘‚BHv?b¹ÚʨèÑ3Ýî)Tqnï,>ðÏñi…†…wodÝü(ºÊ/bæ¿[õisH£–Þ>̹šFjki¶àscw.:|èxŸÖsŸñßÏü§?7“³y3¹#[ú`€s|}Wg ­>=ÙÓ¡³¹…fåW+[ð‰Ãz_°ûɳ„»«qÌ“0âY·$·‘Ú67a™óȉ rÇ>/./htšR©úzŸñر˜Lt›LXl|f“‰n“‹½ï‡ÁÛÓF§SKrv.sRU´×tàðÏÞFu§šÔÙ9d'‡akïÄ*5!4²f/Õ9l½–  k9hA »³uXk Ê9 íÃ~ÿ­ÎëñàóùˆIL uS„BˆICŠBL$G#ÇÎ[‰[y7ù®³|zéêÚ> å{ËpϹ‹¢D3¥§[îR“Žö ä°m{ gç››Eª>àW'•Í’Öí`ëÒ<.+¢hVÔ ‰§6y [7.!of3æä‘Ÿ 6ƒÔ†'Èõ1—ñÎán’ïþ&ß|°ˆ%ùó)XZÄ]g5HVìâ}ƒÞÇ0ŸÅP¼Ýe¼_l#s÷×xâŽBæÏ[Äúmk™~õuuÜR¾ù¯ÏòÓ/å¥ú÷“žÏIÛùrZ\ ,ØPIJy9d¤¥šžÉÌù Y˜‰Êk¥Ëâ#"k³Ò§‘’žEv‚.ø³õö˜ôÓ2H‰æT)™Æ7á³Hö´Pm¸úSâsÐ^݆7qYÚZzd=ŠŒ‰u†Íét¢‰ÏR Í5CÜ¢‰eéêB§×} B1EÈô!&”Úc°­ZŽõèj”e3|X«öq´ŒMê2Ž7 ¥WÃ<ûJ_Ú¾‡¿ÜÔW§tötr¹õêB‰>3g^þG¿¾›‡žZn#çߪæÄ•Þ~Ó4q9,ß¾…=ñZÀƒ¹þ,¯þç‡ÔßìFA¯ßÂ…ß=ÿ›aׯÇùÓ{Õàí¡ñäkœ9TCЙþ>ë ïszAÚR7Ò{²pö¿~Îo÷<ʽ»¾ÁF½ C}>¼x}}CTÊÖï';Ÿ­‰Óûí̘7‡ÜÜdèT€g‘Öj5*Ÿ•æs瘶,Ÿ¼å)àµÓqÑH£)Ø ­4TÔ’¾8—‚-´Ÿ3^]÷ÄGÏå¼wy 7¹énèÀ‘Ž«±–ÀX\†éäÒF£Y†çÔ¥³G”xé†û(>øþˆÞŸ–=›äôlÊOìë÷õðˆ¨ Ó\6=ðe¿ÿ;Ü®áýy¦ÓãröM"+9º·Ã:ä·#»ÕÊ´ÌL ÍÍ¡nŠB1)¨ÞxõeßîGÿ(Ôí"d HKMÆé¼Í¶áJ›±ú‡õœÿÉßóÛjY>t² Ójië0PZZê¦L:‰©tµ5 Ñjñ¸Çc•‘Ûxÿ9ðöoú}M§Ó±dÉ’)Wؘ±p!õ“æ{#„B„ʯ¾,ÓO„bäÂH_s'›W-"Îlæ/ÞÈc_ÞFJW1ÇF0ÊFˆÉèZA`ÃŽ/†°%0mz®__Ð€×Ø˜ œv;Ñ ¡n†BqƒM«Oø5eú‰BŒ”:‚ÔY˹oe jðöÒra?¿|á®߉Wˆ[ÎçoýZ‰Ó²gÓZ?༠ż%EX-Ý4\>@X¸žµÛáÀ;þ‚Ä‚•›èjk¦¥®rÈëOŸ1—ŽæÚA ¶+ÊíÌÖÓCDT¦ŽŽ¡B!&Ðþ%~M)j!ÄHyÍ”¼ô4%/…º!BLœäôì!‹ks¸ö~ €ó'÷zŽ9‹VQUÖWœ(=òɰÚ6Õ =F#i3g¢R©ðùd«!„S›L?B!Ä]´fë¸]Çé°î}N'%%%¬\¹rŒ[4´Å›6±xÓ¦ »žÓÞ÷Œ"cb&ìšB!Äd%E !„BŒHkÃà#6Vݹ‹˜„d4Ú0îxðHÉœ´²bóýJ\w©tÔír8”––†¤°1ÑzŒF££CÝ !„"䤨!„Bˆio¬Qâ-ýÉ ¯ŸØû&c'·‹Ïßú/å=eÇ>ð|•¥ÇǬm¡(l”ìßOÉþýv=·ËEL|ü„^S!BI­ö§®­m˜N‰uáz%ˆTb}¤¿«ÄQ±þ?G£ã•86!Y‰ã’R”8>9M‰S¦+qRZ¦'§g+qJF®§fÍTâ´ìÙJ<=w®gÌœ¯Ä™³ò•8{NçÌ[¤Ä¹óý‹rÎÌ_ªÄ³ –+ñì…þ¿ç®VâyKÖ*ñü¥ë”8ù%^°r“¬Ú¬Ä‹ÖܩąEw)ñ’õ÷}Ï;ùF–mÜ®¼¾lÓ%ìÐX±å%^yçƒJ¼jë.%^}×CJ¼fÛÃ’¢†B!Fmß/(qà?ä†rçÃ_Sb“¡mLÛ4FlôvwƒJêfqƒþ‰gØL8 •xFÞ%ž™¿L‰g¬Pâ9‹ü»7Í-\£Äó–)qÞ²õJ|-QX°ò%^¸z‹ŽŠ+,Ú¦Ä×N€¥îSâ`‰æÊ-Éån%L(×Þý¨Ý»G‰×oÿ#%Þ°ã J¼ñþ/)ñµ‘|›w}E‰‹åWlzðÉ€súwçZwßãJ¼zÛ#í÷'ÄËïØ©Ä‹×߫ċָòÀ¤}þ2b?·ÐŸüróüE„ì9þBCàg#-ÇÿùIÉðÆ?‡ñÓüŸÕØÿç9ð3¯òÿ\èÂý?;?_¨ü?{^W‰Ý.ÿÎ|v›U‰­³÷˜º”¸ÛЮÄ]mÍJÜÙR¯Ä­ ÕJÜ\ã_ø»¾ªè[S«©æ"ÕÅÊëÕçO+q`‡FåÙ£J|ñÌa%¾pê Nƒ-;¶—@RÔB!Ĩ.Tø9µZÍæÝ_eËC_eËî¯ý޽¯ý縶ëv/lø|>Ô ZnèƒoAjF‰û'ÅJ˜GôK„ýÛÝÆÄœü&LKWâ¤TÂ;mzާdKrç)ñ°Û~½§ÉìÀ=¦ýØ›”¸_/éZÒºxÝÝJ˜¨&q+zAû'§þ°èÞÇ”80!ݸӟ<öKB¯þ\Ã`‰çS0áônÔš€=j^[‰×²Ûz”ØjéVb³±S‰» ­JÜÕÖ¨ÄíMµJ8E°ùjb þ„ ö’‡Š`‰æÅ³Éå~%L(KŽ|¤ÄÅÞSⓟýA‰úºýð÷J|ð]ÿŠëûß~Q‰‹åW~=pw®ýð¿÷`ÀâÔ‡ÞóŸÿðû¿° Ç>þ;?ñ·óäÞ7•øÔ>ÿ½œþü%¼ß³‡>Pâ’ÃþgRvÔ¿àõ¹ãþ瘠>ÛŠ3þ$þb±ÿùW–ø“þª2ÿ÷èò¹“Jø}¬ ($Ô^ô¯ë+Ë”¸á²ÿóÐxå‚~fw* \,¼½Ñ_ÔÜ¥ÌÐÚ€ÇãV:,Œíþˆ±£E‰;4L]þBŠÙèßÙËÒíÿì^zÍFIQC!„c"ðró—®cß/ðÙë/ðÙÕˆ&ãE0zÁåö(…~‰±~àÄ82:N‰ƒ÷ §*q´{‚ãÔ€Ä8=gŽOŸ¤ÇwîB%>¬x^ËÅcùÔþ=»ìÍ-\ëOû%ƃ%Æþ^ÛÀaÀkïLŒzjw<¡Äï81Þ,1¾ÿËJÜ?)ö_7°mKû%ÂþaÑù+N~g.ðÍÎÊø}IË –äú‹sÃJlûõž&³÷˜öK`;‚ô’Öù“‹Àd$0Q Lv.ô‚öONý WñR˜ Húú%¡IeðÄó¿¦`ÂéßéJù)%®>F‰k*Î*qà:Bõ•ç”øÚÖØMÕJÜ\sI‰[ꪔ¸­áŠNìh®Sâk '@W[“K4Í]É¥A‰J«Å¤Ä¶^‹;l½Jì´Û”Øåt(±ÇíRb¯Ç£Ä²«“))j!„c,0±œ÷è '™ŽK Ò³œ–¥Äçûõ&Ïð÷&gÍ^ Ä9sý ÜŒ¼{çÜö›gÐkÜ/9œO»¡/¹ì6´Stïrç/V’ÒÓŸ¿ÃÊ;ž8„90‰ÝÐ/1¶¼Ëß8T9°÷xÓý_TFl¬-òßÇê­'Æ‹×ûûà½Âþg3sÁÀ=Á‰qJ@bœ0" >)Ho„ÿs|Xq_bìóù8RâO|ú÷ì<|8°7·9 ®_b\,1ö'qI_É‘ÀÄ8 §öSâyôÃã}Áã7¥Äý“âß*q`"|¤_"üšŸøtàä÷Ìþw•øìAÂ[zäc%ìîŸäPâa%¶ýzO“Ù{Lû%°µAzI›üIk`±#0Qíîô÷è›zAû'§þÏŒ­×_X LH]# ú%¡^ÿðv!„%)j!&†&‘•}¯nJEìu ·íbç¢8ùÃi*REQ°Œ¥¹QúýL^ªZÐûÜ/ÉÞ韼é/+q`s`b8ïwmÀÐëá$Ós—éYæ8|º_or’¿792Æ?/]0]­¸9pÎm¿y¶A’ãÀù´Wµæš‹Ø­f/\Ù/‰ºxfàù±C˜{[÷KŒ†-¿íïñ ìþìõçý_³/av8œ<~”+ú 7ß81>úá«þëí~[‰Ïì¸'801>Ÿ?ù¹_8¤Ç·ô˜VÜ—÷šÍ˜†æöïÙxøp`onGÀ÷°_bÜ,1ö÷Ú~.‹)ýzjíþÏ‘Ë9pbì“ÄX!ÄMR½ñê˾ÝþÑÐG q›*,, -5§Ó5ôÁ“nÿÃ_±äâ/øÁo*± ýPéI™3—ÔÞ*Ê›lŒÛ¿ëÛ–Þ—e'Âß¾VÏ€O[7“/=ýæíûG~ð^+•–¨Ä$¢œ:,î¡?kš8 6¯#½é0Ÿ]01ž©N˜VK[‡ÒÒrbâ“”¤;&.ËÕd-*&žÞ«Éš>2»µo®³.<§£ï§M¦SàR©Õ’ Ñ7j£4 ç>aÚtf.XªVny ßÐö‰¤×ëY¸p!§Núà[@BJ qq4WýÿìÝwtÔ×µèñïTõÞPDBôjªeŠ1ƒmŒc'.¹Nœç”çäæ%NoŽsÄ%žÁ&vâŒmlÀ¸¢wBH¨÷6¨ÏŒ¦¾?£ 0šŸ$ög-Ö:Œ~sÎ!!ýöœ³÷¹k_,„BôC›Þ{«ë7L…ž¦"`h*kï[ÀÔaáøÖÆ Îüˆ fÒx#÷B3†ÊJª Fì×¾º~«ž|‚ÁÛ~ÁÏËMW~ž*ˆÉÿˆÇ§„£»¸”Ñ@qÎ1>û`G«,×¾A½‘ØDŸ£Ò7b$â ñU6L u”æf‘c`ÔÔ)„䥱«¹RT*=±“æ09Î×µ³Ãi5ÑXWAþÙs”·tï+Ï}A³{*·wŸ/&4WB:W—„F;÷]õµOëØáá~¶_ïë×é¬uO3›Ídff2uêÔ~‘Ø0ED(†ÂCRS'–vòÚ !:‘ÞÂã4Zkì^¥Ü/ È5v¯LÜéÌxGÑ5÷mÔî…ÖÜ[k¹Ws¯4îÞ6˽U–{µ$·sâÞ ™Âãß¿Yùl}c=/ýíUÞÚqƒ©óÞ Ù*ùì/Ïð§­eX®}u÷©´F…£+ÿ„þð'žýóßùÇGé‡-â›ÿïFûw£`OÅ&z •.’”Ùs™<<Ke.'çØ‰J›ÀGíÉ=@*tÁ€¦Óãjôþ¾¨›s9¼ÿ û%ýl%ÖðaLž5–(]Ó ›ëÖábôwÝG@P(óV?Ö©“…ûÙ~÷ÚÞâžØèëÚŒF4:*µü:'D !nŒüì&÷_Ü w¹÷Øv/ôÚñΉ{¥ôð˜x×8*.Ñ5v/îïV`l Û¹i÷*èÃÆtœ­vïiíÞ̽wµ{믉nÂÝÏtOw+ÞÖÕYr÷êåîý¤Ý+–»us¯R>éöeñ¸U&ÕéÌxGÑi8ÃN IDAT5÷„…{¡5÷ÖZîÅÕÜ+»·Íro•å~î·Ñ휰7èŒe¸¾…}o¼É–}éœ%UT”qöÔqNU¶¹vó&§²|ùRV,_ʬX¨|0z© ïbÅò¥¬X~'3ùqµT™OtãF†£»ÒE–fΟ?¡®†ò¢lŽeÔáÔ‡é'_ =ɽ…æ!·ÜþZ›ØùÁk8ìv†$OìTŒ:×¶ˆ:êÒ)zŒ{bC¥ŸÁQj|Tîõ8{„6n63î¿“hŸ›ŸËépà°Ùð ¸ùÉ„Bˆ>êŠ?º;½Óîë~ÓÞñ ŒûM»{ßy÷Šìîï®'Mè¸qí̭ܺ »[»2÷¶oî½»GOé¨@Þ¹òú"ר½ÙÔy+]ãNUÖ»õâv» ïTôí·¢on=·ÝoòÝ{l»zs¯ï^)=1©#17äÊÅÝ‚Ý*¢û»µsO¦8ìoí»÷´voæÞ»Ú½õWɹŽjÛîEÈθoËp;í^°Í½z¹{?i÷Šå»>ü§kì^¥|ÿ¶w\c÷Êäîg«Ý‹®¹·Ür/´æÞZ˽¸š{¥q÷¶Y܋¨¹W÷[C3g,QWº3Sù0ìî§øþÊATñOžÿóK¼üŸ42ꬠdèäbª¿à•žç¹—þŶ3M—×Pû30eaåÛyù¥¿ðÂÿ¾Ï1ëÖþ÷S¬Ü9µ`Øñ2¿þÕoùù¯~ëgÌ—ÎtIlZ¢Fq{j"êÆ\² 8P¢ `̃OñõÔÎm~ç_|•ÍY¦Î7­*?’Ö>Å“©zŽ¿ù<¿ýŸ×ØÑ2†û¿»Ž±ªî^„çé– ÇV‘M~ýÕ–˜ ±g÷^ÒvïáD­ T>„ÅFКÏñC‡8p8ƒ¼Ú¶›¯ï¢R£ ˆ$11U[uFùJèIî-4Ý“Ç])Ì>Ñ©½á¥‚â»üXO0›Ídçd²pù’'G±`ÕX’'G“¨GÛU&Ö4aÃ4"Ÿ›Ižèâ™ôäÓ,_:kk+ZdH„Bˆ>ÊUScÁ½»nLç.ÐuczÛÂÕ®Ö[æ,qU=5•Ã_lÚo|ãê=p(Õe„Ew´¢ḙ̈ܺ »ÛÛ#îç”Ý{w7:¶­v®¼ÞQåÛ½Yî©C®q§*ënUÊÝ[޹WAß¹©£ÍØ·Êêî­ÅÜ{l»WM¿ø¹Î•ÒÓ÷luÝ ©¹Ç–íV=Ç­=˜{¢0;Ý5vïiíÞ̽wµ{ë/CUG²Ã½ªy³[B¤«³äîÕËÝûI‹k³×¤ñÊ¿xòÁ¯ó‡ÉUdîßËŽȬ6ãT)¬šMåæ_ó¿[+;ɼðj,?ÍɬâŽã]TÅ1UžáTfûu™%8ýCî\‘Ìg=ÅÅ&m–†*JËj®^ïbä7øëz÷ ìýûÇ䘜p“¿C«‚Ç²ì¶ Ê7ý‘×>»Gn‰³S¸ØˆR4†•sB8ûÆÿ°ép#N èß¡Lzf%s}ȸЪý²Ï‹èqjŸ`5ÐRÛ|Í‚®vs M­I‹ ÿÕ[›j©ªmèøÔ\þÜn‰˜Ìâåî˜(9šCTšõ¸ØÁ#\?[Ü[hº>v6«…¢³í?‹SW=̾­owú¹~%Ùn‰õ ³ïê”Øî *5F8°éjyðžo9Œ#¡i ý0P]dÁéžS0hí·˜1¢cW ½µŽºÜdï>Ju³&:,´ÖÕÒÔØj?üƒ‚hª«»öó„Bˆ~ÈuKÔå;ퟸߴwüã~ÓîþNþ‰½ŸºÆîï®ú¼ãúέË:’î}¼/þ2{w»'/:µ%«(vÝ[‘5ª]ãNíÇZÜzq»µëTôÍ)ïò rZ¨Ø»ŸÙ̈É3I½c ß›·‚ümçÅr±DŽ$AÛĉ3Ïvýh+'=§•;“†©íHjtKÉ&~ÿÏlÌ*-¾Á1$ÍZÆÝßú¼ø;^¿ÉbûºÈ!Ĩš8–[ßebE9œX†GŸåµG;ÌæsÕã ¢ïÓ„eáìAnÇ—Æqç²öÝ}Ƽ½ì<{áÿëÆlö¬Ã† ­O ƒF’¢¾º–¶V÷u5è}¡n/û¶`Ñøà™ÈÐ9 IÞW>¦Ò襯6{-9ï®' ÇWŽŸ!„¸…I÷!¼ÌÙf ÷À'äü‚Ï—~ŸŸ¯ü‹ÿ’Íj*è™0 R]À\GyY9íi¿bÎåV4âGÜ1?‘wϵáp‚Z§¹±ä‚³ýµªÕWy¶ T˜Ißð<»7uÒVߌ“K poq´5ct@Xxšs´ßµ7cÿž4¨ð4‰)e>YÅéÄÞÖŠãbo›‘榦 m‚9¾ŸðY JVM͕ۋn5i¶ë͈¬#»nz¾îìÀpOxÌ[óuv¼¿þ*WßC“b™>1•iÓ¦ÊìÙ³iii¡¤áÙu—$5.0ÕP[RÒ¾3¬ð•Z–<2‰¤AŸSyÖê bç,a´‘û‚¥6‡Ü/>%;¿ùÊGã|‡0åá{å‹ 'mUYdmÙÆ¹Ê6@Càø»˜:3‰ˆÈöB¹¦3ï²ý£&’¾õ8CνÆSNüBãH»€qðÓ€£¥˜¬·ßæLµlWBÑÿIRC¥8Í”gžåüÊy Õb-. Ê1—äÑh *=w“¨`ÔðìU…l€Î†Ù ú@Ôp]íVUZ?´à°ØqÚÔµÂäø¨*±u3£º¼±Ö£Â>—Ñc£Ñç_¹Cе®*ç\ÅAÝÁÊ˯‘ÿÁ”c«§¨ÚFtBƒó“¥­÷NVhôšëJØ©TjýÚÍ47š6“§ÝDSc#æ‹©ºho¢Ò¢S·×’]7Ï}—ãÕ„DD3rüLŽîü¨Óãs—?Ä¡/Þ¿áÖ­î ¨øDjÝŽžÞŒ6£“‚œJŽ„¦ÈŒ38tèGNì¦ §‚¶nîºpXÛp F£Uz"|¹Ó¡äË÷9Q ¡0þ¯¢Ý°žSåWøŸÎb t÷fJ›[±ûÄ0äÎ¥LZÓ@íË»h°«ñKM´'7£¾M‹ÖZ‰•Žm-F'LaÚ$+ç6ÿ‹bƒŸ° ښ䈨Bˆ[ƒÜá%>C–òÈ:ò³‹¨n² Œcâ¢TÂÅ|ZiÁÙ”Áæ½õü÷Êoó„úöæÕc÷ïêG¯³QKèÄ%,¯<Â9ƒŠøÛV²*¦‘Ýož¡Å XëÈ­°“:{9 vQª '¸á$ò[/¿ ˆgd’‘6­áwûÌ l`wZm6éÇjY±ø¾¶øCöµ ŽHÐâÀa¢Þa£§0&¶šU§ù`g-?YúžTofgvVÿaĺ5±q6eðñ¾~p×·ø–ãvç°úFTÁþ=…×w”Fx–ÓBuÖi*#&2w¡yÅT5š°©ô„„КÏé2#ç›$LbX}øâk®¦´±‹9í 7ߨx¢[©j±u/)¡""ŠM­AïBÌàa Ô›).jì‘$·‚é ׏ޙº¹šFCÍe èÜáÄ]wkm¸‹<ÒcI ‹ÉIUYÃ…ÐÒÒ‘»9–û1Ue XL]|õ©4h´Z´Z_ü¢†0látœåä”›P$3fjÍi¯pø`- ªØ€6ö$ß>Œ³og_>Ÿ£‰êì‹GbËiÜ1ŠÄµCõK£ábI«Ör*r i¾¸ÕCÓ‘ÔpªÔøù¢²TS“_Ây£*<ñB!úIjá*ÔÎfŒ!·³ä¡E„èÌÔå§³ñO›ØUkLd¿ûG^jYËšyñå*h«åØ[9»Î¤†Ãĸå²4XÅË—ëßâý³¦öDgÇßz›ßXÃ=OŒ[=Yp8¿µãÐi§¥®Ûäe|çjk¤"ÿï=¿…Ùís•nù;¯|•5+eа1”œ¥ðJïÚÛë8¸iSIåy'8ýVùïÿ‘çšïåÞù÷óÅZÀFKMÇ*LíÛ´Fμý/5ÝÇêÛàÛKÔàh¡ìÈFŽï•¤†Òœ¦rŽ¥™’4‚ÄÄâõ*À¥¥žª5*§‘ŠÌL¢&&yJ48ÌÔž­§¬«¤†ÓHiv±3¤’šÌ‹õVœ´äífËe ‹œXLf±#™v±I–­æú ²åRX'O®‡Z£Åaoÿ_À½ËTO¹´ÖFwœ:ÐQ`{ôÔÛ9st÷ ¯ït@]™0ÐÒðQû‘“œ ªÊ¨+¿¤H¨»„U¬øñ*×_ ùœy÷òë¨ã¢n¡¬ ¡ã¨‰½žª"#)# Í¾ìÿ-•ß@FÞ9ŸáÃc ðWaoÓ ¦ m7 çZM-˜‹ÏR–0‰Ùß~œ²cGÈ=šIm“¤ô„BÜT›Þ{˹fíW”ŽCÅŒ?†1‘X,ýàHÏêßü”ÙÏò£w¤ˆ]ÑiµT×8uêôµ/¾EÌ[ý(;?Øpí ¯`8ôÙûXÚ.?b2{éÛõq§NZž—˜DEQÎMÏ£R· õñWÑftb19¯œÐP3ì±ï2E³›=Û °Ø,´57ÐÚÒQŒV¿„e&Q¶á¯/¿ø3EGäÒï0D_üu;­I³jµ“|ƒR³? ÷}‡YƒJ8½u ú¡w1wŽ/½J^£–¨•ßc^ì~¶ýã€ÛNÆ^¨©±õ³JGá™b¢ÆN#yöt„œ'ÿír<ß(G°„Bôk›Þ{‹›é’.„Bˆ>,<:Î5¾Ñ„Àžÿ}Å„´·Jï*¡ÆŒE÷ÞКî ÛW~õ†æ€öm­Nšj´µv‘Ðp×V‡¡´ Ce --»ë8Šit=4´ã,MýqÔ–ÐêÚ<¡B¥4!DÐaÎÚÍÙ3%ÔWWa¨lºrAÑ._€‡ÝŽVm¢&ýsvÿýɬ gèÌÁtQuF!„èW$©!„BÜ¢“')º~kS=?ÛxÓóøô?®qHDÌMÏw£œ­ç8}´žàÔµL»-™C“µr-)áçÉMË£ °›Z°Å ”øÑÌù:;¾£ç4:‘ˆØX£¯ó—35šÈD†§ #2.ŽÈ¡Ã[‹ùú’#B!D%55„èOlå|ðã'ø@é8„½ÖäIf§¾{ËMÍ•L›¿Šàðè‰åFøô]×xÐÈq FâN§tB!„"$©!„Bôaq‰I®ñÁíÿéñõv¼ÿ*N§£Û×ÙñMçk®k™‹×âr½¡Ý_¿¾lÃétâptÿßD!„èO$©!D_¢ gÚýßàë©1h•ŽEOSù?f2“ä‡Óu twàÓ÷0¶4ze­ÜS‡\ãɩ˽²¦§9N¤†èRS'(‚¢’ß…èKÔA ›¨”ŽåZT¾DÇØx¿Þ«è*-1Di»÷5 Ö;€IÙ]Kò”¹®ñ™£»½²æ¸ ‰NrÝÏ›œºœ°¨ØˆÈó ²:ÚÝjuz#¹>Z•Z~¥}_ZÚÉk_$„—ß…ðCSY{ߦ ǰ6VpöàGlø0“FÅÞd롸ôƒXõä Þö ~^nÂîÁˆEï¡Ò7b$â ñU6L u”æf‘c`ÔÔ)„䥱«Ùæá…õ ˜tÓbêIßy”2³Óõ!upwÜ> Ëé4öq^eš¾ªÉp}Ç9¾Ü¶è>ö|ü¯zþ¨¯­tg/}€´^÷ÚÚ7ÃfµbmkS: !„B’ÔÂKÔ!Sxüû÷1æüQ>|ce-*‚c‡’¨jÁ¬à®áÞ—èýTºHRfMgh™ÚÂ\NžoŦö%8"µ'S *tÁ1Äú5S^ÝÚž sZ¨ÎΡ>6…QCC¨<ÓÐþ¸Ê‡Øä!´p¼¤%4f,º—ƒŸm ¼ð¬ÂÑôϦAÁ¤•ë¸75…_-¥élyó¾(0âT‡3íÁÇX=a ÑA:ÀAù}|zÜÊè9ÓH‰BÛVKægo²~k.-’xQˆ–ðä‰ j&wßÎÖwìĨ(+ºpI É©,OnÈpìsöW©<žQñáûj+µ§ör¨¼ëÕ|¢“[@MM+ö ™ §±”Œ¼Dn>šAE)4:ц %ÚNé¡|úÁö ­Nïêb’qðKEbˆ<‚𘲎캡çOœ»„’œ ÕeŽÌ{ÂczuRÃf±`6•C!„P„$5D¿Õ›¶†2 LeÌœ±D§Sk½ä=d•/Ã×|Ÿï-„Ã×óN9 œy7÷}÷)ôÏ<ÃÆÂ+m-¶Ó”›ÆÛ/o§¡UEø˜E|uÅc|­ài^LoÁ©dèäbª·ðÊëçhVûAyîy€«Çe¦èhމcH ý„²:G{]ё؊6Qµˆ§×ÇðÉz~ŸQ*$š š¦N ÃŽ—ùÛ>6œ˜ëÌ ò#iíSOúù@Æ.ˆû¿»Žª§7ÑæGBòPBÊ6ó·O i ÊÂW°nxûßßÈ_ËÛ›°‚¯®ø/îÉþ ¯çõLÛJq ºp†%è±Uœ$¿þêGKL…Ç8Zb›Ѫ Âb£hÍåøÉó´©t¨šÛpr½5 ì4æeP4p£ÇÆQy¼™ÄqƒÑV »Îz㯭q?ÑÚT¯H •Åçnê†þÄžm7µ~T|"qƒGrêÀç75ÏÍpO茱€L…L]Ñùú¢Óé(B!nE’ÔÂKì5i¼ò¯ž|ðëüar™û÷²cç2«Í8UàVÍ‹¤jó¯yíóJl@VN5>ƒÊÒ•)l1æËfub,ÍäxiûßòK[ˆŸñ3RGG¡Koáâí¾±ü4'³Š¹ÒíÿÕãrÒrî0ùއ¸mT»öÕãðg|œ“Òw‹±øM$#ÙÙä™qRtÙü–†*JËj\‰UðVÎ áìÿæÃ8¢‡2陕ÌMô!#§ý:Se6™ÙÅXÈÇ5±K«8´ç(Yf ßJÊmß!)9m^ûçJx—Ú'˜@ ´Ô6_óóo7·ÐØÔÚqäB=CkS-Uµ I0Í b«çì©râ¦aêD3¡þ2WbîÃçN"$`¨jßÕÐWj:ô¤Úò"jË‹”Ã¥¦´@é.ã°Z±X$Á+„¸5¤Nkï’“v¤w½)”#I !¼Åi¡bï~rd3#&Ï$õŽ%|oÞ ò·ý?ÊÅ5‚xMég 7‰¶Z²r[X9v8‘Ú+%5tDOYκ¥SŠ¯Ý„UV}7»Mt#®–Æ3ìÎsòèÌQ„8H뀱 ÑU²=§ SÃçlÍǽ?ü Cïæ‹/ws´¨ùªEAu‘ÉÕhyôY^{´óÇlaWêêb£¹¦ôÁéT`v‚½™ÚVä+-œú1MØXÎä¶cw.€1o/;³›pàÄR{†SÑL  þô1JL}8£ 1ΕÔP’V§÷H±Ìñ³QQx–ÚŠbϦ°ê²Ž¤Æ¼5±sÓk FÓN­Óµ·uB!nA’ÔÂËœmr|BîÁ/ø|é÷ùùʯ±øø/Ù|sió½o,À¹÷^ùW! ªÌûæcLñ`\Kš8vû×ç0.ô8ùc“ ®9Ìéóv°•óé̉1³X´xÿd1‹¶>Ç6aîj!¨0“¾áy>(v?"भ¾'—=ÅnµãDƒæbÃiÇbé`¨G[3F„… )1÷Èn{Ó9öï)Aƒ ¿A“˜QÆá“ÕXœNìm­ǨœêÊpÆùQYÓ7‹ƒM™LAÖqNì½¹ãžb³Z<²SäÔþÏn>`ĸéXÚÌçœòÈ|žöáë®qDL‚buCT*6kÿ8r%„×";4Ä¥ä–@¥8Í”gžå<¡ Õb­=G¹=˜QÉÙFm$)#±•çQçvרR«P¾F0€"¶mÞGfa¥EETÜl­¸Kâ'ͧ¿ä¸9‘ù3F1irµGNPm븾*so<÷ žÝÕÄàyóHÔNf+è}:ýGc­+¤ÊéË 8¨«¨¤Âõ§ ƒIª~ö¶zŠªmè’ÔŹ§«4zM÷w*í×ÛÍ476ÒÐØH³ÉŽÓn¢©±‘†Æ&šÍý  ¨»¾˜‰ñ²s‡{UBÀáèø:”4N‘ÔÚöŸN{?ûžB!ºIvjá%>C–òÈ:ò³‹¨n² Œcâ¢TÂÅ|ZiÁÙršvÕñôª'xÔú!ûËT œu7+cjÙ¾!‹'à0Qo„°ÑS[ÍéšB ÌgѲY4.¥ÙNŒ¯gãpšòølo=¿Xþ4ul=Vƒ ÐFNäŽ(+=YÁè05br¶:r+ì¤Î^΂]”ªÂ n8É‚ >Þ×Àîúßr|ÂîVßâ‚*Ø¿§VOâEÏpZ¨Î:MeÄRæÎ"4¯˜ªF6•ž€0Zó9]fä|³“ÄI «/¢_|ÍÕ”6v1§£=æOt`+U-¶~}¯?iîÒ/Ñ,8s\áh:›|û2 ²Ó©¯©¸é¹ÆÎX@MiA§cý‘{AÔQ“fs6}ŸWÖõñõÅa“ÊBB!n]’ÔÂ+T¨ÍCngÉC‹Ñ˜©ËOgãŸ6±«ÖØÉ{ÿϼ`¾ŸµËç6?0Vd°ùÅ·ÙRÐÖ~sg¯ãà¦]Ly$•æàé··ñ·wBøÚ²uüßÔöý––:òªŒtoÏCwâ°RºóSÎ-ü #J?ãà…mšÁLY6Ÿu¡ZÀNSÉ Þûǧ”Xš8þÖÛøÆîyb,ØêÉú°€Ãù•œyû9^jºÕ·?À·—¨ÁÑBÙ‘ß+I¾Äi*çXš™!I#HLL!^¯XZê©*P£r©ÈÌ$jòh’§DƒÃLíÙzʺJj8”f;!‘1C*©É¬¿PŸÅIKÞn¶äyí¥yEQN†Ò!téøî-›ËÓB¦Î[Iî©C4ª=:¯'µ4ž÷ÚZŽ6s—þ„Bˆ~Oµé½·œkÖ~Eé8„PÌøñc‰Å"瑯Êg8_ýí“DðKþ|°¡›I!z'VKu­S§N{uÝ;î~„]þÓ«k eÍYööny«Çæ8ÊóúYÖO!„è†Mï½%55„W¡Ò™8„Á‰£™ÿÕG˜eÚÅûÇ%¡!Äõ w÷|ò¦‚‘\Û c9a¦GçL™v±ƒGxtξäÈŽ\ã ÐHÏï°Ùhmjòø¼B!D_!I !D×´H}ì‡üâ'ÿ‡e¼­fCq IDATúâVŠ,J%Dß2fú<רnëÝ;ÂJÎ&÷äΙud•Åç<:'À‚{÷øœ=¡ÍÔq¨nôÔ¹Ÿß?8½ŸŸÇçB!ú ©©!„èšµ„÷öï+‡}LüÐdÊ ²8ô¹|õ„/7þCé®Ûá/>p‡Žžì‘±³ccW…j„BˆþOvj!„æùc=É/ ˆYKÖõÈÜÉ“ç?tTÌݧ]OŸã«ð ðÌDB!D%I !„ÂR¦¦ºÆÙÇ÷*È 0µ6³Û;=2wöñ=”œí‘¹C"b˜:oeÌÝÓ ²:viL_°ú†çQk4˜ZZ<’BÑ'IRã*RS'š:Aé0„Bôçk+”á–Óh¨æèÎÍJ‡qÓÎßãûøuç…V§Ã Øm¶ˆJ!„è$©!„BÜ ™w­u+‹rŒäÆÝvç=:´xZÒÄY žÒcó÷Íõu®ñ´ù«ºý<½¿?vkï.>+„Bô4)zii'•A!D/£Óû`µ´prÿv…£¹y=]È4çÄþ`pÒxô>¾œË8Üãkõ´½[Þr†¦,ÿL—×úúùa1™¼–BÑkÉN !„â:Ì\|¿kll–®½AqΩ~‘иԵvÐø‡†Ò&I !„·8Ij!„×;È5Þ½ù #ñœa)S2zR¯3bÜm N×ãëôGgÓ÷¹Æç,¾ìãZ­–&ƒÁ›! !„½Ž$5„è-ÔaL{ð;¡p4=£¹¾Îk €üÓǼ¶ÖEsW|•CŸýK›Ùëk{‹ÕbÁÔÒÂàá$©!„â–&I !¼­µ’¼Üs´8/yü’ïF•ÿHÖ>ý=R6ò«wQi4¡Œ[ºŽûæ#ÎZ+NñåÆwØrº;€Ê—Ä;Öñðò© TÅÀÉw_ào{ëS×B\[á™þ™È¸Õìùø_J‡ÐãBB¨¯¬$ãà®ÇR¦¥’u$M¹ „BÈÑ}!z#], žø ì_òüßÓÚ*_†¯ù>ß[GéÖõüé…õl-‹gåwŸbõ´ ‹xbÝxŒ»Öóûßýgÿñ1ûóš$¡!ÄUL_¸Æ5n¨«T0’ž7gÙWðñ ðêšC’'1l̯®y+Ðêt4××wzì|u…BÑ!„Ê‘BxÛˆ¯óÒ?¾Þñ÷¶tžûÁz²/ß×D0ãѵ¬NçïÏ|Dޱ}K‡*p «æERµù×¼öy%6 +§ŸÁ?eéʶ¿˜ŽÅ/œŒddg“WdÆI‘·_}ÎÑ)‚×ìÝò–××Tª–†`0æ,áÀ§ï*²~O Åf±\öxeq®kœºêaÒ>zÝ‹Q !„ʤ†ÞV¶™ç^?ƒéÂ_¶VªÚMûßCçþ_SØþ»wIot¸ž¦‹A¼¦‰ô3\å mµdå¶°rìp"µé}ÎÖÜqÜûÃß0ôðn¾ør7G‹še§†WápÈwHdliê— ŸÀ@LÍÍW½fßÖ·]ãÐÈ4ÔUõtXýÞ¬Ô ìO;©tB!.!ÇO„ð6S-¥%%·ÿ).7ÐæV_ÃtîÙ-,zd9ɪë›ÛRΧü1O¿´’ð¹<þ“gùéªD|¯s!Dÿ’4q‡§(²öà¤qŒw›"k÷Wþ´44\õ›µc'Çð±Óz:¤[‚$4n΄ÔT¥CBôS’Ô¢—i+ÛÅßžù7§BðÝo¦2àÂ~*kí9ÊíÁŒJŽèØb¥$ed ¶ò<ê.nßpš©ÊÜÁÏý‚gw51xÞ<õ ¼!D¯‘sb?¥yYЬ]œ“Á¹ŒCЬ ›8’”©©Š­ïi~AA¨T*L--Ý~α]»Æ’`J9™–¦tBˆ~JŽŸámñ$ju?p˜ë(,»ø7'¶Úƒ¬)†Ÿþè>ž\VÀo6ÓÖršvÕñôª'xÔú!ûËT œu7+cjÙ¾!‹'h#'rG ”•žÇ¬`t‚?˜19®‡BÜ*‹r©,ʽö…}D`X-—½–6££B!”'I !¼-v1ÿç©Å«ÛÎÏuÌí'¦ü-üýÃÑüæîGXšþ ”˜É{ÿϼ`¾ŸµËç6?0Vd°ùÅ·ÙRІЄ fʲù¬ ÕvšJNðÞ?>¥ÄêÅ×'„è5ÂcHLGúžmŠÅ0px þA¡äœØ¯X ýI@p0UEE7üü✠×xæ]k9°ý=D%„B(Gµé½·œkÖ~Eé8„PÌøñc‰Å"wþBÜ*tZ-ÕµN:­t(ÂK’'Ï¥©¾†ò‚³J‡rÃô¾¾Ä Biv¶Gæó ÁØÜxaбùêu:„BˆÞfÓ{oIM !„BôÙÇ÷ôé„@hL v«çðfÝé±y…Bo’¤†BÑÍ[ý(jò§L㇌"yÊ\¥Ãè´z=ç++{dîÛÿãNß#k!„=A’B!D?´óƒ 8ì¶k_ØÃÊ Ï’}lÒa¸ÌXt/ÁaJ‡qÝô¾¾øúûcnmíùµ||{| !„ÂS$©!„Bˆ[ÆÁÏ6ÒÚtãÝC”âHc]WÖ:—qØ5žrÇ ¯¬)„BÜ(Ij!„ýȘéó0h¸Òa¸ÄAÊ´;”£Ï ŽŠB£Ñx}ݼÌ#®±Nïãõõ…Bˆk‘¤†BÑœ>¼“ª’<¥Ãp©,>GÖ‘]J‡Ñ‰V§'uÕÃJ‡q]6Mƒ××m¨«rg.¾ßëë !ú¶Ôé”AÜ$©!„Bˆ[ŠÍj!í£×•£Û|üüÐûúz¥žÆÕìÞü†k—8RÁH„}EÚá“J‡ n’ÔB!ú˜Ã{Û|¥Ã¸LLÂPÆÎX t}Z`h( ìÒ¸šÐ¨8¥CB!Ij!„ýBui>™‡v(ÆeªË È<ø¥Òa\QÄ€Lœ³Xé0®)("cc£Òatræhšk*-1Di»÷µ¨Ö;€ùJë‡ÝÎÎ^S:Œ.ÕVsjÿgJ‡qUÃÆLeHòD¥Ã¸ŒJ£AïïO}UÕµ/î%vnzÕ5ŽŒ¤`$B!n’ÔèÃRS¥ER_æ;ú¼²þW¬ˆóЛÌ¡²’*ƒ;€~«ž|‚ûÇyèÝŽÕö6Îë|¦ÿøïðêú?òø¸€+ÜÔê¸òlXÿ3Ek<©ð>•6ˆøäÉܾp1+–/eÅòE,œ3™Q1¾¨4!Œš:…11>^‰E5™¥ËSI ’q¢{òO¥0û„Òa\&|ÀLÍÍJ‡q]œÎŽŸ ÃF+‰Bˆ[…üÆ×‡¥¥I‹$áÆVÉgy†?m-ÃÒóÛœoµÒrވ㺞¨& *5Üvß|t—|4l ÷ß5ð'Â_þKê‹TºHRfÏeòðP,•¹œ<~œc'r(mõõ¦À®ººà Š @Ò_í&Î]BDL‚Òa\UxL“æ.Q:Œ>)(4”ó}h—Æ¥NîÛî'Ož£`$B!ú3ÙÛ+Doà3’ÇŸû¿ Þö ~¾½; ‰^È/w%Ï=Íú<¦>ðwOÌ€`=`£¾0Ïßßȹ-íIm<«óSfg<ËÞ)v%6b×üŠõkÚǹ/ŸgÓ `ÒÊuÜ›šBŒ¯ƒ–Òt¶¼ù_qªC˜°ú!îž>œ¡>€‘¬7~Çóû)*-EWÓÖ¾SCåKâëxxùTªÀbàä»/ð·½uí;E\4F‚å<-1óX=>—Ž5]Øí¡gÈ]ËI¶ÖbÒ† ¬žʸ¥ë¸oþ8âü¡µâ_n|‡-§;æ¿Ö5êp¦=øwOH &H8i-?ɶ7ßb{^+În¿Ñ5-áÉÔL­·¹>RQVtá’@“SYžÜþáØçì¯R3 y<£âà ömÿ·¯=µ—Cå]¯æĸØjjZ±{2_ÒGسMé®é|uç«Ë”£[&§.£ +úÚ ¥C!8"«ÕŠÅdR:hª¯S:!„ý”$5„è Ôþ LAXùǼüz1m>Q¤Ü±‚µÿ=€ß>ËÅ]ïÍ0ìx™¿í3`ɹΠ*?’Ö>Å“·Øöæó¤Ÿdìò‡¸ÿ»ë¨zzm œBLõ^yýÍj?(oÂá´‘þòs¤_˜W›°ˆ'ÖÇðÉz~ŸQ*$š š¦+$4ø‡ùá,ÿˆ—.⛫R‰?õ1eVP‡MfÍœN¿÷&Æ5¤G…§Ê—ák¾Ï÷Âáëy§μ›û¾ûúgžacatçµ ÉC -ÿ˜—?+Âì3€‰K×pï·í”ýxÙÝ} ¢Kºp†%è±Uœ$ß-¡q%¦Âc-1áÀ‰ÍhUa±Q´ærüäyÚT:TÍm8Ñ{)x!:;ž¶Eé\‚##i¨®V: )/Èvç,½Ÿ¼©`4B!úIjч˜*Ïp*³}FFf Î_ÿ;W$óÙ_OÑU³?KC¥e5®uUðVÎ áìÿæÃ8¢‡2陕ÌMô!#§ý:cùiNfwy”EãNF2²³É+2ã¤èʪ´éq´Ö’ýé§Ì\Á²Q;øßL ©KHjÞËoW3m ŠðGCöÀ1¬šIÕæ_óÚç•Ø€¬œj|ÿ”¥+SØþb:-ݸæâItSÅN.ÆB69uŒÿémLב«êæk]Rû¨–Úf®žÒ»¹…Ʀ֎š,NY›j©ªmpÛÓ#¡ö+ ÃFÎÙô}J‡rMaQ± M™Âñ´O”¥ÏЕЖ†¥CéG¾øÀ5‹¢©¾VÁh„Bôu’Ô=bVêöKÍžÕVNzN+w& 'R{ŠÖnnÅ×E'V£!äÑgyíÑγ…ùt»SJ[ÑçlÍǽ?ü Cïæ‹/ws´¨ùò]*_‚ý ­® «á[Î÷VL%ªØÀ²ÔPòÞßI±IÇh øû¢ÔQ#ˆ×4‘~ÆÐq£l«%+·…•c‡©M§­×4_ásbm(£?Âü5´ewó5EiÂÆ²pö ·ýã¸sÙ8Œy{Ù™Ýtu^ú¾²ü3J‡Ðmõµ•}.¡1oÍ×Iûp‡÷¿²´z=!ÑÑTæç{}moi3]ãQ“gsäËŒF!D_'I Ñ#$¡Ñ *à¼ð®µ‡Ô:Íõµ_u´Ïs]ÏQ 3éžçƒb«Ûœ´Õ7ã$°{óXÊùô?æÄ˜Y,Z¼ˆÇ²˜E[Ÿã›‹0»'Ô¾û€¥Õ‚Ói"çÓÔür1÷ßÓÊXû1þ|¬‡3”Ö6>AíI•-•à°ã@Õ¾I »¯AtÉÑÖŒÑaáhJÌ×Ü­q#ìMçØ¿§ *üMbJD‡OVcq:±·µ^!¡¡êù¯#ѯ¹·%õ¶¨„šê갘͊ÅàMî a)SÈÏ:¦`4B!ú"i5 „"ÔÆD §&“ìFêZ!t`>ÝÍPh#5<{U!†+ÝI:m˜­ ôéôn­+¤ÊéË 8¨«¨¤Âõ§ ƒé:ß•tš©ÊÜÁÏý‚gw51xÞ</-‡ Öè“ `«ÚÏ–³þL˜KÍŽÏ9gr‚ÓŠÑ >>hkí9ÊíÁŒJŽèȼj#Iˆ­<:[÷®ñØk]³ÕSTmC—Äà .Î8X Ñ__ÒN¥¢ýz»™æÆFi6ÙqÚM456ÒÐØD³ùÒ}5*ô~¨±Ñfíi ?æ,{Pé0®Kpx4Óæ¯R:Œ>Aïヿ?õý¨–Æõp8oµ=WB! ]õu–èÎpôœGh V'@É&rš`¯!ýX-+?À×Ⱦ¢ÔÑ ºd–ЉKX^y„sñ·­dUL#»ßëBµ€¦’¼÷O)±^r¡JŸ¬¦ŽÓ¶’¼¾Áý"V“‚ðUC£ÝLÞûæóý¬]ö8·ù±"ƒÍ/¾Í–‚ -eݸÆS¯A\•ÓTα43C’F˜˜B¼^8°´ÔSU Få4R‘™IÔäÑ$O‰‡™Ú³õ”u•Ôp)Í."vB"c†TR“Y¡Î‰“–¼ÝlÉs»VN³Ÿa£˜þþTxmÍÞìÌÑ4ר×?³±E¹`„BôjªMï½å\³ö+JÇ!„bÆÀ˜H,–^|'«gõo~ÊìŒgùÑ;]w$BtN«¥ºÖÀ©S§•¥Û¦-¸›³Ç÷õ¹N!ጙ>CŸ¿¯t(½ZÜðᛚh¨©Q:”^göÒØ·õm¥ÃBÑ mzï-©©!„BôG¾ü°Ï%4ZÏ÷鄯ÀcHš0³G×ð D§×KB£ î ÃSŒD!Do$ÇO„B!ºPz®çwó„ÇÄÐÚØÕy,áÎ?(Té„Bô2²SCˆ¾ÀVÎ?~‚§ä艷œ!É“6fŠÒaÜ0ÿ PfÞuŸÒaôZZ½µVKkS“Ò¡ô 9'ö»Æ“æ.Q0!„½…ìÔB!z±Âìôk_Ô‹›8°ý?J‡qÓÆÞ¶€šòªK=[ÈÓ?8œNLÍÍ÷VP”“á«5ZöîöòBÑŸÈN !„BˆkÈ<ô¥ÇÐ~ô¤¦¤ÄãóÞ ÎW—¹Æ©+¿ª`$B!”$I !„¢ ç¶;ïQ:Œ›æëÈì¥(F¯5p f£‹Ù¬t(}ÞÎ:ú„Ç ¦`$B!¼M’B!D/t£]C&¤¦z>˜›`6¶ô«vœs–}¿€›žG­ÑFUa¡¢î¢ã•A!„IRC!„èGN¦¥)B¿¶wË[´™Zozž¸áÃi¨í{-zû‚ÌC;\ã1Óç)‰Bo¤†BÑËÌZr?~ÁJ‡áz_?æ.Hé0z•€v;õ••J‡ÒïÕUJ½!„èï$©!„Bô2û·½‹©µ´ø´˜MìùäßJ‡áQ7['$:1QŠƒzIUIžk|ÇÝ(‰Bˆž"I !z M8Óîÿ_O‘^ËBÑ‹ÝL¨„ÎWT`³X<•¸–=Ÿ¼é‡EÇ)‰BO’¤†½…:ˆa“'0*ʕұ!1rü «t¥ÕéI]õ°Òaô A†…Ñ(µ4a·Y]ã¡É“ŒD!„'ÉÂBx‹:ŒÛŸ~†¯ ¾üC…oüˆßö~HBˆÞ%÷ÔA¥Cð8›ÕBÚG¯+F0h8‘±ƒ8}xç5¯Õèt„@Iv¶"×r|÷×xä„äžìß{Bq«¤†ÞV±•—ÞÎÁä¼ø€cU+NB•ŒJ!Äuª*ÉëT³¡+þÁÁDÄÅQWV†ÝfóBdâz˜[[”A!ÄM¤†ÞÖZI^î9Zœ—<®»ôB-æ?Á¯M˜°ÈÞµ‘W?•A*°8ùî ümoöK—B(*,*–¡)S8žö‰Ò¡xœZ£!uåÃìüà5¥CQ†JÅ€ÄD 22”ŽDt¡ä\¦k?XäÃÉOþ͇çQEOå¡§32L‹–;ybÝx Ÿ¬ç÷õ¨B¢ ªi’„†½P}me¿Lh8ìö~ŸÐHš8 cs¥yY—}là¨QÔ–•)•¸é»·¹ÆÁa´6Õ+Bˆî¤†Þ6üQ^xåÑŽ¿7ìä×Oo¤è² K39^Úþ·üÒâgüŒÔÑQèÒ[°¦°r~4uŸþžW>.Á¨+Ãi|p:¿p0’‘M^‘çVBqórNì¿âãa11XÌfÌ­­^ŽHÜ(÷VÊãf,ààgŒF!DwHRCo+ÿ˜?ý묫¦†ÓÚ@…+|7ꈞ²œuK§226_» «¬z-*@9’m3§NUq¥Æ€mEŸ³5w÷þð7 =¼›/¾ÜÍÑ¢fÙ©!D/“ºêaöm}›µÿ¶ø\pïã|¹ñJ‡áU½¯/“ k[›ÒáˆàžÐH5¢³'ŒF!DW$©!„·k(.,¼¼¦Æ%´ ‹ùÞ7àÜû¯ü«Õæ}ó1¦\¼@­Aû®–r>ýã91f‹/âñŸ,fÑÖçøÃæ"Ì×X[á=ýµ3ˆ»[%¡1}ájÎÝCsCþÁÁøP]\¬tX´:½Ò!!„è‚Zé„©Ô*T€ï€  ˆm›÷‘YXFiQÆŽëlç‹0ÂÈahºšÌi¦*so<÷ žÝÕÄàyóH”ßË„¢GþâšêPk4DÆÅQræŒÒ! ÉË<âO·JÁH„B\J’BôõF=…1±¾Xj 10˜EËf1vø`ãëvyC[ÓM$¬yœïϨ¤qÌY4“¸ ×FNdáíI:˜!#’à¦FLE^â)ÓR‰ƒå’Т·ŠŽOdÜÌ;•ëRW=ܯ -ê}} §Ù`P:¡ Ðˆh¥CBˆ[’?¢¯Rû3l K§ $ÌO ŽV*ϤñÊ«“/Ý…èµjÊ‹¨)/R: ¯êï^BBp:4ÖÕ¹ Ž'1yé»·*™ð¦3Çö¸Æãg-âÔþÏŒF!n’Ô¢¯r4qòÍg9ù¦Ò!Ä­K£ÓCAFF§ÇÏ×”s¾¦\¡¨„ÒäŠBx$5„¸@¯×)‚ÂKœ§××Týöî+¸­ó^÷ÿ‚ €½±ˆ)Z’)Q•–å&·¸$NâïìdÏœ›3óŸÙ“›\œ«=g&³gÎ̹93;ÍÉŽ’8±ìØŽW‰–-S’e™"E‰Eì½ ÁŽö¿p²D'’ØüÖžÏÕ»-ëëmE"x×»t:î‡øðÕŸEüÚÒŽ>õ]\|÷U¬®,I§„”9) Y%%èkk“N!•™PÖ'"”› IDAT¾þoøàOÿ%XCDÝ8Ô ˜—`ÄÜÜ‚tEX‚1²g<ƒÁ˜hÀù7ÿ[:!äò**à]YÁàï>$+©Úèôèm¿Á:R“µôœÂ¯ <ˆˆhû8Ô ˜wùòUéZÑ›‹E·K Ñ=h*Þ¹}7ù{H‹rvìÀêÒ\ããðû|÷|mï þïœnË)®äPƒˆ(Äøô"R•·I6›tFX”TïUÖhÄ–ºÃ"-»@:CÌáÇ¿ Sb’tFH˜,èõzÌNOûÊ'MÑæ¬=<´jßQÁ""m°Ù 8yrl¶»ïÇàPƒˆTeyaF“ ‰V«tJÈ-zf¥HHË'ï`zlP:CÌ'oÿË‹óÒÛf4™QP€©áa¬,.nêkï;rެ¼0•‘¹g&¥ˆˆT¯¡¡?ýéËhh¨»ëk8Ô "Õ™GzAtzíÿµïøÓÊz|ð–` mGÎŽHËÏÇDÿ¦ðÅÇg03>†2ÒªµOH9úÔ÷KˆˆÔ«¹¹?þñKhnn¹ëk´ÿEåùyÌNM!%=]:eKâ ··Çu|~^°„¤åWb'·˜ãà£/ Ñš"±e…ÕÕøýpŽŽbe)ºžàBêpñÝ?*ëG†` ‘º¸\>œ9s.×ÝϰâPƒˆTivz65Ž<ñ]e=?ë,!i#}¸y…ƒ­OßyE³·_îÚ…ÉÁAÌŒŽb9?ðÌ¿ ÎÀGˆÓW­®,+ëòÝKˆˆ´‡C "R­@ €ä´4éŒ ±gÞ¾W¾éÏ¿,!¢P)®­Åhw7–<xWVBòžç^ÿü>oHÞ‹"#ÉfÀñ“ûtCêBé³³VÖ;jê#rM""-ãPƒˆT)`qvƒ6ž<]TQ+@*Ÿ`Â1Þ#¯8ððóHJ±Kgl˜ÑdBrZü>_Ȇ¤]õ uø¿?}õ÷8¤.\|^>a‡ˆh=j‘j9''aµ«÷¡’ª=Êúêù3‚%¤6Þ•e|ôæo¤3Tãâ{¯b~Ö)±!)iiÈ,.F²ÃÁ7Âr¤;<ü|XÞ›Bïrs þç_Âg÷8¤.\ú;n_³á‘¯GüúDDZÀ¡©–ßëÅâü<ìÙÙÒ)w¦ã¡DÑ$-/Iv;æ]. wv†í:ó³N\|ïÕ°½?…ּˇ³g®ÀsCê"¡µùem¶$ –© ¿#'"UsŽÁ’’‚$›M:pߑǔuoûÁR«û?G¦t†ªÔ?ø ’íê>ø7£°:#]]pKçý“…¹ÛOï9vR°„ˆH]8Ô "Uó{½ïëƒ-S?$v_—N •ûì옙ÎP•˾Ž9ç”tÆ]Å'$ ÞdÂÔÐPD¯›WZ…Ê=‡#zMŠÎüAY”Õ–ÉãPƒˆTÏ»²‚•ÅE±Ý_{IYÏŒGö‡" Ÿ8ƒ)iiÈ//Çd__į?ÜsW?‰øu)º˜,IÒ DD¢8Ô "M=[Ã’|{€òÉÛ¿‹ØuI» +v£¬v¿t†*í{à)¤¦eIg|…-3Žìl¤ff¢·­ ÞU>e‚´©«¥YYï=ö„` ‘ 5ˆHf§¦€`qzÄkmà eÍGêÑF t^Cwë%é UºrîM¸§UrN…N‡üÊJ˜’’àõz1ÐÞ.]„êú]X&AQ ÷æUe©¿/‰ˆ¤q¨ADšà]YÁÒü<“Ãwâ{nq¥²n~÷Oa»ÉHIOGIM \˜QÍ í—Ïal [:ƒ¢€krTY{ò{‚%DD‘ái†k|öìlŒÆ°¼²##,ïKÑ-ÑšŠƒ~C:CÕö= {fžhCFa!’ô¶¶bÞåÂêò²hQ¸ t·I'E‡D¤>¯î‰ äWT„ì=«öSÖ7¯œÙûRìXô¸ñé;”ÎPµ«çÏÀ91,rmN‡¢]»°º´„¡Ž‘†:tò›0[·bKÏõÏ”õýÇŸ,Q'›Í€“'÷Áfãm:DZÇ¡iÊìô4ÜSS!{Ä«{FÛω(<yyXžŸ‡{rR:e]ÎüK sÒ…º®]TÖñÆÁõhh¨ÃOú2ê¤Sˆh›8Ô "Íq#5#:ýæÿ³¥g£á‘Û· Œöu†2bÌÁG¿Dkªt†êÕ~iÙ½fbJ Škká]ZÂxD¯M¤6³3Êúàcß,QææüøÇ/¡¹¹E:…ˆ¶‰C "Ò$¿ßGNΦ¿®¤zZ›ß CÅ¢Oßù#=né ÕkùäL FäZ:½ù••HIOÇHWf§§#rÝP1šqôÉïHgPûè_+ëìÂrÁY.—gÎ\Ëå“N!¢mâPƒˆ4É=1}\t:ݺ¯-¬Ø­¬?oz sü!”(¥çç£h×.,ÎÍaìÖ-Mºº¼ˆóoýV:ƒb„=só© ‡t›Á€ô†<ð?þ ééÒ5D÷473ƒ“ †„õï 6&˜"PD±dGM=Š*yöFÕ6œ@F^IØÞ?%- …ÕÕˆ7Ñ×ÚŠ™ÑÑõ¿ˆˆÐ~¹IY×4œ !"Ú÷K {EþÏÿþßx¼®Ï8÷ÿþŸtÑ=MŽ"³ ýýð®®~å×jBkóû€îÖKyÅnµ]–NÐ”ÖæÂò¾f«öìlèõzŒöôÀ«Áw“‘WŒ¬üÊŸcDá69Ô+@D´%Ü©A_ᙞÆÛ--¸ÞÂC“Hý–<Ì»ÝÈÞ±ãŸ~m|è–@E‚ÑdBvi)2 àž˜ÀPGGT 4`r¸ Ѝ‰áÛCãÏþ@°„ˆhs¸SƒÎÎNüäý/eM¤îÉIèãâš‘šú‡ðñ_Nøò¢PKqd¢|÷|vö éM©®ΉaŒ toë} pdgÃlµÂ51±žžÑZMoüFYÛ3ràœä-]D¤^jÐm>œííÒD›’`¶À96†¢]»p¥éMéŠr³3hlAûåsÛúz£Ù {VÌII˜šÂD =¢µ|÷,/-b°«U:…bHÀû‰ E;÷p¨ADªÆÛOˆHÓêüÀãt"=/W¸†ˆBÉdµ"»¤9¥¥X^X@_[œããÒYÕuí"$êêGQÖå»KˆˆîŒC "ÒœŒÜbeý÷ÛMfFGá]]…Ùj•Ê¢(wô©ïñI:[Tµï(rŠ+7üúä´4ä–—#³ ‹ú¯_‡{r2Œ…D´ËKóÒ DDÿ„C "q°±·öH̬Â>¦††žŸ¿,¢»:ÿæo°º]‡QFÊ+ç1Ú×qÏ×èt:¤ç磠º)iipOL` ½³SSªT·ûüRìÒã»Ú”õÁÇ^,! ½Æ-~_Jòx¦‰Ñmâµeµû•G³¶~zç'ƒAý~ز²àб-êDZe¶ZaÏÊ‚ÑlÆòü<Æ{z°eO2 …Ï>ü³tÑW´|ü޲N´¦`Ñ3+XC´}MM|ú£Vq¨± ûëÐt‰¿Ù‰BáÓMþű‘OÈý>æÝnŒFèt:ƒÁ­æ)vî;й™IŒ¬³Ó€î®â¾CXô¸1t«£É€/o3ñ./cyq#ÝÛ{2 EÖâüí!FÝ¡Gñé;¯ÖQ,ãí'›ÀQdím|RYt^ÛÐ׸&&`²X`0ÕE1ææ•óhlSï+˜DÑ®]È)-…Õá@Àx_Fº»132"¨ :>ÿ#é ¢²v QP^+XBD±ˆ;5ˆHµzÛ¯léë&‘U\ŒÉ¬,-…¸Šˆ6Âh2!%=F“ F³ž™8ÇÆ073#¦YÁ`¾ú3é ¢{2™¥ˆ(Æp§©ÊñçþUY»¦Æ¶ô+‹‹pŽ!«´qÎnikÒ² PwøQé ÍIIKCaU²JKaˆ‡s|]",D1 ëÚEe½÷Ø‚%D+øÝ>…]ccÞ¡{²¦:àqùÃNÓë/‡ä=fgá÷û‘YXˆÑžž¼'Å–é±AL Jg¨šN§ƒ%%F³–ädÄ›L_ÇÇáq:•×u¯ù!‡¶/5- ;jêqåÜ›Ò)D÷Ô{󪲎‹3Àï÷ ÖQ´âPƒÂŽ ZOÕý¸ôþi@ àÙû.ÏÏÃg·#5#îÉɽ/Q,2šL0šL°:ÐétH°X àóz±87÷ô4<܉îéq4H\“£ÊúèÓßù×~)XCDÑŠC "‘[²#½7@h„Ãäà Škk±07/Iôàó?ÂÙÓ?Ù'èÄÅÇØ€D«&«ñF# ×cÙãA Àìä$t:–€ üÿ¨t×ýø}è»ùEê‰HÖ42óK01Ô+XCDÑ„C "‘lKC¤žw0zë2òóùÈHÚ°h>ŒQ§Ó!ÎhD||<ÌIIÐétHLNF €ÑlF €^¯‡we¾ÕUÌ»\Xp»áóz·|͞럅ð߀þ®¨²†x#nµ]–N!Ú”Œ\5ˆ(t8Ô ¢ˆ©ºÿn|öàæçGìº+‹‹XšŸçm(uò**°²´„ø„ø½^èõzèôzþöÏŒf3|++ˆOH@øò×ý~ø}>ü~,zp “#}Ò)o4bnfŸ~¯:½áóù ×ë±²´£É„åÅEèt:xWWXWí^§GÏÓLDÑË91,@DÇ¡…UÃ#_Gó»ŒöwŠuÌŒŽÂlµB§Ó‰5úµ~ú¾t¦yœN»þúòÂBkî¬ïÆÕõ_D[¶ûÐ#íëÀÔè€t Ѧ ܾ5ôØÓßÇGoüZ°†ˆ´H/@DÑ'ί¬[›?¸Ç+#k¼·Y%%ÒD¡£Ómè NŠn×.¼ËE…OÿúeâÈ,!"-áPƒˆBîè“ßQÖ s.Á’¯òy½Xôx[V&B*“]XŽêúFéŒMÓétšxBKaÅn”Õî—Î "•ó®Þ¾=´|÷Á"Ò5ˆ($ì¹ÊúÜë¿,¹·é¡!øý~$ÙlÒ)¤"c]h¿Ü$µ:¯¡»õ’tFÔ;öô÷oLÎ ‰Ïξ¡¬wÔÔ –Ðf4N Ä¡…DÑNíü%6ÞÛ‹´Ü\èãâ¤Sˆ¶L+»4(r>zã×_ù¤›(Zø¼«Ò ´AM—øT&Š<5ˆhËŠwÞ§¬¯~ô¶`ÉæM !¯²R:ƒ„âhüÚKÒ[£¡ó4òwìBÅ}¥3ˆH£Ö>¾øÀÃÏ –‘q¨AD[¦Óî”fg1ït"#?_:…ù¼«húóËÒ[¢¥C·®£ó‹O¥3bB¢5ý†tQØ\¿tVY›“KˆH-8Ô ¢MÙ}èeÝsý3Á’ísŽAKJŠt Ѧii¨A‘³èqãÓwþ(A6ó³Ne½ï§KˆH-8Ô ¢MíëN©‰þ~dÀ`4J§P„ím|¶ô錘[²;÷‘Î ¢(óÉÛ¿SÖy¥U‚%D$‰C "Z×ѧ¾«¬§FKÂc´§‡yAŸ7ý®©QéŒ-ÓÒN‘Þ›¸ùùÇÒ1%§¸UûŽIgELRŠ]:ˆ„p¨ADwd¶X•õÅw_, ¿•ÅEÌŒŒ £°P:…hã4tP(EÞh_'n\ùH:ƒ(b:®~¢¬ï;ò˜` E‡DtG{Ž=¡¬WW–K"cÞí†^§ƒ#‡·#D»‚²”×5Hgl›–vjd–£º¾Q:ƒˆbÄ`÷ue­ÓéKˆ(8Ô "Ef~©²¾pæ÷‚%2Æûû‘hµÂb³I§P v·¡«¥Y:cÛ´4ÔèBûå&錘TµïrŠ+¤3ˆ"jf|HY%D j‘"#·H:AÜPg'Òss¡‹“N!"Ú¶W>Âh_§t‘˜_ý™²NÏám¦DшC ¢·v~ÛÅKÔc¨³••H⎨b¶Xqèä·¤3BFK;52óKPsà„tŸœâJé" 5ˆbÜò¼t‚êø½^ŒÜº³ÕŠ´Ü\é ‘¥OtÝV¥¡ƒB'†zÑvñ錘Öðè7`±¦Jg‰ºvá]e½sïQÁ¢Øa°Ù°ïäIÂøa!‡D1èþãO+ëÁî6Á’Ð9ÔXÒ÷ó®¬`jpA)ii!}o¢PÐÒN ’×üαàqKg©ÆœkR:(&Ô54àåŸþu á;¤C ¢Ôuí¢tBÈ]hj Ëû.ÏÏÃ’šŠÄää°¼?EƇŸGRŠ]:#¤´4ÔHÏ)ÄîCHg)Fz;”õ‘'¿#XBÝZš›ñÒŒ–æðÒΡQŒxðù)ëÙ™ ÁmY˜EÀï‡Ùjåá¡vñ½W1?ë”Î- Ý~25:ð•mß$#Þ˜€cO_:ƒHu.¿ÿ𲶦rw&Q(ù\.\9s>—+l×àPƒ(Š%ÛÒ•õÙÓ?,Ѷñ¾>˜“’`ˆ—N!Rèhc¤Ajá]]ÁGoüZ:ƒHuV–•uÕýíí(¬ª’ΠMè¹~}7¯Jg„…–ÎԘƟ‘Π¿á'D£×ñG%"-àÿR‰¢L"Ù6¿ƒ(عS:…HS;5H]xÆ ÑÆô´_QÖõ~M°„ˆî…C ¢(P]ÿ€²îüâ‚`Iôó­®Â96Æ*—lKCý‰g¥3ÂJK;5léÙØÛø¤tÑ–u|ñ©²6&˜Kˆèq¨AœÃÒ 1eÞíÆÌØrvìN¡»˜sMãò¯­ÿB ÓÒPÃ55†Ï›Þ’Π°£¦E•uÒDš0çœTÖù†` ý#5ˆ4êàc/(ë±nÁ’Ø4ïranz……Ò)DD[r«í2ú;Z¤3ˆ4çü›¿QÖ9E‚%Dp¨A¤)ñÆeÝòñ;‚%|¹c#à÷#¯‚ßШɑ'^D‚Ù"vZÚ©‘bÏÀý¼ˆ¢Pjz–tQÌãPƒHC>öMe½8?+XB7=<Œy§iyyÒ)ô7ÿåV–¤3ÂOC…Î:'ñÙ‡–Π»Ø÷ÀSHMãfD[qã³”uíÁ‡K(Z5Öñ6Áõp¨A¤rެ|eýÑ¿,¡»qOMÁ»²Gn®t Å-íÔ u»rîM¸§Ç¥3ˆ4o|à–tE¡¦Þ&¸5ˆT® l—tmÀìÔ|++È.-•N‰Y•{#¯4vžJ£¥¡†5Õý='Aõ5ò]I“#}Êúøs?,!Š-j©PIõ^eýÅÇ,¡Í˜žÆ’ÇGNŽtJLê¸ú †{nHgÐxÜ3¸ôþié ZǃÏÿ:N:ƒ¶áB?ÑU‹¦×©¬í™¼E•(œ8Ô R#m|øJwàžœ„ÏëEFAt E9-íÔ møðÕŸñ÷Qˆe]TQ+XBý8Ô R‰ûŽ<¦¬{o|.XBÛ5;5…Õ•䔕I§ÄGfî;zR:#ò4tP¨ÅšŠ†G¿!AD$âêù3ʺ¢î ` QtâPƒÂ¢q?ïéܬÁîëÒ Bî‰ ¬,."³°P:%êÍL ã‹5ß0Æ -íÔXð¸ÑüÎ¥3høø]¢ðZ\˜“N Š:jPX4]â=qìéï+ë™ñ!Á ‡™‘ÌÏÎ"·¼\:…¢–†¤|ü.Qx ­ùëÐÉo –E5ˆ",1)EYú×?–P$,¸Ý˜›šBAUì<•#’Ž?ûèã Ò24tû‰ÙbÅ¡“ß’Î "R•«ÝÞehIN,!Ò65ˆ"¬îÈ£ÊÚ»º"XB‘âq¹0=<Œ‚ª*¤ffJç¨Jã6?xöµ_"à÷…¨F[tÐΙÂK \8ó{é Ú„‚òZ”ï> AÕ–ÖÜŠRÛð°` ‘¶q¨AÙ…·Œüô¯¯–ÐzêÃò¾‹ssíî†-3ŵµˆ3Äèî‚ÐÄÇn†vjö vµ¢ëÚEé ¢˜Ñüîís‡ +v –i‡DÀç“kGKSSØÞÛçõ¢¿­ ÎÑQäUT 199l׊v5Df~©t†(-©a4%âè“ߑΠ"Òc‚I:HS8Ô “Šû)ëöËçKHM‚Á f§§1ÐÞŽô¼<$§¥I'iRÛÅ11Ô#!KC;5V—qþ­ß®û:€j»Õv;¸—IjBF^±tQLén½¤¬÷6>)XB¤ üž(L=néR¹7]ZЏ¸8¸&&¤sHc´´Sc£*ìvüä?ÿðÿþïhw:…‹¨µù}题ÖÛ~EYÇâá÷ykˆÔ‰;5ˆB¨þÄ3ÊzèV»` iÅXO Ó!·¬lý2óKQsàAé UÐÒA¡ñÆ„¯<šˆˆ6Æ55¦¬yÑéN¿r*øÜ /JwiÖÚOK“mé˜sM ‘Ù³³aIIÁPG‡t iDfQæ].,ÌÎJ§„Œ_îÖ€N§±ù\u:úäwpñý×°º¼(BD² v`|ð–t‘¸Ó¯œâN ¢í:þÜ•5´Uα1L£¸¶¦ÄDéÒ€h¼ýÄ ÝéD;ªsþ­ßr A¤"iÙÒ DªÁ¡Ñ¤82•õ‡¯þL°„¢ÉÒü<úÛÚUR‚ä¿}ZM_ÒÇpüÙHg¨Š–†üïGDZ×/UÖ»ö,!’ǡєï> @Q* ¢ÿúuXl6س³¥sT#à÷áìk¿”ÎP =ý„ÿý¢ƒÙbÅ¡“ß’Î ¢0=6(@$ŠC ¢ *(¯QÖŸ}C°„bÁXO~?ò+*¤SH¥´tP(E‡¥.œù½týƒµgk4~í%¹"!jmÉœ$@1Æ=9 ÷ô4Šjka4™¤sÄì9zöÌ<é õÑÐN 8ñõ“N "ŠzŸ¼ý;eš–u××ÕiŒ@ Qdp¨AtkÙu­Y°„b•gfC7n £°É‡tŽˆ«çÏÀ91,¡:Z:S>øÓI'Pˆd–¡ºþé "ºŸwUY﨩¿ëëZ>nŠ@ Qdp¨At“#ýÒ Dðû|îìDbJ ,))Ò9¤Û©AÑcl í—ÏIgÑ:®œ{SY—Õî,! /5ˆþÁÚCÐ&†zKˆ¾Ê9:Š´¼<Ä'$H§DD^i*÷–ÎP-­íÔ8þÜ¡×óÛ"" «+ËÒ DaÃï.ˆÌÊúêG,!º»Õåe wu!«¸¶ÌÌõ¿@ã†{n ãê'Òª¥µ¡ÆÙÓ?G ΠªÜsy¥UÒD´×”uÃ#_,! =5ˆxäye½´à,!º7¿×‹¡ŽÍfäUVJçQ ë¸ú †{nHgÑ&µ6 ¬Í«` Qhp¨A1+=§PYŸó¿Kˆ6o¢¿ž™”ÔÖ"19Y:'¤ÌyâEé ÕÓÚNžùÄâ¥3ªqtѺæ\Êzϱ'KˆBƒC ŠY9Åü”›´mvj ½mmHMOGîŽÒ9!³²´€ÿrJ:Cý4vPè¹×¿Ï+VM—Z¤Dxøy$¥Ø¥3ˆh .œù½²Îß±K°„hë8Ô ˜Rºë~e}í»‚%D! b´§«««(¨ªB‚Ù £É$]E µ½.¾÷*ægÒD´M‰ÖèÚùI±ƒC Š)¿O:A3¸Z[¦1ÚÝ´ü|d•”Àž-´%õ'žE²-M:C´6Ô8úÔ÷`LàÀˆH­:¿øTYßwô¤` Ñæp¨AQoϱǕußÍ/K´%V·Qk™ÏëÅHW<33Èß¹Ssçm\þà5̹¦¥3( οù>R0ŠÅâñÀ3ÿ"ríºÆFÔ56Š\›(Z v¶*kÇM*Çß¡õúoò‡sŠ-®‰ ¸&&0ÞÓƒôü|däçK'Qhm§E7¿Ï‹s¯ÿJ:ƒˆBdfbXYæ‚%DëãPƒ¢ÒÚO‹œ“#‚%Dr¼««ho‡ÏçCau5ÌVõ>¶­¸jJ«÷Igh‹Æ =üø·aJL’Π(ÔÒÔ„–¦&é ¢¨õáéŸ+ëŒÜ"¹¢»àPƒ¢†%Ù¦¬Ï¿õ[Á"uqŽa¸³ö¬,¤¤©ó¼Š¾WÑÓ~E:CS´¶Sã“·‡åÅyé 3GfïÅ'ŠbY…åÒ Dÿ„C е '”u´?6h³ü>&`ÏÉA²J´ Û¥A±cfb_œ?#ADaÒúé{ʺjß1Á¢Û8Ô MË)®PÖÍïþI°„Hý¼««èkm…1!¹ååH²ÙÖÿ¢0KJ±ãÀÃÏKghŽ^§CP§“ÎØ”ƒ}‰IÚ:¸¶² IDAT–ˆˆîÎ=3.@€C Ò¸TG–t‘æLŒ`zhiyyHÏχNð‡ãùY'.¾÷ªØõµJ§ÁŸþõXœŸ“Π)©Þ‡’ª=ÒDF£}ÊúèSß,¡XÇ¡iÎνG”õ+ –i×ÊÒúÛÚ°¼°€¢šä•—#%=]:‹6( HgÝUoûôÞ¸*ADrñÝÛP$ÛùýE‡¤9s®i風áq:Ñ×ÚŠ¥ùy˜,TUÁ’’‘k:ù-˜-ê}"‹šiíP8ððóHJ±KgQ¬®,)ëÊû –P,âPƒ4aÿCÏ)ë‘Þ›‚%DÑift“ƒƒíîFrZ2 Â~Í g~¥Oد•4xûÉÅ÷^Åü¬S:ƒ"lϱÇaÏȕΠ¢ºüáëʺ„k§àPƒTK¯SÖ7>k’ !ŠÁ@>¯ýý0šLÈ,,„!>qƒtý-îÔ Øtõ£·áœ‘Î ")AÞ*IáÇ¡©Vã3/)k{F.„(Æü~ wua~v9eeÈ-/GZn.ôqqëñ”×5  ¬&$ï«tøò\ -©?ñ ’m¼Ï:V¥¦e‰JLD2Öž­³ÿij‚%Í8Ô U±¥g+ë³§!XBD n7oÜÀìä$ vî„#'çË[¶¡«¥ƒÝm!ªŒQ¼ýäò¯cÎ5%ABŠwîáPƒ(ÆÝøü¼²N0% –P´áPƒT…÷Ý©Ïìô4œccè¿~qòÊÊøÃ‰0Þ~BZóÅÇgà{h ÷×I'PˆxÖö_ÿwmPèp¨Aâ +v+ëÏ›Þ,!¢õLbÞíFQM ìÙÙHMO‡N¿±¿J² +pøño…¹0Fhp§Æ¾žBjZ–t ²¦¦a?¡MjºÔ"@aðñ[¿UÖ¹%•‚% xú‰3&˜¤ˆhÜ““pON"5#¶œز²073÷ä$ü>ß]¿nl c,^ZÜ©qåÜ›Ò $ÌãžÆ¥÷_“Î "•I¶e`Ò¤aÜ©A"j>¤¬»[/ –ÑV¹''ÑwínÜ€ÏëE~e%R33¥³b‚‡DDDwrsÍY»=,XBZÅ¡‰¸%@D!ðû1;5…‘®.¤8È,*‚ÉbÑt{Vã×^‚!Þ(XIÒö{öŒ\é Rü»PqßAé "R¡Ñ¾.éÒ 5(bŽ<ñ¢²žé,!¢pð®®bàÆ Ì»\°gg#§´¹ååH²ÙÐôç—áó®J'F -îÔ¸úÑÛpNŽHg ݺŽÎ/>•Î "šíWÖ>ÿ#¹Òž©Aa•`¶`eipùÃ? ×Q$,ÌÎbav ‰‰0°$'Ñ“×Ä榧×ZŸ %""ÚŒ³§®¬Yù˜¬!5ãN «ú¿¦¬ÿ>Ü ¢Ø°²¸ˆÂ²:ô&Œõö‘ŒÂB˜,Œ¼e;´¸S£îð£HË.Î ©9p™ù%ÒD¤Rkÿž+(Û%XBj§;ýÊ©às/¼¸þ+‰6(#¯“ü½„ˆþYRj*’ÓÒŸ ÏÔæff4÷º4KJ ’l6Lô÷K§ETÅ}‡ÐùÅé R‰Ó¯œâN ½¬üÒ D¤Rón7FoÝÂXOfÇÇaHH@QM ò++aNJ’Î#"©kl”N ¢[ô¸¥He8Ô («Ý¯¬[›ß,!"5HÏ)ºçcÙV——áq¹032‚¾ÖV,ÍÏ#5+ UU°eg#.>>‚µÚ£ÅÛOjBF^±t©Ð‘'^D‚Ù²¥¯mij m ©ÞЭve}øño –Zð P ‰Õ•eé"R‘©Ñþ¯œ`¾žéáa€!>¶¬,ä•—Ã=1y·@Á@ L¥¥ÁƒB9ð¦»ùø/§¤ˆH£®œ{SY'¥Ø1?ë¬!)Ü©A[¶·ñIe=ÐyM°„ˆ¢…ÏëÅÔÐÆûú`HH@^Eò+*³c¬v;ôqqÒ‰ª ÅDDD¡¶¼8¯¬wí?.XB’8Ô -ëm¿"@D*tâëÿ¶í÷XY\ÄÌÈÚÛ15<Œ…ÙY˜,ì܉ª*XÄbw³¡‡»öGVÁWÏ\jl¬ª!µ1[¬8tò[ÒQçP´»øÞ«Êº¨’¿Ä>ý„6åøsÿг§!AD1,%# &ÌV+|««Xp»ážš’Ί¨ä´40=2"BDD¤:;jêq«í²tEÀéWNñL ZŸ5Õ{Ðôú˲1Dóf''•ubr2Òóóau8°87‡•ÅEÌ»£ÿTtmíÓ ""Šœµ}<õ•³7(úðöZWÕýÊ:ðË…‘ªÕ|¹E½æâÜÚÛ1Þן׋ÔÌL”ÔÖ"«¸Éii°ÚlˆOHˆhSDhð Ðª}ÇS\!A*—]X†êú¤3ˆ(Ьpâ‚%.Ü©Aw”[²#½7—Þ?-\CDZÐúé{b×ö®¬`vj ³SSÐëõ0%%!Ñj…%5U9{by~¿îéiøVVÄZCA‹gjܸò‘tiÀØ@7ƺ¥3ˆ6ÄlCÝþ´\j†oÎ%CwážWÖ‡ÿ6šþü²\ …wjÐ%ÛÒ¤ˆˆ¶$`qnÓ;ht¨££ÝÝXôx`JJBnI ªªQX[V–tî–hq¨ADmêö7àåÿóSÔíoN¡ Z;ÐÈ.,“ ¡âN RTÝ 7>ûò“´›Ÿ,\CDZ‘]X{fÚ/7I§ÜQ0€/À¼Ë…y×—Ÿ¤âãaNJ‚#'©X^X€we~¯§>¯W¸z¼ý¤rÏaÌÏ:1ÜsC:…4`çÞ#˜sM+»F‰Ô¨åR3^úÿ~Œ–KÍÒ)´öÌ<î ‹jÂ=5¾þ‹ˆˆþÁØ@ƺ¤36ÅçõÂãrÁãrA§ÓÁd±Àlµ"ÉfƒÕá€N§ƒouÞ•,ÌÎbuy~Ÿ¿:ÎÒétÒ›ÒqõéÒ~¸BZà›sáÊûg¤3h‹Ú/ŸSÖ5DÛÅkh;8Ôˆq |Íïþ 0Úß)\CDyÁ`KóóXšŸ‡sl g0 19æ¤$$Ùl0šLˆ7°º´„Õåeèt:,z÷‹ÒA<ó/QùC‘$³Õ ½^¸øxݤ¦Âï÷Ã`0@¯×# binN¹E§×cya~ŸÁ@@~¯wCÊ´Ü\¬®¬`nz:ÿfDDD±#Î`P>Œ°¥çÀ55*\D÷rú•SÜ©+왹pNŒDDa°äñ|åÿž½ýM>.«ANÙéaLL„Ñdúr÷‡Ñˆ8½^94 Á ¼^/‚ôqqƒð­®Á LIIˆóx0éI""¢(·vweIõ|ÞÄ¡†Úq¨#Š*ꔡÑví9öúo^…s’ÑoDÀï‡ÇíÞÐkuÛíaøÛ C§×#Þhtº/*Õé`HH€/Ä·´„ÛŽšzø¼«èïh‘N!1ÄqøñoåQŒDD‘ðyÓ_”uÙîè¾vQ°†î†C(V¼ó>ôÝüpõüÛÂ5DM®~ô—õ_D[ à[YÁÚ‘ÅòÂÂW^“hµbuy9²aÛt«í²ti”ϻʉ[]Z”N »ÐKHj¬¯Cc}tFØèã8³""ŠFZ<(”ˆˆHËÖt}ðÑo–Ð?Šé¡F4Ú}èeÝsý3Á"ŠFù;ªQqß!錘§Å¡FIÕ”Tï“Î KË.@ÝáG¥3ˆˆÐrá=e˜”,XB@Œß~Òt9úîëíëN ¢(6t«]:å@Q-é½qU:4nzlÓcƒÒDDXôÜ>'«îÈI|ú×?ÖwjD£O}WYO–Q$hq§Q´1šL0šÍÒD$lí@£|wƒ`IìâPC£Ì«²¾øî«‚%D ÌyâEé ú; îÔ(¬Ø²ÚýÒvÔÔ£¨RþL4ƒÑˆÌÂB˜,é"R S’uýQÈq¨¡Q{Ž=¡¬WW–Kˆ(¬,-à㿜’Π¿ÑÐÖH輆îÖKÒnµ]4pRj*’ utÀ‘›‹îØ׸_~ÐEÔºæ¬ûŽž,‰-jhHf~©²¾pæ÷‚%DD$Jƒ;5ˆ¢…Õá@bJ Æûú#]]È((@¼É$\Ûš.EßYy¤mƒ·Ÿ–¢ÓéK¢‡’‘[$@D1¨þÄ3H¶¥KgÐZØlé ƒkÞUÖ•{ –D5T¨ëÚEé""ôÝü=×?“Π;ÑàN‘Þ›¸ùùÇÒ…>øÓ…í½ó++173Ó¹©¯ëokCQMM˜ª(Ú54Ôá§?} |¢K´›ŸÝÜŸ-tgj¨ÄÚOfg&KˆˆHí´xP(‘–èt:×Ô`zd‹ss›þú`0ˆövíÚ†:ŠvÍÍ-øñ_Bs3Ÿèí†{n(ëÃ[°DÛ8Ô”lKSÖáü”ˆh3,É64<òué º-5² ËQ]ß(AQ*Å‘ù•Ûw·J§Ó!ÞhDQM FnÝ’dzå÷ øýêè@auõ¶»(¶¸\>œ9s.œ%Wν©¬-É6ÁíáPCPåÞ£Ò DDÿdaÎ…æwÿ$A÷¢ÁÛOƺÐ~¹I:ƒ¢ÔìÌ>;ûƶÞÔ”„¤”d•–¢¯µ«KKÛîòû|éîFÁÎÛ~/"ŠnË‹·QÛpB°D{8Ôˆ°¼Ò*e}ùƒ×KˆˆH«´xP(‘šYív¤çæ"ÉáÀÐÍ›!}oßê*ÆûúWQÒ÷%¢èµöåŠ݂%ÚÀ¡F„%¥Ø¥ˆˆîêàc/ 1)E:ƒÖ£Á™ù%¨9ÀOž(¼ +v£¬vÿ¦¾Æž “Å‚…¹9Œõô„¥kuySCCÈ-+ ËûQô2&˜¤TC{hl ͉ÃUûŽ)뎫Ÿ„ä=‰ˆÂáÓ¿¾‚ÅùYé Z‡ÏÔ˜êEÛŤ3(Ê t^Cwë¥ ¿>³¨¿SCCpŽ…± XY\ÄÌè(rvìëuˆ(º¬ý3moã‚%êÅ¡Æ=45…æÄa÷ÌxHÞ‡ˆˆ€&wjmD¨>PÚˆ¢šÌ»\pONFìšË pML »¤$b×$¢èÑÛ~UYëã ‚%ê¡F˜xøye=Ú×)XBD´¾²Úý¼gSC´¸S#=§»=,A*ª”vzé9…wüµø„×Öbjp ³‘ß™¶äñ`nf™EE¿6…GãþÈ d]S£·¯ùô÷"rM-àx'„ôú8~ÀõKg…kˆˆ6n3ÛµIž íÇÔh¿tEƺ/xhj¹ûäÚ…wïøÏ-©©°ge¡¿­Mt0¸0; ^Œ‚LŠuQh4] Í@v3ξöKe‘WŒÉᾈ7¨wj„Pã3/)ëùY§\E7Þ~B´i¶¬,$¥¤`¨£C;æ].,ÍÏ#=?_:…ˆ4.+?¶ÏêáPc›RÓ²”õÙÓ¿,!"Ú¼G&î?þ´tm‚o=GVî;rR:ƒ¢@SKË=wi¬uìéï#Þ˜³Õгa®ÛÓ‰Õ¥%8rs¥SˆHÃZ›ßWÖkR+8ÔØ¦5õÒ DD[6;3Ïξ!A›¡Ñ]3ãÃøâã3Òc>zã×È((€s\·ÏNOÃïõž-BDQ RÁ¡Æ”×(ë+çÞ,!"¢X£ÕD‘¦‹ƒ%%9eeîêÂêÒ’tÒ]¹'' aËÌ”N!"[ûŠ£O~G°$r8ÔØ“9I:ˆhÛŽ>ù]Mfé Š¶ôlìm|R:ƒb„=+ Žœ$Ùl˜Cýñ¯I'­Ë5>^ÔŒ é"ŠßMY[SÓK‹C Úµÿ¸²îºÖ,XBDçßúo¬.«÷“Kº3­îÔpMáó¦·¤3(d—–””ïÊ &úû±0çBó»’ÎÚçØ ññHN‹Þ>ˆ(rV—•uÕýGK‹C šã㶈ˆHžV‡Dá–˜ˆâÚZ¸'&03: ÷ä¤tÒ–LŒ Ál†Õn—N!¢(riÍ®â÷ –„‡÷pð±o*ëñÁ[‚%DD¡³sïä–ì”Π­ÚÂA¡uaŠÙ¸Gî×À-¤M)ééHËÍE[–æç±²¸øO¯É-Ù‰{ÔmÞÔÐÌV+’RS¥Sˆ(Eøïi}œ!¢× 7ÝéWNŸ{áEéÕ0Äáó®“’±8?'\DDDt[|B²Š‹1ÔÑ!B¤ £™……éî–N ¹Ì¢"Ì»\X˜•N!¢(uÿñ§5ý$¼Ó¯œâNtøño+k4ˆˆˆˆÔ+!1ypŽI§„ÅD?’˜­Vé"ŠR]×.*kC¼Q°dë8Ô`ÏÌSÖM~Y.D€¶$Qd8²òpß‘“Ò´MZ=SÚêÀþ‡ž“Π(o4"5#i¹¹loÇÒüü†¿¶º¾Ù…åa¬ ­±Þ^Ø23a²X¤Sˆ( ÍÎL(ëµðkItÝL³EEµpN Kgˆhjj‘N ¢™ÆÌxlþYM´:Ôð¸gpéýÓÒ¤u:²‹‹¡×ë±´°°¥[NÚ/7…¾+ÌFoÝBnY¦GFîx^Q(¬ý€?3¿C=r1›³;5ÖžøzõüÁ""¢MØÂA¡DÑ 19%55ÐéõpNLDí-'w3ÒÝôü|M&é"йEÒ ³Ch;ñ•ˆè^Ž?û¯ÐÇÅIgPhu§†ÅšŠ†G¿!A•–—‡d‡½­­½u K϶ßóÐÉoÂlIA]ä wv"«¸£6ï{'"íh»ø¡²®®@°d}15ÔØ}ðaeÝsý3Á" ·ÆýL„x‡ÆZ ¦Dyò;a{ÿpê¿~…ÕÕÐétÒ)DC>ýëí]—Éö Á’Û¢r¨‘`JTÖ—?ü³` ‘¬¶‹jæäjÚ­îÔXZðà™ßKgÊ%§¥Áž™‰œ²2L Á9>Öë­,/âã·~Ök„S[Šjj¤3ˆ(†xW—•uå}Kn‹Ê¡FýCÏ*ë•¥Á""¢ÐÒêPƒè^ââ㑳cRèââ0xãV––¤³T/ b ½E»vI§‘ŠDêl¹µJª÷Fäšw5CôœBe­å‰;Q(dæ— æÀ é "…–·ùSx¥¤¥!¯¬ α1L afd$â ™y%¨iÐæŸ™¿C(¬®–N!"•9[Nðó–¨jäWJ'©ÆÄP/Ú.~ Aa ÕZßæO¡g4™_Y CBnÜÀòÂVEZ&†{ÑÖ¬Ý?3ý>Fº»Q°s§t ŨÞŸ+ëúŸ‰èµ5=Ô(ݵOY_»ð®` Q„hô P¢µR32³cœãã";3¢‘ouã}}È«¨N!¢×ñÅ'Ê:>Áöëiz¨ð¤ˆˆTE¯×ãøs?”Π0ÒêNxcŽ=ý}é –`6#¿¢ú¸8ô_¿Ž·[:é+*ê"¿L»çS¬./cjh¹eeÒ)DÃæœSÊúà#ßûõt§_9|î…×¥JÜwä$¾øøŒt‘«Ã“ÙŒ©áaé¢MqäæÂd±`rpÞååõ¿€¶Ìd±ÀžÑ[·¤SˆˆÙ…åè é{ž~å”övj v·J'‰ÑêN Šm)ii°$'c¤«‹X^X€kbÙ%%Ò)DD {fNXÞWCµÛUgÆùÉÑÔ~iÙÒf:ˆ0¾eú8Ž?ûé Š°¸øxd•”À””„áÎNéœ «?ñ,’miÒÛ²äñ`nf™EEÒ)DD€öËMʺæÀƒ!{_CÈÞ)ÄÌ–d,-Ì>ýë„kˆHk’lÔ7Ôárs æ]>霈hùäéŠðûpöµ_JgPÙ³²`µÛ154„EG:gS.ðštBH,ÌÎB§Ó!£°“Ò9DDŠÉ‘þ½—jwjì9vRY{WWKˆH‹êêðú2êê¤SˆBŠ·ŸÚYRRPR[ ¸qCsh3ïvcÉãAz~¾t QDÕ56J'Ð=L õ(ëžù—m½—ª†™ù¥ÊúÂîÎ ¢­»ÜÜ‚ÿùã—ðYs‹tJØå–TbçÞ£Ò)Ý©¡Óéðàó?’Π0Š7‘]R‚$› #ÝÝpŽK'm‹>.ÇŸýWéŒð8X]Yá`ƒbJKS“tmÐù·~«¬SÓ²6ýõj4ÖÕ¡±îΟœfäE6†ˆ¢Ö¼Ë‡³g®À·žŒôvàæçç¥3(B´z¦F0ć¯þL:ƒÂ$=?Y¥¥pOMa¢¿+KKÒIÛðûqöµ_Hg„Ììä$ôz=²KK×1Qù}^e½£¦~Ó_/¾S£l÷eÝvñCÁ"""õãí'¤&ÉJvï†ÏëÅÐÍ›Xâ­&ª610€åùyز6ÿI(Q$\9÷¦²^oÀgøòˆÐˆ5šZZÐÔr{;øêÒb¤ˆˆ¢B|‚ ÇžúžtEšFo?€Ÿû!tzñÏS(ÌV+ò++a4›ÑßÖ—Æo5¹{f.ö}\:#d\ÐétHÍÈN!"º'Ÿwõž¿žš™ @h§ÆÞÆ'•õ@W«D‘æyW–ñÑ›¿‘ΠÓòNOÿÁ@@:ƒ¶Ál± ·¬ ©ééïëÃôð0QþßÔ91‚«çß–Î)çØ ññHNÓöck‰(ºõwÜÞ ±ÿ¡ç¾òkz½V»€Ð#]{Û¯H\–ˆˆHû4¼Sƒ´+Î`€=;‰ÉÉpŽŽÂãrI'Ñ6MŒ =?V»§S:‡ˆèžn|Ö¤¬̉H²Ûàž˜ÁÇŸý²vMEê²DDQißOméthÒ>­ |ùȶ8C¼tm‚Éb=;yX^XÀ@{{Ì4Jª÷¢¤j¯tFHM Álµ")5U:%d÷óQîDÑÈãžQÖ;÷B²Ã÷ä$@wú•SÁç^x1,¶$Û°0÷å_|ú8þè Q8¥çåayi ž™™õ_L´E©°ggøòV…¿ãHÑ)³¨ó.fg¥SˆˆÖU²{7z¯]œ~åTxwjÔ6œPÖh…o?¡0JIOGqM ôqqïíEïµkhÄ€‰þ~$;0[­Ò)DuÒ [fËÌ„ëo·ü]ȇ9EåʺùÝ?…ú퉈bZayíW…M±GË…}ê{0&˜¤3èlYY(®©!>ýíípŽa‘g½£=LJ=#W:#äÆz{aËÌ„Éb‘N!¢0kij’NØ2[VÖ?=q+äCÔôœP¿%ýÍ@W+º¯]”Î IÞ©qþÍß`ueY:ƒÖHv8PXUSb"†;;13:Ê'Ô¬ãêGoÃ99"£·nÁ‘›‹³Y:EUêŽ4J'î¼KÑP£rÏae½öTR""" --ïÔ õHv8PX] £ÙŒáînŒõö»º*E*0ÒÕ…Œ‚Ä›¸«êïZ>n’N "Üy—¢¡Æü,EDNæ¤dzì›Ò¤££Q:cKŽ<ñ"ÌÜÚ.-³°Iv;†;;1=< ¿×+¤IÇŸýWèãâ¤3Âb¨³ÙÅÅšý³†ˆ¢ÏÝviÛjÔ?øŒ²î¹±Õ·!"¢ XšŸ'{åm IDATÃ…¿þA:ƒT  "àÓæáÛÿåV–¤3bVjFŠkjàóû1ÞÓ¿F©ÅÙ×~€ß/6ƒ7o"·¬ qƒt ‘-¨mî¶KØÆP£ã‹O¶DDD-5ÖEôzA¿>þ0Jg0 £°%»wC‡þövÌ #Às3hÚÛ‘¿s'ôú°>0‘Hµ´| f4¹×. `“C_ÿ7e=çœÚzm؇ŸGRŠ]:ƒîâBSKd/¨áƒB>öM$&%KgÄ„ø„¤ç磠ª ¿½×®Á96ÆC@C,Ù–†úÏJg„U[Šjj¤3ˆ(†Ýk—èN¿r*øÜ /Þõɶt̹8À ""Rƒœ;àÃòoã –˜’‚d»ñ pONÂãä¹g´}:E55èkm•N!¢“UR¾|ìôœ~åÔú;5*÷^ï%DD$ 1·=‘zYívVUÁž•…¹™ utp A! 1ÐÞŽ¢]»¤Sˆ(†èôz˜-–»4þîŽC¼Ò*e}ùƒ×C[FDDRZ½ÅU{îúëM‘¾íTAËtå­T¡g0À–•…¢]»`²X09<ŒáÎN,ÎÍI§Åœ‚²](¯;(V¿C(¬ªZÿÅDD!`ÏÊ‚ó·üÝ3æ7DDòzÚ¯H'iøL‹ï½*L ¹¹ˆOHÀìä$oÞŒê§phÁ`÷u鄈ð{½½u ù;wbèæMé"Šbq’l6 ´·¯ûZe§FÕýÇ”Øq•O6!""R#-ïÔ ­ÓëõHv8WQ{v6–ææÐßÖ×ÄQÞÕULôõ!¯¢B:…ˆÂÌ`ª«í¨®¶#ÒOw¶ggÃ96¶¡×*C ÷ÔúÛ:ˆˆ(ü¬©iØÿPtŸ¦O[§ Õ‘Fý‰g‘lK“ÎÐŒ³iyy(Ù½E550šÍ˜Àè­[ÚŽK‘WÛpy%Òa·º¼Œ©¡!ä–•I§QUTØñ“Ÿü'~ò“ÿDEEäîæ00'%møl(eÞ2Úß¶(""Ú8{—ÞM:ƒÔJ÷Ÿ\þ€¿¯×£Óé`µÛau8ž™LbÞå.£hmþ@:á®êÑÒÔ²÷[Y\„sl Ù¥¥ëé Ùûmf—p—35ˆˆˆHxûIt2'%Áêp )5s33˜ÂÊÒ’tE‘P4þni~:½YÅÅïë ùû‘¬ÎN'þã?þ]YG‚Ñd‚ÑdÂäÀÀ†¿fÝGºQä~üÛ0%&Igšix§Æ¾žBjZ–t†jÄ'$À–•…‚ª*¤ffbÉãAïµk˜æ@CãŽ>ùM‰Ò±87Ӊ̢"é" 1Ÿhow¢½Ý Ÿ/2×Üì. €;5ˆˆTå“·'@*§åWν)  ú¸8Ø23‘d³anz£ÝÝðy½ÒYBçßú­tBD-ÌÎB§×#£ “ƒƒÒ9D¤Q‰II0ÄÇoúÑäjE@’͆¤ÔT˜,,¸Ý½u Þ•é,¢˜w¹ ×ë‘–—‡éáaé"Ò ô‚‚-„ÍÛOˆˆT ¼î ÊvIghy§ÆžcÞ‘+Q–ÔTä–•¡d÷n$Z­˜›™Aÿõë˜æ@#Ê™-É8tò›Ò573ïÊ 99Ò)D¤1–”,/.nø‰'kq§‘ tµ|*@¡å¡ÆÕÞ–N;KJ ô“’`IMłۅÙYŒüÿìÝw|uþÇñ×öl6½÷B(¡ŠRD‰X@áìØ{ïíÔŸýÔ;ÖóTî<° ¢b 6 ¤‡Jz¯»Ù2¿?˜v3Ùäó|<æ¡lv¿óÞÉÎfæ3ßïw²²ÔŽ&ºXSC-ËøLí]®¦¬Œ ˆˆN‹Bô^!ÑÑžpXŠB!„7ñâ‰B{"­N‡%0K` ¾¸\.¬uuÔUWSr3· Ñ“T—–EPd$Õ%%jÇBts¡¡4Õ×wº£ ?B‡Ç0"mªÚ1„ÑÞZÒ>~2aÑ jÇp “ÙL\j* âc±P[QÁžM›ÈÙ¼™âœª«ÕŽ(º‰èÄþ ¦vŒ.WY\ŒN§#0<\í(Bˆn.$&†ÊÂÂN¿^zj!„ŠªÊ Y—Þù/qÑ yqO˪ᘘýüð À7 ´Z¬µµÈ„ˆ¢E¹;)ÊÝ©v UTG@h(µjÇBtCÁ‘‘Ô”•ár¹:݆5„B/áÍóix#½Ñˆ¯¿?þ!!˜Ìfšh¬­¥8;[&ù¢ƒÊ Ç_Q:5 ¢çÒh4EF’‘qLíHQC!T’vÞµ,ûþöfµ£oáŽ4†;ƒâÜ]”îíÜD`ž¤Õj1ûûãŒËéÄÇbA£ÑÐXWG}UE{öàr:ÕŽ)¼ØÀ§P[UÊÞ=™jGéreùùD&&âr¹dh–â€Ð˜*ŽaØÉ~RÔB•¤3[íÂËxó|+~Q;Â!Ìþþ˜ýü0ûùaðñ¡©®½^O}CÕ¥¥ÒC¸ÕöuKÔŽ ª’Ü\¢’“Q\.kkÕŽ#„P™ÞhÄ7 €òíÛ½-7äBˆ)môpÒÿبr!ZxyO µiu:|,|,|ÐëõT—•Q¾w/¶ÆFµãQZÚpÒÓå»Hx·âìl¢SRP\.šêëÕŽ#„PQDb"ÕeeniKŠBÑÅJ£º¬ÂœÞ9qœè>ûn‡ª×£ÓéLÔér8ö=¦×ã´Ûq:(ŠBCu5V+N»Gs3 à´Ûåê«­(+Ì¥¬0WíÝFöæÍ$JöæÍjGB¸‘É׃ÉD}^žGÚ—¢†Bá%ôF#ƒ°¸¸}( `0qº\¸4°¯G‡Fƒ¯/ÍV+fߢբ7@«— V‹V«E£Õèâr:,ûV¡ÐT_ÃfÃætât8Ð 4[­8š›eŽ !„Û(.yÛ·“8x0¹[·ªGˆ^-môpÒÿØxÌm…ÆÄPQXxÌí´EŠBáAÃÆIqîNJ÷æ¨Eôv›ºêjìVë¾44€V«Ý×[ÂáØ×“CQô¼°ÖÕ¡(ʾ¥¥Ç…ËéÜ׋ÂåÂår¡¸\ì{™g{Sô?n,Ö¦zòvÊUX!Öo؉4Û¬äî¡N‡ƒ½YY$ èö;$!ºž_PÐcO‘¢†BxPÆ ¹}«pŸf«• Gí ;7­T;‚ÝRVÆjµ#t+ŽæfŠöì!>5•üÌLµãÑ+¹£‡€Ád¢¹©‰æýd<@ë±–…B!„¢ì6UEE$ªv!Ä1 §¼ À£ë¢†Bx@tb?>UíBt+}‡Ž&)u¸Ú1„è¶FM<—ÀÐHµctõ55íÙCLß¾jGBtBHt4Õ¥¥¿Ó™`â7xt%BÑÛåf±õßÕŽ!D·²kóädº§;«=ÑšEßRSQ¢vŒnÅÚÐ@UI Ñ}ú¨EqôF#~AAT—–z¦}ƒ‘´ó®ZŠ‹¾zß#+êm†§¥1<-MíB!„BôMuuÔ”—•œ¬v!D…ÅÅyt؉ÃÞLú7³~"„n¥Ó8õüëÔŽ!D·ÔgÐ ôž²ü|®ã_¿:âÏeø‰Bt’Éláä©2'‘Ûg Gœ¬v !¼Òðñ“ ‹NP;F·Vš—‡%0K` ÚQ„èü‚ƒqÚí4Õ׫š£Ý¢†V§gâ×wE!„ð*¶¦–~7GíBx½{¶³}ÝRµcá•6.[Hy‘çî*ÐS”ääŠÙß_í(Bôxžî¥qüÉgßîóÚ-j¸œ}õ?·„B!D÷Ö‘[Ë !DwV´gÁ‘‘øX,jG¢Ç ‹¥¢°EQ<¶Ž K¤¢¸ý¢‰ ?BˆN}úù‡«C·Û˜žî±¶c’ú3hTšÇÚ¢7H;ïZô£Ú1º½Â]»‰Áäë«v!z“ÙŒÉb¡¶¼\í(ÀQ5dŠhKšÜ–QôBüú5µUejÇ«æìdÛštµcáÕÒ¿™ÃÞ¬v ¯°7+‹ðøxŒ>>jG¢G‰HH ¹©Écí•FtRÿ?¿ÃE †"Ú’.w1B!„ÝPÁŽD%'£7Jï!ÜÁ`2VKM™ç.îm]“NQÎÎ?_†Ÿ!ÄQHx<)CF©C¯ŸÂÐ1§©C¯ç˜3/R;†×ÈÛ¾Ø~ýÐéõjGÂëYi¨®¦ÙjU;ÊG]Ôa(BˆÞ,{ûvoY£v !¼RIþn6¯úMíBx½úšJVýü¥Ú1¼Jî֭ĈV+×t…è,^O`x8•EEi¿ß°IpÜQ¿î¨÷j†"„B!„ð69›7“8dFí(Bx¥ˆÄDJss=Ö~VÆjrwl:ê×I©R!:ÀÌØIÓÔŽ!„W Mâ¸qgªCˆ#.e©'ŒW;†WÉÙ¼™¤¡CÕŽ!„×ñ ÁÑÜLS}½ÚQþ¢ÓE †"„èMj«XùÓjÇ«•íÍaÓŠŸÕŽ!DQ°{™ë—©ë(ŠBîÖ­$ ¢v!¼‡FCx\eùùi>qÀ0ú Óé×wº¨!ÃP„B!„ÞÆåt’Ÿ™IâàÁjG=ÜIiÃÕŽà ”æåy¬ýÜde¬êôëeø‰B´cÜäKðõT;†^/4*žãO>KíBô8CNœHTB_µcx§ÃAaVñªEô`ËÓ7ªá˜ù Õj©¯®V;J›Ž¹¨!ÃP„=ÝŠ…si¬«Q;†^¯¢8Ÿ KT;†=ΖՋ(ÎÛ¥v ¯con¦$;›¸ÔŽ"D·d0™<ÚK#:±?ƒG§s;Ç\Ôa(Bѽ¥õ®B­´å³/„8²f«•²ü|bûõS;ŠÝŠÅBTŸ>”åçãr:=²Ž˜äþìÜØùa'ûÉð!„hCgï•Ýݤ÷€®¢gŽˆaÄ„©]¶¾ôÕòÙ½ËIg_†Ùâ¯v ¯ckl¤¢¨ˆ˜”µ£ÑmD$&R´k 5žë­¼.ý;ìÍÖcnÇmE NÏ©2 Eуtö^ÙBˆÖU•²nñwjÇ¢ÇZþç45Ô©Ã+Yëë©.-%ªOµ£¡ºÐ˜jÊÊpØíjGé·5œN¿Ë0!„B!„j¬«£®¢‚Ȥ$µ£¡‹“¯/5eei?,:áã'»µM~"„‡ `ÔÄóÔŽ!Dɨ‰çªCˆÍd¶pòÔ+ÔŽáµjjh¨©!"!Aí(B¨""1‘ÒÜ\µ_^”ÇÆe ÝÚ¦Û‹2 Eˆž£·N²WSQÊšEߨCˆ§¦¢„5‹¾U;†=š­©¥ßÍQ;†W«¯ª¢©¡ð¸8µ£Ñ¥¼mØÉ~n/jÈ0!z™dO!„½Q]EÍV+¡±±jG¢K˜|}ñ±X<6ìdظ3ˆˆMöHÛ2üD!rÊß®ÂècV;†=’P'žqÚ1„è"ãS:æ4µcxµšòrœv;!ÑÑjGÂã")ñà°“Œ¿Pº7Û#m{¬¨!ÃP„èZZ=ôBŸÁ!hõj§ñ^K|D³µIíBôHuÕå¬þå+µcÑ+”äïfóªßÔŽáõªKKQ…àÈHµ£á1¡11Ô”—ãhnV;J§x¬¨!ÃP„èZIBxú±Wxú±WH¢v!„Bˆ¡ª¸­NG`x¸ÚQ„p;“¯/~ÁÁh4··­7I;ïZ··{8?ÑhµD'öóôj„☠q2±}ªCž–Æð´4U3X‚;išª„èmñ}«ÃëUb0  S;Šn¥Ñj±ÛlT—–º½m‡½™ôof»½ÝÃy¼“º†}·p+ÊÍòôª„èÕrvTòéøqt¶¯[ªvá'¥ gyºLxÛ]4ÔV±ò§/ÔŽ!D¯²cÃrµ#ôå{÷Hu•r¬%z†ÈÄD vìP;Æ1ñxQÃår‘¹~™§W#D¯çrÀž­òVmrݽÈïâOÓÓÕŽ „^¯,?ŸÈÄD\. ÕÕjÇ☄ÇÅQY\ŒÓápk»‰†a4ù’•±Ê­í¶¥Kï~2àøqÄ÷Ò•«Bˆ# Šãø“Ïv[{r-DÛÌ–N:ûRµcÑ+xÆ…ø…ª£G(ÉÍÅ?8߀µ£Ñi¾è ê**ÜÞv.+h@ôÔ8ØŽ +ºruBÑ®Šâ*Š ÔŽ!D¯ÐÔPËò>S;†½Òê_æ©¡G)ÎÎ&:%Å墩¾^í8¢‹¤¥ '½‡\ÀŠJJbÏæÍjÇp‹.í©!„B!„=AÑîÝGEác±¨Et‘žRЈLJ¢$7Å­íŽ|1ÿ ·¶Ùª5 &&œsµ«Bˆ&^pZNíBô&³/'O½RíBôZ:žS/¸^í=Já®]„ÅÆb4›ÕŽ"D‡ø£¸\4ÔÔ¸½í• ?§¡®ëçšÑÌ›;G¹ð’+º|ÅB!„BÑħ¦RœÝfS;ŠmÒjµ$ LNv0oî~"„B!„Ç"?3“è”ôƒÚQ„hS\j*•……nm304’QÏuk›GKõ¢Fê ã‰K¤v !D/2äĉD%ôU;†½ŽÁèÄseø©j ŒãøSÜwç/ÑBQˆíß¾KïÅ D‡GFÒT[K­›ïvRSQšEߺµÍ£¥ú—¹~™Ú„½Ì–Õ‹ÔŽ D¯do¶²øÛÕŽ!D¯WQR@E‰ÜùËÝò¶o'qð`ÐhÔŽ"Ä! &~ÁÁägfªÅ#Tï©!„B!„=AîÖ­$¤¦¢ÑÊi–è>¢ûô¡hÏ·¶9hÔb’¸µÍÎê6{›Ìˆ.„ð´Èø> sºÚ1„èµtz=§ž/w^¢»H<’äA'¨£ÇÉÞ¼™ä¡CÕŽ!aqqT—–âhnvk»ÛÖ,¦0g‡[Ûì,¹û‰B!„B¸‘F«%ið`²{Ð]&„÷ñ  4”âìlµ£xŒÜýÄ ¥¥ W;‚B!„â—ëÏ96„PF£!2)ÉíÓ§ÝìÖöÜ¡[5œ@Lr÷ŸÓݤ§oT;‚^G£ÕrÚ…7ªCˆ^O£ÑpÚE7©Cq˜¦£vŒÇép°7+‹„ÕŽ"z¡¨>}(vó<¿~1Óím+~"„B!„b0™ˆJNî±wž8Vz ˆ `ÇÞJŠÊz€Àðpô……jGi×IiÃY~ îeø‰B!„BxÝf£47—ØþýÕŽÒ- ˆ á±é¯ðØôW7Dç™ýüŽŒtkA#69•#Oq[{;–‚Æ~ݺ¨a¶øsÒÙ—©CáņŸLXt‚Ú1„-N»èF4šn}ø!D¯uÚ…7Ê­H=ÄÖÔDÅÞ½Äôí«vÑÃE$&²wçN·¶¹7;“ík—¸µMw’á'B!„BÑÌþþ…‡S乺³´´ámΠ؆Ÿ¤8œôÕžŸ1L8çjµc!Ú0áÜ«1}ÔŽ!D¯qRÚðN½.¡ÿPú7ÖÍiÄájÊËqÚí„DG«Ex!KP:ŽÚŠ ·µÙP[ÅÊŸ¾p[{]A†Ÿ!„B!„Š‚£¢@Q¨*)Q;Šð:½ž¸ÈݺUí(ª’á'B!„B¡²ªâb´:áájG^">5Õ­ó±ô?n, ý‡º­½®äÕE¡cN'2¾Ú1„›h4‚£¢Šèþ· ÝÓÈSÏ!(,JíBˆ#8yê˜Ìµc!:hظ3ˆˆMV;F¯PQXˆÁd" 4Tí(¢› §¦´”ÆÚZ·µ¹sÓJòvnv[{]I†ŸˆnÃ`4b ÂÚЀµ¡Aí8B!„Bt9K` ŽæflMMjGÝÑǃ ÕÕjGédø‰è6´:¾T—–¢Õjñ Àh6«K!„Bˆ.ÕPSƒÁdÂè#“*‹Ciu:Ìþþn-h$¥G«Ó¹­=5ôˆ¢†7ÞvF*0,Œšòrëêp:øøúâ¬r2Ñš¤¸î5Ä#(4’Ш8µc!:ळ/Ål P;†â(rÎUMrÁ©«ÔWWc4›1˜LjG݈Hu••nm3's#.§Ó­mv5~"TF}u5N‡ãÇ &zƒ£ÙLmEŠË¥RB!„B!ºžHÖúzìÍÍjG*³a·Zi¶ZÕŽÒ­Èð¡:¿àà=3g·Ù°64PWYI`XzƒA…„B!„B¨£®²³¿¿÷r&__—Ëm K@pºáF+j w&±IjÇà€ÝfÃn³µùEQp9T—–â€IæÙ1M$ô¢v !ÄQ;i–Z(„7òõ dÜY—¨£×©­¨À70^¯v¡½Á€ÉlvëÔ§¬À IDATNj«(Éßã¶öÔ&ÃO„*||}ÑêõG½sZQ\.ëê<”L!„Bˆî'0<œúÊJœ^>ÿ8:Á‘‘T—–¢(ŠÚQº%~"Tá€ÞdêTµ±Iw·¼ø÷Œ÷@2•èÂuݳÜó·$¤c¡çiƒFséƒO05Á¨jŽ„¤#O¶Ú]r !„Âý4~Ç1íÿ¦siÿŽOZSV†Hˆ×ß©Bt\PDMõõn+hD'öÃׯçMÖÝc‹þAaœxÆjLJ1øøM}UU§^¯õÂ)gL Á`#("­¶„5!œòÚÖ|ÿ8©&@ëG¨³ß?àÐOÉØ«ïàœá!Ýj‡ÔøÄ2,m2CBTøÃiÉÿóÓ³ãñk#K\Ê Œ>G?I–Æ•7_Å ¡|_‡e물œâ#þü˜s ÑMxúø‡©Cq b’0hÔµcx‡6Žÿ4#˜vãuœÞÆ%­Ã#[T—•Ц; ·0ûùaklÄÚÐà¶6‹r³h¬wß0–î¢Çî uÕå¬þå+µcˆƒh4âú÷'ÇŽ#–ɯî cÓ7\ÑÏŒæ°lBˆ£³úׯ¨«*W;†âæì`ÛšÅjÇð>þ;ì8ò Õ¥¥…‡£ÑhÜTt'Ftz=MõõjGñ 2ÛŒè2ñ’¿}ûŸ£ ?Ÿ>z‘qE_òö#ß‘U­%$eƒµ46|ÐÙ2¨p0:½¾ÓṲ¸(r Ž|Õ¼S”:6¾ú7L§%cK:®yÜ4Zþ©ZÙJÉ©OÏKgdóæåWòIV ‡fB!„p»Ã#S]ZJPDU%%]KxžV§Ã×ߟê²2·´§7‰MN%wg†[ÚëŽzlOƒ ?™°èµcôj1))”æåá°ùÚ¶OŸÉ ÷©àÛGïá?_~Kú¯_óÕ{òÏwÖÒxø“Í#ylE?<2_­KPèâ¹ø›*Öýo*Aú8&Oÿ‰ïV’‘UEFV%K¿ÿ€ëFÿùÁ×ø¢ý³‘‘UÅú¿ðÔE©˜[-|û3æ<2?ýg7@¿S™±©ŠÏ¯J`_G-áü̦Ìo™baÄKÙdüx •û<¸†õ-½PfM :ðxøó˶*2²*þšóX8íÁ/ùncYå,™ÿ>W ji[Gð˜xã§Ü}Ûhgó_>›+Žä²yE-ÛoWŧª šÈƒýÁo›*ZÚÎç»÷Ÿà䈖öƒ¹û÷*~{℃ gYu ‹.l7ýk9K³ªÈÈÊeþ̇þgNCÌÙÜóþ:VeU‘±ssŸ»€ÈŽ„ÔErêS yýÜ2Þ¾òbÞß\ÒJ¶vß &bÏ|’·~ÌiyN K~û…ç.M9Pëj7§Æ‡„©Ó™¹h/YUlX•Î ×&pÿïJ×òy_½•ñÛ§3¸òšé¼õÝNÖeU‘‘±žß9ž Ñ"T4j⹆vh/BtsƒFžBLrªÚ1·hýøÏÌø—Ö±¦ÕãßC#˲igóg½Ìí§÷;èx±ýã Ñý„„P[Yé¶ööæ]Ѐ–ž>‹[Çêt7—-T;B¯O]UÖtŸj.ÝBqÒ´IÄmù†ë&ÅiÊä×Õµ\2út"š—³GãC`Â$&ô…¬é©ÕÒÜhÂw>ËÿÝ@£o_N½ãYî›é kâ,«ÑzÆÛ|ðâ86¼z3×®¨&vê3<õü—4íÍ‹/£Ô³ãç 8ÏÏà`™ÅNL ì ¡'ÄòQµXè›6ÍŽ§Ù\£ÝJìÂ/çþ/òpࢡàÏ»¸ØvÎeÆÌe”iH»ãî›ùJKζ·FoÂd2ñç9ªÝAM#ýž×ÎËãý'þÆ¢¢PÆßõ¾?ƒÜ‰7²Ä>†¿ÿë1Ž_ýw>¼’*S, >[¨sí¯xVñÓÓøÏ+¸š(-vé××q¾IŒÓ—Š÷næ©UUbÓ¸úÑûyý•]œuݧ”¶ú¢V²ø ææÙó¸Ñ5›é×ÞÃ㮞þOÞœ‘ÏÔëæPlÁ]ÍáZóBÞyðQ6Õ†0ì¼¹½½|†XNöm^šZÌ[—_Ä3êhó·ÐÞ{qj šð*½} žæ¡6QkÊeÏ=Ãøaaè?ÛÝÜ^N-Á§¾ÉG¯GÉgp÷9XFÞÊCüÀ;Ú±\ûß,šu-Ÿ÷Ìrÿ{ki Í•O?ÆC'd1ÿåG¸/³ðÓŸà‰{fqïªãxjí_Ê„Bt‰5‹¾U;‚ÂM¶­]¢vÏÒµw,©iÿïsKS9þk™«qïÏÏóÖï¹8ÂOáªÇ>~èX–[_´³çâGY¸»¬ýã®Üv¢Sü‚ƒi¨­Å%w¸9*z€è>}ÈÙºÅåjïùB• ˆœu¬6:rfòÐãCxýéÿñÃY°ìËÿñéG³<»ž¿~:Øþí§NeBÜódî¶bÃS.ŸoªÂE õY¿³xÉzl,bMA"§|}“ú™Y¶1Š©wŸ‡öû‹xôÝߨS`}æ½ô;3)çâõŒÍèýÌì«(¸¬õÔlü–Lgâ¾*®#läD¢Ú!gÑÇü•~œr‚ys—Qæ¤Õ¢†µ$‹™»8Ph™û¢vËþ¶°®(…´¯/gR?3Ë7jð³ØÃEsC-Ö–—¦<¸’•¶²’‚}ÿцžÉm—D²æ‘ÓykA .`ÛãÑœ¶èI.æÇ’Œ"ý rË"VoÈÄÆ6·4±¯‡D3•Ù[Ù™ime%ÇÊÊÞ• Yº¼XB–å4¾ð"û}Ji«Seü5‹ß‰çª;yëŒGø*Ǭ#wú™,zíjF…|Êïïá’„½|táõ¼“±¯Ñ5yñL=ëÆ#&ë{Û<^²”óÙ¹ Ñ‘÷RÏY÷^NØÖG˜òлä;c1ÇU?ÃqûßǨvrê8ûÞi®»—KŸú€"'°b5Åáë˜uëý ûä6Ö¶| êw¥³tÅzl¬¢(ábÆß¶ƒ>ÇŠ`c3ãΛÇȱ ÖfÊÁBÑmKnˆhÿïsK‡ÿišÈþõK~[^,eOÀ™|ÿPËñCMdzŒkdeÃqíoˆîÍìç‡ÓnÇn³¹¥½àðhÐh¨*-tK{Ý™  +‹¸~ýÔÎâq!Œ:í<µcô~AAÍf*‹Š:þ"¥‰ÝsoâÜã㺧¾¡ö¸yûç|xÿx‚þ2C¡nýl–Õ÷å¬31`$bÔIÿÀšj|,½3†½d ¥ ŸdF&ß”/Y¾s_wÀŒ-é\ÁñÁø&ÞÎ'ë²Y¶6›eksøò®J³0ˇag¬ aø™ ìü÷ lô;…“LbÒ^À¢ß²é„Ñ^²•2ˆ02õ–¯mɱ.ƒ'GX<¯ðã¸þÒÉ\s`¹€WþyÒoŒK²ÞÀ¨—3ÙØÒåqâç陈H Úº%¼û¿Í$Ü¿‚ïæ¼ÁõgÂ_•¡ vªvçѬ!Ô·õ4øÅÜÅÕHø°!ø1û)=Ð-tñë'£Ó„h&bH*fkË÷Ý„¢U!ÓÃ¥Óÿɤ¸£Ñã°÷âÓ—1}!÷×ßi½£‹±ýœæÆôÜÅ+(?P¸¯gÇo›°ÏÐÈÖ:•6S•[æH‚}ZzïØË(¨sˆ_ï{(º¥¦£v !„›|1¾þAí?±8äX²SŸÛâ jwþ…ÚÊnÑP߯ø~G:ÞÝ™ÁdBg0¸mbP­N‡¢(½¢ -=5ìV+U¥¥D$$Pš—§v&©©,eÍoߨ£W0™ÍFD°wçÎN½ÞՔǺ¯žcÝ×oòÑí?òé½ÿæú…£xó°QRJí|þK%ÿ=ÿ|fˆ3’©Zü-› +Є¦`8ìÌMqÚq¢mé}¡ ”|zw}xh—µ5Ûyìª øi\4ægcwØYúãnî¿ìúEüÊäÔb~yæj&<À¥ã™o?Äòùy ðíÔ{?4§‹ÚepÃ5Á-CL¬”ÔS¡1×eÿÙë † ;Ä·üS£AC¿=xÿÚr𠳋¦’r\.ë^šÈ©óÏfÚ÷qãÛ˹yÅÃ\vÓ{tæ+PqXq`ÂÏçÐ ¯5ã8lm—z‡Z´mLä­ P_yÈc àúƒç.¹›5ÿ PlTä9Vö=éhOà+–<É5ÎåÙòÒW!ø^r_ew¼j~È{ÑhÐëÀiwµÙãCédÎö8lÍ(ø¢Ój;Vhu ó¥ µ¬[üÚ„n¶báçjGè2‡Kº¿í# )‹Ó© Ñ‚bè@SÑL&,T—¶>»3\N'Õå¸B7õç7 F³™À0¹w¼86ZŽè””N4¡Ô³kñbЉa@äþ+ætnaUǦ?¡(ù.:éL¦ ¨"ý›Í4Mu 8ИƒÐêZ©x[sÙ‘ÃhÈÙÁî].9Å8› ز*U+ÒYµb ù ¸h&oáä…OáÒ‹¯ç¸ÚŸXš›Çêrˆ?ÿj.š–Jù¹dµ6ZC±Qos°/GÓÂ^¾‰5+öçXÅžÚŽ±k.øƒ\—?©ý vü³(¬Û_ qP“9Ÿÿ>pSïX€kÜœßׄ«¹ŽfÌuðj€mïZ H攓⺵’ް±çÑ—¶î®meQûZÏÒLé–í4jû2Ô·˜=¿¿Ý9TÛm”nÚDig 8Ê“x{á¦O›Ê¬Ò³xjî{L‰éä[¶|6ï…„ñ'Üê¦ln?§u«³!qÂ8´áÇ€ÓŽÃP³‘-%2D!„èrùûÜÉã¿£fË'c/ÄìO|Xˆ'×$ÜÈ/8‹…Úr¹Õù±8pÞQWY‰ÑǧåÒgÏwÂ)g“³#ƒÊ’µ£ô8 ’×έ[Ûbö<}¥›V®#¯¼ mð@N½ñ&¢\˜½» Wc15Š…açLaàöyl¯r`ËœÅÇÛnãï/LG[;—×·þ9¢KÅV‹HκºC‡„8ö°àíï¹ñµçyç ï}»Ž2Wñý,¬ÿx»Û˜Fžóßì~˜»ïФèýgɶÙqþ:—‚‡þÁ5ðÁÃ[hõš¾-— »ì\<íQ®Ü8“LM,a¥ß³`K§6U‡¸ÊòîE̼y.¯:§óåêƒÊ1÷ÿ×Þ5݆ǘ³­sssDûW²ã/Y>cÅšW™³ûnú×§ÔÎx—eÙõ#ûUþ-Ÿ/-¦aí+¼¿ù|î}÷ š^þKv×a0tÿÜ\ío¿šå¼qÝU5‡çßϦ`Ú3ljnÿu‡°ïfÁÿVpÓ3¯ñÊCFÞ_”‡1õ"ÎŒþØ÷”vs:rùþµ/¸ù½—yã:þ½0ËÈ[xðÒP¶¼ø*›907‹ÝÝñ'ŸE^Ö*ŠóÕŽ"„p#ƒÉ‡q“.fñüÕŽÒu:ò÷YÛúñß÷~àxãež¹ÕÈG«JqÅN>äxCt/fÍÍn»a‡V§#¡ßPr27º¥=orÈý+ ‰îÓ‡f«•¦ºº¶^Ó#¬_òƒÚz¤Ø~ý(Ú³§“3öjÑ)eÔEÜÈ ÿ¼—0@=…¾áµkž`nž—ögÞxk /ßö·ýø#w/®G Þú‘Ûß™Jíû³Øvø´.5ee‚‚ð ö;èNʾ¿™+´óð]wóì”4X)Ïø˜Çç}Ênk} ìÙüðá:î~:œ¾ÉÜWÀÈ_À·;þÁýúY|µ³a ®R~}ò~ƽù,÷ükØ Y1ã~ô`Q¥šUÏLâë²Wyã6=8+Èúî~ýb-õ~‰ ;ç\ôH(ZÀV¼†o½‡/óí ,eÆÃ3‰~ú^øß}¸*×2릯É2@©¢¤®•›Î"Þ:š{Ÿã–i/òâZhÌeÝç÷óÈËÛÉy?╇S‰:$˃G°gë:·µç•Zë©(>~~JLJŠê•Yºÿ⢄ÇÇwùzõ‰·(m«R2²ö(Ÿ¿t’b>ºÞ~ÁÁŠÙß_õíçu‹V¯èµÝ ‡×,%öÂÿ*_®Ø«lʪR2²J”ß|¨Ü–#WMd‘EYd‘E7-moø[,Нóv‹% ,LÑéõªçèI˼¹sͼ¹s” /¹‚Ö„…a4™(ß»·ÕŸ ác±Má®]jGé‹£u••(Š¢v!„B!ÜÎì·o^¹¦úz•“ô^–  ìV+ÍÖÎMœ/Z7oîœ}…îܾUí,B!„B!„¶9cã¾¢FrßþjgB!„B!„è°E¦F÷ÜW!„B!„¢«hÕ „B!„BÑRÔB!„B!„W’¢†B!„B!¼R×5œå¬üðuÞùµG—¯\!„B!DW°/cî‡ ØÕ¤¨Eô`*5êÈúc-ÛJlxÃG[i®`׆õì®u©¥[eB!„n XÉþáUî»á®¾å1f®(Çéö•Ô³ê™+¹üñETzà0Òž=‡;¯¸•÷vÔ°í훸üÞÏɳ»=GE¶k·`/\ÂüŸÖSâe¹{œ¾?èÛz‘mû»ÜùìzŽü-nh:â \¿òøÝ³1Þö6ÿˆÆ­ñÕå(ú…7_YÆ Ï¼JJ€±ó 5nàù[fP}Ù ž;;€³‚•ï>É[ëã¸úé¿3)ÖEÁ²¹|øõr¶•4¢ Ã?º?£Ï»‰kÆG ¸+‹B!„è¥a7¿}þ Wm¥°^A”Ìè)WsíYýðk9èuUüÆwÏ"ûàOâÙW¯¢Ï!‡l ¶‚ŸxåÉÙšpïþã$ükÖðÑ‚&þöü[;ÇßøŠ]#nf€éè2(Ùüúñl¾^¶›j§Ž >c9ﺫ9£/´ôp™Ðá@ÝU·y/½È|óݼ÷è ø¶¶=³ørú?ùºðx~ç>†ù::ô˜ôZtèMèÜpBÐÑíÚZn¥ÛõЕհaöÓ¼ü[3g>ó*צqÍçá>§°•§§ÞóOŽ>ÒvU°æ/ç‹9ß°xK1Š‘°þ'qÁ W2!δï|I±Q°äcf~¾„]ÕNLáƒ9íÒë¹dL$ðØv=z M9 yãÙO©»ì5þyZh+WÆÿúÙèLV{>Ÿ?ôß”ü`—¿:©‘Íd̸Ö·R! Âs3.#ÉЉu¦qý ÜýŒ Æ“¹ôξZwóËœùÌ|ɇÄW® Ù¨PŸ1‹Î\KüùwòÔ þ”®˜ÃÌ·žÃü"× ð9ÆíêÎúV-øœO¿Ë Hnëy­~¶;A±Q߬!þ⇹uXKYMëCh¨0Ñ÷òÇyêoö1pÕoçó·¾ løÐ}'ÞÇL¡¹ÁŠÆ,·Ý6q z¿(öŸÿwt›tNûC»û¸P¨ßþ9Ï¿ò+Îág0ñò3¹*"œX Ôw`p–±è™[0ˆ ︌¸ê|2çu^0>ËsÓ’04fòÕ‡K±:—[/ ÃU°Œ¹_Ìá%âxãö¡øjŽô=£Ð´ã¦¿³ßS¯æ¾Œd-˜Í‡Ómθ—±A.JÒ?æëÝ‘œqÕÝ$škÙüý~xãu‚g<Ô(Ý÷‡6õKý‚aÑö"lgGî«Ô6íæ·eÙìm²±¦ìtΉÖNªvçÑdˆ#5¼åÆ^Æšy³øô— Š­ZüOäÜë¯å¬¾–Ö‹Îj2æÏfÎÂuä×+èR8ëþG¸¼Ÿ8«ØøÍ,æ,\ÏÞF°ÄŽ`ò×rÞqÁè[öW¼þÞb² +ht>1Œ:ûL’ÊV’¾feV-}NáòÛ¯á”è–|нË>åý/“YaGÔ—S.¾‘«O‰ÃÔjÀj~~òz~ «_ÿ'“õ4å¦3gö×,ÝY‰ÝJÿ“Ïçº+&èÓNiÇYͺÙÏðê²@¦=þçôñAƒ²ŒíÔé†ñ÷Û.fÄþÒøèSÚÍ2É’ÉûO¿AzA#.´ø'Žá‚›¯ãÌ$3”,ýÿ,XGÖÞì@ИxñæH6ΙÉg‹³¨r€!h çÿßCœ—à†ò¦B!„·±ðó‡sxhä!E |úð·ëûøç°þf¶­z…œœZœƒ÷]xrYk°jƒHé߇ä6.O+Ö=|õꇔ¤ÝÇ…¯óF;Ç5£¸æ¼u¼ñ[ùÄÏ)×ÜCÊárÛË`+`õÖ&âοŒ³ŽBG qWífùc+Ù\jçx?=!)Iô±‡¶zðoË]À;_ìbÀù÷0zùÌiõ Ô“ñÑ ¾æ|¼ô¦ÏþóGZK,ɉI$˜î“LrcæŽ^éTšØ5_×—‰¾‡äko»)w‡¶+N*WÎäÕôp®¹o ?¼ðóŸm‚Iìüç3K~fΊZ^ý8gDéMÛÛUÌø¿¿Æ8­¡åjòñ¤¸6sçûÛȪu‘ÚHæ+©‹¿”›ÎE¤ú'ÜJéÆÿcÁ÷™\<`8¾Ç²]ÝÂAiúû|´!ŒÉwßÈŽwþKm+Ïjë³ý§ÝÌyô&Þ®²‚!”ÔS/å–ËÇÙÚi‡ËJ­ÕDTŸ’“}û¡ßèúGï_q#[߃L¿3xò²Áøi@©ßÌœ7>bÅž"ª­ àGÒÉSH ÉaÉ’õ쩲cÈ×ÞÆe#BøkÈ…µÖŠ& ‘~)ÉDýå ímµÛæóŸY?°®°4f¢Oº•Ü:¢c=WÚÚÚÝÇ(µkùÏKˆºéEnsØ{ëÀþ`/XÄ‚L=£þ~;œà‡†A„–gòäÂdN¹‹a–¡Ü<ãU´ݾsú‘ðݹYÛ(s %Ñp„漏ŒoÒ©Œ¾€G®=X=ŒH†]÷Ìäëe¥ŒžEô”'yëlú–./Ãb«Ùôß²¡ÀÆ”(ß#~Ï´]Ï2F28ÁÀÂü”Ù‡“h€¦=KØbÒAr÷IDAT#Þ·˜¥kJ™rN4:¥‰‚m%}q&@idûœgymi8çÜð#CëØ4ï?|üâ,¢_»ƒãÿ2Q¬ìúüY^øÎÁèi·qY?UµX  XÙùé?yéGwùÝ\•y‹?å“—¦c{úY.ï냳*“Œ|-§ßú£Cœ¯þŒÿ}5›­CÿÆ·Kˆ#‡Ÿg}Î{ï&1ðÉÓ ×¹¨Y;“§ÞÍdÀÅ·óä` e+?å?3_ÆûW÷m­Ošcn{pß ¿ÖDHˆgÅbÞxê}v&OẆX»‰ùþ—§+õ¼|ÿxBÛª¢:*ùãý™¼µ2ˆ‹ŸxˆsSÌ-…ñá蜻ø}e>ƒ'ÆÓzmä¯Y4D3æÂ[ˆÁšË’gñÁ›á |q 'U[W³­vWÞ{I¾¬†D–¼Ê»¿;™|ËcŒ‹ÑQWZ…ˆ[Ê›B!„=’b-cÛo Ù¡$2eèŸ' ®ÆJ5zlU•Ô›Cð3v§4²ý³·øÉr1Ï^Øò·ú™ÆD¤{˜1é2èƒH…Õk7Q|f±•;vRíÛ—Áá@CÄñhmšR®dÆ¿5h©eùªÖžá¢zÍ,þ½6‰Ÿ;“øÌ5‡þØ'•ŸNÝ÷ÿÇßÁSÇwì½´§½ízÄÜÜ®ÎÒtþ=+—Ñ÷>ËIaéüÐÖ•Z6~ö%Y!“™~rxËï^wÄíªÑ:±tRWV¾}‰öÕv`¼Ü‚>”~q&l9T9†ãë¡íÚqz¢§<Í;Sµh¬›yµÕ!GølÈЩW26ÖDÃîß™óÅÛ¼’ȳ‹ùËɨÒ\Gµ]OhC9•QûêÛ `ßû3³oâ¸;Î¥ËÙ­b+aÛ¶"Ϲƒ[‡øaË_̇Íevð(¦]rû7²mþÿ˜ÿ¯Y xó~Fùÿ¥»• ¸´ µ•µ‡`:d\I;ÛĺƒOÞø’©WðàÍ©øÛ+(±%à{¬Å¨v÷qŋ籦b¿xœþU‡&¸/c톫NK·ÝýÁEÝž-”Ï´”ýLÄ€ÏO»ØRjgX’aß°›ª‰òúˆ8õpÄý¡¹˜M9‚Æ%¼å—® HeT |¼)—¦)Qøit‡ [qÔTÐ@#B[ª_GØŽpëCÂqѰi;9õ ‰AMì\´ ÇÐë¸*ösž_²šâ³Ï#ÖUÂÖ'Á£û¨¥f#óU3èæ§¹d\ ùÆ*ÖÜ7—ß÷X9>õе(u›øra11½ÀçÅqpÁN©]×?—}Ñ‹Ü2%0,5[ö#ÌŸ—ÁÔ‡F·¼ ú7”Á'kÙ´ôeІLà”‘QèLdÅ 6|¾Ûé„›ÊX6o5ʘ‡¸ýœaøj 5ñò×<βe\Ú7…¿qõDÇ“°ÿ'vr~þš íî¿ïRFúk€A¤øsÏëóøuï‰\ÒF‡üo_ä-kg>þàA -ÁcnáÎ]¯óîÿáæƒ?ñ &O<¿ƒ+$‡gaÈè–ÿO!îÒµ,ž±…œ†‹Hjy8 …NÒRm´“»¡Å'™ÃRI Ð@„B!D«¬l~ã.žÿ£ 01èʧ˜¿ÿXOÁnÓâoÜÍûÞÃÑ6ôl®¹éBF„ê…¦_òÞò0.{n"Qz;åîΠæÌÛ.cÛsñÐß×2f€ƒ-ëš™tÿõÿ—“¶ÖhÐîë½Þ*¥vÎÚÎÀëŸgt°–†Nå?Zím×ös·ËYÆâÿÌ¥ì¤û¹méžZ¼”/Ö89þÎÉ$õÔv M»¾åÝïkrÍ4ùø‘4$æÇÛR¹`P4”PÜàG3ŽîrGöÓ tô³ưq£ !}ÐnÙÀ[«¶S3%†ÐÃ&¢pÙšð r±ö­‡Y ˜bFpîµ7pÎà€Cç¬PØöÍìœÌ£‚›ÏÂ@Ä á ìƒâhZµ’w5#™8þ5Ͳ'V²±ÈÎ(ÿ¿\q§ÙnÁRö%OÝ;¤ÿé—qó'³LÅ‘¶‰£žÊFè3”!ýâ0Lß6·ßQhoWصn/DŒç´ËN¡_˜žÊõ_ðάé4ÎàÞQíL᤾¼|’:hè‚Î?6RÖà„CÎÔ›Ùûëø,?‰óŸEP{_3ÎzÊëÁ?ÜïÏBŸÎˆ JM% .8ø”×U›Á§ÿY†vìÝLMl$ÁŠ:û "„t6X9Ű_6ÁÐ;‡Ò?4Ÿ°ï³dï.öÝEf•‘>C£1¶²:T¿s'W¼sh‹†*ë_¾sšK·“çdäÐÛ\šI3ÿoïÎ㢪úŽ.³À ;ÃŽÈæ."™ä‚¢©™iþÌŸZiQY.Y–iV%KšÚÓS¡e¹ýp—Ç1ÍG+3Ô‡„rITöm`îïPq·çõ˜çýçÌëÞ{î™{îÜó½ç|O§6.׿S¹Ò&Ä–õG~'ßÐ÷FÛ ²Eo'/•R(P sÕ!U—QjAÊ%#ʲæÓ(¢«È+6/ ¬\FÖïÁk(W³3!¡ ËT2²Ë‘}TM^8¶-Ûáðǯì\²Š ©ÏÑÅ¥ÞY+‰ý!ýÉ¡}ß³kçB¦®ó¤÷Ä©<×É¡‰½Õ2–gûŠD’œ!¯Øˆ¥¦†ZPUs³»¡ ÏžO¾w1ó_‡ðÞ}é×§+ANM—YAá/I.`oìx¾ì“ÇÙ#;H\Ë\E÷é•™ü¼ç(ÑÁ‘èïh0n'¾[ÆQ¯aÌîh‡Å]Y¿°šs§òÖºœë˜Â³ë´D¾»ˆWC¬L×ëϘ–)>šÈêì¶¼<1­„‰~€ÌÝ;ÉÔ=Âèö·º(‚LÙñÌúÛfŒ}ßfb”¾®S§ÂgàFe-`Eü’P ƒ¯sƒÞ-Ãm\Û’.:øí e5ÜÔP¸D1uQÈвþÍ®U ¬™½ë§­¿^)rá¶üRNÀ³Quy/n~»¤ÁW³^˜ÂS³gò”É^eSè£Þ ¾}%²\Ê‘/ç’4ŽI=]PHJtn*¤‰zõñºÓiÓœL>DqQ mø¦3ûýù3f>q=ëV´¨Êä‡/aуÒ<˜$Svb=ññ[¨éû6ï>zmµIãGÿÉ èSv…KÅ2ZõY¾zs>†>„í½^Jã.¨ÎmþÚönb;IiF£é> ¤ÂÖ'ŒÁ/àðøÅüp¬^zǺ2…i»É0¶ä¥ŽMåÅh°#”jEíñä«ÇW£@Æ(›î…ZX¹õ /žN%îà~²F…Ø\ÿ]Òü¿3ø¼k*)[7ñÝ'ÓØØê>z«ï ‰ƒ¯k¾=ÈE‡L·ñöJÔJ¨,*»¾¸„B‡«ƒã•*Œ 3YQ lœ­¡â …•2Wó ÔçS‚5.W3rÊUdïü„¸åyD¼Ë áÍÔÿÕÝ[ãlçòK¨áêJ %äËH¶ŽX×]ïÆ¢#|;c>û†ñþ›ðk.WeÓwK:·T±ÿðìH?޶ËtZjÜèÚÛ— 6“ré$ÏÁÖ…~Ô.-q—R8“-áéyãTŽº¾¶l¬ .¨]Zân‘±´<  §Ÿ¨õAx)v‘þ[>†€Úé'òHË(Bé„ËíDhÕz‚õpøÔE¬ÜÂpmæW”TTQRY/’'iñ r‚”Cœ*éJ˜®vÜ[ñÉ_ÉÆ…¾Þ“J§pž›Žö£x>øÕÌqtm* ‡JO›®pì<¹eF‚š*Kõ%þ8[‰ÝÃCðp–@E±#*rnÜ_ãsS;5’ÐÈî|7}*k·d0¬mç7[AA„¿.6n-°¨(ÁF©ÄÖÃ??S=—º)FCÝ ³ÆäºÎS]Ö4×h¦Í‰¨÷»’ãË>dqÑb'vÃä¡Ì,CuÁi²*´´ò¼:´[BëÓ_õ6rrK1bc^Ç£)*_†ÅÍaÀµÀ2¥G>'nµ51q1„»ÞNpABeç¯ q^c¥ƒ¾~7æX¸®a½Þ9+Bbf1·ÞÛúš‹{™?çüÇOcx»ëS 9©¾bC›OÌŠ?Õ1$aî&Ê{Oåƒá¡7yÆ–PjqÕ”plÅJVú1*Êû†ìÿ̹¶ïtõFYnb4…\ʉ§0úŒ$ô?ý‘enñ²S`ãΠW:Ñ3|¯²=ç¢qÓõf›o†¼fڸŽžVÈGȪ'Ä 0\ädN V-ÜÍX5ÇÝC­q!™ƒ–òH$ªÈNýƒr«–´Ö«™²ŒUÌ^žMØÄÍh¨Ýië«dϱc\¬öÇC Æ¢?8t<‡µ¨S“KÊÂEì±JÜ-4 ¹ †dMà£(þ¾‹âÊà±-ê²—ˆÇ^õI©à1¬ÝµÄ˜’]†ôp ~óÇ,°J¯TùdyÑ=*k…Gk¸œv€£çÜèäÑ‘§z:2s݇Ò#ÐEÙeÊÝ;ó°w†Fë‰[;ŸõpºyËœÝû¬ÏqåñWÚ¡“ ÜìS­£p%rpI_άT0$Ò‹Rr³+ Šî†W£?¥£?¾Ú2­ÝÄþÁ!X^BÝêQZõL›ä%|¶ ‘ç¶Å¶ð(›–¥B»—‰ölþ6$Ù„ðôÔI½÷ ŸÍvEÿÁ¬÷-aCž­=pÔH”^H#y}¸>¿Î¥¦‰²„¶ÅßCÅ?Nb{«~´rÑP•]ØÌ ¤†‚´Ý,qÃ×Õ¹ ƒSE ñµ6¹v¹ ‚ ƒÆpî{¾ûEÆÇÏk©Œ¼ŒïYs  —~‘µÏÕ¹ìO:@¹§n: JÏeÇÚÃTy ¡³«I¥Cï©«·Ç ®h-*qw»É«·X•¾ mì6±÷›D‚FvÃת˜)Ë9T¥çñÐ[èx4ERcïæýµdŠ3-‘œ=ôèL<öÖänã7V¢÷binN#ÍÔë“°rrdz~y•v¨%%:½;.Ú«µ&S|ú7ò$ožö¼•†¬‰4¶'&\Cþ™Óä`•“'î¶JdC!ç3Ï“wþ$©{¶‘ò»DøKSˆv¿æž`Öµ}kA*²v%‘ªðÅW¯ÁXx–_¶®ã”2˜ZÛ]Ï›a¸ÀÑ3Õ8v­Íéx7 ޲9%g?OìTä¥ïaÝ÷E8ö~os"M†sü|k?oTœKÏ¡-ö‹;jÍ·q5ýºã|p ˈéíIÑOËÙœëLô+h›?*¯(oãë„6îWá¬Ø~÷A Ò5ùì[•BAÀÓts. ótÝ I½‡¦nf’ŽvƒzàøÑz>]i˰öjNnù– UG&DÖNɪ8¾‘µ¿[ÓuB(\8ÃéºM-´®x»j1¾jæŽ aÒ Å1Ò½£éVoÌŒ…}{úu°$ý]Âô×w$YÓú¹X&Û-gMÊRæ%Õ€B‡w—QtBOäð¾ü2™e;;ÓvLKZ=Ë›ºoIܹ˜yd°t%bL(}œ ñ.oi¾aÕÆ…ì/­gGžš2†'¬n3NkÃï2ø–o7l募ÊQaÿPÆv{¯Æ?†&„‘/õáâÒÍ|6{’mßìB[ÿîLŠ5²â›|5w+Õ*G#cˆeb哯%±côä!œž¾«‚™hKEÆN–m»D€Ú¿vO2yä Z¨UÓeùŸÇóxÂJ’ų@¡ÁÞ'‡Æ™·¯1R’y„­ë’_Yû›y¶ÌÄ‘Áwgú ‚ Âýƪ o,n¼tƒLuYg~ÚÅ–µ…KLJè8ôMF ô¯}Ùg¬äJö¯lß²žËU€Ú ÿNØ6r€y f™Q«`F½ó2Êo6ðM|2•€Æ­5ý&Œaè­gµ¼käêJª±ÂÁV}óçvÉ–®qKéÚøó{^¯æ2pùÔE° ÇåÞ#—‘™žåù,™‘Úà+ï‘óˆï›Âü÷7pÑÖÿÖQŒ‹ïÏ#-´îTp¹šÒ‚³ìOÙÂê¢@ƒ>¸+£ßN”K½|e¹d“¯csÙ[f¬, 'm;Iò©”v>´4‰g™µœ®\žÏÉý«Ù½²# rlI_ —^|þÚƒmÜ*p8ÓÇÉ$$.cæ^ ‡ z›Æð–föð.D½6…’ÅKIZ4‡ ¥¡ý_cìßÚûLUiÙ25†Dâß«¿¡#ý?ú˜QÍŒpÓ`úX™/—2wg Zï.<ûζ·ª¹|üOJ¸ÂžOãØSÓ  $¼ar6´~õJ9ú±æ¨ ‚ ‚ ‚ÊÓ2nn£¾K7‡¶«.€h÷Jòö­&Gq‚ ‚ ‚ ܹéçP·ëKûf×{„¿:Ñ=bGAA„ž ß§gñY„Bôᄞh÷’j‚ ‚ ‚pX ¸?r^ €h÷Š˜~"‚ ‚ ‚ Â}I5AAAA¸/)¡6c¨ ‚ ‚ ‚ Âýäÿˆ‹RPöï~ IEND®B`‚libindi/libs/indibase/alignment/LoaderMain.cpp0000664000175000017500000000047313263645557020702 0ustar jasemjasem#include #include "LoaderClient.h" using namespace std; static LoaderClient Client; int main(int argc, char *argv[]) { Client.Initialise(argc, argv); Client.Load(); cout << "Press any key followed by return to terminate the client.\n"; string term; cin >> term; return 0; } libindi/libs/indibase/alignment/DummyMathPlugin.h0000664000175000017500000000155413263645557021421 0ustar jasemjasem #pragma once #include "AlignmentSubsystemForMathPlugins.h" #include "ConvexHull.h" namespace INDI { namespace AlignmentSubsystem { class DummyMathPlugin : public AlignmentSubsystemForMathPlugins { public: DummyMathPlugin(); virtual ~DummyMathPlugin(); virtual bool Initialise(InMemoryDatabase *pInMemoryDatabase); virtual bool TransformCelestialToTelescope(const double RightAscension, const double Declination, double JulianOffset, TelescopeDirectionVector &ApparentTelescopeDirectionVector); virtual bool TransformTelescopeToCelestial(const TelescopeDirectionVector &ApparentTelescopeDirectionVector, double &RightAscension, double &Declination); }; } // namespace AlignmentSubsystem } // namespace INDI libindi/libs/indibase/alignment/ConvexHull.cpp0000664000175000017500000006713413263645557020765 0ustar jasemjasem/*! * \file ConvexHull.cpp * * \author Roger James * \date December 2013 * */ // This C++ code is based on the c code described below // it was ported to c++ and modded to work on on coordinate types // other than integers by Roger James in December 2013 /* This code is described in "Computational Geometry in C" (Second Edition), Chapter 4. It is not written to be comprehensible without the explanation in that book. Input: 3n integer coordinates for the points. Output: the 3D convex hull, in postscript with embedded comments showing the vertices and faces. Compile: gcc -o chull chull.c (or simply: make) Written by Joseph O'Rourke, with contributions by Kristy Anderson, John Kutcher, Catherine Schevon, Susan Weller. Last modified: May 2000 Questions to orourke@cs.smith.edu. -------------------------------------------------------------------- This code is Copyright 2000 by Joseph O'Rourke. It may be freely redistributed in its entirety provided that this copyright notice is not removed. -------------------------------------------------------------------- */ #include "ConvexHull.h" #include #include namespace INDI { namespace AlignmentSubsystem { ConvexHull::ConvexHull() : vertices(nullptr), edges(nullptr), faces(nullptr), debug(false), check(false), ScaleFactor(SAFE-1) { } bool ConvexHull::AddOne(tVertex p) { tFace f; tEdge e, temp; int vol; bool vis = false; if (debug) { std::cerr << "AddOne: starting to add v" << p->vnum << ".\n"; //PrintOut( vertices ); } /* Mark faces visible from p. */ f = faces; do { vol = VolumeSign(f, p); if (debug) std::cerr << "faddr: " << std::hex << f << " paddr: " << p << " Vol = " << std::dec << vol << '\n'; if (vol < 0) { f->visible = VISIBLE; vis = true; } f = f->next; } while (f != faces); /* If no faces are visible from p, then p is inside the hull. */ if (!vis) { p->onhull = !ONHULL; return false; } /* Mark edges in interior of visible region for deletion. Erect a newface based on each border edge. */ e = edges; do { temp = e->next; if (e->adjface[0]->visible && e->adjface[1]->visible) /* e interior: mark for deletion. */ e->delete_it = REMOVED; else if (e->adjface[0]->visible || e->adjface[1]->visible) /* e border: make a new face. */ e->newface = MakeConeFace(e, p); e = temp; } while (e != edges); return true; } void ConvexHull::CheckEndpts() { int i; tFace fstart; tEdge e; tVertex v; bool error = false; fstart = faces; if (faces) do { for (i = 0; i < 3; ++i) { v = faces->vertex[i]; e = faces->edge[i]; if (v != e->endpts[0] && v != e->endpts[1]) { error = true; std::cerr << "CheckEndpts: Error!\n"; std::cerr << " addr: " << std::hex << faces << ':'; std::cerr << " edges:"; std::cerr << "(" << e->endpts[0]->vnum << "," << e->endpts[1]->vnum << ")"; std::cerr << "\n"; } } faces = faces->next; } while (faces != fstart); if (error) std::cerr << "Checks: ERROR found and reported above.\n"; else std::cerr << "Checks: All endpts of all edges of all faces check.\n"; } void ConvexHull::CheckEuler(int V, int E, int F) { if (check) std::cerr << "Checks: V, E, F = " << V << ' ' << E << ' ' << F << ":\t"; if ((V - E + F) != 2) std::cerr << "Checks: V-E+F != 2\n"; else if (check) std::cerr << "V-E+F = 2\t"; if (F != (2 * V - 4)) std::cerr << "Checks: F=" << F << " != 2V-4=" << 2 * V - 4 << "; V=" << V << '\n'; else if (check) std::cerr << "F = 2V-4\t"; if ((2 * E) != (3 * F)) std::cerr << "Checks: 2E=" << 2 * E << " != 3F=" << 3 * F << "; E=" << E << ", F=" << F << '\n'; else if (check) std::cerr << "2E = 3F\n"; } void ConvexHull::Checks() { tVertex v; tEdge e; tFace f; int V = 0, E = 0, F = 0; Consistency(); Convexity(); if ((v = vertices) != nullptr) do { if (v->mark) V++; v = v->next; } while (v != vertices); if ((e = edges) != nullptr) do { E++; e = e->next; } while (e != edges); if ((f = faces) != nullptr) do { F++; f = f->next; } while (f != faces); CheckEuler(V, E, F); CheckEndpts(); } void ConvexHull::CleanEdges() { tEdge e; /* Primary index into edge list. */ tEdge t; /* Temporary edge pointer. */ /* Integrate the newface's into the data structure. */ /* Check every edge. */ e = edges; do { if (e->newface) { if (e->adjface[0]->visible) e->adjface[0] = e->newface; else e->adjface[1] = e->newface; e->newface = nullptr; } e = e->next; } while (e != edges); /* Delete any edges marked for deletion. */ while (edges && edges->delete_it) { e = edges; remove(edges, e); } e = edges->next; do { if (e->delete_it) { t = e; e = e->next; remove(edges, t); } else e = e->next; } while (e != edges); } void ConvexHull::CleanFaces() { tFace f; /* Primary pointer into face list. */ tFace t; /* Temporary pointer, for deleting. */ while (faces && faces->visible) { f = faces; remove(faces, f); } f = faces->next; do { if (f->visible) { t = f; f = f->next; remove(faces, t); } else f = f->next; } while (f != faces); } void ConvexHull::CleanUp(tVertex *pvnext) { CleanEdges(); CleanFaces(); CleanVertices(pvnext); } void ConvexHull::CleanVertices(tVertex *pvnext) { tEdge e; tVertex v, t; /* Mark all vertices incident to some undeleted edge as on the hull. */ e = edges; do { e->endpts[0]->onhull = e->endpts[1]->onhull = ONHULL; e = e->next; } while (e != edges); /* Delete all vertices that have been processed but are not on the hull. */ while (vertices && vertices->mark && !vertices->onhull) { /* If about to delete vnext, advance it first. */ v = vertices; if (v == *pvnext) *pvnext = v->next; remove(vertices, v); } v = vertices->next; do { if (v->mark && !v->onhull) { t = v; v = v->next; if (t == *pvnext) *pvnext = t->next; remove(vertices, t); } else v = v->next; } while (v != vertices); /* Reset flags. */ v = vertices; do { v->duplicate = nullptr; v->onhull = !ONHULL; v = v->next; } while (v != vertices); } bool ConvexHull::Collinear(tVertex a, tVertex b, tVertex c) { return (c->v[Z] - a->v[Z]) * (b->v[Y] - a->v[Y]) - (b->v[Z] - a->v[Z]) * (c->v[Y] - a->v[Y]) == 0 && (b->v[Z] - a->v[Z]) * (c->v[X] - a->v[X]) - (b->v[X] - a->v[X]) * (c->v[Z] - a->v[Z]) == 0 && (b->v[X] - a->v[X]) * (c->v[Y] - a->v[Y]) - (b->v[Y] - a->v[Y]) * (c->v[X] - a->v[X]) == 0; } void ConvexHull::Consistency() { tEdge e; int i, j; e = edges; do { /* find index of endpoint[0] in adjacent face[0] */ for (i = 0; e->adjface[0]->vertex[i] != e->endpts[0]; ++i) ; /* find index of endpoint[0] in adjacent face[1] */ for (j = 0; e->adjface[1]->vertex[j] != e->endpts[0]; ++j) ; /* check if the endpoints occur in opposite order */ if (!(e->adjface[0]->vertex[(i + 1) % 3] == e->adjface[1]->vertex[(j + 2) % 3] || e->adjface[0]->vertex[(i + 2) % 3] == e->adjface[1]->vertex[(j + 1) % 3])) break; e = e->next; } while (e != edges); if (e != edges) std::cerr << "Checks: edges are NOT consistent.\n"; else std::cerr << "Checks: edges consistent.\n"; } void ConvexHull::ConstructHull() { tVertex v, vnext; v = vertices; do { vnext = v->next; if (!v->mark) { v->mark = PROCESSED; AddOne(v); CleanUp(&vnext); /* Pass down vnext in case it gets deleted. */ if (check) { std::cerr << "ConstructHull: After Add of " << v->vnum << " & Cleanup:\n"; Checks(); } // if ( debug ) // PrintOut( v ); } v = vnext; } while (v != vertices); } void ConvexHull::Convexity() { tFace f; tVertex v; int vol; f = faces; do { v = vertices; do { if (v->mark) { vol = VolumeSign(f, v); if (vol < 0) break; } v = v->next; } while (v != vertices); f = f->next; } while (f != faces); if (f != faces) std::cerr << "Checks: NOT convex.\n"; else if (check) std::cerr << "Checks: convex.\n"; } void ConvexHull::DoubleTriangle() { tVertex v0, v1, v2, v3; tFace f0, f1 = nullptr; int vol; /* Find 3 noncollinear points. */ v0 = vertices; while (Collinear(v0, v0->next, v0->next->next)) if ((v0 = v0->next) == vertices) { std::cout << "DoubleTriangle: All points are Collinear!\n"; exit(0); } v1 = v0->next; v2 = v1->next; /* Mark the vertices as processed. */ v0->mark = PROCESSED; v1->mark = PROCESSED; v2->mark = PROCESSED; /* Create the two "twin" faces. */ f0 = MakeFace(v0, v1, v2, f1); f1 = MakeFace(v2, v1, v0, f0); /* Link adjacent face fields. */ f0->edge[0]->adjface[1] = f1; f0->edge[1]->adjface[1] = f1; f0->edge[2]->adjface[1] = f1; f1->edge[0]->adjface[1] = f0; f1->edge[1]->adjface[1] = f0; f1->edge[2]->adjface[1] = f0; /* Find a fourth, noncoplanar point to form tetrahedron. */ v3 = v2->next; vol = VolumeSign(f0, v3); while (!vol) { if ((v3 = v3->next) == v0) { std::cout << "DoubleTriangle: All points are coplanar!\n"; exit(0); } vol = VolumeSign(f0, v3); } /* Insure that v3 will be the first added. */ vertices = v3; if (debug) { std::cerr << "DoubleTriangle: finished. Head repositioned at v3.\n"; //PrintOut( vertices ); } } void ConvexHull::EdgeOrderOnFaces() { tFace f = faces; tEdge newEdge; int i, j; do { for (i = 0; i < 3; i++) { if (!(((f->edge[i]->endpts[0] == f->vertex[i]) && (f->edge[i]->endpts[1] == f->vertex[(i + 1) % 3])) || ((f->edge[i]->endpts[1] == f->vertex[i]) && (f->edge[i]->endpts[0] == f->vertex[(i + 1) % 3])))) { /* Change the order of the edges on the face: */ for (j = 0; j < 3; j++) { /* find the edge that should be there */ if (((f->edge[j]->endpts[0] == f->vertex[i]) && (f->edge[j]->endpts[1] == f->vertex[(i + 1) % 3])) || ((f->edge[j]->endpts[1] == f->vertex[i]) && (f->edge[j]->endpts[0] == f->vertex[(i + 1) % 3]))) { /* Swap it with the one erroneously put into its place: */ if (debug) std::cerr << "Making a swap in EdgeOrderOnFaces: F(" << f->vertex[0]->vnum << ',' << f->vertex[1]->vnum << ',' << f->vertex[2]->vnum << "): e[" << i << "] and e[" << j << "]\n"; newEdge = f->edge[i]; f->edge[i] = f->edge[j]; f->edge[j] = newEdge; } } } } f = f->next; } while (f != faces); } void ConvexHull::MakeCcw(tFace f, tEdge e, tVertex p) { tFace fv; /* The visible face adjacent to e */ int i; /* Index of e->endpoint[0] in fv. */ tEdge s = nullptr; /* Temporary, for swapping */ if (e->adjface[0]->visible) fv = e->adjface[0]; else fv = e->adjface[1]; /* Set vertex[0] & [1] of f to have the same orientation as do the corresponding vertices of fv. */ for (i = 0; fv->vertex[i] != e->endpts[0]; ++i) ; /* Orient f the same as fv. */ if (fv->vertex[(i + 1) % 3] != e->endpts[1]) { f->vertex[0] = e->endpts[1]; f->vertex[1] = e->endpts[0]; } else { f->vertex[0] = e->endpts[0]; f->vertex[1] = e->endpts[1]; swap(s, f->edge[1], f->edge[2]); } /* This swap is tricky. e is edge[0]. edge[1] is based on endpt[0], edge[2] on endpt[1]. So if e is oriented "forwards," we need to move edge[1] to follow [0], because it precedes. */ f->vertex[2] = p; } ConvexHull::tFace ConvexHull::MakeConeFace(tEdge e, tVertex p) { tEdge new_edge[2]; tFace new_face; int i, j; /* Make two new edges (if don't already exist). */ for (i = 0; i < 2; ++i) /* If the edge exists, copy it into new_edge. */ if (!(new_edge[i] = e->endpts[i]->duplicate)) { /* Otherwise (duplicate is nullptr), MakeNullEdge. */ new_edge[i] = MakeNullEdge(); new_edge[i]->endpts[0] = e->endpts[i]; new_edge[i]->endpts[1] = p; e->endpts[i]->duplicate = new_edge[i]; } /* Make the new face. */ new_face = MakeNullFace(); new_face->edge[0] = e; new_face->edge[1] = new_edge[0]; new_face->edge[2] = new_edge[1]; MakeCcw(new_face, e, p); /* Set the adjacent face pointers. */ for (i = 0; i < 2; ++i) for (j = 0; j < 2; ++j) /* Only one nullptr link should be set to new_face. */ if (!new_edge[i]->adjface[j]) { new_edge[i]->adjface[j] = new_face; break; } return new_face; } ConvexHull::tFace ConvexHull::MakeFace(tVertex v0, tVertex v1, tVertex v2, tFace fold) { tFace f; tEdge e0, e1, e2; /* Create edges of the initial triangle. */ if (!fold) { e0 = MakeNullEdge(); e1 = MakeNullEdge(); e2 = MakeNullEdge(); } else /* Copy from fold, in reverse order. */ { e0 = fold->edge[2]; e1 = fold->edge[1]; e2 = fold->edge[0]; } e0->endpts[0] = v0; e0->endpts[1] = v1; e1->endpts[0] = v1; e1->endpts[1] = v2; e2->endpts[0] = v2; e2->endpts[1] = v0; /* Create face for triangle. */ f = MakeNullFace(); f->edge[0] = e0; f->edge[1] = e1; f->edge[2] = e2; f->vertex[0] = v0; f->vertex[1] = v1; f->vertex[2] = v2; /* Link edges to face. */ e0->adjface[0] = e1->adjface[0] = e2->adjface[0] = f; return f; } void ConvexHull::MakeNewVertex(double x, double y, double z, int VertexId) { tVertex v; v = MakeNullVertex(); v->v[X] = x * ScaleFactor; v->v[Y] = y * ScaleFactor; v->v[Z] = z * ScaleFactor; v->vnum = VertexId; if ((std::abs(x) > SAFE) || (std::abs(y) > SAFE) || (std::abs(z) > SAFE)) { std::cout << "Coordinate of vertex below might be too large: run with -d flag\n"; PrintPoint(v); } } ConvexHull::tEdge ConvexHull::MakeNullEdge() { tEdge e; e = new tsEdge; e->adjface[0] = e->adjface[1] = e->newface = nullptr; e->endpts[0] = e->endpts[1] = nullptr; e->delete_it = !REMOVED; add(edges, e); return e; } ConvexHull::tFace ConvexHull::MakeNullFace() { tFace f; f = new tsFace; for (int i = 0; i < 3; ++i) { f->edge[i] = nullptr; f->vertex[i] = nullptr; } f->visible = !VISIBLE; add(faces, f); return f; } ConvexHull::tVertex ConvexHull::MakeNullVertex() { tVertex v; v = new tsVertex; v->duplicate = nullptr; v->onhull = !ONHULL; v->mark = !PROCESSED; add(vertices, v); return v; } void ConvexHull::Print() { /* Pointers to vertices, edges, faces. */ tVertex v; tEdge e; tFace f; int xmin, ymin, xmax, ymax; int a[3], b[3]; /* used to compute normal vector */ /* Counters for Euler's formula. */ int V = 0, E = 0, F = 0; /* Note: lowercase==pointer, uppercase==counter. */ /*-- find X min & max --*/ v = vertices; xmin = xmax = v->v[X]; do { if (v->v[X] > xmax) xmax = v->v[X]; else if (v->v[X] < xmin) xmin = v->v[X]; v = v->next; } while (v != vertices); /*-- find Y min & max --*/ v = vertices; ymin = ymax = v->v[Y]; do { if (v->v[Y] > ymax) ymax = v->v[Y]; else if (v->v[Y] < ymin) ymin = v->v[Y]; v = v->next; } while (v != vertices); /* PostScript header */ std::cout << "%!PS\n"; std::cout << "%%BoundingBox: " << xmin << ' ' << ymin << ' ' << xmax << ' ' << ymax << '\n'; std::cout << ".00 .00 setlinewidth\n"; std::cout << -xmin + 72 << ' ' << -ymin + 72 << " translate\n"; /* The +72 shifts the figure one inch from the lower left corner */ /* Vertices. */ v = vertices; do { if (v->mark) V++; v = v->next; } while (v != vertices); std::cout << "\n%% Vertices:\tV = " << V << '\n'; std::cout << "%% index:\t\tx\ty\tz\n"; do { std::cout << "%% " << v->vnum << ":\t" << v->v[X] << '\t' << v->v[Y] << '\t' << v->v[Z] << '\n'; v = v->next; } while (v != vertices); /* Faces. */ /* visible faces are printed as PS output */ f = faces; do { ++F; f = f->next; } while (f != faces); std::cout << "\n%% Faces:\tF = " << F << '\n'; std::cout << "%% Visible faces only: \n"; do { /* Print face only if it is visible: if normal vector >= 0 */ // RFJ This code is 2d so what is calculated below // is actually the perp product or signed area. SubVec(f->vertex[1]->v, f->vertex[0]->v, a); SubVec(f->vertex[2]->v, f->vertex[1]->v, b); if ((a[0] * b[1] - a[1] * b[0]) >= 0) { std::cout << "%% vnums: " << f->vertex[0]->vnum << " " << f->vertex[1]->vnum << " " << f->vertex[2]->vnum << '\n'; std::cout << "newpath\n"; std::cout << f->vertex[0]->v[X] << '\t' << f->vertex[0]->v[Y] << "\tmoveto\n"; std::cout << f->vertex[1]->v[X] << '\t' << f->vertex[1]->v[Y] << "\tlineto\n"; std::cout << f->vertex[2]->v[X] << '\t' << f->vertex[2]->v[Y] << "\tlineto\n"; std::cout << "closepath stroke\n\n"; } f = f->next; } while (f != faces); /* prints a list of all faces */ std::cout << "%% List of all faces: \n"; std::cout << "%%\tv0\tv1\tv2\t(vertex indices)\n"; do { std::cout << "%%\t" << f->vertex[0]->vnum << '\t' << f->vertex[1]->vnum << '\t' << f->vertex[2]->vnum << '\n'; f = f->next; } while (f != faces); /* Edges. */ e = edges; do { E++; e = e->next; } while (e != edges); std::cout << "\n%% Edges:\tE = " << E << '\n'; /* Edges not printed out (but easily added). */ std::cout << "\nshowpage\n\n"; check = true; CheckEuler(V, E, F); } void ConvexHull::PrintEdges(std::ofstream &Ofile) { tEdge temp; int i; temp = edges; Ofile << "Edge List\n"; if (edges) do { Ofile << " addr: " << std::hex << edges << '\t'; Ofile << "adj: "; for (i = 0; i < 2; ++i) Ofile << edges->adjface[i] << ' '; Ofile << " endpts:" << std::dec; for (i = 0; i < 2; ++i) Ofile << edges->endpts[i]->vnum << ' '; Ofile << " del:" << edges->delete_it << '\n'; edges = edges->next; } while (edges != temp); } void ConvexHull::PrintFaces(std::ofstream &Ofile) { int i; tFace temp; temp = faces; Ofile << "Face List\n"; if (faces) do { Ofile << " addr: " << std::hex << faces << " "; Ofile << " edges:" << std::hex; for (i = 0; i < 3; ++i) Ofile << faces->edge[i] << ' '; Ofile << " vert:" << std::dec; for (i = 0; i < 3; ++i) Ofile << ' ' << faces->vertex[i]->vnum; Ofile << " vis: " << faces->visible << '\n'; faces = faces->next; } while (faces != temp); } void ConvexHull::PrintObj(const char *FileName) { tVertex v; tFace f; std::map vnumToOffsetMap; int a[3], b[3]; /* used to compute normal vector */ double c[3], length; std::ofstream Ofile; Ofile.open(FileName, std::ios_base::out | std::ios_base::trunc); Ofile << "# obj file written by chull\n"; Ofile << "mtllib chull.mtl\n"; Ofile << "g Object001\n"; Ofile << "s 1\n"; Ofile << "usemtl default\n"; // The convex hull code removes vertices from the list that at are on the hull // so the vertices list might have missing vnums. So I need to construct a map of // vnums to new vertex array indices. int Offset = 1; v = vertices; do { vnumToOffsetMap[v->vnum] = Offset; Ofile << "v " << v->v[X] << ' ' << v->v[Y] << ' ' << v->v[Z] << '\n'; Offset++; v = v->next; } while (v != vertices); // normals f = faces; do { // get two tangent vectors SubVec(f->vertex[1]->v, f->vertex[0]->v, a); // SubVec( f->vertex[2]->v, f->vertex[1]->v, b ); SubVec(f->vertex[2]->v, f->vertex[0]->v, b); // cross product for the normal c[0] = a[1] * b[2] - a[2] * b[1]; c[1] = a[2] * b[0] - a[0] * b[2]; c[2] = a[0] * b[1] - a[1] * b[0]; // normalise length = sqrt((c[0] * c[0]) + (c[1] * c[1]) + (c[2] * c[2])); c[0] = c[0] / length; c[1] = c[1] / length; c[2] = c[2] / length; Ofile << "vn " << c[0] << ' ' << c[1] << ' ' << c[2] << '\n'; f = f->next; } while (f != faces); // Faces int i = 1; f = faces; do { Ofile << "f " << vnumToOffsetMap[f->vertex[0]->vnum] << "//" << i << ' ' << vnumToOffsetMap[f->vertex[1]->vnum] << "//" << i << ' ' << vnumToOffsetMap[f->vertex[2]->vnum] << "//" << i << '\n'; i++; f = f->next; } while (f != faces); Ofile.close(); Ofile.open("chull.mtl", std::ios_base::out | std::ios_base::trunc); Ofile << "newmtl default\n"; Ofile << "Ka 0.2 0 0\n"; Ofile << "Kd 0.8 0 0\n"; Ofile << "illum 1\n"; Ofile.close(); } void ConvexHull::PrintOut(const char *FileName, tVertex v) { std::ofstream Ofile; Ofile.open(FileName, std::ios_base::out | std::ios_base::trunc); Ofile << "\nHead vertex " << v->vnum << " = " << std::hex << v << " :\n"; PrintVertices(Ofile); PrintEdges(Ofile); PrintFaces(Ofile); Ofile.close(); } void ConvexHull::PrintPoint(tVertex p) { for (int i = 0; i < 3; i++) std::cout << '\t' << p->v[i]; std::cout << '\n'; } void ConvexHull::PrintVertices(std::ofstream &Ofile) { tVertex temp; temp = vertices; Ofile << "Vertex List\n"; if (vertices) do { Ofile << " addr " << std::hex << vertices << "\t"; Ofile << " vnum " << std::dec << vertices->vnum; Ofile << '(' << vertices->v[X] << ',' << vertices->v[Y] << ',' << vertices->v[Z] << ')'; Ofile << " active:" << vertices->onhull; Ofile << " dup:" << std::hex << vertices->duplicate; Ofile << " mark:" << std::dec << vertices->mark << '\n'; vertices = vertices->next; } while (vertices != temp); } void ConvexHull::ReadVertices() { tVertex v; int x, y, z; int vnum = 0; while (!(std::cin.eof() || std::cin.fail())) { std::cin >> x >> y >> z; v = MakeNullVertex(); v->v[X] = x; v->v[Y] = y; v->v[Z] = z; v->vnum = vnum++; if ((abs(x) > SAFE) || (abs(y) > SAFE) || (abs(z) > SAFE)) { std::cout << "Coordinate of vertex below might be too large: run with -d flag\n"; PrintPoint(v); } } } void ConvexHull::Reset() { tVertex CurrentVertex = vertices; tEdge CurrentEdge = edges; tFace CurrentFace = faces; if (nullptr != CurrentVertex) { do { tVertex TempVertex = CurrentVertex; CurrentVertex = CurrentVertex->next; delete TempVertex; } while (CurrentVertex != vertices); vertices = nullptr; } if (nullptr != CurrentEdge) { do { tEdge TempEdge = CurrentEdge; CurrentEdge = CurrentEdge->next; delete TempEdge; } while (CurrentEdge != edges); edges = nullptr; } if (nullptr != CurrentFace) { do { tFace TempFace = CurrentFace; CurrentFace = CurrentFace->next; delete TempFace; } while (CurrentFace != faces); faces = nullptr; } debug = false; check = false; } void ConvexHull::SubVec(int a[3], int b[3], int c[3]) { int i; for (i = 0; i < 3; i++) // RFJ //for( i=0; i < 2; i++ ) c[i] = a[i] - b[i]; } int ConvexHull::Volumei(tFace f, tVertex p) { int vol; int ax, ay, az, bx, by, bz, cx, cy, cz; ax = f->vertex[0]->v[X] - p->v[X]; ay = f->vertex[0]->v[Y] - p->v[Y]; az = f->vertex[0]->v[Z] - p->v[Z]; bx = f->vertex[1]->v[X] - p->v[X]; by = f->vertex[1]->v[Y] - p->v[Y]; bz = f->vertex[1]->v[Z] - p->v[Z]; cx = f->vertex[2]->v[X] - p->v[X]; cy = f->vertex[2]->v[Y] - p->v[Y]; cz = f->vertex[2]->v[Z] - p->v[Z]; vol = (ax * (by * cz - bz * cy) + ay * (bz * cx - bx * cz) + az * (bx * cy - by * cx)); return vol; } int ConvexHull::VolumeSign(tFace f, tVertex p) { double vol; int voli; double ax, ay, az, bx, by, bz, cx, cy, cz; ax = f->vertex[0]->v[X] - p->v[X]; ay = f->vertex[0]->v[Y] - p->v[Y]; az = f->vertex[0]->v[Z] - p->v[Z]; bx = f->vertex[1]->v[X] - p->v[X]; by = f->vertex[1]->v[Y] - p->v[Y]; bz = f->vertex[1]->v[Z] - p->v[Z]; cx = f->vertex[2]->v[X] - p->v[X]; cy = f->vertex[2]->v[Y] - p->v[Y]; cz = f->vertex[2]->v[Z] - p->v[Z]; vol = ax * (by * cz - bz * cy) + ay * (bz * cx - bx * cz) + az * (bx * cy - by * cx); if (debug) { /* Compute the volume using integers for comparison. */ voli = Volumei(f, p); std::cerr << "Face=" << std::hex << f << "; Vertex=" << std::dec << p->vnum << ": vol(int) = " << voli << ", vol(double) = " << vol << "\n"; } /* The volume should be an integer. */ if (vol > 0.5) return 1; else if (vol < -0.5) return -1; else return 0; } } // namespace AlignmentSubsystem } // namespace INDI libindi/libs/indibase/alignment/DriverCommon.cpp0000664000175000017500000000047613263645557021276 0ustar jasemjasem/*! * \file DriverCommon.cpp * * \author Roger James * \date 28th January 2014 * */ #include "indilogger.h" namespace INDI { namespace AlignmentSubsystem { int DBG_ALIGNMENT = INDI::Logger::getInstance().addDebugLevel("Alignment Subsystem", "ALIGNMENT"); } // namespace AlignmentSubsystem } // namespace INDI libindi/libs/indibase/alignment/crosshair.png0000664000175000017500000020042313263645557020663 0ustar jasemjasem‰PNG  IHDR5okµêJsBIT|dˆtEXtSoftwaregnome-screenshotï¿> IDATxœìwœÕùÿßÓnÝ»vÙBï ±`A± bï=kŠ&jŒš¢FS4ÆòÕ˜ägb&Ä^° ¨Á‚ (X@A¤,°°½Ý½}Êùý1wïîÂî²Ë.“ó~½†ËΜyÎgÎÌÞyæyž£TVVŠD"AëǹþºkH$‰D"‘H$‰D"ÙS¸ñ¦[ñz½x½^ü~?>ŸeãÆ"‹Å¸úª+()Ô¶—ÒûŽz·‹Òë>z-i‡º·° ‡Üƒ”]·Ýã>¶oÒÿ»ó&;iÑ·Í=Û« #}û íh­ßìwaLéÏ:µßÆ»ÐG¯¿zÙ´o#ÓÍÞýb'}öþ+°÷=uqÍö¯ý.·öÝ~¯Lõ¼Ï^þièuŸýg¿sëšÛ­_»ùbÝ‘ù§ãª~µßÙª~ìEtûc¶ô¦ÞYéak±+¶{Ù§èçsÛIŸb÷uЉýþíLìðŸn[í&û»Þgÿ Mç;ï°¶_…»ü¢è'û]¬í§>z~ÉìZ‡}»$»oÐ#E}¸fwý]èiûèÕèõ²]ÖžïµËÚÞq[eà:7@å÷Ü)É$ó^y™¼¼Ü]íB"‘H$‰D"‘H$‰d·ÓØØÄ¡‡Ìp#5Þ_¼XÜpÃµäææ|Óº$‰D"‘H$‰D"‘HvJSS3W_}-j"ÿ¦µH$‰D"‘H$‰D"‘ôŠh4†rÈÁˆœœì]2 „ÀÂqsÎD/…(Š’ÙÏý¸I»î§¢¤kQ¤Ûµþ¼S;B€¢ (  ¢i*Š¢ ªj—v$‰D"‘H$‰D"‘|;hn£»äŒp„ƒ°LÓIJml[dœš¦ k†®CÎ!„ëº!LÓÆ±mÇÁ5í€P5 ÃÐ@QÝúdÛ9$„H×-Nì訚†¦ºéØH$‰D"‘H$‰äÛ‹ÞÛ„8¶À´L’‰$'*aÿÒ|†å†(ÊòS‰SÞÔ²­ ¼¸n>Ÿ]×w°A:²Â2-’©Æ £´´„‚‚äååÒØØD}}[·n£¼¼¯ÇƒnènDm‘(®=˲H&S”””0jÔHróòee‰DhjjfýúõTTTàõxÀc i¤í|£Û ihÒÇ"‘H$‰D"‘H$IQ9ø eõ¨±ÇqH¥LzTî8t"%Y^„cƒã EUAUQTm‘$7¼ûµIÃãÉØhýL™&†a0ó°ÃÈÏÏŶlÛÁq,TUGÓÜ´‘††&Þ~çLÓÄcÛEjLÓDÓT>øŠŠ qœÎíÔÕÕóÞ{‹I™&>M×zïÔ6ÑÆ¢6hÁù гNˆGAØ&±h‚xÊÆq=1è†Ї_WÀNÒ5((ÎG6ªtlH$‰D"‘H$‰DÒZZ"®S#++Ø£á`¦,J<*÷Íœ€_˜8©D—íU¸bpÅ¿WQc“‰Øp7²ÂÐ Ž>zš®`¦L·.‡Ó– £¨ †ÇÀ¶ ,IJMtÝ@U•´CÃBUŽ>z¯‘±ƒhs ´·c™¯¿¾GØx š¦ö|Ä„M4’Ç)wþšÓ aÛÓ¿äº×›ñX ƒÏú9¿>¡¨æµßÝÆ““ÿxN¼àdŽš:Œ|`†ÙüåÇÌêE–ÖÛ$˜ÌͽœqTóÔOo浨Ÿ€Ös9‰D"‘H$‰D"‘ü¯‰D{—~"°Ì7O…m ™ÜÉÌ)Ñ0†/À-SK¹ìý èé”3•â°CA‹xÌJ×Öh[Zk^(Š‚e™†‡ƒÚŸ·Þz]ukl8ŽC*™äð#f¢ªÅwbÇÂ0 9dzÆN¯œ‰i“Ì5ÜtB Їº“¬³ÐÔa|çÖ«8:¿]c#›!“'Sô¯§±PŠJ›Ã!ÚØLTó‘ŸçEC`F£„é(4à ôáÓ‰XœxÊÆ²ÓÎÍCN®ÕL‰¦HÙn±UU7ÈÊöãíÛ¡J$‰D"‘H$‰D²GÑãB¡B€e™Ì¤PÄ0›š¨‰¥¨‹›L(h‹öXÛ#ËÐ(ÉòbÇ[”•Ëìâ,Þ¬O¡i:¦mQZZ‚Ïç#‹CzæÇqÒη¾hël%Š¢`š~€’Òj«kðhlÓfРAdƒi;°hÑ;X–Õé1Ìšu$¦ifìÔÔÔ ‹^øuv'ƒ/çÖ Æ`â‹'þÀ_>Ž“å1‰•Âù ¼~Û­<úy‚EŒ›G}ƒŽOs03æŠ8û®9`ó¿¸âÆwˆi†Ÿý ~xèP4 AÍš%<÷È3,iVPílö=ûŽÙgà ƒh@¢â-~wóÓÔO<…Î?œÉƒ| ’4mZʃ˜ËjÛ‹G:6$‰D"‘H$‰Dò_BکѳƦesØ@?V4Œc™T'lîY¾…k§3!?ÀÚ¦8w~²+÷ ŸÛÆŠ†™9ÐÏüê8ªªc[Å%ÅÄãqlÛÆÕàd¢+Zi‹²pŸÄÇ¡¤¤˜Êm•,Û¦¤d‰„k§u×Y³ŽØÁ΢Eïbš&ŠâöUVZBå¶Ê»+Òh¶•œ}¾Ã­£F‘…Å×OßÁoÔãË2À¶‰0q Dc&¥tý *R lXÓ‚ß§£:NDZmi¢Å‚Tm GUމ@‘¢¥ÙÁ—¤pÜL~pCŠM×>C¹2€šÌ(À$iøÔf"¡éüø'³§€Ý\CMÊÏ€|±¸¾žŸk‰D"‘H$‰D"‘HötÒa ;ÒUPpl›Á>'C¤Ê÷*ÌåºãÎ—ßæôa¹¼PÞÌUÇÍ`o%ŒÓÒ€ãØ”ý8¶íÎZâØdƒ¤RI·÷íž´[ÓFÚÖ»ûÙ¶E0p ’± ƒ¤R©6âñx;­Ø¶•þ´ ¤íô䨻"8j±%áö—*ðäxQ xªòÈ[ûò³# qü¹çøŸ¼Ë«/¾Æ;›LB¾öýVóü­7òì6Uó’2ðâ°éÁk8ë¯Y9!‚ùûrÍoÏfdÁD&ä>ËÆæÖ}üqóuüu­‰æ5ð”žA¡°Gn¾…5h~²C> Ez4$‰D"‘H$‰DòßCÓOHæ 3ãŒpšj29cÿ½ylñ§\pÐ$&: Xᆶ}Ÿcf"1œôT¬­i"Š¢ðÁK»ìú ƒÈìëõi8B 'í´0M3cgûãiÿ7šÃýÙëMÛéMèBmÓ/ä²OoãO¶h(€á·Xý÷›øþûÓ9fÖaÌœ>‚²©G󽩇2ý‘[¸óÝ&ðµ·¢âËòá×áŒûØë¼«ùþìQ„:ôæ!àÙÞ£à  ª6±ªå|T?‹c FpÙ}÷1ké"^za>Kkm²dî‰D"‘H$‰D"‘Hþ‹èqú‰*ÜT–x¯í D[úÄšÍÛx~M3ç8‰?þ’acsŸãÉlWT•H<ᦒ¨®x,nꉢ¨Ìœyh¦ŽF+­õ5âñ8Žp£3â1×鶉DEIÄTÛœMÕê8Ž#A2™LGƒônÀÚ7¯œ÷o ù.ß’ÃßÿÉè¯ùÛ*“ ×í[ÑU›–òÜ_󯿕qÌÕ?ç’É>&ŸzeïR£½SâÁà²Ë.Ë´m¿]A dôØñ¬ùruu5 XÔ©¦T"‰×ïGQUTUEÓt÷žYQPQQ: ­z2áÐí£Ý;u÷&¼&E8Õ½±…@ ‹T2%ut¡cOaO©cÏÔ!鈔6c™ŸEë›ÅôzÑ.|ºu¿ö?R;€ÊÎÉív{+£GfÆ nºX77u¹mO©cÏÔÑÙ?þÖ½ïßM&ü tØû(Ûßí)R xõÇPýY'V8㟠ûàÉÓÑíuÚ©Ž/µ¯‚Õî^ãð_ÁÈ£á¥Ë öKwÝùó ÞÏ_èþì Áyó i¼0§£QU'ÜX×eŸ{òyùÎyç¡ú<Äcqt§Ã\´9CTÅ ¨¥‡BwÛéºk[5É$‘HѸEECŒ>ßÐm¿‰D"éž]vj˜f G@"£¶®‰€ßÀÐurròvûƒg{lÛB7t’ “ú†¼U…`0„aÿ1ßFÖ­[Ïo¾I8Æ4M,˲Ü?Æ›7oA×u à ;;›YGŨQ#¥Ž]Ô!„ ‰›_°[´ÿ7sÒI'1oÞ¼N·•””±~ÝaÛE feµ»¹mÓê~>øàƒ¬ZµŠßüæ×ƒAþüç¿ðØcr饗nçÌP2Ž`Vñx¼KŽpŽpP G8„B9ý8m´´„ÂAú-£ÔÑ™Ž¾°nÝ:öÚk/V®\Ùg[{Êxìé:EEÜgEᦴ¦´þn¶>–Šôƒ,m™"“~Ðú°)2ÿö÷x‚ÙL?ìxò ádádá da¦’45Ô°mËz>Yú-Í ;NiÜãOuîý»Ü£ØºÌ]r‡ÁÔ90r–ÛöПÃÏÀׯÙî{ªdÈê:%Úç{Ç?‚i—Áêà‹gÁIÁ°™Ð¸¡Í¡A'æ“-PþŽ«qÀX¨û ¼90þ˜pz·]î)×ig˜Š€d¯ªáõ¸NràÞofé¯ÇÀ‡ßëFcfù]ïFVV¯Å›JBÄ¢¹ÅKeÜ@1ûörJ"‘H$»èÔˆ'b,]þ%M‘$ÙY!B¹Ù¬ÛÖLcc"fÿÉc5jtkÝáØ¼·äSª[È å›ŸKmC„ºº ÑÂÁûNdذ»]Ç·D"ÁÂ7Þä‹/¾ Mqq1%%%!¨¬¬¤¾¾žp8LKK Ï<û,'NäèYGáóù¤Ž]Ô!SN:gû4‘ζw†?À²ìÌk9Ó²P¥ÓH !ࡇbÕªUüâ¿ ?í`úÞ÷¾ÇC=Äü•Ë.» UUÚ9DÚB•S¦Ù½>  g €²2/••Iº pœt^¹p:ŒMêè {ºŽ¾ „૯¾bòäÉ|öYgoÁ{gkwGAAõõõÖíéç¥+BPpœtÍ„¶öm¿Ëi#J»ŽD›³CˆÖ}Ýßa¥­C?ÇÈqS8攋ðzý;lÓu ‹â²ì3ýHj«*¨¯ÝF$ÜÔ»ëtþÕ0ûÿ\'€,N;6šÊÛœëBÉ48è'°ïå®ã‹g ¥Æžà¶Ùü~Ž«S¶|Ã…)ß…½Ï…úµ y r9ør!ÑMäǺ…®ScÚe­†Q³A÷î´Ë=å:í C(h˜¨@®ÏÀkTÅÂëñ2TüŠM‘GÔ’$½Ùh>ƒlJHK‘g[ø’QºC )Ó&Øñ:’H$IïèµS£¥¥™—Þ\ÎŒƒ¢h@6¿‡p$•µ ¼ùÖÔÕ×1ý€w‡fL3É?_|—#fÊÁ…ùXÂA8`»Î}*ëyuÁ|¦ï]Ë´iì6ß6‰=üµµµ„ÃaFŒÁŒ30`~¿?‚oÛ6‰D‚p8Ìo¼Á† H$lÞ¼™K/¹¸Ï©C²=ÛßLžrÊ)™ÿŸ|òɼøâ‹Ú¨ªÚa?Û¶qÒKÛ[[°,“G}”Õ«×pÓM7’——‡ã¸^ŸÏË%—\ÌÃ?ÂÃ?Äw¿ó4MÇ­Eç>X)Š’‰ÚéT»ã>@¹wà!z–²ÐýëÅ,Zô!wÝõi—m”t_ÑᆿÇ:N? ÊJ»1÷_PÛuxx¿éè#P_ߵ޾’J¥(//gï½÷îSÄÆî}÷Ý—“N:‰›o¾¹Ãúoú¼ìªŽV§E(»ë¡Á£'0tÜ$9ñb‘0+½Ö¡M4ÜÄ'ï¼N¸¹!Òåúi<†šÈñg\î~Gììø•Ââ!¡±¾ªwש…ù׸Žq§ ,¾ÓÕ;h Œ9Öâ_ƒªÃÈ£ÜÔ”½Î†‰gBÅR(ÝÏM©XºS­]òæ/ «FcŽƒÂ‰îú gºK*M›ÁUƒÃoP‰»øóܶƒÓ÷‚‘j× ³n!ܽ¢Ë.÷”ë´3Zº®“ç‡Â, ¿b# A B†BPÓáOáq4‚^‚äym 4Å£‘j4ð¦¿Ma–¯W¾ìH$’¾Ò+§FcSï,[ÃGÏk´-/§ÿžþÇŸiŽXd‡BÌ<ú–½ÿ&#†WSXØyz_ˆD#¼±xÇÎ>¯ÇC,•BU5„â>Ĥd‡B5ûT–¼·€aÃë)ØÍaÿ¶móûßßÁÂ7Þ ¸¸˜_ýêfÆÀê5k¸õÖ_SYYÉѳfñ³ŸÝ€¦i»UOWÌ}ÕÕÕ$“IŽ9æöÞ{orss1 ]×Q%ó¦Û¶m²³³9ûì³Y¹r% ¸ûÎ}§žr²ÔÑ:þçIß¼¶çÅ^à”SOÍüû6Ûÿ¬ª ÍÍMƒYm7¨ 457óå—«¹þºëÈÉÎA8¢-Œðz¼Ì¹ðBzøašÃarrrÜJû¸oð èZ×7ž­¹Ú#ŠU³ËRÎ>œgËËiî"wûò˦ªªûbu¶ã ÒÒñÍsOtPV /¾¼e=û,ðîümj¿èØ àºëàÔS!€Áƒ»ÖÑ477SWWǤI“øüóÏwÉÆîÃ0˜3g>ŸýöÛ>ú(³í›8/Ñ[ŠÀ~³ÎøÑM (.#ÒÔ ³s™qòy™íš¦1fêt.œ:Àµ-ÜHþEQ9ò„ zäÐèÍxtŠ'ËuÌ¿fßãÒc>ûõ;pLxûVÜœ ¾žï.%Ó\ÇÆƒÝö©(Œ8Êu&Ø»X3R Ÿ< [>„S†º5n­ŒÂ‰,„ n;Ý×–ƒp£EÌä„åÁ'Ñ“4˜=å:í Õ焉ϣà÷* ËVî3ip ¨ ¼Â"ßï0Èã:Áó} Bz _È>0d$Žb:8-I AÂðà ~3÷ƒ‰DòßD¯þ2WW×àÏ.ÁQ4TtÀaÒäÐ4 :𦢅†S±µb·85jêëðäã8¶š¡¢h]usq *ùš‚讬ÜíN^xå+VpòÉ'³zõj®¹æ'ÌíU®¹æ' <˜ý÷ߟx<Î /¼Àgœ±[õtÆš5_±bÅ 9þøãÙwß}ÉÎÎÆãñ ë:º®wH°m¯×‹¦i 2„ã?žgžy†+V0~Ü8ÆûÕáñxØgŸ}Ðu}·è?~< ÀëõþGuHÒ9ï;ÙÞ“õEEŬY½ŠiûMǶl÷ͯ#ÈÎÎæg?»áìàQ…@ÀÏ¥—^‚ÏçCCº¨›ª²ú‹ÏTZÖ¥>'.ÅáÞˆw¢ú¥Ÿü„#9¿ü%3n¿½S[[·öìÁ£õeæö5z¢€–„Ã;®ï&"e·èè!¡üà§lí IDATpÉ%0r$hüå/ÝëèŒqãÆ±fÍš^õ]YYÉìÙ³;v,Ï<óL¯µïŽñ8å”Sؼy3‹-âÜsÏeùòåŠþ§ÎËÎè•EAtS¨Ñðú¿ÿ îüþé|ýiçÑÁP.ÿï½õ@[ÚŠÛG/ttÁ˜‰ÓÈÎéÛ½DO¯SÆž+ÿÕ±qlÚ±1üpðfÃ[7º‘Û³m¹»œ9r†€ásknìÿCXý|Ÿ´gÒY>z¶¶9ÑÐ}pöÓŠÁü«\GJ*êh°ÎyέïñÉ£=êfO¹N;CIDÑu &®3bx ޹©«Yz¿báÍÊBIEѵf(„RœCk¼‰fÅQH¢x³0“^#¶L"‘Hþ7èqÌ[Cc#Ÿ­«bÈRÇ!¥‘TÉRÀÂg;äø /[Åc¨ *+aÁ;ŸÒØËJÛ;£±©‰Wmaäð¡X dåjäUò½ 9äy`€вU<>Ò¡¥Ì{c)Mý¬£Çqxûw¸ï÷#„à±Çcþüù”——sÑEsáœ9”——3þ|{ì1,Ëâ¾?ÞÏÛï¼³Ó*ÛýI2™dÞË/‰D5jÓ¦M# ¡( ¶m£ë:š¦uXt]§¾¾žx€õë×3iÒ$&L˜@$aÞË/“L&ûEGNN^¯×MpÇÉhH&“Äb1lÛ& …v›Žh4Ê»ï¾K4Í8sÚ/Š¢L&Bà÷ûûEGG¢kŸæÚcãÒkøÏ]{­ÑÛ-/<ÿ|—Û¶gøˆ‘D[¢|¸ø=ZÂ-蚎Çðàõú().ÁãñâñÇCNv^Ãð i-áf>|ÿ]’ñd·õyZkp ¶AØn\; ¿´…ü²2ºÝ=޶ßY€èè5 Pv¿ŽqCà«áÝ»`ÑàõÛà'Á¥—À•WÂãÃØ±°e TTÀÏÞ½ŽÎ°,‹3Î8ƒsÎ9‡Ñ£{^ûéÑGeÈ!\}õÕ½.BÝß祰°Ù³góØcñÑGF™9sffûôúè†Þêpl«[='\r-M5U¬ÿü£.ÛCS]Mº.ƒƒpì~iÝ«ãßžÞ\§L8ÍM+7Úáƒÿ+é:4꿆ow½oÙ3Ö¿ sOvv &]°ëâu¯…ÑR [?î¸ÍJ€c»}DªÝ“V'D´ÆM)ž Ùݤ¼µcO¹N»ÂÒPÉ÷d4L[£x€AQª‰üh Þd=Cs,Ÿ†ž@õh–ƒ'G‰¶ ð7<CÖÓH$’þ Ç‘Ñh„hJ¥ºÅdJY6ç_ð&NÙǶ(5ën{GQY³r9sÿõg</1[§¦ºŠ¼N¡×âñ(- ¨jH±ßà ³gŸËÔ}!ÒÇ•À‚ÿÀãñ¶Tjª«ÉíG‘H„3Ï:‹x<Á\@}}=%%%<ÿüó̘1ƒI“ܼ_¯×Ç+¯¼ÂŒ3²³³qg·èhjj¢¦¦†¹sç²ï¾ûrÈ!‡tØgóæÍÜ{ï½TTTpÞyçqÄGôYG+vóZÞšûW˜»ŒZ`׬|»é÷pª¦1ý ƒY·n-KÞ§ÛKv†¢(øü~† ÁÈ‘£3Óv†pÜB¤™b…é´—m€ks–,á¹>èóñfîû·«°3½âXàû€H€Û€vu)ûKÇNkN„l¼Y(ºpöÙÂÆÚ§üJPU^C‡ÂñÇw 4éLGg¬[·ŽuëÖ¡ª*ûï¿?Gy$–e±hÑ"Ö¯ßñÍ·¢(}ôÑŒ3Û4ÀÏn¸?Þ?ÍÍÍÝTÆ£;.¼ðBæÏŸOMM sçÎ媫®bñâŤRn¤O_Ï‹¢(x<*ÉdÏê|p1BÀTvXß; ޽ã뢪Ì:ÿ‡œ|ùµÜrþQݾ3u:¿ø¤­Pû>ƒJ‡ST2´Ë~[šÐ þ@÷Ï{z#Ž„u `è ˜yhDª `4̸Þ»ƒ¾9ƒaæÍ®dùƒî,$Ÿ=áF} ;ØÅha‡»)1+ŸÜ±ÏñÕËn]1ÇÃÇÛió~ÿë==/L”F¶a1‰âÚhyÙøG †ª­²)e¡å†Ú؈ӒÀ©­ƒ¤…¥Øš›æg;»<¡D"‘HÒôø›Ô2-Û¡%îPÛgîÜ?bð ‡ßÿã#îøÙRŠ‚Â…„S6A¿;5¢ÿ™) ášUõI¼ú8†p =)é¿{B…ïÑl9d44wk¿ê¸íöÛ)+Ì>ûìƒÏçcæÌ™|øá‡øý~>úè#6mÚ@MM ~¿Ÿ#F0eÊV®\ÉÙgŸÍŠ+¸íöÛ¹íw¿ëwmÛ³ióf’É$¤¨¨Ã0PU•X,FKKK‡›ÄmÛ¶ñ·¿ý•+W‰D°, Ó4ñz½ƒAŠ‹‹©¨¨`ÓæÍ½w&t¢C×uÇ!•J‡;<ˆ*ŠBss3õõõ¨ªŠa»M‡ã8Äb1Âá0¯¼ò Ë–-ã¼óΣ¤¤$£%S__ßoãáb²õÕ?pÿûƒ8ó×7ðùoï ±›ÖVÍbþvûyáãJ¨d>Ûÿüc&ù³Šw¾‹ÿ÷ÂR*b*9£ç»×^ËCé°¬ÛÞ~ˆ»ÿúË*bàÈ~ßÍç ç¹‹È‹í·Šª2zÌ8F×Âz*ÃÒ7⎚ž^ב·++yûé§ûÞŸ#@é>Ûú驎18¸øð 0¸'½Îé?Wœ לBÔœ|”`Åð€ã qFyþ– ~ö”ÍŸþßÿ>ÌŸ¿óñèÇqX²d K–,Áï÷3kÖ,Î?ÿ|xå•W(//' qÏ]w±ßþûF©¯«£ººšÍ›61ûØcyò©§zÔWž—)S¦0dÈî½÷Þ̺p8ŒÏ磠 €ÊÊÊ>—ââ _<¼ læ­·¶tù+ª( ?üá$Gðᇕ™v½Õ!„@UüY!’ñ¾@{t'\| ƒJùíEÇSþE×&EáÀãÎ`ѳÜ´”tîI¯ÓA¥Ã»ì÷«/>bþs!„`à Áìw𱌙¸ïŽÐ^]§v¦^£Žq#/ÍðæO fs›ž‚ïýžŒ“AÕáˆß¸³’,ý3„·¶Ùs,ØðïîûìŒý¯€/ŸsSO„“ž¶ ºrün~ân±Ñåí4ê¢_¿Çz@o¿?ü^•?gë-An¸eSF–ÀÄ1­²‡ ä B!‰S½Pâ(µõ`¦†æè(¦@¤dú‰D"‘ô•;5<^/šˆ²lI›xÍ}­¥ê+l{,7ZB((B¡©vÅŧö¯`OaZѸI$©áQÝQE„¢`CºÄŸJsm9Åŧõ«ŽO?ý”… ß`ôèѼú꫃A‰uuu~øá¼õÖ[lÝêÞPƒAŽ<òHžþy–,Y‚ªªD£QB¡Ÿ|ò gy&S¦LéW}Û³eËR©$™LbÛ6¦iÇ3N ÇqX¼x1Ï?ÿ<•••455‡33†ÇãaèСlذ-[¶ô‹ŽÖº­³‹´¾il%ÓÐЙ¦swéˆÇãÄb1"‘‘H„††~ûÛßrâ‰'rôÑnØq4¥¾¾žd2Ù/:\ †œû7槢Ėñ‹îüo¢‰÷ÿð+þµu×ßs£ýQªªuJ=€ˆòéýWò˃¸à§dFa3Ký=÷ÿôn?õ+Ì4/¹‹Ü´œÙ?ä×׌$¯!2h ßt™²ôK²]ÚoOÀi}͇“v Ú»æ¤é!Bqܺy¢µïÝ ã2àçÐÁÃö&p ®ÃãþÑ¡kpÑQ æ¢å¢fç¡x}ÛFÄ"8^{ Œ³ðÒ~z=üm»—½]éè)ñxœyóæ1oÞ<È駟ι瞋ã8”•– ‰D"8ŽC"‘   €¦ÆFrsrhêA´F]×™3gÿûß1ÓS {<®¾újæÎKe¥%±«ç¥ ÀÇwLyy ?ÿù˜¦ÃAsÝuûíáùç;©á!øÕ¯–à¤ß°gÖ÷Z‡‚ãØ<øa…[¤YUQ?ûw~wÑq$c‘nÇgÊÌã;u:÷ýä»i]NÛ‹Ž>^§‹»ì÷«•Ë2i µU[xí¹yãåÇ)X‚nx°Ìwþ겞_§VrCN™ëxÿ.7•`ÁOà˜»]Gƒ¼›vlìííò\õd÷ö{ÊÄ3ÝYUªW¶iØžîŽÇ±`Ý|Øû<˜üWc7ì)ß§‘¥ ò}0Èùv3yf#š•@³Òûyˆý/B ŽD(^”Tªol~ «¦5j¢dçâÍ)@$ü¤Äì݆"‘H$ÿôØ©1¨¨ˆ‹êÆ&œÒ,Z£ ™2B ) PP…º¦0!#Ivvv¿ TXÄÀ€ÃƆFF—eá8 ª®CCUqC š¢R×ÐD¶'Iv¨ÿt´´´pÅ•W2zôh¦OŸž),¹iÓ&æÍ›ÇÒ¥K™9s&‹/àCaéÒ¥hšÆ¸qã:t(–eej0\q啼>>¡P¨ß4nÏæÍ›1M“’’¼é™ *++Y¼x1ÍÍÍTWW³`Á¶lÙB$¡¹¹™ššÑ4 Ó43µ%FÍÂ… Ù¼ys¿èh]dýúõ¬[·ŽÉ“'gÚÛ¶Mcc#555é·xênÓ±qãFÞI×:q‡`0HNNÏ=÷Ÿþ9‡~8---ÔÔÔ`YV¿èÈ ¨=‹%rÔ7¤Ð ÷b¿}&0Hƒq{¥75|À#/×3õgå²Y¨ÀØëky÷ì¿ñòšN óæƒóiÚëzþvÉ}ÓžŒôü {GöŒ©ðZÃ¥qëÛ¡Ðqv•ž2áØ3Ñ}~>ññîÚ­Ap;NQÙcgž©NŠ’æ~Z qæ·¶Fx§tœ| ËÓQ²rQs P ŠÐ¼Q´,ާ=TP-ëß׸ÿþNÒ!ºÐ±+ÔÖÖòÀðâ‹/òÒ‹/ÌÊbÀ€x¼^ñ8áæf¹¹¹Œ;–%Ë–íÔf]'tUUU|üq[Mƒ‹.ºˆmÛ¶±pᶆ»x^Î?,Á ÁçŸ×ašîïãÒ¥U$6ç7–?¬bÖ¬ÁdeyXº´Š òI&m**"L˜ß¯sÿýŸõI‡Ì™6¯/€™JrÞu¿#•ˆgªªvš~åpñMÿGùW_pÛ³ïó—.sûtC7û|¶:':#™Lì°ÎL%©Úº±ãÊÞ\§Þô=˶°ðúíŒÇáõŸÀ±wØtñέË`¯³ Ù ‹nÙi4DYõ$ì}®ëÔ(ÚŽÿlü·;ûIófˆÖ¦vq<þÈ.+ý]³ïå;í²¿~_zL/΋ףíQÉñX¨ÙAÌGÉâaÒi¨þÉXzÝàÍÆ |CÆ 78 )E¥6ªPÓ¥.b÷®8³D"‘Hv¤ÇN EQ2xEXhŠJK\à)¤üæÆïA*†hªJs–-x’³Ï<«ß+ŠBiÙŠt¦Ówðg+™ çJ:ÜTÓTê£*Ë^ÿguN¿j¸÷Þû‚”ò÷ÿw?uMÍ„‚>N8éT¦M›Æ[o½ÅªU«255V­ZEKK Gy$©H3ÿwû­´D ÈÍáˆc§®®Ž{ï½›nº±_u¶§µ'™¦ ªªŠ/¾ø‚H$BUU6l ¦¦†êêjjkk©««#??‡}[§8í/BˆL8 Ó>ÓÜܼCîúîÐá8•••ÔÔÔÐÐÐÀ€(,,¤¨¨ˆfQáp¸ßÆ£×h…̼øžþùœwÁÛœtú™œqÂþ”ùR•Ÿ³É²høí)öÛŽ»yjã8‰-|\eN"rh¸çW çö÷ô»J«CF¤ß.:B…^”zÝë¤ïÈ ‘ˆEðø:©È¹î„ˆÎÂé{ ã©gÀçëÜø" ¦¶WàFj܉›~²ø °²Ÿt#ŠA5¼àó£²P=¹hÞA¨Æ„“@ãKŒàÇ$¶ !¹¡Ø1T»+}¡´¤„æ¦&<µµµx<ŠÓ…^kjjðú|äçç÷ÈV_¯²²2Î9çöÝw_–,Y‚Çã!•J1cÆ öÚk/n¸á†íwõ¼Œ“ÀÆmÅJl[ÐР¤$HNއp8Å©§Ž$Ð t¦NÈE½É¹çŽ¡´4«ƒS£÷:ÜÇW+•ÂJ;ÜÖ|üÇ]x%¹…Åœ~åì{øñÜ|îáÔV´9 T]çÊ»§bÃZîüÞ)|Ò¹\sß?"–I=íëuÚôê:}òT˜ñs·Ev„+:n·ðúµpÌ]®ccÔ±ntÇ‚ë¡yW£;aùƒ0úxw•úõnÁÏâ©u4ooh>8ð»EA³ŠÝ£­´~_¿ðD—]îÑç yjŠ,=cEñ)" x4ð ûAó#¥yjK»¥D'œ âÍ'*|4¥Ä“)¶tjH$I_é±SCUUÆÇŠËY¶x#C&†^¦S¥¢Z lUU¥6¯>û†hfÒ¤É;7ÜKTUe̘1¬\ùKV}MÙ¤™èºFAPEA  ¢jÔÇ4^xòhv“öî_ËW¬ ¸¸˜/>Å´}&3$?DÜ,^ð{t$BJKK3i¥¥¥ÔÔÔ`¨°xÁK7cüºÂ憼øS=šå+ºÎîÊÊÊØºu[·n¥ý\캮“MQQ¿üå/y饗xøá‡Ñ4 Ã0…B†ÑÁù°~ýzE¡¬¬ë).{«@Ó4šÖöÄ­( º®t|ÐÛ:EÙḽ^/^x!çž{.åååhšFvvv&e¦¯:zJþ!7ðÄK'ðïçþÅù)Ï=q87=ø+A½ñ>.ÛîFßÀTOˆgQõW0#“HUç“0k;µÑ•޾ÐZÏÇçó‘—Ÿ™J±níZZ"Ü¿EÞ΂²«×GQQg¦ÓçÍ›ÇÃ?ÌÅ_Ìm·ÝÆ“O>Éœ9s¸í¶ÛˆÅ::zúz^b±ŽYšæ^û©”CS“aøÉ'µ¬ZUOYYMMIš›S”n7¹Eïu´=T–ŽϘ)0tìÞŒß÷þ¼h-Ëÿ=Ÿ/–½Ë5÷Íåæófb%“xü®øÃc ÎÍg†c[¼÷Â|þÞBÖ|¾<=¨Òçë´?èÕuot‹r9ØÀøàžÛX Øø šìÖÓ¨ú j¿ìÚfáÄÞ‹.;ùn¡Ñ¥‚¬"·¿Â½ÝÈÜaP®%eøab»éê­¤[¤vµ»ør`úU`쬘êž{^¼–æ¥AÓÉózñ+4Ö'‚RU²å™îä¹Q,ᵈm+P·lÂÙ¶ Ëò#r#Ô§tb‚ƸIÊÚÿ(K$É·Œ^•\öxÉú|¹l!--QÊËË=jªªòÕÚµ´´´°~ÙBö6€#ÆæÒ\³• Ÿ¯Dwb”¯þŒ˜Ø½eÌG}LUUU¦†„‚@ À€2©§vS¦LáÖ[oeùòåø|¾L1OÛ¶±m› 6 ª*ƒî­Z¼^/yyy™tp‚Á yyy""v‡UU äää˜4iwÜq£FÒ³aø| 0 ßÆcW1ò'rÌe¿åÈçqÕÙwò¯wÀá‡Od°ú"_oT(>fÞíw²J™\K>øŒ†ó†îQé'Âî´z»°_‡Ÿ…`sùF<^/#F& f¦-®¯¯§°°°[{Ñh¿ßªª!ˆE£l­ØÌ–ò >¢ËP2yØ"üæ€#v~àZÀüw&”>êÈt³ æ™$d™`š'c×£‰§Põ¥XñýH6DX[ˆÄ:£Ù•޾P^^NcCC&ý®5:"‰`š¦{M5vWÒ·ÞŒ‡¦iL:•#Ž8‚ñãÇóúë¯óãÿ8㸸ûî»™={6×\s sçÎítÆ–]=/_~YOiiAƒ|ñ…ëêòx4 |D"&[¶´0dHÛiuuŒêꮋöV‡Ò.ìès/c¯é‡³jÉÛüåg—²âíù4×Ucxýüæ™÷8ï§¿çíçãÊ?<ˆ‰Sxâ7 ·æºjÇFEUôù:UÕ®¿0{Ôëë´r4¬k›5$Õ®¦ˆ€C®w§Y5cîT«%ÓàÐ_À»·í˜~³nï‘ÎŒ>Þýüê÷3R ëºKF‹ΘëÖÎXxƒ«3qÓdÚ{#tL»Ì­Ò »ú}º«ôæ¼' Ò÷) GÅ „ÐS)DCëëUèÍU(!±µ‘hA˜!Tï` ¦fÓ˜0ˆ$lb AÊR0¥SC"‘HúL¯ç‘òû}œp܉ì3e‹ßwŸYH*•" 1nÜxn¼þ§|ùå—äæærÐA2þ| òòûý-¶ßïã¸Ù'0eò>¼ÿþ{¼÷ô’)“P0‹1ãÆsÓÏ®OëÈãЇòòË/“Ÿ›×/žC† !ÙÒÈ€äf ‡bíÖ*´/—’hr(+›@“Çâ¸Ù³¹âÊ+P•ûï¿Ÿ×æÏ§¬P'Y÷%+•SLšq2ö{oÎÉg@(¯F¥k†‚ªªTUUÑÒÒBnn.¶m“©Ñʈ#xøá‡yüñÇyä‘Gðz½™Â¢7nDUU†Ò/:Z ÞeeeQPP€ßßqÞöœœ4MË/|ÀèÑ9õÔüúë^<““Nê†Ãacþü 8ž’¿[¹élŠë}†a”ümÕ5GÑ–ÞûÁ`ù÷ŸñÙËeWsäñÂÍÓyö«•œö׫ùæÝóöãã¾ÿ|Åî„xb=º:‡Q|Ÿ¦Á±¼N:vެòºÜ쬪oXJ½^§ë?‚“€çBüÞË:DÁŸõKÉIïôO™úèw†w›Ò… »?LyÚ;¿E]uç ôHRÕÛ8ó¼ w!¤ï¨z;W>lÿœSí.«|^nÅû¦µïü>Ç"˜ ­Ûóâo8l.À¿éÄióÒ6¨èµŽI¸öíÆ€y8 £0 üÃ0‚ö‘É, Û ©¹šS@z®‹<³iÌ %"ÒœÕkqlzõêU¦@Pú 8²{w¾øâKn¼é&&OÎæãO?fÎÕs|ÒÐÚäøôãO¹óî»ÈÌÌä£?æÚk®9æӦͧŸ~ÆØÉѸ÷Ç“}$•¶a¡ŒÅм>œØÓG}¤dûG}„ÔÔT\.'ãŽ&uW¡®l\;—2|ÌX~ÛYÀ´iÕsq¬úöíKxx8à›o¾aÖ¬YŒ;–qãÆa³ÙèØ±c™íW]uÑÑѼôÒKøùùQPPÀ§Ÿ~Jvv6]ºt¡oß¾ ’# €   ž{î9€2EóÏ?Ÿ©S§bš&ááAB€´ IDATá8NŸåp8 :”ë®»®Òû<î¸ãxõÕW),,$00°ArÔ™;‹¤ØÿñÒ¢]äucä_äî“Úa3`ôm¯òT»—˜ûÕ?¹÷=7ØÃésÊÍœ|ö °ÑþOðºk./¾3ŸGæ‚JŸ³bÒ¤HÂ-ü\Õ`Eƒ)œ0xhÉJ9ÅʵUüÍbñùºäxo³Å­E?àM2oÇKÀ-¥o¬løðòËpóͰ -|tÿ>zô,$¨k ¦+܃ѸóÃ0s÷áJKfùÚ}|SùGU9ê#$$¤äÿK–-£CûötïÞ½¤ƒ¬°°ÌŒ ~úùgRW:p§ò|µx}\xá…üøã|ñÅ5ÞgU’ýÕãyÉÈ(äÖ[çÒK£¸å–ádeÒ¹s0>ºŠØØC„†ú1`@7fàÀvlÝz„]»²4È;aè¦M‡™6­ÿûßÎzåðLj>Ø?˜”È=ç#/'›#)ûxõÞk¸õÅ÷yð¯b׿8Ün6ãh§F]b»õ¢}§nUæÉÊL«1sÉþêú:Ýþ3œx½wXÇ–¯aÀ4=l~°k ,}r‹^‡?Þ §=[±°qÒ½Ðaì[ƒwÆß:°Ùvi4„­_×\Ô¨êy98˜·*{HÃ;qqdYx?ݶÂðNnì´Úáø¸cÑùÌú=/¡ö‚pŠápî#¹Ø ²±€û ›3 3¬Ù¹ào'ÛÌ.¿ìÈñp0ßÑ›º4DDH½ŠÅJJëÖµ+“'Mä­·Þäê«®&77·ç½ÍÍÿwsÉ\ ©º“&Mà•W^æ†ëo 77Ÿwæ½ËMÿwã1å¸tút¾ûî{V4™ÉÞ½{Ù´i‹-*ÉXÕDƒõÈa³ÙhÓ¦M™ù;À{€T¦CÃW9úõëÇ)§œBhh(n·Ã0Ê<6›­¤³¤¡rT|"Oþ¼¸êë]8ãá÷9ãá*®÷ë¤ÙO1iv×ô<í&^8í¦c Ú°*+j̺òª Û•ÿÛ*_ÔÈËÍ!$4´Ô|-ÞYôív×\3‡¹sçòÐCrß}÷•tldff2w††rõÕWݯwÒÂâÂHHh(yyyÕæ/:WôŸY´†dýµîÏM3ÿANN./½=…ž#¥wˆix?†—Ÿ# ¦ÿ–º¼˜ ÌÃ;ÏÆµ YÏ6¼÷,X‡C^lw2`ð~ò÷Q°³'¦í6ó iö±hU×lþþþìLJbÉÒ¥ì-Z²»6jó¼tïÞðòËõëÞ)·Ãz¿>23 yýõõ•Þmv¶“×^«xÝÆilÜXÉ*0uÌáñ˜µžógÿέe~^ñÝ'tíÝŸ{æþ—ÛÏB^V&&& Ó¨÷ãÚ†©ç΄*Ö§JK=€ËYÉ*B•©ÏëÔtÃÞUÞBÅŒÿÝrSaù °3¦ì¶®|XX®°qdô=Í;Œå§ûðþ•×»vü\‹œµü}R6usT]\©ñyéTtj(ux^\n7O!Fv„:ÈÇ{Hž|oëH¾éÀpæà0s(!ËåOŽ;œÎl̬ r Löfû³/Ç$=··ÇlBcAEDš©c*jTÅ0 † BZZŸ}þçœsùùyüç­¹\7çZ_ì²Ú©©©,øp^x!ùù¹¼ùÖ\®9†¼7Ï?ÿ?Æ,áóìl6#F ç½ùó8®’a½{÷æ½ùóxáÅyyþWx<BCC˜4q·ß~[™y$|eĈáŒ7Ž¥K—òñÇãççÇðáÃñx<àççW¦@äv»K:#RRRxõÕW dܸqŒ1Ü’›6mòYŽË.»Œ:`F£æoÁã)7÷Ã[s+lW~›òߪ9]®’]éN o‘fÏžÃo¼ÁO<É<@HH¯¿þm۶媫®ò~³k+nŸ71Í£2…Ϊû=EE”£Ÿ‡ZƯʔqWóЫÞo4ãâWóSÜщ=¥>ã—Ÿ# ¦.`:Þ©2žÀÛµQÌÍÑÂÆóx:©N}s<ñ\t|ÿ=ÜqÜy'ìÞ '½ò˜8d !A{š¼ò_É©õËQ™®]»–½Ün7©©©UvÕ´oßžY³f1jÔ(¾øâ ~ÿýwRRRªSi¾šŸ—óÎ;… V[<«ýþêÿúhHõÉQ]‡SH› òsqT¸.¸MÃ&žÂ¡ä]¸‹þV‹ßL[ý¶í:qÁå·Þ¶C•™¶n\SåuåÕåuJÛžÞÂÄñS!¤èÞ0`í›°ápæT~;Ww Ø)Ï6Ì¢!*wT}›ê8‚½+¬Ä{–W³a-^H6;ôþ3ø…T»YSyVÆíÌÅ‘gìçÄ,(ÀàÀ“HaN>ž“<· ›ÃŸ`¿’Ý!dÚ‚Hɳq8׆#ß‚ÂBÒ3 8œcz»4 ì~êÖ9V>)j€·{bâĉ|÷Ý÷,[¶ŒÑ£Ç›—ËÇŸ~â“¥^«Ë1yòd¾þú/^̸qãÉÉÍ9æÜwß½€÷`·ªn‘ÒŽ;î8^xþù:ݦ¡Í¸t:‰‰‰¤¤¤ðúë¯3nÜ8þú׿Š¿¿?v»½ä Ðív“——Ç'Ÿ|¢E‹ ¢S§N̸ôØ»J”CJ«ïð“ò·q»ÝxŠN&f©ÏÙfÉÙY³f2oÞ<zè¡’b䌗–LLOÑ—ŽFÑ|¦÷_—«êe÷LJÝ÷`Ra¢¾:Ú¸u+¾O%;3ŸõÛ)¿Ç£ó”z j›Ã·ƒû=àU¼©¥®› ¬z;«MY÷£GÃ]wy·{ë-o1ã®»`þ|ïã¾xïçº>v•ç¨LuÃ6JëÒ¥ çœs¿üò ï¼óiiµbPiºž—Ž;2jÔ(n¾ùæzï£ÜéõÑpê–Ãûwgß’Q:‹¶ºÑŽ.=§Ï ¼÷ì½|=÷¹£»0 FDŸÅì‡_bçæxþuë óóÊìßÀVçÇ£ßÀ‘œ:írƒª>÷xÜlþ£ºýÚ=•ºhÁÑóiÛ 0º …}kk.N¸ ½+¡DŽ 8œxtˆJ]¦]Æ5}‡· rh3dîñPjbsxWLéíBRs‹ESyVÆï„‚ò³Lò]nœ.‡òÈqÛÈ2BÀB3“O….ç¤fW`PPh’å!+rÜÞåd]ÕLB+""µã³¢x;¦N=O?ýŒˆˆFML̯¬Z³šGñå®+ä8óÌ3øøãOh×®ãÆŽãçŸbõšÕŒi€õ)NXQÐæá‡äƒ²lÙ2V¯^͆ èÕ«}ûö%** Ã0ضmÛ·ogÇŽäääÌ„ ˜qéô Ë«*GÍ9Ї²ørž…款E¼ü\üü%Å ›Í #ã!!¡˜˜ÞIß ŠlŒ’¯œu%˜ï0óŠ™Þý»‹æÑ(º3›aÃÌô4öª‡¬•žÑ4î³:7Þ8–ž=ÛqçßWzýú½_rÊ9Ëpºóqše®+*fRv?uÉá.®b€ExW@Ù]tÝWÀà?ÕüõÉqÁpä,] o¼áíÖ8Ö?‹ªrÔGDDcÆŒaÏž=Ì;÷˜ïj~^Î9çbbbÈʪ݄“5iˆ×‡9¼ß˜{¯[ôñ[Ú» ›ÝÎŽ±,úømº÷ÄØ)çòõÜç°Ùl ›|:Ó®¾ž‡ðÞÓ÷òÛ—óÊ”z<Þê¤QÇg^8›¨Á'ÖøûÅ­ú•#é•/3\—Ç£RÛBò*ï)÷0Dô† ߇Á—ÀÁøªoçÑ@Ï“ ;rS¼Ëžüüöx­³–Xp!ôœ QgAä‰0ùž¢+LïJ({ c·w¿¦ †_a‘Öºy FÑ{§«À;GȶïṪÜeSyVºmA®\if!n·A@¡wû ɶÙÈrùì4Ø’SHZn!yƒ#Ù&ù.œ.N—A–ÛûñÛå±ìïï³ßMD¤µðiQ¼çw.,XÀÔ©§3dÈ08àëÝVšã‚ Îç½÷ßç¼óÎgäÈÑìÝ»§Ñs4ÁÁÁÌ™}5£FŽdþ{žNBB›6m⫯¾(YâÕn·Ó©S'®¸ürFŽ¡õÌaAÁ!¤>LÛ¶¾]é¦9ªOQ#00ˆ”ƒˆˆh§è[¼Î»²eóF‡ÛåÅôx‡ xJZ0¼ÿÌœyàý¶µdˆ GWoð-óa³Ùؼ1ž.‘U¯àTÒ*mxoã1)ÙWU–,ÙÎŽÕeÈu¬ò:éÁ0*¤Õ%‡oÑâmà2à{`ÞîŒs€YÕ¦«_ŽûîóžZe9ê"44”‘#G’ššÊO?ýÔ Q5=/ÉÉɬ^½ºÁöçÝDZ¿>;GéívoÝÈî­ËÜמÄM\~÷SÜûÖ7ô8 —ËÉ÷ó_á¹ÿ»˜œ¬#”gš ÃuÊ5¸æ/;òr³Yñ[Ý'Ьõë4摲?§ïô8zä-dUÒqÔ¶—wÙÖðã u üxfÁiÏx‡±T1/Hõ°óï)¤ô‡@§ÁÞB»@d©Çkt©¡½¦ÇÛÝ‘²¬‡]‹k5¦©¼N+““KJ¶O›´À@ÀI®ÝÓp‘?‡ssÉ÷¸1œnRò3ÈwBŽË§ËE¶ÓƒËcÇSôÑÛã„¿¾çi>/j€wIÎKþòbãâèÖµ#†[3ÿ@xx8Óÿú×’Æ µ$GS1räN8a ‰‰‰ìLJ"©èЫW/zõêEï^½èׯ_ÃL‚ÙŠsØíŽ;® [6LÇÎ]|2inseš&aaµ[•È4M òóIIÙiB‡Žq»½CCz÷éËÊåËX¾d1NL»ví¼E J}œ¯îs½yôÓôp$=Í7àrºè5¢OÕ7óxŠÊlEûòÔ8Â<..•¸¸&‹¨&gñИ íôuÌÞy6ÞÅ;åD 3ð"°¯‘sÔ[9j#00Áƒ“““Ã’%K|ÒMUÓãñÃUk]¿6íç¥Êïpƒí‰ @ñHÓ{´Yô·¼‡±'w©ÛÓí®9`Øð®qâ½£èýÕ(ÆP—‰ÍV}`yÌÿ(ȯ¸Ün}Z[ÿ±·[bÐŰâ_e¯ë '=~AôÄ<ê8`áÝÞ%]ûœR÷}––“[þë=„AP;h §<ê}ã\ò4æ€32÷ÍPMåuZ™´ŒA¾û\#"ÒZ4JQ¼®zÊ1þµå8V;u¦  îÊ dÈ! 2¤R)GUì:w& 0€MãÉ^±´Aï¿9ëÖ½‰[·Ôz{Ã0°;´‹hO‡N¼óg}»f³Û7a"Û¶meÅÒߎiÒEÃ0 ¢Wï>ôíۯ̪<å·-{L“’ Jm—q¨³h ¸Ûã)3±Ý±æpuœ!À'9ꪪ•Ę́ø¾¯5•Ç£©ç(ªaP¼°ëÑyzM(Ö(>kb LÓÀ;ÔŒâÕ, ï*(uÈát`·;¼÷‡÷ºÒsô¤>Hüšßìñ¨Lõ¯ÓG‹NUÜPç|uÏQ™“yŸU¾N]x—rmHžº=/,Klà""Ò­¨!Òš††Aû騩K½º€[ªzMškzÇA»]® í†ÍF¿þè×@¦¬^@@ÙÙY„µiSÔÂì=¬Ú¹}ëÑîR9½ß8›eŽÕ è`Î(>¤+¾Àû­¥ †­ä쬬 ‚Ju )GÙMESy<šzŽ’#Xo5¢ä¾ û$kÉ7êEl€i³ÝÎ<:wÞÉA½9Œ:ç¸sÎ5>ßuEƒ=RV•¯Ó©­óýCDDj¦¢†H#r»Ý¸Ýîš7le<Íü1éÙ«III 2Ì{ex?LEkzçéðnkâýÒѨª¡ºèëiãèço¯’o ^¼cÛ6z÷ë¯Uäh*šÊã¡M3‡”¥çEDDêJƒúEDŽÑÀAƒqòGìÒ¤{'(Äô¾Ã·ÕÞÓÑ/š 'ðy? -kYü¥µa+õ µi’™™Ilì<¦É€'(G9šŠ¦òx(GÓÌ!eéy‘º2&Mk~÷ÝVçiÖ<[6odÛÖrsë8`†App}û÷gÀ€*L8«M³VßTåhš9¤,=/""R[gž9UE i~Îæv»Ñ´õã~âpÔþ*jˆ4ŠôôtBBBèÖ­û÷ïÇ4M«#‰HS‡†HEGM&3=•ä›­Ž""ÒhÜ.u9.—£ì‡wNºÜ@E ‘Æ‘““Caa!=zôààÁƒXIDDħ6¯]lu‘c2!z8Ëbâj}Ë…MEz±ÛíØj;§†a6·ÛDZD¤˜Óéd÷îÝDDDfu©QÇí5ü¤þÅ«ŸÔ†º4D¬sàÀÚµkGûöí9|ø°ÕqDDD|âø!'âr’´¥ößpŠˆ4%uéÐ(fz<6[Éy©{]& UQC¤˜APÔ n¾gýo¯iiiеëqt?ónîœ3–°º–EDHôØáVGhÛúU*hˆH«¤y5êGE ‘z1r³¯˜BçF~ßÉÎÎæðáÆÎ¸ƒËÏ@ Š"b‘˜•:ði(‚R?ušSÃîç‡Ûéôq¤VÀÞ‹¾L'þÃói[t@<þ-–'¦_Åé÷'ÇTÓýô¼à-%¬ã¦¨€2WÙÂFpÉÓ?±hs:ñ‰)üòùkLŽ­’»±…â/|É×&>1µË~噫N$¼xã ÑÜ¿¬òœË^˜Lp…ß·cg¿Æ¼_öx·‹_ϼǯ _p©£q#žç>Çü¥)Ä'¦±øë7™QE¾*Ù{pÉÓ‰ÿßlºÕø’fÔ³;‰_ö,ƒ±Ó¢¡’’g‚#„àÐPŸìÃ1‰??Ⱥy§SfF0}/z‘÷—">1U¿~Ågõ& Šû ;åsbwðtt»JžÏ¢n]E|â2föôk¸ìmÇð×'¾ã‡?Š^—?Á§÷Ä¿ô6í&1ûÕU,KL'~ÃzÞ¼çlºUˆ`#tÐ ¼›ÊGÓ#«x=ö¿ž7ãÓ‰ÿäbÚ©È$"rLzF ¥ßÐqVÇitš,´~uYÒU¾“¿ñYn˜ù€­ÝiÜÿÂõ¸^ŸÉSË3û7Sõº6BúžÉå·?Ìu§õÅFR¹«;ò§§ÿËý#çù¦°2³Sþö2÷¾ãOòŸgó{FÙ¥B=N“Û&>ôu¶e†Ðÿ¬{¸íÞpoÆ}˲ `3ïÌ™Êw%-A'ÜÄ3Ndõ¢msš.Œ°<Ö½}3o&åÒvøåÜrËKü;+ŽsŸ‰§ƒÐÑóæ?g’ýñÝÜöKîù'wÏæð”ËùáP-'¦u§’°>Ξ@À7Ù—ã½Ø¯×l^xõR<~>O,ËÀ°wbà mñl[ÊÞæ¼ ˆ3ƒ€€Úµó'--­aîÓ¯3#.¼“[︚áàYVúJá'=Ï;OMâ+Ws×&ŽÑã‘¿ÆqpO¯É¦ì«ÉAxÎØ‰àÌûoàå“Pêñ¶u¹;çôöÓ%Ü4DÑÔŸã.z’k‡&ññã³Ø’Ýñ³çŠ¿Ç¡)§0/É öž\ôòçÜÜo1¯Ýþw¶u¼˜¿Ýû¯Dó—þ °·Æé×üÛæœB' ª…‘åéù¥b†ˆHƒØ•ouKhøIýظþÚ«M ÆSûnÝÌðŽkµ­NÕœìÝÌ‹¾L7ã?<ßlkT¼ÞÖù óÃÄtsÞÙíML0ÌП7زÝü×´®¦­Âýu5§ÍÛj~ÿöCæ´ó^6IŒ5oŠ 8z}ðdó™ØtóËÙ}MGÑeƒ3LÜdÞ=4ÐÄÑÛ¼ä½ÝæÚï2O¬$oØiæ«›ÒÍ/g÷)¹}™SÐ0ó¦…iæ’m¶·aB€uëbsõß›—õö¯¸½­«yÁçéfüG˜&Fó´wšñ ï0ûú?3Ìw¶¤›_Î>¾ò}Vz2Ìöç|oþ‘¸ÊœÓ§x¿~æqW/5ãÓÍ/Ÿj†ozŠùb|ºùÍML?01‚;üÓ|ç·ýf|bº¹nÙOæÃ 0ƒJž»1î6óùowšñ‰éfüºÕæü¯v›ñ›>1'…bbïnžþÄæ7+öy¯OL3;ϼrTD©çËf† ½Æ|âó361ÝŒÿÃ|óÞsÌnþÞëÀ~æ´G4mJ7ãÓÍ5˾1ç (zmmÌA—½f.X~ÈŒOL7Wÿú“ùþÊt3þófGfXx?sú˜+½·]»ô'óÁó7Àôës»ùßăæë§µ+z=am§™¯oJ7?šÞ½Âë)pèSæ7+bÌÇgM3/ÿ<ÝŒ›wºZ|½aþù3þ›Ìö¢Ëü¢Ì릛«^›rôñ-9šƒÿ¾ÙŒ_oþž¸×|ùÌN¥ölûûF3>v¹,qŸùâ¤Ð’×ó/­«ôwÌ€þ7˜Ï~²Öü}³÷úu?=k *ÿó3ýìGöïŸùmbšùæ”pï} |Èü!q¯ùÂ)ÅI°9ôï›Ìø¸wÌ a˜àoöšó«ùÛóÌ9g\a¾¶!Ýüxzd…ÇÊiÞøõnóó¿ýÙ<õ•dóO.6ÛUò7­“N:餓N:餓N5ÚGFšá:Xž£¹®¿öjSsj46;v»[eýïîý|seg\õ wäb–¿Þy€­û¡ç©S8.ÀŸÎ£ÆÑ!k +÷x¿6·9ü°Ûí–²ljӯg¸±ž—$SñÙwÐ휧˜ÕóÞxîGMÔkØüð³;¨ðʲ·¡Wô5ü%*‡•_­%Ëû2a?‡–üÊÞ¢/ê=©KY´ zŸ<˜ÐZn’µåwöЛû‡yG¢ÏëOæ–d&\ư6Þ;óï>þA™lX½'vÚOy…yÏL#û£k˜uÑ9<ô­ƒiO}ÆÍüƒiüzßÀ«ód\ú{<|ýEÜøà|vÚJ ¸°‡Ó‰tÜúw\}!7Üt =gpÛþÉ„pï>=fòâO1"én¹d ×=ù;]®šÇ¿®êº]ôý%˜ïÆ¥žÎ­O¼ÉòýNÀAçsÞâí‡ÎÇöÓýÜ~õÅÜÿÎ:\¥vŸ•±‡ÕŸ>Ê?n¿‚™—]΋kºqÑ3¯s^¤gò"~ßïÏàÓ• è;…üv²dÝ!ÊÏ­œçŒæy+H+ß$cЦd$»ø†ÎdþHÌ%°ïp:V¾á ¬K8æÖ—yâ£l&ß6‡ã‹rÛºœÇÍ—D°ì™'Y’@ûöÁÞçÌÎÆ/á®§1cúô2¿ €_©LáÇ¢‡ÿÊìË.à¶'Þ'©°Ün=Nœ¥²û·ïNûز?°ÓvèÉtc#‹þH/ú{ÉeÛ¢å䄌b\ ¤7O%úô™Ìý}/þ¨#œÑw¾ÍôÌǸí_«È®l©³n½£8aôÉVÇit~R?v»G­çÔPQÃ"&Ù+naJ¿[ªÞÂSÍ•sÝù cæ=Å—?ÍkýwJ ®»ßÓM`'NïʇenÊø—7ñÆÔ0 —UONáÝ„JÆi å²Ç“ýŹ|¹»xè@[žÇðçKohÐþœoYôÜxl@ú×ó·Ïwy‹$ŽvD†CFr:%Ç¡ît’{°uˆ$ÌGjù²+Ü»„ Yw1vBo~8Œ«Ç4Î?~7^õ,CÿóiËÒE´‰šH¤¹‰W·ç£gß|¶o/â¾×‘eº-·Òï´Î:÷^ŒÛ@¿ËÿAÙ_pÃÿ=Ä’ ø•Íçqþ£e÷Ÿø+¿ý¾Ž~aõÞžœôåt¦ö bÉQWÜÁ˜#¯séý¯³!X·…ÇNœÆÜóÎ$òÍ×èÒ[n,k/cCšŠàñþî IDAT»o}9ûÚSñ[y=7=ò‡ÜÀï+É;éJFFï9Ÿm?Îvàk×®l{é%Î:óQ&õ âãä­üôËAfN=ŸãƒóGž?ÝÆŽ£mÚb–î®lì‰Ç¤òŵ=él^š×ÞÂåã—òúòƒÞ›ãÚØÁ/¿ò·1iÛ>÷‘$V¾ñÖ_ø³Ç¿ÊÝ1¹ôŸq'£ÓÞaÆÛ˜zƒÁ€Èp¤à$ŸÝ¿ý—ÝEw±qoÎ:óé¢ß%«èÒý¬þi!«Òj®$ØÚ­O]‚ùÍ,ÞÝ\Þ-rÿ 5ïèí]iIa*ÝÚú`º+þb„ŽºÇÎÝËç¼ËngÝjL"-YôØáš4S¤ìÛ™À¾ VÇitn·ÿ€ªfª“ª8ê´ú‰& m¦ì„Dö'Ò–Èw}Ï®Bî Aœvñ”J&F,–KܧqéŒé<òæjúß·ˆW.ëKùÍCGÞÀ¹]vðñ;+kø¦Ú$ý×ë¹äâs¹ý±·I>é5>zá:VóÒ«×ßù ü¾ÙCûÑãéäð§ç´Ëéµ{߯[Äg«lL˜M„-ˆ>'ÄHú™MG<؛ѽ ô¬ÏXºµhÒÓ 1Ìê="ð³…Óghgر͙µOå<¸ÚЩlmè3¼t¹ë‹'VÝÃÜiáÑ‹¶~ìøø,*8—YÌów]Á¨®ÞºB`O†{–¬%½ªéE]˜xã»,øuÿûm1Ÿ?È øägy$|ñ)Î䬨 °µcÈɽÉ^ñ‰yu}€ ØúŸ+ùç¯Ìž·™5[ÓX³ú7î{È(ŸÏJûPÈKÏ¡ ù3^ýÎé7]D÷Ñ̹´+q¯¼Ææì2ò ¤CïDœE¿Ë‡1;Y“˜Æªï+õ»Ô­ÃîZðçìŒëîÿ†ÔZNÏR#ÿþLø2²çÞÁ÷ê=Q´ ˆˆˆˆ;Í©Q?v»‡†Ÿ´l¶vgð÷\Bú“#yà㽸y•yŸ>Æg>ÏÝß.äæŸÓ*) xÈÛ¿… û·°aÕ2öwYÏë³gqü'gsÉ—ûá ›~:a;žãû5϶éÉÚEBÜ.â–›ÞŸEÏßÎÔnÿãýÔtöe@ŸÈìàíÞ°Gн OZ2Yu9õ¤¿pÜ;…:/á„ Ž#ñÝ/Ø]p˜´1¸_¾†“»o§ûð6Љ!Ù `ÀÁgpÓüíe¦ªtgï!—PL0lØ j]m1ÝNÜØ°†lšÙ·|ÅáR¿“éL'9œ{Þçö?ýÆèó®ãêë^ä9·ðÉÕ§ñä:L0lF¥Íà ò’ù¼rK$ Ÿ¼gVÓù<3÷vüÛv2)Hø€o’¯å¼ ‡òbR¦,díÜ8rêðЖäÍŽcþuCù°MWº´5È*ʃ?.`Њu‹.öPÚ…@~F3ƒÕ¯¿ÎžoïàλӘäú‚ë¿KÆmv##× ¨]ö*~—§æÞ^眶öS¸÷Ã9ûàc̺ö_lÎ-~â\dìK‡à®´2 Ç{¹#¢ᤳïHÍE ¿žçsnT0ÇE­`M™hÿ!fé$fžr ±u.5 ×Þzo¼ø™Õ1D¤ëÙ‹n=ûóDz…VGiT~R?v»›Ý¡¢FKæè4œãƒ3Ù±-­hx‡Iö–_ÙœDd϶罨Àă Ž€¢ô"Aƒ8c\É òêÂ41qàï0 `;Ë6ÒqÂÉ%#¶ãùs?Øùû†:ÎUà"eÅìµä¼¿ÜÄ´køð‡Ý¸09²ò åœÈ3§óçý9Á»RKþ.bwCç¡Ç‘“”ÀömGOIrñx2ضf/ô?ŸQíë´ÈìQž vÄ€îchŸ–Xf;v¥”ÌÙ`æïaõG÷sÝéù÷Ž>\4gm v°r'ô<%šÎ•>Yt?q ¶oóÚûß·i#›b×spåfÒ½{wîm|1mϼ–“OšÎps _®9R¿noRœ™ûس7—þ³çÔ X>øxSÅ•oìÁDC~V>&àÜùsW´%ú¼ìž÷ë²Lðä“UÁ!Ø«ø]ÒëÏÑ›‹ÿ=Ÿó3žäªkþŦœÒ¿©›#ñ¿±8eHÛ¢BQ}OGhÎV쩹@çÚ5—ëNÇùgŸNááe°åfÎx‚Mùu Üt¨ !"V;”œ¤‚†ˆ´JêÔ¨»£–sj¨ áí†2vr&Ù%¸ÉؼŒM64=ñ9>›.[ïœÄ­_ï¯0¹cu ÷üŠÔÛ9÷¡‡‰{j[rÚ3túSü9(‰wWìÃåèÍ%ïüÆ]Þâòóa{äLæL5غq'ž6ô;“Ï'ùOØ^ê`Í/r"óٰ<‰²ó4uëϼe6/w.ïÌå×Dö†õìË6h{ü.½}2¶ðë^'xRYúêûüàž¹7•Wbr2çYFçÏ]_í¬drÒ~ߤoøyß̺á|ò½‚_µd¯äï²`æuû-/l,êSpíàëW¾eö OñÚ?Cxã¿k9äiK~!¬{ÿ¶ç°õ½§Y:ãežzçßt~í 6§Ûé>¢3p¸–©òÙ2ïEb§?Íco¾@ǹ_³5ÝND¯>¸‹…{ÚYIeËLxfç{_³î,zö¾˜Á·y»bL'¹¹.láí´Uó»Ô!cȨ»¸q̾¹õwŒ>Ã8¡èrOÖN¶îÊ  ñ]Þ^}<õ W?òÛ;\Èßfv#éµY—Uí]`fïöÒ‰Bé˜åÆ JfWRJ勊ˆˆˆH«dØlÞn‡£¨«À{²;eÎÛüüý­ŽÛì8¼ÃOj®©¨á}nåoÝZæ¢ø;£˜µ¼÷“³Œ\q·îæÞùs²“~åý[ïàÕùàïØ¿6‰:ójfÝÖ  ÿÀ:b^ü Ïÿg ¥;꺤 ûùïþʾÙ60 ïp [p{: »”9×ô£­ÈNbÝ÷qå3ÿÁ{l’½úæÜeðØÏñïËd%|Á3WÜÎÂCõ˜¡ ~ÜǬ+ƒùùÝß8Rr€™OÂGï4ó>º®\@|Éün}{ 3lpÏM7óøYm0È'5þ}øüC¶ç{pï[Àß.tqÓwqí‹— xrö±uñrÕòϵk.7^âäo÷ÞÊÿ½pA@î¾¥|°~ ÷¸i3p*WÜ|*‘A€y„1ÏrÇ“KÉÁCί·0ã†dî¾õzŸ{?6 ?5‘•1Iä›NR?šÍý}þÍÍw~ÀYþ…dˆeEº ·ÛMrr2;.áƒoR˜pQßÍ_\êq©ÇqWòÂç÷Ðíp"ë—Íãžs^æ‡Í•ÚlA„x;5НÏÛø*ßUz#7YùеA6'{ªù]jÇŸN#GN.xñ'.(}Õš«8éÒ/9âÚÅg7^Dø“Ï1û_ŸìLfÕÛWðà¿ÿ 7Yˆˆ´»ÒgÐhÖÆ|mu‘2J"JŠU*Š‹6»L·ÛÇåýlî):¹].î!Þø×BrYôï‹yðËäÙtíڕÇ“—§– ©H"b•6 *ËüÔê("â#†a”JN6[ÅËŠ.·Wr9¦‰Ëé,ÓQ\|p9xòó+-Z˜žºÌN(Í£¶KºÚýüp;ëºÄ…ˆXËCÚ÷a|¿ò»Ø»w/:uÂÏÏÌÌLKÒ‰ˆˆ”—“™®‚†HWe¢’ℽ’ë0Œ’BCÉÉã)󳫠 ärwùmÕ5!¥ØívµYÒÕîpà,¨y©Ci>RRRèÝ»7ݺucË–-VÇ«Ð%QM‡D¥… »½B¢²â„«°°ì0ŽRש[B’ÝQÛN ?i‘vîÜIÇŽ=z4ë֭ãÿɈˆˆ…ƒ9qÊ,þú}«£ˆ4)†Ív´ÐPô¯QÃχG@—ëè°ò]•(\e./ß)!Ҕص¤kÓ6|r4q‹c¬Ž!-Ü¡C‡ÈÎÎfüøñ¬_¿^ÃQDDÄ2ù¹*hH‹Q\`0Jw>'Ê_f”º®²Û™ÏÑBC©Ž‡ò?—.H˜€Í0((š_B]ÒÙíMÚ”© !%//¥K—2lØ0:ľ}û¬Ž$"""Ò¨ è¼û¡.¥.+.8˜¥;"ŠŠ¥/s9G.µ]émD¤rµŸ(TE ‘Vá?þ oß¾ôïߟ­[·ZGDDZ›ÍNôù³øå󷬎"M\IÂf;úoq‡CùË‹‹ ÅçK+ › ‡Ãáp€iVÚýP¾C¢dΈÊ:&4o„H£²×¦¨a³Ù0A˜"­ÄöíÛéܹ3#GŽdݺuVÇ‘VÄãq« Ñáv»ËÊ*ý·š"PRp0Ëu?T¸Üãñ.óY|Y©b…éñ`š&— ·:"DšZ5Ô¥!Òú—””ÄáÇ=z4v}¨‘j 5™È>­Ž! H"âK*jˆH£8tè7nä¯×]Gxx¸ÕqDD¤‰Ú¼v1É;6[CæÔ_RQCDM^^¼ò ={ö¤[·nVÇ‘F N ñ%5D¤ÑÅÇÇDÿþý­Ž"""MÌñCN¤×€áVǤN ñ%5DÄÛ·o'##ƒ‘#GZEDDšmëW‘´%ÎêÒ€Ô©!R7ÑcUØ­ 5DÄ2$!!É“'juñujˆÔMÌJvë¢Êw—âjªéñ4Zi}rrrX¼x1ýúõ£K—.VÇ‹õŒF¿¡c­Ž! H"âKŽ*¯p8p©KCDIll,ýúõ#44”mÛ¶YGDD,²+áJ/>9€¸Å1F„iš†¦iuiaª,™Ú<*jˆH#JLL$''‡áÃ5ŽPDD¤%Ññ•*;54Ÿ†ˆXaÿþýdgg3qâDbccÉÍ͵:’ˆˆ4¢n½£hÛ¾ ›ÖüVæruh4o%CPÜn«£ˆH Se¹TE ±JVVË—/gàÀtêÔÉê8""ÒˆöíL¨PÐæO"â+*jˆH“äñxX»v-ôîÝÛê8"""r 4Y¨ˆøJÕE ??ÜNgcf© !!§ÓÉ!C¬Ž""" cd/†M8ÍêÒÀÔ©!"¾¢N iòöîÝËž={?~—˜˜HNNÇ·:ŠˆˆH« N ñ• ï,êÒ‘Ö`ÿþýlß¾I“&lu)eÒY—ju i@êÔ_©XÔðóÃítZ‘ED¤Qeee±téRH§N¬Ž#""E–|»€üÜl«cH1 ›Ý®Â†ˆøD…˜Ô©!"­‰iš¬]»–¨¨(BCCÙ±c‡Õ‘DDD|ΰٰÙl%CÏkõs©ó†ax¯¯ÅϦiz †ÃáÀYXhõ¯/"-ˆŠ""@BB‘‘‘ :”øøx«ãˆˆ´jc§\ȦÕ1d9luß3 oá ¨``·ÛÁ00M³L¡|¡|¡LÁ¡¦ë ÓãÁãñ”jõ³ib:ÞŸMÓ{}ñ©šŸED|IE ‘"ÉÉÉdgg3~üxbccÉÏÏ·:’ˆH«´ò§Ï}zÿ¥‹åÏSÅå¾:Þ®Á’n› L·Ëå-"TQ`(.xÜnL§³Ì¶•<ån+"ÒRTZÔpiN i¥222X½z5#FŒ`çΤ¦¦ZID¤Y()¬Ÿ÷^YñòêÎãQÕmí~8 ŽJ ª*"w)”*"”?O—Wv¾L¡¡ÛWvDDŽI¥E‚¼<+²ˆˆ4 .—‹Õ«W3pà@BCCIJJ²:’ˆ4aÅá”;€¯pYÑ¿•]VÝöu½¯Ò™‚BCK>×UZ8¨K¡¡hUmSR(u°nš&&T~y5ç;t=ŽÌ´CäçåTئ¤û(ÈÉ9Z (]4¨ªˆÐ »&FgiLœÕ1DD|FÃODDª°yófzôèÁ AƒØ¸q£ÕqD¤‰é3lÀуärð.+ú·²ËªÛ¾®÷U&SqWBé.„R÷Y—BCñ>ªÚ¦!Ú³§Aï¯5SACDZº E ›ÃGE öìÙC»ví;v,±±±jÆv)âv¹Ø› /ƒDDD,Ta¡hujˆˆ”•––Fll,#FŒ ""Âê8""-ÞÐñ§Ò©{«cˆˆH3 ¢†ˆH-²råJ"##éÑ£‡ÕqD¤)0Í’9,¤aÅ/ÿ™”½;¬Ž!""Í@™¢†Íno•(‰ˆÔÖ† °Ûí 8Ðê("b1PICDDÄZeŠêÒ©YRR‡b̘1Øív«ãˆˆUÔ©á3GM&²ŠÇÒzDnu‘f«ÌD¡*jˆˆÔNjj*ÙÙÙŒ;–7’‘‘au$‘cóÚÅVGiT1+µJH}©SCD¤žòóóY¶l={ö$22Òê8"ÒÈ4üDDDÄz*jˆˆ£øøxˆŠŠ²:Šˆ4& ?ñ™ã‡œH¯jÇ‘š©¨!"ÒvìØAzz:#GŽÄÐAŽˆÈ1Ù¶~I[ÔŽ/""5SQCD¤¤¤¤Àĉ ³:Žˆø˜iš*bŠˆˆX¬lQÃÏ·ÓiU‘fcb´Zb¥r999,Y²„¾}ûÒµkW«ãˆˆ4K=£†ÑoèX«cˆˆH3 N ‘zX£–X©^\\!!!üñVG_Ñœ>³+áãWZCDDš5DD|dÛ¶määä0|¸:{DZ"­~"""b=5DD|hÿþýlß¾I“&buiHêÔð™n½£8aôÉVÇ‘fàhQÃ0°ÙlxÜn ㈈´q8Ñ'jùéÖÌ4M ujøD@`0“§]fu il†a`ØlxÜn«óˆˆH)©©©¬_¿ž1cÆÐ¶m[«ã4¨áãÇóî³Ï2|üx«£ˆHSŸËâ¯ß·:†ˆˆ4P—†ˆHS–ŸŸÏòåË2d!!!$''[©AÄ-_ά»î"nùr«£ÔK̪8«#ˆÕ4§†ˆˆˆål •ODDšƒõë×@TT”ÕQ„+=5ß}‡+=Ýê("õ¢ÕO|Çf³óç ¯¶:†ˆˆ46P§†ˆHs±cÇÒÓÓ5j”Æò‹XM>ãñ¸ùå󷬎!""Í€Š""ÍLJJ ›7ofâĉ„……YGDDDDÄ2*jˆˆ4C¹¹¹,Y²„¾}ûÒµkW«ãˆ´J~â[ºà*ìö–·ò“ˆˆ4,5DD𱏏8BBBèׯŸÕQDZ ?ñ©_¿x·[ŸOEDJ‹ŽÖrò婨!"RGÃ'G[¡ŒmÛ¶‘••ň#¬Ž""""">£Õ×ÊSQCD¤ŽâÇX¡‚˜˜È¤I“ ±:ŽHë N ŸštÖ¥‡ZCDDš85DDZˆììl–.]JTT;w¶:ŽH£mÙ¾5§†o-ùvù¹ÙVÇ‘&Î`s8ð¨¨!"Ò왦ɺuë§OŸ>VÇñ™âbF\LŒu!Ô©!""b9ujˆˆ´@[·n¥  €¡C‡ZEÄ',-fQ§†or!amÛ[CDDš8€ax<«³H=LÔì·"R…äädvíÚÅ„  ²:ŽˆH¬üés²Ž¶:†ˆˆ4q6@]ÍØRÍ~+"ÕÈÈÈ`åÊ• 4Ñu IDATˆ:XG¤eÑðË©¨!"Ò¹ÝnÖ¬YCÇŽéÕ«—ÕqDDjeÔÉgÑ©›Õ1DDZ•è±Ío$€Š""­ÄæÍ›q»Ý <Øê("-‚išêÔ𙵿}CzÊ>«cˆˆ´*1+›ßH •ODäÿÙ»óø¶ïûÎó/€§HˆE“EÑEQ$%RÔaY’­8±ãn;©›³iÓi·ç´3“énwºÙ>fÛÙîtÛél¦wê¦IãÜN'¶˲¬‹‡.Š’xß‚ °üHˆ’u‹à$ßÏÇCKÄ?¼I_Ä›Ÿï÷+«D?ƒƒƒ<òÈ#dffZGDDDDîÒW¿ùGVGHI)5©±G]DD–ÇÙ3ghllÄ0 «ãˆ,_ÚS#©vì6êhj‘y/|ô¬Ž’RªÔ8r¢UņˆÜÒáý÷a±D"Nœ8Aqq1›6m²:ŽÈ²¤#]“ëìñ36Ðeu Iq)UjÀò\Ã#"KãH«þû°Ø.\¸€ÍfcûöíVGY~4©!""b¹”+5DDdiõöö2::ÊÞ½{IOO·:Žˆ5M‡(}¸Æê""’âÒA¥†ˆÈjçr¹ðûýìÙ³‡ööv¼^¯Õ‘DR^œ¹ŸIR´Ÿ9juY4©!""ÌÌÌpüøqÊÊÊØ¸q£ÕqDRŸ–ŸˆˆˆXN¥†ˆˆ\çܹsdddP]]muYŶÔïeó6m-""·gˆÇbVç‘ÒÝÝÇã¡©© »]ö"7DZiR#i®ž;IÏ%m-""·§ïTeÙJßÐ@ý“{È_û¦áØþ –‘au‘»466F{{;û÷ïgíÚµVÇy—eñvPRLºAùS?ÅŽeä¤Ã¬o¾#ßät›‡¥›ù±‘QÒÄö=A&Þ<Ådʯ JgíÎCTçøè8ÞO$nu‘» 9vì ŒŽŽ2<ÀöõÝ´~ëG\>w•ÉÌ-l;¸ƒô«ÍŒLŰ;÷óžÏ¿ÃuŠæWÒÕ5EvY%¹^zŸÇ?›Nþ#ÏPWÔÇÙo½B{ËEFú†™ ä³í³ŸggÑ—^þ!Ï “^õ8;öæãn»Œ?é%±w_ÝßýgO]`"^Á¶GvbÎsîõ·è¼äbÍŽýÔ”OÑÓ6L$n#{Û³<õÜüǿ˩7š§’Ú'šÈèlfd rv|œ'?¼•ð¹Ñ|䃮 ŒÊRrƒ—éh! Ä£!¦®œáò©³ŒÖSyp?cÍôMD ½ˆÚÏ}ŽÚìK´|û.]ôâhz‚ºòIzÎIê? "·‡éë룾¾žX,–RÅFÏàˆÕdÉÌÉ!##ƒ Ïgu”iÊëb|¨×ê""’Â^øøóšÔë…B!ìv;iiilÙ²…ü²4Ün7n·ÇCFù“þpcÿúWœ¸4E<ã[æçØ÷‰)|_ú1ÛÎ(ßOEn?-ç=Äf¡ûĵï”â×zé ,X´føJ'S1é bT}Š’­´ ¹Y·çŒ™ ¼ùµ3蓾ŠÞðbA†.w35ׯ¤o~5Å®þý7¸ÐoŽ“ŒÄÉÿõR_û&#§æG'üL\éd4£CqŠë?ÁÚî6z:\ÄèÆŸ_GÉ{¶’Ÿq†@¤€òÇ·c»ðϼóV'`|ôeò·ýå;6pv8Dù£[°÷~›£?8Ëtèì'º¥‰Âœù q"£W5ÿä™&Ç/QYQ€½=ˆ}ÓAª7Œsî/H—; 2õjy®‘ ¹môøµÓ¨X¯¹¹™­[·âp8èìì´:ŽÈÒÓ¤†ˆˆˆåTj£þo~ÿ¥ÄŸßüþKœéŸÄž‘a”––R[»“ȶÖ¸Oq©{ncÐÈ(WøU¿´—êÒ79ÞsÃ:[.ÅlÅÖù †¦Ì7âÓ—Ž3üÔól­Î£¿yòæ{qD}xýP´6»-›¼¸®â ÝËg•FÎÆb2¢C ]Ë÷÷1ìšÍi‰RcÙþi0ò²æ6€‹1ã BZ6él0 cÃ'y®öú§ÆÖ­Áž±†õøÏ 2sËî!ÇöÃ4¬£°p-é±bé0›‘†4Ö”l ƒõìüÕ/°óºç¹É]cÿMÖ‰XàòåË”””°sçNÚږߦ~"’º K7SR¾•¶·_µ:Šˆˆ¤0•@xÎôO2ðâ—³äϳ!†††ÂfÏ£¾öÌÆØÞØD02§8BfÈ"wmp}©aË«fëÃdðýƒ^to޶cLÝìýyÓ餢¢b‘S‰¤ Mj$UAa1M‡Ÿ±:†ˆˆ¤8MjÈ=™ù G^Œ²ëÉ}ìûäcØbA<í¯ñÆßa,ƒ¹“ (¬z’ÊuQ¦; )ÝÇívã›ß!>4@×Õʶ7±þµïs§CJã“­¼ý71ê߈Úçv’ÄÃ>¼}Lßn5Æì8—þù‰?ý~¶=ûIêlüݧ8þõ×Þï:–8Ó¾Éíï¡ñ±GÙW›D µr¢­Éhœ™+/óã¯ùh<¼}Ÿ8Œ ˜ L0Úæ!‡Ùá£{ÅÁžCà±&³ì˜ö0ᚙ۫d˜óÿøe"O½­O}Œ­iÀŒ›¡ŸtÑÙ9•ô8"éy4ìÛOë‰ãD}ž{zî¥K—ظq#uuuœ?>I Ed¥óŒsæÈw­Ž!"")ÎöÒW_Œ?ÿ§¬Î!+Xnn.†a`999‰ÓT\.±Øê ±Âî'?ÈßÿÙÿÅç~ë÷8ý£ïß×= ¨®®¦¥¥…™™‡YþŒÒRÖæåÑÛÞnu‘U饯¾¨I I¾@ @  ¿¿Ÿôôt ÃÀét²uëV|>_¢äÐæ‚"©£õÄq>÷[¿Gë‰ã÷}ÇÙ3ghlläêÕ«¸ÝîEL(b½¿ŸŒŒ «c¬XŽ|ƒº}OðΫ_·:Šˆˆ¤0•²¤¢Ñ(cccŒ°nÝ: à¶Ö<u¾àðzorÔªˆ,™¨Ïsß E"NžzzzÈÊÊÂ0 Š‹‹ßµL%[UDî`ff†ãÇS__Ãá```ÀêH"÷%¨Ò±–Ýê"÷jff†ááa.\¸ÀÑ£GfíÚµ466ÒØØHyy9k×®µ:¦ˆÜÁ¹sçÈÈÈ`Û¶mVG‘d·§ñÄóŸ·:†ˆˆ¤8MjȲ·pŠÃáÀ0 *++ÉÎÎN<ær¹ˆÇã'‘uwwSXXHSS---Äb1«#‰Ü=-?IªXl–×_ú«cˆˆHŠS©!+ŠßïÇï÷Ó××GFF†aPXXȶmÛðz½‰’# YUDæŒØ¿?çÎÃçóYIDDDD– •²bE"FGG   Ã0¨¯¯'‹% ŽÉÉI‹“ŠH0䨱cìܹ“ññq†††¬Ž$rG:Ò5ùÞóÜÏóæ·ÿ‘ÙY–$""7§RCV ǃÇ㡳³“œœ ༼‡ÃqÝ2•ÙÙY«£Š¬ZmmmTVVRUUÅ•+W¬Ž#"{ãkuIq*5dU ƒƒAHKKÃ0 Ã`Ë–-øýþDÉ ­Ž*²êtvvòÐCÑØØHKK‹ÕqDDDD$…©ÔUovv–ññqÆÇÇÈÏÏÇ0 jjj°Û퉂ÃãñXœTdõÁï÷sèÐ!š›› VGy-?I¾ƒú$§ßø¡ ßê(""’¢TjˆÜ`rr’ÉÉIº»»ÉÎÎÆ0 JKK©­­Åår%JŽH$buT‘Íï÷sôèQvíÚÅàà`bY=ÞzùŸ­Ž "")N¥†Èm„B!†††Âf³át:1 ƒŠŠ B¡P¢àðûõ$‘dinnfëÖ­8:;;­Ž#rŽt±œJ ‘»Ç™˜˜`bb€µk×bUUUdff&6u»Ý'Yy._¾LII ;w­Íê8"ÄUɵïÉç¹xêS^—ÕQDD$E©Ô¹OSSSLMMÑÛÛKff&†aP\\Lmmmb‚Ãív333cuT‘ahh¿ßÏhnnfzzÚêH«JÃáô9buŒÔ¢I¤;ñ£—¬Ž "")N¥†È"‡ÃŒŒŒ0228M¥¬¬Œh4š(8|>ŸÅIE–7ŸÏÇñãÇÙµk½½½‰ ~%ùThˆˆˆH*R©!’ó%@nnnbŽœœœë–©Äb1‹“Š,?±XŒÓ§O³mÛ6ÝÝÝVG’UJËO’¯éñÓÕÞŒglÈê(""’¢Tjˆ$Y  ÐßßOzz:†aPXXHuu5>Ÿ/Q€h”^äÞ\ºt‰7RWWÇùóç­Ž#«‘–Ÿ$Ý™Ÿ|Ïê""’â–U©¡õ¼²ÜE£QÆÆÆ`ݺu†Amm-pmÂÃëõZSdÙ ðÈ#ÐÒÒ¢=lDDDDV™eUj¨Ð•ÆëõâõzéêêbÍš5‰}8jkkKTÜn7ÑhÔê¨")ËãñpæÌéììÄåÒ) ²D4©‘t;ö?ÉHÿUÆ´ÌLDDnnY•"+Ùôô4ƒƒƒ b·Ûq:†Aee%Á`0Qp«£Š¤œH$ÂÉ“'Ù¾};‡ƒÞÞ^«#É* =5’ïìñYADDRœJ ‘‹ÅOœì——‡aTWW“žžžØlÔãñXœT$µ\¼x‘M›6±}ûv.^¼huYé4©!""b9•"Ë€ÏçÃçóÑÓÓCVV†aPZZJ]]ÝuËTÂá°ÕQE,××ׇÓédïÞ½´´´‰D¬Ž$"÷©¦é>σ]íVG‘¥RCd™™™™axx˜ááa€Ä2•òòrÂáp¢à˜šš²8©ˆu\.~¿Ÿ¦¦&:::4Õ$I¡å'É×~æ¨ÕDD$Å©ÔYæ\.WbcD‡Ã‘؇#;;;±LÅívÇ-N*²´fffxçw¨««#77—«#ÉJ£å'"""–S©!²‚øý~ü~?}}}ddd`EEEÔÔÔàõzS¡PÈê¨"KæüùóTTT°mÛ6.]ºdu¹[ê÷„é¹ÔjuIQ*5DV¨H$Âèè(£££`õõõÄb±DÁ199iqR‘äëî°Ý»wÓÜÜL,³:’¬ñx›&5’ê깓VG‘§RCd•ðx>Žßïgÿþýœ;wŸÏgu$y@*5DV¡`0H0d``€´´4œN'N§“ªª*ü~¢àƒVGYTÓÓÓ;vŒ;w2>>ÎÐÐÕ‘d9ÓžIW^½“̬l®œ=auIQ*5DV¹ÙÙYÆÆÆ ??Ã0¨©©Án·'¦8¼^¯ÅIEO[[•••lݺ•Ë—/[G–)~’|½mVG‘§RCD®399Éää$ÝÝÝdggãt:)++£¶¶61Ááv»‰D"VGy ±k×.š››­Ž#Ë‘&5DDD,§RCDn) 188Èàà v»Ã00 ƒŠŠ B¡PâÈØ@ `uT‘û2::ŠßïçСC´´´à÷û­Ž$" ”TT³ÎùOÿÄê(""’¢TjˆÈ]‰ÅbLLL011ÀÚµk1 ƒêêj222®ÛlTä^Þ×À‘ÖÙ8zô( 322bIy·¡î†º;¬Ž!"")L¥†ˆÜ—©©)¦¦¦èíí%33§ÓIqqñ»–©ÌÌÌXU–« …ZZZ¨ªª"77—ÎÎN«ãÈ2 #]EDD¬§RCDX8fxx˜ááa€Ä2•²²2¢Ñhb™ÊÔÔ”ÅIEnïÊ•+”””°sçNÚÚ´A¡ˆÕ K7SR¾•¶·_µ:Šˆˆ¤(•‹Àʱi‘T´pJnn.†aPYYIvvöuS±XÌâ¤"ï644„ßïçÀ´´´èhc ö0>Øcu Ia*5 ‘[ úûûÉÈÈÀ0 ©®®Æçó% Žééi«£Š$ø|>Ž?Ncc#}}}Œ[IRÔü”xùG¿ûOVG‘¥RCDV ¿ßß璘¯ÌÌL ਨˆšš¼^o¢à…BVG•ÂívÓÖÖFSSÚçEd‘Í„‚*4DDä¶TjˆÈЇadd€‚‚ ྾žX,–X¦âóù,N*ËÝÌÌ ï¼óuuuäææ200`u$YJóGºŠˆˆˆ%ìVY ‡ÎÎNN:E{{;‘H„ŠŠ öïßOuu5………¤¥¥YS–±óçÏ“‘‘Á¶mÛ¬Ž"K(¨ÒH»='žÿ¼Õ1DD$…iRCDV`0H0d``€ôôt ÃÀétRUU…ßïO,S ƒVG•e¦»»›ÂÂBvïÞMKK ³³³VG’dÓ¤FRÅb³¼þÒßXCDdÕKåƒTjˆÈªFcll €üü| দ»ÝŽËåÂívãõz-N*ËÅøø8~¿Ÿ}ûöqþüy-q‘e/U Ðò‘ëLNNÒÝÝÍ™3g8wî333”••qàÀjjj(**"##Ãꘒ⦧§yûí·©¨¨ ¤¤Äê8’DZ~’|ïyîçIKÓÏáDDäæô‘[…B 288ˆÝnÇ0 ࢢ‚P(”˜âVG•ÕÖÖFee%[·nåòåËVÇ‘dÐò“¤{ãkuIa*5DDîB,cbb‚‰‰ Ö®]‹Ó餺ºšôôôÄ>n·Û⤒j:;;)**b×®]477[GDDDdEQ©!"r¦¦¦˜šš¢§§‡¬¬, ุ˜ÚÚÚD¹ár¹‡ÃVG•0::ŠßïçСC´´´à÷û­Ž$‹E“IwðCŸäôß!Ô¿7""òn*5DDÐÌÌ ÃÃà $–©lÚ´‰H$’X¦255eqR±R àèÑ£4662<<ÌÈȈՑ–¥N Òê hOä{ëå¶:‚ˆˆ¤0•""‹l~RãêÕ«äææât:©¬¬$;;ûºe*±XÌê¨b––ªªªp8\½zÕê8""""ËšJ ‘$ úúúÈÈÈÀ0 ©®®Æçó%–©„B!«£Êºrå ÅÅÅ444ÐÚšºG¤É]Ðò“¤Û÷äó\|>ÝÝÝdggc¥¥¥ÔÖÖ^·L%[UÐÀÀ~¿Ÿýû÷ÓÒÒ¢ dEDDDî@¥†ˆÈ2 …bhh›Í–X¦R^^N8N,SñûýVG•ûäõz9uêtuuáréćT–™Mfv6¡@Àê(+ÖŽýO2Ò•±-Í‘wS©!"²LÅãq\.WâM¯ÃáÀétRUUEffæuSñxÜâ´r/¢Ñ(§Nbûöí8z{{­Ž$·03=MdfÆê+ÚÙã?²:‚ˆˆ¤0•""+„ßïÇï÷ÓÛÛKff&†aPTTDMM ^¯·ÛËåbFoÀ–‹/²iÓ&jkk¹pá‚ÕqD,Q»ç0îñ!†{.[EDDRJ ‘(322ÂÈȆÁÎ;‰F£‰ ŸÏgq1ÿ IDATR¹“¾¾> Ã`ß¾}477‰D¬Ž$ éHפ»pêˆÕDD$…©ÔY<‡ÎÎNrss1 ƒŠŠ rrr®[¦2;;kuT¹ ·ÛMKK MMMtttèä›TiˆˆˆXG¥†Èj”žŽQ] €»£¢Q‹ÉR úûûIOOÇ0ŒÄ^~¿?±LezzÚꨲ@8æwÞ¡®®‡ÃA¿Õ‘4©±¶6†aP[[ ˜àðz½VÆ”Ο?ÏæÍ›©©©¡½½Ýê8"Iw¹õm«#ˆˆH ³[@DDRÇää$ÝÝÝœ>}š .033CYY ¦¦† 6ž®>Üj===Œ³{÷nÒÒÒ¬Žc©‡¬Ž """Òw¦"«»£ƒßù⿹™ééiÄn·'–©TVV S@ÀꨫÒÄÄ@€}ûöqáÂ&''­Žd‰cGZ-}ýx<ŽMËO’êáíM`ƒ® g¬Ž""")H¥†ÈjjɉܓX,ÆÄÄäååaÕÕÕ¤§§_·Ù¨,ééiÞ~ûmvìØËåbppÐêH"‹®ë¢Ê ¹5•""rÏ|>>Ÿžž²²²0 ƒââbjkkºÝnÂá°ÕQW…³gÏòðóuëV._¾luœÕE…ŠˆˆXJ¥†ˆˆ<™™†‡‡H,S)//''&8¦¦¦,Nº²uuuQTTÄ®]»hii![iUБ®É·©ªŽìÜ>@~~>†aPSSƒÝnOÇâ¤ËÇÔÔÇŽc×®]ô÷÷366fu¤CËO’¯°¤œ’Šm´û¡ÕQDD$©Ô‘”699Éää$ÝÝÝdggc¥¥¥ÔÖÖ&–¨¸Ýn"‘ˆÕQSZ<çÌ™3TWWãp8èêê²:’È]êe|¨×ê""’¢TjˆˆÈ² …bhh›Í†a8N***…B‰)ezk”––R__Ϲs笎³üéHWK©Ô‘e)ãr¹p¹\¬]»Ã0¨ªª"33óºÍFåzƒƒƒöïßOKK‹ŽÔ•”¶nýCl©ßËé7¾cuIA*5DDdE˜ššbjjŠÞÞ^2331 ƒ¢¢"¶oߎÇãI,S™™™±:jJðz½œ:uŠÆÆFº»»™˜˜°:Ò²4§‘\Þ‰""rK*5DDdÅ ‡ÃŒŒŒ022@AAN§“²²2¢ÑhbŠÃçóYœÔZÑh”S§NQSSƒÃá §§ÇêHË–ŸˆˆˆXJ¥†ˆˆ¬x'qZJnn.†aPQQANNÎuËTb±˜ÅI­ÑÞÞNYYµµµ\¸pÁê8"×qäÔí{‚w^ýºÕQDD$©Ô‘U%èïï'===±ÙèÖ­[ñù|‰’czzÚê¨Kª¿¿Ã0Ø·o---„Ãa˲tÞçã•‹ä.èH×äóOºUhˆˆÈ-©Ô‘U+266ÆØØëÖ­Ã0 jkk‡×ëµ2æ’q»Ý´´´ÐØØÈåË—Ó-KaaQq»r¢ó6ßí=DDDdåP©!""2ÇëõâõzéêêbÍš5†‘X–1¿Ñ¨Ûí&Z5iÂá0'Nœ ®®‡ÃAÒ^k±Kˆ…÷PÁ±rd­Éeï{Ÿåè÷^´:Šˆˆ¤ •r[vJJ² áñ¬Îµæ"²:MOO388Èàà v»§Ó‰aTVV G °:jRœ?žÍ›7SSSC{{û¢Þ{¾pHfÙp³‚#¯§å'É73Xu…Fáô=bu Y¡ÒmP]jÐ1è&·8ÈR©!·TP`çÙgðÄ¿È믉o}똊 Y•b±ãã㌗—‡aTWW“žžžØht)—k,…žžÖ¯_Ïž={hnnfvvöïy»å#É2ÿzV¼¶ÈýP¡!ÉT]jð…/þ)_üÂïpaÀmq"‘£RCn©¤$›'žøE>ýéOpêÔ<ž Å©DD¬çóùðù|ôôô••…a”––RWWwÝ2+7Û\,øý~öíÛÇ… ˜œœ¼¯û,ÅtÆT&#‡ŽtMº´ôt{æ³¼ñÍ¿µ:Šˆˆ¤ ”-5¦õÈ«c¬jCC!^ýK¼þú— YœHdeÓ÷–§™™†‡‡H,S)//'' Ž©©)‹“Þ¿P(ÄÛo¿ÍŽ;p¹\ ÞÓóSiBb±§6â€}î#·6ªÐYDƒn¾ø…ßIü^d¹³½ôÕãÏ¿ð)«sHŠ*(°SZšÍà öÔ¹W‡Ã00 ƒìììÄ2·ÛM<¾<1?üðÃdddÐÑÑqÇk“1±˜Ébä[·aö´4Üs¥–ˆˆˆ,—¾ú¢~¸ ·çñÄ8>¨BCDä>øý~úúúhmmåÌ™3LNNRTTĨ««£¤¤„ììl«cÞ“®®.<»víºí™óåCªLhÜÌ|¾Î;](–{ïó¿€Í®o[EËá} VGY4)»üDDDd%‰D"ŒŽŽ2:: @AA†aP__O,K,S¹ß=+–ÒØØ@€ÐÖÖö®¥5©´ÜänÌË)ójóÚKmu‘åȉV«#ˆ,•"""ðxJss3¡Pè®ï;ïN¥E2 Mk¤Ž=ï}–Ë-o3é^žKšDD$yTjˆˆˆ¬pó%@nnnbŽœœœë–©Äb±EíÉÉINœ8Á®]»èîîN,—¹™ù‚â^J„…×ÞÏóey8õÚ·¬Ž "")J¥†ˆˆÈ*ô÷÷“žžŽaR]]ÏçK ÓÓÓ·½Ï½L0ÌÎÎrêÔ)jjjp8ôôô<Ðýneþù‹9]q§i -?±–Ýê"""bh4ÊØØííí;vŒþþ~²²²¨­­e÷îÝTTTŸŸ¿h¯×ÞÞÎìì,uuu×}|±—x̫Շ€¿2­rlÀ'€ÏÜ䱯CÄùÐÆ%N$""Ë&5DDD¯×‹×륫«‹5kÖàt:)//Çáp\·Ùh4½ï×èïïÇï÷³oß>ZZZh‡“²\d1÷øí½RðH×mÀN¬ÿÉU9°8 ÜÍÂ&;ðI`øò µýþ¢f[él@æ×½Íâ,""ÉfõÿïDDD$MOO300ÀÙ³g9qâ.—‹‚‚öìÙC'p¸¬ŒÜÜÜûº·ÇãI{ cqƒ/°qÌ7òn¿ üGRë›Í4àŸ0ÿ¹(»á±là‡sÍÿzø}À1wMpøÂ‚çýöܵ[nñzÿ8Üß¿-÷'øoÀ³Køš""VѤ†ˆˆˆÜÖìì,ãããŒ'>–––Fuu5ééé‰F=Ï]ßs~B£¶¸˜ÜÜ\úûû“| N0IÁI•¨þ‘÷26ØÃhÿƒÕT[€ýÀ,ðyà8ßý%c8…9¥ñ—˜_¿Ü¼³Å€xséϼ4à÷×ÜŸîN³øÇ¹Œ—?[3kîµÿ·×Ì]û¾¹?WßÚÜãO¸þï+Àg¹ö÷çs7É/"²hRCDDDîÚS333 144„ÍfÃ0 ༼œp8œ82Öï÷ßò===¬_¿ž={öÐÒÒò@{vÜÌbLk$}âã½ø7˜o¸/¾®±a¾©ýYÌÍ7¯b.¯ø4æ›ê‹s×=Y<4cN0ì˜{,kî÷ë1—_LUs÷m"˜K+<˜Ë~³y ÎÝ{ø ʵ)…Ïc2¼=wÿË r÷ÿuî9ŸŸ{ÇQ̉‰¿šûØæ>þ xx³Xù?1‹05wßBÌBäW0—ƒüðaànòõ]hþù7«éÒ€<ÌB§rîžóÓõsy øs®•ð¯ÀÈܵ¹ÀþŽ[OƒÜÈ Ôÿèj0'E|À-¸îeà¿Ïý~ä.ï-"²Ü¨Ô‘EÇq¹\¸\æÏ¦†aPUUEfffb£Q\®w=wbb¿ßÏž={¸xñ"“““Kÿ¾Xu¤k!fq³88w“k>‚¹§Å¯'æ>v³px³¸XƒùæøEÀù“ÿ'¯0K‚4Ì7ìÍsÏëÁ,f1‹…yá¹çsm/“iÌ)ùš*øå¹×ûÓòÎOƒta–`1oØy×'»ù·=ƒœÀ,=bsŸ÷S˜eÀ îõÚ\ο“ÀI „9éðsÀ¿3 ®KÇ,tr0'4þWÌi‹.Þ-X ßÂÜà´³dù ÌÒæäܵysý)Ì‚ã#ÀÐÜdž1'YÖ›¼Î­üdîó<Šù÷ñ½ÀYð¸ óë/"²’iù‰ˆˆˆ$…ß璘¯––ZZZ˜ššâ¤ËÅÏîŽÏ@ æOýßÀ,)¾‡yjÆ`×&8Ï]³Èˆ`Nüg®-#ù›¹×ȸMî‡0ßÐÿø.?O7fYj{‹ñžAÆœ~¸ÂµeÛæ>ßÛ}S›…9¡òÌBÌ CË0'*ú,fásósšÀ\ºáçÝì˜Ó Ìe=˜4c~­þŸ¹×‹`.!x³ˆ_pŸö¹kvÞæs¸“Ì‚E?±‘ÕF¥†ˆˆˆ$]8fdĀ뭷dÍš5Ô××ÓÔÔDEEyyæÏ²Ïž=KVVÕÕÕ×nž‰=ßÀžo@úÝí×q+¶ì"Jk*É˺ûoƒÒÖ›o7i7<ÔBmdl¨`ãÃÜø²ã˜ ÿóMòë˜?ùŸçÇÜOâó˜c&âÿŒ¹×E9fAàæZásso‡jÌ¥¯`N <‰¹Äe3ðƒ¹k¿€¹/Å™»öïâ3º×o<ã˜eËüçoÃ,iÞï_ãöÇÆÖcæÿ-®í11Hìç¸þ›ïÇ,~va~]/rsé˜så¿aNbü'Ì¢¢mîs˜Æ\vr7âsŸË½þSA'ñˆÈê¤2WDDD–œÇãÁãñÐÙÙINN†aPQQANNNb™JZZMMM´^é"wóVjæJŽöŽüÝĦ¼·¼¿míÿõP4ú /ÿÝ)ü·{Çû’{¤kEOšißâÛ]fox4|s#Ï1'¾7÷Xsïˆ?þó úüò‰ÌeŸj1§=æ÷Àè™ûõÌ¥†ù†ü??ƒ9µÐY24oÍe€w~Ì%i\[~2:÷ZïÃÜ ónnÛÃCÑQº¯öQƒ¹™gèNOš3¿·ÈUàw¹¾üøf©áäÚ×`s:ån¬Áü¼ç÷ y sÉÇ!Ìý2¢˜ßlÏpmRã8æäK!×–ŸÔ`N¸´a–8“ÀÖ¹{ßÏ?º±^SDd%S©!"""– ƒƒAHOOÇ0 œN'†a˜‰pàÙŸfãúžj0ÏåøFAGßň†orÇ4òšQd /9ÀÖ’Vš"Kù)%ÝNÌ%]˜Ë9J0ˈø‚kÂÀ¿Ã<¡ä¯€b¾qanJùï1 ‡?YðœÈÜõÿó w'æ›ìà0 ’ù2¡sJ⣘…Á‚¹À\¾ñIÌ}+.b¾‰ÿ&æ’˜_›Ër ³ø˜àö{IŒ_:E7ðÿbžò—˜KI˜S'/a7S€¹×ÄŸgox,ü"æ^!ßãÞÍ/žšß“#„¹Qç!Ì}.À,B\;iåÛÀ¯ÿædG.æÔËYÌÂiøæ^¿‰yËú{ÌÁüš?‰992=—ñ^Š$‘åB¥†ˆˆˆ¤Œh4ÊØØccc¬+ÛÌÁ}>ýÔaÙ»7q2ʨÇÇ©Þ+Ä&Ýï¾IV95»ó™øá‹ôïü ;Uqñ_.^ÿŽÛš v<ÿå¥X“ Ä‚x/ŸæÜßbÈwý|DÃ/þ>¬Í„¨ñ–söÞÄò[n5xŠêš d2ÃÔ•4ï(#þØ”<ñ õuå¬[›„ùî—x³-FÙs?Ëží¤±©Aº_ÿ6-g]×&36>ËGþàY"¾Ìw~Î3{ùÄ… óMufO𣬣ðæØuyýÀ¯c.ùeÌ 6Á,~³¸q“Ñoc–ßáÚ›õ¯c– _[pÝÿù†|~ÃÏf 2?µð æ›ûßûóQÌ©’?ŸËõ ˜Ó%3˜!w³Aæ1Ì ˆßÆ,Àœy…[—a–¯Ýä±~Ì}9>õå(÷bÍÜ_n4ú*×ïc2‹YjÌ/?qc.Ûù#ÌÏavîµÿ³¤s‰P æò¡_»æ2æ¤ËÝúc ók¾„J Y™l/}õÅøó/|Êê"""r:ï|IÊ»—cQíù{žú0¿üÌûøð?H,ãû¯¼Â_}÷Çœzõ{7)5l¬©ÿz€·þâ[xÊ?Ƈ?žÏ¹¿ü[.{b+âë·Å–ENá:2l€-ç¡Ø]íåÜ_ÿGîn"%óô×€ÿ™Ä¬‹¥¼z'™YÙ\9{â΋ˆÈªñÒW_Ô¤†ˆˆÈrr/…@2t>`†{-b?í|£ÀÜDÔn·ó·NÒÞÑA,p“ó(ì”ï/gæÜß1Š3Ûõ½Ï±µq?x}„JÀnìç©_}ŒÉ/ÿÇ{ÂØò›xò7ž$ðâÿͱ.ógåöõòä/¿—Ù—ÿœ×š'Ißú.]þ2Oä@o —MŸý-ö¯y“¯30»“g>µž‹ù?¹ä6wAȪý y.“ö÷ôLo þW~‘Š+Ã˯ ½kŒy¶¼]<ùoŸfúŸÿ+G;mlüìïš{jüݹk{HÄgŒ6²¶|˜ºmv†¾ý¯´ßE¡Q7÷ןƜ²øÚm®M%½mVG‘¥RCDDDRW4Œ¿»ƒ#˜KNàÚF¡7ÛO#mC#[6xèúæ¨YD†¸Òâ¦rW¼þò]¿lÌÓÉç½lÝ\@ZóÍ6„ L¡ÒAºÍÅš’ d°ž¿ú…Žåt“»Ænnjp3öµ|?u ¬Ëφ™0vÒßxÜÉMØÖÖ°û£ ÐúNž›¼Õêš„tÌ%˜û-ü,·^²!""²\¨Ô‘”›òâ»ØÂ©Þ+æŸþ[lšIAÓNrm¹Ôý/ÿ11•`Ên_jVïàðú(GN¶q³$¸åq­qâ±ØlØl6Àñšÿî{Œ.ŒòÜj.ÃNî®qèñ<ú_ý6-½“Ì:¶óÈ'Þ6'¶µlúà3l Ÿæ]eæNæI»óe)©´byÎ ´ŸÖ®""r=•"""’ú¢á›o ºPv[ksñ½õN\ \›\°9ØôìϘ—Ø®mÆx˾°96òÐ:ðŸô2˾aŠ1=wUÆFªŸÙKNzŒÐÐYNÿÃéóßE¥a³Ad˜óÿøe"O½­O}Œ­iÀŒ›¡ŸtÑÙ9u‹b$†ÿÌ78á|†ú÷þ4åi³D|CŒc@Œ©–ïÒºé£Ô=ýåD˜ºð}Ƽe”ä‚=÷IÞóo®Ý-ÚþO|ûëÝDïý«—ò6l|˜‡Êæìñ[EDDRÀ‘×¾ßב®"""r×£HJ©ñ¯y³ÓOîö™kÖ°¡¬ŒË—)©ˆˆˆÜ­—¾ú¢–ŸˆˆˆÈÝ›Ÿ´°ú÷"™ELFf&YYxw¹_*5DDDDîSdf†HøÖÓ²8Œ ¥ìzüCVÇÔpø0 ‡[CD,¤=5DDDdÉ%moÜÍkÄ'Ïð깯ûÇãñ¹#]%™Ücƒ¸Ç­Ž!"")H“"rS wYh±–${Êb•&Vì""w§õÈZ±:†ˆXH¥†ˆÜÔÂ…ED’%YÅ†Šˆ•%¯`={ß÷œÕ1DD$iù‰ˆˆˆXja±±z.Æ}î––Ÿ, Ÿg‚“?þ†Õ1DD$©Ô‘{¶Ø{bÌßç~K‰d•šøImZ~""""÷%KG*Üwþ×­,¼fþyK.Mj$ÝšÜ<|ðg¬Ž!"")H“"""’r·*6RáäYÓǾÿ/VÇY4‡÷5hÿ2‘E¢RCDDDîÛRÍzcÁ±EÃݾŽöÔ‘û¡BCdñhù‰ˆˆˆˆ¤´ŒÌ,ÿÈÏZCDDRJ y É:–Õ*Zv’z"á~òí°:†ˆˆ¤ •"""òÀVJ±q¯…†–ŸˆˆˆXK¥†ˆˆˆ,Šå^lhB#µ½ïã¿hu±XÃáÃ4>|ÝÇTjˆˆˆÈ¢YîÅÆ=Ó‘®KæÇÿú%«#ˆˆH Òé'"""²è–ÓÔÃ| s?yã€* ‘¥ÑzäÈ»>¦I YT•,Ÿ‰ùòe¹0«Ùág?GzF¦Õ1DD$ŨÔ‘¤HõbcQ¦I´üdÉùÖß„­Ž±$¾ùßÿÈê""ˆJ IšT-6–ÓòY}>ú+`u‘eC¥†ˆˆˆÜ³Ã w}í|±‘ åÆ|ŽÅ*4t¤ëÒ9ðÁO°&w­Õ1DD$Åh£P¹gGZ[ïéúùáA6å|V½®,žcßÿŠÕDD$©Ô‘¤q¬[GFVÑH€ùùŽN·€F§ÓüÀ‚i‡ëænœ‚g݆ ×Ìf{÷ $sÏ;=2Àžâbþöî;:ªjo㊒gk IDATø3%=„B¨¡÷z* `½Ø,× *Š/`ÂbÁ 6QAz‘¢@èBè„zK ©“™÷H€„3¾Ÿµf)““3Ïœ)ÙûwöÞG’¯ØÌtùWæ¸lSN¹€!(jPˆµ ÓòˆëU‘Ÿ¼‹•»‡‡R““/¹¿™··’6:tᾦ¥KKʼLêŽKþ%I2[,—Þáp\ò;ç ’Ô($$óg9æs\¾ÿlÿväpß…ûsÈ…‚ÕôîÿhÛÚ%œ9mt€¡¨@.Œ. Ü £s§§¦*=%EqÇçøóìÓAö>œë6’{ôhæöWyÌK¶¿Êãæ§„¬Q'(X«ÿ™lt€¢¨@.Œ. Ü.®¶ÞÅåÅ ÖÇçqõàÔ*e»é²ÿÇí£a›Î (^Êè'CQ ›ðð¼_®·ÎúųwâˆÑ1N†¢@6L7ÂÃ(”‰¢œ^ÝíU¼ty£c×-"’B)PX(N/jÅ|£#œ#5€K¢¨§W³Q•ªPÍè(„›²æàÊ(jÀém[·XGöí4: ¡ˆÕ¬yq;ãŠW®¢rÅÙl…W¼r}5¸M´äln‚Ñg³«Ôk¦rUëšà|¸ú ÏÎa£ê ÀÙ,ç{ .,zÓ*£#œ#5€Kb¤Ï¡@þ«P£Ì³ölYgt€a¤2¬› 0Ú·} À(j@!ú ¸]PÔ€Ó+[¹–ªÕoit €“aM 8½ƒ»·à„©\E €ëÞ”ÅxP"´²j7mgt €“¡¨q ……‡ܤˆÕ®·oa¸*Ò±˜ÝÚ²z¡Ñ1N†¢Æ-atœRXx8ÅÿÄU‘…E 8½b%CÖª£Ñ1N†«ŸÃ1š¹9u4F§ŽÆàd©\E 8½¢A!jÜî£cœ E  b±=@asöôq­]8Ýè'CQ(„˜›àv@Q@¾jft@!äãç¯æ5:ÀÉPÔ¯–GDP‹?£•ó&œZØáFG¸å(jPD.0:Â-GQ‡C2™ŒNBÀÝÓ[­ïënt €“¡¨§—–’¤%33:ÀÉPÔ.£’Ñ€S¡¨§g6[Ôî?Ïàd(jÀéÙíZ8ù'£cœ E à’(jÀ%´{øY™-£cœˆÕè@^,œÂôÀ¥©\E ¸„;îë.Oo£cœÓOà–ÎüÍè'ÃH à’(jÀ%4ïðˆ|üŒŽp"5®CËð0£#ܶVÎûKçâ㌎p"5®ÃòˆH£#€,5à·{@EƒBŒŽp"\ý.aíÂéFG8Fj@XSp~5àÂZuT±’¡FÇÀm„5õçÇô¸„ÈesŽp2ŒÔ.‰¢\B­&mU²\£cÀɰîp{cú \ÂÖ5‹ŒŽ'ĺÀí‘À%QÔ€K¨V¿¥ÊV®et .,¼)Ó• Š@ 7:…ÊÎËup÷V£cpa«™®TØPÔ HdD„ÑN$< .¥†áŽp"L?€Kˆ;yDë#ŽàD©\E ¸„"þAjz÷ŒŽp"5`¨°;ÂvG¸Ñ1€ H8sZ«ÿ™lt €¡¨\E *ri„"—F¸Oo_µêÔÕè'BQ.!%)QËfÿnt €¡¨\E ¸«›»Âìat €¡¨—`KOSÄ´1FÇ8ŠÀ%QÔ ÂÂÃnt Ìd2á¶wg—Þ¼€ (jäQPPjƒ-˜ôƒ‡Ñ1NÂjt\AdD„ÑàV­Z¥FéÌ™3FGb¤@žU¬XQ+V¬P‘"EŒŽrÛjóÀÓrs÷0:ÀIPÔÈ£ŒŒ Õ¨QCK—.•ÑqnK‹§ÿªô´T£cœE Ü-ÃÃŒŽÀM³Ùlr8ªW¯ž.\(///£#p[£¨[byD¤Ñ¸iÊÈÈ$5iÒDóçϧ°q‹µ¼çqyùúà$(jä‘Íf»PÔ¤V­ZiÆŒòôô40Õíeùœ?•œot €“ ¨G²Ùl²Ùlî»ë®»4iÒ$ €¢@©‘}´†$uêÔIcÇŽ¥°q 4¹ë!ùà$¬FpçGjX,Y,Y­›R<òˆ’““õüóÏ+%%ÅÀ”…Ûš§àD(jäÑùQçof³YuêÔÑž={.l“––&“É$‡Ãa`RÙ…7 SÄj® #¦ŸäÑùõ4Ž9¢ß~ûM‡C½{÷–ÉdRjjªRSSåp8\² ÑºµŸ.üH­[;÷•E´î¤ÀÒFÇ€‹¡ ^5ò(##C‡V»víÔ·o_ÅÅÅé¹çž“›››ÑÑnÚàÁÔ¶í@ <Àè(×´aÉlÅ?lt €“ ¨G‡Ò½÷Þ«ãÇËf³iäÈ‘òððЫ¯¾*£ãÝ”Áƒ‡jÑ¢5xðP££g¦ÉÆ;þóX7£s€BÈ¿xq™-Å=jt”|¤¸¸8ÙívI’bbbd6›UªT)%''œ°ð«Óü.8¸WÇí5: À`“'Œg¤@^ÅÆÆ^(hHRzzºFŽ©"EЍnݺ&»}l^ù/ À5òèò@ÓÒÒ4tèP…‡‡+**Ê Tܾ(jÜ„ÔÔT-[¶Œ©'·H†w¨tÅFÇ8 Š8‘ðð0…‡‡pZÛ×/Õá½ÛŽÀÅñ·¶ð ¨¸­DDDùÄjtp,àÚªÔmª´ÔعÉè('@Q.#:jµÑN„é'¸eZ„‡©sÙù„¢n)ÓMünùêaª\§I¾e¸6¦Ÿà–Yq“ëÆìßÁº3€‹©\E ¸Œ2•jªzƒVFÇ8 ¦ŸÀeÚ³Íè'ÂH à’(jÀe„”­¤:Íî4:ÀI0ý.ãøÁ=:~pÑ1N‚‘À%QÔ€Ë )£ú­ï5:ÀI0ý.ãôñC:}üÑ1N‚‘À%QÔ€Ëð ,®&w>ht €“ ¨ ŒÃáÉd2: ‘øØZ³`šÑ1N‚¢pI5à2¼}‹ªÅ=à$(jÀe$%žÕŠ9ŒŽp5n¡ð&a oft ŠpnžjsÿSFÇ8 «Ñ@áe±Ze¶ÒÜÈ.bM¤Ñ\ZzjŠÏkt €“`¤(0f«UŠ €PÔ&=%Eé))FÇ@!b2™ug—^FÇ8 Šp‡] &ýht €“ ¨\E ¸”¶õ”Åêft € ¨·‰ð¦aFG€|±hê/ʰ¥à(jbt`d±šËH p¡¨QˆÑ…Q«N]åéíkt €àÂñp)Ëfÿnt€“(‘L{€Â/<œïzŒBŸ ÈT E ¦=@áÁw=\ÿÂ¥Yû.ò-ht ÀPô¹€LL?…ÿÂeÕüIFG8  .‰¢\JÃðû\Òè'Àô¸”õ3ŽpŒÔ.‰¢\J½–\ªœÑ1N€¢€[¦e8—TܼMËçéä‘FÇ8Šn™å\R@þ¡¨—R«q¸J–¯jt €àê'p)[×Fà$©\E ‹äù«jXs…V©ct € ¨@c‘\ íŠ\©˜èÍFÇrÞ”¢6PÐ(j@ˆXMQ(h5àR*Öl¨Šµà¸ú \ÊÞmëŽpŒÔ.‰¢\JÙ*µU-¬…Ñ1N€¢à– 7:€Bâ`ôíŒ\at € ¨¸%"#"ŒŽ€B†¢\JÉrUT«I[£cœW?ÇáL&£S 9z ZGDà©\E ¸”àRåT¯e£cœE ¸”“GhÓòyFÇ(Ô›†ò„¢€KD¬Ž4:ä E ¸ÿb%Ô¨íýFÇ8Šp)gNÓºE3ŒŽp5'Ú75àR|‹ªYû.FÇ(P¬iyCQ.%ñl¬VÍŸdt € ¨—busWéŠÕŽp5àrÜ=¼Žp5àRléiÚ·}ƒÑ1N€¢\Š›»‡Ú<ð´Ñ1N€¢\JzZªOÿÕè'@Q¸$Šp)&“Iwvémt € ¨—âp8´`ÒFÇ8ŠÀ%QÔ€ËiûPOY¬nFÇÌjtàz-šú‹ÑN€‘À%QԀ˹£s7yxù`0Šp9KgWjò9£cà6Ð2<Ì订(0I&£CÀMXit×@Q.§y‡Gäã`t €Á¸ú \ÎÊyà© ¼)ëq¢\Nãv¨hPˆÑ1€kŠXÍz@Acú \ÎÚ…ÓŽpŒÔpË1@~ ¨à–c(&pû°X­²XŠüWÿŽ{T¢¬Ñ1£• ŒÙj•™¢ ÀÆ¥sŒŽp´2@IOI‘Éá0:(¤˜~—S§Ù )[Éèƒ1R.góªFG8Fj—DQ.§FÃ;Tºb £c Æô¸œíë—à©\E ¸œ*u›ª\µzFÇŒé'Ài„7 “$E¬‰48 œ]tÔj£#œ#5€Kb¤(8‡d2åysFh ¯*Ô¨/³Åª=[Ö` Šp9û¶o4:À 0ý¸$Šp9e+×Rµú-Ž0ÓOàrîÞjt€`¤"áaaFGnŠPˆDDrå·‡’媨V“¶FÇŒé'p9GDëèh£c ÆH à’(jÀå+ª°VŽ0E `ˆðp¶Å;u4F‘Ëæ`0ŠÀ,l nE ¸ÿb%Ô¨íýFÇ¥|™FGÀmŠ¢\ΙSÇ´nÑ £cȲÿÐ1£#à6EQ¸$Šp9¾EÕ¬}—|ݧUR­À@Õ ”5_÷ ((5@¾ãj&¸uÏÆjÕüIùºÏjzóóÏõæçŸ«Z``¾îÛaááFG€GQ.ÅØV}¿£çky%ÿXŠ©qÏõò}ååft–Û€Ù¿‰ïÿ¶:‡ºåš\%']D$W3œIdD„Ñp&ßzzäÿ†èñªFG\E ¸s‘ÚjÛñNUò³%w¦@µ¾Skg¿¥ê’̾ m|ZUõ»ôƒg Qó§^ÒýaNõ4y–VÝðŽªhÀ±öj¤ ŽiÞ‡­ä›ÏY,ÅÂÕý¹'Õ è÷uY¶‚rÓ9 óôöU«N]óuŸ;cc5äõ×5äõ×µ366_÷ÛÔUÚ&¿†z¤WO5 ¾Ê)­ËÛ‘®Ê™úP€$³üžÓ6kutœ¢¢ã´vÅ }ûF»\ß.]ñwiïÞÃJ²Kò ÓGý®×Ëßü¯HÚ§)ÏT¸lίš QÔ¢·Uõ&Oð»Wè­OøR÷•ÉûøßðIÚ½R=Ê]ö;EИè8Mx¢tÞž{F‚ŽîÝ©}GÎ*ý³ÜœŽ«µ´:Û©¨MÓÔ­Š—L—e#%)QËfÿž¯û´IÚ«­±±²åëžqÛºáößeíHWÅHp*æà‡4tÜ'jqt’¾8KÑgÌ ¬ÔXµÌ§u.Ãèt×É‘ Èa÷é±óÿfnIÞ¥íÔ¯½Ûè׬v‚¢¶ïÍÖ§wïÓÈ®Ýõ{t²º4@¾»¼ ઩§âY±£ÂKY=:-Nëî,ku2O³VQTtœ¢¢cµtö¯êÙ0àâßä­J®_UTtœ6¬øGƒ»T——)§tEÔlDŒ¢¿wq o[}±)NŸ Uæ@³‚ž¯M;¦«C ~ºOQs^Vh¶òaÅþkµ!kÊ/ü/Üüðoúg[œ¢¢O_™óf¸‡êÎþ“4+2NQѧ´dÆOz2Ì?kß4{]#æÈpRßtT?mN”#‡lfÿvê?nÍÕŸ‹$ÉC¥Û¿£¯æìÏÚæ¸–,øG=^éB­+ל&O…v¢Ñ +*:NWEhè3MTôükeÉz¿¯>ŒNjÁ_¨ûÓCôÕ¬]Z§¨¨ úö­äïr£žàJV7w…?ØÃè(Ì,yiKæò÷9KÎí?/µút½ÖæØ~ð¾´™—,yho…#5àTÒNlÑQuQËG:¨Ì–i:”â¸úÆÉ;ôïêx=Öä.…z,׎TI¾µÕ¦²=$Rñ梪ڢ‰‚w}¨×ܨ$ïÊjûÒ‡zu´MÑíziÙY³‚îþF¿~ÒB‡=§+Ψtç÷5øãIJÞÝDŸD^^FIÔÎù•qo+Õ °hDZ y„¶Q-o)èŽò£xù¨rx ™v¾§Íg*™Cì#c»ªß_1²É®s‡.ÜŸºk‚¾½L'Í¡ i¨^ýyVΫ“ÕCºØGõ”›%[EÆä§Fƒfkøƒ1úéíû´ðhZõùJýúBÚõÒ’ôfzíë7Uõ›ú߀•Šó(­PÏ-J°Ÿ¯xÆiÞëè‡)’=Y'ŽåÓ`\ïòªVA§G=§Á«âäV:\O ê§/?ß­{zþ¡9þRYFCz¼¬½îÍôÔ4ò‹ƒêÜs¼Ž¹7TŸqãÕÃk®¾ë?H›âU÷ÁþúonùÜJ뮿ѧ髮]ôcT‚®ú*x—Wãf•¯þ\2Ìòo3Lã¾yBI3ßÓC7)Þ£Žžøè}µª[LÖ?÷(Ý+·œf´©qÃÔñ?ªïœýòiô‚Þø·¾37W£•fÉz¿ïø@ýF­SR@uïM½Ñ Z3>¨WwœSð]oëí—Ñ+«êiðº+Ê„àRléiŠ˜6Æè(Ì,¹µ%M¹ÿ}ÎÚÕí?¿ÌûÏÿX_-: [pk=ùVööÃõgɵ½q+p QÔ€S±í­7Þª­/ßûYß3PË&ý¬?Æý¦åûuåtÂsÚ>}‰’:wV›2kÇž4yV¼[5=öh⺲«¨$)1z‘/Ù T-ÔÚCåÔzêêPÅKË"K¨sßežÝEƒ¾_ ‡´aÇ+ªÒ>B¨©/£6Ëêë¥ÌCö”Dœ®¢vµý4åX‚Š5j§›ÌµïQE¯yŠtTQ뾊™°L'3”cQ#åx´víØ}q®nÖÚñ[fjî‚ J•´þh%…OíªU¼´<Ò$_7eư+í\¼R²~µRÿ•ZÙ?‡9”ùsP{½øXˆÖ¼K_Í<.»¤mo•Ô ßÑÃu}µ$*P!¾Rì–…Z½q‡RµQ›³v‘9B"M±û¶j׎”äf¥èðʹZºõ9¥?»=zí‚F^žKbYÝóJWÛ:PÞø^m’ܩޙ÷UïüóhœKNK¨î}å]ÿŠü«ŽfHZ±ZÇ‚×ë—ú©îï/j]Ö*qw„–®Ø T­ÒÑÐGÕêÅúûÉZqNRdšZ<8Yš‡ÊmÝ7¸%’rü.âªmÉÅsÿûœµËÛf?IJÖ¾'iÁòDIKµ×¯½f¿‘Õ~8{½Y‚smo…ÓOà\ÉÚ3¡·¨_O=OS|½þúfþNí×JþW¼[JØ0FË+ëžöåä&w•lÖJÇækÉÁ´œö®ôã[tB~*îg•<+¨QyÉ·Ó$-ß•90jK„z”’ÊÈ»Üõûú}Z¶nŸ–­Û¯I}jÈíøbÍöTÝö5åeTXûPíúv¨"}[ëŽP¹• W‹àCZ¸`ßMuÓoÕIù©¸Ÿ»;OÓòuY9ÖGé†>¶;òÛ³zæñŽzúÂía}²òb§ß½LsU°º©ñg;™5äqãÂUU^*â#sÂ}ÿóf…ö[¡YãGè™ö5UÄ© éŠÛ£4k ‚¼óÀ]ÁukËW5Ôb.þòYLA*]ÔKÅkW—WJ”–ïͱJrUq‘ÿhGJ)=>äu(s½+z\ö\<+«YeéÀ¿‹”ó@÷ÜszUR³ŠÒÅ+tê™›Dí\°IéEë«NHNƒJÓwà´ä¢Ï¬.eúI:#yúòå P¸ë‘猎€ÛÌ%mÉúû|56Åí9x]m¡KÛµ¹µ7€Â‹‘pJöä­Ÿò‘ÖO©qÿ£?^ùVÏÌm¬‘ç.ÝοFÿ‰Õ=¤Ð1¿«áÝ·xºö¦èˆK¶ÏHW†ÌY£/L’I:þG7õ{鼌ăŠ?»]o>¹Q¾fI²+éà>¥ÛÒµtÎõ{âaU)þ¯:V?¦ÞŸ¦³m^×ã­ÊiFúƒ*wjŽæïM•.®ôqÝ.æ´+~Ùëzö逬)&):‘m¤BÒÁŠ\¿/Û í>r;.•Íú§É$“´ ÿýúzKö³]ÉÇOÉn·iý§íÔvƽz¤×«êõÍr=·b€žè=JGn$·-E6yÈ×óÒ.³Ù+@ž’l©W/õ8lé²Ë,óuœÒ5™$Ù×è£Çújmbö¥êtŒMŽÌ®·zÉ;zzн>f¬>(ïÇžÕ”}©yþýKž‹É$«EÊH·_ućãsæÆ–š&‡¼e1›$9$GºRl’Ù™s…ÿ6:n3—¶%óß×Óº$KÚ@aÅÉ:87G¢v/^¬c*¥j!çϘ›d1ÿ¶OЦ±¿ëh…§Õ¥e{uª§ˆi›•§óò)´1F ©ªsûwjÏî‹·ýÇ’”‘|H[VEhÕŠ­Z±DQÏÉ®4ÅÌýK1Áôø£Ï¨^ü<-=£ÕïWÙ‡žR—GªëÔÜ ŠÎi¶†#U‰©’W€·®g0Dú©MZ»â|ŽUÚŸ÷ËÀ¤Z£ö"ª^Å¡C»³?ÇhI8_[Wæ¹ IDAT ±éìŽúñõ;Õù¥™²·xIUö=-Aiò’žGNH©‡×é*¨uË2Ù*¦kþ *븶î‰ÏaQîrÎ’¦[¶+É\Yu¼ioöç·g¿Î¤§êĦMJðh¨»ëø]g'Þ¡ô#35ä‘ÎúåÄ=ß`ûïºåÚÞ /FjÀ©xÕý?½×ÝS›V®WÌ©d™j¨m¯Þ*aߨ1{’eO:¦³Õ½¿“jlŸ¬íq6¥îøE¿m{Q¯ "sü}¹5 Úöjæ7³ÕkøÇúîsš¾^'íþ*[ÅG~¯=WYF"}ÿMÛ3@}û„èèOj_jº2þ Co¼«§uH¿Ø¢Ïé§ÐÆÝézô‘Aê9Z;L¥UìÄlÍÜr£G+wöSsõý_G5ú¹ –1D“VÇ(Õ'T•·kúÄuŠw«¦û»5Qü–í:™æ£ŠM+É[gu2Ñ®ôØ Ú‘à§v¯¼¦{¿Y¦”àÒJY1AÛL×’þvwkÙigÚ3A£ÿí§OÎÒÈà/4sã)yU½O=ú´‘e㛿íÆÖæH?žS–?µbí0ßs¿zý‡â¿ø^Ëö%Ê=¤ŠJœš®‰KéܺÏõÓæ‡ôÊ÷)ù³¯µdO‚<ªÕ9¿6WîÇïìrèù¤¦Œ×Ç?íÓ¡GÞצœg6]#üÍüy…z¿?\Ÿ¿á®ŸÆÈ½zµ/#iMæ&¹æ´Ðìáé¹QŸiÄ»};÷€|=¯þiË'ô)I9ŽL€Â®íC=µdæoʰQÜ…òò÷Ùœsûoö±|Î’‡öPXQÔ€1Ëâ8©„â½ô쯨˜‡$%êÈÆiþôÛš“.»y¾F|µDŸ½ø©^œ3G}ÇK¶ýšùÕý÷»ÎŠÿémËóò :9û9u3¿¥}úêÃN~2)E§¢~Ó[“ÿО”«Œ)Hß§¿Ç®Wß÷‚õ÷´™Œƒ35}ç»êgýESv]eš‚ý„þ}§ŸZŒüP/ÝAJ?¢_¬Ñœ,jÈqF«Þï ¾§‡ªÏÃ4âE«”qZѳêß¿Ö)Ñ·œêÞÿ®º ’YRê±µš6èeM:˜.9–ê‹£Uò½—5ôçWe]§_zOU´—›äˆÓñ„&lfÕÜ~wÉôÊGzþ‘OôI/³”t@ë'öÓÀÏ~Õms&æ”e²VÛ¤o»Ý¯Ä·ÞS÷A¿ª›»äHØ£%#iòÒcÊHÛ©1=;*iÀ‡êùÆXuõ’”qVG¢þÑÖ¸¼x±Ÿü[=?T5§аÿ‹ÐƒžºÎð6žø”ž÷û\o<õ©¾~ÆS‰1QеKö GæÑ\sÚ·¨¯º¿zJo¿ö¡F>á%{l”æ}ÜIÿºK×[gn)‡#k®ÿMýÅ踭åáïóUÚóæäw–<´7€Âjò„ñe.NΛËÞÜ*÷wÌØ²ÔÑ£‚›áY ÷ÍÓQûímŽÈ9¯8Ê[ÎâÂ7¯fŽÁ«ãÓ_¬âp3: 7n|+è.[ÖðܸqãvÛÝhop» n“'Œw0R®ËZ\5ë”’ÜkéáwûÉ{F׬Ëz"'¡åK(fÿMŽu´–TÝzÚüÛÔÌK…!Lò­û”®¯ýŽ*ÁTBuzSnÓ7ÿÄpYU¸ wtî¦5 ¦)5ù\î…í ܾ(jÀeYK?¤ã‡ªž[œvL ç?ˆP¼ÃèTÎë¦ ’dÛ§ß­¦‰²)ïË•Þî¬*ZåÝß÷U ö–Ii:½cžFõ 1»ó~EÀ•–ÎotÀIÐÞÀíË4yÂxÇëftP ”§N¥»+zË$Iöxm›õ«ÆÎ^£˜D‡Üƒª©Í#Ϫk«Rr?»Vãf&ë¾TóøÉzkÄínøœªy\_ûéz»ï/º¤ÐA{RÝÍr³š$»‡¬¦+ž N­›¦ñÓ—hãÞX¥ÉK!µÃõسªYq·K¶t¤ÓÆù³4oùFí:”®ÚýGèµz^’ÅMYåa5Ëâf‘¬²\ñ8×É‘¦ãëgè·I y0^Ö¢ªÐ¤³zôè¨*>¦ Û]þ»Fý¹H»âìò m¦‡{÷T‡ŠÞR^Žkvö³Ú8æ=}¶ Míߦ•ܳ‡Qòþ¹ñáJxb¸>¸3(ëÌ𵎫¤ôƒšøÆ@M;‘ýÎ2ê:lˆ:‡XdOŒÖ¿þ©Y+wêTŠäR[mV5 –›T0ÇõzeÄjý_£5vÞLóTÉúÔó¹TÛ/ëÜø5^ƒŠšÇý]õ½˜ŸrxOØŽÎЀ×'êH›Wy”Þiâ“¿ÎËíó¶G¿¼ú®þ9så¯z5¤¯»Ÿ¾ùï™\?“¹|òú^±Åjý¸ôåê*zsÄóª~>ã5>9×(J?¤×zÖ”÷…;Ìò,^LÔP(eÄiËŠír¯×TUý.¶zScæè×¹'TýžgtI«N®ªñ“‡)µø0 h ³#^ëF}¤_¢kªë«ï©–Ï)­™0Zc>¶)xøKªïk×ÉE#4tµèù†zW´èð’ßôÃ÷Ce.ù™ž*f’IvÙlveØl²›Ì2]Ö!È5ƒ${ê9¥šƒÕñåÿ鎠̧ÉÝ_¥2{Çò ,"ïÔ¢r¿¢³a’9#UEÂÔK]ŠÉ·]óÆÏÔÈAªð~G…XΞuúeÈ·ZëÓTíÛ÷Ð}e‚\&³·añ ’¯›¯ŠùºÉ'ÈWn>AòÉsÇ!]Ç7¬Ôþ FjRîbçÆ‘´CSÆ.UjãôB—b²Z¦ ×§*£ÿ­#o“CÉ;×ïÊ»íSzµ»¢gŽÑØ!©*úÅ+j¦ÜëŽ$íúë\ž$ÓeÝ£ŒÄýZ5s¢þ˜¥XI.ù鵎«$GªÓL*ûè½P7«geöTPE’MÇ#~ÓÔ=!ºûɾ*ç¯Í³Çëï_*à‹÷Õ©„å&k~HWÌôOõÅÌd5ïÚG=ŽèŸ±“õÉp}þV{…X®ý4÷¿ÞIyÛßµÞ‹ùæ*ï K±;Ôgpu¥^˜McÓ±E£ôýÊbjšnðóàVZû¿«–içƒ9”²ÿo}ýëvÕhXRnнéïS®ŸÉk}òðÚ:ÒtrËMúã/-=&ùV¹d×ú<ä\Ôð-£jÕkÈ÷Š0é:2ÿ{}ó÷V<™(›$·àºêЩ–’×.ÒªmÇtÎá­²Íþ£z·WO“”qZ«~ýN­Û«£gÓ$¹)°RÝÓµ›î©îwqþKÚz }¶{æÿuÒ¯ûè»ST앯õ}e’äH\«O^¡„§Fd«Jr$kß?côÔÚŸà܃Õð©z¥mñËòÛt|Ñp úñˆZ¾1XžýR}F¥«Çˆ÷uw±Ì½¥î¥>ìÔÝC?Ñ#eÝ€Û@ú!Í;^~o4º¤¨áQñq i’õü)ÁÚ:ùÖn>¦ôVòH=¤Õ[“Uæ¡'tOý²¨’Ê<¹GËß\©Í'ÒUß7MûVî’­êÕõÎ:ò3I•JuצeiëÎ8Ù+5ÖӮ׈w_ÐïneÕúé—uÉ ¼ddO9«³¿*U­¨ ~—7â­ ¬T^Óƒrhü[Ø´›žizþß5»VoÌÚ©“éYEŒ“Zôõhm­ÓO_‚Ù§´*”+¯ò~ ¨XA’JÈ+¯§éÉÚ=ãM©WY Ëy_Ègò©£ç¾&³›%³cרš¼wmÔÑÛtÒVGå¬ Šš¡Ø’k`»TÚ*5¬ í~y´¦.;¡&r?®™2»r´†EëéW›éï¡ó³ý̦?iÜÆbêØ·—v~÷£âó|\%ÙSŸâ¡+©BÏË~hUÉNïè«{-²fujê–>£Mÿ7]¥ªS ï›;®ù!%Z³ç’ÿ]ï¨w§ªò”CU¼êåÏgjÞp=U!åÚ¯AçY;Ú£ñƒz뛸É-HÕÛ>®ç»6WÈå]-G.¯iç²äò^t$nÖøã´bïQIqHòUù;:)yxmÍIÛõçè¿×°»úÔ›£¯^º‹k}®s6‰]ñ{·h_jmõx­J[â5ígͳ]•îîª^JÈ|t™Æ§¯JÕÒ'ÿ)#7û9ˆÚ¡Ø2¨ïsä™z\QóÿÒøöë܇èÑ Yo%K =óÖ“ªê)™ÜŠªDPœêMTĺ¥6®)OIiG6iŸ-DwUõ»d1ôƒ35â×u*öp_½[?PŽ3Ç”âÙ›Ô®øMãôÑO»U½×ûz²^Q™7UYý¡µ{uW1?™”®“[w)Ñ·ºê§ ³¬Ù•ŽÔ3ŠK‘‚Ëd6¤­þ ’V¯Û¤cíK¨´›M±;wéŒweÕ ÎlO•õ—–­ÒæØ&jdVê±mÚŸâ¯ZUd1y(´ÃËú¢ÃMddOŠU’ɪԸX%zÊ×-{Ê¢âw¿¡A¹=U‡M ×hÞ²“ò«ÿ¸ÊfuzÒöÏÑÔ©²&þ¤~=N)É-X5ZwÑ37WIw“äY]½Þ«ž¹qý—4¸~n”¦Ì!æçe$ëÔY›¬Å˨¨URÚ1mÚo“ó: Î:f¿êj\JúmÓ%w*‘‡ã*eœˆÐ·¿P“W>TËbúû’ŸZU²Ó{ú®³Y¦”ÍvE§ôÚÇÕ‘– 3éV;¥Ø¤ ð¶^:ÌÞd¹d˜¾íìiS eõC 䏿]ú©ÚuÎ]U–UfIÆ$ŸÊMTÞ´FÛ£Ï*£t\®¯AfŸ­¨êtî®æ¥=tnÏ"ÿë ,§ï»liƒ<¼¦î¹¼©ÇµmÛQ½ÿ%½PÛW©kì¸ ÐX<ÖGIÒ¶?kÆ×¿¨ÚÈ~j\äÊJõßÙ8âùç$EvÔ;‚3ûž);õûˆIÚU½›ú?W]EÒOëxj¨¼oº•ËçáÒg SËÇiÖ±òz¼}5IR~|Ïä–ះ<¼¶¾>uõ¿‘#e2ÙutæÜ+÷qÏCÎE_é¹î_eÛAS ú¦jŸ/0© ºõj©„Eªì³[Kß]¯Êw´QÓJîRÝP¥­Y¡o"w+áá2 Ìúï2uÔ ¬¢Ü%……U”¹ÿ»ú{òfÝûZXæo Uè…"¦—š6ð×ì5«t0­¦ª¸ÛtjÛN%øÕP½Ë ös§•(…Õ®­ª½dR¥ËžCI»¦è“¯—*ðñwÕ'<$sžZP]5/õ›¦-Û­¤& ä£Dí‰:!÷*ÝT&ÇJ.n[§µrÌmöï ·ÂC2;1Ö’jÿâÚöÑ8½ñÚ:5«fÓ–õiêÐïÕ/b’䮊ÿyQìýLßôû?-mª³ë·¨è£ôD•®žS9”žjV÷=úiÐËúQ«s¯žîý5 ÊÛ9ÌŒ£³4èõ?uP’‚Û©ÿ“ TÔ,I:½u‹â,eÕ¾ýÔ¼‚¿ìGVè·¾Õ¶@ ëY]ž>z M‡ÿýA,¯‡>l,“¤ŒDJ”Šû^<‘iñUq?“gcuÎ.ùæ6U#ã¤ÿ0A'[öS¿š>2ŸÈa“ùÆÖ†dOM–§¿]ë¾ •’Jü°Lææ}Õ¹œsœ\µ'žÔ9ùªX¶ir÷W1oiëÉDeäá5("I*¦º-š(ÌÏ$Õ®(ó–újÕvíTJAÙD®ûËPB.ïÅÌ.œ›Š× SÝZžRÍ2J^µRß›©]«*j’ªûíÓ²·W*òhº¹¬Ó——÷ÄùM-Õ_k3TÿUþünl‰ŠM’ü*ÖQí*eä¦ ª|ƒÇÿêrø<\òãš3e§¼[Rxñœ¯”ã÷Ìud¸byù¼š®>E,9Ë…>ªAÏÕÑ…eN¬¾*y•ï\k‘Ñ9NÊZâÔä¡€ /é@‚R¯¶ê©G5ªé«¿·íÔI[˜|sÜÈ]¡­›*ðŸÕZÓ]U*ÓžM'ä]ói•½,‹G¥Nz Æzýñ~?íiy·:v¼KÍ*úe;ø»5vøn™ë¼¨/ï-wñ‹×¢&mÊhÂÔ¥ŠNª¯0Ó­?dQ¥g*ÉçVíÀ­ç8£ÅïþO£öd»oð3Êõ\C¯ýð¦ž_dÎvRËG} o·VѳƒW §^íJ:yD'¥Ô¢]?¾NÛRc´:b“î®ÞJÅ­’-ᄎœvWÅ6mUÇ{¿V˜“½l‰¶·.¯׳îÀU3˜T¤a-)#QG·-ÕÄÑãõÅP‹†~ÔE¡yè#[‚ÃÕï£j:qd‡–N¤ÏÞ5iÐG=TÛǦ„ã ’+µi]_Ü$U,£Þ1k4hÑííZ]5/ŸY‘ûÑá©ÔÒ±‹wE¿¡§&I’·Z½5Rÿ­‘µSGªþ3RŒ;®¦}ëþÐü:óèP¦?5áP]=ß·š¼MÊ÷«AX‚ÛiÀÈv’#]ñ£ôïï£5ñ“/åóù›º;[gÓ~6Rㆠ×ÒÀGõN¯Fºî¥(\‰ÉSÁ¥ŠH[â””¡K‹¹Êý½X=‡Ç ô”Å*É.µH–"!*¢ŧ\ÞY½ž÷DºbÎWL‘êVôbá˧–¾·œ†L¨W7·Q‡Žtgƒ²ò¾æóÌßÏCÒÎ9Z¢Ž÷V¹±"WýžQž3Üj95¼BT¾|…ÖÔ¸’Éê&‹ʰŸ_”Ä$«»E²Ûå¸ÖåˆÍ’éÛ¸—m­–Áó´xñ~=^"A«ö[U­sy]ñ½éªûÞüZ6-Òì3õÍÛÓõ÷ƒïèí.çGl«is­^9V?FTÒ+íJdŽÔEÅ›´U…?'êßçTÝwv¦—ÓcÕ‹ÜpE.ÂTD ÿ;DC’íRênûd’|{¼¦‡BÝ$³·BÎ7:m§´ü»Áúv[õü’ÂK^¬8â7èçï—©H÷az¡m,ê¨Ná¿kàû?kló0½–¬Åßÿ¢-•újxÏÌyõ÷ÜÛ\ß½1L£'µÐˆ^U•§ñ×Èp ‹¯JÖ¹G½znÓÆ/VjíñZ&£5¬¾ )WE!媨fE«ôûCÓ6ÿGµšyÊân‘’Ï*9CÊlD[åWª¨”§D[^Â_RÅÛõÓGa©r8Î)rÔgZPí%½Ò6X“UEJdGšÍ®ÁãN¨éËïêÙÆÙÎòú¨˜¯tød¢2”u&9#Q'2ùÊ'×ÎrŠvÿ³N ñúü…U—üäÐ;½µ·×0 ntňŠbr“_hC=ô|WmüßZ²õ¬î,˜¹Àk|¤~}˜–<ªw^&¡“0û“uêÜÅο#í¬N%I¾Á¾²XÒnè50YÍ™}ÅËëkjRâu¿¯ì›š¬î²È!ûÕëxO¤ÅhÉÊÓòo®K–K1y«úïë»–´`ö M>PSk=©ûwÈZ´7'ùðy¸ IÑ 7*©ÔýjQâFüäö=“§ 9=Å›ý¼^[þ_¡5/l'µmW¢,¥*+ÈÍ,»·›”ž¤d›CÊ^Np+£víC5kêLñV­Òç‡U›äZGåÝçèØñs²Ûâ}4C~uC.,jg.RIõJHËÄ*Í!yäÚ½v†+9²:p7ØA6™$Ùe³9$¹)¨RˆLó÷hëÉtÕ,ë&)U'v’š+è†NÒšäV´”Ê•äˆ×/³<ʨ|…ìk,8”´ýw}2îöýàÊΓ{IÕ-oUÄÖ­:e«¤RVÉ¿SëK¥-—‡5=U£×P}–íl}Æ©ÅöéUúß@=^Ï? Ù8— È8®#F*·‹;YAC’ÜŠÕPUŸ©Ú¼þ RëT‘‡JÚ³Fj]¥¨,î¹¾©×ó€¹¾¦yx/^×^.ïï‰ôc´1ÎWuš–Ρ(i‘ohc=ðb#µm·ùðy8/5F+·¥(¸]Ø…µ+ò.·ï™5¥£ûuþe5{‡¨lˆw¾U®›guêXFK'§ŸKvSË€#ú÷×ÕJ­ò¤:”s—Ln7ù\Æ”ÛkzãïÅ<Èã{¡„}[tÂTV•¾¬¤‘~XKþ‰–O…² pKÑámÇ”"où{™ øó¹7Û©ŠNôT¥ZÅu½ã4rýž±ç-CŽr}moNÎßr‡§køÇÓ/½/ø~ ýì~ ³[¼"§|£égír/VC_zV×ʼö®[¹ûõâƒ5jöúôÉ»l;õiÚH!n&™üèáÖþüošZ¶Ïé2³’íÌ^­š:WcãÒ%YäW¾±ºõy@åݤäìZŠéŽÞ/hSÿáúéç5ªõjsù›Mò®qŸ:–Z®I'j©Cƒ@ã¿@pkyÖQ¿¾¹âî´Û“.™ù…Þž™}ûó éWW÷AÏË:fŠÆ|ôR%y•¨­{úôT—òî’ÜÕøùzzÜXÍõ¦§Kæ"eTÿá×õÌÝ9-Àw¥\3˜Swh½æÎš¬Ø4IîAªÔèQ ìÖIesëÙØSuæÄÍŸ9C'’’ÅWÿßÞÝ…D…aÿ/¶›*ŠEE‘ˆ˜R"IXQä…!EQ!–˜Ñ‚I`-ˆ‰†fÈBH¥T$I%‰]‘àE”•”V.fšÙ²Ó…]H±®²»äæó»æ0gfÞaÎË9ïIHÊâHÑ6Åüž{²0•²ÃX®µP_ñ·%Ž”œв¦œöm¸¸'*Òâ}gŠ$£¼Œ¿:ý‰îngUÖÉ¢ÙYy‘ƒ+,D$îçìQƒú¦ªºY°4¼²Clœ E) cÃïéztŸæ7A\bùÖ}dņ.†^õò¯t\)§cò¹kNb³n˜V)€à2³,§”‡ë·jétÎ'~].g ·Nlõ‹)ÀÏÀw{¾ÞEo%ËÉÐ;,J#öÙ5Æø ¯»šyÜ8Š0G¯fsa[âÂ0úƒN{/v¢ÉŒ™ùÒŸßcz×à¥cAWSKs£±mG¶ß yå죹´Œ§ë+¨Í[ÉÌhìOÎsúî*¬Õ¼ì/í'c”ç—J°YNPs,EEBEDDDDü4Þ]ÇñêòëΑ¥l™ÛÁÑÞÖújjLƒ{´·ƒ<_žqóF?ɧ˜2ù3c?zp ¿¼ƒ­{ »ª’”Ðñ›“Ï=±¬ÝMªÏýEþwЇ`š¥I ƒ±žÛT]~ÁÏðŤå–R”àe!ÞÝ«¡²óæØd²‹‹Ù?Ko‡ˆˆˆˆHH1³|﮺ ÃêþEIDATM„i 'sžâ!˜‚¿üDDDDDDDD$ÀÚÛZUSDDDDDDDB“’"""""""’æÁÄ” ‘Pò Â!Âîo…YIEND®B`‚libindi/libs/indibase/alignment/DummyMathPlugin.cpp0000664000175000017500000000247613263645557021760 0ustar jasemjasem/// \file DummyMathPlugin.cpp /// \author Roger James /// \date 13th November 2013 #include "DummyMathPlugin.h" namespace INDI { namespace AlignmentSubsystem { // Standard functions required for all plugins extern "C" { DummyMathPlugin *Create() { return new DummyMathPlugin; } void Destroy(DummyMathPlugin *pPlugin) { delete pPlugin; } const char *GetDisplayName() { return "Dummy Math Plugin"; } } DummyMathPlugin::DummyMathPlugin() { //ctor } DummyMathPlugin::~DummyMathPlugin() { //dtor } bool DummyMathPlugin::Initialise(InMemoryDatabase *pInMemoryDatabase) { // Call the base class to initialise to in in memory database pointer MathPlugin::Initialise(pInMemoryDatabase); return false; } bool DummyMathPlugin::TransformCelestialToTelescope(const double RightAscension, const double Declination, double JulianOffset, TelescopeDirectionVector &ApparentTelescopeDirectionVector) { return false; } bool DummyMathPlugin::TransformTelescopeToCelestial(const TelescopeDirectionVector &ApparentTelescopeDirectionVector, double &RightAscension, double &Declination) { return false; } } // namespace AlignmentSubsystem } // namespace INDI libindi/libs/indibase/alignment/MathPluginManagement.cpp0000664000175000017500000004147713263645557022745 0ustar jasemjasem/*! * \file MathPluginManagement.cpp * * \author Roger James * \date 13th November 2013 * */ #include "MathPluginManagement.h" #include #include #include namespace INDI { namespace AlignmentSubsystem { MathPluginManagement::MathPluginManagement() : CurrentInMemoryDatabase(nullptr), pGetApproximateMountAlignment(&MathPlugin::GetApproximateMountAlignment), pInitialise(&MathPlugin::Initialise), pSetApproximateMountAlignment(&MathPlugin::SetApproximateMountAlignment), pTransformCelestialToTelescope(&MathPlugin::TransformCelestialToTelescope), pTransformTelescopeToCelestial(&MathPlugin::TransformTelescopeToCelestial), pLoadedMathPlugin(&BuiltInPlugin), LoadedMathPluginHandle(nullptr) { memset(&AlignmentSubsystemCurrentMathPlugin, 0, sizeof(IText)); } void MathPluginManagement::InitProperties(Telescope *ChildTelescope) { EnumeratePlugins(); AlignmentSubsystemMathPlugins.reset(new ISwitch[MathPluginDisplayNames.size() + 1]); IUFillSwitch(AlignmentSubsystemMathPlugins.get(), "INBUILT_MATH_PLUGIN", "Inbuilt Math Plugin", ISS_ON); for (int i = 0; i < (int)MathPluginDisplayNames.size(); i++) { IUFillSwitch(AlignmentSubsystemMathPlugins.get() + i + 1, MathPluginDisplayNames[i].c_str(), MathPluginDisplayNames[i].c_str(), ISS_OFF); } IUFillSwitchVector(&AlignmentSubsystemMathPluginsV, AlignmentSubsystemMathPlugins.get(), MathPluginDisplayNames.size() + 1, ChildTelescope->getDeviceName(), "ALIGNMENT_SUBSYSTEM_MATH_PLUGINS", "Math Plugins", ALIGNMENT_TAB, IP_RW, ISR_1OFMANY, 60, IPS_IDLE); ChildTelescope->registerProperty(&AlignmentSubsystemMathPluginsV, INDI_SWITCH); IUFillSwitch(&AlignmentSubsystemMathPluginInitialise, "ALIGNMENT_SUBSYSTEM_MATH_PLUGIN_INITIALISE", "OK", ISS_OFF); IUFillSwitchVector(&AlignmentSubsystemMathPluginInitialiseV, &AlignmentSubsystemMathPluginInitialise, 1, ChildTelescope->getDeviceName(), "ALIGNMENT_SUBSYSTEM_MATH_PLUGIN_INITIALISE", "(Re)Initialise Plugin", ALIGNMENT_TAB, IP_RW, ISR_ATMOST1, 60, IPS_IDLE); ChildTelescope->registerProperty(&AlignmentSubsystemMathPluginInitialiseV, INDI_SWITCH); IUFillSwitch(&AlignmentSubsystemActive, "ALIGNMENT SUBSYSTEM ACTIVE", "Alignment Subsystem Active", ISS_OFF); IUFillSwitchVector(&AlignmentSubsystemActiveV, &AlignmentSubsystemActive, 1, ChildTelescope->getDeviceName(), "ALIGNMENT_SUBSYSTEM_ACTIVE", "Activate alignment subsystem", ALIGNMENT_TAB, IP_RW, ISR_ATMOST1, 60, IPS_IDLE); ChildTelescope->registerProperty(&AlignmentSubsystemActiveV, INDI_SWITCH); // The following property is used for configuration purposes only and is not exposed to the client. IUFillText(&AlignmentSubsystemCurrentMathPlugin, "ALIGNMENT_SUBSYSTEM_CURRENT_MATH_PLUGIN", "Current Math Plugin", AlignmentSubsystemMathPlugins.get()[0].label); IUFillTextVector(&AlignmentSubsystemCurrentMathPluginV, &AlignmentSubsystemCurrentMathPlugin, 1, ChildTelescope->getDeviceName(), "ALIGNMENT_SUBSYSTEM_CURRENT_MATH_PLUGIN", "Current Math Plugin", ALIGNMENT_TAB, IP_RO, 60, IPS_IDLE); } void MathPluginManagement::ProcessTextProperties(Telescope *pTelescope, const char *name, char *texts[], char *names[], int n) { DEBUGFDEVICE(pTelescope->getDeviceName(), INDI::Logger::DBG_DEBUG, "ProcessTextProperties - name(%s)", name); if (strcmp(name, AlignmentSubsystemCurrentMathPluginV.name) == 0) { AlignmentSubsystemCurrentMathPluginV.s = IPS_OK; IUUpdateText(&AlignmentSubsystemCurrentMathPluginV, texts, names, n); if (0 != strcmp(AlignmentSubsystemMathPlugins.get()[0].label, AlignmentSubsystemCurrentMathPlugin.text)) { // Unload old plugin if required if (nullptr != LoadedMathPluginHandle) { typedef void Destroy_t(MathPlugin *); Destroy_t *Destroy = (Destroy_t *)dlsym(LoadedMathPluginHandle, "Destroy"); if (nullptr != Destroy) { Destroy(pLoadedMathPlugin); pLoadedMathPlugin = nullptr; if (0 == dlclose(LoadedMathPluginHandle)) { LoadedMathPluginHandle = nullptr; } else { IDLog("MathPluginManagement - dlclose failed on loaded plugin - %s\n", dlerror()); AlignmentSubsystemMathPluginsV.s = IPS_ALERT; } } else { IDLog("MathPluginManagement - cannot get Destroy function - %s\n", dlerror()); AlignmentSubsystemMathPluginsV.s = IPS_ALERT; } } // It is not the built in so try to load it if (nullptr != (LoadedMathPluginHandle = dlopen(AlignmentSubsystemCurrentMathPlugin.text, RTLD_NOW))) { typedef MathPlugin *Create_t(); Create_t *Create = (Create_t *)dlsym(LoadedMathPluginHandle, "Create"); if (nullptr != Create) { pLoadedMathPlugin = Create(); // TODO - Update the client to reflect the new plugin int i = 0; for (i = 0; i < (int)MathPluginFiles.size(); i++) { if (0 == strcmp(AlignmentSubsystemCurrentMathPlugin.text, MathPluginFiles[i].c_str())) break; } if (i < (int)MathPluginFiles.size()) { IUResetSwitch(&AlignmentSubsystemMathPluginsV); (AlignmentSubsystemMathPlugins.get() + i + 1)->s = ISS_ON; // Update client IDSetSwitch(&AlignmentSubsystemMathPluginsV, nullptr); } else { IDLog("MathPluginManagement - cannot find %s in list of plugins\n", MathPluginFiles[i].c_str()); } } else { IDLog("MathPluginManagement - cannot get Create function - %s\n", dlerror()); } } else { IDLog("MathPluginManagement - cannot load plugin %s error %s\n", AlignmentSubsystemCurrentMathPlugin.text, dlerror()); } } else { // It is the inbuilt plugin // Unload old plugin if required if (nullptr != LoadedMathPluginHandle) { typedef void Destroy_t(MathPlugin *); Destroy_t *Destroy = (Destroy_t *)dlsym(LoadedMathPluginHandle, "Destroy"); if (nullptr != Destroy) { Destroy(pLoadedMathPlugin); pLoadedMathPlugin = nullptr; if (0 == dlclose(LoadedMathPluginHandle)) { LoadedMathPluginHandle = nullptr; } else { IDLog("MathPluginManagement - dlclose failed on loaded plugin - %s\n", dlerror()); AlignmentSubsystemMathPluginsV.s = IPS_ALERT; } } else { IDLog("MathPluginManagement - cannot get Destroy function - %s\n", dlerror()); AlignmentSubsystemMathPluginsV.s = IPS_ALERT; } } pLoadedMathPlugin = &BuiltInPlugin; IUResetSwitch(&AlignmentSubsystemMathPluginsV); AlignmentSubsystemMathPlugins.get()->s = ISS_ON; // Update client IDSetSwitch(&AlignmentSubsystemMathPluginsV, nullptr); } } } void MathPluginManagement::ProcessSwitchProperties(Telescope *pTelescope, const char *name, ISState *states, char *names[], int n) { //DEBUGFDEVICE(pTelescope->getDeviceName(), INDI::Logger::DBG_DEBUG, "ProcessSwitchProperties - name(%s)", name); INDI_UNUSED(pTelescope); if (strcmp(name, AlignmentSubsystemMathPluginsV.name) == 0) { int CurrentPlugin = IUFindOnSwitchIndex(&AlignmentSubsystemMathPluginsV); IUUpdateSwitch(&AlignmentSubsystemMathPluginsV, states, names, n); AlignmentSubsystemMathPluginsV.s = IPS_OK; // Assume OK for the time being int NewPlugin = IUFindOnSwitchIndex(&AlignmentSubsystemMathPluginsV); if (NewPlugin != CurrentPlugin) { // New plugin requested // Unload old plugin if required if (0 != CurrentPlugin) { typedef void Destroy_t(MathPlugin *); Destroy_t *Destroy = (Destroy_t *)dlsym(LoadedMathPluginHandle, "Destroy"); if (nullptr != Destroy) { Destroy(pLoadedMathPlugin); pLoadedMathPlugin = nullptr; if (0 == dlclose(LoadedMathPluginHandle)) { LoadedMathPluginHandle = nullptr; } else { IDLog("MathPluginManagement - dlclose failed on loaded plugin - %s\n", dlerror()); AlignmentSubsystemMathPluginsV.s = IPS_ALERT; } } else { IDLog("MathPluginManagement - cannot get Destroy function - %s\n", dlerror()); AlignmentSubsystemMathPluginsV.s = IPS_ALERT; } } // Load the requested plugin if required if (0 != NewPlugin) { std::string PluginPath(MathPluginFiles[NewPlugin - 1]); if (nullptr != (LoadedMathPluginHandle = dlopen(PluginPath.c_str(), RTLD_NOW))) { typedef MathPlugin *Create_t(); Create_t *Create = (Create_t *)dlsym(LoadedMathPluginHandle, "Create"); if (nullptr != Create) { pLoadedMathPlugin = Create(); IUSaveText(&AlignmentSubsystemCurrentMathPlugin, PluginPath.c_str()); } else { IDLog("MathPluginManagement - cannot get Create function - %s\n", dlerror()); AlignmentSubsystemMathPluginsV.s = IPS_ALERT; } } else { IDLog("MathPluginManagement - cannot load plugin %s error %s\n", PluginPath.c_str(), dlerror()); AlignmentSubsystemMathPluginsV.s = IPS_ALERT; } } else { // It is in built plugin just set up the pointers pLoadedMathPlugin = &BuiltInPlugin; } } // Update client IDSetSwitch(&AlignmentSubsystemMathPluginsV, nullptr); } else if (strcmp(name, AlignmentSubsystemMathPluginInitialiseV.name) == 0) { AlignmentSubsystemMathPluginInitialiseV.s = IPS_OK; IUResetSwitch(&AlignmentSubsystemMathPluginInitialiseV); // Update client display IDSetSwitch(&AlignmentSubsystemMathPluginInitialiseV, nullptr); // Initialise or reinitialise the current math plugin Initialise(CurrentInMemoryDatabase); } else if (strcmp(name, AlignmentSubsystemActiveV.name) == 0) { AlignmentSubsystemActiveV.s = IPS_OK; if (0 == IUUpdateSwitch(&AlignmentSubsystemActiveV, states, names, n)) // Update client IDSetSwitch(&AlignmentSubsystemActiveV, nullptr); } } void MathPluginManagement::SetAlignmentSubsystemActive(bool enable) { AlignmentSubsystemActive.s = enable ? ISS_ON : ISS_OFF; AlignmentSubsystemActiveV.s = IPS_OK; IDSetSwitch(&AlignmentSubsystemActiveV, nullptr); } void MathPluginManagement::SaveConfigProperties(FILE *fp) { IUSaveConfigText(fp, &AlignmentSubsystemCurrentMathPluginV); IUSaveConfigSwitch(fp, &AlignmentSubsystemActiveV); } void MathPluginManagement::SetApproximateMountAlignmentFromMountType(MountType_t Type) { if (EQUATORIAL == Type) { ln_lnlat_posn Position { 0, 0 }; if (CurrentInMemoryDatabase->GetDatabaseReferencePosition(Position)) { if (Position.lat >= 0) SetApproximateMountAlignment(NORTH_CELESTIAL_POLE); else SetApproximateMountAlignment(SOUTH_CELESTIAL_POLE); } //else // TODO some kind of error!!! } else SetApproximateMountAlignment(ZENITH); } // These must match the function signatures in MathPlugin MountAlignment_t MathPluginManagement::GetApproximateMountAlignment() { return (pLoadedMathPlugin->*pGetApproximateMountAlignment)(); } bool MathPluginManagement::Initialise(InMemoryDatabase *pInMemoryDatabase) { return (pLoadedMathPlugin->*pInitialise)(pInMemoryDatabase); } void MathPluginManagement::SetApproximateMountAlignment(MountAlignment_t ApproximateAlignment) { (pLoadedMathPlugin->*pSetApproximateMountAlignment)(ApproximateAlignment); } bool MathPluginManagement::TransformCelestialToTelescope(const double RightAscension, const double Declination, double JulianOffset, TelescopeDirectionVector &ApparentTelescopeDirectionVector) { if (AlignmentSubsystemActive.s == ISS_ON) return (pLoadedMathPlugin->*pTransformCelestialToTelescope)(RightAscension, Declination, JulianOffset, ApparentTelescopeDirectionVector); else return false; } bool MathPluginManagement::TransformTelescopeToCelestial( const TelescopeDirectionVector &ApparentTelescopeDirectionVector, double &RightAscension, double &Declination) { if (AlignmentSubsystemActive.s == ISS_ON) return (pLoadedMathPlugin->*pTransformTelescopeToCelestial)(ApparentTelescopeDirectionVector, RightAscension, Declination); else return false; } void MathPluginManagement::EnumeratePlugins() { MathPluginFiles.clear(); MathPluginDisplayNames.clear(); #ifndef OSX_EMBEDED_MODE dirent *de; DIR *dp; errno = 0; char MATH_PLUGINS_DIRECTORY[2048]; #if defined(__APPLE__) const char *indiprefix = getenv("INDIPREFIX"); if (indiprefix) snprintf(MATH_PLUGINS_DIRECTORY, 2048 - 1, "%s/Contents/Resources/MathPlugins", indiprefix); else snprintf(MATH_PLUGINS_DIRECTORY, 2048 - 1, INDI_MATH_PLUGINS_DIRECTORY); #else snprintf(MATH_PLUGINS_DIRECTORY, 2048 - 1, INDI_MATH_PLUGINS_DIRECTORY); #endif dp = opendir(MATH_PLUGINS_DIRECTORY); snprintf(MATH_PLUGINS_DIRECTORY, 2048 - 1, "%s%s", MATH_PLUGINS_DIRECTORY, "/"); if (dp) { while (true) { void *Handle; std::string PluginPath(MATH_PLUGINS_DIRECTORY); errno = 0; de = readdir(dp); if (de == nullptr) break; if (0 == strcmp(de->d_name, ".")) continue; if (0 == strcmp(de->d_name, "..")) continue; // Try to load the plugin PluginPath.append(de->d_name); Handle = dlopen(PluginPath.c_str(), RTLD_NOW); if (nullptr == Handle) { IDLog("EnumeratePlugins - cannot load plugin %s error %s\n", PluginPath.c_str(), dlerror()); continue; } // Try to get the plugin display name typedef const char *GetDisplayName_t(); GetDisplayName_t *GetDisplayNamePtr = (GetDisplayName_t *)dlsym(Handle, "GetDisplayName"); if (nullptr == GetDisplayNamePtr) { IDLog("EnumeratePlugins - cannot get plugin %s DisplayName error %s\n", PluginPath.c_str(), dlerror()); continue; } IDLog("EnumeratePlugins - found plugin %s\n", GetDisplayNamePtr()); MathPluginDisplayNames.push_back(GetDisplayNamePtr()); MathPluginFiles.push_back(PluginPath); dlclose(Handle); } closedir(dp); } else { IDLog("EnumeratePlugins - Failed to open %s error %s\n", MATH_PLUGINS_DIRECTORY, strerror(errno)); } #endif } } // namespace AlignmentSubsystem } // namespace INDI libindi/libs/indibase/alignment/MapPropertiesToInMemoryDatabase.cpp0000664000175000017500000003423213263645557025071 0ustar jasemjasem/*! * \file MapPropertiesToInMemoryDatabase.cpp * * \author Roger James * \date 13th November 2013 * */ #include "MapPropertiesToInMemoryDatabase.h" #include namespace INDI { namespace AlignmentSubsystem { void MapPropertiesToInMemoryDatabase::InitProperties(Telescope *pTelescope) { IUFillNumber(&AlignmentPointSetEntry[ENTRY_OBSERVATION_JULIAN_DATE], "ALIGNMENT_POINT_ENTRY_OBSERVATION_JULIAN_DATE", "Observation Julian date", "%g", 0, 60000, 0, 0); IUFillNumber(&AlignmentPointSetEntry[ENTRY_RA], "ALIGNMENT_POINT_ENTRY_RA", "Right Ascension (hh:mm:ss)", "%010.6m", 0, 24, 0, 0); IUFillNumber(&AlignmentPointSetEntry[ENTRY_DEC], " ALIGNMENT_POINT_ENTRY_DEC", "Declination (dd:mm:ss)", "%010.6m", -90, 90, 0, 0); IUFillNumber(&AlignmentPointSetEntry[ENTRY_VECTOR_X], "ALIGNMENT_POINT_ENTRY_VECTOR_X", "Telescope direction vector x", "%g", -FLT_MAX, FLT_MAX, 0, 0); IUFillNumber(&AlignmentPointSetEntry[ENTRY_VECTOR_Y], " ALIGNMENT_POINT_ENTRY_VECTOR_Y", "Telescope direction vector y", "%g", -FLT_MAX, FLT_MAX, 0, 0); IUFillNumber(&AlignmentPointSetEntry[ENTRY_VECTOR_Z], " ALIGNMENT_POINT_ENTRY_VECTOR_Z", "Telescope direction vector z", "%g", -FLT_MAX, FLT_MAX, 0, 0); IUFillNumberVector(&AlignmentPointSetEntryV, AlignmentPointSetEntry, 6, pTelescope->getDeviceName(), "ALIGNMENT_POINT_MANDATORY_NUMBERS", "Mandatory sync point numeric fields", ALIGNMENT_TAB, IP_RW, 60, IPS_IDLE); pTelescope->registerProperty(&AlignmentPointSetEntryV, INDI_NUMBER); IUFillBLOB(&AlignmentPointSetPrivateBinaryData, "ALIGNMENT_POINT_ENTRY_PRIVATE", "Private binary data", "alignmentPrivateData"); IUFillBLOBVector(&AlignmentPointSetPrivateBinaryDataV, &AlignmentPointSetPrivateBinaryData, 1, pTelescope->getDeviceName(), "ALIGNMENT_POINT_OPTIONAL_BINARY_BLOB", "Optional sync point binary data", ALIGNMENT_TAB, IP_RW, 60, IPS_IDLE); pTelescope->registerProperty(&AlignmentPointSetPrivateBinaryDataV, INDI_BLOB); IUFillNumber(&AlignmentPointSetSize, "ALIGNMENT_POINTSET_SIZE", "Size", "%g", 0, 100000, 0, 0); IUFillNumberVector(&AlignmentPointSetSizeV, &AlignmentPointSetSize, 1, pTelescope->getDeviceName(), "ALIGNMENT_POINTSET_SIZE", "Current Set", ALIGNMENT_TAB, IP_RO, 60, IPS_IDLE); pTelescope->registerProperty(&AlignmentPointSetSizeV, INDI_NUMBER); IUFillNumber(&AlignmentPointSetPointer, "ALIGNMENT_POINTSET_CURRENT_ENTRY", "Pointer", "%g", 0, 100000, 0, 0); IUFillNumberVector(&AlignmentPointSetPointerV, &AlignmentPointSetPointer, 1, pTelescope->getDeviceName(), "ALIGNMENT_POINTSET_CURRENT_ENTRY", "Current Set", ALIGNMENT_TAB, IP_RW, 60, IPS_IDLE); pTelescope->registerProperty(&AlignmentPointSetPointerV, INDI_NUMBER); IUFillSwitch(&AlignmentPointSetAction[0], "APPEND", "Add entries at end of set", ISS_ON); IUFillSwitch(&AlignmentPointSetAction[1], "INSERT", "Insert entries at current index", ISS_OFF); IUFillSwitch(&AlignmentPointSetAction[2], "EDIT", "Overwrite entry at current index", ISS_OFF); IUFillSwitch(&AlignmentPointSetAction[3], "DELETE", "Delete entry at current index", ISS_OFF); IUFillSwitch(&AlignmentPointSetAction[4], "CLEAR", "Delete all the entries in the set", ISS_OFF); IUFillSwitch(&AlignmentPointSetAction[5], "READ", "Read the entry at the current pointer", ISS_OFF); IUFillSwitch(&AlignmentPointSetAction[6], "READ INCREMENT", "Increment the pointer before reading the entry", ISS_OFF); IUFillSwitch(&AlignmentPointSetAction[7], "LOAD DATABASE", "Load the alignment database from local storage", ISS_OFF); IUFillSwitch(&AlignmentPointSetAction[8], "SAVE DATABASE", "Save the alignment database to local storage", ISS_OFF); IUFillSwitchVector(&AlignmentPointSetActionV, AlignmentPointSetAction, 9, pTelescope->getDeviceName(), "ALIGNMENT_POINTSET_ACTION", "Action to take", ALIGNMENT_TAB, IP_RW, ISR_1OFMANY, 60, IPS_IDLE); pTelescope->registerProperty(&AlignmentPointSetActionV, INDI_SWITCH); IUFillSwitch(&AlignmentPointSetCommit, "ALIGNMENT_POINTSET_COMMIT", "OK", ISS_OFF); IUFillSwitchVector(&AlignmentPointSetCommitV, &AlignmentPointSetCommit, 1, pTelescope->getDeviceName(), "ALIGNMENT_POINTSET_COMMIT", "Execute the action", ALIGNMENT_TAB, IP_RW, ISR_ATMOST1, 60, IPS_IDLE); pTelescope->registerProperty(&AlignmentPointSetCommitV, INDI_SWITCH); } void MapPropertiesToInMemoryDatabase::ProcessBlobProperties(Telescope *pTelescope, const char *name, int sizes[], int blobsizes[], char *blobs[], char *formats[], char *names[], int n) { DEBUGFDEVICE(pTelescope->getDeviceName(), INDI::Logger::DBG_DEBUG, "ProcessBlobProperties - name(%s)", name); if (strcmp(name, AlignmentPointSetPrivateBinaryDataV.name) == 0) { AlignmentPointSetPrivateBinaryDataV.s = IPS_OK; if (0 == IUUpdateBLOB(&AlignmentPointSetPrivateBinaryDataV, sizes, blobsizes, blobs, formats, names, n)) { // Reset the blob format string just in case it got trashed strncpy(AlignmentPointSetPrivateBinaryData.format, "alignmentPrivateData", MAXINDIBLOBFMT); // Send back a dummy zero length blob // to inform client I have received the data IBLOB DummyBlob; IBLOBVectorProperty DummyBlobV; IUFillBLOB(&DummyBlob, "ALIGNMENT_POINT_ENTRY_PRIVATE", "Private binary data", "alignmentPrivateData"); IUFillBLOBVector(&DummyBlobV, &DummyBlob, 1, pTelescope->getDeviceName(), "ALIGNMENT_POINT_OPTIONAL_BINARY_BLOB", "Optional sync point binary data", ALIGNMENT_TAB, IP_RW, 60, IPS_OK); IDSetBLOB(&DummyBlobV, nullptr); } } } void MapPropertiesToInMemoryDatabase::ProcessNumberProperties(Telescope *pTelescope, const char *name, double values[], char *names[], int n) { DEBUGFDEVICE(pTelescope->getDeviceName(), INDI::Logger::DBG_DEBUG, "ProcessNumberProperties - name(%s)", name); if (strcmp(name, AlignmentPointSetEntryV.name) == 0) { AlignmentPointSetEntryV.s = IPS_OK; if (0 == IUUpdateNumber(&AlignmentPointSetEntryV, values, names, n)) // Update client IDSetNumber(&AlignmentPointSetEntryV, nullptr); } else if (strcmp(name, AlignmentPointSetPointerV.name) == 0) { AlignmentPointSetPointerV.s = IPS_OK; if (0 == IUUpdateNumber(&AlignmentPointSetPointerV, values, names, n)) // Update client IDSetNumber(&AlignmentPointSetPointerV, nullptr); } } void MapPropertiesToInMemoryDatabase::ProcessSwitchProperties(Telescope *pTelescope, const char *name, ISState *states, char *names[], int n) { //DEBUGFDEVICE(pTelescope->getDeviceName(), INDI::Logger::DBG_DEBUG, "ProcessSwitchProperties - name(%s)", name); AlignmentDatabaseType &AlignmentDatabase = GetAlignmentDatabase(); if (strcmp(name, AlignmentPointSetActionV.name) == 0) { AlignmentPointSetActionV.s = IPS_OK; if (0 == IUUpdateSwitch(&AlignmentPointSetActionV, states, names, n)) // Update client IDSetSwitch(&AlignmentPointSetActionV, nullptr); } else if (strcmp(name, AlignmentPointSetCommitV.name) == 0) { const unsigned int Offset = AlignmentPointSetPointer.value; AlignmentPointSetCommitV.s = IPS_OK; // Perform the database action AlignmentDatabaseEntry CurrentValues; CurrentValues.ObservationJulianDate = AlignmentPointSetEntry[ENTRY_OBSERVATION_JULIAN_DATE].value; CurrentValues.RightAscension = AlignmentPointSetEntry[ENTRY_RA].value; CurrentValues.Declination = AlignmentPointSetEntry[ENTRY_DEC].value; CurrentValues.TelescopeDirection.x = AlignmentPointSetEntry[ENTRY_VECTOR_X].value; CurrentValues.TelescopeDirection.y = AlignmentPointSetEntry[ENTRY_VECTOR_Y].value; CurrentValues.TelescopeDirection.z = AlignmentPointSetEntry[ENTRY_VECTOR_Z].value; if ((0 != AlignmentPointSetPrivateBinaryData.size) && (nullptr != AlignmentPointSetPrivateBinaryData.blob)) { CurrentValues.PrivateData.reset(new unsigned char[AlignmentPointSetPrivateBinaryData.size]); memcpy(CurrentValues.PrivateData.get(), AlignmentPointSetPrivateBinaryData.blob, AlignmentPointSetPrivateBinaryData.size); CurrentValues.PrivateDataSize = AlignmentPointSetPrivateBinaryData.size; } if (AlignmentPointSetAction[APPEND].s == ISS_ON) { AlignmentDatabase.push_back(CurrentValues); AlignmentPointSetSize.value = AlignmentDatabase.size(); // Update client IDSetNumber(&AlignmentPointSetSizeV, nullptr); } else if (AlignmentPointSetAction[INSERT].s == ISS_ON) { if (Offset > AlignmentDatabase.size()) AlignmentPointSetCommitV.s = IPS_ALERT; else { AlignmentDatabase.insert(AlignmentDatabase.begin() + Offset, CurrentValues); AlignmentPointSetSize.value = AlignmentDatabase.size(); // Update client IDSetNumber(&AlignmentPointSetSizeV, nullptr); } } else if (AlignmentPointSetAction[EDIT].s == ISS_ON) { if (Offset >= AlignmentDatabase.size()) AlignmentPointSetCommitV.s = IPS_ALERT; else AlignmentDatabase[Offset] = CurrentValues; } else if (AlignmentPointSetAction[DELETE].s == ISS_ON) { if (Offset >= AlignmentDatabase.size()) AlignmentPointSetCommitV.s = IPS_ALERT; else { AlignmentDatabase.erase(AlignmentDatabase.begin() + Offset); AlignmentPointSetSize.value = AlignmentDatabase.size(); // Update client IDSetNumber(&AlignmentPointSetSizeV, nullptr); } } else if (AlignmentPointSetAction[CLEAR].s == ISS_ON) { // AlignmentDatabaseType().swap(AlignmentDatabase); // Do it this wasy to force a reallocation AlignmentDatabase.clear(); AlignmentPointSetSize.value = 0; // Update client IDSetNumber(&AlignmentPointSetSizeV, nullptr); } else if ((AlignmentPointSetAction[READ].s == ISS_ON) || (AlignmentPointSetAction[READ_INCREMENT].s == ISS_ON)) { if (AlignmentPointSetAction[READ_INCREMENT].s == ISS_ON) { AlignmentPointSetPointer.value++; // Update client IDSetNumber(&AlignmentPointSetPointerV, nullptr); } if (Offset >= AlignmentDatabase.size()) AlignmentPointSetCommitV.s = IPS_ALERT; else { AlignmentPointSetEntry[ENTRY_OBSERVATION_JULIAN_DATE].value = AlignmentDatabase[Offset].ObservationJulianDate; AlignmentPointSetEntry[ENTRY_RA].value = AlignmentDatabase[Offset].RightAscension; AlignmentPointSetEntry[ENTRY_DEC].value = AlignmentDatabase[Offset].Declination; AlignmentPointSetEntry[ENTRY_VECTOR_X].value = AlignmentDatabase[Offset].TelescopeDirection.x; AlignmentPointSetEntry[ENTRY_VECTOR_Y].value = AlignmentDatabase[Offset].TelescopeDirection.y; AlignmentPointSetEntry[ENTRY_VECTOR_Z].value = AlignmentDatabase[Offset].TelescopeDirection.z; // Update client IDSetNumber(&AlignmentPointSetEntryV, nullptr); if ((0 != AlignmentDatabase[Offset].PrivateDataSize) && (nullptr != AlignmentDatabase[Offset].PrivateData.get())) { // Hope that INDI has freed the old pointer !!!!!!!!!!! AlignmentPointSetPrivateBinaryData.blob = malloc(AlignmentDatabase[Offset].PrivateDataSize); memcpy(AlignmentPointSetPrivateBinaryData.blob, AlignmentDatabase[Offset].PrivateData.get(), AlignmentDatabase[Offset].PrivateDataSize); AlignmentPointSetPrivateBinaryData.bloblen = AlignmentDatabase[Offset].PrivateDataSize; AlignmentPointSetPrivateBinaryData.size = AlignmentDatabase[Offset].PrivateDataSize; AlignmentPointSetPrivateBinaryDataV.s = IPS_OK; IDSetBLOB(&AlignmentPointSetPrivateBinaryDataV, nullptr); } } } else if (AlignmentPointSetAction[LOAD_DATABASE].s == ISS_ON) { LoadDatabase(pTelescope->getDeviceName()); AlignmentPointSetSize.value = AlignmentDatabase.size(); // Update client IDSetNumber(&AlignmentPointSetSizeV, nullptr); } else if (AlignmentPointSetAction[SAVE_DATABASE].s == ISS_ON) { SaveDatabase(pTelescope->getDeviceName()); } // Update client IUResetSwitch(&AlignmentPointSetCommitV); IDSetSwitch(&AlignmentPointSetCommitV, nullptr); } } void MapPropertiesToInMemoryDatabase::UpdateLocation(double latitude, double longitude, double elevation) { INDI_UNUSED(elevation); ln_lnlat_posn Position { 0, 0 }; if (GetDatabaseReferencePosition(Position)) { // Position is already valid if ((latitude != Position.lat) || (longitude != Position.lng)) { // Warn the user somehow } } else SetDatabaseReferencePosition(latitude, longitude); } void MapPropertiesToInMemoryDatabase::UpdateSize() { AlignmentPointSetSize.value = GetAlignmentDatabase().size(); // Update client IDSetNumber(&AlignmentPointSetSizeV, nullptr); } } // namespace AlignmentSubsystem } // namespace INDI libindi/libs/indibase/alignment/ClientAPIForAlignmentDatabase.h0000664000175000017500000001437513263645557024045 0ustar jasemjasem/*! * \file ClientAPIForAlignmentDatabase.h * * \author Roger James * \date 13th November 2013 * */ #pragma once #include "basedevice.h" #include "baseclient.h" #include "Common.h" namespace INDI { namespace AlignmentSubsystem { /*! * \class ClientAPIForAlignmentDatabase * \brief This class provides the client API to the driver side alignment database. It communicates * with the driver via the INDI properties interface. */ class ClientAPIForAlignmentDatabase { public: /// \brief Default constructor ClientAPIForAlignmentDatabase(); /// \brief Virtual destructor virtual ~ClientAPIForAlignmentDatabase(); /** * \brief Append a sync point to the database. * \param[in] CurrentValues The entry to append. * \return True if successful */ bool AppendSyncPoint(const AlignmentDatabaseEntry &CurrentValues); /** * \brief Delete all sync points from the database. * \return True if successful */ bool ClearSyncPoints(); /** * \brief Delete a sync point from the database. * \param[in] Offset Pointer to the entry to delete * \return True if successful */ bool DeleteSyncPoint(unsigned int Offset); /** * \brief Edit a sync point in the database. * \param[in] Offset Pointer to where to make the edit. * \param[in] CurrentValues The entry to edit. * \return True if successful */ bool EditSyncPoint(unsigned int Offset, const AlignmentDatabaseEntry &CurrentValues); /** * \brief Return the number of entries in the database. * \return The number of entries in the database */ int GetDatabaseSize(); /** * \brief Initialise the API * \param[in] BaseClient A pointer to the INDI::BaseClient class */ void Initialise(INDI::BaseClient *BaseClient); /** * \brief Insert a sync point in the database. * \param[in] Offset Pointer to where to make then insertion. * \param[in] CurrentValues The entry to insert. * \return True if successful */ bool InsertSyncPoint(unsigned int Offset, const AlignmentDatabaseEntry &CurrentValues); /** * \brief Load the database from persistent storage * \return True if successful */ bool LoadDatabase(); /** * \brief Process new BLOB message from driver. This routine should be called from within * the newBLOB handler in the client. This routine is not normally called directly but is called by * the ProcessNewBLOB function in INDI::Alignment::AlignmentSubsystemForClients which filters out * calls from unwanted devices. TODO maybe hide this function. * \param[in] BLOBPointer A pointer to the INDI::IBLOB. */ void ProcessNewBLOB(IBLOB *BLOBPointer); /** * \brief Process new device message from driver. This routine should be called from within * the newDevice handler in the client. This routine is not normally called directly but is * called by the ProcessNewDevice function in INDI::Alignment::AlignmentSubsystemForClients which * filters out calls from unwanted devices. TODO maybe hide this function. * \param[in] DevicePointer A pointer to the INDI::BaseDevice object. */ void ProcessNewDevice(INDI::BaseDevice *DevicePointer); /** * \brief Process new number message from driver. This routine should be called from within * the newNumber handler in the client. This routine is not normally called directly but * it is called by the ProcessNewNumber function in INDI::Alignment::AlignmentSubsystemForClients * which filters out calls from unwanted devices. TODO maybe hide this function. * \param[in] NumberVectorProperty A pointer to the INDI::INumberVectorProperty. */ void ProcessNewNumber(INumberVectorProperty *NumberVectorProperty); /** * \brief Process new property message from driver. This routine should be called from within * the newProperty handler in the client. This routine is not normally called directly but it is * called by the ProcessNewProperty function in INDI::Alignment::AlignmentSubsystemForClients * which filters out calls from unwanted devices. TODO maybe hide this function. * \param[in] PropertyPointer A pointer to the INDI::Property object. */ void ProcessNewProperty(INDI::Property *PropertyPointer); /** * \brief Process new switch message from driver. This routine should be called from within * the newSwitch handler in the client. This routine is not normally called directly but it is * called by the ProcessNewSwitch function in INDI::Alignment::AlignmentSubsystemForClients * which filters out calls from unwanted devices. TODO maybe hide this function. * \param[in] SwitchVectorProperty A pointer to the INDI::ISwitchVectorProperty. */ void ProcessNewSwitch(ISwitchVectorProperty *SwitchVectorProperty); /** * \brief Increment the current offset then read a sync point from the database. * \param[out] CurrentValues The entry read. * \return True if successful */ bool ReadIncrementSyncPoint(AlignmentDatabaseEntry &CurrentValues); /** * \brief Read a sync point from the database. * \param[in] Offset Pointer to where to read from. * \param[out] CurrentValues The entry read. * \return True if successful */ bool ReadSyncPoint(unsigned int Offset, AlignmentDatabaseEntry &CurrentValues); /** * \brief Save the database to persistent storage * \return True if successful */ bool SaveDatabase(); private: // Private methods bool SendEntryData(const AlignmentDatabaseEntry &CurrentValues); bool SetDriverBusy(); bool SignalDriverCompletion(); bool WaitForDriverCompletion(); // Private properties INDI::BaseClient *BaseClient { nullptr }; pthread_cond_t DriverActionCompleteCondition; pthread_mutex_t DriverActionCompleteMutex; bool DriverActionComplete { false }; INDI::BaseDevice *Device { nullptr }; INDI::Property *MandatoryNumbers { nullptr }; INDI::Property *OptionalBinaryBlob { nullptr }; INDI::Property *PointsetSize { nullptr }; INDI::Property *CurrentEntry { nullptr }; INDI::Property *Action { nullptr }; INDI::Property *Commit { nullptr }; }; } // namespace AlignmentSubsystem } // namespace INDI libindi/libs/indibase/indifilterinterface.h0000664000175000017500000001132713263645557020370 0ustar jasemjasem/* Filter Interface Copyright (C) 2011 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 */ #pragma once #include #include #include "indibase.h" /** * \class FilterInterface \brief Provides interface to implement Filter Wheel functionality. A filter wheel can be an independent device, or an embedded filter wheel within another device (e.g. CCD camera). Child class must implement all the pure virtual functions and call SelectFilterDone(int) when selection of a new filter position is complete in the hardware. \e IMPORTANT: initFilterProperties() must be called before any other function to initilize the filter properties. \e IMPORTANT: processFilterSlot() must be called in your driver's ISNewNumber() function. processFilterSlot() will call the driver's SelectFilter() accordingly. \note Filter position starts from 1 and \e not 0 \author Gerry Rozema, Jasem Mutlaq */ namespace INDI { class FilterInterface { public: /** \brief Return current filter position */ virtual int QueryFilter() = 0; /** * \brief Select a new filter position * \return True if operation is successful, false otherwise */ virtual bool SelectFilter(int position) = 0; /** * \brief Set filter names as defined by the client for each filter position. * The desired filter names are stored in FilterNameTP property. Filter names should be * saved in hardware if possible. The default implementation saves them in the configuration file. * \return True if successful, false if supported or failed operation */ virtual bool SetFilterNames(); /** * \brief Obtains a list of filter names from the hardware and initializes the FilterNameTP * property. The function should check for the number of filters available in the filter * wheel and build the FilterNameTP property accordingly. The default implementation loads the filter names from * configuration file. * \return True if successful, false if unsupported or failed operation */ virtual bool GetFilterNames(); /** * \brief The child class calls this function when the hardware successfully finished * selecting a new filter wheel position * \param newpos New position of the filter wheel */ void SelectFilterDone(int newpos); protected: /** * @brief FilterInterface Initiailize Filter Interface * @param defaultDevice default device that owns the interface */ explicit FilterInterface(DefaultDevice *defaultDevice); ~FilterInterface(); /** * \brief Initilize filter wheel properties. It is recommended to call this function within * initProperties() of your primary device * \param groupName Group or tab name to be used to define filter wheel properties. */ void initProperties(const char *groupName); /** * @brief updateProperties Defines or Delete proprties based on default device connection status * @return True if all is OK, false otherwise. */ bool updateProperties(); /** \brief Process number properties */ bool processNumber(const char *dev, const char *name, double values[], char *names[], int n); /** \brief Process text properties */ bool processText(const char *dev, const char *name, char *texts[], char *names[], int n); /** * @brief generateSampleFilters Generate sample 8-filter wheel and fill it sample filters */ void generateSampleFilters(); /** * @brief saveConfigItems save Filter Names in config file * @param fp pointer to config file * @return Always return true */ bool saveConfigItems(FILE *fp); // A number vector for filter slot INumberVectorProperty FilterSlotNP; INumber FilterSlotN[1]; // A text vector that stores out physical port name ITextVectorProperty *FilterNameTP { nullptr }; IText *FilterNameT; int CurrentFilter; int TargetFilter; bool loadingFromConfig = false; DefaultDevice *m_defaultDevice { nullptr }; }; } libindi/libs/indibase/indistandardproperty.h0000664000175000017500000001715113263645557020630 0ustar jasemjasem/******************************************************************************* Copyright(c) 2017 Jasem Mutlaq. All rights reserved. List of INDI Stanadrd Properties This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2 as published by the Free Software Foundation. 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 Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. *******************************************************************************/ #pragma once #include "indibase.h" namespace INDI { /** * @namespace INDI::SP @brief INDI Standard Properties are common properties standarized across drivers and clients alike. INDI does not place any special semantics on property names (i.e. properties are just texts, numbers, or switches that represent no physical function). While GUI clients can construct graphical representation of properties in order to permit the user to operate the device, we run into situations where clients and drivers need to agree on the exact meaning of some fundamental properties. What if some client need to be aware of the existence of some property in order to perform some function useful to the user? How can that client tie itself to such a property if the property can be arbitrary defined by drivers? The solution is to define Standard Properties in order to establish a level of interoperability among INDI drivers and clients. We propose a set of shared INDI properties that encapsulate the most common characteristics of astronomical instrumentation of interest. If the semantics of such properties are properly defined, not only it will insure base interoperability, but complete device automation becomes possible as well. Put another way, INDI standard properties are in essence properties that represent a clearly defined characteristic related to the operation of the device drivers. For example, a very common standard property is EQUATORIAL_EOD_COORD. This property represents the telescope's current RA and DEC. Clients need to be aware of this property in order to, for example, draw the telescope's cross hair on the sky map. If you write a script to control a telescope, you know that any telescope supporting EQUATORIAL_EOD_COORD will behave in an expected manner when the property is invoked. INDI clients are required to honor standard properties if when and they implement any functions associated with a particular standard property. Furthermore, INDI drivers employing standard properties should strictly adhere to the standard properties structure as defined next. The properties are defined as string constants. To refer to the property in device drivers, use INDI::StandardProperty::PROPERTY_NAME or the shortcut INDI::SP::PROPERTY_NAME. The standard properties are divided into the following categories:
  1. @ref GeneralProperties "General Properties" shared across multiple devices.
  2. @ref Connection::Interface "Connection Properties"
    • @ref SerialProperties "Serial Properties" used to communicate with and manage serial devices (including Bluetooth).
    • @ref TCPProperties "TCP Properties" used to communicate with and manage devices over the network.
@author Jasem Mutlaq */ namespace SP { /** * \defgroup GeneralProperties Standard Properties - General: Common properties shared across devices of multiple genres. * The following tables describe standard properties pertaining to generic devices. The name of a standard property and its members must be * strictly reserved in all drivers. However, it is permissible to change the label element of properties. You can find numerous uses of the * standard properties in the INDI library driver repository. * * As a general rule of the thumb, the status of properties reflects the command execution result: * IPS_OKAY: Command excuted successfully. * IPS_BUSY: Command execution under progress. * IPS_ALERT: Command execution failed. */ /*@{*/ /** * @brief Connect to and disconnect from device. * Name | Type | Member | Default | Description * ---- | ---- | ------ | ------- | ----------- * CONNECTION | SWITCH | CONNECT | OFF | Establish connection to device * CONNECTION | SWITCH | DISCONNECT | ON | Disconnect device */ extern const char *CONNECTION; /*@}*/ /** * \defgroup SerialProperties Standard Properties - Serial: Properties used to communicate with and manage serial devices. * Serial communication over RS232/485 and Bluetooth. Unless otherwise noted, all the properties are saved in the configuration file so that they are remembered across sessions. */ /*@{*/ /** * @brief Device serial (or bluetooth) connection port. The default value on Linux is /dev/ttyUSB0 while on MacOS it is /dev/cu.usbserial * It is part of Connection::SerialInterface to manage connections to serial devices. * Name | Type | Member | Default | Description * ---- | ---- | ------ | ------- | ----------- * DEVICE_PORT | TEXT | PORT | /dev/ttyUSB0 | Device serial connection port */ extern const char *DEVICE_PORT; /** * @brief Toggle device auto search. * If enabled and on connection failure with the default port, the SerialInterface class shall scan the system for available * serial ports and attempts connection and handshake with each until successful. Please note if this option is enabled it can take * a while before connection is established depending on how many ports are available on the system and the handshake timeout of the * the underlying device. * Name | Type | Member | Default | Description * ---- | ---- | ------ | ------- | ----------- * DEVICE_AUTO_SEARCH | SWITCH | ENABLED | ON | Auto Search ON * DEVICE_AUTO_SEARCH | SWITCH | DISABLED | OFF | Auto Search OFF */ extern const char *DEVICE_AUTO_SEARCH; /** * @brief Set device baud rate * Name | Type | Member | Default | Description * ---- | ---- | ------ | ------- | ----------- * DEVICE_BAUD_RATE | SWITCH | 9600 | ON | 9600 * DEVICE_BAUD_RATE | SWITCH | 19200 | OFF | 19200 * DEVICE_BAUD_RATE | SWITCH | 38400 | OFF | 38400 * DEVICE_BAUD_RATE | SWITCH | 57600 | OFF | 57600 * DEVICE_BAUD_RATE | SWITCH | 115200 | OFF | 115200 * DEVICE_BAUD_RATE | SWITCH | 230400 | OFF | 230400 */ extern const char *DEVICE_BAUD_RATE; /*@}*/ /** * \defgroup TCPProperties Standard Properties - TCP: Properties used to communicate with and manage devices over the network. * Communication with devices over TCP/IP. Unless otherwise noted, all the properties are saved in the configuration file so that they are remembered across sessions. */ /*@{*/ /** * @brief Device hostname and port. * It is part of Connection::TCPInterface to manage connections to devices over the network. * Name | Type | Member | Default | Description * ---- | ---- | ------ | ------- | ----------- * DEVICE_TCP_ADDRESS | TEXT | ADDRESS | | Device hostname or IP Address * DEVICE_TCP_ADDRESS | TEXT | PORT | | Device port */ extern const char *DEVICE_TCP_ADDRESS; /*@}*/ } } // namespace INDI libindi/libs/indicom.c0000664000175000017500000011205513263645557014215 0ustar jasemjasem/* INDI LIB Common routines used by all drivers Copyright (C) 2003 by Jason Harris (jharris@30doradus.org) Elwood C. Downey This is the C version of the astronomical library in KStars modified by 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 */ #define _GNU_SOURCE 1 #include "indicom.h" #include "indidevapi.h" #include "locale_compat.h" #include "config.h" #if defined(HAVE_LIBNOVA) #include #include #endif // HAVE_LIBNOVA #include #include #include #include #include #include #include #include #include #ifdef __APPLE__ #include #endif #if defined(BSD) && !defined(__GNU__) #include #include #endif #ifdef _WIN32 #undef CX #undef CY #endif #ifndef _WIN32 #include #include #include #define PARITY_NONE 0 #define PARITY_EVEN 1 #define PARITY_ODD 2 #endif #if defined(_MSC_VER) #define snprintf _snprintf #pragma warning(push) ///@todo Introduce plattform indipendent safe functions as macros to fix this #pragma warning(disable : 4996) #endif #define MAXRBUF 2048 int tty_debug = 0; int ttyGeminiUdpFormat = 0; int sequenceNumber = 1; #if defined(HAVE_LIBNOVA) int extractISOTime(const char *timestr, struct ln_date *iso_date) { struct tm utm; if (strptime(timestr, "%Y/%m/%dT%H:%M:%S", &utm)) { ln_get_date_from_tm(&utm, iso_date); return (0); } if (strptime(timestr, "%Y-%m-%dT%H:%M:%S", &utm)) { ln_get_date_from_tm(&utm, iso_date); return (0); } return (-1); } #endif /* sprint the variable a in sexagesimal format into out[]. * w is the number of spaces for the whole part. * fracbase is the number of pieces a whole is to broken into; valid options: * 360000: :mm:ss.ss * 36000: :mm:ss.s * 3600: :mm:ss * 600: :mm.m * 60: :mm * return number of characters written to out, not counting final '\0'. */ int fs_sexa(char *out, double a, int w, int fracbase) { char *out0 = out; unsigned long n; int d; int f; int m; int s; int isneg; /* save whether it's negative but do all the rest with a positive */ isneg = (a < 0); if (isneg) a = -a; /* convert to an integral number of whole portions */ n = (unsigned long)(a * fracbase + 0.5); d = n / fracbase; f = n % fracbase; /* form the whole part; "negative 0" is a special case */ if (isneg && d == 0) out += snprintf(out, MAXINDIFORMAT, "%*s-0", w - 2, ""); else out += snprintf(out, MAXINDIFORMAT, "%*d", w, isneg ? -d : d); /* do the rest */ switch (fracbase) { case 60: /* dd:mm */ m = f / (fracbase / 60); out += snprintf(out, MAXINDIFORMAT, ":%02d", m); break; case 600: /* dd:mm.m */ out += snprintf(out, MAXINDIFORMAT, ":%02d.%1d", f / 10, f % 10); break; case 3600: /* dd:mm:ss */ m = f / (fracbase / 60); s = f % (fracbase / 60); out += snprintf(out, MAXINDIFORMAT, ":%02d:%02d", m, s); break; case 36000: /* dd:mm:ss.s*/ m = f / (fracbase / 60); s = f % (fracbase / 60); out += snprintf(out, MAXINDIFORMAT, ":%02d:%02d.%1d", m, s / 10, s % 10); break; case 360000: /* dd:mm:ss.ss */ m = f / (fracbase / 60); s = f % (fracbase / 60); out += snprintf(out, MAXINDIFORMAT, ":%02d:%02d.%02d", m, s / 100, s % 100); break; default: printf("fs_sexa: unknown fracbase: %d\n", fracbase); return -1; } return (out - out0); } /* convert sexagesimal string str AxBxC to double. * x can be anything non-numeric. Any missing A, B or C will be assumed 0. * optional - and + can be anywhere. * return 0 if ok, -1 if can't find a thing. */ int f_scansexa(const char *str0, /* input string */ double *dp) /* cracked value, if return 0 */ { locale_char_t *orig = indi_locale_C_numeric_push(); double a = 0, b = 0, c = 0; char str[128]; //char *neg; uint8_t isNegative=0; int r= 0; /* copy str0 so we can play with it */ strncpy(str, str0, sizeof(str) - 1); str[sizeof(str) - 1] = '\0'; /* remove any spaces */ char* i = str; char* j = str; while(*j != 0) { *i = *j++; if(*i != ' ') i++; } *i = 0; // This has problem process numbers in scientific notations e.g. 1e-06 /*neg = strchr(str, '-'); if (neg) *neg = ' '; */ if (str[0] == '-') { isNegative = 1; str[0] = ' '; } r = sscanf(str, "%lf%*[^0-9]%lf%*[^0-9]%lf", &a, &b, &c); indi_locale_C_numeric_pop(orig); if (r < 1) return (-1); *dp = a + b / 60 + c / 3600; if (isNegative) *dp *= -1; return (0); } void getSexComponents(double value, int *d, int *m, int *s) { *d = (int32_t)fabs(value); *m = (int32_t)((fabs(value) - *d) * 60.0); *s = (int32_t)rint(((fabs(value) - *d) * 60.0 - *m) * 60.0); if (value < 0) *d *= -1; } void getSexComponentsIID(double value, int *d, int *m, double *s) { *d = (int32_t)fabs(value); *m = (int32_t)((fabs(value) - *d) * 60.0); *s = (double)(((fabs(value) - *d) * 60.0 - *m) * 60.0); if (value < 0) *d *= -1; } /* fill buf with properly formatted INumber string. return length */ int numberFormat(char *buf, const char *format, double value) { int w, f, s; char m; if (sscanf(format, "%%%d.%d%c", &w, &f, &m) == 3 && m == 'm') { /* INDI sexi format */ switch (f) { case 9: s = 360000; break; case 8: s = 36000; break; case 6: s = 3600; break; case 5: s = 600; break; default: s = 60; break; } return (fs_sexa(buf, value, w - f, s)); } else { /* normal printf format */ return (snprintf(buf, MAXINDIFORMAT, format, value)); } } /* log message locally. * this has nothing to do with XML or any Clients. */ void IDLog(const char *fmt, ...) { va_list ap; /* JM: Since all INDI's stderr are timestampped now, we don't need to time stamp ID Log */ /*fprintf (stderr, "%s ", timestamp());*/ va_start(ap, fmt); vfprintf(stderr, fmt, ap); va_end(ap); } /* return current system time in message format */ const char *timestamp() { static 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); return (ts); } void tty_set_debug(int debug) { tty_debug = debug; } void tty_set_gemini_udp_format(int enabled) { ttyGeminiUdpFormat = enabled; } int tty_timeout(int fd, int timeout) { #if defined(_WIN32) || defined(ANDROID) INDI_UNUSED(fd); INDI_UNUSED(timeout); return TTY_ERRNO; #else if (fd == -1) return TTY_ERRNO; struct timeval tv; fd_set readout; int retval; FD_ZERO(&readout); FD_SET(fd, &readout); /* wait for 'timeout' seconds */ tv.tv_sec = timeout; tv.tv_usec = 0; /* Wait till we have a change in the fd status */ retval = select(fd + 1, &readout, NULL, NULL, &tv); /* Return 0 on successful fd change */ if (retval > 0) return TTY_OK; /* Return -1 due to an error */ else if (retval == -1) return TTY_SELECT_ERROR; /* Return -2 if time expires before anything interesting happens */ else return TTY_TIME_OUT; #endif } int tty_write(int fd, const char *buf, int nbytes, int *nbytes_written) { #ifdef _WIN32 return TTY_ERRNO; #else int geminiBuffer[66]={0}; char *buffer = (char *)buf; if (ttyGeminiUdpFormat) { buffer = (char*)geminiBuffer; geminiBuffer[0] = ++sequenceNumber; geminiBuffer[1] = 0; memcpy((char *)&geminiBuffer[2], buf, nbytes); // Add on the 8 bytes for the header and 1 byte for the null terminator nbytes += 9; } if (fd == -1) return TTY_ERRNO; int bytes_w = 0; *nbytes_written = 0; if (tty_debug) { int i = 0; for (i = 0; i < nbytes; i++) IDLog("%s: buffer[%d]=%#X (%c)\n", __FUNCTION__, i, (unsigned char)buf[i], buf[i]); } while (nbytes > 0) { bytes_w = write(fd, buffer + (*nbytes_written), nbytes); if (bytes_w < 0) return TTY_WRITE_ERROR; *nbytes_written += bytes_w; nbytes -= bytes_w; } if (ttyGeminiUdpFormat) *nbytes_written -= 9; return TTY_OK; #endif } int tty_write_string(int fd, const char *buf, int *nbytes_written) { unsigned int nbytes; nbytes = strlen(buf); return tty_write(fd, buf, nbytes, nbytes_written); } int tty_read(int fd, char *buf, int nbytes, int timeout, int *nbytes_read) { #ifdef _WIN32 return TTY_ERRNO; #else if (fd == -1) return TTY_ERRNO; int numBytesToRead = nbytes; int bytesRead = 0; int err = 0; *nbytes_read = 0; if (nbytes <= 0) return TTY_PARAM_ERROR; if (tty_debug) IDLog("%s: Request to read %d bytes with %d timeout for fd %d\n", __FUNCTION__, nbytes, timeout, fd); char geminiBuffer[257]={0}; char* buffer = buf; if (ttyGeminiUdpFormat) { numBytesToRead = nbytes + 8; buffer = geminiBuffer; } while (numBytesToRead > 0) { if ((err = tty_timeout(fd, timeout))) return err; bytesRead = read(fd, buffer + (*nbytes_read), ((uint32_t)numBytesToRead)); if (bytesRead < 0) return TTY_READ_ERROR; if (tty_debug) { IDLog("%d bytes read and %d bytes remaining...\n", bytesRead, numBytesToRead - bytesRead); int i = 0; for (i = *nbytes_read; i < (*nbytes_read + bytesRead); i++) IDLog("%s: buffer[%d]=%#X (%c)\n", __FUNCTION__, i, (unsigned char)buf[i], buf[i]); } *nbytes_read += bytesRead; numBytesToRead -= bytesRead; } if (ttyGeminiUdpFormat) { int *intSizedBuffer = (int *)geminiBuffer; if (intSizedBuffer[0] != sequenceNumber) { // Not the right reply just do the read again. return tty_read(fd, buf, nbytes, timeout, nbytes_read); } *nbytes_read -= 8; memcpy(buf, geminiBuffer+8, *nbytes_read); } return TTY_OK; #endif } int tty_read_section(int fd, char *buf, char stop_char, int timeout, int *nbytes_read) { #ifdef _WIN32 return TTY_ERRNO; #else char readBuffer[257]={0}; if (fd == -1) return TTY_ERRNO; int bytesRead = 0; int err = TTY_OK; *nbytes_read = 0; uint8_t *read_char = 0; if (tty_debug) IDLog("%s: Request to read until stop char '%#02X' with %d timeout for fd %d\n", __FUNCTION__, stop_char, timeout, fd); if (ttyGeminiUdpFormat) { bytesRead = read(fd, readBuffer, 255); if (bytesRead < 0) return TTY_READ_ERROR; int *intSizedBuffer = (int *)readBuffer; if (intSizedBuffer[0] != sequenceNumber) { // Not the right reply just do the read again. return tty_read_section(fd, buf, stop_char, timeout, nbytes_read); } for (int index = 8; index < bytesRead; index++) { (*nbytes_read)++; if (*(readBuffer+index) == stop_char) { strncpy(buf, readBuffer+8, *nbytes_read); return TTY_OK; } } } else { for (;;) { if ((err = tty_timeout(fd, timeout))) return err; read_char = (uint8_t*)(buf + *nbytes_read); bytesRead = read(fd, read_char, 1); if (bytesRead < 0) return TTY_READ_ERROR; if (tty_debug) IDLog("%s: buffer[%d]=%#X (%c)\n", __FUNCTION__, (*nbytes_read), *read_char, *read_char); (*nbytes_read)++; if (*read_char == stop_char) return TTY_OK; } } return TTY_TIME_OUT; #endif } int tty_nread_section(int fd, char *buf, int nsize, char stop_char, int timeout, int *nbytes_read) { #ifdef _WIN32 return TTY_ERRNO; #else if (fd == -1) return TTY_ERRNO; int bytesRead = 0; int err = TTY_OK; *nbytes_read = 0; uint8_t *read_char = 0; if (tty_debug) IDLog("%s: Request to read until stop char '%#02X' with %d timeout for fd %d\n", __FUNCTION__, stop_char, timeout, fd); for (;;) { if ((err = tty_timeout(fd, timeout))) return err; read_char = (uint8_t*)(buf + *nbytes_read); bytesRead = read(fd, read_char, 1); if (bytesRead < 0) return TTY_READ_ERROR; if (tty_debug) IDLog("%s: buffer[%d]=%#X (%c)\n", __FUNCTION__, (*nbytes_read), *read_char, *read_char); (*nbytes_read)++; if (*read_char == stop_char) return TTY_OK; else if (*nbytes_read >= nsize) return TTY_OVERFLOW; } return TTY_TIME_OUT; #endif } #if defined(BSD) && !defined(__GNU__) // BSD - OSX version int tty_connect(const char *device, int bit_rate, int word_size, int parity, int stop_bits, int *fd) { int t_fd = -1; int bps; char msg[80]; int handshake; struct termios tty_setting; // Open the serial port read/write, with no controlling terminal, and don't wait for a connection. // The O_NONBLOCK flag also causes subsequent I/O on the device to be non-blocking. // See open(2) ("man 2 open") for details. t_fd = open(device, O_RDWR | O_NOCTTY | O_NONBLOCK); if (t_fd == -1) { IDLog("Error opening serial port (%s) - %s(%d).\n", device, strerror(errno), errno); goto error; } // Note that open() follows POSIX semantics: multiple open() calls to the same file will succeed // unless the TIOCEXCL ioctl is issued. This will prevent additional opens except by root-owned // processes. // See tty(4) ("man 4 tty") and ioctl(2) ("man 2 ioctl") for details. if (ioctl(t_fd, TIOCEXCL) == -1) { IDLog("Error setting TIOCEXCL on %s - %s(%d).\n", device, strerror(errno), errno); goto error; } // Now that the device is open, clear the O_NONBLOCK flag so subsequent I/O will block. // See fcntl(2) ("man 2 fcntl") for details. if (fcntl(t_fd, F_SETFL, 0) == -1) { IDLog("Error clearing O_NONBLOCK %s - %s(%d).\n", device, strerror(errno), errno); goto error; } // Get the current options and save them so we can restore the default settings later. if (tcgetattr(t_fd, &tty_setting) == -1) { IDLog("Error getting tty attributes %s - %s(%d).\n", device, strerror(errno), errno); goto error; } // Set raw input (non-canonical) mode, with reads blocking until either a single character // has been received or a one second timeout expires. // See tcsetattr(4) ("man 4 tcsetattr") and termios(4) ("man 4 termios") for details. cfmakeraw(&tty_setting); tty_setting.c_cc[VMIN] = 1; tty_setting.c_cc[VTIME] = 10; // The baud rate, word length, and handshake options can be set as follows: switch (bit_rate) { case 0: bps = B0; break; case 50: bps = B50; break; case 75: bps = B75; break; case 110: bps = B110; break; case 134: bps = B134; break; case 150: bps = B150; break; case 200: bps = B200; break; case 300: bps = B300; break; case 600: bps = B600; break; case 1200: bps = B1200; break; case 1800: bps = B1800; break; case 2400: bps = B2400; break; case 4800: bps = B4800; break; case 9600: bps = B9600; break; case 19200: bps = B19200; break; case 38400: bps = B38400; break; case 57600: bps = B57600; break; case 115200: bps = B115200; break; case 230400: bps = B230400; break; default: if (snprintf(msg, sizeof(msg), "tty_connect: %d is not a valid bit rate.", bit_rate) < 0) perror(NULL); else perror(msg); return TTY_PARAM_ERROR; } cfsetspeed(&tty_setting, bps); // Set baud rate /* word size */ switch (word_size) { case 5: tty_setting.c_cflag |= CS5; break; case 6: tty_setting.c_cflag |= CS6; break; case 7: tty_setting.c_cflag |= CS7; break; case 8: tty_setting.c_cflag |= CS8; break; default: if (snprintf(msg, sizeof(msg), "tty_connect: %d is not a valid data bit count.", word_size) < 0) perror(NULL); else perror(msg); return TTY_PARAM_ERROR; } /* parity */ switch (parity) { case PARITY_NONE: break; case PARITY_EVEN: tty_setting.c_cflag |= PARENB; break; case PARITY_ODD: tty_setting.c_cflag |= PARENB | PARODD; break; default: if (snprintf(msg, sizeof(msg), "tty_connect: %d is not a valid parity selection value.", parity) < 0) perror(NULL); else perror(msg); return TTY_PARAM_ERROR; } /* stop_bits */ switch (stop_bits) { case 1: break; case 2: tty_setting.c_cflag |= CSTOPB; break; default: if (snprintf(msg, sizeof(msg), "tty_connect: %d is not a valid stop bit count.", stop_bits) < 0) perror(NULL); else perror(msg); return TTY_PARAM_ERROR; } #if defined(MAC_OS_X_VERSION_10_4) && (MAC_OS_X_VERSION_MIN_REQUIRED >= MAC_OS_X_VERSION_10_4) // Starting with Tiger, the IOSSIOSPEED ioctl can be used to set arbitrary baud rates // other than those specified by POSIX. The driver for the underlying serial hardware // ultimately determines which baud rates can be used. This ioctl sets both the input // and output speed. speed_t speed = 14400; // Set 14400 baud if (ioctl(t_fd, IOSSIOSPEED, &speed) == -1) { IDLog("Error calling ioctl(..., IOSSIOSPEED, ...) - %s(%d).\n", strerror(errno), errno); } #endif // Cause the new options to take effect immediately. if (tcsetattr(t_fd, TCSANOW, &tty_setting) == -1) { IDLog("Error setting tty attributes %s - %s(%d).\n", device, strerror(errno), errno); goto error; } // To set the modem handshake lines, use the following ioctls. // See tty(4) ("man 4 tty") and ioctl(2) ("man 2 ioctl") for details. if (ioctl(t_fd, TIOCSDTR) == -1) // Assert Data Terminal Ready (DTR) { IDLog("Error asserting DTR %s - %s(%d).\n", device, strerror(errno), errno); } if (ioctl(t_fd, TIOCCDTR) == -1) // Clear Data Terminal Ready (DTR) { IDLog("Error clearing DTR %s - %s(%d).\n", device, strerror(errno), errno); } handshake = TIOCM_DTR | TIOCM_RTS | TIOCM_CTS | TIOCM_DSR; if (ioctl(t_fd, TIOCMSET, &handshake) == -1) // Set the modem lines depending on the bits set in handshake { IDLog("Error setting handshake lines %s - %s(%d).\n", device, strerror(errno), errno); } // To read the state of the modem lines, use the following ioctl. // See tty(4) ("man 4 tty") and ioctl(2) ("man 2 ioctl") for details. if (ioctl(t_fd, TIOCMGET, &handshake) == -1) // Store the state of the modem lines in handshake { IDLog("Error getting handshake lines %s - %s(%d).\n", device, strerror(errno), errno); } IDLog("Handshake lines currently set to %d\n", handshake); #if defined(MAC_OS_X_VERSION_10_3) && (MAC_OS_X_VERSION_MIN_REQUIRED >= MAC_OS_X_VERSION_10_3) unsigned long mics = 1UL; // Set the receive latency in microseconds. Serial drivers use this value to determine how often to // dequeue characters received by the hardware. Most applications don't need to set this value: if an // app reads lines of characters, the app can't do anything until the line termination character has been // received anyway. The most common applications which are sensitive to read latency are MIDI and IrDA // applications. if (ioctl(t_fd, IOSSDATALAT, &mics) == -1) { // set latency to 1 microsecond IDLog("Error setting read latency %s - %s(%d).\n", device, strerror(errno), errno); goto error; } #endif *fd = t_fd; /* return success */ return TTY_OK; // Failure path error: if (t_fd != -1) { close(t_fd); *fd = -1; } return TTY_PORT_FAILURE; } #else int tty_connect(const char *device, int bit_rate, int word_size, int parity, int stop_bits, int *fd) { #ifdef _WIN32 return TTY_PORT_FAILURE; #else int t_fd = -1; char msg[80]; int bps; struct termios tty_setting; if ((t_fd = open(device, O_RDWR | O_NOCTTY)) == -1) { *fd = -1; return TTY_PORT_FAILURE; } // Get the current options and save them so we can restore the default settings later. if (tcgetattr(t_fd, &tty_setting) == -1) { perror("tty_connect: failed getting tty attributes."); return TTY_PORT_FAILURE; } /* Control Modes Set bps rate */ switch (bit_rate) { case 0: bps = B0; break; case 50: bps = B50; break; case 75: bps = B75; break; case 110: bps = B110; break; case 134: bps = B134; break; case 150: bps = B150; break; case 200: bps = B200; break; case 300: bps = B300; break; case 600: bps = B600; break; case 1200: bps = B1200; break; case 1800: bps = B1800; break; case 2400: bps = B2400; break; case 4800: bps = B4800; break; case 9600: bps = B9600; break; case 19200: bps = B19200; break; case 38400: bps = B38400; break; case 57600: bps = B57600; break; case 115200: bps = B115200; break; case 230400: bps = B230400; break; default: if (snprintf(msg, sizeof(msg), "tty_connect: %d is not a valid bit rate.", bit_rate) < 0) perror(NULL); else perror(msg); return TTY_PARAM_ERROR; } if ((cfsetispeed(&tty_setting, bps) < 0) || (cfsetospeed(&tty_setting, bps) < 0)) { perror("tty_connect: failed setting bit rate."); return TTY_PORT_FAILURE; } /* Control Modes set no flow control word size, parity and stop bits. Also don't hangup automatically and ignore modem status. Finally enable receiving characters. */ tty_setting.c_cflag &= ~(CSIZE | CSTOPB | PARENB | PARODD | HUPCL | CRTSCTS); tty_setting.c_cflag |= (CLOCAL | CREAD); /* word size */ switch (word_size) { case 5: tty_setting.c_cflag |= CS5; break; case 6: tty_setting.c_cflag |= CS6; break; case 7: tty_setting.c_cflag |= CS7; break; case 8: tty_setting.c_cflag |= CS8; break; default: fprintf(stderr, "Default\n"); if (snprintf(msg, sizeof(msg), "tty_connect: %d is not a valid data bit count.", word_size) < 0) perror(NULL); else perror(msg); return TTY_PARAM_ERROR; } /* parity */ switch (parity) { case PARITY_NONE: break; case PARITY_EVEN: tty_setting.c_cflag |= PARENB; break; case PARITY_ODD: tty_setting.c_cflag |= PARENB | PARODD; break; default: fprintf(stderr, "Default1\n"); if (snprintf(msg, sizeof(msg), "tty_connect: %d is not a valid parity selection value.", parity) < 0) perror(NULL); else perror(msg); return TTY_PARAM_ERROR; } /* stop_bits */ switch (stop_bits) { case 1: break; case 2: tty_setting.c_cflag |= CSTOPB; break; default: fprintf(stderr, "Default2\n"); if (snprintf(msg, sizeof(msg), "tty_connect: %d is not a valid stop bit count.", stop_bits) < 0) perror(NULL); else perror(msg); return TTY_PARAM_ERROR; } /* Control Modes complete */ /* Ignore bytes with parity errors and make terminal raw and dumb.*/ tty_setting.c_iflag &= ~(PARMRK | ISTRIP | IGNCR | ICRNL | INLCR | IXOFF | IXON | IXANY); tty_setting.c_iflag |= INPCK | IGNPAR | IGNBRK; /* Raw output.*/ tty_setting.c_oflag &= ~(OPOST | ONLCR); /* Local Modes Don't echo characters. Don't generate signals. Don't process any characters.*/ tty_setting.c_lflag &= ~(ICANON | ECHO | ECHOE | ISIG | IEXTEN | NOFLSH | TOSTOP); tty_setting.c_lflag |= NOFLSH; /* blocking read until 1 char arrives */ tty_setting.c_cc[VMIN] = 1; tty_setting.c_cc[VTIME] = 0; /* now clear input and output buffers and activate the new terminal settings */ tcflush(t_fd, TCIOFLUSH); if (tcsetattr(t_fd, TCSANOW, &tty_setting)) { perror("tty_connect: failed setting attributes on serial port."); tty_disconnect(t_fd); return TTY_PORT_FAILURE; } *fd = t_fd; /* return success */ return TTY_OK; #endif } // Unix - Linux version #endif int tty_disconnect(int fd) { if (fd == -1) return TTY_ERRNO; #ifdef _WIN32 return TTY_ERRNO; #else int err; tcflush(fd, TCIOFLUSH); err = close(fd); if (err != 0) return TTY_ERRNO; return TTY_OK; #endif } void tty_error_msg(int err_code, char *err_msg, int err_msg_len) { char error_string[512]; switch (err_code) { case TTY_OK: strncpy(err_msg, "No Error", err_msg_len); break; case TTY_READ_ERROR: snprintf(error_string, 512, "Read Error: %s", strerror(errno)); strncpy(err_msg, error_string, err_msg_len); break; case TTY_WRITE_ERROR: snprintf(error_string, 512, "Write Error: %s", strerror(errno)); strncpy(err_msg, error_string, err_msg_len); break; case TTY_SELECT_ERROR: snprintf(error_string, 512, "Select Error: %s", strerror(errno)); strncpy(err_msg, error_string, err_msg_len); break; case TTY_TIME_OUT: strncpy(err_msg, "Timeout error", err_msg_len); break; case TTY_PORT_FAILURE: if (errno == EACCES) snprintf(error_string, 512, "Port failure Error: %s. Try adding your user to the dialout group and restart (sudo adduser " "$USER dialout)", strerror(errno)); else snprintf(error_string, 512, "Port failure Error: %s. Check if device is connected to this port.", strerror(errno)); strncpy(err_msg, error_string, err_msg_len); break; case TTY_PARAM_ERROR: strncpy(err_msg, "Parameter error", err_msg_len); break; case TTY_ERRNO: snprintf(error_string, 512, "%s", strerror(errno)); strncpy(err_msg, error_string, err_msg_len); break; case TTY_OVERFLOW: strncpy(err_msg, "Read overflow", err_msg_len); break; default: strncpy(err_msg, "Error: unrecognized error code", err_msg_len); break; } } /* return static string corresponding to the given property or light state */ const char *pstateStr(IPState s) { switch (s) { case IPS_IDLE: return ("Idle"); case IPS_OK: return ("Ok"); case IPS_BUSY: return ("Busy"); case IPS_ALERT: return ("Alert"); default: fprintf(stderr, "Impossible IPState %d\n", s); return NULL; } } /* crack string into IPState. * return 0 if ok, else -1 */ int crackIPState(const char *str, IPState *ip) { if (!strcmp(str, "Idle")) *ip = IPS_IDLE; else if (!strncmp(str, "Ok", 2)) *ip = IPS_OK; else if (!strcmp(str, "Busy")) *ip = IPS_BUSY; else if (!strcmp(str, "Alert")) *ip = IPS_ALERT; else return (-1); return (0); } /* crack string into ISState. * return 0 if ok, else -1 */ int crackISState(const char *str, ISState *ip) { if (!strncmp(str, "On", 2)) *ip = ISS_ON; else if (!strcmp(str, "Off")) *ip = ISS_OFF; else return (-1); return (0); } int crackIPerm(const char *str, IPerm *ip) { if (!strncmp(str, "rw", 2)) *ip = IP_RW; else if (!strncmp(str, "ro", 2)) *ip = IP_RO; else if (!strncmp(str, "wo", 2)) *ip = IP_WO; else return (-1); return (0); } int crackISRule(const char *str, ISRule *ip) { if (!strcmp(str, "OneOfMany")) *ip = ISR_1OFMANY; else if (!strcmp(str, "AtMostOne")) *ip = ISR_ATMOST1; else if (!strcmp(str, "AnyOfMany")) *ip = ISR_NOFMANY; else return (-1); return (0); } /* return static string corresponding to the given switch state */ const char *sstateStr(ISState s) { switch (s) { case ISS_ON: return ("On"); case ISS_OFF: return ("Off"); default: fprintf(stderr, "Impossible ISState %d\n", s); return NULL; } } /* return static string corresponding to the given Rule */ const char *ruleStr(ISRule r) { switch (r) { case ISR_1OFMANY: return ("OneOfMany"); case ISR_ATMOST1: return ("AtMostOne"); case ISR_NOFMANY: return ("AnyOfMany"); default: fprintf(stderr, "Impossible ISRule %d\n", r); return NULL; } } /* return static string corresponding to the given IPerm */ const char *permStr(IPerm p) { switch (p) { case IP_RO: return ("ro"); case IP_WO: return ("wo"); case IP_RW: return ("rw"); default: fprintf(stderr, "Impossible IPerm %d\n", p); return NULL; } } /* print the boilerplate comment introducing xml */ void xmlv1() { printf("\n"); } /* pull out device and name attributes from root. * return 0 if ok else -1 with reason in msg[]. */ int crackDN(XMLEle *root, char **dev, char **name, char msg[]) { XMLAtt *ap; ap = findXMLAtt(root, "device"); if (!ap) { sprintf(msg, "%s requires 'device' attribute", tagXMLEle(root)); return (-1); } *dev = valuXMLAtt(ap); ap = findXMLAtt(root, "name"); if (!ap) { sprintf(msg, "%s requires 'name' attribute", tagXMLEle(root)); return (-1); } *name = valuXMLAtt(ap); return (0); } /* find a member of an IText vector, else NULL */ IText *IUFindText(const ITextVectorProperty *tvp, const char *name) { int i; for (i = 0; i < tvp->ntp; i++) if (strcmp(tvp->tp[i].name, name) == 0) return (&tvp->tp[i]); fprintf(stderr, "No IText '%s' in %s.%s\n", name, tvp->device, tvp->name); return (NULL); } /* find a member of an INumber vector, else NULL */ INumber *IUFindNumber(const INumberVectorProperty *nvp, const char *name) { int i; for (i = 0; i < nvp->nnp; i++) if (strcmp(nvp->np[i].name, name) == 0) return (&nvp->np[i]); fprintf(stderr, "No INumber '%s' in %s.%s\n", name, nvp->device, nvp->name); return (NULL); } /* find a member of an ISwitch vector, else NULL */ ISwitch *IUFindSwitch(const ISwitchVectorProperty *svp, const char *name) { int i; for (i = 0; i < svp->nsp; i++) if (strcmp(svp->sp[i].name, name) == 0) return (&svp->sp[i]); fprintf(stderr, "No ISwitch '%s' in %s.%s\n", name, svp->device, svp->name); return (NULL); } /* find a member of an ILight vector, else NULL */ ILight *IUFindLight(const ILightVectorProperty *lvp, const char *name) { int i; for (i = 0; i < lvp->nlp; i++) if (strcmp(lvp->lp[i].name, name) == 0) return (&lvp->lp[i]); fprintf(stderr, "No ILight '%s' in %s.%s\n", name, lvp->device, lvp->name); return (NULL); } /* find a member of an IBLOB vector, else NULL */ IBLOB *IUFindBLOB(const IBLOBVectorProperty *bvp, const char *name) { int i; for (i = 0; i < bvp->nbp; i++) if (strcmp(bvp->bp[i].name, name) == 0) return (&bvp->bp[i]); fprintf(stderr, "No IBLOB '%s' in %s.%s\n", name, bvp->device, bvp->name); return (NULL); } /* find an ON member of an ISwitch vector, else NULL. * N.B. user must make sense of result with ISRule in mind. */ ISwitch *IUFindOnSwitch(const ISwitchVectorProperty *svp) { int i; for (i = 0; i < svp->nsp; i++) if (svp->sp[i].s == ISS_ON) return (&svp->sp[i]); /*fprintf(stderr, "No ISwitch On in %s.%s\n", svp->device, svp->name);*/ return (NULL); } /* Find index of the ON member of an ISwitchVectorProperty */ int IUFindOnSwitchIndex(const ISwitchVectorProperty *svp) { int i; for (i = 0; i < svp->nsp; i++) if (svp->sp[i].s == ISS_ON) return i; return -1; } /* Find name the ON member in the given states and names */ const char *IUFindOnSwitchName(ISState *states, char *names[], int n) { int i; for (i = 0; i < n; i++) if (states[i] == ISS_ON) return names[i]; return NULL; } /* Set all switches to off */ void IUResetSwitch(ISwitchVectorProperty *svp) { int i; for (i = 0; i < svp->nsp; i++) svp->sp[i].s = ISS_OFF; } /* save malloced copy of newtext in tp->text, reusing if not first time */ void IUSaveText(IText *tp, const char *newtext) { /* seed for realloc */ if (tp->text == NULL) tp->text = malloc(1); /* copy in fresh string */ tp->text = strcpy(realloc(tp->text, strlen(newtext) + 1), newtext); } double rangeHA(double r) { double res = r; while (res < -12.0) res += 24.0; while (res >= 12.0) res -= 24.0; return res; } double range24(double r) { double res = r; while (res < 0.0) res += 24.0; while (res > 24.0) res -= 24.0; return res; } double range360(double r) { double res = r; while (res < 0.0) res += 360.0; while (res > 360.0) res -= 360.0; return res; } double rangeDec(double decdegrees) { if ((decdegrees >= 270.0) && (decdegrees <= 360.0)) return (decdegrees - 360.0); if ((decdegrees >= 180.0) && (decdegrees < 270.0)) return (180.0 - decdegrees); if ((decdegrees >= 90.0) && (decdegrees < 180.0)) return (180.0 - decdegrees); return decdegrees; } #if defined(HAVE_LIBNOVA) double get_local_sidereal_time(double longitude) { double SD = ln_get_apparent_sidereal_time(ln_get_julian_from_sys()) - (360.0 - longitude) / 15.0; return range24(SD); } #endif // HAVE_LIBNOVA double get_local_hour_angle(double sideral_time, double ra) { double HA = sideral_time - ra; return rangeHA(HA); } #if defined(_MSC_VER) #undef snprintf #pragma warning(pop) #endif libindi/libs/stream/0000775000175000017500000000000013263645557013716 5ustar jasemjasemlibindi/libs/stream/streammanager.cpp0000664000175000017500000010627213263645557017260 0ustar jasemjasem/* Copyright (C) 2015 by Jasem Mutlaq Copyright (C) 2014 by geehalel Stream Recorder 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 */ #include #include "streammanager.h" #include "indiccd.h" #include "indilogger.h" #include #include #include const char *STREAM_TAB = "Streaming"; namespace INDI { StreamManager::StreamManager(CCD *mainCCD) { currentCCD = mainCCD; m_isStreaming = false; m_isRecording = false; // Timer // now use BSD setimer to avoi librt dependency //sevp.sigev_notify=SIGEV_NONE; //timer_create(CLOCK_MONOTONIC, &sevp, &fpstimer); //fpssettings.it_interval.tv_sec=24*3600; //fpssettings.it_interval.tv_nsec=0; //fpssettings.it_value=fpssettings.it_interval; //timer_settime(fpstimer, 0, &fpssettings, nullptr); struct itimerval fpssettings; fpssettings.it_interval.tv_sec = 24 * 3600; fpssettings.it_interval.tv_usec = 0; fpssettings.it_value = fpssettings.it_interval; signal(SIGALRM, SIG_IGN); //portable setitimer(ITIMER_REAL, &fpssettings, nullptr); recorderManager = new RecorderManager(); recorder = recorderManager->getDefaultRecorder(); direct_record = false; LOGF_DEBUG("Using default recorder (%s)", recorder->getName()); encoderManager = new EncoderManager(); encoder = encoderManager->getDefaultEncoder(); encoder->init(mainCCD); LOGF_DEBUG("Using default encoder (%s)", encoder->getName()); } StreamManager::~StreamManager() { delete (recorderManager); delete (encoderManager); delete [] downscaleBuffer; } const char *StreamManager::getDeviceName() { return currentCCD->getDeviceName(); } bool StreamManager::initProperties() { /* Video Stream */ IUFillSwitch(&StreamS[0], "STREAM_ON", "Stream On", ISS_OFF); IUFillSwitch(&StreamS[1], "STREAM_OFF", "Stream Off", ISS_ON); IUFillSwitchVector(&StreamSP, StreamS, NARRAY(StreamS), getDeviceName(), "CCD_VIDEO_STREAM", "Video Stream", STREAM_TAB, IP_RW, ISR_1OFMANY, 0, IPS_IDLE); /* Stream Rate divisor */ IUFillNumber(&StreamOptionsN[OPTION_TARGET_FPS], "STREAM_FPS", "Target FPS", "%.f", 0, 30.0, 1, 10); IUFillNumber(&StreamOptionsN[OPTION_RATE_DIVISOR], "STREAM_RATE", "Rate Divisor", "%3.0f", 0, 60.0, 5, 0); IUFillNumberVector(&StreamOptionsNP, StreamOptionsN, NARRAY(StreamOptionsN), getDeviceName(), "STREAM_OPTIONS", "Settings", STREAM_TAB, IP_RW, 60, IPS_IDLE); /* Measured FPS */ IUFillNumber(&FpsN[FPS_INSTANT], "EST_FPS", "Instant.", "%3.2f", 0.0, 999.0, 0.0, 30); IUFillNumber(&FpsN[FPS_AVERAGE], "AVG_FPS", "Average (1 sec.)", "%3.2f", 0.0, 999.0, 0.0, 30); IUFillNumberVector(&FpsNP, FpsN, NARRAY(FpsN), getDeviceName(), "FPS", "FPS", STREAM_TAB, IP_RO, 60, IPS_IDLE); /* Frames to Drop */ //IUFillNumber(&FramestoDropN[0], "To drop", "", "%2.0f", 0, 99, 1, 0); //IUFillNumberVector(&FramestoDropNP, FramestoDropN, NARRAY(FramestoDropN), getDeviceName(), "Frames", "", STREAM_TAB, IP_RW, 60, IPS_IDLE); /* Record Frames */ /* File */ IUFillText(&RecordFileT[0], "RECORD_FILE_DIR", "Dir.", "/tmp/indi__D_"); IUFillText(&RecordFileT[1], "RECORD_FILE_NAME", "Name", "indi_record__T_"); IUFillTextVector(&RecordFileTP, RecordFileT, NARRAY(RecordFileT), getDeviceName(), "RECORD_FILE", "Record File", STREAM_TAB, IP_RW, 0, IPS_IDLE); /* Record Options */ IUFillNumber(&RecordOptionsN[0], "RECORD_DURATION", "Duration (sec)", "%6.3f", 0.001, 999999.0, 0.0, 1); IUFillNumber(&RecordOptionsN[1], "RECORD_FRAME_TOTAL", "Frames", "%9.0f", 1.0, 999999999.0, 1.0, 30.0); IUFillNumberVector(&RecordOptionsNP, RecordOptionsN, NARRAY(RecordOptionsN), getDeviceName(), "RECORD_OPTIONS", "Record Options", STREAM_TAB, IP_RW, 60, IPS_IDLE); /* Record Switch */ IUFillSwitch(&RecordStreamS[0], "RECORD_ON", "Record On", ISS_OFF); IUFillSwitch(&RecordStreamS[1], "RECORD_DURATION_ON", "Record (Duration)", ISS_OFF); IUFillSwitch(&RecordStreamS[2], "RECORD_FRAME_ON", "Record (Frames)", ISS_OFF); IUFillSwitch(&RecordStreamS[3], "RECORD_OFF", "Record Off", ISS_ON); IUFillSwitchVector(&RecordStreamSP, RecordStreamS, NARRAY(RecordStreamS), getDeviceName(), "RECORD_STREAM", "Video Record", STREAM_TAB, IP_RW, ISR_1OFMANY, 0, IPS_IDLE); // CCD Streaming Frame IUFillNumber(&StreamFrameN[0], "X", "Left ", "%4.0f", 0, 0.0, 0, 0); IUFillNumber(&StreamFrameN[1], "Y", "Top", "%4.0f", 0, 0, 0, 0); IUFillNumber(&StreamFrameN[2], "WIDTH", "Width", "%4.0f", 0, 0.0, 0, 0.0); IUFillNumber(&StreamFrameN[3], "HEIGHT", "Height", "%4.0f", 0, 0, 0, 0.0); IUFillNumberVector(&StreamFrameNP, StreamFrameN, 4, getDeviceName(), "CCD_STREAM_FRAME", "Frame", STREAM_TAB, IP_RW, 60, IPS_IDLE); // Encoder Selection IUFillSwitch(&EncoderS[ENCODER_RAW], "RAW", "RAW", ISS_ON); IUFillSwitch(&EncoderS[ENCODER_MJPEG], "MJPEG", "MJPEG", ISS_OFF); IUFillSwitchVector(&EncoderSP, EncoderS, NARRAY(EncoderS), getDeviceName(), "CCD_STREAM_ENCODER", "Encoder", STREAM_TAB, IP_RW, ISR_1OFMANY, 0, IPS_IDLE); // Recorder Selector IUFillSwitch(&RecorderS[RECORDER_RAW], "SER", "SER", ISS_ON); IUFillSwitch(&RecorderS[RECORDER_OGV], "OGV", "OGV", ISS_OFF); IUFillSwitchVector(&RecorderSP, RecorderS, NARRAY(RecorderS), getDeviceName(), "CCD_STREAM_RECORDER", "Recorder", STREAM_TAB, IP_RW, ISR_1OFMANY, 0, IPS_IDLE); // If we do not have theora installed, let's just define SER default recorder #ifndef HAVE_THEORA RecorderSP.nsp = 1; #endif return true; } void StreamManager::ISGetProperties(const char *dev) { if (dev != nullptr && strcmp(getDeviceName(), dev)) return; if (currentCCD->isConnected()) { currentCCD->defineSwitch(&StreamSP); currentCCD->defineNumber(&StreamOptionsNP); currentCCD->defineNumber(&FpsNP); currentCCD->defineSwitch(&RecordStreamSP); currentCCD->defineText(&RecordFileTP); currentCCD->defineNumber(&RecordOptionsNP); currentCCD->defineNumber(&StreamFrameNP); currentCCD->defineSwitch(&EncoderSP); currentCCD->defineSwitch(&RecorderSP); } } bool StreamManager::updateProperties() { if (currentCCD->isConnected()) { imageBP = currentCCD->getBLOB("CCD1"); imageB = imageBP->bp; currentCCD->defineSwitch(&StreamSP); currentCCD->defineNumber(&StreamOptionsNP); currentCCD->defineNumber(&FpsNP); currentCCD->defineSwitch(&RecordStreamSP); currentCCD->defineText(&RecordFileTP); currentCCD->defineNumber(&RecordOptionsNP); currentCCD->defineNumber(&StreamFrameNP); currentCCD->defineSwitch(&EncoderSP); currentCCD->defineSwitch(&RecorderSP); } else { currentCCD->deleteProperty(StreamSP.name); currentCCD->deleteProperty(StreamOptionsNP.name); currentCCD->deleteProperty(FpsNP.name); //ccd->deleteProperty(FramestoDropNP.name); currentCCD->deleteProperty(RecordFileTP.name); currentCCD->deleteProperty(RecordStreamSP.name); currentCCD->deleteProperty(RecordOptionsNP.name); currentCCD->deleteProperty(StreamFrameNP.name); currentCCD->deleteProperty(EncoderSP.name); currentCCD->deleteProperty(RecorderSP.name); return true; } return true; } /* * The camera driver is expected to send the FULL FRAME of the Camera after BINNING without any subframing at all * Subframing for streaming/recording is done in the stream manager. * Therefore nbytes is expected to be SubW/BinX * SubH/BinY * Bytes_Per_Pixels * Number_Color_Components * Binned frame must be sent from the camera driver for this to work consistentaly for all drivers.*/ void StreamManager::newFrame(const uint8_t *buffer, uint32_t nbytes) { double ms1, ms2, deltams; // Measure FPS getitimer(ITIMER_REAL, &tframe2); //ms2=capture->get(CV_CAP_PROP_POS_MSEC); ms1 = (1000.0 * (double)tframe1.it_value.tv_sec) + ((double)tframe1.it_value.tv_usec / 1000.0); ms2 = (1000.0 * (double)tframe2.it_value.tv_sec) + ((double)tframe2.it_value.tv_usec / 1000.0); if (ms2 > ms1) deltams = ms2 - ms1; //ms1 +( (24*3600*1000.0) - ms2); else deltams = ms1 - ms2; //EstFps->value=1000.0 / deltams; tframe1 = tframe2; mssum += deltams; framecountsec += 1; FpsN[0].value = 1000.0 / deltams; if (mssum >= 1000.0) { FpsN[1].value = (framecountsec * 1000.0) / mssum; mssum = 0; framecountsec = 0; } IDSetNumber(&FpsNP, nullptr); // For streaming, downscale 16 to 8 if (m_PixelDepth == 16 && (StreamSP.s == IPS_BUSY || RecordStreamSP.s == IPS_BUSY)) { // Do not downscale for SER recorder. if (isRecording() && !strcmp(recorder->getName(), "SER")) { recordStream(buffer, nbytes, deltams); } uint32_t npixels = (currentCCD->PrimaryCCD.getSubW()/currentCCD->PrimaryCCD.getBinX()) * (currentCCD->PrimaryCCD.getSubH()/currentCCD->PrimaryCCD.getBinY()) * ((m_PixelFormat == INDI_RGB) ? 3 : 1); //uint32_t npixels = StreamFrameN[CCDChip::FRAME_W].value * StreamFrameN[CCDChip::FRAME_H].value * ((m_PixelFormat == INDI_RGB) ? 3 : 1); // Allocale new buffer if size changes if (downscaleBufferSize != npixels) { downscaleBufferSize = npixels; delete [] downscaleBuffer; downscaleBuffer = new uint8_t[npixels]; } const uint16_t *srcBuffer = reinterpret_cast(buffer); //buffer = downscaleBuffer; // Slow method: proper downscale /* uint16_t max = 32768; uint16_t min = 0; for (uint32_t i=0; i < npixels; i++) { if (srcBuffer[i] > max) max = srcBuffer[i]; else if (srcBuffer[i] < min) min = srcBuffer[i]; } double bscale = 255. / (max - min); double bzero = (-min) * (255. / (max - min)); for (uint32_t i=0; i < npixels; i++) streamBuffer[i] = srcBuffer[i] * bscale + bzero; */ // Fast method: Cut off anything higher than 255. Image will be saturated. // Dividing by 255 works, but for astronomical images it's too dark. for (uint32_t i=0; i < npixels; i++) downscaleBuffer[i] = std::max(0, std::min(255, static_cast(srcBuffer[i]))); nbytes /= 2; if (StreamSP.s == IPS_BUSY) { streamframeCount++; if (streamframeCount >= StreamOptionsN[OPTION_RATE_DIVISOR].value) { uploadStream(downscaleBuffer, nbytes); streamframeCount = 0; } } // If anything but SER, let's call recorder. Otherwise, it's been called up before. if (isRecording() && strcmp(recorder->getName(), "SER")) { recordStream(downscaleBuffer, nbytes, deltams); } } else { if (StreamSP.s == IPS_BUSY) { streamframeCount++; if (streamframeCount >= StreamOptionsN[OPTION_RATE_DIVISOR].value) { if (uploadStream(buffer, nbytes) == false) { LOG_ERROR("Streaming failed."); setStream(false); return; } streamframeCount = 0; } } if (RecordStreamSP.s == IPS_BUSY) { if (recordStream(buffer, nbytes, deltams) == false) { LOG_ERROR("Recording failed."); stopRecording(); return; } } } } void StreamManager::setSize(uint16_t width, uint16_t height) { if (width != StreamFrameN[CCDChip::FRAME_W].value || height != StreamFrameN[CCDChip::FRAME_H].value) { if (m_PixelFormat == INDI_JPG) LOG_WARN("Cannot subframe JPEG streams."); StreamFrameN[CCDChip::FRAME_X].value = 0; StreamFrameN[CCDChip::FRAME_X].max = width - 1; StreamFrameN[CCDChip::FRAME_Y].value = 0; StreamFrameN[CCDChip::FRAME_Y].max = height - 1; StreamFrameN[CCDChip::FRAME_W].value = width; StreamFrameN[CCDChip::FRAME_W].min = 10; StreamFrameN[CCDChip::FRAME_W].max = width; StreamFrameN[CCDChip::FRAME_H].value = height; StreamFrameN[CCDChip::FRAME_H].min = 10; StreamFrameN[CCDChip::FRAME_H].max = height; StreamFrameNP.s = IPS_OK; IUUpdateMinMax(&StreamFrameNP); } // Width & Height are BINNED dimensions. // Since they're the final size to make it to encoders and recorders. rawWidth = width; rawHeight = height; for (EncoderInterface *oneEncoder : encoderManager->getEncoderList()) oneEncoder->setSize(rawWidth, rawHeight); for (RecorderInterface *oneRecorder : recorderManager->getRecorderList()) oneRecorder->setSize(rawWidth, rawHeight); } bool StreamManager::close() { return recorder->close(); } bool StreamManager::setPixelFormat(INDI_PIXEL_FORMAT pixelFormat, uint8_t pixelDepth) { bool recorderOK = recorder->setPixelFormat(pixelFormat, pixelDepth); if (recorderOK == false) { LOGF_ERROR("Pixel format is not supported by %s recorder.", recorder->getName()); } bool encoderOK = encoder->setPixelFormat(pixelFormat, pixelDepth); if (encoderOK == false) { LOGF_ERROR("Pixel format is not supported by %s encoder.", encoder->getName()); } m_PixelFormat = pixelFormat; m_PixelDepth = pixelDepth; return true; } bool StreamManager::recordStream(const uint8_t *buffer, uint32_t nbytes, double deltams) { if (!m_isRecording) return false; bool rc = recorder->writeFrame(buffer, nbytes); if (rc == false) return rc; recordDuration += deltams; recordframeCount += 1; if ((RecordStreamSP.sp[1].s == ISS_ON) && (recordDuration >= (RecordOptionsNP.np[0].value * 1000.0))) { LOGF_INFO("Ending record after %g millisecs", recordDuration); stopRecording(); RecordStreamSP.sp[1].s = ISS_OFF; RecordStreamSP.sp[3].s = ISS_ON; RecordStreamSP.s = IPS_IDLE; IDSetSwitch(&RecordStreamSP, nullptr); } if ((RecordStreamSP.sp[2].s == ISS_ON) && (recordframeCount >= (RecordOptionsNP.np[1].value))) { LOGF_INFO("Ending record after %d frames", recordframeCount); stopRecording(); RecordStreamSP.sp[2].s = ISS_OFF; RecordStreamSP.sp[3].s = ISS_ON; RecordStreamSP.s = IPS_IDLE; IDSetSwitch(&RecordStreamSP, nullptr); } return true; } int StreamManager::mkpath(std::string s, mode_t mode) { size_t pre = 0, pos; std::string dir; int mdret = 0; struct stat st; if (s[s.size() - 1] != '/') s += '/'; while ((pos = s.find_first_of('/', pre)) != std::string::npos) { dir = s.substr(0, pos++); pre = pos; if (dir.size() == 0) continue; if (stat(dir.c_str(), &st)) { if (errno != ENOENT || ((mdret = mkdir(dir.c_str(), mode)) && errno != EEXIST)) { LOGF_WARN("mkpath: can not create %s", dir.c_str()); return mdret; } } else { if (!S_ISDIR(st.st_mode)) { LOGF_WARN("mkpath: %s is not a directory", dir.c_str()); return -1; } } } return mdret; } std::string StreamManager::expand(std::string fname, const std::map &patterns) { std::string res = fname; std::size_t pos; time_t now; struct tm *tm_now; char val[20]; *(val + 19) = '\0'; time(&now); tm_now = gmtime(&now); pos = res.find("_D_"); if (pos != std::string::npos) { strftime(val, 11, "%F", tm_now); res.replace(pos, 3, val); } pos = res.find("_T_"); if (pos != std::string::npos) { strftime(val, 20, "%F@%T", tm_now); res.replace(pos, 3, val); } pos = res.find("_H_"); if (pos != std::string::npos) { strftime(val, 9, "%T", tm_now); res.replace(pos, 3, val); } for (std::map::const_iterator it = patterns.begin(); it != patterns.end(); ++it) { pos = res.find(it->first); if (pos != std::string::npos) { res.replace(pos, it->first.size(), it->second); } } // Replace all : to - to be valid filename on Windows size_t start_pos = 0; while ((start_pos = res.find(":", start_pos)) != std::string::npos) { res.replace(start_pos, 1, "-"); start_pos++; } return res; } bool StreamManager::startRecording() { char errmsg[MAXRBUF]; std::string filename, expfilename, expfiledir; std::string filtername; std::map patterns; if (m_isRecording) return true; /* get filter name for pattern substitution */ if (currentCCD->CurrentFilterSlot != -1 && currentCCD->CurrentFilterSlot <= (int)currentCCD->FilterNames.size()) { filtername = currentCCD->FilterNames.at(currentCCD->CurrentFilterSlot - 1); patterns["_F_"] = filtername; LOGF_DEBUG("Adding filter pattern %s", filtername.c_str()); } recorder->setFPS(FpsN[FPS_AVERAGE].value); /* pattern substitution */ recordfiledir.assign(RecordFileTP.tp[0].text); expfiledir = expand(recordfiledir, patterns); if (expfiledir.at(expfiledir.size() - 1) != '/') expfiledir += '/'; recordfilename.assign(RecordFileTP.tp[1].text); expfilename = expand(recordfilename, patterns); if (expfilename.substr(expfilename.size() - 4, 4) != recorder->getExtension()) expfilename += recorder->getExtension(); filename = expfiledir + expfilename; //LOGF_INFO("Expanded file is %s", filename.c_str()); //filename=recordfiledir+recordfilename; LOGF_INFO("Record file is %s", filename.c_str()); /* Create/open file/dir */ if (mkpath(expfiledir, 0755)) { LOGF_WARN("Can not create record directory %s: %s", expfiledir.c_str(), strerror(errno)); return false; } if (!recorder->open(filename.c_str(), errmsg)) { RecordStreamSP.s = IPS_ALERT; IDSetSwitch(&RecordStreamSP, nullptr); LOGF_WARN("Can not open record file: %s", errmsg); return false; } #if 0 /* start capture */ // TODO direct recording should this be part of StreamManager? if (direct_record) { LOG_INFO("Using direct recording (no software cropping)."); //v4l_base->doDecode(false); //v4l_base->doRecord(true); } else { //if (ImageColorS[IMAGE_GRAYSCALE].s == ISS_ON) if (currentCCD->PrimaryCCD.getNAxis() == 2) recorder->setDefaultMono(); else recorder->setDefaultColor(); } #endif recordDuration = 0.0; recordframeCount = 0; getitimer(ITIMER_REAL, &tframe1); mssum = 0; framecountsec = 0; if (m_isStreaming == false && currentCCD->StartStreaming() == false) { LOG_ERROR("Failed to start recording."); RecordStreamSP.s = IPS_ALERT; IUResetSwitch(&RecordStreamSP); RecordStreamS[RECORD_OFF].s = ISS_ON; IDSetSwitch(&RecordStreamSP, nullptr); } m_isRecording = true; return true; } bool StreamManager::stopRecording() { if (!m_isRecording) return true; if (!m_isStreaming) currentCCD->StopStreaming(); m_isRecording = false; recorder->close(); LOGF_INFO("Record Duration(millisec): %g -- Frame count: %d", recordDuration, recordframeCount); return true; } bool StreamManager::ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) { if (dev != nullptr && strcmp(getDeviceName(), dev)) return true; /* Video Stream */ if (!strcmp(name, StreamSP.name)) { for (int i = 0; i < n; i++) { if (!strcmp(names[i], "STREAM_ON") && states[i] == ISS_ON) { setStream(true); break; } else if (!strcmp(names[i], "STREAM_OFF") && states[i] == ISS_ON) { setStream(false); break; } } return true; } // Record Stream if (!strcmp(name, RecordStreamSP.name)) { int prevSwitch = IUFindOnSwitchIndex(&RecordStreamSP); IUUpdateSwitch(&RecordStreamSP, states, names, n); if (m_isRecording && RecordStreamSP.sp[3].s != ISS_ON) { IUResetSwitch(&RecordStreamSP); RecordStreamS[prevSwitch].s = ISS_ON; IDSetSwitch(&RecordStreamSP, nullptr); LOG_WARN("Recording device is busy."); return false; } if ((RecordStreamSP.sp[0].s == ISS_ON) || (RecordStreamSP.sp[1].s == ISS_ON) || (RecordStreamSP.sp[2].s == ISS_ON)) { if (!m_isRecording) { RecordStreamSP.s = IPS_BUSY; if (RecordStreamSP.sp[1].s == ISS_ON) LOGF_INFO("Starting video record (Duration): %g secs.", RecordOptionsNP.np[0].value); else if (RecordStreamSP.sp[2].s == ISS_ON) LOGF_INFO("Starting video record (Frame count): %d.", (int)(RecordOptionsNP.np[1].value)); else LOG_INFO("Starting video record."); if (!startRecording()) { RecordStreamSP.sp[0].s = ISS_OFF; RecordStreamSP.sp[1].s = ISS_OFF; RecordStreamSP.sp[2].s = ISS_OFF; RecordStreamSP.sp[3].s = ISS_ON; RecordStreamSP.s = IPS_ALERT; } } } else { RecordStreamSP.s = IPS_IDLE; if (m_isRecording) { LOGF_INFO("Recording stream has been disabled. Frame count %d", recordframeCount); stopRecording(); } } IDSetSwitch(&RecordStreamSP, nullptr); return true; } // Encoder Selection if (!strcmp(name, EncoderSP.name)) { IUUpdateSwitch(&EncoderSP, states, names, n); EncoderSP.s = IPS_ALERT; const char *selectedEncoder = IUFindOnSwitch(&EncoderSP)->name; for (EncoderInterface *oneEncoder : encoderManager->getEncoderList()) { if (!strcmp(selectedEncoder, oneEncoder->getName())) { encoderManager->setEncoder(oneEncoder); oneEncoder->setPixelFormat(m_PixelFormat, m_PixelDepth); encoder = oneEncoder; EncoderSP.s = IPS_OK; } } IDSetSwitch(&EncoderSP, nullptr); } // Recorder Selection if (!strcmp(name, RecorderSP.name)) { IUUpdateSwitch(&RecorderSP, states, names, n); RecorderSP.s = IPS_ALERT; const char *selectedRecorder = IUFindOnSwitch(&RecorderSP)->name; for (RecorderInterface *oneRecorder : recorderManager->getRecorderList()) { if (!strcmp(selectedRecorder, oneRecorder->getName())) { recorderManager->setRecorder(oneRecorder); oneRecorder->setPixelFormat(m_PixelFormat, m_PixelDepth); recorder = oneRecorder; RecorderSP.s = IPS_OK; } } IDSetSwitch(&RecorderSP, nullptr); } return true; } bool StreamManager::ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n) { /* ignore if not ours */ if (dev != nullptr && strcmp(getDeviceName(), dev)) return true; if (!strcmp(name, RecordFileTP.name)) { IText *tp = IUFindText(&RecordFileTP, "RECORD_FILE_NAME"); if (strchr(tp->text, '/')) { LOG_WARN("Dir. separator (/) not allowed in filename."); return false; } IUUpdateText(&RecordFileTP, texts, names, n); IDSetText(&RecordFileTP, nullptr); return true; } return true; } bool StreamManager::ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) { /* ignore if not ours */ if (dev != nullptr && strcmp(getDeviceName(), dev)) return true; /* Stream rate */ if (!strcmp(StreamOptionsNP.name, name)) { IUUpdateNumber(&StreamOptionsNP, values, names, n); StreamOptionsNP.s = IPS_OK; IDSetNumber(&StreamOptionsNP, nullptr); return true; } /* Record Options */ if (!strcmp(RecordOptionsNP.name, name)) { if (m_isRecording) { LOG_WARN("Recording device is busy"); return false; } IUUpdateNumber(&RecordOptionsNP, values, names, n); RecordOptionsNP.s = IPS_OK; IDSetNumber(&RecordOptionsNP, nullptr); return true; } /* Stream Frame */ if (!strcmp(StreamFrameNP.name, name)) { if (m_isRecording) { LOG_WARN("Recording device is busy"); return false; } int subW = currentCCD->PrimaryCCD.getSubW() / currentCCD->PrimaryCCD.getBinX(); int subH = currentCCD->PrimaryCCD.getSubH() / currentCCD->PrimaryCCD.getBinY(); IUUpdateNumber(&StreamFrameNP, values, names, n); StreamFrameNP.s = IPS_OK; if (StreamFrameN[CCDChip::FRAME_X].value + StreamFrameN[CCDChip::FRAME_W].value > subW) StreamFrameN[CCDChip::FRAME_W].value = subW - StreamFrameN[CCDChip::FRAME_X].value; if (StreamFrameN[CCDChip::FRAME_Y].value + StreamFrameN[CCDChip::FRAME_H].value > subH) StreamFrameN[CCDChip::FRAME_H].value = subH - StreamFrameN[CCDChip::FRAME_Y].value; setSize(StreamFrameN[CCDChip::FRAME_W].value, StreamFrameN[CCDChip::FRAME_H].value); IDSetNumber(&StreamFrameNP, nullptr); return true; } /* Frames to drop */ /*if (!strcmp (FramestoDropNP.name, name)) { IUUpdateNumber(&FramestoDropNP, values, names, n); //v4l_base->setDropFrameCount(values[0]); FramestoDropNP.s = IPS_OK; IDSetNumber(&FramestoDropNP, nullptr); return true; }*/ return true; } bool StreamManager::setStream(bool enable) { if (enable) { if (!m_isStreaming) { StreamSP.s = IPS_BUSY; streamframeCount = 0; if (StreamOptionsN[OPTION_RATE_DIVISOR].value > 0) DEBUGF(INDI::Logger::DBG_SESSION, "Starting the video stream with target FPS %.f and rate divisor of %.f", StreamOptionsN[OPTION_TARGET_FPS].value, StreamOptionsN[OPTION_RATE_DIVISOR].value); else LOGF_INFO("Starting the video stream with target FPS %.f", StreamOptionsN[OPTION_TARGET_FPS].value); streamframeCount = 0; getitimer(ITIMER_REAL, &tframe1); mssum = 0; framecountsec = 0; if (currentCCD->StartStreaming() == false) { IUResetSwitch(&StreamSP); StreamS[1].s = ISS_ON; StreamSP.s = IPS_ALERT; LOG_ERROR("Failed to start streaming."); IDSetSwitch(&StreamSP, nullptr); return false; } m_isStreaming = true; IUResetSwitch(&StreamSP); StreamS[0].s = ISS_ON; recorder->setStreamEnabled(true); } } else { StreamSP.s = IPS_IDLE; if (m_isStreaming) { LOGF_DEBUG("The video stream has been disabled. Frame count %d", streamframeCount); //if (!is_exposing && !is_recording) stop_capturing(); if (!m_isRecording) { if (currentCCD->StopStreaming() == false) { StreamSP.s = IPS_ALERT; LOG_ERROR("Failed to stop streaming."); IDSetSwitch(&StreamSP, nullptr); return false; } } IUResetSwitch(&StreamSP); StreamS[1].s = ISS_ON; m_isStreaming = false; recorder->setStreamEnabled(false); } } IDSetSwitch(&StreamSP, nullptr); return true; } bool StreamManager::saveConfigItems(FILE *fp) { IUSaveConfigSwitch(fp, &EncoderSP); IUSaveConfigText(fp, &RecordFileTP); IUSaveConfigNumber(fp, &RecordOptionsNP); IUSaveConfigSwitch(fp, &RecorderSP); return true; } void StreamManager::getStreamFrame(uint16_t *x, uint16_t *y, uint16_t *w, uint16_t *h) { *x = StreamFrameN[CCDChip::FRAME_X].value; *y = StreamFrameN[CCDChip::FRAME_Y].value; *w = StreamFrameN[CCDChip::FRAME_W].value; *h = StreamFrameN[CCDChip::FRAME_H].value; } bool StreamManager::uploadStream(const uint8_t *buffer, uint32_t nbytes) { // Send as is, already encoded. if (m_PixelFormat == INDI_JPG) { // Upload to client now imageB->blob = (const_cast(buffer)); imageB->bloblen = nbytes; imageB->size = nbytes; strcpy(imageB->format, ".stream_jpg"); imageBP->s = IPS_OK; IDSetBLOB(imageBP, nullptr); return true; } //memcpy(currentCCD->PrimaryCCD.getFrameBuffer(), buffer, currentCCD->PrimaryCCD.getFrameBufferSize()); // Binning for grayscale frames only for now #if 0 if (currentCCD->PrimaryCCD.getNAxis() == 2) { currentCCD->PrimaryCCD.binFrame(); nbytes /= (currentCCD->PrimaryCCD.getBinX() * currentCCD->PrimaryCCD.getBinY()); } #endif int subX, subY, subW, subH; subX = currentCCD->PrimaryCCD.getSubX() / currentCCD->PrimaryCCD.getBinX(); subY = currentCCD->PrimaryCCD.getSubY() / currentCCD->PrimaryCCD.getBinY(); subW = currentCCD->PrimaryCCD.getSubW() / currentCCD->PrimaryCCD.getBinX(); subH = currentCCD->PrimaryCCD.getSubH() / currentCCD->PrimaryCCD.getBinY(); //uint8_t *streamBuffer = buffer; // If stream frame was not yet initilized, let's do that now if (StreamFrameN[CCDChip::FRAME_W].value == 0 || StreamFrameN[CCDChip::FRAME_H].value == 0) { //if (currentCCD->PrimaryCCD.getNAxis() == 2) // binFactor = currentCCD->PrimaryCCD.getBinX(); StreamFrameN[CCDChip::FRAME_X].value = subX; StreamFrameN[CCDChip::FRAME_Y].value = subY; StreamFrameN[CCDChip::FRAME_W].value = subW; StreamFrameN[CCDChip::FRAME_W].value = subH; StreamFrameNP.s = IPS_IDLE; IDSetNumber(&StreamFrameNP, nullptr); } // Check if we need to subframe else if ((StreamFrameN[CCDChip::FRAME_W].value > 0 && StreamFrameN[CCDChip::FRAME_H].value > 0) && (StreamFrameN[CCDChip::FRAME_X].value != subX || StreamFrameN[CCDChip::FRAME_Y].value != subY || StreamFrameN[CCDChip::FRAME_W].value != subW || StreamFrameN[CCDChip::FRAME_H].value != subH)) { uint32_t npixels = StreamFrameN[CCDChip::FRAME_W].value * StreamFrameN[CCDChip::FRAME_H].value * ((m_PixelFormat == INDI_RGB) ? 3 : 1); if (downscaleBufferSize < npixels) { downscaleBufferSize = npixels; delete [] downscaleBuffer; downscaleBuffer = new uint8_t[npixels]; } uint32_t sourceOffset = (subW * StreamFrameN[CCDChip::FRAME_Y].value) + StreamFrameN[CCDChip::FRAME_X].value; uint8_t components = (m_PixelFormat == INDI_RGB) ? 3 : 1; const uint8_t *srcBuffer = buffer + sourceOffset * components; uint32_t sourceStride = subW * components; uint8_t *destBuffer = downscaleBuffer; uint32_t desStride = StreamFrameN[CCDChip::FRAME_W].value * components; // Copy line-by-line for (int i = 0; i < StreamFrameN[CCDChip::FRAME_H].value; i++) memcpy(destBuffer + i * desStride, srcBuffer + sourceStride * i, desStride); //encoder->setSize(StreamFrameN[CCDChip::FRAME_W].value, StreamFrameN[CCDChip::FRAME_H].value); nbytes = StreamFrameN[CCDChip::FRAME_W].value * StreamFrameN[CCDChip::FRAME_H].value; if (encoder->upload(imageB, downscaleBuffer, nbytes, currentCCD->PrimaryCCD.isCompressed())) { // Upload to client now imageBP->s = IPS_OK; IDSetBLOB(imageBP, nullptr); return true; } return false; } #if 0 // For MONO if (currentCCD->PrimaryCCD.getNAxis() == 2) { int binFactor = (currentCCD->PrimaryCCD.getBinX() * currentCCD->PrimaryCCD.getBinY()); int offset = ((subW * StreamFrameN[CCDChip::FRAME_Y].value) + StreamFrameN[CCDChip::FRAME_X].value) / binFactor; uint8_t *srcBuffer = buffer + offset; uint8_t *destBuffer = buffer; for (int i = 0; i < StreamFrameN[CCDChip::FRAME_H].value; i++) memcpy(destBuffer + i * static_cast(StreamFrameN[CCDChip::FRAME_W].value), srcBuffer + subW * i, StreamFrameN[CCDChip::FRAME_W].value); streamW = StreamFrameN[CCDChip::FRAME_W].value; streamH = StreamFrameN[CCDChip::FRAME_H].value; } // For Color else { // Subframe offset in source frame. i.e. where we start copying data from in the original data frame int sourceOffset = (subW * StreamFrameN[CCDChip::FRAME_Y].value) + StreamFrameN[CCDChip::FRAME_X].value; // Total bytes //totalBytes = (StreamFrameN[CCDChip::FRAME_W].value * StreamFrameN[CCDChip::FRAME_H].value) * 3; // Copy each color component back into buffer. Since each subframed page is equal or small than source component // no need to a new buffer uint8_t *srcBuffer = buffer + sourceOffset * 3; uint8_t *destBuffer = buffer; // RGB for (int i = 0; i < StreamFrameN[CCDChip::FRAME_H].value; i++) memcpy(destBuffer + i * static_cast(StreamFrameN[CCDChip::FRAME_W].value * 3), srcBuffer + subW * 3 * i, StreamFrameN[CCDChip::FRAME_W].value * 3); } #endif if (encoder->upload(imageB, buffer, nbytes, currentCCD->PrimaryCCD.isCompressed())) { // Upload to client now imageBP->s = IPS_OK; IDSetBLOB(imageBP, nullptr); return true; } return false; } } libindi/libs/stream/jpegutils.c0000664000175000017500000013535413263645557016103 0ustar jasemjasem/* * jpegutils.c: Some Utility programs for dealing with * JPEG encoded images * * Copyright (C) 1999 Rainer Johanni * Copyright (C) 2001 pHilipp Zabel * Copyright (C) 2008 Angel Carpintero * * based on jdatasrc.c and jdatadst.c from the Independent * JPEG Group's software by Thomas G. Lane * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 2 * of the License, or (at your option) any later version. * * This program 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 General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. */ #include "jpegutils.h" #include #include #include #include #include #include #include /* * jpeg_data: buffer with input / output jpeg * len: Length of jpeg buffer * itype: 0: Not interlaced * 1: Interlaced, Top field first * 2: Interlaced, Bottom field first * ctype Chroma format for decompression. * Currently only Y4M_CHROMA_{420JPEG,422} are available * raw0 buffer with input / output raw Y channel * raw1 buffer with input / output raw U/Cb channel * raw2 buffer with input / output raw V/Cr channel * width width of Y channel (width of U/V is width/2) * height height of Y channel (height of U/V is height/2) */ static void jpeg_buffer_src(j_decompress_ptr cinfo, unsigned char *buffer, long num); static void jpeg_buffer_dest(j_compress_ptr cinfo, unsigned char *buffer, long len); static void jpeg_skip_ff(j_decompress_ptr cinfo); /******************************************************************* * * * The following routines define a JPEG Source manager which * * just reads from a given buffer (instead of a file as in * * the jpeg library) * * * *******************************************************************/ /* * Initialize source --- called by jpeg_read_header * before any data is actually read. */ static void init_source(j_decompress_ptr cinfo) { (void)cinfo; /* no work necessary here */ } /* * Fill the input buffer --- called whenever buffer is emptied. * * Should never be called since all data should be allready provided. * Is nevertheless sometimes called - sets the input buffer to data * which is the JPEG EOI marker; * */ static uint8_t EOI_data[2] = { 0xFF, 0xD9 }; static boolean fill_input_buffer(j_decompress_ptr cinfo) { cinfo->src->next_input_byte = EOI_data; cinfo->src->bytes_in_buffer = 2; return TRUE; } /* * Skip data --- used to skip over a potentially large amount of * uninteresting data (such as an APPn marker). * */ static void skip_input_data(j_decompress_ptr cinfo, long num_bytes) { if (num_bytes > 0) { if (num_bytes > (long)cinfo->src->bytes_in_buffer) num_bytes = (long)cinfo->src->bytes_in_buffer; cinfo->src->next_input_byte += (size_t)num_bytes; cinfo->src->bytes_in_buffer -= (size_t)num_bytes; } } /* * Terminate source --- called by jpeg_finish_decompress * after all data has been read. Often a no-op. */ static void term_source(j_decompress_ptr cinfo) { (void)cinfo; /* no work necessary here */ } /* * Prepare for input from a data buffer. */ static void jpeg_buffer_src(j_decompress_ptr cinfo, unsigned char *buffer, long num) { /* The source object and input buffer are made permanent so that a series * of JPEG images can be read from the same buffer by calling jpeg_buffer_src * only before the first one. (If we discarded the buffer at the end of * one image, we'd likely lose the start of the next one.) * This makes it unsafe to use this manager and a different source * manager serially with the same JPEG object. Caveat programmer. */ if (cinfo->src == NULL) /* first time for this JPEG object? */ { cinfo->src = (struct jpeg_source_mgr *)(*cinfo->mem->alloc_small)((j_common_ptr)cinfo, JPOOL_PERMANENT, sizeof(struct jpeg_source_mgr)); } cinfo->src->init_source = init_source; cinfo->src->fill_input_buffer = fill_input_buffer; cinfo->src->skip_input_data = skip_input_data; cinfo->src->resync_to_restart = jpeg_resync_to_restart; /* use default method */ cinfo->src->term_source = term_source; cinfo->src->bytes_in_buffer = num; cinfo->src->next_input_byte = (JOCTET *)buffer; } /* * jpeg_skip_ff is not a part of the source manager but it is * particularly useful when reading several images from the same buffer: * It should be called to skip padding 0xff bytes beetween images. */ static void jpeg_skip_ff(j_decompress_ptr cinfo) { while (cinfo->src->bytes_in_buffer > 1 && cinfo->src->next_input_byte[0] == 0xff && cinfo->src->next_input_byte[1] == 0xff) { cinfo->src->bytes_in_buffer--; cinfo->src->next_input_byte++; } } /******************************************************************* * * * The following routines define a JPEG Destination manager * * which just reads from a given buffer (instead of a file * * as in the jpeg library) * * * *******************************************************************/ /* * Initialize destination --- called by jpeg_start_compress * before any data is actually written. */ static void init_destination(j_compress_ptr cinfo) { (void)cinfo; /* No work necessary here */ } /* * Empty the output buffer --- called whenever buffer fills up. * * Should never be called since all data should be written to the buffer. * If it gets called, the given jpeg buffer was too small. * */ static boolean empty_output_buffer(j_compress_ptr cinfo) { (void)cinfo; return TRUE; } /* * Terminate destination --- called by jpeg_finish_compress * after all data has been written. Usually needs to flush buffer. * * NB: *not* called by jpeg_abort or jpeg_destroy; surrounding * application must deal with any cleanup that should happen even * for error exit. */ static void term_destination(j_compress_ptr cinfo) { (void)cinfo; /* no work necessary here */ } /* * Prepare for output to a stdio stream. * The caller must have already opened the stream, and is responsible * for closing it after finishing compression. */ static void jpeg_buffer_dest(j_compress_ptr cinfo, unsigned char *buf, long len) { /* The destination object is made permanent so that multiple JPEG images * can be written to the same file without re-executing jpeg_stdio_dest. * This makes it dangerous to use this manager and a different destination * manager serially with the same JPEG object, because their private object * sizes may be different. Caveat programmer. */ if (cinfo->dest == NULL) /* first time for this JPEG object? */ { cinfo->dest = (struct jpeg_destination_mgr *)(*cinfo->mem->alloc_small)((j_common_ptr)cinfo, JPOOL_PERMANENT, sizeof(struct jpeg_destination_mgr)); } cinfo->dest->init_destination = init_destination; cinfo->dest->empty_output_buffer = empty_output_buffer; cinfo->dest->term_destination = term_destination; cinfo->dest->free_in_buffer = len; cinfo->dest->next_output_byte = (JOCTET *)buf; } /******************************************************************* * * * decode_jpeg_data: Decode a (possibly interlaced) JPEG frame * * * *******************************************************************/ /* * ERROR HANDLING: * * We want in all cases to return to the user. * The following kind of error handling is from the * example.c file in the Independent JPEG Group's JPEG software */ struct my_error_mgr { struct jpeg_error_mgr pub; /* "public" fields */ jmp_buf setjmp_buffer; /* for return to caller */ /* original emit_message method */ JMETHOD(void, original_emit_message, (j_common_ptr cinfo, int msg_level)); /* was a corrupt-data warning seen */ int warning_seen; }; static void my_error_exit(j_common_ptr cinfo) { /* cinfo->err really points to a my_error_mgr struct, so coerce pointer */ struct my_error_mgr *myerr = (struct my_error_mgr *)cinfo->err; /* Always display the message. */ /* We could postpone this until after returning, if we chose. */ (*cinfo->err->output_message)(cinfo); /* Return control to the setjmp point */ longjmp(myerr->setjmp_buffer, 1); } static void my_emit_message(j_common_ptr cinfo, int msg_level) { /* cinfo->err really points to a my_error_mgr struct, so coerce pointer */ struct my_error_mgr *myerr = (struct my_error_mgr *)cinfo->err; if (msg_level < 0) myerr->warning_seen = 1; /* call original emit_message() */ /* geehalel (myerr->original_emit_message)(cinfo, msg_level); */ } #define MAX_LUMA_WIDTH 4096 #define MAX_CHROMA_WIDTH 2048 static unsigned char buf0[16][MAX_LUMA_WIDTH]; static unsigned char buf1[8][MAX_CHROMA_WIDTH]; static unsigned char buf2[8][MAX_CHROMA_WIDTH]; static unsigned char chr1[8][MAX_CHROMA_WIDTH]; static unsigned char chr2[8][MAX_CHROMA_WIDTH]; #if 1 /* generation of 'std' Huffman tables... */ static void add_huff_table(j_decompress_ptr dinfo, JHUFF_TBL **htblptr, const UINT8 *bits, const UINT8 *val) /* Define a Huffman table */ { int nsymbols, len; if (*htblptr == NULL) *htblptr = jpeg_alloc_huff_table((j_common_ptr)dinfo); /* Copy the number-of-symbols-of-each-code-length counts */ memcpy((*htblptr)->bits, bits, sizeof((*htblptr)->bits)); /* Validate the counts. We do this here mainly so we can copy the right * number of symbols from the val[] array, without risking marching off * the end of memory. jchuff.c will do a more thorough test later. */ nsymbols = 0; for (len = 1; len <= 16; len++) nsymbols += bits[len]; if (nsymbols < 1 || nsymbols > 256) fprintf(stderr, "%s: Given jpeg buffer was too small", __FUNCTION__); memcpy((*htblptr)->huffval, val, nsymbols * sizeof(UINT8)); } static void std_huff_tables(j_decompress_ptr dinfo) /* Set up the standard Huffman tables (cf. JPEG standard section K.3) */ /* IMPORTANT: these are only valid for 8-bit data precision! */ { static const UINT8 bits_dc_luminance[17] = { /* 0-base */ 0, 0, 1, 5, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0 }; static const UINT8 val_dc_luminance[] = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11 }; static const UINT8 bits_dc_chrominance[17] = { /* 0-base */ 0, 0, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0 }; static const UINT8 val_dc_chrominance[] = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11 }; static const UINT8 bits_ac_luminance[17] = { /* 0-base */ 0, 0, 2, 1, 3, 3, 2, 4, 3, 5, 5, 4, 4, 0, 0, 1, 0x7d }; static const UINT8 val_ac_luminance[] = { 0x01, 0x02, 0x03, 0x00, 0x04, 0x11, 0x05, 0x12, 0x21, 0x31, 0x41, 0x06, 0x13, 0x51, 0x61, 0x07, 0x22, 0x71, 0x14, 0x32, 0x81, 0x91, 0xa1, 0x08, 0x23, 0x42, 0xb1, 0xc1, 0x15, 0x52, 0xd1, 0xf0, 0x24, 0x33, 0x62, 0x72, 0x82, 0x09, 0x0a, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3a, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4a, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5a, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6a, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7a, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, 0x8a, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 0x98, 0x99, 0x9a, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6, 0xa7, 0xa8, 0xa9, 0xaa, 0xb2, 0xb3, 0xb4, 0xb5, 0xb6, 0xb7, 0xb8, 0xb9, 0xba, 0xc2, 0xc3, 0xc4, 0xc5, 0xc6, 0xc7, 0xc8, 0xc9, 0xca, 0xd2, 0xd3, 0xd4, 0xd5, 0xd6, 0xd7, 0xd8, 0xd9, 0xda, 0xe1, 0xe2, 0xe3, 0xe4, 0xe5, 0xe6, 0xe7, 0xe8, 0xe9, 0xea, 0xf1, 0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf8, 0xf9, 0xfa }; static const UINT8 bits_ac_chrominance[17] = { /* 0-base */ 0, 0, 2, 1, 2, 4, 4, 3, 4, 7, 5, 4, 4, 0, 1, 2, 0x77 }; static const UINT8 val_ac_chrominance[] = { 0x00, 0x01, 0x02, 0x03, 0x11, 0x04, 0x05, 0x21, 0x31, 0x06, 0x12, 0x41, 0x51, 0x07, 0x61, 0x71, 0x13, 0x22, 0x32, 0x81, 0x08, 0x14, 0x42, 0x91, 0xa1, 0xb1, 0xc1, 0x09, 0x23, 0x33, 0x52, 0xf0, 0x15, 0x62, 0x72, 0xd1, 0x0a, 0x16, 0x24, 0x34, 0xe1, 0x25, 0xf1, 0x17, 0x18, 0x19, 0x1a, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3a, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4a, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5a, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6a, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7a, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, 0x8a, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 0x98, 0x99, 0x9a, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6, 0xa7, 0xa8, 0xa9, 0xaa, 0xb2, 0xb3, 0xb4, 0xb5, 0xb6, 0xb7, 0xb8, 0xb9, 0xba, 0xc2, 0xc3, 0xc4, 0xc5, 0xc6, 0xc7, 0xc8, 0xc9, 0xca, 0xd2, 0xd3, 0xd4, 0xd5, 0xd6, 0xd7, 0xd8, 0xd9, 0xda, 0xe2, 0xe3, 0xe4, 0xe5, 0xe6, 0xe7, 0xe8, 0xe9, 0xea, 0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf8, 0xf9, 0xfa }; add_huff_table(dinfo, &dinfo->dc_huff_tbl_ptrs[0], bits_dc_luminance, val_dc_luminance); add_huff_table(dinfo, &dinfo->ac_huff_tbl_ptrs[0], bits_ac_luminance, val_ac_luminance); add_huff_table(dinfo, &dinfo->dc_huff_tbl_ptrs[1], bits_dc_chrominance, val_dc_chrominance); add_huff_table(dinfo, &dinfo->ac_huff_tbl_ptrs[1], bits_ac_chrominance, val_ac_chrominance); } static void guarantee_huff_tables(j_decompress_ptr dinfo) { if ((dinfo->dc_huff_tbl_ptrs[0] == NULL) && (dinfo->dc_huff_tbl_ptrs[1] == NULL) && (dinfo->ac_huff_tbl_ptrs[0] == NULL) && (dinfo->ac_huff_tbl_ptrs[1] == NULL)) { std_huff_tables(dinfo); } } #endif /* ...'std' Huffman table generation */ /* * jpeg_data: Buffer with jpeg data to decode * len: Length of buffer * itype: 0: Not interlaced * 1: Interlaced, Top field first * 2: Interlaced, Bottom field first * ctype Chroma format for decompression. * Currently only Y4M_CHROMA_{420JPEG,422} are available * returns: * -1 on fatal error * 0 on success * 1 if jpeg lib threw a "corrupt jpeg data" warning. * in this case, "a damaged output image is likely." * */ int decode_jpeg_raw(unsigned char *jpeg_data, int len, int itype, int ctype, unsigned int width, unsigned int height, unsigned char *raw0, unsigned char *raw1, unsigned char *raw2) { int numfields, hsf[3], field, yl, yc; int i, xsl, xsc, xs, hdown; unsigned int x, y = 0, vsf[3], xd; JSAMPROW row0[16] = { buf0[0], buf0[1], buf0[2], buf0[3], buf0[4], buf0[5], buf0[6], buf0[7], buf0[8], buf0[9], buf0[10], buf0[11], buf0[12], buf0[13], buf0[14], buf0[15] }; JSAMPROW row1[8] = { buf1[0], buf1[1], buf1[2], buf1[3], buf1[4], buf1[5], buf1[6], buf1[7] }; JSAMPROW row2[16] = { buf2[0], buf2[1], buf2[2], buf2[3], buf2[4], buf2[5], buf2[6], buf2[7] }; JSAMPROW row1_444[16], row2_444[16]; JSAMPARRAY scanarray[3] = { row0, row1, row2 }; struct jpeg_decompress_struct dinfo; struct my_error_mgr jerr; /* We set up the normal JPEG error routines, then override error_exit. */ dinfo.err = jpeg_std_error(&jerr.pub); jerr.pub.error_exit = my_error_exit; /* also hook the emit_message routine to note corrupt-data warnings */ jerr.original_emit_message = jerr.pub.emit_message; jerr.pub.emit_message = my_emit_message; jerr.warning_seen = 0; /* Establish the setjmp return context for my_error_exit to use. */ if (setjmp(jerr.setjmp_buffer)) { /* If we get here, the JPEG code has signaled an error. */ jpeg_destroy_decompress(&dinfo); return -1; } jpeg_create_decompress(&dinfo); jpeg_buffer_src(&dinfo, jpeg_data, len); /* Read header, make some checks and try to figure out what the user really wants */ jpeg_read_header(&dinfo, TRUE); dinfo.raw_data_out = TRUE; dinfo.out_color_space = JCS_YCbCr; dinfo.dct_method = JDCT_IFAST; guarantee_huff_tables(&dinfo); jpeg_start_decompress(&dinfo); if (dinfo.output_components != 3) { fprintf(stderr, "%s: Output components of JPEG image = %d, must be 3", __FUNCTION__, dinfo.output_components); goto ERR_EXIT; } for (i = 0; i < 3; i++) { hsf[i] = dinfo.comp_info[i].h_samp_factor; vsf[i] = dinfo.comp_info[i].v_samp_factor; } if ((hsf[0] != 2 && hsf[0] != 1) || hsf[1] != 1 || hsf[2] != 1 || (vsf[0] != 1 && vsf[0] != 2) || vsf[1] != 1 || vsf[2] != 1) { fprintf(stderr, "%s: Unsupported sampling factors, hsf=(%d, %d, %d) " "vsf=(%d, %d, %d) !", __FUNCTION__, hsf[0], hsf[1], hsf[2], vsf[0], vsf[1], vsf[2]); goto ERR_EXIT; } if (hsf[0] == 1) { if (height % 8 != 0) { fprintf(stderr, "%s: YUV 4:4:4 sampling, but image height %d " "not dividable by 8 !", __FUNCTION__, height); goto ERR_EXIT; } for (y = 0; y < 16; y++) // allocate a special buffer for the extra sampling depth { row1_444[y] = (unsigned char *)malloc(dinfo.output_width * sizeof(unsigned char)); row2_444[y] = (unsigned char *)malloc(dinfo.output_width * sizeof(unsigned char)); } scanarray[1] = row1_444; scanarray[2] = row2_444; } /* Height match image height or be exact twice the image height */ if (dinfo.output_height == height) { numfields = 1; } else if (2 * dinfo.output_height == height) { numfields = 2; } else { fprintf(stderr, "%s: Read JPEG: requested height = %d, " "height of image = %d", __FUNCTION__, height, dinfo.output_height); goto ERR_EXIT; } /* Width is more flexible */ if (dinfo.output_width > MAX_LUMA_WIDTH) { fprintf(stderr, "%s: Image width of %d exceeds max", __FUNCTION__, dinfo.output_width); goto ERR_EXIT; } if (width < 2 * dinfo.output_width / 3) { /* Downsample 2:1 */ hdown = 1; if (2 * width < dinfo.output_width) xsl = (dinfo.output_width - 2 * width) / 2; else xsl = 0; } else if (width == 2 * dinfo.output_width / 3) { /* special case of 3:2 downsampling */ hdown = 2; xsl = 0; } else { /* No downsampling */ hdown = 0; if (width < dinfo.output_width) xsl = (dinfo.output_width - width) / 2; else xsl = 0; } /* Make xsl even, calculate xsc */ xsl = xsl & ~1; xsc = xsl / 2; yl = yc = 0; for (field = 0; field < numfields; field++) { if (field > 0) { jpeg_read_header(&dinfo, TRUE); dinfo.raw_data_out = TRUE; dinfo.out_color_space = JCS_YCbCr; dinfo.dct_method = JDCT_IFAST; jpeg_start_decompress(&dinfo); } if (numfields == 2) { switch (itype) { case Y4M_ILACE_TOP_FIRST: yl = yc = field; break; case Y4M_ILACE_BOTTOM_FIRST: yl = yc = (1 - field); break; default: fprintf(stderr, "%s: Input is interlaced but no interlacing set", __FUNCTION__); goto ERR_EXIT; } } else { yl = yc = 0; } while (dinfo.output_scanline < dinfo.output_height) { /* read raw data */ jpeg_read_raw_data(&dinfo, scanarray, 8 * vsf[0]); for (y = 0; y < 8 * vsf[0]; yl += numfields, y++) { xd = yl * width; xs = xsl; if (hdown == 0) { for (x = 0; x < width; x++) raw0[xd++] = row0[y][xs++]; } else if (hdown == 1) { for (x = 0; x < width; x++, xs += 2) raw0[xd++] = (row0[y][xs] + row0[y][xs + 1]) >> 1; } else { for (x = 0; x < width / 2; x++, xd += 2, xs += 3) { raw0[xd] = (2 * row0[y][xs] + row0[y][xs + 1]) / 3; raw0[xd + 1] = (2 * row0[y][xs + 2] + row0[y][xs + 1]) / 3; } } } /* Horizontal downsampling of chroma */ for (y = 0; y < 8; y++) { xs = xsc; if (hsf[0] == 1) for (x = 0; x < width / 2; x++, xs++) { row1[y][xs] = (row1_444[y][2 * x] + row1_444[y][2 * x + 1]) >> 1; row2[y][xs] = (row2_444[y][2 * x] + row2_444[y][2 * x + 1]) >> 1; } xs = xsc; if (hdown == 0) { for (x = 0; x < width / 2; x++, xs++) { chr1[y][x] = row1[y][xs]; chr2[y][x] = row2[y][xs]; } } else if (hdown == 1) { for (x = 0; x < width / 2; x++, xs += 2) { chr1[y][x] = (row1[y][xs] + row1[y][xs + 1]) >> 1; chr2[y][x] = (row2[y][xs] + row2[y][xs + 1]) >> 1; } } else { for (x = 0; x < width / 2; x += 2, xs += 3) { chr1[y][x] = (2 * row1[y][xs] + row1[y][xs + 1]) / 3; chr1[y][x + 1] = (2 * row1[y][xs + 2] + row1[y][xs + 1]) / 3; chr2[y][x] = (2 * row2[y][xs] + row2[y][xs + 1]) / 3; chr2[y][x + 1] = (2 * row2[y][xs + 2] + row2[y][xs + 1]) / 3; } } } /* Vertical resampling of chroma */ switch (ctype) { case Y4M_CHROMA_422: if (vsf[0] == 1) { /* Just copy */ for (y = 0; y < 8 /*&& yc < height */; y++, yc += numfields) { xd = yc * width / 2; for (x = 0; x < width / 2; x++, xd++) { raw1[xd] = chr1[y][x]; raw2[xd] = chr2[y][x]; } } } else { /* upsample */ for (y = 0; y < 8 /*&& yc < height */; y++) { xd = yc * width / 2; for (x = 0; x < width / 2; x++, xd++) { raw1[xd] = chr1[y][x]; raw2[xd] = chr2[y][x]; } yc += numfields; xd = yc * width / 2; for (x = 0; x < width / 2; x++, xd++) { raw1[xd] = chr1[y][x]; raw2[xd] = chr2[y][x]; } yc += numfields; } } break; default: /* * should be case Y4M_CHROMA_420JPEG: but use default: for compatibility. Some * pass things like '420' in with the expectation that anything other than * Y4M_CHROMA_422 will default to 420JPEG. */ if (vsf[0] == 1) { /* Really downsample */ for (y = 0; y < 8 /*&& yc < height/2*/; y += 2, yc += numfields) { xd = yc * width / 2; for (x = 0; x < width / 2; x++, xd++) { assert(xd < (width * height / 4)); raw1[xd] = (chr1[y][x] + chr1[y + 1][x]) >> 1; raw2[xd] = (chr2[y][x] + chr2[y + 1][x]) >> 1; } } } else { /* Just copy */ for (y = 0; y < 8 /*&& yc < height/2 */; y++, yc += numfields) { xd = yc * width / 2; for (x = 0; x < width / 2; x++, xd++) { raw1[xd] = chr1[y][x]; raw2[xd] = chr2[y][x]; } } } break; } } (void)jpeg_finish_decompress(&dinfo); if (field == 0 && numfields > 1) jpeg_skip_ff(&dinfo); } if (hsf[0] == 1) { for (y = 0; y < 16; y++) // allocate a special buffer for the extra sampling depth { free(row1_444[y]); free(row2_444[y]); } } jpeg_destroy_decompress(&dinfo); if (jerr.warning_seen) return 1; else return 0; ERR_EXIT: jpeg_destroy_decompress(&dinfo); return -1; } /* * jpeg_data: Buffer with jpeg data to decode, must be grayscale mode * len: Length of buffer * itype: 0: Not interlaced * 1: Interlaced, Top field first * 2: Interlaced, Bottom field first * ctype Chroma format for decompression. * Currently only Y4M_CHROMA_{420JPEG,422} are available */ int decode_jpeg_gray_raw(unsigned char *jpeg_data, int len, int itype, int ctype, unsigned int width, unsigned int height, unsigned char *raw0, unsigned char *raw1, unsigned char *raw2) { int numfields, field, yl, yc, xsl, xsc, xs, xd, hdown; unsigned int x, y, vsf[3]; JSAMPROW row0[16] = { buf0[0], buf0[1], buf0[2], buf0[3], buf0[4], buf0[5], buf0[6], buf0[7], buf0[8], buf0[9], buf0[10], buf0[11], buf0[12], buf0[13], buf0[14], buf0[15] }; JSAMPARRAY scanarray[3] = { row0 }; struct jpeg_decompress_struct dinfo; struct my_error_mgr jerr; /* We set up the normal JPEG error routines, then override error_exit. */ dinfo.err = jpeg_std_error(&jerr.pub); jerr.pub.error_exit = my_error_exit; /* Establish the setjmp return context for my_error_exit to use. */ if (setjmp(jerr.setjmp_buffer)) { /* If we get here, the JPEG code has signaled an error. */ jpeg_destroy_decompress(&dinfo); return -1; } jpeg_create_decompress(&dinfo); jpeg_buffer_src(&dinfo, jpeg_data, len); /* Read header, make some checks and try to figure out what the user really wants */ jpeg_read_header(&dinfo, TRUE); dinfo.raw_data_out = TRUE; dinfo.out_color_space = JCS_GRAYSCALE; dinfo.dct_method = JDCT_IFAST; if (dinfo.jpeg_color_space != JCS_GRAYSCALE) { fprintf(stderr, "%s: Expected grayscale colorspace for JPEG raw decoding", __FUNCTION__); goto ERR_EXIT; } guarantee_huff_tables(&dinfo); jpeg_start_decompress(&dinfo); // hsf[0] = 1; // hsf[1] = 1; // hsf[2] = 1; vsf[0] = 1; vsf[1] = 1; vsf[2] = 1; /* Height match image height or be exact twice the image height */ if (dinfo.output_height == height) { numfields = 1; } else if (2 * dinfo.output_height == height) { numfields = 2; } else { fprintf(stderr, "%s: Read JPEG: requested height = %d, " "height of image = %d", __FUNCTION__, height, dinfo.output_height); goto ERR_EXIT; } /* Width is more flexible */ if (dinfo.output_width > MAX_LUMA_WIDTH) { fprintf(stderr, "%s: Image width of %d exceeds max", __FUNCTION__, dinfo.output_width); goto ERR_EXIT; } if (width < 2 * dinfo.output_width / 3) { /* Downsample 2:1 */ hdown = 1; if (2 * width < dinfo.output_width) xsl = (dinfo.output_width - 2 * width) / 2; else xsl = 0; } else if (width == 2 * dinfo.output_width / 3) { /* special case of 3:2 downsampling */ hdown = 2; xsl = 0; } else { /* No downsampling */ hdown = 0; if (width < dinfo.output_width) xsl = (dinfo.output_width - width) / 2; else xsl = 0; } /* Make xsl even, calculate xsc */ xsl = xsl & ~1; xsc = xsl / 2; yl = yc = 0; for (field = 0; field < numfields; field++) { if (field > 0) { jpeg_read_header(&dinfo, TRUE); dinfo.raw_data_out = TRUE; dinfo.out_color_space = JCS_GRAYSCALE; dinfo.dct_method = JDCT_IFAST; jpeg_start_decompress(&dinfo); } if (numfields == 2) { switch (itype) { case Y4M_ILACE_TOP_FIRST: yl = yc = field; break; case Y4M_ILACE_BOTTOM_FIRST: yl = yc = (1 - field); break; default: fprintf(stderr, "%s: Input is interlaced but no interlacing set", __FUNCTION__); goto ERR_EXIT; } } else { yl = yc = 0; } while (dinfo.output_scanline < dinfo.output_height) { jpeg_read_raw_data(&dinfo, scanarray, 16); for (y = 0; y < 8 * vsf[0]; yl += numfields, y++) { xd = yl * width; xs = xsl; if (hdown == 0) // no horiz downsampling { for (x = 0; x < width; x++) raw0[xd++] = row0[y][xs++]; } else if (hdown == 1) // half the res { for (x = 0; x < width; x++, xs += 2) raw0[xd++] = (row0[y][xs] + row0[y][xs + 1]) >> 1; } else // 2:3 downsampling { for (x = 0; x < width / 2; x++, xd += 2, xs += 3) { raw0[xd] = (2 * row0[y][xs] + row0[y][xs + 1]) / 3; raw0[xd + 1] = (2 * row0[y][xs + 2] + row0[y][xs + 1]) / 3; } } } for (y = 0; y < 8; y++) { xs = xsc; if (hdown == 0) { for (x = 0; x < width / 2; x++, xs++) { chr1[y][x] = 0; //row1[y][xs]; chr2[y][x] = 0; //row2[y][xs]; } } else if (hdown == 1) { for (x = 0; x < width / 2; x++, xs += 2) { chr1[y][x] = 0; //(row1[y][xs] + row1[y][xs + 1]) >> 1; chr2[y][x] = 0; //(row2[y][xs] + row2[y][xs + 1]) >> 1; } } else { for (x = 0; x < width / 2; x += 2, xs += 3) { chr1[y][x] = 0; //(2 * row1[y][xs] + row1[y][xs + 1]) / 3; chr1[y][x + 1] = 0; //(2 * row1[y][xs + 2] + row1[y][xs + 1]) / 3; chr2[y][x] = 0; // (2 * row2[y][xs] + row2[y][xs + 1]) / 3; chr2[y][x + 1] = 0; //(2 * row2[y][xs + 2] + row2[y][xs + 1]) / 3; } } } switch (ctype) { case Y4M_CHROMA_422: if (vsf[0] == 1) { /* Just copy */ for (y = 0; y < 8 /*&& yc < height */; y++, yc += numfields) { xd = yc * width / 2; for (x = 0; x < width / 2; x++, xd++) { raw1[xd] = 127; //chr1[y][x]; raw2[xd] = 127; //chr2[y][x]; } } } else { /* upsample */ for (y = 0; y < 8 /*&& yc < height */; y++) { xd = yc * width / 2; for (x = 0; x < width / 2; x++, xd++) { raw1[xd] = 127; //chr1[y][x]; raw2[xd] = 127; //chr2[y][x]; } yc += numfields; xd = yc * width / 2; for (x = 0; x < width / 2; x++, xd++) { raw1[xd] = 127; //chr1[y][x]; raw2[xd] = 127; //chr2[y][x]; } yc += numfields; } } break; /* * should be case Y4M_CHROMA_420JPEG: but use default: for compatibility. Some * pass things like '420' in with the expectation that anything other than * Y4M_CHROMA_422 will default to 420JPEG. */ default: if (vsf[0] == 1) { /* Really downsample */ for (y = 0; y < 8; y += 2, yc += numfields) { xd = yc * width / 2; for (x = 0; x < width / 2; x++, xd++) { raw1[xd] = 127; //(chr1[y][x] + chr1[y + 1][x]) >> 1; raw2[xd] = 127; //(chr2[y][x] + chr2[y + 1][x]) >> 1; } } } else { /* Just copy */ for (y = 0; y < 8; y++, yc += numfields) { xd = yc * width / 2; for (x = 0; x < width / 2; x++, xd++) { raw1[xd] = 127; //chr1[y][x]; raw2[xd] = 127; //chr2[y][x]; } } } break; } } (void)jpeg_finish_decompress(&dinfo); if (field == 0 && numfields > 1) jpeg_skip_ff(&dinfo); } jpeg_destroy_decompress(&dinfo); return 0; ERR_EXIT: jpeg_destroy_decompress(&dinfo); return -1; } /******************************************************************* * * * encode_jpeg_data: Compress raw YCbCr data (output JPEG * * may be interlaced * * * *******************************************************************/ /* * jpeg_data: Buffer to hold output jpeg * len: Length of buffer * itype: 0: Not interlaced * 1: Interlaced, Top field first * 2: Interlaced, Bottom field first * ctype Chroma format for decompression. * Currently only Y4M_CHROMA_{420JPEG,422} are available */ int encode_jpeg_raw(unsigned char *jpeg_data, int len, int quality, int itype, int ctype, unsigned int width, unsigned int height, unsigned char *raw0, unsigned char *raw1, unsigned char *raw2) { int numfields, field, yl, yc, y, i; JSAMPROW row0[16] = { buf0[0], buf0[1], buf0[2], buf0[3], buf0[4], buf0[5], buf0[6], buf0[7], buf0[8], buf0[9], buf0[10], buf0[11], buf0[12], buf0[13], buf0[14], buf0[15] }; JSAMPROW row1[8] = { buf1[0], buf1[1], buf1[2], buf1[3], buf1[4], buf1[5], buf1[6], buf1[7] }; JSAMPROW row2[8] = { buf2[0], buf2[1], buf2[2], buf2[3], buf2[4], buf2[5], buf2[6], buf2[7] }; JSAMPARRAY scanarray[3] = { row0, row1, row2 }; struct jpeg_compress_struct cinfo; struct my_error_mgr jerr; /* We set up the normal JPEG error routines, then override error_exit. */ cinfo.err = jpeg_std_error(&jerr.pub); jerr.pub.error_exit = my_error_exit; /* Establish the setjmp return context for my_error_exit to use. */ if (setjmp(jerr.setjmp_buffer)) { /* If we get here, the JPEG code has signaled an error. */ jpeg_destroy_compress(&cinfo); return -1; } jpeg_create_compress(&cinfo); jpeg_buffer_dest(&cinfo, jpeg_data, len); /* Set some jpeg header fields */ cinfo.input_components = 3; jpeg_set_defaults(&cinfo); jpeg_set_quality(&cinfo, quality, FALSE); cinfo.raw_data_in = TRUE; cinfo.in_color_space = JCS_YCbCr; cinfo.dct_method = JDCT_IFAST; cinfo.input_gamma = 1.0; cinfo.comp_info[0].h_samp_factor = 2; cinfo.comp_info[0].v_samp_factor = 1; /*1||2 */ cinfo.comp_info[1].h_samp_factor = 1; cinfo.comp_info[1].v_samp_factor = 1; cinfo.comp_info[2].h_samp_factor = 1; /*1||2 */ cinfo.comp_info[2].v_samp_factor = 1; if ((width > 4096) || (height > 4096)) { fprintf(stderr, "%s: Image dimensions (%dx%d) exceed lavtools' max " "(4096x4096)", __FUNCTION__, width, height); goto ERR_EXIT; } if ((width % 16) || (height % 16)) { fprintf(stderr, "%s: Image dimensions (%dx%d) not multiples of 16", __FUNCTION__, width, height); goto ERR_EXIT; } cinfo.image_width = width; switch (itype) { case Y4M_ILACE_TOP_FIRST: case Y4M_ILACE_BOTTOM_FIRST: /* interlaced */ numfields = 2; break; default: numfields = 1; if (height > 2048) { fprintf(stderr, "%s: Image height (%d) exceeds lavtools max " "for non-interlaced frames", __FUNCTION__, height); goto ERR_EXIT; } } cinfo.image_height = height / numfields; yl = yc = 0; /* y luma, chroma */ for (field = 0; field < numfields; field++) { jpeg_start_compress(&cinfo, FALSE); if (numfields == 2) { static const JOCTET marker0[40]; jpeg_write_marker(&cinfo, JPEG_APP0, marker0, 14); jpeg_write_marker(&cinfo, JPEG_APP0 + 1, marker0, 40); switch (itype) { case Y4M_ILACE_TOP_FIRST: /* top field first */ yl = yc = field; break; case Y4M_ILACE_BOTTOM_FIRST: /* bottom field first */ yl = yc = (1 - field); break; default: fprintf(stderr, "%s: Input is interlaced but no interlacing set", __FUNCTION__); goto ERR_EXIT; } } else { yl = yc = 0; } while (cinfo.next_scanline < cinfo.image_height) { for (y = 0; y < 8 * cinfo.comp_info[0].v_samp_factor; yl += numfields, y++) { row0[y] = &raw0[yl * width]; } for (y = 0; y < 8; y++) { row1[y] = &raw1[yc * width / 2]; row2[y] = &raw2[yc * width / 2]; if ((ctype == Y4M_CHROMA_422) || (y % 2)) yc += numfields; } jpeg_write_raw_data(&cinfo, scanarray, 8 * cinfo.comp_info[0].v_samp_factor); } (void)jpeg_finish_compress(&cinfo); } /* FIXME */ i = len - cinfo.dest->free_in_buffer; jpeg_destroy_compress(&cinfo); return i; /* size of jpeg */ ERR_EXIT: jpeg_destroy_compress(&cinfo); return -1; } int decode_jpeg_rgb(unsigned char *inBuffer, unsigned long inSize, uint8_t **memptr, size_t *memsize, int *naxis, int *w, int *h) { /* these are standard libjpeg structures for reading(decompression) */ struct jpeg_decompress_struct cinfo; struct jpeg_error_mgr jerr; /* libjpeg data structure for storing one row, that is, scanline of an image */ JSAMPROW row_pointer[1] = { NULL }; /* here we set up the standard libjpeg error handler */ cinfo.err = jpeg_std_error(&jerr); /* setup decompression process and source, then read JPEG header */ jpeg_create_decompress(&cinfo); /* this makes the library read from infile */ jpeg_mem_src(&cinfo, inBuffer, inSize); /* reading the image header which contains image information */ jpeg_read_header(&cinfo, (boolean)TRUE); /* Start decompression jpeg here */ jpeg_start_decompress(&cinfo); *memsize = cinfo.output_width * cinfo.output_height * cinfo.num_components; *memptr = (uint8_t *)realloc(*memptr, *memsize); uint8_t *destmem = *memptr; *naxis = cinfo.num_components; *w = cinfo.output_width; *h = cinfo.output_height; /* now actually read the jpeg into the raw buffer */ row_pointer[0] = (unsigned char *)malloc(cinfo.output_width * cinfo.num_components); /* read one scan line at a time */ for (unsigned int row = 0; row < cinfo.image_height; row++) { unsigned char *ppm8 = row_pointer[0]; jpeg_read_scanlines(&cinfo, row_pointer, 1); memcpy(destmem, ppm8, cinfo.output_width * cinfo.num_components); destmem += cinfo.output_width * cinfo.num_components; } /* wrap up decompression, destroy objects, free pointers and close open files */ jpeg_finish_decompress(&cinfo); jpeg_destroy_decompress(&cinfo); if (row_pointer[0]) free(row_pointer[0]); return 0; } libindi/libs/stream/jpegutils.h0000664000175000017500000000727013263645557016103 0ustar jasemjasem/* * jpegutils.h: Some Utility programs for dealing with * JPEG encoded images * * Copyright (C) 1999 Rainer Johanni * Copyright (C) 2001 pHilipp Zabel * Copyright (C) 2008 Angel Carpintero * */ #pragma once #include #include #include /** * \defgroup jpegSpace Functions to encode and decode JPEG jpeg_data: buffer with input / output jpeg\n len: Length of jpeg buffer\n itype: Y4M_ILACE_NONE: Not interlaced\n Y4M_ILACE_TOP_FIRST: Interlaced, top-field-first\n Y4M_ILACE_BOTTOM_FIRST: Interlaced, bottom-field-first\n ctype Chroma format for decompression.\n Currently always 420 and hence ignored.\n raw0 buffer with input / output raw Y channel\n raw1 buffer with input / output raw U/Cb channel\n raw2 buffer with input / output raw V/Cr channel\n width width of Y channel (width of U/V is width/2)\n height height of Y channel (height of U/V is height/2)\n */ /*@{*/ #define Y4M_ILACE_NONE 0 /** non-interlaced, progressive frame */ #define Y4M_ILACE_TOP_FIRST 1 /** interlaced, top-field first */ #define Y4M_ILACE_BOTTOM_FIRST 2 /** interlaced, bottom-field first */ #define Y4M_ILACE_MIXED 3 /** mixed, "refer to frame header" */ #define Y4M_CHROMA_420JPEG 0 /** 4:2:0, H/V centered, for JPEG/MPEG-1 */ #define Y4M_CHROMA_420MPEG2 1 /** 4:2:0, H cosited, for MPEG-2 */ #define Y4M_CHROMA_420PALDV 2 /** 4:2:0, alternating Cb/Cr, for PAL-DV */ #define Y4M_CHROMA_444 3 /** 4:4:4, no subsampling, phew. */ #define Y4M_CHROMA_422 4 /** 4:2:2, H cosited */ #define Y4M_CHROMA_411 5 /** 4:1:1, H cosited */ #define Y4M_CHROMA_MONO 6 /** luma plane only */ #define Y4M_CHROMA_444ALPHA 7 /** 4:4:4 with an alpha channel */ #ifdef __cplusplus extern "C" { #endif /** * @short decode JPEG buffer */ int decode_jpeg_raw(unsigned char *jpeg_data, int len, int itype, int ctype, unsigned int width, unsigned int height, unsigned char *raw0, unsigned char *raw1, unsigned char *raw2); /** * @brief decode_jpeg_rgb Read jpeg in memory buffer and produce RGB image * @param inBuffer pointer to jpeg file in memory * @param inSize file of jpeg file in bytes * @param memptr pointer to store RGB data. To enhance performance, the memory must be allocated at least byte. memptr = malloc(1) since subsequent calls * will use realloc to allocate memory. The caller is responsible for free(*memptr) eventually. * @param memsize size of RGB data as determined after jpeg decompression * @param naxis 1 for mono, 3 for color * @param w width of image in pixels * @param h height image in pixels * @return 0 if decoding sucseeds, -1 otherwise. */ int decode_jpeg_rgb(unsigned char *inBuffer, unsigned long inSize, uint8_t **memptr, size_t *memsize, int *naxis, int *w, int *h); /** * @short decode JPEG raw gray buffer */ int decode_jpeg_gray_raw(unsigned char *jpeg_data, int len, int itype, int ctype, unsigned int width, unsigned int height, unsigned char *raw0, unsigned char *raw1, unsigned char *raw2); /** * @short encode raw JPEG buffer */ int encode_jpeg_raw(unsigned char *jpeg_data, int len, int quality, int itype, int ctype, unsigned int width, unsigned int height, unsigned char *raw0, unsigned char *raw1, unsigned char *raw2); #ifdef __cplusplus } #endif /*@}*/ libindi/libs/stream/streammanager.h0000664000175000017500000002153713263645557016725 0ustar jasemjasem/* Copyright (C) 2015 by Jasem Mutlaq Copyright (C) 2014 by geehalel Stream Recorder 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 */ #pragma once #include "indidevapi.h" #include "recorder/recordermanager.h" #include "encoder/encodermanager.h" #include #include #include #include /** * \class StreamManager \brief Class to provide video streaming and recording functionality. INDI::CCD can utilize this class to add streaming and recording functionality to the driver. Transfer of the video stream is done via the same BLOB property \e CCD1 used for transfer of image data to the client. Therefore, it is not possible to transmit image data and video stream at the same time. Two formats are accepted for video streaming: + Grayscale 8bit frame that represents intensity/lumienance. + Color 24bit RGB frame. Use setPixelFormat() and setSize() before uploading the stream data. 16bit frames are only supported in some recorders. You can send 16bit frames, but they will be downscaled to 8bit when necessary for streaming and recording purposes. Base classes must implement startStreaming() and stopStreaming() functions. When a frame is ready, use uploadStream() to send the data to active encoders and recorders. It is highly recommended to implement the streaming functionality in a dedicated thread. \section Encoders Encoders are responsible for encoding the frame and transmitting it to the client. The CCD1 BLOB format is set to the desired format. Default encoding format is RAW (format = ".stream"). Currently, two encoders are supported: 1. RAW Encoder: Frame is sent as is (lossless). If compression is enabled, the frame is compressed with zlib. Uncompressed format is ".stream" and compressed format is ".stream.z" 2. MJPEG Encoder: Frame is encoded to a JPEG image before being transmitted. Format is ".stream_jpg" \section Recorders Recorders are responsible for recording the video stream to a file. The recording file directory and name can be set via the RECORD_FILE property which is composed of RECORD_FILE_DIR and RECORD_FILE_NAME elements. You can specify a record directory name together with a file name. You may use special character sequences to generate dynamic names: * _D_ is replaced with the date ('YYYY-MM-DD') * _H_ is replaced with the time ('hh-mm-ss') * _T_ is replaced with a timestamp * _F_ is replaced with the filter name currently in use (see Snoop Devices in Options tab) Currently, two recorders are supported: 1. SER recorder: Saves video streams along with timestamps in SER format. 2. OGV recorder: Saves video streams in libtheora OGV files. INDI must be compiled with the optional OGG Theora support for this functionality to be available. Frame rate is estimated from the average FPS. \section Subframing By default, the full image width and height are used for transmitting the data. Subframing is possible by updating the CCD_STREAM_FRAME property. All values set in this property must be set in BINNED coordinates, unlike the CCD_FRAME which is set in UNBINNED coordinates. \example Check CCD Simulator, V4L2 CCD, and ZWO ASI drivers for example implementations. \author Jasem Mutlaq \author Jean-Luc Geehalel */ namespace INDI { class CCD; class StreamManager { public: enum { RECORD_ON, RECORD_TIME, RECORD_FRAME, RECORD_OFF }; StreamManager(CCD *mainCCD); virtual ~StreamManager(); virtual void ISGetProperties(const char *dev); virtual bool ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n); virtual bool ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n); virtual bool ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n); virtual bool initProperties(); virtual bool updateProperties(); virtual bool saveConfigItems(FILE *fp); /** * @brief newFrame CCD drivers call this function when a new frame is received. It is then streamed, or recorded, or both according to the settings in the streamer. */ void newFrame(const uint8_t *buffer, uint32_t nbytes); /** * @brief setStream Enables (starts) or disables (stops) streaming. * @param enable True to enable, false to disable * @return True if operation is successful, false otherwise. */ bool setStream(bool enable); RecorderInterface *getRecorder() { return recorder; } bool isDirectRecording() { return direct_record; } bool isStreaming() { return m_isStreaming; } bool isRecording() { return m_isRecording; } bool isBusy() { return (isStreaming() || isRecording()); } uint8_t getTargetFPS() { return static_cast(StreamOptionsN[OPTION_TARGET_FPS].value); } uint8_t *getDownscaleBuffer() { return downscaleBuffer; } uint32_t getDownscaleBufferSize() { return downscaleBufferSize; } const char *getDeviceName(); void setSize(uint16_t width, uint16_t height); bool setPixelFormat(INDI_PIXEL_FORMAT pixelFormat, uint8_t pixelDepth=8); void getStreamFrame(uint16_t *x, uint16_t *y, uint16_t *w, uint16_t *h); bool close(); protected: CCD *currentCCD = nullptr; private: /* Utility for record file */ int mkpath(std::string s, mode_t mode); std::string expand(std::string fname, const std::map &patterns); bool startRecording(); bool stopRecording(); /** * @brief uploadStream Upload frame to client using the selected encoder * @param buffer pointer to frame image buffer * @param nbytes size of frame in bytes * @return True if frame is encoded and sent to client, false otherwise. */ bool uploadStream(const uint8_t *buffer, uint32_t nbytes); /** * @brief recordStream Calls the backend recorder to record a single frame. * @param deltams time in milliseconds since last frame */ bool recordStream(const uint8_t *buffer, uint32_t nbytes, double deltams); /* Stream switch */ ISwitch StreamS[2]; ISwitchVectorProperty StreamSP; /* Record switch */ ISwitch RecordStreamS[4]; ISwitchVectorProperty RecordStreamSP; /* Record File Info */ IText RecordFileT[2] {}; ITextVectorProperty RecordFileTP; /* Streaming Options */ INumber StreamOptionsN[2]; INumberVectorProperty StreamOptionsNP; enum { OPTION_TARGET_FPS, OPTION_RATE_DIVISOR}; /* Measured FPS */ INumber FpsN[2]; INumberVectorProperty FpsNP; enum { FPS_INSTANT, FPS_AVERAGE }; /* Record Options */ INumber RecordOptionsN[2]; INumberVectorProperty RecordOptionsNP; // Stream Frame INumberVectorProperty StreamFrameNP; INumber StreamFrameN[4]; /* BLOBs */ IBLOBVectorProperty *imageBP; IBLOB *imageB; // Encoder Selector. It's static now but should this implemented as plugin interface? ISwitch EncoderS[2]; ISwitchVectorProperty EncoderSP; enum { ENCODER_RAW, ENCODER_MJPEG }; // Recorder Selector. Static but should be implmeneted as a dynamic plugin interface ISwitch RecorderS[2]; ISwitchVectorProperty RecorderSP; enum { RECORDER_RAW, RECORDER_OGV }; bool m_isStreaming; bool m_isRecording; int streamframeCount; int recordframeCount; double recordDuration; // Recorder RecorderManager *recorderManager = nullptr; RecorderInterface *recorder = nullptr; bool direct_record; std::string recordfiledir, recordfilename; /* in case we should move it */ // Encoders EncoderManager *encoderManager = nullptr; EncoderInterface *encoder = nullptr; // Measure FPS // timer_t fpstimer; // struct itimerspec tframe1, tframe2; // use bsd timers struct itimerval tframe1, tframe2; double mssum, framecountsec; INDI_PIXEL_FORMAT m_PixelFormat; uint8_t m_PixelDepth; uint16_t rawWidth=0, rawHeight=0; // Downscale buffer for streaming uint8_t *downscaleBuffer = nullptr; uint32_t downscaleBufferSize=0; }; } libindi/libs/stream/encoder/0000775000175000017500000000000013263645557015335 5ustar jasemjasemlibindi/libs/stream/encoder/rawencoder.cpp0000664000175000017500000000432413263645557020175 0ustar jasemjasem/* Copyright (C) 2017 by Jasem Mutlaq INDI Raw Encoder 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 */ #include "rawencoder.h" #include "stream/streammanager.h" #include "indiccd.h" #include namespace INDI { RawEncoder::RawEncoder() { name = "RAW"; compressedFrame = (uint8_t *)malloc(1); } RawEncoder::~RawEncoder() { free(compressedFrame); } const char *RawEncoder::getDeviceName() { return currentCCD->getDeviceName(); } bool RawEncoder::upload(IBLOB *bp, const uint8_t *buffer, uint32_t nbytes, bool isCompressed) { // Do we want to compress ? if (isCompressed) { /* Compress frame */ compressedFrame = (uint8_t *)realloc(compressedFrame, sizeof(uint8_t) * nbytes + nbytes / 64 + 16 + 3); uLongf compressedBytes = sizeof(uint8_t) * nbytes + nbytes / 64 + 16 + 3; int ret = compress2(compressedFrame, &compressedBytes, buffer, nbytes, 4); if (ret != Z_OK) { /* this should NEVER happen */ LOGF_ERROR("internal error - compression failed: %d", ret); return false; } // Send it compressed bp->blob = compressedFrame; bp->bloblen = compressedBytes; bp->size = nbytes; strcpy(bp->format, ".stream.z"); } else { // Send it uncompressed bp->blob = (const_cast(buffer)); bp->bloblen = nbytes; bp->size = nbytes; strcpy(bp->format, ".stream"); } return true; } } libindi/libs/stream/encoder/encodermanager.cpp0000664000175000017500000000323213263645557021013 0ustar jasemjasem/* Copyright (C) 2017 by Jasem Mutlaq Encoder Manager 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 */ #include "encodermanager.h" #include "rawencoder.h" #include "mjpegencoder.h" namespace INDI { EncoderManager::EncoderManager() { encoder_list.push_back(new RawEncoder()); encoder_list.push_back(new MJPEGEncoder()); default_encoder = encoder_list.at(0); } EncoderManager::~EncoderManager() { std::vector::iterator it; for (it = encoder_list.begin(); it != encoder_list.end(); it++) { delete (*it); } encoder_list.clear(); } std::vector EncoderManager::getEncoderList() { return encoder_list; } EncoderInterface *EncoderManager::getEncoder() { return current_encoder; } EncoderInterface *EncoderManager::getDefaultEncoder() { return default_encoder; } void EncoderManager::setEncoder(EncoderInterface *encoder) { current_encoder = encoder; } } libindi/libs/stream/encoder/rawencoder.h0000664000175000017500000000247113263645557017643 0ustar jasemjasem/* Copyright (C) 2017 by Jasem Mutlaq Encoder Interface 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 */ #pragma once #include "encoderinterface.h" namespace INDI { /** * @brief The RawEncoder class sends the image as-is (lossless) to the client. * * It supports compression via zlib (.stream.z) */ class RawEncoder : public EncoderInterface { public: RawEncoder(); ~RawEncoder(); virtual bool upload(IBLOB *bp, const uint8_t *buffer, uint32_t nbytes, bool isCompressed=false) override; private: const char *getDeviceName(); uint8_t *compressedFrame = nullptr; }; } libindi/libs/stream/encoder/encoderinterface.h0000664000175000017500000000336613263645557021016 0ustar jasemjasem/* Copyright (C) 2017 by Jasem Mutlaq Encoder Interface 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 */ #pragma once #include "indidevapi.h" #include "indibasetypes.h" #include #include #include #include namespace INDI { class CCD; /** * @brief The EncoderInterface class is the base class for video streaming encoders. */ class EncoderInterface { public: EncoderInterface() = default; virtual ~EncoderInterface() = default; virtual void init(CCD *activeCCD); virtual bool setPixelFormat(INDI_PIXEL_FORMAT pixelFormat, uint8_t pixelDepth); virtual bool setSize(uint16_t width, uint16_t height); virtual bool upload(IBLOB *bp, const uint8_t *buffer, uint32_t nbytes, bool isCompressed=false) = 0; const char *getName(); protected: const char *name; CCD *currentCCD = nullptr; INDI_PIXEL_FORMAT pixelFormat; // INDI Pixel Format uint8_t pixelDepth = 8; // Bits per Pixels uint16_t rawWidth, rawHeight; }; } libindi/libs/stream/encoder/mjpegencoder.cpp0000664000175000017500000001252713263645557020512 0ustar jasemjasem/* Copyright (C) 2017 by Jasem Mutlaq INDI Raw Encoder 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 */ #include "mjpegencoder.h" #include "stream/streammanager.h" #include "indiccd.h" #include #include #include static void init_destination(j_compress_ptr cinfo) { INDI_UNUSED(cinfo); /* No work necessary here */ } static boolean empty_output_buffer(j_compress_ptr cinfo) { INDI_UNUSED(cinfo); /* No work necessary here */ return TRUE; } static void term_destination(j_compress_ptr cinfo) { INDI_UNUSED(cinfo); /* no work necessary here */ } namespace INDI { MJPEGEncoder::MJPEGEncoder() { name = "MJPEG"; } MJPEGEncoder::~MJPEGEncoder() { delete [] jpegBuffer; } const char *MJPEGEncoder::getDeviceName() { return currentCCD->getDeviceName(); } bool MJPEGEncoder::upload(IBLOB *bp, const uint8_t *buffer, uint32_t nbytes, bool isCompressed) { // We do not support compression if (isCompressed) { LOG_ERROR("Compression is not supported in MJPEG stream."); return false; } INDI_UNUSED(nbytes); int bufsize = rawWidth * rawHeight * ((pixelFormat == INDI_RGB) ? 3 : 1); if (bufsize != jpegBufferSize) { delete [] jpegBuffer; jpegBuffer = new uint8_t[bufsize]; jpegBufferSize = bufsize; } if (pixelFormat == INDI_RGB) jpeg_compress_8u_rgb(buffer, rawWidth, rawHeight, rawWidth*3, jpegBuffer, &bufsize, 70); else jpeg_compress_8u_gray(buffer, rawWidth, rawHeight, rawWidth, jpegBuffer, &bufsize, 70); bp->blob = jpegBuffer; bp->bloblen = bufsize; bp->size = bufsize; strcpy(bp->format, ".stream_jpg"); return true; } /* FROM: https://svn.csail.mit.edu/rrg_pods/jpeg-utils/ Name: jpeg-utils Maintainers: Albert Huang Summary: Wrapper functions around libjpeg to simplify JPEG compression and decompression with in-memory buffers. Description: note: This is a simple enough library that you could just copy the .c and .h files in to your own programs. To use the library, link against -ljpeg-utils, or use pkg-config --cflags --libs jpeg-utils Requirements: libjpeg62 For faster performance, install libjpeg-turbo, an SSE-accelerated library that is ABI compatible with libjpeg62. */ int MJPEGEncoder::jpeg_compress_8u_gray (const uint8_t * src, uint16_t width, uint16_t height, int stride, uint8_t * dest, int * destsize, int quality) { struct jpeg_compress_struct cinfo; struct jpeg_error_mgr jerr; struct jpeg_destination_mgr jdest; int out_size = *destsize; cinfo.err = jpeg_std_error (&jerr); jpeg_create_compress (&cinfo); jdest.next_output_byte = dest; jdest.free_in_buffer = out_size; jdest.init_destination = init_destination; jdest.empty_output_buffer = empty_output_buffer; jdest.term_destination = term_destination; cinfo.dest = &jdest; cinfo.image_width = width; cinfo.image_height = height; cinfo.input_components = 1; cinfo.in_color_space = JCS_GRAYSCALE; jpeg_set_defaults (&cinfo); jpeg_set_quality (&cinfo, quality, TRUE); jpeg_start_compress (&cinfo, TRUE); while (cinfo.next_scanline < height) { JSAMPROW row = (JSAMPROW)(src + cinfo.next_scanline * stride); jpeg_write_scanlines (&cinfo, &row, 1); } jpeg_finish_compress (&cinfo); *destsize = out_size - jdest.free_in_buffer; jpeg_destroy_compress (&cinfo); return 0; } int MJPEGEncoder::jpeg_compress_8u_rgb (const uint8_t * src, uint16_t width, uint16_t height, int stride, uint8_t * dest, int * destsize, int quality) { struct jpeg_compress_struct cinfo; struct jpeg_error_mgr jerr; struct jpeg_destination_mgr jdest; int out_size = *destsize; cinfo.err = jpeg_std_error (&jerr); jpeg_create_compress (&cinfo); jdest.next_output_byte = dest; jdest.free_in_buffer = out_size; jdest.init_destination = init_destination; jdest.empty_output_buffer = empty_output_buffer; jdest.term_destination = term_destination; cinfo.dest = &jdest; cinfo.image_width = width; cinfo.image_height = height; cinfo.input_components = 3; cinfo.in_color_space = JCS_RGB; jpeg_set_defaults (&cinfo); jpeg_set_quality (&cinfo, quality, TRUE); jpeg_start_compress (&cinfo, TRUE); while (cinfo.next_scanline < height) { JSAMPROW row = (JSAMPROW)(src + cinfo.next_scanline * stride); jpeg_write_scanlines (&cinfo, &row, 1); } jpeg_finish_compress (&cinfo); *destsize = out_size - jdest.free_in_buffer; jpeg_destroy_compress (&cinfo); return 0; } } libindi/libs/stream/encoder/mjpegencoder.h0000664000175000017500000000331713263645557020154 0ustar jasemjasem/* Copyright (C) 2017 by Jasem Mutlaq MJPEG Encoder Interface 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 */ #pragma once #include "encoderinterface.h" namespace INDI { /** * @brief The MJPEGEncoder class encodes frames in JPEG format before transmitting them to the client. * * The quality is now hard-coded at 70 when encoding the JPEG image. Further compression is not supported. */ class MJPEGEncoder : public EncoderInterface { public: MJPEGEncoder(); ~MJPEGEncoder(); virtual bool upload(IBLOB *bp, const uint8_t *buffer, uint32_t nbytes, bool isCompressed=false) override; private: const char *getDeviceName(); int jpeg_compress_8u_gray (const uint8_t * src, uint16_t width, uint16_t height, int stride, uint8_t * dest, int * destsize, int quality); int jpeg_compress_8u_rgb (const uint8_t * src, uint16_t width, uint16_t height, int stride, uint8_t * dest, int * destsize, int quality); uint8_t *jpegBuffer = nullptr; uint16_t jpegBufferSize=1; }; } libindi/libs/stream/encoder/encodermanager.h0000664000175000017500000000261313263645557020462 0ustar jasemjasem/* Copyright (C) 2017 by Jasem Mutlaq Encoder Manager 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 */ #pragma once #include #include "encoderinterface.h" namespace INDI { /** * @brief The EncoderManager class contains a list of active supported encoders. */ class EncoderManager { public: EncoderManager(); ~EncoderManager(); std::vector getEncoderList(); EncoderInterface *getEncoder(); EncoderInterface *getDefaultEncoder(); void setEncoder(EncoderInterface *encoder); protected: std::vector encoder_list; EncoderInterface *current_encoder; EncoderInterface *default_encoder; }; } libindi/libs/stream/encoder/encoderinterface.cpp0000664000175000017500000000304613263645557021344 0ustar jasemjasem/* Copyright (C) 2017 by Jasem Mutlaq Copyright (C) 2014 by geehalel Encoder Interface. Subclass to implement specific recording backend. Current supported encoders: 1. raw INDI encoder (8 bit grayscale and RGB24 color) 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 */ #include "encoderinterface.h" #include "indiccd.h" namespace INDI { const char *EncoderInterface::getName() { return name; } void EncoderInterface::init(CCD *activeCCD) { currentCCD = activeCCD; } bool EncoderInterface::setSize(uint16_t width, uint16_t height) { rawWidth = width; rawHeight = height; return true; } bool EncoderInterface::setPixelFormat(INDI_PIXEL_FORMAT pixelFormat, uint8_t pixelDepth) { this->pixelFormat = pixelFormat; this->pixelDepth = pixelDepth; return true; } } libindi/libs/stream/recorder/0000775000175000017500000000000013263645557015523 5ustar jasemjasemlibindi/libs/stream/recorder/recordermanager.h0000664000175000017500000000273113263645557021037 0ustar jasemjasem/* Copyright (C) 2017 by Jasem Mutlaq Copyright (C) 2014 by geehalel Recorder Manager 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 */ #pragma once #include #include "recorderinterface.h" namespace INDI { /** * @brief The RecorderManager class contains a list of active supported recorders. */ class RecorderManager { public: RecorderManager(); ~RecorderManager(); std::vector getRecorderList(); RecorderInterface *getRecorder(); RecorderInterface *getDefaultRecorder(); void setRecorder(RecorderInterface *recorder); protected: std::vector recorder_list; RecorderInterface *current_recorder; RecorderInterface *default_recorder; }; } libindi/libs/stream/recorder/recorderinterface.cpp0000664000175000017500000000214613263645557021720 0ustar jasemjasem/* Copyright (C) 2017 by Jasem Mutlaq Copyright (C) 2014 by geehalel Recorder Interface. Subclass to implement specific recording backend. Current supported recorders: 1. SER Recorder 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 */ #include "recorderinterface.h" namespace INDI { const char *RecorderInterface::getName() { return name; } } libindi/libs/stream/recorder/serrecorder.h0000664000175000017500000000757213263645557020226 0ustar jasemjasem/* Copyright (C) 2014 by geehalel SER Recorder 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 */ #pragma once #include "recorderinterface.h" #include #include typedef struct ser_header { char FileID[14]; uint32_t LuID; uint32_t ColorID; uint32_t LittleEndian; uint32_t ImageWidth; uint32_t ImageHeight; uint32_t PixelDepth; uint32_t FrameCount; char Observer[40]; char Instrume[40]; char Telescope[40]; uint64_t DateTime; uint64_t DateTime_UTC; } ser_header; enum ser_color_id { SER_MONO = 0, SER_BAYER_RGGB = 8, SER_BAYER_GRBG = 9, SER_BAYER_GBRG = 10, SER_BAYER_BGGR = 11, SER_BAYER_CYYM = 16, SER_BAYER_YCMY = 17, SER_BAYER_YMCY = 18, SER_BAYER_MYYC = 19, SER_RGB = 100, SER_BGR = 101 }; #define SER_BIG_ENDIAN 0 #define SER_LITTLE_ENDIAN 1 namespace INDI { /** * @brief The SER_Recorder class implements recording of video streams in SER format. */ class SER_Recorder : public RecorderInterface { public: SER_Recorder(); virtual ~SER_Recorder(); virtual const char *getExtension() { return ".ser"; } virtual bool setPixelFormat(INDI_PIXEL_FORMAT pixelFormat, uint8_t pixelDepth); virtual bool setSize(uint16_t width, uint16_t height); virtual bool open(const char *filename, char *errmsg); virtual bool close(); virtual bool writeFrame(const uint8_t *frame, uint32_t nbytes); virtual void setStreamEnabled(bool enable) { isStreamingActive = enable; } // Public constants static const uint64_t C_SEPASECONDS_PER_SECOND = 10000000; protected: uint64_t utcTo64BitTS(); bool is_little_endian(); void write_int_le(uint32_t *i); void write_long_int_le(uint64_t *i); void write_header(ser_header *s); ser_header serh; bool isRecordingActive = false, isStreamingActive = false; FILE *f; uint32_t frame_size; uint32_t number_of_planes; uint16_t rawWidth = 0, rawHeight = 0; std::vector frameStamps; private: // From pipp_timestamp.h // Copyright (C) 2015 Chris Garry // Date to MS 64bit timestamp format for SER header void dateTo64BitTS(int32_t year, int32_t month, int32_t day, int32_t hour, int32_t minute, int32_t second, int32_t microsec, uint64_t *p_ts); uint64_t getUTCTimeStamp(); uint64_t getLocalTimeStamp(); // Calculate if a year is a leap yer /// static bool is_leap_year(uint32_t year); // Constants static const uint64_t m_sepaseconds_per_microsecond = 10; static const uint64_t m_septaseconds_per_part_minute = C_SEPASECONDS_PER_SECOND * 6; static const uint64_t m_septaseconds_per_minute = C_SEPASECONDS_PER_SECOND * 60; static const uint64_t m_septaseconds_per_hour = C_SEPASECONDS_PER_SECOND * 60 * 60; static const uint64_t m_septaseconds_per_day = m_septaseconds_per_hour * 24; static const uint32_t m_days_in_400_years = 303 * 365 + 97 * 366; static const uint64_t m_septaseconds_per_400_years = m_days_in_400_years * m_septaseconds_per_day; uint8_t *jpegBuffer=nullptr; INDI_PIXEL_FORMAT m_PixelFormat; }; } libindi/libs/stream/recorder/recordermanager.cpp0000664000175000017500000000351313263645557021371 0ustar jasemjasem/* Copyright (C) 2017 by Jasem Mutlaq Copyright (C) 2014 by geehalel Recorder Manager 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 */ #include #include "recordermanager.h" #include "serrecorder.h" #ifdef HAVE_THEORA #include "theorarecorder.h" #endif namespace INDI { RecorderManager::RecorderManager() { recorder_list.push_back(new SER_Recorder()); #ifdef HAVE_THEORA recorder_list.push_back(new TheoraRecorder()); #endif default_recorder = recorder_list.at(0); } RecorderManager::~RecorderManager() { std::vector::iterator it; for (it = recorder_list.begin(); it != recorder_list.end(); it++) { delete (*it); } recorder_list.clear(); } std::vector RecorderManager::getRecorderList() { return recorder_list; } RecorderInterface *RecorderManager::getRecorder() { return current_recorder; } RecorderInterface *RecorderManager::getDefaultRecorder() { return default_recorder; } void RecorderManager::setRecorder(RecorderInterface *recorder) { current_recorder = recorder; } } libindi/libs/stream/recorder/theorarecorder.h0000664000175000017500000000533513263645557020712 0ustar jasemjasem/* Copyright (C) 2017 by Jasem Mutlaq Theora Recorder 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 */ #pragma once #include "recorderinterface.h" #include #include #include #include namespace INDI { /** * @brief The TheoraRecorder class implemented recording of video streaming data in a libtheora OGV file. */ class TheoraRecorder : public RecorderInterface { public: TheoraRecorder(); virtual ~TheoraRecorder(); virtual const char *getExtension() { return ".ogv"; } virtual bool setPixelFormat(INDI_PIXEL_FORMAT pixelFormat, uint8_t pixelDepth); virtual bool setSize(uint16_t width, uint16_t height); virtual bool open(const char *filename, char *errmsg); virtual bool close(); virtual bool writeFrame(const uint8_t *frame, uint32_t nbytes); virtual void setStreamEnabled(bool enable) { isStreamingActive = enable; } protected: bool isRecordingActive = false, isStreamingActive = false; uint32_t number_of_planes; uint16_t rawWidth = 0, rawHeight = 0; std::vector frameStamps; INDI_PIXEL_FORMAT m_PixelFormat; uint8_t m_PixelDepth=8; private: bool allocateBuffers(); //int theora_write_frame(th_ycbcr_buffer ycbcr, int last); int theora_write_frame(int last); bool frac(double fps, uint32_t &num, uint32_t &den); th_ycbcr_buffer ycbcr; ogg_uint32_t video_fps_numerator = 24; ogg_uint32_t video_fps_denominator = 1; ogg_uint32_t video_aspect_numerator = 0; ogg_uint32_t video_aspect_denominator = 0; int video_rate = -1; int video_quality = -1; int soft_target=0; ogg_uint32_t keyframe_frequency=0; int buf_delay=-1; int vp3_compatible=0; int chroma_format = TH_PF_420; FILE *twopass_file = nullptr; int twopass=0; int passno=0; FILE *ogg_fp = nullptr; ogg_stream_state ogg_os; ogg_packet op; ogg_page og; th_enc_ctx *td = nullptr; th_info ti; th_comment tc; }; } libindi/libs/stream/recorder/recorderinterface.h0000664000175000017500000000611313263645557021363 0ustar jasemjasem/* Copyright (C) 2014 by geehalel V4L2 Record 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 */ #pragma once #include "indidevapi.h" #include "indibasetypes.h" #include #include #include #include #if 0 #ifdef OSX_EMBEDED_MODE #define v4l2_fourcc(a, b, c, d) ((uint32_t)(a) | ((uint32_t)(b) << 8) | ((uint32_t)(c) << 16) | ((uint32_t)(d) << 24)) #define V4L2_PIX_FMT_GREY v4l2_fourcc('G', 'R', 'E', 'Y') /* 8 Greyscale */ #define V4L2_PIX_FMT_SBGGR8 v4l2_fourcc('B', 'A', '8', '1') /* 8 BGBG.. GRGR.. */ #define V4L2_PIX_FMT_SBGGR16 v4l2_fourcc('B', 'Y', 'R', '2') /* 16 BGBG.. GRGR.. */ #define V4L2_PIX_FMT_SGBRG8 v4l2_fourcc('G', 'B', 'R', 'G') /* 8 GBGB.. RGRG.. */ #define V4L2_PIX_FMT_BGR24 v4l2_fourcc('B', 'G', 'R', '3') /* 24 BGR-8-8-8 */ #define V4L2_PIX_FMT_RGB24 v4l2_fourcc('R', 'G', 'B', '3') /* 24 RGB-8-8-8 */ #define V4L2_PIX_FMT_SRGGB8 v4l2_fourcc('R', 'G', 'G', 'B') /* 8 RGRG.. GBGB.. */ #define V4L2_PIX_FMT_SGRBG8 v4l2_fourcc('G', 'R', 'B', 'G') /* 8 GRGR.. BGBG.. */ #else #include #endif #endif namespace INDI { /** * @brief The RecorderInterface class is the base class for recorders. */ class RecorderInterface { public: RecorderInterface() = default; virtual ~RecorderInterface() = default; virtual const char *getName(); virtual const char *getExtension() = 0; // true when direct encoding of pixel format virtual bool setPixelFormat(INDI_PIXEL_FORMAT pixelFormat, uint8_t pixelDepth=8) = 0; // set full image size in pixels virtual bool setSize(uint16_t width, uint16_t height) = 0; // Set FPS virtual bool setFPS(float FPS) { m_FPS = FPS; return true; } virtual bool open(const char *filename, char *errmsg) = 0; virtual bool close() = 0; // when frame is in known encoding format virtual bool writeFrame(const uint8_t *frame, uint32_t nbytes) = 0; // If streaming is enabled, then any subframing is already done by the stream recorder // and no need to do any further subframing operations. Otherwise, subframing must be done. // This is to reduce process time and save memory for a dedicated subframe buffer virtual void setStreamEnabled(bool enable) = 0; protected: const char *name; float m_FPS = 1; }; } libindi/libs/stream/recorder/theorarecorder.cpp0000664000175000017500000004156213263645557021247 0ustar jasemjasem/* Copyright (C) 2017 by Jasem Mutlaq Theora Recorder 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 */ #include "theorarecorder.h" #include "jpegutils.h" #define _FILE_OFFSET_BITS 64 #include #include #include #include #define ERRMSGSIZ 1024 static int ilog(unsigned _v) { int ret; for(ret=0;_v;ret++)_v>>=1; return ret; } namespace INDI { TheoraRecorder::TheoraRecorder() { name = "OGV"; isRecordingActive = false; ycbcr[0].data = nullptr; ycbcr[1].data = nullptr; ycbcr[2].data = nullptr; } TheoraRecorder::~TheoraRecorder() { delete [] ycbcr[0].data; delete [] ycbcr[1].data; delete [] ycbcr[2].data; th_encode_free(td); } bool TheoraRecorder::setPixelFormat(INDI_PIXEL_FORMAT pixelFormat, uint8_t pixelDepth) { m_PixelFormat = pixelFormat; m_PixelDepth = pixelDepth; return true; } bool TheoraRecorder::setSize(uint16_t width, uint16_t height) { if (isRecordingActive) return false; rawWidth = width; rawHeight = height; return allocateBuffers(); } bool TheoraRecorder::allocateBuffers() { /* Must hold: yuv_w >= w */ uint16_t yuv_w = (rawWidth + 15) & ~15; /* Must hold: yuv_h >= h */ uint16_t yuv_h = (rawHeight + 15) & ~15; /* Do we need to allocate a buffer */ if (!ycbcr[0].data || yuv_w != ycbcr[0].width || yuv_h != ycbcr[0].height) { ycbcr[0].width = yuv_w; ycbcr[0].height = yuv_h; ycbcr[0].stride = yuv_w; #if 0 if (m_PixelFormat == INDI_MONO) { ycbcr[1].width = 0; ycbcr[1].stride = 0; ycbcr[1].height = 0; ycbcr[2].width = 0; ycbcr[2].stride = 0; ycbcr[2].height = 0; } else { #endif ycbcr[1].width = (chroma_format == TH_PF_444) ? yuv_w : (yuv_w >> 1); ycbcr[1].stride = ycbcr[1].width; ycbcr[1].height = (chroma_format == TH_PF_420) ? (yuv_h >> 1) : yuv_h; ycbcr[2].width = ycbcr[1].width; ycbcr[2].stride = ycbcr[1].stride; ycbcr[2].height = ycbcr[1].height; #if 0 } #endif delete [] ycbcr[0].data; delete [] ycbcr[1].data; delete [] ycbcr[2].data; ycbcr[0].data = new uint8_t[ycbcr[0].stride * ycbcr[0].height]; ycbcr[1].data = new uint8_t[ycbcr[1].stride * ycbcr[1].height]; ycbcr[2].data = new uint8_t[ycbcr[2].stride * ycbcr[2].height]; #if 0 ycbcr[0].data = new uint8_t[ycbcr[0].stride * ycbcr[0].height]; ycbcr[1].data = (m_PixelFormat == INDI_MONO) ? 0 : new uint8_t[ycbcr[1].stride * ycbcr[1].height]; ycbcr[2].data = (m_PixelFormat == INDI_MONO) ? 0 : new uint8_t[ycbcr[2].stride * ycbcr[2].height]; #endif } return true; } bool TheoraRecorder::open(const char *filename, char *errmsg) { if (isRecordingActive) return false; if(soft_target) { if(video_rate<=0) { snprintf(errmsg, ERRMSGSIZ, "Soft rate target requested without a bitrate."); return false; } if(video_quality==-1) video_quality=0; } else { if(video_rate>0) video_quality=0; if(video_quality==-1) video_quality=48; } if(keyframe_frequency<=0) { /*Use a default keyframe frequency of 64 for 1-pass (streaming) mode, and 256 for two-pass mode.*/ keyframe_frequency=twopass?256:64; } ogg_fp = fopen(filename, "wb"); if(!ogg_fp) { snprintf(errmsg, ERRMSGSIZ, "%s: error: could not open output file", filename); return false; } srand(time(NULL)); if(ogg_stream_init(&ogg_os, rand())) { snprintf(errmsg, ERRMSGSIZ, "%s: error: could not create ogg stream state", filename); return false; } th_info_init(&ti); ti.frame_width = ((rawWidth + 15) >>4)<<4; ti.frame_height = ((rawHeight + 15)>>4)<<4; ti.pic_width = rawWidth; ti.pic_height = rawHeight; ti.pic_x = 0; ti.pic_y = 0; frac(m_FPS, video_fps_numerator, video_fps_denominator); ti.fps_numerator = video_fps_numerator; ti.fps_denominator = video_fps_denominator; ti.aspect_numerator = video_aspect_numerator; ti.aspect_denominator = video_aspect_denominator; ti.colorspace = TH_CS_UNSPECIFIED; ti.pixel_fmt = static_cast(chroma_format); ti.target_bitrate = video_rate; ti.quality = video_quality; ti.keyframe_granule_shift=ilog(keyframe_frequency-1); td=th_encode_alloc(&ti); th_info_clear(&ti); /* setting just the granule shift only allows power-of-two keyframe spacing. Set the actual requested spacing. */ int ret=th_encode_ctl(td,TH_ENCCTL_SET_KEYFRAME_FREQUENCY_FORCE, &keyframe_frequency,sizeof(keyframe_frequency-1)); if(ret<0) { snprintf(errmsg, ERRMSGSIZ, "Could not set keyframe interval to %d.",(int)keyframe_frequency); } if(vp3_compatible) { ret=th_encode_ctl(td,TH_ENCCTL_SET_VP3_COMPATIBLE,&vp3_compatible, sizeof(vp3_compatible)); if(ret<0||!vp3_compatible) { snprintf(errmsg, ERRMSGSIZ, "Could not enable strict VP3 compatibility."); } } if(soft_target) { /* reverse the rate control flags to favor a 'long time' strategy */ int arg = TH_RATECTL_CAP_UNDERFLOW; ret=th_encode_ctl(td,TH_ENCCTL_SET_RATE_FLAGS,&arg,sizeof(arg)); if(ret<0) snprintf(errmsg, ERRMSGSIZ, "Could not set encoder flags for soft-target"); /* Default buffer control is overridden on two-pass */ if(!twopass&&buf_delay<0) { if((keyframe_frequency*7>>1) > 5*video_fps_numerator/video_fps_denominator) arg=keyframe_frequency*7>>1; else arg=5*video_fps_numerator/video_fps_denominator; ret=th_encode_ctl(td,TH_ENCCTL_SET_RATE_BUFFER,&arg,sizeof(arg)); if(ret<0) snprintf(errmsg, ERRMSGSIZ, "Could not set rate control buffer for soft-target"); } } /* set up two-pass if needed */ if(passno==1) { unsigned char *buffer; int bytes; bytes=th_encode_ctl(td,TH_ENCCTL_2PASS_OUT,&buffer,sizeof(buffer)); if(bytes<0) { //IDLog("Could not set up the first pass of two-pass mode."); //IDLog("Did you remember to specify an estimated bitrate?"); //exit(1); return false; } /*Perform a seek test to ensure we can overwrite this placeholder data at the end; this is better than letting the user sit through a whole encode only to find out their pass 1 file is useless at the end.*/ if(fseek(twopass_file,0,SEEK_SET)<0) { //IDLog("Unable to seek in two-pass data file."); //exit(1); return false; } if(fwrite(buffer,1,bytes,twopass_file)< static_cast(bytes)) { IDLog("Unable to write to two-pass data file."); return false; //exit(1); } fflush(twopass_file); } if(passno==2){ /*Enable the second pass here. We make this call just to set the encoder into 2-pass mode, because by default enabling two-pass sets the buffer delay to the whole file (because there's no way to explicitly request that behavior). If we waited until we were actually encoding, it would overwite our settings.*/ if(th_encode_ctl(td,TH_ENCCTL_2PASS_IN,NULL,0)<0) { snprintf(errmsg, ERRMSGSIZ, "Could not set up the second pass of two-pass mode."); return false; } if(twopass==3) { if(fseek(twopass_file,0,SEEK_SET)<0) { snprintf(errmsg, ERRMSGSIZ, "Unable to seek in two-pass data file."); return false; } } } /*Now we can set the buffer delay if the user requested a non-default one (this has to be done after two-pass is enabled).*/ if(passno!=1&&buf_delay>=0) { ret=th_encode_ctl(td,TH_ENCCTL_SET_RATE_BUFFER, &buf_delay,sizeof(buf_delay)); if(ret<0) { snprintf(errmsg, ERRMSGSIZ, "Warning: could not set desired buffer delay."); } } /* write the bitstream header packets with proper page interleave */ th_comment_init(&tc); /* first packet will get its own page automatically */ if(th_encode_flushheader(td,&tc,&op)<=0) { snprintf(errmsg, ERRMSGSIZ, "Internal Theora library error."); return false; } th_comment_clear(&tc); if(passno!=1) { ogg_stream_packetin(&ogg_os,&op); if(ogg_stream_pageout(&ogg_os,&og)!=1) { snprintf(errmsg, ERRMSGSIZ, "Internal Ogg library error."); return false; } fwrite(og.header,1,og.header_len,ogg_fp); fwrite(og.body,1,og.body_len,ogg_fp); } /* create the remaining theora headers */ for(;;) { ret=th_encode_flushheader(td,&tc,&op); if(ret<0) { snprintf(errmsg, ERRMSGSIZ,"Internal Theora library error."); return false; } else if(!ret) break; if(passno!=1) ogg_stream_packetin(&ogg_os,&op); } /* Flush the rest of our headers. This ensures the actual data in each stream will start on a new page, as per spec. */ if(passno!=1) { for(;;) { int result = ogg_stream_flush(&ogg_os,&og); if(result<0) { /* can't get here */ snprintf(errmsg, ERRMSGSIZ,"Internal Ogg library error."); return false; } if(result==0) break; fwrite(og.header,1,og.header_len,ogg_fp); fwrite(og.body,1,og.body_len,ogg_fp); } } isRecordingActive = true; return true; } bool TheoraRecorder::close() { theora_write_frame(1); if(passno==1) { /* need to read the final (summary) packet */ unsigned char *buffer; int bytes = th_encode_ctl(td, TH_ENCCTL_2PASS_OUT, &buffer, sizeof(buffer)); if(bytes<0) { IDLog("Could not read two-pass summary data from encoder."); return false; } if(fseek(twopass_file,0,SEEK_SET)<0) { IDLog("Unable to seek in two-pass data file."); return false; } if(fwrite(buffer,1,bytes,twopass_file)< static_cast(bytes)) { IDLog("Unable to write to two-pass data file."); return false; } fflush(twopass_file); } /*th_encode_free(td); free(ycbcr[0].data); ycbcr[0].data = nullptr; free(ycbcr[1].data); free(ycbcr[2].data);*/ if(ogg_stream_flush(&ogg_os, &og)) { fwrite(og.header, og.header_len, 1, ogg_fp); fwrite(og.body, og.body_len, 1, ogg_fp); } if(ogg_fp) { fflush(ogg_fp); fclose(ogg_fp); } ogg_stream_clear(&ogg_os); if(twopass_file) fclose(twopass_file); isRecordingActive = false; return true; } bool TheoraRecorder::writeFrame(const uint8_t *frame, uint32_t nbytes) { if (!isRecordingActive) return false; if (m_PixelFormat == INDI_MONO) { memcpy(ycbcr[0].data, frame, ycbcr[0].stride * ycbcr[0].height); // Cb and Cr values to 0x80 (128) for grayscale image memset(ycbcr[1].data, 0x80, ycbcr[1].stride * ycbcr[1].height); memset(ycbcr[2].data, 0x80, ycbcr[2].stride * ycbcr[2].height); } else if (m_PixelFormat == INDI_JPG) { decode_jpeg_raw((const_cast(frame)), nbytes, 0, 0, rawWidth, rawHeight, ycbcr[0].data, ycbcr[1].data, ycbcr[2].data ); } else return false; theora_write_frame(0); return true; } # if 0 bool TheoraRecorder::writeFrameMono(uint8_t *frame) { if (isStreamingActive == false && (subX > 0 || subY > 0 || subW != rawWidth || subH != rawHeight)) { int offset = ((rawWidth * subY) + subX); uint8_t *srcBuffer = frame + offset; uint8_t *destBuffer = frame; int imageWidth = subW; int imageHeight = subH; for (int i = 0; i < imageHeight; i++) memcpy(destBuffer + i * imageWidth, srcBuffer + rawWidth * i, imageWidth); } return writeFrame(frame); } bool TheoraRecorder::writeFrameColor(uint8_t *frame) { if (isStreamingActive == false && (subX > 0 || subY > 0 || subW != rawWidth || subH != rawHeight)) { int offset = ((rawWidth * subY) + subX); uint8_t *srcBuffer = frame + offset * 3; uint8_t *destBuffer = frame; int imageWidth = subW; int imageHeight = subH; // RGB for (int i = 0; i < imageHeight; i++) memcpy(destBuffer + i * imageWidth * 3, srcBuffer + rawWidth * 3 * i, imageWidth * 3); } return writeFrame(frame); } #endif int TheoraRecorder::theora_write_frame(int last) { ogg_packet op; ogg_page og; int rc = -1; if( (rc = th_encode_ycbcr_in(td, ycbcr)) ) { IDLog("error: could not encode frame %d", rc); return rc; } /* in two-pass mode's first pass we need to extract and save the pass data */ if(passno==1) { unsigned char *buffer; int bytes = th_encode_ctl(td, TH_ENCCTL_2PASS_OUT, &buffer, sizeof(buffer)); if(bytes<0) { IDLog("Could not read two-pass data from encoder."); return 1; } if(fwrite(buffer,1,bytes,twopass_file) < static_cast(bytes)) { IDLog("Unable to write to two-pass data file."); return 1; } fflush(twopass_file); } if(!th_encode_packetout(td, last, &op)) { IDLog("error: could not read packets"); return 1; } if (passno!=1) { ogg_stream_packetin(&ogg_os, &op); while(ogg_stream_pageout(&ogg_os, &og)) { fwrite(og.header, og.header_len, 1, ogg_fp); fwrite(og.body, og.body_len, 1, ogg_fp); } } return 0; } /* ** find rational approximation to given real number ** David Eppstein / UC Irvine / 8 Aug 1993 ** ** With corrections from Arno Formella, May 2008 ** ** usage: a.out r d ** r is real number to approx ** d is the maximum denominator allowed ** ** based on the theory of continued fractions ** if x = a1 + 1/(a2 + 1/(a3 + 1/(a4 + ...))) ** then best approximation is found by truncating this series ** (with some adjustments in the last term). ** ** Note the fraction can be recovered as the first column of the matrix ** ( a1 1 ) ( a2 1 ) ( a3 1 ) ... ** ( 1 0 ) ( 1 0 ) ( 1 0 ) ** Instead of keeping the sequence of continued fraction terms, ** we just keep the last partial product of these matrices. */ bool TheoraRecorder::frac(double fps, uint32_t &num, uint32_t &den) { long m[2][2]; double x, startx; long maxden; long ai; startx = x = fps; maxden = 100; /* initialize matrix */ m[0][0] = m[1][1] = 1; m[0][1] = m[1][0] = 0; /* loop finding terms until denom gets too big */ while (m[1][0] * ( ai = (long)x ) + m[1][1] <= maxden) { long t; t = m[0][0] * ai + m[0][1]; m[0][1] = m[0][0]; m[0][0] = t; t = m[1][0] * ai + m[1][1]; m[1][1] = m[1][0]; m[1][0] = t; if(x==(double)ai) break; // AF: division by zero x = 1/(x - (double) ai); if(x>(double)0x7FFFFFFF) break; // AF: representation failure } num = m[0][0]; den = m[1][0]; return true; /* now remaining x is between 0 and 1/ai */ /* approx as either 0 or 1/m where m is max that will fit in maxden */ /* first try zero */ //printf("%ld/%ld, error = %e\n", m[0][0], m[1][0], startx - ((double) m[0][0] / (double) m[1][0])); /* now try other possibility */ //ai = (maxden - m[1][1]) / m[1][0]; //m[0][0] = m[0][0] * ai + m[0][1]; //m[1][0] = m[1][0] * ai + m[1][1]; //printf("%ld/%ld, error = %e\n", m[0][0], m[1][0], // startx - ((double) m[0][0] / (double) m[1][0])); } } libindi/libs/stream/recorder/serrecorder.cpp0000664000175000017500000002534213263645557020554 0ustar jasemjasem/* Copyright (C) 2014 by geehalel SER File Format Recorder (see http://www.grischa-hahn.homepage.t-online.de/astro/ser/index.htm) Specifications can be found in - for V2: http://www.grischa-hahn.homepage.t-online.de/astro/ser/SER%20Doc%20V2.pdf - for V3: http://www.grischa-hahn.homepage.t-online.de/astro/ser/SER%20Doc%20V3b.pdf SER Files may be used as input files for Registax 6 or astrostakkert (which you can both run under Linux using wine), or also Siril, the linux iris version. 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 */ #include "serrecorder.h" #include "jpegutils.h" #include #include #include #include #define ERRMSGSIZ 1024 namespace INDI { SER_Recorder::SER_Recorder() { name = "SER"; strncpy(serh.FileID, "LUCAM-RECORDER", 14); strncpy(serh.Observer, " Unknown Observer", 40); strncpy(serh.Instrume, " Unknown Instrument", 40); strncpy(serh.Telescope, " Unknown Telescope", 40); serh.LuID = 0; serh.PixelDepth = 8; number_of_planes=1; if (is_little_endian()) serh.LittleEndian = SER_LITTLE_ENDIAN; else serh.LittleEndian = SER_BIG_ENDIAN; isRecordingActive = false; f = nullptr; jpegBuffer = static_cast(malloc(1)); } SER_Recorder::~SER_Recorder() { free(jpegBuffer); } bool SER_Recorder::is_little_endian() { unsigned int magic = 0x00000001; unsigned char black_magic = *(unsigned char *)&magic; return black_magic == 0x01; } void SER_Recorder::write_int_le(uint32_t *i) { if (is_little_endian()) fwrite((const void *)(i), sizeof(uint32_t), 1, f); else { unsigned char *c = (unsigned char *)i; fwrite((const void *)(c + 3), sizeof(char), 1, f); fwrite((const void *)(c + 2), sizeof(char), 1, f); fwrite((const void *)(c + 1), sizeof(char), 1, f); fwrite((const void *)(c), sizeof(char), 1, f); } } void SER_Recorder::write_long_int_le(uint64_t *i) { if (is_little_endian()) { fwrite((const void *)((uint32_t *)i), sizeof(int), 1, f); fwrite((const void *)((uint32_t *)(i) + 1), sizeof(int), 1, f); } else { write_int_le((uint32_t *)(i) + 1); write_int_le((uint32_t *)(i)); } } void SER_Recorder::write_header(ser_header *s) { fwrite((const void *)(s->FileID), sizeof(char), 14, f); write_int_le(&(s->LuID)); write_int_le(&(s->ColorID)); write_int_le(&(s->LittleEndian)); write_int_le(&(s->ImageWidth)); write_int_le(&(s->ImageHeight)); write_int_le(&(s->PixelDepth)); write_int_le(&(s->FrameCount)); fwrite((const void *)(s->Observer), sizeof(char), 40, f); fwrite((const void *)(s->Instrume), sizeof(char), 40, f); fwrite((const void *)(s->Telescope), sizeof(char), 40, f); write_long_int_le(&(s->DateTime)); write_long_int_le(&(s->DateTime_UTC)); } bool SER_Recorder::setPixelFormat(INDI_PIXEL_FORMAT pixelFormat, uint8_t pixelDepth) { serh.PixelDepth = pixelDepth; m_PixelFormat = pixelFormat; number_of_planes = 1; switch (pixelFormat) { case INDI_MONO: serh.ColorID = SER_MONO; break; case INDI_BAYER_BGGR: serh.ColorID = SER_BAYER_BGGR; break; case INDI_BAYER_GBRG: serh.ColorID = SER_BAYER_GBRG; break; case INDI_BAYER_GRBG: serh.ColorID = SER_BAYER_GRBG; break; case INDI_BAYER_RGGB: serh.ColorID = SER_BAYER_RGGB; break; case INDI_RGB: number_of_planes = 3; serh.ColorID = SER_RGB; break; case INDI_BGR: number_of_planes = 3; serh.ColorID = SER_BGR; break; case INDI_JPG: number_of_planes = 3; serh.ColorID = SER_RGB; break; default: return false; } return true; } bool SER_Recorder::setSize(uint16_t width, uint16_t height) { if (isRecordingActive) return false; rawWidth = width; rawHeight = height; serh.ImageWidth = width; serh.ImageHeight = height; return true; } bool SER_Recorder::open(const char *filename, char *errmsg) { if (isRecordingActive) return false; serh.FrameCount = 0; if ((f = fopen(filename, "w")) == nullptr) { snprintf(errmsg, ERRMSGSIZ, "recorder open error %d, %s\n", errno, strerror(errno)); return false; } serh.DateTime = getLocalTimeStamp(); serh.DateTime_UTC = getUTCTimeStamp(); write_header(&serh); frame_size = serh.ImageWidth * serh.ImageHeight * (serh.PixelDepth <= 8 ? 1 : 2) * number_of_planes; isRecordingActive = true; frameStamps.clear(); return true; } bool SER_Recorder::close() { if (f) { // Write all timestamps for (auto value : frameStamps) write_long_int_le(&value); frameStamps.clear(); fseek(f, 0L, SEEK_SET); write_header(&serh); fclose(f); f = nullptr; } isRecordingActive = false; return true; } bool SER_Recorder::writeFrame(const uint8_t *frame, uint32_t nbytes) { if (!isRecordingActive) return false; #if 0 if (serh.ColorID == SER_MONO) { if (isStreamingActive == false && (offsetX > 0 || offsetY > 0 || serh.ImageWidth != rawWidth || serh.ImageHeight != rawHeight)) { int offset = ((rawWidth * offsetY) + offsetX); uint8_t *srcBuffer = frame + offset; uint8_t *destBuffer = frame; int imageWidth = serh.ImageWidth; int imageHeight = serh.ImageHeight; for (int i = 0; i < imageHeight; i++) memcpy(destBuffer + i * imageWidth, srcBuffer + rawWidth * i, imageWidth); } } else { if (isStreamingActive == false && (offsetX > 0 || offsetY > 0 || serh.ImageWidth != rawWidth || serh.ImageHeight != rawHeight)) { int offset = ((rawWidth * offsetY) + offsetX); uint8_t *srcBuffer = frame + offset * 3; uint8_t *destBuffer = frame; int imageWidth = serh.ImageWidth; int imageHeight = serh.ImageHeight; // RGB for (int i = 0; i < imageHeight; i++) memcpy(destBuffer + i * imageWidth * 3, srcBuffer + rawWidth * 3 * i, imageWidth * 3); } } #endif frameStamps.push_back(getUTCTimeStamp()); // Not technically pixel format, but let's use this for now. if (m_PixelFormat == INDI_JPG) { int w=0,h=0,naxis=1; size_t memsize=0; if (decode_jpeg_rgb(const_cast(frame), nbytes, &jpegBuffer, &memsize, &naxis, &w, &h) < 0) return false; serh.ImageWidth = w; serh.ImageHeight = h; serh.ColorID = (naxis == 3) ? SER_RGB : SER_MONO; fwrite(jpegBuffer, memsize, 1, f); } else fwrite(frame, nbytes, 1, f); serh.FrameCount += 1; return true; } // Copyright (C) 2015 Chris Garry // // // Calculate if a year is a leap yer // bool SER_Recorder::is_leap_year(uint32_t year) { if ((year % 400) == 0) { // If year is divisible by 400 then is_leap_year return true; } else if ((year % 100) == 0) { // Else if year is divisible by 100 then not_leap_year return false; } else if ((year % 4) == 0) { // Else if year is divisible by 4 then is_leap_year return true; } else { // Else not_leap_year return false; } } uint64_t SER_Recorder::getUTCTimeStamp() { uint64_t utcTS; // Get starting time timeval currentTime; gettimeofday(¤tTime, nullptr); struct tm *tp; time_t t = (time_t)currentTime.tv_sec; uint32_t u = currentTime.tv_usec; // UTC Time tp = gmtime(&t); dateTo64BitTS(tp->tm_year + 1900, tp->tm_mon + 1, tp->tm_mday, tp->tm_hour, tp->tm_min, tp->tm_sec, u, &utcTS); return utcTS; } uint64_t SER_Recorder::getLocalTimeStamp() { uint64_t localTS; // Get starting time timeval currentTime; gettimeofday(¤tTime, nullptr); struct tm *tp; time_t t = (time_t)currentTime.tv_sec; uint32_t u = currentTime.tv_usec; // Local Time tp = localtime(&t); dateTo64BitTS(tp->tm_year, tp->tm_mon, tp->tm_mday, tp->tm_hour, tp->tm_min, tp->tm_sec, u, &localTS); return localTS; } // Convert real time to timestamp // void SER_Recorder::dateTo64BitTS(int32_t year, int32_t month, int32_t day, int32_t hour, int32_t minute, int32_t second, int32_t microsec, uint64_t *p_ts) { uint64_t ts = 0; int32_t yr; // Add 400 year blocks for (yr = 1; yr < (year - 400); yr += 400) { ts += m_septaseconds_per_400_years; } // Add 1 years for (; yr < year; yr++) { uint32_t days_this_year = 365; if (is_leap_year(yr)) { days_this_year = 366; } ts += (days_this_year * m_septaseconds_per_day); } // Add months for (int mon = 1; mon < month; mon++) { switch (mon) { case 4: // April case 6: // June case 9: // September case 11: // Novenber ts += (30 * m_septaseconds_per_day); break; case 2: // Feburary if (is_leap_year(year)) { ts += (29 * m_septaseconds_per_day); } else { ts += (28 * m_septaseconds_per_day); } break; default: ts += (31 * m_septaseconds_per_day); break; } } // Add days ts += ((day - 1) * m_septaseconds_per_day); // Add hours ts += (hour * m_septaseconds_per_hour); // Add minutes ts += (minute * m_septaseconds_per_minute); // Add seconds ts += (second * C_SEPASECONDS_PER_SECOND); // Micro seconds ts += (microsec * m_sepaseconds_per_microsecond); // Output result *p_ts = ts; } } libindi/libs/lx/0000775000175000017500000000000013263645557013046 5ustar jasemjasemlibindi/libs/lx/Lx.cpp0000664000175000017500000005307513263645557014147 0ustar jasemjasem #include "Lx.h" #include "indicom.h" #include #include #include #include // from indicom.cpp for tty_connect #define PARITY_NONE 0 #define PARITY_EVEN 1 #define PARITY_ODD 2 void Lx::setCamerafd(int fd) { camerafd = fd; } bool Lx::isEnabled() { return (LxEnableS[1].s == ISS_ON); } bool Lx::initProperties(INDI::DefaultDevice *device) { //IDLog("Initializing Long Exposure Properties\n"); dev = device; device_name = dev->getDeviceName(); IUFillSwitch(&LxEnableS[0], "Disable", "", ISS_ON); IUFillSwitch(&LxEnableS[1], "Enable", "", ISS_OFF); IUFillSwitchVector(&LxEnableSP, LxEnableS, NARRAY(LxEnableS), device_name, "Activate", "", LX_TAB, IP_RW, ISR_1OFMANY, 0, IPS_IDLE); IUFillSwitch(&LxModeS[LXSERIAL], "Serial", "", ISS_ON); //IUFillSwitch(&LxModeS[LXPARALLEL], "Parallel", "", ISS_OFF); IUFillSwitch(&LxModeS[LXLED], "SPC900 LED", "", ISS_OFF); //IUFillSwitch(&LxModeS[LXGPIO], "GPIO (Arm/RPI)", "", ISS_OFF); // IUFillSwitch(&LxModeS[4], "IndiDuino Switcher", "", ISS_OFF); // Snooping is not enough IUFillSwitchVector(&LxModeSP, LxModeS, NARRAY(LxModeS), device_name, "LX Mode", "", LX_TAB, IP_RW, ISR_1OFMANY, 0, IPS_IDLE); IUFillText(&LxPortT[0], "Port", "", "/dev/ttyUSB0"); IUFillTextVector(&LxPortTP, LxPortT, NARRAY(LxPortT), device_name, "Lx port", "", LX_TAB, IP_RW, 0, IPS_IDLE); IUFillSwitch(&LxSerialOptionS[0], "Use DTR (pin 4)", "", ISS_OFF); IUFillSwitch(&LxSerialOptionS[1], "Use RTS (pin 7)", "", ISS_ON); IUFillSwitch(&LxSerialOptionS[2], "Use Serial command", "", ISS_OFF); IUFillSwitchVector(&LxSerialOptionSP, LxSerialOptionS, NARRAY(LxSerialOptionS), device_name, "Serial Options", "", LX_TAB, IP_RW, ISR_1OFMANY, 0, IPS_IDLE); IUFillSwitch(&LxParallelOptionS[0], "Use Data 0 (pin 2)", "", ISS_OFF); IUFillSwitch(&LxParallelOptionS[1], "Use Data 1 (pin 3)", "", ISS_ON); // Steve's Chambers Schematics IUFillSwitch(&LxParallelOptionS[2], "Use Data 2 (pin 4)", "", ISS_OFF); IUFillSwitch(&LxParallelOptionS[3], "Use Data 3 (pin 5)", "", ISS_OFF); IUFillSwitch(&LxParallelOptionS[4], "Use Data 4 (pin 6)", "", ISS_OFF); IUFillSwitch(&LxParallelOptionS[5], "Use Data 5 (pin 7)", "", ISS_OFF); IUFillSwitch(&LxParallelOptionS[6], "Use Data 6 (pin 8)", "", ISS_OFF); IUFillSwitch(&LxParallelOptionS[7], "Use Data 7 (pin 9)", "", ISS_OFF); IUFillSwitch(&LxParallelOptionS[8], "Use Parallel command", "", ISS_OFF); IUFillSwitchVector(&LxParallelOptionSP, LxParallelOptionS, NARRAY(LxParallelOptionS), device_name, "Parallel Options", "", LX_TAB, IP_RW, ISR_1OFMANY, 0, IPS_IDLE); IUFillText(&LxStartStopCmdT[0], "Start command", "", ":O1"); IUFillText(&LxStartStopCmdT[1], "Stop command", "", ":O0"); IUFillTextVector(&LxStartStopCmdTP, LxStartStopCmdT, NARRAY(LxStartStopCmdT), device_name, "Start/Stop commands", "", LX_TAB, IP_RW, 0, IPS_IDLE); IUFillSwitch(&LxLogicalLevelS[0], "Low to High", "", ISS_ON); IUFillSwitch(&LxLogicalLevelS[1], "High to Low", "", ISS_OFF); IUFillSwitchVector(&LxLogicalLevelSP, LxLogicalLevelS, NARRAY(LxLogicalLevelS), device_name, "Start Transition", "", LX_TAB, IP_RW, ISR_1OFMANY, 0, IPS_IDLE); IUFillSwitch(&LxSerialSpeedS[0], "1200", "", ISS_OFF); IUFillSwitch(&LxSerialSpeedS[1], "2400", "", ISS_OFF); IUFillSwitch(&LxSerialSpeedS[2], "4800", "", ISS_OFF); IUFillSwitch(&LxSerialSpeedS[3], "9600", "", ISS_ON); IUFillSwitch(&LxSerialSpeedS[4], "19200", "", ISS_OFF); IUFillSwitch(&LxSerialSpeedS[5], "38400", "", ISS_OFF); IUFillSwitch(&LxSerialSpeedS[6], "57600", "", ISS_OFF); IUFillSwitch(&LxSerialSpeedS[7], "115200", "", ISS_OFF); IUFillSwitch(&LxSerialSpeedS[8], "230400", "", ISS_OFF); IUFillSwitchVector(&LxSerialSpeedSP, LxSerialSpeedS, NARRAY(LxSerialSpeedS), device_name, "Serial speed", "", LX_TAB, IP_RW, ISR_1OFMANY, 0, IPS_IDLE); IUFillSwitch(&LxSerialSizeS[0], "5", "", ISS_OFF); IUFillSwitch(&LxSerialSizeS[1], "6", "", ISS_OFF); IUFillSwitch(&LxSerialSizeS[2], "7", "", ISS_OFF); IUFillSwitch(&LxSerialSizeS[3], "8", "", ISS_ON); IUFillSwitchVector(&LxSerialSizeSP, LxSerialSizeS, NARRAY(LxSerialSizeS), device_name, "Serial size", "", LX_TAB, IP_RW, ISR_1OFMANY, 0, IPS_IDLE); IUFillSwitch(&LxSerialParityS[0], "None", "", ISS_ON); IUFillSwitch(&LxSerialParityS[1], "Even", "", ISS_OFF); IUFillSwitch(&LxSerialParityS[2], "Odd", "", ISS_OFF); IUFillSwitchVector(&LxSerialParitySP, LxSerialParityS, NARRAY(LxSerialParityS), device_name, "Serial parity", "", LX_TAB, IP_RW, ISR_1OFMANY, 0, IPS_IDLE); IUFillSwitch(&LxSerialStopS[0], "1", "", ISS_ON); IUFillSwitch(&LxSerialStopS[1], "2", "", ISS_OFF); IUFillSwitchVector(&LxSerialStopSP, LxSerialStopS, NARRAY(LxSerialStopS), device_name, "Serial stop", "", LX_TAB, IP_RW, ISR_1OFMANY, 0, IPS_IDLE); IUFillSwitch(&LxSerialAddeolS[0], "None", "", ISS_OFF); IUFillSwitch(&LxSerialAddeolS[1], "CR (OxD, \\r)", "", ISS_ON); IUFillSwitch(&LxSerialAddeolS[2], "LF (0xA, \\n)", "", ISS_OFF); IUFillSwitch(&LxSerialAddeolS[3], "CR+LF", "", ISS_OFF); IUFillSwitchVector(&LxSerialAddeolSP, LxSerialAddeolS, NARRAY(LxSerialAddeolS), device_name, "Add EOL", "", LX_TAB, IP_RW, ISR_1OFMANY, 0, IPS_IDLE); FlashStrobeSP = nullptr; FlashStrobeStopSP = nullptr; ledmethod = PWCIOCTL; return true; } bool Lx::updateProperties() { if (dev->isConnected()) { INDI::Property *pfound; dev->defineSwitch(&LxEnableSP); dev->defineSwitch(&LxModeSP); dev->defineText(&LxPortTP); dev->defineSwitch(&LxSerialOptionSP); //dev->defineSwitch(&LxParallelOptionSP); dev->defineText(&LxStartStopCmdTP); dev->defineSwitch(&LxLogicalLevelSP); dev->defineSwitch(&LxSerialSpeedSP); dev->defineSwitch(&LxSerialSizeSP); dev->defineSwitch(&LxSerialParitySP); dev->defineSwitch(&LxSerialStopSP); dev->defineSwitch(&LxSerialAddeolSP); pfound = findbyLabel(dev, (char *)"Strobe"); if (pfound) { FlashStrobeSP = dev->getSwitch(pfound->getName()); pfound = findbyLabel(dev, (char *)"Stop Strobe"); FlashStrobeStopSP = dev->getSwitch(pfound->getName()); } } else { dev->deleteProperty(LxEnableSP.name); dev->deleteProperty(LxModeSP.name); dev->deleteProperty(LxPortTP.name); dev->deleteProperty(LxSerialOptionSP.name); //dev->deleteProperty(LxParallelOptionSP.name); dev->deleteProperty(LxStartStopCmdTP.name); dev->deleteProperty(LxLogicalLevelSP.name); dev->deleteProperty(LxSerialSpeedSP.name); dev->deleteProperty(LxSerialSizeSP.name); dev->deleteProperty(LxSerialParitySP.name); dev->deleteProperty(LxSerialStopSP.name); dev->deleteProperty(LxSerialAddeolSP.name); FlashStrobeSP = nullptr; FlashStrobeStopSP = nullptr; } return true; } bool Lx::ISNewSwitch(const char *devname, const char *name, ISState *states, char *names[], int n) { /* ignore if not ours */ if (devname && strcmp(device_name, devname)) return true; if (!strcmp(name, LxEnableSP.name)) { IUResetSwitch(&LxEnableSP); IUUpdateSwitch(&LxEnableSP, states, names, n); LxEnableSP.s = IPS_OK; IDSetSwitch(&LxEnableSP, "%s long exposure on device %s", (LxEnableS[0].s == ISS_ON ? "Disabling" : "Enabling"), device_name); return true; } if (!strcmp(name, LxModeSP.name)) { unsigned int index, oldindex; oldindex = IUFindOnSwitchIndex(&LxModeSP); IUResetSwitch(&LxModeSP); IUUpdateSwitch(&LxModeSP, states, names, n); LxModeSP.s = IPS_OK; index = IUFindOnSwitchIndex(&LxModeSP); if (index == LXLED) if (!checkPWC()) { IUResetSwitch(&LxModeSP); LxModeSP.s = IPS_ALERT; LxModeS[oldindex].s = ISS_ON; IDSetSwitch(&LxModeSP, "Can not set Lx Mode to %s", LxModeS[index].name); return false; } IDSetSwitch(&LxModeSP, "Setting Lx Mode to %s", LxModeS[index].name); return true; } if (!strcmp(name, LxSerialOptionSP.name)) { unsigned int index; IUResetSwitch(&LxSerialOptionSP); IUUpdateSwitch(&LxSerialOptionSP, states, names, n); LxSerialOptionSP.s = IPS_OK; index = IUFindOnSwitchIndex(&LxSerialOptionSP); IDSetSwitch(&LxSerialOptionSP, "Setting Lx Serial option: %s", LxSerialOptionS[index].name); return true; } if (!strcmp(name, LxParallelOptionSP.name)) { unsigned int index; IUResetSwitch(&LxParallelOptionSP); IUUpdateSwitch(&LxParallelOptionSP, states, names, n); LxParallelOptionSP.s = IPS_OK; index = IUFindOnSwitchIndex(&LxParallelOptionSP); IDSetSwitch(&LxParallelOptionSP, "Setting Lx Parallel option: %s", LxParallelOptionS[index].name); return true; } if (!strcmp(name, LxLogicalLevelSP.name)) { unsigned int index; IUResetSwitch(&LxLogicalLevelSP); IUUpdateSwitch(&LxLogicalLevelSP, states, names, n); LxLogicalLevelSP.s = IPS_OK; index = IUFindOnSwitchIndex(&LxLogicalLevelSP); IDSetSwitch(&LxLogicalLevelSP, "Setting Lx logical levels for start transition: %s", LxLogicalLevelS[index].name); return true; } if (!strcmp(name, LxSerialSpeedSP.name)) { unsigned int index; IUResetSwitch(&LxSerialSpeedSP); IUUpdateSwitch(&LxSerialSpeedSP, states, names, n); LxSerialSpeedSP.s = IPS_OK; index = IUFindOnSwitchIndex(&LxSerialSpeedSP); IDSetSwitch(&LxSerialSpeedSP, "Setting Lx serial speed: %s", LxSerialSpeedS[index].name); return true; } if (!strcmp(name, LxSerialSizeSP.name)) { unsigned int index; IUResetSwitch(&LxSerialSizeSP); IUUpdateSwitch(&LxSerialSizeSP, states, names, n); LxSerialSizeSP.s = IPS_OK; index = IUFindOnSwitchIndex(&LxSerialSizeSP); IDSetSwitch(&LxSerialSizeSP, "Setting Lx serial word size: %s", LxSerialSizeS[index].name); return true; } if (!strcmp(name, LxSerialParitySP.name)) { unsigned int index; IUResetSwitch(&LxSerialParitySP); IUUpdateSwitch(&LxSerialParitySP, states, names, n); LxSerialParitySP.s = IPS_OK; index = IUFindOnSwitchIndex(&LxSerialParitySP); IDSetSwitch(&LxSerialParitySP, "Setting Lx serial parity: %s", LxSerialParityS[index].name); return true; } if (!strcmp(name, LxSerialStopSP.name)) { unsigned int index; IUResetSwitch(&LxSerialStopSP); IUUpdateSwitch(&LxSerialStopSP, states, names, n); LxSerialStopSP.s = IPS_OK; index = IUFindOnSwitchIndex(&LxSerialStopSP); IDSetSwitch(&LxSerialStopSP, "Setting Lx serial stop bits: %s", LxSerialStopS[index].name); return true; } if (!strcmp(name, LxSerialAddeolSP.name)) { unsigned int index; IUResetSwitch(&LxSerialAddeolSP); IUUpdateSwitch(&LxSerialAddeolSP, states, names, n); LxSerialAddeolSP.s = IPS_OK; index = IUFindOnSwitchIndex(&LxSerialAddeolSP); IDSetSwitch(&LxSerialAddeolSP, "Setting Lx End of Line: %s", LxSerialAddeolS[index].name); return true; } return true; // not ours, don't care } bool Lx::ISNewText(const char *devname, const char *name, char *texts[], char *names[], int n) { IText *tp; /* ignore if not ours */ if (devname && strcmp(device_name, devname)) return true; if (!strcmp(name, LxPortTP.name)) { LxPortTP.s = IPS_OK; tp = IUFindText(&LxPortTP, names[0]); if (!tp) return false; IUSaveText(tp, texts[0]); IDSetText(&LxPortTP, "Setting Lx port to %s", tp->text); return true; } if (!strcmp(name, LxStartStopCmdTP.name)) { LxStartStopCmdTP.s = IPS_OK; for (int i = 0; i < n; i++) { tp = IUFindText(&LxStartStopCmdTP, names[i]); if (!tp) return false; IUSaveText(tp, texts[i]); } IDSetText(&LxStartStopCmdTP, "Setting Lx Start/stop commands"); return true; } return true; // not ours, don't care } unsigned int Lx::getLxmode() { return IUFindOnSwitchIndex(&LxModeSP); } bool Lx::startLx() { unsigned int index; IDMessage(device_name, "Starting Long Exposure"); index = IUFindOnSwitchIndex(&LxModeSP); switch (index) { case LXSERIAL: return startLxSerial(); case LXLED: return startLxPWC(); default: return false; } return false; } int Lx::stopLx() { unsigned int index = 0; IDMessage(device_name, "Stopping Long Exposure"); index = IUFindOnSwitchIndex(&LxModeSP); switch (index) { case LXSERIAL: return stopLxSerial(); case LXLED: return stopLxPWC(); default: return -1; } return 0; } // Serial Stuff void Lx::closeserial(int fd) { tcsetattr(fd, TCSANOW, &oldterminfo); if (close(fd) < 0) perror("closeserial()"); } int Lx::openserial(char *devicename) { int fd; struct termios attr; if ((fd = open(devicename, O_RDWR)) == -1) { IDLog("openserial(): open()"); return -1; } if (tcgetattr(fd, &oldterminfo) == -1) { IDLog("openserial(): tcgetattr()"); return -1; } attr = oldterminfo; //attr.c_cflag |= CRTSCTS | CLOCAL; attr.c_cflag |= CLOCAL; attr.c_oflag = 0; if (tcflush(fd, TCIOFLUSH) == -1) { IDLog("openserial(): tcflush()"); return -1; } if (tcsetattr(fd, TCSANOW, &attr) == -1) { IDLog("initserial(): tcsetattr()"); return -1; } return fd; } int Lx::setRTS(int fd, int level) { // int status; int mcr = 0; // does not work for RTS //if (ioctl(fd, TIOCMGET, &status) == -1) { // IDLog("setRTS(): TIOCMGET"); // return 0; //} //if (level) // status |= TIOCM_RTS; //else // status &= ~TIOCM_RTS; //if (ioctl(fd, TIOCMSET, &status) == -1) { // IDLog("setRTS(): TIOCMSET"); // return 0; //} mcr = TIOCM_RTS; if (level) { if (ioctl(fd, TIOCMBIS, &mcr) == -1) { IDLog("setRTS(): TIOCMBIS"); return 0; } } else { if (ioctl(fd, TIOCMBIC, &mcr) == -1) { IDLog("setRTS(): TIOCMBIC"); return 0; } } return 1; } int Lx::setDTR(int fd, int level) { int status; if (ioctl(fd, TIOCMGET, &status) == -1) { IDLog("setDTR(): TIOCMGET"); return 0; } if (level) status |= TIOCM_DTR; else status &= ~TIOCM_DTR; if (ioctl(fd, TIOCMSET, &status) == -1) { IDLog("setDTR(): TIOCMSET"); return 0; } return 1; } void Lx::getSerialOptions(unsigned int *speed, unsigned int *wordsize, unsigned int *parity, unsigned int *stops) { unsigned int index; unsigned int speedopts[] = { 1200, 2400, 4800, 9600, 19200, 38400, 57600, 115200, 230400 }; unsigned int sizeopts[] = { 5, 6, 7, 8 }; unsigned int parityopts[] = { PARITY_NONE, PARITY_EVEN, PARITY_ODD }; unsigned int stopopts[] = { 1, 2 }; index = IUFindOnSwitchIndex(&LxSerialSpeedSP); *speed = speedopts[index]; index = IUFindOnSwitchIndex(&LxSerialSizeSP); *wordsize = sizeopts[index]; index = IUFindOnSwitchIndex(&LxSerialParitySP); *parity = parityopts[index]; index = IUFindOnSwitchIndex(&LxSerialStopSP); *stops = stopopts[index]; } const char *Lx::getSerialEOL() { unsigned int index; index = IUFindOnSwitchIndex(&LxSerialAddeolSP); switch (index) { case 0: return ""; case 1: return "\r"; case 2: return "\n"; case 3: return "\r\n"; } return nullptr; } bool Lx::startLxSerial() { unsigned int speed = 0, wordsize = 0, parity = 0, stops = 0; const char *eol = nullptr; unsigned int index = IUFindOnSwitchIndex(&LxSerialOptionSP); int ret = 0; switch (index) { case 0: serialfd = openserial(LxPortT[0].text); if (serialfd < 0) return false; if (LxLogicalLevelS[0].s == ISS_ON) setDTR(serialfd, 1); else setDTR(serialfd, 0); break; case 1: serialfd = openserial(LxPortT[0].text); if (serialfd < 0) return false; if (LxLogicalLevelS[0].s == ISS_ON) setRTS(serialfd, 1); else setRTS(serialfd, 0); break; case 2: getSerialOptions(&speed, &wordsize, &parity, &stops); eol = getSerialEOL(); tty_connect(LxPortT[0].text, speed, wordsize, parity, stops, &serialfd); if (serialfd < 0) return false; ret = write(serialfd, LxStartStopCmdT[0].text, strlen(LxStartStopCmdT[0].text)); ret = write(serialfd, eol, strlen(eol)); break; } return true; } int Lx::stopLxSerial() { int ret = 0; const char *eol = nullptr; unsigned int index = IUFindOnSwitchIndex(&LxSerialOptionSP); switch (index) { case 0: if (LxLogicalLevelS[0].s == ISS_ON) setDTR(serialfd, 0); else setDTR(serialfd, 1); break; case 1: if (LxLogicalLevelS[0].s == ISS_ON) setRTS(serialfd, 0); else setRTS(serialfd, 1); break; case 2: ret = write(serialfd, LxStartStopCmdT[1].text, strlen(LxStartStopCmdT[1].text)); eol = getSerialEOL(); ret = write(serialfd, eol, strlen(eol)); break; } close(serialfd); return 0; } INDI::Property *Lx::findbyLabel(INDI::DefaultDevice *dev, char *label) { std::vector *allprops = dev->getProperties(); for (std::vector::iterator it = allprops->begin(); it != allprops->end(); ++it) { if (!(strcmp((*it)->getLabel(), label))) return *it; } return nullptr; } // PWC Stuff bool Lx::checkPWC() { if (FlashStrobeSP && FlashStrobeStopSP) { IDMessage(device_name, "Using Flash control for led Lx Mode"); ledmethod = FLASHLED; return true; } if (ioctl(camerafd, VIDIOCPWCPROBE, &probe) != 0) { IDMessage(device_name, "ERROR: device does not support PWC ioctl"); return false; } if (probe.type < 730) { IDMessage(device_name, "ERROR: camera type %d does not support led control", probe.type); return false; } IDMessage(device_name, "Using PWC ioctl for led Lx Mode"); return true; } void Lx::pwcsetLed(int on, int off) { struct pwc_leds leds; leds.led_on = on; leds.led_off = off; if (ioctl(camerafd, VIDIOCPWCSLED, &leds)) { IDLog("ioctl: can't set Led.\n"); } } void Lx::pwcsetflashon() { ISState states[2] = { ISS_ON, ISS_OFF }; const char *names[2] = { FlashStrobeSP->sp[0].name, FlashStrobeStopSP->sp[0].name }; dev->ISNewSwitch(device_name, FlashStrobeSP->name, &(states[0]), (char **)names, 1); //dev->ISNewSwitch(device_name, FlashStrobeStopSP->name, &(states[1]), (char **)(names + 1), 1); FlashStrobeSP->s = IPS_OK; IDSetSwitch(FlashStrobeSP, nullptr); FlashStrobeStopSP->s = IPS_IDLE; IDSetSwitch(FlashStrobeStopSP, nullptr); } void Lx::pwcsetflashoff() { ISState states[2] = { ISS_OFF, ISS_ON }; const char *names[2] = { FlashStrobeSP->sp[0].name, FlashStrobeStopSP->sp[0].name }; //dev->ISNewSwitch(device_name, FlashStrobeSP->name, &(states[0]), (char **)names, 1); dev->ISNewSwitch(device_name, FlashStrobeStopSP->name, &(states[1]), (char **)(names + 1), 1); FlashStrobeStopSP->s = IPS_OK; IDSetSwitch(FlashStrobeStopSP, nullptr); FlashStrobeSP->s = IPS_IDLE; IDSetSwitch(FlashStrobeSP, nullptr); } bool Lx::startLxPWC() { switch (ledmethod) { case PWCIOCTL: if (LxLogicalLevelS[0].s == ISS_ON) pwcsetLed(25500, 0); else pwcsetLed(0, 25500); return true; case FLASHLED: if (LxLogicalLevelS[0].s == ISS_ON) pwcsetflashon(); else pwcsetflashoff(); return true; } return false; } int Lx::stopLxPWC() { switch (ledmethod) { case PWCIOCTL: if (LxLogicalLevelS[0].s == ISS_ON) pwcsetLed(0, 25500); else pwcsetLed(25500, 0); return 0; case FLASHLED: if (LxLogicalLevelS[0].s == ISS_ON) pwcsetflashoff(); else pwcsetflashon(); return 0; } return -1; } libindi/libs/lx/Lx.h0000664000175000017500000000471613263645557013612 0ustar jasemjasem #pragma once #include "defaultdevice.h" //For SPC900 Led control #include "webcam/pwc-ioctl.h" //For serial control #include #define LX_TAB "Long Exposure" // LX Modes #define LXSERIAL 0 #define LXLED 1 #define LXPARALLEL 2 #define LXGPIO 3 #define LXMODENUM 2 class Lx { public: ISwitch LxEnableS[2]; ISwitchVectorProperty LxEnableSP; ISwitch LxModeS[LXMODENUM]; ISwitchVectorProperty LxModeSP; IText LxPortT[1]; ITextVectorProperty LxPortTP; ISwitch LxSerialOptionS[3]; ISwitchVectorProperty LxSerialOptionSP; ISwitch LxParallelOptionS[9]; ISwitchVectorProperty LxParallelOptionSP; IText LxStartStopCmdT[2]; ITextVectorProperty LxStartStopCmdTP; ISwitch LxLogicalLevelS[2]; ISwitchVectorProperty LxLogicalLevelSP; ISwitch LxSerialSpeedS[9]; ISwitchVectorProperty LxSerialSpeedSP; ISwitch LxSerialSizeS[4]; ISwitchVectorProperty LxSerialSizeSP; ISwitch LxSerialParityS[3]; ISwitchVectorProperty LxSerialParitySP; ISwitch LxSerialStopS[2]; ISwitchVectorProperty LxSerialStopSP; ISwitch LxSerialAddeolS[4]; ISwitchVectorProperty LxSerialAddeolSP; bool isEnabled(); void setCamerafd(int fd); bool initProperties(INDI::DefaultDevice *device); bool ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n); bool ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n); bool updateProperties(); bool startLx(); int stopLx(); unsigned int getLxmode(); private: INDI::DefaultDevice *dev; const char *device_name; int camerafd; // Serial int serialfd; struct termios oldterminfo; void closeserial(int fd); int openserial(char *devicename); int setRTS(int fd, int level); int setDTR(int fd, int level); bool startLxSerial(); int stopLxSerial(); void getSerialOptions(unsigned int *speed, unsigned int *wordsize, unsigned int *parity, unsigned int *stops); const char *getSerialEOL(); INDI::Property *findbyLabel(INDI::DefaultDevice *dev, char *label); // PWC Cameras ISwitchVectorProperty *FlashStrobeSP; ISwitchVectorProperty *FlashStrobeStopSP; enum pwcledmethod { PWCIOCTL, FLASHLED }; char ledmethod; struct pwc_probe probe; bool checkPWC(); void pwcsetLed(int on, int off); void pwcsetflashon(); void pwcsetflashoff(); bool startLxPWC(); int stopLxPWC(); }; libindi/Doxyfile0000664000175000017500000031262213263645557013206 0ustar jasemjasem# Doxyfile 1.8.8 # This file describes the settings to be used by the documentation system # doxygen (www.doxygen.org) for a project. # # All text after a double hash (##) is considered a comment and is placed in # front of the TAG it is preceding. # # All text after a single hash (#) is considered a comment and will be ignored. # The format is: # TAG = value [value, ...] # For lists, items can also be appended using: # TAG += value [value, ...] # Values that contain spaces should be placed between quotes (\" \"). #--------------------------------------------------------------------------- # Project related configuration options #--------------------------------------------------------------------------- # This tag specifies the encoding used for all characters in the config file # that follow. The default is UTF-8 which is also the encoding used for all text # before the first occurrence of this tag. Doxygen uses libiconv (or the iconv # built into libc) for the transcoding. See http://www.gnu.org/software/libiconv # for the list of possible encodings. # The default value is: UTF-8. DOXYFILE_ENCODING = UTF-8 # The PROJECT_NAME tag is a single word (or a sequence of words surrounded by # double-quotes, unless you are using Doxywizard) that should identify the # project for which the documentation is generated. This name is used in the # title of most generated pages and in a few other places. # The default value is: My Project. PROJECT_NAME = "Instrument Neutral Distributed Interface INDI" # The PROJECT_NUMBER tag can be used to enter a project or revision number. This # could be handy for archiving the generated documentation or if some version # control system is used. PROJECT_NUMBER = 1.7.1 # Using the PROJECT_BRIEF tag one can provide an optional one line description # for a project that appears at the top of each page and should give viewer a # quick idea about the purpose of the project. Keep the description short. PROJECT_BRIEF = # With the PROJECT_LOGO tag one can specify an logo or icon that is included in # the documentation. The maximum height of the logo should not exceed 55 pixels # and the maximum width should not exceed 200 pixels. Doxygen will copy the logo # to the output directory. PROJECT_LOGO = # The OUTPUT_DIRECTORY tag is used to specify the (relative or absolute) path # into which the generated documentation will be written. If a relative path is # entered, it will be relative to the location where doxygen was started. If # left blank the current directory will be used. OUTPUT_DIRECTORY = /home/jasem/Projects/doc # If the CREATE_SUBDIRS tag is set to YES, then doxygen will create 4096 sub- # directories (in 2 levels) under the output directory of each output format and # will distribute the generated files over these directories. Enabling this # option can be useful when feeding doxygen a huge amount of source files, where # putting all generated files in the same directory would otherwise causes # performance problems for the file system. # The default value is: NO. CREATE_SUBDIRS = NO # If the ALLOW_UNICODE_NAMES tag is set to YES, doxygen will allow non-ASCII # characters to appear in the names of generated files. If set to NO, non-ASCII # characters will be escaped, for example _xE3_x81_x84 will be used for Unicode # U+3044. # The default value is: NO. ALLOW_UNICODE_NAMES = NO # The OUTPUT_LANGUAGE tag is used to specify the language in which all # documentation generated by doxygen is written. Doxygen will use this # information to generate all constant output in the proper language. # Possible values are: Afrikaans, Arabic, Armenian, Brazilian, Catalan, Chinese, # Chinese-Traditional, Croatian, Czech, Danish, Dutch, English (United States), # Esperanto, Farsi (Persian), Finnish, French, German, Greek, Hungarian, # Indonesian, Italian, Japanese, Japanese-en (Japanese with English messages), # Korean, Korean-en (Korean with English messages), Latvian, Lithuanian, # Macedonian, Norwegian, Persian (Farsi), Polish, Portuguese, Romanian, Russian, # Serbian, Serbian-Cyrillic, Slovak, Slovene, Spanish, Swedish, Turkish, # Ukrainian and Vietnamese. # The default value is: English. OUTPUT_LANGUAGE = English # If the BRIEF_MEMBER_DESC tag is set to YES doxygen will include brief member # descriptions after the members that are listed in the file and class # documentation (similar to Javadoc). Set to NO to disable this. # The default value is: YES. BRIEF_MEMBER_DESC = YES # If the REPEAT_BRIEF tag is set to YES doxygen will prepend the brief # description of a member or function before the detailed description # # Note: If both HIDE_UNDOC_MEMBERS and BRIEF_MEMBER_DESC are set to NO, the # brief descriptions will be completely suppressed. # The default value is: YES. REPEAT_BRIEF = YES # This tag implements a quasi-intelligent brief description abbreviator that is # used to form the text in various listings. Each string in this list, if found # as the leading text of the brief description, will be stripped from the text # and the result, after processing the whole list, is used as the annotated # text. Otherwise, the brief description is used as-is. If left blank, the # following values are used ($name is automatically replaced with the name of # the entity):The $name class, The $name widget, The $name file, is, provides, # specifies, contains, represents, a, an and the. ABBREVIATE_BRIEF = # If the ALWAYS_DETAILED_SEC and REPEAT_BRIEF tags are both set to YES then # doxygen will generate a detailed section even if there is only a brief # description. # The default value is: NO. ALWAYS_DETAILED_SEC = NO # If the INLINE_INHERITED_MEMB tag is set to YES, doxygen will show all # inherited members of a class in the documentation of that class as if those # members were ordinary class members. Constructors, destructors and assignment # operators of the base classes will not be shown. # The default value is: NO. INLINE_INHERITED_MEMB = NO # If the FULL_PATH_NAMES tag is set to YES doxygen will prepend the full path # before files name in the file list and in the header files. If set to NO the # shortest path that makes the file name unique will be used # The default value is: YES. FULL_PATH_NAMES = NO # The STRIP_FROM_PATH tag can be used to strip a user-defined part of the path. # Stripping is only done if one of the specified strings matches the left-hand # part of the path. The tag can be used to show relative paths in the file list. # If left blank the directory from which doxygen is run is used as the path to # strip. # # Note that you can specify absolute paths here, but also relative paths, which # will be relative from the directory where doxygen is started. # This tag requires that the tag FULL_PATH_NAMES is set to YES. STRIP_FROM_PATH = # The STRIP_FROM_INC_PATH tag can be used to strip a user-defined part of the # path mentioned in the documentation of a class, which tells the reader which # header file to include in order to use a class. If left blank only the name of # the header file containing the class definition is used. Otherwise one should # specify the list of include paths that are normally passed to the compiler # using the -I flag. STRIP_FROM_INC_PATH = # If the SHORT_NAMES tag is set to YES, doxygen will generate much shorter (but # less readable) file names. This can be useful is your file systems doesn't # support long names like on DOS, Mac, or CD-ROM. # The default value is: NO. SHORT_NAMES = NO # If the JAVADOC_AUTOBRIEF tag is set to YES then doxygen will interpret the # first line (until the first dot) of a Javadoc-style comment as the brief # description. If set to NO, the Javadoc-style will behave just like regular Qt- # style comments (thus requiring an explicit @brief command for a brief # description.) # The default value is: NO. JAVADOC_AUTOBRIEF = NO # If the QT_AUTOBRIEF tag is set to YES then doxygen will interpret the first # line (until the first dot) of a Qt-style comment as the brief description. If # set to NO, the Qt-style will behave just like regular Qt-style comments (thus # requiring an explicit \brief command for a brief description.) # The default value is: NO. QT_AUTOBRIEF = NO # The MULTILINE_CPP_IS_BRIEF tag can be set to YES to make doxygen treat a # multi-line C++ special comment block (i.e. a block of //! or /// comments) as # a brief description. This used to be the default behavior. The new default is # to treat a multi-line C++ comment block as a detailed description. Set this # tag to YES if you prefer the old behavior instead. # # Note that setting this tag to YES also means that rational rose comments are # not recognized any more. # The default value is: NO. MULTILINE_CPP_IS_BRIEF = NO # If the INHERIT_DOCS tag is set to YES then an undocumented member inherits the # documentation from any documented member that it re-implements. # The default value is: YES. INHERIT_DOCS = YES # If the SEPARATE_MEMBER_PAGES tag is set to YES, then doxygen will produce a # new page for each member. If set to NO, the documentation of a member will be # part of the file/class/namespace that contains it. # The default value is: NO. SEPARATE_MEMBER_PAGES = NO # The TAB_SIZE tag can be used to set the number of spaces in a tab. Doxygen # uses this value to replace tabs by spaces in code fragments. # Minimum value: 1, maximum value: 16, default value: 4. TAB_SIZE = 8 # This tag can be used to specify a number of aliases that act as commands in # the documentation. An alias has the form: # name=value # For example adding # "sideeffect=@par Side Effects:\n" # will allow you to put the command \sideeffect (or @sideeffect) in the # documentation, which will result in a user-defined paragraph with heading # "Side Effects:". You can put \n's in the value part of an alias to insert # newlines. ALIASES = # This tag can be used to specify a number of word-keyword mappings (TCL only). # A mapping has the form "name=value". For example adding "class=itcl::class" # will allow you to use the command class in the itcl::class meaning. TCL_SUBST = # Set the OPTIMIZE_OUTPUT_FOR_C tag to YES if your project consists of C sources # only. Doxygen will then generate output that is more tailored for C. For # instance, some of the names that are used will be different. The list of all # members will be omitted, etc. # The default value is: NO. OPTIMIZE_OUTPUT_FOR_C = NO # Set the OPTIMIZE_OUTPUT_JAVA tag to YES if your project consists of Java or # Python sources only. Doxygen will then generate output that is more tailored # for that language. For instance, namespaces will be presented as packages, # qualified scopes will look different, etc. # The default value is: NO. OPTIMIZE_OUTPUT_JAVA = NO # Set the OPTIMIZE_FOR_FORTRAN tag to YES if your project consists of Fortran # sources. Doxygen will then generate output that is tailored for Fortran. # The default value is: NO. OPTIMIZE_FOR_FORTRAN = NO # Set the OPTIMIZE_OUTPUT_VHDL tag to YES if your project consists of VHDL # sources. Doxygen will then generate output that is tailored for VHDL. # The default value is: NO. OPTIMIZE_OUTPUT_VHDL = NO # Doxygen selects the parser to use depending on the extension of the files it # parses. With this tag you can assign which parser to use for a given # extension. Doxygen has a built-in mapping, but you can override or extend it # using this tag. The format is ext=language, where ext is a file extension, and # language is one of the parsers supported by doxygen: IDL, Java, Javascript, # C#, C, C++, D, PHP, Objective-C, Python, Fortran (fixed format Fortran: # FortranFixed, free formatted Fortran: FortranFree, unknown formatted Fortran: # Fortran. In the later case the parser tries to guess whether the code is fixed # or free formatted code, this is the default for Fortran type files), VHDL. For # instance to make doxygen treat .inc files as Fortran files (default is PHP), # and .f files as C (default is Fortran), use: inc=Fortran f=C. # # Note For files without extension you can use no_extension as a placeholder. # # Note that for custom extensions you also need to set FILE_PATTERNS otherwise # the files are not read by doxygen. EXTENSION_MAPPING = # If the MARKDOWN_SUPPORT tag is enabled then doxygen pre-processes all comments # according to the Markdown format, which allows for more readable # documentation. See http://daringfireball.net/projects/markdown/ for details. # The output of markdown processing is further processed by doxygen, so you can # mix doxygen, HTML, and XML commands with Markdown formatting. Disable only in # case of backward compatibilities issues. # The default value is: YES. MARKDOWN_SUPPORT = YES # When enabled doxygen tries to link words that correspond to documented # classes, or namespaces to their corresponding documentation. Such a link can # be prevented in individual cases by by putting a % sign in front of the word # or globally by setting AUTOLINK_SUPPORT to NO. # The default value is: YES. AUTOLINK_SUPPORT = YES # If you use STL classes (i.e. std::string, std::vector, etc.) but do not want # to include (a tag file for) the STL sources as input, then you should set this # tag to YES in order to let doxygen match functions declarations and # definitions whose arguments contain STL classes (e.g. func(std::string); # versus func(std::string) {}). This also make the inheritance and collaboration # diagrams that involve STL classes more complete and accurate. # The default value is: NO. BUILTIN_STL_SUPPORT = NO # If you use Microsoft's C++/CLI language, you should set this option to YES to # enable parsing support. # The default value is: NO. CPP_CLI_SUPPORT = NO # Set the SIP_SUPPORT tag to YES if your project consists of sip (see: # http://www.riverbankcomputing.co.uk/software/sip/intro) sources only. Doxygen # will parse them like normal C++ but will assume all classes use public instead # of private inheritance when no explicit protection keyword is present. # The default value is: NO. SIP_SUPPORT = NO # For Microsoft's IDL there are propget and propput attributes to indicate # getter and setter methods for a property. Setting this option to YES will make # doxygen to replace the get and set methods by a property in the documentation. # This will only work if the methods are indeed getting or setting a simple # type. If this is not the case, or you want to show the methods anyway, you # should set this option to NO. # The default value is: YES. IDL_PROPERTY_SUPPORT = YES # If member grouping is used in the documentation and the DISTRIBUTE_GROUP_DOC # tag is set to YES, then doxygen will reuse the documentation of the first # member in the group (if any) for the other members of the group. By default # all members of a group must be documented explicitly. # The default value is: NO. DISTRIBUTE_GROUP_DOC = NO # Set the SUBGROUPING tag to YES to allow class member groups of the same type # (for instance a group of public functions) to be put as a subgroup of that # type (e.g. under the Public Functions section). Set it to NO to prevent # subgrouping. Alternatively, this can be done per class using the # \nosubgrouping command. # The default value is: YES. SUBGROUPING = YES # When the INLINE_GROUPED_CLASSES tag is set to YES, classes, structs and unions # are shown inside the group in which they are included (e.g. using \ingroup) # instead of on a separate page (for HTML and Man pages) or section (for LaTeX # and RTF). # # Note that this feature does not work in combination with # SEPARATE_MEMBER_PAGES. # The default value is: NO. INLINE_GROUPED_CLASSES = NO # When the INLINE_SIMPLE_STRUCTS tag is set to YES, structs, classes, and unions # with only public data fields or simple typedef fields will be shown inline in # the documentation of the scope in which they are defined (i.e. file, # namespace, or group documentation), provided this scope is documented. If set # to NO, structs, classes, and unions are shown on a separate page (for HTML and # Man pages) or section (for LaTeX and RTF). # The default value is: NO. INLINE_SIMPLE_STRUCTS = NO # When TYPEDEF_HIDES_STRUCT tag is enabled, a typedef of a struct, union, or # enum is documented as struct, union, or enum with the name of the typedef. So # typedef struct TypeS {} TypeT, will appear in the documentation as a struct # with name TypeT. When disabled the typedef will appear as a member of a file, # namespace, or class. And the struct will be named TypeS. This can typically be # useful for C code in case the coding convention dictates that all compound # types are typedef'ed and only the typedef is referenced, never the tag name. # The default value is: NO. TYPEDEF_HIDES_STRUCT = NO # The size of the symbol lookup cache can be set using LOOKUP_CACHE_SIZE. This # cache is used to resolve symbols given their name and scope. Since this can be # an expensive process and often the same symbol appears multiple times in the # code, doxygen keeps a cache of pre-resolved symbols. If the cache is too small # doxygen will become slower. If the cache is too large, memory is wasted. The # cache size is given by this formula: 2^(16+LOOKUP_CACHE_SIZE). The valid range # is 0..9, the default is 0, corresponding to a cache size of 2^16=65536 # symbols. At the end of a run doxygen will report the cache usage and suggest # the optimal cache size from a speed point of view. # Minimum value: 0, maximum value: 9, default value: 0. LOOKUP_CACHE_SIZE = 0 #--------------------------------------------------------------------------- # Build related configuration options #--------------------------------------------------------------------------- # If the EXTRACT_ALL tag is set to YES doxygen will assume all entities in # documentation are documented, even if no documentation was available. Private # class members and static file members will be hidden unless the # EXTRACT_PRIVATE respectively EXTRACT_STATIC tags are set to YES. # Note: This will also disable the warnings about undocumented members that are # normally produced when WARNINGS is set to YES. # The default value is: NO. EXTRACT_ALL = YES # If the EXTRACT_PRIVATE tag is set to YES all private members of a class will # be included in the documentation. # The default value is: NO. EXTRACT_PRIVATE = NO # If the EXTRACT_PACKAGE tag is set to YES all members with package or internal # scope will be included in the documentation. # The default value is: NO. EXTRACT_PACKAGE = NO # If the EXTRACT_STATIC tag is set to YES all static members of a file will be # included in the documentation. # The default value is: NO. EXTRACT_STATIC = NO # If the EXTRACT_LOCAL_CLASSES tag is set to YES classes (and structs) defined # locally in source files will be included in the documentation. If set to NO # only classes defined in header files are included. Does not have any effect # for Java sources. # The default value is: YES. EXTRACT_LOCAL_CLASSES = YES # This flag is only useful for Objective-C code. When set to YES local methods, # which are defined in the implementation section but not in the interface are # included in the documentation. If set to NO only methods in the interface are # included. # The default value is: NO. EXTRACT_LOCAL_METHODS = NO # If this flag is set to YES, the members of anonymous namespaces will be # extracted and appear in the documentation as a namespace called # 'anonymous_namespace{file}', where file will be replaced with the base name of # the file that contains the anonymous namespace. By default anonymous namespace # are hidden. # The default value is: NO. EXTRACT_ANON_NSPACES = NO # If the HIDE_UNDOC_MEMBERS tag is set to YES, doxygen will hide all # undocumented members inside documented classes or files. If set to NO these # members will be included in the various overviews, but no documentation # section is generated. This option has no effect if EXTRACT_ALL is enabled. # The default value is: NO. HIDE_UNDOC_MEMBERS = YES # If the HIDE_UNDOC_CLASSES tag is set to YES, doxygen will hide all # undocumented classes that are normally visible in the class hierarchy. If set # to NO these classes will be included in the various overviews. This option has # no effect if EXTRACT_ALL is enabled. # The default value is: NO. HIDE_UNDOC_CLASSES = YES # If the HIDE_FRIEND_COMPOUNDS tag is set to YES, doxygen will hide all friend # (class|struct|union) declarations. If set to NO these declarations will be # included in the documentation. # The default value is: NO. HIDE_FRIEND_COMPOUNDS = NO # If the HIDE_IN_BODY_DOCS tag is set to YES, doxygen will hide any # documentation blocks found inside the body of a function. If set to NO these # blocks will be appended to the function's detailed documentation block. # The default value is: NO. HIDE_IN_BODY_DOCS = NO # The INTERNAL_DOCS tag determines if documentation that is typed after a # \internal command is included. If the tag is set to NO then the documentation # will be excluded. Set it to YES to include the internal documentation. # The default value is: NO. INTERNAL_DOCS = NO # If the CASE_SENSE_NAMES tag is set to NO then doxygen will only generate file # names in lower-case letters. If set to YES upper-case letters are also # allowed. This is useful if you have classes or files whose names only differ # in case and if your file system supports case sensitive file names. Windows # and Mac users are advised to set this option to NO. # The default value is: system dependent. CASE_SENSE_NAMES = YES # If the HIDE_SCOPE_NAMES tag is set to NO then doxygen will show members with # their full class and namespace scopes in the documentation. If set to YES the # scope will be hidden. # The default value is: NO. HIDE_SCOPE_NAMES = NO # If the SHOW_INCLUDE_FILES tag is set to YES then doxygen will put a list of # the files that are included by a file in the documentation of that file. # The default value is: YES. SHOW_INCLUDE_FILES = YES # If the SHOW_GROUPED_MEMB_INC tag is set to YES then Doxygen will add for each # grouped member an include statement to the documentation, telling the reader # which file to include in order to use the member. # The default value is: NO. SHOW_GROUPED_MEMB_INC = NO # If the FORCE_LOCAL_INCLUDES tag is set to YES then doxygen will list include # files with double quotes in the documentation rather than with sharp brackets. # The default value is: NO. FORCE_LOCAL_INCLUDES = NO # If the INLINE_INFO tag is set to YES then a tag [inline] is inserted in the # documentation for inline members. # The default value is: YES. INLINE_INFO = YES # If the SORT_MEMBER_DOCS tag is set to YES then doxygen will sort the # (detailed) documentation of file and class members alphabetically by member # name. If set to NO the members will appear in declaration order. # The default value is: YES. SORT_MEMBER_DOCS = YES # If the SORT_BRIEF_DOCS tag is set to YES then doxygen will sort the brief # descriptions of file, namespace and class members alphabetically by member # name. If set to NO the members will appear in declaration order. Note that # this will also influence the order of the classes in the class list. # The default value is: NO. SORT_BRIEF_DOCS = NO # If the SORT_MEMBERS_CTORS_1ST tag is set to YES then doxygen will sort the # (brief and detailed) documentation of class members so that constructors and # destructors are listed first. If set to NO the constructors will appear in the # respective orders defined by SORT_BRIEF_DOCS and SORT_MEMBER_DOCS. # Note: If SORT_BRIEF_DOCS is set to NO this option is ignored for sorting brief # member documentation. # Note: If SORT_MEMBER_DOCS is set to NO this option is ignored for sorting # detailed member documentation. # The default value is: NO. SORT_MEMBERS_CTORS_1ST = NO # If the SORT_GROUP_NAMES tag is set to YES then doxygen will sort the hierarchy # of group names into alphabetical order. If set to NO the group names will # appear in their defined order. # The default value is: NO. SORT_GROUP_NAMES = NO # If the SORT_BY_SCOPE_NAME tag is set to YES, the class list will be sorted by # fully-qualified names, including namespaces. If set to NO, the class list will # be sorted only by class name, not including the namespace part. # Note: This option is not very useful if HIDE_SCOPE_NAMES is set to YES. # Note: This option applies only to the class list, not to the alphabetical # list. # The default value is: NO. SORT_BY_SCOPE_NAME = NO # If the STRICT_PROTO_MATCHING option is enabled and doxygen fails to do proper # type resolution of all parameters of a function it will reject a match between # the prototype and the implementation of a member function even if there is # only one candidate or it is obvious which candidate to choose by doing a # simple string match. By disabling STRICT_PROTO_MATCHING doxygen will still # accept a match between prototype and implementation in such cases. # The default value is: NO. STRICT_PROTO_MATCHING = NO # The GENERATE_TODOLIST tag can be used to enable ( YES) or disable ( NO) the # todo list. This list is created by putting \todo commands in the # documentation. # The default value is: YES. GENERATE_TODOLIST = YES # The GENERATE_TESTLIST tag can be used to enable ( YES) or disable ( NO) the # test list. This list is created by putting \test commands in the # documentation. # The default value is: YES. GENERATE_TESTLIST = YES # The GENERATE_BUGLIST tag can be used to enable ( YES) or disable ( NO) the bug # list. This list is created by putting \bug commands in the documentation. # The default value is: YES. GENERATE_BUGLIST = YES # The GENERATE_DEPRECATEDLIST tag can be used to enable ( YES) or disable ( NO) # the deprecated list. This list is created by putting \deprecated commands in # the documentation. # The default value is: YES. GENERATE_DEPRECATEDLIST= YES # The ENABLED_SECTIONS tag can be used to enable conditional documentation # sections, marked by \if ... \endif and \cond # ... \endcond blocks. ENABLED_SECTIONS = # The MAX_INITIALIZER_LINES tag determines the maximum number of lines that the # initial value of a variable or macro / define can have for it to appear in the # documentation. If the initializer consists of more lines than specified here # it will be hidden. Use a value of 0 to hide initializers completely. The # appearance of the value of individual variables and macros / defines can be # controlled using \showinitializer or \hideinitializer command in the # documentation regardless of this setting. # Minimum value: 0, maximum value: 10000, default value: 30. MAX_INITIALIZER_LINES = 30 # Set the SHOW_USED_FILES tag to NO to disable the list of files generated at # the bottom of the documentation of classes and structs. If set to YES the list # will mention the files that were used to generate the documentation. # The default value is: YES. SHOW_USED_FILES = YES # Set the SHOW_FILES tag to NO to disable the generation of the Files page. This # will remove the Files entry from the Quick Index and from the Folder Tree View # (if specified). # The default value is: YES. SHOW_FILES = YES # Set the SHOW_NAMESPACES tag to NO to disable the generation of the Namespaces # page. This will remove the Namespaces entry from the Quick Index and from the # Folder Tree View (if specified). # The default value is: YES. SHOW_NAMESPACES = YES # The FILE_VERSION_FILTER tag can be used to specify a program or script that # doxygen should invoke to get the current version for each file (typically from # the version control system). Doxygen will invoke the program by executing (via # popen()) the command command input-file, where command is the value of the # FILE_VERSION_FILTER tag, and input-file is the name of an input file provided # by doxygen. Whatever the program writes to standard output is used as the file # version. For an example see the documentation. FILE_VERSION_FILTER = # The LAYOUT_FILE tag can be used to specify a layout file which will be parsed # by doxygen. The layout file controls the global structure of the generated # output files in an output format independent way. To create the layout file # that represents doxygen's defaults, run doxygen with the -l option. You can # optionally specify a file name after the option, if omitted DoxygenLayout.xml # will be used as the name of the layout file. # # Note that if you run doxygen from a directory containing a file called # DoxygenLayout.xml, doxygen will parse it automatically even if the LAYOUT_FILE # tag is left empty. LAYOUT_FILE = # The CITE_BIB_FILES tag can be used to specify one or more bib files containing # the reference definitions. This must be a list of .bib files. The .bib # extension is automatically appended if omitted. This requires the bibtex tool # to be installed. See also http://en.wikipedia.org/wiki/BibTeX for more info. # For LaTeX the style of the bibliography can be controlled using # LATEX_BIB_STYLE. To use this feature you need bibtex and perl available in the # search path. See also \cite for info how to create references. CITE_BIB_FILES = #--------------------------------------------------------------------------- # Configuration options related to warning and progress messages #--------------------------------------------------------------------------- # The QUIET tag can be used to turn on/off the messages that are generated to # standard output by doxygen. If QUIET is set to YES this implies that the # messages are off. # The default value is: NO. QUIET = NO # The WARNINGS tag can be used to turn on/off the warning messages that are # generated to standard error ( stderr) by doxygen. If WARNINGS is set to YES # this implies that the warnings are on. # # Tip: Turn warnings on while writing the documentation. # The default value is: YES. WARNINGS = YES # If the WARN_IF_UNDOCUMENTED tag is set to YES, then doxygen will generate # warnings for undocumented members. If EXTRACT_ALL is set to YES then this flag # will automatically be disabled. # The default value is: YES. WARN_IF_UNDOCUMENTED = YES # If the WARN_IF_DOC_ERROR tag is set to YES, doxygen will generate warnings for # potential errors in the documentation, such as not documenting some parameters # in a documented function, or documenting parameters that don't exist or using # markup commands wrongly. # The default value is: YES. WARN_IF_DOC_ERROR = YES # This WARN_NO_PARAMDOC option can be enabled to get warnings for functions that # are documented, but have no documentation for their parameters or return # value. If set to NO doxygen will only warn about wrong or incomplete parameter # documentation, but not about the absence of documentation. # The default value is: NO. WARN_NO_PARAMDOC = NO # The WARN_FORMAT tag determines the format of the warning messages that doxygen # can produce. The string should contain the $file, $line, and $text tags, which # will be replaced by the file and line number from which the warning originated # and the warning text. Optionally the format may contain $version, which will # be replaced by the version of the file (if it could be obtained via # FILE_VERSION_FILTER) # The default value is: $file:$line: $text. WARN_FORMAT = "$file:$line: $text" # The WARN_LOGFILE tag can be used to specify a file to which warning and error # messages should be written. If left blank the output is written to standard # error (stderr). WARN_LOGFILE = doxygen_warnings.txt #--------------------------------------------------------------------------- # Configuration options related to the input files #--------------------------------------------------------------------------- # The INPUT tag is used to specify the files and/or directories that contain # documented source files. You may enter file names like myfile.cpp or # directories like /usr/src/myproject. Separate the files or directories with # spaces. # Note: If this tag is empty the current directory is searched. INPUT = # This tag can be used to specify the character encoding of the source files # that doxygen parses. Internally doxygen uses the UTF-8 encoding. Doxygen uses # libiconv (or the iconv built into libc) for the transcoding. See the libiconv # documentation (see: http://www.gnu.org/software/libiconv) for the list of # possible encodings. # The default value is: UTF-8. INPUT_ENCODING = UTF-8 # If the value of the INPUT tag contains directories, you can use the # FILE_PATTERNS tag to specify one or more wildcard patterns (like *.cpp and # *.h) to filter out the source-files in the directories. If left blank the # following patterns are tested:*.c, *.cc, *.cxx, *.cpp, *.c++, *.java, *.ii, # *.ixx, *.ipp, *.i++, *.inl, *.idl, *.ddl, *.odl, *.h, *.hh, *.hxx, *.hpp, # *.h++, *.cs, *.d, *.php, *.php4, *.php5, *.phtml, *.inc, *.m, *.markdown, # *.md, *.mm, *.dox, *.py, *.f90, *.f, *.for, *.tcl, *.vhd, *.vhdl, *.ucf, # *.qsf, *.as and *.js. FILE_PATTERNS = *.c \ *.cc \ *.cxx \ *.cpp \ *.c++ \ *.java \ *.ii \ *.ixx \ *.ipp \ *.i++ \ *.inl \ *.h \ *.hh \ *.hxx \ *.hpp \ *.h++ \ *.idl \ *.odl \ *.cs \ *.php \ *.php3 \ *.inc \ *.C \ *.H \ *.tlh \ *.diff \ *.patch \ *.moc \ *.xpm \ *.dox \ *.md # The RECURSIVE tag can be used to specify whether or not subdirectories should # be searched for input files as well. # The default value is: NO. RECURSIVE = YES # The EXCLUDE tag can be used to specify files and/or directories that should be # excluded from the INPUT source files. This way you can easily exclude a # subdirectory from a directory tree whose root is specified with the INPUT tag. # # Note that relative paths are relative to the directory from which doxygen is # run. EXCLUDE = # The EXCLUDE_SYMLINKS tag can be used to select whether or not files or # directories that are symbolic links (a Unix file system feature) are excluded # from the input. # The default value is: NO. EXCLUDE_SYMLINKS = NO # If the value of the INPUT tag contains directories, you can use the # EXCLUDE_PATTERNS tag to specify one or more wildcard patterns to exclude # certain files from those directories. # # Note that the wildcards are matched against the file with absolute path, so to # exclude all test directories for example use the pattern */test/* EXCLUDE_PATTERNS = # The EXCLUDE_SYMBOLS tag can be used to specify one or more symbol names # (namespaces, classes, functions, etc.) that should be excluded from the # output. The symbol name can be a fully qualified name, a word, or if the # wildcard * is used, a substring. Examples: ANamespace, AClass, # AClass::ANamespace, ANamespace::*Test # # Note that the wildcards are matched against the file with absolute path, so to # exclude all test directories use the pattern */test/* EXCLUDE_SYMBOLS = # The EXAMPLE_PATH tag can be used to specify one or more files or directories # that contain example code fragments that are included (see the \include # command). EXAMPLE_PATH = examples # If the value of the EXAMPLE_PATH tag contains directories, you can use the # EXAMPLE_PATTERNS tag to specify one or more wildcard pattern (like *.cpp and # *.h) to filter out the source-files in the directories. If left blank all # files are included. EXAMPLE_PATTERNS = * # If the EXAMPLE_RECURSIVE tag is set to YES then subdirectories will be # searched for input files to be used with the \include or \dontinclude commands # irrespective of the value of the RECURSIVE tag. # The default value is: NO. EXAMPLE_RECURSIVE = YES # The IMAGE_PATH tag can be used to specify one or more files or directories # that contain images that are to be included in the documentation (see the # \image command). IMAGE_PATH = libs/indibase/alignment # The INPUT_FILTER tag can be used to specify a program that doxygen should # invoke to filter for each input file. Doxygen will invoke the filter program # by executing (via popen()) the command: # # # # where is the value of the INPUT_FILTER tag, and is the # name of an input file. Doxygen will then use the output that the filter # program writes to standard output. If FILTER_PATTERNS is specified, this tag # will be ignored. # # Note that the filter must not add or remove lines; it is applied before the # code is scanned, but not when the output code is generated. If lines are added # or removed, the anchors will not be placed correctly. INPUT_FILTER = # The FILTER_PATTERNS tag can be used to specify filters on a per file pattern # basis. Doxygen will compare the file name with each pattern and apply the # filter if there is a match. The filters are a list of the form: pattern=filter # (like *.cpp=my_cpp_filter). See INPUT_FILTER for further information on how # filters are used. If the FILTER_PATTERNS tag is empty or if none of the # patterns match the file name, INPUT_FILTER is applied. FILTER_PATTERNS = # If the FILTER_SOURCE_FILES tag is set to YES, the input filter (if set using # INPUT_FILTER ) will also be used to filter the input files that are used for # producing the source files to browse (i.e. when SOURCE_BROWSER is set to YES). # The default value is: NO. FILTER_SOURCE_FILES = NO # The FILTER_SOURCE_PATTERNS tag can be used to specify source filters per file # pattern. A pattern will override the setting for FILTER_PATTERN (if any) and # it is also possible to disable source filtering for a specific pattern using # *.ext= (so without naming a filter). # This tag requires that the tag FILTER_SOURCE_FILES is set to YES. FILTER_SOURCE_PATTERNS = # If the USE_MDFILE_AS_MAINPAGE tag refers to the name of a markdown file that # is part of the input, its contents will be placed on the main page # (index.html). This can be useful if you have a project on for instance GitHub # and want to reuse the introduction page also for the doxygen output. USE_MDFILE_AS_MAINPAGE = #--------------------------------------------------------------------------- # Configuration options related to source browsing #--------------------------------------------------------------------------- # If the SOURCE_BROWSER tag is set to YES then a list of source files will be # generated. Documented entities will be cross-referenced with these sources. # # Note: To get rid of all source code in the generated output, make sure that # also VERBATIM_HEADERS is set to NO. # The default value is: NO. SOURCE_BROWSER = YES # Setting the INLINE_SOURCES tag to YES will include the body of functions, # classes and enums directly into the documentation. # The default value is: NO. INLINE_SOURCES = NO # Setting the STRIP_CODE_COMMENTS tag to YES will instruct doxygen to hide any # special comment blocks from generated source code fragments. Normal C, C++ and # Fortran comments will always remain visible. # The default value is: YES. STRIP_CODE_COMMENTS = YES # If the REFERENCED_BY_RELATION tag is set to YES then for each documented # function all documented functions referencing it will be listed. # The default value is: NO. REFERENCED_BY_RELATION = NO # If the REFERENCES_RELATION tag is set to YES then for each documented function # all documented entities called/used by that function will be listed. # The default value is: NO. REFERENCES_RELATION = NO # If the REFERENCES_LINK_SOURCE tag is set to YES and SOURCE_BROWSER tag is set # to YES, then the hyperlinks from functions in REFERENCES_RELATION and # REFERENCED_BY_RELATION lists will link to the source code. Otherwise they will # link to the documentation. # The default value is: YES. REFERENCES_LINK_SOURCE = YES # If SOURCE_TOOLTIPS is enabled (the default) then hovering a hyperlink in the # source code will show a tooltip with additional information such as prototype, # brief description and links to the definition and documentation. Since this # will make the HTML file larger and loading of large files a bit slower, you # can opt to disable this feature. # The default value is: YES. # This tag requires that the tag SOURCE_BROWSER is set to YES. SOURCE_TOOLTIPS = YES # If the USE_HTAGS tag is set to YES then the references to source code will # point to the HTML generated by the htags(1) tool instead of doxygen built-in # source browser. The htags tool is part of GNU's global source tagging system # (see http://www.gnu.org/software/global/global.html). You will need version # 4.8.6 or higher. # # To use it do the following: # - Install the latest version of global # - Enable SOURCE_BROWSER and USE_HTAGS in the config file # - Make sure the INPUT points to the root of the source tree # - Run doxygen as normal # # Doxygen will invoke htags (and that will in turn invoke gtags), so these # tools must be available from the command line (i.e. in the search path). # # The result: instead of the source browser generated by doxygen, the links to # source code will now point to the output of htags. # The default value is: NO. # This tag requires that the tag SOURCE_BROWSER is set to YES. USE_HTAGS = NO # If the VERBATIM_HEADERS tag is set the YES then doxygen will generate a # verbatim copy of the header file for each class for which an include is # specified. Set to NO to disable this. # See also: Section \class. # The default value is: YES. VERBATIM_HEADERS = NO # If the CLANG_ASSISTED_PARSING tag is set to YES, then doxygen will use the # clang parser (see: http://clang.llvm.org/) for more accurate parsing at the # cost of reduced performance. This can be particularly helpful with template # rich C++ code for which doxygen's built-in parser lacks the necessary type # information. # Note: The availability of this option depends on whether or not doxygen was # compiled with the --with-libclang option. # The default value is: NO. CLANG_ASSISTED_PARSING = NO # If clang assisted parsing is enabled you can provide the compiler with command # line options that you would normally use when invoking the compiler. Note that # the include paths will already be set by doxygen for the files and directories # specified with INPUT and INCLUDE_PATH. # This tag requires that the tag CLANG_ASSISTED_PARSING is set to YES. CLANG_OPTIONS = #--------------------------------------------------------------------------- # Configuration options related to the alphabetical class index #--------------------------------------------------------------------------- # If the ALPHABETICAL_INDEX tag is set to YES, an alphabetical index of all # compounds will be generated. Enable this if the project contains a lot of # classes, structs, unions or interfaces. # The default value is: YES. ALPHABETICAL_INDEX = YES # The COLS_IN_ALPHA_INDEX tag can be used to specify the number of columns in # which the alphabetical index list will be split. # Minimum value: 1, maximum value: 20, default value: 5. # This tag requires that the tag ALPHABETICAL_INDEX is set to YES. COLS_IN_ALPHA_INDEX = 5 # In case all classes in a project start with a common prefix, all classes will # be put under the same header in the alphabetical index. The IGNORE_PREFIX tag # can be used to specify a prefix (or a list of prefixes) that should be ignored # while generating the index headers. # This tag requires that the tag ALPHABETICAL_INDEX is set to YES. IGNORE_PREFIX = #--------------------------------------------------------------------------- # Configuration options related to the HTML output #--------------------------------------------------------------------------- # If the GENERATE_HTML tag is set to YES doxygen will generate HTML output # The default value is: YES. GENERATE_HTML = YES # The HTML_OUTPUT tag is used to specify where the HTML docs will be put. If a # relative path is entered the value of OUTPUT_DIRECTORY will be put in front of # it. # The default directory is: html. # This tag requires that the tag GENERATE_HTML is set to YES. HTML_OUTPUT = doc # The HTML_FILE_EXTENSION tag can be used to specify the file extension for each # generated HTML page (for example: .htm, .php, .asp). # The default value is: .html. # This tag requires that the tag GENERATE_HTML is set to YES. HTML_FILE_EXTENSION = .html # The HTML_HEADER tag can be used to specify a user-defined HTML header file for # each generated HTML page. If the tag is left blank doxygen will generate a # standard header. # # To get valid HTML the header file that includes any scripts and style sheets # that doxygen needs, which is dependent on the configuration options used (e.g. # the setting GENERATE_TREEVIEW). It is highly recommended to start with a # default header using # doxygen -w html new_header.html new_footer.html new_stylesheet.css # YourConfigFile # and then modify the file new_header.html. See also section "Doxygen usage" # for information on how to generate the default header that doxygen normally # uses. # Note: The header is subject to change so you typically have to regenerate the # default header when upgrading to a newer version of doxygen. For a description # of the possible markers and block names see the documentation. # This tag requires that the tag GENERATE_HTML is set to YES. HTML_HEADER = # The HTML_FOOTER tag can be used to specify a user-defined HTML footer for each # generated HTML page. If the tag is left blank doxygen will generate a standard # footer. See HTML_HEADER for more information on how to generate a default # footer and what special commands can be used inside the footer. See also # section "Doxygen usage" for information on how to generate the default footer # that doxygen normally uses. # This tag requires that the tag GENERATE_HTML is set to YES. HTML_FOOTER = # The HTML_STYLESHEET tag can be used to specify a user-defined cascading style # sheet that is used by each HTML page. It can be used to fine-tune the look of # the HTML output. If left blank doxygen will generate a default style sheet. # See also section "Doxygen usage" for information on how to generate the style # sheet that doxygen normally uses. # Note: It is recommended to use HTML_EXTRA_STYLESHEET instead of this tag, as # it is more robust and this tag (HTML_STYLESHEET) will in the future become # obsolete. # This tag requires that the tag GENERATE_HTML is set to YES. HTML_STYLESHEET = # The HTML_EXTRA_STYLESHEET tag can be used to specify additional user-defined # cascading style sheets that are included after the standard style sheets # created by doxygen. Using this option one can overrule certain style aspects. # This is preferred over using HTML_STYLESHEET since it does not replace the # standard style sheet and is therefor more robust against future updates. # Doxygen will copy the style sheet files to the output directory. # Note: The order of the extra stylesheet files is of importance (e.g. the last # stylesheet in the list overrules the setting of the previous ones in the # list). For an example see the documentation. # This tag requires that the tag GENERATE_HTML is set to YES. HTML_EXTRA_STYLESHEET = # The HTML_EXTRA_FILES tag can be used to specify one or more extra images or # other source files which should be copied to the HTML output directory. Note # that these files will be copied to the base HTML output directory. Use the # $relpath^ marker in the HTML_HEADER and/or HTML_FOOTER files to load these # files. In the HTML_STYLESHEET file, use the file name only. Also note that the # files will be copied as-is; there are no commands or markers available. # This tag requires that the tag GENERATE_HTML is set to YES. HTML_EXTRA_FILES = # The HTML_COLORSTYLE_HUE tag controls the color of the HTML output. Doxygen # will adjust the colors in the stylesheet and background images according to # this color. Hue is specified as an angle on a colorwheel, see # http://en.wikipedia.org/wiki/Hue for more information. For instance the value # 0 represents red, 60 is yellow, 120 is green, 180 is cyan, 240 is blue, 300 # purple, and 360 is red again. # Minimum value: 0, maximum value: 359, default value: 220. # This tag requires that the tag GENERATE_HTML is set to YES. HTML_COLORSTYLE_HUE = 220 # The HTML_COLORSTYLE_SAT tag controls the purity (or saturation) of the colors # in the HTML output. For a value of 0 the output will use grayscales only. A # value of 255 will produce the most vivid colors. # Minimum value: 0, maximum value: 255, default value: 100. # This tag requires that the tag GENERATE_HTML is set to YES. HTML_COLORSTYLE_SAT = 100 # The HTML_COLORSTYLE_GAMMA tag controls the gamma correction applied to the # luminance component of the colors in the HTML output. Values below 100 # gradually make the output lighter, whereas values above 100 make the output # darker. The value divided by 100 is the actual gamma applied, so 80 represents # a gamma of 0.8, The value 220 represents a gamma of 2.2, and 100 does not # change the gamma. # Minimum value: 40, maximum value: 240, default value: 80. # This tag requires that the tag GENERATE_HTML is set to YES. HTML_COLORSTYLE_GAMMA = 80 # If the HTML_TIMESTAMP tag is set to YES then the footer of each generated HTML # page will contain the date and time when the page was generated. Setting this # to NO can help when comparing the output of multiple runs. # The default value is: YES. # This tag requires that the tag GENERATE_HTML is set to YES. HTML_TIMESTAMP = YES # If the HTML_DYNAMIC_SECTIONS tag is set to YES then the generated HTML # documentation will contain sections that can be hidden and shown after the # page has loaded. # The default value is: NO. # This tag requires that the tag GENERATE_HTML is set to YES. HTML_DYNAMIC_SECTIONS = NO # With HTML_INDEX_NUM_ENTRIES one can control the preferred number of entries # shown in the various tree structured indices initially; the user can expand # and collapse entries dynamically later on. Doxygen will expand the tree to # such a level that at most the specified number of entries are visible (unless # a fully collapsed tree already exceeds this amount). So setting the number of # entries 1 will produce a full collapsed tree by default. 0 is a special value # representing an infinite number of entries and will result in a full expanded # tree by default. # Minimum value: 0, maximum value: 9999, default value: 100. # This tag requires that the tag GENERATE_HTML is set to YES. HTML_INDEX_NUM_ENTRIES = 100 # If the GENERATE_DOCSET tag is set to YES, additional index files will be # generated that can be used as input for Apple's Xcode 3 integrated development # environment (see: http://developer.apple.com/tools/xcode/), introduced with # OSX 10.5 (Leopard). To create a documentation set, doxygen will generate a # Makefile in the HTML output directory. Running make will produce the docset in # that directory and running make install will install the docset in # ~/Library/Developer/Shared/Documentation/DocSets so that Xcode will find it at # startup. See http://developer.apple.com/tools/creatingdocsetswithdoxygen.html # for more information. # The default value is: NO. # This tag requires that the tag GENERATE_HTML is set to YES. GENERATE_DOCSET = NO # This tag determines the name of the docset feed. A documentation feed provides # an umbrella under which multiple documentation sets from a single provider # (such as a company or product suite) can be grouped. # The default value is: Doxygen generated docs. # This tag requires that the tag GENERATE_DOCSET is set to YES. DOCSET_FEEDNAME = "Doxygen generated docs" # This tag specifies a string that should uniquely identify the documentation # set bundle. This should be a reverse domain-name style string, e.g. # com.mycompany.MyDocSet. Doxygen will append .docset to the name. # The default value is: org.doxygen.Project. # This tag requires that the tag GENERATE_DOCSET is set to YES. DOCSET_BUNDLE_ID = org.doxygen.Project # The DOCSET_PUBLISHER_ID tag specifies a string that should uniquely identify # the documentation publisher. This should be a reverse domain-name style # string, e.g. com.mycompany.MyDocSet.documentation. # The default value is: org.doxygen.Publisher. # This tag requires that the tag GENERATE_DOCSET is set to YES. DOCSET_PUBLISHER_ID = org.doxygen.Publisher # The DOCSET_PUBLISHER_NAME tag identifies the documentation publisher. # The default value is: Publisher. # This tag requires that the tag GENERATE_DOCSET is set to YES. DOCSET_PUBLISHER_NAME = Publisher # If the GENERATE_HTMLHELP tag is set to YES then doxygen generates three # additional HTML index files: index.hhp, index.hhc, and index.hhk. The # index.hhp is a project file that can be read by Microsoft's HTML Help Workshop # (see: http://www.microsoft.com/en-us/download/details.aspx?id=21138) on # Windows. # # The HTML Help Workshop contains a compiler that can convert all HTML output # generated by doxygen into a single compiled HTML file (.chm). Compiled HTML # files are now used as the Windows 98 help format, and will replace the old # Windows help format (.hlp) on all Windows platforms in the future. Compressed # HTML files also contain an index, a table of contents, and you can search for # words in the documentation. The HTML workshop also contains a viewer for # compressed HTML files. # The default value is: NO. # This tag requires that the tag GENERATE_HTML is set to YES. GENERATE_HTMLHELP = NO # The CHM_FILE tag can be used to specify the file name of the resulting .chm # file. You can add a path in front of the file if the result should not be # written to the html output directory. # This tag requires that the tag GENERATE_HTMLHELP is set to YES. CHM_FILE = # The HHC_LOCATION tag can be used to specify the location (absolute path # including file name) of the HTML help compiler ( hhc.exe). If non-empty # doxygen will try to run the HTML help compiler on the generated index.hhp. # The file has to be specified with full path. # This tag requires that the tag GENERATE_HTMLHELP is set to YES. HHC_LOCATION = # The GENERATE_CHI flag controls if a separate .chi index file is generated ( # YES) or that it should be included in the master .chm file ( NO). # The default value is: NO. # This tag requires that the tag GENERATE_HTMLHELP is set to YES. GENERATE_CHI = NO # The CHM_INDEX_ENCODING is used to encode HtmlHelp index ( hhk), content ( hhc) # and project file content. # This tag requires that the tag GENERATE_HTMLHELP is set to YES. CHM_INDEX_ENCODING = # The BINARY_TOC flag controls whether a binary table of contents is generated ( # YES) or a normal table of contents ( NO) in the .chm file. Furthermore it # enables the Previous and Next buttons. # The default value is: NO. # This tag requires that the tag GENERATE_HTMLHELP is set to YES. BINARY_TOC = NO # The TOC_EXPAND flag can be set to YES to add extra items for group members to # the table of contents of the HTML help documentation and to the tree view. # The default value is: NO. # This tag requires that the tag GENERATE_HTMLHELP is set to YES. TOC_EXPAND = NO # If the GENERATE_QHP tag is set to YES and both QHP_NAMESPACE and # QHP_VIRTUAL_FOLDER are set, an additional index file will be generated that # can be used as input for Qt's qhelpgenerator to generate a Qt Compressed Help # (.qch) of the generated HTML documentation. # The default value is: NO. # This tag requires that the tag GENERATE_HTML is set to YES. GENERATE_QHP = NO # If the QHG_LOCATION tag is specified, the QCH_FILE tag can be used to specify # the file name of the resulting .qch file. The path specified is relative to # the HTML output folder. # This tag requires that the tag GENERATE_QHP is set to YES. QCH_FILE = # The QHP_NAMESPACE tag specifies the namespace to use when generating Qt Help # Project output. For more information please see Qt Help Project / Namespace # (see: http://qt-project.org/doc/qt-4.8/qthelpproject.html#namespace). # The default value is: org.doxygen.Project. # This tag requires that the tag GENERATE_QHP is set to YES. QHP_NAMESPACE = org.doxygen.Project # The QHP_VIRTUAL_FOLDER tag specifies the namespace to use when generating Qt # Help Project output. For more information please see Qt Help Project / Virtual # Folders (see: http://qt-project.org/doc/qt-4.8/qthelpproject.html#virtual- # folders). # The default value is: doc. # This tag requires that the tag GENERATE_QHP is set to YES. QHP_VIRTUAL_FOLDER = doc # If the QHP_CUST_FILTER_NAME tag is set, it specifies the name of a custom # filter to add. For more information please see Qt Help Project / Custom # Filters (see: http://qt-project.org/doc/qt-4.8/qthelpproject.html#custom- # filters). # This tag requires that the tag GENERATE_QHP is set to YES. QHP_CUST_FILTER_NAME = # The QHP_CUST_FILTER_ATTRS tag specifies the list of the attributes of the # custom filter to add. For more information please see Qt Help Project / Custom # Filters (see: http://qt-project.org/doc/qt-4.8/qthelpproject.html#custom- # filters). # This tag requires that the tag GENERATE_QHP is set to YES. QHP_CUST_FILTER_ATTRS = # The QHP_SECT_FILTER_ATTRS tag specifies the list of the attributes this # project's filter section matches. Qt Help Project / Filter Attributes (see: # http://qt-project.org/doc/qt-4.8/qthelpproject.html#filter-attributes). # This tag requires that the tag GENERATE_QHP is set to YES. QHP_SECT_FILTER_ATTRS = # The QHG_LOCATION tag can be used to specify the location of Qt's # qhelpgenerator. If non-empty doxygen will try to run qhelpgenerator on the # generated .qhp file. # This tag requires that the tag GENERATE_QHP is set to YES. QHG_LOCATION = # If the GENERATE_ECLIPSEHELP tag is set to YES, additional index files will be # generated, together with the HTML files, they form an Eclipse help plugin. To # install this plugin and make it available under the help contents menu in # Eclipse, the contents of the directory containing the HTML and XML files needs # to be copied into the plugins directory of eclipse. The name of the directory # within the plugins directory should be the same as the ECLIPSE_DOC_ID value. # After copying Eclipse needs to be restarted before the help appears. # The default value is: NO. # This tag requires that the tag GENERATE_HTML is set to YES. GENERATE_ECLIPSEHELP = NO # A unique identifier for the Eclipse help plugin. When installing the plugin # the directory name containing the HTML and XML files should also have this # name. Each documentation set should have its own identifier. # The default value is: org.doxygen.Project. # This tag requires that the tag GENERATE_ECLIPSEHELP is set to YES. ECLIPSE_DOC_ID = org.doxygen.Project # If you want full control over the layout of the generated HTML pages it might # be necessary to disable the index and replace it with your own. The # DISABLE_INDEX tag can be used to turn on/off the condensed index (tabs) at top # of each HTML page. A value of NO enables the index and the value YES disables # it. Since the tabs in the index contain the same information as the navigation # tree, you can set this option to YES if you also set GENERATE_TREEVIEW to YES. # The default value is: NO. # This tag requires that the tag GENERATE_HTML is set to YES. DISABLE_INDEX = NO # The GENERATE_TREEVIEW tag is used to specify whether a tree-like index # structure should be generated to display hierarchical information. If the tag # value is set to YES, a side panel will be generated containing a tree-like # index structure (just like the one that is generated for HTML Help). For this # to work a browser that supports JavaScript, DHTML, CSS and frames is required # (i.e. any modern browser). Windows users are probably better off using the # HTML help feature. Via custom stylesheets (see HTML_EXTRA_STYLESHEET) one can # further fine-tune the look of the index. As an example, the default style # sheet generated by doxygen has an example that shows how to put an image at # the root of the tree instead of the PROJECT_NAME. Since the tree basically has # the same information as the tab index, you could consider setting # DISABLE_INDEX to YES when enabling this option. # The default value is: NO. # This tag requires that the tag GENERATE_HTML is set to YES. GENERATE_TREEVIEW = NO # The ENUM_VALUES_PER_LINE tag can be used to set the number of enum values that # doxygen will group on one line in the generated HTML documentation. # # Note that a value of 0 will completely suppress the enum values from appearing # in the overview section. # Minimum value: 0, maximum value: 20, default value: 4. # This tag requires that the tag GENERATE_HTML is set to YES. ENUM_VALUES_PER_LINE = 4 # If the treeview is enabled (see GENERATE_TREEVIEW) then this tag can be used # to set the initial width (in pixels) of the frame in which the tree is shown. # Minimum value: 0, maximum value: 1500, default value: 250. # This tag requires that the tag GENERATE_HTML is set to YES. TREEVIEW_WIDTH = 250 # When the EXT_LINKS_IN_WINDOW option is set to YES doxygen will open links to # external symbols imported via tag files in a separate window. # The default value is: NO. # This tag requires that the tag GENERATE_HTML is set to YES. EXT_LINKS_IN_WINDOW = NO # Use this tag to change the font size of LaTeX formulas included as images in # the HTML documentation. When you change the font size after a successful # doxygen run you need to manually remove any form_*.png images from the HTML # output directory to force them to be regenerated. # Minimum value: 8, maximum value: 50, default value: 10. # This tag requires that the tag GENERATE_HTML is set to YES. FORMULA_FONTSIZE = 10 # Use the FORMULA_TRANPARENT tag to determine whether or not the images # generated for formulas are transparent PNGs. Transparent PNGs are not # supported properly for IE 6.0, but are supported on all modern browsers. # # Note that when changing this option you need to delete any form_*.png files in # the HTML output directory before the changes have effect. # The default value is: YES. # This tag requires that the tag GENERATE_HTML is set to YES. FORMULA_TRANSPARENT = YES # Enable the USE_MATHJAX option to render LaTeX formulas using MathJax (see # http://www.mathjax.org) which uses client side Javascript for the rendering # instead of using prerendered bitmaps. Use this if you do not have LaTeX # installed or if you want to formulas look prettier in the HTML output. When # enabled you may also need to install MathJax separately and configure the path # to it using the MATHJAX_RELPATH option. # The default value is: NO. # This tag requires that the tag GENERATE_HTML is set to YES. USE_MATHJAX = NO # When MathJax is enabled you can set the default output format to be used for # the MathJax output. See the MathJax site (see: # http://docs.mathjax.org/en/latest/output.html) for more details. # Possible values are: HTML-CSS (which is slower, but has the best # compatibility), NativeMML (i.e. MathML) and SVG. # The default value is: HTML-CSS. # This tag requires that the tag USE_MATHJAX is set to YES. MATHJAX_FORMAT = HTML-CSS # When MathJax is enabled you need to specify the location relative to the HTML # output directory using the MATHJAX_RELPATH option. The destination directory # should contain the MathJax.js script. For instance, if the mathjax directory # is located at the same level as the HTML output directory, then # MATHJAX_RELPATH should be ../mathjax. The default value points to the MathJax # Content Delivery Network so you can quickly see the result without installing # MathJax. However, it is strongly recommended to install a local copy of # MathJax from http://www.mathjax.org before deployment. # The default value is: http://cdn.mathjax.org/mathjax/latest. # This tag requires that the tag USE_MATHJAX is set to YES. MATHJAX_RELPATH = http://cdn.mathjax.org/mathjax/latest # The MATHJAX_EXTENSIONS tag can be used to specify one or more MathJax # extension names that should be enabled during MathJax rendering. For example # MATHJAX_EXTENSIONS = TeX/AMSmath TeX/AMSsymbols # This tag requires that the tag USE_MATHJAX is set to YES. MATHJAX_EXTENSIONS = # The MATHJAX_CODEFILE tag can be used to specify a file with javascript pieces # of code that will be used on startup of the MathJax code. See the MathJax site # (see: http://docs.mathjax.org/en/latest/output.html) for more details. For an # example see the documentation. # This tag requires that the tag USE_MATHJAX is set to YES. MATHJAX_CODEFILE = # When the SEARCHENGINE tag is enabled doxygen will generate a search box for # the HTML output. The underlying search engine uses javascript and DHTML and # should work on any modern browser. Note that when using HTML help # (GENERATE_HTMLHELP), Qt help (GENERATE_QHP), or docsets (GENERATE_DOCSET) # there is already a search function so this one should typically be disabled. # For large projects the javascript based search engine can be slow, then # enabling SERVER_BASED_SEARCH may provide a better solution. It is possible to # search using the keyboard; to jump to the search box use + S # (what the is depends on the OS and browser, but it is typically # , /