IP 192.168.0.240 (можно сменить через настройки)
v0.81 (примерно)
#include <SPI.h>
#include <Ethernet.h>
#include <EthernetUdp.h>
#include <Wire.h>
#include <LiquidCrystal_I2C.h>
#include <EEPROM.h>
// =====================================================
// === 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();
}
// Ищем <Freq>
char* f = strstr(buf, "<Freq>");
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);
}
// Если <Freq> нет — это 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);
}