Files
2023-03-17 11:40:49 +00:00

620 lines
16 KiB
C++

/* 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 <SetupAPI.h>
#include <Devpropdef.h>
#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 <sys/stat.h>
#include <dirent.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <errno.h>
#include <cstring>
#include <linux/serial.h>
#endif
#include <string>
#include <codecvt>
#include <locale>
using convert_t = std::codecvt_utf8<wchar_t>;
static std::wstring_convert<convert_t, wchar_t> 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<SerialPortInformation>& 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();
}