Files
roro9stack/test/test_file_views/test_file_views.cpp
T
twislaandClaude Opus 5.5 b7aed8e91c F1 steps 2-6: the Storage App, its viewers, and Maintenance moved in (#3)
The Storage App browses the SD card: folders first with sizes and dates,
three sorts, one item at a time with a clipboard (c, x, v), rename, delete
after counting what's inside, new folder, details. A listing holds 256
entries and says when a folder has more.

FileOps does the card's work for the App and the console alike, one
operation at a time on the storage task in turns of about 150 ms, so Logs
and Captures are still written during a long copy. A copy shows progress,
can be cancelled (what it wrote is taken back) and compares sizes after.
The read-only rules are checked there: the firmware's top-level folders,
/gemini/cache, and files being written (a Track, a Capture, an upload,
today's IRC Logs). A listing reads the folder straight from FatFs: through
the Arduino File, 329 entries took over two seconds.

Viewers by type: text read a screen at a time whatever the file's size
(logs open at the end), a hex dump, a Capture's packets as the LoRa Scanner
lists them, a Track's summary, and an Update File checked as an install
would check it, without writing anything. Tab shows any file as hex or text.

Settings > Storage is gone: usage, Storage Clean-up and Erase are the App's
Maintenance, behind a warning. The Storage Warning points there.

The Clock sets the system time whatever its source, so files are dated
correctly with a GNSS Fix alone (Q137).

Console: cp, mv, mkdir, du, cancel; rm takes folders and follows the rules;
ls shows dates; Debug Builds get `sd fill`.

424 host tests. Checked on the device: docs/milestones/F1.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EhqxQ49eCju4CzKYNjZzwT
2026-10-06 08:45:43 +02:00

134 lines
5.2 KiB
C++

#include <unity.h>
#include <cstdlib>
#include <ctime>
#include <string>
#include <vector>
#include "file_views.h"
#include "loratap.h"
#include "track.h"
using namespace roro;
using namespace roro::files;
void setUp() {}
void tearDown() {}
void test_hex_row() {
const uint8_t d[] = {'H', 'e', 'l', 'l', 'o', ',', ' ', 'w', 0x00, 0xFF, 'x'};
TEST_ASSERT_EQUAL_STRING("00010 4865 6c6c 6f2c 2077 Hello, w", hexRow(0x10, d, 8).c_str());
TEST_ASSERT_EQUAL_STRING("00018 00ff 78 ..x", hexRow(0x18, d + 8, 3).c_str());
TEST_ASSERT_TRUE(hexRow(0xFFFFF, d, 8).size() <= 38);
}
// A Track as the GNSS Service writes it, fed in pieces of every size: the same summary each time.
void test_gpx_summary() {
setenv("TZ", "UTC", 1);
tzset();
std::string file = gnss::gpx::header("test");
int64_t t0 = 1791230400; // 2026-10-05 20:00:00 UTC
for (int i = 0; i < 50; i++) file += gnss::gpx::point(48.0 + i * 0.0001, 7.0, i % 2 == 0, 200, t0 + i * 5) + "\n";
file += gnss::gpx::footer();
for (size_t piece : {size_t(1), size_t(7), size_t(64), size_t(1000), file.size()}) {
GpxSummary s;
for (size_t at = 0; at < file.size(); at += piece) s.feed(file.data() + at, std::min(piece, file.size() - at));
TEST_ASSERT_EQUAL_UINT32(50, s.points());
TEST_ASSERT_EQUAL_INT64(t0, s.start());
TEST_ASSERT_EQUAL_INT64(t0 + 245, s.end());
TEST_ASSERT_FLOAT_WITHIN(1.0f, 49 * 11.12f, static_cast<float>(s.meters())); // 0.0001 degree of latitude: 11.12 m
}
GpxSummary s;
s.feed(file.data(), file.size());
auto lines = s.lines();
TEST_ASSERT_EQUAL_size_t(4, lines.size());
TEST_ASSERT_EQUAL_STRING("50 points", lines[0].c_str());
TEST_ASSERT_EQUAL_STRING("Started 2026-10-05 20:00", lines[1].c_str());
TEST_ASSERT_EQUAL_STRING("Lasted 4 min 05 s", lines[2].c_str());
TEST_ASSERT_EQUAL_STRING("Distance 545 m", lines[3].c_str());
}
void test_gpx_from_elsewhere_and_not_gpx() {
GpxSummary s;
std::string other = "<gpx><trk><trkseg><trkpt lon=\"7.0\" lat=\"48.0\"/><trkpt lat=\"48.01\" lon=\"7.0\">\n<ele>1</ele>\n</trkpt></trkseg></trk></gpx>";
s.feed(other.data(), other.size());
TEST_ASSERT_EQUAL_UINT32(2, s.points());
TEST_ASSERT_EQUAL_INT64(0, s.start());
TEST_ASSERT_FLOAT_WITHIN(5.0f, 1112.0f, static_cast<float>(s.meters()));
TEST_ASSERT_EQUAL_size_t(2, s.lines().size()); // no times: points and distance only
GpxSummary none;
std::string junk(10000, 'x');
none.feed(junk.data(), junk.size());
TEST_ASSERT_EQUAL_UINT32(0, none.points());
}
void test_durations() {
TEST_ASSERT_EQUAL_STRING("12 s", formatDuration(12).c_str());
TEST_ASSERT_EQUAL_STRING("4 min 10 s", formatDuration(250).c_str());
TEST_ASSERT_EQUAL_STRING("1 h 02 min", formatDuration(3725).c_str());
}
// What the LoRa Capture Service writes reads back the same.
void test_pcap_round_trip() {
std::vector<uint8_t> file;
lora::appendPcapHeader(file);
lora::RxInfo rx;
rx.frequencyHz = 869525000;
rx.bandwidthKHz = 250;
rx.spreadingFactor = 11;
rx.rssi = -97;
rx.snr = -6.25f;
rx.noiseFloor = -106;
rx.syncWord = 0x2B;
const uint8_t payload[] = {1, 2, 3, 4, 5};
lora::appendRecord(file, 1791230400, 250000, rx, payload, sizeof payload);
lora::appendRecord(file, 1791230401, 0, rx, payload, 0);
PcapHeader h = parsePcapHeader(file.data(), file.size());
TEST_ASSERT_TRUE(h.ok);
TEST_ASSERT_EQUAL_UINT32(kLinkLoraTap, h.linkType);
size_t at = kPcapHeaderSize;
PcapRecord r;
TEST_ASSERT_TRUE(parsePcapRecord(file.data() + at, file.size() - at, r));
TEST_ASSERT_EQUAL_UINT32(1791230400, r.seconds);
TEST_ASSERT_EQUAL_UINT32(250000, r.micros);
TEST_ASSERT_EQUAL_UINT32(lora::kLoraTapSize + 5, r.length);
lora::RxInfo back;
TEST_ASSERT_TRUE(parseLoraTap(file.data() + at + kPcapRecordSize, r.length, back));
TEST_ASSERT_EQUAL_UINT32(869525000, back.frequencyHz);
TEST_ASSERT_EQUAL_FLOAT(250, back.bandwidthKHz);
TEST_ASSERT_EQUAL_UINT8(11, back.spreadingFactor);
TEST_ASSERT_EQUAL_FLOAT(-97, back.rssi);
TEST_ASSERT_EQUAL_FLOAT(-6.25f, back.snr);
TEST_ASSERT_EQUAL_FLOAT(-106, back.noiseFloor);
TEST_ASSERT_EQUAL_HEX8(0x2B, back.syncWord);
at += kPcapRecordSize + r.length;
TEST_ASSERT_TRUE(parsePcapRecord(file.data() + at, file.size() - at, r));
TEST_ASSERT_EQUAL_UINT32(lora::kLoraTapSize, r.length);
at += kPcapRecordSize + r.length;
TEST_ASSERT_EQUAL_size_t(file.size(), at);
TEST_ASSERT_FALSE(parsePcapRecord(file.data() + at, file.size() - at, r)); // the end
}
void test_not_a_pcap() {
const uint8_t junk[24] = {'n', 'o', 'p', 'e'};
TEST_ASSERT_FALSE(parsePcapHeader(junk, sizeof junk).ok);
TEST_ASSERT_FALSE(parsePcapHeader(junk, 10).ok);
const uint8_t huge[16] = {0, 0, 0, 0, 0, 0, 0, 0, 0xFF, 0xFF, 0xFF, 0x7F};
PcapRecord r;
TEST_ASSERT_FALSE(parsePcapRecord(huge, sizeof huge, r));
lora::RxInfo rx;
TEST_ASSERT_FALSE(parseLoraTap(junk, 10, rx));
}
int main() {
UNITY_BEGIN();
RUN_TEST(test_hex_row);
RUN_TEST(test_gpx_summary);
RUN_TEST(test_gpx_from_elsewhere_and_not_gpx);
RUN_TEST(test_durations);
RUN_TEST(test_pcap_round_trip);
RUN_TEST(test_not_a_pcap);
return UNITY_END();
}