Skip to content

Commit

Permalink
Merge pull request #7 from timia2109/feature/mqtt-url-export
Browse files Browse the repository at this point in the history
Feature/mqtt url export
  • Loading branch information
timia2109 authored Apr 4, 2024
2 parents f947a3e + 978e4ba commit 0ac479b
Show file tree
Hide file tree
Showing 11 changed files with 259 additions and 61 deletions.
31 changes: 31 additions & 0 deletions EspUtilities/EspUtilitiesInitExtension.cs
Original file line number Diff line number Diff line change
@@ -1,13 +1,44 @@
using System.Text;

namespace DisplayUtil.EspUtilities;

public static class EspUtilitiesInitExtension
{
public const string CompressedImageRoute = "/esp/{providerId}",
PlainImageRoute = "/esp/bits/{providerId}";

public static IHostApplicationBuilder AddEspUtilities(this IHostApplicationBuilder builder)
{
builder.Services.AddScoped<EspImageProvider>();
return builder;
}

public static WebApplication UseEspUtilities(this WebApplication app)
{
app.MapGet(CompressedImageRoute, async (string providerId, HttpContext ctx, EspImageProvider espProvider) =>
{
var (data, size) = await espProvider.GetAsRunLengthAsync(providerId);
var base64 = Convert.ToBase64String(data);
ctx.Response.Headers.Append("X-Width", size.Width.ToString());
ctx.Response.Headers.Append("X-Height", size.Height.ToString());
return Results.Text(base64, "text/plain", Encoding.ASCII);
})
.WithName("Get ESP Image")
.WithOpenApi();

app.MapGet(PlainImageRoute, async (string providerId, HttpContext ctx, EspImageProvider espProvider) =>
{
var (data, size) = await espProvider.GetAsPlainBytesAsync(providerId);
var base64 = Convert.ToBase64String(data);
ctx.Response.Headers.Append("X-Width", size.Width.ToString());
ctx.Response.Headers.Append("X-Height", size.Height.ToString());
return Results.Text(base64, "text/plain", Encoding.ASCII);
})
.WithName("Get ESP Bit Image")
.WithOpenApi();

return app;
}


}
11 changes: 6 additions & 5 deletions MqttExport/ExportingMqttClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
namespace DisplayUtil.MqttExport;

/// <summary>
/// Responsible to export the Image to Mqtt
/// Responsible to export the new URL to MQTT
/// </summary>
public partial class ExportingMqttClient(
IMqttClient client,
Expand All @@ -26,15 +26,16 @@ private async Task EnsureConnectedAsync()
}
}

public async Task SendAsync(byte[] data)
public async Task SendAsync(string payload)
{
await EnsureConnectedAsync();

await client.PublishBinaryAsync(
await client.PublishStringAsync(
settings.Value.Topic
?? throw new ArgumentNullException(nameof(settings.Value.Topic)),
data,
MqttQualityOfServiceLevel.AtLeastOnce
payload,
MqttQualityOfServiceLevel.AtLeastOnce,
true
);
}

Expand Down
40 changes: 40 additions & 0 deletions MqttExport/MqttExportJob.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
using Microsoft.Extensions.Options;

namespace DisplayUtil.MqttExport;

public class MqttExportJob(
IServiceScopeFactory scopeFactory,
IOptions<MqttSettings> optionsSnapshot
) : IHostedService
{
private CancellationTokenSource? _cancellationTokenSource;

private async Task RunAsync(CancellationToken cancellation)
{
while (!cancellation.IsCancellationRequested)
{
await using (var scope = scopeFactory.CreateAsyncScope())
{
var renderer = scope.ServiceProvider
.GetRequiredService<MqttUrlRenderer>();
await renderer.RenderUrlAndPublish();
}

await Task.Delay(optionsSnapshot.Value.RefreshInterval!.Value,
cancellation);
}
}

public Task StartAsync(CancellationToken cancellationToken)
{
_cancellationTokenSource = new CancellationTokenSource();
_ = RunAsync(_cancellationTokenSource.Token);
return Task.CompletedTask;
}

public Task StopAsync(CancellationToken cancellationToken)
{
_cancellationTokenSource?.Cancel();
return Task.CompletedTask;
}
}
66 changes: 61 additions & 5 deletions MqttExport/MqttExporter.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System.Diagnostics;
using DisplayUtil.EspUtilities;
using DisplayUtil.Scenes;
using Microsoft.Extensions.Options;
using MQTTnet.Client;

namespace DisplayUtil.MqttExport;
Expand All @@ -10,13 +11,68 @@ namespace DisplayUtil.MqttExport;
/// Scoped
/// </summary>
public class MqttExporter(
EspImageProvider espImageProvider,
ExportingMqttClient exportingMqttClient
ExportingMqttClient exportingMqttClient,
IOptions<MqttSettings> options
)
{
public async Task ExportScreenToMqtt(string providerId)
public Task ExportUriToMqtt(string providerId)
{
var (data, _) = await espImageProvider.GetAsRunLengthAsync(providerId);
await exportingMqttClient.SendAsync(data);
var settings = options.Value;

var query = providerId.IndexOf('?');
var providerPath = query == -1
? providerId
: providerId[0..(query - 1)];

var uriBuilder = new UriBuilder
{
Port = 80,
Scheme = "http",
Host = settings.ServerHostName,
Path = EspUtilitiesInitExtension.CompressedImageRoute
};

uriBuilder.Path = EspUtilitiesInitExtension.CompressedImageRoute
.Replace("{providerId}", providerPath);

if (query != -1)
uriBuilder.Query = providerId[query..];

return SubmitAsync(uriBuilder.Uri);
}

public virtual async Task SubmitAsync(Uri uri)
{
await exportingMqttClient.SendAsync(uri.ToString());
}
}

internal partial class CachedMqttExporter(
ExportingMqttClient exportingMqttClient,
IOptions<MqttSettings> options,
ILogger<CachedMqttExporter> logger)
: MqttExporter(exportingMqttClient, options)
{
private readonly ILogger _logger = logger;

private Uri? _lastSubmission;

public override Task SubmitAsync(Uri uri)
{
if (_lastSubmission == uri)
{
LogSkip(_lastSubmission);
return Task.CompletedTask;
}

LogSubmitting(uri);
_lastSubmission = uri;
return base.SubmitAsync(uri);
}

[LoggerMessage(LogLevel.Debug, "Skipping resubmission of {uri}")]
private partial void LogSkip(Uri uri);

[LoggerMessage(LogLevel.Debug, "Submitting Uri {uri}")]
private partial void LogSubmitting(Uri uri);
}
40 changes: 26 additions & 14 deletions MqttExport/MqttInitExtension.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using System.Security.Cryptography.X509Certificates;
using DisplayUtil.Utils;
using MQTTnet;
using MQTTnet.Client;

Expand All @@ -8,25 +8,36 @@ public static class MqttInitExtension
{
public static IHostApplicationBuilder AddMqttWriter(this IHostApplicationBuilder builder)
{
builder.Services.Configure<MqttSettings>(
builder.Configuration.GetSection("Mqtt"));
var settings = builder.ConfigureAndGet<MqttSettings>("Mqtt");
if (settings is null) return builder;

builder.Services.AddScoped<MqttExporter>();

if (!CreateMqttClient(builder))
{
if (!CreateMqttClient(builder, settings))
return builder;
}

if (settings.IncrementalUpdate)
builder.Services.AddSingleton<MqttExporter, CachedMqttExporter>();
else
builder.Services.AddSingleton<MqttExporter>();

if (
settings.ScreenDetectTemplate is null
|| settings.RefreshInterval is null
) return builder;

builder.Services.AddHostedService<MqttExportJob>();

return builder;
}

private static bool CreateMqttClient(IHostApplicationBuilder builder)
private static bool CreateMqttClient(
IHostApplicationBuilder builder,
MqttSettings settings
)
{
var settings = builder.Configuration.GetSection("Mqtt")
.Get<MqttSettings>();

if (settings is null || settings.Uri is null)
if (settings is null
|| settings.Uri is null
|| settings.ServerHostName is null
)
return false;

var factory = new MqttFactory();
Expand All @@ -45,7 +56,8 @@ private static bool CreateMqttClient(IHostApplicationBuilder builder)
builder.Services
.AddSingleton(client)
.AddSingleton(clientOptions)
.AddSingleton<ExportingMqttClient>();
.AddSingleton<ExportingMqttClient>()
.AddScoped<MqttUrlRenderer>();

return true;
}
Expand Down
37 changes: 37 additions & 0 deletions MqttExport/MqttSettings.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,47 @@
namespace DisplayUtil.MqttExport;

/// <summary>
/// Settings for the MQTT Handling
/// </summary>
public record MqttSettings
{
/// <summary>
/// URI of MQTT Server
/// </summary>
public string? Uri { get; init; }

/// <summary>
/// User
/// </summary>
public string? User { get; init; }

/// <summary>
/// Password
/// </summary>
public string? Password { get; init; }

/// <summary>
/// MQTT Topic
/// </summary>
public string? Topic { get; init; }

/// <summary>
/// Template used, to detect the MQTT Template
/// </summary>
public string? ScreenDetectTemplate { get; init; }

/// <summary>
/// Interval for rerendering the Template
/// </summary>
public TimeSpan? RefreshInterval { get; init; }

/// <summary>
/// Should the message only be updated, when the value changes?
/// </summary>
public bool IncrementalUpdate { get; init; }

/// <summary>
/// Hostname of the current server
/// </summary>
public string? ServerHostName { get; init; }
}
27 changes: 27 additions & 0 deletions MqttExport/MqttUrlRenderer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
using DisplayUtil.Template;
using Microsoft.Extensions.Options;

namespace DisplayUtil.MqttExport;

public class MqttUrlRenderer(
TemplateRenderer renderer,
MqttExporter exporter,
IOptionsSnapshot<MqttSettings> options
)
{

/// <summary>
/// Exports the Uri to MQTT
/// </summary>
/// <returns>Task</returns>
public async Task RenderUrlAndPublish()
{
var template = options.Value.ScreenDetectTemplate!;

var result = await renderer.RenderAsync(template,
EnrichScope.TemplateProvider);

await exporter.ExportUriToMqtt(result);
}

}
31 changes: 1 addition & 30 deletions Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -48,36 +48,7 @@
.WithName("Preview Image")
.WithOpenApi();

app.MapPost("/publish/{providerId}", async (string providerId, MqttExporter exporter) =>
{
await exporter.ExportScreenToMqtt(providerId);
return Results.NoContent();
})
.WithName("Publish manual to MQTT")
.WithOpenApi();

app.MapGet("/esp/{providerId}", async (string providerId, HttpContext ctx, EspImageProvider espProvider) =>
{
var (data, size) = await espProvider.GetAsRunLengthAsync(providerId);
var base64 = Convert.ToBase64String(data);
ctx.Response.Headers.Append("X-Width", size.Width.ToString());
ctx.Response.Headers.Append("X-Height", size.Height.ToString());
return Results.Text(base64, "text/plain", Encoding.ASCII);
})
.WithName("Get ESP Image")
.WithOpenApi();

app.MapGet("/esp/bits/{providerId}", async (string providerId, HttpContext ctx, EspImageProvider espProvider) =>
{
var (data, size) = await espProvider.GetAsPlainBytesAsync(providerId);
var base64 = Convert.ToBase64String(data);
ctx.Response.Headers.Append("X-Width", size.Width.ToString());
ctx.Response.Headers.Append("X-Height", size.Height.ToString());
return Results.Text(base64, "text/plain", Encoding.ASCII);
})
.WithName("Get ESP Bit Image")
.WithOpenApi();

app.UseEspUtilities();
app.UseStaticFiles();

app.Run();
7 changes: 6 additions & 1 deletion Template/ITemplateExtender.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,12 @@ public enum EnrichScope
/// <summary>
/// This template will used for ScreenRendering
/// </summary>
ScreenRendering
ScreenRendering,

/// <summary>
/// This template will used for Providing an other template
/// </summary>
TemplateProvider
}

/// <summary>
Expand Down
Loading

0 comments on commit 0ac479b

Please sign in to comment.