230 lines
10 KiB
C#
230 lines
10 KiB
C#
using System.Text.Json;
|
|
using System.Text.Json.Serialization;
|
|
|
|
namespace TrafficLights
|
|
{
|
|
public class TrafficLightSimulatorService : IHostedService, IDisposable
|
|
{
|
|
const long TICKS_PER_SECOND = 10000000;
|
|
const int UPDATES_PER_SIMULATED_SECOND = 4;
|
|
|
|
public class State
|
|
{
|
|
[JsonPropertyName("duration")]
|
|
public int Duration { get; set; } = -1;
|
|
[JsonPropertyName("north")]
|
|
public string North { get; set; } = "";
|
|
[JsonPropertyName("south")]
|
|
public string South { get; set; } = "";
|
|
[JsonPropertyName("east")]
|
|
public string East { get; set; } = "";
|
|
[JsonPropertyName("west")]
|
|
public string West { get; set; } = "";
|
|
[JsonPropertyName("north-right")]
|
|
public string NorthRight { get; set; } = "";
|
|
}
|
|
|
|
public class Period
|
|
{
|
|
[JsonPropertyName("name")]
|
|
public string Name { get; set; } = "";
|
|
[JsonPropertyName("verbose-name")]
|
|
public string VerboseName { get; set; } = "";
|
|
[JsonPropertyName("timestart")]
|
|
public string TimeStartString { get; set; } = "";
|
|
public TimeOnly TimeStart;
|
|
[JsonPropertyName("states")]
|
|
public List<State> States { get; set; } = new List<State>();
|
|
}
|
|
public class Status
|
|
{
|
|
[JsonPropertyName("currentPeriod")]
|
|
public Period CurrentPeriod { get; set; }
|
|
[JsonPropertyName("currentState")]
|
|
public State CurrentState { get; set; }
|
|
[JsonPropertyName("currentStateIndex")]
|
|
public int CurrentStateIndex { get; set; }
|
|
[JsonPropertyName("secondsUntilChangeover")]
|
|
public double SecondsUntilChangeover { get; set; }
|
|
[JsonPropertyName("simulationTime")]
|
|
public string SimulationTime { get; set; }
|
|
|
|
public Status(Period currentPeriod, State currentState, int currentStateIndex, double secondsUntilChangeover, TimeOnly simulationTime)
|
|
{
|
|
CurrentPeriod = currentPeriod;
|
|
CurrentState = currentState;
|
|
CurrentStateIndex = currentStateIndex;
|
|
SecondsUntilChangeover = secondsUntilChangeover;
|
|
SimulationTime = simulationTime.ToLongTimeString();
|
|
}
|
|
}
|
|
|
|
private readonly ILogger<TrafficLightSimulatorService> _logger;
|
|
private IConfiguration _configuration;
|
|
private Timer? _timer = null;
|
|
|
|
private List<Period>? _periodData;
|
|
private TimeOnly _startSimulationTime;
|
|
private TimeSpan _runTimeSpan;
|
|
private double _timeFactor;
|
|
private long _startTicks;
|
|
private int _currentPeriodIndex;
|
|
private int _currentStateIndex;
|
|
private long _nextChangeoverTicks;
|
|
private DateTime _startSimulationDateTime;
|
|
private DateTime _currentSimulationDateTime;
|
|
|
|
private Mutex _mut = new Mutex();
|
|
|
|
public TrafficLightSimulatorService(ILogger<TrafficLightSimulatorService> logger, IConfiguration configuration)
|
|
{
|
|
_logger = logger;
|
|
_configuration = configuration;
|
|
}
|
|
|
|
private int _getPeriodIndex(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;
|
|
}
|
|
|
|
public Task StartAsync(CancellationToken stoppingToken)
|
|
{
|
|
bool parseSuccess = false;
|
|
|
|
_logger.LogInformation("Traffic Light Simulator Service Running.");
|
|
|
|
var periodConfigurationFilename = _configuration.GetValue<string>("PeriodConfigurationFile");
|
|
_logger.LogInformation(string.Format("Using periods configuration file: '{0}'", periodConfigurationFilename));
|
|
|
|
var periodConfigurationStream = File.OpenRead(periodConfigurationFilename);
|
|
|
|
_periodData = JsonSerializer.Deserialize<List<Period>>(periodConfigurationStream);
|
|
|
|
if (_periodData is null)
|
|
{
|
|
_logger.LogError("Error loading period data - return value is null.");
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
_logger.LogInformation(_periodData.GetType().ToString());
|
|
|
|
foreach (Period period in _periodData)
|
|
{
|
|
parseSuccess = TimeOnly.TryParse(period.TimeStartString, out period.TimeStart);
|
|
_logger.LogInformation(string.Format("Period found with name: '{0}', timestart: '{1}', parsed timestart: {2}, parse success: {3}", period.Name, period.TimeStartString, period.TimeStart.ToString(), parseSuccess));
|
|
foreach (State state in period.States)
|
|
{
|
|
// _logger.LogInformation(string.Format(" state with duration: {0}", state.Duration));
|
|
}
|
|
}
|
|
|
|
// From configuration:
|
|
parseSuccess = TimeOnly.TryParse(_configuration.GetValue<string>("StartTime"), out _startSimulationTime);
|
|
_runTimeSpan = new TimeSpan(_configuration.GetValue<long>("RunTime") * TICKS_PER_SECOND);
|
|
_timeFactor = _configuration.GetValue<double>("TimeFactor");
|
|
|
|
DateTime now = DateTime.UtcNow;
|
|
_startTicks = now.Ticks;
|
|
|
|
_startSimulationDateTime = new DateTime(1900, 1, 1, _startSimulationTime.Hour, _startSimulationTime.Minute, _startSimulationTime.Second);
|
|
_currentSimulationDateTime = _startSimulationDateTime;
|
|
|
|
_currentPeriodIndex = _getPeriodIndex(TimeOnly.FromDateTime(_startSimulationDateTime));
|
|
_currentStateIndex = 0;
|
|
|
|
_nextChangeoverTicks = _startTicks + (long)((_periodData[_currentPeriodIndex].States[_currentStateIndex].Duration * TICKS_PER_SECOND) / _timeFactor);
|
|
|
|
_logger.LogInformation(string.Format("Start period: {0}", _periodData[_currentPeriodIndex].Name));
|
|
_logger.LogInformation(string.Format("Start ticks: {0}", _startTicks));
|
|
_logger.LogInformation(string.Format("Next changeover ticks: {0}", _nextChangeoverTicks));
|
|
_logger.LogInformation(string.Format("Next changeover second(s): {0}", TimeSpan.FromTicks(_nextChangeoverTicks - _startTicks).ToString()));
|
|
_logger.LogInformation(string.Format("Start simulation DT: {0}", _startSimulationDateTime.ToString()));
|
|
|
|
_timer = new Timer(DoWork, null, 0, (int)((1000 / UPDATES_PER_SIMULATED_SECOND) / _timeFactor)); // Value is in milliseconds
|
|
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
private void DoWork(object? state)
|
|
{
|
|
if (_periodData is null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
_mut.WaitOne();
|
|
|
|
DateTime now = DateTime.UtcNow;
|
|
long nowTicks = now.Ticks;
|
|
|
|
_currentSimulationDateTime = _startSimulationDateTime + (TimeSpan.FromTicks(nowTicks - _startTicks) * _timeFactor);
|
|
|
|
if (nowTicks >= _nextChangeoverTicks)
|
|
{
|
|
int timedPeriodIndex = _getPeriodIndex(TimeOnly.FromDateTime(_currentSimulationDateTime));
|
|
|
|
if (timedPeriodIndex != _currentPeriodIndex && _currentStateIndex == _periodData[_currentPeriodIndex].States.Count - 1)
|
|
{
|
|
_logger.LogInformation(string.Format("{0} : {1} : Changing period from {2} to {3}", TimeOnly.FromDateTime(_currentSimulationDateTime).ToString(), _periodData[_currentPeriodIndex].Name, _currentPeriodIndex, timedPeriodIndex));
|
|
// Current sequence complete, therefore safe to change periods
|
|
_currentPeriodIndex = timedPeriodIndex;
|
|
_currentStateIndex = 0;
|
|
}
|
|
else
|
|
{
|
|
int nextStateIndex = (_currentStateIndex + 1) % _periodData[_currentPeriodIndex].States.Count;
|
|
_logger.LogInformation(string.Format("{0} : {1} : Changing state from {2} to {3}", TimeOnly.FromDateTime(_currentSimulationDateTime).ToString(), _periodData[_currentPeriodIndex].Name, _currentStateIndex, nextStateIndex));
|
|
_currentStateIndex = nextStateIndex;
|
|
}
|
|
_nextChangeoverTicks = nowTicks + (long)((_periodData[_currentPeriodIndex].States[_currentStateIndex].Duration * TICKS_PER_SECOND) / _timeFactor);
|
|
_logger.LogInformation(string.Format(" Next changeover in {0} simulation second(s)", _periodData[_currentPeriodIndex].States[_currentStateIndex].Duration));
|
|
}
|
|
|
|
_mut.ReleaseMutex();
|
|
}
|
|
|
|
public Status GetStatus()
|
|
{
|
|
_mut.WaitOne();
|
|
var currentStatus = new Status(
|
|
_periodData[_currentPeriodIndex],
|
|
_periodData[_currentPeriodIndex].States[_currentStateIndex],
|
|
_currentStateIndex,
|
|
(_nextChangeoverTicks - DateTime.UtcNow.Ticks) / (double)TICKS_PER_SECOND,
|
|
TimeOnly.FromDateTime(_currentSimulationDateTime));
|
|
_mut.ReleaseMutex();
|
|
return currentStatus;
|
|
}
|
|
|
|
public Task StopAsync(CancellationToken stoppingToken)
|
|
{
|
|
_logger.LogInformation("Traffic Light Simulator Service is stopping.");
|
|
|
|
_timer?.Change(Timeout.Infinite, 0);
|
|
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
_timer?.Dispose();
|
|
}
|
|
}
|
|
}
|