Newer
Older
EinkScreen / Program.cs
@fabre fabre on 23 Apr 7 KB 安定版本
using System.Diagnostics;
using System.Globalization;
using System.Text.Json;
using System.Text.RegularExpressions;
using SkiaSharp;

const int Width = 250;
const int Height = 122;
const int SensorBottom = 44;
const int DetailsTop = 75;
const int LeftPaneRight = 126;
const int RightPaneLeft = 127;
const string OutputPath = "output.png";
const float LineSpacing = -3;
const string LocationCode = "14104";
const string SensorReadingsUrl = "http://10.16.1.100:8016/odata/SensorReadings?$orderby=Ts%20desc&$top=1&$select=TemperatureC,HumidityRh";
const string ReadyUrl = "http://10.16.1.100:8016/health/ready";

using var bitmap = new SKBitmap(Width, Height, SKColorType.Bgra8888, SKAlphaType.Premul);
using var canvas = new SKCanvas(bitmap);

canvas.Clear(SKColors.White);

using var textPaint = new SKPaint
{
    Color = SKColors.Black,
    IsAntialias = true
};

using var typeface = SKTypeface.Default;
using var sensorFont = new SKFont(typeface, 40);
using var weatherFont = new SKFont(typeface, 21);
using var statusFont = new SKFont(typeface, 27);
using var dateFont = new SKFont(typeface, 14);
using var timeFont = new SKFont(typeface, 23);

var currentSensor = await GetCurrentSensor();
var weatherForecast = await GetWeatherForecast();
using var statusHttpClient = new HttpClient();
var statusText = (await statusHttpClient.GetStringAsync(ReadyUrl)).Trim();
var now = DateTime.Now;
var date = now.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture);
var time = now.ToString("HH:mm", CultureInfo.InvariantCulture);

DrawTextFit(canvas, currentSensor, sensorFont, textPaint, new SKRect(0, 0, Width, SensorBottom), SKTextAlign.Center);
DrawTextFit(canvas, weatherForecast, weatherFont, textPaint, new SKRect(0, SensorBottom, Width, DetailsTop), SKTextAlign.Center);
DrawTextFit(canvas, statusText, statusFont, textPaint, new SKRect(0, DetailsTop, LeftPaneRight, Height), SKTextAlign.Center);
DrawTextStack(
    canvas,
    [(date, dateFont), (time, timeFont)],
    textPaint,
    new SKRect(RightPaneLeft, DetailsTop, Width, Height),
    SKTextAlign.Center,
    LineSpacing);

using var image = SKImage.FromBitmap(bitmap);
using var data = image.Encode(SKEncodedImageFormat.Png, 100);

if (data is null)
{
    Console.Error.WriteLine("SkiaSharp failed to encode the bitmap as PNG.");
    return;
}

var fullOutputPath = Path.GetFullPath(OutputPath);
await using (var fileStream = File.Create(fullOutputPath))
{
    data.SaveTo(fileStream);
}

Console.WriteLine($"Generated {fullOutputPath} ({Width}x{Height}).");

var showImageScriptPath = Path.GetFullPath("show_image.py");
using var showImageProcess = Process.Start(new ProcessStartInfo
{
    FileName = "python3",
    ArgumentList = { showImageScriptPath, fullOutputPath },
    UseShellExecute = false
});

if (showImageProcess != null)
{
    await showImageProcess.WaitForExitAsync();
}

static void DrawTextFit(
    SKCanvas canvas,
    string text,
    SKFont font,
    SKPaint paint,
    SKRect bounds,
    SKTextAlign align)
{
    var originalSize = font.Size;
    var measuredWidth = font.MeasureText(text);

    if (measuredWidth > bounds.Width)
    {
        font.Size = Math.Max(8, originalSize * bounds.Width / measuredWidth);
    }

    var metrics = font.Metrics;
    var x = align switch
    {
        SKTextAlign.Center => bounds.MidX,
        SKTextAlign.Right => bounds.Right,
        _ => bounds.Left
    };
    var y = bounds.MidY - (metrics.Ascent + metrics.Descent) / 2;

    canvas.DrawText(text, x, y, align, font, paint);
    font.Size = originalSize;
}

static void DrawTextStack(
    SKCanvas canvas,
    IReadOnlyList<(string Text, SKFont Font)> lines,
    SKPaint paint,
    SKRect bounds,
    SKTextAlign align,
    float lineSpacing)
{
    var originalSizes = lines.Select(line => line.Font.Size).ToArray();

    for (var i = 0; i < lines.Count; i++)
    {
        var line = lines[i];
        var measuredWidth = line.Font.MeasureText(line.Text);

        if (measuredWidth > bounds.Width)
        {
            line.Font.Size = Math.Max(8, line.Font.Size * bounds.Width / measuredWidth);
        }
    }

    var lineHeights = lines
        .Select(line =>
        {
            var metrics = line.Font.Metrics;
            return metrics.Descent - metrics.Ascent;
        })
        .ToArray();
    var totalHeight = lineHeights.Sum() + lineSpacing * Math.Max(0, lines.Count - 1);
    var y = bounds.MidY - totalHeight / 2;
    var x = align switch
    {
        SKTextAlign.Center => bounds.MidX,
        SKTextAlign.Right => bounds.Right,
        _ => bounds.Left
    };

    for (var i = 0; i < lines.Count; i++)
    {
        var line = lines[i];
        var metrics = line.Font.Metrics;
        var baseline = y - metrics.Ascent;

        canvas.DrawText(line.Text, x, baseline, align, line.Font, paint);
        y += lineHeights[i] + lineSpacing;
    }

    for (var i = 0; i < lines.Count; i++)
    {
        lines[i].Font.Size = originalSizes[i];
    }
}

static async Task<string> GetCurrentSensor()
{
    using var httpClient = new HttpClient();
    using var stream = await httpClient.GetStreamAsync(SensorReadingsUrl);
    using var json = await JsonDocument.ParseAsync(stream);

    var latest = json.RootElement.GetProperty("value").EnumerateArray().FirstOrDefault();
    var temp = latest.GetProperty("TemperatureC").GetDouble();
    var humidity = latest.GetProperty("HumidityRh").GetDouble();

    return $"{temp:0}°C {humidity:0}%";
}

static async Task<string> GetWeatherForecast()
{
    var forecastAfter = DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ssZ");
    var filter = Uri.EscapeDataString($"ForecastDt gt {forecastAfter} and LocationCode eq '{LocationCode}'");
    var url = $"http://10.16.1.100:8016/odata/Weather?$filter={filter}&$orderby=ForecastDt asc&$top=1&$select=ForecastDt,WeatherText,TemperatureC";

    using var httpClient = new HttpClient();
    using var stream = await httpClient.GetStreamAsync(url);
    using var json = await JsonDocument.ParseAsync(stream);

    var forecast = json.RootElement.GetProperty("value").EnumerateArray().FirstOrDefault();
    var temp = forecast.GetProperty("TemperatureC").GetDouble();
    var weatherText = forecast.GetProperty("WeatherText").GetString() ?? "";
    var weatherMatch = Regex.Match(weatherText, @"\(([^()]*)\)");
    var weather = weatherMatch.Success ? weatherMatch.Groups[1].Value : weatherText;
    var forecastDt = forecast.GetProperty("ForecastDt").GetDateTimeOffset();
    var (low, high) = await GetDailyTemperatureRange(httpClient, forecastDt);

    return $"{low:0}~{high:0}°C {temp:0}°C {weather}";
}

static async Task<(double Low, double High)> GetDailyTemperatureRange(HttpClient httpClient, DateTimeOffset forecastDt)
{
    var dayStart = new DateTimeOffset(forecastDt.Year, forecastDt.Month, forecastDt.Day, 0, 0, 0, forecastDt.Offset);
    var dayEnd = dayStart.AddDays(1);
    var filter = Uri.EscapeDataString(
        $"ForecastDt ge {dayStart.UtcDateTime:yyyy-MM-ddTHH:mm:ssZ} and ForecastDt lt {dayEnd.UtcDateTime:yyyy-MM-ddTHH:mm:ssZ} and LocationCode eq '{LocationCode}'");
    var url = $"http://10.16.1.100:8016/odata/Weather?$filter={filter}&$orderby=ForecastDt asc&$select=TemperatureC";

    using var stream = await httpClient.GetStreamAsync(url);
    using var json = await JsonDocument.ParseAsync(stream);

    var temperatures = json.RootElement
        .GetProperty("value")
        .EnumerateArray()
        .Select(item => item.GetProperty("TemperatureC").GetDouble())
        .ToArray();

    return (temperatures.Min(), temperatures.Max());
}