STM32 device finder for Darwin

pull/25/head
Paul Hollinsky 2020-02-14 23:18:34 -05:00
parent 57fb55b686
commit ef34959c91
5 changed files with 428 additions and 186 deletions

View File

@ -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

View File

@ -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<neodevice_t> 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();

View File

@ -0,0 +1,190 @@
#include "icsneo/platform/stm32.h"
#include <mutex>
#include <vector>
#include <CoreFoundation/CoreFoundation.h>
#include <IOKit/IOKitLib.h>
#include <IOKit/serial/IOSerialKeys.h>
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<std::string> ttyTable;
static neodevice_handle_t TTYToHandle(const std::string& tty) {
std::lock_guard<std::mutex> 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<char> 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<neodevice_t> STM32::FindByProduct(int product) {
std::vector<neodevice_t> 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<IOReleaser> 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<CFNumberRef>(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<CFNumberRef>(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<CFStringRef>(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<CFStringRef>(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<std::mutex> 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];
}

View File

@ -0,0 +1,202 @@
#include "icsneo/platform/stm32.h"
#include <dirent.h>
#include <cstring>
#include <iostream>
#include <sstream>
#include <fstream>
#include <map>
#include <algorithm>
#include <termios.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
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<Listing> ls() {
std::vector<Listing> 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<neodevice_t> STM32::FindByProduct(int product) {
std::vector<neodevice_t> found;
Directory directory("/sys/bus/usb/drivers/cdc_acm"); // Query the STM32 driver
if(!directory.openedSuccessfully())
return found;
std::vector<std::string> 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<std::string, std::string> 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();
}

View File

@ -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<Listing> ls() {
std::vector<Listing> 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<neodevice_t> STM32::FindByProduct(int product) {
std::vector<neodevice_t> found;
Directory directory("/sys/bus/usb/drivers/cdc_acm"); // Query the STM32 driver
if(!directory.openedSuccessfully())
return found;
std::vector<std::string> 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<std::string, std::string> 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;
}