Skip to content
Draft
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
98 changes: 98 additions & 0 deletions Inferno.Api/Controllers/SessionsController.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
using System.Globalization;
using System.Text;
using Inferno.Api.Interfaces;
using Inferno.Api.Models;
using Microsoft.AspNetCore.Mvc;

namespace Inferno.Api.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class SessionsController : ControllerBase
{
readonly ICookLogStore _store;

public SessionsController(ICookLogStore store)
{
_store = store;
}

// GET api/sessions
[HttpGet]
public ActionResult<IReadOnlyList<CookSessionDto>> List()
{
return Ok(_store.ListSessions());
}

// GET api/sessions/active
[HttpGet("active")]
public ActionResult<CookSessionDto> Active()
{
var id = _store.GetActiveSessionId();
if (id == null)
{
return NoContent();
}

var session = _store.GetSession(id.Value);
return session == null ? NoContent() : Ok(session);
}

// GET api/sessions/{id}?from=&to=
[HttpGet("{id:long}")]
public ActionResult Get(long id, [FromQuery] DateTime? from, [FromQuery] DateTime? to)
{
var session = _store.GetSession(id);
if (session == null)
{
return NotFound();
}

return Ok(new { session, samples = _store.GetSamples(id, from, to) });
}

// GET api/sessions/{id}/export.csv
[HttpGet("{id:long}/export.csv")]
public ActionResult ExportCsv(long id, [FromQuery] DateTime? from, [FromQuery] DateTime? to)
{
var session = _store.GetSession(id);
if (session == null)
{
return NotFound();
}

var samples = _store.GetSamples(id, from, to);
var csv = new StringBuilder();
csv.AppendLine("timestamp,grill_temp,probe_temp,mode,setpoint,pvalue,auger_on,blower_on,igniter_on,fire_healthy,preheated");
foreach (var s in samples)
{
csv.Append(s.Timestamp.ToUniversalTime().ToString("o", CultureInfo.InvariantCulture)).Append(',')
.Append(s.GrillTemp.ToString(CultureInfo.InvariantCulture)).Append(',')
.Append(s.ProbeTemp.ToString(CultureInfo.InvariantCulture)).Append(',')
.Append(s.Mode).Append(',')
.Append(s.SetPoint).Append(',')
.Append(s.PValue).Append(',')
.Append(s.AugerOn ? 1 : 0).Append(',')
.Append(s.BlowerOn ? 1 : 0).Append(',')
.Append(s.IgniterOn ? 1 : 0).Append(',')
.Append(s.FireHealthy ? 1 : 0).Append(',')
.Append(s.Preheated ? 1 : 0).Append('\n');
}

return File(Encoding.UTF8.GetBytes(csv.ToString()), "text/csv", $"inferno-session-{id}.csv");
}

// POST api/sessions/{id}/label
[HttpPost("{id:long}/label")]
public ActionResult SetLabel(long id, [FromBody] string label)
{
if (_store.GetSession(id) == null)
{
return NotFound();
}

_store.SetLabel(id, label);
return Accepted();
}
}
}
1 change: 1 addition & 0 deletions Inferno.Api/Inferno.Api.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

<ItemGroup>
<PackageReference Include="IoT.Device.Bindings" Version="3.1.0" />
<PackageReference Include="Microsoft.Data.Sqlite" Version="8.0.*" />
</ItemGroup>

<ItemGroup>
Expand Down
40 changes: 40 additions & 0 deletions Inferno.Api/Interfaces/ICookLogStore.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
using Inferno.Api.Models;

namespace Inferno.Api.Interfaces
{
/// <summary>
/// Persistence abstraction for cook history. Implementations own their storage
/// connection for their lifetime; call <see cref="Initialize"/> once before use
/// and dispose to flush and release resources. Methods are safe to call from the
/// logger loop and request threads concurrently.
/// </summary>
public interface ICookLogStore : IDisposable
{
/// <summary>Open the store, applying any one-time setup (connection, schema).</summary>
void Initialize();

/// <summary>Begin a new cook session. Returns its id.</summary>
long OpenSession(DateTime startUtc, string? label);

/// <summary>Finalize a session with its end time and summary stats.</summary>
void CloseSession(long id, DateTime endUtc, double peakGrillTemp, double peakProbeTemp, int sampleCount);

/// <summary>Persist a batch of samples in a single transaction.</summary>
void InsertSamples(long sessionId, IReadOnlyList<CookSample> samples);

/// <summary>The id of the currently-open session (end_time IS NULL), or null.</summary>
long? GetActiveSessionId();

/// <summary>Set or rename a session's label.</summary>
void SetLabel(long id, string label);

/// <summary>All sessions, most recent first.</summary>
IReadOnlyList<CookSessionDto> ListSessions();

/// <summary>A single session, or null if it does not exist.</summary>
CookSessionDto? GetSession(long id);

/// <summary>A session's samples ordered by timestamp, optionally bounded to [fromUtc, toUtc].</summary>
IReadOnlyList<CookSample> GetSamples(long id, DateTime? fromUtc, DateTime? toUtc);
}
}
21 changes: 21 additions & 0 deletions Inferno.Api/Models/CookSample.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
namespace Inferno.Api.Models
{
/// <summary>
/// A single point-in-time snapshot of the smoker, captured by <see cref="Services.CookLogger"/>
/// and persisted as one row in the <c>sample</c> table. Timestamps are UTC.
/// </summary>
public class CookSample
{
public DateTime Timestamp { get; set; }
public double GrillTemp { get; set; }
public double ProbeTemp { get; set; }
public string Mode { get; set; } = "";
public int SetPoint { get; set; }
public int PValue { get; set; }
public bool AugerOn { get; set; }
public bool BlowerOn { get; set; }
public bool IgniterOn { get; set; }
public bool FireHealthy { get; set; }
public bool Preheated { get; set; }
}
}
17 changes: 17 additions & 0 deletions Inferno.Api/Models/CookSessionDto.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
namespace Inferno.Api.Models
{
/// <summary>
/// A cook session: the span from entering a cooking mode until returning to Ready.
/// <see cref="EndTime"/> is null while the session is still active. Timestamps are UTC.
/// </summary>
public class CookSessionDto
{
public long Id { get; set; }
public DateTime StartTime { get; set; }
public DateTime? EndTime { get; set; }
public string? Label { get; set; }
public double? PeakGrillTemp { get; set; }
public double? PeakProbeTemp { get; set; }
public int SampleCount { get; set; }
}
}
32 changes: 27 additions & 5 deletions Inferno.Api/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using System.Device.Spi;
using Inferno.Api.Services;
using Inferno.Api.Devices;
using Inferno.Api.Interfaces;
using Inferno.Common.Interfaces;

GpioController _gpio = new GpioController(PinNumberingScheme.Logical, new RaspberryPi3Driver());
Expand All @@ -20,11 +21,28 @@

builder.Services.AddControllers();

builder.Services.AddSingleton<ISmoker>(new Smoker(new Auger(_gpio, 22),
new Blower(_gpio, 21),
new Igniter(_gpio, 23),
new RtdArray(_spi),
new Display()));
var smoker = new Smoker(new Auger(_gpio, 22),
new Blower(_gpio, 21),
new Igniter(_gpio, 23),
new RtdArray(_spi),
new Display());
builder.Services.AddSingleton<ISmoker>(smoker);

// Cook history: a SQLite store plus a background logger that samples the smoker and
// records each cook as a session. The logger only depends on ISmoker.Status, so it
// stays decoupled from the safety-critical state machine. DB lives outside the
// publish dir (which the deploy replaces) at ~/inferno/data/inferno.db; override
// with INFERNO_DB_PATH.
string dbPath = Environment.GetEnvironmentVariable("INFERNO_DB_PATH")
?? Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
"inferno", "data", "inferno.db");
Directory.CreateDirectory(Path.GetDirectoryName(dbPath)!);
var cookLogStore = new SqliteCookLogStore(dbPath);
cookLogStore.Initialize();
builder.Services.AddSingleton<ICookLogStore>(cookLogStore);

var cookLogger = new CookLogger(smoker, cookLogStore);
builder.Services.AddSingleton(cookLogger);

var app = builder.Build();

Expand All @@ -36,7 +54,11 @@
// the shared GPIO controller is disposed afterward.
app.Lifetime.ApplicationStopping.Register(() =>
{
// Order matters: flush + close the active cook session before the smoker tears
// down, then close the DB, then release the GPIO.
cookLogger.Dispose();
(app.Services.GetService<ISmoker>() as IDisposable)?.Dispose();
cookLogStore.Dispose();
_gpio.Dispose();
});

Expand Down
Loading
Loading