diff --git a/configure.ac b/configure.ac index c65e1ac4f..7cb321c75 100644 --- a/configure.ac +++ b/configure.ac @@ -67,7 +67,7 @@ dnl Beware of duplication should a backend directory include both rig and dnl rotor definitions, e.g. "dummy". Optional backends will not be listed dnl here but will be added later, e.g. "winradio". RIG_BACKEND_LIST="rigs/adat rigs/alinco rigs/aor rigs/barrett rigs/codan rigs/dorji rigs/drake rigs/dummy rigs/elad rigs/flexradio rigs/icom rigs/icmarine rigs/jrc rigs/kachina rigs/kenwood rigs/kit rigs/lowe rigs/pcr rigs/prm80 rigs/racal rigs/rft rigs/rs rigs/skanti rigs/tapr rigs/tentec rigs/tuner rigs/uniden rigs/winradio rigs/wj rigs/yaesu rigs/gomspace rigs/mds rigs/anytone rigs/motorola rigs/commradio" -ROT_BACKEND_LIST="rotators/amsat rotators/apex rotators/ars rotators/celestron rotators/cnctrk rotators/grbltrk rotators/easycomm rotators/ether6 rotators/flir rotators/fodtrack rotators/gs232a rotators/heathkit rotators/m2 rotators/meade rotators/rotorez rotators/sartek rotators/saebrtrack rotators/spid rotators/ts7400 rotators/prosistel rotators/ioptron rotators/satel rotators/radant" +ROT_BACKEND_LIST="rotators/amsat rotators/apex rotators/ars rotators/celestron rotators/cnctrk rotators/grbltrk rotators/easycomm rotators/ether6 rotators/flir rotators/fodtrack rotators/gs232a rotators/heathkit rotators/m2 rotators/meade rotators/rotorez rotators/sartek rotators/saebrtrack rotators/spid rotators/ts7400 rotators/prosistel rotators/ioptron rotators/satel rotators/skywatcher rotators/radant" # Amplifiers are all in the amplifiers directory AMP_BACKEND_LIST="amplifiers/elecraft amplifiers/gemini amplifiers/expert" @@ -898,6 +898,7 @@ rotators/spid/Makefile rotators/ts7400/Makefile rotators/indi/Makefile rotators/satel/Makefile +rotators/skywatcher/Makefile rotators/radant/Makefile rigs/adat/Makefile rigs/alinco/Makefile diff --git a/include/hamlib/rotlist.h b/include/hamlib/rotlist.h index cd49c7c10..37d410846 100644 --- a/include/hamlib/rotlist.h +++ b/include/hamlib/rotlist.h @@ -702,6 +702,18 @@ //! @endcond #define ROT_MODEL_SAEBRTRACK ROT_MAKE_MODEL(ROT_SAEBRTRACK, 1) +/** + * \brief A macro that returns the model number of the SKYWATCHER backend. + * + * \def ROT_MODEL_SKYWATCHER + * + * The SKYWATCHER backend can be used with SKYWATCHER * rotators. + */ +//! @cond Doxygen_Suppress +#define ROT_SKYWATCHER 28 +#define ROT_BACKEND_SKYWATCHER "SkyWatcher" +//! @endcond +#define ROT_MODEL_SKYWATCHER ROT_MAKE_MODEL(ROT_SKYWATCHER, 1) /** * \brief Convenience type definition for a rotator model. diff --git a/rotators/skywatcher/Android.mk b/rotators/skywatcher/Android.mk new file mode 100644 index 000000000..8dd021d8c --- /dev/null +++ b/rotators/skywatcher/Android.mk @@ -0,0 +1,12 @@ +LOCAL_PATH:= $(call my-dir) + +include $(CLEAR_VARS) + +LOCAL_SRC_FILES := skywatcher.c +LOCAL_MODULE := skywatcher + +LOCAL_CFLAGS := +LOCAL_C_INCLUDES := android include src +LOCAL_LDLIBS := -lhamlib -Lobj/local/$(TARGET_ARCH_ABI) + +include $(BUILD_STATIC_LIBRARY) diff --git a/rotators/skywatcher/Makefile.am b/rotators/skywatcher/Makefile.am new file mode 100644 index 000000000..dfc0290d9 --- /dev/null +++ b/rotators/skywatcher/Makefile.am @@ -0,0 +1,5 @@ + +noinst_LTLIBRARIES = libhamlib-skywatcher.la +libhamlib_skywatcher_la_SOURCES = skywatcher.c skywatcher.h + +EXTRA_DIST = Android.mk diff --git a/rotators/skywatcher/skywatcher.c b/rotators/skywatcher/skywatcher.c new file mode 100644 index 000000000..6c82e11e6 --- /dev/null +++ b/rotators/skywatcher/skywatcher.c @@ -0,0 +1,249 @@ +#include +#include "skywatcher.h" +#include "register.h" +#include "serial.h" + +#define ERROR_CHECK(x) \ + do { \ + int __err_rc = (x); \ + if (__err_rc != RIG_OK) { \ + return __err_rc; \ + } \ + } while (0) + +static const int skywatcher_error_codes_length = 9; +static const char *skywatcher_error_codes[skywatcher_error_codes_length] = { + "Unknown Command", + "Command Length Error", + "Motor not Stopped", + "Invalid Character", + "Not Initialized", + "Driver Sleeping", + "Unknown", + "PEC Training is running", + "No Valid PEC data" +}; + +typedef struct { + uint32_t cpr[2]; +} skywatcher_priv_data; + +static int skywatcher_cmd(ROT *rot, const char *cmd, char *response, size_t response_len) { + hamlib_port_t *port = ROTPORT(rot); + rig_flush(port); + size_t cmd_len = strlen(cmd); + ERROR_CHECK(write_block(port, (unsigned char *) cmd, cmd_len)); + // echo from the device + int code = read_string(port, (unsigned char *) response, response_len, "\r", 1, 0, 1); + if (code < 0) { + return -code; + } + // the actual response + code = read_string(port, (unsigned char *) response, response_len, "\r", 1, 0, 1); + if (code < 0) { + return -code; + } + // nullify last \r + response[strlen(response) - 1] = '\0'; + if (response[0] == '!') { + code = atoi(&response[1]); + if (code < skywatcher_error_codes_length) { + rig_debug(RIG_DEBUG_ERR, "Error code: %d Message: '%s'\n", code, skywatcher_error_codes[code]); + } else { + rig_debug(RIG_DEBUG_ERR, "Error response: '%s'\n", response); + } + return RIG_EPROTO; + } + // remove leading '=' + memmove(response, response + 1, strlen(response) - 1); + response[strlen(response) - 1] = '\0'; + return RIG_OK; +} + +inline uint32_t skywatcher_convert24bit(long input) { + return ((input & 0x0000FF) << 16) | (input & 0x00FF00) | ((input & 0xFF0000) >> 16); +} + +static const char *skywatcher_get_info(ROT *rot) { + static char info[32]; + char str[16]; + + rig_debug(RIG_DEBUG_TRACE, "%s called\n", __func__); + + // Read from 1st motor only. In practice both motors have the same version + if (skywatcher_cmd(rot, ":e1\r", str, sizeof(str)) != RIG_OK) { + return NULL; + } + + SNPRINTF(info, sizeof(info), "V%s", str); + + return info; +} + +int skywatcher_get_spr(ROT *rot, int motor_index, uint32_t *result) { + skywatcher_priv_data *priv = ROTSTATE(rot)->priv; + if (priv->cpr[motor_index - 1] != 0) { + *result = priv->cpr[motor_index - 1]; + return RIG_OK; + } + char str[16]; + char req[16]; + SNPRINTF(req, sizeof(req), ":a%d\r", motor_index); + ERROR_CHECK(skywatcher_cmd(rot, req, str, sizeof(str))); + priv->cpr[motor_index - 1] = skywatcher_convert24bit(strtol(str, NULL, 16)); + *result = priv->cpr[motor_index - 1]; + return RIG_OK; +} + +int skywatcher_get_motor_position(ROT *rot, int motor_index, float *result) { + char str[16]; + char req[16]; + SNPRINTF(req, sizeof(req), ":j%d\r", motor_index); + ERROR_CHECK(skywatcher_cmd(rot, req, str, sizeof(str))); + long motor_ticks = skywatcher_convert24bit(strtol(str, NULL, 16)) - 0x800000; + uint32_t cpr; + ERROR_CHECK(skywatcher_get_spr(rot, motor_index, &cpr)); + double ticks_per_angle = (double) cpr / 360.0; + *result = (float) ((double) motor_ticks / ticks_per_angle); + return RIG_OK; +} + +static int skywatcher_get_position(ROT *rot, azimuth_t *az, elevation_t *el) { + rig_debug(RIG_DEBUG_TRACE, "%s called\n", __func__); + + ERROR_CHECK(skywatcher_get_motor_position(rot, 1, (float *) az)); + // normalize to 0 ~ 360 + if (*az < 0.0f) { + *az = 360.0f + *az; + } + ERROR_CHECK(skywatcher_get_motor_position(rot, 2, (float *) el)); + + rig_debug(RIG_DEBUG_TRACE, "%s: (az, el) = (%.1f, %.1f)\n", __func__, *az, *el); + return RIG_OK; +} + +static int skywatcher_stop(ROT *rot) { + rig_debug(RIG_DEBUG_TRACE, "%s called\n", __func__); + char str[16]; + // don't wait for actual stop. Operation can be async + ERROR_CHECK(skywatcher_cmd(rot, ":K1\r", str, sizeof(str))); + ERROR_CHECK(skywatcher_cmd(rot, ":K2\r", str, sizeof(str))); + return RIG_OK; +} + +int skywatcher_set_motor_position(ROT *rot, int motor_index, float angle) { + char str[16]; + char req[16]; + hamlib_port_t *rotp = ROTPORT(rot); + SNPRINTF(req, sizeof(req), ":f%d\r", motor_index); + int current_retry = 0; + int stopped = 0; + while (current_retry < rotp->retry) { + ERROR_CHECK(skywatcher_cmd(rot, req, str, sizeof(str))); + int status = str[1] - '0'; + if (status & 0b10) { + rig_debug(RIG_DEBUG_ERR, "%s: motor is blocked\n", __func__); + return RIG_EPROTO; + } + if (status & 0b1) { + current_retry++; + //10ms sleep just to make sure motor is fully stopped + hl_usleep(10000); + continue; + } + stopped = 1; + break; + } + if (!stopped) { + return RIG_EPROTO; + } + SNPRINTF(req, sizeof(req), ":G%d00\r", motor_index); + ERROR_CHECK(skywatcher_cmd(rot, req, str, sizeof(str))); + uint32_t cpr; + ERROR_CHECK(skywatcher_get_spr(rot, motor_index, &cpr)); + double ticks_per_angle = (double) cpr / 360.0; + uint32_t value = ((uint32_t) (ticks_per_angle * angle)) & 0xFFFFFF; + value = value | 0x800000; + SNPRINTF(req, sizeof(req), ":S%d%02X%02X%02X\r", motor_index, (value & 0xFF), (value & 0xFF00) >> 8, (value & 0xFF0000) >> 16); + ERROR_CHECK(skywatcher_cmd(rot, req, str, sizeof(str))); + // start motion, don't wait until it completes + SNPRINTF(req, sizeof(req), ":J%d\r", motor_index); + ERROR_CHECK(skywatcher_cmd(rot, req, str, sizeof(str))); + return RIG_OK; +} + +static int skywatcher_set_position(ROT *rot, azimuth_t az, elevation_t el) { + rig_debug(RIG_DEBUG_TRACE, "%s called: %f %f\n", __func__, az, el); + ERROR_CHECK(skywatcher_stop(rot)); + // stop is called for 1 then for 2 motor. do motor position in reverse because it awaits "stop motor" state. + // potentially saving 1 call to :f + ERROR_CHECK(skywatcher_set_motor_position(rot, 2, el)); + ERROR_CHECK(skywatcher_set_motor_position(rot, 1, az)); + return RIG_OK; +} + +static int skywatcher_init(ROT *rot) { + rig_debug(RIG_DEBUG_VERBOSE, "%s called\n", __func__); + skywatcher_priv_data *result = calloc(1, sizeof(skywatcher_priv_data)); + if (result == NULL) { + return -RIG_ENOMEM; + } + memset(result->cpr, 0, sizeof(result->cpr)); + ROTSTATE(rot)->priv = result; + return RIG_OK; +} + +static int skywatcher_cleanup(ROT *rot) { + skywatcher_priv_data *priv = ROTSTATE(rot)->priv; + if (priv == NULL) { + return RIG_OK; + } + free(priv); + return RIG_OK; +} + +/* ************************************************************************* */ +/* + * Protocol documentation: + * https://inter-static.skywatcher.com/downloads/skywatcher_motor_controller_command_set.pdf + */ +const struct rot_caps skywatcher_rot_caps = + { + ROT_MODEL(ROT_MODEL_SKYWATCHER), + .model_name = "Sky-Watcher", + .mfg_name = "Sky-Watcher", + .version = "20240825.0", + .copyright = "LGPL", + .status = RIG_STATUS_STABLE, + .rot_type = ROT_TYPE_AZEL, + .port_type = RIG_PORT_SERIAL, + .serial_rate_min = 9600, + .serial_rate_max = 9600, + .serial_data_bits = 8, + .serial_stop_bits = 1, + .serial_parity = RIG_PARITY_NONE, + .serial_handshake = RIG_HANDSHAKE_NONE, + .write_delay = 0, + .post_write_delay = 0, + .timeout = 3500, /* worst case scenario */ + .retry = 1, + + .min_az = 0.0f, + .max_az = 360.0f, + .min_el = 0.0f, + .max_el = 180.0f, + + .rot_init = skywatcher_init, + .rot_cleanup = skywatcher_cleanup, + .get_position = skywatcher_get_position, + .set_position = skywatcher_set_position, + .stop = skywatcher_stop, + .get_info = skywatcher_get_info, + }; + + +DECLARE_INITROT_BACKEND(skywatcher) { + rig_debug(RIG_DEBUG_VERBOSE, "%s called\n", __func__); + rot_register(&skywatcher_rot_caps); + return RIG_OK; +} \ No newline at end of file diff --git a/rotators/skywatcher/skywatcher.h b/rotators/skywatcher/skywatcher.h new file mode 100644 index 000000000..d45f42506 --- /dev/null +++ b/rotators/skywatcher/skywatcher.h @@ -0,0 +1,26 @@ +/* + * Hamlib Rotator backend - Celestron interface protocol + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ +#ifndef HAMLIB_SKYWATCHER_H +#define HAMLIB_SKYWATCHER_H 1 + +#include "rotator.h" + +extern const struct rot_caps skywatcher_rot_caps; + +#endif //HAMLIB_SKYWATCHER_H diff --git a/src/rot_reg.c b/src/rot_reg.c index 44c0d88b9..5951d597d 100644 --- a/src/rot_reg.c +++ b/src/rot_reg.c @@ -93,6 +93,7 @@ DEFINE_INITROT_BACKEND(grbltrk); DEFINE_INITROT_BACKEND(flir); DEFINE_INITROT_BACKEND(apex); DEFINE_INITROT_BACKEND(saebrtrack); +DEFINE_INITROT_BACKEND(skywatcher); //! @endcond @@ -145,6 +146,7 @@ static struct { ROT_FLIR, ROT_BACKEND_FLIR, ROT_FUNCNAMA(flir) }, { ROT_APEX, ROT_BACKEND_APEX, ROT_FUNCNAMA(apex) }, { ROT_SAEBRTRACK, ROT_BACKEND_SAEBRTRACK, ROT_FUNCNAMA(saebrtrack)}, + { ROT_SKYWATCHER, ROT_BACKEND_SKYWATCHER, ROT_FUNCNAMA(skywatcher)}, { 0, NULL }, /* end */ }; @@ -260,7 +262,7 @@ static int rot_lookup_backend(rot_model_t rot_model) { int i; - for (i = 0; i < ROT_BACKEND_MAX && rot_backend_list[i].be_name; i++) + for (i = 0; i < ROT_BACKEND_MAX && rot_backend_list[i].be_name; i++) { if (ROT_BACKEND_NUM(rot_model) == rot_backend_list[i].be_num)