diff --git a/README.md b/README.md index 4ea27b5..b67dabb 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,5 @@ # Dwarf II TCP server -**NOTE: This is a work in progress. It's been cloudy, so I haven't tested this code yet.** - This software lets you send goto commands to the Dwarf II using Stellarium's Telescope Control. Requires: @@ -22,15 +20,28 @@ This software setups up a TCP server. When you select an object Stellarium, and 3. Install libraries +On Linux, Mac or WSL for Windows : + ``` pip install -r requirements.txt ``` +On Windows : + +you can install Python 3.11 (search on Microsoft Store) +open a command prompt on the directory and do : + +``` +python3 -m pip install -r requirements.txt +``` + 4. Copy `config.sample.py`, and rename it to `config.py`. The `HOST` and `PORT` should be the same as those used in Stellarium Telescope Plugin. -Fill in your `LATITUDE` and `LONGITUDE`. +Fill in your `LATITUDE` and `LONGITUDE` (LONGITUDE is negative west of Greenwich) + +Add also your `TIMEZONE`, it's need when you are using the Stellarium Mobile App If you are using the Dwarf wifi, the `DWARF_IP` is 192.168.88.1. If you are using Dwarf II in STA mode, then get the IP for your Dwarf II. @@ -43,4 +54,16 @@ If you are using the Dwarf wifi, the `DWARF_IP` is 192.168.88.1. If you are usin python server.py ``` -6. Select an object in Stellarium, and issue a slew command. The Dwarf II should move to that object. +On Windows : + +``` +python3 server.py +``` + +6. Start the dwarf II and use the mobile app and go to Astro Mode and do the calibration + +7. Select an object in Stellarium, and issue a slew command. (shortcut for Windows is Alt + number, See Stellarium documentation, Command + number for Mac). The Dwarf II should move to that object. + +8. You can also use the Stellarium Plus Mobile App with the remote Telescopte function, Select an object in Stellarium, and issue a goto command with the remote control. The Dwarf II should move to that object. + +**NOTE: If the server disconnects from Stellarium, You can try reconnect on the cmd window by typing Y otherwise the program will stop ** diff --git a/config.sample.py b/config.sample.py index 6225c32..8acd619 100644 --- a/config.sample.py +++ b/config.sample.py @@ -3,4 +3,5 @@ LATITUDE = 0 LONGITUDE = 0 DWARF_IP = "192.168.88.1" +TIME_ZONE="Europe/Paris" DEBUG = True diff --git a/lib/dwarfII_api.py b/lib/dwarfII_api.py index 88812a0..2ad96a8 100644 --- a/lib/dwarfII_api.py +++ b/lib/dwarfII_api.py @@ -9,18 +9,21 @@ def ws_uri(dwarf_ip): return f"ws://{dwarf_ip}:9900" -def now(): - return str(datetime.datetime.now()).split(".")[0] +def nowUTC(): + return str(datetime.datetime.utcnow()).split(".")[0] +def nowLocalFileName(): + return datetime.datetime.today().strftime('%Y%m%d%H%M%S') + def goto_target(latitude, longitude, rightAscension, declination, planet=None): options = { "interface": startGotoCmd, "camId": telephotoCamera, "lon": longitude, "lat": latitude, - "date": now(), - "path": "DWARF_GOTO_timestamp", + "date": nowUTC(), + "path": "DWARF_GOTO_" + nowLocalFileName(), } if planet is not None: diff --git a/lib/dwarf_utils.py b/lib/dwarf_utils.py index 1fdb92d..726d7e2 100644 --- a/lib/dwarf_utils.py +++ b/lib/dwarf_utils.py @@ -4,14 +4,18 @@ import lib.my_logger as log -def perform_goto(ra, dec): - payload = d2.goto_target(config.LATITUDE, config.LONGITUDE, ra, dec) +def perform_goto(ra, dec, result_queue): + # Inverse LONGITUDE for DwarfII !!!!!!! + payload = d2.goto_target(config.LATITUDE, - config.LONGITUDE, ra, dec) response = connect_socket(payload) - if response["interface"] == payload["interface"]: + if response: + + if response["interface"] == payload["interface"]: if response["code"] == 0: log.debug("Goto success") + result_queue.put("ok") return "ok" elif response["code"] == -45: log.error("Target below horizon") @@ -19,9 +23,12 @@ def perform_goto(ra, dec): log.error("Goto or correction bump limit") else: log.error("Error:", response) - else: + else: log.error("Dwarf API:", response) + else: + log.error("Dwarf API:", "Dwarf II not connected") + result_queue.put(False) def perform_camera_status(): payload = d2.cameraWorkingState() diff --git a/lib/stellarium_utils.py b/lib/stellarium_utils.py index 2120d5c..87bb0ca 100644 --- a/lib/stellarium_utils.py +++ b/lib/stellarium_utils.py @@ -4,7 +4,6 @@ import lib.my_logger as my_logger - def process_ra_dec_int(ra_int, dec_int): h = ra_int d = math.floor(0.5 + dec_int * (360 * 3600 * 1000 / 4294967296.0)) @@ -45,6 +44,21 @@ def process_ra_dec_int(ra_int, dec_int): }, } +def process_ra_dec(ra_int, dec_int): + data = process_ra_dec_int(ra_int, dec_int) + ra = data["ra"] + dec = data["dec"] + ra_number = ra['hour'] + ra['minute'] / 60 + ra['second'] / 3600 + if dec_int >= 0: + dec_number = dec['degree'] + dec['minute'] / 60 + dec['second'] / 3600 + else: + dec_number = -(dec['degree'] + dec['minute'] / 60 + dec['second'] / 3600) + + return { + "ra_number": ra_number, + "dec_number": dec_number + } + def format_ra_dec(ra_int, dec_int): data = process_ra_dec_int(ra_int, dec_int) @@ -59,20 +73,34 @@ def format_ra_dec(ra_int, dec_int): def process_stellarium_data(raw_data): - data = struct.unpack("3iIi", raw_data) - my_logger.debug("data from Stellarium >>", data) + my_logger.debug("data from Stellarium >>", raw_data) + try: + data = struct.unpack("3iIi", raw_data) + my_logger.debug("data from Stellarium >>", data) - ra_int = data[3] - dec_int = data[4] - formatted_data = format_ra_dec(ra_int, dec_int) - my_logger.debug("ra: " + formatted_data["ra"] + ", dec: " + formatted_data["dec"]) + ra_int = data[3] + dec_int = data[4] + formatted_data = format_ra_dec(ra_int, dec_int) + my_logger.debug("ra: " + formatted_data["ra"] + ", dec: " + formatted_data["dec"]) - return { - "ra_int": ra_int, - "ra": formatted_data["ra"], - "dec_int": dec_int, - "dec": formatted_data["dec"], - } + data_number = process_ra_dec(ra_int, dec_int) + ra_number = data_number["ra_number"] + dec_number = data_number["dec_number"] + my_logger.debug("ra: " + f"{ra_number}" + ", dec: " + f"{dec_number}") + + return { + "ra_int": ra_int, + "ra": formatted_data["ra"], + "ra_number": ra_number, + "dec_int": dec_int, + "dec": formatted_data["dec"], + "dec_number": dec_number, + } + + except struct.error: + # If a struct.error occurs, the data is not in the expected structure format + my_logger.debug("data from Stellarium Mobile >>", raw_data) + return False def update_stellarium(ra_int, dec_int, connection): diff --git a/lib/websockets_utils.py b/lib/websockets_utils.py index b49b58b..f2bbe7b 100644 --- a/lib/websockets_utils.py +++ b/lib/websockets_utils.py @@ -16,3 +16,4 @@ def connect_socket(payload): return json.loads(message) except TimeoutError: my_logger.error("Could not connect to websocket") + return False diff --git a/server.py b/server.py index fd0ce5d..e64f1c8 100644 --- a/server.py +++ b/server.py @@ -1,36 +1,277 @@ import socket +import threading +from queue import Queue + +from datetime import datetime +from zoneinfo import ZoneInfo import config import lib.my_logger as my_logger from lib.stellarium_utils import process_stellarium_data, update_stellarium from lib.dwarf_utils import perform_goto -# create socket -sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) -# ensure we can quickly restart server -sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) -# set host and port -sock.bind((config.HOST, config.PORT)) -# set number of client that can be queued -sock.listen(5) +# Convert Deg to +def process_deg_dec(deg_dec): + deg_sign = "" + + if deg_dec >= 0: + deg_sign = "+" + else: + deg_sign = "-" + deg_dec = -deg_dec + + d = int(deg_dec) + my_logger.debug(f"process_deg_dec: {deg_sign}{deg_dec}<->{d}") + + dec_min = int((deg_dec - d) * 60 ) + dec_sec = int((deg_dec - d - (dec_min / 60))*3600) + + return { + "deg_sign": deg_sign, + "degree": d, + "minute": dec_min, + "second": dec_sec, + } + + +# Create a Queue to pass the result from the thread to the main program +result_queue = Queue() +dwarf_ra_deg = 0 +dwarf_dec_deg = 0 +dwarf_goto_thread_started = False + +# Create a thread and pass variables to it +dwarf_goto_thread = False + +nbDeconnect = 10 while True: - # create new socket to interact with client - new_socket, addr = sock.accept() - my_logger.debug(f"Connected by {addr}") - raw_data = new_socket.recv(1024) - if not raw_data: - break + my_logger.debug(f"Waiting Connection to Stellarium : {config.HOST}") + + # create socket + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + # ensure we can quickly restart server + sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + # set host and port + sock.bind((config.HOST, config.PORT)) + # set number of client that can be queued + sock.listen(5) + + while True: + # create new socket to interact with client + new_socket, addr = sock.accept() + my_logger.debug(f"Connected by {addr}") + + raw_data = new_socket.recv(1024) + if not raw_data: + break + + # process data from Stellarium PC to get RA/Dec + data = process_stellarium_data(raw_data) + + if (data): # For Stellarium PC + # send goto command to DWARF II + result = perform_goto(data["ra_number"], data["dec_number"], result_queue) + + # add DWARF II to Stellarium's sky map + if result == "ok": + update_stellarium(data["ra_int"], data["dec_int"], new_socket) + + new_socket.close() + + else: # For Stellarium Mobile Plus simulate Telescope Nexstar + location = "" + datetoday = "" + goto_data = b'1B4A735F,3F7A6B85#' # Init to Polaris Position + goto = False + ra_deg = 0 + dec_deg = 0 + + while True: + send_data = b'##' + if (raw_data == b'Ka'): + send_data = b'a#' + if (raw_data == b'V'): # Version + send_data = b'\x02\x06#' + if (raw_data == b'P\x01\x10\xfe\x00\x00\x00\x02'): # Version AZM/ RA motor + send_data = b'\x02\x06#' + if (raw_data == b'P\x01\x11\xfe\x00\x00\x00\x02'): # Version Alt/ DEC motor + send_data = b'\x02\x06#' + if (raw_data == b'm'): # Model + send_data = b'\x10#' + if (raw_data == b't'): # Tracking Mode + send_data = b'\x00#' # b'\x02# + if (raw_data == b'L'): # Is GOTO + if (goto): + send_data = b'1#' + else: + send_data = b'0#' + if (raw_data == b'J'): # Is Align + send_data = b'\x01#' + if (raw_data == b'e'): # Position + send_data = goto_data + + if (raw_data == b'h'): # Send Time + today = datetime.now() + # Get Local Timezone from Config + try: + local_timezone = config.TIME_ZONE + except AttributeError as e: + local_timezone = "UTC" + my_logger.debug(f"No TimeZone defined in config, UTC used") + + # Show Local Timezone + my_logger.debug(f"Local Timezone:", local_timezone) + date_GMTDST = datetime(today.year, today.month, today.day, tzinfo=ZoneInfo(local_timezone)) + # Get GMT + gmt = int(date_GMTDST.utcoffset().total_seconds() // 3600) + if (gmt < 0): + gmt = 256 - gmt + # Get DST + dst_active = 1 + if (int(date_GMTDST.dst().total_seconds()) == 0): + dst_active = 0 + my_logger.debug(f"Local Date Time :", today, gmt, dst_active) + + # today_hexa = f"{hex(today.hour)[2:]}{hex(today.minute)[2:]}{hex(today.second)[2:]}{hex(today.month)[2:]}{hex(today.day)[2:]}{hex(today.year-2000)[2:]}{hex(gmt)[2:]}{hex(dst_active)[2:]}" + today_hexa = f"{today.hour:02x}{today.minute:02x}{today.second:02x}{today.month:02x}{today.day:02x}{today.year-2000:02x}{gmt:02x}{dst_active:02x}" + my_logger.debug(f"Date Time hexa: ", today_hexa) + send_data = bytes.fromhex(today_hexa) + b'#' + + if (raw_data[0] == ord('H')): # Get Time + datetoday = raw_data[1:] + Time = f"{raw_data[1]}H{raw_data[2]}M{raw_data[3]}S" + Date = f"{raw_data[4]}/{raw_data[5]}/20{raw_data[6]}\"" + GMT = f"GMT +{raw_data[7]}" + my_logger.debug(f"Date Time GMT :", GMT) + if (int(raw_data[7]) <= 127): + GMT = f"GMT +{raw_data[7]}" + else: + GMT = f"GMT -{256-raw_data[7]}" + DST = "" + if (raw_data[8] == 1): + DST = "DST" + my_logger.debug(f"Date Time :", Date, Time, GMT, DST) + send_data = b'#' + + if (raw_data == b'w'): # send Local Location + send_data = b'/\x13,\x00\x01),\x01' + if (location): + send_data = location + b'#' + # Get Local Location from Config + try: + Latitude = config.LATITUDE + Longitude = config.LONGITUDE + my_logger.debug(f"Config Local Location:", Latitude, Longitude) + except AttributeError as e: + local_timezone = "UTC" + my_logger.debug(f"No Local Location Info in config, send Location from phone") + if (location): + send_data = location + b'#' + + data_Lat = process_deg_dec(Latitude) + Lat_NS = 0 + if (data_Lat['deg_sign'] == "-"): + Lat_NS = 1 + + data_Lon = process_deg_dec(Longitude) + Lon_WE = 0 + if (data_Lon['deg_sign'] == "-"): + Lon_WE = 1 + + str_lattitude = f"{data_Lat['degree']}°{data_Lat['minute']}'{data_Lat['second']}\"" + if (data_Lat['deg_sign'] == "+"): + str_lattitude += "N" + else: + str_lattitude += "S" + + str_longitude = f"{data_Lon['degree']}°{data_Lon['minute']}'{data_Lon['second']}\"" + if (data_Lon['deg_sign'] == "-"): + str_longitude += "W" + else: + str_longitude += "E" + my_logger.debug(f"Local Location:", str_lattitude, str_longitude) + + data_location_hexa = f"{data_Lat['degree']:02x}{data_Lat['minute']:02x}{data_Lat['second']:02x}{Lat_NS:02x}{data_Lon['degree']:02x}{data_Lon['minute']:02x}{data_Lon['second']:02x}{Lon_WE:02x}" + my_logger.debug(f"Local Location hexa: ", data_location_hexa) + send_data = bytes.fromhex(data_location_hexa) + b'#' + + if (raw_data[0] == ord('W')): # get Location + location = raw_data[1:] + lattitude = f"{raw_data[1]}°{raw_data[2]}'{raw_data[3]}\"" + if (raw_data[4] == 0): + lattitude += "N" + else: + lattitude += "S" + longitude = f"{raw_data[5]}°{raw_data[6]}'{raw_data[7]}\"" + if (raw_data[8] == 0): + longitude += "E" + else: + longitude += "W" + my_logger.debug(f"Location:", lattitude, longitude) + send_data = b'#' + + # GOTO Command + if (raw_data[0] == ord('r')): + my_logger.debug(f"Receive Goto Command : {raw_data}") + my_logger.debug(f"Receive Goto Command : RA {raw_data[1:7]}") + my_logger.debug(f"Receive Goto Command : DEC {raw_data[10:16]}") + ra_data = int(raw_data[1:7], base=16) + ra_deg = (ra_data / 16777216) * 360 + dec_data = int(raw_data[10:16], base=16) + dec_deg = (dec_data / 16777216) * 360 + if (dec_deg > 90): + dec_deg = 90 - dec_deg + my_logger.debug(f"Receive Goto Command : Ra:{ra_deg} Dec:{dec_deg}") + + if (dwarf_goto_thread_started): + my_logger.debug(f"Receive Goto Command : Goto is still Processing Wait...") + else: + goto = True + + # start thread goto command to DWARF II + dwarf_goto_thread_started = True + dwarf_ra_deg = ra_deg; + dwarf_dec_deg = dec_deg; + dwarf_goto_thread = threading.Thread(target=perform_goto, args=(dwarf_ra_deg, dwarf_dec_deg, result_queue)) + dwarf_goto_thread.start() + + send_data = b'#' + + my_logger.debug(f"Sending ...", send_data) + new_socket.send(send_data) + + raw_data = new_socket.recv(1024) + if not raw_data: + break + + my_logger.debug("data from Stellarium >>", raw_data) + + if goto: + if not(dwarf_goto_thread.is_alive()): + my_logger.debug("End of Dwarf Goto") + dwarf_goto_thread_started = False + goto = False + # Retrieve the result from the Queue + result = result_queue.get() + + # add DWARF II to Stellarium's sky map + if result == "ok": + goto_data = raw_data[1:] - # process data from Stellarium to get RA/Dec - data = process_stellarium_data(raw_data) + # Create a thread and pass variables to it for next time + dwarf_goto_thread = False - # send goto command to DWARF II - result = perform_goto(data["ra"], data["dec"]) + my_logger.debug(f"Disconnected from Stellarium : {addr}") - # add DWARF II to Stellarium's sky map - if result == "ok": - update_stellarium(data["ra_int"], data["dec_int"], new_socket) + nbDeconnect -= 1 - new_socket.close() + if (nbDeconnect <= 0): + restart = input("Restart? [Y/n]") + + if restart != "Y" and restart != "y": + my_logger.debug("End of Server") + break; + else: + nbDeconnect = 3