Files
libicsneo/platform/windows/vcp.cpp
T
Kyle SchwarzandPaul Hollinsky a5b27a15b0 Allow disconnections to be signaled by drivers
This allows for disconnections to be detected quickly
where possible.

It also makes sure other driver errors aren't thrown
in the event of a disconnection.
2020-10-08 17:38:10 -04:00

440 lines
14 KiB
C++

#include "icsneo/platform/windows/ftdi.h"
#include "icsneo/platform/ftdi.h"
#include "icsneo/platform/registry.h"
#include <iostream>
#include <iomanip>
#include <sstream>
#include <cwctype>
#include <algorithm>
#include <codecvt>
#include <limits>
#include <stdio.h>
using namespace icsneo;
static std::wstring_convert<std::codecvt_utf8_utf16<wchar_t>> converter;
static const std::wstring DRIVER_SERVICES_REG_KEY = L"SYSTEM\\CurrentControlSet\\services\\";
static const std::wstring ALL_ENUM_REG_KEY = L"SYSTEM\\CurrentControlSet\\Enum\\";
static constexpr unsigned int RETRY_TIMES = 5;
static constexpr unsigned int RETRY_DELAY = 50;
std::vector<neodevice_t> VCP::FindByProduct(int product, std::vector<std::wstring> driverNames) {
std::vector<neodevice_t> found;
for(auto& driverName : driverNames) {
std::wstringstream regss;
regss << DRIVER_SERVICES_REG_KEY << driverName << L"\\Enum\\";
std::wstring driverEnumRegKey = regss.str();
uint32_t deviceCount = 0;
if(!Registry::Get(driverEnumRegKey, L"Count", deviceCount)) {
return found;
}
for(uint32_t i = 0; i < deviceCount; i++) {
neodevice_t device = {};
// First we want to look at what devices FTDI is enumerating (inside driverEnumRegKey)
// The entry for a ValueCAN 3 with SN 138635 looks like "FTDIBUS\VID_093C+PID_0601+138635A\0000"
// The entry for a ValueCAN 4 with SN V20227 looks like "USB\VID_093C&PID_1101\V20227"
std::wstringstream ss;
ss << i;
std::wstring entry;
if(!Registry::Get(driverEnumRegKey, ss.str(), entry))
continue;
std::transform(entry.begin(), entry.end(), entry.begin(), std::towupper);
std::wstringstream vss;
vss << "VID_" << std::setfill(L'0') << std::setw(4) << std::uppercase << std::hex << INTREPID_USB_VENDOR_ID; // Intrepid Vendor ID
if(entry.find(vss.str()) == std::wstring::npos)
continue;
std::wstringstream pss;
pss << "PID_" << std::setfill(L'0') << std::setw(4) << std::uppercase << std::hex << product;
auto pidpos = entry.find(pss.str());
if(pidpos == std::wstring::npos)
continue;
// Okay, this is a device we want
// Get the serial number
auto startchar = entry.find(L"+", pidpos + 1);
if(startchar == std::wstring::npos)
startchar = entry.find(L"\\", pidpos + 1);
bool conversionError = false;
int sn = 0;
try {
sn = std::stoi(entry.substr(startchar + 1));
}
catch(...) {
conversionError = true;
}
std::wstringstream oss;
if(!sn || conversionError)
oss << entry.substr(startchar + 1, 6); // This is a device with characters in the serial number
else
oss << sn;
std::string serial = converter.to_bytes(oss.str());
// The serial number should not have a path slash in it. If it does, that means we don't have the real serial.
if(serial.find_first_of('\\') != std::string::npos) {
// The serial number was not in the first serenum key where we expected it.
// We can try to match the ContainerID with the one in ALL_ENUM\USB and get a serial that way
std::wstringstream uess;
uess << ALL_ENUM_REG_KEY << L"\\USB\\" << vss.str() << L'&' << pss.str() << L'\\';
std::wstringstream ciss;
ciss << ALL_ENUM_REG_KEY << entry;
std::wstring containerIDFromEntry, containerIDFromEnum;
if(!Registry::Get(ciss.str(), L"ContainerID", containerIDFromEntry))
continue; // We did not get a container ID. This can happen on Windows XP and before.
if(containerIDFromEntry.empty())
continue; // The container ID was empty?
std::vector<std::wstring> subkeys;
if(!Registry::EnumerateSubkeys(uess.str(), subkeys))
continue; // VID/PID combo was not present at all.
if(subkeys.empty())
continue; // No devices for VID/PID.
std::wstring correctSerial;
for(auto& subkey : subkeys) {
std::wstringstream skss;
skss << uess.str() << L'\\' << subkey;
if(!Registry::Get(skss.str(), L"ContainerID", containerIDFromEnum))
continue;
if(containerIDFromEntry != containerIDFromEnum)
continue;
correctSerial = subkey;
break;
}
if(correctSerial.empty())
continue; // Didn't find the device within the subkeys of the enumeration
sn = 0;
conversionError = false;
try {
sn = std::stoi(correctSerial);
}
catch(...) {
conversionError = true;
}
if(!sn || conversionError) {
// This is a device with characters in the serial number
if(correctSerial.size() != 6)
continue;
serial = converter.to_bytes(correctSerial);
}
else {
std::wstringstream soss;
soss << sn;
serial = converter.to_bytes(soss.str());
}
if(serial.find_first_of('\\') != std::string::npos)
continue;
}
strcpy_s(device.serial, sizeof(device.serial), serial.c_str());
// Serial number is saved, we want the COM port number now
// This will be stored under ALL_ENUM_REG_KEY\entry\Device Parameters\PortName (entry from the FTDI_ENUM)
std::wstringstream dpss;
dpss << ALL_ENUM_REG_KEY << entry << L"\\Device Parameters";
std::wstring port;
Registry::Get(dpss.str(), L"PortName", port); // TODO If error do something else (Plasma maybe?)
std::transform(port.begin(), port.end(), port.begin(), std::towupper);
auto compos = port.find(L"COM");
device.handle = 0;
if(compos != std::wstring::npos) {
try {
device.handle = std::stoi(port.substr(compos + 3));
}
catch(...) {} // In case of this, or any other error, handle has already been initialized to 0
}
bool alreadyFound = false;
neodevice_t* shouldReplace = nullptr;
for(auto& foundDev : found) {
if((foundDev.handle == device.handle || foundDev.handle == 0 || device.handle == 0) && serial == foundDev.serial) {
alreadyFound = true;
if(foundDev.handle == 0)
shouldReplace = &foundDev;
break;
}
}
if(!alreadyFound)
found.push_back(device);
else if(shouldReplace != nullptr)
*shouldReplace = device;
}
}
return found;
}
bool VCP::IsHandleValid(neodevice_handle_t handle) {
if(handle < 1)
return false;
if(handle > 256) // Windows default max COM port is COM256
return false; // TODO Enumerate subkeys of HKLM\HARDWARE\DEVICEMAP\SERIALCOMM as a user might have more serial ports somehow
return true;
}
bool VCP::open(bool fromAsync) {
if(isOpen() || (!fromAsync && opening)) {
report(APIEvent::Type::DeviceCurrentlyOpen, APIEvent::Severity::Error);
return false;
}
if(!IsHandleValid(device.handle)) {
report(APIEvent::Type::DriverFailedToOpen, APIEvent::Severity::Error);
return false;
}
opening = true;
std::wstringstream comss;
comss << L"\\\\.\\COM" << device.handle;
// We're going to attempt to open 5 (RETRY_TIMES) times in a row
for(int i = 0; !isOpen() && i < RETRY_TIMES; i++) {
handle = CreateFileW(comss.str().c_str(), GENERIC_READ | GENERIC_WRITE, 0, nullptr, OPEN_EXISTING, FILE_FLAG_OVERLAPPED, nullptr);
if(GetLastError() == ERROR_SUCCESS)
break; // We have the file handle
std::this_thread::sleep_for(std::chrono::milliseconds(RETRY_DELAY));
}
opening = false;
if(!isOpen()) {
report(APIEvent::Type::DriverFailedToOpen, APIEvent::Severity::Error);
return false;
}
// Set the timeouts
COMMTIMEOUTS timeouts;
if(!GetCommTimeouts(handle, &timeouts)) {
close();
report(APIEvent::Type::DriverFailedToOpen, APIEvent::Severity::Error);
return false;
}
// See https://docs.microsoft.com/en-us/windows/desktop/api/winbase/ns-winbase-_commtimeouts#remarks
timeouts.ReadIntervalTimeout = MAXDWORD;
timeouts.ReadTotalTimeoutMultiplier = MAXDWORD;
timeouts.ReadTotalTimeoutConstant = 100;
timeouts.WriteTotalTimeoutConstant = 10000;
timeouts.WriteTotalTimeoutMultiplier = 0;
if(!SetCommTimeouts(handle, &timeouts)) {
close();
report(APIEvent::Type::DriverFailedToOpen, APIEvent::Severity::Error);
return false;
}
// Set the COM state
DCB comstate;
if(!GetCommState(handle, &comstate)) {
close();
report(APIEvent::Type::DriverFailedToOpen, APIEvent::Severity::Error);
return false;
}
comstate.BaudRate = 115200;
comstate.ByteSize = 8;
comstate.Parity = NOPARITY;
comstate.StopBits = 0;
comstate.fDtrControl = DTR_CONTROL_ENABLE;
comstate.fRtsControl = RTS_CONTROL_ENABLE;
if(!SetCommState(handle, &comstate)) {
close();
report(APIEvent::Type::DriverFailedToOpen, APIEvent::Severity::Error);
return false;
}
PurgeComm(handle, PURGE_RXCLEAR);
// Set up events so that overlapped IO can work with them
overlappedRead.hEvent = CreateEvent(nullptr, false, false, nullptr);
overlappedWrite.hEvent = CreateEvent(nullptr, false, false, nullptr);
overlappedWait.hEvent = CreateEvent(nullptr, true, false, nullptr);
if (overlappedRead.hEvent == nullptr || overlappedWrite.hEvent == nullptr || overlappedWait.hEvent == nullptr) {
close();
report(APIEvent::Type::DriverFailedToOpen, APIEvent::Severity::Error);
return false;
}
// Set up event so that we will satisfy overlappedWait when a character comes in
if(!SetCommMask(handle, EV_RXCHAR)) {
close();
report(APIEvent::Type::DriverFailedToOpen, APIEvent::Severity::Error);
return false;
}
// TODO Set up some sort of shared memory, save which COM port we have open so we don't try to open it again
// Create threads
readThread = std::thread(&VCP::readTask, this);
writeThread = std::thread(&VCP::writeTask, this);
return true;
}
void VCP::openAsync(fn_boolCallback callback) {
threads.push_back(std::make_shared<std::thread>([&]() {
callback(open(true));
}));
}
bool VCP::close() {
if(!isOpen()) {
report(APIEvent::Type::DeviceCurrentlyClosed, APIEvent::Severity::Error);
return false;
}
closing = true; // Signal the threads that we are closing
for(auto& t : threads)
t->join(); // Wait for the threads to close
readThread.join();
writeThread.join();
closing = false;
if(!CloseHandle(handle)) {
report(APIEvent::Type::DriverFailedToClose, APIEvent::Severity::Error);
return false;
}
handle = INVALID_HANDLE_VALUE;
bool ret = true; // If one of the events fails closing, we probably still want to try and close the others
if(overlappedRead.hEvent != INVALID_HANDLE_VALUE) {
if(!CloseHandle(overlappedRead.hEvent))
ret = false;
overlappedRead.hEvent = INVALID_HANDLE_VALUE;
}
if(overlappedWrite.hEvent != INVALID_HANDLE_VALUE) {
if(!CloseHandle(overlappedWrite.hEvent))
ret = false;
overlappedWrite.hEvent = INVALID_HANDLE_VALUE;
}
if(overlappedWait.hEvent != INVALID_HANDLE_VALUE) {
if(!CloseHandle(overlappedWait.hEvent))
ret = false;
overlappedWait.hEvent = INVALID_HANDLE_VALUE;
}
uint8_t flush;
WriteOperation flushop;
while(readQueue.try_dequeue(flush)) {}
while(writeQueue.try_dequeue(flushop)) {}
if(!ret)
report(APIEvent::Type::DriverFailedToClose, APIEvent::Severity::Error);
// TODO Set up some sort of shared memory, free which COM port we had open so we can try to open it again
return ret;
}
void VCP::readTask() {
constexpr size_t READ_BUFFER_SIZE = 10240;
uint8_t readbuf[READ_BUFFER_SIZE];
IOTaskState state = LAUNCH;
DWORD bytesRead = 0;
EventManager::GetInstance().downgradeErrorsOnCurrentThread();
while(!closing && !isDisconnected()) {
switch(state) {
case LAUNCH: {
COMSTAT comStatus;
unsigned long errorCodes;
ClearCommError(handle, &errorCodes, &comStatus);
bytesRead = 0;
if(ReadFile(handle, readbuf, READ_BUFFER_SIZE, nullptr, &overlappedRead)) {
if(GetOverlappedResult(handle, &overlappedRead, &bytesRead, FALSE)) {
if(bytesRead)
readQueue.enqueue_bulk(readbuf, bytesRead);
}
continue;
}
auto lastError = GetLastError();
if(lastError == ERROR_IO_PENDING)
state = WAIT;
else if(lastError != ERROR_SUCCESS) {
if(lastError == ERROR_ACCESS_DENIED) {
if(!isDisconnected()) {
disconnected = true;
report(APIEvent::Type::DeviceDisconnected, APIEvent::Severity::Error);
}
} else
report(APIEvent::Type::FailedToRead, APIEvent::Severity::Error);
}
}
break;
case WAIT: {
auto ret = WaitForSingleObject(overlappedRead.hEvent, 100);
if(ret == WAIT_OBJECT_0) {
if(GetOverlappedResult(handle, &overlappedRead, &bytesRead, FALSE)) {
readQueue.enqueue_bulk(readbuf, bytesRead);
state = LAUNCH;
} else
report(APIEvent::Type::FailedToRead, APIEvent::Severity::Error);
}
if(ret == WAIT_ABANDONED || ret == WAIT_FAILED) {
state = LAUNCH;
report(APIEvent::Type::FailedToRead, APIEvent::Severity::Error);
}
}
}
}
}
void VCP::writeTask() {
IOTaskState state = LAUNCH;
VCP::WriteOperation writeOp;
DWORD bytesWritten = 0;
EventManager::GetInstance().downgradeErrorsOnCurrentThread();
while(!closing && !isDisconnected()) {
switch(state) {
case LAUNCH: {
if(!writeQueue.wait_dequeue_timed(writeOp, std::chrono::milliseconds(100)))
continue;
bytesWritten = 0;
if(WriteFile(handle, writeOp.bytes.data(), (DWORD)writeOp.bytes.size(), nullptr, &overlappedWrite))
continue;
auto winerr = GetLastError();
if(winerr == ERROR_IO_PENDING) {
state = WAIT;
}
else if(winerr == ERROR_ACCESS_DENIED) {
if(!isDisconnected()) {
disconnected = true;
report(APIEvent::Type::DeviceDisconnected, APIEvent::Severity::Error);
}
} else
report(APIEvent::Type::FailedToWrite, APIEvent::Severity::Error);
}
break;
case WAIT: {
auto ret = WaitForSingleObject(overlappedWrite.hEvent, 50);
if(ret == WAIT_OBJECT_0) {
if(!GetOverlappedResult(handle, &overlappedWrite, &bytesWritten, FALSE))
report(APIEvent::Type::FailedToWrite, APIEvent::Severity::Error);
state = LAUNCH;
}
if(ret == WAIT_ABANDONED) {
report(APIEvent::Type::FailedToWrite, APIEvent::Severity::Error);
state = LAUNCH;
}
}
}
}
}