Skip to content
This repository was archived by the owner on Jan 23, 2023. It is now read-only.

WIP - initial serial port support for Linux#28415

Closed
wfurt wants to merge 1 commit intodotnet:masterfrom
wfurt:serial
Closed

WIP - initial serial port support for Linux#28415
wfurt wants to merge 1 commit intodotnet:masterfrom
wfurt:serial

Conversation

@wfurt
Copy link
Copy Markdown
Member

@wfurt wfurt commented Mar 23, 2018

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.

  • PAL uses POSIX + few BSD extensions. It builds on Linux and OSX. FreeBSD should be probably OK.
  • I tested only simple test program on Ubuntu with RS-232 and USB-Serial IOgear adapter
  • on Raspberry ttyAMA0 should be detected but I don't currently have gear to test actual transfer.
  • no async event notifications

@danmoseley
Copy link
Copy Markdown
Member

@kasper3 @JairoMarques @JohnRusk @dima117 @@daniel-mcassey-tmsuk @jamespettigrew in case folks want to try / give feedback.

@ghost
Copy link
Copy Markdown

ghost commented Mar 23, 2018

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.. :)

@wfurt
Copy link
Copy Markdown
Member Author

wfurt commented Mar 23, 2018

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>
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You said it worked on BSD and OSX (or at least built) .. if so, no condition should be required on TargetsXXX .

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@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 {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A nit - the coding convention is to place the { on a separate line.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another nit - it seems that it would be nice to name these "ParityXXXX" to match the style of the enum below.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: either rename as suggested by @janvorli or change NoneParity to NoParity

}

static speed_t
SystemNative_speed2termios(int speed)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you please keep the naming consistent - to start each part of the name with uppercase letter?

@karelz karelz added this to the 2.1.0 milestone Mar 27, 2018
@karelz karelz removed this from the 2.1.0 milestone Mar 28, 2018

using System.Runtime.InteropServices;
using Microsoft.Win32.SafeHandles;
using System.IO.Ports;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)]
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: remove one blank line

{
return -1;
}
return status;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: blank line after end of block

HandshakeBoth // Software & Hardware Flow Control
};

static int SystemNative_Termios_GetStatus(int fd)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: remove commented lines

term.c_iflag &= ~(static_cast<tcflag_t>(INPCK | ISTRIP));
switch (parity)
{
case OddParity:
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: align inside switch, make like the switch following it

return 0;
}

extern "C" int32_t SystemNative_Termios_GetDcd(int fd)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: spacing between function name and firt '('

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The same happens in other places of the file.

ports.Add("/dev/" + entry.Name);
}
}
return ports.ToArray();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.*"))
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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);
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: blank line after block

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same below

private bool _inBreak = false;
private SafeFileHandle _handle = null;
private bool _rtsEnable = false;
ArrayList selectList;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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" />
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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."))
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)
{

Copy link
Copy Markdown
Contributor

@pjanotti pjanotti Mar 28, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: remove blank line


internal void SetBufferSizes(int readBufferSize, int writeBufferSize)
{
if (_handle == null) throw new IOException();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That should be InvalidOperationException, or something else, not an actual IOException.

}

internal bool IsOpen => _handle != null;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is _handle null only after Dispose?

if (_handle == null) throw new IOException();

selectList.Add(_handle);
Socket.Select(selectList, null, null, timeout);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: spacing of last parameter


internal unsafe void Write(byte[] array, int offset, int count, int timeout)
{

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: remove blank line

int numBytes;
if (_isAsync)
{
throw new NotSupportedException(SR.PlatformNotSupported_IOPorts);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above: use a specific message

{
throw new NotSupportedException(SR.PlatformNotSupported_IOPorts);
}
else
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this doesn't need to be in a else block


if (numBytes == 0)
throw new TimeoutException(SR.Write_timed_out);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)
{

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typically we have the constructors at the top of the source after the fields.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: there is a extra blank line here too

}

DtrEnable = dtrEnable;
// query and cache the initial RtsEnable value
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment does not match the actual code _rtsEnable is written to but not read anywhere.

_dataBits = dataBits;
_parity = parity;

selectList = new ArrayList();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: identation

}
catch
{
// if there are any exceptions after the call to CreateFile, we need to be sure to close the
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Windows related comment

// handle before we let them continue up.
tempHandle.Close();
_handle = null;
//_threadPoolBinding?.Dispose();
Copy link
Copy Markdown
Contributor

@pjanotti pjanotti Mar 28, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: delete commented out code

_handle.Close();
_handle = null;
if (PinChanged != null || ErrorReceived != null || DataReceived != null)
{
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Anything to do in this case?

}

if (numBytes == 0)
throw new TimeoutException(SR.Write_timed_out);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: put the return on its own line (there are other occurrences of this).

@pjanotti
Copy link
Copy Markdown
Contributor

@wfurt were you able to run any of the tests with actual ports in Linux?

@wfurt
Copy link
Copy Markdown
Member Author

wfurt commented Mar 28, 2018

I did not attempt to run any test. I use my own app. I'd be happy to try. Are there any specific instructions?

@karelz karelz added this to the 2.2.0 milestone Apr 4, 2018
/**
* Get states of serial line.
*/
extern "C" int32_t SystemNative_Terminal_GetDcd(int fd);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

@pjanotti
Copy link
Copy Markdown
Contributor

I did not attempt to run any test. I use my own app. I'd be happy to try. Are there any specific instructions?

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.

@wfurt
Copy link
Copy Markdown
Member Author

wfurt commented Jun 25, 2018

I run bunch of tests as part of #29033 @pjanotti This PR is old and I'll try to recover effort on getting #29033 (and second part) completed.

@wfurt wfurt closed this Jun 25, 2018
@wfurt wfurt deleted the serial branch June 15, 2020 18:21
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants