├── .gitignore ├── images ├── block_diagram.png └── nextdriver_schematic.png ├── .lvimrc ├── test ├── test_nexstar │ └── test_nexstar.cpp └── test_astrolib │ ├── generate_test_data.py │ ├── test_astrolib.cpp │ └── test_data.h ├── src ├── serial_command.h ├── serial_command.cpp └── main.cpp ├── platformio.ini ├── LICENSE ├── lib ├── nexstar │ ├── nexstar_base.cpp │ ├── nexstar_aux.h │ ├── nexstar_base.h │ └── nexstar_aux.cpp └── AstroLib │ ├── AstroLib.h │ └── AstroLib.cpp ├── pynexstar ├── test_nexstar.py └── nexstar.py └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .pio 2 | -------------------------------------------------------------------------------- /images/block_diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/juanmb/nextdriver/HEAD/images/block_diagram.png -------------------------------------------------------------------------------- /.lvimrc: -------------------------------------------------------------------------------- 1 | set wildignore+=*.orig 2 | let NERDTreeIgnore=['\.o$', '\.orig$'] 3 | 4 | set softtabstop=4 sw=4 ts=4 et 5 | -------------------------------------------------------------------------------- /images/nextdriver_schematic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/juanmb/nextdriver/HEAD/images/nextdriver_schematic.png -------------------------------------------------------------------------------- /test/test_nexstar/test_nexstar.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include "nexstar_base.h" 6 | 7 | struct TestPosition { 8 | uint32_t uint; 9 | char bin[3]; 10 | }; 11 | 12 | 13 | void test_uint32To24bits(void) { 14 | char out[3]; 15 | uint32To24bits(0x12345678, out); 16 | 17 | char expected[3] = {0x12, 0x34, 0x56}; 18 | TEST_ASSERT_EQUAL_MEMORY(expected, out, 3); 19 | 20 | uint32To24bits(0, out); 21 | char expected0[3] = {0x00, 0x00, 0x00}; 22 | TEST_ASSERT_EQUAL_MEMORY(expected0, out, 3); 23 | } 24 | 25 | void test_uint32From24bits(void) { 26 | char in[3] = {0x12, 0x34, 0x56}; 27 | uint32_t expected = 0x12345600; 28 | uint32_t out = uint32From24bits(in); 29 | TEST_ASSERT_EQUAL(expected, out); 30 | } 31 | 32 | 33 | int main(int argc, char **argv) { 34 | UNITY_BEGIN(); 35 | RUN_TEST(test_uint32To24bits); 36 | RUN_TEST(test_uint32From24bits); 37 | UNITY_END(); 38 | 39 | return 0; 40 | } 41 | -------------------------------------------------------------------------------- /src/serial_command.h: -------------------------------------------------------------------------------- 1 | /****************************************************************** 2 | Author: Juan Menendez Blanco 3 | 4 | This code is part of the NextDriver project: 5 | https://github.com/juanmb/NextDriver 6 | 7 | *******************************************************************/ 8 | 9 | #ifndef _SerialCommand_h_ 10 | #define _SerialCommand_h_ 11 | 12 | #include 13 | 14 | #define MAX_CMD_SIZE 32 15 | #define MAX_COMMANDS 32 16 | 17 | 18 | typedef void (* cbFunction)(char*); 19 | 20 | 21 | typedef struct commandCallback { 22 | char firstChar; 23 | uint8_t size; 24 | cbFunction function; 25 | } commandCallback; 26 | 27 | 28 | class SerialCommand { 29 | public: 30 | SerialCommand(Stream *device); 31 | void readSerial(); 32 | int addCommand(const char, uint8_t, cbFunction); 33 | private: 34 | Stream *stream; 35 | commandCallback commandList[MAX_COMMANDS]; 36 | char buffer[MAX_CMD_SIZE]; 37 | cbFunction cmdFunction; 38 | int nCommands; 39 | int cmdSize; 40 | int bufPos; 41 | }; 42 | 43 | #endif 44 | -------------------------------------------------------------------------------- /platformio.ini: -------------------------------------------------------------------------------- 1 | ; PlatformIO Project Configuration File 2 | ; 3 | ; Build options: build flags, source filter 4 | ; Upload options: custom upload port, speed and extra flags 5 | ; Library options: dependencies, extra library storages 6 | ; Advanced options: extra scripting 7 | ; 8 | ; Please visit documentation for the other options and examples 9 | ; https://docs.platformio.org/page/projectconf.html 10 | 11 | [platformio] 12 | default_envs = uno 13 | 14 | [env:uno] 15 | platform = atmelavr 16 | board = uno 17 | framework = arduino 18 | lib_deps = paulstoffregen/Time@^1.6.0 19 | lib_ignore = 20 | ArduinoFake 21 | DueFlashStorage 22 | #build_flags = -DSIMPLE_AUX_INTERFACE 23 | test_ignore = test_astrolib, test_nexstar 24 | 25 | [env:due] 26 | platform = atmelsam 27 | board = due 28 | framework = arduino 29 | lib_deps = 30 | paulstoffregen/Time@^1.6.0 31 | sebnil/DueFlashStorage@^1.0.0 32 | build_flags = -DTARGET_DUE -DSIMPLE_AUX_INTERFACE 33 | lib_ignore = ArduinoFake 34 | test_ignore = test_astrolib, test_nexstar 35 | 36 | [env:native] 37 | platform = native 38 | lib_deps = fabiobatsilva/ArduinoFake@^0.2.2 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 davidvg 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /src/serial_command.cpp: -------------------------------------------------------------------------------- 1 | /****************************************************************** 2 | Author: Juan Menendez Blanco 3 | 4 | This code is part of the NextDriver project: 5 | https://github.com/juanmb/NextDriver 6 | 7 | *******************************************************************/ 8 | 9 | #include 10 | #include "serial_command.h" 11 | 12 | 13 | SerialCommand::SerialCommand(Stream *dev) 14 | { 15 | stream = dev; 16 | nCommands = 0; 17 | cmdSize = 0; 18 | bufPos = 0; 19 | } 20 | 21 | int SerialCommand::addCommand(const char firstChar, uint8_t size, cbFunction function) 22 | { 23 | if (nCommands >= MAX_COMMANDS) { 24 | return 1; 25 | } 26 | 27 | commandList[nCommands].firstChar = firstChar; 28 | commandList[nCommands].size = size; 29 | commandList[nCommands].function = function; 30 | nCommands++; 31 | return 0; 32 | } 33 | 34 | void SerialCommand::readSerial() 35 | { 36 | while (stream->available()) { 37 | char c = stream->read(); 38 | 39 | if (bufPos == 0) { 40 | cmdSize = 0; 41 | for (int i = 0; i < nCommands; i++) { 42 | if (commandList[i].firstChar == c) { 43 | cmdSize = commandList[i].size; 44 | cmdFunction = commandList[i].function; 45 | break; 46 | } 47 | } 48 | if (cmdSize == 0) { 49 | continue; 50 | } 51 | } 52 | buffer[bufPos++] = c; 53 | if (bufPos >= cmdSize) { 54 | cmdFunction(buffer); 55 | bufPos = 0; 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /lib/nexstar/nexstar_base.cpp: -------------------------------------------------------------------------------- 1 | /****************************************************************** 2 | Author: Juan Menendez Blanco 3 | 4 | This code is part of the NextDriver project: 5 | https://github.com/juanmb/NextDriver 6 | 7 | *******************************************************************/ 8 | 9 | 10 | #include 11 | #include "nexstar_base.h" 12 | 13 | 14 | // The number 0x12345678 will be converted into {0x12, 0x34, 0x56} 15 | void uint32To24bits(uint32_t in, char *out) 16 | { 17 | uint32_t tmp = in; 18 | for (int i=0; i<3; i++) { 19 | tmp >>= 8; 20 | out[2-i] = tmp & 0xff; 21 | } 22 | } 23 | 24 | 25 | // The char array {0x12, 0x34, 0x56} will be converted into 0x12345600 26 | uint32_t uint32From24bits(const char *data) 27 | { 28 | uint32_t out = 0; 29 | 30 | for (int i=0; i<3; i++) { 31 | out |= data[i] & 0xff; 32 | out <<= 8; 33 | } 34 | return out; 35 | } 36 | 37 | 38 | int NexStarBase::sendRawCommand(char *cmd, char *resp, uint8_t *resp_size) { 39 | uint8_t size = cmd[1] - 1; 40 | uint8_t dest = cmd[2]; 41 | uint8_t cmdId = cmd[3]; 42 | char *payload = &cmd[4]; 43 | int ret = 0; 44 | 45 | // expected response length 46 | *resp_size = cmd[7]; 47 | 48 | // process command 49 | switch(cmdId) { 50 | case MC_GET_VER: 51 | ret = getVersion(dest, &resp[0], &resp[1]); 52 | break; 53 | case MC_SET_POSITION: 54 | ret = setPosition(dest, (uint32_t)(*payload)); 55 | break; 56 | case MC_GET_POSITION: 57 | ret = getPosition(dest, (uint32_t*)resp); 58 | break; 59 | case MC_MOVE_POS: 60 | case MC_MOVE_NEG: 61 | ret = move(dest, cmdId == MC_MOVE_POS, payload[0]); 62 | break; 63 | default: 64 | // pass the raw command to the scope 65 | NexStarMessage msg; 66 | ret = sendCommand(dest, cmdId, size, payload, &msg); 67 | if (ret == 0) 68 | memcpy(resp, msg.payload, *resp_size); 69 | } 70 | return ret; 71 | } 72 | -------------------------------------------------------------------------------- /lib/nexstar/nexstar_aux.h: -------------------------------------------------------------------------------- 1 | /****************************************************************** 2 | Author: Juan Menendez Blanco 3 | 4 | This code is part of the NextDriver project: 5 | https://github.com/juanmb/NextDriver 6 | 7 | This code is based on Andre Paquette's documentation about 8 | the NexStar AUX protocol: 9 | http://www.paquettefamily.ca/nexstar/NexStar_AUX_Commands_10.pdf 10 | 11 | *******************************************************************/ 12 | 13 | #ifndef _nexstar_aux_h_ 14 | #define _nexstar_aux_h_ 15 | 16 | #include "nexstar_base.h" 17 | 18 | #define AUX_BAUDRATE 19200 19 | 20 | // Convert between nexstar angle format and radians 21 | float nex2rad(uint16_t angle); 22 | float pnex2rad(uint32_t angle); 23 | uint16_t rad2nex(float rad); 24 | uint32_t rad2pnex(float rad); 25 | 26 | 27 | class NexStarAux : public NexStarBase { 28 | public: 29 | //NexStarAux(int rx, int tx, int select, int busy); 30 | NexStarAux(Stream *serial, int select, int busy); 31 | void init(); 32 | 33 | int setPosition(uint8_t dest, uint32_t pos); 34 | int getPosition(uint8_t dest, uint32_t *pos); 35 | 36 | int gotoPosition(uint8_t dest, bool slow, uint32_t pos); 37 | int move(uint8_t dest, bool dir, uint8_t rate); 38 | int slewDone(uint8_t dest, bool *done); 39 | 40 | int setGuiderate(uint8_t dest, bool dir, bool custom_rate, uint32_t rate); 41 | 42 | int getVersion(uint8_t dest, char *major, char *minor); 43 | 44 | int setApproach(uint8_t dest, bool dir); 45 | int getApproach(uint8_t dest, bool *dir); 46 | 47 | void run() {}; 48 | 49 | //int setAutoGuiderate(uint8_t dest, uint8_t rate); 50 | //int getAutoGuiderate(uint8_t dest, uint8_t *rate); 51 | //int setBacklash(uint8_t dest, bool dir, uint8_t steps); 52 | //int getBacklash(uint8_t dest, bool dir, uint8_t *steps); 53 | 54 | protected: 55 | int sendCommand(uint8_t dest, uint8_t id, uint8_t size, char* data, 56 | NexStarMessage *resp); 57 | 58 | private: 59 | int newMessage(NexStarMessage *msg, uint8_t dest, uint8_t id, 60 | uint8_t size, char* data); 61 | 62 | Stream *serial; 63 | int select_pin; 64 | int busy_pin; 65 | }; 66 | 67 | #endif 68 | -------------------------------------------------------------------------------- /lib/AstroLib/AstroLib.h: -------------------------------------------------------------------------------- 1 | #ifndef _AstroLib_h_ 2 | #define _AstroLib_h_ 3 | 4 | #include 5 | 6 | #ifdef ARDUINO_ARCH_AVR 7 | #include 8 | #else 9 | #include 10 | #endif 11 | 12 | // Location in radians 13 | struct Location { 14 | float lon; 15 | float lat; 16 | }; 17 | 18 | // Horizontal coordinates (Azimuth/altitude) in radians 19 | struct HorizCoords { 20 | float az; // azimuth 21 | float alt; // altitude 22 | }; 23 | 24 | // Equatorial coordinates (RA/dec) in radians 25 | struct EqCoords { 26 | float ra; // right ascension 27 | float dec; // declination 28 | }; 29 | 30 | // Local coordinates (HA/dec) in radians 31 | struct LocalCoords { 32 | float ha; // hour angle 33 | float dec; // declination 34 | }; 35 | 36 | // Mechanical coordinates of an equatorial telescope mount (radians) 37 | // 38 | // The 'ra' angle is: 39 | // * 0: counterweight pointing to the East 40 | // * pi/2: counterweight pointing to the ground 41 | // * pi: counterweight pointing to the West 42 | // 43 | // If 'ra' is pi/2, the 'dec' angle is: 44 | // * 0: tube to the East 45 | // * pi/2: tube to the North pole 46 | // * pi: tube to the West 47 | struct AxisCoords { 48 | float ra; // mechanical angle of RA axis 49 | float dec; // mechanical angle of dec axis 50 | }; 51 | 52 | // Angle in sexagesimal format 53 | // TODO: make 'deg' int16_t and remove the sign field 54 | struct SxAngle { 55 | uint16_t deg; 56 | uint8_t min; 57 | uint8_t sec; 58 | uint8_t sign; 59 | }; 60 | 61 | /***************************************************************************** 62 | Angle conversion functions 63 | ******************************************************************************/ 64 | 65 | float limitDec(float, uint8_t*); 66 | float normalizePi(float); 67 | float normalize2Pi(float); 68 | float sx2rad(SxAngle sx); 69 | void rad2sx(float rad, SxAngle *sx); 70 | void sx2string(SxAngle sx, char *buffer); 71 | 72 | /***************************************************************************** 73 | Date conversion functions 74 | ******************************************************************************/ 75 | 76 | float getJulianDate0(time_t t); 77 | float getJulianDate(time_t t); 78 | float getJ2000Date(time_t t); 79 | float getGMST(time_t t); 80 | float getLST(time_t t, Location loc); 81 | //void dateFromJ2000(float jd, time_t *t); 82 | 83 | /***************************************************************************** 84 | Coordinates conversion functions 85 | ******************************************************************************/ 86 | 87 | void localToHoriz(Location loc, LocalCoords lc, HorizCoords *hor); 88 | void horizToLocal(Location loc, HorizCoords hor, LocalCoords *lc); 89 | void axisToLocalCoords(AxisCoords ac, LocalCoords *lc); 90 | void localToAxisCoords(LocalCoords lc, AxisCoords *ac); 91 | void localToEqCoords(LocalCoords lc, float lst, EqCoords *eq); 92 | void eqToLocalCoords(EqCoords eq, float lst, LocalCoords *lc); 93 | void eqToAxisCoords(EqCoords eq, float lst, AxisCoords *ac); 94 | void axisToEqCoords(AxisCoords ac, float lst, EqCoords *eq); 95 | 96 | #endif 97 | -------------------------------------------------------------------------------- /pynexstar/test_nexstar.py: -------------------------------------------------------------------------------- 1 | from math import pi 2 | import ephem 3 | import unittest 4 | from nexstar import NexStar, RA_DEV, DEC_DEV 5 | 6 | PORT = '/dev/ttyACM0' 7 | 8 | 9 | class TestNexStar(unittest.TestCase): 10 | @classmethod 11 | def setUpClass(cls): 12 | cls.nex = NexStar(PORT, False) 13 | 14 | @classmethod 15 | def tearDownClass(cls): 16 | cls.nex.close() 17 | 18 | def setUp(self): 19 | self.obs = ephem.city('Madrid') 20 | self.obs.date = ephem.Date('2017/12/11 22:02:33') 21 | self.obs.pressure = 0.0 22 | 23 | self.nex.set_location(self.obs.lat, self.obs.long) 24 | self.nex.set_time(str(self.obs.date)) 25 | 26 | def test_echo(self): 27 | c = self.nex.echo('x') 28 | self.assertEqual('x', c) 29 | 30 | def test_versions(self): 31 | self.assertEqual('4.21', self.nex.get_version()) 32 | self.assertEqual('5.07', self.nex.get_device_version(RA_DEV)) 33 | self.assertEqual('5.07', self.nex.get_device_version(DEC_DEV)) 34 | 35 | def test_model(self): 36 | self.assertEqual(10, self.nex.get_model()) 37 | 38 | def test_location(self): 39 | lat, lon = self.nex.get_location() 40 | self.assertAlmostEqual(self.obs.lat.real, lat, 5) 41 | self.assertAlmostEqual(self.obs.lon.real, lon, 5) 42 | 43 | def test_julian_date(self): 44 | jd1 = ephem.julian_date(self.obs.date) - 2451545.0 45 | jd2 = self.nex.get_j2000_date() 46 | self.assertAlmostEqual(jd1, jd2, 4) 47 | 48 | def test_sidereal_time(self): 49 | lst1 = self.obs.sidereal_time() 50 | 51 | self.nex.set_time(str(self.obs.date)) 52 | lst2 = self.nex.get_sidereal_time() 53 | 54 | # difference should be < 10 seconds 55 | self.assertLess(abs(lst1 - lst2), 10*pi/43200) 56 | 57 | def test_eq_coords(self): 58 | stars = ['Polaris', 'Dubhe', 'Merak', 'Deneb', 'Algol', 'Vega', 59 | 'Altair', 'Rigel', 'Spica', 'Sirius', 'Achernar', 'Canopus'] 60 | 61 | for star in stars: 62 | star = ephem.star(star) 63 | star.compute(self.obs) 64 | 65 | self.nex.sync_eq_coords(star.ra, star.dec, precise=True) 66 | 67 | ra, dec = self.nex.get_eq_coords(precise=False) 68 | self.assertAlmostEqual(star.ra, ra, 3) 69 | self.assertAlmostEqual(star.dec, dec, 3) 70 | 71 | ra, dec = self.nex.get_eq_coords(precise=True) 72 | self.assertAlmostEqual(star.ra, ra, 5) 73 | self.assertAlmostEqual(star.dec, dec, 5) 74 | 75 | def test_az_coords(self): 76 | stars = ['Polaris', 'Dubhe', 'Merak', 'Deneb', 'Algol', 'Vega', 77 | 'Altair', 'Rigel', 'Spica', 'Sirius', 'Achernar', 'Canopus'] 78 | 79 | for star in stars: 80 | star = ephem.star(star) 81 | star.compute(self.obs) 82 | 83 | self.nex.sync_eq_coords(star.ra, star.dec, precise=True) 84 | 85 | az, alt = self.nex.get_az_coords(precise=False) 86 | self.assertAlmostEqual(star.az, az, 2) 87 | self.assertAlmostEqual(star.alt, alt, 2) 88 | 89 | az, alt = self.nex.get_az_coords(precise=True) 90 | self.assertAlmostEqual(star.az, az, 3) 91 | self.assertAlmostEqual(star.alt, alt, 3) 92 | 93 | 94 | 95 | if __name__ == '__main__': 96 | unittest.main() 97 | -------------------------------------------------------------------------------- /test/test_astrolib/generate_test_data.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # Generate data for testing the AstroLib code 4 | 5 | import sys 6 | from math import pi, floor 7 | from random import random 8 | import ephem 9 | from jinja2 import Template 10 | from ephem.stars import stars as ephem_stars 11 | 12 | tmpl = Template('''/* Test data generated by running '{{command}}'. DO NOT EDIT! */ 13 | 14 | #include "AstroLib.h" 15 | 16 | struct TestAngle { 17 | float rad; 18 | SxAngle sx; 19 | }; 20 | 21 | struct TestDate { 22 | const char *date; 23 | float jd; // Julian date 24 | float gmst; // Greenwich mean sidereal time 25 | float lst; // Local sidereal time 26 | }; 27 | 28 | struct TestStar { 29 | const char *name; 30 | EqCoords eq; 31 | HorizCoords hc; 32 | }; 33 | 34 | TestAngle test_angles[] = { 35 | {%- for a in angles %} 36 | { {{ a['rad'] | round(8)}}, { {{a['deg']}}, {{a['min']}}, {{a['sec']}}, {{a['sign']}} } }, 37 | {%- endfor %} 38 | }; 39 | 40 | TestDate test_dates[] = { 41 | {%- for d in dates %} 42 | { "{{ d['str']}}", {{d['jd'] | round(6)}}, {{d['gmst'] | round(6)}}, {{d['lst'] | round(6)}} }, 43 | {%- endfor %} 44 | }; 45 | 46 | const char *obs_date = "{{obs.date}}"; 47 | Location obs_loc = { {{obs.lon | float}}, {{obs.lat | float}} }; 48 | 49 | TestStar test_stars[] = { 50 | {%- for s in stars %} 51 | { "{{ s.name}}", { {{s.ra | round(6)}}, {{s.dec | round(6)}} }, { {{s.az | round(6)}}, {{s.alt | round(6)}} {{'}}'}}, 52 | {%- endfor %} 53 | }; 54 | ''') 55 | 56 | def rad2sx(a): 57 | deg = abs(a*180/pi) 58 | min = 60*(deg % 1) 59 | sec = 60*(min % 1) 60 | return int(floor(deg)), int(floor(min)), int(round(sec)) 61 | 62 | 63 | def gen_test_angles(): 64 | """Generate test angles""" 65 | test_angles = [2*pi*random() for i in range(20)] 66 | test_angles += [-2*pi*random() for i in range(20)] 67 | angles = [] 68 | for a in test_angles: 69 | deg, min, sec = rad2sx(a) 70 | angle = { 71 | 'rad': a, 72 | 'deg': deg, 73 | 'min': min, 74 | 'sec': sec, 75 | 'sign': int(a < 0), 76 | } 77 | angles.append(angle) 78 | return angles 79 | 80 | 81 | def gen_test_dates(obs): 82 | """Generate test dates""" 83 | greenwich = ephem.Observer() 84 | greenwich.lat = '51:28:38' 85 | 86 | str_dates = [ 87 | '1978/11/13 04:34:00', 88 | '1984/05/30 02:20:00', 89 | '2000/01/01 00:00:00', 90 | '2003/05/06 08:03:01', 91 | '2004/02/29 12:59:11', 92 | '2014/12/31 16:10:22', 93 | '2017/12/11 22:02:33', 94 | '2037/05/11 20:01:44', 95 | ] 96 | 97 | dates = [] 98 | for str_date in str_dates: 99 | greenwich.date = str_date 100 | obs.date = str_date 101 | date = { 102 | 'str': str_date, 103 | 'jd': ephem.julian_date(str_date), 104 | 'gmst': greenwich.sidereal_time(), 105 | 'lst': obs.sidereal_time(), 106 | } 107 | dates.append(date) 108 | return dates 109 | 110 | 111 | def gen_test_stars(obs): 112 | stars = [] 113 | for name, star in ephem_stars.items(): 114 | star.compute(obs) 115 | stars.append(star) 116 | return stars 117 | 118 | 119 | if __name__ == "__main__": 120 | obs = ephem.city('Madrid') 121 | obs.pressure = 0 # ignore the effect of refraction 122 | 123 | angles = gen_test_angles() 124 | dates = gen_test_dates(obs) 125 | 126 | obs.date = '2020/9/12 01:50:41' 127 | stars = gen_test_stars(obs) 128 | 129 | txt = tmpl.render(command=sys.argv[0], angles=angles, dates=dates, 130 | stars=stars, obs=obs) 131 | open('test_data.h', 'w').write(txt) 132 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NextDriver 2 | 3 | An Arduino-based controller for Celestron CG5-series equatorial mounts. 4 | 5 | ## Disclaimer 6 | 7 | :warning: **This is a work in progress!** :warning: 8 | 9 | No stable version was released yet, so you can expect some critical bugs to occur. 10 | It shouldn't be used in unattended telescopes! 11 | 12 | Currently NextDriver was tested only with [KStars/Ekos](https://www.indilib.org/about/ekos.html) 13 | using the [Celestron NexStar](https://www.indilib.org/telescopes/celestron/celestron-nexstar.html) INDI driver to control a 14 | [Celestron CG-5 GT Advanced Series](https://s3.amazonaws.com/celestron-site-support-files/support_files/1196122269_cg5mount9151791.pdf) mount. 15 | 16 | ## Description 17 | 18 | NextDriver is a USB controller for the Celestron NexStar AUX protocol 19 | implemented in Arduino. The Arduino replaces the hand controller, receiving 20 | the commands from the PC (HC procotol) and controlling the mount (AUX protocol). 21 | 22 | With **NextDriver** you won't need a hand control to control your Celestron mount anymore. 23 | It allows a fully remote operation of the mount by removing the tedious manual startup proceeding. 24 | 25 | ![](images/block_diagram.png) 26 | 27 | Documentation on the NexStar HC and Aux protocols: 28 | - [NexStar HC Protocol](http://www.nexstarsite.com/download/manuals/NexStarCommunicationProtocolV1.2.zip) 29 | - [NexStar AUX Protocol](http://www.paquettefamily.ca/nexstar/NexStar_AUX_Commands_10.pdf) 30 | 31 | ### LEDs 32 | - Dual color red-green status LED: 33 | - Red: time not set 34 | - Yellow: time set. no synced/aligned 35 | - Green: synced/aligned 36 | - Green LED: slewing 37 | - Red LED: Error (limits, communication) 38 | - Yellow: not used yet 39 | 40 | ## Hardware 41 | 42 | ![](images/nextdriver_schematic.png) 43 | 44 | Required parts: 45 | - Arduino UNO 46 | - [Prototype Expansion Board for Arduino UNO](https://www.aliexpress.com/item/33045177683.html) 47 | - Infrared sensor modules. You can build the circuit shown in the schematics or 48 | you can buy [already-built modules](https://www.aliexpress.com/item/32886394063.html) 49 | - [Limit switch](https://www.aliexpress.com/item/4001033375208.html) for RA axis 50 | - [Tilt switch](https://www.aliexpress.com/item/33040347015.html) for declination axis 51 | - [6P6C female connector](https://www.aliexpress.com/item/33041450076.html) 52 | - [RJ12 male to male cable](https://www.aliexpress.com/item/1005001412508363.html) 53 | - 1N4148 (or similar) diode 54 | - Red, green, yellow LEDs 55 | - Dual red-green LED 56 | - Resistors (shown in the schematics) 57 | 58 | ## Features 59 | 60 | - **Home (index) sensors** to allow remote startup without requiring manual alignment 61 | - **Limit sensors** to avoid collisions beween the telescope and the mount 62 | - Automatic **meridian flip** when slewing to a target 63 | - Home and abort buttons 64 | - Status LEDs 65 | 66 | Supported HC commands: 67 | - GetEqCoords 68 | - GetAzCoords 69 | - GetPierSide 70 | - SyncEqCoords 71 | - GotoEqCoords 72 | - GotoAzCoords 73 | - GotoInProgress 74 | - CancelGoto 75 | - IsAligned 76 | - SetTrackingMode 77 | - GetTrackingMode 78 | - SetLocation 79 | - GetLocation 80 | - SetTime 81 | - GetTime 82 | - GetVersion 83 | - GetVariant 84 | - GetModel 85 | - Hibernate (not working yet) 86 | - Wakeup 87 | - Echo 88 | - PassThrough 89 | 90 | ## Building 91 | 92 | The code is built using [PlatformIO](https://platformio.org/), so you'll need to install it 93 | in order to build the Arduino firmware and upload it to the board. 94 | 95 | Test the code in your computer (it does not require an Arduino board): 96 | 97 | pio platform install native 98 | pio test -e native 99 | 100 | Build the firmware for Arduino UNO (default target): 101 | 102 | pio run 103 | 104 | Program the board: 105 | 106 | pio run -t upload 107 | 108 | 109 | ## Current limitations 110 | 111 | Currently, only CG-5 mounts in the North hemisphere are supported. 112 | -------------------------------------------------------------------------------- /lib/nexstar/nexstar_base.h: -------------------------------------------------------------------------------- 1 | /****************************************************************** 2 | Author: Juan Menendez Blanco 3 | 4 | This code is part of the NextDriver project: 5 | https://github.com/juanmb/NextDriver 6 | 7 | *******************************************************************/ 8 | 9 | #ifndef _nexstar_scope_h_ 10 | #define _nexstar_scope_h_ 11 | 12 | #include 13 | 14 | #define MAX_PAYLOAD_SIZE 10 15 | 16 | #define ERR_INVALID -1 17 | #define ERR_TIMEOUT -2 18 | #define ERR_BAD_SIZE -3 19 | #define ERR_BAD_HDR -4 20 | #define ERR_CRC -5 21 | 22 | #define TRACKING_NONE 0 23 | #define TRACKING_ALTAZ 1 24 | #define TRACKING_EQ_NORTH 2 25 | #define TRACKING_EQ_SOUTH 3 26 | 27 | #define GUIDERATE_NEG 0 28 | #define GUIDERATE_POS 1 29 | 30 | #define GUIDERATE_SIDEREAL 0xffff 31 | #define GUIDERATE_SOLAR 0xfffe 32 | #define GUIDERATE_LUNAR 0xfffd 33 | 34 | // device IDs 35 | #define DEV_MAIN 0x01 36 | #define DEV_HC 0x04 37 | #define DEV_RA 0x10 38 | #define DEV_AZ 0x10 39 | #define DEV_DEC 0x11 40 | #define DEV_ALT 0x11 41 | #define DEV_GPS 0xb0 42 | #define DEV_RTC 0xb2 43 | 44 | // command IDs 45 | #define MC_GET_POSITION 0x01 46 | #define MC_GOTO_FAST 0x02 47 | #define MC_SET_POSITION 0x04 48 | #define MC_SET_POS_GUIDERATE 0x06 49 | #define MC_SET_NEG_GUIDERATE 0x07 50 | #define MC_LEVEL_START 0x0b 51 | #define MC_PEC_RECORD_START 0x0c 52 | #define MC_PEC_PLAYBACK 0x0d 53 | #define MC_SET_POS_BACKLASH 0x10 54 | #define MC_SET_NEG_BACKLASH 0x11 55 | #define MC_LEVEL_DONE 0x12 56 | #define MC_SLEW_DONE 0x13 57 | #define MC_PEC_RECORD_DONE 0x15 58 | #define MC_PEC_RECORD_STOP 0x16 59 | #define MC_GOTO_SLOW 0x17 60 | #define MC_AT_INDEX 0x18 61 | #define MC_SEEK_INDEX 0x19 62 | #define MC_MOVE_POS 0x24 63 | #define MC_MOVE_NEG 0x25 64 | #define MC_MOVE_PULSE 0x26 65 | #define MC_GET_PULSE_STATUS 0x27 66 | #define MC_ENABLE_CORDWRAP 0x38 67 | #define MC_DISABLE_CORDWRAP 0x39 68 | #define MC_SET_CORDWRAP_POS 0x3a 69 | #define MC_POLL_CORDWRAP 0x3b 70 | #define MC_GET_CORDWRAP_POS 0x3c 71 | #define MC_GET_POS_BACKLASH 0x40 72 | #define MC_GET_NEG_BACKLASH 0x41 73 | #define MC_SET_AUTOGUIDE_RATE 0x46 74 | #define MC_GET_AUTOGUIDE_RATE 0x47 75 | #define MC_PROGRAM_ENTER 0x81 76 | #define MC_PROGRAM_INIT 0x82 77 | #define MC_PROGRAM_DATA 0x83 78 | #define MC_PROGRAM_END 0x84 79 | #define MC_GET_APPROACH 0xfc 80 | #define MC_SET_APPROACH 0xfd 81 | #define MC_GET_VER 0xfe 82 | 83 | 84 | void uint32To24bits(uint32_t in, char *out); 85 | uint32_t uint32From24bits(const char *data); 86 | 87 | 88 | typedef struct NexStarHeader { 89 | uint8_t preamble; 90 | uint8_t length; 91 | uint8_t source; 92 | uint8_t dest; 93 | uint8_t id; 94 | } NexStarHeader; 95 | 96 | 97 | struct NexStarMessage { 98 | NexStarHeader header; 99 | char payload[MAX_PAYLOAD_SIZE]; 100 | uint8_t crc; 101 | }; 102 | 103 | 104 | class NexStarBase { 105 | protected: 106 | virtual int sendCommand(uint8_t dest, uint8_t id, uint8_t size, char* data, 107 | NexStarMessage *resp); 108 | 109 | public: 110 | virtual int setPosition(uint8_t dest, uint32_t pos); 111 | virtual int getPosition(uint8_t dest, uint32_t *pos); 112 | virtual int gotoPosition(uint8_t dest, bool slow, uint32_t pos); 113 | virtual int move(uint8_t dest, bool dir, uint8_t rate); 114 | virtual int slewDone(uint8_t dest, bool *done); 115 | virtual int setGuiderate(uint8_t dest, bool dir, bool custom_rate, uint32_t rate); 116 | virtual int getVersion(uint8_t dest, char *major, char *minor); 117 | 118 | int sendRawCommand(char *cmd, char *resp, uint8_t *resp_size); 119 | 120 | // Call this method periodically in the main loop 121 | virtual void run(); 122 | }; 123 | 124 | #endif 125 | -------------------------------------------------------------------------------- /lib/nexstar/nexstar_aux.cpp: -------------------------------------------------------------------------------- 1 | /****************************************************************** 2 | Author: Juan Menendez Blanco 3 | 4 | This code is part of the NextDriver project: 5 | https://github.com/juanmb/NextDriver 6 | 7 | This code is based on Andre Paquette's documentation about 8 | the NexStar AUX protocol: 9 | http://www.paquettefamily.ca/nexstar/NexStar_AUX_Commands_10.pdf 10 | 11 | *******************************************************************/ 12 | 13 | #include 14 | #include "nexstar_aux.h" 15 | 16 | #define RESP_TIMEOUT 800 // timeout in milliseconds 17 | 18 | #ifdef TARGET_DUE 19 | // In Arduino DUE, use Serial1 20 | #define serialBegin(x) (Serial1.begin(x)) 21 | #define serialWrite(x) (Serial1.write(x)) 22 | #define serialFlush(x) (Serial1.flush()) 23 | #define serialAvailable() (Serial1.available()) 24 | #define serialRead() (Serial1.read()) 25 | #else 26 | // In AVR-based Arduinos, use SoftwareSerial 27 | #define serialBegin(x) (serial->begin(x)) 28 | #define serialWrite(x) (serial->write(x)) 29 | #define serialFlush() (serial->flush()) 30 | #define serialAvailable() (serial->available()) 31 | #define serialRead() (serial->read()) 32 | 33 | #ifndef UNIT_TEST 34 | #include 35 | SoftwareSerial *serial; 36 | #endif 37 | #endif 38 | 39 | // Convert nexstar angle format to radians 40 | float nex2rad(uint16_t angle) 41 | { 42 | return 2*M_PI*((float)angle / 0x10000); 43 | } 44 | 45 | // Convert nexstar precise angle format to radians 46 | float pnex2rad(uint32_t angle) 47 | { 48 | return 2*M_PI*((float)angle / 0x100000000); 49 | } 50 | 51 | // Convert radians to nexstar angle format 52 | uint16_t rad2nex(float rad) 53 | { 54 | return (uint16_t)(rad * 0x10000 / (2*M_PI)); 55 | } 56 | 57 | // Convert radians to nexstar precise angle format 58 | uint32_t rad2pnex(float rad) 59 | { 60 | return (uint32_t)(rad * 0x100000000 / (2*M_PI)); 61 | } 62 | 63 | 64 | uint8_t calcCRC(NexStarMessage *msg) 65 | { 66 | int result = 0; 67 | char *data = (char*)msg; 68 | 69 | for (int i = 1; i < msg->header.length + 2; i++) { 70 | result += data[i]; 71 | } 72 | return -result & 0xff; 73 | } 74 | 75 | 76 | NexStarAux::NexStarAux(Stream *serial, int select, int busy) 77 | { 78 | this->serial = serial; 79 | //#ifdef __AVR__ 80 | //serial = new SoftwareSerial(rx, tx); 81 | //#endif 82 | 83 | select_pin = select; 84 | busy_pin = busy; 85 | } 86 | 87 | // Initialize pins and setup serial port 88 | void NexStarAux::init() 89 | { 90 | // Select and Busy pins can be the same pin, allowing very simple hardware 91 | // to be used (requiring only a resistor and a diode) to interface the AUX connector. 92 | pinMode(busy_pin, INPUT); 93 | 94 | if (select_pin != busy_pin) 95 | pinMode(select_pin, OUTPUT); 96 | } 97 | 98 | // Fill NexStarMessage struct 99 | // data: payload data 100 | // size: size of payload data 101 | int NexStarAux::newMessage(NexStarMessage *msg, uint8_t dest, uint8_t 102 | id, uint8_t size, char* data) 103 | { 104 | if (size > MAX_PAYLOAD_SIZE) { 105 | return ERR_INVALID; 106 | } 107 | msg->header.preamble = 0x3b; 108 | msg->header.length = size + 3; 109 | msg->header.source = DEV_HC; 110 | msg->header.dest = dest; 111 | msg->header.id = id; 112 | memcpy(msg->payload, data, size); 113 | msg->crc = calcCRC(msg); 114 | return 0; 115 | } 116 | 117 | // Send a message and receive its response 118 | int NexStarAux::sendCommand(uint8_t dest, uint8_t id, uint8_t size, 119 | char* data, NexStarMessage *resp) 120 | { 121 | NexStarMessage msg; 122 | char *bytes = (char*)(&msg); 123 | 124 | int ret = newMessage(&msg, dest, id, size, data); 125 | if (ret != 0) { 126 | return ret; 127 | } 128 | 129 | digitalWrite(select_pin, LOW); 130 | if (select_pin == busy_pin) 131 | pinMode(select_pin, OUTPUT); 132 | 133 | for (int i = 0; i < size + 5; i++) { 134 | serialWrite(bytes[i]); 135 | } 136 | 137 | serialWrite(msg.crc); 138 | serialFlush(); 139 | 140 | if (select_pin == busy_pin) 141 | pinMode(select_pin, INPUT); 142 | else 143 | digitalWrite(select_pin, HIGH); 144 | 145 | long int t0 = millis(); 146 | 147 | delay(1); 148 | // wait while select pin is high 149 | while(digitalRead(busy_pin) == HIGH) { 150 | if (millis() - t0 > RESP_TIMEOUT) { 151 | return ERR_TIMEOUT; 152 | } 153 | delay(1); 154 | } 155 | // wait while select pin is low 156 | while(digitalRead(busy_pin) == LOW) { 157 | delay(1); 158 | if (millis() - t0 > RESP_TIMEOUT) { 159 | return ERR_TIMEOUT; 160 | } 161 | } 162 | 163 | bytes = (char*)(resp); 164 | unsigned int pos = 0; 165 | while (serialAvailable()) { 166 | bytes[pos++] = serialRead(); 167 | if (pos >= sizeof(NexStarMessage)) { 168 | return ERR_BAD_SIZE; 169 | } 170 | } 171 | if (pos <= sizeof(NexStarHeader) + 1) { 172 | return ERR_BAD_SIZE; 173 | } 174 | resp->crc = bytes[resp->header.length + 2]; 175 | if (calcCRC(resp) != resp->crc) { 176 | return ERR_CRC; 177 | } 178 | return 0; 179 | } 180 | 181 | int NexStarAux::setPosition(uint8_t dest, uint32_t pos) 182 | { 183 | NexStarMessage resp; 184 | char payload[3]; 185 | uint32To24bits(pos, payload); 186 | return sendCommand(dest, MC_SET_POSITION, 3, payload, &resp); 187 | } 188 | 189 | int NexStarAux::getPosition(uint8_t dest, uint32_t *pos) 190 | { 191 | NexStarMessage resp; 192 | int ret = sendCommand(dest, MC_GET_POSITION, 0, NULL, &resp); 193 | *pos = uint32From24bits(resp.payload); 194 | return ret; 195 | } 196 | 197 | int NexStarAux::gotoPosition(uint8_t dest, bool slow, uint32_t pos) 198 | { 199 | NexStarMessage resp; 200 | char payload[3]; 201 | uint32To24bits(pos, payload); 202 | 203 | char cmdId = slow ? MC_GOTO_SLOW : MC_GOTO_FAST; 204 | return sendCommand(dest, cmdId, 3, payload, &resp); 205 | } 206 | 207 | int NexStarAux::move(uint8_t dest, bool dir, uint8_t rate) 208 | { 209 | NexStarMessage resp; 210 | uint8_t payload[1] = { rate }; 211 | 212 | char cmdId = dir ? MC_MOVE_POS : MC_MOVE_NEG; 213 | return sendCommand(dest, cmdId, 1, (char *)payload, &resp); 214 | } 215 | 216 | int NexStarAux::slewDone(uint8_t dest, bool *done) 217 | { 218 | NexStarMessage resp; 219 | int ret = sendCommand(dest, MC_SLEW_DONE, 0, NULL, &resp); 220 | *done = (bool)resp.payload[0]; 221 | return ret; 222 | } 223 | 224 | int NexStarAux::setGuiderate(uint8_t dest, bool dir, bool custom_rate, uint32_t rate) 225 | { 226 | NexStarMessage resp; 227 | 228 | char payload[3]; 229 | uint32To24bits(rate << 16, payload); 230 | 231 | char cmdId = dir ? MC_SET_POS_GUIDERATE : MC_SET_NEG_GUIDERATE; 232 | char msgSize = custom_rate ? 3 : 2; 233 | return sendCommand(dest, cmdId, msgSize, payload, &resp); 234 | } 235 | 236 | int NexStarAux::setApproach(uint8_t dest, bool dir) 237 | { 238 | NexStarMessage resp; 239 | char payload[1] = { dir }; 240 | return sendCommand(dest, MC_SET_APPROACH, 1, payload, &resp); 241 | } 242 | 243 | int NexStarAux::getApproach(uint8_t dest, bool *dir) 244 | { 245 | NexStarMessage resp; 246 | int ret = sendCommand(dest, MC_GET_APPROACH, 0, NULL, &resp); 247 | *dir = (bool)resp.payload[0]; 248 | return ret; 249 | } 250 | 251 | int NexStarAux::getVersion(uint8_t dest, char *major, char *minor) 252 | { 253 | NexStarMessage resp; 254 | int ret = sendCommand(dest, MC_GET_VER, 0, NULL, &resp); 255 | *major = resp.payload[0]; 256 | *minor = resp.payload[1]; 257 | return ret; 258 | } 259 | -------------------------------------------------------------------------------- /test/test_astrolib/test_astrolib.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include "AstroLib.h" 6 | #include "test_data.h" 7 | 8 | 9 | // Parse a UTC string date into a timestamp value 10 | time_t parse_date(const char *date) { 11 | struct tm tm; 12 | strptime(date, "%Y/%m/%d %T", &tm); 13 | return timegm(&tm); 14 | } 15 | 16 | void test_astrolib_limitDec(void) { 17 | uint8_t over; 18 | TEST_ASSERT_EQUAL_FLOAT(0.0, limitDec(0.0, &over)); 19 | TEST_ASSERT_FALSE(over); 20 | TEST_ASSERT_EQUAL_FLOAT(M_PI/4, limitDec(M_PI/4, &over)); 21 | TEST_ASSERT_FALSE(over); 22 | TEST_ASSERT_EQUAL_FLOAT(M_PI/2, limitDec(M_PI/2, &over)); 23 | TEST_ASSERT_TRUE(over); 24 | TEST_ASSERT_EQUAL_FLOAT(M_PI/4, limitDec(3*M_PI/4, &over)); 25 | TEST_ASSERT_TRUE(over); 26 | TEST_ASSERT_FLOAT_WITHIN(1e-6, 0, limitDec(M_PI, &over)); 27 | TEST_ASSERT_TRUE(over); 28 | TEST_ASSERT_EQUAL_FLOAT(-M_PI/4, limitDec(-M_PI/4, &over)); 29 | TEST_ASSERT_FALSE(over); 30 | TEST_ASSERT_EQUAL_FLOAT(-M_PI/4, limitDec(-3*M_PI/4, &over)); 31 | TEST_ASSERT_TRUE(over); 32 | TEST_ASSERT_FLOAT_WITHIN(1e-6, 0, limitDec(-M_PI, &over)); 33 | TEST_ASSERT_TRUE(over); 34 | } 35 | 36 | void test_astrolib_normalize2Pi(void) { 37 | TEST_ASSERT_EQUAL_FLOAT(0.0, normalize2Pi(0.0)); 38 | TEST_ASSERT_EQUAL_FLOAT(4.0, normalize2Pi(4.0)); 39 | TEST_ASSERT_EQUAL_FLOAT(2*M_PI - 2.0, normalize2Pi(-2.0)); 40 | TEST_ASSERT_EQUAL_FLOAT(3.0, normalize2Pi(4*M_PI + 3.0)); 41 | TEST_ASSERT_FLOAT_WITHIN(1e-6, 0.0, normalize2Pi(2*M_PI)); 42 | TEST_ASSERT_FLOAT_WITHIN(1e-6, 0.0, normalize2Pi(8*M_PI)); 43 | TEST_ASSERT_FLOAT_WITHIN(1e-6, 2*M_PI, normalize2Pi(-8*M_PI)); 44 | } 45 | 46 | void test_astrolib_normalizePi(void) { 47 | TEST_ASSERT_EQUAL_FLOAT(0.0, normalizePi(0.0)); 48 | TEST_ASSERT_EQUAL_FLOAT(3.0, normalizePi(3.0)); 49 | TEST_ASSERT_EQUAL_FLOAT(-M_PI, normalizePi(M_PI)); 50 | TEST_ASSERT_EQUAL_FLOAT(-3.0, normalizePi(2*M_PI - 3.0)); 51 | TEST_ASSERT_EQUAL_FLOAT(3.0, normalizePi(8*M_PI + 3.0)); 52 | TEST_ASSERT_EQUAL_FLOAT(-3.0, normalizePi(8*M_PI - 3.0)); 53 | } 54 | 55 | void test_astrolib_rad2sx(void) { 56 | for (int i=0; i pi: 41 | return angle - 2*pi 42 | return angle 43 | 44 | 45 | class NexStar(object): 46 | def __init__(self, port, debug=False): 47 | self.debug = debug 48 | self.serial = serial.Serial(port, 9600, timeout=1.0) 49 | self.serial.flushInput() 50 | #print "Wait until Arduino is ready..." 51 | time.sleep(2) 52 | 53 | def close(self): 54 | self.serial.close() 55 | 56 | def send_packet(self, data, resp_len): 57 | if self.debug: 58 | print "Sent:", to_hex(data) if data[0] in 'PWH' else data 59 | self.serial.write(data) 60 | self.serial.flushInput() 61 | resp = self.serial.read(resp_len) 62 | if self.debug: 63 | print "Recv:", resp if data[0] in 'eEzZ' else to_hex(resp) 64 | return resp 65 | 66 | def send_passthrough(self, dest, Id, payload='', resp_len=0): 67 | if len(payload) > 4: 68 | raise ValueError("Invalid payload size") 69 | 70 | length = len(payload) + 1 71 | fill = '\0'*(3 - len(payload)) 72 | fmt = 'cBBB%ds%dsB' % (len(payload), len(fill)) 73 | data = struct.pack(fmt, 'P', length, dest, Id, payload, fill, resp_len-1) 74 | return self.send_packet(data, resp_len) 75 | 76 | def get_version(self): 77 | ret = self.send_packet('V', 3) 78 | return '%d.%02d' % (ord(ret[0]), ord(ret[1])) 79 | 80 | def get_variant(self): 81 | ret = self.send_packet('v', 3) 82 | return ret[0] 83 | 84 | def get_model(self): 85 | ret = self.send_packet('m', 2) 86 | return ord(ret[0]) 87 | 88 | def echo(self, char): 89 | ret = self.send_packet('K%c' % char, 2) 90 | return ret[0] 91 | 92 | def get_device_version(self, dest): 93 | ret = self.send_passthrough(dest, 0xfe, resp_len=3) 94 | return '%d.%02d' % (ord(ret[0]), ord(ret[1])) 95 | 96 | def slew_ra(self, rate, direction): 97 | _id = 0x24 if direction else 0x25 98 | payload = struct.pack('B', rate) 99 | self.send_passthrough(RA_DEV, _id, payload) 100 | 101 | def get_eq_coords(self, precise=False): 102 | '''Get equatorial coords in radians''' 103 | if precise: 104 | ret = self.send_packet('e', 18) 105 | else: 106 | ret = self.send_packet('E', 10) 107 | 108 | ra, dec = [int(a, 16) for a in ret.strip('#').split(',')] 109 | return nex2rad(ra, precise), normalize_dec(nex2rad(dec, precise)) 110 | 111 | def get_az_coords(self, precise=False): 112 | if precise: 113 | ret = self.send_packet('z', 18) 114 | else: 115 | ret = self.send_packet('Z', 10) 116 | 117 | az, alt = [int(a, 16) for a in ret.strip('#').split(',')] 118 | return nex2rad(az, precise), normalize_dec(nex2rad(alt, precise)) 119 | 120 | def sync_eq_coords(self, ra, dec, precise=False): 121 | coords = rad2nex(ra, precise), rad2nex(dec, precise) 122 | if precise: 123 | self.send_packet('s%08X,%08X' % coords, 1) 124 | else: 125 | self.send_packet('S%04X,%04X' % coords, 1) 126 | 127 | def goto_eq_coords(self, ra, dec, precise=False): 128 | coords = rad2nex(ra, precise), rad2nex(dec, precise) 129 | if precise: 130 | self.send_packet('r%08X,%08X' % coords, 1) 131 | else: 132 | self.send_packet('R%04X,%04X' % coords, 1) 133 | 134 | def set_location(self, lat, lon): 135 | ''' Set location in radians ''' 136 | lat = ephem.degrees(lat) 137 | lon = ephem.degrees(lon) 138 | 139 | lat_split = [int(float(s)) for s in str(lat).split(':')] 140 | lon_split = [int(float(s)) for s in str(lon).split(':')] 141 | lat_sign = int(lat_split[0] < 0) 142 | lon_sign = int(lon_split[0] < 0) 143 | lat_split[0] = abs(lat_split[0]) 144 | lon_split[0] = abs(lon_split[0]) 145 | 146 | args = lat_split + [lat_sign] + lon_split + [lon_sign] 147 | payload = struct.pack('8B', *args) 148 | self.send_packet('W%s' % payload, 1); 149 | 150 | def get_location(self): 151 | ret = self.send_packet('w', 9) 152 | lat_deg, lat_min, lat_sec, lat_sign, lon_deg, lon_min, lon_sec, lon_sign, _ = \ 153 | struct.unpack('BBBBBBBBB', ret) 154 | 155 | if lat_sign: 156 | lat_deg *= -1 157 | 158 | if lon_sign: 159 | lon_deg *= -1 160 | 161 | lat = '%d:%d:%d' % (lat_deg, lat_min, lat_sec) 162 | lon = '%d:%d:%d' % (lon_deg, lon_min, lon_sec) 163 | return ephem.degrees(lat).real, ephem.degrees(lon).real 164 | 165 | def set_time(self, date_str): 166 | t = time.strptime(date_str, '%Y/%m/%d %H:%M:%S') 167 | payload = struct.pack('BBBBBBBB', t.tm_hour, t.tm_min, t.tm_sec, 168 | t.tm_mon, t.tm_mday, t.tm_year % 2000, 0, 0) 169 | self.send_packet('H%s' % payload, 1); 170 | 171 | def get_time(self): 172 | ret = self.send_packet('h', 9) 173 | h, m, s, M, D, Y, offset, dst, _ = struct.unpack('BBBBBBBBB', ret) 174 | return '%d/%d/%d %d:%d:%d +%03d' % (2000+Y, M, D, h, m, s, offset) 175 | 176 | def cancel_goto(self): 177 | self.send_packet('M', 1) 178 | 179 | def hibernate(self): 180 | self.send_packet('x', 1) 181 | 182 | def is_slewing(self): 183 | ret = self.send_packet('L', 2) 184 | return ret[0] != '0' 185 | 186 | def get_tracking_mode(self): 187 | ret = self.send_packet('t', 2) 188 | return ord(ret[0]) 189 | 190 | def set_tracking_mode(self, mode=0): 191 | self.send_packet('T%c' % mode, 1) 192 | 193 | 194 | if __name__ == '__main__': 195 | nex = NexStar('/dev/ttyACM0', False) 196 | while nex.echo('x') != 'x': 197 | pass 198 | 199 | nex.set_time(time.strftime('%Y/%m/%d %H:%M:%S')) 200 | nex.set_location(43.540*pi/180, -5.658*pi/180) 201 | 202 | print "Version:\t", nex.get_version() 203 | #print "Variant:\t", nex.get_variant() 204 | print "Model: \t", nex.get_model() 205 | print "Dec version:\t", nex.get_device_version(DEC_DEV) 206 | print "RA version:\t", nex.get_device_version(RA_DEV) 207 | print "Time: \t", nex.get_time() 208 | print "Location:\t", nex.get_location() 209 | print "EQ coords:\t", nex.get_eq_coords(precise=True) 210 | 211 | #nex.sync_eq_coords(1.5, 0.3, precise=True) 212 | 213 | # nex.set_tracking_mode(0) 214 | # print "Tracking mode:", nex.get_tracking_mode() 215 | 216 | # set coords (in deg) 217 | # nex.goto_eq_coords(0, 0) 218 | # nex.cancel_goto() 219 | 220 | # goto coords (in deg) 221 | # nex.goto_eq_coords(10, 0, precise=True) 222 | # while nex.is_slewing(): 223 | # time.sleep(1) 224 | # print nex.get_eq_coords(precise=True) 225 | # time.sleep(1) 226 | # print nex.get_eq_coords(precise=True) 227 | 228 | # nex.hibernate() 229 | -------------------------------------------------------------------------------- /lib/AstroLib/AstroLib.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include "AstroLib.h" 4 | 5 | #define abs(x) (((x) > 0) ? (x) : -(x)) 6 | #define sign(x) (((x) > 0) ? 1 : -1) 7 | 8 | 9 | // Normalize an angle in radians between 0 and 2*pi 10 | float normalize2Pi(float h) 11 | { 12 | int ih = (int)(h/2/M_PI); 13 | h = h - (float)ih*2*M_PI; 14 | return h < 0 ? h + 2*M_PI : h; 15 | } 16 | 17 | // Normalize an angle in radians between -pi and pi 18 | float normalizePi(float h) 19 | { 20 | h = normalize2Pi(h); 21 | int ih = (int)(h/2/M_PI); 22 | h = h - (float)ih*2*M_PI; 23 | return h > M_PI ? h - 2*M_PI: h; 24 | } 25 | 26 | // Limit an angle to [-pi/2, pi/2] 27 | // This is useful for transforming spherical coordinates 28 | float limitDec(float h, uint8_t *over) 29 | { 30 | h = normalizePi(h); 31 | 32 | if (h > M_PI/2) { 33 | *over = 1; 34 | return M_PI - h; 35 | } else if (h < -M_PI/2) { 36 | *over = 1; 37 | return -M_PI - h; 38 | } 39 | *over = 0; 40 | return h; 41 | } 42 | 43 | 44 | // Convert an angle from sexagesimal to radians 45 | float sx2rad(SxAngle sx) 46 | { 47 | float rad = M_PI*((float)sx.deg + 48 | (float)sx.min/60.0 + 49 | (float)sx.sec/3600.0)/180.0; 50 | 51 | if (sx.sign) 52 | return -rad; 53 | return rad; 54 | } 55 | 56 | // Convert an angle from radians to sexagesimal 57 | void rad2sx(float rad, SxAngle *sx) 58 | { 59 | sx->sign = (rad < 0); 60 | rad = fabs(rad); 61 | float deg = rad*180/M_PI; 62 | float min = 60*(deg - int(deg)); 63 | float sec = 60*(min - int(min)); 64 | 65 | sx->deg = (uint16_t)deg; 66 | sx->min = (uint8_t)min; 67 | sx->sec = (uint8_t)round(sec); 68 | } 69 | 70 | void sx2string(SxAngle sx, char *buffer) 71 | { 72 | sprintf(buffer, " %d:%02d:%02d", sx.deg, sx.min, sx.sec); 73 | if (sx.sign) 74 | buffer[0] = '-'; 75 | } 76 | 77 | // Calculate the julian date of the midnight of a given date 78 | float getJulianDate0(time_t t) 79 | { 80 | float a, b, c, d; 81 | #ifdef ARDUINO_ARCH_AVR 82 | float _month = month(t); 83 | float _year = year(t); 84 | float _day = day(t); 85 | #else 86 | struct tm *ptm; 87 | ptm = gmtime(&t); 88 | float _month = ptm->tm_mon + 1; 89 | float _year = ptm->tm_year + 1900; 90 | float _day = ptm->tm_mday; 91 | #endif 92 | 93 | if (_month < 3) { 94 | _month += 12; 95 | _year -= 1; 96 | } 97 | 98 | a = (long)(_year/100); 99 | b = 2 - a + (long)(a/4); 100 | c = (long)(365.25*(_year + 4716)); 101 | d = (long)(30.6001*(_month + 1)); 102 | 103 | return _day + b + c + d - 1524.5; 104 | } 105 | 106 | // Calculate the julian date of a given timestamp 107 | float getJulianDate(time_t t) 108 | { 109 | float _hour, _min, _sec, h, jd; 110 | #ifdef ARDUINO_ARCH_AVR 111 | _hour = hour(t); 112 | _min = minute(t); 113 | _sec = second(t); 114 | #else 115 | struct tm *ptm = gmtime(&t); 116 | _hour = ptm->tm_hour; 117 | _min = ptm->tm_min; 118 | _sec = ptm->tm_sec; 119 | #endif 120 | 121 | h = (_hour + _min/60 + _sec/3600); 122 | jd = (float)getJulianDate0(t) + (float)h/24; 123 | //printf("%f, %f, %f\n", getJulianDate0(t), h/24, jd); 124 | return jd; 125 | } 126 | 127 | float getJ2000Date(time_t t) 128 | { 129 | return getJulianDate(t) - 2451545.0; 130 | } 131 | 132 | 133 | #if 0 134 | void dateFromJ2000(float jd, time_t *t) 135 | { 136 | //TODO 137 | 138 | tmElements_t tm; 139 | tm.Year = 2017 - 1970; 140 | tm.Month = 12; 141 | tm.Day = 16; 142 | tm.Hour = 11; 143 | tm.Minute = 58; 144 | tm.Second = 35; 145 | *t = makeTime(tm); 146 | 147 | /* 148 | float q = jd + 0.5; 149 | int z = int(jd + 0.5); 150 | 151 | w = (int)((z - 1867216.25)/36524.25); 152 | b = 1524 + z + 1 + w - (int)(w/4); 153 | c = (int)((b - 122.1)/365.25); 154 | d = (int)(365.25*c); 155 | e = (int)((b - d)/30.6001); 156 | f = 30.6001*e; 157 | 158 | tm.Day = b - d - f + q - z; 159 | tm.Month = e - 1; 160 | if (tm.Month > 13) 161 | tm.Month -= 12; 162 | 163 | tm.Year = c - 4715; 164 | if (tm.Month < 3) 165 | tm.Year -= 1; 166 | */ 167 | } 168 | #endif 169 | 170 | // Calculate Greenwich Mean Sidereal Time in radians 171 | // given a J2000-based julian date 172 | // Reference: http://aa.usno.navy.mil/faq/docs/GAST.php 173 | float getGMST(time_t t) 174 | { 175 | float _hour, _min, _sec, frac, gmst; 176 | 177 | #ifdef ARDUINO_ARCH_AVR 178 | _hour = hour(t); 179 | _min = minute(t); 180 | _sec = second(t); 181 | #else 182 | struct tm *ptm = gmtime(&t); 183 | _hour = ptm->tm_hour; 184 | _min = ptm->tm_min; 185 | _sec = ptm->tm_sec; 186 | #endif 187 | 188 | long jdx = getJulianDate0(t) - 2451544.5; 189 | frac = (_hour + _min/60 + _sec/3600)/24 - 0.5; 190 | gmst = 4.8949612127 + 0.0172027918*jdx + 6.3003880989849*frac; 191 | return normalize2Pi(gmst); 192 | } 193 | 194 | // Calculate Local Sidereal Time in radians 195 | // given a J2000-based julian date 196 | float getLST(time_t t, Location loc) 197 | { 198 | float lst = getGMST(t) + loc.lon; 199 | return normalize2Pi(lst); 200 | } 201 | 202 | // Convert equatorial to horizontal coordinates 203 | // Reference: http://www.geocities.jp/toshimi_taki/matrix/matrix.htm 204 | void localToHoriz(Location loc, LocalCoords lc, HorizCoords *hor) 205 | { 206 | float x, y, z, x2, y2, z2; 207 | 208 | // Convert to rectangular coordinates 209 | x = cos(-lc.ha)*cos(lc.dec); 210 | y = sin(-lc.ha)*cos(lc.dec); 211 | z = sin(lc.dec); 212 | 213 | // Rotate the coordinate system along east-west axis 214 | float pz = M_PI/2 - loc.lat; 215 | x2 = x*cos(pz) - z*sin(pz); 216 | y2 = y; 217 | z2 = x*sin(pz) + z*cos(pz); 218 | 219 | // Convert to horizontal coordinates 220 | hor->az = M_PI - atan2(y2, x2); 221 | hor->alt = asin(z2); 222 | } 223 | 224 | // Convert horizontal to equatorial coordinates 225 | // Reference: http://www.geocities.jp/toshimi_taki/matrix/matrix.htm 226 | void horizToLocal(Location loc, HorizCoords hor, LocalCoords *lc) 227 | { 228 | float x, y, z, x2, y2, z2; 229 | 230 | // Convert to rectangular coordinates 231 | x = cos(M_PI - hor.az)*cos(hor.alt); 232 | y = sin(M_PI - hor.az)*cos(hor.alt); 233 | z = sin(hor.alt); 234 | 235 | // Rotate the coordinate system along east-west axis 236 | float zp = loc.lat - M_PI/2; 237 | x2 = x*cos(zp) - z*sin(zp); 238 | y2 = y; 239 | z2 = x*sin(zp) + z*cos(zp); 240 | 241 | // Convert to equatorial coordinates 242 | lc->ha = -atan2(y2, x2); 243 | lc->dec = asin(z2); 244 | 245 | if (lc->ha < 0) 246 | lc->ha += 2*M_PI; 247 | } 248 | 249 | // Convert mechanical coordinates to local coordinates (HA/dec) 250 | void axisToLocalCoords(AxisCoords ac, LocalCoords *lc) 251 | { 252 | uint8_t over; 253 | float dec = limitDec(ac.dec, &over); 254 | 255 | float ha; 256 | if (over) { 257 | ha = ac.ra; 258 | } else { 259 | ha = ac.ra + M_PI; 260 | } 261 | 262 | lc->ha = normalizePi(ha); 263 | lc->dec = dec; 264 | } 265 | 266 | // Convert local coordinates (HA/dec) to mechanical coordinates 267 | void localToAxisCoords(LocalCoords lc, AxisCoords *ac) 268 | 269 | { 270 | float ha = normalizePi(lc.ha); 271 | 272 | if (lc.ha >= 0) { 273 | ac->ra = normalize2Pi(ha); 274 | ac->dec = normalize2Pi(M_PI - lc.dec); 275 | } else { 276 | ac->ra = normalize2Pi(ha - M_PI); 277 | ac->dec = normalize2Pi(lc.dec); 278 | } 279 | } 280 | 281 | // Convert local coordinates to equatorial coordinates 282 | void localToEqCoords(LocalCoords lc, float lst, EqCoords *eq) 283 | { 284 | eq->ra = normalizePi(lst - lc.ha); 285 | eq->dec = lc.dec; 286 | } 287 | 288 | // Convert equatorial coordinates to local coordinates 289 | void eqToLocalCoords(EqCoords eq, float lst, LocalCoords *lc) 290 | { 291 | lc->ha = normalizePi(lst - eq.ra); 292 | lc->dec = eq.dec; 293 | } 294 | 295 | // Convert equatorial coordinates to mechanical coordinates 296 | void eqToAxisCoords(EqCoords eq, float lst, AxisCoords *ac) 297 | { 298 | LocalCoords lc; 299 | eqToLocalCoords(eq, lst, &lc); 300 | localToAxisCoords(lc, ac); 301 | } 302 | 303 | // Convert mechanical coordinates equatorial to coordinates 304 | void axisToEqCoords(AxisCoords ac, float lst, EqCoords *eq) 305 | { 306 | LocalCoords lc; 307 | axisToLocalCoords(ac, &lc); 308 | localToEqCoords(lc, lst, eq); 309 | } 310 | -------------------------------------------------------------------------------- /test/test_astrolib/test_data.h: -------------------------------------------------------------------------------- 1 | /* Test data generated by running 'generate_test_data.py'. DO NOT EDIT! */ 2 | 3 | #include "AstroLib.h" 4 | 5 | struct TestAngle { 6 | float rad; 7 | SxAngle sx; 8 | }; 9 | 10 | struct TestDate { 11 | const char *date; 12 | float jd; // Julian date 13 | float gmst; // Greenwich mean sidereal time 14 | float lst; // Local sidereal time 15 | }; 16 | 17 | struct TestStar { 18 | const char *name; 19 | EqCoords eq; 20 | HorizCoords hc; 21 | }; 22 | 23 | TestAngle test_angles[] = { 24 | { 1.65320142, { 94, 43, 17, 0 } }, 25 | { 3.34296201, { 191, 32, 15, 0 } }, 26 | { 3.26724323, { 187, 11, 57, 0 } }, 27 | { 1.99631594, { 114, 22, 50, 0 } }, 28 | { 1.42628534, { 81, 43, 12, 0 } }, 29 | { 5.56577254, { 318, 53, 43, 0 } }, 30 | { 3.7915516, { 217, 14, 24, 0 } }, 31 | { 4.83111479, { 276, 48, 9, 0 } }, 32 | { 2.44915182, { 140, 19, 34, 0 } }, 33 | { 3.00271332, { 172, 2, 34, 0 } }, 34 | { 6.15013525, { 352, 22, 36, 0 } }, 35 | { 1.32711598, { 76, 2, 17, 0 } }, 36 | { 6.12257428, { 350, 47, 52, 0 } }, 37 | { 4.02123151, { 230, 23, 59, 0 } }, 38 | { 1.86023335, { 106, 35, 1, 0 } }, 39 | { 4.94106179, { 283, 6, 7, 0 } }, 40 | { 4.95035165, { 283, 38, 3, 0 } }, 41 | { 1.64223512, { 94, 5, 35, 0 } }, 42 | { 5.50109946, { 315, 11, 23, 0 } }, 43 | { 1.32448485, { 75, 53, 15, 0 } }, 44 | { -0.3513777, { 20, 7, 57, 1 } }, 45 | { -3.03632394, { 173, 58, 7, 1 } }, 46 | { -0.3341651, { 19, 8, 46, 1 } }, 47 | { -4.62136073, { 264, 47, 4, 1 } }, 48 | { -5.69416743, { 326, 15, 6, 1 } }, 49 | { -4.84845914, { 277, 47, 46, 1 } }, 50 | { -1.94745063, { 111, 34, 51, 1 } }, 51 | { -2.01682328, { 115, 33, 20, 1 } }, 52 | { -2.56636077, { 147, 2, 30, 1 } }, 53 | { -3.53201951, { 202, 22, 11, 1 } }, 54 | { -4.74609707, { 271, 55, 53, 1 } }, 55 | { -2.2110533, { 126, 41, 2, 1 } }, 56 | { -3.15238789, { 180, 37, 7, 1 } }, 57 | { -0.13849362, { 7, 56, 6, 1 } }, 58 | { -5.6656227, { 324, 36, 59, 1 } }, 59 | { -5.15098923, { 295, 7, 48, 1 } }, 60 | { -3.71885489, { 213, 4, 29, 1 } }, 61 | { -5.34988838, { 306, 31, 34, 1 } }, 62 | { -3.79160088, { 217, 14, 34, 1 } }, 63 | { -3.66746382, { 210, 7, 49, 1 } }, 64 | }; 65 | 66 | TestDate test_dates[] = { 67 | { "1978/11/13 04:34:00", 2443825.690278, 2.102117, 2.037534 }, 68 | { "1984/05/30 02:20:00", 2445850.597222, 4.935499, 4.870916 }, 69 | { "2000/01/01 00:00:00", 2451544.5, 1.744705, 1.680122 }, 70 | { "2003/05/06 08:03:01", 2452765.835428, 6.013077, 5.948494 }, 71 | { "2004/02/29 12:59:11", 2453065.0411, 6.169358, 6.104775 }, 72 | { "2014/12/31 16:10:22", 2457023.173866, 5.979519, 5.914935 }, 73 | { "2017/12/11 22:02:33", 2458099.418438, 0.897803, 0.833219 }, 74 | { "2037/05/11 20:01:44", 2465190.334537, 2.973657, 2.909074 }, 75 | }; 76 | 77 | const char *obs_date = "2020/9/12 01:50:41"; 78 | Location obs_loc = { -0.06458321069102657, 0.7054043289658303 }; 79 | 80 | TestStar test_stars[] = { 81 | { "Sirrah", { 0.041305, 0.509717 }, { 3.976586, 1.299863 }}, 82 | { "Caph", { 0.044965, 1.03433 }, { 5.950969, 1.21259 }}, 83 | { "Algenib", { 0.062433, 0.267011 }, { 3.593187, 1.096429 }}, 84 | { "Schedar", { 0.181988, 0.988706 }, { 6.111712, 1.281678 }}, 85 | { "Mirach", { 0.309355, 0.623585 }, { 2.772531, 1.483481 }}, 86 | { "Achernar", { 0.4298, -0.997106 }, { 3.054627, -0.136987 }}, 87 | { "Almach", { 0.54619, 0.740479 }, { 1.312227, 1.361499 }}, 88 | { "Hamal", { 0.559993, 0.411182 }, { 2.363199, 1.189026 }}, 89 | { "Polaris", { 0.779258, 1.559375 }, { 0.007369, 0.715366 }}, 90 | { "Menkar", { 0.800053, 0.072796 }, { 2.355186, 0.778933 }}, 91 | { "Algol", { 0.826918, 0.716138 }, { 1.361805, 1.151402 }}, 92 | { "Electra", { 0.986536, 0.421956 }, { 1.791673, 0.910095 }}, 93 | { "Taygeta", { 0.988003, 0.428134 }, { 1.782181, 0.912573 }}, 94 | { "Maia", { 0.990699, 0.426389 }, { 1.782207, 0.909562 }}, 95 | { "Merope", { 0.992867, 0.419066 }, { 1.790041, 0.903712 }}, 96 | { "Alcyone", { 0.997929, 0.421796 }, { 1.782126, 0.901531 }}, 97 | { "Atlas", { 1.005245, 0.420882 }, { 1.777129, 0.895553 }}, 98 | { "Zaurak", { 1.04278, -0.234712 }, { 2.318134, 0.389457 }}, 99 | { "Aldebaran", { 1.209055, 0.288853 }, { 1.762957, 0.66368 }}, 100 | { "Rigel", { 1.37671, -0.142702 }, { 1.991463, 0.24805 }}, 101 | { "Capella", { 1.388394, 0.803127 }, { 1.058985, 0.772469 }}, 102 | { "Bellatrix", { 1.423424, 0.111153 }, { 1.763352, 0.388762 }}, 103 | { "Elnath", { 1.429338, 0.499564 }, { 1.402331, 0.616562 }}, 104 | { "Nihal", { 1.436047, -0.361979 }, { 2.105257, 0.051198 }}, 105 | { "Mintaka", { 1.453192, -0.004939 }, { 1.833764, 0.288987 }}, 106 | { "Arneb", { 1.455727, -0.310754 }, { 2.055904, 0.074503 }}, 107 | { "Alnilam", { 1.471514, -0.02073 }, { 1.832927, 0.264813 }}, 108 | { "Alnitak", { 1.491319, -0.033693 }, { 1.828939, 0.241467 }}, 109 | { "Saiph", { 1.521582, -0.168607 }, { 1.908573, 0.12751 }}, 110 | { "Betelgeuse", { 1.554529, 0.129347 }, { 1.658579, 0.301807 }}, 111 | { "Menkalinan", { 1.575245, 0.78445 }, { 1.022406, 0.643177 }}, 112 | { "Mirzam", { 1.673734, -0.313517 }, { 1.916419, -0.079181 }}, 113 | { "Canopus", { 1.677238, -0.919821 }, { 2.412957, -0.456 }}, 114 | { "Alhena", { 1.740441, 0.285908 }, { 1.412843, 0.259361 }}, 115 | { "Sirius", { 1.771701, -0.292143 }, { 1.838964, -0.13638 }}, 116 | { "Adara", { 1.830054, -0.506083 }, { 1.976336, -0.311764 }}, 117 | { "Wezen", { 1.872779, -0.461158 }, { 1.914701, -0.315089 }}, 118 | { "Castor", { 1.989164, 0.555746 }, { 1.0493, 0.249399 }}, 119 | { "Procyon", { 2.008704, 0.090336 }, { 1.393243, -0.067837 }}, 120 | { "Pollux", { 2.035721, 0.488271 }, { 1.075843, 0.175188 }}, 121 | { "Naos", { 2.113082, -0.699126 }, { 2.021481, -0.610925 }}, 122 | { "Alphard", { 2.480855, -0.152628 }, { 1.246443, -0.579538 }}, 123 | { "Regulus", { 2.659172, 0.207153 }, { 0.823939, -0.422762 }}, 124 | { "Algieba", { 2.709924, 0.344528 }, { 0.699203, -0.334258 }}, 125 | { "Merak", { 2.892974, 0.982159 }, { 0.283462, 0.173075 }}, 126 | { "Dubhe", { 2.901297, 1.07585 }, { 0.242563, 0.257935 }}, 127 | { "Denebola", { 3.098313, 0.252386 }, { 0.362934, -0.569619 }}, 128 | { "Phecda", { 3.119158, 0.935206 }, { 0.173024, 0.089142 }}, 129 | { "Minkar", { 3.190256, -0.396736 }, { 0.612184, -1.209719 }}, 130 | { "Megrez", { 3.213105, 0.99347 }, { 0.109167, 0.136358 }}, 131 | { "Gienah Corvi", { 3.215056, -0.308106 }, { 0.461836, -1.138626 }}, 132 | { "Mimosa", { 3.354891, -1.043709 }, { 3.055159, -1.230603 }}, 133 | { "Alioth", { 3.381073, 0.974807 }, { 0.017568, 0.109623 }}, 134 | { "Vindemiatrix", { 3.417244, 0.189414 }, { 6.276818, -0.675965 }}, 135 | { "Mizar", { 3.511212, 0.956841 }, { 6.225947, 0.093607 }}, 136 | { "Spica", { 3.51793, -0.196616 }, { 6.072306, -1.053511 }}, 137 | { "Alcor", { 3.516867, 0.957937 }, { 6.222773, 0.094953 }}, 138 | { "Alcaid", { 3.614218, 0.858979 }, { 6.151732, 0.003702 }}, 139 | { "Agena", { 3.688072, -1.05544 }, { 3.503126, -1.181485 }}, 140 | { "Thuban", { 3.686608, 1.121946 }, { 6.160891, 0.269356 }}, 141 | { "Arcturus", { 3.737557, 0.333094 }, { 5.933929, -0.489024 }}, 142 | { "Izar", { 3.865308, 0.471113 }, { 5.859517, -0.321174 }}, 143 | { "Kochab", { 3.886082, 1.29288 }, { 6.143489, 0.452934 }}, 144 | { "Alphecca", { 4.082061, 0.465153 }, { 5.674235, -0.245036 }}, 145 | { "Unukalhai", { 4.124486, 0.111093 }, { 5.434464, -0.524529 }}, 146 | { "Antares", { 4.322546, -0.462094 }, { 4.735855, -0.785417 }}, 147 | { "Rasalgethi", { 4.518558, 0.25083 }, { 5.209834, -0.170241 }}, 148 | { "Shaula", { 4.603279, -0.647835 }, { 4.365384, -0.663992 }}, 149 | { "Rasalhague", { 4.607147, 0.21904 }, { 5.125247, -0.132281 }}, 150 | { "Cebalrai", { 4.644678, 0.079626 }, { 4.996958, -0.201676 }}, 151 | { "Etamin", { 4.699671, 0.89871 }, { 5.582409, 0.384217 }}, 152 | { "Kaus Australis", { 4.82379, -0.599944 }, { 4.311889, -0.484386 }}, 153 | { "Vega", { 4.876602, 0.67732 }, { 5.31299, 0.350455 }}, 154 | { "Sheliak", { 4.934226, 0.582795 }, { 5.202937, 0.3319 }}, 155 | { "Nunki", { 4.959073, -0.458499 }, { 4.366488, -0.308133 }}, 156 | { "Sulafat", { 4.972946, 0.571117 }, { 5.172617, 0.351224 }}, 157 | { "Arkab Prior", { 5.079401, -0.775286 }, { 4.030197, -0.413087 }}, 158 | { "Arkab Posterior", { 5.081965, -0.781233 }, { 4.023701, -0.414914 }}, 159 | { "Rukbat", { 5.084616, -0.708201 }, { 4.087361, -0.371545 }}, 160 | { "Albereo", { 5.111874, 0.48882 }, { 5.025984, 0.401555 }}, 161 | { "Tarazed", { 5.180313, 0.186172 }, { 4.729003, 0.269959 }}, 162 | { "Altair", { 5.20014, 0.155773 }, { 4.691786, 0.265754 }}, 163 | { "Alshain", { 5.219958, 0.112792 }, { 4.644629, 0.253274 }}, 164 | { "Sadr", { 5.336249, 0.703831 }, { 5.135033, 0.669224 }}, 165 | { "Peacock", { 5.354998, -0.989078 }, { 3.73092, -0.400242 }}, 166 | { "Deneb", { 5.419894, 0.791638 }, { 5.213736, 0.75828 }}, 167 | { "Alderamin", { 5.581119, 1.093894 }, { 5.642552, 0.883233 }}, 168 | { "Alfirk", { 5.62418, 1.233133 }, { 5.86028, 0.867321 }}, 169 | { "Enif", { 5.695028, 0.174036 }, { 4.346833, 0.64626 }}, 170 | { "Sadalmelik", { 5.789467, -0.003805 }, { 4.114895, 0.578915 }}, 171 | { "Alnair", { 5.801195, -0.817878 }, { 3.63022, -0.093237 }}, 172 | { "Fomalhaut", { 6.016116, -0.515082 }, { 3.619883, 0.252377 }}, 173 | { "Scheat", { 6.042263, 0.492103 }, { 4.401643, 1.100536 }}, 174 | { "Markab", { 6.04668, 0.267338 }, { 4.076854, 0.949573 }}, 175 | { "Acamar", { 0.781257, -0.701958 }, { 2.757545, 0.088655 }}, 176 | { "Acrux", { 3.262453, -1.103258 }, { 2.971187, -1.163147 }}, 177 | { "Adhara", { 1.83005, -0.506081 }, { 1.976336, -0.31176 }}, 178 | { "Alkaid", { 3.614222, 0.858978 }, { 6.15173, 0.003702 }}, 179 | { "Alpheratz", { 0.041302, 0.509717 }, { 3.976592, 1.299861 }}, 180 | { "Ankaa", { 0.119173, -0.736387 }, { 3.254435, 0.122499 }}, 181 | { "Atria", { 4.410572, -1.205437 }, { 3.615461, -0.852442 }}, 182 | { "Avior", { 2.194314, -1.039708 }, { 2.422608, -0.764696 }}, 183 | { "Diphda", { 0.194733, -0.311933 }, { 3.226355, 0.551011 }}, 184 | { "Eltanin", { 4.699669, 0.898711 }, { 5.58241, 0.384217 }}, 185 | { "Formalhaut", { 6.016118, -0.51508 }, { 3.619883, 0.252379 }}, 186 | { "Gacrux", { 3.282388, -0.998791 }, { 2.906212, -1.26565 }}, 187 | { "Gienah", { 3.215053, -0.308106 }, { 0.461841, -1.138625 }}, 188 | { "Hadar", { 3.688076, -1.055439 }, { 3.503132, -1.181486 }}, 189 | { "Menkent", { 3.699546, -0.636494 }, { 4.917752, -1.335825 }}, 190 | { "Miaplacidus", { 2.414511, -1.218203 }, { 2.686613, -0.849614 }}, 191 | { "Mirfak", { 0.89798, 0.871448 }, { 0.996031, 1.103779 }}, 192 | { "Rigil Kentaurus", { 3.843965, -1.063265 }, { 3.633925, -1.125975 }}, 193 | { "Sabik", { 4.500968, -0.27486 }, { 4.808977, -0.541771 }}, 194 | { "Suhail", { 2.394249, -0.759422 }, { 1.993033, -0.827637 }}, 195 | { "Zubenelgenubi", { 3.892067, -0.281423 }, { 5.367915, -0.977044 }}, 196 | }; -------------------------------------------------------------------------------- /src/main.cpp: -------------------------------------------------------------------------------- 1 | /****************************************************************** 2 | Author: Juan Menendez 3 | 4 | This code is part of the NextDriver project: 5 | https://github.com/juanmb/NextDriver 6 | 7 | ********************************************************************/ 8 | 9 | #include 10 | #ifdef TARGET_DUE 11 | #include 12 | #else 13 | #include 14 | #include 15 | #endif 16 | #include 17 | #include "serial_command.h" 18 | #include "nexstar_aux.h" 19 | #include "AstroLib.h" 20 | 21 | #define VERSION_MAJOR 4 22 | #define VERSION_MINOR 21 23 | #define MOUNT_MODEL 14 // CGEM DX 24 | #define CONTROLLER_VARIANT 0x11 // NexStar 25 | #define BAUDRATE 9600 26 | #define GO_BELOW_HORIZON 27 | #define ENABLE_HOMING 28 | 29 | /////////////////////////////////////////// 30 | // Pin definitions 31 | /////////////////////////////////////////// 32 | 33 | #ifdef SIMPLE_AUX_INTERFACE 34 | // Use a simple interface circuit containing only a resistor and a diode 35 | #define AUX_BUSY 3 36 | #else 37 | // Use a more elaborate interface circuit built around a 74LS125 38 | #define AUX_BUSY 2 39 | #endif 40 | #define AUX_SELECT 3 41 | #define AUX_TX 4 42 | #define AUX_RX 5 43 | 44 | #define LED_STATUS_G 12 // synced/homed 45 | #define LED_STATUS_R 13 // no time set 46 | #define LED_RED 11 // error 47 | #define LED_GREEN 10 // slewing 48 | #define LED_YELLOW 9 // parked 49 | 50 | #define HOME_BUTTON 7 51 | #define ABORT_BUTTON 8 52 | #define HOME_RA_PIN A0 53 | #define LIMIT_RA_PIN A1 54 | #define HOME_DEC_PIN A2 55 | #define LIMIT_ALT_PIN A3 56 | 57 | #define ABS(a) ((a) < 0 ? -(a) : (a)) 58 | 59 | #define decHomeSensor() (analogRead(HOME_DEC_PIN) > 512) // true if blocked 60 | #define raHomeSensor() (analogRead(HOME_RA_PIN) > 512) // true if blocked 61 | 62 | enum ScopeState { 63 | ST_IDLE, 64 | ST_MERIDIAN_FLIP, 65 | ST_SLEWING_FAST, 66 | ST_SLEWING_SLOW, 67 | ST_HOMING_DEC, 68 | ST_HOMING_FAST1, 69 | ST_HOMING_FAST2, 70 | ST_HOMING_SLOW1, 71 | ST_HOMING_SLOW2, 72 | }; 73 | 74 | enum ScopeEvent { 75 | EV_NONE, 76 | EV_GOTO, 77 | EV_ABORT, 78 | EV_HOME, 79 | }; 80 | 81 | enum PierSide { 82 | PIER_EAST, // Telescope on the east side of pier (looking west) 83 | PIER_WEST, // Telescope on the west side of pier (looking east) 84 | }; 85 | 86 | ScopeState state = ST_IDLE; 87 | ScopeEvent event = EV_NONE; 88 | 89 | // Position of the home (or index) sensors in axis coordinates. 90 | // For now, we assume the sensors are always at exactly pi/2, pi/2: 91 | // the telescope looking at the pole and the counterweight pointing downwards 92 | const AxisCoords home_pos = {M_PI/2, M_PI/2}; 93 | 94 | Location location = {0, 0}; // Default location 95 | uint8_t tracking_mode = 0; 96 | uint32_t ref_t = 0; // millis() at last cmdSetTime call 97 | float ref_lst = 0.0; // Last synced LST 98 | float ref_jd = 0.0; // Last synced julian date refered to J2000 99 | bool synced = false; 100 | bool time_set = false; 101 | bool aligned = false; 102 | bool parked = false; 103 | EqCoords target = {0}; 104 | 105 | // Addresses in EEPROM/Flash 106 | #define ADDR_LOCATION 0 107 | #define ADDR_HOME 2*4 108 | #define ADDR_TRACKING 2*4 109 | 110 | #ifdef TARGET_DUE 111 | #define aux Serial1 112 | DueFlashStorage FS; 113 | #else 114 | SoftwareSerial aux(AUX_RX, AUX_TX); 115 | #endif 116 | 117 | NexStarAux nexstar(&aux, AUX_SELECT, AUX_BUSY); 118 | SerialCommand sCmd(&Serial); 119 | 120 | 121 | /* 122 | // Get current Julian date 123 | static float getCurrentJD() 124 | { 125 | return ref_jd + (float)(now() - ref_t)/(24.0*60*60); 126 | } 127 | */ 128 | 129 | // Obtain current Local Sidereal Time from last synced time 130 | // This is faster than performing the full calculation from JD & location 131 | // by calling getLST() 132 | float getCurrentLST() { 133 | return ref_lst + 2*M_PI*(float)(now() - ref_t)/(24.0*60*60); 134 | } 135 | 136 | /***************************************************************************** 137 | Get/set/goto position 138 | ******************************************************************************/ 139 | 140 | void getAxisCoords(AxisCoords *ac) 141 | { 142 | uint32_t ra, dec; 143 | nexstar.getPosition(DEV_RA, &ra); 144 | nexstar.getPosition(DEV_DEC, &dec); 145 | 146 | ac->ra = normalizePi(pnex2rad(ra)); 147 | ac->dec = normalizePi(pnex2rad(dec)); 148 | } 149 | 150 | void getLocalCoords(LocalCoords *lc) 151 | { 152 | AxisCoords ac; 153 | getAxisCoords(&ac); 154 | axisToLocalCoords(ac, lc); 155 | } 156 | 157 | void getEqCoords(EqCoords *eq) 158 | { 159 | LocalCoords lc; 160 | getLocalCoords(&lc); 161 | localToEqCoords(lc, getCurrentLST(), eq); 162 | } 163 | 164 | void getHorizCoords(HorizCoords *hc) 165 | { 166 | LocalCoords lc; 167 | getLocalCoords(&lc); 168 | localToHoriz(location, lc, hc); 169 | } 170 | 171 | void setAxisCoords(const AxisCoords ac) 172 | { 173 | nexstar.setPosition(DEV_RA, rad2pnex(ac.ra)); 174 | nexstar.setPosition(DEV_DEC, rad2pnex(ac.dec)); 175 | synced = true; 176 | } 177 | 178 | void setLocalCoords(const LocalCoords lc) 179 | { 180 | AxisCoords ac; 181 | localToAxisCoords(lc, &ac); 182 | setAxisCoords(ac); 183 | } 184 | 185 | void setEqCoords(const EqCoords eq) 186 | { 187 | AxisCoords ac; 188 | eqToAxisCoords(eq, getCurrentLST(), &ac); 189 | setAxisCoords(ac); 190 | } 191 | 192 | void gotoAxisCoords(const AxisCoords ac, bool slow) 193 | { 194 | if (synced) { 195 | nexstar.gotoPosition(DEV_RA, slow, rad2pnex(ac.ra)); 196 | nexstar.gotoPosition(DEV_DEC, slow, rad2pnex(ac.dec)); 197 | } 198 | } 199 | 200 | void gotoEqCoords(const EqCoords eq, bool slow) 201 | { 202 | AxisCoords ac; 203 | eqToAxisCoords(eq, getCurrentLST(), &ac); 204 | gotoAxisCoords(ac, slow); 205 | } 206 | 207 | void stopMotors() 208 | { 209 | nexstar.move(DEV_RA, 0, 0); 210 | nexstar.move(DEV_DEC, 0, 0); 211 | } 212 | 213 | // Check if both axes have reached their target position 214 | bool slewDone() 215 | { 216 | bool ra_done, dec_done; 217 | 218 | nexstar.slewDone(DEV_RA, &ra_done); 219 | nexstar.slewDone(DEV_DEC, &dec_done); 220 | return ra_done && dec_done; 221 | } 222 | 223 | // Indicates the pointing state of the mount at a given position 224 | // Reference: https://ascom-standards.org/Help/Platform/html/P_ASCOM_DeviceInterface_ITelescopeV3_SideOfPier.htm 225 | PierSide getPierSide(AxisCoords ac) 226 | { 227 | float dec = normalizePi(ac.dec); 228 | return ABS(dec) <= M_PI/2 ? PIER_WEST : PIER_EAST; 229 | } 230 | 231 | PierSide getPierSide() 232 | { 233 | AxisCoords ac; 234 | getAxisCoords(&ac); 235 | return getPierSide(ac); 236 | } 237 | 238 | // Check if a meridian flip is required 239 | bool checkMeridianFlip(EqCoords eq) 240 | { 241 | AxisCoords ac; 242 | PierSide target_side, current_side; 243 | 244 | eqToAxisCoords(eq, getCurrentLST(), &ac); 245 | target_side = getPierSide(ac); 246 | current_side = getPierSide(); 247 | 248 | // A meridian flip is required if both angles have different sign 249 | return target_side != current_side; 250 | } 251 | 252 | void readLocation(Location *loc) 253 | { 254 | #ifdef __arm__ 255 | byte *b1 = FS.readAddress(ADDR_LOCATION); 256 | memcpy(loc, b1, sizeof(Location)); 257 | #else 258 | EEPROM.get(ADDR_LOCATION, *loc); 259 | #endif 260 | } 261 | 262 | // Store the location in EEPROM 263 | void writeLocation(Location loc) 264 | { 265 | #ifdef __arm__ 266 | byte b[sizeof(Location)]; 267 | memcpy(b, &loc, sizeof(Location)); 268 | FS.write(ADDR_LOCATION, b, sizeof(Location)); 269 | #else 270 | EEPROM.put(ADDR_LOCATION, loc); 271 | #endif 272 | } 273 | 274 | // Read the position of the index sensors from EEPROM. 275 | void readHomePosition(AxisCoords *ac) 276 | { 277 | #ifdef __arm__ 278 | byte *b2 = FS.readAddress(ADDR_HOME); 279 | memcpy(ac, b2, sizeof(LocalCoords)); 280 | #else 281 | EEPROM.get(ADDR_HOME, *ac); 282 | #endif 283 | } 284 | 285 | // Save current axis coordinates as the home position 286 | void writeHomePosition() 287 | { 288 | AxisCoords ac; 289 | getAxisCoords(&ac); 290 | 291 | // Store the home position in EEPROM 292 | #ifdef __arm__ 293 | byte b[sizeof(AxisCoords)]; 294 | memcpy(b, &ac, sizeof(AxisCoords)); 295 | FS.write(ADDR_HOME, b, sizeof(AxisCoords)); 296 | #else 297 | EEPROM.put(ADDR_HOME, ac); 298 | #endif 299 | } 300 | 301 | /***************************************************************************** 302 | Serial commands 303 | ******************************************************************************/ 304 | 305 | void cmdGetEqCoords(char *cmd) 306 | { 307 | EqCoords eq; 308 | getEqCoords(&eq); 309 | 310 | char tmp[19]; 311 | 312 | if (cmd[0] == 'E') 313 | sprintf(tmp, "%04X,%04X#", rad2nex(eq.ra), rad2nex(eq.dec)); 314 | else 315 | sprintf(tmp, "%08lX,%08lX#", rad2pnex(eq.ra), rad2pnex(eq.dec)); 316 | 317 | Serial.write(tmp); 318 | } 319 | 320 | void cmdGetAzCoords(char *cmd) 321 | { 322 | HorizCoords hc; 323 | getHorizCoords(&hc); 324 | 325 | char tmp[19]; 326 | 327 | if (cmd[0] == 'Z') 328 | sprintf(tmp, "%04X,%04X#", rad2nex(hc.az), rad2nex(hc.alt)); 329 | else 330 | sprintf(tmp, "%08lX,%08lX#", rad2pnex(hc.az), rad2pnex(hc.alt)); 331 | 332 | Serial.write(tmp); 333 | } 334 | 335 | void cmdGetPierSide(char *cmd) 336 | { 337 | // Celestron protocol use the opposite definition of pier side 338 | Serial.write(getPierSide() == PIER_EAST ? 'W' : 'E'); 339 | Serial.write('#'); 340 | } 341 | 342 | void cmdSyncEqCoords(char *cmd) 343 | { 344 | EqCoords eq; 345 | 346 | if (cmd[0] == 'S') { 347 | short unsigned int ra, dec; 348 | sscanf(cmd + 1, "%4hx,%4hx", &ra, &dec); 349 | eq.ra = nex2rad(ra); 350 | eq.dec = nex2rad(dec); 351 | } else { 352 | uint32_t ra, dec; 353 | sscanf(cmd + 1, "%8lx,%8lx", &ra, &dec); 354 | eq.ra = pnex2rad(ra); 355 | eq.dec = pnex2rad(dec); 356 | } 357 | 358 | setEqCoords(eq); 359 | Serial.write('#'); 360 | } 361 | 362 | void cmdGotoEqCoords(char *cmd) 363 | { 364 | EqCoords eq; 365 | 366 | if (cmd[0] == 'R') { 367 | short unsigned int ra, dec; 368 | sscanf(cmd + 1, "%4hx,%4hx", &ra, &dec); 369 | eq.ra = nex2rad(ra); 370 | eq.dec = nex2rad(dec); 371 | #ifdef ENABLE_HOMING 372 | // Home the mount if dec=pi/2 373 | if (dec == 0x4000) { 374 | event = EV_HOME; 375 | Serial.write('#'); 376 | return; 377 | } 378 | #endif 379 | } else { 380 | uint32_t ra, dec; 381 | sscanf(cmd + 1, "%8lx,%8lx", &ra, &dec); 382 | eq.ra = pnex2rad(ra); 383 | eq.dec = pnex2rad(dec); 384 | #ifdef ENABLE_HOMING 385 | // Home the mount if dec=pi/2 386 | if (dec == 0x40000000) { 387 | event = EV_HOME; 388 | Serial.write('#'); 389 | return; 390 | } 391 | #endif 392 | } 393 | 394 | #ifdef GO_BELOW_HORIZON 395 | target.ra = eq.ra; 396 | target.dec = eq.dec; 397 | event = EV_GOTO; 398 | #else 399 | // Obtain the horizontal coordinates of the target 400 | LocalCoords lc; 401 | HorizCoords hc; 402 | eqToLocalCoords(eq, &lc); 403 | localToHoriz(location, lc, &hc); 404 | 405 | // If target is above the horizon, go 406 | if (hc.alt >= 0) { 407 | target.ra = eq.ra; 408 | target.dec = eq.dec; 409 | event = EV_GOTO; 410 | } 411 | #endif 412 | Serial.write('#'); 413 | } 414 | 415 | void cmdGotoAzCoords(char *cmd) 416 | { 417 | HorizCoords hc; 418 | 419 | if (cmd[0] == 'B') { 420 | short unsigned int az, alt; 421 | sscanf(cmd + 1, "%4hx,%4hx", &az, &alt); 422 | hc.az = nex2rad(az); 423 | hc.alt = nex2rad(alt); 424 | } else { 425 | uint32_t az, alt; 426 | sscanf(cmd + 1, "%8lx,%8lx", &az, &alt); 427 | hc.az = pnex2rad(az); 428 | hc.alt = pnex2rad(alt); 429 | } 430 | 431 | if (hc.alt >= 0) { 432 | // If target is above the horizon, go 433 | LocalCoords lc; 434 | horizToLocal(location, hc, &lc); 435 | localToEqCoords(lc, getCurrentLST(), &target); 436 | event = EV_GOTO; 437 | } 438 | Serial.write('#'); 439 | } 440 | 441 | void cmdIsAligned(char *cmd) 442 | { 443 | Serial.write(aligned ? 1 : 0); 444 | Serial.write('#'); 445 | } 446 | 447 | void cmdGotoInProgress(char *cmd) 448 | { 449 | Serial.write(state == ST_IDLE ? '0' : '1'); 450 | Serial.write('#'); 451 | } 452 | 453 | void cmdCancelGoto(char *cmd) 454 | { 455 | event = EV_ABORT; 456 | Serial.write('#'); 457 | } 458 | 459 | void cmdGetTrackingMode(char *cmd) 460 | { 461 | Serial.write(tracking_mode); 462 | Serial.write('#'); 463 | } 464 | 465 | void cmdSetTrackingMode(char *cmd) 466 | { 467 | //TODO: store tracking mode in EEPROM and engage tracking at start 468 | tracking_mode = cmd[1]; 469 | nexstar.setGuiderate(DEV_RA, GUIDERATE_POS, true, 0); // stop RA motor 470 | nexstar.setGuiderate(DEV_DEC, GUIDERATE_POS, true, 0); // stop DEC motor 471 | 472 | switch(tracking_mode) { 473 | case TRACKING_EQ_NORTH: 474 | nexstar.setGuiderate(DEV_RA, GUIDERATE_POS, 0, GUIDERATE_SIDEREAL); 475 | break; 476 | case TRACKING_EQ_SOUTH: 477 | nexstar.setGuiderate(DEV_RA, GUIDERATE_NEG, 0, GUIDERATE_SIDEREAL); 478 | break; 479 | } 480 | 481 | Serial.write('#'); 482 | } 483 | 484 | void cmdGetLocation(char *cmd) 485 | { 486 | SxAngle sxLat, sxLong; 487 | 488 | rad2sx(normalizePi(location.lat), &sxLat); 489 | Serial.write(sxLat.deg); 490 | Serial.write(sxLat.min); 491 | Serial.write(sxLat.sec); 492 | Serial.write(sxLat.sign); 493 | 494 | rad2sx(normalizePi(location.lon), &sxLong); 495 | Serial.write(sxLong.deg); 496 | Serial.write(sxLong.min); 497 | Serial.write(sxLong.sec); 498 | Serial.write(sxLong.sign); 499 | Serial.write('#'); 500 | } 501 | 502 | void cmdSetLocation(char *cmd) 503 | { 504 | SxAngle latitude = { 505 | (uint8_t)cmd[1], (uint8_t)cmd[2], 506 | (uint8_t)cmd[3], (uint8_t)cmd[4] 507 | }; 508 | 509 | SxAngle longitude = { 510 | (uint8_t)cmd[5], (uint8_t)cmd[6], 511 | (uint8_t)cmd[7], (uint8_t)cmd[8] 512 | }; 513 | 514 | float lat = normalizePi(sx2rad(latitude)); 515 | float lon = normalizePi(sx2rad(longitude)); 516 | 517 | if ((lat != location.lat) || (lon != location.lon)) { 518 | location.lat = lat; 519 | location.lon = lon; 520 | 521 | ref_lst = getLST(now(), location); 522 | synced = false; 523 | 524 | writeLocation(location); 525 | } 526 | 527 | Serial.write('#'); 528 | } 529 | 530 | void cmdSetTime(char *cmd) 531 | { 532 | int hour = (int)cmd[1]; 533 | int min = (int)cmd[2]; 534 | int sec = (int)cmd[3]; 535 | int month = (int)cmd[4]; 536 | int day = (int)cmd[5]; 537 | int year = (int)cmd[6] + 2000; 538 | int offset = (int)cmd[7]; 539 | //int dst = (int)cmd[8]; 540 | 541 | setTime(hour, min, sec, day, month, year); 542 | adjustTime(-offset*3600); 543 | 544 | ref_t = now(); 545 | ref_lst = getLST(ref_t, location); 546 | time_set = true; 547 | synced = false; 548 | 549 | Serial.write('#'); 550 | } 551 | 552 | void cmdGetTime(char *cmd) 553 | { 554 | time_t t = now(); 555 | Serial.write(hour(t)); 556 | Serial.write(minute(t)); 557 | Serial.write(second(t)); 558 | Serial.write(month(t)); 559 | Serial.write(day(t)); 560 | Serial.write(year(t) % 2000); 561 | Serial.write((uint8_t)0); 562 | Serial.write((uint8_t)0); 563 | Serial.write("#"); 564 | } 565 | 566 | void cmdPassThrough(char *cmd) 567 | { 568 | char resp[8]; 569 | uint8_t size; 570 | 571 | if (nexstar.sendRawCommand(cmd, resp, &size) == 0) { 572 | for (int i=0; i < size; i++) { 573 | Serial.write(resp[i]); 574 | } 575 | } else { 576 | // indicate an error by returning a response of size = resp_size + 1 577 | for (int i=0; i <= size; i++) { 578 | Serial.write('0'); 579 | } 580 | } 581 | 582 | Serial.write('#'); 583 | } 584 | 585 | void cmdGetVersion(char *cmd) 586 | { 587 | Serial.write(VERSION_MAJOR); 588 | Serial.write(VERSION_MINOR); 589 | Serial.write('#'); 590 | } 591 | 592 | void cmdGetVariant(char *cmd) 593 | { 594 | Serial.write(CONTROLLER_VARIANT); 595 | Serial.write('#'); 596 | } 597 | 598 | void cmdGetModel(char *cmd) 599 | { 600 | Serial.write(MOUNT_MODEL); 601 | Serial.write('#'); 602 | } 603 | 604 | void cmdEcho(char *cmd) 605 | { 606 | Serial.write(cmd[1]); 607 | Serial.write('#'); 608 | } 609 | 610 | void cmdHibernate(char *cmd) 611 | { 612 | //TODO 613 | } 614 | 615 | void cmdWakeup(char *cmd) 616 | { 617 | //TODO 618 | Serial.write('#'); 619 | } 620 | 621 | void setup() 622 | { 623 | // Map serial commands to functions 624 | sCmd.addCommand('E', 1, cmdGetEqCoords); 625 | sCmd.addCommand('e', 1, cmdGetEqCoords); 626 | sCmd.addCommand('Z', 1, cmdGetAzCoords); 627 | sCmd.addCommand('z', 1, cmdGetAzCoords); 628 | sCmd.addCommand('p', 1, cmdGetPierSide); 629 | 630 | sCmd.addCommand('S', 10, cmdSyncEqCoords); 631 | sCmd.addCommand('s', 18, cmdSyncEqCoords); 632 | 633 | sCmd.addCommand('R', 10, cmdGotoEqCoords); 634 | sCmd.addCommand('r', 18, cmdGotoEqCoords); 635 | 636 | sCmd.addCommand('B', 10, cmdGotoAzCoords); 637 | sCmd.addCommand('b', 18, cmdGotoAzCoords); 638 | 639 | sCmd.addCommand('L', 1, cmdGotoInProgress); 640 | sCmd.addCommand('M', 1, cmdCancelGoto); 641 | sCmd.addCommand('J', 1, cmdIsAligned); 642 | 643 | sCmd.addCommand('T', 2, cmdSetTrackingMode); 644 | sCmd.addCommand('t', 1, cmdGetTrackingMode); 645 | 646 | sCmd.addCommand('W', 9, cmdSetLocation); 647 | sCmd.addCommand('w', 1, cmdGetLocation); 648 | sCmd.addCommand('H', 9, cmdSetTime); 649 | sCmd.addCommand('h', 1, cmdGetTime); 650 | 651 | sCmd.addCommand('P', 8, cmdPassThrough); 652 | sCmd.addCommand('V', 1, cmdGetVersion); 653 | sCmd.addCommand('v', 1, cmdGetVariant); 654 | sCmd.addCommand('m', 1, cmdGetModel); 655 | sCmd.addCommand('K', 2, cmdEcho); 656 | 657 | sCmd.addCommand('x', 1, cmdHibernate); 658 | sCmd.addCommand('y', 1, cmdWakeup); 659 | 660 | pinMode(AUX_SELECT, OUTPUT); 661 | //pinMode(LED_BUILTIN, OUTPUT); 662 | pinMode(LED_STATUS_R, OUTPUT); 663 | pinMode(LED_STATUS_G, OUTPUT); 664 | pinMode(LED_RED, OUTPUT); 665 | pinMode(LED_GREEN, OUTPUT); 666 | pinMode(LED_YELLOW, OUTPUT); 667 | 668 | pinMode(HOME_BUTTON, INPUT_PULLUP); 669 | pinMode(ABORT_BUTTON, INPUT_PULLUP); 670 | pinMode(LIMIT_RA_PIN, INPUT_PULLUP); 671 | pinMode(LIMIT_ALT_PIN, INPUT_PULLUP); 672 | 673 | aux.begin(AUX_BAUDRATE); 674 | 675 | Serial.begin(BAUDRATE); 676 | nexstar.init(); 677 | 678 | // read the location from EEPROM 679 | readLocation(&location); 680 | 681 | setAxisCoords(home_pos); 682 | 683 | nexstar.setGuiderate(DEV_RA, GUIDERATE_POS, true, 0); // stop RA motor 684 | nexstar.setGuiderate(DEV_DEC, GUIDERATE_POS, true, 0); // stop DEC motor 685 | } 686 | 687 | // Update the scope status with a simple state machine 688 | void updateFSM() 689 | { 690 | static uint32_t t_timer; 691 | static bool ra_homed = 0, dec_homed = 0, dec_homing_dir = 0; 692 | 693 | if (event == EV_ABORT) { 694 | stopMotors(); 695 | state = ST_IDLE; 696 | event = EV_NONE; 697 | return; 698 | } 699 | 700 | switch(state) { 701 | case ST_IDLE: 702 | if (event == EV_GOTO) { 703 | t_timer = millis(); 704 | if (checkMeridianFlip(target)) { 705 | // go to the pole before flipping 706 | gotoAxisCoords((AxisCoords){M_PI/2, M_PI/2}, false); 707 | state = ST_MERIDIAN_FLIP; 708 | break; 709 | } 710 | // TODO: home the mount if the target coords are (0, pi/2) 711 | gotoEqCoords(target, false); 712 | state = ST_SLEWING_FAST; 713 | } else if (event == EV_HOME) { 714 | dec_homing_dir = !decHomeSensor(); 715 | nexstar.move(DEV_DEC, dec_homing_dir, 9); 716 | aligned = false; 717 | state = ST_HOMING_DEC; 718 | } 719 | break; 720 | 721 | case ST_MERIDIAN_FLIP: 722 | if (millis() - t_timer > 500) { 723 | // Every 0.5 seconds, check if we are close to the target 724 | t_timer = millis(); 725 | 726 | if (slewDone()) { 727 | gotoEqCoords(target, false); 728 | state = ST_SLEWING_FAST; 729 | } 730 | } 731 | break; 732 | 733 | case ST_SLEWING_FAST: 734 | if (millis() - t_timer > 500) { 735 | // Every 0.5 seconds, check if we are close to the target 736 | t_timer = millis(); 737 | 738 | if (slewDone()) { 739 | gotoEqCoords(target, true); 740 | state = ST_SLEWING_SLOW; 741 | } 742 | } 743 | break; 744 | 745 | case ST_SLEWING_SLOW: 746 | if (millis() - t_timer > 250) { 747 | // Every 0.25 seconds, check if slew is done 748 | t_timer = millis(); 749 | 750 | if (slewDone()) { 751 | state = ST_IDLE; 752 | } 753 | } 754 | break; 755 | 756 | case ST_HOMING_DEC: 757 | // Move CW dec axis until the photodiode changes 758 | if (dec_homing_dir == decHomeSensor()) { 759 | nexstar.move(DEV_DEC, 0, 9); 760 | nexstar.move(DEV_RA, 0, 9); 761 | ra_homed = 0; 762 | dec_homed = 0; 763 | state = ST_HOMING_FAST1; 764 | } 765 | break; 766 | 767 | case ST_HOMING_FAST1: 768 | // Move CW each axis until the photodiode receives light 769 | if (!dec_homed && !decHomeSensor()) { 770 | nexstar.move(DEV_DEC, 0, 0); 771 | dec_homed = 1; 772 | } 773 | if (!ra_homed && !raHomeSensor()) { 774 | nexstar.move(DEV_RA, 0, 0); 775 | ra_homed = 1; 776 | } 777 | 778 | if (ra_homed && dec_homed) { 779 | ra_homed = 0; 780 | dec_homed = 0; 781 | nexstar.move(DEV_DEC, 1, 9); 782 | nexstar.move(DEV_RA, 1, 9); 783 | state = ST_HOMING_FAST2; 784 | } 785 | break; 786 | 787 | case ST_HOMING_FAST2: 788 | // Move CCW each axis until the photodiode stops receiving light 789 | if (!dec_homed && decHomeSensor()) { 790 | nexstar.move(DEV_DEC, 0, 0); 791 | dec_homed = 1; 792 | } 793 | if (!ra_homed && raHomeSensor()) { 794 | nexstar.move(DEV_RA, 0, 0); 795 | ra_homed = 1; 796 | } 797 | 798 | if (ra_homed && dec_homed) { 799 | ra_homed = 0; 800 | dec_homed = 0; 801 | nexstar.move(DEV_DEC, 0, 6); 802 | nexstar.move(DEV_RA, 0, 6); 803 | state = ST_HOMING_SLOW1; 804 | } 805 | break; 806 | 807 | case ST_HOMING_SLOW1: 808 | // Move CCW each axis until the photodiode stops receiving light 809 | if (!dec_homed && !decHomeSensor()) { 810 | nexstar.move(DEV_DEC, 0, 0); 811 | dec_homed = 1; 812 | } 813 | if (!ra_homed && !raHomeSensor()) { 814 | nexstar.move(DEV_RA, 0, 0); 815 | ra_homed = 1; 816 | } 817 | 818 | if (ra_homed && dec_homed) { 819 | ra_homed = 0; 820 | dec_homed = 0; 821 | nexstar.move(DEV_DEC, 1, 4); 822 | nexstar.move(DEV_RA, 1, 4); 823 | state = ST_HOMING_SLOW2; 824 | } 825 | break; 826 | 827 | case ST_HOMING_SLOW2: 828 | if (!dec_homed && decHomeSensor()) { 829 | nexstar.move(DEV_DEC, 0, 0); 830 | dec_homed = 1; 831 | } 832 | if (!ra_homed && raHomeSensor()) { 833 | nexstar.move(DEV_RA, 0, 0); 834 | ra_homed = 1; 835 | } 836 | 837 | if (ra_homed && dec_homed) { 838 | setAxisCoords(home_pos); 839 | 840 | aligned = true; 841 | state = ST_IDLE; 842 | } 843 | break; 844 | }; 845 | 846 | event = EV_NONE; 847 | } 848 | 849 | void updateLEDs() 850 | { 851 | if (!time_set) { 852 | digitalWrite(LED_STATUS_G, LOW); 853 | digitalWrite(LED_STATUS_R, HIGH); 854 | } else if (!synced) { 855 | digitalWrite(LED_STATUS_G, HIGH); 856 | digitalWrite(LED_STATUS_R, HIGH); 857 | } else { 858 | digitalWrite(LED_STATUS_G, HIGH); 859 | digitalWrite(LED_STATUS_R, LOW); 860 | } 861 | 862 | digitalWrite(LED_GREEN, (state != ST_IDLE)); 863 | digitalWrite(LED_YELLOW, parked); 864 | } 865 | 866 | void loop() 867 | { 868 | if (!digitalRead(ABORT_BUTTON) && state != ST_IDLE) 869 | event = EV_ABORT; 870 | else if (!digitalRead(HOME_BUTTON) && state != ST_HOMING_DEC) 871 | event = EV_HOME; 872 | 873 | sCmd.readSerial(); 874 | updateFSM(); 875 | updateLEDs(); 876 | nexstar.run(); 877 | } 878 | --------------------------------------------------------------------------------