42#include <sys/select.h>
58 std::condition_variable
cv;
72 std::condition_variable
cv;
87static bool isAsciiWhitespace(
char c)
89 return c ==
' ' || c ==
'\t' || c ==
'\r' || c ==
'\n' || c ==
'\f' || c ==
'\v';
94 return open(port, settleMs,
false);
101 lastOpenBusy_ =
false;
102 fd_ =
::open(port.c_str(), O_RDWR | O_NOCTTY);
105 lastOpenErrno_ =
errno;
106 lastOpenBusy_ =
errno == EBUSY;
110#if !defined(__CYGWIN__)
111 if (exclusive && ioctl(fd_, TIOCEXCL) != 0)
113 lastOpenErrno_ =
errno;
114 lastOpenBusy_ =
errno == EBUSY;
124 std::memset(&tio, 0,
sizeof(tio));
125 if (tcgetattr(fd_, &tio) != 0)
127 lastOpenErrno_ =
errno;
132 cfsetospeed(&tio, B19200);
133 cfsetispeed(&tio, B19200);
134 tio.c_cflag |= (CLOCAL | CREAD);
135 tio.c_cflag &= ~CSIZE;
137 tio.c_cflag &= ~PARENB;
138 tio.c_cflag &= ~CSTOPB;
139 tio.c_cflag &= ~CRTSCTS;
140 tio.c_lflag &= ~(ICANON | ECHO | ECHOE | ISIG);
141 tio.c_iflag &= ~(IXON | IXOFF | IXANY);
142 tio.c_iflag &= ~(INLCR | ICRNL | IGNCR);
143 tio.c_oflag &= ~OPOST;
146 tcflush(fd_, TCIOFLUSH);
147 if (tcsetattr(fd_, TCSANOW, &tio) != 0)
149 lastOpenErrno_ =
errno;
154 int flags = TIOCM_DTR | TIOCM_RTS;
155 if (ioctl(fd_, TIOCMSET, &flags) != 0)
166 lastOpenErrno_ =
errno;
169 const std::string message =
"failed to assert RTS/DTR on " + port +
": " + std::strerror(
errno);
182 std::this_thread::sleep_for(std::chrono::milliseconds(settleMs));
191 ::tcflush(fd_, TCIOFLUSH);
192 int flags = ::fcntl(fd_, F_GETFL, 0);
193 if (flags >= 0) ::fcntl(fd_, F_SETFL, flags | O_NONBLOCK);
205 return lastOpenErrno_;
210 return lastOpenBusy_;
220 return instrumentation_.
level();
225 return instrumentation_.
enabled(level);
239bool GeminiProtocol::openWakePipe()
241 if (wakeReadFd_ >= 0 && wakeWriteFd_ >= 0)
return true;
243 int fds[2] = {-1, -1};
244 if (::pipe(fds) != 0)
248 const std::string message = std::string(
"failed to create reader wake pipe: ") + std::strerror(
errno);
258 int flags = ::fcntl(
fd, F_GETFL, 0);
259 if (flags < 0 || ::fcntl(
fd, F_SETFL, flags | O_NONBLOCK) != 0)
263 const std::string message = std::string(
"failed to configure reader wake pipe: ") + std::strerror(
errno);
274 wakeReadFd_ = fds[0];
275 wakeWriteFd_ = fds[1];
279void GeminiProtocol::closeWakePipe()
281 if (wakeReadFd_ >= 0)
286 if (wakeWriteFd_ >= 0)
293void GeminiProtocol::signalReaderWake()
295 if (wakeWriteFd_ < 0)
return;
297 const char byte =
'x';
298 while (::write(wakeWriteFd_, &
byte, 1) < 0)
300 if (
errno == EINTR)
continue;
305void GeminiProtocol::drainReaderWake(
int wakeFd)
307 if (wakeFd < 0)
return;
310 while (::read(wakeFd, buf,
sizeof(buf)) > 0) {}
313void GeminiProtocol::emitInstrumentationEvent(instrumentation::Event event)
const
315 event.source =
"geminipbh.GeminiProtocol";
316 event.connectionGeneration = connectionGeneration_.load(std::memory_order_acquire);
317 instrumentation_.
emit(std::move(event));
322 const std::string &reason)
const
328 instrumentation::ConnectionPayload{state, reason}));
332 const std::string &frame)
const
336 if (frame.size() >= 2 && (frame[0] ==
'>' || frame[0] ==
'*')) token.assign(1, frame[1]);
341 instrumentation::FramePayload{direction, frame, token, true}));
346 const std::string &message)
const
352 instrumentation::WarningPayload{code, message}));
357 const std::string &detail,
358 const std::string &frame)
const
364 instrumentation::ParserAnomalyPayload{code, detail, frame}));
367void GeminiProtocol::emitReconnectInstrumentation(
unsigned attempt,
368 unsigned maxAttempts,
369 std::chrono::milliseconds delayBeforeAttempt,
387 instrumentation::ReconnectAttemptPayload{attempt, maxAttempts, delayBeforeAttempt, finalAttempt, result}));
391 std::chrono::steady_clock::time_point start,
395 const auto duration = std::chrono::duration_cast<std::chrono::microseconds>(
396 std::chrono::steady_clock::now() - start);
400 instrumentation::TimingPayload{operation, duration, success}));
404 const std::string &detail,
405 unsigned long long relatedSequence)
const
418 emitInstrumentationEvent(std::move(event));
421void GeminiProtocol::drainInputLocked()
425 struct timeval tv = {0, 0};
429 while (select(fd_ + 1, &set,
nullptr,
nullptr, &tv) > 0)
431 if (::read(fd_, buf,
sizeof(buf)) <= 0)
break;
437bool GeminiProtocol::writeAllLocked(
const std::string &command)
439 if (fd_ < 0)
return false;
440 std::string frame = command +
"\r\n";
443 while (written < frame.size())
445 ssize_t n = writeFn_(fd_, frame.data() + written, frame.size() - written);
448 if (
errno == EINTR)
continue;
451 const std::string message = std::string(
"serial write failed: ") + std::strerror(
errno);
462 "serial write returned 0 bytes");
465 written +=
static_cast<size_t>(n);
470bool GeminiProtocol::readFrameOnce(std::string &response,
int timeoutMs)
473 if (fd_ < 0)
return false;
474 auto deadline = std::chrono::steady_clock::now() +
475 std::chrono::milliseconds(timeoutMs);
477 bool seenNonWhitespace =
false;
478 while (std::chrono::steady_clock::now() < deadline && !cancelled())
480 struct timeval tv = {0, 50000};
484 int rc = select(fd_ + 1, &set,
nullptr,
nullptr, &tv);
487 if (
errno == EINTR)
continue;
490 if (rc == 0)
continue;
491 ssize_t n = ::read(fd_, &buf, 1);
492 if (n <= 0)
continue;
496 return !response.empty();
498 if (!seenNonWhitespace)
500 if (isAsciiWhitespace(buf))
continue;
501 seenNonWhitespace =
true;
508bool GeminiProtocol::readTokenFrameOnce(
char token, std::string &response,
int timeoutMs)
510 auto deadline = std::chrono::steady_clock::now() + std::chrono::milliseconds(timeoutMs);
511 while (std::chrono::steady_clock::now() < deadline && !cancelled())
513 int remaining = (int)std::chrono::duration_cast<std::chrono::milliseconds>(
514 deadline - std::chrono::steady_clock::now()).count();
516 if (!readFrameOnce(frame, remaining > 0 ? remaining : 1)) return false;
517 if (frame.size() >= 2 && frame[0] ==
'*' && frame[1] == token)
522 if (frame.size() >= 2 && frame[0] ==
'*' && frame[1] ==
'G')
528 storeStatusFrame(parsed,
false);
535 const std::string detail = std::string(
"ignored malformed G while waiting for ") + token +
": " + err;
547 const std::string detail = std::string(
"ignored frame while waiting for ") + token;
557bool GeminiProtocol::parseFirmwareFrame(
const std::string &frame, std::string &raw,
int &numeric)
const
559 if (frame.size() < 3 || frame[0] !=
'*' || frame[1] !=
'V')
return false;
560 raw = frame.substr(2);
561 if (raw.empty())
return false;
565 numeric = std::stoi(raw, &pos);
566 if (pos != raw.size())
return false;
575bool GeminiProtocol::cancelled()
const
577 return cancelFn_ && cancelFn_();
580bool GeminiProtocol::sleepCancelable(
int milliseconds)
const
583 while (elapsed < milliseconds)
585 if (cancelled())
return false;
586 int slice = std::min(50, milliseconds - elapsed);
587 std::this_thread::sleep_for(std::chrono::milliseconds(slice));
593bool GeminiProtocol::transactOnce(
const std::string &command, std::string &response,
int timeoutMs)
595 std::lock_guard<std::mutex> wlock(writeMutex_);
597 if (!writeAllLocked(command))
return false;
598 return readFrameOnce(response, timeoutMs);
604 std::string &response,
608 if (detail) detail->clear();
610 std::lock_guard<std::mutex> wlock(writeMutex_);
612 if (!writeAllLocked(command))
614 if (detail) *detail =
"write failed";
618 auto deadline = std::chrono::steady_clock::now() + std::chrono::milliseconds(timeoutMs);
619 std::string firstUnexpected;
620 while (std::chrono::steady_clock::now() < deadline && !cancelled())
622 const int remaining =
static_cast<int>(std::chrono::duration_cast<std::chrono::milliseconds>(
623 deadline - std::chrono::steady_clock::now()).count());
625 if (!readFrameOnce(frame, remaining > 0 ? remaining : 1))
628 if (frame.size() >= 2 && frame[0] ==
'*' && frame[1] == token)
634 if (frame.size() >= 2 && frame[0] ==
'*' && frame[1] ==
'G')
639 storeStatusFrame(parsed,
false);
643 if (firstUnexpected.empty())
644 firstUnexpected = frame;
647 if (!firstUnexpected.empty())
649 if (detail) *detail = firstUnexpected;
653 if (detail) *detail =
"timeout";
662 DirectQueryStatus status = queryTokenFrame(
">H#",
'H', timeoutMs, frame, detail);
666 if (frame.size() < 3)
668 if (detail) *detail = frame;
672 identity = frame.substr(2);
673 if (identity.empty())
675 if (detail) *detail = frame;
687 DirectQueryStatus status = queryTokenFrame(
">V#",
'V', timeoutMs, frame, detail);
691 if (!parseFirmwareFrame(frame, raw, numeric))
693 if (frame.size() >= 2 && frame[0] ==
'*' && frame[1] ==
'V')
694 raw = frame.substr(2);
695 if (detail) *detail = frame;
704 std::string identity;
706 identity ==
"GeminiPowerBoxPlusAdv3";
716 std::lock_guard<std::mutex> lock(firmwareMutex_);
717 if (!haveFirmwareVersion_)
return false;
718 raw = firmwareVersionRaw_;
719 numeric = firmwareVersionNumeric_;
723static bool parseBit(
const std::string &s,
size_t idx)
725 return idx < s.size() && s[idx] ==
'1';
728static bool validateBits(
const std::string &s,
const std::string &name, std::string &error)
732 if (c !=
'0' && c !=
'1')
734 error = name +
" field contains non-binary char";
741static bool parseDbl(
const std::string &s,
double &v,
const std::string &name, std::string &error)
745 error = name +
" field empty";
748 std::istringstream iss(s);
749 iss.imbue(std::locale::classic());
751 if (iss.fail() || !iss.eof())
753 error = name +
" field not numeric: '" + s +
"'";
756 if (!std::isfinite(v))
758 error = name +
" field is not finite";
764static bool parseBool01(
const std::string &s,
bool &v,
const std::string &name, std::string &error)
776 error = name +
" field must be 0 or 1";
780static bool parseMode(
const std::string &s,
int &mode,
const std::string &name, std::string &error)
785 mode = std::stoi(s, &p);
788 error = name +
" field has trailing garbage";
794 error = name +
" field not integer";
797 if (mode < 0 || mode > 2)
799 error = name +
" field must be 0, 1, or 2";
805static bool parseRange(
const std::string &s,
double &v,
double lo,
double hi,
806 const std::string &name, std::string &error)
808 if (!parseDbl(s, v, name, error))
return false;
809 if (v < lo || v > hi)
811 error = name +
" field out of range [" +
812 std::to_string(lo) +
", " + std::to_string(hi) +
"]";
820 static const char terminators[17] =
822 'D',
'U',
'A',
'T',
'A',
'M',
'B',
'M',
'C',
'C',
'S',
'T',
'H',
'D',
'V',
'C',
'P'
824 std::string fields[17];
826 for (
int i = 0; i < 17; ++i)
828 size_t mpos = payload.find(terminators[i], pos);
829 if (mpos == std::string::npos)
831 error = std::string(
"missing terminator ") + terminators[i];
834 fields[i] = payload.substr(pos, mpos - pos);
837 if (pos != payload.size())
839 error =
"trailing data after final terminator P";
843 if (fields[0].size() != 4)
845 error =
"DC field length != 4";
848 if (fields[1].size() != 6)
850 error =
"USB field length != 6";
853 if (!validateBits(fields[0],
"DC", error))
return false;
854 if (!validateBits(fields[1],
"USB", error))
return false;
855 if (!parseBool01(fields[2], status.
aht20Attached,
"AHT20", error))
return false;
856 if (!parseBool01(fields[3], status.
ds18b20Attached,
"DS18B20", error))
return false;
857 if (!parseBool01(fields[4], status.
dew6Enabled,
"dew6Enabled", error))
return false;
858 if (!parseMode(fields[5], status.
dew6Mode,
"dew6Mode", error))
return false;
859 if (!parseBool01(fields[6], status.
dew7Enabled,
"dew7Enabled", error))
return false;
860 if (!parseMode(fields[7], status.
dew7Mode,
"dew7Mode", error))
return false;
861 if (!parseRange(fields[8], status.
dew6OutputPercent, 0.0, 100.0,
"dew6OutputPercent", error))
return false;
862 if (!parseRange(fields[9], status.
dew7OutputPercent, 0.0, 100.0,
"dew7OutputPercent", error))
return false;
863 if (!parseDbl(fields[10], status.
deviceTemperatureC,
"deviceTemperatureC", error))
return false;
864 if (!parseDbl(fields[11], status.
airTemperatureC,
"airTemperatureC", error))
return false;
865 if (!parseRange(fields[12], status.
humidityPercent, 0.0, 100.0,
"humidityPercent", error))
return false;
866 if (!parseDbl(fields[13], status.
dewPointC,
"dewPointC", error))
return false;
867 if (!parseDbl(fields[14], status.
inputVoltageV,
"inputVoltageV", error))
return false;
868 if (!parseDbl(fields[15], status.
outputCurrentA,
"outputCurrentA", error))
return false;
869 if (!parseDbl(fields[16], status.
outputPowerW,
"outputPowerW", error))
return false;
871 status.
dc = { parseBit(fields[0], 0), parseBit(fields[0], 1), parseBit(fields[0], 2), parseBit(fields[0], 3) };
872 status.
usb = { parseBit(fields[1], 0), parseBit(fields[1], 1), parseBit(fields[1], 2), parseBit(fields[1], 3), parseBit(fields[1], 4), parseBit(fields[1], 5) };
878 std::lock_guard<std::mutex> lock(cacheMutex_);
879 if (frameSeq_ == 0)
return false;
881 frameSeq = frameSeq_;
887 if (!callback)
return 0;
889 std::lock_guard<std::mutex> lock(telemetryCallbackMutex_);
891 if (nextTelemetryCallbackHandle_ == 0) ++nextTelemetryCallbackHandle_;
892 telemetryCallbacks_.push_back(std::make_shared<TelemetryCallbackEntry>(handle, std::move(callback)));
898 if (handle == 0)
return false;
900 std::shared_ptr<TelemetryCallbackEntry> entry;
901 std::unique_lock<std::mutex> lock(telemetryCallbackMutex_);
902 auto it = std::find_if(telemetryCallbacks_.begin(), telemetryCallbacks_.end(),
903 [handle](
const std::shared_ptr<TelemetryCallbackEntry> &candidate)
905 return candidate->handle == handle;
907 if (it == telemetryCallbacks_.end())
return false;
910 entry->active =
false;
911 telemetryCallbacks_.erase(it);
913 if (currentTelemetryCallbackHandle == handle)
return true;
915 entry->cv.wait(lock, [&] {
return entry->activeCalls == 0; });
921 if (!callback)
return 0;
923 std::lock_guard<std::mutex> lock(connectionCallbackMutex_);
925 if (nextConnectionCallbackHandle_ == 0) ++nextConnectionCallbackHandle_;
926 connectionCallbacks_.push_back(std::make_shared<ConnectionCallbackEntry>(handle, std::move(callback)));
932 if (handle == 0)
return false;
934 std::shared_ptr<ConnectionCallbackEntry> entry;
935 std::unique_lock<std::mutex> lock(connectionCallbackMutex_);
936 auto it = std::find_if(connectionCallbacks_.begin(), connectionCallbacks_.end(),
937 [handle](
const std::shared_ptr<ConnectionCallbackEntry> &candidate)
939 return candidate->handle == handle;
941 if (it == connectionCallbacks_.end())
return false;
944 entry->active =
false;
945 connectionCallbacks_.erase(it);
947 if (currentConnectionCallbackHandle == handle)
return true;
949 entry->cv.wait(lock, [&] {
return entry->activeCalls == 0; });
953static HeaterMode heaterModeFromStatus(
int mode)
967static void addStatusField(std::vector<instrumentation::NamedValue> &fields,
968 const std::string &name,
971 fields.push_back(instrumentation::NamedValue{name, std::move(value)});
974static std::vector<instrumentation::NamedValue> statusFields(
const StatusFrame &status)
976 std::vector<instrumentation::NamedValue> fields;
978 for (std::size_t i = 0; i < status.dc.size(); ++i)
979 addStatusField(fields,
"dc[" + std::to_string(i) +
"]", status.dc[i]);
980 for (std::size_t i = 0; i < status.usb.size(); ++i)
981 addStatusField(fields,
"usb[" + std::to_string(i) +
"]", status.usb[i]);
982 addStatusField(fields,
"aht20Attached", status.aht20Attached);
983 addStatusField(fields,
"ds18b20Attached", status.ds18b20Attached);
984 addStatusField(fields,
"dew6Enabled", status.dew6Enabled);
985 addStatusField(fields,
"dew6Mode",
static_cast<std::int64_t
>(status.dew6Mode));
986 addStatusField(fields,
"dew7Enabled", status.dew7Enabled);
987 addStatusField(fields,
"dew7Mode",
static_cast<std::int64_t
>(status.dew7Mode));
988 addStatusField(fields,
"dew6OutputPercent", status.dew6OutputPercent);
989 addStatusField(fields,
"dew7OutputPercent", status.dew7OutputPercent);
990 addStatusField(fields,
"deviceTemperatureC", status.deviceTemperatureC);
991 addStatusField(fields,
"airTemperatureC", status.airTemperatureC);
992 addStatusField(fields,
"humidityPercent", status.humidityPercent);
993 addStatusField(fields,
"dewPointC", status.dewPointC);
994 addStatusField(fields,
"inputVoltageV", status.inputVoltageV);
995 addStatusField(fields,
"outputCurrentA", status.outputCurrentA);
996 addStatusField(fields,
"outputPowerW", status.outputPowerW);
1000static void addStatusChange(std::vector<instrumentation::FieldChange> &changes,
1001 const std::string &name,
1005 if (previous != current)
1007 changes.push_back(instrumentation::FieldChange{name, std::move(previous), std::move(current)});
1011static std::vector<instrumentation::FieldChange> statusChanges(
const StatusFrame &previous,
1012 const StatusFrame ¤t)
1014 std::vector<instrumentation::FieldChange> changes;
1015 changes.reserve(25);
1016 for (std::size_t i = 0; i < current.dc.size(); ++i)
1017 addStatusChange(changes,
"dc[" + std::to_string(i) +
"]", previous.dc[i], current.dc[i]);
1018 for (std::size_t i = 0; i < current.usb.size(); ++i)
1019 addStatusChange(changes,
"usb[" + std::to_string(i) +
"]", previous.usb[i], current.usb[i]);
1020 addStatusChange(changes,
"aht20Attached", previous.aht20Attached, current.aht20Attached);
1021 addStatusChange(changes,
"ds18b20Attached", previous.ds18b20Attached, current.ds18b20Attached);
1022 addStatusChange(changes,
"dew6Enabled", previous.dew6Enabled, current.dew6Enabled);
1023 addStatusChange(changes,
"dew6Mode",
static_cast<std::int64_t
>(previous.dew6Mode),
1024 static_cast<std::int64_t
>(current.dew6Mode));
1025 addStatusChange(changes,
"dew7Enabled", previous.dew7Enabled, current.dew7Enabled);
1026 addStatusChange(changes,
"dew7Mode",
static_cast<std::int64_t
>(previous.dew7Mode),
1027 static_cast<std::int64_t
>(current.dew7Mode));
1028 addStatusChange(changes,
"dew6OutputPercent", previous.dew6OutputPercent, current.dew6OutputPercent);
1029 addStatusChange(changes,
"dew7OutputPercent", previous.dew7OutputPercent, current.dew7OutputPercent);
1030 addStatusChange(changes,
"deviceTemperatureC", previous.deviceTemperatureC, current.deviceTemperatureC);
1031 addStatusChange(changes,
"airTemperatureC", previous.airTemperatureC, current.airTemperatureC);
1032 addStatusChange(changes,
"humidityPercent", previous.humidityPercent, current.humidityPercent);
1033 addStatusChange(changes,
"dewPointC", previous.dewPointC, current.dewPointC);
1034 addStatusChange(changes,
"inputVoltageV", previous.inputVoltageV, current.inputVoltageV);
1035 addStatusChange(changes,
"outputCurrentA", previous.outputCurrentA, current.outputCurrentA);
1036 addStatusChange(changes,
"outputPowerW", previous.outputPowerW, current.outputPowerW);
1040TelemetrySnapshot GeminiProtocol::makeTelemetrySnapshot(
const StatusFrame &status,
1041 unsigned long long sequence,
1042 std::chrono::steady_clock::time_point timestamp)
const
1044 return TelemetrySnapshot(
1047 status.aht20Attached,
1048 status.ds18b20Attached,
1049 {status.dew6Enabled, status.dew7Enabled},
1050 {heaterModeFromStatus(status.dew6Mode), heaterModeFromStatus(status.dew7Mode)},
1051 {status.dew6OutputPercent, status.dew7OutputPercent},
1052 status.deviceTemperatureC,
1053 status.airTemperatureC,
1054 status.humidityPercent,
1056 status.inputVoltageV,
1057 status.outputCurrentA,
1058 status.outputPowerW,
1063TelemetrySnapshot GeminiProtocol::storeStatusFrame(
const StatusFrame &status,
bool dispatchCallbacks)
1065 const auto timestamp = std::chrono::steady_clock::now();
1068 StatusFrame previousStatus;
1069 bool havePreviousStatus =
false;
1070 unsigned long long sequence = 0;
1072 std::lock_guard<std::mutex> lock(cacheMutex_);
1073 if (verboseInstrumentationEnabled && frameSeq_ != 0)
1075 previousStatus = cachedStatus_;
1076 havePreviousStatus =
true;
1078 cachedStatus_ = status;
1079 sequence = ++frameSeq_;
1080 lastFrameTime_ = timestamp;
1083 frameCv_.notify_all();
1085 TelemetrySnapshot snapshot = makeTelemetrySnapshot(status, sequence, timestamp);
1086 if (protocolInstrumentationEnabled)
1088 instrumentation::StatusDecodedPayload payload;
1089 payload.frameType =
"G";
1090 payload.sequence = sequence;
1091 if (verboseInstrumentationEnabled) payload.fields = statusFields(status);
1095 std::move(payload));
1096 event.sequence = sequence;
1097 emitInstrumentationEvent(std::move(event));
1099 if (verboseInstrumentationEnabled && havePreviousStatus)
1101 auto changes = statusChanges(previousStatus, status);
1102 if (!changes.empty())
1104 instrumentation::FieldChangesPayload payload;
1105 payload.sequence = sequence;
1106 payload.changes = std::move(changes);
1110 std::move(payload));
1111 event.sequence = sequence;
1112 emitInstrumentationEvent(std::move(event));
1115 if (dispatchCallbacks)
1117 dispatchTelemetrySnapshot(snapshot);
1122void GeminiProtocol::beginCallbackDispatch()
1124 std::unique_lock<std::mutex> lock(callbackDispatchMutex_);
1125 callbackDispatchCv_.wait(lock, [&] {
return !callbackDispatchInProgress_; });
1126 callbackDispatchInProgress_ =
true;
1129void GeminiProtocol::endCallbackDispatch()
1132 std::lock_guard<std::mutex> lock(callbackDispatchMutex_);
1133 callbackDispatchInProgress_ =
false;
1135 callbackDispatchCv_.notify_all();
1138void GeminiProtocol::dispatchTelemetrySnapshot(
const TelemetrySnapshot &snapshot)
1140 beginCallbackDispatch();
1144 std::vector<std::shared_ptr<TelemetryCallbackEntry>> entries;
1146 std::lock_guard<std::mutex> lock(telemetryCallbackMutex_);
1147 entries = telemetryCallbacks_;
1150 for (
const auto &entry : entries)
1154 std::lock_guard<std::mutex> lock(telemetryCallbackMutex_);
1155 if (!entry->active)
continue;
1156 ++entry->activeCalls;
1157 callback = entry->callback;
1160 currentTelemetryCallbackHandle = entry->handle;
1161 std::string diagnostic;
1166 catch (
const std::exception &ex)
1168 diagnostic = std::string(
"telemetry callback threw exception: ") + ex.what();
1172 diagnostic =
"telemetry callback threw unknown exception";
1174 currentTelemetryCallbackHandle = 0;
1177 std::lock_guard<std::mutex> lock(telemetryCallbackMutex_);
1178 --entry->activeCalls;
1179 if (entry->activeCalls == 0) entry->cv.notify_all();
1182 if (!diagnostic.empty())
1196 endCallbackDispatch();
1200 endCallbackDispatch();
1203void GeminiProtocol::dispatchConnectionEvent(
ConnectionEvent event,
const std::string &reason)
1205 beginCallbackDispatch();
1209 std::vector<std::shared_ptr<ConnectionCallbackEntry>> entries;
1211 std::lock_guard<std::mutex> lock(connectionCallbackMutex_);
1212 entries = connectionCallbacks_;
1215 for (
const auto &entry : entries)
1219 std::lock_guard<std::mutex> lock(connectionCallbackMutex_);
1220 if (!entry->active)
continue;
1221 ++entry->activeCalls;
1222 callback = entry->callback;
1225 currentConnectionCallbackHandle = entry->handle;
1226 std::string diagnostic;
1231 catch (
const std::exception &ex)
1233 diagnostic = std::string(
"connection callback threw exception: ") + ex.what();
1237 diagnostic =
"connection callback threw unknown exception";
1239 currentConnectionCallbackHandle = 0;
1242 std::lock_guard<std::mutex> lock(connectionCallbackMutex_);
1243 --entry->activeCalls;
1244 if (entry->activeCalls == 0) entry->cv.notify_all();
1247 if (!diagnostic.empty())
1261 endCallbackDispatch();
1265 endCallbackDispatch();
1285 emitConnectionInstrumentation(state, level, reason);
1288void GeminiProtocol::notifyCommunicationFailure(
const std::string &reason)
1290 if (!connected_.load(std::memory_order_acquire))
return;
1291 bool alreadyNotified = communicationFailureNotified_.exchange(
true, std::memory_order_acq_rel);
1292 if (alreadyNotified)
return;
1296 std::lock_guard<std::mutex> qlock(queueMutex_);
1300 connected_.store(
false, std::memory_order_release);
1301 queueCv_.notify_all();
1302 frameCv_.notify_all();
1308 std::unique_lock<std::mutex> lock(cacheMutex_);
1309 auto deadline = std::chrono::steady_clock::now() +
1310 std::chrono::milliseconds(timeoutMs);
1311 return frameCv_.wait_until(lock, deadline, [&] {
return frameSeq_ > minSeq; });
1317 std::unique_lock<std::mutex> lock(cacheMutex_);
1318 auto deadline = std::chrono::steady_clock::now() +
1319 std::chrono::milliseconds(timeoutMs);
1320 if (!frameCv_.wait_until(lock, deadline, [&] { return frameSeq_ > minSeq; }))
1324 out = cachedStatus_;
1325 frameSeq = frameSeq_;
1333 unsigned long long &verifiedSeq)
1335 auto start = std::chrono::steady_clock::now();
1336 auto deadline = start + std::chrono::milliseconds(timeoutMs);
1337 auto remainingMs = [&]
1339 auto now = std::chrono::steady_clock::now();
1340 if (now >= deadline)
return 0;
1341 return static_cast<int>(std::chrono::duration_cast<std::chrono::milliseconds>(deadline - now).count());
1345 unsigned long long firstSeq = 0;
1348 error =
"verification timed out waiting for first post-write telemetry frame";
1356 error =
"verification timed out waiting for second post-write telemetry frame";
1361 if (!verifier || !verifier(candidate))
1363 error =
"second post-write telemetry frame did not confirm requested state";
1369 "verified by telemetry frame " + std::to_string(verifiedSeq),
1374static bool outputCommand(
unsigned firmwareIndex,
bool enabled, std::string &
cmd)
1376 if (firmwareIndex < 2 || firmwareIndex > 11)
return false;
1377 cmd = enabled ?
">O" :
">C";
1378 cmd += std::to_string(firmwareIndex) +
"#";
1382static bool dewModeCommand(
unsigned channel,
DewMode mode, std::string &
cmd)
1384 if (channel != 6 && channel != 7)
return false;
1385 unsigned prefix = channel == 6 ? 10 : 20;
1386 unsigned suffix = 0;
1399 cmd =
">M" + std::to_string(prefix + suffix) +
"#";
1403static bool dewPercentCommand(
unsigned channel,
unsigned percent, std::string &
cmd)
1405 if ((channel != 6 && channel != 7) || percent > 100)
return false;
1406 char cmdChar = channel == 6 ?
'X' :
'Y';
1407 cmd = std::string(
">") + cmdChar + std::to_string(percent) +
"#";
1411static bool dewEnableCommand(
unsigned channel,
bool enabled, std::string &
cmd)
1415 cmd = enabled ?
">Z11#" :
">Z10#";
1420 cmd = enabled ?
">Z21#" :
">Z20#";
1426int GeminiProtocol::syncCommand(
const std::string &command)
1430 const auto timingStart = timingEnabled ? std::chrono::steady_clock::now() :
std::chrono::steady_clock::time_point{};
1433 std::lock_guard<std::mutex> wlock(writeMutex_);
1434 ok = writeAllLocked(command);
1437 if (!ok) notifyCommunicationFailure(
"serial write failed");
1441#ifdef GEMINI_TESTING
1442int GeminiProtocol::syncWriteForTesting(
const std::string &command)
1445 return syncCommand(command);
1448size_t GeminiProtocol::queuedCommandCountForTesting()
1450 std::lock_guard<std::mutex> qlock(queueMutex_);
1451 return queue_.size();
1454void GeminiProtocol::injectStatusForTesting(
const StatusFrame &status)
1456 storeStatusFrame(status,
true);
1459void GeminiProtocol::emitConnectionEventForTesting(
ConnectionEvent event)
1461 dispatchConnectionEvent(event);
1464bool GeminiProtocol::parseFirmwarePayloadForTesting(
const std::string &frame, std::string &raw,
int &numeric)
const
1466 return parseFirmwareFrame(frame, raw, numeric);
1473 if (!outputCommand(firmwareIndex, enabled,
cmd))
return 1;
1474 return syncCommand(
cmd);
1480 if (!dewModeCommand(channel, mode,
cmd))
return 1;
1481 return syncCommand(
cmd);
1487 if (!dewPercentCommand(channel, percent,
cmd))
return 1;
1488 return syncCommand(
cmd);
1494 if (!dewEnableCommand(channel, enabled,
cmd))
return 1;
1495 return syncCommand(
cmd);
1501 if (!outputCommand(firmwareIndex, enabled,
cmd))
return 1;
1502 return enqueueCommand(
cmd);
1508 if (!dewModeCommand(channel, mode,
cmd))
return 1;
1509 return enqueueCommand(
cmd);
1515 if (!dewPercentCommand(channel, percent,
cmd))
return 1;
1516 return enqueueCommand(
cmd);
1522 if (!dewEnableCommand(channel, enabled,
cmd))
return 1;
1523 return enqueueCommand(
cmd);
1528 return enqueueCommand(
">G#");
1531int GeminiProtocol::enqueueCommand(
const std::string &command)
1533 std::lock_guard<std::mutex> qlock(queueMutex_);
1539 const std::string message =
"writes not accepted, rejecting: " + command;
1546 if (queue_.size() >= kQueueCap)
1551 const std::string message =
"queue full, rejecting: " + command;
1558 queue_.push_back(command);
1559 queueCv_.notify_one();
1563void GeminiProtocol::readerLoop(
int serialFd,
int wakeFd)
1567 std::string failureReason;
1568 while (
running_.load(std::memory_order_acquire) &&
1571 if (serialFd < 0 && wakeFd < 0)
break;
1572 struct timeval tv = {1, 0};
1578 FD_SET(serialFd, &set);
1579 maxFd = std::max(maxFd, serialFd);
1583 FD_SET(wakeFd, &set);
1584 maxFd = std::max(maxFd, wakeFd);
1586 int rc = select(maxFd + 1, &set,
nullptr,
nullptr, &tv);
1589 if (
errno == EINTR)
continue;
1590 failureReason = std::string(
"serial select failed: ") + std::strerror(
errno);
1593 if (rc == 0)
continue;
1594 if (wakeFd >= 0 && FD_ISSET(wakeFd, &set))
1596 drainReaderWake(wakeFd);
1597 if (!
running_.load(std::memory_order_acquire) ||
1603 if (serialFd < 0 || !FD_ISSET(serialFd, &set))
continue;
1605 ssize_t n = ::read(serialFd, &buf, 1);
1608 if (
errno == EINTR ||
errno == EAGAIN ||
errno == EWOULDBLOCK)
continue;
1609 failureReason = std::string(
"serial read failed: ") + std::strerror(
errno);
1614 failureReason =
"serial read returned end-of-file";
1620 if (acc.size() >= 2 && acc[0] ==
'*' && acc[1] ==
'G')
1622 std::string payload = acc.substr(2);
1627 storeStatusFrame(parsed,
true);
1634 const std::string detail =
"status parse error: " + err;
1635 const std::string raw =
"*G" + payload +
"#";
1643 else if (!acc.empty())
1648 const std::string raw = acc +
"#";
1657 else if (acc.empty() && isAsciiWhitespace(buf))
1661 else if (acc.size() < 4096)
1671 "oversized frame discarded",
1678 if (!failureReason.empty() &&
1679 running_.load(std::memory_order_acquire) &&
1682 notifyCommunicationFailure(failureReason);
1686void GeminiProtocol::writerLoop()
1688 while (
running_.load(std::memory_order_acquire) &&
1691 std::unique_lock<std::mutex> qlock(queueMutex_);
1692 queueCv_.wait(qlock,
1693 [&] {
return !
running_.load(std::memory_order_acquire)
1697 if (!
running_.load(std::memory_order_acquire) ||
1702 if (queue_.empty())
continue;
1703 std::string
cmd = queue_.front();
1707 const auto timingStart = timingEnabled ? std::chrono::steady_clock::now() :
std::chrono::steady_clock::time_point{};
1710 std::lock_guard<std::mutex> wlock(writeMutex_);
1711 ok = writeAllLocked(
cmd);
1718 const std::string message =
"async write failed for command: " +
cmd;
1723 notifyCommunicationFailure(
"serial write failed");
1727 std::lock_guard<std::mutex> qlock(queueMutex_);
1728 if (!queue_.empty())
1732 const std::string message =
"discarded " + std::to_string(queue_.size()) +
" queued commands on session stop";
1746int GeminiProtocol::connectInternal(
const std::string &port,
int settleMs,
ConnectionEvent successEvent)
1749 const auto connectTimingStart = connectTimingEnabled ? std::chrono::steady_clock::now() :
1750 std::chrono::steady_clock::time_point{};
1751 auto finishConnect = [&](
int rc)
1753 if (connectTimingEnabled)
1761 lastSettleMs_ = settleMs;
1763 connected_.store(
false, std::memory_order_release);
1764 communicationFailureNotified_.store(
false, std::memory_order_release);
1766 std::lock_guard<std::mutex> lock(cacheMutex_);
1767 cachedStatus_ = StatusFrame{};
1769 lastFrameTime_ = {};
1771 if (!
open(port, settleMs))
1775 const std::string message =
"connect: open(" + port +
") failed";
1780 return finishConnect(5);
1783 auto start = std::chrono::steady_clock::now();
1784 auto deadline = start + std::chrono::seconds(8);
1785 auto remainingMs = [&]
1787 auto now = std::chrono::steady_clock::now();
1788 if (now >= deadline)
return 0;
1789 return (
int)std::chrono::duration_cast<std::chrono::milliseconds>(deadline - now).count();
1795 std::lock_guard<std::mutex> wlock(writeMutex_);
1797 if (!writeAllLocked(
">H#"))
1800 return finishConnect(2);
1802 if (!readTokenFrameOnce(
'H', resp, std::min(2000, remainingMs())))
1805 return finishConnect(2);
1808 if (resp !=
"*HGeminiPowerBoxPlusAdv3")
1812 const std::string message =
"connect: handshake mismatch: '" + resp +
"'";
1820 "handshake mismatch",
1825 return finishConnect(2);
1832 std::lock_guard<std::mutex> wlock(writeMutex_);
1834 if (!writeAllLocked(
">V#"))
1837 return finishConnect(3);
1839 if (!readTokenFrameOnce(
'V', resp, std::min(2000, remainingMs())))
1842 return finishConnect(3);
1845 if (!parseFirmwareFrame(resp, rawFw, numericFw))
1848 return finishConnect(3);
1850 if (numericFw < 308)
1854 const std::string message =
"connect: firmware " + std::to_string(numericFw) +
" < 308, rejected";
1860 return finishConnect(3);
1862 if (numericFw < 309)
1866 const std::string message =
"warning: firmware " + std::to_string(numericFw) +
1867 " < 309; DS18B20 detection fix not present, recommend 309+";
1876 std::lock_guard<std::mutex> lock(cacheMutex_);
1877 cachedStatus_ = StatusFrame{};
1879 lastFrameTime_ = {};
1882 std::lock_guard<std::mutex> wlock(writeMutex_);
1884 if (!writeAllLocked(
">G#"))
1887 return finishConnect(4);
1889 if (!readFrameOnce(resp, std::min(4000, remainingMs())))
1892 return finishConnect(4);
1895 if (resp.size() < 4 || resp[0] !=
'*' || resp[1] !=
'G')
1899 const std::string message =
"connect: first G frame not received";
1908 resp.empty() ? std::string() : resp +
"#");
1912 return finishConnect(4);
1920 const std::string message =
"connect: first frame parse error: " + err;
1933 return finishConnect(4);
1935 if (!openWakePipe())
1938 return finishConnect(5);
1945 running_.store(
true, std::memory_order_release);
1947 std::lock_guard<std::mutex> qlock(queueMutex_);
1951 connected_.store(
true, std::memory_order_release);
1952 connectionGeneration_.fetch_add(1, std::memory_order_acq_rel);
1954 std::lock_guard<std::mutex> lock(firmwareMutex_);
1955 firmwareVersionRaw_ = rawFw;
1956 firmwareVersionNumeric_ = numericFw;
1957 haveFirmwareVersion_ =
true;
1959 TelemetrySnapshot firstSnapshot = storeStatusFrame(first,
false);
1960 const int readerSerialFd = fd_;
1961 const int readerWakeFd = wakeReadFd_;
1962 reader_ = std::thread([
this, readerSerialFd, readerWakeFd] { readerLoop(readerSerialFd, readerWakeFd); });
1963 writer_ = std::thread([
this] { writerLoop(); });
1964 dispatchTelemetrySnapshot(firstSnapshot);
1965 dispatchConnectionEvent(successEvent);
1966 return finishConnect(0);
1979 std::lock_guard<std::mutex> qlock(queueMutex_);
1983 connected_.store(
false, std::memory_order_release);
1984 queueCv_.notify_all();
1985 frameCv_.notify_all();
1986 if (reader_.joinable()) reader_.join();
1987 if (writer_.joinable()) writer_.join();
1989 std::lock_guard<std::mutex> wlock(writeMutex_);
1994 std::lock_guard<std::mutex> qlock(queueMutex_);
1995 if (!queue_.empty())
1999 const std::string message =
"discarded " + std::to_string(queue_.size()) +
" queued commands on reconnect";
2010 const int delaysMs[3] = {0, 1000, 2000};
2011 for (
int attempt = 0; attempt < 3; ++attempt)
2013 if (delaysMs[attempt] > 0)
2015 if (!sleepCancelable(delaysMs[attempt]))
2017 emitReconnectInstrumentation(attempt + 1, 3,
2018 std::chrono::milliseconds(delaysMs[attempt]),
2025 if (!
running_.load(std::memory_order_acquire) || cancelled())
2028 emitReconnectInstrumentation(attempt + 1, 3,
2029 std::chrono::milliseconds(delaysMs[attempt]),
2035 emitReconnectInstrumentation(attempt + 1, 3,
2036 std::chrono::milliseconds(delaysMs[attempt]),
2043 emitReconnectInstrumentation(attempt + 1, 3,
2044 std::chrono::milliseconds(delaysMs[attempt]),
2050 emitReconnectInstrumentation(attempt + 1, 3,
2051 std::chrono::milliseconds(delaysMs[attempt]),
2055 emitReconnectInstrumentation(3, 3,
2056 std::chrono::milliseconds(delaysMs[2]),
2065 const bool wasConnected = connected_.exchange(
false, std::memory_order_acq_rel);
2068 std::lock_guard<std::mutex> qlock(queueMutex_);
2070 running_.store(
false, std::memory_order_release);
2073 queueCv_.notify_all();
2074 frameCv_.notify_all();
2075 if (reader_.joinable())
2079 if (writer_.joinable())
2084 std::lock_guard<std::mutex> wlock(writeMutex_);
2089 std::lock_guard<std::mutex> qlock(queueMutex_);
2090 if (!queue_.empty())
2094 const std::string message =
"discarded " + std::to_string(queue_.size()) +
" queued commands on shutdown";
bool latestStatus(StatusFrame &out, unsigned long long &frameSeq)
void setInstrumentationLevel(instrumentation::Level level)
InstrumentationListenerHandle registerInstrumentationListener(InstrumentationListener listener)
int setDewMode(unsigned channel, DewMode mode)
std::atomic< bool > recoveryAttempted_
bool waitForFrameAfter(unsigned long long minSeq, int timeoutMs)
bool firmwareVersion(std::string &raw, int &numeric) const
bool readFirmware(std::string &raw, int &numeric)
DirectQueryStatus queryIdentity(std::string &identity, int timeoutMs, std::string *detail=nullptr)
int setDewModeSync(unsigned channel, DewMode mode)
bool removeInstrumentationListener(InstrumentationListenerHandle handle)
int setDewManualPercentSync(unsigned channel, unsigned percent)
std::atomic< bool > reconnectPending_
int setOutputSync(unsigned firmwareIndex, bool enabled)
DirectQueryStatus queryFirmware(std::string &raw, int &numeric, int timeoutMs, std::string *detail=nullptr)
int lastOpenErrno() const
instrumentation::Level instrumentationLevel() const
int setOutput(unsigned firmwareIndex, bool enabled)
bool waitForVerifiedState(unsigned long long preWriteSeq, StatusVerifier verifier, int timeoutMs, std::string &error, unsigned long long &verifiedSeq)
int setDewManualPercent(unsigned channel, unsigned percent)
std::function< bool(const StatusFrame &)> StatusVerifier
bool open(const std::string &port, int settleMs=0)
bool lastOpenWasBusy() const
instrumentation::Dispatcher::Listener InstrumentationListener
bool parseStatusPayload(const std::string &payload, StatusFrame &status, std::string &error) const
std::uint64_t TelemetryCallbackHandle
std::atomic< bool > acceptingWrites_
int setDewEnabledSync(unsigned channel, bool enabled)
TelemetryCallbackHandle registerTelemetryCallback(TelemetryCallback callback)
bool removeTelemetryCallback(TelemetryCallbackHandle handle)
std::function< void(ConnectionEvent)> ConnectionCallback
std::atomic< bool > sessionStopRequested_
std::atomic< bool > running_
int setDewEnabled(unsigned channel, bool enabled)
std::function< void(const TelemetrySnapshot &)> TelemetryCallback
instrumentation::Dispatcher::ListenerHandle InstrumentationListenerHandle
std::uint64_t ConnectionCallbackHandle
bool instrumentationEnabled(instrumentation::Level level) const
bool removeConnectionCallback(ConnectionCallbackHandle handle)
ConnectionCallbackHandle registerConnectionCallback(ConnectionCallback callback)
int connect(const std::string &port, int settleMs=0)
ListenerHandle registerListener(Listener listener)
void setLevel(Level level)
bool removeListener(ListenerHandle handle)
void emit(Event event) noexcept
bool enabled(Level eventLevel) const
@ OversizedFrameDiscarded
@ IgnoredFrameWhileWaiting
@ FirmwareBelowRecommended
@ QueuedCommandsDiscarded
Event makeEvent(Level level, EventType type, Payload payload)
std::variant< std::monostate, bool, std::int64_t, std::uint64_t, double, std::string > Value
@ error
throw a parse_error exception in case of a tag
ConnectionCallback callback
std::condition_variable cv
ConnectionCallbackEntry(ConnectionCallbackHandle handle, ConnectionCallback callback)
const ConnectionCallbackHandle handle
std::condition_variable cv
const TelemetryCallbackHandle handle
TelemetryCallback callback
TelemetryCallbackEntry(TelemetryCallbackHandle handle, TelemetryCallback callback)
double deviceTemperatureC
std::array< bool, 6 > usb