The Serial Framework's wclSerialClient class is the piece of the SDK that actually opens a serial port and talks to a device. It handles configuration, timeouts, buffer sizes, serial control lines, and — most importantly — gives you an event-driven channel for sending and receiving bytes. This article walks through a complete Serial Client sample implemented in Delphi, C++ Builder, C#, and VB.NET.

Serial Client data flow — Write() sends bytes down through the OS driver to the device, and OnData events deliver received bytes back to the application

The wclSerialClient has a single responsibility: given a device name (for example, COM4 on Windows or /dev/cu.usbserial-1420 on macOS), it opens the port, applies your configuration, and starts delivering data. It does not enumerate devices — that job belongs to the wclSerialMonitor class. In a typical application you use both: the monitor to discover ports, the client to open one. See Monitoring serial and USB devices for the discovery side.


Getting started

The sample is a simple forms application with buttons for enumeration, connect, disconnect, and send, plus a large log window. The essential pieces are the same in every language: create the client, subscribe to its events, create the monitor for enumeration, then wire up the UI.

Creating the client

Both objects — the client and the monitor — are created once, usually in the form's Create or Load handler. All event subscriptions happen here as well.

procedure TfmMain.FormCreate(Sender: TObject);
begin
  FClient := TwclSerialClient.Create;
  FClient.OnConnect := ClientConnect;
  FClient.OnData := ClientData;
  FClient.OnDisconnect := ClientDisconnect;
  FClient.OnError := ClientError;
  FClient.OnReadError := ClientReadError;
  FClient.OnEvents := ClientEvents;

  FMonitor := TwclSerialMonitor.Create;
end;

procedure TfmMain.FormDestroy(Sender: TObject);
begin
  FClient.Disconnect;
  FMonitor.Stop;

  FMonitor.Free;
  FClient.Free;
end;
void __fastcall TfmMain::FormCreate(TObject *Sender)
{
  FClient = new TwclSerialClient();
  FClient->OnConnect = ClientConnect;
  FClient->OnData = ClientData;
  FClient->OnDisconnect = ClientDisconnect;
  FClient->OnError = ClientError;
  FClient->OnReadError = ClientReadError;
  FClient->OnEvents = ClientEvents;

  FMonitor = new TwclSerialMonitor();
}

void __fastcall TfmMain::FormDestroy(TObject *Sender)
{
  FClient->Disconnect();
  FMonitor->Stop();

  FClient->Free();
  FMonitor->Free();
}
private void fmMain_Load(Object sender, EventArgs e)
{
  FClient = new wclSerialClient();
  FClient.OnConnect += ClientConnect;
  FClient.OnData += ClientData;
  FClient.OnDisconnect += ClientDisconnect;
  FClient.OnError += ClientError;
  FClient.OnReadError += ClientReadError;
  FClient.OnEvents += ClientEvents;

  FMonitor = new wclSerialMonitor();
}

private void fmMain_FormClosed(Object sender, FormClosedEventArgs e)
{
  FClient.Disconnect();
  FMonitor.Stop();
}
Private Sub fmMain_Load(sender As Object, e As EventArgs) Handles MyBase.Load
  FClient = New wclSerialClient()
  AddHandler FClient.OnConnect, AddressOf ClientConnect
  AddHandler FClient.OnData, AddressOf ClientData
  AddHandler FClient.OnDisconnect, AddressOf ClientDisconnect
  AddHandler FClient.OnError, AddressOf ClientError
  AddHandler FClient.OnReadError, AddressOf ClientReadError
  AddHandler FClient.OnEvents, AddressOf ClientEvents

  FMonitor = New wclSerialMonitor()
End Sub

Private Sub fmMain_FormClosed(sender As Object, e As FormClosedEventArgs) Handles MyBase.FormClosed
  FClient.Disconnect()
  FMonitor.Stop()
End Sub

Enumerating ports

Before the client can connect to anything, the user has to choose a port. Enumeration is a one-shot operation: the code below fills a combo box with the device names returned by the monitor.

procedure TfmMain.EnumComPorts;
var
  Ports: TwclSerialDevices;
  Res: Integer;
  i: Integer;
begin
  cbPorts.Clear;
  Res := FMonitor.EnumSerialDevices(Ports);
  if Res <> WCL_E_SUCCESS then
    lbEvents.Items.Add('Error enumerating COM ports: 0x' + IntToHex(Res, 8))
  else begin
    for i := 0 to Length(Ports) - 1 do
      cbPorts.Items.Add(Ports[i].DeviceName);

    if cbPorts.Items.Count > 0 then
      cbPorts.ItemIndex := 0
    else
      cbPorts.ItemIndex := -1;
  end;
end;
void __fastcall TfmMain::EnumComPorts()
{
  cbPorts->Clear();

  TwclSerialDevices Ports;
  int Res = FMonitor->EnumSerialDevices(Ports);
  if (Res != WCL_E_SUCCESS)
  {
    lbEvents->Items->Add("Error enumerating COM ports: 0x" + IntToHex(Res, 8));
    return;
  }

  for (int i = 0; i < Ports.Length; i++)
    cbPorts->Items->Add(Ports[i].DeviceName);

  if (cbPorts->Items->Count > 0)
    cbPorts->ItemIndex = 0;
  else
    cbPorts->ItemIndex = -1;
}
private void EnumComPorts()
{
  cbPorts.Items.Clear();

  List<wclSerialDevice> Ports;
  Int32 Res = FMonitor.EnumSerialDevices(out Ports);
  if (Res != wclErrors.WCL_E_SUCCESS)
  {
    lbEvents.Items.Add("Error enumerating COM ports: 0x" + Res.ToString("X8"));
    return;
  }

  foreach (wclSerialDevice Port in Ports)
    cbPorts.Items.Add(Port.DeviceName);

  if (cbPorts.Items.Count > 0)
    cbPorts.SelectedIndex = 0;
  else
    cbPorts.SelectedIndex = -1;
}
Private Sub EnumComPorts()
  cbPorts.Items.Clear()

  Dim Ports As List(Of wclSerialDevice) = Nothing
  Dim Res As Int32 = FMonitor.EnumSerialDevices(Ports)
  If Res <> wclErrors.WCL_E_SUCCESS Then
    lbEvents.Items.Add("Error enumerating COM ports: 0x" + Res.ToString("X8"))
    Return
  End If

  For Each Port As wclSerialDevice In Ports
    cbPorts.Items.Add(Port.DeviceName)
  Next

  If cbPorts.Items.Count > 0 Then
    cbPorts.SelectedIndex = 0
  Else
    cbPorts.SelectedIndex = -1
  End If
End Sub

Connecting and disconnecting

Connecting is a two-step dance. The Connect method only starts the connection procedure — it returns immediately with a success code if the request was accepted, but the real outcome (port opened, or error) is delivered later through the OnConnect event. Check the event's Error parameter to know what actually happened.

procedure TfmMain.btConnectClick(Sender: TObject);
var
  Res: Integer;
begin
  if cbPorts.ItemIndex = -1 then
    MessageDlg('Select COM port', mtWarning, [mbOK], 0)
  else begin
    Res := FClient.Connect(cbPorts.Items[cbPorts.ItemIndex]);
    if Res <> WCL_E_SUCCESS then
      MessageDlg('Error: 0x' + IntToHex(Res, 8), mtError, [mbOK], 0);
  end;
end;

procedure TfmMain.ClientConnect(Sender: TObject; const Error: Integer);
begin
  if Error = WCL_E_SUCCESS then begin
    lbEvents.Items.Add('Connected to Serial Device: ' + FClient.DeviceName);
    ReadConfiguration;
    ReadTimeouts;
    ReadBuffers;
  end else
    lbEvents.Items.Add('Connect error: 0x' + IntToHex(Error, 8));
end;

procedure TfmMain.btDisconnectClick(Sender: TObject);
var
  Res: Integer;
begin
  Res := FClient.Disconnect;
  if Res <> WCL_E_SUCCESS then
    MessageDlg('Error: 0x' + IntToHex(Res, 8), mtError, [mbOK], 0);
end;
void __fastcall TfmMain::btConnectClick(TObject *Sender)
{
  if (cbPorts->ItemIndex == -1)
  {
    MessageDlg("Select COM port", mtWarning, TMsgDlgButtons() << mbOK, 0);
    return;
  }

  int Res = FClient->Connect(cbPorts->Items->Strings[cbPorts->ItemIndex]);
  if (Res != WCL_E_SUCCESS)
    MessageDlg("Error: 0x" + IntToHex(Res, 8), mtError,
      TMsgDlgButtons() << mbOK, 0);
}

void __fastcall TfmMain::ClientConnect(TObject *Sender, const int Error)
{
  if (Error != WCL_E_SUCCESS)
  {
    lbEvents->Items->Add("Connect error: 0x" + IntToHex(Error, 8));
    return;
  }

  lbEvents->Items->Add("Connected to Serial Device: " + FClient->DeviceName);
  ReadConfiguration();
  ReadTimeouts();
  ReadBuffers();
}

void __fastcall TfmMain::btDisconnectClick(TObject *Sender)
{
  int Res = FClient->Disconnect();
  if (Res != WCL_E_SUCCESS)
    MessageDlg("Error: 0x" + IntToHex(Res, 8), mtError,
      TMsgDlgButtons() << mbOK, 0);
}
private void btConnect_Click(Object sender, EventArgs e)
{
  if (cbPorts.SelectedIndex == -1)
  {
    MessageBox.Show("Select COM port");
    return;
  }

  Int32 Res = FClient.Connect(cbPorts.Items[cbPorts.SelectedIndex].ToString());
  if (Res != wclErrors.WCL_E_SUCCESS)
    MessageBox.Show("Error: 0x" + Res.ToString("X8"));
}

private void ClientConnect(Object sender, Int32 Error)
{
  if (Error == wclErrors.WCL_E_SUCCESS)
  {
    lbEvents.Items.Add("Connected to Serial Device: " + FClient.DeviceName);
    ReadConfiguration();
    ReadTimeouts();
    ReadBuffers();
  }
  else
    lbEvents.Items.Add("Connect error: 0x" + Error.ToString("X8"));
}

private void btDisconnect_Click(Object sender, EventArgs e)
{
  Int32 Res = FClient.Disconnect();
  if (Res != wclErrors.WCL_E_SUCCESS)
    MessageBox.Show("Error: 0x" + Res.ToString("X8"));
}
Private Sub btConnect_Click(sender As Object, e As EventArgs) Handles btConnect.Click
  If cbPorts.SelectedIndex = -1 Then
    MessageBox.Show("Select COM port")
    Return
  End If

  Dim Res As Int32 = FClient.Connect(cbPorts.Items(cbPorts.SelectedIndex).ToString())
  If Res <> wclErrors.WCL_E_SUCCESS Then
    MessageBox.Show("Error: 0x" + Res.ToString("X8"))
  End If
End Sub

Private Sub ClientConnect(Sender As Object, [Error] As Integer)
  If [Error] = wclErrors.WCL_E_SUCCESS Then
    lbEvents.Items.Add("Connected to Serial Device: " + FClient.DeviceName)
    ReadConfiguration()
    ReadTimeouts()
    ReadBuffers()
  Else
    lbEvents.Items.Add("Connect error: 0x" + [Error].ToString("X8"))
  End If
End Sub

Private Sub btDisconnect_Click(sender As Object, e As EventArgs) Handles btDisconnect.Click
  Dim Res As Int32 = FClient.Disconnect()
  If Res <> wclErrors.WCL_E_SUCCESS Then
    MessageBox.Show("Error: 0x" + Res.ToString("X8"))
  End If
End Sub

Serial port configuration

The port's operating parameters — baud rate, byte size, parity, stop bits, flow control, DTR/RTS behaviour, XON/XOFF limits, and the special characters — are bundled into a single wclSerialConfig structure. Get the current configuration with GetConfig, modify what you need, then push it back with SetConfig. The structure has the same fields on every platform and every language binding.

The structure is large, but the values map one-to-one with the classic DCB structure from Win32 — plus matching semantics on macOS via termios. The sample UI exposes them all. The two parameters most applications actually change are BaudRate and the Parity / ByteSize / StopBits triple; everything else has safe defaults.

procedure TfmMain.btSetConfigClick(Sender: TObject);
var
  Config: TwclSerialConfig;
  Res: Integer;
begin
  Config.BaudRate := StrToInt64(edBaudRate.Text);
  Config.XonLim := StrToInt(edXonLim.Text);
  Config.XoffLim := StrToInt(edXoffLim.Text);
  Config.XonChar := AnsiChar(StrToInt(edXonChar.Text));
  Config.XoffChar := AnsiChar(StrToInt(edXoffChar.Text));
  Config.ErrorChar := AnsiChar(StrToInt(edErrorChar.Text));
  Config.EofChar := AnsiChar(StrToInt(edEofChar.Text));
  Config.EvtChar := AnsiChar(StrToInt(edEvtChar.Text));

  Config.ParityCheck := cbParityCheck.Checked;
  Config.OutxCtsFlow := cbOutxCtsFlow.Checked;
  Config.OutxDsrFlow := cbOutxDsrFlow.Checked;
  Config.DsrSensitivity := cbDsrSensitivity.Checked;
  Config.TxContinueOnXoff := cbTXContinueOnXoff.Checked;
  Config.OutX := cbOutX.Checked;
  Config.InX := cbInX.Checked;
  Config.ErrorCharReplace := cbErrorCharReplace.Checked;
  Config.NullStrip := cbNullStrip.Checked;
  Config.AbortOnError := cbAbortOnError.Checked;

  Config.RtsControl := IndexToRtsControl(cbRtsControl.ItemIndex);
  Config.DtrControl := IndexToDtrControl(cbDtrControl.ItemIndex);
  Config.Parity := IndexToParity(cbParity.ItemIndex);
  Config.StopBits := IndexToStopBits(cbStopBits.ItemIndex);
  Config.ByteSize := cbByteSize.ItemIndex + 4;

  Res := FClient.SetConfig(Config);
  if Res <> WCL_E_SUCCESS then
    MessageDlg('Error: 0x' + IntToHex(Res, 8), mtError, [mbOK], 0);
end;
void __fastcall TfmMain::btSetConfigClick(TObject *Sender)
{
  TwclSerialConfig Config;
  Config.BaudRate = StrToInt64(edBaudRate->Text);
  Config.XonLim = StrToInt(edXonLim->Text);
  Config.XoffLim = StrToInt(edXoffLim->Text);
  Config.XonChar = Char(StrToInt(edXonChar->Text));
  Config.XoffChar = Char(StrToInt(edXoffChar->Text));
  Config.ErrorChar = Char(StrToInt(edErrorChar->Text));
  Config.EofChar = Char(StrToInt(edEofChar->Text));
  Config.EvtChar = Char(StrToInt(edEvtChar->Text));

  Config.ParityCheck = cbParityCheck->Checked;
  Config.OutxCtsFlow = cbOutxCtsFlow->Checked;
  Config.OutxDsrFlow = cbOutxDsrFlow->Checked;
  Config.DsrSensitivity = cbDsrSensitivity->Checked;
  Config.TxContinueOnXoff = cbTXContinueOnXoff->Checked;
  Config.OutX = cbOutX->Checked;
  Config.InX = cbInX->Checked;
  Config.ErrorCharReplace = cbErrorCharReplace->Checked;
  Config.NullStrip = cbNullStrip->Checked;
  Config.AbortOnError = cbAbortOnError->Checked;

  Config.RtsControl = IndexToRtsControl(cbRtsControl->ItemIndex);
  Config.DtrControl = IndexToDtrControl(cbDtrControl->ItemIndex);
  Config.Parity = IndexToParity(cbParity->ItemIndex);
  Config.StopBits = IndexToStopBits(cbStopBits->ItemIndex);
  Config.ByteSize = cbByteSize->ItemIndex + 4;

  int Res = FClient->SetConfig(Config);
  if (Res != WCL_E_SUCCESS)
    MessageDlg("Error: 0x" + IntToHex(Res, 8), mtError,
      TMsgDlgButtons() << mbOK, 0);
}
private void btSetConfig_Click(Object sender, EventArgs e)
{
  wclSerialConfig Config = new wclSerialConfig();
  Config.BaudRate = Convert.ToUInt32(edBaudRate.Text);
  Config.XonLim = Convert.ToUInt16(edXonLim.Text);
  Config.XoffLim = Convert.ToUInt16(edXoffLim.Text);
  Config.XonChar = Convert.ToByte(edXonChar.Text);
  Config.XoffChar = Convert.ToByte(edXoffChar.Text);
  Config.ErrorChar = Convert.ToByte(edErrorChar.Text);
  Config.EofChar = Convert.ToByte(edEofChar.Text);
  Config.EvtChar = Convert.ToByte(edEvtChar.Text);

  Config.ParityCheck = cbParityCheck.Checked;
  Config.OutxCtsFlow = cbOutxCtsFlow.Checked;
  Config.OutxDsrFlow = cbOutxDsrFlow.Checked;
  Config.DsrSensitivity = cbDsrSensitivity.Checked;
  Config.TxContinueOnXoff = cbTXContinueOnXoff.Checked;
  Config.OutX = cbOutX.Checked;
  Config.InX = cbInX.Checked;
  Config.ErrorCharReplace = cbErrorCharReplace.Checked;
  Config.NullStrip = cbNullStrip.Checked;
  Config.AbortOnError = cbAbortOnError.Checked;

  Config.RtsControl = IndexToRtsControl(cbRtsControl.SelectedIndex);
  Config.DtrControl = IndexToDtrControl(cbDtrControl.SelectedIndex);
  Config.Parity = IndexToParity(cbParity.SelectedIndex);
  Config.StopBits = IndexToStopBits(cbStopBits.SelectedIndex);
  Config.ByteSize = (Byte)(cbByteSize.SelectedIndex + 4);

  Int32 Res = FClient.SetConfig(Config);
  if (Res != wclErrors.WCL_E_SUCCESS)
    MessageBox.Show("Error: 0x" + Res.ToString("X8"));
}
Private Sub btSetConfig_Click(sender As Object, e As EventArgs) Handles btSetConfig.Click
  Dim Config As wclSerialConfig = New wclSerialConfig()
  Config.BaudRate = Convert.ToUInt32(edBaudRate.Text)
  Config.XonLim = Convert.ToUInt16(edXonLim.Text)
  Config.XoffLim = Convert.ToUInt16(edXoffLim.Text)
  Config.XonChar = Convert.ToByte(edXonChar.Text)
  Config.XoffChar = Convert.ToByte(edXoffChar.Text)
  Config.ErrorChar = Convert.ToByte(edErrorChar.Text)
  Config.EofChar = Convert.ToByte(edEofChar.Text)
  Config.EvtChar = Convert.ToByte(edEvtChar.Text)

  Config.ParityCheck = cbParityCheck.Checked
  Config.OutxCtsFlow = cbOutxCtsFlow.Checked
  Config.OutxDsrFlow = cbOutxDsrFlow.Checked
  Config.DsrSensitivity = cbDsrSensitivity.Checked
  Config.TxContinueOnXoff = cbTXContinueOnXoff.Checked
  Config.OutX = cbOutX.Checked
  Config.InX = cbInX.Checked
  Config.ErrorCharReplace = cbErrorCharReplace.Checked
  Config.NullStrip = cbNullStrip.Checked
  Config.AbortOnError = cbAbortOnError.Checked

  Config.RtsControl = IndexToRtsControl(cbRtsControl.SelectedIndex)
  Config.DtrControl = IndexToDtrControl(cbDtrControl.SelectedIndex)
  Config.Parity = IndexToParity(cbParity.SelectedIndex)
  Config.StopBits = IndexToStopBits(cbStopBits.SelectedIndex)
  Config.ByteSize = CType(cbByteSize.SelectedIndex + 4, Byte)

  Dim Res As Int32 = FClient.SetConfig(Config)
  If Res <> wclErrors.WCL_E_SUCCESS Then
    MessageBox.Show("Error: 0x" + Res.ToString("X8"))
  End If
End Sub

The GetConfig method works the opposite way: call it right after OnConnect succeeds to fill the UI with the port's current defaults, so the user can see what the driver has set. The sample does exactly that — it refreshes configuration, timeouts, and buffer sizes as soon as a connection is established.

Timeouts are handled through a separate structure, wclSerialTimeouts, and buffer sizes through the GetReadBufferSize / SetReadBufferSize and GetWriteBufferSize / SetWriteBufferSize pair. All three groups — configuration, timeouts, buffers — are independent and use the same WCL_E_SUCCESS / error-code convention.


Sending data

Once connected, sending is a direct method call: build a byte array and pass it to Write. The method returns the number of bytes the driver accepted through an out parameter — on a serial port this can be less than you passed in if the output buffer is full, so check both the return code and the count.

procedure TfmMain.btSendClick(Sender: TObject);
var
  Res: Integer;
  Str: AnsiString;
  Written: Cardinal;
begin
  Str := AnsiString(edText.Text);
  case cbLineFeed.ItemIndex of
    1: Str := Str + #13;
    2: Str := Str + #10;
    3: Str := Str + #13#10;
  end;

  Res := FClient.Write(Pointer(Str), Length(Str), Written);
  lbEvents.Items.Add('Sent: ' + IntToStr(Written) + ' bytes from ' +
    IntToStr(Length(Str)));
  if Res <> WCL_E_SUCCESS then
    lbEvents.Items.Add('Write error: 0x' + IntToHex(Res, 8));
end;
void __fastcall TfmMain::btSendClick(TObject *Sender)
{
  AnsiString Str = AnsiString(edText->Text);
  switch (cbLineFeed->ItemIndex)
  {
    case 1: Str += AnsiChar(13); break;
    case 2: Str += AnsiChar(10); break;
    case 3: Str += (AnsiChar(13) + AnsiChar(10)); break;
  }

  unsigned int Written = 0;
  int Res = FClient->Write(Str.c_str(), Str.Length(), Written);
  lbEvents->Items->Add("Sent: " + IntToStr((int)Written) + " bytes from " +
    IntToStr(Str.Length()));
  if (Res != WCL_E_SUCCESS)
    lbEvents->Items->Add("Write error: 0x" + IntToHex(Res, 8));
}
private void btSend_Click(Object sender, EventArgs e)
{
  String Str = edText.Text;
  switch (cbLineFeed.SelectedIndex)
  {
    case 1: Str += "\r"; break;
    case 2: Str += "\n"; break;
    case 3: Str += "\r\n"; break;
  }

  Byte[] Ansi = Encoding.ASCII.GetBytes(Str);
  UInt32 Sent = 0;
  Int32 Res = FClient.Write(Ansi, out Sent);
  lbEvents.Items.Add("Sent: " + Sent.ToString() + " bytes from " + Ansi.Length.ToString());
  if (Res != wclErrors.WCL_E_SUCCESS)
    lbEvents.Items.Add("Write error: 0x" + Res.ToString("X8"));
}
Private Sub btSend_Click(sender As Object, e As EventArgs) Handles btSend.Click
  Dim Str As String = edText.Text
  Select Case cbLineFeed.SelectedIndex
    Case 1
      Str += vbCr
    Case 2
      Str += vbLf
    Case 3
      Str += vbCrLf
  End Select

  Dim Ansi As Byte() = Encoding.ASCII.GetBytes(Str)
  Dim Sent As UInt32 = 0
  Dim Res As Int32 = FClient.Write(Ansi, Sent)
  lbEvents.Items.Add("Sent: " + Sent.ToString() + " bytes from " + Ansi.Length.ToString())
  If Res <> wclErrors.WCL_E_SUCCESS Then
    lbEvents.Items.Add("Write error: 0x" + Res.ToString("X8"))
  End If
End Sub

If you need to send a single character out of band — for example, an XOFF or a break — use TransmitCommChar. For sending a break condition on the line itself, use SetCommBreak followed by ClearCommBreak. Both are direct method calls, no event involvement.


Receiving data

There is no synchronous Read method. Data arrives through the OnData event, which fires with a byte array (or pointer plus size in native languages) whenever the OS reports bytes available on the port. The framework does not split or merge frames — it hands you exactly what the driver returned, in whatever chunks it happened to deliver. Protocols with a defined frame length, a delimiter, or a length prefix need to be assembled by your own state machine.

procedure TfmMain.ClientData(Sender: TObject; const Data: Pointer;
  const Size: Cardinal);
var
  Str: AnsiString;
begin
  if Size > 0 then begin
    SetLength(Str, Size);
    CopyMemory(Pointer(Str), Data, Size);
    lbEvents.Items.Add('Received: ' + string(Str));
  end else
    lbEvents.Items.Add('Empty data received');
end;
void __fastcall TfmMain::ClientData(TObject *Sender, const void* Data,
  const unsigned int Size)
{
  if (Size == 0)
  {
    lbEvents->Items->Add("Empty data received");
    return;
  }

  AnsiString Str((char*)Data, Size);
  lbEvents->Items->Add("Received: " + Str);
}
private void ClientData(Object sender, Byte[] Data)
{
  if (Data != null && Data.Length > 0)
  {
    String Str = Encoding.ASCII.GetString(Data);
    lbEvents.Items.Add("Received: " + Str);
  }
  else
    lbEvents.Items.Add("Empty data received");
}
Private Sub ClientData(Sender As Object, Data() As Byte)
  If Data IsNot Nothing AndAlso Data.Length > 0 Then
    Dim Str As String = Encoding.ASCII.GetString(Data)
    lbEvents.Items.Add("Received: " + Str)
  Else
    lbEvents.Items.Add("Empty data received")
  End If
End Sub

Serial events and errors

Besides OnData, the client raises three more events that describe the state of the line itself. They come straight from the OS driver and tell you about the hardware-level conditions on the port:

  • OnEvents — line-status notifications such as evRing (a ring indicator on a modem), evCts, evDsr, evRlsd (carrier detect), evBreak (a break condition received), and evChar (a character matched the EvtChar configuration value).
  • OnError — communication errors (erFrame, erOverrun, erRxParity) combined with the driver's current communication state (csCtsHold, csDsrHold, csXoffHold, and so on).
  • OnReadError — a lower-level error that occurred while reading from the port. In practice this means the OS reported a problem with the read operation itself, as opposed to a framing error inside the data.

Both OnEvents and OnError pass flag sets. The natural follow-up is to query the modem status with GetModemStatus, which returns another flag set (msCtsOn, msDsrOn, msRingOn, msRlsdOn) telling you which control lines are currently asserted.

procedure TfmMain.ClientEvents(Sender: TObject; const Events: TwclSerialEvents);
var
  Str: string;
  Res: Integer;
  Status: TwclModemStatuses;
begin
  Str := '';
  if evBreak in Events then Str := Str + 'evBreak ';
  if evCts in Events   then Str := Str + 'evCts ';
  if evDsr in Events   then Str := Str + 'evDsr ';
  if evRing in Events  then Str := Str + 'evRing ';
  if evRlsd in Events  then Str := Str + 'evRlsd ';
  if evChar in Events  then Str := Str + 'evChar ';
  lbEvents.Items.Add('Event: ' + Str);

  if Events <> [] then begin
    Res := FClient.GetModemStatus(Status);
    if Res <> WCL_E_SUCCESS then
      lbEvents.Items.Add('GetModemStatus error: 0x' + IntToHex(Res, 8))
    else begin
      Str := '';
      if msCtsOn in Status  then Str := Str + 'msCtsOn ';
      if msDsrOn in Status  then Str := Str + 'msDsrOn ';
      if msRingOn in Status then Str := Str + 'msRingOn ';
      if msRlsdOn in Status then Str := Str + 'msRlsdOn ';
      if Str <> '' then
        lbEvents.Items.Add('Modem status: ' + Str);
    end;
  end;
end;
void __fastcall TfmMain::ClientEvents(TObject *Sender,
  const TwclSerialEvents Events)
{
  String Str = "";
  if (Events.Contains(evBreak)) Str += "evBreak ";
  if (Events.Contains(evCts))   Str += "evCts ";
  if (Events.Contains(evDsr))   Str += "evDsr ";
  if (Events.Contains(evRing))  Str += "evRing ";
  if (Events.Contains(evRlsd))  Str += "evRlsd ";
  if (Events.Contains(evChar))  Str += "evChar ";
  lbEvents->Items->Add("Event: " + Str);

  if (!Events.Empty())
  {
    TwclModemStatuses Status;
    int Res = FClient->GetModemStatus(Status);
    if (Res != WCL_E_SUCCESS)
      lbEvents->Items->Add("GetModemStatus error: 0x" + IntToHex(Res, 8));
    else
    {
      Str = "";
      if (Status.Contains(msCtsOn))  Str += "msCtsOn ";
      if (Status.Contains(msDsrOn))  Str += "msDsrOn ";
      if (Status.Contains(msRingOn)) Str += "msRingOn ";
      if (Status.Contains(msRlsdOn)) Str += "msRlsdOn ";
      if (Str != "")
        lbEvents->Items->Add("Modem status: " + Str);
    }
  }
}
private void ClientEvents(Object sender, wclSerialEvent Events)
{
  String Str = "";
  if ((Events & wclSerialEvent.evBreak) != 0) Str += "evBreak ";
  if ((Events & wclSerialEvent.evCts) != 0)   Str += "evCts ";
  if ((Events & wclSerialEvent.evDsr) != 0)   Str += "evDsr ";
  if ((Events & wclSerialEvent.evRing) != 0)  Str += "evRing ";
  if ((Events & wclSerialEvent.evRlsd) != 0)  Str += "evRlsd ";
  if ((Events & wclSerialEvent.evChar) != 0)  Str += "evChar ";
  lbEvents.Items.Add("Event: " + Str);

  if (Events != 0)
  {
    wclModemStatus Status;
    Int32 Res = FClient.GetModemStatus(out Status);
    if (Res != wclErrors.WCL_E_SUCCESS)
      lbEvents.Items.Add("GetModemStatus error: 0x" + Res.ToString("X8"));
    else
    {
      Str = "";
      if ((Status & wclModemStatus.msCtsOn) != 0)  Str += "msCtsOn ";
      if ((Status & wclModemStatus.msDsrOn) != 0)  Str += "msDsrOn ";
      if ((Status & wclModemStatus.msRingOn) != 0) Str += "msRingOn ";
      if ((Status & wclModemStatus.msRlsdOn) != 0) Str += "msRlsdOn ";
      if (Str != "")
        lbEvents.Items.Add("Modem status: " + Str);
    }
  }
}
Private Sub ClientEvents(Sender As Object, Events As wclSerialEvent)
  Dim Str As String = ""
  If (Events And wclSerialEvent.evBreak) <> 0 Then Str += "evBreak "
  If (Events And wclSerialEvent.evCts) <> 0 Then   Str += "evCts "
  If (Events And wclSerialEvent.evDsr) <> 0 Then   Str += "evDsr "
  If (Events And wclSerialEvent.evRing) <> 0 Then  Str += "evRing "
  If (Events And wclSerialEvent.evRlsd) <> 0 Then  Str += "evRlsd "
  If (Events And wclSerialEvent.evChar) <> 0 Then  Str += "evChar "
  lbEvents.Items.Add("Event: " + Str)

  If Events <> 0 Then
    Dim Status As wclModemStatus
    Dim Res As Int32 = FClient.GetModemStatus(Status)
    If Res <> wclErrors.WCL_E_SUCCESS Then
      lbEvents.Items.Add("GetModemStatus error: 0x" + Res.ToString("X8"))
    Else
      Str = ""
      If (Status And wclModemStatus.msCtsOn) <> 0 Then  Str += "msCtsOn "
      If (Status And wclModemStatus.msDsrOn) <> 0 Then  Str += "msDsrOn "
      If (Status And wclModemStatus.msRingOn) <> 0 Then Str += "msRingOn "
      If (Status And wclModemStatus.msRlsdOn) <> 0 Then Str += "msRlsdOn "
      If Str <> "" Then
        lbEvents.Items.Add("Modem status: " + Str)
      End If
    End If
  End If
End Sub

If you need to actively drive the control lines yourself, the client exposes EscapeCommFunction — a single method that takes one of the standard escape codes (escSetDtr, escClrDtr, escSetRts, escClrRts, escSetBreak, escClrBreak, escSetXon, escSetXoff) and applies it immediately. SetCommBreak and ClearCommBreak are convenience wrappers around the two break-related escape codes.


Cross-platform notes: macOS

The wclSerialClient class is fully cross-platform, but there are two things to keep in mind on macOS. First, the device name is the full node path — for example, /dev/cu.usbserial-1420 or /dev/cu.Bluetooth-Incoming-Port — not a short name like COM4. Second, when you call Connect, pass wclMessageProcessingMethod.mpAsync. On macOS there is no Windows-style message pump, so events must be delivered on a worker thread and marshalled by the framework. The macOS demo shows the pattern — the rest of the API is byte-for-byte identical to the Windows sample.

// macOS (C#) — the Serial Framework API is identical to Windows,
// except that Connect() needs the mpAsync message processing method.
wclSerialClient client = new wclSerialClient();
client.OnConnect += (s, error) => {
    if (error == wclErrors.WCL_E_SUCCESS)
        Console.WriteLine("Connected to " + client.DeviceName);
};
client.OnData += (s, data) =>
    Console.WriteLine("Received " + data.Length + " bytes");

int res = client.Connect("/dev/cu.usbserial-1420",
    wclMessageProcessingMethod.mpAsync);
if (res != wclErrors.WCL_E_SUCCESS)
    Console.WriteLine("Connect failed: 0x" + res.ToString("X8"));

// Send bytes exactly like on Windows:
byte[] payload = Encoding.ASCII.GetBytes("hello\r\n");
uint written;
client.Write(payload, out written);

client.Disconnect();

Everything else — GetConfig / SetConfig, timeouts, buffer sizes, OnEvents, OnError, EscapeCommFunction, PurgeComm — behaves the same on both platforms. The complete macOS UI example (which builds a native Cocoa interface through the Objective-C runtime) ships with the SDK and uses exactly this core.


Frequently asked questions

Why does Connect() return success before the port is actually open?
Connect() only starts the connection procedure. The real result is delivered asynchronously through the OnConnect event. Check the Error parameter of OnConnect to know whether the port was opened successfully.
Does the Serial Client work on macOS?
Yes. wclSerialClient is cross-platform. On macOS, pass the full device node path (for example, /dev/cu.usbserial-1420) as the device name, and use wclMessageProcessingMethod.mpAsync when calling Connect(). The rest of the API — configuration, timeouts, events, writes — is identical to Windows.
How do I read data from the port?
Data arrives asynchronously through the OnData event. There is no synchronous Read() method: bytes are delivered as soon as the driver reports them. You may have to split or concatenate frames yourself based on your device protocol.
Do I need the Serial Monitor to use the Serial Client?
No. The client works with any device name you already know (COM4, /dev/cu.usbserial-1420, and so on). The wclSerialMonitor is only needed if you want to enumerate the ports dynamically instead of hard-coding a name. See Monitoring serial and USB devices for that.