diff --git a/src/Iot.Device.Bindings/CompatibilitySuppressions.xml b/src/Iot.Device.Bindings/CompatibilitySuppressions.xml index 1c4c65ce8c..36073fb0ce 100644 --- a/src/Iot.Device.Bindings/CompatibilitySuppressions.xml +++ b/src/Iot.Device.Bindings/CompatibilitySuppressions.xml @@ -78,6 +78,13 @@ lib/net6.0/Iot.Device.Bindings.dll true + + CP0002 + F:Iot.Device.Ssd13xx.Ssd13xx._i2cDevice + lib/net6.0/Iot.Device.Bindings.dll + lib/net6.0/Iot.Device.Bindings.dll + true + CP0002 M:Iot.Device.Axp192.Axp192.SetGPIO0(Iot.Device.Axp192.Gpio0Behavior,System.Byte) @@ -225,6 +232,13 @@ lib/netstandard2.0/Iot.Device.Bindings.dll true + + CP0002 + F:Iot.Device.Ssd13xx.Ssd13xx._i2cDevice + lib/netstandard2.0/Iot.Device.Bindings.dll + lib/netstandard2.0/Iot.Device.Bindings.dll + true + CP0002 M:Iot.Device.Axp192.Axp192.SetGPIO0(Iot.Device.Axp192.Gpio0Behavior,System.Byte) diff --git a/src/devices/Ssd13xx/Commands/ISsd1309Command.cs b/src/devices/Ssd13xx/Commands/ISsd1309Command.cs new file mode 100644 index 0000000000..f983a58ac0 --- /dev/null +++ b/src/devices/Ssd13xx/Commands/ISsd1309Command.cs @@ -0,0 +1,12 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Iot.Device.Ssd13xx.Commands +{ + /// + /// Interface for all Ssd1309 commands + /// + public interface ISsd1309Command : ICommand + { + } +} diff --git a/src/devices/Ssd13xx/Ssd1306.cs b/src/devices/Ssd13xx/Ssd1306.cs index 54dfc5252e..45180ec37c 100644 --- a/src/devices/Ssd13xx/Ssd1306.cs +++ b/src/devices/Ssd13xx/Ssd1306.cs @@ -60,7 +60,7 @@ private void SendCommand(ICommand command) // Be aware there is a Continuation Bit in the Control byte and can be used // to state (logic LOW) if there is only data bytes to follow. // This binding separates commands and data by using SendCommand and SendData. - _i2cDevice.Write(writeBuffer); + I2cDevice?.Write(writeBuffer); } // Display size 128x32 or 128x64 diff --git a/src/devices/Ssd13xx/Ssd1309.cs b/src/devices/Ssd13xx/Ssd1309.cs new file mode 100644 index 0000000000..82dec53b30 --- /dev/null +++ b/src/devices/Ssd13xx/Ssd1309.cs @@ -0,0 +1,227 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Device.Gpio; +using System.Device.Spi; +using System.Linq; +using System.Threading; +using Iot.Device.Graphics; +using Iot.Device.Ssd13xx.Commands; +using Iot.Device.Ssd13xx.Commands.Ssd1306Commands; + +namespace Iot.Device.Ssd13xx +{ + /// + /// A single-chip CMOS OLED/PLED driver with controller for organic/polymer + /// light emitting diode dot-matrix graphic display system. + /// + public class Ssd1309 : Ssd13xx + { + private readonly GpioPin _csGpioPin; + private readonly GpioPin _dcGpioPin; + private readonly GpioPin _rstGpioPin; + + private GpioController _gpioController; + + /// + /// Initializes new instance of Ssd13069 device that will communicate using SPI + /// in a non-traditional "4-wire SPI" mode that uses DC and RST GPIO pins to switch + /// between Data/Command instructions and reset behavior. + /// A single-chip CMOS OLED/PLED driver with controller for organic/polymer + /// light emitting diode dot-matrix graphic display system. + /// + /// The SPI device used for communication. + /// Width of the display. Typically 128 pixels + /// Height of the display + /// Instance of the boards GpioController + /// GPIO pin for chip-select. Active state is LOW. Does not guarantee support for multiple SPI bus devices. + /// GPIO pin for Data/Command control + /// GPIO pin for Reset behavior + public Ssd1309(SpiDevice spiDevice, GpioController gpioController, int csGpioPin, int dcGpioPin, int rstGpioPin, int width, int height) + : base(spiDevice, width, height) + { + _gpioController = gpioController; + + _csGpioPin = _gpioController.OpenPin(csGpioPin, PinMode.Output, PinValue.High); + _dcGpioPin = _gpioController.OpenPin(dcGpioPin, PinMode.Output, PinValue.Low); + _rstGpioPin = _gpioController.OpenPin(rstGpioPin, PinMode.Output, PinValue.High); + + Reset(); + Initialize(); + } + + /// + /// Sends command to the device + /// + /// Command being sent + public virtual void SendCommand(ISsd1309Command command) => SendCommand((ICommand)command); + + /// + /// Sends command to the device + /// + /// Command being sent + public override void SendCommand(ISharedCommand command) => SendCommand(command); + + /// + /// Send a command to the display controller. + /// + /// The command to send to the display controller. + private void SendCommand(ICommand command) + { + Span commandBytes = command?.GetBytes(); + + if (commandBytes is not { Length: >0 }) + { + throw new ArgumentNullException(nameof(command), "Argument is either null or there were no bytes to send."); + } + + // TODO: Is this needed at all if data/command is controlled via GPIO instead of a control byte? + Span writeBuffer = SliceGenericBuffer(commandBytes.Length); + + commandBytes.CopyTo(writeBuffer.Slice(0)); + + if (SpiDevice != null) + { + // Begin consuming SPI bus data + // Because the timing is not perfect, this may have adverse side-effects when using multiple SPI bus devices + _csGpioPin.Write(PinValue.Low); + + // Enable command mode + _dcGpioPin.Write(PinValue.Low); + SpiDevice.Write(writeBuffer); + + // Stop consuming SPI bus data + _csGpioPin.Write(PinValue.High); + } + else + { + throw new InvalidOperationException("No SPI device available or it has been disposed."); + } + } + + /// + /// Sends data to the device + /// + /// Data being sent + public override void SendData(Span data) + { + if (data.IsEmpty) + { + throw new ArgumentNullException(nameof(data)); + } + + if (SpiDevice != null) + { + // Begin consuming SPI bus data + // Because the timing is not perfect, this may have adverse side-effects when using multiple SPI bus devices + _csGpioPin.Write(PinValue.Low); + + // Enable data mode + _dcGpioPin.Write(PinValue.High); + SpiDevice.Write(data); + + // Stop consuming SPI bus data + _csGpioPin.Write(PinValue.High); + } + else + { + throw new InvalidOperationException("No SPI device available or it has been disposed."); + } + } + + /// + /// Init 128x64 + /// + protected virtual void Initialize() + { + SendCommand(new SetDisplayOff()); + SendCommand(new SetDisplayClockDivideRatioOscillatorFrequency()); + SendCommand(new SetMultiplexRatio()); + SendCommand(new SetDisplayOffset()); + SendCommand(new SetDisplayStartLine()); + SendCommand(new SetMemoryAddressingMode(SetMemoryAddressingMode.AddressingMode.Horizontal)); + SetStartAddress(); + SendCommand(new SetSegmentReMap(true)); + SendCommand(new SetComOutputScanDirection(false)); + SendCommand(new SetComPinsHardwareConfiguration()); + SendCommand(new SetContrastControlForBank0()); + SendCommand(new SetPreChargePeriod()); + SendCommand(new SetVcomhDeselectLevel()); + SendCommand(new EntireDisplayOn(false)); + SendCommand(new SetNormalDisplay()); + SendCommand(new SetDisplayOn()); + Thread.Sleep(200); + ClearScreen(); + } + + /// + /// Set the start address for the display + /// + protected override void SetStartAddress() + { + SendCommand(new SetColumnAddress()); + + if (ScreenHeight == 32) + { + SendCommand(new SetPageAddress(PageAddress.Page0, PageAddress.Page3)); + } + else + { + SendCommand(new SetPageAddress()); + } + } + + /// + /// Reset device by cycling RST pin to LOW and resting at HIGH. + /// This will not re-initialize the device. + /// + protected virtual void Reset() + { + _rstGpioPin.Write(PinValue.Low); + Thread.Sleep(100); + _rstGpioPin.Write(PinValue.High); + Thread.Sleep(100); + } + + /// + /// Sends the image to the display + /// + /// Image to send to display + public override void DrawBitmap(BitmapImage image) + { + if (!CanConvertFromPixelFormat(image.PixelFormat)) + { + throw new InvalidOperationException($"{image.PixelFormat} is not a supported pixel format"); + } + + SetStartAddress(); + + int width = ScreenWidth; + int pages = image.Height / 8; + List buffer = new(); + + for (int page = 0; page < pages; page++) + { + for (int x = 0; x < width; x++) + { + int bits = 0; + for (byte bit = 0; bit < 8; bit++) + { + bits = bits << 1; + bits |= image[x, page * 8 + 7 - bit].GetBrightness() > BrightnessThreshold ? 1 : 0; + } + + buffer.Add((byte)bits); + } + } + + int chunk_size = 16; + for (int i = 0; i < buffer.Count; i += chunk_size) + { + SendData(buffer.Skip(i).Take(chunk_size).ToArray()); + } + } + } +} diff --git a/src/devices/Ssd13xx/Ssd1327.cs b/src/devices/Ssd13xx/Ssd1327.cs index fab557e4ec..ae237b71b5 100644 --- a/src/devices/Ssd13xx/Ssd1327.cs +++ b/src/devices/Ssd13xx/Ssd1327.cs @@ -95,8 +95,7 @@ private void Initialize() public void SendCommand(byte command) { Span writeBuffer = new byte[] { Command_Mode, command }; - - _i2cDevice.Write(writeBuffer); + I2cDevice?.Write(writeBuffer); } /// diff --git a/src/devices/Ssd13xx/Ssd13xx.cs b/src/devices/Ssd13xx/Ssd13xx.cs index 1105e923d6..1d961f5fcb 100644 --- a/src/devices/Ssd13xx/Ssd13xx.cs +++ b/src/devices/Ssd13xx/Ssd13xx.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Device.I2c; +using System.Device.Spi; using System.Linq; using Iot.Device.Graphics; using Iot.Device.Ssd13xx.Commands; @@ -23,10 +24,15 @@ public abstract class Ssd13xx : GraphicDisplay /// /// Underlying I2C device /// - protected I2cDevice _i2cDevice; + protected I2cDevice? I2cDevice { get; set; } /// - /// Constructs instance of Ssd13xx + /// Underlying SPI device + /// + protected SpiDevice? SpiDevice { get; set; } + + /// + /// Constructs instance of Ssd13xx using an I2C device /// /// I2C device used to communicate with the device /// Width of the display, in pixels @@ -37,7 +43,22 @@ protected Ssd13xx(I2cDevice i2cDevice, int width, int height) ScreenHeight = height; ScreenWidth = width; BrightnessThreshold = DefaultThreshold; - _i2cDevice = i2cDevice ?? throw new ArgumentNullException(nameof(i2cDevice)); + I2cDevice = i2cDevice ?? throw new ArgumentNullException(nameof(i2cDevice)); + } + + /// + /// Constructs instance of Ssd13xx using an SPI device + /// + /// SPI device used to communicate with the device + /// Width of the display, in pixels + /// Height of the display, in pixels + protected Ssd13xx(SpiDevice spiDevice, int width, int height) + { + _genericBuffer = new byte[DefaultBufferSize]; + ScreenHeight = height; + ScreenWidth = width; + BrightnessThreshold = DefaultThreshold; + SpiDevice = spiDevice ?? throw new ArgumentNullException(nameof(spiDevice)); } /// @@ -107,7 +128,19 @@ public virtual void SendData(Span data) writeBuffer[0] = 0x40; // Control byte. data.CopyTo(writeBuffer.Slice(1)); - _i2cDevice.Write(writeBuffer); + + if (I2cDevice != null) + { + I2cDevice.Write(writeBuffer); + } + else if (SpiDevice != null) + { + SpiDevice.Write(writeBuffer); + } + else + { + throw new InvalidOperationException("No I2C/SPI device available or it has been disposed."); + } } /// @@ -115,8 +148,11 @@ protected override void Dispose(bool disposing) { if (disposing) { - _i2cDevice?.Dispose(); - _i2cDevice = null!; + I2cDevice?.Dispose(); + I2cDevice = null!; + + SpiDevice?.Dispose(); + SpiDevice = null!; } } @@ -206,5 +242,38 @@ public override void DrawBitmap(BitmapImage image) SendData(buffer.Skip(i).Take(chunk_size).ToArray()); } } + + /// + /// Returns the display-ready span of bytes for a bitmap without sending the data to the display + /// + /// Image to render + public virtual byte[] PreRenderBitmap(BitmapImage image) + { + if (!CanConvertFromPixelFormat(image.PixelFormat)) + { + throw new InvalidOperationException($"{image.PixelFormat} is not a supported pixel format"); + } + + int width = ScreenWidth; + int pages = image.Height / 8; + List buffer = new(); + + for (int page = 0; page < pages; page++) + { + for (int x = 0; x < width; x++) + { + int bits = 0; + for (byte bit = 0; bit < 8; bit++) + { + bits = bits << 1; + bits |= image[x, page * 8 + 7 - bit].GetBrightness() > BrightnessThreshold ? 1 : 0; + } + + buffer.Add((byte)bits); + } + } + + return buffer.ToArray(); + } } } diff --git a/src/devices/Ssd13xx/Ssd13xx.csproj b/src/devices/Ssd13xx/Ssd13xx.csproj index d52aaa67c5..1122fff0f3 100644 --- a/src/devices/Ssd13xx/Ssd13xx.csproj +++ b/src/devices/Ssd13xx/Ssd13xx.csproj @@ -8,6 +8,7 @@ + diff --git a/src/devices/Ssd13xx/Ssd13xx.sln b/src/devices/Ssd13xx/Ssd13xx.sln index 27d2007b0a..dc55226559 100644 --- a/src/devices/Ssd13xx/Ssd13xx.sln +++ b/src/devices/Ssd13xx/Ssd13xx.sln @@ -9,8 +9,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ssd13xx.Tests", "tests\Ssd1 EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{DA6C1243-517A-4827-A7B0-05DDCEFA88A5}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ssd13xx.Samples", "samples\Ssd13xx.Samples.csproj", "{26A1466F-AAD0-4DCF-AB48-061ED86EA645}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ssd13xx", "Ssd13xx.csproj", "{CDC920A8-123F-432D-834E-49BAB00191E7}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Arduino", "..\Arduino\Arduino.csproj", "{9531CD82-A4B7-4F7A-A220-E2E004A6F0E9}" @@ -19,6 +17,14 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SkiaSharpAdapter", "..\Skia EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Common", "..\Common\Common.csproj", "{F067B019-474D-442E-B7A8-A87F59B46F41}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "i2c", "i2c", "{D1B22546-FB0C-498F-B637-449A3F9DC347}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "spi", "spi", "{C7302976-43EC-49DE-88F9-F2227C7F18B9}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ssd13xx.Samples", "samples\i2c\Ssd13xx.Samples\Ssd13xx.Samples.csproj", "{1BE0775E-A211-4D00-9016-C2825B065519}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ssd1309.Spi.Samples", "samples\spi\Ssd1309.Spi.Samples\Ssd1309.Spi.Samples.csproj", "{FF639AE0-9057-4FC5-AF8B-956368B20EBE}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -41,18 +47,6 @@ Global {0C3BE886-C62F-41CA-BD18-1CECFE591589}.Release|x64.Build.0 = Release|Any CPU {0C3BE886-C62F-41CA-BD18-1CECFE591589}.Release|x86.ActiveCfg = Release|Any CPU {0C3BE886-C62F-41CA-BD18-1CECFE591589}.Release|x86.Build.0 = Release|Any CPU - {26A1466F-AAD0-4DCF-AB48-061ED86EA645}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {26A1466F-AAD0-4DCF-AB48-061ED86EA645}.Debug|Any CPU.Build.0 = Debug|Any CPU - {26A1466F-AAD0-4DCF-AB48-061ED86EA645}.Debug|x64.ActiveCfg = Debug|Any CPU - {26A1466F-AAD0-4DCF-AB48-061ED86EA645}.Debug|x64.Build.0 = Debug|Any CPU - {26A1466F-AAD0-4DCF-AB48-061ED86EA645}.Debug|x86.ActiveCfg = Debug|Any CPU - {26A1466F-AAD0-4DCF-AB48-061ED86EA645}.Debug|x86.Build.0 = Debug|Any CPU - {26A1466F-AAD0-4DCF-AB48-061ED86EA645}.Release|Any CPU.ActiveCfg = Release|Any CPU - {26A1466F-AAD0-4DCF-AB48-061ED86EA645}.Release|Any CPU.Build.0 = Release|Any CPU - {26A1466F-AAD0-4DCF-AB48-061ED86EA645}.Release|x64.ActiveCfg = Release|Any CPU - {26A1466F-AAD0-4DCF-AB48-061ED86EA645}.Release|x64.Build.0 = Release|Any CPU - {26A1466F-AAD0-4DCF-AB48-061ED86EA645}.Release|x86.ActiveCfg = Release|Any CPU - {26A1466F-AAD0-4DCF-AB48-061ED86EA645}.Release|x86.Build.0 = Release|Any CPU {CDC920A8-123F-432D-834E-49BAB00191E7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {CDC920A8-123F-432D-834E-49BAB00191E7}.Debug|Any CPU.Build.0 = Debug|Any CPU {CDC920A8-123F-432D-834E-49BAB00191E7}.Debug|x64.ActiveCfg = Debug|Any CPU @@ -101,13 +95,40 @@ Global {F067B019-474D-442E-B7A8-A87F59B46F41}.Release|x64.Build.0 = Release|Any CPU {F067B019-474D-442E-B7A8-A87F59B46F41}.Release|x86.ActiveCfg = Release|Any CPU {F067B019-474D-442E-B7A8-A87F59B46F41}.Release|x86.Build.0 = Release|Any CPU + {1BE0775E-A211-4D00-9016-C2825B065519}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1BE0775E-A211-4D00-9016-C2825B065519}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1BE0775E-A211-4D00-9016-C2825B065519}.Debug|x64.ActiveCfg = Debug|Any CPU + {1BE0775E-A211-4D00-9016-C2825B065519}.Debug|x64.Build.0 = Debug|Any CPU + {1BE0775E-A211-4D00-9016-C2825B065519}.Debug|x86.ActiveCfg = Debug|Any CPU + {1BE0775E-A211-4D00-9016-C2825B065519}.Debug|x86.Build.0 = Debug|Any CPU + {1BE0775E-A211-4D00-9016-C2825B065519}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1BE0775E-A211-4D00-9016-C2825B065519}.Release|Any CPU.Build.0 = Release|Any CPU + {1BE0775E-A211-4D00-9016-C2825B065519}.Release|x64.ActiveCfg = Release|Any CPU + {1BE0775E-A211-4D00-9016-C2825B065519}.Release|x64.Build.0 = Release|Any CPU + {1BE0775E-A211-4D00-9016-C2825B065519}.Release|x86.ActiveCfg = Release|Any CPU + {1BE0775E-A211-4D00-9016-C2825B065519}.Release|x86.Build.0 = Release|Any CPU + {FF639AE0-9057-4FC5-AF8B-956368B20EBE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FF639AE0-9057-4FC5-AF8B-956368B20EBE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FF639AE0-9057-4FC5-AF8B-956368B20EBE}.Debug|x64.ActiveCfg = Debug|Any CPU + {FF639AE0-9057-4FC5-AF8B-956368B20EBE}.Debug|x64.Build.0 = Debug|Any CPU + {FF639AE0-9057-4FC5-AF8B-956368B20EBE}.Debug|x86.ActiveCfg = Debug|Any CPU + {FF639AE0-9057-4FC5-AF8B-956368B20EBE}.Debug|x86.Build.0 = Debug|Any CPU + {FF639AE0-9057-4FC5-AF8B-956368B20EBE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FF639AE0-9057-4FC5-AF8B-956368B20EBE}.Release|Any CPU.Build.0 = Release|Any CPU + {FF639AE0-9057-4FC5-AF8B-956368B20EBE}.Release|x64.ActiveCfg = Release|Any CPU + {FF639AE0-9057-4FC5-AF8B-956368B20EBE}.Release|x64.Build.0 = Release|Any CPU + {FF639AE0-9057-4FC5-AF8B-956368B20EBE}.Release|x86.ActiveCfg = Release|Any CPU + {FF639AE0-9057-4FC5-AF8B-956368B20EBE}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection GlobalSection(NestedProjects) = preSolution {0C3BE886-C62F-41CA-BD18-1CECFE591589} = {59ED0E9C-95C8-469A-9EA8-0BC0624806A9} - {26A1466F-AAD0-4DCF-AB48-061ED86EA645} = {DA6C1243-517A-4827-A7B0-05DDCEFA88A5} + {D1B22546-FB0C-498F-B637-449A3F9DC347} = {DA6C1243-517A-4827-A7B0-05DDCEFA88A5} + {C7302976-43EC-49DE-88F9-F2227C7F18B9} = {DA6C1243-517A-4827-A7B0-05DDCEFA88A5} + {1BE0775E-A211-4D00-9016-C2825B065519} = {D1B22546-FB0C-498F-B637-449A3F9DC347} + {FF639AE0-9057-4FC5-AF8B-956368B20EBE} = {C7302976-43EC-49DE-88F9-F2227C7F18B9} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {4AE79F3F-25E2-4C61-BE07-B2D2169C7000} diff --git a/src/devices/Ssd13xx/samples/Program.cs b/src/devices/Ssd13xx/samples/i2c/Ssd13xx.Samples/Program.cs similarity index 100% rename from src/devices/Ssd13xx/samples/Program.cs rename to src/devices/Ssd13xx/samples/i2c/Ssd13xx.Samples/Program.cs diff --git a/src/devices/Ssd13xx/samples/Ssd13xx.Samples.csproj b/src/devices/Ssd13xx/samples/i2c/Ssd13xx.Samples/Ssd13xx.Samples.csproj similarity index 61% rename from src/devices/Ssd13xx/samples/Ssd13xx.Samples.csproj rename to src/devices/Ssd13xx/samples/i2c/Ssd13xx.Samples/Ssd13xx.Samples.csproj index 17d6950378..0814656b87 100644 --- a/src/devices/Ssd13xx/samples/Ssd13xx.Samples.csproj +++ b/src/devices/Ssd13xx/samples/i2c/Ssd13xx.Samples/Ssd13xx.Samples.csproj @@ -5,9 +5,9 @@ - - - + + + diff --git a/src/devices/Ssd13xx/samples/images/dotnet-bot.bmp b/src/devices/Ssd13xx/samples/i2c/Ssd13xx.Samples/images/dotnet-bot.bmp similarity index 100% rename from src/devices/Ssd13xx/samples/images/dotnet-bot.bmp rename to src/devices/Ssd13xx/samples/i2c/Ssd13xx.Samples/images/dotnet-bot.bmp diff --git a/src/devices/Ssd13xx/samples/images/github-dotnet-iot-black.bmp b/src/devices/Ssd13xx/samples/i2c/Ssd13xx.Samples/images/github-dotnet-iot-black.bmp similarity index 100% rename from src/devices/Ssd13xx/samples/images/github-dotnet-iot-black.bmp rename to src/devices/Ssd13xx/samples/i2c/Ssd13xx.Samples/images/github-dotnet-iot-black.bmp diff --git a/src/devices/Ssd13xx/samples/images/github-dotnet-iot-white.bmp b/src/devices/Ssd13xx/samples/i2c/Ssd13xx.Samples/images/github-dotnet-iot-white.bmp similarity index 100% rename from src/devices/Ssd13xx/samples/images/github-dotnet-iot-white.bmp rename to src/devices/Ssd13xx/samples/i2c/Ssd13xx.Samples/images/github-dotnet-iot-white.bmp diff --git a/src/devices/Ssd13xx/samples/images/shapes.bmp b/src/devices/Ssd13xx/samples/i2c/Ssd13xx.Samples/images/shapes.bmp similarity index 100% rename from src/devices/Ssd13xx/samples/images/shapes.bmp rename to src/devices/Ssd13xx/samples/i2c/Ssd13xx.Samples/images/shapes.bmp diff --git a/src/devices/Ssd13xx/samples/spi/Ssd1309.Spi.Samples/Program.cs b/src/devices/Ssd13xx/samples/spi/Ssd1309.Spi.Samples/Program.cs new file mode 100644 index 0000000000..a55f3b1008 --- /dev/null +++ b/src/devices/Ssd13xx/samples/spi/Ssd1309.Spi.Samples/Program.cs @@ -0,0 +1,32 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Device.Spi; +using System.Threading; +using Iot.Device.Board; +using Iot.Device.Graphics.SkiaSharpAdapter; +using Iot.Device.Ssd13xx; +using Ssd13xx.Samples.Simulations; + +SkiaSharpAdapter.Register(); + +var spiSettings = new SpiConnectionSettings(0) +{ + ClockFrequency = 8_000_000 +}; + +var board = new RaspberryPiBoard(); +var spiDevice = board.CreateSpiDevice(spiSettings); +var gpioController = board.CreateGpioController(); + +var display = new Ssd1309(spiDevice, gpioController, csGpioPin: 8, dcGpioPin: 25, rstGpioPin: 27, width: 128, height: 64); + +var simulation = new FallingSandSimulation(display, fps: 30, debug: true); +await simulation.StartAsync(iterations: 5000, initialDelayMs: 500); + +display.ClearScreen(); + +gpioController.Dispose(); +spiDevice.Dispose(); +display.Dispose(); +board.Dispose(); diff --git a/src/devices/Ssd13xx/samples/spi/Ssd1309.Spi.Samples/Scripts/deployToPi.ps1 b/src/devices/Ssd13xx/samples/spi/Ssd1309.Spi.Samples/Scripts/deployToPi.ps1 new file mode 100644 index 0000000000..a7fd3001f0 --- /dev/null +++ b/src/devices/Ssd13xx/samples/spi/Ssd1309.Spi.Samples/Scripts/deployToPi.ps1 @@ -0,0 +1,26 @@ +# Licensed to the .NET Foundation under one or more agreements. +# The .NET Foundation licenses this file to you under the MIT license. + +# Uses SCP to copy local output files to a RaspberryPi on the same network +# This assumes development on a Windows machine. For Linux, consider rsync instead. +# Replace the variables below with your desired values: + +$PiUser = "pi" +$PiHostnameOrIp = "pi" +$TargetRuntime = "linux-arm64" +$PublishDirectory = "C:\ssd1309Sample" +$RemoteDirectory = "/home/${PiUser}/ssd1309sample" + +# Publish +dotnet publish "../Ssd1309.Spi.Samples.csproj" --self-contained -c Debug -o $PublishDirectory -r $TargetRuntime + +# Deploy +if ($?) +{ + Write-Host "Deploying files from ${PublishDirectory} to ${PiUser}@${PiHostnameOrIp}" + scp -r $PublishDirectory ${PiUser}@${PiHostnameOrIp}:${RemoteDirectory} +} +else +{ + Write-Host "Publish not successful. Skipping deployment." +} diff --git a/src/devices/Ssd13xx/samples/spi/Ssd1309.Spi.Samples/Simulations/FallingSandSimulation.cs b/src/devices/Ssd13xx/samples/spi/Ssd1309.Spi.Samples/Simulations/FallingSandSimulation.cs new file mode 100644 index 0000000000..e75c98249f --- /dev/null +++ b/src/devices/Ssd13xx/samples/spi/Ssd1309.Spi.Samples/Simulations/FallingSandSimulation.cs @@ -0,0 +1,118 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Drawing; +using Iot.Device.Graphics.SkiaSharpAdapter; +using Iot.Device.Ssd13xx; +using SkiaSharp; + +namespace Ssd13xx.Samples.Simulations +{ + /// + /// A demo simulation of falling sand for SSD1309 displays + /// + public class FallingSandSimulation : Ssd1309Simulation + { + private readonly Random _random; + private readonly int _grainsPerPour; + + private int _grainsPoured; + private int _pourColumn; + + private SKCanvas _canvas; + + /// + /// A demo simulation of falling sand for SSD1309 displays + /// + /// Ssd1309 display device + /// Frames-per-second of the simulation + /// Number of grains to pour per column before moving to a random column + /// Toggle buffer debug logging on each frame + public FallingSandSimulation(Ssd1309 display, int fps = 1, int grainsPerPour = 20, bool debug = false) + : base(display, fps, debug: debug) + { + _grainsPerPour = grainsPerPour; + _random = new Random(); + _pourColumn = _random.Next(_display.ScreenWidth); + var drawingApi = _renderState.GetDrawingApi(); + _canvas = drawingApi.GetCanvas(); + drawingApi.DrawText("Hello Sand!", "DejaVu Sans", 18, Color.White, new Point(15, 0)); + } + + /// + /// Generates the next frame of the falling sand simulation. + /// + protected override void Update() + { + if (_grainsPoured < _grainsPerPour) + { + // Pour a single grain of sand in the current pour position + SetPixel(_pourColumn, 0, SKColors.White); + _grainsPoured++; + } + else + { + // Move the pour position to a random column + _pourColumn = _random.Next(_display.ScreenWidth); + _grainsPoured = 0; + } + + // Iterate starting at the bottom and working up + for (int y = _display.ScreenHeight - 1; y >= 0; y--) + { + for (int x = 0; x < _display.ScreenWidth; x++) + { + // If current pixel is sand, take action + if (GetPixelWithinBounds(x, y).Equals(SKColors.White)) + { + // Move sand down if space is empty + if (GetPixelWithinBounds(x, y + 1).Equals(SKColors.Black)) + { + SetPixel(x, y, SKColors.Black); + SetPixel(x, y + 1, SKColors.White); + } + + // Move sand down-left if space is empty + else if (GetPixelWithinBounds(x - 1, y + 1).Equals(SKColors.Black)) + { + SetPixel(x, y, SKColors.Black); + SetPixel(x - 1, y + 1, SKColors.White); + } + + // Move sand down-right if space is empty + else if (GetPixelWithinBounds(x + 1, y + 1).Equals(SKColors.Black)) + { + SetPixel(x, y, SKColors.Black); + SetPixel(x + 1, y + 1, SKColors.White); + } + } + } + } + } + + private void SetPixel(int x, int y, SKColor color) + { + _canvas.DrawPoint(new SKPoint(x, y), color); + } + + /// Null if the pixel is outside the bounds of the display + private SKColor? GetPixelWithinBounds(int x, int y) + { + if (x >= 0 && x < _display.ScreenWidth - 1) + { + if (y >= 0 && y < _display.ScreenHeight - 1) + { + return ConvertColor(_renderState.GetPixel(x, y)); + } + } + + return null; + } + + private SKColor ConvertColor(Color c) + { + return new SKColor(c.R, c.G, c.B, c.A); + } + } +} diff --git a/src/devices/Ssd13xx/samples/spi/Ssd1309.Spi.Samples/Simulations/Ssd1309Simulation.cs b/src/devices/Ssd13xx/samples/spi/Ssd1309.Spi.Samples/Simulations/Ssd1309Simulation.cs new file mode 100644 index 0000000000..2a1316d4cc --- /dev/null +++ b/src/devices/Ssd13xx/samples/spi/Ssd1309.Spi.Samples/Simulations/Ssd1309Simulation.cs @@ -0,0 +1,250 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Threading; +using System; +using System.Threading.Tasks; +using System.Collections.Concurrent; +using Iot.Device.Ssd13xx; +using Iot.Device.Graphics; + +namespace Ssd13xx.Samples.Simulations +{ + /// + /// Base class for rendering simulations on SSD1309 displays + /// + public abstract class Ssd1309Simulation + { + /// + /// Optional value to enable debug logging + /// + protected readonly bool _debug; + + /// + /// Instance of the Ssd1309 display to display the simulation + /// + protected readonly Ssd1309 _display; + + /// + /// Number of display-ready frames to store in the buffer + /// + protected readonly int _frameBufferSize; + + /// + /// Frames-per-second sent to the display + /// + protected readonly int _fps; + + /// + /// Frame buffer for display-ready frames + /// + protected ConcurrentQueue _frameBuffer; + + /// + /// Stores the current render state + /// + protected BitmapImage _renderState; + + /// + /// Semaphore to limit the number of concurent threads that can access rendering resources + /// + protected SemaphoreSlim _bufferSemaphore; + + /// + /// Semaphore to limit the number of concurrent threads that can send data to the display + /// + protected SemaphoreSlim _displaySemaphore; + + /// + /// Start time of the simulation + /// + protected DateTime _startTime; + + /// + /// Max frames of the simulation to run before stopping + /// + protected int _maxFrames = 0; + + /// + /// Number of frames sent to the display + /// + protected int _framesSent = 0; + + /// + /// Backing field. True if simulation is running + /// + protected bool _isRunning = false; + + /// + /// True if simulation is running + /// + public bool IsRunning => IsRunning; + + /// + /// Backing field. True if simulation has completed + /// + protected bool _isCompleted = false; + + /// + /// True if simulation has completed + /// + public bool IsCompleted => _isCompleted; + + /// + /// Base class for rendering simulations on SSD1309 displays + /// + /// Instance of a SSD1309 display + /// Frames-per-second to render the simulation + /// Number of device-ready frames to store in the buffer + /// Optional value to enable debug logging + public Ssd1309Simulation(Ssd1309 display, int fps = 30, int frameBufferSize = 30, bool debug = false) + { + _display = display; + _fps = fps; + _frameBufferSize = frameBufferSize; + _debug = debug; + _frameBuffer = new ConcurrentQueue(); + _bufferSemaphore = new SemaphoreSlim(1); + _displaySemaphore = new SemaphoreSlim(1); + _renderState = _display.GetBackBufferCompatibleImage(); + } + + /// + /// Starts the simulation + /// + /// Number of frames to simulate before completing + /// Initial delay to buffer frames. If unspecified, delay will be a factor of FPS and buffer size. + public async virtual Task StartAsync(int iterations, int? initialDelayMs = null) + { + _startTime = DateTime.UtcNow; + _maxFrames += iterations; + + _isRunning = true; + + var displayInterval = 1000 / _fps; + + // Assumes the buffer needs to be topped off when its 50% empty + var bufferInterval = displayInterval * (_frameBufferSize / 2); + + if (initialDelayMs == null) + { + initialDelayMs = bufferInterval * 2; + } + + // Sends the next frame in the simulation to the device after an initial delay + var displayTimer = new Timer(async (state) => await SendNextFrame(), null, initialDelayMs.Value, displayInterval); + + // Populates buffer immediatley and upon every bufferInterval thereafter + var bufferTimer = new Timer(async (state) => await PopulateBufferAsync(), null, 0, bufferInterval); + + Console.WriteLine("Running simulation. Press any key to stop..."); + + while (!Console.KeyAvailable) + { + if (_framesSent >= iterations) + { + Stop(); + _isCompleted = true; + + Console.WriteLine("Simulation completed"); + break; + } + + await Task.Delay(bufferInterval); + } + + if (_isRunning) + { + Console.ReadKey(); + Stop(); + } + } + + /// + /// Stops the simulation + /// + public virtual void Stop() + { + _isRunning = false; + + Console.WriteLine("Simulation stopped"); + } + + /// + /// Timer delegate that dequeues the next available frame from the buffer and sends to the display + /// + protected async virtual Task SendNextFrame() + { + if (_isRunning) + { + // Ensure only one thread can send data to the display at a time. If busy, skip. + if (await _displaySemaphore.WaitAsync(0)) + { + if (_frameBuffer.TryDequeue(out var nextFrame)) + { + _display.SendData(nextFrame); + _framesSent++; + } + else + { + if (_debug) + { + // This indicates that the buffer cannot be populated as fast as the display is attemting to send frames. + // Performance improvements could be achieved by optimizing the Update method or the simulation pattern. + // One obvious improvement would be to use byte arrays for render state instead of drawing to a bitmap, + // but that comes with development tradeoffs for scenarios like drawing text and shapes. + Console.WriteLine($"Frame skipped, buffer is empty. Actual FPS: {GetActualFps()}"); + } + } + + _displaySemaphore.Release(); + } + else + { + if (_debug) + { + // This indicates a bottleneck with the device itself. It may be possible to configure the display + // for better performance such as increasing clock speed. Otherwise consider reducing FPS. + Console.WriteLine($"Frame skipped, previous frame is still being sent. Actual FPS: {GetActualFps()}"); + } + } + } + } + + /// + /// Timer delegate that pre-renders frames and populates the buffer up to the max buffer size + /// + protected async virtual Task PopulateBufferAsync() + { + // Ensure only one thread can access rendering resources at a time. If busy, skip. + if (_isRunning && _renderState != null && await _bufferSemaphore.WaitAsync(0)) + { + while (_frameBuffer.Count < _frameBufferSize) + { + // Stores to a buffer of display-ready bytes without sending them to the display. + // This compensates for frames that may take longer to render than others by rendering on a separate thread and + // allows for fewer skipped frames at higher frame rates. + Update(); + _frameBuffer.Enqueue(_display.PreRenderBitmap(_renderState)); + } + + _bufferSemaphore.Release(); + } + } + + /// + /// Updates the render state for the next step of the simulation and returns a copy of the current state + /// + protected abstract void Update(); + + /// + /// Returns the actual FPS expressed as frames sent to the display in the elapsed time + /// + /// + protected virtual int GetActualFps() + { + var elapsed = DateTime.UtcNow - _startTime; + return (int)(_framesSent / elapsed.TotalSeconds); + } + } +} diff --git a/src/devices/Ssd13xx/samples/spi/Ssd1309.Spi.Samples/Ssd1309.Spi.Samples.csproj b/src/devices/Ssd13xx/samples/spi/Ssd1309.Spi.Samples/Ssd1309.Spi.Samples.csproj new file mode 100644 index 0000000000..f2521524fa --- /dev/null +++ b/src/devices/Ssd13xx/samples/spi/Ssd1309.Spi.Samples/Ssd1309.Spi.Samples.csproj @@ -0,0 +1,17 @@ + + + + Exe + $(DefaultSampleTfms) + enable + enable + 10 + + + + + + + + + diff --git a/src/devices/Ssd13xx/samples/spi/Ssd1309.Spi.Samples/TransparentSsd1309FallingSand.jpeg b/src/devices/Ssd13xx/samples/spi/Ssd1309.Spi.Samples/TransparentSsd1309FallingSand.jpeg new file mode 100644 index 0000000000..7404d1a973 Binary files /dev/null and b/src/devices/Ssd13xx/samples/spi/Ssd1309.Spi.Samples/TransparentSsd1309FallingSand.jpeg differ