/*
1) პირველი რეჟიმია "ნორმალური" მოდა. კონტროლერის ჩართვისას, ვთქვათ,
ჩართულია პირველი ანტენა (შესბამისი ნომრის ლედ ნათურა ანთია RX-TX). არცერთი
TX ნათურა არ ანთია. ამ რეჟიმში მიმღები და გადამცემი ანტენა ერთიდაიგივეა.
კლავიატურაზე 1-დან-8 მდე აკრეფვით, ჩართავთ შესაბამის ანტენას, რომელიც
მიმღებიც (RX) და გადამცემიც (TX) ერთდოულად იქნება.
2) მეორე (RX Scan Mode) რეჟიმია, სადაც ხდება მიმღები (RX) ანტენის არჩევა.
ამ რეჟიმის გააქტიურებას ვახდენთ * ღილაკით კლავიატურიდან. ამავდროულად
ინთება TX ანტენის ლედ ნათურაც. TX ანტენა იქნება ის, რომელიც იყო ბოლო
მოქმედების დროს "ნორმალურ" რეჟიხში. ამ დროს სადაც არ უნდა გადართოთ
მიმღები ანტენა კლავიატურიდან, გადამცემი ანტენა, ptt-ს დაჭერით, ირთვება
მხოლოდ არჩეული გადამცემი TX ანტენა. რჟიმიდან გამოსვლა ხდება *-ზე ხელახალი
დაკლიკებით.
3) მესამე (TX Scan Mode) რეჟიმია, სადაც ხდება გადამცემი (TX) ანტენის არჩევა.
ამ რჟიმში შესვლა შესაძლებელია # ზე დაკლიკებით. ამ რეჟიმში RX ანტენა
ფიქსირებულია და ვახდენთ მხოლოდ TX ანტენის გადართვას. რეჟიმიდან გამოსვლა
ხდება #-ზე ხელახალი დაკლიკებით. (2)-დან (3) რეჟიმში, ან (3)-დან (2)-ში
გადასვლა ხდება ეგრევე * ან # ის დაკლიკებით.
4) არსებობს ასევე, Swap რეჟიმი, როცა არჩეული TX და RX ანტენებს შეგიძლიათ
შეუცვალოთ როლები და გადამცემი ანტენა გახდეს მიმღები, ხოლო მიმღები - გადამცემი,
9-ს ღილაკით. Swap რეჟიმი ჩართვადია მხოლოდ RX Scan Mode ან TX Scan Mode რეჟიმში.
Swap რეჟიმში შეუძლებელია სხვა ანტენის არჩევა ან შეცვლა, მხოლოდ არჩეული ორი ანტენა
ცვლის როლებს. Swap ის გამორთვა ხდება ისევ 9 ღილაკით, ან (RX Scan Mode)
რეჟიმის (*-ით), ან (TX Scan Mode) რეჟიმის (#-ით) გაუქმებით).
ანუ, SWAP რეჟიმიდან 3 გზაა: ა) 9, ბ) *, გ) #.
ა) SWAP-დან 9-ით გამოსვლა დაგაბრუნებს იმ რეჟიმში რომელიდანაც გადახვედი SWAP-ში.
ბ) SWAP-დან *-ით გამოსვლა გადაგიყვანს ეგრევე RX Scan Mode-ში.
გ) SWAP-დან #-ით გამოსვლა გადაგიყვანს ეგრევე TX Scan Mode-ში.
დ) გადასვლა (RX Scan Mode) -> (TX Scan Mode): TX-RX ანტენა ხდება ის,
რომელიც იყო RX (იმიტომ, რომ RX სკანირებას ახდენდი).
ე) გადასვლა (TX Scan Mode) -> (RX Scan Mode): TX-RX ანტენა ხდება ის,
რომელიც იყო TX (იმიტომ, რომ TX სკანირებას ახდენდი).
5) ანტენების შეზღუდვა: დავუშვათ 8 ანტენის ნაცვლად გვაქვს მხოლოდ 5 ანტენა:
1 დან 5 ის ჩათვლით დაკავებულია კონექტორები სვიჩზე. ამ დროს 6, 7, 8 კონექტორი ცარიელია.
შეგვიძლია შევზღუდოთ გადართვა 6, 7, 8 ანტენებზე შემდეგნაირად: ვკრებთ 005 კომბინაციას და
სისტემა გადავა 5 ანტენიან სვიჩის რეჟიმზე. ამ დროს შეუძლებელია 6, 7, 8 ანტენის ჩართვა.
შეზღუდვიდან გამოსვლის 2 გზაა: ა) 008 კომბინაცია, და ბ) სვიჩის გადატივრთვა, გამორთვა-ჩართვა.
ასევე, კლავიატურაზე მხოლოდ 0-ის აკრეფა რეაგირებას არ ახდენს; ხოლო 00 კომბინაციის მერე
კოდი გელოდება 2 წამი. ამ 2 წამში თუ არ მოხდა ანტენების რაოდენობის არჩევა, სისტემა
გამოდის ლოდინის რეჟიმიდან და უქმდება 00 კომბინაცია (რათა არ მოხდეს სისტემის ბლოკირება).
6) როდესაც PTT აქტიურია და ტრანსივერი/სვიჩი TX ზეა, იბლოკება რელეების გადართვა.
7) ** კომბინაცია უდრის # კომბინაციას.
8) რომელიმე ანტენაზე 3 წამზე მეტი ხნით დაკლიკებისას ხდება ამ
ანტენაზე TX აკრძალვის დადება. განმეორებით 3 წამზე მეტი ხნით დაწოლა
მოხსნის აკრძალვას.
9) 0 ზე 3 წამით დაწოლა შლის მეხსიერებას და წერს ქარხნულ ვერსიას: 0xFFFF
(იტერაციას უკეთებს ყველა 15 მეხსიერების სლოტს. წერს არაინიციალიზებული
მნიშვნელობას (0xFFFF) თითოეული სლოტის „ჯადოსნურ ბაიტში“, რაც ეფექტურად
აღადგენს გაშვების დროის ცვლადებს მათ ნაგულისხმევ მდგომარეობაში
(ანტენა 1 აქტიურია, მაქსიმუმ 8 ანტენა, სკანირების რეჟიმი არ არის).
დაუყოვნებლივ ინახავს ამ ნაგულისხმევ მდგომარეობას პირველ სლოტში,
ცვეთის დონის რეგულირების პროცესი იწყება თავიდან 1 ვერსიიდან.)
10) 000 კომბინაცია არის მეხსიერებაში შენახვა.
ავტორები: 4L7ZS, 4L4CR და 4L0VE.
--------------------------------
4x3 kaypad Black pinouts:
Pin_Number = Pad_RC
NC
1 = C2
2 = R1
3 = C1
4 = R4
5 = C3
6 = R3
7 = R2
NC
-------------- ---------------
Connections:
(Arduino) = (Pad_RC) = (Pad Kay_Number)
A0 = R1 = 2
A1 = R2 = 7
A2 = R3 = 6
A3 = R4 = 4
D3 = C1 = 3
D4 = C2 = 1
D5 = C3 = 5
-----------------------------
*/
#include <Keypad.h>
#include <Wire.h>
// I2C EEPROM device address (24C64)
#define EEPROM_ADDR 0x50
unsigned long bootTime = 0;
// EEPROM wear-leveling configuration adapted for 24C64 (8192 bytes)
// SLOT_SIZE = 17, SLOT_COUNT = 480 -> 8160 bytes used (safe under 8192)
const uint16_t EEPROM_START_ADDR = 0; // starting offset in external EEPROM
const uint16_t SLOT_COUNT = 480; // adjusted for 24C64 (480 * 17 = 8160)
const uint8_t SLOT_SIZE = 17; // bytes per slot (magic + ver(2) + csum + payload)
const uint8_t PAYLOAD_LEN = SLOT_SIZE - 4; // 13
// Manual save configuration
const unsigned long MANUAL_SAVE_TIMEOUT = 3000; // 3 seconds to complete "000" sequence
// Slot layout (total SLOT_SIZE bytes):
// [0] magic(1) = 0xA5
// [1..2] version (uint16_t little-endian)
// [3] checksum (xor of payload bytes)
// [4] maxAntennas (1)
// [5] activeRelay (1)
// [6] rxRelay (1)
// [7] txRelay (1)
// [8] flags (bits: bit0=rxScanMode, bit1=txScanMode, bit2=swapMode)
// [9..16] blockedAntennas[8] (1 byte each, 0/1)
const uint8_t SLOT_MAGIC = 0xA5;
// Shift register pins
const int latchPin1 = 6;
const int clockPin1 = 7;
const int dataPin1 = 8;
const int latchPin2 = 9;
const int clockPin2 = 10;
const int dataPin2 = 11;
const int statusLedPin = 13;
const int swapLedPin = 12;
const int pttPin = 2;
const int txScanLedPin = 0;
const int MaxAntLedPin = 1;
// State variables (runtime)
int activeRelay = 0;
int txRelay = 0;
int rxRelay = 0;
bool rxScanMode = false;
bool txScanMode = false;
bool swapMode = false;
int maxAntennas = 8;
// Array to track blocked antennas (true = blocked from TX)
bool blockedAntennas[8] = {false, false, false, false, false, false, false, false};
// Tracking changes
int lastRxRelay = -1;
int lastTxRelay = -1;
bool lastPttState = false;
bool lastRxScanMode = false;
bool lastTxScanMode = false;
bool lastSwapMode = false;
// Manual save tracking
String manualSaveBuffer = "";
unsigned long manualSaveStartTime = 0;
bool isManualSaveActive = false;
// Keypad configuration
const byte ROWS = 4;
const byte COLS = 3;
char keys[ROWS][COLS] = {
{'1','2','3'},
{'4','5','6'},
{'7','8','9'},
{'*','0','#'}
};
byte rowPins[ROWS] = {A0, A1, A2, A3};
byte colPins[COLS] = {3, 4, 5};
Keypad keypad = Keypad(makeKeymap(keys), rowPins, colPins, ROWS, COLS);
// ---------- Utility: pack/unpack and checksum ----------
uint8_t packFlags() {
uint8_t f = 0;
if (rxScanMode) f |= 0x01;
if (txScanMode) f |= 0x02;
if (swapMode) f |= 0x04;
return f;
}
void unpackFlags(uint8_t f) {
rxScanMode = (f & 0x01);
txScanMode = (f & 0x02);
swapMode = (f & 0x04);
}
uint8_t calcPayloadChecksum(const uint8_t *payload, uint8_t len) {
uint8_t c = 0;
for (uint8_t i = 0; i < len; i++) c ^= payload[i];
return c;
}
// ---------- Defaults and helpers ----------
void loadDefaults() {
maxAntennas = 8;
activeRelay = 0;
rxRelay = 0;
txRelay = 0;
rxScanMode = false;
txScanMode = false;
swapMode = false;
for (int i = 0; i < 8; i++) blockedAntennas[i] = false;
}
// ------------------------
// EEPROM performance & wear-leveling helpers
// ------------------------
// We keep a small RAM cache for the *current* payload and the latest slot index/version
// (storing all slots in RAM would exceed Nano's 2KB SRAM). This avoids scanning on every write.
uint8_t cachedPayload[PAYLOAD_LEN]; // cached payload from the best slot on startup
uint16_t cachedVersion = 0;
uint16_t cachedSlotIdx = 0xFFFF; // index of slot holding cachedVersion
// Page size for AT24C64
const uint8_t EEPROM_PAGE = 32;
// --- low-level 16-bit address reads/writes ---
uint8_t extEEPROM_readByte(uint16_t addr) {
Wire.beginTransmission((uint8_t)EEPROM_ADDR);
Wire.write((uint8_t)(addr >> 8)); // high byte
Wire.write((uint8_t)(addr & 0xFF)); // low byte
uint8_t err = Wire.endTransmission();
if (err != 0) return 0xFF;
Wire.requestFrom((uint8_t)EEPROM_ADDR, (uint8_t)1);
if (Wire.available()) return Wire.read();
return 0xFF;
}
// Polls device until it ACKs or timeout (non-blocking-friendly short loop)
bool extEEPROM_waitReady(uint16_t timeoutMs = 200) {
unsigned long start = millis();
while (millis() - start < timeoutMs) {
Wire.beginTransmission((uint8_t)EEPROM_ADDR);
uint8_t e = Wire.endTransmission();
if (e == 0) return true;
delay(5);
}
return false;
}
// Write a contiguous chunk that does not cross page boundary
bool extEEPROM_writeChunk(uint16_t addr, const uint8_t *data, uint8_t len) {
Wire.beginTransmission((uint8_t)EEPROM_ADDR);
Wire.write((uint8_t)(addr >> 8));
Wire.write((uint8_t)(addr & 0xFF));
for (uint8_t i = 0; i < len; i++) Wire.write(data[i]);
uint8_t err = Wire.endTransmission();
if (err != 0) {
// retry couple times
for (int r = 0; r < 3; r++) {
delay(5);
Wire.beginTransmission((uint8_t)EEPROM_ADDR);
Wire.write((uint8_t)(addr >> 8));
Wire.write((uint8_t)(addr & 0xFF));
for (uint8_t i = 0; i < len; i++) Wire.write(data[i]);
err = Wire.endTransmission();
if (err == 0) break;
}
}
// wait for internal write
extEEPROM_waitReady();
return (err == 0);
}
// Write up to 'len' bytes starting at 'addr', splitting across page boundaries as needed
bool extEEPROM_writePageSafe(uint16_t addr, const uint8_t *data, uint16_t len) {
uint16_t cur = addr;
uint16_t remaining = len;
const uint8_t *p = data;
while (remaining) {
uint8_t pageOffset = cur % EEPROM_PAGE;
uint8_t canWrite = EEPROM_PAGE - pageOffset; // bytes until page end
uint8_t toWrite = (remaining < canWrite) ? remaining : canWrite;
if (!extEEPROM_writeChunk(cur, p, toWrite)) return false;
cur += toWrite;
p += toWrite;
remaining -= toWrite;
}
return true;
}
// Read multiple bytes (sequential) from ext EEPROM into buffer.
void extEEPROM_readBuffer(uint16_t addr, uint8_t *buf, uint16_t len) {
uint16_t remaining = len;
uint16_t cur = addr;
uint8_t *p = buf;
while (remaining) {
uint8_t toRead = (remaining > 32) ? 32 : (uint8_t)remaining; // Wire buffer limit ~32
Wire.beginTransmission((uint8_t)EEPROM_ADDR);
Wire.write((uint8_t)(cur >> 8));
Wire.write((uint8_t)(cur & 0xFF));
if (Wire.endTransmission() != 0) {
for (uint16_t i = 0; i < remaining; i++) p[i] = 0xFF;
return;
}
Wire.requestFrom((uint8_t)EEPROM_ADDR, toRead);
uint8_t i = 0;
while (Wire.available() && i < toRead) p[i++] = Wire.read();
p += toRead;
cur += toRead;
remaining -= toRead;
}
}
// Update buffer by writing full slot at once (we use page-safe writes)
bool extEEPROM_writeSlotBuffer(uint16_t addr, const uint8_t *buf, uint8_t len) {
// We will write the entire slot buffer (len bytes) using page-safe writes
return extEEPROM_writePageSafe(addr, buf, len);
}
// ------------------------
// EEPROM Clear Function for 24C64WP
// ------------------------
void clearEEPROM() {
// Clear all slots by writing 0xFF to the magic byte
for (uint16_t s = 0; s < SLOT_COUNT; s++) {
uint32_t base = (uint32_t)EEPROM_START_ADDR + (uint32_t)s * SLOT_SIZE;
// Read the current slot to check if it's already 0xFF (to reduce write wear)
uint8_t currentMagic = extEEPROM_readByte((uint16_t)base);
if (currentMagic != 0xFF) {
// Write 0xFF to just the magic byte to invalidate this slot
uint8_t clearByte = 0xFF;
extEEPROM_writeChunk((uint16_t)base, &clearByte, 1);
}
}
// Reset runtime cache
cachedSlotIdx = 0xFFFF;
cachedVersion = 0;
// Load default values
loadDefaults();
// Force a save of the default state to slot 0
saveStateToEEPROM();
}
// ------------------------
// Slot read/write helpers (using 16-bit indices)
// ------------------------
bool readSlot(uint16_t slotIndex, uint8_t *slotBuf) {
if (slotIndex >= SLOT_COUNT) return false;
uint32_t base = (uint32_t)EEPROM_START_ADDR + (uint32_t)slotIndex * SLOT_SIZE;
if (base + SLOT_SIZE - 1 > 8191u) return false;
extEEPROM_readBuffer((uint16_t)base, slotBuf, SLOT_SIZE);
return true;
}
// Immediate write of a full slot (used during commit). This writes the entire SLOT_SIZE.
bool writeSlotImmediate(uint16_t slotIndex, const uint8_t *slotBuf) {
if (slotIndex >= SLOT_COUNT) return false;
uint32_t base = (uint32_t)EEPROM_START_ADDR + (uint32_t)slotIndex * SLOT_SIZE;
if (base + SLOT_SIZE - 1 > 8191u) return false;
return extEEPROM_writeSlotBuffer((uint16_t)base, slotBuf, SLOT_SIZE);
}
// ------------------------
// Wear-leveling load/save for entire state (optimized for Nano)
// ------------------------
bool stateLoaded = false;
void loadStateFromEEPROM() {
// Check that I2C EEPROM is present; if not, use defaults
Wire.beginTransmission((uint8_t)EEPROM_ADDR);
uint8_t err = Wire.endTransmission();
if (err != 0) {
loadDefaults();
stateLoaded = true;
// Make sure cachedSlotIdx is valid
cachedSlotIdx = 0xFFFF;
cachedVersion = 0;
return;
}
// Scan slots once to find best slot/version and cache payload
uint16_t bestVersion = 0;
bool found = false;
uint8_t buf[SLOT_SIZE];
uint16_t bestIdx = 0xFFFF;
for (uint16_t s = 0; s < SLOT_COUNT; s++) {
if (!readSlot(s, buf)) continue;
if (buf[0] != SLOT_MAGIC) continue;
uint16_t ver = (uint16_t)buf[1] | ((uint16_t)buf[2] << 8);
if (ver == 0) continue; // Version 0 is invalid/uninitialized
uint8_t checksum = buf[3];
uint8_t comp = calcPayloadChecksum(&buf[4], PAYLOAD_LEN);
if (checksum != comp) continue;
if (!found || ver > bestVersion) {
bestVersion = ver;
bestIdx = s;
memcpy(cachedPayload, &buf[4], PAYLOAD_LEN);
found = true;
}
}
if (!found) {
// No valid stored state — use defaults
loadDefaults();
cachedSlotIdx = 0xFFFF;
cachedVersion = 0;
stateLoaded = true;
return;
}
// unpack payload
uint8_t payloadIdx = 0;
uint8_t ee_maxAntennas = cachedPayload[payloadIdx++];
uint8_t ee_activeRelay = cachedPayload[payloadIdx++];
uint8_t ee_rxRelay = cachedPayload[payloadIdx++];
uint8_t ee_txRelay = cachedPayload[payloadIdx++];
uint8_t ee_flags = cachedPayload[payloadIdx++];
bool ee_blockedAnt[8];
for (uint8_t i = 0; i < 8; i++) ee_blockedAnt[i] = (cachedPayload[payloadIdx++] == 1);
// validate ranges
bool dataValid = (ee_maxAntennas >= 1 && ee_maxAntennas <= 8 &&
ee_activeRelay < 8 && ee_rxRelay < 8 && ee_txRelay < 8);
if (!dataValid) {
loadDefaults();
cachedSlotIdx = 0xFFFF;
cachedVersion = 0;
stateLoaded = true;
return;
}
// load into runtime
maxAntennas = ee_maxAntennas;
activeRelay = ee_activeRelay;
rxRelay = ee_rxRelay;
txRelay = ee_txRelay;
unpackFlags(ee_flags);
for (uint8_t i = 0; i < 8; i++) blockedAntennas[i] = ee_blockedAnt[i];
cachedVersion = bestVersion;
cachedSlotIdx = bestIdx;
stateLoaded = true;
}
// Build payload from runtime into buffer
void packPayload(uint8_t *outPayload) {
uint8_t idx = 0;
outPayload[idx++] = (uint8_t)maxAntennas;
outPayload[idx++] = (uint8_t)activeRelay;
outPayload[idx++] = (uint8_t)rxRelay;
outPayload[idx++] = (uint8_t)txRelay;
outPayload[idx++] = packFlags();
for (uint8_t i = 0; i < 8; i++) outPayload[idx++] = blockedAntennas[i] ? 1 : 0;
}
// Save current state to EEPROM using wear-leveling
void saveStateToEEPROM() {
// Build current payload
uint8_t payload[PAYLOAD_LEN];
packPayload(payload);
// Compute checksum
uint8_t checksum = calcPayloadChecksum(payload, PAYLOAD_LEN);
// If we have cached existing payload and it's identical, skip write
if (cachedSlotIdx != 0xFFFF) {
bool same = true;
for (uint8_t i = 0; i < PAYLOAD_LEN; i++) {
if (cachedPayload[i] != payload[i]) { same = false; break; }
}
if (same) {
return; // nothing to do
}
}
// Determine next slot to write
uint16_t nextSlot = 0;
uint16_t currentBestVersion = cachedVersion;
if (cachedSlotIdx != 0xFFFF) {
nextSlot = (cachedSlotIdx + 1) % SLOT_COUNT;
} else {
// find first empty or use slot 0
bool found = false;
uint8_t buf[SLOT_SIZE];
for (uint16_t s = 0; s < SLOT_COUNT; s++) {
if (!readSlot(s, buf)) continue;
if (buf[0] != SLOT_MAGIC) { nextSlot = s; found = true; break; }
}
if (!found) nextSlot = 0;
}
// prepare slot buffer
uint8_t slotBuf[SLOT_SIZE];
memset(slotBuf, 0xFF, SLOT_SIZE);
slotBuf[0] = SLOT_MAGIC;
uint16_t newVersion = (uint16_t)(currentBestVersion + 1);
if (newVersion == 0) newVersion = 1; // Prevent version 0 after rollover, as 0 is used for default/empty
slotBuf[1] = (uint8_t)(newVersion & 0xFF);
slotBuf[2] = (uint8_t)((newVersion >> 8) & 0xFF);
slotBuf[3] = checksum;
for (uint8_t i = 0; i < PAYLOAD_LEN; i++) slotBuf[4 + i] = payload[i];
// Write full slot (page-safe)
bool ok = writeSlotImmediate(nextSlot, slotBuf);
if (ok) {
// update cache
cachedSlotIdx = nextSlot;
cachedVersion = newVersion;
memcpy(cachedPayload, payload, PAYLOAD_LEN);
}
}
// ------------------------
// Manual Save Functions
// ------------------------
void startManualSave() {
isManualSaveActive = true;
manualSaveBuffer = "";
manualSaveStartTime = millis();
}
void processManualSave(char key) {
if (!isManualSaveActive) {
if (key == '0') {
startManualSave();
manualSaveBuffer += key;
}
return;
}
// Check timeout
if (millis() - manualSaveStartTime > MANUAL_SAVE_TIMEOUT) {
isManualSaveActive = false;
manualSaveBuffer = "";
return;
}
if (key == '0') {
manualSaveBuffer += key;
// Check if we have "000" sequence
if (manualSaveBuffer == "000") {
// Perform save
saveStateToEEPROM();
// Provide visual feedback - blink status LED 3 times
for (int i = 0; i < 3; i++) {
digitalWrite(statusLedPin, HIGH);
delay(200);
digitalWrite(statusLedPin, LOW);
delay(200);
}
// Reset manual save state
isManualSaveActive = false;
manualSaveBuffer = "";
}
} else {
// Any non-zero key cancels manual save
isManualSaveActive = false;
manualSaveBuffer = "";
}
}
// ------------------------
// Keypad event for long press
// ------------------------
void keypadEvent(KeypadEvent key) {
switch (keypad.getState()) {
case HOLD:
if (key >= '1' && key <= '8') {
int antenna = key - '1';
if (antenna >= 0 && antenna < 8) {
toggleAntennaBlock(antenna);
}
} else if (key == '0') {
// Long press on '0' to clear EEPROM
clearEEPROM();
// Provide visual feedback - blink status LED 3 times
for (int i = 0; i < 3; i++) {
digitalWrite(statusLedPin, HIGH);
delay(200);
digitalWrite(statusLedPin, LOW);
delay(200);
}
}
break;
}
}
// --- Global variables for non-blocking blinking ---
unsigned long lastBlinkTime = 0;
bool isBlinkingNonBlocking = false;
int blinkStep = 0;
int antennaToBlink = -1;
const int BLINK_ON_TIME = 300; // clearly visible
const int BLINK_OFF_TIME = 150; // quick fade-out
const int BLINK_COUNT = 3; // number of blinks
/////////////////////////UART
char serialKey = 0;
bool hasSerialKey = false;
// Add this function to execute short key presses
void executeKeyPress(char key) {
Serial.print("Short press: ");
Serial.println(key);
if (key >= '1' && key <= '8') {
int antenna = key - '1';
if (antenna >= 0 && antenna < maxAntennas) {
if (rxScanMode || txScanMode) {
setScanAntenna(antenna);
} else {
setActiveAntenna(antenna);
}
}
} else if (key == '9') {
toggleSwapMode();
} else if (key == '*') {
static unsigned long lastStarPressTime = 0;
unsigned long now = millis();
if (now - lastStarPressTime < 500) {
toggleTxScanMode();
} else {
toggleRxScanMode();
}
lastStarPressTime = now;
} else if (key == '#') {
toggleTxScanMode();
} else if (key == '0') {
// Single '0' press - no action as per your requirements
Serial.println("Single 0 pressed - no action");
}
}
// Add this function to execute long key presses
void executeLongPress(char key) {
Serial.print("LONG PRESS detected on: ");
Serial.println(key);
if (key >= '1' && key <= '8') {
int antenna = key - '1';
if (antenna >= 0 && antenna < 8) {
toggleAntennaBlock(antenna);
Serial.print("Toggled block on antenna ");
Serial.println(antenna + 1);
}
} else if (key == '0') {
clearEEPROM();
Serial.println("EEPROM cleared via UART long press");
} else {
Serial.println("No long press action defined for this key");
}
}
//////////////////////////
// ------------------------
// Serial Command Handler
// ------------------------
void handleSerialCommands() {
if (Serial.available() > 0) {
String command = Serial.readStringUntil('\n');
command.trim();
// Handle "000" combination for manual save via UART
if (command == "000") {
saveStateToEEPROM();
Serial.println("Manual save via UART: EEPROM saved successfully!");
// Provide visual feedback
for (int i = 0; i < 3; i++) {
digitalWrite(statusLedPin, HIGH);
delay(200);
digitalWrite(statusLedPin, LOW);
delay(200);
}
return;
}
// Handle multi-digit antenna restriction commands (like "005")
if (command.length() == 3 && command.startsWith("00") && isDigit(command[2])) {
int maxAnt = command[2] - '0';
if (maxAnt >= 1 && maxAnt <= 8) {
// Simulate the key sequence: 0, 0, 5
serialKey = '0';
hasSerialKey = true;
delay(200); // Small delay between key presses
serialKey = '0';
hasSerialKey = true;
delay(200);
serialKey = command[2]; // the last digit (5)
hasSerialKey = true;
Serial.print("Setting max antennas to: ");
Serial.println(maxAnt);
maxAntennas = maxAnt;
updateMaxAntLed();
startNonBlockingBlink(maxAntennas - 1);
}
return;
}
// ===== METHOD 2: LONG PRESS WITH COLON FORMAT =====
// Handle long press commands (format: "LONG:1", "LONG:2", etc.)
if (command.startsWith("LONG:")) {
String keyStr = command.substring(5);
if (keyStr.length() == 1) {
char key = keyStr[0];
executeLongPress(key);
} else {
Serial.println("Invalid LONG command format. Use: LONG:X where X is 0-8");
}
return;
}
// ===== METHOD 4: SIMPLE LONG PRESS FORMAT =====
// Handle long press commands (format: "LONG1", "LONG2", etc.)
if (command.startsWith("LONG") && command.length() == 5) {
char key = command[4];
if (key >= '0' && key <= '8') {
executeLongPress(key);
} else {
Serial.println("Invalid LONG command. Use: LONG0 to LONG8");
}
return;
}
// Handle single character commands by simulating keypad input
if (command.length() == 1) {
char key = command[0];
// Simulate keypad press for the existing handleKeys() function
if (key >= '1' && key <= '8' || key == '*' || key == '#' || key == '9' || key == '0') {
executeKeyPress(key);
}
}
// Keep STATUS command
else if (command == "STATUS" || command == "status") {
// Print status
Serial.println("=== Antenna Controller Status ===");
Serial.print("Active Antenna: "); Serial.println(activeRelay + 1);
Serial.print("RX Antenna: "); Serial.println(rxRelay + 1);
Serial.print("TX Antenna: "); Serial.println(txRelay + 1);
Serial.print("RX Scan Mode: "); Serial.println(rxScanMode ? "ON" : "OFF");
Serial.print("TX Scan Mode: "); Serial.println(txScanMode ? "ON" : "OFF");
Serial.print("Swap Mode: "); Serial.println(swapMode ? "ON" : "OFF");
Serial.print("Max Antennas: "); Serial.println(maxAntennas);
Serial.print("Blocked Antennas: ");
for (int i = 0; i < 8; i++) {
if (blockedAntennas[i]) {
Serial.print(i + 1);
Serial.print(" ");
}
}
Serial.println();
}
}
}
// ------------------------
// Setup
// ------------------------
void setup() {
bootTime = millis();
Wire.begin(); // initialize I2C for external EEPROM communication
//Wire.setClock(100000);
Serial.begin(9600); // Initialize serial communication at 9600 baud
pinMode(latchPin1, OUTPUT);
pinMode(clockPin1, OUTPUT);
pinMode(dataPin1, OUTPUT);
pinMode(latchPin2, OUTPUT);
pinMode(clockPin2, OUTPUT);
pinMode(dataPin2, OUTPUT);
pinMode(pttPin, INPUT);
pinMode(statusLedPin, OUTPUT);
pinMode(swapLedPin, OUTPUT);
digitalWrite(latchPin1, LOW);
digitalWrite(clockPin1, LOW);
digitalWrite(dataPin1, LOW);
digitalWrite(latchPin2, LOW);
digitalWrite(clockPin2, LOW);
digitalWrite(dataPin2, LOW);
pinMode(txScanLedPin, OUTPUT);
digitalWrite(txScanLedPin, LOW);
pinMode(MaxAntLedPin, OUTPUT);
digitalWrite(MaxAntLedPin, LOW);
keypad.addEventListener(keypadEvent);
keypad.setHoldTime(3000);
// Load state from EEPROM (wear-leveled)
delay(100);
loadStateFromEEPROM();
updateAntennas();
updateMaxAntLed();
digitalWrite(statusLedPin, rxScanMode ? HIGH : LOW);
digitalWrite(txScanLedPin, txScanMode ? HIGH : LOW);
digitalWrite(swapLedPin, swapMode ? HIGH : LOW);
}
// ------------------------
// Loop and other functions
// ------------------------
void loop() {
if (!stateLoaded) return;
handleKeys();
checkPTT();
updateAntennas();
handleNonBlockingBlink();
handleSerialCommands(); // Line to check for serial commands
bool currentPtt = digitalRead(pttPin);
int currentRx = (rxScanMode || txScanMode) ? rxRelay : activeRelay;
int currentTx = (rxScanMode || txScanMode) ? txRelay : activeRelay;
if (currentRx != lastRxRelay || currentTx != lastTxRelay ||
currentPtt != lastPttState || rxScanMode != lastRxScanMode ||
txScanMode != lastTxScanMode || swapMode != lastSwapMode) {
lastRxRelay = currentRx;
lastTxRelay = currentTx;
lastPttState = currentPtt;
lastRxScanMode = rxScanMode;
lastTxScanMode = txScanMode;
lastSwapMode = swapMode;
}
}
// The blinking logic without blocking the program
void startNonBlockingBlink(int antenna) {
antennaToBlink = antenna;
blinkStep = 0;
isBlinkingNonBlocking = true;
lastBlinkTime = millis();
}
void blinkTxIndicator(int antenna) {
// Start non-blocking blink instead of blocking delays
startNonBlockingBlink(antenna);
}
void handleNonBlockingBlink() {
if (!isBlinkingNonBlocking) return;
unsigned long now = millis();
bool isOnStep = (blinkStep % 2 == 0);
// Select proper timing
int interval = isOnStep ? BLINK_ON_TIME : BLINK_OFF_TIME;
if (now - lastBlinkTime < interval) return;
lastBlinkTime = now;
// Set LED state
digitalWrite(latchPin2, LOW);
if (isOnStep && antennaToBlink >= 0) {
shiftOut(dataPin2, clockPin2, MSBFIRST, (1 << antennaToBlink));
} else {
shiftOut(dataPin2, clockPin2, MSBFIRST, 0);
}
digitalWrite(latchPin2, HIGH);
blinkStep++;
// Finished all blinks?
if (blinkStep >= BLINK_COUNT * 2) {
isBlinkingNonBlocking = false;
antennaToBlink = -1;
updateLatch2(); // restore normal display
}
}
//////////////////End of Non Blocking blinking
void handleKeys() {
// Increased startup delay to prevent phantom key detection
if (millis() - bootTime < 1000) return;
static String inputBuffer = "";
static unsigned long zeroPressTime = 0;
static unsigned long lastStarPressTime = 0;
char key = keypad.getKey();
if (!key) {
if (inputBuffer == "00" && (millis() - zeroPressTime > 2000)) {
inputBuffer = "";
}
return;
}
// Add key debounce check
static unsigned long lastKeyTime = 0;
if (millis() - lastKeyTime < 50) { // 50ms debounce
return;
}
lastKeyTime = millis();
// Process manual save first (handles "000" sequence)
processManualSave(key);
if (inputBuffer == "00" && isDigit(key)) {
processKey(key, inputBuffer, zeroPressTime, lastStarPressTime);
return;
}
if (digitalRead(pttPin) == HIGH) {
if (key) {
delay(1000);
}
return;
}
if (key >= '1' && key <= '8') {
int antenna = key - '1';
if (antenna >= 0 && antenna < maxAntennas) {
if (rxScanMode || txScanMode) {
setScanAntenna(antenna);
} else {
setActiveAntenna(antenna);
}
}
if (inputBuffer == "0") inputBuffer = "";
return;
}
else {
processKey(key, inputBuffer, zeroPressTime, lastStarPressTime);
}
}
void processKey(char key, String &inputBuffer, unsigned long &zeroPressTime, unsigned long &lastStarPressTime) {
if (key == '0') {
if (inputBuffer == "") {
inputBuffer = "0";
} else if (inputBuffer == "0") {
inputBuffer = "00";
zeroPressTime = millis();
}
return;
}
if (inputBuffer == "00" && isDigit(key)) {
int newMax = key - '0';
if (newMax >= 1 && newMax <= 8) {
maxAntennas = newMax;
updateMaxAntLed();
startNonBlockingBlink(maxAntennas - 1);
if (activeRelay >= maxAntennas) activeRelay = maxAntennas - 1;
if (rxRelay >= maxAntennas) rxRelay = maxAntennas - 1;
if (txRelay >= maxAntennas) txRelay = maxAntennas - 1;
updateAntennas();
// Removed: scheduleSaveState();
}
inputBuffer = "";
return;
}
if (inputBuffer == "00" && !isDigit(key)) {
inputBuffer = "";
}
if (key == '9') {
toggleSwapMode();
} else if (key == '*') {
unsigned long now = millis();
if (now - lastStarPressTime < 500) {
toggleTxScanMode();
} else {
toggleRxScanMode();
}
lastStarPressTime = now;
} else if (key == '#') {
toggleTxScanMode();
}
}
void toggleAntennaBlock(int antennaNumber) {
if (antennaNumber < 0 || antennaNumber >= 8) return;
blockedAntennas[antennaNumber] = !blockedAntennas[antennaNumber];
blinkTxIndicator(antennaNumber);
if (blockedAntennas[antennaNumber] && txRelay == antennaNumber) {
findNewTxAntenna();
}
// Removed: scheduleSaveState();
}
void findNewTxAntenna() {
// Check if ALL antennas are blocked --------------------
bool allBlocked = true;
for (int i = 0; i < maxAntennas; i++) {
if (!blockedAntennas[i]) {
allBlocked = false;
break;
}
}
if (allBlocked) {
blockedAntennas[0] = false; // <---: auto-unblock Antenna-1
}
// ----------------------------------------------------------
// Find first allowed antenna
for (int i = 0; i < maxAntennas; i++) {
if (!blockedAntennas[i]) {
blinkTxIndicator(i); // <--- REMINDER BLINK
txRelay = i;
updateLatch2();
return;
}
}
// 3) Fallback (should never happen after auto-unblock)
blinkTxIndicator(0);
txRelay = 0;
updateLatch2();
}
bool canUseAsTx(int antennaNumber) {
if (antennaNumber < 0 || antennaNumber >= 8) return false;
return !blockedAntennas[antennaNumber];
}
void toggleRxScanMode() {
// Prevent mode changes during startup
if (millis() - bootTime < 3000) return;
bool cameFromTxScan = false;
if (txScanMode) {
rxRelay = txRelay;
txScanMode = false;
cameFromTxScan = true;
}
rxScanMode = !rxScanMode;
digitalWrite(statusLedPin, rxScanMode ? HIGH : LOW);
if (rxScanMode) {
if (!cameFromTxScan) {
txRelay = activeRelay;
rxRelay = activeRelay;
}
swapMode = false;
digitalWrite(swapLedPin, LOW);
updateLatch2();
// -------------------------------
// SAFETY: if the TX antenna is restricted,
// switch immediately to next allowed one
// -------------------------------
if (txRelay < maxAntennas && blockedAntennas[txRelay]) {
blinkTxIndicator(txRelay);
int next = txRelay;
for (int i = 0; i < maxAntennas; i++) {
next = (next + 1) % maxAntennas;
if (!blockedAntennas[next]) {
txRelay = next;
updateLatch2();
break;
}
}
}
} else {
activeRelay = rxRelay;
txRelay = rxRelay; //// me new
swapMode = false;
digitalWrite(swapLedPin, LOW);
digitalWrite(latchPin2, LOW);
shiftOut(dataPin2, clockPin2, MSBFIRST, 0);
digitalWrite(latchPin2, HIGH);
}
// Removed: scheduleSaveState();
}
void toggleTxScanMode() {
// Prevent mode changes during startup
if (millis() - bootTime < 3000) return;
bool cameFromRxScan = false;
if (rxScanMode) {
txRelay = rxRelay;
rxScanMode = false;
cameFromRxScan = true;
digitalWrite(statusLedPin, LOW);
}
txScanMode = !txScanMode;
if (txScanMode) {
if (!cameFromRxScan) {
rxRelay = activeRelay;
txRelay = activeRelay;
}
swapMode = false;
digitalWrite(swapLedPin, LOW);
updateLatch2();
// -------------------------------
// NEW SAFETY: if the TX antenna is restricted,
// switch immediately to next allowed one
// -------------------------------
if (txRelay < maxAntennas && blockedAntennas[txRelay]) {
blinkTxIndicator(txRelay);
int next = txRelay;
for (int i = 0; i < maxAntennas; i++) {
next = (next + 1) % maxAntennas;
if (!blockedAntennas[next]) {
txRelay = next;
updateLatch2();
break;
}
}
}
} else {
activeRelay = txRelay;
swapMode = false;
digitalWrite(swapLedPin, LOW);
digitalWrite(latchPin2, LOW);
shiftOut(dataPin2, clockPin2, MSBFIRST, 0);
digitalWrite(latchPin2, HIGH);
}
// Removed: scheduleSaveState();
}
void toggleSwapMode() {
// Prevent mode changes during startup
if (millis() - bootTime < 3000) return;
if (!(rxScanMode || txScanMode)) return;
if (!swapMode) {
if (canUseAsTx(rxRelay) && canUseAsTx(txRelay)) {
swapMode = true;
int temp = rxRelay;
rxRelay = txRelay;
txRelay = temp;
digitalWrite(swapLedPin, HIGH);
} else {
blinkTxIndicator(txRelay);
delay(1200);
return;
}
} else {
swapMode = false;
int temp = rxRelay;
rxRelay = txRelay;
txRelay = temp;
digitalWrite(swapLedPin, LOW);
}
// Removed: scheduleSaveState();
updateLatch2();
}
void setScanAntenna(int antennaNumber) {
// Prevent antenna changes during startup
if (millis() - bootTime < 3000) return;
if (antennaNumber < 0 || antennaNumber >= maxAntennas) return;
if (swapMode) return;
bool stateChanged = false;
if (rxScanMode) {
if (rxRelay != antennaNumber) {
rxRelay = antennaNumber;
stateChanged = true;
}
} else if (txScanMode) {
if (canUseAsTx(antennaNumber)) {
if (txRelay != antennaNumber) {
txRelay = antennaNumber;
updateLatch2();
stateChanged = true;
}
} else {
blinkTxIndicator(antennaNumber);
delay(1000);
return;
}
}
if (stateChanged) {
// Removed: scheduleSaveState();
}
}
void setActiveAntenna(int antennaNumber) {
// Prevent antenna changes during startup
if (millis() - bootTime < 3000) return;
if (antennaNumber < 0 || antennaNumber >= maxAntennas) return;
// If user selects a restricted antenna, blink it immediately ---
if (blockedAntennas[antennaNumber]) {
blinkTxIndicator(antennaNumber); // reminder blink
}
if (activeRelay != antennaNumber) {
activeRelay = antennaNumber;
if (!rxScanMode && !txScanMode) {
txRelay = activeRelay;
rxRelay = activeRelay;
}
// Removed: scheduleSaveState();
}
}
void checkPTT() {
static bool lastPttStateLocal = LOW;
bool currentPttState = digitalRead(pttPin);
// Detect change
if (currentPttState != lastPttStateLocal) {
// Rising edge: PTT pressed
if (currentPttState == HIGH) {
// If we are in TX scan mode and the current txRelay is blocked,
// blink indicator and auto-select the next available (not blocked) antenna.
if (txScanMode) {
if (txRelay < maxAntennas && blockedAntennas[txRelay]) {
// Blink the currently blocked antenna to indicate problem
blinkTxIndicator(txRelay);
// Find next allowed antenna (circular search)
int next = -1;
for (int i = 1; i < maxAntennas; i++) {
int cand = (txRelay + i) % maxAntennas;
if (!blockedAntennas[cand]) {
next = cand;
break;
}
}
if (next != -1) {
txRelay = next;
updateLatch2();
// Removed: scheduleSaveState();
}
// If no next found, leave txRelay as-is (nothing allowed)
}
}
}
// Update local last state and refresh antennas display
lastPttStateLocal = currentPttState;
updateAntennas();
}
}
void updateAntennas() {
bool pttActive = digitalRead(pttPin);
// Compute current RX (what RX should be showing)
int currentRx = (rxScanMode || txScanMode) ? rxRelay : activeRelay;
int currentTx = -1;
// ---- Decide currentTx consistently (same safety rules as updateLatch2) ----
if (rxScanMode || txScanMode) {
// In scan modes prefer txRelay if allowed, otherwise mirror RX
currentTx = (!blockedAntennas[txRelay]) ? txRelay : currentRx;
} else {
// NORMAL MODE
if (blockedAntennas[activeRelay]) {
// If active is blocked and we're transmitting, find a fallback
if (pttActive) {
// search forward then backward for next allowed
for (int i = activeRelay + 1; i < maxAntennas; i++) {
if (!blockedAntennas[i]) { currentTx = i; break; }
}
if (currentTx == -1) {
for (int i = 0; i < activeRelay; i++) {
if (!blockedAntennas[i]) { currentTx = i; break; }
}
}
} else {
// Not transmitting: prefer existing txRelay if allowed (for display),
// otherwise leave currentTx unset (-1) so we can decide display from rx
if (!blockedAntennas[txRelay]) currentTx = txRelay;
}
} else {
// activeRelay is allowed
if (pttActive) {
currentTx = activeRelay;
} else {
// not transmitting: prefer txRelay if allowed, else no special TX
if (!blockedAntennas[txRelay]) currentTx = txRelay;
}
}
}
// ---- COMMIT fallback to txRelay in NORMAL mode when we actually choose one ----
// This is the crucial missing step you observed: latch2 showed fallback but txRelay
// variable was not updated, so real relays (latch1) stayed on the old antenna.
if (!rxScanMode && !txScanMode && currentTx >= 0) {
// Only update txRelay if it's different (avoid excessive saves)
if (txRelay != currentTx) {
txRelay = currentTx;
// Removed: scheduleSaveState();
}
}
// ---- Decide what latch-1 (real TX relays) should show ----
int latch1Antenna;
if (pttActive) {
// While transmitting latch-1 must follow the TX choice (currentTx if available)
if (currentTx >= 0) latch1Antenna = currentTx;
else latch1Antenna = currentRx; // fallback if nothing calculated
} else {
// Not transmitting show RX (normal behavior)
latch1Antenna = currentRx;
}
// Output to latch-1 (real TX relays)
digitalWrite(latchPin1, LOW);
shiftOut(dataPin1, clockPin1, MSBFIRST, 1 << latch1Antenna);
digitalWrite(latchPin1, HIGH);
// Update latch-2 (display) and TX-scan LED as before
updateLatch2();
digitalWrite(txScanLedPin, txScanMode ? HIGH : LOW);
}
void updateLatch2() {
if (isBlinkingNonBlocking) return;
byte latch2Pattern = 0;
bool pttActive = digitalRead(pttPin);
int currentTx = -1;
// ------------------------------------------------------------------
// 1) SCAN MODES (UNCHANGED)
// ------------------------------------------------------------------
if (rxScanMode || txScanMode) {
currentTx = blockedAntennas[txRelay] ? rxRelay : txRelay;
}
// ------------------------------------------------------------------
// 2) NORMAL MODE (TX SAFETY LOGIC UNCHANGED)
// ------------------------------------------------------------------
else {
if (blockedAntennas[activeRelay]) {
if (pttActive) {
// forward search
for (int i = activeRelay + 1; i < maxAntennas; i++) {
if (!blockedAntennas[i]) {
currentTx = i;
break;
}
}
// backward search
if (currentTx == -1) {
for (int i = 0; i < activeRelay; i++) {
if (!blockedAntennas[i]) {
currentTx = i;
break;
}
}
}
}
} else {
if (pttActive) currentTx = activeRelay;
}
// ------------------------------------------------------------------
// 3) NORMAL MODE — LED-ONLY TX DISPLAY FIX
// ------------------------------------------------------------------
if (!pttActive && currentTx == -1) {
// CASE A: activeRelay is BLOCKED (newly restricted antenna)
if (blockedAntennas[activeRelay]) {
// forward search
for (int i = activeRelay + 1; i < maxAntennas; i++) {
if (!blockedAntennas[i]) {
currentTx = i;
break;
}
}
// backward search
if (currentTx == -1) {
for (int i = 0; i < activeRelay; i++) {
if (!blockedAntennas[i]) {
currentTx = i;
break;
}
}
}
}
// CASE B: activeRelay is allowed → show txRelay if allowed
else {
if (!blockedAntennas[txRelay]) {
currentTx = txRelay;
}
}
}
}
// ------------------------------------------------------------------
// 4) OUTPUT TO SHIFT REGISTER (UNCHANGED)
// ------------------------------------------------------------------
if (currentTx >= 0) {
latch2Pattern = 1 << currentTx;
}
digitalWrite(latchPin2, LOW);
shiftOut(dataPin2, clockPin2, MSBFIRST, latch2Pattern);
digitalWrite(latchPin2, HIGH);
}
void updateMaxAntLed() {
digitalWrite(MaxAntLedPin, maxAntennas < 8 ? HIGH : LOW);
}PTT
TX
RX-TX
RX Scan Mode (*)
SWAP
Antenna-1
Antenna-2
Antenna-3
Antenna-4
Antenna-5
Antenna-6
Antenna-7
Antenna-8
1
1
TX Scan Mode (**)
0
BLK
RTS
SWP
2
3
4
5
6
7
8
2
3
4
5
8
6
7