// serial_listen.cpp // // Opens a serial port and prints every byte read, one per line. // // Windows: cl /EHsc /std:c++17 serial_listen.cpp // mac/Linux: g++ -std=c++17 -O2 -o serial_listen serial_listen.cpp // // ./serial_listen /dev/cu.usbserial-14420 115200 // serial_listen.exe COM4 115200 // // macOS note: always use the /dev/cu.* name, not /dev/tty.*. The tty.* node // is the "call-in" device and its open() blocks until the peer asserts DCD, // which most USB-serial adapters never do. This program opens non-blocking // and auto-redirects tty.* -> cu.* so it can't hang, but cu.* is the name to // use by hand. #include #include #include #include #include #ifdef _WIN32 # include #else # include # include # include # include # include #endif class SerialPort { public: enum class Read { Data, Timeout, Error }; SerialPort() = default; ~SerialPort() { close(); } SerialPort(const SerialPort&) = delete; SerialPort& operator=(const SerialPort&) = delete; bool open(const std::string& portName, int baud, std::string& err); // Waits up to ~1 second for one byte. Read readByte(unsigned char& byte, std::string& err); void close(); private: #ifdef _WIN32 HANDLE handle_ = INVALID_HANDLE_VALUE; #else int fd_ = -1; #endif }; #ifdef _WIN32 static std::string lastWinError() { DWORD code = ::GetLastError(); char* msg = nullptr; ::FormatMessageA(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, nullptr, code, 0, reinterpret_cast(&msg), 0, nullptr); std::string out = msg ? msg : "unknown error"; if (msg) ::LocalFree(msg); while (!out.empty() && (out.back() == '\n' || out.back() == '\r')) out.pop_back(); return out + " (" + std::to_string(code) + ")"; } bool SerialPort::open(const std::string& portName, int baud, std::string& err) { // COM10 and above need the \\.\ prefix; it is harmless for COM1-COM9. std::string path = portName; if (path.rfind("\\\\.\\", 0) != 0) path = "\\\\.\\" + path; handle_ = ::CreateFileA(path.c_str(), GENERIC_READ | GENERIC_WRITE, 0, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr); if (handle_ == INVALID_HANDLE_VALUE) { err = "CreateFile(" + portName + "): " + lastWinError(); return false; } DCB dcb; std::memset(&dcb, 0, sizeof(dcb)); dcb.DCBlength = sizeof(dcb); if (!::GetCommState(handle_, &dcb)) { err = "GetCommState: " + lastWinError(); close(); return false; } dcb.BaudRate = static_cast(baud); dcb.ByteSize = 8; dcb.Parity = NOPARITY; dcb.StopBits = ONESTOPBIT; dcb.fBinary = TRUE; dcb.fParity = FALSE; dcb.fOutxCtsFlow = FALSE; // no hardware flow control dcb.fOutxDsrFlow = FALSE; dcb.fDtrControl = DTR_CONTROL_ENABLE; // assert DTR dcb.fRtsControl = RTS_CONTROL_ENABLE; // assert RTS dcb.fOutX = FALSE; // no XON/XOFF dcb.fInX = FALSE; dcb.fAbortOnError = FALSE; if (!::SetCommState(handle_, &dcb)) { err = "SetCommState: " + lastWinError(); close(); return false; } COMMTIMEOUTS to; std::memset(&to, 0, sizeof(to)); to.ReadTotalTimeoutConstant = 1000; if (!::SetCommTimeouts(handle_, &to)) { err = "SetCommTimeouts: " + lastWinError(); close(); return false; } ::PurgeComm(handle_, PURGE_RXCLEAR | PURGE_TXCLEAR); return true; } SerialPort::Read SerialPort::readByte(unsigned char& byte, std::string& err) { DWORD got = 0; if (!::ReadFile(handle_, &byte, 1, &got, nullptr)) { err = "ReadFile: " + lastWinError(); return Read::Error; } return (got == 1) ? Read::Data : Read::Timeout; } void SerialPort::close() { if (handle_ != INVALID_HANDLE_VALUE) { ::CloseHandle(handle_); handle_ = INVALID_HANDLE_VALUE; } } #else // POSIX (macOS, Linux) static bool baudToSpeed(int baud, speed_t& out) { switch (baud) { case 1200: out = B1200; return true; case 2400: out = B2400; return true; case 4800: out = B4800; return true; case 9600: out = B9600; return true; case 19200: out = B19200; return true; case 38400: out = B38400; return true; case 57600: out = B57600; return true; case 115200: out = B115200; return true; case 230400: out = B230400; return true; default: return false; } } bool SerialPort::open(const std::string& portName, int baud, std::string& err) { speed_t speed; if (!baudToSpeed(baud, speed)) { err = "Unsupported baud rate: " + std::to_string(baud); return false; } // macOS: /dev/tty.X is the call-in device and blocks waiting for carrier. // If the matching call-out device exists, use it instead. std::string path = portName; const std::string ttyPrefix = "/dev/tty."; if (path.rfind(ttyPrefix, 0) == 0) { std::string cu = "/dev/cu." + path.substr(ttyPrefix.size()); if (::access(cu.c_str(), F_OK) == 0) { std::cerr << "Note: switched to " << cu << " instead of " << path << " (tty.* blocks until DCD is asserted)\n"; path = cu; } } // O_NONBLOCK so open() returns immediately even without carrier; // O_NOCTTY so the port does not become our controlling terminal. fd_ = ::open(path.c_str(), O_RDWR | O_NOCTTY | O_NONBLOCK); if (fd_ < 0) { err = "open(" + path + "): " + std::strerror(errno); return false; } termios tty; std::memset(&tty, 0, sizeof(tty)); if (::tcgetattr(fd_, &tty) != 0) { err = std::string("tcgetattr: ") + std::strerror(errno); close(); return false; } cfmakeraw(&tty); cfsetispeed(&tty, speed); cfsetospeed(&tty, speed); tty.c_cflag |= (CLOCAL | CREAD); // ignore modem lines, enable receiver tty.c_cflag &= ~CRTSCTS; // no hardware flow control tty.c_iflag &= ~(IXON | IXOFF | IXANY); // no XON/XOFF tty.c_cc[VMIN] = 0; // timed read: return after VTIME... tty.c_cc[VTIME] = 10; // ...i.e. 10 deciseconds = 1 s if (::tcsetattr(fd_, TCSANOW, &tty) != 0) { err = std::string("tcsetattr: ") + std::strerror(errno); close(); return false; } if (::fcntl(fd_, F_SETFL, 0) != 0) { err = std::string("fcntl: ") + std::strerror(errno); close(); return false; } int bits = TIOCM_DTR | TIOCM_RTS; ::ioctl(fd_, TIOCMBIS, &bits); ::tcflush(fd_, TCIFLUSH); return true; } SerialPort::Read SerialPort::readByte(unsigned char& byte, std::string& err) { ssize_t n = ::read(fd_, &byte, 1); if (n == 1) return Read::Data; if (n == 0) return Read::Timeout; if (errno == EINTR || errno == EAGAIN) return Read::Timeout; err = std::string("read: ") + std::strerror(errno); return Read::Error; } void SerialPort::close() { if (fd_ >= 0) { ::close(fd_); fd_ = -1; } } #endif int main(int argc, char** argv) { if (argc < 2) { std::cerr << "Usage: " << argv[0] << " [baud]\n" << " e.g. " << argv[0] << " COM4 115200\n" << " " << argv[0] << " /dev/cu.usbserial-14420 115200\n"; return 1; } const std::string port = argv[1]; const int baud = (argc > 2) ? std::atoi(argv[2]) : 115200; SerialPort serial; std::string err; if (!serial.open(port, baud, err)) { std::cerr << "error: " << err << "\n"; return 1; } std::cerr << "Listening on " << port << " at " << baud << " baud (Ctrl-C to quit)\n"; for (;;) { unsigned char byte = 0; SerialPort::Read r = serial.readByte(byte, err); if (r == SerialPort::Read::Error) { std::cerr << "error: " << err << "\n"; return 1; } if (r == SerialPort::Read::Timeout) { continue; } std::printf("0x%02X %3u %c\n", byte, byte, (byte >= 0x20 && byte < 0x7F) ? static_cast(byte) : '.'); std::fflush(stdout); } }