Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions examples/companion_radio/DataStore.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ static File openWrite(FILESYSTEM* fs, const char* filename) {
return fs->open(filename, FILE_O_WRITE);
#elif defined(RP2040_PLATFORM)
return fs->open(filename, "w");
#elif defined(ARDULINUX_PLATFORM)
return fs->open(filename, "w");
#else
return fs->open(filename, "w", true);
#endif
Expand Down Expand Up @@ -140,6 +142,8 @@ File DataStore::openRead(const char* filename) {
return _fs->open(filename, FILE_O_READ);
#elif defined(RP2040_PLATFORM)
return _fs->open(filename, "r");
#elif defined(ARDULINUX_PLATFORM)
return _fs->open(filename, "r");
#else
return _fs->open(filename, "r", false);
#endif
Expand All @@ -150,6 +154,8 @@ File DataStore::openRead(FILESYSTEM* fs, const char* filename) {
return fs->open(filename, FILE_O_READ);
#elif defined(RP2040_PLATFORM)
return fs->open(filename, "r");
#elif defined(ARDULINUX_PLATFORM)
return fs->open(filename, "r");
#else
return fs->open(filename, "r", false);
#endif
Expand All @@ -176,6 +182,10 @@ bool DataStore::formatFileSystem() {
bool fs_success = ((fs::SPIFFSFS *)_fs)->format();
esp_err_t nvs_err = nvs_flash_erase(); // no need to reinit, will be done by reboot
return fs_success && (nvs_err == ESP_OK);
#elif defined(ARDULINUX_PLATFORM)
// Wiping is handled by the daemon's --erase startup flag (ArduLinux core
// clears the VFS root before any open). The runtime "erase" command is a no-op.
return false;
#else
#error "need to implement format()"
#endif
Expand Down
2 changes: 2 additions & 0 deletions examples/companion_radio/MyMesh.h
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
#include <LittleFS.h>
#elif defined(ESP32)
#include <SPIFFS.h>
#elif defined(ARDULINUX_PLATFORM)
#include <ArduLinuxFS.h>
#endif

#include "DataStore.h"
Expand Down
36 changes: 36 additions & 0 deletions examples/companion_radio/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -85,8 +85,17 @@ MultiSerialInterface interface_manager;
#elif defined(ESP32)
#include <SPIFFS.h>
DataStore store(SPIFFS, rtc_clock);
#elif defined(ARDULINUX_PLATFORM)
DataStore store(ArduLinuxFS, rtc_clock);
#endif

// include linux native interface
#if defined(ARDULINUX_PLATFORM)
#include <helpers/ArduinoSerialInterface.h>
#include <helpers/LinuxTcpInterface.h>
ArduinoSerialInterface linux_serial_interface;
LinuxTcpInterface tcp_interface;
#endif
/* GLOBAL OBJECTS */
#ifdef DISPLAY_CLASS
#include "UITask.h"
Expand Down Expand Up @@ -180,6 +189,17 @@ void setup() {
false
#endif
);
#elif defined(ARDULINUX_PLATFORM)
// the VFS root is established by the ArduLinux core from --fsdir
// (default: the XDG data dir, e.g. ~/.local/share/meshcored/default)
store.begin();
the_mesh.begin(
#ifdef DISPLAY_CLASS
disp != NULL
#else
false
#endif
);
#else
#error "need to define filesystem"
#endif
Expand Down Expand Up @@ -230,6 +250,22 @@ void setup() {
interface_manager.addInterface(InterfaceType::HardwareSerial, &hardware_serial_interface);
#endif

// add linux native interface: the TCP listener when companion_tcp_port is set
// and binds, otherwise the stdio transport (ArduLinux maps Serial to stdin/stdout)
#if defined(ARDULINUX_PLATFORM)
if (board.config.companion_tcp_port != 0
&& tcp_interface.begin(board.config.companion_tcp_port, board.config.companion_tcp_bind)) {
interface_manager.addInterface(InterfaceType::WiFi, &tcp_interface);
fprintf(stderr, "Companion: TCP listener on %s:%u\n",
board.config.companion_tcp_bind, board.config.companion_tcp_port);
} else {
linux_serial_interface.begin(Serial);
interface_manager.addInterface(InterfaceType::USB, &linux_serial_interface);
fprintf(stderr, "Companion: stdin/stdout transport (companion_tcp_port = %u)\n",
board.config.companion_tcp_port);
}
#endif

the_mesh.startInterface(interface_manager);
sensors.begin();

Expand Down
232 changes: 232 additions & 0 deletions src/helpers/LinuxTcpInterface.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,232 @@
#ifdef ARDULINUX_PLATFORM

#include "LinuxTcpInterface.h"

#include <arpa/inet.h>
#include <errno.h>
#include <netinet/in.h>
#include <stdio.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>

#include <Arduino.h> // millis()

LinuxTcpInterface::LinuxTcpInterface()
: _server_fd(-1), _client_fd(-1), _enabled(false),
_device_connected(false), _port(0), _last_write(0),
_send_queue_len(0), _rx_len(0) {
_received_frame_header.type = 0;
_received_frame_header.length = 0;
}

LinuxTcpInterface::~LinuxTcpInterface() {
closeClient();
if (_server_fd >= 0) ::close(_server_fd);
}

bool LinuxTcpInterface::begin(uint16_t port, const char* bind_addr) {
if (port == 0) return false;

_server_fd = ::socket(AF_INET, SOCK_STREAM | SOCK_NONBLOCK | SOCK_CLOEXEC, 0);
if (_server_fd < 0) {
fprintf(stderr, "LinuxTcpInterface: socket() failed: %s\n", strerror(errno));
return false;
}

int one = 1;
(void)::setsockopt(_server_fd, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one));

struct sockaddr_in addr;
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_port = htons(port);
if (::inet_pton(AF_INET, bind_addr, &addr.sin_addr) != 1) {
fprintf(stderr, "LinuxTcpInterface: inet_pton(%s) failed (not a valid IPv4 address)\n", bind_addr);
::close(_server_fd);
_server_fd = -1;
return false;
}

if (::bind(_server_fd, (struct sockaddr*)&addr, sizeof(addr)) < 0) {
fprintf(stderr, "LinuxTcpInterface: bind(%s:%u) failed: %s\n", bind_addr, port, strerror(errno));
::close(_server_fd);
_server_fd = -1;
return false;
}

if (::listen(_server_fd, 1) < 0) {
fprintf(stderr, "LinuxTcpInterface: listen() failed: %s\n", strerror(errno));
::close(_server_fd);
_server_fd = -1;
return false;
}

_port = port;
return true;
}

void LinuxTcpInterface::enable() {
if (_enabled) return;
_enabled = true;
_send_queue_len = 0;
_rx_len = 0;
resetReceivedFrameHeader();
}

void LinuxTcpInterface::disable() {
_enabled = false;
closeClient();
}

void LinuxTcpInterface::closeClient() {
if (_client_fd >= 0) {
::close(_client_fd);
_client_fd = -1;
}
_device_connected = false;
_rx_len = 0;
resetReceivedFrameHeader();
}

void LinuxTcpInterface::acceptIfNeeded() {
if (_server_fd < 0) return;

int new_fd = ::accept4(_server_fd, NULL, NULL, SOCK_NONBLOCK | SOCK_CLOEXEC);
if (new_fd < 0) return; // EAGAIN/EWOULDBLOCK = no incoming connection

// Single-client semantics: a new connection kicks the previous one,
// mirroring SerialWifiInterface::checkRecvFrame()'s replace-on-new behavior.
closeClient();
_client_fd = new_fd;
}

bool LinuxTcpInterface::hasReceivedFrameHeader() const {
return _received_frame_header.type != 0 && _received_frame_header.length != 0;
}

void LinuxTcpInterface::resetReceivedFrameHeader() {
_received_frame_header.type = 0;
_received_frame_header.length = 0;
}

size_t LinuxTcpInterface::writeFrame(const uint8_t src[], size_t len) {
if (len == 0 || len > MAX_FRAME_SIZE) return 0;
if (!_device_connected) return 0;
if (_send_queue_len >= FRAME_QUEUE_SIZE) return 0;

_send_queue[_send_queue_len].len = (uint8_t)len;
memcpy(_send_queue[_send_queue_len].buf, src, len);
_send_queue_len++;
return len;
}

void LinuxTcpInterface::drainSendQueue() {
while (_send_queue_len > 0 && _device_connected) {
int len = _send_queue[0].len;
uint8_t pkt[3 + MAX_FRAME_SIZE];
pkt[0] = '>'; // same framing as serial / SerialWifiInterface so meshcli can delimit
pkt[1] = (uint8_t)(len & 0xFF);
pkt[2] = (uint8_t)((len >> 8) & 0xFF);
memcpy(&pkt[3], _send_queue[0].buf, len);

ssize_t n = ::send(_client_fd, pkt, 3 + len, MSG_NOSIGNAL | MSG_DONTWAIT);
if (n < 0) {
if (errno == EAGAIN || errno == EWOULDBLOCK) return; // try next tick
closeClient();
return;
}
// Partial writes are rare on local TCP with small frames; on the unlikely
// partial we drop the frame to keep the protocol aligned rather than try
// to track partial-write state across ticks.
_last_write = millis();
_send_queue_len--;
for (int i = 0; i < _send_queue_len; i++) {
_send_queue[i] = _send_queue[i + 1];
}
}
}

size_t LinuxTcpInterface::readFrameFromBuffer(uint8_t dest[]) {
// Parse header if we don't have one yet
if (!hasReceivedFrameHeader()) {
const size_t HDR_LEN = 3;
if (_rx_len < HDR_LEN) return 0;

_received_frame_header.type = _rx_buf[0];
_received_frame_header.length = (uint16_t)_rx_buf[1] | ((uint16_t)_rx_buf[2] << 8);

memmove(_rx_buf, _rx_buf + HDR_LEN, _rx_len - HDR_LEN);
_rx_len -= HDR_LEN;
}

uint16_t frame_length = _received_frame_header.length;
uint8_t frame_type = _received_frame_header.type;

// '<' (0x3c) is the only valid app→radio frame type
if (frame_type != '<') {
fprintf(stderr, "LinuxTcpInterface: bad frame type 0x%02x, resyncing\n", frame_type);
_rx_len = 0;
resetReceivedFrameHeader();
return 0;
}

if (frame_length > MAX_FRAME_SIZE) {
fprintf(stderr, "LinuxTcpInterface: frame length %u > MAX_FRAME_SIZE %d, resyncing\n",
frame_length, MAX_FRAME_SIZE);
_rx_len = 0;
resetReceivedFrameHeader();
return 0;
}

if (_rx_len < frame_length) return 0; // need more bytes

memcpy(dest, _rx_buf, frame_length);
memmove(_rx_buf, _rx_buf + frame_length, _rx_len - frame_length);
_rx_len -= frame_length;
resetReceivedFrameHeader();
return frame_length;
}

size_t LinuxTcpInterface::checkRecvFrame(uint8_t dest[]) {
if (!_enabled) return 0;

acceptIfNeeded(); // kicks the previous client if a new one connects

if (_client_fd < 0) {
if (_device_connected) _device_connected = false;
return 0;
}

if (!_device_connected) {
_device_connected = true;
fprintf(stderr, "Companion TCP: client connected\n");
}

drainSendQueue();
if (!_device_connected) return 0; // closed during drain

// Top up rx buffer from socket
if (_rx_len < sizeof(_rx_buf)) {
ssize_t n = ::recv(_client_fd, _rx_buf + _rx_len, sizeof(_rx_buf) - _rx_len, MSG_DONTWAIT);
if (n == 0) {
fprintf(stderr, "Companion TCP: client disconnected\n");
closeClient();
return 0;
}
if (n < 0) {
if (errno != EAGAIN && errno != EWOULDBLOCK) {
fprintf(stderr, "Companion TCP: recv error: %s\n", strerror(errno));
closeClient();
return 0;
}
} else {
_rx_len += (size_t)n;
}
}

return readFrameFromBuffer(dest);
}

#endif // ARDULINUX_PLATFORM
58 changes: 58 additions & 0 deletions src/helpers/LinuxTcpInterface.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
#pragma once

#ifdef ARDULINUX_PLATFORM

#include "BaseSerialInterface.h"
#include <stddef.h>
#include <stdint.h>

class LinuxTcpInterface : public BaseSerialInterface {
int _server_fd;
int _client_fd;
bool _enabled;
bool _device_connected;
uint16_t _port;
unsigned long _last_write;

struct FrameHeader {
uint8_t type;
uint16_t length;
};

struct Frame {
uint8_t len;
uint8_t buf[MAX_FRAME_SIZE];
};

FrameHeader _received_frame_header;

static const int FRAME_QUEUE_SIZE = 4;
int _send_queue_len;
Frame _send_queue[FRAME_QUEUE_SIZE];

uint8_t _rx_buf[MAX_FRAME_SIZE + 16];
size_t _rx_len;

void closeClient();
void acceptIfNeeded();
void drainSendQueue();
size_t readFrameFromBuffer(uint8_t dest[]);
bool hasReceivedFrameHeader() const;
void resetReceivedFrameHeader();

public:
LinuxTcpInterface();
~LinuxTcpInterface();

bool begin(uint16_t port, const char* bind_addr);

void enable() override;
void disable() override;
bool isEnabled() const override { return _enabled; }
bool isConnected() const override { return _device_connected; }
bool isWriteBusy() const override { return false; }
size_t writeFrame(const uint8_t src[], size_t len) override;
size_t checkRecvFrame(uint8_t dest[]) override;
};

#endif // ARDULINUX_PLATFORM
Loading
Loading