2010-12-03

Asynchronous Serial Port Communication with F#

Asynchronous programming in F# is almost as easy as programming synchronously. It is a compelling reason to use F# rather than another language like C#, which will not have an easy asynchronous model until C# 5.0. Below, I show how communicate to a device over a serial port both ways. The advantage of the asynchronous approach is that it does not lock up the user interface if the device does not respond right away.

module Program
 
open System.Windows
open System.Windows.Controls
open System.IO.Ports
open AsyncSerialPort
 
type Form() as this =
  inherit Window()
 
  do
    let sp = StackPanel()
    this.Content <- sp
    let tbRequest = TextBox();
    tbRequest.Text <- "*IDN?"
    let tbResponse = TextBlock()
    let btn = Button(Content="Query Device")
 
    sp.Children.Add tbRequest |> ignore
    sp.Children.Add tbResponse |> ignore
    sp.Children.Add btn |> ignore
 
    let sp = new SerialPort(PortName="COM1", BaudRate=9600, DataBits=8, Parity=Parity.None, StopBits=StopBits.One);
 
    // synchronous, blocks UI
//    btn.Click.Add(fun _ ->
//      sp.WriteLine tbRequest.Text
//      let rsp = sp.ReadLine();
//      tbResponse.Text <- rsp;
//    )
 
    // asynchronous, does not block UI
    btn.Click.Add(fun _ ->
      async {
        do! sp.AsyncWriteLine tbRequest.Text
        let! rsp = sp.AsyncReadLine()
        tbResponse.Text <- rsp;
      }
      |> Async.StartImmediate
    )
 
    sp.Open()
    ()
 
 
[<System.STAThread>]
do
  Application().Run(Form()) |> ignore

As you can see, the asynchronous code in the above example is just as easy as the synchronous code. The code to deal with the serial port was a bit trickier. The F# libraries provide extension methods like AsyncWrite and AsyncRead for classes like Stream. In this module, I use those extension methods to create ones for System.IO.Ports.SerialPort. These methods implementations may not be perfect, but they work.

module AsyncSerialPort
 
open System.IO.Ports
open System.Text
 
type SerialPort with
 
  member this.AsyncWriteLine(s:string) =
    this.BaseStream.AsyncWrite(this.Encoding.GetBytes(s+"\n"))
 
  // expects a terminating line feed '\n'
  member this.AsyncReadLine() =
    async {
      let sb = StringBuilder()
      let bufferRef = ref (Array.zeroCreate<byte> this.ReadBufferSize)
      let buffer = !bufferRef
      let lastChr = ref 0uy
      while !lastChr <> byte '\n' do
        let! readCount = this.BaseStream.AsyncRead buffer
        lastChr := buffer.[readCount-1]
        sb.Append (this.Encoding.GetString(buffer.[0 .. readCount-1])) |> ignore
      sb.Length <- sb.Length-1 // get rid of '\n'
      return sb.ToString()
    }

Like usual, the code for the solution is available. Feel free to browse the code there and add comments directly to it.