Added README, changed frontend to only poll when expecting changes.

Hardened JS against accidental simultaneous/duplicate XMLHttpRequest
sends.
This commit is contained in:
Chris Davoren 2023-11-08 08:04:36 +10:00
parent 4afb39f369
commit 41023d7d97
3 changed files with 346 additions and 236 deletions

72
README.md Normal file
View File

@ -0,0 +1,72 @@
# Traffic Light System
## Problem Description
### Summary
This test is a take home test, please return once completed
### Requirements
In this test we would like you to implement a traffic light system. We are required to have 4 sets of lights, as follows.
- Lights 1: Traffic is travelling south
- Lights 2: Traffic is travelling west
- Lights 3: Traffic is travelling north
- Lights 4: Traffic is travelling east
The lights in which traffic is travelling on the same axis can be green at the same time. During normal hours all lights stay green for 20 seconds, but during peak times north and south lights are green for 40 seconds while west and east are green for 10 seconds. Peak hours are 0800 to 1000 and 1700 to 1900. Yellow lights are shown for 5 seconds before red lights are shown. Red lights stay on until the cross-traffic is red for at least 4 seconds, once a red light goes off then the green is shown for the required time(eg the sequence is reset).
Bonus: At this intersection north bound traffic has a green right-turn signal, which stops the south bound traffic and allows north bound traffic to turn right. This is green at the end of north/south green light and stays green for 10 seconds. During this time north bound is green, north right-turn is green and all other lights are red.
### Implementation/Outcomes
1. Implement a front-end and backend (you can use dotnet new templates of your choice)
2. The backend will contain the logic and state of the running traffic lights. The front-end will be a visual representation of the traffic lights, with the data served from the backend.
3. Theres no need to have a perfect design on the front end, something simple and functional is fine (unless this is an area of strength you would like to show off). Noting* we will review the client side code.
4. Theres no need to implement entity framework (or similar) to store the data in a database, a in-memory store is fine
5. Code needs to follow architecture & best practices for enterprise grade systems
Note: Code will be evaluated not just for function, but on the quality of the code.
# Design
## Algorithmic Assumptions
The problem clearly specifies a kind of timed "simulation" with a decoupled front- and back-ends. The problem has been approached on that basis.
Additionally:
- The algorithm uses the term `period` to denote specific timing profiles (e.g. off-peak vs peak period). Within each `period` is a single implicit `sequence` that itself is comprised of a set of timed `states` that each fully specify the colour of each light. These are stored in JSON format in the file `periods.json`.
- The problem description does not specify behaviour on period change (i.e. transitioning from normal timing to period timing). One would assume that an instantaneous change would easily create unpredictable and unsafe conditions for any hypothetical drivers, and so in this implementation the current full `sequence` is allowed to complete before transitioning to the next period. For this example, a full cycle should not last longer than approximately 80 seconds and therefore this is the expected maximum delay between period transitions.
Period data is configurable/stored in `periods.json`.
## Environment and Infrastructure
As per the problem description, C# ASP.NET has been used. A web front-end was chosen due to the broad use of HTML/CSS/JavaScript and the ease with which it can be changed or updated according to need. It would also lend itself particularly well to a scenario where the front- and back-ends would exist on different machines.
The server uses an IHostedService `TrafficLightSimulatorService` to simulate the traffic light changes. By default, this service updates four times per simulated second.
The UI is a simple web page that polls for updates from the server. Due to the use of locks to avoid race conditions, this can induced minor delays (milliseconds at worse) in the processing and updates of traffic light information in the simulation thread. The main loop is designed in such a way that this will not cause cumulative delays in traffic light timing. However, it is worth considering that another approach may be desirable (such as pushed client updates rather than polling) if, for instance, many clients were expected to be making simultaneous requests of the server.
Incorporating some feedback on an earlier version of this solution in Python/Django, the UI now only polls when it knows the server is due for a state update. The update thread continues to run, however, ensuring that the displayed simulation time is continuously updated.
The main frontend web page, including the JavaScript polling code, is specified in its entirety in `Pages/Index.cshtml`. **In a production project, CSS, JavaScript, and HTML would all be separated into individual packages**. However for ease of review these remain inline in a single file in this project.
# Installation and Usage
The project was written in Visual Studio Community 2022 (17.7.6) and targets the .NET 6.0 framework.
## Execution
The project should run directly from Visual Studio. It has not been tested for deployment.
## Configuration
Settings are in the standard `appsettings.json`. The following keys are used:
1. **PeriodConfigurationFile**: The file containining the period/state data for traffic light behaviour.
1. **StartTime**: The internal time at which the simulation starts, in ISO text format. By default this is 07:59 for demonstration purposes (as it will show the changeover from morning off-peak to morning peak).
1. **TimeFactor**: The time "acceleration" factor (floating point) for the simulation. A factor of 1.0 will run in real-time, and higher factors will run faster. Default is 4.0.

View File

@ -48,6 +48,7 @@
<p><strong>Current state data: </strong><span id="state-data">Awaiting first update...</span></p> <p><strong>Current state data: </strong><span id="state-data">Awaiting first update...</span></p>
<hr /> <hr />
<p><strong>Time until next update: </strong><span id="time-remaining">Awaiting first update...</span> second(s)</p> <p><strong>Time until next update: </strong><span id="time-remaining">Awaiting first update...</span> second(s)</p>
<p><strong>Time factor: </strong><span id="time-factor">Awaiting first update...</span></p>
<hr /> <hr />
<div class="wrapper"> <div class="wrapper">
<div></div> <div></div>
@ -83,21 +84,32 @@
async function ready() async function ready()
{ {
const req = new XMLHttpRequest(); const req = new XMLHttpRequest();
const MIN_UPDATE_TIME_MS = 100;
let nextUpdateTime = Date.now() let isSending = false;
let nextUpdateTime = Date.now() + 1;
let timeFactor = null;
let lastSimulationTimestamp = null;
let lastUpdateTimestamp = null;
req.onload = function() req.onload = function()
{ {
if (this.status != 200) if (this.status !== 200)
{ {
console.log("Failed update: status " + this.status); console.log("Failed update: status " + this.status);
return; return;
} }
light_status = JSON.parse(this.responseText);
let static_path_prefix = "/images/"; let static_path_prefix = "/images/";
document.getElementById("period").innerHTML = light_status.currentPeriodVerboseName; let now = Date.now()
document.getElementById("time").innerHTML = light_status.simulationTime;
let light_status = JSON.parse(this.responseText);
lastSimulationTimestamp = light_status.currentSimulationTimestampMs;
lastUpdateTimestamp = now;
timeFactor = light_status.timeFactor;
document.getElementById("period").innerHTML = light_status.currentPeriod['verbose-name'];
document.getElementById("state-index").innerHTML = light_status.currentStateIndex; document.getElementById("state-index").innerHTML = light_status.currentStateIndex;
document.getElementById("state-data").innerHTML = JSON.stringify(light_status.currentState); document.getElementById("state-data").innerHTML = JSON.stringify(light_status.currentState);
@ -107,23 +119,23 @@
document.getElementById("west").style.backgroundImage = "url(" + static_path_prefix + light_status.currentState.west + ".png)"; document.getElementById("west").style.backgroundImage = "url(" + static_path_prefix + light_status.currentState.west + ".png)";
document.getElementById("east").style.backgroundImage = "url(" + static_path_prefix + light_status.currentState.east + ".png)"; document.getElementById("east").style.backgroundImage = "url(" + static_path_prefix + light_status.currentState.east + ".png)";
document.getElementById("south").style.backgroundImage = "url(" + static_path_prefix + light_status.currentState.south + ".png)"; document.getElementById("south").style.backgroundImage = "url(" + static_path_prefix + light_status.currentState.south + ".png)";
document.getElementById("time-factor").innerHTML = light_status.timeFactor.toFixed(2);
// Due to differences in what the server considers now and what the local script considers now, this may not be accurate nextUpdateTime = light_status.nextChangeoverTimestampMs;
nextUpdateTime = Date.now() + (light_status.secondsUntilChangeover * 1000); isSending = false;
}; };
while (true) while (true)
{ {
await sleep(100); let now = Date.now();
let now = Date.now() if (!isSending && now >= nextUpdateTime)
if (Date.now() >= nextUpdateTime)
{ {
// console.log("Tick"); isSending = true;
req.open("GET", "/GetStatus", false); req.open("GET", "/GetStatus", false);
// Note try/catch for exceptions is the only way to account for net::ERR_CONNECTION_REFUSED - this is NOT dealt with by XMLHttpRequest event hooks. // Note try/catch is the only way to account for net::ERR_CONNECTION_REFUSED - this is NOT dealt with by XMLHttpRequest event hooks.
// In the case of net::ERR_CONNECTION_REFUSED should throw DOMException with name "NetworkError". // In the case of net::ERR_CONNECTION_REFUSED should throw DOMException with name "NetworkError".
try try
{ {
@ -137,6 +149,15 @@
// Important note: Due to marginal overshoot of scheduled timing, it is possible for time-remaining to be very slightly negative // Important note: Due to marginal overshoot of scheduled timing, it is possible for time-remaining to be very slightly negative
// It is undesirable to present this on the frontend, and therefore zero is the lowest number displayed // It is undesirable to present this on the frontend, and therefore zero is the lowest number displayed
document.getElementById("time-remaining").innerHTML = Math.max(0, (nextUpdateTime - now) / 1000).toFixed(3); document.getElementById("time-remaining").innerHTML = Math.max(0, (nextUpdateTime - now) / 1000).toFixed(3);
document.getElementById("")
if (lastSimulationTimestamp !== null && lastUpdateTimestamp !== null)
{
let simulationDate = new Date(lastSimulationTimestamp + parseInt((now - lastUpdateTimestamp) * timeFactor));
document.getElementById("time").innerHTML = simulationDate.toUTCString().slice(17, 25); // Time section of date string only
}
await sleep(Math.min(MIN_UPDATE_TIME_MS, nextUpdateTime - now));
} }
} }

View File

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