This document is about making the Stellarium software communicate with some Python script using the LX200 Serial Protocol. This protocol was made by Meade for their LX200 telescopes, and many others brands and people used that same protocol, making it a de-facto standard. This is not the only way Stellarium can communicate, as you can make it to communicate directly to computer program by TCP/IP or anothers serial protocols.

Oh by the way, if you're here you should already know what Stellarium is for, but if you're don't know, it's for rendering the sky and stars in real time, and more. And so, it can communicate with some telescopes for visualising where they are pointed to, and to GOTO them to a sky's position.

Some helpful links:
Complete documentation about the LX200 protocol and commands set
Link to Stellarium's LX200 plugin source code
Link to a very interesting instructables.com page

First, as it is a serial protocol and we're not running our Python code on a device, we need a couple of virtual serial ports. It's easy to do on GNU/Linux with the Socat command (you may need to apt-get install it before). If you're on Windows, you still can use VSPE.
$ socat -dd pty,raw,echo=0,link=/tmp/A pty,raw,echo=0,link=/tmp/B
2025/05/07 13:39:59 socat[2267928] N PTY is /dev/pts/16
2025/05/07 13:39:59 socat[2267928] N PTY is /dev/pts/17
2025/05/07 13:39:59 socat[2267928] N starting data transfer loop with FDs [5,5] and [7,7]
Thanks to the link options, there are reproducible endpoints symlinks pointing to the pts devices.
$ ls -l /tmp/A /tmp/B
lrwxrwxrwx 1 clx clx 11 May  7 13:39 /tmp/A -> /dev/pts/16
lrwxrwxrwx 1 clx clx 11 May  7 13:39 /tmp/B -> /dev/pts/17
So, in this example, we can configure /tmp/A on Stellarium and use /tmp/B in our code without the need to change that if in a later use the pts numbers have changed. If the command is terminated (with Ctrl+C for example), the bridge is destroyed, along with its two endpoints /tmp/A and /tmp/B.

[screen capture]
First, you need to activate the telescope control plugin. Press F2, then click on Plugins tab, select "Telescope Control" on the list, and click on "Load at startup". Then restart Stellarium.

[screen capture]
Once Stellarium restarted, click on the "configure button" of the previous screen, which is now activated. You can also do Ctrl+0 and click on "Configure telescopes". Then click on the "Add a new telescope" button (the one with a "+" symbol), and you get this dialog window. Here you can type one virtual serial port we made, and select device model "Meade LX200 (compatible)".

[screen capture]
Usage: press Alt+1 to GOTO to the center of the current view, or Ctrl+1 to GOTO to the selected object coordinates. Ctrl+0 brings up the "Slew telescope to" dialog.


Very basic Python example code:
from serial import Serial

class Lx200Responder:
	def __init__(self, serialDeviceName):
		self.ra = 0.0 # Right angle (azimut)
		self.dec = 0.0 # Declination

		serial = Serial(serialDeviceName)
		s = bytes()
		while True:
			c = serial.read(1)
			s+=c
			if c == b'#':
				if len(s) > 1 and s[0] == 0x3a:
					r = self.parse(s[1:-1].decode('latin1'))
					if r:
						serial.write(r.encode('latin1'))
					elif r == False:
						serial.write(0x15)
				s=bytes()

	def parse(self, s):
		if s == "GR": # Get Telescope RA
			return "%02d:%02d:%02d#" % v2hms(self.ra) # HH:MM:SS#

		if s == "GD": # Get Telescope Declination
			r = "%c%02dß%02d:%02d#" % v2sdms(self.dec) # sDD*MM'SS#
			return r

		if s.startswith("Sr"): # Set target object RA to HH:MM.T or HH:MM:SS depending on the current precision setting
			self.ra = hms2v(s[2:])
			print("Sr:", s[2:], self.ra)
			return '1' # accept

		if s.startswith("Sd"): # Set target object declination to sDD*MM or sDD*MM:SS depending on the current precision setting
			self.dec = dms2v(s[2:])
			print("Sd:", s[2:], self.dec)
			return '1' # accept

		if s == "Q": # Halt all current slewing (returns nothing)
			print("Halt move.")
			return

		if s == "MS": # Slew to Target Object
			print("Slew to", self.ra, self.dec)
			return '0' # says slew is possible

		if s == "CM": # Synchronizes the telescope's position with the currently selected database object's coordinates
			print("Software wants the sync...")
			return '1Sync OK#'

		print("Unknown command:", s)
		return False

def main():
	lx200Responder = Lx200Responder("/tmp/B")

# Now some misc helper functions...

def v2hms(v):
	h = int(v); v=60*(v-h)
	m = int(v); s=60*(v-m)
	return (h, m, round(s))

def hms2v(s):
	f = 1.0
	value = 0.0
	for v in s.split(':'):
		value+=float(v)*f
		f/=60.0
	return value

def v2sdms(v):
	if v<0: sign = '-'; v=-v
	else: sign = '+'
	d = int(v); v=60*(v-d)
	m = int(v); s=60*(v-m)
	return sign, d, m, s

def dms2v(s):
	sign = 1
	if s[0] == '-':
		s = s[1:]
		sign = -1

	value = 0.0
	f = 1
	for v in s.replace('ß', ':').split(':'):
		value+=float(v)*f
		f/=60.0
	return sign*value

main()