diff --git a/libs/indibase/indigpsinterface.cpp b/libs/indibase/indigpsinterface.cpp new file mode 100644 index 0000000000..9cdff47f21 --- /dev/null +++ b/libs/indibase/indigpsinterface.cpp @@ -0,0 +1,295 @@ +/* + GPS Interface + Copyright (C) 2023 Jasem Mutlaq (mutlaqja@ikarustech.com) + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + +*/ + +#include "indigpsinterface.h" + +#include "indilogger.h" + +#include +#include + +namespace INDI +{ + +////////////////////////////////////////////////////////////////////// +/// +////////////////////////////////////////////////////////////////////// +GPSInterface::GPSInterface(DefaultDevice * defaultDevice) : m_DefaultDevice(defaultDevice) +{ + m_UpdateTimer.callOnTimeout(std::bind(&GPSInterface::checkGPSState, this)); + m_UpdateTimer.setSingleShot(true); +} + +////////////////////////////////////////////////////////////////////// +/// +////////////////////////////////////////////////////////////////////// +void GPSInterface::initProperties(const char * groupName) +{ + time(&m_GPSTime); + + PeriodNP[0].fill("PERIOD", "Period (s)", "%.f", 0, 3600, 60.0, 0); + PeriodNP.fill(m_DefaultDevice->getDeviceName(), "GPS_REFRESH_PERIOD", "Refresh", groupName, IP_RW, 0, IPS_IDLE); + + RefreshSP[0].fill("REFRESH", "GPS", ISS_OFF); + RefreshSP.fill(m_DefaultDevice->getDeviceName(), "GPS_REFRESH", "Refresh", groupName, IP_RW, ISR_ATMOST1, 0, IPS_IDLE); + + LocationNP[LOCATION_LATITUDE].fill("LAT", "Lat (dd:mm:ss)", "%010.6m", -90, 90, 0, 0.0); + LocationNP[LOCATION_LONGITUDE].fill("LONG", "Lon (dd:mm:ss)", "%010.6m", 0, 360, 0, 0.0); + LocationNP[LOCATION_ELEVATION].fill("ELEV", "Elevation (m)", "%g", -200, 10000, 0, 0); + LocationNP.fill(m_DefaultDevice->getDeviceName(), "GEOGRAPHIC_COORD", "Location", groupName, IP_RO, 60, IPS_IDLE); + + // System Time Settings + SystemTimeUpdateSP[UPDATE_NEVER].fill("UPDATE_NEVER", "Never", ISS_OFF); + SystemTimeUpdateSP[UPDATE_ON_STARTUP].fill("UPDATE_ON_STARTUP", "On Startup", ISS_ON); + SystemTimeUpdateSP[UPDATE_ON_REFRESH].fill("UPDATE_ON_REFRESH", "On Refresh", ISS_OFF); + SystemTimeUpdateSP.fill(m_DefaultDevice->getDeviceName(), "SYSTEM_TIME_UPDATE", "System Time", groupName, IP_RW, + ISR_1OFMANY, 60, + IPS_IDLE); + SystemTimeUpdateSP.load(); + + TimeTP[0].fill("UTC", "UTC Time", nullptr); + TimeTP[1].fill("OFFSET", "UTC Offset", nullptr); + TimeTP.fill(m_DefaultDevice->getDeviceName(), "TIME_UTC", "UTC", groupName, IP_RO, 60, IPS_IDLE); +} + +////////////////////////////////////////////////////////////////////// +/// +////////////////////////////////////////////////////////////////////// +void GPSInterface::checkGPSState() +{ + IPState state = updateGPS(); + + LocationNP.setState(state); + TimeTP.setState(state); + RefreshSP.setState(state); + + switch (state) + { + // Ok + case IPS_OK: + LocationNP.apply(); + TimeTP.apply(); + // We got data OK, but if we are required to update once in a while, we'll call it. + if (PeriodNP[0].getValue() > 0) + m_UpdateTimer.setInterval(PeriodNP[0].getValue() * 1000); + + // Update system time + // This ideally should be done only ONCE + switch (SystemTimeUpdateSP.findOnSwitchIndex()) + { + case UPDATE_ON_STARTUP: + if (m_SystemTimeUpdated == false) + { + setSystemTime(m_GPSTime); + m_SystemTimeUpdated = true; + } + break; + + case UPDATE_ON_REFRESH: + setSystemTime(m_GPSTime); + break; + + default: + break; + } + + m_UpdateTimer.start(); + return; + break; + + // GPS fix is in progress or alert + case IPS_ALERT: + LocationNP.apply(); + TimeTP.apply(); + break; + + default: + break; + } + + m_UpdateTimer.setInterval(5000); + m_UpdateTimer.start(); +} + +////////////////////////////////////////////////////////////////////// +/// +////////////////////////////////////////////////////////////////////// +bool GPSInterface::updateProperties() +{ + if (m_DefaultDevice->isConnected()) + { + // Update GPS and send values to client + IPState state = updateGPS(); + + LocationNP.setState(state); + m_DefaultDevice->defineProperty(LocationNP); + TimeTP.setState(state); + m_DefaultDevice->defineProperty(TimeTP); + RefreshSP.setState(state); + m_DefaultDevice->defineProperty(RefreshSP); + m_DefaultDevice->defineProperty(PeriodNP); + m_DefaultDevice->defineProperty(SystemTimeUpdateSP); + + if (state != IPS_OK) + { + if (state == IPS_BUSY) + DEBUGDEVICE(m_DefaultDevice->getDeviceName(), Logger::DBG_SESSION, "GPS fix is in progress..."); + + m_UpdateTimer.setInterval(5000); + m_UpdateTimer.start(); + } + else if (PeriodNP[0].getValue() > 0) + { + m_UpdateTimer.setInterval(PeriodNP[0].getValue()); + m_UpdateTimer.start(); + } + } + else + { + m_DefaultDevice->deleteProperty(LocationNP); + m_DefaultDevice->deleteProperty(TimeTP); + m_DefaultDevice->deleteProperty(RefreshSP); + m_DefaultDevice->deleteProperty(PeriodNP); + m_DefaultDevice->deleteProperty(SystemTimeUpdateSP); + m_UpdateTimer.stop(); + m_SystemTimeUpdated = false; + } + + return true; +} + +////////////////////////////////////////////////////////////////////// +/// +////////////////////////////////////////////////////////////////////// +bool GPSInterface::processNumber(const char * dev, const char * name, double values[], char * names[], int n) +{ + INDI_UNUSED(dev); + + if (PeriodNP.isNameMatch(name)) + { + double prevPeriod = PeriodNP[0].getValue(); + PeriodNP.update(values, names, n); + // Do not remove timer if GPS update is still in progress + if (m_UpdateTimer.isActive() && RefreshSP.getState() != IPS_BUSY) + { + m_UpdateTimer.stop(); + } + + if (PeriodNP[0].getValue() == 0) + { + DEBUGDEVICE(m_DefaultDevice->getDeviceName(), Logger::DBG_SESSION, "GPS Update Timer disabled."); + } + else + { + m_UpdateTimer.setInterval(PeriodNP[0].value * 1000); + m_UpdateTimer.start(); + // Need to warn user this is not recommended. Startup values should be enough + if (prevPeriod == 0) + DEBUGDEVICE(m_DefaultDevice->getDeviceName(), Logger::DBG_SESSION, + "GPS Update Timer enabled. Warning: Updating system-wide time repeatedly may lead to undesirable side-effects."); + } + + PeriodNP.setState(IPS_OK); + PeriodNP.apply(); + + return true; + } + + return false; +} + +////////////////////////////////////////////////////////////////////// +/// +////////////////////////////////////////////////////////////////////// +bool GPSInterface::processSwitch(const char * dev, const char * name, ISState * states, char * names[], int n) +{ + INDI_UNUSED(dev); + + // Refresh + if (RefreshSP.isNameMatch(name)) + { + RefreshSP[0].s = ISS_OFF; + RefreshSP.setState(IPS_OK); + RefreshSP.apply(); + + // Manual trigger + checkGPSState(); + return true; + } + // System Time Update + else if (SystemTimeUpdateSP.isNameMatch(name)) + { + SystemTimeUpdateSP.update(states, names, n); + SystemTimeUpdateSP.setState(IPS_OK); + SystemTimeUpdateSP.apply(); + if (SystemTimeUpdateSP.findOnSwitchIndex() == UPDATE_ON_REFRESH) + DEBUGDEVICE(m_DefaultDevice->getDeviceName(), Logger::DBG_WARNING, + "Updating system time on refresh may lead to undesirable effects on system time accuracy."); + return true; + } + + return false; +} + +////////////////////////////////////////////////////////////////////// +/// +////////////////////////////////////////////////////////////////////// +bool GPSInterface::setSystemTime(time_t &raw_time) +{ +#ifdef __linux__ +#if defined(__GNU_LIBRARY__) +#if (__GLIBC__ >= 2) && (__GLIBC_MINOR__ > 30) + timespec sTime = {}; + sTime.tv_sec = raw_time; + auto rc = clock_settime(CLOCK_REALTIME, &sTime); + if (rc) + DEBUGFDEVICE(m_DefaultDevice->getDeviceName(), Logger::DBG_WARNING, "Failed to update system time: %s", strerror(rc)); +#else + stime(&raw_time); +#endif +#else + stime(&raw_time); +#endif +#else + INDI_UNUSED(raw_time); +#endif + return true; +} + + +////////////////////////////////////////////////////////////////////// +/// +////////////////////////////////////////////////////////////////////// +IPState GPSInterface::updateGPS() +{ + DEBUGDEVICE(m_DefaultDevice->getDeviceName(), Logger::DBG_ERROR, + "updateGPS() must be implemented in GPS device child class to update TIME_UTC and GEOGRAPHIC_COORD properties."); + return IPS_ALERT; +} + + +bool GPSInterface::saveConfigItems(FILE * fp) +{ + PeriodNP.save(fp); + SystemTimeUpdateSP.save(fp); + return true; +} + +} diff --git a/libs/indibase/indigpsinterface.h b/libs/indibase/indigpsinterface.h new file mode 100644 index 0000000000..0bc24e18af --- /dev/null +++ b/libs/indibase/indigpsinterface.h @@ -0,0 +1,134 @@ +/* + GPS Interface + Copyright (C) 2023 Jasem Mutlaq (mutlaqja@ikarustech.com) + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + +*/ + +#pragma once + +#include "indibase.h" +#include "indipropertynumber.h" +#include "indipropertyswitch.h" +#include "indipropertytext.h" +#include "inditimer.h" + +#include + +// Alias +using GI = INDI::GPSInterface; + +namespace INDI +{ + +/** + * \class GPSInterface + \brief Provides interface to implement GPS functionality. + +\author Jasem Mutlaq +*/ +class GPSInterface +{ + public: + enum GPSLocation + { + LOCATION_LATITUDE, + LOCATION_LONGITUDE, + LOCATION_ELEVATION + }; + + protected: + explicit GPSInterface(DefaultDevice * defaultDevice); + virtual ~GPSInterface() = default; + + /** + * \brief Initilize focuser properties. It is recommended to call this function within + * initProperties() of your primary device + * \param groupName Group or tab name to be used to define focuser properties. + */ + void initProperties(const char * groupName); + + /** + * @brief updateProperties Define or Delete Rotator properties based on the connection status of the base device + * @return True if successful, false otherwise. + */ + bool updateProperties(); + + /** \brief Process focus number properties */ + bool processNumber(const char * dev, const char * name, double values[], char * names[], int n); + + /** \brief Process focus switch properties */ + bool processSwitch(const char * dev, const char * name, ISState * states, char * names[], int n); + + /** + * @brief saveConfigItems save focuser properties defined in the interface in config file + * @param fp pointer to config file + * @return Always return true + */ + virtual bool saveConfigItems(FILE * fp); + + /** + * @brief checkGPSState Check and update GPS state + */ + void checkGPSState(); + + /** + * @brief SetSystemTime Update system-wide time + * @param raw_time raw time + * @return true if successful, false other. + * @note Process user must have permission to set time. + */ + bool setSystemTime(time_t &raw_time); + + /** + * @brief updateGPS Retrieve Location & Time from GPS. Update LocationNP & TimeTP properties (value and state) without sending them to the client (i.e. IDSetXXX). + * @return Return overall state. The state should be IPS_OK if data is valid. IPS_BUSY if GPS fix is in progress. IPS_ALERT is there is an error. The clients will only accept values with IPS_OK state. + */ + virtual IPState updateGPS(); + + + // A number vector that stores latitude, longitude and altitude. + INDI::PropertyNumber LocationNP {3}; + + // UTC and UTC Offset + INDI::PropertyText TimeTP {2}; + + // Refresh data + INDI::PropertySwitch RefreshSP {1}; + + // Refresh Period + INDI::PropertyNumber PeriodNP {1}; + + // System Time + INDI::PropertySwitch SystemTimeUpdateSP {3}; + enum + { + UPDATE_NEVER, + UPDATE_ON_STARTUP, + UPDATE_ON_REFRESH + }; + + INDI::Timer m_UpdateTimer; + + // # of seconds since 1970 UTC + std::time_t m_GPSTime; + + // Track whether system time was updated + bool m_SystemTimeUpdated {false}; + + DefaultDevice * m_DefaultDevice { nullptr }; +}; +}