You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
831 lines
33 KiB
831 lines
33 KiB
// SPDX-License-Identifier: GPL-2.0-only
|
|
/*
|
|
* Digital Voice Modem - FNE Call Simulator
|
|
* GPLv2 Open Source. Use is subject to license terms.
|
|
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
|
|
*
|
|
* Copyright (C) 2026 Bryan Biedenkapp, N2PLL
|
|
*
|
|
*/
|
|
/**
|
|
* @defgroup callsim FNE Call Simulator
|
|
* @brief Command-line generation of deterministic DMR, P25, and NXDN calls.
|
|
*
|
|
* @file CallSim.cpp
|
|
* @brief Implements a command-line FNE call simulator for DMR, P25 Phase 1, and NXDN.
|
|
* @ingroup callsim
|
|
*
|
|
* The simulator authenticates as a normal FNE peer and emits group calls at
|
|
* each protocol's native network-frame cadence. Calls are sequential by
|
|
* default; simultaneous mode multiplexes one independent RTP stream per
|
|
* configured talkgroup over the same authenticated connection.
|
|
*/
|
|
#include "Defines.h"
|
|
#include "CallSimNetwork.h"
|
|
#include "common/Log.h"
|
|
#include "common/Thread.h"
|
|
#include "common/dmr/DMRDefines.h"
|
|
#include "common/dmr/data/NetData.h"
|
|
#include "common/network/Network.h"
|
|
#include "common/nxdn/NXDNDefines.h"
|
|
#include "common/nxdn/Audio.h"
|
|
#include "common/nxdn/NXDNUtils.h"
|
|
#include "common/nxdn/Sync.h"
|
|
#include "common/nxdn/channel/LICH.h"
|
|
#include "common/nxdn/channel/SACCH.h"
|
|
#include "common/nxdn/lc/RTCH.h"
|
|
#include "common/p25/P25Defines.h"
|
|
#include "common/p25/data/LowSpeedData.h"
|
|
#include "common/p25/lc/LC.h"
|
|
#include "modem/Modem.h"
|
|
|
|
#undef __PROG_NAME__
|
|
#define __PROG_NAME__ "Digital Voice Modem (DVM) FNE Call Simulator"
|
|
#undef __EXE_NAME__
|
|
#define __EXE_NAME__ "callsim"
|
|
|
|
using namespace network;
|
|
|
|
#include <algorithm>
|
|
#include <atomic>
|
|
#include <cerrno>
|
|
#include <chrono>
|
|
#include <csignal>
|
|
#include <cstdio>
|
|
#include <cstdlib>
|
|
#include <cstring>
|
|
#include <string>
|
|
#include <thread>
|
|
#include <vector>
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Constants
|
|
// ---------------------------------------------------------------------------
|
|
|
|
static constexpr uint32_t DEFAULT_CALL_COUNT = 1U; //!< Default number of calls to emit.
|
|
static constexpr uint32_t DEFAULT_CALL_DURATION_SECONDS = 5U; //!< Default call duration in seconds.
|
|
static constexpr uint32_t DEFAULT_CALL_GAP_MS = 500U; //!< Default inter-call delay in milliseconds.
|
|
static constexpr uint32_t DEFAULT_CONNECT_TIMEOUT_MS = 10000U; //!< FNE login timeout in milliseconds.
|
|
static constexpr uint32_t MILLISECONDS_PER_SECOND = 1000U; //!< Milliseconds in one second.
|
|
static constexpr uint32_t P25_LDU_PERIOD_MS = 180U; //!< P25 Phase 1 LDU interval in milliseconds.
|
|
static constexpr uint32_t DMR_BURST_PERIOD_MS = 60U; //!< DMR voice burst interval in milliseconds.
|
|
static constexpr uint32_t NXDN_FRAME_PERIOD_MS = 80U; //!< NXDN 4800 voice-frame interval in milliseconds.
|
|
|
|
/** @brief Stationary IMBE frame taken from the project's P25 1011 Hz calibration vector. */
|
|
static const uint8_t P25_TONE_IMBE[P25DEF::RAW_IMBE_LENGTH_BYTES] = {
|
|
0xCBU, 0x19U, 0x4DU, 0x0DU, 0xCEU, 0x24U, 0xA1U, 0x24U, 0x0DU, 0x43U, 0x3CU
|
|
};
|
|
|
|
/** @brief Fixed AMBE payload taken from the project's DMR 1031 Hz calibration vector. */
|
|
static const uint8_t DMR_TONE_BURST[DMRDEF::DMR_FRAME_LENGTH_BYTES] = {
|
|
0xCEU, 0xA8U, 0xFEU, 0x83U, 0xACU, 0xC4U, 0x58U, 0x20U, 0x0AU, 0xCEU, 0xA8U,
|
|
0xFEU, 0x83U, 0xA0U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x0CU, 0xC4U, 0x58U,
|
|
0x20U, 0x0AU, 0xCEU, 0xA8U, 0xFEU, 0x83U, 0xACU, 0xC4U, 0x58U, 0x20U, 0x0AU
|
|
};
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Global Variables
|
|
// ---------------------------------------------------------------------------
|
|
|
|
std::string g_progExe = std::string(__EXE_NAME__);
|
|
static std::atomic<bool> g_stop(false); //!< Set by the signal handler to request an orderly shutdown.
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Enumeration Declaration
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/**
|
|
* @brief Identifies the air-interface protocol simulated for each call.
|
|
* @ingroup callsim
|
|
*/
|
|
enum class Mode {
|
|
DMR, //!< Digital Mobile Radio Tier II/III voice.
|
|
P25, //!< APCO Project 25 Phase 1 FDMA voice.
|
|
NXDN //!< NXDN 4800 baud FDMA voice.
|
|
};
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Structure Declaration
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/**
|
|
* @brief Holds validated command-line configuration for one simulator run.
|
|
* @ingroup callsim
|
|
*/
|
|
struct Options {
|
|
std::string address = "127.0.0.1"; //!< FNE hostname or IP address.
|
|
uint16_t port = TRAFFIC_DEFAULT_PORT; //!< FNE UDP traffic port.
|
|
uint32_t peerId = 9000998U; //!< Peer ID presented during FNE authentication.
|
|
std::string password = "passw0rd"; //!< Password used for FNE authentication.
|
|
std::string identity = "CallSim"; //!< Descriptive identity included in peer metadata.
|
|
|
|
Mode mode = Mode::P25; //!< Protocol used for generated calls.
|
|
|
|
uint32_t callCount = DEFAULT_CALL_COUNT; //!< Calls, or simultaneous TG-batch repetitions, to emit.
|
|
uint32_t durationSeconds = DEFAULT_CALL_DURATION_SECONDS; //!< Duration of each call in seconds.
|
|
uint32_t gapMs = DEFAULT_CALL_GAP_MS; //!< Delay between completed calls in milliseconds.
|
|
uint32_t srcId = 100001U; //!< Source ID used by the first call; incremented per call.
|
|
|
|
std::vector<uint32_t> talkgroups; //!< Destination IDs selected round-robin by call index.
|
|
std::vector<uint8_t> slots; //!< DMR slots selected round-robin by call index.
|
|
|
|
bool grantDemand = false; //!< Sends an FNE grant request before each call when true.
|
|
|
|
bool simultaneous = false; //!< Emits all configured talkgroups concurrently when true.
|
|
|
|
bool debug = false; //!< Enables verbose network logging when true.
|
|
};
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Global Functions
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/**
|
|
* @brief Signal handler for orderly simulator shutdown.
|
|
* @param signal The signal number received (ignored).
|
|
*/
|
|
static void signalHandler(int)
|
|
{
|
|
g_stop = true;
|
|
}
|
|
|
|
/**
|
|
* @brief Prints version information for the program.
|
|
*/
|
|
static void version()
|
|
{
|
|
::fprintf(stdout, __PROG_NAME__ " %s (built %s)\r\n", __VER__, __BUILD__);
|
|
::fprintf(stdout, "Copyright (c) 2017-2026 Bryan Biedenkapp, N2PLL and DVMProject (https://github.com/dvmproject) Authors.\n");
|
|
::fprintf(stdout, "Portions Copyright (c) 2015-2021 by Jonathan Naylor, G4KLX and others\n");
|
|
::fprintf(stdout, HIGHLY_UNNECESSARY_DISCLAIMER_FOR_THE_MENTAL "\n\n");
|
|
}
|
|
|
|
/**
|
|
* @brief Prints command-line usage, optionally preceded by an argument error.
|
|
* @param message An optional error message to display before the usage.
|
|
* @param arg An optional argument to format into the error message.
|
|
*/
|
|
static void usage(const char* message, const char* arg)
|
|
{
|
|
version();
|
|
if (message != nullptr) {
|
|
::fprintf(stderr, "%s: ", g_progExe.c_str());
|
|
::fprintf(stderr, message, arg);
|
|
::fprintf(stderr, "\n\n");
|
|
}
|
|
|
|
::fprintf(stdout,
|
|
"usage: %s --mode <p25|dmr|nxdn> --talkgroups <tg[,tg...]> [options]\n"
|
|
"\n"
|
|
"Connection:\n"
|
|
" --address <host> FNE address (default 127.0.0.1)\n"
|
|
" --port <port> FNE traffic port (default %u)\n"
|
|
" --peer-id <id> Simulated peer ID (default 9000998)\n"
|
|
" --password <password> FNE peer password\n"
|
|
" --identity <name> Peer identity sent to the FNE\n"
|
|
"\n"
|
|
"Calls:\n"
|
|
" --mode <mode> p25, dmr, or nxdn\n"
|
|
" --calls <count> Calls, or simultaneous batch repetitions (default 1)\n"
|
|
" --duration <seconds> Call duration in seconds (default 5)\n"
|
|
" --gap <ms> Delay between calls (default 500)\n"
|
|
" --source <rid> First source radio ID (default 100001)\n"
|
|
" --talkgroups <list> Comma-separated destination IDs\n"
|
|
" --slots <list> DMR slots, one per TG or cycled (default 1)\n"
|
|
" --grant-demand Send an FNE grant request before each call\n"
|
|
" --simultaneous Emit all configured TGs concurrently per repetition\n"
|
|
" --debug Enable network debug logging\n"
|
|
" --help Show this help\n",
|
|
g_progExe.c_str(), TRAFFIC_DEFAULT_PORT);
|
|
}
|
|
|
|
/**
|
|
* @brief Parses and range-checks an unsigned decimal command-line value.
|
|
* @param text The string containing the value to parse.
|
|
* @param minimum The minimum allowed value.
|
|
* @param maximum The maximum allowed value.
|
|
* @param value The variable to store the parsed value.
|
|
* @return true if the value was successfully parsed and within range, false otherwise.
|
|
*/
|
|
static bool parseUnsigned(const char* text, uint32_t minimum, uint32_t maximum, uint32_t& value)
|
|
{
|
|
if (text == nullptr || text[0U] == '\0')
|
|
return false;
|
|
|
|
errno = 0;
|
|
char* end = nullptr;
|
|
unsigned long parsed = ::strtoul(text, &end, 10);
|
|
if (errno != 0 || end == text || *end != '\0' || parsed < minimum || parsed > maximum)
|
|
return false;
|
|
|
|
value = static_cast<uint32_t>(parsed);
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* @brief Parses a comma-separated list of range-checked unsigned decimal values.
|
|
* @param text The comma-separated list of values as a string.
|
|
* @param minimum The minimum allowed value for each item.
|
|
* @param maximum The maximum allowed value for each item.
|
|
* @param values The vector to populate with parsed values.
|
|
* @return true if all values were successfully parsed and within range, false otherwise.
|
|
*/
|
|
static bool parseList(const std::string& text, uint32_t minimum, uint32_t maximum, std::vector<uint32_t>& values)
|
|
{
|
|
// parse a comma-separated list of unsigned decimal values within the specified range
|
|
size_t start = 0U;
|
|
while (start <= text.length()) {
|
|
size_t end = text.find(',', start);
|
|
std::string item = text.substr(start, end == std::string::npos ? std::string::npos : end - start);
|
|
|
|
uint32_t value = 0U;
|
|
if (!parseUnsigned(item.c_str(), minimum, maximum, value))
|
|
return false;
|
|
|
|
values.push_back(value);
|
|
if (end == std::string::npos)
|
|
break;
|
|
|
|
start = end + 1U;
|
|
}
|
|
return !values.empty();
|
|
}
|
|
|
|
/**
|
|
* @brief Parses and validates all command-line options.
|
|
* @param argc The number of command-line arguments.
|
|
* @param argv The array of command-line argument strings.
|
|
* @param options The structure to populate with parsed options.
|
|
* @return true if all options were successfully parsed and validated, false otherwise.
|
|
*/
|
|
static bool parseOptions(int argc, char** argv, Options& options)
|
|
{
|
|
for (int i = 1; i < argc; i++) {
|
|
const std::string arg(argv[i]);
|
|
auto requireValue = [&](const char* name) -> const char* {
|
|
if (++i >= argc) {
|
|
std::string message = std::string(name) + " requires a value";
|
|
usage("error: %s", message.c_str());
|
|
return nullptr;
|
|
}
|
|
return argv[i];
|
|
};
|
|
|
|
if (arg == "--help" || arg == "-h") {
|
|
usage(nullptr, nullptr);
|
|
return false;
|
|
} else if (arg == "--debug") {
|
|
options.debug = true;
|
|
} else if (arg == "--grant-demand") {
|
|
options.grantDemand = true;
|
|
} else if (arg == "--simultaneous") {
|
|
options.simultaneous = true;
|
|
} else if (arg == "--address") {
|
|
const char* value = requireValue("--address");
|
|
if (value == nullptr)
|
|
return false;
|
|
options.address = value;
|
|
} else if (arg == "--password") {
|
|
const char* value = requireValue("--password");
|
|
if (value == nullptr)
|
|
return false;
|
|
options.password = value;
|
|
} else if (arg == "--identity") {
|
|
const char* value = requireValue("--identity");
|
|
if (value == nullptr)
|
|
return false;
|
|
options.identity = value;
|
|
} else if (arg == "--mode") {
|
|
const char* value = requireValue("--mode");
|
|
if (value == nullptr)
|
|
return false;
|
|
if (::strcmp(value, "p25") == 0)
|
|
options.mode = Mode::P25;
|
|
else if (::strcmp(value, "dmr") == 0)
|
|
options.mode = Mode::DMR;
|
|
else if (::strcmp(value, "nxdn") == 0)
|
|
options.mode = Mode::NXDN;
|
|
else {
|
|
usage("error: %s", "--mode must be p25, dmr, or nxdn");
|
|
return false;
|
|
}
|
|
} else if (arg == "--talkgroups") {
|
|
const char* value = requireValue("--talkgroups");
|
|
if (value == nullptr || !parseList(value, 1U, 0xFFFFFFU, options.talkgroups)) {
|
|
usage("error: %s", "invalid --talkgroups list");
|
|
return false;
|
|
}
|
|
} else if (arg == "--slots") {
|
|
const char* value = requireValue("--slots");
|
|
std::vector<uint32_t> slots;
|
|
if (value == nullptr || !parseList(value, 1U, 2U, slots)) {
|
|
usage("error: %s", "invalid --slots list");
|
|
return false;
|
|
}
|
|
for (uint32_t slot : slots)
|
|
options.slots.push_back(static_cast<uint8_t>(slot));
|
|
} else {
|
|
const char* value = nullptr;
|
|
uint32_t parsed = 0U;
|
|
if (arg == "--port") {
|
|
value = requireValue("--port");
|
|
if (value == nullptr || !parseUnsigned(value, 1U, 65535U, parsed)) {
|
|
usage("error: %s", "invalid --port");
|
|
return false;
|
|
}
|
|
options.port = static_cast<uint16_t>(parsed);
|
|
} else if (arg == "--peer-id") {
|
|
value = requireValue("--peer-id");
|
|
if (value == nullptr || !parseUnsigned(value, 1U, 999999999U, options.peerId)) {
|
|
usage("error: %s", "invalid --peer-id");
|
|
return false;
|
|
}
|
|
} else if (arg == "--calls") {
|
|
value = requireValue("--calls");
|
|
if (value == nullptr || !parseUnsigned(value, 1U, UINT32_MAX, options.callCount)) {
|
|
usage("error: %s", "invalid --calls");
|
|
return false;
|
|
}
|
|
} else if (arg == "--duration") {
|
|
value = requireValue("--duration");
|
|
if (value == nullptr || !parseUnsigned(value, 1U, UINT32_MAX / MILLISECONDS_PER_SECOND, options.durationSeconds)) {
|
|
usage("error: %s", "invalid --duration");
|
|
return false;
|
|
}
|
|
} else if (arg == "--gap") {
|
|
value = requireValue("--gap");
|
|
if (value == nullptr || !parseUnsigned(value, 0U, UINT32_MAX, options.gapMs)) {
|
|
usage("error: %s", "invalid --gap");
|
|
return false;
|
|
}
|
|
} else if (arg == "--source") {
|
|
value = requireValue("--source");
|
|
if (value == nullptr || !parseUnsigned(value, 1U, 0xFFFFFFU, options.srcId)) {
|
|
usage("error: %s", "invalid --source");
|
|
return false;
|
|
}
|
|
} else {
|
|
usage("error: %s", (std::string("unknown option: ") + arg).c_str());
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (options.talkgroups.empty()) {
|
|
usage("error: %s", "--talkgroups is required");
|
|
return false;
|
|
}
|
|
|
|
if (options.mode == Mode::NXDN && (*std::max_element(options.talkgroups.begin(), options.talkgroups.end()) > 65535U || options.srcId > 65535U)) {
|
|
usage("error: %s", "NXDN source and destination IDs must not exceed 65535");
|
|
return false;
|
|
}
|
|
|
|
if (options.slots.empty())
|
|
options.slots.push_back(1U);
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* @brief Clocks FNE timers and transport processing while maintaining a protocol delay.
|
|
* @param network The network interface to use for clocking.
|
|
* @param durationMs The duration to wait and clock in milliseconds.
|
|
*/
|
|
static void waitAndClock(CallSimNetwork& network, uint32_t durationMs)
|
|
{
|
|
auto start = std::chrono::steady_clock::now();
|
|
auto last = start;
|
|
|
|
while (!g_stop) {
|
|
auto now = std::chrono::steady_clock::now();
|
|
uint32_t elapsed = static_cast<uint32_t>(std::chrono::duration_cast<std::chrono::milliseconds>(now - start).count());
|
|
uint32_t delta = static_cast<uint32_t>(std::chrono::duration_cast<std::chrono::milliseconds>(now - last).count());
|
|
|
|
if (elapsed >= durationMs)
|
|
break;
|
|
if (delta > 0U) {
|
|
network.clockConnection(delta);
|
|
last = now;
|
|
}
|
|
|
|
Thread::sleep(1U);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @brief Waits between protocol frames while the owning scheduler clocks the connection.
|
|
* @param durationMs The duration to wait in milliseconds.
|
|
*/
|
|
static void waitForFrame(uint32_t durationMs)
|
|
{
|
|
const auto start = std::chrono::steady_clock::now();
|
|
while (!g_stop) {
|
|
const auto now = std::chrono::steady_clock::now();
|
|
const uint32_t elapsed = static_cast<uint32_t>(std::chrono::duration_cast<std::chrono::milliseconds>(now - start).count());
|
|
if (elapsed >= durationMs)
|
|
break;
|
|
|
|
Thread::sleep(1U);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @brief Sends an optional group-call grant request using the selected protocol and slot.
|
|
* @param network The network interface to use for sending the request.
|
|
* @param mode The protocol mode (DMR, P25, NXDN).
|
|
* @param srcId The source ID of the call.
|
|
* @param dstId The destination ID of the call.
|
|
* @param slot The slot number for the call (relevant for DMR mode).
|
|
* @return true if the grant request was successfully sent, false otherwise.
|
|
*/
|
|
static bool writeGrantDemand(CallSimNetwork& network, Mode mode, uint32_t srcId, uint32_t dstId, uint8_t slot)
|
|
{
|
|
uint8_t dvmState = modem::DVM_STATE::STATE_IDLE;
|
|
switch (mode) {
|
|
case Mode::DMR:
|
|
dvmState = modem::DVM_STATE::STATE_DMR;
|
|
break;
|
|
case Mode::P25:
|
|
dvmState = modem::DVM_STATE::STATE_P25;
|
|
slot = 0U;
|
|
break;
|
|
case Mode::NXDN:
|
|
dvmState = modem::DVM_STATE::STATE_NXDN;
|
|
slot = 0U;
|
|
break;
|
|
}
|
|
|
|
// calls emitted by this simulator are group calls with default service
|
|
// options; only DMR carries a meaningful TDMA slot in this implementation
|
|
return network.writeGroupGrantRequest(dvmState, srcId, dstId, slot);
|
|
}
|
|
|
|
/**
|
|
* @brief Fills each vocoder position in a raw P25 LDU with the fixed tone vector.
|
|
* @param data Pointer to the raw P25 LDU data buffer to be filled.
|
|
*/
|
|
static void fillP25Tone(uint8_t* data)
|
|
{
|
|
static const uint16_t offsets[] = { 10U, 26U, 55U, 80U, 105U, 130U, 155U, 180U, 204U };
|
|
::memset(data, 0x00U, P25DEF::P25_LDU_FRAME_LENGTH_BYTES);
|
|
|
|
for (uint16_t offset : offsets)
|
|
::memcpy(data + offset, P25_TONE_IMBE, P25DEF::RAW_IMBE_LENGTH_BYTES);
|
|
}
|
|
|
|
/**
|
|
* @brief Emits alternating P25 LDU1/LDU2 frames followed by a TDU terminator.
|
|
* @param network The network interface to use for emitting the frames.
|
|
* @param srcId The source ID of the call.
|
|
* @param dstId The destination ID of the call.
|
|
* @param durationMs The duration of the call in milliseconds.
|
|
* @param stream The call stream state.
|
|
* @return true if the frames were successfully emitted, false otherwise.
|
|
*/
|
|
static bool emitP25(CallSimNetwork& network, uint32_t srcId, uint32_t dstId, uint32_t durationMs, CallStreamState& stream)
|
|
{
|
|
p25::lc::LC control;
|
|
control.setLCO(P25DEF::LCO::GROUP);
|
|
control.setGroup(true);
|
|
control.setSrcId(srcId);
|
|
control.setDstId(dstId);
|
|
control.setPriority(4U);
|
|
p25::data::LowSpeedData lsd;
|
|
|
|
uint8_t ldu1[P25DEF::P25_LDU_FRAME_LENGTH_BYTES];
|
|
uint8_t ldu2[P25DEF::P25_LDU_FRAME_LENGTH_BYTES];
|
|
fillP25Tone(ldu1);
|
|
fillP25Tone(ldu2);
|
|
|
|
uint32_t elapsed = 0U;
|
|
bool useLDU1 = true;
|
|
while (!g_stop && elapsed < durationMs) {
|
|
bool written = useLDU1 ? network.writeP25LDU1Call(control, lsd, ldu1, stream) : network.writeP25LDU2Call(control, lsd, ldu2, stream);
|
|
if (!written)
|
|
return false;
|
|
|
|
useLDU1 = !useLDU1;
|
|
|
|
uint32_t delay = std::min(P25_LDU_PERIOD_MS, durationMs - elapsed);
|
|
waitForFrame(delay);
|
|
|
|
elapsed += delay;
|
|
}
|
|
|
|
return network.writeP25TDUCall(control, lsd, stream);
|
|
}
|
|
|
|
/**
|
|
* @brief Emits a six-burst DMR voice sequence followed by a terminator-with-LC marker.
|
|
* @param network The network interface to use for emitting the call.
|
|
* @param srcId The source ID of the call.
|
|
* @param dstId The destination ID of the call.
|
|
* @param slot The slot number for the call (relevant for DMR mode).
|
|
* @param durationMs The duration of the call in milliseconds.
|
|
* @param stream The call stream state.
|
|
* @return true if the call was successfully emitted, false otherwise.
|
|
*/
|
|
static bool emitDMR(CallSimNetwork& network, uint32_t srcId, uint32_t dstId, uint8_t slot, uint32_t durationMs, CallStreamState& stream)
|
|
{
|
|
uint32_t elapsed = 0U;
|
|
uint8_t n = 0U;
|
|
uint8_t seqNo = 0U;
|
|
|
|
while (!g_stop && elapsed < durationMs) {
|
|
dmr::data::NetData frame;
|
|
frame.setSlotNo(slot);
|
|
frame.setDataType(n == 0U ? DMRDEF::DataType::VOICE_SYNC : DMRDEF::DataType::VOICE);
|
|
frame.setSrcId(srcId);
|
|
frame.setDstId(dstId);
|
|
frame.setFLCO(DMRDEF::FLCO::GROUP);
|
|
frame.setN(n);
|
|
frame.setSeqNo(seqNo++);
|
|
frame.setBER(0U);
|
|
frame.setRSSI(0U);
|
|
frame.setData(DMR_TONE_BURST);
|
|
|
|
if (!network.writeDMRCall(frame, stream))
|
|
return false;
|
|
|
|
n = (n + 1U) % 6U;
|
|
uint32_t delay = std::min(DMR_BURST_PERIOD_MS, durationMs - elapsed);
|
|
waitForFrame(delay);
|
|
|
|
elapsed += delay;
|
|
}
|
|
|
|
dmr::data::NetData terminator;
|
|
uint8_t data[DMRDEF::DMR_FRAME_LENGTH_BYTES] = { 0U };
|
|
terminator.setSlotNo(slot);
|
|
terminator.setDataType(DMRDEF::DataType::TERMINATOR_WITH_LC);
|
|
terminator.setSrcId(srcId);
|
|
terminator.setDstId(dstId);
|
|
terminator.setFLCO(DMRDEF::FLCO::GROUP);
|
|
terminator.setSeqNo(seqNo);
|
|
terminator.setData(data);
|
|
|
|
return network.writeDMRCall(terminator, stream, true);
|
|
}
|
|
|
|
/**
|
|
* @brief Emits one NXDN 4800 voice frame with segmented SACCH link control.
|
|
* @param network The network interface to use for emitting the frame.
|
|
* @param control The RTCH link control structure.
|
|
* @param sequence The sequence number of the frame within the superframe.
|
|
* @param stream The call stream state.
|
|
* @param end Indicates if this is the last frame of the call.
|
|
* @return true if the frame was successfully emitted, false otherwise.
|
|
*/
|
|
static bool emitNXDNFrame(CallSimNetwork& network, nxdn::lc::RTCH& control, uint8_t sequence, CallStreamState& stream, bool end = false)
|
|
{
|
|
using namespace nxdn;
|
|
using namespace nxdn::defines;
|
|
|
|
uint8_t frame[NXDN_FRAME_LENGTH_BYTES];
|
|
::memset(frame, 0x00U, sizeof(frame));
|
|
Sync::addNXDNSync(frame);
|
|
|
|
channel::LICH lich;
|
|
lich.setRFCT(RFChannelType::RDCH);
|
|
lich.setFCT(FuncChannelType::USC_SACCH_SS);
|
|
lich.setOption(ChOption::STEAL_NONE);
|
|
lich.setOutbound(true);
|
|
lich.encode(frame);
|
|
|
|
uint8_t lcData[NXDN_RTCH_LC_LENGTH_BYTES] = { 0U };
|
|
control.encode(lcData, NXDN_RTCH_LC_LENGTH_BITS);
|
|
static const ChStructure::E structures[] = {
|
|
ChStructure::SR_1_4, ChStructure::SR_2_4, ChStructure::SR_3_4, ChStructure::SR_4_4
|
|
};
|
|
uint8_t sacchData[3U] = { 0U };
|
|
uint8_t superframeIndex = sequence % 4U;
|
|
for (uint32_t bit = 0U; bit < 18U; bit++)
|
|
WRITE_BIT(sacchData, bit, READ_BIT(lcData, superframeIndex * 18U + bit));
|
|
|
|
channel::SACCH sacch;
|
|
sacch.setData(sacchData);
|
|
sacch.setRAN(0U);
|
|
sacch.setStructure(structures[superframeIndex]);
|
|
sacch.encode(frame);
|
|
|
|
// reuse the fixed AMBE test bits as the four vocoder payloads carried by
|
|
// this NXDN frame, then apply the NXDN-specific audio FEC/interleaving
|
|
Audio audio;
|
|
uint8_t packedBits[13U] = { 0U };
|
|
const uint8_t* ambe0 = DMR_TONE_BURST;
|
|
const uint8_t* ambe1 = DMR_TONE_BURST + 9U;
|
|
const uint8_t* ambe2 = DMR_TONE_BURST + 18U;
|
|
for (uint32_t bit = 0U; bit < 49U; bit++) {
|
|
WRITE_BIT(packedBits, bit, READ_BIT(ambe0, bit));
|
|
WRITE_BIT(packedBits, 49U + bit, READ_BIT(ambe1, bit));
|
|
}
|
|
|
|
audio.encode(packedBits, frame + NXDN_FSW_LICH_SACCH_LENGTH_BYTES);
|
|
::memset(packedBits, 0x00U, sizeof(packedBits));
|
|
|
|
for (uint32_t bit = 0U; bit < 49U; bit++) {
|
|
WRITE_BIT(packedBits, bit, READ_BIT(ambe2, bit));
|
|
WRITE_BIT(packedBits, 49U + bit, READ_BIT(ambe0, bit));
|
|
}
|
|
|
|
audio.encode(packedBits, frame + NXDN_FSW_LICH_SACCH_LENGTH_BYTES + 18U);
|
|
|
|
NXDNUtils::scrambler(frame);
|
|
|
|
return network.writeNXDNCall(control, frame, NXDN_FRAME_LENGTH_BYTES, stream, end);
|
|
}
|
|
|
|
/**
|
|
* @brief Emits NXDN voice frames followed by an RTCH transmission-release frame.
|
|
* @param network The network interface to use for emitting the frames.
|
|
* @param srcId The source ID of the call.
|
|
* @param dstId The destination ID of the call.
|
|
* @param durationMs The duration of the call in milliseconds.
|
|
* @param stream The call stream state.
|
|
* @return true if the frames were successfully emitted, false otherwise.
|
|
*/
|
|
static bool emitNXDN(CallSimNetwork& network, uint32_t srcId, uint32_t dstId, uint32_t durationMs, CallStreamState& stream)
|
|
{
|
|
using namespace nxdn::defines;
|
|
nxdn::lc::RTCH control;
|
|
control.setMessageType(MessageType::RTCH_VCALL);
|
|
control.setCallType(CallType::UNSPECIFIED);
|
|
control.setGroup(true);
|
|
control.setSrcId(static_cast<uint16_t>(srcId));
|
|
control.setDstId(static_cast<uint16_t>(dstId));
|
|
control.setTransmissionMode(TransmissionMode::MODE_4800);
|
|
|
|
uint32_t elapsed = 0U;
|
|
uint8_t sequence = 0U;
|
|
while (!g_stop && elapsed < durationMs) {
|
|
if (!emitNXDNFrame(network, control, sequence++, stream))
|
|
return false;
|
|
|
|
uint32_t delay = std::min(NXDN_FRAME_PERIOD_MS, durationMs - elapsed);
|
|
waitForFrame(delay);
|
|
|
|
elapsed += delay;
|
|
}
|
|
|
|
control.setMessageType(MessageType::RTCH_TX_REL);
|
|
|
|
return emitNXDNFrame(network, control, sequence, stream, true);
|
|
}
|
|
|
|
/**
|
|
* @brief Emits one complete call using the specified options and call parameters.
|
|
* @param network The network interface to use for emitting the call.
|
|
* @param options The call simulation options.
|
|
* @param callNumber The current call number.
|
|
* @param totalCalls The total number of calls to simulate.
|
|
* @param srcId The source ID of the call.
|
|
* @param dstId The destination ID of the call.
|
|
* @param slot The slot number for the call (relevant for DMR mode).
|
|
* @param durationMs The duration of the call in milliseconds.
|
|
* @return true if the call was successfully emitted, false otherwise.
|
|
*/
|
|
static bool emitCall(CallSimNetwork& network, const Options& options, uint32_t callNumber, uint32_t totalCalls,
|
|
uint32_t srcId, uint32_t dstId, uint8_t slot, uint32_t durationMs)
|
|
{
|
|
LogInfo("Call %u/%u start: srcId = %u, dstId = %u, slot = %u, duration = %u seconds",
|
|
callNumber, totalCalls, srcId, dstId, slot, options.durationSeconds);
|
|
|
|
if (options.grantDemand) {
|
|
LogInfo("Call %u/%u grant demand: srcId = %u, dstId = %u, slot = %u",
|
|
callNumber, totalCalls, srcId, dstId, options.mode == Mode::DMR ? slot : 0U);
|
|
if (!writeGrantDemand(network, options.mode, srcId, dstId, slot)) {
|
|
LogError(LOG_NET, "call %u grant demand failed while writing to the FNE", callNumber);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
CallStreamState stream;
|
|
bool success = false;
|
|
switch (options.mode) {
|
|
case Mode::P25:
|
|
success = emitP25(network, srcId, dstId, durationMs, stream);
|
|
break;
|
|
case Mode::DMR:
|
|
success = emitDMR(network, srcId, dstId, slot, durationMs, stream);
|
|
break;
|
|
case Mode::NXDN:
|
|
success = emitNXDN(network, srcId, dstId, durationMs, stream);
|
|
break;
|
|
}
|
|
|
|
if (!success) {
|
|
LogError(LOG_NET, "call %u failed while writing to the FNE", callNumber);
|
|
return false;
|
|
}
|
|
|
|
LogInfo("Call %u/%u end: srcId = %u, dstId = %u", callNumber, totalCalls, srcId, dstId);
|
|
return true;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Program Entry Point
|
|
// ---------------------------------------------------------------------------
|
|
|
|
int main(int argc, char** argv)
|
|
{
|
|
if (argv[0] != nullptr && *argv[0] != 0)
|
|
g_progExe = std::string(argv[0]);
|
|
|
|
Options options;
|
|
if (!parseOptions(argc, argv, options))
|
|
return argc > 1 && (std::string(argv[1U]) == "--help" || std::string(argv[1U]) == "-h") ? EXIT_SUCCESS : EXIT_FAILURE;
|
|
|
|
version();
|
|
|
|
::signal(SIGINT, signalHandler);
|
|
::signal(SIGTERM, signalHandler);
|
|
if (!LogInitialise("", "callsim", 0U, options.debug ? 4U : 2U, false, false)) {
|
|
::fprintf(stderr, "callsim: failed to initialize logging\n");
|
|
return EXIT_FAILURE;
|
|
}
|
|
|
|
CallSimNetwork network(options.address, options.port, 0U, options.peerId, options.password,
|
|
true, options.debug, true, true, true, false, true, true, false, false, false, false);
|
|
network.setMetadata(options.identity, 0U, 0U, 0.0F, 0.0F, 0U, 0U, 0U, 0.0F, 0.0F, 0, "");
|
|
network.enable(true);
|
|
if (!network.open()) {
|
|
LogError(LOG_NET, "failed to open FNE connection to %s:%u", options.address.c_str(), options.port);
|
|
LogFinalise();
|
|
return EXIT_FAILURE;
|
|
}
|
|
|
|
// Network::open() starts the login exchange; continue clocking until the
|
|
// FNE accepts the peer or the bounded login interval expires.
|
|
uint32_t waited = 0U;
|
|
while (!g_stop && network.getStatus() != NET_STAT_RUNNING && waited < DEFAULT_CONNECT_TIMEOUT_MS) {
|
|
waitAndClock(network, 10U);
|
|
waited += 10U;
|
|
}
|
|
if (network.getStatus() != NET_STAT_RUNNING) {
|
|
LogError(LOG_NET, "FNE login did not complete within %u ms", DEFAULT_CONNECT_TIMEOUT_MS);
|
|
network.close();
|
|
LogFinalise();
|
|
return EXIT_FAILURE;
|
|
}
|
|
|
|
// in simultaneous mode each repetition starts one call per talkgroup; the
|
|
// calls retain independent RTP stream and sequence state while all socket
|
|
// access is serialized through the single authenticated connection
|
|
const uint32_t durationMs = options.durationSeconds * MILLISECONDS_PER_SECOND;
|
|
const uint32_t callsPerRepetition = options.simultaneous ? static_cast<uint32_t>(options.talkgroups.size()) : 1U;
|
|
const uint32_t totalCalls = options.callCount * callsPerRepetition;
|
|
bool success = true;
|
|
|
|
// iterate over the number of call repetitions, spawning and managing worker threads for each call
|
|
for (uint32_t repetition = 0U; repetition < options.callCount && !g_stop; repetition++) {
|
|
std::atomic<uint32_t> remaining(callsPerRepetition);
|
|
std::vector<uint8_t> results(callsPerRepetition, 0U);
|
|
std::vector<std::thread> workers;
|
|
workers.reserve(callsPerRepetition);
|
|
|
|
// spawn worker threads for each call in the current repetition
|
|
for (uint32_t member = 0U; member < callsPerRepetition; member++) {
|
|
const uint32_t scheduleIndex = options.simultaneous ? member : repetition;
|
|
const uint32_t callIndex = repetition * callsPerRepetition + member;
|
|
const uint32_t dstId = options.talkgroups[scheduleIndex % options.talkgroups.size()];
|
|
const uint8_t slot = options.slots[scheduleIndex % options.slots.size()];
|
|
const uint32_t srcId = options.srcId + callIndex;
|
|
|
|
workers.emplace_back([&, member, callIndex, srcId, dstId, slot]() {
|
|
results[member] = emitCall(network, options, callIndex + 1U, totalCalls, srcId, dstId, slot, durationMs) ? 1U : 0U;
|
|
remaining--;
|
|
});
|
|
}
|
|
|
|
auto lastClock = std::chrono::steady_clock::now();
|
|
while (remaining.load() > 0U) {
|
|
const auto now = std::chrono::steady_clock::now();
|
|
const uint32_t elapsed = static_cast<uint32_t>(std::chrono::duration_cast<std::chrono::milliseconds>(now - lastClock).count());
|
|
if (elapsed > 0U) {
|
|
network.clockConnection(elapsed);
|
|
lastClock = now;
|
|
}
|
|
|
|
Thread::sleep(1U);
|
|
}
|
|
|
|
// join all worker threads for the current repetition before proceeding
|
|
for (std::thread& worker : workers)
|
|
worker.join();
|
|
|
|
// determine if all calls in the current repetition were successful
|
|
success = std::find(results.begin(), results.end(), 0U) == results.end();
|
|
if (!success)
|
|
break;
|
|
|
|
if (repetition + 1U < options.callCount)
|
|
waitAndClock(network, options.gapMs);
|
|
}
|
|
|
|
network.close();
|
|
LogFinalise();
|
|
|
|
return success ? EXIT_SUCCESS : EXIT_FAILURE;
|
|
}
|