Instrument Neutral Distributed Interface INDI 2.2.5
safetymonitor.cpp
Go to the documentation of this file.
1/*******************************************************************************
2 Copyright(c) 2025 Jasem Mutlaq. All rights reserved.
3
4 INDI Safety Monitor Driver
5 Monitors multiple INDI devices' SAFETY_STATUS properties and reports the worst case
6
7 This program is free software; you can redistribute it and/or modify it
8 under the terms of the GNU General Public License as published by the Free
9 Software Foundation; either version 2 of the License, or (at your option)
10 any later version.
11
12 This program is distributed in the hope that it will be useful, but WITHOUT
13 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
14 FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
15 more details.
16
17 You should have received a copy of the GNU Library General Public License
18 along with this library; see the file COPYING.LIB. If not, write to
19 the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
20 Boston, MA 02110-1301, USA.
21
22 The full GNU General Public License is included in this distribution in the
23 file called LICENSE.
24*******************************************************************************/
25
26#include "safetymonitor.h"
27
28#include <memory>
29#include <regex>
30#include <sstream>
31
32// We declare an auto pointer to SafetyMonitor
33std::unique_ptr<SafetyMonitor> safetyMonitor(new SafetyMonitor());
34
39
41{
42 return "Safety Monitor";
43}
44
46{
48
49 // Connection strings
50 ConnectionStringsTP[0].fill("DEVICES", "Devices", "");
51 ConnectionStringsTP.fill(getDeviceName(), "CONNECTION_STRINGS", "Safety Devices",
53 ConnectionStringsTP.load();
54
55 // Start with empty safety status property
56 SafetyStatusLP.fill(getDeviceName(), "SAFETY_STATUS", "Status",
58
59 // Override switch
60 OverrideSP[0].fill("OVERRIDE", "Override Status", ISS_OFF);
61 OverrideSP.fill(getDeviceName(), "SAFETY_OVERRIDE", "Safety",
63
66
67 return true;
68}
69
70void SafetyMonitor::ISGetProperties(const char *dev)
71{
73
74 defineProperty(ConnectionStringsTP);
75}
76
78{
80
81 if (isConnected())
82 {
83 if (SafetyStatusLP.count() > 0)
84 defineProperty(SafetyStatusLP);
85 defineProperty(OverrideSP);
86 }
87 else
88 {
89 if (SafetyStatusLP.count() > 0)
90 deleteProperty(SafetyStatusLP);
91 deleteProperty(OverrideSP);
92 }
93
94 return true;
95}
96
98{
99 // Parse connection strings and build property
100 rebuildSafetyStatusProperty();
101 return true;
102}
103
105{
106 // Disconnect all clients
107 for (auto &client : m_Clients)
108 {
109 client->disconnectServer();
110 }
111 return true;
112}
113
114bool SafetyMonitor::ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n)
115{
116 if (dev != nullptr && strcmp(dev, getDeviceName()) == 0)
117 {
118 if (ConnectionStringsTP.isNameMatch(name))
119 {
120 ConnectionStringsTP.update(texts, names, n);
121 ConnectionStringsTP.setState(IPS_OK);
122 ConnectionStringsTP.apply();
123
124 // Rebuild everything when connection strings change
125 rebuildSafetyStatusProperty();
126 saveConfig(ConnectionStringsTP);
127 return true;
128 }
129 }
130
131 return INDI::DefaultDevice::ISNewText(dev, name, texts, names, n);
132}
133
134bool SafetyMonitor::ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n)
135{
136 if (dev != nullptr && strcmp(dev, getDeviceName()) == 0)
137 {
138 if (OverrideSP.isNameMatch(name))
139 {
140 OverrideSP.update(states, names, n);
141
142 if (OverrideSP[0].getState() == ISS_ON)
143 {
144 LOG_WARN("Safety override is enabled. Observatory safety is overridden. Turn off as soon as possible.");
145 OverrideSP.setState(IPS_BUSY);
146 }
147 else
148 {
149 LOG_INFO("Safety override is disabled");
150 OverrideSP.setState(IPS_IDLE);
151 }
152
153 OverrideSP.apply();
154
155 // Update overall status considering the override
156 updateOverallStatus();
157
158 return true;
159 }
160 }
161
162 return INDI::DefaultDevice::ISNewSwitch(dev, name, states, names, n);
163}
164
166{
168
169 ConnectionStringsTP.save(fp);
170
171 return true;
172}
173
174void SafetyMonitor::parseConnectionStrings()
175{
176 std::string connStr = ConnectionStringsTP[0].getText();
177
178 if (connStr.empty())
179 {
180 LOG_WARN("No connection strings configured");
181 return;
182 }
183
184 // Split by comma to get individual device entries
185 // Format: DeviceName@host:port or just DeviceName (use localhost:7624)
186 // Device names can contain spaces, @ separates name from connection
187 // Multiple devices separated by commas
188 // Examples:
189 // "Open Weather Map, UPS@localhost:7624"
190 // "Weather [email protected], Power [email protected]:7624"
191
192 std::istringstream iss(connStr);
193 std::string deviceEntry;
194
195 while (std::getline(iss, deviceEntry, ','))
196 {
197 // Trim leading/trailing whitespace
198 size_t start = deviceEntry.find_first_not_of(" \t\n\r");
199 size_t end = deviceEntry.find_last_not_of(" \t\n\r");
200
201 if (start == std::string::npos)
202 continue; // Empty entry
203
204 deviceEntry = deviceEntry.substr(start, end - start + 1);
205
206 if (deviceEntry.empty())
207 continue;
208
209 // Parse: DeviceName@host:port
210 // The @ symbol separates device name from connection info
211 std::string deviceName, host;
212 int port = 7624; // Default INDI port
213
214 size_t atPos = deviceEntry.find('@');
215 if (atPos != std::string::npos)
216 {
217 // Has explicit host
218 deviceName = deviceEntry.substr(0, atPos);
219 std::string hostPort = deviceEntry.substr(atPos + 1);
220
221 // Trim trailing space from device name
222 size_t nameEnd = deviceName.find_last_not_of(" \t");
223 if (nameEnd != std::string::npos)
224 deviceName = deviceName.substr(0, nameEnd + 1);
225
226 // Parse host:port
227 size_t colonPos = hostPort.find(':');
228 if (colonPos != std::string::npos)
229 {
230 // Has explicit port
231 host = hostPort.substr(0, colonPos);
232 try {
233 port = std::stoi(hostPort.substr(colonPos + 1));
234 } catch (...) {
235 LOGF_ERROR("Invalid port in '%s', using default 7624", deviceEntry.c_str());
236 }
237 }
238 else
239 {
240 // Only host, no port
241 host = hostPort;
242 }
243
244 // Trim host
245 size_t hostStart = host.find_first_not_of(" \t");
246 size_t hostEnd = host.find_last_not_of(" \t");
247 if (hostStart != std::string::npos)
248 host = host.substr(hostStart, hostEnd - hostStart + 1);
249 }
250 else
251 {
252 // No @ symbol, just device name (use localhost)
253 deviceName = deviceEntry;
254 host = "localhost";
255 }
256
257 if (deviceName.empty())
258 {
259 LOGF_WARN("Empty device name in entry: %s", deviceEntry.c_str());
260 continue;
261 }
262
263 LOGF_INFO("Configuring connection to '%s'@%s:%d",
264 deviceName.c_str(), host.c_str(), port);
265
266 // Create client with callback to update overall status
267 auto client = std::make_unique<SafetyMonitorClient>(
268 deviceName,
269 [this]() { this->updateOverallStatus(); }
270 );
271
272 if (client->connectToServer(host, port))
273 {
274 m_Clients.push_back(std::move(client));
275 }
276 else
277 {
278 LOGF_ERROR("Failed to create client for %s", deviceName.c_str());
279 }
280 }
281
282 if (m_Clients.empty())
283 {
284 LOG_WARN("No valid device connections configured");
285 }
286}
287
288void SafetyMonitor::rebuildSafetyStatusProperty()
289{
290 // Delete existing property if defined
291 if (SafetyStatusLP.count() > 0)
292 {
293 deleteProperty(SafetyStatusLP);
294 }
295
296 // Disconnect and clear all existing clients
297 for (auto &client : m_Clients)
298 {
299 client->disconnectServer();
300 }
301 m_Clients.clear();
302
303 // Parse connection strings and create new clients
304 parseConnectionStrings();
305
306 SafetyStatusLP.resize(0);
307
308 // Build SafetyStatusLP with one element per device
309 for (auto &client : m_Clients)
310 {
311 INDI::WidgetLight oneLight;
312 oneLight.fill(client->getMonitoredDeviceName().c_str(),
313 client->getMonitoredDeviceName().c_str(),
314 IPS_IDLE);
315 SafetyStatusLP.push(std::move(oneLight));
316 }
317
318 // Define the new property if connected and has elements
319 if (isConnected() && SafetyStatusLP.count() > 0)
320 {
321 defineProperty(SafetyStatusLP);
322
323 // Initial status update
324 updateOverallStatus();
325 }
326}
327
328void SafetyMonitor::updateOverallStatus()
329{
330 if (SafetyStatusLP.count() == 0)
331 return;
332
333 // Start with IDLE as the base state
334 IPState worstState = IPS_IDLE;
335
336 // Update each device's status and find worst case
337 for (size_t i = 0; i < m_Clients.size() && i < SafetyStatusLP.count(); i++)
338 {
339 auto &client = m_Clients[i];
340 auto &light = SafetyStatusLP[i];
341
342 if (client->isDeviceOnline() && client->hasSafetyStatus())
343 {
344 IPState deviceState = client->getSafetyStatus();
345 light.setState(deviceState);
346
347 // Track worst state (higher enum value = worse)
348 if (deviceState > worstState)
349 worstState = deviceState;
350 }
351 else
352 {
353 // Device offline or no status yet
354 light.setState(IPS_IDLE);
355 }
356 }
357
358 // Apply override if enabled
359 if (OverrideSP[0].getState() == ISS_ON)
360 {
361 worstState = IPS_OK;
362 LOGF_DEBUG("Safety override active, forcing status to OK (actual worst: %d)", worstState);
363 }
364
365 // Set overall property state to worst case
366 SafetyStatusLP.setState(worstState);
367 SafetyStatusLP.apply();
368
369 LOGF_DEBUG("Overall safety status updated: %d", worstState);
370}
bool isConnected() const
const char * getDeviceName() const
virtual bool updateProperties()
updateProperties is called whenever there is a change in the CONNECTION status of the driver....
virtual bool saveConfig(bool silent=false, const char *property=nullptr)
Save the current properties in a configuration file.
virtual bool ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n)
Process the client newSwitch command.
virtual void ISGetProperties(const char *dev)
define the driver's properties to the client. Usually, only a minimum set of properties are defined t...
void setDriverInterface(uint32_t value)
setInterface Set driver interface. By default the driver interface is set to GENERAL_DEVICE....
void setVersion(uint16_t vMajor, uint16_t vMinor)
Set driver version information to be defined in DRIVER_INFO property as vMajor.vMinor.
virtual bool deleteProperty(const char *propertyName)
Delete a property and unregister it. It will also be deleted from all clients.
void defineProperty(INumberVectorProperty *property)
virtual bool saveConfigItems(FILE *fp)
saveConfigItems Save specific properties in the provide config file handler. Child class usually over...
virtual bool initProperties()
Initialize properties initial state and value. The child class must implement this function.
void addDebugControl()
Add Debug control to the driver.
virtual bool ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n)
Process the client newSwitch command.
void push(WidgetView< T > &&item)
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.
void resize(size_t size)
bool isNameMatch(const char *otherName) const
void fill(const char *device, const char *name, const char *label, const char *group, 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)
bool update(const char *const texts[], 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)
INDI::PropertyViewText * getText() const
virtual bool updateProperties() override
updateProperties is called whenever there is a change in the CONNECTION status of the driver....
const char * getDefaultName() override
virtual bool saveConfigItems(FILE *fp) override
saveConfigItems Save specific properties in the provide config file handler. Child class usually over...
virtual bool Disconnect() override
Disconnect from device.
virtual bool Connect() override
Connect to the device. INDI::DefaultDevice implementation connects to appropriate connection interfac...
virtual bool ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n) override
Process the client newSwitch command.
virtual void ISGetProperties(const char *dev) override
define the driver's properties to the client. Usually, only a minimum set of properties are defined t...
virtual bool ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) override
Process the client newSwitch command.
virtual bool initProperties() override
Initialize properties initial state and value. The child class must implement this function.
const char * MAIN_CONTROL_TAB
MAIN_CONTROL_TAB Where all the primary controls for the device are located.
const char * OPTIONS_TAB
OPTIONS_TAB Where all the driver's options are located. Those may include auxiliary controls,...
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
IPState
Property state.
Definition indiapi.h:158
@ IPS_BUSY
Definition indiapi.h:161
@ IPS_IDLE
Definition indiapi.h:159
@ IPS_OK
Definition indiapi.h:160
@ ISR_NOFMANY
Definition indiapi.h:173
#define LOGF_INFO(fmt,...)
Definition indilogger.h:82
#define LOGF_WARN(fmt,...)
Definition indilogger.h:81
#define LOG_WARN(txt)
Definition indilogger.h:73
#define LOGF_DEBUG(fmt,...)
Definition indilogger.h:83
#define LOGF_ERROR(fmt,...)
Definition indilogger.h:80
#define LOG_INFO(txt)
Definition indilogger.h:74
std::unique_ptr< SafetyMonitor > safetyMonitor(new SafetyMonitor())
void fill(const char *name, const char *label, IPState state=IPS_OK)