Home icon indicating copy to clipboard operation
Home copied to clipboard

Make SpiDevice and I2cDevice abstract/virtual and make current implementations a subclass

Open CZEMacLeod opened this issue 7 months ago • 4 comments

Description

In order to support Bridge chips, such as an I2C to SPI converter, Onewire to I2C or SPI, or an I2C multiplexor (e.g. DS28E18, SC18IS606, or TCA9548A) the existing base classes should be abstract and the implementation details virtual. This would allow using any iot driver (e.g. Ssd1306) via one of these devices. It would also make the classes more closely align with the .NET IoT Libraries https://learn.microsoft.com/en-us/dotnet/api/system.device.spi.spidevice https://learn.microsoft.com/en-us/dotnet/api/system.device.spi.i2cdevice

How to solve the problem

Align the existing SpiDevice and I2cDevice with the https://github.com/dotnet/iot/ implementations

https://github.com/dotnet/iot/blob/main/src/System.Device.Gpio/System/Device/Spi/SpiDevice.cs https://github.com/dotnet/iot/blob/main/src/System.Device.Gpio/System/Device/I2c/I2cDevice.cs

Moving the existing implementations to a 'NativeXXXDeviceclass similar to theUnixXXXDevice` classes.

The static Create methods would simply return the NativeXXXDevice implementation.

https://github.com/nanoframework/System.Device.I2c/blob/ac5858c8be7d7b917854328ebc023c8bf66dbfee/System.Device.I2c/I2cDevice.cs#L119-L122

        public static I2cDevice Create(I2cConnectionSettings settings)
        {
            return new NativeI2cDevice(settings);
        }

https://github.com/nanoframework/System.Device.Spi/blob/96f0c1a031839eff2f003a490565b73e16abba39/System.Device.Spi/SpiDevice.cs#L159-L167

        public static SpiDevice Create(SpiConnectionSettings settings)
        {
            return new NativeSpiDevice(settings);
        }

Example for using an Ssd1306 via a TCA9548A

// TCA9538A connected to I2C1
// https://www.ti.com/lit/ds/symlink/tca9548a.pdf
var tca9548a = new Tca9538A(I2cDevice.Create(
        new I2cConnectionSettings(
            1, 
            Tca9538A.DefaultI2cAddress, 
            I2cBusSpeed.FastMode)))

// SSD1306 connected to SC5/SC5 output of TCA9548A
var ssd1306 = new Ssd1306(
    tca9548a.Create(
        new I2cConnectionSettings(
            5, 
            Ssd1306.DefaultI2cAddress, 
            I2cBusSpeed.FastMode)), 
    Ssd13xx.DisplayResolution.OLED128x64);

Describe alternatives you've considered

It would be possible to manually copy the existing IoT drivers required and adjust them to work over whatever expander was in use, but this would end up duplicating code where it is not required.

Aditional context

https://discord.com/channels/478725473862549535/709495312401694735/1390131333317132419

CZEMacLeod avatar Jul 03 '25 17:07 CZEMacLeod

A similar issue might be with GpioController where you can't easily make a derived implementation for e.g. Tca955x

CZEMacLeod avatar Jul 05 '25 22:07 CZEMacLeod

We're following the .NET IoT implementation. I understand the scenario you describe. And yes, because we do have the native elements, it doesn't make it super easy to reuse. That said with a derived class and using "new" on the function, you'll override the native class. Which should just do what you need. As an example:

class BaseClass
{
    public void Display()
    {
        Console.WriteLine("BaseClass Display");
    }
}

class DerivedClass : BaseClass
{
    public new void Display()
    {
        Console.WriteLine("DerivedClass Display");
    }
}

Even if the base class is not marked as virtual, the new keyword will just override it.

Ellerbach avatar Jul 07 '25 06:07 Ellerbach

The .NET IoT implementation is abstract and has a derived class that has the native implementation. Check the link to the dotnet/iot repository given.

/// <summary>
/// The communications channel to a device on a SPI bus.
/// </summary>
public abstract partial class SpiDevice : IDisposable
{
    /// <summary>
    /// Creates a communications channel to a device on a SPI bus running on the current hardware
    /// </summary>
    /// <param name="settings">The connection settings of a device on a SPI bus.</param>
    /// <returns>A communications channel to a device on a SPI bus.</returns>
    public static SpiDevice Create(SpiConnectionSettings settings)
    {
        if (Environment.OSVersion.Platform == PlatformID.Win32NT)
        {
            throw new PlatformNotSupportedException();
        }
        else
        {
            return new UnixSpiDevice(settings);
        }
    }

And in your example, if you pass DerivedClass to a function as BaseClass then calling Display will invoke the BaseClass method, unless nanoFramework has a bad implementation of the CLR.

var derived = new DerivedClass();
derived.Display();  // Works as expected.

var useClass = new UseClass(derived);
useClass.UseBaseClass(); // Will call the BaseClass method even when passed a derived class if the method is not virtual.

public class UseClass
{
    private BaseClass baseClass;

    public UseClass(BaseClass baseClass)
    {
        this.baseClass = baseClass;
    }

    public void UseBaseClass() => baseClass.Display();
}

public class BaseClass
{
    public void Display()
    {
        Console.WriteLine("BaseClass Display");
    }
}

public class DerivedClass : BaseClass
{
    public new void Display()
    {
        Console.WriteLine("DerivedClass Display");
    }
}

Thus my example will not work:

// Native I2C device
I2cDevice i2c1 = I2cDevice.Create(
        new I2cConnectionSettings(
            1, 
            Tca9538A.DefaultI2cAddress, 
            I2cBusSpeed.FastMode) );
// TCA9538A connected to I2C1
// https://www.ti.com/lit/ds/symlink/tca9548a.pdf
var tca9548a = new Tca9538A( i2c1 )

// Get I2C 5 from the multiplexor
I2cDevice i2c5 = tca9548a.Create(
        new I2cConnectionSettings(
            5, 
            Ssd1306.DefaultI2cAddress, 
            I2cBusSpeed.FastMode) );
// SSD1306 connected to SC5/SC5 output of TCA9548A
var ssd1306 = new Ssd1306(
    i2c5, 
    Ssd13xx.DisplayResolution.OLED128x64 );

CZEMacLeod avatar Jul 07 '25 07:07 CZEMacLeod

I started an implementation of this at https://github.com/CZEMacLeod/nanoFramework.System.Device.Spi There should be no compatibility issues when using SpiDevice unless you try and create it using the previously public constructor instead of the the static Create method.

CZEMacLeod avatar Jul 07 '25 16:07 CZEMacLeod