/* * ebusd - daemon for communication with eBUS heating systems. * Copyright (C) 2015-2018 John Baier * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program 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 General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #ifdef HAVE_CONFIG_H # include #endif #include "lib/ebus/device.h" #include #include #include #include #include #include #ifdef HAVE_LINUX_SERIAL # include #endif #include #ifdef HAVE_PPOLL # include #endif #include #include #include #include "lib/ebus/data.h" namespace ebusd { #define MTU 1540 #ifndef POLLRDHUP #define POLLRDHUP 0 #endif // ebusd enhanced protocol IDs: //TODO use this: #define ENH_INIT ((symbol_t)0x00) #define ENH_RESETTED ((symbol_t)0x00) #define ENH_SEND ((symbol_t)0x01) #define ENH_RECEIVED ((symbol_t)0x01) #define ENH_START ((symbol_t)0x02) #define ENH_STARTED ((symbol_t)0x02) #define ENH_FAILED ((symbol_t)0x82) /** * Construct a new instance. * @param name the device name (e.g. "/dev/ttyUSB0" for serial, "127.0.0.1:1234" for network). * @param address the socket address of the device. * @param readOnly whether to allow read access to the device only. * @param initialSend whether to send an initial @a ESC symbol in @a open(). * @param udp true for UDP, false to TCP. * @param enhancedProto whether to use the ebusd enhanced protocol. */ Device::Device(const char* name, bool checkDevice, bool readOnly, bool initialSend, bool enhancedProto) : m_name(name), m_checkDevice(checkDevice), m_readOnly(readOnly), m_initialSend(initialSend), m_enhancedProto(enhancedProto), m_fd(-1), m_listener(nullptr), m_arbitrationMaster(SYN), m_arbitrationCheck(false), m_bufSize(((MAX_LEN+1+3)/4)*4), m_bufLen(0), m_bufPos(0) { m_buffer = reinterpret_cast(malloc(m_bufSize)); if (!m_buffer) { m_bufSize = 0; } } Device::~Device() { close(); if (m_buffer) { free(m_buffer); } } Device* Device::create(const char* name, bool checkDevice, bool readOnly, bool initialSend) { bool enhanced = strncmp(name, "enh:", 4) == 0; if (enhanced) { name += 4; } if (strchr(name, '/') == nullptr && strchr(name, ':') != nullptr) { char* in = strdup(name); bool udp = false; char* addrpos = in; char* portpos = strchr(addrpos, ':'); if (!enhanced && portpos >= addrpos+3 && strncmp(addrpos, "enh", 3) == 0) { enhanced = true; // support enhtcp:: and enhudp:: addrpos += 3; if (portpos == addrpos) { addrpos++; portpos = strchr(addrpos, ':'); } } // else: support enh:: defaulting to TCP if (portpos == addrpos+3 && (strncmp(addrpos, "tcp", 3) == 0 || (udp=(strncmp(addrpos, "udp", 3) == 0)))) { addrpos += 4; portpos = strchr(addrpos, ':'); } if (portpos == nullptr) { free(in); return nullptr; // invalid protocol or missing port } result_t result = RESULT_OK; unsigned int port = parseInt(portpos+1, 10, 1, 65535, &result); if (result != RESULT_OK) { free(in); return nullptr; // invalid port } struct sockaddr_in address; memset(reinterpret_cast(&address), 0, sizeof(address)); *portpos = 0; if (inet_aton(addrpos, &address.sin_addr) == 0) { struct hostent* h = gethostbyname(addrpos); if (h == nullptr) { free(in); return nullptr; // invalid host } memcpy(&address.sin_addr, h->h_addr_list[0], h->h_length); } free(in); address.sin_family = AF_INET; address.sin_port = (in_port_t)htons((uint16_t)port); return new NetworkDevice(name, address, readOnly, initialSend, udp, enhanced); } // support enh:/dev/ return new SerialDevice(name, checkDevice, readOnly, initialSend, enhanced); } result_t Device::open() { close(); return m_bufSize == 0 ? RESULT_ERR_DEVICE : RESULT_OK; } void Device::close() { if (m_fd != -1) { ::close(m_fd); m_fd = -1; } m_bufLen = 0; // flush read buffer } bool Device::isValid() { if (m_fd == -1) { return false; } if (m_checkDevice) { checkDevice(); } return m_fd != -1; } result_t Device::send(symbol_t value) { if (!isValid()) { return RESULT_ERR_DEVICE; } if (m_readOnly || !write(value)) { return RESULT_ERR_SEND; } if (m_listener != nullptr) { m_listener->notifyDeviceData(value, false); } return RESULT_OK; } result_t Device::recv(unsigned int timeout, symbol_t* value, ArbitrationState* arbitrationState) { if (!isValid()) { return RESULT_ERR_DEVICE; } bool isAvailable = available(); if (!isAvailable && timeout > 0) { int ret; struct timespec tdiff; // set select timeout tdiff.tv_sec = timeout/1000000; tdiff.tv_nsec = (timeout%1000000)*1000; #ifdef HAVE_PPOLL nfds_t nfds = 1; struct pollfd fds[nfds]; memset(fds, 0, sizeof(fds)); fds[0].fd = m_fd; fds[0].events = POLLIN | POLLERR | POLLHUP | POLLRDHUP; ret = ppoll(fds, nfds, &tdiff, nullptr); if (ret >= 0 && fds[0].revents & (POLLERR | POLLHUP | POLLRDHUP)) { ret = -1; } #else #ifdef HAVE_PSELECT fd_set readfds, exceptfds; FD_ZERO(&readfds); FD_ZERO(&exceptfds); FD_SET(m_fd, &readfds); ret = pselect(m_fd + 1, &readfds, nullptr, &exceptfds, &tdiff, nullptr); if (ret >= 1 && FD_ISSET(m_fd, &exceptfds)) { ret = -1; } #else ret = 1; // ignore timeout if neither ppoll nor pselect are available #endif #endif if (ret == -1) { close(); return RESULT_ERR_DEVICE; } if (ret == 0) { return RESULT_ERR_TIMEOUT; } } ArbitrationState prevState = *arbitrationState; // directly read byte from device if (!read(value, isAvailable, arbitrationState)) { close(); return RESULT_ERR_DEVICE; } if (m_enhancedProto || *value != SYN || m_arbitrationMaster == SYN) { if (m_listener != nullptr) { m_listener->notifyDeviceData(*value, true); } if (m_enhancedProto) { if (*arbitrationState != prevState) { m_arbitrationMaster = SYN; m_arbitrationCheck = false; } } else if (m_arbitrationMaster != SYN) { if (m_arbitrationCheck) { *arbitrationState = *value == m_arbitrationMaster ? as_won : as_lost; m_arbitrationMaster = SYN; m_arbitrationCheck = false; } else { *arbitrationState = m_arbitrationMaster == SYN ? as_none : as_start; } } return RESULT_OK; } bool wrote = write(m_arbitrationMaster); // send as fast as possible if (m_listener != nullptr) { m_listener->notifyDeviceData(*value, true); } if (!wrote) { *arbitrationState = as_error; m_arbitrationMaster = SYN; m_arbitrationCheck = false; return RESULT_OK; } if (m_listener != NULL) { m_listener->notifyDeviceData(m_arbitrationMaster, false); } m_arbitrationCheck = true; *arbitrationState = as_running; return RESULT_OK; } result_t Device::startArbitration(symbol_t masterAddress) { if (m_arbitrationCheck) { return RESULT_ERR_DUPLICATE; } if (m_readOnly) { return RESULT_ERR_SEND; } m_arbitrationMaster = masterAddress; m_arbitrationCheck = false; if (m_enhancedProto && masterAddress != SYN) { if (!write(masterAddress, true)) { m_arbitrationMaster = SYN; return RESULT_ERR_SEND; } m_arbitrationCheck = true; } return RESULT_OK; } bool Device::write(symbol_t value, bool startArbitration) { if (m_enhancedProto) { symbol_t buf[2] = {startArbitration ? ENH_START : ENH_SEND, value}; return ::write(m_fd, buf, 2) == 2; } return ::write(m_fd, &value, 1) == 1; } bool Device::available() { if (m_bufLen <= 0) { return false; } if (!m_enhancedProto) { return true; } // peek into the received enhanced proto bytes to determine symbol availability for (size_t pos = 0; pos < m_bufLen; pos++) { symbol_t ch = m_buffer[(pos+m_bufPos)%m_bufSize]; if (ch == ENH_RECEIVED) { return pos+1 < m_bufLen; } } return false; } bool Device::read(symbol_t* value, bool isAvailable, ArbitrationState* arbitrationState) { if (!isAvailable) { if (m_bufLen > 0 && m_bufPos != 0) { if (m_bufLen > m_bufSize / 2) { m_bufLen = 0; // TODO report error } else { size_t tail; if (m_bufPos+m_bufLen > m_bufSize) { // move wrapped tail away tail = (m_bufPos+m_bufLen) % m_bufSize; size_t head = m_bufLen-tail; memmove(m_buffer+head, m_buffer, tail); } else { tail = 0; } // move head to first position memmove(m_buffer, m_buffer + m_bufPos, m_bufLen - tail); } } m_bufPos = 0; // fill up the buffer ssize_t size = ::read(m_fd, m_buffer + m_bufLen, m_bufSize - m_bufLen); if (size <= 0) { return false; } m_bufLen += size; } if (!available()) { return false; } if (!m_enhancedProto) { *value = m_buffer[m_bufPos]; m_bufPos = (m_bufPos+1)%m_bufSize; m_bufLen--; return true; } while (m_bufLen > 0) { symbol_t ch = m_buffer[m_bufPos]; m_bufPos = (m_bufPos+1)%m_bufSize; m_bufLen--; switch (ch) { case ENH_STARTED: *arbitrationState = as_won; if (m_listener != NULL) { m_listener->notifyDeviceData(m_arbitrationMaster, false); } m_arbitrationMaster = SYN; break; case ENH_FAILED: *arbitrationState = as_error; if (m_listener != NULL) { m_listener->notifyDeviceData(m_arbitrationMaster, false); } m_arbitrationMaster = SYN; break; case ENH_RECEIVED: if (m_bufLen <= 0) { return false; } *value = m_buffer[m_bufPos]; m_bufPos = (m_bufPos+1)%m_bufSize; m_bufLen--; return true; case ENH_RESETTED: // TODO *arbitrationState = as_error; break; default: // TODO proto error return false; } } return false; } result_t SerialDevice::open() { result_t result = Device::open(); if (result != RESULT_OK) { return result; } struct termios newSettings; // open file descriptor m_fd = ::open(m_name, O_RDWR | O_NOCTTY); if (m_fd < 0) { return RESULT_ERR_NOTFOUND; } if (isatty(m_fd) == 0) { close(); return RESULT_ERR_NOTFOUND; } if (flock(m_fd, LOCK_EX|LOCK_NB)) { close(); return RESULT_ERR_DEVICE; } #ifdef HAVE_LINUX_SERIAL struct serial_struct serial; if (ioctl(m_fd, TIOCGSERIAL, &serial) == 0) { serial.flags |= ASYNC_LOW_LATENCY; ioctl(m_fd, TIOCSSERIAL, &serial); } #endif // save current settings tcgetattr(m_fd, &m_oldSettings); // create new settings memset(&newSettings, 0, sizeof(newSettings)); newSettings.c_cflag |= ((m_enhancedProto ? B115200 : B2400) | CS8 | CLOCAL | CREAD); newSettings.c_lflag &= ~(ICANON | ECHO | ECHOE | ISIG); // non-canonical mode newSettings.c_iflag |= IGNPAR; // ignore parity errors newSettings.c_oflag &= ~OPOST; // non-canonical mode: read() blocks until at least one byte is available newSettings.c_cc[VMIN] = 1; newSettings.c_cc[VTIME] = 0; // empty device buffer tcflush(m_fd, TCIFLUSH); // activate new settings of serial device tcsetattr(m_fd, TCSAFLUSH, &newSettings); // set serial device into blocking mode fcntl(m_fd, F_SETFL, fcntl(m_fd, F_GETFL) & ~O_NONBLOCK); if (m_initialSend && !write(ESC)) { return RESULT_ERR_SEND; } return RESULT_OK; } void SerialDevice::close() { if (m_fd != -1) { // empty device buffer tcflush(m_fd, TCIOFLUSH); // restore previous settings of the device tcsetattr(m_fd, TCSANOW, &m_oldSettings); } Device::close(); } void SerialDevice::checkDevice() { int port; if (ioctl(m_fd, TIOCMGET, &port) == -1) { close(); } } result_t NetworkDevice::open() { result_t result = Device::open(); if (result != RESULT_OK) { return result; } m_fd = socket(AF_INET, m_udp ? SOCK_DGRAM : SOCK_STREAM, 0); if (m_fd < 0) { return RESULT_ERR_GENERIC_IO; } int ret; if (m_udp) { struct sockaddr_in address = m_address; address.sin_addr.s_addr = INADDR_ANY; ret = bind(m_fd, (struct sockaddr*)&address, sizeof(address)); } else { int value = 1; ret = setsockopt(m_fd, IPPROTO_TCP, TCP_NODELAY, reinterpret_cast(&value), sizeof(value)); value = 1; setsockopt(m_fd, SOL_SOCKET, SO_KEEPALIVE, reinterpret_cast(&value), sizeof(value)); } if (ret >= 0) { ret = connect(m_fd, (struct sockaddr*)&m_address, sizeof(m_address)); } if (ret < 0) { close(); return RESULT_ERR_GENERIC_IO; } if (!m_udp) { usleep(25000); // wait 25ms for potential initial garbage } int cnt; symbol_t buf[MTU]; while (ioctl(m_fd, FIONREAD, &cnt) >= 0 && cnt > 1) { // skip buffered input ssize_t read = ::read(m_fd, &buf, MTU); if (read <= 0) { break; } } m_bufLen = 0; if (m_initialSend && !write(ESC)) { return RESULT_ERR_SEND; } return RESULT_OK; } void NetworkDevice::checkDevice() { int cnt; if (ioctl(m_fd, FIONREAD, &cnt) < 0) { close(); } } } // namespace ebusd