ioptron-for-python 0.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,4 @@
1
+ __version__ = "0.2.0"
2
+ __author__ = "Matheus J. Castro"
3
+
4
+ from ioptron_for_python.controller import Ioptron
@@ -0,0 +1 @@
1
+ from ioptron_for_python.controller import Ioptron
@@ -0,0 +1,655 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ | iOptron Driver for Linux
4
+ | Matheus J. Castro
5
+
6
+ | This is the main file. aaaHere you find the main classes.
7
+ """
8
+
9
+ import serial
10
+ import time
11
+
12
+
13
+ class MountConnectionError(Exception):
14
+ """
15
+ Class to raise Mount Connection Errors
16
+ """
17
+
18
+ pass
19
+
20
+
21
+ class WrongCommand(Exception):
22
+ """
23
+ Class to raise Wrong Commands Errors
24
+ """
25
+
26
+ pass
27
+
28
+
29
+ class IoptronDevices:
30
+ """
31
+ Define all RS-232-enabled iOptron devices and their code-tags.
32
+ """
33
+
34
+ devices_ref = {
35
+ "0010": "Cube_II_Cube_Pro_EQ_mode",
36
+ "0011": "SmartEQ_Pro_Plus",
37
+ "0025": "CEM25_CEM25P",
38
+ "0026": "CEM25-EC",
39
+ "0030": "iEQ30_Pro",
40
+ "0040": "CEM40",
41
+ "0041": "CEM40-EC",
42
+ "0045": "iEQ45_Pro_EQ_mode",
43
+ "0046": "iEQ45_Pro_AA_mode",
44
+ "0060": "CEM60",
45
+ "0061": "CEM60-EC",
46
+ "5010": "Cube_II_Cube_Pro_AA_mode",
47
+ "5035": "AZ_Mount_Pro"
48
+ }
49
+
50
+
51
+ class IoptronCommands:
52
+ """
53
+ | Define all iOptron RS-232 Command Language commands.
54
+ | - iOptron Mount RS-232 Command Language 2014, Version 2.5 from Jan. 15th 2019.
55
+ """
56
+
57
+ # PositionStatus
58
+ longitude_latitude_status = "GLS"
59
+ """Get longitude, latitude & status."""
60
+ local_time_info = "GLT"
61
+ """Get local time/time zone info."""
62
+ declination_right_ascension = "GEC"
63
+ """Get current declination & right ascension."""
64
+ altitude_azimuth = "GAC"
65
+ """Get altitude & azimuth."""
66
+ parking_position = "GPC"
67
+ """Get parking position alt/az."""
68
+ max_slew_rate = "GSR"
69
+ """Get maximum slewing speed."""
70
+ altitude_limit = "GAL"
71
+ """Get altitude limit (tracking & slewing)."""
72
+ guiding_rate = "AG"
73
+ """Get guiding rate (RA & DEC)."""
74
+ meridian_treatment = "GMT"
75
+ """Get meridian treatment mode/limit."""
76
+
77
+ # Change Settings
78
+ @staticmethod
79
+ def tracking_rate(rate: int) -> str:
80
+ """
81
+ Set the tracking rate.
82
+
83
+ :param rate: an int (n) between 0 and 4.
84
+ :return: RTn.
85
+ """
86
+
87
+ if type(rate) is not int or rate not in range(0, 5):
88
+ raise WrongCommand("'tracking_rate' must be an int between 0 and 4.")
89
+ return f"RT{rate}"
90
+
91
+ @staticmethod
92
+ def moving_rate(rate: int) -> str:
93
+ """
94
+ Set the moving rate.
95
+
96
+ :param rate: an int (n) between 1 and 9.
97
+ :return: SRn.
98
+ """
99
+
100
+ if type(rate) is not int or rate not in range(1, 10):
101
+ raise WrongCommand("'moving_rate' must be an int between 1 and 9.")
102
+ return f"SR{rate}"
103
+
104
+ @staticmethod
105
+ def utc_offset(minutes: int) -> str:
106
+ """
107
+ Set the UTC offset in minutes.
108
+
109
+ :param minutes: an int (sMMM) between -720 and 780.
110
+ :return: SGsMMM.
111
+ """
112
+
113
+ if type(minutes) is not int or not (-720 <= minutes <= 780):
114
+ raise WrongCommand("'utc_offset' must be an int between -720 and +780 minutes.")
115
+ sign = "+" if minutes >= 0 else "-"
116
+ return f"SG{sign}{abs(minutes):03d}"
117
+
118
+ @staticmethod
119
+ def daylight_saving(enabled: int) -> str:
120
+ """
121
+ Enable or disable the Daylight Saving Time.
122
+
123
+ :param enabled: 0 or 1 (n).
124
+ :return: SDSn.
125
+ """
126
+
127
+ if enabled not in (0, 1):
128
+ raise WrongCommand("'daylight_saving' must be 0 or 1.")
129
+ return f"SDS{enabled}"
130
+
131
+ @staticmethod
132
+ def set_date(year: int, month: int, day: int) -> str:
133
+ """
134
+ Set local date.
135
+
136
+ :param year: an int between 0 and 99 (YY).
137
+ :param month: an int between 1 and 12 (MM).
138
+ :param day: an int between 1 and 31 (DD).
139
+ :return: SCYYMMDD.
140
+ """
141
+
142
+ if not (0 <= year <= 99):
143
+ raise WrongCommand("Year must be an int between 0 and 99.")
144
+ if not (1 <= month <= 12):
145
+ raise WrongCommand("Month must be an int between 1 and 12.")
146
+ if not (1 <= day <= 31):
147
+ raise WrongCommand("Day must be an int between 1 and 31.")
148
+ return f"SC{year:02d}{month:02d}{day:02d}"
149
+
150
+ @staticmethod
151
+ def set_time(hour: int, minute: int, second: int) -> str:
152
+ """
153
+ Set local time.
154
+
155
+ :param hour: an int between 0 and 23 (HH).
156
+ :param minute: an int between 0 and 59 (MM).
157
+ :param second: an int between 0 and 59 (SS).
158
+ :return: SLHHMMSS.
159
+ """
160
+
161
+ if not (0 <= hour <= 23):
162
+ raise WrongCommand("Hour must be an int between 0 and 23.")
163
+ if not (0 <= minute <= 59):
164
+ raise WrongCommand("Minute must be an int between 0 and 59.")
165
+ if not (0 <= second <= 59):
166
+ raise WrongCommand("Second must be an int between 0 and 59.")
167
+ return f"SL{hour:02d}{minute:02d}{second:02d}"
168
+
169
+ @staticmethod
170
+ def longitude(value: int) -> str:
171
+ """
172
+ Set the Longitude (arc-seconds).
173
+
174
+ :param value: an int (sSSSSSS) between -648000 and 648000.
175
+ :return: SgsSSSSSS.
176
+ """
177
+
178
+ if type(value) is not int or not (-648000 <= value <= 648000):
179
+ raise WrongCommand("'longitude' must be an int between -648000 and +648000 arcsec.")
180
+ sign = "+" if value >= 0 else "-"
181
+ return f"Sg{sign}{abs(value):06d}"
182
+
183
+ @staticmethod
184
+ def latitude(value: int) -> str:
185
+ """
186
+ Set the Latitude (arc-seconds).
187
+
188
+ :param value: an int (sSSSSSS) between -324000 and 324000.
189
+ :return: StsSSSSSS.
190
+ """
191
+
192
+ if type(value) is not int or not (-324000 <= value <= 324000):
193
+ raise WrongCommand("'latitude' must be an int between -324000 and +324000 arcsec.")
194
+ sign = "+" if value >= 0 else "-"
195
+ return f"St{sign}{abs(value):06d}"
196
+
197
+ @staticmethod
198
+ def hemisphere(value: int) -> str:
199
+ """
200
+ Set the Hemisphere.
201
+
202
+ :param value: 0 for south or 1 for north (n).
203
+ :return: SHEn.
204
+ """
205
+
206
+ if value not in (0, 1):
207
+ raise WrongCommand("'hemisphere' must be 0 (South) or 1 (North).")
208
+ return f"SHE{value}"
209
+
210
+ @staticmethod
211
+ def set_max_slew_rate(rate: int) -> str:
212
+ """
213
+ Set the Maximum slewing speed.
214
+
215
+ :param rate: 7, 8 or 9 (n).
216
+ :return: MSRn.
217
+ """
218
+
219
+ if rate not in (7, 8, 9):
220
+ raise WrongCommand("'max_slew_rate' must be 7, 8, or 9.")
221
+ return f"MSR{rate}"
222
+
223
+ @staticmethod
224
+ def set_altitude_limit(value: int) -> str:
225
+ """
226
+ Set the Altitude limit.
227
+
228
+ :param value: an int (snn) between -89 and 89.
229
+ :return: SALsnn.
230
+ """
231
+
232
+ if type(value) is not int or not (-89 <= value <= 89):
233
+ raise WrongCommand("'altitude_limit' must be between -89 and +89 degrees.")
234
+ sign = "+" if value >= 0 else "-"
235
+ return f"SAL{sign}{abs(value):02d}"
236
+
237
+ @staticmethod
238
+ def set_guiding_rate(ra: int, dec: int) -> str:
239
+ """
240
+ Set the Guiding rate.
241
+
242
+ :param ra: an int (nn) between 1 and 90 (0.01-0.90).
243
+ :param def: an int (nn) between 10 and 99 (0.10-0.99).
244
+ :return: RGnnnn.
245
+ """
246
+
247
+ if not (1 <= ra <= 90):
248
+ raise WrongCommand("RA guiding rate must be between 1 and 90 (0.01-0.90).")
249
+ if not (10 <= dec <= 99):
250
+ raise WrongCommand("DEC guiding rate must be between 10 and 99 (0.10-0.99).")
251
+ return f"RG{ra:02d}{dec:02d}"
252
+
253
+ @staticmethod
254
+ def set_meridian_treatment(mode: int, degrees: int) -> str:
255
+ """
256
+ Set the Meridian treatment.
257
+
258
+ :param mode: 0 to stop or 1 to flip (n).
259
+ :param degrees: an int (nn) between 0 and 99.
260
+ :return: SMTnnn.
261
+ """
262
+
263
+ if mode not in (0, 1):
264
+ raise WrongCommand("Mode must be 0 (stop) or 1 (flip).")
265
+ if not (0 <= degrees <= 99):
266
+ raise WrongCommand("Degrees past meridian must be between 0 and 99.")
267
+ return f"SMT{mode}{degrees:02d}"
268
+
269
+ # Mount Motion
270
+ slew = "MS"
271
+ """Slew to the most recently defined coordinates."""
272
+ stop_slew = "Q"
273
+ """Stop slewing only."""
274
+ park = "MP1"
275
+ """Park mount to the most recently defined parking position."""
276
+ unpark = "MP0"
277
+ """Unpark mount."""
278
+ go_home = "MH"
279
+ """Slew immediately to zero/home position."""
280
+ search_home = "MSH"
281
+ """Automatically search the mechanical home position using sensors."""
282
+ stop_motion = "q"
283
+ """Stop movement from arrow commands (:mn, :me, :ms, :mw)."""
284
+ stop_lr = "qR"
285
+ """Stop left/right movement."""
286
+ stop_ud = "qD"
287
+ """Stop up/down movement."""
288
+
289
+ move_north = "mn"
290
+ """Continuous motion (like pressing arrow keys) to north until stop_motion is called."""
291
+ move_east = "me"
292
+ """Continuous motion (like pressing arrow keys) to east until stop_motion is called."""
293
+ move_south = "ms"
294
+ """Continuous motion (like pressing arrow keys) to south until stop_motion is called."""
295
+ move_west = "mw"
296
+ """Continuous motion (like pressing arrow keys) to west until stop_motion is called."""
297
+
298
+ @staticmethod
299
+ def tracking(enabled: int) -> str:
300
+ """
301
+ Enable or disable tracking state.
302
+
303
+ :param enabled: 0 to stop or 1 to start tracking (n).
304
+ :return: STn.
305
+ """
306
+
307
+ if enabled not in (0, 1):
308
+ raise WrongCommand("'tracking' must be 0 (stop) or 1 (start).")
309
+ return f"ST{enabled}"
310
+
311
+ @staticmethod
312
+ def guide_north(ms: int) -> str:
313
+ """
314
+ Guide north for specified milliseconds.
315
+
316
+ :param ms: an int (XXXXX) between 0 and 99999.
317
+ :return: MnXXXXX.
318
+ """
319
+
320
+ if type(ms) is not int or not (0 <= ms <= 99999):
321
+ raise WrongCommand("'guide_north' must be between 0 and 99999 ms.")
322
+ return f"Mn{ms:05d}"
323
+
324
+ @staticmethod
325
+ def guide_east(ms: int) -> str:
326
+ """
327
+ Guide east for specified milliseconds.
328
+
329
+ :param ms: an int (XXXXX) between 0 and 99999.
330
+ :return: MeXXXXX.
331
+ """
332
+
333
+ if type(ms) is not int or not (0 <= ms <= 99999):
334
+ raise WrongCommand("'guide_east' must be between 0 and 99999 ms.")
335
+ return f"Me{ms:05d}"
336
+
337
+ @staticmethod
338
+ def guide_south(ms: int) -> str:
339
+ """
340
+ Guide south for specified milliseconds.
341
+
342
+ :param ms: an int (XXXXX) between 0 and 99999.
343
+ :return: MsXXXXX.
344
+ """
345
+
346
+ if type(ms) is not int or not (0 <= ms <= 99999):
347
+ raise WrongCommand("'guide_south' must be between 0 and 99999 ms.")
348
+ return f"Ms{ms:05d}"
349
+
350
+ @staticmethod
351
+ def guide_west(ms: int) -> str:
352
+ """
353
+ Guide west for specified milliseconds.
354
+
355
+ :param ms: an int (XXXXX) between 0 and 99999.
356
+ :return: MwXXXXX.
357
+ """
358
+
359
+ if type(ms) is not int or not (0 <= ms <= 99999):
360
+ raise WrongCommand("'guide_west' must be between 0 and 99999 ms.")
361
+ return f"Mw{ms:05d}"
362
+
363
+ @staticmethod
364
+ def custom_ra_rate(rate: float) -> str:
365
+ """
366
+ Set the Custom RA tracking rate (n.nnnn * sidereal rate).
367
+
368
+ :param rate: a float (nnnnn) between 0.5000 and 1.5000.
369
+ :return: RRnnnnn.
370
+ """
371
+
372
+ if not (0.5 <= rate <= 1.5):
373
+ raise WrongCommand("'custom_ra_rate' must be between 0.5000 and 1.5000.")
374
+ value = int(rate * 10000)
375
+ return f"RR{value:05d}"
376
+
377
+ # Position
378
+ calibrate_mount = "CM"
379
+ """Synchronize / calibrate mount."""
380
+ set_zero = "SZP"
381
+ """Set current position as zero position."""
382
+
383
+ @staticmethod
384
+ def set_ra(value: str) -> str:
385
+ """
386
+ Set Right Ascension.
387
+
388
+ :param value: an 8 character string (XXXXXXXX).
389
+ :return: SrXXXXXXXX.
390
+ """
391
+
392
+ if type(value) is not str or len(value) != 8:
393
+ raise WrongCommand("'set_ra' requires an 8 character string.")
394
+ return f"Sr{value}"
395
+
396
+ @staticmethod
397
+ def set_dec(value: str) -> str:
398
+ """
399
+ Set Declination.
400
+
401
+ :param value: an 8 character string (TTTTTTTT).
402
+ :return: SdsTTTTTTTT.
403
+ """
404
+
405
+ if type(value) is not str or len(value) != 8:
406
+ raise WrongCommand("'set_dec' requires an 8 character string.")
407
+ return f"Sds{value}"
408
+
409
+ @staticmethod
410
+ def set_altitude(value: str) -> str:
411
+ """
412
+ Set Altitude.
413
+
414
+ :param value: an 8 character string (TTTTTTTT).
415
+ :return: SasTTTTTTTT.
416
+ """
417
+
418
+ if type(value) is not str or len(value) != 8:
419
+ raise WrongCommand("'set_altitude' requires an 8 character string.")
420
+ return f"Sas{value}"
421
+
422
+ @staticmethod
423
+ def set_azimuth(value: str) -> str:
424
+ """
425
+ Set Azimuth.
426
+
427
+ :param value: a 9 character string (TTTTTTTTT).
428
+ :return: SzTTTTTTTT.
429
+ """
430
+
431
+ if type(value) is not str or len(value) != 9:
432
+ raise WrongCommand("'set_azimuth' requires a 9 character string.")
433
+ return f"Sz{value}"
434
+
435
+ @staticmethod
436
+ def set_parking_azimuth(value: str) -> str:
437
+ """
438
+ Set parking azimuth.
439
+
440
+ :param value: a 9 character string (TTTTTTTTT).
441
+ :return: SPATTTTTTTTT.
442
+ """
443
+
444
+ if type(value) is not str or len(value) != 9:
445
+ raise WrongCommand("'set_parking_azimuth' requires a 9 character string.")
446
+ return f"SPA{value}"
447
+
448
+ @staticmethod
449
+ def set_parking_altitude(value: str) -> str:
450
+ """
451
+ Set parking altitude.
452
+
453
+ :param value: an 8 character string (TTTTTTTT).
454
+ :return: SPHTTTTTTTT.
455
+ """
456
+
457
+ if type(value) is not str or len(value) != 8:
458
+ raise WrongCommand("'set_parking_altitude' requires an 8 character string.")
459
+ return f"SPH{value}"
460
+
461
+ # Misc
462
+ firmware_main_and_hc_date = "FW1"
463
+ """Get firmware date: mainboard & hand controller"""
464
+ firmware_motor_date = "FW2"
465
+ """Get firmware date: RA & Dec motor boards"""
466
+ mount_model = "MountInfo"
467
+ """Gets the mount model number"""
468
+
469
+
470
+ class IoptronCall:
471
+ """
472
+ Wrapper class to create a command call for RS-232 with the command.
473
+
474
+ :param main_instance: The main class.
475
+ :param commands: The class where the commands are translated.
476
+ :param fast: True: no output returned; False: reads the output from the mount. Default: False.
477
+ """
478
+
479
+ def __init__(self, main_instance, commands, fast=False):
480
+ """
481
+ Initialize the class with the outer instances and
482
+ where to return or not the output of the mount.
483
+ """
484
+
485
+ if fast:
486
+ self.send = main_instance.fast_send_cmd
487
+ else:
488
+ self.send = main_instance.send_cmd
489
+
490
+ self.commands = commands
491
+
492
+ def __getattr__(self, name):
493
+ """
494
+ Fallback from calling a non existing function in this class, and it will look up
495
+ in the commands class.
496
+
497
+ :param name: Function name.
498
+ """
499
+
500
+ def call(*args):
501
+ return self.send(cmd(*args))
502
+
503
+ cmd = getattr(self.commands, name)
504
+ if callable(cmd):
505
+ return lambda *values: call(*values)
506
+ else:
507
+ return self.send(cmd)
508
+
509
+
510
+ class Ioptron(IoptronDevices, IoptronCommands):
511
+ """
512
+ Main class to bridge iOptron RS-232 Command Language with python calls
513
+
514
+ :param IoptronDevices: Device list class.
515
+ :param IoptronCommands: Command list class.
516
+ :param port: The RS-232 port where the device is connected.
517
+ """
518
+
519
+ def __init__(self, port):
520
+ """
521
+ Initialize variables and define functions.
522
+ """
523
+
524
+ self.port = port
525
+ self.baudrate = 115200
526
+ self.bytesize = 8
527
+ self.parity = "N"
528
+ self.stopbits = 1
529
+ self.serial_timeout = 2
530
+
531
+ self.device = None
532
+
533
+ self.device_num = None
534
+ self.device_name = None
535
+
536
+ self.exec = IoptronCall(self, IoptronCommands, fast=True)
537
+ self.exec.read = IoptronCall(self, IoptronCommands)
538
+
539
+
540
+ def init_serial(self, timedate=True):
541
+ """
542
+ Initialize the serial connection.
543
+
544
+ :param timedate: If True, it will update the mount with the current computer date and time.
545
+ :return: 1 if successful.
546
+ """
547
+
548
+ self.device = serial.Serial(
549
+ port=self.port,
550
+ baudrate=self.baudrate,
551
+ bytesize=self.bytesize,
552
+ parity=self.parity,
553
+ stopbits=self.stopbits,
554
+ timeout=self.serial_timeout,
555
+ )
556
+
557
+ self.device_num = self.send_cmd(self.mount_model)
558
+
559
+ if self.device_num is None or self.device_num == "":
560
+ raise MountConnectionError("Device did not respond.")
561
+ else:
562
+ self.device_name = self.devices_ref.get(self.device_num, "Unknown device.")
563
+
564
+ if timedate:
565
+ self.set_current_timedate()
566
+
567
+ return 1
568
+
569
+
570
+
571
+ def health_check(self):
572
+ """
573
+ Check if the mount is still connected. Raises an *MountConnectionError* error if not.
574
+
575
+ :return: 1 if successful.
576
+ """
577
+
578
+ if self.device_num != self.send_cmd(self.mount_model):
579
+ raise MountConnectionError("Health check failed.")
580
+ else:
581
+ return 1
582
+
583
+ def close_serial(self):
584
+ """
585
+ Closes the serial connection.
586
+
587
+ :return: 1 if successful.
588
+ """
589
+
590
+ self.device.close()
591
+ return 1
592
+
593
+ def send_cmd(self, data):
594
+ """
595
+ Send a string to the serial device and **read** its output. It encodes and decodes the string.
596
+
597
+ :param data: The unencoded string.
598
+ :return: The decoded output without *#*.
599
+ """
600
+
601
+ data = self.format_command(data)
602
+ self.device.write(data.encode())
603
+ time.sleep(0.01)
604
+ line = self.device.read_until(b"#")
605
+ return line.decode().strip("#")
606
+
607
+ def fast_send_cmd(self, data):
608
+ """
609
+ Send a string to the serial **without** reading its output. It encodes the string.
610
+
611
+ :param data: The unencoded string.
612
+ :return: 1 after completion.
613
+ """
614
+
615
+ data = self.format_command(data)
616
+ self.device.write(data.encode())
617
+ return 1
618
+
619
+ @staticmethod
620
+ def format_command(data):
621
+ """
622
+ Format the input command and check for errors. It also **adds** *:* at the beginning
623
+ and *#* at the end of the command string.
624
+
625
+ :param data: The unformatted command.
626
+ :return: The formatted command.
627
+ """
628
+
629
+ if type(data) is not str:
630
+ raise TypeError("Command should be a string.")
631
+ elif " " in data:
632
+ raise ValueError("Command should not have spaces.")
633
+ return ":" + data + "#"
634
+
635
+ def set_current_timedate(self):
636
+ """
637
+ Send the commands to update the date and time of the mount with the current computer time.
638
+ """
639
+
640
+ curr_time = time.localtime()
641
+
642
+ # UTC offset em min
643
+ if curr_time.tm_isdst and time.daylight:
644
+ utc_offset_minutes = -time.altzone // 60
645
+ else:
646
+ utc_offset_minutes = -time.timezone // 60
647
+
648
+ status = []
649
+ status.append(self.exec.read.set_date(curr_time.tm_year-2000, curr_time.tm_mon, curr_time.tm_mday) == "1")
650
+ status.append(self.exec.read.set_time(curr_time.tm_hour, curr_time.tm_min, curr_time.tm_sec) == "1")
651
+ status.append(self.exec.read.utc_offset(utc_offset_minutes) == "1")
652
+ status.append(self.exec.read.daylight_saving(0) == "1")
653
+
654
+ if not all(status):
655
+ raise MountConnectionError("Error while setting the current time and date.")