From ef34959c91bc7ee96d9ad08c9a90d00a12d78521 Mon Sep 17 00:00:00 2001 From: Paul Hollinsky Date: Fri, 14 Feb 2020 23:18:34 -0500 Subject: [PATCH] STM32 device finder for Darwin --- CMakeLists.txt | 19 ++- include/icsneo/platform/posix/stm32.h | 13 +- platform/posix/darwin/stm32darwin.cpp | 190 ++++++++++++++++++++++++ platform/posix/linux/stm32linux.cpp | 202 ++++++++++++++++++++++++++ platform/posix/stm32.cpp | 190 ++---------------------- 5 files changed, 428 insertions(+), 186 deletions(-) create mode 100644 platform/posix/darwin/stm32darwin.cpp create mode 100644 platform/posix/linux/stm32linux.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 460696e..6dc5b95 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -80,13 +80,24 @@ if(LIBICSNEO_BUILD_DOCS) endif() endif() - -if(WIN32) +if(${CMAKE_SYSTEM_NAME} STREQUAL "Windows") file(GLOB PLATFORM_SRC_EXTERNAL ${CMAKE_CURRENT_SOURCE_DIR}/platform/windows/*.cpp) file(GLOB PLATFORM_SRC_INTERNAL ${CMAKE_CURRENT_SOURCE_DIR}/platform/windows/internal/*.cpp) set(PLATFORM_SRC ${PLATFORM_SRC_EXTERNAL} ${PLATFORM_SRC_INTERNAL}) -else() - file(GLOB PLATFORM_SRC ${CMAKE_CURRENT_SOURCE_DIR}/platform/posix/*.cpp) +elseif(${CMAKE_SYSTEM_NAME} STREQUAL "Darwin") + file(GLOB PLATFORM_SRC_DARWIN ${CMAKE_CURRENT_SOURCE_DIR}/platform/posix/darwin/*.cpp) + file(GLOB PLATFORM_SRC_POSIX ${CMAKE_CURRENT_SOURCE_DIR}/platform/posix/*.cpp) + set(PLATFORM_SRC ${PLATFORM_SRC_POSIX} ${PLATFORM_SRC_DARWIN}) +else() # elseif(${CMAKE_SYSTEM_NAME} STREQUAL "Linux") + file(GLOB PLATFORM_SRC_LINUX ${CMAKE_CURRENT_SOURCE_DIR}/platform/posix/linux/*.cpp) + file(GLOB PLATFORM_SRC_POSIX ${CMAKE_CURRENT_SOURCE_DIR}/platform/posix/*.cpp) + set(PLATFORM_SRC ${PLATFORM_SRC_POSIX} ${PLATFORM_SRC_LINUX}) + if(NOT ${CMAKE_SYSTEM_NAME} STREQUAL "Linux") + message(WARNING + "There is no platform port defined for ${CMAKE_SYSTEM_NAME}!\n" + "The Linux platform code will be used, as it will generally allow building, but some devices may not enumerate properly." + ) + endif() endif() set(COMMON_SRC diff --git a/include/icsneo/platform/posix/stm32.h b/include/icsneo/platform/posix/stm32.h index 4be0567..0690283 100644 --- a/include/icsneo/platform/posix/stm32.h +++ b/include/icsneo/platform/posix/stm32.h @@ -11,6 +11,16 @@ namespace icsneo { class STM32 : public ICommunication { public: + /* + * Note: This is a driver for all devices which use CDC_ACM + * Once we find the TTY we want it's a pretty generic POSIX TTY driver, but + * the method for finding the TTY we want varies by OS. + * On Linux, we read sysfs to find users of the CDC_ACM driver + * On macOS, we use IOKit to find the USB device we're looking for + * As such platform specific FindByProduct & HandleToTTY code can be found + * in stm32linux.cpp and stm32darwin.cpp respectively + * Other POSIX systems (BSDs, QNX, etc) will need bespoke code written in the future + */ STM32(const device_eventhandler_t& err, neodevice_t& forDevice) : ICommunication(err), device(forDevice) {} static std::vector FindByProduct(int product); @@ -21,7 +31,8 @@ public: private: neodevice_t& device; int fd = -1; - static constexpr neodevice_handle_t HANDLE_OFFSET = 10; + + static std::string HandleToTTY(neodevice_handle_t handle); void readTask(); void writeTask(); diff --git a/platform/posix/darwin/stm32darwin.cpp b/platform/posix/darwin/stm32darwin.cpp new file mode 100644 index 0000000..12ba134 --- /dev/null +++ b/platform/posix/darwin/stm32darwin.cpp @@ -0,0 +1,190 @@ +#include "icsneo/platform/stm32.h" +#include +#include +#include +#include +#include + +using namespace icsneo; + +/* The index into the TTY table starts at zero, but we want to keep zero + * for an undefined handle, so add a constant. + */ +static constexpr const neodevice_handle_t HANDLE_OFFSET = 10; + +static std::mutex ttyTableMutex; +static std::vector ttyTable; + +static neodevice_handle_t TTYToHandle(const std::string& tty) { + std::lock_guard lk(ttyTableMutex); + for(size_t i = 0; i < ttyTable.size(); i++) { + if(ttyTable[i] == tty) + return neodevice_handle_t(i + HANDLE_OFFSET); + } + ttyTable.push_back(tty); + return neodevice_handle_t(ttyTable.size() - 1 + HANDLE_OFFSET); +} + +static std::string CFStringToString(CFStringRef cfString) { + if(cfString == nullptr) + return std::string(); + // As an optimization, we can try to directly read the CFString's CStringPtr + // CoreFoundation will seemingly not lift a finger if the pointer is not readily available + // so it will often return nullptr and we'll have to get the string "the hard way" + const char* cstr = CFStringGetCStringPtr(cfString, kCFStringEncodingUTF8); + if(cstr != nullptr) + return std::string(cstr); + // "The hard way" + // CFStringGetLength returns the length in UTF-16 Code Points + // CFStringGetCString will convert to UTF-8 for us so long as we give it enough space + const int len = CFStringGetLength(cfString); + if(len <= 0) + return std::string(); + std::vector utf8data; + utf8data.resize(len * 4 + 1); // UTF-16 => UTF-8, 1 code point can become up to 4 bytes, plus NUL + if(!CFStringGetCString(cfString, utf8data.data(), utf8data.size(), kCFStringEncodingUTF8)) + return std::string(); + return std::string(utf8data.data()); +} + +class CFReleaser { +public: + CFReleaser(CFTypeRef obj) : toRelease(obj) {} + ~CFReleaser() { + if(toRelease != nullptr) + CFRelease(toRelease); + } + CFReleaser(CFReleaser const&) = delete; + CFReleaser& operator=(CFReleaser const&) = delete; + +private: + CFTypeRef toRelease; +}; + +class IOReleaser { +public: + IOReleaser(io_object_t obj) : toRelease(obj) {} + ~IOReleaser() { + if(toRelease != 0) + IOObjectRelease(toRelease); + } + IOReleaser(IOReleaser&& moved) : toRelease(moved.toRelease) { + moved.toRelease = 0; + } + IOReleaser(IOReleaser const&) = delete; + IOReleaser& operator=(IOReleaser const&) = delete; + +private: + io_object_t toRelease; +}; + +std::vector STM32::FindByProduct(int product) { + std::vector found; + + CFMutableDictionaryRef ref = IOServiceMatching(kIOSerialBSDServiceValue); + if(ref == nullptr) + return found; + io_iterator_t matchingServices = 0; + kern_return_t kernResult = IOServiceGetMatchingServices(kIOMasterPortDefault, ref, &matchingServices); + if(KERN_SUCCESS != kernResult || matchingServices == 0) + return found; + IOReleaser matchingServicesReleaser(matchingServices); + + io_object_t serialPort; + while((serialPort = IOIteratorNext(matchingServices))) { + IOReleaser serialPortReleaser(serialPort); + neodevice_t device; + + // First get the parent device + // We want to check that it has the right VID/PID + + // Find the parent structure that describes the USB device providing this port + io_object_t parent = 0; + io_object_t current = serialPort; + io_object_t usb = 0; + // Need to release every parent we find in the chain, but we should defer that until later + std::vector releasers; + const std::string usbClass = "IOUSBDevice"; + while(IORegistryEntryGetParentEntry(current, kIOServicePlane, &parent) == KERN_SUCCESS) { + releasers.emplace_back(parent); + current = parent; + io_name_t className; + // io_name_t does not need to be freed, it's just a stack char[128] + if(IOObjectGetClass(parent, className) != KERN_SUCCESS) + continue; + if(std::string(className) == usbClass) { + usb = parent; + break; + } + } + if(!usb) // Did not find a USB parent (maybe this is a Bluetooth modem or something) + continue; + + // Get the VID + CFTypeRef vendorProp = IORegistryEntryCreateCFProperty(usb, CFSTR("idVendor"), kCFAllocatorDefault, 0); + if(vendorProp == nullptr) + continue; + CFReleaser vendorPropReleaser(vendorProp); + if(CFGetTypeID(vendorProp) != CFNumberGetTypeID()) + continue; + uint16_t vid = 0; + if(!CFNumberGetValue(static_cast(vendorProp), kCFNumberSInt16Type, &vid)) + continue; + if(vid != INTREPID_USB_VENDOR_ID) + continue; + + // Get the PID + CFTypeRef productProp = IORegistryEntryCreateCFProperty(usb, CFSTR("idProduct"), kCFAllocatorDefault, 0); + if(productProp == nullptr) + continue; + CFReleaser productPropReleaser(productProp); + if(CFGetTypeID(productProp) != CFNumberGetTypeID()) + continue; + uint16_t pid = 0; + if(!CFNumberGetValue(static_cast(productProp), kCFNumberSInt16Type, &pid)) + continue; + if(pid != product) + continue; + + // Now, let's get the "call-out" device (/dev/cu.*) + // We get the /dev/cu.* variant instead of the /dev/tty.* variant because the device will not assert DCD + // Therefore, the open call on /dev/tty.* will hang, whereas /dev/cu.* will not + // This `propertyValue` will need to be freed, that will happen in CFStringToString + CFTypeRef calloutProp = IORegistryEntryCreateCFProperty(serialPort, CFSTR(kIOCalloutDeviceKey), kCFAllocatorDefault, 0); + if(calloutProp == nullptr) + continue; + CFReleaser calloutPropReleaser(calloutProp); + if(CFGetTypeID(calloutProp) != CFStringGetTypeID()) + continue; + // We can static cast here because we have verified the type to be a CFString + const std::string tty = CFStringToString(static_cast(calloutProp)); + if(tty.empty()) + continue; + device.handle = TTYToHandle(tty); + + // Last but not least, get the serial number + CFTypeRef serialProp = IORegistryEntryCreateCFProperty(usb, CFSTR("kUSBSerialNumberString"), kCFAllocatorDefault, 0); + if(serialProp == nullptr) + continue; + CFReleaser serialPropReleaser(serialProp); + if(CFGetTypeID(serialProp) != CFStringGetTypeID()) + continue; + // We can static cast here because we have verified the type to be a CFString + const std::string serial = CFStringToString(static_cast(serialProp)); + if(serial.empty()) + continue; + device.serial[serial.copy(device.serial, sizeof(device.serial)-1)] = '\0'; + + found.push_back(device); + } + + return found; +} + +std::string STM32::HandleToTTY(neodevice_handle_t handle) { + std::lock_guard lk(ttyTableMutex); + const size_t index = size_t(handle - HANDLE_OFFSET); + if(index >= ttyTable.size()) + return ""; // Not found, generic driver (stm32.cpp) will throw an error + return ttyTable[index]; +} \ No newline at end of file diff --git a/platform/posix/linux/stm32linux.cpp b/platform/posix/linux/stm32linux.cpp new file mode 100644 index 0000000..c36e998 --- /dev/null +++ b/platform/posix/linux/stm32linux.cpp @@ -0,0 +1,202 @@ +#include "icsneo/platform/stm32.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace icsneo; + +/* The TTY numbering starts at zero, but we want to keep zero for an undefined + * handle, so add a constant. + */ +static constexpr const neodevice_handle_t HANDLE_OFFSET = 10; + +class Directory { +public: + class Listing { + public: + Listing(std::string newName, uint8_t newType) : name(newName), type(newType) {} + const std::string& getName() const { return name; } + uint8_t getType() const { return type; } + private: + std::string name; + uint8_t type; + }; + Directory(std::string directory) { + dir = opendir(directory.c_str()); + } + ~Directory() { + if(openedSuccessfully()) + closedir(dir); + dir = nullptr; + } + bool openedSuccessfully() { return dir != nullptr; } + std::vector ls() { + std::vector results; + struct dirent* entry; + while((entry = readdir(dir)) != nullptr) { + std::string name = entry->d_name; + if(name != "." && name != "..") // Ignore parent and self + results.emplace_back(name, entry->d_type); + } + return results; + } +private: + DIR* dir; +}; + +class USBSerialGetter { +public: + USBSerialGetter(std::string usbid) { + std::stringstream ss; + auto colonpos = usbid.find(":"); + if(colonpos == std::string::npos) { + succeeded = false; + return; + } + + ss << "/sys/bus/usb/devices/" << usbid.substr(0, colonpos) << "/serial"; + try { + std::ifstream reader(ss.str()); + std::getline(reader, serial); + } catch(...) { + succeeded = false; + return; + } + + succeeded = true; + } + bool success() const { return succeeded; } + const std::string& getSerial() const { return serial; } +private: + bool succeeded; + std::string serial; +}; + +std::vector STM32::FindByProduct(int product) { + std::vector found; + + Directory directory("/sys/bus/usb/drivers/cdc_acm"); // Query the STM32 driver + if(!directory.openedSuccessfully()) + return found; + + std::vector foundusbs; + for(auto& entry : directory.ls()) { + /* This directory will have directories (links) for all devices using the cdc_acm driver (as STM32 devices do) + * There will also be other files and directories providing information about the driver in here. We want to ignore them. + * Devices will be named like "7-2:1.0" where 7 is the enumeration for the USB controller, 2 is the device enumeration on + * that specific controller (will change if the device is unplugged and replugged), 1 is the device itself and 0 is + * enumeration for different services provided by the device. We're looking for the service that provides TTY. + * For now we find the directories with a digit for the first character, these are likely to be our USB devices. + */ + if(isdigit(entry.getName()[0]) && entry.getType() == DT_LNK) + foundusbs.emplace_back(entry.getName()); + } + + // Pair the USB and TTY if found + std::map foundttys; + for(auto& usb : foundusbs) { + std::stringstream ss; + ss << "/sys/bus/usb/drivers/cdc_acm/" << usb << "/tty"; + Directory devicedir(ss.str()); + if(!devicedir.openedSuccessfully()) // The tty directory doesn't exist, because this is not the tty service we want + continue; + + auto listing = devicedir.ls(); + if(listing.size() != 1) // We either got no serial ports or multiple, either way no good + continue; + + foundttys.insert(std::make_pair(usb, listing[0].getName())); + } + + // We're going to remove from the map if this is not the product we're looking for + for(auto iter = foundttys.begin(); iter != foundttys.end(); ) { + const auto& dev = *iter; + const std::string matchString = "PRODUCT="; + std::stringstream ss; + ss << "/sys/class/tty/" << dev.second << "/device/uevent"; // Read the uevent file, which contains should have a line like "PRODUCT=93c/1101/100" + std::ifstream fs(ss.str()); + std::string productLine; + size_t pos = std::string::npos; + do { + std::getline(fs, productLine, '\n'); + } while(((pos = productLine.find(matchString)) == std::string::npos) && !fs.eof()); + + if(pos != 0) { // We did not find a product line... weird + iter = foundttys.erase(iter); // Remove the element, this also moves iter forward for us + continue; + } + + size_t firstSlashPos = productLine.find('/', matchString.length()); + if(firstSlashPos == std::string::npos) { + iter = foundttys.erase(iter); + continue; + } + size_t pidpos = firstSlashPos + 1; + + std::string vidstr = productLine.substr(matchString.length(), firstSlashPos - matchString.length()); + std::string pidstr = productLine.substr(pidpos, productLine.find('/', pidpos) - pidpos); // In hex like "1101" or "93c" + + uint16_t vid, pid; + try { + vid = (uint16_t)std::stoul(vidstr, nullptr, 16); + pid = (uint16_t)std::stoul(pidstr, nullptr, 16); + } catch(...) { + iter = foundttys.erase(iter); // We could not parse the numbers + continue; + } + + if(vid != INTREPID_USB_VENDOR_ID || pid != product) { + iter = foundttys.erase(iter); // Not the right VID or PID, remove + continue; + } + iter++; // If the loop ends without erasing the iter from the map, the item is good + } + + // At this point, foundttys contains the the devices we want + + // Get the serial number, create the neodevice_t + for(auto& dev : foundttys) { + neodevice_t device; + + USBSerialGetter getter(dev.first); + if(!getter.success()) + continue; // Failure, could not get serial number + + // In ttyACM0, we want the i to be the first character of the number + size_t i; + for(i = 0; i < dev.second.length(); i++) { + if(isdigit(dev.second[i])) + break; + } + // Now we try to parse the number so we have a handle for later + try { + device.handle = (neodevice_handle_t)std::stoul(dev.second.substr(i)); + /* The TTY numbering starts at zero, but we want to keep zero for an undefined + * handle, so add a constant, and we'll subtract that constant in the open function. + */ + device.handle += HANDLE_OFFSET; + } catch(...) { + continue; // Somehow this failed, have to toss the device + } + + device.serial[getter.getSerial().copy(device.serial, sizeof(device.serial)-1)] = '\0'; + + found.push_back(device); // Finally, add device to search results + } + + return found; +} + +std::string STM32::HandleToTTY(neodevice_handle_t handle) { + std::stringstream ss; + ss << "/dev/ttyACM" << (int)(handle - HANDLE_OFFSET); + return ss.str(); +} \ No newline at end of file diff --git a/platform/posix/stm32.cpp b/platform/posix/stm32.cpp index 065b0f2..159fceb 100644 --- a/platform/posix/stm32.cpp +++ b/platform/posix/stm32.cpp @@ -13,193 +13,21 @@ using namespace icsneo; -class Directory { -public: - class Listing { - public: - Listing(std::string newName, uint8_t newType) : name(newName), type(newType) {} - const std::string& getName() const { return name; } - uint8_t getType() const { return type; } - private: - std::string name; - uint8_t type; - }; - Directory(std::string directory) { - dir = opendir(directory.c_str()); - } - ~Directory() { - if(openedSuccessfully()) - closedir(dir); - dir = nullptr; - } - bool openedSuccessfully() { return dir != nullptr; } - std::vector ls() { - std::vector results; - struct dirent* entry; - while((entry = readdir(dir)) != nullptr) { - std::string name = entry->d_name; - if(name != "." && name != "..") // Ignore parent and self - results.emplace_back(name, entry->d_type); - } - return results; - } -private: - DIR* dir; -}; - -class USBSerialGetter { -public: - USBSerialGetter(std::string usbid) { - std::stringstream ss; - auto colonpos = usbid.find(":"); - if(colonpos == std::string::npos) { - succeeded = false; - return; - } - - ss << "/sys/bus/usb/devices/" << usbid.substr(0, colonpos) << "/serial"; - try { - std::ifstream reader(ss.str()); - std::getline(reader, serial); - } catch(...) { - succeeded = false; - return; - } - - succeeded = true; - } - bool success() const { return succeeded; } - const std::string& getSerial() const { return serial; } -private: - bool succeeded; - std::string serial; -}; - -std::vector STM32::FindByProduct(int product) { - std::vector found; - - Directory directory("/sys/bus/usb/drivers/cdc_acm"); // Query the STM32 driver - if(!directory.openedSuccessfully()) - return found; - - std::vector foundusbs; - for(auto& entry : directory.ls()) { - /* This directory will have directories (links) for all devices using the cdc_acm driver (as STM32 devices do) - * There will also be other files and directories providing information about the driver in here. We want to ignore them. - * Devices will be named like "7-2:1.0" where 7 is the enumeration for the USB controller, 2 is the device enumeration on - * that specific controller (will change if the device is unplugged and replugged), 1 is the device itself and 0 is - * enumeration for different services provided by the device. We're looking for the service that provides TTY. - * For now we find the directories with a digit for the first character, these are likely to be our USB devices. - */ - if(isdigit(entry.getName()[0]) && entry.getType() == DT_LNK) - foundusbs.emplace_back(entry.getName()); - } - - // Pair the USB and TTY if found - std::map foundttys; - for(auto& usb : foundusbs) { - std::stringstream ss; - ss << "/sys/bus/usb/drivers/cdc_acm/" << usb << "/tty"; - Directory devicedir(ss.str()); - if(!devicedir.openedSuccessfully()) // The tty directory doesn't exist, because this is not the tty service we want - continue; - - auto listing = devicedir.ls(); - if(listing.size() != 1) // We either got no serial ports or multiple, either way no good - continue; - - foundttys.insert(std::make_pair(usb, listing[0].getName())); - } - - // We're going to remove from the map if this is not the product we're looking for - for(auto iter = foundttys.begin(); iter != foundttys.end(); ) { - const auto& dev = *iter; - const std::string matchString = "PRODUCT="; - std::stringstream ss; - ss << "/sys/class/tty/" << dev.second << "/device/uevent"; // Read the uevent file, which contains should have a line like "PRODUCT=93c/1101/100" - std::ifstream fs(ss.str()); - std::string productLine; - size_t pos = std::string::npos; - do { - std::getline(fs, productLine, '\n'); - } while(((pos = productLine.find(matchString)) == std::string::npos) && !fs.eof()); - - if(pos != 0) { // We did not find a product line... weird - iter = foundttys.erase(iter); // Remove the element, this also moves iter forward for us - continue; - } - - size_t firstSlashPos = productLine.find('/', matchString.length()); - if(firstSlashPos == std::string::npos) { - iter = foundttys.erase(iter); - continue; - } - size_t pidpos = firstSlashPos + 1; - - std::string vidstr = productLine.substr(matchString.length(), firstSlashPos - matchString.length()); - std::string pidstr = productLine.substr(pidpos, productLine.find('/', pidpos) - pidpos); // In hex like "1101" or "93c" - - uint16_t vid, pid; - try { - vid = (uint16_t)std::stoul(vidstr, nullptr, 16); - pid = (uint16_t)std::stoul(pidstr, nullptr, 16); - } catch(...) { - iter = foundttys.erase(iter); // We could not parse the numbers - continue; - } - - if(vid != INTREPID_USB_VENDOR_ID || pid != product) { - iter = foundttys.erase(iter); // Not the right VID or PID, remove - continue; - } - iter++; // If the loop ends without erasing the iter from the map, the item is good - } - - // At this point, foundttys contains the the devices we want - - // Get the serial number, create the neodevice_t - for(auto& dev : foundttys) { - neodevice_t device; - - USBSerialGetter getter(dev.first); - if(!getter.success()) - continue; // Failure, could not get serial number - - // In ttyACM0, we want the i to be the first character of the number - size_t i; - for(i = 0; i < dev.second.length(); i++) { - if(isdigit(dev.second[i])) - break; - } - // Now we try to parse the number so we have a handle for later - try { - device.handle = (neodevice_handle_t)std::stoul(dev.second.substr(i)); - /* The TTY numbering starts at zero, but we want to keep zero for an undefined - * handle, so add a constant, and we'll subtract that constant in the open function. - */ - device.handle += HANDLE_OFFSET; - } catch(...) { - continue; // Somehow this failed, have to toss the device - } - - device.serial[getter.getSerial().copy(device.serial, sizeof(device.serial)-1)] = '\0'; - - found.push_back(device); // Finally, add device to search results - } - - return found; -} - bool STM32::open() { if(isOpen()) { report(APIEvent::Type::DeviceCurrentlyOpen, APIEvent::Severity::Error); return false; } - std::stringstream ss; - ss << "/dev/ttyACM" << (int)(device.handle - HANDLE_OFFSET); - fd = ::open(ss.str().c_str(), O_RDWR | O_NOCTTY | O_SYNC); + + const std::string& ttyPath = HandleToTTY(device.handle); + if(ttyPath.empty()) { + report(APIEvent::Type::DriverFailedToOpen, APIEvent::Severity::Error); + return false; + } + + fd = ::open(ttyPath.c_str(), O_RDWR | O_NOCTTY | O_SYNC); if(!isOpen()) { - //std::cout << "Open of " << ss.str().c_str() << " failed with " << strerror(errno) << ' '; + //std::cout << "Open of " << ttyPath.c_str() << " failed with " << strerror(errno) << ' '; report(APIEvent::Type::DriverFailedToOpen, APIEvent::Severity::Error); return false; }