You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
756 lines
18 KiB
756 lines
18 KiB
//+-------------------------------------------------------------------------
|
|
//
|
|
// Microsoft Windows
|
|
//
|
|
// Copyright (C) SCM Microsystems, 1998 - 1999
|
|
//
|
|
// File: serialnt.c
|
|
//
|
|
//--------------------------------------------------------------------------
|
|
|
|
#include "drivernt.h"
|
|
#include "stc.h"
|
|
#include "cbhndlr.h"
|
|
#include "drvnt5.h"
|
|
|
|
const ULONG ConfigTable[] =
|
|
{
|
|
IOCTL_SERIAL_SET_BAUD_RATE,
|
|
IOCTL_SERIAL_SET_LINE_CONTROL,
|
|
IOCTL_SERIAL_SET_CHARS,
|
|
IOCTL_SERIAL_SET_TIMEOUTS,
|
|
IOCTL_SERIAL_SET_HANDFLOW,
|
|
#if !defined( __NT4__ )
|
|
IOCTL_SERIAL_PURGE,
|
|
#endif
|
|
IOCTL_SERIAL_SET_BREAK_OFF,
|
|
IOCTL_SERIAL_SET_WAIT_MASK,
|
|
0
|
|
};
|
|
|
|
NTSTATUS
|
|
IFInitializeInterface(
|
|
PREADER_EXTENSION ReaderExtension,
|
|
PVOID ConfigData
|
|
)
|
|
/*++
|
|
|
|
Routine Description:
|
|
|
|
Arguments:
|
|
|
|
Return Value:
|
|
|
|
--*/
|
|
{
|
|
NTSTATUS NTStatus = STATUS_SUCCESS;
|
|
ULONG OutDataLen;
|
|
PVOID OutData;
|
|
PULONG ActIoctl;
|
|
PSERIAL_PORT_CONFIG SerialPortConfig = (PSERIAL_PORT_CONFIG) ConfigData;
|
|
//
|
|
// set all parameters defined in config table
|
|
//
|
|
|
|
ActIoctl = (PULONG) ConfigTable;
|
|
do
|
|
{
|
|
switch( *ActIoctl )
|
|
{
|
|
case IOCTL_SERIAL_SET_BAUD_RATE:
|
|
OutData = &SerialPortConfig->BaudRate;
|
|
OutDataLen = sizeof( SERIAL_BAUD_RATE );
|
|
break;
|
|
|
|
case IOCTL_SERIAL_SET_LINE_CONTROL:
|
|
OutData = &SerialPortConfig->LineControl;
|
|
OutDataLen = sizeof( SERIAL_LINE_CONTROL );
|
|
break;
|
|
|
|
case IOCTL_SERIAL_SET_CHARS:
|
|
OutData = &SerialPortConfig->SerialChars;
|
|
OutDataLen = sizeof( SERIAL_CHARS );
|
|
break;
|
|
|
|
case IOCTL_SERIAL_SET_TIMEOUTS:
|
|
OutData = &SerialPortConfig->Timeouts;
|
|
OutDataLen = sizeof( SERIAL_TIMEOUTS );
|
|
break;
|
|
|
|
case IOCTL_SERIAL_SET_HANDFLOW:
|
|
OutData = &SerialPortConfig->HandFlow;
|
|
OutDataLen = sizeof( SERIAL_HANDFLOW );
|
|
break;
|
|
|
|
case IOCTL_SERIAL_SET_WAIT_MASK:
|
|
OutData = &SerialPortConfig->WaitMask;
|
|
OutDataLen = sizeof( ULONG );
|
|
break;
|
|
|
|
case IOCTL_SERIAL_PURGE:
|
|
OutData = &SerialPortConfig->Purge;
|
|
OutDataLen = sizeof( ULONG );
|
|
break;
|
|
|
|
case IOCTL_SERIAL_SET_BREAK_OFF:
|
|
OutData = NULL;
|
|
OutDataLen = 0;
|
|
break;
|
|
}
|
|
|
|
NTStatus = IFSerialIoctl(
|
|
ReaderExtension,
|
|
*ActIoctl,
|
|
OutData,
|
|
OutDataLen,
|
|
NULL,
|
|
0
|
|
);
|
|
|
|
SysDelay(25);
|
|
|
|
} while( *(++ActIoctl) && ( NTStatus == STATUS_SUCCESS ));
|
|
|
|
if( NTStatus == STATUS_SUCCESS )
|
|
{
|
|
// initialize the read thread
|
|
NTStatus = IFSerialWaitOnMask( ReaderExtension );
|
|
}
|
|
|
|
return( NTStatus );
|
|
}
|
|
|
|
NTSTATUS
|
|
IFWrite(
|
|
PREADER_EXTENSION ReaderExtension,
|
|
PUCHAR OutData,
|
|
ULONG OutDataLen
|
|
)
|
|
/*++
|
|
|
|
Routine Description:
|
|
|
|
Arguments:
|
|
|
|
Return Value:
|
|
|
|
--*/
|
|
{
|
|
NTSTATUS NTStatus = STATUS_INSUFFICIENT_RESOURCES;
|
|
IO_STATUS_BLOCK IoStatus;
|
|
KEVENT Event;
|
|
PIRP Irp;
|
|
PIO_STACK_LOCATION IrpStack;
|
|
|
|
if (KeReadStateEvent(&(ReaderExtension->SerialCloseDone))) {
|
|
|
|
//
|
|
// we have no connection to serial, fail the call
|
|
// this could be the case if the reader was removed
|
|
// during stand by / hibernation
|
|
//
|
|
return STATUS_UNSUCCESSFUL;
|
|
}
|
|
ReaderExtension->Available = 0;
|
|
KeInitializeEvent( &Event, NotificationEvent, FALSE );
|
|
|
|
//
|
|
// build irp to be send to serial driver
|
|
//
|
|
Irp = IoBuildDeviceIoControlRequest(
|
|
SERIAL_WRITE,
|
|
ReaderExtension->SerialDeviceObject,
|
|
OutData,
|
|
OutDataLen,
|
|
NULL,
|
|
0,
|
|
FALSE,
|
|
&Event,
|
|
&IoStatus
|
|
);
|
|
|
|
if( Irp != NULL )
|
|
{
|
|
IrpStack = IoGetNextIrpStackLocation( Irp );
|
|
|
|
IrpStack->MajorFunction = IRP_MJ_WRITE;
|
|
IrpStack->Parameters.Write.Length = OutDataLen;
|
|
IrpStack->Parameters.Write.ByteOffset.QuadPart = 0;
|
|
|
|
NTStatus = IoCallDriver( ReaderExtension->SerialDeviceObject, Irp );
|
|
|
|
if( NTStatus == STATUS_PENDING )
|
|
{
|
|
KeWaitForSingleObject(
|
|
&Event,
|
|
Executive,
|
|
KernelMode,
|
|
FALSE,
|
|
NULL
|
|
);
|
|
NTStatus = IoStatus.Status;
|
|
}
|
|
}
|
|
return( NTStatus );
|
|
}
|
|
|
|
NTSTATUS
|
|
IFRead(
|
|
PREADER_EXTENSION ReaderExtension,
|
|
PUCHAR InData,
|
|
ULONG InDataLen
|
|
)
|
|
/*++
|
|
|
|
Routine Description:
|
|
|
|
Arguments:
|
|
|
|
Return Value:
|
|
--*/
|
|
{
|
|
NTSTATUS NTStatus = STATUS_UNSUCCESSFUL;
|
|
KIRQL CurrentIrql;
|
|
|
|
// acquire spinlock to protect buffer/flag manipulation
|
|
KeAcquireSpinLock( &ReaderExtension->ReadSpinLock, &CurrentIrql );
|
|
|
|
// check if data already available
|
|
if( ReaderExtension->Available >= InDataLen )
|
|
{
|
|
|
|
NTStatus = STATUS_SUCCESS;
|
|
|
|
}
|
|
else
|
|
{
|
|
LARGE_INTEGER Timeout;
|
|
|
|
// setup read thread
|
|
ReaderExtension->Expected = InDataLen;
|
|
KeClearEvent( &ReaderExtension->DataAvailable );
|
|
|
|
KeReleaseSpinLock( &ReaderExtension->ReadSpinLock, CurrentIrql );
|
|
|
|
// setup wait time (in 100 ns)
|
|
Timeout.QuadPart =
|
|
(LONGLONG) ReaderExtension->ReadTimeout * -10L * 1000;
|
|
|
|
NTStatus = KeWaitForSingleObject(
|
|
&ReaderExtension->DataAvailable,
|
|
Executive,
|
|
KernelMode,
|
|
FALSE,
|
|
&Timeout
|
|
);
|
|
|
|
KeAcquireSpinLock(&ReaderExtension->ReadSpinLock, &CurrentIrql);
|
|
|
|
// reset the read queue
|
|
KeClearEvent(&ReaderExtension->DataAvailable);
|
|
}
|
|
|
|
if( NTStatus == STATUS_SUCCESS )
|
|
{
|
|
|
|
if (ReaderExtension->Available >= InDataLen) {
|
|
|
|
SysCopyMemory(
|
|
InData,
|
|
&ReaderExtension->TPDUStack[0],
|
|
InDataLen
|
|
);
|
|
|
|
ReaderExtension->Available -= InDataLen;
|
|
|
|
SysCopyMemory(
|
|
&ReaderExtension->TPDUStack[ 0 ],
|
|
&ReaderExtension->TPDUStack[ InDataLen ],
|
|
ReaderExtension->Available
|
|
);
|
|
|
|
} else {
|
|
|
|
//
|
|
// oops, that should not happen.
|
|
// InDataLen should not be bigger than
|
|
// the number of bytes available
|
|
//
|
|
|
|
ASSERT(FALSE);
|
|
NTStatus = STATUS_IO_TIMEOUT;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// ReaderExtension->Available = 0;;
|
|
NTStatus = STATUS_IO_TIMEOUT;
|
|
}
|
|
|
|
if( NTStatus != STATUS_SUCCESS )
|
|
{
|
|
|
|
NTStatus = STATUS_IO_TIMEOUT;
|
|
}
|
|
|
|
ReaderExtension->Expected = 0;
|
|
|
|
KeReleaseSpinLock( &ReaderExtension->ReadSpinLock, CurrentIrql );
|
|
|
|
return( NTStatus );
|
|
}
|
|
|
|
NTSTATUS
|
|
IFSerialIoctl(
|
|
PREADER_EXTENSION ReaderExtension,
|
|
ULONG IoctlCode,
|
|
PVOID OutData,
|
|
ULONG OutDataLen,
|
|
PVOID InData,
|
|
ULONG InDataLen
|
|
)
|
|
/*++
|
|
|
|
Routine Description:
|
|
|
|
Arguments:
|
|
|
|
Return Value:
|
|
|
|
--*/
|
|
{
|
|
NTSTATUS NTStatus = STATUS_INSUFFICIENT_RESOURCES;
|
|
IO_STATUS_BLOCK IoStatus;
|
|
KEVENT Event;
|
|
PIRP Irp;
|
|
|
|
|
|
|
|
if (KeReadStateEvent(&(ReaderExtension->SerialCloseDone))) {
|
|
|
|
//
|
|
// we have no connection to serial, fail the call
|
|
// this could be the case if the reader was removed
|
|
// during stand by / hibernation
|
|
//
|
|
return STATUS_UNSUCCESSFUL;
|
|
}
|
|
|
|
KeInitializeEvent( &Event, NotificationEvent, FALSE );
|
|
//
|
|
// build irp to be send to serial driver
|
|
//
|
|
Irp = IoBuildDeviceIoControlRequest(
|
|
IoctlCode,
|
|
ReaderExtension->SerialDeviceObject,
|
|
OutData,
|
|
OutDataLen,
|
|
InData,
|
|
InDataLen,
|
|
FALSE,
|
|
&Event,
|
|
&IoStatus
|
|
);
|
|
|
|
if( Irp != NULL )
|
|
{
|
|
NTStatus = IoCallDriver( ReaderExtension->SerialDeviceObject, Irp );
|
|
|
|
if( NTStatus == STATUS_PENDING )
|
|
{
|
|
LARGE_INTEGER Timeout;
|
|
|
|
Timeout.QuadPart =
|
|
(LONGLONG) ReaderExtension->ReadTimeout * -10 * 1000;
|
|
|
|
KeWaitForSingleObject(
|
|
&Event,
|
|
Executive,
|
|
KernelMode,
|
|
FALSE,
|
|
&Timeout
|
|
);
|
|
|
|
NTStatus = IoStatus.Status;
|
|
}
|
|
}
|
|
|
|
return( NTStatus );
|
|
}
|
|
|
|
|
|
|
|
NTSTATUS
|
|
IFSerialRead(
|
|
PREADER_EXTENSION ReaderExtension,
|
|
PUCHAR InData,
|
|
ULONG InDataLen
|
|
)
|
|
{
|
|
NTSTATUS NTStatus = STATUS_INSUFFICIENT_RESOURCES;
|
|
IO_STATUS_BLOCK IoStatus;
|
|
KEVENT Event;
|
|
PIRP Irp;
|
|
PIO_STACK_LOCATION IrpStack;
|
|
|
|
if (KeReadStateEvent(&(ReaderExtension->SerialCloseDone))) {
|
|
|
|
//
|
|
// we have no connection to serial, fail the call
|
|
// this could be the case if the reader was removed
|
|
// during stand by / hibernation
|
|
//
|
|
return STATUS_UNSUCCESSFUL;
|
|
}
|
|
// build irp to be send to serial driver
|
|
KeInitializeEvent( &Event, NotificationEvent, FALSE );
|
|
|
|
Irp = IoBuildDeviceIoControlRequest(
|
|
SERIAL_READ,
|
|
ReaderExtension->SerialDeviceObject,
|
|
NULL,
|
|
0,
|
|
InData,
|
|
InDataLen,
|
|
FALSE,
|
|
&Event,
|
|
&IoStatus
|
|
);
|
|
|
|
if( Irp != NULL )
|
|
{
|
|
IrpStack = IoGetNextIrpStackLocation( Irp );
|
|
|
|
IrpStack->MajorFunction = IRP_MJ_READ;
|
|
IrpStack->Parameters.Read.Length = InDataLen;
|
|
|
|
NTStatus = IoCallDriver( ReaderExtension->SerialDeviceObject, Irp );
|
|
|
|
if( NTStatus == STATUS_PENDING )
|
|
{
|
|
KeWaitForSingleObject(
|
|
&Event,
|
|
Executive,
|
|
KernelMode,
|
|
FALSE,
|
|
NULL
|
|
);
|
|
NTStatus = IoStatus.Status;
|
|
|
|
}
|
|
}
|
|
|
|
return( NTStatus );
|
|
}
|
|
|
|
NTSTATUS
|
|
IFSerialWaitOnMask( PREADER_EXTENSION ReaderExtension )
|
|
/*++
|
|
|
|
Routine Description:
|
|
|
|
Arguments:
|
|
|
|
Return Value:
|
|
|
|
--*/
|
|
{
|
|
PIRP irp;
|
|
PIO_STACK_LOCATION irpSp;
|
|
NTSTATUS NTStatus;
|
|
|
|
|
|
if (KeReadStateEvent(&(ReaderExtension->SerialCloseDone))) {
|
|
|
|
//
|
|
// we have no connection to serial, fail the call
|
|
// this could be the case if the reader was removed
|
|
// during stand by / hibernation
|
|
//
|
|
return STATUS_UNSUCCESSFUL;
|
|
}
|
|
|
|
irp = IoAllocateIrp(
|
|
(CCHAR) (ReaderExtension->SerialDeviceObject->StackSize + 1),
|
|
FALSE
|
|
);
|
|
|
|
ASSERT(irp != NULL);
|
|
|
|
if (irp == NULL) {
|
|
|
|
return STATUS_INSUFFICIENT_RESOURCES;
|
|
}
|
|
|
|
irpSp = IoGetNextIrpStackLocation( irp );
|
|
irpSp->MajorFunction = IRP_MJ_DEVICE_CONTROL;
|
|
|
|
irpSp->Parameters.DeviceIoControl.InputBufferLength = 0;
|
|
irpSp->Parameters.DeviceIoControl.OutputBufferLength =
|
|
sizeof(ReaderExtension->EventMask);
|
|
irpSp->Parameters.DeviceIoControl.IoControlCode =
|
|
IOCTL_SERIAL_WAIT_ON_MASK;
|
|
|
|
irp->AssociatedIrp.SystemBuffer = &ReaderExtension->EventMask;
|
|
|
|
// set completion routine & start io
|
|
IoSetCompletionRoutine(
|
|
irp,
|
|
IFReadThreadCallback,
|
|
ReaderExtension,
|
|
TRUE,
|
|
TRUE,
|
|
TRUE
|
|
);
|
|
|
|
NTStatus = IoCallDriver( ReaderExtension->SerialDeviceObject, irp );
|
|
|
|
return (NTStatus == STATUS_PENDING ? STATUS_SUCCESS : NTStatus);
|
|
}
|
|
|
|
NTSTATUS
|
|
IFReadThreadCallback(
|
|
PDEVICE_OBJECT DeviceObject,
|
|
PIRP Irp,
|
|
PREADER_EXTENSION ReaderExtension
|
|
)
|
|
/*++
|
|
|
|
Routine Description:
|
|
|
|
Arguments:
|
|
|
|
Return Value:
|
|
|
|
--*/
|
|
{
|
|
|
|
KIRQL irql;
|
|
|
|
// event_rx?
|
|
if( ReaderExtension->EventMask & SERIAL_EV_RXCHAR )
|
|
{
|
|
IoQueueWorkItem(
|
|
ReaderExtension->ReadWorkItem,
|
|
(PIO_WORKITEM_ROUTINE) IFReadWorkRoutine,
|
|
CriticalWorkQueue,
|
|
ReaderExtension
|
|
);
|
|
}
|
|
else
|
|
{
|
|
SmartcardDebug(
|
|
DEBUG_TRACE,
|
|
("SCMSTCS!IFReadThreadCallback: Device removed\n" )
|
|
);
|
|
KeAcquireSpinLock(&ReaderExtension->SmartcardExtension->OsData->SpinLock,
|
|
&irql);
|
|
|
|
ReaderExtension->SmartcardExtension->ReaderCapabilities.CurrentState =
|
|
(ULONG) SCARD_UNKNOWN;
|
|
|
|
KeReleaseSpinLock(&ReaderExtension->SmartcardExtension->OsData->SpinLock,
|
|
irql);
|
|
|
|
// last call: disconnect from the serial driver
|
|
IoQueueWorkItem(
|
|
ReaderExtension->CloseSerial,
|
|
(PIO_WORKITEM_ROUTINE) DrvWaitForDeviceRemoval,
|
|
DelayedWorkQueue,
|
|
NULL
|
|
);
|
|
}
|
|
|
|
IoFreeIrp(Irp);
|
|
return STATUS_MORE_PROCESSING_REQUIRED;
|
|
}
|
|
|
|
VOID
|
|
IFReadWorkRoutine(
|
|
IN PDEVICE_OBJECT DeviceObject,
|
|
IN PREADER_EXTENSION ReaderExtension
|
|
)
|
|
{
|
|
NTSTATUS NTStatus = STATUS_SUCCESS;
|
|
PIO_STACK_LOCATION IrpStack;
|
|
PIRP Irp;
|
|
SERIAL_STATUS CommStatus;
|
|
PUCHAR IOData;
|
|
BOOLEAN purgePort = FALSE;
|
|
BOOLEAN bUpdatCardState = FALSE;
|
|
USHORT shrtBuf;
|
|
|
|
ASSERT( ReaderExtension != NULL );
|
|
|
|
|
|
if (ReaderExtension == NULL) {
|
|
|
|
return;
|
|
}
|
|
|
|
__try {
|
|
|
|
IOData = &ReaderExtension->IOData[0];
|
|
|
|
while (NTStatus == STATUS_SUCCESS) {
|
|
|
|
// read head
|
|
NTStatus = IFSerialRead(
|
|
ReaderExtension,
|
|
&IOData[0],
|
|
3
|
|
);
|
|
|
|
if (NTStatus != STATUS_SUCCESS) {
|
|
|
|
__leave;
|
|
}
|
|
|
|
if ((IOData[NAD_IDX] & 0x20) != 0x20) {
|
|
|
|
SmartcardDebug(
|
|
DEBUG_ERROR,
|
|
("SCMSTCS!IFReadWorkRoutine: Invalid packet received\n" )
|
|
);
|
|
|
|
purgePort = TRUE;
|
|
__leave;
|
|
}
|
|
|
|
|
|
if (IOData[LEN_IDX] > STC_BUFFER_SIZE - 4) {
|
|
|
|
purgePort = TRUE;
|
|
__leave;
|
|
}
|
|
|
|
// read tail
|
|
NTStatus = IFSerialRead(
|
|
ReaderExtension,
|
|
&IOData[DATA_IDX],
|
|
IOData[LEN_IDX] + 1
|
|
);
|
|
|
|
ASSERT(NTStatus == STATUS_SUCCESS);
|
|
|
|
if (NTStatus != STATUS_SUCCESS) {
|
|
|
|
purgePort = TRUE;
|
|
__leave;
|
|
}
|
|
|
|
if (IOData[LEN_IDX] == 0) {
|
|
|
|
purgePort = TRUE;
|
|
__leave;
|
|
}
|
|
|
|
// check for card insertion / removal
|
|
RtlRetrieveUshort(&shrtBuf, &IOData[DATA_IDX]);
|
|
|
|
if( ( IOData[NAD_IDX] == STC1_TO_HOST ) &&
|
|
( IOData[LEN_IDX] == 2 ) &&
|
|
( (shrtBuf == SW_INSERTED) ||
|
|
(shrtBuf == SW_REMOVED))) {
|
|
|
|
CBUpdateCardState(
|
|
ReaderExtension->SmartcardExtension,
|
|
(shrtBuf == SW_INSERTED ? SCARD_PRESENT : SCARD_ABSENT)
|
|
);
|
|
|
|
|
|
} else {
|
|
|
|
KIRQL CurrentIrql;
|
|
|
|
|
|
// acquire spinlock to protect buffer/flag manipulation
|
|
KeAcquireSpinLock(
|
|
&ReaderExtension->ReadSpinLock,
|
|
&CurrentIrql
|
|
);
|
|
|
|
// check size & copy data to TPDU stack
|
|
ASSERT(
|
|
ReaderExtension->Available+IOData[LEN_IDX] + 4 <
|
|
TPDU_STACK_SIZE
|
|
);
|
|
|
|
if (ReaderExtension->Available + IOData[LEN_IDX] + 4 <
|
|
TPDU_STACK_SIZE ) {
|
|
|
|
SysCopyMemory(
|
|
&ReaderExtension->TPDUStack[ReaderExtension->Available],
|
|
&IOData[ 0 ],
|
|
IOData[ LEN_IDX ] + 4
|
|
);
|
|
|
|
ReaderExtension->Available +=
|
|
IOData[LEN_IDX] + 4;
|
|
|
|
if(ReaderExtension->Available >= ReaderExtension->Expected ) {
|
|
|
|
KeSetEvent(
|
|
&ReaderExtension->DataAvailable,
|
|
IO_SERIAL_INCREMENT,
|
|
FALSE
|
|
);
|
|
}
|
|
}
|
|
|
|
KeReleaseSpinLock( &ReaderExtension->ReadSpinLock, CurrentIrql );
|
|
}
|
|
}
|
|
}
|
|
__finally {
|
|
|
|
|
|
if (purgePort) {
|
|
|
|
ULONG request;
|
|
KIRQL CurrentIrql;
|
|
|
|
// acquire spinlock to protect buffer/flag manipulation
|
|
KeAcquireSpinLock(
|
|
&ReaderExtension->ReadSpinLock,
|
|
&CurrentIrql
|
|
);
|
|
|
|
ReaderExtension->Available = 0;
|
|
|
|
KeReleaseSpinLock(
|
|
&ReaderExtension->ReadSpinLock,
|
|
CurrentIrql
|
|
);
|
|
|
|
// we got an error and need to clean up the port
|
|
request = SR_PURGE;
|
|
NTStatus = IFSerialIoctl(
|
|
ReaderExtension,
|
|
IOCTL_SERIAL_PURGE,
|
|
&request,
|
|
sizeof(request),
|
|
NULL,
|
|
0
|
|
);
|
|
|
|
ASSERT(NTStatus == STATUS_SUCCESS);
|
|
}
|
|
|
|
IFSerialWaitOnMask( ReaderExtension );
|
|
}
|
|
}
|
|
|
|
UCHAR IFCalcLRC( PUCHAR IOData, ULONG IODataLen )
|
|
{
|
|
ULONG Idx = 0;
|
|
UCHAR CS = 0;
|
|
|
|
do CS ^= IOData[ Idx ];
|
|
while( ++Idx < IODataLen );
|
|
|
|
return( CS );
|
|
}
|
|
|
|
|
|
//---------------------------------------- END OF FILE ----------------------------------------
|