2023-11-07 22:04:36 +00:00
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 ;
2023-11-07 22:04:36 +00:00
}
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 ( ) ;
}
}
}