Public Access
Shell: the console's commands on the device's own screen and keyboard (#67)
An App in the Launcher that runs the same commands as USB serial and the Debug Console, trusted like the first. It shows what the console prints while it is open, through a second ring of the console's that exists only meanwhile; Ctrl+b keeps only what follows your own commands. Tab completes a command's name from the firmware's help text, Fn with up and down recalls earlier lines, Alt with up and down scrolls back. `rm` asks first in the Shell, `rm -f` doesn't. Nothing is kept once the App is left. `screenshot [seconds]` saves the screen as a PNG in /screenshots on the card, now or after a pause: written a row at a time, indexed colour with RGB332 as the palette, in one stored deflate block. 487 host tests (11 new: the PNG writer, the Shell's log filter, Tab). Checked on the device over the Debug Console: commands, Tab, history, a screenshot fetched and decoded on the PC, rm with and without the question, a delayed screenshot of another screen. 12 KB of flash; 7 KB of heap while open. Decisions Q204 to Q212 in docs/milestones/S1.md. Safe Mode: #77. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EhqxQ49eCju4CzKYNjZzwT
This commit is contained in:
@@ -0,0 +1,180 @@
|
||||
#include "apps/shell_app.h"
|
||||
|
||||
#include <Arduino.h>
|
||||
|
||||
#include "app_keys.h"
|
||||
#include "platform/console.h"
|
||||
#include "ui/fonts.h"
|
||||
#include "ui/theme.h"
|
||||
#include "ui/widgets.h"
|
||||
|
||||
namespace roro {
|
||||
|
||||
namespace {
|
||||
|
||||
bool startsWith(const std::string& s, const char* prefix) { return s.rfind(prefix, 0) == 0; }
|
||||
|
||||
} // namespace
|
||||
|
||||
void ShellApp::onEnter() {
|
||||
open_ = console.openShellRing();
|
||||
ringPos_ = 0;
|
||||
scroll_ = 0;
|
||||
confirm_.reset();
|
||||
words_ = commandWords(helpText_);
|
||||
words_.push_back("help");
|
||||
words_.push_back("clear"); // the Shell's own
|
||||
words_.push_back("quit");
|
||||
log_.clear();
|
||||
log_.add(open_ ? "The console's commands. `help` lists them." : "No memory for the Shell: leave an App, or stop IRC.");
|
||||
}
|
||||
|
||||
// Nothing is kept once it's left: the ring, the lines and the list of commands all go (Q207).
|
||||
void ShellApp::onExit() {
|
||||
console.closeShellRing();
|
||||
open_ = false;
|
||||
log_.clear();
|
||||
std::vector<std::string>().swap(words_);
|
||||
confirm_.reset();
|
||||
}
|
||||
|
||||
void ShellApp::update(uint32_t nowMs) {
|
||||
uint8_t buf[256];
|
||||
uint32_t skipped = 0;
|
||||
size_t n;
|
||||
while ((n = console.readShellSince(ringPos_, buf, sizeof buf, skipped)) > 0) {
|
||||
if (skipped) log_.add("[... " + std::to_string(skipped) + " bytes lost: more was printed than fits]");
|
||||
log_.feed(reinterpret_cast<const char*>(buf), n, nowMs);
|
||||
}
|
||||
if (log_.revision() != seenRevision_) {
|
||||
seenRevision_ = log_.revision();
|
||||
requestRedraw();
|
||||
}
|
||||
}
|
||||
|
||||
void ShellApp::runNow(const std::string& line) {
|
||||
log_.commandRun(millis());
|
||||
// The runner echoes the line into the console, where the Shell reads it back like everything
|
||||
// else, except a token being set, which goes nowhere (Q210): that one is shown here, masked.
|
||||
if (startsWith(line, "debug token ") && line != "debug token new") log_.add("> debug token ...");
|
||||
run_(line);
|
||||
}
|
||||
|
||||
void ShellApp::enter(const std::string& line) {
|
||||
history_.add(line);
|
||||
scroll_ = 0;
|
||||
if (line == "quit" || line == "exit") return apps_.home();
|
||||
if (line == "clear") return log_.clear();
|
||||
// Q209: deleting asks first, here where a slip of the finger is a key away. `rm -f` doesn't.
|
||||
if (startsWith(line, "rm ") && !startsWith(line, "rm -f ")) {
|
||||
pending_ = line;
|
||||
confirm_.reset(new DialogModel({"Cancel", "Delete"}));
|
||||
return;
|
||||
}
|
||||
runNow(line);
|
||||
}
|
||||
|
||||
bool ShellApp::onKey(const KeyEvent& e) {
|
||||
requestRedraw();
|
||||
if (confirm_) {
|
||||
confirm_->onKey(e);
|
||||
if (confirm_->result() == DialogModel::kPending) return true;
|
||||
if (confirm_->result() == 1) runNow(pending_);
|
||||
else log_.add("Not deleted.");
|
||||
confirm_.reset();
|
||||
return true;
|
||||
}
|
||||
// Alt + ; / Alt + . scroll back and forward; Up / Down (Fn + ; / Fn + .) recall earlier lines.
|
||||
if (e.key == Key::Char && e.alt && (e.ch == ';' || e.ch == '.')) {
|
||||
if (e.ch == ';') scroll_++;
|
||||
else if (scroll_ > 0) scroll_--;
|
||||
return true;
|
||||
}
|
||||
if (e.key == Key::Char && e.ctrl && (e.ch == 'b' || e.ch == 'B')) { // Q206
|
||||
log_.showBackground(!log_.showsBackground());
|
||||
log_.add(log_.showsBackground() ? "Showing everything the console prints." : "Showing only what follows your commands.");
|
||||
return true;
|
||||
}
|
||||
std::string recalled;
|
||||
switch (e.key) {
|
||||
case Key::Char: input_.insert(e.ch); break;
|
||||
case Key::Delete: input_.backspace(); break;
|
||||
case Key::Left: input_.left(); break;
|
||||
case Key::Right: input_.right(); break;
|
||||
case Key::Up:
|
||||
if (history_.up(input_.text(), recalled)) input_.setText(recalled);
|
||||
break;
|
||||
case Key::Down:
|
||||
if (history_.down(recalled)) input_.setText(recalled);
|
||||
break;
|
||||
case Key::Tab: {
|
||||
std::vector<std::string> matches;
|
||||
input_.setText(completeCommand(input_.text(), words_, matches));
|
||||
if (matches.size() > 1) {
|
||||
std::string all;
|
||||
for (auto& m : matches) all += (all.empty() ? "" : " ") + m;
|
||||
log_.add(all);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case Key::Select: {
|
||||
std::string line = input_.text();
|
||||
input_.setText("");
|
||||
if (!line.empty()) enter(line);
|
||||
break;
|
||||
}
|
||||
default: return false; // Back leaves the Shell
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void ShellApp::help(std::vector<KeyHelp>& out) const {
|
||||
if (confirm_) return keys::add(out, keys::kDialog);
|
||||
keys::add(out, keys::kShell);
|
||||
}
|
||||
|
||||
void ShellApp::draw(Canvas& c) {
|
||||
const auto& area = theme::kContent;
|
||||
const int inputH = theme::kLineHeight + 4;
|
||||
const theme::Rect output{area.x, area.y, area.w, area.h - inputH - 1};
|
||||
const int rows = output.h / theme::kLineHeight;
|
||||
|
||||
// Wrapped from the newest line backwards, only as far as the screen and the scroll need.
|
||||
std::vector<std::pair<std::string, uint16_t>> shown; // newest first
|
||||
auto measure = widgets::bodyMeasure(c);
|
||||
c.setFont(&fonts::body);
|
||||
const auto& lines = log_.lines();
|
||||
int needed = rows + scroll_;
|
||||
for (auto it = lines.rbegin(); it != lines.rend() && static_cast<int>(shown.size()) < needed; ++it) {
|
||||
uint16_t color = it->rfind("> ", 0) == 0 ? theme::kAccent : theme::kText;
|
||||
auto wrapped = wrapText(it->empty() ? std::string(" ") : *it, output.w - 8, measure);
|
||||
for (auto w = wrapped.rbegin(); w != wrapped.rend(); ++w) shown.push_back({*w, color});
|
||||
}
|
||||
int total = static_cast<int>(shown.size());
|
||||
if (scroll_ > total - rows) scroll_ = total > rows ? total - rows : 0;
|
||||
c.setClipRect(output.x, output.y, output.w, output.h);
|
||||
for (int r = 0; r < rows; r++) {
|
||||
int i = scroll_ + (rows - 1 - r); // the row at the bottom is the newest
|
||||
if (i >= total) continue;
|
||||
c.setTextColor(shown[i].second);
|
||||
c.drawString(shown[i].first.c_str(), 4, output.y + r * theme::kLineHeight + 1);
|
||||
}
|
||||
c.clearClipRect();
|
||||
|
||||
// State, not keys: how far back it's scrolled, and whether background lines are hidden.
|
||||
c.setFont(&fonts::small);
|
||||
c.setTextDatum(top_right);
|
||||
if (scroll_ > 0) {
|
||||
c.setTextColor(theme::kWarning);
|
||||
c.drawString(("^ " + std::to_string(scroll_)).c_str(), area.w - 3, output.y + 1);
|
||||
} else if (!log_.showsBackground()) {
|
||||
c.setTextColor(theme::kMuted);
|
||||
c.drawString("mine", area.w - 3, output.y + 1);
|
||||
}
|
||||
c.setTextDatum(top_left);
|
||||
|
||||
widgets::lineEditor(c, input_, {2, area.y + area.h - inputH, area.w - 4, 0});
|
||||
if (confirm_) widgets::dialog(c, "Delete?", pending_.substr(3) + ", with what's inside it. It can't be undone.", *confirm_);
|
||||
}
|
||||
|
||||
} // namespace roro
|
||||
@@ -0,0 +1,55 @@
|
||||
#pragma once
|
||||
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "app.h"
|
||||
#include "app_manager.h"
|
||||
#include "dialog_model.h"
|
||||
#include "input_history.h"
|
||||
#include "line_editor.h"
|
||||
#include "shell_log.h"
|
||||
|
||||
namespace roro {
|
||||
|
||||
// The Shell (issue #67): the console's commands on the device's own screen and keyboard. A third
|
||||
// place to type them, after USB serial and the Debug Console, and trusted like the first: whoever
|
||||
// holds the device can do all of it in Settings anyway (Q205).
|
||||
//
|
||||
// It shows what the console prints while it is open, read from a ring of the console's that exists
|
||||
// only meanwhile; nothing is kept once the App is left.
|
||||
class ShellApp : public App {
|
||||
public:
|
||||
using Run = std::function<void(const std::string& line)>;
|
||||
|
||||
ShellApp(Run run, const char* helpText, AppManager& apps) : run_(std::move(run)), helpText_(helpText), apps_(apps) {}
|
||||
|
||||
void onEnter() override;
|
||||
void onExit() override;
|
||||
bool onKey(const KeyEvent& e) override;
|
||||
bool textEntryActive() const override { return !confirm_; }
|
||||
void update(uint32_t nowMs) override;
|
||||
void draw(Canvas& c) override;
|
||||
void help(std::vector<KeyHelp>& out) const override;
|
||||
|
||||
private:
|
||||
void enter(const std::string& line);
|
||||
void runNow(const std::string& line);
|
||||
|
||||
Run run_;
|
||||
const char* helpText_;
|
||||
AppManager& apps_;
|
||||
ShellLog log_;
|
||||
std::vector<std::string> words_; // the commands Tab completes: built on entering, from `help`
|
||||
LineEditor input_{240};
|
||||
InputHistory history_{16};
|
||||
std::unique_ptr<DialogModel> confirm_;
|
||||
std::string pending_; // the `rm` being asked about
|
||||
uint32_t ringPos_ = 0, seenRevision_ = 0;
|
||||
int scroll_ = 0; // wrapped lines scrolled back from the bottom
|
||||
bool open_ = false;
|
||||
};
|
||||
|
||||
} // namespace roro
|
||||
+80
-4
@@ -7,7 +7,9 @@
|
||||
#include <memory>
|
||||
|
||||
#include "app_manager.h"
|
||||
#include "png_rgb332.h"
|
||||
#include "apps/demo_app.h"
|
||||
#include "apps/shell_app.h"
|
||||
#include "apps/gemini_app.h"
|
||||
#include "apps/gnss_app.h"
|
||||
#include "apps/irc_app.h"
|
||||
@@ -151,6 +153,8 @@ extern "C" bool verifyRollbackLater() { return true; } // C linkage, or the wea
|
||||
size_t getArduinoLoopTaskStackSize() { return 6144; }
|
||||
|
||||
static void setupSafeMode(int crashes);
|
||||
static const char* helpText();
|
||||
static void shellRun(const std::string& line);
|
||||
|
||||
void setup() {
|
||||
nvs.begin();
|
||||
@@ -209,6 +213,7 @@ void setup() {
|
||||
apps->registerApp({"lora", "LoRa Scanner", false, new LoraScannerApp(*radioService, *loraCapture, settings, *clockService)});
|
||||
apps->registerApp({"storage", "Storage", false, new StorageApp(*fileOps, *storageService, *clockService, *update, *power, bus)});
|
||||
apps->registerApp({"notes", "Notes", false, new NotesApp(*fileOps, *storageService, *clockService, *power)});
|
||||
apps->registerApp({"shell", "Shell", false, new ShellApp(shellRun, helpText(), *apps)});
|
||||
// Leaving the foreground App makes it save: a note being typed, when the device is powered off.
|
||||
power->beforePowerOff = []() { apps->home(); };
|
||||
apps->registerApp({"system", "System", false,
|
||||
@@ -553,10 +558,11 @@ static const char* const kHelp =
|
||||
"reboot restart\n"
|
||||
"boot other restart into the other app slot (manual Rollback)\n"
|
||||
"log level <0-5> ESP-IDF log level (0 none ... 5 verbose)\n"
|
||||
"ls [folder] | du <path> | mkdir <path> | rm <path> | cp [-f] <from> <to> | mv [-f] <from> <to> | cancel the SD card, with the Storage App's rules\n"
|
||||
"ls [folder] | du <path> | mkdir <path> | rm [-f] <path> | cp [-f] <from> <to> | mv [-f] <from> <to> | cancel the SD card, with the Storage App's rules\n"
|
||||
"screenshot [seconds] the screen as a PNG in /screenshots on the card, now or after a pause\n"
|
||||
"lora probe | lora status | lora rx on|off | lora preset <name> the LoRa radio, receive only\n"
|
||||
"lora capture start|stop a LoRa Capture to /captures/lora (pcap, LoRaTap)\n"
|
||||
"lora sweep on [from MHz] [to MHz] [step kHz] | off | dump RSSI across a band (863 870 100)\n"
|
||||
"lora sweep on [from MHz] [to MHz] [step kHz] | lora sweep off | lora sweep dump RSSI across a band (863 870 100)\n"
|
||||
"lora custom <MHz> <BW kHz> <SF> <CR 5-8> <sync hex> [preamble] e.g. 868.1 125 7 5 34 8 (LoRaWAN)\n"
|
||||
"gnss quiet on|off pause the GNSS receiver while the LoRa radio listens (it costs the radio 8 dB)\n"
|
||||
"gnss status | gnss restart | gnss track start|stop | gnss nmea on|off | gnss send <sentence without $ and checksum>\n"
|
||||
@@ -569,7 +575,7 @@ static const char* const kHelp =
|
||||
"gemini get <url> fetch a Gemini page and report header, size, certificate, heap\n"
|
||||
"irc start | irc stop | irc dump | irc say <buffer> <text>\n"
|
||||
"install <path.ota> Update from SD\n"
|
||||
"update check | list | status | install <tag> the project's releases on Gitea\n"
|
||||
"update check | update list | update status | update install <tag> the project's releases on Gitea\n"
|
||||
"sd card | sd list | cat <path> | log <text> | burst | sound on|off | short | normal\n"
|
||||
"debug status | debug off [seconds] the Debug Console over Wi-Fi (Settings > Debug Console); with seconds, it comes back\n"
|
||||
"debug on | debug token <16 to 64 characters> | debug token new (USB serial only) switch it on, set its token\n"
|
||||
@@ -581,10 +587,61 @@ static const char* const kHelp =
|
||||
"sd fill <folder> <count> makes that many small files there, to test a crowded folder\n"
|
||||
"coredump get (Debug Console only) send the raw core dump: use scripts/rdbg.py coredump\n"
|
||||
"reset (Debug Console only) restart at once, even if the main loop is stuck\n"
|
||||
"get <path> | put <path> <size> <sha256> | screenshot (Debug Console only) binary, see rdbg.py\n"
|
||||
"get <path> | put <path> <size> <sha256> | screenshot (Debug Console only) binary, see rdbg.py: there, a bare `screenshot` sends the screen instead of saving it\n"
|
||||
"quit close the Debug Console connection\n"
|
||||
;
|
||||
|
||||
static const char* helpText() { return kHelp; }
|
||||
|
||||
// `screenshot [seconds]` (issue #67, Q209): the frame as it is composed, as a PNG on the card. The
|
||||
// pause is for getting to the screen you want: from the Shell, "now" is always the Shell.
|
||||
static bool shotPending = false;
|
||||
static uint32_t shotDueMs = 0;
|
||||
static std::atomic<bool> shotSaved{false};
|
||||
|
||||
static void saveScreenshot() {
|
||||
if (!storageService || !storageService->state().present) return (void)console.println("screenshot: error no SD card");
|
||||
char name[40];
|
||||
int64_t now = clockService ? clockService->utcNow() : -1;
|
||||
if (now >= 0) {
|
||||
time_t t = static_cast<time_t>(now);
|
||||
struct tm local;
|
||||
localtime_r(&t, &local);
|
||||
snprintf(name, sizeof name, "/screenshots/%04d%02d%02d-%02d%02d%02d.png", local.tm_year + 1900, local.tm_mon + 1, local.tm_mday,
|
||||
local.tm_hour, local.tm_min, local.tm_sec);
|
||||
} else snprintf(name, sizeof name, "/screenshots/shot-%lu.png", (unsigned long)(millis() / 1000)); // no clock yet
|
||||
std::string path = name;
|
||||
storageService->runJob([path]() {
|
||||
Canvas& frame = screen.canvas();
|
||||
const uint8_t* pixels = static_cast<const uint8_t*>(frame.getBuffer());
|
||||
int w = frame.width(), h = frame.height();
|
||||
if (!pixels) return (void)console.println("screenshot: error no frame");
|
||||
if (!SD.exists("/screenshots")) SD.mkdir("/screenshots");
|
||||
File f = SD.open(path.c_str(), FILE_WRITE);
|
||||
if (!f) return (void)console.printf("screenshot: error the card refused to make %s\n", path.c_str());
|
||||
png::Rgb332Writer writer(w, h, [&f](const uint8_t* data, size_t len) { return f.write(data, len) == len; });
|
||||
bool ok = writer.begin();
|
||||
for (int y = 0; ok && y < h; y++) ok = writer.row(pixels + y * w); // read as it stands: it may tear
|
||||
ok = ok && writer.end();
|
||||
f.close();
|
||||
if (!ok) {
|
||||
SD.remove(path.c_str());
|
||||
return (void)console.println("screenshot: error the card refused a write");
|
||||
}
|
||||
console.printf("screenshot: saved %s (%u bytes)\n", path.c_str(), (unsigned)png::Rgb332Writer::fileSize(w, h));
|
||||
shotSaved = true; // the main loop says so on the screen, after the picture is taken
|
||||
});
|
||||
}
|
||||
|
||||
static void screenshotStep() {
|
||||
if (shotPending && static_cast<int32_t>(millis() - shotDueMs) >= 0) {
|
||||
shotPending = false;
|
||||
saveScreenshot();
|
||||
}
|
||||
if (shotSaved.exchange(false))
|
||||
bus.publish(Event::withText(EventType::Notification, "Screenshot saved in /screenshots", static_cast<int32_t>(NotificationLevel::Info)));
|
||||
}
|
||||
|
||||
// Commands that only touch what Safe Mode starts.
|
||||
static bool safeModeCommand(const String& line) {
|
||||
return line == "help" || line == "info" || line == "tasks" || line == "net" || line == "reboot" || line == "boot other" ||
|
||||
@@ -640,6 +697,16 @@ static void runCommand(String line, bool fromSerial = false) {
|
||||
if (safeMode && !safeModeCommand(line)) return (void)console.println("not available in Safe Mode");
|
||||
if (line == "help") console.print(kHelp);
|
||||
if (line.startsWith("debug ")) return debugCommand(line.substring(6), fromSerial);
|
||||
if (line == "screenshot" || line.startsWith("screenshot ")) {
|
||||
uint32_t seconds = constrain(line.substring(10).toInt(), 0, 60);
|
||||
shotPending = true;
|
||||
shotDueMs = millis() + seconds * 1000;
|
||||
if (seconds) console.printf("screenshot: in %lu s\n", (unsigned long)seconds);
|
||||
return;
|
||||
}
|
||||
// Over the Debug Console these never get here: its own task answers them.
|
||||
if (line == "coredump get" || line == "reset" || line.startsWith("get ") || line.startsWith("put "))
|
||||
return (void)console.println("Debug Console only: scripts/rdbg.py speaks it");
|
||||
if (line == "info") {
|
||||
system_info::printSystem(console);
|
||||
console.printf("wifi: %s, ip %s, rssi %d | sd: %s, %u write faults\n", wifi->ssid().c_str(), wifi->ip().c_str(),
|
||||
@@ -1009,6 +1076,14 @@ static void runCommand(String line, bool fromSerial = false) {
|
||||
}
|
||||
}
|
||||
|
||||
// What the Shell App runs (issue #67): trusted like USB serial (Q205), and echoed into the console so
|
||||
// that a session reads the same from afar, except a token being set (Q210).
|
||||
static void shellRun(const std::string& line) {
|
||||
String l = line.c_str();
|
||||
if (!l.startsWith("debug token ") || l == "debug token new") console.printf("> %s\n", line.c_str());
|
||||
runCommand(l, true);
|
||||
}
|
||||
|
||||
static void serialCommands() {
|
||||
if (upload.active()) return readUploadBytes(); // raw file bytes, not commands
|
||||
static String line;
|
||||
@@ -1098,6 +1173,7 @@ static void loopPass() {
|
||||
serialCommands();
|
||||
remoteCommands();
|
||||
tasksStep();
|
||||
screenshotStep();
|
||||
ipTrialStep();
|
||||
if (noiseTest) noiseTest->step(now);
|
||||
noteStableOnce(now);
|
||||
|
||||
+53
-31
@@ -35,38 +35,71 @@ void Console::captureEspLogs() {
|
||||
if (!espLogNext) espLogNext = esp_log_set_vprintf(teeEspLog); // once: it stays, and costs nothing with the ring closed
|
||||
}
|
||||
|
||||
bool Console::openRing() {
|
||||
if (ring_) return true;
|
||||
auto* fresh = static_cast<uint8_t*>(malloc(kRingBytes));
|
||||
namespace {
|
||||
|
||||
// One ring: the last kRingBytes written, and how many were written in all. Under ringLock.
|
||||
bool openOne(uint8_t*& ring, uint32_t& head) {
|
||||
if (ring) return true;
|
||||
auto* fresh = static_cast<uint8_t*>(malloc(Console::kRingBytes));
|
||||
if (!fresh) return false;
|
||||
portENTER_CRITICAL(&ringLock);
|
||||
head_ = 0;
|
||||
ring_ = fresh;
|
||||
head = 0;
|
||||
ring = fresh;
|
||||
portEXIT_CRITICAL(&ringLock);
|
||||
return true;
|
||||
}
|
||||
|
||||
void Console::closeRing() {
|
||||
void closeOne(uint8_t*& ring) {
|
||||
portENTER_CRITICAL(&ringLock);
|
||||
uint8_t* old = ring_;
|
||||
ring_ = nullptr;
|
||||
uint8_t* old = ring;
|
||||
ring = nullptr;
|
||||
portEXIT_CRITICAL(&ringLock);
|
||||
free(old);
|
||||
}
|
||||
|
||||
void writeOne(uint8_t* ring, uint32_t& head, const uint8_t* data, size_t len) { // inside the lock
|
||||
if (!ring) return;
|
||||
size_t at = head % Console::kRingBytes;
|
||||
size_t first = std::min(len, Console::kRingBytes - at);
|
||||
memcpy(ring + at, data, first);
|
||||
memcpy(ring, data + first, len - first);
|
||||
head += len;
|
||||
}
|
||||
|
||||
size_t readOne(const uint8_t* ring, uint32_t head, uint32_t& pos, uint8_t* out, size_t max, uint32_t& skipped) {
|
||||
portENTER_CRITICAL(&ringLock);
|
||||
size_t n = 0;
|
||||
skipped = 0;
|
||||
if (ring) {
|
||||
uint32_t from = head > Console::kRingBytes ? head - Console::kRingBytes : 0;
|
||||
skipped = pos < from ? from - pos : 0;
|
||||
if (pos < from) pos = from;
|
||||
n = std::min<size_t>(max, head - pos);
|
||||
size_t at = pos % Console::kRingBytes;
|
||||
size_t first = std::min(n, Console::kRingBytes - at);
|
||||
memcpy(out, ring + at, first);
|
||||
memcpy(out + first, ring, n - first);
|
||||
pos += n;
|
||||
}
|
||||
portEXIT_CRITICAL(&ringLock);
|
||||
return n;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool Console::openRing() { return openOne(ring_, head_); }
|
||||
void Console::closeRing() { closeOne(ring_); }
|
||||
bool Console::openShellRing() { return openOne(shellRing_, shellHead_); }
|
||||
void Console::closeShellRing() { closeOne(shellRing_); }
|
||||
|
||||
void Console::toRing(const uint8_t* data, size_t len) {
|
||||
if (len > kRingBytes) {
|
||||
data += len - kRingBytes; // only the tail can fit
|
||||
len = kRingBytes;
|
||||
}
|
||||
portENTER_CRITICAL(&ringLock);
|
||||
if (ring_) {
|
||||
size_t at = head_ % kRingBytes;
|
||||
size_t first = std::min(len, kRingBytes - at);
|
||||
memcpy(ring_ + at, data, first);
|
||||
memcpy(ring_, data + first, len - first);
|
||||
head_ += len;
|
||||
}
|
||||
writeOne(ring_, head_, data, len);
|
||||
writeOne(shellRing_, shellHead_, data, len);
|
||||
portEXIT_CRITICAL(&ringLock);
|
||||
}
|
||||
|
||||
@@ -78,22 +111,11 @@ uint32_t Console::oldest() const {
|
||||
}
|
||||
|
||||
size_t Console::readSince(uint32_t& pos, uint8_t* out, size_t max, uint32_t& skipped) {
|
||||
portENTER_CRITICAL(&ringLock);
|
||||
size_t n = 0;
|
||||
skipped = 0;
|
||||
if (ring_) {
|
||||
uint32_t from = head_ > kRingBytes ? head_ - kRingBytes : 0;
|
||||
skipped = pos < from ? from - pos : 0;
|
||||
if (pos < from) pos = from;
|
||||
n = std::min<size_t>(max, head_ - pos);
|
||||
size_t at = pos % kRingBytes;
|
||||
size_t first = std::min(n, kRingBytes - at);
|
||||
memcpy(out, ring_ + at, first);
|
||||
memcpy(out + first, ring_, n - first);
|
||||
pos += n;
|
||||
}
|
||||
portEXIT_CRITICAL(&ringLock);
|
||||
return n;
|
||||
return readOne(ring_, head_, pos, out, max, skipped);
|
||||
}
|
||||
|
||||
size_t Console::readShellSince(uint32_t& pos, uint8_t* out, size_t max, uint32_t& skipped) {
|
||||
return readOne(shellRing_, shellHead_, pos, out, max, skipped);
|
||||
}
|
||||
|
||||
size_t Console::write(const uint8_t* data, size_t len) {
|
||||
|
||||
@@ -34,9 +34,17 @@ class Console : public Print {
|
||||
// The ring only, for output that already reaches the serial port another way.
|
||||
void toRing(const uint8_t* data, size_t len);
|
||||
|
||||
// A second ring of the same kind, for the Shell App (issue #67): open while the App is, so that
|
||||
// it can show what the console prints. The two don't know of each other.
|
||||
bool openShellRing();
|
||||
void closeShellRing();
|
||||
size_t readShellSince(uint32_t& pos, uint8_t* out, size_t max, uint32_t& skipped);
|
||||
|
||||
private:
|
||||
uint8_t* ring_ = nullptr;
|
||||
uint32_t head_ = 0; // total bytes written since the ring opened; it holds the last kRingBytes of them
|
||||
uint8_t* shellRing_ = nullptr;
|
||||
uint32_t shellHead_ = 0;
|
||||
};
|
||||
|
||||
extern Console console;
|
||||
|
||||
Reference in New Issue
Block a user