/*
Automatic Turntable
Automatic turntable for 3D scanning using photogrammetry
https://palmacas.com/mesa-automatica
*/
#include <Wire.h>
#include <U8g2lib.h>
#include <EEPROM.h>
// SSD1306 constructor using I2C
U8G2_SSD1306_128X64_NONAME_F_HW_I2C display(U8G2_R0);
// Rotary encoder pins and varibles
#define CLK 2
#define DAT 3
#define BTN 4
int relative_position = 0;
unsigned long last_press = 0;
// Buzzer pin
#define BUZ 9
//Stepper drivers pins
#define STEP1_EN A3
#define STEP1 7
#define DIR1 8
#define STEP2_EN A2
#define STEP2 13
#define DIR2 12
#define CW HIGH
#define CCW LOW
// Menu structure
int menu_index = 0;
int menu_items = 0;
int main_lvl = -1;
int step_lvl = -1;
int auto_lvl = -1;
int man_lvl = -1;
int flag;
// Variables
char *project = "Automatic Turntable";
char *version = "v1.0";
char *author = "palmacas 2025";
char *main_menu[] = {"Steps", "Auto Rotation", "Manual", "Settings"};
char *step_menu[] = {"Degrees:", "Delay:", "Rotations:", "Start", "Back", "Hola", "Prueba"};
char *auto_menu[] = {"Rotations:", "Time:", "RPM:", "Start", "Back"};
char *manual_menu[] = {"Degrees:", "Clockwise", "Counterclockwise", "Back"};
char *settings_menu[] = {"Sound", "Save"};
int step_degree = 35;
int step_delay = 1000;
int step_rotations = 2;
int auto_rotations = 12;
int auto_time = 234;
int auto_rpm = 180;
int manual_degree;
int min_deg = 1;
int max_deg = 360;
int min_delay = 1;
int max_delay = 60;
int min_rot = 1;
int max_rot = 10;
int min_time = 1;
int max_time = 60;
int min_rpm = 1;
int max_rpm = 60;
void setup() {
// Starts serial port
Serial.begin(115200);
// Project data
Serial.println(project);
Serial.println(version);
Serial.println(author);
// Project data
display.begin();
display.firstPage();
do {
display.setFont(u8g2_font_t0_15_tr);
display.drawStr(64 - display.getStrWidth(project)/2, 15, project);
display.drawStr(64 - display.getStrWidth(version)/2, 45, version);
display.drawStr(64 - display.getStrWidth(author)/2, 60, author);
} while (display.nextPage());
// Startup sound
pinMode(BUZ, OUTPUT);
tone(BUZ, 1568, 100); // G6
delay(250);
tone(BUZ, 1047, 100); // C6
delay(250);
tone(BUZ, 1568, 100); // G6
delay(100);
noTone(BUZ);
delay(100);
// Splash screen
/*display.firstPage();
do {
display.drawXBM(0, 0, 128, 64, splash_screen);
} while (display.nextPage());*/
delay(1000);
// Encoder pins
pinMode(CLK, INPUT_PULLUP);
pinMode(DAT, INPUT_PULLUP);
pinMode(BTN, INPUT_PULLUP);
attachInterrupt(digitalPinToInterrupt(2), isr, FALLING);
// Stepper driver pins
pinMode(STEP1_EN, OUTPUT);
pinMode(STEP1, OUTPUT);
pinMode(DIR1, OUTPUT);
digitalWrite(STEP1_EN, HIGH);
// Reads previous settings
/*EEPROM.get(1, step_degree);
EEPROM.get(3, step_delay);
EEPROM.get(5, step_rotations);
EEPROM.get(7, auto_rotations);
EEPROM.get(9, auto_time);
EEPROM.get(11, auto_rpm);
EEPROM.get(13, manual_degree);*/
}
void loop() {
switch (main_lvl) {
case -1:
menu_items = sizeof(step_menu)/sizeof(step_menu[0]);
Serial.println(menu_items);
relative_position = relativePositionLimit(relative_position, menu_items - 1);
menu_index = relative_position;
Serial.println(menu_index);
char step_degree_char[] = {"1000"};
char step_delay_char[] = {"1000"};
char step_rotations_char[] = {"1000"};
itoa(step_degree, step_degree_char, 10);
itoa(step_delay, step_delay_char, 10);
itoa(step_rotations, step_rotations_char, 10);
char *step_values[] = {step_degree_char, step_delay_char, step_rotations_char, "", "", "", ""};
showMainMenu(step_menu, step_values, menu_items, menu_index);
if (readButton() == 1) {
main_lvl = relative_position;
relative_position = 0;
}
break;
/*case 0:
while (main_lvl == 0) {
StepMenu();
}
relative_position = 0;
break;
case 1:
while (main_lvl == 1) {
AutoMenu();
}
relative_position = 1;
break;
case 2:
while (main_lvl == 2) {
ManualMenu();
}
relative_position = 2;*/
}
delay(2);
}
void isr() {
static unsigned long last_time = 0;
if (millis() - last_time > 5) {
if (!digitalRead(DAT)) {
relative_position --;
}
else {
relative_position ++;
}
}
last_time = millis();
}
void StepMenu() {
delay(200);
switch (step_lvl) {
case -1:
flag = 1;
menu_items = 4;
showMenu(step_menu, step_degree, step_delay, step_rotations);
relativePositionLimit(0, menu_items);
menu_index = relative_position;
if (readButton() == 1) {
step_lvl = relative_position;
flag = 0;
}
break;
case 0:
if (flag == 0) {
relative_position = step_degree;
flag = 1;
}
showMenu(step_menu, step_degree, step_delay, step_rotations);
relativePositionLimit(min_deg, max_deg);
step_degree = relative_position;
if (readButton() == 1) {
//EEPROM.put(1, step_degree);
relative_position = step_lvl;
step_lvl = -1;
}
break;
case 1:
if (flag == 0) {
relative_position = step_delay;
flag = 1;
}
showMenu(step_menu, step_degree, step_delay, step_rotations);
relativePositionLimit(min_delay, max_delay);
step_delay = relative_position;
if (readButton() == 1) {
//EEPROM.put(3, step_delay);
relative_position = step_lvl;
step_lvl = -1;
}
break;
case 2:
if (flag == 0) {
relative_position = step_rotations;
flag = 1;
}
showMenu(step_menu, step_degree, step_delay, step_rotations);
relativePositionLimit(min_rot, max_rot);
step_rotations = relative_position;
if (readButton() == 1) {
//EEPROM.put(5, step_rotations);
relative_position = step_lvl;
step_lvl = -1;
}
break;
case 3:
step_menu[4] = "STOP";
showMenu(step_menu, step_degree, step_delay, step_rotations);
moveMotor(CW, 10, 500);
step_menu[4] = "START";
relative_position = step_lvl;
step_lvl = -1;
break;
case 4:
step_lvl = -1;
main_lvl = -1;
break;
}
}
void AutoMenu() {
switch (auto_lvl) {
case -1:
flag = 1;
menu_items = 4;
showMenu(auto_menu, auto_rotations, auto_time, auto_rpm);
relativePositionLimit(0, menu_items);
menu_index = relative_position;
if (readButton() == 1) {
auto_lvl = relative_position;
flag = 0;
}
break;
case 0:
if (flag == 0) {
relative_position = auto_rotations;
flag = 1;
}
showMenu(auto_menu, auto_rotations, auto_time, auto_rpm);
relativePositionLimit(min_rot, max_rot);
auto_rotations = relative_position;
if (readButton() == 1) {
//EEPROM.put(7, auto_rotations);
relative_position = auto_lvl;
auto_lvl = -1;
}
break;
case 1:
if (flag == 0) {
relative_position = auto_time;
flag = 1;
}
showMenu(auto_menu, auto_rotations, auto_time, auto_rpm);
relativePositionLimit(min_time, max_time);
auto_time = relative_position;
if (readButton() == 1) {
//EEPROM.put(9, auto_time);
relative_position = auto_lvl;
auto_lvl = -1;
}
break;
case 2:
if (flag == 0) {
relative_position = auto_rpm;
flag = 1;
}
showMenu(auto_menu, auto_rotations, auto_time, auto_rpm);
relativePositionLimit(min_rpm, max_rpm);
auto_rpm = relative_position;
if (readButton() == 1) {
//EEPROM.put(11, auto_rpm);
relative_position = auto_lvl;
auto_lvl = -1;
}
break;
case 3:
auto_menu[4] = "STOP";
showMenu(auto_menu, auto_rotations, auto_time, auto_rpm);
moveMotor(CCW, 10, 500);
auto_menu[4] = "START";
relative_position = auto_lvl;
auto_lvl = -1;
break;
case 4:
auto_lvl = -1;
main_lvl = -1;
break;
}
}
void ManualMenu() {
switch (man_lvl) {
case -1:
flag = 1;
menu_items = 3;
//showManualMenu();
relativePositionLimit(0, menu_items);
menu_index = relative_position;
if (readButton() == 1) {
man_lvl = relative_position;
flag = 0;
}
break;
case 0:
if (flag == 0) {
relative_position = manual_degree;
flag = 1;
}
//showManualMenu();
relativePositionLimit(min_deg, max_deg);
manual_degree = relative_position;
if (readButton() == 1) {
//EEPROM.put(13, manual_degree);
relative_position = man_lvl;
man_lvl = -1;
}
break;
case 1:
manual_menu[2] = "STOP";
//showManualMenu();
moveMotor(CW, manual_degree, 500);
manual_menu[2] = "CLOCKWISE";
man_lvl = -1;
break;
case 2:
manual_menu[3] = "STOP";
//showManualMenu();
moveMotor(CCW, manual_degree, 500);
manual_menu[3] = "ANTICLOCKWISE";
man_lvl = -1;
break;
case 3:
man_lvl = -1;
main_lvl = -1;
break;
}
}
// Allows loops on the menus
int relativePositionLimit(int position, int max_position) {
int rel_position;
if (position < 0) { rel_position = max_position; }
else if (position > max_position) { rel_position = 0; }
else { rel_position = position; }
return rel_position;
}
// Displays main menu
void showMainMenu(char *menu[], char *values[], int num_items, int index) {
int menu_div = 64.0/num_items + 0.5;
int index_offset = 0;
if (index >= 4){
index_offset = index - 3;
Serial.println(index_offset);
}
int entero1 = 234;
display.firstPage();
do {
// Draws the whole menu
display.setFontMode(1);
//display.setFont(u8g2_font_9x15_tr);
display.drawStr(2, 13, menu[0 + index_offset]);
display.drawStr(4 + display.getStrWidth(menu[0 + index_offset]), 13, entero1);
display.drawStr(2, 29, menu[1 + index_offset]);
display.drawStr(4 + display.getStrWidth(menu[1 + index_offset]), 29, values[1 + index_offset]);
display.drawStr(2, 45, menu[2 + index_offset]);
display.drawStr(4 + display.getStrWidth(menu[2 + index_offset]), 45, values[2 + index_offset]);
display.drawStr(2, 61, menu[3 + index_offset]);
display.drawStr(4 + display.getStrWidth(menu[3 + index_offset]), 61, values[3 + index_offset]);
// Draws the scrollbar
display.setDrawColor(1);
display.drawLine(126, 0, 126, 63);
display.drawBox(125, menu_div * index , 3, menu_div);
// Draws the selected item
display.drawRBox(0, ((index - index_offset) * 16), 123, 16, 3);
display.setDrawColor(2);
display.drawStr(2, ((index - index_offset) * 16) + 13, menu[index]);
display.drawStr(4 + display.getStrWidth(menu[index]), ((index - index_offset) * 16) + 13, values[index]);
} while (display.nextPage());
}
// Displays menus
void showMenu(char *menu[], int16_t param1, int16_t param2, int16_t param3) {
int16_t param[] = {param1, param2, param3};
display.firstPage();
do {
display.setFont(u8g2_font_7x14_mr);
display.drawRBox(0, 0, 124, 16, 2);
display.drawStr(1, 10, menu[0]);
display.drawStr(1, 22, menu[1]);
display.setCursor(15 + display.getStrWidth(menu[1]) ,22);
display.print("56.57");
display.drawStr(1, 34, menu[2]);
display.drawStr(1, 46, menu[3]);
display.drawStr(1, 58, menu[4]);
display.drawStr(1, (menu_index * 8) + 15, ">");
} while (display.nextPage());
}
// Displays menu to setup MANUAL mode
/*void showManualMenu() {
display.clearBuffer();
//display.setTextSize(1);
//display.fillRect(0, 0, SCREEN_WIDTH, 10, WHITE);
//display.setTextColor(SSD1306_BLACK);
display.setCursor(tab, 1);
display.print(manual_menu[0]);
//display.setTextColor(SSD1306_WHITE);
display.setCursor(tab, 12);
display.print(manual_menu[1]); display.print(manual_degree);
display.setCursor(tab, 23);
display.print(manual_menu[2]);
display.setCursor(tab, 34);
display.print(manual_menu[3]);
display.setCursor(tab, 45);
display.print(manual_menu[4]);
display.setCursor(2, (menu_index * spacing) + 12);
display.print(">");
display.sendBuffer();
}*/
void moveMotor(bool mot_dir, int mot_deg, int time) {
int mot_step = mot_deg / 0.1125; // Divide over 1.8 for full step, 0.9 for half step and so on
Serial.println(mot_step);
digitalWrite(STEP1_EN, LOW);
digitalWrite(DIR1, mot_dir);
for (int i = 0; i < mot_step; i++) {
digitalWrite(STEP1, HIGH);
if (readButton() == 1) {
digitalWrite(STEP1_EN, HIGH);
break;
}
delayMicroseconds(time);
digitalWrite(STEP1, LOW);
if (readButton() == 1) {
digitalWrite(STEP1_EN, HIGH);
break;
}
delayMicroseconds(time);
}
digitalWrite(STEP1_EN, HIGH);
}
int readButton() {
int btn_state = 0;
if (digitalRead(BTN) == LOW) {
if (millis() - last_press > 100) {
btn_state = 1;
playTone();
}
last_press = millis();
}
delay(5);
return btn_state;
}
int playTone() {
tone(BUZ, 1047, 100); // C6
delay(100);
noTone(BUZ);
}