diff --git a/src/System.IO.Ports/src/Configurations.props b/src/System.IO.Ports/src/Configurations.props index b747a1461244..1697d233e028 100644 --- a/src/System.IO.Ports/src/Configurations.props +++ b/src/System.IO.Ports/src/Configurations.props @@ -3,6 +3,7 @@ netstandard-Windows_NT; + netstandard-Linux; netstandard; net461; diff --git a/src/System.IO.Ports/src/Interop/Unix/Interop.Termios.cs b/src/System.IO.Ports/src/Interop/Unix/Interop.Termios.cs index 1e65aca21897..29743b9a6ceb 100644 --- a/src/System.IO.Ports/src/Interop/Unix/Interop.Termios.cs +++ b/src/System.IO.Ports/src/Interop/Unix/Interop.Termios.cs @@ -42,7 +42,7 @@ internal enum Queue internal static extern int TermiosGetSpeed(SafeFileHandle handle); [DllImport(Libraries.IOPortsNative, EntryPoint = "SystemIoPortsNative_TermiosAvailableBytes", SetLastError = true)] - internal static extern int TermiosGetAvailableBytes(SafeFileHandle handle, int input); + internal static extern int TermiosGetAvailableBytes(SafeFileHandle handle, Queue input); [DllImport(Libraries.IOPortsNative, EntryPoint = "SystemIoPortsNative_TermiosDiscard", SetLastError = true)] internal static extern int TermiosDiscard(SafeFileHandle handle, Queue input); diff --git a/src/System.IO.Ports/src/System.IO.Ports.csproj b/src/System.IO.Ports/src/System.IO.Ports.csproj index 2128036904c5..7710ff209cf0 100644 --- a/src/System.IO.Ports/src/System.IO.Ports.csproj +++ b/src/System.IO.Ports/src/System.IO.Ports.csproj @@ -3,15 +3,14 @@ true {187503F4-BEF9-4369-A1B2-E3DC5D564E4E} true - SR.PlatformNotSupported_IOPorts + SR.PlatformNotSupported_IOPorts $(DefineConstants);NOSPAN true - net461-Debug;net461-Release;netfx-Debug;netfx-Release;netstandard-Debug;netstandard-Release;netstandard-Windows_NT-Debug;netstandard-Windows_NT-Release;uap-Windows_NT-Debug;uap-Windows_NT-Release + net461-Debug;net461-Release;netfx-Debug;netfx-Release;netstandard-Debug;netstandard-Release;netstandard-Windows_NT-Debug;netstandard-Windows_NT-Release;uap-Windows_NT-Debug;uap-Windows_NT-Release;netstandard-Linux-Debug;netstandard-Linux-Release - + - @@ -23,9 +22,12 @@ - - + + + + + Common\Interop\Windows\kernel32\Interop.DCB.cs @@ -139,6 +141,30 @@ + + + + + + + Common\Interop\Unix\Interop.Libraries.cs + + + Common\Interop\Unix\Interop.Errors.cs + + + Interop\Unix\System.Native\Interop.Read.cs + + + Interop\Unix\System.Native\Interop.Write.cs + + + Interop\Unix\System.Native\Interop.Poll.cs + + + Interop\Unix\System.Native\Interop.Shutdown.cs + + @@ -161,4 +187,19 @@ - \ No newline at end of file + + + + + + + + + + + + + + + + diff --git a/src/System.IO.Ports/src/System/IO/Ports/SerialPort.Linux.cs b/src/System.IO.Ports/src/System/IO/Ports/SerialPort.Linux.cs new file mode 100644 index 000000000000..c137c24ea8e6 --- /dev/null +++ b/src/System.IO.Ports/src/System/IO/Ports/SerialPort.Linux.cs @@ -0,0 +1,43 @@ +// 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. + +using System; +using System.ComponentModel; +using System.Collections.Generic; +using System.IO; + +namespace System.IO.Ports +{ + public partial class SerialPort : Component + { + public static string[] GetPortNames() + { + const string sysTtyDir = "/sys/class/tty"; + const string sysUsbDir = "/sys/bus/usb-serial/devices/"; + + if (Directory.Exists(sysTtyDir)) + { + // /sys is mounted. Let's explore tty class and pick active nodes. + List ports = new List(); + DirectoryInfo di = new DirectoryInfo(sysTtyDir); + var entries = di.EnumerateFileSystemInfos(@"*", SearchOption.TopDirectoryOnly); + foreach (var entry in entries) + { + if (Directory.Exists(sysUsbDir + entry.Name) || File.Exists(entry.FullName + "/device/id")) + { + ports.Add("/dev/" + entry.Name); + } + } + + return ports.ToArray(); + } + else + { + // Fallback to scanning /dev. That may have more devices then needed. + // This can also miss usb or serial devices with non-standard name. + return Directory.GetFiles("/dev", "ttyS*"); + } + } + } +} diff --git a/src/System.IO.Ports/src/System/IO/Ports/SerialPort.OSX.cs b/src/System.IO.Ports/src/System/IO/Ports/SerialPort.OSX.cs new file mode 100644 index 000000000000..dcd2a47eb1c2 --- /dev/null +++ b/src/System.IO.Ports/src/System/IO/Ports/SerialPort.OSX.cs @@ -0,0 +1,39 @@ +// 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. + +using System; +using System.ComponentModel; +using System.Collections.Generic; +using System.IO; + +namespace System.IO.Ports +{ + public partial class SerialPort : Component + { + public static string[] GetPortNames() + { + List ports = new List(); + + foreach (string name in Directory.GetFiles("/dev", "tty.*")) + { + // GetFiles can return unexpected results because of 8.3 matching. + // Like /dev/tty + if (name.StartsWith("/dev/tty.")) + { + ports.Add(name); + } + } + + foreach (string name in Directory.GetFiles("/dev", "cu.*")) + { + if (name.StartsWith("/dev/cu.")) + { + ports.Add(name); + } + } + + return ports.ToArray(); + } + } +} diff --git a/src/System.IO.Ports/src/System/IO/Ports/SerialPort.cs b/src/System.IO.Ports/src/System/IO/Ports/SerialPort.Windows.cs similarity index 100% rename from src/System.IO.Ports/src/System/IO/Ports/SerialPort.cs rename to src/System.IO.Ports/src/System/IO/Ports/SerialPort.Windows.cs diff --git a/src/System.IO.Ports/src/System/IO/Ports/SerialStream.Unix.cs b/src/System.IO.Ports/src/System/IO/Ports/SerialStream.Unix.cs new file mode 100644 index 000000000000..63009b2ecf71 --- /dev/null +++ b/src/System.IO.Ports/src/System/IO/Ports/SerialStream.Unix.cs @@ -0,0 +1,583 @@ +// 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. + +using Microsoft.Win32.SafeHandles; +using System.Collections; +using System.Diagnostics; +using System.Runtime.InteropServices; +using System.IO.Ports; +using System.Net.Sockets; + +namespace System.IO.Ports +{ + internal sealed partial class SerialStream : Stream + { + private const int MaxDataBits = 8; + private const int MinDataBits = 5; + + private string _portName; + private int _baudRate; + private StopBits _stopBits; + private Handshake _handshake; + private Parity _parity; + private int _dataBits = 8; + private bool _inBreak = false; + private SafeFileHandle _handle = null; + private bool _rtsEnable = false; + private int _readTimeout = 0; + private int _writeTimeout = 0; + private byte[] _tempBuf = new byte[1]; + + // three different events, also wrapped by SerialPort. + internal event SerialDataReceivedEventHandler DataReceived; // called when one character is received. + internal event SerialPinChangedEventHandler PinChanged; // called when any of the pin/ring-related triggers occurs + internal event SerialErrorReceivedEventHandler ErrorReceived; // called when any runtime error occurs on the port (frame, overrun, parity, etc.) + + // ----SECTION: inherited properties from Stream class ------------* + + // These six properties are required for SerialStream to inherit from the abstract Stream class. + // Note four of them are always true or false, and two of them throw exceptions, so these + // are not usefully queried by applications which know they have a SerialStream, etc... + + public override int ReadTimeout + { + get { return _readTimeout; } + set + { + if (value < 0 && value != SerialPort.InfiniteTimeout) + throw new ArgumentOutOfRangeException(nameof(ReadTimeout), SR.ArgumentOutOfRange_Timeout); + if (_handle == null) { + throw new ObjectDisposedException(SR.Port_not_open); + } + _readTimeout = value; + } + } + + public override int WriteTimeout + { + get { return _writeTimeout; } + set + { + if (value < 0 && value != SerialPort.InfiniteTimeout) + throw new ArgumentOutOfRangeException(nameof(ReadTimeout), SR.ArgumentOutOfRange_Timeout); + if (_handle == null) { + throw new ObjectDisposedException(SR.Port_not_open); + } + _writeTimeout = value; + } + } + + public override bool CanRead + { + get { return (_handle != null); } + } + + public override bool CanSeek + { + get { return false; } + } + + public override bool CanTimeout + { + get { return (_handle != null); } + } + + public override bool CanWrite + { + get { return (_handle != null); } + } + + public override long Length + { + get { throw new NotSupportedException(SR.NotSupported_UnseekableStream); } + } + + public override long Position + { + get { throw new NotSupportedException(SR.NotSupported_UnseekableStream); } + set { throw new NotSupportedException(SR.NotSupported_UnseekableStream); } + } + + internal int BaudRate + { + set + { + if (value != _baudRate) + { + if (value <= 0 || value > 230400) + { + throw new ArgumentOutOfRangeException(nameof(BaudRate), SR.ArgumentOutOfRange_NeedPosNum); + } + + if (Interop.Termios.TermiosSetSpeed(_handle, value) < 0) + { + throw new IOException(); + } + + _baudRate = value; + } + } + + get + { + return Interop.Termios.TermiosGetSpeed(_handle); + } + } + + public bool BreakState + { + get { return _inBreak; } + set + { + if (value) + { + // Unlike Windows, there is no infinite break and positive value is platform dependent. + // As best guess, send break with default duration. + Interop.Termios.TermiosSendBreak(_handle, 0); + } + _inBreak = value; + } + } + + internal int BytesToWrite + { + get { return Interop.Termios.TermiosGetAvailableBytes(_handle, Interop.Termios.Queue.SendQueue); } + } + + internal int BytesToRead + { + get { return Interop.Termios.TermiosGetAvailableBytes(_handle, Interop.Termios.Queue.ReceiveQueue); } + } + + internal bool CDHolding + { + get + { + int status = Interop.Termios.TermiosGetSignal(_handle, Interop.Termios.Signals.SignalDcd); + if (status < 0) + { + throw new IOException(); + } + + return status == 1; + } + } + + internal bool CtsHolding + { + get + { + int status = Interop.Termios.TermiosGetSignal(_handle, Interop.Termios.Signals.SignalCts); + if (status < 0) + { + throw new IOException(); + } + + return status == 1; + } + } + + internal bool DsrHolding + { + get + { + int status = Interop.Termios.TermiosGetSignal(_handle, Interop.Termios.Signals.SignalDsr); + if (status < 0) + { + throw new IOException(); + } + + return status == 1; + } + } + + internal bool DtrEnable + { + get + { + int status = Interop.Termios.TermiosGetSignal(_handle, Interop.Termios.Signals.SignalDtr); + if (status < 0) + { + throw new IOException(); + } + + return status == 1; + } + + set + { + if (Interop.Termios.TermiosGetSignal(_handle, Interop.Termios.Signals.SignalDtr, value ? 1 : 0) != 0) + { + throw new IOException(); + } + } + } + + internal bool RtsEnable + { + get + { + int status = Interop.Termios.TermiosGetSignal(_handle, Interop.Termios.Signals.SignalRts); + if (status < 0) + { + throw new IOException(); + } + + return status == 1; + } + + set + { + if (Interop.Termios.TermiosGetSignal(_handle, Interop.Termios.Signals.SignalRts, value ? 1 : 0) != 0) + { + throw new IOException(); + } + } + } + + internal Handshake Handshake + { + set + { + Debug.Assert(!(value < Handshake.None || value > Handshake.RequestToSendXOnXOff), + "An invalid value was passed to Handshake"); + + if (value != _handshake) + { + if (Interop.Termios.TermiosReset(_handle, _baudRate, _dataBits, _stopBits, _parity, _handshake) != 0) + { + throw new ArgumentException(); + } + _handshake = value; + } + } + } + + internal int DataBits + { + set + { + Debug.Assert(!(value < MinDataBits || value > MaxDataBits), "An invalid value was passed to DataBits"); + if (value != _dataBits) + { + _dataBits = value; + if (Interop.Termios.TermiosReset(_handle, _baudRate, _dataBits, _stopBits, _parity, _handshake) != 0) + { + throw new ArgumentException(); + } + } + } + } + + internal Parity Parity + { + set + { + Debug.Assert(!(value < Parity.None || value > Parity.Space), "An invalid value was passed to Parity"); + + if (value != _parity) + { + _parity = value; + if (Interop.Termios.TermiosReset(_handle, _baudRate, _dataBits, _stopBits, _parity, _handshake) != 0) + { + throw new ArgumentException(); + } + } + } + } + + internal StopBits StopBits + { + set + { + Debug.Assert(!(value < StopBits.One || value > StopBits.OnePointFive), "An invalid value was passed to StopBits"); + if (value != _stopBits) + { + _stopBits = value; + if (Interop.Termios.TermiosReset(_handle, _baudRate, _dataBits, _stopBits, _parity, _handshake) != 0) + { + throw new ArgumentException(); + } + } + } + } + + internal bool DiscardNull + { + set + { + // Ignore. + } + } + + internal byte ParityReplace + { + set + { + // Ignore. + } + } + + internal void DiscardInBuffer() + { + if (_handle == null) throw new ObjectDisposedException(SR.Port_not_open); + // This may or may not work depending on hardware. + Interop.Termios.TermiosDiscard(_handle, Interop.Termios.Queue.ReceiveQueue); + } + + internal void DiscardOutBuffer() + { + if (_handle == null) throw new ObjectDisposedException(SR.Port_not_open); + // This may or may not work depending on hardware. + Interop.Termios.TermiosDiscard(_handle, Interop.Termios.Queue.SendQueue); + } + + internal void SetBufferSizes(int readBufferSize, int writeBufferSize) + { + if (_handle == null) throw new ObjectDisposedException(SR.Port_not_open); + + // Ignore for now. + } + + internal bool IsOpen => _handle != null; + + + // Flush dumps the contents of the serial driver's internal read and write buffers. + // We actually expose the functionality for each, but fulfilling Stream's contract + // requires a Flush() method. Fails if handle closed. + // 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); + Interop.Termios.TermiosDiscard(_handle, Interop.Termios.Queue.AllQueues); + } + + public override long Seek(long offset, SeekOrigin origin) + { + throw new NotSupportedException(SR.NotSupported_UnseekableStream); + } + + public override void SetLength(long value) + { + throw new NotSupportedException(SR.NotSupported_UnseekableStream); + } + + public override int ReadByte() + { + return ReadByte(ReadTimeout); + } + + internal int ReadByte(int timeout) + { + Read(_tempBuf, 0, 1, timeout); + return _tempBuf[0]; + } + + public override int Read(byte[] array, int offset, int count) + { + return Read(array, offset, count, ReadTimeout); + } + + internal unsafe int Read(byte[] array, int offset, int count, int timeout) + { + if (_handle == null) throw new ObjectDisposedException(SR.Port_not_open); + if (array == null) + throw new ArgumentNullException(nameof(array)); + if (offset < 0) + throw new ArgumentOutOfRangeException(nameof(offset), SR.ArgumentOutOfRange_NeedNonNegNumRequired); + if (count < 0) + throw new ArgumentOutOfRangeException(nameof(count), SR.ArgumentOutOfRange_NeedNonNegNumRequired); + if (array.Length - offset < count) + throw new ArgumentException(SR.Argument_InvalidOffLen); + if (count == 0) return 0; // return immediately if no bytes requested; no need for overhead. + + if (timeout != 0) + { + Interop.Sys.PollEvents events = Interop.Sys.PollEvents.POLLNONE; + Interop.Sys.Poll(_handle, Interop.Sys.PollEvents.POLLIN | Interop.Sys.PollEvents.POLLERR, timeout,out events); + + if ((events & (Interop.Sys.PollEvents.POLLERR | Interop.Sys.PollEvents.POLLNVAL)) != 0) + { + throw new IOException(); + } + + if ( (events & Interop.Sys.PollEvents.POLLIN) == 0) + { + throw new TimeoutException(); + } + } + + int numBytes; + fixed (byte* bufPtr = array) + { + numBytes = Interop.Sys.Read(_handle, bufPtr + offset, count); + } + + if (numBytes < 0) + { + if (Interop.Error.EWOULDBLOCK == Interop.Sys.GetLastError()) + { + throw new TimeoutException(); + } + throw new IOException(); + } + + return numBytes; + } + + public override void Write(byte[] array, int offset, int count) + { + Write(array, offset, count, WriteTimeout); + } + + internal unsafe void Write(byte[] array, int offset, int count, int timeout) + { + if (_inBreak) + throw new InvalidOperationException(SR.In_Break_State); + if (array == null) + throw new ArgumentNullException(nameof(array)); + if (offset < 0) + 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. + if (array.Length - offset < count) + throw new ArgumentException(SR.Argument_InvalidOffLen); + + // check for open handle, though the port is always supposed to be open + if (_handle == null) throw new ObjectDisposedException(SR.Port_not_open); + + int numBytes = 0; + + while (count > 0) + { + if (timeout > 0) + { + Interop.Sys.PollEvents events = Interop.Sys.PollEvents.POLLNONE; + Interop.Sys.Poll(_handle, Interop.Sys.PollEvents.POLLOUT | Interop.Sys.PollEvents.POLLERR, timeout,out events); + + if ((events & (Interop.Sys.PollEvents.POLLERR | Interop.Sys.PollEvents.POLLNVAL)) != 0) + { + throw new IOException(); + } + + if ( (events & Interop.Sys.PollEvents.POLLOUT) == 0) + { + throw new TimeoutException(SR.Write_timed_out); + } + } + + fixed (byte* bufPtr = array) + { + numBytes = Interop.Sys.Write(_handle, bufPtr + offset, count); + } + + if (numBytes == -1) + { + throw new IOException(); + } + + if (numBytes == 0) + { + throw new TimeoutException(SR.Write_timed_out); + } + count -= numBytes; + offset += numBytes; + } + } + + internal SafeFileHandle OpenPort(string portName) + { + SafeFileHandle handle = Interop.Serial.SerialPortOpen(portName); + + return handle; + } + + // this method is used by SerialPort upon SerialStream's creation + 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) + { + + if (portName == null) + { + throw new ArgumentException(SR.Arg_InvalidSerialPort, nameof(portName)); + } + + // Error checking done in SerialPort. + + SafeFileHandle tempHandle = OpenPort(portName); + + if (tempHandle.IsInvalid) + { + throw new ArgumentException(SR.Arg_InvalidSerialPort, nameof(portName)); + } + + try + { + _handle = tempHandle; + // set properties of the stream that exist as members in SerialStream + _portName = portName; + _handshake = handshake; + _parity = parity; + _readTimeout = readTimeout; + _writeTimeout = writeTimeout; + _baudRate = baudRate; + _stopBits = stopBits; + _dataBits = dataBits; + _parity = parity; + + if (Interop.Termios.TermiosReset(_handle, _baudRate, _dataBits, _stopBits, _parity, _handshake) != 0) + { + throw new ArgumentException(); + } + + DtrEnable = dtrEnable; + // query and cache the initial RtsEnable value + // so that set_RtsEnable can do the (value != rtsEnable) optimization + //_rtsEnable = (GetDcbFlag(NativeMethods.FRTSCONTROL) == NativeMethods.RTS_CONTROL_ENABLE); + _rtsEnable = RtsEnable; + + BaudRate = baudRate; + + // now set this.RtsEnable to the specified value. + // Handshake takes precedence, this will be a nop if + // handshake is either RequestToSend or RequestToSendXOnXOff + if ((handshake != Handshake.RequestToSend && handshake != Handshake.RequestToSendXOnXOff)) + { + RtsEnable = rtsEnable; + } + } + catch + { + // if there are any exceptions after the call to CreateFile, we need to be sure to close the + // handle before we let them continue up. + tempHandle.Close(); + _handle = null; + throw; + } + } + + ~SerialStream() + { + Dispose(false); + } + + protected override void Dispose(bool disposing) + { + // Signal the other side that we're closing. Should do regardless of whether we've called + // Close() or not Dispose() + if (_handle != null && !_handle.IsInvalid) + { + Interop.Sys.Shutdown(_handle, SocketShutdown.Both); + _handle.Close(); + _handle = null; + base.Dispose(disposing); + if (PinChanged != null || ErrorReceived != null || DataReceived != null) + { + } + base.Dispose(disposing); + } + } + } +} diff --git a/src/System.IO.Ports/src/System/IO/Ports/SerialStream.cs b/src/System.IO.Ports/src/System/IO/Ports/SerialStream.Windows.cs similarity index 100% rename from src/System.IO.Ports/src/System/IO/Ports/SerialStream.cs rename to src/System.IO.Ports/src/System/IO/Ports/SerialStream.Windows.cs diff --git a/src/System.IO.Ports/tests/Configurations.props b/src/System.IO.Ports/tests/Configurations.props index 1e5845b0b616..17e83be7494e 100644 --- a/src/System.IO.Ports/tests/Configurations.props +++ b/src/System.IO.Ports/tests/Configurations.props @@ -3,7 +3,8 @@ netstandard-Windows_NT; + netstandard-Linux; netfx; - \ No newline at end of file + diff --git a/src/System.IO.Ports/tests/SerialPort/RtsEnable.cs b/src/System.IO.Ports/tests/SerialPort/RtsEnable.cs index c0aee56a2fcc..230eef1518e0 100644 --- a/src/System.IO.Ports/tests/SerialPort/RtsEnable.cs +++ b/src/System.IO.Ports/tests/SerialPort/RtsEnable.cs @@ -196,6 +196,7 @@ public void RtsEnable_Get_Handshake_RequestToSend() } [ConditionalFact(nameof(HasOneSerialPort))] + [PlatformSpecific(~TestPlatforms.AnyUnix)] public void RtsEnable_Get_Handshake_RequestToSendXOnXOff() { using (SerialPort com1 = new SerialPort(TCSupport.LocalMachineSerialInfo.FirstAvailablePortName)) diff --git a/src/System.IO.Ports/tests/Support/PortHelper.cs b/src/System.IO.Ports/tests/Support/PortHelper.cs index fce77e6916ad..d562606ac30c 100644 --- a/src/System.IO.Ports/tests/Support/PortHelper.cs +++ b/src/System.IO.Ports/tests/Support/PortHelper.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using System.IO.Ports; using System.Runtime.InteropServices; using System.Text.RegularExpressions; @@ -20,6 +21,11 @@ public class PortHelper public static string[] GetPorts() { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + return SerialPort.GetPortNames(); + } + if (PlatformDetection.IsUap) { return new [] { "COM3", "COM4", "COM5", "COM6", "COM7" }; // we are waiting for a Win32 new QueryDosDevice API since the current doesn't work for Uap https://github.com/dotnet/corefx/issues/21156 diff --git a/src/System.IO.Ports/tests/System.IO.Ports.Tests.csproj b/src/System.IO.Ports/tests/System.IO.Ports.Tests.csproj index 043fda7ce748..5e5dc4137d79 100644 --- a/src/System.IO.Ports/tests/System.IO.Ports.Tests.csproj +++ b/src/System.IO.Ports/tests/System.IO.Ports.Tests.csproj @@ -28,7 +28,6 @@ - @@ -41,12 +40,10 @@ - - @@ -114,7 +111,12 @@ + + + + + - \ No newline at end of file +