Public Access
Shell: the console's commands on the device's own screen and keyboard (#67) #78
@@ -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