Instrument Neutral Distributed Interface INDI 2.2.5
discovery.cpp
Go to the documentation of this file.
1/*
2 Gemini Power Box Hub Advanced v3 INDI Driver
3
4 Copyright (C) 2026 Dieter R Kedrowitsch <[email protected]>
5
6 This program is free software; you can redistribute it and/or modify it
7 under the terms of the GNU General Public License as published by the Free
8 Software Foundation; either version 2 of the License, or (at your option)
9 any later version.
10
11 This program is distributed in the hope that it will be useful, but WITHOUT
12 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
13 FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
14 more details.
15
16 You should have received a copy of the GNU General Public License along with
17 this program; if not, write to the Free Software Foundation, Inc., 59 Temple
18 Place - Suite 330, Boston, MA 02111-1307, USA.
19
20 The full GNU General Public License is included in this distribution in the
21 file called LICENSE.
22*/
23
25
27
28#include <algorithm>
29#include <cerrno>
30#include <cstring>
31#include <dirent.h>
32#include <limits.h>
33#include <string>
34#include <thread>
35#include <utility>
36#include <unistd.h>
37#include <vector>
38
39namespace geminipbh
40{
41
42namespace
43{
44constexpr const char *kDevDirectory = "/dev";
45constexpr const char *kByIdDirectory = "/dev/serial/by-id";
46constexpr const char *kByPathDirectory = "/dev/serial/by-path";
47
48struct Candidate
49{
50 std::string requested;
51 std::string canonical;
52 std::string preferred;
53};
54
55bool startsWith(const std::string &value, const std::string &prefix)
56{
57 return value.size() >= prefix.size() && value.compare(0, prefix.size(), prefix) == 0;
58}
59
60bool containsPathComponent(const std::string &value, const std::string &component)
61{
62 return value.find(component) != std::string::npos;
63}
64
65std::string baseName(const std::string &path)
66{
67 const std::string::size_type pos = path.find_last_of('/');
68 if (pos == std::string::npos)
69 return path;
70 return path.substr(pos + 1);
71}
72
73bool isLikelySerialTty(const std::string &path)
74{
75 const std::string name = baseName(path);
76 return startsWith(name, "ttyUSB") || startsWith(name, "ttyACM");
77}
78
79std::vector<std::string> listDirectory(const std::string &directory)
80{
81 std::vector<std::string> entries;
82 DIR *dir = ::opendir(directory.c_str());
83 if (!dir)
84 return entries;
85
86 while (dirent *entry = ::readdir(dir))
87 {
88 const std::string name = entry->d_name;
89 if (name == "." || name == "..")
90 continue;
91 std::string fullPath;
92 if (directory.empty() || directory == "/")
93 fullPath = "/" + name;
94 else if (directory.back() == '/')
95 fullPath = directory + name;
96 else
97 fullPath = directory + "/" + name;
98 entries.push_back(fullPath);
99 }
100 ::closedir(dir);
101 std::sort(entries.begin(), entries.end());
102 return entries;
103}
104
105std::string canonicalPath(const std::string &path)
106{
107 char resolved[PATH_MAX] = {0};
108 if (::realpath(path.c_str(), resolved) != nullptr)
109 return resolved;
110 return path;
111}
112
113int preferredPathRank(const std::string &path)
114{
115 if (containsPathComponent(path, "/serial/by-id/"))
116 return 0;
117 if (containsPathComponent(path, "/serial/by-path/"))
118 return 1;
119 if (isLikelySerialTty(path))
120 return 2;
121 return 3;
122}
123
124bool preferPath(const std::string &candidate, const std::string &current)
125{
126 const int candidateRank = preferredPathRank(candidate);
127 const int currentRank = preferredPathRank(current);
128 if (candidateRank != currentRank)
129 return candidateRank < currentRank;
130 return candidate < current;
131}
132
133std::vector<std::string> enumerateSerialCandidatesFromDirectories(const std::string &devDirectory,
134 const std::string &byIdDirectory,
135 const std::string &byPathDirectory)
136{
137 std::vector<std::string> candidates;
138
139 for (const auto &path : listDirectory(devDirectory))
140 {
141 if (isLikelySerialTty(path))
142 candidates.push_back(path);
143 }
144
145 auto addSerialLinks = [&](const std::string & directory)
146 {
147 for (const auto &path : listDirectory(directory))
148 {
149 const std::string canonical = canonicalPath(path);
150 if (isLikelySerialTty(canonical))
151 candidates.push_back(path);
152 }
153 };
154 addSerialLinks(byIdDirectory);
155 addSerialLinks(byPathDirectory);
156
157 return candidates;
158}
159
160std::vector<Candidate> canonicalizeCandidates(const std::vector<std::string> &ports)
161{
162 std::vector<Candidate> candidates;
163 for (const std::string &port : ports)
164 {
165 if (port.empty())
166 continue;
167
168 const std::string canonical = canonicalPath(port);
169 auto existing = std::find_if(candidates.begin(), candidates.end(), [&](const Candidate & candidate)
170 {
171 return candidate.canonical == canonical;
172 });
173
174 if (existing == candidates.end())
175 {
176 candidates.push_back(Candidate{port, canonical, port});
177 }
178 else if (preferPath(port, existing->preferred))
179 {
180 existing->preferred = port;
181 }
182 }
183
184 for (Candidate &candidate : candidates)
185 {
186 if (preferPath(candidate.canonical, candidate.preferred))
187 candidate.preferred = candidate.canonical;
188 }
189
190 return candidates;
191}
192
193bool cancelled(const DiscoveryOptions &options)
194{
195 return options.shouldCancel && options.shouldCancel();
196}
197
198bool sleepWithCancellation(std::chrono::milliseconds delay, const DiscoveryOptions &options)
199{
200 auto remaining = delay;
201 while (remaining.count() > 0)
202 {
203 if (cancelled(options))
204 return false;
205 const auto slice = std::min(remaining, std::chrono::milliseconds(50));
206 std::this_thread::sleep_for(slice);
207 remaining -= slice;
208 }
209 return !cancelled(options);
210}
211
212ProbeResult makeResult(const Candidate &candidate, ProbeStatus status, const std::string &reason = std::string())
213{
214 ProbeResult result;
215 result.status = status;
216 result.requestedPort = candidate.requested;
217 result.canonicalPort = candidate.canonical;
218 result.preferredPort = candidate.preferred;
219 result.failureReason = reason;
220 return result;
221}
222
223ProbeStatus identityStatusFromQuery(GeminiProtocol::DirectQueryStatus status)
224{
225 switch (status)
226 {
236 }
238}
239
240ProbeStatus firmwareStatusFromQuery(GeminiProtocol::DirectQueryStatus status)
241{
242 switch (status)
243 {
253 }
255}
256
257ProbeResult probeCandidate(const Candidate &candidate, const DiscoveryOptions &options)
258{
259 if (cancelled(options))
260 return makeResult(candidate, ProbeStatus::Cancelled, "cancelled before opening port");
261
262 ProbeResult result = makeResult(candidate, ProbeStatus::NotTried);
263 GeminiProtocol protocol;
264 protocol.setCancelFunction(options.shouldCancel);
265
266 if (!protocol.open(candidate.preferred, 0, true))
267 {
268 const int openErrno = protocol.lastOpenErrno();
269 const std::string reason = openErrno != 0 ? std::strerror(openErrno) : "open failed";
270 result.status = protocol.lastOpenWasBusy() ? ProbeStatus::PortBusy : ProbeStatus::OpenFailed;
271 result.failureReason = reason;
272 return result;
273 }
274
275 if (!sleepWithCancellation(options.settleDelay, options))
276 {
277 protocol.close();
278 result.status = ProbeStatus::Cancelled;
279 result.failureReason = "cancelled after settle delay";
280 return result;
281 }
282
283 std::string detail;
284 GeminiProtocol::DirectQueryStatus identityStatus = protocol.queryIdentity(
285 result.identity,
286 static_cast<int>(options.identityTimeout.count()),
287 &detail);
288 if (cancelled(options))
289 {
290 protocol.close();
291 result.status = ProbeStatus::Cancelled;
292 result.failureReason = "cancelled during identity query";
293 return result;
294 }
295 if (identityStatus != GeminiProtocol::DirectQueryStatus::Success)
296 {
297 protocol.close();
298 result.status = identityStatusFromQuery(identityStatus);
299 result.failureReason = detail;
300 return result;
301 }
302
303 result.status = ProbeStatus::IdentityMatched;
304 if (cancelled(options))
305 {
306 protocol.close();
307 result.status = ProbeStatus::Cancelled;
308 result.failureReason = "cancelled before firmware query";
309 return result;
310 }
311
312 int firmwareNumeric = 0;
313 GeminiProtocol::DirectQueryStatus firmwareStatus = protocol.queryFirmware(
314 result.firmware.raw,
315 firmwareNumeric,
316 static_cast<int>(options.firmwareTimeout.count()),
317 &detail);
318 if (cancelled(options))
319 {
320 protocol.close();
321 result.status = ProbeStatus::Cancelled;
322 result.failureReason = "cancelled during firmware query";
323 return result;
324 }
325
326 protocol.close();
327 result.status = firmwareStatusFromQuery(firmwareStatus);
328 if (firmwareStatus == GeminiProtocol::DirectQueryStatus::Success)
329 {
330 result.firmware.numeric = firmwareNumeric;
331 }
332 else
333 {
334 result.failureReason = detail;
335 }
336 return result;
337}
338}
339
340std::vector<std::string> enumerateSerialCandidates()
341{
342 return enumerateSerialCandidatesFromDirectories(kDevDirectory, kByIdDirectory, kByPathDirectory);
343}
344
345ProbeResult probeGeminiDevice(const std::string &port, const DiscoveryOptions &options)
346{
347 std::vector<Candidate> candidates = canonicalizeCandidates({port});
348 if (candidates.empty())
349 {
350 Candidate candidate{port, port, port};
351 return makeResult(candidate, ProbeStatus::OpenFailed, "empty port");
352 }
353 return probeCandidate(candidates.front(), options);
354}
355
357{
358 DiscoveryResult result;
359 const std::vector<std::string> ports = options.candidatePorts.empty() ? enumerateSerialCandidates() :
360 options.candidatePorts;
361 const std::vector<Candidate> candidates = canonicalizeCandidates(ports);
362
363 for (const Candidate &candidate : candidates)
364 {
365 if (cancelled(options))
366 {
367 result.cancelled = true;
368 result.probes.push_back(makeResult(candidate, ProbeStatus::Cancelled, "cancelled before probing candidate"));
369 break;
370 }
371
372 ProbeResult probe = probeCandidate(candidate, options);
373 if (probe.status == ProbeStatus::Cancelled)
374 result.cancelled = true;
375 if (probe.status == ProbeStatus::Identified)
376 result.devices.push_back(probe);
377 result.probes.push_back(std::move(probe));
378
379 if (result.cancelled)
380 break;
381 }
382
383 return result;
384}
385
386const char *probeStatusName(ProbeStatus status)
387{
388 switch (status)
389 {
391 return "NotTried";
393 return "OpenFailed";
395 return "PortBusy";
397 return "IdentityTimeout";
399 return "IdentityMismatch";
401 return "IdentityMatched";
403 return "FirmwareTimeout";
405 return "FirmwareMalformed";
407 return "ProtocolError";
409 return "Identified";
411 return "Cancelled";
412 }
413 return "Unknown";
414}
415
416#ifdef GEMINI_TESTING
417std::vector<std::string> enumerateSerialCandidatesForTesting(const std::string &devDirectory,
418 const std::string &byIdDirectory,
419 const std::string &byPathDirectory)
420{
421 return enumerateSerialCandidatesFromDirectories(devDirectory, byIdDirectory, byPathDirectory);
422}
423#endif
424
425} // namespace geminipbh
std::string canonical
Definition discovery.cpp:51
std::string requested
Definition discovery.cpp:50
std::string preferred
Definition discovery.cpp:52
DiscoveryResult discoverGeminiDevices(const DiscoveryOptions &options)
ProbeResult probeGeminiDevice(const std::string &port, const DiscoveryOptions &options)
const char * probeStatusName(ProbeStatus status)
std::vector< std::string > enumerateSerialCandidates()
std::vector< std::string > candidatePorts
Definition discovery.h:72
std::vector< ProbeResult > probes
Definition discovery.h:81
std::vector< ProbeResult > devices
Definition discovery.h:82