using System.Text.Json; using System.Text.Json.Serialization; namespace TrafficLights { public class TrafficLightSimulatorService : IHostedService, IDisposable { const long TICKS_PER_SECOND = 10000000; static long EPOCH_TICKS = new DateTime(1970, 1, 1).Ticks; 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 States { get; set; } = new List(); } 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("nextChangeoverTimestampMs")] public long NextChangeoverTimestampMs { get; set; } [JsonPropertyName("currentSimulationTime")] public string CurrentSimulationTime { get; set; } // UTC [JsonPropertyName("timeFactor")] public double TimeFactor { get; set; } [JsonPropertyName("currentSimulationTimestampMs")] public long CurrentSimulationTimestampMs { get; set; } // UNIX epoch timestamp (UTC in milliseconds) [JsonPropertyName("startSimulationTimestampMs")] public long StartSimulationTimestampMs { get; set; } // UNIX epoch timestamp (UTC in milliseconds) public Status(Period currentPeriod, State currentState, int currentStateIndex, double secondsUntilChangeover, long nextChangeoverTimestampMs, TimeOnly currentSimulationTime, double timeFactor, long currentSimulationTimestampMs, long startSimulationTimestampMs) { CurrentPeriod = currentPeriod; CurrentState = currentState; CurrentStateIndex = currentStateIndex; SecondsUntilChangeover = secondsUntilChangeover; NextChangeoverTimestampMs = nextChangeoverTimestampMs; CurrentSimulationTime = currentSimulationTime.ToLongTimeString(); TimeFactor = timeFactor; CurrentSimulationTimestampMs = currentSimulationTimestampMs; StartSimulationTimestampMs = startSimulationTimestampMs; } } private readonly ILogger _logger; private IConfiguration _configuration; private Timer? _timer = null; private List? _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 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("PeriodConfigurationFile"); _logger.LogInformation(string.Format("Using periods configuration file: '{0}'", periodConfigurationFilename)); var periodConfigurationStream = File.OpenRead(periodConfigurationFilename); _periodData = JsonSerializer.Deserialize>(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("StartTime"), out _startSimulationTime); _runTimeSpan = new TimeSpan(_configuration.GetValue("RunTime") * TICKS_PER_SECOND); _timeFactor = _configuration.GetValue("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, (_nextChangeoverTicks - EPOCH_TICKS) / (TICKS_PER_SECOND / 1000), // Needs to be in ms TimeOnly.FromDateTime(_currentSimulationDateTime), _timeFactor, (_currentSimulationDateTime.Ticks - EPOCH_TICKS) / (TICKS_PER_SECOND / 1000), // Needs to be in ms (_startSimulationDateTime.Ticks - EPOCH_TICKS) / (TICKS_PER_SECOND / 1000)); // Needs to be in ms _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(); } } }