Instrument Neutral Distributed Interface INDI 2.2.5
wanderer_cover_v4_pro_ec.cpp
Go to the documentation of this file.
1/*******************************************************************************
2 Copyright(c) 2024 Frank Wang/Jérémie Klein. All rights reserved.
3
4 WandererCover V4 Pro-EC
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
26#include "indicom.h"
28#include <cstring>
29#include <string>
30#include <memory>
31#include <termios.h>
32#include <unistd.h>
33#include <sys/ioctl.h>
34#include <mutex>
35#include <chrono>
36#include <sstream>
37#include <algorithm>
38
39static std::unique_ptr<WandererCoverV4ProEC> wanderercoverv4ec(new WandererCoverV4ProEC());
40
41// Protocol implementations
42bool WandererCoverLegacyProtocol::supportsFeature(const std::string& feature) const
43{
44 // Legacy protocol supports basic features only
45 static const std::vector<std::string> supportedFeatures = {
46 "cover_control", "light_control", "position_setting"
47 };
48 return std::find(supportedFeatures.begin(), supportedFeatures.end(), feature) != supportedFeatures.end();
49}
50
52{
53 // Parse firmware version from data
54 std::string dataStr(data);
55 std::vector<std::string> tokens;
56 std::string token;
57 std::istringstream tokenStream(dataStr);
58
59 while (std::getline(tokenStream, token, 'A'))
60 {
61 if (!token.empty())
62 tokens.push_back(token);
63 }
64
65 // Check if we have enough tokens and the first one is correct
66 if (tokens.size() < 2)
67 return false;
68
69 // Enhanced device detection for legacy protocol
70 std::string deviceName = tokens[0];
71
72 // Check for exact match first
73 if (deviceName == "WandererCoverV4Pro")
74 {
75 // Parse firmware version
76 int firmwareVersion = std::atoi(tokens[1].c_str());
77
78 // Legacy protocol for firmware < 20250405
79 return firmwareVersion < 20250405 && firmwareVersion > 0;
80 }
81
82 return false;
83}
84
86{
87 try
88 {
89 // For legacy protocol, we need to parse the data differently
90 // The data comes as a single string but we need to extract individual values
91 std::string dataStr(data);
92
93 // Remove any trailing newlines or carriage returns
94 dataStr.erase(std::remove(dataStr.begin(), dataStr.end(), '\n'), dataStr.end());
95 dataStr.erase(std::remove(dataStr.begin(), dataStr.end(), '\r'), dataStr.end());
96
97 // Split by 'A' separator
98 std::vector<std::string> tokens;
99 std::string token;
100 std::istringstream tokenStream(dataStr);
101
102 while (std::getline(tokenStream, token, 'A'))
103 {
104 if (!token.empty())
105 tokens.push_back(token);
106 }
107
108 // Debug: log all tokens
109 for (size_t i = 0; i < tokens.size(); i++)
110 {
111 // Note: Can't use LOGF_DEBUG here as we don't have access to getDeviceName()
112 }
113
114 // Check if we have enough tokens for legacy protocol
115 if (tokens.size() < 4)
116 {
117 return false;
118 }
119
120 // Device Model - check if it's WandererCoverV4
121 std::string deviceName = tokens[0];
122 if (deviceName != "WandererCoverV4Pro")
123 {
124 // Check for similar devices that might be compatible
125 if (deviceName == "ZXWBProV3" || deviceName == "ZXWBPlusV3" ||
126 deviceName == "UltimateV2" || deviceName == "PlusV2"|| deviceName == "WandererEclipse"|| deviceName == "WandererCoverV4")
127 {
128 // Note: Can't use LOG_WARN here as we don't have access to getDeviceName()
129 }
130 else
131 {
132 // Note: Can't use LOG_ERROR here as we don't have access to getDeviceName()
133 }
134 return false;
135 }
136
137 // Firmware version
138 device->firmware = std::atoi(tokens[1].c_str());
139
140 // Update firmware information in the UI
141 char firmwareStr[16];
142 snprintf(firmwareStr, sizeof(firmwareStr), "%d", device->firmware);
143 device->FirmwareTP[device->FIRMWARE_VERSION].setText(firmwareStr);
144 device->FirmwareTP.setState(IPS_OK);
145 device->FirmwareTP.apply();
146
147 // For legacy protocol, try to parse based on the actual data structure
148 // Based on the old driver, the structure is: Device, Firmware, CloseSet, OpenSet, CurrentPos, Voltage
149 if (tokens.size() >= 5)
150 {
151 // Standard legacy format: Device, Firmware, CloseSet, OpenSet, CurrentPos, Voltage
152 device->closesetread = std::strtod(tokens[2].c_str(), NULL);
153 device->opensetread = std::strtod(tokens[3].c_str(), NULL);
154 device->positionread = std::strtod(tokens[4].c_str(), NULL);
155
156 if (tokens.size() > 5)
157 {
158 device->voltageread = std::strtod(tokens[5].c_str(), NULL);
159 }
160 else
161 {
162 device->voltageread = 0.0;
163 }
164 }
165 else
166 {
167 return false;
168 }
169
170 // Update status data
171 device->statusData.firmware = device->firmware;
172 device->statusData.closePositionSet = device->closesetread;
173 device->statusData.openPositionSet = device->opensetread;
174 device->statusData.currentPosition = device->positionread;
175 device->statusData.voltage = device->voltageread;
176
177 // Update the UI with the parsed data
178 device->updateData(device->closesetread, device->opensetread, device->positionread, device->voltageread, device->flatpanelbrightnessread, device->asiaircontrolenabledread);
179
180 // Update the Close and Open position settings with the values from the device
181 device->CloseSetNP[device->CloseSet].setValue(device->closesetread);
182 device->CloseSetNP.setState(IPS_OK);
183 device->CloseSetNP.apply();
184
185 device->OpenSetNP[device->OpenSet].setValue(device->opensetread);
186 device->OpenSetNP.setState(IPS_OK);
187 device->OpenSetNP.apply();
188
189 return true;
190 }
191 catch(std::exception &e)
192 {
193 // Note: Can't use logging here as we don't have access to getDeviceName()
194 return false;
195 }
196}
197
198std::string WandererCoverLegacyProtocol::generateOpenCommand() const { return "1001"; }
199std::string WandererCoverLegacyProtocol::generateCloseCommand() const { return "1000"; }
200std::string WandererCoverLegacyProtocol::generateSetBrightnessCommand(uint16_t value) const { return std::to_string(value); }
201std::string WandererCoverLegacyProtocol::generateTurnOffLightCommand() const { return "9999"; }
202std::string WandererCoverLegacyProtocol::generateSetOpenPositionCommand(double value) const { return std::to_string((int)(value * 100 + 40000)); }
203std::string WandererCoverLegacyProtocol::generateSetClosePositionCommand(double value) const { return std::to_string((int)(value * 100 + 10000)); }
204std::string WandererCoverLegacyProtocol::generateASIAIRControlCommand(bool enable) const { return enable ? "1500003" : "1500004"; }
205std::string WandererCoverLegacyProtocol::generateCustomBrightnessCommand(int brightness, int customNumber) const { return std::to_string(customNumber * 1000000 + brightness); }
206
207// Modern protocol implementation
208bool WandererCoverModernProtocol::supportsFeature(const std::string& feature) const
209{
210 // Modern protocol supports all features
211 static const std::vector<std::string> supportedFeatures = {
212 "cover_control", "light_control", "position_setting",
213 "asiair_control", "custom_brightness", "extended_status"
214 };
215 return std::find(supportedFeatures.begin(), supportedFeatures.end(), feature) != supportedFeatures.end();
216}
217
219{
220 // Parse firmware version from data
221 std::string dataStr(data);
222 std::vector<std::string> tokens;
223 std::string token;
224 std::istringstream tokenStream(dataStr);
225
226 while (std::getline(tokenStream, token, 'A'))
227 {
228 if (!token.empty())
229 tokens.push_back(token);
230 }
231
232 // Check if we have enough tokens and the first one is correct
233 if (tokens.size() < 2)
234 return false;
235
236 // Enhanced device detection for modern protocol
237 std::string deviceName = tokens[0];
238
239 // Check for exact match first
240 if (deviceName == "WandererCoverV4Pro")
241 {
242 // Parse firmware version
243 int firmwareVersion = std::atoi(tokens[1].c_str());
244
245 // Modern protocol for firmware >= 20250405
246 return firmwareVersion >= 20250405;
247 }
248
249 return false;
250}
251
253{
254 try
255 {
256 std::vector<std::string> tokens;
257 std::string token;
258 std::istringstream tokenStream(data);
259
260 while (std::getline(tokenStream, token, 'A'))
261 {
262 if (!token.empty())
263 tokens.push_back(token);
264 }
265
266 if (tokens.size() < 8)
267 return false;
268
269 // Enhanced device detection for modern protocol
270 std::string deviceName = tokens[0];
271 if (deviceName != "WandererCoverV4Pro")
272 {
273 // Check for similar devices that might be compatible
274 if (deviceName == "ZXWBProV3" || deviceName == "ZXWBPlusV3" ||
275 deviceName == "UltimateV2" || deviceName == "PlusV2"|| deviceName == "WandererEclipse"|| deviceName == "WandererDewTerminator"|| deviceName == "WandererCoverV4")
276 {
277 // Note: Can't use LOG_WARN here as we don't have access to getDeviceName()
278 }
279 else
280 {
281 // Note: Can't use LOG_ERROR here as we don't have access to getDeviceName()
282 }
283 return false;
284 }
285
286 // Parse firmware version
287 device->firmware = std::atoi(tokens[1].c_str());
288
289 // Update firmware information in the UI
290 char firmwareStr[16];
291 snprintf(firmwareStr, sizeof(firmwareStr), "%d", device->firmware);
292 device->FirmwareTP[device->FIRMWARE_VERSION].setText(firmwareStr);
293 device->FirmwareTP.setState(IPS_OK);
294 device->FirmwareTP.apply();
295
296 // Parse position data
297 device->closesetread = std::strtod(tokens[2].c_str(), NULL);
298 device->opensetread = std::strtod(tokens[3].c_str(), NULL);
299 device->positionread = std::strtod(tokens[4].c_str(), NULL);
300 device->voltageread = std::strtod(tokens[5].c_str(), NULL);
301
302 // Parse extended data for modern protocol
303 if (tokens.size() >= 8)
304 {
305 device->flatpanelbrightnessread = std::atoi(tokens[6].c_str());
306 device->statusData.flatPanelBrightness = device->flatpanelbrightnessread;
307
308 device->asiaircontrolenabledread = (tokens.size() > 8) ? (std::atoi(tokens[8].c_str()) == 1) : false;
309 device->statusData.asiairControlEnabled = device->asiaircontrolenabledread;
310
311
312 }
313
314 // Update status data
315 device->statusData.firmware = device->firmware;
316 device->statusData.closePositionSet = device->closesetread;
317 device->statusData.openPositionSet = device->opensetread;
318 device->statusData.currentPosition = device->positionread;
319 device->statusData.voltage = device->voltageread;
320
321 // Update the UI with the parsed data
322 device->updateData(device->closesetread, device->opensetread, device->positionread, device->voltageread, device->flatpanelbrightnessread, device->asiaircontrolenabledread);
323
324 // Update the Close and Open position settings with the values from the device
325 device->CloseSetNP[device->CloseSet].setValue(device->closesetread);
326 device->CloseSetNP.setState(IPS_OK);
327 device->CloseSetNP.apply();
328
329 device->OpenSetNP[device->OpenSet].setValue(device->opensetread);
330 device->OpenSetNP.setState(IPS_OK);
331 device->OpenSetNP.apply();
332
333 return true;
334 }
335 catch(std::exception &e)
336 {
337 // Note: Can't use LOG_ERROR here as we don't have access to getDeviceName()
338 return false;
339 }
340}
341
342std::string WandererCoverModernProtocol::generateOpenCommand() const { return "1001"; }
343std::string WandererCoverModernProtocol::generateCloseCommand() const { return "1000"; }
344std::string WandererCoverModernProtocol::generateSetBrightnessCommand(uint16_t value) const { return std::to_string(value); }
345std::string WandererCoverModernProtocol::generateTurnOffLightCommand() const { return "9999"; }
346std::string WandererCoverModernProtocol::generateSetOpenPositionCommand(double value) const { return std::to_string((int)(value * 100 + 40000)); }
347std::string WandererCoverModernProtocol::generateSetClosePositionCommand(double value) const { return std::to_string((int)(value * 100 + 10000)); }
348std::string WandererCoverModernProtocol::generateASIAIRControlCommand(bool enable) const { return enable ? "1500003" : "1500004"; }
349std::string WandererCoverModernProtocol::generateCustomBrightnessCommand(int brightness, int customNumber) const { return std::to_string(customNumber * 1000000 + brightness); }
350
355
360{
361 return "WandererCover V4 Pro-EC";
362}
363
368{
370
373
376
377 //Data read - extended for new protocol
378 DataNP[closeset_read].fill( "Closed_Position", "Closed Position Set(°)", "%4.2f", 0, 999, 100, 0);
379 DataNP[openset_read].fill( "Open_Position", "Open Position Set(°)", "%4.2f", 0, 999, 100, 0);
380 DataNP[position_read].fill( "Current_Position", "Current Position(°)", "%4.2f", 0, 999, 100, 0);
381 DataNP[voltage_read].fill( "Voltage", "Voltage (V)", "%4.2f", 0, 999, 100, 0);
382 DataNP[flat_panel_brightness_read].fill( "Flat_Panel_Brightness", "Flat Panel Brightness", "%4.2f", 0, 255, 1, 0);
383 DataNP[asiair_control_enabled_read].fill( "ASIAIR_Control_Enabled", "ASIAIR Control Enabled", "%4.2f", 0, 1, 1, 0);
384 DataNP.fill(getDeviceName(), "STATUS", "Real Time Status", MAIN_CONTROL_TAB, IP_RO, 60, IPS_IDLE);
385
386 // Firmware information
387 FirmwareTP[FIRMWARE_VERSION].fill("FIRMWARE_VERSION", "Firmware Version", "Unknown");
388 FirmwareTP.fill(getDeviceName(), "FIRMWARE_INFO", "Firmware", MAIN_CONTROL_TAB, IP_RO, 60, IPS_IDLE);
389
390 LightIntensityNP[0].setMax(255);
391 LightIntensityNP[0].setValue(100);
392
393
394 // Close Set
395 CloseSetNP[CloseSet].fill( "CloseSet", "10-90", "%.2f", 10, 90, 0.01, 20);
396 CloseSetNP.fill(getDeviceName(), "CloseSet", "Close Position(°)", MAIN_CONTROL_TAB, IP_RW, 60, IPS_IDLE);
397
398 // Open Set
399 OpenSetNP[OpenSet].fill( "OpenSet", "100-300", "%.2f", 100, 300, 0.01, 150);
400 OpenSetNP.fill(getDeviceName(), "OpenSet", "Open Position(°)", MAIN_CONTROL_TAB, IP_RW, 60, IPS_IDLE);
401
402 // New protocol features
403 ASIAIRControlSP[ASIAIR_ENABLE].fill("ASIAIR_ENABLE", "Enable ASIAIR Control", ISS_OFF);
404 ASIAIRControlSP[ASIAIR_DISABLE].fill("ASIAIR_DISABLE", "Disable ASIAIR Control", ISS_ON);
405 ASIAIRControlSP.fill(getDeviceName(), "ASIAIR_CONTROL", "ASIAIR Control", MAIN_CONTROL_TAB, IP_RW, ISR_1OFMANY, 60, IPS_IDLE);
406
407 CustomBrightnessNP[CUSTOM_BRIGHTNESS_1].fill("CUSTOM_BRIGHTNESS_1", "Custom Brightness 1", "%1.0f", 0, 255, 1, 1);
408 CustomBrightnessNP[CUSTOM_BRIGHTNESS_2].fill("CUSTOM_BRIGHTNESS_2", "Custom Brightness 2", "%1.0f", 0, 255, 1, 50);
409 CustomBrightnessNP[CUSTOM_BRIGHTNESS_3].fill("CUSTOM_BRIGHTNESS_3", "Custom Brightness 3", "%1.0f", 0, 255, 1, 255);
410 CustomBrightnessNP.fill(getDeviceName(), "CUSTOM_BRIGHTNESS", "Custom Brightness", MAIN_CONTROL_TAB, IP_RW, 60, IPS_IDLE);
411
412
414
415 serialConnection = new Connection::Serial(this);
417 serialConnection->registerHandshake([&]()
418 {
419 return detectProtocol();
420 });
421 registerConnection(serialConnection);
422
423 return true;
424}
425
430{
432
433 if (isConnected())
434 {
435 // Update firmware information
436 char firmwareStr[16];
437 snprintf(firmwareStr, sizeof(firmwareStr), "%d", firmware);
438 FirmwareTP[FIRMWARE_VERSION].setText(firmwareStr);
439
440
441 // Update the Close and Open position settings with the values from the device
442 CloseSetNP[CloseSet].setValue(closesetread);
443 OpenSetNP[OpenSet].setValue(opensetread);
444
445 defineProperty(DataNP);
446 defineProperty(FirmwareTP);
447 defineProperty(CloseSetNP);
448 defineProperty(OpenSetNP);
449
450 // Define new protocol features if supported
451 if (currentProtocol && currentProtocol->supportsFeature("asiair_control"))
452 {
453 defineProperty(ASIAIRControlSP);
454 }
455 if (currentProtocol && currentProtocol->supportsFeature("custom_brightness"))
456 {
457 defineProperty(CustomBrightnessNP);
458 }
459
460 }
461 else
462 {
463 deleteProperty(DataNP);
464 deleteProperty(FirmwareTP);
465 deleteProperty(OpenSetNP);
466 deleteProperty(CloseSetNP);
467 deleteProperty(ASIAIRControlSP);
468 deleteProperty(CustomBrightnessNP);
469 }
470
473 return true;
474}
475
484
493
497bool WandererCoverV4ProEC::detectProtocol()
498{
499 try
500 {
501 LOG_DEBUG("Starting protocol detection...");
502
503 // Try to lock the mutex with a short timeout
504 if (!serialPortMutex.try_lock_for(std::chrono::milliseconds(100)))
505 {
506 LOG_DEBUG("Serial port is busy during protocol detection");
507 return false;
508 }
509
510 std::lock_guard<std::timed_mutex> lock(serialPortMutex, std::adopt_lock);
511
512 PortFD = serialConnection->getPortFD();
513 tcflush(PortFD, TCIOFLUSH);
514
515 // Read data from the device
516 char buffer[512] = {0};
517 int nbytes_read = 0, rc = -1;
518
519 // First, try to read any available data immediately
520 int available = 0;
521 if (ioctl(PortFD, FIONREAD, &available) == 0)
522 {
523 LOGF_DEBUG("Bytes available for reading: %d", available);
524 }
525
526 if ((rc = tty_read_section(PortFD, buffer, '\n', 2, &nbytes_read)) != TTY_OK)
527 {
528 if (rc == TTY_TIME_OUT)
529 {
530 LOG_DEBUG("Timeout reading from device during protocol detection");
531 LOG_DEBUG("Trying to read any available data without timeout...");
532
533 // Try to read without timeout to see if there's any data
534 if (tty_read(PortFD, buffer, 1, 0, &nbytes_read) == TTY_OK)
535 {
536 LOGF_DEBUG("Found %d bytes without timeout: '%s'", nbytes_read, buffer);
537 }
538 else
539 {
540 LOG_DEBUG("No data available without timeout either");
541 }
542
543 LOG_ERROR("Protocol detection failed: No data received from device");
544 return false;
545 }
546
547 char errorMessage[MAXRBUF];
548 tty_error_msg(rc, errorMessage, MAXRBUF);
549 LOGF_ERROR("Failed to read data from device during protocol detection. Error: %s", errorMessage);
550 return false;
551 }
552
553 LOGF_DEBUG("Raw data received from device: '%s' (length: %d)", buffer, nbytes_read);
554
555 // Parse firmware version from the data
556 std::string dataStr(buffer);
557 std::vector<std::string> tokens;
558 std::string token;
559 std::istringstream tokenStream(dataStr);
560
561 while (std::getline(tokenStream, token, 'A'))
562 {
563 if (!token.empty())
564 tokens.push_back(token);
565 }
566
567 LOGF_DEBUG("Parsed %zu tokens from device data", tokens.size());
568 for (size_t i = 0; i < tokens.size(); i++)
569 {
570 LOGF_DEBUG("Token[%zu]: '%s'", i, tokens[i].c_str());
571 }
572
573 // Check if we have enough tokens and the first one is correct
574 if (tokens.size() < 2)
575 {
576 LOGF_ERROR("Invalid data format: Not enough tokens. Expected at least 2, got %zu", tokens.size());
577 return false;
578 }
579
580 // Enhanced device detection with better error reporting
581 std::string deviceName = tokens[0];
582 LOGF_DEBUG("Device identification: '%s'", deviceName.c_str());
583
584 // Check for exact match first
585 if (deviceName == "WandererCoverV4Pro")
586 {
587 LOG_INFO("WandererCover V4 Pro-EC device detected");
588 }
589 // Check for similar devices that might be compatible
590 else if (deviceName == "ZXWBProV3" || deviceName == "ZXWBPlusV3" ||
591 deviceName == "UltimateV2" || deviceName == "PlusV2"|| deviceName == "WandererEclipse"|| deviceName == "WandererDewTerminator"|| deviceName == "WandererCoverV4")
592 {
593 LOGF_ERROR("WandererAstro products detected, but the model does not match:'%s'. This driver is designed for WandererCover V4 Pro-EC, please choose the right driver or try another serial port!", deviceName.c_str());
594 return false;
595 }
596 else
597 {
598 LOGF_ERROR("Unsupported device detected: '%s'. Expected 'WandererCoverV4Pro'", deviceName.c_str());
599 LOG_ERROR("This driver is specifically designed for WandererCover V4 Pro-EC devices only.");
600 return false;
601 }
602
603 // Parse firmware version
604 int firmwareVersion = std::atoi(tokens[1].c_str());
605 LOGF_INFO("Detected firmware version: %d", firmwareVersion);
606
607 // Validate firmware version
608 if (firmwareVersion <= 0)
609 {
610 LOGF_ERROR("Invalid firmware version: %d. Cannot determine protocol.", firmwareVersion);
611 return false;
612 }
613
614 // Choose protocol based on firmware version
615 if (firmwareVersion >= 20250405)
616 {
617 std::unique_ptr<WandererCoverModernProtocol> modernProtocol = std::make_unique<WandererCoverModernProtocol>();
618 setProtocol(std::move(modernProtocol));
619 LOG_INFO("Using modern protocol (firmware >= 20250405) Please note that in the newer firmware, to protect dark conditions, the flat light will remain off whenever the Cover is open.");
620 return true;
621 }
622 else if (firmwareVersion > 0)
623 {
624 std::unique_ptr<WandererCoverLegacyProtocol> legacyProtocol = std::make_unique<WandererCoverLegacyProtocol>();
625 setProtocol(std::move(legacyProtocol));
626 LOG_INFO("Using legacy protocol (firmware < 20250405) Firmware update recommended.");
627 return true;
628 }
629 else
630 {
631 LOGF_ERROR("Invalid firmware version: %d. Cannot determine protocol.", firmwareVersion);
632 return false;
633 }
634 }
635 catch(std::exception &e)
636 {
637 LOGF_ERROR("Exception occurred during protocol detection: %s", e.what());
638 return false;
639 }
640}
641
642void WandererCoverV4ProEC::setProtocol(std::unique_ptr<IWandererCoverProtocol> protocol)
643{
644 currentProtocol = std::move(protocol);
645
646}
647
651bool WandererCoverV4ProEC::getData()
652{
653 // Try to lock the mutex with a short timeout
654 if (!serialPortMutex.try_lock_for(std::chrono::milliseconds(100)))
655 {
656 LOG_DEBUG("Serial port is busy, skipping status update");
657 return true;
658 }
659
660 std::lock_guard<std::timed_mutex> lock(serialPortMutex, std::adopt_lock);
661
662 try
663 {
664 LOG_DEBUG("Reading data from device...");
665
666 PortFD = serialConnection->getPortFD();
667 tcflush(PortFD, TCIOFLUSH);
668
669 // Read all data from the device as a single line with 'A' separators
670 char buffer[512] = {0};
671 int nbytes_read = 0, rc = -1;
672
673 // Use a shorter timeout for reading to prevent blocking too long
674 if ((rc = tty_read_section(PortFD, buffer, '\n', 2, &nbytes_read)) != TTY_OK)
675 {
676 // If we get a timeout, it's not necessarily an error - the device might just be busy
677 if (rc == TTY_TIME_OUT)
678 {
679 LOG_DEBUG("Timeout reading from device, will try again later");
680 return true;
681 }
682
683 char errorMessage[MAXRBUF];
684 tty_error_msg(rc, errorMessage, MAXRBUF);
685 LOGF_ERROR("Failed to read data from device. Error: %s", errorMessage);
686 return false;
687 }
688
689 LOGF_DEBUG("Data received from device: '%s' (length: %d)", buffer, nbytes_read);
690
691 // Parse the received data using the current protocol
692 bool parseResult = parseDeviceData(buffer);
693 LOGF_DEBUG("Data parsing result: %s", parseResult ? "success" : "failed");
694 return parseResult;
695 }
696 catch(std::exception &e)
697 {
698 LOGF_ERROR("Exception occurred while reading data from device: %s", e.what());
699 return false;
700 }
701
702 return true;
703}
704
705bool WandererCoverV4ProEC::parseDeviceData(const char *data)
706{
707 if (!currentProtocol)
708 {
709 LOG_ERROR("No protocol handler available");
710 return false;
711 }
712
713 LOGF_DEBUG("Parsing data with protocol: %s", currentProtocol->getProtocolName().c_str());
714 return currentProtocol->parseDeviceData(data, this);
715}
716
720void WandererCoverV4ProEC::updateData(double closesetread, double opensetread, double positionread, double voltageread, double flatpanelbrightnessread, double asiaircontrolenabledread)
721{
722 // Update basic data
723 DataNP[closeset_read].setValue(closesetread);
724 DataNP[openset_read].setValue(opensetread);
725 DataNP[position_read].setValue(positionread);
726 DataNP[voltage_read].setValue(voltageread);
727
728
729 // Update extended data only if supported by current protocol
730 if (currentProtocol && currentProtocol->supportsFeature("extended_status"))
731 {
732 DataNP[flat_panel_brightness_read].setValue(flatpanelbrightnessread);
733 DataNP[asiair_control_enabled_read].setValue(asiaircontrolenabledread);
734 }
735 else
736 {
737 // For legacy protocol, set default values for unsupported features
738 DataNP[flat_panel_brightness_read].setValue(-1);
739 DataNP[asiair_control_enabled_read].setValue(-1);
740 }
741
742
743 DataNP.setState(IPS_OK);
744 DataNP.apply();
745
746 auto prevParked = ParkCapSP[CAP_PARK].getState() == ISS_ON;
747 auto prevState = ParkCapSP.getState();
748
749 ParkCapSP[CAP_PARK].setState((positionread - 10 <= closesetread) ? ISS_ON : ISS_OFF);
750 ParkCapSP[CAP_UNPARK].setState((positionread + 10 >= opensetread) ? ISS_ON : ISS_OFF);
752 || ParkCapSP[CAP_UNPARK].getState() == ISS_ON) ? IPS_OK : IPS_IDLE);
753
754 auto currentParked = ParkCapSP[CAP_PARK].getState() == ISS_ON;
755 auto currentState = ParkCapSP.getState();
756
757 // Only update on state changes
758 if ((prevParked != currentParked) || (prevState != currentState))
760}
761
765bool WandererCoverV4ProEC::ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n)
766{
767 if (LI::processText(dev, name, texts, names, n))
768 return true;
769
770 return INDI::DefaultDevice::ISNewText(dev, name, texts, names, n);
771}
772
776bool WandererCoverV4ProEC::ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n)
777{
778 if (LI::processSwitch(dev, name, states, names, n))
779 return true;
780
781 if (DI::processSwitch(dev, name, states, names, n))
782 return true;
783
784 if (dev && !strcmp(dev, getDeviceName()))
785 {
786 // ASIAIR Control
787 if (ASIAIRControlSP.isNameMatch(name))
788 {
789 if (!currentProtocol || !currentProtocol->supportsFeature("asiair_control"))
790 {
791 LOG_ERROR("ASIAIR control not supported by current protocol");
792 ASIAIRControlSP.setState(IPS_ALERT);
793 ASIAIRControlSP.apply();
794 return true;
795 }
796
797 bool rc = false;
798 for (int i = 0; i < n; i++)
799 {
800 if (strcmp(names[i], "ASIAIR_ENABLE") == 0 && states[i] == ISS_ON)
801 {
802 rc = sendCommand(currentProtocol->generateASIAIRControlCommand(true));
803 }
804 else if (strcmp(names[i], "ASIAIR_DISABLE") == 0 && states[i] == ISS_ON)
805 {
806 rc = sendCommand(currentProtocol->generateASIAIRControlCommand(false));
807 }
808 }
809
810 ASIAIRControlSP.setState(rc ? IPS_OK : IPS_ALERT);
811 if (ASIAIRControlSP.getState() == IPS_OK)
812 ASIAIRControlSP.update(states, names, n);
813 ASIAIRControlSP.apply();
814 return true;
815 }
816
817
818 }
819
820 return DefaultDevice::ISNewSwitch(dev, name, states, names, n);
821}
822
826bool WandererCoverV4ProEC::ISNewNumber(const char * dev, const char * name, double values[], char * names[], int n)
827{
828 if (LI::processNumber(dev, name, values, names, n))
829 return true;
830
831 if (dev && !strcmp(dev, getDeviceName()))
832 {
833
834 // Close Set
835 if (CloseSetNP.isNameMatch(name))
836 {
837 if (!currentProtocol)
838 {
839 LOG_ERROR("No protocol handler available");
840 CloseSetNP.setState(IPS_ALERT);
841 CloseSetNP.apply();
842 return true;
843 }
844
845 bool rc1 = false;
846
847 for (int i = 0; i < n; i++)
848 {
849 if(values[i] < 10 || values[i] > 90)
850 {
851 CloseSetNP.setState(IPS_ALERT);
852 LOG_ERROR("Out of range! Allowed closed angle: 10-90 degrees.");
853 return false;
854 }
855 rc1 = sendCommand(currentProtocol->generateSetClosePositionCommand(values[i]));
856 }
857
858 CloseSetNP.setState( (rc1) ? IPS_OK : IPS_ALERT);
859 if (CloseSetNP.getState() == IPS_OK)
860 {
861 CloseSetNP.update(values, names, n);
862 saveConfig(CloseSetNP);
863 }
864 CloseSetNP.apply();
865 return true;
866 }
867 // Open Set
868 if (OpenSetNP.isNameMatch(name))
869 {
870 if (!currentProtocol)
871 {
872 LOG_ERROR("No protocol handler available");
873 OpenSetNP.setState(IPS_ALERT);
874 OpenSetNP.apply();
875 return true;
876 }
877
878 bool rc1 = false;
879
880 for (int i = 0; i < n; i++)
881 {
882 if(values[i] < 100 || values[i] > 300)
883 {
884 OpenSetNP.setState(IPS_ALERT);
885 LOG_ERROR("Out of range! Allowed open angle: 100-300 degrees.");
886 return false;
887 }
888 rc1 = sendCommand(currentProtocol->generateSetOpenPositionCommand(values[i]));
889 }
890
891 OpenSetNP.setState( (rc1) ? IPS_OK : IPS_ALERT);
892 if (OpenSetNP.getState() == IPS_OK)
893 {
894 OpenSetNP.update(values, names, n);
895 saveConfig(OpenSetNP);
896 }
897 OpenSetNP.apply();
898 return true;
899 }
900
901 // Custom Brightness
902 if (CustomBrightnessNP.isNameMatch(name))
903 {
904 if (!currentProtocol || !currentProtocol->supportsFeature("custom_brightness"))
905 {
906 LOG_ERROR("Custom brightness not supported by current protocol");
907 CustomBrightnessNP.setState(IPS_ALERT);
908 CustomBrightnessNP.apply();
909 return true;
910 }
911
912 bool rc = false;
913 for (int i = 0; i < n; i++)
914 {
915 if (strcmp(names[i], "CUSTOM_BRIGHTNESS_1") == 0)
916 {
917 rc = sendCommand(currentProtocol->generateCustomBrightnessCommand(static_cast<int>(values[i]), 1));
918 }
919 else if (strcmp(names[i], "CUSTOM_BRIGHTNESS_2") == 0)
920 {
921 rc = sendCommand(currentProtocol->generateCustomBrightnessCommand(static_cast<int>(values[i]), 2));
922 }
923 else if (strcmp(names[i], "CUSTOM_BRIGHTNESS_3") == 0)
924 {
925 rc = sendCommand(currentProtocol->generateCustomBrightnessCommand(static_cast<int>(values[i]), 3));
926 }
927 }
928
929 CustomBrightnessNP.setState(rc ? IPS_OK : IPS_ALERT);
930 if (CustomBrightnessNP.getState() == IPS_OK)
931 CustomBrightnessNP.update(values, names, n);
932 CustomBrightnessNP.apply();
933 return true;
934 }
935 }
936 return INDI::DefaultDevice::ISNewNumber(dev, name, values, names, n);
937}
938
942bool WandererCoverV4ProEC::toggleCover(bool open)
943{
944 if (!currentProtocol)
945 {
946 LOG_ERROR("No protocol handler available");
947 return false;
948 }
949
950 std::string command = open ? currentProtocol->generateOpenCommand() : currentProtocol->generateCloseCommand();
951 return sendCommand(command);
952}
953
958{
959 // Set park status to busy
962
963 if (toggleCover(false))
964 {
965 return IPS_BUSY;
966 }
967
968 // If toggleCover failed, set back to alert
971 return IPS_ALERT;
972}
973
978{
979 // Set park status to busy
982
983 if (toggleCover(true))
984 {
985 return IPS_BUSY;
986 }
987
988 // If toggleCover failed, set back to alert
991 return IPS_ALERT;
992}
993
998{
999 if (!currentProtocol)
1000 {
1001 LOG_ERROR("No protocol handler available");
1002 return false;
1003 }
1004
1005 auto rc = true;
1006 if(value > 0)
1007 {
1008 // Only change if already enabled.
1009 if (LightSP[INDI_ENABLED].getState() == ISS_ON)
1010 rc = sendCommand(currentProtocol->generateSetBrightnessCommand(value));
1011 }
1012 else
1013 {
1014 EnableLightBox(false);
1018 LightSP.apply();
1019 }
1020
1021 return rc;
1022}
1023
1028{
1029 if (!currentProtocol)
1030 {
1031 LOG_ERROR("No protocol handler available");
1032 return false;
1033 }
1034
1035 auto rc = false;
1036 if(!enable)
1037 rc = sendCommand(currentProtocol->generateTurnOffLightCommand());
1038 else
1039 rc = sendCommand(currentProtocol->generateSetBrightnessCommand(static_cast<int>(LightIntensityNP[0].getValue())));
1040
1041 return rc;
1042}
1043
1047bool WandererCoverV4ProEC::sendCommand(std::string command)
1048{
1049 // Lock the mutex for the duration of this function
1050 std::lock_guard<std::timed_mutex> lock(serialPortMutex);
1051
1052 int nbytes_written = 0, rc = -1;
1053 std::string command_termination = "\n";
1054 LOGF_DEBUG("CMD: %s", command.c_str());
1055 if ((rc = tty_write_string(PortFD, (command + command_termination).c_str(), &nbytes_written)) != TTY_OK)
1056 {
1057 char errorMessage[MAXRBUF];
1058 tty_error_msg(rc, errorMessage, MAXRBUF);
1059 LOGF_ERROR("Serial write error: %s", errorMessage);
1060 return false;
1061 }
1062
1063 return true;
1064}
1065
1066
1070bool WandererCoverV4ProEC::setClose(double value)
1071{
1072 if (!currentProtocol)
1073 {
1074 LOG_ERROR("No protocol handler available");
1075 return false;
1076 }
1077
1078 return sendCommand(currentProtocol->generateSetClosePositionCommand(value));
1079}
1080
1084bool WandererCoverV4ProEC::setOpen(double value)
1085{
1086 if (!currentProtocol)
1087 {
1088 LOG_ERROR("No protocol handler available");
1089 return false;
1090 }
1091
1092 return sendCommand(currentProtocol->generateSetOpenPositionCommand(value));
1093}
1094
1099{
1100 if (!isConnected())
1101 {
1103 return;
1104 }
1105
1106 getData();
1108}
1109
1114{
1116
1117 CloseSetNP.save(fp);
1118 OpenSetNP.save(fp);
1119 CustomBrightnessNP.save(fp);
1120
1121 return LI::saveConfigItems(fp);
1122}
hid_device * device
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...
Provides interface to implement remotely controlled dust cover.
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 saveConfig(bool silent=false, const char *property=nullptr)
Save the current properties in a configuration file.
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.
virtual void ISGetProperties(const char *dev)
define the driver's properties to the client. Usually, only a minimum set of properties are defined t...
virtual bool ISSnoopDevice(XMLEle *root)
Process a snoop event from INDI server. This function is called when a snooped property is updated in...
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.
virtual bool ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n)
Process the client newSwitch command.
INDI::PropertySwitch ParkCapSP
bool updateProperties()
updateProperties Defines or Delete properties based on default device connection status
void initProperties(const char *group, uint32_t capabilities=0)
Initialize dust cap properties. It is recommended to call this function within initProperties() of yo...
bool processSwitch(const char *dev, const char *name, ISState *states, char *names[], int n)
Process dust cap switch properties.
void initProperties(const char *group, uint32_t capabilities)
Initialize light box properties. It is recommended to call this function within initProperties() of y...
void ISGetProperties(const char *deviceName)
isGetLightBoxProperties Get light box properties
INDI::PropertyNumber LightIntensityNP
bool processSwitch(const char *dev, const char *name, ISState *states, char *names[], int n)
Process light box switch properties.
bool processText(const char *dev, const char *name, char *texts[], char *names[], int n)
Process light box text properties.
bool processNumber(const char *dev, const char *name, double values[], char *names[], int n)
Process light box number properties.
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)
bool update(const ISState states[], const char *const names[], int n)
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)
Provides interface to implement controllable light box/switch device.
bool detectProtocol(const char *data) override
std::string generateSetBrightnessCommand(uint16_t value) const override
std::string generateCustomBrightnessCommand(int brightness, int customNumber) const override
std::string generateCloseCommand() const override
std::string generateSetClosePositionCommand(double value) const override
bool supportsFeature(const std::string &feature) const override
std::string generateSetOpenPositionCommand(double value) const override
std::string generateOpenCommand() const override
std::string generateTurnOffLightCommand() const override
bool parseDeviceData(const char *data, WandererCoverV4EC *device) override
std::string generateASIAIRControlCommand(bool enable) const override
std::string generateTurnOffLightCommand() const override
bool supportsFeature(const std::string &feature) const override
std::string generateASIAIRControlCommand(bool enable) const override
bool parseDeviceData(const char *data, WandererCoverV4EC *device) override
bool detectProtocol(const char *data) override
std::string generateCloseCommand() const override
std::string generateSetBrightnessCommand(uint16_t value) const override
std::string generateSetOpenPositionCommand(double value) const override
std::string generateCustomBrightnessCommand(int brightness, int customNumber) const override
std::string generateOpenCommand() const override
std::string generateSetClosePositionCommand(double value) const override
virtual bool initProperties() override
Initialize properties initial state and value. The child class must implement this function.
virtual IPState ParkCap() override
Park dust cap (close cover). Must be implemented by child.
virtual bool ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) override
Process the client newNumber command.
virtual bool updateProperties() override
updateProperties is called whenever there is a change in the CONNECTION status of the driver....
const char * getDefaultName() override
virtual void TimerHit() override
Callback function to be called once SetTimer duration elapses.
virtual bool SetLightBoxBrightness(uint16_t value) override
setBrightness Set light level. Must be implemented in the child class, if supported.
virtual void ISGetProperties(const char *dev) override
define the driver's properties to the client. Usually, only a minimum set of properties are defined t...
virtual bool saveConfigItems(FILE *fp) override
saveConfigItems Save specific properties in the provide config file handler. Child class usually over...
virtual bool ISSnoopDevice(XMLEle *root) override
Process a snoop event from INDI server. This function is called when a snooped property is updated in...
virtual IPState UnParkCap() override
unPark dust cap (open cover). Must be implemented by child.
virtual bool ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n) override
Process the client newSwitch command.
virtual bool EnableLightBox(bool enable) override
EnableLightBox Turn on/off on a light box. Must be implemented in the child class.
virtual bool ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) override
Process the client newSwitch command.
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
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_read(int fd, char *buf, int nbytes, int timeout, int *nbytes_read)
read buffer from terminal
Definition indicom.c:548
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_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