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
25 changes: 13 additions & 12 deletions examples/simple_repeater/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,11 @@ SimpleMeshTables tables;

MyMesh the_mesh(board, radio_driver, *new ArduinoMillis(), fast_rng, rtc_clock, tables);

// CLI console stream. On Linux the interactive CLI runs over a Unix-domain
// socket (so the socket carries only the CLI while Serial keeps the logs); on
// MCU targets, and as the Linux fallback, it is just Serial.
// CLI console stream. On Linux the interactive CLI runs over a pseudo-terminal
// (so the PTY carries only the CLI while Serial keeps the logs) and over stdin
// when meshcored runs in a terminal; on MCU targets it is just Serial.
#if defined(ARDULINUX_PLATFORM) || defined(LINUX_PLATFORM)
#include <helpers/LinuxConsole.h>
static LinuxConsole linux_console;
#endif
static Stream* console = &Serial;

Expand Down Expand Up @@ -75,15 +74,17 @@ void setup() {
#endif

#if defined(ARDULINUX_PLATFORM) || defined(LINUX_PLATFORM)
// Bring up the local CLI console (config path, or a per-user default). Log
// which console is active so a failure (e.g. an unwritable configured path) is
// diagnosable rather than a silent no-CLI daemon; on failure the CLI stays on
// Serial (stdin/stdout).
if (linux_console.begin(board.config.console_path)) {
console = &linux_console;
Serial.print("CLI console on "); Serial.println(linux_console.path());
// Bring up the local CLI console: the PTY at console_path (or the default
// search order), plus stdin when that is a terminal. Log which so a failure
// (an unwritable configured path, say) is diagnosable rather than a silent
// no-CLI daemon. Serial's read() is a stub on Linux, so there is no fallback
// to it: without a PTY the CLI is reachable only from a terminal's stdin.
Console.begin(board.config.console_path);
console = &Console;
if (Console.hasPty()) {
Serial.print("CLI console on "); Serial.println(Console.path());
} else {
Serial.println("CLI console: unavailable (see stderr), using stdio");
Serial.println("CLI console: no PTY (see stderr); stdin only, if this is a terminal");
}
#endif

Expand Down
2 changes: 2 additions & 0 deletions platformio.ini
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,8 @@ build_src_filter =
+<../src/Packet.cpp>
+<../src/helpers/ConfigSerializer.cpp>
+<../src/helpers/DynamicConfigSerializer.cpp>
+<../src/helpers/PtyConsole.cpp>
+<../src/helpers/LinuxConsole.cpp>
+<../variants/linux/LinuxEventLoop.cpp>
+<../variants/linux/LinuxRadioWait.cpp>
lib_deps =
Expand Down
98 changes: 98 additions & 0 deletions src/helpers/LinuxConsole.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
#include "LinuxConsole.h"

#if defined(ARDULINUX_PLATFORM) || defined(LINUX_PLATFORM) || defined(PIO_UNIT_TESTING)

#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>

LinuxConsole Console;

LinuxConsole::~LinuxConsole() { end(); }

bool LinuxConsole::begin(const char* link) {
bool pty_ok = _pty.begin(link);

// Foreground use: read keystrokes one at a time, no kernel echo (we echo
// ourselves, exactly as the PTY path does), Enter delivered as the '\r' the
// CLI terminates on, and never block the mesh loop waiting for input. ISIG
// stays on so Ctrl-C still stops the daemon.
_stdin_tty = isatty(STDIN_FILENO);
if (_stdin_tty && !_raw_active && tcgetattr(STDIN_FILENO, &_orig_tty) == 0) {
struct termios raw = _orig_tty;
raw.c_lflag &= ~(ICANON | ECHO);
raw.c_iflag &= ~(ICRNL | INLCR);
raw.c_cc[VMIN] = 0;
raw.c_cc[VTIME] = 0;
_raw_active = tcsetattr(STDIN_FILENO, TCSANOW, &raw) == 0;
}
if (_stdin_tty) {
int fl = fcntl(STDIN_FILENO, F_GETFL, 0);
if (fl != -1) fcntl(STDIN_FILENO, F_SETFL, fl | O_NONBLOCK);
}
return pty_ok || _stdin_tty;
}

void LinuxConsole::end() {
_pty.end();
if (_raw_active) {
tcsetattr(STDIN_FILENO, TCSANOW, &_orig_tty);
_raw_active = false;
}
_stdin_tty = false;
_peeked = -1;
}

int LinuxConsole::stdinFd() const {
return _stdin_tty ? STDIN_FILENO : -1;
}

// The next byte from either source, or -1. The PTY is asked first, but nothing
// waits on it: whichever has a byte delivers it, so neither can starve the
// other or leave itself readable and unread.
int LinuxConsole::fetch() {
int c = _pty.read(); // already maps '\n' -> '\r'
if (c >= 0) { _src = FROM_PTY; return c; }
if (_stdin_tty) {
unsigned char b;
if (::read(STDIN_FILENO, &b, 1) == 1) {
_src = FROM_STDIN;
return (b == '\n') ? '\r' : b; // same mapping, same reason
}
}
return -1;
}

int LinuxConsole::available() {
if (_peeked < 0) _peeked = fetch();
return _peeked >= 0 ? 1 : 0;
}

int LinuxConsole::peek() {
if (_peeked < 0) _peeked = fetch();
return _peeked;
}

int LinuxConsole::read() {
int c = peek();
_peeked = -1;
return c;
}

size_t LinuxConsole::write(uint8_t c) {
putchar(c); // journald's copy, or the terminal
if (_src == FROM_PTY) _pty.write(c); // and the attached console client
// stdout is line-buffered (ardulinux sets that up for Serial). A command
// being typed at the terminal has no newline yet, and its echo would sit
// in the buffer until Enter -- the user types blind. Flush per byte only
// there: nobody is watching journald arrive character by character.
if (_stdin_tty) fflush(stdout);
return 1;
}

void LinuxConsole::flush() {
fflush(stdout);
_pty.flush();
}

#endif // ARDULINUX_PLATFORM || LINUX_PLATFORM || PIO_UNIT_TESTING
99 changes: 70 additions & 29 deletions src/helpers/LinuxConsole.h
Original file line number Diff line number Diff line change
@@ -1,45 +1,86 @@
#pragma once

#if defined(ARDULINUX_PLATFORM) || defined(LINUX_PLATFORM)
#if defined(ARDULINUX_PLATFORM) || defined(LINUX_PLATFORM) || defined(PIO_UNIT_TESTING)

#include <Arduino.h>
#include <stdio.h>
#include <termios.h>
#include "PtyConsole.h"

// Arduino Stream adapter over PtyConsole.
// Arduino Stream adapter over PtyConsole, plus stdin when that is a terminal.
//
// The repeater's text CLI reads from / writes to this instead of Serial, so the
// PTY carries only the CLI while Serial (stdout) keeps the debug logs — the
// clean log/CLI split. write() also mirrors each byte to stdout, so admin
// commands and their replies are still recorded in the log (journald) alongside
// the debug output, while the console stays free of log noise.
// PTY carries only the CLI while Serial (stdout) keeps the debug logs -- the
// clean log/CLI split. On Linux Serial's read() is a stub that never returns a
// byte, so this is the only way a command reaches the daemon at all.
//
// A client attaches to the published PTY symlink, e.g.:
// meshcore-cli -r -s /run/meshcored/console
// Two doors onto one CLI:
//
// All the PTY mechanics live in PtyConsole (host-unit-tested); this adapter is a
// thin delegating shim.
// * the PTY, published at a stable path (see PtyConsole) for
// meshcore-cli -r -s <path> or any serial tool; and
// * stdin, when meshcored runs in a foreground terminal, so a developer can
// type at it without attaching anything.
//
// Both are drained on every read, so both can be watched by the event loop
// permanently. Output follows the input: every byte is written to stdout (when
// the command came over the PTY that is the copy journald keeps, alongside the
// debug log), and to the PTY only when the current command arrived there -- a
// foreground session's echo must not sit in the PTY's buffer waiting to greet
// the next client.
class LinuxConsole : public Stream {
PtyConsole _pty;
enum Source { FROM_PTY, FROM_STDIN };

PtyConsole _pty;
bool _stdin_tty = false;
bool _raw_active = false; // stdin termios changed; restore on end()
struct termios _orig_tty;
int _peeked = -1; // one-byte lookahead over both sources
Source _src = FROM_PTY; // where the byte last read came from

int fetch();

public:
// Open the PTY and publish the symlink (see PtyConsole::begin). Returns true
// on success; on failure the caller should fall back to Serial.
bool begin(const char *link) { return _pty.begin(link); }
void end() { _pty.end(); }
bool isOpen() const { return _pty.isOpen(); }
const char *path() const { return _pty.path(); }

int available() override { return _pty.available(); }
int read() override { return _pty.read(); }
int peek() override { return _pty.peek(); }
size_t write(uint8_t c) override {
putchar(c); // mirror to stdout so commands/replies reach journald
_pty.write(c); // and to the attached console client
return 1;
}
void flush() override { fflush(stdout); _pty.flush(); }
LinuxConsole() = default;
~LinuxConsole();

// The descriptors below are owned, not shared, and stdin's terminal state is
// process-wide. There is exactly one console.
LinuxConsole(const LinuxConsole&) = delete;
LinuxConsole& operator=(const LinuxConsole&) = delete;

// Publish the PTY console (see PtyConsole::begin for how `link` and the
// default search order are used) and, when stdin is a terminal, put it in
// raw non-blocking mode for keystrokes. Never blocks. Returns true if at
// least one input is usable; a failure is reported on stderr either way.
bool begin(const char* link);

// Unpublish and close the PTY and restore the terminal. Idempotent.
//
// Exists for LinuxBoard::reboot(), which re-execs this process image: the
// descriptors are close-on-exec, but the symlink is on disk, and a symlink
// left pointing at a /dev/pts/N that the exec has just freed would name
// whatever terminal next reuses that number. Removing it here first makes
// the new image's begin() find nothing to reclaim rather than something to
// decline.
void end();

bool hasPty() const { return _pty.isOpen(); }
const char* path() const { return _pty.path(); }

// Descriptors to watch for readability: the PTY master (-1 when there is
// none) and stdin (-1 unless it is a terminal). Every byte either delivers
// is consumed by read(), so registering both permanently cannot leave a
// level-triggered POLLIN that nothing drains.
int ptyFd() const { return _pty.fd(); }
int stdinFd() const;

int available() override;
int peek() override;
int read() override;
size_t write(uint8_t c) override;
void flush() override;
using Print::write; // pull in write(str) / write(buf, size)
};

#endif // ARDULINUX_PLATFORM || LINUX_PLATFORM
extern LinuxConsole Console;

#endif // ARDULINUX_PLATFORM || LINUX_PLATFORM || PIO_UNIT_TESTING
Loading
Loading