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
|
||||
|
||||
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
|
||||
|
||||
@@ -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 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 |
|
||||
| `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 |
|
||||
| `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) |
|
||||
| `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 |
|
||||
| `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 |
|
||||
| `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 |
|
||||
|
||||
+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. |
|
||||
| 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. |
|
||||
| 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.** |
|
||||
| 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. |
|
||||
| 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
|
||||
|
||||
- **`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).
|
||||
- **The console has a second ring** (`Console::openShellRing`), filled like the Debug Console's and independent of it.
|
||||
- **`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.
|
||||
- **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.
|
||||
- **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`)
|
||||
|
||||
| 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` |
|
||||
| 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 |
|
||||
| `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 |
|
||||
| `quit` | Back to the Launcher, and the memory comes back |
|
||||
| The help panel in the Shell | Its keys, then the ones that work everywhere |
|
||||
|
||||
@@ -4,12 +4,6 @@
|
||||
|
||||
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) {
|
||||
lines_.push_back(line);
|
||||
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::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++) {
|
||||
char c = data[i];
|
||||
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
|
||||
continue;
|
||||
}
|
||||
if (wanted(partial_, nowMs)) push(partial_);
|
||||
if (partial_.rfind("status: heap ", 0) != 0) push(partial_);
|
||||
partial_.clear();
|
||||
}
|
||||
}
|
||||
@@ -61,7 +55,9 @@ std::vector<std::string> commandWords(const char* helpText) {
|
||||
if (start != std::string::npos) {
|
||||
size_t end = one.find(' ', 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 (bar == std::string::npos) break;
|
||||
|
||||
@@ -8,43 +8,30 @@
|
||||
|
||||
namespace roro {
|
||||
|
||||
// What the Shell App shows (issue #67): the lines the console printed, with the oldest dropped past
|
||||
// a size. Everything the firmware prints passes through; the Shell keeps what's worth a place on a
|
||||
// ten-line screen (Q206).
|
||||
// What the Shell App shows (issue #67): the lines it was given, with the oldest dropped past a size.
|
||||
// Which lines it is given is the console's business: by default, only what is printed for the
|
||||
// Shell's own commands (Console::Origin).
|
||||
class ShellLog {
|
||||
public:
|
||||
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.
|
||||
void feed(const char* data, size_t len, uint32_t nowMs);
|
||||
// A line the Shell adds itself: the command typed, an answer of its own.
|
||||
// Bytes as the console printed them: lines may arrive in pieces. The line with the free heap
|
||||
// every ten seconds is never kept: it would push everything else off a ten-line screen.
|
||||
void feed(const char* data, size_t len);
|
||||
// A line the Shell adds itself.
|
||||
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_; }
|
||||
void clear();
|
||||
uint32_t revision() const { return revision_; } // changes when the lines do
|
||||
|
||||
private:
|
||||
bool wanted(const std::string& line, uint32_t nowMs) const;
|
||||
void push(const std::string& line);
|
||||
|
||||
std::deque<std::string> lines_;
|
||||
std::string partial_;
|
||||
size_t bytes_ = 0;
|
||||
bool background_ = true, ranOne_ = false;
|
||||
uint32_t lastCommandMs_ = 0, revision_ = 0;
|
||||
uint32_t revision_ = 0;
|
||||
};
|
||||
|
||||
// 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"},
|
||||
{"Fn ; .", "lines you typed before"},
|
||||
{"Alt ; .", "scroll back, forward"},
|
||||
{"Ctrl b", "all output, or only yours"},
|
||||
{"Ctrl b", "your replies only, or all"},
|
||||
{"Fn , /", "move the cursor"},
|
||||
{"Del", "delete backwards"},
|
||||
{"help", "every command"},
|
||||
{"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"},
|
||||
};
|
||||
|
||||
|
||||
@@ -82,6 +82,19 @@ void AppManager::handleKey(const KeyEvent& event) {
|
||||
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 {
|
||||
for (auto& info : apps_)
|
||||
if (info.app == foreground_) return info.title;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "app.h"
|
||||
@@ -34,6 +35,13 @@ class AppManager {
|
||||
void handleKey(const KeyEvent& event);
|
||||
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_; }
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
@@ -39,4 +39,12 @@ FileKind kindOf(const std::string& name);
|
||||
bool opensAtEnd(const std::string& name); // logs
|
||||
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
|
||||
|
||||
@@ -19,7 +19,7 @@ net bytes each network service has read and written since boot
|
||||
reboot restart
|
||||
boot other restart into the other app slot (manual Rollback)
|
||||
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
|
||||
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)
|
||||
@@ -38,6 +38,7 @@ irc start | irc stop | irc dump | irc say <buffer> <text>
|
||||
install <path.ota> Update from SD
|
||||
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
|
||||
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 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)
|
||||
@@ -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 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 |
|
||||
| `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 |
|
||||
| `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) |
|
||||
| `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 |
|
||||
| `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 |
|
||||
| `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 |
|
||||
|
||||
@@ -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.
|
||||
|
||||
## 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
|
||||
|
||||
**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
|
||||
```
|
||||
|
||||
- **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`.
|
||||
- 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.
|
||||
|
||||
@@ -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. |
|
||||
| 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. |
|
||||
| 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.** |
|
||||
| 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. |
|
||||
| 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
|
||||
|
||||
- **`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).
|
||||
- **The console has a second ring** (`Console::openShellRing`), filled like the Debug Console's and independent of it.
|
||||
- **`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.
|
||||
- **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.
|
||||
- **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`)
|
||||
|
||||
| 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` |
|
||||
| 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 |
|
||||
| `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 |
|
||||
| `quit` | Back to the Launcher, and the memory comes back |
|
||||
| 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
|
||||
|
||||
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
|
||||
|
||||
|
||||
+3
-2
@@ -383,12 +383,13 @@ rows = [
|
||||
["Tab", "complete the command"],
|
||||
["Fn ; .", "lines you typed before"],
|
||||
["Alt ; .", "scroll back, forward"],
|
||||
["Ctrl b", "all output, or only yours"],
|
||||
["Ctrl b", "your replies only, or all"],
|
||||
["Fn , /", "move the cursor"],
|
||||
["Del", "delete backwards"],
|
||||
["help", "every command"],
|
||||
["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"],
|
||||
]
|
||||
|
||||
|
||||
+31
-15
@@ -2,7 +2,10 @@
|
||||
|
||||
#include <Arduino.h>
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
#include "app_keys.h"
|
||||
#include "file_names.h"
|
||||
#include "platform/console.h"
|
||||
#include "ui/fonts.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() {
|
||||
open_ = console.openShellRing();
|
||||
console.shellShowsAll(false); // its own replies only, each time it's opened
|
||||
ringPos_ = 0;
|
||||
scroll_ = 0;
|
||||
confirm_.reset();
|
||||
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("clear"); // the Shell's own
|
||||
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).
|
||||
void ShellApp::onExit() {
|
||||
console.closeShellRing();
|
||||
console.shellShowsAll(false);
|
||||
open_ = false;
|
||||
log_.clear();
|
||||
std::vector<std::string>().swap(words_);
|
||||
confirm_.reset();
|
||||
}
|
||||
|
||||
void ShellApp::update(uint32_t nowMs) {
|
||||
void ShellApp::update(uint32_t) {
|
||||
uint8_t buf[256];
|
||||
uint32_t skipped = 0;
|
||||
size_t n;
|
||||
while ((n = console.readShellSince(ringPos_, buf, sizeof buf, skipped)) > 0) {
|
||||
if (skipped) log_.add("[... " + std::to_string(skipped) + " bytes lost: more was printed than fits]");
|
||||
log_.feed(reinterpret_cast<const char*>(buf), n, nowMs);
|
||||
log_.feed(reinterpret_cast<const char*>(buf), n);
|
||||
}
|
||||
if (log_.revision() != seenRevision_) {
|
||||
seenRevision_ = log_.revision();
|
||||
@@ -53,9 +62,8 @@ void ShellApp::update(uint32_t nowMs) {
|
||||
}
|
||||
|
||||
void ShellApp::runNow(const std::string& line) {
|
||||
log_.commandRun(millis());
|
||||
// The runner echoes the line into the console, where the Shell reads it back like everything
|
||||
// else, except a token being set, which goes nowhere (Q210): that one is shown here, masked.
|
||||
// The runner echoes the line into the console, where the Shell reads it back with the reply,
|
||||
// except a token being set, which goes nowhere (Q210): that one is shown here, masked.
|
||||
if (startsWith(line, "debug token ") && line != "debug token new") log_.add("> debug token ...");
|
||||
run_(line);
|
||||
}
|
||||
@@ -65,11 +73,19 @@ void ShellApp::enter(const std::string& line) {
|
||||
scroll_ = 0;
|
||||
if (line == "quit" || line == "exit") return apps_.home();
|
||||
if (line == "clear") return log_.clear();
|
||||
// Q209: deleting asks first, here where a slip of the finger is a key away. `rm -f` doesn't.
|
||||
if (startsWith(line, "rm ") && !startsWith(line, "rm -f ")) {
|
||||
pending_ = line;
|
||||
confirm_.reset(new DialogModel({"Cancel", "Delete"}));
|
||||
return;
|
||||
// Q209: `rm` as Unix has it, with a question where Unix has none, since a slip of the finger is
|
||||
// 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;
|
||||
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"}));
|
||||
return;
|
||||
}
|
||||
}
|
||||
runNow(line);
|
||||
}
|
||||
@@ -91,8 +107,8 @@ bool ShellApp::onKey(const KeyEvent& e) {
|
||||
return true;
|
||||
}
|
||||
if (e.key == Key::Char && e.ctrl && (e.ch == 'b' || e.ch == 'B')) { // Q206
|
||||
log_.showBackground(!log_.showsBackground());
|
||||
log_.add(log_.showsBackground() ? "Showing everything the console prints." : "Showing only what follows your commands.");
|
||||
console.shellShowsAll(!console.shellShowsAll());
|
||||
log_.add(console.shellShowsAll() ? "Showing everything the console prints." : "Showing only the replies to your commands.");
|
||||
return true;
|
||||
}
|
||||
std::string recalled;
|
||||
@@ -167,14 +183,14 @@ void ShellApp::draw(Canvas& c) {
|
||||
if (scroll_ > 0) {
|
||||
c.setTextColor(theme::kWarning);
|
||||
c.drawString(("^ " + std::to_string(scroll_)).c_str(), area.w - 3, output.y + 1);
|
||||
} else if (!log_.showsBackground()) {
|
||||
} else if (console.shellShowsAll()) {
|
||||
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);
|
||||
|
||||
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
|
||||
|
||||
+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
|
||||
// holds the device can do all of it in Settings anyway (Q205).
|
||||
//
|
||||
// It shows what the console prints while it is open, read from a ring of the console's that exists
|
||||
// only meanwhile; nothing is kept once the App is left.
|
||||
// It shows the replies to its own commands, and only those unless asked otherwise (Q206): the
|
||||
// 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 {
|
||||
public:
|
||||
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 onExit() override;
|
||||
@@ -39,6 +44,7 @@ class ShellApp : public App {
|
||||
void runNow(const std::string& line);
|
||||
|
||||
Run run_;
|
||||
Probe probe_;
|
||||
const char* helpText_;
|
||||
AppManager& apps_;
|
||||
ShellLog log_;
|
||||
@@ -46,7 +52,7 @@ class ShellApp : public App {
|
||||
LineEditor input_{240};
|
||||
InputHistory history_{16};
|
||||
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;
|
||||
int scroll_ = 0; // wrapped lines scrolled back from the bottom
|
||||
bool open_ = false;
|
||||
|
||||
+87
-12
@@ -3,6 +3,7 @@
|
||||
#include <SD.h>
|
||||
|
||||
|
||||
#include <deque>
|
||||
#include <atomic>
|
||||
#include <memory>
|
||||
|
||||
@@ -155,6 +156,7 @@ size_t getArduinoLoopTaskStackSize() { return 6144; }
|
||||
static void setupSafeMode(int crashes);
|
||||
static const char* helpText();
|
||||
static void shellRun(const std::string& line);
|
||||
static ShellApp::Target shellProbe(const std::string& path);
|
||||
|
||||
void setup() {
|
||||
nvs.begin();
|
||||
@@ -213,7 +215,7 @@ void setup() {
|
||||
apps->registerApp({"lora", "LoRa Scanner", false, new LoraScannerApp(*radioService, *loraCapture, settings, *clockService)});
|
||||
apps->registerApp({"storage", "Storage", false, new StorageApp(*fileOps, *storageService, *clockService, *update, *power, bus)});
|
||||
apps->registerApp({"notes", "Notes", false, new NotesApp(*fileOps, *storageService, *clockService, *power)});
|
||||
apps->registerApp({"shell", "Shell", false, new ShellApp(shellRun, helpText(), *apps)});
|
||||
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.
|
||||
power->beforePowerOff = []() { apps->home(); };
|
||||
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.
|
||||
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() {
|
||||
if (!listingWanted || !storageService->listingReady()) return;
|
||||
listingWanted = false;
|
||||
Console::As as(listingFrom);
|
||||
auto listing = storageService->listing();
|
||||
for (size_t i = 0; i < listing.size(); i++) {
|
||||
uint64_t bytes = 0;
|
||||
@@ -431,6 +436,7 @@ static void fileOpsStep() {
|
||||
FileOps::Status s;
|
||||
if (!consoleFileOp || !fileOps->finished(s)) return;
|
||||
consoleFileOp = false;
|
||||
Console::As as(fileOpFrom);
|
||||
static const char* const kNames[] = {"", "ls", "du", "cp", "mv", "rm", "mkdir"};
|
||||
const char* name = kNames[static_cast<int>(s.op)];
|
||||
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() {
|
||||
if (!updateWatch || update->giteaBusy()) return;
|
||||
Console::As as(updateFrom);
|
||||
if (updateWatch == 3) {
|
||||
console.printf("update: probe: %s\n", update->probeResult().c_str());
|
||||
updateWatch = 0;
|
||||
@@ -478,6 +485,7 @@ static void updateCommand(const String& args) {
|
||||
if (update->giteaBusy()) return (void)console.println("update: busy");
|
||||
args == "check" ? update->requestCheck() : update->requestList();
|
||||
updateWatch = args == "check" ? 1 : 2;
|
||||
updateFrom = console.origin();
|
||||
} else if (args == "status") {
|
||||
GiteaReleases& g = update->gitea();
|
||||
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(' ');
|
||||
update->requestProbe((space < 0 ? rest : rest.substring(0, space)).c_str(), space < 0 ? "/" : rest.substring(space + 1).c_str());
|
||||
updateWatch = 3;
|
||||
updateFrom = console.origin();
|
||||
} else if (args.startsWith("damage ")) { // update damage cut <bytes> | flip <offset>: for the next download
|
||||
long n = args.substring(args.lastIndexOf(' ') + 1).toInt();
|
||||
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) {
|
||||
int space = line.indexOf(' ');
|
||||
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
|
||||
if (force) rest = rest.substr(3);
|
||||
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";
|
||||
else if (command == "du") why = fileOps->count(rest);
|
||||
else if (command == "mkdir") why = fileOps->makeFolder(rest);
|
||||
else if (command == "rm") {
|
||||
folder = fileOps->isFolder(rest, exists);
|
||||
why = exists ? fileOps->remove(rest, folder) : "It isn't there";
|
||||
else if (command == "rm") { // as Unix has it: a folder needs -r. The questions are the Shell's (-f is its)
|
||||
files::RmArgs args = files::parseRm(everything);
|
||||
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 {
|
||||
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());
|
||||
consoleFileOp = true;
|
||||
fileOpFrom = console.origin();
|
||||
}
|
||||
|
||||
static uint32_t loopPasses = 0; // counted in loop(), for `tasks` (issue #40)
|
||||
@@ -547,6 +562,7 @@ static uint32_t tasksPasses = 0;
|
||||
static void tasksStep() {
|
||||
if (!tasksPending || static_cast<int32_t>(millis() - tasksDueMs) < 0) return;
|
||||
tasksPending = false;
|
||||
Console::As as(tasksFrom);
|
||||
system_info::printTasks(console, tasksBefore, tasksTotal);
|
||||
console.printf("loop: %lu passes in the last second, chip %.1f C\n", (unsigned long)(loopPasses - tasksPasses), temperatureRead());
|
||||
std::vector<TaskSample>().swap(tasksBefore);
|
||||
@@ -558,7 +574,7 @@ static const char* const kHelp =
|
||||
"reboot restart\n"
|
||||
"boot other restart into the other app slot (manual Rollback)\n"
|
||||
"log level <0-5> ESP-IDF log level (0 none ... 5 verbose)\n"
|
||||
"ls [folder] | du <path> | mkdir <path> | rm [-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"
|
||||
"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"
|
||||
@@ -577,6 +593,7 @@ static const char* const kHelp =
|
||||
"install <path.ota> Update from SD\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"
|
||||
"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 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"
|
||||
@@ -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
|
||||
// pause is for getting to the screen you want: from the Shell, "now" is always the Shell.
|
||||
static bool shotPending = false;
|
||||
static Console::Origin shotFrom;
|
||||
static uint32_t shotDueMs = 0;
|
||||
static std::atomic<bool> shotSaved{false};
|
||||
|
||||
@@ -611,7 +629,9 @@ static void saveScreenshot() {
|
||||
local.tm_hour, local.tm_min, local.tm_sec);
|
||||
} else snprintf(name, sizeof name, "/screenshots/shot-%lu.png", (unsigned long)(millis() / 1000)); // no clock yet
|
||||
std::string path = name;
|
||||
storageService->runJob([path]() {
|
||||
Console::Origin from = shotFrom;
|
||||
storageService->runJob([path, from]() {
|
||||
Console::As as(from);
|
||||
Canvas& frame = screen.canvas();
|
||||
const uint8_t* pixels = static_cast<const uint8_t*>(frame.getBuffer());
|
||||
int w = frame.width(), h = frame.height();
|
||||
@@ -696,10 +716,22 @@ static void runCommand(String line, bool fromSerial = false) {
|
||||
if (line.isEmpty()) return;
|
||||
if (safeMode && !safeModeCommand(line)) return (void)console.println("not available in Safe Mode");
|
||||
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 == "screenshot" || line.startsWith("screenshot ")) {
|
||||
uint32_t seconds = constrain(line.substring(10).toInt(), 0, 60);
|
||||
shotPending = true;
|
||||
shotFrom = console.origin();
|
||||
shotDueMs = millis() + seconds * 1000;
|
||||
if (seconds) console.printf("screenshot: in %lu s\n", (unsigned long)seconds);
|
||||
return;
|
||||
@@ -711,6 +743,7 @@ static void runCommand(String line, bool fromSerial = false) {
|
||||
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(),
|
||||
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",
|
||||
!debugConsole->on() ? "off" : debugConsole->clientConnected() ? "on, a client connected" : "on");
|
||||
system_info::printSlots(console, nvs);
|
||||
@@ -724,6 +757,7 @@ static void runCommand(String line, bool fromSerial = false) {
|
||||
tasksDueMs = millis() + 1000;
|
||||
tasksPasses = loopPasses;
|
||||
tasksPending = true;
|
||||
tasksFrom = console.origin();
|
||||
}
|
||||
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++) {
|
||||
@@ -750,7 +784,9 @@ static void runCommand(String line, bool fromSerial = false) {
|
||||
if (line == "ls" || line.startsWith("ls ")) {
|
||||
if (!storageService->state().present) return (void)console.println("sd: no card");
|
||||
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());
|
||||
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())
|
||||
@@ -764,7 +800,9 @@ static void runCommand(String line, bool fromSerial = false) {
|
||||
int space = line.lastIndexOf(' ');
|
||||
std::string folder = line.substring(8, space).c_str();
|
||||
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;
|
||||
for (int i = 1; i <= count; i++) {
|
||||
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 == "sd card") { // what the card says it is, from its CID register
|
||||
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];
|
||||
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");
|
||||
@@ -963,6 +1003,7 @@ static void runCommand(String line, bool fromSerial = false) {
|
||||
if (line == "sd list") {
|
||||
storageService->requestListing();
|
||||
listingWanted = true;
|
||||
listingFrom = console.origin();
|
||||
}
|
||||
if (line.startsWith("gemini trust ")) { // gemini trust <host> <port> <sha256>: accept a changed certificate
|
||||
char host[96] = "", fp[80] = "";
|
||||
@@ -1078,10 +1119,43 @@ static void runCommand(String line, bool fromSerial = false) {
|
||||
|
||||
// What the Shell App runs (issue #67): trusted like USB serial (Q205), and echoed into the console so
|
||||
// that a session reads the same from afar, except a token being set (Q210).
|
||||
//
|
||||
// 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) {
|
||||
String l = line.c_str();
|
||||
if (!l.startsWith("debug token ") || l == "debug token new") console.printf("> %s\n", line.c_str());
|
||||
runCommand(l, true);
|
||||
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();
|
||||
if (!l.startsWith("debug token ") || l == "debug token new") console.printf("> %s\n", line.c_str());
|
||||
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() {
|
||||
@@ -1172,6 +1246,7 @@ static void loopPass() {
|
||||
|
||||
serialCommands();
|
||||
remoteCommands();
|
||||
shellCommands();
|
||||
tasksStep();
|
||||
screenshotStep();
|
||||
ipTrialStep();
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
#include <esp_log.h>
|
||||
#include <freertos/FreeRTOS.h>
|
||||
#include <freertos/task.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdio>
|
||||
@@ -37,6 +38,20 @@ void Console::captureEspLogs() {
|
||||
|
||||
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.
|
||||
bool openOne(uint8_t*& ring, uint32_t& head) {
|
||||
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
|
||||
len = kRingBytes;
|
||||
}
|
||||
TaskHandle_t task = xTaskGetCurrentTaskHandle();
|
||||
portENTER_CRITICAL(&ringLock);
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
+23
-2
@@ -34,10 +34,30 @@ class Console : public Print {
|
||||
// The ring only, for output that already reaches the serial port another way.
|
||||
void toRing(const uint8_t* data, size_t len);
|
||||
|
||||
// A second ring of the same kind, for the Shell App (issue #67): open while the App is, so that
|
||||
// it can show what the console prints. The two don't know of each other.
|
||||
// Who asked for what is being printed. A command typed in the Shell App prints its reply `As`
|
||||
// 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();
|
||||
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);
|
||||
|
||||
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
|
||||
uint8_t* shellRing_ = nullptr;
|
||||
uint32_t shellHead_ = 0;
|
||||
volatile bool shellAll_ = false;
|
||||
};
|
||||
|
||||
extern Console console;
|
||||
|
||||
@@ -140,6 +140,7 @@ bool GeminiService::fetch(const std::string& url, bool preferSaved) {
|
||||
bool GeminiService::fetchToConsole(const std::string& url) {
|
||||
if (busy_) return false;
|
||||
toConsole_ = true;
|
||||
consoleFrom_ = console.origin();
|
||||
if (!fetch(url)) {
|
||||
toConsole_ = false;
|
||||
return false;
|
||||
@@ -380,7 +381,10 @@ void GeminiService::runFetch(GeminiPage& page) {
|
||||
}
|
||||
}
|
||||
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.
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "platform/console.h"
|
||||
#include "event_bus.h"
|
||||
#include "gemini_response.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.
|
||||
bool fetchToConsole(const std::string& url);
|
||||
Console::Origin consoleFrom_ = Console::Origin::System; // who asked for that fetch: its report is theirs
|
||||
|
||||
private:
|
||||
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());
|
||||
}
|
||||
|
||||
// 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() {
|
||||
UNITY_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_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_an_app_command_is_its_id_with_a_capital);
|
||||
return UNITY_END();
|
||||
}
|
||||
|
||||
@@ -104,6 +104,30 @@ void test_looks_like_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() {
|
||||
UNITY_BEGIN();
|
||||
RUN_TEST(test_path_parts);
|
||||
@@ -114,5 +138,6 @@ int main() {
|
||||
RUN_TEST(test_copy_names);
|
||||
RUN_TEST(test_kinds);
|
||||
RUN_TEST(test_looks_like_text);
|
||||
RUN_TEST(test_rm_takes_its_switches_like_unix);
|
||||
return UNITY_END();
|
||||
}
|
||||
|
||||
@@ -10,13 +10,13 @@ using namespace roro;
|
||||
void setUp() {}
|
||||
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() {
|
||||
ShellLog log;
|
||||
feed(log, "firmware: roro", 0);
|
||||
feed(log, "firmware: roro");
|
||||
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_STRING("firmware: roro9stack", log.lines()[0].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() {
|
||||
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_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() {
|
||||
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;
|
||||
for (auto& l : log.lines()) bytes += l.size() + 1;
|
||||
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"
|
||||
"log level <0-5> ESP-IDF log level\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() {
|
||||
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());
|
||||
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("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("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**) {
|
||||
UNITY_BEGIN();
|
||||
RUN_TEST(test_lines_arrive_in_pieces);
|
||||
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_command_words_come_from_the_help_text);
|
||||
RUN_TEST(test_tab_completes_the_first_word);
|
||||
|
||||
Reference in New Issue
Block a user