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
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
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
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
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
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
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
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:
- Select the appropriate serial port parameters, save them to internal variables - "Save Parameters", and then press "Start Serial Comms".
- 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, 0, (LPTHREAD_START_ROUTINE)Rs422ListeningThread, this, 0, &m_hThreadId); 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;
if (m_nBytesRecv < m_nRecvDataLen)
{
ASSERT(m_nBytesRecv < MAX_BUF_SIZE && m_nRecvDataLen <= MAX_BUF_SIZE);
nRead = Receive(m_recvBuff,MAX_BUF_SIZE);
CAsyncServerDlg* pDlg = (CAsyncServerDlg*) (AfxGetApp()->GetMainWnd());
if (nRead > 0)
{
m_nBytesRecv = nRead;
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);
m_nRecvDataLen = m_nBytesRecv;
m_nBytesRecv = 0;
}
else
{ 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)
{
unsigned bytesRead = 0;
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);
}
}
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)
{
do
{
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."));
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.