windows-nt/Source/XPSP1/NT/drivers/sac/uterm/common/serial/serial.c
2020-09-26 16:20:57 +08:00

1076 lines
23 KiB
C

#include "std.h"
// This is the communications mask used by our serial port. More may be
// necessary, but for right now, this seems to work.
#define EV_SERIAL EV_RXCHAR | EV_ERR | EV_BREAK
#define SERIALPORT_NAME L"Serial Port"
// This GUID is used to identify objects opened by this library. It is
// placed in the m_Secret member of the SERIALPORT structure. Any external
// interface accepting a SERIALPORT object as a parameter should check this
// out before using the structure.
static const GUID uuidSerialPortObjectGuid =
{ 0x86ae9c9b, 0x9444, 0x4d00, { 0x84, 0xbb, 0xc1, 0xd9, 0xc2, 0xd9, 0xfb, 0xf3 } };
// Structure defining an open serial port object. All external users of this
// library will only have a void pointer to one of these, and the structure is
// not published anywhere. This abstration makes it more difficult for the
// user to mess things up.
typedef struct __SERIALPORT
{
GUID m_Secret; // Identifies this as a serial port
HANDLE m_hPort; // Handle to the opened serial port
HANDLE m_hAbort; // Event signalled when port is closing
HANDLE m_hReadMutex; // Only one thread allowed to read a port
HANDLE m_hWriteMutex; // Only one thread allowed to read a port
HANDLE m_hCloseMutex; // Only one thread allowed to close a port
HANDLE m_hReadComplete; // Event to signal read completion
HANDLE m_hWriteComplete; // Event to signal write completion
} SERIALPORT, *PSERIALPORT;
extern PVOID APIENTRY lhcOpen(
PCWSTR pcszPortSpec);
extern BOOL APIENTRY lhcRead(
PVOID pObject,
PVOID pBuffer,
DWORD dwSize,
PDWORD pdwBytesRead);
extern BOOL APIENTRY lhcWrite(
PVOID pObject,
PVOID pBuffer,
DWORD dwSize);
extern BOOL APIENTRY lhcClose(
PVOID pObject);
extern DWORD APIENTRY lhcGetLibraryName(
PWSTR pszBuffer,
DWORD dwSize);
BOOL lhcpAcquireWithAbort(
HANDLE hMutex,
HANDLE hAbort);
BOOL lhcpAcquireReadWithAbort(
PSERIALPORT pObject);
BOOL lhcpAcquireWriteWithAbort(
PSERIALPORT pObject);
BOOL lhcpAcquireCloseWithAbort(
PSERIALPORT pObject);
BOOL lhcpAcquireReadAndWrite(
PSERIALPORT pObject);
BOOL lhcpReleaseRead(
PSERIALPORT pObject);
BOOL lhcpReleaseWrite(
PSERIALPORT pObject);
BOOL lhcpReleaseClose(
PSERIALPORT pObject);
BOOL lhcpIsValidObject(
PSERIALPORT pObject);
PSERIALPORT lhcpCreateNewObject();
void lhcpDeleteObject(
PSERIALPORT pObject);
BOOL lhcpParseParameters(
PCWSTR pcszPortSpec,
PWSTR* pszPort,
PDWORD pdwBaudRate);
void lhcpParseParametersFree(
PWSTR* pszPort,
PDWORD pdwBaudRate);
BOOL lhcpSetCommState(
HANDLE hPort,
DWORD dwBaudRate);
BOOL lhcpWaitForCommEvent(
PSERIALPORT pObject,
PDWORD pdwEventMask);
BOOL lhcpReadCommPort(
PSERIALPORT pObject,
PVOID pBuffer,
DWORD dwSize,
PDWORD pdwBytesRead);
BOOL lhcpWriteCommPort(
PSERIALPORT pObject,
PVOID pBuffer,
DWORD dwSize);
BOOL lhcpAcquireWithAbort(HANDLE hMutex, HANDLE hAbort)
{
HANDLE hWaiters[2];
DWORD dwWaitResult;
hWaiters[0] = hAbort;
hWaiters[1] = hMutex;
// We should honour the m_hAbort event, since this is signalled when the
// port is closed by another thread
dwWaitResult = WaitForMultipleObjects(
2,
hWaiters,
FALSE,
INFINITE);
if (WAIT_OBJECT_0==dwWaitResult)
{
goto Error;
}
else if ((WAIT_OBJECT_0+1)!=dwWaitResult)
{
// This should never, ever happen - so I will put a debug breapoint
// in here (checked only).
#ifdef DBG
DebugBreak();
#endif
goto Error;
}
return TRUE; // We have acquired the write mutex
Error:
return FALSE; // We have aborted
}
BOOL lhcpAcquireReadWithAbort(PSERIALPORT pObject)
{
return lhcpAcquireWithAbort(
pObject->m_hReadMutex,
pObject->m_hAbort);
}
BOOL lhcpAcquireWriteWithAbort(PSERIALPORT pObject)
{
return lhcpAcquireWithAbort(
pObject->m_hWriteMutex,
pObject->m_hAbort);
}
BOOL lhcpAcquireCloseWithAbort(PSERIALPORT pObject)
{
return lhcpAcquireWithAbort(
pObject->m_hCloseMutex,
pObject->m_hAbort);
}
BOOL lhcpAcquireReadAndWrite(PSERIALPORT pObject)
{
HANDLE hWaiters[2];
DWORD dwWaitResult;
hWaiters[0] = pObject->m_hReadMutex;
hWaiters[1] = pObject->m_hWriteMutex;
dwWaitResult = WaitForMultipleObjects(
2,
hWaiters,
TRUE,
1000); // Timeout after 1 second
if (WAIT_OBJECT_0!=dwWaitResult)
{
goto Error;
}
return TRUE; // We have acquired the write mutex
Error:
return FALSE; // We have aborted
}
BOOL lhcpReleaseRead(PSERIALPORT pObject)
{
return ReleaseMutex(
pObject->m_hReadMutex);
}
BOOL lhcpReleaseWrite(PSERIALPORT pObject)
{
return ReleaseMutex(
pObject->m_hWriteMutex);
}
BOOL lhcpReleaseClose(PSERIALPORT pObject)
{
return ReleaseMutex(
pObject->m_hCloseMutex);
}
BOOL lhcpIsValidObject(PSERIALPORT pObject)
{
BOOL bResult;
__try
{
bResult = IsEqualGUID(
&uuidSerialPortObjectGuid,
&pObject->m_Secret);
}
__except(EXCEPTION_EXECUTE_HANDLER)
{
SetLastError(
ERROR_INVALID_HANDLE);
bResult = FALSE;
goto Done;
}
Done:
return bResult;
}
PSERIALPORT lhcpCreateNewObject()
{
PSERIALPORT pObject = (PSERIALPORT)malloc(
sizeof(SERIALPORT));
pObject->m_Secret = uuidSerialPortObjectGuid;
pObject->m_hPort = INVALID_HANDLE_VALUE;
pObject->m_hAbort = NULL;
pObject->m_hReadMutex = NULL; // Only one thread allowed to read a port
pObject->m_hWriteMutex = NULL; // Only one thread allowed to read a port
pObject->m_hCloseMutex = NULL; // Only one thread allowed to read a port
pObject->m_hReadComplete = NULL; // Event to signal read completion
pObject->m_hWriteComplete = NULL; // Event to signal write completion
return pObject;
}
void lhcpDeleteObject(PSERIALPORT pObject)
{
if (pObject==NULL)
{
return;
}
ZeroMemory(
&(pObject->m_Secret),
sizeof(pObject->m_Secret));
if (pObject->m_hPort!=INVALID_HANDLE_VALUE)
{
CloseHandle(
pObject->m_hPort);
}
if (pObject->m_hAbort!=NULL)
{
CloseHandle(
pObject->m_hAbort);
}
if (pObject->m_hReadMutex!=NULL)
{
CloseHandle(
pObject->m_hReadMutex);
}
if (pObject->m_hWriteMutex!=NULL)
{
CloseHandle(
pObject->m_hWriteMutex);
}
if (pObject->m_hCloseMutex!=NULL)
{
CloseHandle(
pObject->m_hCloseMutex);
}
if (pObject->m_hReadComplete!=NULL)
{
CloseHandle(
pObject->m_hReadComplete);
}
if (pObject->m_hWriteComplete!=NULL)
{
CloseHandle(
pObject->m_hWriteComplete);
}
FillMemory(
pObject,
sizeof(SERIALPORT),
0x00);
free(
pObject);
}
BOOL lhcpParseParameters(PCWSTR pcszPortSpec, PWSTR* pszPort, PDWORD pdwBaudRate)
{
PWSTR pszSettings;
*pszPort = malloc(
(wcslen(pcszPortSpec) + 5) * sizeof(WCHAR));
if (NULL==*pszPort)
{
SetLastError(
ERROR_NOT_ENOUGH_MEMORY);
goto Error;
}
wcscpy(
*pszPort,
L"\\\\.\\"); // Append the device prefix to the port name
wcscat(
*pszPort,
pcszPortSpec);
pszSettings = wcschr( // Find where the settings start
*pszPort,
L'@');
if (NULL==pszSettings)
{
SetLastError(
ERROR_INVALID_PARAMETER);
goto Error;
}
*pszSettings++ = L'\0'; // Separate the strings
*pdwBaudRate = 0;
while (*pszSettings!=L'\0' && *pdwBaudRate<115200)
{
if (L'0'<=*pszSettings && *pszSettings<=L'9')
{
*pdwBaudRate *= 10;
*pdwBaudRate += *pszSettings - L'0';
pszSettings++;
}
else
{
break;
}
}
if (*pszSettings!=L'0' && *pdwBaudRate!=9600 && *pdwBaudRate!=19200 &&
*pdwBaudRate!=38400 && *pdwBaudRate!=57600 && *pdwBaudRate!=115200)
{
SetLastError(
ERROR_INVALID_PARAMETER);
goto Error;
}
return TRUE;
Error:
lhcpParseParametersFree(
pszPort, pdwBaudRate);
return FALSE;
}
void lhcpParseParametersFree(PWSTR* pszPort, PDWORD pdwBaudRate)
{
if (*pszPort != NULL)
{
free(*pszPort);
*pszPort = NULL;
}
*pdwBaudRate = 0;
}
BOOL lhcpSetCommState(HANDLE hPort, DWORD dwBaudRate)
{
DCB MyDCB;
COMMTIMEOUTS CommTimeouts;
BOOL bResult;
ZeroMemory(
&MyDCB,
sizeof(DCB));
MyDCB.DCBlength = sizeof(DCB);
MyDCB.BaudRate = dwBaudRate;
MyDCB.fBinary = 1;
MyDCB.fParity = 1;
MyDCB.fOutxCtsFlow = 0;
MyDCB.fOutxDsrFlow = 0;
MyDCB.fDtrControl = 1;
MyDCB.fDsrSensitivity = 0;
MyDCB.fTXContinueOnXoff = 1;
MyDCB.fOutX = 1;
MyDCB.fInX = 1;
MyDCB.fErrorChar = 0;
MyDCB.fNull = 0;
MyDCB.fRtsControl = 1;
MyDCB.fAbortOnError = 0;
MyDCB.XonLim = 0x50;
MyDCB.XoffLim = 0xc8;
MyDCB.ByteSize = 0x8;
MyDCB.Parity = 0;
MyDCB.StopBits = 0;
MyDCB.XonChar = 17;
MyDCB.XoffChar = 19;
MyDCB.ErrorChar = 0;
MyDCB.EofChar = 0;
MyDCB.EvtChar = 0;
bResult = SetCommState(
hPort,
&MyDCB);
if (!bResult)
{
goto Error;
}
CommTimeouts.ReadIntervalTimeout = 0xffffffff; //MAXDWORD
CommTimeouts.ReadTotalTimeoutMultiplier = 0x0; //MAXDWORD
CommTimeouts.ReadTotalTimeoutConstant = 0x0;
CommTimeouts.WriteTotalTimeoutMultiplier = 0;
CommTimeouts.WriteTotalTimeoutConstant = 0;
bResult = SetCommTimeouts(
hPort,
&CommTimeouts);
if (!bResult)
{
goto Error;
}
bResult = SetCommMask(
hPort,
EV_SERIAL);
if (!bResult)
{
goto Error;
}
return TRUE;
Error:
return FALSE;
}
BOOL lhcpWaitForCommEvent(PSERIALPORT pObject, PDWORD pdwEventMask)
{
OVERLAPPED Overlapped;
BOOL bResult;
HANDLE hWaiters[2];
DWORD dwWaitResult;
DWORD dwBytesTransferred;
// I have no idea whether this is necessary, so I will do it just to be
// on the safe side.
ZeroMemory(
&Overlapped,
sizeof(OVERLAPPED));
Overlapped.hEvent = pObject->m_hReadComplete;
// Start waiting for a comm event
bResult = WaitCommEvent(
pObject->m_hPort,
pdwEventMask,
&Overlapped);
if (!bResult && GetLastError()!=ERROR_IO_PENDING)
{
goto Error;
}
hWaiters[0] = pObject->m_hAbort;
hWaiters[1] = pObject->m_hReadComplete;
// Let's wait for the operation to complete. This will quit waiting if
// the m_hAbort event is signalled.
dwWaitResult = WaitForMultipleObjects(
2,
hWaiters,
FALSE,
INFINITE);
if (WAIT_OBJECT_0==dwWaitResult)
{
// The m_hAbort event was signalled. This means that Close was called
// on this serial port object. So let's cancel the pending IO.
CancelIo(
pObject->m_hPort);
// The serial port object is being closed, so let's call it invalid.
SetLastError(
ERROR_INVALID_HANDLE);
goto Error;
}
else if ((WAIT_OBJECT_0+1)!=dwWaitResult)
{
// This should never, ever happen - so I will put a debug breapoint
// in here (checked only).
#ifdef DBG
DebugBreak();
#endif
goto Error;
}
// Check the success or failure of the operation
bResult = GetOverlappedResult(
pObject->m_hPort,
&Overlapped,
&dwBytesTransferred,
TRUE);
if (!bResult)
{
goto Error;
}
return TRUE;
Error:
return FALSE;
}
BOOL lhcpReadCommPort(
PSERIALPORT pObject,
PVOID pBuffer,
DWORD dwSize,
PDWORD pdwBytesRead)
{
OVERLAPPED Overlapped;
BOOL bResult;
DWORD dwWaitResult;
HANDLE hWaiters[2];
// I have no idea whether this is necessary, so I will do it just to be
// on the safe side.
ZeroMemory(
&Overlapped,
sizeof(OVERLAPPED));
Overlapped.hEvent = pObject->m_hReadComplete;
// We can now read the comm port
bResult = ReadFile(
pObject->m_hPort,
pBuffer,
dwSize,
pdwBytesRead,
&Overlapped);
if (!bResult && GetLastError()!=ERROR_IO_PENDING)
{
goto Error;
}
hWaiters[0] = pObject->m_hAbort;
hWaiters[1] = pObject->m_hReadComplete;
// Let's wait for the operation to complete. This will quit waiting if
// the m_hAbort event is signalled.
dwWaitResult = WaitForMultipleObjects(
2,
hWaiters,
FALSE,
INFINITE);
if (WAIT_OBJECT_0==dwWaitResult)
{
// The m_hAbort event was signalled. This means that Close was called
// on this serial port object. So let's cancel the pending IO.
CancelIo(
pObject->m_hPort);
// The serial port object is being closed, so let's call it invalid.
SetLastError(
ERROR_INVALID_HANDLE);
goto Error;
}
else if ((WAIT_OBJECT_0+1)!=dwWaitResult)
{
// This should never, ever happen - so I will put a debug breapoint
// in here (checked only).
#ifdef DBG
DebugBreak();
#endif
goto Error;
}
// Check the success or failure of the read operation
bResult = GetOverlappedResult(
pObject->m_hPort,
&Overlapped,
pdwBytesRead,
TRUE);
if (!bResult)
{
goto Error;
}
return TRUE;
Error:
return FALSE;
}
BOOL lhcpWriteCommPort(
PSERIALPORT pObject,
PVOID pBuffer,
DWORD dwSize)
{
OVERLAPPED Overlapped;
BOOL bResult;
DWORD dwBytesWritten;
DWORD dwWaitResult;
HANDLE hWaiters[2];
// I have no idea whether this is necessary, so I will do it just to be
// on the safe side.
ZeroMemory(
&Overlapped,
sizeof(OVERLAPPED));
Overlapped.hEvent = pObject->m_hWriteComplete;
// We can now read the comm port
bResult = WriteFile(
pObject->m_hPort,
pBuffer,
dwSize,
&dwBytesWritten,
&Overlapped);
if (!bResult && GetLastError()!=ERROR_IO_PENDING)
{
goto Error;
}
hWaiters[0] = pObject->m_hAbort;
hWaiters[1] = pObject->m_hWriteComplete;
// Let's wait for the operation to complete. This will quit waiting if
// the m_hAbort event is signalled. If the read operation completed
// immediately, then this wait will succeed immediately.
dwWaitResult = WaitForMultipleObjects(
2,
hWaiters,
FALSE,
INFINITE);
if (WAIT_OBJECT_0==dwWaitResult)
{
// The m_hAbort event was signalled. This means that Close was called
// on this serial port object. So let's cancel the pending IO.
CancelIo(
pObject->m_hPort);
// The serial port object is being closed, so let's call it invalid.
SetLastError(
ERROR_INVALID_HANDLE);
goto Error;
}
else if ((WAIT_OBJECT_0+1)!=dwWaitResult)
{
// This should never, ever happen - so I will put a debug breapoint
// in here (checked only).
#ifdef DBG
DebugBreak();
#endif
goto Error;
}
// Check the success or failure of the write operation
bResult = GetOverlappedResult(
pObject->m_hPort,
&Overlapped,
&dwBytesWritten,
TRUE);
if (!bResult)
{
goto Error;
}
return TRUE;
Error:
return FALSE;
}
extern PVOID APIENTRY lhcOpen(PCWSTR pcszPortSpec)
{
BOOL bResult;
PWSTR pszPort;
DWORD dwBaudRate;
PSERIALPORT pObject = NULL;
DCB MyDCB;
bResult = lhcpParseParameters(
pcszPortSpec,
&pszPort,
&dwBaudRate);
if (!bResult)
{
goto Error;
}
// Allocate space and initialize the serial port object
pObject = lhcpCreateNewObject();
if (NULL==pObject)
{
goto Error;
}
// Open the serial port
pObject->m_hPort = CreateFileW(
pszPort,
GENERIC_ALL,
0,
NULL,
OPEN_EXISTING,
FILE_FLAG_OVERLAPPED,
NULL);
if (INVALID_HANDLE_VALUE==pObject->m_hPort)
{
goto Error;
}
// Set the properties of the serial port
bResult = lhcpSetCommState(
pObject->m_hPort,
dwBaudRate);
if (!bResult)
{
goto Error;
}
// This event will be set when we want to close the port
pObject->m_hAbort = CreateEvent(
NULL,
TRUE,
FALSE,
NULL);
if (NULL==pObject->m_hAbort)
{
goto Error;
}
// This event will be used for overlapped reading from the port
pObject->m_hReadComplete = CreateEvent(
NULL,
TRUE,
FALSE,
NULL);
if (NULL==pObject->m_hReadComplete)
{
goto Error;
}
// This event will be used for overlapped writing to the port
pObject->m_hWriteComplete = CreateEvent(
NULL,
TRUE,
FALSE,
NULL);
if (NULL==pObject->m_hWriteComplete)
{
goto Error;
}
// This mutex will ensure that only one thread can read at a time
pObject->m_hReadMutex = CreateMutex(
NULL,
FALSE,
NULL);
if (NULL==pObject->m_hReadMutex)
{
goto Error;
}
// This mutex will ensure that only one thread can write at a time
pObject->m_hWriteMutex = CreateMutex(
NULL,
FALSE,
NULL);
if (NULL==pObject->m_hWriteMutex)
{
goto Error;
}
// This mutex will ensure that only one thread can close the port
pObject->m_hCloseMutex = CreateMutex(
NULL,
FALSE,
NULL);
if (NULL==pObject->m_hCloseMutex)
{
goto Error;
}
// Free up the temporary memory used to parse the parameters
lhcpParseParametersFree(
&pszPort, &dwBaudRate);
// Return a pointer to the new object
return pObject;
Error:
lhcpParseParametersFree(
&pszPort, &dwBaudRate);
lhcpDeleteObject(
pObject);
return NULL;
}
extern BOOL APIENTRY lhcRead(
PVOID pObject,
PVOID pBuffer,
DWORD dwSize,
PDWORD pdwBytesRead)
{
OVERLAPPED Overlapped;
DWORD dwEventMask;
BOOL bResult;
// Firstly, we need to check whether the pointer that got passed in
// points to a valid SERIALPORT object
if (!lhcpIsValidObject(pObject))
{
goto NoMutex;
}
bResult = lhcpAcquireReadWithAbort(
(PSERIALPORT)pObject);
if (!bResult)
{
SetLastError(
ERROR_INVALID_HANDLE);
goto NoMutex;
}
// Wait for something to happen to the serial port
bResult = lhcpWaitForCommEvent(
(PSERIALPORT)pObject, &dwEventMask);
if (!bResult)
{
goto Error;
}
// We should now have a valid serial port event, so let's read the port.
bResult = lhcpReadCommPort(
(PSERIALPORT)pObject,
pBuffer,
dwSize,
pdwBytesRead);
if (!bResult)
{
goto Error;
}
lhcpReleaseRead(
(PSERIALPORT)pObject);
return TRUE;
Error:
lhcpReleaseRead(
(PSERIALPORT)pObject);
NoMutex:
return FALSE;
}
extern BOOL APIENTRY lhcWrite(
PVOID pObject,
PVOID pBuffer,
DWORD dwSize)
{
OVERLAPPED Overlapped;
BOOL bResult;
// Firstly, we need to check whether the pointer that got passed in
// points to a valid SERIALPORT object
if (!lhcpIsValidObject(pObject))
{
goto NoMutex;
}
// Block until it is your turn
bResult = lhcpAcquireWriteWithAbort(
pObject);
if (!bResult)
{
SetLastError(
ERROR_INVALID_HANDLE);
goto NoMutex;
}
// Wait for something to happen to the serial port
bResult = lhcpWriteCommPort(
(PSERIALPORT)pObject,
pBuffer,
dwSize);
if (!bResult)
{
goto Error;
}
lhcpReleaseWrite(
(PSERIALPORT)pObject);
return TRUE;
Error:
lhcpReleaseWrite(
(PSERIALPORT)pObject);
NoMutex:
return FALSE;
}
extern BOOL APIENTRY lhcClose(PVOID pObject)
{
BOOL bResult;
// Firstly, we need to check whether the pointer that got passed in
// points to a valid SERIALPORT object
if (!lhcpIsValidObject(pObject))
{
goto Error;
}
// We need to ensure that we are the only thread closing this object
bResult = lhcpAcquireCloseWithAbort(
pObject);
if (!bResult)
{
SetLastError(
ERROR_INVALID_HANDLE);
goto NoMutex;
}
// Signal everyone to quit doing what they're doing. Any new threads
// calling lhcRead and lhcWrite will be immediately sent packing, since
// the m_hAbort event is waited on along with the relevant mutex.
bResult = SetEvent(
((PSERIALPORT)pObject)->m_hAbort);
if (!bResult)
{
goto Error;
}
// Now acquire the read and write mutexes so that no-one else will try to
// access this object to read or write. Abort does not apply, since we
// have already signalled it. We know that we are closing, and we need
// the read and write mutexes.
bResult = lhcpAcquireReadAndWrite(
(PSERIALPORT)pObject);
if (!bResult)
{
SetLastError(
ERROR_INVALID_HANDLE);
goto Error;
}
// Closes all of the open handles, erases the secret and frees up the
// memory associated with the object. We can close the mutex objects,
// even though we are the owners, since we can guarantee that no-one
// else is waiting on them. The m_hAbort event being signalled will
// ensure this.
lhcpDeleteObject(
(PSERIALPORT)pObject);
return TRUE;
Error:
lhcpReleaseClose(
(PSERIALPORT)pObject);
NoMutex:
return FALSE;
}
extern DWORD APIENTRY lhcGetLibraryName(
PWSTR pszBuffer,
DWORD dwSize)
{
DWORD dwNameSize = wcslen(SERIALPORT_NAME)+1;
// If zero is passed in as the buffer length, we will return the
// required buffer size in characters, as calulated above. If the
// incoming buffer size is not zero, and smaller than the required
// buffer size, we return 0 (failure) with a valid error code. Notice
// that in the case where the incoming size is zero, we don't touch
// the buffer pointer at all.
if (dwSize!=0 && dwSize < dwNameSize)
{
SetLastError(
ERROR_INSUFFICIENT_BUFFER);
dwNameSize = 0;
}
else
{
wcscpy(
pszBuffer,
SERIALPORT_NAME);
}
return dwNameSize;
}