65.9K
CodeProject 正在变化。 阅读更多。
Home

串口到网络接口

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.85/5 (13投票s)

2009年3月2日

CPOL

5分钟阅读

viewsIcon

90565

downloadIcon

6406

实现一个串行端口到网络的用户界面。

引言

我开发此应用程序是由于需要从一台桌面 PC 监视实验室中的串行 PC 端口。我发现有大量免费的串行端口和 TCP/IP 网络应用程序,但没有一个能够连接这两个接口,使串行端口可以通过网络访问。其想法是允许通过网络与串行端口进行全双工通信,处理二进制和 ASCII 数据,允许用户创建脚本以自动响应特定数据模式,并能够将所有活动记录到文本文件中。请参见上面的屏幕截图。

开发

最初使用 MS VC++ 6.0 开发,然后转换为 8.0 (2005),再转换为 9.0 (2008)。

如何使用它

串行端口部分

Serial.jpg

设置为匹配串行端口参数。“保存配置”会将选定的端口参数保存在此会话和未来会话的首选项文件中。如果为“端口名称”或“速度”选择了“用户选择”,您将在右侧的文本框中输入端口和速度。“开始串行通信”将使用选定的参数打开一个端口。

网络部分

Network.jpg

由于此应用程序设计用于运行在监视串行线路的 PC 上,因此它被设置为充当 TCP 服务器。输入端口号(也将保存到首选项文件中)。“监听”以启动服务器。它将连接到任何设置为连接到所选端口和运行此应用程序的 PC 的 IP 地址的客户端。远程桌面 PC 可以使用任何 TCP/IP 客户端应用程序进行此连接。

监视器部分

Monitor.jpg

为流量类型(ASCII 或二进制)设置单选按钮。如果是二进制,监视器文本框窗格会将二进制数据转换为 ASCII 编码的十六进制字节 (0x01020304 == 01 02 03 04)。“关闭”控件会关闭文本显示面板 - 从而通过绕过将文本字符串写入文本框来提高响应时间。另外,为了本地测试应用程序,“toPort”和“toNet”文本框用于手动输入数据(在各自的接口方向上)。请注意,对于二进制模式,数据以 ASCII 编码的十六进制二进制形式输入 (ABCD == 41 42 43 44)。

文件到网络部分

File.jpg

允许用户从磁盘文件输入数据(ASCII 或 ASCII 编码二进制 - 取决于上面的单选按钮选择),作为串行端口数据的代理。它还允许用户选择记录在文件中的记录之间的重复计数和延迟(以毫秒为单位)。这主要用于测试接口。

命令/响应脚本部分

Command.jpg

此部分允许用户创建触发器/响应对。第一列是触发器;第二列是响应。P 和 N 单选按钮决定方向(对于触发器 - N 查找网络输入,P 查找端口输入;对于响应 - N 将响应(第二列)发送到网络,P 将响应发送到端口)。对于二进制脚本字符,使用 <>;例如,换行符将是 <0a>。如果选中“LineEnds”,则会在响应字符串中附加行尾。响应延迟将在识别出触发器字符串后延迟 X 毫秒发送响应。例如,触发器可能是“User Id”,关联的响应是“pvanbell”。另一个触发器可能是“Password”,关联的响应是“123456”。

日志记录部分

Log.jpg

键入日志文件的路径。“开始”以开始记录流量,“停止”以关闭日志文件。您甚至可以在关闭日志文件之前查看日志(“View Tx”、“View Rx”)。

消息结束延迟部分

tweak.jpg

此调整用于确定何时读取完整的串行记录。有关完整解释,请参见下面的“棘手的问题”部分。

代码工作原理

当串行通信线程和 TCP 服务器监听线程启动时,应用程序即可使用。为完成此操作,用户必须

  1. 选择适当的串行端口参数,将其保存到内部变量 - “保存参数”,然后按“开始串行通信”。
  2. 通过选择一个端口值并按“监听”来启动网络监听线程。

“开始串行通信”代码在根据用户输入的参数设置好串行端口后,会启动串行监听线程 (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。
© . All rights reserved.