код 3.45 /* EW8ZO Rotator Controller v3.47 === ОСНОВНЫЕ ФУНКЦИИ === - Автоматическая калибровка COUNTS_PER_REV (CW + CCW) - Режим Overlap: North 360° / South 180° - DX-индикатор направления (префиксы стран) - Chase-mode: реальное время следование за энкодером - Многоуровневое меню с энкодером - Ручная настройка CPR через меню === ЖЕЛЕЗО === - Геркон на D2 (прямой подсчет импульсов в ISR) - Энкодер EncButton на пинах 5,6,7 - Реле: RELAY_1 = 8 (CCW), RELAY_2 = 9 (CW) - START/STOP кнопка на D3 (INPUT_PULLUP) - LCD 16x2 I2C (0x27) === МЕНЮ === 1. Overlap (North/South) 2. Correct azimuth 3. Calibration 4. DX Indicator ON/OFF 5. Chase Mode 6. Change CPR 7. Exit === КАЛИБРОВКА v3.04 === Фаза 1: Подтверждение входа (показ CPR) Фаза 2: Настройка CPR (энкодер + двигатель) Фаза 3: Сохранение CPR + финальный круг CCW до 0° Фаза 4: Выход в NORMAL режим === ПОСЛЕДНИЕ ИЗМЕНЕНИЯ === - v3.47: Унифицирована логика overlap (градусы вместо импульсов) - v3.46: Исправлена логика South overlap (<= 180 вместо < 180) - v3.45: Исправлен выход из Change CPR в NORMAL mode - v3.44: Добавлен пункт меню Change CPR для ручной настройки CPR - v3.43: Исправлена обработка энкодера в chase-mode во время движения - v3.42: Добавлен chase-mode с динамическим следованием за энкодером - v3.41: Исправлена синхронизация eb.counter с displayAzimuth - v3.40: Добавлена фильтрация чувствительности chase-mode (5°) - v3.39: Исправлено сохранение chase-mode в EEPROM - v3.38: Добавлен toggle chase-mode через энкодер в меню */ #include #include #include #include // ===== LCD и энкодер ===== LiquidCrystal_I2C lcd(0x27, 16, 2); EncButton eb(5, 6, 7); // ===== Реле ===== #define RELAY_1 8 #define RELAY_2 9 const bool RELAY_ACTIVE_HIGH = false; 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_DEFAULT = 151; const unsigned long SAVE_DELAY_MS = 1000; const unsigned long LCD_UPDATE_MS = 100; const unsigned long SPLASH_MS = 2000; const unsigned long SAVE_DEDUP_MS = 1500; const char* FW_VERSION = "3.47"; // EEPROM const int EEPROM_ADDR_MAGIC = 0; const int EEPROM_ADDR_VALUE = 2; const int EEPROM_ADDR_OVERLAP = 6; // адрес для настройки overlap const int EEPROM_ADDR_CPR = 8; // адрес для настройки CPR const int EEPROM_ADDR_DX = 12; // адрес для настройки DX const int EEPROM_ADDR_CHASE = 14; // адрес для настройки chase-mode const uint16_t EEPROM_MAGIC = 0xA5A5; // ===== START/STOP кнопка ===== const int STARTSTOP_PIN = 3; const unsigned long BTN_DEBOUNCE_MS = 50; bool startStopLastState = HIGH; unsigned long startStopLastChange = 0; bool startStopPressedHandled = false; // ===== Геркон ===== const int REED_PIN = 2; // ===== Глобальные переменные ===== volatile long sensor = 0; volatile long temp_sensor = 0; long prevSensor = 0; long prevCur = 0; // Для точной остановки unsigned long lastMoveMillis = 0; bool movedSinceLastSave = false; unsigned long lastLcdMillis = 0; enum Mode { MODE_NORMAL, MODE_ADD, MODE_MENU, MODE_OVERLAP_SELECT, MODE_DX_SELECT, // Новый режим: включение/выключение DX MODE_CHASE_SELECT, // Новый режим: chase-mode toggle MODE_CPR_CHANGE, // Новый режим: изменение CPR MODE_CALIB_CONFIRM, // Новый режим: подтверждение входа в калибровку MODE_CALIB_MANUAL_SELECT, // Новый режим: выбор импульсов MODE_CALIB_MANUAL_RUN, // Новый режим: движение CW/CCW MODE_CALIB_MANUAL_DONE // Новый режим: результат }; // ===== Menu table structure ===== struct MenuItem { const char* label; // указатель на строку в PROGMEM Mode nextMode; // режим, в который переходим }; // Строки меню в PROGMEM const char menu_overlap[] PROGMEM = "Overlap"; const char menu_change_azimuth[] PROGMEM = "Change Azimuth"; const char menu_calibration[] PROGMEM = "Calibration"; const char menu_dx[] PROGMEM = "DX Indicator"; const char menu_chase[] PROGMEM = "Chase Mode"; const char menu_cpr_change[] PROGMEM = "Change CPR"; const char menu_exit[] PROGMEM = "Exit"; const MenuItem menuItems[] PROGMEM = { {menu_overlap, MODE_OVERLAP_SELECT}, {menu_change_azimuth, MODE_ADD}, {menu_calibration, MODE_CALIB_MANUAL_SELECT}, {menu_dx, MODE_DX_SELECT}, {menu_chase, MODE_CHASE_SELECT}, {menu_cpr_change, MODE_CPR_CHANGE}, {menu_exit, MODE_NORMAL} }; const uint8_t MENU_COUNT = 7; // 7 пунктов меню Mode mode = MODE_NORMAL; // Overlap mode: 0 = North (360), 1 = South (180) int overlapMode = 0; int menuSelection = 1; // DX indicator enable/disable bool dxEnabled = true; int dxSelectValue = 1; // 1 = ON, 0 = OFF // Chase-mode enable/disable bool chaseModeEnabled = false; // Для сохранения overlap режима во время калибровки int savedOverlapForCalib = 0; int addAzimuth = 1; // Для подтверждения калибровки int calibConfirmSelection = 0; // 0 = Да, 1 = Нет bool suppressNextClick = false; bool movementActive = false; long movementTargetCounts = -1; int movementDir = 0; unsigned long savedDisplayMillis = 0; unsigned long lastSavedMillis = 0; // ===== Флаги направления ===== volatile bool dirRightFlag = false; volatile bool dirLeftFlag = false; // ===== Debug ===== volatile unsigned long reedEvents = 0; volatile unsigned long lastReedTime = 0; // ===== Отображаемый азимут ===== int displayAzimuth = 0; // ===== LCD cache ===== int lastDisplayedAzimuth = -1; int lastDisplayedNew = -1; int lastDisplayedAddAzimuth = -1; Mode lastDisplayedMode = MODE_NORMAL; int lastMenuSelection = -1; int lastOverlapMode = -1; bool lastDxEnabled = false; // ← добавляем для отслеживания изменений bool lastMovementActive = false; int lastMovementDir = 0; // ===== Calibration variables ===== long countsPerRev = COUNTS_PER_REV_DEFAULT; // ===== Manual calibration variables ===== long manualTargetCounts; // желаемое CPR // ===== DX direction table tuned for KO52mk, refined ===== struct Sector { int16_t azMin; int16_t azMax; char prefix[4]; // храним сам текст, НЕ указатель }; const Sector dirSectors[] PROGMEM = { // --- 0–50: Океания --- { 0, 4, "FO" }, { 5, 12, "KH6" }, { 16, 22, "E5" }, { 49, 54, "C21" }, // --- Азия / Тихий океан --- { 54, 66, "JA" }, { 59, 67, "UA9" }, { 68, 84, "BY" }, { 80, 88, "ZL" }, { 94, 98, "HS" }, {102, 108, "YB" }, {113, 121, "VU" }, // --- Middle East --- {140, 145, "A4" }, // --- Africa --- {167, 175, "5H" }, {186, 194, "ZS" }, // --- South America --- {238, 247, "PY" }, {248, 253, "CX" }, {254, 261, "CE" }, // --- Atlantic / Azores --- {268, 274, "CU" }, // --- Central Europe --- {269, 273, "DL" }, {259, 263, "OK" }, {246, 250, "OM" }, {278, 283, "SP" }, // --- UK --- {284, 288, "G" }, // --- North America --- {311, 319, "W" }, {331, 339, "VE" }, // --- Northern Europe --- {307, 313, "LY" }, {332, 342, "ES" }, {340, 349, "OH" }, {350, 359, "SM" } }; const uint8_t DIR_SECTORS_COUNT = sizeof(dirSectors) / sizeof(dirSectors[0]); void printDXPrefixToLCD(int az, LiquidCrystal_I2C& lcd) { for (uint8_t i = 0; i < DIR_SECTORS_COUNT; i++) { Sector s; memcpy_P(&s, &dirSectors[i], sizeof(Sector)); if (az >= s.azMin && az <= s.azMax) { lcd.print(s.prefix); return; } } lcd.print(" "); } // Совместимость для старого кода const char* getDXPrefix(int az) { static char prefixBuf[4]; for (uint8_t i = 0; i < DIR_SECTORS_COUNT; i++) { Sector s; memcpy_P(&s, &dirSectors[i], sizeof(Sector)); if (az >= s.azMin && az <= s.azMax) { strcpy(prefixBuf, s.prefix); return prefixBuf; } } strcpy(prefixBuf, " "); return prefixBuf; } // ===== 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; } void saveOverlapModeToEEPROM(int mode) { uint16_t magic; EEPROM.get(EEPROM_ADDR_MAGIC, magic); if (magic != EEPROM_MAGIC) EEPROM.put(EEPROM_ADDR_MAGIC, EEPROM_MAGIC); int stored; EEPROM.get(EEPROM_ADDR_OVERLAP, stored); if (stored != mode) EEPROM.put(EEPROM_ADDR_OVERLAP, mode); } bool loadOverlapModeFromEEPROM(int &outMode) { uint16_t magic; EEPROM.get(EEPROM_ADDR_MAGIC, magic); if (magic != EEPROM_MAGIC) return false; EEPROM.get(EEPROM_ADDR_OVERLAP, outMode); return true; } bool loadCPRFromEEPROM(long &outCPR) { uint16_t magic; EEPROM.get(EEPROM_ADDR_MAGIC, magic); if (magic != EEPROM_MAGIC) return false; EEPROM.get(EEPROM_ADDR_CPR, outCPR); return true; } // ===== DX setting EEPROM helpers ===== void saveDXSettingToEEPROM(bool enabled) { uint16_t magic; EEPROM.get(EEPROM_ADDR_MAGIC, magic); if (magic != EEPROM_MAGIC) EEPROM.put(EEPROM_ADDR_MAGIC, EEPROM_MAGIC); bool stored; EEPROM.get(EEPROM_ADDR_DX, stored); if (stored != enabled) EEPROM.put(EEPROM_ADDR_DX, enabled); } bool loadDXSettingFromEEPROM(bool &outEnabled) { uint16_t magic; EEPROM.get(EEPROM_ADDR_MAGIC, magic); if (magic != EEPROM_MAGIC) return false; EEPROM.get(EEPROM_ADDR_DX, outEnabled); return true; } // ===== Chase-mode EEPROM helpers ===== void saveChaseModeToEEPROM(bool enabled) { uint16_t magic; EEPROM.get(EEPROM_ADDR_MAGIC, magic); if (magic != EEPROM_MAGIC) EEPROM.put(EEPROM_ADDR_MAGIC, EEPROM_MAGIC); // Always save current value EEPROM.put(EEPROM_ADDR_CHASE, enabled); Serial.print(F("EEPROM: Chase-mode saved as ")); Serial.println(enabled ? F("ON") : F("OFF")); } bool loadChaseModeFromEEPROM(bool &outEnabled) { uint16_t magic; EEPROM.get(EEPROM_ADDR_MAGIC, magic); if (magic != EEPROM_MAGIC) return false; EEPROM.get(EEPROM_ADDR_CHASE, outEnabled); return true; } // ===== Conversions ===== int roundToMultipleOf5(int value) { // нормализуем в диапазон 1..360 if (value <= 0) value = 1; if (value > 360) value = ((value - 1) % 360) + 1; int mod = value % 5; int base = value - mod; if (mod == 1 || mod == 2) return base; // округляем вниз if (mod == 3 || mod == 4) return base + 5; // округляем вверх return base; // если делится на 5 — оставляем } // ===== LCD timer reset ===== void resetLcdTimer() { lastLcdMillis = 0; } int countsToAzimuth(long counts) { if (countsPerRev < 10) return 0; // защита от деления на 0 // нормализуем в диапазон 0..countsPerRev-1 long c = counts % countsPerRev; if (c < 0) c += countsPerRev; // классическое округление до ближайшего целого градуса long degTimes10 = (c * 3600L) / countsPerRev; // умножаем на 10, чтобы учесть десятые int az = (int)((degTimes10 + 5) / 10); // +5 → округление: 0.5 → вверх if (az == 0) az = 360; // 0° → 360° return az; } long azimuthToCounts(int az) { int a = az % 360; long c = ((long)a * countsPerRev + 180) / 360; if (az == 360) c = 0; c = c % countsPerRev; if (c < 0) c += countsPerRev; return c; } int shortestTurnDirection(int curAz, int tgtAz) { // === 1. Калибровка — всегда кратчайший путь === if (mode == MODE_CALIB_MANUAL_RUN) return (tgtAz - curAz > 0 ? +1 : -1); // === 2. Переводим в counts === long cur = sensor % countsPerRev; if (cur < 0) cur += countsPerRev; long tgt = azimuthToCounts(tgtAz); long diff = tgt - cur; // нормализация разницы в диапазон -CPR/2 .. +CPR/2 if (diff > countsPerRev / 2) diff -= countsPerRev; if (diff < -countsPerRev / 2) diff += countsPerRev; int dirShort = (diff > 0) ? +1 : -1; // === 3. Если overlap выключен — просто кратчайший путь === if (overlapMode == -1) return dirShort; // === 4. OVERLAP = NORTH (360°) === if (overlapMode == 0) { if (dirShort > 0) { // Если кратчайший путь CW, но мы в северной зоне и цель южная if (curAz >= 360 && tgtAz < 360) return -1; // CCW через 0° } else { // Если кратчайший путь CCW, но мы в северной зоне и цель северная if (curAz <= 0 && tgtAz > 0) return +1; // CW через 360° } return dirShort; } // === 5. OVERLAP = SOUTH (180°) === if (overlapMode == 1) { if (dirShort > 0) { // Если кратчайший путь CW, но мы в южной зоне и цель тоже южная if (curAz <= 180 && tgtAz > 180) return -1; // CCW через 0° } else { // Если кратчайший путь CCW, но мы в южной зоне и цель северная if (curAz >= 180 && tgtAz < 180) return +1; // CW через 360° } return dirShort; } return dirShort; } // ===== Target check ===== bool isTargetReached(int currentAz, int targetAz, int dir) { long curCounts = sensor % countsPerRev; if (curCounts < 0) curCounts += countsPerRev; long tgtCounts = azimuthToCounts(targetAz); long diff = tgtCounts - curCounts; // нормализация разницы в диапазон -CPR/2 .. +CPR/2 if (diff > countsPerRev / 2) diff -= countsPerRev; if (diff < -countsPerRev / 2) diff += countsPerRev; return diff == 0; } // ===== Interrupt control ===== void enableReedInterrupt() { attachInterrupt(digitalPinToInterrupt(REED_PIN), reedISR, FALLING); } void disableReedInterrupt() { detachInterrupt(digitalPinToInterrupt(REED_PIN)); } // ===== Motor control ===== void motorForward() { Serial.println(F("DIR = CW")); disableReedInterrupt(); relayWrite(RELAY_1, false); relayWrite(RELAY_2, false); delay(50); relayWrite(RELAY_2, true); dirRightFlag = true; dirLeftFlag = false; movementActive = true; enableReedInterrupt(); Serial.println(F("Motor: FORWARD (RELAY_2 ON)")); } void motorBackward() { Serial.println(F("DIR = CCW")); disableReedInterrupt(); relayWrite(RELAY_1, false); relayWrite(RELAY_2, false); delay(50); relayWrite(RELAY_1, true); dirRightFlag = false; dirLeftFlag = true; movementActive = true; enableReedInterrupt(); Serial.println(F("Motor: BACKWARD (RELAY_1 ON)")); } void motorStop() { disableReedInterrupt(); relayWrite(RELAY_1, false); relayWrite(RELAY_2, false); delay(50); movementActive = false; dirRightFlag = false; dirLeftFlag = false; Serial.println(F("Motor: STOP")); } // ===== Movement control ===== void stopMovement() { motorStop(); noInterrupts(); long cur = sensor; interrupts(); if (millis() - lastSavedMillis > SAVE_DEDUP_MS) { savePositionToEEPROM(cur); Serial.println(F("STOP: position saved")); } else { Serial.println(F("STOP: save skipped (recent)")); } savedDisplayMillis = millis(); movementDir = 0; // === ДОБАВЛЕНО: корректная обработка STOP в калибровке === if (mode == MODE_CALIB_MANUAL_RUN) { mode = MODE_CALIB_MANUAL_SELECT; resetLcdTimer(); return; // ← ВАЖНО: не выполнять код ниже } // === обычный режим === int azimuth = countsToAzimuth(cur); eb.counter = roundToMultipleOf5(azimuth); } void startMovementTo(int targetAzimuth) { if (movementActive) return; // Калибровка — отдельная логика if (mode == MODE_CALIB_MANUAL_RUN) { if (movementDir > 0) motorForward(); else motorBackward(); return; } noInterrupts(); long cur = sensor; interrupts(); // Переводим градусы → импульсы long targetCounts = azimuthToCounts(targetAzimuth); // Определяем направление с учетом overlap int curAz = countsToAzimuth(cur); int dir = shortestTurnDirection(curAz, targetAzimuth); movementTargetCounts = targetCounts; movementDir = dir; prevCur = cur; if (dir > 0) motorForward(); else motorBackward(); Serial.print(F("START movement: targetCounts=")); Serial.print(targetCounts); Serial.print(F(" cur=")); Serial.print(cur); Serial.print(F(" dir=")); Serial.println((dir > 0) ? F("CW") : F("CCW")); } // ===== ISR геркона ===== void reedISR() { static unsigned long lastUs = 0; unsigned long now = micros(); // Фильтр дребезга if (now - lastUs < 20000) return; // 20 мс unsigned long delta = now - lastUs; lastUs = now; if (!movementActive) return; // Направление int dir = 0; if (dirRightFlag) { if (sensor < 1000000) sensor++; dir = +1; } else if (dirLeftFlag) { if (sensor > -1000000) sensor--; dir = -1; } // Нормализация выполняется только при отображении, не здесь // Sensor debug disabled for production } // ===== Setup ===== void setup() { delay(10); Serial.begin(115200); Serial.println(F("=== CONTROLLER START ===")); // Reed sensor pinMode(REED_PIN, INPUT_PULLUP); // Relays pinMode(RELAY_1, OUTPUT); pinMode(RELAY_2, OUTPUT); if (RELAY_ACTIVE_HIGH) { relayWriteRaw(RELAY_1, LOW); relayWriteRaw(RELAY_2, LOW); } else { relayWriteRaw(RELAY_1, HIGH); relayWriteRaw(RELAY_2, HIGH); } //motorStop(); dirRightFlag = false; dirLeftFlag = false; movementActive = false; // START/STOP button pinMode(STARTSTOP_PIN, INPUT_PULLUP); startStopLastState = digitalRead(STARTSTOP_PIN); startStopLastChange = millis(); // LCD lcd.begin(); lcd.backlight(); lcd.clear(); lcd.setCursor(0,0); lcd.print(F("EW8ZO Rotator")); lcd.setCursor(0,1); lcd.print(F("VER. ")); lcd.print(FW_VERSION); delay(SPLASH_MS); lcd.clear(); // Encoder eb.setBtnLevel(LOW); eb.setClickTimeout(800); eb.setDebTimeout(8); eb.setHoldTimeout(1500); eb.setEncType(EB_STEP4_LOW); eb.setEncReverse(0); eb.setFastTimeout(10); eb.counter = 0; // Load saved position long saved = 0; if (loadPositionFromEEPROM(saved)) { noInterrupts(); sensor = saved; interrupts(); Serial.print("Loaded pos counts: "); Serial.println(saved); lastSavedMillis = millis(); } else { Serial.println(F("No saved pos in EEPROM")); } // Load overlap mode int savedOverlap = 0; if (loadOverlapModeFromEEPROM(savedOverlap)) { if (savedOverlap < -1 || savedOverlap > 1) overlapMode = -1; else overlapMode = savedOverlap; Serial.print("Loaded overlap mode: "); if (overlapMode == -1) Serial.println(F("OFF")); else if (overlapMode == 0) Serial.println(F("North 360")); else if (overlapMode == 1) Serial.println(F("South 180")); } else { overlapMode = 0; Serial.println(F("OFF")); } // Load CPR long savedCPR = 0; if (loadCPRFromEEPROM(savedCPR)) { if (savedCPR > 50 && savedCPR < 2000) { countsPerRev = savedCPR; Serial.print("Loaded CPR: "); Serial.println(countsPerRev); } else { countsPerRev = COUNTS_PER_REV_DEFAULT; Serial.println(F("Invalid CPR in EEPROM, using default")); } } else { countsPerRev = COUNTS_PER_REV_DEFAULT; Serial.println(F("No CPR in EEPROM, using default")); } // Load DX setting bool savedDX = true; if (loadDXSettingFromEEPROM(savedDX)) { dxEnabled = savedDX; dxSelectValue = dxEnabled ? 1 : 0; // синхронизируем Serial.print("Loaded DX setting: "); Serial.println(dxEnabled ? "ON" : "OFF"); } else { dxEnabled = true; dxSelectValue = 1; Serial.println(F("No DX setting in EEPROM, using default ON")); } // Load Chase-mode setting bool savedChase = false; if (loadChaseModeFromEEPROM(savedChase)) { chaseModeEnabled = savedChase; Serial.print(F("Loaded Chase-mode: ")); Serial.println(chaseModeEnabled ? F("ON") : F("OFF")); } else { chaseModeEnabled = false; Serial.println(F("No Chase-mode in EEPROM, using default OFF")); } // Initial azimuth noInterrupts(); int azimuth = countsToAzimuth(sensor); interrupts(); displayAzimuth = (azimuth / 5) * 5; if (displayAzimuth <= 0) displayAzimuth = 360; eb.counter = displayAzimuth; lastLcdMillis = millis(); Serial.print("Controller v"); Serial.print(FW_VERSION); Serial.println(F(" started")); Serial.print("Initial Azimuth="); Serial.print(azimuth); Serial.print(" displayAzimuth="); Serial.println(displayAzimuth); Serial.print("CPR="); Serial.println(countsPerRev); attachInterrupt(digitalPinToInterrupt(REED_PIN), reedISR, FALLING); } // ===== Main loop ===== void loop() { eb.tick(); // ===== Encoder events (single read per loop) ===== bool encTurn = eb.turn(); bool encClick = eb.click(); int encDir = eb.dir(); static int lastReed = HIGH; int cur = digitalRead(REED_PIN); if (cur != lastReed) { lastReed = cur; } // Global STOP block REMOVED - was causing race conditions // START/STOP кнопка — ТОЛЬКО STOP и ТОЛЬКО ВО ВРЕМЯ ДВИЖЕНИЯ 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 && movementActive) { stopMovement(); suppressNextClick = true; resetLcdTimer(); } startStopPressedHandled = true; } } // Вход в MENU при удержании энкодера if (eb.hold() && mode == MODE_NORMAL && !movementActive) { mode = MODE_MENU; menuSelection = 1; // Универсальный сброс - меню всегда начинается с пункта 1 suppressNextClick = false; resetLcdTimer(); } // Track actual sensor value for consistency prevSensor = sensor; // Отслеживание изменений sensor if (sensor != temp_sensor) { Serial.print("SENSOR CHANGE: "); Serial.print(temp_sensor); Serial.print(" -> "); Serial.println(sensor); // ЗАЩИТА ОТ ПЕРЕПОЛНЕНИЯ if (sensor > 1000000 || sensor < -1000000) { sensor = 0; Serial.println(F("Sensor overflow detected — reset to 0")); } temp_sensor = sensor; lastMoveMillis = millis(); movedSinceLastSave = true; } // ===== Movement target check (IMPULSE-BASED STOP) ===== if (movementActive && mode == MODE_NORMAL) { noInterrupts(); long cur = sensor; interrupts(); // === Chase-mode logic === if (chaseModeEnabled) { // обновляем цель на лету long newTarget = azimuthToCounts(displayAzimuth); // если цель изменилась — пересчитать направление if (newTarget != movementTargetCounts) { movementTargetCounts = newTarget; long diff = (movementTargetCounts - cur + countsPerRev) % countsPerRev; int newDir = (diff <= countsPerRev / 2) ? +1 : -1; if (newDir != movementDir) { Serial.print(F("CHASE: Direction change ")); Serial.print(movementDir > 0 ? F("CW") : F("CCW")); Serial.print(F(" → ")); Serial.println(newDir > 0 ? F("CW") : F("CCW")); motorStop(); movementDir = newDir; if (movementDir > 0) motorForward(); else motorBackward(); } } } // Нормализованная разница long diff = (movementTargetCounts - cur + countsPerRev) % countsPerRev; // CW if (movementDir > 0) { if (diff == 0 || prevCur > cur) { Serial.println(F("STOPPING: Target reached (CW, impulse-based)")); stopMovement(); resetLcdTimer(); } } // CCW else { if (diff == 0 || prevCur < cur) { Serial.println(F("STOPPING: Target reached (CCW, impulse-based)")); stopMovement(); resetLcdTimer(); } } prevCur = cur; } // ===== EEPROM save after movement ===== if (movedSinceLastSave && (millis() - lastMoveMillis >= SAVE_DELAY_MS)) { if (millis() - lastSavedMillis > SAVE_DEDUP_MS) { noInterrupts(); long toSave = sensor; interrupts(); savePositionToEEPROM(toSave); movedSinceLastSave = false; savedDisplayMillis = millis(); int newAz = countsToAzimuth(toSave); //eb.counter = roundToMultipleOf5(newAz); resetLcdTimer(); } else movedSinceLastSave = false; } // ===================================================================== // ========================= ENCODER TURN =============================== // ===================================================================== // ===== TURN (общий) ===== static unsigned long lastTurnMs = 0; if (encTurn) { unsigned long now = millis(); unsigned long dt = now - lastTurnMs; lastTurnMs = now; int dir = encDir; // ===== Special handling for chase-mode during movement ===== if (mode == MODE_NORMAL && movementActive && chaseModeEnabled) { displayAzimuth += dir * 5; while (displayAzimuth <= 0) displayAzimuth += 360; displayAzimuth = ((displayAzimuth - 1) % 360) + 1; eb.counter = displayAzimuth; resetLcdTimer(); } else if (mode == MODE_NORMAL) { if (!movementActive) { displayAzimuth += dir * 5; while (displayAzimuth <= 0) displayAzimuth += 360; displayAzimuth = ((displayAzimuth - 1) % 360) + 1; eb.counter = displayAzimuth; resetLcdTimer(); } } else if (mode == MODE_ADD) { int baseStep = 5; int mult = (dt < 30) ? 20 : (dt < 80 ? 5 : 1); int delta = dir * baseStep * mult; addAzimuth += delta; while (addAzimuth <= 0) addAzimuth += 360; addAzimuth = ((addAzimuth - 1) % 360) + 1; resetLcdTimer(); } else if (mode == MODE_MENU) { menuSelection += dir; if (menuSelection < 0) menuSelection = MENU_COUNT - 1; if (menuSelection >= MENU_COUNT) menuSelection = 0; suppressNextClick = false; resetLcdTimer(); } else if (mode == MODE_OVERLAP_SELECT) { if (dir > 0) { overlapMode++; if (overlapMode > 1) overlapMode = -1; } else { overlapMode--; if (overlapMode < -1) overlapMode = 1; } resetLcdTimer(); } else if (mode == MODE_DX_SELECT) { if (dir > 0) { dxSelectValue++; if (dxSelectValue > 1) dxSelectValue = 0; } else { dxSelectValue--; if (dxSelectValue < 0) dxSelectValue = 1; } dxEnabled = (dxSelectValue == 1); resetLcdTimer(); } else if (mode == MODE_CHASE_SELECT) { // Изменяем угол шагами по 5° (как везде) displayAzimuth += dir * 5; while (displayAzimuth <= 0) displayAzimuth += 360; displayAzimuth = ((displayAzimuth - 1) % 360) + 1; eb.counter = displayAzimuth; suppressNextClick = false; resetLcdTimer(); } else if (mode == MODE_CPR_CHANGE) { // Изменяем CPR шагами по 1 countsPerRev += dir; if (countsPerRev < 10) countsPerRev = 10; if (countsPerRev > 2000) countsPerRev = 2000; resetLcdTimer(); } } // ===================================================================== // =================== CALIBRATION: SELECT ============================== // ===================================================================== if (mode == MODE_CALIB_MANUAL_SELECT) { // Поворот энкодера — меняем CPR (только положительные значения) if (encTurn) { manualTargetCounts += encDir; if (manualTargetCounts < 10) manualTargetCounts = 10; if (manualTargetCounts > 2000) manualTargetCounts = 2000; resetLcdTimer(); } // Клик — запускаем движение if (encClick) { noInterrupts(); long cur = sensor; interrupts(); if (manualTargetCounts > cur) { movementDir = +1; motorForward(); Serial.println(F("CALIB: Moving CW to reach target CPR")); } else if (manualTargetCounts < cur) { movementDir = -1; motorBackward(); Serial.println(F("CALIB: Moving CCW to reach target CPR")); } else { // CPR == sensor → сразу DONE mode = MODE_CALIB_MANUAL_DONE; resetLcdTimer(); return; } movementActive = true; mode = MODE_CALIB_MANUAL_RUN; resetLcdTimer(); } } // ===================================================================== // =================== CALIBRATION: RUN ================================ // ===================================================================== if (mode == MODE_CALIB_MANUAL_RUN && movementActive) { noInterrupts(); long cur = sensor; interrupts(); // === Обычный RUN: едем к CPR === if (manualTargetCounts > 0) { // Отладка условия остановки bool shouldStop = false; if (movementDir > 0) { shouldStop = (cur >= manualTargetCounts); if (shouldStop) { Serial.print(F("CALIB STOP: CW cur=")); Serial.print(cur); Serial.print(F(" target=")); Serial.println(manualTargetCounts); } } else if (movementDir < 0) { shouldStop = (cur <= manualTargetCounts); if (shouldStop) { Serial.print(F("CALIB STOP: CCW cur=")); Serial.print(cur); Serial.print(F(" target=")); Serial.println(manualTargetCounts); } } if (shouldStop) { motorStop(); movementActive = false; mode = MODE_CALIB_MANUAL_DONE; resetLcdTimer(); } } // === Финальный CCW круг до нуля (только после сохранения CPR) === else if (manualTargetCounts == 0 && movementDir < 0) { // Останавливаемся точно на 0 // (обнаруживаем переход через ноль) static long prevCalibCur = cur; bool shouldStop = (cur > prevCalibCur) || (cur == 0); // Отладка финального круга if (shouldStop) { Serial.print(F("CALIB FINAL STOP: cur=")); Serial.print(cur); Serial.print(F(" prev=")); Serial.print(prevCalibCur); if (cur > prevCalibCur) Serial.println(F(" (wrap detected)")); else Serial.println(F(" (near zero)")); } prevCalibCur = cur; if (shouldStop) { motorStop(); movementActive = false; // Не сбрасываем sensor=0 в режиме калибровки! // Это делается только после сохранения CPR Serial.println(F("CALIB: Full circle completed, exiting to NORMAL mode")); mode = MODE_NORMAL; suppressNextClick = true; resetLcdTimer(); } } } /// ===================================================================== // =================== CALIBRATION: DONE =============================== // ===================================================================== if (mode == MODE_CALIB_MANUAL_DONE) { // Поворот энкодера → возвращаемся в SELECT if (encTurn) { manualTargetCounts += encDir; if (manualTargetCounts < 10) manualTargetCounts = 10; if (manualTargetCounts > 2000) manualTargetCounts = 2000; mode = MODE_CALIB_MANUAL_SELECT; resetLcdTimer(); } // Клик → сохранить CPR → CCW полный круг → ноль → NORMAL if (encClick) { // 1. Сохраняем CPR countsPerRev = manualTargetCounts; EEPROM.put(EEPROM_ADDR_CPR, countsPerRev); Serial.print(F("CALIB: CPR saved to EEPROM: ")); Serial.println(countsPerRev); // 2. Запускаем CCW полный круг movementDir = -1; movementActive = true; motorBackward(); Serial.println(F("CALIB: Starting CCW full circle to zero")); // 3. RUN будет вести до sensor=0 manualTargetCounts = 0; mode = MODE_CALIB_MANUAL_RUN; resetLcdTimer(); } } // ===== MENU click ===== if (mode == MODE_MENU && encClick && !suppressNextClick) { // Получаем режим из таблицы Mode nextMode; memcpy_P(&nextMode, &menuItems[menuSelection].nextMode, sizeof(Mode)); mode = nextMode; suppressNextClick = true; // Special handling for certain modes if (mode == MODE_ADD) { noInterrupts(); int azimuth = countsToAzimuth(sensor); interrupts(); addAzimuth = roundToMultipleOf5(azimuth); } else if (mode == MODE_CALIB_CONFIRM) { eb.reset(); // Показываем текущее CPR и спрашиваем подтверждение manualTargetCounts = countsPerRev; if (manualTargetCounts < 10 || manualTargetCounts > 2000) manualTargetCounts = 100; // Сброс sensor в 0 при отсутствии сохраненного CPR if (countsPerRev == COUNTS_PER_REV_DEFAULT) { noInterrupts(); sensor = 0; temp_sensor = 0; prevSensor = 0; interrupts(); Serial.println(F("CALIB: Reset sensor to 0 for initial calibration")); } calibConfirmSelection = 0; movementActive = false; movementDir = 0; movementTargetCounts = -1; } else if (mode == MODE_DX_SELECT) { lastDxEnabled = !dxEnabled; lastDisplayedMode = (Mode)-1; } else if (mode == MODE_NORMAL) { eb.counter = roundToMultipleOf5(eb.counter); } resetLcdTimer(); } // ===== CHASE_SELECT click ===== if (mode == MODE_CHASE_SELECT && encClick) { if (suppressNextClick) suppressNextClick = false; else { // По образцу DX - сохраняем текущее значение saveChaseModeToEEPROM(chaseModeEnabled); mode = MODE_NORMAL; suppressNextClick = true; Serial.print(F("Chase-mode: ")); Serial.println(chaseModeEnabled ? F("ON") : F("OFF")); // Force LCD update lastDisplayedMode = (Mode)-1; lastMenuSelection = -1; resetLcdTimer(); } } // ===== CPR_CHANGE click ===== if (mode == MODE_CPR_CHANGE && encClick) { if (suppressNextClick) suppressNextClick = false; else { // Сохраняем CPR в EEPROM EEPROM.put(EEPROM_ADDR_CPR, countsPerRev); mode = MODE_NORMAL; suppressNextClick = true; Serial.print(F("CPR saved: ")); Serial.println(countsPerRev); // Force LCD update lastDisplayedMode = (Mode)-1; lastMenuSelection = -1; resetLcdTimer(); } } // ===== OVERLAP_SELECT click ===== if (mode == MODE_OVERLAP_SELECT && encClick) { if (suppressNextClick) suppressNextClick = false; else { EEPROM.put(EEPROM_ADDR_OVERLAP, overlapMode); mode = MODE_NORMAL; suppressNextClick = true; resetLcdTimer(); } } // ===== CALIB_CONFIRM encoder turn ===== if (mode == MODE_CALIB_CONFIRM) { if (encTurn) { calibConfirmSelection += encDir; if (calibConfirmSelection < 0) calibConfirmSelection = 1; if (calibConfirmSelection > 1) calibConfirmSelection = 0; suppressNextClick = false; resetLcdTimer(); } // ===== CALIB_CONFIRM click ===== if (encClick) { if (suppressNextClick) suppressNextClick = false; else { if (calibConfirmSelection == 0) { // Да - переходим к настройке CPR mode = MODE_CALIB_MANUAL_SELECT; } else { // Нет - возвращаемся в меню mode = MODE_MENU; menuSelection = 4; // Остаемся на пункте калибровки } suppressNextClick = true; resetLcdTimer(); } } } // ===== DX_SELECT click ===== if (mode == MODE_DX_SELECT && encClick) { if (suppressNextClick) suppressNextClick = false; else { saveDXSettingToEEPROM(dxEnabled); // сохраняем в EEPROM только при клике mode = MODE_NORMAL; suppressNextClick = true; resetLcdTimer(); } } // ===== Normal mode encoder click ===== if (mode == MODE_NORMAL && encClick) { if (suppressNextClick) { suppressNextClick = false; return; } // === 1. ЕСЛИ ДВИЖЕНИЕ → STOP === if (movementActive) { stopMovement(); resetLcdTimer(); return; } // === 2. ПОКОЙ → возможный START === noInterrupts(); long curSensor = sensor; interrupts(); int curAz = countsToAzimuth(curSensor); int desiredAz = eb.counter; if (desiredAz <= 0) desiredAz = 1; if (desiredAz > 360) desiredAz = ((desiredAz - 1) % 360) + 1; // кратчайшая разница int diff = abs(desiredAz - curAz); if (diff > 180) diff = 360 - diff; // === ПОРОГ 5° === if (diff >= 5) { startMovementTo(desiredAz); } resetLcdTimer(); } // ===== ADD click ===== if (mode == MODE_ADD && encClick) { if (suppressNextClick) suppressNextClick = false; else { long newCounts = azimuthToCounts(addAzimuth); noInterrupts(); sensor = newCounts; interrupts(); if (millis() - lastSavedMillis > SAVE_DEDUP_MS) savePositionToEEPROM(newCounts); eb.counter = roundToMultipleOf5(addAzimuth); mode = MODE_NORMAL; suppressNextClick = true; savedDisplayMillis = millis(); resetLcdTimer(); displayAzimuth = addAzimuth; } } // === LCD cache reset when entering NORMAL === if (mode == MODE_NORMAL && lastDisplayedMode != MODE_NORMAL) { lastDisplayedMode = (Mode)-1; lastDisplayedAzimuth = -1; lastDisplayedNew = -1; lastDisplayedAddAzimuth = -1; } // === LCD cache reset when entering DX SELECT === if (mode == MODE_DX_SELECT && lastDisplayedMode != MODE_DX_SELECT) { lastDxEnabled = !dxEnabled; // гарантируем needUpdate } // ===================================================================== // =========================== LCD UPDATE =============================== // ===================================================================== if (millis() - lastLcdMillis >= LCD_UPDATE_MS) { lastLcdMillis = millis(); noInterrupts(); long curSensor = sensor; interrupts(); int azimuth = countsToAzimuth(curSensor); bool needUpdate = false; if (mode != lastDisplayedMode) { needUpdate = true; // lastDisplayedMode обновляется ПОСЛЕ успешной отрисовки LCD (строка 1270) } if (mode == MODE_MENU) { if (menuSelection != lastMenuSelection) { needUpdate = true; lastMenuSelection = menuSelection; } } else if (mode == MODE_OVERLAP_SELECT) { if (overlapMode != lastOverlapMode) { needUpdate = true; lastOverlapMode = overlapMode; } } else if (mode == MODE_DX_SELECT) { if (dxEnabled != lastDxEnabled) { // отслеживаем изменения needUpdate = true; lastDxEnabled = dxEnabled; // сохраняем предыдущее значение } } else if (mode == MODE_ADD) { if (addAzimuth != lastDisplayedAddAzimuth || azimuth != lastDisplayedAzimuth) { needUpdate = true; lastDisplayedAddAzimuth = addAzimuth; lastDisplayedAzimuth = azimuth; } } else if (mode == MODE_NORMAL) { if (eb.counter != lastDisplayedNew || azimuth != lastDisplayedAzimuth || movementActive != lastMovementActive || movementDir != lastMovementDir) { needUpdate = true; lastDisplayedNew = eb.counter; lastDisplayedAzimuth = azimuth; lastMovementActive = movementActive; lastMovementDir = movementDir; } } else { needUpdate = true; } if (needUpdate) { lcd.clear(); // MENU if (mode == MODE_MENU) { // Получаем указатель на строку из PROGMEM const char* labelPtr; memcpy_P(&labelPtr, &menuItems[menuSelection].label, sizeof(const char*)); // Копируем строку в буфер char labelBuffer[16]; // немного больше для безопасности strcpy_P(labelBuffer, labelPtr); lcd.setCursor(0,0); lcd.print(F("MENU:")); lcd.setCursor(0,1); lcd.print(F("> ")); lcd.print(labelBuffer); } // OVERLAP SELECT else if (mode == MODE_OVERLAP_SELECT) { lcd.setCursor(0,0); lcd.print(F("Overlap:")); lcd.setCursor(0,1); if (overlapMode == -1) lcd.print(F("OFF")); else if (overlapMode == 0) lcd.print(F("North 360")); else if (overlapMode == 1) lcd.print(F("South 180")); } // DX SELECT else if (mode == MODE_DX_SELECT) { lcd.setCursor(0,0); lcd.print(F("DX Indicator:")); lcd.setCursor(0,1); if (dxEnabled) lcd.print(F("ON")); else lcd.print(F("OFF")); } // CHASE SELECT else if (mode == MODE_CHASE_SELECT) { lcd.setCursor(0,0); lcd.print(F("Chase Mode:")); lcd.setCursor(0,1); if (chaseModeEnabled) lcd.print(F("ON")); else lcd.print(F("OFF")); } // CPR CHANGE else if (mode == MODE_CPR_CHANGE) { lcd.setCursor(0,0); lcd.print(F("Change CPR")); lcd.setCursor(0,1); lcd.print(F("Value: ")); lcd.print(countsPerRev); } // CALIBRATION CONFIRM else if (mode == MODE_CALIB_CONFIRM) { lcd.setCursor(0,0); lcd.print(F("Start calibration?")); lcd.setCursor(0,1); lcd.print(F("CPR: ")); lcd.print(manualTargetCounts); lcd.print(F(" ")); if (calibConfirmSelection == 0) lcd.print(F("YES")); else lcd.print(F("NO")); } // ADD else if (mode == MODE_ADD) { lcd.setCursor(0,0); lcd.print(F("New Azimuth:")); lcd.setCursor(0,1); lcd.print(addAzimuth); lcd.setCursor(5,1); lcd.print(F(" Cur:")); lcd.setCursor(11,1); lcd.print(azimuth); } else if (mode == MODE_CALIB_MANUAL_SELECT) { lcd.setCursor(0,0); lcd.print("Manual Calib"); lcd.setCursor(0,1); lcd.print("Impulses: "); lcd.print(manualTargetCounts); } else if (mode == MODE_CALIB_MANUAL_RUN) { lcd.setCursor(0,0); lcd.print(movementDir > 0 ? "Moving CW" : "Moving CCW"); lcd.setCursor(0,1); lcd.print("Impulses: "); lcd.print(sensor); } else if (mode == MODE_CALIB_MANUAL_DONE) { lcd.setCursor(0,0); lcd.print("CPR OK:"); lcd.setCursor(0,1); lcd.print(manualTargetCounts); lcd.print(" Click=Save"); } // NORMAL else if (mode == MODE_NORMAL) { // Строка 1: New: 156 > lcd.setCursor(0,0); lcd.print(F("New:")); lcd.setCursor(5,0); lcd.print(eb.counter); lcd.setCursor(11,0); if (movementActive) { if (movementDir > 0) lcd.print(F("CW-->")); else lcd.print(F("<-CCW")); } else lcd.print(F(" ")); // 2 пробела когда нет движения // Строка 2: Azimuth: 187 KH6 lcd.setCursor(0,1); lcd.print(F("Azimuth:")); lcd.setCursor(9,1); lcd.print(azimuth); // Очищаем 3 позиции для префикса (выравнивание по ширине) lcd.setCursor(14,1); lcd.print(F(" ")); if (dxEnabled) { lcd.setCursor(14,1); lcd.print(getDXPrefix(azimuth)); } } lastDisplayedMode = mode; } } }