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


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

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

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

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

// =====================================================
// PINS — ARDUINO NANO
// =====================================================

// Injector driver inputs
const byte INJ1_PIN = 2;
const byte INJ2_PIN = 3;
const byte INJ3_PIN = 4;
const byte INJ4_PIN = 5;

// Buttons — INPUT_PULLUP, button connects pin to GND
const byte START_BUTTON_PIN      = 6;
const byte RELAY1_BUTTON_PIN     = 7;
const byte RELAY2_BUTTON_PIN     = 9;
const byte CONTINUOUS_BUTTON_PIN = A2;

// Relay control outputs
const byte RELAY1_PIN = 8;
const byte RELAY2_PIN = 10;

// Temperature sensor
const byte TEMP_PIN = 11;

// Buzzer
const byte BUZZER_PIN = 12;

// Potentiometers
const byte PULSE_POT_PIN = A0;
const byte CYCLE_POT_PIN = A1;

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

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

// =====================================================
// INJECTOR SETTINGS
// =====================================================

const int MIN_PULSE_MS = 5;
const int MAX_PULSE_MS = 25;

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

// =====================================================
// TEMPERATURE SETTINGS
// =====================================================

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 TEMP_READ_INTERVAL = 500UL;
const unsigned long TEMP_CONVERSION_TIME = 750UL;

// =====================================================
// TIMING
// =====================================================

const unsigned long R1_TIME = 15000UL;
const unsigned long DISPLAY_INTERVAL = 100UL;
const unsigned long DEBOUNCE_TIME = 30UL;

const byte POT_SAMPLES = 8;

// EEPROM write throttle
const unsigned long EEPROM_SAVE_INTERVAL = 10000UL;

// =====================================================
// EEPROM
// =====================================================

const int EEPROM_MAGIC_ADDR = 0;
const int EEPROM_COUNTER_ADDR = 4;

const uint32_t EEPROM_MAGIC = 0x4A544553UL;

// =====================================================
// INJECTOR STATE
// =====================================================

byte injectorPins[4] = {
  INJ1_PIN, INJ2_PIN, INJ3_PIN, INJ4_PIN
};

bool injectorRunning = false;
bool continuousMode = false;

byte currentInjector = 0;
int currentCycle = 0;

int pulseMs = MIN_PULSE_MS;
int cycleCount = MIN_CYCLES;

unsigned long injectorStartTime = 0;

// =====================================================
// BUTTON STATE
// =====================================================

bool lastStartState = HIGH;
bool lastR1State = HIGH;
bool lastR2State = HIGH;
bool lastContinuousState = HIGH;

unsigned long lastStartChange = 0;
unsigned long lastR1Change = 0;
unsigned long lastR2Change = 0;
unsigned long lastContinuousChange = 0;

// =====================================================
// RELAYS
// =====================================================

bool relay1Active = false;
bool relay2Active = false;

unsigned long relay1StartTime = 0;

// =====================================================
// TEMPERATURE STATE
// =====================================================

float temperature = 0.0;

bool temperatureValid = false;
bool temperatureFault = true;
bool overheatLock = false;
bool emergencyLock = true;

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

// =====================================================
// TEMPERATURE TRIGGER COUNTER
// =====================================================

uint32_t triggerCount = 0;

bool triggerArmed = true;
bool triggerAlreadyCounted = false;

float triggerStartTemperature = 0.0;
unsigned long triggerStartTime = 0;

bool eepromDirty = false;
unsigned long lastEepromSave = 0;

// =====================================================
// POTENTIOMETERS
// =====================================================

int lastPulseRaw = -1000;
int lastCycleRaw = -1000;

// =====================================================
// DISPLAY
// =====================================================

unsigned long lastDisplayUpdate = 0;

// =====================================================
// SOUND ENGINE
// =====================================================

enum SoundType {
  SOUND_NONE,
  SOUND_STARTUP,
  SOUND_BUTTON,
  SOUND_STOP,
  SOUND_OVERHEAT,
  SOUND_ERROR
};

SoundType currentSound = SOUND_NONE;

byte soundStep = 0;
unsigned long soundStepStart = 0;
bool soundPlaying = false;

struct SoundNote {
  int frequency;
  unsigned long duration;
  unsigned long pause;
};

const SoundNote startupSound[] = {
  {523, 100, 25},
  {659, 100, 25},
  {784, 100, 25},
  {1047, 180, 40}
};

const byte startupSoundLength =
  sizeof(startupSound) / sizeof(startupSound[0]);

const SoundNote buttonSound[] = {
  {880, 70, 30},
  {1175, 110, 40}
};

const byte buttonSoundLength =
  sizeof(buttonSound) / sizeof(buttonSound[0]);

const SoundNote stopSound[] = {
  {988, 70, 30},
  {784, 100, 30},
  {523, 160, 50}
};

const byte stopSoundLength =
  sizeof(stopSound) / sizeof(stopSound[0]);

const SoundNote overheatSound[] = {
  {988, 120, 40},
  {784, 120, 40},
  {523, 200, 60},
  {988, 120, 40},
  {523, 250, 80}
};

const byte overheatSoundLength =
  sizeof(overheatSound) / sizeof(overheatSound[0]);

const SoundNote errorSound[] = {
  {330, 140, 50},
  {330, 140, 50},
  {220, 220, 80}
};

const byte errorSoundLength =
  sizeof(errorSound) / sizeof(errorSound[0]);

// =====================================================
// GET SOUND NOTES
// =====================================================

const SoundNote* getSoundNotes(byte &length) {
  switch (currentSound) {
    case SOUND_STARTUP:
      length = startupSoundLength;
      return startupSound;

    case SOUND_BUTTON:
      length = buttonSoundLength;
      return buttonSound;

    case SOUND_STOP:
      length = stopSoundLength;
      return stopSound;

    case SOUND_OVERHEAT:
      length = overheatSoundLength;
      return overheatSound;

    case SOUND_ERROR:
      length = errorSoundLength;
      return errorSound;

    default:
      length = 0;
      return nullptr;
  }
}

// =====================================================
// PLAY SOUND
// =====================================================

void playSound(SoundType sound) {
  noTone(BUZZER_PIN);

  currentSound = sound;
  soundStep = 0;
  soundStepStart = millis();
  soundPlaying = true;
}

// =====================================================
// UPDATE SOUND
// =====================================================

void updateSound() {
  if (!soundPlaying) return;

  byte length = 0;
  const SoundNote* notes = getSoundNotes(length);

  if (notes == nullptr || length == 0) {
    soundPlaying = false;
    noTone(BUZZER_PIN);
    currentSound = SOUND_NONE;
    return;
  }

  if (soundStep >= length) {
    soundPlaying = false;
    noTone(BUZZER_PIN);
    currentSound = SOUND_NONE;
    return;
  }

  const SoundNote &note = notes[soundStep];
  unsigned long elapsed = millis() - soundStepStart;
  unsigned long totalTime = note.duration + note.pause;

  if (elapsed < note.duration) {
    if (elapsed < 5) {
      tone(BUZZER_PIN, note.frequency);
    }
  } else {
    noTone(BUZZER_PIN);

    if (elapsed >= totalTime) {
      soundStep++;
      soundStepStart = millis();

      if (soundStep < length) {
        tone(BUZZER_PIN, notes[soundStep].frequency);
      }
    }
  }
}

// =====================================================
// ALL INJECTORS OFF
// =====================================================

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

// =====================================================
// STOP INJECTORS
// =====================================================

void stopInjectorTest(bool emergency) {
  allInjectorsOff();

  injectorRunning = false;
  continuousMode = false;
  currentInjector = 0;
  currentCycle = 0;

  if (emergency) {
    emergencyLock = true;
    playSound(SOUND_STOP);
  }
}

// =====================================================
// EEPROM LOAD
// =====================================================

void loadTriggerCounter() {
  uint32_t magic = 0;
  EEPROM.get(EEPROM_MAGIC_ADDR, magic);

  if (magic != EEPROM_MAGIC) {
    triggerCount = 0;

    EEPROM.put(EEPROM_MAGIC_ADDR, EEPROM_MAGIC);
    EEPROM.put(EEPROM_COUNTER_ADDR, triggerCount);
  } else {
    EEPROM.get(EEPROM_COUNTER_ADDR, triggerCount);
  }

  eepromDirty = false;
  lastEepromSave = millis();
}

// =====================================================
// EEPROM SAVE — THROTTLED
// =====================================================

void saveTriggerCounterIfNeeded() {
  if (!eepromDirty) return;

  if (millis() - lastEepromSave < EEPROM_SAVE_INTERVAL) {
    return;
  }

  EEPROM.put(EEPROM_COUNTER_ADDR, triggerCount);

  eepromDirty = false;
  lastEepromSave = millis();
}

// =====================================================
// RESET TRIGGER COUNTER
// =====================================================

void resetTriggerCounter() {
  triggerCount = 0;

  EEPROM.put(EEPROM_COUNTER_ADDR, triggerCount);

  eepromDirty = false;
  lastEepromSave = millis();
}

// =====================================================
// READ FILTERED POTENTIOMETER
// =====================================================

int readFilteredPot(byte pin, int &lastRaw) {
  long sum = 0;

  for (byte i = 0; i < POT_SAMPLES; i++) {
    sum += analogRead(pin);
  }

  int raw = sum / POT_SAMPLES;

  if (raw <= 3) raw = 0;
  if (raw >= 1020) raw = 1023;

  if (lastRaw >= 0 && abs(raw - lastRaw) < 3) {
    raw = lastRaw;
  }

  lastRaw = raw;
  return raw;
}

// =====================================================
// UPDATE POTENTIOMETERS
// =====================================================

void updatePots() {
  int rawPulse = readFilteredPot(PULSE_POT_PIN, lastPulseRaw);
  int rawCycles = readFilteredPot(CYCLE_POT_PIN, lastCycleRaw);

  pulseMs = map(rawPulse, 0, 1023, MIN_PULSE_MS, MAX_PULSE_MS);
  cycleCount = map(rawCycles, 0, 1023, MIN_CYCLES, MAX_CYCLES);

  pulseMs = constrain(pulseMs, MIN_PULSE_MS, MAX_PULSE_MS);
  cycleCount = constrain(cycleCount, MIN_CYCLES, MAX_CYCLES);
}

// =====================================================
// START INJECTOR TEST
// =====================================================

void startInjectorTest() {
  if (temperatureFault || overheatLock || emergencyLock) return;

  if (!temperatureValid || temperature >= OVERHEAT_TEMP) {
    if (temperatureValid && temperature >= OVERHEAT_TEMP) {
      overheatLock = true;
      emergencyLock = true;
      playSound(SOUND_OVERHEAT);
    }
    return;
  }

  injectorRunning = true;
  continuousMode = false;

  currentInjector = 0;
  currentCycle = 1;

  injectorStartTime = micros();

  digitalWrite(injectorPins[currentInjector], HIGH);

  playSound(SOUND_BUTTON);
}

// =====================================================
// START CONTINUOUS TEST
// =====================================================

void startContinuousTest() {
  if (temperatureFault || overheatLock || emergencyLock) return;

  if (!temperatureValid || temperature >= OVERHEAT_TEMP) {
    if (temperatureValid && temperature >= OVERHEAT_TEMP) {
      overheatLock = true;
      emergencyLock = true;
      playSound(SOUND_OVERHEAT);
    }
    return;
  }

  injectorRunning = true;
  continuousMode = true;

  currentInjector = 0;
  currentCycle = 1;

  injectorStartTime = micros();

  digitalWrite(injectorPins[currentInjector], HIGH);

  playSound(SOUND_BUTTON);
}

// =====================================================
// UPDATE INJECTORS
// =====================================================

void updateInjectors() {
  if (!injectorRunning) return;

  if (temperatureFault || overheatLock || emergencyLock) {
    allInjectorsOff();

    injectorRunning = false;
    continuousMode = false;
    currentInjector = 0;
    currentCycle = 0;

    return;
  }

  if (temperature >= OVERHEAT_TEMP) {
    allInjectorsOff();

    injectorRunning = false;
    continuousMode = false;
    currentInjector = 0;
    currentCycle = 0;

    overheatLock = true;
    emergencyLock = true;

    playSound(SOUND_OVERHEAT);
    return;
  }

  unsigned long elapsed = micros() - injectorStartTime;

  if (elapsed < (unsigned long)pulseMs * 1000UL) return;

  // Current injector pulse finished
  digitalWrite(injectorPins[currentInjector], LOW);

  currentInjector++;

  // All four injectors completed
  if (currentInjector >= 4) {
    currentInjector = 0;

    if (continuousMode) {
      // Continue only if the button is still held
      if (digitalRead(CONTINUOUS_BUTTON_PIN) == LOW) {
        currentCycle++;

        injectorStartTime = micros();
        digitalWrite(injectorPins[currentInjector], HIGH);
      } else {
        injectorRunning = false;
        continuousMode = false;
        currentCycle = 0;
      }

      return;
    }

    if (currentCycle >= cycleCount) {
      injectorRunning = false;
      currentCycle = 0;
      return;
    }

    currentCycle++;

    injectorStartTime = micros();
    digitalWrite(injectorPins[currentInjector], HIGH);
    return;
  }

  // Start the next injector
  injectorStartTime = micros();
  digitalWrite(injectorPins[currentInjector], HIGH);
}

// =====================================================
// TEMPERATURE TRIGGER LOGIC
// =====================================================

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

  if (triggerArmed) {
    triggerStartTemperature = temperature;
    triggerStartTime = now;

    triggerArmed = false;
    triggerAlreadyCounted = false;
    return;
  }

  float rise = temperature - triggerStartTemperature;
  unsigned long elapsed = now - triggerStartTime;

  // Count once if temperature rises by strictly MORE than 5 C
  // within the 2-second observation window.
  if (!triggerAlreadyCounted &&
      rise > TRIGGER_TEMP_RISE &&
      elapsed <= TRIGGER_WINDOW) {

    triggerCount++;
    triggerAlreadyCounted = true;
    eepromDirty = true;
  }

  // Rearm only after cooling at least 1 C below the baseline.
  if (temperature <= triggerStartTemperature - 1.0) {
    triggerArmed = true;
    triggerAlreadyCounted = false;
    return;
  }

  // If no trigger occurred, begin a fresh observation window.
  if (elapsed > TRIGGER_WINDOW && !triggerAlreadyCounted) {
    triggerStartTemperature = temperature;
    triggerStartTime = now;
  }
}

// =====================================================
// UPDATE TEMPERATURE — ASYNCHRONOUS
// =====================================================

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

  // Start conversion when none is pending.
  if (!temperatureConversionPending) {
    if (now - lastTemperatureRequest >= TEMP_READ_INTERVAL) {
      sensors.requestTemperatures();

      temperatureConversionPending = true;
      temperatureConversionStart = now;
      lastTemperatureRequest = now;
    }

    return;
  }

  // Wait without blocking the main loop.
  if (now - temperatureConversionStart < TEMP_CONVERSION_TIME) {
    return;
  }

  temperatureConversionPending = false;

  float newTemperature = sensors.getTempCByIndex(0);

  if (newTemperature == DEVICE_DISCONNECTED_C ||
      newTemperature < -55.0 ||
      newTemperature > 125.0) {

    temperatureValid = false;
    temperatureFault = true;
    emergencyLock = true;

    allInjectorsOff();

    injectorRunning = false;
    continuousMode = false;
    currentInjector = 0;
    currentCycle = 0;

    static bool errorAlreadyReported = false;

    if (!errorAlreadyReported) {
      playSound(SOUND_ERROR);
      errorAlreadyReported = true;
    }

    return;
  }

  temperature = newTemperature;
  temperatureValid = true;
  temperatureFault = false;

  // Reset the one-shot error notification after recovery.
  static bool errorAlreadyReported = false;
  errorAlreadyReported = false;

  // Overheat protection
  if (temperature >= OVERHEAT_TEMP) {
    if (!overheatLock) {
      overheatLock = true;
      emergencyLock = true;

      allInjectorsOff();

      injectorRunning = false;
      continuousMode = false;
      currentInjector = 0;
      currentCycle = 0;

      playSound(SOUND_OVERHEAT);
    }

    updateTemperatureTrigger();
    return;
  }

  // Release the temperature lock after sufficient cooling.
  // Emergency lock remains until manual acknowledgement.
  if (overheatLock && temperature <= COOL_DOWN_TEMP) {
    overheatLock = false;
  }

  updateTemperatureTrigger();
}

// =====================================================
// BUTTON DEBOUNCE
// Returns true after a stable LOW has lasted long enough.
// =====================================================

bool buttonPressed(byte pin,
                   bool &lastState,
                   unsigned long &lastChange) {
  bool state = digitalRead(pin);

  if (state != lastState) {
    lastChange = millis();
    lastState = state;
  }

  return state == LOW &&
         millis() - lastChange >= DEBOUNCE_TIME;
}

// =====================================================
// START BUTTON
// =====================================================

void updateStartButton() {
  static bool handled = false;

  bool pressed = buttonPressed(
    START_BUTTON_PIN, lastStartState, lastStartChange
  );

  if (!pressed) {
    handled = false;
    return;
  }

  if (handled) return;
  handled = true;

  // Do not acknowledge an emergency while a fault persists.
  if (temperatureFault || overheatLock) {
    playSound(SOUND_ERROR);
    return;
  }

  if (emergencyLock) {
    emergencyLock = false;
    playSound(SOUND_BUTTON);
    return;
  }

  if (injectorRunning) {
    stopInjectorTest(true);
    return;
  }

  startInjectorTest();
}

// =====================================================
// CONTINUOUS BUTTON
// =====================================================

void updateContinuousButton() {
  bool pressed = buttonPressed(
    CONTINUOUS_BUTTON_PIN,
    lastContinuousState,
    lastContinuousChange
  );

  static bool wasPressed = false;

  if (pressed && !wasPressed) {
    wasPressed = true;

    if (!injectorRunning) {
      startContinuousTest();
    }
  }

  if (!pressed && wasPressed) {
    wasPressed = false;

    // Do not stop immediately. The active 4-injector cycle
    // is allowed to finish by updateInjectors().
  }
}

// =====================================================
// R1 BUTTON
// =====================================================

void updateRelay1() {
  bool pressed = buttonPressed(
    RELAY1_BUTTON_PIN, lastR1State, lastR1Change
  );

  static bool wasPressed = false;

  if (pressed && !wasPressed) {
    wasPressed = true;

    if (!injectorRunning && !emergencyLock &&
        !temperatureFault && !overheatLock) {
      relay1Active = true;
      relay1StartTime = millis();

      digitalWrite(RELAY1_PIN, HIGH);
    }
  }

  if (!pressed) {
    wasPressed = false;
  }

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

// =====================================================
// R2 BUTTON — ACTIVE WHILE HELD
// =====================================================

void updateRelay2() {
  bool pressed = buttonPressed(
    RELAY2_BUTTON_PIN, lastR2State, lastR2Change
  );

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

// =====================================================
// OLED STATUS
// =====================================================

void printStatus() {
  display.clearDisplay();

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

  // Line 1
  display.setCursor(0, 0);
  display.print(F("PULSE:"));
  display.print(pulseMs);
  display.print(F("ms"));

  display.setCursor(70, 0);
  display.print(F("N:"));
  display.print(cycleCount);

  // Temperature
  display.setCursor(0, 11);
  display.print(F("TEMP:"));

  if (temperatureFault || !temperatureValid) {
    display.print(F("ERROR"));
  } else {
    display.print(temperature, 1);
    display.print(F(" C"));
  }

  // Main status
  display.setCursor(0, 23);

  if (temperatureFault || !temperatureValid) {
    display.print(F("SENSOR ERROR"));
  } else if (overheatLock) {
    display.print(F("OVERHEAT"));
  } else if (emergencyLock) {
    display.print(F("EMERGENCY LOCK"));
  } else if (injectorRunning) {
    display.print(continuousMode ? F("CONTINUOUS") : F("RUN"));
  } else {
    display.print(F("READY"));
  }

  // Injector / cycle
  display.setCursor(0, 35);

  if (injectorRunning) {
    display.print(F("INJ:"));
    display.print(currentInjector + 1);

    display.setCursor(58, 35);
    display.print(F("CYCLE:"));
    display.print(currentCycle);

    if (!continuousMode) {
      display.print('/');
      display.print(cycleCount);
    }
  } else {
    display.print(F("INJ:OFF"));
  }

  // Relays
  display.setCursor(0, 47);
  display.print(F("R1:"));

  if (relay1Active) {
    unsigned long elapsed = millis() - relay1StartTime;
    long remaining = (long)((R1_TIME - elapsed + 999) / 1000);

    if (remaining < 0) remaining = 0;

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

  display.setCursor(65, 47);
  display.print(F("R2:"));
  display.print(relay2Active ? F("ON") : F("OFF"));

  // Counter — shifted up to fit the 64-pixel screen
  display.setCursor(0, 56);
  display.print(F("TRIG:"));
  display.print(triggerCount);

  display.display();
}

// =====================================================
// STARTUP SCREEN
// =====================================================

void startupScreen() {
  unsigned long startTime = millis();

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

    int percent = (elapsed * 100UL) / 5000UL;
    percent = constrain(percent, 0, 100);

    int barWidth = map(percent, 0, 100, 0, 108);

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

    display.setCursor(30, 8);
    display.print(F("INJECTOR TEST"));

    display.setCursor(45, 22);
    display.print(F("STARTING..."));

    display.drawRect(10, 38, 108, 10, SSD1306_WHITE);

    if (barWidth > 0) {
      display.fillRect(12, 40, barWidth - 2, 6, SSD1306_WHITE);
    }

    display.setCursor(52, 52);
    display.print(percent);
    display.print('%');

    display.display();
    delay(20);
  }

  playSound(SOUND_STARTUP);
}

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

void setup() {
  // Injector outputs
  pinMode(INJ1_PIN, OUTPUT);
  pinMode(INJ2_PIN, OUTPUT);
  pinMode(INJ3_PIN, OUTPUT);
  pinMode(INJ4_PIN, OUTPUT);

  allInjectorsOff();

  // Relay outputs
  pinMode(RELAY1_PIN, OUTPUT);
  pinMode(RELAY2_PIN, OUTPUT);

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

  // Buttons
  pinMode(START_BUTTON_PIN, INPUT_PULLUP);
  pinMode(RELAY1_BUTTON_PIN, INPUT_PULLUP);
  pinMode(RELAY2_BUTTON_PIN, INPUT_PULLUP);
  pinMode(CONTINUOUS_BUTTON_PIN, INPUT_PULLUP);

  // Buzzer
  pinMode(BUZZER_PIN, OUTPUT);
  noTone(BUZZER_PIN);

  // OLED
  Wire.begin();

  if (!display.begin(SSD1306_SWITCHCAPVCC, OLED_ADDR)) {
    // If the OLED fails, keep outputs in a safe state.
    allInjectorsOff();
    digitalWrite(RELAY1_PIN, LOW);
    digitalWrite(RELAY2_PIN, LOW);

    while (true) {
      noTone(BUZZER_PIN);
    }
  }

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

  // Temperature sensor — non-blocking conversion
  sensors.begin();
  sensors.setResolution(12);
  sensors.setWaitForConversion(false);

  // EEPROM counter
  loadTriggerCounter();

  // Hold R1 + R2 during power-on to reset TRIG.
  delay(100);

  if (digitalRead(RELAY1_BUTTON_PIN) == LOW &&
      digitalRead(RELAY2_BUTTON_PIN) == LOW) {

    resetTriggerCounter();

    display.clearDisplay();
    display.setTextSize(1);
    display.setTextColor(SSD1306_WHITE);
    display.setCursor(20, 25);
    display.print(F("TRIGGER RESET"));
    display.display();

    while (digitalRead(RELAY1_BUTTON_PIN) == LOW ||
           digitalRead(RELAY2_BUTTON_PIN) == LOW) {
      delay(10);
    }

    delay(800);
  }

  // Request the first temperature conversion.
  sensors.requestTemperatures();
  temperatureConversionPending = true;
  temperatureConversionStart = millis();
  lastTemperatureRequest = millis();

  // Stay locked until a valid temperature has been read
  // and the operator acknowledges readiness with START.
  temperatureValid = false;
  temperatureFault = true;
  emergencyLock = true;

  startupScreen();
}

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

void loop() {
  updateSound();

  updateTemperature();

  updatePots();

  updateStartButton();
  updateContinuousButton();

  updateRelay1();
  updateRelay2();

  updateInjectors();

  saveTriggerCounterIfNeeded();

  if (millis() - lastDisplayUpdate >= DISPLAY_INTERVAL) {
    lastDisplayUpdate = millis();
    printStatus();
  }
}