The goal of this API is to allow .Net Core developers to access General Purpose IO (GPIO) pins of IoT devices like Raspberry Pi. This will allow applications to read and write data from/to sensors, leds and other kind of peripherals connected to those pins. This API should also support most commonly used serial communication protocols like SPI and I2C.
For more details please see gpio spec.
``` C#
using (var controller = new GpioController())
{
GpioPin led = controller.OpenPin(26, PinMode.Output);
for (var i = 0; i < 5; ++i)
{
led.Write(PinValue.High);
Thread.Sleep(TimeSpan.FromSeconds(1));
led.Write(PinValue.Low);
Thread.Sleep(TimeSpan.FromSeconds(1));
}
}
### Turning on LED while pressing a button
```C#
using (var controller = new GpioController())
{
GpioPin button = controller.OpenPin(18, PinMode.Input);
GpioPin led = controller.OpenPin(26, PinMode.Output);
Stopwatch watch = Stopwatch.StartNew();
while (watch.Elapsed.TotalSeconds < 15)
{
PinValue value = button.Read();
led.Write(value);
}
}
```C#
using (var controller = new GpioController())
{
GpioPin button = controller.OpenPin(18, PinMode.Input);
button.DebounceTimeout = TimeSpan.FromSeconds(1);
button.NotifyEvents = PinEvent.SyncRisingEdge;
Stopwatch watch = Stopwatch.StartNew();
while (watch.Elapsed.TotalSeconds < 15)
{
TimeSpan timeout = TimeSpan.FromSeconds(1);
bool eventDetected = button.WaitForEvent(timeout);
string message = eventDetected ? "Event detected!" : "Timeout!";
Console.WriteLine(message);
}
}
### Detecting when a button is pressed using events
```C#
using (var controller = new GpioController())
{
GpioPin button = controller.OpenPin(18, PinMode.Input);
button.DebounceTimeout = TimeSpan.FromMilliseconds(100);
button.NotifyEvents = PinEvent.SyncFallingRisingEdge;
button.ValueChanged += OnPinValueChanged;
button.EnableRaisingEvents = true;
Stopwatch watch = Stopwatch.StartNew();
while (watch.Elapsed.TotalSeconds < 15)
{
Thread.Sleep(1 * 100);
if (buttonPressed)
{
Console.WriteLine($"Button press!");
}
}
}
private static void OnPinValueChanged(object sender, PinValueChangedEventArgs e)
{
string message = buttonPressed ? "Button up!" : "Button down!";
Console.WriteLine(message);
buttonPressed = !buttonPressed;
}
Just change the code to use the right driver for the new platform. If there is no driver available yet for a particular platform or device, the user can extend the model by implementing his own driver if needed and all the already existing APIs should also work just fine for that platform by using the new driver.
``` C#
using (Driver driver = new UnixDriver()) // or
using (Driver driver = new WindowsDriver()) // or
using (Driver driver = new RaspberryPiDriver()) // etc.
using (var controller = new GpioController(driver))
...
### Using a different pin numbering scheme
When creating the controller the user can specify the pin numbering scheme to use and it will be used everywhere a pin number is required or returned by the API. To get pin number in a different scheme the user can call the `GetNumber` pin method specifying the desired numbering scheme.
``` C#
var controller = new GpioController() // defaults to PinNumberingScheme.Gpio
var controller = new GpioController(PinNumberingScheme.Gpio) // or
var controller = new GpioController(PinNumberingScheme.Board)
``` C#
var busId = 1;
var chipSelectLine = 0;
var settings = new SpiConnectionSettings(busId, chipSelectLine);
SpiDevice device = new UnixSpiDevice(settings);
device.WriteByte(byte.MaxValue);
byte data8 = device.ReadByte();
device.WriteUShort(ushort.MaxValue);
ushort data16 = device.ReadUShort();
device.WriteUInt(uint.MaxValue);
uint data32 = device.ReadUInt();
device.WriteULong(ulong.MaxValue);
ulong data64 = device.ReadULong();
### Communicating with sensors using I2C
``` C#
var busId = 1;
var settings = new I2cConnectionSettings(busId, Sensor.DefaultI2cAddress);
I2cDevice device = new UnixI2cDevice(settings);
device.WriteByte(byte.MaxValue);
byte data8 = device.ReadByte();
device.WriteUShort(ushort.MaxValue);
ushort data16 = device.ReadUShort();
device.WriteUInt(uint.MaxValue);
uint data32 = device.ReadUInt();
device.WriteULong(ulong.MaxValue);
ulong data64 = device.ReadULong();
Following are the proposed APIs.
``` C#
namespace System.Devices.Gpio
{
public enum PinNumberingScheme
{
Board,
Gpio
}
public class GpioController : IDisposable
{
public GpioController(PinNumberingScheme numbering = PinNumberingScheme.Gpio);
public GpioController(GpioDriver driver, PinNumberingScheme numbering = PinNumberingScheme.Gpio);
public void Dispose();
public PinNumberingScheme Numbering { get; set; }
public int PinCount { get; };
public IEnumerable<GpioPin> OpenPins { get; };
public GpioPin this[int pinNumber] { get; }
public bool IsPinOpen(int number);
public GpioPin OpenPin(int number);
public void ClosePin(int number);
public void ClosePin(GpioPin pin);
}
}
### GpioPin class
``` C#
namespace System.Devices.Gpio
{
public enum PinMode
{
Input,
Output,
InputPullDown,
InputPullUp
}
[Flags]
public enum PinEvent
{
None = 0,
Low = 1,
High = 2,
SyncFallingEdge = 4,
SyncRisingEdge = 8,
AsyncFallingEdge = 16,
AsyncRisingEdge = 32,
LowHigh = Low | High,
SyncFallingRisingEdge = SyncFallingEdge | SyncRisingEdge,
AsyncFallingRisingEdge = AsyncFallingEdge | AsyncRisingEdge,
Any = LowHigh | SyncFallingRisingEdge | AsyncFallingRisingEdge
}
public enum PinValue
{
Low = 0,
High = 1
}
public class PinValueChangedEventArgs : EventArgs
{
public PinValueChangedEventArgs(int gpioPinNumber);
public int GpioPinNumber { get; }
}
public class GpioPin : IDisposable
{
public void Dispose();
public event EventHandler<PinValueChangedEventArgs> ValueChanged;
public GpioController Controller { get; }
public int GpioNumber { get; }
public TimeSpan DebounceTimeout { get; set; }
public PinMode Mode { get; set; }
public PinEvent NotifyEvents { get; set; }
public bool EnableRaisingEvents { get; set; }
public bool IsModeSupported(PinMode mode) { get; }
public int GetNumber(PinNumberingScheme numbering) { get; }
public PinValue Read() { get; }
public void Write(PinValue value) { get; }
public bool WaitForEvent(TimeSpan timeout) { get; }
}
}
``` C#
namespace System.Devices.Gpio
{
public abstract class GpioDriver : IDisposable
{
public event EventHandler
protected internal abstract int PinCount { get; }
protected internal abstract int ConvertPinNumber(int pinNumber, PinNumberingScheme from, PinNumberingScheme to);
protected internal abstract bool IsPinModeSupported(PinMode mode);
protected internal abstract void OpenPin(int gpioPinNumber);
protected internal abstract void ClosePin(int gpioPinNumber);
protected internal abstract void SetPinMode(int gpioPinNumber, PinMode mode);
protected internal abstract PinMode GetPinMode(int gpioPinNumber);
protected internal abstract void Output(int gpioPinNumber, PinValue value);
protected internal abstract PinValue Input(int gpioPinNumber);
protected internal abstract void SetDebounce(int gpioPinNumber, TimeSpan timeout);
protected internal abstract TimeSpan GetDebounce(int gpioPinNumber);
protected internal abstract void SetPinEventsToDetect(int gpioPinNumber, PinEvent events);
protected internal abstract PinEvent GetPinEventsToDetect(int gpioPinNumber);
protected internal abstract void SetEnableRaisingPinEvents(int gpioPinNumber, bool enable);
protected internal abstract bool GetEnableRaisingPinEvents(int gpioPinNumber);
protected internal abstract bool WaitForPinEvent(int gpioPinNumber, TimeSpan timeout);
public abstract void Dispose();
}
// General class for supporting any device running a Unix based OS
public class UnixDriver : GpioDriver { ... }
// General class for supporting any device running Windows OS
public class WindowsDriver : GpioDriver { ... }
// Class for supporting Raspberry Pi specific features
public class RaspberryPiDriver : GpioDriver { ... }
}
### Gpio extension methods
``` C#
namespace System.Devices.Gpio
{
public static class GpioExtensions
{
public static GpioPin OpenPin(this GpioController controller, int number, PinMode mode);
public static GpioPin[] OpenPins(this GpioController controller, PinMode mode, params int[] numbers);
public static GpioPin[] OpenPins(this GpioController controller, params int[] numbers);
public static void Set(this GpioPin pin);
public static void Clear(this GpioPin pin);
public static void Toggle(this GpioPin pin);
}
}
``` C#
namespace System.Devices.Spi
{
///
/// Defines the SPI communication mode.
/// The communication mode defines the clock edge on which the master out line toggles,
/// the master in line samples, and the signal clock's signal steady level (named SCLK).
/// Each mode is defined with a pair of parameters called clock polarity (CPOL) and clock phase (CPHA).
///
public enum SpiMode
{
///
Mode0 = 0,
///
Mode1 = 1,
///
Mode2 = 2,
///
Mode3 = 3
}
/// <summary>
/// Represents the settings for the connection with an <see cref="SpiDevice"/>.
/// </summary>
public sealed class SpiConnectionSettings
{
/// <summary>
/// Gets or sets the Spi bus id for this connection.
/// </summary>
public uint BusId { get; set; }
/// <summary>
/// Gets or sets the chip select line for this connection.
/// </summary>
public uint ChipSelectLine { get; set; }
/// <summary>
/// Gets or sets the <see cref="SpiMode"/> for this connection.
/// </summary>
public SpiMode Mode { get; set; }
/// <summary>
/// Gets or sets the bit length for data on this connection.
/// </summary>
public uint DataBitLength { get; set; }
/// <summary>
/// Gets or sets the clock frequency in Hz for the connection.
/// </summary>
public uint ClockFrequency { get; set; }
/// <summary>
/// Initializes new instance of <see cref="SpiConnectionSettings"/>.
/// </summary>
/// <param name="busId">The Spi bus id on which the connection will be made.</param>
/// <param name="chipSelectLine">The chip select line on which the connection will be made.</param>
public SpiConnectionSettings(uint busId, uint chipSelectLine);
}
public abstract class SpiDevice : IDisposable
{
public SpiDevice(SpiConnectionSettings settings);
public abstract void Dispose();
public SpiConnectionSettings GetConnectionSettings();
public abstract void TransferFullDuplex(byte[] writeBuffer, byte[] readBuffer);
public abstract void ReadBinary(byte[] buffer);
public abstract byte ReadByte();
public abstract ushort ReadUShort();
public abstract uint ReadUInt();
public abstract ulong ReadULong();
public abstract void WriteBinary(byte[] buffer);
public abstract void WriteByte(byte value);
public abstract void WriteUShort(ushort value);
public abstract void WriteUInt(uint value);
public abstract void WriteULong(ulong value);
}
// General class for supporting any device running a Unix based OS
public class UnixSpiDevice : SpiDevice { ... }
// General class for supporting any device running Windows OS
public class WindowsSpiDevice : SpiDevice { ... }
}
### I2cDevice classes
``` C#
namespace System.Devices.I2c
{
/// <summary>
/// Represents the settings for the connection with an <see cref="I2cDevice"/>.
/// </summary>
public sealed class I2cConnectionSettings
{
/// <summary>
/// Gets or sets the I2c bus id for this connection.
/// </summary>
public uint BusId { get; set; }
/// <summary>
/// Gets or sets the I2c connection device address.
/// </summary>
public uint DeviceAddress { get; set; }
/// <summary>
/// Initializes new instance of <see cref="I2cConnectionSettings"/>.
/// </summary>
/// <param name="busId">The I2c bus id on which the connection will be made.</param>
/// <param name="deviceAddress">The I2c connection device address.</param>
public I2cConnectionSettings(uint busId, uint deviceAddress);
/// <summary>
/// Initializes new instance of <see cref="I2cConnectionSettings"/>.
/// </summary>
/// <param name="busId">The I2c bus id on which the connection will be made.</param>
public I2cConnectionSettings(uint busId);
}
public abstract class I2cDevice : IDisposable
{
public I2cDevice(I2cConnectionSettings settings);
public abstract void Dispose();
public I2cConnectionSettings GetConnectionSettings();
public abstract void WriteRead(byte[] writeBuffer, byte[] readBuffer);
public abstract void ReadBinary(byte[] buffer);
public abstract byte ReadByte();
public abstract ushort ReadUShort();
public abstract uint ReadUInt();
public abstract ulong ReadULong();
public abstract void WriteBinary(byte[] buffer);
public abstract void WriteByte(byte value);
public abstract void WriteUShort(ushort value);
public abstract void WriteUInt(uint value);
public abstract void WriteULong(ulong value);
}
// General class for supporting any device running a Unix based OS
public class UnixI2cDevice : I2cDevice { ... }
// General class for supporting any device running Windows OS
public class WindowsI2cDevice : I2cDevice { ... }
}
``` C#
namespace System.Devices.Spi
{
public static class SpiExtensions
{
public static ulong Read(this SpiDevice device, uint byteCount);
}
}
namespace System.Devices.I2c
{
public static class I2cExtensions
{
public static ulong Read(this I2cDevice device, uint byteCount);
}
}
```
Nice work getting this design proposal put together @edgardozoppi! Here's some initial feedback to seed the discussion on the draft
using (Driver driver = new UnixDriver(RaspberryPiPinCount))
using (var controller = new GpioController(driver, PinNumberingScheme.Bcm))
This "double using statement" pattern caught my attention right away
1) What scenarios does a user need a Driver but not a controller? How common is this?
2) Can we refactor the shape or provide a way to construct the controller directly without the outer driver scope?
Blinking LED
Can we add a "Toggle" method for blinking LEDs? Scratch this, I see it's an extension method at the very bottom...
Pin button = controller.OpenPin(18, PinMode.Input);
if (button.IsModeSupported(PinMode.InputPullDown))
{
button.Mode = PinMode.InputPullDown;
}
Can the InputPullDown be specificed during OpenPin to reduce the number of lines of code needed?
var busId = 1;
var chipSelectLine = 0;
var settings = new SpiConnectionSettings(busId, chipSelectLine);
SpiDevice device = new UnixSpiDevice(settings);device.Write8(byte.MaxValue);
byte data8 = device.Read8();
From someone new to SPI and I2C, it's not clear that BusID is almost (always?) "-1, 0, 1" depending on if they want to use "block 0" or "block 1" of the SPI pins on the controller. Is there a way to make this more clear in the design? Should we have an enum? What happens if we pass in a nonsensical value like 0xBAADF00D today?
Pin class
public class Pin : IDisposable
Do we use the name "Pin" anywhere else in .NET? Can the type be used throughout all of System.Devices.* (i.e., moved up and out of the GPIO subnamespace) or is it too specific to GPIO (And if so, is this "too good of a name"?) If it needs to remain under GPIO then why don't we name this "GpioPin" like we've named other things "GpioDriver" and "GpioController"?
GpioDriver classes
protected internal abstract int ConvertPinNumber(int pinNumber, PinNumberingScheme from, PinNumberingScheme to);
Should this be a simpler "ConvertTo(int pinNUmber, PinNumberingScheme scheme)" instead?
Adding my initial feedback as well. I am looking forward to learning more about this space!
public abstract class SpiDevice : IDisposable
public abstract void TransferFullDuplex(byte[] writeBuffer, byte[] readBuffer);
public abstract void Read(byte[] buffer);
Have you considered turning these into span-based APIs instead? That would provide the additional benefit of reading into a slice of an array (alternatively you would have to pass an array and length).
public static class SpiExtensions
public static ulong Read(this SpiDevice device, uint byteCount);
Why did you choose to make these extension methods instead of instance methods?
public abstract class I2cDevice : IDisposable
public abstract void WriteRead(byte[] writeBuffer, byte[] readBuffer);
I think WriteRead is a somewhat confusing name for this API. Should it be called Transfer (or TransferFullDuplex) similar to SPI?
public abstract class I2cDevice : IDisposable
public abstract void Read(byte[] buffer);
public abstract byte Read8();
I am not very familiar with this space, so this might be a naive question, but and looking at other APIs that support I2c, it looks like they support reading/writing from/to specific registers or addresses. Do we plan to provide such capability?
https://developer.android.com/reference/com/google/android/things/pio/I2cDevice
https://docs.mbed.com/docs/trying-new-skeletong-for-apis/en/latest/APIs/interfaces/digital/I2C/
public sealed class SpiConnectionSettings
Should we support both little and big endianness for reading and writing? If so, how?
Thanks for your early feedback!
This "double using statement" pattern caught my attention right away
- What scenarios does a user need a Driver but not a controller? How common is this?
- Can we refactor the shape or provide a way to construct the controller directly without the outer driver scope?
Actually, there is no real need to use using for the driver and the controller, with only one of them is enough because the controller is only disposing the driver. So the user can use the driver and/or the controller.
The controller takes a driver a parameter in the constructor because the user needs to specify wich driver wants to use with the controller. The idea is that in the future many drivers could exist for implementing specific features of each device\platform, but the same controller will be used in all those cases. The controller doesn't know which drivers the user wants to use. If the controller takes an enumeration to specify which driver to create internally, it won't be extensible in the future when some user adds a new driver for a new device.
Pin button = controller.OpenPin(18, PinMode.Input);
if (button.IsModeSupported(PinMode.InputPullDown))
{
button.Mode = PinMode.InputPullDown;
}Can the InputPullDown be specificed during OpenPin to reduce the number of lines of code needed?
Yes, as you can see in the above example the user can specify the PinMode directly when opening the pin. But some devices don't support pull up/down modes for pins because they don't have internal resistors already included (only a subset, possibly empty, of the pins support it) so the example is also showing how to know if a particular pin supports this mode, and only using it if that is the case.
An alternative implementation could be marking PinMode enumeration with Flags attribute and do something like this instead:
C#
Pin button = controller.OpenPin(18, PinMode.Input | PinMode.InputPullDown);
Meaning: try to use input pull down if it is supported or just input if it's not.
From someone new to SPI and I2C, it's not clear that BusID is almost (always?) "-1, 0, 1" depending on if they want to use "block 0" or "block 1" of the SPI pins on the controller. Is there a way to make this more clear in the design? Should we have an enum? What happens if we pass in a nonsensical value like 0xBAADF00D today?
The problem with the enum is that different devices have different number of SPI busses available to use. The current Unix implementation throws a IOException with message "Cannot open Spi device file '{deviceFileName}'" when the busId have a wrong value.
GpioDriver classes
protected internal abstract int ConvertPinNumber(int pinNumber, PinNumberingScheme from, PinNumberingScheme to);Should this be a simpler "ConvertTo(int pinNUmber, PinNumberingScheme scheme)" instead?
The ConvertPinNumber method takes a pin number in the from numbering scheme and converts it to the specified to numbering scheme. The from parameter is needed because the method have to know which numbering scheme the given pin number is using.
public abstract class SpiDevice : IDisposable
public abstract void TransferFullDuplex(byte[] writeBuffer, byte[] readBuffer);
public abstract void Read(byte[] buffer);Have you considered turning these into span-based APIs instead? That would provide the additional benefit of reading into a slice of an array (alternatively you would have to pass an array and length).
Actually, I didn't consider Span because I still don't know too much about it, but I will. Thanks for the suggestion.
public static class SpiExtensions
public static ulong Read(this SpiDevice device, uint byteCount);Why did you choose to make these extension methods instead of instance methods?
I decided that way because those methods can be easily implemented using the already existing SpiDevice and I2cDevice public methods and this way they will be automatically available for any new possible implementation of those abstract classes.
public abstract class I2cDevice : IDisposable
public abstract void WriteRead(byte[] writeBuffer, byte[] readBuffer);I think WriteRead is a somewhat confusing name for this API. Should it be called Transfer (or TransferFullDuplex) similar to SPI?
The name is different because it does a different thing in comparison with Spi TransferFullDuplex method. The later performs a simultaneous write and read transfer while the former executes a write immediately followed by a read, which is not the same as calling Write method followed by Read method because the first alternative is only one transaction while the last is making two transactions.
The name is different because it does a different thing in comparison with Spi TransferFullDuplex method. The later performs a simultaneous write and read transfer while the former executes a write immediately followed by a read,
How about TransferSequential? Similar to this:
https://docs.microsoft.com/en-us/uwp/api/windows.devices.spi.spidevice
Could be, but I still like more the name WriteRead because with TransferSequential is not clear the order of the operations.
I am not very familiar with this space, so this might be a naive question, but and looking at other APIs that support I2c, it looks like they support reading/writing from/to specific registers or addresses. Do we plan to provide such capability?
We already support that use case. It can be accomplished by just writing the register address first followed by a read or write of the data the user wants to read from or write to the register. Maybe we can add an extension method that do exactly this but takes the register address and data as two separate parameters, so it is more clear.
Should we support both little and big endianness for reading and writing? If so, how?
Good point, we can add an enum for endianness (if there is not one already) and take an additional (optional? what default?) parameter of this type.
@edgardozoppi, you can take a look at NpgsqlReadBuffer and NpgsqlWriteBuffer for an example how they handle such situation. There is an optional parameter to specify the required endianness, but by default Npgsql uses big endianness.
聽聽聽聽聽聽聽聽[MethodImpl(MethodImplOptions.AggressiveInlining)]
聽聽聽聽聽聽聽聽public short ReadInt16()
聽聽聽聽聽聽聽聽聽聽聽聽=> ReadInt16(false);
聽聽聽聽聽聽聽聽[MethodImpl(MethodImplOptions.AggressiveInlining)]
聽聽聽聽聽聽聽聽public short ReadInt16(bool littleEndian)
聽聽聽聽聽聽聽聽{
聽聽聽聽聽聽聽聽聽聽聽聽var result = Read<short>();
聽聽聽聽聽聽聽聽聽聽聽聽return littleEndian == BitConverter.IsLittleEndian
聽聽聽聽聽聽聽聽聽聽聽聽聽聽聽聽? result : PGUtil.ReverseEndianness(result);
聽聽聽聽聽聽聽聽}
聽聽聽聽聽聽聽聽[MethodImpl(MethodImplOptions.AggressiveInlining)]
聽聽聽聽聽聽聽聽public void WriteInt16(short value)
聽聽聽聽聽聽聽聽聽聽聽聽=> WriteInt16(value, false);
聽聽聽聽聽聽聽聽[MethodImpl(MethodImplOptions.AggressiveInlining)]
聽聽聽聽聽聽聽聽public void WriteInt16(short value, bool littleEndian)
聽聽聽聽聽聽聽聽聽聽聽聽=> Write(littleEndian == BitConverter.IsLittleEndian ? value : PGUtil.ReverseEndianness(value));
You have methods named like ReadUShort but precedent eg in BinaryPrimitives, BlobBuilder, BinaryReader, UnmanagedMemoryAccessor is to use BCL names not C# names eg ReadUInt16.
@ianhays can you please drive this going forward now that @edgardozoppi has finished his summer internship?
I'd like to push to have the project through API Review, setup an automated testing environment for IoT devices, and move the project to dotnet/corefx by Thanksgiving this year.
@joperezr Any update on the status of this? I know a lot of us are looking for a good Gpio solution in .net core and this looks to have been a great start!
Cc @richlander
Thanks for the correction, sorry @rlander
@los93sol we have moved this work to It鈥檚 own independent repo that we will make public very soon. We have made some great progress in this space. We will update this thread with a link to the blogpost once that happens to invite all of you guys to contribute!
@joperezr while i would like to see a progressed API and something more supported (both MS and community), i question why would this even be in a private repo? What happened to the openness? This is one reason I gave up trying to put in little extra time to provide feedback and such (e.g. I2C, SPI, etc).
Anyways, i'm still looking forward to the public release.
@joperezr Thank you for the update, great to hear things have still been progressing behind the scenes!
All of the Api Reviews have been live streamed in our youtube channel, the repo was only private while we figured out how the layout would look like and did all the infrastructure work for the actual build and distribution.
Thanks for explanation @joperezr and excuse me if my wording was terse. The mentions of the private repo was a little confusing as I鈥檝e been trying to watch the review sessions and progress on the CoreFxLabs repo. It just seems like all prototypes would have been thought through (like similar APIs) before moving over to CoreFx or other final repo.
As mentioned before, I still look forward to the progress and release. I鈥檓 sure it will be very useful. Until then... happy holidays 馃槃
Closing as this package has moved to the https://github.com/dotnet/iot/ repo
It is great to see how this project moved forward to support Windows 10 and other pin features like PWM. It is also good to have device bindings using the new public API like ADC. I can't wait to see more bindings for LCD, BME280 and the other devices we used, as well as new contributions from the community!
Great work guys!! 馃槃
@edgardozoppi Thanks for all the hard work you did on this project. You were key to delivering the foundations of the project 馃槃
Most helpful comment
It is great to see how this project moved forward to support Windows 10 and other pin features like PWM. It is also good to have device bindings using the new public API like ADC. I can't wait to see more bindings for LCD, BME280 and the other devices we used, as well as new contributions from the community!
Great work guys!! 馃槃