Instrument Neutral Distributed Interface INDI 2.2.5
protocol.h
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#pragma once
25
26#include <atomic>
27#include <chrono>
28#include <condition_variable>
29#include <cstdint>
30#include <deque>
31#include <functional>
32#include <memory>
33#include <mutex>
34#include <string>
35#include <thread>
36#include <utility>
37#include <vector>
38#include <sys/types.h>
43
44namespace geminipbh
45{
46
48{
49 public:
50 using TelemetryCallback = std::function<void(const TelemetrySnapshot &)>;
51 using TelemetryCallbackHandle = std::uint64_t;
52 using ConnectionCallback = std::function<void(ConnectionEvent)>;
53 using ConnectionCallbackHandle = std::uint64_t;
56
65
68
69 // Open the serial port at 19200 8N1, enable RTS/DTR. settleMs is an optional
70 // delay (ms) after open() to let the bridge stabilize.
71 bool open(const std::string &port, int settleMs = 0);
72 bool open(const std::string &port, int settleMs, bool exclusive);
73 void close();
74 bool isOpen() const;
75 int lastOpenErrno() const;
76 bool lastOpenWasBusy() const;
77
78 // Connect: full H → V → G → first-frame sequence with 8 s overall budget.
79 // On success, the reader and writer threads are running and the cached
80 // status reflects the first telemetry frame. On failure, no threads are
81 // running and fd_ is closed.
82 // Returns 0 on success; non-zero on failure. Diagnostics are delivered to
83 // the optional diagnostic callback when configured.
84 // 2 = handshake failed
85 // 3 = firmware < 308 (rejected)
86 // 4 = first G frame not received in time
87 // 5 = open failed
88 int connect(const std::string &port, int settleMs = 0);
89
90 // Reconnect: same as connect() but assumes an existing fd_. Closes fd_,
91 // joins both worker threads, opens a new fd, runs the full sequence,
92 // and respawns worker threads. Retries up to 3 times with delays 0, 1 s,
93 // 2 s. Returns 0 on success; non-zero on final failure.
94 int reconnect();
95
96 // Disconnect: signal both threads to stop, join, close fd. Used by SIGINT
97 // and by callers wanting a clean teardown.
98 void disconnect();
99
100 bool handshake();
101 bool readFirmware(std::string &raw, int &numeric);
102 DirectQueryStatus queryIdentity(std::string &identity, int timeoutMs,
103 std::string *detail = nullptr);
104 DirectQueryStatus queryFirmware(std::string &raw, int &numeric, int timeoutMs,
105 std::string *detail = nullptr);
106 bool firmwareVersion(std::string &raw, int &numeric) const;
107
108 // Cached status snapshot. Returns true if at least one frame has been
109 // parsed; false otherwise. The caller-provided StatusFrame is filled
110 // from the cached state.
111 bool latestStatus(StatusFrame &out, unsigned long long &frameSeq);
112
113 // Wait until a frame with seq > minSeq has been received, or timeout
114 // elapses. Returns true if the condition was met, false on timeout.
115 bool waitForFrameAfter(unsigned long long minSeq, int timeoutMs);
116 bool waitForFrameAfter(unsigned long long minSeq, int timeoutMs,
117 StatusFrame &out, unsigned long long &frameSeq);
118
119 using StatusVerifier = std::function<bool(const StatusFrame &)>;
120 bool waitForVerifiedState(unsigned long long preWriteSeq,
121 StatusVerifier verifier,
122 int timeoutMs,
123 std::string &error,
124 unsigned long long &verifiedSeq);
125
126 // Telemetry callbacks receive immutable semantic snapshots after decoded
127 // status frames have been stored in the cache. See docs/api.md for the
128 // callback threading and lifetime contract.
131
132 // Connection callbacks receive semantic connection events. See docs/api.md
133 // for the callback threading and lifetime contract.
136
142
143 // Synchronous output / dew command setters. Used by short-lived CLI
144 // commands. They acquire writeMutex_ and return only after the complete
145 // command frame has been transmitted or a hard write error occurs.
146 // Returns 0 on success, 1 on invalid argument, 4 on write failure.
147 int setOutputSync(unsigned firmwareIndex, bool enabled);
148 int setDewModeSync(unsigned channel, DewMode mode);
149 int setDewManualPercentSync(unsigned channel, unsigned percent);
150 int setDewEnabledSync(unsigned channel, bool enabled);
151
152 // Asynchronous output / dew command setters. Used by --follow mode and
153 // future INDI integration. They enqueue into the writer FIFO and return
154 // before transmission.
155 // Returns 0 on success, 1 on invalid argument, 2 if the queue is full,
156 // 3 if writes are not currently accepted (shutdown/reconnect in progress).
157 int setOutput(unsigned firmwareIndex, bool enabled);
158 int setDewMode(unsigned channel, DewMode mode);
159 int setDewManualPercent(unsigned channel, unsigned percent);
160 int setDewEnabled(unsigned channel, bool enabled);
161
162 // Recovery: enqueue ">G#" into the writer queue so the firmware re-arms
163 // its 3-second broadcast. Returns 0 on success, 2 if the queue is full,
164 // 3 if writes are not currently accepted.
165 int recoverStream();
166
167 // Optional cancellation hook used by the CLI supervisor during reconnect.
168 // Must be callable from normal thread context; never invoked from signal
169 // handlers.
170 void setCancelFunction(std::function<bool()> fn)
171 {
172 cancelFn_ = std::move(fn);
173 }
174
175#ifdef GEMINI_TESTING
176 void setWriteFunctionForTesting(std::function<ssize_t(int, const void *, size_t)> fn)
177 {
178 writeFn_ = std::move(fn);
179 }
180 void setFdForTesting(int fd)
181 {
182 fd_ = fd;
183 }
184 int syncWriteForTesting(const std::string &command);
185 size_t queuedCommandCountForTesting();
186 void injectStatusForTesting(const StatusFrame &status);
187 void emitConnectionEventForTesting(ConnectionEvent event);
188 bool parseFirmwarePayloadForTesting(const std::string &frame, std::string &raw, int &numeric) const;
189#endif
190
191 // Deterministic parser: consumes a payload that already has the 2-char
192 // response prefix removed. Public so it can be unit-tested without
193 // opening a serial port.
194 bool parseStatusPayload(const std::string &payload,
195 StatusFrame &status,
196 std::string &error) const;
197
198 // Public for the supervisor / test code. The supervisor main thread
199 // observes these:
200 std::atomic<bool> running_{true}; // false on SIGINT (permanent shutdown)
201 std::atomic<bool> sessionStopRequested_{false}; // true on reconnect decision
202 std::atomic<bool> reconnectPending_{false}; // supervisor-only signal
203 std::atomic<bool> recoveryAttempted_{false}; // supervisor-only signal
204 std::atomic<bool> acceptingWrites_{false}; // false during shutdown/reconnect
205
206 private:
207 struct TelemetryCallbackEntry;
208 struct ConnectionCallbackEntry;
209
210 int fd_ = -1;
211 int lastOpenErrno_ = 0;
212 bool lastOpenBusy_ = false;
213 int wakeReadFd_ = -1;
214 int wakeWriteFd_ = -1;
215 std::string lastPort_;
216 int lastSettleMs_ = 0;
217 std::atomic<bool> connected_{false};
218 std::atomic<bool> communicationFailureNotified_{false};
219 std::atomic<std::uint64_t> connectionGeneration_{0};
220 mutable instrumentation::Dispatcher instrumentation_;
221
222 mutable std::mutex firmwareMutex_;
223 std::string firmwareVersionRaw_;
224 int firmwareVersionNumeric_ = 0;
225 bool haveFirmwareVersion_ = false;
226
227 // Cached state
228 std::mutex cacheMutex_;
229 std::condition_variable frameCv_;
230 StatusFrame cachedStatus_{};
231 unsigned long long frameSeq_{0};
232 std::chrono::steady_clock::time_point lastFrameTime_{};
233
234 // Worker threads (only alive while connected)
235 std::thread reader_;
236 std::thread writer_;
237
238 // Write queue (only used in --follow / INDI-style mode)
239 std::mutex queueMutex_;
240 std::condition_variable queueCv_;
241 std::deque<std::string> queue_;
242 static constexpr size_t kQueueCap = 16;
243
244 // Mutex serializing any write to fd_
245 std::mutex writeMutex_;
246 std::function<ssize_t(int, const void *, size_t)> writeFn_;
247 std::function<bool()> cancelFn_;
248
249 std::mutex telemetryCallbackMutex_;
250 std::mutex callbackDispatchMutex_;
251 std::condition_variable callbackDispatchCv_;
252 bool callbackDispatchInProgress_ = false;
253 std::vector<std::shared_ptr<TelemetryCallbackEntry>> telemetryCallbacks_;
254 TelemetryCallbackHandle nextTelemetryCallbackHandle_ = 1;
255
256 std::mutex connectionCallbackMutex_;
257 std::vector<std::shared_ptr<ConnectionCallbackEntry>> connectionCallbacks_;
258 ConnectionCallbackHandle nextConnectionCallbackHandle_ = 1;
259
260 // Direct (synchronous) one-shot helpers used during connect()/reconnect()
261 // when no worker threads are running. timeoutMs is a wall-clock budget.
262 bool transactOnce(const std::string &command, std::string &response,
263 int timeoutMs);
264 DirectQueryStatus queryTokenFrame(const std::string &command,
265 char token,
266 int timeoutMs,
267 std::string &response,
268 std::string *detail);
269 bool readFrameOnce(std::string &response, int timeoutMs);
270 bool readTokenFrameOnce(char token, std::string &response, int timeoutMs);
271 bool parseFirmwareFrame(const std::string &frame, std::string &raw, int &numeric) const;
272 int connectInternal(const std::string &port, int settleMs, ConnectionEvent successEvent);
273 bool cancelled() const;
274 bool sleepCancelable(int milliseconds) const;
275
276 // Worker thread bodies
277 void readerLoop(int serialFd, int wakeFd);
278 void writerLoop();
279
280 TelemetrySnapshot storeStatusFrame(const StatusFrame &status, bool dispatchCallbacks);
281 TelemetrySnapshot makeTelemetrySnapshot(const StatusFrame &status,
282 unsigned long long sequence,
283 std::chrono::steady_clock::time_point timestamp) const;
284 void beginCallbackDispatch();
285 void endCallbackDispatch();
286 void dispatchTelemetrySnapshot(const TelemetrySnapshot &snapshot);
287 void dispatchConnectionEvent(ConnectionEvent event, const std::string &reason = std::string());
288 void notifyCommunicationFailure(const std::string &reason);
289
290 // Under writeMutex_, send the raw command. For `>...#`, `\r\n` is added.
291 // Handles partial writes and retries EINTR until the frame is fully
292 // transmitted or a hard write error occurs.
293 bool writeAllLocked(const std::string &command);
294
295 int syncCommand(const std::string &command);
296
297 // Enqueue without dropping oldest. Returns 0 on success, 2 on full.
298 int enqueueCommand(const std::string &command);
299
300 void drainInputLocked();
301 bool openWakePipe();
302 void closeWakePipe();
303 void signalReaderWake();
304 void drainReaderWake(int wakeFd);
305
306 void emitInstrumentationEvent(instrumentation::Event event) const;
307 void emitConnectionInstrumentation(instrumentation::ConnectionState state,
309 const std::string &reason = std::string()) const;
310 void emitFrameInstrumentation(instrumentation::FrameDirection direction,
311 const std::string &frame) const;
312 void emitWarningInstrumentation(instrumentation::Level level,
314 const std::string &message) const;
315 void emitParserAnomalyInstrumentation(instrumentation::Level level,
317 const std::string &detail,
318 const std::string &frame = std::string()) const;
319 void emitReconnectInstrumentation(unsigned attempt,
320 unsigned maxAttempts,
321 std::chrono::milliseconds delayBeforeAttempt,
322 bool finalAttempt,
324 void emitTimingInstrumentation(instrumentation::TimingOperation operation,
325 std::chrono::steady_clock::time_point start,
326 bool success) const;
327 void emitVerificationInstrumentation(instrumentation::VerificationOutcome outcome,
328 const std::string &detail,
329 unsigned long long relatedSequence) const;
330};
331
332} // namespace geminipbh
bool latestStatus(StatusFrame &out, unsigned long long &frameSeq)
Definition protocol.cpp:876
void setInstrumentationLevel(instrumentation::Level level)
Definition protocol.cpp:213
InstrumentationListenerHandle registerInstrumentationListener(InstrumentationListener listener)
Definition protocol.cpp:228
int setDewMode(unsigned channel, DewMode mode)
std::atomic< bool > recoveryAttempted_
Definition protocol.h:203
void setCancelFunction(std::function< bool()> fn)
Definition protocol.h:170
bool waitForFrameAfter(unsigned long long minSeq, int timeoutMs)
bool firmwareVersion(std::string &raw, int &numeric) const
Definition protocol.cpp:714
bool readFirmware(std::string &raw, int &numeric)
Definition protocol.cpp:709
DirectQueryStatus queryIdentity(std::string &identity, int timeoutMs, std::string *detail=nullptr)
Definition protocol.cpp:657
int setDewModeSync(unsigned channel, DewMode mode)
bool removeInstrumentationListener(InstrumentationListenerHandle handle)
Definition protocol.cpp:234
int setDewManualPercentSync(unsigned channel, unsigned percent)
std::atomic< bool > reconnectPending_
Definition protocol.h:202
int setOutputSync(unsigned firmwareIndex, bool enabled)
DirectQueryStatus queryFirmware(std::string &raw, int &numeric, int timeoutMs, std::string *detail=nullptr)
Definition protocol.cpp:681
instrumentation::Level instrumentationLevel() const
Definition protocol.cpp:218
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
Definition protocol.h:119
bool open(const std::string &port, int settleMs=0)
Definition protocol.cpp:92
bool lastOpenWasBusy() const
Definition protocol.cpp:208
instrumentation::Dispatcher::Listener InstrumentationListener
Definition protocol.h:54
bool parseStatusPayload(const std::string &payload, StatusFrame &status, std::string &error) const
Definition protocol.cpp:818
std::uint64_t TelemetryCallbackHandle
Definition protocol.h:51
std::atomic< bool > acceptingWrites_
Definition protocol.h:204
int setDewEnabledSync(unsigned channel, bool enabled)
TelemetryCallbackHandle registerTelemetryCallback(TelemetryCallback callback)
Definition protocol.cpp:885
bool removeTelemetryCallback(TelemetryCallbackHandle handle)
Definition protocol.cpp:896
std::function< void(ConnectionEvent)> ConnectionCallback
Definition protocol.h:52
std::atomic< bool > sessionStopRequested_
Definition protocol.h:201
std::atomic< bool > running_
Definition protocol.h:200
int setDewEnabled(unsigned channel, bool enabled)
std::function< void(const TelemetrySnapshot &)> TelemetryCallback
Definition protocol.h:50
instrumentation::Dispatcher::ListenerHandle InstrumentationListenerHandle
Definition protocol.h:55
std::uint64_t ConnectionCallbackHandle
Definition protocol.h:53
bool instrumentationEnabled(instrumentation::Level level) const
Definition protocol.cpp:223
bool removeConnectionCallback(ConnectionCallbackHandle handle)
Definition protocol.cpp:930
ConnectionCallbackHandle registerConnectionCallback(ConnectionCallback callback)
Definition protocol.cpp:919
int connect(const std::string &port, int settleMs=0)
std::function< void(const Event &)> Listener
int fd