dotnet / iot

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

"System.Device.Model - attributes for device bindings" progression #2074

Open logicaloud opened 1 year ago

logicaloud commented 1 year ago

The approach of adding attributes to devices for plug-and-play scenarios sounds like a good idea. It has not progressed much recently and many devices don't seem to use the attributes that are already defined.

Are there any plans for this to be pushed further?

Or has this been moved to a different package elsewhere?

Or are there any insights that indicate that such an approach is not flexible enough?

krwq commented 1 year ago

Hello @logicaloud - yes but it's unofficial, I'm driving that project on the side but progress is rather slow because I don't actively work on that (I usually spend a bit of time on that once every couple of weeks). Current goals are following:

basically the largest problems we stumbled upon is that device components (i.e. SenseHat contains: thermometers, accelerometer etc.) are independent from their connection and we need a good way to represent that. We're currently trying to create abstraction which will allow us to reuse that in both our project (so we need to change our prototype generator to use that library) and we're working with folks who designed https://github.com/pi-top/pi-top-4-.NET-SDK because they have similar problem - we're trying to create something which will work for both of our projects.

Some other problems we found:

cc: @colombod

krwq commented 1 year ago

ref: https://github.com/dotnet/iot/issues/2074 - note that we've slightly shifted from "this needs to be Azure PnP" toward "we prefer more generic solution" - prorotype is still using Azure PnP though

logicaloud commented 1 year ago

Hi @krwq, Thank you for the detailed response.

It is still a while before I can do more research into this topic and I would be very much in favor of a device model that is generic and independent from Azure PnP (which does not preclude targeting it separately, I presume).

In my simple mind, I had envisioned that .NET itself becomes the definition language for this project and all devices can be loaded dynamically and explored using .NET reflection (by applications developed outside of this project). Defined attributes would guide an application to the relevant types and methods, which may be device factories, properties, telemetry, commands, and so forth, and the application could then expose a generic user interface for device configuration and operation, transfer specific data to a cloud IoT provider, or convert device information into a different format (like DTDL). Having a .NET assembly for a device module dropped into a folder for a generic application should be enough to make the device usable. More specialized applications could restrict themselves to deal with some specific device category only, of course.

What you are describing seems to already aim a bit higher and includes developing applications consuming the device model attributes, i.e., applications that generate device representations, solve how device connections should be handled, or how often data should be sent, correct?

The scope that I had envisioned for the project would simply specify attributes and their meanings, sufficient to define the behavior for a large number of devices, including the problem cases you have mentions. If the goal is to create applications, then maybe these applications can be separated out? From my point of view, the project can stand on its own by just specifying the "right attributes", because all kinds of applications can be based on that separately. At the same time I see that development of an application is needed to proof that attribute specifications are sufficient (that can serve as an example).

I may be misunderstanding something. Any thoughts?

krwq commented 1 year ago

Our first goal is to provide versatile library (model). The goal of the model is to:

How the devices are connected is not our first goal but we should have at least some sort of initial design for that before we can say that our design of model will play nicely with that. I.e. some device may allow connecting other devices to them (i.e. GPIO expander or FT4222; there are also some devices which produce telemetry AND allow connecting other things) - on its own it may not produce telemetry but stuff connected to it can do it. At the query time we're not aware that anything is connected but we'd still like to understand how to represent that tree structure and how we could add such capability in the future.

Attributes goal is to make it easier to create such model (i.e. rather than doing it by hand you simply tell which type you'd like the model generated for). Some applications require model to be created at runtime and that's our design will revolve around model (this is similar approach to what serializers are doing). We added them early so that we could start experimenting with that since their shape is unlikely going to change greatly and we can start using them for docs as well (unfortunately we lacked time to do that).

Prototype/code-gen is only to prove the design is useful and can be used for i.e. code-gen and if something useful is created from that - even better.

I'd be really interested in hearing more how you'd envision working with attributes/model (i.e. write prototype code of how you'd interact with it). I'm curious of how that would be similar/different to our prototype :smile:

logicaloud commented 1 year ago

Thanks again for sharing your thoughts. Happy to share prototypes but it may be a few months before I can have a good look. My envisioned approach would be simple: Pick some devices and see if the defined interfaces and attributes are sufficient (and convenient / efficient) to construct a working device dynamically by using reflection. If not then revise interfaces and attributes. Turning the code that uses reflection into a library would be secondary step.

krwq commented 1 year ago

Sure, there is no rush, I'm unlikely going to have time to play with this any time soon... Here is some brain dump and notes of various scenarios to think about...

Some interesting devices to look at when prototyping:

other interesting devices:

other interesting scenarios:

for the last one - we've had fairly good luck with Rx - so far we haven't seen any problems with that approach and code is clean and really easy to add stats/filtering/essentially whatever. Also plays really well with anything telemetry related including combining, transforming etc. The downside of Rx is that writing Rx code requires knowing the library (reading is simple though) - with ChatGPT now on the table perhaps is no longer that big of deal anymore.

logicaloud commented 1 year ago

Great info, thank you. I'll leave it to you to decide whether this issue should be closed in the meantime.

logicaloud commented 1 year ago

Edit: Having dug a bit deeper, I think some of my previous thoughts are just not feasible (i.e. device probing/discovery) or already realized in some other form. It is probably best to carry on when I have something more tangible.

logicaloud commented 5 months ago

Finally, I had a chance to have a closer look at the device model. Overall, I think the current attributes and constraints worked out well. There are of course quite a few devices that do not have these attributes.

Initially I had envisioned an approach where all devices are loaded dynamically at runtime, accessing reflected methods and properties, and so forth. While that would be possible, the parsing code would become quite complex, and performance could become a concern. I ended up going down the code generation path. This then resulted in code that could be tightly integrated into the targeted solution. It does mean though that for each new device or device change, another code generation run is required (which could still be automated).

I carried on with the mindset that the library’s device model is not primarily targeted at dynamic device instantiation and operation, but to form a basis for custom solutions. Any solution on top of what is already there is likely to be opinionated. A ‘less opiniated’ generic solution for plug and play would probably benefit from using interfaces as definite contracts rather than attributes; attributes could still be used to generate that solution (which could become part of the library itself).

In my case, the custom solution involved the integration of the IoT library for a browser UI with a containerized backend, designed around the concepts of the loT library, i.e. exposing properties, telemetries, commands, and “events”:

The addition of an “Event” attribute may be useful to indicate that a device generates data on its own. For example, the GPIO controller has callbacks to signal a pin value change, and in my experimentation, I squeezed the GPIO controller into the existing device model with the addition of the “Event” attribute on pin values. If I am not mistaken, then there are other devices that can notify values changed. In general, the Event attribute could either be added to telemetry, or to Subscribe/Unsubscribe methods to indicate to the code generator that event notifications are available.

Components provided through SenseHat did not really pose a challenge. The Component attribute was simply used to browse through the components and then treat them like any other device, limited to those components that expose device model attributes. This does not include the LED matrix, for example – it will be interesting to see how this is going to work. That the SenseHat components allow for default connection settings is already expressed in the optional construction parameters (i.e., i2caddress = null), which can be evaluated in the code generator, although I have not done so yet.

I have kept device discoverability simple: All devices within the IoT library (with attributes) are available for selection in the UI, and it is left to the user to pick one that is actually there, then the user can select any of the device's properties, telemetries, commands or events to operate on,

Some challenges for integrating the ~50 devices that currently use the device model attributes were:

So far, I have only tested the generated solution with the various SenseHat devices and did some conceptual GPIO testing. There is no way for me to test all devices. But if one “representative” device works, then so should others that fall into the same category.

Is the current objective to continue with the use of the device model attributes?

If yes, then I’d like to contribute by adding attributes to more devices and explore the potential limitations imposed by device categories that I have not explored yet.

Ellerbach commented 5 months ago

Thanks @logicaloud for this feedback. And explaining your thoughts process and where you ended up. We're very open for improvements and PR on the device model and on the bindings to make them better! Some thoughts from you challenges:

There could be multiple device constructors.

Maybe this can be addressed with a specific attribute like [DefaultConstructor] or something like this that can be added to the device model?

Device constructors could derive from a base class.

Similar to the previous one, if there is an heritage, it still can be trapped with the same principle and a default one.

Constructor arguments could be optional.

In that case, then, we can assume that the default ones are the correct ones for those scenarios. In most cases, it's indeed what's done and the reason they are optionals.

Some device constructors take another device as a parameter. I have not explored this yet, but I think the solution will work for this scenario with a small addition, also for a chain of embedded devices.

Yes, I guess it's possible to create a "device tree" with the dependencies, so that you can ask to have a class created before. That should also solve the I2C, SPI problem. They can be created the same way as any other object.

Not sure yet how to best handle shoudDispose in constructors; this parameter is currently not exposed in the user interface and is set to “true”.

As any default parameter: they are ment to be for "normal usage"

Methods returning tuples required some special handling.

For that one, no idea :-)

logicaloud commented 5 months ago

Thank you for your feedback, @Ellerbach.

Maybe this can be addressed with a specific attribute like [DefaultConstructor] or something like this that can be added to the device model?

I solved this by ordering the constructors by the number of parameters (higher number first), and then check whether the user has specified all parameters of the first constructor; if not, then move onto the next constructor, and so forth. A [DefaultConstructor] parameter is not required, I believe. If there are gaps in the specified parameters, then the constructor will complain and the user gets an error message.

Methods returning tuples required some special handling.

It just required some type unpacking to have some presentable type information (that will also be required for other types used in commands, i.e. ReadOnlySpan), but that is unrelated to the device model itself.

I have added a pull request for SenseHat here https://github.com/dotnet/iot/pull/2324, if you'd like to check how that fits into the device model.

If that's OK, then the next device I'd like to apply the device model to is the GPIO controller with a concept of an "Event" attribute.