// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.IO; using System.Linq; using System.Threading; namespace System.Device.Gpio.Drivers { /// /// A GPIO driver for Unix. /// public class SysFsDriver : UnixDriver { private const string GpioBasePath = "/sys/class/gpio"; private const string GpioChip = "gpiochip"; private const string GpioLabel = "/label"; private const string GpioContoller = "pinctrl"; private const string GpioOffsetBase = "/base"; private const int PollingTimeout = 50; private readonly CancellationTokenSource _eventThreadCancellationTokenSource; private readonly List _exportedPins = new List(); private readonly Dictionary _devicePins = new Dictionary(); private static readonly int s_pinOffset = ReadOffset(); private TimeSpan _statusUpdateSleepTime = TimeSpan.FromMilliseconds(1); private int _pollFileDescriptor = -1; private Thread? _eventDetectionThread; private int _pinsToDetectEventsCount; private static int ReadOffset() { IEnumerable fileNames = Directory.EnumerateFileSystemEntries(GpioBasePath); foreach (string name in fileNames) { if (name.Contains(GpioChip)) { try { if (File.ReadAllText($"{name}{GpioLabel}").StartsWith(GpioContoller, StringComparison.Ordinal)) { if (int.TryParse(File.ReadAllText($"{name}{GpioOffsetBase}"), out int pinOffset)) { return pinOffset; } } } catch (IOException) { // Ignoring file not found or any other IO exceptions as it is not guaranteed the folder would have files "label" "base" // And don't want to throw in this case just continue to load the gpiochip with default offset = 0 } } } return 0; } /// /// Initializes a new instance of the class. /// public SysFsDriver() { if (Environment.OSVersion.Platform != PlatformID.Unix) { throw new PlatformNotSupportedException($"{GetType().Name} is only supported on Linux/Unix."); } _eventThreadCancellationTokenSource = new CancellationTokenSource(); } /// /// The sleep time after an event occured and before the new value is read. /// internal TimeSpan StatusUpdateSleepTime { get { return _statusUpdateSleepTime; } set { _statusUpdateSleepTime = value; } } /// /// The number of pins provided by the driver. /// protected internal override int PinCount => throw new PlatformNotSupportedException("This driver is generic so it can not enumerate how many pins are available."); /// /// Converts a board pin number to the driver's logical numbering scheme. /// /// The board pin number to convert. /// The pin number in the driver's logical numbering scheme. protected internal override int ConvertPinNumberToLogicalNumberingScheme(int pinNumber) => throw new PlatformNotSupportedException("This driver is generic so it can not perform conversions between pin numbering schemes."); /// /// Opens a pin in order for it to be ready to use. /// /// The pin number in the driver's logical numbering scheme. protected internal override void OpenPin(int pinNumber) { int pinOffset = pinNumber + s_pinOffset; string pinPath = $"{GpioBasePath}/gpio{pinOffset}"; // If the directory exists, this becomes a no-op since the pin might have been opened already by the some controller or somebody else. if (!Directory.Exists(pinPath)) { try { File.WriteAllText(Path.Combine(GpioBasePath, "export"), pinOffset.ToString(CultureInfo.InvariantCulture)); SysFsHelpers.EnsureReadWriteAccessToPath(pinPath); _exportedPins.Add(pinNumber); } catch (UnauthorizedAccessException e) { // Wrapping the exception in order to get a better message. throw new UnauthorizedAccessException("Opening pins requires root permissions.", e); } } } /// /// Closes an open pin. /// /// The pin number in the driver's logical numbering scheme. protected internal override void ClosePin(int pinNumber) { int pinOffset = pinNumber + s_pinOffset; string pinPath = $"{GpioBasePath}/gpio{pinOffset}"; // If the directory doesn't exist, this becomes a no-op since the pin was closed already. if (Directory.Exists(pinPath)) { try { SetPinEventsToDetect(pinNumber, PinEventTypes.None); if (_devicePins.ContainsKey(pinNumber)) { _devicePins[pinNumber].Dispose(); _devicePins.Remove(pinNumber); } File.WriteAllText(Path.Combine(GpioBasePath, "unexport"), pinOffset.ToString(CultureInfo.InvariantCulture)); _exportedPins.Remove(pinNumber); } catch (UnauthorizedAccessException e) { throw new UnauthorizedAccessException("Closing pins requires root permissions.", e); } } } /// /// Sets the mode to a pin. /// /// The pin number in the driver's logical numbering scheme. /// The mode to be set. protected internal override void SetPinMode(int pinNumber, PinMode mode) { if (mode == PinMode.InputPullDown || mode == PinMode.InputPullUp) { throw new PlatformNotSupportedException("This driver is generic so it does not support Input Pull Down or Input Pull Up modes."); } string directionPath = $"{GpioBasePath}/gpio{pinNumber + s_pinOffset}/direction"; string sysFsMode = ConvertPinModeToSysFsMode(mode); if (File.Exists(directionPath)) { try { File.WriteAllText(directionPath, sysFsMode); } catch (UnauthorizedAccessException e) { throw new UnauthorizedAccessException("Setting a mode to a pin requires root permissions.", e); } } else { throw new InvalidOperationException("There was an attempt to set a mode to a pin that is not open."); } } private string ConvertPinModeToSysFsMode(PinMode mode) { if (mode == PinMode.Input) { return "in"; } if (mode == PinMode.Output) { return "out"; } throw new PlatformNotSupportedException($"{mode} is not supported by this driver."); } private PinMode ConvertSysFsModeToPinMode(string sysFsMode) { sysFsMode = sysFsMode.Trim(); if (sysFsMode == "in") { return PinMode.Input; } if (sysFsMode == "out") { return PinMode.Output; } throw new ArgumentException($"Unable to parse {sysFsMode} as a PinMode."); } /// /// Reads the current value of a pin. /// /// The pin number in the driver's logical numbering scheme. /// The value of the pin. protected internal override PinValue Read(int pinNumber) { PinValue result = default; string valuePath = $"{GpioBasePath}/gpio{pinNumber + s_pinOffset}/value"; if (File.Exists(valuePath)) { try { string valueContents = File.ReadAllText(valuePath); result = ConvertSysFsValueToPinValue(valueContents); } catch (UnauthorizedAccessException e) { throw new UnauthorizedAccessException("Reading a pin value requires root permissions.", e); } } else { throw new InvalidOperationException("There was an attempt to read from a pin that is not open."); } return result; } private PinValue ConvertSysFsValueToPinValue(string value) { return value.Trim() switch { "0" => PinValue.Low, "1" => PinValue.High, _ => throw new ArgumentException($"Invalid GPIO pin value {value}.") }; } /// /// Writes a value to a pin. /// /// The pin number in the driver's logical numbering scheme. /// The value to be written to the pin. protected internal override void Write(int pinNumber, PinValue value) { string valuePath = $"{GpioBasePath}/gpio{pinNumber + s_pinOffset}/value"; if (File.Exists(valuePath)) { try { string sysFsValue = ConvertPinValueToSysFs(value); File.WriteAllText(valuePath, sysFsValue); } catch (UnauthorizedAccessException e) { throw new UnauthorizedAccessException("Reading a pin value requires root permissions.", e); } } else { throw new InvalidOperationException("There was an attempt to write to a pin that is not open."); } } private string ConvertPinValueToSysFs(PinValue value) => value == PinValue.High ? "1" : "0"; /// /// Checks if a pin supports a specific mode. /// /// The pin number in the driver's logical numbering scheme. /// The mode to check. /// The status if the pin supports the mode. protected internal override bool IsPinModeSupported(int pinNumber, PinMode mode) { // Unix driver does not support pull up or pull down resistors. if (mode == PinMode.InputPullDown || mode == PinMode.InputPullUp) { return false; } return true; } /// /// Blocks execution until an event of type eventType is received or a cancellation is requested. /// /// The pin number in the driver's logical numbering scheme. /// The event types to wait for. Can be , or both. /// The cancellation token of when the operation should stop waiting for an event. /// A structure that contains the result of the waiting operation. protected internal override WaitForEventResult WaitForEvent(int pinNumber, PinEventTypes eventTypes, CancellationToken cancellationToken) { int pollFileDescriptor = -1; int valueFileDescriptor = -1; SetPinEventsToDetect(pinNumber, eventTypes); AddPinToPoll(pinNumber, ref valueFileDescriptor, ref pollFileDescriptor, out bool closePinValueFileDescriptor); bool eventDetected = WasEventDetected(pollFileDescriptor, valueFileDescriptor, out _, cancellationToken); if (_statusUpdateSleepTime > TimeSpan.Zero) { Thread.Sleep(_statusUpdateSleepTime); // Adding some delay to make sure that the value of the File has been updated so that we will get the right event type. } PinEventTypes detectedEventType = PinEventTypes.None; if (eventDetected) { // This is the only case where we need to read the new state. Although there are reports of this not being 100% reliable in all situations, // it seems to be working fine most of the time. if (eventTypes == (PinEventTypes.Rising | PinEventTypes.Falling)) { detectedEventType = (Read(pinNumber) == PinValue.High) ? PinEventTypes.Rising : PinEventTypes.Falling; } else if (eventTypes != PinEventTypes.None) { // If we're only waiting for one event type, we know which one it has to be detectedEventType = eventTypes; } } RemovePinFromPoll(pinNumber, ref valueFileDescriptor, ref pollFileDescriptor, closePinValueFileDescriptor, closePollFileDescriptor: true, cancelEventDetectionThread: false); return new WaitForEventResult { TimedOut = !eventDetected, EventTypes = detectedEventType, }; } private void SetPinEventsToDetect(int pinNumber, PinEventTypes eventTypes) { string edgePath = Path.Combine(GpioBasePath, $"gpio{pinNumber + s_pinOffset}", "edge"); // Even though the pin is open, we might sometimes need to wait for access SysFsHelpers.EnsureReadWriteAccessToPath(edgePath); string stringValue = PinEventTypeToStringValue(eventTypes); File.WriteAllText(edgePath, stringValue); } private PinEventTypes GetPinEventsToDetect(int pinNumber) { string edgePath = Path.Combine(GpioBasePath, $"gpio{pinNumber + s_pinOffset}", "edge"); // Even though the pin is open, we might sometimes need to wait for access SysFsHelpers.EnsureReadWriteAccessToPath(edgePath); string stringValue = File.ReadAllText(edgePath); return StringValueToPinEventType(stringValue); } private PinEventTypes StringValueToPinEventType(string value) { return value.Trim() switch { "none" => PinEventTypes.None, "both" => PinEventTypes.Falling | PinEventTypes.Rising, "rising" => PinEventTypes.Rising, "falling" => PinEventTypes.Falling, _ => throw new ArgumentException("Invalid pin event value.", value) }; } private string PinEventTypeToStringValue(PinEventTypes kind) { if (kind == PinEventTypes.None) { return "none"; } if ((kind & PinEventTypes.Falling) != 0 && (kind & PinEventTypes.Rising) != 0) { return "both"; } if (kind == PinEventTypes.Rising) { return "rising"; } if (kind == PinEventTypes.Falling) { return "falling"; } throw new ArgumentException("Invalid Pin Event Type.", nameof(kind)); } private void AddPinToPoll(int pinNumber, ref int valueFileDescriptor, ref int pollFileDescriptor, out bool closePinValueFileDescriptor) { if (pollFileDescriptor == -1) { pollFileDescriptor = Interop.epoll_create(1); if (pollFileDescriptor < 0) { throw new IOException("Error while trying to initialize pin interrupts (epoll_create failed)."); } } closePinValueFileDescriptor = false; if (valueFileDescriptor == -1) { string valuePath = Path.Combine(GpioBasePath, $"gpio{pinNumber + s_pinOffset}", "value"); valueFileDescriptor = Interop.open(valuePath, FileOpenFlags.O_RDONLY | FileOpenFlags.O_NONBLOCK); if (valueFileDescriptor < 0) { throw new IOException($"Error while trying to open pin value file {valuePath}."); } closePinValueFileDescriptor = true; } epoll_event epollEvent = new epoll_event { events = PollEvents.EPOLLIN | PollEvents.EPOLLET | PollEvents.EPOLLPRI, data = new epoll_data() { pinNumber = pinNumber } }; int result = Interop.epoll_ctl(pollFileDescriptor, PollOperations.EPOLL_CTL_ADD, valueFileDescriptor, ref epollEvent); if (result == -1) { throw new IOException("Error while trying to initialize pin interrupts (epoll_ctl failed)."); } // Ignore first time because it will always return the current state. Interop.epoll_wait(pollFileDescriptor, out _, 1, 0); } private unsafe bool WasEventDetected(int pollFileDescriptor, int valueFileDescriptor, out int pinNumber, CancellationToken cancellationToken) { char buf; IntPtr bufPtr = new IntPtr(&buf); pinNumber = -1; while (!cancellationToken.IsCancellationRequested) { // Wait until something happens int waitResult = Interop.epoll_wait(pollFileDescriptor, out epoll_event events, 1, PollingTimeout); if (waitResult == -1) { throw new IOException("Error while waiting for pin interrupts."); } if (waitResult > 0) { pinNumber = events.data.pinNumber; // This entire section is probably not necessary, but this seems to be hard to validate. // See https://github.com/dotnet/iot/pull/914#discussion_r389924106 and issue #1024. if (valueFileDescriptor == -1) { // valueFileDescriptor will be -1 when using the callback eventing. For WaitForEvent, the value will be set. valueFileDescriptor = _devicePins[pinNumber].FileDescriptor; } int lseekResult = Interop.lseek(valueFileDescriptor, 0, SeekFlags.SEEK_SET); if (lseekResult == -1) { throw new IOException("Error while trying to seek in value file."); } int readResult = Interop.read(valueFileDescriptor, bufPtr, 1); if (readResult != 1) { throw new IOException("Error while trying to read value file."); } return true; } } return false; } private void RemovePinFromPoll(int pinNumber, ref int valueFileDescriptor, ref int pollFileDescriptor, bool closePinValueFileDescriptor, bool closePollFileDescriptor, bool cancelEventDetectionThread) { epoll_event epollEvent = new epoll_event { events = PollEvents.EPOLLIN | PollEvents.EPOLLET | PollEvents.EPOLLPRI }; int result = Interop.epoll_ctl(pollFileDescriptor, PollOperations.EPOLL_CTL_DEL, valueFileDescriptor, ref epollEvent); if (result == -1) { throw new IOException("Error while trying to delete pin interrupts."); } if (closePinValueFileDescriptor) { Interop.close(valueFileDescriptor); valueFileDescriptor = -1; } if (closePollFileDescriptor) { if (cancelEventDetectionThread) { try { _eventThreadCancellationTokenSource.Cancel(); } catch (ObjectDisposedException) { } while (_eventDetectionThread != null && _eventDetectionThread.IsAlive) { Thread.Sleep(TimeSpan.FromMilliseconds(10)); // Wait until the event detection thread is aborted. } } Interop.close(pollFileDescriptor); pollFileDescriptor = -1; } } /// protected override void Dispose(bool disposing) { _pinsToDetectEventsCount = 0; if (_eventDetectionThread != null && _eventDetectionThread.IsAlive) { try { _eventThreadCancellationTokenSource.Cancel(); _eventThreadCancellationTokenSource.Dispose(); } catch (ObjectDisposedException) { // The Cancellation Token source may already be disposed. } while (_eventDetectionThread != null && _eventDetectionThread.IsAlive) { Thread.Sleep(TimeSpan.FromMilliseconds(10)); // Wait until the event detection thread is aborted. } } foreach (UnixDriverDevicePin devicePin in _devicePins.Values) { devicePin.Dispose(); } _devicePins.Clear(); if (_pollFileDescriptor != -1) { Interop.close(_pollFileDescriptor); _pollFileDescriptor = -1; } while (_exportedPins.Count > 0) { ClosePin(_exportedPins.FirstOrDefault()); } base.Dispose(disposing); } /// /// Adds a handler for a pin value changed event. /// /// The pin number in the driver's logical numbering scheme. /// The event types to wait for. /// Delegate that defines the structure for callbacks when a pin value changed event occurs. protected internal override void AddCallbackForPinValueChangedEvent(int pinNumber, PinEventTypes eventTypes, PinChangeEventHandler callback) { if (!_devicePins.ContainsKey(pinNumber)) { _devicePins.Add(pinNumber, new UnixDriverDevicePin(Read(pinNumber))); _pinsToDetectEventsCount++; AddPinToPoll(pinNumber, ref _devicePins[pinNumber].FileDescriptor, ref _pollFileDescriptor, out _); } if ((eventTypes & PinEventTypes.Rising) != 0) { _devicePins[pinNumber].ValueRising += callback; } if ((eventTypes & PinEventTypes.Falling) != 0) { _devicePins[pinNumber].ValueFalling += callback; } PinEventTypes events = (GetPinEventsToDetect(pinNumber) | eventTypes); SetPinEventsToDetect(pinNumber, events); // Remember which events are active _devicePins[pinNumber].ActiveEdges = events; InitializeEventDetectionThread(); } private void InitializeEventDetectionThread() { if (_eventDetectionThread == null) { _eventDetectionThread = new Thread(DetectEvents) { IsBackground = true }; _eventDetectionThread.Start(); } } private void DetectEvents() { while (_pinsToDetectEventsCount > 0) { try { bool eventDetected = WasEventDetected(_pollFileDescriptor, -1, out int pinNumber, _eventThreadCancellationTokenSource.Token); if (eventDetected) { if (_statusUpdateSleepTime > TimeSpan.Zero) { Thread.Sleep(_statusUpdateSleepTime); // Adding some delay to make sure that the value of the File has been updated so that we will get the right event type. } PinValue newValue = Read(pinNumber); UnixDriverDevicePin currentPin = _devicePins[pinNumber]; PinEventTypes activeEdges = currentPin.ActiveEdges; PinEventTypes eventType = activeEdges; PinEventTypes secondEvent = PinEventTypes.None; // Only if the active edges are both, we need to query the current state and guess about the change if (activeEdges == (PinEventTypes.Falling | PinEventTypes.Rising)) { PinValue oldValue = currentPin.LastValue; if (oldValue == PinValue.Low && newValue == PinValue.High) { eventType = PinEventTypes.Rising; } else if (oldValue == PinValue.High && newValue == PinValue.Low) { eventType = PinEventTypes.Falling; } else if (oldValue == PinValue.High) { // Both high -> There must have been a low-active peak eventType = PinEventTypes.Falling; secondEvent = PinEventTypes.Rising; } else { // Both low -> There must have been a high-active peak eventType = PinEventTypes.Rising; secondEvent = PinEventTypes.Falling; } currentPin.LastValue = newValue; } else { // Update the value, in case we need it later currentPin.LastValue = newValue; } PinValueChangedEventArgs args = new PinValueChangedEventArgs(eventType, pinNumber); currentPin.OnPinValueChanged(args); if (secondEvent != PinEventTypes.None) { args = new PinValueChangedEventArgs(secondEvent, pinNumber); currentPin.OnPinValueChanged(args); } } } catch (ObjectDisposedException) { break; // If cancellation token source is disposed then we need to exit this thread. } } _eventDetectionThread = null; } /// /// Removes a handler for a pin value changed event. /// /// The pin number in the driver's logical numbering scheme. /// Delegate that defines the structure for callbacks when a pin value changed event occurs. protected internal override void RemoveCallbackForPinValueChangedEvent(int pinNumber, PinChangeEventHandler callback) { if (!_devicePins.ContainsKey(pinNumber)) { throw new InvalidOperationException("Attempted to remove a callback for a pin that is not listening for events."); } _devicePins[pinNumber].ValueFalling -= callback; _devicePins[pinNumber].ValueRising -= callback; if (_devicePins[pinNumber].IsCallbackListEmpty()) { _pinsToDetectEventsCount--; bool closePollFileDescriptor = (_pinsToDetectEventsCount == 0); RemovePinFromPoll(pinNumber, ref _devicePins[pinNumber].FileDescriptor, ref _pollFileDescriptor, true, closePollFileDescriptor, true); _devicePins[pinNumber].Dispose(); _devicePins.Remove(pinNumber); } } /// /// Gets the mode of a pin. /// /// The pin number in the driver's logical numbering scheme. /// The mode of the pin. protected internal override PinMode GetPinMode(int pinNumber) { pinNumber += s_pinOffset; string directionPath = $"{GpioBasePath}/gpio{pinNumber}/direction"; if (File.Exists(directionPath)) { try { string sysFsMode = File.ReadAllText(directionPath); return ConvertSysFsModeToPinMode(sysFsMode); } catch (UnauthorizedAccessException e) { throw new UnauthorizedAccessException("Getting a mode to a pin requires root permissions.", e); } } else { throw new InvalidOperationException("There was an attempt to get a mode to a pin that is not open."); } } } }