Загрузка данных


#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <OneWire.h>
#include <DallasTemperature.h>
#include <EEPROM.h>
#include <string.h>

// =====================================================
// OLED
// =====================================================

#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
#define OLED_RESET -1
#define OLED_ADDRESS 0x3C

Adafruit_SSD1306 display(
  SCREEN_WIDTH,
  SCREEN_HEIGHT,
  &Wire,
  OLED_RESET
);

// =====================================================
// РАСПИНОВКА
// =====================================================

// Форсунки
const byte INJECTOR_PINS[4] = {2, 3, 4, 5};

// Кнопки
const byte START_BUTTON_PIN = 6;
const byte RELAY1_BUTTON_PIN = 7;
const byte RELAY2_BUTTON_PIN = 9;
const byte CONTINUOUS_BUTTON_PIN = A2;

// Реле
const byte RELAY1_PIN = 8;
const byte RELAY2_PIN = 10;

// Температурный датчик DS18B20
const byte TEMP_SENSOR_PIN = 11;

// Динамик
const byte BUZZER_PIN = 12;

// Потенциометры
const byte PULSE_POT_PIN = A0;
const byte CYCLES_POT_PIN = A1;

// =====================================================
// НАСТРОЙКИ
// =====================================================

const unsigned long RELAY1_DURATION = 15000UL;

const unsigned long TEMP_CONVERSION_TIME = 750UL;
const unsigned long TEMP_READ_INTERVAL = 1000UL;

const float OVERHEAT_LIMIT = 65.0;
const float OVERHEAT_RELEASE = 60.0;

const float TRIGGER_RISE = 5.0;
const float TRIGGER_REARM_DROP = 1.0;

const unsigned long TRIGGER_WINDOW = 2000UL;

const unsigned long DISPLAY_INTERVAL = 200UL;
const unsigned long DEBOUNCE_DELAY = 35UL;

const int EEPROM_TRIGGER_ADDRESS = 0;

// =====================================================
// ТЕМПЕРАТУРА
// =====================================================

OneWire oneWire(TEMP_SENSOR_PIN);
DallasTemperature sensors(&oneWire);

float temperatureC = DEVICE_DISCONNECTED_C;

bool temperatureConversionPending = false;
unsigned long temperatureRequestTime = 0;
unsigned long lastTemperatureRequest = 0;

bool temperatureFault = false;
bool overheatLock = false;
bool emergencyLock = false;

bool temperatureErrorBeeped = false;
bool overheatBeeped = false;

// =====================================================
// ФОРСУНКИ
// =====================================================

int pulseDuration = 5;
int targetCycles = 10;

unsigned long cyclesCompleted = 0;

byte currentInjector = 0;

bool injectorRunning = false;
bool injectorPulseActive = false;

unsigned long injectorPulseStart = 0;

bool continuousMode = false;
bool continuousStopRequested = false;

// =====================================================
// РЕЛЕ
// =====================================================

bool relay1Active = false;
unsigned long relay1StartTime = 0;

bool relay2Active = false;

// =====================================================
// КНОПКИ И АНТИДРЕБЕЗГ
// =====================================================

struct ButtonState {
  byte pin;
  bool stableState;
  bool lastReading;
  unsigned long lastChangeTime;
};

ButtonState startButton = {
  START_BUTTON_PIN, HIGH, HIGH, 0
};

ButtonState relay1Button = {
  RELAY1_BUTTON_PIN, HIGH, HIGH, 0
};

ButtonState relay2Button = {
  RELAY2_BUTTON_PIN, HIGH, HIGH, 0
};

ButtonState continuousButton = {
  CONTINUOUS_BUTTON_PIN, HIGH, HIGH, 0
};

// =====================================================
// ЗВУК
// =====================================================

bool buzzerActive = false;

unsigned long buzzerStartTime = 0;
unsigned long buzzerDuration = 0;

// =====================================================
// СЧЁТЧИК TRIG
// =====================================================

uint32_t triggerCount = 0;

bool triggerReferenceReady = false;
bool triggerArmed = true;

float triggerReferenceTemp = 0.0;
float triggerEventTemp = 0.0;

unsigned long triggerReferenceTime = 0;

// =====================================================
// ДИСПЛЕЙ
// =====================================================

unsigned long lastDisplayUpdate = 0;

// =====================================================
// ЗВУКОВОЙ СИГНАЛ
// =====================================================

void beep(unsigned int frequency, unsigned long duration) {
  if (buzzerActive) {
    return;
  }

  tone(BUZZER_PIN, frequency);

  buzzerActive = true;
  buzzerStartTime = millis();
  buzzerDuration = duration;
}

void updateBuzzer() {
  if (!buzzerActive) {
    return;
  }

  if (millis() - buzzerStartTime >= buzzerDuration) {
    noTone(BUZZER_PIN);
    buzzerActive = false;
  }
}

// =====================================================
// МЕЛОДИЯ ПРИ ЗАПУСКЕ
// =====================================================

void playStartupMelody() {
  const unsigned int notes[] = {
    523, 659, 784, 1047, 784, 1047
  };

  const unsigned int durations[] = {
    140, 140, 140, 260, 140, 350
  };

  for (byte i = 0; i < 6; i++) {
    tone(BUZZER_PIN, notes[i]);
    delay(durations[i]);

    noTone(BUZZER_PIN);
    delay(45);
  }

  noTone(BUZZER_PIN);
}

// =====================================================
// АНТИДРЕБЕЗГ КНОПОК
// Возвращает true при новом нажатии
// =====================================================

bool readDebouncedButton(ButtonState &button) {
  bool reading = digitalRead(button.pin);
  unsigned long now = millis();

  if (reading != button.lastReading) {
    button.lastReading = reading;
    button.lastChangeTime = now;
  }

  if (now - button.lastChangeTime >= DEBOUNCE_DELAY) {
    if (reading != button.stableState) {
      button.stableState = reading;

      if (button.stableState == LOW) {
        return true;
      }
    }
  }

  return false;
}

// =====================================================
// ПОТЕНЦИОМЕТРЫ
// P: 5–25 мс
// N: 10–100 циклов
// =====================================================

void readPotentiometers() {
  static float filteredP = 0;
  static float filteredN = 0;

  static bool initialized = false;

  int rawP = analogRead(PULSE_POT_PIN);
  int rawN = analogRead(CYCLES_POT_PIN);

  if (!initialized) {
    filteredP = rawP;
    filteredN = rawN;
    initialized = true;
  }

  const float alpha = 0.08;

  filteredP += alpha * (rawP - filteredP);
  filteredN += alpha * (rawN - filteredN);

  pulseDuration = map(
    (int)(filteredP + 0.5),
    0, 1023,
    5, 25
  );

  targetCycles = map(
    (int)(filteredN + 0.5),
    0, 1023,
    10, 100
  );

  pulseDuration = constrain(pulseDuration, 5, 25);
  targetCycles = constrain(targetCycles, 10, 100);
}

// =====================================================
// УПРАВЛЕНИЕ ВЫХОДАМИ ФОРСУНОК
// =====================================================

void allInjectorsOff() {
  for (byte i = 0; i < 4; i++) {
    digitalWrite(INJECTOR_PINS[i], LOW);
  }

  injectorPulseActive = false;
}

bool safetyConditionsOK() {
  return !temperatureFault && !overheatLock;
}

void stopInjectorTest() {
  allInjectorsOff();

  injectorRunning = false;
  continuousMode = false;
  continuousStopRequested = false;

  currentInjector = 0;
  cyclesCompleted = 0;
}

void startInjectorTest(bool continuous) {
  if (!safetyConditionsOK() || emergencyLock) {
    beep(300, 200);
    return;
  }

  allInjectorsOff();

  cyclesCompleted = 0;
  currentInjector = 0;

  continuousMode = continuous;
  continuousStopRequested = false;

  injectorRunning = true;
  injectorPulseActive = false;

  digitalWrite(INJECTOR_PINS[currentInjector], HIGH);

  injectorPulseStart = millis();
  injectorPulseActive = true;
}

// =====================================================
// ЦИКЛЫ ФОРСУНОК
// =====================================================

void updateInjectorTest() {
  if (!injectorRunning) {
    return;
  }

  // Защита на случай появления ошибки во время работы
  if (!safetyConditionsOK() || emergencyLock) {
    stopInjectorTest();
    return;
  }

  if (!injectorPulseActive) {
    return;
  }

  if (millis() - injectorPulseStart < (unsigned long)pulseDuration) {
    return;
  }

  // Завершаем импульс текущей форсунки
  digitalWrite(INJECTOR_PINS[currentInjector], LOW);
  injectorPulseActive = false;

  // Переходим к следующей форсунке
  currentInjector++;

  if (currentInjector >= 4) {
    currentInjector = 0;
    cyclesCompleted++;

    // Останов после завершения полного цикла
    if (continuousMode && continuousStopRequested) {
      stopInjectorTest();
      return;
    }

    if (!continuousMode && cyclesCompleted >= (unsigned long)targetCycles) {
      stopInjectorTest();
      beep(1500, 100);
      return;
    }
  }

  // Запускаем следующую форсунку
  digitalWrite(INJECTOR_PINS[currentInjector], HIGH);

  injectorPulseStart = millis();
  injectorPulseActive = true;
}

// =====================================================
// КНОПКА START
// Повторное нажатие во время теста — аварийная остановка
// =====================================================

void updateStartButton() {
  if (!readDebouncedButton(startButton)) {
    return;
  }

  if (injectorRunning) {
    stopInjectorTest();

    emergencyLock = true;

    beep(350, 250);
    return;
  }

  if (!safetyConditionsOK()) {
    beep(300, 200);
    return;
  }

  if (emergencyLock) {
    emergencyLock = false;
    beep(1000, 100);
    return;
  }

  startInjectorTest(false);
}

// =====================================================
// НЕПРЕРЫВНЫЙ РЕЖИМ
// Нажатие — запуск, отпускание — останов после цикла
// =====================================================

void updateContinuousButton() {
  bool pressed = readDebouncedButton(continuousButton);

  if (pressed && !injectorRunning) {
    if (safetyConditionsOK() && !emergencyLock) {
      startInjectorTest(true);
    } else {
      beep(300, 200);
    }
  }

  // Считываем фактическое стабильное состояние кнопки
  if (continuousMode && injectorRunning) {
    if (continuousButton.stableState == HIGH) {
      continuousStopRequested = true;
    }
  }
}

// =====================================================
// РЕЛЕ R1
// Включение на 15 секунд
// =====================================================

void updateRelay1() {
  if (readDebouncedButton(relay1Button)) {
    if (!injectorRunning && !relay1Active) {
      relay1Active = true;
      relay1StartTime = millis();

      digitalWrite(RELAY1_PIN, HIGH);
    }
  }

  if (relay1Active) {
    if (millis() - relay1StartTime >= RELAY1_DURATION) {
      relay1Active = false;
      digitalWrite(RELAY1_PIN, LOW);
    }
  }
}

// =====================================================
// РЕЛЕ R2
// Работает, пока кнопка нажата
// Заблокировано во время теста форсунок
// =====================================================

void updateRelay2() {
  readDebouncedButton(relay2Button);

  bool shouldBeActive =
    (relay2Button.stableState == LOW) && !injectorRunning;

  if (shouldBeActive != relay2Active) {
    relay2Active = shouldBeActive;

    digitalWrite(
      RELAY2_PIN,
      relay2Active ? HIGH : LOW
    );
  }
}

// =====================================================
// СЧЁТЧИК TRIG — EEPROM
// =====================================================

void loadTriggerCounter() {
  EEPROM.get(EEPROM_TRIGGER_ADDRESS, triggerCount);

  // Проверка на незаписанную или повреждённую EEPROM
  if (triggerCount == 0xFFFFFFFFUL) {
    triggerCount = 0;
  }
}

void saveTriggerCounter() {
  EEPROM.put(EEPROM_TRIGGER_ADDRESS, triggerCount);
}

// Сброс TRIG при удержании R1 и R2 во время включения
void resetTriggerCounterIfRequested() {
  if (digitalRead(RELAY1_BUTTON_PIN) == LOW &&
      digitalRead(RELAY2_BUTTON_PIN) == LOW) {

    triggerCount = 0;
    saveTriggerCounter();

    display.clearDisplay();
    display.setTextColor(SSD1306_WHITE);
    display.setTextSize(2);
    display.setCursor(12, 20);
    display.print(F("TRIG RESET"));
    display.display();

    delay(700);
  }
}

void updateTriggerCounter() {
  if (temperatureC == DEVICE_DISCONNECTED_C ||
      temperatureFault) {
    return;
  }

  unsigned long now = millis();

  // После запуска ждём первое достоверное значение
  if (!triggerReferenceReady) {
    triggerReferenceTemp = temperatureC;
    triggerReferenceTime = now;
    triggerReferenceReady = true;
    return;
  }

  // Повторное разрешение счётчика после охлаждения
  if (!triggerArmed) {
    if (temperatureC <= triggerEventTemp - TRIGGER_REARM_DROP) {
      triggerArmed = true;

      triggerReferenceTemp = temperatureC;
      triggerReferenceTime = now;
    }

    return;
  }

  // Обнаружение скачка температуры в пределах 2 секунд
  if (now - triggerReferenceTime <= TRIGGER_WINDOW) {
    if (temperatureC - triggerReferenceTemp >= TRIGGER_RISE) {
      triggerCount++;
      saveTriggerCounter();

      triggerEventTemp = temperatureC;
      triggerArmed = false;

      return;
    }
  } else {
    // Обновляем точку отсчёта для следующего окна
    triggerReferenceTemp = temperatureC;
    triggerReferenceTime = now;
  }
}

// =====================================================
// ОБРАБОТКА ТЕМПЕРАТУРЫ
// =====================================================

void processTemperature() {
  if (temperatureC == DEVICE_DISCONNECTED_C) {
    temperatureFault = true;
    emergencyLock = true;

    stopInjectorTest();

    if (!temperatureErrorBeeped) {
      beep(300, 400);
      temperatureErrorBeeped = true;
    }

    return;
  }

  // Датчик снова отвечает
  temperatureFault = false;
  temperatureErrorBeeped = false;

  // Защита от перегрева
  if (temperatureC >= OVERHEAT_LIMIT) {
    overheatLock = true;
    emergencyLock = true;

    stopInjectorTest();

    if (!overheatBeeped) {
      beep(250, 500);
      overheatBeeped = true;
    }
  }

  // Автоматическое снятие блокировки перегрева
  if (overheatLock && temperatureC <= OVERHEAT_RELEASE) {
    overheatLock = false;
    overheatBeeped = false;
  }

  updateTriggerCounter();
}

void updateTemperature() {
  unsigned long now = millis();

  // Запрашиваем новое измерение
  if (!temperatureConversionPending &&
      now - lastTemperatureRequest >= TEMP_READ_INTERVAL) {

    sensors.requestTemperatures();

    temperatureConversionPending = true;
    temperatureRequestTime = now;
    lastTemperatureRequest = now;
  }

  // Получаем результат после завершения преобразования
  if (temperatureConversionPending &&
      now - temperatureRequestTime >= TEMP_CONVERSION_TIME) {

    temperatureC = sensors.getTempCByIndex(0);

    temperatureConversionPending = false;

    processTemperature();
  }
}

// =====================================================
// ЗАГРУЗОЧНАЯ АНИМАЦИЯ — 5 СЕКУНД
// =====================================================

void startupAnimation() {
  unsigned long startTime = millis();
  const unsigned long animationDuration = 5000UL;

  while (millis() - startTime < animationDuration) {
    unsigned long elapsed = millis() - startTime;

    int progress = map(
      elapsed,
      0,
      animationDuration,
      0,
      100
    );

    display.clearDisplay();

    display.setTextColor(SSD1306_WHITE);
    display.setTextSize(1);

    display.setCursor(22, 8);
    display.print(F("INJECTOR TESTER"));

    display.drawRect(9, 29, 110, 13, SSD1306_WHITE);

    int barWidth = map(progress, 0, 100, 0, 106);

    display.fillRect(11, 31, barWidth, 9, SSD1306_WHITE);

    display.setTextSize(2);

    display.setCursor(43, 47);
    display.print(progress);
    display.print(F("%"));

    display.display();

    delay(30);
  }

  display.clearDisplay();
  display.display();
}

// =====================================================
// ОБНОВЛЕНИЕ ДИСПЛЕЯ
// =====================================================

void updateDisplay() {
  if (millis() - lastDisplayUpdate < DISPLAY_INTERVAL) {
    return;
  }

  lastDisplayUpdate = millis();

  display.clearDisplay();
  display.setTextColor(SSD1306_WHITE);

  // Заголовки P и N
  display.setTextSize(1);

  display.setCursor(0, 0);
  display.print(F("P"));

  display.setCursor(76, 0);
  display.print(F("N"));

  // Крупные значения параметров
  display.setTextSize(3);

  display.setCursor(0, 10);
  display.print(pulseDuration);

  char nBuffer[4];

  snprintf(
    nBuffer,
    sizeof(nBuffer),
    "%d",
    targetCycles
  );

  int nWidth = strlen(nBuffer) * 18;
  int nX = 124 - nWidth;

  if (nX < 64) {
    nX = 64;
  }

  display.setCursor(nX, 10);
  display.print(nBuffer);

  // Температура
  display.setTextSize(1);

  display.setCursor(0, 39);
  display.print(F("TEMP:"));

  if (temperatureFault) {
    display.print(F("ERROR"));
  } else if (overheatLock) {
    display.print(F("OVERHEAT"));
  } else if (temperatureC != DEVICE_DISCONNECTED_C) {
    display.print(temperatureC, 1);
    display.print(F(" C"));
  } else {
    display.print(F("--.- C"));
  }

  // TRIG слева внизу
  display.setCursor(0, 51);
  display.print(F("TRIG:"));
  display.print(triggerCount);

  // Обратный отсчёт R1 справа внизу
  display.setCursor(92, 51);

  if (relay1Active) {
    unsigned long elapsed = millis() - relay1StartTime;
    unsigned long remaining = 0;

    if (elapsed < RELAY1_DURATION) {
      remaining =
        (RELAY1_DURATION - elapsed + 999UL) / 1000UL;
    }

    display.print(F("R1:"));
    display.print(remaining);
    display.print(F("s"));
  } else {
    display.print(F("R1:OFF"));
  }

  display.display();
}

// =====================================================
// SETUP
// =====================================================

void setup() {
  // Форсунки
  for (byte i = 0; i < 4; i++) {
    pinMode(INJECTOR_PINS[i], OUTPUT);
    digitalWrite(INJECTOR_PINS[i], LOW);
  }

  // Кнопки
  pinMode(START_BUTTON_PIN, INPUT_PULLUP);
  pinMode(RELAY1_BUTTON_PIN, INPUT_PULLUP);
  pinMode(RELAY2_BUTTON_PIN, INPUT_PULLUP);
  pinMode(CONTINUOUS_BUTTON_PIN, INPUT_PULLUP);

  // Реле
  pinMode(RELAY1_PIN, OUTPUT);
  pinMode(RELAY2_PIN, OUTPUT);

  digitalWrite(RELAY1_PIN, LOW);
  digitalWrite(RELAY2_PIN, LOW);

  // Динамик
  pinMode(BUZZER_PIN, OUTPUT);
  noTone(BUZZER_PIN);

  // OLED
  Wire.begin();

  if (!display.begin(
        SSD1306_SWITCHCAPVCC,
        OLED_ADDRESS
      )) {
    while (true) {
      // Остановка при ошибке инициализации дисплея
    }
  }

  display.clearDisplay();
  display.display();

  // Температурный датчик
  sensors.begin();
  sensors.setWaitForConversion(false);

  // Загружаем счётчик
  loadTriggerCounter();

  // При удержании R1 + R2 сбрасываем TRIG
  resetTriggerCounterIfRequested();

  // Пятисекундная загрузочная анимация
  startupAnimation();

  // НОВОЕ: мелодия после загрузочной анимации
  playStartupMelody();

  // После мелодии динамик остаётся выключенным
  noTone(BUZZER_PIN);
  buzzerActive = false;
}

// =====================================================
// ОСНОВНОЙ ЦИКЛ
// =====================================================

void loop() {
  updateBuzzer();

  readPotentiometers();

  updateTemperature();

  updateStartButton();

  updateContinuousButton();

  updateRelay1();

  updateRelay2();

  updateInjectorTest();

  updateDisplay();
}