Instrument Neutral Distributed Interface INDI 2.2.5
indigpsinterface.cpp
Go to the documentation of this file.
1/*
2 GPS Interface
3 Copyright (C) 2023 Jasem Mutlaq ([email protected])
4
5 This library is free software; you can redistribute it and/or
6 modify it under the terms of the GNU Lesser General Public
7 License as published by the Free Software Foundation; either
8 version 2.1 of the License, or (at your option) any later version.
9
10 This library is distributed in the hope that it will be useful,
11 but WITHOUT ANY WARRANTY; without even the implied warranty of
12 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13 Lesser General Public License for more details.
14
15 You should have received a copy of the GNU Lesser General Public
16 License along with this library; if not, write to the Free Software
17 Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
18
19*/
20
21#include "indigpsinterface.h"
22
23#include "indilogger.h"
24
25#include <cstring>
26#include <cmath>
27#ifdef __linux__
28#include <cerrno>
29#include <cstdlib>
30#include <ctime>
31#include <fcntl.h>
32#include <unistd.h>
33#include <sys/types.h>
34#include <sys/wait.h>
35#endif
36
37namespace INDI
38{
39
43GPSInterface::GPSInterface(DefaultDevice * defaultDevice) : m_DefaultDevice(defaultDevice)
44{
47}
48
52void GPSInterface::initProperties(const char * groupName)
53{
54 time(&m_GPSTime);
55
56 // @INDI_STANDARD_PROPERTY@
57 PeriodNP[0].fill("PERIOD", "Period (s)", "%.f", 0, 3600, 60.0, 0);
58 PeriodNP.fill(m_DefaultDevice->getDeviceName(), "GPS_REFRESH_PERIOD", "Refresh", groupName, IP_RW, 0, IPS_IDLE);
59
60 // @INDI_STANDARD_PROPERTY@
61 RefreshSP[0].fill("REFRESH", "GPS", ISS_OFF);
62 RefreshSP.fill(m_DefaultDevice->getDeviceName(), "GPS_REFRESH", "Refresh", groupName, IP_RW, ISR_ATMOST1, 0, IPS_IDLE);
63
64 // @INDI_STANDARD_PROPERTY@
65 LocationNP[LOCATION_LATITUDE].fill("LAT", "Lat (dd:mm:ss)", "%010.6m", -90, 90, 0, 0.0);
66 LocationNP[LOCATION_LONGITUDE].fill("LONG", "Lon (dd:mm:ss)", "%010.6m", 0, 360, 0, 0.0);
67 LocationNP[LOCATION_ELEVATION].fill("ELEV", "Elevation (m)", "%g", -200, 10000, 0, 0);
68 LocationNP.fill(m_DefaultDevice->getDeviceName(), "GEOGRAPHIC_COORD", "Location", groupName, IP_RO, 60, IPS_IDLE);
69
70 // System Time Settings
71 // @INDI_STANDARD_PROPERTY@
72 SystemTimeUpdateSP[UPDATE_NEVER].fill("UPDATE_NEVER", "Never", ISS_OFF);
73 SystemTimeUpdateSP[UPDATE_ON_STARTUP].fill("UPDATE_ON_STARTUP", "On Startup", ISS_ON);
74 SystemTimeUpdateSP[UPDATE_ON_REFRESH].fill("UPDATE_ON_REFRESH", "On Refresh", ISS_OFF);
75 SystemTimeUpdateSP.fill(m_DefaultDevice->getDeviceName(), "SYSTEM_TIME_UPDATE", "System Time", groupName, IP_RW,
76 ISR_1OFMANY, 60,
77 IPS_IDLE);
79
80 // @INDI_STANDARD_PROPERTY@
81 TimeTP[0].fill("UTC", "UTC Time", nullptr);
82 TimeTP[1].fill("OFFSET", "UTC Offset", nullptr);
83 TimeTP.fill(m_DefaultDevice->getDeviceName(), "TIME_UTC", "UTC", groupName, IP_RO, 60, IPS_IDLE);
84}
85
90{
91 IPState state = updateGPS();
92
93 LocationNP.setState(state);
94 TimeTP.setState(state);
95 RefreshSP.setState(state);
96
97 switch (state)
98 {
99 // Ok
100 case IPS_OK:
102 TimeTP.apply();
103
104 // Update system time
105 // This ideally should be done only ONCE
107 {
109 if (m_SystemTimeUpdated == false)
110 {
112 m_SystemTimeUpdated = true;
113 }
114 break;
115
118 break;
119
120 default:
121 break;
122 }
123
124 if (PeriodNP[0].getValue() > 0)
125 {
126 // We got data OK, but if we are required to update once in a while, we'll call it.
127 m_UpdateTimer.setInterval(PeriodNP[0].getValue() * 1000);
129 }
130 else
132 return;
133 break;
134
135 // GPS fix is in progress or alert
136 case IPS_ALERT:
138 TimeTP.apply();
139 break;
140
141 default:
142 break;
143 }
144
147}
148
153{
155 {
156 // Update GPS and send values to client
157 IPState state = updateGPS();
158
159 LocationNP.setState(state);
161 TimeTP.setState(state);
163 RefreshSP.setState(state);
167
168 if (state != IPS_OK)
169 {
170 if (state == IPS_BUSY)
171 DEBUGDEVICE(m_DefaultDevice->getDeviceName(), Logger::DBG_SESSION, "GPS fix is in progress...");
172
175 }
176 else if (PeriodNP[0].getValue() > 0)
177 {
178 m_UpdateTimer.setInterval(PeriodNP[0].getValue() * 1000);
180 }
181 }
182 else
183 {
190 m_SystemTimeUpdated = false;
191
192#ifdef __linux__
193 // Restore NTP on disconnect if we were the ones who disabled it
194 restoreNTPOnDisconnect();
195#endif
196 }
197
198 return true;
199}
200
204bool GPSInterface::processNumber(const char * dev, const char * name, double values[], char * names[], int n)
205{
206 INDI_UNUSED(dev);
207
208 if (PeriodNP.isNameMatch(name))
209 {
210 double prevPeriod = PeriodNP[0].getValue();
211 PeriodNP.update(values, names, n);
212 // Do not remove timer if GPS update is still in progress
214 {
216 }
217
218 if (PeriodNP[0].getValue() == 0)
219 {
220 DEBUGDEVICE(m_DefaultDevice->getDeviceName(), Logger::DBG_SESSION, "GPS Update Timer disabled.");
221 }
222 else
223 {
224 m_UpdateTimer.setInterval(PeriodNP[0].value * 1000);
226 // Need to warn user this is not recommended. Startup values should be enough
227 if (prevPeriod == 0)
228 {
229 DEBUGDEVICE(m_DefaultDevice->getDeviceName(), Logger::DBG_SESSION, "GPS Update Timer enabled.");
232 "Updating system-wide time repeatedly may lead to undesirable side-effects.");
233 }
234 }
235
237 PeriodNP.apply();
238
239 return true;
240 }
241
242 return false;
243}
244
248bool GPSInterface::processSwitch(const char * dev, const char * name, ISState * states, char * names[], int n)
249{
250 INDI_UNUSED(dev);
251
252 // Refresh
253 if (RefreshSP.isNameMatch(name))
254 {
255 RefreshSP[0].s = ISS_OFF;
258
259 // Manual trigger
261 return true;
262 }
263 // System Time Update
264 else if (SystemTimeUpdateSP.isNameMatch(name))
265 {
266 SystemTimeUpdateSP.update(states, names, n);
271 "Updating system time on refresh may lead to undesirable effects on system time accuracy.");
272 return true;
273 }
274
275 return false;
276}
277
281bool GPSInterface::setSystemTime(time_t &raw_time)
282{
283#ifdef __linux__
284 // timedate1 SetTime refuses to change the clock while NTP synchronization is active.
285 disableNTPForGPSTime();
286
287 // SetTime takes microseconds since the epoch (UTC), so the host timezone does not matter.
288 const std::string usec = std::to_string(static_cast<int64_t>(raw_time) * 1000000LL);
289 if (!timedateCall({"call", "org.freedesktop.timedate1", "/org/freedesktop/timedate1", "org.freedesktop.timedate1", "SetTime", "xbb", usec, "false", "false"},
290 {"--method", "org.freedesktop.timedate1.SetTime", usec, "false", "false"}))
291 {
293 "Failed to update system time via org.freedesktop.timedate1 SetTime. "
294 "Ensure polkit allows org.freedesktop.timedate1.set-time for this user.");
295 return false;
296 }
297#else
298 INDI_UNUSED(raw_time);
299#endif
300 return true;
301}
302
303
304#ifdef __linux__
308bool GPSInterface::runCommand(const std::vector<std::string> &args, std::string *output)
309{
310 // Build argv before fork, the child may only make async-signal-safe calls.
311 std::vector<char *> argv;
312 for (const auto &arg : args)
313 argv.push_back(const_cast<char *>(arg.c_str()));
314 argv.push_back(nullptr);
315
316 int pipefd[2] = {-1, -1};
317 if (output != nullptr && pipe(pipefd) != 0)
318 return false;
319
320 pid_t pid = fork();
321 if (pid == 0)
322 {
323 // Child process: the driver's stdin/stdout are the INDI protocol channel, so they must not be inherited.
324 int devnull = open("/dev/null", O_RDWR);
325 if (devnull >= 0)
326 dup2(devnull, STDIN_FILENO);
327 if (output != nullptr)
328 {
329 close(pipefd[0]);
330 dup2(pipefd[1], STDOUT_FILENO);
331 close(pipefd[1]);
332 }
333 else if (devnull >= 0)
334 dup2(devnull, STDOUT_FILENO);
335 if (devnull > STDERR_FILENO)
336 close(devnull);
337
338 execvp(argv[0], argv.data());
339 _exit(EXIT_FAILURE);
340 }
341 else if (pid < 0)
342 {
344 "Failed to fork for command %s: %s", argv[0], strerror(errno));
345 if (output != nullptr)
346 {
347 close(pipefd[0]);
348 close(pipefd[1]);
349 }
350 return false;
351 }
352
353 if (output != nullptr)
354 {
355 close(pipefd[1]);
356 output->clear();
357 char buffer[256];
358 ssize_t n;
359 while ((n = read(pipefd[0], buffer, sizeof(buffer))) != 0)
360 {
361 if (n > 0)
362 output->append(buffer, n);
363 else if (errno != EINTR)
364 break;
365 }
366 close(pipefd[0]);
367 }
368
369 int status = 0;
370 while (waitpid(pid, &status, 0) < 0 && errno == EINTR);
371 return WIFEXITED(status) && WEXITSTATUS(status) == 0;
372}
373
377bool GPSInterface::timedateCall(const std::vector<std::string> &busctlArgs, const std::vector<std::string> &gdbusArgs,
378 std::string *output)
379{
380 std::vector<std::string> args;
381
382 if (getenv("FLATPAK_ID") == nullptr)
383 {
384 args = {"busctl"};
385 args.insert(args.end(), busctlArgs.begin(), busctlArgs.end());
386 return runCommand(args, output);
387 }
388
389 // Inside Flatpak, the system bus socket only exists if a --system-talk-name permission was granted.
390 // busctl is not part of the runtime, but gdbus is.
391 if (access("/run/dbus/system_bus_socket", F_OK) == 0)
392 {
393 args = {"gdbus", "call", "--system", "--dest", "org.freedesktop.timedate1", "--object-path", "/org/freedesktop/timedate1"};
394 args.insert(args.end(), gdbusArgs.begin(), gdbusArgs.end());
395 if (runCommand(args, output))
396 return true;
397 }
398
399 // Fall back to running busctl on the host.
400 args = {"flatpak-spawn", "--host", "busctl"};
401 args.insert(args.end(), busctlArgs.begin(), busctlArgs.end());
402 return runCommand(args, output);
403}
404
408bool GPSInterface::isNTPActive()
409{
410 std::string output;
411 if (!timedateCall({"get-property", "org.freedesktop.timedate1", "/org/freedesktop/timedate1", "org.freedesktop.timedate1", "NTP"},
412 {"--method", "org.freedesktop.DBus.Properties.Get", "org.freedesktop.timedate1", "NTP"},
413 &output))
414 return false;
415
416 // busctl prints "b true", gdbus prints "(<true>,)"
417 return output.find("true") != std::string::npos;
418}
419
423bool GPSInterface::setNTPEnabled(bool enabled)
424{
425 const std::string value = enabled ? "true" : "false";
426 return timedateCall({"call", "org.freedesktop.timedate1", "/org/freedesktop/timedate1", "org.freedesktop.timedate1", "SetNTP", "bb", value, "false"},
427 {"--method", "org.freedesktop.timedate1.SetNTP", value, "false"});
428}
429
433void GPSInterface::disableNTPForGPSTime()
434{
435 if (!isNTPActive())
436 return;
437
439 "NTP is active; disabling for GPS time authority...");
440 if (!setNTPEnabled(false))
441 {
442 DEBUGDEVICE(m_DefaultDevice->getDeviceName(), Logger::DBG_WARNING, "Failed to disable NTP.");
443 return;
444 }
445
446 m_NTPDisabledByGPS = true;
447
448 // SetNTP returns once the stop job is queued, and SetTime is rejected until timedated reports NTP inactive.
449 for (int i = 0; i < 10 && isNTPActive(); i++)
450 usleep(200000);
451
452 DEBUGDEVICE(m_DefaultDevice->getDeviceName(), Logger::DBG_SESSION, "NTP disabled by GPS interface.");
453}
454
458void GPSInterface::restoreNTPOnDisconnect()
459{
461 {
463 "Restoring NTP after GPS disconnect...");
464 if (setNTPEnabled(true))
465 {
466 m_NTPDisabledByGPS = false;
468 "NTP re-enabled.");
469 }
470 else
471 {
473 "Failed to re-enable NTP.");
474 }
475 }
476}
477#endif
478
479
484{
486 "updateGPS() must be implemented in GPS device child class to update TIME_UTC and GEOGRAPHIC_COORD properties.");
487 return IPS_ALERT;
488}
489
490
492{
493 PeriodNP.save(fp);
495 return true;
496}
497
498}
bool isConnected() const
const char * getDeviceName() const
Class to provide extended functionality for devices in addition to the functionality provided by INDI...
virtual bool deleteProperty(const char *propertyName)
Delete a property and unregister it. It will also be deleted from all clients.
void defineProperty(INumberVectorProperty *property)
INDI::PropertyNumber LocationNP
void checkGPSState()
checkGPSState Check and update GPS state
DefaultDevice * m_DefaultDevice
bool setSystemTime(time_t &raw_time)
SetSystemTime Update system-wide time.
bool updateProperties()
updateProperties Define or Delete Rotator properties based on the connection status of the base devic...
virtual IPState updateGPS()
updateGPS Retrieve Location & Time from GPS. Update LocationNP & TimeTP properties (value and state) ...
INDI::PropertyNumber PeriodNP
bool processSwitch(const char *dev, const char *name, ISState *states, char *names[], int n)
Process focus switch properties.
INDI::PropertySwitch SystemTimeUpdateSP
bool processNumber(const char *dev, const char *name, double values[], char *names[], int n)
Process focus number properties.
virtual bool saveConfigItems(FILE *fp)
saveConfigItems save focuser properties defined in the interface in config file
INDI::PropertyText TimeTP
INDI::PropertySwitch RefreshSP
void initProperties(const char *groupName)
Initialize focuser properties. It is recommended to call this function within initProperties() of you...
GPSInterface(DefaultDevice *defaultDevice)
void setState(IPState state)
void apply(const char *format,...) const ATTRIBUTE_FORMAT_PRINTF(2
bool load()
load Attempt to load property values from configuration file.
void save(FILE *f) const
save Save property to configuration file.
bool isNameMatch(const char *otherName) const
bool update(const double values[], const char *const names[], int n)
void fill(const char *device, const char *name, const char *label, const char *group, IPerm permission, double timeout, IPState state)
bool update(const ISState states[], const char *const names[], int n)
void fill(const char *device, const char *name, const char *label, const char *group, IPerm permission, ISRule rule, double timeout, IPState state)
void fill(const char *device, const char *name, const char *label, const char *group, IPerm permission, double timeout, IPState state)
void setSingleShot(bool singleShot)
Set whether the timer is a single-shot timer.
void callOnTimeout(const std::function< void()> &callback)
Definition inditimer.cpp:76
void start()
Starts or restarts the timer with the timeout specified in interval.
Definition inditimer.cpp:82
bool isActive() const
Returns true if the timer is running (pending); otherwise returns false.
void setInterval(int msec)
Set the timeout interval in milliseconds.
void stop()
Stops the timer.
Definition inditimer.cpp:97
int errno
ISState
Switch state.
Definition indiapi.h:148
@ ISS_OFF
Definition indiapi.h:149
@ ISS_ON
Definition indiapi.h:150
@ IP_RW
Definition indiapi.h:184
@ IP_RO
Definition indiapi.h:182
IPState
Property state.
Definition indiapi.h:158
@ IPS_BUSY
Definition indiapi.h:161
@ IPS_ALERT
Definition indiapi.h:162
@ IPS_IDLE
Definition indiapi.h:159
@ IPS_OK
Definition indiapi.h:160
@ ISR_1OFMANY
Definition indiapi.h:171
@ ISR_ATMOST1
Definition indiapi.h:172
#define INDI_UNUSED(x)
Definition indidevapi.h:132
#define DEBUGDEVICE(device, priority, msg)
Definition indilogger.h:60
#define DEBUGFDEVICE(device, priority, msg,...)
Definition indilogger.h:61
std::vector< uint8_t > buffer
Namespace to encapsulate INDI client, drivers, and mediator classes.