Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions Inferno.Api/Services/FireMinder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,25 @@ public class FireMinder : IDisposable
double _ignitionHigh;
bool _fireStarted;
int _ignitionTemp;
/// <summary>
/// Grill temperature at which the fire was first declared started, captured
/// once on the initial ignition. Unlike <see cref="_ignitionTemp"/> — which the
/// recovery path raises to the fire-check temp when relighting a struggling
/// fire — this stays anchored to where the fire actually caught. That lets Sear
/// gate its full-feed transition on a margin above the catch temp instead of a
/// fixed absolute temperature that a cold or windy firepot may never reach.
/// </summary>
int _initialIgnitionTemp;
bool _initialIgnition;

public bool IsFireHealthy => !_fireCheck;
public bool IsFireStarted => _fireStarted;
/// <summary>
/// Grill temp (F) at which the fire was first declared started. Anchored to the
/// initial catch — never raised by recovery relights — so callers can derive a
/// relative establish threshold from it.
/// </summary>
public int InitialIgnitionTemp => _initialIgnitionTemp;
public bool IsReigniting => _fireCheck && _igniter.IsOn;
// Recovery dominates: never report a lid-open while we're actively recovering,
// so the Smoker stays on the aggressive RecoveryFeed instead of the floor.
Expand All @@ -63,6 +78,9 @@ public void ResetFireStatus()
_fireCheck = false;
_initialIgnition = true;
_ignitionTemp = 150;
// Conservative default so Sear's relative establish gate stays high until
// the fire actually catches (and overwrites this with the real catch temp).
_initialIgnitionTemp = 150;
_belowCheckSince = null;
_recoveryHigh = 0;
_ignitionHigh = 0;
Expand Down Expand Up @@ -177,6 +195,12 @@ internal void Tick()
if(_smoker.Temps.GrillTemp >= _ignitionTemp)
{
// The fire has started, make sure the igniter is off
if (!_fireStarted)
{
// Capture where the fire caught, once. Recovery may later raise
// _ignitionTemp, but this anchor must stay at the initial catch.
_initialIgnitionTemp = _ignitionTemp;
}
_fireStarted = true;
_igniter.Off();
}
Expand Down
15 changes: 13 additions & 2 deletions Inferno.Api/Services/Smoker.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,16 @@ public class Smoker : ISmoker, IDisposable

int _maxGrillTemp = 425;

/// <summary>
/// In Sear, how far above the ignition temperature the grill must climb before
/// the auger switches from the gentle establishing feed to a continuous full
/// feed. A margin relative to where the fire actually caught — not a fixed
/// absolute temp — so a cold or windy start (where the firepot settles lower)
/// still reaches full feed instead of stalling under the old absolute 180F gate.
/// Lower this if the worst windy days still stall before full feed.
/// </summary>
int _searEstablishMargin = 25;

/// <summary>
/// Timeout for the blower to run after shutdown.
/// </summary>
Expand Down Expand Up @@ -489,9 +499,10 @@ private async Task Sear()
return;
}

if (_rtdArray.GrillTemp < _minSetPoint)
int establishTemp = _fireMinder.InitialIgnitionTemp + _searEstablishMargin;
if (_rtdArray.GrillTemp < establishTemp)
{
Debug.WriteLine($"Sear: Grill temp {_rtdArray.GrillTemp} below {_minSetPoint}. Diverting to SMOKE to establish fire.");
Debug.WriteLine($"Sear: Grill temp {_rtdArray.GrillTemp} below establish temp {establishTemp} (ignition {_fireMinder.InitialIgnitionTemp} + {_searEstablishMargin}). Diverting to SMOKE to establish fire.");
await Smoke();
return;
}
Expand Down
54 changes: 54 additions & 0 deletions Inferno.Tests/FireMinderTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,60 @@ public void ResetFireStatus_ClearsState()
Assert.False(fm.IsLidOpen);
}

[Fact]
public void InitialIgnitionTemp_DefaultsToFloor_BeforeFireStarts()
{
var smoker = new FakeSmoker { Mode = SmokerMode.Sear, SetPoint = 400 };
var igniter = new FakeRelay();
var fm = new FireMinder(smoker, igniter, autoStart: false);
fm.ResetFireStatus();

// Before the fire catches, the anchor sits at the 150F floor so Sear's
// relative establish gate stays conservative.
Assert.False(fm.IsFireStarted);
Assert.Equal(150, fm.InitialIgnitionTemp);
}

[Fact]
public void ColdStart_CapturesInitialIgnitionTempAtCatch()
{
var smoker = new FakeSmoker { Mode = SmokerMode.Sear, SetPoint = 400 };
var igniter = new FakeRelay();
var clock = new TestClock();
var fm = new FireMinder(smoker, igniter, () => clock.Now, autoStart: false);
fm.ResetFireStatus();

SetGrill(smoker, 75);
fm.Tick(); // igniter lights; ignition temp floors at 150
Assert.True(igniter.IsOn);
Assert.False(fm.IsFireStarted);

SetGrill(smoker, 152);
fm.Tick(); // crosses 150 → fire started

Assert.True(fm.IsFireStarted);
Assert.Equal(150, fm.InitialIgnitionTemp);
}

[Fact]
public void InitialIgnitionTemp_UnchangedByRecoveryRelight()
{
// EstablishedFire catches cold (grill jumps to 200 over the 150 floor).
var (fm, smoker, igniter, clock) = EstablishedFire();
Assert.Equal(150, fm.InitialIgnitionTemp);

// Trip recovery: the relight raises the internal ignition threshold to the
// fire-check temp (195), but the initial-catch anchor must stay put so Sear's
// establish gate doesn't drift upward after a recovery.
SetGrill(smoker, 180);
fm.Tick();
clock.Advance(TimeSpan.FromSeconds(46));
fm.Tick();
Assert.True(fm.IsReigniting);

Assert.Equal(150, fm.InitialIgnitionTemp);
}

[Fact]
public void BriefDip_RecoversBeforeDebounce_DoesNotTrip()
{
Expand Down
Loading