Instrument Neutral Distributed Interface INDI 2.2.5
indi_gemini_pbh.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
24#include "indi_gemini_pbh.h"
25
26#include <algorithm>
27#include <cmath>
28#include <cstring>
29#include <optional>
30#include <utility>
31
32#include <basedevice.h>
34#include <indicom.h>
36#include <indilogger.h>
37
39
40namespace
41{
42constexpr const char *kDefaultDeviceName = "Gemini Power Box Hub Advanced v3";
43constexpr const char *kEnvironmentTab = "Environment";
44constexpr uint32_t kHousekeepingPeriodMs = 1000;
45constexpr auto kTelemetryStaleTimeout = std::chrono::seconds(10);
46constexpr auto kReconnectInterval = std::chrono::seconds(5);
47constexpr auto kOperationConfirmationTimeout = std::chrono::seconds(8);
48constexpr int kMinimumSupportedFirmware = 308;
49constexpr const char *kSupportedIdentity = "GeminiPowerBoxPlusAdv3";
50constexpr size_t kDcOutputCount = 4;
51constexpr size_t kDewOutputCount = 2;
52constexpr size_t kUsbOutputCount = 6;
53constexpr const char *kDcLabels[kDcOutputCount] = {"DC 2", "DC 3", "DC 4", "DC 5"};
54constexpr const char *kDewLabels[kDewOutputCount] = {"DEW 6", "DEW 7"};
55constexpr const char *kUsbLabels[kUsbOutputCount] = {"USB A", "USB B", "USB C", "USB D", "USB E", "USB F"};
56
57bool loggerSwitchEnabled(ISwitch switches[], unsigned int count, const char *name)
58{
59 for (unsigned int i = 0; i < count; ++i)
60 {
61 if (std::strcmp(switches[i].name, name) == 0)
62 return switches[i].s == ISS_ON;
63 }
64 return false;
65}
66
67bool indiLevelEnabled(const char *debugLevelName, const char *loggingLevelName)
68{
69 const unsigned int count = INDI::Logger::customLevel;
70 return loggerSwitchEnabled(INDI::Logger::DebugLevelS, count, debugLevelName) ||
71 loggerSwitchEnabled(INDI::Logger::LoggingLevelS, count, loggingLevelName);
72}
73
74#ifndef GEMINI_TESTING
75std::unique_ptr<GeminiPBH> driver(new GeminiPBH());
76#endif
77}
78
80
97
99{
100 shutdownInProgress_.store(true, std::memory_order_release);
101 userDisconnectRequested_ = true;
102 callbackStaging_.beginShutdown();
103 removeDeviceCallbacks();
104
105 if (device_)
106 {
107 device_->disconnect();
108 }
109
110 pendingOperations_.clear();
111 callbackStaging_.clear();
112}
113
114void GeminiPBH::registerDeviceCallbacks()
115{
116 if (!device_)
117 return;
118
119 telemetryCallbackHandle_ = device_->registerTelemetryCallback(
120 [this](const geminipbh::TelemetrySnapshot & snapshot)
121 {
122 handleTelemetry(snapshot);
123 });
124 connectionCallbackHandle_ = device_->registerConnectionCallback(
125 [this](geminipbh::ConnectionEvent event)
126 {
127 handleConnectionEvent(event);
128 });
129 instrumentationListenerHandle_ = device_->registerInstrumentationListener(
130 [this](const geminipbh::instrumentation::Event & event)
131 {
132 handleInstrumentation(event);
133 });
134}
135
136void GeminiPBH::removeDeviceCallbacks()
137{
138 if (!device_)
139 return;
140
141 if (telemetryCallbackHandle_ != 0)
142 {
143 device_->removeTelemetryCallback(telemetryCallbackHandle_);
144 telemetryCallbackHandle_ = 0;
145 }
146 if (connectionCallbackHandle_ != 0)
147 {
148 device_->removeConnectionCallback(connectionCallbackHandle_);
149 connectionCallbackHandle_ = 0;
150 }
151 if (instrumentationListenerHandle_ != 0)
152 {
153 device_->removeInstrumentationListener(instrumentationListenerHandle_);
154 instrumentationListenerHandle_ = 0;
155 }
156}
157
159{
160 return kDefaultDeviceName;
161}
162
164{
166
167 if (!serialConnection_)
168 {
169 serialConnection_.reset(new Connection::Serial(this));
170 serialConnection_->setDefaultBaudRate(Connection::Serial::B_19200);
171 registerConnection(serialConnection_.get());
172 setActiveConnection(serialConnection_.get());
173 }
174
176 setDefaultPollingPeriod(kHousekeepingPeriodMs);
177
178 initializePowerInterfaceProperties();
179 initializeCustomProperties();
180
184
185 return true;
186}
187
188void GeminiPBH::initializePowerInterfaceProperties()
189{
190 INDI::PowerInterface::initProperties(MAIN_CONTROL_TAB, kDcOutputCount, kDewOutputCount, 0, 0, kUsbOutputCount);
191
192 for (size_t i = 0; i < kDcOutputCount; ++i)
193 {
194 PowerChannelLabelsTP[i].setLabel(kDcLabels[i]);
195 if (std::strncmp(PowerChannelLabelsTP[i].getText(), "Channel ", 8) == 0)
196 PowerChannelLabelsTP[i].setText(kDcLabels[i]);
198 }
199
200 for (size_t i = 0; i < kDewOutputCount; ++i)
201 {
202 DewChannelLabelsTP[i].setLabel(kDewLabels[i]);
203 if (std::strncmp(DewChannelLabelsTP[i].getText(), "Channel ", 8) == 0)
204 DewChannelLabelsTP[i].setText(kDewLabels[i]);
206 DewChannelDutyCycleNP[i].setLabel((std::string(DewChannelLabelsTP[i].getText()) + " (%)").c_str());
207 }
208
209 for (size_t i = 0; i < kUsbOutputCount; ++i)
210 {
211 USBPortLabelsTP[i].setLabel(kUsbLabels[i]);
212 if (std::strncmp(USBPortLabelsTP[i].getText(), "Port ", 5) == 0)
213 USBPortLabelsTP[i].setText(kUsbLabels[i]);
215 }
216}
217
218void GeminiPBH::initializeCustomProperties()
219{
220 DeviceInfoTP[0].fill("FIRMWARE_VERSION", "Firmware Version", "Unavailable");
221 DeviceInfoTP.fill(getDeviceName(), "DEVICE_INFO", "Device Info", MAIN_CONTROL_TAB, IP_RO, 60, IPS_IDLE);
222
223 EnvironmentNP[0].fill("DEVICE_SURFACE_TEMPERATURE", "Device Surface Temperature (C)", "%.1f", -50, 100, 0, 0);
224 EnvironmentNP[1].fill("AIR_TEMPERATURE", "Air Temperature (C)", "%.1f", -50, 100, 0, 0);
225 EnvironmentNP[2].fill("HUMIDITY", "Humidity (%)", "%.1f", 0, 100, 0, 0);
226 EnvironmentNP[3].fill("DEW_POINT", "Calculated Dew Point (C)", "%.1f", -50, 100, 0, 0);
227 EnvironmentNP.fill(getDeviceName(), "PBH_ENVIRONMENT", "Environment", kEnvironmentTab, IP_RO, 60, IPS_IDLE);
228
229 auto fillModeProperty = [this](INDI::PropertySwitch & property, size_t index)
230 {
231 property[0].fill("AUTO", "Auto", ISS_ON);
232 property[1].fill("MANUAL_PWM", "Manual PWM", ISS_OFF);
233 property[2].fill("SWITCH", "Switch", ISS_OFF);
234 const std::string name = "DEW_" + std::to_string(index + 1) + "_MODE";
235 const std::string label = std::string(kDewLabels[index]) + " Mode";
236 property.fill(getDeviceName(), name.c_str(), label.c_str(), DEW_TAB, IP_RW, ISR_1OFMANY, 60, IPS_IDLE);
237 };
238 fillModeProperty(Dew1ModeSP, 0);
239 fillModeProperty(Dew2ModeSP, 1);
240
241 DewOutputsNP[0].fill("DEW_CHANNEL_1", "DEW 6 Current Output (%)", "%.0f", 0, 100, 0, 0);
242 DewOutputsNP[1].fill("DEW_CHANNEL_2", "DEW 7 Current Output (%)", "%.0f", 0, 100, 0, 0);
243 DewOutputsNP.fill(getDeviceName(), "DEW_OUTPUTS", "Current Output", DEW_TAB, IP_RO, 60, IPS_IDLE);
244}
245
247{
248 if (isConnected())
249 defineConnectedProperties();
250 else
251 deleteConnectedProperties();
252
253 return true;
254}
255
256void GeminiPBH::defineConnectedProperties()
257{
258 if (connectedPropertiesDefined_)
259 return;
260
261 defineProperty(DeviceInfoTP);
263 defineProperty(EnvironmentNP);
267 defineProperty(Dew1ModeSP);
268 defineProperty(Dew2ModeSP);
269 defineProperty(DewOutputsNP);
271
272 connectedPropertiesDefined_ = true;
273}
274
275void GeminiPBH::deleteConnectedProperties()
276{
277 if (!connectedPropertiesDefined_)
278 return;
279
280 deleteProperty(DeviceInfoTP);
282 deleteProperty(EnvironmentNP);
286 deleteProperty(Dew1ModeSP);
287 deleteProperty(Dew2ModeSP);
288 deleteProperty(DewOutputsNP);
290
291 connectedPropertiesDefined_ = false;
292}
293
295{
296 if (!device_)
297 {
298 LOG_ERROR("Gemini Power Box Hub Advanced v3 semantic device is unavailable.");
299 return false;
300 }
301
302 userDisconnectRequested_ = false;
303 driverState_ = DriverState::Connecting;
304 pendingOperations_.clear();
305 haveTelemetry_ = false;
306 callbackStaging_.clear();
307
308 const char *configuredPort = serialConnection_ ? serialConnection_->port() : "";
309 const bool haveConfiguredPort = configuredPort != nullptr && *configuredPort != '\0';
310 const bool canAutoSearch = autoSearchEnabled();
311 if (!haveConfiguredPort && !canAutoSearch)
312 {
313 driverState_ = DriverState::Disconnected;
314 LOG_ERROR("No serial port configured for Gemini Power Box Hub Advanced v3.");
315 return false;
316 }
317
318 if (haveConfiguredPort)
319 {
320 LOGF_INFO("Connecting to Gemini Power Box Hub Advanced v3 on %s.", configuredPort);
321
322 const geminipbh::Result result = device_->connect(configuredPort);
323 if (result)
324 return finishSuccessfulConnection(false);
325
326 if (!canAutoSearch)
327 {
328 driverState_ = DriverState::Disconnected;
329 logDeviceError("connect", result.error());
330 deleteConnectedProperties();
331 return false;
332 }
333
334 LOGF_WARN("Connection to %s failed; starting Gemini Power Box Hub Advanced v3 auto search.", configuredPort);
335 }
336 else
337 {
338 LOG_WARN("No serial port configured; starting Gemini Power Box Hub Advanced v3 auto search.");
339 }
340
341 const geminipbh::DiscoveryResult discovery = runDiscovery();
342 if (discovery.cancelled)
343 {
344 driverState_ = DriverState::Disconnected;
345 LOG_ERROR("Gemini Power Box Hub Advanced v3 auto search was cancelled.");
346 deleteConnectedProperties();
347 return false;
348 }
349
350 const std::vector<geminipbh::ProbeResult> supportedDevices = supportedDiscoveryDevices(discovery);
351 if (supportedDevices.empty())
352 {
353 driverState_ = DriverState::Disconnected;
354 LOGF_ERROR("Gemini Power Box Hub Advanced v3 auto search found no supported device among %zu probe(s).", discovery.probes.size());
355 deleteConnectedProperties();
356 return false;
357 }
358 if (supportedDevices.size() > 1)
359 {
360 driverState_ = DriverState::Disconnected;
361 for (const auto &device : supportedDevices)
362 LOGF_ERROR("Auto search candidate: %s firmware %s.",
363 device.preferredPort.c_str(), device.firmware.raw.c_str());
364 LOG_ERROR("Multiple supported Gemini Power Box Hub Advanced v3 devices detected; select the desired serial port manually.");
365 deleteConnectedProperties();
366 return false;
367 }
368
369 const geminipbh::ProbeResult &detected = supportedDevices.front();
370 if (!updateSerialPortSelection(detected.preferredPort))
371 {
372 driverState_ = DriverState::Disconnected;
373 LOGF_ERROR("Auto search detected %s but failed to update DEVICE_PORT.", detected.preferredPort.c_str());
374 deleteConnectedProperties();
375 return false;
376 }
377
378 LOGF_INFO("Auto search selected Gemini Power Box Hub Advanced v3 on %s (identity %s, firmware %s).",
379 detected.preferredPort.c_str(), detected.identity.c_str(), detected.firmware.raw.c_str());
380
381 const geminipbh::Result detectedConnect = device_->connect(detected.preferredPort);
382 if (!detectedConnect)
383 {
384 driverState_ = DriverState::Disconnected;
385 logDeviceError("connect after auto search", detectedConnect.error());
386 deleteConnectedProperties();
387 return false;
388 }
389
390 return finishSuccessfulConnection(true);
391}
392
393bool GeminiPBH::finishSuccessfulConnection(bool detectedPort)
394{
395 driverState_ = DriverState::Connected;
396 defineConnectedProperties();
397 flushConnectionEvents();
398 flushTelemetryToINDI();
399 refreshPropertiesFromDevice();
400 SetTimer(kHousekeepingPeriodMs);
401 if (detectedPort)
402 {
403#ifdef GEMINI_TESTING
404 detectedPortSaveRequestedForTesting_ = true;
405#else
407#endif
408 }
409 LOG_INFO("Gemini Power Box Hub Advanced v3 connected.");
410 return true;
411}
412
413bool GeminiPBH::autoSearchEnabled() const
414{
415#ifdef GEMINI_TESTING
416 if (autoSearchOverrideForTesting_)
417{
418 return *autoSearchOverrideForTesting_;
419}
420#endif
421
423if (!property || property.isEmpty())
424 {
425 return false;
426 }
427 return property.isSwitchOn("INDI_ENABLED");
428}
429
430bool GeminiPBH::updateSerialPortSelection(const std::string &port)
431{
432 if (port.empty())
433 return false;
434
436 if (property && !property.isEmpty() && property.size() > 0)
437 {
438 property[0].setText(port.c_str());
439 property.setState(IPS_OK);
440 property.apply();
441 return true;
442 }
443
444 if (!serialConnection_)
445 return false;
446 serialConnection_->setDefaultPort(port.c_str());
447 return true;
448}
449
450bool GeminiPBH::discoveryCancellationRequested() const
451{
452 return shutdownInProgress_.load(std::memory_order_acquire) || userDisconnectRequested_;
453}
454
455geminipbh::DiscoveryResult GeminiPBH::runDiscovery()
456{
457#ifdef GEMINI_TESTING
458 if (discoveryFunctionForTesting_)
459 return discoveryFunctionForTesting_();
460#endif
461
462 if (serialConnection_)
463 serialConnection_->Refresh(true);
464
466 options.shouldCancel = [this]
467 {
468 return discoveryCancellationRequested();
469 };
470 return geminipbh::discoverGeminiDevices(options);
471}
472
473std::vector<geminipbh::ProbeResult> GeminiPBH::supportedDiscoveryDevices(
474 const geminipbh::DiscoveryResult &discovery) const
475{
476 std::vector<geminipbh::ProbeResult> supported;
477 for (const auto &device : discovery.devices)
478 {
479 std::string reason;
480 if (isSupportedDiscoveryDevice(device, reason))
481 {
482 supported.push_back(device);
483 }
484 else
485 {
486 LOGF_WARN("Ignoring discovered Gemini device on %s: %s.",
487 device.preferredPort.c_str(), reason.c_str());
488 }
489 }
490 return supported;
491}
492
493bool GeminiPBH::isSupportedDiscoveryDevice(const geminipbh::ProbeResult &device, std::string &reason) const
494{
496 {
497 reason = std::string("probe status is ") + geminipbh::probeStatusName(device.status);
498 return false;
499 }
500 if (device.identity != kSupportedIdentity)
501 {
502 reason = "identity '" + device.identity + "' is not supported by this driver";
503 return false;
504 }
505 if (!device.firmware.numeric)
506 {
507 reason = "firmware version is unavailable";
508 return false;
509 }
510 if (*device.firmware.numeric < kMinimumSupportedFirmware)
511 {
512 reason = "firmware " + std::to_string(*device.firmware.numeric) + " is below the supported minimum " +
513 std::to_string(kMinimumSupportedFirmware);
514 return false;
515 }
516 return true;
517}
518
520{
521 userDisconnectRequested_ = true;
522 driverState_ = DriverState::Disconnected;
523 failPendingOperations(IPS_IDLE);
524 haveTelemetry_ = false;
525
527 if (device_)
528 result = device_->disconnect();
529
530 if (!result && result.error() != geminipbh::Error::InvalidState)
531 logDeviceError("disconnect", result.error());
532
533 callbackStaging_.clear();
534
535 deleteConnectedProperties();
536 LOG_INFO("Gemini Power Box Hub Advanced v3 disconnected.");
537 return true;
538}
539
541{
542 flushConnectionEvents();
543 flushTelemetryToINDI();
544 checkTelemetryFreshness();
545 checkPendingOperations();
546 attemptReconnectIfDue();
547 scheduleNextTimer();
548}
549
550void GeminiPBH::scheduleNextTimer()
551{
552 if (!userDisconnectRequested_ && driverState_ != DriverState::Disconnected)
553 SetTimer(kHousekeepingPeriodMs);
554}
555
557{
559 syncInstrumentationLevelWithIndiDebug();
560}
561
562bool GeminiPBH::ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n)
563{
564 if (dev != nullptr && std::strcmp(dev, getDeviceName()) != 0)
565 return false;
566
568 return handlePowerSwitchChange(PowerChannelsSP, states, names, n, gindi::PendingOperationKind::DcOutput);
569 if (USBPortSP.isNameMatch(name))
570 return handlePowerSwitchChange(USBPortSP, states, names, n, gindi::PendingOperationKind::UsbOutput);
571 if (DewChannelsSP.isNameMatch(name))
572 return handlePowerSwitchChange(DewChannelsSP, states, names, n, gindi::PendingOperationKind::HeaterEnabled);
573 if (Dew1ModeSP.isNameMatch(name))
574 return handleDewModeChange(0, Dew1ModeSP, states, names, n);
575 if (Dew2ModeSP.isNameMatch(name))
576 return handleDewModeChange(1, Dew2ModeSP, states, names, n);
577
578 const bool affectsInstrumentationLevel = name != nullptr &&
579 (std::strcmp(name, "DEBUG") == 0 ||
580 std::strcmp(name, "DEBUG_LEVEL") == 0 ||
581 std::strcmp(name, "LOGGING_LEVEL") == 0);
582 const bool handled = INDI::DefaultDevice::ISNewSwitch(dev, name, states, names, n);
583 if (handled && affectsInstrumentationLevel)
584 syncInstrumentationLevelWithIndiDebug();
585 return handled;
586}
587
588bool GeminiPBH::ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n)
589{
590 if (dev != nullptr && std::strcmp(dev, getDeviceName()) != 0)
591 return false;
592
594 return handleDewDutyCycleChange(values, names, n);
595
596 return INDI::DefaultDevice::ISNewNumber(dev, name, values, names, n);
597}
598
599bool GeminiPBH::ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n)
600{
601 if (dev != nullptr && std::strcmp(dev, getDeviceName()) != 0)
602 return false;
603
605 {
606 const bool processed = INDI::PowerInterface::processText(dev, name, texts, names, n);
607 if (processed)
608 refreshLabelsFromTextProperties();
609 return processed;
610 }
611
612 return INDI::DefaultDevice::ISNewText(dev, name, texts, names, n);
613}
614
615bool GeminiPBH::handlePowerSwitchChange(INDI::PropertySwitch &property, ISState *states, char *names[], int n,
617{
618 bool accepted = true;
619 for (int request = 0; request < n; ++request)
620 {
621 for (size_t index = 0; index < property.size(); ++index)
622 {
623 if (!property[index].isNameMatch(names[request]))
624 continue;
625
626 const bool enabled = states[request] == ISS_ON;
627 bool rc = false;
628 if (kind == gindi::PendingOperationKind::DcOutput)
629 rc = requestDcOutputChange(index, enabled);
630 else if (kind == gindi::PendingOperationKind::UsbOutput)
631 rc = requestUsbOutputChange(index, enabled);
632 else if (kind == gindi::PendingOperationKind::HeaterEnabled)
633 rc = requestHeaterEnabledChange(index, enabled);
634
635 if (!rc)
636 {
637 accepted = false;
638 }
639 }
640 }
641
642 property.setState(accepted ? IPS_BUSY : IPS_ALERT);
643 property.apply();
644 return true;
645}
646
647bool GeminiPBH::handleDewModeChange(size_t heaterIndex, INDI::PropertySwitch &property, ISState *states,
648 char *names[], int n)
649{
651 bool foundTarget = false;
652 for (int request = 0; request < n; ++request)
653 {
654 if (states[request] != ISS_ON)
655 continue;
656
657 for (size_t i = 0; i < property.size(); ++i)
658 {
659 if (!property[i].isNameMatch(names[request]))
660 continue;
661
662 if (gindi::heaterModeFromSwitchName(names[request], target))
663 {
664 foundTarget = true;
665 break;
666 }
667 }
668 }
669
670 if (!foundTarget)
671 {
672 property.setState(IPS_ALERT);
673 property.apply();
674 return true;
675 }
676
678 preservedManualPower_[heaterIndex] = static_cast<unsigned>(std::round(DewChannelDutyCycleNP[heaterIndex].getValue()));
679
680 if (!requestHeaterModeChange(heaterIndex, target))
681 {
682 property.setState(IPS_ALERT);
683 property.apply();
684 return true;
685 }
686
687 property.setState(IPS_BUSY);
688 property.apply();
689 return true;
690}
691
692bool GeminiPBH::handleDewDutyCycleChange(double values[], char *names[], int n)
693{
694 bool accepted = true;
695 for (int request = 0; request < n; ++request)
696 {
697 for (size_t index = 0; index < DewChannelDutyCycleNP.size(); ++index)
698 {
699 if (!DewChannelDutyCycleNP[index].isNameMatch(names[request]))
700 continue;
701
702 if (!requestHeaterManualPowerChange(index, values[request]))
703 {
704 accepted = false;
705 continue;
706 }
707 }
708 }
709
712 return true;
713}
714
715bool GeminiPBH::SetPowerPort(size_t port, bool enabled)
716{
717 return requestDcOutputChange(port, enabled);
718}
719
720bool GeminiPBH::SetDewPort(size_t port, bool enabled, double dutyCycle)
721{
722 if (!requestHeaterEnabledChange(port, enabled))
723 return false;
724 return requestHeaterManualPowerChange(port, dutyCycle);
725}
726
727bool GeminiPBH::SetUSBPort(size_t port, bool enabled)
728{
729 return requestUsbOutputChange(port, enabled);
730}
731
732bool GeminiPBH::requestDcOutputChange(size_t port, bool enabled)
733{
734 const geminipbh::Result ready = requireWriteReady();
735 if (!ready)
736 {
737 logDeviceError("set DC output", ready.error());
738 return false;
739 }
740
741 const geminipbh::Result result = device_->setDcOutput(port, enabled);
742 if (!result)
743 {
744 logDeviceError("set DC output", result.error());
745 return false;
746 }
747
748 auto target = gindi::makeBooleanTarget(gindi::PendingOperationKind::DcOutput, port, enabled);
749 if (!target)
750 {
751 logDeviceError("create DC confirmation target", target.error());
752 return false;
753 }
754 addPendingOperation(gindi::PendingOperation(gindi::PendingOperationKind::DcOutput,
755 port,
756 target.value(),
757 std::chrono::steady_clock::now() + kOperationConfirmationTimeout));
758 LOGF_DEBUG("DC output %zu accepted; waiting for telemetry confirmation.", port + 1);
759 return true;
760}
761
762bool GeminiPBH::requestUsbOutputChange(size_t port, bool enabled)
763{
764 const geminipbh::Result ready = requireWriteReady();
765 if (!ready)
766 {
767 logDeviceError("set USB output", ready.error());
768 return false;
769 }
770
771 const geminipbh::Result result = device_->setUsbOutput(port, enabled);
772 if (!result)
773 {
774 logDeviceError("set USB output", result.error());
775 return false;
776 }
777
778 auto target = gindi::makeBooleanTarget(gindi::PendingOperationKind::UsbOutput, port, enabled);
779 if (!target)
780 {
781 logDeviceError("create USB confirmation target", target.error());
782 return false;
783 }
784 addPendingOperation(gindi::PendingOperation(gindi::PendingOperationKind::UsbOutput,
785 port,
786 target.value(),
787 std::chrono::steady_clock::now() + kOperationConfirmationTimeout));
788 LOGF_DEBUG("USB output %zu accepted; waiting for telemetry confirmation.", port + 1);
789 return true;
790}
791
792bool GeminiPBH::requestHeaterEnabledChange(size_t port, bool enabled)
793{
794 const geminipbh::Result ready = requireWriteReady();
795 if (!ready)
796 {
797 logDeviceError("set heater enabled state", ready.error());
798 return false;
799 }
800
801 const geminipbh::Result result = device_->setHeaterEnabled(port, enabled);
802 if (!result)
803 {
804 logDeviceError("set heater enabled state", result.error());
805 return false;
806 }
807
808 auto target = gindi::makeBooleanTarget(gindi::PendingOperationKind::HeaterEnabled, port, enabled);
809 if (!target)
810 {
811 logDeviceError("create heater-enabled confirmation target", target.error());
812 return false;
813 }
814 addPendingOperation(gindi::PendingOperation(gindi::PendingOperationKind::HeaterEnabled,
815 port,
816 target.value(),
817 std::chrono::steady_clock::now() + kOperationConfirmationTimeout));
818 LOGF_DEBUG("Heater %zu enabled-state change accepted; waiting for telemetry confirmation.", port + 1);
819 return true;
820}
821
822bool GeminiPBH::requestHeaterModeChange(size_t port, geminipbh::HeaterMode mode)
823{
824 const geminipbh::Result ready = requireWriteReady();
825 if (!ready)
826 {
827 logDeviceError("set heater mode", ready.error());
828 return false;
829 }
830
831 const geminipbh::Result result = device_->setHeaterMode(port, mode);
832 if (!result)
833 {
834 logDeviceError("set heater mode", result.error());
835 return false;
836 }
837
838 auto target = gindi::makeHeaterModeTarget(port, mode);
839 if (!target)
840 {
841 logDeviceError("create heater-mode confirmation target", target.error());
842 return false;
843 }
844 addPendingOperation(gindi::PendingOperation(gindi::PendingOperationKind::HeaterMode,
845 port,
846 target.value(),
847 std::chrono::steady_clock::now() + kOperationConfirmationTimeout));
848 LOGF_DEBUG("Heater %zu mode change to %s accepted; waiting for telemetry confirmation.",
849 port + 1, heaterModeName(mode));
850 return true;
851}
852
853bool GeminiPBH::requestHeaterManualPowerChange(size_t port, double percent)
854{
855 const geminipbh::Result ready = requireWriteReady();
856 if (!ready)
857 {
858 logDeviceError("set heater manual power", ready.error());
859 return false;
860 }
861 if (port >= kDewOutputCount || percent < 0.0 || percent > 100.0)
862 {
863 logDeviceError("set heater manual power", geminipbh::Error::InvalidValue);
864 return false;
865 }
866
867 auto mode = device_->heaterMode(port);
868 if (!mode)
869 {
870 logDeviceError("set heater manual power", mode.error());
871 return false;
872 }
873 if (mode.value() != geminipbh::HeaterMode::ManualPwm)
874 {
875 LOGF_WARN("Heater %zu manual power is editable only in Manual PWM mode.", port + 1);
876 return false;
877 }
878
879 const unsigned rounded = static_cast<unsigned>(std::round(percent));
880 const geminipbh::Result result = device_->setHeaterManualPower(port, rounded);
881 if (!result)
882 {
883 logDeviceError("set heater manual power", result.error());
884 return false;
885 }
886
887 preservedManualPower_[port] = rounded;
888 auto configured = device_->heaterManualPower(port);
889 DewChannelDutyCycleNP[port].setValue(configured ? configured.value() : rounded);
890 LOGF_DEBUG("Heater %zu manual power setpoint accepted as driver/library configuration; telemetry does not independently confirm it.",
891 port + 1);
892 return true;
893}
894
895geminipbh::Result GeminiPBH::requireWriteReady() const
896{
897 if (shutdownInProgress_.load(std::memory_order_acquire) || !device_)
899
900 switch (driverState_)
901 {
902 case DriverState::Connected:
904 case DriverState::CommunicationFailure:
906 case DriverState::Connecting:
907 case DriverState::Reconnecting:
909 case DriverState::Disconnected:
911 }
913}
914
915void GeminiPBH::handleTelemetry(const geminipbh::TelemetrySnapshot &snapshot)
916{
917 if (shutdownInProgress_.load(std::memory_order_acquire))
918 return;
919
920 callbackStaging_.stageTelemetry(snapshot);
921}
922
923void GeminiPBH::handleConnectionEvent(geminipbh::ConnectionEvent event)
924{
925 if (shutdownInProgress_.load(std::memory_order_acquire))
926 return;
927
928 callbackStaging_.stageConnectionEvent(event);
929}
930
931void GeminiPBH::syncInstrumentationLevelWithIndiDebug()
932{
933 if (!device_)
934 return;
935
936 namespace instr = geminipbh::instrumentation;
937
938 instr::Level level = instr::Level::Off;
939 if (isDebug())
940 {
941 if (indiLevelEnabled("DBG_DEBUG", "LOG_DEBUG"))
942 level = instr::Level::Verbose;
943 else if (indiLevelEnabled("DBG_SESSION", "LOG_SESSION"))
944 level = instr::Level::Info;
945 else if (indiLevelEnabled("DBG_ERROR", "LOG_ERROR"))
946 level = instr::Level::Error;
947 }
948
949 protocolInstrumentationForwarding_.store(level != instr::Level::Off, std::memory_order_release);
950 device_->setInstrumentationLevel(level);
951}
952
953void GeminiPBH::handleInstrumentation(const geminipbh::instrumentation::Event &event)
954{
955 if (shutdownInProgress_.load(std::memory_order_acquire))
956 return;
957 if (!protocolInstrumentationForwarding_.load(std::memory_order_acquire))
958 return;
960 return;
962 return;
963
964 const std::string message = geminipbh::instrumentation::EventFormatter().format(event);
966 LOGF_WARN("libgeminipbh: %s", message.c_str());
968 LOGF_ERROR("libgeminipbh: %s", message.c_str());
970 LOGF_INFO("libgeminipbh: %s", message.c_str());
971 else
972 LOGF_DEBUG("libgeminipbh: %s", message.c_str());
973}
974
975void GeminiPBH::flushConnectionEvents()
976{
977 std::deque<geminipbh::ConnectionEvent> events = callbackStaging_.takeConnectionEvents();
978
979 for (geminipbh::ConnectionEvent event : events)
980 processConnectionEvent(event);
981}
982
983void GeminiPBH::processConnectionEvent(geminipbh::ConnectionEvent event)
984{
985 switch (event)
986 {
988 if (userDisconnectRequested_ || shutdownInProgress_.load(std::memory_order_acquire))
989 return;
990 driverState_ = DriverState::Connected;
991 defineConnectedProperties();
992 refreshPropertiesFromDevice();
993 LOG_INFO("Gemini Power Box Hub Advanced v3 semantic connection established.");
994 break;
995
997 if (userDisconnectRequested_ || shutdownInProgress_.load(std::memory_order_acquire))
998 return;
999 markCommunicationFailure("library reported communication failure");
1000 break;
1001
1003 if (userDisconnectRequested_ || shutdownInProgress_.load(std::memory_order_acquire))
1004 return;
1005 driverState_ = DriverState::Connected;
1006 defineConnectedProperties();
1007 refreshPropertiesFromDevice();
1008 LOG_INFO("Gemini Power Box Hub Advanced v3 reconnect succeeded.");
1009 break;
1010
1012 if (!userDisconnectRequested_ && !shutdownInProgress_.load(std::memory_order_acquire))
1013 markCommunicationFailure("library reported disconnect");
1014 break;
1015 }
1016}
1017
1018void GeminiPBH::flushTelemetryToINDI()
1019{
1020 auto snapshot = callbackStaging_.takeTelemetry();
1021
1022 if (!snapshot || driverState_ != DriverState::Connected)
1023 return;
1024
1025 haveTelemetry_ = true;
1026 lastTelemetryTime_ = snapshot->timestamp;
1027
1028 for (size_t i = 0; i < kDcOutputCount; ++i)
1029 PowerChannelsSP[i].setState(snapshot->dcOutputs[i] ? ISS_ON : ISS_OFF);
1030
1031 for (size_t i = 0; i < kUsbOutputCount; ++i)
1032 USBPortSP[i].setState(snapshot->usbOutputs[i] ? ISS_ON : ISS_OFF);
1033
1034 for (size_t i = 0; i < kDewOutputCount; ++i)
1035 {
1036 DewChannelsSP[i].setState(snapshot->heaterEnabled[i] ? ISS_ON : ISS_OFF);
1037 DewChannelDutyCycleNP[i].setValue(preservedManualPower_[i]);
1038 DewOutputsNP[i].setValue(snapshot->heaterOutputPercent[i]);
1039
1040 INDI::PropertySwitch &modeProperty = (i == 0) ? Dew1ModeSP : Dew2ModeSP;
1041 const size_t modeIndex = gindi::heaterModeSwitchIndex(snapshot->heaterModes[i]);
1042 for (size_t mode = 0; mode < modeProperty.size(); ++mode)
1043 modeProperty[mode].setState(mode == modeIndex ? ISS_ON : ISS_OFF);
1044 }
1045 DewOutputsNP.setState(IPS_OK);
1046
1047 PowerSensorsNP[SENSOR_VOLTAGE].setValue(snapshot->inputVoltageV);
1048 PowerSensorsNP[SENSOR_CURRENT].setValue(snapshot->outputCurrentA);
1049 PowerSensorsNP[SENSOR_POWER].setValue(snapshot->outputPowerW);
1051
1052 EnvironmentNP[0].setValue(snapshot->deviceSurfaceTemperatureC);
1053 EnvironmentNP[1].setValue(snapshot->airTemperatureC);
1054 EnvironmentNP[2].setValue(snapshot->humidityPercent);
1055 EnvironmentNP[3].setValue(snapshot->dewPointC);
1056 EnvironmentNP.setState(IPS_OK);
1057
1058 auto firmware = device_ ? device_->firmwareVersion() : geminipbh::ValueResult<std::string>::failure(
1060 if (firmware)
1061 DeviceInfoTP[0].setText(firmware.value().c_str());
1062 DeviceInfoTP.setState(firmware ? IPS_OK : IPS_ALERT);
1063
1068 Dew1ModeSP.setState(IPS_OK);
1069 Dew2ModeSP.setState(IPS_OK);
1070
1071 checkPendingOperations(&*snapshot);
1072 applyPendingAwareLiveStates();
1073
1075 USBPortSP.apply();
1078 Dew1ModeSP.apply();
1079 Dew2ModeSP.apply();
1080 DewOutputsNP.apply();
1082 EnvironmentNP.apply();
1083 DeviceInfoTP.apply();
1084}
1085
1086void GeminiPBH::refreshPropertiesFromDevice()
1087{
1088 if (!device_ || !connectedPropertiesDefined_)
1089 return;
1090
1091 auto firmware = device_->firmwareVersion();
1092 if (firmware)
1093 DeviceInfoTP[0].setText(firmware.value().c_str());
1094 DeviceInfoTP.setState(firmware ? IPS_OK : IPS_ALERT);
1095
1096 for (size_t i = 0; i < kDcOutputCount; ++i)
1097 {
1098 auto value = device_->dcOutputState(i);
1099 if (value) PowerChannelsSP[i].setState(value.value() ? ISS_ON : ISS_OFF);
1100 }
1101 for (size_t i = 0; i < kUsbOutputCount; ++i)
1102 {
1103 auto value = device_->usbOutputState(i);
1104 if (value) USBPortSP[i].setState(value.value() ? ISS_ON : ISS_OFF);
1105 }
1106 for (size_t i = 0; i < kDewOutputCount; ++i)
1107 {
1108 auto enabled = device_->heaterEnabled(i);
1109 if (enabled) DewChannelsSP[i].setState(enabled.value() ? ISS_ON : ISS_OFF);
1110
1111 auto mode = device_->heaterMode(i);
1112 if (mode)
1113 {
1114 INDI::PropertySwitch &modeProperty = (i == 0) ? Dew1ModeSP : Dew2ModeSP;
1115 const size_t modeIndex = gindi::heaterModeSwitchIndex(mode.value());
1116 for (size_t j = 0; j < modeProperty.size(); ++j)
1117 modeProperty[j].setState(j == modeIndex ? ISS_ON : ISS_OFF);
1118 }
1119
1120 auto manual = device_->heaterManualPower(i);
1121 if (manual)
1122 preservedManualPower_[i] = manual.value();
1123 DewChannelDutyCycleNP[i].setValue(preservedManualPower_[i]);
1124
1125 auto output = device_->heaterCurrentOutput(i);
1126 if (output) DewOutputsNP[i].setValue(output.value());
1127 }
1128
1129 auto voltage = device_->inputVoltage();
1130 auto current = device_->inputCurrent();
1131 auto power = device_->inputPower();
1132 if (voltage) PowerSensorsNP[SENSOR_VOLTAGE].setValue(voltage.value());
1133 if (current) PowerSensorsNP[SENSOR_CURRENT].setValue(current.value());
1134 if (power) PowerSensorsNP[SENSOR_POWER].setValue(power.value());
1135 PowerSensorsNP.setState(voltage && current && power ? IPS_OK : IPS_ALERT);
1136
1137 auto surface = device_->deviceSurfaceTemperature();
1138 auto air = device_->airTemperature();
1139 auto humidity = device_->humidity();
1140 auto dewPoint = device_->dewPoint();
1141 if (surface) EnvironmentNP[0].setValue(surface.value());
1142 if (air) EnvironmentNP[1].setValue(air.value());
1143 if (humidity) EnvironmentNP[2].setValue(humidity.value());
1144 if (dewPoint) EnvironmentNP[3].setValue(dewPoint.value());
1145 EnvironmentNP.setState(surface && air && humidity && dewPoint ? IPS_OK : IPS_ALERT);
1146
1147 DewOutputsNP.setState(IPS_OK);
1152 Dew1ModeSP.setState(IPS_OK);
1153 Dew2ModeSP.setState(IPS_OK);
1154 applyPendingAwareLiveStates();
1155
1156 DeviceInfoTP.apply();
1158 EnvironmentNP.apply();
1160 USBPortSP.apply();
1163 Dew1ModeSP.apply();
1164 Dew2ModeSP.apply();
1165 DewOutputsNP.apply();
1166}
1167
1168void GeminiPBH::checkTelemetryFreshness()
1169{
1170 checkTelemetryFreshness(std::chrono::steady_clock::now());
1171}
1172
1173void GeminiPBH::checkTelemetryFreshness(std::chrono::steady_clock::time_point now)
1174{
1175 if (driverState_ != DriverState::Connected || !haveTelemetry_)
1176 return;
1177
1178 if (now - lastTelemetryTime_ > kTelemetryStaleTimeout)
1179 markCommunicationFailure("telemetry is stale");
1180}
1181
1182#ifdef GEMINI_TESTING
1183void GeminiPBH::simulateConnectedTelemetryForTesting(const geminipbh::TelemetrySnapshot &snapshot)
1184{
1185 userDisconnectRequested_ = false;
1186 driverState_ = DriverState::Connected;
1187 haveTelemetry_ = false;
1188 pendingOperations_.clear();
1189 callbackStaging_.clear();
1190 defineConnectedProperties();
1191 callbackStaging_.stageTelemetry(snapshot);
1192 flushTelemetryToINDI();
1193}
1194
1195void GeminiPBH::checkTelemetryFreshnessForTesting(std::chrono::steady_clock::time_point now)
1196{
1197 checkTelemetryFreshness(now);
1198}
1199
1200bool GeminiPBH::communicationFailureForTesting() const
1201{
1202 return driverState_ == DriverState::CommunicationFailure;
1203}
1204
1205IPState GeminiPBH::powerSensorsStateForTesting() const
1206{
1207 return PowerSensorsNP.getState();
1208}
1209
1210IPState GeminiPBH::environmentStateForTesting() const
1211{
1212 return EnvironmentNP.getState();
1213}
1214
1215IPState GeminiPBH::powerChannelsStateForTesting() const
1216{
1217 return PowerChannelsSP.getState();
1218}
1219
1220IPState GeminiPBH::dewModeStateForTesting(size_t index) const
1221{
1222 if (index == 0)
1223 return Dew1ModeSP.getState();
1224 if (index == 1)
1225 return Dew2ModeSP.getState();
1226 return IPS_ALERT;
1227}
1228
1229void GeminiPBH::simulateHeaterModePendingForTesting(size_t heaterIndex, geminipbh::HeaterMode targetMode,
1230 std::chrono::steady_clock::time_point deadline)
1231{
1232 auto target = gindi::makeHeaterModeTarget(heaterIndex, targetMode);
1233 if (!target) return;
1234 addPendingOperation(gindi::PendingOperation(gindi::PendingOperationKind::HeaterMode,
1235 heaterIndex,
1236 target.value(),
1237 deadline));
1238}
1239
1240void GeminiPBH::checkPendingOperationsForTesting(const geminipbh::TelemetrySnapshot &snapshot)
1241{
1242 checkPendingOperations(&snapshot);
1243}
1244
1245geminipbh::instrumentation::Level GeminiPBH::instrumentationLevelForTesting() const
1246{
1247 return device_ ? device_->instrumentationLevel() : geminipbh::instrumentation::Level::Off;
1248}
1249
1250void GeminiPBH::syncInstrumentationLevelForTesting()
1251{
1252 syncInstrumentationLevelWithIndiDebug();
1253}
1254
1255void GeminiPBH::setDebugForTesting(bool enable)
1256{
1257 setDebug(enable);
1258}
1259
1260void GeminiPBH::setSerialPortForTesting(const std::string &port)
1261{
1262 updateSerialPortSelection(port);
1263}
1264
1265std::string GeminiPBH::serialPortForTesting() const
1266{
1267 const char *port = serialConnection_ ? serialConnection_->port() : "";
1268 return port ? port : "";
1269}
1270
1271void GeminiPBH::setAutoSearchForTesting(bool enabled)
1272{
1273 autoSearchOverrideForTesting_ = enabled;
1274}
1275
1276void GeminiPBH::setDiscoveryFunctionForTesting(std::function<geminipbh::DiscoveryResult()> fn)
1277{
1278 discoveryFunctionForTesting_ = std::move(fn);
1279}
1280
1281bool GeminiPBH::detectedPortSaveRequestedForTesting() const
1282{
1283 return detectedPortSaveRequestedForTesting_;
1284}
1285
1286#endif
1287
1288void GeminiPBH::checkPendingOperations()
1289{
1290 auto snapshot = callbackStaging_.latestTelemetry();
1291
1292 checkPendingOperations(snapshot ? &*snapshot : nullptr);
1293}
1294
1295void GeminiPBH::checkPendingOperations(const geminipbh::TelemetrySnapshot *snapshot)
1296{
1297 if (pendingOperations_.empty())
1298 return;
1299
1300 const auto now = std::chrono::steady_clock::now();
1301 bool dcAlert = false;
1302 bool usbAlert = false;
1303 bool dewEnabledAlert = false;
1304 std::array<bool, kDewOutputCount> dewModeAlert{};
1305
1306 auto markAlert = [&](gindi::PendingOperationKind kind, size_t index)
1307 {
1308 switch (kind)
1309 {
1310 case gindi::PendingOperationKind::DcOutput:
1311 dcAlert = true;
1312 break;
1313 case gindi::PendingOperationKind::UsbOutput:
1314 usbAlert = true;
1315 break;
1316 case gindi::PendingOperationKind::HeaterEnabled:
1317 dewEnabledAlert = true;
1318 break;
1319 case gindi::PendingOperationKind::HeaterMode:
1320 if (index < kDewOutputCount)
1321 dewModeAlert[index] = true;
1322 break;
1323 }
1324 };
1325
1326 pendingOperations_.erase(std::remove_if(pendingOperations_.begin(), pendingOperations_.end(),
1327 [&](const gindi::PendingOperation & operation)
1328 {
1329 switch (gindi::evaluatePendingOperation(operation, snapshot, now))
1330 {
1331 case gindi::PendingEvaluation::Confirmed:
1332 LOGF_DEBUG("Confirmed %s %zu from telemetry.",
1333 gindi::pendingOperationName(operation.kind), operation.index + 1);
1334 return true;
1335
1336 case gindi::PendingEvaluation::Pending:
1337 case gindi::PendingEvaluation::ValueUnavailable:
1338 return false;
1339
1340 case gindi::PendingEvaluation::Timeout:
1341 LOGF_WARN("Telemetry confirmation timed out for %s %zu.",
1342 gindi::pendingOperationName(operation.kind), operation.index + 1);
1343 markAlert(operation.kind, operation.index);
1344 return true;
1345
1346 case gindi::PendingEvaluation::NotConfirmable:
1347 LOGF_ERROR("Internal driver error: non-confirmable operation was added as pending for %s %zu.",
1348 gindi::pendingOperationName(operation.kind), operation.index + 1);
1349 markAlert(operation.kind, operation.index);
1350 return true;
1351
1352 case gindi::PendingEvaluation::InvalidTarget:
1353 LOGF_ERROR("Internal driver error: invalid pending target for %s %zu.",
1354 gindi::pendingOperationName(operation.kind), operation.index + 1);
1355 markAlert(operation.kind, operation.index);
1356 return true;
1357 }
1358 return false;
1359 }), pendingOperations_.end());
1360
1361 if (dcAlert) PowerChannelsSP.setState(IPS_ALERT);
1362 if (usbAlert) USBPortSP.setState(IPS_ALERT);
1363 if (dewEnabledAlert) DewChannelsSP.setState(IPS_ALERT);
1364 if (dewModeAlert[0]) Dew1ModeSP.setState(IPS_ALERT);
1365 if (dewModeAlert[1]) Dew2ModeSP.setState(IPS_ALERT);
1366
1367 applyPendingAwareLiveStates();
1368 PowerChannelsSP.apply();
1369 USBPortSP.apply();
1370 DewChannelsSP.apply();
1371 Dew1ModeSP.apply();
1372 Dew2ModeSP.apply();
1373}
1374
1375void GeminiPBH::attemptReconnectIfDue()
1376{
1377 if (userDisconnectRequested_ || shutdownInProgress_.load(std::memory_order_acquire)
1378 || driverState_ != DriverState::CommunicationFailure || !device_)
1379 return;
1380
1381 const auto now = std::chrono::steady_clock::now();
1382 if (lastReconnectAttempt_.time_since_epoch().count() != 0 && now - lastReconnectAttempt_ < kReconnectInterval)
1383 return;
1384
1385 driverState_ = DriverState::Reconnecting;
1386 lastReconnectAttempt_ = now;
1387 failPendingOperations(IPS_ALERT);
1388 markHardwareProperties(IPS_ALERT);
1389 LOG_INFO("Attempting Gemini Power Box Hub Advanced v3 reconnect through libgeminipbh.");
1390
1391 const geminipbh::Result result = device_->reconnect();
1392 if (!result)
1393 {
1394 driverState_ = DriverState::CommunicationFailure;
1395 logDeviceError("reconnect", result.error());
1396 markHardwareProperties(IPS_ALERT);
1397 return;
1398 }
1399
1400 driverState_ = DriverState::Connected;
1401 defineConnectedProperties();
1402 flushConnectionEvents();
1403 flushTelemetryToINDI();
1404 refreshPropertiesFromDevice();
1405}
1406
1407void GeminiPBH::markCommunicationFailure(const std::string &reason)
1408{
1409 if (driverState_ == DriverState::CommunicationFailure || userDisconnectRequested_)
1410 return;
1411
1412 driverState_ = DriverState::CommunicationFailure;
1413 haveTelemetry_ = false;
1414 failPendingOperations(IPS_ALERT);
1415
1417 EnvironmentNP.setState(IPS_ALERT);
1422 Dew1ModeSP.setState(IPS_ALERT);
1423 Dew2ModeSP.setState(IPS_ALERT);
1424 DewOutputsNP.setState(IPS_ALERT);
1425
1427 EnvironmentNP.apply();
1429 USBPortSP.apply();
1432 Dew1ModeSP.apply();
1433 Dew2ModeSP.apply();
1434 DewOutputsNP.apply();
1435
1436 LOGF_WARN("Gemini Power Box Hub Advanced v3 communication failure: %s", reason.c_str());
1437}
1438
1439void GeminiPBH::markHardwareProperties(IPState state)
1440{
1441 if (!connectedPropertiesDefined_)
1442 return;
1443
1444 DeviceInfoTP.setState(state);
1445 PowerSensorsNP.setState(state);
1446 EnvironmentNP.setState(state);
1448 USBPortSP.setState(state);
1449 DewChannelsSP.setState(state);
1451 Dew1ModeSP.setState(state);
1452 Dew2ModeSP.setState(state);
1453 DewOutputsNP.setState(state);
1454
1455 DeviceInfoTP.apply();
1457 EnvironmentNP.apply();
1459 USBPortSP.apply();
1462 Dew1ModeSP.apply();
1463 Dew2ModeSP.apply();
1464 DewOutputsNP.apply();
1465}
1466
1467void GeminiPBH::failPendingOperations(IPState state)
1468{
1469 if (pendingOperations_.empty())
1470 return;
1471
1472 pendingOperations_.clear();
1473 if (!connectedPropertiesDefined_)
1474 return;
1475
1477 USBPortSP.setState(state);
1478 DewChannelsSP.setState(state);
1479 Dew1ModeSP.setState(state);
1480 Dew2ModeSP.setState(state);
1481
1483 USBPortSP.apply();
1485 Dew1ModeSP.apply();
1486 Dew2ModeSP.apply();
1487}
1488
1489void GeminiPBH::addPendingOperation(gindi::PendingOperation operation)
1490{
1491 gindi::replacePendingOperation(pendingOperations_, operation);
1492}
1493
1494bool GeminiPBH::hasPendingOperation(gindi::PendingOperationKind kind) const
1495{
1496 return std::any_of(pendingOperations_.begin(), pendingOperations_.end(),
1497 [kind](const gindi::PendingOperation & operation)
1498 {
1499 return operation.kind == kind;
1500 });
1501}
1502
1503bool GeminiPBH::hasPendingOperation(gindi::PendingOperationKind kind, size_t index) const
1504{
1505 return std::any_of(pendingOperations_.begin(), pendingOperations_.end(),
1506 [kind, index](const gindi::PendingOperation & operation)
1507 {
1508 return operation.kind == kind && operation.index == index;
1509 });
1510}
1511
1512void GeminiPBH::applyPendingAwareLiveStates()
1513{
1514 if (!connectedPropertiesDefined_ || driverState_ != DriverState::Connected)
1515 return;
1516
1517 auto setState = [](auto & property, bool pending)
1518 {
1519 if (pending)
1520 property.setState(IPS_BUSY);
1521 else if (property.getState() != IPS_ALERT)
1522 property.setState(IPS_OK);
1523 };
1524
1525 setState(PowerChannelsSP, hasPendingOperation(gindi::PendingOperationKind::DcOutput));
1526 setState(USBPortSP, hasPendingOperation(gindi::PendingOperationKind::UsbOutput));
1527 setState(DewChannelsSP, hasPendingOperation(gindi::PendingOperationKind::HeaterEnabled));
1528 setState(Dew1ModeSP, hasPendingOperation(gindi::PendingOperationKind::HeaterMode, 0));
1529 setState(Dew2ModeSP, hasPendingOperation(gindi::PendingOperationKind::HeaterMode, 1));
1532}
1533
1534void GeminiPBH::logDeviceError(const char *action, geminipbh::Error error) const
1535{
1536 LOGF_ERROR("Gemini Power Box Hub Advanced v3 %s failed: %s.", action, gindi::errorName(error));
1537}
1538
1539void GeminiPBH::refreshLabelsFromTextProperties()
1540{
1541 for (size_t i = 0; i < kDcOutputCount; ++i)
1543 for (size_t i = 0; i < kDewOutputCount; ++i)
1544 {
1546 DewChannelDutyCycleNP[i].setLabel((std::string(DewChannelLabelsTP[i].getText()) + " (%)").c_str());
1547 DewOutputsNP[i].setLabel((std::string(DewChannelLabelsTP[i].getText()) + " Current Output (%)").c_str());
1548 }
1549 for (size_t i = 0; i < kUsbOutputCount; ++i)
1550 USBPortSP[i].setLabel(USBPortLabelsTP[i].getText());
1551}
1552
1554{
1559 return true;
1560}
1561
1562const char *GeminiPBH::heaterModeName(geminipbh::HeaterMode mode)
1563{
1564 return gindi::heaterModeName(mode);
1565}
double humidity
double dewPoint
hid_device * device
The Serial class manages connection with serial devices including Bluetooth. Serial communication is ...
~GeminiPBH() override
bool updateProperties() override
updateProperties is called whenever there is a change in the CONNECTION status of the driver....
bool ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) override
Process the client newSwitch command.
bool Connect() override
Connect to the device. INDI::DefaultDevice implementation connects to appropriate connection interfac...
bool SetDewPort(size_t port, bool enabled, double dutyCycle) override
SetDewPort Set DEW/Dew port duty cycle.
bool SetPowerPort(size_t port, bool enabled) override
SetPowerPort Set power port on/off.
const char * getDefaultName() override
bool ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) override
Process the client newNumber command.
bool ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n) override
Process the client newSwitch command.
bool saveConfigItems(FILE *fp) override
saveConfigItems Save specific properties in the provide config file handler. Child class usually over...
void TimerHit() override
Callback function to be called once SetTimer duration elapses.
bool Disconnect() override
Disconnect from device.
void debugTriggered(bool enable) override
Inform driver that the debug option was triggered. This function is called after setDebug is triggere...
bool initProperties() override
Initialize properties initial state and value. The child class must implement this function.
bool SetUSBPort(size_t port, bool enabled) override
SetUSBPort Set USB port on/off.
bool isConnected() const
const char * getDeviceName() const
INDI::PropertySwitch getSwitch(const char *name) const
void registerProperty(const INDI::Property &property)
Register the property to be able to observe and update.
INDI::PropertyText getText(const char *name) const
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.
void registerConnection(Connection::Interface *newConnection)
registerConnection Add new connection plugin to the existing connection pool. The connection type sha...
void setDefaultPollingPeriod(uint32_t msec)
setDefaultPollingPeriod Change the default polling period to call TimerHit() function in the driver.
void setActiveConnection(Connection::Interface *existingConnection)
setActiveConnection Switch the active connection to the passed connection plugin
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...
void setDebug(bool enable)
Toggle driver debug status A driver can be more verbose if Debug option is enabled by the client.
virtual bool initProperties()
Initialize properties initial state and value. The child class must implement this function.
virtual bool ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n)
Process the client newNumber command.
virtual void debugTriggered(bool enable)
Inform driver that the debug option was triggered. This function is called after setDebug is triggere...
int SetTimer(uint32_t ms)
Set a timer to call the function TimerHit after ms milliseconds.
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.
INDI::PropertySwitch DewChannelsSP
void SetCapability(uint32_t cap)
SetPowerCapability sets the Power capabilities. All capabilities must be initialized.
INDI::PropertyNumber PowerSensorsNP
void initProperties(const char *groupName, size_t nPowerPorts=0, size_t nDewPorts=0, size_t nVariablePorts=0, size_t nAutoDewPorts=0, size_t nUSBPorts=0)
Initialize Power properties. It is recommended to call this function within initProperties() of your ...
INDI::PropertyText PowerChannelLabelsTP
bool processText(const char *dev, const char *name, char *texts[], char *names[], int n)
Process Power text properties.
INDI::PropertySwitch PowerChannelsSP
INDI::PropertySwitch USBPortSP
INDI::PropertyNumber DewChannelDutyCycleNP
INDI::PropertyText USBPortLabelsTP
INDI::PropertyText DewChannelLabelsTP
void setState(IPState state)
void apply(const char *format,...) const ATTRIBUTE_FORMAT_PRINTF(2
void save(FILE *f) const
save Save property to configuration file.
void setLabel(const char *label)
bool isNameMatch(const char *otherName) const
void fill(const char *device, const char *name, const char *label, const char *group, IPerm permission, double timeout, IPState state)
void fill(const char *device, const char *name, const char *label, const char *group, IPerm permission, double timeout, IPState state)
constexpr Error error() const
Definition result.h:69
static constexpr Result failure(Error error)
Definition result.h:56
static constexpr Result success()
Definition result.h:52
static ValueResult failure(Error error)
Definition result.h:86
std::optional< TelemetrySnapshot > takeTelemetry()
bool stageTelemetry(const TelemetrySnapshot &snapshot)
std::optional< TelemetrySnapshot > latestTelemetry() const
std::deque< ConnectionEvent > takeConnectionEvents()
bool stageConnectionEvent(ConnectionEvent event)
std::string format(const Event &event) const
const char * MAIN_CONTROL_TAB
MAIN_CONTROL_TAB Where all the primary controls for the device are located.
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
Implementations for common driver routines.
#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 LOG_ERROR(txt)
Shorter logging macros. In order to use these macros, the function (or method) "getDeviceName()" must...
Definition indilogger.h:72
#define LOGF_ERROR(fmt,...)
Definition indilogger.h:80
#define LOG_INFO(txt)
Definition indilogger.h:74
const char * DEVICE_AUTO_SEARCH
Toggle device auto search. If enabled and on connection failure with the default port,...
const char * DEVICE_PORT
Device serial (or bluetooth) connection port. The default value on Linux is /dev/ttyUSB0 while on Mac...
Namespace to encapsulate INDI client, drivers, and mediator classes.
const char * errorName(Error error)
ValueResult< OperationTarget > makeHeaterModeTarget(std::size_t index, HeaterMode mode)
bool heaterModeFromSwitchName(const char *name, HeaterMode &mode)
std::size_t heaterModeSwitchIndex(HeaterMode mode)
bool instrumentationDuplicatesSemanticDriverLog(const instrumentation::Event &event)
ValueResult< OperationTarget > makeBooleanTarget(PendingOperationKind kind, std::size_t index, bool enabled)
const char * heaterModeName(HeaterMode mode)
void replacePendingOperation(std::vector< PendingOperation > &operations, PendingOperation replacement)
DiscoveryResult discoverGeminiDevices(const DiscoveryOptions &options)
const char * probeStatusName(ProbeStatus status)
#define GEMINIPBH_VERSION_MAJOR
#define GEMINIPBH_VERSION_MINOR
static ISwitch DebugLevelS[nlevels]
Definition indilogger.h:271
static ISwitch LoggingLevelS[nlevels]
Definition indilogger.h:213
static unsigned int customLevel
Definition indilogger.h:222
One switch descriptor.
std::function< bool()> shouldCancel
Definition discovery.h:73
std::vector< ProbeResult > probes
Definition discovery.h:81
FirmwareInfo firmware
Definition discovery.h:65
std::string preferredPort
Definition discovery.h:62
std::string identity
Definition discovery.h:64
const std::array< HeaterMode, HeaterCount > heaterModes
const std::array< bool, UsbOutputCount > usbOutputs
const std::array< double, HeaterCount > heaterOutputPercent
const std::array< bool, DcOutputCount > dcOutputs
const std::array< bool, HeaterCount > heaterEnabled
const std::chrono::steady_clock::time_point timestamp