TrafficLightsCSharp/TrafficLights/TrafficLightSimulatorServic...

247 lines
11 KiB
C#
Raw Permalink Normal View History

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<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("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<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));
2023-11-04 11:02:30 +00:00
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();
}
}
}