From 5e699cab0064423f2f607f0e2068bace29caf5f8 Mon Sep 17 00:00:00 2001 From: Peter Englmaier Date: Wed, 7 Feb 2024 15:02:40 +0100 Subject: [PATCH] Initial release of Alluna TCS2 Driver (#1991) * Initial release of Alluna TCS2 Driver The Telescope-Control-System TCS of ALLUNA Optics, has been designed for the remote control of high-quality telescopes. Alluna Optics provides only ASCOM drivers and a Windows EXE program to control the TCS2 Device. The TCS2 can also be controlled by an optional handheld device. The indi driver has been developed by me, Peter Englmaier, and I am not affiliated with Alluna Optics, but own a small Alluna Telescope and want to use it with Ekos and other indi software. With TCS several telescope functions are controlled and monitored: - Air conditioning of the optical tube and the main mirror i.e., primary/secondary mirror heating and ventilation - Temperature of ambient (focuser), primary and secondary mirror - Focusing of the optics - Control of the optionally available CCD Rotator - Control of the optionally available robotic Dust Cover - Setting configuration parameters As I do not have the rotator option, I have not (yet) implemented that part. Also some less useful functions are still missing. But it is possible, to use the focuser with ekos, control the dust cover, and the climate settings. * Use INDI::PropertyXXX instead of the old ISwitch/INumber properties * Fix bugs introduced by refactoring * Add alluna tcs2 driver to drivers.xml --- drivers.xml | 4 + drivers/focuser/CMakeLists.txt | 9 + drivers/focuser/alluna_tcs2.cpp | 1025 +++++++++++++++++++++++++++++++ drivers/focuser/alluna_tcs2.h | 168 +++++ 4 files changed, 1206 insertions(+) create mode 100644 drivers/focuser/alluna_tcs2.cpp create mode 100644 drivers/focuser/alluna_tcs2.h diff --git a/drivers.xml b/drivers.xml index fbfb9d2199..7b7c00a838 100644 --- a/drivers.xml +++ b/drivers.xml @@ -517,6 +517,10 @@ indi_activefocuser_focus 1.0 + + indi_alluna_tcs2 + 1.0 + diff --git a/drivers/focuser/CMakeLists.txt b/drivers/focuser/CMakeLists.txt index 88f094dc24..10af16cdaf 100644 --- a/drivers/focuser/CMakeLists.txt +++ b/drivers/focuser/CMakeLists.txt @@ -251,6 +251,15 @@ add_executable(indi_steeldrive_focus ${steeldrive_SRC}) target_link_libraries(indi_steeldrive_focus indidriver) install(TARGETS indi_steeldrive_focus RUNTIME DESTINATION bin) +################ Alluna TCS2 Focuser ################ + +SET(allunatcs2_SRC + alluna_tcs2.cpp) + +add_executable(indi_alluna_tcs2 ${allunatcs2_SRC}) +target_link_libraries(indi_alluna_tcs2 indidriver) +install(TARGETS indi_alluna_tcs2 RUNTIME DESTINATION bin) + # ############### FocusLynx Focuser ################ SET(focuslynx_SRC focuslynxbase.cpp diff --git a/drivers/focuser/alluna_tcs2.cpp b/drivers/focuser/alluna_tcs2.cpp new file mode 100644 index 0000000000..28d19c873a --- /dev/null +++ b/drivers/focuser/alluna_tcs2.cpp @@ -0,0 +1,1025 @@ +/* + Alluna TCS2 Focus, Dust Cover, Climate, Rotator, and Settings + (Dust Cover and Rotator are not implemented) + + Copyright(c) 2022 Peter Englmaier. All rights reserved. + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +*/ + +#include "alluna_tcs2.h" + +#include "indicom.h" + +#include +#include +#include +#include +#include + +// create an instance of this driver +static std::unique_ptr allunaTCS2(new AllunaTCS2 ()); + +AllunaTCS2::AllunaTCS2() //: DustCapInterface() +{ + LOG_DEBUG("Init AllunaTCS2"); + // Let's specify the driver version + setVersion(1, 0); + + // we know only about serial (USB) connections + setSupportedConnections(CONNECTION_SERIAL); + // Connection parameters should be 19200 @ 8-N-1 + // FIXME: add some code to warn if settings are not ok + + // What capabilities do we support? + FI::SetCapability(FOCUSER_CAN_ABORT | + FOCUSER_CAN_ABS_MOVE | + FOCUSER_CAN_REL_MOVE ); //FIXME: maybe remove CAN_REL_MOVE +} + + +bool AllunaTCS2::initProperties() +{ + INDI::Focuser::initProperties(); + //INDI::DustCapInterface::initDustCapProperties(getDeviceName(), "groupname"); + + // Focuser temperature / ambient temperature, ekos uses first number of "FOCUS_TEMPERATURE" property + TemperatureNP[0].fill("TEMPERATURE", "Focuser Temp [C]", "%6.2f", -100, 100, 0, 0); + TemperatureNP[1].fill("TEMPERATURE_PRIMARY", "Primary Temp [C]", "%6.2f", -100, 100, 0, 0); + TemperatureNP[2].fill("TEMPERATURE_SECONDARY", "Secondary Temp [C]", "%6.2f", -100, 100, 0, 0); + TemperatureNP[3].fill("HUMIDITY", "Humidity [%]", "%6.2f", 0, 100, 0, 0); + TemperatureNP.fill(getDeviceName(),"FOCUS_TEMPERATURE", "Climate",CLIMATE_TAB, IP_RO, 0, IPS_IDLE); + + // Climate control + ClimateControlSP[AUTO].fill("CLIMATE_AUTO", "On", ISS_OFF); + ClimateControlSP[MANUAL].fill("CLIMATE_MANUAL", "Off", ISS_ON); + ClimateControlSP.fill(getDeviceName(), "CLIMATE_CONTROL", "Climate Control", CLIMATE_TAB, IP_RW, ISR_1OFMANY, 60, IPS_IDLE); + + PrimaryDewHeaterSP[ON].fill("PRIMARY_HEATER_ON", "On", ISS_OFF); + PrimaryDewHeaterSP[OFF].fill("PRIMARY_HEATER_OFF", "Off", ISS_ON); + PrimaryDewHeaterSP.fill(getDeviceName(), "PRIMARY_HEATER", "Heat primary", CLIMATE_TAB, IP_RW, ISR_1OFMANY, 60, IPS_IDLE); + + SecondaryDewHeaterSP[ON].fill("SECONDARY_HEATER_ON", "On", ISS_OFF); + SecondaryDewHeaterSP[OFF].fill("SECONDARY_HEATER_OFF", "Off", ISS_ON); + SecondaryDewHeaterSP.fill(getDeviceName(), "SECONDARY_HEATER", "Heat secondary", CLIMATE_TAB, IP_RW, ISR_1OFMANY, 60, IPS_IDLE); + + FanPowerNP[0].fill("FANPOWER", "Fan power [130..255]", "%3.0f", 130, 255, 1, 255); + FanPowerNP.fill(getDeviceName(), "FANPOWER", "Fan Power", CLIMATE_TAB, IP_RW, 60, IPS_IDLE); + + // Stepping Modes "SpeedStep" and "MicroStep" + SteppingModeSP[SPEED].fill("STEPPING_SPEED", "SpeedStep", ISS_ON); + SteppingModeSP[MICRO].fill("STEPPING_MICRO", "MicroStep", ISS_OFF); + SteppingModeSP.fill(getDeviceName(), "STEPPING_MODE", "Mode", MAIN_CONTROL_TAB, IP_RW, ISR_1OFMANY, 0, IPS_IDLE); + + + // Set limits as per documentation + FocusAbsPosN[0].min = 0; + FocusAbsPosN[0].max = (steppingMode == MICRO) ? 22400 : 1400; // 22400 in microstep mode, 1400 in speedstep mode + FocusAbsPosN[0].step = 1; + + FocusRelPosN[0].min = 0; + FocusRelPosN[0].max = 1000; + FocusRelPosN[0].step = 1; + + // Maximum Position + FocusMaxPosN[0].value = FocusAbsPosN[0].max; + FocusMaxPosNP.p = IP_RO; + + // Dust Cover + CoverSP[OPEN].fill("COVER_OPEN", "Open", ISS_OFF); + CoverSP[CLOSED].fill("COVER_CLOSE", "Close", ISS_ON); + CoverSP.fill(getDeviceName(), "COVER_CONTROL", "Cover Control", DUSTCOVER_TAB, IP_RW, ISR_1OFMANY, 60, IPS_IDLE); + + setDriverInterface(FOCUSER_INTERFACE); //| DUSTCAP_INTERFACE); + + //addAuxControls(); + addDebugControl(); + addConfigurationControl(); + addPollPeriodControl(); + + + return true; +} + +const char *AllunaTCS2::getDefaultName() +{ + return "Alluna TCS2"; +} + +bool AllunaTCS2::updateProperties() +{ + LOG_INFO("updateProperties called"); + INDI::Focuser::updateProperties(); + + if (isConnected()) + { + // turn on green Connected-LED + if (sendCommand("Connect 1\n")) + { + LOG_DEBUG("Turned on Connected-LED¨"); + } + else + { + LOG_ERROR("Cannot turn on Connected-LED"); + } + + // Read these values before defining focuser interface properties + // Only ask for values in sync, because TimerHit is not running, yet. + getPosition(); + getStepping(); + getDustCover(); + getTemperature(); + getClimateControl(); + getFanPower(); + + // Focuser + defineProperty(SteppingModeSP); + defineProperty(&FocusMaxPosNP); + defineProperty(&FocusAbsPosNP); + + // Climate + defineProperty(TemperatureNP); + defineProperty(ClimateControlSP); + defineProperty(PrimaryDewHeaterSP); + defineProperty(SecondaryDewHeaterSP); + defineProperty(FanPowerNP); + + // Cover + defineProperty(CoverSP); + + LOG_INFO("AllunaTCS2 is ready."); + } + else + { + deleteProperty(SteppingModeSP); + deleteProperty(FocusMaxPosNP.name); + deleteProperty(FocusAbsPosNP.name); + + deleteProperty(TemperatureNP); + deleteProperty(ClimateControlSP); + deleteProperty(PrimaryDewHeaterSP); + deleteProperty(SecondaryDewHeaterSP); + deleteProperty(FanPowerNP); + + deleteProperty(CoverSP); + } + + return true; +} + +bool AllunaTCS2::Handshake() +{ + char cmd[DRIVER_LEN] = "HandShake\n", res[DRIVER_LEN] = {0}; + + tcs.unlock(); + bool rc = sendCommand(cmd, res, 0, 2); + + if (rc == false) + return false; + + return res[0] == '\r' && res[1] == '\n'; +} + +bool AllunaTCS2::sendCommand(const char * cmd, char * res, int cmd_len, int res_len) +{ + + if (tcs.try_lock() ) { + bool result; + result = sendCommandNoLock(cmd, res, cmd_len, res_len); + tcs.unlock(); + return result; + } else { + LOG_INFO("sendCommand: lock failed, abort"); + return false; + } + +} + +bool AllunaTCS2::sendCommandNoLock(const char * cmd, char * res, int cmd_len, int res_len) +{ + int nbytes_written = 0, nbytes_read = 0, rc = -1; + + LOG_DEBUG("sendCommand: Send Command"); + tcflush(PortFD, TCIOFLUSH); + + if (cmd_len > 0) + { + char hex_cmd[DRIVER_LEN * 3] = {0}; + hexDump(hex_cmd, cmd, cmd_len); + LOGF_DEBUG("Byte string '%s'", hex_cmd); + rc = tty_write(PortFD, cmd, cmd_len, &nbytes_written); + } + else + { + LOGF_DEBUG("Char string '%s'", cmd); + rc = tty_write_string(PortFD, cmd, &nbytes_written); + } + + if (rc != TTY_OK) + { + char errstr[MAXRBUF] = {0}; + tty_error_msg(rc, errstr, MAXRBUF); + LOGF_ERROR("Serial write error: %s.", errstr); + return false; + } + + if (res == nullptr) + return true; + + if (res_len > 0) { + LOG_DEBUG("sendCommand: Read Answer Bytes"); + rc = tty_read(PortFD, res, res_len, DRIVER_TIMEOUT, &nbytes_read); + } else { + LOG_DEBUG("sendCommand: Read Answer String"); + rc = tty_nread_section(PortFD, res, DRIVER_LEN, DRIVER_STOP_CHAR, DRIVER_TIMEOUT, &nbytes_read); + } + + if (rc != TTY_OK) + { + char errstr[MAXRBUF] = {0}; + tty_error_msg(rc, errstr, MAXRBUF); + LOGF_ERROR("203 Serial read error: %s.", errstr); + return false; + } + + if (res_len > 0) + { + char hex_res[DRIVER_LEN * 3] = {0}; + hexDump(hex_res, res, res_len); + LOGF_DEBUG("Bytes '%s'", hex_res); + } + else + { + LOGF_DEBUG("String '%s'", res); + } + + tcflush(PortFD, TCIOFLUSH); + LOG_DEBUG("sendCommand: Ende"); + return true; +} + + +bool AllunaTCS2::sendCommandOnly(const char * cmd, int cmd_len) +{ + int nbytes_written = 0, rc = -1; + + if (! tcs.try_lock() ) { + LOGF_INFO("sendCommandOnly: %s: lock failed, abort", cmd); + return false; + } + LOG_DEBUG("sendCommandOnly: Anfang"); + tcflush(PortFD, TCIOFLUSH); + + if (cmd_len > 0) + { + char hex_cmd[DRIVER_LEN * 3] = {0}; + hexDump(hex_cmd, cmd, cmd_len); + LOGF_DEBUG("Bytes '%s'", hex_cmd); + rc = tty_write(PortFD, cmd, cmd_len, &nbytes_written); + } + else + { + LOGF_DEBUG("String '%s'", cmd); + rc = tty_write_string(PortFD, cmd, &nbytes_written); + } + + if (rc != TTY_OK) + { + char errstr[MAXRBUF] = {0}; + tty_error_msg(rc, errstr, MAXRBUF); + LOGF_ERROR("Serial write error: %s.", errstr); + tcs.unlock(); + return false; + } + + LOG_DEBUG("sendCommandOnly: Ende"); + return true; +} + +bool AllunaTCS2::receiveNext(char * res, int res_len) +{ + int nbytes_read = 0, rc = -1; + LOG_DEBUG("receiveNext: Anfang"); + + if (res_len > 0) + rc = tty_read(PortFD, res, res_len, DRIVER_TIMEOUT, &nbytes_read); + else + rc = tty_nread_section(PortFD, res, DRIVER_LEN, DRIVER_STOP_CHAR, DRIVER_TIMEOUT, &nbytes_read); + + if (rc != TTY_OK) + { + char errstr[MAXRBUF] = {0}; + tty_error_msg(rc, errstr, MAXRBUF); + LOGF_ERROR("285 Serial read error: %s.", errstr); + tcs.unlock(); + return false; + } + + if (res_len > 0) + { + char hex_res[DRIVER_LEN * 3] = {0}; + hexDump(hex_res, res, res_len); + LOGF_DEBUG("Bytes '%s'", hex_res); + } + else + { + LOGF_DEBUG("String '%s'", res); + } + LOG_DEBUG("receiveNext: Ende"); + + return true; +} + +void AllunaTCS2::receiveDone() +{ + LOG_DEBUG("receiveDone"); + tcflush(PortFD, TCIOFLUSH); + tcs.unlock(); +} + +void AllunaTCS2::hexDump(char * buf, const char * data, int size) +{ + for (int i = 0; i < size; i++) + sprintf(buf + 3 * i, "%02X ", static_cast(data[i])); + + if (size > 0) + buf[3 * size - 1] = '\0'; +} + +// client asks for list of all properties +void AllunaTCS2::ISGetProperties(const char *dev) +{ + INDI::Focuser::ISGetProperties(dev); + LOG_INFO("ISGetProperties called"); + // FIXME: do something like upclass does with controller class +} + +// client wants to change switch value (i.e. click on switch in GUI) +bool AllunaTCS2::ISNewSwitch(const char * dev, const char * name, ISState * states, char * names[], int n) +{ + if (dev != nullptr && !strcmp(dev, getDeviceName()) ) + { + LOGF_INFO("ISNewSwitch called for %s", name); + if (!strcmp(name, "CONNECTION") && !strcmp(names[0], "DISCONNECT") && states[0] == ISS_ON) + { + // turn off green Connected-LED + if (sendCommand("Connect 0\n")) + LOG_DEBUG("Turned off Connected-LED"); + else + LOG_ERROR("Cannot turn off Connected-LED"); + } + // Stepping Mode? + if (SteppingModeSP.isNameMatch(name)) + { + SteppingModeSP.update(states, names, n); + SteppingModeSP.setState(IPS_OK); + SteppingModeSP.apply(); + + // write new stepping mode to tcs2 + setStepping((SteppingModeSP[SPEED].s == ISS_ON) ? SPEED : MICRO); + // update maximum stepping position + FocusAbsPosN[0].max = (steppingMode == MICRO) ? 22400 : 1400; // 22400 in microstep mode, 1400 in speedstep mode + // update max position value + FocusMaxPosN[0].value = FocusAbsPosN[0].max; + // update maximum stepping postion for presets + SetFocuserMaxPosition( FocusAbsPosN[0].max ); // 22400 in microstep mode, 1400 in speedstep mode + // Update clients + IDSetNumber(&FocusAbsPosNP, nullptr); // not sure if this is necessary, because not shown in driver panel + IDSetNumber(&FocusMaxPosNP, nullptr ); + LOGF_INFO("Setting new max position to %d", (steppingMode == MICRO) ? 22400 : 1400 ); + + defineProperty(&FocusMaxPosNP); + defineProperty(&FocusAbsPosNP); + // read focuser position (depends on stepping mode) + getPosition(); + LOGF_INFO("Processed %s",name); + return true; + } + + // Cover Switch? + if (CoverSP.isNameMatch(name)) + { + // Find out which state is requested by the client + const char *actionName = IUFindOnSwitchName(states, names, n); + // Do nothing, if state is already what it should be + int currentCoverIndex = CoverSP.findOnSwitchIndex(); + if (CoverSP[currentCoverIndex].isNameMatch(actionName)) + { + DEBUGF(INDI::Logger::DBG_SESSION, "Cover is already %s", CoverSP[currentCoverIndex].label); + CoverSP.setState(IPS_IDLE); + CoverSP.apply(); + return true; + } + + // Otherwise, let us update the switch state + CoverSP.update(states, names, n); + currentCoverIndex = CoverSP.findOnSwitchIndex(); + if ( setDustCover() ) { + isCoverMoving = true; + DEBUGF(INDI::Logger::DBG_SESSION, "Cover is now %s", CoverSP[currentCoverIndex].label); + CoverSP.setState(IPS_OK); + CoverSP.apply(); + return true; + } else { + DEBUG(INDI::Logger::DBG_SESSION, "Cannot get lock, try again"); + CoverSP.setState(IPS_ALERT); + CoverSP.apply(); + } + } + + // Climate Control Switch? + if (ClimateControlSP.isNameMatch(name)) + { + // Find out which state is requested by the client + const char *actionName = IUFindOnSwitchName(states, names, n); + // Do nothing, if state is already what it should be + int currentClimateControlIndex = ClimateControlSP.findOnSwitchIndex(); + if (ClimateControlSP[currentClimateControlIndex].isNameMatch(actionName)) + { + DEBUGF(INDI::Logger::DBG_SESSION, "Climate Control is already %s", ClimateControlSP[currentClimateControlIndex].label); + ClimateControlSP.setState(IPS_IDLE); + ClimateControlSP.apply(); + return true; + } + + // Otherwise, let us update the switch state + ClimateControlSP.update(states, names, n); + currentClimateControlIndex = ClimateControlSP.findOnSwitchIndex(); + if ( setClimateControl((currentClimateControlIndex==AUTO) ? MANUAL: AUTO) ) { + DEBUGF(INDI::Logger::DBG_SESSION, "ClimateControl is now %s", CoverSP[currentClimateControlIndex].label); + ClimateControlSP.setState(IPS_OK); + ClimateControlSP.apply(); + return true; + } else { + DEBUG(INDI::Logger::DBG_SESSION, "Cannot get lock, try again"); + ClimateControlSP.setState(IPS_ALERT); + ClimateControlSP.apply(); + } + } + + // PrimaryDewHeater Switch? + if (PrimaryDewHeaterSP.isNameMatch(name)) + { + // Find out which state is requested by the client + const char *actionName = IUFindOnSwitchName(states, names, n); + // Do nothing, if state is already what it should be + int currentPrimaryDewHeaterIndex = PrimaryDewHeaterSP.findOnSwitchIndex(); + if (PrimaryDewHeaterSP[currentPrimaryDewHeaterIndex].isNameMatch(actionName)) + { + DEBUGF(INDI::Logger::DBG_SESSION, "PrimaryDewHeater is already %s", PrimaryDewHeaterSP[currentPrimaryDewHeaterIndex].label); + PrimaryDewHeaterSP.setState(IPS_IDLE); + PrimaryDewHeaterSP.apply(); + return true; + } + + // Otherwise, let us update the switch state + PrimaryDewHeaterSP.update(states, names, n); + currentPrimaryDewHeaterIndex = PrimaryDewHeaterSP.findOnSwitchIndex(); + if ( setPrimaryDewHeater((currentPrimaryDewHeaterIndex==OFF) ? ON:OFF) ) { + DEBUGF(INDI::Logger::DBG_SESSION, "PrimaryDewHeater is now %s", PrimaryDewHeaterSP[currentPrimaryDewHeaterIndex].label); + PrimaryDewHeaterSP.setState(IPS_OK); + PrimaryDewHeaterSP.apply(); + return true; + } else { + DEBUG(INDI::Logger::DBG_SESSION, "Cannot get lock, try again"); + PrimaryDewHeaterSP.setState(IPS_ALERT); + PrimaryDewHeaterSP.apply(); + } + } + + // SecondaryDewHeater Switch? + if (SecondaryDewHeaterSP.isNameMatch(name)) + { + // Find out which state is requested by the client + const char *actionName = IUFindOnSwitchName(states, names, n); + // Do nothing, if state is already what it should be + int currentSecondaryDewHeaterIndex = SecondaryDewHeaterSP.findOnSwitchIndex(); + if (SecondaryDewHeaterSP[currentSecondaryDewHeaterIndex].isNameMatch(actionName)) + { + DEBUGF(INDI::Logger::DBG_SESSION, "SecondaryDewHeater is already %s", SecondaryDewHeaterSP[currentSecondaryDewHeaterIndex].label); + SecondaryDewHeaterSP.setState(IPS_IDLE); + SecondaryDewHeaterSP.apply(); + return true; + } + + // Otherwise, let us update the switch state + SecondaryDewHeaterSP.update(states, names, n); + currentSecondaryDewHeaterIndex = SecondaryDewHeaterSP.findOnSwitchIndex(); + if ( setSecondaryDewHeater((currentSecondaryDewHeaterIndex==OFF) ? ON: OFF) ) { + DEBUGF(INDI::Logger::DBG_SESSION, "SecondaryDewHeater is now %s", SecondaryDewHeaterSP[currentSecondaryDewHeaterIndex].label); + SecondaryDewHeaterSP.setState(IPS_OK); + SecondaryDewHeaterSP.apply(); + return true; + } else { + DEBUG(INDI::Logger::DBG_SESSION, "Cannot get lock, try again"); + SecondaryDewHeaterSP.setState(IPS_ALERT); + SecondaryDewHeaterSP.apply(); + } + } + + + + } + return INDI::Focuser::ISNewSwitch(dev, name, states, names, n); +} + +// client wants to change number value +bool AllunaTCS2::ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) +{ + LOGF_INFO("ISNewSwitch called for %s\n", name); + if (dev != nullptr && strcmp(dev, getDeviceName()) == 0) + { + // Fan Power + if (FanPowerNP.isNameMatch(name)) + { + // Try to update settings + int power=values[0]; + if (power>255) power=255; + if (power<0) power=0; + if (setFanPower(power)) + { + FanPowerNP.update(values, names, n); + FanPowerNP.setState(IPS_OK); + } + else + { + FanPowerNP.setState(IPS_ALERT); + } + FanPowerNP.apply(); + return true; + } + + } + + return INDI::Focuser::ISNewNumber(dev, name, values, names, n); +} + +IPState AllunaTCS2::MoveAbsFocuser(uint32_t targetTicks) +{ + LOGF_INFO("MoveAbsFocuser %d called", targetTicks); + char cmd[DRIVER_LEN]; + snprintf(cmd, DRIVER_LEN, "FocuserGoTo %d\r\n", targetTicks); + bool rc = sendCommandOnly(cmd); + if (rc == false) { + LOGF_ERROR("MoveAbsFocuser %d failed", targetTicks); + return IPS_ALERT; + } + isFocuserMoving = true; + + return IPS_BUSY; +} + +IPState AllunaTCS2::MoveRelFocuser(FocusDirection dir, uint32_t ticks) +{ + m_TargetDiff = ticks * ((dir == FOCUS_INWARD) ? -1 : 1); + return MoveAbsFocuser(FocusAbsPosN[0].value + m_TargetDiff); +} + +bool AllunaTCS2::AbortFocuser() +{ + return sendCommand("FocuserStop\n"); +} + +void AllunaTCS2::TimerHit() +{ + //LOG_INFO("TimerHit"); + if (!isConnected()) + return; // No need to reset timer if we are not connected anymore + + // try to read temperature, if it works no lock was present + if (getTemperature() && getFanPower()) { + SetTimer(getCurrentPollingPeriod()); + return; + } + // if we could not read temperature, a lock is set and we need to check if there is input to be processed. + + bool actionInProgress = isFocuserMoving || isCoverMoving; + // expect and process device output while present + char res[DRIVER_LEN] = {0}; + + // read a line, if available + while (actionInProgress && receiveNext(res)) + { + int32_t pos; + + if ( res[1] == '#') { + switch (res[0]) + { + case 'A': // aux1 on (primary mirror heating) + LOG_INFO("Primary heater switched ON"); + break; + case 'B': // aux1 off (primary mirror heating) + LOG_INFO("Primary heater switched OFF"); + break; + case 'C': // aux2 on (secondary mirror heating) + LOG_INFO("Secondary heater switched ON"); + break; + case 'D': // aux2 off (secondary mirror heating) + LOG_INFO("Secondary heater switched OFF"); + break; + case 'E': // climate control ON + LOG_INFO("Climate Control switched ON"); + break; + case 'F': // climate control OFF + LOG_INFO("Climate Control switched OFF"); + break; + case 'G': // fan slider return value + break; + case 'Q': // focuser home run start + break; + case 'U': // back focus minimum for optic "None" + break; + case 'V': // back focus maximum for optic "None" + break; + case 'W': // back focus minimum for optic "Corrector" + break; + case 'X': // back focus maximum for optic "Corrector" + break; + case 'Y': // back focus minimum for optic "Reducer" + break; + case 'Z': // back focus maximum for optic "Reducer" + break; + case 'a': // ambient temperature correction value + break; + case 'b': // primary temperature correction value + break; + case 'c': // secondary temperature correction value + break; + case 'K': // new focuser position + pos = 1e6; + sscanf(res, "K#%d", &pos); + //LOGF_INFO("TimerHit: new pos (%d)",pos); + if (pos != 1e6) { + FocusAbsPosN[0].value = pos; + } + FocusAbsPosNP.s = IPS_BUSY; + FocusRelPosNP.s = IPS_BUSY; + IDSetNumber(&FocusAbsPosNP, nullptr); + break; + case 'I': // starting to focus + LOG_INFO("TimerHit: starting to focus"); + break; + case 'J': // end of focusing + LOG_INFO("TimetHit: end of focusing"); + isFocuserMoving = false; + FocusAbsPosNP.s = IPS_OK; + IDSetNumber(&FocusAbsPosNP, nullptr); + receiveDone(); + break; + case 'O': // cover started moving + LOG_INFO("TimerHit: cover started moving"); + CoverSP.setState(IPS_BUSY); + CoverSP.apply(); + break; + case 'H': // cover stopped moving + LOG_INFO("TimetHit: cover stopped moving"); + isCoverMoving = false; + receiveDone(); + CoverSP.setState(IPS_OK); + CoverSP.apply(); + break; + default: // unexpected output + LOGF_INFO("TimerHit: unexpected response (%s)", res); + } + } else { + LOGF_INFO("TimerHit: unexpected response (%s)", res); + } + actionInProgress = isFocuserMoving || isCoverMoving; + } + + // What is the last read position? + double currentPosition = FocusAbsPosN[0].value; + + // Check if we have a pending motion + // if isMoving() is false, then we stopped, so we need to set the Focus Absolute + // and relative properties to OK + if ( (FocusAbsPosNP.s == IPS_BUSY || FocusRelPosNP.s == IPS_BUSY) ) + { + FocusAbsPosNP.s = IPS_OK; + FocusRelPosNP.s = IPS_OK; + IDSetNumber(&FocusAbsPosNP, nullptr); + IDSetNumber(&FocusRelPosNP, nullptr); + } + // If there was a different between last and current positions, let's update all clients + else if (currentPosition != FocusAbsPosN[0].value) + { + IDSetNumber(&FocusAbsPosNP, nullptr); + } + + SetTimer(getCurrentPollingPeriod()); +} + +bool AllunaTCS2::isMoving() +{ + return isFocuserMoving; +} + +bool AllunaTCS2::getTemperature() +{ + // timestamp, when we updated temperatur + static std::chrono::system_clock::time_point last_temp_update = std::chrono::system_clock::now(); + static bool first_run = true; + + // the command GetTemperatures will respond with 4 lines: + // R#{ambient_temperature} + // S#{primary_mirror_tempertature} + // T#{secondary_mirror_temperature} + // d#{ambient-humidity} + + std::chrono::duration seconds = std::chrono::system_clock::now() - last_temp_update; + if ( !first_run && seconds.count() < 300 ) // update every 300 seconds + { + if (tcs.try_lock()) { + tcs.unlock(); // we need to get lock, to make TimerHit behave the same when we block reading temperature + return true; // return true, if we could get the lock + } else { + return false; // return false, if we could not get the lock + } + } + else if ( sendCommandOnly("GetTemperatures\n") ) + { + TemperatureNP.setState(IPS_BUSY); + isGetTemperature = true; + + // expect and process device output while present + char res[DRIVER_LEN] = {0}; + float value; + + // read a line, if available + while (isGetTemperature && receiveNext(res)) + { + switch (res[0]) + { + case 'R': // ambient temperature value + sscanf(res, "R#%f", &value); + TemperatureNP[0].value = value; + break; + case 'S': // primary mirror temperature value + sscanf(res, "S#%f", &value); + TemperatureNP[1].value = value; + break; + case 'T': // secondary mirror temperature value + sscanf(res, "T#%f", &value); + TemperatureNP[2].value = value; + break; + case 'd': // ambient humidity value + sscanf(res, "d#%f", &value); + TemperatureNP[3].value = value; + receiveDone(); + isGetTemperature=false; + TemperatureNP.setState(IPS_OK); + break; + default: // unexpected output + LOGF_ERROR("GetTemperatures: unexpected response (%s)", res); + } + } + first_run = false; + last_temp_update = std::chrono::system_clock::now(); + return true; + } + return false; +} + +bool AllunaTCS2::getPosition() +{ + char res[DRIVER_LEN] = {0}; + + if (sendCommand("GetFocuserPosition\n", res, 0) == false) + return false; + + int32_t pos = 1e6; + sscanf(res, "%d", &pos); + + if (pos == 1e6) + return false; + + FocusAbsPosN[0].value = pos; + + FocusAbsPosNP.s = IPS_OK; + IDSetNumber(&FocusAbsPosNP, nullptr); // display in user interface + + return true; +} + +bool AllunaTCS2::getDustCover() +{ + char res[DRIVER_LEN] = {0}; + + if (sendCommand("GetDustCover\n", res, 0) == false) + return false; + + int32_t value = -1; + sscanf(res, "%d", &value); + + if (value == -1) + return false; + + DEBUGF(INDI::Logger::DBG_SESSION, "Cover status read to be %s (%d)", (value==1)?"open":"closed", value); + CoverSP[OPEN ].setState((value==1)?ISS_ON:ISS_OFF); + CoverSP[CLOSED].setState((value!=1)?ISS_ON:ISS_OFF); + CoverSP.setState(IPS_OK); + + return true; +} + +bool AllunaTCS2::getStepping() +{ + char res[DRIVER_LEN] = {0}; + + if (sendCommand("GetFocuserMode\n", res) == false) + return false; + + int32_t mode = 1e6; + sscanf(res, "%d", &mode); + + if (mode == 1e6) + return false; + + // mode=1: Microstep, mode=0: Speedstep + steppingMode = (mode == 0) ? SPEED : MICRO; + SteppingModeSP[SPEED].setState((mode == 0) ? ISS_ON : ISS_OFF); + SteppingModeSP[MICRO].setState((mode == 0) ? ISS_OFF : ISS_ON); + SteppingModeSP.setState(IPS_OK); + + // Set limits as per documentation + FocusAbsPosN[0].max = (steppingMode == MICRO) ? 22400 : 1400; // 22400 in microstep mode, 1400 in speedstep mode + LOGF_INFO("readStepping: set max position to %d",(int)FocusAbsPosN[0].max); + return true; +} + +bool AllunaTCS2::setStepping(SteppingMode mode) +{ + int value; + char cmd[DRIVER_LEN] = {0}; + steppingMode=mode; + value = (mode == SPEED) ? 0 : 1; + LOGF_INFO("Setting stepping mde to: %s", (mode==SPEED)?"SPEED":"micro"); + LOGF_INFO("Setting stepping mode to: %d", value); + snprintf(cmd, DRIVER_LEN, "SetFocuserMode %d\n", value); + return sendCommand(cmd); +} + +bool AllunaTCS2::setDustCover() +{ + char cmd[DRIVER_LEN] = {0}; + snprintf(cmd, DRIVER_LEN, "SetDustCover\n"); // opens/closes dust cover (state toggle) + return sendCommandOnly(cmd); // response is processed in TimerHit +} + +bool AllunaTCS2::getClimateControl() +{ + char res[DRIVER_LEN] = {0}; + + if (sendCommand("GetClimateControl\n", res, 0) == false) + return false; + + int32_t value = -1; + sscanf(res, "%d", &value); + + if (value == -1) + return false; + + DEBUGF(INDI::Logger::DBG_SESSION, "Climate Control status read to be %s (%d)", (value==1)?"automatic":"manual", value); + ClimateControlSP[AUTO ].setState((value==1)?ISS_ON:ISS_OFF); + ClimateControlSP[MANUAL].setState((value!=1)?ISS_ON:ISS_OFF); + ClimateControlSP.setState(IPS_OK); + + return true; +} + +bool AllunaTCS2::setClimateControl(ClimateControlMode mode) +{ + char cmd[DRIVER_LEN] = {0}; + int value; + value = (mode == AUTO) ? 1 : 0; + snprintf(cmd, DRIVER_LEN, "SetClimateControl %d\n", value); // enable/disable climate control + return sendCommand(cmd); +} + +bool AllunaTCS2::getPrimaryDewHeater() +{ + char res[DRIVER_LEN] = {0}; + + if (sendCommand("GetAux1\n", res, 0) == false) + return false; + + int32_t value = -1; + sscanf(res, "%d", &value); + + if (value == -1) + return false; + + DEBUGF(INDI::Logger::DBG_SESSION, "PrimaryDewHeater status read to be %s (%d)", (value==1)?"ON":"OFF", value); + PrimaryDewHeaterSP[ON ].setState((value==1)?ISS_ON:ISS_OFF); + PrimaryDewHeaterSP[OFF].setState((value!=1)?ISS_ON:ISS_OFF); + PrimaryDewHeaterSP.setState(IPS_OK); + + return true; +} + +bool AllunaTCS2::setPrimaryDewHeater(DewHeaterMode mode) +{ + char cmd[DRIVER_LEN] = {0}; + int value; + value = (mode == ON) ? 1 : 0; + snprintf(cmd, DRIVER_LEN, "SetAux1 %d\n", value); // enable/disable heating + return sendCommand(cmd); +} + +bool AllunaTCS2::getSecondaryDewHeater() +{ + char res[DRIVER_LEN] = {0}; + + if (sendCommand("GetAux2\n", res, 0) == false) + return false; + + int32_t value = -1; + sscanf(res, "%d", &value); + + if (value == -1) + return false; + + DEBUGF(INDI::Logger::DBG_SESSION, "SecondaryDewHeater status read to be %s (%d)", (value==1)?"ON":"OFF", value); + SecondaryDewHeaterSP[ON ].setState((value==1)?ISS_ON:ISS_OFF); + SecondaryDewHeaterSP[OFF].setState((value!=1)?ISS_ON:ISS_OFF); + SecondaryDewHeaterSP.setState(IPS_OK); + + return true; +} + +bool AllunaTCS2::setSecondaryDewHeater(DewHeaterMode mode) +{ + char cmd[DRIVER_LEN] = {0}; + int value; + value = (mode == ON) ? 1 : 0; + snprintf(cmd, DRIVER_LEN, "SetAux2 %d\n", value); // enable/disable heating + return sendCommand(cmd); +} + +bool AllunaTCS2::getFanPower() +{ + // timestamp, when we updated temperatur + static std::chrono::system_clock::time_point last_temp_update = std::chrono::system_clock::now(); + static bool first_run = true; + + char res[DRIVER_LEN] = {0}; + + std::chrono::duration seconds = std::chrono::system_clock::now() - last_temp_update; + if ( !first_run && seconds.count() < 3 ) // update every 3 seconds + { + if (tcs.try_lock()) { + tcs.unlock(); // we need to get lock, to make TimerHit behave the same when we block reading temperature + return true; // return true, if we could get the lock + } else { + return false; // return false, if we could not get the lock + } + } + if (sendCommand("GetFanPower\n", res, 0) == false) + return false; + + int32_t value = -1; + sscanf(res, "%d", &value); + + if (value == -1) + return false; + + if (value != (int)FanPowerNP[0].value) { + LOGF_INFO("FanPower read to be %d", value); + FanPowerNP[0].value = (double)value; + FanPowerNP.setState(IPS_OK); + FanPowerNP.apply(); + } + return true; +} + +bool AllunaTCS2::setFanPower(int value) +{ + char cmd[DRIVER_LEN] = {0}; + snprintf(cmd, DRIVER_LEN, "SetFanPower %d\n", value); // enable/disable heating + return sendCommand(cmd); +} + + +bool AllunaTCS2::saveConfigItems(FILE *fp) +{ + INDI::Focuser::saveConfigItems(fp); + + // We need to reserve and save stepping mode + // so that the next time the driver is loaded, it is remembered and applied. + //IUSaveConfigSwitch(fp, &SteppingModeSP); -- not needed, because tcs2 stores state internally + + return true; +} + diff --git a/drivers/focuser/alluna_tcs2.h b/drivers/focuser/alluna_tcs2.h new file mode 100644 index 0000000000..c16859c0c5 --- /dev/null +++ b/drivers/focuser/alluna_tcs2.h @@ -0,0 +1,168 @@ +/* + Skeleton Focuser Driver + + Modify this driver when developing new absolute position + based focusers. This driver uses serial communication by default + but it can be changed to use networked TCP/UDP connection as well. + + Copyright(c) 2019 Jasem Mutlaq. All rights reserved. + + Thanks to Rigel Systems, especially Gene Nolan and Leon Palmer, + for their support in writing this driver. + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +*/ + +#pragma once +#include "indifocuser.h" +#include +#include +#include + +class AllunaTCS2 : public INDI::Focuser //, public INDI::DustCapInterface +{ + public: + AllunaTCS2(); + + virtual bool Handshake() override; + const char *getDefaultName() override; + + bool initProperties() override; + bool updateProperties() override; + void ISGetProperties(const char *dev) override; + + bool ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) override; + bool ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) override; + + protected: + // From INDI::DefaultDevice + void TimerHit() override; + bool saveConfigItems(FILE *fp) override; + + // From INDI::Focuser + IPState MoveRelFocuser(FocusDirection dir, uint32_t ticks) override; + IPState MoveAbsFocuser(uint32_t targetTicks) override; + bool AbortFocuser() override; + + private: + // while a multi response action is running (such as GetTemperatures), we cannot process additional commands + std::mutex tcs; + + // focuser internal state + bool isFocuserMoving = false; + + // is getTemperature command in progress + bool isGetTemperature = false; + + // is setDustCover command in progress + bool isCoverMoving = false; + + // Climate + INDI::PropertyNumber TemperatureNP{4}; // note: last value is humidity, not temperature. + INDI::PropertySwitch ClimateControlSP{2}; + typedef enum { AUTO, MANUAL } ClimateControlMode; + INDI::PropertySwitch PrimaryDewHeaterSP{2}, SecondaryDewHeaterSP{2}; + typedef enum { ON, OFF } DewHeaterMode; + INDI::PropertyNumber FanPowerNP{1}; + + // Focuser + INDI::PropertySwitch SteppingModeSP{2}; + + typedef enum { MICRO = 1, SPEED = 0 } SteppingMode; + SteppingMode steppingMode=MICRO; + + // Dust cover + INDI::PropertySwitch CoverSP{2}; + typedef enum { OPEN, CLOSED } CoverMode; + + + /////////////////////////////////////////////////////////////////////////////// + /// Read Data From Controller + /////////////////////////////////////////////////////////////////////////////// + bool getTemperature(); + bool getPosition(); + bool getStepping(); + bool getDustCover(); + bool getClimateControl(); + bool getPrimaryDewHeater(); + bool getSecondaryDewHeater(); + bool getFanPower(); + + /////////////////////////////////////////////////////////////////////////////// + /// Write Data to Controller + /////////////////////////////////////////////////////////////////////////////// + bool setStepping(SteppingMode mode); + bool setDustCover(void); // open/close dust cover + bool setClimateControl(ClimateControlMode mode); // turn on/off climate control + bool setPrimaryDewHeater(DewHeaterMode mode); // turn on/off climate control + bool setSecondaryDewHeater(DewHeaterMode mode); // turn on/off climate control + bool setFanPower(int); // read fan power (between 121=47% and 255=100%, or 0=off) + + /////////////////////////////////////////////////////////////////////////////// + /// Utility Functions + /////////////////////////////////////////////////////////////////////////////// + /** + * @brief sendCommand Send a string command to device. + * @param cmd Command to be sent. Can be either NULL TERMINATED or just byte buffer. + * @param res If not nullptr, the function will wait for a response from the device. If nullptr, it returns true immediately + * after the command is successfully sent. + * @param cmd_len if -1, it is assumed that the @a cmd is a null-terminated string. Otherwise, it would write @a cmd_len bytes from + * the @a cmd buffer. + * @param res_len if -1 and if @a res is not nullptr, the function will read until it detects the default delimeter DRIVER_STOP_CHAR + * up to DRIVER_LEN length. Otherwise, the function will read @a res_len from the device and store it in @a res. + * @return True if successful, false otherwise. + */ + bool sendCommand(const char * cmd, char * res = nullptr, int cmd_len = -1, int res_len = -1); + bool sendCommandNoLock(const char * cmd, char * res = nullptr, int cmd_len = -1, int res_len = -1); + bool sendCommandOnly(const char * cmd, int cmd_len = -1); + bool receiveNext(char * res, int res_len = -1); + void receiveDone(); + + /** + * @brief hexDump Helper function to print non-string commands to the logger so it is easier to debug + * @param buf buffer to format the command into hex strings. + * @param data the command + * @param size length of the command in bytes. + * @note This is called internally by sendCommand, no need to call it directly. + */ + void hexDump(char * buf, const char * data, int size); + + /** + * @return is the focuser in motion? + */ + bool isMoving(); + + ///////////////////////////////////////////////////////////////////////////// + /// Class Variables + ///////////////////////////////////////////////////////////////////////////// + int32_t m_TargetDiff { 0 }; + + ///////////////////////////////////////////////////////////////////////////// + /// Static Helper Values + ///////////////////////////////////////////////////////////////////////////// + static constexpr const char * FOCUSER_TAB = "Focus"; + static constexpr const char * ROTATOR_TAB = "Rotate"; + static constexpr const char * CLIMATE_TAB = "Climate"; + static constexpr const char * DUSTCOVER_TAB = "Dust Cover"; + // 'LF' is the stop char + static const char DRIVER_STOP_CHAR { 0x0A }; + // Update temperature every 10x POLLMS. For 500ms, we would + // update the temperature one every 5 seconds. + static constexpr const uint8_t DRIVER_TEMPERATURE_FREQ {10}; + // Wait up to a maximum of 3 seconds for serial input + static constexpr const uint8_t DRIVER_TIMEOUT {5}; + // Maximum buffer for sending/receving. + static constexpr const uint8_t DRIVER_LEN {64}; +};