En este post realizaremos un programa en Python que simule la conexión con un telescopio para Stellarium, de forma que podamos tanto recibir coordenadas desde él, como enviárselas para que muestre el visor en dichas coordenadas, indicando así hacia dónde apunta, supuestamente, el telescopio simulado.
Las coordenadas que se reciben (o se esperan) desde Stellarium son coordenadas ecuatoriales. En otro post entraré en las cuestiones de conversión de coordenadas, pero por el momento nos limitaremos a mostrar los valores recibidos desde Stellarium, y a enviarle otros valores (o los mismos..) para que muestre el visor en pantalla.
En la página oficial de Stellarium podemos encontrar información acerca de cómo controlar telescopios. Para este proyecto optaremos por la alternativa cliente-servidor sobre TCP/IP, de forma que implementaremos un servidor en Python para el “Stellarium Telescope Protocol”.
En principio, sólo necesitamos gestionar una sola conexión de forma asíncrona, por lo que usaremos una implementación simple de un socket ayudándonos del módulo asyncore de Python.
Este protocolo se basa en dos tipos de mensajes, uno en el sentido servidor→cliente, y otro para el sentido contrario, cliente→servidor, con la siguiente estructura:
Esquema básico de un mensaje servidor→cliente
LENGTH (2 bytes, entero): tamaño de mensaje
TYPE (2 bytes, entero): 0
TIME (8 bytes, entero): tiempo actual en el servidor en
microsegundos desde 01/01/1970 UT.
RA
(4 bytes, entero sin signo): Ascensión recta (J2000)
un valor de 0x100000000 = 0x0 significa 24h=0h,
un valor de 0x080000000 significa 12h
DEC (4 bytes, entero con signo): Declinación (J2000)
un valor de -0x40000000 significa -90 grados,
un valor de 0x0 significa 0 grados,
un valor de 0x40000000 significa 90 grados
STATUS (4 bytes, entero con signo): estado.
0 significa ok, < 0 significa algún error
Esquema básico de un mensaje cliente→servidor
LENGTH (2 bytes, entero): tamaño de mensaje
TYPE (2 bytes, entero): 0
TIME (8 bytes, entero): tiempo actual en el servidor en
microsegundos desde 01/01/1970 UT.
RA
(4 bytes, entero sin signo): Ascensión recta (J2000)
un valor de 0x100000000 = 0x0 significa 24h=0h,
un valor de 0x080000000 significa 12h
DEC (4 bytes, entero con signo): Declinación (J2000)
un valor de -0x40000000 significa -90 grados,
un valor de 0x0 significa 0 grados,
un valor de 0x40000000 significa 90 grados
Todos los valores se transmiten en formato little-endian, y para su tratamiento usaremos el tipo ConstBitStream de la biblioteca python-bitstring. Esta biblioteca se encuentra disponible en code.google.com bajo licencia del MIT y su Open Source Initiative, y permite un tratamiento flexible e intuitivo de los datos a nivel de bits en Python.
El siguiente fragmento de código muestra por la terminal las coordenadas recibidas desde Stellarium, y se las vuelve a enviar para mostrar el visor en pantalla:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 |
#!/usr/bin/python # -*- coding: utf-8 -*- import math import logging from PyQt4 import QtCore import asyncore, socket from time import sleep, time from string import replace from bitstring import BitArray, BitStream, ConstBitStream import coords logging.basicConfig(level=logging.DEBUG, format="%(filename)s: %(funcName)s - %(levelname)s: %(message)s") ## \brief Implementación de la parte del servidor para el 'Stellarium Telescope Protocol'. # # Establece la ejecución de un hilo independiente para gestionar las comunicaciones con el lado del # cliente (programa Stellarium). class Telescope_Channel(QtCore.QThread, asyncore.dispatcher): ## Constructor de la clase. # # \param conn_sock Socket de la conexión establecida con el cliente. def __init__(self, conn_sock): self.is_writable = False self.buffer = '' asyncore.dispatcher.__init__(self, conn_sock) QtCore.QThread.__init__(self, None) ## Indica si ya se puede leer del socket. # # \return Booleano True/False. def readable(self): return True ## Indica si el socket está disponible para escribir en él. # # \return Booleano True/False. def writable(self): return self.is_writable ## Manejador para el evento de cierre de la conexión. def handle_close(self): logging.debug("Desconectado") self.close() ## Manejador para la lectura del socket. # # Lee los datos recibidos por el socket desde el cliente, los procesa para obtener las coordenadas # en el formato adecuado y los imprime por consola. def handle_read(self): #formato: 20 bytes en total. #Los mensajes entrantes vienen con 160 bytes.. data0 = self.recv(160); if data0: data = ConstBitStream(bytes=data0, length=160) #print "Recibido (binario): %s" % data.bin msize = data.read('intle:16') mtype = data.read('intle:16') mtime = data.read('intle:64') # RA: ant_pos = data.bitpos ra = data.read('hex:32') data.bitpos = ant_pos ra_uint = data.read('uintle:32') # DEC: ant_pos = data.bitpos dec = data.read('hex:32') data.bitpos = ant_pos dec_int = data.read('intle:32') logging.debug("Size: %d, Type: %d, Time: %d, RA: %d (%s), DEC: %d (%s)" % (msize, mtype, mtime, ra_uint, ra, dec_int, dec)) (sra, sdec, stime) = coords.eCoords2str(float("%f" % ra_uint), float("%f" % dec_int), float("%f" % mtime)) #Enviando de nuevo las coordenadas a Stellarium self.act_pos(coords.hourStr_2_rad(sra), coords.degStr_2_rad(sdec)) ## Método que actualiza en Stellarium la posición del visor en pantalla. # # Recibe las coordenadas y las vuelve a enviar hacia Stellarium a través del método 'move' de la clase. # # \param ra Ascensión recta. # \param dec Declinación. def act_pos(self, ra, dec): (ra_p, dec_p) = coords.rad_2_stellarium_protocol(ra, dec) times = 10 #Número de veces que Stellarium espera recibir las coordenadas //Absolutamente empírico... for i in range(times): self.move(ra_p, dec_p) ## Método que envía a Stellarium las coordenadas ecuatoriales para actualizar la posición del visor en pantalla. # # Recibe las coordenadas en formato float, las transforma al formato del protocolo y las envía al cliente (Stellarium). # Obtiene el timestamp de la medida a partir de la hora local. # # \param ra Ascensión recta. # \param dec Declinación. def move(self, ra, dec): msize = '0x1800' mtype = '0x0000' localtime = ConstBitStream(replace('int:64=%r' % time(), '.', '')) #print "move: (%d, %d)" % (ra, dec) sdata = ConstBitStream(msize) + ConstBitStream(mtype) sdata += ConstBitStream(intle=localtime.intle, length=64) + ConstBitStream(uintle=ra, length=32) sdata += ConstBitStream(intle=dec, length=32) + ConstBitStream(intle=0, length=32) self.buffer = sdata self.is_writable = True self.handle_write() ## Manejador del envío de datos a través del socket. def handle_write(self): self.send(self.buffer.bytes) self.is_writable = False ## \brief Implementación de la parte del servidor para el 'Stellarium Telescope Protocol'. # # Establece la ejecución de un hilo independiente para gestionar las peticiones de conexión desde el # cliente (programa Stellarium). # Por cada conexión, ejecuta un nuevo hilo que gestionará las comunicaciones (Telescope_Channel). class Telescope_Server(QtCore.QThread, asyncore.dispatcher): ## Constructor de la clase. # # \param port Puerto en el que se pondrá a la escucha. def __init__(self, port=10001): asyncore.dispatcher.__init__(self, None) QtCore.QThread.__init__(self, None) self.tel = None self.port = port ## Comienza la ejecución del hilo. # # Pone a la escucha el socket por el puerto indicado en un hilo de ejecución independiente. def run(self): logging.info(self.__class__.__name__+" disponible.") self.create_socket(socket.AF_INET, socket.SOCK_STREAM) self.set_reuse_addr() self.bind(('localhost', self.port)) self.listen(1) self.connected = False asyncore.loop() ## Manejador del evento de petición de conexión. # # Inicia un nuevo hilo con una instancia de Telescope_Channel, pasándole como parámetro el socket abierto. def handle_accept(self): self.conn, self.addr = self.accept() logging.debug('Conectado: %s', self.addr) self.connected = True self.tel = Telescope_Channel(self.conn) ## Cierra la conexión, si estuviera establecida. def close_socket(self): if self.connected: self.conn.close() #Inicia un "Telescope Server" if __name__ == '__main__': try: Server = Telescope_Server() Server.run() except KeyboardInterrupt: logging.debug("\nBye!") |
Heredamos de la clase QtCore.QThread porque más adelante este código (con algunas modificaciones…) se usará como módulo en una interfaz desarrollada en Qt. Se puede observar también que se usan algunas funciones auxiliares para los cambios en el formato de los datos, como coords.rad_2_stellarium_protocol o coords.hourStr_2_rad. Estas funciones se definen en un archivo a parte, en este caso “coords.py”, y se importan como módulo con “import coords”:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
#!/usr/bin/python # -*- coding: utf-8 -*- import math import re import logging from time import strftime, localtime from string import replace # \brief Biblioteca de funciones para la conversión de unidades. # # Contiene las funciones necesarias para los cálculos de conversión de unidades más comúnmente usados. ## De radianes a horas, con hasta seis decimales de precisión (float). # (rads * 180)/(15 * pi) # # \param rads Radianes. # \return Float con el número de horas equivalentes a los radianes recibidos como parámetros. def rad_2_hour(rads): h = round( (rads * 180)/(15 * math.pi), 6) if h > 24.0: h = h - 24.0 if h < 0.0: h = 24.0 + h return h ## De grados en texto a radianes (float). # d = DºM'S'' => D+(M/60)+(S/60^2) grados || D.dº # # \param d Grados en texto (DºM'S'' || D.dº). # \return Radianes en float. def degStr_2_rad(d): exp1 = re.compile('^-?[0-9]{,3}(º|ᵒ)[0-9]{,3}\'[0-9]{,3}([\']{2}|") |
Ponemos los dos archivos en el mismo directorio (telescope_server.py y coords.py), le damos permiso de ejecución (chmod +x telescope_server.py) y ejecutamos en consola:
./telescope_server.py telescope_server.py: run - INFO: Telescope_Server disponible.
Ya sólo quedaría conectarnos desde Stellarium, para lo que debemos configurar primero la conexión a un telescopio. Se puede acceder al menú correspondiente mediante las opciones:
Configuración > Complementos > Telescope Control > Configure Telescope
A continuación se deben indicar el modo de conexión, nombre, IP y Puerto, especificando los valores:
- Modo de conexión: External Software or a remote computer
- Nombre: Telescopio Virtual
- IP: localhost
- Puerto: 10001
Una vez conectado, y asumiendo que sólo tengamos un dispositivo configurado, para enviar las coordenadas desde Stellarium pulsaremos las teclas “Ctrl + 1” (el número 1 alude al primer dispositivo configurado), de modo que se mostrarán por consola las coordenadas recibidas (entre otros datos de depuración que pueden resultar de interés):
./telescope_server.py telescope_server.py: run - INFO: Telescope_Server disponible. telescope_server.py: handle_accept - DEBUG: Conectado: ('127.0.0.1', 37023) telescope_server.py: handle_read - DEBUG: Size: 20, Type: 0, Time: 1342975913635132, RA: 1932791525 (e50e3473), DEC: 17943536 (f0cb1101) coords.py: rad_2_stellarium_protocol - DEBUG: (horas, grados): (10.800277, 1.503900) -> (10h48m1.0s, 1º30'14'')
El código implementado devuelve de nuevo las coordenadas recibidas a Stellarium, de forma que éste mostrará el visor en el lugar correspondiente, como se ve en la siguiente captura:
Y bueno, partir de este punto, ya disponemos en nuestro programa de las coordenadas ecuatoriales de cualquier objeto celeste, además de una vía de comunicación con Stellarium.
En otro post usaremos estas coordenadas en la implementación de un método para calcular las coordenadas locales (horizontales) a partir de éstas, en Arduino.
Salud! y Saludos..
)
exp2 = re.compile(‘^-?[0-9]{,3}\.[0-9]{,6}(º|ᵒ)
Ponemos los dos archivos en el mismo directorio (telescope_server.py y coords.py), le damos permiso de ejecución (chmod +x telescope_server.py) y ejecutamos en consola:
1 |
Ya sólo quedaría conectarnos desde Stellarium, para lo que debemos configurar primero la conexión a un telescopio. Se puede acceder al menú correspondiente mediante las opciones:
1 |
A continuación se deben indicar el modo de conexión, nombre, IP y Puerto, especificando los valores:
- Modo de conexión: External Software or a remote computer
- Nombre: Telescopio Virtual
- IP: localhost
- Puerto: 10001
Una vez conectado, y asumiendo que sólo tengamos un dispositivo configurado, para enviar las coordenadas desde Stellarium pulsaremos las teclas “Ctrl + 1” (el número 1 alude al primer dispositivo configurado), de modo que se mostrarán por consola las coordenadas recibidas (entre otros datos de depuración que pueden resultar de interés):
1 |
El código implementado devuelve de nuevo las coordenadas recibidas a Stellarium, de forma que éste mostrará el visor en el lugar correspondiente, como se ve en la siguiente captura:
Y bueno, partir de este punto, ya disponemos en nuestro programa de las coordenadas ecuatoriales de cualquier objeto celeste, además de una vía de comunicación con Stellarium.
En otro post usaremos estas coordenadas en la implementación de un método para calcular las coordenadas locales (horizontales) a partir de éstas, en Arduino.
Salud! y Saludos..
Saludos JuanRa y feliz año Nuevo 2017:
Tu Proyecto me interesa y me gustaría saber si tienes una version para controlar la montura usando sólo (Sin Arduino) el Raspberry PI-3. Este tiene 4 USB y Wifi integrado.
Mi interes es poder controlar el telescopio usando Stellarium remotamente y sólo usar las cordenadas que envia Stellarium y dirigirlas a travez del USB-serial a la montura en formato “Celestron Protocol”.
Si me puedes ayudar te lo agradeceré.
Gracias,
Desde Puerto Rico.
Hector