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.