diff --git a/HomeApi/Controllers/HomeController.cs b/HomeApi/Controllers/HomeController.cs index e1f8c4e..ddb3195 100644 --- a/HomeApi/Controllers/HomeController.cs +++ b/HomeApi/Controllers/HomeController.cs @@ -6,35 +6,28 @@ using Microsoft.AspNetCore.Mvc; namespace HomeApi.Controllers; [ApiController] -[Route("[controller]")] +[Route("home")] public class HomeController(IMediator mediator) : ControllerBase { - [HttpGet(Name = "GetHome")] + [HttpGet(Name = "getHome")] public async Task> Get() { return Ok(await mediator.Send(new Weather.Command())); } - [HttpGet("default.bmp")] + [HttpGet("default.jpg")] public async Task GetImage() { - return File(await mediator.Send(new ImageGeneration.Command()), "image/bmp"); + return File(await mediator.Send(new ImageGeneration.Command()), "image/jpeg"); } - /*[HttpGet("screen/buffers")] - public async Task GetCombinedBuffers() + [HttpGet("configuration")] + public async Task> GetCombinedBuffers() { - var (black, red) = await mediator.Send(new ImageGeneration.Command()); - - // Combine buffers - byte[] combined = new byte[black.Length + red.Length]; - Buffer.BlockCopy(black, 0, combined, 0, black.Length); - Buffer.BlockCopy(red, 0, combined, black.Length, red.Length); - - return File(combined, "application/octet-stream"); - }*/ + return Ok(await mediator.Send(new Configuration.Command())); + } - [HttpGet("departureboard")] + [HttpGet("departure-board")] public async Task>> GetDepartureBoard() { return Ok(await mediator.Send(new DepartureBoard.Command())); diff --git a/HomeApi/Handlers/Configuration.cs b/HomeApi/Handlers/Configuration.cs new file mode 100644 index 0000000..e306d76 --- /dev/null +++ b/HomeApi/Handlers/Configuration.cs @@ -0,0 +1,31 @@ +using HomeApi.Models; +using HomeApi.Models.Configuration; +using MediatR; +using Microsoft.Extensions.Options; + +namespace HomeApi.Handlers; + +public static class Configuration +{ + public record Command : IRequest; + + public class Handler(IOptions configuration) + : IRequestHandler + { + private readonly ApiConfiguration _apiConfiguration = configuration.Value; + + public Task Handle(Command request, CancellationToken cancellationToken) + { + return Task.FromResult(new MicroProcessorConfiguration + { + InformationBoardImageUrl = _apiConfiguration.EspConfiguration.InformationBoardImageUrl, + UpdateIntervalMinutes = _apiConfiguration.EspConfiguration.UpdateIntervalMinutes, + BlackTextThreshold = _apiConfiguration.EspConfiguration.BlackTextThreshold, + ContrastStrength = _apiConfiguration.EspConfiguration.ContrastStrength, + DitheringStrength = _apiConfiguration.EspConfiguration.DitheringStrength, + EnableDithering = _apiConfiguration.EspConfiguration.EnableDithering, + EnhanceContrast = _apiConfiguration.EspConfiguration.EnhanceContrast + }); + } + } +} \ No newline at end of file diff --git a/HomeApi/Handlers/DepartureBoard.cs b/HomeApi/Handlers/DepartureBoard.cs index 8bf80dd..eb425f9 100644 --- a/HomeApi/Handlers/DepartureBoard.cs +++ b/HomeApi/Handlers/DepartureBoard.cs @@ -8,18 +8,11 @@ public static class DepartureBoard { public record Command : IRequest>; - public class Handler : IRequestHandler> + public class Handler(IDepartureBoardService departureBoardService) : IRequestHandler> { - private readonly IDepartureBoardService _departureBoardService; - - public Handler(IDepartureBoardService departureBoardService) - { - _departureBoardService = departureBoardService; - } - public async Task> Handle(Command request, CancellationToken cancellationToken) { - return await _departureBoardService.GetDepartureBoard() ?? new List(); + return await departureBoardService.GetDepartureBoard() ?? new List(); } } } \ No newline at end of file diff --git a/HomeApi/Handlers/ImageGeneration.cs b/HomeApi/Handlers/ImageGeneration.cs index 744537b..22a88f2 100644 --- a/HomeApi/Handlers/ImageGeneration.cs +++ b/HomeApi/Handlers/ImageGeneration.cs @@ -5,10 +5,6 @@ using MediatR; using Microsoft.Extensions.Options; using PuppeteerSharp; using RazorLight; -using SixLabors.ImageSharp.Formats.Bmp; -using SixLabors.ImageSharp.PixelFormats; -using SixLabors.ImageSharp.Processing; -using Image = SixLabors.ImageSharp.Image; namespace HomeApi.Handlers; @@ -16,24 +12,18 @@ public static class ImageGeneration { public record Command : IRequest; - public class Handler : IRequestHandler + public class Handler( + IWebHostEnvironment env, + IMediator mediator, + IOptions apiConfiguration) + : IRequestHandler { - private readonly ILogger _logger; - private readonly IWebHostEnvironment _env; - private readonly IMediator _mediator; - public Handler( - IOptions apiConfiguration, - ILogger logger, IWebHostEnvironment env, IMediator mediator) - { - _logger = logger; - _env = env; - _mediator = mediator; - } + private readonly ApiConfiguration _apiConfiguration = apiConfiguration.Value; public async Task Handle(Command request, CancellationToken cancellationToken) { - var weather = await _mediator.Send(new Weather.Command(), cancellationToken); - var departureBoard = await _mediator.Send(new DepartureBoard.Command(), cancellationToken); + var weather = await mediator.Send(new Weather.Command(), cancellationToken); + var departureBoard = await mediator.Send(new DepartureBoard.Command(), cancellationToken); var model = new Models.Image { @@ -50,13 +40,19 @@ public static class ImageGeneration .UseMemoryCachingProvider() .Build(); - var path = Path.Combine(_env.WebRootPath, "index.cshtml"); + var path = Path.Combine(env.WebRootPath, "index.cshtml"); var template = await File.ReadAllTextAsync(path, cancellationToken); - var result = await engine.CompileRenderStringAsync("templateKey", template, model, viewBag: new ExpandoObject()); + dynamic viewBag = new ExpandoObject(); + viewBag.IsHighContrast = _apiConfiguration.EspConfiguration.IsHighContrastMode; + + var result = await engine.CompileRenderStringAsync("templateKey", template, model, viewBag: viewBag); - return await CreateImage(result); + if (!string.IsNullOrEmpty(result)) + return await CreateImage(result); + + throw new Exception("Failed to generate HTML content for image."); } private static async Task CreateImage(string htmlContent) @@ -68,63 +64,16 @@ public static class ImageGeneration Headless = true, Args = ["--disable-gpu"] }); + var page = await browser.NewPageAsync(); await page.SetViewportAsync(new ViewPortOptions { Width = 800, Height = 480 }); - await page.SetContentAsync(htmlContent, new NavigationOptions { WaitUntil = new[] { WaitUntilNavigation.Networkidle0 } }); - var stream = await page.ScreenshotStreamAsync(new ScreenshotOptions { Type = ScreenshotType.Png }); - - return await stream.ToBmpStream(); + + await page.SetContentAsync(htmlContent, new NavigationOptions { WaitUntil = [WaitUntilNavigation.Networkidle0] }); + return await page.ScreenshotStreamAsync(new ScreenshotOptions { Type = ScreenshotType.Jpeg, Quality = 60 }); } } - - private static async Task ToBmpStream(this Stream stream) - { - var image = await Image.LoadAsync(stream); - // Resize or crop to 800x480 if necessary - image.Mutate(x => x.Resize(800, 480)); - - // Reduce to 3-color e-paper palette - image.ProcessPixelRows(accessor => - { - for (var y = 0; y < accessor.Height; y++) - { - var row = accessor.GetRowSpan(y); - for (var x = 0; x < row.Length; x++) - { - var pixel = row[x]; - - // Compute perceived brightness (gray) - var brightness = 0.299f * pixel.R + 0.587f * pixel.G + 0.114f * pixel.B; - - if (pixel.R > 150 && pixel.G < 80 && pixel.B < 80) // RED threshold - { - row[x] = new Rgba32(255, 0, 0); // Red - } - else if (brightness > 180) - { - row[x] = new Rgba32(255, 255, 255); // White - } - else - { - row[x] = new Rgba32(0, 0, 0); // Black - } - } - } - }); - - var bmpStream = new MemoryStream(); - var bmpEncoder = new BmpEncoder - { - BitsPerPixel = BmpBitsPerPixel.Pixel24 - }; - - await image.SaveAsync(bmpStream, bmpEncoder); - bmpStream.Position = 0; - - return bmpStream; - } } \ No newline at end of file diff --git a/HomeApi/HomeApi.csproj b/HomeApi/HomeApi.csproj index 5d6b263..0d348c1 100644 --- a/HomeApi/HomeApi.csproj +++ b/HomeApi/HomeApi.csproj @@ -17,7 +17,6 @@ - diff --git a/HomeApi/Integration/AuroraService.cs b/HomeApi/Integration/AuroraService.cs index 52ef175..d1bfd65 100644 --- a/HomeApi/Integration/AuroraService.cs +++ b/HomeApi/Integration/AuroraService.cs @@ -12,6 +12,14 @@ public class AuroraService(IAuroraClient auroraApi) : IAuroraService { public Task GetAuroraForecastAsync(string lat, string lon) { - return auroraApi.GetForecastAsync(latitude: lat, longitude: lon); + try + { + return auroraApi.GetForecastAsync(latitude: lat, longitude: lon); + } + catch (Exception e) + { + Console.WriteLine(e); + throw; + } } } \ No newline at end of file diff --git a/HomeApi/Models/Configuration/ApiConfiguration.cs b/HomeApi/Models/Configuration/ApiConfiguration.cs index e21eb70..c4644f9 100644 --- a/HomeApi/Models/Configuration/ApiConfiguration.cs +++ b/HomeApi/Models/Configuration/ApiConfiguration.cs @@ -6,6 +6,7 @@ public class ApiConfiguration public BaseUrls BaseUrls { get; set; } = new(); public string DefaultCity { get; set; } = "Vega stockholms lan"; public string DefaultStation { get; set; } = "Vega station"; + public EspConfig EspConfiguration { get; set; } = new(); } public class BaseUrls @@ -22,4 +23,16 @@ public class Keys public string Nominatim { get; set; } = string.Empty; public string Aurora { get; set; } = string.Empty; public string ResRobot { get; set; } = string.Empty; +} + +public class EspConfig +{ + public string InformationBoardImageUrl { get; set; } = string.Empty; + public int UpdateIntervalMinutes { get; set; } = 2; + public int BlackTextThreshold { get; set; } = 190; // (0-255) + public bool EnableDithering { get; set; } = true; + public int DitheringStrength { get; set; } = 8; // (8-32) + public bool EnhanceContrast { get; set; } = true; + public int ContrastStrength { get; set; } = 10; // (0-100) + public bool IsHighContrastMode { get; set; } = true; } \ No newline at end of file diff --git a/HomeApi/Models/MicroProcessorConfiguration.cs b/HomeApi/Models/MicroProcessorConfiguration.cs new file mode 100644 index 0000000..fdb8bd3 --- /dev/null +++ b/HomeApi/Models/MicroProcessorConfiguration.cs @@ -0,0 +1,12 @@ +namespace HomeApi.Models; + +public class MicroProcessorConfiguration +{ + public string InformationBoardImageUrl { get; set; } = string.Empty; + public int UpdateIntervalMinutes { get; set; } = 2; + public int BlackTextThreshold { get; set; } = 190; // (0-255) + public bool EnableDithering { get; set; } = true; + public int DitheringStrength { get; set; } = 8; // (8-32) + public bool EnhanceContrast { get; set; } = true; + public int ContrastStrength { get; set; } = 10; // (0-100) +} \ No newline at end of file diff --git a/HomeApi/Program.cs b/HomeApi/Program.cs index 817a1de..5482fb1 100644 --- a/HomeApi/Program.cs +++ b/HomeApi/Program.cs @@ -13,11 +13,8 @@ var app = builder.Build(); app.UseStaticFiles(); -if (app.Environment.IsDevelopment()) -{ - app.MapOpenApi(); - app.MapScalarApiReference(); -} +app.MapOpenApi(); +app.MapScalarApiReference(); app.UseHttpsRedirection(); diff --git a/HomeApi/appsettings.Development.json b/HomeApi/appsettings.Development.json index 0b65097..c288e85 100644 --- a/HomeApi/appsettings.Development.json +++ b/HomeApi/appsettings.Development.json @@ -13,6 +13,16 @@ } }, "ApiConfiguration": { + "EspConfiguration": { + "InformationBoardImageUrl": "http://192.168.101.178:5000/home/default.jpg", + "UpdateIntervalMinutes": 2, + "BlackTextThreshold": 190, + "EnableDithering": true, + "DitheringStrength": 8, + "EnhanceContrast": true, + "ContrastStrength": 10, + "IsHighContrastMode": true + }, "Keys": { "Weather": "NOT COMMITED", "ResRobot": "NOT COMMITED" diff --git a/HomeApi/appsettings.json b/HomeApi/appsettings.json index 0b65097..c288e85 100644 --- a/HomeApi/appsettings.json +++ b/HomeApi/appsettings.json @@ -13,6 +13,16 @@ } }, "ApiConfiguration": { + "EspConfiguration": { + "InformationBoardImageUrl": "http://192.168.101.178:5000/home/default.jpg", + "UpdateIntervalMinutes": 2, + "BlackTextThreshold": 190, + "EnableDithering": true, + "DitheringStrength": 8, + "EnhanceContrast": true, + "ContrastStrength": 10, + "IsHighContrastMode": true + }, "Keys": { "Weather": "NOT COMMITED", "ResRobot": "NOT COMMITED" diff --git a/HomeApi/wwwroot/index.cshtml b/HomeApi/wwwroot/index.cshtml index 840c1c1..f6cd989 100644 --- a/HomeApi/wwwroot/index.cshtml +++ b/HomeApi/wwwroot/index.cshtml @@ -122,6 +122,11 @@ return map.TryGetValue(code, out var value) ? value : "fa-question-circle"; } + + private static string UseHighContrast(bool isHighContrast) + { + return isHighContrast ? "high-contrast" : string.Empty; + } } @@ -143,6 +148,11 @@ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; } + .high-contrast * { + color: hsl(0 0% 0%) !important; + border-color: hsl(0 0% 0%) !important; + } + /* Color Variables */ :root { /* Base colors - Grayscale */ @@ -205,7 +215,7 @@ } .location-icon { - color: hsl(0 85% 60%); + color: hsl(0 85% 60%) !important; font-size: 14px; } @@ -256,24 +266,24 @@ } .cloud-icon { - color: hsl(var(--weather-cloudy)); + color: hsl(var(--weather-cloudy)) !important; } .wind-icon, .activity-icon { - color: hsl(0 85% 60%); + color: hsl(0 85% 60%) !important; } .aurora-icon { - color: hsl(var(--weather-aurora)); + color: hsl(var(--weather-aurora)) !important; } .air-quality-badge { background: hsl(var(--air-good)); - color: white; + color: hsl(0, 0%, 100%) !important; padding: 8px 16px; border-radius: 20px; font-size: 12px; - font-weight: 500; + font-weight: bolder; text-align: center; } @@ -317,7 +327,7 @@ display: flex; justify-content: center; margin: 4px 0; - color: hsl(var(--weather)); + color: hsl(var(--weather)) !important; } .forecast-condition { @@ -383,11 +393,11 @@ } .sunrise-icon, .sunset-icon { - color: hsl(0 85% 60%); + color: hsl(0 85% 60%) !important; } .moon-icon { - color: hsl(0 85% 60%); + color: hsl(0 85% 60%) !important; } /* Right Column - Transport */ @@ -425,7 +435,7 @@ } .transport-color { - color: hsl(var(--transport-color)); + color: hsl(var(--transport-color)) !important; } .transport-info { @@ -472,145 +482,145 @@ -
- -
-
- - @Model.Weather.CityName +
+ +
+
+ + @Model.Weather.CityName +
+ +
+
@Model.Weather.Current.Temperature°C
+
Feels like @Model.Weather.Current.Feelslike°C
+
+ +
+
+
+ + Clouds +
+ @Model.Weather.Current.Cloud%
- -
-
@Model.Weather.Current.Temperature°C
-
Feels like @Model.Weather.Current.Feelslike°C
+ +
+
+ + Wind +
+ @Model.Weather.Current.WindPerMeterSecond.ToString("0.##"); m/s @Model.Weather.Current.WindDirection
- -
-
-
- - Clouds -
- @Model.Weather.Current.Cloud% -
- -
-
- - Wind -
- @Model.Weather.Current.WindPerMeterSecond.ToString("0.##"); m/s @Model.Weather.Current.WindDirection -
- -
-
- - Gusts -
- @Model.Weather.Current.WindGustPerMeterSecond.ToString("0.##"); m/s -
- -
-
- - Aurora -
- @Model.Weather.Current.AuroraProbability.Value% + +
+
+ + Gusts
+ @Model.Weather.Current.WindGustPerMeterSecond.ToString("0.##"); m/s
- -
- Air Quality: @GetAirQualityStatus(Model.Weather.Current.AirQuality) + +
+
+ + Aurora +
+ @Model.Weather.Current.AuroraProbability.Value%
- - -
-

@Model.Weather.Forecast.Count-Day Forecast

- -
+ +
+ Air Quality: @GetAirQualityStatus(Model.Weather.Current.AirQuality) +
+
+ + +
+

@Model.Weather.Forecast.Count-Day Forecast

+ +
@foreach (var day in Model.Weather.Forecast) {
@GetDayStatus(day.Date)
- +
@(day.Day?.ConditionText ?? "Unspecified")
@day.MinTempC°/@day.MaxTempC°
Rain: @day.ChanceOfRain%
} -
- -
-
-
- - Sunrise -
- @Model.Weather.Forecast[0].Astro.Sunrise -
- -
-
- - Sunset -
- @Model.Weather.Forecast[0].Astro.Sunset -
- -
-
- - Moonrise -
- @Model.Weather.Forecast[0].Astro.Moonrise -
- -
-
- - Moonset -
- @Model.Weather.Forecast[0].Astro.Moonset -
- -
- Moon phase - @Model.Weather.Forecast[0].Astro.Moon_Illumination% -
-
- - -
-

Upcoming Departures

- -
- @foreach (var transport in Model.TimeTable.Take(5)) - { - - var departureTime = DateTime.Parse(transport.DepartureTime); - var minutesUntilDeparture = (int)(departureTime - DateTime.Now).TotalMinutes; - -
- -
-
@transport.LineNumber
-
to @transport.Direction
-
-
@DateTime.Parse(transport.DepartureTime).ToShortTimeString() (@minutesUntilDeparture min)
-
- } + +
+
+
+ + Sunrise +
+ @Model.Weather.Forecast[0].Astro.Sunrise
- -
-
Last updated:
+ +
+
+ + Sunset +
+ @Model.Weather.Forecast[0].Astro.Sunset +
+ +
+
+ + Moonrise +
+ @Model.Weather.Forecast[0].Astro.Moonrise +
+ +
+
+ + Moonset +
+ @Model.Weather.Forecast[0].Astro.Moonset +
+ +
+ Moon phase + @Model.Weather.Forecast[0].Astro.Moon_Illumination%
+ +
+

Upcoming Departures

+ +
+ @foreach (var transport in Model.TimeTable.Take(5)) + { + + var departureTime = DateTime.Parse(transport.DepartureTime); + var minutesUntilDeparture = (int)(departureTime - DateTime.Now).TotalMinutes; + +
+ +
+
@transport.LineNumber
+
to @transport.Direction
+
+
@DateTime.Parse(transport.DepartureTime).ToShortTimeString() (@minutesUntilDeparture min)
+
+ } +
+ +
+
Last updated:
+
+
+
+