#include "fgc/ChannelTopics.h" #include #include 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(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(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(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(cp); else if (cp < 0x800) { out += static_cast(0xC0 | (cp >> 6)); out += static_cast(0x80 | (cp & 0x3F)); } else { out += static_cast(0xE0 | (cp >> 12)); out += static_cast(0x80 | ((cp >> 6) & 0x3F)); out += static_cast(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