diff --git a/src/System.Device.Gpio.Tests/GpioPinTests.cs b/src/System.Device.Gpio.Tests/GpioPinTests.cs index 1babe34a37..b0f9a52996 100644 --- a/src/System.Device.Gpio.Tests/GpioPinTests.cs +++ b/src/System.Device.Gpio.Tests/GpioPinTests.cs @@ -41,8 +41,11 @@ public void TestOpenPin() Assert.Equal(PinMode.Input, pin.GetPinMode()); } + /// + /// Closes the pin via the controller first + /// [Fact] - public void TestClosePin() + public void TestClosePin1() { // Arrange _mockedGpioDriver.Setup(x => x.OpenPinEx(PinNumber)); @@ -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(() => pin.Read()); + pin.Dispose(); // Shouldn't throw + } + + /// + /// Closes the pin via Dispose first + /// + [Fact] + public void TestClosePin2() + { + // Arrange + _mockedGpioDriver.Setup(x => x.OpenPinEx(PinNumber)); + _mockedGpioDriver.Setup(x => x.IsPinModeSupportedEx(PinNumber, It.IsAny())).Returns(true); + _mockedGpioDriver.Setup(x => x.SetPinModeEx(PinNumber, It.IsAny())); + 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(() => ctrl.Read(PinNumber)); + // That is not valid now + Assert.Throws(() => ctrl.ClosePin(PinNumber)); } [Fact] diff --git a/src/System.Device.Gpio/CompatibilitySuppressions.xml b/src/System.Device.Gpio/CompatibilitySuppressions.xml index cc1c357495..82a50c6fdc 100644 --- a/src/System.Device.Gpio/CompatibilitySuppressions.xml +++ b/src/System.Device.Gpio/CompatibilitySuppressions.xml @@ -235,6 +235,27 @@ lib/netstandard2.0/System.Device.Gpio.dll lib/net6.0-windows10.0.17763/System.Device.Gpio.dll + + CP0007 + T:System.Device.Gpio.GpioPin + lib/net6.0-windows10.0.17763/System.Device.Gpio.dll + lib/net6.0-windows10.0.17763/System.Device.Gpio.dll + true + + + CP0007 + T:System.Device.Gpio.GpioPin + lib/net6.0/System.Device.Gpio.dll + lib/net6.0/System.Device.Gpio.dll + true + + + CP0007 + T:System.Device.Gpio.GpioPin + lib/netstandard2.0/System.Device.Gpio.dll + lib/netstandard2.0/System.Device.Gpio.dll + true + CP0008 T:System.Device.Gpio.Drivers.GpiodException diff --git a/src/System.Device.Gpio/System.Device.Gpio.csproj b/src/System.Device.Gpio/System.Device.Gpio.csproj index 2b57462226..39e675a525 100644 --- a/src/System.Device.Gpio/System.Device.Gpio.csproj +++ b/src/System.Device.Gpio/System.Device.Gpio.csproj @@ -19,7 +19,7 @@ - + diff --git a/src/System.Device.Gpio/System/Device/Gpio/GpioController.cs b/src/System.Device.Gpio/System/Device/Gpio/GpioController.cs index dabb555e42..64dad51567 100644 --- a/src/System.Device.Gpio/System/Device/Gpio/GpioController.cs +++ b/src/System.Device.Gpio/System/Device/Gpio/GpioController.cs @@ -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]; } @@ -261,7 +261,7 @@ public virtual PinMode GetPinMode(int pinNumber) /// /// The pin number in the controller's numbering scheme. /// The status if the pin is open or closed. - public bool IsPinOpen(int pinNumber) + public virtual bool IsPinOpen(int pinNumber) { CheckDriverValid(); return _openPins.ContainsKey(pinNumber); diff --git a/src/System.Device.Gpio/System/Device/Gpio/GpioPin.cs b/src/System.Device.Gpio/System/Device/Gpio/GpioPin.cs index 2bef40424c..05b8cbdf91 100644 --- a/src/System.Device.Gpio/System/Device/Gpio/GpioPin.cs +++ b/src/System.Device.Gpio/System/Device/Gpio/GpioPin.cs @@ -6,14 +6,20 @@ namespace System.Device.Gpio /// /// Represents a general-purpose I/O (GPIO) pin. /// - public class Gpio​Pin + public class Gpio​Pin : MarshalByRefObject, IEquatable, IDisposable { private readonly int _pinNumber; - private readonly GpioDriver _driver; + private readonly GpioController _controller; - internal Gpio​Pin(int pinNumber, GpioDriver driver) + /// + /// Create an instance of a GpioPin + /// + /// The pin number + /// The matching controller + /// Instances of this class are usually only generated by an instance of + protected internal Gpio​Pin(int pinNumber, GpioController controller) { - _driver = driver; + _controller = controller; _pinNumber = pinNumber; } @@ -25,12 +31,17 @@ public class Gpio​Pin /// public virtual int PinNumber => _pinNumber; + /// + /// The GPIO Controller this pin is assigned to + /// + public GpioController Controller => _controller; + /// /// 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. /// /// 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. - public virtual PinMode GetPinMode() => _driver.GetPinMode(_pinNumber); + public virtual PinMode GetPinMode() => _controller.GetPinMode(_pinNumber); /// /// Gets whether the general-purpose I/O (GPIO) pin supports the specified pin mode. @@ -40,7 +51,7 @@ public class Gpio​Pin /// if the GPIO pin supports the pin mode that pinMode specifies; otherwise false. /// If you specify a pin mode for which this method returns when you call , generates an exception. /// - public virtual bool IsPinModeSupported(PinMode pinMode) => _driver.IsPinModeSupported(_pinNumber, pinMode); + public virtual bool IsPinModeSupported(PinMode pinMode) => _controller.IsPinModeSupported(_pinNumber, pinMode); /// /// Sets the pin mode of the general-purpose I/O (GPIO) pin. @@ -49,13 +60,13 @@ public class Gpio​Pin /// 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. /// The GPIO pin does not support the specified pin mode. - public virtual void SetPinMode(PinMode value) => _driver.SetPinMode(_pinNumber, value); + public virtual void SetPinMode(PinMode value) => _controller.SetPinMode(_pinNumber, value); /// /// Reads the current value of the general-purpose I/O (GPIO) pin. /// /// 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. - public virtual PinValue Read() => _driver.Read(_pinNumber); + public virtual PinValue Read() => _controller.Read(_pinNumber); /// /// Drives the specified value onto the general purpose I/O (GPIO) pin according to the current pin mode for the pin @@ -66,27 +77,99 @@ public class Gpio​Pin /// 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. /// /// This exception will be thrown on an attempt to write to a pin that hasn't been opened or is not configured as output. - public virtual void Write(PinValue value) => _driver.Write(_pinNumber, value); + public virtual void Write(PinValue value) => _controller.Write(_pinNumber, value); /// - /// 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. /// 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); } } /// /// Toggles the output of the general purpose I/O (GPIO) pin if the pin is configured as an output. /// - public virtual void Toggle() => _driver.Toggle(_pinNumber); + public virtual void Toggle() => _controller.Toggle(_pinNumber); + + /// + public virtual bool Equals(GpioPin? other) + { + if (other == null) + { + return false; + } + + return PinNumber == other.PinNumber && ReferenceEquals(_controller, other._controller); + } + + /// + 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; + } + + /// + public override int GetHashCode() + { + return _controller.GetHashCode() ^ _pinNumber; + } + + /// + /// Closes the pin + /// + public virtual void Close() + { + // Avoid an exception when called multiple times + if (_controller.IsPinOpen(_pinNumber)) + { + _controller.ClosePin(_pinNumber); + } + } + + /// + /// Disposes (closes) the pin + /// + /// Should always be true + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + Close(); + } + } + + /// + /// Standard Dispose pattern + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } } } diff --git a/src/devices/Gpio/Drivers/VirtualGpioController.cs b/src/devices/Gpio/Drivers/VirtualGpioController.cs new file mode 100644 index 0000000000..9ce32de176 --- /dev/null +++ b/src/devices/Gpio/Drivers/VirtualGpioController.cs @@ -0,0 +1,291 @@ +// 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.Tasks; +using System.Threading; +using System.Collections.Generic; +using System.Linq; +using System.Collections.Concurrent; +using System.Device.Gpio; +using System; +using System.Device; +using Iot.Device.Board; + +namespace Iot.Device.Gpio +{ + /// + /// An implementation of a Virtual . + /// + public class VirtualGpioController : GpioController + { + private readonly ConcurrentDictionary _pins = new ConcurrentDictionary(); + private readonly ConcurrentDictionary _callbackEvents = new ConcurrentDictionary(); + + private record PinEvent(PinEventTypes PinEventTypes, PinChangeEventHandler? Callbacks); + + /// + /// Initializes a new empty instance of the class. + /// + public VirtualGpioController() + : base(new DummyGpioDriver()) + { + } + + /// + /// Initializes a new instance of the class. + /// + public VirtualGpioController(Dictionary pins) + : this() + { + foreach (var pin in pins) + { + if (!_pins.TryAdd(pin.Key, new VirtualGpioPin(pin.Value, pin.Key, this))) + { + throw new ArgumentException($"Duplicate virtual pin number in collection: {pin.Key}."); + } + } + } + + /// + /// Initializes a new instance of the class. + /// + public VirtualGpioController(IEnumerable pins) + : this() + { + int inc = 0; + foreach (var pin in pins) + { + _pins.TryAdd(inc, new VirtualGpioPin(pin, inc, this)); + inc++; + } + } + + /// + /// Adds a new pin number with an associated . + /// + /// The pin number. + /// The to add. + /// True on success, false if the pin number is in use already + public bool Add(int pinNumber, GpioPin gpioPin) => _pins.TryAdd(pinNumber, new VirtualGpioPin(gpioPin, pinNumber, this)); + + /// + /// Adds a new . The number is done automatically by adding after the last element. + /// + /// The to add. + public void Add(GpioPin gpioPin) + { + // Find the last number of the pin and assign the new number to it + int newPin = _pins.Count > 0 ? _pins.Keys.Max() + 1 : 0; + _pins.TryAdd(newPin, new VirtualGpioPin(gpioPin, newPin, this)); + } + + /// + public override int PinCount => _pins.Count; + + /// + /// Disposes this instance and removes all pins associated with this virtual controller. + /// + /// True to dispose all instances, false to dispose only unmanaged resources + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + // We're just emptying the lists + _pins.Clear(); + } + + /// + protected override void OpenPinCore(int pinNumber) + { + // Not clear what this should do + throw new InvalidOperationException("Cannot open a pin on the VirtualGpioController, as they're already open when constructed"); + } + + /// + /// Closes the given pin + /// + /// The pin number + /// There is no such virtual pin number registered or it was already closed + protected override void ClosePinCore(int pinNumber) + { + if (!_pins.TryGetValue(pinNumber, out var pin)) + { + throw new InvalidOperationException($"Virtual pin {pinNumber} does not exist"); + } + + pin.Close(); + _pins.TryRemove(pinNumber, out _); + } + + /// + public override bool IsPinOpen(int pinNumber) + { + return _pins.ContainsKey(pinNumber); + } + + /// + public override PinMode GetPinMode(int pinNumber) + { + if (!IsPinOpen(pinNumber)) + { + throw new InvalidOperationException($"Can not get a mode to pin {pinNumber} because it is not open."); + } + + return _pins[pinNumber].GetPinMode(); + } + + /// + public override bool IsPinModeSupported(int pinNumber, PinMode mode) + { + return _pins[pinNumber].IsPinModeSupported(mode); + } + + /// + public override ComponentInformation QueryComponentInformation() + { + ComponentInformation self = new ComponentInformation(this, "Virtual GPIO Controller"); + + HashSet controllers = new HashSet(); + foreach (var pin in _pins) + { + controllers.Add(pin.Value.OldController); + self.Properties.Add($"PinMapping{pin.Key}", pin.Value.OldPinNumber.ToString()); + } + + foreach (var c in controllers) + { + self.AddSubComponent(c.QueryComponentInformation()); + } + + return self; + } + + /// + public override PinValue Read(int pinNumber) + { + if (!IsPinOpen(pinNumber)) + { + throw new InvalidOperationException($"Can not read from pin {pinNumber} because it is not open."); + } + + return _pins[pinNumber].Read(); + } + + /// + /// Sets the mode to a pin. + /// + /// The pin number in the controller's numbering scheme. + /// The mode to be set. + public override void SetPinMode(int pinNumber, PinMode mode) + { + if (!IsPinOpen(pinNumber)) + { + throw new InvalidOperationException($"Can not set a mode to pin {pinNumber} because it is not open."); + } + + if (!IsPinModeSupported(pinNumber, mode)) + { + throw new InvalidOperationException($"Pin {pinNumber} does not support mode {mode}."); + } + + _pins[pinNumber].SetPinMode(mode); + } + + /// + public override void Write(int pinNumber, PinValue value) + { + if (!IsPinOpen(pinNumber)) + { + throw new InvalidOperationException($"Can not write to pin {pinNumber} because it is not open."); + } + + _pins[pinNumber].Write(value); + } + + /// + public override void Toggle(int pinNumber) + { + if (!IsPinOpen(pinNumber)) + { + throw new InvalidOperationException($"Can not read from pin {pinNumber} because it is not open."); + } + + _pins[pinNumber].Toggle(); + } + + /// + public override void RegisterCallbackForPinValueChangedEvent(int pinNumber, PinEventTypes eventTypes, PinChangeEventHandler callback) + { + if (_callbackEvents.Count == 0) + { + _pins[pinNumber].ValueChanged += PinValueChanged; + } + + _callbackEvents.TryAdd(pinNumber, new PinEvent(eventTypes, callback)); + } + + private void PinValueChanged(object sender, PinValueChangedEventArgs pinValueChangedEventArgs) + { + // Get GpioPin associated with the real life pin + // Note that if we have multiple pins with the same pin number, we will only take the first one in the list + var pins = _pins.Where(m => m.Value.PinNumber == pinValueChangedEventArgs.PinNumber); + foreach (var pin in pins) + { + if (_callbackEvents.ContainsKey(pin.Key)) + { + var pinEvent = _callbackEvents[pin.Key]; + if (pinEvent == null) + { + return; + } + + if ((pinEvent.PinEventTypes & pinValueChangedEventArgs.ChangeType) != 0) + { + pinEvent.Callbacks?.Invoke(this, new PinValueChangedEventArgs(pinValueChangedEventArgs.ChangeType, pin.Key)); + } + + break; + } + } + } + + /// + public override void UnregisterCallbackForPinValueChangedEvent(int pinNumber, PinChangeEventHandler callback) + { + _callbackEvents.TryRemove(pinNumber, out _); + if (_callbackEvents.Count == 0) + { + _pins[pinNumber].ValueChanged -= PinValueChanged; + } + } + + /// + /// Gets a reference to an open pin + /// + /// The pin number + /// A instance representing the given pin + /// The pin number is invalid + public GpioPin GetOpenPin(int pinNumber) + { + if (_pins.TryGetValue(pinNumber, out var pin)) + { + return pin; + } + + throw new InvalidOperationException($"No such pin: {pinNumber}"); + } + + /// + public override WaitForEventResult WaitForEvent(int pinNumber, PinEventTypes eventTypes, CancellationToken cancellationToken) + { + var pin = _pins[pinNumber]; + return pin.OldController.WaitForEvent(pin.OldPinNumber, eventTypes, cancellationToken); + } + + /// + public override async ValueTask WaitForEventAsync(int pinNumber, PinEventTypes eventTypes, CancellationToken token) + { + var pin = _pins[pinNumber]; + return await pin.OldController.WaitForEventAsync(pin.OldPinNumber, eventTypes, token); + } + } +} diff --git a/src/devices/Gpio/Drivers/VirtualGpioPin.cs b/src/devices/Gpio/Drivers/VirtualGpioPin.cs new file mode 100644 index 0000000000..0b01cdc7bd --- /dev/null +++ b/src/devices/Gpio/Drivers/VirtualGpioPin.cs @@ -0,0 +1,105 @@ +// 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.Gpio; + +namespace Iot.Device.Gpio +{ + /// + /// A virtual GpioPin to allow creating a new GpioPin and a new pin number + /// + internal class VirtualGpioPin : GpioPin + { + private readonly GpioController _virtualController; + private readonly GpioPin _oldPin; + + /// + /// Create a virtual GPIO Pin from a real one + /// + /// The existing pin + /// The new pin number + /// The controller containing the virtual pin numbering + protected internal VirtualGpioPin(GpioPin oldPin, int newPinNumber, GpioController virtualController) + : base(newPinNumber, virtualController) + { + _virtualController = virtualController; + _oldPin = oldPin; + } + + private event PinChangeEventHandler? InternalValueChanged; + + internal int OldPinNumber => _oldPin.PinNumber; + + internal GpioController OldController => _oldPin.Controller; + + public override event PinChangeEventHandler ValueChanged + { + add + { + _oldPin.Controller.RegisterCallbackForPinValueChangedEvent(_oldPin.PinNumber, PinEventTypes.Falling | PinEventTypes.Rising, OldValueChanged); + InternalValueChanged += value; + } + + remove + { + _oldPin.Controller.UnregisterCallbackForPinValueChangedEvent(_oldPin.PinNumber, OldValueChanged); + InternalValueChanged -= value; + } + } + + private void OldValueChanged(object sender, PinValueChangedEventArgs pinvaluechangedeventargs) + { + if (pinvaluechangedeventargs.PinNumber == _oldPin.PinNumber) + { + // Forward the event, but with the new number + var toForward = new PinValueChangedEventArgs(pinvaluechangedeventargs.ChangeType, PinNumber); + InternalValueChanged?.Invoke(sender, toForward); + } + } + + public override PinMode GetPinMode() + { + return _oldPin.GetPinMode(); + } + + public override bool IsPinModeSupported(PinMode pinMode) + { + return _oldPin.IsPinModeSupported(pinMode); + } + + public override PinValue Read() + { + return _oldPin.Read(); + } + + public override void SetPinMode(PinMode value) + { + _oldPin.SetPinMode(value); + } + + public override void Toggle() + { + _oldPin.Toggle(); + } + + public override void Write(PinValue value) + { + _oldPin.Write(value); + } + + public override void Close() + { + _oldPin.Close(); + } + + public override bool Equals(GpioPin? other) + { + if (other is VirtualGpioPin otherPin) + { + return base.Equals(other) && _oldPin.Equals(otherPin._oldPin); + } + + return false; + } + } +} diff --git a/src/devices/Gpio/Gpio.sln b/src/devices/Gpio/Gpio.sln index a0409a1280..3900addd02 100644 --- a/src/devices/Gpio/Gpio.sln +++ b/src/devices/Gpio/Gpio.sln @@ -5,12 +5,20 @@ VisualStudioVersion = 17.0.32002.185 MinimumVisualStudioVersion = 15.0.26124.0 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{45EC0518-E2FF-4278-ABDB-E0A2D79E71BE}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Iot.Device.Gpio.Samples", "Iot.Device.Gpio.Samples.csproj", "{356BCA51-E8A9-4190-A814-70A6F87E33E1}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Iot.Device.Gpio", "Iot.Device.Gpio.csproj", "{B9CCBDB6-FAE3-49BF-B654-30698EF8131D}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Common", "..\Common\Common.csproj", "{52F56388-FBD6-4658-ABFC-161FBD79A5EA}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Iot.Device.Gpio.Samples", "samples\Iot.Device.Gpio.Samples.csproj", "{455A62CA-C3D1-40AA-BD70-985E14129A92}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.Device.Gpio", "..\..\System.Device.Gpio\System.Device.Gpio.csproj", "{CCEE7D99-484E-42F2-BF84-C6C82A0D44C4}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{9CF7DED7-33FE-4177-95CC-626CA3E2B5B2}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Gpio.Tests", "tests\Gpio.Tests.csproj", "{02FB3A2F-CA90-4ACA-8A83-F270E13BD07C}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Board", "..\Board\Board.csproj", "{BB45F1F6-25AB-43E4-945E-F2B9673EA684}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -21,18 +29,6 @@ Global Release|x86 = Release|x86 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution - {356BCA51-E8A9-4190-A814-70A6F87E33E1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {356BCA51-E8A9-4190-A814-70A6F87E33E1}.Debug|Any CPU.Build.0 = Debug|Any CPU - {356BCA51-E8A9-4190-A814-70A6F87E33E1}.Debug|x64.ActiveCfg = Debug|Any CPU - {356BCA51-E8A9-4190-A814-70A6F87E33E1}.Debug|x64.Build.0 = Debug|Any CPU - {356BCA51-E8A9-4190-A814-70A6F87E33E1}.Debug|x86.ActiveCfg = Debug|Any CPU - {356BCA51-E8A9-4190-A814-70A6F87E33E1}.Debug|x86.Build.0 = Debug|Any CPU - {356BCA51-E8A9-4190-A814-70A6F87E33E1}.Release|Any CPU.ActiveCfg = Release|Any CPU - {356BCA51-E8A9-4190-A814-70A6F87E33E1}.Release|Any CPU.Build.0 = Release|Any CPU - {356BCA51-E8A9-4190-A814-70A6F87E33E1}.Release|x64.ActiveCfg = Release|Any CPU - {356BCA51-E8A9-4190-A814-70A6F87E33E1}.Release|x64.Build.0 = Release|Any CPU - {356BCA51-E8A9-4190-A814-70A6F87E33E1}.Release|x86.ActiveCfg = Release|Any CPU - {356BCA51-E8A9-4190-A814-70A6F87E33E1}.Release|x86.Build.0 = Release|Any CPU {B9CCBDB6-FAE3-49BF-B654-30698EF8131D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {B9CCBDB6-FAE3-49BF-B654-30698EF8131D}.Debug|Any CPU.Build.0 = Debug|Any CPU {B9CCBDB6-FAE3-49BF-B654-30698EF8131D}.Debug|x64.ActiveCfg = Debug|Any CPU @@ -57,12 +53,61 @@ Global {52F56388-FBD6-4658-ABFC-161FBD79A5EA}.Release|x64.Build.0 = Release|Any CPU {52F56388-FBD6-4658-ABFC-161FBD79A5EA}.Release|x86.ActiveCfg = Release|Any CPU {52F56388-FBD6-4658-ABFC-161FBD79A5EA}.Release|x86.Build.0 = Release|Any CPU + {455A62CA-C3D1-40AA-BD70-985E14129A92}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {455A62CA-C3D1-40AA-BD70-985E14129A92}.Debug|Any CPU.Build.0 = Debug|Any CPU + {455A62CA-C3D1-40AA-BD70-985E14129A92}.Debug|x64.ActiveCfg = Debug|Any CPU + {455A62CA-C3D1-40AA-BD70-985E14129A92}.Debug|x64.Build.0 = Debug|Any CPU + {455A62CA-C3D1-40AA-BD70-985E14129A92}.Debug|x86.ActiveCfg = Debug|Any CPU + {455A62CA-C3D1-40AA-BD70-985E14129A92}.Debug|x86.Build.0 = Debug|Any CPU + {455A62CA-C3D1-40AA-BD70-985E14129A92}.Release|Any CPU.ActiveCfg = Release|Any CPU + {455A62CA-C3D1-40AA-BD70-985E14129A92}.Release|Any CPU.Build.0 = Release|Any CPU + {455A62CA-C3D1-40AA-BD70-985E14129A92}.Release|x64.ActiveCfg = Release|Any CPU + {455A62CA-C3D1-40AA-BD70-985E14129A92}.Release|x64.Build.0 = Release|Any CPU + {455A62CA-C3D1-40AA-BD70-985E14129A92}.Release|x86.ActiveCfg = Release|Any CPU + {455A62CA-C3D1-40AA-BD70-985E14129A92}.Release|x86.Build.0 = Release|Any CPU + {CCEE7D99-484E-42F2-BF84-C6C82A0D44C4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CCEE7D99-484E-42F2-BF84-C6C82A0D44C4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CCEE7D99-484E-42F2-BF84-C6C82A0D44C4}.Debug|x64.ActiveCfg = Debug|Any CPU + {CCEE7D99-484E-42F2-BF84-C6C82A0D44C4}.Debug|x64.Build.0 = Debug|Any CPU + {CCEE7D99-484E-42F2-BF84-C6C82A0D44C4}.Debug|x86.ActiveCfg = Debug|Any CPU + {CCEE7D99-484E-42F2-BF84-C6C82A0D44C4}.Debug|x86.Build.0 = Debug|Any CPU + {CCEE7D99-484E-42F2-BF84-C6C82A0D44C4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CCEE7D99-484E-42F2-BF84-C6C82A0D44C4}.Release|Any CPU.Build.0 = Release|Any CPU + {CCEE7D99-484E-42F2-BF84-C6C82A0D44C4}.Release|x64.ActiveCfg = Release|Any CPU + {CCEE7D99-484E-42F2-BF84-C6C82A0D44C4}.Release|x64.Build.0 = Release|Any CPU + {CCEE7D99-484E-42F2-BF84-C6C82A0D44C4}.Release|x86.ActiveCfg = Release|Any CPU + {CCEE7D99-484E-42F2-BF84-C6C82A0D44C4}.Release|x86.Build.0 = Release|Any CPU + {02FB3A2F-CA90-4ACA-8A83-F270E13BD07C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {02FB3A2F-CA90-4ACA-8A83-F270E13BD07C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {02FB3A2F-CA90-4ACA-8A83-F270E13BD07C}.Debug|x64.ActiveCfg = Debug|Any CPU + {02FB3A2F-CA90-4ACA-8A83-F270E13BD07C}.Debug|x64.Build.0 = Debug|Any CPU + {02FB3A2F-CA90-4ACA-8A83-F270E13BD07C}.Debug|x86.ActiveCfg = Debug|Any CPU + {02FB3A2F-CA90-4ACA-8A83-F270E13BD07C}.Debug|x86.Build.0 = Debug|Any CPU + {02FB3A2F-CA90-4ACA-8A83-F270E13BD07C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {02FB3A2F-CA90-4ACA-8A83-F270E13BD07C}.Release|Any CPU.Build.0 = Release|Any CPU + {02FB3A2F-CA90-4ACA-8A83-F270E13BD07C}.Release|x64.ActiveCfg = Release|Any CPU + {02FB3A2F-CA90-4ACA-8A83-F270E13BD07C}.Release|x64.Build.0 = Release|Any CPU + {02FB3A2F-CA90-4ACA-8A83-F270E13BD07C}.Release|x86.ActiveCfg = Release|Any CPU + {02FB3A2F-CA90-4ACA-8A83-F270E13BD07C}.Release|x86.Build.0 = Release|Any CPU + {BB45F1F6-25AB-43E4-945E-F2B9673EA684}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BB45F1F6-25AB-43E4-945E-F2B9673EA684}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BB45F1F6-25AB-43E4-945E-F2B9673EA684}.Debug|x64.ActiveCfg = Debug|Any CPU + {BB45F1F6-25AB-43E4-945E-F2B9673EA684}.Debug|x64.Build.0 = Debug|Any CPU + {BB45F1F6-25AB-43E4-945E-F2B9673EA684}.Debug|x86.ActiveCfg = Debug|Any CPU + {BB45F1F6-25AB-43E4-945E-F2B9673EA684}.Debug|x86.Build.0 = Debug|Any CPU + {BB45F1F6-25AB-43E4-945E-F2B9673EA684}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BB45F1F6-25AB-43E4-945E-F2B9673EA684}.Release|Any CPU.Build.0 = Release|Any CPU + {BB45F1F6-25AB-43E4-945E-F2B9673EA684}.Release|x64.ActiveCfg = Release|Any CPU + {BB45F1F6-25AB-43E4-945E-F2B9673EA684}.Release|x64.Build.0 = Release|Any CPU + {BB45F1F6-25AB-43E4-945E-F2B9673EA684}.Release|x86.ActiveCfg = Release|Any CPU + {BB45F1F6-25AB-43E4-945E-F2B9673EA684}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection GlobalSection(NestedProjects) = preSolution - {356BCA51-E8A9-4190-A814-70A6F87E33E1} = {45EC0518-E2FF-4278-ABDB-E0A2D79E71BE} + {455A62CA-C3D1-40AA-BD70-985E14129A92} = {45EC0518-E2FF-4278-ABDB-E0A2D79E71BE} + {02FB3A2F-CA90-4ACA-8A83-F270E13BD07C} = {9CF7DED7-33FE-4177-95CC-626CA3E2B5B2} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {3D905E48-D204-4FA4-AEF3-4B7AFC8047BC} diff --git a/src/devices/Gpio/Iot.Device.Gpio.csproj b/src/devices/Gpio/Iot.Device.Gpio.csproj index 5189701209..dbed440ee4 100644 --- a/src/devices/Gpio/Iot.Device.Gpio.csproj +++ b/src/devices/Gpio/Iot.Device.Gpio.csproj @@ -4,10 +4,12 @@ true latest enable + false - - - + + + + \ No newline at end of file diff --git a/src/devices/Gpio/README.md b/src/devices/Gpio/README.md index 9e701716c6..2623ab96fa 100644 --- a/src/devices/Gpio/README.md +++ b/src/devices/Gpio/README.md @@ -1,4 +1,4 @@ -# GpioDriver for other boards +# GpioDriver for other boards and virtual GpioController and virtual GpioPin This project contains some **full function(PULL-UP, PULL-DOWN)** generic GPIO drivers, and it can provide faster GPIO access. @@ -7,6 +7,26 @@ This project contains some **full function(PULL-UP, PULL-DOWN)** generic GPIO dr * For Allwinner SoCs: [SunxiDriver](Drivers/Sunxi/README.md) * For Rockchip SoCs: [RockchipDriver](Drivers/Rockchip/README.md) +## Usage of virtual GpioController and virtual GpioPin + +When you have multiple GpioControllers and want to use them together in a specific binding, the `VirtualGpioController` allows to create a GpioController from a set of `GpioPin`s already open from different controllers. Usage is straight forward and the class is fully compatible with a normal `GpioController`. + +```csharp +// Create pins from 2 different GpioControllers +var pinFromA = gpioControllerA.Open(42, PinMode.Output); +var pinFromB = gpioControllerB.Open(24, PinMode.Input); +var pinFromC = gpioControllerC.Open(12, PinMode.Input); +VirtualGpioController gpio = new(); +// pin number will be 0 in this case +gpio.Add(pinFromA); +// pin number will be 12 in this case +gpio.Add(12, pinFromB); +// the pin number will be 13 in this case +gpio.Add(pinFromC); +``` + +When adding an existing GpioPin, by default, the new pin allocation will directly follow the higher pin number already existing. You can also use this method to change the numbering of your board. Note that the VirtualGpioController is only operating in the logical numbering. + ## Board specific drivers | Board | Driver | diff --git a/src/devices/Gpio/samples/Program.cs b/src/devices/Gpio/samples/Program.cs index 5a06292d0c..fc97f33970 100644 --- a/src/devices/Gpio/samples/Program.cs +++ b/src/devices/Gpio/samples/Program.cs @@ -68,4 +68,4 @@ bool Debounce() } } } -} \ No newline at end of file +} diff --git a/src/devices/Gpio/tests/Gpio.Tests.csproj b/src/devices/Gpio/tests/Gpio.Tests.csproj new file mode 100644 index 0000000000..09c2d6fffe --- /dev/null +++ b/src/devices/Gpio/tests/Gpio.Tests.csproj @@ -0,0 +1,15 @@ + + + $(DefaultTestTfms) + false + latest + false + Iot.Device.Gpio.Tests + + + + + + + + diff --git a/src/devices/Gpio/tests/VirtualGpioTests.cs b/src/devices/Gpio/tests/VirtualGpioTests.cs new file mode 100644 index 0000000000..14a3c527a6 --- /dev/null +++ b/src/devices/Gpio/tests/VirtualGpioTests.cs @@ -0,0 +1,248 @@ +// 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.Gpio.Tests; +using System.Device.I2c; +using System.Device.Spi; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; +using Moq; +using Xunit; + +namespace Iot.Device.Gpio.Tests +{ + public class VirtualGpioTests : IDisposable + { + private readonly Mock _mockedGpioDriver; + private readonly GpioController _baseController; + + public VirtualGpioTests() + { + _mockedGpioDriver = new Mock(MockBehavior.Default); + _mockedGpioDriver.CallBase = true; + _baseController = new GpioController(_mockedGpioDriver.Object); + } + + public void Dispose() + { + _baseController.Dispose(); + _mockedGpioDriver.VerifyAll(); + } + + [Fact] + public void CreateSingleVirtualPin() + { + VirtualGpioController controller = new VirtualGpioController(); + var myPin = _baseController.OpenPin(1); + _mockedGpioDriver.Setup(x => x.IsPinModeSupportedEx(1, PinMode.Input)).Returns(true); + _mockedGpioDriver.Setup(x => x.ReadEx(1)).Returns(PinValue.High); + controller.Add(7, myPin); + controller.SetPinMode(7, PinMode.Input); + Assert.Equal(PinValue.High, controller.Read(7)); + } + + [Fact] + public void OpenClosePin() + { + VirtualGpioController controller = new VirtualGpioController(); + var myPin = _baseController.OpenPin(1); + controller.Add(7, myPin); + _mockedGpioDriver.Setup(x => x.ClosePinEx(1)).Verifiable(); + controller.ClosePin(7); + } + + [Fact] + public void UsePin() + { + VirtualGpioController controller = new VirtualGpioController(); + var myPin = _baseController.OpenPin(1); + controller.Add(7, myPin); + var newPin = controller.GetOpenPin(7); + Assert.NotNull(newPin); + _mockedGpioDriver.Setup(x => x.IsPinModeSupportedEx(1, PinMode.Input)).Returns(true); + _mockedGpioDriver.Setup(x => x.ReadEx(1)).Returns(PinValue.High); + newPin.SetPinMode(PinMode.Input); + Assert.Equal(PinValue.High, newPin.Read()); + } + + [Fact] + public void ClosePinDirectlyDoesNotThrow() + { + VirtualGpioController controller = new VirtualGpioController(); + var myPin = _baseController.OpenPin(1); + controller.Add(7, myPin); + _mockedGpioDriver.Setup(x => x.ClosePinEx(1)).Verifiable(); + controller.ClosePin(7); + // Also closing or disposing the original pin shouldn't throw + myPin.Close(); + myPin.Dispose(); + } + + [Fact] + public void ClosePinDirectlyDoesNotThrow2() + { + VirtualGpioController controller = new VirtualGpioController(); + var myPin = _baseController.OpenPin(1); + controller.Add(7, myPin); + _mockedGpioDriver.Setup(x => x.ClosePinEx(1)).Verifiable(); + // Close pin first, then close via controller + myPin.Close(); + myPin.Dispose(); + controller.ClosePin(7); + + Assert.Throws(() => controller.Read(7)); + } + + [Fact] + public void AddPinTwiceFails() + { + VirtualGpioController controller = new VirtualGpioController(); + var myPin = _baseController.OpenPin(1); + var myOtherPin = _baseController.OpenPin(2); + controller.Add(7, myPin); + Assert.False(controller.Add(7, myOtherPin)); + controller.Add(8, myOtherPin); + } + + [Fact] + public void CannotOpenPin() + { + VirtualGpioController controller = new VirtualGpioController(); + var myPin = _baseController.OpenPin(1); + controller.Add(2, myPin); + + // The exception thrown here is a bit odd, as we're going through the old GpioController + Assert.Throws(() => controller.OpenPin(2)); + } + + [Fact] + public void Callback1() + { + VirtualGpioController controller = new VirtualGpioController(); + var myPin = _baseController.OpenPin(1); + controller.Add(7, myPin); + var newPin = controller.GetOpenPin(7); + Assert.NotNull(newPin); + _mockedGpioDriver.Setup(x => x.IsPinModeSupportedEx(1, PinMode.Input)).Returns(true); + newPin.SetPinMode(PinMode.Input); + bool wasCalled = false; + int callbackAsNo = 0; + newPin.ValueChanged += (o, e) => + { + wasCalled = true; + callbackAsNo = e.PinNumber; + }; + + _mockedGpioDriver.Object.FireEventHandler(1, PinEventTypes.Rising); + Assert.True(wasCalled); + Assert.Equal(7, callbackAsNo); + } + + [Fact] + public void Callback2() + { + VirtualGpioController controller = new VirtualGpioController(); + var myPin = _baseController.OpenPin(1); + controller.Add(7, myPin); + _mockedGpioDriver.Setup(x => x.IsPinModeSupportedEx(1, PinMode.Input)).Returns(true); + controller.SetPinMode(7, PinMode.Input); + bool wasCalled = false; + int callbackAsNo = 0; + controller.RegisterCallbackForPinValueChangedEvent(7, PinEventTypes.Rising | PinEventTypes.Falling, (o, e) => + { + wasCalled = true; + callbackAsNo = e.PinNumber; + }); + + _mockedGpioDriver.Object.FireEventHandler(1, PinEventTypes.Rising); + Assert.True(wasCalled); + Assert.Equal(7, callbackAsNo); + } + + [Fact] + public void Callback3() + { + VirtualGpioController controller = new VirtualGpioController(); + var myPin = _baseController.OpenPin(1); + controller.Add(7, myPin); + _mockedGpioDriver.Setup(x => x.IsPinModeSupportedEx(1, PinMode.Input)).Returns(true); + controller.SetPinMode(7, PinMode.Input); + bool wasCalled = false; + int callbackAsNo = 0; + + void Callback(object o, PinValueChangedEventArgs e) + { + wasCalled = true; + callbackAsNo = e.PinNumber; + } + + controller.RegisterCallbackForPinValueChangedEvent(7, PinEventTypes.Falling, Callback); + + _mockedGpioDriver.Object.FireEventHandler(1, PinEventTypes.Rising); + // We expect no callback when we expect a Falling event but a Rising event is triggered + Assert.False(wasCalled); + controller.UnregisterCallbackForPinValueChangedEvent(7, Callback); + } + + [Fact] + public async Task WaitForEventAsync() + { + VirtualGpioController controller = new VirtualGpioController(); + var myPin = _baseController.OpenPin(1); + controller.Add(7, myPin); + _mockedGpioDriver.Setup(x => x.IsPinModeSupportedEx(1, PinMode.Input)).Returns(true); + controller.SetPinMode(7, PinMode.Input); + _mockedGpioDriver.Setup(x => x.WaitForEventEx(1, PinEventTypes.Rising | PinEventTypes.Falling, It.IsAny())) + .Returns(new WaitForEventResult() { EventTypes = PinEventTypes.Falling, TimedOut = false }); + var ret = controller.WaitForEventAsync(7, PinEventTypes.Rising | PinEventTypes.Falling, TimeSpan.FromMinutes(1)); + WaitForEventResult result = await ret; + Assert.False(result.TimedOut); + Assert.Equal(PinEventTypes.Falling, result.EventTypes); + } + + [Fact] + public void CtorCall() + { + var myPin1 = _baseController.OpenPin(99); + var myPin2 = _baseController.OpenPin(100); + VirtualGpioController controller = new VirtualGpioController(new GpioPin[] { myPin1, myPin2 }); + Assert.NotNull(controller.GetOpenPin(0)); + Assert.NotNull(controller.GetOpenPin(1)); + } + + [Fact] + public void CtorCall2() + { + var myPin1 = _baseController.OpenPin(99); + var myPin2 = _baseController.OpenPin(100); + VirtualGpioController controller = new VirtualGpioController(new Dictionary + { + { 5, myPin1 }, + { 9, myPin2 } + }); + Assert.NotNull(controller.GetOpenPin(5)); + Assert.NotNull(controller.GetOpenPin(9)); + Assert.Throws(() => controller.GetOpenPin(0)); + } + + [Fact] + public void QueryComponentInformation() + { + var myPin1 = _baseController.OpenPin(99); + var myPin2 = _baseController.OpenPin(100); + VirtualGpioController controller = new VirtualGpioController(new Dictionary + { + { 5, myPin1 }, + { 9, myPin2 } + }); + + var result = controller.QueryComponentInformation(); + Assert.NotNull(result); + Assert.NotEmpty(result.SubComponents); + } + } +}