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


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

#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 injectorPins[4] = {2, 3, 4, 5};

const byte injectorButtonPin = 6;

const byte relay1ButtonPin = 7;
const byte relay1Pin = 8;

const byte relay2ButtonPin = 9;
const byte relay2Pin = 10;

const byte temperaturePin = 11;

// Динамик
// Можно использовать A2 как цифровой выход.
// Если хочешь вернуть D12 — просто поставь 12.
const byte buzzerPin = A2;

const byte timePotPin = A0;
const byte countPotPin = A1;


// ======================================================
// DS18B20
// ======================================================

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

float currentTemperature = 0.0;

bool temperatureValid = false;
bool temperatureFault = false;


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

const unsigned int MIN_INJECTOR_TIME = 5;
const unsigned int MAX_INJECTOR_TIME = 25;

const int MIN_CYCLES = 10;
const int MAX_CYCLES = 100;


// ======================================================
// ТЕМПЕРАТУРНАЯ ЗАЩИТА
// ======================================================

const float OVERHEAT_TEMP = 65.0;
const float COOL_DOWN_TEMP = 60.0;


// ======================================================
// СЧЁТЧИК РЕЗКОГО ПОВЫШЕНИЯ ТЕМПЕРАТУРЫ
// ======================================================

const float TRIGGER_TEMP_RISE = 5.0;

const unsigned long TRIGGER_WINDOW = 2000UL;

const unsigned long TEMPERATURE_READ_TIME = 500UL;

float triggerStartTemperature = 0.0;

unsigned long triggerStartTime = 0;

unsigned long triggerCount = 0;

bool triggerArmed = true;


// ======================================================
// ВРЕМЕНА
// ======================================================

const unsigned long INJECTOR_LOCK_TIME = 10000UL;

const unsigned long RELAY1_TIME = 10000UL;

const unsigned long DEBOUNCE_TIME = 25UL;

const unsigned long DISPLAY_UPDATE_TIME = 100UL;


// ======================================================
// ПОТЕНЦИОМЕТРЫ
// ======================================================

const byte POT_SAMPLES = 8;

unsigned int injectorOpenTime = MIN_INJECTOR_TIME;

int injectorCycles = MIN_CYCLES;

int filteredTimeValue = 0;

int filteredCountValue = 0;


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

bool injectorRunning = false;

byte currentInjector = 0;

int currentCycle = 0;

unsigned long injectorStartTime = 0;


// ======================================================
// БЛОКИРОВКА КНОПКИ
// ======================================================

bool injectorButtonLocked = false;

unsigned long injectorUnlockTime = 0;

bool overheatLock = false;


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

bool relay1Active = false;

unsigned long relay1StartTime = 0;


// ======================================================
// КНОПКИ
// ======================================================

struct Button {

  byte pin;

  bool stableState;
  bool lastReading;

  unsigned long lastChangeTime;
};


Button injectorButton = {
  injectorButtonPin,
  HIGH,
  HIGH,
  0
};


Button relay1Button = {
  relay1ButtonPin,
  HIGH,
  HIGH,
  0
};


Button relay2Button = {
  relay2ButtonPin,
  HIGH,
  HIGH,
  0
};


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

unsigned long lastDisplayUpdate = 0;


// ======================================================
// ЗВУКОВОЙ ДВИЖОК
// ======================================================

struct SoundNote {

  unsigned int frequency;

  unsigned int duration;

  unsigned int pause;
};


enum SoundType {

  SOUND_NONE,

  SOUND_STARTUP,

  SOUND_BUTTON,

  SOUND_OVERHEAT,

  SOUND_ERROR
};


SoundType currentSound = SOUND_NONE;

byte currentSoundNote = 0;

unsigned long soundStartTime = 0;

bool soundPlaying = false;


// ======================================================
// НОТЫ
// ======================================================

// Запуск:
// ДО5 → МИ5 → СОЛЬ5 → ДО6

const SoundNote startupSound[] = {

  {523, 100, 25},
  {659, 100, 25},
  {784, 100, 25},
  {1047, 160, 20}
};


// Кнопка

const SoundNote buttonSound[] = {

  {880, 75, 45},
  {1175, 110, 20}
};


// Перегрев

const SoundNote overheatSound[] = {

  {988, 120, 40},
  {784, 120, 40},
  {523, 200, 30}
};


// Ошибка датчика

const SoundNote errorSound[] = {

  {330, 140, 50},
  {330, 140, 50},
  {220, 220, 30}
};


// ======================================================
// ПОЛУЧИТЬ ТЕКУЩИЙ ЗВУК
// ======================================================

const SoundNote* getCurrentSound() {

  switch (currentSound) {

    case SOUND_STARTUP:
      return startupSound;

    case SOUND_BUTTON:
      return buttonSound;

    case SOUND_OVERHEAT:
      return overheatSound;

    case SOUND_ERROR:
      return errorSound;

    default:
      return nullptr;
  }
}


// ======================================================
// КОЛИЧЕСТВО НОТ
// ======================================================

byte getSoundLength() {

  switch (currentSound) {

    case SOUND_STARTUP:
      return sizeof(startupSound) /
             sizeof(startupSound[0]);

    case SOUND_BUTTON:
      return sizeof(buttonSound) /
             sizeof(buttonSound[0]);

    case SOUND_OVERHEAT:
      return sizeof(overheatSound) /
             sizeof(overheatSound[0]);

    case SOUND_ERROR:
      return sizeof(errorSound) /
             sizeof(errorSound[0]);

    default:
      return 0;
  }
}


// ======================================================
// ЗАПУСК ЗВУКА
// ======================================================

void playSound(SoundType type) {

  currentSound = type;

  currentSoundNote = 0;

  soundStartTime = millis();

  soundPlaying = true;


  const SoundNote* sound =
    getCurrentSound();


  if (sound != nullptr) {

    tone(
      buzzerPin,
      sound[0].frequency
    );
  }
}


// ======================================================
// ОБНОВЛЕНИЕ ЗВУКА
// ======================================================

void updateSound() {

  if (!soundPlaying)
    return;


  const SoundNote* sound =
    getCurrentSound();


  if (sound == nullptr) {

    noTone(buzzerPin);

    soundPlaying = false;

    currentSound = SOUND_NONE;

    return;
  }


  unsigned long elapsed =
    millis() - soundStartTime;


  unsigned long noteTotal =
    sound[currentSoundNote].duration +
    sound[currentSoundNote].pause;


  if (
    elapsed >=
    sound[currentSoundNote].duration
  ) {

    noTone(buzzerPin);
  }


  if (
    elapsed >= noteTotal
  ) {

    currentSoundNote++;


    if (
      currentSoundNote >=
      getSoundLength()
    ) {

      noTone(buzzerPin);

      soundPlaying = false;

      currentSound = SOUND_NONE;

      return;
    }


    soundStartTime = millis();


    tone(
      buzzerPin,
      sound[currentSoundNote].frequency
    );
  }
}


// ======================================================
// РУССКИЙ ШРИФТ 5×7
// ======================================================

// Каждая буква:
// 5 пикселей шириной
// 7 пикселей высотой


const byte glyphA[7] = {
  0b01110,
  0b10001,
  0b10001,
  0b11111,
  0b10001,
  0b10001,
  0b10001
};


const byte glyphB[7] = {
  0b11110,
  0b10001,
  0b10001,
  0b11110,
  0b10001,
  0b10001,
  0b11110
};


const byte glyphV[7] = {
  0b11110,
  0b10001,
  0b10001,
  0b11110,
  0b10001,
  0b10001,
  0b11110
};


const byte glyphG[7] = {
  0b11111,
  0b10000,
  0b10000,
  0b10000,
  0b10000,
  0b10000,
  0b10000
};


const byte glyphD[7] = {
  0b01110,
  0b10001,
  0b10001,
  0b10001,
  0b10001,
  0b10001,
  0b11111
};


const byte glyphE[7] = {
  0b11111,
  0b10000,
  0b10000,
  0b11110,
  0b10000,
  0b10000,
  0b11111
};


// Ё

const byte glyphYO[7] = {
  0b01010,
  0b00000,
  0b11111,
  0b10000,
  0b11110,
  0b10000,
  0b11111
};


const byte glyphZh[7] = {
  0b10101,
  0b10101,
  0b01110,
  0b00100,
  0b01110,
  0b10101,
  0b10101
};


const byte glyphZ[7] = {
  0b01110,
  0b00001,
  0b00001,
  0b00110,
  0b00001,
  0b00001,
  0b01110
};


const byte glyphI[7] = {
  0b10001,
  0b10001,
  0b10011,
  0b10101,
  0b11001,
  0b10001,
  0b10001
};


const byte glyphY[7] = {
  0b01010,
  0b00000,
  0b10001,
  0b10001,
  0b10011,
  0b10101,
  0b11001
};


const byte glyphK[7] = {
  0b10001,
  0b10010,
  0b10100,
  0b11000,
  0b10100,
  0b10010,
  0b10001
};


const byte glyphL[7] = {
  0b00111,
  0b01001,
  0b10001,
  0b10001,
  0b10001,
  0b10001,
  0b10001
};


const byte glyphM[7] = {
  0b10001,
  0b11011,
  0b10101,
  0b10101,
  0b10001,
  0b10001,
  0b10001
};


const byte glyphN[7] = {
  0b10001,
  0b10001,
  0b10001,
  0b11111,
  0b10001,
  0b10001,
  0b10001
};


const byte glyphO[7] = {
  0b01110,
  0b10001,
  0b10001,
  0b10001,
  0b10001,
  0b10001,
  0b01110
};


const byte glyphP[7] = {
  0b11111,
  0b10001,
  0b10001,
  0b10001,
  0b10001,
  0b10001,
  0b10001
};


const byte glyphR[7] = {
  0b11110,
  0b10001,
  0b10001,
  0b11110,
  0b10000,
  0b10000,
  0b10000
};


const byte glyphS[7] = {
  0b01111,
  0b10000,
  0b10000,
  0b01110,
  0b00001,
  0b00001,
  0b11110
};


const byte glyphT[7] = {
  0b11111,
  0b00100,
  0b00100,
  0b00100,
  0b00100,
  0b00100,
  0b00100
};


const byte glyphU[7] = {
  0b10001,
  0b10001,
  0b01010,
  0b00100,
  0b00100,
  0b01000,
  0b10000
};


const byte glyphF[7] = {
  0b00100,
  0b01110,
  0b10101,
  0b10101,
  0b01110,
  0b00100,
  0b00100
};


const byte glyphH[7] = {
  0b10001,
  0b10001,
  0b01010,
  0b00100,
  0b01010,
  0b10001,
  0b10001
};


const byte glyphC[7] = {
  0b10010,
  0b10010,
  0b10010,
  0b10010,
  0b10010,
  0b10010,
  0b11111
};


const byte glyphCh[7] = {
  0b10001,
  0b10001,
  0b10001,
  0b10001,
  0b10001,
  0b01010,
  0b00100
};


// Ш

const byte glyphSh[7] = {
  0b10101,
  0b10101,
  0b10101,
  0b10101,
  0b10101,
  0b10101,
  0b11111
};


// Щ

const byte glyphShch[7] = {
  0b10101,
  0b10101,
  0b10101,
  0b10101,
  0b10101,
  0b10101,
  0b11111
};


// Ъ

const byte glyphHard[7] = {
  0b11000,
  0b10000,
  0b10000,
  0b11110,
  0b10001,
  0b10001,
  0b11110
};


// Ы

const byte glyphYI[7] = {
  0b10001,
  0b10001,
  0b10001,
  0b11101,
  0b10011,
  0b10011,
  0b11101
};


// Ь

const byte glyphSoft[7] = {
  0b10000,
  0b10000,
  0b10000,
  0b11110,
  0b10001,
  0b10001,
  0b11110
};


// Э

const byte glyphEReverse[7] = {
  0b11110,
  0b00001,
  0b00001,
  0b01111,
  0b00001,
  0b00001,
  0b11110
};


// Ю

const byte glyphYu[7] = {
  0b10111,
  0b10101,
  0b10101,
  0b11101,
  0b10101,
  0b10101,
  0b10111
};


// Я

const byte glyphYa[7] = {
  0b01111,
  0b10001,
  0b10001,
  0b01111,
  0b00101,
  0b01001,
  0b10001
};


// ======================================================
// ПОЛУЧИТЬ GLYPH ПО UNICODE
// ======================================================

const byte* getGlyphUnicode(unsigned int code) {

  switch (code) {

    case 0x0410: return glyphA;        // А
    case 0x0411: return glyphB;        // Б
    case 0x0412: return glyphV;        // В
    case 0x0413: return glyphG;        // Г
    case 0x0414: return glyphD;        // Д
    case 0x0415: return glyphE;        // Е
    case 0x0401: return glyphYO;       // Ё
    case 0x0416: return glyphZh;       // Ж
    case 0x0417: return glyphZ;        // З
    case 0x0418: return glyphI;        // И
    case 0x0419: return glyphY;        // Й
    case 0x041A: return glyphK;        // К
    case 0x041B: return glyphL;        // Л
    case 0x041C: return glyphM;        // М
    case 0x041D: return glyphN;        // Н
    case 0x041E: return glyphO;        // О
    case 0x041F: return glyphP;        // П
    case 0x0420: return glyphR;        // Р
    case 0x0421: return glyphS;        // С
    case 0x0422: return glyphT;        // Т
    case 0x0423: return glyphU;        // У
    case 0x0424: return glyphF;        // Ф
    case 0x0425: return glyphH;        // Х
    case 0x0426: return glyphC;        // Ц
    case 0x0427: return glyphCh;       // Ч
    case 0x0428: return glyphSh;       // Ш
    case 0x0429: return glyphShch;     // Щ
    case 0x042A: return glyphHard;     // Ъ
    case 0x042B: return glyphYI;       // Ы
    case 0x042C: return glyphSoft;     // Ь
    case 0x042D: return glyphEReverse; // Э
    case 0x042E: return glyphYu;       // Ю
    case 0x042F: return glyphYa;       // Я

    // Строчные буквы
    // автоматически превращаем в заглавные

    case 0x0430: return glyphA;
    case 0x0431: return glyphB;
    case 0x0432: return glyphV;
    case 0x0433: return glyphG;
    case 0x0434: return glyphD;
    case 0x0435: return glyphE;
    case 0x0451: return glyphYO;
    case 0x0436: return glyphZh;
    case 0x0437: return glyphZ;
    case 0x0438: return glyphI;
    case 0x0439: return glyphY;
    case 0x043A: return glyphK;
    case 0x043B: return glyphL;
    case 0x043C: return glyphM;
    case 0x043D: return glyphN;
    case 0x043E: return glyphO;
    case 0x043F: return glyphP;
    case 0x0440: return glyphR;
    case 0x0441: return glyphS;
    case 0x0442: return glyphT;
    case 0x0443: return glyphU;
    case 0x0444: return glyphF;
    case 0x0445: return glyphH;
    case 0x0446: return glyphC;
    case 0x0447: return glyphCh;
    case 0x0448: return glyphSh;
    case 0x0449: return glyphShch;
    case 0x044A: return glyphHard;
    case 0x044B: return glyphYI;
    case 0x044C: return glyphSoft;
    case 0x044D: return glyphEReverse;
    case 0x044E: return glyphYu;
    case 0x044F: return glyphYa;

    default:
      return nullptr;
  }
}


// ======================================================
// UTF-8 → UNICODE
// ======================================================

unsigned int decodeUTF8(
  const char* &text
) {

  unsigned char b1 =
    (unsigned char)text[0];


  // ASCII
  if (b1 < 0x80) {

    text++;

    return b1;
  }


  // 2-байтовый UTF-8
  if (
    (b1 & 0xE0) ==
    0xC0
  ) {

    unsigned char b2 =
      (unsigned char)text[1];


    unsigned int code =
      ((unsigned int)(b1 & 0x1F) << 6) |
      (b2 & 0x3F);


    text += 2;

    return code;
  }


  // 3-байтовый UTF-8
  if (
    (b1 & 0xF0) ==
    0xE0
  ) {

    unsigned char b2 =
      (unsigned char)text[1];

    unsigned char b3 =
      (unsigned char)text[2];


    unsigned int code =
      ((unsigned int)(b1 & 0x0F) << 12) |
      ((unsigned int)(b2 & 0x3F) << 6) |
      (b3 & 0x3F);


    text += 3;

    return code;
  }


  // Неизвестный символ
  text++;

  return '?';
}


// ======================================================
// ВЫВОД РУССКОГО / UTF-8 ТЕКСТА
// ======================================================

void printRussian(const char* text) {

  while (*text) {

    unsigned int code =
      decodeUTF8(text);


    // ==================================================
    // ПРОБЕЛ
    // ==================================================

    if (code == ' ') {

      display.setCursor(
        display.getCursorX() + 6,
        display.getCursorY()
      );

      continue;
    }


    // ==================================================
    // ASCII
    // ==================================================

    if (code < 128) {

      display.write(
        (char)code
      );

      continue;
    }


    // ==================================================
    // РУССКАЯ БУКВА
    // ==================================================

    const byte* glyph =
      getGlyphUnicode(code);


    if (glyph != nullptr) {

      int x =
        display.getCursorX();

      int y =
        display.getCursorY();


      for (
        byte row = 0;
        row < 7;
        row++
      ) {

        for (
          byte col = 0;
          col < 5;
          col++
        ) {

          if (
            glyph[row] &
            (1 << (4 - col))
          ) {

            display.drawPixel(
              x + col,
              y + row,
              SSD1306_WHITE
            );
          }
        }
      }


      display.setCursor(
        x + 6,
        y
      );
    }

    else {

      display.write('?');
    }
  }
}


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

void setup() {

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

  for (
    byte i = 0;
    i < 4;
    i++
  ) {

    pinMode(
      injectorPins[i],
      OUTPUT
    );


    digitalWrite(
      injectorPins[i],
      LOW
    );
  }


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

  pinMode(
    relay1Pin,
    OUTPUT
  );


  pinMode(
    relay2Pin,
    OUTPUT
  );


  digitalWrite(
    relay1Pin,
    LOW
  );


  digitalWrite(
    relay2Pin,
    LOW
  );


  // ====================================================
  // КНОПКИ
  // ====================================================

  pinMode(
    injectorButtonPin,
    INPUT_PULLUP
  );


  pinMode(
    relay1ButtonPin,
    INPUT_PULLUP
  );


  pinMode(
    relay2ButtonPin,
    INPUT_PULLUP
  );


  // ====================================================
  // ДИНАМИК
  // ====================================================

  pinMode(
    buzzerPin,
    OUTPUT
  );


  noTone(
    buzzerPin
  );


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

  display.begin(
    SSD1306_SWITCHCAPVCC,
    OLED_ADDRESS
  );


  display.clearDisplay();


  display.setTextColor(
    SSD1306_WHITE
  );


  display.setTextSize(1);


  display.setCursor(
    0,
    0
  );


  printRussian(
    "ФОРСУНКИ"
  );


  display.display();


  // ====================================================
  // DS18B20
  // ====================================================

  sensors.begin();


  sensors.setResolution(
    10
  );


  sensors.setWaitForConversion(
    true
  );


  // ====================================================
  // ПЕРВОЕ ИЗМЕРЕНИЕ
  // ====================================================

  sensors.requestTemperatures();


  float t =
    sensors.getTempCByIndex(0);


  if (
    t !=
    DEVICE_DISCONNECTED_C
  ) {

    currentTemperature =
      t;


    temperatureValid =
      true;


    triggerStartTemperature =
      t;


    triggerStartTime =
      millis();
  }

  else {

    temperatureValid =
      false;


    temperatureFault =
      true;
  }


  // ====================================================
  // ПАУЗА ПОСЛЕ СТАРТА
  // ====================================================

  delay(500);


  // ====================================================
  // ЗВУК ЗАПУСКА
  // ====================================================

  playSound(
    SOUND_STARTUP
  );
}


// ======================================================
// LOOP
// ======================================================

void loop() {

  updateSound();


  readSettings();


  updateButton(
    injectorButton
  );


  updateButton(
    relay1Button
  );


  updateButton(
    relay2Button
  );


  updateTemperature();


  updateInjectors();


  updateInjectorLock();


  updateRelay1();


  updateRelay2();


  updateDisplay();
}


// ======================================================
// ПОТЕНЦИОМЕТРЫ
// ======================================================

int readPotFiltered(
  byte pin
) {

  long sum = 0;


  for (
    byte i = 0;
    i < POT_SAMPLES;
    i++
  ) {

    sum += analogRead(pin);
  }


  return sum / POT_SAMPLES;
}


void readSettings() {

  int newTimeValue =
    readPotFiltered(
      timePotPin
    );


  int newCountValue =
    readPotFiltered(
      countPotPin
    );


  // ====================================================
  // ВРЕМЯ
  // ====================================================

  if (
    newTimeValue <= 3
  ) {

    filteredTimeValue =
      0;
  }

  else if (
    newTimeValue >= 1020
  ) {

    filteredTimeValue =
      1023;
  }

  else if (
    abs(
      newTimeValue -
      filteredTimeValue
    ) >= 3
  ) {

    filteredTimeValue =
      newTimeValue;
  }


  // ====================================================
  // КОЛИЧЕСТВО ЦИКЛОВ
  // ====================================================

  if (
    newCountValue <= 3
  ) {

    filteredCountValue =
      0;
  }

  else if (
    newCountValue >= 1020
  ) {

    filteredCountValue =
      1023;
  }

  else if (
    abs(
      newCountValue -
      filteredCountValue
    ) >= 3
  ) {

    filteredCountValue =
      newCountValue;
  }


  // ====================================================
  // ПРЕОБРАЗОВАНИЕ
  // ====================================================

  injectorOpenTime =
    map(
      filteredTimeValue,
      0,
      1023,
      MIN_INJECTOR_TIME,
      MAX_INJECTOR_TIME
    );


  injectorCycles =
    map(
      filteredCountValue,
      0,
      1023,
      MIN_CYCLES,
      MAX_CYCLES
    );


  injectorOpenTime =
    constrain(
      injectorOpenTime,
      MIN_INJECTOR_TIME,
      MAX_INJECTOR_TIME
    );


  injectorCycles =
    constrain(
      injectorCycles,
      MIN_CYCLES,
      MAX_CYCLES
    );
}


// ======================================================
// КНОПКИ
// ======================================================

bool buttonPressed(
  Button &button
) {

  bool reading =
    digitalRead(
      button.pin
    );


  unsigned long now =
    millis();


  if (
    reading !=
    button.lastReading
  ) {

    button.lastChangeTime =
      now;


    button.lastReading =
      reading;
  }


  if (
    now -
    button.lastChangeTime >=
    DEBOUNCE_TIME
  ) {

    if (
      reading !=
      button.stableState
    ) {

      button.stableState =
        reading;


      if (
        button.stableState ==
        LOW
      ) {

        return true;
      }
    }
  }


  return false;
}


void updateButton(
  Button &button
) {

  // ====================================================
  // КНОПКА ФОРСУНОК
  // ====================================================

  if (
    &button ==
    &injectorButton
  ) {

    if (
      buttonPressed(button)
    ) {

      if (
        !injectorRunning &&
        !injectorButtonLocked &&
        !overheatLock &&
        temperatureValid &&
        !temperatureFault
      ) {

        playSound(
          SOUND_BUTTON
        );


        startInjectorCycle();
      }

      else if (
        overheatLock
      ) {

        playSound(
          SOUND_OVERHEAT
        );
      }
    }
  }


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

  if (
    &button ==
    &relay1Button
  ) {

    if (
      buttonPressed(button)
    ) {

      if (
        !injectorRunning &&
        !relay1Active
      ) {

        relay1Active =
          true;


        relay1StartTime =
          millis();


        digitalWrite(
          relay1Pin,
          HIGH
        );
      }
    }
  }
}


// ======================================================
// ЗАПУСК ФОРСУНОК
// ======================================================

void startInjectorCycle() {

  if (!temperatureValid)
    return;


  if (temperatureFault)
    return;


  if (overheatLock)
    return;


  if (
    currentTemperature >=
    OVERHEAT_TEMP
  ) {

    overheatLock =
      true;


    injectorButtonLocked =
      true;


    playSound(
      SOUND_OVERHEAT
    );


    return;
  }


  injectorRunning =
    true;


  currentInjector =
    0;


  currentCycle =
    0;


  injectorButtonLocked =
    true;


  digitalWrite(
    injectorPins[
      currentInjector
    ],
    HIGH
  );


  injectorStartTime =
    micros();
}


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

void updateInjectors() {

  if (!injectorRunning)
    return;


  if (
    overheatLock ||
    temperatureFault ||
    !temperatureValid
  ) {

    for (
      byte i = 0;
      i < 4;
      i++
    ) {

      digitalWrite(
        injectorPins[i],
        LOW
      );
    }


    injectorRunning =
      false;


    injectorUnlockTime =
      millis();


    return;
  }


  unsigned long now =
    micros();


  unsigned long requiredTime =
    (unsigned long)
    injectorOpenTime *
    1000UL;


  if (
    now -
    injectorStartTime >=
    requiredTime
  ) {

    digitalWrite(
      injectorPins[
        currentInjector
      ],
      LOW
    );


    currentInjector++;


    if (
      currentInjector >= 4
    ) {

      currentInjector =
        0;


      currentCycle++;


      if (
        currentCycle >=
        injectorCycles
      ) {

        injectorRunning =
          false;


        injectorUnlockTime =
          millis();


        return;
      }
    }


    digitalWrite(
      injectorPins[
        currentInjector
      ],
      HIGH
    );


    injectorStartTime =
      micros();
  }
}


// ======================================================
// БЛОКИРОВКА КНОПКИ
// ======================================================

void updateInjectorLock() {

  if (
    !injectorButtonLocked
  )
    return;


  if (overheatLock)
    return;


  if (injectorRunning)
    return;


  if (
    millis() -
    injectorUnlockTime >=
    INJECTOR_LOCK_TIME
  ) {

    injectorButtonLocked =
      false;
  }
}


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

void updateRelay1() {

  if (!relay1Active)
    return;


  if (
    millis() -
    relay1StartTime >=
    RELAY1_TIME
  ) {

    digitalWrite(
      relay1Pin,
      LOW
    );


    relay1Active =
      false;
  }
}


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

void updateRelay2() {

  if (
    relay2Button.stableState ==
    LOW
  ) {

    digitalWrite(
      relay2Pin,
      HIGH
    );
  }

  else {

    digitalWrite(
      relay2Pin,
      LOW
    );
  }
}


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

void updateTemperature() {

  static unsigned long
    lastTemperatureRead = 0;


  static bool
    errorAlreadyReported = false;


  if (
    millis() -
    lastTemperatureRead <
    TEMPERATURE_READ_TIME
  ) {

    return;
  }


  lastTemperatureRead =
    millis();


  sensors.requestTemperatures();


  float newTemperature =
    sensors.getTempCByIndex(0);


  // ====================================================
  // ОШИБКА ДАТЧИКА
  // ====================================================

  if (
    newTemperature ==
    DEVICE_DISCONNECTED_C
  ) {

    temperatureValid =
      false;


    temperatureFault =
      true;


    for (
      byte i = 0;
      i < 4;
      i++
    ) {

      digitalWrite(
        injectorPins[i],
        LOW
      );
    }


    injectorRunning =
      false;


    injectorButtonLocked =
      true;


    if (
      !errorAlreadyReported
    ) {

      playSound(
        SOUND_ERROR
      );


      errorAlreadyReported =
        true;
    }


    return;
  }


  // ====================================================
  // ДАТЧИК СНОВА РАБОТАЕТ
  // ====================================================

  errorAlreadyReported =
    false;


  temperatureValid =
    true;


  temperatureFault =
    false;


  currentTemperature =
    newTemperature;


  // ====================================================
  // ПЕРЕГРЕВ
  // ====================================================

  if (
    currentTemperature >=
    OVERHEAT_TEMP
  ) {

    if (!overheatLock) {

      playSound(
        SOUND_OVERHEAT
      );
    }


    overheatLock =
      true;


    injectorButtonLocked =
      true;


    for (
      byte i = 0;
      i < 4;
      i++
    ) {

      digitalWrite(
        injectorPins[i],
        LOW
      );
    }


    injectorRunning =
      false;
  }


  // ====================================================
  // ОХЛАЖДЕНИЕ
  // ====================================================

  if (
    overheatLock &&
    currentTemperature <=
    COOL_DOWN_TEMP
  ) {

    overheatLock =
      false;


    injectorButtonLocked =
      false;
  }


  // ====================================================
  // СЧЁТЧИК РЕЗКОГО ПОВЫШЕНИЯ ТЕМПЕРАТУРЫ
  // ====================================================

  if (!triggerArmed) {

    if (
      currentTemperature <
      triggerStartTemperature - 1.0
    ) {

      triggerArmed =
        true;


      triggerStartTemperature =
        currentTemperature;


      triggerStartTime =
        millis();
    }


    return;
  }


  unsigned long now =
    millis();


  float rise =
    currentTemperature -
    triggerStartTemperature;


  // Более 5 °C за 2 секунды

  if (
    rise > TRIGGER_TEMP_RISE &&
    now -
    triggerStartTime <=
    TRIGGER_WINDOW
  ) {

    triggerCount++;


    triggerArmed =
      false;


    triggerStartTemperature =
      currentTemperature;


    triggerStartTime =
      now;


    return;
  }


  // ====================================================
  // ОКНО ЗАКОНЧИЛОСЬ
  // ====================================================

  if (
    now -
    triggerStartTime >
    TRIGGER_WINDOW
  ) {

    triggerStartTemperature =
      currentTemperature;


    triggerStartTime =
      now;
  }
}


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

void updateDisplay() {

  if (
    millis() -
    lastDisplayUpdate <
    DISPLAY_UPDATE_TIME
  ) {

    return;
  }


  lastDisplayUpdate =
    millis();


  display.clearDisplay();


  display.setTextColor(
    SSD1306_WHITE
  );


  display.setTextSize(1);


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

  display.setCursor(
    0,
    0
  );


  printRussian(
    "ИМПУЛЬС:"
  );


  display.print(
    injectorOpenTime
  );


  display.print(
    "ms"
  );


  display.setCursor(
    70,
    0
  );


  printRussian(
    "ЦИКЛЫ:"
  );


  display.print(
    injectorCycles
  );


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

  display.setCursor(
    0,
    12
  );


  printRussian(
    "ТЕМП:"
  );


  if (temperatureValid) {

    display.print(
      currentTemperature,
      1
    );


    display.print(
      "C"
    );
  }

  else {

    printRussian(
      "ОШИБКА"
    );
  }


  // ====================================================
  // СОСТОЯНИЕ
  // ====================================================

  display.setCursor(
    0,
    23
  );


  if (temperatureFault) {

    printRussian(
      "ОШИБКА ДАТЧИКА"
    );
  }

  else if (overheatLock) {

    printRussian(
      "ПЕРЕГРЕВ"
    );
  }

  else if (injectorRunning) {

    printRussian(
      "РАБОТА"
    );


    display.setCursor(
      70,
      23
    );


    display.print(
      currentInjector + 1
    );


    display.print(
      "/"
    );


    display.print(
      currentCycle + 1
    );
  }

  else if (injectorButtonLocked) {

    printRussian(
      "БЛОК:"
    );


    unsigned long elapsed =
      millis() -
      injectorUnlockTime;


    unsigned long remaining =
      0;


    if (
      elapsed <
      INJECTOR_LOCK_TIME
    ) {

      remaining =
        (
          INJECTOR_LOCK_TIME -
          elapsed +
          999
        ) / 1000;
    }


    display.print(
      remaining
    );


    display.print(
      "s"
    );
  }

  else {

    printRussian(
      "ГОТОВ"
    );
  }


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

  display.setCursor(
    0,
    35
  );


  display.print(
    "R1:"
  );


  display.print(
    relay1Active ?
    "ON" :
    "OFF"
  );


  display.setCursor(
    65,
    35
  );


  display.print(
    "R2:"
  );


  display.print(
    relay2Button.stableState == LOW ?
    "ON" :
    "OFF"
  );


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

  display.setCursor(
    0,
    48
  );


  printRussian(
    "СРАБАТЫВАНИЙ:"
  );


  display.setCursor(
    0,
    56
  );


  display.print(
    triggerCount
  );


  display.display();
}