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
15 changes: 12 additions & 3 deletions Inferno.Api/Devices/RtdArray.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ public class RtdArray : IRtdArray, IDisposable
int _probeInvalidCount;

Task _adcReadTask;
readonly CancellationTokenSource _stopCts = new();

public RtdArray(SpiDevice spi)
{
Expand All @@ -52,7 +53,7 @@ private static double GetTemp(ConcurrentQueue<double> resistances)

private async Task ReadAdc()
{
while (true)
while (!_stopCts.IsCancellationRequested)
{
int grillValue;
int probeValue;
Expand All @@ -62,17 +63,23 @@ private async Task ReadAdc()
grillValue = _adc.Read(0);
probeValue = _adc.Read(1);
}
catch (OperationCanceledException)
{
break;
}
catch (Exception ex)
{
Console.WriteLine($"{DateTime.Now} {ex.Message} {ex.StackTrace}");
await Task.Delay(TimeSpan.FromMilliseconds(10));
try { await Task.Delay(TimeSpan.FromMilliseconds(10), _stopCts.Token); }
catch (OperationCanceledException) { break; }
continue;
}

EnqueueIfValid(_grillResistances, grillValue, "Grill", ref _grillInvalidCount);
EnqueueIfValid(_probeResistances, probeValue, "Probe", ref _probeInvalidCount);

await Task.Delay(TimeSpan.FromMilliseconds(10));
try { await Task.Delay(TimeSpan.FromMilliseconds(10), _stopCts.Token); }
catch (OperationCanceledException) { break; }
}
}

Expand Down Expand Up @@ -129,6 +136,8 @@ protected virtual void Dispose(bool disposing)
{
if (disposing)
{
_stopCts.Cancel();
_stopCts.Dispose();
_adc.Dispose();
}
disposedValue = true;
Expand Down
50 changes: 41 additions & 9 deletions Inferno.Api/Pid/SmokerPid.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,33 +22,65 @@ public SmokerPid(double PB, double Ti, double Td)
_PB = PB;
_Ti = Ti;
_Td = Td;
_lastUpdate = DateTime.Now;
// NaN means "no valid previous sample yet" — the next reading seeds state
// instead of computing a derivative/integral across an unknown gap.
_lastTemp = double.NaN;
}

public double GetControlVariable(double currentTemp)
{
DateTime now = DateTime.Now;

if (double.IsNaN(currentTemp))
{
_lastUpdate = DateTime.Now;
// Sensor dropout: hold the integral, advance the clock, and force the
// next valid reading to re-seed so we don't compute a bogus derivative
// across the gap.
_lastUpdate = now;
_lastTemp = double.NaN;
return 0;
}

double error = currentTemp - SetPoint;

double P = GainP() * error;

TimeSpan dT = DateTime.Now - _lastUpdate;
_integral += error * dT.TotalSeconds;
_integral = _integral.Clamp(-IntegralMax(), IntegralMax());
double I = GainI() * _integral;
if (double.IsNaN(_lastTemp))
{
// First reading (or first after a dropout): seed state and return
// proportional-only. A stale/huge dt here would otherwise spike the
// integral and derivative.
_lastTemp = currentTemp;
_lastUpdate = now;
Debug.WriteLine($"u={P} (seed)");
return P;
}

double dtSeconds = (now - _lastUpdate).TotalSeconds;

double derivative = (currentTemp - _lastTemp) / dT.TotalSeconds;
double D = GainD() * derivative;
double I;
double D = 0;
if (dtSeconds > 0)
{
_integral += error * dtSeconds;
_integral = _integral.Clamp(-IntegralMax(), IntegralMax());
I = GainI() * _integral;

double derivative = (currentTemp - _lastTemp) / dtSeconds;
D = GainD() * derivative;
}
else
{
// Two calls in the same instant: no time elapsed, so don't accumulate
// the integral or divide by zero for the derivative.
I = GainI() * _integral;
}

double u = P + I + D;
Debug.WriteLine($"u={u} ({P}+{I}+{D})");

_lastTemp = currentTemp;
_lastUpdate = DateTime.Now;
_lastUpdate = now;

return u;
}
Expand Down
10 changes: 10 additions & 0 deletions Inferno.Api/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,14 @@

app.MapControllers();

// On a clean stop (systemctl stop / SIGTERM / Ctrl-C), tear the smoker down so the
// auger and igniter relays are de-energized instead of being left in their last
// commanded state. Smoker.Dispose() drives a hard safe-off and releases the devices;
// the shared GPIO controller is disposed afterward.
app.Lifetime.ApplicationStopping.Register(() =>
{
(app.Services.GetService<ISmoker>() as IDisposable)?.Dispose();
_gpio.Dispose();
});

app.Run();
17 changes: 14 additions & 3 deletions Inferno.Api/Services/DisplayUpdater.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,15 @@

namespace Inferno.Api.Services
{
public class DisplayUpdater
public class DisplayUpdater : IDisposable
{
ISmoker _smoker;
IDisplay _display;

bool _heartbeatFlag;

Task _updateDisplayLoop;
readonly CancellationTokenSource _stopCts = new();

public DisplayUpdater(ISmoker smoker, IDisplay display)
{
Expand All @@ -25,7 +26,7 @@ public DisplayUpdater(ISmoker smoker, IDisplay display)
private async Task UpdateDisplayLoop()
{
Debug.WriteLine("Starting display thread.");
while (true)
while (!_stopCts.IsCancellationRequested)
{
try
{
Expand Down Expand Up @@ -61,7 +62,11 @@ private async Task UpdateDisplayLoop()
}

_heartbeatFlag = !_heartbeatFlag;
await Task.Delay(TimeSpan.FromSeconds(1));
await Task.Delay(TimeSpan.FromSeconds(1), _stopCts.Token);
}
catch (OperationCanceledException)
{
break;
}
catch (Exception ex)
{
Expand All @@ -71,6 +76,12 @@ private async Task UpdateDisplayLoop()
}
}

public void Dispose()
{
_stopCts.Cancel();
_stopCts.Dispose();
}

private string HardwareStatus()
{
var status = _smoker.Status;
Expand Down
17 changes: 14 additions & 3 deletions Inferno.Api/Services/FireMinder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,14 @@

namespace Inferno.Api.Services
{
public class FireMinder
public class FireMinder : IDisposable
{
ISmoker _smoker;
IRelayDevice _igniter;
Func<DateTime> _now;
LidMonitor _lidMonitor;
Task _fireMinderLoop;
readonly CancellationTokenSource _stopCts = new();
TimeSpan _igniterTimeout = TimeSpan.FromMinutes(10);
TimeSpan _fireTimeout = TimeSpan.FromMinutes(10);
/// <summary>
Expand Down Expand Up @@ -84,12 +85,16 @@ private async Task FireMinderLoop()
{
Debug.WriteLine("Starting Fire Minder thread.");
ResetFireStatus();
while (true)
while (!_stopCts.IsCancellationRequested)
{
try
{
Tick();
await Task.Delay(TimeSpan.FromSeconds(1));
await Task.Delay(TimeSpan.FromSeconds(1), _stopCts.Token);
}
catch (OperationCanceledException)
{
break;
}
catch (Exception ex)
{
Expand All @@ -100,6 +105,12 @@ private async Task FireMinderLoop()
}
}

public void Dispose()
{
_stopCts.Cancel();
_stopCts.Dispose();
}

/// <summary>
/// One iteration of the fire-health state machine. Extracted from the loop so
/// it can be driven deterministically in tests with an injected clock.
Expand Down
57 changes: 33 additions & 24 deletions Inferno.Api/Services/PreheatMonitor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,46 +7,55 @@ public class PreheatMonitor
public const double ProximityPct = 0.10;

private readonly Queue<double> _tempHistory = new();
// Update() runs on a fixed-cadence loop while Reset() is driven from API
// threads (mode changes); guard the (non-concurrent) queue against both.
private readonly object _lock = new();

public bool IsPreheated { get; private set; }

public void Update(double grillTemp, int setPoint, bool isCookingMode, bool isFireHealthy)
{
if (IsPreheated) return;

if (!isCookingMode || !isFireHealthy)
lock (_lock)
{
_tempHistory.Clear();
return;
}
if (IsPreheated) return;

if (!isCookingMode || !isFireHealthy)
{
_tempHistory.Clear();
return;
}

if (Double.IsNaN(grillTemp) || grillTemp < 0)
return;
if (Double.IsNaN(grillTemp) || grillTemp < 0)
return;

_tempHistory.Enqueue(grillTemp);
while (_tempHistory.Count > WindowSize)
_tempHistory.Dequeue();
_tempHistory.Enqueue(grillTemp);
while (_tempHistory.Count > WindowSize)
_tempHistory.Dequeue();

if (_tempHistory.Count < WindowSize)
return;
if (_tempHistory.Count < WindowSize)
return;

double min = _tempHistory.Min();
double max = _tempHistory.Max();
if (max - min >= MaxRange)
return;
double min = _tempHistory.Min();
double max = _tempHistory.Max();
if (max - min >= MaxRange)
return;

double avg = _tempHistory.Average();
double threshold = setPoint * (1.0 - ProximityPct);
if (avg < threshold)
return;
double avg = _tempHistory.Average();
double threshold = setPoint * (1.0 - ProximityPct);
if (avg < threshold)
return;

IsPreheated = true;
IsPreheated = true;
}
}

public void Reset()
{
IsPreheated = false;
_tempHistory.Clear();
lock (_lock)
{
IsPreheated = false;
_tempHistory.Clear();
}
}
}
}
Loading
Loading