WIP - initial serial port support for Linux#28415
WIP - initial serial port support for Linux#28415wfurt wants to merge 1 commit intodotnet:masterfrom
Conversation
|
@kasper3 @JairoMarques @JohnRusk @dima117 @@daniel-mcassey-tmsuk @jamespettigrew in case folks want to try / give feedback. |
|
Thank you! 👍 One question, in #25032, the effort was started to convert CPP to C, can the new code be C (to widen the compiler/platform options)? Just like bash vs. sh debate, sometimes we are not even using C++ features but yet the cpp extension deceives the compiler and cmake.. :) |
|
C would be easy if needed. Adding @janvorli for some thoughts. |
| <ProjectGuid>{187503F4-BEF9-4369-A1B2-E3DC5D564E4E}</ProjectGuid> | ||
| <IsPartialFacadeAssembly Condition="'$(TargetGroup)' == 'netfx'">true</IsPartialFacadeAssembly> | ||
| <GeneratePlatformNotSupportedAssemblyMessage Condition="'$(TargetGroup)' == 'netstandard' AND '$(TargetsWindows)' != 'true'">SR.PlatformNotSupported_IOPorts</GeneratePlatformNotSupportedAssemblyMessage> | ||
| <GeneratePlatformNotSupportedAssemblyMessage Condition="'$(TargetGroup)' == 'netstandard' AND '$(TargetsWindows)' != 'true' AND '$(TargetsLinux)' != 'true'">SR.PlatformNotSupported_IOPorts</GeneratePlatformNotSupportedAssemblyMessage> |
There was a problem hiding this comment.
You said it worked on BSD and OSX (or at least built) .. if so, no condition should be required on TargetsXXX .
There was a problem hiding this comment.
I did build PAL (e.g. cpp on OSX). This was primarily to verify I can use certain termios functions.
I have done 0 testing so I was not comfortable enabling it in build. That will probably come in some next round.
There was a problem hiding this comment.
@kasper3 the purpose of porting part of the stuff to C was to be able to share that piece with Mono. There is no plan to do such change globally. Even if it could be done for corefx, it is not possible for coreclr. We need exception handling, for example.
| #include <stdio.h> | ||
|
|
||
| /* This is dup of System/IO/Ports/NativeMethods.cs */ | ||
| enum { |
There was a problem hiding this comment.
A nit - the coding convention is to place the { on a separate line.
There was a problem hiding this comment.
Another nit - it seems that it would be nice to name these "ParityXXXX" to match the style of the enum below.
There was a problem hiding this comment.
nit: either rename as suggested by @janvorli or change NoneParity to NoParity
| } | ||
|
|
||
| static speed_t | ||
| SystemNative_speed2termios(int speed) |
There was a problem hiding this comment.
Could you please keep the naming consistent - to start each part of the name with uppercase letter?
|
|
||
| using System.Runtime.InteropServices; | ||
| using Microsoft.Win32.SafeHandles; | ||
| using System.IO.Ports; |
There was a problem hiding this comment.
nit: put using System.IO.Ports as first using to match most other source files
| { | ||
| internal static partial class Sys | ||
| { | ||
| [DllImport(Libraries.SystemNative, EntryPoint = "SystemNative_Termios_Reset", SetLastError = true)] |
There was a problem hiding this comment.
nit: it seems that the convention is to use just one underscore after SystemNative and the remaining Pascal casing.
| // Licensed to the .NET Foundation under one or more agreements. | ||
| // The .NET Foundation licenses this file to you under the MIT license. | ||
| // See the LICENSE file in the project root for more information. | ||
|
|
There was a problem hiding this comment.
nit: remove one blank line
| { | ||
| return -1; | ||
| } | ||
| return status; |
There was a problem hiding this comment.
nit: blank line after end of block
| HandshakeBoth // Software & Hardware Flow Control | ||
| }; | ||
|
|
||
| static int SystemNative_Termios_GetStatus(int fd) |
There was a problem hiding this comment.
nit: there is an extra space after int same pattern happens below
|
|
||
| static int SystemNative_Termios_SetStatus(int fd, int status) | ||
| { | ||
| if (ioctl(fd, TIOCMSET, &status) < 0) |
There was a problem hiding this comment.
In this case it seems that we don't need the if statement it can just be return ioctl(fd, TIOCMSET, &status); at least per http://man7.org/linux/man-pages/man4/tty_ioctl.4.html
| } | ||
|
|
||
| cfsetspeed(&term, SystemNative_speed2termios(speed)); | ||
| // cfsetospeed(&term, (uint32_t)speed); |
There was a problem hiding this comment.
nit: remove commented lines
| term.c_iflag &= ~(static_cast<tcflag_t>(INPCK | ISTRIP)); | ||
| switch (parity) | ||
| { | ||
| case OddParity: |
There was a problem hiding this comment.
nit: align inside switch, make like the switch following it
| return 0; | ||
| } | ||
|
|
||
| extern "C" int32_t SystemNative_Termios_GetDcd(int fd) |
There was a problem hiding this comment.
nit: all these Get/Set methods could be consolidated in a single function and having the specific flag as parameter, it is small code but anyway avoids the repetition
| } | ||
|
|
||
| extern "C" int32_t | ||
| SystemNative_Termios_GetSpeed(int fd) |
There was a problem hiding this comment.
nit: consistency, keep on a single line like previous functions
| // cfsetospeed(&term, (uint32_t)speed); | ||
| // cfsetispeed(&term, (uint32_t)speed); | ||
|
|
||
| if (tcsetattr (fd, TCSANOW, &term) < 0) |
There was a problem hiding this comment.
nit: spacing between function name and firt '('
There was a problem hiding this comment.
The same happens in other places of the file.
| ports.Add("/dev/" + entry.Name); | ||
| } | ||
| } | ||
| return ports.ToArray(); |
There was a problem hiding this comment.
nit: blank line after block
| // /sys is mounted. Let's explore tty class and pick active nodes. | ||
| List<string> ports = new List<string>(); | ||
| DirectoryInfo di = new DirectoryInfo(@"/sys/class/tty"); | ||
| var entries = di.EnumerateFileSystemInfos(@"*", SearchOption.TopDirectoryOnly); |
There was a problem hiding this comment.
I think that Directory.GetFiles will be better: you can avoid the allocations of FileInfo objects, set the capacity of ports at creation and use Path.GetFileName to get the name for the comparison below only when the one that uses the full path is not true.
@JeremyKuhne any better option?
There was a problem hiding this comment.
GetFiles does not work as this is full os symbolic links.
| { | ||
| List<string> ports = new List<string>(); | ||
|
|
||
| foreach (string name in Directory.GetFiles("/dev", "tty.*")) |
There was a problem hiding this comment.
nit: since you are using Directory.GetFiles you have an upper-bound for ports, perform the two GetFiles and create ports using this upper-bound as capacity.
| { | ||
| ports.Add(name); | ||
| } | ||
| } |
There was a problem hiding this comment.
nit: blank line after block
| private bool _inBreak = false; | ||
| private SafeFileHandle _handle = null; | ||
| private bool _rtsEnable = false; | ||
| ArrayList selectList; |
There was a problem hiding this comment.
naming, missing underscore, and why use ArrayList? Is there some usage that requires it to be an ArrayList?
| { | ||
| internal sealed partial class SerialStream : Stream | ||
| { | ||
| private const int infiniteTimeoutConst = -2; |
There was a problem hiding this comment.
nit: Pascal casing for constant
| </ItemGroup> | ||
| <ItemGroup Condition="'$(TargetGroup)' != 'netfx' AND '$(TargetsWindows)' == 'true'"> | ||
| <Compile Include="System\IO\Ports\InternalResources.cs" /> | ||
| <Compile Include="System\IO\Ports\SerialStream.cs" /> |
There was a problem hiding this comment.
Isn't enough common code between the serial stream to use the typical partial class pattern and have SerialStream.Windows.cs and so on?
| foreach (string name in Directory.GetFiles("/dev", "tty.*")) | ||
| { | ||
| // GetFiles can return unexpected results because of 8.3 matching. | ||
| if (name.StartsWith("/dev/tty.")) |
There was a problem hiding this comment.
I don't think you need this extra check here, nor the one in the other loop. If you don't need them you also don't need the ports list you can just create the array and put the entries.
There was a problem hiding this comment.
I think I do. Without that check I was getting "/dev/tty" and that is not what I want.
Per the comment, it seems to be expected per MsDos compatibility ;(
| { | ||
| if (Interop.Sys.TerminalSetSpeed(_handle, value) != 0) | ||
| { | ||
| throw new IOException(); |
There was a problem hiding this comment.
Since these functiontions set errno it will be better to have some wrapper that combines Interop.Sys.GetLastErrorInfo() w/ Interop.GetExceptionForIoErrno and possibly the name of the affected port.
|
|
||
| if (value != _handshake) | ||
| { | ||
|
|
There was a problem hiding this comment.
Add a TODO for now? Same for the similar ones below.
| set | ||
| { | ||
| Debug.Assert(!(value < StopBits.One || value > StopBits.OnePointFive), "An invalid value was passed to StopBits"); | ||
|
|
|
|
||
| internal void SetBufferSizes(int readBufferSize, int writeBufferSize) | ||
| { | ||
| if (_handle == null) throw new IOException(); |
There was a problem hiding this comment.
That should be InvalidOperationException, or something else, not an actual IOException.
| } | ||
|
|
||
| internal bool IsOpen => _handle != null; | ||
|
|
There was a problem hiding this comment.
nit: remove extra blank line
| // Note: Serial driver's write buffer is *already* attempting to write it, so we can only wait until it finishes. | ||
| public override void Flush() | ||
| { | ||
| if (_handle == null) throw new ObjectDisposedException(SR.Port_not_open); |
There was a problem hiding this comment.
Is _handle null only after Dispose?
| if (_handle == null) throw new IOException(); | ||
|
|
||
| selectList.Add(_handle); | ||
| Socket.Select(selectList, null, null, timeout); |
There was a problem hiding this comment.
Is this needed to wait for data a the serial port? Anyway selectList.Add is called every time ReadByte but it seems that if it is removed from the list then a timeout exception is throw but then the list keeps growing on each success call? What happens with one that is added if Socket.Select throws?
| Interop.Sys.PollEvents events = Interop.Sys.PollEvents.POLLNONE; | ||
| if (timeout > 0) | ||
| { | ||
| Interop.Sys.Poll(_handle, Interop.Sys.PollEvents.POLLIN | Interop.Sys.PollEvents.POLLERR, timeout,out events); |
There was a problem hiding this comment.
nit: spacing of last parameter
|
|
||
| internal unsafe void Write(byte[] array, int offset, int count, int timeout) | ||
| { | ||
|
|
| int numBytes; | ||
| if (_isAsync) | ||
| { | ||
| throw new NotSupportedException(SR.PlatformNotSupported_IOPorts); |
There was a problem hiding this comment.
Same as above: use a specific message
| { | ||
| throw new NotSupportedException(SR.PlatformNotSupported_IOPorts); | ||
| } | ||
| else |
There was a problem hiding this comment.
this doesn't need to be in a else block
|
|
||
| if (numBytes == 0) | ||
| throw new TimeoutException(SR.Write_timed_out); | ||
|
|
There was a problem hiding this comment.
nit: remove extra blank line
| internal SafeFileHandle OpenPort(string portName) | ||
| { | ||
| // TBD O_NOCTTY | O_NDELAY ??? | ||
| SafeFileHandle handle = Interop.Sys.Open(portName, Interop.Sys.OpenFlags.O_RDWR | Interop.Sys.OpenFlags.O_CLOEXEC, 0); |
There was a problem hiding this comment.
Is it worth as a separate method?
| internal SerialStream(string portName, int baudRate, Parity parity, int dataBits, StopBits stopBits, int readTimeout, int writeTimeout, Handshake handshake, | ||
| bool dtrEnable, bool rtsEnable, bool discardNull, byte parityReplace) | ||
| { | ||
|
|
There was a problem hiding this comment.
Typically we have the constructors at the top of the source after the fields.
There was a problem hiding this comment.
nit: there is a extra blank line here too
| } | ||
|
|
||
| DtrEnable = dtrEnable; | ||
| // query and cache the initial RtsEnable value |
There was a problem hiding this comment.
The comment does not match the actual code _rtsEnable is written to but not read anywhere.
| _dataBits = dataBits; | ||
| _parity = parity; | ||
|
|
||
| selectList = new ArrayList(); |
There was a problem hiding this comment.
nit: this seems just the normal initialization for selectList without a line separation from the if it gives the impression that it is related to next statement.
|
|
||
| BaudRate = baudRate; | ||
|
|
||
| // now set this.RtsEnable to the specified value. |
| } | ||
| catch | ||
| { | ||
| // if there are any exceptions after the call to CreateFile, we need to be sure to close the |
There was a problem hiding this comment.
nit: Windows related comment
| // handle before we let them continue up. | ||
| tempHandle.Close(); | ||
| _handle = null; | ||
| //_threadPoolBinding?.Dispose(); |
There was a problem hiding this comment.
nit: delete commented out code
| _handle.Close(); | ||
| _handle = null; | ||
| if (PinChanged != null || ErrorReceived != null || DataReceived != null) | ||
| { |
There was a problem hiding this comment.
Anything to do in this case?
| } | ||
|
|
||
| if (numBytes == 0) | ||
| throw new TimeoutException(SR.Write_timed_out); |
There was a problem hiding this comment.
nit: some of the if statements followed by a single statement have blocks others not, I don't mind either way but it should be consistent at least for a type.
| throw new ArgumentOutOfRangeException(nameof(offset), SR.ArgumentOutOfRange_NeedPosNum); | ||
| if (count < 0) | ||
| throw new ArgumentOutOfRangeException(nameof(count), SR.ArgumentOutOfRange_NeedPosNum); | ||
| if (count == 0) return; // no need to expend overhead in creating asyncResult, etc. |
There was a problem hiding this comment.
nit: put the return on its own line (there are other occurrences of this).
|
@wfurt were you able to run any of the tests with actual ports in Linux? |
|
I did not attempt to run any test. I use my own app. I'd be happy to try. Are there any specific instructions? |
| /** | ||
| * Get states of serial line. | ||
| */ | ||
| extern "C" int32_t SystemNative_Terminal_GetDcd(int fd); |
There was a problem hiding this comment.
System.IO.Ports is standalone package, but this is adding dependency on System.Native. System.Native is internal implementation detail. We do not want things outside .NET Core distro to depend on it.
Should System.IO.Ports have its own supporting native library instead? Or depend on Mono.Posix?
The first thing will be the serial ports and respective configuration identified in https://github.com/dotnet/corefx/blob/master/src/System.IO.Ports/tests/Support/TCSupport.cs from there tests are properly attributed to run/skip according to available hardware. That said I do expect a lot of them to fail, it is more about getting a very rough grasp of their shape with this PR. |
related to #18012
This still needs some work and testing but I would like to get early feedback.
I can do simple serial IO now on Linux.
This is first minimal set of changes.