fwt_software/src/core/ChannelTopics.cpp

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