
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.
The 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 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 | (T Direct) 1 um resolution |
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
24 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.
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
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.
Great work!
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!
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 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.
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.
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!
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
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
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.
This is nice! Thanks for sharing!
Thanks for replies !
Will look
Best regards,
Sylvain
Hi Gary,
This is superb! I am working on focuser my self, the original plan was to connect to my Accufocus, but I now think I will do it with the 28BYJ-48 stepper motor and ULN2003 that came with my Android Kit.
When you thought about controlling it with Sharpcap, the idea was to build an ASCOM driver for it?
Thank you for sharing,
Daniel
Hi Daniel,
My original thought was to use the efocuser and the tools in Sharpcap "open loop". I would move with the focuser and determine best focus using the Sharpcap tools. I originally had no intention of connecting the two in SW. However, I found that by using the temp compensation routine I can maintain focus all night without rechecking the image with Sharpcap, or any other focus measuring tools. All I do is use a Bahtinov mask for an initial focus and then adjust when the temp compensation tells me that I am out of focus greater than about 5 microns. I didn't want my efocuser to automatically adjust focus because I might be in the middle of a frame integration. Any motion of the focusing knob not only moves the mirror in the Z axis, but also slightly moves it in angle (alpha/beta) causing a blur. So, I just monitor the temp comp calculation and click on "move" at my convenience. Works slick!