Skip to content

Commit

Permalink
Merge pull request #3 from timia2109/feature/mqtt
Browse files Browse the repository at this point in the history
Feature/mqtt
  • Loading branch information
timia2109 authored Mar 29, 2024
2 parents d9d3e2d + b44b12b commit 92991d2
Show file tree
Hide file tree
Showing 10 changed files with 429 additions and 2 deletions.
1 change: 1 addition & 0 deletions DisplayUtil.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="8.0.0" />
<PackageReference Include="MQTTnet" Version="4.3.3.952" />
<PackageReference Include="NetDaemon.Client" Version="24.8.0" />
<PackageReference Include="NetDaemon.HassModel" Version="24.8.0" />
<PackageReference Include="Scriban" Version="5.9.1" />
Expand Down
85 changes: 85 additions & 0 deletions MqttExport/BinaryImageStreamCreater.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
using SkiaSharp;

namespace DisplayUtil.MqttExport;

/// <summary>
/// Responsible to render the Bitmap to the two color stream
/// </summary>
public static class BinaryImageStreamCreator
{

public static byte[] GetImageStream(SKBitmap bitmap)
{
var pixelWriter = new PixelWriter();

var width = bitmap.Width;
var height = bitmap.Height;

// The stream is width -> height
for (var y = 1; y <= height; y++)
{
for (var x = 1; x <= width; x++)
{
var pixel = bitmap.GetPixel(x, y);
pixelWriter.WritePixel(pixel);
}
}

return pixelWriter.ToArray();
}

private class PixelWriter
{
public void WritePixel(SKColor color)
{
var luma = 0.2126 * color.Red
+ 0.7152 * color.Green
+ 0.0722 * color.Blue;

WriteBit(luma < 40);
}

private readonly MemoryStream _stream;
private byte _currentByte;
private int _bitsFilled;

public PixelWriter()
{
_stream = new MemoryStream();
_currentByte = 0;
_bitsFilled = 0;
}

public void WriteBit(bool bit)
{
// Shift the current byte to the left by 1 and add the new bit on the end.
_currentByte = (byte)((_currentByte << 1) | (bit ? 1 : 0));
_bitsFilled++;

// If the current byte is full (contains 8 bits), write it to the stream.
if (_bitsFilled == 8)
{
_stream.WriteByte(_currentByte);
// Reset for the next byte.
_currentByte = 0;
_bitsFilled = 0;
}
}

public byte[] ToArray()
{
// If there are any bits that haven't been written yet because they didn't make up a full byte,
// write them now. This will effectively pad the last byte with zeroes if it's not full.
if (_bitsFilled > 0)
{
_currentByte = (byte)(_currentByte << (8 - _bitsFilled));
_stream.WriteByte(_currentByte);
_currentByte = 0;
_bitsFilled = 0;
}

return _stream.ToArray();
}
}

}
72 changes: 72 additions & 0 deletions MqttExport/EspImageProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
using System.Diagnostics;
using DisplayUtil.Scenes;

namespace DisplayUtil.MqttExport;

/// <summary>
/// Responsible to provide the images in a form for the ESP.
/// Scoped
/// </summary>
public partial class EspImageProvider(
ILogger<MqttExporter> logger,
ScreenRepository screenRepository
)
{
private readonly ILogger _logger = logger;

/// <summary>
/// Gets the image as plain true / false Bytes
/// </summary>
/// <param name="providerId">Id of the provider</param>
/// <returns>Byte Array</returns>
public async Task<byte[]> GetAsPlainBytesAsync(string providerId)
{
var stopwatch = new Stopwatch();
LogRender(providerId);
stopwatch.Start();

//Render
using var image = await screenRepository.GetImageAsync(providerId);
stopwatch.Stop();
var elapsed = stopwatch.ElapsedMilliseconds;
LogRenderTime(elapsed);

var binaryData = BinaryImageStreamCreator.GetImageStream(image);
LogPlainBytes(binaryData.Length);
return binaryData;
}

/// <summary>
/// Gets the Image as RunLength Compressed
/// </summary>
/// <param name="providerId">Id of the provider</param>
/// <returns>Compressed Data</returns>
public async Task<byte[]> GetAsRunLengthAsync(string providerId)
{
var plainBytes = await GetAsPlainBytesAsync(providerId);

var runLengthEncoder = new RunLengthCompressor();
var compressedData = runLengthEncoder.WriteStream(plainBytes);

var compressedPercent = compressedData.Length / (float)plainBytes.Length;
LogCompressedBytes(
compressedData.Length,
Math.Round((1 - compressedPercent) * 100, 2)
);

return compressedData;
}


[LoggerMessage(LogLevel.Information, "Render screen {providerId}")]
private partial void LogRender(string providerId);

[LoggerMessage(LogLevel.Information, "Rendering took {time} ms")]
private partial void LogRenderTime(long time);

[LoggerMessage(LogLevel.Information, "Plain binary takes {bytes} bytes")]
private partial void LogPlainBytes(int bytes);

[LoggerMessage(LogLevel.Information, "Compressing takes {bytes} bytes ({percent} % less)")]
private partial void LogCompressedBytes(int bytes, double percent);
}
43 changes: 43 additions & 0 deletions MqttExport/ExportingMqttClient.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
using Microsoft.Extensions.Options;
using MQTTnet.Client;
using MQTTnet.Protocol;

namespace DisplayUtil.MqttExport;

/// <summary>
/// Responsible to export the Image to Mqtt
/// </summary>
public partial class ExportingMqttClient(
IMqttClient client,
MqttClientOptions clientOptions,
IOptions<MqttSettings> settings,
ILogger<ExportingMqttClient> logger
)
{

private readonly ILogger _logger = logger;

private async Task EnsureConnectedAsync()
{
if (!client.IsConnected)
{
LogConnecting();
await client.ConnectAsync(clientOptions);
}
}

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

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

[LoggerMessage(LogLevel.Information, "Connecting to Mqtt Server")]
private partial void LogConnecting();
}
21 changes: 21 additions & 0 deletions MqttExport/MqttExporter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
using System.Diagnostics;
using DisplayUtil.Scenes;
using MQTTnet.Client;

namespace DisplayUtil.MqttExport;

/// <summary>
/// Responsible to export the image to Mqtt.
/// Scoped
/// </summary>
public class MqttExporter(
EspImageProvider espImageProvider,
ExportingMqttClient exportingMqttClient
)
{
public async Task ExportScreenToMqtt(string providerId)
{
var data = await espImageProvider.GetAsRunLengthAsync(providerId);
await exportingMqttClient.SendAsync(data);
}
}
52 changes: 52 additions & 0 deletions MqttExport/MqttInitExtension.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
using System.Security.Cryptography.X509Certificates;
using MQTTnet;
using MQTTnet.Client;

namespace DisplayUtil.MqttExport;

public static class MqttInitExtension
{
public static IHostApplicationBuilder AddMqttWriter(this IHostApplicationBuilder builder)
{
builder.Services.Configure<MqttSettings>(
builder.Configuration.GetSection("Mqtt"));

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

return builder;
}

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

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

var factory = new MqttFactory();

var client = factory.CreateMqttClient();

var clientOptions = new MqttClientOptionsBuilder()
.WithTcpServer(settings.Uri)
.WithClientId("displayutil_" + builder.Environment.EnvironmentName)
.WithCredentials(settings.User, settings.Password)
.Build();

if (client is null || clientOptions is null)
throw new Exception("Error creating MQTT instances");

builder.Services
.AddSingleton(client)
.AddSingleton(clientOptions)
.AddSingleton<ExportingMqttClient>()
.AddScoped<EspImageProvider>()
.AddScoped<MqttExporter>();

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

public record MqttSettings
{
public string? Uri { get; init; }
public string? User { get; init; }
public string? Password { get; init; }
public string? Topic { get; init; }
public string? ScreenDetectTemplate { get; init; }
}
Loading

0 comments on commit 92991d2

Please sign in to comment.