Implement initial skywatcher rotator

Use GOTO mode when setting position.
Use cache for CPR (Counts Per Revolution) because it is hardcoded for the motor
This commit is contained in:
dernasherbrezon 2024-08-27 23:41:01 +01:00
parent 137231cb7b
commit 099cc7fc48
7 changed files with 309 additions and 2 deletions

View File

@ -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

View File

@ -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.

View File

@ -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)

View File

@ -0,0 +1,5 @@
noinst_LTLIBRARIES = libhamlib-skywatcher.la
libhamlib_skywatcher_la_SOURCES = skywatcher.c skywatcher.h
EXTRA_DIST = Android.mk

View File

@ -0,0 +1,249 @@
#include <stdlib.h>
#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;
}

View File

@ -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

View File

@ -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 */
};