namespace Unosquare.RaspberryIO.Gpio
{
using Native;
using Swan;
using System;
using System.Linq;
using System.Threading.Tasks;
///
/// Represents a GPIO Pin, its location and its capabilities.
/// Full pin reference available here:
/// http://pinout.xyz/pinout/pin31_gpio6 and http://wiringpi.com/pins/
///
public sealed partial class GpioPin
{
#region Property Backing
private readonly object _syncLock = new object();
private GpioPinDriveMode m_PinMode;
private GpioPinResistorPullMode m_ResistorPullMode;
private int m_PwmRegister;
private PwmMode m_PwmMode = PwmMode.Balanced;
private uint m_PwmRange = 1024;
private int m_PwmClockDivisor = 1;
private int m_SoftPwmValue = -1;
private int m_SoftToneFrequency = -1;
#endregion
#region Constructor
///
/// Initializes a new instance of the class.
///
/// The wiring pi pin number.
/// The header pin number.
private GpioPin(WiringPiPin wiringPiPinNumber, int headerPinNumber)
{
PinNumber = (int)wiringPiPinNumber;
WiringPiPinNumber = wiringPiPinNumber;
BcmPinNumber = GpioController.WiringPiToBcmPinNumber((int)wiringPiPinNumber);
HeaderPinNumber = headerPinNumber;
Header = (PinNumber >= 17 && PinNumber <= 20) ? GpioHeader.P5 : GpioHeader.P1;
}
#endregion
#region Pin Properties
///
/// Gets or sets the Wiring Pi pin number as an integer.
///
public int PinNumber { get; }
///
/// Gets the WiringPi Pin number
///
public WiringPiPin WiringPiPinNumber { get; }
///
/// Gets the BCM chip (hardware) pin number.
///
public int BcmPinNumber { get; }
///
/// Gets or the physical header (physical board) pin number.
///
public int HeaderPinNumber { get; }
///
/// Gets the pin's header (physical board) location.
///
public GpioHeader Header { get; }
///
/// Gets the friendly name of the pin.
///
public string Name { get; private set; }
///
/// Gets the hardware mode capabilities of this pin.
///
public PinCapability[] Capabilities { get; private set; }
#endregion
#region Hardware-Specific Properties
///
/// Gets or sets the pin operating mode.
///
///
/// The pin mode.
///
/// Thrown when a pin does not support the given operation mode.
public GpioPinDriveMode PinMode
{
get => m_PinMode;
set
{
lock (_syncLock)
{
var mode = value;
if ((mode == GpioPinDriveMode.GpioClock && Capabilities.Contains(PinCapability.GPCLK) == false) ||
(mode == GpioPinDriveMode.PwmOutput && Capabilities.Contains(PinCapability.PWM) == false) ||
(mode == GpioPinDriveMode.Input && Capabilities.Contains(PinCapability.GP) == false) ||
(mode == GpioPinDriveMode.Output && Capabilities.Contains(PinCapability.GP) == false))
{
throw new NotSupportedException(
$"Pin {WiringPiPinNumber} '{Name}' does not support mode '{mode}'. Pin capabilities are limited to: {string.Join(", ", Capabilities)}");
}
WiringPi.PinMode(PinNumber, (int)mode);
m_PinMode = mode;
}
}
}
///
/// Gets the interrupt callback. Returns null if no interrupt
/// has been registered.
///
public InterruptServiceRoutineCallback InterruptCallback { get; private set; }
///
/// Gets the interrupt edge detection mode.
///
public EdgeDetection InterruptEdgeDetection { get; private set; } = EdgeDetection.ExternalSetup;
#endregion
#region Hardware PWM Members
///
/// This sets or gets the pull-up or pull-down resistor mode on the pin, which should be set as an input.
/// Unlike the Arduino, the BCM2835 has both pull-up an down internal resistors.
/// The parameter pud should be; PUD_OFF, (no pull up/down), PUD_DOWN (pull to ground) or PUD_UP (pull to 3.3v)
/// The internal pull up/down resistors have a value of approximately 50KΩ on the Raspberry Pi.
///
public GpioPinResistorPullMode InputPullMode
{
get => PinMode == GpioPinDriveMode.Input ? m_ResistorPullMode : GpioPinResistorPullMode.Off;
set
{
lock (_syncLock)
{
if (PinMode != GpioPinDriveMode.Input)
{
m_ResistorPullMode = GpioPinResistorPullMode.Off;
throw new InvalidOperationException(
$"Unable to set the {nameof(InputPullMode)} for pin {PinNumber} because operating mode is {PinMode}."
+ $" Setting the {nameof(InputPullMode)} is only allowed if {nameof(PinMode)} is set to {GpioPinDriveMode.Input}");
}
WiringPi.PullUpDnControl(PinNumber, (int)value);
m_ResistorPullMode = value;
}
}
}
///
/// Gets or sets the PWM register. Values should be between 0 and 1024
///
///
/// The PWM register.
///
public int PwmRegister
{
get => m_PwmRegister;
set
{
lock (_syncLock)
{
if (PinMode != GpioPinDriveMode.PwmOutput)
{
m_PwmRegister = 0;
throw new InvalidOperationException(
$"Unable to write PWM register for pin {PinNumber} because operating mode is {PinMode}."
+ $" Writing the PWM register is only allowed if {nameof(PinMode)} is set to {GpioPinDriveMode.PwmOutput}");
}
var val = value.Clamp(0, 1024);
WiringPi.PwmWrite(PinNumber, val);
m_PwmRegister = val;
}
}
}
///
/// The PWM generator can run in 2 modes – “balanced” and “mark:space”. The mark:space mode is traditional,
/// however the default mode in the Pi is “balanced”.
///
///
/// The PWM mode.
///
/// When pin mode is not set a Pwn output
public PwmMode PwmMode
{
get => PinMode == GpioPinDriveMode.PwmOutput ? m_PwmMode : PwmMode.Balanced;
set
{
lock (_syncLock)
{
if (PinMode != GpioPinDriveMode.PwmOutput)
{
m_PwmMode = PwmMode.Balanced;
throw new InvalidOperationException(
$"Unable to set PWM mode for pin {PinNumber} because operating mode is {PinMode}."
+ $" Setting the PWM mode is only allowed if {nameof(PinMode)} is set to {GpioPinDriveMode.PwmOutput}");
}
WiringPi.PwmSetMode((int)value);
m_PwmMode = value;
}
}
}
///
/// This sets the range register in the PWM generator. The default is 1024.
///
///
/// The PWM range.
///
/// When pin mode is not set to PWM output
public uint PwmRange
{
get => PinMode == GpioPinDriveMode.PwmOutput ? m_PwmRange : 0;
set
{
lock (_syncLock)
{
if (PinMode != GpioPinDriveMode.PwmOutput)
{
m_PwmRange = 1024;
throw new InvalidOperationException(
$"Unable to set PWM range for pin {PinNumber} because operating mode is {PinMode}."
+ $" Setting the PWM range is only allowed if {nameof(PinMode)} is set to {GpioPinDriveMode.PwmOutput}");
}
WiringPi.PwmSetRange(value);
m_PwmRange = value;
}
}
}
///
/// Gets or sets the PWM clock divisor.
///
///
/// The PWM clock divisor.
///
/// When pin mode is not set to PWM output
public int PwmClockDivisor
{
get => PinMode == GpioPinDriveMode.PwmOutput ? m_PwmClockDivisor : 0;
set
{
lock (_syncLock)
{
if (PinMode != GpioPinDriveMode.PwmOutput)
{
m_PwmClockDivisor = 1;
throw new InvalidOperationException(
$"Unable to set PWM range for pin {PinNumber} because operating mode is {PinMode}."
+ $" Setting the PWM range is only allowed if {nameof(PinMode)} is set to {GpioPinDriveMode.PwmOutput}");
}
WiringPi.PwmSetClock(value);
m_PwmClockDivisor = value;
}
}
}
#endregion
#region Software Tone Members
///
/// Gets a value indicating whether this instance is in software based tone generator mode.
///
///
/// true if this instance is in soft tone mode; otherwise, false.
///
public bool IsInSoftToneMode => m_SoftToneFrequency >= 0;
///
/// Gets or sets the soft tone frequency. 0 to 5000 Hz is typical
///
///
/// The soft tone frequency.
///
/// When soft tones cannot be initialized on the pin
public int SoftToneFrequency
{
get => m_SoftToneFrequency;
set
{
lock (_syncLock)
{
if (IsInSoftToneMode == false)
{
var setupResult = WiringPi.SoftToneCreate(PinNumber);
if (setupResult != 0)
{
throw new InvalidOperationException(
$"Unable to initialize soft tone on pin {PinNumber}. Error Code: {setupResult}");
}
}
WiringPi.SoftToneWrite(PinNumber, value);
m_SoftToneFrequency = value;
}
}
}
#endregion
#region Software PWM Members
///
/// Gets a value indicating whether this pin is in software based PWM mode.
///
///
/// true if this instance is in soft PWM mode; otherwise, false.
///
public bool IsInSoftPwmMode => m_SoftPwmValue >= 0;
///
/// Gets or sets the software PWM value on the pin.
///
///
/// The soft PWM value.
///
/// StartSoftPwm
public int SoftPwmValue
{
get => m_SoftPwmValue;
set
{
lock (_syncLock)
{
if (IsInSoftPwmMode && value >= 0)
{
WiringPi.SoftPwmWrite(PinNumber, value);
m_SoftPwmValue = value;
}
else
{
throw new InvalidOperationException($"Software PWM requires a call to {nameof(StartSoftPwm)}.");
}
}
}
}
///
/// Gets the software PWM range used upon starting the PWM.
///
public int SoftPwmRange { get; private set; } = -1;
///
/// Starts the software based PWM on this pin.
///
/// The value.
/// The range.
/// When the pin does not suppoert PWM
/// StartSoftPwm
/// or
public void StartSoftPwm(int value, int range)
{
lock (_syncLock)
{
if (Capabilities.Contains(PinCapability.GP) == false)
throw new NotSupportedException($"Pin {PinNumber} does not support software PWM");
if (IsInSoftPwmMode)
throw new InvalidOperationException($"{nameof(StartSoftPwm)} has already been called.");
var startResult = WiringPi.SoftPwmCreate(PinNumber, value, range);
if (startResult == 0)
{
m_SoftPwmValue = value;
SoftPwmRange = range;
}
else
{
throw new InvalidOperationException(
$"Could not start software based PWM on pin {PinNumber}. Error code: {startResult}");
}
}
}
#endregion
#region Output Mode (Write) Members
///
/// Writes the specified pin value.
/// This method performs a digital write
///
/// The value.
public void Write(GpioPinValue value)
{
lock (_syncLock)
{
if (PinMode != GpioPinDriveMode.Output)
{
throw new InvalidOperationException(
$"Unable to write to pin {PinNumber} because operating mode is {PinMode}."
+ $" Writes are only allowed if {nameof(PinMode)} is set to {GpioPinDriveMode.Output}");
}
WiringPi.DigitalWrite(PinNumber, (int)value);
}
}
///
/// Writes the value asynchronously.
///
/// The value.
/// The awaitable task
public Task WriteAsync(GpioPinValue value) => Task.Run(() => { Write(value); });
///
/// Writes the specified bit value.
/// This method performs a digital write
///
/// if set to true [value].
public void Write(bool value)
=> Write(value ? GpioPinValue.High : GpioPinValue.Low);
///
/// Writes the specified bit value.
/// This method performs a digital write
///
/// The value.
///
/// The awaitable task
///
public Task WriteAsync(bool value) => Task.Run(() => { Write(value); });
///
/// Writes the specified value. 0 for low, any other value for high
/// This method performs a digital write
///
/// The value.
public void Write(int value) => Write(value != 0 ? GpioPinValue.High : GpioPinValue.Low);
///
/// Writes the specified value. 0 for low, any other value for high
/// This method performs a digital write
///
/// The value.
/// The awaitable task
public Task WriteAsync(int value) => Task.Run(() => { Write(value); });
///
/// Writes the specified value as an analog level.
/// You will need to register additional analog modules to enable this function for devices such as the Gertboard.
///
/// The value.
public void WriteLevel(int value)
{
lock (_syncLock)
{
if (PinMode != GpioPinDriveMode.Output)
{
throw new InvalidOperationException(
$"Unable to write to pin {PinNumber} because operating mode is {PinMode}."
+ $" Writes are only allowed if {nameof(PinMode)} is set to {GpioPinDriveMode.Output}");
}
WiringPi.AnalogWrite(PinNumber, value);
}
}
///
/// Writes the specified value as an analog level.
/// You will need to register additional analog modules to enable this function for devices such as the Gertboard.
///
/// The value.
/// The awaitable task
public Task WriteLevelAsync(int value) => Task.Run(() => { WriteLevel(value); });
#endregion
#region Input Mode (Read) Members
///
/// Wait for specific pin status
///
/// status to check
/// timeout to reach status
/// true/false
public bool WaitForValue(GpioPinValue status, int timeOutMillisecond)
{
if (PinMode != GpioPinDriveMode.Input)
{
throw new InvalidOperationException(
$"Unable to read from pin {PinNumber} because operating mode is {PinMode}."
+ $" Reads are only allowed if {nameof(PinMode)} is set to {GpioPinDriveMode.Input}");
}
var hrt = new HighResolutionTimer();
hrt.Start();
do
{
if (ReadValue() == status)
return true;
Pi.Timing.SleepMicroseconds(101); // 101 uses nanosleep as opposed to a loop.
}
while (hrt.ElapsedMilliseconds <= timeOutMillisecond);
return false;
}
///
/// Reads the digital value on the pin as a boolean value.
///
/// The state of the pin
public bool Read()
{
lock (_syncLock)
{
if (PinMode != GpioPinDriveMode.Input && PinMode != GpioPinDriveMode.Output)
{
throw new InvalidOperationException(
$"Unable to read from pin {PinNumber} because operating mode is {PinMode}."
+ $" Reads are only allowed if {nameof(PinMode)} is set to {GpioPinDriveMode.Input} or {GpioPinDriveMode.Output}");
}
return WiringPi.DigitalRead(PinNumber) != 0;
}
}
///
/// Reads the digital value on the pin as a boolean value.
///
/// The state of the pin
public Task ReadAsync() => Task.Run(() => Read());
///
/// Reads the digital value on the pin as a High or Low value.
///
/// The state of the pin
public GpioPinValue ReadValue()
=> Read() ? GpioPinValue.High : GpioPinValue.Low;
///
/// Reads the digital value on the pin as a High or Low value.
///
/// The state of the pin
public Task ReadValueAsync() => Task.Run(() => ReadValue());
///
/// Reads the analog value on the pin.
/// This returns the value read on the supplied analog input pin. You will need to register
/// additional analog modules to enable this function for devices such as the Gertboard,
/// quick2Wire analog board, etc.
///
/// The analog level
/// When the pin mode is not configured as an input.
public int ReadLevel()
{
lock (_syncLock)
{
if (PinMode != GpioPinDriveMode.Input)
{
throw new InvalidOperationException(
$"Unable to read from pin {PinNumber} because operating mode is {PinMode}."
+ $" Reads are only allowed if {nameof(PinMode)} is set to {GpioPinDriveMode.Input}");
}
return WiringPi.AnalogRead(PinNumber);
}
}
///
/// Reads the analog value on the pin.
/// This returns the value read on the supplied analog input pin. You will need to register
/// additional analog modules to enable this function for devices such as the Gertboard,
/// quick2Wire analog board, etc.
///
/// The analog level
public Task ReadLevelAsync() => Task.Run(() => ReadLevel());
#endregion
#region Interrupts
///
/// Registers the interrupt callback on the pin. Pin mode has to be set to Input.
///
/// The edge detection.
/// The callback.
/// callback
///
/// An interrupt callback was already registered.
/// or
/// RegisterInterruptCallback
///
public void RegisterInterruptCallback(EdgeDetection edgeDetection, InterruptServiceRoutineCallback callback)
{
if (callback == null)
throw new ArgumentException($"{nameof(callback)} cannot be null");
if (InterruptCallback != null)
throw new InvalidOperationException("An interrupt callback was already registered.");
if (PinMode != GpioPinDriveMode.Input)
{
throw new InvalidOperationException(
$"Unable to {nameof(RegisterInterruptCallback)} for pin {PinNumber} because operating mode is {PinMode}."
+ $" Calling {nameof(RegisterInterruptCallback)} is only allowed if {nameof(PinMode)} is set to {GpioPinDriveMode.Input}");
}
lock (_syncLock)
{
var registerResult = WiringPi.WiringPiISR(PinNumber, (int)edgeDetection, callback);
if (registerResult == 0)
{
InterruptEdgeDetection = edgeDetection;
InterruptCallback = callback;
}
else
{
HardwareException.Throw(nameof(GpioPin), nameof(RegisterInterruptCallback));
}
}
}
#endregion
}
}