/* ArduinoFloppyReader (and writer) * * Copyright (C) 2017-2021 Robert Smith (@RobSmithDev) * https://amiga.robsmithdev.co.uk * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 3 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 * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public * License along with this library; if not, see http://www.gnu.org/licenses/ */ //////////////////////////////////////////////////////////////////////////////////////// // Class to manage the communication between the computer and a serial port // //////////////////////////////////////////////////////////////////////////////////////// // // #include "SerialIO.h" #ifdef _WIN32 #include #include #include "RotationExtractor.h" #pragma comment(lib, "setupapi.lib") static const DEVPROPKEY DEVPKEY_Device_BusReportedDeviceDesc2 = { {0x540b947e, 0x8b40, 0x45bc, 0xa8, 0xa2, 0x6a, 0x0b, 0x89, 0x4c, 0xbd, 0xa2}, 4 }; static const DEVPROPKEY DEVPKEY_Device_InstanceId2 = { {0x78c34fc8, 0x104a, 0x4aca, 0x9e, 0xa4, 0x52, 0x4d, 0x52, 0x99, 0x6e, 0x57}, 256 }; #ifndef GUID_DEVINTERFACE_COMPORT DEFINE_GUID(GUID_DEVINTERFACE_COMPORT,0x86e0d1e0, 0x8089, 0x11d0, 0x9c, 0xe4, 0x08, 0x00, 0x3e, 0x30, 0x1f, 0x73); #endif #else #include #include #include #include #include #include #include #include #endif #include #include #include using convert_t = std::codecvt_utf8; static std::wstring_convert strconverter; void quickw2a(const std::wstring& wstr, std::string& str) { str = strconverter.to_bytes(wstr); } void quicka2w(const std::string& str, std::wstring& wstr) { wstr = strconverter.from_bytes(str); } // Constructor etc SerialIO::SerialIO() { } SerialIO::~SerialIO() { closePort(); } // Returns TRUE if the port is open bool SerialIO::isPortOpen() const { #ifdef _WIN32 return m_portHandle != INVALID_HANDLE_VALUE; #else return m_portHandle != -1; #endif return false; } // Purge any data left in the buffer void SerialIO::purgeBuffers() { if (!isPortOpen()) return; #ifdef _WIN32 PurgeComm(m_portHandle, PURGE_RXCLEAR | PURGE_TXCLEAR); #else tcflush(m_portHandle, TCIOFLUSH); #endif } // Sets the status of the DTR line void SerialIO::setDTR(bool enableDTR) { if (!isPortOpen()) return; #ifdef _WIN32 EscapeCommFunction(m_portHandle, enableDTR ? SETDTR : CLRDTR); #else int pinToControl = TIOCM_DTR; ioctl(m_portHandle, enableDTR ? TIOCMBIS : TIOCMBIC, &pinToControl); #endif } // Returns the status of the CTS pin bool SerialIO::getCTSStatus() { if (!isPortOpen()) return false; #ifdef _WIN32 DWORD mask = 0; if (!GetCommModemStatus(m_portHandle, &mask)) return false; return (mask & MS_CTS_ON) != 0; #else int status; ioctl(m_portHandle, TIOCMGET, &status); return (status & TIOCM_CTS) != 0; #endif return false; } // Returns a list of serial ports discovered on the system void SerialIO::enumSerialPorts(std::vector& serialPorts) { serialPorts.clear(); #ifdef _WIN32 // Query for hardware HDEVINFO hDevInfoSet = SetupDiGetClassDevs(&GUID_DEVINTERFACE_COMPORT, nullptr, nullptr, DIGCF_PRESENT | DIGCF_DEVICEINTERFACE); if (hDevInfoSet == INVALID_HANDLE_VALUE) return; // Scan for items DWORD devIndex = 0; SP_DEVINFO_DATA devInfo; devInfo.cbSize = sizeof(SP_DEVINFO_DATA); // Enum devices while (SetupDiEnumDeviceInfo(hDevInfoSet, devIndex, &devInfo)) { HKEY key = SetupDiOpenDevRegKey(hDevInfoSet, &devInfo, DICS_FLAG_GLOBAL, 0, DIREG_DEV, KEY_QUERY_VALUE); if (key != INVALID_HANDLE_VALUE) { WCHAR name[128]; DWORD nameLength = 128; // Get the COM Port Name if (RegQueryValueExW(key, L"PortName", NULL, NULL, (LPBYTE)name, &nameLength) == ERROR_SUCCESS) { SerialPortInformation port; port.portName = name; // Check it starts with COM if ((port.portName.length() >= 4) && (port.portName.substr(0, 3) == L"COM")) { // Get the hardware ID nameLength = 128; DWORD dwType; if (SetupDiGetDeviceRegistryPropertyW(hDevInfoSet, &devInfo, SPDRP_HARDWAREID, &dwType, (LPBYTE)name, 128, &nameLength)) { std::wstring deviceString = name; int a = (int)deviceString.find(L"VID_"); if (a != std::wstring::npos) port.vid = wcstol(deviceString.substr(a + 4).c_str(), NULL, 16); a = (int)deviceString.find(L"PID_"); if (a != std::wstring::npos) port.pid = wcstol(deviceString.substr(a + 4).c_str(), NULL, 16); } // Description DWORD type; if (SetupDiGetDeviceProperty(hDevInfoSet, &devInfo, &DEVPKEY_Device_BusReportedDeviceDesc2, &type, (PBYTE)name, 128, 0, 0)) port.productName = name; // Instance if (SetupDiGetDeviceProperty(hDevInfoSet, &devInfo, &DEVPKEY_Device_InstanceId2, &type, (PBYTE)name, 128, 0, 0)) port.instanceID = name; serialPorts.push_back(port); } } RegCloseKey(key); } devIndex++; } SetupDiDestroyDeviceInfoList(hDevInfoSet); #else DIR* dir = opendir("/sys/class/tty"); if (!dir) return; struct dirent* entry; struct stat statbuf; while ((entry = readdir(dir))) { std::string deviceRoot = "/sys/class/tty/" + std::string(entry->d_name); if (lstat(deviceRoot.c_str(), &statbuf) == -1) continue; if (!S_ISLNK(statbuf.st_mode)) deviceRoot += "/device"; char target[PATH_MAX]; int len = readlink(deviceRoot.c_str(), target, sizeof(target)); if ((len <= 0) || (len >= PATH_MAX-1)) continue; target[len] = '\0'; if (strstr(target, "virtual")) continue; if (strstr(target, "bluetooth")) continue; if (strstr(target, "usb")) { std::string name = "/dev/" + std::string(entry->d_name); SerialPortInformation prt; quicka2w(name, prt.portName); std::string dirName = "/sys/class/tty/" + std::string(entry->d_name) + "/device/"; std::string subDir = ""; for (int i=0; i<5; i++) { subDir += "../"; std::string vidPath = dirName + "/"+subDir+"/idVendor"; int file = open(vidPath.c_str(), O_RDONLY | O_CLOEXEC); if (file == -1) continue; FILE* fle = fdopen(file, "r"); int count = fscanf(fle, "%4x", &prt.vid); fclose(fle); if (count !=1) continue; vidPath = dirName + "/"+subDir+"/idProduct"; file = open(vidPath.c_str(), O_RDONLY | O_CLOEXEC); if (file == -1) continue; fle = fdopen(file, "r"); count = fscanf(fle, "%4x", &prt.pid); fclose(fle); if (count !=1) continue; vidPath = dirName + "/"+subDir+"/product"; file = open(vidPath.c_str(), O_RDONLY | O_CLOEXEC); if (file == -1) continue; fle = fdopen(file, "r"); char* p; if ((p = fgets(target, sizeof(target), fle))) { if (p[strlen(p)-1] == '\n') p[strlen(p)-1] = '\0'; quicka2w(target, prt.productName); } fclose(fle); vidPath = dirName + "/"+subDir+"/serial"; file = open(vidPath.c_str(), O_RDONLY | O_CLOEXEC); if (file == -1) continue; fle = fdopen(file, "r"); if ((p = fgets(target, sizeof(target), fle))) quicka2w(target, prt.instanceID); fclose(fle); serialPorts.push_back(prt); break; } } } closedir(dir); #endif } // Attempt ot change the size of the buffers used by the OS void SerialIO::setBufferSizes(const unsigned int rxSize, const unsigned int txSize) { if (!isPortOpen()) return; #ifdef _WIN32 SetupComm(m_portHandle, rxSize, txSize); #endif } // Open a port by name SerialIO::Response SerialIO::openPort(const std::wstring& portName) { closePort(); #ifdef _WIN32 std::wstring path = L"\\\\.\\" + portName; m_portHandle = CreateFileW(path.c_str(), GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, 0, 0); if (m_portHandle == INVALID_HANDLE_VALUE) { switch (GetLastError()) { case ERROR_FILE_NOT_FOUND: return Response::rNotFound; case ERROR_ACCESS_DENIED: return Response::rInUse; default: return Response::rUnknownError; } } updateTimeouts(); return Response::rOK; #else std::string apath; quickw2a(portName, apath); m_portHandle = open(apath.c_str(), O_RDWR | O_NOCTTY | O_NDELAY); if (m_portHandle == -1) { switch (errno) { case ENOENT: return Response::rNotFound; case EBUSY: return Response::rInUse; default: return Response::rUnknownError; } } #ifdef HAVE_FLOCK flock(m_portHandle, LOCK_EX | LOCK_NB); #endif #ifdef TIOCEXCL ioctl(m_portHandle, TIOCEXCL); #endif updateTimeouts(); return Response::rOK; #endif return Response::rNotImplemented; } // Shuts the port down void SerialIO::closePort() { if (!isPortOpen()) return; #ifdef _WIN32 CloseHandle(m_portHandle); m_portHandle = INVALID_HANDLE_VALUE; #else close(m_portHandle); m_portHandle = -1; #endif } // Changes the configuration on the port SerialIO::Response SerialIO::configurePort(const Configuration& configuration) { if (!isPortOpen()) return Response::rUnknownError; #ifdef _WIN32 // Configure the port COMMCONFIG config; DWORD comConfigSize = sizeof(config); memset(&config, 0, sizeof(config)); GetCommConfig(m_portHandle, &config, &comConfigSize); config.dwSize = sizeof(config); config.dcb.DCBlength = sizeof(config.dcb); config.dcb.BaudRate = configuration.baudRate; config.dcb.ByteSize = 8; config.dcb.fBinary = true; config.dcb.Parity = false; config.dcb.fOutxCtsFlow = configuration.ctsFlowControl; config.dcb.fOutxDsrFlow = false; config.dcb.fDtrControl = DTR_CONTROL_DISABLE; config.dcb.fDsrSensitivity = false; config.dcb.fNull = false; config.dcb.fTXContinueOnXoff = false; config.dcb.fRtsControl = RTS_CONTROL_DISABLE; config.dcb.fAbortOnError = false; config.dcb.StopBits = 0; config.dcb.fOutX = 0; config.dcb.fInX = 0; config.dcb.fErrorChar = 0; config.dcb.fAbortOnError = 0; config.dcb.fInX = 0; return SetCommConfig(m_portHandle, &config, sizeof(config)) ? Response::rOK : Response::rUnknownError; #else if (tcgetattr(m_portHandle, &term) < 0) return Response::rUnknownError; #ifdef cfmakeraw cfmakeraw(&term); #endif term.c_iflag &= ~ (IXON | IXOFF | IXANY | IGNBRK | BRKINT | PARMRK | INPCK | ISTRIP | INLCR | IGNCR | ICRNL); term.c_lflag &= ~ (ISIG | ICANON | ECHO | ECHOE | ECHONL | IEXTEN); term.c_oflag &= ~ (OPOST | ONLCR | OCRNL | ONOCR | ONLRET); term.c_cflag &= ~(HUPCL | CSIZE | PARENB | PARODD | CSTOPB); term.c_cflag |= CREAD | CLOCAL | CS8; term.c_iflag |= IGNPAR; #ifdef XCASE term.c_lflag &= ~XCASE; #endif #ifdef IUTF8 term.c_iflag &= ~IUTF8; #endif #ifdef CMSPAR term.c_cflag &= ~CMSPAR; #endif #ifdef IMAXBEL term.c_iflag &= ~IMAXBEL; #endif #ifdef IUCLC term.c_iflag &= ~IUCLC; #endif #ifdef OLCUC term.c_oflag &= ~OLCUC; #endif #ifdef NLDLY term.c_oflag &= ~NLDLY; #endif #ifdef CRDLY term.c_oflag &= ~CRDLY; #endif #ifdef TABDLY term.c_oflag &= ~TABDLY; #endif #ifdef BSDLY term.c_oflag &= ~BSDLY; #endif #ifdef VTDLY term.c_oflag &= ~VTDLY; #endif #ifdef FFDLY term.c_oflag &= ~FFDLY; #endif #ifdef OFILL term.c_oflag &= ~OFILL; #endif term.c_cc[VMIN] = 0; term.c_cc[VTIME] = 0; int ctsRtsFlags = 0; #ifdef CRTSCTS ctsRtsFlags = CRTSCTS; #else #ifdef CNEW_RTSCTS ctsRtsFlags = CNEW_RTSCTS; #else ctsRtsFlags = CCTS_OFLOW; #endif #endif if (configuration.ctsFlowControl) term.c_cflag |= ctsRtsFlags; else term.c_cflag &= ~ctsRtsFlags; // Now try to set the baud rate int baud = configuration.baudRate; #ifdef __APPLE__ if (ioctl(m_portHandle, IOSSIOSPEED, &baud) == -1) return Response::rUnknownError; #else if (baud == 9600) { term.c_cflag &= ~CBAUD; term.c_cflag |= B9600; } else { #if defined(BOTHER) && defined(HAVE_STRUCT_TERMIOS2) term.c_cflag &= ~CBAUD; term.c_cflag |= BOTHER; term.c_ispeed = configuration.baudRate; term.c_ospeed = configuration.baudRate; if (ioctl(m_portHandle, TCSETS2, &term) < 0) return Response::rUnknownError; #else term.c_cflag &= ~CBAUD; term.c_cflag |= CBAUDEX; if (cfsetspeed(&term, baud) < 0) return Response::rUnknownError; #endif #endif } // Apply that nonsense tcflush (m_portHandle, TCIFLUSH); if (tcsetattr(m_portHandle, TCSANOW, &term) != 0) return Response::rUnknownError; #ifdef ASYNC_LOW_LATENCY struct serial_struct serial; ioctl(m_portHandle, TIOCGSERIAL, &serial); serial.flags |= ASYNC_LOW_LATENCY; ioctl(m_portHandle, TIOCSSERIAL, &serial); #endif setDTR(false); return Response::rOK; #endif return Response::rNotImplemented; } // Returns the number of bytes waiting to be read unsigned int SerialIO::getBytesWaiting() { if (!isPortOpen()) return 0; #ifdef _WIN32 DWORD errors; COMSTAT comstatbuffer; if (!ClearCommError(m_portHandle, &errors, &comstatbuffer)) return 0; return comstatbuffer.cbInQue; #else int waiting; if (ioctl(m_portHandle, TIOCINQ, &waiting) < 0) return 0; return (unsigned int)waiting; #endif } // Attempts to write some data to the port. Returns how much it actually wrote. // If writeAll is not TRUE then it will write what it can until it times out unsigned int SerialIO::write(const void* data, unsigned int dataLength) { if ((data == nullptr) || (dataLength == 0)) return 0; if (!isPortOpen()) return 0; #ifdef _WIN32 DWORD written = 0; if (!WriteFile(m_portHandle, data, dataLength, &written, NULL)) written = 0; return written; #else unsigned int totalTime = m_writeTimeout + (m_writeTimeoutMultiplier * dataLength); struct timeval timeout; timeout.tv_sec = totalTime / 1000; timeout.tv_usec = (totalTime - (timeout.tv_sec * 1000)) * 1000; size_t written = 0; unsigned char* buffer = (unsigned char*)data; fd_set fds; FD_ZERO(&fds); FD_SET(m_portHandle, &fds); // Write with a timeout while (written < dataLength) { if ((timeout.tv_sec < 1) && (timeout.tv_usec < 1)) break; int result = select(m_portHandle + 1, NULL, &fds, NULL, &timeout); if (result < 0) { if (errno == EINTR) continue; else { return 0; } } else if (result == 0) break; result = ::write(m_portHandle, buffer, dataLength - written); if (result < 0) { if (errno == EAGAIN) continue; else { return 0; } } written += result; buffer += result; } return written; #endif return 0; } // Attempts to read some data from the port. Returns how much it actually read. // If readAll is TRUE then it will wait until all data has been read or an error occurs // Returns how mcuh it actually read unsigned int SerialIO::read(void* data, unsigned int dataLength) { if ((data == nullptr) || (dataLength == 0)) return 0; if (!isPortOpen()) return 0; #ifdef _WIN32 DWORD read = 0; if (!ReadFile(m_portHandle, data, dataLength, &read, NULL)) read = 0; return read; #else unsigned int totalTime = m_readTimeout + (m_readTimeoutMultiplier * dataLength); struct timeval timeout; timeout.tv_sec = totalTime / 1000; timeout.tv_usec = (totalTime - (timeout.tv_sec * 1000)) * 1000; size_t read = 0; unsigned char* buffer = (unsigned char*)data; fd_set fds; FD_ZERO(&fds); FD_SET(m_portHandle, &fds); while (read < dataLength) { if ((timeout.tv_sec < 1) && (timeout.tv_usec < 1)) { break; } int result = select(m_portHandle + 1, &fds, NULL, NULL, &timeout); if (result < 0) { if (errno == EINTR) continue; else break; } else if (result == 0) break; result = ::read(m_portHandle, buffer, dataLength - read); if (result < 0) { if (errno == EAGAIN) continue; else break; } read += result; buffer += result; } return read; #endif return 0; } // Update timeouts void SerialIO::updateTimeouts() { if (!isPortOpen()) return; #ifdef _WIN32 COMMTIMEOUTS tm; tm.ReadTotalTimeoutConstant = m_readTimeout; tm.ReadTotalTimeoutMultiplier = m_readTimeoutMultiplier; tm.ReadIntervalTimeout = 0; tm.WriteTotalTimeoutConstant = m_writeTimeout; tm.WriteTotalTimeoutMultiplier = m_writeTimeoutMultiplier; SetCommTimeouts(m_portHandle, &tm); #endif } // Sets the read timeouts. The actual timeout is calculated as waitTimetimeout + (multiplier * num bytes) void SerialIO::setReadTimeouts(unsigned int waitTimetimeout, unsigned int multiplier) { m_readTimeout = waitTimetimeout; m_readTimeoutMultiplier = multiplier; updateTimeouts(); } // Sets the write timeouts. The actual timeout is calculated as waitTimetimeout + (multiplier * num bytes) void SerialIO::setWriteTimeouts(unsigned int waitTimetimeout, unsigned int multiplier) { m_writeTimeout = waitTimetimeout; m_writeTimeoutMultiplier = multiplier; updateTimeouts(); }