/* EW8ZO Rotator Controller v1.76 - displayAz гарантирует шаг 5° при повороте энкодера (300,305,310...) - Геркон (reed) на D2 - EncButton eb(5,6,7) - Реле: RELAY_1 = 8, RELAY_2 = 9 (2 реле) - START/STOP кнопка на D3 (второй контакт -> GND), INPUT_PULLUP - Режимы реле: Ожидание: оба OFF Вправо: RELAY_2 ON Влево: RELAY_1 + RELAY_2 ON */ #include #include #include #include // LCD и энкодер (CLK->5, DT->6, SW->7) LiquidCrystal_I2C lcd(0x27, 16, 2); EncButton eb(5, 6, 7); // ===== Реле (2 реле) ===== #define RELAY_1 8 #define RELAY_2 9 const bool RELAY_ACTIVE_HIGH = false; // true если модуль реле активен HIGH inline void relayWriteRaw(uint8_t pin, bool level) { digitalWrite(pin, level ? HIGH : LOW); } inline void relayWrite(uint8_t pin, bool active) { if (RELAY_ACTIVE_HIGH) relayWriteRaw(pin, active); else relayWriteRaw(pin, !active); } // ===== Константы ===== const long COUNTS_PER_REV = 1200; const unsigned long SAVE_DELAY_MS = 1000; const unsigned long LCD_UPDATE_MS = 100; const unsigned long SAVED_DISPLAY_MS = 1000; const unsigned long SPLASH_MS = 2000; const unsigned long SAVE_DEDUP_MS = 1500; // EEPROM const int EEPROM_ADDR_MAGIC = 0; const int EEPROM_ADDR_VALUE = 2; const uint16_t EEPROM_MAGIC = 0xA5A5; // ===== START/STOP кнопка ===== const int STARTSTOP_PIN = 3; // D3 const unsigned long BTN_DEBOUNCE_MS = 50; bool startStopLastState = HIGH; unsigned long startStopLastChange = 0; bool startStopPressedHandled = false; // ===== Глобальные переменные ===== volatile long sensor = 0; volatile long temp_sensor = 0; unsigned long lastMoveMillis = 0; bool movedSinceLastSave = false; unsigned long lastLcdMillis = 0; const int COL_NEW_VAL = 5; const int W_NEW_VAL = 5; const int COL_CUR_AZ = 9; const int W_CUR_AZ = 3; enum Mode { MODE_NORMAL, MODE_ADD }; Mode mode = MODE_NORMAL; int addAz = 1; bool suppressNextClick = false; bool movementActive = false; int movementTargetAz = 1; unsigned long savedDisplayMillis = 0; unsigned long lastSavedMillis = 0; // ===== Геркон (reed) ===== const int REED_PIN = 2; const unsigned long REED_DEBOUNCE_US = 8000UL; volatile unsigned long lastReedMicros = 0; // ===== Флаги направления для ISR геркона ===== volatile bool dirRightFlag = false; volatile bool dirLeftFlag = false; // ===== Новая переменная: отображаемый азимут, всегда кратный 5 ===== int displayAz = 0; // будет инициализирован в setup() // ===== Функции управления мотором (по вашей логике) ===== void motorStop() { relayWrite(RELAY_1, false); relayWrite(RELAY_2, false); dirRightFlag = false; dirLeftFlag = false; Serial.println("Motor: STOP (both relays OFF)"); } void motorForward() { // вправо: только RELAY_2 ON relayWrite(RELAY_1, false); relayWrite(RELAY_2, true); dirRightFlag = true; dirLeftFlag = false; Serial.println("Motor: FORWARD (RELAY_2 ON)"); } void motorBackward() { // влево: оба реле ON relayWrite(RELAY_1, true); relayWrite(RELAY_2, true); dirRightFlag = false; dirLeftFlag = true; Serial.println("Motor: BACKWARD (both RELAY_1 & RELAY_2 ON)"); } // ===== EEPROM helpers ===== void savePositionToEEPROM(long val) { uint16_t magic; EEPROM.get(EEPROM_ADDR_MAGIC, magic); if (magic != EEPROM_MAGIC) EEPROM.put(EEPROM_ADDR_MAGIC, EEPROM_MAGIC); long stored; EEPROM.get(EEPROM_ADDR_VALUE, stored); if (stored != val) EEPROM.put(EEPROM_ADDR_VALUE, val); lastSavedMillis = millis(); } bool loadPositionFromEEPROM(long &outVal) { uint16_t magic; EEPROM.get(EEPROM_ADDR_MAGIC, magic); if (magic != EEPROM_MAGIC) return false; EEPROM.get(EEPROM_ADDR_VALUE, outVal); return true; } // ===== Вспомогательные конвертации ===== int countsToAzimuth1(long counts) { long c = counts % COUNTS_PER_REV; if (c < 0) c += COUNTS_PER_REV; long deg = (c * 360L) / COUNTS_PER_REV; int az = (int)deg; return (az == 0) ? 360 : az; } long azimuthToCounts(int az) { int a = az % 360; long c = ((long)a * COUNTS_PER_REV + 180) / 360; if (az == 360) c = 0; c = c % COUNTS_PER_REV; if (c < 0) c += COUNTS_PER_REV; return c; } int shortestTurnDirection(int current, int target) { if (current == target) return 0; int diff = target - current; if (diff > 180) diff -= 360; if (diff < -180) diff += 360; return (diff > 0) ? 1 : -1; } // ===== Управление движением ===== void stopMovement() { motorStop(); movementActive = false; noInterrupts(); long cur = sensor; interrupts(); if (millis() - lastSavedMillis > SAVE_DEDUP_MS) { savePositionToEEPROM(cur); Serial.println("Movement stopped, saved (immediate)"); } else Serial.println("Movement stopped (save skipped - recent)"); movedSinceLastSave = false; lastMoveMillis = millis(); savedDisplayMillis = millis(); eb.counter = countsToAzimuth1(cur); // Лог остановки Serial.print("STOP event: curCounts="); Serial.print(cur); Serial.print(" curAz="); Serial.print(countsToAzimuth1(cur)); Serial.print(" targetAz="); Serial.print(movementTargetAz); Serial.print(" dirR="); Serial.print(dirRightFlag); Serial.print(" dirL="); Serial.println(dirLeftFlag); } void startMovementTo(int targetAz) { if (movementActive) return; noInterrupts(); long curSensor = sensor; interrupts(); int curAz = countsToAzimuth1(curSensor); int dir = shortestTurnDirection(curAz, targetAz); if (dir == 0) { Serial.println("Already at target azimuth, no movement"); if (millis() - lastSavedMillis > SAVE_DEDUP_MS) { savePositionToEEPROM(curSensor); savedDisplayMillis = millis(); } return; } movementTargetAz = targetAz; movementActive = true; if (dir > 0) motorForward(); else motorBackward(); // Лог старта Serial.print("START event: curCounts="); Serial.print(curSensor); Serial.print(" curAz="); Serial.print(curAz); Serial.print(" targetAz="); Serial.print(targetAz); Serial.print(" dir="); Serial.println((dir>0) ? "CW" : "CCW"); } // ===== LCD helpers ===== void lcdPrintFieldStr(int col, int row, const char* s, int width) { lcd.setCursor(col, row); for (int i = 0; i < width; ++i) lcd.print(' '); lcd.setCursor(col, row); lcd.print(s); } void lcdPrintFieldLong(int col, int row, long value, int width) { char buf[20]; snprintf(buf, sizeof(buf), "%ld", value); lcdPrintFieldStr(col, row, buf, width); } void lcdPrintFieldIntFmt(int col, int row, int value, const char* fmt, int width) { char buf[20]; snprintf(buf, sizeof(buf), fmt, value); lcdPrintFieldStr(col, row, buf, width); } // ===== ISR геркона (reed) ===== void reedISR() { unsigned long t = micros(); if (t - lastReedMicros < REED_DEBOUNCE_US) return; lastReedMicros = t; if (dirRightFlag) sensor++; else if (dirLeftFlag) sensor--; } // ===== Setup ===== void setup() { Serial.begin(115200); // Геркон pinMode(REED_PIN, INPUT_PULLUP); // Реле: безопасный уровень до OUTPUT if (RELAY_ACTIVE_HIGH) { relayWriteRaw(RELAY_1, LOW); relayWriteRaw(RELAY_2, LOW); } else { relayWriteRaw(RELAY_1, HIGH); relayWriteRaw(RELAY_2, HIGH); } pinMode(RELAY_1, OUTPUT); pinMode(RELAY_2, OUTPUT); motorStop(); // START/STOP кнопка (D3) pinMode(STARTSTOP_PIN, INPUT_PULLUP); startStopLastState = digitalRead(STARTSTOP_PIN); startStopLastChange = millis(); // Прерывание для геркона attachInterrupt(digitalPinToInterrupt(REED_PIN), reedISR, FALLING); // LCD splash lcd.begin(); lcd.backlight(); lcd.clear(); lcd.setCursor(0,0); lcd.print("EW8ZO Controller"); lcd.setCursor(0,1); lcd.print("VER 1.76"); delay(SPLASH_MS); lcd.clear(); // EncButton настройки eb.setBtnLevel(LOW); eb.setClickTimeout(800); eb.setDebTimeout(8); eb.setHoldTimeout(3000); eb.setEncType(EB_STEP4_LOW); eb.setEncReverse(0); eb.setFastTimeout(10); eb.counter = 0; // Загрузка сохранённой позиции long saved = 0; if (loadPositionFromEEPROM(saved)) { noInterrupts(); sensor = saved; interrupts(); Serial.print("Loaded pos counts: "); Serial.println(saved); lastSavedMillis = millis(); } else Serial.println("No saved pos in EEPROM"); // Инициализация eb.counter и displayAz noInterrupts(); int curAz = countsToAzimuth1(sensor); interrupts(); // Инициализируем displayAz как нижнее кратное 5 (304 -> 300) displayAz = (curAz / 5) * 5; if (displayAz <= 0) displayAz = 360; eb.counter = displayAz; lcdPrintFieldStr(0,0,"New:",5); lcdPrintFieldStr(0,1,"Azimuth:",8); lastLcdMillis = millis(); // Информационный лог при старте Serial.println("Controller v1.76 started"); Serial.print("Initial curAz="); Serial.print(curAz); Serial.print(" displayAz="); Serial.println(displayAz); } // ===== Main loop ===== void loop() { eb.tick(); // Обработка START/STOP кнопки (debounce) с логом в Serial bool curState = digitalRead(STARTSTOP_PIN); if (curState != startStopLastState) { startStopLastChange = millis(); startStopLastState = curState; startStopPressedHandled = false; } else { if (!startStopPressedHandled && (millis() - startStopLastChange) > BTN_DEBOUNCE_MS) { if (curState == LOW) { // нажата noInterrupts(); long curSensor = sensor; interrupts(); int curAz = countsToAzimuth1(curSensor); Serial.print("BUTTON START/STOP pressed -> New="); Serial.print(eb.counter); Serial.print(" CurAz="); Serial.print(curAz); Serial.print(" movementActive="); Serial.println(movementActive); if (movementActive) { stopMovement(); } else { int desiredAz = eb.counter; if (desiredAz <= 0) desiredAz = 1; if (desiredAz > 360) desiredAz = ((desiredAz - 1) % 360) + 1; if (desiredAz != curAz) startMovementTo(desiredAz); else Serial.println("Start button: desired == current, nothing to do"); } } startStopPressedHandled = true; } } // Вход в ADD режим при удержании энкодера if (eb.hold() && mode == MODE_NORMAL && !movementActive) { mode = MODE_ADD; noInterrupts(); int curAz = countsToAzimuth1(sensor); interrupts(); addAz = curAz; Serial.println("Entered ADD mode"); suppressNextClick = false; lastLcdMillis = 0; } // Отслеживание изменений sensor if (sensor != temp_sensor) { temp_sensor = sensor; lastMoveMillis = millis(); movedSinceLastSave = true; } // Если движение активно — проверяем достижение цели if (movementActive) { noInterrupts(); long curSensor = sensor; interrupts(); int curAz = countsToAzimuth1(curSensor); if (curAz == movementTargetAz) { stopMovement(); lastLcdMillis = 0; } } // Сохранение в EEPROM после задержки if (movedSinceLastSave && (millis() - lastMoveMillis >= SAVE_DELAY_MS)) { if (millis() - lastSavedMillis > SAVE_DEDUP_MS) { noInterrupts(); long toSave = sensor; interrupts(); int azForLog = countsToAzimuth1(toSave); Serial.print("Saving counts: "); Serial.print(toSave); Serial.print(" azimuth: "); Serial.println(azForLog); savePositionToEEPROM(toSave); movedSinceLastSave = false; savedDisplayMillis = millis(); int newAz = countsToAzimuth1(toSave); eb.counter = newAz; lastLcdMillis = 0; } else movedSinceLastSave = false; } // Обработка поворотов энкодера static unsigned long lastTurnMs = 0; if (eb.turn()) { unsigned long now = millis(); unsigned long dt = now - lastTurnMs; lastTurnMs = now; int dir = eb.dir(); // 1 или -1 if (mode == MODE_NORMAL) { if (!movementActive) { // Обновляем displayAz ровно на 5 градусов за один шаг энкодера displayAz += dir * 5; // Нормализация 1..360 while (displayAz <= 0) displayAz += 360; displayAz = ((displayAz - 1) % 360) + 1; // Синхронизируем New с displayAz eb.counter = displayAz; lastLcdMillis = 0; } } else if (mode == MODE_ADD) { // ADD режим с ускорением (как раньше) int baseStep = 5; int mult; if (dt < 30) mult = 20; else if (dt < 80) mult = 5; else mult = 1; int delta = dir * baseStep * mult; addAz += delta; while (addAz <= 0) addAz += 360; addAz = ((addAz - 1) % 360) + 1; lastLcdMillis = 0; } } // Подтверждение ADD кликом if (mode == MODE_ADD && eb.click()) { long newCounts = azimuthToCounts(addAz); noInterrupts(); sensor = newCounts; interrupts(); if (millis() - lastSavedMillis > SAVE_DEDUP_MS) { savePositionToEEPROM(newCounts); Serial.print("ADD confirmed: az="); Serial.print(addAz); Serial.print(" counts="); Serial.println(newCounts); } else { Serial.print("ADD confirmed: az="); Serial.print(addAz); Serial.println(" (save skipped - recent)"); } eb.counter = addAz; mode = MODE_NORMAL; suppressNextClick = true; savedDisplayMillis = millis(); lastLcdMillis = 0; // Синхронизируем displayAz с новым значением displayAz = addAz; } // Нормальный клик энкодера: старт движения if (mode == MODE_NORMAL && eb.click()) { if (suppressNextClick) suppressNextClick = false; else { noInterrupts(); long curSensor = sensor; interrupts(); int curAz = countsToAzimuth1(curSensor); int desiredAz = eb.counter; if (desiredAz <= 0) desiredAz = 1; if (desiredAz > 360) desiredAz = ((desiredAz - 1) % 360) + 1; if (desiredAz != curAz) startMovementTo(desiredAz); else Serial.println("Click: desired == current, nothing to do"); lastLcdMillis = 0; } } // Обновление LCD if (millis() - lastLcdMillis >= LCD_UPDATE_MS) { lastLcdMillis = millis(); noInterrupts(); long curSensor = sensor; interrupts(); int curAz = countsToAzimuth1(curSensor); if (mode == MODE_NORMAL) { if (millis() - savedDisplayMillis < SAVED_DISPLAY_MS) { lcdPrintFieldStr(0,0,"SAVED",5); lcdPrintFieldLong(COL_NEW_VAL,0,eb.counter,W_NEW_VAL); } else { lcdPrintFieldStr(0,0,"New:",5); lcdPrintFieldLong(COL_NEW_VAL,0,eb.counter,W_NEW_VAL); } lcdPrintFieldIntFmt(COL_CUR_AZ,1,curAz,"%03d",W_CUR_AZ); lcd.setCursor(13,1); if (movementActive) { int dir = shortestTurnDirection(curAz, movementTargetAz); if (dir > 0) lcd.print('>'); else if (dir < 0) lcd.print('<'); else lcd.print('='); } else lcd.print(' '); } else if (mode == MODE_ADD) { lcdPrintFieldStr(0,0,"ADD:",5); lcdPrintFieldIntFmt(COL_NEW_VAL,0,addAz,"%03d",3); lcdPrintFieldIntFmt(COL_CUR_AZ,1,curAz,"%03d",W_CUR_AZ); lcd.setCursor(13,1); lcd.print(' '); } } delay(2); }