Today a bit about serial communication as it will be a base communication layer ELM327 and CAN communication, just like IP is a base layer for TCP/UDP.
Why abstraction though? Application is currently for OS X so this is not really a must have. On the other hand this is how it usually should be done, especially if we consider multiplatform in the future. This is also not a really big change big difference too and makes code a bit cleaner.
So what do we need? Shortly: a common interface and common types for all parameters and return values. I divided it into two parts, may be the same library finally, but can be also easily split in two. Just the matter of preference. It is:
- Serial devices enumeration and recognition.
- Serial device access: open, read, write, close with parameters as baud rate, parity control etc.
First thing is a really OS specific thing, but interestingly quite easy to create an interface. I made a decision to support both Bluetooth and USB serial adapters and tested it with:
- ELM327 Bluetooth
- ELM327 USB (CH34x adapter, drivers: https://github.com/jimaobian/CH34x_Install_V1.3/blob/master/CH34x_Install_V1.3.zip)
- Prolific USB2.0-Serial (drivers: http://www.prolific.com.tw/US/ShowProduct.aspx?p_id=229&pcid=41)
Enumeration will be explained in next post, now just an interface:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
enum class DeviceType { USB_DEVICE, BLUETOOTH_DEVICE, OTHER }; struct ParentDevice { // COMMON std::string name; //> Device name (BTName, USB Product Name) DeviceType type; //> Device type, also indicates possibly filled fields // for parent device - USB or Bluetooth. // BLUETOOTH uint32_t channel; //> Bluetooth channel number uint32_t connectionType; //> Serial device type // USB uint32_t vendorId; //> USB vendor ID uint32_t productId; //> USB product ID uint32_t serialNumber; //> USB device serial number std::string vendorName; //> USB vendor name string }; struct SerialDevice { std::string name; //> Serial device name std::string deviceClass; //> In most cases: IOSerialBSDClient std::string calloutDevice; //> UNIX serial callout device (serial port in /dev/tty...) std::string dialinDevice; //> UNIX serial dialin device ParentDevice parent; //> Parent device, either USB or Bluetooth supported }; class Interfaces { /** * Returns list of serial devices available on the platform as a vector of @{SerialDevice} objects. */ virtual std::vector<SerialDevice> GetDevices() = 0; }; |
Easy to use, one method that returns a list of SerialDevice objects that describes serial port and parent objects that describes a specific device that handles serial communication: USB. Bluetooth or Other, unknown device type.
For USB and Bluetooth devices I try to get more specific information about name, vendor etc.
Current implementation is here:
https://github.com/killpl/obd_cougar/blob/master/cougar_lib/serial/IInterfaces.h
Later on I’ve opened the man page for open(): x-man-page://2/open and made some assumptions for possible errors and parameters:
Next thing, which is currently in progress, is device access and read write. The interface will be much more complicated, as it has to support a lot of open() flags, configuration parameters and errors,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
enum class SerialError { NO_ERROR = -1, UNKNOWN, // Open ACCESS_DENIED, AGAIN, QUOTA, //> Should not happen with no "O_CREAT" EXIST, //> As above ^. FAULT, INTERRUPTED, INVALID_FLAG, //> One or more of specified flags is invalid. INPUT_OUTPUT_ERROR, IS_DIRECTORY, //> Specified path leads to directory, not file. NOT_DIRECTORY, //> Path component is not a directory. DEVICE_DISCONNECTED, //> Path leads to device and associated device does not exist. LOCK_NOT_SUPPORTED, //> Shared or exclusive lock not supported by filesystem. OVERFLOW, //> Filesize too big READ_ONLY, //> File is on read-only filesystem, cannot be modified. INVALID_PATH, //> Invalid path LOOP, //> Symlinks infinite loop. NAME_TOO_LONG, //> Component of specified path too long. FILETABLE_FULL, //> DESCRIPTORS_LIMIT, //> Process descriptors limit reached. }; enum class SerialOpenFlag { READ_ONLY = (1u << 0), WRITE_ONLY = (1u << 1), READ_WRITE = (1u << 2), NONBLOCK = (1u << 3), APPEND = (1u << 4), SHARED_LOCK = (1u << 5), EXCLUSIVE_LOCK = (1u << 6), NO_FOLLOW = (1u << 7), NO_CTTY = (1u << 8), SYMLINK = (1u << 9), TRUNCATE = (1u << 10) }; |
First thing to notice is that we have specific errors in enum, but SerialOpenFlag is a bitmask, so it had to be implemented with consecutive powers of 2: 0x0001, 0x0002, 0x0004, 0x0008… so in a result we might set any bit of the flag without affecting other ones – READ_WRITE and NOBLOCK is for example b’00001100′.
On the other hand the interface cannot be platform specific, so we have to map this later to system specific codes, I decided for it to look like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
unsigned int SerialPortOSX::UnixFlagsForEnumFlags(int flags) { unsigned int result = 0; #define MAP(x,y) if (flags & (unsigned)SerialOpenFlag::x) result |= y; MAP(READ_ONLY, O_RDONLY) MAP(WRITE_ONLY, O_WRONLY) MAP(READ_WRITE, O_RDWR) MAP(NONBLOCK, O_NONBLOCK) MAP(APPEND, O_APPEND) MAP(SHARED_LOCK, O_SHLOCK) MAP(EXCLUSIVE_LOCK, O_EXLOCK) MAP(NO_FOLLOW, O_NOFOLLOW) MAP(NO_CTTY, O_NOCTTY) MAP(SYMLINK, O_SYMLINK) MAP(TRUNCATE, O_TRUNC) #undef MAP return result; } |
Similar way for error handling for open() operation:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
SerialError SerialPortOSX::Open(unsigned int flags) { int descriptor = open(_portPath.c_str(), UnixFlagsForEnumFlags(flags)); if (descriptor >= 0) { _descriptor = descriptor; return SerialError::NO_ERROR; } #define MAP(x,y) case (x): return SerialError::y; switch (errno) { MAP(EACCES, ACCESS_DENIED) MAP(EAGAIN, AGAIN) MAP(EDQUOT, QUOTA) MAP(EEXIST, EXIST) MAP(EFAULT, FAULT) MAP(EINTR, INTERRUPTED) MAP(EINVAL, INVALID_FLAG) MAP(EIO, INPUT_OUTPUT_ERROR) MAP(EISDIR, IS_DIRECTORY) MAP(ELOOP, LOOP) MAP(EMFILE, DESCRIPTORS_LIMIT) MAP(ENAMETOOLONG, NAME_TOO_LONG) MAP(ENFILE, FILETABLE_FULL) MAP(ENOENT, INVALID_PATH) MAP(ENOTDIR, INVALID_PATH) MAP(ENXIO, DEVICE_DISCONNECTED) MAP(EOPNOTSUPP, LOCK_NOT_SUPPORTED) MAP(EOVERFLOW, OVERFLOW) MAP(EBADF, INVALID_PATH) default: ; } #undef MAP return SerialError::UNKNOWN; } |
Easy to maintain, easily readable and extendable. Moreover, totally abstract from the OS specific functions 😉
Next post will be about macOS serial devices enumeration and recognition, stay tuned 😉