Skip to content

Field Control System Architecture


System Overview

This is an alternator field controller for marine/vehicle applications featuring: - Cascaded PID control with outer voltage loop and inner current loop - Adaptive learning tables that adjust current limits based on thermal feedback - Multi-tier setpoint caps (MaxTableValue, RPM-dependent mechanical limits, MaximumAllowedBatteryAmps) - Multi-tier fault protection (temperature, voltage, sensor validation) - Two-phase field shutdown protecting LM2907 tachometer circuitry from rapid changes in stator waveform - Dual charging stages (Bulk and Float) with automatic transitions - Persistent learning tables stored in NVS - Web-configurable parameters stored in LittleFS


Main Loop Architecture

┌─────────────────────────────────────────────────────────────────────┐
│                        MAIN LOOP (every tick)                       │
├─────────────────────────────────────────────────────────────────────┤
│  1. Build TickSnapshot (immutable state capture)                    │
│  2. Update sustained timers (temp warning, voltage disagreement)    │
│  3. Update charging stage (bulk ↔ float transitions)                │
│  4. Administrative actions (table reset, clear history)             │
│  5. Select mode via pure function (priority-based)                  │
│  6. Detect mode transitions, handle state resets                    │
│  7. Execute mode:                                                   │
│     ├─ NON-NORMAL: Two-phase shutdown                               │
│     │   a) Check immediate cut (critical temp only)                 │
│     │   b) Ramp duty to 0% with rate limiting                       │
│     │   c) Cut GPIO4 after settle (or immediately for critical)     │
│     │   d) Start lockout timer if WARNING/CRITICAL                  │
│     └─ NORMAL: Cascaded PID control                                 │
│         a) Get target from learning table (RPM-based lookup)        │
│         b) Apply corrections (ambient temp, overheat penalty)       │
│         c) Apply modifiers (HiLow, ForceFloat)                      │
│         d) Apply setpoint caps (MaxTableValue, RPM cap, battery cap)│
│         e) Voltage outer loop (float stage only): cap current       │
│         f) Rate-limit setpoint changes                              │
│         g) Inner PID compute → duty cycle                           │
│         h) Soft limiters (temp, voltage)                            │
│         i) Process learning logic (if enabled and conditions met)   │
│  8. Apply duty with rate limiting + RPM minimum enforcement         │
│  9. Update telemetry                                                │
│ 10. Calculate diagnostics (1Hz)                                     │
└─────────────────────────────────────────────────────────────────────┘

Mode Priority Hierarchy

Modes are selected via pure functions based on immutable TickSnapshot. Higher priority wins.

Priority Mode Trigger Conditions GPIO4 Alarm Lockout
1 MODE_CRITICAL_RAMP Temp stale, temp critical (+30°F), voltage implausible, voltage disagree critical Cut (immediate for temp critical) ON No
2 MODE_WARNING_RAMP_AND_LOCKOUT Voltage spike, voltage disagree warning, temp warning (+10°F) Cut after settle ON Yes
3 MODE_LOCKOUT_RAMP Auto-zero active, lockout timer active Cut after settle ON N/A
4 MODE_DISABLED_RAMP Charging disabled (ignition off, OnOff=0, BMS signal, weather mode) Cut after settle OFF No
5 MODE_NORMAL_MANUAL Manual mode enabled HIGH OFF No
6 MODE_NORMAL_AUTO_PID Default normal operation HIGH OFF No

Two-Phase Shutdown Mechanism

Purpose: Protect LM2907 tachometer IC's AC coupling capacitor (τ = RC = 100kΩ × 20µF = 2.0s) from sending stator pulse waveform (RPM input) outside measurable range

┌──────────────────────────────────────────────────────────────────┐
│ PHASE 1: Controlled Ramp-Down                                    │
│ ─────────────────────────────────────────────────────────────── │
│ • GPIO4 remains HIGH (field driver active)                       │
│ • Duty ramped to 0% at DutyRampRate (default 50%/sec)           │
│ • Takes ~2 seconds from 100% to 0%                               │
│ • Prevents voltage transients that charge coupling cap           │
└──────────────────────────────────────────────────────────────────┘
                              ↓
                    After SettleTimeBeforeCut (500ms) at 0%
                              ↓
┌──────────────────────────────────────────────────────────────────┐
│ PHASE 2: Field Disconnect                                        │
│ ─────────────────────────────────────────────────────────────── │
│ • GPIO4 goes LOW (field driver disabled)                         │
│ • PWM output electrically irrelevant                             │
│ • Rate limiter continues tracking for smooth restart             │
└──────────────────────────────────────────────────────────────────┘

EXCEPTION: TEMP_CRITICAL skips Phase 1 entirely (immediate GPIO4 cut)

Why This Matters: - Sudden duty changes cause stator voltage transients - Transients charge the AC coupling capacitor - Charged cap affects tach signal for 4.6τ ≈ 9.2 seconds - Rate-limited field ramp prevents this problem


Cascaded Control Architecture

The system uses cascaded control loops during the Float charging stage to prevent PID/voltage-limiter fighting:

┌─────────────────────────────────────────────────────────────────┐
│                    CASCADED CONTROL (Float Stage)               │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  ┌─────────────────────────────────────────────────────────┐   │
│  │ OUTER LOOP (Voltage Control) - 500ms update rate        │   │
│  │ ──────────────────────────────────────────────────────  │   │
│  │ Input:  ChargingVoltageTarget - BatteryVoltage (error)  │   │
│  │ Output: voltageCapAmps = vError × VoltageKp             │   │
│  │ Bounds: 0 to finalLearningTarget (thermal limit)        │   │
│  │ Effect: Caps current setpoint to maintain target voltage│   │
│  └─────────────────────────────────────────────────────────┘   │
│                              │                                  │
│                              ▼                                  │
│                    uTargetAmps = min(uTargetAmps, voltageCapAmps)
│                              │                                  │
│                              ▼                                  │
│  ┌─────────────────────────────────────────────────────────┐   │
│  │ INNER LOOP (Current Control) - 100ms update rate        │   │
│  │ ──────────────────────────────────────────────────────  │   │
│  │ Input:  uTargetAmps - measuredCurrent (error)           │   │
│  │ Output: dutyCycle (0-100%)                              │   │
│  │ Type:   PID controller                                  │   │
│  └─────────────────────────────────────────────────────────┘   │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

BULK STAGE: Outer loop inactive, inner PID targets learning table current
            Voltage soft limiter provides backup protection

FLOAT STAGE: Outer loop active, smoothly reduces current to maintain voltage
             No PID/limiter fighting, no integral windup

Key Design Points: - Outer loop can only reduce current, never exceed thermal limit - Upper bound is finalLearningTarget (includes ambient correction and penalty) - P-only controller sufficient for voltage maintenance - Rate limiting on duty output protects LM2907 regardless of loop


Setpoint Cap Chain

Before the voltage outer loop and PID compute, uTargetAmps passes through a chain of setpoint caps. This is the correct pattern: cap the setpoint, not the duty cycle. Capping duty causes the PID to fight the limiter; capping the setpoint lets the PID settle cleanly to the constrained target.

┌─────────────────────────────────────────────────────────────────┐
│                    SETPOINT CAP CHAIN                           │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  uTargetAmps (from learning table + corrections + modifiers)    │
│                              │                                  │
│                              ▼                                  │
│  ┌─────────────────────────────────────────────────────────┐   │
│  │ 1. MaxTableValue cap (absolute table ceiling)           │   │
│  │    if uTargetAmps > MaxTableValue → clamp               │   │
│  └─────────────────────────────────────────────────────────┘   │
│                              │                                  │
│                              ▼                                  │
│  ┌─────────────────────────────────────────────────────────┐   │
│  │ 2. RPM Cap Table (mechanical limits: belt/shaft/mount)  │   │
│  │    Interpolated from rpmCapCurrentTable[]               │   │
│  │    if uTargetAmps > rpmCapAmps → clamp                  │   │
│  └─────────────────────────────────────────────────────────┘   │
│                              │                                  │
│                              ▼                                  │
│  ┌─────────────────────────────────────────────────────────┐   │
│  │ 3. MaximumAllowedBatteryAmps (global battery ceiling)   │   │
│  │    if uTargetAmps > MaximumAllowedBatteryAmps → clamp   │   │
│  └─────────────────────────────────────────────────────────┘   │
│                              │                                  │
│                              ▼                                  │
│                    uTargetAmps (capped)                         │
│                              │                                  │
│                              ▼                                  │
│              Voltage outer loop (float stage only)              │
│                              │                                  │
│                              ▼                                  │
│                    Inner PID compute                            │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

If ANY cap binds → learning is suppressed for that tick
(system is not thermally limited, so learning data would be invalid)

RPM Cap Table: - Always enabled; uses same RPM breakpoints as learning table (rpmTableRPMPoints[]) - Interpolated between breakpoints for smooth behavior - To disable a cap point, set its value to 99999 (effectively unlimited) - Protects against mechanical limits (belt slip, shaft stress, mounting fatigue)


Charging Stage State Machine

┌─────────────────────────────────────────────────────────────────┐
│                      BULK STAGE                                 │
│ ─────────────────────────────────────────────────────────────── │
│ ChargingVoltageTarget = BulkVoltage                             │
│ Control: Inner PID targets learning table current               │
│ Voltage soft limiter: Active (backup protection)                │
│ Learning: Active (thermal feedback valid)                       │
│                                                                 │
│ Exit condition:                                                 │
│ voltage >= BulkVoltage for bulkCompleteTime                     │
└──────────────────────────┬──────────────────────────────────────┘
                           │
                           ▼
┌─────────────────────────────────────────────────────────────────┐
│                      FLOAT STAGE                                │
│ ─────────────────────────────────────────────────────────────── │
│ ChargingVoltageTarget = FloatVoltage                            │
│ Control: Cascaded - outer voltage loop caps inner current PID   │
│ Voltage soft limiter: Disabled (outer loop handles it)          │
│ Learning: Suppressed (voltage-limited, not thermally-limited)   │
│                                                                 │
│ Exit conditions (return to bulk):                               │
│ • Time in float > FLOAT_DURATION, OR                            │
│ • Voltage drops below FloatVoltage - 0.5V                       │
└─────────────────────────────────────────────────────────────────┘

Fault Response Matrix

Temperature Faults

Severity Condition Mode Action Learning
Soft temp > limit NORMAL_AUTO_PID Soft limiter: reduce duty Continues
Warning temp > limit + TempWarnExcess WARNING_RAMP_AND_LOCKOUT Two-phase shutdown, lockout Paused
Sustained Warning for > TempSustainedTimeout WARNING_RAMP_AND_LOCKOUT GPIO4 cut after settle Triggers penalty
Critical temp > limit + TempCritExcess CRITICAL_RAMP Immediate GPIO4 cut Triggers penalty
Stale No fresh temp data > 30s (after 60s boot) CRITICAL_RAMP Two-phase shutdown Paused

Voltage Faults

Severity Condition Mode Action
Soft voltage > ChargingVoltageTarget (bulk only) NORMAL_AUTO_PID Soft limiter: reduce duty
Spike voltage > BulkVoltage + VoltageSpikeMargin WARNING_RAMP_AND_LOCKOUT Two-phase shutdown, lockout
Disagree Warning |BatteryV - IBV| > threshold for > timeout WARNING_RAMP_AND_LOCKOUT Two-phase shutdown, lockout
Disagree Critical |BatteryV - IBV| > 1/2/4V (12/24/48V systems) CRITICAL_RAMP Two-phase shutdown
Implausible Either sensor NaN, zero, or out of range CRITICAL_RAMP Two-phase shutdown

Soft Limiters (Duty-Based Backstops)

These remain as duty-based "nudges" rather than setpoint caps because they are rare emergency backstops, not expected to bind during normal operation. Immediate duty reduction provides fastest relief.

Limiter Condition Action
Temperature temp > TemperatureLimitF Reduce duty by 2 * dutyStep
Voltage (bulk only) voltage > ChargingVoltageTarget Reduce duty by 3 * dutyStep

Note: Battery current limiting is handled by the MaximumAllowedBatteryAmps setpoint cap, not a duty-based soft limiter. This prevents PID/limiter fighting during normal high-current operation.


Lockout (Cooldown) System

After WARNING or CRITICAL faults, a mandatory cooldown period prevents immediate restart.

┌─────────────────────────────────────────────────────────────────┐
│                    LOCKOUT BEHAVIOR                             │
├─────────────────────────────────────────────────────────────────┤
│ Trigger: WARNING or CRITICAL mode entered                       │
│ Duration: FIELD_COLLAPSE_DELAY (configurable)                   │
│                                                                 │
│ During lockout:                                                 │
│ • Mode forced to MODE_LOCKOUT_RAMP                              │
│ • GPIO4 remains LOW                                             │
│ • Charging cannot resume regardless of fault clearing           │
│                                                                 │
│ After lockout expires:                                          │
│ • If fault cleared → return to normal operation                 │
│ • If fault persists → remain in fault mode                      │
│                                                                 │
│ Console message on lockout start:                               │
│ "Field lockout started: Xs cooldown before restart allowed"     │
└─────────────────────────────────────────────────────────────────┘


Learning System

RPM-Based Tables (Shared X-Axis)

All tables share the same RPM breakpoints (rpmTableRPMPoints[]) with length RPM_TABLE_SIZE:

Table Purpose Interpolation
rpmCurrentTable[] Thermal learning targets (A) Stepped (uses index)
rpmMinDutyTable[] Minimum duty for tach signal (%) Linear interpolation
rpmCapCurrentTable[] Mechanical current ceiling (A) Linear interpolation
┌─────────────────────────────────────────────────────────────────┐
│                    TABLE STRUCTURE                              │
├─────────────────────────────────────────────────────────────────┤
│ Index:        [0]   [1]   [2]   [3]   [4]   [5]   [6]  ...     │
│ RPM Points:   100   600  1100  1600  2100  2600  3100  ...     │
│ Current (A):    0    40    50    50    50    50    50  ...     │
│ Min Duty (%): 35    30    20    15    10     5     3  ...     │
│ Cap (A):    99999 99999 99999 99999 99999 99999 99999  ...     │
├─────────────────────────────────────────────────────────────────┤
│ Learning: Adjusts rpmCurrentTable[] based on thermal feedback  │
│ Min Duty: Protects tach signal at low RPM                      │
│ Cap: Protects mechanical components (set 99999 to disable)     │
└─────────────────────────────────────────────────────────────────┘

Learning Events

Event Trigger Table Action Side Effects
Overheat TempToUse > TemperatureLimitF Reduce current at RPM index by LearningDownStep Penalty applied, neighbors reduced if enabled
Safe Operation Temp below (limit - LearningTempHysteresis) for SafeOperationThreshold Increase current at RPM index by LearningUpStep Cumulative time tracked

Learning Upward Clamp

When learning increases a table entry, the result is clamped to the minimum of: - MaxTableValue (absolute ceiling) - rpmCapCurrentTable[index] (mechanical limit at that RPM) - MaximumAllowedBatteryAmps (battery ceiling)

This prevents learning from writing values that would immediately be capped at runtime.

Learning Suppression Conditions

Learning is blocked when ANY of these conditions are true:

Condition Reason
LearningMode == 0 Globally disabled
LearningPaused == 1 User-paused
HiLow == 0 Half-power mode active
ForceFloat == 1 SOC maintenance mode active
voltageControlActive == true Float stage (voltage-limited, not thermally-limited)
Any setpoint cap binding MaxTableValue, RPM cap, or MaximumAllowedBatteryAmps is limiting
Penalty active Recently overheated
Within LearningSettlingPeriod of RPM change RPM transition settling
Within MinLearningInterval of last update Rate limiting
Temp within LearningTempHysteresis of limit Hysteresis band
Non-normal mode Fault or shutdown in progress

Design Principle: Learning only runs when the system is thermally limited under full power with no overrides or caps binding. This ensures table adjustments reflect true thermal limits.


Special Operating Modes

ForceFloat (SOC Maintenance Mode)

Note: "ForceFloat" is a historical misnomer. This mode maintains current battery state-of-charge by targeting zero battery current, not a specific float voltage.

┌─────────────────────────────────────────────────────────────────┐
│                    FORCEFLOAT BEHAVIOR                          │
├─────────────────────────────────────────────────────────────────┤
│ Trigger: ForceFloat == 1 (user-activated)                       │
│                                                                 │
│ Behavior:                                                       │
│ • uTargetAmps = 0 (target zero battery current)                 │
│ • Uses getBatteryCurrent() as feedback source                   │
│ • Learning suppressed (learningSuppressedByMode = true)         │
│ • All safety limits still active (temp, voltage)                │
│                                                                 │
│ Use Case: Battery is fully charged, minimize alternator         │
│ contribution while allowing other loads to draw from alternator │
│                                                                 │
│ Future Enhancement: Could target present battery voltage        │
│ instead of zero current for true maintenance charging.          │
└─────────────────────────────────────────────────────────────────┘

Weather Mode (Solar Priority)

Weather mode cleanly disables charging via the mode system when solar forecast indicates sufficient renewable generation.

┌─────────────────────────────────────────────────────────────────┐
│                    WEATHER MODE BEHAVIOR                        │
├─────────────────────────────────────────────────────────────────┤
│ Trigger: weatherModeEnabled == 1 && currentWeatherMode == 1     │
│                                                                 │
│ Mechanism:                                                      │
│ • Folds into chargingEnabled in buildTickSnapshot()             │
│ • Results in MODE_DISABLED_RAMP (clean shutdown)                │
│ • No alarm (GPIO21 LOW) - this is intentional, not a fault      │
│ • Two-phase ramp-down protects LM2907                           │
│                                                                 │
│ Decision Logic (in analyzeWeatherMode):                         │
│ • Counts days with UV/solar above UVThresholdHigh               │
│ • If 2+ of 3 days above threshold → disable alternator          │
└─────────────────────────────────────────────────────────────────┘


Hardware Interface

GPIO Function Active State Notes
GPIO4 Field driver enable HIGH = enabled Hard enable for PWM output
GPIO21 Alarm output HIGH = fault Active during non-normal modes (except DISABLED)
GPIO36 BMS input Configurable Polarity set by bmsLogicLevelOff

Key Parameters Reference

Timing Parameters

Parameter Example Default Units Purpose
PidSampleTime 100 ms Inner PID update interval
VoltageLoopInterval 500 ms Outer voltage loop update interval
MinLearningInterval 30000 ms Min time between learning updates
SafeOperationThreshold 30000 ms Time for upward learning credit
LearningSettlingPeriod 30000 ms Post-RPM-change settling delay
SettleTimeBeforeCut 500 ms Duty at 0% before GPIO4 cut
TempSustainedTimeout 120000 ms Warning → sustained escalation
VoltageDisagreeTimeout 10000 ms Sensor disagreement warning threshold
MaxPenaltyDuration 60000 ms Overheat penalty duration
FIELD_COLLAPSE_DELAY (define) ms Lockout/cooldown duration
bulkCompleteTime (configurable) ms Time at bulk voltage before float transition
FLOAT_DURATION (define) sec Duration of float stage before returning to bulk

Threshold Parameters

Parameter Example Default Units Purpose
TempWarnExcess 10.0 °F Above limit → WARNING mode
TempCritExcess 30.0 °F Above limit → CRITICAL mode (immediate cut)
LearningTempHysteresis 10 °F Below limit required for upward learning
VoltageSpikeMargin 0.2 V Above bulk → voltage spike warning
VoltageDisagreeThreshold 0.15 V Sensor disagreement detection

Control Parameters

Parameter Example Default Units Purpose
VoltageKp 25.0 A/V Voltage loop gain (P-only)
PidKp 0.5 - Inner PID proportional gain
PidKi 0.0 - Inner PID integral gain
PidKd 0.0 - Inner PID derivative gain
SetpointRampRate 5.0 A/sec PID setpoint change rate limit
DutyRampRate 50.0 %/sec PWM duty change rate limit

Setpoint Cap Parameters

Parameter Example Default Units Purpose
MaxTableValue 120.0 A Absolute ceiling for table entries and setpoint
MaximumAllowedBatteryAmps (configurable) A Global battery current ceiling
rpmCapCurrentTable[] 99999 (all) A RPM-dependent mechanical ceiling (99999 = disabled)

Voltage Plausibility Ranges (Auto-Detected)

System BulkVoltage Range Min Plausible Max Plausible
12V < 18V 4.5V 15.5V
24V 18V - 36V 9.0V 30.5V
48V > 36V 18V 60.5V

Data Storage

NVS Namespaces (Array/Blob Data)

Namespace Key Contents
learning rpmTable Current limit table (float[10])
learning rpmPoints RPM breakpoints (int[10])
learning overheatCount Per-RPM overheat counts
learning lastOverheat Per-RPM overheat timestamps
learning cumulativeTime Per-RPM safe operation time
learning totalEvents Total learning events (u32)
learning totalOverheats Total overheats (u32)
learning totalSafeMs Total safe operation time (u64 blob)
minduty minDutyTable RPM-minimum-duty table (float[10])
capcurrent capTable RPM-cap-current table (float[10])

LittleFS Files (Scalar Parameters)

All user-adjustable parameters stored as /ParameterName.txt files. Loaded in initSystemSettings(), written by web handler on change.


Telemetry Variables

Variable Type Purpose
dutyCycle float Current duty cycle (%)
vvout float Calculated field voltage (V)
iiout float Calculated field current (A)
fieldActiveStatus int 1 if field energized, 0 if not
uTargetAmps float Current PID setpoint (A)
learningTargetFromRPM float Raw table lookup result (A)
finalLearningTarget float After corrections applied (A)
currentRPMTableIndex int Which table entry is active
overheatPenaltyEndMs uint32_t When penalty expires (0 = none)
overheatingPenaltyAmps float Current penalty amount (A)
voltageControlActive bool True when in float stage
voltageCapAmps float Current output of voltage loop
inBulkStage bool True during bulk charging

State Machine Diagram

                         ┌─────────────────┐
          ┌──────────────│ NORMAL_AUTO_PID │◄────────────────────┐
          │              │   or MANUAL     │                     │
          │              └────────┬────────┘                     │
          │                       │                              │
          │      Fault detected   │   Fault cleared + lockout    │
          │                       │        expired               │
          │                       ▼                              │
          │  ┌────────────────────────────────────────────┐      │
          │  │           FAULT MODES                      │      │
          │  ├────────────────────────────────────────────┤      │
          │  │                                            │      │
          │  │  ┌─────────────┐    ┌──────────────────┐  │      │
          │  │  │  CRITICAL   │    │ WARNING_LOCKOUT  │  │      │
          │  │  │   RAMP      │    │                  │  │      │
          │  │  │             │    │  • Starts        │  │      │
          │  │  │ • Temp crit │    │    lockout       │  │      │
          │  │  │   (immed)   │    │    timer         │  │      │
          │  │  │ • V implaus │    │  • Temp warning  │  │      │
          │  │  │ • V disagree│    │  • V spike       │  │      │
          │  │  │   critical  │    │  • V disagree    │  │      │
          │  │  │ • Temp stale│    │    warning       │  │      │
          │  │  └──────┬──────┘    └────────┬─────────┘  │      │
          │  │         │                    │            │      │
          │  │         │   GPIO4 cut        │            │      │
          │  │         ▼                    ▼            │      │
          │  │  ┌────────────────────────────────────┐   │      │
          │  │  │          LOCKOUT_RAMP              │   │      │
          │  │  │  (waiting for cooldown to expire)  │───┼──────┘
          │  │  └────────────────────────────────────┘   │
          │  │                                            │
          │  └────────────────────────────────────────────┘
          │
          │              ┌─────────────────┐
          └──────────────│  DISABLED_RAMP  │
                         │ (charging off)  │
                         │ (weather mode)  │
                         └─────────────────┘

Future Work / TODOs

1. Temperature Rate-of-Rise Detection

Opportunity: Proactively reduce current when temperature is climbing rapidly toward limit, before soft limiter engages.

Blocker: Current OneWire sensor updates every ~5 seconds with Core0 scheduling variability. Derivative calculation is too coarse for reliable dT/dt detection.

Solution Path: Add faster temperature sensor (filtered thermistor or TMP235A4DCKR) with ~100ms response time.

2. ForceFloat Voltage Maintenance

Current: ForceFloat targets zero battery current, which can allow voltage drift over time.

Enhancement: Use voltage control loop to maintain present battery voltage instead of targeting zero current. This would provide true maintenance charging behavior.

3. Adaptive VoltageKp Tuning

Current: VoltageKp is a fixed user parameter.

Enhancement: Could auto-tune based on observed system response, or scale with battery capacity / alternator rating.


Console Messages Reference

Mode Transitions

  • "Entering normal mode"
  • "Exiting normal mode: <reason>"
  • "Field lockout expired - normal operation resumed"
  • "Field lockout started: Xs cooldown before restart allowed"

GPIO4 Events

  • "GPIO4 immediate cut: TEMP_CRITICAL"
  • "GPIO4 cut after settle: <reason>"

Learning Events

  • "Learning: Overheat at X RPM, reduced to YA"
  • "Learning: Safe operation at X RPM, increased to YA"
  • "Learning: RPM transition detected (X RPM) - settling for Ys"
  • "Learning: Overheat penalty expired"
  • "Learning: Table reset to factory defaults"

Charging Stage

  • "CHARGING: Bulk stage complete, switching to float"
  • "CHARGING: Returning to bulk stage"

Soft Limiters

  • "Temp limit reached, backing off..."
  • "Voltage limit reached, backing off..."
  • "Battery current limit reached, backing off..."

Sensor Faults

  • "OneWire sensor stale, sensor dead or disconnected"
  • "Battery Voltage disagreement! BatteryV=X V, IBV=Y V. Field shut off for safety!"