diff --git a/src/Aether/Devices/Drivers/BufferedDisplayDriver.cs b/src/Aether/Devices/Drivers/BufferedDisplayDriver.cs
index 094cee8..42ba897 100644
--- a/src/Aether/Devices/Drivers/BufferedDisplayDriver.cs
+++ b/src/Aether/Devices/Drivers/BufferedDisplayDriver.cs
@@ -22,38 +22,35 @@ protected BufferedDisplayDriver(int width, int height, float dpiX, float dpiY)
///
/// Draws an image onto the pixel buffer.
///
- /// The to draw onto the pixel buffer. Must have been created via on this instance.
+ /// The to draw onto the pixel buffer. Must have been created via on this instance.
/// The X position to draw the image at.
/// The Y position to draw the image at.
- /// The orientation to draw the image in.
- public void DrawImage(Image srcImage, int fillPositionX, int fillPositionY, DrawOrientation orientation = DrawOrientation.Default)
+ /// Options controlling how the image is drawn.
+ 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);
}
- ///
- protected abstract void DrawImageCore(Image srcImage, int fillPositionX, int fillPositionY, DrawOrientation orientation);
+ ///
+ protected abstract void DrawImageCore(Image srcImage, int fillPositionX, int fillPositionY, DrawOptions options);
///
/// Flushes the pixel buffer to the device.
///
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();
}
}
diff --git a/src/Aether/Devices/Drivers/DisplayDriver.cs b/src/Aether/Devices/Drivers/DisplayDriver.cs
index 2d02c34..b0639eb 100644
--- a/src/Aether/Devices/Drivers/DisplayDriver.cs
+++ b/src/Aether/Devices/Drivers/DisplayDriver.cs
@@ -52,28 +52,25 @@ protected DisplayDriver(int width, int height, float dpiX, float dpiY)
/// Displays an image to the device.
///
/// The image to display. Must have been created via on this .
- /// The orientation to display the image in
+ /// Options controlling how the image is displayed.
///
///
- 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);
}
- ///
- protected abstract void DisplayImageCore(Image image, DrawOrientation orientation);
+ ///
+ protected abstract void DisplayImageCore(Image image, DrawOptions options);
public abstract void Dispose();
}
diff --git a/src/Aether/Devices/Drivers/DrawOptions.cs b/src/Aether/Devices/Drivers/DrawOptions.cs
new file mode 100644
index 0000000..af5387d
--- /dev/null
+++ b/src/Aether/Devices/Drivers/DrawOptions.cs
@@ -0,0 +1,24 @@
+namespace Aether.Devices.Drivers
+{
+ ///
+ /// Options for drawing to a .
+ ///
+ [Flags]
+ public enum DrawOptions
+ {
+ ///
+ /// Default draw behavior.
+ ///
+ None,
+
+ ///
+ /// If supported, perform a refresh that trades quality for improved speed.
+ ///
+ PartialRefresh = 1,
+
+ ///
+ /// Rotates the image counter-clockwise by 90 degrees.
+ ///
+ Rotate90 = 2,
+ }
+}
diff --git a/src/Aether/Devices/Drivers/DrawOrientation.cs b/src/Aether/Devices/Drivers/DrawOrientation.cs
deleted file mode 100644
index 397dd90..0000000
--- a/src/Aether/Devices/Drivers/DrawOrientation.cs
+++ /dev/null
@@ -1,8 +0,0 @@
-namespace Aether.Devices.Drivers
-{
- public enum DrawOrientation
- {
- Default,
- Rotate90
- }
-}
diff --git a/src/Aether/Devices/Drivers/Sk9822.cs b/src/Aether/Devices/Drivers/Sk9822.cs
index a3dc365..d8a7072 100644
--- a/src/Aether/Devices/Drivers/Sk9822.cs
+++ b/src/Aether/Devices/Drivers/Sk9822.cs
@@ -85,7 +85,7 @@ public override void Flush() =>
public override Image CreateImage(int width, int height) =>
new Image(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 img)
{
diff --git a/src/Aether/Devices/Drivers/WaveshareEPD2_9inV2.cs b/src/Aether/Devices/Drivers/WaveshareEPD2_9inV2.cs
index 6d0c733..a74002c 100644
--- a/src/Aether/Devices/Drivers/WaveshareEPD2_9inV2.cs
+++ b/src/Aether/Devices/Drivers/WaveshareEPD2_9inV2.cs
@@ -37,6 +37,29 @@ internal sealed class WaveshareEPD2_9inV2 : DisplayDriver
0x22, 0x17, 0x41, 0x0, 0x32, 0x36
};
+ private static ReadOnlySpan 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;
@@ -101,28 +124,45 @@ public override void Dispose()
public override Image CreateImage(int width, int height) =>
new Image(width, height);
- protected override void DisplayImageCore(Image image, DrawOrientation orientation)
+ protected override void DisplayImageCore(Image image, DrawOptions options)
{
if (image is not Image 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 dest, Image src)
@@ -162,14 +202,14 @@ private static void ConvertTo1bppRotated(Span dest, Image src)
for (int y = 0; y < srcHeight; y += 8)
{
- Span row0 = src.GetPixelRowSpan(y);
- Span row1 = src.GetPixelRowSpan(y + 1);
- Span row2 = src.GetPixelRowSpan(y + 2);
- Span row3 = src.GetPixelRowSpan(y + 3);
- Span row4 = src.GetPixelRowSpan(y + 4);
- Span row5 = src.GetPixelRowSpan(y + 5);
- Span row6 = src.GetPixelRowSpan(y + 6);
- Span row7 = src.GetPixelRowSpan(y + 7);
+ Span row0 = src.GetPixelRowSpan(y).Slice(0, srcWidth);
+ Span row1 = src.GetPixelRowSpan(y + 1).Slice(0, srcWidth);
+ Span row2 = src.GetPixelRowSpan(y + 2).Slice(0, srcWidth);
+ Span row3 = src.GetPixelRowSpan(y + 3).Slice(0, srcWidth);
+ Span row4 = src.GetPixelRowSpan(y + 4).Slice(0, srcWidth);
+ Span row5 = src.GetPixelRowSpan(y + 5).Slice(0, srcWidth);
+ Span row6 = src.GetPixelRowSpan(y + 6).Slice(0, srcWidth);
+ Span row7 = src.GetPixelRowSpan(y + 7).Slice(0, srcWidth);
int destIdx = destWidth * destHeight + y / 8;
@@ -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();
@@ -283,10 +323,15 @@ private void SetImage(ReadOnlySpan buffer)
private void SetDisplayUpdateControl() =>
SendCommand(0x21, new byte[] { 0x00, 0x80 });
- private void SetLUTByHost(ReadOnlySpan lut)
+ private void SetLUT(ReadOnlySpan lut)
{
SendCommand(0x32, lut[..153]); // LUT
ReadBusy();
+ }
+
+ private void SetLUTByHost(ReadOnlySpan lut)
+ {
+ SetLUT(lut);
SendCommand(0x3F, lut[153..154]); // Unknown.
SendCommand(0x03, lut[154..155]); // Gate voltage.
@@ -330,19 +375,17 @@ private void SendCommand(byte command, ReadOnlySpan data)
///
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
}
}
}
diff --git a/src/Aether/Devices/Simulated/SimulatedDisplayDriver.cs b/src/Aether/Devices/Simulated/SimulatedDisplayDriver.cs
index 53c52eb..19e5232 100644
--- a/src/Aether/Devices/Simulated/SimulatedDisplayDriver.cs
+++ b/src/Aether/Devices/Simulated/SimulatedDisplayDriver.cs
@@ -18,7 +18,7 @@ public SimulatedDisplayDriver(string imageDirectoryPath, int width, int height,
public override Image CreateImage(int width, int height) =>
new Image(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");
diff --git a/src/Aether/Themes/MultiLineTheme.cs b/src/Aether/Themes/MultiLineTheme.cs
index 2f525f5..44aa409 100644
--- a/src/Aether/Themes/MultiLineTheme.cs
+++ b/src/Aether/Themes/MultiLineTheme.cs
@@ -20,10 +20,10 @@ public static IDisposable Run(DisplayDriver driver, IEnumerable 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);
@@ -149,12 +149,29 @@ public static IDisposable Run(DisplayDriver driver, IEnumerable 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();
+ 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 =>
{
@@ -164,14 +181,14 @@ public static IDisposable Run(DisplayDriver driver, IEnumerable 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"),
@@ -184,25 +201,14 @@ public static IDisposable Run(DisplayDriver driver, IEnumerable 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);
}
});
}