Public Access
Shell: only its own replies, Apps by their names, and rm as Unix has it (#67)
The Shell shows the replies to its own commands and nothing else. The console knows who each line is printed for (Console::As, Console::origin): a command run from the Shell prints as the Shell's, and what answers it later from another task carries that along (ls, tasks, du, cp, update check, sd list, screenshot, gemini get). Ctrl+b shows everything instead. This replaces the ten-second window, which was a guess. An App's name with a capital opens it (Notes, Irc, Wifi, Gnss, Gemini, Lora, Storage, Shell, System, Settings), from the Shell and from the consoles. rm needs -r for a folder, here and over the consoles. In the Shell a file, or a folder with something in it, is asked about unless -f; an empty folder with -r goes without a word. The Shell now hands its line to the main loop to run: run from inside the key handler, rm on a folder overflowed the loop's stack and crashed the device. `info` says which App is in front. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EhqxQ49eCju4CzKYNjZzwT
This commit is contained in:
@@ -158,7 +158,7 @@ A note holds up to 16 KB while it's edited. A bigger text file opens read-only i
|
|||||||
|
|
||||||
## Shell
|
## Shell
|
||||||
|
|
||||||
The Shell App (docs/milestones/S1.md) runs the commands below on the device's own screen and keyboard: no PC, no cable, no Wi-Fi. It shows what the console prints while it is open; Ctrl+b hides everything but what follows your own commands. Tab completes a command's name, Fn with up and down recalls earlier lines, Alt with up and down scrolls back. `rm` asks before deleting (`rm -f` doesn't), `clear` empties the screen and `quit` leaves. It is trusted like the USB port: `debug on` and `debug token` work from it. It uses about 7 KB of memory while it is open, and none otherwise.
|
The Shell App (docs/milestones/S1.md) runs the commands below on the device's own screen and keyboard: no PC, no cable, no Wi-Fi. **It shows the replies to its own commands and nothing else**: the console knows who each line is printed for, so a listing read by another task a moment later is still the Shell's, and what USB or the Debug Console asked for is not. Ctrl+b shows everything instead. Tab completes a command's name, Fn with up and down recalls earlier lines, Alt with up and down scrolls back. **An App's name with a capital opens it** (`Notes`, `Irc`, `Wifi`, `Gnss`, `Gemini`, `Lora`, `Storage`, `System`, `Settings`), from the consoles too. `rm` is Unix's, with a question: a folder needs `-r`; a file, or a folder with something in it, is asked about unless `-f` (`rm -rf`); an empty folder goes without a word. `clear` empties the screen and `quit` leaves. It is trusted like the USB port: `debug on` and `debug token` work from it. It uses about 7 KB of memory while it is open, and none otherwise.
|
||||||
|
|
||||||
## Development aids
|
## Development aids
|
||||||
|
|
||||||
@@ -185,14 +185,15 @@ The Shell App (docs/milestones/S1.md) runs the commands below on the device's ow
|
|||||||
| `irc say <buffer> <text>` | Types into a Buffer, commands included (`irc say 0 /join #test`) |
|
| `irc say <buffer> <text>` | Types into a Buffer, commands included (`irc say 0 /join #test`) |
|
||||||
| `irc dump` | Prints IRC status, memory, and the last lines of each Buffer |
|
| `irc dump` | Prints IRC status, memory, and the last lines of each Buffer |
|
||||||
| `wifi status` | Prints Wi-Fi state, network, signal, clock and free heap, then the address, gateway, DNS and NTP servers in use and where each came from |
|
| `wifi status` | Prints Wi-Fi state, network, signal, clock and free heap, then the address, gateway, DNS and NTP servers in use and where each came from |
|
||||||
| `info` | Firmware, uptime, last start reason, memory, Wi-Fi, the SD card and its write faults since boot, and both app slots with their versions and OTA states |
|
| `info` | Firmware, uptime, last start reason, memory, Wi-Fi, the SD card and its write faults since boot, **which App is in front**, and both app slots with their versions and OTA states |
|
||||||
|
| `Notes`, `Irc`, `Wifi`, `Gnss`, `Gemini`, `Lora`, `Storage`, `Shell`, `System`, `Settings` | Opens that App: a capital letter is an App, not a command |
|
||||||
| `tasks` | FreeRTOS tasks over the next second: state, priority, lowest free stack, share of a core, each core's load, and how many passes the main loop made |
|
| `tasks` | FreeRTOS tasks over the next second: state, priority, lowest free stack, share of a core, each core's load, and how many passes the main loop made |
|
||||||
| `net` | Bytes each network service has read and written since boot |
|
| `net` | Bytes each network service has read and written since boot |
|
||||||
| `reboot` / `boot other` | Restart, or restart into the other app slot (a manual Rollback) |
|
| `reboot` / `boot other` | Restart, or restart into the other app slot (a manual Rollback) |
|
||||||
| `log level <0-5>` | ESP-IDF log level |
|
| `log level <0-5>` | ESP-IDF log level |
|
||||||
| `ls [folder]` / `du <path>` | Lists a folder of the SD card with sizes and dates, or counts the files and bytes under a path |
|
| `ls [folder]` / `du <path>` | Lists a folder of the SD card with sizes and dates, or counts the files and bytes under a path |
|
||||||
| `screenshot [seconds]` | The screen as a PNG in `/screenshots` on the card, now or after a pause to get to the screen you want (240 x 135, about 33 KB). Over the Debug Console a bare `screenshot` sends the screen to the PC instead |
|
| `screenshot [seconds]` | The screen as a PNG in `/screenshots` on the card, now or after a pause to get to the screen you want (240 x 135, about 33 KB). Over the Debug Console a bare `screenshot` sends the screen to the PC instead |
|
||||||
| `cp [-f] <from> <to>` / `mv [-f] <from> <to>` / `rm [-f] <path>` / `mkdir <path>` / `cancel` | What the Storage App does, with its rules: copy (folders too), move or rename, delete (a folder with what's in it), new folder. `-f` replaces a file that's in the way; a tab separates paths that hold spaces; `cancel` stops a copy or a delete |
|
| `cp [-f] <from> <to>` / `mv [-f] <from> <to>` / `rm [-r] [-f] <path>` / `mkdir <path>` / `cancel` | What the Storage App does, with its rules: copy (folders too), move or rename, delete (`rm -r` for a folder and what's in it, as Unix has it), new folder. `-f` replaces a file that's in the way; a tab separates paths that hold spaces; `cancel` stops a copy or a delete |
|
||||||
| `install <path>` | Update from SD with that `.ota` file, as Settings → Firmware does |
|
| `install <path>` | Update from SD with that `.ota` file, as Settings → Firmware does |
|
||||||
| `update check` / `update list` / `update status` / `update install <tag>` | The project's releases on Gitea: look at the latest, list the last ten, say what's known, or download and install one |
|
| `update check` / `update list` / `update status` / `update install <tag>` | The project's releases on Gitea: look at the latest, list the last ten, say what's known, or download and install one |
|
||||||
| `update pretend <version>` / `update probe <host>` / `update damage cut\|flip <n>` / `update daily` | Pretend to run another version (so a release counts as an update), see whether a server's certificate is accepted, cut or damage the next download, run the daily check again |
|
| `update pretend <version>` / `update probe <host>` / `update damage cut\|flip <n>` / `update daily` | Pretend to run another version (so a release counts as an update), see whether a server's certificate is accepted, cut or damage the next download, run the daily check again |
|
||||||
|
|||||||
+23
-6
@@ -147,22 +147,34 @@ The console's commands could only be typed on a PC: over USB, or over Wi-Fi with
|
|||||||
|---|---|
|
|---|---|
|
||||||
| Q204 | **An App, "Shell", in the Launcher**, in every firmware: its commands already work over USB for anyone holding the device. |
|
| Q204 | **An App, "Shell", in the Launcher**, in every firmware: its commands already work over USB for anyone holding the device. |
|
||||||
| Q205 | **Trusted like USB serial**, not like the network: `debug on` and `debug token` work from it, as they do in Settings. The token is never shown. |
|
| Q205 | **Trusted like USB serial**, not like the network: `debug on` and `debug token` work from it, as they do in Settings. The token is never shown. |
|
||||||
| Q206 | **It shows what the console prints while it is open**, replies and background alike, without the free-heap line every ten seconds. **Ctrl+b hides the background:** then only what is printed in the ten seconds after a command is kept. A reply can't be told from other output any better: `ls` and `tasks` answer later, from other tasks. |
|
| Q206 | *Revised the same day, after trying it.* **It shows the replies to its own commands, and only those.** The console knows who each line is printed for (`Console::Origin`): a command run from the Shell prints as the Shell's, and so does what answers it later from another task, which notes who asked and takes it back when it prints (`ls`, `tasks`, `du`, `cp`, `update check`, `sd list`, `screenshot`, `gemini get`). What USB or the Debug Console asked for, and the system's own lines, are not the Shell's. **Ctrl+b shows everything** instead. The first version kept whatever was printed in the ten seconds after a command, which was a guess, and a noisy one. |
|
||||||
| Q207 | **Nothing while it's closed.** Open, a 4 KB ring of the console's and up to 4 KB of lines; both go when the App is left, with the list of commands for Tab. |
|
| Q207 | **Nothing while it's closed.** Open, a 4 KB ring of the console's and up to 4 KB of lines; both go when the App is left, with the list of commands for Tab. |
|
||||||
| Q208 | Enter runs the line; Fn with up and down recalls the last 16; Alt with up and down scrolls back; **Tab completes the command's first word** from the firmware's `help` text. Paths aren't completed. |
|
| Q208 | Enter runs the line; Fn with up and down recalls the last 16; Alt with up and down scrolls back; **Tab completes the command's first word** from the firmware's `help` text. Paths aren't completed. |
|
||||||
| Q209 | **`rm` asks first, `rm -f` doesn't** (in the Shell only: over the consoles, scripts delete as before). **`screenshot [seconds]`** saves the screen as a PNG in `/screenshots`, now or after a pause, since from the Shell "now" is the Shell. `get`, `put`, `coredump get` and `reset` answer `Debug Console only`. |
|
| Q209 | *Revised the same day.* **`rm` is Unix's, with a question.** A folder needs `-r`, here and over the consoles (where `rm <folder>` used to remove it with what was in it). In the Shell, a file, or a folder with something in it, is asked about unless `-f` (`-rf`, `-r -f`); an empty folder with `-r` goes without a word; what `rm` would refuse anyway, it refuses itself. Over the consoles nothing is asked: scripts delete as before. **`screenshot [seconds]`** saves the screen as a PNG in `/screenshots`, now or after a pause, since from the Shell "now" is the Shell. `get`, `put`, `coredump get` and `reset` answer `Debug Console only`. |
|
||||||
| Q210 | Commands are echoed as `> command` into the console, so a session reads the same from afar. **A token being set is not echoed.** |
|
| Q210 | Commands are echoed as `> command` into the console, so a session reads the same from afar. **A token being set is not echoed.** |
|
||||||
| Q211 | **Not in Safe Mode**, which starts no Apps: issue #77. |
|
| Q211 | **Not in Safe Mode**, which starts no Apps: issue #77. |
|
||||||
| Q212 | Its keys are a table in `app_keys.h`, so the help panel and the website have them; a page in the user guide. |
|
| Q212 | Its keys are a table in `app_keys.h`, so the help panel and the website have them; a page in the user guide. |
|
||||||
|
| Q213 | *Added the same day.* **An App's name with a capital opens it:** `Irc`, `Wifi`, `Gnss`, `Gemini`, `Lora`, `Storage`, `Notes`, `Shell`, `System`, `Settings` (the App's id, up to its first dash). From the Shell, without going back to the Launcher, and from the consoles too. The capital says "an App": every command of the firmware is in small letters. Tab completes them. |
|
||||||
|
|
||||||
### As built
|
### As built
|
||||||
|
|
||||||
- **`ShellApp`** (`src/apps/shell_app.cpp`), with its model host-tested in `lib/apps_model/src/shell_log.h`: lines arriving in pieces, the two filters, the 4 KB limit, the command words out of the `help` text, and Tab (7 tests).
|
- **`ShellApp`** (`src/apps/shell_app.cpp`), with its model host-tested in `lib/apps_model/src/shell_log.h`: lines arriving in pieces, the 4 KB limit, the command words out of the `help` text (Apps' names included), and Tab.
|
||||||
- **The console has a second ring** (`Console::openShellRing`), filled like the Debug Console's and independent of it.
|
- **Who a line is for:** `Console::As` marks the calling task as printing for the Shell while it lives, and `Console::origin()` lets a command that answers later carry that to wherever it prints. The console has a second ring for the Shell, which gets the lines marked so, or everything.
|
||||||
|
- **The Shell hands its lines to the main loop**, which runs them like the consoles' commands. See below for why.
|
||||||
|
- **`info` says which App is in front** (`app: Shell`): so that a hand driving the device from afar can look before it types.
|
||||||
- **`screenshot`** writes the PNG a row at a time with no buffer: 8-bit indexed colour, the 256 colours of RGB332 as the palette, the pixels in one stored deflate block (`lib/files/src/png_rgb332.h`, 4 tests). 33,383 bytes for the 240 x 135 screen. A Toast says so once it is on the card, so the Toast is never in the picture.
|
- **`screenshot`** writes the PNG a row at a time with no buffer: 8-bit indexed colour, the 256 colours of RGB332 as the palette, the pixels in one stored deflate block (`lib/files/src/png_rgb332.h`, 4 tests). 33,383 bytes for the 240 x 135 screen. A Toast says so once it is on the card, so the Toast is never in the picture.
|
||||||
- **The `help` text lost its shorthand** (`update check | list | status` is written out), since Tab reads its first words from there.
|
- **The `help` text lost its shorthand** (`update check | list | status` is written out), since Tab reads its first words from there.
|
||||||
- **Cost:** 12 KB of flash, 16 bytes of static RAM. Open: 7 KB of heap (107.6 KB free before, 100.7 with it open, 106.1 after leaving).
|
- **Cost:** 16 KB of flash, 56 bytes of static RAM. Open: 7 KB of heap (107.6 KB free before, 100.7 with it open, 106.1 after leaving).
|
||||||
|
|
||||||
|
### What went wrong while building it
|
||||||
|
|
||||||
|
**The device crashed, and the test sent a message to an IRC channel.** The first version ran a command from inside the key handler. Driven from the Debug Console, that is: the main loop, a remote command, `key select`, the App manager, the Shell, `runCommand` a second time, the file command, and `printf` under all of it. The main loop has under 2 KB of stack to spare; `rm` on a folder went past it. The crash report decoded to exactly that chain.
|
||||||
|
|
||||||
|
The device restarted into the Launcher, and the test script, which did not look, went on typing. Its next Enter opened IRC, which connected, and a few lines later it typed "No" into a channel and pressed Enter. One word, sent to real people, that can't be taken back.
|
||||||
|
|
||||||
|
Two changes came of it. The Shell now **queues** its line and the main loop runs it, at the same stack depth as a console's command. And `info` reports the App in front, which the test script now checks before every line it types.
|
||||||
|
|
||||||
|
### Checks on the device (2026-10-07, driven over the Debug Console with `key`)
|
||||||
### Checks on the device (2026-10-07, driven over the Debug Console with `key`)
|
### Checks on the device (2026-10-07, driven over the Debug Console with `key`)
|
||||||
|
|
||||||
| Check | Result |
|
| Check | Result |
|
||||||
@@ -171,7 +183,12 @@ The console's commands could only be typed on a PC: over USB, or over Wi-Fi with
|
|||||||
| Tab on `in` | `info install` is shown and the line stays; on `u` it becomes `update `; on `l`, `log ls lora loop` |
|
| Tab on `in` | `info install` is shown and the line stays; on `u` it becomes `update `; on `l`, `log ls lora loop` |
|
||||||
| Up | The line before comes back |
|
| Up | The line before comes back |
|
||||||
| `screenshot` | `/screenshots/20261007-104357.png`, 33,383 bytes. Fetched and decoded on the PC: 240 x 135, indexed, every chunk's CRC right, the pixels the Shell's screen |
|
| `screenshot` | `/screenshots/20261007-104357.png`, 33,383 bytes. Fetched and decoded on the PC: 240 x 135, indexed, every chunk's CRC right, the pixels the Shell's screen |
|
||||||
| `rm /shelltest` | Asks. Cancel leaves the folder; Delete removes it. `rm -f` removes without asking |
|
| Output | With a Debug Console client connecting and disconnecting for every key, the Shell shows the commands and their replies and nothing else: `info`, `ls /` (answered by the storage task), `tasks` (answered a second later) |
|
||||||
|
| `rm` on a folder, without `-r` | Refused, the folder stays |
|
||||||
|
| `rm -r` on an empty folder | Removed, no question |
|
||||||
|
| `rm -r` on a folder with files | Asks; Cancel leaves it. `rm -rf` removes it without asking |
|
||||||
|
| `rm` on a file | Asks; Delete removes it |
|
||||||
|
| `No`, Tab, Enter | Completes to `Notes ` and opens Notes. `Shell` from the Debug Console opens the Shell |
|
||||||
| `screenshot 4`, then Home | The picture, taken four seconds later, is of the Launcher: the pause works, and the Toast isn't in it |
|
| `screenshot 4`, then Home | The picture, taken four seconds later, is of the Launcher: the pause works, and the Toast isn't in it |
|
||||||
| `quit` | Back to the Launcher, and the memory comes back |
|
| `quit` | Back to the Launcher, and the memory comes back |
|
||||||
| The help panel in the Shell | Its keys, then the ones that work everywhere |
|
| The help panel in the Shell | Its keys, then the ones that work everywhere |
|
||||||
|
|||||||
@@ -4,12 +4,6 @@
|
|||||||
|
|
||||||
namespace roro {
|
namespace roro {
|
||||||
|
|
||||||
bool ShellLog::wanted(const std::string& line, uint32_t nowMs) const {
|
|
||||||
if (line.rfind("status: heap ", 0) == 0) return false;
|
|
||||||
if (background_) return true;
|
|
||||||
return ranOne_ && nowMs - lastCommandMs_ < kMineMs; // unsigned: right across the clock's wrap
|
|
||||||
}
|
|
||||||
|
|
||||||
void ShellLog::push(const std::string& line) {
|
void ShellLog::push(const std::string& line) {
|
||||||
lines_.push_back(line);
|
lines_.push_back(line);
|
||||||
bytes_ += line.size() + 1;
|
bytes_ += line.size() + 1;
|
||||||
@@ -22,7 +16,7 @@ void ShellLog::push(const std::string& line) {
|
|||||||
|
|
||||||
void ShellLog::add(const std::string& line) { push(line); }
|
void ShellLog::add(const std::string& line) { push(line); }
|
||||||
|
|
||||||
void ShellLog::feed(const char* data, size_t len, uint32_t nowMs) {
|
void ShellLog::feed(const char* data, size_t len) {
|
||||||
for (size_t i = 0; i < len; i++) {
|
for (size_t i = 0; i < len; i++) {
|
||||||
char c = data[i];
|
char c = data[i];
|
||||||
if (c == '\r') continue;
|
if (c == '\r') continue;
|
||||||
@@ -30,7 +24,7 @@ void ShellLog::feed(const char* data, size_t len, uint32_t nowMs) {
|
|||||||
if (partial_.size() < 512) partial_ += c; // a line that never ends doesn't take the heap
|
if (partial_.size() < 512) partial_ += c; // a line that never ends doesn't take the heap
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (wanted(partial_, nowMs)) push(partial_);
|
if (partial_.rfind("status: heap ", 0) != 0) push(partial_);
|
||||||
partial_.clear();
|
partial_.clear();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -61,7 +55,9 @@ std::vector<std::string> commandWords(const char* helpText) {
|
|||||||
if (start != std::string::npos) {
|
if (start != std::string::npos) {
|
||||||
size_t end = one.find(' ', start);
|
size_t end = one.find(' ', start);
|
||||||
std::string word = one.substr(start, end == std::string::npos ? std::string::npos : end - start);
|
std::string word = one.substr(start, end == std::string::npos ? std::string::npos : end - start);
|
||||||
bool plain = !word.empty() && std::all_of(word.begin(), word.end(), [](char ch) { return ch >= 'a' && ch <= 'z'; });
|
// Small letters, and for an App's name a capital first.
|
||||||
|
bool plain = !word.empty() && std::all_of(word.begin() + 1, word.end(), [](char ch) { return ch >= 'a' && ch <= 'z'; }) &&
|
||||||
|
((word[0] >= 'a' && word[0] <= 'z') || (word[0] >= 'A' && word[0] <= 'Z'));
|
||||||
if (plain && std::find(words.begin(), words.end(), word) == words.end()) words.push_back(word);
|
if (plain && std::find(words.begin(), words.end(), word) == words.end()) words.push_back(word);
|
||||||
}
|
}
|
||||||
if (bar == std::string::npos) break;
|
if (bar == std::string::npos) break;
|
||||||
|
|||||||
@@ -8,43 +8,30 @@
|
|||||||
|
|
||||||
namespace roro {
|
namespace roro {
|
||||||
|
|
||||||
// What the Shell App shows (issue #67): the lines the console printed, with the oldest dropped past
|
// What the Shell App shows (issue #67): the lines it was given, with the oldest dropped past a size.
|
||||||
// a size. Everything the firmware prints passes through; the Shell keeps what's worth a place on a
|
// Which lines it is given is the console's business: by default, only what is printed for the
|
||||||
// ten-line screen (Q206).
|
// Shell's own commands (Console::Origin).
|
||||||
class ShellLog {
|
class ShellLog {
|
||||||
public:
|
public:
|
||||||
static constexpr size_t kMaxBytes = 4096;
|
static constexpr size_t kMaxBytes = 4096;
|
||||||
static constexpr uint32_t kMineMs = 10000; // how long after a command its output is taken to be
|
|
||||||
|
|
||||||
// Bytes as the console printed them: lines may arrive in pieces.
|
// Bytes as the console printed them: lines may arrive in pieces. The line with the free heap
|
||||||
void feed(const char* data, size_t len, uint32_t nowMs);
|
// every ten seconds is never kept: it would push everything else off a ten-line screen.
|
||||||
// A line the Shell adds itself: the command typed, an answer of its own.
|
void feed(const char* data, size_t len);
|
||||||
|
// A line the Shell adds itself.
|
||||||
void add(const std::string& line);
|
void add(const std::string& line);
|
||||||
// A command was run now: what follows for a while is "its".
|
|
||||||
void commandRun(uint32_t nowMs) {
|
|
||||||
lastCommandMs_ = nowMs;
|
|
||||||
ranOne_ = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// All, or mine only: with background lines hidden, only what's printed in the ten seconds after
|
|
||||||
// a command is kept. A reply can't be told from other output any better: `ls` and `tasks` answer
|
|
||||||
// later, from other tasks. The line with the free heap every ten seconds is never kept.
|
|
||||||
void showBackground(bool show) { background_ = show; }
|
|
||||||
bool showsBackground() const { return background_; }
|
|
||||||
|
|
||||||
const std::deque<std::string>& lines() const { return lines_; }
|
const std::deque<std::string>& lines() const { return lines_; }
|
||||||
void clear();
|
void clear();
|
||||||
uint32_t revision() const { return revision_; } // changes when the lines do
|
uint32_t revision() const { return revision_; } // changes when the lines do
|
||||||
|
|
||||||
private:
|
private:
|
||||||
bool wanted(const std::string& line, uint32_t nowMs) const;
|
|
||||||
void push(const std::string& line);
|
void push(const std::string& line);
|
||||||
|
|
||||||
std::deque<std::string> lines_;
|
std::deque<std::string> lines_;
|
||||||
std::string partial_;
|
std::string partial_;
|
||||||
size_t bytes_ = 0;
|
size_t bytes_ = 0;
|
||||||
bool background_ = true, ranOne_ = false;
|
uint32_t revision_ = 0;
|
||||||
uint32_t lastCommandMs_ = 0, revision_ = 0;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// The commands `help` lists: the first word of each (`ls [folder] | du <path>` gives ls and du),
|
// The commands `help` lists: the first word of each (`ls [folder] | du <path>` gives ls and du),
|
||||||
|
|||||||
@@ -329,12 +329,13 @@ inline constexpr KeyHelp kShell[] = {
|
|||||||
{"Tab", "complete the command"},
|
{"Tab", "complete the command"},
|
||||||
{"Fn ; .", "lines you typed before"},
|
{"Fn ; .", "lines you typed before"},
|
||||||
{"Alt ; .", "scroll back, forward"},
|
{"Alt ; .", "scroll back, forward"},
|
||||||
{"Ctrl b", "all output, or only yours"},
|
{"Ctrl b", "your replies only, or all"},
|
||||||
{"Fn , /", "move the cursor"},
|
{"Fn , /", "move the cursor"},
|
||||||
{"Del", "delete backwards"},
|
{"Del", "delete backwards"},
|
||||||
{"help", "every command"},
|
{"help", "every command"},
|
||||||
{"clear", "an empty screen"},
|
{"clear", "an empty screen"},
|
||||||
{"rm -f", "delete without being asked"},
|
{"Notes", "an App, by its name"},
|
||||||
|
{"rm -rf", "delete without being asked"},
|
||||||
{"quit `", "leave the Shell"},
|
{"quit `", "leave the Shell"},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -82,6 +82,19 @@ void AppManager::handleKey(const KeyEvent& event) {
|
|||||||
if (!consumed && event.key == Key::Back) home();
|
if (!consumed && event.key == Key::Back) home();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::string AppManager::commandFor(const char* id) {
|
||||||
|
std::string command;
|
||||||
|
for (const char* p = id; *p && *p != '-'; p++) command += *p;
|
||||||
|
if (!command.empty() && command[0] >= 'a' && command[0] <= 'z') command[0] = static_cast<char>(command[0] - 'a' + 'A');
|
||||||
|
return command;
|
||||||
|
}
|
||||||
|
|
||||||
|
const AppInfo* AppManager::byCommand(const std::string& command) const {
|
||||||
|
for (auto& info : apps_)
|
||||||
|
if (!info.hidden && commandFor(info.id) == command) return &info;
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
const char* AppManager::foregroundTitle() const {
|
const char* AppManager::foregroundTitle() const {
|
||||||
for (auto& info : apps_)
|
for (auto& info : apps_)
|
||||||
if (info.app == foreground_) return info.title;
|
if (info.app == foreground_) return info.title;
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
#include "app.h"
|
#include "app.h"
|
||||||
@@ -34,6 +35,13 @@ class AppManager {
|
|||||||
void handleKey(const KeyEvent& event);
|
void handleKey(const KeyEvent& event);
|
||||||
void update(uint32_t nowMs) { foreground_->update(nowMs); }
|
void update(uint32_t nowMs) { foreground_->update(nowMs); }
|
||||||
|
|
||||||
|
// The command that opens an App from the consoles and the Shell (issue #67): its id with a
|
||||||
|
// capital letter, up to the first dash. "notes" is Notes, "wifi-tools" is Wifi. The capital says
|
||||||
|
// "an App", where every command of the firmware is in small letters.
|
||||||
|
static std::string commandFor(const char* id);
|
||||||
|
// The App that command names, among the ones the Launcher lists; nullptr if there's none.
|
||||||
|
const AppInfo* byCommand(const std::string& command) const;
|
||||||
|
|
||||||
App& foreground() const { return *foreground_; }
|
App& foreground() const { return *foreground_; }
|
||||||
const char* foregroundTitle() const; // nullptr for the Launcher
|
const char* foregroundTitle() const; // nullptr for the Launcher
|
||||||
|
|
||||||
|
|||||||
@@ -96,4 +96,23 @@ bool looksLikeText(const uint8_t* data, size_t len) {
|
|||||||
return odd * 20 <= len; // a stray control character or two is still text
|
return odd * 20 <= len; // a stray control character or two is still text
|
||||||
}
|
}
|
||||||
|
|
||||||
|
RmArgs parseRm(const std::string& args) {
|
||||||
|
RmArgs out;
|
||||||
|
size_t at = 0;
|
||||||
|
while (at < args.size()) {
|
||||||
|
while (at < args.size() && args[at] == ' ') at++;
|
||||||
|
if (at >= args.size() || args[at] != '-') break;
|
||||||
|
size_t end = args.find(' ', at);
|
||||||
|
std::string flags = args.substr(at + 1, end == std::string::npos ? std::string::npos : end - at - 1);
|
||||||
|
bool known = !flags.empty();
|
||||||
|
for (char c : flags) known = known && (c == 'r' || c == 'R' || c == 'f');
|
||||||
|
if (!known) break; // a name that starts with a dash
|
||||||
|
for (char c : flags) (c == 'f' ? out.force : out.recursive) = true;
|
||||||
|
at = end == std::string::npos ? args.size() : end;
|
||||||
|
}
|
||||||
|
out.path = at < args.size() ? args.substr(at) : "";
|
||||||
|
while (!out.path.empty() && out.path.back() == ' ') out.path.pop_back();
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace roro::files
|
} // namespace roro::files
|
||||||
|
|||||||
@@ -39,4 +39,12 @@ FileKind kindOf(const std::string& name);
|
|||||||
bool opensAtEnd(const std::string& name); // logs
|
bool opensAtEnd(const std::string& name); // logs
|
||||||
bool looksLikeText(const uint8_t* data, size_t len);
|
bool looksLikeText(const uint8_t* data, size_t len);
|
||||||
|
|
||||||
|
// `rm`'s arguments, as Unix has them (issue #67): -r for a folder and what's in it, -f for no
|
||||||
|
// question, alone or together (-rf, -fr, -r -f), then the path, which may hold spaces.
|
||||||
|
struct RmArgs {
|
||||||
|
bool recursive = false, force = false;
|
||||||
|
std::string path;
|
||||||
|
};
|
||||||
|
RmArgs parseRm(const std::string& args);
|
||||||
|
|
||||||
} // namespace roro::files
|
} // namespace roro::files
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ net bytes each network service has read and written since boot
|
|||||||
reboot restart
|
reboot restart
|
||||||
boot other restart into the other app slot (manual Rollback)
|
boot other restart into the other app slot (manual Rollback)
|
||||||
log level <0-5> ESP-IDF log level (0 none ... 5 verbose)
|
log level <0-5> ESP-IDF log level (0 none ... 5 verbose)
|
||||||
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
|
ls [folder] | du <path> | mkdir <path> | rm [-r] [-f] <path> | cp [-f] <from> <to> | mv [-f] <from> <to> | cancel the SD card, with the Storage App's rules (rm -r for a folder; -f: the Shell doesn't ask)
|
||||||
screenshot [seconds] the screen as a PNG in /screenshots on the card, now or after a pause
|
screenshot [seconds] the screen as a PNG in /screenshots on the card, now or after a pause
|
||||||
lora probe | lora status | lora rx on|off | lora preset <name> the LoRa radio, receive only
|
lora probe | lora status | lora rx on|off | lora preset <name> the LoRa radio, receive only
|
||||||
lora capture start|stop a LoRa Capture to /captures/lora (pcap, LoRaTap)
|
lora capture start|stop a LoRa Capture to /captures/lora (pcap, LoRaTap)
|
||||||
@@ -38,6 +38,7 @@ irc start | irc stop | irc dump | irc say <buffer> <text>
|
|||||||
install <path.ota> Update from SD
|
install <path.ota> Update from SD
|
||||||
update check | update list | update status | update install <tag> the project's releases on Gitea
|
update check | update list | update status | update install <tag> the project's releases on Gitea
|
||||||
sd card | sd list | cat <path> | log <text> | burst | sound on|off | short | normal
|
sd card | sd list | cat <path> | log <text> | burst | sound on|off | short | normal
|
||||||
|
Irc | Wifi | Gnss | Gemini | Lora | Storage | Notes | Shell | System | Settings open that App: a capital letter is an App, not a command
|
||||||
debug status | debug off [seconds] the Debug Console over Wi-Fi (Settings > Debug Console); with seconds, it comes back
|
debug status | debug off [seconds] the Debug Console over Wi-Fi (Settings > Debug Console); with seconds, it comes back
|
||||||
debug on | debug token <16 to 64 characters> | debug token new (USB serial only) switch it on, set its token
|
debug on | debug token <16 to 64 characters> | debug token new (USB serial only) switch it on, set its token
|
||||||
crash abort|wdt crash on purpose (to test crash reports and Safe Mode)
|
crash abort|wdt crash on purpose (to test crash reports and Safe Mode)
|
||||||
@@ -79,14 +80,15 @@ In **Safe Mode** (see [Crashes and Safe Mode](/dev/debug/crashes/)) only a few r
|
|||||||
| `irc say <buffer> <text>` | Types into a Buffer, commands included (`irc say 0 /join #test`) |
|
| `irc say <buffer> <text>` | Types into a Buffer, commands included (`irc say 0 /join #test`) |
|
||||||
| `irc dump` | Prints IRC status, memory, and the last lines of each Buffer |
|
| `irc dump` | Prints IRC status, memory, and the last lines of each Buffer |
|
||||||
| `wifi status` | Prints Wi-Fi state, network, signal, clock and free heap, then the address, gateway, DNS and NTP servers in use and where each came from |
|
| `wifi status` | Prints Wi-Fi state, network, signal, clock and free heap, then the address, gateway, DNS and NTP servers in use and where each came from |
|
||||||
| `info` | Firmware, uptime, last start reason, memory, Wi-Fi, the SD card and its write faults since boot, and both app slots with their versions and OTA states |
|
| `info` | Firmware, uptime, last start reason, memory, Wi-Fi, the SD card and its write faults since boot, **which App is in front**, and both app slots with their versions and OTA states |
|
||||||
|
| `Notes`, `Irc`, `Wifi`, `Gnss`, `Gemini`, `Lora`, `Storage`, `Shell`, `System`, `Settings` | Opens that App: a capital letter is an App, not a command |
|
||||||
| `tasks` | FreeRTOS tasks over the next second: state, priority, lowest free stack, share of a core, each core's load, and how many passes the main loop made |
|
| `tasks` | FreeRTOS tasks over the next second: state, priority, lowest free stack, share of a core, each core's load, and how many passes the main loop made |
|
||||||
| `net` | Bytes each network service has read and written since boot |
|
| `net` | Bytes each network service has read and written since boot |
|
||||||
| `reboot` / `boot other` | Restart, or restart into the other app slot (a manual Rollback) |
|
| `reboot` / `boot other` | Restart, or restart into the other app slot (a manual Rollback) |
|
||||||
| `log level <0-5>` | ESP-IDF log level |
|
| `log level <0-5>` | ESP-IDF log level |
|
||||||
| `ls [folder]` / `du <path>` | Lists a folder of the SD card with sizes and dates, or counts the files and bytes under a path |
|
| `ls [folder]` / `du <path>` | Lists a folder of the SD card with sizes and dates, or counts the files and bytes under a path |
|
||||||
| `screenshot [seconds]` | The screen as a PNG in `/screenshots` on the card, now or after a pause to get to the screen you want (240 x 135, about 33 KB). Over the Debug Console a bare `screenshot` sends the screen to the PC instead |
|
| `screenshot [seconds]` | The screen as a PNG in `/screenshots` on the card, now or after a pause to get to the screen you want (240 x 135, about 33 KB). Over the Debug Console a bare `screenshot` sends the screen to the PC instead |
|
||||||
| `cp [-f] <from> <to>` / `mv [-f] <from> <to>` / `rm [-f] <path>` / `mkdir <path>` / `cancel` | What the Storage App does, with its rules: copy (folders too), move or rename, delete (a folder with what's in it), new folder. `-f` replaces a file that's in the way; a tab separates paths that hold spaces; `cancel` stops a copy or a delete |
|
| `cp [-f] <from> <to>` / `mv [-f] <from> <to>` / `rm [-r] [-f] <path>` / `mkdir <path>` / `cancel` | What the Storage App does, with its rules: copy (folders too), move or rename, delete (`rm -r` for a folder and what's in it, as Unix has it), new folder. `-f` replaces a file that's in the way; a tab separates paths that hold spaces; `cancel` stops a copy or a delete |
|
||||||
| `install <path>` | Update from SD with that `.ota` file, as Settings → Firmware does |
|
| `install <path>` | Update from SD with that `.ota` file, as Settings → Firmware does |
|
||||||
| `update check` / `update list` / `update status` / `update install <tag>` | The project's releases on Gitea: look at the latest, list the last ten, say what's known, or download and install one |
|
| `update check` / `update list` / `update status` / `update install <tag>` | The project's releases on Gitea: look at the latest, list the last ten, say what's known, or download and install one |
|
||||||
| `update pretend <version>` / `update probe <host>` / `update damage cut\|flip <n>` / `update daily` | Pretend to run another version (so a release counts as an update), see whether a server's certificate is accepted, cut or damage the next download, run the daily check again |
|
| `update pretend <version>` / `update probe <host>` / `update damage cut\|flip <n>` / `update daily` | Pretend to run another version (so a release counts as an update), see whether a server's certificate is accepted, cut or damage the next download, run the daily check again |
|
||||||
|
|||||||
@@ -24,6 +24,16 @@ Two things to know before you use them:
|
|||||||
|
|
||||||
**`key help` opens the help panel** (<kbd>Fn</kbd>+<kbd>h</kbd> on the device): the keys of the screen that is showing. A screenshot of it is the quickest way to learn what a screen accepts, and it is how every screen's list was checked. Any key but the arrows closes it.
|
**`key help` opens the help panel** (<kbd>Fn</kbd>+<kbd>h</kbd> on the device): the keys of the screen that is showing. A screenshot of it is the quickest way to learn what a screen accepts, and it is how every screen's list was checked. Any key but the arrows closes it.
|
||||||
|
|
||||||
|
## Open an App by its name
|
||||||
|
|
||||||
|
```
|
||||||
|
Notes # an App's name, with a capital: opens it
|
||||||
|
Shell # Irc Wifi Gnss Gemini Lora Storage Notes Shell System Settings
|
||||||
|
info # ...and `app: Notes` says which App is in front
|
||||||
|
```
|
||||||
|
|
||||||
|
Far better than `key home`, some `key down` and `key select`: it doesn't depend on where the Launcher's selection was.
|
||||||
|
|
||||||
## Look before you press
|
## Look before you press
|
||||||
|
|
||||||
**Take a screenshot before any key that deletes, renames or installs.** A blind sequence of `key` commands goes wrong the moment the screen is not where you think it is, and the screen is often not where you think it is: a Toast, a dialog that has not closed, a different App. A sequence that was meant to open a note once renamed real data instead.
|
**Take a screenshot before any key that deletes, renames or installs.** A blind sequence of `key` commands goes wrong the moment the screen is not where you think it is, and the screen is often not where you think it is: a Toast, a dialog that has not closed, a different App. A sequence that was meant to open a note once renamed real data instead.
|
||||||
@@ -36,6 +46,7 @@ scripts/rdbg.py key select
|
|||||||
scripts/rdbg.py screenshot b.png # look again before the next destructive step
|
scripts/rdbg.py screenshot b.png # look again before the next destructive step
|
||||||
```
|
```
|
||||||
|
|
||||||
|
- **Check the App in front before typing anything.** `info` prints `app: <name>`. A crash restarts the device into the Launcher, and a script that goes on typing is typing somewhere else: one of this project's own test scripts sent a word to an IRC channel that way.
|
||||||
- Prefer **reading a state** to assuming it: `info`, `ls <folder>`, `cat <file>`, `irc dump`, `gnss status`, `lora status`, `wifi status`, `update status`.
|
- Prefer **reading a state** to assuming it: `info`, `ls <folder>`, `cat <file>`, `irc dump`, `gnss status`, `lora status`, `wifi status`, `update status`.
|
||||||
- Test on a **scratch folder** on the card, not on your real files.
|
- Test on a **scratch folder** on the card, not on your real files.
|
||||||
- For anything that deletes (`rm`, a delete dialog), `ls` first and `ls` after.
|
- For anything that deletes (`rm`, a delete dialog), `ls` first and `ls` after.
|
||||||
|
|||||||
@@ -155,22 +155,34 @@ The console's commands could only be typed on a PC: over USB, or over Wi-Fi with
|
|||||||
|---|---|
|
|---|---|
|
||||||
| Q204 | **An App, "Shell", in the Launcher**, in every firmware: its commands already work over USB for anyone holding the device. |
|
| Q204 | **An App, "Shell", in the Launcher**, in every firmware: its commands already work over USB for anyone holding the device. |
|
||||||
| Q205 | **Trusted like USB serial**, not like the network: `debug on` and `debug token` work from it, as they do in Settings. The token is never shown. |
|
| Q205 | **Trusted like USB serial**, not like the network: `debug on` and `debug token` work from it, as they do in Settings. The token is never shown. |
|
||||||
| Q206 | **It shows what the console prints while it is open**, replies and background alike, without the free-heap line every ten seconds. **Ctrl+b hides the background:** then only what is printed in the ten seconds after a command is kept. A reply can't be told from other output any better: `ls` and `tasks` answer later, from other tasks. |
|
| Q206 | *Revised the same day, after trying it.* **It shows the replies to its own commands, and only those.** The console knows who each line is printed for (`Console::Origin`): a command run from the Shell prints as the Shell's, and so does what answers it later from another task, which notes who asked and takes it back when it prints (`ls`, `tasks`, `du`, `cp`, `update check`, `sd list`, `screenshot`, `gemini get`). What USB or the Debug Console asked for, and the system's own lines, are not the Shell's. **Ctrl+b shows everything** instead. The first version kept whatever was printed in the ten seconds after a command, which was a guess, and a noisy one. |
|
||||||
| Q207 | **Nothing while it's closed.** Open, a 4 KB ring of the console's and up to 4 KB of lines; both go when the App is left, with the list of commands for Tab. |
|
| Q207 | **Nothing while it's closed.** Open, a 4 KB ring of the console's and up to 4 KB of lines; both go when the App is left, with the list of commands for Tab. |
|
||||||
| Q208 | Enter runs the line; Fn with up and down recalls the last 16; Alt with up and down scrolls back; **Tab completes the command's first word** from the firmware's `help` text. Paths aren't completed. |
|
| Q208 | Enter runs the line; Fn with up and down recalls the last 16; Alt with up and down scrolls back; **Tab completes the command's first word** from the firmware's `help` text. Paths aren't completed. |
|
||||||
| Q209 | **`rm` asks first, `rm -f` doesn't** (in the Shell only: over the consoles, scripts delete as before). **`screenshot [seconds]`** saves the screen as a PNG in `/screenshots`, now or after a pause, since from the Shell "now" is the Shell. `get`, `put`, `coredump get` and `reset` answer `Debug Console only`. |
|
| Q209 | *Revised the same day.* **`rm` is Unix's, with a question.** A folder needs `-r`, here and over the consoles (where `rm <folder>` used to remove it with what was in it). In the Shell, a file, or a folder with something in it, is asked about unless `-f` (`-rf`, `-r -f`); an empty folder with `-r` goes without a word; what `rm` would refuse anyway, it refuses itself. Over the consoles nothing is asked: scripts delete as before. **`screenshot [seconds]`** saves the screen as a PNG in `/screenshots`, now or after a pause, since from the Shell "now" is the Shell. `get`, `put`, `coredump get` and `reset` answer `Debug Console only`. |
|
||||||
| Q210 | Commands are echoed as `> command` into the console, so a session reads the same from afar. **A token being set is not echoed.** |
|
| Q210 | Commands are echoed as `> command` into the console, so a session reads the same from afar. **A token being set is not echoed.** |
|
||||||
| Q211 | **Not in Safe Mode**, which starts no Apps: issue #77. |
|
| Q211 | **Not in Safe Mode**, which starts no Apps: issue #77. |
|
||||||
| Q212 | Its keys are a table in `app_keys.h`, so the help panel and the website have them; a page in the user guide. |
|
| Q212 | Its keys are a table in `app_keys.h`, so the help panel and the website have them; a page in the user guide. |
|
||||||
|
| Q213 | *Added the same day.* **An App's name with a capital opens it:** `Irc`, `Wifi`, `Gnss`, `Gemini`, `Lora`, `Storage`, `Notes`, `Shell`, `System`, `Settings` (the App's id, up to its first dash). From the Shell, without going back to the Launcher, and from the consoles too. The capital says "an App": every command of the firmware is in small letters. Tab completes them. |
|
||||||
|
|
||||||
### As built
|
### As built
|
||||||
|
|
||||||
- **`ShellApp`** (`src/apps/shell_app.cpp`), with its model host-tested in `lib/apps_model/src/shell_log.h`: lines arriving in pieces, the two filters, the 4 KB limit, the command words out of the `help` text, and Tab (7 tests).
|
- **`ShellApp`** (`src/apps/shell_app.cpp`), with its model host-tested in `lib/apps_model/src/shell_log.h`: lines arriving in pieces, the 4 KB limit, the command words out of the `help` text (Apps' names included), and Tab.
|
||||||
- **The console has a second ring** (`Console::openShellRing`), filled like the Debug Console's and independent of it.
|
- **Who a line is for:** `Console::As` marks the calling task as printing for the Shell while it lives, and `Console::origin()` lets a command that answers later carry that to wherever it prints. The console has a second ring for the Shell, which gets the lines marked so, or everything.
|
||||||
|
- **The Shell hands its lines to the main loop**, which runs them like the consoles' commands. See below for why.
|
||||||
|
- **`info` says which App is in front** (`app: Shell`): so that a hand driving the device from afar can look before it types.
|
||||||
- **`screenshot`** writes the PNG a row at a time with no buffer: 8-bit indexed colour, the 256 colours of RGB332 as the palette, the pixels in one stored deflate block (`lib/files/src/png_rgb332.h`, 4 tests). 33,383 bytes for the 240 x 135 screen. A Toast says so once it is on the card, so the Toast is never in the picture.
|
- **`screenshot`** writes the PNG a row at a time with no buffer: 8-bit indexed colour, the 256 colours of RGB332 as the palette, the pixels in one stored deflate block (`lib/files/src/png_rgb332.h`, 4 tests). 33,383 bytes for the 240 x 135 screen. A Toast says so once it is on the card, so the Toast is never in the picture.
|
||||||
- **The `help` text lost its shorthand** (`update check | list | status` is written out), since Tab reads its first words from there.
|
- **The `help` text lost its shorthand** (`update check | list | status` is written out), since Tab reads its first words from there.
|
||||||
- **Cost:** 12 KB of flash, 16 bytes of static RAM. Open: 7 KB of heap (107.6 KB free before, 100.7 with it open, 106.1 after leaving).
|
- **Cost:** 16 KB of flash, 56 bytes of static RAM. Open: 7 KB of heap (107.6 KB free before, 100.7 with it open, 106.1 after leaving).
|
||||||
|
|
||||||
|
### What went wrong while building it
|
||||||
|
|
||||||
|
**The device crashed, and the test sent a message to an IRC channel.** The first version ran a command from inside the key handler. Driven from the Debug Console, that is: the main loop, a remote command, `key select`, the App manager, the Shell, `runCommand` a second time, the file command, and `printf` under all of it. The main loop has under 2 KB of stack to spare; `rm` on a folder went past it. The crash report decoded to exactly that chain.
|
||||||
|
|
||||||
|
The device restarted into the Launcher, and the test script, which did not look, went on typing. Its next Enter opened IRC, which connected, and a few lines later it typed "No" into a channel and pressed Enter. One word, sent to real people, that can't be taken back.
|
||||||
|
|
||||||
|
Two changes came of it. The Shell now **queues** its line and the main loop runs it, at the same stack depth as a console's command. And `info` reports the App in front, which the test script now checks before every line it types.
|
||||||
|
|
||||||
|
### Checks on the device (2026-10-07, driven over the Debug Console with `key`)
|
||||||
### Checks on the device (2026-10-07, driven over the Debug Console with `key`)
|
### Checks on the device (2026-10-07, driven over the Debug Console with `key`)
|
||||||
|
|
||||||
| Check | Result |
|
| Check | Result |
|
||||||
@@ -179,7 +191,12 @@ The console's commands could only be typed on a PC: over USB, or over Wi-Fi with
|
|||||||
| Tab on `in` | `info install` is shown and the line stays; on `u` it becomes `update `; on `l`, `log ls lora loop` |
|
| Tab on `in` | `info install` is shown and the line stays; on `u` it becomes `update `; on `l`, `log ls lora loop` |
|
||||||
| Up | The line before comes back |
|
| Up | The line before comes back |
|
||||||
| `screenshot` | `/screenshots/20261007-104357.png`, 33,383 bytes. Fetched and decoded on the PC: 240 x 135, indexed, every chunk's CRC right, the pixels the Shell's screen |
|
| `screenshot` | `/screenshots/20261007-104357.png`, 33,383 bytes. Fetched and decoded on the PC: 240 x 135, indexed, every chunk's CRC right, the pixels the Shell's screen |
|
||||||
| `rm /shelltest` | Asks. Cancel leaves the folder; Delete removes it. `rm -f` removes without asking |
|
| Output | With a Debug Console client connecting and disconnecting for every key, the Shell shows the commands and their replies and nothing else: `info`, `ls /` (answered by the storage task), `tasks` (answered a second later) |
|
||||||
|
| `rm` on a folder, without `-r` | Refused, the folder stays |
|
||||||
|
| `rm -r` on an empty folder | Removed, no question |
|
||||||
|
| `rm -r` on a folder with files | Asks; Cancel leaves it. `rm -rf` removes it without asking |
|
||||||
|
| `rm` on a file | Asks; Delete removes it |
|
||||||
|
| `No`, Tab, Enter | Completes to `Notes ` and opens Notes. `Shell` from the Debug Console opens the Shell |
|
||||||
| `screenshot 4`, then Home | The picture, taken four seconds later, is of the Launcher: the pause works, and the Toast isn't in it |
|
| `screenshot 4`, then Home | The picture, taken four seconds later, is of the Launcher: the pause works, and the Toast isn't in it |
|
||||||
| `quit` | Back to the Launcher, and the memory comes back |
|
| `quit` | Back to the Launcher, and the memory comes back |
|
||||||
| The help panel in the Shell | Its keys, then the ones that work everywhere |
|
| The help panel in the Shell | Its keys, then the ones that work everywhere |
|
||||||
|
|||||||
@@ -30,13 +30,31 @@ Type a command and press <kbd>Enter</kbd>. `help` lists them all; the [command r
|
|||||||
|
|
||||||
## What you see
|
## What you see
|
||||||
|
|
||||||
The Shell shows **everything the firmware prints** while it is open: the answers to your commands, and whatever the rest of the system says meanwhile (IRC connecting, a packet received). On a ten-line screen that can be a lot.
|
**The replies to your own commands, and nothing else.** The firmware prints a lot besides: IRC connecting, a packet received, whatever a PC on the USB port or the Debug Console is asking for. None of that reaches the Shell. The firmware knows who each line is printed for, so an answer that comes a moment later from another part of it (a folder listing, `tasks`) is still yours.
|
||||||
|
|
||||||
<kbd>Ctrl</kbd> + <kbd>b</kbd> hides the background: then only what is printed in the **ten seconds after each of your commands** is kept, and `mine` shows in the corner. It is ten seconds and not "the answer" because some commands answer a moment later, from another part of the firmware, and nothing marks their lines as theirs.
|
<kbd>Ctrl</kbd> + <kbd>b</kbd> shows **everything** the firmware prints instead, and `all` shows in the corner. Press it again to go back.
|
||||||
|
|
||||||
## Deleting asks first
|
## Opening an App
|
||||||
|
|
||||||
`rm <path>` asks before it deletes, here where a slip of the finger is one key away. `rm -f <path>` does not ask.
|
Type an App's name **with a capital letter** to open it, without going back to the Launcher:
|
||||||
|
|
||||||
|
`Irc` `Wifi` `Gnss` `Gemini` `Lora` `Storage` `Notes` `System` `Settings`
|
||||||
|
|
||||||
|
The capital is the difference: every command is in small letters, every App starts with a capital. <kbd>Tab</kbd> completes them too.
|
||||||
|
|
||||||
|
## Deleting
|
||||||
|
|
||||||
|
`rm` works as it does on Unix, with one addition: it asks.
|
||||||
|
|
||||||
|
| You type | What happens |
|
||||||
|
|---|---|
|
||||||
|
| `rm /notes/a.txt` | Asks, then deletes the file |
|
||||||
|
| `rm -f /notes/a.txt` | Deletes it without asking |
|
||||||
|
| `rm /captures/old` | Refused: it is a folder, and a folder needs `-r` |
|
||||||
|
| `rm -r /captures/old` | Removed at once if it is empty. If not, asks first |
|
||||||
|
| `rm -rf /captures/old` | Removed with everything in it, without asking |
|
||||||
|
|
||||||
|
The folders the firmware keeps its own files in can't be removed, as in the [Storage App](/guide/storage/).
|
||||||
|
|
||||||
## Screenshots
|
## Screenshots
|
||||||
|
|
||||||
|
|||||||
+3
-2
@@ -383,12 +383,13 @@ rows = [
|
|||||||
["Tab", "complete the command"],
|
["Tab", "complete the command"],
|
||||||
["Fn ; .", "lines you typed before"],
|
["Fn ; .", "lines you typed before"],
|
||||||
["Alt ; .", "scroll back, forward"],
|
["Alt ; .", "scroll back, forward"],
|
||||||
["Ctrl b", "all output, or only yours"],
|
["Ctrl b", "your replies only, or all"],
|
||||||
["Fn , /", "move the cursor"],
|
["Fn , /", "move the cursor"],
|
||||||
["Del", "delete backwards"],
|
["Del", "delete backwards"],
|
||||||
["help", "every command"],
|
["help", "every command"],
|
||||||
["clear", "an empty screen"],
|
["clear", "an empty screen"],
|
||||||
["rm -f", "delete without being asked"],
|
["Notes", "an App, by its name"],
|
||||||
|
["rm -rf", "delete without being asked"],
|
||||||
["quit `", "leave the Shell"],
|
["quit `", "leave the Shell"],
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
+28
-12
@@ -2,7 +2,10 @@
|
|||||||
|
|
||||||
#include <Arduino.h>
|
#include <Arduino.h>
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
#include "app_keys.h"
|
#include "app_keys.h"
|
||||||
|
#include "file_names.h"
|
||||||
#include "platform/console.h"
|
#include "platform/console.h"
|
||||||
#include "ui/fonts.h"
|
#include "ui/fonts.h"
|
||||||
#include "ui/theme.h"
|
#include "ui/theme.h"
|
||||||
@@ -18,10 +21,15 @@ bool startsWith(const std::string& s, const char* prefix) { return s.rfind(prefi
|
|||||||
|
|
||||||
void ShellApp::onEnter() {
|
void ShellApp::onEnter() {
|
||||||
open_ = console.openShellRing();
|
open_ = console.openShellRing();
|
||||||
|
console.shellShowsAll(false); // its own replies only, each time it's opened
|
||||||
ringPos_ = 0;
|
ringPos_ = 0;
|
||||||
scroll_ = 0;
|
scroll_ = 0;
|
||||||
confirm_.reset();
|
confirm_.reset();
|
||||||
words_ = commandWords(helpText_);
|
words_ = commandWords(helpText_);
|
||||||
|
for (auto* app : apps_.visibleApps()) { // and the Apps, by their names with a capital
|
||||||
|
std::string name = AppManager::commandFor(app->id);
|
||||||
|
if (std::find(words_.begin(), words_.end(), name) == words_.end()) words_.push_back(name);
|
||||||
|
}
|
||||||
words_.push_back("help");
|
words_.push_back("help");
|
||||||
words_.push_back("clear"); // the Shell's own
|
words_.push_back("clear"); // the Shell's own
|
||||||
words_.push_back("quit");
|
words_.push_back("quit");
|
||||||
@@ -32,19 +40,20 @@ void ShellApp::onEnter() {
|
|||||||
// Nothing is kept once it's left: the ring, the lines and the list of commands all go (Q207).
|
// Nothing is kept once it's left: the ring, the lines and the list of commands all go (Q207).
|
||||||
void ShellApp::onExit() {
|
void ShellApp::onExit() {
|
||||||
console.closeShellRing();
|
console.closeShellRing();
|
||||||
|
console.shellShowsAll(false);
|
||||||
open_ = false;
|
open_ = false;
|
||||||
log_.clear();
|
log_.clear();
|
||||||
std::vector<std::string>().swap(words_);
|
std::vector<std::string>().swap(words_);
|
||||||
confirm_.reset();
|
confirm_.reset();
|
||||||
}
|
}
|
||||||
|
|
||||||
void ShellApp::update(uint32_t nowMs) {
|
void ShellApp::update(uint32_t) {
|
||||||
uint8_t buf[256];
|
uint8_t buf[256];
|
||||||
uint32_t skipped = 0;
|
uint32_t skipped = 0;
|
||||||
size_t n;
|
size_t n;
|
||||||
while ((n = console.readShellSince(ringPos_, buf, sizeof buf, skipped)) > 0) {
|
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]");
|
if (skipped) log_.add("[... " + std::to_string(skipped) + " bytes lost: more was printed than fits]");
|
||||||
log_.feed(reinterpret_cast<const char*>(buf), n, nowMs);
|
log_.feed(reinterpret_cast<const char*>(buf), n);
|
||||||
}
|
}
|
||||||
if (log_.revision() != seenRevision_) {
|
if (log_.revision() != seenRevision_) {
|
||||||
seenRevision_ = log_.revision();
|
seenRevision_ = log_.revision();
|
||||||
@@ -53,9 +62,8 @@ void ShellApp::update(uint32_t nowMs) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void ShellApp::runNow(const std::string& line) {
|
void ShellApp::runNow(const std::string& line) {
|
||||||
log_.commandRun(millis());
|
// The runner echoes the line into the console, where the Shell reads it back with the reply,
|
||||||
// The runner echoes the line into the console, where the Shell reads it back like everything
|
// except a token being set, which goes nowhere (Q210): that one is shown here, masked.
|
||||||
// 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 ...");
|
if (startsWith(line, "debug token ") && line != "debug token new") log_.add("> debug token ...");
|
||||||
run_(line);
|
run_(line);
|
||||||
}
|
}
|
||||||
@@ -65,12 +73,20 @@ void ShellApp::enter(const std::string& line) {
|
|||||||
scroll_ = 0;
|
scroll_ = 0;
|
||||||
if (line == "quit" || line == "exit") return apps_.home();
|
if (line == "quit" || line == "exit") return apps_.home();
|
||||||
if (line == "clear") return log_.clear();
|
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.
|
// Q209: `rm` as Unix has it, with a question where Unix has none, since a slip of the finger is
|
||||||
if (startsWith(line, "rm ") && !startsWith(line, "rm -f ")) {
|
// a key away here. A file, or a folder with something in it, is asked about unless -f says not
|
||||||
|
// to. An empty folder goes without a word; anything `rm` would refuse anyway, it refuses itself.
|
||||||
|
if (startsWith(line, "rm ")) {
|
||||||
|
files::RmArgs args = files::parseRm(line.substr(3));
|
||||||
|
Target target = args.force || args.path.empty() ? Target::Missing : probe_(args.path);
|
||||||
|
bool ask = target == Target::File || (target == Target::FullFolder && args.recursive);
|
||||||
|
if (ask) {
|
||||||
pending_ = line;
|
pending_ = line;
|
||||||
|
question_ = target == Target::File ? args.path + ". It can't be undone." : args.path + " and everything in it. It can't be undone.";
|
||||||
confirm_.reset(new DialogModel({"Cancel", "Delete"}));
|
confirm_.reset(new DialogModel({"Cancel", "Delete"}));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
runNow(line);
|
runNow(line);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -91,8 +107,8 @@ bool ShellApp::onKey(const KeyEvent& e) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (e.key == Key::Char && e.ctrl && (e.ch == 'b' || e.ch == 'B')) { // Q206
|
if (e.key == Key::Char && e.ctrl && (e.ch == 'b' || e.ch == 'B')) { // Q206
|
||||||
log_.showBackground(!log_.showsBackground());
|
console.shellShowsAll(!console.shellShowsAll());
|
||||||
log_.add(log_.showsBackground() ? "Showing everything the console prints." : "Showing only what follows your commands.");
|
log_.add(console.shellShowsAll() ? "Showing everything the console prints." : "Showing only the replies to your commands.");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
std::string recalled;
|
std::string recalled;
|
||||||
@@ -167,14 +183,14 @@ void ShellApp::draw(Canvas& c) {
|
|||||||
if (scroll_ > 0) {
|
if (scroll_ > 0) {
|
||||||
c.setTextColor(theme::kWarning);
|
c.setTextColor(theme::kWarning);
|
||||||
c.drawString(("^ " + std::to_string(scroll_)).c_str(), area.w - 3, output.y + 1);
|
c.drawString(("^ " + std::to_string(scroll_)).c_str(), area.w - 3, output.y + 1);
|
||||||
} else if (!log_.showsBackground()) {
|
} else if (console.shellShowsAll()) {
|
||||||
c.setTextColor(theme::kMuted);
|
c.setTextColor(theme::kMuted);
|
||||||
c.drawString("mine", area.w - 3, output.y + 1);
|
c.drawString("all", area.w - 3, output.y + 1);
|
||||||
}
|
}
|
||||||
c.setTextDatum(top_left);
|
c.setTextDatum(top_left);
|
||||||
|
|
||||||
widgets::lineEditor(c, input_, {2, area.y + area.h - inputH, area.w - 4, 0});
|
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_);
|
if (confirm_) widgets::dialog(c, "Delete?", question_, *confirm_);
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace roro
|
} // namespace roro
|
||||||
|
|||||||
+10
-4
@@ -18,13 +18,18 @@ namespace roro {
|
|||||||
// place to type them, after USB serial and the Debug Console, and trusted like the first: whoever
|
// 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).
|
// 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
|
// It shows the replies to its own commands, and only those unless asked otherwise (Q206): the
|
||||||
// only meanwhile; nothing is kept once the App is left.
|
// console knows who each line was printed for (Console::Origin) and fills a ring that exists only
|
||||||
|
// while the App is open. Nothing is kept once it is left.
|
||||||
class ShellApp : public App {
|
class ShellApp : public App {
|
||||||
public:
|
public:
|
||||||
using Run = std::function<void(const std::string& line)>;
|
using Run = std::function<void(const std::string& line)>;
|
||||||
|
// What `rm` is pointed at: it decides whether the Shell asks first.
|
||||||
|
enum class Target { Missing, File, EmptyFolder, FullFolder };
|
||||||
|
using Probe = std::function<Target(const std::string& path)>;
|
||||||
|
|
||||||
ShellApp(Run run, const char* helpText, AppManager& apps) : run_(std::move(run)), helpText_(helpText), apps_(apps) {}
|
ShellApp(Run run, Probe probe, const char* helpText, AppManager& apps)
|
||||||
|
: run_(std::move(run)), probe_(std::move(probe)), helpText_(helpText), apps_(apps) {}
|
||||||
|
|
||||||
void onEnter() override;
|
void onEnter() override;
|
||||||
void onExit() override;
|
void onExit() override;
|
||||||
@@ -39,6 +44,7 @@ class ShellApp : public App {
|
|||||||
void runNow(const std::string& line);
|
void runNow(const std::string& line);
|
||||||
|
|
||||||
Run run_;
|
Run run_;
|
||||||
|
Probe probe_;
|
||||||
const char* helpText_;
|
const char* helpText_;
|
||||||
AppManager& apps_;
|
AppManager& apps_;
|
||||||
ShellLog log_;
|
ShellLog log_;
|
||||||
@@ -46,7 +52,7 @@ class ShellApp : public App {
|
|||||||
LineEditor input_{240};
|
LineEditor input_{240};
|
||||||
InputHistory history_{16};
|
InputHistory history_{16};
|
||||||
std::unique_ptr<DialogModel> confirm_;
|
std::unique_ptr<DialogModel> confirm_;
|
||||||
std::string pending_; // the `rm` being asked about
|
std::string pending_, question_; // the `rm` being asked about, and what's asked
|
||||||
uint32_t ringPos_ = 0, seenRevision_ = 0;
|
uint32_t ringPos_ = 0, seenRevision_ = 0;
|
||||||
int scroll_ = 0; // wrapped lines scrolled back from the bottom
|
int scroll_ = 0; // wrapped lines scrolled back from the bottom
|
||||||
bool open_ = false;
|
bool open_ = false;
|
||||||
|
|||||||
+84
-9
@@ -3,6 +3,7 @@
|
|||||||
#include <SD.h>
|
#include <SD.h>
|
||||||
|
|
||||||
|
|
||||||
|
#include <deque>
|
||||||
#include <atomic>
|
#include <atomic>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
|
|
||||||
@@ -155,6 +156,7 @@ size_t getArduinoLoopTaskStackSize() { return 6144; }
|
|||||||
static void setupSafeMode(int crashes);
|
static void setupSafeMode(int crashes);
|
||||||
static const char* helpText();
|
static const char* helpText();
|
||||||
static void shellRun(const std::string& line);
|
static void shellRun(const std::string& line);
|
||||||
|
static ShellApp::Target shellProbe(const std::string& path);
|
||||||
|
|
||||||
void setup() {
|
void setup() {
|
||||||
nvs.begin();
|
nvs.begin();
|
||||||
@@ -213,7 +215,7 @@ void setup() {
|
|||||||
apps->registerApp({"lora", "LoRa Scanner", false, new LoraScannerApp(*radioService, *loraCapture, settings, *clockService)});
|
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({"storage", "Storage", false, new StorageApp(*fileOps, *storageService, *clockService, *update, *power, bus)});
|
||||||
apps->registerApp({"notes", "Notes", false, new NotesApp(*fileOps, *storageService, *clockService, *power)});
|
apps->registerApp({"notes", "Notes", false, new NotesApp(*fileOps, *storageService, *clockService, *power)});
|
||||||
apps->registerApp({"shell", "Shell", false, new ShellApp(shellRun, helpText(), *apps)});
|
apps->registerApp({"shell", "Shell", false, new ShellApp(shellRun, shellProbe, helpText(), *apps)});
|
||||||
// Leaving the foreground App makes it save: a note being typed, when the device is powered off.
|
// Leaving the foreground App makes it save: a note being typed, when the device is powered off.
|
||||||
power->beforePowerOff = []() { apps->home(); };
|
power->beforePowerOff = []() { apps->home(); };
|
||||||
apps->registerApp({"system", "System", false,
|
apps->registerApp({"system", "System", false,
|
||||||
@@ -275,10 +277,13 @@ static void setupSafeMode(int crashes) {
|
|||||||
|
|
||||||
// Dev aid: commands to drive the UI without the keyboard, from the serial port or the Debug Console.
|
// Dev aid: commands to drive the UI without the keyboard, from the serial port or the Debug Console.
|
||||||
static bool listingWanted = false;
|
static bool listingWanted = false;
|
||||||
|
// Who asked for each reply that is printed later (Console::Origin): the Shell sees its own, only.
|
||||||
|
static Console::Origin listingFrom, fileOpFrom, updateFrom, tasksFrom;
|
||||||
|
|
||||||
static void printListingWhenReady() {
|
static void printListingWhenReady() {
|
||||||
if (!listingWanted || !storageService->listingReady()) return;
|
if (!listingWanted || !storageService->listingReady()) return;
|
||||||
listingWanted = false;
|
listingWanted = false;
|
||||||
|
Console::As as(listingFrom);
|
||||||
auto listing = storageService->listing();
|
auto listing = storageService->listing();
|
||||||
for (size_t i = 0; i < listing.size(); i++) {
|
for (size_t i = 0; i < listing.size(); i++) {
|
||||||
uint64_t bytes = 0;
|
uint64_t bytes = 0;
|
||||||
@@ -431,6 +436,7 @@ static void fileOpsStep() {
|
|||||||
FileOps::Status s;
|
FileOps::Status s;
|
||||||
if (!consoleFileOp || !fileOps->finished(s)) return;
|
if (!consoleFileOp || !fileOps->finished(s)) return;
|
||||||
consoleFileOp = false;
|
consoleFileOp = false;
|
||||||
|
Console::As as(fileOpFrom);
|
||||||
static const char* const kNames[] = {"", "ls", "du", "cp", "mv", "rm", "mkdir"};
|
static const char* const kNames[] = {"", "ls", "du", "cp", "mv", "rm", "mkdir"};
|
||||||
const char* name = kNames[static_cast<int>(s.op)];
|
const char* name = kNames[static_cast<int>(s.op)];
|
||||||
if (!s.error.empty()) console.printf("%s: error %s\n", name, s.error.c_str());
|
if (!s.error.empty()) console.printf("%s: error %s\n", name, s.error.c_str());
|
||||||
@@ -464,6 +470,7 @@ static void printReleases(bool list) {
|
|||||||
|
|
||||||
static void updateStep() {
|
static void updateStep() {
|
||||||
if (!updateWatch || update->giteaBusy()) return;
|
if (!updateWatch || update->giteaBusy()) return;
|
||||||
|
Console::As as(updateFrom);
|
||||||
if (updateWatch == 3) {
|
if (updateWatch == 3) {
|
||||||
console.printf("update: probe: %s\n", update->probeResult().c_str());
|
console.printf("update: probe: %s\n", update->probeResult().c_str());
|
||||||
updateWatch = 0;
|
updateWatch = 0;
|
||||||
@@ -478,6 +485,7 @@ static void updateCommand(const String& args) {
|
|||||||
if (update->giteaBusy()) return (void)console.println("update: busy");
|
if (update->giteaBusy()) return (void)console.println("update: busy");
|
||||||
args == "check" ? update->requestCheck() : update->requestList();
|
args == "check" ? update->requestCheck() : update->requestList();
|
||||||
updateWatch = args == "check" ? 1 : 2;
|
updateWatch = args == "check" ? 1 : 2;
|
||||||
|
updateFrom = console.origin();
|
||||||
} else if (args == "status") {
|
} else if (args == "status") {
|
||||||
GiteaReleases& g = update->gitea();
|
GiteaReleases& g = update->gitea();
|
||||||
console.printf("update: running %s, failed here before: %s, daily check %s, heap %u\n", update->runningVersion().c_str(),
|
console.printf("update: running %s, failed here before: %s, daily check %s, heap %u\n", update->runningVersion().c_str(),
|
||||||
@@ -494,6 +502,7 @@ static void updateCommand(const String& args) {
|
|||||||
int space = rest.indexOf(' ');
|
int space = rest.indexOf(' ');
|
||||||
update->requestProbe((space < 0 ? rest : rest.substring(0, space)).c_str(), space < 0 ? "/" : rest.substring(space + 1).c_str());
|
update->requestProbe((space < 0 ? rest : rest.substring(0, space)).c_str(), space < 0 ? "/" : rest.substring(space + 1).c_str());
|
||||||
updateWatch = 3;
|
updateWatch = 3;
|
||||||
|
updateFrom = console.origin();
|
||||||
} else if (args.startsWith("damage ")) { // update damage cut <bytes> | flip <offset>: for the next download
|
} else if (args.startsWith("damage ")) { // update damage cut <bytes> | flip <offset>: for the next download
|
||||||
long n = args.substring(args.lastIndexOf(' ') + 1).toInt();
|
long n = args.substring(args.lastIndexOf(' ') + 1).toInt();
|
||||||
args.startsWith("damage cut") ? update->damageNextDownload(n, -1) : update->damageNextDownload(-1, n);
|
args.startsWith("damage cut") ? update->damageNextDownload(n, -1) : update->damageNextDownload(-1, n);
|
||||||
@@ -513,6 +522,7 @@ static void updateCommand(const String& args) {
|
|||||||
static void fileCommand(const String& line) {
|
static void fileCommand(const String& line) {
|
||||||
int space = line.indexOf(' ');
|
int space = line.indexOf(' ');
|
||||||
std::string command = line.substring(0, space).c_str(), rest = line.substring(space + 1).c_str();
|
std::string command = line.substring(0, space).c_str(), rest = line.substring(space + 1).c_str();
|
||||||
|
const std::string everything = rest; // rm reads its own switches
|
||||||
bool force = rest.rfind("-f ", 0) == 0; // replace what's there
|
bool force = rest.rfind("-f ", 0) == 0; // replace what's there
|
||||||
if (force) rest = rest.substr(3);
|
if (force) rest = rest.substr(3);
|
||||||
size_t split = rest.find('\t'); // two paths: a tab between them if either has a space
|
size_t split = rest.find('\t'); // two paths: a tab between them if either has a space
|
||||||
@@ -524,9 +534,13 @@ static void fileCommand(const String& line) {
|
|||||||
if (!storageService->state().present) why = "no SD card";
|
if (!storageService->state().present) why = "no SD card";
|
||||||
else if (command == "du") why = fileOps->count(rest);
|
else if (command == "du") why = fileOps->count(rest);
|
||||||
else if (command == "mkdir") why = fileOps->makeFolder(rest);
|
else if (command == "mkdir") why = fileOps->makeFolder(rest);
|
||||||
else if (command == "rm") {
|
else if (command == "rm") { // as Unix has it: a folder needs -r. The questions are the Shell's (-f is its)
|
||||||
folder = fileOps->isFolder(rest, exists);
|
files::RmArgs args = files::parseRm(everything);
|
||||||
why = exists ? fileOps->remove(rest, folder) : "It isn't there";
|
folder = fileOps->isFolder(args.path, exists);
|
||||||
|
if (args.path.empty()) why = "rm [-r] [-f] <path>";
|
||||||
|
else if (!exists) why = "It isn't there";
|
||||||
|
else if (folder && !args.recursive) why = "It's a folder: rm -r removes it and what's in it";
|
||||||
|
else why = fileOps->remove(args.path, folder);
|
||||||
} else if (b.empty()) why = "two paths, please";
|
} else if (b.empty()) why = "two paths, please";
|
||||||
else {
|
else {
|
||||||
folder = fileOps->isFolder(a, exists);
|
folder = fileOps->isFolder(a, exists);
|
||||||
@@ -538,6 +552,7 @@ static void fileCommand(const String& line) {
|
|||||||
}
|
}
|
||||||
if (!why.empty()) return (void)console.printf("%s: error %s\n", command.c_str(), why.c_str());
|
if (!why.empty()) return (void)console.printf("%s: error %s\n", command.c_str(), why.c_str());
|
||||||
consoleFileOp = true;
|
consoleFileOp = true;
|
||||||
|
fileOpFrom = console.origin();
|
||||||
}
|
}
|
||||||
|
|
||||||
static uint32_t loopPasses = 0; // counted in loop(), for `tasks` (issue #40)
|
static uint32_t loopPasses = 0; // counted in loop(), for `tasks` (issue #40)
|
||||||
@@ -547,6 +562,7 @@ static uint32_t tasksPasses = 0;
|
|||||||
static void tasksStep() {
|
static void tasksStep() {
|
||||||
if (!tasksPending || static_cast<int32_t>(millis() - tasksDueMs) < 0) return;
|
if (!tasksPending || static_cast<int32_t>(millis() - tasksDueMs) < 0) return;
|
||||||
tasksPending = false;
|
tasksPending = false;
|
||||||
|
Console::As as(tasksFrom);
|
||||||
system_info::printTasks(console, tasksBefore, tasksTotal);
|
system_info::printTasks(console, tasksBefore, tasksTotal);
|
||||||
console.printf("loop: %lu passes in the last second, chip %.1f C\n", (unsigned long)(loopPasses - tasksPasses), temperatureRead());
|
console.printf("loop: %lu passes in the last second, chip %.1f C\n", (unsigned long)(loopPasses - tasksPasses), temperatureRead());
|
||||||
std::vector<TaskSample>().swap(tasksBefore);
|
std::vector<TaskSample>().swap(tasksBefore);
|
||||||
@@ -558,7 +574,7 @@ static const char* const kHelp =
|
|||||||
"reboot restart\n"
|
"reboot restart\n"
|
||||||
"boot other restart into the other app slot (manual Rollback)\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"
|
"log level <0-5> ESP-IDF log level (0 none ... 5 verbose)\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"
|
"ls [folder] | du <path> | mkdir <path> | rm [-r] [-f] <path> | cp [-f] <from> <to> | mv [-f] <from> <to> | cancel the SD card, with the Storage App's rules (rm -r for a folder; -f: the Shell doesn't ask)\n"
|
||||||
"screenshot [seconds] the screen as a PNG in /screenshots on the card, now or after a pause\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 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 capture start|stop a LoRa Capture to /captures/lora (pcap, LoRaTap)\n"
|
||||||
@@ -577,6 +593,7 @@ static const char* const kHelp =
|
|||||||
"install <path.ota> Update from SD\n"
|
"install <path.ota> Update from SD\n"
|
||||||
"update check | update list | update status | update 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"
|
"sd card | sd list | cat <path> | log <text> | burst | sound on|off | short | normal\n"
|
||||||
|
"Irc | Wifi | Gnss | Gemini | Lora | Storage | Notes | Shell | System | Settings open that App: a capital letter is an App, not a command\n"
|
||||||
"debug status | debug off [seconds] the Debug Console over Wi-Fi (Settings > Debug Console); with seconds, it comes back\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"
|
"debug on | debug token <16 to 64 characters> | debug token new (USB serial only) switch it on, set its token\n"
|
||||||
"crash abort|wdt crash on purpose (to test crash reports and Safe Mode)\n"
|
"crash abort|wdt crash on purpose (to test crash reports and Safe Mode)\n"
|
||||||
@@ -596,6 +613,7 @@ static const char* helpText() { return kHelp; }
|
|||||||
// `screenshot [seconds]` (issue #67, Q209): the frame as it is composed, as a PNG on the card. The
|
// `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.
|
// pause is for getting to the screen you want: from the Shell, "now" is always the Shell.
|
||||||
static bool shotPending = false;
|
static bool shotPending = false;
|
||||||
|
static Console::Origin shotFrom;
|
||||||
static uint32_t shotDueMs = 0;
|
static uint32_t shotDueMs = 0;
|
||||||
static std::atomic<bool> shotSaved{false};
|
static std::atomic<bool> shotSaved{false};
|
||||||
|
|
||||||
@@ -611,7 +629,9 @@ static void saveScreenshot() {
|
|||||||
local.tm_hour, local.tm_min, local.tm_sec);
|
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
|
} else snprintf(name, sizeof name, "/screenshots/shot-%lu.png", (unsigned long)(millis() / 1000)); // no clock yet
|
||||||
std::string path = name;
|
std::string path = name;
|
||||||
storageService->runJob([path]() {
|
Console::Origin from = shotFrom;
|
||||||
|
storageService->runJob([path, from]() {
|
||||||
|
Console::As as(from);
|
||||||
Canvas& frame = screen.canvas();
|
Canvas& frame = screen.canvas();
|
||||||
const uint8_t* pixels = static_cast<const uint8_t*>(frame.getBuffer());
|
const uint8_t* pixels = static_cast<const uint8_t*>(frame.getBuffer());
|
||||||
int w = frame.width(), h = frame.height();
|
int w = frame.width(), h = frame.height();
|
||||||
@@ -696,10 +716,22 @@ static void runCommand(String line, bool fromSerial = false) {
|
|||||||
if (line.isEmpty()) return;
|
if (line.isEmpty()) return;
|
||||||
if (safeMode && !safeModeCommand(line)) return (void)console.println("not available in Safe Mode");
|
if (safeMode && !safeModeCommand(line)) return (void)console.println("not available in Safe Mode");
|
||||||
if (line == "help") console.print(kHelp);
|
if (line == "help") console.print(kHelp);
|
||||||
|
if (line[0] >= 'A' && line[0] <= 'Z') { // an App, by its name with a capital
|
||||||
|
const AppInfo* app = apps ? apps->byCommand(line.c_str()) : nullptr;
|
||||||
|
if (!app) {
|
||||||
|
console.printf("No App called %s:", line.c_str());
|
||||||
|
if (apps)
|
||||||
|
for (auto* a : apps->visibleApps()) console.printf(" %s", AppManager::commandFor(a->id).c_str());
|
||||||
|
console.println();
|
||||||
|
} else if (apps->open(app->id)) console.printf("%s\n", app->title);
|
||||||
|
else console.println("Not now: Setup is running");
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (line.startsWith("debug ")) return debugCommand(line.substring(6), fromSerial);
|
if (line.startsWith("debug ")) return debugCommand(line.substring(6), fromSerial);
|
||||||
if (line == "screenshot" || line.startsWith("screenshot ")) {
|
if (line == "screenshot" || line.startsWith("screenshot ")) {
|
||||||
uint32_t seconds = constrain(line.substring(10).toInt(), 0, 60);
|
uint32_t seconds = constrain(line.substring(10).toInt(), 0, 60);
|
||||||
shotPending = true;
|
shotPending = true;
|
||||||
|
shotFrom = console.origin();
|
||||||
shotDueMs = millis() + seconds * 1000;
|
shotDueMs = millis() + seconds * 1000;
|
||||||
if (seconds) console.printf("screenshot: in %lu s\n", (unsigned long)seconds);
|
if (seconds) console.printf("screenshot: in %lu s\n", (unsigned long)seconds);
|
||||||
return;
|
return;
|
||||||
@@ -711,6 +743,7 @@ static void runCommand(String line, bool fromSerial = false) {
|
|||||||
system_info::printSystem(console);
|
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(),
|
console.printf("wifi: %s, ip %s, rssi %d | sd: %s, %u write faults\n", wifi->ssid().c_str(), wifi->ip().c_str(),
|
||||||
wifi->rssi(), storageService->state().present ? "present" : "none", (unsigned)sdLastFault().count);
|
wifi->rssi(), storageService->state().present ? "present" : "none", (unsigned)sdLastFault().count);
|
||||||
|
if (apps) console.printf("app: %s%s\n", apps->foregroundTitle() ? apps->foregroundTitle() : "Launcher", apps->help().isOpen() ? " (the help panel is open)" : "");
|
||||||
console.printf("update: %s | debug console: %s\n", update->onProbation() ? "on probation" : "confirmed",
|
console.printf("update: %s | debug console: %s\n", update->onProbation() ? "on probation" : "confirmed",
|
||||||
!debugConsole->on() ? "off" : debugConsole->clientConnected() ? "on, a client connected" : "on");
|
!debugConsole->on() ? "off" : debugConsole->clientConnected() ? "on, a client connected" : "on");
|
||||||
system_info::printSlots(console, nvs);
|
system_info::printSlots(console, nvs);
|
||||||
@@ -724,6 +757,7 @@ static void runCommand(String line, bool fromSerial = false) {
|
|||||||
tasksDueMs = millis() + 1000;
|
tasksDueMs = millis() + 1000;
|
||||||
tasksPasses = loopPasses;
|
tasksPasses = loopPasses;
|
||||||
tasksPending = true;
|
tasksPending = true;
|
||||||
|
tasksFrom = console.origin();
|
||||||
}
|
}
|
||||||
if (line == "net") { // bytes each service has read and written since boot (S1, Q121)
|
if (line == "net") { // bytes each service has read and written since boot (S1, Q121)
|
||||||
for (int i = 0; i < static_cast<int>(net::User::Count); i++) {
|
for (int i = 0; i < static_cast<int>(net::User::Count); i++) {
|
||||||
@@ -750,7 +784,9 @@ static void runCommand(String line, bool fromSerial = false) {
|
|||||||
if (line == "ls" || line.startsWith("ls ")) {
|
if (line == "ls" || line.startsWith("ls ")) {
|
||||||
if (!storageService->state().present) return (void)console.println("sd: no card");
|
if (!storageService->state().present) return (void)console.println("sd: no card");
|
||||||
std::string path = line.length() > 3 ? line.substring(3).c_str() : "/";
|
std::string path = line.length() > 3 ? line.substring(3).c_str() : "/";
|
||||||
storageService->runJob([path]() { // card access stays on the storage task
|
Console::Origin from = console.origin();
|
||||||
|
storageService->runJob([path, from]() { // card access stays on the storage task
|
||||||
|
Console::As as(from);
|
||||||
File dir = SD.open(path.c_str());
|
File dir = SD.open(path.c_str());
|
||||||
if (!dir || !dir.isDirectory()) return (void)console.printf("ls: %s is not a folder\n", path.c_str());
|
if (!dir || !dir.isDirectory()) return (void)console.printf("ls: %s is not a folder\n", path.c_str());
|
||||||
for (File f = dir.openNextFile(); f; f = dir.openNextFile())
|
for (File f = dir.openNextFile(); f; f = dir.openNextFile())
|
||||||
@@ -764,7 +800,9 @@ static void runCommand(String line, bool fromSerial = false) {
|
|||||||
int space = line.lastIndexOf(' ');
|
int space = line.lastIndexOf(' ');
|
||||||
std::string folder = line.substring(8, space).c_str();
|
std::string folder = line.substring(8, space).c_str();
|
||||||
int count = constrain(line.substring(space + 1).toInt(), 0, 1000);
|
int count = constrain(line.substring(space + 1).toInt(), 0, 1000);
|
||||||
storageService->runJob([folder, count]() {
|
Console::Origin from = console.origin();
|
||||||
|
storageService->runJob([folder, count, from]() {
|
||||||
|
Console::As as(from);
|
||||||
int made = 0;
|
int made = 0;
|
||||||
for (int i = 1; i <= count; i++) {
|
for (int i = 1; i <= count; i++) {
|
||||||
char name[24];
|
char name[24];
|
||||||
@@ -950,7 +988,9 @@ static void runCommand(String line, bool fromSerial = false) {
|
|||||||
if (line.startsWith("sd put ")) startUpload(line.substring(7)); // then raw bytes: see uploadStep()
|
if (line.startsWith("sd put ")) startUpload(line.substring(7)); // then raw bytes: see uploadStep()
|
||||||
if (line == "sd card") { // what the card says it is, from its CID register
|
if (line == "sd card") { // what the card says it is, from its CID register
|
||||||
if (!storageService->state().present) return (void)console.println("sd card: no card");
|
if (!storageService->state().present) return (void)console.println("sd card: no card");
|
||||||
storageService->runJob([]() {
|
Console::Origin from = console.origin();
|
||||||
|
storageService->runJob([from]() {
|
||||||
|
Console::As as(from);
|
||||||
uint8_t cid[16];
|
uint8_t cid[16];
|
||||||
const char* type = SD.cardType() == CARD_SDHC ? "SDHC/SDXC" : SD.cardType() == CARD_SD ? "SDSC" : "MMC or unknown";
|
const char* type = SD.cardType() == CARD_SDHC ? "SDHC/SDXC" : SD.cardType() == CARD_SD ? "SDSC" : "MMC or unknown";
|
||||||
if (!sdReadCid(cid)) return (void)console.println("sd card: the card didn't answer");
|
if (!sdReadCid(cid)) return (void)console.println("sd card: the card didn't answer");
|
||||||
@@ -963,6 +1003,7 @@ static void runCommand(String line, bool fromSerial = false) {
|
|||||||
if (line == "sd list") {
|
if (line == "sd list") {
|
||||||
storageService->requestListing();
|
storageService->requestListing();
|
||||||
listingWanted = true;
|
listingWanted = true;
|
||||||
|
listingFrom = console.origin();
|
||||||
}
|
}
|
||||||
if (line.startsWith("gemini trust ")) { // gemini trust <host> <port> <sha256>: accept a changed certificate
|
if (line.startsWith("gemini trust ")) { // gemini trust <host> <port> <sha256>: accept a changed certificate
|
||||||
char host[96] = "", fp[80] = "";
|
char host[96] = "", fp[80] = "";
|
||||||
@@ -1078,11 +1119,44 @@ 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
|
// 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).
|
// that a session reads the same from afar, except a token being set (Q210).
|
||||||
|
//
|
||||||
|
// The App only hands the line over: it is run from the main loop, like the consoles' commands, not
|
||||||
|
// from inside the key handler. Run from there, `rm` on a folder overflowed the main loop's stack
|
||||||
|
// (a key from the Debug Console, the App manager, the Shell, then runCommand a second time and
|
||||||
|
// printf under all of it): the loop has under 2 KB of stack to spare.
|
||||||
|
static std::deque<std::string> shellLines;
|
||||||
|
|
||||||
static void shellRun(const std::string& line) {
|
static void shellRun(const std::string& line) {
|
||||||
|
if (shellLines.size() < 8) shellLines.push_back(line);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void shellCommands() {
|
||||||
|
while (!shellLines.empty()) {
|
||||||
|
std::string line = std::move(shellLines.front());
|
||||||
|
shellLines.pop_front();
|
||||||
|
Console::As as(Console::Origin::Shell); // what it prints, and what answers it later, is the Shell's
|
||||||
String l = line.c_str();
|
String l = line.c_str();
|
||||||
if (!l.startsWith("debug token ") || l == "debug token new") console.printf("> %s\n", line.c_str());
|
if (!l.startsWith("debug token ") || l == "debug token new") console.printf("> %s\n", line.c_str());
|
||||||
runCommand(l, true);
|
runCommand(l, true);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// What a path is, for the Shell to know whether `rm` deserves a question (Q209).
|
||||||
|
static ShellApp::Target shellProbe(const std::string& path) {
|
||||||
|
bool exists = false;
|
||||||
|
bool folder = fileOps->isFolder(path, exists);
|
||||||
|
if (!exists) return ShellApp::Target::Missing;
|
||||||
|
if (!folder) return ShellApp::Target::File;
|
||||||
|
bool empty = true;
|
||||||
|
storageService->runAndWait([&]() {
|
||||||
|
File dir = SD.open(path.c_str());
|
||||||
|
if (dir) {
|
||||||
|
File first = dir.openNextFile();
|
||||||
|
empty = !first;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return empty ? ShellApp::Target::EmptyFolder : ShellApp::Target::FullFolder;
|
||||||
|
}
|
||||||
|
|
||||||
static void serialCommands() {
|
static void serialCommands() {
|
||||||
if (upload.active()) return readUploadBytes(); // raw file bytes, not commands
|
if (upload.active()) return readUploadBytes(); // raw file bytes, not commands
|
||||||
@@ -1172,6 +1246,7 @@ static void loopPass() {
|
|||||||
|
|
||||||
serialCommands();
|
serialCommands();
|
||||||
remoteCommands();
|
remoteCommands();
|
||||||
|
shellCommands();
|
||||||
tasksStep();
|
tasksStep();
|
||||||
screenshotStep();
|
screenshotStep();
|
||||||
ipTrialStep();
|
ipTrialStep();
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
#include <esp_log.h>
|
#include <esp_log.h>
|
||||||
#include <freertos/FreeRTOS.h>
|
#include <freertos/FreeRTOS.h>
|
||||||
|
#include <freertos/task.h>
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <cstdio>
|
#include <cstdio>
|
||||||
@@ -37,6 +38,20 @@ void Console::captureEspLogs() {
|
|||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
|
// The tasks printing As the Shell right now, and how deep each is in it. Under ringLock. Two at
|
||||||
|
// once is the most there is (the main loop, and the storage task finishing a listing).
|
||||||
|
struct ShellTask {
|
||||||
|
TaskHandle_t task;
|
||||||
|
uint8_t depth;
|
||||||
|
};
|
||||||
|
ShellTask shellTasks[4] = {};
|
||||||
|
|
||||||
|
bool fromShellLocked(TaskHandle_t task) {
|
||||||
|
for (auto& t : shellTasks)
|
||||||
|
if (t.depth && t.task == task) return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
// One ring: the last kRingBytes written, and how many were written in all. Under ringLock.
|
// One ring: the last kRingBytes written, and how many were written in all. Under ringLock.
|
||||||
bool openOne(uint8_t*& ring, uint32_t& head) {
|
bool openOne(uint8_t*& ring, uint32_t& head) {
|
||||||
if (ring) return true;
|
if (ring) return true;
|
||||||
@@ -97,9 +112,45 @@ void Console::toRing(const uint8_t* data, size_t len) {
|
|||||||
data += len - kRingBytes; // only the tail can fit
|
data += len - kRingBytes; // only the tail can fit
|
||||||
len = kRingBytes;
|
len = kRingBytes;
|
||||||
}
|
}
|
||||||
|
TaskHandle_t task = xTaskGetCurrentTaskHandle();
|
||||||
portENTER_CRITICAL(&ringLock);
|
portENTER_CRITICAL(&ringLock);
|
||||||
writeOne(ring_, head_, data, len);
|
writeOne(ring_, head_, data, len);
|
||||||
writeOne(shellRing_, shellHead_, data, len);
|
if (shellRing_ && (shellAll_ || fromShellLocked(task))) writeOne(shellRing_, shellHead_, data, len);
|
||||||
|
portEXIT_CRITICAL(&ringLock);
|
||||||
|
}
|
||||||
|
|
||||||
|
Console::Origin Console::origin() const {
|
||||||
|
TaskHandle_t task = xTaskGetCurrentTaskHandle();
|
||||||
|
portENTER_CRITICAL(&ringLock);
|
||||||
|
bool shell = fromShellLocked(task);
|
||||||
|
portEXIT_CRITICAL(&ringLock);
|
||||||
|
return shell ? Origin::Shell : Origin::System;
|
||||||
|
}
|
||||||
|
|
||||||
|
Console::As::As(Origin origin) : entered_(false) {
|
||||||
|
if (origin != Origin::Shell) return;
|
||||||
|
TaskHandle_t task = xTaskGetCurrentTaskHandle();
|
||||||
|
portENTER_CRITICAL(&ringLock);
|
||||||
|
ShellTask* slot = nullptr;
|
||||||
|
for (auto& t : shellTasks)
|
||||||
|
if (t.depth && t.task == task) slot = &t;
|
||||||
|
if (!slot)
|
||||||
|
for (auto& t : shellTasks)
|
||||||
|
if (!t.depth && !slot) slot = &t;
|
||||||
|
if (slot) {
|
||||||
|
slot->task = task;
|
||||||
|
slot->depth++;
|
||||||
|
entered_ = true;
|
||||||
|
}
|
||||||
|
portEXIT_CRITICAL(&ringLock);
|
||||||
|
}
|
||||||
|
|
||||||
|
Console::As::~As() {
|
||||||
|
if (!entered_) return;
|
||||||
|
TaskHandle_t task = xTaskGetCurrentTaskHandle();
|
||||||
|
portENTER_CRITICAL(&ringLock);
|
||||||
|
for (auto& t : shellTasks)
|
||||||
|
if (t.depth && t.task == task) t.depth--;
|
||||||
portEXIT_CRITICAL(&ringLock);
|
portEXIT_CRITICAL(&ringLock);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+23
-2
@@ -34,10 +34,30 @@ class Console : public Print {
|
|||||||
// The ring only, for output that already reaches the serial port another way.
|
// The ring only, for output that already reaches the serial port another way.
|
||||||
void toRing(const uint8_t* data, size_t len);
|
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
|
// Who asked for what is being printed. A command typed in the Shell App prints its reply `As`
|
||||||
// it can show what the console prints. The two don't know of each other.
|
// the Shell, and so does whatever answers it later from another task (a listing read by the
|
||||||
|
// storage task, `tasks` a second on): the one that starts the work notes origin() and the one that
|
||||||
|
// prints takes it back. Everything else is the System's: logs, and the consoles' own commands.
|
||||||
|
enum class Origin : uint8_t { System, Shell };
|
||||||
|
Origin origin() const; // of the task that asks, now
|
||||||
|
class As {
|
||||||
|
public:
|
||||||
|
explicit As(Origin origin);
|
||||||
|
~As();
|
||||||
|
As(const As&) = delete;
|
||||||
|
As& operator=(const As&) = delete;
|
||||||
|
|
||||||
|
private:
|
||||||
|
bool entered_;
|
||||||
|
};
|
||||||
|
|
||||||
|
// A second ring of the same kind, for the Shell App (issue #67): open while the App is. It gets
|
||||||
|
// what is printed As the Shell, and nothing else, so that the Shell shows the replies to its own
|
||||||
|
// commands and not the rest (Q206); or everything, when asked.
|
||||||
bool openShellRing();
|
bool openShellRing();
|
||||||
void closeShellRing();
|
void closeShellRing();
|
||||||
|
void shellShowsAll(bool all) { shellAll_ = all; }
|
||||||
|
bool shellShowsAll() const { return shellAll_; }
|
||||||
size_t readShellSince(uint32_t& pos, uint8_t* out, size_t max, uint32_t& skipped);
|
size_t readShellSince(uint32_t& pos, uint8_t* out, size_t max, uint32_t& skipped);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
@@ -45,6 +65,7 @@ class Console : public Print {
|
|||||||
uint32_t head_ = 0; // total bytes written since the ring opened; it holds the last kRingBytes of them
|
uint32_t head_ = 0; // total bytes written since the ring opened; it holds the last kRingBytes of them
|
||||||
uint8_t* shellRing_ = nullptr;
|
uint8_t* shellRing_ = nullptr;
|
||||||
uint32_t shellHead_ = 0;
|
uint32_t shellHead_ = 0;
|
||||||
|
volatile bool shellAll_ = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
extern Console console;
|
extern Console console;
|
||||||
|
|||||||
@@ -140,6 +140,7 @@ bool GeminiService::fetch(const std::string& url, bool preferSaved) {
|
|||||||
bool GeminiService::fetchToConsole(const std::string& url) {
|
bool GeminiService::fetchToConsole(const std::string& url) {
|
||||||
if (busy_) return false;
|
if (busy_) return false;
|
||||||
toConsole_ = true;
|
toConsole_ = true;
|
||||||
|
consoleFrom_ = console.origin();
|
||||||
if (!fetch(url)) {
|
if (!fetch(url)) {
|
||||||
toConsole_ = false;
|
toConsole_ = false;
|
||||||
return false;
|
return false;
|
||||||
@@ -380,7 +381,10 @@ void GeminiService::runFetch(GeminiPage& page) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
page.ms = millis() - started;
|
page.ms = millis() - started;
|
||||||
if (toConsole_) report(page, before, lowest_);
|
if (toConsole_) {
|
||||||
|
Console::As as(consoleFrom_);
|
||||||
|
report(page, before, lowest_);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// One request, following up to kMaxRedirects redirects.
|
// One request, following up to kMaxRedirects redirects.
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
|
#include "platform/console.h"
|
||||||
#include "event_bus.h"
|
#include "event_bus.h"
|
||||||
#include "gemini_response.h"
|
#include "gemini_response.h"
|
||||||
#include "key_value_store.h"
|
#include "key_value_store.h"
|
||||||
@@ -95,6 +96,7 @@ class GeminiService {
|
|||||||
|
|
||||||
// `gemini get <url>`: the same fetch, reported on the console instead of to the App.
|
// `gemini get <url>`: the same fetch, reported on the console instead of to the App.
|
||||||
bool fetchToConsole(const std::string& url);
|
bool fetchToConsole(const std::string& url);
|
||||||
|
Console::Origin consoleFrom_ = Console::Origin::System; // who asked for that fetch: its report is theirs
|
||||||
|
|
||||||
private:
|
private:
|
||||||
enum class Job { Fetch, Save, SaveWithLinks, Refresh, Delete, Bookmark, Download, Window };
|
enum class Job { Fetch, Save, SaveWithLinks, Refresh, Delete, Bookmark, Download, Window };
|
||||||
|
|||||||
@@ -260,6 +260,19 @@ void test_the_help_panel_scrolls_within_its_rows() {
|
|||||||
TEST_ASSERT_EQUAL(0, m.rows().size());
|
TEST_ASSERT_EQUAL(0, m.rows().size());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Issue #67: an App is opened by its name with a capital letter.
|
||||||
|
void test_an_app_command_is_its_id_with_a_capital() {
|
||||||
|
TEST_ASSERT_EQUAL_STRING("Notes", AppManager::commandFor("notes").c_str());
|
||||||
|
TEST_ASSERT_EQUAL_STRING("Wifi", AppManager::commandFor("wifi-tools").c_str());
|
||||||
|
TEST_ASSERT_EQUAL_STRING("Irc", AppManager::commandFor("irc").c_str());
|
||||||
|
Fixture f;
|
||||||
|
TEST_ASSERT_NOT_NULL(f.manager.byCommand("Notes"));
|
||||||
|
TEST_ASSERT_EQUAL_STRING("notes", f.manager.byCommand("Notes")->id);
|
||||||
|
TEST_ASSERT_NULL(f.manager.byCommand("notes")); // small letters are commands, not Apps
|
||||||
|
TEST_ASSERT_NULL(f.manager.byCommand("Demo")); // hidden: not in the Launcher, not here
|
||||||
|
TEST_ASSERT_NULL(f.manager.byCommand("Nope"));
|
||||||
|
}
|
||||||
|
|
||||||
int main() {
|
int main() {
|
||||||
UNITY_BEGIN();
|
UNITY_BEGIN();
|
||||||
RUN_TEST(test_launcher_is_in_foreground_after_begin);
|
RUN_TEST(test_launcher_is_in_foreground_after_begin);
|
||||||
@@ -283,5 +296,6 @@ int main() {
|
|||||||
RUN_TEST(test_open_help_takes_every_key_and_any_but_the_arrows_closes_it);
|
RUN_TEST(test_open_help_takes_every_key_and_any_but_the_arrows_closes_it);
|
||||||
RUN_TEST(test_help_works_in_a_modal_app_and_closes_when_the_app_changes);
|
RUN_TEST(test_help_works_in_a_modal_app_and_closes_when_the_app_changes);
|
||||||
RUN_TEST(test_the_help_panel_scrolls_within_its_rows);
|
RUN_TEST(test_the_help_panel_scrolls_within_its_rows);
|
||||||
|
RUN_TEST(test_an_app_command_is_its_id_with_a_capital);
|
||||||
return UNITY_END();
|
return UNITY_END();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -104,6 +104,30 @@ void test_looks_like_text() {
|
|||||||
TEST_ASSERT_TRUE(looksLikeText(nullptr, 0)); // an empty file reads as text
|
TEST_ASSERT_TRUE(looksLikeText(nullptr, 0)); // an empty file reads as text
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void test_rm_takes_its_switches_like_unix() {
|
||||||
|
auto a = parseRm("/notes/a.txt");
|
||||||
|
TEST_ASSERT_FALSE(a.recursive);
|
||||||
|
TEST_ASSERT_FALSE(a.force);
|
||||||
|
TEST_ASSERT_EQUAL_STRING("/notes/a.txt", a.path.c_str());
|
||||||
|
a = parseRm("-r /gemini/saved");
|
||||||
|
TEST_ASSERT_TRUE(a.recursive);
|
||||||
|
TEST_ASSERT_FALSE(a.force);
|
||||||
|
TEST_ASSERT_EQUAL_STRING("/gemini/saved", a.path.c_str());
|
||||||
|
for (const char* both : {"-rf /x", "-fr /x", "-r -f /x", "-f -r /x", "-R -f /x"}) {
|
||||||
|
a = parseRm(both);
|
||||||
|
TEST_ASSERT_TRUE(a.recursive);
|
||||||
|
TEST_ASSERT_TRUE(a.force);
|
||||||
|
TEST_ASSERT_EQUAL_STRING("/x", a.path.c_str());
|
||||||
|
}
|
||||||
|
a = parseRm("-f /a file with spaces.txt ");
|
||||||
|
TEST_ASSERT_TRUE(a.force);
|
||||||
|
TEST_ASSERT_EQUAL_STRING("/a file with spaces.txt", a.path.c_str());
|
||||||
|
a = parseRm("-x /odd"); // not a switch of rm: taken as the name
|
||||||
|
TEST_ASSERT_FALSE(a.force);
|
||||||
|
TEST_ASSERT_EQUAL_STRING("-x /odd", a.path.c_str());
|
||||||
|
TEST_ASSERT_EQUAL_STRING("", parseRm("-rf").path.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
int main() {
|
int main() {
|
||||||
UNITY_BEGIN();
|
UNITY_BEGIN();
|
||||||
RUN_TEST(test_path_parts);
|
RUN_TEST(test_path_parts);
|
||||||
@@ -114,5 +138,6 @@ int main() {
|
|||||||
RUN_TEST(test_copy_names);
|
RUN_TEST(test_copy_names);
|
||||||
RUN_TEST(test_kinds);
|
RUN_TEST(test_kinds);
|
||||||
RUN_TEST(test_looks_like_text);
|
RUN_TEST(test_looks_like_text);
|
||||||
|
RUN_TEST(test_rm_takes_its_switches_like_unix);
|
||||||
return UNITY_END();
|
return UNITY_END();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,13 +10,13 @@ using namespace roro;
|
|||||||
void setUp() {}
|
void setUp() {}
|
||||||
void tearDown() {}
|
void tearDown() {}
|
||||||
|
|
||||||
static void feed(ShellLog& log, const std::string& text, uint32_t now) { log.feed(text.data(), text.size(), now); }
|
static void feed(ShellLog& log, const std::string& text) { log.feed(text.data(), text.size()); }
|
||||||
|
|
||||||
void test_lines_arrive_in_pieces() {
|
void test_lines_arrive_in_pieces() {
|
||||||
ShellLog log;
|
ShellLog log;
|
||||||
feed(log, "firmware: roro", 0);
|
feed(log, "firmware: roro");
|
||||||
TEST_ASSERT_EQUAL(0, log.lines().size());
|
TEST_ASSERT_EQUAL(0, log.lines().size());
|
||||||
feed(log, "9stack\r\nuptime: 1s\n", 0);
|
feed(log, "9stack\r\nuptime: 1s\n");
|
||||||
TEST_ASSERT_EQUAL(2, log.lines().size());
|
TEST_ASSERT_EQUAL(2, log.lines().size());
|
||||||
TEST_ASSERT_EQUAL_STRING("firmware: roro9stack", log.lines()[0].c_str());
|
TEST_ASSERT_EQUAL_STRING("firmware: roro9stack", log.lines()[0].c_str());
|
||||||
TEST_ASSERT_EQUAL_STRING("uptime: 1s", log.lines()[1].c_str());
|
TEST_ASSERT_EQUAL_STRING("uptime: 1s", log.lines()[1].c_str());
|
||||||
@@ -24,38 +24,14 @@ void test_lines_arrive_in_pieces() {
|
|||||||
|
|
||||||
void test_the_heap_line_is_never_kept() {
|
void test_the_heap_line_is_never_kept() {
|
||||||
ShellLog log;
|
ShellLog log;
|
||||||
feed(log, "status: heap 105000 min 90000\nirc: connected\n", 0);
|
feed(log, "status: heap 105000 min 90000\nirc: connected\n");
|
||||||
TEST_ASSERT_EQUAL(1, log.lines().size());
|
TEST_ASSERT_EQUAL(1, log.lines().size());
|
||||||
TEST_ASSERT_EQUAL_STRING("irc: connected", log.lines()[0].c_str());
|
TEST_ASSERT_EQUAL_STRING("irc: connected", log.lines()[0].c_str());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Q206: with background lines hidden, only what's printed in the ten seconds after a command.
|
|
||||||
void test_mine_only_keeps_what_follows_a_command() {
|
|
||||||
ShellLog log;
|
|
||||||
log.showBackground(false);
|
|
||||||
feed(log, "irc: someone joined\n", 1000); // no command yet
|
|
||||||
TEST_ASSERT_EQUAL(0, log.lines().size());
|
|
||||||
log.commandRun(5000);
|
|
||||||
feed(log, "ls: end of /\n", 5200); // the reply, a moment later, from another task
|
|
||||||
feed(log, "tasks: ...\n", 14999);
|
|
||||||
feed(log, "irc: someone left\n", 15000); // ten seconds on: background again
|
|
||||||
TEST_ASSERT_EQUAL(2, log.lines().size());
|
|
||||||
log.showBackground(true);
|
|
||||||
feed(log, "irc: back\n", 99000);
|
|
||||||
TEST_ASSERT_EQUAL(3, log.lines().size());
|
|
||||||
}
|
|
||||||
|
|
||||||
void test_mine_only_holds_across_the_clock_wrap() {
|
|
||||||
ShellLog log;
|
|
||||||
log.showBackground(false);
|
|
||||||
log.commandRun(0xFFFFFF00u);
|
|
||||||
feed(log, "reply\n", 0x00000100u); // 512 ms later, past the wrap
|
|
||||||
TEST_ASSERT_EQUAL(1, log.lines().size());
|
|
||||||
}
|
|
||||||
|
|
||||||
void test_the_oldest_lines_go_past_4_kb() {
|
void test_the_oldest_lines_go_past_4_kb() {
|
||||||
ShellLog log;
|
ShellLog log;
|
||||||
for (int i = 0; i < 200; i++) feed(log, std::string(49, 'x') + "\n", 0); // 10 KB in all
|
for (int i = 0; i < 200; i++) feed(log, std::string(49, 'x') + "\n"); // 10 KB in all
|
||||||
size_t bytes = 0;
|
size_t bytes = 0;
|
||||||
for (auto& l : log.lines()) bytes += l.size() + 1;
|
for (auto& l : log.lines()) bytes += l.size() + 1;
|
||||||
TEST_ASSERT_TRUE(bytes <= ShellLog::kMaxBytes);
|
TEST_ASSERT_TRUE(bytes <= ShellLog::kMaxBytes);
|
||||||
@@ -74,11 +50,12 @@ static const char* kHelp =
|
|||||||
"lora probe | lora status | lora rx on|off the LoRa radio\n"
|
"lora probe | lora status | lora rx on|off the LoRa radio\n"
|
||||||
"log level <0-5> ESP-IDF log level\n"
|
"log level <0-5> ESP-IDF log level\n"
|
||||||
"sd card | sd list | cat <path> | log <text> | burst\n"
|
"sd card | sd list | cat <path> | log <text> | burst\n"
|
||||||
"get <path> | put <path> <size> <sha256> | screenshot (Debug Console only) binary\n";
|
"get <path> | put <path> <size> <sha256> | screenshot (Debug Console only) binary\n"
|
||||||
|
"Irc | Notes | Storage open that App\n";
|
||||||
|
|
||||||
void test_command_words_come_from_the_help_text() {
|
void test_command_words_come_from_the_help_text() {
|
||||||
auto words = commandWords(kHelp);
|
auto words = commandWords(kHelp);
|
||||||
const char* expected[] = {"info", "ls", "du", "mkdir", "rm", "lora", "log", "sd", "cat", "burst", "get", "put", "screenshot"};
|
const char* expected[] = {"info", "ls", "du", "mkdir", "rm", "lora", "log", "sd", "cat", "burst", "get", "put", "screenshot", "Irc", "Notes", "Storage"};
|
||||||
TEST_ASSERT_EQUAL(sizeof expected / sizeof expected[0], words.size());
|
TEST_ASSERT_EQUAL(sizeof expected / sizeof expected[0], words.size());
|
||||||
for (size_t i = 0; i < words.size(); i++) TEST_ASSERT_EQUAL_STRING(expected[i], words[i].c_str());
|
for (size_t i = 0; i < words.size(); i++) TEST_ASSERT_EQUAL_STRING(expected[i], words[i].c_str());
|
||||||
}
|
}
|
||||||
@@ -96,14 +73,14 @@ void test_tab_completes_the_first_word() {
|
|||||||
TEST_ASSERT_EQUAL_STRING("zz", completeCommand("zz", words, matches).c_str()); // nothing: as it was
|
TEST_ASSERT_EQUAL_STRING("zz", completeCommand("zz", words, matches).c_str()); // nothing: as it was
|
||||||
TEST_ASSERT_EQUAL_STRING("ls /no", completeCommand("ls /no", words, matches).c_str()); // past the first word
|
TEST_ASSERT_EQUAL_STRING("ls /no", completeCommand("ls /no", words, matches).c_str()); // past the first word
|
||||||
TEST_ASSERT_EQUAL_STRING("", completeCommand("", words, matches).c_str());
|
TEST_ASSERT_EQUAL_STRING("", completeCommand("", words, matches).c_str());
|
||||||
|
TEST_ASSERT_EQUAL_STRING("Notes ", completeCommand("N", words, matches).c_str()); // an App: a capital
|
||||||
|
TEST_ASSERT_EQUAL_STRING("n", completeCommand("n", words, matches).c_str()); // and no command starts with n here
|
||||||
}
|
}
|
||||||
|
|
||||||
int main(int, char**) {
|
int main(int, char**) {
|
||||||
UNITY_BEGIN();
|
UNITY_BEGIN();
|
||||||
RUN_TEST(test_lines_arrive_in_pieces);
|
RUN_TEST(test_lines_arrive_in_pieces);
|
||||||
RUN_TEST(test_the_heap_line_is_never_kept);
|
RUN_TEST(test_the_heap_line_is_never_kept);
|
||||||
RUN_TEST(test_mine_only_keeps_what_follows_a_command);
|
|
||||||
RUN_TEST(test_mine_only_holds_across_the_clock_wrap);
|
|
||||||
RUN_TEST(test_the_oldest_lines_go_past_4_kb);
|
RUN_TEST(test_the_oldest_lines_go_past_4_kb);
|
||||||
RUN_TEST(test_command_words_come_from_the_help_text);
|
RUN_TEST(test_command_words_come_from_the_help_text);
|
||||||
RUN_TEST(test_tab_completes_the_first_word);
|
RUN_TEST(test_tab_completes_the_first_word);
|
||||||
|
|||||||
Reference in New Issue
Block a user