//****************************************************************************************************** // SerialClient.cs - Gbtc // // Copyright © 2012, Grid Protection Alliance. All Rights Reserved. // // Licensed to the Grid Protection Alliance (GPA) under one or more contributor license agreements. See // the NOTICE file distributed with this work for additional information regarding copyright ownership. // The GPA licenses this file to you under the MIT License (MIT), the "License"; you may // not use this file except in compliance with the License. You may obtain a copy of the License at: // // http://www.opensource.org/licenses/MIT // // Unless agreed to in writing, the subject software distributed under the License is distributed on an // "AS-IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. Refer to the // License for the specific language governing permissions and limitations. // // Code Modification History: // ---------------------------------------------------------------------------------------------------- // 07/24/2006 - Pinal C. Patel // Original version of source code generated. // 09/06/2006 - J. Ritchie Carroll // Added bypass optimizations for high-speed serial port data access. // 09/29/2008 - J. Ritchie Carroll // Converted to C#. // 07/08/2009 - J. Ritchie Carroll // Added WaitHandle return value from asynchronous connection. // 07/15/2009 - Pinal C. Patel // Modified Disconnect() to add error checking. // 09/14/2009 - Stephen C. Wills // Added new header and license agreement. // 11/29/2010 - Pinal C. Patel // Corrected the implementation of ConnectAsync() method. // 09/21/2011 - J. Ritchie Carroll // Added Mono implementation exception regions. // 12/13/2012 - Starlynn Danyelle Gilliam // Modified Header. // //****************************************************************************************************** using System; using System.Collections.Generic; using System.ComponentModel; using System.IO.Ports; using System.Text; using System.Threading; namespace GSF.Communication { /// /// Represents a communication client based on . /// /// /// This example shows how to use for communicating with : /// /// using System; /// using GSF; /// using GSF.Communication; /// /// class Program /// { /// static SerialClient s_client; /// /// static void Main(string[] args) /// { /// // Initialize the client. /// s_client = new SerialClient("Port=COM1; BaudRate=9600; Parity=None; StopBits=One; DataBits=8; DtrEnable=False; RtsEnable=False"); /// s_client.Initialize(); /// // Register event handlers. /// s_client.ConnectionAttempt += s_client_ConnectionAttempt; /// s_client.ConnectionEstablished += s_client_ConnectionEstablished; /// s_client.ConnectionTerminated += s_client_ConnectionTerminated; /// s_client.SendDataComplete += s_client_SendDataComplete; /// s_client.ReceiveDataComplete += s_client_ReceiveDataComplete; /// // Connect the client. /// s_client.Connect(); /// /// // Write user input to the serial port. /// string input; /// while (string.Compare(input = Console.ReadLine(), "Exit", true) != 0) /// { /// s_client.Send(input); /// } /// /// // Disconnect the client on shutdown. /// s_client.Dispose(); /// } /// /// static void s_client_ConnectionAttempt(object sender, EventArgs e) /// { /// Console.WriteLine("Client is connecting to serial port."); /// } /// /// static void s_client_ConnectionEstablished(object sender, EventArgs e) /// { /// Console.WriteLine("Client connected to serial port."); /// } /// /// static void s_client_ConnectionTerminated(object sender, EventArgs e) /// { /// Console.WriteLine("Client disconnected from serial port."); /// } /// /// static void s_client_SendDataComplete(object sender, EventArgs e) /// { /// Console.WriteLine(string.Format("Sent data - {0}", s_client.TextEncoding.GetString(s_client.Client.SendBuffer))); /// } /// /// static void s_client_ReceiveDataComplete(object sender, EventArgs<byte[], int> e) /// { /// Console.WriteLine(string.Format("Received data - {0}", s_client.TextEncoding.GetString(e.Argument1, 0, e.Argument2))); /// } /// } /// /// public class SerialClient : ClientBase { #region [ Members ] // Constants /// /// Specifies the default value for the property. /// public const string DefaultConnectionString = "Port=COM1; BaudRate=9600; Parity=None; StopBits=One; DataBits=8; DtrEnable=False; RtsEnable=False; ReceivedBytesThreshold=512"; /// /// Default value for property. /// public const int DefaultReceivedBytesThreshold = 1; // Fields private readonly TransportProvider m_serialClient; private Dictionary m_connectData; private ManualResetEvent m_connectionHandle; private Thread m_connectionThread; private bool m_disposed; #endregion #region [ Constructors ] /// /// Initializes a new instance of the class. /// public SerialClient() : this(DefaultConnectionString) { } /// /// Initializes a new instance of the class. /// /// Connect string of the . See for format. public SerialClient(string connectString) : base(TransportProtocol.Serial, connectString) => m_serialClient = new TransportProvider(); /// /// Initializes a new instance of the class. /// /// object that contains the . public SerialClient(IContainer container) : this() => container?.Add(this); #endregion #region [ Properties ] /// /// Gets the object for the . /// [Browsable(false)] public SerialPort Client => m_serialClient.Provider; /// /// Gets the server URI of the . /// [Browsable(false)] public override string ServerUri => $"{TransportProtocol}://{m_connectData["port"]}".ToLower(); /// /// Gets or sets the needed number of bytes in the internal input buffer before a event occurs. /// /// /// This option is ignored under Mono deployments. /// [DefaultValue(DefaultReceivedBytesThreshold)] public int ReceivedBytesThreshold { get; set; } = DefaultReceivedBytesThreshold; /// /// Gets the descriptive status of the . /// public override string Status { get { if (m_connectData == null) return base.Status; StringBuilder status = new StringBuilder(); status.Append(base.Status); if (m_connectData.TryGetValue("port", out string value)) status.AppendLine($" COM port: {value}"); if (m_connectData.TryGetValue("baudRate", out value)) status.AppendLine($" Baud rate: {value}bps"); if (m_connectData.TryGetValue("parity", out value)) status.AppendLine($" Parity: {value}"); if (m_connectData.TryGetValue("stopBits", out value)) status.AppendLine($" Stop bits: {value}"); if (m_connectData.TryGetValue("dataBits", out value)) status.AppendLine($" Data bits: {value}"); if (m_connectData.TryGetValue("receivedBytesThreshold", out value)) status.AppendLine($" Received bytes threshold: {value}"); return status.ToString(); } } #endregion #region [ Methods ] /// /// Reads a number of bytes from the current received data buffer and writes those bytes into a byte array at the specified offset. /// /// Destination buffer used to hold copied bytes. /// 0-based starting index into destination to begin writing data. /// The number of bytes to read from current received data buffer and write into . /// The number of bytes read. /// /// This function should only be called from within the event handler. Calling this method outside /// this event will have unexpected results. /// /// No received data buffer has been defined to read. /// is null. /// /// or is less than 0 -or- /// and will exceed length. /// public override int Read(byte[] buffer, int startIndex, int length) { buffer.ValidateParameters(startIndex, length); if (m_serialClient.ReceiveBuffer == null) throw new InvalidOperationException("No received data buffer has been defined to read."); int sourceLength = m_serialClient.BytesReceived - ReadIndex; int readBytes = length > sourceLength ? sourceLength : length; Buffer.BlockCopy(m_serialClient.ReceiveBuffer, ReadIndex, buffer, startIndex, readBytes); // Update read index for next call ReadIndex += readBytes; if (ReadIndex >= m_serialClient.BytesReceived) ReadIndex = 0; return readBytes; } /// /// Disconnects the from the . /// public override void Disconnect() { if (CurrentState == ClientState.Disconnected) return; if (m_serialClient.Provider != null) { m_serialClient.Provider.DataReceived -= SerialPort_DataReceived; m_serialClient.Provider.ErrorReceived -= SerialPort_ErrorReceived; } m_serialClient.Reset(); m_connectionThread?.Abort(); OnConnectionTerminated(); } /// /// Connects the to the asynchronously. /// /// Attempt is made to connect the when it is connected. /// for the asynchronous operation. public override WaitHandle ConnectAsync() { m_connectionHandle = (ManualResetEvent)base.ConnectAsync(); m_serialClient.SetReceiveBuffer(ReceiveBufferSize); m_serialClient.Provider = new SerialPort { #if !MONO ReceivedBytesThreshold = int.Parse(m_connectData["receivedBytesThreshold"]), #endif PortName = m_connectData["port"], BaudRate = int.Parse(m_connectData["baudRate"]), DataBits = int.Parse(m_connectData["dataBits"]), Parity = (Parity)Enum.Parse(typeof(Parity), m_connectData["parity"], true), StopBits = (StopBits)Enum.Parse(typeof(StopBits), m_connectData["stopBits"], true) }; m_serialClient.Provider.DataReceived += SerialPort_DataReceived; m_serialClient.Provider.ErrorReceived += SerialPort_ErrorReceived; if (m_connectData.ContainsKey("dtrEnable")) m_serialClient.Provider.DtrEnable = m_connectData["dtrEnable"].ParseBoolean(); if (m_connectData.ContainsKey("rtsEnable")) m_serialClient.Provider.RtsEnable = m_connectData["rtsEnable"].ParseBoolean(); m_connectionThread = new Thread(OpenPort) { IsBackground = true }; m_connectionThread.Start(); return m_connectionHandle; } /// /// Releases the unmanaged resources used by the and optionally releases the managed resources. /// /// true to release both managed and unmanaged resources; false to release only unmanaged resources. protected override void Dispose(bool disposing) { if (m_disposed) return; try { // This will be done regardless of whether the object is finalized or disposed. if (!disposing) return; // This will be done only when the object is disposed by calling Dispose(). m_connectionHandle?.Dispose(); } finally { m_disposed = true; // Prevent duplicate dispose. base.Dispose(disposing); // Call base class Dispose(). } } /// /// Validates the specified . /// /// Connection string to be validated. /// Port property is missing. /// BaudRate property is missing. /// Parity property is missing. /// StopBits property is missing. /// DataBits property is missing. protected override void ValidateConnectionString(string connectionString) { m_connectData = connectionString.ParseKeyValuePairs(); if (!m_connectData.ContainsKey("port")) throw new ArgumentException($"Port property is missing (Example: {DefaultConnectionString})"); if (!m_connectData.ContainsKey("baudRate") || !int.TryParse(m_connectData["baudRate"], out _)) throw new ArgumentException($"BaudRate property is missing or invalid (Example: {DefaultConnectionString})"); if (!m_connectData.ContainsKey("parity")) throw new ArgumentException($"Parity property is missing (Example: {DefaultConnectionString})"); if (!m_connectData.ContainsKey("stopBits")) throw new ArgumentException($"StopBits property is missing (Example: {DefaultConnectionString})"); if (!m_connectData.ContainsKey("dataBits") || !int.TryParse(m_connectData["dataBits"], out _)) throw new ArgumentException($"DataBits property is missing or invalid (Example: {DefaultConnectionString})"); if (!m_connectData.ContainsKey("receivedBytesThreshold") || !int.TryParse(m_connectData["receivedBytesThreshold"], out _)) m_connectData["receivedBytesThreshold"] = ReceivedBytesThreshold.ToString(); } /// /// Sends (writes) data to the asynchronously. /// /// The buffer that contains the binary data to be sent (written). /// The zero-based position in the at which to begin sending (writing) data. /// The number of bytes to be sent (written) from starting at the . /// for the asynchronous operation. protected override WaitHandle SendDataAsync(byte[] data, int offset, int length) { // Send data to the file asynchronously. WaitHandle handle = m_serialClient.Provider.BaseStream.BeginWrite(data, offset, length, SendDataAsyncCallback, null).AsyncWaitHandle; // Notify that the send operation has started. m_serialClient.Statistics.UpdateBytesSent(length); OnSendDataStart(); // Return the async handle that can be used to wait for the async operation to complete. return handle; } /// /// Callback method for asynchronous send operation. /// private void SendDataAsyncCallback(IAsyncResult asyncResult) { try { // Send operation is complete. m_serialClient.Provider.BaseStream.EndWrite(asyncResult); OnSendDataComplete(); } catch (Exception ex) { // Send operation failed to complete - don't raised exceptions for a closed port if (!m_disposed && m_serialClient.Provider != null && m_serialClient.Provider.IsOpen) OnSendDataException(ex); } } /// /// Connects to the . /// private void OpenPort() { int connectionAttempts = 0; while (MaxConnectionAttempts == -1 || connectionAttempts < MaxConnectionAttempts) { try { OnConnectionAttempt(); m_serialClient.Provider.Open(); m_connectionHandle.Set(); OnConnectionEstablished(); break; } catch (ThreadAbortException) { break; } catch (Exception ex) { Thread.Sleep(1000); connectionAttempts++; OnConnectionException(ex); } } } /// /// Receive (read) data from the (.NET serial port class raises this event when data is available). /// private void SerialPort_DataReceived(object sender, SerialDataReceivedEventArgs e) { try { int bytesRead = 0; // Retrieve data from the port. while (bytesRead < m_serialClient.Provider.BytesToRead) bytesRead += m_serialClient.Provider.Read(m_serialClient.ReceiveBuffer, bytesRead, m_serialClient.ReceiveBuffer.Length - bytesRead); m_serialClient.BytesReceived = bytesRead; m_serialClient.Statistics.UpdateBytesReceived(bytesRead); // Notify of the retrieved data. OnReceiveDataComplete(m_serialClient.ReceiveBuffer, bytesRead); } catch (Exception ex) { // Don't raised exceptions for a closed port if (!m_disposed && m_serialClient.Provider != null && m_serialClient.Provider.IsOpen) OnReceiveDataException(ex); } } /// /// Receive (read) error data from the (.NET serial port class raises this event when error occurs). /// private void SerialPort_ErrorReceived(object sender, SerialErrorReceivedEventArgs e) => OnReceiveDataException(new SerialException(e.EventType)); /// /// Raises the event. /// /// Exception to send to event. protected override void OnConnectionException(Exception ex) { base.Disconnect(); base.OnConnectionException(ex); } #endregion } }