From 13e6fb857c80a634177c6c6646f9154b3f98f5d0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 2 Oct 2025 15:52:58 +0000 Subject: [PATCH 1/3] Initial plan From eb9a542dbb09840540a98c5316f1f861185fc6ca Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 2 Oct 2025 16:03:08 +0000 Subject: [PATCH 2/3] Convert _interruptPinsSubscribedEvents to ConcurrentDictionary and document lock usage Co-authored-by: krwq <660048+krwq@users.noreply.github.com> --- src/devices/Tca955x/Tca955x.cs | 84 +++++++++++++++++++++------------- 1 file changed, 51 insertions(+), 33 deletions(-) diff --git a/src/devices/Tca955x/Tca955x.cs b/src/devices/Tca955x/Tca955x.cs index bad6cb7462..c47c26af81 100644 --- a/src/devices/Tca955x/Tca955x.cs +++ b/src/devices/Tca955x/Tca955x.cs @@ -20,7 +20,7 @@ public abstract class Tca955x : GpioDriver private readonly int _interrupt; private readonly Dictionary _pinValues = new Dictionary(); private readonly ConcurrentDictionary _eventHandlers = new ConcurrentDictionary(); - private readonly Dictionary _interruptPinsSubscribedEvents = new Dictionary(); + private readonly ConcurrentDictionary _interruptPinsSubscribedEvents = new ConcurrentDictionary(); private readonly ConcurrentDictionary _interruptLastInputValues = new ConcurrentDictionary(); private GpioController? _controller; @@ -29,6 +29,10 @@ public abstract class Tca955x : GpioDriver private I2cDevice _busDevice; + // Lock protects: + // 1. I2C bus access - I2C operations must be atomic and sequential + // 2. _pinValues dictionary - tightly coupled with I2C read/write operations + // 3. Interrupt task coordination - _interruptProcessingTask and _interruptPending state private object _interruptHandlerLock = new object(); // This task processes the i2c reading of the io expander in a background task to @@ -89,13 +93,13 @@ protected Tca955x(I2cDevice device, int interrupt = -1, GpioController? gpioCont } if (_interrupt != -1) - { - // Initialise the interrupt handling state because ints may start coming from the INT pin - // on the expander as soon as we register the interrupt handler. - for (int i = 0; i < PinCount; i++) - { - _interruptPinsSubscribedEvents.Add(i, PinEventTypes.None); - _interruptLastInputValues.TryAdd(i, PinValue.Low); + { + // Initialise the interrupt handling state because ints may start coming from the INT pin + // on the expander as soon as we register the interrupt handler. + for (int i = 0; i < PinCount; i++) + { + _interruptPinsSubscribedEvents.TryAdd(i, PinEventTypes.None); + _interruptLastInputValues.TryAdd(i, PinValue.Low); } _shouldDispose = shouldDispose || gpioController is null; @@ -182,6 +186,7 @@ protected void InternalWriteByte(byte register, byte value) /// The mode to be set. protected override void SetPinMode(int pinNumber, PinMode mode) { + // Lock required: I2C bus operations must be atomic and sequential lock (_interruptHandlerLock) { if (mode != PinMode.Input && mode != PinMode.Output && mode != PinMode.InputPullUp) @@ -250,6 +255,7 @@ protected override void SetPinMode(int pinNumber, PinMode mode) protected override PinValue Read(int pinNumber) { PinValue pinValue; + // Lock required: I2C bus operations must be atomic, and _pinValues is updated during read lock (_interruptHandlerLock) { ValidatePin(pinNumber); @@ -272,6 +278,7 @@ protected override PinValue Read(int pinNumber) /// protected override void Read(Span pinValuePairs) { + // Lock required: I2C bus operations must be atomic, and _pinValues is updated during read lock (_interruptHandlerLock) { byte? lowReg = null; @@ -318,6 +325,7 @@ protected override void Read(Span pinValuePairs) /// The value to be written. protected override void Write(int pinNumber, PinValue value) { + // Lock required: I2C bus operations must be atomic, and _pinValues is updated during write lock (_interruptHandlerLock) { ValidatePin(pinNumber); @@ -338,6 +346,7 @@ protected override void Write(ReadOnlySpan pinValuePairs) bool lowChanged = false; bool highChanged = false; + // Lock required: I2C bus operations must be atomic, and _pinValues is updated during write lock (_interruptHandlerLock) { (uint mask, uint newBits) = new PinVector32(pinValuePairs); @@ -445,8 +454,11 @@ private void InterruptHandler(object sender, PinValueChangedEventArgs e) // i2c and detect a change in the returned input register data, not to mention run the // event handlers that the consumer of the library has signed up, we are likely // to miss edges while we are doing that anyway. Dropping interrupts in this - // case is the best we can do and prevents flooding the consumer with events + // case is the best we can do and prevents flooding the consumer with events // that could queue up in the INT gpio pin driver. + + // Lock required for task coordination: atomically check/start _interruptProcessingTask + // or set _interruptPending flag to ensure proper interrupt queueing lock (_interruptHandlerLock) { if (_interruptProcessingTask == null) @@ -461,10 +473,11 @@ private void InterruptHandler(object sender, PinValueChangedEventArgs e) } private Task ProcessInterruptInTask() - { - // Take a snapshot of the current interrupt pin configuration and last known input values - // so we can safely process them outside the lock in a background task. - var interruptPinsSnapshot = new Dictionary(_interruptPinsSubscribedEvents); + { + // Take a snapshot of the current interrupt pin configuration and last known input values + // so we can safely process them in a background task. ConcurrentDictionary enumeration + // is thread-safe and provides a consistent snapshot. + var interruptPinsSnapshot = new Dictionary(_interruptPinsSubscribedEvents); var interruptLastInputValuesSnapshot = new Dictionary(_interruptLastInputValues); Task processingTask = new Task(() => @@ -511,14 +524,16 @@ private Task ProcessInterruptInTask() processingTask.ContinueWith(t => { - lock (_interruptHandlerLock) - { - _interruptProcessingTask = null; - if (_interruptPending) - { - _interruptPending = false; - _interruptProcessingTask = ProcessInterruptInTask(); - } + // Lock required for task coordination: atomically check/update _interruptProcessingTask + // and _interruptPending to ensure only one processing task runs at a time + lock (_interruptHandlerLock) + { + _interruptProcessingTask = null; + if (_interruptPending) + { + _interruptPending = false; + _interruptProcessingTask = ProcessInterruptInTask(); + } } }); @@ -563,26 +578,29 @@ protected override void AddCallbackForPinValueChangedEvent(int pinNumber, PinEve throw new InvalidOperationException("No interrupt pin configured"); } + // Update subscription state using thread-safe ConcurrentDictionary operations + _interruptPinsSubscribedEvents[pinNumber] = eventType; + + // Read current value needs lock because it accesses I2C bus + PinValue currentValue; lock (_interruptHandlerLock) { - _interruptPinsSubscribedEvents[pinNumber] = eventType; - var currentValue = Read(pinNumber); - _interruptLastInputValues.TryUpdate(pinNumber, currentValue, !currentValue); - if (!_eventHandlers.TryAdd(pinNumber, callback)) - { - throw new InvalidOperationException($"An event handler is already registered for pin {pinNumber}"); - } + currentValue = Read(pinNumber); + } + + _interruptLastInputValues.TryUpdate(pinNumber, currentValue, !currentValue); + if (!_eventHandlers.TryAdd(pinNumber, callback)) + { + throw new InvalidOperationException($"An event handler is already registered for pin {pinNumber}"); } } /// protected override void RemoveCallbackForPinValueChangedEvent(int pinNumber, PinChangeEventHandler callback) { - lock (_interruptHandlerLock) - { - _eventHandlers.TryRemove(pinNumber, out _); - _interruptPinsSubscribedEvents[pinNumber] = PinEventTypes.None; - } + // Use thread-safe ConcurrentDictionary operations - no lock needed + _eventHandlers.TryRemove(pinNumber, out _); + _interruptPinsSubscribedEvents[pinNumber] = PinEventTypes.None; } /// From 400fe84bd238f42cc10eb2e7d731298c4366627a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 19 Dec 2025 14:09:02 +0000 Subject: [PATCH 3/3] Remove trailing whitespace to fix SA1028 build error Co-authored-by: krwq <660048+krwq@users.noreply.github.com> --- src/devices/Tca955x/Tca955x.cs | 1380 ++++++++++++++++---------------- 1 file changed, 690 insertions(+), 690 deletions(-) diff --git a/src/devices/Tca955x/Tca955x.cs b/src/devices/Tca955x/Tca955x.cs index 9ce8de39c7..f007572f47 100644 --- a/src/devices/Tca955x/Tca955x.cs +++ b/src/devices/Tca955x/Tca955x.cs @@ -1,690 +1,690 @@ -// 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.Concurrent; -using System.Collections.Generic; -using System.Device.Gpio; -using System.Device.I2c; -using System.Runtime.CompilerServices; -using System.Threading; -using System.Threading.Tasks; - -namespace Iot.Device.Tca955x -{ - /// - /// Base class for the Tca55x I2C I/O Expander - /// - public abstract class Tca955x : GpioDriver - { - private readonly int _interrupt; - private readonly Dictionary _pinValues = new Dictionary(); - private readonly ConcurrentDictionary _eventHandlers = new ConcurrentDictionary(); - private readonly ConcurrentDictionary _interruptPinsSubscribedEvents = new ConcurrentDictionary(); - private readonly ConcurrentDictionary _interruptLastInputValues = new ConcurrentDictionary(); - - private GpioController? _controller; - - private bool _shouldDispose; - - private I2cDevice _busDevice; - - // Lock protects: - // 1. I2C bus access - I2C operations must be atomic and sequential - // 2. _pinValues dictionary - tightly coupled with I2C read/write operations - // 3. Interrupt task coordination - _interruptProcessingTask and _interruptPending state - private object _interruptHandlerLock = new object(); - - // This task processes the i2c reading of the io expander in a background task to - // avoid blocking the interrupt handler for too long. While it is running, further - // incoming events for the Interrupt pin are ignored but if there are any, then we - // will read the device a second time just in case we missed the most current state. - // This is the best we can do given the limitations of the i2c bus speed and the fact that - // the device can clear and reassert the INT pin regardless of whether we have read - // the expander state over i2c. Bear in mind that the interrupt processing routine - // also synchronously calls out to any user event handlers that have been registered - // and they could take an arbitrary amount of time to complete. - private Task? _interruptProcessingTask = null; - // Set to true if an interrupt occurred while we were already processing one. - // If so, we need to do another read when we finish the current one. - private bool _interruptPending = false; - - private ushort _gpioOutputCache; - - /// - /// Default Address of the Tca955X Family. - /// - public const byte DefaultI2cAddress = 0x20; - - /// - /// Maximum number of addresses configurable via A0, A1, A2 pins. - /// - public const byte AddressRange = 7; - - /// - /// Represents the number of bits in a byte. - /// - public const int BitsPerByte = 8; - - /// - /// Constructor for the Tca9555 I2C I/O Expander. - /// - /// The I2C device used for communication. - /// The input pin number that is connected to the interrupt.Must be set together with the - /// The controller for the interrupt pin. Must be set together with the - /// True to dispose the when this object is disposed - /// True to skip checking the I2C address is in the valid range for the device. Only set this to true if you are using a compatible device with a different addresss scheme. - protected Tca955x(I2cDevice device, int interrupt = -1, GpioController? gpioController = null, bool shouldDispose = true, bool skipAddressCheck = false) - { - _busDevice = device; - _interrupt = interrupt; - - if (!skipAddressCheck && - (_busDevice.ConnectionSettings.DeviceAddress < DefaultI2cAddress || - _busDevice.ConnectionSettings.DeviceAddress > DefaultI2cAddress + AddressRange)) - { - throw new ArgumentOutOfRangeException(nameof(device), $"Address should be in Range {DefaultI2cAddress} to {DefaultI2cAddress + AddressRange} inclusive"); - } - - if ((_interrupt != -1 && gpioController is null) || - (_interrupt == -1 && (gpioController is not null))) - { - throw new ArgumentException("gpioController and interrupt must be set together."); - } - - if (_interrupt != -1) - { - // Initialise the interrupt handling state because ints may start coming from the INT pin - // on the expander as soon as we register the interrupt handler. - for (int i = 0; i < PinCount; i++) - { - _interruptPinsSubscribedEvents.TryAdd(i, PinEventTypes.None); - _interruptLastInputValues.TryAdd(i, PinValue.Low); - } - - _shouldDispose = shouldDispose || gpioController is null; - _controller = gpioController ?? new GpioController(); - if (!_controller.IsPinOpen(_interrupt)) - { - _controller.OpenPin(_interrupt); - } - - var currentIntPinMode = _controller.GetPinMode(_interrupt); - // The INT pin is open drain active low so we need to ensure it is configured as an input, - // but it could be configured with pullup or use an external pullup. - if (currentIntPinMode != PinMode.Input && currentIntPinMode != PinMode.InputPullUp) - { - _controller.SetPinMode(interrupt, PinMode.Input); - } - - // This is only be done once as there is only one INT pin interrupt for the entire ioexpander - _controller.RegisterCallbackForPinValueChangedEvent(_interrupt, PinEventTypes.Falling, InterruptHandler); - } - } - - /// - /// Reads a number of bytes from registers. - /// - /// The register to read from. - /// The buffer to read bytes into. - protected void InternalRead(byte register, Span buffer) - { - // First send write then read. - _busDevice.WriteRead(stackalloc byte[1] { register }, buffer); - } - - /// - /// Writes a number of bytes to registers. - /// - /// The register address to write to. - /// The data to write to the registers. - protected void InternalWrite(byte register, byte data) - { - _busDevice.Write(stackalloc byte[2] { register, data }); - } - - /// - /// Reads byte from the device register - /// - /// Register to read the value from - /// Byte read from the device register - protected byte InternalReadByte(byte register) - { - Span buffer = stackalloc byte[1]; - InternalRead(register, buffer); - return buffer[0]; - } - - /// - /// Write byte to device register - /// - /// Register to write the value to - /// Value to be written to the - protected void InternalWriteByte(byte register, byte value) - { - InternalWrite(register, value); - } - - /// - /// Read a byte from the given register. Use with caution as it does not synchronize with interrupts - /// - public byte ReadByte(byte register) => InternalReadByte(register); - - /// - /// Write a byte to the given register. Use with caution as it does not synchronize with interrupts - /// - public void WriteByte(byte register, byte value) => InternalWriteByte(register, value); - - private byte SetBit(byte data, int bitNumber) => (byte)(data | (1 << bitNumber)); - - private byte ClearBit(byte data, int bitNumber) => (byte)(data & ~(1 << bitNumber)); - - /// - /// Sets a mode to a pin. - /// - /// The pin number. - /// The mode to be set. - protected override void SetPinMode(int pinNumber, PinMode mode) - { - // Lock required: I2C bus operations must be atomic and sequential - lock (_interruptHandlerLock) - { - if (mode != PinMode.Input && mode != PinMode.Output && mode != PinMode.InputPullUp) - { - throw new ArgumentException("The Mcp controller supports the following pin modes: Input, Output and InputPullUp."); - } - - byte polarityInversionRegister = GetRegisterIndex(pinNumber, Register.PolarityInversionPort); - byte configurationRegister = GetRegisterIndex(pinNumber, Register.ConfigurationPort); - ValidatePin(pinNumber); - - byte value; - if (mode == PinMode.Output) - { - value = ClearBit(InternalReadByte(configurationRegister), GetBitNumber(pinNumber)); - } - else - { - value = SetBit(InternalReadByte(configurationRegister), GetBitNumber(pinNumber)); - } - - InternalWriteByte(configurationRegister, value); - - byte value2; - if (mode == PinMode.InputPullUp) - { - value2 = SetBit(InternalReadByte(polarityInversionRegister), GetBitNumber(pinNumber)); - } - else - { - value2 = ClearBit(InternalReadByte(polarityInversionRegister), GetBitNumber(pinNumber)); - } - - InternalWriteByte(polarityInversionRegister, value2); - } - } - - /// - /// Converts the pin number to the Register byte. - /// - /// The pin number. - /// The register byte. - /// The register byte. - protected abstract byte GetRegisterIndex(int pinNumber, Register registerType); - - /// - /// Converts the pin number to the Bit number. - /// - /// The pin number. - /// The bit position. - protected abstract int GetBitNumber(int pinNumber); - - /// - /// Mask the right byte from an input based on the pinnumber. - /// - /// The pin number. - /// The reding value of the inputs - /// The masked byte of the given value. - protected abstract byte GetByteRegister(int pinNumber, ushort value); - - /// - /// Reads the value of a pin. - /// - /// The pin number. - /// High or low pin value. - protected override PinValue Read(int pinNumber) - { - PinValue pinValue; - // Lock required: I2C bus operations must be atomic, and _pinValues is updated during read - lock (_interruptHandlerLock) - { - ValidatePin(pinNumber); - Span pinValuePairs = stackalloc PinValuePair[] - { - new PinValuePair(pinNumber, default) - }; - Read(pinValuePairs); - pinValue = _pinValues[pinNumber]; - } - - return pinValue; - } - - /// - protected override void Toggle(int pinNumber) => Write(pinNumber, !_pinValues[pinNumber]); - - /// - /// Reads the value of a set of pins - /// - protected override void Read(Span pinValuePairs) - { - // Lock required: I2C bus operations must be atomic, and _pinValues is updated during read - lock (_interruptHandlerLock) - { - byte? lowReg = null; - byte? highReg = null; - - for (int i = 0; i < pinValuePairs.Length; i++) - { - int pin = pinValuePairs[i].PinNumber; - PinValue value = PinValue.Low; - - if (pin >= 0 && pin < BitsPerByte) - { - if (lowReg == null) - { - lowReg = InternalReadByte(GetRegisterIndex(0, Register.InputPort)); - } - - value = (lowReg.Value & (1 << GetBitNumber(pin))) != 0 ? PinValue.High : PinValue.Low; - } - else if (PinCount > BitsPerByte && pin >= BitsPerByte && pin < 2 * BitsPerByte) - { - if (highReg == null) - { - highReg = InternalReadByte(GetRegisterIndex(BitsPerByte, Register.InputPort)); - } - - value = (highReg.Value & (1 << GetBitNumber(pin))) != 0 ? PinValue.High : PinValue.Low; - } - else - { - ThrowBadPin(nameof(pinValuePairs)); - } - - pinValuePairs[i] = new PinValuePair(pin, value); - _pinValues[pin] = value; - } - } - } - - /// - /// Writes a value to a pin. - /// - /// The pin number. - /// The value to be written. - protected override void Write(int pinNumber, PinValue value) - { - // Lock required: I2C bus operations must be atomic, and _pinValues is updated during write - lock (_interruptHandlerLock) - { - ValidatePin(pinNumber); - Span pinValuePairs = stackalloc PinValuePair[] - { - new PinValuePair(pinNumber, value) - }; - Write(pinValuePairs); - _pinValues[pinNumber] = value; - } - } - - /// - /// Writes values to a set of pins - /// - protected override void Write(ReadOnlySpan pinValuePairs) - { - bool lowChanged = false; - bool highChanged = false; - - // Lock required: I2C bus operations must be atomic, and _pinValues is updated during write - lock (_interruptHandlerLock) - { - (uint mask, uint newBits) = new PinVector32(pinValuePairs); - if ((mask >> PinCount) > 0) - { - ThrowBadPin(nameof(pinValuePairs)); - } - - ushort cachedValue = _gpioOutputCache; - ushort newValue = SetBits(cachedValue, (ushort)newBits, (ushort)mask); - if (cachedValue == newValue) - { - return; - } - - if (((mask & 0x00FF) != 0) && ((cachedValue & 0x00FF) != (newValue & 0x00FF))) - { - lowChanged = true; - } - - if (((mask & 0xFF00) != 0) && ((cachedValue & 0xFF00) != (newValue & 0xFF00))) - { - highChanged = true; - } - - _gpioOutputCache = newValue; - - if (lowChanged) - { - byte lowValue = GetByteRegister(0, newValue); - InternalWriteByte(GetRegisterIndex(0, Register.OutputPort), lowValue); - } - - if (highChanged) - { - byte highValue = GetByteRegister(BitsPerByte, newValue); - InternalWriteByte(GetRegisterIndex(BitsPerByte, Register.OutputPort), highValue); - } - - foreach (PinValuePair pinValuePair in pinValuePairs) - { - _pinValues[pinValuePair.PinNumber] = pinValuePair.PinValue; - } - } - } - - private ushort SetBits(ushort current, ushort bits, ushort mask) - { - current &= (ushort)~mask; - current |= bits; - return current; - } - - private void ValidatePin(int pinNumber) - { - if (pinNumber >= PinCount || pinNumber < 0) - { - ThrowBadPin(nameof(pinNumber)); - } - } - - private void ThrowBadPin(string argument) - { - throw new ArgumentOutOfRangeException(argument, $"Only pins {0} through {PinCount - 1} are valid."); - } - - /// - protected override void OpenPin(int pinNumber) - { - // No-op - if (!_pinValues.ContainsKey(pinNumber)) - { - _pinValues.Add(pinNumber, PinValue.Low); - } - } - - /// - protected override void ClosePin(int pinNumber) - { - // No-op - _pinValues.Remove(pinNumber); - } - - /// - protected override PinMode GetPinMode(int pinNumber) - { - ValidatePin(pinNumber); - - // IsBitSet returns true if bitNumber is flipped on in data. - bool IsBitSet(byte data, int bitNumber) => (data & (1 << bitNumber)) != 0; - - byte register = GetRegisterIndex(pinNumber, Register.ConfigurationPort); - return IsBitSet(InternalReadByte(register), GetBitNumber(pinNumber)) - ? PinMode.Input - : PinMode.Output; - } - - private void InterruptHandler(object sender, PinValueChangedEventArgs e) - { - // If the handler task is already running (not null), it means interrupts are being - // fired reentrantly and we are already processing an interrupt. - // OR we are reading/writing or configuring a pin. - // In this case record the missed interrupt, and return. - // We may miss an interrupt while busy, but because we have to slowly read the - // i2c and detect a change in the returned input register data, not to mention run the - // event handlers that the consumer of the library has signed up, we are likely - // to miss edges while we are doing that anyway. Dropping interrupts in this - // case is the best we can do and prevents flooding the consumer with events - // that could queue up in the INT gpio pin driver. - - // Lock required for task coordination: atomically check/start _interruptProcessingTask - // or set _interruptPending flag to ensure proper interrupt queueing - lock (_interruptHandlerLock) - { - if (_interruptProcessingTask == null) - { - _interruptProcessingTask = ProcessInterruptInTask(); - } - else - { - _interruptPending = true; - } - } - } - - private Task ProcessInterruptInTask() - { - // Take a snapshot of the current interrupt pin configuration and last known input values - // so we can safely process them in a background task. ConcurrentDictionary enumeration - // is thread-safe and provides a consistent snapshot. - var interruptPinsSnapshot = new Dictionary(_interruptPinsSubscribedEvents); - var interruptLastInputValuesSnapshot = new Dictionary(_interruptLastInputValues); - - Task processingTask = new Task(() => - { - if (interruptPinsSnapshot.Count > 0) - { - Span pinValuePairs = stackalloc PinValuePair[interruptPinsSnapshot.Count]; - int i = 0; - foreach (var kvp in interruptPinsSnapshot) - { - pinValuePairs[i++] = new PinValuePair(kvp.Key, default); - } - - Read(pinValuePairs); - - foreach (var pvp in pinValuePairs) - { - int pin = pvp.PinNumber; - PinValue newValue = pvp.PinValue; - PinValue lastValue = interruptLastInputValuesSnapshot[pin]; - var eventTypes = interruptPinsSnapshot[pin]; - - bool isRisingEdge = lastValue == PinValue.Low && newValue == PinValue.High; - bool isFallingEdge = lastValue == PinValue.High && newValue == PinValue.Low; - - if (eventTypes.HasFlag(PinEventTypes.Rising) && isRisingEdge) - { - CallHandlerOnPin(pin, PinEventTypes.Rising); - } - else if (eventTypes.HasFlag(PinEventTypes.Falling) && isFallingEdge) - { - CallHandlerOnPin(pin, PinEventTypes.Falling); - } - - interruptLastInputValuesSnapshot[pin] = newValue; - } - - foreach (var pin in interruptLastInputValuesSnapshot.Keys) - { - _interruptLastInputValues.TryUpdate(pin, interruptLastInputValuesSnapshot[pin], !interruptLastInputValuesSnapshot[pin]); - } - } - }); - - processingTask.ContinueWith(t => - { - // Lock required for task coordination: atomically check/update _interruptProcessingTask - // and _interruptPending to ensure only one processing task runs at a time - lock (_interruptHandlerLock) - { - _interruptProcessingTask = null; - if (_interruptPending) - { - _interruptPending = false; - _interruptProcessingTask = ProcessInterruptInTask(); - } - } - }); - - processingTask.Start(); - - return processingTask; - } - - /// - /// Calls the event handler for the given pin, if any. - /// - /// Pin to call the event handler on (if any exists) - /// What type of event led to calling this handler - private void CallHandlerOnPin(int pin, PinEventTypes pinEvent) - { - if (_eventHandlers.TryGetValue(pin, out var handler)) - { - handler.Invoke(this, new PinValueChangedEventArgs(pinEvent, pin)); - } - } - - /// - /// Calls an event handler if the given pin changes. - /// - /// Pin number to get callbacks for - /// Whether the handler should trigger on rising, falling or both edges - /// The method to call when an interrupt is triggered - /// There's no GPIO controller for the master interrupt configured, or no interrupt lines are configured for the - /// required port. - /// Only one event handler can be registered per pin. Calling this again with a different handler for the same pin replaces the handler - protected override void AddCallbackForPinValueChangedEvent(int pinNumber, PinEventTypes eventType, PinChangeEventHandler callback) - { - ValidatePin(pinNumber); - if (_controller == null) - { - // We could offer a polling solution here instead. - throw new InvalidOperationException("No GPIO controller available. Specify a GPIO controller and the relevant interrupt line numbers in the constructor"); - } - - if (_interrupt == -1) - { - throw new InvalidOperationException("No interrupt pin configured"); - } - - // Update subscription state using thread-safe ConcurrentDictionary operations - _interruptPinsSubscribedEvents[pinNumber] = eventType; - - // Read current value needs lock because it accesses I2C bus - PinValue currentValue; - lock (_interruptHandlerLock) - { - currentValue = Read(pinNumber); - } - - _interruptLastInputValues.TryUpdate(pinNumber, currentValue, !currentValue); - if (!_eventHandlers.TryAdd(pinNumber, callback)) - { - throw new InvalidOperationException($"An event handler is already registered for pin {pinNumber}"); - } - } - - /// - protected override void RemoveCallbackForPinValueChangedEvent(int pinNumber, PinChangeEventHandler callback) - { - // Use thread-safe ConcurrentDictionary operations - no lock needed - _eventHandlers.TryRemove(pinNumber, out _); - _interruptPinsSubscribedEvents[pinNumber] = PinEventTypes.None; - } - - /// - /// Waits for an event to occur on the given pin. - /// - /// The pin on which to wait - /// The event to wait for (rising, falling or either) - /// A timeout token - /// The wait result - /// This method can only be used on pins that are not otherwise used in event handling. - protected override WaitForEventResult WaitForEvent(int pinNumber, PinEventTypes eventTypes, - CancellationToken cancellationToken) - { - ManualResetEventSlim slim = new ManualResetEventSlim(); - slim.Reset(); - PinEventTypes eventTypes1 = PinEventTypes.None; - void InternalHandler(object sender, PinValueChangedEventArgs pinValueChangedEventArgs) - { - if (pinValueChangedEventArgs.PinNumber != pinNumber) - { - return; - } - - if ((pinValueChangedEventArgs.ChangeType & eventTypes) != 0) - { - slim.Set(); - } - - eventTypes1 = pinValueChangedEventArgs.ChangeType; - } - - AddCallbackForPinValueChangedEvent(pinNumber, eventTypes, InternalHandler); - slim.Wait(cancellationToken); - RemoveCallbackForPinValueChangedEvent(pinNumber, InternalHandler); - - if (cancellationToken.IsCancellationRequested) - { - return new WaitForEventResult() - { - EventTypes = PinEventTypes.None, - TimedOut = true - }; - } - - return new WaitForEventResult() - { - EventTypes = eventTypes1, - TimedOut = false - }; - } - - /// - protected override bool IsPinModeSupported(int pinNumber, PinMode mode) => - (mode == PinMode.Input || mode == PinMode.Output); - - /// - protected override void Dispose(bool disposing) - { - _controller?.UnregisterCallbackForPinValueChangedEvent(_interrupt, InterruptHandler); - _interruptPending = false; - - // Make a copy of the task refernce to avoid a race condition - // between checking it for null and then waiting on it. - var localinterruptProcessingTask = _interruptProcessingTask; - localinterruptProcessingTask?.Wait(); - - if (_shouldDispose) - { - _controller?.Dispose(); - _controller = null; - } - else - { - // We don't own the interrupt controller, so we must unregister our interrupt handler - if (_controller != null && _interrupt != -1) - { - _controller.UnregisterCallbackForPinValueChangedEvent(_interrupt, InterruptHandler); - } - } - - _busDevice?.Dispose(); - - base.Dispose(true); - } - } - -} +// 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.Concurrent; +using System.Collections.Generic; +using System.Device.Gpio; +using System.Device.I2c; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; + +namespace Iot.Device.Tca955x +{ + /// + /// Base class for the Tca55x I2C I/O Expander + /// + public abstract class Tca955x : GpioDriver + { + private readonly int _interrupt; + private readonly Dictionary _pinValues = new Dictionary(); + private readonly ConcurrentDictionary _eventHandlers = new ConcurrentDictionary(); + private readonly ConcurrentDictionary _interruptPinsSubscribedEvents = new ConcurrentDictionary(); + private readonly ConcurrentDictionary _interruptLastInputValues = new ConcurrentDictionary(); + + private GpioController? _controller; + + private bool _shouldDispose; + + private I2cDevice _busDevice; + + // Lock protects: + // 1. I2C bus access - I2C operations must be atomic and sequential + // 2. _pinValues dictionary - tightly coupled with I2C read/write operations + // 3. Interrupt task coordination - _interruptProcessingTask and _interruptPending state + private object _interruptHandlerLock = new object(); + + // This task processes the i2c reading of the io expander in a background task to + // avoid blocking the interrupt handler for too long. While it is running, further + // incoming events for the Interrupt pin are ignored but if there are any, then we + // will read the device a second time just in case we missed the most current state. + // This is the best we can do given the limitations of the i2c bus speed and the fact that + // the device can clear and reassert the INT pin regardless of whether we have read + // the expander state over i2c. Bear in mind that the interrupt processing routine + // also synchronously calls out to any user event handlers that have been registered + // and they could take an arbitrary amount of time to complete. + private Task? _interruptProcessingTask = null; + // Set to true if an interrupt occurred while we were already processing one. + // If so, we need to do another read when we finish the current one. + private bool _interruptPending = false; + + private ushort _gpioOutputCache; + + /// + /// Default Address of the Tca955X Family. + /// + public const byte DefaultI2cAddress = 0x20; + + /// + /// Maximum number of addresses configurable via A0, A1, A2 pins. + /// + public const byte AddressRange = 7; + + /// + /// Represents the number of bits in a byte. + /// + public const int BitsPerByte = 8; + + /// + /// Constructor for the Tca9555 I2C I/O Expander. + /// + /// The I2C device used for communication. + /// The input pin number that is connected to the interrupt.Must be set together with the + /// The controller for the interrupt pin. Must be set together with the + /// True to dispose the when this object is disposed + /// True to skip checking the I2C address is in the valid range for the device. Only set this to true if you are using a compatible device with a different addresss scheme. + protected Tca955x(I2cDevice device, int interrupt = -1, GpioController? gpioController = null, bool shouldDispose = true, bool skipAddressCheck = false) + { + _busDevice = device; + _interrupt = interrupt; + + if (!skipAddressCheck && + (_busDevice.ConnectionSettings.DeviceAddress < DefaultI2cAddress || + _busDevice.ConnectionSettings.DeviceAddress > DefaultI2cAddress + AddressRange)) + { + throw new ArgumentOutOfRangeException(nameof(device), $"Address should be in Range {DefaultI2cAddress} to {DefaultI2cAddress + AddressRange} inclusive"); + } + + if ((_interrupt != -1 && gpioController is null) || + (_interrupt == -1 && (gpioController is not null))) + { + throw new ArgumentException("gpioController and interrupt must be set together."); + } + + if (_interrupt != -1) + { + // Initialise the interrupt handling state because ints may start coming from the INT pin + // on the expander as soon as we register the interrupt handler. + for (int i = 0; i < PinCount; i++) + { + _interruptPinsSubscribedEvents.TryAdd(i, PinEventTypes.None); + _interruptLastInputValues.TryAdd(i, PinValue.Low); + } + + _shouldDispose = shouldDispose || gpioController is null; + _controller = gpioController ?? new GpioController(); + if (!_controller.IsPinOpen(_interrupt)) + { + _controller.OpenPin(_interrupt); + } + + var currentIntPinMode = _controller.GetPinMode(_interrupt); + // The INT pin is open drain active low so we need to ensure it is configured as an input, + // but it could be configured with pullup or use an external pullup. + if (currentIntPinMode != PinMode.Input && currentIntPinMode != PinMode.InputPullUp) + { + _controller.SetPinMode(interrupt, PinMode.Input); + } + + // This is only be done once as there is only one INT pin interrupt for the entire ioexpander + _controller.RegisterCallbackForPinValueChangedEvent(_interrupt, PinEventTypes.Falling, InterruptHandler); + } + } + + /// + /// Reads a number of bytes from registers. + /// + /// The register to read from. + /// The buffer to read bytes into. + protected void InternalRead(byte register, Span buffer) + { + // First send write then read. + _busDevice.WriteRead(stackalloc byte[1] { register }, buffer); + } + + /// + /// Writes a number of bytes to registers. + /// + /// The register address to write to. + /// The data to write to the registers. + protected void InternalWrite(byte register, byte data) + { + _busDevice.Write(stackalloc byte[2] { register, data }); + } + + /// + /// Reads byte from the device register + /// + /// Register to read the value from + /// Byte read from the device register + protected byte InternalReadByte(byte register) + { + Span buffer = stackalloc byte[1]; + InternalRead(register, buffer); + return buffer[0]; + } + + /// + /// Write byte to device register + /// + /// Register to write the value to + /// Value to be written to the + protected void InternalWriteByte(byte register, byte value) + { + InternalWrite(register, value); + } + + /// + /// Read a byte from the given register. Use with caution as it does not synchronize with interrupts + /// + public byte ReadByte(byte register) => InternalReadByte(register); + + /// + /// Write a byte to the given register. Use with caution as it does not synchronize with interrupts + /// + public void WriteByte(byte register, byte value) => InternalWriteByte(register, value); + + private byte SetBit(byte data, int bitNumber) => (byte)(data | (1 << bitNumber)); + + private byte ClearBit(byte data, int bitNumber) => (byte)(data & ~(1 << bitNumber)); + + /// + /// Sets a mode to a pin. + /// + /// The pin number. + /// The mode to be set. + protected override void SetPinMode(int pinNumber, PinMode mode) + { + // Lock required: I2C bus operations must be atomic and sequential + lock (_interruptHandlerLock) + { + if (mode != PinMode.Input && mode != PinMode.Output && mode != PinMode.InputPullUp) + { + throw new ArgumentException("The Mcp controller supports the following pin modes: Input, Output and InputPullUp."); + } + + byte polarityInversionRegister = GetRegisterIndex(pinNumber, Register.PolarityInversionPort); + byte configurationRegister = GetRegisterIndex(pinNumber, Register.ConfigurationPort); + ValidatePin(pinNumber); + + byte value; + if (mode == PinMode.Output) + { + value = ClearBit(InternalReadByte(configurationRegister), GetBitNumber(pinNumber)); + } + else + { + value = SetBit(InternalReadByte(configurationRegister), GetBitNumber(pinNumber)); + } + + InternalWriteByte(configurationRegister, value); + + byte value2; + if (mode == PinMode.InputPullUp) + { + value2 = SetBit(InternalReadByte(polarityInversionRegister), GetBitNumber(pinNumber)); + } + else + { + value2 = ClearBit(InternalReadByte(polarityInversionRegister), GetBitNumber(pinNumber)); + } + + InternalWriteByte(polarityInversionRegister, value2); + } + } + + /// + /// Converts the pin number to the Register byte. + /// + /// The pin number. + /// The register byte. + /// The register byte. + protected abstract byte GetRegisterIndex(int pinNumber, Register registerType); + + /// + /// Converts the pin number to the Bit number. + /// + /// The pin number. + /// The bit position. + protected abstract int GetBitNumber(int pinNumber); + + /// + /// Mask the right byte from an input based on the pinnumber. + /// + /// The pin number. + /// The reding value of the inputs + /// The masked byte of the given value. + protected abstract byte GetByteRegister(int pinNumber, ushort value); + + /// + /// Reads the value of a pin. + /// + /// The pin number. + /// High or low pin value. + protected override PinValue Read(int pinNumber) + { + PinValue pinValue; + // Lock required: I2C bus operations must be atomic, and _pinValues is updated during read + lock (_interruptHandlerLock) + { + ValidatePin(pinNumber); + Span pinValuePairs = stackalloc PinValuePair[] + { + new PinValuePair(pinNumber, default) + }; + Read(pinValuePairs); + pinValue = _pinValues[pinNumber]; + } + + return pinValue; + } + + /// + protected override void Toggle(int pinNumber) => Write(pinNumber, !_pinValues[pinNumber]); + + /// + /// Reads the value of a set of pins + /// + protected override void Read(Span pinValuePairs) + { + // Lock required: I2C bus operations must be atomic, and _pinValues is updated during read + lock (_interruptHandlerLock) + { + byte? lowReg = null; + byte? highReg = null; + + for (int i = 0; i < pinValuePairs.Length; i++) + { + int pin = pinValuePairs[i].PinNumber; + PinValue value = PinValue.Low; + + if (pin >= 0 && pin < BitsPerByte) + { + if (lowReg == null) + { + lowReg = InternalReadByte(GetRegisterIndex(0, Register.InputPort)); + } + + value = (lowReg.Value & (1 << GetBitNumber(pin))) != 0 ? PinValue.High : PinValue.Low; + } + else if (PinCount > BitsPerByte && pin >= BitsPerByte && pin < 2 * BitsPerByte) + { + if (highReg == null) + { + highReg = InternalReadByte(GetRegisterIndex(BitsPerByte, Register.InputPort)); + } + + value = (highReg.Value & (1 << GetBitNumber(pin))) != 0 ? PinValue.High : PinValue.Low; + } + else + { + ThrowBadPin(nameof(pinValuePairs)); + } + + pinValuePairs[i] = new PinValuePair(pin, value); + _pinValues[pin] = value; + } + } + } + + /// + /// Writes a value to a pin. + /// + /// The pin number. + /// The value to be written. + protected override void Write(int pinNumber, PinValue value) + { + // Lock required: I2C bus operations must be atomic, and _pinValues is updated during write + lock (_interruptHandlerLock) + { + ValidatePin(pinNumber); + Span pinValuePairs = stackalloc PinValuePair[] + { + new PinValuePair(pinNumber, value) + }; + Write(pinValuePairs); + _pinValues[pinNumber] = value; + } + } + + /// + /// Writes values to a set of pins + /// + protected override void Write(ReadOnlySpan pinValuePairs) + { + bool lowChanged = false; + bool highChanged = false; + + // Lock required: I2C bus operations must be atomic, and _pinValues is updated during write + lock (_interruptHandlerLock) + { + (uint mask, uint newBits) = new PinVector32(pinValuePairs); + if ((mask >> PinCount) > 0) + { + ThrowBadPin(nameof(pinValuePairs)); + } + + ushort cachedValue = _gpioOutputCache; + ushort newValue = SetBits(cachedValue, (ushort)newBits, (ushort)mask); + if (cachedValue == newValue) + { + return; + } + + if (((mask & 0x00FF) != 0) && ((cachedValue & 0x00FF) != (newValue & 0x00FF))) + { + lowChanged = true; + } + + if (((mask & 0xFF00) != 0) && ((cachedValue & 0xFF00) != (newValue & 0xFF00))) + { + highChanged = true; + } + + _gpioOutputCache = newValue; + + if (lowChanged) + { + byte lowValue = GetByteRegister(0, newValue); + InternalWriteByte(GetRegisterIndex(0, Register.OutputPort), lowValue); + } + + if (highChanged) + { + byte highValue = GetByteRegister(BitsPerByte, newValue); + InternalWriteByte(GetRegisterIndex(BitsPerByte, Register.OutputPort), highValue); + } + + foreach (PinValuePair pinValuePair in pinValuePairs) + { + _pinValues[pinValuePair.PinNumber] = pinValuePair.PinValue; + } + } + } + + private ushort SetBits(ushort current, ushort bits, ushort mask) + { + current &= (ushort)~mask; + current |= bits; + return current; + } + + private void ValidatePin(int pinNumber) + { + if (pinNumber >= PinCount || pinNumber < 0) + { + ThrowBadPin(nameof(pinNumber)); + } + } + + private void ThrowBadPin(string argument) + { + throw new ArgumentOutOfRangeException(argument, $"Only pins {0} through {PinCount - 1} are valid."); + } + + /// + protected override void OpenPin(int pinNumber) + { + // No-op + if (!_pinValues.ContainsKey(pinNumber)) + { + _pinValues.Add(pinNumber, PinValue.Low); + } + } + + /// + protected override void ClosePin(int pinNumber) + { + // No-op + _pinValues.Remove(pinNumber); + } + + /// + protected override PinMode GetPinMode(int pinNumber) + { + ValidatePin(pinNumber); + + // IsBitSet returns true if bitNumber is flipped on in data. + bool IsBitSet(byte data, int bitNumber) => (data & (1 << bitNumber)) != 0; + + byte register = GetRegisterIndex(pinNumber, Register.ConfigurationPort); + return IsBitSet(InternalReadByte(register), GetBitNumber(pinNumber)) + ? PinMode.Input + : PinMode.Output; + } + + private void InterruptHandler(object sender, PinValueChangedEventArgs e) + { + // If the handler task is already running (not null), it means interrupts are being + // fired reentrantly and we are already processing an interrupt. + // OR we are reading/writing or configuring a pin. + // In this case record the missed interrupt, and return. + // We may miss an interrupt while busy, but because we have to slowly read the + // i2c and detect a change in the returned input register data, not to mention run the + // event handlers that the consumer of the library has signed up, we are likely + // to miss edges while we are doing that anyway. Dropping interrupts in this + // case is the best we can do and prevents flooding the consumer with events + // that could queue up in the INT gpio pin driver. + + // Lock required for task coordination: atomically check/start _interruptProcessingTask + // or set _interruptPending flag to ensure proper interrupt queueing + lock (_interruptHandlerLock) + { + if (_interruptProcessingTask == null) + { + _interruptProcessingTask = ProcessInterruptInTask(); + } + else + { + _interruptPending = true; + } + } + } + + private Task ProcessInterruptInTask() + { + // Take a snapshot of the current interrupt pin configuration and last known input values + // so we can safely process them in a background task. ConcurrentDictionary enumeration + // is thread-safe and provides a consistent snapshot. + var interruptPinsSnapshot = new Dictionary(_interruptPinsSubscribedEvents); + var interruptLastInputValuesSnapshot = new Dictionary(_interruptLastInputValues); + + Task processingTask = new Task(() => + { + if (interruptPinsSnapshot.Count > 0) + { + Span pinValuePairs = stackalloc PinValuePair[interruptPinsSnapshot.Count]; + int i = 0; + foreach (var kvp in interruptPinsSnapshot) + { + pinValuePairs[i++] = new PinValuePair(kvp.Key, default); + } + + Read(pinValuePairs); + + foreach (var pvp in pinValuePairs) + { + int pin = pvp.PinNumber; + PinValue newValue = pvp.PinValue; + PinValue lastValue = interruptLastInputValuesSnapshot[pin]; + var eventTypes = interruptPinsSnapshot[pin]; + + bool isRisingEdge = lastValue == PinValue.Low && newValue == PinValue.High; + bool isFallingEdge = lastValue == PinValue.High && newValue == PinValue.Low; + + if (eventTypes.HasFlag(PinEventTypes.Rising) && isRisingEdge) + { + CallHandlerOnPin(pin, PinEventTypes.Rising); + } + else if (eventTypes.HasFlag(PinEventTypes.Falling) && isFallingEdge) + { + CallHandlerOnPin(pin, PinEventTypes.Falling); + } + + interruptLastInputValuesSnapshot[pin] = newValue; + } + + foreach (var pin in interruptLastInputValuesSnapshot.Keys) + { + _interruptLastInputValues.TryUpdate(pin, interruptLastInputValuesSnapshot[pin], !interruptLastInputValuesSnapshot[pin]); + } + } + }); + + processingTask.ContinueWith(t => + { + // Lock required for task coordination: atomically check/update _interruptProcessingTask + // and _interruptPending to ensure only one processing task runs at a time + lock (_interruptHandlerLock) + { + _interruptProcessingTask = null; + if (_interruptPending) + { + _interruptPending = false; + _interruptProcessingTask = ProcessInterruptInTask(); + } + } + }); + + processingTask.Start(); + + return processingTask; + } + + /// + /// Calls the event handler for the given pin, if any. + /// + /// Pin to call the event handler on (if any exists) + /// What type of event led to calling this handler + private void CallHandlerOnPin(int pin, PinEventTypes pinEvent) + { + if (_eventHandlers.TryGetValue(pin, out var handler)) + { + handler.Invoke(this, new PinValueChangedEventArgs(pinEvent, pin)); + } + } + + /// + /// Calls an event handler if the given pin changes. + /// + /// Pin number to get callbacks for + /// Whether the handler should trigger on rising, falling or both edges + /// The method to call when an interrupt is triggered + /// There's no GPIO controller for the master interrupt configured, or no interrupt lines are configured for the + /// required port. + /// Only one event handler can be registered per pin. Calling this again with a different handler for the same pin replaces the handler + protected override void AddCallbackForPinValueChangedEvent(int pinNumber, PinEventTypes eventType, PinChangeEventHandler callback) + { + ValidatePin(pinNumber); + if (_controller == null) + { + // We could offer a polling solution here instead. + throw new InvalidOperationException("No GPIO controller available. Specify a GPIO controller and the relevant interrupt line numbers in the constructor"); + } + + if (_interrupt == -1) + { + throw new InvalidOperationException("No interrupt pin configured"); + } + + // Update subscription state using thread-safe ConcurrentDictionary operations + _interruptPinsSubscribedEvents[pinNumber] = eventType; + + // Read current value needs lock because it accesses I2C bus + PinValue currentValue; + lock (_interruptHandlerLock) + { + currentValue = Read(pinNumber); + } + + _interruptLastInputValues.TryUpdate(pinNumber, currentValue, !currentValue); + if (!_eventHandlers.TryAdd(pinNumber, callback)) + { + throw new InvalidOperationException($"An event handler is already registered for pin {pinNumber}"); + } + } + + /// + protected override void RemoveCallbackForPinValueChangedEvent(int pinNumber, PinChangeEventHandler callback) + { + // Use thread-safe ConcurrentDictionary operations - no lock needed + _eventHandlers.TryRemove(pinNumber, out _); + _interruptPinsSubscribedEvents[pinNumber] = PinEventTypes.None; + } + + /// + /// Waits for an event to occur on the given pin. + /// + /// The pin on which to wait + /// The event to wait for (rising, falling or either) + /// A timeout token + /// The wait result + /// This method can only be used on pins that are not otherwise used in event handling. + protected override WaitForEventResult WaitForEvent(int pinNumber, PinEventTypes eventTypes, + CancellationToken cancellationToken) + { + ManualResetEventSlim slim = new ManualResetEventSlim(); + slim.Reset(); + PinEventTypes eventTypes1 = PinEventTypes.None; + void InternalHandler(object sender, PinValueChangedEventArgs pinValueChangedEventArgs) + { + if (pinValueChangedEventArgs.PinNumber != pinNumber) + { + return; + } + + if ((pinValueChangedEventArgs.ChangeType & eventTypes) != 0) + { + slim.Set(); + } + + eventTypes1 = pinValueChangedEventArgs.ChangeType; + } + + AddCallbackForPinValueChangedEvent(pinNumber, eventTypes, InternalHandler); + slim.Wait(cancellationToken); + RemoveCallbackForPinValueChangedEvent(pinNumber, InternalHandler); + + if (cancellationToken.IsCancellationRequested) + { + return new WaitForEventResult() + { + EventTypes = PinEventTypes.None, + TimedOut = true + }; + } + + return new WaitForEventResult() + { + EventTypes = eventTypes1, + TimedOut = false + }; + } + + /// + protected override bool IsPinModeSupported(int pinNumber, PinMode mode) => + (mode == PinMode.Input || mode == PinMode.Output); + + /// + protected override void Dispose(bool disposing) + { + _controller?.UnregisterCallbackForPinValueChangedEvent(_interrupt, InterruptHandler); + _interruptPending = false; + + // Make a copy of the task refernce to avoid a race condition + // between checking it for null and then waiting on it. + var localinterruptProcessingTask = _interruptProcessingTask; + localinterruptProcessingTask?.Wait(); + + if (_shouldDispose) + { + _controller?.Dispose(); + _controller = null; + } + else + { + // We don't own the interrupt controller, so we must unregister our interrupt handler + if (_controller != null && _interrupt != -1) + { + _controller.UnregisterCallbackForPinValueChangedEvent(_interrupt, InterruptHandler); + } + } + + _busDevice?.Dispose(); + + base.Dispose(true); + } + } + +}