/* ANY-LICENSE V1.0 ---------------- You can use these files under any license approved by the Open Source Initiative, preferrably one of the popular licenses, as long as the license you choose is compatible to the dependencies of these files. See http://www.opensource.org/licenses/ for a list of approved licenses. Author: Martin Furter Project: borgnet, borg.ch C# classes and examples. Modified: 2019 A serial port class which works reliable on Windows and Linux. The knowledge how to do it right can be found here: http://www.sparxeng.com/blog/software/must-use-net-system-io-ports-serialport http://www.sparxeng.com/wp-content/uploads/2014/05/If-you-must-use-net-systems-2.jpg */ using System; using System.Diagnostics.CodeAnalysis; using System.IO; using System.IO.Ports; using BorgCh; using BorgCh.Comm; namespace BorgCh.Comm { /** * A serial port class which works reliable on Windows and Linux. */ public class SerPort { /** * The callback to receive data from the serial port. */ public delegate void ReadData( byte[] data ); /// The standard serial port. private SerialPort serial; /// When true the port is open. private bool running; /// When true the port is closed. private bool stopped; /// The buffer for incoming data. private byte[] rd_buf; /// private Action kickoff_read; /// The callback to deliver received data. private ReadData serialDataReceived; protected Logger logger; /** * Creates a new SerPort and opens it. * Portspec format: portname-baudrate-databits-parity-stopbits * * @param port_spec Port specification string as returned by ToString(). * @param cb The read callback. */ public SerPort( string port_spec, ReadData? cb=null, Logger? _logger=null ) { string port_name; int baud_rate; Parity parity; int data_bits; StopBits stop_bits; if( _logger != null ) { logger = _logger; } else { logger = Logger.get_logger( "BorgCh.SerPort" ); } string[] parts = port_spec.Split( new char[]{ '-' } ); if( parts.Length != 5 ) { throw new ArgumentException( "The port description must have 5 fields separated by '-'.", "port_spec" ); } // part 0: port name port_name = parts[0]; // part 1: baud rate if( !Int32.TryParse( parts[1], out baud_rate ) ) { throw new ArgumentException( "The baud rate must be an integer.", "baud_rate" ); } // part 2: number of data bits if( !Int32.TryParse( parts[2], out data_bits ) ) { throw new ArgumentException( "The number of data bits must be an integer." ); } if( data_bits < 5 || data_bits > 8 ) { throw new ArgumentOutOfRangeException( "data_bits", "The number of data bits must be between 5 and 8." ); } // part 3: parity if( parts[3] == "N" ) { parity = Parity.None; } else if( parts[3] == "E" ) { parity = Parity.Even; } else if( parts[3] == "O" ) { parity = Parity.Odd; } else if( parts[3] == "M" ) { parity = Parity.Mark; } else if( parts[3] == "S" ) { parity = Parity.Space; } else { throw new ArgumentException( "The parity must be one of letters 'NEOMS'.", "parity" ); } // part 4: number of stop bits if( parts[4] == "1" ) { stop_bits = StopBits.One; } else if( parts[4] == "1.5" ) { stop_bits = StopBits.OnePointFive; } else if( parts[4] == "2" ) { stop_bits = StopBits.Two; } else { throw new ArgumentException( "The number of stop bits must be '1', '1.5' or '2'.", "stop_bits" ); } initialize( port_name, baud_rate, parity, data_bits, stop_bits, cb ); } /** * Creates a new SerPort and opens it. * * @param port_name Name of the serial port. * @param baud_rate The baud rate. * @param parity The parity. * @param data_bits The number of data bits per word. * @param stop_bits The number of stop bits. * @param cb The read callback. */ public SerPort( string port_name, int baud_rate, Parity parity, int data_bits, StopBits stop_bits, ReadData? cb=null, Logger? _logger=null ) { if( _logger != null ) { logger = _logger; } else { logger = Logger.get_logger( "BorgCh.SerPort" ); } initialize( port_name, baud_rate, parity, data_bits, stop_bits, cb ); } /** * Creates a new SerPort and opens it. * * @param port_name Name of the serial port. * @param baud_rate The baud rate. * @param parity The parity. * @param data_bits The number of data bits per word. * @param stop_bits The number of stop bits. * @param cb The read callback. */ [MemberNotNull( nameof(serial), nameof(rd_buf), nameof(kickoff_read), nameof(serialDataReceived) )] private void initialize( string port_name, int baud_rate, Parity parity, int data_bits, StopBits stop_bits, ReadData? cb ) { serial = new SerialPort( port_name, baud_rate, parity, data_bits, stop_bits ); serial.Handshake = Handshake.None; serial.RtsEnable = false; serial.ReadTimeout = 100; if( serial.IsOpen ) { Console.WriteLine( "Serial port " + port_name + " seems to be already open!" ); } running = false; stopped = true; rd_buf = new byte[256]; set_read_callback( cb ); kickoff_read = delegate { serial.BaseStream.BeginRead( rd_buf, 0, rd_buf.Length, delegate( IAsyncResult ar ) { try { int actualLength = serial.BaseStream.EndRead( ar ); byte[] received = new byte[actualLength]; Buffer.BlockCopy( rd_buf, 0, received, 0, actualLength ); serialDataReceived( received ); } catch( IOException exc ) { Console.WriteLine( "IOException:" ); Console.WriteLine( exc.Message ); } // catch( Exception exc ) catch( Exception ) { /* * / Console.WriteLine( "Exception: {0}", exc.Message ); / * */ // just assume it is a timeout if( !running ) { stopped = true; } } if( running ) { // Console.WriteLine( "kickoff_read again..." ); if( kickoff_read != null ) { kickoff_read(); } } }, null ); }; } /** * A callback which just ignores the data, used as default callback. */ private void ignore_callback( byte[] data ) { } /** * Sets the read callback. */ [MemberNotNull( nameof(serialDataReceived) )] public void set_read_callback( ReadData? cb ) { if( cb == null ) { cb = ignore_callback; } serialDataReceived = cb; } /** * Opens the serial port. */ public void open() { if( running ) { logger.log( LogLevel.DEBUG2, "SerPort.open(): already open." ); return; } logger.log( LogLevel.DEBUG2, "SerPort.open(): called." ); try { serial.Open(); } catch( Exception ex ) { Console.WriteLine( serial.PortName ); logger.log( LogLevel.DEBUG2, "SerPort.open(): failed." ); logger.log( LogLevel.DEBUG2, ex ); throw; } running = true; stopped = false; kickoff_read(); } /** * Closes the serial port. */ public void close() { int counter = 25; if( !running ) { logger.log( LogLevel.DEBUG2, "SerPort.close(): already closed." ); return; } logger.log( LogLevel.DEBUG2, "SerPort.close(): called." ); /* running = false; // Console.WriteLine( "running = false" ); while( counter > 0 && !stopped ) { counter--; System.Threading.Thread.Sleep( 100 ); } // Console.WriteLine( "close" ); serial.Close(); */ // not running anymore running = false; // close the serial port, this will also end the BeginRead() // (actually it should terminate the async read, but it does not on linux) serial.Close(); // wait util it's finished System.Threading.Thread.Sleep( 10 ); while( counter > 0 && !stopped ) { // Console.WriteLine( "{0}...", counter ); counter--; System.Threading.Thread.Sleep( 100 ); } logger.log( LogLevel.DEBUG2, "SerPort.close(): done, counter={0}.", counter ); } /** * Returns true when the port is open. */ public bool is_open() { return running; } /** * Writes the string @c s to the serial port. */ public void write( string s ) { write( System.Text.Encoding.ASCII.GetBytes( s ) ); } /** * Writes the bytes in @c wr_buf to the serial port. */ public void write( byte[] wr_buf ) { // on Linux WriteAsync does not write until something is received // serial.BaseStream.WriteAsync( wr_buf, 0, wr_buf.Length ); serial.BaseStream.Write( wr_buf, 0, wr_buf.Length ); } /** * Writes @c length bytes from @c offset to the serial port. */ public void write( byte[] wr_buf, int offset, int length ) { // on Linux WriteAsync does not write until something is received // serial.BaseStream.WriteAsync( wr_buf, 0, wr_buf.Length ); serial.BaseStream.Write( wr_buf, offset, length ); } /** * Returns the duration of sending/receiving @n data words. * * @n: The number of data words to calculate the duration for. * @return: The duration in seconds. */ public double get_word_duration( int n=1 ) { // start bit + stop bit + number of data bits int bits_per_word = 2 + serial.DataBits; // if there is a parity add one bit if( serial.Parity != Parity.None ) { bits_per_word++; } // if there are 1.5 or 2 stop bits add one bit if( serial.StopBits != StopBits.One ) { bits_per_word++; } double duration = bits_per_word * n; duration /= serial.BaudRate; return duration; } /** * The serial port name. */ public String PortName { get { return serial.PortName; } } /** * The baud rate. */ public int BaudRate { get { return serial.BaudRate; } set { serial.BaudRate = value; } } /** * The number of data bits. */ public int DataBits { get { return serial.DataBits; } set { serial.DataBits = value; } } /** * The parity as defined in System.IO.Ports.Parity */ public Parity Parity { get { return serial.Parity; } set { serial.Parity = value; } } /** * The number of stop bits as defined in System.IO.Ports.StopBits */ public StopBits StopBits { get { return serial.StopBits; } set { serial.StopBits = value; } } /** * Returns a string describing the configuration of the serial port. * * The returned string consists of 5 substrings separated by '-'. * The five parts are port name, baud rate, number of data bits, * parity and number of stop bits in this order. * * The parity take on the values 'E', 'O', 'M', 'S' or 'N'. * * The number of stop bits can be '1', '1.5' or '2'. */ public override String ToString() { String parity; String stopbits; switch( serial.Parity ) { case Parity.Even: parity = "E"; break; case Parity.Odd: parity = "O"; break; case Parity.Mark: parity = "M"; break; case Parity.Space: parity = "S"; break; default: parity = "N"; break; } switch( serial.StopBits ) { case StopBits.One: stopbits = "1"; break; case StopBits.OnePointFive: stopbits = "1.5"; break; case StopBits.Two: stopbits = "2"; break; default: stopbits = "1"; break; } return String.Format( "{0}-{1}-{2}-{3}-{4}", serial.PortName, serial.BaudRate, serial.DataBits, parity, stopbits ); } } } // namespace BorgCh.Comm