rotator_control_code
код 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 <Arduino.h>
#include <EncButton.h>
#include <LiquidCrystal_I2C.h>
#include <EEPROM.h>
// ===== 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;
}
}
}
rotator_control_code.txt · Последнее изменение: — eu8t
