From d700b4d4a0b02a73615576cb8218669fab196294 Mon Sep 17 00:00:00 2001 From: Max Watermolen Date: Fri, 9 Feb 2024 00:35:47 -0800 Subject: [PATCH 1/4] Adds Trunk-PlayerNG as a Plugin yo! --- CMakeLists.txt | 6 + README.md | 1 + docs/Plugins.md | 27 ++ lib/base64.h | 134 +++++++++ plugins/tpng_uploader/CMakeLists.txt | 20 ++ plugins/tpng_uploader/tpng_uploader.cc | 273 ++++++++++++++++++ .../call_concluder/call_concluder.h | 1 + 7 files changed, 462 insertions(+) create mode 100644 lib/base64.h create mode 100644 plugins/tpng_uploader/CMakeLists.txt create mode 100644 plugins/tpng_uploader/tpng_uploader.cc diff --git a/CMakeLists.txt b/CMakeLists.txt index a33d7c016..a12155528 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -337,6 +337,10 @@ install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/lib/json.hpp" # source file DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}/trunk-recorder" # target directory ) +install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/lib/base64.h" # source file + DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}/trunk-recorder" # target directory +) + add_subdirectory(plugins/openmhz_uploader) @@ -350,6 +354,8 @@ add_subdirectory(plugins/rdioscanner_uploader) add_subdirectory(plugins/simplestream) +add_subdirectory(plugins/tpng_uploader) + add_executable(trunk-recorder trunk-recorder/main.cc) # ${trunk_recorder_sources}) target_link_libraries(trunk-recorder git trunk_recorder_library gnuradio-op25_repeater ${CMAKE_DL_LIBS} ssl crypto ${CURL_LIBRARIES} ${Boost_LIBRARIES} ${GNURADIO_PMT_LIBRARIES} ${GNURADIO_RUNTIME_LIBRARIES} ${GNURADIO_FILTER_LIBRARIES} ${GNURADIO_DIGITAL_LIBRARIES} ${GNURADIO_ANALOG_LIBRARIES} ${GNURADIO_AUDIO_LIBRARIES} ${GNURADIO_UHD_LIBRARIES} ${UHD_LIBRARIES} ${GNURADIO_BLOCKS_LIBRARIES} ${GNURADIO_OSMOSDR_LIBRARIES} ) # gRPC::grpc++_reflection protobuf::libprotobuf) diff --git a/README.md b/README.md index 162dd7855..0b06ab8c1 100644 --- a/README.md +++ b/README.md @@ -68,6 +68,7 @@ RTL-SDR dongles; HackRF; Ettus USRP B200, B210, B205; BladeRF; Airspy; SDRplay By default, Trunk Recorder just dumps a lot of recorded files into a directory. Here are a couple of options to make it easier to browse through recordings and share them on the Internet. * [OpenMHz](https://github.com/robotastic/trunk-recorder/wiki/Uploading-to-OpenMHz): This is my free hosted platform for sharing recordings * [Trunk Player](https://github.com/ScanOC/trunk-player): A great Python based server, if you want to you want to run your own +* [Trunk-PlayerNG](https://github.com/Trunk-Player) an Enterprise Grade Trunking API with ACLs, notifications, User management, a filterable API, Mertics, and more... WebUI not guaranteed™ (yee ik grovl grovel). Bur since TP-NG is ACL based you dont have to worry about multiple configs. Its time to make radio analysis easy! * [Rdio Scanner](https://github.com/chuot/rdio-scanner): Provide a good looking, scanner style interface for listening to Trunk Recorder * Broadcastify Calls (API): see Radio Reference [forum thread](https://forums.radioreference.com/threads/405236/) and [wiki page](https://wiki.radioreference.com/index.php/Broadcastify-Calls-Trunk-Recorder) * [Broadcastify via Liquidsoap](https://github.com/robotastic/trunk-recorder/wiki/Streaming-online-to-Broadcastify-with-Liquid-Soap) diff --git a/docs/Plugins.md b/docs/Plugins.md index e2b8aaa07..eacbf4b27 100644 --- a/docs/Plugins.md +++ b/docs/Plugins.md @@ -186,6 +186,33 @@ The matching simplestream config to send audio from talkgroup 58918 to TCP port ``` +##### Trunk Player Next Gen Plugin + +**Name:** tpng_uploader +**Library:** tpng_uploader.so + +This plugin makes it easy to connect Trunk Recorder with [Trunk-PlayerNG](https://github.com/Trunk-Player) an Enterprise Grade Trunking API with ACLs, notifications, User management, a filterable API, Mertics, and more... WebUI not guaranteed™ (yee ik grovl grovel). Since [Trunk-PlayerNG](https://github.com/Trunk-Player) is ACL based you dont have to worry about multiple, the server will take what you are allowed to send it. **So you only need one!** Its time to make radio analysis easy! + + + +| Key | Required | Default Value | Type | Description | +| ------- | :------: | ------------- | ------ | ------------------------------------------------------------ | +| url | ✓ | | string | The base API URL for your TPNG. You need this duh 🙄| +| token | ✓ | | array | This is your recorder token, since its ACL based it can be used for any systems Trunk-Recorder can hear; TLDR ya only need one 🔥 \*\*mic drop\*\* --- END N3RD FLEX --- | + + + +##### Example Plugin Object: + +```json + { // Yeah its hard IK + "name": "tpng_uploader", + "library": "tpng_uploader.so", + "url": "http://trunkplayer.io/api/v1", + "token": "96eae4bc-8e31-46a4-9756-66eeb2af3440" + } +``` + ## Community Plugins * [MQTT Status](https://github.com/robotastic/trunk-recorder-mqtt-status): Publishes the current status of a Trunk Recorder instance over MQTT diff --git a/lib/base64.h b/lib/base64.h new file mode 100644 index 000000000..e48d969d7 --- /dev/null +++ b/lib/base64.h @@ -0,0 +1,134 @@ +#ifndef _MACARON_BASE64_H_ +#define _MACARON_BASE64_H_ + +/** + * The MIT License (MIT) + * Copyright (c) 2016-2024 tomykaira + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +#include +#include + +namespace macaron { + +class Base64 { +public: + static std::string Encode(const std::string &data) { + static constexpr char sEncodingTable[] = { + 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', + 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', + 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', + 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '+', '/'}; + + size_t in_len = data.size(); + size_t out_len = 4 * ((in_len + 2) / 3); + std::string ret(out_len, '\0'); + size_t i; + char *p = const_cast(ret.c_str()); + + for (i = 0; in_len > 2 && i < in_len - 2; i += 3) { + *p++ = sEncodingTable[(data[i] >> 2) & 0x3F]; + *p++ = sEncodingTable[((data[i] & 0x3) << 4) | + ((int)(data[i + 1] & 0xF0) >> 4)]; + *p++ = sEncodingTable[((data[i + 1] & 0xF) << 2) | + ((int)(data[i + 2] & 0xC0) >> 6)]; + *p++ = sEncodingTable[data[i + 2] & 0x3F]; + } + if (i < in_len) { + *p++ = sEncodingTable[(data[i] >> 2) & 0x3F]; + if (i == (in_len - 1)) { + *p++ = sEncodingTable[((data[i] & 0x3) << 4)]; + *p++ = '='; + } else { + *p++ = sEncodingTable[((data[i] & 0x3) << 4) | + ((int)(data[i + 1] & 0xF0) >> 4)]; + *p++ = sEncodingTable[((data[i + 1] & 0xF) << 2)]; + } + *p++ = '='; + } + + return ret; + } + + static std::string Decode(const std::string &input, std::string &out) { + static constexpr unsigned char kDecodingTable[] = { + 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, + 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, + 64, 64, 64, 64, 64, 64, 64, 62, 64, 64, 64, 63, 52, 53, 54, 55, 56, 57, + 58, 59, 60, 61, 64, 64, 64, 64, 64, 64, 64, 0, 1, 2, 3, 4, 5, 6, + 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, + 25, 64, 64, 64, 64, 64, 64, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, + 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 64, 64, 64, + 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, + 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, + 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, + 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, + 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, + 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, + 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, + 64, 64, 64, 64}; + + size_t in_len = input.size(); + if (in_len % 4 != 0) + return "Input data size is not a multiple of 4"; + + size_t out_len = in_len / 4 * 3; + if (in_len >= 1 && input[in_len - 1] == '=') + out_len--; + if (in_len >= 2 && input[in_len - 2] == '=') + out_len--; + + out.resize(out_len); + + for (size_t i = 0, j = 0; i < in_len;) { + uint32_t a = input[i] == '=' + ? 0 & i++ + : kDecodingTable[static_cast(input[i++])]; + uint32_t b = input[i] == '=' + ? 0 & i++ + : kDecodingTable[static_cast(input[i++])]; + uint32_t c = input[i] == '=' + ? 0 & i++ + : kDecodingTable[static_cast(input[i++])]; + uint32_t d = input[i] == '=' + ? 0 & i++ + : kDecodingTable[static_cast(input[i++])]; + + uint32_t triple = + (a << 3 * 6) + (b << 2 * 6) + (c << 1 * 6) + (d << 0 * 6); + + if (j < out_len) + out[j++] = (triple >> 2 * 8) & 0xFF; + if (j < out_len) + out[j++] = (triple >> 1 * 8) & 0xFF; + if (j < out_len) + out[j++] = (triple >> 0 * 8) & 0xFF; + } + + return ""; + } +}; + +} // namespace macaron + +#endif /* _MACARON_BASE64_H_ */ \ No newline at end of file diff --git a/plugins/tpng_uploader/CMakeLists.txt b/plugins/tpng_uploader/CMakeLists.txt new file mode 100644 index 000000000..88a3310d8 --- /dev/null +++ b/plugins/tpng_uploader/CMakeLists.txt @@ -0,0 +1,20 @@ +add_library(tpng_uploader + MODULE + tpng_uploader.cc + ) + +target_link_libraries(tpng_uploader trunk_recorder_library ssl crypto ${CURL_LIBRARIES} ${Boost_LIBRARIES} ${GNURADIO_PMT_LIBRARIES} ${GNURADIO_RUNTIME_LIBRARIES} ${GNURADIO_FILTER_LIBRARIES} ${GNURADIO_DIGITAL_LIBRARIES} ${GNURADIO_ANALOG_LIBRARIES} ${GNURADIO_AUDIO_LIBRARIES} ${GNURADIO_UHD_LIBRARIES} ${UHD_LIBRARIES} ${GNURADIO_BLOCKS_LIBRARIES} ${GNURADIO_OSMOSDR_LIBRARIES} ${LIBOP25_REPEATER_LIBRARIES} gnuradio-op25_repeater ) # gRPC::grpc++_reflection protobuf::libprotobuf) + +if(NOT Gnuradio_VERSION VERSION_LESS "3.8") + + target_link_libraries(tpng_uploader + gnuradio::gnuradio-analog + gnuradio::gnuradio-blocks + gnuradio::gnuradio-digital + gnuradio::gnuradio-filter + gnuradio::gnuradio-pmt + ) + +endif() + +install(TARGETS tpng_uploader LIBRARY DESTINATION ${CMAKE_INSTALL_PREFIX}/lib/trunk-recorder) \ No newline at end of file diff --git a/plugins/tpng_uploader/tpng_uploader.cc b/plugins/tpng_uploader/tpng_uploader.cc new file mode 100644 index 000000000..b4ab19921 --- /dev/null +++ b/plugins/tpng_uploader/tpng_uploader.cc @@ -0,0 +1,273 @@ +#include +#include +#include +#include +#include + +#include + +#include "../../trunk-recorder/call_concluder/call_concluder.h" +#include "../../trunk-recorder/plugin_manager/plugin_api.h" +#include "../../trunk-recorder/plugin_manager/plugin_api.h" +#include "../../lib/json.hpp" +#include "../../lib/base64.h" +#include // for BOOST_DLL_ALIAS +#include +#include + +using json = nlohmann::json; + +struct TPNG_Uploader_Data { + std::string token; + std::string tpng_server; +}; + +class TPNG_Uploader : public Plugin_Api { + // float aggr_; + // my_plugin_aggregator() : aggr_(0) {} + TPNG_Uploader_Data data; + +public: + static size_t write_callback(void *contents, size_t size, size_t nmemb, void *userp) { + ((std::string *)userp)->append((char *)contents, size * nmemb); + return size * nmemb; + } + + std::stringstream create_call_json(Call_Data_t call_info) { + // Create the JSON -- Moderatly borrowed + std::stringstream json; + + json << "{\n"; + json << "\"freq\": " << std::fixed << std::setprecision(0) << call_info.freq << ",\n"; + json << "\"start_time\": " << call_info.start_time << ",\n"; + json << "\"stop_time\": " << call_info.stop_time << ",\n"; + json << "\"emergency\": " << call_info.emergency << ",\n"; + json << "\"priority\": " << call_info.priority << ",\n"; + json << "\"mode\": " << call_info.mode << ",\n"; + json << "\"duplex\": " << call_info.duplex << ",\n"; + json << "\"encrypted\": " << call_info.encrypted << ",\n"; + json << "\"call_length\": " << call_info.length << ",\n"; + json << "\"talkgroup\": " << call_info.talkgroup << ",\n"; + json << "\"talkgroup_tag\": \"" << call_info.talkgroup_alpha_tag << "\",\n"; + json << "\"talkgroup_description\": \"" << call_info.talkgroup_description << "\",\n"; + json << "\"talkgroup_group_tag\": \"" << call_info.talkgroup_tag << "\",\n"; + json << "\"talkgroup_group\": \"" << call_info.talkgroup_group << "\",\n"; + json << "\"audio_type\": \"" << call_info.audio_type << "\",\n"; + json << "\"short_name\": \"" << call_info.short_name << "\",\n"; + + if (call_info.patched_talkgroups.size() > 1) { + json << "\"patched_talkgroups\": ["; + bool first = true; + BOOST_FOREACH (auto &TGID, call_info.patched_talkgroups) { + if (!first) { + json << ","; + } + first = false; + json << (int)TGID; + } + json << "],\n"; + } + + json << "\"freqList\": [ "; + for (std::size_t i = 0; i < call_info.transmission_error_list.size(); i++) { + if (i != 0) { + json << ", "; + } + json << "{\"freq\": " << std::fixed << std::setprecision(0) << call_info.freq << ", \"time\": " << call_info.transmission_error_list[i].time << ", \"pos\": " << std::fixed << std::setprecision(2) << call_info.transmission_error_list[i].position << ", \"len\": " << call_info.transmission_error_list[i].total_len << ", \"error_count\": \"" << std::fixed << std::setprecision(0) << call_info.transmission_error_list[i].error_count << "\", \"spike_count\": \"" << call_info.transmission_error_list[i].spike_count << "\"}"; + } + json << " ],\n"; + json << "\"srcList\": [ "; + + for (std::size_t i = 0; i < call_info.transmission_source_list.size(); i++) { + if (i != 0) { + json << ", "; + } + json << "{\"src\": " << std::fixed << call_info.transmission_source_list[i].source << ", \"time\": " << call_info.transmission_source_list[i].time << ", \"pos\": " << std::fixed << std::setprecision(2) << call_info.transmission_source_list[i].position << ", \"emergency\": " << call_info.transmission_source_list[i].emergency << ", \"signal_system\": \"" << call_info.transmission_source_list[i].signal_system << "\", \"tag\": \"" << call_info.transmission_source_list[i].tag << "\"}"; + } + json << " ]\n"; + json << "}\n"; + + return json; + } + + int upload(Call_Data_t call_info) { + + std::string token = this->data.token; + if (token.size() == 0) { + // BOOST_LOG_TRIVIAL(error) << "[" << call_info.short_name << "]\tTG: " << talkgroup_display << "\t " << std::put_time(std::localtime(&start_time), "%c %Z") << "\tOpenMHz Upload failed, API Key not found in config for shortName"; + return 0; + } + + std::stringstream json_buffer = create_call_json(call_info); + nlohmann::json call_json = nlohmann::json::parse(json_buffer.str()); + + std::string base64_audio = macaron::Base64::Encode(call_info.converted); + + nlohmann::json payload = { + {"recorder", this->data.token}, + {"name", call_info.status_filename}, + {"json", call_json}, + {"audio_file", base64_audio} + }; + + CURLM *multi_handle; + CURL *curl; + curl = curl_easy_init(); + multi_handle = curl_multi_init(); + + std::string url = data.tpng_server + "/radio/transmission/create"; + + /* what URL that receives this POST */ + curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); + + std::string post_data = payload.dump(); + curl_easy_setopt(curl, CURLOPT_POSTFIELDS, post_data.c_str()); + + int still_running = 0; + curl_multi_add_handle(multi_handle, curl); + curl_multi_perform(multi_handle, &still_running); + + while (still_running) { + struct timeval timeout; + int rc; /* select() return code */ + CURLMcode mc; /* curl_multi_fdset() return code */ + + fd_set fdread; + fd_set fdwrite; + fd_set fdexcep; + int maxfd = -1; + + long curl_timeo = -1; + + FD_ZERO(&fdread); + FD_ZERO(&fdwrite); + FD_ZERO(&fdexcep); + + /* set a suitable timeout to play around with */ + timeout.tv_sec = 1; + timeout.tv_usec = 0; + + curl_multi_timeout(multi_handle, &curl_timeo); + if (curl_timeo >= 0) { + timeout.tv_sec = curl_timeo / 1000; + if (timeout.tv_sec > 1) + timeout.tv_sec = 1; + else + timeout.tv_usec = (curl_timeo % 1000) * 1000; + } + + /* get file descriptors from the transfers */ + mc = curl_multi_fdset(multi_handle, &fdread, &fdwrite, &fdexcep, &maxfd); + + if (mc != CURLM_OK) { + fprintf(stderr, "curl_multi_fdset() failed, code %d.\n", mc); + break; + } + + /* On success the value of maxfd is guaranteed to be >= -1. We call + select(maxfd + 1, ...); specially in case of (maxfd == -1) there are + no fds ready yet so we call select(0, ...) --or Sleep() on Windows-- + to sleep 100ms, which is the minimum suggested value in the + curl_multi_fdset() doc. */ + + if (maxfd == -1) { + /* Portable sleep for platforms other than Windows. */ + struct timeval wait = {0, 100 * 1000}; /* 100ms */ + rc = select(0, NULL, NULL, NULL, &wait); + } else { + /* Note that on some platforms 'timeout' may be modified by select(). + If you need access to the original value save a copy beforehand. */ + rc = select(maxfd + 1, &fdread, &fdwrite, &fdexcep, &timeout); + } + + switch (rc) { + case -1: + /* select error */ + break; + case 0: + default: + /* timeout or readable/writable sockets */ + curl_multi_perform(multi_handle, &still_running); + break; + } + } + + long response_code; + curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &response_code); + + CURLMcode res = curl_multi_cleanup(multi_handle); + + /* always cleanup */ + curl_easy_cleanup(curl); + + if (res == CURLM_OK && response_code == 200) { + struct stat file_info; + stat(call_info.converted, &file_info); + + BOOST_LOG_TRIVIAL(info) << "[" << call_info.short_name << "]\t\033[0;34m" << call_info.call_num << "C\033[0m\tTG: " << call_info.talkgroup_display << "\tFreq: " << format_freq(call_info.freq) << "\tOpenMHz Upload Success - file size: " << file_info.st_size; + ; + return 0; + } + return 1; + } + + int call_end(Call_Data_t call_info) { + return upload(call_info); + } + + int parse_config(json config_data) { + + // Tests to see if the uploadServer value exists in the config file + bool upload_server_exists = config_data.contains("url"); + if (!upload_server_exists) { + return 1; + } + + this->data.tpng_server = config_data.value("url", ""); + this->data.token = config_data.value("token", ""); + BOOST_LOG_TRIVIAL(info) << "Trunk-PlayerNG Server: " << this->data.tpng_server; + + // from: http://www.zedwood.com/article/cpp-boost-url-regex + boost::regex ex("(http|https)://([^/ :]+):?([^/ ]*)(/?[^ #?]*)\\x3f?([^ #]*)#?([^ ]*)"); + boost::cmatch what; + + if (!regex_match(this->data.tpng_server.c_str(), what, ex)) { + BOOST_LOG_TRIVIAL(error) << "Unable to parse Server URL\n"; + return 1; + } + + if (this->data.token == "") { + BOOST_LOG_TRIVIAL(error) << "Trunk-PlayerNG Server set, but no token is setted\n"; + return 1; + } + + return 0; + } + + /* + int init(Config *config, std::vector sources, std::vector systems) { return 0; } + int start() { return 0; } + int stop() { return 0; } + int poll_one() { return 0; } + int signal(long unitId, const char *signaling_type, gr::blocks::SignalType sig_type, Call *call, System *system, Recorder *recorder) { return 0; } + int audio_stream(Recorder *recorder, float *samples, int sampleCount) { return 0; } + int call_start(Call *call) { return 0; } + int calls_active(std::vector calls) { return 0; } + int setup_recorder(Recorder *recorder) { return 0; } + int setup_system(System *system) { return 0; } + int setup_systems(std::vector systems) { return 0; } + int setup_sources(std::vector sources) { return 0; } + int setup_config(std::vector sources, std::vector systems) { return 0; } + int system_rates(std::vector systems, float timeDiff) { return 0; } +*/ + // Factory method + static boost::shared_ptr create() { + return boost::shared_ptr( + new TPNG_Uploader()); + } +}; + +BOOST_DLL_ALIAS( + TPNG_Uploader::create, // <-- this function is exported with... + create_plugin // <-- ...this alias name +) diff --git a/trunk-recorder/call_concluder/call_concluder.h b/trunk-recorder/call_concluder/call_concluder.h index 7831ff4d2..01b43beb3 100644 --- a/trunk-recorder/call_concluder/call_concluder.h +++ b/trunk-recorder/call_concluder/call_concluder.h @@ -16,6 +16,7 @@ #include "../systems/system_impl.h" Call_Data_t upload_call_worker(Call_Data_t call_info); +int create_call_json(Call_Data_t call_info) ; class Call_Concluder { static const int MAX_RETRY = 2; From 34e661b83f4ce059cb9b37b2f1d9801265b71ca6 Mon Sep 17 00:00:00 2001 From: Max Watermolen Date: Fri, 9 Feb 2024 14:13:50 -0800 Subject: [PATCH 2/4] Adds TrunkPlayerNG Yo --- docs/CONFIGURE.md | 27 +++ docs/Playback.md | 1 + docs/Plugins.md | 8 +- plugins/tpng_uploader/tpng_uploader.cc | 233 +++++++++++++------------ trunk-recorder/config.cc | 1 + 5 files changed, 151 insertions(+), 119 deletions(-) diff --git a/docs/CONFIGURE.md b/docs/CONFIGURE.md index af1e22f0c..a7fb54014 100644 --- a/docs/CONFIGURE.md +++ b/docs/CONFIGURE.md @@ -445,6 +445,33 @@ The matching simplestream config to send audio from talkgroup 58918 to TCP port } ``` +##### Trunk Player Next Gen Plugin + +**Name:** tpng_uploader +**Library:** libtpng_uploader.so + +This plugin makes it easy to connect Trunk Recorder with [Trunk-PlayerNG](https://github.com/Trunk-Player) an Enterprise Grade Trunking API with ACLs, notifications, User management, a filterable API, Mertics, and more... WebUI not guaranteed™ (yee ik grovl grovel). Since [Trunk-PlayerNG](https://github.com/Trunk-Player) is ACL based you dont have to worry about multiple, the server will take what you are allowed to send it. **So you only need one!** Its time to make radio analysis easy! + + + +| Key | Required | Default Value | Type | Description | +| ------- | :------: | ------------- | ------ | ------------------------------------------------------------ | +| url | ✓ | | string | The base API URL for your TPNG. You need this duh 🙄| +| token | ✓ | | array | This is your recorder token, since its ACL based it can be used for any systems Trunk-Recorder can hear; TLDR ya only need one 🔥 \*\*mic drop\*\* --- END N3RD FLEX --- | + +###### Example Plugin Object: + +```json + { // Yeah its hard IK + "name": "tpng_uploader", + "library": "libtpng_uploader.so", + "url": "http://trunkplayer.io/api/v1", + "token": "96eae4bc-8e31-46a4-9756-66eeb2af3440" + } +``` + + + ## talkgroupsFile This file provides info on the different talkgroups in a trunking system. A lot of this info can be found on the [Radio Reference](http://www.radioreference.com/) website. You need to be a Radio Reference member to download the table for your system preformatted as a CSV file. You can also try clicking on the "List All in one table" link, selecting everything in the table and copying it into a spreadsheet program, and then exporting or saving as a CSV file. diff --git a/docs/Playback.md b/docs/Playback.md index fac04bdf8..5c213486d 100644 --- a/docs/Playback.md +++ b/docs/Playback.md @@ -7,6 +7,7 @@ sidebar_position: 5 By default, Trunk Recorder just dumps a lot of recorded files into a directory. Here are a couple of options to make it easier to browse through recordings and share them on the Internet. * [OpenMHz](https://github.com/robotastic/trunk-recorder/wiki/Uploading-to-OpenMHz): This is my free hosted platform for sharing recordings * [Trunk Player](https://github.com/ScanOC/trunk-player): A great Python based server, if you want to you want to run your own +* [Trunk-PlayerNG](https://github.com/Trunk-Player) an Enterprise Grade Trunking API with ACLs, notifications, User management, a filterable API, Mertics, and more... WebUI not guaranteed™ (yee ik grovl grovel). Bur since TP-NG is ACL based you dont have to worry about multiple configs. Its time to make radio analysis easy! * [Rdio Scanner](https://github.com/chuot/rdio-scanner): Provide a good looking, scanner style interface for listening to Trunk Recorder * Broadcastify Calls (API): see Radio Reference [forum thread](https://forums.radioreference.com/threads/405236/) and [wiki page](https://wiki.radioreference.com/index.php/Broadcastify-Calls-Trunk-Recorder) * [Broadcastify via Liquidsoap](https://github.com/robotastic/trunk-recorder/wiki/Streaming-online-to-Broadcastify-with-Liquid-Soap) diff --git a/docs/Plugins.md b/docs/Plugins.md index eacbf4b27..95d78ea6d 100644 --- a/docs/Plugins.md +++ b/docs/Plugins.md @@ -189,7 +189,7 @@ The matching simplestream config to send audio from talkgroup 58918 to TCP port ##### Trunk Player Next Gen Plugin **Name:** tpng_uploader -**Library:** tpng_uploader.so +**Library:** libtpng_uploader.so This plugin makes it easy to connect Trunk Recorder with [Trunk-PlayerNG](https://github.com/Trunk-Player) an Enterprise Grade Trunking API with ACLs, notifications, User management, a filterable API, Mertics, and more... WebUI not guaranteed™ (yee ik grovl grovel). Since [Trunk-PlayerNG](https://github.com/Trunk-Player) is ACL based you dont have to worry about multiple, the server will take what you are allowed to send it. **So you only need one!** Its time to make radio analysis easy! @@ -200,14 +200,12 @@ This plugin makes it easy to connect Trunk Recorder with [Trunk-PlayerNG](https: | url | ✓ | | string | The base API URL for your TPNG. You need this duh 🙄| | token | ✓ | | array | This is your recorder token, since its ACL based it can be used for any systems Trunk-Recorder can hear; TLDR ya only need one 🔥 \*\*mic drop\*\* --- END N3RD FLEX --- | - - -##### Example Plugin Object: +###### Example Plugin Object: ```json { // Yeah its hard IK "name": "tpng_uploader", - "library": "tpng_uploader.so", + "library": "libtpng_uploader.so", "url": "http://trunkplayer.io/api/v1", "token": "96eae4bc-8e31-46a4-9756-66eeb2af3440" } diff --git a/plugins/tpng_uploader/tpng_uploader.cc b/plugins/tpng_uploader/tpng_uploader.cc index b4ab19921..21db687bb 100644 --- a/plugins/tpng_uploader/tpng_uploader.cc +++ b/plugins/tpng_uploader/tpng_uploader.cc @@ -3,7 +3,6 @@ #include #include #include - #include #include "../../trunk-recorder/call_concluder/call_concluder.h" @@ -11,7 +10,7 @@ #include "../../trunk-recorder/plugin_manager/plugin_api.h" #include "../../lib/json.hpp" #include "../../lib/base64.h" -#include // for BOOST_DLL_ALIAS +#include #include #include @@ -22,9 +21,12 @@ struct TPNG_Uploader_Data { std::string tpng_server; }; +static size_t write_callback(void *contents, size_t size, size_t nmemb, void *userp) { + ((std::string *)userp)->append((char *)contents, size * nmemb); + return size * nmemb; +}; + class TPNG_Uploader : public Plugin_Api { - // float aggr_; - // my_plugin_aggregator() : aggr_(0) {} TPNG_Uploader_Data data; public: @@ -90,124 +92,146 @@ class TPNG_Uploader : public Plugin_Api { return json; } - int upload(Call_Data_t call_info) { + std::string base64_encode_m4a(const std::string& path) { + std::vector temp; - std::string token = this->data.token; - if (token.size() == 0) { - // BOOST_LOG_TRIVIAL(error) << "[" << call_info.short_name << "]\tTG: " << talkgroup_display << "\t " << std::put_time(std::localtime(&start_time), "%c %Z") << "\tOpenMHz Upload failed, API Key not found in config for shortName"; - return 0; + std::ifstream infile; + infile.open(path, std::ios::binary); // Open file in binary mode + if (infile.is_open()) { + while (!infile.eof()) { + char c = (char)infile.get(); + temp.push_back(c); + } + infile.close(); } + else return "File could not be opened"; + std::string ret(temp.begin(), temp.end() - 1); + ret = macaron::Base64::Encode(ret); + + return ret; +} + int upload(Call_Data_t call_info) { + + std::string token = this->data.token; std::stringstream json_buffer = create_call_json(call_info); nlohmann::json call_json = nlohmann::json::parse(json_buffer.str()); + std::string base64_audio = base64_encode_m4a(call_info.converted); - std::string base64_audio = macaron::Base64::Encode(call_info.converted); + if (token.size() == 0) { + return 0; + } nlohmann::json payload = { - {"recorder", this->data.token}, - {"name", call_info.status_filename}, + {"recorder", token}, + {"name", call_info.filename}, {"json", call_json}, {"audio_file", base64_audio} }; + std::string post_data = payload.dump(); CURLM *multi_handle; CURL *curl; curl = curl_easy_init(); multi_handle = curl_multi_init(); + std::string response_buffer; - std::string url = data.tpng_server + "/radio/transmission/create"; + if (curl && multi_handle) { + std::string url = data.tpng_server + "/radio/transmission/create"; - /* what URL that receives this POST */ - curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); + curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); + curl_easy_setopt(curl, CURLOPT_POSTFIELDS, post_data.c_str()); - std::string post_data = payload.dump(); - curl_easy_setopt(curl, CURLOPT_POSTFIELDS, post_data.c_str()); - - int still_running = 0; - curl_multi_add_handle(multi_handle, curl); - curl_multi_perform(multi_handle, &still_running); - - while (still_running) { - struct timeval timeout; - int rc; /* select() return code */ - CURLMcode mc; /* curl_multi_fdset() return code */ - - fd_set fdread; - fd_set fdwrite; - fd_set fdexcep; - int maxfd = -1; - - long curl_timeo = -1; - - FD_ZERO(&fdread); - FD_ZERO(&fdwrite); - FD_ZERO(&fdexcep); - - /* set a suitable timeout to play around with */ - timeout.tv_sec = 1; - timeout.tv_usec = 0; - - curl_multi_timeout(multi_handle, &curl_timeo); - if (curl_timeo >= 0) { - timeout.tv_sec = curl_timeo / 1000; - if (timeout.tv_sec > 1) - timeout.tv_sec = 1; - else - timeout.tv_usec = (curl_timeo % 1000) * 1000; - } + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response_buffer); - /* get file descriptors from the transfers */ - mc = curl_multi_fdset(multi_handle, &fdread, &fdwrite, &fdexcep, &maxfd); + int still_running = 0; + curl_multi_add_handle(multi_handle, curl); + curl_multi_perform(multi_handle, &still_running); - if (mc != CURLM_OK) { - fprintf(stderr, "curl_multi_fdset() failed, code %d.\n", mc); - break; - } + while (still_running) { + struct timeval timeout; + int rc; /* select() return code */ + CURLMcode mc; /* curl_multi_fdset() return code */ - /* On success the value of maxfd is guaranteed to be >= -1. We call - select(maxfd + 1, ...); specially in case of (maxfd == -1) there are - no fds ready yet so we call select(0, ...) --or Sleep() on Windows-- - to sleep 100ms, which is the minimum suggested value in the - curl_multi_fdset() doc. */ - - if (maxfd == -1) { - /* Portable sleep for platforms other than Windows. */ - struct timeval wait = {0, 100 * 1000}; /* 100ms */ - rc = select(0, NULL, NULL, NULL, &wait); - } else { - /* Note that on some platforms 'timeout' may be modified by select(). - If you need access to the original value save a copy beforehand. */ - rc = select(maxfd + 1, &fdread, &fdwrite, &fdexcep, &timeout); - } + fd_set fdread; + fd_set fdwrite; + fd_set fdexcep; + int maxfd = -1; + + long curl_timeo = -1; + + FD_ZERO(&fdread); + FD_ZERO(&fdwrite); + FD_ZERO(&fdexcep); + + /* set a suitable timeout to play around with */ + timeout.tv_sec = 1; + timeout.tv_usec = 0; - switch (rc) { - case -1: - /* select error */ - break; - case 0: - default: - /* timeout or readable/writable sockets */ - curl_multi_perform(multi_handle, &still_running); - break; + curl_multi_timeout(multi_handle, &curl_timeo); + if (curl_timeo >= 0) { + timeout.tv_sec = curl_timeo / 1000; + if (timeout.tv_sec > 1) + timeout.tv_sec = 1; + else + timeout.tv_usec = (curl_timeo % 1000) * 1000; + } + + /* get file descriptors from the transfers */ + mc = curl_multi_fdset(multi_handle, &fdread, &fdwrite, &fdexcep, &maxfd); + + if (mc != CURLM_OK) { + fprintf(stderr, "curl_multi_fdset() failed, code %d.\n", mc); + break; + } + + /* On success the value of maxfd is guaranteed to be >= -1. We call + select(maxfd + 1, ...); specially in case of (maxfd == -1) there are + no fds ready yet so we call select(0, ...) --or Sleep() on Windows-- + to sleep 100ms, which is the minimum suggested value in the + curl_multi_fdset() doc. */ + + if (maxfd == -1) { + /* Portable sleep for platforms other than Windows. */ + struct timeval wait = {0, 100 * 1000}; /* 100ms */ + rc = select(0, NULL, NULL, NULL, &wait); + } else { + /* Note that on some platforms 'timeout' may be modified by select(). + If you need access to the original value save a copy beforehand. */ + rc = select(maxfd + 1, &fdread, &fdwrite, &fdexcep, &timeout); + } + + switch (rc) { + case -1: + /* select error */ + break; + case 0: + default: + /* timeout or readable/writable sockets */ + curl_multi_perform(multi_handle, &still_running); + break; + } } - } - long response_code; - curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &response_code); + long response_code; + curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &response_code); - CURLMcode res = curl_multi_cleanup(multi_handle); + CURLMcode res = curl_multi_cleanup(multi_handle); - /* always cleanup */ - curl_easy_cleanup(curl); + /* always cleanup */ + curl_easy_cleanup(curl); - if (res == CURLM_OK && response_code == 200) { - struct stat file_info; - stat(call_info.converted, &file_info); + if (res == CURLM_OK) { + struct stat file_info; + stat(call_info.converted, &file_info); - BOOST_LOG_TRIVIAL(info) << "[" << call_info.short_name << "]\t\033[0;34m" << call_info.call_num << "C\033[0m\tTG: " << call_info.talkgroup_display << "\tFreq: " << format_freq(call_info.freq) << "\tOpenMHz Upload Success - file size: " << file_info.st_size; - ; - return 0; + BOOST_LOG_TRIVIAL(info) << "[" << call_info.short_name << "]\t\033[0;34m" << call_info.call_num << "C\033[0m\tTG: " << call_info.talkgroup_display << "\tFreq: " << format_freq(call_info.freq) << "\tTrunk-PlayerNG Upload Success - file size: " << file_info.st_size; + ; + return 0; + } } + BOOST_LOG_TRIVIAL(error) << "[" << call_info.short_name << "]\t\033[0;34m" << call_info.call_num << "C\033[0m\tTG: " << call_info.talkgroup_display << "\tFreq: " << format_freq(call_info.freq) << "\tTrunk-PlayerNG Upload Error: " << response_buffer; return 1; } @@ -217,7 +241,7 @@ class TPNG_Uploader : public Plugin_Api { int parse_config(json config_data) { - // Tests to see if the uploadServer value exists in the config file + // Tests to see if the url value exists in the config file bool upload_server_exists = config_data.contains("url"); if (!upload_server_exists) { return 1; @@ -227,7 +251,6 @@ class TPNG_Uploader : public Plugin_Api { this->data.token = config_data.value("token", ""); BOOST_LOG_TRIVIAL(info) << "Trunk-PlayerNG Server: " << this->data.tpng_server; - // from: http://www.zedwood.com/article/cpp-boost-url-regex boost::regex ex("(http|https)://([^/ :]+):?([^/ ]*)(/?[^ #?]*)\\x3f?([^ #]*)#?([^ ]*)"); boost::cmatch what; @@ -240,27 +263,9 @@ class TPNG_Uploader : public Plugin_Api { BOOST_LOG_TRIVIAL(error) << "Trunk-PlayerNG Server set, but no token is setted\n"; return 1; } - return 0; } - /* - int init(Config *config, std::vector sources, std::vector systems) { return 0; } - int start() { return 0; } - int stop() { return 0; } - int poll_one() { return 0; } - int signal(long unitId, const char *signaling_type, gr::blocks::SignalType sig_type, Call *call, System *system, Recorder *recorder) { return 0; } - int audio_stream(Recorder *recorder, float *samples, int sampleCount) { return 0; } - int call_start(Call *call) { return 0; } - int calls_active(std::vector calls) { return 0; } - int setup_recorder(Recorder *recorder) { return 0; } - int setup_system(System *system) { return 0; } - int setup_systems(std::vector systems) { return 0; } - int setup_sources(std::vector sources) { return 0; } - int setup_config(std::vector sources, std::vector systems) { return 0; } - int system_rates(std::vector systems, float timeDiff) { return 0; } -*/ - // Factory method static boost::shared_ptr create() { return boost::shared_ptr( new TPNG_Uploader()); @@ -268,6 +273,6 @@ class TPNG_Uploader : public Plugin_Api { }; BOOST_DLL_ALIAS( - TPNG_Uploader::create, // <-- this function is exported with... - create_plugin // <-- ...this alias name + TPNG_Uploader::create, + create_plugin ) diff --git a/trunk-recorder/config.cc b/trunk-recorder/config.cc index df5139465..b9e054a89 100644 --- a/trunk-recorder/config.cc +++ b/trunk-recorder/config.cc @@ -605,6 +605,7 @@ bool load_config(string config_file, Config &config, gr::top_block_sptr &tb, std BOOST_LOG_TRIVIAL(info) << "\n\n-------------------------------------\nPLUGINS\n-------------------------------------\n"; add_internal_plugin("openmhz_uploader", "libopenmhz_uploader.so", data); add_internal_plugin("broadcastify_uploader", "libbroadcastify_uploader.so", data); + add_internal_plugin("tpng_uploader", "libtpng_uploader.so", data); add_internal_plugin("unit_script", "libunit_script.so", data); add_internal_plugin("stat_socket", "libstat_socket.so", data); initialize_plugins(data, &config, sources, systems); From 2e32d30ea9217ba47c4a541e05bfeca9e4b84982 Mon Sep 17 00:00:00 2001 From: Max Watermolen Date: Sat, 10 Feb 2024 17:41:03 -0800 Subject: [PATCH 3/4] A few changes --- Dockerfile | 2 +- plugins/tpng_uploader/tpng_uploader.cc | 63 +---------- .../call_concluder/call_concluder.cc | 107 ++++++++++-------- .../call_concluder/call_concluder.h | 2 +- 4 files changed, 67 insertions(+), 107 deletions(-) diff --git a/Dockerfile b/Dockerfile index 244d00cb7..2b8c8d580 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM ubuntu:22.04 AS base +FROM --platform=amd64 ubuntu:22.04 AS base # Install docker for passing the socket to allow for intercontainer exec RUN apt-get update && \ diff --git a/plugins/tpng_uploader/tpng_uploader.cc b/plugins/tpng_uploader/tpng_uploader.cc index 21db687bb..8168465e3 100644 --- a/plugins/tpng_uploader/tpng_uploader.cc +++ b/plugins/tpng_uploader/tpng_uploader.cc @@ -35,63 +35,6 @@ class TPNG_Uploader : public Plugin_Api { return size * nmemb; } - std::stringstream create_call_json(Call_Data_t call_info) { - // Create the JSON -- Moderatly borrowed - std::stringstream json; - - json << "{\n"; - json << "\"freq\": " << std::fixed << std::setprecision(0) << call_info.freq << ",\n"; - json << "\"start_time\": " << call_info.start_time << ",\n"; - json << "\"stop_time\": " << call_info.stop_time << ",\n"; - json << "\"emergency\": " << call_info.emergency << ",\n"; - json << "\"priority\": " << call_info.priority << ",\n"; - json << "\"mode\": " << call_info.mode << ",\n"; - json << "\"duplex\": " << call_info.duplex << ",\n"; - json << "\"encrypted\": " << call_info.encrypted << ",\n"; - json << "\"call_length\": " << call_info.length << ",\n"; - json << "\"talkgroup\": " << call_info.talkgroup << ",\n"; - json << "\"talkgroup_tag\": \"" << call_info.talkgroup_alpha_tag << "\",\n"; - json << "\"talkgroup_description\": \"" << call_info.talkgroup_description << "\",\n"; - json << "\"talkgroup_group_tag\": \"" << call_info.talkgroup_tag << "\",\n"; - json << "\"talkgroup_group\": \"" << call_info.talkgroup_group << "\",\n"; - json << "\"audio_type\": \"" << call_info.audio_type << "\",\n"; - json << "\"short_name\": \"" << call_info.short_name << "\",\n"; - - if (call_info.patched_talkgroups.size() > 1) { - json << "\"patched_talkgroups\": ["; - bool first = true; - BOOST_FOREACH (auto &TGID, call_info.patched_talkgroups) { - if (!first) { - json << ","; - } - first = false; - json << (int)TGID; - } - json << "],\n"; - } - - json << "\"freqList\": [ "; - for (std::size_t i = 0; i < call_info.transmission_error_list.size(); i++) { - if (i != 0) { - json << ", "; - } - json << "{\"freq\": " << std::fixed << std::setprecision(0) << call_info.freq << ", \"time\": " << call_info.transmission_error_list[i].time << ", \"pos\": " << std::fixed << std::setprecision(2) << call_info.transmission_error_list[i].position << ", \"len\": " << call_info.transmission_error_list[i].total_len << ", \"error_count\": \"" << std::fixed << std::setprecision(0) << call_info.transmission_error_list[i].error_count << "\", \"spike_count\": \"" << call_info.transmission_error_list[i].spike_count << "\"}"; - } - json << " ],\n"; - json << "\"srcList\": [ "; - - for (std::size_t i = 0; i < call_info.transmission_source_list.size(); i++) { - if (i != 0) { - json << ", "; - } - json << "{\"src\": " << std::fixed << call_info.transmission_source_list[i].source << ", \"time\": " << call_info.transmission_source_list[i].time << ", \"pos\": " << std::fixed << std::setprecision(2) << call_info.transmission_source_list[i].position << ", \"emergency\": " << call_info.transmission_source_list[i].emergency << ", \"signal_system\": \"" << call_info.transmission_source_list[i].signal_system << "\", \"tag\": \"" << call_info.transmission_source_list[i].tag << "\"}"; - } - json << " ]\n"; - json << "}\n"; - - return json; - } - std::string base64_encode_m4a(const std::string& path) { std::vector temp; @@ -114,7 +57,7 @@ class TPNG_Uploader : public Plugin_Api { int upload(Call_Data_t call_info) { std::string token = this->data.token; - std::stringstream json_buffer = create_call_json(call_info); + std::stringstream json_buffer = Call_Concluder::create_call_json_string(call_info); nlohmann::json call_json = nlohmann::json::parse(json_buffer.str()); std::string base64_audio = base64_encode_m4a(call_info.converted); @@ -229,6 +172,10 @@ class TPNG_Uploader : public Plugin_Api { BOOST_LOG_TRIVIAL(info) << "[" << call_info.short_name << "]\t\033[0;34m" << call_info.call_num << "C\033[0m\tTG: " << call_info.talkgroup_display << "\tFreq: " << format_freq(call_info.freq) << "\tTrunk-PlayerNG Upload Success - file size: " << file_info.st_size; ; return 0; + }else if (response_code == 401) { + BOOST_LOG_TRIVIAL(info) << "[" << call_info.short_name << "]\t\033[0;34m" << call_info.call_num << "C\033[0m\tTG: " << call_info.talkgroup_display << "\tFreq: " << format_freq(call_info.freq) << "\tTrunk-PlayerNG Upload Denyed - CHECK TOKEN OR ACLS" + ; + return 0; } } BOOST_LOG_TRIVIAL(error) << "[" << call_info.short_name << "]\t\033[0;34m" << call_info.call_num << "C\033[0m\tTG: " << call_info.talkgroup_display << "\tFreq: " << format_freq(call_info.freq) << "\tTrunk-PlayerNG Upload Error: " << response_buffer; diff --git a/trunk-recorder/call_concluder/call_concluder.cc b/trunk-recorder/call_concluder/call_concluder.cc index ea99fdb5e..b72bfe128 100644 --- a/trunk-recorder/call_concluder/call_concluder.cc +++ b/trunk-recorder/call_concluder/call_concluder.cc @@ -50,58 +50,14 @@ int convert_media(char *filename, char *converted) { return nchars; } + int create_call_json(Call_Data_t call_info) { // Create the JSON status file std::ofstream json_file(call_info.status_filename); + std::stringstream json_string = Call_Concluder::create_call_json_string(call_info); if (json_file.is_open()) { - json_file << "{\n"; - json_file << "\"freq\": " << std::fixed << std::setprecision(0) << call_info.freq << ",\n"; - json_file << "\"start_time\": " << call_info.start_time << ",\n"; - json_file << "\"stop_time\": " << call_info.stop_time << ",\n"; - json_file << "\"emergency\": " << call_info.emergency << ",\n"; - json_file << "\"priority\": " << call_info.priority << ",\n"; - json_file << "\"mode\": " << call_info.mode << ",\n"; - json_file << "\"duplex\": " << call_info.duplex << ",\n"; - json_file << "\"encrypted\": " << call_info.encrypted << ",\n"; - json_file << "\"call_length\": " << call_info.length << ",\n"; - json_file << "\"talkgroup\": " << call_info.talkgroup << ",\n"; - json_file << "\"talkgroup_tag\": \"" << call_info.talkgroup_alpha_tag << "\",\n"; - json_file << "\"talkgroup_description\": \"" << call_info.talkgroup_description << "\",\n"; - json_file << "\"talkgroup_group_tag\": \"" << call_info.talkgroup_tag << "\",\n"; - json_file << "\"talkgroup_group\": \"" << call_info.talkgroup_group << "\",\n"; - json_file << "\"audio_type\": \"" << call_info.audio_type << "\",\n"; - json_file << "\"short_name\": \"" << call_info.short_name << "\",\n"; - if (call_info.patched_talkgroups.size() > 1) { - json_file << "\"patched_talkgroups\": ["; - bool first = true; - BOOST_FOREACH (auto &TGID, call_info.patched_talkgroups) { - if (!first) { - json_file << ","; - } - first = false; - json_file << (int)TGID; - } - json_file << "],\n"; - } - json_file << "\"freqList\": [ "; - for (std::size_t i = 0; i < call_info.transmission_error_list.size(); i++) { - if (i != 0) { - json_file << ", "; - } - json_file << "{\"freq\": " << std::fixed << std::setprecision(0) << call_info.freq << ", \"time\": " << call_info.transmission_error_list[i].time << ", \"pos\": " << std::fixed << std::setprecision(2) << call_info.transmission_error_list[i].position << ", \"len\": " << call_info.transmission_error_list[i].total_len << ", \"error_count\": \"" << std::fixed << std::setprecision(0) << call_info.transmission_error_list[i].error_count << "\", \"spike_count\": \"" << call_info.transmission_error_list[i].spike_count << "\"}"; - } - json_file << " ],\n"; - json_file << "\"srcList\": [ "; - - for (std::size_t i = 0; i < call_info.transmission_source_list.size(); i++) { - if (i != 0) { - json_file << ", "; - } - json_file << "{\"src\": " << std::fixed << call_info.transmission_source_list[i].source << ", \"time\": " << call_info.transmission_source_list[i].time << ", \"pos\": " << std::fixed << std::setprecision(2) << call_info.transmission_source_list[i].position << ", \"emergency\": " << call_info.transmission_source_list[i].emergency << ", \"signal_system\": \"" << call_info.transmission_source_list[i].signal_system << "\", \"tag\": \"" << call_info.transmission_source_list[i].tag << "\"}"; - } - json_file << " ]\n"; - json_file << "}\n"; + json_file << json_string.rdbuf(); json_file.close(); return 0; } else { @@ -250,6 +206,63 @@ Call_Data_t upload_call_worker(Call_Data_t call_info) { } +std::stringstream Call_Concluder::create_call_json_string(Call_Data_t call_info) { + // Create the JSON + std::stringstream json; + + json << "{\n"; + json << "\"freq\": " << std::fixed << std::setprecision(0) << call_info.freq << ",\n"; + json << "\"start_time\": " << call_info.start_time << ",\n"; + json << "\"stop_time\": " << call_info.stop_time << ",\n"; + json << "\"emergency\": " << call_info.emergency << ",\n"; + json << "\"priority\": " << call_info.priority << ",\n"; + json << "\"mode\": " << call_info.mode << ",\n"; + json << "\"duplex\": " << call_info.duplex << ",\n"; + json << "\"encrypted\": " << call_info.encrypted << ",\n"; + json << "\"call_length\": " << call_info.length << ",\n"; + json << "\"talkgroup\": " << call_info.talkgroup << ",\n"; + json << "\"talkgroup_tag\": \"" << call_info.talkgroup_alpha_tag << "\",\n"; + json << "\"talkgroup_description\": \"" << call_info.talkgroup_description << "\",\n"; + json << "\"talkgroup_group_tag\": \"" << call_info.talkgroup_tag << "\",\n"; + json << "\"talkgroup_group\": \"" << call_info.talkgroup_group << "\",\n"; + json << "\"audio_type\": \"" << call_info.audio_type << "\",\n"; + json << "\"short_name\": \"" << call_info.short_name << "\",\n"; + + if (call_info.patched_talkgroups.size() > 1) { + json << "\"patched_talkgroups\": ["; + bool first = true; + BOOST_FOREACH (auto &TGID, call_info.patched_talkgroups) { + if (!first) { + json << ","; + } + first = false; + json << (int)TGID; + } + json << "],\n"; + } + + json << "\"freqList\": [ "; + for (std::size_t i = 0; i < call_info.transmission_error_list.size(); i++) { + if (i != 0) { + json << ", "; + } + json << "{\"freq\": " << std::fixed << std::setprecision(0) << call_info.freq << ", \"time\": " << call_info.transmission_error_list[i].time << ", \"pos\": " << std::fixed << std::setprecision(2) << call_info.transmission_error_list[i].position << ", \"len\": " << call_info.transmission_error_list[i].total_len << ", \"error_count\": \"" << std::fixed << std::setprecision(0) << call_info.transmission_error_list[i].error_count << "\", \"spike_count\": \"" << call_info.transmission_error_list[i].spike_count << "\"}"; + } + json << " ],\n"; + json << "\"srcList\": [ "; + + for (std::size_t i = 0; i < call_info.transmission_source_list.size(); i++) { + if (i != 0) { + json << ", "; + } + json << "{\"src\": " << std::fixed << call_info.transmission_source_list[i].source << ", \"time\": " << call_info.transmission_source_list[i].time << ", \"pos\": " << std::fixed << std::setprecision(2) << call_info.transmission_source_list[i].position << ", \"emergency\": " << call_info.transmission_source_list[i].emergency << ", \"signal_system\": \"" << call_info.transmission_source_list[i].signal_system << "\", \"tag\": \"" << call_info.transmission_source_list[i].tag << "\"}"; + } + json << " ]\n"; + json << "}\n"; + + return json; +} + // static int rec_counter=0; Call_Data_t Call_Concluder::create_base_filename(Call *call, Call_Data_t call_info) { char base_filename[255]; diff --git a/trunk-recorder/call_concluder/call_concluder.h b/trunk-recorder/call_concluder/call_concluder.h index 01b43beb3..ef77a6826 100644 --- a/trunk-recorder/call_concluder/call_concluder.h +++ b/trunk-recorder/call_concluder/call_concluder.h @@ -16,7 +16,6 @@ #include "../systems/system_impl.h" Call_Data_t upload_call_worker(Call_Data_t call_info); -int create_call_json(Call_Data_t call_info) ; class Call_Concluder { static const int MAX_RETRY = 2; @@ -28,6 +27,7 @@ class Call_Concluder { static Call_Data_t create_call_data(Call *call, System *sys, Config config); static void conclude_call(Call *call, System *sys, Config config); static void manage_call_data_workers(); + static std::stringstream create_call_json_string(Call_Data_t call_info); private: static Call_Data_t create_base_filename(Call *call, Call_Data_t call_info); From 20a7e5bdcf42dbca290cf1bcd43b1d055582ba09 Mon Sep 17 00:00:00 2001 From: Max Watermolen Date: Sat, 10 Feb 2024 18:09:44 -0800 Subject: [PATCH 4/4] Fixes import --- Dockerfile | 2 +- plugins/tpng_uploader/tpng_uploader.cc | 2 +- trunk-recorder/call_concluder/call_concluder.cc | 2 ++ 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index 2b8c8d580..244d00cb7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM --platform=amd64 ubuntu:22.04 AS base +FROM ubuntu:22.04 AS base # Install docker for passing the socket to allow for intercontainer exec RUN apt-get update && \ diff --git a/plugins/tpng_uploader/tpng_uploader.cc b/plugins/tpng_uploader/tpng_uploader.cc index 8168465e3..4566a5967 100644 --- a/plugins/tpng_uploader/tpng_uploader.cc +++ b/plugins/tpng_uploader/tpng_uploader.cc @@ -172,7 +172,7 @@ class TPNG_Uploader : public Plugin_Api { BOOST_LOG_TRIVIAL(info) << "[" << call_info.short_name << "]\t\033[0;34m" << call_info.call_num << "C\033[0m\tTG: " << call_info.talkgroup_display << "\tFreq: " << format_freq(call_info.freq) << "\tTrunk-PlayerNG Upload Success - file size: " << file_info.st_size; ; return 0; - }else if (response_code == 401) { + } else if (response_code == 401) { BOOST_LOG_TRIVIAL(info) << "[" << call_info.short_name << "]\t\033[0;34m" << call_info.call_num << "C\033[0m\tTG: " << call_info.talkgroup_display << "\tFreq: " << format_freq(call_info.freq) << "\tTrunk-PlayerNG Upload Denyed - CHECK TOKEN OR ACLS" ; return 0; diff --git a/trunk-recorder/call_concluder/call_concluder.cc b/trunk-recorder/call_concluder/call_concluder.cc index b72bfe128..b25c6e2e9 100644 --- a/trunk-recorder/call_concluder/call_concluder.cc +++ b/trunk-recorder/call_concluder/call_concluder.cc @@ -2,6 +2,8 @@ #include "../plugin_manager/plugin_manager.h" #include #include +#include + namespace fs = std::filesystem; std::list> Call_Concluder::call_data_workers = {};