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}; +};