串口到网络接口






4.85/5 (13投票s)
实现一个串行端口到网络的用户界面。
引言
我开发此应用程序是由于需要从一台桌面 PC 监视实验室中的串行 PC 端口。我发现有大量免费的串行端口和 TCP/IP 网络应用程序,但没有一个能够连接这两个接口,使串行端口可以通过网络访问。其想法是允许通过网络与串行端口进行全双工通信,处理二进制和 ASCII 数据,允许用户创建脚本以自动响应特定数据模式,并能够将所有活动记录到文本文件中。请参见上面的屏幕截图。
开发
最初使用 MS VC++ 6.0 开发,然后转换为 8.0 (2005),再转换为 9.0 (2008)。
如何使用它
串行端口部分
设置为匹配串行端口参数。“保存配置”会将选定的端口参数保存在此会话和未来会话的首选项文件中。如果为“端口名称”或“速度”选择了“用户选择”,您将在右侧的文本框中输入端口和速度。“开始串行通信”将使用选定的参数打开一个端口。
网络部分
由于此应用程序设计用于运行在监视串行线路的 PC 上,因此它被设置为充当 TCP 服务器。输入端口号(也将保存到首选项文件中)。“监听”以启动服务器。它将连接到任何设置为连接到所选端口和运行此应用程序的 PC 的 IP 地址的客户端。远程桌面 PC 可以使用任何 TCP/IP 客户端应用程序进行此连接。
监视器部分
为流量类型(ASCII 或二进制)设置单选按钮。如果是二进制,监视器文本框窗格会将二进制数据转换为 ASCII 编码的十六进制字节 (0x01020304 == 01 02 03 04)。“关闭”控件会关闭文本显示面板 - 从而通过绕过将文本字符串写入文本框来提高响应时间。另外,为了本地测试应用程序,“toPort”和“toNet”文本框用于手动输入数据(在各自的接口方向上)。请注意,对于二进制模式,数据以 ASCII 编码的十六进制二进制形式输入 (ABCD == 41 42 43 44)。
文件到网络部分
允许用户从磁盘文件输入数据(ASCII 或 ASCII 编码二进制 - 取决于上面的单选按钮选择),作为串行端口数据的代理。它还允许用户选择记录在文件中的记录之间的重复计数和延迟(以毫秒为单位)。这主要用于测试接口。
命令/响应脚本部分
此部分允许用户创建触发器/响应对。第一列是触发器;第二列是响应。P 和 N 单选按钮决定方向(对于触发器 - N 查找网络输入,P 查找端口输入;对于响应 - N 将响应(第二列)发送到网络,P 将响应发送到端口)。对于二进制脚本字符,使用 <>;例如,换行符将是 <0a>。如果选中“LineEnds”,则会在响应字符串中附加行尾。响应延迟将在识别出触发器字符串后延迟 X 毫秒发送响应。例如,触发器可能是“User Id”,关联的响应是“pvanbell”。另一个触发器可能是“Password”,关联的响应是“123456”。
日志记录部分
键入日志文件的路径。“开始”以开始记录流量,“停止”以关闭日志文件。您甚至可以在关闭日志文件之前查看日志(“View Tx”、“View Rx”)。
消息结束延迟部分
此调整用于确定何时读取完整的串行记录。有关完整解释,请参见下面的“棘手的问题”部分。
代码工作原理
当串行通信线程和 TCP 服务器监听线程启动时,应用程序即可使用。为完成此操作,用户必须
- 选择适当的串行端口参数,将其保存到内部变量 - “保存参数”,然后按“开始串行通信”。
- 通过选择一个端口值并按“监听”来启动网络监听线程。
“开始串行通信”代码在根据用户输入的参数设置好串行端口后,会启动串行监听线程 (Rs422ListeningThread
)
/////////////////////////////////////////////////////////////
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();
}
}
而“监听”代码则对网络端执行相同的操作,但使用主程序线程在用户选择的端口上进行监听。
/////////////////////////////////////////////////////////////
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();
}
}
串行监听线程基本上是主循环。它不仅设置了用于读取的串行端口,而且一旦建立串行通信,它就会将所有接收到的数据路由到网络端口 (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();
}
在网络端,一旦建立连接,通过网络接收到的任何数据都会通过 CAsyncServerDlg::OnNewString()
处理程序路由到串行端口。
/////////////////////////////////////////////////////////////
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"));
}
}
}
CAsyncServerDlg::OnNewString()
处理程序只是将任何接收到的数据写入串行端口。
/////////////////////////////////////////////////////////////
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;
}
需要解决的棘手问题
像这样一个通用的应用程序,特别是当读取/写入二进制和 ASCII 记录,并且需要处理可变大小的缓冲区时,问题的关键在于确定什么构成一个完整的记录。这在接口的串行端 [CSerial::Read()
] 尤其成问题。使用“死时间”常数在某种程度上是一种变通方法,但如果死时间值对于特定接口是可配置的,则可以使用。但是,如果使用此解决方案,您将需要一个高分辨率时钟(相对于通常的 Windows 系统时钟,其粒度为 16 毫秒/32 毫秒)。因此,为了做到这一点,我使用了一个类,该类可以在 Windows 下模拟高分辨率时钟延迟 [CMicroSecond::MicroDelay( int uSec )
],我从 www.pudn.com 下载了它。它可能不是最优解决方案,但如果针对特定接口进行调整,则可以奏效。
/////////////////////////////////////////////////////////////
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);
}
/////////////////////////////////////////////////////////////
项目源代码
您可以从上面的链接下载项目源代码。
我包含了用于使用 VC6 (TestServer.dsw)、VC8 (AsyncServer.vcproj.8.txt - 重命名为 AsyncServer.vcproj) 和 VC9 (AsyncServer.vcproj) 进行构建的项目文件。
致谢
- 网络源代码:Microsoft Developer Support 示例代码。
- 串行端口源代码:Ramon de Klein。
- 动画 - Oleg Bykov。