159 lines
5.8 KiB
C++
159 lines
5.8 KiB
C++
#include "fgc/ChannelTopics.h"
|
|
|
|
#include <cctype>
|
|
#include <cstdio>
|
|
|
|
namespace fgc {
|
|
|
|
std::string towerTopic(const std::string& tower, const std::string& leaf) {
|
|
return "GGS/FWT/" + tower + "/" + leaf;
|
|
}
|
|
|
|
std::string camEventJson(const CamEvent& e) {
|
|
return "{ \"fwt\":\"" + e.tower + "\", \"cam\":\"" + e.camera +
|
|
"\", \"hdg\":" + std::to_string(e.heading_decideg) +
|
|
", \"pit\":" + std::to_string(e.pitch_decideg) +
|
|
", \"time\":" + std::to_string(e.timestamp_ms) + " }";
|
|
}
|
|
|
|
std::string jsonString(const std::string& s) {
|
|
std::string out = "\"";
|
|
for (unsigned char c : s) {
|
|
switch (c) {
|
|
case '"': out += "\\\""; break;
|
|
case '\\': out += "\\\\"; break;
|
|
case '\n': out += "\\n"; break;
|
|
case '\r': out += "\\r"; break;
|
|
case '\t': out += "\\t"; break;
|
|
default:
|
|
if (c < 0x20) {
|
|
char b[8]; std::snprintf(b, sizeof(b), "\\u%04x", c); out += b;
|
|
} else {
|
|
out += static_cast<char>(c); // UTF-8 passes through as is
|
|
}
|
|
}
|
|
}
|
|
return out + "\"";
|
|
}
|
|
|
|
namespace {
|
|
|
|
// A minimal reader for one flat JSON object whose values are strings. Enough
|
|
// for {"id":"...","cmd":"..."}; anything else it reports rather than guesses.
|
|
struct FlatJson {
|
|
const std::string& s;
|
|
size_t i = 0;
|
|
std::string err;
|
|
|
|
void ws() { while (i < s.size() && std::isspace(static_cast<unsigned char>(s[i]))) ++i; }
|
|
bool eat(char c) { ws(); if (i < s.size() && s[i] == c) { ++i; return true; } return false; }
|
|
|
|
bool str(std::string& out) {
|
|
ws();
|
|
if (i >= s.size() || s[i] != '"') { err = "expected a string"; return false; }
|
|
++i;
|
|
while (i < s.size() && s[i] != '"') {
|
|
char c = s[i++];
|
|
if (c != '\\') { out += c; continue; }
|
|
if (i >= s.size()) break;
|
|
char e = s[i++];
|
|
switch (e) {
|
|
case 'n': out += '\n'; break;
|
|
case 'r': out += '\r'; break;
|
|
case 't': out += '\t'; break;
|
|
case 'b': out += '\b'; break;
|
|
case 'f': out += '\f'; break;
|
|
case 'u': {
|
|
if (i + 4 > s.size()) { err = "bad \\u escape"; return false; }
|
|
unsigned cp = static_cast<unsigned>(std::stoul(s.substr(i, 4), nullptr, 16));
|
|
i += 4;
|
|
// Encode as UTF-8 (the BMP is enough for a command line).
|
|
if (cp < 0x80) out += static_cast<char>(cp);
|
|
else if (cp < 0x800) {
|
|
out += static_cast<char>(0xC0 | (cp >> 6));
|
|
out += static_cast<char>(0x80 | (cp & 0x3F));
|
|
} else {
|
|
out += static_cast<char>(0xE0 | (cp >> 12));
|
|
out += static_cast<char>(0x80 | ((cp >> 6) & 0x3F));
|
|
out += static_cast<char>(0x80 | (cp & 0x3F));
|
|
}
|
|
break;
|
|
}
|
|
default: out += e; break; // \" \\ \/
|
|
}
|
|
}
|
|
if (i >= s.size()) { err = "unterminated string"; return false; }
|
|
++i;
|
|
return true;
|
|
}
|
|
|
|
// Skip a non-string value (number, true/false/null) so other keys may exist.
|
|
void skipScalar() {
|
|
ws();
|
|
while (i < s.size() && s[i] != ',' && s[i] != '}') ++i;
|
|
}
|
|
};
|
|
|
|
} // namespace
|
|
|
|
RemoteCommand parseCommandPayload(const std::string& payload) {
|
|
RemoteCommand rc;
|
|
size_t a = payload.find_first_not_of(" \t\r\n");
|
|
if (a == std::string::npos) { rc.error = "empty command"; return rc; }
|
|
if (payload[a] != '{') {
|
|
// A bare command line: what a person types into mosquitto_pub.
|
|
size_t b = payload.find_last_not_of(" \t\r\n");
|
|
rc.cmd = payload.substr(a, b - a + 1);
|
|
if (rc.cmd.find('\n') != std::string::npos) rc.error = "one command per message";
|
|
return rc;
|
|
}
|
|
FlatJson j{payload};
|
|
j.i = a;
|
|
try {
|
|
if (!j.eat('{')) { rc.error = "not a JSON object"; return rc; }
|
|
bool first = true;
|
|
while (!j.eat('}')) {
|
|
if (!first && !j.eat(',')) { rc.error = "expected , or }"; return rc; }
|
|
first = false;
|
|
std::string key;
|
|
if (!j.str(key)) { rc.error = "JSON: " + j.err; return rc; }
|
|
if (!j.eat(':')) { rc.error = "expected :"; return rc; }
|
|
j.ws();
|
|
if (j.i < payload.size() && payload[j.i] == '"') {
|
|
std::string val;
|
|
if (!j.str(val)) { rc.error = "JSON: " + j.err; return rc; }
|
|
if (key == "id") rc.id = val;
|
|
else if (key == "cmd") rc.cmd = val;
|
|
} else {
|
|
if (key == "id" || key == "cmd") { rc.error = key + " must be a string"; return rc; }
|
|
j.skipScalar();
|
|
}
|
|
}
|
|
} catch (const std::exception&) {
|
|
rc.error = "unreadable JSON";
|
|
return rc;
|
|
}
|
|
if (rc.cmd.find_first_not_of(" \t") == std::string::npos) rc.error = "no \"cmd\" in the message";
|
|
else if (rc.cmd.find('\n') != std::string::npos) rc.error = "one command per message";
|
|
return rc;
|
|
}
|
|
|
|
std::string commandReplyJson(const CommandReply& r) {
|
|
std::string lines = "[";
|
|
size_t bytes = 0, n = 0;
|
|
bool truncated = r.truncated;
|
|
for (const auto& l : r.lines) {
|
|
if (n == kReplyMaxLines || bytes + l.size() > kReplyMaxBytes) { truncated = true; break; }
|
|
if (n) lines += ",";
|
|
lines += jsonString(l);
|
|
bytes += l.size();
|
|
++n;
|
|
}
|
|
lines += "]";
|
|
return "{\"id\":" + jsonString(r.id) + ",\"cmd\":" + jsonString(r.cmd) +
|
|
",\"level\":" + jsonString(r.level) + ",\"lines\":" + lines +
|
|
",\"truncated\":" + (truncated ? "true" : "false") + "}";
|
|
}
|
|
|
|
} // namespace fgc
|