Add uname, /uname and scp (key or masked password) in the Shell
CI / build (pull_request) Failing after 1m59s
Site / build (pull_request) Successful in 9s

- uname in the console and /uname in IRC: the firmware, its version and the chip
- scp: one file between the card and a trusted server, over SSH, with the
  device's key or a password asked (masked) in the Shell
- shared libssh helpers in src/platform/libssh_util
- README, user guide, command reference and a how-to updated

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0162FokPdvY2KsS4NBfwyWPk
This commit is contained in:
2026-10-09 19:01:36 +02:00
co-authored by Claude Sonnet 5.5
parent e707a5f2d4
commit 18285c3212
25 changed files with 730 additions and 34 deletions
+3 -1
View File
@@ -360,10 +360,12 @@ void IrcSession::command(int b, const std::string& text, int64_t utc) {
} else if (verb == "quit") {
quit_ = true;
send(IrcMessage::serialize("QUIT", {rest.empty() ? "roro9stack" : rest}));
} else if (verb == "uname") { // shown here, not said to anyone
info(b, unameString(), utc);
} else if (verb == "raw" || verb == "quote") {
if (!rest.empty()) send(rest);
} else {
info(b, "Unknown command /" + verb + " (try /join or /j, /part /msg /me /nick /topic /names /quit /raw)", utc);
info(b, "Unknown command /" + verb + " (try /join or /j, /part /msg /me /nick /topic /names /uname /quit /raw)", utc);
}
}
+81
View File
@@ -0,0 +1,81 @@
#include "scp_args.h"
#include <vector>
namespace roro::term {
namespace {
std::vector<std::string> words(const std::string& text) {
std::vector<std::string> out;
for (size_t at = 0; at < text.size();) {
size_t end = text.find(' ', at);
if (end == std::string::npos) end = text.size();
if (end > at) out.push_back(text.substr(at, end - at));
at = end + 1;
}
return out;
}
// user@host:path, as opposed to a path on the card.
bool isRemote(const std::string& w) {
if (w.empty() || w[0] == '/') return false;
size_t at = w.find('@'), colon = w.find(':');
return at != std::string::npos && colon != std::string::npos && at < colon;
}
std::string baseName(const std::string& path) {
size_t slash = path.rfind('/');
return slash == std::string::npos ? path : path.substr(slash + 1);
}
} // namespace
std::string parseScp(const std::string& args, ScpArgs& out) {
static const char* kUsage = "scp [-f] [-P port] <file on the card> user@host:path | scp [-f] [-P port] user@host:path <file on the card>";
std::vector<std::string> w = words(args);
long port = 0;
bool replace = false;
while (!w.empty() && w[0][0] == '-') {
if (w[0] == "-f") {
replace = true;
w.erase(w.begin());
} else if (w[0] == "-P" && w.size() >= 2) {
port = 0;
for (char c : w[1]) {
if (c < '0' || c > '9' || port > 65535) return "a port from 1 to 65535";
port = port * 10 + (c - '0');
}
if (port < 1 || port > 65535) return "a port from 1 to 65535";
w.erase(w.begin(), w.begin() + 2);
} else {
return kUsage;
}
}
if (w.size() != 2) return kUsage;
bool firstRemote = isRemote(w[0]), secondRemote = isRemote(w[1]);
if (firstRemote == secondRemote) return firstRemote ? "one side has to be on the card, not both on servers" : kUsage;
ScpArgs a;
a.upload = secondRemote;
a.replace = replace;
const std::string& remote = a.upload ? w[1] : w[0];
std::string local = a.upload ? w[0] : w[1];
size_t colon = remote.find(':');
std::string why = parseSshTarget(remote.substr(0, colon), a.target);
if (!why.empty()) return why;
if (port) a.target.port = static_cast<uint16_t>(port);
a.remote = remote.substr(colon + 1);
if (a.remote.empty()) return "say where on " + a.target.host + ": user@host:path";
if (local.empty() || local[0] != '/') return "a path on the card starts with a slash";
if (local.back() == '/') {
if (a.upload) return "that's a folder: scp copies one file";
std::string name = baseName(a.remote);
if (name.empty()) return "name the file to fetch";
local += name;
}
a.local = local;
out = a;
return "";
}
} // namespace roro::term
+23
View File
@@ -0,0 +1,23 @@
#pragma once
#include <string>
#include "ssh_hosts.h"
namespace roro::term {
// What `scp` was asked: one file to the card from a server, or from the card to a server.
// scp [-f] [-P port] /notes/a.txt user@host:path (upload)
// scp [-f] [-P port] user@host:path /notes/a.txt (download; a destination ending in / gets the remote file's name)
// The card's paths start with a slash; the server's are the server's own (relative ones start at the home folder).
struct ScpArgs {
bool upload = false;
bool replace = false; // -f: a download may replace a file on the card
SshTarget target;
std::string local, remote;
};
// "" or what is wrong with it.
std::string parseScp(const std::string& args, ScpArgs& out);
} // namespace roro::term
+18
View File
@@ -8,8 +8,26 @@
#define RORO_VERSION "unknown"
#endif
#if __has_include("sdkconfig.h")
#include "sdkconfig.h"
#endif
namespace roro {
const char* versionString() { return RORO_VERSION; }
const char* architectureString() {
#if defined(CONFIG_IDF_TARGET_ESP32S3)
return "xtensa-lx7 esp32s3";
#elif defined(CONFIG_IDF_TARGET_ESP32)
return "xtensa-lx6 esp32";
#elif defined(CONFIG_IDF_TARGET)
return CONFIG_IDF_TARGET;
#else
return "host";
#endif
}
std::string unameString() { return std::string(kProductName) + " " + versionString() + " " + architectureString(); }
} // namespace roro
+8
View File
@@ -1,5 +1,7 @@
#pragma once
#include <string>
namespace roro {
constexpr const char* kProductName = "roro9stack";
@@ -9,4 +11,10 @@ constexpr const char* kProductName = "roro9stack";
// that one file, and everything else comes from the build cache (issue #74).
const char* versionString();
// The chip this firmware is built for: "xtensa-lx7 esp32s3". "host" in the native tests.
const char* architectureString();
// What `uname` and IRC's /uname report: "roro9stack v0.22.0 xtensa-lx7 esp32s3".
std::string unameString();
} // namespace roro