diff --git a/.clusterfuzzlite/Dockerfile b/.clusterfuzzlite/Dockerfile index 6114417c9..86de4f5db 100644 --- a/.clusterfuzzlite/Dockerfile +++ b/.clusterfuzzlite/Dockerfile @@ -18,7 +18,7 @@ ENV PIP_ROOT_USER_ACTION=ignore # trunk-ignore(hadolint/DL3008): apt packages are not pinned. # trunk-ignore(terrascan/AC_DOCKER_0002): apt packages are not pinned. RUN apt-get update && apt-get install --no-install-recommends -y \ - cmake git zip libgpiod-dev libbluetooth-dev libi2c-dev \ + cmake git zip libgpiod-dev libjsoncpp-dev libbluetooth-dev libi2c-dev \ libunistring-dev libmicrohttpd-dev libgnutls28-dev libgcrypt20-dev \ libusb-1.0-0-dev libssl-dev pkg-config libsqlite3-dev libsdl2-dev && \ apt-get clean && rm -rf /var/lib/apt/lists/* && \ diff --git a/.clusterfuzzlite/build.sh b/.clusterfuzzlite/build.sh index 86ab775f9..64bbbd56c 100644 --- a/.clusterfuzzlite/build.sh +++ b/.clusterfuzzlite/build.sh @@ -31,7 +31,7 @@ cmake --install "$WORK/ulfius/$SANITIZER" --prefix /usr cd "$SRC/firmware" PLATFORMIO_EXTRA_SCRIPTS=$(echo -e "pre:.clusterfuzzlite/platformio-clusterfuzzlite-pre.py\npost:.clusterfuzzlite/platformio-clusterfuzzlite-post.py") -STATIC_LIBS=$(pkg-config --libs --static libulfius openssl libgpiod yaml-cpp bluez --silence-errors) +STATIC_LIBS=$(pkg-config --libs --static libulfius openssl libgpiod yaml-cpp jsoncpp bluez --silence-errors) export PLATFORMIO_EXTRA_SCRIPTS export STATIC_LIBS export PLATFORMIO_WORKSPACE_DIR="$WORK/pio/$SANITIZER" diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index f546d4cfd..d10db534f 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -16,6 +16,7 @@ RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ libssl-dev \ libulfius-dev \ libyaml-cpp-dev \ + libjsoncpp-dev \ pipx \ pkg-config \ python3 \ diff --git a/.github/actions/setup-native/action.yml b/.github/actions/setup-native/action.yml index 05f95cd40..3a38a4a22 100644 --- a/.github/actions/setup-native/action.yml +++ b/.github/actions/setup-native/action.yml @@ -11,4 +11,4 @@ runs: - name: Install libs needed for native build shell: bash run: | - sudo apt-get install -y libbluetooth-dev libgpiod-dev libyaml-cpp-dev openssl libssl-dev libulfius-dev liborcania-dev libusb-1.0-0-dev libi2c-dev libuv1-dev + sudo apt-get install -y libbluetooth-dev libgpiod-dev libyaml-cpp-dev libjsoncpp-dev openssl libssl-dev libulfius-dev liborcania-dev libusb-1.0-0-dev libi2c-dev libuv1-dev diff --git a/Dockerfile b/Dockerfile index e00d81658..01f1b3540 100644 --- a/Dockerfile +++ b/Dockerfile @@ -12,7 +12,7 @@ ENV TZ=Etc/UTC ENV PIP_ROOT_USER_ACTION=ignore RUN apt-get update && apt-get install --no-install-recommends -y \ curl wget g++ zip git ca-certificates pkg-config \ - libgpiod-dev libyaml-cpp-dev libbluetooth-dev libi2c-dev libuv1-dev \ + libgpiod-dev libyaml-cpp-dev libjsoncpp-dev libbluetooth-dev libi2c-dev libuv1-dev \ libusb-1.0-0-dev libulfius-dev liborcania-dev libssl-dev \ libx11-dev libinput-dev libxkbcommon-x11-dev libsqlite3-dev libsdl2-dev \ && apt-get clean && rm -rf /var/lib/apt/lists/* \ @@ -51,7 +51,7 @@ ENV TZ=Etc/UTC USER root RUN apt-get update && apt-get --no-install-recommends -y install \ - libc-bin libc6 libgpiod3 libyaml-cpp0.8 libi2c0 libuv1t64 libusb-1.0-0-dev \ + libc-bin libc6 libgpiod3 libyaml-cpp0.8 libjsoncpp25 libi2c0 libuv1t64 libusb-1.0-0-dev \ liborcania2.3 libulfius2.7t64 libssl3t64 \ libx11-6 libinput10 libxkbcommon-x11-0 libsdl2-2.0-0 \ && apt-get clean && rm -rf /var/lib/apt/lists/* \ diff --git a/Dockerfile.test b/Dockerfile.test index 12479b36d..a3077ea72 100644 --- a/Dockerfile.test +++ b/Dockerfile.test @@ -7,7 +7,7 @@ ENV PIP_ROOT_USER_ACTION=ignore # hadolint ignore=DL3008 RUN apt-get update && apt-get install --no-install-recommends -y \ g++ git ca-certificates pkg-config \ - libgpiod-dev libyaml-cpp-dev libbluetooth-dev libi2c-dev libuv1-dev \ + libgpiod-dev libyaml-cpp-dev libjsoncpp-dev libbluetooth-dev libi2c-dev libuv1-dev \ libusb-1.0-0-dev libulfius-dev liborcania-dev libssl-dev \ libx11-dev libinput-dev libxkbcommon-x11-dev libsqlite3-dev libsdl2-dev \ && apt-get clean && rm -rf /var/lib/apt/lists/* \ diff --git a/alpine.Dockerfile b/alpine.Dockerfile index 75c9aa594..22258ed28 100644 --- a/alpine.Dockerfile +++ b/alpine.Dockerfile @@ -9,7 +9,7 @@ ENV PIP_ROOT_USER_ACTION=ignore RUN apk --no-cache add \ bash g++ libstdc++-dev linux-headers zip git ca-certificates libbsd-dev \ - libgpiod-dev yaml-cpp-dev bluez-dev \ + libgpiod-dev yaml-cpp-dev jsoncpp-dev bluez-dev \ libusb-dev i2c-tools-dev libuv-dev openssl-dev pkgconf argp-standalone \ libx11-dev libinput-dev libxkbcommon-dev sqlite-dev sdl2-dev \ && rm -rf /var/cache/apk/* \ @@ -41,7 +41,7 @@ LABEL org.opencontainers.image.title="Meshtastic" \ USER root RUN apk --no-cache add \ - shadow libstdc++ libbsd libgpiod yaml-cpp libusb \ + shadow libstdc++ libbsd libgpiod yaml-cpp jsoncpp libusb \ i2c-tools libuv libx11 libinput libxkbcommon sdl2 \ && rm -rf /var/cache/apk/* \ && mkdir -p /var/lib/meshtasticd \ diff --git a/bin/config-dist.yaml b/bin/config-dist.yaml index 9bf4e5943..38fc057a0 100644 --- a/bin/config-dist.yaml +++ b/bin/config-dist.yaml @@ -185,6 +185,10 @@ Input: Logging: LogLevel: info # debug, info, warn, error +# TraceFile: /var/log/meshtasticd.json +# JSONFile: /packets.json # File location for JSON output of decoded packets +# JSONFileRotate: 60 # Rotate JSON file every N minutes, or 0 for no rotation +# JSONFilter: position # filter for packets to save to JSON file # AsciiLogs: true # default if not specified is !isatty() on stdout Webserver: diff --git a/debian/control b/debian/control index 8e5f17af9..38e998b97 100644 --- a/debian/control +++ b/debian/control @@ -14,6 +14,7 @@ Build-Depends: debhelper-compat (= 13), g++, pkg-config, libyaml-cpp-dev, + libjsoncpp-dev, libgpiod-dev, libbluetooth-dev, libusb-1.0-0-dev, diff --git a/meshtasticd.spec.rpkg b/meshtasticd.spec.rpkg index a9eb552d7..0fd5f1427 100644 --- a/meshtasticd.spec.rpkg +++ b/meshtasticd.spec.rpkg @@ -34,6 +34,7 @@ BuildRequires: python3dist(grpcio-tools) BuildRequires: git-core BuildRequires: gcc-c++ BuildRequires: pkgconfig(yaml-cpp) +BuildRequires: pkgconfig(jsoncpp) BuildRequires: pkgconfig(libgpiod) BuildRequires: pkgconfig(bluez) BuildRequires: pkgconfig(libusb-1.0) diff --git a/src/RedirectablePrint.cpp b/src/RedirectablePrint.cpp index 0dcd90080..89326798f 100644 --- a/src/RedirectablePrint.cpp +++ b/src/RedirectablePrint.cpp @@ -285,8 +285,20 @@ void RedirectablePrint::log(const char *logLevel, const char *format, ...) newFormat[len + 1] = '\0'; #if ARCH_PORTDUINO - if (portduino_config.logoutputlevel < level_trace && strcmp(logLevel, MESHTASTIC_LOG_LEVEL_TRACE) == 0) { - return; + // level trace is special, two possible ways to handle it. + if (strcmp(logLevel, MESHTASTIC_LOG_LEVEL_TRACE) == 0) { + if (portduino_config.traceFilename != "") { + va_list arg; + va_start(arg, format); + try { + traceFile << va_arg(arg, char *) << std::endl; + } catch (const std::ios_base::failure &e) { + } + va_end(arg); + } + if (portduino_config.logoutputlevel < level_trace && strcmp(logLevel, MESHTASTIC_LOG_LEVEL_TRACE) == 0) { + return; + } } if (portduino_config.logoutputlevel < level_debug && strcmp(logLevel, MESHTASTIC_LOG_LEVEL_DEBUG) == 0) { return; diff --git a/src/mesh/Router.cpp b/src/mesh/Router.cpp index 7666e4ca4..90c6aca25 100644 --- a/src/mesh/Router.cpp +++ b/src/mesh/Router.cpp @@ -19,7 +19,9 @@ #endif #include "Default.h" #if ARCH_PORTDUINO +#include "Throttle.h" #include "platform/portduino/PortduinoGlue.h" +#include "serialization/MeshPacketSerializer.h" #endif #define MAX_RX_FROMRADIO \ @@ -540,6 +542,34 @@ DecodeState perhapsDecode(meshtastic_MeshPacket *p) } */ printPacket("decoded message", p); +#if ARCH_PORTDUINO + if (portduino_config.traceFilename != "" || portduino_config.logoutputlevel == level_trace) { + LOG_TRACE("%s", MeshPacketSerializer::JsonSerialize(p, false).c_str()); + } else if (portduino_config.JSONFilename != "") { + if (portduino_config.JSONFileRotate != 0) { + static uint32_t fileage = 0; + + if (portduino_config.JSONFileRotate != 0 && + (fileage == 0 || !Throttle::isWithinTimespanMs(fileage, portduino_config.JSONFileRotate * 60 * 1000))) { + time_t timestamp = time(NULL); + struct tm *timeinfo; + char buffer[80]; + timeinfo = localtime(×tamp); + strftime(buffer, 80, "%Y%m%d-%H%M%S", timeinfo); + + std::string datetime(buffer); + if (JSONFile.is_open()) { + JSONFile.close(); + } + JSONFile.open(portduino_config.JSONFilename + "_" + datetime, std::ios::out | std::ios::app); + fileage = millis(); + } + } + if (portduino_config.JSONFilter == (_meshtastic_PortNum)0 || portduino_config.JSONFilter == p->decoded.portnum) { + JSONFile << MeshPacketSerializer::JsonSerialize(p, false) << std::endl; + } + } +#endif return DecodeState::DECODE_SUCCESS; } else { LOG_WARN("No suitable channel found for decoding, hash was 0x%x!", p->channel); @@ -804,6 +834,13 @@ void Router::handleReceived(meshtastic_MeshPacket *p, RxSource src) void Router::perhapsHandleReceived(meshtastic_MeshPacket *p) { +#if ARCH_PORTDUINO + // Even ignored packets get logged in the trace + if (portduino_config.traceFilename != "" || portduino_config.logoutputlevel == level_trace) { + p->rx_time = getValidTime(RTCQualityFromNet); // store the arrival timestamp for the phone + LOG_TRACE("%s", MeshPacketSerializer::JsonSerializeEncrypted(p).c_str()); + } +#endif // assert(radioConfig.has_preferences); if (is_in_repeated(config.lora.ignore_incoming, p->from)) { LOG_DEBUG("Ignore msg, 0x%x is in our ignore list", p->from); diff --git a/src/platform/portduino/PortduinoGlue.cpp b/src/platform/portduino/PortduinoGlue.cpp index 9a9d4dde2..9e0a1b2a5 100644 --- a/src/platform/portduino/PortduinoGlue.cpp +++ b/src/platform/portduino/PortduinoGlue.cpp @@ -33,6 +33,8 @@ portduino_config_struct portduino_config; portduino_status_struct portduino_status; +std::ofstream traceFile; +std::ofstream JSONFile; Ch341Hal *ch341Hal = nullptr; char *configPath = nullptr; char *optionMac = nullptr; @@ -620,6 +622,31 @@ void portduinoSetup() SPI.begin(portduino_config.lora_spi_dev.c_str()); } + if (portduino_config.traceFilename != "") { + try { + traceFile.open(portduino_config.traceFilename, std::ios::out | std::ios::app); + } catch (std::ofstream::failure &e) { + std::cout << "*** traceFile Exception " << e.what() << std::endl; + exit(EXIT_FAILURE); + } + if (!traceFile.is_open()) { + std::cout << "*** traceFile open failure" << std::endl; + exit(EXIT_FAILURE); + } + } else if (portduino_config.JSONFilename != "") { + try { + if (portduino_config.JSONFileRotate == 0) { + JSONFile.open(portduino_config.JSONFilename, std::ios::out | std::ios::app); + } + } catch (std::ofstream::failure &e) { + std::cout << "*** JSONFile Exception " << e.what() << std::endl; + exit(EXIT_FAILURE); + } + if (!JSONFile.is_open()) { + std::cout << "*** JSONFile open failure" << std::endl; + exit(EXIT_FAILURE); + } + } if (verboseEnabled && portduino_config.logoutputlevel != level_trace) { portduino_config.logoutputlevel = level_debug; } @@ -665,6 +692,31 @@ bool loadConfig(const char *configPath) } else if (yamlConfig["Logging"]["LogLevel"].as("info") == "error") { portduino_config.logoutputlevel = level_error; } + portduino_config.traceFilename = yamlConfig["Logging"]["TraceFile"].as(""); + portduino_config.JSONFilename = yamlConfig["Logging"]["JSONFile"].as(""); + portduino_config.JSONFileRotate = yamlConfig["Logging"]["JSONFileRotate"].as(0); + portduino_config.JSONFilter = (_meshtastic_PortNum)yamlConfig["Logging"]["JSONFilter"].as(0); + if (yamlConfig["Logging"]["JSONFilter"].as("") == "textmessage") + portduino_config.JSONFilter = meshtastic_PortNum_TEXT_MESSAGE_APP; + else if (yamlConfig["Logging"]["JSONFilter"].as("") == "telemetry") + portduino_config.JSONFilter = meshtastic_PortNum_TELEMETRY_APP; + else if (yamlConfig["Logging"]["JSONFilter"].as("") == "nodeinfo") + portduino_config.JSONFilter = meshtastic_PortNum_NODEINFO_APP; + else if (yamlConfig["Logging"]["JSONFilter"].as("") == "position") + portduino_config.JSONFilter = meshtastic_PortNum_POSITION_APP; + else if (yamlConfig["Logging"]["JSONFilter"].as("") == "waypoint") + portduino_config.JSONFilter = meshtastic_PortNum_WAYPOINT_APP; + else if (yamlConfig["Logging"]["JSONFilter"].as("") == "neighborinfo") + portduino_config.JSONFilter = meshtastic_PortNum_NEIGHBORINFO_APP; + else if (yamlConfig["Logging"]["JSONFilter"].as("") == "traceroute") + portduino_config.JSONFilter = meshtastic_PortNum_TRACEROUTE_APP; + else if (yamlConfig["Logging"]["JSONFilter"].as("") == "detection") + portduino_config.JSONFilter = meshtastic_PortNum_DETECTION_SENSOR_APP; + else if (yamlConfig["Logging"]["JSONFilter"].as("") == "paxcounter") + portduino_config.JSONFilter = meshtastic_PortNum_PAXCOUNTER_APP; + else if (yamlConfig["Logging"]["JSONFilter"].as("") == "remotehardware") + portduino_config.JSONFilter = meshtastic_PortNum_REMOTE_HARDWARE_APP; + if (yamlConfig["Logging"]["AsciiLogs"]) { // Default is !isatty(1) but can be set explicitly in config.yaml portduino_config.ascii_logs = yamlConfig["Logging"]["AsciiLogs"].as(); diff --git a/src/platform/portduino/PortduinoGlue.h b/src/platform/portduino/PortduinoGlue.h index d3720a56b..b38cfca25 100644 --- a/src/platform/portduino/PortduinoGlue.h +++ b/src/platform/portduino/PortduinoGlue.h @@ -55,6 +55,9 @@ struct pinMapping { bool default_high = false; }; +extern std::ofstream traceFile; +extern std::ofstream JSONFile; + extern Ch341Hal *ch341Hal; int initGPIOPin(int pinNum, const std::string &gpioChipname, int line); bool loadConfig(const char *configPath); @@ -157,9 +160,14 @@ extern struct portduino_config_struct { // Logging portduino_log_level logoutputlevel = level_debug; + std::string traceFilename; bool ascii_logs = !isatty(1); bool ascii_logs_explicit = false; + std::string JSONFilename; + int JSONFileRotate = 0; + meshtastic_PortNum JSONFilter = (_meshtastic_PortNum)0; + // Webserver std::string webserver_root_path = ""; std::string webserver_ssl_key_path = "/etc/meshtasticd/ssl/private_key.pem"; @@ -461,6 +469,34 @@ extern struct portduino_config_struct { out << "trace"; break; } + if (traceFilename != "") + out << YAML::Key << "TraceFile" << YAML::Value << traceFilename; + if (JSONFilename != "") { + out << YAML::Key << "JSONFile" << YAML::Value << JSONFilename; + if (JSONFileRotate != 0) + out << YAML::Key << "JSONFileRotate" << YAML::Value << JSONFileRotate; + + if (JSONFilter == meshtastic_PortNum_TEXT_MESSAGE_APP) + out << YAML::Key << "JSONFilter" << YAML::Value << "textmessage"; + else if (JSONFilter == meshtastic_PortNum_TELEMETRY_APP) + out << YAML::Key << "JSONFilter" << YAML::Value << "telemetry"; + else if (JSONFilter == meshtastic_PortNum_NODEINFO_APP) + out << YAML::Key << "JSONFilter" << YAML::Value << "nodeinfo"; + else if (JSONFilter == meshtastic_PortNum_POSITION_APP) + out << YAML::Key << "JSONFilter" << YAML::Value << "position"; + else if (JSONFilter == meshtastic_PortNum_WAYPOINT_APP) + out << YAML::Key << "JSONFilter" << YAML::Value << "waypoint"; + else if (JSONFilter == meshtastic_PortNum_NEIGHBORINFO_APP) + out << YAML::Key << "JSONFilter" << YAML::Value << "neighborinfo"; + else if (JSONFilter == meshtastic_PortNum_TRACEROUTE_APP) + out << YAML::Key << "JSONFilter" << YAML::Value << "traceroute"; + else if (JSONFilter == meshtastic_PortNum_DETECTION_SENSOR_APP) + out << YAML::Key << "JSONFilter" << YAML::Value << "detection"; + else if (JSONFilter == meshtastic_PortNum_PAXCOUNTER_APP) + out << YAML::Key << "JSONFilter" << YAML::Value << "paxcounter"; + else if (JSONFilter == meshtastic_PortNum_REMOTE_HARDWARE_APP) + out << YAML::Key << "JSONFilter" << YAML::Value << "remotehardware"; + } if (ascii_logs_explicit) { out << YAML::Key << "AsciiLogs" << YAML::Value << ascii_logs; } diff --git a/src/serialization/MeshPacketSerializer.cpp b/src/serialization/MeshPacketSerializer.cpp new file mode 100644 index 000000000..c440902a3 --- /dev/null +++ b/src/serialization/MeshPacketSerializer.cpp @@ -0,0 +1,470 @@ +#if ARCH_PORTDUINO +#include "MeshPacketSerializer.h" +#include "NodeDB.h" +#include "mesh/generated/meshtastic/mqtt.pb.h" +#include "mesh/generated/meshtastic/telemetry.pb.h" +#include "modules/RoutingModule.h" +#include +#include +#include +#include +#if defined(ARCH_ESP32) +#include "../mesh/generated/meshtastic/paxcount.pb.h" +#endif +#include "mesh/generated/meshtastic/remote_hardware.pb.h" +#include + +static const char *errStr = "Error decoding proto for %s message!"; + +static std::string writeCompact(const Json::Value &v) +{ + Json::StreamWriterBuilder b; + b["indentation"] = ""; + b["emitUTF8"] = true; + return Json::writeString(b, v); +} + +static bool tryParseJson(const char *s, Json::Value &out) +{ + Json::CharReaderBuilder b; + std::unique_ptr reader(b.newCharReader()); + std::string errs; + const char *end = s + strlen(s); + return reader->parse(s, end, &out, &errs); +} + +std::string MeshPacketSerializer::JsonSerialize(const meshtastic_MeshPacket *mp, bool shouldLog) +{ + std::string msgType; + Json::Value jsonObj(Json::objectValue); + + if (mp->which_payload_variant == meshtastic_MeshPacket_decoded_tag) { + Json::Value msgPayload(Json::objectValue); + switch (mp->decoded.portnum) { + case meshtastic_PortNum_TEXT_MESSAGE_APP: { + msgType = "text"; + if (shouldLog) + LOG_DEBUG("got text message of size %u", mp->decoded.payload.size); + + char payloadStr[(mp->decoded.payload.size) + 1]; + memcpy(payloadStr, mp->decoded.payload.bytes, mp->decoded.payload.size); + payloadStr[mp->decoded.payload.size] = 0; + Json::Value parsed; + if (tryParseJson(payloadStr, parsed)) { + if (shouldLog) + LOG_INFO("text message payload is of type json"); + jsonObj["payload"] = parsed; + } else { + if (shouldLog) + LOG_INFO("text message payload is of type plaintext"); + msgPayload["text"] = payloadStr; + jsonObj["payload"] = msgPayload; + } + break; + } + case meshtastic_PortNum_TELEMETRY_APP: { + msgType = "telemetry"; + meshtastic_Telemetry scratch; + meshtastic_Telemetry *decoded = NULL; + memset(&scratch, 0, sizeof(scratch)); + if (pb_decode_from_bytes(mp->decoded.payload.bytes, mp->decoded.payload.size, &meshtastic_Telemetry_msg, &scratch)) { + decoded = &scratch; + if (decoded->which_variant == meshtastic_Telemetry_device_metrics_tag) { + if (decoded->variant.device_metrics.has_battery_level) { + msgPayload["battery_level"] = (int)decoded->variant.device_metrics.battery_level; + } + msgPayload["voltage"] = decoded->variant.device_metrics.voltage; + msgPayload["channel_utilization"] = decoded->variant.device_metrics.channel_utilization; + msgPayload["air_util_tx"] = decoded->variant.device_metrics.air_util_tx; + msgPayload["uptime_seconds"] = (Json::UInt)decoded->variant.device_metrics.uptime_seconds; + } else if (decoded->which_variant == meshtastic_Telemetry_environment_metrics_tag) { + if (decoded->variant.environment_metrics.has_temperature) { + msgPayload["temperature"] = decoded->variant.environment_metrics.temperature; + } + if (decoded->variant.environment_metrics.has_relative_humidity) { + msgPayload["relative_humidity"] = decoded->variant.environment_metrics.relative_humidity; + } + if (decoded->variant.environment_metrics.has_barometric_pressure) { + msgPayload["barometric_pressure"] = decoded->variant.environment_metrics.barometric_pressure; + } + if (decoded->variant.environment_metrics.has_gas_resistance) { + msgPayload["gas_resistance"] = decoded->variant.environment_metrics.gas_resistance; + } + if (decoded->variant.environment_metrics.has_voltage) { + msgPayload["voltage"] = decoded->variant.environment_metrics.voltage; + } + if (decoded->variant.environment_metrics.has_current) { + msgPayload["current"] = decoded->variant.environment_metrics.current; + } + if (decoded->variant.environment_metrics.has_lux) { + msgPayload["lux"] = decoded->variant.environment_metrics.lux; + } + if (decoded->variant.environment_metrics.has_white_lux) { + msgPayload["white_lux"] = decoded->variant.environment_metrics.white_lux; + } + if (decoded->variant.environment_metrics.has_iaq) { + msgPayload["iaq"] = (Json::UInt)decoded->variant.environment_metrics.iaq; + } + if (decoded->variant.environment_metrics.has_distance) { + msgPayload["distance"] = decoded->variant.environment_metrics.distance; + } + if (decoded->variant.environment_metrics.has_wind_speed) { + msgPayload["wind_speed"] = decoded->variant.environment_metrics.wind_speed; + } + if (decoded->variant.environment_metrics.has_wind_direction) { + msgPayload["wind_direction"] = (Json::UInt)decoded->variant.environment_metrics.wind_direction; + } + if (decoded->variant.environment_metrics.has_wind_gust) { + msgPayload["wind_gust"] = decoded->variant.environment_metrics.wind_gust; + } + if (decoded->variant.environment_metrics.has_wind_lull) { + msgPayload["wind_lull"] = decoded->variant.environment_metrics.wind_lull; + } + if (decoded->variant.environment_metrics.has_radiation) { + msgPayload["radiation"] = decoded->variant.environment_metrics.radiation; + } + if (decoded->variant.environment_metrics.has_ir_lux) { + msgPayload["ir_lux"] = decoded->variant.environment_metrics.ir_lux; + } + if (decoded->variant.environment_metrics.has_uv_lux) { + msgPayload["uv_lux"] = decoded->variant.environment_metrics.uv_lux; + } + if (decoded->variant.environment_metrics.has_weight) { + msgPayload["weight"] = decoded->variant.environment_metrics.weight; + } + if (decoded->variant.environment_metrics.has_rainfall_1h) { + msgPayload["rainfall_1h"] = decoded->variant.environment_metrics.rainfall_1h; + } + if (decoded->variant.environment_metrics.has_rainfall_24h) { + msgPayload["rainfall_24h"] = decoded->variant.environment_metrics.rainfall_24h; + } + if (decoded->variant.environment_metrics.has_soil_moisture) { + msgPayload["soil_moisture"] = (Json::UInt)decoded->variant.environment_metrics.soil_moisture; + } + if (decoded->variant.environment_metrics.has_soil_temperature) { + msgPayload["soil_temperature"] = decoded->variant.environment_metrics.soil_temperature; + } + } else if (decoded->which_variant == meshtastic_Telemetry_air_quality_metrics_tag) { + if (decoded->variant.air_quality_metrics.has_pm10_standard) { + msgPayload["pm10"] = (Json::UInt)decoded->variant.air_quality_metrics.pm10_standard; + } + if (decoded->variant.air_quality_metrics.has_pm25_standard) { + msgPayload["pm25"] = (Json::UInt)decoded->variant.air_quality_metrics.pm25_standard; + } + if (decoded->variant.air_quality_metrics.has_pm100_standard) { + msgPayload["pm100"] = (Json::UInt)decoded->variant.air_quality_metrics.pm100_standard; + } + if (decoded->variant.air_quality_metrics.has_co2) { + msgPayload["co2"] = (Json::UInt)decoded->variant.air_quality_metrics.co2; + } + if (decoded->variant.air_quality_metrics.has_co2_temperature) { + msgPayload["co2_temperature"] = decoded->variant.air_quality_metrics.co2_temperature; + } + if (decoded->variant.air_quality_metrics.has_co2_humidity) { + msgPayload["co2_humidity"] = decoded->variant.air_quality_metrics.co2_humidity; + } + if (decoded->variant.air_quality_metrics.has_form_formaldehyde) { + msgPayload["form_formaldehyde"] = decoded->variant.air_quality_metrics.form_formaldehyde; + } + if (decoded->variant.air_quality_metrics.has_form_temperature) { + msgPayload["form_temperature"] = decoded->variant.air_quality_metrics.form_temperature; + } + if (decoded->variant.air_quality_metrics.has_form_humidity) { + msgPayload["form_humidity"] = decoded->variant.air_quality_metrics.form_humidity; + } + } else if (decoded->which_variant == meshtastic_Telemetry_power_metrics_tag) { + if (decoded->variant.power_metrics.has_ch1_voltage) { + msgPayload["voltage_ch1"] = decoded->variant.power_metrics.ch1_voltage; + } + if (decoded->variant.power_metrics.has_ch1_current) { + msgPayload["current_ch1"] = decoded->variant.power_metrics.ch1_current; + } + if (decoded->variant.power_metrics.has_ch2_voltage) { + msgPayload["voltage_ch2"] = decoded->variant.power_metrics.ch2_voltage; + } + if (decoded->variant.power_metrics.has_ch2_current) { + msgPayload["current_ch2"] = decoded->variant.power_metrics.ch2_current; + } + if (decoded->variant.power_metrics.has_ch3_voltage) { + msgPayload["voltage_ch3"] = decoded->variant.power_metrics.ch3_voltage; + } + if (decoded->variant.power_metrics.has_ch3_current) { + msgPayload["current_ch3"] = decoded->variant.power_metrics.ch3_current; + } + } + jsonObj["payload"] = msgPayload; + } else if (shouldLog) { + LOG_ERROR(errStr, msgType.c_str()); + } + break; + } + case meshtastic_PortNum_NODEINFO_APP: { + msgType = "nodeinfo"; + meshtastic_User scratch; + meshtastic_User *decoded = NULL; + memset(&scratch, 0, sizeof(scratch)); + if (pb_decode_from_bytes(mp->decoded.payload.bytes, mp->decoded.payload.size, &meshtastic_User_msg, &scratch)) { + decoded = &scratch; + msgPayload["id"] = decoded->id; + msgPayload["longname"] = decoded->long_name; + msgPayload["shortname"] = decoded->short_name; + msgPayload["hardware"] = (int)decoded->hw_model; + msgPayload["role"] = (int)decoded->role; + jsonObj["payload"] = msgPayload; + } else if (shouldLog) { + LOG_ERROR(errStr, msgType.c_str()); + } + break; + } + case meshtastic_PortNum_POSITION_APP: { + msgType = "position"; + meshtastic_Position scratch; + meshtastic_Position *decoded = NULL; + memset(&scratch, 0, sizeof(scratch)); + if (pb_decode_from_bytes(mp->decoded.payload.bytes, mp->decoded.payload.size, &meshtastic_Position_msg, &scratch)) { + decoded = &scratch; + if ((int)decoded->time) { + msgPayload["time"] = (Json::UInt)decoded->time; + } + if ((int)decoded->timestamp) { + msgPayload["timestamp"] = (Json::UInt)decoded->timestamp; + } + msgPayload["latitude_i"] = (int)decoded->latitude_i; + msgPayload["longitude_i"] = (int)decoded->longitude_i; + if ((int)decoded->altitude) { + msgPayload["altitude"] = (int)decoded->altitude; + } + if ((int)decoded->ground_speed) { + msgPayload["ground_speed"] = (Json::UInt)decoded->ground_speed; + } + if (int(decoded->ground_track)) { + msgPayload["ground_track"] = (Json::UInt)decoded->ground_track; + } + if (int(decoded->sats_in_view)) { + msgPayload["sats_in_view"] = (Json::UInt)decoded->sats_in_view; + } + if ((int)decoded->PDOP) { + msgPayload["PDOP"] = (int)decoded->PDOP; + } + if ((int)decoded->HDOP) { + msgPayload["HDOP"] = (int)decoded->HDOP; + } + if ((int)decoded->VDOP) { + msgPayload["VDOP"] = (int)decoded->VDOP; + } + if ((int)decoded->precision_bits) { + msgPayload["precision_bits"] = (int)decoded->precision_bits; + } + jsonObj["payload"] = msgPayload; + } else if (shouldLog) { + LOG_ERROR(errStr, msgType.c_str()); + } + break; + } + case meshtastic_PortNum_WAYPOINT_APP: { + msgType = "waypoint"; + meshtastic_Waypoint scratch; + meshtastic_Waypoint *decoded = NULL; + memset(&scratch, 0, sizeof(scratch)); + if (pb_decode_from_bytes(mp->decoded.payload.bytes, mp->decoded.payload.size, &meshtastic_Waypoint_msg, &scratch)) { + decoded = &scratch; + msgPayload["id"] = (Json::UInt)decoded->id; + msgPayload["name"] = decoded->name; + msgPayload["description"] = decoded->description; + msgPayload["expire"] = (Json::UInt)decoded->expire; + msgPayload["locked_to"] = (Json::UInt)decoded->locked_to; + msgPayload["latitude_i"] = (int)decoded->latitude_i; + msgPayload["longitude_i"] = (int)decoded->longitude_i; + jsonObj["payload"] = msgPayload; + } else if (shouldLog) { + LOG_ERROR(errStr, msgType.c_str()); + } + break; + } + case meshtastic_PortNum_NEIGHBORINFO_APP: { + msgType = "neighborinfo"; + meshtastic_NeighborInfo scratch; + meshtastic_NeighborInfo *decoded = NULL; + memset(&scratch, 0, sizeof(scratch)); + if (pb_decode_from_bytes(mp->decoded.payload.bytes, mp->decoded.payload.size, &meshtastic_NeighborInfo_msg, + &scratch)) { + decoded = &scratch; + msgPayload["node_id"] = (Json::UInt)decoded->node_id; + msgPayload["node_broadcast_interval_secs"] = (Json::UInt)decoded->node_broadcast_interval_secs; + msgPayload["last_sent_by_id"] = (Json::UInt)decoded->last_sent_by_id; + msgPayload["neighbors_count"] = (Json::UInt)decoded->neighbors_count; + Json::Value neighbors(Json::arrayValue); + for (uint8_t i = 0; i < decoded->neighbors_count; i++) { + Json::Value neighborObj(Json::objectValue); + neighborObj["node_id"] = (Json::UInt)decoded->neighbors[i].node_id; + neighborObj["snr"] = (int)decoded->neighbors[i].snr; + neighbors.append(neighborObj); + } + msgPayload["neighbors"] = neighbors; + jsonObj["payload"] = msgPayload; + } else if (shouldLog) { + LOG_ERROR(errStr, msgType.c_str()); + } + break; + } + case meshtastic_PortNum_TRACEROUTE_APP: { + if (mp->decoded.request_id) { + msgType = "traceroute"; + meshtastic_RouteDiscovery scratch; + meshtastic_RouteDiscovery *decoded = NULL; + memset(&scratch, 0, sizeof(scratch)); + if (pb_decode_from_bytes(mp->decoded.payload.bytes, mp->decoded.payload.size, &meshtastic_RouteDiscovery_msg, + &scratch)) { + decoded = &scratch; + Json::Value route(Json::arrayValue); + Json::Value routeBack(Json::arrayValue); + Json::Value snrTowards(Json::arrayValue); + Json::Value snrBack(Json::arrayValue); + + auto addToRoute = [](Json::Value *r, NodeNum num) { + char long_name[40] = "Unknown"; + meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(num); + bool name_known = node ? node->has_user : false; + if (name_known) + memcpy(long_name, node->user.long_name, sizeof(long_name)); + r->append(long_name); + }; + addToRoute(&route, mp->to); + for (uint8_t i = 0; i < decoded->route_count; i++) { + addToRoute(&route, decoded->route[i]); + } + addToRoute(&route, mp->from); + + addToRoute(&routeBack, mp->from); + for (uint8_t i = 0; i < decoded->route_back_count; i++) { + addToRoute(&routeBack, decoded->route_back[i]); + } + addToRoute(&routeBack, mp->to); + + for (uint8_t i = 0; i < decoded->snr_back_count; i++) { + snrBack.append((float)decoded->snr_back[i] / 4); + } + for (uint8_t i = 0; i < decoded->snr_towards_count; i++) { + snrTowards.append((float)decoded->snr_towards[i] / 4); + } + + msgPayload["route"] = route; + msgPayload["route_back"] = routeBack; + msgPayload["snr_back"] = snrBack; + msgPayload["snr_towards"] = snrTowards; + jsonObj["payload"] = msgPayload; + } else if (shouldLog) { + LOG_ERROR(errStr, msgType.c_str()); + } + } + break; + } + case meshtastic_PortNum_DETECTION_SENSOR_APP: { + msgType = "detection"; + char payloadStr[(mp->decoded.payload.size) + 1]; + memcpy(payloadStr, mp->decoded.payload.bytes, mp->decoded.payload.size); + payloadStr[mp->decoded.payload.size] = 0; + msgPayload["text"] = payloadStr; + jsonObj["payload"] = msgPayload; + break; + } +#ifdef ARCH_ESP32 + case meshtastic_PortNum_PAXCOUNTER_APP: { + msgType = "paxcounter"; + meshtastic_Paxcount scratch; + meshtastic_Paxcount *decoded = NULL; + memset(&scratch, 0, sizeof(scratch)); + if (pb_decode_from_bytes(mp->decoded.payload.bytes, mp->decoded.payload.size, &meshtastic_Paxcount_msg, &scratch)) { + decoded = &scratch; + msgPayload["wifi_count"] = (Json::UInt)decoded->wifi; + msgPayload["ble_count"] = (Json::UInt)decoded->ble; + msgPayload["uptime"] = (Json::UInt)decoded->uptime; + jsonObj["payload"] = msgPayload; + } else if (shouldLog) { + LOG_ERROR(errStr, msgType.c_str()); + } + break; + } +#endif + case meshtastic_PortNum_REMOTE_HARDWARE_APP: { + meshtastic_HardwareMessage scratch; + meshtastic_HardwareMessage *decoded = NULL; + memset(&scratch, 0, sizeof(scratch)); + if (pb_decode_from_bytes(mp->decoded.payload.bytes, mp->decoded.payload.size, &meshtastic_HardwareMessage_msg, + &scratch)) { + decoded = &scratch; + if (decoded->type == meshtastic_HardwareMessage_Type_GPIOS_CHANGED) { + msgType = "gpios_changed"; + msgPayload["gpio_value"] = (Json::UInt)decoded->gpio_value; + jsonObj["payload"] = msgPayload; + } else if (decoded->type == meshtastic_HardwareMessage_Type_READ_GPIOS_REPLY) { + msgType = "gpios_read_reply"; + msgPayload["gpio_value"] = (Json::UInt)decoded->gpio_value; + msgPayload["gpio_mask"] = (Json::UInt)decoded->gpio_mask; + jsonObj["payload"] = msgPayload; + } + } else if (shouldLog) { + LOG_ERROR(errStr, "RemoteHardware"); + } + break; + } + default: + break; + } + } else if (shouldLog) { + LOG_WARN("Couldn't convert encrypted payload of MeshPacket to JSON"); + } + + jsonObj["id"] = (Json::UInt)mp->id; + jsonObj["timestamp"] = (Json::UInt)mp->rx_time; + jsonObj["to"] = (Json::UInt)mp->to; + jsonObj["from"] = (Json::UInt)mp->from; + jsonObj["channel"] = (Json::UInt)mp->channel; + jsonObj["type"] = msgType; + jsonObj["sender"] = nodeDB->getNodeId(); + if (mp->rx_rssi != 0) + jsonObj["rssi"] = (int)mp->rx_rssi; + if (mp->rx_snr != 0) + jsonObj["snr"] = (float)mp->rx_snr; + const int8_t hopsAway = getHopsAway(*mp); + if (hopsAway >= 0) { + jsonObj["hops_away"] = (Json::UInt)(hopsAway); + jsonObj["hop_start"] = (Json::UInt)(mp->hop_start); + } + + std::string jsonStr = writeCompact(jsonObj); + + if (shouldLog) + LOG_INFO("serialized json message: %s", jsonStr.c_str()); + + return jsonStr; +} + +std::string MeshPacketSerializer::JsonSerializeEncrypted(const meshtastic_MeshPacket *mp) +{ + Json::Value jsonObj(Json::objectValue); + + jsonObj["id"] = (Json::UInt)mp->id; + jsonObj["time_ms"] = (double)millis(); + jsonObj["timestamp"] = (Json::UInt)mp->rx_time; + jsonObj["to"] = (Json::UInt)mp->to; + jsonObj["from"] = (Json::UInt)mp->from; + jsonObj["channel"] = (Json::UInt)mp->channel; + jsonObj["want_ack"] = mp->want_ack; + + if (mp->rx_rssi != 0) + jsonObj["rssi"] = (int)mp->rx_rssi; + if (mp->rx_snr != 0) + jsonObj["snr"] = (float)mp->rx_snr; + const int8_t hopsAway = getHopsAway(*mp); + if (hopsAway >= 0) { + jsonObj["hops_away"] = (Json::UInt)(hopsAway); + jsonObj["hop_start"] = (Json::UInt)(mp->hop_start); + } + jsonObj["size"] = (Json::UInt)mp->encrypted.size; + auto encryptedStr = bytesToHex(mp->encrypted.bytes, mp->encrypted.size); + jsonObj["bytes"] = encryptedStr; + + return writeCompact(jsonObj); +} +#endif diff --git a/src/serialization/MeshPacketSerializer.h b/src/serialization/MeshPacketSerializer.h new file mode 100644 index 000000000..03860ab35 --- /dev/null +++ b/src/serialization/MeshPacketSerializer.h @@ -0,0 +1,23 @@ +#include +#include + +static const char hexChars[16] = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'}; + +class MeshPacketSerializer +{ + public: + static std::string JsonSerialize(const meshtastic_MeshPacket *mp, bool shouldLog = true); + static std::string JsonSerializeEncrypted(const meshtastic_MeshPacket *mp); + + private: + static std::string bytesToHex(const uint8_t *bytes, int len) + { + std::string result = ""; + for (int i = 0; i < len; ++i) { + char const byte = bytes[i]; + result += hexChars[(byte & 0xF0) >> 4]; + result += hexChars[(byte & 0x0F) >> 0]; + } + return result; + } +}; \ No newline at end of file diff --git a/variants/native/portduino.ini b/variants/native/portduino.ini index 86b1fe60a..95796ecba 100644 --- a/variants/native/portduino.ini +++ b/variants/native/portduino.ini @@ -52,6 +52,7 @@ build_flags = -lbluetooth -lgpiod -lyaml-cpp + -ljsoncpp -li2c -luv -std=gnu17