When using half-duplex communication (e.g. only a single wire for both Tx and Rx) a data packet needs to be transmitted entirely before attempting to switch into receiving mode. For legacy serial hardware, the tcdrain() probably works as advertised, and waits until the data has been transmitted. However for common usb-serial converters, the hardware doesn't provide any feedback to the driver, and the tcdrain() function can only wait until the data has been transmitted to the usb-serial chip. There is no guarantee that the data has actually been transmitted by the usb-serial chip. As a workaround, we wait at least the minimum amount of time required to transmit the data packet over a serial line, taking into account the current configuration.
616 lines
13 KiB
C
616 lines
13 KiB
C
/*
|
|
* libdivecomputer
|
|
*
|
|
* Copyright (C) 2008 Jef Driesen
|
|
*
|
|
* This library is free software; you can redistribute it and/or
|
|
* modify it under the terms of the GNU Lesser General Public
|
|
* License as published by the Free Software Foundation; either
|
|
* version 2.1 of the License, or (at your option) any later version.
|
|
*
|
|
* This library is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
* Lesser General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU Lesser General Public
|
|
* License along with this library; if not, write to the Free Software
|
|
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
|
|
* MA 02110-1301 USA
|
|
*/
|
|
|
|
#include <stdlib.h>
|
|
#include <windows.h>
|
|
|
|
#include "serial.h"
|
|
#include "utils.h"
|
|
|
|
#define TRACE(expr) \
|
|
{ \
|
|
DWORD error = GetLastError (); \
|
|
message ("TRACE (%s:%d, %s): %s (%d)\n", __FILE__, __LINE__, \
|
|
expr, serial_errmsg (), serial_errcode ()); \
|
|
SetLastError (error); \
|
|
}
|
|
|
|
struct serial_t {
|
|
/*
|
|
* The file descriptor corresponding to the serial port.
|
|
*/
|
|
HANDLE hFile;
|
|
/*
|
|
* Serial port settings are saved into this variables immediately
|
|
* after the port is opened. These settings are restored when the
|
|
* serial port is closed.
|
|
*/
|
|
DCB dcb;
|
|
COMMTIMEOUTS timeouts;
|
|
/* Half-duplex settings */
|
|
int halfduplex;
|
|
unsigned int baudrate;
|
|
unsigned int nbits;
|
|
};
|
|
|
|
//
|
|
// Error reporting.
|
|
//
|
|
|
|
int serial_errcode (void)
|
|
{
|
|
return GetLastError ();
|
|
}
|
|
|
|
|
|
const char* serial_errmsg (void)
|
|
{
|
|
static char buffer[256] = {0};
|
|
unsigned int size = sizeof (buffer) / sizeof (char);
|
|
|
|
DWORD errcode = GetLastError ();
|
|
DWORD rc = FormatMessageA (FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS,
|
|
NULL, errcode, 0, buffer, size, NULL);
|
|
// Remove certain characters ('\r', '\n' and '.')
|
|
// at the end of the error message.
|
|
while (rc > 0 && (
|
|
buffer[rc-1] == '\n' ||
|
|
buffer[rc-1] == '\r' ||
|
|
buffer[rc-1] == '.')) {
|
|
buffer[rc-1] = '\0';
|
|
rc--;
|
|
}
|
|
if (rc) {
|
|
return buffer;
|
|
} else {
|
|
return NULL;
|
|
}
|
|
}
|
|
|
|
//
|
|
// Open the serial port.
|
|
//
|
|
|
|
int
|
|
serial_open (serial_t **out, const char* name)
|
|
{
|
|
if (out == NULL)
|
|
return -1; // ERROR_INVALID_PARAMETER (The parameter is incorrect)
|
|
|
|
// Build the device name.
|
|
const char *devname = NULL;
|
|
char buffer[MAX_PATH] = "\\\\.\\";
|
|
if (name && strncmp (name, buffer, 4) != 0) {
|
|
size_t length = strlen (name) + 1;
|
|
if (length + 4 > sizeof (buffer))
|
|
return -1;
|
|
memcpy (buffer + 4, name, length);
|
|
devname = buffer;
|
|
} else {
|
|
devname = name;
|
|
}
|
|
|
|
// Allocate memory.
|
|
serial_t *device = (serial_t *) malloc (sizeof (serial_t));
|
|
if (device == NULL) {
|
|
TRACE ("malloc");
|
|
return -1; // ERROR_OUTOFMEMORY (Not enough storage is available to complete this operation)
|
|
}
|
|
|
|
// Default to full-duplex.
|
|
device->halfduplex = 0;
|
|
device->baudrate = 0;
|
|
device->nbits = 0;
|
|
|
|
// Open the device.
|
|
device->hFile = CreateFileA (devname,
|
|
GENERIC_READ | GENERIC_WRITE, 0,
|
|
NULL, // No security attributes.
|
|
OPEN_EXISTING,
|
|
0, // Non-overlapped I/O.
|
|
NULL);
|
|
if (device->hFile == INVALID_HANDLE_VALUE) {
|
|
TRACE ("CreateFile");
|
|
free (device);
|
|
return -1;
|
|
}
|
|
|
|
// Retrieve the current communication settings and timeouts,
|
|
// to be able to restore them when closing the device.
|
|
// It is also used to check if the obtained handle
|
|
// represents a serial device.
|
|
if (!GetCommState (device->hFile, &device->dcb) ||
|
|
!GetCommTimeouts (device->hFile, &device->timeouts)) {
|
|
TRACE ("GetCommState/GetCommTimeouts");
|
|
CloseHandle (device->hFile);
|
|
free (device);
|
|
return -1;
|
|
}
|
|
|
|
*out = device;
|
|
|
|
return 0;
|
|
}
|
|
|
|
//
|
|
// Close the serial port.
|
|
//
|
|
|
|
int
|
|
serial_close (serial_t *device)
|
|
{
|
|
if (device == NULL)
|
|
return 0;
|
|
|
|
// Restore the initial communication settings and timeouts.
|
|
if (!SetCommState (device->hFile, &device->dcb) ||
|
|
!SetCommTimeouts (device->hFile, &device->timeouts)) {
|
|
TRACE ("SetCommState/SetCommTimeouts");
|
|
CloseHandle (device->hFile);
|
|
free (device);
|
|
return -1;
|
|
}
|
|
|
|
// Close the device.
|
|
if (!CloseHandle (device->hFile)) {
|
|
TRACE ("CloseHandle");
|
|
free (device);
|
|
return -1;
|
|
}
|
|
|
|
// Free memory.
|
|
free (device);
|
|
|
|
return 0;
|
|
}
|
|
|
|
//
|
|
// Configure the serial port (baudrate, databits, parity, stopbits and flowcontrol).
|
|
//
|
|
|
|
int
|
|
serial_configure (serial_t *device, int baudrate, int databits, int parity, int stopbits, int flowcontrol)
|
|
{
|
|
if (device == NULL)
|
|
return -1; // ERROR_INVALID_PARAMETER (The parameter is incorrect)
|
|
|
|
// Retrieve the current settings.
|
|
DCB dcb;
|
|
if (!GetCommState (device->hFile, &dcb)) {
|
|
TRACE ("GetCommState");
|
|
return -1;
|
|
}
|
|
|
|
dcb.fBinary = TRUE; // Enable Binary Transmission
|
|
dcb.fAbortOnError = FALSE;
|
|
|
|
// Baudrate.
|
|
dcb.BaudRate = baudrate;
|
|
|
|
// Character size.
|
|
if (databits >= 5 && databits <= 8)
|
|
dcb.ByteSize = databits;
|
|
else
|
|
return -1;
|
|
|
|
// Parity checking.
|
|
switch (parity) {
|
|
case SERIAL_PARITY_NONE: // No parity
|
|
dcb.Parity = NOPARITY;
|
|
dcb.fParity = FALSE;
|
|
break;
|
|
case SERIAL_PARITY_EVEN: // Even parity
|
|
dcb.Parity = EVENPARITY;
|
|
dcb.fParity = TRUE;
|
|
break;
|
|
case SERIAL_PARITY_ODD: // Odd parity
|
|
dcb.Parity = ODDPARITY;
|
|
dcb.fParity = TRUE;
|
|
break;
|
|
default:
|
|
return -1;
|
|
}
|
|
// Stopbits.
|
|
switch (stopbits) {
|
|
case 1: // One stopbit
|
|
dcb.StopBits = ONESTOPBIT;
|
|
break;
|
|
case 2: // Two stopbits
|
|
dcb.StopBits = TWOSTOPBITS;
|
|
break;
|
|
default:
|
|
return -1;
|
|
}
|
|
|
|
// Flow control.
|
|
switch (flowcontrol) {
|
|
case SERIAL_FLOWCONTROL_NONE: // No flow control.
|
|
dcb.fInX = FALSE;
|
|
dcb.fOutX = FALSE;
|
|
dcb.fOutxCtsFlow = FALSE;
|
|
dcb.fOutxDsrFlow = FALSE;
|
|
dcb.fDtrControl = DTR_CONTROL_ENABLE;
|
|
dcb.fRtsControl = RTS_CONTROL_ENABLE;
|
|
break;
|
|
case SERIAL_FLOWCONTROL_HARDWARE: // Hardware (RTS/CTS) flow control.
|
|
dcb.fInX = FALSE;
|
|
dcb.fOutX = FALSE;
|
|
dcb.fOutxCtsFlow = TRUE;
|
|
dcb.fOutxDsrFlow = TRUE;
|
|
dcb.fDtrControl = DTR_CONTROL_HANDSHAKE;
|
|
dcb.fRtsControl = RTS_CONTROL_HANDSHAKE;
|
|
break;
|
|
case SERIAL_FLOWCONTROL_SOFTWARE: // Software (XON/XOFF) flow control.
|
|
dcb.fInX = TRUE;
|
|
dcb.fOutX = TRUE;
|
|
dcb.fOutxCtsFlow = FALSE;
|
|
dcb.fOutxDsrFlow = FALSE;
|
|
dcb.fDtrControl = DTR_CONTROL_ENABLE;
|
|
dcb.fRtsControl = RTS_CONTROL_ENABLE;
|
|
break;
|
|
default:
|
|
return -1;
|
|
}
|
|
|
|
// Apply the new settings.
|
|
if (!SetCommState (device->hFile, &dcb)) {
|
|
TRACE ("SetCommState");
|
|
return -1;
|
|
}
|
|
|
|
device->baudrate = baudrate;
|
|
device->nbits = 1 + databits + stopbits + (parity ? 1 : 0);
|
|
|
|
return 0;
|
|
}
|
|
|
|
//
|
|
// Configure the serial port (timeouts).
|
|
//
|
|
|
|
int
|
|
serial_set_timeout (serial_t *device, long timeout)
|
|
{
|
|
if (device == NULL)
|
|
return -1; // ERROR_INVALID_PARAMETER (The parameter is incorrect)
|
|
|
|
// Retrieve the current timeouts.
|
|
COMMTIMEOUTS timeouts;
|
|
if (!GetCommTimeouts (device->hFile, &timeouts)) {
|
|
TRACE ("GetCommTimeouts");
|
|
return -1;
|
|
}
|
|
|
|
// Update the settings.
|
|
if (timeout < 0) {
|
|
// Blocking mode.
|
|
timeouts.ReadIntervalTimeout = 0;
|
|
timeouts.ReadTotalTimeoutMultiplier = 0;
|
|
timeouts.ReadTotalTimeoutConstant = 0;
|
|
timeouts.WriteTotalTimeoutMultiplier = 0;
|
|
timeouts.WriteTotalTimeoutConstant = 0;
|
|
} else if (timeout == 0) {
|
|
// Non-blocking mode.
|
|
timeouts.ReadIntervalTimeout = MAXDWORD;
|
|
timeouts.ReadTotalTimeoutMultiplier = 0;
|
|
timeouts.ReadTotalTimeoutConstant = 0;
|
|
timeouts.WriteTotalTimeoutMultiplier = 0;
|
|
timeouts.WriteTotalTimeoutConstant = 0;
|
|
} else {
|
|
// Standard timeout mode.
|
|
timeouts.ReadIntervalTimeout = 0;
|
|
timeouts.ReadTotalTimeoutMultiplier = 0;
|
|
timeouts.ReadTotalTimeoutConstant = timeout;
|
|
timeouts.WriteTotalTimeoutMultiplier = 0;
|
|
timeouts.WriteTotalTimeoutConstant = 0;
|
|
}
|
|
|
|
// Activate the new timeouts.
|
|
if (!SetCommTimeouts (device->hFile, &timeouts)) {
|
|
TRACE ("SetCommTimeouts");
|
|
return -1;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
//
|
|
// Configure the serial port (recommended size of the input/output buffers).
|
|
//
|
|
|
|
int
|
|
serial_set_queue_size (serial_t *device, unsigned int input, unsigned int output)
|
|
{
|
|
if (device == NULL)
|
|
return -1; // ERROR_INVALID_PARAMETER (The parameter is incorrect)
|
|
|
|
if (!SetupComm (device->hFile, input, output)) {
|
|
TRACE ("SetupComm");
|
|
return -1;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
|
|
int
|
|
serial_set_halfduplex (serial_t *device, int value)
|
|
{
|
|
if (device == NULL)
|
|
return -1; // ERROR_INVALID_PARAMETER (The parameter is incorrect)
|
|
|
|
device->halfduplex = value;
|
|
|
|
return 0;
|
|
}
|
|
|
|
|
|
int
|
|
serial_read (serial_t *device, void* data, unsigned int size)
|
|
{
|
|
if (device == NULL)
|
|
return -1; // ERROR_INVALID_PARAMETER (The parameter is incorrect)
|
|
|
|
DWORD dwRead = 0;
|
|
if (!ReadFile (device->hFile, data, size, &dwRead, NULL)) {
|
|
TRACE ("ReadFile");
|
|
return -1;
|
|
}
|
|
|
|
return dwRead;
|
|
}
|
|
|
|
|
|
int
|
|
serial_write (serial_t *device, const void* data, unsigned int size)
|
|
{
|
|
if (device == NULL)
|
|
return -1; // ERROR_INVALID_PARAMETER (The parameter is incorrect)
|
|
|
|
LARGE_INTEGER begin, end, freq;
|
|
if (device->halfduplex) {
|
|
// Get the current time.
|
|
if (!QueryPerformanceFrequency(&freq) ||
|
|
!QueryPerformanceCounter(&begin)) {
|
|
TRACE ("QueryPerformanceCounter");
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
DWORD dwWritten = 0;
|
|
if (!WriteFile (device->hFile, data, size, &dwWritten, NULL)) {
|
|
TRACE ("WriteFile");
|
|
return -1;
|
|
}
|
|
|
|
if (device->halfduplex) {
|
|
// Get the current time.
|
|
if (!QueryPerformanceCounter(&end)) {
|
|
TRACE ("QueryPerformanceCounter");
|
|
return -1;
|
|
}
|
|
|
|
// Calculate the elapsed time (microseconds).
|
|
unsigned long elapsed = 1000000.0 * (end.QuadPart - begin.QuadPart) / freq.QuadPart + 0.5;
|
|
|
|
// Calculate the expected duration (microseconds). A 2 millisecond fudge
|
|
// factor is added because it improves the success rate significantly.
|
|
unsigned long expected = 1000000.0 * device->nbits / device->baudrate * size + 0.5 + 2000;
|
|
|
|
// Wait for the remaining time.
|
|
if (elapsed < expected) {
|
|
unsigned long remaining = expected - elapsed;
|
|
|
|
// The remaining time is rounded up to the nearest millisecond
|
|
// because the Windows Sleep() function doesn't have a higher
|
|
// resolution.
|
|
serial_sleep ((remaining + 999) / 1000);
|
|
}
|
|
}
|
|
|
|
return dwWritten;
|
|
}
|
|
|
|
|
|
int
|
|
serial_flush (serial_t *device, int queue)
|
|
{
|
|
if (device == NULL)
|
|
return -1; // ERROR_INVALID_PARAMETER (The parameter is incorrect)
|
|
|
|
DWORD flags = 0;
|
|
|
|
switch (queue) {
|
|
case SERIAL_QUEUE_INPUT:
|
|
flags = PURGE_RXABORT | PURGE_RXCLEAR;
|
|
break;
|
|
case SERIAL_QUEUE_OUTPUT:
|
|
flags = PURGE_TXABORT | PURGE_TXCLEAR;
|
|
break;
|
|
default:
|
|
flags = PURGE_RXABORT | PURGE_RXCLEAR | PURGE_TXABORT | PURGE_TXCLEAR;
|
|
break;
|
|
}
|
|
|
|
if (!PurgeComm (device->hFile, flags)) {
|
|
TRACE ("PurgeComm");
|
|
return -1;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
|
|
int
|
|
serial_send_break (serial_t *device)
|
|
{
|
|
if (device == NULL)
|
|
return -1; // ERROR_INVALID_PARAMETER (The parameter is incorrect)
|
|
|
|
if (!SetCommBreak (device->hFile)) {
|
|
TRACE ("SetCommBreak");
|
|
return -1;
|
|
}
|
|
|
|
Sleep (250);
|
|
|
|
if (!ClearCommBreak (device->hFile)) {
|
|
TRACE ("ClearCommBreak");
|
|
return -1;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
|
|
int
|
|
serial_set_break (serial_t *device, int level)
|
|
{
|
|
if (device == NULL)
|
|
return -1; // ERROR_INVALID_PARAMETER (The parameter is incorrect)
|
|
|
|
if (level) {
|
|
if (!SetCommBreak (device->hFile)) {
|
|
TRACE ("SetCommBreak");
|
|
return -1;
|
|
}
|
|
} else {
|
|
if (!ClearCommBreak (device->hFile)) {
|
|
TRACE ("ClearCommBreak");
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
int
|
|
serial_set_dtr (serial_t *device, int level)
|
|
{
|
|
if (device == NULL)
|
|
return -1; // ERROR_INVALID_PARAMETER (The parameter is incorrect)
|
|
|
|
int status = (level ? SETDTR : CLRDTR);
|
|
|
|
if (!EscapeCommFunction (device->hFile, status)) {
|
|
TRACE ("EscapeCommFunction");
|
|
return -1;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
|
|
int
|
|
serial_set_rts (serial_t *device, int level)
|
|
{
|
|
if (device == NULL)
|
|
return -1; // ERROR_INVALID_PARAMETER (The parameter is incorrect)
|
|
|
|
int status = (level ? SETRTS : CLRRTS);
|
|
|
|
if (!EscapeCommFunction (device->hFile, status)) {
|
|
TRACE ("EscapeCommFunction");
|
|
return -1;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
|
|
int
|
|
serial_get_received (serial_t *device)
|
|
{
|
|
if (device == NULL)
|
|
return -1; // ERROR_INVALID_PARAMETER (The parameter is incorrect)
|
|
|
|
COMSTAT stats;
|
|
|
|
if (!ClearCommError (device->hFile, NULL, &stats)) {
|
|
TRACE ("ClearCommError");
|
|
return -1;
|
|
}
|
|
|
|
return stats.cbInQue;
|
|
}
|
|
|
|
|
|
int
|
|
serial_get_transmitted (serial_t *device)
|
|
{
|
|
if (device == NULL)
|
|
return -1; // ERROR_INVALID_PARAMETER (The parameter is incorrect)
|
|
|
|
COMSTAT stats;
|
|
|
|
if (!ClearCommError (device->hFile, NULL, &stats)) {
|
|
TRACE ("ClearCommError");
|
|
return -1;
|
|
}
|
|
|
|
return stats.cbOutQue;
|
|
}
|
|
|
|
|
|
int
|
|
serial_get_line (serial_t *device, int line)
|
|
{
|
|
if (device == NULL)
|
|
return -1; // ERROR_INVALID_PARAMETER (The parameter is incorrect)
|
|
|
|
DWORD stats = 0;
|
|
if (!GetCommModemStatus (device->hFile, &stats)) {
|
|
TRACE ("GetCommModemStatus");
|
|
return -1;
|
|
}
|
|
|
|
switch (line) {
|
|
case SERIAL_LINE_DCD:
|
|
return (stats & MS_RLSD_ON) == MS_RLSD_ON;
|
|
case SERIAL_LINE_CTS:
|
|
return (stats & MS_CTS_ON) == MS_CTS_ON;
|
|
case SERIAL_LINE_DSR:
|
|
return (stats & MS_DSR_ON) == MS_DSR_ON;
|
|
case SERIAL_LINE_RNG:
|
|
return (stats & MS_RING_ON) == MS_RING_ON;
|
|
default:
|
|
return -1;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
|
|
int
|
|
serial_sleep (unsigned long timeout)
|
|
{
|
|
Sleep (timeout);
|
|
|
|
return 0;
|
|
}
|
|
|
|
|
|
int
|
|
serial_timer (void)
|
|
{
|
|
return GetTickCount ();
|
|
}
|