#include "mbftpch.h" #include "nullmdm.h" // #undef TRACE_OUT // #define TRACE_OUT WARNING_OUT BYTE g_szNULLMStartString[] = "NULLMDM"; ULONG g_nConnID = 0; DWORD __stdcall TPhysWorkerThreadProc(void *lpv); CNullModem::CNullModem(HINSTANCE hDllInst) : m_fInitialized(FALSE), m_hDllInst(hDllInst), m_pfnCallback(NULL), m_fListening(FALSE), m_hwnd(NULL), m_nTransportID(0), m_nConnectionID(0), m_dwThreadID(0), m_hThread(NULL), m_hevtOverlapped(NULL), m_dwEventMask(0), m_fCommPortInUse(FALSE) { ::ZeroMemory(&m_Line, sizeof(m_Line)); ::ZeroMemory(&m_Overlapped, sizeof(m_Overlapped)); ::ZeroMemory(&m_DefaultTimeouts, sizeof(m_DefaultTimeouts)); m_hevtOverlapped = ::CreateEvent(NULL, TRUE, FALSE, NULL); // manual reset ASSERT(NULL != m_hevtOverlapped); } CNullModem::~CNullModem(void) { if (m_fInitialized) { TPhysTerminate(); } if (NULL != m_hevtOverlapped) { ::CloseHandle(m_hevtOverlapped); } } ////////////////////////////////////////////////////////////////////////////// // TPhysInitialize ////////////////////////////////////////////////////////////////////////////// TPhysicalError CNullModem::TPhysInitialize ( TPhysCallback callback, UINT nTransportID ) { const char *pszNULLMClassName = "COMMWndClass"; TPhysicalError rc = TPHYS_SUCCESS; TRACE_OUT(("TPhysInitialize")); // // If we have already been initialized then this is a reinit call from // the node controller so just do nothing. // if (! m_fInitialized) { // // zero out the SESSION INFO structure // ::ZeroMemory(&m_Line, sizeof(m_Line)); // // Store control information // m_pfnCallback = callback; m_nTransportID = nTransportID; m_fInitialized = TRUE; m_nConnectionID = ++g_nConnID; // // Create a window so we can decouple to the node controller context // Register the main window class. Since it is invisible, leave // the wndclass structure sparse. // WNDCLASS wc; ::ZeroMemory(&wc, sizeof(wc)); wc.style = 0; wc.lpfnWndProc = TPhysWndProc; wc.cbClsExtra = 0; wc.cbWndExtra = 0; wc.hInstance = m_hDllInst; wc.hIcon = NULL; wc.hCursor = NULL; wc.hbrBackground = NULL; wc.lpszMenuName = NULL; wc.lpszClassName = pszNULLMClassName; ::RegisterClass(&wc); // // Create the main window. // m_hwnd = ::CreateWindow( pszNULLMClassName, // See RegisterClass() call. NULL, // It's invisible! 0, // Window style. 0, // Default horizontal position. 0, // Default vertical position. 0, // Default width. 0, // Default height. NULL, // No parent. NULL, // No menu. m_hDllInst, // This instance owns this window. NULL // Pointer not needed. ); if (NULL == m_hwnd) { ERROR_OUT(( "Failed to create wnd")); return(TPHYS_RESULT_FAIL); } // rc = g_lpfnPTPhysicalInit(TPhysDriverCallback, NULL); ASSERT(TPHYS_SUCCESS == rc); } return rc; } ////////////////////////////////////////////////////////////////////////////////// // TPhysTerminate // // The node controller is shutting down // Destroy our window and clean up transports. ////////////////////////////////////////////////////////////////////////////////// TPhysicalError CNullModem::TPhysTerminate(void) { TRACE_OUT(("TPhysTerminate")); if (! m_fInitialized) { return(TPHYS_RESULT_NOT_INITIALIZED); } // // Clean up the PSTN transport // // g_lpfnPTPhysicalCleanup(); // // Destroy the window // if (NULL != m_hwnd) { ::DestroyWindow(m_hwnd); m_hwnd = NULL; } m_pfnCallback = NULL; m_nTransportID = 0; m_fInitialized = FALSE; return(TPHYS_SUCCESS); } ////////////////////////////////////////////////////////////////////////////// // TPhysConnectRequest // // The node controller wants to place a call and has determined that it // first needs a physical modem connection. Make a call and flag an // outgoing call so that when the call comes active we can tell the modem // transport to begin negotiation. ////////////////////////////////////////////////////////////////////////////// TPhysicalError CNullModem::TPhysConnectRequest(LPSTR pszComPort) { TPhysicalError rc = TPHYS_SUCCESS; HANDLE hCommLink; DCB dcb; DWORD dwWritten; TRACE_OUT(("TPhysConnectRequest")); if (! m_fInitialized) { ERROR_OUT(("NULL MODEM OOB not initialized")); return TPHYS_RESULT_NOT_INITIALIZED; } // // Select a comm port for the call. // if (CALL_STATE_IDLE == m_Line.eCallState || CALL_STATE_DROP == m_Line.eCallState) { // // Also prime our local copy of the conninfo structure for use in callbacks // m_Line.connInfo.resultCode = 0; m_Line.connInfo.connectionID = m_nConnectionID; } else { ERROR_OUT(("No comm port is available")); return TPHYS_RESULT_COMM_PORT_BUSY; } // lonchanc: From now on, we can bail out thru the common exit point // because the cleanup checks for m_fCommPortInUse. // // Only alow one at at time // // lonchanc: g_COMM_Thread_Users is starting from -1 // why can we simply use a flag??? if (m_fCommPortInUse) { ERROR_OUT(("TPhysConnectRequest: Waiting for a previous null mode connection")); rc = TPHYS_RESULT_WAITING_FOR_CONNECTION; goto bail; } m_fCommPortInUse = TRUE; // // Open the comm port // hCommLink = ::CreateFile(pszComPort, GENERIC_READ | GENERIC_WRITE, 0, // exclusive access NULL, // no security attrs OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED, // overlapped I/O NULL ); if (hCommLink == INVALID_HANDLE_VALUE) { ERROR_OUT(("TPhysConnectRequest: CreateFile failed. err=%d", ::GetLastError())); rc = TPHYS_RESULT_COMM_PORT_BUSY; goto bail; } m_Line.hCommLink = hCommLink; // // remember the default timeouts // ::GetCommTimeouts(hCommLink, &m_DefaultTimeouts); // // Let the other side know that we are trying to connect // if (! ::EscapeCommFunction(hCommLink, SETDTR)) { ERROR_OUT(("TPhysConnectRequest: Unable to Set DTR: err=%d", ::GetLastError())); } ::ZeroMemory(&m_Overlapped, sizeof(m_Overlapped)); m_Overlapped.hEvent = m_hevtOverlapped; m_dwEventMask = 0; ::ResetEvent(m_hevtOverlapped); if (! ::WriteFile(hCommLink, g_szNULLMStartString, sizeof(g_szNULLMStartString), &dwWritten, &m_Overlapped)) { DWORD dwErr = ::GetLastError(); if (ERROR_IO_PENDING != dwErr) { ERROR_OUT(("TPhysConnectRequest: WriteFile failed. err=%d", dwErr)); ::ResetEvent(m_hevtOverlapped); rc = TPHYS_RESULT_COMM_PORT_BUSY; goto bail; } else { DWORD dwWait = ::WaitForSingleObject(m_hevtOverlapped, INFINITE); BOOL fRet = ::GetOverlappedResult(hCommLink, &m_Overlapped, &dwWritten, TRUE); ASSERT(fRet); ASSERT(dwWritten == sizeof(g_szNULLMStartString)); } } else { ASSERT(dwWritten == sizeof(g_szNULLMStartString)); } ::ResetEvent(m_hevtOverlapped); // // Get the default dcb // ::ZeroMemory(&dcb, sizeof(dcb)); ::GetCommState(hCommLink, &dcb); dcb.BaudRate = 19200; // Default: BaudRate // // Set our state so we can get notification in the comm port // dcb.DCBlength = sizeof(DCB); dcb.fBinary = 1; // binary mode, no EOF check dcb.fParity = 0; // enable parity checking dcb.fOutxCtsFlow = 1; // CTS output flow control dcb.fOutxDsrFlow = 0; // DSR output flow control dcb.fDtrControl = DTR_CONTROL_ENABLE; // DTR flow control type dcb.fDsrSensitivity = 0; // DSR sensitivity dcb.fTXContinueOnXoff = 0; // XOFF continues Tx dcb.fOutX = 0; // XON/XOFF out flow control dcb.fInX = 0; // XON/XOFF in flow control dcb.fErrorChar = 0; // enable error replacement dcb.fNull = 0; // enable null stripping dcb.fRtsControl = RTS_CONTROL_HANDSHAKE;// RTS flow control dcb.XonLim = 0; // transmit XON threshold dcb.XoffLim = 0; // transmit XOFF threshold dcb.fErrorChar = 0; // enable error replacement dcb.fNull = 0; // enable null stripping dcb.fAbortOnError = 0; // abort reads/writes on error ::SetCommState(hCommLink, &dcb); ::PurgeComm(hCommLink, PURGE_TXABORT | PURGE_RXABORT | PURGE_TXCLEAR | PURGE_RXCLEAR); m_Line.eCallState = CALL_STATE_MAKE; m_Line.hevtCall = m_hevtOverlapped; m_Line.pstnHandle = (PHYSICAL_HANDLE) hCommLink; m_Line.fCaller = TRUE; if (! ::SetCommMask(hCommLink, EV_RXCHAR | // Any Character received EV_CTS | // CTS changed state EV_DSR | // DSR changed state EV_RLSD| // RLSD changed state EV_RXFLAG)) // Certain character { ERROR_OUT(("TPhysConnectRequest: Unable to SetCommMask: err=%d", ::GetLastError())); } ::ZeroMemory(&m_Overlapped, sizeof(m_Overlapped)); m_Overlapped.hEvent = m_hevtOverlapped; m_dwEventMask = 0; ::ResetEvent(m_hevtOverlapped); if (! ::WaitCommEvent(hCommLink, &m_dwEventMask, &m_Overlapped)) { DWORD dwErr = ::GetLastError(); if (ERROR_IO_PENDING != dwErr) { ERROR_OUT(("TPhysConnectRequest: WaitCommEvent failed, err=%d", dwErr)); m_fCommPortInUse = FALSE; } } #if 1 WorkerThreadProc(); #else // // If the comm thread doesn't exist, create it now. // // lonchanc: I am not sure that the thread will exist, because // the while loop inside the thread will exit if m_fCommPortInUse is false. // If m_fCommPortInUse is true, then we should bail out already. // ASSERT(NULL == m_hThread); // // We need to create another thread that will wait on comm events. // m_hThread = ::CreateThread(NULL, 0, TPhysWorkerThreadProc, this, 0, &m_dwThreadID); ASSERT(NULL != m_hThread); #endif bail: return(rc); } ////////////////////////////////////////////////////////////////////////////// // TPhysDisconnect // // The node controller wants us to bring the call down now. We must first // ask the transports to close down their physicall connection. // // Note that in the case of the PSTN transport we use a NOWAIT call which // completes syncronously. Because we will not get a follow on confirm // we simulate one from here. If we switch to using WAIT mode then // take the event generation code out! ////////////////////////////////////////////////////////////////////////////// TPhysicalError CNullModem::TPhysDisconnect(void) { TRACE_OUT(("TPhysDisconnect")); if (! m_fInitialized) { ERROR_OUT(( "Not initialised")); return(TPHYS_RESULT_NOT_INITIALIZED); } TRACE_OUT(("Disconnect call, state %u", m_Line.eCallState)); // // Otherwise it must be a pstn call in progress so end that. Note // that NC may still think it is MODEM, but we still need to close // PSTN. // // g_lpfnPTPhysicalDisconnectRequest(m_aLines[lineID].pstnHandle, TPHYSICAL_NO_WAIT); ::PostMessage(m_hwnd, WM_TPHYS_DISCONNECT_CONFIRM, (WPARAM) this, m_Line.pstnHandle); // // close the comport // DropCall(); return(TPHYS_SUCCESS); } ////////////////////////////////////////////////////////////////////////////// // TPhysListen/TPhysUnlisten // // NULLMMAN does very little with listen/unlisten, just sets a state variable // that is interrogated to see if we should accept incoming calls. // // NOTE - This is different from the Listen/Unlisten that is used to tell // the PSTN transport about the presence of an incoming call. ////////////////////////////////////////////////////////////////////////////// TPhysicalError CNullModem::TPhysListen(void) { TRACE_OUT(("TPhysListen")); if (! m_fInitialized) { ERROR_OUT(("TPhysListen: not initialized")); return(TPHYS_RESULT_NOT_INITIALIZED); } m_fListening = TRUE; return(TPHYS_SUCCESS); } TPhysicalError CNullModem::TPhysUnlisten(void) { TRACE_OUT(("TPhysUnlisten")); if (! m_fInitialized) { ERROR_OUT(("TPhysUnlisten: not initialized")); return(TPHYS_RESULT_NOT_INITIALIZED); } m_fListening = FALSE; return(TPHYS_SUCCESS); } ////////////////////////////////////////////////////////////////////////////// // DropCall - close the comm port ///////////////////////////////////////////////////////////////////////////// void CNullModem::DropCall(void) { TRACE_OUT(("DropCall")); // // close the device handle // if (NULL != m_Line.hCommLink) { CALL_STATE eCallState = m_Line.eCallState; m_Line.eCallState = CALL_STATE_IDLE; // // If this call is connected it is not using the comm thread // if (eCallState != CALL_STATE_CONNECTED) { m_fCommPortInUse = FALSE; } // // restore comm timeouts // SetCommTimeouts(m_Line.hCommLink, &m_DefaultTimeouts); TRACE_OUT(("Closing device handle %x", m_Line.hCommLink)); ::CloseHandle(m_Line.hCommLink); m_Line.hCommLink = NULL; } } ////////////////////////////////////////////////////////////////////////////// // // FUNCTION: PSTNCallback // // DESCRIPTION: // // PSTN callback function, called by the PSTN driver with the resuts of // TPhysical operations // // See MCATTPRT.H for definitions of the parameters // ////////////////////////////////////////////////////////////////////////////// TPhysicalError CALLBACK TPhysDriverCallback(USHORT msg, ULONG lParam, void *userData) { TRACE_OUT(("NULLM_PSTNCallback")); TRACE_OUT(("Hit TPhysical callback, %x %lx %lx", msg, lParam, userData)); CNullModem *p = (CNullModem *) userData; BOOL fRet = ::PostMessage(p->GetHwnd(), msg, (WPARAM) p, lParam); ASSERT(fRet); return TPHYS_SUCCESS; } ////////////////////////////////////////////////////////////////////////////// // // FUNCTION:TPhysWndProc // // Window procedure used to decouple callback requests // ////////////////////////////////////////////////////////////////////////////// LRESULT CALLBACK TPhysWndProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) { CNullModem *p = (CNullModem *) wParam; if (uMsg >= WM_APP) { return p->TPhysProcessMessage(uMsg, lParam); } return ::DefWindowProc(hwnd, uMsg, wParam, lParam); } LRESULT CNullModem::TPhysProcessMessage(UINT uMsg, LPARAM lParam) { USHORT rc; USHORT reason; ULONG status; LINE_INFO *pLine; if (WM_TPHYS_STATUS_INDICATION == uMsg) { return 0; } ASSERT(lParam == (LPARAM) m_Line.pstnHandle); switch (uMsg) { case WM_TPHYS_CONNECT_INDICATION: TRACE_OUT(("got a WM_TPHYS_CONNECT_INDICATION")); m_Line.connInfo.resultCode = TPHYS_RESULT_INUSE; m_Line.eCallState = CALL_STATE_CONNECTED; if (NULL != m_pfnCallback) { (*m_pfnCallback)(WM_TPHYS_CONNECT_CONFIRM, &m_Line.connInfo, m_nTransportID); } break; case WM_TPHYS_CONNECT_CONFIRM: // // The transport has connected OK! We will take the line // back when the transport pulls DTR down // TRACE_OUT(("got a WM_TPHYS_CONNECT_CONFIRM")); m_Line.connInfo.resultCode = TPHYS_RESULT_SUCCESS_ALTERNATE; m_Line.eCallState = CALL_STATE_CONNECTED; if (NULL != m_pfnCallback) { (*m_pfnCallback)(WM_TPHYS_CONNECT_CONFIRM, &m_Line.connInfo, m_nTransportID); } break; case WM_TPHYS_DISCONNECT_INDICATION: case WM_TPHYS_DISCONNECT_CONFIRM: // // If the disconnect is the result of a failed connect request // then tell the NC of the failure (Otherwise it is a // successful disconnect) // if (WM_TPHYS_DISCONNECT_INDICATION == uMsg) { TRACE_OUT(("WM_TPHYS_DISCONNECT_INDICATION, %ld", lParam)); } else { TRACE_OUT(("WM_TPHYS_DISCONNECT_CONFIRM, %ld", lParam)); } if ((m_Line.eCallState == CALL_STATE_MAKE) || (m_Line.eCallState == CALL_STATE_ANSWER) || (m_Line.eCallState == CALL_STATE_CONNECTED)) { TRACE_OUT(( "T120 connection attempt has failed")); if (m_Line.fCaller) { m_Line.connInfo.resultCode = TPHYS_RESULT_CONNECT_FAILED; if (NULL != m_pfnCallback) { (*m_pfnCallback)(WM_TPHYS_CONNECT_CONFIRM, &m_Line.connInfo, m_nTransportID); } } DropCall(); } else { // // The transport has disconnected OK. We can take the line // back as soon as the unlisten returns // TRACE_OUT(("T120 has disconnected - unlistening")); if (! m_Line.fCaller) { // g_lpfnPTPhysicalUnlisten(m_Line.pstnHandle); } else { // // We only drop the call for outgoing requests - leave // incoming calls to drop when the line goes down // DropCall(); } } break; } return 0; } ////////////////////////////////////////////////////////////////////////////// // SetConnectedPort // // Waits for comm port changes ////////////////////////////////////////////////////////////////////////////// void CNullModem::SetConnectedPort(void) { DCB dcb; TRACE_OUT(("SetConnectedPort")); ::ZeroMemory(&dcb, sizeof(dcb)); // // Set comm mask and state // ::SetCommMask(m_Line.hCommLink, 0); // RLSD changed state ::GetCommState(m_Line.hCommLink, &dcb); dcb.DCBlength = sizeof(DCB); dcb.fBinary = 1; // binary mode, no EOF check dcb.fOutxDsrFlow = 0; // DSR output flow control dcb.fDsrSensitivity = 0; // DSR sensitivity dcb.fTXContinueOnXoff = 0; // XOFF continues Tx dcb.fOutX = 0; // XON/XOFF out flow control dcb.fInX = 0; // XON/XOFF in flow control dcb.fErrorChar = 0; // enable error replacement dcb.fNull = 0; // enable null stripping dcb.XonLim = 0; // transmit XON threshold dcb.XoffLim = 0; // transmit XOFF threshold ::SetCommState(m_Line.hCommLink, &dcb); } ////////////////////////////////////////////////////////////////////////////// // WaitForConnection // // Waits for comm port changes ////////////////////////////////////////////////////////////////////////////// BOOL CNullModem::WaitForConnection(void) { BOOL fRet = FALSE; TRACE_OUT(("WaitForConnection")); while (TRUE) { DWORD dwWait = ::WaitForSingleObject(m_hevtOverlapped, COMM_PORT_TIMEOUT); TRACE_OUT(("WaitForConnection: WaitForSingleObject returns %d", dwWait)); ::ResetEvent(m_hevtOverlapped); if (dwWait == WAIT_ABANDONED || dwWait == WAIT_TIMEOUT || dwWait == WAIT_FAILED) { DropCall(); m_fCommPortInUse = FALSE; ERROR_OUT(("WaitForConnection: Unable to WaitCommEvent: error = %d", ::GetLastError())); goto Failure; } ASSERT(m_hevtOverlapped == m_Line.hevtCall); if (CALL_STATE_MAKE != m_Line.eCallState && CALL_STATE_ANSWER != m_Line.eCallState) { ERROR_OUT(("WaitForConnection: Got a bad event = %d", m_hevtOverlapped)); goto Failure; } TRACE_OUT(("WaitForConnection: m_dwEventMask = %d", m_dwEventMask)); switch (m_Line.eCallState) { case CALL_STATE_MAKE: { // // The other side was connected and cleared DTR // if (m_dwEventMask & (EV_RXCHAR)) { ::EscapeCommFunction(m_Line.hCommLink, CLRDTR); ::EscapeCommFunction(m_Line.hCommLink, SETDTR); SetConnectedPort(); m_Line.fCaller = FALSE; goto Success; } // // The other side just connected // else if(m_dwEventMask & (EV_DSR | EV_RLSD | EV_CTS)) { // // Change the state of this connection so we dont get here again // m_Line.eCallState = CALL_STATE_ANSWER; // // Wait sometime so the other side can transition to the wait state // ::Sleep(2000); // // Tell the other side we connected before // ::EscapeCommFunction(m_Line.hCommLink, SETBREAK); ::ZeroMemory(&m_Overlapped, sizeof(m_Overlapped)); m_Overlapped.hEvent = m_Line.hevtCall; m_dwEventMask = 0; ::ResetEvent(m_Overlapped.hEvent); if (! ::WaitCommEvent(m_Line.hCommLink, &m_dwEventMask, &m_Overlapped)) { DWORD dwErr = ::GetLastError(); if (ERROR_IO_PENDING != dwErr) { ERROR_OUT(("TPhysConnectRequest: Unable to WaitCommEvent: error = %d", dwErr)); DropCall(); goto Failure; } } } } break; case CALL_STATE_ANSWER: { ::EscapeCommFunction(m_Line.hCommLink, CLRBREAK); SetConnectedPort(); goto Success; } break; } } // while Success: fRet = TRUE; Failure: return fRet; } ////////////////////////////////////////////////////////////////////////////// // COMMThread // // Waits for comm port changes ////////////////////////////////////////////////////////////////////////////// DWORD __stdcall TPhysWorkerThreadProc(void *lParam) { return ((CNullModem *) lParam)->WorkerThreadProc(); } DWORD CNullModem::WorkerThreadProc(void) { ULONG rc = 0; ULONG dwResult; TRACE_OUT(("TPhysWorkerThreadProc")); while (m_fCommPortInUse) { // // Wait for connection to happen // if (WaitForConnection()) { SetBuffers(); SetTimeouts(); // // Call T120 physicall request // if (m_Line.fCaller) { // rc = g_lpfnPTPhysicalConnectRequest(0, // CALL_CONTROL_MANUAL // &m_Line.hCommLink, NULL, &m_Line.pstnHandle); } else { m_Line.connInfo.connectionID = m_nConnectionID; m_Line.connInfo.resultCode = TPHYS_SUCCESS; if (NULL != m_pfnCallback) { (*m_pfnCallback)(WM_TPHYS_CONNECT_INDICATION, &m_Line.connInfo, m_nTransportID); } // rc = g_lpfnPTPhysicalListen(0, // CALL_CONTROL_MANUAL // &m_Line.hCommLink, NULL, &m_Line.pstnHandle); } if (rc != 0) { TRACE_OUT(( "Failed COMM connect, rc %d",rc)); m_Line.connInfo.resultCode = TPHYS_RESULT_CONNECT_FAILED; if (NULL != m_pfnCallback) { (*m_pfnCallback)(WM_TPHYS_CONNECT_CONFIRM, &m_Line.connInfo, m_nTransportID); } DropCall(); } else { m_Line.eCallState = CALL_STATE_CONNECTED; m_Line.connInfo.resultCode = TPHYS_SUCCESS; // // This comm port doesn't need the thread anymore // m_fCommPortInUse = FALSE; } } else { TRACE_OUT(( "Failed COMM connect, rc %d",rc)); m_Line.connInfo.resultCode = TPHYS_RESULT_CONNECT_FAILED; if (NULL != m_pfnCallback) { (*m_pfnCallback)(WM_TPHYS_CONNECT_CONFIRM, &m_Line.connInfo, m_nTransportID); } // // Something went wrong in the wait, get out of the loop // break; } } // // We are going down. // if (NULL != m_hThread) { ::CloseHandle(m_hThread); m_hThread = NULL; } return 0; } void CNullModem::SetBuffers(void) { BOOL fRet = ::SetupComm(m_Line.hCommLink, /* rx */ 10240, /* tx */ 1024); ASSERT(fRet); } void CNullModem::SetTimeouts(void) { COMMTIMEOUTS com_timeouts; ::ZeroMemory(&com_timeouts, sizeof(com_timeouts)); com_timeouts.ReadIntervalTimeout = 10; com_timeouts.ReadTotalTimeoutMultiplier = 0; com_timeouts.ReadTotalTimeoutConstant = 100; com_timeouts.WriteTotalTimeoutMultiplier = 0; com_timeouts.WriteTotalTimeoutConstant = 10000; BOOL fRet = ::SetCommTimeouts(m_Line.hCommLink, &com_timeouts); ASSERT(fRet); }