diff --git a/CMakeLists.txt b/CMakeLists.txt index 326b437a0..ba5df0eb3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -203,6 +203,7 @@ option(WITH_PLAYERONE "Install Player One Astronomy's Camera Driver" On) option(WITH_WEEWX_JSON "Install Weewx JSON Driver" On) option(WITH_ROLLOFFINO "Install RollOff ino Dome Driver" On) option(WITH_ASTROASIS "Install Astroasis Driver" On) +option(WITH_OCS "Install OCS Driver" On) # FFMPEG required for INDI Webcam driver find_package(FFmpeg) @@ -769,6 +770,11 @@ SET(LIBRARIES_FOUND FALSE) endif(ASTROASIS_FOUND) endif(WITH_ASTROASIS) +# OCS +if (WITH_OCS) +add_subdirectory(indi-ocs) +endif() + # Check if libraries are found. If not, we must build them, install them, THEN run CMake again to build and instal the drivers. If all the libraraies are installed, then we build and install the drivers only now. if (LIBRARIES_FOUND) message(STATUS "############################################################################") diff --git a/indi-ocs/CMakeLists.txt b/indi-ocs/CMakeLists.txt new file mode 100644 index 000000000..1d1cccde6 --- /dev/null +++ b/indi-ocs/CMakeLists.txt @@ -0,0 +1,34 @@ +cmake_minimum_required(VERSION 3.13) +PROJECT(indi_ocs C CXX) + +LIST(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake_modules/") +LIST(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/../cmake_modules/") +include(GNUInstallDirs) + +set(INDI_OCS_VERSION_MAJOR 0) +set(INDI_OCS_VERSION_MINOR 1) + +find_package(INDI REQUIRED) +find_package(Nova REQUIRED) + +configure_file(${CMAKE_CURRENT_SOURCE_DIR}/config.h.cmake ${CMAKE_CURRENT_BINARY_DIR}/config.h ) +configure_file(${CMAKE_CURRENT_SOURCE_DIR}/indi_ocs.xml.cmake ${CMAKE_CURRENT_BINARY_DIR}/indi_ocs.xml ) + +include_directories( ${CMAKE_CURRENT_BINARY_DIR}) +include_directories( ${CMAKE_CURRENT_SOURCE_DIR}) +include_directories( ${INDI_INCLUDE_DIR}) + +include(CMakeCommon) + +########### OCS ########### +set(indi_ocs_srcs + ${CMAKE_CURRENT_SOURCE_DIR}/ocs.cpp + ) + +add_executable(indi_ocs ${indi_ocs_srcs}) + +target_link_libraries(indi_ocs ${INDI_LIBRARIES} ${NOVA_LIBRARIES}) + +install(TARGETS indi_ocs RUNTIME DESTINATION bin ) + +install(FILES ${CMAKE_CURRENT_BINARY_DIR}/indi_ocs.xml DESTINATION ${INDI_DATA_DIR}) diff --git a/indi-ocs/Documentation/Connection_Tab.png b/indi-ocs/Documentation/Connection_Tab.png new file mode 100644 index 000000000..f81a28cbc Binary files /dev/null and b/indi-ocs/Documentation/Connection_Tab.png differ diff --git a/indi-ocs/Documentation/Indi-ocs.md b/indi-ocs/Documentation/Indi-ocs.md new file mode 100644 index 000000000..c88e2513c --- /dev/null +++ b/indi-ocs/Documentation/Indi-ocs.md @@ -0,0 +1,148 @@ +# Observatory Control System (OCS) + +The OCS is an open source (hardware and software) Observatory control that is very flexible and customisable. +See [OCS groups wiki](https://onstep.groups.io/g/onstep-ocs/wiki) for full info. + +## Installation + +indi_ocs is released as a 3rd party driver in source and binary packages. It is compatible with libindi >= v2.0.1 + +## Features + +The following features of OCS are supported by this driver: + + - Roll Off Roof / Dome Shutter control + - Dome control and mount slaving + - Power device control + - Lighting control + - Observatory thermostat control + - Weather monitoring + +## Configuration +Connection via USB and Ethernet are supported. +If using Ethernet connectivity a persistent port on the OCS is recommended (eg. 9998). + +Similar to the modular functionality of the in-build OCS website, options that are not enabled in the OCS config.h are not displayed. + +Functional options configured in the OCS software are detected upon connection and applied in the driver. For example, if you have configured a Power Device 1 with a name of "Flats Panel" this name will be used for the control in the driver. Similarly if you have defined a weather device safety limit in the OCS configuration this limit will initially be used in the driver. + +The standard INDI Dome parameters configuration is required for Dome slaving. + +## Driver Tab walkthrough +### Main Control +Main Control Tab + +- Connection - Connect(ed)/Disconnect(ed) control/indication +- Shutter - Open(ed)/Close(d) control/indication. For OCS ROR configurations this control is for the **roof**. Unfortunately there is no way to rename this standard control +- Status > Roof/Shutter - The current status of the roof/shutter system is displayed eg. Open, Closing: Travel 36%, Error code, etc. +- Absolute Position > Degrees - Setting an Azimuth position instructs the dome to move to it. +- Abort Motion - Immediately requests a halt of both dome and roof/shutter movement. +- Sync > Az - Setting an Azimuth position performs a sync on the dome ie. tells it "you are here". +- Parking - Park(ed)/UnPark(ed) control/indication. Sends the dome to the defined park position/removed the dome from parked status. +- Additional Controls + + Set Park - Sets the current Azimuth position as the dome park position. + + Return Home - Sends the dome to the defined home position. + + At Home (Reset) - Tells the dome it is at the defined home position. +- Status > Dome - The current status of the dome eg. Home, Parked, Idle, Slewing, Error code, etc. + +### Connection +Connection Tab + +Standard INDI Connection tab controls. +Use either Serial (USB) connection (which in testing of OCS4 hardware showed up as /dev/ttyACM0 or auto mapped /dev/serial/by-id/usb-Teensyduino\_USB\_Serial_12345678-if00) or Network using the OCS address and port 9998. + +### Options +Options Tab + +Standard INDI Dome Option tab controls. + +- Polling - Sets the refresh period (in ms) of the roof/shutter and dome status. +- Mount Policy - If set to: Mount locks the the **dome** is prevented from parking when mount is unparked. +- Shutter > Close on Park, Open on UnPark. + +**Note that this option is independent of the OCS option ROOF\_CLOSE\_PARKS\_MOUNT. If that option is set in the OCS configuration it is always active. These controls act in addition to that option and are not aware of it.** + +### Presets +Presets Tab + +Allows creation and GoTo of up to three stored locations of dome Azimuth. + +### Slaving +Slaving Tab + +Controls for configuring the dome > mount slaving functionality. Refer to [INDI Dome Devices](https://www.indilib.org/devices/domes.html) for details. + +### Status +Status Tab + +Shows miscellaneous details about the OCS. + +- Firmware version - As reported by OCS. The driver expects a minimum version and will report if the connected OCS software is below this. +- Roof last error - This message is persistent until a roof/shutter movement clears the status. +- Mains status - This is dependent on OCS configuration and does not contribute toward the Safe status of the observatory within the driver, which is independent of the safe status within OCS. +- MCU temperature ℃ - the internal temperature of the OCS microcontroller, if supported by the OCS hardware used. + +### Weather +Weather Tab + +**Note: this tab is only displayed if the Weather function is configured within the OCS software.** + +These controls define the minimum and maximum limits outside of which the measurement is considered unsafe to the observatory operation. + +- Update > Period (s) - This sets the refresh period for weather measurements. Don't bother going lower than the default 60 seconds as the OCS software internally polls once per minute so a faster refresh will just retrieve stale data. +- Weather > Refresh - Forces an immediate poll of weather measurements - see note above. +- Safety > Override Status - Ignore unsafe weather conditions. +- Status - The safe/unsafe status of the OCS configured measurements. +- Parameters - The readings returned of the OCS configured measurements. +- Limits - Each OCS configured weather measurement has three controls, a maximum level above which it's status is considered unsafe, a minimum level below which it's status is considered unsafe, and a percentage towards the limit that is considered to be a warning status + +**Note that the OCS internal safety status is independent of these driver settings. OCS considers Rain, Cloud cover, Wind speed & Mains power present towards it's safety status (if they are configured) and will operate independent of these settings and driver connection.** + +**Safe limits defined in the OCS software for Wind speed and Cloud cover are copied into the driver as initial limits upon connection. They may be over-ridden here but are not applied back to the OCS software. In addition, temperature, pressure & humidity are considered towards the INDI safety status. If not required for this purpose but only to be monitored, just set the min/max limits very wide so that they are always considered to be safe**. + +There are some weather measurements supported by the OCS that can not be used as contributors towards the safety status. If configured in the OCS software these will appear at the bottom of the tab. + +- Cloud > description - A text string describing the amount of cloud cover, from "Clear" to "Very-Cldy". +- Sky Quality> mag/arc-sec² - A measurement of the sky darkness. +- Sky temp > ℃ - The sky temperature taken from an IR sensor. + +### Thermostat +Thermostat Tab + +**Note: this tab is only displayed if the Thermostat function is configured within the OCS software.** + +These controls display the status of and control the setpoints of any configured observatory thermostat devices. Readings are update once per minute regardless of the Options > Polling period setting. + +- Obsy Status > Temperature ℃ - Internal observatory temperature +- Obsy Status > Humidity % - Internal observatory humidity +- Setpoint > Heat ℃ (0=OFF) - If the internal observatory temperature falls below this setpoint the defined Heat Relay is activated, for example to turn on a heater. +- Setpoint > Cool ℃ (0=OFF) - If the internal observatory temperature rises above this setpoint the defined Cool Relay is activated, for example to turn on a ventilation fan. +- Setpoint > Dehumidify % (0=OFF) - If the internal observatory humidity rises above this setpoint the defined Humidity Relay is activated, for example to turn on a dehumidifer. +- Heat Relay/Cool Relay/Rh Relay - These are indication of status only. + +**Note that the setpoints are saved in the OCS non-volatile storage and therefore are active until overwritten or disabled regardless of driver connection.** + +**Note that there is an OCS option CONTROL\_WITH\_ROOF\_OPEN. If this is set to OFF (the default) then the thermostat setpoint controls are ignored when the roof/shutter is opened. This is independent of this driver.** + +### Power +Power Tab + +Any OCS configured power device is shown with the defined name, and ON/OFF controls/status indication that activate the defined relay. + +### Lights +Lights Tab + +Any OCS configured lighting device is shown with ON/OFF controls/status indication that activate the defined relay. + +### Manual +Manual Tab + +- + +**Note the warning at the top of this tab - these are controls that directly address the hardware in ways that could result in lost data, damage to equipment, or in the worst case sudden movement of equipment that has the potential for injury.** + +- + +- Interlocks > OVERRIDE. This control causes the roof/shutter interlock signals to be ignored for one movement command. Only intended for testing and/or temporary interlock failure overrides. +- Roof Power > MAX. This control overrides any configured roof/shutter power level and just enables the motor at 100% power. Only intended for forcing the roof/shutter to close under failure conditions. +- Watchdog > REBOOT. **If the OCS Watchdog function is configured ON**, and neither the roof/shutter nor the dome are in motion, this control will cause the OCS controller to enter a deadlock forcing the watchdog to reboot the controller. diff --git a/indi-ocs/Documentation/Lights_Tab.png b/indi-ocs/Documentation/Lights_Tab.png new file mode 100644 index 000000000..154a6eba2 Binary files /dev/null and b/indi-ocs/Documentation/Lights_Tab.png differ diff --git a/indi-ocs/Documentation/Main_Control_Tab.png b/indi-ocs/Documentation/Main_Control_Tab.png new file mode 100644 index 000000000..5daf6da5e Binary files /dev/null and b/indi-ocs/Documentation/Main_Control_Tab.png differ diff --git a/indi-ocs/Documentation/Manual_Tab.png b/indi-ocs/Documentation/Manual_Tab.png new file mode 100644 index 000000000..1de11179e Binary files /dev/null and b/indi-ocs/Documentation/Manual_Tab.png differ diff --git a/indi-ocs/Documentation/Options_Tab.png b/indi-ocs/Documentation/Options_Tab.png new file mode 100644 index 000000000..4a3e4888f Binary files /dev/null and b/indi-ocs/Documentation/Options_Tab.png differ diff --git a/indi-ocs/Documentation/Power_Tab.png b/indi-ocs/Documentation/Power_Tab.png new file mode 100644 index 000000000..50c9be0be Binary files /dev/null and b/indi-ocs/Documentation/Power_Tab.png differ diff --git a/indi-ocs/Documentation/Presets_Tab.png b/indi-ocs/Documentation/Presets_Tab.png new file mode 100644 index 000000000..35ee5f7e5 Binary files /dev/null and b/indi-ocs/Documentation/Presets_Tab.png differ diff --git a/indi-ocs/Documentation/Slaving_Tab.png b/indi-ocs/Documentation/Slaving_Tab.png new file mode 100644 index 000000000..6ac4994c7 Binary files /dev/null and b/indi-ocs/Documentation/Slaving_Tab.png differ diff --git a/indi-ocs/Documentation/Status_Tab.png b/indi-ocs/Documentation/Status_Tab.png new file mode 100644 index 000000000..e2b03496a Binary files /dev/null and b/indi-ocs/Documentation/Status_Tab.png differ diff --git a/indi-ocs/Documentation/Thermostat_Tab.png b/indi-ocs/Documentation/Thermostat_Tab.png new file mode 100644 index 000000000..d4a4b482b Binary files /dev/null and b/indi-ocs/Documentation/Thermostat_Tab.png differ diff --git a/indi-ocs/Documentation/Weather_Tab.png b/indi-ocs/Documentation/Weather_Tab.png new file mode 100644 index 000000000..2421dc17f Binary files /dev/null and b/indi-ocs/Documentation/Weather_Tab.png differ diff --git a/indi-ocs/INSTALL.txt b/indi-ocs/INSTALL.txt new file mode 100644 index 000000000..36ba94ecb --- /dev/null +++ b/indi-ocs/INSTALL.txt @@ -0,0 +1,12 @@ +COMPILE INSTRUCTIONS: + +> cmake . +> make +> sudo make install + + +TEST INSTRUCTIONS: +(Tested with Indi 2.0.1) + +indiserver ./indi_ocs + diff --git a/indi-ocs/LICENSE.txt b/indi-ocs/LICENSE.txt new file mode 100644 index 000000000..af3c0c55c --- /dev/null +++ b/indi-ocs/LICENSE.txt @@ -0,0 +1,86 @@ +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. +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. diff --git a/indi-ocs/README.txt b/indi-ocs/README.txt new file mode 100644 index 000000000..d604b7c53 --- /dev/null +++ b/indi-ocs/README.txt @@ -0,0 +1,10 @@ +Observatory Control System (OCS) Indi Driver V0.1 +------------------------------------------------- + +An Indi driver for the OCS (https://onstep.groups.io/g/onstep-ocs/wiki) + + Copyright (C) 2023 Ed Lee + +Version 0.1 + + Initial test release diff --git a/indi-ocs/config.h.cmake b/indi-ocs/config.h.cmake new file mode 100644 index 000000000..483095b31 --- /dev/null +++ b/indi-ocs/config.h.cmake @@ -0,0 +1,11 @@ +#ifndef CONFIG_H +#define CONFIG_H + +/* Define INDI Data Dir */ +#cmakedefine INDI_DATA_DIR "@INDI_DATA_DIR@" + +/* Define Driver version */ +#define INDI_OCS_VERSION_MAJOR @INDI_OCS_VERSION_MAJOR@ +#define INDI_OCS_VERSION_MINOR @INDI_OCS_VERSION_MINOR@ + +#endif // CONFIG_H diff --git a/indi-ocs/indi_ocs.xml.cmake b/indi-ocs/indi_ocs.xml.cmake new file mode 100644 index 000000000..5d4673773 --- /dev/null +++ b/indi-ocs/indi_ocs.xml.cmake @@ -0,0 +1,9 @@ + + + + + indi_ocs + 1.0 + + + diff --git a/indi-ocs/ocs.cpp b/indi-ocs/ocs.cpp new file mode 100644 index 000000000..a5c582ed1 --- /dev/null +++ b/indi-ocs/ocs.cpp @@ -0,0 +1,2309 @@ +/****************************************************************************** + Copyright(c) 2014/2023 Jasem Mutlaq/Ed Lee. 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. +*******************************************************************************/ + +/****************************************************************************** +Driver for the Observatory Control System (OCS), an open source project created +by Howard Dutton. Refer to: +https://onstep.groups.io/g/onstep-ocs/wiki +https://github.com/hjd1964/OCS + +Capabilites include: Roll off roof, dome roof, weather monitoring, +themostat control, power device control, lighting control. +Hardware communication is via a simple text protocol similar to the LX200. +USB and network connections supported. +*******************************************************************************/ + +#include "ocs.h" +#include "termios.h" + +#include +#include +#include +#include + +// Custom tabs +#define STATUS_TAB "Status" +#define THERMOSTAT_TAB "Thermostat" +#define POWER_TAB "Power" +#define LIGHTS_TAB "Lights" +#define WEATHER_TAB "Weather" +#define MANUAL_TAB "Manual" + +// Mutex for communications +std::mutex ocsCommsLock; + +// Declare an auto pointer to OCS. +std::unique_ptr ocs(new OCS()); + +OCS::OCS() : INDI::Dome(), WI(this) +{ + setVersion(1, 0); + SetDomeCapability(DOME_CAN_ABORT | DOME_HAS_SHUTTER); + SlowTimer.callOnTimeout(std::bind(&OCS::SlowTimerHit, this)); +} + +/******************************************************* + * INDI is asking us for our default device name. + * Must match Ekos selection menu and ParkData.xml names + *******************************************************/ +const char *OCS::getDefaultName() +{ + return (const char *)"OCS"; +} + +/*************************************************************** + * Called from Dome, BaseDevice to establish contact with device + **************************************************************/ +bool OCS::Handshake() +{ + bool handshake_status = false; + + if (PortFD > 0) { + Connection::Interface *activeConnection = getActiveConnection(); + if (!activeConnection->name().compare("CONNECTION_TCP")) { + LOG_INFO("Network based connection, detection timeouts set to 0.5 seconds"); + OCSTimeoutMicroSeconds = 50000; + OCSTimeoutSeconds = 0; + } + else { + LOG_INFO("Non-Network based connection, detection timeouts set to 0.1 seconds"); + OCSTimeoutMicroSeconds = 100000; + OCSTimeoutSeconds = 0; + } + + char handshake_response[RB_MAX_LEN] = {0}; + handshake_status = getCommandSingleCharErrorOrLongResponse(PortFD, handshake_response, + OCS_handshake); + if (strcmp(handshake_response, "OCS") == 0) + { + LOG_DEBUG("OCS handshake established"); + handshake_status = true; + GetCapabilites(); + SlowTimer.start(60000); + } + else { + LOGF_DEBUG("OCS handshake error, reponse was: %s", handshake_response); + } + } + else { + LOG_ERROR("OCS can't handshake, device not connected"); + } + + return handshake_status; +} + +/************************************************************** + * Query connected OCS for capabilities - called from Handshake + **************************************************************/ +void OCS::GetCapabilites() +{ + // Get firmware version + char OCS_firmware_response[RB_MAX_LEN] = {0}; + int OCS_firmware_error_or_fail = getCommandSingleCharErrorOrLongResponse(PortFD, OCS_firmware_response, + OCS_get_firmware); + if (OCS_firmware_error_or_fail > 1) { + IUSaveText(&Status_ItemsT[STATUS_FIRMWARE], OCS_firmware_response); + IDSetText(&Status_ItemsTP, nullptr); + LOGF_DEBUG("OCS version: %s", OCS_firmware_response); + } else { + LOG_ERROR("OCS version not retrieved"); + } + if (std::stof(OCS_firmware_response) < minimum_OCS_fw) { + LOGF_WARN("OCS version %s is lower than this driver expects (%1.1f). Behaviour is unknown.", OCS_firmware_response, minimum_OCS_fw); + } + + // Get dome presence + char OCS_dome_present_response[RB_MAX_LEN] = {0}; + int OCS_dome_present_error_or_fail = getCommandSingleCharErrorOrLongResponse(PortFD, OCS_dome_present_response, + OCS_get_dome_status); + if (OCS_dome_present_error_or_fail > 0) { + SetDomeCapability(DOME_CAN_ABORT | DOME_CAN_PARK | DOME_CAN_ABS_MOVE | DOME_CAN_SYNC | DOME_HAS_SHUTTER); + setDomeState(DOME_UNKNOWN); + hasDome = true; + LOG_INFO("OCS has dome"); + } else { + LOG_INFO("OCS does not have dome"); + } + + // Get roof delays + char roof_timeout_response[RB_MAX_LEN] = {0}; + int roof_timeout_error_or_fail = getCommandSingleCharErrorOrLongResponse(PortFD, roof_timeout_response, + OCS_get_timeouts); + if (roof_timeout_error_or_fail > 1) { + char *split; + split = strtok(roof_timeout_response, ","); + if (charToInt(split) != conversion_error) { + ROOF_TIME_PRE_MOTION = charToInt(split); + } + split = strtok(NULL, ","); + if (charToInt(split) != conversion_error) { + ROOF_TIME_POST_MOTION = charToInt(split); + } + } + else { + LOGF_WARN("Communication error on get roof delays %s", OCS_get_timeouts); + } + + // Get the Obsy Thermostat presence + char thermostat_status_response[RB_MAX_LEN] = {0}; + int thermostat_status_error_or_fail = getCommandSingleCharErrorOrLongResponse(PortFD, thermostat_status_response, + OCS_get_thermostat_status); + if (thermostat_status_error_or_fail > 1) { //> 1 as an OCS error would be 1 char in response + if (strcmp(thermostat_status_response, "nan,nan") == 0) { + thermostat_controls_enabled = false; + LOG_INFO("OCS does not have a thermostat, disabling tab"); + } else { + thermostat_controls_enabled = true; + LOG_WARN("OCS has a thermostat, enabling tab"); + + // Get thermostat relay definitions + char thermostat_relay_definitions_response[RB_MAX_LEN] = {0}; + int thermostat_relay_definitions_error_or_fail = getCommandSingleCharErrorOrLongResponse(PortFD, thermostat_relay_definitions_response, + OCS_get_thermostat_definitions); + if (thermostat_relay_definitions_error_or_fail > 1) { + char *split; + split = strtok(thermostat_relay_definitions_response, ","); + for (int relayNo = 0; relayNo < THERMOSTAT_RELAY_COUNT; relayNo ++) { + if (charToInt(split) != conversion_error) { + thermostat_relays[relayNo] = charToInt(split); + } + split = strtok(NULL, ","); + } + } + } + } else if (strcmp(thermostat_status_response, "0") == 0) { + LOG_INFO("OCS does not have a thermostat, disabling tab"); + } + + // Get power relay definitions + char power_relay_definitions_response[RB_MAX_LEN] = {0}; + int power_relay_definitions_error_or_fail = getCommandSingleCharErrorOrLongResponse(PortFD, power_relay_definitions_response, + OCS_get_power_definitions); + if (power_relay_definitions_error_or_fail > 1) { + char *split; + split = strtok(power_relay_definitions_response, ","); + for (int deviceNo = 0; deviceNo < POWER_DEVICE_COUNT; deviceNo ++) { + if (charToInt(split) != conversion_error) { + power_device_relays[deviceNo] = charToInt(split); + } + split = strtok(NULL, ","); + } + // Defined devices have a positive integer relay definition, undefined return -1 + // so we can sum these to check if any are defined, if not then keep tab hidden + int powerDisabled = 0; + for (int deviceNo = 1; deviceNo < POWER_DEVICE_COUNT; deviceNo ++) { + powerDisabled += power_device_relays[deviceNo]; + } + if (powerDisabled != (-1 * POWER_DEVICE_COUNT)) { + power_tab_enabled = true; + LOG_INFO("OCS has power device(s), enabling tab"); + for (int deviceNo = 1; deviceNo < POWER_DEVICE_COUNT; deviceNo ++) { + if (power_device_relays[(deviceNo - 1)] != -1) { + char power_relay_name_response[RB_MAX_LEN] = {0}; + char get_power_device_name_command[CMD_MAX_LEN] = {0}; + sprintf(get_power_device_name_command, "%s%i%s", + OCS_get_power_names_part, deviceNo, OCS_command_terminator); + int power_relay_name_error_or_fail = getCommandSingleCharErrorOrLongResponse(PortFD, power_relay_name_response, + get_power_device_name_command); + if (power_relay_name_error_or_fail > 0) { + if (deviceNo == 1) { + indi_strlcpy(POWER_DEVICE1_NAME, power_relay_name_response, sizeof(POWER_DEVICE1_NAME)); + IUSaveText(&Power_Device_Name1T[0], POWER_DEVICE1_NAME); + IDSetText(&Power_Device_Name1TP, nullptr); + } else if (deviceNo == 2) { + indi_strlcpy(POWER_DEVICE2_NAME, power_relay_name_response, sizeof(POWER_DEVICE2_NAME)); + IUSaveText(&Power_Device_Name2T[0], POWER_DEVICE2_NAME); + IDSetText(&Power_Device_Name2TP, nullptr); + } else if (deviceNo == 3) { + indi_strlcpy(POWER_DEVICE3_NAME, power_relay_name_response, sizeof(POWER_DEVICE3_NAME)); + IUSaveText(&Power_Device_Name3T[0], POWER_DEVICE3_NAME); + IDSetText(&Power_Device_Name3TP, nullptr); + } else if (deviceNo == 4) { + indi_strlcpy(POWER_DEVICE4_NAME, power_relay_name_response, sizeof(POWER_DEVICE4_NAME)); + IUSaveText(&Power_Device_Name4T[0], POWER_DEVICE4_NAME); + IDSetText(&Power_Device_Name4TP, nullptr); + } else if (deviceNo == 5) { + indi_strlcpy(POWER_DEVICE5_NAME, power_relay_name_response, sizeof(POWER_DEVICE5_NAME)); + IUSaveText(&Power_Device_Name5T[0], POWER_DEVICE5_NAME); + IDSetText(&Power_Device_Name5TP, nullptr); + } else if (deviceNo == 61) { + indi_strlcpy(POWER_DEVICE6_NAME, power_relay_name_response, sizeof(POWER_DEVICE6_NAME)); + IUSaveText(&Power_Device_Name6T[0], POWER_DEVICE6_NAME); + IDSetText(&Power_Device_Name6TP, nullptr); + } + } + } + } + } else { + LOG_INFO("OCS does not have power device(s), disabling tab"); + } + } else if (strcmp(power_relay_definitions_response, "0") == 0) { + LOG_INFO("OCS does not have power device(s), disabling tab"); + } + + // Get light relay definitions + char light_relay_definitions_response[RB_MAX_LEN] = {0}; + int light_relay_definitions_error_or_fail = getCommandSingleCharErrorOrLongResponse(PortFD, light_relay_definitions_response, + OCS_get_light_definitions); + if (light_relay_definitions_error_or_fail > 1) { + char *split; + split = strtok(light_relay_definitions_response, ","); + for (int lrelay = 0; lrelay < LIGHT_COUNT; lrelay ++) { + if (charToInt(split) != conversion_error) { + light_relays[lrelay] = charToInt(split); + } + split = strtok(NULL, ","); + } + // Defined lights have a positive integer relay definition, undefined return -1 + // so we can sum these to check if any are defined, if not then keep tab hidden + int lightsDisabled = 0; + for (int lrelay = 1; lrelay < LIGHT_COUNT; lrelay ++) { + lightsDisabled += light_relays[lrelay]; + } + if (lightsDisabled != (-1 * LIGHT_COUNT)) { + lights_tab_enabled = true; + LOG_INFO("OCS has light(s), enabling tab"); + } else { + LOG_INFO("OCS does not have light(s), disabling tab"); + } + } else if (strcmp(light_relay_definitions_response, "0") == 0) { + LOG_INFO("OCS does not have light(s), disabling tab"); + } + + // Get available weather measurements + for (int measurement = 0; measurement < WEATHER_MEASUREMENTS_COUNT; measurement ++) { + char measurement_reponse[RB_MAX_LEN]; + char measurement_command[CMD_MAX_LEN]; + if (measurement == WEATHER_TEMPERATURE) { + indi_strlcpy(measurement_command, OCS_get_outside_temperature, sizeof(measurement_command)); + } else if (measurement == WEATHER_SKY_TEMP) { + indi_strlcpy(measurement_command, OCS_get_sky_IR_temperature, sizeof(measurement_command)); + } else if (measurement == WEATHER_DIFF_SKY_TEMP) { + indi_strlcpy(measurement_command, OCS_get_sky_diff_temperature, sizeof(measurement_command)); + } else if (measurement == WEATHER_PRESSURE) { + indi_strlcpy(measurement_command, OCS_get_pressure, sizeof(measurement_command)); + } else if (measurement == WEATHER_HUMIDITY) { + indi_strlcpy(measurement_command, OCS_get_humidity, sizeof(measurement_command)); + } else if (measurement == WEATHER_WIND) { + indi_strlcpy(measurement_command, OCS_get_wind_speed, sizeof(measurement_command)); + } else if (measurement == WEATHER_RAIN) { + indi_strlcpy(measurement_command, OCS_get_rain_sensor_status, sizeof(measurement_command)); + } else if (measurement == WEATHER_CLOUD) { + indi_strlcpy(measurement_command, OCS_get_cloud_description, sizeof(measurement_command)); + } else if (measurement == WEATHER_SKY) { + indi_strlcpy(measurement_command, OCS_get_sky_quality, sizeof(measurement_command)); + } + int measurement_error_or_fail = getCommandSingleCharErrorOrLongResponse(PortFD, measurement_reponse, + measurement_command); + if (measurement_error_or_fail > 1 && strcmp(measurement_reponse, "N/A") && strcmp(measurement_reponse, "0") != 0) { + weather_enabled[measurement] = 1; + } else { + weather_enabled[measurement] = 0; + } + } + + // Available weather measurements are now defined as = 1, unavailable as = 0 + // so we can sum these to check if any are defined, if not then keep tab disabled + int weatherDisabled = 0; + for (int wmeasure = 1; wmeasure < WEATHER_MEASUREMENTS_COUNT; wmeasure ++) { + weatherDisabled += weather_enabled[wmeasure]; + } + if (weatherDisabled > 0) { + weather_tab_enabled = true; + LOG_INFO("OCS has weather sensor(s), enabling tab"); + // If a weather measurement that has a safety limit set in OCS is active then get that limit + if (weather_enabled[WEATHER_WIND] || weather_enabled[WEATHER_DIFF_SKY_TEMP]) { + char threshold_reponse[RB_MAX_LEN]; + int threshold_error_or_fail = getCommandSingleCharErrorOrLongResponse(PortFD, threshold_reponse, + OCS_get_weather_thresholds); + if (threshold_error_or_fail > 1 ) { //> 1 as an OCS error would be 1 char in response + char *split; + split = strtok(threshold_reponse, ","); + if (strcmp(split, "N/A") != 0) { + if (charToInt(split) != conversion_error) { + wind_speed_threshold = charToInt(split); + } + } + split = strtok(NULL, ","); + if (strcmp(split, "N/A") != 0) { + if (charToInt(split) != conversion_error) { + diff_temp_threshold = charToInt(split); + } + } + } else { + LOGF_WARN("Communication error on get Weather thresholds %s", + OCS_get_weather_thresholds); + } + } + + // Loop through only the first 6 measurements rather than WEATHER_MEASUREMENTS_COUNT + // as only these are usable for safety status with limits + for (int measurements = 0; measurements < 6; measurements ++) { + if (measurements == WEATHER_TEMPERATURE && weather_enabled[WEATHER_TEMPERATURE] == 1) { + addParameter("WEATHER_TEMPERATURE", "Temperature °C", -10, 40, 15); + setCriticalParameter("WEATHER_TEMPERATURE"); + } else if (measurements == WEATHER_PRESSURE && weather_enabled[WEATHER_PRESSURE] == 1) { + addParameter("WEATHER_PRESSURE", "Pressure mbar", 970, 1050, 10); + setCriticalParameter("WEATHER_PRESSURE"); + } else if (measurements == WEATHER_HUMIDITY && weather_enabled[WEATHER_HUMIDITY] == 1) { + addParameter("WEATHER_HUMIDITY", "Humidity %", 0, 95, 15); + setCriticalParameter("WEATHER_HUMIDITY"); + } else if (measurements == WEATHER_WIND && weather_enabled[WEATHER_WIND] == 1) { + addParameter("WEATHER_WIND", "Wind kph", 0, wind_speed_threshold, 15); + setCriticalParameter("WEATHER_WIND"); + } else if (measurements == WEATHER_RAIN && weather_enabled[WEATHER_RAIN] == 1) { + addParameter("WEATHER_RAIN", "Rain state", 3, 3, 67); + setCriticalParameter("WEATHER_RAIN"); + } else if (measurements == WEATHER_DIFF_SKY_TEMP && weather_enabled[WEATHER_DIFF_SKY_TEMP] == 1) { + addParameter("WEATHER_SKY_DIFF_TEMP", "Sky vs Cloud °C", -50, diff_temp_threshold, 15); + setCriticalParameter("WEATHER_SKY_DIFF_TEMP"); + } + } + } else { + LOG_INFO("OCS does not have weather sensor(s), disabling tab"); + } + + // Call the slow property update once as this is startup and we want to populate now + SlowTimerHit(); +} + +/********************************************************************** +** INDI request to init properties. Connected Define properties to Ekos +***********************************************************************/ +bool OCS::initProperties() +{ + INDI::Dome::initProperties(); + + setDriverInterface(DOME_INTERFACE | WEATHER_INTERFACE); + + // Main control tab controls + //-------------------------- + IUFillTextVector(&ShutterStatusTP, ShutterStatusT, 1, getDeviceName(), "SHUTTER_STATUS", "Status", + MAIN_CONTROL_TAB, IP_RO, 60, IPS_OK); + IUFillText(&ShutterStatusT[0], "ROOF_SHUTTER_STATUS", "Roof/Shutter", "---"); + IUFillSwitchVector(&DomeControlsSP, DomeControlsS, DOME_CONTROL_COUNT, getDeviceName(), "DOME", "Additional controls", + MAIN_CONTROL_TAB, IP_WO, ISR_1OFMANY, 60, IPS_OK); + IUFillSwitch(&DomeControlsS[DOME_SET_PARK], "SET_PARK_SW", "Set Park", ISS_OFF); + IUFillSwitch(&DomeControlsS[DOME_RETURN_HOME], "RETURN_HOME_SW", "Return Home", ISS_OFF); + IUFillSwitch(&DomeControlsS[DOME_SET_HOME], "RESET_HOME_SW", "At Home (Reset)", ISS_OFF); + IUFillTextVector(&DomeStatusTP, DomeStatusT, 1, getDeviceName(), "DOME_STATUS", "Status", + MAIN_CONTROL_TAB, IP_RO, 60, IPS_OK); + IUFillText(&DomeStatusT[0], "DOME_STATUS", "Dome", "---"); + + // Status tab controls + //-------------------- + IUFillTextVector(&Status_ItemsTP, Status_ItemsT, STATUS_ITEMS_COUNT, getDeviceName(), "Status", "OCS Status", + STATUS_TAB, IP_RO, 60, IPS_OK); + IUFillText(&Status_ItemsT[STATUS_FIRMWARE], "FIRMWARE_VERSION", "Firmware version", "---"); + IUFillText(&Status_ItemsT[STATUS_ROOF_LAST_ERROR], "ROOF_LAST_ERROR", "Roof last error", "---"); + IUFillText(&Status_ItemsT[STATUS_MAINS], "MAINS_STATUS", "Mains status", "---"); + IUFillText(&Status_ItemsT[STATUS_OCS_SAFETY], "OCS_SAFETY_STATUS", "OCS safety", "---"); + IUFillText(&Status_ItemsT[STATUS_MCU_TEMPERATURE], "MCU_TEMPERATURE", "MCU temperature °C", "---"); + + // Thermostat tab controls + //------------------------ + IUFillTextVector(&Thermostat_StatusTP, Thermostat_StatusT, THERMOSTAT_COUNT, getDeviceName(), "THERMOSTAT_STATUS", "Obsy Status", + THERMOSTAT_TAB, IP_RO, 60, IPS_OK); + IUFillText(&Thermostat_StatusT[THERMOSTAT_TEMERATURE], "THERMOSTAT_TEMPERATURE", "Temperature °C", "---"); + IUFillText(&Thermostat_StatusT[THERMOSTAT_HUMIDITY], "THERMOSTAT_HUMIDITY", "Humidity %", "---"); + + IUFillNumberVector(&Thermostat_setpointsNP, Thermostat_setpointN, THERMOSTAT_SETPOINT_COUNT, getDeviceName(), "THERMOSTAT_SETPOINTS", "Setpoints", + THERMOSTAT_TAB, IP_RW, 60, IPS_OK); + IUFillNumber(&Thermostat_setpointN[THERMOSTAT_HEAT_SETPOINT], "THERMOSTAT_HEAT_SETPOINT", "Heat °C (0=OFF)", "%.0f", 0, 40, 1, 0); + IUFillNumber(&Thermostat_setpointN[THERMOSTAT_COOL_SETPOINT], "THERMOSTAT_COOL_SETPOINT", "Cool °C (0=OFF)", "%.0f", 0, 40, 1, 0); + IUFillNumber(&Thermostat_setpointN[THERMOSTAT_HUMIDITY_SETPOINT], "THERMOSTAT_HUMIDITY_SETPOINT", "Dehumidify % (0=OFF)", "%.0f", 0, 80, 1, 0); + IUFillSwitchVector(&Thermostat_heat_relaySP, Thermostat_heat_relayS, SWITCH_TOGGLE_COUNT, getDeviceName(), "Thermo_heat_relay", "Heat Relay", + THERMOSTAT_TAB, IP_RO, ISR_1OFMANY, 60, IPS_OK); + IUFillSwitch(&Thermostat_heat_relayS[ON_SWITCH], "Heat_Relay_On", "ON", ISS_OFF); + IUFillSwitch(&Thermostat_heat_relayS[OFF_SWITCH], "Heat_Relay_Off", "OFF", ISS_ON); + IUFillSwitchVector(&Thermostat_cool_relaySP, Thermostat_cool_relayS, SWITCH_TOGGLE_COUNT, getDeviceName(), "Thermo_cool_relay", "Cool Relay", + THERMOSTAT_TAB, IP_RO, ISR_1OFMANY, 60, IPS_OK); + IUFillSwitch(&Thermostat_cool_relayS[ON_SWITCH], "Cool_Relay_On", "ON", ISS_OFF); + IUFillSwitch(&Thermostat_cool_relayS[OFF_SWITCH], "Cool_Relay_Off", "OFF", ISS_ON); + IUFillSwitchVector(&Thermostat_humidity_relaySP, Thermostat_humidity_relayS, SWITCH_TOGGLE_COUNT, getDeviceName(), "Thermo_humidity_relay", "Rh Relay", + THERMOSTAT_TAB, IP_RO, ISR_1OFMANY, 60, IPS_OK); + IUFillSwitch(&Thermostat_humidity_relayS[ON_SWITCH], "Humidity_Relay_On", "ON", ISS_OFF); + IUFillSwitch(&Thermostat_humidity_relayS[OFF_SWITCH], "Humidity_Relay_Off", "OFF", ISS_ON); + + // Power devices tab controls + //--------------------------- + IUFillSwitchVector(&Power_Device1SP, Power_Device1S, SWITCH_TOGGLE_COUNT, getDeviceName(), "POWER_DEVICE1", "Device 1", + POWER_TAB, IP_RW, ISR_1OFMANY, 60, IPS_OK); + IUFillSwitch(&Power_Device1S[ON_SWITCH], "POWER_DEVICE1_ON", "ON", ISS_OFF); + IUFillSwitch(&Power_Device1S[OFF_SWITCH], "POWER_DEVICE1_OFF", "OFF", ISS_ON); + IUFillTextVector(&Power_Device_Name1TP, Power_Device_Name1T, 1, getDeviceName(), "POWER_DEVICE_1_NAME", "Device 1", + POWER_TAB, IP_RO, 60, IPS_OK); + IUFillText(&Power_Device_Name1T[0], "DEVICE_1_NAME", "Name", ""); + + IUFillSwitchVector(&Power_Device2SP, Power_Device2S, SWITCH_TOGGLE_COUNT, getDeviceName(), "POWER_DEVICE2", "Device 2", + POWER_TAB, IP_RW, ISR_1OFMANY, 60, IPS_OK); + IUFillSwitch(&Power_Device2S[ON_SWITCH], "POWER_DEVICE2_ON", "ON", ISS_OFF); + IUFillSwitch(&Power_Device2S[OFF_SWITCH], "POWER_DEVICE2_OFF", "OFF", ISS_ON); + IUFillTextVector(&Power_Device_Name2TP, Power_Device_Name2T, 1, getDeviceName(), "POWER_DEVICE_2_NAME", "Device 2", + POWER_TAB, IP_RO, 60, IPS_OK); + IUFillText(&Power_Device_Name2T[0], "DEVICE_2_NAME", "Name", ""); + + IUFillSwitchVector(&Power_Device3SP, Power_Device3S, SWITCH_TOGGLE_COUNT, getDeviceName(), "POWER_DEVICE3", "Device 3", + POWER_TAB, IP_RW, ISR_1OFMANY, 60, IPS_OK); + IUFillSwitch(&Power_Device3S[ON_SWITCH], "POWER_DEVICE3_ON", "ON", ISS_OFF); + IUFillSwitch(&Power_Device3S[OFF_SWITCH], "POWER_DEVICE3_OFF", "OFF", ISS_ON); + IUFillTextVector(&Power_Device_Name3TP, Power_Device_Name3T, 1, getDeviceName(), "POWER_DEVICE_3_NAME", "Device 3", + POWER_TAB, IP_RO, 60, IPS_OK); + IUFillText(&Power_Device_Name3T[0], "DEVICE_3_NAME", "Name", ""); + + IUFillSwitchVector(&Power_Device4SP, Power_Device4S, SWITCH_TOGGLE_COUNT, getDeviceName(), "POWER_DEVICE4", "Device 4", + POWER_TAB, IP_RW, ISR_1OFMANY, 60, IPS_OK); + IUFillSwitch(&Power_Device4S[ON_SWITCH], "POWER_DEVICE4_ON", "ON", ISS_OFF); + IUFillSwitch(&Power_Device4S[OFF_SWITCH], "POWER_DEVICE4_OFF", "OFF", ISS_ON); + IUFillTextVector(&Power_Device_Name4TP, Power_Device_Name4T, 1, getDeviceName(), "POWER_DEVICE_4_NAME", "Device 4", + POWER_TAB, IP_RO, 60, IPS_OK); + IUFillText(&Power_Device_Name4T[0], "DEVICE_4_NAME", "Name", ""); + + IUFillSwitchVector(&Power_Device5SP, Power_Device5S, SWITCH_TOGGLE_COUNT, getDeviceName(), "POWER_DEVICE5", "Device 5", + POWER_TAB, IP_RW, ISR_1OFMANY, 60, IPS_OK); + IUFillSwitch(&Power_Device5S[ON_SWITCH], "POWER_DEVICE5_ON", "ON", ISS_OFF); + IUFillSwitch(&Power_Device5S[OFF_SWITCH], "POWER_DEVICE5_OFF", "OFF", ISS_ON); + IUFillTextVector(&Power_Device_Name5TP, Power_Device_Name5T, 1, getDeviceName(), "POWER_DEVICE_5_NAME", "Device 5", + POWER_TAB, IP_RO, 60, IPS_OK); + IUFillText(&Power_Device_Name5T[0], "DEVICE_5_NAME", "Name", ""); + + IUFillSwitchVector(&Power_Device6SP, Power_Device6S, SWITCH_TOGGLE_COUNT, getDeviceName(), "POWER_DEVICE6", "Device 6", + POWER_TAB, IP_RW, ISR_1OFMANY, 60, IPS_OK); + IUFillSwitch(&Power_Device6S[ON_SWITCH], "POWER_DEVICE6_ON", "ON", ISS_OFF); + IUFillSwitch(&Power_Device6S[OFF_SWITCH], "POWER_DEVICE6_OFF", "OFF", ISS_ON); + IUFillTextVector(&Power_Device_Name6TP, Power_Device_Name6T, 1, getDeviceName(), "POWER_DEVICE_6_NAME", "Device 6", + POWER_TAB, IP_RO, 60, IPS_OK); + IUFillText(&Power_Device_Name6T[0], "DEVICE_6_NAME", "Name", ""); + + // Lights tab controls + //-------------------- + IUFillSwitchVector(&LIGHT_WRWSP, LIGHT_WRWS, SWITCH_TOGGLE_COUNT, getDeviceName(), "LIGHT_WRW", "Warm Room White", + LIGHTS_TAB, IP_RW, ISR_1OFMANY, 60, IPS_OK); + IUFillSwitch(&LIGHT_WRWS[ON_SWITCH], "WRW_ON", "ON", ISS_OFF); + IUFillSwitch(&LIGHT_WRWS[OFF_SWITCH], "WRW_OFF", "OFF", ISS_ON); + + IUFillSwitchVector(&LIGHT_WRRSP, LIGHT_WRRS, SWITCH_TOGGLE_COUNT, getDeviceName(), "LIGHT_WRR", "Warm Room Red", + LIGHTS_TAB, IP_RW, ISR_1OFMANY, 60, IPS_OK); + IUFillSwitch(&LIGHT_WRRS[ON_SWITCH], "WRR_ON", "ON", ISS_OFF); + IUFillSwitch(&LIGHT_WRRS[OFF_SWITCH], "WRR_OFF", "OFF", ISS_ON); + + IUFillSwitchVector(&LIGHT_ORWSP, LIGHT_ORWS, SWITCH_TOGGLE_COUNT, getDeviceName(), "LIGHT_ORW", "Obsy White", + LIGHTS_TAB, IP_RW, ISR_1OFMANY, 60, IPS_OK); + IUFillSwitch(&LIGHT_ORWS[ON_SWITCH], "ORW_ON", "ON", ISS_OFF); + IUFillSwitch(&LIGHT_ORWS[OFF_SWITCH], "ORW_OFF", "OFF", ISS_ON); + + IUFillSwitchVector(&LIGHT_ORRSP, LIGHT_ORRS, SWITCH_TOGGLE_COUNT, getDeviceName(), "LIGHT_ORR", "Obsy Red", + LIGHTS_TAB, IP_RW, ISR_1OFMANY, 60, IPS_OK); + IUFillSwitch(&LIGHT_ORRS[ON_SWITCH], "ORR_ON", "ON", ISS_OFF); + IUFillSwitch(&LIGHT_ORRS[OFF_SWITCH], "ORR_OFF", "OFF", ISS_ON); + + IUFillSwitchVector(&LIGHT_OUTSIDESP, LIGHT_OUTSIDES, SWITCH_TOGGLE_COUNT, getDeviceName(), "LIGHT_OUTSIDE", "Outside", + LIGHTS_TAB, IP_RW, ISR_1OFMANY, 60, IPS_OK); + IUFillSwitch(&LIGHT_OUTSIDES[ON_SWITCH], "OUTSIDE_ON", "ON", ISS_OFF); + IUFillSwitch(&LIGHT_OUTSIDES[OFF_SWITCH], "OUTSIDE_OFF", "OFF", ISS_ON); + + // Weather tab controls - in addition to the WI managed controls - these are for display only + WI::initProperties(WEATHER_TAB, WEATHER_TAB); + + IUFillTextVector(&Weather_CloudTP, Weather_CloudT, 1, getDeviceName(), "WEATHER_CLOUD", "Cloud", + WEATHER_TAB, IP_RO, 60, IPS_OK); + IUFillText(&Weather_CloudT[0], "WEATHER_CLOUD", "Desciption","---"); + IUFillTextVector(&Weather_SkyTP, Weather_SkyT, 1, getDeviceName(), "WEATHER_SKY", "Sky quality", + WEATHER_TAB, IP_RO, 60, IPS_OK); + IUFillText(&Weather_SkyT[0], "WEATHER_SKY", "mag/arc-sec\u00b2","---"); + IUFillTextVector(&Weather_Sky_TempTP, Weather_Sky_TempT, 1, getDeviceName(), "WEATHER_SKY_TEMP", "Sky temp", + WEATHER_TAB, IP_RO, 60, IPS_OK); + IUFillText(&Weather_Sky_TempT[0], "WEATHER_SKY_TEMP", "°C","---"); + + // Manual tab controls + //-------------------- + IUFillTextVector(&Manual_WarningTP, Manual_WarningT, 2, getDeviceName(), "MANUAL_WARNINGS", "NOTE", + MANUAL_TAB, IP_RO, 60, IPS_ALERT); + IUFillText(&Manual_WarningT[0], "WARNING_LINE1", "CAUTION:", "THESE CONTROLS ARE POTENTIALLY HAZARDOUS"); + IUFillText(&Manual_WarningT[1], "WARNING_LINE2", "CAUTION:", "UNDERSTAND THE IMPLICATIONS BEFORE USING"); + + IUFillSwitchVector(&Safety_Interlock_OverrideSP, Safety_Interlock_OverrideS, 1, getDeviceName(), "SAFETY_INTERLOCK_OVERRIDE", "Interlocks", + MANUAL_TAB, IP_RW, ISR_1OFMANY, 60, IPS_IDLE); + IUFillSwitch(&Safety_Interlock_OverrideS[0], "Safety_Interlock_Override", "OVERRIDE", ISS_OFF); + + IUFillSwitchVector(&Roof_High_PowerSP, Roof_High_PowerS, 1, getDeviceName(), "ROOF_HIGH_POWER", "Roof Power", + MANUAL_TAB, IP_RW, ISR_1OFMANY, 60, IPS_IDLE); + IUFillSwitch(&Roof_High_PowerS[0], "Roof High Power", "MAX", ISS_OFF); + IUFillSwitchVector(&Watchdog_ResetSP, Watchdog_ResetS, 1, getDeviceName(), "WATCHDOG_RESET", "Watchdog", + MANUAL_TAB, IP_RW, ISR_1OFMANY, 60, IPS_IDLE); + IUFillSwitch(&Watchdog_ResetS[0], "Watchdog Reset", "REBOOT", ISS_OFF); + + // Debug only + // IUFillTextVector(&Arbitary_CommandTP, Arbitary_CommandT, 1, getDeviceName(), "ARBITARY_COMMAND", "Command", + // MANUAL_TAB, IP_RW, 60, IPS_IDLE); + // IUFillText(&Arbitary_CommandT[0], "ARBITARY_COMMANDT", "Response:", ":IP#"); + + + // Standard Indi aux controls + //--------------------------- + addAuxControls(); + + return true; +} + +/************************************************************************************** +** INDI request to update the properties because there is a change in CONNECTION status +** This function is called whenever the device is connected or disconnected. +***************************************************************************************/ +bool OCS::updateProperties() +{ + INDI::Dome::updateProperties(); + WI::updateProperties(); + + // Remove unsupported derived controls + //------------------------------------ + deleteProperty(DomeMotionSP.name); + +// if (weather_tab_enabled) { +// WI::updateProperties(); +// } + if (isConnected()) { + defineProperty(&ShutterStatusTP); + defineProperty(&DomeControlsSP); + defineProperty(&DomeStatusTP); + defineProperty(&Status_ItemsTP); + + // Dynamically defined properties + //------------------------------- + if (thermostat_controls_enabled) { + defineProperty(&Thermostat_StatusTP); + defineProperty(&Thermostat_setpointsNP); + defineProperty(&Thermostat_heat_relaySP); + defineProperty(&Thermostat_cool_relaySP); + defineProperty(&Thermostat_humidity_relaySP); + } + if (power_device_relays[0] > 0) { + defineProperty(&Power_Device1SP); + defineProperty(&Power_Device_Name1TP); + } + if (power_device_relays[1] > 0) { + defineProperty(&Power_Device2SP); + defineProperty(&Power_Device_Name2TP); + } + if (power_device_relays[2] > 0) { + defineProperty(&Power_Device3SP); + defineProperty(&Power_Device_Name3TP); + } + if (power_device_relays[3] > 0) { + defineProperty(&Power_Device4SP); + defineProperty(&Power_Device_Name4TP); + } + if (power_device_relays[4] > 0) { + defineProperty(&Power_Device5SP); + defineProperty(&Power_Device_Name5TP); + } + if (power_device_relays[5] > 0) { + defineProperty(&Power_Device6SP); + defineProperty(&Power_Device_Name6TP); + } + if (light_relays[LIGHT_WRW_RELAY] > 0) { + defineProperty(&LIGHT_WRWSP); + } + if (light_relays[LIGHT_WRR_RELAY] > 0) { + defineProperty(&LIGHT_WRRSP); + } + if (light_relays[LIGHT_ORW_RELAY] > 0) { + defineProperty(&LIGHT_ORWSP); + } + if (light_relays[LIGHT_ORR_RELAY] > 0) { + defineProperty(&LIGHT_ORRSP); + } + if (light_relays[LIGHT_OUTSIDE_RELAY] > 0) { + defineProperty(&LIGHT_OUTSIDESP); + } + if (weather_enabled[WEATHER_CLOUD]) { + defineProperty(&Weather_CloudTP); + } + if (weather_enabled[WEATHER_SKY]) { + defineProperty(&Weather_SkyTP); + } + if (weather_enabled[WEATHER_SKY_TEMP]) { + defineProperty(&Weather_Sky_TempTP); + } + //------------------------------------------ + defineProperty(&Manual_WarningTP); + defineProperty(&Safety_Interlock_OverrideSP); + defineProperty(&Roof_High_PowerSP); + defineProperty(&Watchdog_ResetSP); + + // Debug only + // defineProperty(&Arbitary_CommandTP); + } + else { + deleteProperty(ShutterStatusTP.name); + deleteProperty(DomeControlsSP.name); + deleteProperty(DomeStatusTP.name); + deleteProperty(Status_ItemsTP.name); + + // Dynamically defined properties + //------------------------------- + if (thermostat_controls_enabled) { + deleteProperty(Thermostat_StatusTP.name); + deleteProperty(Thermostat_setpointsNP.name); + deleteProperty(Thermostat_heat_relaySP.name); + deleteProperty(Thermostat_cool_relaySP.name); + deleteProperty(Thermostat_humidity_relaySP.name); + } + if (power_device_relays[0] > 0) { + deleteProperty(Power_Device1SP.name); + deleteProperty(Power_Device_Name1TP.name); + } + if (power_device_relays[1] > 0) { + deleteProperty(Power_Device2SP.name); + deleteProperty(Power_Device_Name2TP.name); + } + if (power_device_relays[2] > 0) { + deleteProperty(Power_Device3SP.name); + deleteProperty(Power_Device_Name3TP.name); + } + if (power_device_relays[3] > 0) { + deleteProperty(Power_Device4SP.name); + deleteProperty(Power_Device_Name4TP.name); + } + if (power_device_relays[4] > 0) { + deleteProperty(Power_Device5SP.name); + deleteProperty(Power_Device_Name5TP.name); + } + if (power_device_relays[5] > 0) { + deleteProperty(Power_Device6SP.name); + deleteProperty(Power_Device_Name6TP.name); + } + if (light_relays[LIGHT_WRW_RELAY] > 0) { + deleteProperty(LIGHT_WRWSP.name); + } + if (light_relays[LIGHT_WRR_RELAY] > 0) { + deleteProperty(LIGHT_WRRSP.name); + } + if (light_relays[LIGHT_ORW_RELAY] > 0) { + deleteProperty(LIGHT_ORWSP.name); + } + if (light_relays[LIGHT_ORR_RELAY] > 0) { + deleteProperty(LIGHT_ORRSP.name); + } + if (light_relays[LIGHT_OUTSIDE_RELAY] > 0) { + deleteProperty(LIGHT_OUTSIDESP.name); + } + if (weather_enabled[WEATHER_CLOUD]) { + deleteProperty(Weather_CloudTP.name); + } + if (weather_enabled[WEATHER_SKY]) { + deleteProperty(Weather_SkyTP.name); + } + if (weather_enabled[WEATHER_SKY_TEMP]) { + deleteProperty(Weather_Sky_TempTP.name); + } + //---------------------------------------------- + deleteProperty(Manual_WarningTP.name); + deleteProperty(Safety_Interlock_OverrideSP.name); + deleteProperty(Roof_High_PowerSP.name); + deleteProperty(Watchdog_ResetSP.name); + + // Debug only + // deleteProperty(Arbitary_CommandTP.name); + + // As we're disconnected, stop calling one minute updates + SlowTimer.stop(); + } + + return true; +} + +/************************************************************ +* Poll properties for updates - period set by Options polling +*************************************************************/ +void OCS::TimerHit() +{ + // Get the roof/shutter status + char roof_status_response[RB_MAX_LEN] = {0}; + int roof_status_error_or_fail = getCommandSingleCharErrorOrLongResponse(PortFD, roof_status_response, + OCS_get_roof_status); + if (roof_status_error_or_fail > 1) { + bool roof_was_in_error = (getShutterState() == SHUTTER_ERROR); + + LOGF_DEBUG("roof_was_in_error, %d", roof_was_in_error); + + char *split; + char roof_message[30]; + split = strtok(roof_status_response, ","); + if (strcmp(split, "o") == 0) { + if (getShutterState() != SHUTTER_MOVING) { + setShutterState(SHUTTER_MOVING); + } + split = strtok(NULL, ","); + sprintf(roof_message, "Opening, travel %s", split); + } else if (strcmp(split, "c") == 0) { + if (getShutterState() != SHUTTER_MOVING) { + setShutterState(SHUTTER_MOVING); + } + split = strtok(NULL, ","); + sprintf(roof_message, "Closing, travel %s", split); + } else if (strcmp(split, "i") == 0) { + split = strtok(NULL, ","); + if (strcmp(split, "OPEN") == 0) { + if (getShutterState() != SHUTTER_OPENED) { + setShutterState(SHUTTER_OPENED); + } + sprintf(roof_message, "Idle - Open"); + } else if (strcmp(split, "CLOSED") == 0) { + if (getShutterState() != SHUTTER_CLOSED) { + setShutterState(SHUTTER_CLOSED); + } + sprintf(roof_message, "Idle - Closed"); + } else if (strcmp(split, "No Error") == 0) { + sprintf(roof_message, "Idle - No Error"); + } else if (strcmp(split, "Waiting for mount to park") == 0) { + sprintf(roof_message, "Waiting for mount to park"); + } else { + // Must be an error message + sprintf(roof_message, "Roof/shutter: %s", split); + if (getShutterState() != SHUTTER_ERROR) { + setShutterState(SHUTTER_ERROR); + } + } + } + + if (strcmp(last_shutter_status, roof_message) != 0) { + if (getShutterState() == SHUTTER_ERROR) { + LOGF_ERROR("Roof/shutter error - %s", roof_message); + } else { + LOGF_DEBUG("Roof/shutter is %s", roof_message); + if (roof_was_in_error) { + LOG_INFO("Roof/shutter error cleared"); + } + } + sprintf(last_shutter_status, "%s", roof_message); + } + + IUSaveText(&ShutterStatusT[0], roof_message); + IDSetText(&ShutterStatusTP, nullptr); + } + + // Dome updates + if (hasDome) { + // Get the dome status + char dome_message[10]; + char dome_status_response[RB_MAX_LEN] = {0}; + int dome_status_error_or_fail = getCommandSingleCharErrorOrLongResponse(PortFD, dome_status_response, + OCS_get_dome_status); + if (dome_status_error_or_fail > 1) { //> 1 as an OCS error would be 1 char in response + if (strcmp(dome_status_response, "H") == 0) { + if (getDomeState() != DOME_IDLE) { + setDomeState(DOME_IDLE); + ParkS[0].s = ISS_OFF; + ParkS[1].s = ISS_ON; + ParkSP.s = IPS_OK; + IDSetSwitch(&ParkSP, nullptr); + } + sprintf(dome_message, "Home"); + } else if (strcmp(dome_status_response, "P") == 0) { + if (getDomeState() != DOME_PARKED) { + setDomeState(DOME_PARKED); + ParkS[0].s = ISS_ON; + ParkS[1].s = ISS_OFF; + ParkSP.s = IPS_OK; + IDSetSwitch(&ParkSP, nullptr); + } + sprintf(dome_message, "Parked"); + } else if (strcmp(dome_status_response, "K") == 0) { + if (getDomeState() != DOME_PARKING) { + setDomeState(DOME_PARKING); + ParkS[0].s = ISS_OFF; + ParkS[1].s = ISS_OFF; + ParkSP.s = IPS_BUSY; + IDSetSwitch(&ParkSP, nullptr); + } + sprintf(dome_message, "Parking"); + } else if (strcmp(dome_status_response, "S") == 0) { + if (getDomeState() != DOME_MOVING) { + setDomeState(DOME_MOVING); + ParkS[0].s = ISS_OFF; + ParkS[1].s = ISS_ON; + ParkSP.s = IPS_OK; + IDSetSwitch(&ParkSP, nullptr); + } + sprintf(dome_message, "Slewing"); + } else if (strcmp(dome_status_response, "I") == 0) { + if (getDomeState() != DOME_IDLE) { + setDomeState(DOME_IDLE); + ParkS[0].s = ISS_OFF; + ParkS[1].s = ISS_ON; + ParkSP.s = IPS_OK; + IDSetSwitch(&ParkSP, nullptr); + } + sprintf(dome_message, "Idle"); + } + IUSaveText(&DomeStatusT[0], dome_message); + IDSetText(&DomeStatusTP, nullptr); + } else { + LOGF_WARN("Communication error on get Dome status %s, this update aborted, will try again...", OCS_get_dome_status); + } + + // Get the dome position + char dome_position_response[RB_MAX_LEN] = {0}; + double position = conversion_error ; + int dome_position_error_or_fail = getCommandDoubleResponse(PortFD, &position, dome_position_response, + OCS_get_dome_azimuth); + if (dome_position_error_or_fail > 1 && position != conversion_error) { + DomeAbsPosN->value = position; + IDSetNumber(&DomeAbsPosNP, nullptr); + } else { + LOGF_WARN("Communication error on get Dome position %s, this update aborted, will try again...", OCS_get_dome_azimuth); + } + } + + IDSetText(&Status_ItemsTP, nullptr); + + // Timer loop control + if (!isConnected()) + return; // No need to reset timer if we are not connected anymore + + SetTimer(getCurrentPollingPeriod()); +} + +/*************************************** +* Poll properties for updates per minute +****************************************/ +void OCS::SlowTimerHit() +{ + // Status tab + char power_status_response[RB_MAX_LEN] = {0}; + int power_status_error_or_fail = getCommandSingleCharErrorOrLongResponse(PortFD, power_status_response, + OCS_get_power_status); + if (power_status_error_or_fail > 1) { + IUSaveText(&Status_ItemsT[STATUS_MAINS], power_status_response); + IDSetText(&Status_ItemsTP, nullptr); + } else { + LOGF_WARN("Communication error on get Power Status %s, this update aborted, will try again...", OCS_get_power_status); + } + + char safety_status_response[RB_MAX_LEN] = {0}; + int safety_status_error_or_fail = getCommandSingleCharErrorOrLongResponse(PortFD, safety_status_response, + OCS_get_safety_status); + if (safety_status_error_or_fail > 1) { + IUSaveText(&Status_ItemsT[STATUS_OCS_SAFETY], safety_status_response); + IDSetText(&Status_ItemsTP, nullptr); + } else { + LOGF_WARN("Communication error on get OCS Safety Status %s, this update aborted, will try again...", OCS_get_safety_status); + } + + char MCU_temp_response[RB_MAX_LEN] = {0}; + int MCU_temp_status_error_or_fail = getCommandSingleCharErrorOrLongResponse(PortFD, MCU_temp_response, + OCS_get_MCU_temperature); + if (MCU_temp_status_error_or_fail > 1) { + IUSaveText(&Status_ItemsT[STATUS_MCU_TEMPERATURE], MCU_temp_response); + IDSetText(&Status_ItemsTP, nullptr); + } else { + LOGF_WARN("Communication error on get MCU temperature %s, this update aborted, will try again...", OCS_get_thermostat_status); + } + + // Get the last roof error (if any) + // This is here because although the 1 second polled get roof status would return any error flagged + // at the time it could miss a transient condition that has been cleared in-between poll periods. + // Last roof error holds the condition until cleared by a shutter/roof action. + char roof_error_response[RB_MAX_LEN] = {0}; + int roof_error_error_or_fail = getCommandSingleCharErrorOrLongResponse(PortFD, roof_error_response, + OCS_get_roof_last_error); + if (roof_error_error_or_fail > 1) { + if (strcmp(roof_error_response, "Error: Open safety interlock") == 0 && + strcmp(roof_error_response, last_shutter_error) != 0) { + indi_strlcpy(last_shutter_error,roof_error_response, RB_MAX_LEN); + if (getShutterState() != SHUTTER_ERROR) { + setShutterState(SHUTTER_ERROR); + } + LOG_WARN("Roof/shutter error - Open safety interlock"); + } else if (strcmp(roof_error_response, "Error: Close safety interlock") == 0 && + strcmp(roof_error_response, last_shutter_error) != 0) { + indi_strlcpy(last_shutter_error,roof_error_response, RB_MAX_LEN); + if (getShutterState() != SHUTTER_ERROR) { + setShutterState(SHUTTER_ERROR); + } + LOG_WARN("Roof/shutter error - Close safety interlock"); + } else if (strcmp(roof_error_response, "Error: Open unknown error") == 0 && + strcmp(roof_error_response, last_shutter_error) != 0) { + indi_strlcpy(last_shutter_error,roof_error_response, RB_MAX_LEN); + if (getShutterState() != SHUTTER_ERROR) { + setShutterState(SHUTTER_ERROR); + } + LOG_WARN("Roof/shutter error - Open unknown"); + } else if (strcmp(roof_error_response, "Error: Open limit sw fail") == 0 && + strcmp(roof_error_response, last_shutter_error) != 0) { + indi_strlcpy(last_shutter_error,roof_error_response, RB_MAX_LEN); + if (getShutterState() != SHUTTER_ERROR) { + setShutterState(SHUTTER_ERROR); + } + LOG_WARN("Roof/shutter error - Open limit switch fail"); + } else if (strcmp(roof_error_response, "Error: Open over time") == 0 && + strcmp(roof_error_response, last_shutter_error) != 0) { + indi_strlcpy(last_shutter_error,roof_error_response, RB_MAX_LEN); + if (getShutterState() != SHUTTER_ERROR) { + setShutterState(SHUTTER_ERROR); + } + LOG_WARN("Roof/shutter error - Open max time exceeded"); + } else if (strcmp(roof_error_response, "Error: Open under time") == 0 && + strcmp(roof_error_response, last_shutter_error) != 0) { + indi_strlcpy(last_shutter_error,roof_error_response, RB_MAX_LEN); + if (getShutterState() != SHUTTER_ERROR) { + setShutterState(SHUTTER_ERROR); + } + LOG_WARN("Roof/shutter error - Open min time not reached"); + } else if (strcmp(roof_error_response, "Error: Close unknown error") == 0 && + strcmp(roof_error_response, last_shutter_error) != 0) { + indi_strlcpy(last_shutter_error,roof_error_response, RB_MAX_LEN); + if (getShutterState() != SHUTTER_ERROR) { + setShutterState(SHUTTER_ERROR); + } + LOG_WARN("Roof/shutter error - Close unknow"); + } else if (strcmp(roof_error_response, "Error: Close limit sw fail") == 0 && + strcmp(roof_error_response, last_shutter_error) != 0) { + indi_strlcpy(last_shutter_error,roof_error_response, RB_MAX_LEN); + if (getShutterState() != SHUTTER_ERROR) { + setShutterState(SHUTTER_ERROR); + } + LOG_WARN("Roof/shutter error - Close limit switch"); + } else if (strcmp(roof_error_response, "Error: Close over time") == 0 && + strcmp(roof_error_response, last_shutter_error) != 0) { + indi_strlcpy(last_shutter_error,roof_error_response, RB_MAX_LEN); + if (getShutterState() != SHUTTER_ERROR) { + setShutterState(SHUTTER_ERROR); + } + LOG_WARN("Roof/shutter error - Close max time exceeded"); + } else if (strcmp(roof_error_response, "Error: Close under tim") == 0 && + strcmp(roof_error_response, last_shutter_error) != 0) { + indi_strlcpy(last_shutter_error,roof_error_response, RB_MAX_LEN); + LOG_WARN("Roof/shutter error - Close min time not reached"); + if (getShutterState() != SHUTTER_ERROR) { + setShutterState(SHUTTER_ERROR); + } + } else if (strcmp(roof_error_response, "Error: Limit switch malfunction") == 0 && + strcmp(roof_error_response, last_shutter_error) != 0) { + indi_strlcpy(last_shutter_error,roof_error_response, RB_MAX_LEN); + if (getShutterState() != SHUTTER_ERROR) { + setShutterState(SHUTTER_ERROR); + } + LOG_WARN("Roof/shutter error - Both open & close limit switches active together"); + } else if (strcmp(roof_error_response, "Error: Closed/opened limit sw on") == 0 && + strcmp(roof_error_response, last_shutter_error) != 0) { + indi_strlcpy(last_shutter_error,roof_error_response, RB_MAX_LEN); + if (getShutterState() != SHUTTER_ERROR) { + setShutterState(SHUTTER_ERROR); + } + LOG_WARN("Roof/shutter error - Closed/opened limit switch on"); + } else if (strcmp(roof_error_response, "Warning: Already closed") == 0 && + strcmp(roof_error_response, last_shutter_error) != 0) { + indi_strlcpy(last_shutter_error,roof_error_response, RB_MAX_LEN); + LOG_WARN("Roof/shutter warning - Roof/shutter is already closed"); + } else if (strcmp(roof_error_response, "Error: Close location unknown") == 0 && + strcmp(roof_error_response, last_shutter_error) != 0) { + indi_strlcpy(last_shutter_error,roof_error_response, RB_MAX_LEN); + if (getShutterState() != SHUTTER_ERROR) { + setShutterState(SHUTTER_ERROR); + } + LOG_WARN("Roof/shutter error - Close location unknown"); + } else if (strcmp(roof_error_response, "Error: Motion direction unknown") == 0 && + strcmp(roof_error_response, last_shutter_error) != 0) { + indi_strlcpy(last_shutter_error,roof_error_response, RB_MAX_LEN); + if (getShutterState() != SHUTTER_ERROR) { + setShutterState(SHUTTER_ERROR); + } + LOG_WARN("Roof/shutter error - Motion direction unknown"); + } else if (strcmp(roof_error_response, "Error: Close already in motion") == 0 && + strcmp(roof_error_response, last_shutter_error) != 0) { + indi_strlcpy(last_shutter_error,roof_error_response, RB_MAX_LEN); + if (getShutterState() != SHUTTER_ERROR) { + setShutterState(SHUTTER_ERROR); + } + LOG_WARN("Roof/shutter error - Close already in motion"); + } else if (strcmp(roof_error_response, "Error: Opened/closed limit sw on") == 0 && + strcmp(roof_error_response, last_shutter_error) != 0) { + indi_strlcpy(last_shutter_error,roof_error_response, RB_MAX_LEN); + if (getShutterState() != SHUTTER_ERROR) { + setShutterState(SHUTTER_ERROR); + } + LOG_WARN("Roof/shutter error - Opened/closed limit switch on"); + } else if (strcmp(roof_error_response, "Warning: Already open") == 0 && + strcmp(roof_error_response, last_shutter_error) != 0) { + indi_strlcpy(last_shutter_error,roof_error_response, RB_MAX_LEN); + LOG_WARN("Roof/shutter warning - Roof/shutter is already open"); + } else if (strcmp(roof_error_response, "Error: Open location unknow") == 0 && + strcmp(roof_error_response, last_shutter_error) != 0) { + indi_strlcpy(last_shutter_error,roof_error_response, RB_MAX_LEN); + if (getShutterState() != SHUTTER_ERROR) { + setShutterState(SHUTTER_ERROR); + } + LOG_WARN("Roof/shutter error - Open location unknow"); + } else if (strcmp(roof_error_response, "Error: Open already in motion") == 0 && + strcmp(roof_error_response, last_shutter_error) != 0) { + indi_strlcpy(last_shutter_error,roof_error_response, RB_MAX_LEN); + if (getShutterState() != SHUTTER_ERROR) { + setShutterState(SHUTTER_ERROR); + } + LOG_WARN("Roof/shutter error - Open already in motion"); + } else if (strcmp(roof_error_response, "Error: Close mount not parked") == 0 && + strcmp(roof_error_response, last_shutter_error) != 0) { + indi_strlcpy(last_shutter_error,roof_error_response, RB_MAX_LEN); + if (getShutterState() != SHUTTER_ERROR) { + setShutterState(SHUTTER_ERROR); + } + LOG_WARN("Roof/shutter error - Timeout waiting for mount to park before closing"); + } + IUSaveText(&Status_ItemsT[STATUS_ROOF_LAST_ERROR], last_shutter_error); + } else if (roof_error_error_or_fail == 1) { + LOGF_WARN("Communication error on get Roof/Shutter last error %s, this update aborted, will try again...", OCS_get_roof_last_error); + } + + // Thermostat tab + if (thermostat_controls_enabled) { + // Get the Obsy Thermostat readings + char thermostat_status_response[RB_MAX_LEN] = {0}; + int thermostat_status_error_or_fail = getCommandSingleCharErrorOrLongResponse(PortFD, thermostat_status_response, + OCS_get_thermostat_status); + if (thermostat_status_error_or_fail > 1) { + char *split; + split = strtok(thermostat_status_response, ","); + IUSaveText(&Thermostat_StatusT[THERMOSTAT_TEMERATURE], split); + split = strtok(NULL, ","); + IUSaveText(&Thermostat_StatusT[THERMOSTAT_HUMIDITY], split); + IDSetText(&Thermostat_StatusTP, nullptr); + } else { + LOGF_WARN("Communication error on get Thermostat Status %s, this update aborted, will try again...", OCS_get_thermostat_status); + } + + // Get the Thermostat setpoints + char heat_response[RB_MAX_LEN] = {0}; + int heat_int_response = 0; + int heat_setpoint_error_or_fail = getCommandIntFromCharResponse(PortFD, heat_response, &heat_int_response, + OCS_get_thermostat_heat_setpoint); + if (heat_setpoint_error_or_fail >= 0 && heat_int_response != conversion_error) { // errors are negative + Thermostat_setpointN[THERMOSTAT_HEAT_SETPOINT].value = heat_int_response; + } else { + LOGF_WARN("Communication error on get Thermostat Heat Setpoint %d, this update aborted, will try again...", heat_int_response); + } + + char cool_response[RB_MAX_LEN] = {0}; + int cool_int_response = 0; + int cool_setpoint_error_or_fail = getCommandIntFromCharResponse(PortFD, cool_response, &cool_int_response, + OCS_get_thermostat_cool_setpoint); + if (cool_setpoint_error_or_fail >= 0 && cool_int_response != conversion_error) { // errors are negative + Thermostat_setpointN[THERMOSTAT_COOL_SETPOINT].value = cool_int_response; + } else { + LOGF_WARN("Communication error on get Thermostat Cool Setpoint %d, this update aborted, will try again...", cool_int_response); + } + + char humidity_response[RB_MAX_LEN] = {0}; + int humidity_int_response = 0; + int humidity_setpoint_error_or_fail = getCommandIntFromCharResponse(PortFD, humidity_response, &humidity_int_response, + OCS_get_thermostat_humidity_setpoint); + if (humidity_setpoint_error_or_fail >= 0 && humidity_int_response != conversion_error) { // errors are negative + Thermostat_setpointN[THERMOSTAT_HUMIDITY_SETPOINT].value = humidity_int_response; + } else { + LOGF_WARN("Communication error on get Thermostat Humidity Setpoint %d, this update aborted, will try again...", humidity_int_response); + } + IDSetNumber(&Thermostat_setpointsNP, nullptr); + + // Get the Thermostat relay status' + for (int relay = 0; relay < THERMOSTAT_RELAY_COUNT; relay++) { + if (thermostat_relays[relay] > 0) { + char thermo_relay_response[RB_MAX_LEN] = {0}; + char thermo_relay_command[RB_MAX_LEN] = {0}; + sprintf(thermo_relay_command, "%s%d%s", OCS_get_relay_part, thermostat_relays[relay], OCS_command_terminator); + int thermo_relay_error_or_fail = getCommandSingleCharErrorOrLongResponse(PortFD, thermo_relay_response, + thermo_relay_command); + if (thermo_relay_error_or_fail > 1) { + if (relay == THERMOSTAT_HEAT_RELAY) { + if (strcmp(thermo_relay_response, "ON") == 0) { + Thermostat_heat_relayS[ON_SWITCH].s = ISS_ON; + Thermostat_heat_relayS[OFF_SWITCH].s = ISS_OFF; + } else if (strcmp(thermo_relay_response, "OFF") == 0) { + Thermostat_heat_relayS[ON_SWITCH].s = ISS_OFF; + Thermostat_heat_relayS[OFF_SWITCH].s = ISS_ON; + } + IDSetSwitch(&Thermostat_heat_relaySP, nullptr); + } else if (relay == THERMOSTAT_COOL_RELAY) { + if (strcmp(thermo_relay_response, "ON") == 0) { + Thermostat_cool_relayS[ON_SWITCH].s = ISS_ON; + Thermostat_cool_relayS[OFF_SWITCH].s = ISS_OFF; + } else if (strcmp(thermo_relay_response, "OFF") == 0) { + Thermostat_cool_relayS[ON_SWITCH].s = ISS_OFF; + Thermostat_cool_relayS[OFF_SWITCH].s = ISS_ON; + } + IDSetSwitch(&Thermostat_cool_relaySP, nullptr); + } else if (relay == THERMOSTAT_HUMIDITY_RELAY) { + if (strcmp(thermo_relay_response, "ON") == 0) { + Thermostat_humidity_relayS[ON_SWITCH].s = ISS_ON; + Thermostat_humidity_relayS[OFF_SWITCH].s = ISS_OFF; + } else if (strcmp(thermo_relay_response, "OFF") == 0) { + Thermostat_humidity_relayS[ON_SWITCH].s = ISS_OFF; + Thermostat_humidity_relayS[OFF_SWITCH].s = ISS_ON; + } + IDSetSwitch(&Thermostat_humidity_relaySP, nullptr); + } + } + } + } + } + + // Power tab + if (power_tab_enabled) { + // Get the Power relay status' + for (int relay = 0; relay < POWER_DEVICE_COUNT; relay++) { + if (power_device_relays[relay] > 0) { + char power_relay_response[RB_MAX_LEN] = {0}; + char power_relay_command[RB_MAX_LEN] = {0}; + sprintf(power_relay_command, "%s%d%s", OCS_get_relay_part, power_device_relays[relay], OCS_command_terminator); + int power_relay_error_or_fail = getCommandSingleCharErrorOrLongResponse(PortFD, power_relay_response, + power_relay_command); + if (power_relay_error_or_fail > 1) { + if (relay == POWER_DEVICE1) { + if (strcmp(power_relay_response, "ON") == 0) { + Power_Device1S[ON_SWITCH].s = ISS_ON; + Power_Device1S[OFF_SWITCH].s = ISS_OFF; + } else if (strcmp(power_relay_response, "OFF") == 0) { + Power_Device1S[ON_SWITCH].s = ISS_OFF; + Power_Device1S[OFF_SWITCH].s = ISS_ON; + } + IDSetSwitch(&Power_Device1SP, nullptr); + } else if (relay == POWER_DEVICE2) { + if (strcmp(power_relay_response, "ON") == 0) { + Power_Device2S[ON_SWITCH].s = ISS_ON; + Power_Device2S[OFF_SWITCH].s = ISS_OFF; + } else if (strcmp(power_relay_response, "OFF") == 0) { + Power_Device2S[ON_SWITCH].s = ISS_OFF; + Power_Device2S[OFF_SWITCH].s = ISS_ON; + } + IDSetSwitch(&Power_Device2SP, nullptr); + } else if (relay == POWER_DEVICE3) { + if (strcmp(power_relay_response, "ON") == 0) { + Power_Device3S[ON_SWITCH].s = ISS_ON; + Power_Device3S[OFF_SWITCH].s = ISS_OFF; + } else if (strcmp(power_relay_response, "OFF") == 0) { + Power_Device3S[ON_SWITCH].s = ISS_OFF; + Power_Device3S[OFF_SWITCH].s = ISS_ON; + } + IDSetSwitch(&Power_Device3SP, nullptr); + } else if (relay == POWER_DEVICE4) { + if (strcmp(power_relay_response, "ON") == 0) { + Power_Device4S[ON_SWITCH].s = ISS_ON; + Power_Device4S[OFF_SWITCH].s = ISS_OFF; + } else if (strcmp(power_relay_response, "OFF") == 0) { + Power_Device4S[ON_SWITCH].s = ISS_OFF; + Power_Device4S[OFF_SWITCH].s = ISS_ON; + } + IDSetSwitch(&Power_Device4SP, nullptr); + } else if (relay == POWER_DEVICE5) { + if (strcmp(power_relay_response, "ON") == 0) { + Power_Device5S[ON_SWITCH].s = ISS_ON; + Power_Device5S[OFF_SWITCH].s = ISS_OFF; + } else if (strcmp(power_relay_response, "OFF") == 0) { + Power_Device5S[ON_SWITCH].s = ISS_OFF; + Power_Device5S[OFF_SWITCH].s = ISS_ON; + } + IDSetSwitch(&Power_Device5SP, nullptr); + } else if (relay == POWER_DEVICE6) { + if (strcmp(power_relay_response, "ON") == 0) { + Power_Device6S[ON_SWITCH].s = ISS_ON; + Power_Device6S[OFF_SWITCH].s = ISS_OFF; + } else if (strcmp(power_relay_response, "OFF") == 0) { + Power_Device6S[ON_SWITCH].s = ISS_OFF; + Power_Device6S[OFF_SWITCH].s = ISS_ON; + } + IDSetSwitch(&Power_Device6SP, nullptr); + } + } + } + } + } + + // Lights tab + if (lights_tab_enabled) { + // Get the Lights relay status' + for (int relay = 0; relay < LIGHT_COUNT; relay++) { + if (light_relays[relay] > 0) { + char light_relay_response[RB_MAX_LEN] = {0}; + char light_relay_command[RB_MAX_LEN] = {0}; + sprintf(light_relay_command, "%s%d%s", OCS_get_relay_part, light_relays[relay], OCS_command_terminator); + int light_relay_error_or_fail = getCommandSingleCharErrorOrLongResponse(PortFD, light_relay_response, + light_relay_command); + if (light_relay_error_or_fail > 1) { + if (relay == LIGHT_WRW_RELAY) { + if (strcmp(light_relay_response, "ON") == 0) { + LIGHT_WRWS[ON_SWITCH].s = ISS_ON; + LIGHT_WRWS[OFF_SWITCH].s = ISS_OFF; + } else if (strcmp(light_relay_response, "OFF") == 0) { + LIGHT_WRWS[ON_SWITCH].s = ISS_OFF; + LIGHT_WRWS[OFF_SWITCH].s = ISS_ON; + } + IDSetSwitch(&LIGHT_WRWSP, nullptr); + } else if (relay == LIGHT_WRR_RELAY) { + if (strcmp(light_relay_response, "ON") == 0) { + LIGHT_WRRS[ON_SWITCH].s = ISS_ON; + LIGHT_WRRS[OFF_SWITCH].s = ISS_OFF; + } else if (strcmp(light_relay_response, "OFF") == 0) { + LIGHT_WRRS[ON_SWITCH].s = ISS_OFF; + LIGHT_WRRS[OFF_SWITCH].s = ISS_ON; + } + IDSetSwitch(&LIGHT_WRRSP, nullptr); + } else if (relay == LIGHT_ORW_RELAY) { + if (strcmp(light_relay_response, "ON") == 0) { + LIGHT_ORWS[ON_SWITCH].s = ISS_ON; + LIGHT_ORWS[OFF_SWITCH].s = ISS_OFF; + } else if (strcmp(light_relay_response, "OFF") == 0) { + LIGHT_ORWS[ON_SWITCH].s = ISS_OFF; + LIGHT_ORWS[OFF_SWITCH].s = ISS_ON; + } + IDSetSwitch(&LIGHT_ORWSP, nullptr); + } else if (relay == LIGHT_ORR_RELAY) { + if (strcmp(light_relay_response, "ON") == 0) { + LIGHT_ORRS[ON_SWITCH].s = ISS_ON; + LIGHT_ORRS[OFF_SWITCH].s = ISS_OFF; + } else if (strcmp(light_relay_response, "OFF") == 0) { + LIGHT_ORRS[ON_SWITCH].s = ISS_OFF; + LIGHT_ORRS[OFF_SWITCH].s = ISS_ON; + } + IDSetSwitch(&LIGHT_ORRSP, nullptr); + } else if (relay == LIGHT_OUTSIDE_RELAY) { + if (strcmp(light_relay_response, "ON") == 0) { + LIGHT_OUTSIDES[ON_SWITCH].s = ISS_ON; + LIGHT_OUTSIDES[OFF_SWITCH].s = ISS_OFF; + } else if (strcmp(light_relay_response, "OFF") == 0) { + LIGHT_OUTSIDES[ON_SWITCH].s = ISS_OFF; + LIGHT_OUTSIDES[OFF_SWITCH].s = ISS_ON; + } + IDSetSwitch(&LIGHT_OUTSIDESP, nullptr); + } + } + } + } + } +} + +/***************************************************************** +* Poll Weather properties for updates - period set by Weather poll +******************************************************************/ +IPState OCS::updateWeather() { + if (weather_tab_enabled) { + for (int measurement = 0; measurement < WEATHER_MEASUREMENTS_COUNT; measurement ++) { + if (weather_enabled[measurement] == 1) { + char measurement_reponse[RB_MAX_LEN]; + char measurement_command[CMD_MAX_LEN]; + if (measurement == WEATHER_TEMPERATURE) { + indi_strlcpy(measurement_command, OCS_get_outside_temperature, sizeof(measurement_command)); + } else if (measurement == WEATHER_SKY_TEMP) { + indi_strlcpy(measurement_command, OCS_get_sky_IR_temperature, sizeof(measurement_command)); + } else if (measurement == WEATHER_DIFF_SKY_TEMP) { + indi_strlcpy(measurement_command, OCS_get_sky_diff_temperature, sizeof(measurement_command)); + } else if (measurement == WEATHER_PRESSURE) { + indi_strlcpy(measurement_command, OCS_get_pressure, sizeof(measurement_command)); + } else if (measurement == WEATHER_HUMIDITY) { + indi_strlcpy(measurement_command, OCS_get_humidity, sizeof(measurement_command)); + } else if (measurement == WEATHER_WIND) { + indi_strlcpy(measurement_command, OCS_get_wind_speed, sizeof(measurement_command)); + } else if (measurement == WEATHER_RAIN) { + indi_strlcpy(measurement_command, OCS_get_rain_sensor_status, sizeof(measurement_command)); + } else if (measurement == WEATHER_CLOUD) { + indi_strlcpy(measurement_command, OCS_get_cloud_description, sizeof(measurement_command)); + } else if (measurement == WEATHER_SKY) { + indi_strlcpy(measurement_command, OCS_get_sky_quality, sizeof(measurement_command)); + } + double value = conversion_error; + int measurement_error_or_fail = getCommandDoubleResponse(PortFD, &value, measurement_reponse, + measurement_command); + if (measurement_error_or_fail >= 0 && value != conversion_error) { + if (measurement == WEATHER_TEMPERATURE && weather_enabled[WEATHER_TEMPERATURE] == 1) { + setParameterValue("WEATHER_TEMPERATURE", value); + } else if (measurement == WEATHER_PRESSURE && weather_enabled[WEATHER_PRESSURE] == 1) { + setParameterValue("WEATHER_PRESSURE", value); + } else if (measurement == WEATHER_HUMIDITY && weather_enabled[WEATHER_HUMIDITY] == 1) { + setParameterValue("WEATHER_HUMIDITY", value); + } else if (measurement == WEATHER_WIND && weather_enabled[WEATHER_WIND] == 1) { + setParameterValue("WEATHER_WIND", value); + } else if (measurement == WEATHER_DIFF_SKY_TEMP && weather_enabled[WEATHER_DIFF_SKY_TEMP] == 1) { + setParameterValue("WEATHER_SKY_DIFF_TEMP", value); + } else if (measurement == WEATHER_CLOUD && weather_enabled[WEATHER_CLOUD] ==1) { + IUSaveText(&Weather_CloudT[0], measurement_reponse); + IDSetText(&Weather_CloudTP, nullptr); + } else if (measurement == WEATHER_SKY && weather_enabled[WEATHER_SKY] ==1) { + IUSaveText(&Weather_SkyT[0], measurement_reponse); + IDSetText(&Weather_SkyTP, nullptr); + } else if (measurement == WEATHER_SKY_TEMP && weather_enabled[WEATHER_SKY_TEMP] ==1) { + IUSaveText(&Weather_Sky_TempT[0], measurement_reponse); + IDSetText(&Weather_Sky_TempTP, nullptr); + } + } + } + } + if (WI::syncCriticalParameters()) + { + LOG_DEBUG("SyncCriticalParameters = true"); + } else { + LOG_DEBUG("SyncCriticalParameters = false"); + } + } + + return IPS_OK; +} + +/************************************* + * Stop any roof/shutter/dome movement + * ***********************************/ +bool OCS::Abort() +{ + sendOCSCommandBlind(OCS_roof_stop); + sendOCSCommandBlind(OCS_dome_stop); + return true; +} + +/********************** + * Roof/shutter control + * ********************/ +IPState OCS::ControlShutter(ShutterOperation operation) +{ + if (operation == SHUTTER_OPEN) { + // Sending roof/shutter commands clears any OCS roof errors so we need to do the same here + indi_strlcpy(last_shutter_error, "", RB_MAX_LEN); + sendOCSCommandBlind(OCS_roof_open); + } + else if (operation == SHUTTER_CLOSE) { + // Sending roof/shutter commands clears any OCS roof errors so we need to do the same here + indi_strlcpy(last_shutter_error, "", RB_MAX_LEN); + sendOCSCommandBlind(OCS_roof_close); + } + + // We have to delay the polling timer to account for the delays built + // into the functions feeding into the OCS get roof status function + // that allow for the delays between roof/shutter start/end of travel + // and the activation of the respective interlock switches + // Delay from OCS in seconds, need to convert to ms and add 1/2 second + SetTimer((ROOF_TIME_PRE_MOTION * 1000) + 500); + + return IPS_BUSY; +} + +/************** + * Dome control + * ************/ + +/************************************ + * Send the dome to its park position + * **********************************/ +IPState OCS::Park() +{ + if (sendOCSCommand(OCS_dome_park)) { + setDomeState(DOME_PARKING); + return IPS_BUSY; + } else { + setDomeState(DOME_ERROR); + return IPS_ALERT; + } +} + +/********************************************************* + * Bring the dome out of parked status - doesn't move dome + * *******************************************************/ +IPState OCS::UnPark() +{ + if (sendOCSCommand(OCS_restore_dome_park)) { + setDomeState(DOME_UNPARKING); + return IPS_OK; + } else { + setDomeState(DOME_ERROR); + return IPS_ALERT; + } +} + +/************************************************************ + * Set the current dome Azimuth position as the park position + * **********************************************************/ +bool OCS::SetCurrentPark() +{ + if (sendOCSCommand(OCS_set_dome_park)) { + return true; + } else { + setDomeState(DOME_ERROR); + LOG_ERROR("Failed to set park position"); + return false; + } +} + +/************************************ + * Send the dome to the Home position + * **********************************/ +bool OCS::ReturnHome() +{ + // This command has no return + sendOCSCommandBlind(OCS_dome_home); + return true; +} + +/************************************************************ + * Set the current dome Azimuth position as the home position + * **********************************************************/ +bool OCS::ResetHome() +{ + // This command has no return + sendOCSCommand(OCS_reset_dome_home); + return true; +} + +/********************************** + * Move dome to an absoute position + * ********************************/ +IPState OCS::MoveAbs(double az) +{ + char set_dome_azimuth_command[CMD_MAX_LEN] = {0}; + sprintf(set_dome_azimuth_command, "%s%f%s", + OCS_set_dome_azimuth_part, az, OCS_command_terminator); + sendOCSCommandBlind(set_dome_azimuth_command); + char dome_goto_target_response[RB_MAX_LEN] = {0}; + int dome_goto_target_int_response = 0; + int dome_goto_target_error_or_fail = getCommandIntResponse(PortFD, &dome_goto_target_int_response, dome_goto_target_response, + OCS_dome_goto_taget); + if (dome_goto_target_error_or_fail >= 1) { + switch (dome_goto_target_int_response) { + case GOTO_IS_POSSIBLE: + LOGF_INFO("Begin dome move to %1.1f°", az); + return IPS_BUSY; + break; + case BELOW_HORIZON_LIMIT: + // Should never get here - Indi doesn't support dome Alt + LOGF_ERROR("Dome target (%1.1f°) is below the horizon limit", az); + return IPS_ALERT; + break; + case ABOVE_OVERHEAD_LIMIT: + // Should never get here - Indi doesn't support dome Alt + LOGF_ERROR("Dome target (%1.1f°) is above the overhead limit", az); + return IPS_ALERT; + break; + case CONTROLLER_IN_STANDBY: + LOG_ERROR("Dome can not move, controller in standby"); + return IPS_ALERT; + break; + case DOME_IS_PARKED: + LOG_ERROR("Dome can not move, dome is parked"); + return IPS_ALERT; + break; + case GOTO_IN_PROGRESS: + LOG_ERROR("Can not ask dome to move, dome is already moving"); + return IPS_ALERT; + break; + case OUTSIDE_LIMITS: + LOGF_ERROR("Dome target (%1.1f°) is outside safe limits", az); + return IPS_ALERT; + break; + case HARDWARE_FAULT: + LOG_ERROR("Dome can not move, hardware fault"); + return IPS_ALERT; + break; + case ALREADY_IN_MOTION: + LOG_ERROR("Can not ask dome to move, dome is already moving"); + return IPS_ALERT; + break; + case UNSPECIFIED_ERROR: + LOG_ERROR("Dome returned an unspecified error"); + return IPS_ALERT; + break; + default: + return IPS_IDLE; + break; + } + } else { + LOGF_ERROR("Dome goto produced error %s", dome_goto_target_error_or_fail); + return IPS_ALERT; + } +} + +/**************************************************** + * Sync domes actual position to supplied co-ordinate + * **************************************************/ +bool OCS::Sync(double az) { + char set_dome_azimuth_command[CMD_MAX_LEN] = {0}; + sprintf(set_dome_azimuth_command, "%s%f%s", + OCS_set_dome_azimuth_part, az, OCS_command_terminator); + sendOCSCommandBlind(set_dome_azimuth_command); + char dome_sync_target_response[RB_MAX_LEN] = {0}; + int dome_sync_target_int_response = 0; + int dome_sync_target_error_or_fail = getCommandIntResponse(PortFD, &dome_sync_target_int_response, dome_sync_target_response, + OCS_dome_sync_target); + if (dome_sync_target_error_or_fail >= 1) { + switch (dome_sync_target_int_response) { + case GOTO_IS_POSSIBLE: + LOGF_INFO("Dome syncronised to %1.1f°", az); + return true; + break; + case BELOW_HORIZON_LIMIT: + // Should never get here - Indi doesn't support dome Alt + LOGF_ERROR("Dome target (%1.1f°) is below the horizon limit", az); + return false; + break; + case ABOVE_OVERHEAD_LIMIT: + // Should never get here - Indi doesn't support dome Alt + LOGF_ERROR("Dome target (%1.1f°) is above the overhead limit", az); + return false; + break; + case CONTROLLER_IN_STANDBY: + LOG_ERROR("Dome can not sync, controller in standby"); + return false; + break; + case DOME_IS_PARKED: + LOG_ERROR("Dome can not sync, dome is parked"); + return false; + break; + case GOTO_IN_PROGRESS: + LOG_ERROR("Can not ask dome to sync, dome is moving"); + return false; + break; + case OUTSIDE_LIMITS: + LOGF_ERROR("Dome sync target (%1.1f°) is outside safe limits", az); + return false; + break; + case HARDWARE_FAULT: + LOG_ERROR("Dome can not sync, hardware fault"); + return false; + break; + case ALREADY_IN_MOTION: + LOG_ERROR("Can not ask dome to sync, dome is moving"); + return false; + break; + case UNSPECIFIED_ERROR: + LOG_ERROR("Dome returned an unspecified error"); + return false; + break; + default: + return false; + break; + } + } else { + LOGF_ERROR("Dome sync to target produced error %s", dome_sync_target_response); + return false; + } +} + +/*********************************************************** +** Client is asking us to establish connection to the device +************************************************************/ +bool OCS::Connect() +{ + bool status = INDI::Dome::Connect(); + return status; +} + +/*********************************************************** +** Client is asking us to terminate connection to the device +************************************************************/ +bool OCS::Disconnect() +{ + bool status = INDI::Dome::Disconnect(); + return status; +} + +//******************* +// Required overrides +//******************/ +void ISPoll(void *p); + +void ISSnoopDevice(XMLEle *root) +{ + ocs->ISSnoopDevice(root); +} + +void OCS::ISGetProperties(const char *dev) +{ + INDI::Dome::ISGetProperties(dev); +} + +bool OCS::saveConfigItems(FILE *fp) +{ + INDI::Dome::saveConfigItems(fp); + WI::saveConfigItems(fp); + return true; +} + +/************************************************** + * Client has changed the state of a switch, update + **************************************************/ +bool OCS::ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) +{ + if (dev != nullptr && strcmp(dev, getDeviceName()) == 0) { + + LOGF_DEBUG("Got an IsNewSwitch for: %s", name); + + // Power devices + //-------------- + if (strcmp(Power_Device1SP.name, name) == 0) { + IUUpdateSwitch(&Power_Device1SP, states, names, n); + for (int i = 0; i < n; i++) { + if (strcmp(names[i], "POWER_DEVICE1_ON") == 0) { + char set_power_dev_1_on_cmd[CMD_MAX_LEN]; + sprintf(set_power_dev_1_on_cmd, "%s%d,ON%s", OCS_set_relay_part, power_device_relays[POWER_DEVICE1], OCS_command_terminator); + IDSetSwitch(&Power_Device1SP, nullptr); + return sendOCSCommand(set_power_dev_1_on_cmd); + } else if (strcmp(names[i], "POWER_DEVICE1_OFF") == 0) { + char set_power_dev_1_off_cmd[CMD_MAX_LEN]; + sprintf(set_power_dev_1_off_cmd, "%s%d,OFF%s", OCS_set_relay_part, power_device_relays[POWER_DEVICE1], OCS_command_terminator); + IDSetSwitch(&Power_Device1SP, nullptr); + return sendOCSCommand(set_power_dev_1_off_cmd); + } + } + IDSetSwitch(&Power_Device1SP, nullptr); + return false; + } else if (strcmp(Power_Device2SP.name, name) == 0) { + IUUpdateSwitch(&Power_Device2SP, states, names, n); + for (int i = 0; i < n; i++) { + if (strcmp(names[i], "POWER_DEVICE2_ON") == 0) { + char set_power_dev_2_on_cmd[CMD_MAX_LEN]; + sprintf(set_power_dev_2_on_cmd, "%s%d,ON%s", OCS_set_relay_part, power_device_relays[POWER_DEVICE2], OCS_command_terminator); + IDSetSwitch(&Power_Device2SP, nullptr); + return sendOCSCommand(set_power_dev_2_on_cmd); + } else if (strcmp(names[i], "POWER_DEVICE2_OFF") == 0) { + char set_power_dev_2_off_cmd[CMD_MAX_LEN]; + sprintf(set_power_dev_2_off_cmd, "%s%d,OFF%s", OCS_set_relay_part, power_device_relays[POWER_DEVICE2], OCS_command_terminator); + IDSetSwitch(&Power_Device2SP, nullptr); + return sendOCSCommand(set_power_dev_2_off_cmd); + } + } + IDSetSwitch(&Power_Device2SP, nullptr); + return false; + } else if (strcmp(Power_Device3SP.name, name) == 0) { + IUUpdateSwitch(&Power_Device3SP, states, names, n); + for (int i = 0; i < n; i++) { + if (strcmp(names[i], "POWER_DEVICE3_ON") == 0) { + char set_power_dev_3_on_cmd[CMD_MAX_LEN]; + sprintf(set_power_dev_3_on_cmd, "%s%d,ON%s", OCS_set_relay_part, power_device_relays[POWER_DEVICE3], OCS_command_terminator); + IDSetSwitch(&Power_Device3SP, nullptr); + return sendOCSCommand(set_power_dev_3_on_cmd); + } else if (strcmp(names[i], "POWER_DEVICE3_OFF") == 0) { + char set_power_dev_3_off_cmd[CMD_MAX_LEN]; + sprintf(set_power_dev_3_off_cmd, "%s%d,OFF%s", OCS_set_relay_part, power_device_relays[POWER_DEVICE3], OCS_command_terminator); + IDSetSwitch(&Power_Device3SP, nullptr); + return sendOCSCommand(set_power_dev_3_off_cmd); + } + } + IDSetSwitch(&Power_Device3SP, nullptr); + return false; + } else if (strcmp(Power_Device4SP.name, name) == 0) { + IUUpdateSwitch(&Power_Device4SP, states, names, n); + for (int i = 0; i < n; i++) { + if (strcmp(names[i], "POWER_DEVICE4_ON") == 0) { + char set_power_dev_4_on_cmd[CMD_MAX_LEN]; + sprintf(set_power_dev_4_on_cmd, "%s%d,ON%s", OCS_set_relay_part, power_device_relays[POWER_DEVICE4], OCS_command_terminator); + IDSetSwitch(&Power_Device4SP, nullptr); + return sendOCSCommand(set_power_dev_4_on_cmd); + } else if (strcmp(names[i], "POWER_DEVICE4_OFF") == 0) { + char set_power_dev_4_off_cmd[CMD_MAX_LEN]; + sprintf(set_power_dev_4_off_cmd, "%s%d,OFF%s", OCS_set_relay_part, power_device_relays[POWER_DEVICE4], OCS_command_terminator); + IDSetSwitch(&Power_Device4SP, nullptr); + return sendOCSCommand(set_power_dev_4_off_cmd); + } + } + IDSetSwitch(&Power_Device4SP, nullptr); + return false; + } else if (strcmp(Power_Device5SP.name, name) == 0) { + IUUpdateSwitch(&Power_Device5SP, states, names, n); + for (int i = 0; i < n; i++) { + if (strcmp(names[i], "POWER_DEVICE5_ON") == 0) { + char set_power_dev_5_on_cmd[CMD_MAX_LEN]; + sprintf(set_power_dev_5_on_cmd, "%s%d,ON%s", OCS_set_relay_part, power_device_relays[POWER_DEVICE5], OCS_command_terminator); + IDSetSwitch(&Power_Device5SP, nullptr); + return sendOCSCommand(set_power_dev_5_on_cmd); + } else if (strcmp(names[i], "POWER_DEVICE5_OFF") == 0) { + char set_power_dev_5_off_cmd[CMD_MAX_LEN]; + sprintf(set_power_dev_5_off_cmd, "%s%d,OFF%s", OCS_set_relay_part, power_device_relays[POWER_DEVICE5], OCS_command_terminator); + IDSetSwitch(&Power_Device5SP, nullptr); + return sendOCSCommand(set_power_dev_5_off_cmd); + } + } + IDSetSwitch(&Power_Device5SP, nullptr); + return false; + } else if (strcmp(Power_Device6SP.name, name) == 0) { + IUUpdateSwitch(&Power_Device6SP, states, names, n); + for (int i = 0; i < n; i++) { + if (strcmp(names[i], "POWER_DEVICE1_ON") == 0) { + char set_power_dev_6_on_cmd[CMD_MAX_LEN]; + sprintf(set_power_dev_6_on_cmd, "%s%d,ON%s", OCS_set_relay_part, power_device_relays[POWER_DEVICE6], OCS_command_terminator); + IDSetSwitch(&Power_Device6SP, nullptr); + return sendOCSCommand(set_power_dev_6_on_cmd); + } else if (strcmp(names[i], "POWER_DEVICE6_OFF") == 0) { + char set_power_dev_6_off_cmd[CMD_MAX_LEN]; + sprintf(set_power_dev_6_off_cmd, "%s%d,OFF%s", OCS_set_relay_part, power_device_relays[POWER_DEVICE6], OCS_command_terminator); + IDSetSwitch(&Power_Device6SP, nullptr); + return sendOCSCommand(set_power_dev_6_off_cmd); + } + } + IDSetSwitch(&Power_Device6SP, nullptr); + return false; + + // Lights + //------- + } else if (strcmp(LIGHT_WRWSP.name, name) == 0) { + IUUpdateSwitch(&LIGHT_WRWSP, states, names, n); + for (int i = 0; i < n; i++) { + if (strcmp(names[i], "WRW_ON") == 0) { + char set_light_wrw_on_cmd[CMD_MAX_LEN]; + sprintf(set_light_wrw_on_cmd, "%s%d,ON%s", OCS_set_relay_part, light_relays[LIGHT_WRW_RELAY], OCS_command_terminator); + IDSetSwitch(&LIGHT_WRWSP, nullptr); + return sendOCSCommand(set_light_wrw_on_cmd); + } else if (strcmp(names[i], "WRW_OFF") == 0) { + char set_light_wrw_off_cmd[CMD_MAX_LEN]; + sprintf(set_light_wrw_off_cmd, "%s%d,OFF%s", OCS_set_relay_part, light_relays[LIGHT_WRW_RELAY], OCS_command_terminator); + IDSetSwitch(&LIGHT_WRWSP, nullptr); + return sendOCSCommand(set_light_wrw_off_cmd); + } + } + IDSetSwitch(&LIGHT_WRWSP, nullptr); + return false; + } else if (strcmp(LIGHT_WRRSP.name, name) == 0) { + IUUpdateSwitch(&LIGHT_WRRSP, states, names, n); + for (int i = 0; i < n; i++) { + if (strcmp(names[i], "WRR_ON") == 0) { + char set_light_wrr_on_cmd[CMD_MAX_LEN]; + sprintf(set_light_wrr_on_cmd, "%s%d,ON%s", OCS_set_relay_part, light_relays[LIGHT_WRR_RELAY], OCS_command_terminator); + IDSetSwitch(&LIGHT_WRRSP, nullptr); + return sendOCSCommand(set_light_wrr_on_cmd); + } else if (strcmp(names[i], "WRR_OFF") == 0) { + char set_light_wrr_off_cmd[CMD_MAX_LEN]; + sprintf(set_light_wrr_off_cmd, "%s%d,OFF%s", OCS_set_relay_part, light_relays[LIGHT_WRR_RELAY], OCS_command_terminator); + IDSetSwitch(&LIGHT_WRRSP, nullptr); + return sendOCSCommand(set_light_wrr_off_cmd); + } + } + IDSetSwitch(&LIGHT_WRRSP, nullptr); + return false; + } else if (strcmp(LIGHT_ORWSP.name, name) == 0) { + IUUpdateSwitch(&LIGHT_ORWSP, states, names, n); + for (int i = 0; i < n; i++) { + if (strcmp(names[i], "ORW_ON") == 0) { + char set_light_orw_on_cmd[CMD_MAX_LEN]; + sprintf(set_light_orw_on_cmd, "%s%d,ON%s", OCS_set_relay_part, light_relays[LIGHT_ORW_RELAY], OCS_command_terminator); + IDSetSwitch(&LIGHT_ORWSP, nullptr); + return sendOCSCommand(set_light_orw_on_cmd); + } else if (strcmp(names[i], "ORW_OFF") == 0) { + char set_light_orw_off_cmd[CMD_MAX_LEN]; + sprintf(set_light_orw_off_cmd, "%s%d,OFF%s", OCS_set_relay_part, light_relays[LIGHT_ORW_RELAY], OCS_command_terminator); + IDSetSwitch(&LIGHT_ORWSP, nullptr); + return sendOCSCommand(set_light_orw_off_cmd); + } + } + IDSetSwitch(&LIGHT_ORWSP, nullptr); + return false; + } else if (strcmp(LIGHT_ORRSP.name, name) == 0) { + IUUpdateSwitch(&LIGHT_ORRSP, states, names, n); + for (int i = 0; i < n; i++) { + if (strcmp(names[i], "ORR_ON") == 0) { + char set_light_orr_on_cmd[CMD_MAX_LEN]; + sprintf(set_light_orr_on_cmd, "%s%d,ON%s", OCS_set_relay_part, light_relays[LIGHT_ORR_RELAY], OCS_command_terminator); + IDSetSwitch(&LIGHT_ORRSP, nullptr); + return sendOCSCommand(set_light_orr_on_cmd); + } else if (strcmp(names[i], "ORR_OFF") == 0) { + char set_light_orr_off_cmd[CMD_MAX_LEN]; + sprintf(set_light_orr_off_cmd, "%s%d,OFF%s", OCS_set_relay_part, light_relays[LIGHT_ORR_RELAY], OCS_command_terminator); + IDSetSwitch(&LIGHT_ORRSP, nullptr); + return sendOCSCommand(set_light_orr_off_cmd); + } + } + IDSetSwitch(&LIGHT_ORRSP, nullptr); + return false; + } else if (strcmp(LIGHT_OUTSIDESP.name, name) == 0) { + IUUpdateSwitch(&LIGHT_OUTSIDESP, states, names, n); + for (int i = 0; i < n; i++) { + if (strcmp(names[i], "OUTSIDE_ON") == 0) { + char set_light_outside_on_cmd[CMD_MAX_LEN]; + sprintf(set_light_outside_on_cmd, "%s%d,ON%s", OCS_set_relay_part, light_relays[LIGHT_OUTSIDE_RELAY], OCS_command_terminator); + IDSetSwitch(&LIGHT_OUTSIDESP, nullptr); + return sendOCSCommand(set_light_outside_on_cmd); + } else if (strcmp(names[i], "OUTSIDE_OFF") == 0) { + char set_light_outside_off_cmd[CMD_MAX_LEN]; + sprintf(set_light_outside_off_cmd, "%s%d,OFF%s", OCS_set_relay_part, light_relays[LIGHT_OUTSIDE_RELAY], OCS_command_terminator); + IDSetSwitch(&LIGHT_OUTSIDESP, nullptr); + return sendOCSCommand(set_light_outside_off_cmd); + } + } + IDSetSwitch(&LIGHT_OUTSIDESP, nullptr); + return false; + + // Safety Override + //---------------- + } else if (strcmp(Safety_Interlock_OverrideSP.name, name) == 0) { + IUUpdateSwitch(&Safety_Interlock_OverrideSP, states, names, n); + IUResetSwitch(&Safety_Interlock_OverrideSP); + return sendOCSCommand(OCS_roof_safety_override); + + // Roof max power + //--------------- + } else if (strcmp(Roof_High_PowerSP.name, name) == 0) { + IUUpdateSwitch(&Roof_High_PowerSP, states, names, n); + IUResetSwitch(&Roof_High_PowerSP); + return sendOCSCommand(OCS_roof_high_power_mode); + + // Reset Watchdog + //--------------- + } else if (strcmp(Watchdog_ResetSP.name, name) == 0) { + char watchdog_response[RB_MAX_LEN] = {0}; + int watchdog_fail_or_error = getCommandSingleCharErrorOrLongResponse(PortFD, watchdog_response, OCS_set_watchdog_flag); + (void) watchdog_fail_or_error; + if (strcmp(watchdog_response, "Rebooting in a few seconds...") == 0) { + LOG_WARN("Rebooting the OCS controller in a few seconds..."); + IDSetSwitch(&Safety_Interlock_OverrideSP, nullptr); + return true; + } else if (strcmp(watchdog_response, "23") == 0) { + LOG_WARN("Unable to reboot, roof/shutter/dome in motion"); + IDSetSwitch(&Safety_Interlock_OverrideSP, nullptr); + return false; + } else if (strcmp(watchdog_response, "0") == 0) { + LOGF_DEBUG("OCS watchdog reset error, reponse was: %s. Maybe watchdog is not enabled?", watchdog_response); + IDSetSwitch(&Safety_Interlock_OverrideSP, nullptr); + return false; + } + + // Additional dome controls + //------------------------- + } else if (strcmp(DomeControlsSP.name, name) == 0) { + for (int i = 0; i < n; i++) { + if (strcmp(names[i], "SET_PARK_SW") == 0) { + return SetCurrentPark(); + } else if (strcmp(names[i], "RETURN_HOME_SW") == 0) { + return ReturnHome(); + } else if (strcmp(names[i], "RESET_HOME_SW") == 0) { + return ResetHome(); + } + } + } + return INDI::Dome::ISNewSwitch(dev, name, states, names, n); + } else { + return false; + } +} + +/************************************* + * Client has changed a number, update + *************************************/ +bool OCS::ISNewNumber(const char *dev,const char *name,double values[],char *names[],int n) +{ + if (dev != nullptr && strcmp(dev, getDeviceName()) == 0) { + + LOGF_DEBUG("Got an IsNewNumber for: %s", name); + + if (!strcmp(Thermostat_setpointsNP.name, name)) { + if (THERMOSTAT_SETPOINT_COUNT == n) { + for (int parameter = THERMOSTAT_HEAT_SETPOINT; parameter < THERMOSTAT_SETPOINT_COUNT; parameter++) { + if (parameter == THERMOSTAT_HEAT_SETPOINT) { + char thermostat_setpoint_command[CMD_MAX_LEN]; + sprintf(thermostat_setpoint_command, "%s%.0f%s", + OCS_set_thermostat_heat_setpoint_part, values[THERMOSTAT_HEAT_SETPOINT], OCS_command_terminator); + char response[RB_MAX_LEN]; + int res = getCommandSingleCharResponse(PortFD, response, thermostat_setpoint_command); + if(res < 0 || response[0] == '0') { + LOGF_ERROR("Failed to set Thermostat heat setpoint %s", response); + return false; + } else { + LOGF_INFO("Set Thermostat heat setpoint to: %.0f °C", values[THERMOSTAT_HEAT_SETPOINT]); + } + } + else if (parameter == THERMOSTAT_COOL_SETPOINT) { + char thermostat_setpoint_command[CMD_MAX_LEN]; + sprintf(thermostat_setpoint_command, "%s%.0f%s", + OCS_set_thermostat_cool_setpoint_part, values[THERMOSTAT_COOL_SETPOINT], OCS_command_terminator); + char response[RB_MAX_LEN]; + int res = getCommandSingleCharResponse(PortFD, response, thermostat_setpoint_command); + if(res < 0 || response[0] == '0') { + LOGF_ERROR("Failed to set Thermostat cool setpoint %s", response); + return false; + } else { + LOGF_INFO("Set Thermostat cool setpoint to: %.0f °C", values[THERMOSTAT_COOL_SETPOINT]); + } + } else if (parameter == THERMOSTAT_HUMIDITY_SETPOINT) { + char thermostat_setpoint_command[CMD_MAX_LEN]; + sprintf(thermostat_setpoint_command, "%s%.0f%s", + OCS_set_thermostat_humidity_setpoint_part, values[THERMOSTAT_HUMIDITY_SETPOINT], OCS_command_terminator); + char response[RB_MAX_LEN]; + int res = getCommandSingleCharResponse(PortFD, response, thermostat_setpoint_command); + if(res < 0 || response[0] == '0') { + LOGF_ERROR("Failed to set Thermostat humidity setpoint %s", response); + return false; + } else { + LOGF_INFO("Set Thermostat humidity setpoint to: %.0f %%", values[THERMOSTAT_HUMIDITY_SETPOINT]); + } + } + } + IUUpdateNumber(&Thermostat_setpointsNP, values, names, n); + Thermostat_setpointsNP.s = IPS_OK; + IDSetNumber(&Thermostat_setpointsNP, nullptr); + return true; + } + } + } + + if (strstr(name, "WEATHER_")) { + return WI::processNumber(dev, name, values, names, n); + } + + if (INDI::Dome::ISNewNumber(dev, name, values, names, n)) { + return true; + } + + return INDI::DefaultDevice::ISNewNumber(dev, name, values, names, n); +} + +/***************************************** + * Client has changed a text field, update + *****************************************/ +bool OCS::ISNewText(const char *dev,const char *name,char *texts[],char *names[],int n) +{ + if (dev != nullptr && strcmp(dev, getDeviceName()) == 0) { + + // Debug only + // Manual tab - Arbitary command + // if (!strcmp(Arbitary_CommandTP.name, name)) { + // if (1 == n) { + // char command_response[RB_MAX_LEN] = {0}; + // int command_error_or_fail = getCommandSingleCharErrorOrLongResponse(PortFD, command_response, texts[0]); + // if (command_error_or_fail > 0) { + // if (strcmp(command_response, "") == 0) { + // indi_strlcpy(command_response, "No response", sizeof(command_response)); + // } + // } else { + // char error_code[RB_MAX_LEN] = {0}; + // if (command_error_or_fail == TTY_TIME_OUT) { + // indi_strlcpy(command_response, "No response", sizeof(command_response)); + // } else { + // sprintf(error_code, "Error: %d", command_error_or_fail); + // indi_strlcpy(command_response, error_code, sizeof(command_response)); + // } + // } + // + // // Replace the user entered string with the OCS response + // indi_strlcpy(texts[0], command_response, RB_MAX_LEN); + // IUUpdateText(&Arbitary_CommandTP, texts, names, n); + // IDSetText(&Arbitary_CommandTP, nullptr); + // return true; + // } + // } + } + + return INDI::Dome::ISNewText(dev,name,texts,names,n); +} + +/*********************************************************** + * Client wants to know which devices to snoop, pass through + * *********************************************************/ +bool OCS::ISSnoopDevice(XMLEle *root) +{ + return INDI::Dome::ISSnoopDevice(root); +} + +/******************************************************** + * OCS command functions, mostly copied from lx200_OnStep + *******************************************************/ + +/********************************************************************* + * Send command to OCS without checking (intended non-existent) return + * *******************************************************************/ +bool OCS::sendOCSCommandBlind(const char *cmd) +{ + int error_type; + int nbytes_write = 0; + + DEBUGF(INDI::Logger::DBG_DEBUG, "CMD <%s>", cmd); + + flushIO(PortFD); + /* Add mutex */ + std::unique_lock guard(ocsCommsLock); + tcflush(PortFD, TCIFLUSH); + + if ((error_type = tty_write_string(PortFD, cmd, &nbytes_write)) != TTY_OK) { + LOGF_ERROR("CHECK CONNECTION: Error sending command %s", cmd); + return 0; //Fail if we can't write + //return error_type; + } + + return 1; +} + +/********************************************************************* + * Send command to OCS that expects a 0 (sucess) or 1 (failure) return + * *******************************************************************/ +bool OCS::sendOCSCommand(const char *cmd) +{ + char response[1] = {0}; + int error_type; + int nbytes_write = 0, nbytes_read = 0; + + DEBUGF(INDI::Logger::DBG_DEBUG, "CMD <%s>", cmd); + + flushIO(PortFD); + /* Add mutex */ + std::unique_lock guard(ocsCommsLock); + tcflush(PortFD, TCIFLUSH); + + if ((error_type = tty_write_string(PortFD, cmd, &nbytes_write)) != TTY_OK) + return error_type; + + error_type = tty_read_expanded(PortFD, response, 1, OCSTimeoutSeconds, OCSTimeoutMicroSeconds, &nbytes_read); + + tcflush(PortFD, TCIFLUSH); + DEBUGF(INDI::Logger::DBG_DEBUG, "RES <%c>", response[0]); + + if (nbytes_read < 1) { + LOG_WARN("Timeout/Error on response. Check connection."); + return false; + } + + return (response[0] == '0'); //OCS uses 0 for success and non zero for failure, in *most* cases; +} + +/************************************************************ + * Send command to OCS that expects a single character return + * **********************************************************/ +int OCS::getCommandSingleCharResponse(int fd, char *data, const char *cmd) +{ + char *term; + int error_type; + int nbytes_write = 0, nbytes_read = 0; + + DEBUGF(INDI::Logger::DBG_DEBUG, "CMD <%s>", cmd); + + flushIO(fd); + /* Add mutex */ + std::unique_lock guard(ocsCommsLock); + + if ((error_type = tty_write_string(fd, cmd, &nbytes_write)) != TTY_OK) + return error_type; + + error_type = tty_read_expanded(fd, data, 1, OCSTimeoutSeconds, OCSTimeoutMicroSeconds, &nbytes_read); + tcflush(fd, TCIFLUSH); + + if (error_type != TTY_OK) + return error_type; + + term = strchr(data, '#'); + if (term) + *term = '\0'; + if (nbytes_read < RB_MAX_LEN) { //given this function that should always be true, as should nbytes_read always be 1 + data[nbytes_read] = '\0'; + } else { + LOG_DEBUG("got RB_MAX_LEN bytes back (which should never happen), last byte set to null and possible overflow"); + data[RB_MAX_LEN - 1] = '\0'; + } + + DEBUGF(INDI::Logger::DBG_DEBUG, "RES <%s>", data); + + return nbytes_read; +} + +/************************************************** + * Send command to OCS that expects a double return + * ************************************************/ +int OCS::getCommandDoubleResponse(int fd, double *value, char *data, const char *cmd) +{ + char *term; + int error_type; + int nbytes_write = 0, nbytes_read = 0; + + DEBUGF(INDI::Logger::DBG_DEBUG, "CMD <%s>", cmd); + + flushIO(fd); + /* Add mutex */ + std::unique_lock guard(ocsCommsLock); + tcflush(fd, TCIFLUSH); + + if ((error_type = tty_write_string(fd, cmd, &nbytes_write)) != TTY_OK) + return error_type; + + error_type = tty_read_section_expanded(fd, data, '#', OCSTimeoutSeconds, OCSTimeoutMicroSeconds, &nbytes_read); + tcflush(fd, TCIFLUSH); + + term = strchr(data, '#'); + if (term) + *term = '\0'; + if (nbytes_read < RB_MAX_LEN) { //If within buffer, terminate string with \0 (in case it didn't find the #) + data[nbytes_read] = '\0'; //Indexed at 0, so this is the byte passed it + } else { + LOG_DEBUG("got RB_MAX_LEN bytes back, last byte set to null and possible overflow"); + data[RB_MAX_LEN - 1] = '\0'; + } + + DEBUGF(INDI::Logger::DBG_DEBUG, "RES <%s>", data); + + if (error_type != TTY_OK) { + LOGF_DEBUG("Error %d", error_type); + LOG_DEBUG("Flushing connection"); + tcflush(fd, TCIOFLUSH); + return error_type; + } + + if (sscanf(data, "%lf", value) != 1) { + LOG_WARN("Invalid response, check connection"); + LOG_DEBUG("Flushing connection"); + tcflush(fd, TCIOFLUSH); + return RES_ERR_FORMAT; //-1001, so as not to conflict with TTY_RESPONSE; + } + + return nbytes_read; +} + +/************************************************ + * Send command to OCS that expects an int return + * **********************************************/ +int OCS::getCommandIntResponse(int fd, int *value, char *data, const char *cmd) +{ + char *term; + int error_type; + int nbytes_write = 0, nbytes_read = 0; + + DEBUGF(INDI::Logger::DBG_DEBUG, "CMD <%s>", cmd); + + flushIO(fd); + /* Add mutex */ + std::unique_lock guard(ocsCommsLock); + tcflush(fd, TCIFLUSH); + + if ((error_type = tty_write_string(fd, cmd, &nbytes_write)) != TTY_OK) + return error_type; + + error_type = tty_read_expanded(fd, data, sizeof(char), OCSTimeoutSeconds, OCSTimeoutMicroSeconds, &nbytes_read); + tcflush(fd, TCIFLUSH); + + term = strchr(data, '#'); + if (term) + *term = '\0'; + if (nbytes_read < RB_MAX_LEN) { //If within buffer, terminate string with \0 (in case it didn't find the #) + data[nbytes_read] = '\0'; //Indexed at 0, so this is the byte passed it + } else { + LOG_DEBUG("got RB_MAX_LEN bytes back, last byte set to null and possible overflow"); + data[RB_MAX_LEN - 1] = '\0'; + } + + DEBUGF(INDI::Logger::DBG_DEBUG, "RES <%s>", data); + + if (error_type != TTY_OK) { + LOGF_DEBUG("Error %d", error_type); + LOG_DEBUG("Flushing connection"); + tcflush(fd, TCIOFLUSH); + return error_type; + } + if (sscanf(data, "%i", value) != 1) { + LOG_WARN("Invalid response, check connection"); + LOG_DEBUG("Flushing connection"); + tcflush(fd, TCIOFLUSH); + return RES_ERR_FORMAT; //-1001, so as not to conflict with TTY_RESPONSE; + } + + return nbytes_read; +} + +/*************************************************************************** + * Send command to OCS that expects a char[] return (could be a single char) + * *************************************************************************/ +int OCS::getCommandSingleCharErrorOrLongResponse(int fd, char *data, const char *cmd) +{ + char *term; + int error_type; + int nbytes_write = 0, nbytes_read = 0; + + DEBUGF(INDI::Logger::DBG_DEBUG, "CMD <%s>", cmd); + + flushIO(fd); + /* Add mutex */ + std::unique_lock guard(ocsCommsLock); + tcflush(fd, TCIFLUSH); + + if ((error_type = tty_write_string(fd, cmd, &nbytes_write)) != TTY_OK) + return error_type; + + error_type = tty_read_section_expanded(fd, data, '#', OCSTimeoutSeconds, OCSTimeoutMicroSeconds, &nbytes_read); + tcflush(fd, TCIFLUSH); + + term = strchr(data, '#'); + if (term) + *term = '\0'; + if (nbytes_read < RB_MAX_LEN) { //If within buffer, terminate string with \0 (in case it didn't find the #) + data[nbytes_read] = '\0'; //Indexed at 0, so this is the byte passed it + } else { + LOG_DEBUG("got RB_MAX_LEN bytes back, last byte set to null and possible overflow"); + data[RB_MAX_LEN - 1] = '\0'; + } + + DEBUGF(INDI::Logger::DBG_DEBUG, "RES <%s>", data); + + if (error_type != TTY_OK) { + LOGF_DEBUG("Error %d", error_type); + return error_type; + } + + return nbytes_read; +} + +/******************************************************** + * Converts an OCS char[] return of a numeric into an int + * ******************************************************/ +int OCS::getCommandIntFromCharResponse(int fd, char *data, int *response, const char *cmd) +{ + int errorOrFail = getCommandSingleCharErrorOrLongResponse(fd, data, cmd); + if (errorOrFail < 1) { + return errorOrFail; + } else { + int value = conversion_error; + try { + value = std::stoi(data); + } catch (const std::invalid_argument&) { + LOGF_WARN("Invalid response to %s: %s", cmd, data); + } catch (const std::out_of_range&) { + LOGF_WARN("Invalid response to %s: %s", cmd, data); + } + *response = value; + return errorOrFail; + } +} + + +/********************** + * Flush the comms port + * ********************/ +int OCS::flushIO(int fd) +{ + tcflush(fd, TCIOFLUSH); + int error_type = 0; + int nbytes_read; + std::unique_lock guard(ocsCommsLock); + tcflush(fd, TCIOFLUSH); + do { + char discard_data[RB_MAX_LEN] = {0}; + error_type = tty_read_section_expanded(fd, discard_data, '#', 0, 1000, &nbytes_read); + if (error_type >= 0) { + LOGF_DEBUG("flushIO: Information in buffer: Bytes: %u, string: %s", nbytes_read, discard_data); + } + //LOGF_DEBUG("flushIO: error_type = %i", error_type); + } + while (error_type > 0); + + return 0; +} + +int OCS::charToInt (char *inString) +{ + int value = conversion_error; + try { + value = std::stoi(inString); + } catch (const std::invalid_argument&) { + } catch (const std::out_of_range&) { + } + return value; +} diff --git a/indi-ocs/ocs.h b/indi-ocs/ocs.h new file mode 100644 index 000000000..3fbde5656 --- /dev/null +++ b/indi-ocs/ocs.h @@ -0,0 +1,698 @@ +/******************************************************************************* + 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" +#include "indiweather.h" +#include "connectionplugins/connectiontcp.h" +#include "connectionplugins/connectionserial.h" +#include "indipropertyswitch.h" +#include "inditimer.h" + +#define RB_MAX_LEN 64 +#define CMD_MAX_LEN 32 +enum ResponseErrors {RES_ERR_FORMAT = -1001}; + +/********************************************************************** +OCS lexicon +Extracted from OCS 3.03i +Note all commands sent and responses returned terminate with a # symbol +These are stripped from returned char* by their retrieving functions +An unterminated 0 is returned from unconfigured items +**********************************************************************/ + +// General commands +//----------------- + +// Get Product (compatibility) +#define OCS_handshake ":IP#" +// Returns: OCS# + +// Get firmware version number +#define OCS_get_firmware ":IN#" +// Returns: firmware_string# for example 3.03i# + +// Get timeouts +#define OCS_get_timeouts ":IT#" +// Returns: n.n,m.m# +// where n.n is ROOF_TIME_PRE_MOTION and m.m ROOF_TIME_POST_MOTION + +// Get safety status +// Info only, Indi generates it's own saety status +#define OCS_get_safety_status ":Gs#" +// Returns: SAFE#, UNSAFE# + +// Set the watchdog reset flag - forces OCS software reboot +#define OCS_set_watchdog_flag ":SW#" +// Returns: Rebooting in a few seconds...# or 23# "CE_SLEW_IN_MOTION" for roof/shutter/dome in motion blocking error + +// Set the UTC Date and Time +// ":SU[MM/DD/YYYY,HH:MM:SS]#" +// Example: SU03/31/2023,13:22:00# +// Returns: 0# on failure, 1# on success + +// Get the power status +#define OCS_get_power_status ":GP#" +// Returns: OK#, OUT#, or N/A# + +// Get the internal MCU temperature in deg. C +#define OCS_get_MCU_temperature ":GX9F#" +// Returns: +/-n.n# if supported, 0 if unsupported + +// Set USB Baud Rate where n is an ASCII digit (1..9) with the following interpertation +// 0=115.2K, 1=56.7K, 2=38.4K, 3=28.8K, 4=19.2K, 5=14.4K, 6=9600, 7=4800, 8=2400, 9=1200 +// ":SB[n]#" +// Returns: 1# (at the current baud rate and then changes to the new rate for further communication) + +// Roof/shutter commands +//---------------------- +// note: Roll off roof style observatory or shutter control for dome style observatory + +// Command the roof/shutter to close +#define OCS_roof_close ":RC#" +// Returns: nothing + +// Command the roof/shutter to open +#define OCS_roof_open ":RO#" +// Returns: nothing + +// Command the roof/shutter movement to stop +#define OCS_roof_stop ":RH#" +// Returns: nothing + +// Set the roof/shutter safety override - ignore stuck limit switches and timeout +#define OCS_roof_safety_override ":R!#" +// Returns: 1# on success + +// Set the roof/shutter high power mode - forces motor pwm to 100% +#define OCS_roof_high_power_mode ":R+#" +// Returns: 1# on success + +// Get the roof/shutter status +#define OCS_get_roof_status ":RS#" +// Returns: +// OPEN#, CLOSED#, c,Travel: n%# (for closing), o,Travel: n%# for opening, +// i,No Error# for idle or i,Waiting for mount to park# + +// Get the roof/shutter last status error +#define OCS_get_roof_last_error ":RSL#" +// Returns: +// Error: Open safety interlock# +// Error: Close safety interlock# +// Error: Open unknown error# +// Error: Open limit sw fail# +// Error: Open over time# +// Error: Open under time# +// Error: Close unknown error# +// Error: Close limit sw fail# +// Error: Close over time# +// Error: Close under time# +// Error: Limit switch malfunction# +// Error: Closed/opened limit sw on# +// Warning: Already closed# +// Error: Close location unknown# +// Error: Motion direction unknown# +// Error: Close already in motion# +// Error: Opened/closed limit sw on# +// Warning: Already open# +// Error: Open location unknown# +// Error: Open already in motion# +// Error: Close mount not parked# +// or nothing if never errored + +//Dome commands +//------------- + +// Command the dome to goto the home position +#define OCS_dome_home ":DC#" +// Returns: nothing + +// Reset that the dome is at home +#define OCS_reset_dome_home ":DF#" +// Returns: nothing + +// Command the dome to goto the park position +#define OCS_dome_park ":DP#" +// Returns: 0# on failure, 1# on success + +// Set the dome park position +#define OCS_set_dome_park ":DQ#" +// Returns: 0# on failure, 1# on success + +// Restore the dome park position +#define OCS_restore_dome_park ":DR#" +// Returns: 0# on failure, 1# on success + +// Command the dome movement to stop +#define OCS_dome_stop ":DH#" +// Returns: nothing + +// Get the dome Azimuth (0 to 360 degrees) +#define OCS_get_dome_azimuth ":DZ#" +// Returns: D.DDD# + +// Set the dome Azimuth target (0 to 360 degrees) +#define OCS_set_dome_azimuth_part ":Dz" +// Example: ":Dz[D.D]#" +// Returns: nothing + +// Unused, not supported by Indi +// Get the dome Altitude (0 to 90 degrees) +// #define OCS_get_dome_altitude ":DA#" +// Returns: D.D#, NAN# is no second axis + +// Unused, not supported by Indi +// Set the dome Altitude target (0 to 90 degrees) +// ":Da[D.D]#" +// Returns: nothing + +// Set the dome to sync with target (Azimuth only) +#define OCS_dome_sync_target ":DN#" +// Returns: See :DS# command below + +// Command the dome to goto target +#define OCS_dome_goto_taget ":DS#" +// Returns: +enum { + GOTO_IS_POSSIBLE, + BELOW_HORIZON_LIMIT, + ABOVE_OVERHEAD_LIMIT, + CONTROLLER_IN_STANDBY, + DOME_IS_PARKED, + GOTO_IN_PROGRESS, + OUTSIDE_LIMITS, + HARDWARE_FAULT, + ALREADY_IN_MOTION, + UNSPECIFIED_ERROR, + COUNT_DOME_GOTO_RETURNS +}; + +// 0# = Goto is possible +// 1# = below the horizon limit +// 2# = above overhead limit +// 3# = controller in standby +// 4# = dome is parked +// 5# = Goto in progress +// 6# = outside limits (AXIS2_LIMIT_MAX, AXIS2_LIMIT_MIN, AXIS1_LIMIT_MIN/MAX, MERIDIAN_E/W) +// 7# = hardware fault +// 8# = already in motion +// 9# = unspecified error + +// Get dome status +#define OCS_get_dome_status ":DU#" +// Returns: P# if parked, K#, if parking, H# if at Home, I# if idle + +// Axis commands + +// Axis1 is Dome Azimuth - required if dome = true +// Axis2 is Dome Altitude - optional + +// Curently unused +// Get the axis/driver configuration for axis [n] +// #define OCS_get_axis_configuration_part ":GXA" +// ":GXA[n]#" +// Returns: s,s,s,s# +// where s,s,s,s... comprises: +// parameter [0] = steps per degree, +// parameter [1] = reverse axis +// parameter [2] = minimum limit +// parameter [3] = maximum limit + +// Currently unused +// Get the stepper driver status for axis [n] +// #define OCS_get_driver_status_part ":GXU" +// ":GXU[n]#" +// Returns: +// ST# = At standstill +// OA# = Output A open load +// OB# = Output B open load +// GA# = Output A short to ground +// GB# = Output B short to ground +// OT# = Over temperature (>150 deg. C) +// PW# = Over temperature warning (>120 deg. C) +// GF# = Fault + +// Not used, superset definition? +// Set the axis/driver configuration for axis [n] +// ":SXA[n]#" + +// Revert axis/driver configuration for axis [n] to defaults +// ":SXA[n],R#" + +// Currently unused +// Set the axis/driver configuration for axis [n] +// #define OCS_set_axis_configuration_part ":SXA" +// :SXA[n],[s,s,s,s...]# +// where s,s,s,s... comprises: +// parameter [0] = steps per degree, +// parameter [1] = reverse axis +// parameter [2] = minimum limit +// parameter [3] = maximum limit + +// Weather commands +//----------------- + +// Get the outside temperature in deg. C +#define OCS_get_outside_temperature ":G1#" +// Returns: nnn.n# + +// Get the sky IR temperature in deg. C +#define OCS_get_sky_IR_temperature ":G2#" +// Returns: nnn.n# + +// Get the sky differential temperature +#define OCS_get_sky_diff_temperature ":G3#" +// Returns: nnn.n# +// where <= 21 is cloudy + +// TBC? +// Get averaged sky differential temperature +#define OCS_get_av_sky_diff_temperature ":GS#" +// Returns: nnn.n# +// where <= 21 is cloudy + +// Get the absolute barometric pressure as Float (mbar, sea-level compensated) +#define OCS_get_pressure ":Gb#" +// Returns: n.nnn# +// where n ranges from about 980.0 to 1050.0 + +// Get cloud description +#define OCS_get_cloud_description ":GC#" +// Returns: description_string# + +// Get relative humidity reading as Float (% Rh) +#define OCS_get_humidity ":Gh#" +// Returns: n.n# +// where n ranges from 0.0 to 100.0 + +// Get sky quality in mag/arc-sec^2 +#define OCS_get_sky_quality ":GQ#" +// Returns: nnn.n# + +// Get rain sensor status +#define OCS_get_rain_sensor_status ":GR#" +// Returns: -1000# for invalid, 0# for N/A, 1# for Rain, 2# for Warn, and 3# for Dry + +// Get wind status +#define OCS_get_wind_status ":GW#" +// Returns: OK#, HIGH#, or N/A# + +// Get wind speed +#define OCS_get_wind_speed ":Gw#" +// Returns: n# kph, Invalid#, or N/A# + +// Get the weather threshold #defines +#define OCS_get_weather_thresholds ":IW#" +// Returns: 20,-14#, WEATHER_WIND_SPD_THRESHOLD,WEATHER_SAFE_THRESHOLD, N/A if sensor == OFF + +// Thermostat commands +//-------------------- + +// Get Thermostat relay #defines +#define OCS_get_thermostat_definitions ":It#" +// Returns: n,n,-1#les +// HEAT_RELAY, COOL_RELAY, HUMIDITY_RELAY +// -1 indicates function not defined + +// Get thermostat status +#define OCS_get_thermostat_status ":GT#" +// Returns: n.n,m.m# +// where n.n is temperature in deg. C and m.m is % humidity + +// Get heat setpoint in deg. C +#define OCS_get_thermostat_heat_setpoint ":GH#" +// Returns: n#, or 0# for invalid + +// Set heat setpoint in deg. C +#define OCS_set_thermostat_heat_setpoint_part ":SH" +// Example: ":SH0#" turns heat off +// Example: ":SH21#" heat setpoint 21 deg. C +// Returns: 1# on success + +// Get cool/vent setpoint in deg. C +#define OCS_get_thermostat_cool_setpoint ":GV#" +// Returns: n#, or 0# for invalid + +//Set cool/vent setpoint in deg. C +#define OCS_set_thermostat_cool_setpoint_part ":SC" +// Example: ":SC0#" turns cooling off +// Example: ":SC30#" cool setpoint 30 deg. C +// Returns: 1# on success + +// Get humidity setpoint in % +#define OCS_get_thermostat_humidity_setpoint ":GD#" +// Returns: n#, or 0# for invalid + +// Set humidity setpoint in % +#define OCS_set_thermostat_humidity_setpoint_part ":SD" +// Example: ":SD0#" turns dehumidifying off +// Example: ":SD55#" humidity setpoint 55% +// Returns: 1# on success + +// Power/GPIO commands +//-------------------- + +// Get Light relay #defines +#define OCS_get_light_definitions ":IL#" +// Returns: n,n,-1,n,n# +// LIGHT_WRW_RELAY,LIGHT_WRR_RELAY,LIGHT_ORW_RELAY,LIGHT_ORR_RELAY,LIGHT_OUTSIDE_RELAY +// -1 indicates function not defined + +// Get Power device relays +#define OCS_get_power_definitions ":Ip#" +// Returns: n,n,-1,n,n,n# +// POWER_DEVICE1_RELAY...POWER_DEVICE6_RELAY + +// Get Power device names +#define OCS_get_power_names_part ":Ip" +// Example: ":Ip1#" +// Returns: name_string# + +// Get Relay n state +#define OCS_get_relay_part ":GR" +// Example: ":GR1#" +// Returns: ON#, OFF#, n# (pwm 0-9) + +// Set Relay n [state] = ON, OFF, DELAY, n (pwm 0 to 10) +#define OCS_set_relay_part ":SR" +// Example: ":SR1,ON#" +// Returns: 1# on success + +// Get Analog n state +#define OCS_get_analog_part ":GA" +// Example: ":GA1#" +// Returns: n# (0 to 1023, 0 to 5V) + +// Get Digital Sense n state +#define OCS_get_digital_part ":GS" +// Example: ":GS1#" +// Returns: ON#, OFF# + +// For dynamically assembled commands +//----------------------------------- +#define OCS_command_terminator "#" + +/******************** +OnCue OCS lexicon end +*********************/ + +class OCS : public INDI::Dome, public INDI::WeatherInterface +{ + public: + OCS(); + virtual ~OCS() override = default; + const char *getDefaultName() override; + virtual bool initProperties() 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 ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n) override; + bool updateProperties() override; + virtual bool ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) override; + virtual bool saveConfigItems(FILE *fp) override; + virtual bool ISSnoopDevice(XMLEle *root) override; + virtual bool Handshake() override; + virtual bool Abort() override; + + protected: + bool Connect() override; + bool Disconnect() override; + + void TimerHit() override; + void SlowTimerHit(); + virtual IPState updateWeather() override; + + bool sendOCSCommand(const char *cmd); + bool sendOCSCommandBlind(const char *cmd); + int flushIO(int fd); + int getCommandSingleCharResponse(int fd, char *data, const char *cmd); //Reimplemented from getCommandString + int getCommandSingleCharErrorOrLongResponse(int fd, char *data, const char *cmd); //Reimplemented from getCommandString + int getCommandDoubleResponse(int fd, double *value, char *data, + const char *cmd); //Reimplemented from getCommandString Will return a double, and raw value. + int getCommandIntResponse(int fd, int *value, char *data, const char *cmd); + int getCommandIntFromCharResponse(int fd, char *data, int *response, const char *cmd); //Calls getCommandSingleCharErrorOrLongResponse with conversion of return + int charToInt(char *inString); + + long int OCSTimeoutSeconds = 0; + long int OCSTimeoutMicroSeconds = 100000; + +private: + float minimum_OCS_fw = 3.04; + int conversion_error = -10000; + + // Capability queries on connection + void GetCapabilites(); + bool hasDome = false; + + // Timer for slow updates, once per minute + INDI::Timer SlowTimer; + + // Roof/Shutter control + //--------------------- + int ROOF_TIME_PRE_MOTION = 0; + int ROOF_TIME_POST_MOTION = 0; + char last_shutter_status[RB_MAX_LEN]; + char last_shutter_error[RB_MAX_LEN]; + IPState ControlShutter(ShutterOperation operation) override; + + // Dome control + //------------- + virtual IPState Park() override; + virtual IPState UnPark() override; + virtual bool SetCurrentPark() override; + virtual IPState MoveAbs(double az) override; + virtual bool Sync (double az) override; + bool ReturnHome(); + bool ResetHome(); + + enum { + ON_SWITCH, + OFF_SWITCH, + SWITCH_TOGGLE_COUNT + }; + + // Main control tab controls + //-------------------------- + ITextVectorProperty ShutterStatusTP; + IText ShutterStatusT[1]; + ITextVectorProperty DomeStatusTP; + IText DomeStatusT[1]; + enum { + DOME_SET_PARK, + DOME_RETURN_HOME, + DOME_SET_HOME, + DOME_CONTROL_COUNT + }; + ISwitchVectorProperty DomeControlsSP; + ISwitch DomeControlsS[DOME_CONTROL_COUNT]; + + + // Status tab controls + //-------------------- + enum { + STATUS_FIRMWARE, + STATUS_ROOF_LAST_ERROR, + STATUS_MAINS, + STATUS_OCS_SAFETY, + STATUS_MCU_TEMPERATURE, + STATUS_ITEMS_COUNT + }; + ITextVectorProperty Status_ItemsTP; + IText Status_ItemsT[STATUS_ITEMS_COUNT] {}; + + // Thermostat tab controls + //------------------------ + bool thermostat_controls_enabled = false; + + enum { + THERMOSTAT_TEMERATURE, + THERMOSTAT_HUMIDITY, + THERMOSTAT_COUNT + }; + ITextVectorProperty Thermostat_StatusTP; + IText Thermostat_StatusT[THERMOSTAT_COUNT] {}; + + enum { + THERMOSTAT_HEAT_SETPOINT, + THERMOSTAT_COOL_SETPOINT, + THERMOSTAT_HUMIDITY_SETPOINT, + THERMOSTAT_SETPOINT_COUNT + }; + INumberVectorProperty Thermostat_setpointsNP; + INumber Thermostat_setpointN[THERMOSTAT_SETPOINT_COUNT]; + + ISwitchVectorProperty Thermostat_heat_relaySP; + ISwitch Thermostat_heat_relayS[SWITCH_TOGGLE_COUNT]; + ISwitchVectorProperty Thermostat_cool_relaySP; + ISwitch Thermostat_cool_relayS[SWITCH_TOGGLE_COUNT]; + ISwitchVectorProperty Thermostat_humidity_relaySP; + ISwitch Thermostat_humidity_relayS[SWITCH_TOGGLE_COUNT]; + + enum { + THERMOSTAT_HEAT_RELAY, + THERMOSTAT_COOL_RELAY, + THERMOSTAT_HUMIDITY_RELAY, + THERMOSTAT_RELAY_COUNT + }; + int thermostat_relays[THERMOSTAT_RELAY_COUNT]; + + // Power tab controls + //------------------- + bool power_tab_enabled = false; + + enum { + POWER_DEVICE1, + POWER_DEVICE2, + POWER_DEVICE3, + POWER_DEVICE4, + POWER_DEVICE5, + POWER_DEVICE6, + POWER_DEVICE_COUNT + }; + + int power_device_relays[POWER_DEVICE_COUNT]; + char POWER_DEVICE1_NAME[RB_MAX_LEN]; + char POWER_DEVICE2_NAME[RB_MAX_LEN]; + char POWER_DEVICE3_NAME[RB_MAX_LEN]; + char POWER_DEVICE4_NAME[RB_MAX_LEN]; + char POWER_DEVICE5_NAME[RB_MAX_LEN]; + char POWER_DEVICE6_NAME[RB_MAX_LEN]; + + enum { + RELAY_1, + RELAY_2, + RELAY_3, + RELAY_4, + RELAY_5, + RELAY_6, + RELAY_7, + RELAY_8, + RELAY_9, + RELAY_10, + RELAY_11, + RELAY_12, + RELAY_13, + RELAY_14, + RELAY_15, + RELAY_16, + RELAY_17, + RELAY_18, + RELAY_COUNT + }; + ISwitchVectorProperty Power_Device1SP; + ISwitch Power_Device1S[SWITCH_TOGGLE_COUNT]; + ISwitchVectorProperty Power_Device2SP; + ISwitch Power_Device2S[SWITCH_TOGGLE_COUNT]; + ISwitchVectorProperty Power_Device3SP; + ISwitch Power_Device3S[SWITCH_TOGGLE_COUNT]; + ISwitchVectorProperty Power_Device4SP; + ISwitch Power_Device4S[SWITCH_TOGGLE_COUNT]; + ISwitchVectorProperty Power_Device5SP; + ISwitch Power_Device5S[SWITCH_TOGGLE_COUNT]; + ISwitchVectorProperty Power_Device6SP; + ISwitch Power_Device6S[SWITCH_TOGGLE_COUNT]; + ITextVectorProperty Power_Device_Name1TP; + IText Power_Device_Name1T[1] {}; + ITextVectorProperty Power_Device_Name2TP; + IText Power_Device_Name2T[1] {}; + ITextVectorProperty Power_Device_Name3TP; + IText Power_Device_Name3T[1] {}; + ITextVectorProperty Power_Device_Name4TP; + IText Power_Device_Name4T[1] {}; + ITextVectorProperty Power_Device_Name5TP; + IText Power_Device_Name5T[1] {}; + ITextVectorProperty Power_Device_Name6TP; + IText Power_Device_Name6T[1] {}; + + // Lights tab controls + //-------------------- + bool lights_tab_enabled = false; + + enum { + LIGHT_WRW_RELAY, + LIGHT_WRR_RELAY, + LIGHT_ORW_RELAY, + LIGHT_ORR_RELAY, + LIGHT_OUTSIDE_RELAY, + LIGHT_COUNT + }; + + int light_relays[LIGHT_COUNT]; + + ISwitchVectorProperty LIGHT_WRWSP; + ISwitch LIGHT_WRWS[SWITCH_TOGGLE_COUNT]; + ISwitchVectorProperty LIGHT_WRRSP; + ISwitch LIGHT_WRRS[SWITCH_TOGGLE_COUNT]; + ISwitchVectorProperty LIGHT_ORWSP; + ISwitch LIGHT_ORWS[SWITCH_TOGGLE_COUNT]; + ISwitchVectorProperty LIGHT_ORRSP; + ISwitch LIGHT_ORRS[SWITCH_TOGGLE_COUNT]; + ISwitchVectorProperty LIGHT_OUTSIDESP; + ISwitch LIGHT_OUTSIDES[SWITCH_TOGGLE_COUNT]; + + // Weather tab controls + //--------------------- + bool weather_tab_enabled = false; + int wind_speed_threshold = 0; + int diff_temp_threshold = 0; + + enum { + WEATHER_TEMPERATURE, + WEATHER_PRESSURE, + WEATHER_HUMIDITY, + WEATHER_WIND, + WEATHER_RAIN, + WEATHER_DIFF_SKY_TEMP, + WEATHER_CLOUD, + WEATHER_SKY, + WEATHER_SKY_TEMP, + WEATHER_MEASUREMENTS_COUNT + }; + + int weather_enabled[WEATHER_MEASUREMENTS_COUNT]; + + ITextVectorProperty Weather_CloudTP; + IText Weather_CloudT[1]; + ITextVectorProperty Weather_SkyTP; + IText Weather_SkyT[1]; + ITextVectorProperty Weather_Sky_TempTP; + IText Weather_Sky_TempT[1]; + + // Manual tab controls + //-------------------- + enum { + SAFETY_INTERLOCK_OVERRIDE, + ROOF_HIGH_POWER, + WATCHDOG_RESET, + MANUAL_CONTROLS_COUNT + }; + + ITextVectorProperty Manual_WarningTP; + IText Manual_WarningT[2] {}; + ISwitchVectorProperty Safety_Interlock_OverrideSP; + ISwitch Safety_Interlock_OverrideS[1]; + ISwitchVectorProperty Roof_High_PowerSP; + ISwitch Roof_High_PowerS[1]; + ISwitchVectorProperty Watchdog_ResetSP; + ISwitch Watchdog_ResetS[1]; + + // Debug only + // ITextVectorProperty Arbitary_CommandTP; + // IText Arbitary_CommandT[1]; +}; +