view Serial.cpp @ 1:0b3630a29ad8

Initial version based on previous repository. Project was ported to QT6 and in now cmake based.
author Ideenmodellierer <tiefenrauscher@web.de>
date Thu, 27 Nov 2025 18:40:28 +0100
parents
children 177f640940f2
line wrap: on
line source

//////////////////////////////////////////////////////////////////////////////
/// \file   Serial.cpp
/// \brief  RS232 serial i/o.
/// \author JD Gascuel.
///
/// \copyright (c) 2011-2016 JD Gascuel. All rights reserved.
/// $Id$
//////////////////////////////////////////////////////////////////////////////
//
// BSD 2-Clause License:
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions
// are met:
//
// 1. Redistributions of source code must retain the above copyright notice,
//    this list of conditions and the following disclaimer.
//
// 2. Redistributions in binary form must reproduce the above copyright notice,
//    this list of conditions and the following disclaimer in the documentation
//    and/or other materials provided with the distribution.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
// THE POSSIBILITY OF SUCH DAMAGE.
//
//////////////////////////////////////////////////////////////////////////////

#include "Serial.h"

#include "Utils/Log.h"
#include "Utils/Exception.h"

#include <QString>

#ifdef WIN32
#   define  S_WRITE(p, b, li, lo)   WriteFile(p, b, li, &lo, NULL)
#   define  S_READ(p, b, li, lo)    ReadFile(p, b, li, &lo, NULL)
#   define  S_LEN                   DWORD
#   define  S_FLUSH(p)              FlushFileBuffers(p)
#   define  S_PURGE(p)              PurgeComm(p, PURGE_RXCLEAR | PURGE_TXCLEAR)
#   define  S_CLOSE(p)              CloseHandle(p)
#else
#   include <fcntl.h>
#   include <termios.h>
#   include <sys/ioctl.h>
#   include <unistd.h>

#   define  INVALID_HANDLE_VALUE    (-1)
#   define  S_WRITE(p, b, li, lo)   (lo = write(p, b, li))
#   define  S_READ(p, b, li, lo)    (lo = read (p, b, li))
#   define  S_LEN                   ssize_t
#   define  S_FLUSH(p)              tcdrain(p)
#   define  S_PURGE(p)              tcflush(p,TCIOFLUSH)
#   define  S_CLOSE(p)              ::close(p)
#endif

///////////////////////////////////////////////////////////////////////////////

Serial::~Serial()
{}

///////////////////////////////////////////////////////////////////////////////

void Serial::open(const QString& port, const QString &type)
{
    if( _isOpen ) return;
    LOG_TRACE("Open " << port << " ...");

    _hSerial = INVALID_HANDLE_VALUE;

    //------------------------------------------------------------------------
    // Sanity checks.
    if( port.isEmpty() )
        LOG_THROW( "Port is not defined." );
    if( type.isEmpty() )
        LOG_THROW( "Port type is not defined." );

    bool usbMode = type.contains("ostc2c",    Qt::CaseInsensitive)
                || (   type.contains("ostc3",    Qt::CaseInsensitive)
                   && !type.contains("ostc3p",   Qt::CaseInsensitive))
                || type.contains("ostc_cr",  Qt::CaseInsensitive);
    LOG_TRACE((usbMode ? "Fast USB" : "Slow Bluetooth") << " connection mode.");

    //------------------------------------------------------------------------
#ifdef Q_OS_WIN
    // BUGFIX: COM ports above COM9 are not automatically recognized,
    //         hence we have to prepend DEVICE NAMESPACE...
    // http://msdn.microsoft.com/en-us/library/windows/desktop/aa365247(v=vs.85).aspx
    QByteArray com = port.toLatin1() + "\0";
    if( !com.startsWith("\\\\.\\") )
        com = "\\\\.\\" + com;

    _hSerial = CreateFileA(com.data(),
                         GENERIC_READ | GENERIC_WRITE,
                         0,             // Exclusive access.
                         NULL,          // No security
                         OPEN_EXISTING,
                         FILE_ATTRIBUTE_DEVICE | FILE_FLAG_NO_BUFFERING,
                         0);
    if(_hSerial==INVALID_HANDLE_VALUE)
    {
        if(GetLastError()==ERROR_FILE_NOT_FOUND)
            LOG_THROW( "Unknown port" );
        LOG_THROW( "Unable to open port" );
    }
    S_PURGE(_hSerial);

    DCB dcbSerial = {sizeof(dcbSerial), 0};
    if( !GetCommState(_hSerial, &dcbSerial) )
        LOG_THROW( "Unable to get COM port config" );

    dcbSerial.BaudRate     = CBR_115200;
    dcbSerial.ByteSize     = 8;
    dcbSerial.Parity       = NOPARITY;
    dcbSerial.StopBits     = ONESTOPBIT;
    dcbSerial.fOutxCtsFlow = DTR_CONTROL_ENABLE;                 // NO HARDWARE FLOW CONTROL
    dcbSerial.fRtsControl  = RTS_CONTROL_ENABLE; //RTS_CONTROL_DISABLE;   // NO HARDWARE FLOW CONTROL

    if( !SetCommState(_hSerial, &dcbSerial) )
        LOG_THROW( "Unable to set COM port config" );

    COMMTIMEOUTS timeouts={0};
    if( usbMode ) {
        timeouts.ReadTotalTimeoutConstant  =  500;  // 0.5 sec
        timeouts.WriteTotalTimeoutConstant = 1000;  // 1.0 sec
    } else {
        timeouts.ReadTotalTimeoutConstant  = 2000;  // 2.0 sec timeout.
    }
    if( !SetCommTimeouts(_hSerial, &timeouts) )
        LOG_THROW( "Unable to configure port" );

    LOG_TRACE("Connection:");
    LOG_TRACE("    " << dcbSerial.BaudRate << " bauds.");
    LOG_TRACE("    " << (int)dcbSerial.ByteSize << " bits, "
                     << (dcbSerial.Parity ? "+parity, " : "no parity, ")
                     << (dcbSerial.StopBits ? QString(" +%1").arg(dcbSerial.StopBits) : QString("no")) << " stops bits.");
    LOG_TRACE("    CTS is " << (dcbSerial.fOutxCtsFlow ? "ON." : "OFF."));
    LOG_TRACE("    RTS is " << ((dcbSerial.fRtsControl == RTS_CONTROL_HANDSHAKE) ? "ON."
                              : (dcbSerial.fRtsControl == RTS_CONTROL_ENABLE)    ? "FORCED."
                              :                                                    "OFF."));
    LOG_TRACE("    Read  timeout " << timeouts.ReadTotalTimeoutConstant  << " msec.");
    LOG_TRACE("    Write timeout " << timeouts.WriteTotalTimeoutConstant << " msec.");
#endif

    //------------------------------------------------------------------------
#if defined(Q_OS_MAC) || defined(Q_OS_LINUX)
    QByteArray p = port.toLatin1();
    if( ! p.startsWith("/dev/") )
        p = "/dev/" + p;
    _hSerial = ::open( p.constData(), O_RDWR | O_NOCTTY | O_NONBLOCK, 0);
    if( _hSerial<0 )
        LOG_THROW( "Unable to open port " << p);

    if( ioctl(_hSerial, TIOCEXCL) < 0 )
        LOG_THROW( "Port in use" );

    // Once opened, clearing the O_NONBLOCK flag.
    if( fcntl(_hSerial, F_SETFL, 0) < 0 )
        LOG_THROW( "Can't reset non-blocking I/O" );

    struct termios termios;
    tcgetattr(_hSerial, &termios);

    cfmakeraw(&termios);
    if( cfsetspeed(&termios, B115200) < 0 )
        LOG_THROW( "Bad port speed" );

    termios.c_cflag |= CLOCAL| CS8 | CREAD;     // No DTR/DSR/DCD, 8bit.
    termios.c_cflag &= ~( PARENB | CSTOPB       // No parity, one stop bit.
                    | CCTS_OFLOW | CRTS_IFLOW); // No hardware flow control.

    if( usbMode ) {
        // FAST USB: Fix timeout to 0.5 seconde:
        termios.c_cc[VMIN]  =  0;   // Pure timout mode (no char needed).
        termios.c_cc[VTIME] =  5;   // 0.5s timeout after last received byte.
    } else {
        // SLOW BLUETOOTH: Fix timeout to 2.0 sec:
        termios.c_cc[VMIN]  =  0;   // Pure timout mode (no char needed).
        termios.c_cc[VTIME] = 20;   // 2.0sec timeout after last received byte.
    }

    if( tcsetattr(_hSerial, TCSANOW, &termios) < 0 )
        LOG_THROW( "Unable to configure port" );
#endif

    if( _hSerial == INVALID_HANDLE_VALUE )
        LOG_THROW( "Port not open" );

    _isOpen = true;
    LOG_TRACE("Device open successfull.");
}

///////////////////////////////////////////////////////////////////////////////

void Serial::close()
{
    if( !_isOpen ) return;
    LOG_TRACE("Device close ...");

#ifdef Q_OS_MAC
    if( ioctl(_hSerial, TIOCNXCL) < 0 )
        LOG_THROW( "Port in use" );
#endif

    if( _hSerial == INVALID_HANDLE_VALUE )
        LOG_THROW( "Port not open" );
    S_CLOSE(_hSerial);

    _hSerial = INVALID_HANDLE_VALUE;
    _isOpen  = false;
    LOG_TRACE("Device close successfull.");
}

///////////////////////////////////////////////////////////////////////////////

unsigned char Serial::readByte() const
{
    if( !_isOpen )
        LOG_THROW( "Port not open" );

    unsigned char byte = 0;
    S_LEN len = 0;
    S_READ(_hSerial, &byte, 1, len);
    if( len != 1 )
        // LOG_THROW_E(ReadTimeout, "< timeout" );
        LOG_THROW("< timeout" );

    if( isprint(byte) )
   //     LOG_DEBUG("< " << QString().sprintf("%02x '%c'", byte, byte) );
        LOG_DEBUG("< " << QString::asprintf("%02x '%c'", byte, byte));
    else
        LOG_DEBUG("< " << QString::asprintf("%02x", byte) );

    return byte;
}

void Serial::writeByte(unsigned char byte) const
{
    if( !_isOpen )
        LOG_THROW( "Port not open" );

    if( isprint(byte) )
        LOG_DEBUG("> " << QString::asprintf("%02x '%c'", byte, byte) );
    else
        LOG_DEBUG("> " << QString::asprintf("%02x", byte) );

    S_LEN len = 0;
    S_WRITE(_hSerial, &byte, 1, len);
    if( len != 1 )
   //     LOG_THROW_E(WriteTimeout, "> timeout");
         LOG_THROW("> timeout");
}

///////////////////////////////////////////////////////////////////////////////

unsigned short Serial::readShort() const
{
    unsigned char lo = readByte();
    unsigned char hi = readByte();
    return hi << 8 | lo;
}

void Serial::writeShort(unsigned short word) const
{
    unsigned char lo = word & 0xFF;
    unsigned char hi = word >> 8;
    writeByte(lo); writeByte(hi);
}

///////////////////////////////////////////////////////////////////////////////

void Serial::writeInt24(unsigned int int24) const
{
    unsigned char lo = int24 & 0xFF;
    unsigned char hi = (int24 >>  8) & 0xFF;
    unsigned char up = (int24 >> 16) & 0xFF;
    writeByte(lo); writeByte(hi); writeByte(up);
}

unsigned int Serial::readInt24() const
{
    unsigned char lo = readByte();
    unsigned char hi = readByte();
    unsigned char up = readByte();
    return up << 16 | hi << 8 | lo;
}

///////////////////////////////////////////////////////////////////////////////

unsigned int Serial::readBlock(unsigned char* ptr, unsigned int size) const
{
    if( !_isOpen )
        LOG_THROW( "Port not open" );

    unsigned int bytes = 0;

    bool timeout = false;
    while( size > 0 )
    {
        // Allow up to 1.0sec for each 4K block.
        S_LEN todo = (size > 4096) ? 4096 : size;
        S_LEN done = 0;
        S_READ(_hSerial, ptr+bytes, todo, done);

        if( done == 0 ) {
            timeout = true;
            break;
        }

        size -= done;
        bytes += (unsigned int)done;
    }

    if( Log::minLevel <= Log::LEVEL_DEBUG )
    {
        const int DUMP_LINE = 16;
        const int DUMP_MAX  = 3*DUMP_LINE;
        const unsigned int length = (bytes < DUMP_MAX) ? bytes : DUMP_MAX;
        for(unsigned int i=0; i<length; i += DUMP_LINE)
        {
            LogAction logger(Log::LEVEL_DEBUG, __FILE__ ,__LINE__, "readBlock()");
            logger << "< ";
            for(unsigned int j=i; j<bytes && j<(i+DUMP_LINE); ++j)
                logger << QString::asprintf("%02x ", ptr[j]);
            for(unsigned int j=i; j<bytes && j<(i+DUMP_LINE); ++j)
                logger << (isprint( ptr[j] )
                          ? ptr[j]
                          : (unsigned char)'.');
        }
        if( length < bytes )
            LOG_DEBUG("< ... " << (bytes-length) << " more bytes.");
    }

    if( timeout )
        LOG_THROW_E(ReadTimeout, "< block timeout (missing " << size << " bytes)");

    return bytes;
}

///////////////////////////////////////////////////////////////////////////////

void Serial::writeBlock(const unsigned char* ptr, unsigned int size) const
{
    if( !_isOpen )
        LOG_THROW( "Port not open" );

    if( Log::minLevel <= Log::LEVEL_DEBUG )
    {
        const int DUMP_LINE = 16;
        const int DUMP_MAX  = 3*DUMP_LINE;
        const unsigned int length = (size < DUMP_MAX) ? size : DUMP_MAX;
        for(unsigned int i=0; i<length; i += DUMP_LINE)
        {
            LogAction logger(Log::LEVEL_DEBUG, __FILE__ ,__LINE__, "writeBlock()");
            logger << "> ";
            for(unsigned int j=i; j<size && j<(i+DUMP_LINE); ++j)
                logger << QString::asprintf("%02x ", ptr[j]);
            for(unsigned int j=i; j<size && j<(i+DUMP_LINE); ++j)
                logger << (isprint( ptr[j] )
                          ? ptr[j]
                          : (unsigned char)'.');
        }
        if( length < size )
            LOG_DEBUG("> ... " << (size-length) << " more bytes.");
    }

    while( size > 0 )
    {
        // Allow up to 1.0sec for each 4K block.
        S_LEN chunck = (size > 4096) ? 4096 : size;
        S_LEN len = 0;
        S_WRITE(_hSerial, ptr, chunck, len);

        if( len == 0  )
            LOG_THROW_E(WriteTimeout, "> block timeout");

        size -= len;
        ptr  += len;
    }

    // Auto-fluh on each write.
    flush();
}

///////////////////////////////////////////////////////////////////////////////

void Serial::purge()
{
    if( !_isOpen )
        LOG_THROW( "Port not open" );

    // Empty incomming buffer
    S_PURGE(_hSerial);

    LOG_TRACE("Device purged.");
}

///////////////////////////////////////////////////////////////////////////////

void Serial::flush() const
{
    if( !_isOpen )
        LOG_THROW( "Port not open" );

    S_FLUSH(_hSerial);
}

///////////////////////////////////////////////////////////////////////////////

void Serial::sleep(int msec) const
{
#ifdef WIN32
    Sleep(msec);
#else
    usleep(msec*1000);
#endif
}