TrafficLightsSignalR/TrafficLightsSignalR/TrafficLightsHostedService.cs

165 lines
8.7 KiB
C#

using Microsoft.AspNetCore.SignalR;
using System.Diagnostics.CodeAnalysis;
using System.Text.Json;
using TrafficLightsSignalR.Hubs;
using TrafficLightsSignalR.Schema;
namespace TrafficLightsSignalR
{
public class TrafficLightsHostedService(ITrafficLightStateService stateService, IConfiguration configuration, ILogger<TrafficLightsHostedService> logger) : BackgroundService
{
// Simulation constants
const long TICKS_PER_SECOND = 10000000;
static readonly long EPOCH_TICKS = new DateTime(1970, 1, 1).Ticks;
const int UPDATES_PER_SIMULATED_SECOND = 4;
// Interconnects
private readonly ILogger<TrafficLightsHostedService> _logger = logger;
private readonly ITrafficLightStateService _stateService = stateService;
private readonly IConfiguration _configuration = configuration;
// Simulation source data
private List<Period>? _periodData;
// Simualtion settings
private TimeOnly _startSimulationTime;
private double _timeFactor;
private void UpdateState(TrafficLightState state)
{
_stateService.SetCurrentTrafficLightState(state);
}
private TrafficLightState CreateStateRecord(Period currentPeriod, State currentState, int currentStateIndex, long nextChangeoverTicks, DateTime startSimulationDateTime, DateTime currentSimulationDateTime)
{
return new TrafficLightState(
currentPeriod,
currentState,
currentStateIndex,
// Seconds until next changeover
(nextChangeoverTicks - DateTime.UtcNow.Ticks) / (double)TICKS_PER_SECOND,
// Next changeover timestamp in UNIX time, millisecond granularity
(nextChangeoverTicks - EPOCH_TICKS) / (TICKS_PER_SECOND / 1000), // Division by 1000 -> needs to be in ms
// Current simulation time string, formatted for display
TimeOnly.FromDateTime(currentSimulationDateTime).ToString(),
// Current time factor
_timeFactor,
// Current simulated timestamp in UNIX time, millisecond granularity
(currentSimulationDateTime.Ticks - EPOCH_TICKS) / (TICKS_PER_SECOND / 1000), // Division by 1000 -> needs to be in ms
// Start timestamp (simulation time) in UNIX time, millisecond granularity
(startSimulationDateTime.Ticks - EPOCH_TICKS) / (TICKS_PER_SECOND / 1000)); // Division by 1000 -> needs to be in ms
}
[MemberNotNull(nameof(_periodData))]
private void LoadData()
{
bool timeParseSuccess = TimeOnly.TryParse(_configuration.GetValue<string>("StartTime"), out _startSimulationTime);
if (!timeParseSuccess)
{
throw new InvalidOperationException("Unable to find StartTime (simulation start time) in configuration -> please check application settings");
}
_timeFactor = _configuration.GetValue<double>("TimeFactor");
// Load and deserialised JSON data from file
var periodDataFilename = _configuration.GetValue<string>("PeriodConfigurationFile") ?? throw new InvalidOperationException("Unable to find period data filename (PeriodConfigurationFile) in configuration -> please check application settings");
var periodDataStream = File.OpenRead(periodDataFilename);
var serializerOptions = new JsonSerializerOptions { Converters = { new TimeOnlyConverter() } };
_periodData = JsonSerializer.Deserialize<List<Period>>(periodDataStream, serializerOptions); // may throw JsonException
if (_periodData is null)
{
throw new InvalidOperationException($"JSON deserialisation returned no data available in given configuration file: {periodDataFilename}");
}
periodDataStream.Close();
// Parse file for convenience
foreach (Period period in _periodData)
{
_logger.LogInformation($"Period loaded - name: '{period.Name}', timestart: '{period.TimeStart}', parsed timestart: {period.TimeStart}, parse success: {timeParseSuccess}");
foreach (State state in period.States)
{
_logger.LogInformation($" - state with duration: {state.Duration}");
}
}
}
private int PeriodIndexFromTime(TimeOnly testTime)
{
if (_periodData is null)
{
return -1;
}
for (int index = 0; index < _periodData.Count; index++)
{
if (index == _periodData.Count - 1)
{
return index;
}
if (testTime >= _periodData[index].TimeStart && testTime < _periodData[index + 1].TimeStart)
{
return index;
}
}
// Technically shouldn't be necessary
return _periodData.Count - 1;
}
protected async override Task ExecuteAsync(CancellationToken cancellationToken)
{
LoadData();
DateTime now = DateTime.UtcNow;
long startTicks = now.Ticks;
DateTime startSimulationDateTime = new(1900, 1, 1, _startSimulationTime.Hour, _startSimulationTime.Minute, _startSimulationTime.Second);
int currentPeriodIndex = PeriodIndexFromTime(TimeOnly.FromDateTime(startSimulationDateTime));
int currentStateIndex = 0;
long nextChangeoverTicks = startTicks + (long)((_periodData[currentPeriodIndex].States[currentStateIndex].Duration * TICKS_PER_SECOND) / _timeFactor);
_logger.LogInformation($"Start period: {_periodData[currentPeriodIndex].Name}");
_logger.LogInformation($"Start ticks: {startTicks}");
_logger.LogInformation($"Next changeover ticks: {nextChangeoverTicks}");
_logger.LogInformation($"Next changeover second(s): {TimeSpan.FromTicks(nextChangeoverTicks - startTicks)}");
_logger.LogInformation($"Start simulation DT: {startSimulationDateTime}");
UpdateState(CreateStateRecord(_periodData[currentPeriodIndex], _periodData[currentPeriodIndex].States[currentStateIndex], currentStateIndex, nextChangeoverTicks, startSimulationDateTime, startSimulationDateTime));
while (!cancellationToken.IsCancellationRequested)
{
now = DateTime.UtcNow;
long nowTicks = now.Ticks;
DateTime currentSimulationDateTime = startSimulationDateTime + (TimeSpan.FromTicks(nowTicks - startTicks) * _timeFactor);
if (nowTicks >= nextChangeoverTicks)
{
int timedPeriodIndex = PeriodIndexFromTime(TimeOnly.FromDateTime(currentSimulationDateTime));
if (timedPeriodIndex != currentPeriodIndex && currentStateIndex == _periodData[currentPeriodIndex].States.Count - 1)
{
_logger.LogInformation($"{TimeOnly.FromDateTime(currentSimulationDateTime)} : {_periodData[currentPeriodIndex].Name} : Changing period from {currentPeriodIndex} to {timedPeriodIndex}");
// Current sequence complete, therefore safe to change periods
currentPeriodIndex = timedPeriodIndex;
currentStateIndex = 0;
}
else
{
int nextStateIndex = (currentStateIndex + 1) % _periodData[currentPeriodIndex].States.Count;
_logger.LogInformation($"{TimeOnly.FromDateTime(currentSimulationDateTime)} : {_periodData[currentPeriodIndex].Name} : Changing state from {currentStateIndex} to {nextStateIndex}");
currentStateIndex = nextStateIndex;
}
nextChangeoverTicks = nowTicks + (long)((_periodData[currentPeriodIndex].States[currentStateIndex].Duration * TICKS_PER_SECOND) / _timeFactor);
_logger.LogInformation($" Next changeover in {_periodData[currentPeriodIndex].States[currentStateIndex].Duration} simulation second(s)");
UpdateState(CreateStateRecord(_periodData[currentPeriodIndex], _periodData[currentPeriodIndex].States[currentStateIndex], currentStateIndex, nextChangeoverTicks, startSimulationDateTime, currentSimulationDateTime));
}
await Task.Delay((int)((1000 / UPDATES_PER_SIMULATED_SECOND) / _timeFactor), cancellationToken);
}
}
}
}