IP 192.168.0.240 (можно сменить через настройки) v0.81 (примерно) {{:clip2net_menu_260117214628.jpeg?800|}} {{:clip2net_menu_260117214613.jpeg?800|}} #include #include #include #include #include #include // ===================================================== // === CONFIG: все константы устройства в одном месте === // ===================================================== // --- EEPROM --- const byte EEPROM_MAGIC = 0x42; const int EEPROM_ADDR_MAGIC = 0; const int EEPROM_ADDR_IP = 1; // 1..4 // --- Версия прошивки --- const float VERSION = 0.81; // --- CAT / сеть --- const unsigned long CAT_TIMEOUT = 7000; const unsigned long NET_TIMEOUT = 5000; // --- Пины энкодера --- const int ENC_A = 8; const int ENC_B = A1; const int ENC_BTN = 3; // --- Пины реле --- const int RELAYS[] = {4, 5, 6, 7}; const int RELAY_COUNT = 4; // --- Кнопка --- const unsigned long SHORT_PRESS = 100; const unsigned long LONG_PRESS = 3000; const unsigned long HOLD_TIME = 700; // --- Редактор IP --- const unsigned long EDITOR_CLICK_GUARD = 200; // --- Анимация CAT --- const int CAT_ANIM_STEPS = 5; // ===================================================== // === ГЛОБАЛЬНЫЕ ПЕРЕМЕННЫЕ (состояние устройства) === // ===================================================== // --- IP редактор --- bool ipEditMode = false; byte ipEdit[4]; int ipOctetIndex = 0; // --- LCD --- LiquidCrystal_I2C lcd(0x27, 16, 2); // --- Ethernet --- byte mac[] = { 0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0xED }; IPAddress ip(192, 168, 0, 240); unsigned int localPort = 12060; IPAddress effectiveIP = ip; EthernetUDP Udp; // --- CAT состояние --- unsigned long lastCatTime = 0; bool catActive = false; unsigned long lastPacketTime = 0; long lastFreq = 0; bool freqReceived = false; byte catAnimPos = 0; int catPacketCounter = 0; byte catDot[8] = { 0b00000, 0b00100, 0b01110, 0b01110, 0b01110, 0b00100, 0b00000, 0b00000 }; // --- Режимы --- enum Mode { AUTO, MANUAL }; Mode mode = AUTO; bool manualForced = false; // --- Диапазоны --- struct Band { const char* name; long from; long to; byte abcdCode; }; Band bands[] = { {"160M", 1800000, 2000000, 0b00000001}, {"80M", 3500000, 3800000, 0b00000010}, {"40M", 7000000, 7350000, 0b00000011}, {"20M", 14000000, 14350000, 0b00000101}, {"15M", 21000000, 21450000, 0b00000111}, {"10M", 28000000, 29700000, 0b00001001} }; const int BAND_COUNT = sizeof(bands) / sizeof(Band); // --- Текущее состояние диапазонов --- int currentBand = -1; int manualBandIndex = 0; // --- Энкодер --- int lastA = HIGH; // --- Кнопка --- bool buttonHeld = false; bool pressed = false; unsigned long pressStart = 0; bool ignoreNextEditorClick = false; // --- LCD обновление --- Mode lastMode = AUTO; int lastBandIndex = -1; long lastDisplayedFreq = 0; bool forceDisplayUpdate = false; int detectBand(long freq) { for (int i = 0; i < BAND_COUNT; i++) { if (freq >= bands[i].from && freq <= bands[i].to) return i; } return -1; } void setRelaysByABCD(byte abcdCode) { for (int i = 0; i < RELAY_COUNT; i++) { bool state = (abcdCode >> i) & 1; digitalWrite(RELAYS[i], state ? LOW : HIGH); } } void setRelaysByBand(int bandIndex) { if (bandIndex >= 0 && bandIndex < BAND_COUNT) setRelaysByABCD(bands[bandIndex].abcdCode); else setRelaysByABCD(0); } void disableAllRelays() { setRelaysByABCD(0); } void saveIPToEEPROM(IPAddress ipToSave) { EEPROM.update(EEPROM_ADDR_MAGIC, EEPROM_MAGIC); for (int i = 0; i < 4; i++) EEPROM.update(EEPROM_ADDR_IP + i, ipToSave[i]); } bool loadIPFromEEPROM(IPAddress &out) { if (EEPROM.read(EEPROM_ADDR_MAGIC) != EEPROM_MAGIC) return false; byte b[4]; for (int i = 0; i < 4; i++) b[i] = EEPROM.read(EEPROM_ADDR_IP + i); out = IPAddress(b[0], b[1], b[2], b[3]); return true; } void enterIpEditMode() { ipEditMode = true; for (int i = 0; i < 4; i++) ipEdit[i] = effectiveIP[i]; ipOctetIndex = 0; drawIpEditor(); ignoreNextEditorClick = true; } void drawIpEditor() { lcd.clear(); lcd.setCursor(0,0); lcd.print("SET IP:"); lcd.setCursor(0,1); for (int i = 0; i < 4; i++) { // подчёркивание перед редактируемым октетом if (i == ipOctetIndex) lcd.print("_"); else lcd.print(""); // ← НИЧЕГО НЕ ПЕЧАТАЕМ // вывод октета с ведущими нулями int val = ipEdit[i]; if (val < 100) lcd.print("0"); if (val < 10) lcd.print("0"); lcd.print(val); if (i < 3) lcd.print("."); } } void printFreqCompact(long f) { int MHz = f / 1000000; int kHz = (f / 1000) % 1000; int hundred = (f / 100) % 10; int tens = (f / 10) % 10; lcd.print(MHz); lcd.print("."); if (kHz < 100) lcd.print("0"); if (kHz < 10) lcd.print("0"); lcd.print(kHz); lcd.print("."); lcd.print(hundred); lcd.print(tens); } void runRelayTest() { lcd.clear(); lcd.setCursor(0,0); lcd.print("TEST RELAYS"); for (int i = 0; i < BAND_COUNT; i++) { // включаем реле setRelaysByBand(i); // выводим комбинацию ABCD lcd.setCursor(0,1); for (int b = 3; b >= 0; b--) lcd.print((bands[i].abcdCode >> b) & 1); delay(600); } disableAllRelays(); delay(300); forceDisplayUpdate = true; updateLCDIfNeeded(); } void updateLCDIfNeeded() { if (ipEditMode) return; bool changed = false; if (lastMode != mode) changed = true; if (mode != AUTO && lastBandIndex != manualBandIndex) changed = true; if (mode == AUTO && lastBandIndex != currentBand) changed = true; if (mode == AUTO && lastDisplayedFreq != lastFreq) changed = true; if (forceDisplayUpdate) { changed = true; forceDisplayUpdate = false; } if (!changed) return; updateTopLine(); lcd.setCursor(0,1); lcd.print(" "); lcd.setCursor(0,1); if (mode == AUTO) { lcd.print("[A] "); if (currentBand >= 0) lcd.print(bands[currentBand].name); else lcd.print("--"); lcd.print(" "); if (catActive) printFreqCompact(lastFreq); else lcd.print("NO DATA "); lastBandIndex = currentBand; lastDisplayedFreq = lastFreq; } else { lcd.print("[M] "); lcd.print(bands[manualBandIndex].name); lcd.print(" "); for (int i = 3; i >= 0; i--) lcd.print((bands[manualBandIndex].abcdCode >> i) & 1); lastBandIndex = manualBandIndex; } lastMode = mode; } void updateTopLine() { if (ipEditMode) return; lcd.setCursor(0,0); lcd.print(" "); lcd.setCursor(0,0); // === AUTO режим === if (mode == AUTO) { lcd.print("CAT "); if (catActive) { // 5-позиционная анимация for (int i = 0; i < 5; i++) { if (i == catAnimPos) lcd.write(byte(0)); // кастомная точка else lcd.print("."); } lcd.print(" "); // Показ реле if (currentBand >= 0) { for (int i = 3; i >= 0; i--) lcd.print((bands[currentBand].abcdCode >> i) & 1); } else { lcd.print("----"); } } else { // CAT пропал lcd.print("..... "); if (currentBand >= 0) { for (int i = 3; i >= 0; i--) lcd.print((bands[currentBand].abcdCode >> i) & 1); } else { lcd.print("----"); } } return; } // === MANUAL режим === lcd.print("CAT "); if (catActive) lcd.print("OK"); else lcd.print("--"); } void splashScreen() { lcd.clear(); lcd.setCursor(0,0); lcd.print("EW8ZO v"); lcd.print(VERSION, 2); lcd.setCursor(0,1); lcd.print("ABCD Decoder"); delay(2000); } void readUDP() { int packetSize = Udp.parsePacket(); if (!packetSize) return; char buf[200]; int len = Udp.read(buf, sizeof(buf) - 1); if (len <= 0) return; buf[len] = 0; // Любой пакет = связь жива lastPacketTime = millis(); lastCatTime = millis(); catActive = true; // Если MANUAL был включён автоматически (fallback), а CAT снова появился — возвращаемся в AUTO if (mode == MANUAL && !manualForced) { mode = AUTO; // Восстанавливаем реле по текущему диапазону if (currentBand >= 0) setRelaysByBand(currentBand); forceDisplayUpdate = true; updateLCDIfNeeded(); } // Двигаем CAT-анимацию каждые 2 пакета catPacketCounter++; if (catPacketCounter >= 2) { // ← каждые 2 пакета catPacketCounter = 0; catAnimPos = (catAnimPos + 1) % 5; updateTopLine(); } // Ищем char* f = strstr(buf, ""); if (f) { f += 6; lastFreq = atol(f) * 10; freqReceived = true; // ← ВОТ ЭТОТ if ТЕБЕ НУЖЕН int b = detectBand(lastFreq); if (b >= 0) currentBand = b; // В AUTO обновляем реле if (mode == AUTO && currentBand >= 0) setRelaysByBand(currentBand); } // Если нет — это heartbeat, ничего не трогаем updateLCDIfNeeded(); } void handleButtonPress() { pressed = true; pressStart = millis(); buttonHeld = false; } void handleButtonHold(unsigned long held) { // длинное → вход в редактор if (!ipEditMode && held >= LONG_PRESS) { enterIpEditMode(); pressed = false; buttonHeld = false; return; } // жест удержания if (!ipEditMode && !buttonHeld && held >= HOLD_TIME) { buttonHeld = true; } } void handleIpEditorClick(unsigned long pressTime) { static unsigned long lastEditorClick = 0; // игнорируем первый клик после входа в редактор if (ignoreNextEditorClick) { ignoreNextEditorClick = false; return; } // длинное → выход без сохранения if (pressTime >= LONG_PRESS) { ipEditMode = false; forceDisplayUpdate = true; updateLCDIfNeeded(); return; } // защита от двойного клика if (millis() - lastEditorClick < 200) return; lastEditorClick = millis(); // короткое → следующий октет if (pressTime >= 50) { ipOctetIndex++; if (ipOctetIndex > 3) { effectiveIP = IPAddress(ipEdit[0], ipEdit[1], ipEdit[2], ipEdit[3]); saveIPToEEPROM(effectiveIP); lcd.clear(); lcd.setCursor(0,0); lcd.print("IP SAVED"); lcd.setCursor(0,1); lcd.print(effectiveIP); delay(1200); ipEditMode = false; forceDisplayUpdate = true; updateLCDIfNeeded(); return; } drawIpEditor(); } } void toggleAutoManual() { if (mode == AUTO) { mode = MANUAL; manualForced = true; manualBandIndex = (currentBand >= 0 ? currentBand : 0); setRelaysByBand(manualBandIndex); } else { if (!catActive) { lcd.clear(); lcd.setCursor(0,0); lcd.print("NO CAT LINK"); lcd.setCursor(0,1); lcd.print("AUTO DISABLED"); delay(1200); forceDisplayUpdate = true; updateLCDIfNeeded(); return; } mode = AUTO; manualForced = false; if (currentBand >= 0) setRelaysByBand(currentBand); else disableAllRelays(); } forceDisplayUpdate = true; updateLCDIfNeeded(); } void handleButtonRelease(unsigned long pressTime) { // редактор if (ipEditMode) { handleIpEditorClick(pressTime); return; } // длинное → вход в редактор if (pressTime >= LONG_PRESS) { enterIpEditMode(); return; } // короткое → AUTO/MANUAL if (!buttonHeld && pressTime >= SHORT_PRESS) { toggleAutoManual(); } buttonHeld = false; } // // === КНОПКА С АНТИДРЕБЕЗГОМ И ЗАЩИТОЙ ОТ ЛОЖНЫХ СРАБАТЫВАНИЙ === // void handleButton() { static unsigned long lastCheck = 0; if (millis() - lastCheck < 30) return; lastCheck = millis(); bool btn = !digitalRead(ENC_BTN); // начало нажатия if (btn && !pressed) { handleButtonPress(); return; } // удержание if (btn && pressed) { unsigned long held = millis() - pressStart; handleButtonHold(held); return; } // отпускание if (!btn && pressed) { pressed = false; unsigned long pressTime = millis() - pressStart; handleButtonRelease(pressTime); return; } } // // === ЭНКОДЕР С ЗАЩИТОЙ ОТ ЛОЖНЫХ СРАБАТЫВАНИЙ === // void handleEncoder() { static unsigned long lastMove = 0; int A = digitalRead(ENC_A); if (A == lastA) return; // реагируем только на фронт A == LOW if (A != lastA && A == LOW) { bool clockwise = digitalRead(ENC_B); // === УДЕРЖАНИЕ КНОПКИ + ПОВОРОТ === if (buttonHeld && !ipEditMode) { if (clockwise) { runRelayTest(); } else { // резерв } buttonHeld = false; lastA = A; return; } // === РЕЖИМ IP-РЕДАКТОРА === if (ipEditMode) { if (millis() - lastMove < 80) { lastA = A; return; } lastMove = millis(); int val = ipEdit[ipOctetIndex]; if (clockwise) val++; else val--; if (val < 0) val = 255; if (val > 255) val = 0; ipEdit[ipOctetIndex] = val; drawIpEditor(); lastA = A; return; } // === MANUAL режим === if (mode == MANUAL) { if (millis() - lastMove < 50) { lastA = A; return; } lastMove = millis(); if (clockwise) manualBandIndex = (manualBandIndex + 1) % BAND_COUNT; else manualBandIndex = (manualBandIndex - 1 + BAND_COUNT) % BAND_COUNT; setRelaysByBand(manualBandIndex); forceDisplayUpdate = true; updateLCDIfNeeded(); } } lastA = A; } void checkCatTimeout() { // 1) Сбрасываем CAT независимо от режима if (catActive && millis() - lastCatTime > CAT_TIMEOUT) { catActive = false; // ОБНОВЛЯЕМ ПЕРВУЮ СТРОКУ updateTopLine(); } // 2) Логика fallback только в AUTO if (mode == AUTO && !catActive) { mode = MANUAL; manualForced = false; if (currentBand >= 0) { manualBandIndex = currentBand; } setRelaysByBand(manualBandIndex); forceDisplayUpdate = true; updateLCDIfNeeded(); } } void setup() { //Serial.begin(115200); delay(500); Serial.println("START DECODER"); // --- Пины энкодера --- pinMode(ENC_A, INPUT_PULLUP); pinMode(ENC_B, INPUT_PULLUP); pinMode(ENC_BTN, INPUT_PULLUP); // --- Пины реле --- for (int i = 0; i < RELAY_COUNT; i++) { pinMode(RELAYS[i], OUTPUT); digitalWrite(RELAYS[i], LOW); } disableAllRelays(); // --- LCD --- lcd.begin(); lcd.backlight(); // если есть сохранённый — подхватим effectiveIP = ip; loadIPFromEEPROM(effectiveIP); // Регистрируем кастомный символ CAT‑точки lcd.createChar(0, catDot); // Splash screen splashScreen(); // --- Ethernet --- Ethernet.begin(mac, effectiveIP); Udp.begin(localPort); // Первичное отображение IP (потом будет перерисовано updateTopLine) lcd.clear(); lcd.setCursor(0,0); lcd.print("IP:"); lcd.print(effectiveIP); lcd.setCursor(0,1); lcd.print("READY"); // --- Начальные значения логики --- currentBand = -1; catActive = false; manualBandIndex = 0; lastFreq = 0; lastPacketTime = millis(); disableAllRelays(); // Обновляем дисплей по новой логике forceDisplayUpdate = true; updateLCDIfNeeded(); // Инициализация энкодера (важно!) lastA = digitalRead(ENC_A); } void loop() { // === Всегда читаем UDP === readUDP(); // === Обработка энкодера и кнопки === handleEncoder(); handleButton(); // === Проверка таймаута CAT === checkCatTimeout(); // === Периодическое обновление дисплея === static unsigned long lastDisplayUpdate = 0; if (!ipEditMode && millis() - lastDisplayUpdate > 1000) { lastDisplayUpdate = millis(); updateLCDIfNeeded(); } delay(10); }