Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce a Serial Port class #1832

Open
raffaeler opened this issue Mar 10, 2022 · 41 comments
Open

Introduce a Serial Port class #1832

raffaeler opened this issue Mar 10, 2022 · 41 comments
Labels
api-suggestion Early API idea and discussion, it is NOT ready for implementation area-protocols Bus Protocols Priority:2 Work that is important, but not critical for the release

Comments

@raffaeler
Copy link
Contributor

The initial discussion started here.

Background and motivation

The System.IO.Ports.SerialPort class has been widely adopted in the .NET world to support serial port communication. Since it was first added in the .NET Framework, its implementation is quite old and does not support many of the new language and runtime improvements.

As opposed to many other classes, it is very difficult validating the behavior of the serial port class because it strictly depends on the serial port driver and the connected hardware. For this reason it is preferable not to touch the existing implementation which could cause serious breaking changes. Instead, it is better providing a brand new implementation exposing a modern API and flexible API.

Anyway, the lower layer code (interacting with the serial hardware) is precious and already working cross-platform, therefore it is desirable to reuse it as much as possible.

This is not yet an API proposal that will follow right after collecting all the basic requirements that will shape the new implementation.

Requirements

This is a list of basic requirements that will guide shaping the final API. In the triage team we expect to see this list growing and discussions will follow to include or not them in the final list.

Modernization

  • Use tasks in place of the Begin/End pattern (used in BeginRead, EndRead, BeginWrite, EndWrite)
  • Use Span<byte> in place of byte[] in the API parameters
  • Possibly exposing a parameterless constructor to evaluate the opportunity to use Dependency Injection
  • Being minimal and let the user easily add an "application layer" to support various communication patterns. Examples:
    • Pure text communication (considering UTF8, UTF16 and endianess variants). Evaluate whether exposing text oriented reads using extension methods.
    • Fixed size (binary) packet oriented communication (w/o header)
    • Dynamic size (binary) packet oriented communication (where the header contains the packet size)

Features

  • Expose the installed COM port list by name including the low level name (e.g. COM10) and also the PnP name (e.g. "Silicon Labs Dual CP210x...")
  • Thread-affinity is important to communicate over the serial port. In certain scenarios, the asynchronous communication is interleaved with operations that needs to be fulfilled in a single thread. The current implementation has an EventLoop class that could be wrapped to this purpose. It would be even more interesting to reuse an implementation for a cross-platform message-pump (if available) in the runtime (@stephentoub suggestions?)
  • Provide the ability to bitbang the pins of the serial port like CTS/RTS/DSR/DTR/DCD/RING
  • Provide DisposeAsync
  • In addition to exposing the System.IO.Stream, evaluate the possibility to expose Pipes or Channel (as done in ASP.NET Kestrel)
  • Expose Interrupt-driven callbacks (e.g. old DataReceived event) as delegate instead
  • Verify supporting all the parameters to sustain the popular handshakes (RTS/CTS, DTR/DSR, Xon/Xoff)
  • Verify whether it is possible to support 9 bit word communication typically used by RS485 in microcontrollers
  • Provide support to a future Virtual COM implementation to test application code without the actual device

These lists were on the top of my heads and they are certainly not exhaustive, therefore any feedback/suggestion from the members of the community is welcome.

/cc @joperezr @Ellerbach @krwq @pgrawehr

@raffaeler raffaeler added the api-suggestion Early API idea and discussion, it is NOT ready for implementation label Mar 10, 2022
@ghost ghost added the untriaged label Mar 10, 2022
@pgrawehr
Copy link
Contributor

Expose Interrupt-driven callbacks (e.g. old DataReceived event) as delegate instead

Can you explain the rationale behind this? What's the problem of having an event there?

@krwq
Copy link
Member

krwq commented Mar 11, 2022

@pgrawehr:

  • if you subscribe multiple times and you put ReadLine in all of them what will happen? Who should receive data first, should you get whole line in all of them or you get one character in each of them or random line?
  • when you receive DataReceived and inside of that you put couple of ReadLines should you be getting another DataReceived while the first one is still running?
  • which thread they should run on? internal read loop? separate Task?
  • for delegates we can make them block the internal loop but I'm personally not in favor of adding neither - if you're waiting for data to come in IMO you should just use ReadAsync/ReadLineAsync rather than ill defined chain of events, you'll still be waiting for data but you will define when and how (which thread) it will be executed on
  • serial communication by definition happens serially and events are not serial

@raffaeler
Copy link
Contributor Author

@pgrawehr
The main issue is having multiple subscribers which does not seem (to me) very reasonable. Also, whereas an event is not strictly necessary, it also has an additional small cost that can be avoided (performance-wise).
The other motivations listed by @krwq are indeed a potential issue, but this mostly depends on the scenario. On very high-speed USB serial communication, blocking can be a serious problem. In other simpler scenarios it certainly won't be.

I am afraid that totally removing the support to be "interrupt-driven" could preclude an easy migration from the current Serial Port or even from code that was initially designed in other languages/platforms. I would not go that far. Even if the API can be very different, we should consider an easy migration-path for many different use-cases.

Most interestingly, I believe we need to precisely address the problem of the thread were the hard work should be done. Thread affinity as I said is very important and I think we should provide the pattern and the tools needed to do a clean work (maybe an external class for the EventLoop or a custom task scheduler). This is why I tried to involve Mr Toub.

@pgrawehr
Copy link
Contributor

@krwq Ok, with the current API, that is really a problem. But most of this would be solved by having events that provide the data. So the DataReceived event would deliver the actual data (either as byte[] or string) as argument. Then you don't have the problem of duplicate reads. And while rare, I did have use cases where multiple classes need to talk to the same serial port.

As @raffaeler mentioned, I think it would be a good idea to separate these concerns. So we have a SerialPort "low level" class and then provide some wrappers around it. These may provide protocol-level support (e.g for stringly-typed protocols) or provide different kinds of event callbacks or loops (e.g. async events, or synchronous callbacks).

@Ellerbach
Copy link
Member

So the DataReceived event would deliver the actual data (either as byte[] or string) as argument. Then you don't have the problem of duplicate reads. And while rare, I did have use cases where multiple classes need to talk to the same serial port.

Correct but the issue you have with serial port is that you don't really know how much has arrived. Or how much you want to read. Let me have few examples as this should help finding the right way of calling the event:

  • Option 1: you are interested in a fixed number of bytes. Let say, every 10 bytes received, you want the event to be raised. This is typical of fixed size protocols
  • Option2: you want it to be called as soon as you have received special character(s) (always the same), let's say for this example \r\n. That would be the equivalent of calling every time you have a new line
  • Option 3: be called on a variable way and adjustable based on elements. This is typical of chunk based protocol where first few bytes determined how many bytes you'll receive after. For simplicity, let's say first 2 bytes will determine how many will be received after.

Those 3 options regroup like 80%+, of the case we see in serial and if the data are sent there should be a way to setup this option.

For the 20% remaining (nothing fixed, not really easy to predict), this is where the pattern proposed to have a delegate handling this can be very handy.

@Ellerbach
Copy link
Member

As @raffaeler mentioned, I think it would be a good idea to separate these concerns. So we have a SerialPort "low level" class and then provide some wrappers around it. These may provide protocol-level support (e.g for stringly-typed protocols) or provide different kinds of event callbacks or loops (e.g. async events, or synchronous callbacks).

Fully agree with having multiple layer of the API. Like a ring of services. Something super simple, like the existing one (can even be more simple). Then another for things which are protocol specific, then advance scenarios, maybe as extension or plugin.

For the very low level (bit bang, etc), that should be part of the very simple class as this is something very close to the drivers.

@krwq
Copy link
Member

krwq commented Mar 11, 2022

All of the options above can be solved with simple ReadAsync, there is no need for callback. I agree with having SerialPort low level but I think SerialStream without events would be completely fine implementation to represent that. The layer above would be StreamReader which already exists, if you need fixed buffer then simply use Read/ReadAsync on the stream

@pgrawehr
Copy link
Contributor

Of course they can be solved with Read/ReadAsync. But the suggestion here is to provide the helper class that converts the stream into something more "high level". Like a wrapper that fires an event for each line received. I agree that we have to be sure not to reinvent the wheel. We already have StreamReader and TextReader classes.

@raffaeler
Copy link
Contributor Author

Those 3 options regroup like 80%+, of the case we see in serial and if the data are sent there should be a way to setup this option.

Exactly. I included the three option in my initial requirements not because they need to be addressed in the basic communication class, but because we have to provide all the needed code to make these use cases as simple as possible.

We can also deliver support for them, but they can stay outside the communication class.

@raffaeler
Copy link
Contributor Author

raffaeler commented Mar 11, 2022

@krwq

I agree with having SerialPort low level but I think SerialStream without events would be completely fine implementation to represent that.

Sure, it is definitely possible, but you may remember all the discussions when the Pipes where first introduced. The main example highlighting the power of the Pipes was showing how the data coming from the stream was hard and prone to error.

We definitely have to provide a robust support to make those three major use-cases are easy to write for anyone. We can just limit the base class to stream, but in this case there is the need to provide another classes/helpers with an efficient solution to those cases.

@pgrawehr

We already have StreamReader and TextReader classes.

Sure, but in most of the cases they are not enough to solve the three cases.
For example (and this is a very common scenario), when you receive packets with a header, you may need to "peek" at the data until you get the header tha contains the length of the whole packet. Only after the minimal amount of bytes has been received you want to consume the exact number of bytes matching a single packet. All of this should be done with a single buffer and minimizing the number of additional allocations and copied memory. This is why it is not trivial.

@mguinness
Copy link

Verify whether it is possible to support 9 bit word communication typically used by RS485 in microcontrollers

That could be useful, I assume I could then use with RS485 CAN HAT for Raspberry Pi.

@raffaeler
Copy link
Contributor Author

@mguiness
Any transciever for RS485 would work. The transciever just translate voltage-based signals (0-3.3V in case of the RPi) to current-based signals where the bits depends on the direction of the current and not on the voltage levels. This allows more robust communication over longer wires and multiple receivers on the same wires.

We have to see if the hw supports 9-bit words.
In this configuration the 9th bit is commonly used to "tag" the remaining 8 bits as an address rathern than data. Since the common topology is to have a single master and multiple slaves, the master send a packet tagging the address word with the 9th bit. This ensure that the slave knows which of the receiving bytes represents an address and if it matches with its own address, it processes the packet.
For more details, you can download one of the many Microchip microcontrollers datasheets and see how they support the 9-bit word on RS232.

If that is not possible, you can still use the RS485 with modbus. There are a few modbus library out there, I am using them and they work pretty well (both over the serial port and over the ethernet).

@kasperk81
Copy link

before adding new protocols and apis, please try to resolve the two burning issues: dotnet/runtime#2379 and dotnet/runtime#55146. once done, that will bring true meaning to this statement:

Anyway, the lower layer code (interacting with the serial hardware) is precious and already working cross-platform, therefore it is desirable to reuse it as much as possible.

current situation on unix is "weak". even mono implementation of SerialPortStream is 12 times better in cpu usage dotnet/runtime#2379 (comment), maybe use that as a baseline?

@raffaeler
Copy link
Contributor Author

@kasperk81
I certainly believe that the first goal should be to make the low level communication reliable everywhere, therefore the links you provided are valuable.

Apis are part of that. In one of those links the problem relates to the thread affinity (the thread where data is consumed), which is a very common issue often leading to consuming excessive resources.

even mono implementation of SerialPortStream is 12 times better in cpu usage

There are no reasons to exclude mono implementation if that is better.

The best thing would be to provide a repro code so that we can benchmark the new implementation in a wide range of use-cases.

@kasperk81
Copy link

kasperk81 commented Mar 13, 2022

The best thing would be to provide a repro code so that we can benchmark the new implementation in a wide range of use-cases.

memory leak: here is @dseeg's repro https://github.com/dseeg/SerialPortMemoryLeak

high cpu usage: we can use @jeroenwalter's library https://github.com/jeroenwalter/Arduino/tree/netcore30. delete the folder System.IO.Ports.Mono and ProjectReference:System.IO.Ports.Mono, then run integration tests. (unless @jeroenwalter has a simpler repro like @dseeg's)

@jeroenwalter
Copy link

The best thing would be to provide a repro code so that we can benchmark the new implementation in a wide range of use-cases.

memory leak: here is @dseeg's repro https://github.com/dseeg/SerialPortMemoryLeak

high cpu usage: we can use @jeroenwalter's library https://github.com/jeroenwalter/Arduino/tree/netcore30. delete the folder System.IO.Ports.Mono and ProjectReference:System.IO.Ports.Mono, then run integration tests. (unless @jeroenwalter has a simpler repro like @dseeg's)

I wouldn't delete System.IO.Ports.Mono from my fork, as that was the whole point of the fork. The Microsoft serial port implementation is so borked, that I added the Mono implementation to it and added a Received event (I think, maybe it already had one).
That being said, I also don't recommend just using my code as is, as I haven't touched it in the last few years (and also don't plan to) and it is not in sync with the original SolidSoils Arduino library.
If anything, only use the System.IO.Ports.Mono code, if you need raw serial port functionality.

@raffaeler
Copy link
Contributor Author

Thansk for the suggestions @jeroenwalter and @kasperk81
Knowing in advance the current issues is important to start the right way. No decision has been made yet and there will be time to carefully evaluate the lower-level code to start from.

To be clear, I am not a Microsoft employee, just a MS MVP who is part of the triage team and trying to provide some help in this repository. I strongly believe that a robust and flexible serial port support is needed and the rest of the team agreed that.
Said that, the input from any member of the community is very valuable and always taken into consideration.

@Ellerbach
Copy link
Member

While we are in sharing pre Microsoft implementation for serial, I did created this one for .NET Core 2.0: https://github.com/Ellerbach/serialapp/tree/master/nuget
So far, it's archived. Worked really well. Now, it was inspired for the API by what was existing. So it's not about the API in this case.

Something about the stream is that it can be very handy, like in the UWP implementation to have only 2 streams: one to read and one to write which are really independent.

@kasperk81
Copy link

I wouldn't delete System.IO.Ports.Mono from my fork, as that was the whole point of the fork. The Microsoft serial port implementation is so borked, that I added the Mono implementation to it and added a Received event (I think, maybe it already had one).

sounds like you missed the point or the context of my comment. we want to "reproduce" the high cpu issue in System.IO.Ports, the very reason you switched to System.IO.Ports.Mono. this is to get the underlying issue fixed by someone with relevant hardware knowledge and know how to fix serial port implementation in dotnet runtime.

@krwq
Copy link
Member

krwq commented Mar 14, 2022

@kasperk81 the thread should be sleeping most of the time if there is no work and seems from the description that's currently not happening, simple sleep usually brings down CPU usage to close to 0. Perhaps put a breakpoint somewhere here: https://github.com/dotnet/runtime/blob/main/src/libraries/System.IO.Ports/src/System/IO/Ports/SerialStream.Unix.cs#L853 and step to figure out why that's the case. It's a bit offtopic so let's not discuss this further in this issue. also if you have no events or pending reads/writes technically that loop should stop after few seconds and considering you got high CPU usage it's not the case

@josesimoes
Copy link

Let me throw here a couple of ideas and thoughts from the perspective of using this class in MCUs, like we do in .NET nanoFramework.

  1. Events: I'm all for keeping an event (despite having delegates). That's much easier to deal with the existing "style" and API that we're using there. Mostly because generics are no available, nor is the async/await.

  2. The event args could be improved to include more relevant data to help the code in the event handler (or delegate) to deal with whatever is coming in a more efficient way.

  3. In nanoFramework we added a property WatchChar that causes the event to fire whenever that char shows in the incoming stream. That's very helpful when you're dealing with terminators at the end of packets. This could be extended to a string so that patterns could be detected instead of single chars. This has support on the event arg parameter that reports the type of event: data or watch char present.
    Coupled with this, it's a no brainier to then use ReadTo() to read a "packet" without any other guessing work.

  4. I'm all for adding native support for related protocol like RS485. We already have that too in nanoFramework.

@raffaeler
Copy link
Contributor Author

@josesimoes
I've used infinite times RS485 from 8 bit microcontrollers (in asm and C), sometimes with modbus, sometimes with a custom protocol (that's why I would love to support 9-bit communications for addressing). I agree the microcontroller perspective is different, but I would not "stretch" the API to try to fit all scenarios with a single API.

I would rather prefer a base high-performance class that is open to be extended and also provide some default extensions (either derived classes or extension methods, TBD) for the most common scenarios (which I already listed at the beginning of this thread).

@krwq krwq added this to the Future milestone Apr 28, 2022
@krwq krwq added Priority:2 Work that is important, but not critical for the release and removed untriaged labels Apr 28, 2022
@josesimoes
Copy link

Consider expanding the Write() with an overload to take a single byte as parameter. Often times one has to create a byte array with a single element just to send a wake-up byte to the UART, for example.

(considering adding this to the SerialPort API in .NET nanoFramework)

@raffaeler
Copy link
Contributor Author

@josesimoes

Consider expanding the Write() with an overload to take a single byte as parameter.

Indeed. BTW this is already possible in the current implementation by accessing the underlying stream.

BTW I look forward to get general agreement on removing support (for the new SerialPort) to netstandard 2.0 so that I/we can optimize the code using the memory primitives (Span/Memory) and the newest language features.

@maloo
Copy link
Contributor

maloo commented May 22, 2022

Most, if not all, parsing requirements can easily be solved by creating a Pipe from the BaseStream of the SerialPort. This also removes GC of all the data buffers since it uses MemoryPool. Pipes are really easy to use for all kinds of parsing, whether it is markers like newlines, STXs, header peaking without data copying, buffer data until full package received etc.
All the parsing stuff should be removed from SerialPort and built on top of stream/pipe. Usually I create an application specific parser on top of the serial port pipe exposing simple app Api like async enumerable of protocol parsed data.
I hope the worker thread can be removed, never seen any other stream that have needed that.
Why do we need bit banging in the port class? If you are running on Linux I assume you use the kernel API for serial (read/write, ioctl). If not, the implementation is a completly custom one. Are we going to support drivers? Seems a bit overly complex, can't bit banging drivers be separate implementations?
For single and few bytes you can use stackalloc and Write(Span).
Events/delegates could be put on top of parser, more flexible and probably more efficient than the current implementation anyway.
Anlther advantage of using Pipe is that it is just a few lines of code to proxie data via a tcp socket and both streams can be wrapped in Pipe. Which makes testing/dev on PC with TCP remotely to Pi with serial port really RAD.
It would also be nice if the port list contained the VID:PID of port (USB) which is more reliable than the device name (which is good for user selection though). Today you have to manually go and lookup the port device ID to select the right port.

@pgrawehr
Copy link
Contributor

Why do we need bit banging in the port class?

Because quite often, one would use the serial port's control lines lines as GPIO outputs or inputs. For instance, many microcontrollers wire the RTS line of the USB-to-RS232 controller to the chip reset input. Sometimes, this is even used on the RX/TX line, to transmit 10-bit data or similarly strange data formats.

If you are running on Linux I assume you use the kernel API for serial (read/write, ioctl). If not, the implementation is a completly custom one.

Our goal is to provide an operating-system independent API that can be used the same on all operating systems. We just want that users don't need to directly access the kernel APIs.

@raffaeler
Copy link
Contributor Author

Hi @maloo.
My personal view on your points:

  • I am working on a public separate branch porting the code that is needed to create the new Serial Port. Its shape is not defined yet. In the current code I just unraveled the complexity of the old code.
  • I totally agree the new implementation should only care about raw bytes and nothing more. Anyhway, I am still not convinced to expose the raw tx/rx as Stream because a serial stream is far less than the System.IO.Stream. I would rather expose the essential read/write methods in a separate interface offering the ability to plug-in any higher-level implementations such as:
    • A System.IO.Stream derived class
    • A couple of System.IO.Stream classes (one for reading, one for writing) as requested by @Ellerbach
    • A System.IO.Pipelines.Pipe implementation
    • A packet-oriented (fixed or dynamic) exchange
    • Custom ones
  • Recently we discussed in the triage team the possibility to leverage the Win32 native APIs (at least for Windows of course) the possibility to implement a better "ReadTo". I still have to investigate.
  • Bitbanging. As @pgrawehr said, in the microcontroller space, it is not unusual to see the serial bits used for many purposes.
  • Pipes. I mentioned before the System.IO.Pipelines.Pipe class that has been used to optimize the perf in Kestrel. If you meant instead the traditional pipes, they also could be in the list I wrote before.
  • Port names. Agreed. I already implemented my own "discovery" based on WMI for Windows. We have to provide better platform-specific helpers for that. For this reason, I removed the com port from the serial port constructor. The COM name changes depending on the PnP and it changes every time you change USB receptacle.
  • Public API. The work I am doing is far from being the final shape. There will be a public API review to decide that. We are still far away from that.

I am currently experimenting a request from @krwq to use the FileStream class to manage the async code towards the native code. As I do not work for MS, this will take some time out of my job.

@maloo
Copy link
Contributor

maloo commented Mar 4, 2023

Any progress on the new low level API surface? Review/PR? I just had to do my own serial for Linux since not much was working in the dotnet one (mostly cancelation and timeouts).
One thing I found was that the concept of event loop is missing in dotnet today, at least in public API surface. Having a common event loop where you can register outstanding requests would be very helpful. Now every serial read/write has to use its own event loop (or thread) implementation.
Also, the new Pipe APIs from asp.net core is a very good match to OS read/writes.

@raffaeler
Copy link
Contributor Author

@maloo I am going on very slow because I am very busy with my job (I am just a contributor here).

The Pipe API is one of the thing I tested and dropped because it is definitely slower than the standard Stream management.
I tried in many ways to adopt the pipes in a project acquiring measures via USB (serial) from an A/D and the performance was so slow that I had to convert everything back to strings. When the Pipe were first launched, it was very fast, but later slowed down. Also they can only be used to push etherogenous data (typically bytes) and making if <T> with a variable T causes a lot of headaches.

The current code in the branch is Windows only. If you have feedback there, please let me know.
Please consider that the API surface of the current branch is not the final one, I am still in the plumbing.

@maloo
Copy link
Contributor

maloo commented Mar 10, 2023

@raffaeler Are we talking about the same Pipe API? Linked API is used in ASP.NET Core and saturating 10Gb/s in benchmarks, why is that too slow for a device serial port? And the APIs in Pipe for peeking into data before consuming it makes it really great for serial communication where you want to make sure a full packet has arrived before parsing it. And I don't see why pushing bytes is bad, someone have to convert bytes to whatever the app needs, but it should definitely not be done by the lowest layer.
Where is your current branch with Windows only support?

@raffaeler
Copy link
Contributor Author

@maloo Yes, that API had a fix that worsen down the perf because of a substantial increase of CPU usage.
The problem has been logged a long time ago as an issue. If I remember well the explanation was some kind of internal race condition. I can't find the link right now.

I did not say that pushing bytes is bad. I said that the Pipe API makes the transformation of bytes into other entities unpleasant. Peeking bytes is of course the reason why I initially tried to use the Pipes in the Serial port scenario.
BTW I still am a fan of the Pipes which I presented in several conferences with some plain and edge use-cases, but now I am more careful and measure .

Beyond that, you will always able to add a PipeReader from the SerialPort if you want to do that. The goal in this new implementation is to provide the needed extensibility points so that you can consume the data in different ways.

The branch is here: https://github.com/dotnet/iot/tree/feature-SerialPort
Since the System.IO.SerialPort uses properties to action changes in the serial port, I had to completely revise the code while preserving the interop. I still am tempted to try adopting completion ports but frankly I am not sure if there could be a tangible advantage. Also, it can be quite hard to test adequately.

@slyshykO
Copy link

Is it possible to add an ability to enable RS-485 support on Linux according to https://www.kernel.org/doc/Documentation/serial/serial-rs485.txt ?

Thanks in advance.

@raffaeler
Copy link
Contributor Author

@slyshykO I totally agree.
If you read my initial post, I specifically added the 9-bit support. This is almost exclusively used on RS485 communication. Microcontrollers usually transmit the slave address as a 9-bit word in order to disambiguify the data from the addresses.
I already tested the algorithm on a microcontroller and my goal is to port this implementation in this class.

If you don't need the 9-bit support, you can already use the current System.IO.SerialPort (or any other serial port library) to communicate over RS-485. To do this, you just need a transciever like the MAX487 or any other among the many RS-485 transcievers available. You may want to get a one small board from AliExpress or Amazon in case you don't want to solder the components.

@slyshykO
Copy link

@raffaeler
I am talking about not only 9-bit support but about the possibility of enabling a special mode for serial devices to control DE pin by hardware. This mode can be set by calling ioctl on the port file descriptor with a special struct serial_rs485 filled.
Such special serial devices are present on many industrial SoC's. For example, on STM32MP1 series processor.

@raffaeler
Copy link
Contributor Author

@slyshykO Point noted, but consider that there are simpler solutions:

  1. bitbanging the rs232 pins is very easy on any serial library and without any need to use ioctl on the serial driver.
  2. Some transcievers (but you can add this by yourself) manage the DE/RE automatically using consecutively 2 hex inverter buffers (74HC04)

@maloo
Copy link
Contributor

maloo commented Mar 20, 2023

@maloo Yes, that API had a fix that worsen down the perf because of a substantial increase of CPU usage. The problem has been logged a long time ago as an issue. If I remember well the explanation was some kind of internal race condition. I can't find the link right now.

I think I would like to see a link to it and see if it is still open and if it is relevant for this task. In my mind an API can't have a race condition, an implementation can. Maybe Pipe or PipeReader/Writer? But if it is a problem, IDuplexPipe is the interesting part to expose, which requires PipeReader/Writer, but most in those can be overridden if needed. But hopefully, Pipe is good enough.

I did not say that pushing bytes is bad. I said that the Pipe API makes the transformation of bytes into other entities unpleasant. Peeking bytes is of course the reason why I initially tried to use the Pipes in the Serial port scenario. BTW I still am a fan of the Pipes which I presented in several conferences with some plain and edge use-cases, but now I am more careful and measure .

Not understanding the part about transforming bytes into other entities in unpleasant. Can you give an example? We can't get around the fact that all data from the kernel is bytes. So I can't see how the lowest layer (driver) can Not work with bytes. And if you use "struct messages", those can be "cast" directly to C# types and normal StreamReaders etc can be created from the Pipe handling text style protocols etc.

Beyond that, you will always able to add a PipeReader from the SerialPort if you want to do that. The goal in this new implementation is to provide the needed extensibility points so that you can consume the data in different ways.

Yes, you can create pipe from stream, and stream from pipe. I just think IDuplexPipe makes the most sense for the lower level API.

The branch is here: https://github.com/dotnet/iot/tree/feature-SerialPort Since the System.IO.SerialPort uses properties to action changes in the serial port, I had to completely revise the code while preserving the interop. I still am tempted to try adopting completion ports but frankly I am not sure if there could be a tangible advantage. Also, it can be quite hard to test adequately.

Thanks, had a quick look. I don't like the mixing of async and sync in interfaces. Same issue as in many old .NET types, like stream. One of them will always be paying for the other. If impl use sync kernel calls, async will suffer, if kernel use async, sync calls will suffer. I was hoping for separation of async and sync (hopefully no sync at all). Again favoring IDuplexPipe.
I also think API complexity is high. Do we really need all the old complexity that made the old implementation so complex that it can't be fixed? Things like callback delegates for pin changes, can't that be done using gpio API if needed? Only things that Has to be coupled and put in one interface should go into serial port.
And things like event loops should probably be extracted into its own reusable entity, because at least in IoT/embedded you often want to reuse that between ports, gpio, files, networking that support uring/epoll etc. for async (multithreading usually make devices more prone to unstability and bugs).

@raffaeler
Copy link
Contributor Author

@maloo When we discussed the serial port in the triage, there was a consensus in not adding a verbose api, but just the bare minimum to send/receive data and what else may be needed to put further abstractions ontop of it, but nothing else.

Not understanding the part about transforming bytes into other entities in unpleasant. Can you give an example?

When you pipe a stream of bytes, you may also want to analyze the data, recognize the variable-sized packet and transform it to a <T> which can be consumed from the upper layers.
I worked this way with the pipes. Weird API to do this, but initially worked great ... until some patch.
In any case the debate is pointless, for the reasons I was explaining above.

I agree the old code is quite complex, but the old serial port always worked very well and the difficulty of testing a different logic is my primary concern. In any case, should I rewrite it, I would definitely go with the completion ports, but I am not sure it is really worth the effort.

Bitbanging the serial port pins is still needed. I cannot delegate the serial port pins to the GpioController as this has no knowledge of the serial port and never will. Those pins are important, for example, to drive RS485 as for the last comment on this thread.

Event loops. I don't see the benefits right now of making a super-tested even loop management so that it can be reused, but this is something that can be added later on.

As soon as I can find some time to work on this, any further proposal, discussion and fix is, of course, welcome.
Thanks

@nguyenlamlll
Copy link

Hi everyone. I'm a bit beginner on this dotnet iot thing. But after reading through this issue, it means that System.IO.Ports would not work on Linux, or to be specific, won't work on Raspberry Pi devices? In fact, I have tried to do so but it does not connect to serial ports successfully.

@michaldobrodenka
Copy link

michaldobrodenka commented May 27, 2024

system.IO.SerialPort works on Linux and raspberry pi. There is only one catch. High cpu usage. But there are alternatives - serial ports ported from mono repository - about 5x lower cpu usage. You can find ones on nuget, or check my repo

@pgrawehr
Copy link
Contributor

@nguyenlamlll I can confirm that it works just fine. I have a Raspberry Pi 4 with 5 serial ports open in parallel; no issues at all. This ticket is about a replacement implementation, but less because of technical problems but rather because of a semi-optimal interface. If you have problems getting it to work, please open a separate issue and post the code you're using.

@raffaeler
Copy link
Contributor Author

BTW @nguyenlamlll issues on System.IO.Ports.SerialPort should be opened in the https://github.com/dotnet/runtime/issues repository and not here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
api-suggestion Early API idea and discussion, it is NOT ready for implementation area-protocols Bus Protocols Priority:2 Work that is important, but not critical for the release
Projects
None yet
Development

No branches or pull requests