Dave Dave - 3 months ago 54
C# Question

C# serial port problem - too simple to fail, but

Ok, this should be dirt simple. I'm trying to read charactes from a serial device. It's such that if I send a space character, it echos back a string of numbers and EOL. That's it.

I'm using Unity 3.3 (.Net 2.0 support), and the 'serial port' is a Prolific serial-to-USB adaptor. BTW: Using Hyperterminal, it all works perfectly, so I know it's not driver nor hardware.

I can open the port ok. It seems I can send my space with port.Write(" "); But if I even TRY to call ReadChar, ReadByte, or ReadLine (like polling), it freezes up until I unplug the USB, and my console output shows nothing (exceptions were caught).

So instead I set up a DataReceviedHandler, but it's never called.

I've read some posts where people have done just this type of thing with Arduinos etc. (this is not an Arduino but hey), using nothing more than ReadLine. Their code does not work for me (and no answers thus far from those authors).

So, any tips? Do I need to use a different thread? If you know any Unity (Mono) coding, any tips along those lines greatly appreciated.

This code a mashup from http://plikker.com/?p=163 and http://msdn.microsoft.com/en-us/library/system.io.ports.serialport.datareceived.aspx#Y537

using UnityEngine;
using System.Collections;
using System.IO.Ports;
using System;

public class SerialTest : MonoBehaviour {

SerialPort stream;

void Start () {
try {
stream = new SerialPort("COM3", 9600);
stream.Parity = Parity.None;
stream.StopBits = StopBits.One;
stream.DataBits = 8;
stream.Handshake = Handshake.None;
stream.DataReceived += new SerialDataReceivedEventHandler(DataReceviedHandler);


stream.Open();
Debug.Log("opened ok"); // it DOES open ok!
} catch (Exception e){
Debug.Log("Error opening port "+e.ToString()); // I never see this message
}
}

void Update () { // called about 60 times/second
try {
// Read serialinput from COM3
// if this next line is here, it will hang, I don't even see the startup message
Debug.Log(stream.ReadLine());
// Note: I've also tried ReadByte and ReadChar and the same problem, it hangs
} catch (Exception e){
Debug.Log("Error reading input "+e.ToString());
}
}

private static void DataReceviedHandler(
object sender,
SerialDataReceivedEventArgs e)
{
SerialPort sp = (SerialPort)sender; // It never gets here!
string indata = sp.ReadExisting();
Debug.Log("Data Received:");
Debug.Log(indata);
}

void OnGUI() // simple GUI
{
// Create a button that, when pressed, sends the 'ping'
if (GUI.Button (new Rect(10,10,100,20), "Send"))
stream.Write(" ");
}
}

Answer

Make sure that you are opening the right port, using correct settings. Here is an example of how you could configure it:

serial = new SerialPort();

serial.ReadBufferSize = 8192;
serial.WriteBufferSize = 128;

serial.PortName = "COM1";
serial.BaudRate = 115200;
serial.Parity = Parity.None;
serial.StopBits = StopBits.One;

// attach handlers
// (appears to be broken in some Mono versions?)
serial.DataReceived += SerialPort_DataReceived;
serial.Disposed += SerialPort_Disposed;

serial.Open();

I recommend the open source RealTerm terminal, it has a rich set of features and can help you debug. Try writing a byte manually using such software, and if it works, then the problem is in your program. Otherwise it might be a driver problem (but more likely it isn't).

[Edit]

Calling SerialPort.ReadLine is actually supposed to block the thread until SerialPort.NewLine is received. Also ReadChar and ReadByte will hang until at least one byte is received. You need to make sure that you are actually receiving characters from the other side, and you won't be receiving them if your app is stuck and cannot send the space.

Since I never used Unity, I am not sure how Update is called, but I am presuming it's fired on a foreground thread in regular intervals (otherwise your app wouldn't freeze).

The example that you linked (Arduino and Unity example) shows that Arduino is sending the data continuously, and that is why their Update method is constantly receiving data (no space character needs to be sent towards the device). If they unplug the device, their app will hang just as well.

Well, maybe not, because in .NET 1.1, default value for ReadTimeout was not infinite, like it is in .NET 2.0.

So, what you can do is:

a. Set the ReadTimeout property to a reasonable value. Default in .NET 2.0 is InfiniteTimeout, which doesn't suit your needs. Cons: your update method will still hang for a while on each call, but not infinitely.

b. Someone said that events are not implemented in MONO SerialPort, so I guess using DataReceived only is not an option.

c. Move your sending logic to the Update method also, so that you don't read data at all, until it's time to read it:

private volatile bool _shouldCommunicate = false;
void Update ()
{
  if (_shouldCommunicate) // this is a flag you set in "OnGui"
  {
     try {
       stream.Write(" ");
       Debug.Log(stream.ReadLine());
     } catch (Exception e){
       Debug.Log("Error reading input "+e.ToString());
     }
  }
}

void OnGUI() // simple GUI
{
   if (GUI.Button (new Rect(10,10,100,20), "Send"))
      _shouldCommunicate = true;
}

Note that, if your device is not sending data, it will also block at stream.ReadLine(), so make sure your ReadTimeout is set to a reasonable value. You will also want to stop sending at some point, but I leave that to you.

d. Send the space in OnGui like you are doing now, but always check if there is data in your buffer before reading it:

void Update () { // called about 60 times/second
 try {
   // call our new method
   Debug.Log(ReadLineNonBlocking());
 } catch (Exception e){
  Debug.Log("Error reading input "+e.ToString());
 }
}

private StringBuilder sb = new StringBuilder();
string ReadLineNonBlocking()
{
    int len = stream.BytesToRead;
    if (len == 0)
        return "";

    // read the buffer
    byte[] buffer = new byte[len];
    stream.Read(buffer, 0, len);
    sb.Append(ASCIIEncoding.ASCII.GetString(buffer));

    // got EOL?
    if (sb.Length < 2 ||
        sb[sb.Length-2] != '\r' ||
        sb[sb.Length-1] != '\n')
        return ""; 

    // if we are here, we got both EOL chars
    string entireLine = sb.ToString();
    sb.Length = 0;
    return entireLine;
 }

Disclaimer: this is directly out of my head, untested, so there may be some syntax errors which I am sure you will handle.