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
|
||||
{
|
||||
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)
|
||||
{
|
||||
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 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">
|
||||
<div class="container">
|
||||
© 2024 - TrafficLightsSignalR - <a asp-area="" asp-page="/Privacy">Privacy</a>
|
||||
© 2024 - TrafficLightsSignalR
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
|
|
|
@ -3,12 +3,9 @@ using System.Text.Json.Serialization;
|
|||
|
||||
namespace TrafficLightsSignalR.Schema
|
||||
{
|
||||
public class Period
|
||||
{
|
||||
public string Name { get; set; } = "";
|
||||
public string VerboseName { get; set; } = "";
|
||||
public string TimeStart { get; set; } = "";
|
||||
public TimeOnly TimeStartParsed;
|
||||
public List<State> States { get; set; } = [];
|
||||
}
|
||||
public record Period(
|
||||
string Name,
|
||||
string VerboseName,
|
||||
TimeOnly TimeStart,
|
||||
List<State> States);
|
||||
}
|
||||
|
|
|
@ -1,12 +1,10 @@
|
|||
namespace TrafficLightsSignalR.Schema
|
||||
{
|
||||
public record State
|
||||
{
|
||||
public int Duration { get; set; } = -1;
|
||||
public string North { get; set; } = "";
|
||||
public string South { get; set; } = "";
|
||||
public string East { get; set; } = "";
|
||||
public string West { get; set; } = "";
|
||||
public string NorthRight { get; set; } = "";
|
||||
}
|
||||
public record State(
|
||||
int Duration,
|
||||
string North,
|
||||
string South,
|
||||
string East,
|
||||
string West,
|
||||
string NorthRight);
|
||||
}
|
||||
|
|
|
@ -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
|
||||
{
|
||||
// string objects are immutable
|
||||
protected TrafficLightState _state;
|
||||
private readonly IHubContext<LightHub, ILightHub> _hubContext;
|
||||
protected TrafficLightState? _state;
|
||||
|
||||
public TrafficLightStateService()
|
||||
public TrafficLightStateService(IHubContext<LightHub, ILightHub> hubContext)
|
||||
{
|
||||
_state = new TrafficLightState(
|
||||
new Schema.Period(),
|
||||
new Schema.State(),
|
||||
-1,
|
||||
0.0,
|
||||
-1,
|
||||
"",
|
||||
0.0,
|
||||
-1,
|
||||
-1);
|
||||
_hubContext = hubContext;
|
||||
}
|
||||
|
||||
public void SetCurrentTrafficLightState(TrafficLightState currentTrafficLightState)
|
||||
{
|
||||
_state = currentTrafficLightState;
|
||||
_hubContext.Clients.All.ReceiveStateUpdate(_state);
|
||||
}
|
||||
|
||||
public TrafficLightState GetCurrentTrafficLightState()
|
||||
public TrafficLightState? GetCurrentTrafficLightState()
|
||||
{
|
||||
return _state;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@ using TrafficLightsSignalR.Schema;
|
|||
|
||||
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
|
||||
const long TICKS_PER_SECOND = 10000000;
|
||||
|
@ -15,7 +15,6 @@ namespace TrafficLightsSignalR
|
|||
|
||||
// Interconnects
|
||||
private readonly ILogger<TrafficLightsHostedService> _logger = logger;
|
||||
private readonly IHubContext<LightHub> _hubContext = hubContext;
|
||||
private readonly ITrafficLightStateService _stateService = stateService;
|
||||
private readonly IConfiguration _configuration = configuration;
|
||||
|
||||
|
@ -29,7 +28,26 @@ namespace TrafficLightsSignalR
|
|||
private void UpdateState(TrafficLightState 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))]
|
||||
|
@ -46,22 +64,19 @@ namespace TrafficLightsSignalR
|
|||
// 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 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)
|
||||
{
|
||||
throw new InvalidOperationException($"JSON deserialisation returned no data available in given configuration file: {periodDataFilename}");
|
||||
}
|
||||
periodDataStream.Close();
|
||||
|
||||
// Parse file for convenience
|
||||
foreach (Period period in _periodData)
|
||||
{
|
||||
timeParseSuccess = TimeOnly.TryParse(period.TimeStart, out period.TimeStartParsed);
|
||||
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}");
|
||||
_logger.LogInformation($"Period loaded - name: '{period.Name}', timestart: '{period.TimeStart}', parsed timestart: {period.TimeStart}, parse success: {timeParseSuccess}");
|
||||
|
||||
foreach (State state in period.States)
|
||||
{
|
||||
|
@ -82,7 +97,7 @@ namespace TrafficLightsSignalR
|
|||
{
|
||||
return index;
|
||||
}
|
||||
if (testTime >= _periodData[index].TimeStartParsed && testTime < _periodData[index + 1].TimeStartParsed)
|
||||
if (testTime >= _periodData[index].TimeStart && testTime < _periodData[index + 1].TimeStart)
|
||||
{
|
||||
return index;
|
||||
}
|
||||
|
@ -110,10 +125,10 @@ namespace TrafficLightsSignalR
|
|||
_logger.LogInformation($"Next changeover second(s): {TimeSpan.FromTicks(nextChangeoverTicks - startTicks)}");
|
||||
_logger.LogInformation($"Start simulation DT: {startSimulationDateTime}");
|
||||
|
||||
UpdateState(CreateStateRecord(_periodData[currentPeriodIndex], _periodData[currentPeriodIndex].States[currentStateIndex], currentStateIndex, nextChangeoverTicks, startSimulationDateTime, startSimulationDateTime));
|
||||
|
||||
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;
|
||||
long nowTicks = now.Ticks;
|
||||
|
||||
|
@ -139,17 +154,7 @@ namespace TrafficLightsSignalR
|
|||
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)");
|
||||
|
||||
var currentState = new TrafficLightState(
|
||||
_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);
|
||||
UpdateState(CreateStateRecord(_periodData[currentPeriodIndex], _periodData[currentPeriodIndex].States[currentStateIndex], currentStateIndex, nextChangeoverTicks, startSimulationDateTime, currentSimulationDateTime));
|
||||
}
|
||||
|
||||
await Task.Delay((int)((1000 / UPDATES_PER_SIMULATED_SECOND) / _timeFactor), cancellationToken);
|
||||
|
|
|
@ -3,21 +3,6 @@ using TrafficLightsSignalR.Schema;
|
|||
|
||||
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(
|
||||
Period CurrentPeriod,
|
||||
State CurrentState,
|
||||
|
|
|
@ -8,16 +8,21 @@ var nextUpdateTime = null;
|
|||
var connection = new signalR.HubConnectionBuilder().withUrl("/lightHub").build();
|
||||
|
||||
connection.on("ReceiveStateUpdate", function (stateData) {
|
||||
console.log("Update pushed.");
|
||||
console.log("Received pushed update.");
|
||||
updateView(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);
|
||||
});
|
||||
|
||||
connection.start().then(function () {
|
||||
console.log("Connection started, requesting server state...");
|
||||
connection.invoke("GetServerState", connection.connectionId).catch(function (err) {
|
||||
return console.error(err.toString());
|
||||
});
|
||||
|
@ -25,23 +30,6 @@ connection.start().then(function () {
|
|||
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) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms))
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue