mirror of
https://github.com/ALLFATHER-BV/wadamesh.git
synced 2026-11-12 02:27:35 +00:00
CompanionBackend speaks the companion frame protocol (stock MeshCore 1.17/1.18, wadamesh extras handled) over a CompanionTransport: USB serial and TCP natively (--serial / --tcp), Web Serial, Web Bluetooth and WebSocket in the browser (JS glue in web/mesh/js, framing in C++). The stand-in gains the node-only events (self info, raw RX, login, CLI, regions, contact removal) and computes the RX log, advert routes, repeats heard and message route info from the node's raw RX pushes, as the device does. Demo stays the default. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
222 lines
7.2 KiB
C++
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
|