eth123
eth123

Reputation: 11

Serial Port Connection to OBD II Adapter

I am trying to communicate with an OBD II adapter with ELM327 chip for the first time (similar to this but without the HS CAN switch). I am using a Macbook running on Catalina (10.15.7), and when I connect the OBD II adapter to the laptop (not to the car), I'm able to see a directory for the adapter (located in /dev/cu.usbmodemxxxxx). I've written a script in C++ to try to send some AT commands and see the responses (general ones like "ATZ\r" or "ATI\r") but I always see response set at zero.

Update 2 I tried to perform the same procedure on a Windows laptop, so had to change APIs resulting in the following code snippet: //Header files #include <windows.h>

std::string readFromSerialPort(HANDLE serialHandle)
{
    const int bufferSize = 256;
    char buffer[bufferSize] = {};
    DWORD bytesRead = 0;

    if (!ReadFile(serialHandle, buffer, sizeof(buffer) - 1, &bytesRead, NULL))
    {
        std::cerr << "Error reading from serial port" << std::endl;
    }
    
    buffer[bytesRead] = '\0';
    return std::string(buffer);
}

int main()
{
    HANDLE serialHandle = CreateFile("\\\\.\\COMx", GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
    if (serialHandle == INVALID_HANDLE_VALUE)
    {
        std::cerr << "Failed to open serial port" << std::endl;
        return 1;
    }

    DCB dcbSerialParams = {0};
    dcbSerialParams.DCBlength = sizeof(dcbSerialParams);
    if (!GetCommState(serialHandle, &dcbSerialParams))
    {
        std::cerr << "Error getting serial port state" << std::endl;
        CloseHandle(serialHandle);
        return 1;
    }
    dcbSerialParams.fBinary = TRUE;
    dcbSerialParams.fParity = FALSE;
    dcbSerialParams.fOutxCtsFlow = FALSE;
    dcbSerialParams.fOutxDsrFlow = FALSE;
    dcbSerialParams.fDtrControl = DTR_CONTROL_ENABLE;
    dcbSerialParams.fDsrSensitivity = FALSE;
    dcbSerialParams.fTXContinueOnXoff = TRUE;
    dcbSerialParams.fOutX = FALSE;
    dcbSerialParams.fInX = FALSE;
    dcbSerialParams.fErrorChar = FALSE;
    dcbSerialParams.fNull = FALSE;
    dcbSerialParams.fRtsControl = RTS_CONTROL_ENABLE;
    dcbSerialParams.fAbortOnError = FALSE;

    dcbSerialParams.BaudRate = CBR_38400;
    dcbSerialParams.ByteSize = 8;
    dcbSerialParams.StopBits = ONESTOPBIT;
    dcbSerialParams.Parity = NOPARITY;

    if (!SetCommState(serialHandle, &dcbSerialParams)) 
    {
        std::cerr << "Error setting serial port state" << std::endl;
        CloseHandle(serialHandle);
        return 1;
    }

    COMMTIMEOUTS timeouts = { 0 };
    timeouts.ReadIntervalTimeout = MAXDWORD;
    timeouts.ReadTotalTimeoutConstant = 0;
    timeouts.ReadTotalTimeoutMultiplier = 0;
    timeouts.WriteTotalTimeoutConstant = 0;
    timeouts.WriteTotalTimeoutMultiplier = 0;

    if (!SetCommTimeouts(serialHandle, &timeouts)) 
    {
        std::cerr << "Error setting serial port timeouts" << std::endl;
        CloseHandle(serialHandle);
        return 1;
    }

    if (!PurgeComm(serialHandle, PURGE_RXCLEAR | PURGE_TXCLEAR)) {
        std::cerr << "Error flushing serial port buffers" << std::endl;
        CloseHandle(serialHandle);
        return 1;
    }

    std::vector<std::string> atz_commands = {"ATZ\r", "ATI\r"};
    DWORD bytesWritten = 0;
    std::string response = "";
    std::string atz_command = "";

    for (int i = 0; i < atz_commands.size(); i++) 
    {
        bytesWritten = 0;
        atz_command = atz_commands[i];

        if (!WriteFile(serialHandle, atz_command.c_str(), atz_command.length(), &bytesWritten, NULL))
        {
            std::cerr << "Error writing to serial port" << std::endl;
            CloseHandle(serialHandle);
            return 1;
        }
        else
        {
            std::cout << "Successfully wrote to serial port" << std::endl;
            Sleep(2000);
            response = readFromSerialPort(serialHandle);
            std::cout << response << std::endl;
        }
    }

    CloseHandle(serialHandle);
    return 0;
}

This seems to work, I'm able to see outputs on the terminal (OK, ELM v1.5, etc.). So not sure if it's something to do with platform dependency.

Update I've used a terminal emulation program (Minicom, Screen) to try to communicate (eg. minicom -D /dev/cu.usbmodemxxxx and minicom -D /dev/tty.usbmodemxxxx. I'm able to go in but the screen is empty (whenever I type keys, nothing shows up on the screen). I do see the lights flash whenever I type anything, but the terminal shows nothing.

I've modified the C++ file to the following to add return codes and set parity bit, stop, and data, but still encounter the same issue:

int main() {
    const char *device_path = "/dev/cu.usbmodemxxxxx";
    
    struct termios options;
    int serial_port;
    
    serial_port = open(device_path, O_RDWR | O_NOCTTY | O_NDELAY);
    
    if (serial_port == -1)
    {
        std::cout << "Error opening serial port" << std::endl;
    }
    else
    {
        fcntl(serial_port, F_SETFL, 0);

        if (tcgetattr(serial_port, &options) == -1) {
            perror("Error getting terminal attributes");
            return 1;
        }
        else
        {
            std::cout << "Successfully executed tcgetattr." << std::endl;
        }

        options.c_cflag |= (CLOCAL | CREAD); 
        options.c_lflag &= ~(ICANON | ECHO | ECHOE | ISIG);
        options.c_oflag &= ~(OPOST);

        options.c_cflag &= ~PARENB;                     // Bitwise AND operator. No parity
        options.c_cflag &= ~CSTOPB;                     // 1 stop bit
        options.c_cflag &= ~CSIZE;                      // Clears the data size bits (CSIZE) to ensure a clean slate
        options.c_cflag |= CS8;                         // 8 data bits (This specifies eight bits per byte.)

        options.c_cc[VMIN] = 0;
        options.c_cc[VTIME] = 100;
        
        cfsetispeed(&options, B9600); // Tried with both B9600 and B38400.
        cfsetospeed(&options, B9600); // Tried with both B9600 and B38400.
        
        if (tcsetattr(serial_port, TCSANOW, &options) == -1)
        {
            perror("Error setting terminal attributes");
            return 1;
        }

        if (tcflush(serial_port, TCIOFLUSH) == -1)
        {
            perror("Error flushing terminal");
            return 1;
        }
        
        std::string atz_command = "ATI\r"; // I've also tried "ATZ\r", "ATI\n", "ATZ\n"
        ssize_t bytesWritten = write(serial_port, atz_command.c_str(), atz_command.length());
        if (bytesWritten == -1)
        {
            perror("Error writing to serial port.");
            return 1;
        }

        char buffer[64];
        ssize_t bytesRead = read(serial_port, buffer, 64);
        if (bytesRead == -1)
        {
            perror("Error reading from serial port.");
            return 1;
        }
        else
        {
            std::cout << "Buffer message: " << buffer << std::endl;
        }
    }

    close(serial_port);
    
    return 0;
}

This is the output I received:

Successfully opened serial port.
Successfully executed tcgetattr.
Successfully executed tcsetattr.
Successfully executed tcflush.
Successfully wrote to serial port: 4 bytes.
Successfully read from serial port: 0 bytes.
Buffer message: 

Upvotes: 0

Views: 248

Answers (1)

DrMickeyLauer
DrMickeyLauer

Reputation: 4684

I'm not entirely sure whether this is your problem, but with regards to serial communication, macOS exhibits ugly behavior in that a) it resets the configured bitrate once the file descriptor gets closed and b) it often takes multiple approaches to successfully configure the bitrate.

Here's example code that might be helpful: https://github.com/Cornucopia-Swift/CornucopiaStreams/blob/fbed1496f484b3db52595d9371ed4b86b6a21c7c/Sources/CornucopiaStreams/Streams/TTYInputStreamProxy.swift#L30

Upvotes: 0

Related Questions