dotnet / runtime

.NET is a cross-platform runtime for cloud, mobile, desktop, and IoT apps.
https://docs.microsoft.com/dotnet/core/
MIT License
15.46k stars 4.76k forks source link

SerialPort on Linux fails to open for 250k baud #64507

Open ctacke opened 2 years ago

ctacke commented 2 years ago

Description

Trying to open a System.IO.Ports.SerialPort for 250000 baud under Linux gives an argument exception. Even if it didn't the underlying code would still fail to properly open that rate.

This is a common serial rate for many 3D printers, such as all Lulzbot printers.

Bot said to add an Area, but didn't say where. So here it is: @adamsitnik

Reproduction Steps

Running on any version of .NET (fails in 5 and 6) on a Raspberry Pi with the latest 32-bit Rasberry Pi OS, attempt to open a serial port:

var port port = new SerialPort("/dev/ttyACM0", 250000, Parity.None, 8, StopBits.One);

Expected behavior

I expect it to open and be usable.

Actual behavior

An exception is thrown.

Regression?

No response

Known Workarounds

I have a known-good, working workaround. It's ugly, but works. First, open the port with a supported rate, like 115200. then run this code:

[DllImport("libc", SetLastError = true)]
private static extern int ioctl(IntPtr fd, uint request, int[] data);

private void SetNonStandardBaudRateIfRequired()
{
    if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)
        && _baudRate > 115200)
    {
        // due to limitations/behavior of Linux driver, we need to work around setting the baud rate above 115200
        var stream = m_port.GetType()
            .GetField("_internalSerialStream", BindingFlags.NonPublic | BindingFlags.Instance)
            .GetValue(m_port);
        var handle = (SafeHandle)stream.GetType()
            .GetField("_handle", BindingFlags.NonPublic | BindingFlags.Instance)
            .GetValue(stream);

        uint TCGETS2 = 0x802C542A;
        uint TCSETS2 = 0x402C542B;
        var BOTHER = 0x1000; // 0o010000;
        var CBAUD = 0x100F; // 0o010017;

        var buffer = new int[64];

        // get the current value
        var fd = handle.DangerousGetHandle();
        ioctl(fd, TCGETS2, buffer);
        buffer[2] &= ~CBAUD; // turn off the standard baud flag
        buffer[2] |= BOTHER; // turn on the "other baud rate" flag
        buffer[9] = buffer[10] = _baudRate; // set the same rate in both directions
        Console.WriteLine($"BAUD RATE WORKAROUND");
        // write the new value
        var result = ioctl(fd, TCSETS2, buffer);
        if (result != 0)
        {
            // failure
            Console.WriteLine($"IOCTL FAILED: {Marshal.GetLastWin32Error()}");
            return;
        }
        // verify
        ioctl(fd, TCGETS2, buffer);
        if (buffer[9] != _baudRate || buffer[10] != _baudRate)
        {
            // failure
            Console.WriteLine($"IOCTL FAILED: read != write");
            return;
        }
    }
}

I told you it was ugly.

NOTE that this code is unsetting the CBAUD flag and then setting the BOTHER flag, which the BCL does not do. Simply allowing 250000 baud through is not enough, the logic above must be applied.

NOTE 2 the above is not the right way to do it in the BCL. The proper fix should be done in C in the PAL, somewhere in this method:

https://github.com/dotnet/runtime/blob/c12bea880a2f1290d16adf97ec1000aa63631da2/src/native/libs/System.IO.Ports.Native/pal_termios.c#L350

I would have done a fork and PR, but honestly I don't have the native toolchain set up, and that feels like a huge headache. Anyone with a little C knowledge can port the above into that file.

Configuration

.NET 5.0 Raspberry Pi 4 ARM

pi@raspberrypi:~/marlin $ cat /etc/os-release
PRETTY_NAME="Raspbian GNU/Linux 11 (bullseye)"
NAME="Raspbian GNU/Linux"
VERSION_ID="11"
VERSION="11 (bullseye)"
VERSION_CODENAME=bullseye

This is not specific to this configuration. I'm willing to bet it will fail on any Linux kernel.

Other information

Fix and where it belongs are detailed above

dotnet-issue-labeler[bot] commented 2 years ago

I couldn't figure out the best area label to add to this issue. If you have write-permissions please help me learn by adding exactly one area label.

ghost commented 2 years ago

Tagging subscribers to this area: @dotnet/area-system-io See info in area-owners.md if you want to be subscribed.

Issue Details
### Description Trying to open a System.IO.Ports.SerialPort for 250000 baud under Linux gives an argument exception. Even if it didn't the underlying code would still fail to properly open that rate. This is a common serial rate for many 3D printers, such as all Lulzbot printers. Bot said to add an Area, but didn't say where. So here it is: @adamsitnik ### Reproduction Steps Running on any version of .NET (fails in 5 and 6) on a Raspberry Pi with the latest 32-bit Rasberry Pi OS, attempt to open a serial port: ``` var port port = new SerialPort("/dev/ttyACM0", 250000, Parity.None, 8, StopBits.One); ``` ### Expected behavior I expect it to open and be usable. ### Actual behavior An exception is thrown. ### Regression? _No response_ ### Known Workarounds I have a known-good, working workaround. It's ugly, but works. First, open the port with a supported rate, like 115200. then run this code: ``` [DllImport("libc", SetLastError = true)] private static extern int ioctl(IntPtr fd, uint request, int[] data); private void SetNonStandardBaudRateIfRequired() { if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux) && _baudRate > 115200) { // due to limitations/behavior of Linux driver, we need to work around setting the baud rate above 115200 var stream = m_port.GetType() .GetField("_internalSerialStream", BindingFlags.NonPublic | BindingFlags.Instance) .GetValue(m_port); var handle = (SafeHandle)stream.GetType() .GetField("_handle", BindingFlags.NonPublic | BindingFlags.Instance) .GetValue(stream); uint TCGETS2 = 0x802C542A; uint TCSETS2 = 0x402C542B; var BOTHER = 0x1000; // 0o010000; var CBAUD = 0x100F; // 0o010017; var buffer = new int[64]; // get the current value var fd = handle.DangerousGetHandle(); ioctl(fd, TCGETS2, buffer); buffer[2] &= ~CBAUD; // turn off the standard baud flag buffer[2] |= BOTHER; // turn on the "other baud rate" flag buffer[9] = buffer[10] = _baudRate; // set the same rate in both directions Console.WriteLine($"BAUD RATE WORKAROUND"); // write the new value var result = ioctl(fd, TCSETS2, buffer); if (result != 0) { // failure Console.WriteLine($"IOCTL FAILED: {Marshal.GetLastWin32Error()}"); return; } // verify ioctl(fd, TCGETS2, buffer); if (buffer[9] != _baudRate || buffer[10] != _baudRate) { // failure Console.WriteLine($"IOCTL FAILED: read != write"); return; } } } ``` I told you it was ugly. *NOTE* that this code is unsetting the `CBAUD` flag and then setting the `BOTHER` flag, which the BCL does not do. Simply allowing 250000 baud through is not enough, the logic above must be applied. *NOTE 2* the above is not the right way to do it in the BCL. The proper fix should be done in C in the PAL, somewhere in this method: https://github.com/dotnet/runtime/blob/c12bea880a2f1290d16adf97ec1000aa63631da2/src/native/libs/System.IO.Ports.Native/pal_termios.c#L350 I would have done a fork and PR, but honestly I don't have the native toolchain set up, and that feels like a huge headache. Anyone with a little C knowledge can port the above into that file. ### Configuration .NET 5.0 Raspberry Pi 4 ARM ``` pi@raspberrypi:~/marlin $ cat /etc/os-release PRETTY_NAME="Raspbian GNU/Linux 11 (bullseye)" NAME="Raspbian GNU/Linux" VERSION_ID="11" VERSION="11 (bullseye)" VERSION_CODENAME=bullseye ``` This is not specific to this configuration. I'm willing to bet it will fail on any Linux kernel. ### Other information Fix and where it belongs are detailed above
Author: ctacke
Assignees: -
Labels: `area-System.IO`, `untriaged`
Milestone: -
adamsitnik commented 2 years ago

We are most likely missing a mapping for the 250000 rate:

The managed layer checks only if the value is negative:

https://github.com/dotnet/runtime/blob/6de7147b9266d7730b0d73ba67632b0c198cb11e/src/libraries/System.IO.Ports/src/System/IO/Ports/SerialStream.Unix.cs#L115-L117

The native layer maps the value and returns a default value if no mapping is defined:

https://github.com/dotnet/runtime/blob/71192eacd5194e8afce9686e701aa5f072972b8d/src/native/libs/System.IO.Ports.Native/pal_termios.c#L242

For default value, when HAVE_IOSS_H is not defined an error is set and the method returns:

https://github.com/dotnet/runtime/blob/71192eacd5194e8afce9686e701aa5f072972b8d/src/native/libs/System.IO.Ports.Native/pal_termios.c#L518-L525

And then the managed layer throws:

https://github.com/dotnet/runtime/blob/6de7147b9266d7730b0d73ba67632b0c198cb11e/src/libraries/System.IO.Ports/src/System/IO/Ports/SerialStream.Unix.cs#L596-L598

@wfurt @krwq I am not familiar with how the TermiosSpeed2Rate mapping works. Could you briefly explain what it does and how can we solve the problem?

ctacke commented 2 years ago

termios supports even non-standard rates. It seems that instead of returning B0 and throwing an error, it would be more robust to use the supported TCSETS2 struct with BOTHER (like I'm doing in my workaround) to attempt to set the rate the user is requesting and only throw if that is unsuccessful. Let the underlying platform driver tell you if the requested rate is or is not supported. Beyond these common printers and their 250kbaud, if someone builds some custom machine with some strange clock and supported baud rate, it would still let managed code call it.

wfurt commented 2 years ago

Part of the problem is fact that the PAL code is but agains some headers and it may miss capability of never kernels. It may be worth of adding missing defines as needed to support known speeds even if the build machine does not. And trying it as custom speed makes sense to me as well. I'm not sure how common that is but trying it and leaving the decision to the driver make sense to me.

wfurt commented 10 months ago

I know this is old issue, but is 8.0 working for you @ctacke? I finally got setup where I can play with it and it seems to be working OK e.g. should be fixed by #80534. I think this issue could be closed.