Files
wadamesh/web/mesh/SerialTransport.cpp
T

222 lines
7.2 KiB
C++

// SPDX-License-Identifier: GPL-3.0-or-later
// Native companion transports for the host build: a USB serial port (POSIX termios)
// and a TCP socket (the node's TCP companion server, port 5000). Both are byte streams
// with the '<'/'>' len16 framing (CompanionTransport.h). Not built for the browser.
#include "CompanionTransport.h"
#include <stdio.h>
#if defined(__EMSCRIPTEN__) || defined(_WIN32)
CompanionTransport* wadaCreateSerialTransport(const char*) { return nullptr; }
CompanionTransport* wadaCreateTcpTransport(const char*, int) { return nullptr; }
#else
#include <errno.h>
#include <fcntl.h>
#include <netdb.h>
#include <poll.h>
#include <string>
#include <sys/ioctl.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/tcp.h>
#include <termios.h>
#include <unistd.h>
namespace {
// Shared by both: a non-blocking fd carrying the '<'/'>' stream framing, reopened
// every RETRY_MS after a failure (a node that resets re-enumerates its USB port).
class FdStreamTransport : public CompanionTransport {
public:
bool autoReconnect() const override { return true; }
bool isConnected() const override { return _fd >= 0 && _up; }
bool open() override {
_want_open = true;
_next_try = 0;
return true;
}
void close() override {
_want_open = false;
drop(nullptr);
}
bool sendFrame(const uint8_t* frame, size_t len) override {
if (_fd < 0) return false;
const std::vector<uint8_t> out = companionEncodeHostFrame(frame, len);
size_t off = 0;
int spins = 0;
while (off < out.size()) {
const ssize_t n = ::write(_fd, out.data() + off, out.size() - off);
if (n > 0) { off += (size_t)n; continue; }
if (n < 0 && (errno == EAGAIN || errno == EWOULDBLOCK) && spins++ < 200) {
struct pollfd p = {_fd, POLLOUT, 0};
::poll(&p, 1, 5);
continue;
}
drop("write failed");
return false;
}
return true;
}
void poll(uint32_t now_ms) override {
if (_fd < 0) {
if (!_want_open || (int32_t)(now_ms - _next_try) < 0) return;
_next_try = now_ms + RETRY_MS;
std::string why;
_fd = connectFd(why);
if (_fd < 0) {
if (why != _last_err) fprintf(stderr, "[companion] %s %s: %s (retrying)\n", name(), describe(), why.c_str());
_last_err = why;
return;
}
_last_err.clear();
_deframer.reset();
_up = false;
}
if (!_up) {
if (!connectDone()) return;
_up = true;
fprintf(stderr, "[companion] %s %s: open\n", name(), describe());
if (_listener) _listener->onTransportConnected();
if (_fd < 0) return;
}
uint8_t buf[4096];
for (int guard = 0; guard < 16 && _fd >= 0; ++guard) {
const ssize_t n = ::read(_fd, buf, sizeof buf);
if (n > 0) {
_deframer.feed(buf, (size_t)n, [this](const uint8_t* f, size_t l) {
if (_listener) _listener->onTransportFrame(f, l);
});
continue;
}
if (n == 0 && isSocket()) { drop("closed by the node"); return; }
if (n < 0 && errno != EAGAIN && errno != EWOULDBLOCK && errno != EINTR) { drop(strerror(errno)); return; }
break;
}
}
protected:
static const uint32_t RETRY_MS = 2000;
virtual int connectFd(std::string& why) = 0;
virtual bool connectDone() { return true; }
virtual bool isSocket() const { return false; }
void drop(const char* why) {
if (_fd >= 0) ::close(_fd);
_fd = -1;
const bool was_up = _up;
_up = false;
_deframer.reset();
if (was_up && why) {
fprintf(stderr, "[companion] %s %s: %s\n", name(), describe(), why);
if (_listener) _listener->onTransportDisconnected(why);
}
}
int _fd = -1;
bool _up = false;
bool _want_open = false;
uint32_t _next_try = 0;
std::string _last_err;
StreamDeframer _deframer;
};
// USB serial. Opening a port asserts DTR and RTS; on an ESP32-S3 a DTR/RTS sequence can
// reset the chip or drop it into the bootloader. Both lines are released in ONE ioctl
// right after open (never passing through DTR=0/RTS=1, the reset state), and HUPCL is
// cleared so closing the port does not toggle them either. The phone apps and the
// scratchpad companion_test.py open the same way (dtr = rts = False).
class SerialTransport : public FdStreamTransport {
public:
explicit SerialTransport(const char* path) : _path(path ? path : "") {}
const char* name() const override { return "serial"; }
const char* describe() const override { return _path.c_str(); }
protected:
int connectFd(std::string& why) override {
const int fd = ::open(_path.c_str(), O_RDWR | O_NOCTTY | O_NONBLOCK);
if (fd < 0) { why = strerror(errno); return -1; }
int lines = TIOCM_DTR | TIOCM_RTS;
ioctl(fd, TIOCMBIC, &lines);
struct termios t;
if (tcgetattr(fd, &t) == 0) {
cfmakeraw(&t);
t.c_cflag |= (CLOCAL | CREAD);
t.c_cflag &= ~(tcflag_t)HUPCL;
t.c_cc[VMIN] = 0;
t.c_cc[VTIME] = 0;
cfsetispeed(&t, B115200); // USB CDC ignores it; a UART bridge needs it
cfsetospeed(&t, B115200);
tcsetattr(fd, TCSANOW, &t);
}
ioctl(fd, TIOCMBIC, &lines); // some drivers re-assert on tcsetattr
return fd;
}
private:
std::string _path;
};
// TCP companion server (the node's port 5000; Wi-Fi on). Non-blocking connect.
class TcpTransport : public FdStreamTransport {
public:
TcpTransport(const char* host, int port) : _host(host ? host : ""), _port(port) {
snprintf(_desc, sizeof _desc, "%s:%d", _host.c_str(), _port);
}
const char* name() const override { return "tcp"; }
const char* describe() const override { return _desc; }
protected:
bool isSocket() const override { return true; }
int connectFd(std::string& why) override {
struct addrinfo hints = {}, *res = nullptr;
hints.ai_family = AF_UNSPEC;
hints.ai_socktype = SOCK_STREAM;
char port[8];
snprintf(port, sizeof port, "%d", _port);
const int rc = getaddrinfo(_host.c_str(), port, &hints, &res);
if (rc != 0 || !res) { why = gai_strerror(rc); return -1; }
const int fd = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
if (fd < 0) { why = strerror(errno); freeaddrinfo(res); return -1; }
fcntl(fd, F_SETFL, fcntl(fd, F_GETFL) | O_NONBLOCK);
int one = 1;
setsockopt(fd, IPPROTO_TCP, TCP_NODELAY, &one, sizeof one);
#ifdef SO_NOSIGPIPE
setsockopt(fd, SOL_SOCKET, SO_NOSIGPIPE, &one, sizeof one);
#endif
if (::connect(fd, res->ai_addr, res->ai_addrlen) != 0 && errno != EINPROGRESS) {
why = strerror(errno);
::close(fd);
freeaddrinfo(res);
return -1;
}
freeaddrinfo(res);
return fd;
}
bool connectDone() override {
struct pollfd p = {_fd, POLLOUT, 0};
if (::poll(&p, 1, 0) <= 0) return false;
int err = 0;
socklen_t l = sizeof err;
getsockopt(_fd, SOL_SOCKET, SO_ERROR, &err, &l);
if (err != 0) {
fprintf(stderr, "[companion] tcp %s: %s (retrying)\n", _desc, strerror(err));
::close(_fd);
_fd = -1;
return false;
}
return true;
}
private:
std::string _host;
int _port;
char _desc[96];
};
} // namespace
CompanionTransport* wadaCreateSerialTransport(const char* device_path) { return new SerialTransport(device_path); }
CompanionTransport* wadaCreateTcpTransport(const char* host, int port) { return new TcpTransport(host, port); }
#endif