44constexpr const char *kDevDirectory =
"/dev";
45constexpr const char *kByIdDirectory =
"/dev/serial/by-id";
46constexpr const char *kByPathDirectory =
"/dev/serial/by-path";
55bool startsWith(
const std::string &value,
const std::string &prefix)
57 return value.size() >= prefix.size() && value.compare(0, prefix.size(), prefix) == 0;
60bool containsPathComponent(
const std::string &value,
const std::string &component)
62 return value.find(component) != std::string::npos;
65std::string baseName(
const std::string &path)
67 const std::string::size_type pos = path.find_last_of(
'/');
68 if (pos == std::string::npos)
70 return path.substr(pos + 1);
73bool isLikelySerialTty(
const std::string &path)
75 const std::string name = baseName(path);
76 return startsWith(name,
"ttyUSB") || startsWith(name,
"ttyACM");
79std::vector<std::string> listDirectory(
const std::string &directory)
81 std::vector<std::string> entries;
82 DIR *dir = ::opendir(directory.c_str());
86 while (dirent *entry = ::readdir(dir))
88 const std::string name = entry->d_name;
89 if (name ==
"." || name ==
"..")
92 if (directory.empty() || directory ==
"/")
93 fullPath =
"/" + name;
94 else if (directory.back() ==
'/')
95 fullPath = directory + name;
97 fullPath = directory +
"/" + name;
98 entries.push_back(fullPath);
101 std::sort(entries.begin(), entries.end());
105std::string canonicalPath(
const std::string &path)
107 char resolved[PATH_MAX] = {0};
108 if (::realpath(path.c_str(), resolved) !=
nullptr)
113int preferredPathRank(
const std::string &path)
115 if (containsPathComponent(path,
"/serial/by-id/"))
117 if (containsPathComponent(path,
"/serial/by-path/"))
119 if (isLikelySerialTty(path))
124bool preferPath(
const std::string &candidate,
const std::string ¤t)
126 const int candidateRank = preferredPathRank(candidate);
127 const int currentRank = preferredPathRank(current);
128 if (candidateRank != currentRank)
129 return candidateRank < currentRank;
130 return candidate < current;
133std::vector<std::string> enumerateSerialCandidatesFromDirectories(
const std::string &devDirectory,
134 const std::string &byIdDirectory,
135 const std::string &byPathDirectory)
137 std::vector<std::string> candidates;
139 for (
const auto &path : listDirectory(devDirectory))
141 if (isLikelySerialTty(path))
142 candidates.push_back(path);
145 auto addSerialLinks = [&](
const std::string & directory)
147 for (
const auto &path : listDirectory(directory))
149 const std::string
canonical = canonicalPath(path);
151 candidates.push_back(path);
154 addSerialLinks(byIdDirectory);
155 addSerialLinks(byPathDirectory);
160std::vector<Candidate> canonicalizeCandidates(
const std::vector<std::string> &ports)
162 std::vector<Candidate> candidates;
163 for (
const std::string &port : ports)
168 const std::string
canonical = canonicalPath(port);
169 auto existing = std::find_if(candidates.begin(), candidates.end(), [&](
const Candidate & candidate)
171 return candidate.canonical == canonical;
174 if (existing == candidates.end())
176 candidates.push_back(Candidate{port,
canonical, port});
178 else if (preferPath(port, existing->preferred))
180 existing->preferred = port;
184 for (Candidate &candidate : candidates)
186 if (preferPath(candidate.canonical, candidate.preferred))
187 candidate.preferred = candidate.canonical;
193bool cancelled(
const DiscoveryOptions &options)
195 return options.shouldCancel && options.shouldCancel();
198bool sleepWithCancellation(std::chrono::milliseconds delay,
const DiscoveryOptions &options)
200 auto remaining = delay;
201 while (remaining.count() > 0)
203 if (cancelled(options))
205 const auto slice = std::min(remaining, std::chrono::milliseconds(50));
206 std::this_thread::sleep_for(slice);
209 return !cancelled(options);
212ProbeResult makeResult(
const Candidate &candidate,
ProbeStatus status,
const std::string &reason = std::string())
215 result.status = status;
216 result.requestedPort = candidate.requested;
217 result.canonicalPort = candidate.canonical;
218 result.preferredPort = candidate.preferred;
219 result.failureReason = reason;
257ProbeResult probeCandidate(
const Candidate &candidate,
const DiscoveryOptions &options)
259 if (cancelled(options))
263 GeminiProtocol protocol;
264 protocol.setCancelFunction(options.shouldCancel);
266 if (!protocol.open(candidate.preferred, 0,
true))
268 const int openErrno = protocol.lastOpenErrno();
269 const std::string reason = openErrno != 0 ? std::strerror(openErrno) :
"open failed";
271 result.failureReason = reason;
275 if (!sleepWithCancellation(options.settleDelay, options))
279 result.failureReason =
"cancelled after settle delay";
286 static_cast<int>(options.identityTimeout.count()),
288 if (cancelled(options))
292 result.failureReason =
"cancelled during identity query";
298 result.status = identityStatusFromQuery(identityStatus);
299 result.failureReason = detail;
304 if (cancelled(options))
308 result.failureReason =
"cancelled before firmware query";
312 int firmwareNumeric = 0;
316 static_cast<int>(options.firmwareTimeout.count()),
318 if (cancelled(options))
322 result.failureReason =
"cancelled during firmware query";
327 result.status = firmwareStatusFromQuery(firmwareStatus);
330 result.firmware.numeric = firmwareNumeric;
334 result.failureReason = detail;
342 return enumerateSerialCandidatesFromDirectories(kDevDirectory, kByIdDirectory, kByPathDirectory);
347 std::vector<Candidate> candidates = canonicalizeCandidates({port});
348 if (candidates.empty())
350 Candidate candidate{port, port, port};
353 return probeCandidate(candidates.front(), options);
361 const std::vector<Candidate> candidates = canonicalizeCandidates(ports);
363 for (
const Candidate &candidate : candidates)
365 if (cancelled(options))
372 ProbeResult probe = probeCandidate(candidate, options);
376 result.
devices.push_back(probe);
377 result.
probes.push_back(std::move(probe));
397 return "IdentityTimeout";
399 return "IdentityMismatch";
401 return "IdentityMatched";
403 return "FirmwareTimeout";
405 return "FirmwareMalformed";
407 return "ProtocolError";
417std::vector<std::string> enumerateSerialCandidatesForTesting(
const std::string &devDirectory,
418 const std::string &byIdDirectory,
419 const std::string &byPathDirectory)
421 return enumerateSerialCandidatesFromDirectories(devDirectory, byIdDirectory, byPathDirectory);
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
std::vector< ProbeResult > probes
std::vector< ProbeResult > devices