Public Access
Shell: the console's commands on the device's own screen and keyboard (#67)
An App in the Launcher that runs the same commands as USB serial and the Debug Console, trusted like the first. It shows what the console prints while it is open, through a second ring of the console's that exists only meanwhile; Ctrl+b keeps only what follows your own commands. Tab completes a command's name from the firmware's help text, Fn with up and down recalls earlier lines, Alt with up and down scrolls back. `rm` asks first in the Shell, `rm -f` doesn't. Nothing is kept once the App is left. `screenshot [seconds]` saves the screen as a PNG in /screenshots on the card, now or after a pause: written a row at a time, indexed colour with RGB332 as the palette, in one stored deflate block. 487 host tests (11 new: the PNG writer, the Shell's log filter, Tab). Checked on the device over the Debug Console: commands, Tab, history, a screenshot fetched and decoded on the PC, rm with and without the question, a delayed screenshot of another screen. 12 KB of flash; 7 KB of heap while open. Decisions Q204 to Q212 in docs/milestones/S1.md. Safe Mode: #77. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EhqxQ49eCju4CzKYNjZzwT
This commit is contained in:
@@ -0,0 +1,115 @@
|
||||
#include <unity.h>
|
||||
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "png_rgb332.h"
|
||||
|
||||
using namespace roro::png;
|
||||
|
||||
void setUp() {}
|
||||
void tearDown() {}
|
||||
|
||||
static uint32_t be(const std::vector<uint8_t>& d, size_t at) {
|
||||
return (uint32_t(d[at]) << 24) | (uint32_t(d[at + 1]) << 16) | (uint32_t(d[at + 2]) << 8) | d[at + 3];
|
||||
}
|
||||
|
||||
void test_the_checksums_match_their_reference_values() {
|
||||
const char* nine = "123456789";
|
||||
TEST_ASSERT_EQUAL_HEX32(0xCBF43926, crc32(0, reinterpret_cast<const uint8_t*>(nine), 9));
|
||||
TEST_ASSERT_EQUAL_HEX32(0x11E60398, adler32(1, reinterpret_cast<const uint8_t*>("Wikipedia"), 9));
|
||||
// Running: in two pieces, the same.
|
||||
uint32_t c = crc32(0, reinterpret_cast<const uint8_t*>(nine), 4);
|
||||
TEST_ASSERT_EQUAL_HEX32(0xCBF43926, crc32(c, reinterpret_cast<const uint8_t*>(nine + 4), 5));
|
||||
}
|
||||
|
||||
// Walks the chunks of what was written: every length adds up and every CRC is right.
|
||||
void test_a_small_image_is_a_well_formed_png() {
|
||||
std::vector<uint8_t> out;
|
||||
const int w = 4, h = 3;
|
||||
Rgb332Writer writer(w, h, [&](const uint8_t* d, size_t n) {
|
||||
out.insert(out.end(), d, d + n);
|
||||
return true;
|
||||
});
|
||||
TEST_ASSERT_TRUE(writer.begin());
|
||||
const uint8_t rows[3][4] = {{0x00, 0xE0, 0x1C, 0x03}, {0xFF, 0x80, 0x10, 0x02}, {1, 2, 3, 4}};
|
||||
for (auto& r : rows) TEST_ASSERT_TRUE(writer.row(r));
|
||||
TEST_ASSERT_FALSE(writer.row(rows[0])); // no more rows than the height
|
||||
TEST_ASSERT_TRUE(writer.end());
|
||||
TEST_ASSERT_EQUAL(Rgb332Writer::fileSize(w, h), out.size());
|
||||
|
||||
const uint8_t signature[] = {0x89, 'P', 'N', 'G', '\r', '\n', 0x1A, '\n'};
|
||||
TEST_ASSERT_EQUAL_MEMORY(signature, out.data(), 8);
|
||||
std::vector<std::string> kinds;
|
||||
std::vector<uint8_t> idat;
|
||||
for (size_t at = 8; at < out.size();) {
|
||||
uint32_t len = be(out, at);
|
||||
std::string kind(out.begin() + at + 4, out.begin() + at + 8);
|
||||
kinds.push_back(kind);
|
||||
TEST_ASSERT_EQUAL_HEX32(crc32(0, out.data() + at + 4, len + 4), be(out, at + 8 + len));
|
||||
if (kind == "IDAT") idat.assign(out.begin() + at + 8, out.begin() + at + 8 + len);
|
||||
if (kind == "IHDR") {
|
||||
TEST_ASSERT_EQUAL(w, be(out, at + 8));
|
||||
TEST_ASSERT_EQUAL(h, be(out, at + 12));
|
||||
TEST_ASSERT_EQUAL(8, out[at + 16]);
|
||||
TEST_ASSERT_EQUAL(3, out[at + 17]); // indexed
|
||||
}
|
||||
if (kind == "PLTE") {
|
||||
TEST_ASSERT_EQUAL(768, len);
|
||||
const uint8_t* white = out.data() + at + 8 + 255 * 3;
|
||||
TEST_ASSERT_EQUAL(255, white[0] + 0);
|
||||
TEST_ASSERT_EQUAL(255, white[1] + 0);
|
||||
TEST_ASSERT_EQUAL(255, white[2] + 0);
|
||||
const uint8_t* red = out.data() + at + 8 + 0xE0 * 3;
|
||||
TEST_ASSERT_EQUAL(255, red[0] + 0);
|
||||
TEST_ASSERT_EQUAL(0, red[1] + 0);
|
||||
}
|
||||
at += 12 + len;
|
||||
}
|
||||
TEST_ASSERT_EQUAL(4, kinds.size());
|
||||
TEST_ASSERT_EQUAL_STRING("IHDR", kinds[0].c_str());
|
||||
TEST_ASSERT_EQUAL_STRING("PLTE", kinds[1].c_str());
|
||||
TEST_ASSERT_EQUAL_STRING("IDAT", kinds[2].c_str());
|
||||
TEST_ASSERT_EQUAL_STRING("IEND", kinds[3].c_str());
|
||||
|
||||
// The zlib stream: one stored, final block of (w + 1) * h bytes, then their Adler-32.
|
||||
const size_t raw = (w + 1) * h;
|
||||
TEST_ASSERT_EQUAL(2 + 5 + raw + 4, idat.size());
|
||||
TEST_ASSERT_EQUAL_HEX8(0x78, idat[0]);
|
||||
TEST_ASSERT_EQUAL(0, ((idat[0] << 8) | idat[1]) % 31); // the header's own check
|
||||
TEST_ASSERT_EQUAL_HEX8(0x01, idat[2]); // final, stored
|
||||
TEST_ASSERT_EQUAL(raw, idat[3] | (idat[4] << 8));
|
||||
TEST_ASSERT_EQUAL(static_cast<uint16_t>(~raw), idat[5] | (idat[6] << 8));
|
||||
TEST_ASSERT_EQUAL(0, idat[7]); // the first row's filter byte: none
|
||||
TEST_ASSERT_EQUAL_HEX8(0xE0, idat[9]); // its second pixel
|
||||
TEST_ASSERT_EQUAL_HEX32(adler32(1, idat.data() + 7, raw), be(idat, 7 + raw));
|
||||
}
|
||||
|
||||
void test_the_screen_fits_and_a_bigger_image_is_refused() {
|
||||
TEST_ASSERT_EQUAL(8 + 25 + 780 + 12 + (2 + 5 + 241 * 135 + 4) + 12, Rgb332Writer::fileSize(240, 135));
|
||||
int calls = 0;
|
||||
Rgb332Writer screen(240, 135, [&](const uint8_t*, size_t) { return ++calls > 0; });
|
||||
TEST_ASSERT_TRUE(screen.begin());
|
||||
Rgb332Writer big(320, 240, [](const uint8_t*, size_t) { return true; }); // 77,040 bytes: more than one block holds
|
||||
TEST_ASSERT_FALSE(big.begin());
|
||||
}
|
||||
|
||||
void test_a_sink_that_fails_stops_it_and_too_few_rows_dont_end() {
|
||||
Rgb332Writer failing(4, 3, [](const uint8_t*, size_t) { return false; });
|
||||
TEST_ASSERT_FALSE(failing.begin());
|
||||
Rgb332Writer shortOne(4, 3, [](const uint8_t*, size_t) { return true; });
|
||||
TEST_ASSERT_TRUE(shortOne.begin());
|
||||
const uint8_t row[4] = {};
|
||||
TEST_ASSERT_TRUE(shortOne.row(row));
|
||||
TEST_ASSERT_FALSE(shortOne.end());
|
||||
}
|
||||
|
||||
int main(int, char**) {
|
||||
UNITY_BEGIN();
|
||||
RUN_TEST(test_the_checksums_match_their_reference_values);
|
||||
RUN_TEST(test_a_small_image_is_a_well_formed_png);
|
||||
RUN_TEST(test_the_screen_fits_and_a_bigger_image_is_refused);
|
||||
RUN_TEST(test_a_sink_that_fails_stops_it_and_too_few_rows_dont_end);
|
||||
return UNITY_END();
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
#include <unity.h>
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "shell_log.h"
|
||||
|
||||
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); }
|
||||
|
||||
void test_lines_arrive_in_pieces() {
|
||||
ShellLog log;
|
||||
feed(log, "firmware: roro", 0);
|
||||
TEST_ASSERT_EQUAL(0, log.lines().size());
|
||||
feed(log, "9stack\r\nuptime: 1s\n", 0);
|
||||
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());
|
||||
}
|
||||
|
||||
void test_the_heap_line_is_never_kept() {
|
||||
ShellLog log;
|
||||
feed(log, "status: heap 105000 min 90000\nirc: connected\n", 0);
|
||||
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
|
||||
size_t bytes = 0;
|
||||
for (auto& l : log.lines()) bytes += l.size() + 1;
|
||||
TEST_ASSERT_TRUE(bytes <= ShellLog::kMaxBytes);
|
||||
TEST_ASSERT_TRUE(log.lines().size() >= 80);
|
||||
uint32_t before = log.revision();
|
||||
log.add("> info");
|
||||
TEST_ASSERT_EQUAL_STRING("> info", log.lines().back().c_str());
|
||||
TEST_ASSERT_TRUE(log.revision() != before);
|
||||
log.clear();
|
||||
TEST_ASSERT_EQUAL(0, log.lines().size());
|
||||
}
|
||||
|
||||
static const char* kHelp =
|
||||
"info firmware, uptime, memory\n"
|
||||
"ls [folder] | du <path> | mkdir <path> | rm <path> the SD card\n"
|
||||
"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";
|
||||
|
||||
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"};
|
||||
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());
|
||||
}
|
||||
|
||||
void test_tab_completes_the_first_word() {
|
||||
auto words = commandWords(kHelp);
|
||||
std::vector<std::string> matches;
|
||||
TEST_ASSERT_EQUAL_STRING("info ", completeCommand("in", words, matches).c_str()); // the only one: with its space
|
||||
TEST_ASSERT_EQUAL(0, matches.size());
|
||||
TEST_ASSERT_EQUAL_STRING("l", completeCommand("l", words, matches).c_str()); // ls, lora, log: they agree on no more
|
||||
TEST_ASSERT_EQUAL(3, matches.size());
|
||||
TEST_ASSERT_EQUAL_STRING("mkdir ", completeCommand("m", words, matches).c_str());
|
||||
TEST_ASSERT_EQUAL_STRING("lo", completeCommand("lo", words, matches).c_str()); // lora, log
|
||||
TEST_ASSERT_EQUAL(2, matches.size());
|
||||
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());
|
||||
}
|
||||
|
||||
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);
|
||||
return UNITY_END();
|
||||
}
|
||||
Reference in New Issue
Block a user