Click here to Skip to main content
Click here to Skip to main content
Go to top

Serial Port to Network Interface

, 15 Mar 2009
Rate this:
Please Sign up or sign in to vote.
Implementing a serial port to network user interface.

Introduction

I developed this application due to a need to monitor a serial PC port in a lab from a desktop PC. I found that there was a plentitude of free serial port and TCP/IP network applications, but none that could interface the two by making a serial port accessible over a network. The idea was to allow full duplex communication with the serial port over the network, to handle both binary and ASCII data, to allow the user to create scripts of auto responses to specific data patterns, and to be able to log all activity to a text file. See the screenshot above.

Development

Originally developed with MS VC++ 6.0, converted to 8.0 (2005), and then to 9.0 (2008).

How to Use It

Serial Port Section

Serial.jpg

Set up to match serial port parameters. "Save Configuration" will save the selected port parameters in a preference file for this and future sessions. If "User Selected" is selected for Port Name or Speed, you would type in the Port and speed in the text boxes to the right. "Start Serial Comms" opens a port with the selected parameters.

Network Section

Network.jpg

Since this application is designed to run on the PC that is monitoring the serial line, it is set up to act as a TCP server. Enter port number (will also be saved to the preference file). "Listen" to start the server. It will connect to any client setup to connect to the selected port and IP address of the PC running this application. The remote desktop PC can use any TC/IP client application to make this connection.

Monitor Section

Monitor.jpg

Set radio buttons for type of traffic (ASCII or Binary). If binary, the monitor textbox panes will convert binary data to ASCII coded hexadecimal bytes (0x01020304 == 01 02 03 04). The "Off" control turns the text display panels off - thus increasing the response time by bypassing writing text strings to the textboxes. And, for local testing of the application, the "toPort" and "toNet" textboxes are for manual entry of data (in the respective interface directions). Note that for binary mode, the data is entered in ASCII coded hex binary (ABCD == 41 42 43 44).

File to Network Section

File.jpg

Allows user to input data (ASCII or ASCII Coded Binary - depending on the radio button selection above) from a disk file as a proxy for Serial Port data. It also allows the user to select a repeat count and delay (in milliseconds) between the records in the file. This is primarily for testing the interface.

Command / Response Scripting Section

Command.jpg

This section allows the user to create trigger / response pairs. The first column is the trigger; the second column is for the response. The P and N radio buttons determine the direction (for Trigger - N looks for Network input and P for Port input; for Response - N sends response (second column) to the Network, P sends response to the Port). For binary scripted characters, use <>; e.g., a LineFeed would be <0a>. Line Endings are appended to the Response string if "LineEnds" is checked. The Response Delay will delay the responses X msec after the trigger string is identified. For example, a Trigger may be the "User Id" and the associated response "pvanbell". Another Trigger may be "Password" with the associated response "123456".

Logging Section

Log.jpg

Type in the log file with the path. "Start" to start the logging traffic, "Stop" to close the log file. You can view the log even before closing the log file ("View Tx", "View Rx").

End of Message Delay Section

tweak.jpg

This tweak is to determine when a full serial record has been read. See the "Tricky Problem" section below for a full explanation.

How the Code Works

The application is ready for use when the serial communication thread and the TCP server listening thread is started. To accomplish this, the user must:

  1. Select the appropriate serial port parameters, save them to internal variables - "Save Parameters", and then press "Start Serial Comms".
  2. Start the network listening thread by selecting a port value and pressing "Listen".

The "Start Serial Comms" code starts the Serial Listening thread (Rs422ListeningThread) after setting up the serial port as per the user entered parameters:

///////////////////////////////////////////////////////////// 
void CAsyncServerDlg::OnBnClickedSerialstart()
{
    CString Str;
    GetDlgItem(IDC_SERIALSTART)->GetWindowTextA(Str); 
    if (Str == "Start Serial Comms")  
    {
        if (m_serial422io->setup(m_Port,m_Baudrate,
            m_DataBits,m_Parity,m_StopBits,m_Flow))
        {
            m_serialCom=true;
            m_hThread = CreateThread( 
                NULL,                        // no security attributes 
                0,                           // use default stack size  
                (LPTHREAD_START_ROUTINE)Rs422ListeningThread,   // thread function 
                this,                                       // argument to thread function 
                0,                           // use default creation flags 
                &m_hThreadId);                        // returns the thread identifier 
            if (m_hThread == NULL)
            {
                AfxMessageBox(_TEXT("Error Creating rs422 Listening Thread"));
                GetDlgItem(IDC_SERIALSTART)->EnableWindow(TRUE);
                m_serial422io->close();
                m_serialCom=false;
                return;
            }
            else
            {
                SetThreadPriority (m_hThread, THREAD_PRIORITY_MIN);
                OnThreadStart((WPARAM)m_hThread,0);  
                GetDlgItem(IDC_SERIALSTART)->SetWindowTextA("Stop Serial Comms");
                Str = "Connected to Serial Port: " + m_Port;
                p_status->SetWindowTextA(Str);
                if (m_dogAnim.Load("animation.gif"))  
                {
                    m_dogAnim.Draw();
                }
                GetDlgItem(IDC_LISTEN)->GetWindowTextA(Str);
                if (Str == "Listen") OnBnClickedListen();
            }
        }
        else
        {
            Str = "Unable to Setup Serial Comm Port: " + m_Port;
            AfxMessageBox(Str);
            m_serial422io->close();
        } 
    }  
    else
    {
        GetDlgItem(IDC_SERIALSTART)->SetWindowTextA("Start Serial Comms");
        m_serialCom = false;
        TerminateThread(m_hThread,0);
        Sleep(100);
        m_serial422io->close();
        p_status->SetWindowTextA("Connected to Serial Port Closed");
        m_dogAnim.UnLoad();
        RedrawWindow();
    }
}

and the "Listen" code does the same for the network-side, but uses the main program thread to listen on the user-selected port.

/////////////////////////////////////////////////////////////
void CAsyncServerDlg::OnBnClickedListen()
{
      CString Str;
      GetDlgItem(IDC_LISTEN)->GetWindowTextA(Str);
      if (Str == "Listen") 
      {
            GetDlgItem(IDC_LISTEN)->SetWindowTextA("Close");
            m_port = GetDlgItemInt(IDC_SERVERPORT);
            m_listensoc.Create(m_port);
            m_listensoc.Listen();
      }
      else
      {
            GetDlgItem(IDC_LISTEN)->SetWindowTextA("Listen");
            m_listensoc.Close();
      }
}

The serial listening thread is essentially the main loop. It not only sets up the serial port for reads, but once the serial communication is established, it routes all received data to the network port (AsyncSendBuff()).

///////////////////////////////////////////////////////////// 
void Rs422ListeningThread(CAsyncServerDlg* ptr)
{
      char buf[MAX_BUF_SIZE];
      unsigned msgSize=0;
      int eomWait = ptr->GetDlgItemInt(IDC_EOMTIME);
 
      if (ptr->m_serial422io->setupForRead(ptr->m_monitorType))
      {
            while(ptr->m_serialCom)
            {
                  msgSize = ptr->m_serial422io->read(buf,MAX_BUF_SIZE,eomWait);
                  if (msgSize) 
                  {
                        if (ptr->m_connected) ptr->m_soc->AsyncSendBuff(buf, msgSize);
                        if (ptr->m_monitorTraffic == true)
                        {
                              CString str;
                              char sendMsg[10];
                              sprintf(sendMsg,"%u",msgSize);
                              str = sendMsg;
                              str+= ": ";
 
                              if (ptr->m_monitorType == MonBinary)
                              {
                                    for (unsigned i=0;i<msgSize;i++)
                                    {
                                          sprintf(sendMsg,"%02x ",(unsigned char)buf[i]);
                                          str+=sendMsg;
                                    }
                              }
                              else
                              {
                                    buf[msgSize] = '\0';
                                    str = buf;
                              }
                              ptr->WriteToRxList(str);
                              ptr->logRxData(str);
                              Sleep(0);
                        }
                  } 
                  else
                  {
                        Sleep(0);
                  }
            }
      }
      else
      {
            AfxMessageBox(_TEXT("Serial Read Setup Error - Closing Port"));
      }
      ptr->m_serialCom=false;
      ptr->m_serial422io->close();
}

On the network side, once a connection is established, any data received over the network is routed to the serial port via the CAsyncServerDlg::OnNewString() handler.

/////////////////////////////////////////////////////////////
void CConnectSoc::OnReceive(int nErrorCode) 
{
  int nRead = 0; 

  // data needs to be read (which should be all the time when this is called)
  if (m_nBytesRecv < m_nRecvDataLen) 
  {
      // receive buffer max size is MAX_BUF_SIZE
      // We must have enough room available in buffer AND
      // the expected packet size must be less or equal to MAX_BUF_SIZE
      ASSERT(m_nBytesRecv < MAX_BUF_SIZE && m_nRecvDataLen <= MAX_BUF_SIZE);

      // read all the data
      nRead = Receive(m_recvBuff,MAX_BUF_SIZE);

      CAsyncServerDlg* pDlg = (CAsyncServerDlg*) (AfxGetApp()->GetMainWnd());
      // if something was read
      if (nRead > 0) 
      {
          m_nBytesRecv = nRead;

          // extract data from buffer and pass data to the upper layer.
          // We append the body of the packet with a string terminator.

          if (m_nRecvDataLen <= MAX_BUF_SIZE)
                 m_recvBuff[m_nBytesRecv] = '\0';
          else
                m_recvBuff[MAX_BUF_SIZE] = '\0';

          char sendMsg[10];
          CString printMsg;

          if (pDlg->m_monitorTraffic == true)
          {
            if (pDlg->m_monitorType == MonBinary)
            {
              for (int i=0;i<m_nBytesRecv;i++)
              {
                    sprintf(sendMsg,"%02x ", 
                           (unsigned char)m_recvBuff[i]);
                    printMsg+=sendMsg;
              }
              *m_pLastString = printMsg.GetBuffer();
            }
            else
            {
              m_recvBuff[m_nBytesRecv] = '\0';
              printMsg = m_recvBuff;
              *m_pLastString = printMsg.GetBuffer();
            }
          }
                            
          pDlg->OnNewString((WPARAM)m_recvBuff,(LPARAM)m_nBytesRecv);
          // re-initializaton
          m_nRecvDataLen = m_nBytesRecv;
          m_nBytesRecv = 0;
      } 
      else 
      {     // else error occurred
          if (GetLastError() != WSAEWOULDBLOCK) 
          {
            m_nBytesRecv = m_nRecvDataLen;
            AfxMessageBox(_TEXT("Socket Error. Unable to read data."));
          } 
          else
            TRACE(_TEXT("CConnectSoc: WARNING: WSAEWOULDBLOCK on a Receive in OnReceive\n"));
      }
  }
}

The CAsyncServerDlg::OnNewString() handler simply writes any received data to the serial port.

/////////////////////////////////////////////////////////////
LRESULT CAsyncServerDlg::OnNewString(WPARAM wParam, LPARAM lParam)
{
      // a new string has been received. Update the UI
      // Synchronize access to m_lastString
      // m_criticalSection.Lock();
      unsigned bytesRead = 0;
 
      // write to rs422
      if (m_serialCom)
      {
            bytesRead=m_serial422io->write((char*)wParam,(unsigned)lParam);
            {
                 if (m_monitorTraffic == true)
                 {
                      WriteToTxList(m_lastString);
                      logTxData(m_lastString);
                 }
            }
            Sleep(0);
      } 
      else
      { 
            if (m_monitorTraffic == true)
            {
                  SetDlgItemText(IDC_LASTSTRING, m_lastString);
                  WriteToTxList(m_lastString);
                  logTxData(m_lastString);
            }
      }
      
      // Remove WM_NEWSTRING messages in the message Q.
      // By the time we get here m_lastString truly has the
      // last received message before we locked the critical section
      // so we can remove extra messages.
      MSG msg;
      while(::PeekMessage(&msg, m_hWnd, WM_NEWSTRING, WM_NEWSTRING, PM_REMOVE));
 
      return 0;
}

Tricky Problem to Solve

The problem with a very general application like this one, especially when both binary and ASCII records are read/written and a variable size buffer is expected, is determining what constitutes a complete record. This is particularly a problem on the serial side [CSerial::Read()] of the interface. Using a "dead time" constant is somewhat of a hack, but can work if the dead time value is configurable for a particular interface. However, if this solution is used, you would need a high resolution clock (vs. the typical Windows system clock with a 16 msec / 32 msec granularity). So, to do this, I used a class that emulates a high resolution clock delay under Windows [CMicroSecond::MicroDelay( int uSec )], which I downloaded from www.pudn.com. It may not be the optimal solution, but it can work if you tweak it right for a particular interface.

/////////////////////////////////////////////////////////////
if (eEvent & CSerial::EEventRecv)
{
    // Read data, until there is nothing left
    do
    {
        // Read data from the COM-port
        lLastError = serial.Read(szBuffer+dwTotalBytesRead,
                                RETURN_BUF_SIZE,&dwBytesRead,
                                &m_ovRead, INFINITE);
        dwTotalBytesRead+= dwBytesRead;
        if (dwTotalBytesRead >= (bufSize-RETURN_BUF_SIZE))
        {
            break;
        }
        if (lLastError != ERROR_SUCCESS)
        {
            ShowError(serial.GetLastError(), _T("Unable to read from COM-port."));
            //m_duplexMutex.Unlock();
            return 0;
        }
        if (m_dataType == MonAscii) 
        {
            m_puSec->MicroDelay(eomWait);
        }
        else
        {
            m_puSec->MicroDelay(eomWait);
        }
    }
    while (dwBytesRead);
}

/////////////////////////////////////////////////////////////

Project Source

You can download the project source code from the link above.

I've included project files for building with VC6 (TestServer.dsw), VC8 (AsyncServer.vcproj.8.txt - change name to AsyncServer.vcproj), and VC9 (AsyncServer.vcproj).

Credits

  • Networking source code: Microsoft Developer Support sample code.
  • Serial Port source code: Ramon de Klein.
  • Animations - Oleg Bykov.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Paul Van Bellinghen
Software Developer (Senior) Autoliv Electronics
United States United States
I've been an embedded software developer for 34 years - almost as long as electronic system manufacturers began using microprocessors for real-time controllers. In more recent years, I have been using Microsoft Visual Basic/C++ Studio to create GUIs to test embedded system code developed by myself or other team members. Much of this code was developed using free code found on this web site.

Comments and Discussions

 
Questionconnect with instrument Pinmemberrami hattab6-Jan-13 1:27 
QuestionChanging to client PinmemberLeslie7910-Jan-12 10:27 
GeneralUnable to load or work with VC60 PinmemberP.Opdahl19-Aug-10 11:52 
AnswerProject Source Code PinmemberPaul Van Bellinghen14-Mar-09 13:26 
GeneralRe: Project Source Code PinmemberTonyTrokodero26-Mar-09 9:16 
GeneralRe: Project Source Code PinmemberPaul Van Bellinghen29-Mar-09 15:00 
QuestionAny change you could post the full project source PinmemberCool Hand Bob12-Mar-09 3:19 
AnswerRe: Any change you could post the full project source PinmemberPaul Van Bellinghen14-Mar-09 13:58 
See article - I added a link to the source at the end.
GeneralMy vote of 1 PinmemberJim Crafton2-Mar-09 4:31 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web04 | 2.8.140916.1 | Last Updated 15 Mar 2009
Article Copyright 2009 by Paul Van Bellinghen
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid