160 lines
8.2 KiB
C#
160 lines
8.2 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(IHubContext<LightHub> hubContext, 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 IHubContext<LightHub> _hubContext = hubContext;
|
|
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);
|
|
_ = _hubContext.Clients.All.SendAsync("ReceiveStateUpdate", state);
|
|
}
|
|
|
|
[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);
|
|
_periodData = JsonSerializer.Deserialize<List<Period>>(periodDataStream); // may throw JsonException
|
|
if (_periodData is null)
|
|
{
|
|
throw new InvalidOperationException($"JSON deserialisation returned no data available in given configuration file: {periodDataFilename}");
|
|
}
|
|
|
|
// Parse file for convenience
|
|
foreach (Period period in _periodData)
|
|
{
|
|
timeParseSuccess = TimeOnly.TryParse(period.TimeStart, out period.TimeStartParsed);
|
|
if (!timeParseSuccess)
|
|
{
|
|
throw new InvalidOperationException($"Unable to parse time start string in period data: ${period.TimeStart}");
|
|
}
|
|
|
|
_logger.LogInformation($"Period found with name: '{period.Name}', timestart: '{period.TimeStart}', parsed timestart: {period.TimeStartParsed}, 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].TimeStartParsed && testTime < _periodData[index + 1].TimeStartParsed)
|
|
{
|
|
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}");
|
|
|
|
while (!cancellationToken.IsCancellationRequested)
|
|
{
|
|
// _logger.LogInformation("Working behind the scenes...");
|
|
// _stateService.SetCurrentTrafficLightState(string.Format("Thoughts about life, the universe, and everything! [ {0} ]", DateTime.Now));
|
|
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)");
|
|
|
|
var currentState = new TrafficLightState(
|
|
_periodData[currentPeriodIndex],
|
|
_periodData[currentPeriodIndex].States[currentStateIndex],
|
|
currentStateIndex,
|
|
(nextChangeoverTicks - DateTime.UtcNow.Ticks) / (double)TICKS_PER_SECOND,
|
|
(nextChangeoverTicks - EPOCH_TICKS) / (TICKS_PER_SECOND / 1000), // Division by 1000 -> needs to be in ms
|
|
TimeOnly.FromDateTime(currentSimulationDateTime).ToString(),
|
|
_timeFactor,
|
|
(currentSimulationDateTime.Ticks - EPOCH_TICKS) / (TICKS_PER_SECOND / 1000), // Division by 1000 -> needs to be in ms
|
|
(startSimulationDateTime.Ticks - EPOCH_TICKS) / (TICKS_PER_SECOND / 1000)); // Division by 1000 -> needs to be in ms
|
|
UpdateState(currentState);
|
|
}
|
|
|
|
await Task.Delay((int)((1000 / UPDATES_PER_SIMULATED_SECOND) / _timeFactor), cancellationToken);
|
|
}
|
|
}
|
|
}
|
|
}
|