Instrument Neutral Distributed Interface INDI 2.2.5
wanderer_eta.cpp
Go to the documentation of this file.
1/*******************************************************************************
2 Copyright(c) 2025 cfuture81. All rights reserved.
3
4 Wanderer ETA M54 - Electronic Tilt Adjuster
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 Library General Public License
17 along with this library; see the file COPYING.LIB. If not, write to
18 the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
19 Boston, MA 02110-1301, USA.
20
21 The full GNU General Public License is included in this distribution in the
22 file called LICENSE.
23*******************************************************************************/
24
25#include "wanderer_eta.h"
26#include "indicom.h"
28#include <cstring>
29#include <string>
30#include <sstream>
31#include <vector>
32#include <memory>
33#include <termios.h>
34#include <unistd.h>
35#include <mutex>
36#include <chrono>
37
38static std::unique_ptr<WandererETA> wandererETA(new WandererETA());
39
47
52{
53 return "Wanderer ETA M54";
54}
55
60{
62
65
66 // Motor position targets (user-settable) - separate properties for independent control
67 Position1NP[0].fill("POINT_1", "Position (mm)", "%.3f", 0.000, 1.200, 0.001, 0.000);
68 Position1NP.fill(getDeviceName(), "POINT_1_TARGET", "Point 1 Target", MAIN_CONTROL_TAB, IP_RW, 60, IPS_IDLE);
69
70 Position2NP[0].fill("POINT_2", "Position (mm)", "%.3f", 0.000, 1.200, 0.001, 0.000);
71 Position2NP.fill(getDeviceName(), "POINT_2_TARGET", "Point 2 Target", MAIN_CONTROL_TAB, IP_RW, 60, IPS_IDLE);
72
73 Position3NP[0].fill("POINT_3", "Position (mm)", "%.3f", 0.000, 1.200, 0.001, 0.000);
74 Position3NP.fill(getDeviceName(), "POINT_3_TARGET", "Point 3 Target", MAIN_CONTROL_TAB, IP_RW, 60, IPS_IDLE);
75
76 // Motor position readback (encoder feedback, read-only)
77 PositionReadNP[POINT_1].fill("POINT_1_READ", "Point 1 (mm)", "%.3f", 0.000, 1.200, 0.000, 0.000);
78 PositionReadNP[POINT_2].fill("POINT_2_READ", "Point 2 (mm)", "%.3f", 0.000, 1.200, 0.000, 0.000);
79 PositionReadNP[POINT_3].fill("POINT_3_READ", "Point 3 (mm)", "%.3f", 0.000, 1.200, 0.000, 0.000);
80 PositionReadNP.fill(getDeviceName(), "POSITION_READBACK", "Current Positions", MAIN_CONTROL_TAB, IP_RO, 60, IPS_IDLE);
81
82 // Firmware information
83 FirmwareTP[FIRMWARE_VERSION].fill("FIRMWARE_VERSION", "Firmware Version", "Unknown");
84 FirmwareTP.fill(getDeviceName(), "FIRMWARE_INFO", "Firmware", MAIN_CONTROL_TAB, IP_RO, 60, IPS_IDLE);
85
86 // Zero all points
87 ZeroAllSP[ZERO_ALL].fill("ZERO_ALL", "Zero All Points", ISS_OFF);
88 ZeroAllSP.fill(getDeviceName(), "ZERO_ALL_CMD", "Zero All", MAIN_CONTROL_TAB, IP_RW, ISR_ATMOST1, 60, IPS_IDLE);
89
90 // Backfocus offset - apply uniform offset to all 3 points simultaneously
91 BackfocusOffsetNP[0].fill("OFFSET_MM", "Offset (mm)", "%.3f", -1.200, 1.200, 0.010, 0.000);
92 BackfocusOffsetNP.fill(getDeviceName(), "BACKFOCUS_OFFSET", "Backfocus Offset", MAIN_CONTROL_TAB, IP_RW, 60, IPS_IDLE);
93
94 ApplyOffsetSP[APPLY_OFFSET].fill("APPLY_OFFSET", "Apply Offset", ISS_OFF);
95 ApplyOffsetSP.fill(getDeviceName(), "APPLY_OFFSET_CMD", "Apply Offset", MAIN_CONTROL_TAB, IP_RW, ISR_ATMOST1, 60,
96 IPS_IDLE);
97
99
100 serialConnection = new Connection::Serial(this);
102 serialConnection->registerHandshake([&]()
103 {
104 return getData();
105 });
106 registerConnection(serialConnection);
107
108 return true;
109}
110
115{
117
118 if (isConnected())
119 {
120 char firmwareStr[16];
121 snprintf(firmwareStr, sizeof(firmwareStr), "%d", firmware);
122 FirmwareTP[FIRMWARE_VERSION].setText(firmwareStr);
123 LOGF_INFO("Firmware version: %d", firmware);
124
125 defineProperty(Position1NP);
126 defineProperty(Position2NP);
127 defineProperty(Position3NP);
128 defineProperty(PositionReadNP);
129 defineProperty(FirmwareTP);
130 defineProperty(ZeroAllSP);
131 defineProperty(BackfocusOffsetNP);
132 defineProperty(ApplyOffsetSP);
133 }
134 else
135 {
136 deleteProperty(Position1NP);
137 deleteProperty(Position2NP);
138 deleteProperty(Position3NP);
139 deleteProperty(PositionReadNP);
140 deleteProperty(FirmwareTP);
141 deleteProperty(ZeroAllSP);
142 deleteProperty(BackfocusOffsetNP);
143 deleteProperty(ApplyOffsetSP);
144 }
145
146 return true;
147}
148
152bool WandererETA::getData()
153{
154 if (!serialPortMutex.try_lock_for(std::chrono::milliseconds(500)))
155 {
156 LOG_DEBUG("Serial port is busy, skipping status update");
157 return true;
158 }
159
160 std::lock_guard<std::timed_mutex> lock(serialPortMutex, std::adopt_lock);
161
162 try
163 {
164 PortFD = serialConnection->getPortFD();
165
166 // On first read (handshake), flush stale serial buffer data.
167 // The ETA device streams status continuously (~every 2s); the first bytes after
168 // opening the port may contain garbage from power-on or prior session.
169 // After flushing, we need to wait long enough for a fresh complete status line.
170 if (m_FirstRead)
171 {
172 tcflush(PortFD, TCIOFLUSH);
173 m_FirstRead = false;
174
175 // Try up to 3 reads with 3s timeout each to get a valid status line.
176 // The device streams every ~2s, so worst case we wait one full cycle.
177 char firstBuffer[512] = {0};
178 int firstBytes = 0;
179 for (int attempt = 0; attempt < 3; attempt++)
180 {
181 memset(firstBuffer, 0, sizeof(firstBuffer));
182 firstBytes = 0;
183 int firstRc = tty_read_section(PortFD, firstBuffer, '\n', 3, &firstBytes);
184 if (firstRc == TTY_OK && parseDeviceData(firstBuffer))
185 {
186 LOG_INFO("Wanderer ETA M54 is online.");
187 return true;
188 }
189 // If we got data but it didn't parse (partial/garbage), flush and retry
190 if (firstBytes > 0)
191 tcflush(PortFD, TCIOFLUSH);
192 }
193 LOG_WARN("Handshake: could not get valid data after 3 attempts");
194 return false;
195 }
196
197 char buffer[512] = {0};
198 int nbytes_read = 0, rc = -1;
199
200 if ((rc = tty_read_section(PortFD, buffer, '\n', 2, &nbytes_read)) != TTY_OK)
201 {
202 if (rc == TTY_TIME_OUT)
203 {
204 LOG_DEBUG("Timeout reading from device, will try again later");
205 return true;
206 }
207
208 char errorMessage[MAXRBUF];
209 tty_error_msg(rc, errorMessage, MAXRBUF);
210 LOGF_ERROR("Failed to read data from device. Error: %s", errorMessage);
211 return false;
212 }
213
214 return parseDeviceData(buffer);
215 }
216 catch (std::exception &e)
217 {
218 LOG_ERROR("Exception occurred while reading data from device");
219 return false;
220 }
221
222 return true;
223}
224
228bool WandererETA::parseDeviceData(const char *data)
229{
230 try
231 {
232 std::vector<std::string> tokens;
233 std::string token;
234 std::istringstream tokenStream(data);
235 LOGF_DEBUG("Data: %s", data);
236
237 // Split the data by 'A' separator
238 while (std::getline(tokenStream, token, 'A'))
239 {
240 if (!token.empty())
241 tokens.push_back(token);
242 }
243
244 // Need at least: device_id + firmware + 3 positions
245 if (tokens.size() < 5)
246 {
247 LOGF_DEBUG("Insufficient tokens: %d (need 5)", static_cast<int>(tokens.size()));
248 return false;
249 }
250
251 // Validate device identifier
252 if (tokens[0] != "WandererTilterM54")
253 {
254 // During handshake, garbage on first read is expected — suppress warning
255 if (m_Initializing)
256 LOGF_DEBUG("Handshake: skipping invalid data: %s", tokens[0].c_str());
257 else
258 LOGF_WARN("Unknown device: %s (expected WandererTilterM54)", tokens[0].c_str());
259 return false;
260 }
261
262 // Firmware version
263 firmware = std::atoi(tokens[1].c_str());
264
265 char firmwareStr[16];
266 snprintf(firmwareStr, sizeof(firmwareStr), "%d", firmware);
267 FirmwareTP[FIRMWARE_VERSION].setText(firmwareStr);
268 FirmwareTP.setState(IPS_OK);
269 FirmwareTP.apply();
270
271 // Point positions from encoders
272 double pos1 = std::strtod(tokens[2].c_str(), nullptr);
273 double pos2 = std::strtod(tokens[3].c_str(), nullptr);
274 double pos3 = std::strtod(tokens[4].c_str(), nullptr);
275
276 PositionReadNP[POINT_1].setValue(pos1);
277 PositionReadNP[POINT_2].setValue(pos2);
278 PositionReadNP[POINT_3].setValue(pos3);
279 PositionReadNP.setState(IPS_OK);
280 PositionReadNP.apply();
281
282 return true;
283 }
284 catch (std::exception &e)
285 {
286 LOG_ERROR("Failed to parse device data");
287 return false;
288 }
289}
290
294void WandererETA::setPositionNP(int index, double values[], char *names[], int n, IPState state)
295{
296 switch (index)
297 {
298 case 0:
299 Position1NP.update(values, names, n);
300 Position1NP.setState(state);
301 Position1NP.apply();
302 break;
303 case 1:
304 Position2NP.update(values, names, n);
305 Position2NP.setState(state);
306 Position2NP.apply();
307 break;
308 case 2:
309 Position3NP.update(values, names, n);
310 Position3NP.setState(state);
311 Position3NP.apply();
312 break;
313 }
314}
315
319void WandererETA::setPositionState(int index, IPState state)
320{
321 switch (index)
322 {
323 case 0:
324 Position1NP.setState(state);
325 Position1NP.apply();
326 break;
327 case 1:
328 Position2NP.setState(state);
329 Position2NP.apply();
330 break;
331 case 2:
332 Position3NP.setState(state);
333 Position3NP.apply();
334 break;
335 }
336}
337
341bool WandererETA::ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n)
342{
343 if (dev && !strcmp(dev, getDeviceName()))
344 {
345 // Handle backfocus offset value change (just stores the value)
346 if (BackfocusOffsetNP.isNameMatch(name))
347 {
348 BackfocusOffsetNP.update(values, names, n);
349 BackfocusOffsetNP.setState(IPS_OK);
350 BackfocusOffsetNP.apply();
351 return true;
352 }
353
354 // Determine which point is being commanded
355 int pointIndex = -1;
356
357 if (Position1NP.isNameMatch(name))
358 pointIndex = 0;
359 else if (Position2NP.isNameMatch(name))
360 pointIndex = 1;
361 else if (Position3NP.isNameMatch(name))
362 pointIndex = 2;
363
364 if (pointIndex >= 0)
365 {
366 double target = values[0];
367
368 // During initialization (config load), just store values without moving
369 if (m_Initializing)
370 {
371 setPositionNP(pointIndex, values, names, n, IPS_OK);
372 return true;
373 }
374
375 // Validate
376 if (target < 0.0 || target > 1.2)
377 {
378 LOGF_ERROR("Position value %.3f out of range (0.000 - 1.200 mm)", target);
379 setPositionNP(pointIndex, values, names, n, IPS_ALERT);
380 return true;
381 }
382
383 // Don't allow new moves while one is in progress
384 if (m_Move.active)
385 {
386 LOG_WARN("A move is already in progress. Please wait for it to complete.");
387 setPositionState(pointIndex, IPS_IDLE);
388 return true;
389 }
390
391 setPositionNP(pointIndex, values, names, n, IPS_BUSY);
392 startMove(pointIndex, target);
393 return true;
394 }
395 }
396
397 return INDI::DefaultDevice::ISNewNumber(dev, name, values, names, n);
398}
399
403bool WandererETA::ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n)
404{
405 if (dev && !strcmp(dev, getDeviceName()))
406 {
407 // Apply backfocus offset to all 3 points equally
408 if (ApplyOffsetSP.isNameMatch(name))
409 {
410 if (m_Move.active)
411 {
412 LOG_WARN("A move is already in progress. Please wait for it to complete.");
413 return true;
414 }
415
416 double offset = BackfocusOffsetNP[0].getValue();
417
418 if (std::abs(offset) < 0.001)
419 {
420 LOG_WARN("Offset is zero, nothing to apply");
421 ApplyOffsetSP.setState(IPS_IDLE);
422 ApplyOffsetSP[APPLY_OFFSET].setState(ISS_OFF);
423 ApplyOffsetSP.apply();
424 return true;
425 }
426
427 // Read current positions from readback
428 double pos1 = PositionReadNP[POINT_1].getValue();
429 double pos2 = PositionReadNP[POINT_2].getValue();
430 double pos3 = PositionReadNP[POINT_3].getValue();
431
432 // Calculate new targets
433 double targets[3] = {pos1 + offset, pos2 + offset, pos3 + offset};
434
435 // Validate all within range
436 if (targets[0] < 0.0 || targets[0] > 1.2 ||
437 targets[1] < 0.0 || targets[1] > 1.2 ||
438 targets[2] < 0.0 || targets[2] > 1.2)
439 {
440 LOGF_ERROR("Offset %.3f mm would move one or more points out of range (0.000 - 1.200 mm). "
441 "Current positions: P1=%.3f, P2=%.3f, P3=%.3f",
442 offset, pos1, pos2, pos3);
443 ApplyOffsetSP.setState(IPS_ALERT);
444 ApplyOffsetSP[APPLY_OFFSET].setState(ISS_OFF);
445 ApplyOffsetSP.apply();
446 return true;
447 }
448
449 LOGF_INFO("Applying backfocus offset %.3f mm to all points (P1: %.3f→%.3f, P2: %.3f→%.3f, P3: %.3f→%.3f)",
450 offset, pos1, targets[0], pos2, targets[1], pos3, targets[2]);
451
452 ApplyOffsetSP.setState(IPS_BUSY);
453 ApplyOffsetSP.apply();
454 startMultiPointMove(targets, false, true);
455 return true;
456 }
457
458 if (ZeroAllSP.isNameMatch(name))
459 {
460 if (m_Move.active)
461 {
462 LOG_WARN("A move is already in progress. Please wait for it to complete.");
463 return true;
464 }
465
466 double targets[3] = {0.0, 0.0, 0.0};
467 LOG_INFO("Moving all points to zero position");
468
469 ZeroAllSP.setState(IPS_BUSY);
470 ZeroAllSP.apply();
471 startMultiPointMove(targets, true, false);
472 return true;
473 }
474 }
475
476 return INDI::DefaultDevice::ISNewSwitch(dev, name, states, names, n);
477}
478
482void WandererETA::startMove(int pointIndex, double target)
483{
484 m_Move.active = true;
485 m_Move.isZeroAll = false;
486 m_Move.isApplyOffset = false;
487 m_Move.target[pointIndex] = target;
488 m_Move.pending[0] = m_Move.pending[1] = m_Move.pending[2] = false;
489 m_Move.pending[pointIndex] = true;
490 m_Move.graceCount = 3;
491
492 // Calculate timeout: travel distance / motor speed + safety margin
493 double distance = std::abs(target - PositionReadNP[pointIndex].getValue());
494 m_Move.timeoutCount[pointIndex] = static_cast<int>((distance / MOTOR_SPEED_MMS + TIMEOUT_SAFETY_S) / 2.0);
495 m_Move.startPosition[pointIndex] = PositionReadNP[pointIndex].getValue();
496
497 sendPointCommand(pointIndex, target);
498 LOGF_INFO("Moving Point %d to %.3f mm", pointIndex + 1, target);
499}
500
504void WandererETA::startMultiPointMove(double targets[3], bool isZeroAll, bool isApplyOffset)
505{
506 m_Move.active = true;
507 m_Move.isZeroAll = isZeroAll;
508 m_Move.isApplyOffset = isApplyOffset;
509 m_Move.currentPoint = 0;
510 m_Move.graceCount = 3;
511
512 for (int i = 0; i < 3; i++)
513 {
514 m_Move.target[i] = targets[i];
515 m_Move.pending[i] = true;
516 m_Move.startPosition[i] = PositionReadNP[i].getValue();
517 double distance = std::abs(targets[i] - m_Move.startPosition[i]);
518 m_Move.timeoutCount[i] = static_cast<int>((distance / MOTOR_SPEED_MMS + TIMEOUT_SAFETY_S) / 2.0);
519 }
520
521 // Set all target properties to BUSY
522 setPositionState(0, IPS_BUSY);
523 setPositionState(1, IPS_BUSY);
524 setPositionState(2, IPS_BUSY);
525
526 // Send command for first point
527 sendPointCommand(0, targets[0]);
528 LOGF_INFO("Moving Point 1 to %.3f mm", targets[0]);
529}
530
534void WandererETA::sendPointCommand(int pointIndex, double target)
535{
536 char cmd[32];
537 snprintf(cmd, sizeof(cmd), "%d%.3f\n", pointIndex + 1, target);
538
539 // Flush input buffer to ensure clean command delivery.
540 // The device streams status continuously; we need a clear window to send.
541 tcflush(PortFD, TCIFLUSH);
542 usleep(50000); // 50ms settling time
543
544 int nbytes_written = 0, rc = -1;
545 if ((rc = tty_write_string(PortFD, cmd, &nbytes_written)) != TTY_OK)
546 {
547 char errorMessage[MAXRBUF];
548 tty_error_msg(rc, errorMessage, MAXRBUF);
549 LOGF_ERROR("Serial write error on Point %d: %s", pointIndex + 1, errorMessage);
550 m_Move.pending[pointIndex] = false;
551 setPositionState(pointIndex, IPS_ALERT);
552 }
553 tcdrain(PortFD);
554}
555
559void WandererETA::checkMoveProgress()
560{
561 if (!m_Move.active)
562 return;
563
564 // Grace period: skip first few polls after a command to allow device to respond
565 if (m_Move.graceCount > 0)
566 {
567 m_Move.graceCount--;
568 return;
569 }
570
571 bool allDone = true;
572
573 for (int i = 0; i < 3; i++)
574 {
575 if (!m_Move.pending[i])
576 continue;
577
578 allDone = false;
579 double currentPos = PositionReadNP[i].getValue();
580
581 // Check if target reached (device reported updated position after motor stopped)
582 if (std::abs(currentPos - m_Move.target[i]) < POSITION_TOLERANCE)
583 {
584 m_Move.pending[i] = false;
585 setPositionState(i, IPS_OK);
586 LOGF_INFO("Point %d reached target %.3f mm", i + 1, m_Move.target[i]);
587
588 // For multi-point sequential moves: start next point
589 if ((m_Move.isZeroAll || m_Move.isApplyOffset) && i == m_Move.currentPoint)
590 {
591 m_Move.currentPoint++;
592 if (m_Move.currentPoint < 3 && m_Move.pending[m_Move.currentPoint])
593 {
594 sendPointCommand(m_Move.currentPoint, m_Move.target[m_Move.currentPoint]);
595 m_Move.graceCount = 3;
596 double dist = std::abs(m_Move.target[m_Move.currentPoint] - m_Move.startPosition[m_Move.currentPoint]);
597 m_Move.timeoutCount[m_Move.currentPoint] = static_cast<int>((dist / MOTOR_SPEED_MMS + TIMEOUT_SAFETY_S) / 2.0);
598 LOGF_INFO("Moving Point %d to %.3f mm", m_Move.currentPoint + 1,
599 m_Move.target[m_Move.currentPoint]);
600 return;
601 }
602 }
603 continue;
604 }
605
606 // Timeout: count down polls until expected travel time is exceeded
607 if (m_Move.timeoutCount[i] > 0)
608 {
609 m_Move.timeoutCount[i]--;
610 continue;
611 }
612
613 // Timeout expired — motor should be done; accept current position
614 double finalPos = PositionReadNP[i].getValue();
615 m_Move.pending[i] = false;
616 if (std::abs(finalPos - m_Move.target[i]) < POSITION_TOLERANCE)
617 {
618 setPositionState(i, IPS_OK);
619 LOGF_INFO("Point %d reached target %.3f mm", i + 1, m_Move.target[i]);
620 }
621 else
622 {
623 LOGF_WARN("Point %d timeout: reported %.3f mm (target %.3f mm) — motor likely completed, verify by reconnecting",
624 i + 1, finalPos, m_Move.target[i]);
625 setPositionState(i, IPS_OK);
626 }
627
628 // For multi-point: continue with next point
629 if ((m_Move.isZeroAll || m_Move.isApplyOffset) && i == m_Move.currentPoint)
630 {
631 m_Move.currentPoint++;
632 if (m_Move.currentPoint < 3 && m_Move.pending[m_Move.currentPoint])
633 {
634 sendPointCommand(m_Move.currentPoint, m_Move.target[m_Move.currentPoint]);
635 m_Move.graceCount = 3;
636 double dist = std::abs(m_Move.target[m_Move.currentPoint] - m_Move.startPosition[m_Move.currentPoint]);
637 m_Move.timeoutCount[m_Move.currentPoint] = static_cast<int>((dist / MOTOR_SPEED_MMS + TIMEOUT_SAFETY_S) / 2.0);
638 LOGF_INFO("Moving Point %d to %.3f mm", m_Move.currentPoint + 1,
639 m_Move.target[m_Move.currentPoint]);
640 return;
641 }
642 }
643 }
644
645 // Check if all points are done
646 if (allDone || (!m_Move.pending[0] && !m_Move.pending[1] && !m_Move.pending[2]))
647 {
648 m_Move.active = false;
649
650 if (m_Move.isZeroAll)
651 {
652 updateAllTargets(m_Move.target);
653 LOG_INFO("All points moved to zero position successfully");
654 ZeroAllSP.setState(IPS_OK);
655 ZeroAllSP[ZERO_ALL].setState(ISS_OFF);
656 ZeroAllSP.apply();
657 }
658 else if (m_Move.isApplyOffset)
659 {
660 updateAllTargets(m_Move.target);
661 BackfocusOffsetNP[0].setValue(0.0);
662 BackfocusOffsetNP.setState(IPS_OK);
663 BackfocusOffsetNP.apply();
664 LOGF_INFO("Backfocus offset applied successfully. New positions: P1=%.3f, P2=%.3f, P3=%.3f",
665 m_Move.target[0], m_Move.target[1], m_Move.target[2]);
666 ApplyOffsetSP.setState(IPS_OK);
667 ApplyOffsetSP[APPLY_OFFSET].setState(ISS_OFF);
668 ApplyOffsetSP.apply();
669 }
670 }
671}
672
676void WandererETA::updateAllTargets(double targets[3])
677{
678 Position1NP[0].setValue(targets[0]);
679 Position1NP.setState(IPS_OK);
680 Position1NP.apply();
681 Position2NP[0].setValue(targets[1]);
682 Position2NP.setState(IPS_OK);
683 Position2NP.apply();
684 Position3NP[0].setValue(targets[2]);
685 Position3NP.setState(IPS_OK);
686 Position3NP.apply();
687}
688
692bool WandererETA::sendCommand(std::string command)
693{
694 std::lock_guard<std::timed_mutex> lock(serialPortMutex);
695
696 int nbytes_written = 0, rc = -1;
697 std::string command_termination = "\n";
698 LOGF_DEBUG("CMD: %s", command.c_str());
699
700 // Flush any pending input data before sending command.
701 // The device streams status continuously; we need a clean state.
702 tcflush(PortFD, TCIFLUSH);
703
704 if ((rc = tty_write_string(PortFD, (command + command_termination).c_str(), &nbytes_written)) != TTY_OK)
705 {
706 char errorMessage[MAXRBUF];
707 tty_error_msg(rc, errorMessage, MAXRBUF);
708 LOGF_ERROR("Serial write error: %s", errorMessage);
709 return false;
710 }
711
712 // Ensure the data is fully transmitted before releasing the port
713 tcdrain(PortFD);
714
715 return true;
716}
717
722{
723 if (!isConnected())
724 {
726 return;
727 }
728
729 // Clear initialization flag on first timer tick — by now config loading is complete
730 if (m_Initializing)
731 m_Initializing = false;
732
733 getData();
734
735 // Check if any moves are in progress and update their status
736 checkMoveProgress();
737
739}
740
745{
747 Position1NP.save(fp);
748 Position2NP.save(fp);
749 Position3NP.save(fp);
750 return true;
751}
void registerHandshake(std::function< bool()> callback)
registerHandshake Register a handshake function to be called once the initial connection to the devic...
The Serial class manages connection with serial devices including Bluetooth. Serial communication is ...
void setDefaultBaudRate(BaudRate newRate)
setDefaultBaudRate Set default baud rate. The default baud rate is 9600 unless otherwise changed by t...
bool isConnected() const
const char * getDeviceName() const
virtual bool updateProperties()
updateProperties is called whenever there is a change in the CONNECTION status of the driver....
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 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.
uint32_t getPollingPeriod() const
getPollingPeriod Return the polling period.
void defineProperty(INumberVectorProperty *property)
virtual bool saveConfigItems(FILE *fp)
saveConfigItems Save specific properties in the provide config file handler. Child class usually over...
virtual bool initProperties()
Initialize properties initial state and value. The child class must implement this function.
void addAuxControls()
Add Debug, Simulation, and Configuration options to the driver.
virtual bool ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n)
Process the client newNumber command.
int SetTimer(uint32_t ms)
Set a timer to call the function TimerHit after ms milliseconds.
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.
bool isNameMatch(const char *otherName) const
bool update(const double values[], const char *const names[], int n)
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, ISRule rule, double timeout, IPState state)
void fill(const char *device, const char *name, const char *label, const char *group, IPerm permission, double timeout, IPState state)
virtual bool saveConfigItems(FILE *fp) override
Save config items.
virtual bool ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) override
Handle new number values (position targets)
WandererETA()
Constructor.
const char * getDefaultName() override
Device name.
virtual bool initProperties() override
Init properties.
virtual void TimerHit() override
Timer callback - poll device status.
virtual bool updateProperties() override
Update properties on connect/disconnect.
virtual bool ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) override
Handle new switch values (Zero All)
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
@ 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_ATMOST1
Definition indiapi.h:172
int tty_read_section(int fd, char *buf, char stop_char, int timeout, int *nbytes_read)
read buffer from terminal with a delimiter
Definition indicom.c:653
int tty_write_string(int fd, const char *buf, int *nbytes_written)
Writes a null terminated string to fd.
Definition indicom.c:473
void tty_error_msg(int err_code, char *err_msg, int err_msg_len)
Retrieve the tty error message.
Definition indicom.c:1243
Implementations for common driver routines.
@ TTY_OK
Definition indicom.h:54
@ TTY_TIME_OUT
Definition indicom.h:58
#define MAXRBUF
Definition indidriver.c:52
#define LOGF_INFO(fmt,...)
Definition indilogger.h:82
#define LOG_DEBUG(txt)
Definition indilogger.h:75
#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
std::vector< uint8_t > buffer
uint8_t cmd[4]
Definition pwc-ioctl.h:2