4
votes

I've developed a small program that listens to a serial port. My program is receiving data. The problem is, its not displaying it in the desired format (one string). The data my program is receiving comes in two strings, for example:

ID:34242 State:NY

Zip:12345 StreetType:Ave

Its being displayed by chunks and some of the data is passed on to next lines as such:

 ID:34242
State:N
Y Zip:12
345 Street
Type:Ave

I have used the SerialDataReceive Event Handler to receive my data and it looks like this:

 private static void Port_DataReceived(object sender, SerialDataReceivedEventArgs e)
    {

        SerialPort spL = (SerialPort) sender;
        int bufSize = 20;
        Byte[] dataBuffer = new Byte[bufSize];
        Console.WriteLine("Data Received at"+DateTime.Now);
        Console.WriteLine(spL.Read(dataBuffer, 0, bufSize));
        string s = System.Text.ASCIIEncoding.ASCII.GetString(dataBuffer);
        Console.WriteLine(s);



    }

As you can see, I retrieve the bytes into the buffer, create a byte array to hold the data and use ASCII encoding to translate the bytes into a string. I tried using ReadLine() but my data doesn't event get displayed with that function. Does anyone know of any other way to parse and format the data into one string?

3
Is there any separator character at the end of each record? Print out the numeric byte values, and look especially after the last record sent. The data comes as a continuous stream, but hopefully the protocol was designed with some sort of termination character (a carriage return would be typical, but probably isn't the case since ReadLine didn't work for you).Justin

3 Answers

4
votes

The problem is, as you may have guessed, that the event DataReceived is raised as soon as data has been received over the serial port. There may not be a complete record there; the SerialPort object has no clue what you consider to be "enough" data to be significant, or workable.

The usual solution is to maintain another "buffer" of received data, containing any data you have recognized as incomplete. When data comes in over the port and your event fires, it should first take what's in the buffer and append it to what you have already received. Then, you should start at the beginning of this data buffer and inspect the received data, looking for known patterns of atomic "chunks" of data that have meaning to you; for instance, say the first thing you receive is "ID: 12". You take this, put it in the buffer, then scan the buffer looking for a pattern defined by a regex "ID: \d*? ". Because the trailing space is not present in your buffer, your pattern fails to find anything of meaning, and so you now know you haven't received a full message.

Then, on the next raising of the DataReceived event, you pull "453 Sta" out of the serial buffer. You append it to what you already have and get "ID:12453 Sta", and when you apply the regex, you get the match "ID: 12345 ". You pass this into a method for further processing (display to the console, maybe), and remove the same string from the front of the buffer, leaving "Sta". Scanning again you don't find anything else of interest, so you leave what you have, and the cycle repeats aws data continues to come in. Obviously, you'll be testing more patterns than just the ID pattern; you may search for an entire "string" you expect to receive, such as "ID: \d*? State: \w{2} ". You may even keep the data in your buffer until you have both strings for a record: "ID:\d*? State:\w{2} Zip:\d{5} StreetType:\w*? ".

Either way, you will need to identify whether the data being received is either reliably "fixed-length" (meaning each string of a particular type will always have the same number of bytes or characters), or reliably "delimited" (meaning there will be some character or character combination that always separates significant elements of data). If neither of these apply, it may be very difficult to parse the data into single-field chunks.

Here's a sample based on what you have already:

private static StringBuilder receiveBuffer = new StringBuilder();

private static void Port_DataReceived(object sender, SerialDataReceivedEventArgs e)
{

    SerialPort spL = (SerialPort) sender;
    int bufSize = 20;
    Byte[] dataBuffer = new Byte[bufSize];
    Console.WriteLine("Data Received at"+DateTime.Now);
    Console.WriteLine(spL.Read(dataBuffer, 0, bufSize));
    string s = System.Text.ASCIIEncoding.ASCII.GetString(dataBuffer);
    //here's the difference; append what you have to the buffer, then check it front-to-back
    //for known patterns indicating fields
    receiveBuffer.Append(s);

    var regex = new Regex(@"(ID:\d*? State:\w{2} Zip:\d{5} StreetType:\w*? )");
    Match match;
    do{
       match = regex.Match(receiveBuffer.ToString());
       if(match.Success)
       {
          //"Process" the significant chunk of data
          Console.WriteLine(match.Captures[0].Value);
          //remove what we've processed from the StringBuilder.
          receiveBuffer.Remove(match.Captures[0].Index, match.Captures[0].Length);
       }
    } while (match.Success);
}
2
votes

See tip #1

http://blogs.msdn.com/b/bclteam/archive/2006/10/10/top-5-serialport-tips-_5b00_kim-hamilton_5d00_.aspx

When using SerialPort.Read(buffer, offset, count), where count is the number of bytes you want to read, check the return value, which tells you the number of bytes actually read. Developers sometimes assume that count bytes/chars will be returned when Read finishes.Here’s what Read actually does. If there are bytes available on the serial port, Read returns up to count bytes but will not block for the remaining bytes. If there are no bytes available on the serial port, Read will block until at least one byte is available on the port, up until the ReadTimeout milliseconds have elapsed, at which time a TimeoutException will be thrown. To fix this in your code, check number of bytes actually read and use that value when processing the returned data.

Basically, you are not guaranteed to get count bytes. You will get what's available to be read, up to count bytes - not more than count, but possibly less.

0
votes

Assuming there is no termination character, something like this may work. The tricky part is figuring out when to print a new line.

You may try inserting a newline before every ID: (e.g., replace "ID:" with "\r\n\ID:"). This will still sometimes fail when you receive StreetType:AveI first and then "D:23566 St" next. To fix this, you could just look for any I after StreetType:, but that's not as easy as it sounds either -- what if you see 345 Stre, etTy, pe:RdI. Also, what if I is a valid character (tType:DRI,VE ID:23525)?

I think that the following code should correctly handle these cases. Note that I switched from Console.WriteLine to Console.Write and manually add the new line when needed:

private static var previousStringPerPort = new Dictionary<SerialPort,string>();
private static void Port_DataReceived(object sender, 
                                      SerialDataReceivedEventArgs e)
{
    SerialPort spL = (SerialPort) sender;
    int bufSize = 20;
    Byte[] dataBuffer = new Byte[bufSize];
    Console.WriteLine("Data Received at"+DateTime.Now);
    Console.WriteLine(spL.Read(dataBuffer, 0, bufSize));
    if (!previousStringPerPort.ContainsKey(spL))
        previousStringPerPort[spL] = "";
    string s = previousStringPerPort[spL] + 
               System.Text.ASCIIEncoding.ASCII.GetString(dataBuffer);
    s = s.Replace("ID:",Environment.NewLine + "ID:");
    if (s.EndsWith("I"))
    {
        previousStringPerPort[spL] = "I";
        s = s.Remove(s.Length-1);
    }
    else if (s.EndsWith("ID"))
    {
        previousStringPerPort[spL] = "ID";
        s = s.Remove(s.Length - 2);
    }
    Console.Write(s);
}

Now the only problem remaining is that if the last record really does end in I or ID, it will never be printed. A periodic timeout to flush the previous string could fix this, but it introduces (many) more problems of its own.