using System; using System.Text; using System.Threading; using System.Runtime.CompilerServices; using System.IO.Ports; using Microsoft.SPOT; using Microsoft.SPOT.Hardware; namespace DEV { // Provides a connection to a serial communications port that supports line delimited reading and interruptable writing, // including timeouts and DataReceived event implementation. public class SerialChannel : IDisposable // SerialPort class is marked as sealed, so you can't use this class in components where SerialPort is accepted. { // We are implementing IDisposable because we have private SerialPort, eventually InterruptPort to dispose. // Encoding to use when reading or writing string data. Default is UTF-8. Only expanding encodings are supported. public Encoding Encoding = Encoding.UTF8; // A string representing the line delimiter in string data. public string NewLine = "\r\n"; // Number of milliseconds to pause the writing thread after sending the write buffer. public int WriteTimeout = 33; // Number of milliseconds to wait for data in read methods. Default is "Timeout.Infinite". // Pass zero to make the read methods return immediately when no data are buffered. public int ReadTimeout = Timeout.Infinite; // The value of input pin when pauding the data output is requested. Default is true, that is, the input is of active high type. public bool BusyValue = true; private byte[] _incomingBuffer; // Circular buffer for incoming data when DataReceived event is being requested. private int _incomingBufferPosition; // Start position in the _incomingBuffer where valid data begins. private int _incomingBufferValidLength; // Number of valid bytes in the _incomingBuffer starting at _incomingBufferPosition. protected SerialPort _port; // The actual serial port we are wrapping. private Thread _writeThread; // Thread which is sending data out. This is usually a calling thread. private int _writeBufferSize; // Size of the output buffer, which can be used to insert pauses between some amount of data. private Thread _readThread; // Thread which is reading the data when DataReceived event is being requested. We create and dispose this thread inside the class. private int _readBufferSize; // Size of the input buffer. The DataReceived event won't fire until this amount of bytes comes in. private AutoResetEvent _readToEvent; // Handles the thread synchronization when both DataReceived event is being requested and user calls ReadTo(). // Creates a new instance of SerialInterruptPort class, allowing to specify buffer sizes and blocking input port. public SerialChannel(SerialPort.Configuration config, int writeBufferSize, int readBufferSize) { // some initial parameter checks. if (writeBufferSize < 0) throw new ArgumentOutOfRangeException("writeBufferSize"); if (readBufferSize < 1) throw new ArgumentOutOfRangeException("readBuferSize"); _bufferSync = new object(); // initializing the sync root object _incomingBuffer = new byte[readBufferSize]; // allocating memory for incoming data _port = new SerialPort(config); // creating the serial port _writeBufferSize = writeBufferSize; _readBufferSize = readBufferSize; } /// Releases resources used by a serial port. public void Dispose() { if (_busy != null) _busy.Dispose(); // release the hardware flow control pin, if used if (_port != null) _port.Dispose(); // release the serial port if applicable } #region Writing // This is event handler for changes on the hardware flow pin. private void OnBusyChanged(Cpu.Pin port, bool state, TimeSpan time) { // currently not writing if (_writeThread == null) return; if (state == BusyValue) _writeThread.Suspend(); // if _busy was set, pause sending the data else _writeThread.Resume(); // if it was cleared, resume sending the data } /// /// Writes data to a serial port. /// /// The data to write to the serial port. /// The method does not return until all data are sent, including output buffer pauses and/or hardware flow control pauses, if applicable. public void Write(params byte[] data) { Write(data, 0, data.Length); } /// /// Writes data to a serial port. /// /// The input buffer that is to write to the serial port. /// The offset value that indicates where writing from the input buffer to the serial port is to begin. /// The number of bytes of data to be written to the serial port. /// The method does not return until all data is sent, including output buffer pauses and/or hardware flow control pauses, if applicable. public virtual void Write(byte[] data, int offset, int length) { _writeThread = Thread.CurrentThread; // grab the current thread so that we can pause the writing if (_busy != null && _busy.Read()) _writeThread.Suspend(); // do not continue if _busy is already set (eg. the signal was changed when we weren't writing) if (_writeBufferSize < 1) // If user does not want to split data into chunks, { _port.Write(data, 0, data.Length); // pass it to the SerialPort output without change. return; } int modulus = length % _writeBufferSize; // prepare data which fill the _writeBufferSize completely length -= modulus; // (If there is not enough data to fill it, length would be zero after this line, for (int i = offset; i < offset + length; i += _writeBufferSize) // and this cycle would not execute.) { _port.Write(data, i, _writeBufferSize); // send it out Thread.Sleep(WriteTimeout); // and include pause after chunk } if (modulus > 0) // If any data left which do not fill whole _writeBuferSize chunk, { _port.Write(data, offset + length, modulus); // send it out as well Thread.Sleep(WriteTimeout); // and pause for case consecutive calls to any write method. } _writeThread = null; // release current thread so that the _busy signal does not affect external code execution } /// /// Encodes string data using and sends them to a serial port. /// /// String data to send. /// The method does not return until all data are sent, including output buffer pauses and/or hardware flow control pauses, if applicable. public void Write(string text) { Write(Encoding.GetBytes(text)); } /// /// Appends to the string, encodes it using and sends it to a serial port. /// /// String data to send. /// The method does not return until all data are sent, including output buffer pauses and/or hardware flow control pauses, if applicable. public void WriteLine(string text) { Write(text + NewLine); } #endregion #region Reading public int AvailableBytes { get { return _incomingBufferValidLength; } } // Reads all available bytes and removes them from the reading buffer. public byte[] ReadAvailable() { return ReadAvailable(int.MaxValue); } // Reads data from a serial port. public virtual int Read(byte[] buffer, int offset, int count) { if (IsReading) // If DataReceived event is being requested, lock (_bufferSync) // we have to use our reading buffer: { int usedLength = GetBufferedData(buffer, offset, count); // read data from it AdvancePosition(usedLength); // and remove them. return usedLength; // TODO: Implement read timeout. } else return _port.Read(buffer, offset, count, ReadTimeout); // Otherwise, we can directly read the serial port data. } // Reads data from a serial port up to the value and decodes them as string using "Encoding". public string ReadLine() { byte[] stringData = ReadTo(Encoding.GetBytes(NewLine)); if (stringData == null) return null; // ReadTo timed out if (stringData.Length < 1) return string.Empty; // two consecutive line markers return new string(Encoding.GetChars(stringData)); // fails when data contains invalid bytes for the current Encoding // TODO: Fire an error event with raw data. } // Reads data from a serial port up the specified byte sequence and removes them from the reading buffer. // Do not allow multiple threads to call this method simultaneously, as we have only one _readToEvent [MethodImpl(MethodImplOptions.Synchronized)] protected virtual byte[] ReadTo(params byte[] mark) { // By using byte[] as a marker instead of char[] or string gives us the advantage of processing "lines" in binary data, however, // it definitely limits us to the expanding encondings only. That means, this will not work in the case encoding packs more characters into single byte. // We could solve this eg. by params char[] mark override equivalent, but current limitations of decoders (eg. Utf8Decoder class) makes such parsing too complicated nowadays. if (mark == null) throw new ArgumentNullException("mark"); if (mark.Length == 0) throw new ArgumentException("Mark must have non-zero length."); if (IsReading) // DataReceived event is being requested so we use internal read buffer rather than the serial port directly { _readToEvent = new AutoResetEvent(false); // creates an AutoResetEvent so that the OnDataReceived event handler can signal us that new data are available to check Timer readToTimeout = null; bool timedOut = false; if (ReadTimeout > 0) readToTimeout = new Timer(delegate { timedOut = true; _readToEvent.Set(); }, null, ReadTimeout, 0); while (IsReading) { lock (_bufferSync) { int markIndex = BufferIndexOf(mark); // look for mark in the received data if (markIndex >= 0) // If found, { byte[] receivedData = GetBufferedData(markIndex); // read the data up to the mark, AdvancePosition(markIndex + mark.Length); // and remove them from the buffer. if (readToTimeout != null) readToTimeout.Dispose(); // We are finished, so cancel the timeout timer, if applicable. return receivedData; } } if (ReadTimeout == 0) // If the user does not want to wait and we don't have a line, return null. return null; _readToEvent.WaitOne(); // Wait until the OnDataReceived handler signals us there are new data available to check, // or until the timeout timer signals us. if (timedOut) return null; // If it was the timer, return null. } // Here we are if the DataReceived event was being requested upon calling this method, but all subscribers has detached before any line marker came in. _readToEvent = null; // do some cleaning of stuff we don't need for the direct serial port manipulation if (timedOut) return null; else if (readToTimeout != null) readToTimeout.Dispose(); } byte[] data = new byte[System.Math.Max(_incomingBufferValidLength, _readBufferSize) + mark.Length]; int offset = GetBufferedData(data, 0, _incomingBufferValidLength); // read any data which left in the internal read buffer int markSearchStart = 0; while (true) { if (offset >= data.Length) // If we have filled the buffer, make a bigger one! { // (the >= is for paranoia reasons, the offset never becomes greater than data.Length in this method) byte[] biggerData = new byte[data.Length * 2]; data.CopyTo(biggerData, 0); data = biggerData; } int read = _port.Read(data, offset, data.Length - offset, ReadTimeout); // read as much data from serial port as fits in our buffer if (read < 1) return null; // the operation has timed out offset += read; // offset now points to where next read should start, or in other words valid length int markPos = Array.IndexOf(data, mark[0], markSearchStart, offset - markSearchStart); // try to find the first byte of mark in the buffer if (markPos < 0) markSearchStart = offset; // we didn't find it, there is no reason to search the whole buffer again next time else { // okay, we have the first byte if (markPos + mark.Length <= offset) // do we have enough data in the buffer that whole mark could fit in? { int i = 1; for (i = 1; i < mark.Length; i++) // if so, check if the next bytes in buffer match the mark bytes if (data[markPos + i] != mark[i]) break; if (i >= mark.Length) { byte[] finalData = new byte[markPos]; // if they do, copy data before marker into the new array Array.Copy(data, 0, finalData, 0, markPos); int remains = offset - markPos - mark.Length; if (remains > 0) // If we grabbed any data we haven't used, lock (_bufferSync) // push it to the internal read buffer (we are going to return now). { if (_incomingBuffer.Length < remains) // make enough space if the internal buffer is too small to store remaining data _incomingBuffer = new byte[remains]; // We have already read all data that where in the buffer before the while loop, // so it is okay to lose any current data. Array.Copy(data, offset - remains, _incomingBuffer, 0, remains); _incomingBufferPosition = 0; // And so we are storing at the beginning of the circular buffer. _incomingBufferValidLength = remains; } return finalData; } else // If the other bytes do not match the mark bytes, it is not part of the mark, markSearchStart = markPos + 1; // and start the next search at the next position. } else markSearchStart = markPos; // We don't know if this is marker or not, so try again this position with more data. } } } // Searches the internal circular read buffer for sequence of bytes. protected virtual int BufferIndexOf(byte[] what) { // The same limitations about searching for bytes applies. See ReadTo comments for details. int whatLength = what.Length; int bufferLength = _incomingBuffer.Length; // buffer should not be modified during this method (ie. call in lock(_bufferSync) only) if (whatLength > _incomingBufferValidLength) return -1; // if the desired sequence would not fit into the buffer at all, do not bother searching it for (int i = _incomingBufferPosition; i < _incomingBufferPosition + _incomingBufferValidLength; i++) if (_incomingBuffer[i % bufferLength] == what[0]) { // we have a first byte match int j; for (j = 1; j < whatLength; j++) if (_incomingBuffer[(i + j) % bufferLength] != what[j]) break; // check the remaining bytes if (j >= whatLength) // If the remaining bytes match, return (i - _incomingBufferPosition) % bufferLength; // decode the circular position and return it; } // else try the next byte. return -1; } #endregion // Empties the contents of a serial port's buffer. public void Flush() { _port.Flush(); } // Gets or sets the configuration information for a serial port. public SerialPort.Configuration Config { get { return _port.Config; } set { _port.Config = value; } } #region DataReceived event stuff private object _bufferSync; // Sync root object for manipulation with the _incomingBuffer and/or its position/valid length fields. private bool _continueReading; // A soft way to end the reading thread. // The main loop of reading thread. This uses the blocking SerialPort.Read() method to monitor the serial port and fires the DataReceived event. private void ReadLoop() { byte[] buffer = new byte[_readBufferSize]; int read; while (_continueReading) { try { read = _port.Read(buffer, 0, _readBufferSize, Timeout.Infinite); } // wait for some data (set _readBufferSize to 1 to wait for any data) catch (ThreadAbortException) { return; } // (if we were aborted, pass away silently) OnDataReceived(buffer, read); // and process it } } // Adds the received data into internal circular read buffer and fires DataReceived event. protected virtual void OnDataReceived(byte[] data, int validLength) { lock (_bufferSync) { if (_incomingBufferValidLength + validLength > _incomingBuffer.Length) // If the received data would not fit in the internal buffer, { _incomingBuffer = GetBufferedData(_incomingBuffer.Length * 2); // make it bigger and align the current data at the buffer beginning. _incomingBufferPosition = 0; } int start1 = (_incomingBufferPosition + _incomingBufferValidLength) % _incomingBuffer.Length; // where the first phase of copy should start (start2 = 0) int end1 = start1 + validLength; // virtual copy end int end2 = 0; if (end1 > _incomingBuffer.Length) // if the end is actually wrapped in the circular buffer { end2 = end1 % _incomingBuffer.Length; // move the overlapping part into the second phase of copy end1 = _incomingBuffer.Length; } Array.Copy(data, 0, _incomingBuffer, start1, end1 - start1); // first phase of copy (to the middle of the buffer up to its end) Array.Copy(data, end1 - start1, _incomingBuffer, 0, end2); // second one (to the beginning of the buffer up to the wrapped end) _incomingBufferValidLength += validLength; } if (_readToEvent != null) _readToEvent.Set(); } // Returns specified amount of data from internal circular read buffer. If there is not enough data available, remaining bytes are filled with zeros. protected byte[] GetBufferedData(int arraySize) { byte[] data = new byte[arraySize]; // This method can be (and is) used to increase the internal buffer size, GetBufferedData(data, 0, arraySize); // with the side effect of aligning the circular wrapped data linearly from the beginning. return data; } // Copies specified amount of data from internal circular read buffer to the specified linear buffer. protected virtual int GetBufferedData(byte[] buffer, int offset, int count) { // 0 1 2 3 4 5 6 7 8 9 10 11 12 (13) (14) // example of _internalBufer: [ E F x x x x x x x A B C D ] ( E) ( F) // _internalBufferStartPosition = 9 // _internalBufferValidLength = 6 // copy 1st phase: start1 = 9, end1 = 13 // copy 2nd phase: (start2 = 0) len2 = 2 (virtual copy end = 15) count = System.Math.Min(_incomingBufferValidLength, count); if (count < 1) return 0; int end1 = _incomingBufferPosition + count; // virtual copy end int len2 = 0; if (end1 > _incomingBuffer.Length) // if the end is actually wrapped in the circular buffer { len2 = end1 % _incomingBuffer.Length; // move the overlapping part into the second phase of copy end1 = _incomingBuffer.Length; } Array.Copy(_incomingBuffer, _incomingBufferPosition, buffer, 0, end1 - _incomingBufferPosition); // first phase of copy (from middle of the buffer to the end) Array.Copy(_incomingBuffer, 0, buffer, end1 - _incomingBufferPosition, len2); // second one (from beginning of the buffer to the wrapped end) return count; } // Advances the internal circular read bufer position by specified amount and updates the valid length field accordingly. protected virtual void AdvancePosition(int count) { _incomingBufferPosition = (_incomingBufferPosition + count) % _incomingBuffer.Length; // increase the pointer and wrap it if needed _incomingBufferValidLength -= count; // keep the virtual end at the same position } // Stops waiting for data. protected virtual void StopReading() { _continueReading = false; // If the ReadLoop is not in the _port.Read() method, the soft way will work. if (_writeThread != null && _readThread.ThreadState == ThreadState.WaitSleepJoin) // otherwise, _readThread.Abort(); // take a hammer (we need to end the thread, otherwise it would steal the next data coming) if (_readToEvent != null) // if ReadTo() method is being called, _readToEvent.Set(); // let it know we have some new data to check } // Starts waiting for data in order to fire DataReceived event. protected virtual void StartReading() { _continueReading = true; if (!IsReading) { _readThread = new Thread(ReadLoop); _readThread.Start(); } } // Gets whether the waiting for data is active. protected virtual bool IsReading { get { return _readThread != null && (_readThread.ThreadState == ThreadState.WaitSleepJoin || _readThread.ThreadState == ThreadState.Running); } } #endregion } }