Jump to content

  •  

* * * * *

A Build-Your-Own C-14 Focuser


Discuss this article in our forums

A Build-Your-Own C-14 Focuser

by Gary Redford (CN Member "redfordg")

 

I couldn't find an off the shelf EFocuser for my C14 (pre-2006), so I built my own which allows for Hand Held Focusing control and/or USB remote focusing. No mechanical modifications are required to the telescope.

The design is based on a stepper motor and an Arduino Nano, with a built in temp/humidity sensor. This was really an after-thought feature that I decided to use with a temp compensation algorithm. Turns out it works great! Once I do an initial focus, the temp compensation will calculate a delta focus which I can select in-between images to maintain focus all night. I love it! The design simply replaces the focuser knob with a GT2 pulley and stepper motor driven by a ULN2003 and a Arduino Nano. I control the Nano with either a hand controller or serial communication using a GUI app. Both can be used simultaneously and can be either powered by the USB or telescope power through the hand controller. My original intention was to remotely focus using imaging SW (such as SharpCap). However, I have found that the temp compensation algorithm is so accurate that I don't need to evaluate the images. I just do an initial Bahtinov focus and as the temperature changes the algorithm calculates the amount of microns that I need to move the primary.

Here's a quick video to show the focuser operation:
https://photos.app.goo.gl/CXNQt3y7djAW9hmR9
.

Start by downloading the complete technical data package (drawings, schematics, parts list, SW, analysis, etc.) from:
https://app.box.com/s/0bl9e066zu5mm7hkg2krq2kue0vda85r.

Complete instructions are included on the documentation. The TDP also includes a GUI installer for my app, and SW source code if you want to create your own. The software design document itself is displayed below.

I wanted to make this available free to my fellow Astronomy geeks. My only hope is that it will help someone. Shame to go through all of this and not share it.

 

 

 

101000 SOFTWARE DESIGN DOCUMENT

FOR C14 TELESCOPE E-FOCUSER

 

Revision A

May 27, 2021

 

 

 

 

 

 

 

 

 

 

 

 

 

 

Gary Redford

April 2021


Revision Page

 

Revision Letter         Date                        Description of Change

Revision (-)                Nov 20, 2020         Initial release

Revision A                May 27, 2021       Added interface section to SW paragraph and explanations on interrupt conditions; (Gary Redford)

1. Introduction and Scope

This project adjusts the telescope focus electronically using a stepper motor and laptop with a USB connection and/or a custom hand controller (HC).  It does not attempt to focus automatically by “closing the loop” using image algorithm techniques.  However, it will allow remote control using WIFI, computer sharing technology (Team View, etc.), and a GUI application present on the laptop that is accessible remotely while either viewing the telescope image or using other application image evaluation tools.  SharpCap, for instance, has a focusing tool set when a camera is used.

 

2. Referenced Documents

None

 

3. Overall System Design

The current manual focusing knob on the C14 telescope is to be replaced by an E-Focuser.  The E-Focuser is controlled by both 1) a manual HC (handheld control box) that has a joystick and positioning display and 2) a GUI application that resides on a laptop connected through a serial USB.  The system can be powered simultaneously with 12VDC telescope power through the HC and by a laptop USB (5VDC).  The Nano Every microcontroller will allow both power forms (VCC and USB) simultaneously.

 

The telescope considered for this design has a threaded rod with a nut that moves the primary mirror towards and away from the secondary mirror for focusing.  It was imperically determined that the total travel is 1.05” (26.727mm) with a total of 34 turns.  So, each turn moves the mirror 0.031” (0.786mm), or 786 microns/turn.  The total range of travel of the primary mirror (threaded rod) in microns is 26,727 mm (microns).

 

The HW mounted on the telescope is divided into two sub-assemblies: the mechanical base assembly (102400) and the electronics assembly (102500).  The mechanical base assembly consists of a stepper motor with a pulley and a mounting plate.  Both subassemblies, together with a spacer, modified pulley, and belt, are part of the Focuser Installation Assembly, 102000, which is mounted directly to the scope in place of the focus knob and bearing cover (see picture).

 

A 28BYJ-48 stepper motor with two GT2 toothed pulleys are used in this design.  The drive pulley on the stepper motor contains 20 teeth and the pulley that will attach to the telescope drive screw has 60 teeth.  So, there is a gear advantage of 3:1.  Additionally the stepper motor is geared.  Unfortunately, many reduction gears are used in the package which calculates out to an uneven number.  It is 63.68395:1.  So, multiplying this by 3, the total gear ratio between the motor and the focus drive screw is 191.05185:1.  The stepper has 32 steps per revolution.  This means that it takes 6113.6592 steps of the motor (32x191.05185) to turn the focuser drive screw one turn.  Therefore the focuser resolution is going to be 0.128579 microns (786/6113.6592) of mirror travel for each motor step, or 7.7773 steps/um.  This should give nice resolution without taking too much time to focus.

 

The electronics assembly consists of a cover, Arduino Nano Every micro-controller board, ULN2003 motor driver board, and a DHT22 temperature/humidity sensor.  The electronics are conveniently packaged into the top cover (see picture) which allows both HC and USB interfaces, and which stacks onto the lower cover housing the mechanical base assembly. 

The motor driver and motor can be powered by 5 VDC from the Nano.  They draw about 150 mA, which is below the max load of 500 mA that the Nano can provide through the USB.  However, 7-21 VDC power is provided across the HC cable when operating the HC.  The USB port will be accessible on the side of the cover.

 

A picture containing electronics

Description automatically generatedThe Arduino is controlled by both a laptop which is connected to its USB port and by an HC which has a cable that connects to its digital and analog pins.  The HC simply moves the mirror relative to the left and right joystick position and displays the “focus position (um)” value on the LCD display.  This can be reset to zero by pressing down on the joystick (switch).  The joystick is simply a potentiometer and gives variable control by how much it is moved.  Left moves the mirror in the negative direction (away from the secondary) and right in the positive direction. 

 

The HC is powered externally and when switched ”on” powers up the Arduino.  The HC is connected to the Arduino via an ethernet cable (see “102500 Electronic Assy” and “103000 Hand Controller Box Assy” drawings for electronic hook-ups and pin assignments).  Note that a Nano Every is recommended because some clones CANNOT BE POWERED SIMULATANEOUSLY BY THE USB AND VIN PIN.

 

4. Architectural Design

The System Block Diagram which shows the components and interfaces also serves to show the software architecture.  The focusing knob is removed from the telescope and replaced by a toothed mechanical pulley.  This pulley is driven by a belt and stepper motor with a matching toothed pulley.  The stepper motor is controlled by a ULN2003 Darlington chip array mounted on an off-the-shelf breakout board.  The motor controller interfaces to an Arduino Micro controller that provides the logic and interfaces.  With a camera configured telescope, the Arduino can be plugged into the telescope camera which contains a USB hub that services the main camera, a guide scope camera, and the focuser.  This is for convenience to keep the number of cables coming off the scope to a minimum.  The camera USB is then plugged into the laptop.  The laptop communicates serially with the Arduino and provides the focuser control logic.  Additionally, an HC can be plugged into the Arduino to provide manual focus control.

 

The software consists of two CSU modules:  the Arduino code and the GUI app that resides on the laptop.  There are three modes of operation.  1) The Arduino is operated by the HC with the USB disconnected. 2) The Arduino is operated with the GUI connected serially.  3) Both the HC and the GUI are connected and can operate the Arduino separately.

 

5.  SW Detailed Design

The SW shall consist of two CSC modules;  “101100 GUI Application Code” and “101200 Arduino Code”.  Each module and their interfaces are described below, followed by code samples.  These code samples are notional in nature based on feasibility prototyping and can be changed depending on integration requirements and languages/tools used.  Changes and the final CSC’s shall be recorded in this document.

 

The focuser shall be operated either by the GUI app or the manual hand controller (HC).  It shall also have the capability to be operated in tandem (both together), but it is not necessary to pass focus position back and forth between the GUI app and the HC since absolute positioning is all relative.  When operated from the GUI app the Arduino will receive power from the USB port (5VDC).  It in turn will power the motor controller and the DHT22 temp/humid sensor.  When operated by the HC the Arduino will receive power on the VIN pin (7 to 21VDC) which in turn will power not only the motor and sensor, but also the LCD display and joystick.  The two power forms can be present at the same time. 

 

The SW shall handle all of the following interrupt conditions:

1.       Electronic Assy only plugged into USB and controlled by GUI app.

2.       Electronic Assy only plugged into HC and powered from HC power input jack.

3.       Electronic Assy plugged into USB and while operating connected to HC with the switch “OFF”.

4.       Electronic Assy plugged into USB and while operating connected to HC with the switch “On”.

5.       Electronic Assy plugged into HC and while operating connected to USB.

During any of these interrupts the focuser shall recover and be controllable by either the HC or the GUI app.

 

5.1  10100 GUI Application Code

Shown is a notional visual studio GUI app form with explanations given in the table.  The GUI application shall provide all control functions of the focuser.  The temperature and humidity are displayed at the top as sent by the Arduino every 60 seconds.  The temperature is used in calculating focus changes due to changes in telescope thermal expansions.  Humidity is useful for knowing whether a dew shield is needed. 

 

The “Focus position (um)”, which in the case of the SCT, is the primary mirror position relative to the secondary mirror in microns.  A positive value moves the primary mirror closer to the secondary and a negative value is moving the mirror away from the secondary.  The positive direction has the effect of extending the focal position at the back end of the telescope.  The relative mirror position is changed by sending move values to the Arduino and is reflected in the “FOCUS POSITION (um)” box.  This value can be reset to zero with the reset button.

 

The “delta Focus (um)” box allows the user to enter values which moves the mirror +/- using the arrow buttons.  Values from 0 to 26,727 can be entered in the text field, although typically much smaller adjustments are needed.  A few hundred microns accommodate most temperature excursions and changes in filters or lenses/eyepieces.  Experience shows that adjustments less than about 5 microns are not noticeable in focus performance. Whenever the mirror is moved in the negative direction (pulling away from the secondary by the drive screw) it needs to move about 5-10 steps in the positive direction at the end of the move to remove backlash.  The application needs to check for a sign value and add one if needed so that the Arduino will know how to handle the serial string sent.

 

The “T Coef (um/C)” text box allows the user to enter a calibrated value for his telescope.  This is determined empirically by noticing the microns required to move for a given temperature change.  Based on the temperature input the suggested focus change given in the “delta Focus (um) box is calculated from (newTemp – oldTemp) * FCoef.  This can be sent by the user which sets the oldTemp = newTemp.

 

Four presets are given in which the user can enter values for filters, eyepieces, etc.  The values can either be +/-.  It is assumed that if no sign value is given that the string to be sent should be proceeded by a “+” sign.  Otherwise, it is a “-“ sign.

 


 

The notional form widgets and descriptions are given in the following table. 

Widget name

Widget Type

Labels

Initial Value

Value Range

Control Function

cmbox_port

Combo Box

COM PORT:

searches for available com ports and displays in pull down menu for user select.

btn_connect

Button

CONNECT

reads text file and connects to com port selected.

btn_disconnect

Button

DISCONNECT

writes values to text file and disconnects from com port.

tbox_Fcoefficient

Text Box

FOCUS COEF (mm/oC):

0

+/- XXX.X

displays text file value from user and used in calculating temp compensation move value.

tbox_temp

Text Box

temp (oC):

-10.0 to 140.0

displays temp value (xx.x) in degrees C sent every 60 seconds from Arduino.

tbox_humid

Text Box

humid (%RH):

0.0 to 99.0

displays humidity value (xx.x) in RH% sent every 60 seconds from Arduino.

tbox_Fposition

Text Box

FOCUS POSITION (mM)

0

-30,000 to +30,000

displays text file value and updates for each +/- move commands.

btn_Fpzero

Button

ZERO

zeros "focus position (um)" value.

tbox_Dfocus

Text Box

d focus (mm)

0.0

-30,000 to +30,000

Allows string value to be entered for change in focus (um).

btn_Dfneg

Button

-

Checks for "-" character.  If false appends "-" to string.  Sends DeltaFocus value to Arduino. 

btn_Dfpos

Button

+

Checks for "+" character.  If false appends "+" to string.  Sends DeltaFocus value to Arduino. 

tbox_FCmove

Text Box

TEMP COMPENSATION (mm):

0.0

+/- xxx

Computed value based on (T2 - T1) * TCoef.  Updates every 60 seconds.  Can be overwritten.  Checks for "+" character.  If false, appends "+" to string. 

btn_FCmove

Button

MOVE

Sends temperature compensation move value to Arduino.

tbox_P1name

Text Box

label

>12 characters

displays text file value from user.

tbox_P2name

Text Box

label

>12 characters

displays text file value from user.

tbox_P3name

Text Box

label

>12 characters

displays text file value from user.

tbox_P4name

Text Box

label

>12 char

displays text file value from user.

btn_P1neg

Button

-

moves "d focus valus (mm)" in "-" direction.

btn_P2neg

Button

-

moves "d focus valus (mm)" in "-" direction.

btn_P3neg

Button

-

moves "d focus valus (mm)" in "-" direction.

btn_P4neg

Button

-

moves "d focus valus (mm)" in "-" direction.

box_P1

Text Box

d focus values (mm)

-30,000 to +30,000

displays text file value from user and gives number of um to move for preset.

box_P2

Text Box

d focus values (mm)

-30,000 to +30,001

displays text file value from user and gives number of um to move for preset.

box_P3

Text Box

d focus values (mm)

-30,000 to +30,002

displays text file value from user and gives number of um to move for preset.

box_P4

Text Box

d focus values (mm)

-30,000 to +30,003

displays text file value from user and gives number of um to move for preset.

btn_P1pos

Button

+

moves "d focus valus (mm)" in "+" direction.

btn_P2pos

Button

+

moves "d focus valus (mm)" in "+" direction.

btn_P3pos

Button

+

moves "d focus valus (mm)" in "+" direction.

btn_P4pos

Button

+

moves "d focus valus (mm)" in "+" direction.

 

5.2  101200 Arduino Code

The Arduino code consists of three functions (voids); 1) capture, format, and send serial temp/humidity data across the USB, 2) capture serial motion commands and move the stepper motor accordingly, 3) capture HC motion commands and move the motor according to the joystick and update the display. 

 

Whenever the motor is moved in a negative direction the motor shall be moved an additional 10 steps, then reversed 10 steps to remove the backlash.  When the motor is moved by the HC (joystick) basic run commands shall be used.  However, whenever the motor is moved by the GUI to various positions, an acceleration “ramp up / down” run command shall be used to avoid sudden jerks on the focusing screw.  The Joystick has this already built in because of the “analog” motion of the thumb.

 

The temperature and humidity only need to be updated every 60 seconds.  Telescope temperature changes are slow, and this will ease the load on the processor.

 

Data handling across the USB will need to be “two way” and any conflicts will need to be accounted for.

 

5.3  CSU interfaces

The interface between the two CSU’s shall consist of the Arduino passing temperature and humidity data to the GUI every 60 seconds where it is displayed and updated in the form, and the GUI app passing +/-motor move strings to the Arduino.  The format used for feasibility prototyping is given below:

 

From

To

format

Comments

Arduino

GUI app

“xxx.x,xx.x”

Serial string, two parts (temperature & humidity) comma delimited.  The temperature data can include a negative sign (sign + data, XX.X).  The data and GUI display should only include one decimal precision for both the temperature and the humidity.  The temperature sign is displayed only if negative (“-“).

GUI app

Arduino

“xxxxxxx”

Serial string, the first character is the sign (+/-) which tells the direction the motor must move, followed by the amount it should move in microns.  The Arduino converts this to steps according to,

mm  x  7.7773 = steps

 

6. Requirements

The Telescope Focuser requirements are given in the table.  Shown are Functional and System requirements with verification methods.

 

 

The table below shows the decomposition and flow-down of system requirements to the Arduino and the GUI software modules.

System Requirements

Verif. Method

Arduino

GUI App

1.0 Focuser System Requirements

---

---

---

1.1 Design Requirements

---

---

---

1.1.1 Focus Resolution/Rate: The Focuser shall have a positional resolution of at least 1 um and with rates of 0-25 um/sec.

A/T

(T Direct) 1 um resolution
0-25 um/sec rates

(T Direct) 1 um resolution
0-25 um/sec rates

1.1.4. Power: The Focuser shall operate on USB power, or 5-12 vdc and less than 2.5 watts.  Additionally, the electronic design shall employ power saving modes wherever possible.

A

(T Direct) power saving

(T Direct) power saving

1.3 User's Interface

---

---

---

1.3.1.  User Interfaces:  The Focuser shall have two user interfaces; 1) handheld controller with positioning/ speed control and display, 2) computer app control across USB serial cable.

D

(T Direct) HC/GUI

(T Direct) HC/GUI

1.3.2. User Control Features:  The telescope control features for either UI shall consist of +/- positioning, relative position display, temperature / humidity display, micron change of focus / temperature coefficient and calculated focus change selection, and at least three preset focus change selections for convenience (filter/lens combinations).

D

(T Direct) HC/GUI, +/- motion, display, temp/RH, T algor, 3+ presets

(T Direct) HC/GUI, +/- motion, display, temp/RH, T algor, 3+ presets

 

Prediction of those requirements verified by analysis are given in the Model Summary Table:

 

The underlying analyses details for the predictions follow:

Resolution Analysis: (>1 um)

telescope shaft

 

 

26727

shaft length um

 

34

shaft turns

 

786.0882353

um / turn

motor and pulley steps/gear ratios

 

63.68395

motor gear ratio

 

3

pulley ratio

 

191.05185

total gear ratio

 

32

motor steps / rev

 

6113.6592

motor steps / turn

 

0.128579008

length um / step

 

7.777319295

Steps / um

 

0.128579008

resolution (um)

 

0.871420992

margin (um)

Torque Analysis: (>10Nm)

motor rated torque

 

 

821

gmf-cm

 

191.05

gear ratio multiplier

 

156853.5689

Total motor torque gmf-cm

 

0.000098

Nm / gmf-cm

 

15.4

motor torque (w/o friction) Nm

 

5.4

margin (Nm)

Power Analysis: (< 2.5W)

amp

w

Electronic Device

0.019

0.095

Arduino nano

0.0019

0.009

ULN2003 motor cntrlr

0.128

0.64

28BYJ-48 stepper motor

0.0025

0.0125

DHT22 Temp/Humid sensor

0.15

0.76

total (W)

 

1.74

Margin (W)

 

Physical Properties Model: (drive<125G / <500 cm^3)

 

 

Weight (grams)

 

 

Volume (cm^3)

 

125

weight margin (grams)

 

500

size margin (cm^3)

Temperature / Humidity Analysis: (T=0 to 45C/H=5 to 90%RH)

T Rating ©

H Rating (%)

Part

-40 to 85

0 to 100

Arduino nano

-40 to 85

5 to 95

ULN2003 motor cntrlr

-45 to 125

0 to 90

28BYJ-48 stepper motor

-40 to 125

0 to 100

DHT22 Temp/Humid sensor

0 to 70

5 to 95

PS2 Joystick

0 to 50

0 to 95

1602 LCD

0 to 50

5 to 90

Limited Operating Cond.

0 to 5

0 to 0

Margin

 

 

 

Arduino Code:

#include

#include

#include

#include

#include

#include

 

#define STEPS 32

#define IN1 8

#define IN2 9

#define IN3 10

#define IN4 11

#define joystick A0

 

Stepper JStep(STEPS, IN4, IN2, IN3, IN1);

AccelStepper stepper(AccelStepper::FULL4WIRE, 8, 10, 9, 11);

DHT dht(7, DHT22);

LiquidCrystal_I2C lcd(0x27, 16, 2);

 

float focusPos = 0.0;

int moveMicroAmt = 0;

int numSteps = 0;

int backlash = 10;

char moveDirection = '?';

 

bool went_neg = false;

bool move = false;

bool jstickMoving = false;

bool resetForLED = false;

bool busySent = false;

 

int SWPin = 6;

 

Thread DHTsender = Thread();

 

char float_str[8];

 

void( * resetFunc)(void) = 0;

 

void onSend() {

  float temp = dht.readTemperature();

  float humid = dht.readHumidity();

  Serial.print(temp, 1);

  Serial.print(",");

  Serial.println(humid, 1);

 

}

 

void flushIncoming() {

  while (Serial.available()) {

    Serial.read();

  }

}

 

int getmoveMicroAmt() {

  while(Serial.available()){

  int readch = Serial.read();

  if (readch > 0) {

    switch (readch) {

    case '\r':

      break;

 

    case '\t':

      break;

 

    case '\n':

      move = true;

      flushIncoming();

      break;

      //return moveMicroAmt;

 

    case '+':

      moveDirection = '+';

      break;

 

    case '-':

 

      moveDirection = '-';

 

      break;

 

    default:

      int num = (readch - 48) % 128;

      if(num >= 0 && num <= 9){

        (moveMicroAmt == 0) ? moveMicroAmt = num : moveMicroAmt = (moveMicroAmt * 10) + num;

      }

 

    }

  }

 

  //return 0;

}

}

 

void moveMotorWithGUI() {

    if(!busySent){

      Serial.print("!");

      busySent = true;

 

    }

  switch (moveDirection) {

  case '+':

    numSteps = moveMicroAmt * 7.7773;

    stepper.moveTo(stepper.currentPosition() + numSteps);

    while (stepper.distanceToGo() != 0) {

      stepper.run();

    }

    moveDirection = '?';

    break;

  case '-':

    numSteps = moveMicroAmt * 7.7773;

    stepper.moveTo(stepper.currentPosition() - numSteps - backlash);

    while (stepper.distanceToGo() != 0) {

      stepper.run();

    }

    stepper.moveTo(stepper.currentPosition() + backlash);

    while (stepper.distanceToGo() != 0) {

      stepper.run();

    }

    moveDirection = '?';

    break;

 

  default:

    moveDirection = '?';

    break;

  }

    if(busySent){

      Serial.print("#");

      busySent = false;

    }

  // flushIncoming();

  moveMicroAmt = 0;

  moveDirection = '?';

  move = false;

  digitalWrite(8, LOW);

  digitalWrite(9, LOW);

  digitalWrite(10, LOW);

  digitalWrite(11, LOW);

 

}

 

void moveMotorWithJoyStick() {

  delay(100);

  //analogRead(A4);

  int a4 = analogRead(A4);

  if (a4 < 800 || analogRead(A1) < 800) {

    resetForLED = true;

  }

 

  else if(resetForLED) {

    resetFunc();

    resetForLED = false;

  }

 

  else{

 

  int val = analogRead(joystick);

  if(val>=525 || val <=475){

      Serial.print("!");

      jstickMoving = true;

  }

 

  while (val >= 525) {

    int speed_ = map(val, 500, 1023, 5, 500);

    JStep.setSpeed(speed_);

    JStep.step(-1);

    val = analogRead(joystick);

    focusPos = focusPos + 0.128579;

 

  }

 

  while (val <= 475) {

    int speed_ = map(val, 500, 0, 5, 500);

    JStep.setSpeed(speed_);

    JStep.step(1);

    went_neg = true;

    val = analogRead(joystick);

    focusPos = focusPos - 0.128579;

  }

 

  if (went_neg) {

    JStep.step(-backlash);

    focusPos = focusPos + (0.128579 * backlash);

    went_neg = false;

  }

 

  if (jstickMoving) {

    Serial.print("#");

    jstickMoving = false;

    lcd.setCursor(3, 1);

    dtostrf(focusPos, 8, 1, float_str);

    lcd.print(float_str);

    digitalWrite(8, LOW);

    digitalWrite(9, LOW);

    digitalWrite(10, LOW);

    digitalWrite(11, LOW);

  }

 

}

}

 

void setup() {

  Serial.begin(9600);

  stepper.setMaxSpeed(500.0);

  stepper.setAcceleration(500.0);

  dht.begin();

  DHTsender.onRun(onSend);

  DHTsender.run();

  DHTsender.setInterval(60000);

  if(analogRead(A4) >= 800 && analogRead(A1) >=80){

  lcd.init();

  lcd.backlight();

  lcd.clear();

  lcd.setCursor(0, 0);

  lcd.print("DELTA FOCUS (um)");

  lcd.setCursor(3, 1);

  dtostrf(focusPos, 8, 1, float_str);

  lcd.print(float_str);

  pinMode(SWPin, INPUT_PULLUP);

}

 

}

 

void loop() {

  if (digitalRead(SWPin) == LOW) {

    resetFunc();

  }

  getmoveMicroAmt();

  if (move && moveMicroAmt > 0) moveMotorWithGUI();

  if (DHTsender.shouldRun()) DHTsender.run();

  moveMotorWithJoyStick();

}


 

Python GUI Code

 

do.py

import serial.tools.list_ports

import time

import serial

from PyQt5.QtCore import QObject, QThread, pyqtSignal

from Worker import Worker

from queue import Queue

import os.path

import re

 

def poll_for_ports(gui):

                gui.port_poll_thread = QThread()

                gui.port_poll_thread_worker = Worker(gui)

                gui.port_poll_thread_worker.moveToThread(gui.port_poll_thread)

                gui.port_poll_thread.started.connect(gui.port_poll_thread_worker.pollPorts)

                gui.port_poll_thread.start()

 

 

def handleconnect(gui, port):

                if(gui.port_connect_button.text() == 'CONNECT'):

                                try:

                                                gui.connection_thread = QThread()

                                                gui.connection_thread_worker = Worker(gui,port)

                                                gui.connection_thread_worker.moveToThread(gui.connection_thread)

                                                gui.connection_thread.started.connect(gui.connection_thread_worker.openport)

                                                gui.connection_thread_worker.finished.connect(gui.connection_thread.quit)

                                                gui.connection_thread_worker.finished.connect(gui.connection_thread_worker.deleteLater)

                                                gui.connection_thread.finished.connect(gui.connection_thread.deleteLater)

                                                gui.connection_thread.start()

                                except:

                                                return

 

                else:

                                try:

                                                gui.connection_thread_worker.closeport()

                                except:

                                                return

 

def moveMotor(gui, val):

   

                try:                         

                                if(not re.match(".*[0-9].*", val) or gui.disableMoveButtons or gui.port_connect_button.text() == 'CONNECT'):

                                                return

                                if(val[0]== '!'):

                                                gui.refTemp = None

                                                gui.temp_comp_text.setText("0")

 

                                                val = val[1:]

 

                                if(val[0] == val[1]):

 

                                                val = val[2:]

                                                val = '+' + val

                                if(not val[1].isnumeric()):

 

                                                val = val[2:]

                                                val = '-' + val

                               

                                               

                                updateFocusPos(gui,val)

                                gui.connection_thread_worker.moveMotor(val + '\n')

                               

                except Exception as e:

                                print(e)

                                return

 

def updateFocusPos(gui,val):

                if(gui.focus_pos_text.text()==''):

                                gui.focus_pos_text.setText("0")

                oldDelta = int(gui.focus_pos_text.text())

                amt = int(val[1:])

                newDelta = 0

                if(val[0] == '-'):

                                newDelta = oldDelta - amt

                if(val[0] == '+'):

                                newDelta = oldDelta + amt

                gui.focus_pos_text.setText(str(newDelta))

 

def loadFile(gui):

                try:

                                if(not os.path.isfile("focuser.txt")):

                                                f = open("focuser.txt", "a")

                                                f.write("FOCUS POS (µm):\n")

                                                f.write("DELTA FOCUS (µm):\n")

                                                f.write("FOCUS COEF (µm/°C):\n")

                                                f.write("PRESETS (µm):\n")

                                                f.write(" \n")

                                                f.write(" \n")

                                                f.write(" \n")

                                                f.write(" \n")

                                                f.write(" \n")

                                                f.write(" \n")

                                                f.write(" \n")

                                                f.write(" \n")

                                                f.close()

                                                return

 

                                f = open("focuser.txt", "r")

 

                                lines  = f.readlines()

 

                                fpos = re.sub('([^0-9\+\-]+)','',lines[0])

                                deltaf = re.sub('([^0-9\+\-]+)','',lines[1])

                                fcof = re.sub('([^0-9\+\-\.]+)','',lines[2])

                                tcomp = re.sub('([^0-9\+\-]+)','',lines[3])

 

                                p1 = lines[4].replace("\n","")

                                p1val = lines[5].replace("\n","")

                                p2 = lines[6].replace("\n","")

                                p2val = lines[7].replace("\n","")

                                p3 = lines[8].replace("\n","")

                                p3val = lines[9].replace("\n","")

                                p4 = lines[10].replace("\n","")

                                p4val = lines[11].replace("/n","")

                                gui.focus_pos_text.setText(fpos)

                                gui.delta_focus_text.setText(deltaf)

                                gui.coef_text.setText(fcof)

                                gui.pr1_file_text.setText(p1)

                                gui.pr1_delta_text.setText(re.sub('([^0-9\+\-]+)','',p1val))

                                gui.pr2_file_text.setText(p2)

                                gui.pr2_delta_text.setText(re.sub('([^0-9\+\-]+)','',p2val))

                                gui.pr3_file_text.setText(p3)

                                gui.pr3_delta_text.setText(re.sub('([^0-9\+\-]+)','',p3val))

                                gui.pr4_file_text.setText(p4)

                                gui.pr4_delta_text.setText(re.sub('([^0-9\+\-]+)','',p4val))

                               

                                f.close()

                except Exception as e:

                                print(e)

 

 

focuser.py

# -*- coding: utf-8 -*-

 

# Form implementation generated from reading ui file 'focuser.ui'

#

# Created by: PyQt5 UI code generator 5.15.2

#

# WARNING: Any manual changes made to this file will be lost when pyuic5 is

# run again.  Do not edit this file unless you know what you are doing.

 

from PyQt5 import QtCore, QtGui, QtWidgets             

import do

class Ui_MainWindow(object):

   

    def closeEvent(self, event):

            print("closing...")

            try:

                # lines = open("focuser.txt","r").readlines()

                lines = []

 

                lines.append("FOCUS POS (µm):" + self.focus_pos_text.text() + "\n")

                lines.append("DELTA FOCUS (µm):" + self.delta_focus_text.text() + "\n")

                lines.append("FOCUS COEF (µm/°C):" + self.coef_text.text() + "\n")

                lines.append("PRESETS (µm):\n")

 

                lines.append(self.pr1_file_text.text() + "\n")

                lines.append(self.pr1_delta_text.text() + "\n")

                lines.append(self.pr2_file_text.text()  + "\n")

                lines.append(self.pr2_delta_text.text() + "\n")

                lines.append(self.pr3_file_text.text()  + "\n")

                lines.append(self.pr3_delta_text.text() + "\n")

                lines.append(self.pr4_file_text.text()  + "\n")

                lines.append(self.pr4_delta_text.text() + "\n")

               

                out = open("focuser.txt", "w")

                # for i in range(11):

                #     out.write(lines[i])

                out.writelines(lines)

                out.close()

            except Exception as e:

                print(e)

            event.accept()

 

    def setupUi(self, MainWindow):

        Mainwindow.closeEvent = self.closeEvent

        Mainwindow.setObjectName("MainWindow")

        # Mainwindow.resize(466, 631)

        Mainwindow.setStyleSheet(

           """

           QPushButton{

              padding-left: 1px;

              padding-right:1px;

              padding-top: 3px;

              padding-bottom: 3px;

             

              color: orange;

              background-color: rgb(55,55,55);

           }

 

           QComboBox {

              background-color: rgb(55,55,55);

              selection-background-color: rgb(55,55,55);

              selection-color: orange;

              color: orange; 

           }

           QListView {

              color: orange;

              background-color: rgb(55,55,55);

           }

           QLineEdit {

              qproperty-alignment: AlignCenter;

           }

 

           *{

              color: orange;

              background-color: black;

              font: bold;

           }

           

           """)

        self.refTemp = None

       

        self.numPorts = 0

 

        self.disableMoveButtons = False

 

        Mainwindow.setIconSize(QtCore.QSize(24, 24))

 

 

 

        self.centralwidget = QtWidgets.QWidget(MainWindow)

        self.centralwidget.setObjectName("centralwidget")

        self.centralwidget.setLayout(QtWidgets.QVBoxLayout())

       

 

        self.title = QtWidgets.QLabel()

        self.title.setObjectName("title")

        self.title.setStyleSheet("""

          qproperty-alignment: AlignCenter;

          """)

        self.centralwidget.layout().addWidget(self.title,2)

        self.centralwidget.layout().addSpacing(20)

 

        self.port_connect_label = QtWidgets.QLabel()

        self.port_connect_label.setObjectName("port_connect_label")

 

        self.port_connect_combo = QtWidgets.QComboBox()

        self.port_connect_combo.setObjectName("port_connect_combo")

 

        self.port_connect_button = QtWidgets.QPushButton()

        self.port_connect_button.setObjectName("port_connect_button")

        self.port_connect_button.clicked.connect(lambda: do.handleconnect(self, self.port_connect_combo.currentText()))

 

        self.cpbox = QtWidgets.QHBoxLayout()

        self.cpbox.addStretch(3)

        self.cpbox.addWidget(self.port_connect_label,1)

        self.cpbox.addWidget(self.port_connect_combo,2)

        self.cpbox.addStretch(1)

        self.cpbox.addWidget(self.port_connect_button,2)

        self.cpbox.addStretch(3)

 

        self.centralwidget.layout().addLayout(self.cpbox,2)

        self.centralwidget.layout().addStretch(2)

 

       

 

        self.coef_label = QtWidgets.QLabel()

        self.coef_label.setObjectName("coef_label")

 

        rx = QtCore.QRegExp('-?[1-9][0-9]{0,2}(?:\.\d{0,1})?')

        validator = QtGui.QRegExpValidator(rx)

        self.coef_text = QtWidgets.QLineEdit()

        self.coef_text.setValidator(validator)

        self.coef_text.setObjectName("coef_text")

 

        self.fcbox = QtWidgets.QHBoxLayout()

        self.fcbox.addStretch(4)

        self.fcbox.addWidget(self.coef_label,1)

        self.fcbox.addWidget(self.coef_text,1)

        self.fcbox.addStretch(7)

 

        self.centralwidget.layout().addLayout(self.fcbox,2)

        self.centralwidget.layout().addStretch(2)

 

        self.temp_label = QtWidgets.QLabel()

        self.temp_label.setObjectName("temp_label")

 

        self.temp_text = QtWidgets.QLineEdit()

        self.temp_text.setObjectName("temp_text")

        self.temp_text.setReadOnly(True)

 

        self.humid_label = QtWidgets.QLabel()

        self.humid_label.setObjectName("humid_label")

 

        self.humid_text = QtWidgets.QLineEdit()

        self.humid_text.setObjectName("humid_text")

        self.humid_text.setReadOnly(True)

       

        self.thbox = QtWidgets.QHBoxLayout()

        self.thbox.addStretch(4)

        self.thbox.addWidget(self.temp_label,1)

        self.thbox.addWidget(self.temp_text,1)

        self.thbox.addStretch(1)

        self.thbox.addWidget(self.humid_label,1)

        self.thbox.addWidget(self.humid_text,1)

        self.thbox.addStretch(4)

 

        self.centralwidget.layout().addLayout(self.thbox,2)

        self.centralwidget.layout().addStretch(2)

       

        self.position_group = QtWidgets.QGroupBox()

        self.position_group.setObjectName("position_group")

        self.position_group.setLayout(QtWidgets.QVBoxLayout())

 

        self.focus_pos_text = QtWidgets.QLineEdit()

        self.focus_pos_text.setObjectName("focus_pos_text")

        self.focus_pos_text.setText("0");

        self.focus_pos_text.setReadOnly(True)

 

        self.zero_button = QtWidgets.QPushButton()

        self.zero_button.setObjectName("zero_button")

        self.zero_button.clicked.connect(lambda: self.focus_pos_text.setText("0"))

 

        self.zerobox = QtWidgets.QHBoxLayout()

        self.zerobox.addStretch(3)

        self.zerobox.addWidget(self.focus_pos_text,2)

        self.zerobox.addWidget(self.zero_button,2)

        self.zerobox.addStretch(3)

 

        self.delta_focus_label = QtWidgets.QLabel()

        self.delta_focus_label.setStyleSheet("""

          qproperty-alignment: AlignCenter;

        """)

        self.delta_focus_label.setObjectName("delta_focus_label")

 

        self.position_group.layout().addLayout(self.zerobox)

        self.position_group.layout().addWidget(self.delta_focus_label)

 

        self.minus_button = QtWidgets.QPushButton()

        self.minus_button.setObjectName("minus_button")

        self.minus_button.setStyleSheet("""

          font-size: 20px;

          padding-top: 0px;

          padding-bottom: 0px;

        """)

        self.minus_button.clicked.connect(lambda: do.moveMotor(self, '-' + self.delta_focus_text.text()))

 

        rx = QtCore.QRegExp("^-?([1-9][0-9]{0,3}|[1-2][0-9]{0,4}|30000)$")

        validator = QtGui.QRegExpValidator(rx)

        self.delta_focus_text = QtWidgets.QLineEdit()

        self.delta_focus_text.setValidator(validator)

        self.delta_focus_text.setObjectName("delta_focus_text")

 

        self.plus_button = QtWidgets.QPushButton()

        self.plus_button.setObjectName("plus_button")

        self.plus_button.setStyleSheet("""

          font-size: 20px;

          padding-top: 0px;

          padding-bottom: 0px;

        """)

        self.plus_button.clicked.connect(lambda: do.moveMotor(self, '+' + self.delta_focus_text.text()))

 

        self.dfbox = QtWidgets.QHBoxLayout()

        self.dfbox.addStretch(2)

        self.dfbox.addWidget(self.minus_button,1)

        self.dfbox.addWidget(self.delta_focus_text,2)

        self.dfbox.addWidget(self.plus_button,1)

        self.dfbox.addStretch(2)

        self.position_group.layout().addLayout(self.dfbox)

 

        self.centralwidget.layout().addWidget(self.position_group)

        self.centralwidget.layout().addStretch(2)

 

 

        self.preset_group = QtWidgets.QGroupBox()

        self.preset_group.setLayout(QtWidgets.QVBoxLayout())

        self.preset_group.setObjectName("preset_group")

 

        self.temp_comp_label = QtWidgets.QLabel()

        self.temp_comp_label.setObjectName("temp_comp_label")

 

        self.temp_comp_text = QtWidgets.QLineEdit()

        self.temp_comp_text.setObjectName("temp_comp_text")

        self.temp_comp_text.setReadOnly(True)

        self.temp_comp_text.setText("0")

 

        self.move_button = QtWidgets.QPushButton()

        self.move_button.setObjectName("move_button")

        self.move_button.clicked.connect(lambda: do.moveMotor(self, '!' + self.temp_comp_text.text()))

 

        self.tcbox = QtWidgets.QHBoxLayout()

        self.tcbox.addWidget(self.temp_comp_label,1)

        self.tcbox.addWidget(self.temp_comp_text,3)

        self.tcbox.addWidget(self.move_button,3)

 

        self.pr_file_texts_label = QtWidgets.QLabel()

        self.pr_file_texts_label.setObjectName("pr_file_texts_label")

        self.pr_file_texts_label.setStyleSheet("""

          qproperty-alignment: AlignCenter;

        """)

 

        self.pr_deltas_label = QtWidgets.QLabel()

        self.pr_deltas_label.setObjectName("pr_deltas_label")

        self.pr_deltas_label.setStyleSheet("""

          qproperty-alignment: AlignCenter;

        """)

 

        self.headersbox = QtWidgets.QHBoxLayout()

        self.headersbox.addWidget(self.pr_file_texts_label,1)

        self.headersbox.addWidget(self.pr_deltas_label,1)

 

        self.pr1_file_text = QtWidgets.QLineEdit()

        self.pr1_file_text.setObjectName("pr1_file_text")

 

        self.pr1_minus_button = QtWidgets.QPushButton()

        self.pr1_minus_button.setObjectName("pr1_minus_button")

        self.pr1_minus_button.clicked.connect(lambda: do.moveMotor(self, '-' + self.pr1_delta_text.text()))

 

        rx = QtCore.QRegExp("^-?([1-9][0-9]{0,3}|[1-2][0-9]{0,4}|30000)$")

        validator = QtGui.QRegExpValidator(rx)

        self.pr1_delta_text = QtWidgets.QLineEdit()

        self.pr1_delta_text.setValidator(validator)

        self.pr1_delta_text.setObjectName("pr1_delta_text")

 

        self.pr1_plus_button = QtWidgets.QPushButton()

        self.pr1_plus_button.setObjectName("pr1_plus_button")

        self.pr1_plus_button.clicked.connect(lambda: do.moveMotor(self, '+' + self.pr1_delta_text.text()))

 

        self.pr1box = QtWidgets.QHBoxLayout()

        self.pr1box.addWidget(self.pr1_file_text,5)

        self.pr1box.addWidget(self.pr1_minus_button,1)

        self.pr1box.addWidget(self.pr1_delta_text,2)

        self.pr1box.addWidget(self.pr1_plus_button,1)

 

        self.pr2_file_text = QtWidgets.QLineEdit()

        self.pr2_file_text.setObjectName("pr2_file_text")

 

        self.pr2_minus_button = QtWidgets.QPushButton()

        self.pr2_minus_button.setObjectName("pr2_minus_button")

        self.pr2_minus_button.clicked.connect(lambda: do.moveMotor(self, '-' + self.pr2_delta_text.text()))

 

        rx = QtCore.QRegExp("^-?([1-9][0-9]{0,3}|[1-2][0-9]{0,4}|30000)$")

        validator = QtGui.QRegExpValidator(rx)

        self.pr2_delta_text = QtWidgets.QLineEdit()

        self.pr2_delta_text.setValidator(validator)

        self.pr2_delta_text.setObjectName("pr2_delta_text")

 

        self.pr2_plus_button = QtWidgets.QPushButton()

        self.pr2_plus_button.setObjectName("pr2_plus_button")

        self.pr2_plus_button.clicked.connect(lambda: do.moveMotor(self, '+' + self.pr2_delta_text.text()))

 

        self.pr2box = QtWidgets.QHBoxLayout()

        self.pr2box.addWidget(self.pr2_file_text,5)

        self.pr2box.addWidget(self.pr2_minus_button,1)

        self.pr2box.addWidget(self.pr2_delta_text,2)

        self.pr2box.addWidget(self.pr2_plus_button,1)

 

        self.pr3_file_text = QtWidgets.QLineEdit()

        self.pr3_file_text.setObjectName("pr3_file_text")

 

        self.pr3_minus_button = QtWidgets.QPushButton()

        self.pr3_minus_button.setObjectName("pr3_minus_button")

        self.pr3_minus_button.clicked.connect(lambda: do.moveMotor(self, '-' + self.pr3_delta_text.text()))

 

        rx = QtCore.QRegExp("^-?([1-9][0-9]{0,3}|[1-2][0-9]{0,4}|30000)$")

        validator = QtGui.QRegExpValidator(rx)

        self.pr3_delta_text = QtWidgets.QLineEdit()

        self.pr3_delta_text.setValidator(validator)

        self.pr3_delta_text.setObjectName("pr3_delta_text")

 

        self.pr3_plus_button = QtWidgets.QPushButton()

        self.pr3_plus_button.setObjectName("pr3_plus_button")

        self.pr3_plus_button.clicked.connect(lambda: do.moveMotor(self, '+' + self.pr3_delta_text.text()))

 

        self.pr3box = QtWidgets.QHBoxLayout()

        self.pr3box.addWidget(self.pr3_file_text,5)

        self.pr3box.addWidget(self.pr3_minus_button,1)

        self.pr3box.addWidget(self.pr3_delta_text,2)

        self.pr3box.addWidget(self.pr3_plus_button,1)

 

       

        self.pr4_file_text = QtWidgets.QLineEdit()

        self.pr4_file_text.setObjectName("pr4_file_text")

 

        self.pr4_minus_button = QtWidgets.QPushButton()

        self.pr4_minus_button.setObjectName("pr4_minus_button")

        self.pr4_minus_button.clicked.connect(lambda: do.moveMotor(self, '-' + self.pr4_delta_text.text()))

 

        rx = QtCore.QRegExp("^-?([1-9][0-9]{0,3}|[1-2][0-9]{0,4}|30000)$")

        validator = QtGui.QRegExpValidator(rx)

        self.pr4_delta_text = QtWidgets.QLineEdit()

        self.pr4_delta_text.setValidator(validator)

        self.pr4_delta_text.setObjectName("pr4_delta_text")

 

        self.pr4_plus_button = QtWidgets.QPushButton()

        self.pr4_plus_button.setObjectName("pr4_plus_button")

        self.pr4_plus_button.clicked.connect(lambda: do.moveMotor(self, '+' + self.pr4_delta_text.text()))

 

        self.pr4box = QtWidgets.QHBoxLayout()

        self.pr4box.addWidget(self.pr4_file_text,5)

        self.pr4box.addWidget(self.pr4_minus_button,1)

        self.pr4box.addWidget(self.pr4_delta_text,2)

        self.pr4box.addWidget(self.pr4_plus_button,1)

 

        self.preset_group.layout().addLayout(self.tcbox)

        self.preset_group.layout().addLayout(self.headersbox)

        self.preset_group.layout().addLayout(self.pr1box)

        self.preset_group.layout().addLayout(self.pr2box)

        self.preset_group.layout().addLayout(self.pr3box)

        self.preset_group.layout().addLayout(self.pr4box)

 

        self.centralwidget.layout().addWidget(self.preset_group,2)

 

       

 

        Mainwindow.setCentralWidget(self.centralwidget)

        self.statusbar = QtWidgets.QStatusBar(MainWindow)

        self.statusbar.setObjectName("statusbar")

        Mainwindow.setStatusBar(self.statusbar)

 

       

 

        self.retranslateUi(MainWindow)

        QtCore.QMetaObject.connectSlotsByName(MainWindow)

 

        do.poll_for_ports(self)

        do.loadFile(self)

 

    def retranslateUi(self, MainWindow):

        _translate = QtCore.QCoreApplication.translate

        Mainwindow.setWindowTitle(_translate("MainWindow", "focuser 3.1"))

        self.title.setText(_translate("MainWindow", "TELESCOPE FOCUS CONTROL"))

        self.port_connect_button.setText(_translate("MainWindow", "CONNECT"))

        self.port_connect_label.setText(_translate("MainWindow", "COM PORT:"))

        self.coef_label.setText(_translate("MainWindow", "FOCUS COEF (µm/°C):"))

        self.temp_label.setText(_translate("MainWindow", "temp (°C):"))

        self.humid_label.setText(_translate("MainWindow", "humid (%RH):"))

        self.position_group.setTitle(_translate("MainWindow", "FOCUS POSITION (µm)"))

        self.zero_button.setText(_translate("MainWindow", "ZERO"))

        self.delta_focus_label.setText(_translate("MainWindow", "δ focus (µm)"))

        self.plus_button.setText(_translate("MainWindow", "+"))

        self.minus_button.setText(_translate("MainWindow", "-"))

        self.preset_group.setTitle(_translate("MainWindow", "PRESETS"))

        self.pr_file_texts_label.setText(_translate("MainWindow", "labels"))

        self.temp_comp_label.setText(_translate("MainWindow", "TEMP COMPENSATION (µm):"))

        self.pr3_plus_button.setText(_translate("MainWindow", "IN"))

        self.move_button.setText(_translate("MainWindow", "MOVE"))

        self.pr2_minus_button.setText(_translate("MainWindow", "OUT"))

        self.pr1_plus_button.setText(_translate("MainWindow", "IN"))

        self.pr_deltas_label.setText(_translate("MainWindow", "δ focus values (µm)"))

        self.pr3_minus_button.setText(_translate("MainWindow", "OUT"))

        self.pr4_minus_button.setText(_translate("MainWindow", "OUT"))

        self.pr2_plus_button.setText(_translate("MainWindow", "IN"))

        self.pr4_plus_button.setText(_translate("MainWindow", "IN"))

        self.pr1_minus_button.setText(_translate("MainWindow", "OUT"))

 

 

 

if __name__ == "__main__":

    import sys

    import os

    os.environ["QT_AUTO_SCREEN_SCALE_FACTOR"] = "1"

    QtWidgets.QApplication.setAttribute(QtCore.Qt.AA_EnableHighDpiScaling,True)

    QtWidgets.QApplication.setAttribute(QtCore.Qt.AA_UseHighDpiPixmaps, True)

    app = QtWidgets.QApplication(sys.argv)

   

 

    MainWindow = QtWidgets.QMainWindow()

    ui = Ui_MainWindow()

    ui.setupUi(MainWindow)

    Mainwindow.show()

    sys.exit(app.exec_())

 

 

 

 

Worker.py

import time

import serial

from PyQt5.QtCore import QObject, QThread, pyqtSignal

from queue import Queue

import datetime

class Worker(QObject):

    finished = pyqtSignal()

    move = pyqtSignal(str)

    def __init__(self, gui, port = None):

        super(Worker, self).__init__()

        self.gui = gui

        self.ser = None

        self.port = port

      

    def openport(self):

        try:

            self.ser = serial.Serial(

                port=self.port,

                baudrate=9600,

                parity=serial.PARITY_ODD,

                stopbits=serial.STOPBITS_TWO,

                bytesize=serial.SEVENBITS

            )

            data = ''

            self.gui.port_connect_button.setText('DISCONNECT')

            while self.ser.is_open:

                if self.ser.in_waiting > 0:

                    char = self.ser.read(1).decode()

                    if(char == '#'):

                                print("ready")

                                self.gui.disableMoveButtons = False

                    elif(char == '!'):

                                print("busy")

                                self.gui.disableMoveButtons = True

                    elif(char !='\n'):

                         data += char

                    else:

                        print(data)

                        self.updateGUI(data)

                        data = ''

                       

        except:

            self.writeDataToFile()

            self.closeport()

            return

           

 

    def closeport(self):

        try:

            if(self.ser != None):

                if(self.ser.is_open):              

                    self.ser.close()

            self.gui.port_connect_button.setText('CONNECT')

            self.finished.emit()

           

        except:

            return

       

 

    def pollPorts(self):

        while True:

                try:

                            ports = list(serial.tools.list_ports.comports())

                            if(self.gui.port_connect_combo.count() == 0):

                               self.gui.port_connect_combo.addItem('NONE')

 

                            if(len(ports) != self.gui.numPorts):

                                self.gui.port_connect_combo.clear()

                                self.gui.numPorts = len(ports)

                                if(len(ports)==0):

                                    self.gui.port_connect_combo.addItem('NONE')

                                else:

                                    for port in ports:

                                        self.gui.port_connect_combo.addItem(str(port.device))

                        except:

                                print("Exception in poll ports")

 

 

    def moveMotor(self, val):

        try:

            if(self.ser.is_open):

                print('cmd sent: {0}'.format(val))

                self.ser.write(val.encode())

        except:

            return

   

 

    def updateGUI(self,data):

 

                try:

                                temp = data.split(',')[0]

                                humid = data.split(',')[1]

                                if(self.gui.refTemp == None):

                                                self.gui.refTemp = float(temp)

                                deltaT = float(temp) - float(self.gui.refTemp)

                                coef = self.gui.coef_text.text()

                                if(coef != ""):

                                                tcomp = round(float(deltaT) * float(coef))

                                                if(tcomp > 0):

                                                                tcomp = "+" + str(tcomp)

                                                else:

                                                                tcomp = str(tcomp)

                                                self.gui.temp_comp_text.setText(tcomp)

                                self.gui.temp_text.setText(temp)

                                self.gui.humid_text.setText(humid)

 

                except Exception as e:

                                # print(e)

                                return

 

 

    def  writeTempHumidToFile(self,data):

        try:

            data = data[:-1]

            f = open("focuser.txt","a")

            f.write(data+"  "+str(datetime.datetime.now())+"\n")

            f.close()

        except:

            print("error in write")

 

    def writeDataToFile(self):

                try:

            lines = open("focuser.txt","r").readlines()

            lines[0] = "FOCUS POS (µm):" + self.gui.focus_pos_text.text() + "\n"

            lines[1] = "DELTA FOCUS (µm):" + self.gui.delta_focus_text.text() + "\n"

            lines[2] = "FOCUS COEF (µm/°C):" + self.gui.coef_text.text() + "\n"

            lines[4] = self.gui.pr1_file_text.text()  + "\n"

            lines[5] = self.gui.pr1_delta_text.text()  + "\n"

            lines[6] = self.gui.pr2_file_text.text()  + "\n"

            lines[7] = self.gui.pr2_delta_text.text()  + "\n"

            lines[8] = self.gui.pr3_file_text.text()  + "\n"

            lines[9] = self.gui.pr3_delta_text.text()  + "\n"

            lines[10] = self.gui.pr4_file_text.text()  + "\n"

            lines[11] = self.gui.pr4_delta_text.text()  + "\n"

                               

            out = open("focuser.txt", "w")

            out.writelines(lines)

            out.close()

                except Exception as e:

                                print(e)


  • TDan55, gfstallin, RSJ and 2 others like this


22 Comments

Use github for the code, plus the code wasn't formatted here properly so like includes are not properly displayed.

 

For PyQt use QtDesigner to have an UI file to cut down all those lines of widget creation.

    • deepanshu29, kevinlowen and Tribe_Of_Dan like this
Photo
galacticinsomnia
Oct 01 2021 07:54 PM

PERFECT !  Nice work !! 

Thank you for your contributions to the community this is awesome and flexible..

Clear Skies !!

Hi Gary, very nice work, well organized and professionally done, thank you!

Could you comment on how the mechanical base assembly would have to be resized for a smaller scope, say a C8?,

thanks again!

SteveS

    • kevinlowen likes this
Photo
Aaron Small
Oct 04 2021 07:11 PM

Great job, Gary.  Looks like your hardware could be 3d printed.  Have any files to share?  How about your aluminum bracket?  Wiring diagram for your HC?  Other than time, how much do you think you have in materials?  Just trying to compare to other solutions.

Photo
Tribe_Of_Dan
Oct 05 2021 08:34 AM

Great work!

Use github for the code, plus the code wasn't formatted here properly so like includes are not properly displayed.

 

For PyQt use QtDesigner to have an UI file to cut down all those lines of widget creation.

You are correct.  SW is not one of my strong suits and there are probably better / easier ways to do the code.  I just "brut forced" it.  So, if I can do it, I'm sure that many of you can do a more elegant job!

Great job, Gary.  Looks like your hardware could be 3d printed.  Have any files to share?  How about your aluminum bracket?  Wiring diagram for your HC?  Other than time, how much do you think you have in materials?  Just trying to compare to other solutions.

Hi Aaron,

 

Look at the link for the technical data package.  The folder contains all of the engineering documentation, including wiring diagrams / tables (look at the the specific assembly drawings) and the complete parts list.  I also have included STL files for the 3D printed covers.  I don't recommend a printed baseplate, unless redesigned.  I think that there is too much angular torque from the offset pulleys.  Al can handle that nicely.

Hi Gary, very nice work, well organized and professionally done, thank you!

Could you comment on how the mechanical base assembly would have to be resized for a smaller scope, say a C8?,

thanks again!

SteveS

Hi Steve, Gave my C8 away to my kids and don't have it to look at to specifically answer your question.  I think that the fundamental engineering is the same.  You would just need to look at the interface.

Use github for the code, plus the code wasn't formatted here properly so like includes are not properly displayed.

 

For PyQt use QtDesigner to have an UI file to cut down all those lines of widget creation.

You are correct.  SW is not one of my strong suits and there are probably better / easier ways to do the code.  I just "brut forced" it.  So, if I can do it, I'm sure that many of you can do a more elegant job!

I have a 11 EdgeHD and I wish I had known about this before 2020. I currently use a Celestron focus motor which is nice, but your solution is nifty.

 

Hope your very detailed instructions are used often.

Photo
AstroPotamus
Oct 09 2021 03:17 PM

This is fantastic work!  I haven't downloaded everything but you've obviously got an engineering background and I'll assume it's all as rock solid as the documentation you provided in this post!  While I don't have a C14, your project has inspired me to do something similar for the 4SE that I do own, and maybe make one for a friend with a 6SE.

Great job on a very worthwhile project!

Photo
Sylvain Weiller
Oct 18 2021 09:12 AM

Hi and thanks !

 

Some precisions please ...

 

I found that the stepper motor 28BYJ-48 has a double-D 5 mm shaft.
Then GT2 must have a 5 mm bore.
From the pictures, I guess the width of the teeth should be 6 mm ?
For the pulley on the telescope drive screw what is the bore size as looking larger ?

 

Best,

Sylvain

Photo
Sylvain Weiller
Oct 18 2021 11:19 AM

Hi again,

As I don't have a C14 but interested for another scope another question : How / where is the stepper motor fixed on the rear of the scope ?

Thanks

Photo
munkacsymj
Yesterday, 11:44 AM

Hi, Gary,

Thanks for this design package! I just built one of these for my classic C14 (no aluminum, all PLA, except for the pulleys/belt). I'm finding that it has insufficient torque to turn the focuser knob in the direction to raise the mirror when the telescope is pointed at the zenith. Your analysis shows that you have almost 50% torque margin, but I wasn't able to figure out where your required torque value came from. Is the 10Nm requirement something you measured, or something you estimated?

You are correct, the GT2 pulley bore matches the stepper gear shaft of 5mm.  The teeth width are a little larger than 6mm to allow a 6mm belt.  I believe that the pitch is 2mm.  See the GT2 spec for design details.  Also, you can look at the parts that I used on the parts list.  The telescope screw bore is 0.75 inch.  See the modified part drawing for the 60T GT2 pulley.

Look at the assembly drawings to see how the stepper is mounted.

I actually measured the torque with the scope pointed at zenith.  I couldn't find a spec from Celestron.  I realize that the stepper is small, but with the gear ratio I haven't had a problem through hot and cold for almost a year.  Whenever I install the hyperstar the focuser needs to move quite a bit, about 36,000 microns (pushes the mirror ~3.6 cm).  Seems to not loose steps in both directions, when I install and remove, and lands pretty close to focus.  You might try lubing the lead screw.  Use a grease, like silicon, that doesn't outgas.  Also, I had Dean at Starizona, go through the scope and clean the primary.  In the process he made sure that the shaft that the primary slips on was lubed.  As you can see in the documentation, this is moly, which has excellent friction characteristics over temp.  Are you sure that the plastic parts don't distort alignment under load?

 

The only issue I had is that I needed to open up the clearance a little on the plastic cover.  The Tucson heat is brutal on plastic parts and distorted it slightly.  I had to cut the first cover off after a couple of weeks.  The 2nd edition works fine though.

Photo
darkstar3d
Today, 07:25 AM

This is nice! Thanks for sharing!

Photo
Sylvain Weiller
Today, 10:12 AM

Thanks for replies !

Will look 

Best regards,

Sylvain



Cloudy Nights LLC
Cloudy Nights Sponsor: Astronomics