refactor: multiple
refactor: TimeOnly JSON converter added - regularizes record types refactor: Strongly typed SignalR hub - removes client method name strings chore: Redundant file cleanup
This commit is contained in:
parent
c81f5e8641
commit
311355aca4
|
@ -0,0 +1,8 @@
|
||||||
|
namespace TrafficLightsSignalR.Hubs
|
||||||
|
{
|
||||||
|
public interface ILightHub
|
||||||
|
{
|
||||||
|
Task ReceiveStateUpdate(TrafficLightState state);
|
||||||
|
Task ReceiveServerState(TrafficLightState? state);
|
||||||
|
}
|
||||||
|
}
|
|
@ -4,13 +4,22 @@ using TrafficLightsSignalR.Schema;
|
||||||
|
|
||||||
namespace TrafficLightsSignalR.Hubs
|
namespace TrafficLightsSignalR.Hubs
|
||||||
{
|
{
|
||||||
public class LightHub(ITrafficLightStateService stateService) : Hub
|
public class LightHub : Hub<ILightHub>
|
||||||
{
|
{
|
||||||
private readonly ITrafficLightStateService _stateService = stateService;
|
private readonly ITrafficLightStateService _stateService;
|
||||||
|
private readonly ILogger<LightHub> _logger;
|
||||||
|
|
||||||
|
public LightHub(ITrafficLightStateService stateService, ILogger<LightHub> logger)
|
||||||
|
{
|
||||||
|
_stateService = stateService;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
public async Task GetServerState(string connectionId)
|
public async Task GetServerState(string connectionId)
|
||||||
{
|
{
|
||||||
await Clients.Client(connectionId).SendAsync("ReceiveServerState", _stateService.GetCurrentTrafficLightState());
|
var currentState = _stateService.GetCurrentTrafficLightState();
|
||||||
|
_logger.LogInformation($"Request for server state received, current state null?: ${currentState is null}");
|
||||||
|
await Clients.Client(connectionId).ReceiveServerState(currentState);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,6 @@
|
||||||
public interface ITrafficLightStateService
|
public interface ITrafficLightStateService
|
||||||
{
|
{
|
||||||
public void SetCurrentTrafficLightState(TrafficLightState currentTrafficLightState);
|
public void SetCurrentTrafficLightState(TrafficLightState currentTrafficLightState);
|
||||||
public TrafficLightState GetCurrentTrafficLightState();
|
public TrafficLightState? GetCurrentTrafficLightState();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +0,0 @@
|
||||||
@page
|
|
||||||
@model PrivacyModel
|
|
||||||
@{
|
|
||||||
ViewData["Title"] = "Privacy Policy";
|
|
||||||
}
|
|
||||||
<h1>@ViewData["Title"]</h1>
|
|
||||||
|
|
||||||
<p>Use this page to detail your site's privacy policy.</p>
|
|
|
@ -1,20 +0,0 @@
|
||||||
using Microsoft.AspNetCore.Mvc;
|
|
||||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
|
||||||
|
|
||||||
namespace TrafficLightsSignalR.Pages
|
|
||||||
{
|
|
||||||
public class PrivacyModel : PageModel
|
|
||||||
{
|
|
||||||
private readonly ILogger<PrivacyModel> _logger;
|
|
||||||
|
|
||||||
public PrivacyModel(ILogger<PrivacyModel> logger)
|
|
||||||
{
|
|
||||||
_logger = logger;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void OnGet()
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -38,7 +38,7 @@
|
||||||
|
|
||||||
<footer class="border-top footer text-muted">
|
<footer class="border-top footer text-muted">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
© 2024 - TrafficLightsSignalR - <a asp-area="" asp-page="/Privacy">Privacy</a>
|
© 2024 - TrafficLightsSignalR
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
|
|
|
@ -3,12 +3,9 @@ using System.Text.Json.Serialization;
|
||||||
|
|
||||||
namespace TrafficLightsSignalR.Schema
|
namespace TrafficLightsSignalR.Schema
|
||||||
{
|
{
|
||||||
public class Period
|
public record Period(
|
||||||
{
|
string Name,
|
||||||
public string Name { get; set; } = "";
|
string VerboseName,
|
||||||
public string VerboseName { get; set; } = "";
|
TimeOnly TimeStart,
|
||||||
public string TimeStart { get; set; } = "";
|
List<State> States);
|
||||||
public TimeOnly TimeStartParsed;
|
|
||||||
public List<State> States { get; set; } = [];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,12 +1,10 @@
|
||||||
namespace TrafficLightsSignalR.Schema
|
namespace TrafficLightsSignalR.Schema
|
||||||
{
|
{
|
||||||
public record State
|
public record State(
|
||||||
{
|
int Duration,
|
||||||
public int Duration { get; set; } = -1;
|
string North,
|
||||||
public string North { get; set; } = "";
|
string South,
|
||||||
public string South { get; set; } = "";
|
string East,
|
||||||
public string East { get; set; } = "";
|
string West,
|
||||||
public string West { get; set; } = "";
|
string NorthRight);
|
||||||
public string NorthRight { get; set; } = "";
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,28 @@
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
|
namespace TrafficLightsSignalR.Schema
|
||||||
|
{
|
||||||
|
public class TimeOnlyConverter : JsonConverter<TimeOnly>
|
||||||
|
{
|
||||||
|
private const string TimeFormat = "HH:mm";
|
||||||
|
|
||||||
|
public override TimeOnly Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||||
|
{
|
||||||
|
string timeStr = reader.GetString() ?? throw new JsonException("Invalid time format - null string received.");
|
||||||
|
|
||||||
|
if (TimeOnly.TryParseExact(timeStr, TimeFormat, null, System.Globalization.DateTimeStyles.None, out TimeOnly time))
|
||||||
|
{
|
||||||
|
return time;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new JsonException("Invalid time format. Expected format: HH:mm");
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Write(Utf8JsonWriter writer, TimeOnly value, JsonSerializerOptions options)
|
||||||
|
{
|
||||||
|
writer.WriteStringValue(value.ToString(TimeFormat));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -1,32 +1,29 @@
|
||||||
namespace TrafficLightsSignalR
|
using Microsoft.AspNetCore.SignalR;
|
||||||
|
using TrafficLightsSignalR.Hubs;
|
||||||
|
using TrafficLightsSignalR.Schema;
|
||||||
|
|
||||||
|
namespace TrafficLightsSignalR
|
||||||
{
|
{
|
||||||
public class TrafficLightStateService : ITrafficLightStateService
|
public class TrafficLightStateService : ITrafficLightStateService
|
||||||
{
|
{
|
||||||
// string objects are immutable
|
private readonly IHubContext<LightHub, ILightHub> _hubContext;
|
||||||
protected TrafficLightState _state;
|
protected TrafficLightState? _state;
|
||||||
|
|
||||||
public TrafficLightStateService()
|
public TrafficLightStateService(IHubContext<LightHub, ILightHub> hubContext)
|
||||||
{
|
{
|
||||||
_state = new TrafficLightState(
|
_hubContext = hubContext;
|
||||||
new Schema.Period(),
|
|
||||||
new Schema.State(),
|
|
||||||
-1,
|
|
||||||
0.0,
|
|
||||||
-1,
|
|
||||||
"",
|
|
||||||
0.0,
|
|
||||||
-1,
|
|
||||||
-1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void SetCurrentTrafficLightState(TrafficLightState currentTrafficLightState)
|
public void SetCurrentTrafficLightState(TrafficLightState currentTrafficLightState)
|
||||||
{
|
{
|
||||||
_state = currentTrafficLightState;
|
_state = currentTrafficLightState;
|
||||||
|
_hubContext.Clients.All.ReceiveStateUpdate(_state);
|
||||||
}
|
}
|
||||||
|
|
||||||
public TrafficLightState GetCurrentTrafficLightState()
|
public TrafficLightState? GetCurrentTrafficLightState()
|
||||||
{
|
{
|
||||||
return _state;
|
return _state;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -6,7 +6,7 @@ using TrafficLightsSignalR.Schema;
|
||||||
|
|
||||||
namespace TrafficLightsSignalR
|
namespace TrafficLightsSignalR
|
||||||
{
|
{
|
||||||
public class TrafficLightsHostedService(IHubContext<LightHub> hubContext, ITrafficLightStateService stateService, IConfiguration configuration, ILogger<TrafficLightsHostedService> logger) : BackgroundService
|
public class TrafficLightsHostedService(ITrafficLightStateService stateService, IConfiguration configuration, ILogger<TrafficLightsHostedService> logger) : BackgroundService
|
||||||
{
|
{
|
||||||
// Simulation constants
|
// Simulation constants
|
||||||
const long TICKS_PER_SECOND = 10000000;
|
const long TICKS_PER_SECOND = 10000000;
|
||||||
|
@ -15,7 +15,6 @@ namespace TrafficLightsSignalR
|
||||||
|
|
||||||
// Interconnects
|
// Interconnects
|
||||||
private readonly ILogger<TrafficLightsHostedService> _logger = logger;
|
private readonly ILogger<TrafficLightsHostedService> _logger = logger;
|
||||||
private readonly IHubContext<LightHub> _hubContext = hubContext;
|
|
||||||
private readonly ITrafficLightStateService _stateService = stateService;
|
private readonly ITrafficLightStateService _stateService = stateService;
|
||||||
private readonly IConfiguration _configuration = configuration;
|
private readonly IConfiguration _configuration = configuration;
|
||||||
|
|
||||||
|
@ -29,7 +28,26 @@ namespace TrafficLightsSignalR
|
||||||
private void UpdateState(TrafficLightState state)
|
private void UpdateState(TrafficLightState state)
|
||||||
{
|
{
|
||||||
_stateService.SetCurrentTrafficLightState(state);
|
_stateService.SetCurrentTrafficLightState(state);
|
||||||
_ = _hubContext.Clients.All.SendAsync("ReceiveStateUpdate", state);
|
}
|
||||||
|
|
||||||
|
private TrafficLightState CreateStateRecord(Period currentPeriod, State currentState, int currentStateIndex, long nextChangeoverTicks, DateTime startSimulationDateTime, DateTime currentSimulationDateTime)
|
||||||
|
{
|
||||||
|
return new TrafficLightState(
|
||||||
|
currentPeriod,
|
||||||
|
currentState,
|
||||||
|
currentStateIndex,
|
||||||
|
// Seconds until next changeover
|
||||||
|
(nextChangeoverTicks - DateTime.UtcNow.Ticks) / (double)TICKS_PER_SECOND,
|
||||||
|
// Next changeover timestamp in UNIX time, millisecond granularity
|
||||||
|
(nextChangeoverTicks - EPOCH_TICKS) / (TICKS_PER_SECOND / 1000), // Division by 1000 -> needs to be in ms
|
||||||
|
// Current simulation time string, formatted for display
|
||||||
|
TimeOnly.FromDateTime(currentSimulationDateTime).ToString(),
|
||||||
|
// Current time factor
|
||||||
|
_timeFactor,
|
||||||
|
// Current simulated timestamp in UNIX time, millisecond granularity
|
||||||
|
(currentSimulationDateTime.Ticks - EPOCH_TICKS) / (TICKS_PER_SECOND / 1000), // Division by 1000 -> needs to be in ms
|
||||||
|
// Start timestamp (simulation time) in UNIX time, millisecond granularity
|
||||||
|
(startSimulationDateTime.Ticks - EPOCH_TICKS) / (TICKS_PER_SECOND / 1000)); // Division by 1000 -> needs to be in ms
|
||||||
}
|
}
|
||||||
|
|
||||||
[MemberNotNull(nameof(_periodData))]
|
[MemberNotNull(nameof(_periodData))]
|
||||||
|
@ -46,22 +64,19 @@ namespace TrafficLightsSignalR
|
||||||
// Load and deserialised JSON data from file
|
// Load and deserialised JSON data from file
|
||||||
var periodDataFilename = _configuration.GetValue<string>("PeriodConfigurationFile") ?? throw new InvalidOperationException("Unable to find period data filename (PeriodConfigurationFile) in configuration -> please check application settings");
|
var periodDataFilename = _configuration.GetValue<string>("PeriodConfigurationFile") ?? throw new InvalidOperationException("Unable to find period data filename (PeriodConfigurationFile) in configuration -> please check application settings");
|
||||||
var periodDataStream = File.OpenRead(periodDataFilename);
|
var periodDataStream = File.OpenRead(periodDataFilename);
|
||||||
_periodData = JsonSerializer.Deserialize<List<Period>>(periodDataStream); // may throw JsonException
|
|
||||||
|
var serializerOptions = new JsonSerializerOptions { Converters = { new TimeOnlyConverter() } };
|
||||||
|
_periodData = JsonSerializer.Deserialize<List<Period>>(periodDataStream, serializerOptions); // may throw JsonException
|
||||||
if (_periodData is null)
|
if (_periodData is null)
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException($"JSON deserialisation returned no data available in given configuration file: {periodDataFilename}");
|
throw new InvalidOperationException($"JSON deserialisation returned no data available in given configuration file: {periodDataFilename}");
|
||||||
}
|
}
|
||||||
|
periodDataStream.Close();
|
||||||
|
|
||||||
// Parse file for convenience
|
// Parse file for convenience
|
||||||
foreach (Period period in _periodData)
|
foreach (Period period in _periodData)
|
||||||
{
|
{
|
||||||
timeParseSuccess = TimeOnly.TryParse(period.TimeStart, out period.TimeStartParsed);
|
_logger.LogInformation($"Period loaded - name: '{period.Name}', timestart: '{period.TimeStart}', parsed timestart: {period.TimeStart}, parse success: {timeParseSuccess}");
|
||||||
if (!timeParseSuccess)
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException($"Unable to parse time start string in period data: ${period.TimeStart}");
|
|
||||||
}
|
|
||||||
|
|
||||||
_logger.LogInformation($"Period found with name: '{period.Name}', timestart: '{period.TimeStart}', parsed timestart: {period.TimeStartParsed}, parse success: {timeParseSuccess}");
|
|
||||||
|
|
||||||
foreach (State state in period.States)
|
foreach (State state in period.States)
|
||||||
{
|
{
|
||||||
|
@ -82,7 +97,7 @@ namespace TrafficLightsSignalR
|
||||||
{
|
{
|
||||||
return index;
|
return index;
|
||||||
}
|
}
|
||||||
if (testTime >= _periodData[index].TimeStartParsed && testTime < _periodData[index + 1].TimeStartParsed)
|
if (testTime >= _periodData[index].TimeStart && testTime < _periodData[index + 1].TimeStart)
|
||||||
{
|
{
|
||||||
return index;
|
return index;
|
||||||
}
|
}
|
||||||
|
@ -110,10 +125,10 @@ namespace TrafficLightsSignalR
|
||||||
_logger.LogInformation($"Next changeover second(s): {TimeSpan.FromTicks(nextChangeoverTicks - startTicks)}");
|
_logger.LogInformation($"Next changeover second(s): {TimeSpan.FromTicks(nextChangeoverTicks - startTicks)}");
|
||||||
_logger.LogInformation($"Start simulation DT: {startSimulationDateTime}");
|
_logger.LogInformation($"Start simulation DT: {startSimulationDateTime}");
|
||||||
|
|
||||||
|
UpdateState(CreateStateRecord(_periodData[currentPeriodIndex], _periodData[currentPeriodIndex].States[currentStateIndex], currentStateIndex, nextChangeoverTicks, startSimulationDateTime, startSimulationDateTime));
|
||||||
|
|
||||||
while (!cancellationToken.IsCancellationRequested)
|
while (!cancellationToken.IsCancellationRequested)
|
||||||
{
|
{
|
||||||
// _logger.LogInformation("Working behind the scenes...");
|
|
||||||
// _stateService.SetCurrentTrafficLightState(string.Format("Thoughts about life, the universe, and everything! [ {0} ]", DateTime.Now));
|
|
||||||
now = DateTime.UtcNow;
|
now = DateTime.UtcNow;
|
||||||
long nowTicks = now.Ticks;
|
long nowTicks = now.Ticks;
|
||||||
|
|
||||||
|
@ -139,17 +154,7 @@ namespace TrafficLightsSignalR
|
||||||
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($" Next changeover in {_periodData[currentPeriodIndex].States[currentStateIndex].Duration} simulation second(s)");
|
_logger.LogInformation($" Next changeover in {_periodData[currentPeriodIndex].States[currentStateIndex].Duration} simulation second(s)");
|
||||||
|
|
||||||
var currentState = new TrafficLightState(
|
UpdateState(CreateStateRecord(_periodData[currentPeriodIndex], _periodData[currentPeriodIndex].States[currentStateIndex], currentStateIndex, nextChangeoverTicks, startSimulationDateTime, currentSimulationDateTime));
|
||||||
_periodData[currentPeriodIndex],
|
|
||||||
_periodData[currentPeriodIndex].States[currentStateIndex],
|
|
||||||
currentStateIndex,
|
|
||||||
(nextChangeoverTicks - DateTime.UtcNow.Ticks) / (double)TICKS_PER_SECOND,
|
|
||||||
(nextChangeoverTicks - EPOCH_TICKS) / (TICKS_PER_SECOND / 1000), // Division by 1000 -> needs to be in ms
|
|
||||||
TimeOnly.FromDateTime(currentSimulationDateTime).ToString(),
|
|
||||||
_timeFactor,
|
|
||||||
(currentSimulationDateTime.Ticks - EPOCH_TICKS) / (TICKS_PER_SECOND / 1000), // Division by 1000 -> needs to be in ms
|
|
||||||
(startSimulationDateTime.Ticks - EPOCH_TICKS) / (TICKS_PER_SECOND / 1000)); // Division by 1000 -> needs to be in ms
|
|
||||||
UpdateState(currentState);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await Task.Delay((int)((1000 / UPDATES_PER_SIMULATED_SECOND) / _timeFactor), cancellationToken);
|
await Task.Delay((int)((1000 / UPDATES_PER_SIMULATED_SECOND) / _timeFactor), cancellationToken);
|
||||||
|
|
|
@ -3,21 +3,6 @@ using TrafficLightsSignalR.Schema;
|
||||||
|
|
||||||
namespace TrafficLightsSignalR
|
namespace TrafficLightsSignalR
|
||||||
{
|
{
|
||||||
/*
|
|
||||||
public record TrafficLightsState
|
|
||||||
{
|
|
||||||
public Period CurrentPeriod { get; init; }
|
|
||||||
public State CurrentState { get; init; }
|
|
||||||
public int CurrentStateIndex { get; init; }
|
|
||||||
public double SecondsUntilChangeover { get; init; }
|
|
||||||
public long NextChangeoverTimestampMs { get; init; }
|
|
||||||
public string CurrentSimulationTime { get; init; } // UTC
|
|
||||||
public double TimeFactor { get; init; }
|
|
||||||
public long CurrentSimulationTimestampMs { get; init; } // UNIX epoch timestamp (UTC in milliseconds)
|
|
||||||
public long StartSimulationTimestampMs { get; init; } // UNIX epoch timestamp (UTC in milliseconds)
|
|
||||||
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
public record TrafficLightState(
|
public record TrafficLightState(
|
||||||
Period CurrentPeriod,
|
Period CurrentPeriod,
|
||||||
State CurrentState,
|
State CurrentState,
|
||||||
|
|
|
@ -8,16 +8,21 @@ var nextUpdateTime = null;
|
||||||
var connection = new signalR.HubConnectionBuilder().withUrl("/lightHub").build();
|
var connection = new signalR.HubConnectionBuilder().withUrl("/lightHub").build();
|
||||||
|
|
||||||
connection.on("ReceiveStateUpdate", function (stateData) {
|
connection.on("ReceiveStateUpdate", function (stateData) {
|
||||||
console.log("Update pushed.");
|
console.log("Received pushed update.");
|
||||||
updateView(stateData);
|
updateView(stateData);
|
||||||
});
|
});
|
||||||
|
|
||||||
connection.on("ReceiveServerState", function (stateData) {
|
connection.on("ReceiveServerState", function (stateData) {
|
||||||
console.log("Recevied requested server state...")
|
console.log("Recevied requested server state...");
|
||||||
|
if (stateData == null) {
|
||||||
|
console.log("State not yet initialized, not updating.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
updateView(stateData);
|
updateView(stateData);
|
||||||
});
|
});
|
||||||
|
|
||||||
connection.start().then(function () {
|
connection.start().then(function () {
|
||||||
|
console.log("Connection started, requesting server state...");
|
||||||
connection.invoke("GetServerState", connection.connectionId).catch(function (err) {
|
connection.invoke("GetServerState", connection.connectionId).catch(function (err) {
|
||||||
return console.error(err.toString());
|
return console.error(err.toString());
|
||||||
});
|
});
|
||||||
|
@ -25,23 +30,6 @@ connection.start().then(function () {
|
||||||
return console.error(err.toString());
|
return console.error(err.toString());
|
||||||
});
|
});
|
||||||
|
|
||||||
document.getElementById("sendButton").addEventListener("click", function (event) {
|
|
||||||
var user = document.getElementById("userInput").value;
|
|
||||||
var message = document.getElementById("messageInput").value;
|
|
||||||
connection.invoke("SendMessage", user, message).catch(function (err) {
|
|
||||||
return console.error(err.toString());
|
|
||||||
});
|
|
||||||
event.preventDefault();
|
|
||||||
});
|
|
||||||
|
|
||||||
document.getElementById("updateButton").addEventListener("click", function (event) {
|
|
||||||
var connectionId = connection.connectionId;
|
|
||||||
connection.invoke("GetServerState", connectionId).catch(function (err) {
|
|
||||||
return console.error(err.toString());
|
|
||||||
});
|
|
||||||
event.preventDefault();
|
|
||||||
});
|
|
||||||
|
|
||||||
function sleep(ms) {
|
function sleep(ms) {
|
||||||
return new Promise(resolve => setTimeout(resolve, ms))
|
return new Promise(resolve => setTimeout(resolve, ms))
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue