Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 11 additions & 14 deletions src/Aether/Devices/Drivers/BufferedDisplayDriver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,38 +22,35 @@ protected BufferedDisplayDriver(int width, int height, float dpiX, float dpiY)
/// <summary>
/// Draws an image onto the pixel buffer.
/// </summary>
/// <param name="srcImage">The <see cref="Image"/> to draw onto the pixel buffer. Must have been created via <see cref="DisplayDriver.CreateImage(DrawOrientation)"/> on this instance.</param>
/// <param name="srcImage">The <see cref="Image"/> to draw onto the pixel buffer. Must have been created via <see cref="DisplayDriver.CreateImage()"/> on this instance.</param>
/// <param name="fillPositionX">The X position to draw the image at.</param>
/// <param name="fillPositionY">The Y position to draw the image at.</param>
/// <param name="orientation">The orientation to draw the image in.</param>
public void DrawImage(Image srcImage, int fillPositionX, int fillPositionY, DrawOrientation orientation = DrawOrientation.Default)
/// <param name="options">Options controlling how the image is drawn.</param>
public void DrawImage(Image srcImage, int fillPositionX, int fillPositionY, DrawOptions options = DrawOptions.None)
{
(int w, int h) = orientation switch
{
DrawOrientation.Default => (srcImage.Width, srcImage.Height),
DrawOrientation.Rotate90 => (srcImage.Height, srcImage.Width),
_ => throw new ArgumentOutOfRangeException(nameof(orientation), orientation, $"{nameof(orientation)} is not a valid {nameof(DrawOrientation)} value.")
};
(int w, int h) = options.HasFlag(DrawOptions.Rotate90)
? (srcImage.Height, srcImage.Width)
: (srcImage.Width, srcImage.Height);

if (Width - fillPositionX < w || Height - fillPositionY < h)
{
throw new ArgumentException($"{nameof(srcImage)} is of an invalid size for this orientation; {nameof(DisplayImage)} must be called with images created from {nameof(CreateImage)}.", nameof(srcImage));
}

DrawImageCore(srcImage, fillPositionX, fillPositionY, orientation);
DrawImageCore(srcImage, fillPositionX, fillPositionY, options);
}

/// <inheritdoc cref="DrawImage(Image, int, int, DrawOrientation)"/>
protected abstract void DrawImageCore(Image srcImage, int fillPositionX, int fillPositionY, DrawOrientation orientation);
/// <inheritdoc cref="DrawImage(Image, int, int, DrawOptions)"/>
protected abstract void DrawImageCore(Image srcImage, int fillPositionX, int fillPositionY, DrawOptions options);

/// <summary>
/// Flushes the pixel buffer to the device.
/// </summary>
public abstract void Flush();

protected sealed override void DisplayImageCore(Image image, DrawOrientation orientation)
protected sealed override void DisplayImageCore(Image image, DrawOptions options)
{
DrawImageCore(image, fillPositionX: 0, fillPositionY: 0, orientation);
DrawImageCore(image, fillPositionX: 0, fillPositionY: 0, options);
Flush();
}
}
Expand Down
19 changes: 8 additions & 11 deletions src/Aether/Devices/Drivers/DisplayDriver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -52,28 +52,25 @@ protected DisplayDriver(int width, int height, float dpiX, float dpiY)
/// Displays an image to the device.
/// </summary>
/// <param name="image">The image to display. Must have been created via <see cref="CreateImage(int, int)"/> on this <see cref="DisplayDriver"/>.</param>
/// <param name="orientation">The orientation to display the image in</param>
/// <param name="options">Options controlling how the image is displayed.</param>
/// <exception cref="ArgumentOutOfRangeException"></exception>
/// <exception cref="ArgumentException"></exception>
public void DisplayImage(Image image, DrawOrientation orientation = DrawOrientation.Default)
public void DisplayImage(Image image, DrawOptions options = DrawOptions.None)
{
(int width, int height) = orientation switch
{
DrawOrientation.Default => (Width, Height),
DrawOrientation.Rotate90 => (Height, Width),
_ => throw new ArgumentOutOfRangeException(nameof(orientation), $"{nameof(orientation)} is not a valid {nameof(DrawOrientation)} value.")
};
(int width, int height) = options.HasFlag(DrawOptions.Rotate90)
? (Height, Width)
: (Width, Height);

if (image.Width != width || image.Height != height)
{
throw new ArgumentException($"{nameof(image)} is of an invalid size for this orientation; {nameof(DisplayImage)} must be called with images created from {nameof(CreateImage)}.", nameof(image));
}

DisplayImageCore(image, orientation);
DisplayImageCore(image, options);
}

/// <inheritdoc cref="DisplayImage(Image, DrawOrientation)"/>
protected abstract void DisplayImageCore(Image image, DrawOrientation orientation);
/// <inheritdoc cref="DisplayImage(Image, DrawOptions)"/>
protected abstract void DisplayImageCore(Image image, DrawOptions options);

public abstract void Dispose();
}
Expand Down
24 changes: 24 additions & 0 deletions src/Aether/Devices/Drivers/DrawOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
namespace Aether.Devices.Drivers
{
/// <summary>
/// Options for drawing to a <see cref="DisplayDriver"/>.
/// </summary>
[Flags]
public enum DrawOptions
{
/// <summary>
/// Default draw behavior.
/// </summary>
None,

/// <summary>
/// If supported, perform a refresh that trades quality for improved speed.
/// </summary>
PartialRefresh = 1,

/// <summary>
/// Rotates the image counter-clockwise by 90 degrees.
/// </summary>
Rotate90 = 2,
}
}
8 changes: 0 additions & 8 deletions src/Aether/Devices/Drivers/DrawOrientation.cs

This file was deleted.

2 changes: 1 addition & 1 deletion src/Aether/Devices/Drivers/Sk9822.cs
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ public override void Flush() =>
public override Image CreateImage(int width, int height) =>
new Image<Rgba32>(width, height);

protected override void DrawImageCore(Image srcImage, int fillPositionX, int fillPositionY, DrawOrientation orientation)
protected override void DrawImageCore(Image srcImage, int fillPositionX, int fillPositionY, DrawOptions options)
{
if (srcImage is not Image<Rgba32> img)
{
Expand Down
97 changes: 70 additions & 27 deletions src/Aether/Devices/Drivers/WaveshareEPD2_9inV2.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,29 @@ internal sealed class WaveshareEPD2_9inV2 : DisplayDriver
0x22, 0x17, 0x41, 0x0, 0x32, 0x36
};

private static ReadOnlySpan<byte> s_PartialLUT => new byte[]
{
0x0, 0x40, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
0x80, 0x80, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
0x40, 0x40, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
0x0, 0x80, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
0x0A, 0x0, 0x0, 0x0, 0x0, 0x0, 0x2,
0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x0, 0x0, 0x0,
0x22, 0x17, 0x41, 0xB0, 0x32, 0x36,
};

private readonly SpiDevice _device;
private readonly GpioController _gpio;
private readonly int _dcPinId;
Expand Down Expand Up @@ -101,28 +124,45 @@ public override void Dispose()
public override Image CreateImage(int width, int height) =>
new Image<L8>(width, height);

protected override void DisplayImageCore(Image image, DrawOrientation orientation)
protected override void DisplayImageCore(Image image, DrawOptions options)
{
if (image is not Image<L8> img)
{
throw new ArgumentException($"{nameof(image)} is of an invalid type; {nameof(DisplayImage)} must be called with images created from {nameof(CreateImage)}.", nameof(image));
}

switch (orientation)
if (!options.HasFlag(DrawOptions.Rotate90))
{
case DrawOrientation.Default:
ConvertTo1bpp(_imageBuffer, img);
break;
case DrawOrientation.Rotate90:
ConvertTo1bppRotated(_imageBuffer, img);
break;
ConvertTo1bpp(_imageBuffer, img);
}
else
{
ConvertTo1bppRotated(_imageBuffer, img);
}

Debug.Assert(_imageBuffer.Length == 4736);

SetImage(_imageBuffer);
SetPaintMode(DisplayUpdateMode.Full); // TODO: does this need to be set every draw?
SwapFrameBuffers();
if (!options.HasFlag(DrawOptions.PartialRefresh))
{
SetImage(_imageBuffer);
SetPaintMode(DisplayUpdateMode.DisplayFull); // TODO: does this need to be set every draw?
SwapFrameBuffers();
}
else
{
Reset();
SetLUT(s_PartialLUT);
SendCommand(0x37, new byte[] { 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00 });
SendCommand(0x3C, 0x80); // BorderWavefrom
SetPaintMode(DisplayUpdateMode.BufferPartial);
SwapFrameBuffers();

SetWindow(0, 0, (uint)Width - 1, (uint)Height - 1);
SetCursor(0, 0);
SetImage(_imageBuffer);
SetPaintMode(DisplayUpdateMode.DisplayPartial);
SwapFrameBuffers();
}
}

private static void ConvertTo1bpp(Span<byte> dest, Image<L8> src)
Expand Down Expand Up @@ -162,14 +202,14 @@ private static void ConvertTo1bppRotated(Span<byte> dest, Image<L8> src)

for (int y = 0; y < srcHeight; y += 8)
{
Span<L8> row0 = src.GetPixelRowSpan(y);
Span<L8> row1 = src.GetPixelRowSpan(y + 1);
Span<L8> row2 = src.GetPixelRowSpan(y + 2);
Span<L8> row3 = src.GetPixelRowSpan(y + 3);
Span<L8> row4 = src.GetPixelRowSpan(y + 4);
Span<L8> row5 = src.GetPixelRowSpan(y + 5);
Span<L8> row6 = src.GetPixelRowSpan(y + 6);
Span<L8> row7 = src.GetPixelRowSpan(y + 7);
Span<L8> row0 = src.GetPixelRowSpan(y).Slice(0, srcWidth);
Span<L8> row1 = src.GetPixelRowSpan(y + 1).Slice(0, srcWidth);
Span<L8> row2 = src.GetPixelRowSpan(y + 2).Slice(0, srcWidth);
Span<L8> row3 = src.GetPixelRowSpan(y + 3).Slice(0, srcWidth);
Span<L8> row4 = src.GetPixelRowSpan(y + 4).Slice(0, srcWidth);
Span<L8> row5 = src.GetPixelRowSpan(y + 5).Slice(0, srcWidth);
Span<L8> row6 = src.GetPixelRowSpan(y + 6).Slice(0, srcWidth);
Span<L8> row7 = src.GetPixelRowSpan(y + 7).Slice(0, srcWidth);

int destIdx = destWidth * destHeight + y / 8;

Expand Down Expand Up @@ -207,7 +247,7 @@ private void Reset()
_gpio.Write(_rstPinId, PinValue.High);
Thread.Sleep(10);
_gpio.Write(_rstPinId, PinValue.Low);
Thread.Sleep(2);
Thread.Sleep(10);
_gpio.Write(_rstPinId, PinValue.High);
Thread.Sleep(10);
ReadBusy();
Expand Down Expand Up @@ -283,10 +323,15 @@ private void SetImage(ReadOnlySpan<byte> buffer)
private void SetDisplayUpdateControl() =>
SendCommand(0x21, new byte[] { 0x00, 0x80 });

private void SetLUTByHost(ReadOnlySpan<byte> lut)
private void SetLUT(ReadOnlySpan<byte> lut)
{
SendCommand(0x32, lut[..153]); // LUT
ReadBusy();
}

private void SetLUTByHost(ReadOnlySpan<byte> lut)
{
SetLUT(lut);

SendCommand(0x3F, lut[153..154]); // Unknown.
SendCommand(0x03, lut[154..155]); // Gate voltage.
Expand Down Expand Up @@ -330,19 +375,17 @@ private void SendCommand(byte command, ReadOnlySpan<byte> data)
/// </summary>
private void ReadBusy()
{
bool busy;
do
while (_gpio.Read(_busyPinId) == PinValue.High)
{
busy = _gpio.Read(_busyPinId) == PinValue.High;
Thread.Sleep(50);
}
while (busy);
}

private enum DisplayUpdateMode : byte
{
Full = 0xC7,
Partial = 0x0F
DisplayFull = 0xC7,
BufferPartial = 0xC0,
DisplayPartial = 0x0F
}
}
}
2 changes: 1 addition & 1 deletion src/Aether/Devices/Simulated/SimulatedDisplayDriver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ public SimulatedDisplayDriver(string imageDirectoryPath, int width, int height,
public override Image CreateImage(int width, int height) =>
new Image<SixLabors.ImageSharp.PixelFormats.Rgb24>(width, height);

protected override void DisplayImageCore(Image image, DrawOrientation orientation)
protected override void DisplayImageCore(Image image, DrawOptions options)
{
int imageId = Interlocked.Increment(ref _counter);
string filePath = string.Create(CultureInfo.InvariantCulture, $"image-{imageId}.png");
Expand Down
44 changes: 25 additions & 19 deletions src/Aether/Themes/MultiLineTheme.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,10 @@ public static IDisposable Run(DisplayDriver driver, IEnumerable<Measure> lines,
{
// setup dimensions and common measurements.

(int imgWidth, int imgHeight, float dpiX, float dpiY, DrawOrientation drawOrientation) =
(int imgWidth, int imgHeight, float dpiX, float dpiY, DrawOptions drawOptions) =
(vertical && driver.Height >= driver.Width) || (!vertical && driver.Width >= driver.Height)
? (driver.Width, driver.Height, driver.DpiX, driver.DpiY, DrawOrientation.Default)
: (driver.Height, driver.Width, driver.DpiY, driver.DpiX, DrawOrientation.Rotate90);
? (driver.Width, driver.Height, driver.DpiX, driver.DpiY, DrawOptions.None)
: (driver.Height, driver.Width, driver.DpiY, driver.DpiX, DrawOptions.Rotate90);

int outerMarginInPixelsX = (int)MathF.Ceiling(OuterMarginInInches * dpiX);
int outerMarginInPixelsY = (int)MathF.Ceiling(OuterMarginInInches * dpiY);
Expand Down Expand Up @@ -149,12 +149,29 @@ public static IDisposable Run(DisplayDriver driver, IEnumerable<Measure> lines,
}
});

driver.DisplayImage(image, drawOrientation);
driver.DisplayImage(image, drawOptions);

drawOptions |= DrawOptions.PartialRefresh;

// wire up against the source.
// TODO: abstract and localize stringy bits.

var seen = new HashSet<Measure>();
Point location = default;
string text;

void Draw(IImageProcessingContext ctx)
{
ctx.Fill(Color.White, new RectangleF(
location.X - measurementWidth,
location.Y - lineHeight,
measurementWidth,
lineHeight
));

// TODO: it's possible super large values (PM2.5 seems to have this issue) will overwrite the labels.
ctx.DrawText(measurementDrawingOptions, text, measurementFont, Color.Black, location);
}

return source.Gate().Subscribe(measurements =>
{
Expand All @@ -164,14 +181,14 @@ public static IDisposable Run(DisplayDriver driver, IEnumerable<Measure> lines,
{
Measurement measurement = measurements[i];

if (!seen.Add(measurement.Measure) || !offsets.TryGetValue(measurement.Measure, out Point location))
if (!seen.Add(measurement.Measure) || !offsets.TryGetValue(measurement.Measure, out location))
{
continue;
}

draw = true;

string text = measurement.Measure switch
text = measurement.Measure switch
{
Measure.Humidity => (measurement.RelativeHumidity.Value * (1.0 / 100.0)).ToString("P0"),
Measure.Temperature => measurement.Temperature.DegreesFahrenheit.ToString("N1"),
Expand All @@ -184,25 +201,14 @@ public static IDisposable Run(DisplayDriver driver, IEnumerable<Measure> lines,
_ => throw new Exception($"Unsupported measure '{measurement.Measure}'.")
};

image.Mutate(ctx =>
{
ctx.Fill(Color.White, new RectangleF(
location.X - measurementWidth,
location.Y - lineHeight,
measurementWidth,
lineHeight
));

// TODO: it's possible super large values (PM2.5 seems to have this issue) will overwrite the labels.
ctx.DrawText(measurementDrawingOptions, text, measurementFont, Color.Black, location);
});
image.Mutate(Draw);
}

seen.Clear();

if (draw)
{
driver.DisplayImage(image, drawOrientation);
driver.DisplayImage(image, drawOptions);
}
});
}
Expand Down