/* * Home-Brew Standalone Nunchuck controller for Celestron Focus Motor. * Copyright(c) Mark Lord . * This code is free for personal use/modification/whatever. * * The code works only for the 5V/16Mhz version of the Arduino "Pro Micro". * * Note that I have also hacked my personal copy of this file: * hardware/arduino/avr/cores/arduino/HardwareSerial.h * to set SERIAL_RX_BUFFER_SIZE=128 rather than the (too small!) default value. * * Pull-ups on RXI, TXO, BUSY, and CTS just in case. RJ12 pins 1,2,4,6. * +12V power is NOT connected from AUX to Arduino. * * Version 0.4: * -- Forgot to switch back to the real DEV_FOCUSER in v0.3. * Version 0.3: * -- Fix "nunchuck_detected=no" message so it actually appears. * -- Fix bug on "y" axis movements. * * Version 0.2: * -- Self-calibrate Nunchuck centre coordinates at startup, in case it's not (0,0). * -- Implement "Focus Presets" on the two Nunchuck buttons: * Press and hold a button for 2 seconds to save current position on that button. * Press/Release a button in under 2 seconds to "GO-TO" the saved position. * * Version 0.1 * -- Initial release. */ // The Nunchuck code was originally distilled from the ArduinoNunchuk library. #include #include #define VERSION "0.4" #define VERSION_DATE "2022-09-26" #define ENABLE_BUTTON_PRESETS true #define DEBUG true #define AUXBUS_TXQ_SIZE 8 // PIN definitions for this project: #define auxBus SERIAL_PORT_HARDWARE // Aka. "Serial" for "pro mini", and "Serial1" for "pro micro" #define AUXBUS_RX_PIN 0 // Corresponds to the receive pin on the Serial port #define AUXBUS_BUSY_PIN 4 // The RTS (aka. "Busy") line for the auxBus #define LED_PIN 17 // Pro Micro defines LED_BUILTIN as non-existent pin-13; use the RX LED instead! #define LED_ON LOW #define LED_OFF HIGH // Device IDs on the Celestron/Nexstar AUX bus: //#define DEV_FOCUSER 0x11 // Mount ALT motor: used when testing without a Focus Motor present. #define DEV_FOCUSER 0x12 // Celestron Focus Motor #define DEV_DUMMY 0x00 // #define DEV_NUNCHUCK 0xec // This controller // Motor controller movement commands: #define MC_GET_POS 0x01 #define MC_GOTO_FAST 0x02 #define MC_MOVE_POS 0x24 #define MC_MOVE_NEG 0x25 // Slow/Fast speeds used by this program. #define SLEW_SLOW 5 // Must be >= 1 #define SLEW_MEDIUM 6 // Must be <= 9 #define SLEW_FAST 8 // Must be <= 9 #define SLEW_MAX 9 // Must be <= 9 // get a non-zero timeout: static inline long get_timeout(long now, unsigned int t) { long m = now + t; return m ? m : 1; } static inline long get_timestamp() { return get_timeout(millis(), 0); } // Compare against current time, handling wraparound: static inline bool time_after (long a, long b) {return (b - a) < 0;} #define time_before(a,b) time_after((b),(a)) // Outbound packets get queued up here and sent when the bus is available. struct pkt_s { byte len; byte data[16]; }; static struct pkt_s auxbus_txq[AUXBUS_TXQ_SIZE]; static byte auxbus_txq_head = 0; // next slot for adding to queue static byte auxbus_txq_tail = 0; // next slot to take from static void auxbus_tx_enq (const byte *buf, byte len) { struct pkt_s *p = &auxbus_txq[auxbus_txq_head]; byte *data = p->data; if (p->len != 0) { if (DEBUG) Serial.println("txq overflow"); // txq is full, so nuke/overwrite oldest entry with the new command p->len = 0; if (++auxbus_txq_tail >= AUXBUS_TXQ_SIZE) auxbus_txq_tail = 0; } // Prefix with 0x3b, copy buf to txq, and calculate/append the checksum. byte csum = len; *data++ = 0x3b; *data++ = len; while (len--) { byte c = *buf++; csum += c; *data++ = c; } *data++ = -csum; p->len = data - p->data; if (++auxbus_txq_head >= AUXBUS_TXQ_SIZE) auxbus_txq_head = 0; } static inline struct pkt_s *claim_auxbus_for_tx () { struct pkt_s *p = &auxbus_txq[auxbus_txq_tail]; if (p->len == 0) return NULL; /* Nothing to send; txq is empty. */ if (digitalRead(AUXBUS_BUSY_PIN) == LOW) return NULL; /* auxbus is busy */ // "Claim" the auxbus pinMode(AUXBUS_BUSY_PIN, OUTPUT); digitalWrite(AUXBUS_BUSY_PIN, LOW); // Assert BUSYOUT return p; // auxbus claimed for tx } static inline void auxbus_tx (struct pkt_s *p) { // Transmit the packet for (byte i = 0; i < p->len; ++i) auxBus.write(p->data[i]); auxBus.flush(); // flush both tx and rx pinMode(AUXBUS_BUSY_PIN, INPUT_PULLUP); // De-assert BUSY // Free the txq entry and advance the txq_tail index p->len = 0; if (++auxbus_txq_tail >= AUXBUS_TXQ_SIZE) auxbus_txq_tail = 0; } #if ENABLE_BUTTON_PRESETS struct button_s { unsigned long timeout; /* millis */ unsigned long position; /* Focus Motor position */ bool have_position; }; static struct button_s *waiting_for_position = NULL; static unsigned long position_timeout = 0; static struct rxbuf_s { byte len; byte csum; byte data[32]; const char *name; } auxbus_rxbuf; static void init_rxbuf (struct rxbuf_s *rxbuf, const char *name) { memset(rxbuf, 0, sizeof(*rxbuf)); rxbuf->name = name; } static void print_packet (const char *prefix, byte *data, uint16_t len) { if (DEBUG) { char s[12]; sprintf(s, "%09lu ", millis()); Serial.print(s); Serial.print(prefix); Serial.print(F(": ")); for (uint16_t i = 0; i < len; ++i) { sprintf(s, "%02x ", data[i]); Serial.print(s); } Serial.println(); } } static bool packet_decoder (struct rxbuf_s *buf, byte b) { if (buf->len == 0) { if (b != 0x3b) { buf->len = 0; } else { buf->data[buf->len++] = b; buf->csum = 0; } } else { buf->data[buf->len++] = b; buf->csum += b; if (buf->len > 1) { if (buf->len == 2) { if (b < 3 || b >= (sizeof(buf->data) - 5)) buf->len = 0; } else if (buf->len == (buf->data[1] + 3)) { if (buf->csum == 0) { if (DEBUG) print_packet(buf->name, buf->data, buf->len); #if ENABLE_BUTTON_PRESETS if (buf->data[1] == 6 && buf->data[2] == DEV_FOCUSER && buf->data[3] == DEV_NUNCHUCK && buf->data[4] == MC_GET_POS) { position_timeout = 0; if (waiting_for_position && !waiting_for_position->have_position) { unsigned long pos = buf->data[5]; pos = (pos << 8) | buf->data[6]; pos = (pos << 8) | buf->data[7]; waiting_for_position->position = pos; if (DEBUG) { Serial.print("Current Position: "); Serial.println(waiting_for_position->position); } waiting_for_position->have_position = true; } } #endif return true; } buf->len = 0; } } } return false; } #endif /* ENABLE_BUTTON_PRESETS */ static void auxbus_receive () { while (auxBus.available()) { do { byte b = auxBus.read(); #if ENABLE_BUTTON_PRESETS struct rxbuf_s *rxbuf = &auxbus_rxbuf; if (!position_timeout || packet_decoder(rxbuf, b)) rxbuf->len = 0; // reset rxbuf for next packet #endif /* ENABLE_BUTTON_PRESETS */ } while (auxBus.available()); } } static void service_auxbus () { struct pkt_s *p; auxbus_receive(); // Service/empty the input FIFO p = claim_auxbus_for_tx(); if (!p) return; /* Nothing to send, or bus is busy */ long min_busy_time = micros() + 100; // Allow BUSY to assert for 100usec before tx while (time_before(micros(), min_busy_time)); // Delay tx until BUSY is asserted long enough auxbus_receive(); // Service/empty the input FIFO auxbus_tx(p); // All good; now send the packet } static void send_focus_cmd (uint8_t axis, int speed) { static const uint8_t focus_speeds_slow[4] = {3, 4, 5, 6}; // must be in range 1..9, 0 is STOP static const uint8_t focus_speeds_fast[4] = {6, 7, 8, 9}; // must be in range 1..9, 0 is STOP byte msg[] = {DEV_DUMMY, DEV_FOCUSER, MC_MOVE_POS, 0}; if (speed) { if (speed < 0) { speed = -speed; msg[2] = MC_MOVE_NEG; } speed--; msg[3] = axis ? focus_speeds_fast[speed] : focus_speeds_slow[speed]; } if (DEBUG) { char buf[128]; sprintf(buf, "dev=0x%02x cmd=0x%02x speed=0x%02x", DEV_FOCUSER, msg[2], msg[3]); Serial.println(buf); } auxbus_tx_enq(msg, sizeof(msg)); } #define NUNCHUK_DEVICE_ID 0x52 static byte nunchuck_buffer[6]; static byte nunchuck_x_thumb() {return nunchuck_buffer[0];} static byte nunchuck_y_thumb() {return nunchuck_buffer[1];} static bool nunchuck_z_button(){return (nunchuck_buffer[5] & 0x01) == 0;} static bool nunchuck_c_button(){return (nunchuck_buffer[5] & 0x02) == 0;} static bool nunchuck_detected; static void nunchuck_request_data() { if (!nunchuck_detected) return; Wire.beginTransmission(NUNCHUK_DEVICE_ID); Wire.write(0x00); Wire.endTransmission(); } static bool nunchuck_get_data() { static long nextpoll = 0; long now = millis(); if (time_before(now, nextpoll)) return false; nextpoll = now + 50; // every 50msecs = polling 20 times per second nunchuck_request_data(); // For next time Wire.requestFrom(NUNCHUK_DEVICE_ID, sizeof(nunchuck_buffer)); byte bytecount; for (bytecount = 0; bytecount < sizeof(nunchuck_buffer) && Wire.available(); bytecount++) nunchuck_buffer[bytecount] = Wire.read(); return (bytecount == sizeof(nunchuck_buffer)); } static int nunchuck_interpret (uint16_t val) { const int C = 128; // Center position if (val >= (C + 127)) return 4; if (val >= (C + 100)) return 3; if (val >= (C + 60)) return 2; if (val >= (C + 3)) return 1; // Centre position if (val <= (C - 127)) return -4; if (val <= (C - 100)) return -3; if (val <= (C - 60)) return -2; if (val <= (C - 3)) return -1; return 0; } #if ENABLE_BUTTON_PRESETS static void focus_request_position (void) { if (DEBUG) Serial.println("Focus request position"); position_timeout = get_timeout(millis(), 1000); byte msg[] = {DEV_NUNCHUCK, DEV_FOCUSER, MC_GET_POS}; auxbus_tx_enq(msg, sizeof(msg)); } static void focus_to_position (unsigned long position) { if (DEBUG) { Serial.print("Move to position "); Serial.println(position); } byte msg[] = {DEV_NUNCHUCK, DEV_FOCUSER, MC_GOTO_FAST, 0, 0, 0}; msg[3] = position >> 16; msg[4] = position >> 8; msg[5] = position; auxbus_tx_enq(msg, sizeof(msg)); } static void handle_preset_button(bool moving, bool button_down, struct button_s *b) { if (position_timeout && time_after(millis(), position_timeout)) { position_timeout = 0; if (waiting_for_position) { waiting_for_position->have_position = false; waiting_for_position->timeout = 0; waiting_for_position = NULL; } } if (moving) { b->timeout = 0; waiting_for_position = NULL; return; } if (button_down) { unsigned long now = millis(); if (!b->timeout) { if (DEBUG) Serial.println("Button down "); b->timeout = get_timeout(now, 2000); // Hold button for 2.000 seconds to save current focus position. if (waiting_for_position == b) waiting_for_position = NULL; return; } if (time_after(now, b->timeout)) { if (!waiting_for_position) { b->have_position = false; waiting_for_position = b; focus_request_position(); } } } else if (b->timeout) { b->timeout = 0; if (DEBUG) Serial.println("Button up"); if (b->have_position) { if (waiting_for_position == b) waiting_for_position = NULL; else focus_to_position(b->position); } } } #endif /* ENABLE_BUTTON_PRESETS */ static bool nunchuck_poll (bool first_time) { static int oldx = 0, oldy = 0, offsetx = 0, offsety = 0; if (nunchuck_get_data()) { int newy = nunchuck_interpret(nunchuck_y_thumb()); int newx = nunchuck_interpret(nunchuck_x_thumb()); // First time through, determine the "at rest" coordinates: if (first_time) { oldx = offsetx = newx; oldy = offsety = newy; if (DEBUG) { Serial.print("x="); Serial.print(newx); Serial.print(" y="); Serial.println(newy); } return false; } newx -= offsetx; newy -= offsety; newy = 0 - newy; // Handle a change in X-axis of the controller: if (oldx != newx) { oldx = newx; send_focus_cmd(0, newx); } // Handle a change in Y-axis of the controller: if (oldy != newy) { oldy = newy; send_focus_cmd(1, newy); } } bool moving = (oldy != offsety) || (oldx != offsetx); #if ENABLE_BUTTON_PRESETS static struct button_s z_button, c_button; handle_preset_button(moving, nunchuck_z_button(), &z_button); handle_preset_button(moving, nunchuck_c_button(), &c_button); #endif /* ENABLE_BUTTON_PRESETS */ return moving; // nunchuck is actively moving focus } static void nunchuck_setup () { Wire.begin(); Wire.setClock(100000ul); // Initialize Nunchuck for non-encrypted mode: Wire.beginTransmission(NUNCHUK_DEVICE_ID); Wire.write(0xf0); Wire.write(0x55); nunchuck_detected = (Wire.endTransmission() == 0); if (nunchuck_detected) { Wire.beginTransmission(NUNCHUK_DEVICE_ID); Wire.write(0xfb); Wire.write(0x00); nunchuck_detected = (Wire.endTransmission() == 0); delay(100); // give it time to become ready, otherwise we get a false first reading nunchuck_poll(true); } if (DEBUG) { Serial.print("nunchuck_detected="); Serial.println(nunchuck_detected ? "yes" : "no"); } } void loop() { bool actively_moving = false; if (nunchuck_detected) actively_moving |= nunchuck_poll(false); digitalWrite(LED_PIN, actively_moving ? LED_ON : LED_OFF); service_auxbus(); } void setup() { delay(2000); // necessary for Nunchuck, and for serial port when DEBUG is on if (DEBUG) { Serial.begin(115200); Serial.println("Ready"); } // Get the auxbus configured before _anything_ else: pinMode(AUXBUS_BUSY_PIN, INPUT_PULLUP); // save a resistor by using internal pull-up auxBus.begin(19200, SERIAL_8N2); pinMode(AUXBUS_RX_PIN, INPUT); // default of "PULLUP" rumoured to be too much here pinMode(LED_PIN, OUTPUT); digitalWrite(LED_PIN, LED_ON); memset(auxbus_txq, 0, sizeof(auxbus_txq)); #if ENABLE_BUTTON_PRESETS init_rxbuf(&auxbus_rxbuf, "auxbus_rx"); #endif digitalWrite(LED_PIN, LED_OFF); nunchuck_setup(); }