Skip to content

Safety & Monitoring Systems

Overview

The regulator implements multiple independent safety systems to protect the alternator, battery, electrical system, and controller hardware. These systems operate at different response times and protection levels, from immediate hardware shutdowns to adaptive learning algorithms.

Protection Layer Architecture

Layer 1: Hardware Protection (< 1ms response)

  • INA228 overvoltage detection: Independent hardware threshold monitoring
  • GPIO emergency shutdown: Software-triggered immediate MOSFET disable
  • Flyback diode protection: Field coil inductive spike suppression

Layer 2: Real-time Software Protection (50ms response)

  • Emergency field collapse: Voltage spike detection and rapid shutdown
  • Temperature limits: Immediate field reduction on overheating
  • Current limits: Battery and alternator current protection

Layer 3: Adaptive Protection (seconds to minutes)

  • Learning mode penalties: Thermal history-based current reduction
  • Charging stage management: Bulk/float transitions for battery protection
  • Dynamic sensor validation: Cross-checking redundant measurements

Layer 4: System Health Monitoring (continuous)

  • Watchdog protection: 15-second hang detection with restart
  • Memory monitoring: Heap and stack overflow detection
  • Performance tracking: Loop time and CPU utilization analysis

Hardware Protection Systems

INA228 Overvoltage Protection

The INA228 provides independent hardware monitoring with configurable voltage thresholds:

// Configure hardware overvoltage threshold
uint16_t thresholdLSB = (uint16_t)(VoltageHardwareLimit / 0.003125);
INA.setBusOvervoltageTH(thresholdLSB);
INA.setDiagnoseAlertBit(INA228_DIAG_BUS_OVER_LIMIT);

// Enable latching alert with ALERT pin control
Wire.beginTransmission(INA.getAddress());
Wire.write(0x0F);  // ALERT_MASK_ENABLE register
Wire.write(0x98);  // Enable ALERT pin + latch + bus overvoltage
Wire.write(0x00);
Wire.endTransmission();

Operation: - Threshold: BulkVoltage + 0.1V (typically 14.0V for 12V systems) - Response time: <1ms hardware detection - Action: Pulls ALERT pin low, sets status bit - Recovery: Automatic when voltage drops below threshold

Monitoring Loop:

// Check for hardware overvoltage alert
uint16_t alertStatus = readINA228AlertRegister(INA.getAddress());
if (!inaOvervoltageLatched && (alertStatus & 0x0080)) {
  inaOvervoltageLatched = true;
  inaOvervoltageTime = millis();
  queueConsoleMessage("INA228 hardware overvoltage detected! Field disabled until corrected");
}

// Auto-clear after 10 seconds if condition resolved
if (inaOvervoltageLatched && millis() - inaOvervoltageTime >= 10000) {
  clearINA228AlertLatch(INA.getAddress());
  if (!(readINA228AlertRegister(INA.getAddress()) & 0x0080)) {
    inaOvervoltageLatched = false;
    queueConsoleMessage("INA228 overvoltage condition cleared");
  }
}

Field Coil Protection

Inductive kickback protection prevents MOSFET damage during field switching:

Circuit Design:

Battery 12V → Field Coil (2-6Ω, ~50mH) → MOSFET → Ground
                ↑
            Flyback Diode (1N4007 or equivalent)

Protection Mechanisms: - Flyback diode: Provides current path during MOSFET turn-off - MOSFET ratings: Selected for 2x maximum field current and voltage - PWM frequency: 15kHz above audible range, below EMI concerns

Real-Time Software Protection

Emergency Field Collapse

Immediate field shutdown on dangerous voltage spikes:

void emergencyFieldCollapse() {
  if (currentBatteryVoltage > (ChargingVoltageTarget + 0.2)) {
    digitalWrite(4, 0);              // Immediate MOSFET disable
    dutyCycle = MinDuty;
    setDutyPercent((int)dutyCycle);
    fieldCollapseTime = currentTime;
    queueConsoleMessage("EMERGENCY: Field collapsed - voltage spike (" + 
                       String(currentBatteryVoltage, 2) + "V)");
    return;  // Exit field control immediately
  }
}

Recovery Logic:

// Maintain shutdown for 10 seconds
if (fieldCollapseTime > 0 && (currentTime - fieldCollapseTime) < FIELD_COLLAPSE_DELAY) {
  digitalWrite(4, 0);
  dutyCycle = MinDuty;
  setDutyPercent((int)dutyCycle);
  return;
}

// Clear flag and resume normal operation
if (fieldCollapseTime > 0 && (currentTime - fieldCollapseTime) >= FIELD_COLLAPSE_DELAY) {
  fieldCollapseTime = 0;
  queueConsoleMessage("Field collapse delay expired - normal operation resumed");
}

Parameters: - Trigger threshold: ChargingVoltageTarget + 0.2V - Lockout time: 10 seconds (FIELD_COLLAPSE_DELAY) - Response time: <50ms (next control cycle)

Temperature Protection

Hierarchical temperature protection with increasing severity:

void temperatureProtection() {
  // Determine active temperature source
  if (TempSource == 0) {
    TempToUse = AlternatorTemperatureF;  // OneWire DS18B20
  } else {
    TempToUse = temperatureThermistor;   // ADS1115 thermistor
  }

  // Progressive protection levels
  if (!IgnoreTemperature && TempToUse > TemperatureLimitF) {
    if (dutyCycle > (MinDuty + 2 * dutyStep)) {
      dutyCycle -= 2 * dutyStep;  // Aggressive 1.6% reduction
      queueConsoleMessage("Temperature limit reached, backing off...");
    }
  }

  // Emergency temperature shutdown
  if (TempToUse > (TemperatureLimitF + 20)) {
    dutyCycle = MinDuty;
    digitalWrite(4, 0);
    queueConsoleMessage("EMERGENCY: Temperature excessive - field shutdown");
  }
}

Temperature Sources: - OneWire DS18B20: Digital sensor, ±0.5°C accuracy, noise immune - Thermistor: Analog sensor via ADS1115, faster response - Source selection: User configurable via TempSource setting

Current Limiting

Multiple current protection systems prevent equipment damage:

void currentProtection() {
  // Battery current protection
  if (Bcur > MaximumAllowedBatteryAmps && dutyCycle > (MinDuty + dutyStep)) {
    dutyCycle -= dutyStep;
    queueConsoleMessage("Battery current limit reached, backing off...");
  }

  // Alternator current protection (implicit via voltage regulation)
  // High alternator current → voltage drop → increased field → voltage recovery

  // BMS current limit integration
  if (bmsLogic == 1) {
    bmsSignalActive = !digitalRead(36);
    if ((bmsLogicLevelOff == 0 && !bmsSignalActive) || 
        (bmsLogicLevelOff == 1 && bmsSignalActive)) {
      // BMS requesting charge stop
      chargingEnabled = false;
      queueConsoleMessage("BMS requesting charge stop");
    }
  }
}

Voltage Protection

Multi-level voltage protection prevents overcharging:

void voltageProtection() {
  float currentBatteryVoltage = getBatteryVoltage();

  // Standard voltage regulation
  if (currentBatteryVoltage > ChargingVoltageTarget && 
      dutyCycle > (MinDuty + 3 * dutyStep)) {
    dutyCycle -= 3 * dutyStep;  // Most aggressive: 2.4% reduction
    queueConsoleMessage("Voltage limit reached, backing off...");
  }

  // Cross-validation between voltage sensors
  if (abs(BatteryV - IBV) > 0.1) {
    queueConsoleMessage("Voltage sensor disagreement - Field shut off for safety!");
    digitalWrite(33, HIGH);  // Alarm
    digitalWrite(4, 0);      // Field disable
    dutyCycle = MinDuty;
    return;
  }
}

Voltage Sources Compared: - BatteryV: ADS1115 via voltage divider (1MΩ/75kΩ) - IBV: INA228 high-precision measurement - Cross-validation: 0.1V maximum disagreement allowed

Alarm System

Alarm Conditions

The system monitors multiple parameters and triggers alarms when limits are exceeded:

void CheckAlarms() {
  static unsigned long lastRunTime = 0;
  if (millis() - lastRunTime < 250) return;  // 250ms update rate
  lastRunTime = millis();

  bool currentAlarmCondition = false;
  String alarmReason = "";

  if (AlarmActivate == 1) {
    // Temperature alarm
    if (TempAlarm > 0 && TempToUse > TempAlarm) {
      currentAlarmCondition = true;
      alarmReason = "High alternator temperature: " + String(TempToUse) + 
                   "°F (limit: " + String(TempAlarm) + "°F)";
    }

    // Voltage alarms
    float currentVoltage = getBatteryVoltage();
    if (VoltageAlarmHigh > 0 && currentVoltage > VoltageAlarmHigh) {
      currentAlarmCondition = true;
      alarmReason = "High battery voltage: " + String(currentVoltage, 2) + 
                   "V (limit: " + String(VoltageAlarmHigh) + "V)";
    }

    if (VoltageAlarmLow > 0 && currentVoltage < VoltageAlarmLow && currentVoltage > 8.0) {
      currentAlarmCondition = true;
      alarmReason = "Low battery voltage: " + String(currentVoltage, 2) + 
                   "V (limit: " + String(VoltageAlarmLow) + "V)";
    }

    // Current alarms
    if (CurrentAlarmHigh > 0 && MeasuredAmps > CurrentAlarmHigh) {
      currentAlarmCondition = true;
      alarmReason = "High alternator current: " + String(MeasuredAmps, 1) + 
                   "A (limit: " + String(CurrentAlarmHigh) + "A)";
    }
  }

  // Apply alarm output with latching logic
  processAlarmOutput(currentAlarmCondition, alarmReason);
}

Alarm Processing and Latching

void processAlarmOutput(bool currentCondition, String reason) {
  static bool previousAlarmState = false;
  bool outputAlarmState = false;

  // Handle alarm test (always works regardless of AlarmActivate)
  if (AlarmTest == 1) {
    if (alarmTestStartTime == 0) {
      alarmTestStartTime = millis();
      queueConsoleMessage("ALARM TEST: Testing buzzer for 2 seconds");
    }

    if (millis() - alarmTestStartTime < ALARM_TEST_DURATION) {
      currentCondition = true;
    } else {
      AlarmTest = 0;
      alarmTestStartTime = 0;
    }
  }

  // Handle manual latch reset
  if (ResetAlarmLatch == 1) {
    alarmLatch = false;
    ResetAlarmLatch = 0;
    queueConsoleMessage("ALARM LATCH: Manually reset");
  }

  // Latching logic
  if (AlarmLatchEnabled == 1) {
    if (currentCondition) alarmLatch = true;
    outputAlarmState = alarmLatch;
  } else {
    outputAlarmState = currentCondition;
  }

  // Final output control
  bool finalOutput = false;
  if (AlarmTest == 1 || (AlarmActivate == 1 && outputAlarmState)) {
    finalOutput = true;
  }

  digitalWrite(33, finalOutput ? HIGH : LOW);

  // Console messaging
  if (currentCondition != previousAlarmState) {
    if (currentCondition) {
      queueConsoleMessage("ALARM ACTIVATED: " + reason);
    } else if (AlarmLatchEnabled == 0) {
      queueConsoleMessage("ALARM CLEARED");
    }
    previousAlarmState = currentCondition;
  }
}

Alarm Features: - Configurable thresholds: User-adjustable limits for all parameters - Latching mode: Alarm stays on until manually reset - Test function: 2-second buzzer test - Manual reset: Web interface reset capability

System Health Monitoring

Watchdog Protection

15-second watchdog timer prevents system hangs:

void setupWatchdog() {
  esp_task_wdt_config_t wdt_config = {
    .timeout_ms = 15000,   // 15 seconds
    .idle_core_mask = 0,   // Don't monitor idle cores
    .trigger_panic = true  // Reboot on timeout
  };
  esp_task_wdt_init(&wdt_config);
  esp_task_wdt_add(NULL);  // Add main loop task
  queueConsoleMessage("Watchdog enabled: 15 second timeout for safety");
}

void loop() {
  esp_task_wdt_reset();  // Feed watchdog every loop iteration
  // ... main loop code ...
}

Protection Logic: - Timeout: 15 seconds without watchdog reset - Action: Hardware reset of ESP32 - Recovery: Full system restart with safety state - Field safety: GPIO pins reset to LOW → field disabled

Memory Monitoring

Continuous heap and stack monitoring with early warning:

void updateSystemHealthMetrics() {
  // Heap monitoring
  rawFreeHeap = esp_get_free_heap_size();
  FreeHeap = rawFreeHeap / 1024;  // Convert to KB
  MinFreeHeap = esp_get_minimum_free_heap_size() / 1024;
  FreeInternalRam = heap_caps_get_free_size(MALLOC_CAP_INTERNAL) / 1024;

  // Fragmentation calculation
  if (rawFreeHeap == 0) {
    Heapfrag = 100;
  } else {
    Heapfrag = 100 - ((heap_caps_get_largest_free_block(MALLOC_CAP_8BIT) * 100) / rawFreeHeap);
  }

  // Critical heap warnings (throttled)
  unsigned long now = millis();
  if (FreeHeap < 20 && (now - lastHeapWarningTime > WARNING_THROTTLE_INTERVAL)) {
    queueConsoleMessage("CRITICAL: Heap dangerously low (" + String(FreeHeap) + "KB)");
    lastHeapWarningTime = now;
  }
}

Stack Monitoring:

void printBasicTaskStackInfo() {
  numTasks = uxTaskGetNumberOfTasks();
  if (numTasks > MAX_TASKS) numTasks = MAX_TASKS;

  tasksCaptured = uxTaskGetSystemState(taskArray, numTasks, NULL);

  for (int i = 0; i < tasksCaptured; i++) {
    stackBytes = taskArray[i].usStackHighWaterMark * sizeof(StackType_t);
    const char* taskName = taskArray[i].pcTaskName;

    if (stackBytes < 256) {
      queueConsoleMessage("CRITICAL: " + String(taskName) + " stack very low (" + 
                         String(stackBytes) + "B)");
    }
  }
}

Performance Monitoring

Loop timing and CPU utilization tracking:

void loop() {
  starttime = esp_timer_get_time();  // Start timing

  // ... main loop execution ...

  endtime = esp_timer_get_time();
  LoopTime = (endtime - starttime);  // Microseconds

  if (LoopTime > 5000000) {  // 5 seconds
    queueConsoleMessage("WARNING: Loop took " + String(LoopTime / 1000) + 
                       "ms - potential watchdog risk");
  }

  // Track maximum times
  if (LoopTime > MaximumLoopTime) MaximumLoopTime = LoopTime;
  if (LoopTime > MaxLoopTime) MaxLoopTime = LoopTime;  // Persistent across sessions
}

CPU Load Tracking:

void updateCpuLoad() {
  // Get IDLE task runtime counters
  for (int i = 0; i < taskCount; i++) {
    if (strcmp(taskSnapshot[i].pcTaskName, "IDLE0") == 0) {
      idle0Time = taskSnapshot[i].ulRunTimeCounter;
    } else if (strcmp(taskSnapshot[i].pcTaskName, "IDLE1") == 0) {
      idle1Time = taskSnapshot[i].ulRunTimeCounter;
    }
  }

  // Calculate CPU load as percentage
  unsigned long deltaIdle0 = idle0Time - lastIdle0Time;
  unsigned long deltaIdle1 = idle1Time - lastIdle1Time;
  unsigned long timeDiff = millis() - lastCheckTime;

  cpuLoadCore0 = 100 - ((deltaIdle0 * 100) / (timeDiff * 100));
  cpuLoadCore1 = 100 - ((deltaIdle1 * 100) / (timeDiff * 100));

  cpuLoadCore0 = constrain(cpuLoadCore0, 0, 100);
  cpuLoadCore1 = constrain(cpuLoadCore1, 0, 100);
}

Data Validation and Sensor Safety

Sensor Freshness Tracking

Data age monitoring prevents control decisions based on stale readings:

// Global data freshness system
enum DataIndex {
  IDX_ALTERNATOR_TEMP = 0,
  IDX_BATTERY_V,
  IDX_MEASURED_AMPS,
  // ... 17 total sensor data indices
  MAX_DATA_INDICES = 17
};

unsigned long dataTimestamps[MAX_DATA_INDICES];
const unsigned long DATA_TIMEOUT = 10000;  // 10 seconds

// Mark data fresh when successfully read
#define MARK_FRESH(index) dataTimestamps[index] = millis()

// Check if data is stale
#define IS_STALE(index) (millis() - dataTimestamps[index] > DATA_TIMEOUT)

// Conditional assignment for stale data
#define SET_IF_STALE(index, variable, staleValue) \
  if (IS_STALE(index)) { variable = staleValue; }

Usage in Control Systems:

// Check temperature data freshness for safety
unsigned long tempAge = currentTime - dataTimestamps[IDX_ALTERNATOR_TEMP];
bool tempDataVeryStale = (tempAge > 30000);  // 30 seconds
if (tempDataVeryStale) {
  queueConsoleMessage("OneWire sensor stale - sensor dead or disconnected");
  digitalWrite(33, HIGH);  // Sound alarm
}

Sensor Cross-Validation

Multiple sensors for critical measurements with disagreement detection:

float getBatteryVoltage() {
  float selectedVoltage = 0;
  static unsigned long lastWarningTime = 0;

  switch (BatteryVoltageSource) {
    case 0:  // INA228 (preferred)
      if (!IS_STALE(IDX_IBV) && IBV > 8.0 && IBV < 70.0) {
        selectedVoltage = IBV;
      } else {
        if (millis() - lastWarningTime > 10000) {
          queueConsoleMessage("INA228 unavailable, falling back to ADS1115");
          lastWarningTime = millis();
        }
        selectedVoltage = BatteryV;  // Automatic fallback
      }
      break;

    case 1:  // ADS1115 (backup)
      if (!IS_STALE(IDX_BATTERY_V) && BatteryV > 8.0 && BatteryV < 70.0) {
        selectedVoltage = BatteryV;
      } else {
        queueConsoleMessage("ADS1115 unavailable, falling back to INA228");
        selectedVoltage = IBV;
      }
      break;
  }

  // Final validation
  if (selectedVoltage < 8.0 || selectedVoltage > 70.0 || isnan(selectedVoltage)) {
    queueConsoleMessage("CRITICAL: No valid battery voltage found!");
    selectedVoltage = 999;  // Error indication
  }

  return selectedVoltage;
}

Thermal Stress Monitoring

Alternator Lifetime Modeling

Continuous thermal stress calculation based on Arrhenius equation:

void calculateThermalStress() {
  unsigned long now = millis();
  if (now - lastThermalUpdateTime < THERMAL_UPDATE_INTERVAL) return;

  float elapsedSeconds = (now - lastThermalUpdateTime) / 1000.0f;
  lastThermalUpdateTime = now;

  // Calculate component temperatures
  float T_winding_F = TempToUse + WindingTempOffset;      // User-configurable offset
  float T_bearing_F = TempToUse + 40.0f;                  // Fixed offset
  float T_brush_F = TempToUse + 50.0f;                    // Fixed offset

  // Calculate alternator RPM
  float Alt_RPM = RPM * PulleyRatio;

  // Calculate component life expectancies (hours)
  float T_winding_K = (T_winding_F - 32.0f) * 5.0f / 9.0f + 273.15f;
  float L_insul = L_REF_INSUL * exp(EA_INSULATION / BOLTZMANN_K * 
                                   (1.0f / T_winding_K - 1.0f / T_REF_K));

  float L_grease = L_REF_GREASE * pow(0.5f, (T_bearing_F - 158.0f) / 18.0f) * 
                   (6000.0f / max(Alt_RPM, 100.0f));

  float temp_factor = 1.0f + 0.0025f * (T_brush_F - 150.0f);
  float L_brush = (L_REF_BRUSH * 6000.0f / max(Alt_RPM, 100.0f)) / max(temp_factor, 0.1f);

  // Accumulate damage over time
  float hours_elapsed = elapsedSeconds / 3600.0f;
  CumulativeInsulationDamage += hours_elapsed / L_insul;
  CumulativeGreaseDamage += hours_elapsed / L_grease;
  CumulativeBrushDamage += hours_elapsed / L_brush;

  // Calculate remaining life percentages
  InsulationLifePercent = (1.0f - CumulativeInsulationDamage) * 100.0f;
  GreaseLifePercent = (1.0f - CumulativeGreaseDamage) * 100.0f;
  BrushLifePercent = (1.0f - CumulativeBrushDamage) * 100.0f;

  // Constrain to 0-100% range
  InsulationLifePercent = constrain(InsulationLifePercent, 0.0f, 100.0f);
  GreaseLifePercent = constrain(GreaseLifePercent, 0.0f, 100.0f);
  BrushLifePercent = constrain(BrushLifePercent, 0.0f, 100.0f);
}

Thermal Model Constants: - L_REF_INSUL: 100,000 hours at 100°C reference - L_REF_GREASE: 40,000 hours at 158°F reference
- L_REF_BRUSH: 5,000 hours at 6000 RPM, 150°F - EA_INSULATION: 1.0 eV activation energy - BOLTZMANN_K: 8.617×10⁻⁵ eV/K

Console Logging and Debugging

Message Queue System

Thread-safe circular buffer for system messages:

// Fixed-size circular buffer for thread safety
struct ConsoleMessage {
  char message[128];      // Fixed size prevents dynamic allocation
  unsigned long timestamp;
};

#define CONSOLE_QUEUE_SIZE 10
ConsoleMessage consoleQueue[CONSOLE_QUEUE_SIZE];
volatile int consoleHead = 0;
volatile int consoleTail = 0;
volatile int consoleCount = 0;

void queueConsoleMessage(String message) {
  // Prevent stack overflow from oversized messages
  if (message.length() > 120) {
    message = message.substring(0, 120) + "...";
  }

  // Thread-safe circular buffer operation
  int nextHead = (consoleHead + 1) % CONSOLE_QUEUE_SIZE;
  int localTail = consoleTail;

  if (nextHead != localTail) {  // Not full
    strncpy(consoleQueue[consoleHead].message, message.c_str(), 127);
    consoleQueue[consoleHead].message[127] = '\0';
    consoleQueue[consoleHead].timestamp = millis();
    consoleHead = nextHead;

    if (consoleCount < CONSOLE_QUEUE_SIZE) {
      consoleCount++;
    }
  }
  // If full, oldest message is automatically overwritten
}

Warning Throttling

Prevents console spam from repeated conditions:

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

  // Critical heap warnings (throttled to 30 seconds)
  if (FreeHeap < 20 && (now - lastHeapWarningTime > WARNING_THROTTLE_INTERVAL)) {
    queueConsoleMessage("CRITICAL: Heap dangerously low (" + String(FreeHeap) + "KB)");
    lastHeapWarningTime = now;
  }

  // Stack warnings (throttled)
  if (stackBytes < 256 && (now - lastStackWarningTime > WARNING_THROTTLE_INTERVAL)) {
    queueConsoleMessage("CRITICAL: " + String(taskName) + " stack very low");
    lastStackWarningTime = now;
  }
}

Configuration Parameters

Safety Thresholds

Parameter Default Range Description
TemperatureLimitF 150 100-250 Alternator temperature limit (°F)
VoltageAlarmHigh 15 12-18 High voltage alarm threshold
VoltageAlarmLow 11 8-12 Low voltage alarm threshold
CurrentAlarmHigh 100 50-200 High current alarm threshold
MaximumAllowedBatteryAmps 100 50-500 Battery current protection limit

System Health Parameters

Parameter Default Range Description
WARNING_THROTTLE_INTERVAL 30000 5000-300000 Warning message throttle (ms)
DATA_TIMEOUT 10000 1000-60000 Sensor freshness timeout (ms)
FIELD_COLLAPSE_DELAY 10000 5000-30000 Emergency shutdown lockout (ms)
ALARM_TEST_DURATION 2000 1000-10000 Alarm test duration (ms)

Thermal Model Parameters

Parameter Default Range Description
WindingTempOffset 50.0 0-100 Winding temperature offset (°F)
PulleyRatio 2.0 1.0-4.0 Engine to alternator speed ratio
THERMAL_UPDATE_INTERVAL 10000 1000-60000 Thermal calculation rate (ms)

Error Recovery Procedures

Automatic Recovery Systems

  • Watchdog reset: Complete system restart on hang
  • Sensor fallback: Automatic switch to backup sensors
  • Field collapse recovery: 10-second lockout with automatic resume
  • Memory management: Garbage collection and stack monitoring

Manual Recovery Options

  • Factory reset: GPIO15 pin recovery mode
  • Alarm reset: Web interface manual reset
  • Configuration reset: Delete individual setting files
  • Complete restart: 2-hour automatic maintenance restart

This comprehensive safety and monitoring system ensures reliable operation while providing detailed diagnostics and protection against all major failure modes.