dotnet / iot

This repo includes .NET Core implementations for various IoT boards, chips, displays and PCBs.
MIT License
2.16k stars 582 forks source link

Hardware abstraction - design conversation #878

Closed krwq closed 3 years ago

krwq commented 4 years ago

Moving side conversation from #857 https://github.com/dotnet/iot/issues/857#issuecomment-557268367 https://github.com/dotnet/iot/issues/857#issuecomment-561810527 Related: #874

This issue is created to talk about abstraction of hardware in some reasonable way. See above issue (at minimum comments above) to get some background on the problems and possible solution.

This is a very early and initial problem description so it's not complete yet.

cc: @pgrawehr

Initially we have though of isolating GPIO/PWM/I2C/SPI as separate entities and we have shipped that design but more we are exploring the hardware the more we find that GPIO is not exactly separate. I expect this separation to create more and more issues which will be hard to debug or fix in the future if we don't start to think about them right now.

Here is some broad thought dump into what I think should be covered here

pgrawehr commented 4 years ago

Additional points:

A good solution should also come up with a generic approach of detecting the hardware (or offer connection parameters for virtual devices), see #809

pgrawehr commented 4 years ago

Some devices may support features we haven't even considered yet, such as AnalogRead() on an Arduino board.

Of course an API that is designed like i.e. WMI, where all method names/parameters are passed as string arrays, would be the most flexible, but also the most error-prone and difficult to use.

Ellerbach commented 4 years ago

And you can add on top specific hat like GovePi or GoPiGo3 which add their own I2C or provide their own GPIO where you connect sensors on them. Great example is GovePi. Every sensor you connect for the basic digital ones are working the same way as a generic GPIO controller but as soon as you move forward with advance ones like DHT sensor, then they have their own logic. I renctly had to fix an issue on this specific DHT sensor.

pgrawehr commented 4 years ago

Looks like we need some kind of abstract connection entity. I.e. we want to use the same code for accessing a DHT sensor, whether it's directly connected to the Pi, uses a GroovePi, an attached SPI GPIO expander or a remote Arduino. Tunnelling connections trough other devices should be transparent.

Ellerbach commented 4 years ago

Looks like we need some kind of abstract connection entity

Yes, where it is possible. In the example of the DHT sensor attached to GrovePi, all is done into the GRovePi, you just get back the temp and humidity. And the way to call it is very different from the other cases. Now, if we plug a DHT and don't want to use the feature that is avaible thru the GRovePi, yes, we want the various actions to happen in a transparent way. My guess now I've been implementing quite some hats is that it may be ok in some of them but for some we will have limitation and it will make things very complicated vs what we may gain doing it. Let's try with couple of example, DHT is a good one btw!

pgrawehr commented 4 years ago

The DHT is an example of a device that has very different ways of connecting to it, although the relevant data is very small (just two words from each read operation). One can:

Abstraction of device capabilities

Currently, the class Iot.Device.DHTxx.DHTBase is trying to wrap this in an abstract way, (except for the GroovePi, which is separate), so once the class is created, the connection doesn't matter and one can just read from the device. The CharacterLcd classes use a similar approach, and any higher-level interfaces (see my PR #859 for an example) do not need to know the transport layer. So the second part (abstraction of the device capabilities) isn't handled to bad right now in many places.

Abstraction of connection

Currently more limiting (I think) are the ways the devices are to be set up, especially also because the library has no way of giving hints (either trough syntax or through hardware checks) that what the users does is correct. There's an error if you try to write to I2C but nobody is there, but that you didn't enable the driver or configure the pins isn't told. Also, if you want to connect a "normal, of the shelf" DHT to one of the GroovePi digital pins, it won't easily be possible because GroovePi doesn't implement GpioController. (Oh, and DhtBase doesn't offer a constructor taking one).

So every board device (Raspi, GroovePi, Arduino) should provide as many common interfaces as possible (i.e. IGpioController, II2cBusProvider) and each I/O device (sensors, actors) should take the corresponding connection interface (maybe even IDigitalPin or IAnalogPin?)

shaggygi commented 4 years ago

So every board device (Raspi, GroovePi, Arduino) should provide as many common interfaces as possible (i.e. IGpioController, II2cBusProvider) and each I/O device (sensors, actors) should take the corresponding connection interface (maybe even IDigitalPin or IAnalogPin?)

Referencing... #125, #215

Ellerbach commented 4 years ago

GroovePi doesn't implement GpioController

GroovePi can implement a GpioController class. That's totally possible. But for this case, as the DHT processing is done into the controller itself, using the potential GpioController for it will be much less efficient than using the embedded function. You can see GrovePi as a remote Arduino, it's about the same for the basic functions. Then you have advanced one like DHT and few others. It's clear that for many sensors, having an abstraction calss and a transport will work just perfectly. What's missing is today an equivalent of GpioController for Analogic. I open #255 quite some time ago on having this. It's been open as well in various other ways. I understand that it's not easy as not all ADC controllers working the same way. Binary is so easy compare to analogic.

krwq commented 4 years ago

@Ellerbach in my head GpioController is related to only binary operations and AnalogRead/AnalogWrite is equivalent to PwmChannel and what I think we are lacking is abstraction for "board" (any hardware which composes other pieces of hardware) which would collect all such utilities. Other way we could go about it is to make GpioController a board abstraction and create a separate one for binary GPIO but I think at this stage it is easier to go with adding a board abstraction.

Board abstraction suggest also we should have abstraction for sensor since such board should probably allow listing all of them and I think we should brainstorm a bit on how to group those concepts with making minimal/no changes to the current abstraction.

Ellerbach commented 4 years ago

GpioController is related to only binary operations

Yes, that's correct, fully agree.

AnalogRead/AnalogWrite is equivalent to PwmChannel

Not realy, an analogic read/write is a tension apply to a pin between 0 and a voltage reference. a PWM is a modulation of frequency where you write 0 or the voltage reference which then emulate a real analogic write. Analogic read is really about reading a voltage between 0 and a voltage reference. I would create something similar at GpioController like AnalogController where we will have pin, ability to read, write them exactly like the GpioController but where you can specify the voltage reference and the ADC range (usually express in bits or as a divider). A lot of analogic sensors needs to have both to compute correctly their values. GrovePi is acting like an analog controller, there are few sensors as example. And also keep in mind that all Analog pins can be used as digital ones, so as normal GPIO!

I think we should brainstorm a bit on how to group those concepts with making minimal/no changes to the current abstraction.

Fully agree!

shaggygi commented 4 years ago

I would create something similar at GpioController like AnalogController where we will have pin, ...

Referencing... #90

RPi doesn't have ADC pins (I believe), but other boards like Beagle Bone Black does. You could also use AdcController for bindings like Mcp3xxx.

Ellerbach commented 4 years ago

RPi doesn't have ADC pins (I believe)

No, there is no ADC.

Beagle Bone Black does

Yes, some do. And again with a shield or like with the Mcp3xxx, you get it.

So wroth having one which does standardize all those approaches. That would make life easier to build the associated sensors.

Referencing... #90

Forgot about this discussion :-) At some point, we may finally have one :-D

pgrawehr commented 4 years ago

Picking this up again.

There are a lot of requirements here, but I think one of the central ones is that we want to standardize the interfaces of components as much as possible, so that things can be joined together at will. Any device that requires a digital pin to work (ie a simple LED, or a DHT sensor) should take a GpioController interface [maybe even an IDigialPin?), but where this comes from should not matter (on-board on a Raspi, a remote Arduino, an MCP23xxx port expander, etc).

Any device offering such capabilities should implement the respective interfaces. I couldn't follow the above mentioned discussion in #523 on some problems with that, but it is the exception that proves the rule, anyway 😃 . We could bring together a set of components for which this is not implemented as expected (Dht1 as an example - I'll later try whether I get it to work over an MCP23017)

The second question, basically independent, is how the methods to create the sub-interfaces (i.e. a GpioController from an MCP23xxxx or an I2cController from an MPU9250) should be standardized. There are basically two possibilities, as mentioned above:

While the second possibility would match more closely the current way of using the library, the first has a few advantages, one being that the board can handle conflicting requests. Also, it's not clear why the GpioController should be the master component for a Raspi, and having a CreateI2cBus() method on a GpioController seems strange.

krwq commented 4 years ago

There's a "Board" (base) class or maybe interface that provides methods to create controllers or other connections to other boards.

This sounds almost actionable right now, the only question is how would that interface look like, perhaps something like:

public abstract class Board
{
  public static Board GetBestBoardDriver() { ... }
  public static Board GetBoardDriver(Type type) { ... } // no idea what other arg would it take to combine with enumerating boards
  public static IEnumerable<Type> GetAvailableBoardDriverTypes() { ... }
  public GpioController CreateController(/* TODO:  */) { ... }
  public PwmChannel CreatePwmChannel(int channelId /* TBD */) { ... }
  public IEnumerable<int> GetAvailablePwmChannels() { ... }
  // ...
  // TODO: How to list measurements like CPU temperature or other things (see SenseHat as example which is also a board)
  public IEnumerable<Board> GetSubBoards() { ... } // i.e. SenseHat would be a subboard of RaspberryPI, name TBD
  // TODO: some way of registering boards when creating new board/subboard drivers
  // ...
}
shaggygi commented 4 years ago

Couple of brief thoughts...

public static Board GetBestBoardDriver() { ... }

This is a little confusing as the method states to get a driver, but returns a Board.

public static Board GetBoardDriver(Type type)

Same as above. Board vs Driver?

public IEnumerable GetSubBoards() { ... }

Would GetConnectedBoards be better naming?

krwq commented 4 years ago

In my head "driver" is just a code which allows you to use something which in context of naming is just added because it sounds better but is not the actual thing - similarly as "binding", "implementation" and many other words. Perhaps I'm missing something :roll_eyes:

Would GetConnectedBoards be better naming?

perhaps, no strong feeling either way. Connected sounds a bit too specific to me - i.e. in case of something being on the same PCB vs something connected more flexible way like some kind of connector. Connected for me suggest the latter although both are technically speaking connected. To me board has a components which can have another components and board might be a component as well

pgrawehr commented 4 years ago

This is looking quite good. I also don't really know about GetBestBoard() vs GetBestBoardDriver(). Driver is basically a software component for hardware, but this could be added anywhere, as we are not really getting the hardware, only the driver (or binding in our terminology) for it. Since we already have classes like RaspberryPi3Driver(), which will not be identical to the RaspberryPiBoard(), it's probably better to leave the "Driver" part away when talking about boards and their instances.

Few additional thoughs of mime (I guess details come when somebody provides a first draft implementation)

pgrawehr commented 4 years ago

For buses (I2C, SPI) shall it be

public abstract class Board
{
  // ...
  I2cBus CreateI2c(int busNumber);
  // ...
}

with I2cBus providing I2cDevice CreateI2cDevice(I2cConnectionSettings) or only

public abstract class Board
{
  // ...
  I2cDevice CreateI2cDevice(I2cConnectionSettings);
  // ...
}

? The first variant requires an extra indirection, but is a bit more intuitive about the involved parts. Also, we might want to one time add operations that really operate on the bus, not a specific device, such as a general bus reset or a bus scan.

krwq commented 4 years ago

I have mixed feelings on this. I think ideally this should be:

I2cDevice CreateI2cDevice(I2cConnectionSettings settings) since settings already has busId property. There is nothing really useful to do on the bus itself so not sure if class would be that useful

pgrawehr commented 4 years ago

There are at least two operations that are useful on the bus, I think:

shaggygi commented 4 years ago

I have mixed feelings on this. I think ideally this should be:

I2cDevice CreateI2cDevice(I2cConnectionSettings settings) since settings already has busId property. There is nothing really useful to do on the bus itself so not sure if class would be that useful

Agreed, and while I don't believe there are any plans to add additional properties (like we did for SpiConnectionSettings), using I2cConnectionSettings would reduce possible breaking changes in future if we did.

shaggygi commented 4 years ago

Reset all devices (technically, that is done by sending a command to device ID 0)

Some devices might require an output pin connected to a Reset pin, but agree I believe many I2C devices I've looked at have a Reset command.

Scan the bus for devices (as I2cdetect does)

This can be deceiving as some devices will respond to multiple I2C addresses. IMHO, I2cDetect is good for development/troubleshooting. As a creator of a Board and IoT app, I would most likely know what I2C devices are connected and their set addresses. These values could be changed in a configuration setting for app.

pgrawehr commented 4 years ago

I'm just thinking about future possibilities and whether they would need some preparation in the design now. I agree that normally one would know what's connected where, but IoT may evolve. [There are several absolute-begginer-type languages and examples for programming it around, but rarelly anything for Pros]. I2cDetect may currently be a tool for troubleshooting, but we might one day try auto-detecting devices (most devices have only a handfull of possible addresses and should be identifiable by probing registers). Remember the hard start Plug&Play had, but nowadays everything works plug-and-play?

But long story short: No required action for now, since this can be added later (I just found that our DeviceApiTester test app actually already has an implementation of i2c-detect)

Ellerbach commented 4 years ago

And to add to the discussion on platform/drivers, I recently tried to use the latest FT4222 with GPIO with a simple binding. Because most device uses directly in the code, it's not possible to have those bindings working correctly. Typical implementation is:


private GpioController _gpioController;
private I2cDevice _i2cDevice;

public Constructor(I2cDevice i2cDevice)
{
    _i2cDevice = i2cDevice;
    _gpioController = new GpioController();
}

And with this kind of implementation, using FT4222 as a GPIO controller doesn't work as there is no possibility to change the driver used for the platform. So this case needs to be considered as well.

pgrawehr commented 4 years ago

@Ellerbach: Yes, absolutely. See @krwq and my discussion in PR #919 about GpioController object lifetime. The GpioController class is designed to be bound to the respective devices, but that - as you found out there - is not going to work if the actual implementation of the controller doesn't support multiple instances.

In other words, we need to be very clear about the object lifetime in our design (and eventually fix some of the existing components).

For the "Board" classes, I think it's rather clear that there should be exactly one instance for every physical entity (or maybe several, but distinct ones if splitting a physical board into logical components makes sense). And then from that you would create I2CDevices and GpioControllers to be fed to bindings.

If we want go keep the current concept that the GpioController instance is usually owned by the binding, we might be creating it using

public abstract class Board
{
    /// ....
    GpioController CreateGpioController(int[] pins);
    /// ....
}

So it's clear that this controller is responsible for that specific set of pins, and if it is disposed, these pins are freed (and the implementation would notify the board that these are available again). Technically, this would mean that an FT4222 (or an MCP27xxx, which is logically very similar) provides its own class that derives from GpioController and overrides its specifics, like pin allocations etc.

krwq commented 4 years ago

Note that Xamarin has some model already created we could possibly re-use, i.e.: https://docs.microsoft.com/en-us/xamarin/essentials/barometer?tabs=android https://docs.microsoft.com/en-us/dotnet/api/xamarin.essentials.barometerdata?view=xamarin-essentials

pgrawehr commented 4 years ago

@krwq I believe this is the wrong thread for that? That's more about a new binding proposal or maybe about units, not about the basic board concept.

krwq commented 4 years ago

Barometer is a hardware abstraction, isn't it? 😄

pgrawehr commented 4 years ago

True, but that's more like a single binding/device. But I agree that having bindings (or even a hardware abstraction layer) that works on phones would be nice. Is .NET Core intended to replace xamarin also on Android/IOS?

krwq commented 4 years ago

@pgrawehr there is couple more over there than Barometer. I'm not sure what the plans are though - I'd recommend waiting for the upcoming build conference https://www.microsoft.com/en-us/build - we usually announce the plans there - also perhaps there is some QA you can ask (I can't officially talk about any high level plans even when I know them, better talk to PMs - they will tell you what they can).

krwq commented 4 years ago

Ref: https://github.com/dotnet/iot/issues/1081

mi-hol commented 4 years ago

I'm not sure what the plans are though - I'd recommend waiting for the upcoming build conference

nearly 2 months have passed, didn't a conclusion transpire by now?

krwq commented 4 years ago

@mi-hol we are sorry that we haven't reached out a conclusion yet. We have created a feature branch that has the hardware abstraction prototype that @pgrawehr has been working on and we haven't yet finalized on that yet as our main focus right now is to continue to ship .NET 5.0 which has a tighter schedule than IoT.

mi-hol commented 4 years ago

@krwq appreciate the clarification, which makes perfect sense now ;-)

Ellerbach commented 3 years ago

[Triage] We've been moving forward on all this. Happy to close it!