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