Skip to content

Instantly share code, notes, and snippets.

@Dygear
Last active September 5, 2025 13:44
Show Gist options
  • Save Dygear/a9b3c1958413d0c4e6c794f166a64c6c to your computer and use it in GitHub Desktop.
Save Dygear/a9b3c1958413d0c4e6c794f166a64c6c to your computer and use it in GitHub Desktop.
First crack at a UDP Status server.

Place files in user_plugins/tr-plugin-udp/ under the trunk-recorder directory.

Then follow the regular build instructions for Trunk-Recorder for your system. CMake magic will pick up the new files and compile it with your version of Trunk-Recorder.

add_library(status_udp
MODULE
status_udp.cc
)
include_directories(
${CMAKE_BINARY_DIR}/../
)
target_link_libraries(status_udp trunk_recorder_library ssl crypto ${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(status_udp
gnuradio::gnuradio-analog
gnuradio::gnuradio-blocks
gnuradio::gnuradio-digital
gnuradio::gnuradio-filter
gnuradio::gnuradio-pmt
)
endif()
install(TARGETS status_udp LIBRARY DESTINATION ${CMAKE_INSTALL_PREFIX}/lib/trunk-recorder)
// Trunk-Recorder Status Over UDP Plugin
// ********************************
// Requires trunk-recorder 5.0 or later.
// ********************************
#include <time.h>
#include <vector>
#include <fstream>
#include <iostream>
#include <cstdlib>
#include <string>
#include <cstring>
#include <trunk-recorder/source.h>
#include <json.hpp>
// #include <trunk-recorder/json.hpp>
#include <trunk-recorder/plugin_manager/plugin_api.h>
#include <boost/archive/iterators/base64_from_binary.hpp>
#include <boost/archive/iterators/transform_width.hpp>
#include <boost/date_time/posix_time/posix_time.hpp> //time_formatters.hpp>
#include <boost/dll/alias.hpp> // for BOOST_DLL_ALIAS
#include <boost/property_tree/json_parser.hpp>
#include <boost/log/trivial.hpp>
#include <boost/log/expressions.hpp>
#include <boost/log/sinks/sync_frontend.hpp>
#include <boost/log/sinks/text_ostream_backend.hpp>
#include <boost/crc.hpp>
// UDP Socket Includes.
#include <sys/types.h>
#include <sys/socket.h>
#include <netdb.h> // getaddrinfo, freeaddrinfo
#include <arpa/inet.h>
#include <unistd.h> // close
#include <errno.h>
#define INVALID_SOCKET -1
#define SOCKET_ERROR -1
typedef int SOCKET;
struct UdpTarget {
SOCKET sock;
sockaddr_storage addr;
socklen_t addrlen;
};
// Helper Consts
const int PLUGIN_SUCCESS = 0;
const int PLUGIN_FAILURE = 1;
using namespace std;
namespace logging = boost::log;
class Status_Udp : public Plugin_Api
{
// Trunk-Recorder
Config *tr_config;
std::vector<Source *> tr_sources;
std::vector<System *> tr_systems;
std::vector<Call *> tr_calls;
std::string tr_instance_id;
// Plugin Settings
std::string log_prefix = "\t[Status UDP]\t";
std::string udp_dest;
bool unit_enabled = true;
time_t call_resend_time = time(NULL);
// Plugin Socket
UdpTarget udp_socket = UdpTarget {};
public:
Status_Udp(){};
// ********************************
// trunk-recorder messages
// ********************************
// unit_registration()
// Unit registration on a system (on)
// TRUNK-RECORDER PLUGIN API: Called each REGISTRATION message
// MQTT: topic_unit/shortname/on
int unit_registration(System *sys, long source_id) override
{
if (unit_enabled)
{
nlohmann::ordered_json unit_json = get_unit_json(sys, source_id);
return send_json(unit_json, "on", "on");
}
return PLUGIN_SUCCESS;
}
// unit_deregistration()
// Unit de-registration on a system (off)
// TRUNK-RECORDER PLUGIN API: Called each DEREGISTRATION message
// MQTT: topic_unit/shortname/off
int unit_deregistration(System *sys, long source_id) override
{
if (unit_enabled)
{
nlohmann::ordered_json unit_json = get_unit_json(sys, source_id);
return send_json(unit_json, "off", "off");
}
return PLUGIN_SUCCESS;
}
// unit_acknowledge_response()
// Unit acknowledge response (ackresp)
// TRUNK-RECORDER PLUGIN API: Called each ACKNOWLEDGE message
// MQTT: topic_unit/shortname/ackresp
int unit_acknowledge_response(System *sys, long source_id) override
{
if (unit_enabled)
{
nlohmann::ordered_json unit_json = get_unit_json(sys, source_id);
return send_json(unit_json, "ackresp", "ackresp");
}
return PLUGIN_SUCCESS;
}
// unit_group_affiliation()
// Unit talkgroup affiliation (join)
// TRUNK-RECORDER PLUGIN API: Called each AFFILIATION message
// MQTT: topic_unit/shortname/join
int unit_group_affiliation(System *sys, long source_id, long talkgroup_num) override
{
if (unit_enabled)
{
nlohmann::ordered_json unit_json = get_unit_tg_json(sys, source_id, talkgroup_num);
return send_json(unit_json, "join", "join");
}
return PLUGIN_SUCCESS;
}
// unit_data_grant()
// Unit data grant (data)
// TRUNK-RECORDER PLUGIN API: Called each DATA_GRANT message
// MQTT: topic_unit/shortname/data
int unit_data_grant(System *sys, long source_id) override
{
if (unit_enabled)
{
nlohmann::ordered_json unit_json = get_unit_json(sys, source_id);
return send_json(unit_json, "data", "data");
}
return PLUGIN_SUCCESS;
}
// unit_answer_request()
// TRUNK-RECORDER PLUGIN API: Called each UU_ANS_REQ message
// MQTT: topic_unit/shortname/ans_req
int unit_answer_request(System *sys, long source_id, long talkgroup_num) override
{
if (unit_enabled)
{
nlohmann::ordered_json unit_json = get_unit_tg_json(sys, source_id, talkgroup_num);
return send_json(unit_json, "ans_req", "ans_req");
}
return PLUGIN_SUCCESS;
}
// unit_location()
// Unit location/roaming update (location)
// TRUNK-RECORDER PLUGIN API: Called each LOCATION message
// MQTT: topic_unit/shortname/location
int unit_location(System *sys, long source_id, long talkgroup_num) override
{
if (unit_enabled)
{
nlohmann::ordered_json unit_json = get_unit_tg_json(sys, source_id, talkgroup_num);
return send_json(unit_json, "location", "location");
}
return PLUGIN_SUCCESS;
}
// ********************************
// trunk-recorder plugin API & startup
// ********************************
// parse_config()
// TRUNK-RECORDER PLUGIN API: Called before init(); parses the config information for this plugin.
int parse_config(json config_data) override
{
// Get values from this plugin's config.json section and load into class variables.
udp_dest = config_data.value("destination", "udp://127.0.0.1:7767");
// Print plugin startup info
BOOST_LOG_TRIVIAL(info) << log_prefix << "destination: " << udp_dest << endl;
return PLUGIN_SUCCESS;
}
// init()
// TRUNK-RECORDER PLUGIN API: Plugin initialization; called after parse_config().
int init(Config *config, std::vector<Source *> sources, std::vector<System *> systems) override
{
// Set frequency format
frequency_format = config->frequency_format;
// Set instance ID
tr_instance_id = config->instance_id;
if (tr_instance_id == "")
tr_instance_id = "trunk-recorder";
// Establish pointers to systems, sources, and configs if needed later.
tr_sources = sources;
tr_systems = systems;
tr_config = config;
return PLUGIN_SUCCESS;
}
// start()
// TRUNK-RECORDER PLUGIN API: Called after trunk-recorder finishes setup and the plugin is initialized
int start() override
{
// Start the UDP connection
open_udp_connection();
return PLUGIN_SUCCESS;
}
// ********************************
// Helper functions
// ********************************
// patches_to_str()
// Combine a vector of talkgroup patches into a string.
std::string patches_to_str(std::vector<unsigned long> talkgroup_patches)
{
std::string patch_string;
BOOST_FOREACH (auto &TGID, talkgroup_patches)
{
if (!patch_string.empty())
patch_string += ",";
patch_string += std::to_string(TGID);
}
return patch_string;
}
// get_unit_json()
// Return a JSON object for a unit message WITHOUT a known talkgroup.
nlohmann::ordered_json get_unit_json(System *sys, long source_id)
{
nlohmann::ordered_json unit_json = {
{"sys_num", sys->get_sys_num()},
{"sys_name", sys->get_short_name()},
{"unit", source_id},
{"unit_alpha_tag", sys->find_unit_tag(source_id)}
};
return unit_json;
}
// get_unit_tg_json()
// Return a JSON object for a unit message WITH a known talkgroup.
nlohmann::ordered_json get_unit_tg_json(System *sys, long source_id, long talkgroup_num)
{
json tg_json = get_tg_json(sys, talkgroup_num);
nlohmann::ordered_json unit_tg_json = {
{"sys_num", sys->get_sys_num()},
{"sys_name", sys->get_short_name()},
{"unit", source_id},
{"unit_alpha_tag", sys->find_unit_tag(source_id)},
{"talkgroup", talkgroup_num},
{"talkgroup_alpha_tag", tg_json["talkgroup_alpha_tag"]},
{"talkgroup_description", tg_json["talkgroup_description"]},
{"talkgroup_group", tg_json["talkgroup_group"]},
{"talkgroup_tag", tg_json["talkgroup_tag"]},
{"talkgroup_patches", tg_json["talkgroup_patches"]}
};
return unit_tg_json;
}
// get_tg_json()
// Return a JSON object for talkgroup and patches. Returns "" if TG meta is not found.
json get_tg_json(System *sys, long talkgroup_num)
{
Talkgroup *tg = sys->find_talkgroup(talkgroup_num);
std::string patch_string = patches_to_str(sys->get_talkgroup_patch(talkgroup_num));
json tg_json = {
{"talkgroup", talkgroup_num},
{"talkgroup_alpha_tag", ""},
{"talkgroup_description", ""},
{"talkgroup_group", ""},
{"talkgroup_tag", ""},
{"talkgroup_patches", patch_string}
};
if (tg != NULL)
{
tg_json["talkgroup_alpha_tag"] = tg->alpha_tag;
tg_json["talkgroup_description"] = tg->description;
tg_json["talkgroup_group"] = tg->group;
tg_json["talkgroup_tag"] = tg->tag;
}
return tg_json;
}
void open_udp_connection()
{
this->udp_socket = make_udp_target(udp_dest);
if (this->udp_socket.sock == INVALID_SOCKET) {
BOOST_LOG_TRIVIAL(error) << log_prefix << "Failed to open UDP target for " << udp_dest;
}
}
// send_json()
// Send a JSON message using the configured connection.
// send_json(
// json data <- json payload,
// std::string name <- json payload name,
// std::string type <- subtopic / message type
// )
int send_json(nlohmann::ordered_json data, std::string name, std::string type)
{
if (udp_socket.sock == INVALID_SOCKET || udp_socket.addrlen == 0) {
BOOST_LOG_TRIVIAL(error) << log_prefix << "UDP socket not initialized";
return PLUGIN_FAILED;
}
// Assemble the JSON message
nlohmann::ordered_json payload = {
{"type", type},
{name, data},
{"timestamp", time(NULL)},
{"instance_id", tr_instance_id}
};
std::string payload_str = payload.dump();
ssize_t bytesSent = ::sendto(
udp_socket.sock,
payload_str.data(),
payload_str.size(),
0,
reinterpret_cast<const sockaddr*>(&udp_socket.addr),
udp_socket.addrlen
);
if (bytesSent == -1) {
int err = errno;
BOOST_LOG_TRIVIAL(error) << log_prefix << "sendto failed (" << err << "): " << std::strerror(err);
return PLUGIN_FAILURE;
}
BOOST_LOG_TRIVIAL(info) << log_prefix
<< "send_json: (Sent " << bytesSent << " of "
<< payload_str.size() << " bytes) " << payload_str
;
return PLUGIN_SUCCESS;
}
// ********************************
// Utility Functions
// ********************************
// Parse udp://host[:port], with default port 7727
bool parse_udp_uri(const std::string& uri, std::string& host, std::string& port) {
const std::string prefix = "udp://";
if (uri.rfind(prefix, 0) != 0) {
BOOST_LOG_TRIVIAL(error) << log_prefix << "Destination URI must start with udp://" << endl;
return false;
}
auto without_scheme = uri.substr(prefix.size());
auto colon = without_scheme.find_last_of(':');
if (colon == std::string::npos) {
host = without_scheme;
port = "7767"; // default
} else {
host = without_scheme.substr(0, colon);
port = without_scheme.substr(colon + 1);
if (port.empty()) port = "7767"; // handle udp://host:
}
BOOST_LOG_TRIVIAL(info) << log_prefix << "parse_udp_uri: host: '" << host << "' port: '" << port << "'" << endl;
return !host.empty();
}
UdpTarget make_udp_target(const std::string& uri) {
UdpTarget target{};
target.sock = INVALID_SOCKET;
std::string host, port;
if (!parse_udp_uri(uri, host, port)) {
BOOST_LOG_TRIVIAL(error) << "Invalid URI format";
return target;
}
if (host == "0.0.0.0" || host == "::") {
BOOST_LOG_TRIVIAL(error) << log_prefix << "Refusing to use unspecified address (" << host << ") as a destination";
return target;
}
addrinfo hints{};
hints.ai_family = AF_UNSPEC;
hints.ai_socktype = SOCK_DGRAM;
hints.ai_flags = AI_NUMERICSERV;
addrinfo* res = nullptr;
int rc = ::getaddrinfo(host.c_str(), port.c_str(), &hints, &res);
if (rc != 0) {
BOOST_LOG_TRIVIAL(error) << log_prefix << "getaddrinfo failed for " << host << ":" << port
<< " (" << gai_strerror(rc) << ")";
return target;
}
target.sock = ::socket(res->ai_family, res->ai_socktype, res->ai_protocol);
if (target.sock == INVALID_SOCKET) {
BOOST_LOG_TRIVIAL(error) << log_prefix << "socket() failed";
::freeaddrinfo(res);
return target;
}
std::memcpy(&target.addr, res->ai_addr, res->ai_addrlen);
target.addrlen = static_cast<socklen_t>(res->ai_addrlen);
// Optional: enable broadcast if you're targeting a broadcast address
if (res->ai_family == AF_INET) {
auto sin = reinterpret_cast<const sockaddr_in*>(res->ai_addr);
if (sin->sin_addr.s_addr == INADDR_BROADCAST) {
int yes = 1;
::setsockopt(target.sock, SOL_SOCKET, SO_BROADCAST, &yes, sizeof(yes));
}
}
::freeaddrinfo(res);
return target;
}
// ********************************
// Create the plugin
// ********************************
// Factory method
static boost::shared_ptr<Status_Udp> create()
{
return boost::shared_ptr<Status_Udp>(new Status_Udp());
}
};
BOOST_DLL_ALIAS(
Status_Udp::create, // <-- this function is exported with...
create_plugin // <-- ... this alias name
)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment