|
// 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 |
|
) |