Meade 1209 Focuser Upgrade

Meade 1209 Zero Image Shift MicroFocuser.
Meade 1209 Zero Image Shift Microfocuser

I've had a Meade 1209 Zero Image Shift microfocuser for about three years. It works great, but doesn't provide the benefits an absolute position focuser would provide, such as repeatability and auto-focus capability. I found a geared stepper that was small and had enough torque. There is a Moonlite compatible driver module for Indi, so I built a focus controller using an Arduino Nano.

Meade 1209 Zero Image Shift Microfocuser main parts.
Meade 1209 Main Parts

The image shows the main parts of the focuser. The Crayford body with roller, DC gearmotor, and two external gears. The motor, its gear, and the motor mounting bracket were replaced by new ones. The brass (on my model) 48-tooth gear was re-used.

The Bracket

The part we need to fabricate.
The Old Motor Bracket

The aluminum bracket that holds the gearmotor and the roller assembly together. The way Meade made sure the gears aligned was to counter-bore the back side of the bracket's roller shaft hole with a ~10mm dia. spot face bit. That lets the bracket fit over a protruding lip on the roller body, aligning it. On the replacement I drilled the shaft hole 10mm in diameter to do the same thing.

The motor bracket
The New Motor Bracket

The new bracket is made from 1/8" x 1" flat aluminum bar 2" long. I didn't cut it to length until all of the holes were drilled. It is too small to be handled otherwise. I taped a full-sized drawing to the bar and center punched the hole locations through the drawing. Here is the full-size PDF of the bracket.

I drilled 2.5mm holes (2mm clearance) at each punch for the motor, and 3mm holes (#4 clearance) for each punch on the roller end of the bracket. Then I drilled countersinks for the screw holes, and finally a 10mm hole for the roller shaft and a 13mm hole for the stepper motor. The aluminum bar had a mill finish, so it needed a little filing to smooth it up.

The first attempt was a success. All of the holes lined up perfectly. I had to ream the 13mm hole a little with a hand reamer. The stepper center is exactly 13mm, so the hole needed to be a very little bit larger. The same was true for the 10mm hole. With punching, drilling, countersinking, filing, and cursing my cheap metric drill bits, I spent around 45 minutes on the bracket.

Motor

The motor and gear
The Motor and Gear

The stepper came from OMC-StepperOnline.com, and is the 8HS15-0604S-PG64 NEMA 8 stepper with a 64:1 planetary gear reducer, and a 6mm output shaft. Total focuser range is ~13mm, and it takes 11000 steps to get there, so each step is approximately 0.000044" (1.2 microns) at the focal plane. OMC claims backlash is "1° or less" in the gear motor. When I tested it, the gear backlash was approximately 117 steps, 210.6° at the motor shaft, 3.3° at the output shaft, or 0.0051" at the focal plane.

The drive gear is the same pitch diameter as the brass gear on the roller shaft, (module 0.5, 24mm pitch dia) but is made of acetal with a brass hub. It is the A 1Z 2MYZ0504806 from SDP-SI.com.

Assembled

The focuser with motor installed
The Motor Attached

The assembled focuser in the half-out position (5,500 steps). It easily lifts the 2.7 lb. weight of the camera and filter wheel and holds it in position with the motor powered off. The maximum speed is 2500 steps per second. It takes about 3 seconds to go from zero to the halfway point. Most moves are much smaller than that, and so are done at a lower speed, due to acceleration.

Controller

The focuser controller board
The Controller

The focuser's controller is an Arduino Nano driving an A4988 stepper controller board. The Nano has a USB port for the computer interface. I found code that looked like exactly what I wanted, but it had a few problems driving the stepper reliably, so I changed the stepper driver code to use AccelStepper. That caused a little refactoring because the temperature compensation was built into the motor code. You can find a page on the original code at Hansastro Focuser. There is nothing wrong with that code - I just couldn't tune it to my stepper motor, so I opted for something with which I was already familiar. I have the LM335 hooked up for temperature compensation, and when I get some numbers that describe the relationship between temperature and focus position, I'll try temperature compensation.

The program is burned with a programmer. That's because the Indi driver opens the port, which causes a reset on the Arduino, and then hits it right away. The Arduino takes a couple of seconds to get past the bootloader, so the driver gets a timeout. When you burn the program with the programmer (Shift-Upload), the bootloader is gone, and your program starts right out of reset.

I added EEPROM wear-leveling, backlash compensation, and an OLED display. The display shows the temperature in celsius, a flag showing whether temperature compensation is enabled (t or T), one showing step size (H or F), and the compensation coefficient on a small line at the top. The 5-digit position in steps in a larger font takes the rest of the display.

/**************************************************************************************************
 *
 * AccelFocuser
 *
 * Indi compatible Moonlite focuser lookalike using Meade 1209 and Arduino Nano.
 *
 * Significant portions of this code were taken from Focuser by hansastro.
 * https://hansastro.github.io/Focuser/
 *
 * It was modified to use the AccelStepper library, which necessitated moving the temp comp
 * into the main code.
 *
 **************************************************************************************************/

#include <Wire.h>
#include <LM335.h>
#include <Moonlite.h>
#include <AccelStepper.h>
#include <U8x8lib.h>
#include <WearLeveling.h>

// A4988 pins 
// organized to wire straight across to a Nano
const int enablePin = 9;
const int stepMode1 = 8;
const int stepMode2 = 7;
const int stepMode3 = 6;
const int resetPin = 5;
const int sleepPin = 4;
const int stepPin = 3;
const int directionPin = 2;

// LM335 temp sensor pin
const int temperatureSensorPin = A0;

// Objects for motor, display, etc.
LM335 TemperatureSensor(temperatureSensorPin);
AccelStepper Motor(AccelStepper::DRIVER, stepPin, directionPin);
Moonlite SerialProtocol;
U8X8_SSD1306_128X64_NONAME_HW_I2C u8x8(U8X8_PIN_NONE);

#define FULL_STEP 1
#define HALF_STEP 2
#define FULL_BACKLASH 117
#define HALF_BACKLASH 234

long currentPosition = 0;
long targetPosition = 0;
long stepMode = HALF_STEP;
bool enabled = false;
long setSpeed = 0x02;
unsigned long display_timer;
long eeprom_position;
long backlash_in = 0;
long backlash_out = 0;
bool going_out = false;
bool temperature_compensation = false;
unsigned long temperature_timer;
float last_comp_temperature; // Last temperature compensated.
float temperature_coefficient;  // Steps per degree C.

/*
 * Return the step mode (FULL_STEP or HALF_STEP).
 */
long getStepMode() {
    return stepMode;
}

/*
 * Set the step mode. Also sets acceleration and max speed.
 */
void setStepMode(uint8_t mode) {
    stepMode = mode;
    switch (mode) {
        case FULL_STEP:
            digitalWrite(stepMode1, LOW);
            digitalWrite(stepMode2, LOW);
            digitalWrite(stepMode3, LOW);
            Motor.setMaxSpeed(2500);
            Motor.setAcceleration(2000);
            backlash_in = backlash_out = FULL_BACKLASH;
            break;
        case HALF_STEP:
            digitalWrite(stepMode1, HIGH);
            digitalWrite(stepMode2, LOW);
            digitalWrite(stepMode3, LOW);
            Motor.setMaxSpeed(2000);
            Motor.setAcceleration(2000);
            backlash_in = backlash_out = HALF_BACKLASH;
            break;
        default:
            break;
    }
}

/*
 * Apply backlash compensation.
 */
void eat_backlash() {
    long cp = Motor.currentPosition();
 
    Motor.enableOutputs();
    if (going_out) {
        Motor.moveTo(cp + backlash_out);
        do {
            Motor.run();
        } while(Motor.distanceToGo());
    } else {
        // Can't move below zero.
        if (cp >= backlash_in) {
            Motor.moveTo(cp - backlash_in);
            do {
                Motor.run();
            } while(Motor.distanceToGo());
        }
    }
    Motor.setCurrentPosition(cp);
    Motor.disableOutputs();
}

/*
 * Setup the move and start it.
 */
void gotoTargetPosition() {
    if (currentPosition != targetPosition) {
        if (currentPosition > targetPosition) {
            if (going_out) {
                going_out = false;
                eat_backlash();
            }
        } else {
            if (!going_out) {
                going_out = true;
                eat_backlash();
            }
        }
        Motor.moveTo(targetPosition);
        Motor.enableOutputs();
        enabled = true;
    }
}

/*
 * Apply temperature compensation to the current position.
 * Updates every 30 seconds.
 */
void tempCompensate() {
    float curr_temp = TemperatureSensor.getTemperature();
    float diff_temp = last_comp_temperature-curr_temp;
    if (fabs(diff_temp) >= 0.5) {
        long new_position = currentPosition + (long)(temperature_coefficient * diff_temp);
        if (new_position > 0) {
            last_comp_temperature = curr_temp;
            targetPosition = new_position;
            gotoTargetPosition();
        }
    }
    temperature_timer = millis() + 30000;
}

/*
 * Process incoming and outgoing computer messages
 */
void processCommand() {
    MoonliteCommand_t command;
    long commandID = SerialProtocol.getCommand().commandID;

    switch (commandID) {
        case ML_C:
            // Initiate temperature conversion
            break;
        case ML_FG:
            // Goto target position
            gotoTargetPosition();
            break;
        case ML_FQ:
            // Stop motor movement and clear the move by resetting the
            // current and target positions to the current motor position
            Motor.stop();
            Motor.disableOutputs();
            enabled = false;
            targetPosition = currentPosition = Motor.currentPosition();
            Motor.moveTo(currentPosition);
            break;
        case ML_GB:
            // Set the Red Led backlight value
            // Dump value necessary to run the official moonlite software
            SerialProtocol.setAnswer(2, (long)0x00);
            break;
        case ML_GC:
            // Return the temperature coefficient
            SerialProtocol.setAnswer(2, (long)(temperature_coefficient * 2));
            break;
        case ML_GD:
            // Return the current motor speed. Returns the set speed,
            // not the actual speed. Compatibility only. We don't use it.
            SerialProtocol.setAnswer(2, (long)setSpeed);
            break;
        case ML_GH:
            // Return the current stepping mode (half or full step)
            // See ML_SH, ML_SF, below.
            SerialProtocol.setAnswer(2, (long) (getStepMode() == HALF_STEP ? 0xFF : 0x00));
            break;
        case ML_GI:
            // Get motor is moving
            SerialProtocol.setAnswer(2, (long) (Motor.isRunning() ? 0x01 : 0x00));
            break;
        case ML_GN:
            // Get the target position
            targetPosition = Motor.targetPosition();
            SerialProtocol.setAnswer(4, targetPosition);
            break;
        case ML_GP:
            // Return the current position
            currentPosition = Motor.currentPosition();
            SerialProtocol.setAnswer(4, currentPosition);
            break;
        case ML_GT:
            // Return the temperature
            SerialProtocol.setAnswer(4, (long)(TemperatureSensor.getTemperature()));
            break;
        case ML_GV:
            // Get the version of the firmware
            SerialProtocol.setAnswer(2, (long) (0x01));
            break;
        case ML_SC:
            // Set the temperature coefficient
            temperature_coefficient = (float)(SerialProtocol.getCommand().parameter / 2.0);
            break;
        case ML_SD:
            // Set the motor speed
            setSpeed = (int)SerialProtocol.getCommand().parameter;
            break;
        case ML_SF:
            // Set the stepping mode to full step
            setStepMode(FULL_STEP);
            break;
        case ML_SH:
            // Set the stepping mode to half step
            setStepMode(HALF_STEP);
            break;
        case ML_SN:
            // Set the target position if we aren't currently moving.
            if (!Motor.isRunning()) {
                targetPosition = SerialProtocol.getCommand().parameter;
                Motor.moveTo(targetPosition);
            }
            break;
        case ML_SP:
            // Set the current motor position, EEPROM, and target position.
            currentPosition = SerialProtocol.getCommand().parameter;
            targetPosition = currentPosition;
            Motor.setCurrentPosition(currentPosition);
            writeValue(0, currentPosition);
            eeprom_position = currentPosition;
            break;
        case ML_PLUS:
            // Activate temperature compensation focusing and start timer.
            temperature_compensation = true;
            last_comp_temperature = TemperatureSensor.getTemperature();
            temperature_timer = millis();
            break;
        case ML_MINUS:
            // Disable temperature compensation focusing
            temperature_compensation = false;
            break;
        case ML_PO:
            // Temperature calibration
            break;
        default:
            break;
    }
}

/*
 * Show some data on the display
 */
void display() {
    char tempbuf[17] = {0};
    float tempc = TemperatureSensor.getTemperature() / 2;
    dtostrf(tempc, 4, 2, tempbuf);
    tempbuf[8] = temperature_compensation?'T':'t';
    tempbuf[9] = stepMode==HALF_STEP?'H':'F';
    dtostrf(temperature_coefficient, 4, 1, tempbuf+11);
    for (int x=0; x < sizeof(tempbuf) - 1; ++x) {
        if (tempbuf[x] == '\0') tempbuf[x] = ' ';
    }
    u8x8.setFont(u8x8_font_amstrad_cpc_extended_f);
    u8x8.drawString(0, 1, tempbuf);
    u8x8.setFont(u8x8_font_inb33_3x6_n);
    u8x8.drawString(0, 2, u8x8_u16toa(Motor.currentPosition(), 5));
    display_timer = millis() + 1000;
}

void setup() {

    // Moonlite interface.
    SerialProtocol.init(9600);

    // Stepper interface.
    pinMode(stepMode1, OUTPUT);
    pinMode(stepMode2, OUTPUT);
    pinMode(stepMode3, OUTPUT);
    pinMode(sleepPin, OUTPUT);
    digitalWrite(sleepPin, HIGH);
    pinMode(resetPin, OUTPUT);
    digitalWrite(resetPin, HIGH);
    Motor.setEnablePin(enablePin);
    Motor.setPinsInverted(false, false, true);
    setStepMode(FULL_STEP);

    // Display.
    u8x8.begin();
    u8x8.setFont(u8x8_font_amstrad_cpc_extended_f);
    u8x8.clear();

    // EEPROM.
    loadEeprom(0);
    eeprom_position = getValue(0);
    Motor.setCurrentPosition(eeprom_position);
    currentPosition = eeprom_position;
    targetPosition = currentPosition;

    // LM335 and compensation.
    TemperatureSensor.setNumberOfIntegration(10000);
}

void loop() {

    // Motor shutdown housekeeping
    if (Motor.distanceToGo() == 0) {
        if (enabled) {
            enabled = false;
            delay(10);
            Motor.disableOutputs();
            long mtr_position = Motor.currentPosition();
            if (mtr_position != eeprom_position) {
                writeValue(0, mtr_position);
                eeprom_position = mtr_position;
            }
        }
        // Temperature compensation housekeeping
        TemperatureSensor.Manage();
        if (temperature_compensation) {
            if (millis() > temperature_timer) {
                tempCompensate();
            }
        }
        // Display housekeeping
        if (millis() > display_timer) {
            display();
        }
    } else {
        Motor.run();
    }
    // Interface housekeeping
    SerialProtocol.Manage();
    if (SerialProtocol.isNewCommandAvailable()) {
        processCommand();
    }
}

The Enclosure

The laser cut 1/8" acrylic enclosure. The bottom has a lip to which the top fastens. Four screws hold the enclosure to the focuser, and two screws fasten the two parts together. The signal cable plugs in the bottom of the enclosure.

I spent last night auto-focusing. It actually works. Pinpoint stars, like I get when I luck out and get it right, only no luck required. I spent about $120.00 on the upgrade, so instead of a $240.00 motorized focuser I have a $360.00 absolute position focuser.

Note: What else would I try?

This motor has a 64:1 gearbox. They also have a motor with a 90:1 gearbox. I would like to try that to see if it is smoother at lower speeds. I use between 50 and 75 steps per attempt when autofocusing, and it generates noise (vibration) at low speeds. I can't see the result of the vibration since the camera captures only in between moves, but I hope to have the temperature compensation in use sometime, and it will focus when it wants to - and not just in between exposures.

The backlash is proving to be problematic. When the telescope is pointed up, as telescopes often are, the weight of the imaging gear pulls the backlash out of the system, so there isn't really any backlash to speak of. I've set it to zero for the time being, hoping to determine if no compensation is better than the wrong compensation.