Skip to content

Commit

Permalink
Add virtual gpio controller, attempt 2 (#2368)
Browse files Browse the repository at this point in the history
* Adding virtual gpio controller basic

* add basic eventing

* Adjusting GpioPin internals to take a GpioPin

* Adjusting based on discussions

* Adjusting based on PR feedback

* adjusting logic and removing some complexity

* adjusting based on PR feedback

* adjusting name

* Try an alternate approach

* Some first implementation

* Start testing the implementation

* Various tests added

* Add tests for callback

* Use backwards compatible calls for now

So we can build it without .NET 8.0 dependency

* Fix an unit test

And update the CompatibilitySuppressions.xml

---------

Co-authored-by: Laurent Ellerbach <[email protected]>
  • Loading branch information
pgrawehr and Ellerbach authored Jan 18, 2025
1 parent 272958a commit dd8e964
Show file tree
Hide file tree
Showing 13 changed files with 893 additions and 41 deletions.
32 changes: 27 additions & 5 deletions src/System.Device.Gpio.Tests/GpioPinTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,11 @@ public void TestOpenPin()
Assert.Equal(PinMode.Input, pin.GetPinMode());
}

/// <summary>
/// Closes the pin via the controller first
/// </summary>
[Fact]
public void TestClosePin()
public void TestClosePin1()
{
// Arrange
_mockedGpioDriver.Setup(x => x.OpenPinEx(PinNumber));
Expand All @@ -52,10 +55,29 @@ public void TestClosePin()
// Act
GpioPin pin = ctrl.OpenPin(PinNumber, PinMode.Input);
ctrl.ClosePin(PinNumber);
// Assert
// This should work even if the pin is closed in the controller as the driver has no idea
// Of the controller behavior.
var ret = pin.Read();
// Closing the pin makes its usage invalid
Assert.Throws<InvalidOperationException>(() => pin.Read());
pin.Dispose(); // Shouldn't throw
}

/// <summary>
/// Closes the pin via Dispose first
/// </summary>
[Fact]
public void TestClosePin2()
{
// Arrange
_mockedGpioDriver.Setup(x => x.OpenPinEx(PinNumber));
_mockedGpioDriver.Setup(x => x.IsPinModeSupportedEx(PinNumber, It.IsAny<PinMode>())).Returns(true);
_mockedGpioDriver.Setup(x => x.SetPinModeEx(PinNumber, It.IsAny<PinMode>()));
var ctrl = new GpioController(_mockedGpioDriver.Object);
// Act
GpioPin pin = ctrl.OpenPin(PinNumber, PinMode.Input);
pin.Dispose();
// Closing the pin makes its usage invalid
Assert.Throws<InvalidOperationException>(() => ctrl.Read(PinNumber));
// That is not valid now
Assert.Throws<InvalidOperationException>(() => ctrl.ClosePin(PinNumber));
}

[Fact]
Expand Down
21 changes: 21 additions & 0 deletions src/System.Device.Gpio/CompatibilitySuppressions.xml
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,27 @@
<Left>lib/netstandard2.0/System.Device.Gpio.dll</Left>
<Right>lib/net6.0-windows10.0.17763/System.Device.Gpio.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0007</DiagnosticId>
<Target>T:System.Device.Gpio.GpioPin</Target>
<Left>lib/net6.0-windows10.0.17763/System.Device.Gpio.dll</Left>
<Right>lib/net6.0-windows10.0.17763/System.Device.Gpio.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0007</DiagnosticId>
<Target>T:System.Device.Gpio.GpioPin</Target>
<Left>lib/net6.0/System.Device.Gpio.dll</Left>
<Right>lib/net6.0/System.Device.Gpio.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0007</DiagnosticId>
<Target>T:System.Device.Gpio.GpioPin</Target>
<Left>lib/netstandard2.0/System.Device.Gpio.dll</Left>
<Right>lib/netstandard2.0/System.Device.Gpio.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0008</DiagnosticId>
<Target>T:System.Device.Gpio.Drivers.GpiodException</Target>
Expand Down
2 changes: 1 addition & 1 deletion src/System.Device.Gpio/System.Device.Gpio.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
</PackageReference>
<Content Include="$(RepoRoot)README-nuget.md" Pack="true" Visible="false" PackagePath="\README.md" />
</ItemGroup>

<ItemGroup Condition="'$(TargetFramework)' == 'netstandard2.0'">
<PackageReference Include="Microsoft.Win32.Registry" Version="$(MicrosoftWin32RegistryPackageVersion)" /> <!-- This is Windows specific -->
<PackageReference Include="System.Memory" Version="$(SystemMemoryPackageVersion)" />
Expand Down
4 changes: 2 additions & 2 deletions src/System.Device.Gpio/System/Device/Gpio/GpioController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ public GpioPin OpenPin(int pinNumber)

OpenPinCore(pinNumber);
_openPins.TryAdd(pinNumber, null);
_gpioPins[pinNumber] = new GpioPin(pinNumber, _driver);
_gpioPins[pinNumber] = new GpioPin(pinNumber, this);
return _gpioPins[pinNumber];
}

Expand Down Expand Up @@ -261,7 +261,7 @@ public virtual PinMode GetPinMode(int pinNumber)
/// </summary>
/// <param name="pinNumber">The pin number in the controller's numbering scheme.</param>
/// <returns>The status if the pin is open or closed.</returns>
public bool IsPinOpen(int pinNumber)
public virtual bool IsPinOpen(int pinNumber)
{
CheckDriverValid();
return _openPins.ContainsKey(pinNumber);
Expand Down
109 changes: 96 additions & 13 deletions src/System.Device.Gpio/System/Device/Gpio/GpioPin.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,20 @@ namespace System.Device.Gpio
/// <summary>
/// Represents a general-purpose I/O (GPIO) pin.
/// </summary>
public class Gpio​Pin
public class Gpio​Pin : MarshalByRefObject, IEquatable<GpioPin>, IDisposable
{
private readonly int _pinNumber;
private readonly GpioDriver _driver;
private readonly GpioController _controller;

internal GpioPin(int pinNumber, GpioDriver driver)
/// <summary>
/// Create an instance of a GpioPin
/// </summary>
/// <param name="pinNumber">The pin number</param>
/// <param name="controller">The matching controller</param>
/// <remarks>Instances of this class are usually only generated by an instance of <see cref="GpioController"/></remarks>
protected internal GpioPin(int pinNumber, GpioController controller)
{
_driver = driver;
_controller = controller;
_pinNumber = pinNumber;
}

Expand All @@ -25,12 +31,17 @@ public class Gpio​Pin
/// </value>
public virtual int PinNumber => _pinNumber;

/// <summary>
/// The GPIO Controller this pin is assigned to
/// </summary>
public GpioController Controller => _controller;

/// <summary>
/// Gets the current pin mode for the general-purpose I/O (GPIO) pin. The pin mode specifies whether the pin is configured as an input or an output, and determines how values are driven onto the pin.
/// </summary>
/// <returns>An enumeration value that indicates the current pin mode for the GPIO pin.
/// The pin mode specifies whether the pin is configured as an input or an output, and determines how values are driven onto the pin.</returns>
public virtual PinMode GetPinMode() => _driver.GetPinMode(_pinNumber);
public virtual PinMode GetPinMode() => _controller.GetPinMode(_pinNumber);

/// <summary>
/// Gets whether the general-purpose I/O (GPIO) pin supports the specified pin mode.
Expand All @@ -40,7 +51,7 @@ public class Gpio​Pin
/// <see langword="true"/> if the GPIO pin supports the pin mode that pinMode specifies; otherwise false.
/// If you specify a pin mode for which this method returns <see langword="false"/> when you call <see cref="SetPinMode"/>, <see cref="SetPinMode"/> generates an exception.
/// </returns>
public virtual bool IsPinModeSupported(PinMode pinMode) => _driver.IsPinModeSupported(_pinNumber, pinMode);
public virtual bool IsPinModeSupported(PinMode pinMode) => _controller.IsPinModeSupported(_pinNumber, pinMode);

/// <summary>
/// Sets the pin mode of the general-purpose I/O (GPIO) pin.
Expand All @@ -49,13 +60,13 @@ public class Gpio​Pin
/// <param name="value">An enumeration value that specifies pin mode to use for the GPIO pin.
/// The pin mode specifies whether the pin is configured as an input or an output, and determines how values are driven onto the pin.</param>
/// <exception cref="ArgumentException">The GPIO pin does not support the specified pin mode.</exception>
public virtual void SetPinMode(PinMode value) => _driver.SetPinMode(_pinNumber, value);
public virtual void SetPinMode(PinMode value) => _controller.SetPinMode(_pinNumber, value);

/// <summary>
/// Reads the current value of the general-purpose I/O (GPIO) pin.
/// </summary>
/// <returns>The current value of the GPIO pin. If the pin is configured as an output, this value is the last value written to the pin.</returns>
public virtual PinValue Read() => _driver.Read(_pinNumber);
public virtual PinValue Read() => _controller.Read(_pinNumber);

/// <summary>
/// Drives the specified value onto the general purpose I/O (GPIO) pin according to the current pin mode for the pin
Expand All @@ -66,27 +77,99 @@ public class Gpio​Pin
/// <para>If the GPIO pin is configured as an input, the method updates the latched output value for the pin. The latched output value is driven onto the pin when the configuration for the pin changes to output.</para>
/// </param>
/// <exception cref="InvalidOperationException">This exception will be thrown on an attempt to write to a pin that hasn't been opened or is not configured as output.</exception>
public virtual void Write(PinValue value) => _driver.Write(_pinNumber, value);
public virtual void Write(PinValue value) => _controller.Write(_pinNumber, value);

/// <summary>
/// Occurs when the value of the general-purpose I/O (GPIO) pin changes, either because of an external stimulus when the pin is configured as an input, or when a value is written to the pin when the pin in configured as an output.
/// Occurs when the value of the general-purpose I/O (GPIO) pin changes, either because of an external stimulus when the pin is configured as an input, or when a value is written
/// to the pin when the pin in configured as an output.
/// </summary>
public virtual event PinChangeEventHandler ValueChanged
{
add
{
_driver.AddCallbackForPinValueChangedEvent(_pinNumber, PinEventTypes.Falling | PinEventTypes.Rising, value);
_controller.RegisterCallbackForPinValueChangedEvent(_pinNumber, PinEventTypes.Falling | PinEventTypes.Rising, value);
}

remove
{
_driver.RemoveCallbackForPinValueChangedEvent(_pinNumber, value);
_controller.UnregisterCallbackForPinValueChangedEvent(_pinNumber, value);
}
}

/// <summary>
/// Toggles the output of the general purpose I/O (GPIO) pin if the pin is configured as an output.
/// </summary>
public virtual void Toggle() => _driver.Toggle(_pinNumber);
public virtual void Toggle() => _controller.Toggle(_pinNumber);

/// <inheritdoc />
public virtual bool Equals(GpioPin? other)
{
if (other == null)
{
return false;
}

return PinNumber == other.PinNumber && ReferenceEquals(_controller, other._controller);
}

/// <inheritdoc />
public override bool Equals(object? obj)
{
if (obj == null)
{
return false;
}

if (ReferenceEquals(this, obj))
{
return true;
}

if (obj is GpioPin other)
{
return Equals(other);
}

return false;
}

/// <inheritdoc />
public override int GetHashCode()
{
return _controller.GetHashCode() ^ _pinNumber;
}

/// <summary>
/// Closes the pin
/// </summary>
public virtual void Close()
{
// Avoid an exception when called multiple times
if (_controller.IsPinOpen(_pinNumber))
{
_controller.ClosePin(_pinNumber);
}
}

/// <summary>
/// Disposes (closes) the pin
/// </summary>
/// <param name="disposing">Should always be true</param>
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
Close();
}
}

/// <summary>
/// Standard Dispose pattern
/// </summary>
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
}
}
Loading

0 comments on commit dd8e964

Please sign in to comment.