Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
9bee335
Reduce auger feed during reignition to prevent pellet accumulation
claude Mar 7, 2026
189e952
Divert Sear to Smoke feed until grill temp reaches minimum setpoint
claude Mar 7, 2026
0ac3ee4
Reject out-of-range sensor readings to prevent HA graph spikes
claude Mar 7, 2026
cf50215
Replace Newtonsoft.Json with System.Text.Json
claude Mar 7, 2026
e0a15e5
Update all projects from .NET 8 to .NET 10
claude Mar 7, 2026
b520950
Add unit tests and GitHub Actions CI
claude Mar 7, 2026
ed567d6
Add Pulumi C# project for SSH deployment to Raspberry Pi
claude Mar 7, 2026
b30289e
Fix PID division by zero, shadowed CTS field, remove batch files, cle…
claude Mar 7, 2026
70a9211
Add Claude Code session-start hook for web environments
claude Mar 7, 2026
8029f9e
Fix pulumi preview failing on missing publish directory
claude Mar 8, 2026
01a6cec
Fix pulumi preview failing on missing publish directory
claude Mar 8, 2026
bd05f12
Refactor SmokerPid and RtdArray methods for improved clarity; enhance…
CamSoper Mar 24, 2026
b63bc2c
Refactor publishing process to use sequential commands for improved f…
CamSoper Mar 24, 2026
a3eb4fc
Update .NET SDK version from 10.0 to 8.0 across all projects and scripts
CamSoper Mar 24, 2026
815b2e5
Update systemd service template to resolve home directory path for user
CamSoper Mar 24, 2026
5434499
Add MQTT configuration options to Pulumi.yaml and update service unit…
CamSoper Mar 24, 2026
650ecc3
Refactor deployment steps to copy published artifacts before stopping…
CamSoper Mar 24, 2026
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
20 changes: 20 additions & 0 deletions .claude/hooks/session-start.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
#!/bin/bash
set -euo pipefail

# Only run in remote (Claude Code on the web) environments
if [ "${CLAUDE_CODE_REMOTE:-}" != "true" ]; then
exit 0
fi

# Install .NET 8 SDK if not already present
if ! command -v dotnet &> /dev/null || ! dotnet --list-sdks 2>/dev/null | grep -q "^8\."; then
curl -fsSL https://dot.net/v1/dotnet-install.sh | bash -s -- --channel 8.0
fi

export PATH="$HOME/.dotnet:$PATH"

# Persist PATH for the session
echo "export PATH=\"\$HOME/.dotnet:\$PATH\"" >> "$CLAUDE_ENV_FILE"

# Restore NuGet packages
dotnet restore "$CLAUDE_PROJECT_DIR/Inferno.sln"
14 changes: 14 additions & 0 deletions .claude/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"hooks": {
"SessionStart": [
{
"hooks": [
{
"type": "command",
"command": "$CLAUDE_PROJECT_DIR/.claude/hooks/session-start.sh"
}
]
}
]
}
}
28 changes: 28 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
name: CI

on:
push:
branches: [main]
pull_request:
branches: [main]

jobs:
build-and-test:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4

- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: '8.0.x'

- name: Restore dependencies
run: dotnet restore

- name: Build
run: dotnet build --no-restore --configuration Release

- name: Test
run: dotnet test --no-build --configuration Release --verbosity normal
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -326,5 +326,8 @@ ASALocalRun/
# NVidia Nsight GPU debugger configuration file
*.nvuser

# MFractors (Xamarin productivity tool) working folder
# MFractors (Xamarin productivity tool) working folder
.mfractor/

# Pulumi stack configs (contain environment-specific settings)
Inferno.Deploy/Pulumi.*.yaml
22 changes: 2 additions & 20 deletions Inferno.Api/Devices/Display.cs
Original file line number Diff line number Diff line change
Expand Up @@ -77,8 +77,7 @@ private string JustifyWithSpaces(string string1, string string2, int maxChars =
return $"{string1}{spaces}{string2}";
}

#region IDisposable Support
private bool disposedValue = false; // To detect redundant calls
private bool disposedValue;

protected virtual void Dispose(bool disposing)
{
Expand All @@ -91,31 +90,14 @@ protected virtual void Dispose(bool disposing)
_lcd.Dispose();
_driver.Dispose();
}

// TODO: free unmanaged resources (unmanaged objects) and override a finalizer below.
// TODO: set large fields to null.

disposedValue = true;
}
}

// TODO: override a finalizer only if Dispose(bool disposing) above has code to free unmanaged resources.
// ~Display()
// {
// // Do not change this code. Put cleanup code in Dispose(bool disposing) above.
// Dispose(false);
// }

// This code added to correctly implement the disposable pattern.
public void Dispose()
{
// Do not change this code. Put cleanup code in Dispose(bool disposing) above.
Dispose(true);
// TODO: uncomment the following line if the finalizer is overridden above.
// GC.SuppressFinalize(this);
GC.SuppressFinalize(this);
}


#endregion
}
}
22 changes: 2 additions & 20 deletions Inferno.Api/Devices/RelayDevice.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,7 @@ public void Off()
}


#region IDisposable Support
private bool disposedValue = false; // To detect redundant calls
private bool disposedValue;

protected virtual void Dispose(bool disposing)
{
Expand All @@ -57,31 +56,14 @@ protected virtual void Dispose(bool disposing)
_gpio.ClosePin(_pin);
}
}

// TODO: free unmanaged resources (unmanaged objects) and override a finalizer below.
// TODO: set large fields to null.

disposedValue = true;
}
}

// TODO: override a finalizer only if Dispose(bool disposing) above has code to free unmanaged resources.
// ~RelayDevice()
// {
// // Do not change this code. Put cleanup code in Dispose(bool disposing) above.
// Dispose(false);
// }

// This code added to correctly implement the disposable pattern.
public void Dispose()
{
// Do not change this code. Put cleanup code in Dispose(bool disposing) above.
Dispose(true);
// TODO: uncomment the following line if the finalizer is overridden above.
// GC.SuppressFinalize(this);
GC.SuppressFinalize(this);
}


#endregion
}
}
73 changes: 41 additions & 32 deletions Inferno.Api/Devices/RtdArray.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System;
using System.Collections.Concurrent;
using System.Device.Spi;
using System.Diagnostics;
using System.Threading.Tasks;
using Inferno.Api.Interfaces;
using System.Linq;
Expand All @@ -14,6 +15,12 @@ public class RtdArray : IRtdArray, IDisposable
ConcurrentQueue<double> _grillResistances;
ConcurrentQueue<double> _probeResistances;

// Physically reasonable temperature range for a pellet grill.
// Below -20F the sensor or wiring is likely failed/shorted.
// Above 1000F something is very wrong (firepot max is ~600F).
const double MinValidTempF = -20;
const double MaxValidTempF = 1000;

Task _adcReadTask;

public RtdArray(SpiDevice spi)
Expand All @@ -25,17 +32,23 @@ public RtdArray(SpiDevice spi)
_adcReadTask = ReadAdc();
}

public double GrillTemp => Math.Round(RtdTempFahrenheitFromResistance(_grillResistances.Average()), 0);
public double GrillTemp => GetTemp(_grillResistances);

public double ProbeTemp => Math.Round(RtdTempFahrenheitFromResistance(_probeResistances.Average()), 0);
public double ProbeTemp => GetTemp(_probeResistances);

private static double GetTemp(ConcurrentQueue<double> resistances)
{
if (resistances.IsEmpty) return Double.NaN;
return Math.Round(RtdTempFahrenheitFromResistance(resistances.Average()), 0);
}

private async Task ReadAdc()
{
while (true)
{
int grillValue;
int probeValue;

try
{
grillValue = _adc.Read(0);
Expand All @@ -48,30 +61,39 @@ private async Task ReadAdc()
continue;
}

_grillResistances.Enqueue(CalculateResistanceFromAdc(grillValue));
_probeResistances.Enqueue(CalculateResistanceFromAdc(probeValue));
while (_grillResistances.Count > 100)
{
double temp;
_grillResistances.TryDequeue(out temp);
}
while (_probeResistances.Count > 100)
{
double temp;
_probeResistances.TryDequeue(out temp);
}
EnqueueIfValid(_grillResistances, grillValue, "Grill");
EnqueueIfValid(_probeResistances, probeValue, "Probe");

await Task.Delay(TimeSpan.FromMilliseconds(10));
}
}

static double CalculateResistanceFromAdc(double adcValue)
private static void EnqueueIfValid(ConcurrentQueue<double> queue, int adcValue, string sensorName)
{
double resistance = CalculateResistanceFromAdc(adcValue);
double tempF = RtdTempFahrenheitFromResistance(resistance);

if (Double.IsNaN(tempF) || Double.IsInfinity(tempF) ||
tempF < MinValidTempF || tempF > MaxValidTempF)
{
Debug.WriteLine($"{sensorName} sensor: rejected reading {tempF:F1}F (ADC={adcValue}, R={resistance:F1})");
return;
}

queue.Enqueue(resistance);
while (queue.Count > 100)
{
queue.TryDequeue(out _);
}
}

internal static double CalculateResistanceFromAdc(double adcValue)
{
double rtdV = (adcValue / 1023) * 3.3;
return ((3.3 * 1000) - (rtdV * 1000)) / rtdV;
}

static double RtdTempFahrenheitFromResistance(double Resistance)
internal static double RtdTempFahrenheitFromResistance(double Resistance)
{
double A = 3.90830e-3; // Coefficient A
double B = -5.775e-7; // Coefficient B
Expand All @@ -82,8 +104,7 @@ static double RtdTempFahrenheitFromResistance(double Resistance)
}


#region IDisposable Support
private bool disposedValue = false; // To detect redundant calls
private bool disposedValue;

protected virtual void Dispose(bool disposing)
{
Expand All @@ -93,26 +114,14 @@ protected virtual void Dispose(bool disposing)
{
_adc.Dispose();
}

disposedValue = true;
}
}

// TODO: override a finalizer only if Dispose(bool disposing) above has code to free unmanaged resources.
// ~TempProbes()
// {
// // Do not change this code. Put cleanup code in Dispose(bool disposing) above.
// Dispose(false);
// }

// This code added to correctly implement the disposable pattern.
public void Dispose()
{
// Do not change this code. Put cleanup code in Dispose(bool disposing) above.
Dispose(true);
// TODO: uncomment the following line if the finalizer is overridden above.
// GC.SuppressFinalize(this);
GC.SuppressFinalize(this);
}
#endregion
}
}
4 changes: 4 additions & 0 deletions Inferno.Api/Inferno.Api.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>

<ItemGroup>
<InternalsVisibleTo Include="Inferno.Tests" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="IoT.Device.Bindings" Version="3.1.0" />
</ItemGroup>
Expand Down
5 changes: 4 additions & 1 deletion Inferno.Api/Pid/SmokerPid.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,10 @@ public SmokerPid(double PB, double Ti, double Td)
public double GetControlVariable(double currentTemp)
{
if (double.IsNaN(currentTemp))
{
_lastUpdate = DateTime.Now;
return 0;
}

double error = currentTemp - SetPoint;

Expand All @@ -38,7 +41,7 @@ public double GetControlVariable(double currentTemp)
_integral = _integral.Clamp(-IntegralMax(), IntegralMax());
double I = GainI() * _integral;

double derivative = (currentTemp - _lastTemp) / dT.Seconds;
double derivative = (currentTemp - _lastTemp) / dT.TotalSeconds;
double D = GainD() * derivative;

double u = P + I + D;
Expand Down
1 change: 1 addition & 0 deletions Inferno.Api/Services/FireMinder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ public class FireMinder

public bool IsFireHealthy => !_fireCheck;
public bool IsFireStarted => _fireStarted;
public bool IsReigniting => _fireCheck && _igniter.IsOn;

public FireMinder(ISmoker smoker, IRelayDevice igniter)
{
Expand Down
Loading
Loading