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); } }); }