diff --git a/drivers.xml b/drivers.xml index 66c271730a..39cead5168 100644 --- a/drivers.xml +++ b/drivers.xml @@ -923,6 +923,10 @@ indi_wanderer_eclipse 1.2 + + indi_wanderer_eta + 1.0 + indi_dragon_light 1.0 diff --git a/drivers/auxiliary/CMakeLists.txt b/drivers/auxiliary/CMakeLists.txt index 32f7417ad7..2cf2315f0d 100644 --- a/drivers/auxiliary/CMakeLists.txt +++ b/drivers/auxiliary/CMakeLists.txt @@ -26,6 +26,14 @@ add_executable(indi_wanderer_eclipse ${indi_wanderer_eclipse_SRC}) target_link_libraries(indi_wanderer_eclipse indidriver) install(TARGETS indi_wanderer_eclipse RUNTIME DESTINATION bin) +# ########## Wanderer ETA M54############### +SET(indi_wanderer_eta_SRC + wanderer_eta.cpp) + +add_executable(indi_wanderer_eta ${indi_wanderer_eta_SRC}) +target_link_libraries(indi_wanderer_eta indidriver) +install(TARGETS indi_wanderer_eta RUNTIME DESTINATION bin) + # ########## Astrolink-4############### SET(indi_astrolink4_SRC indi_astrolink4.cpp) diff --git a/drivers/auxiliary/doc/wanderer_eta/README.md b/drivers/auxiliary/doc/wanderer_eta/README.md new file mode 100644 index 0000000000..c24582343c --- /dev/null +++ b/drivers/auxiliary/doc/wanderer_eta/README.md @@ -0,0 +1,198 @@ +# Wanderer ETA M54 — INDI Driver Testing Guide + +## Overview + +INDI driver for the WandererAstro ETA M54 Electronic Tilt Adjuster. Communicates via USB serial (CH340 chip, 19200 baud) and provides control of the three motorized tilt adjustment points. + +## Files Included in This Patch + +**New files (copy into `indi/drivers/auxiliary/`):** +- `wanderer_eta.h` +- `wanderer_eta.cpp` + +**Modified files (overwrite the upstream versions):** +- `indi/drivers/auxiliary/CMakeLists.txt` +- `indi/drivers.xml` + +## Setup on Arch Linux + +### 1. Install Build Dependencies + +```bash +sudo pacman -S --needed base-devel cmake git \ + cfitsio libnova libusb gsl libjpeg-turbo curl +``` + +### 2. Clone INDI Core + +```bash +mkdir -p ~/Projects +cd ~/Projects +git clone https://github.com/indilib/indi.git +cd indi +``` + +### 3. Apply the ETA Driver Patch + +Copy the patched/new files on top of the fresh clone. Assuming you extracted `eta-patch.tar.gz` into `~/Projects/`: + +```bash +cd ~/Projects/indi + +# New driver files +cp ~/Projects/eta-patch/wanderer_eta.h drivers/auxiliary/ +cp ~/Projects/eta-patch/wanderer_eta.cpp drivers/auxiliary/ + +# Modified files (overwrite upstream) +cp ~/Projects/eta-patch/CMakeLists.txt drivers/auxiliary/CMakeLists.txt +cp ~/Projects/eta-patch/drivers.xml drivers.xml +``` + +Verify the files are in place: +```bash +ls -la ~/Projects/indi/drivers/auxiliary/wanderer_eta.* +grep -n "wanderer_eta" ~/Projects/indi/drivers/auxiliary/CMakeLists.txt +grep -n "Wanderer ETA" ~/Projects/indi/drivers.xml +``` + +### 4. Build + +```bash +mkdir -p ~/Projects/build/indi-core +cd ~/Projects/build/indi-core + +cmake -DCMAKE_INSTALL_PREFIX=/usr -DCMAKE_BUILD_TYPE=Debug ~/Projects/indi + +# Build just the ETA driver (fast) +make -j$(nproc) indi_wanderer_eta + +# Install +sudo make install +``` + +Verify: +```bash +which indi_wanderer_eta +``` + +## Note on StellarMate / Flatpak Setups + +On StellarMate, KStars typically runs as a flatpak while INDI drivers run natively on the host. For testing this driver you only need to build and install INDI core natively — you do **not** need to rebuild indi-3rdparty or KStars. + +The workflow is: +1. Build & install the patched INDI core natively (gives you `indi_wanderer_eta` + `indiserver`) +2. Run `indiserver -v indi_wanderer_eta` manually on the host +3. Connect using `indi_control_panel` or any INDI client pointing at localhost:7624 + +**Important**: When KStars flatpak starts an Ekos profile in local mode, it launches its own `indiserver` with the drivers bundled inside the flatpak — our ETA driver won't be there. For initial testing, don't use KStars/Ekos. Instead, start `indiserver` manually on the host and connect with `indi_control_panel`. If you want to test through KStars, set the Ekos profile to **Remote** mode with the machine's **actual IP address** (e.g., `192.168.0.69`) and port `7624`, then start your native `indiserver` separately. Do NOT use `localhost` — that triggers Ekos to launch its own bundled indiserver which will conflict. + +If you need other INDI drivers (cameras, mount, etc.) running alongside the ETA, make sure your existing native INDI installation is compatible. If you're currently using system-packaged INDI, installing our patched build to `/usr` will overwrite `libindi`. To avoid that, you can install to a separate prefix: +```bash +cmake -DCMAKE_INSTALL_PREFIX=/usr/local -DCMAKE_BUILD_TYPE=Debug ~/Projects/indi +make -j$(nproc) indi_wanderer_eta +sudo make install +# Then run with full path: +/usr/local/bin/indiserver /usr/local/bin/indi_wanderer_eta +``` + +## Testing with Hardware + +### 1. Connect the ETA + +Plug the Wanderer ETA M54 into a USB port. Verify the CH340 serial adapter appears: +```bash +dmesg | tail -5 +# Should show: ch341-uart converter now attached to ttyUSB0 + +ls /dev/ttyUSB* +``` + +If you get permission errors later, add yourself to the `uucp` group (Arch uses `uucp` instead of `dialout`): +```bash +sudo usermod -aG uucp $USER +# Then log out and back in +``` + +### 2. Start the INDI Server + +```bash +indiserver -v indi_wanderer_eta indi_simulator_ccd indi_simulator_telescope +``` + +This starts the ETA driver alongside a CCD and telescope simulator so you have a complete setup for testing in Ekos. + +The `-v` flag enables verbose logging so you can see serial communication. + +### 3. Connect with a Client + +**Using INDI Control Panel:** +```bash +indi_control_panel & +``` + +**Using KStars/Ekos:** +1. Ekos → Profile Editor +2. Set mode to **Remote** +3. Set the host to the machine's **actual IP address** (e.g., `192.168.0.69`), port `7624` +4. **Important**: Do NOT use `localhost` — that will make Ekos try to start its own indiserver, which will fail because one is already running on port 7624 +5. Start the profile — you should see the ETA, simulator CCD, and simulator telescope + +### 4. Test Sequence + +#### a) Connection & Handshake +1. In the Connection tab, select the correct serial port (e.g., `/dev/ttyUSB0`) +2. Click "Connect" +3. **Expected**: Driver reads the continuous status stream, validates `WandererTilterM54` identifier, connects +4. **Verify**: Firmware version appears (e.g., `20250318`) + +#### b) Position Readback +1. Check the "Current Positions" fields after connecting +2. **Expected**: Three Point values show current encoder positions (likely near 0.000 if freshly powered) +3. **Verify**: Values update every ~2 seconds + +#### c) Move Points +1. Set Point 1 target to `0.500`, click Set +2. **Expected**: Driver sends `10.500` to device, readback updates to ~0.500 +3. Repeat for Point 2 (`0.250`) and Point 3 (`0.750`) +4. **Verify**: Readback matches target within ~0.002mm + +#### d) Range Validation +1. Try setting a point to `1.500` (above max) +2. **Expected**: Error in log, property goes Alert, no command sent +3. Try `-0.1` (below min) — same expected behavior + +#### e) Zero All +1. With points at various positions, click "Zero All Points" +2. **Expected**: All three points move to 0.000mm +3. **Verify**: Both target and readback show 0.000 + +#### f) Config Save/Load +1. Set points to known values (e.g., 0.100, 0.200, 0.300) +2. Options → Save Configuration +3. Disconnect, reconnect, Options → Load Configuration +4. **Expected**: Target positions restored + +### 5. Debug Logs + +With `-v` on indiserver you'll see: +- `CMD: 10.500` — commands sent +- `Data: WandererTilterM54A20250318A0.500A0.250A0.750A` — raw status received + +For more detail, enable Debug in the driver's Options tab. + +## Troubleshooting + +- **"Timeout reading from device"**: ETA not powered or wrong serial port. Check `dmesg`. +- **"Unknown device: ..."**: Wrong WandererAstro device on that port (cover, rotator, etc.). +- **Permission denied**: Add user to `uucp` group (see above). +- **Build error "indidriver not found"**: Make sure you ran cmake from the indi root, not just the auxiliary directory. + +## What to Report Back + +1. Does the handshake succeed? (firmware version displayed) +2. Do position readbacks update correctly? +3. Do move commands work? (points reach requested positions) +4. Readback accuracy — within ~0.002mm of target? +5. Does Zero All work? +6. Any serial errors or unexpected behavior? +7. Exact firmware version string shown diff --git a/drivers/auxiliary/wanderer_eta.cpp b/drivers/auxiliary/wanderer_eta.cpp new file mode 100644 index 0000000000..41788d4abd --- /dev/null +++ b/drivers/auxiliary/wanderer_eta.cpp @@ -0,0 +1,554 @@ +/******************************************************************************* + Copyright(c) 2025 cfuture81. All rights reserved. + + Wanderer ETA M54 - Electronic Tilt Adjuster + + This program is free software; you can redistribute it and/or modify it + under the terms of the GNU General Public License as published by the Free + Software Foundation; either version 2 of the License, or (at your option) + any later version. + + This program is distributed in the hope that it will be useful, but WITHOUT + ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + more details. + + You should have received a copy of the GNU Library General Public License + along with this library; see the file COPYING.LIB. If not, write to + the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. + + The full GNU General Public License is included in this distribution in the + file called LICENSE. +*******************************************************************************/ + +#include "wanderer_eta.h" +#include "indicom.h" +#include "connectionplugins/connectionserial.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static std::unique_ptr wandererETA(new WandererETA()); + +//////////////////////////////////////////////////////////////////////////////////////////////////////// +/// Constructor +//////////////////////////////////////////////////////////////////////////////////////////////////////// +WandererETA::WandererETA() +{ + setVersion(1, 0); +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////// +/// Device name +//////////////////////////////////////////////////////////////////////////////////////////////////////// +const char *WandererETA::getDefaultName() +{ + return "Wanderer ETA M54"; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////// +/// Init properties +//////////////////////////////////////////////////////////////////////////////////////////////////////// +bool WandererETA::initProperties() +{ + INDI::DefaultDevice::initProperties(); + + setDriverInterface(AUX_INTERFACE); + addAuxControls(); + + // Motor position targets (user-settable) - separate properties for independent control + Position1NP[0].fill("POINT_1", "Position (mm)", "%.3f", 0.000, 1.200, 0.001, 0.000); + Position1NP.fill(getDeviceName(), "POINT_1_TARGET", "Point 1 Target", MAIN_CONTROL_TAB, IP_RW, 60, IPS_IDLE); + + Position2NP[0].fill("POINT_2", "Position (mm)", "%.3f", 0.000, 1.200, 0.001, 0.000); + Position2NP.fill(getDeviceName(), "POINT_2_TARGET", "Point 2 Target", MAIN_CONTROL_TAB, IP_RW, 60, IPS_IDLE); + + Position3NP[0].fill("POINT_3", "Position (mm)", "%.3f", 0.000, 1.200, 0.001, 0.000); + Position3NP.fill(getDeviceName(), "POINT_3_TARGET", "Point 3 Target", MAIN_CONTROL_TAB, IP_RW, 60, IPS_IDLE); + + // Motor position readback (encoder feedback, read-only) + PositionReadNP[POINT_1].fill("POINT_1_READ", "Point 1 (mm)", "%.3f", 0.000, 1.200, 0.000, 0.000); + PositionReadNP[POINT_2].fill("POINT_2_READ", "Point 2 (mm)", "%.3f", 0.000, 1.200, 0.000, 0.000); + PositionReadNP[POINT_3].fill("POINT_3_READ", "Point 3 (mm)", "%.3f", 0.000, 1.200, 0.000, 0.000); + PositionReadNP.fill(getDeviceName(), "POSITION_READBACK", "Current Positions", MAIN_CONTROL_TAB, IP_RO, 60, IPS_IDLE); + + // Firmware information + FirmwareTP[FIRMWARE_VERSION].fill("FIRMWARE_VERSION", "Firmware Version", "Unknown"); + FirmwareTP.fill(getDeviceName(), "FIRMWARE_INFO", "Firmware", MAIN_CONTROL_TAB, IP_RO, 60, IPS_IDLE); + + // Zero all points + ZeroAllSP[ZERO_ALL].fill("ZERO_ALL", "Zero All Points", ISS_OFF); + ZeroAllSP.fill(getDeviceName(), "ZERO_ALL_CMD", "Zero All", MAIN_CONTROL_TAB, IP_RW, ISR_ATMOST1, 60, IPS_IDLE); + + setDefaultPollingPeriod(2000); + + serialConnection = new Connection::Serial(this); + serialConnection->setDefaultBaudRate(Connection::Serial::B_19200); + serialConnection->registerHandshake([&]() + { + return getData(); + }); + registerConnection(serialConnection); + + return true; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////// +/// Update properties on connect/disconnect +//////////////////////////////////////////////////////////////////////////////////////////////////////// +bool WandererETA::updateProperties() +{ + INDI::DefaultDevice::updateProperties(); + + if (isConnected()) + { + char firmwareStr[16]; + snprintf(firmwareStr, sizeof(firmwareStr), "%d", firmware); + FirmwareTP[FIRMWARE_VERSION].setText(firmwareStr); + LOGF_INFO("Firmware version: %d", firmware); + + defineProperty(Position1NP); + defineProperty(Position2NP); + defineProperty(Position3NP); + defineProperty(PositionReadNP); + defineProperty(FirmwareTP); + defineProperty(ZeroAllSP); + } + else + { + deleteProperty(Position1NP); + deleteProperty(Position2NP); + deleteProperty(Position3NP); + deleteProperty(PositionReadNP); + deleteProperty(FirmwareTP); + deleteProperty(ZeroAllSP); + } + + return true; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////// +/// Get data from device (handshake + polling) +//////////////////////////////////////////////////////////////////////////////////////////////////////// +bool WandererETA::getData() +{ + // Skip reading while commands are being sent + if (m_SendingCommand) + return true; + + if (!serialPortMutex.try_lock_for(std::chrono::milliseconds(500))) + { + LOG_DEBUG("Serial port is busy, skipping status update"); + return true; + } + + std::lock_guard lock(serialPortMutex, std::adopt_lock); + + try + { + PortFD = serialConnection->getPortFD(); + + char buffer[512] = {0}; + int nbytes_read = 0, rc = -1; + + if ((rc = tty_read_section(PortFD, buffer, '\n', 2, &nbytes_read)) != TTY_OK) + { + if (rc == TTY_TIME_OUT) + { + LOG_DEBUG("Timeout reading from device, will try again later"); + return true; + } + + char errorMessage[MAXRBUF]; + tty_error_msg(rc, errorMessage, MAXRBUF); + LOGF_ERROR("Failed to read data from device. Error: %s", errorMessage); + return false; + } + + return parseDeviceData(buffer); + } + catch (std::exception &e) + { + LOG_ERROR("Exception occurred while reading data from device"); + return false; + } + + return true; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////// +/// Parse device status data +//////////////////////////////////////////////////////////////////////////////////////////////////////// +bool WandererETA::parseDeviceData(const char *data) +{ + try + { + std::vector tokens; + std::string token; + std::istringstream tokenStream(data); + LOGF_DEBUG("Data: %s", data); + + // Split the data by 'A' separator + while (std::getline(tokenStream, token, 'A')) + { + if (!token.empty()) + tokens.push_back(token); + } + + // Need at least: device_id + firmware + 3 positions + if (tokens.size() < 5) + { + LOGF_DEBUG("Insufficient tokens: %d (need 5)", static_cast(tokens.size())); + return false; + } + + // Validate device identifier + if (tokens[0] != "WandererTilterM54") + { + LOGF_WARN("Unknown device: %s (expected WandererTilterM54)", tokens[0].c_str()); + return false; + } + + // Firmware version + firmware = std::atoi(tokens[1].c_str()); + + char firmwareStr[16]; + snprintf(firmwareStr, sizeof(firmwareStr), "%d", firmware); + FirmwareTP[FIRMWARE_VERSION].setText(firmwareStr); + FirmwareTP.setState(IPS_OK); + FirmwareTP.apply(); + + // Point positions from encoders + double pos1 = std::strtod(tokens[2].c_str(), nullptr); + double pos2 = std::strtod(tokens[3].c_str(), nullptr); + double pos3 = std::strtod(tokens[4].c_str(), nullptr); + + PositionReadNP[POINT_1].setValue(pos1); + PositionReadNP[POINT_2].setValue(pos2); + PositionReadNP[POINT_3].setValue(pos3); + PositionReadNP.setState(IPS_OK); + PositionReadNP.apply(); + + return true; + } + catch (std::exception &e) + { + LOG_ERROR("Failed to parse device data"); + return false; + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////// +/// Helper: update a position property by index +//////////////////////////////////////////////////////////////////////////////////////////////////////// +void WandererETA::setPositionNP(int index, double values[], char *names[], int n, IPState state) +{ + switch (index) + { + case 0: + Position1NP.update(values, names, n); + Position1NP.setState(state); + Position1NP.apply(); + break; + case 1: + Position2NP.update(values, names, n); + Position2NP.setState(state); + Position2NP.apply(); + break; + case 2: + Position3NP.update(values, names, n); + Position3NP.setState(state); + Position3NP.apply(); + break; + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////// +/// Helper: set state on a position property by index +//////////////////////////////////////////////////////////////////////////////////////////////////////// +void WandererETA::setPositionState(int index, IPState state) +{ + switch (index) + { + case 0: + Position1NP.setState(state); + Position1NP.apply(); + break; + case 1: + Position2NP.setState(state); + Position2NP.apply(); + break; + case 2: + Position3NP.setState(state); + Position3NP.apply(); + break; + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////// +/// Handle new number values (position targets) +//////////////////////////////////////////////////////////////////////////////////////////////////////// +bool WandererETA::ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) +{ + if (dev && !strcmp(dev, getDeviceName())) + { + // Determine which point is being commanded + int pointIndex = -1; + + if (Position1NP.isNameMatch(name)) + pointIndex = 0; + else if (Position2NP.isNameMatch(name)) + pointIndex = 1; + else if (Position3NP.isNameMatch(name)) + pointIndex = 2; + + if (pointIndex >= 0) + { + double target = values[0]; + + // During initialization (config load), just store values without moving + if (m_Initializing) + { + setPositionNP(pointIndex, values, names, n, IPS_OK); + return true; + } + + // Validate + if (target < 0.0 || target > 1.2) + { + LOGF_ERROR("Position value %.3f out of range (0.000 - 1.200 mm)", target); + setPositionNP(pointIndex, values, names, n, IPS_ALERT); + return true; + } + + setPositionNP(pointIndex, values, names, n, IPS_BUSY); + + // Block polling while sending command + m_SendingCommand = true; + + usleep(200000); + tcflush(PortFD, TCIOFLUSH); + usleep(100000); + + char cmd[32]; + snprintf(cmd, sizeof(cmd), "%d%.3f\n", pointIndex + 1, target); + + tcflush(PortFD, TCIOFLUSH); + usleep(100000); + + int nbytes_written = 0, rc = -1; + bool success = true; + + if ((rc = tty_write_string(PortFD, cmd, &nbytes_written)) != TTY_OK) + { + char errorMessage[MAXRBUF]; + tty_error_msg(rc, errorMessage, MAXRBUF); + LOGF_ERROR("Serial write error: %s", errorMessage); + success = false; + } + else + { + tcdrain(PortFD); + LOGF_INFO("Moving Point %d to %.3f mm", pointIndex + 1, target); + + if (!waitForPosition(pointIndex, target, 15000)) + { + LOGF_WARN("Point %d did not reach target %.3f within timeout", pointIndex + 1, target); + } + } + + m_SendingCommand = false; + + setPositionState(pointIndex, success ? IPS_OK : IPS_ALERT); + return true; + } + } + + return INDI::DefaultDevice::ISNewNumber(dev, name, values, names, n); +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////// +/// Handle new switch values (Zero All) +//////////////////////////////////////////////////////////////////////////////////////////////////////// +bool WandererETA::ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) +{ + if (dev && !strcmp(dev, getDeviceName())) + { + if (ZeroAllSP.isNameMatch(name)) + { + bool success = true; + + m_SendingCommand = true; + usleep(200000); + tcflush(PortFD, TCIOFLUSH); + usleep(100000); + + // Send zero command for each point with flush and wait + for (int i = 0; i < 3; i++) + { + char cmd[32]; + snprintf(cmd, sizeof(cmd), "%d0.000\n", i + 1); + + tcflush(PortFD, TCIOFLUSH); + usleep(100000); + + int nbytes_written = 0, rc = -1; + if ((rc = tty_write_string(PortFD, cmd, &nbytes_written)) != TTY_OK) + { + success = false; + break; + } + tcdrain(PortFD); + LOGF_INFO("Zeroing Point %d", i + 1); + + if (!waitForPosition(i, 0.0, 15000)) + { + LOGF_WARN("Point %d did not reach zero within timeout", i + 1); + } + } + + m_SendingCommand = false; + + if (success) + { + LOG_INFO("Moving all points to zero position"); + // Update targets to reflect zero + Position1NP[0].setValue(0.0); + Position1NP.setState(IPS_OK); + Position1NP.apply(); + Position2NP[0].setValue(0.0); + Position2NP.setState(IPS_OK); + Position2NP.apply(); + Position3NP[0].setValue(0.0); + Position3NP.setState(IPS_OK); + Position3NP.apply(); + } + + ZeroAllSP.setState(success ? IPS_OK : IPS_ALERT); + ZeroAllSP[ZERO_ALL].setState(ISS_OFF); + ZeroAllSP.apply(); + return true; + } + } + + return INDI::DefaultDevice::ISNewSwitch(dev, name, states, names, n); +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////// +/// Wait for a motor to reach target position by reading status stream +//////////////////////////////////////////////////////////////////////////////////////////////////////// +bool WandererETA::waitForPosition(int pointIndex, double target, int timeoutMs) +{ + auto start = std::chrono::steady_clock::now(); + char buffer[512] = {0}; + int nbytes_read = 0; + + while (true) + { + auto elapsed = std::chrono::duration_cast( + std::chrono::steady_clock::now() - start).count(); + if (elapsed > timeoutMs) + return false; + + nbytes_read = 0; + int rc = tty_read_section(PortFD, buffer, '\n', 3, &nbytes_read); + if (rc != TTY_OK) + continue; + + // Parse the status line to extract positions + std::string data(buffer); + std::vector tokens; + std::string token; + std::istringstream tokenStream(data); + while (std::getline(tokenStream, token, 'A')) + { + if (!token.empty()) + tokens.push_back(token); + } + + if (tokens.size() >= 5 && tokens[0] == "WandererTilterM54") + { + double pos = std::strtod(tokens[2 + pointIndex].c_str(), nullptr); + + // Update readback display + PositionReadNP[POINT_1].setValue(std::strtod(tokens[2].c_str(), nullptr)); + PositionReadNP[POINT_2].setValue(std::strtod(tokens[3].c_str(), nullptr)); + PositionReadNP[POINT_3].setValue(std::strtod(tokens[4].c_str(), nullptr)); + PositionReadNP.setState(IPS_OK); + PositionReadNP.apply(); + + // Check if target reached (within 0.005mm tolerance) + if (std::abs(pos - target) < 0.005) + return true; + } + + memset(buffer, 0, sizeof(buffer)); + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////// +/// Send command to device (thread-safe) +//////////////////////////////////////////////////////////////////////////////////////////////////////// +bool WandererETA::sendCommand(std::string command) +{ + std::lock_guard lock(serialPortMutex); + + int nbytes_written = 0, rc = -1; + std::string command_termination = "\n"; + LOGF_DEBUG("CMD: %s", command.c_str()); + + // Flush any pending input data before sending command. + // The device streams status continuously; we need a clean state. + tcflush(PortFD, TCIFLUSH); + + if ((rc = tty_write_string(PortFD, (command + command_termination).c_str(), &nbytes_written)) != TTY_OK) + { + char errorMessage[MAXRBUF]; + tty_error_msg(rc, errorMessage, MAXRBUF); + LOGF_ERROR("Serial write error: %s", errorMessage); + return false; + } + + // Ensure the data is fully transmitted before releasing the port + tcdrain(PortFD); + + return true; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////// +/// Timer callback - poll device status +//////////////////////////////////////////////////////////////////////////////////////////////////////// +void WandererETA::TimerHit() +{ + if (!isConnected()) + { + SetTimer(getPollingPeriod()); + return; + } + + // Clear initialization flag on first timer tick — by now config loading is complete + if (m_Initializing) + m_Initializing = false; + + getData(); + SetTimer(getPollingPeriod()); +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////// +/// Save config items +//////////////////////////////////////////////////////////////////////////////////////////////////////// +bool WandererETA::saveConfigItems(FILE *fp) +{ + INDI::DefaultDevice::saveConfigItems(fp); + Position1NP.save(fp); + Position2NP.save(fp); + Position3NP.save(fp); + return true; +} diff --git a/drivers/auxiliary/wanderer_eta.h b/drivers/auxiliary/wanderer_eta.h new file mode 100644 index 0000000000..a898345a2a --- /dev/null +++ b/drivers/auxiliary/wanderer_eta.h @@ -0,0 +1,99 @@ +/******************************************************************************* + Copyright(c) 2025 cfuture81. All rights reserved. + + Wanderer ETA M54 - Electronic Tilt Adjuster + + This program is free software; you can redistribute it and/or modify it + under the terms of the GNU General Public License as published by the Free + Software Foundation; either version 2 of the License, or (at your option) + any later version. + + This program is distributed in the hope that it will be useful, but WITHOUT + ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + more details. + + You should have received a copy of the GNU Library General Public License + along with this library; see the file COPYING.LIB. If not, write to + the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. + + The full GNU General Public License is included in this distribution in the + file called LICENSE. +*******************************************************************************/ + +#pragma once + +#include "defaultdevice.h" +#include + +namespace Connection +{ +class Serial; +} + +class WandererETA : public INDI::DefaultDevice +{ + public: + WandererETA(); + virtual ~WandererETA() = default; + + virtual bool initProperties() override; + virtual bool updateProperties() override; + virtual bool ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) override; + virtual bool ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) override; + + protected: + const char *getDefaultName() override; + virtual void TimerHit() override; + virtual bool saveConfigItems(FILE *fp) override; + + private: + // Serial communication + bool sendCommand(std::string command); + bool getData(); + bool parseDeviceData(const char *data); + + // State + int firmware = 0; + + // Motor position targets (RW) - individual properties for independent control + INDI::PropertyNumber Position1NP{1}; + INDI::PropertyNumber Position2NP{1}; + INDI::PropertyNumber Position3NP{1}; + + enum + { + POINT_1, + POINT_2, + POINT_3, + }; + + // Motor position readback (RO) - what the encoders report + INDI::PropertyNumber PositionReadNP{3}; + + // Firmware information + INDI::PropertyText FirmwareTP{1}; + enum + { + FIRMWARE_VERSION, + }; + + // Zero all points + INDI::PropertySwitch ZeroAllSP{1}; + enum + { + ZERO_ALL, + }; + + int PortFD{ -1 }; + Connection::Serial *serialConnection{ nullptr }; + + // Mutex for thread safety + std::timed_mutex serialPortMutex; + bool m_SendingCommand {false}; + bool m_Initializing {true}; + bool waitForPosition(int pointIndex, double target, int timeoutMs); + void setPositionNP(int index, double values[], char *names[], int n, IPState state); + void setPositionState(int index, IPState state); +};