implement proper blocking operations for RPC; don't allow a RPC instance to call itself (i.e. you can't listen on port 127.0.0.1:9890 and then use req() to send an RPC to the listening instance); fix issue with RPC_PERMIT_XXX_TG not being a blocking operation; fix issue with RPC_DMR_TSCC_PAYLOAD_ACT not being a blocking operation;

pull/85/head
Bryan Biedenkapp 10 months ago
parent 81d4a40d24
commit 63c85f3781

@ -13,6 +13,7 @@
#include "common/network/RPCHeader.h"
#include "common/network/json/json.h"
#include "common/Log.h"
#include "common/Thread.h"
#include "common/Utils.h"
#include "network/RPC.h"
@ -27,6 +28,12 @@ using namespace network::frame;
// Public Class Members
// ---------------------------------------------------------------------------
#define REPLY_WAIT 200 // 200ms
// ---------------------------------------------------------------------------
// Public Class Members
// ---------------------------------------------------------------------------
/* Initializes a new instance of the RPC class. */
RPC::RPC(const std::string& address, uint16_t port, uint16_t localPort, const std::string& password, bool debug) :
@ -36,7 +43,8 @@ RPC::RPC(const std::string& address, uint16_t port, uint16_t localPort, const st
m_socket(nullptr),
m_frameQueue(nullptr),
m_password(password),
m_handlers()
m_handlers(),
m_handlerReplied()
{
assert(!address.empty());
assert(port > 0U);
@ -86,7 +94,8 @@ void RPC::clock(uint32_t ms)
}
if (m_debug) {
LogDebugEx(LOG_NET, "RPC::clock()", "RPC, func = $%04X, messageLength = %u", rpcHeader.getFunction(), rpcHeader.getMessageLength());
LogDebugEx(LOG_NET, "RPC::clock()", "received RPC, %s:%u, func = $%04X, messageLength = %u",
udp::Socket::address(address).c_str(), udp::Socket::port(address), rpcHeader.getFunction(), rpcHeader.getMessageLength());
}
// copy message
@ -125,10 +134,15 @@ void RPC::clock(uint32_t ms)
// find RPC function callback
if (m_handlers.find(rpcHeader.getFunction()) != m_handlers.end()) {
bool isReply = (rpcHeader.getFunction() & RPC_REPLY_FUNC) == RPC_REPLY_FUNC;
if (isReply) {
m_handlerReplied[rpcHeader.getFunction()] = true;
}
m_handlers[rpcHeader.getFunction()](request, response);
// remove the reply handler (these should be temporary)
if ((rpcHeader.getFunction() & RPC_REPLY_FUNC) == RPC_REPLY_FUNC) {
if (isReply) {
m_handlers.erase(rpcHeader.getFunction());
} else {
reply(rpcHeader.getFunction(), response, address, addrLen);
@ -136,6 +150,8 @@ void RPC::clock(uint32_t ms)
} else {
bool isReply = (rpcHeader.getFunction() & RPC_REPLY_FUNC) == RPC_REPLY_FUNC;
if (isReply) {
m_handlerReplied[rpcHeader.getFunction()] = true;
if (!request["status"].is<int>()) {
::LogError(LOG_NET, "RPC %s:%u, invalid RPC response", udp::Socket::address(address).c_str(), udp::Socket::port(address));
return;
@ -157,12 +173,13 @@ void RPC::clock(uint32_t ms)
/* Writes an RPC request to the network. */
bool RPC::req(uint16_t func, const json::object& request, RPCType reply, std::string address, uint16_t port)
bool RPC::req(uint16_t func, const json::object& request, RPCType reply, std::string address, uint16_t port,
bool blocking)
{
sockaddr_storage addr;
uint32_t addrLen = 0U;
if (udp::Socket::lookup(address, port, addr, addrLen) == 0) {
return req(func, request, reply, addr, addrLen);
return req(func, request, reply, addr, addrLen, blocking);
}
return false;
@ -170,11 +187,23 @@ bool RPC::req(uint16_t func, const json::object& request, RPCType reply, std::st
/* Writes an RPC request to the network. */
bool RPC::req(uint16_t func, const json::object& request, RPCType reply, sockaddr_storage& address, uint32_t addrLen)
bool RPC::req(uint16_t func, const json::object& request, RPCType reply, sockaddr_storage& address, uint32_t addrLen,
bool blocking)
{
json::value v = json::value(request);
std::string json = v.serialize();
if (m_debug) {
LogDebugEx(LOG_NET, "RPC::req()", "sending RPC, %s:%u, func = $%04X, messageLength = %u",
udp::Socket::address(address).c_str(), udp::Socket::port(address), func, json.length() + 1U);
}
// make sure we're not trying to send an RPC request to ourselves
if (m_address == udp::Socket::address(address) && m_port == udp::Socket::port(address)) {
LogError(LOG_NET, "RPC, cowardly refusing to send RPC to ourselves");
return false;
}
// generate RPC header
RPCHeader header = RPCHeader();
header.setFunction(func & 0x3FFFU);
@ -201,10 +230,46 @@ bool RPC::req(uint16_t func, const json::object& request, RPCType reply, sockadd
::memcpy(buffer + 8U, message, json.length() + 1U);
// install reply handler
if (reply != nullptr)
if (reply != nullptr) {
m_handlers[func | RPC_REPLY_FUNC] = reply;
m_handlerReplied[func | RPC_REPLY_FUNC] = false;
}
return m_frameQueue->write(buffer, json.length() + 9U, address, addrLen);
bool ret = m_frameQueue->write(buffer, json.length() + 9U, address, addrLen);
if (!ret)
return false;
else {
// are we blocking return until a reply is received?
if (blocking) {
// we only block for up to 200ms -- after we we treat the call as failed and return
int timeout = REPLY_WAIT;
while (timeout > 0) {
if (m_handlerReplied.find(func | RPC_REPLY_FUNC) != m_handlerReplied.end()) {
if (m_handlerReplied[func | RPC_REPLY_FUNC]) {
m_handlerReplied[func | RPC_REPLY_FUNC] = false;
break;
}
}
if (m_debug)
LogDebugEx(LOG_HOST, "RPC::req()", "blocking = %u, to = %d", blocking, timeout);
timeout--;
Thread::sleep(1);
}
if (timeout == 0) {
return false;
}
return true;
}
else {
return true;
}
}
return false;
}
/* Helper to generate a default response error payload. */

@ -80,22 +80,26 @@ namespace network
/**
* @brief Writes an RPC request to the network.
* @note When using blocking, execution will only be blocked up to a timeout period maximum of 200ms.
* @param request JSON content body for request.
* @param reply Reply handler.
* @param address IP address to write data to.
* @param port Port number to write data to.
* @param blocking Flag indicating this RPC call should block while waiting for a reply.
* @returns bool True, if message was written, otherwise false.
*/
bool req(uint16_t func, const json::object& request, RPCType reply, std::string address, uint16_t port);
bool req(uint16_t func, const json::object& request, RPCType reply, std::string address, uint16_t port, bool blocking = false);
/**
* @brief Writes an RPC request to the network.
* @note When using blocking, execution will only be blocked up to a timeout period maximum of 200ms.
* @param request JSON content body for request.
* @param reply Reply handler.
* @param address IP address to write data to.
* @param addrLen
* @param blocking Flag indicating this RPC call should block while waiting for a reply.
* @returns bool True, if message was written, otherwise false.
*/
bool req(uint16_t func, const json::object& request, RPCType reply, sockaddr_storage& address, uint32_t addrLen);
bool req(uint16_t func, const json::object& request, RPCType reply, sockaddr_storage& address, uint32_t addrLen, bool blocking = false);
/**
* @brief Helper to generate a default response error payload.
@ -140,6 +144,7 @@ namespace network
std::string m_password;
std::map<uint16_t, RPCType> m_handlers;
std::map<uint16_t, bool> m_handlerReplied;
/**
* @brief Writes an RPC reply to the network.

@ -959,31 +959,36 @@ bool ControlSignaling::writeRF_CSBK_Grant(uint32_t srcId, uint32_t dstId, uint8_
req["dstId"].set<uint32_t>(dstId);
req["slot"].set<uint8_t>(slot);
bool requestFailed = false;
g_RPC->req(RPC_PERMIT_DMR_TG, req, [=, &requestFailed](json::object& req, json::object& reply) {
// send blocking RPC request
bool requestFailed = !g_RPC->req(RPC_PERMIT_DMR_TG, req, [=, &requestFailed](json::object& req, json::object& reply) {
if (!req["status"].is<int>()) {
return;
}
int status = req["status"].get<int>();
if (status != network::RPC::OK) {
::LogError((net) ? LOG_NET : LOG_RF, "DMR Slot %u, CSBK, RAND (Random Access), failed to permit TG for use, chNo = %u, slot = %u", tscc->m_slotNo, chNo, slot);
if (req["message"].is<std::string>()) {
std::string retMsg = req["message"].get<std::string>();
::LogError((net) ? LOG_NET : LOG_RF, "DMR Slot %u, RPC failed, %s", tscc->m_slotNo, retMsg.c_str());
}
tscc->m_affiliations->releaseGrant(dstId, false);
if (!net) {
writeRF_CSBK_ACK_RSP(srcId, ReasonCode::TS_DENY_RSN_TGT_BUSY, (grp) ? 1U : 0U);
m_slot->m_rfState = RS_RF_REJECTED;
}
requestFailed = true;
} else {
requestFailed = false;
}
}, voiceChData.address(), voiceChData.port(), true);
// if the request failed block grant
if (requestFailed) {
::LogError((net) ? LOG_NET : LOG_RF, "DMR Slot %u, CSBK, RAND (Random Access), failed to permit TG for use, chNo = %u, slot = %u", tscc->m_slotNo, chNo, slot);
tscc->m_affiliations->releaseGrant(dstId, false);
if (!net) {
writeRF_CSBK_ACK_RSP(srcId, ReasonCode::TS_DENY_RSN_TGT_BUSY, (grp) ? 1U : 0U);
m_slot->m_rfState = RS_RF_REJECTED;
}
}, voiceChData.address(), voiceChData.port());
if (requestFailed)
return false;
}
}
else {
::LogError(LOG_RF, "DMR Slot %u, CSBK, RAND (Random Access), failed to permit TG for use, chNo = %u, slot = %u", tscc->m_slotNo, chNo, slot);
@ -1023,7 +1028,7 @@ bool ControlSignaling::writeRF_CSBK_Grant(uint32_t srcId, uint32_t dstId, uint8_
bool voice = true;
req["voice"].set<bool>(voice);
g_RPC->req(RPC_DMR_TSCC_PAYLOAD_ACT, req, nullptr, voiceChData.address(), voiceChData.port());
g_RPC->req(RPC_DMR_TSCC_PAYLOAD_ACT, req, nullptr, voiceChData.address(), voiceChData.port(), true);
}
else {
::LogError(LOG_RF, "DMR Slot %u, CSBK, RAND (Random Access), failed to activate payload channel, chNo = %u, slot = %u", tscc->m_slotNo, chNo, slot);
@ -1047,31 +1052,36 @@ bool ControlSignaling::writeRF_CSBK_Grant(uint32_t srcId, uint32_t dstId, uint8_
req["dstId"].set<uint32_t>(dstId);
req["slot"].set<uint8_t>(slot);
bool requestFailed = false;
g_RPC->req(RPC_PERMIT_DMR_TG, req, [=, &requestFailed](json::object& req, json::object& reply) {
// send blocking RPC request
bool requestFailed = !g_RPC->req(RPC_PERMIT_DMR_TG, req, [=, &requestFailed](json::object& req, json::object& reply) {
if (!req["status"].is<int>()) {
return;
}
int status = req["status"].get<int>();
if (status != network::RPC::OK) {
::LogError((net) ? LOG_NET : LOG_RF, "DMR Slot %u, CSBK, RAND (Random Access), failed to permit TG for use, chNo = %u, slot = %u", tscc->m_slotNo, chNo, slot);
if (req["message"].is<std::string>()) {
std::string retMsg = req["message"].get<std::string>();
::LogError((net) ? LOG_NET : LOG_RF, "DMR Slot %u, RPC failed, %s", tscc->m_slotNo, retMsg.c_str());
}
tscc->m_affiliations->releaseGrant(dstId, false);
if (!net) {
writeRF_CSBK_ACK_RSP(srcId, ReasonCode::TS_DENY_RSN_TGT_BUSY, (grp) ? 1U : 0U);
m_slot->m_rfState = RS_RF_REJECTED;
}
requestFailed = true;
} else {
requestFailed = false;
}
}, voiceChData.address(), voiceChData.port(), true);
// if the request failed block grant
if (requestFailed) {
::LogError((net) ? LOG_NET : LOG_RF, "DMR Slot %u, CSBK, RAND (Random Access), failed to permit TG for use, chNo = %u, slot = %u", tscc->m_slotNo, chNo, slot);
tscc->m_affiliations->releaseGrant(dstId, false);
if (!net) {
writeRF_CSBK_ACK_RSP(srcId, ReasonCode::TS_DENY_RSN_TGT_BUSY, (grp) ? 1U : 0U);
m_slot->m_rfState = RS_RF_REJECTED;
}
}, voiceChData.address(), voiceChData.port());
if (requestFailed)
return false;
}
}
else {
::LogError(LOG_RF, "DMR Slot %u, CSBK, RAND (Random Access), failed to permit TG for use, chNo = %u, slot = %u", tscc->m_slotNo, chNo, slot);
@ -1109,7 +1119,7 @@ bool ControlSignaling::writeRF_CSBK_Grant(uint32_t srcId, uint32_t dstId, uint8_
bool voice = true;
req["voice"].set<bool>(voice);
g_RPC->req(RPC_DMR_TSCC_PAYLOAD_ACT, req, nullptr, voiceChData.address(), voiceChData.port());
g_RPC->req(RPC_DMR_TSCC_PAYLOAD_ACT, req, nullptr, voiceChData.address(), voiceChData.port(), true);
}
else {
::LogError(LOG_RF, "DMR Slot %u, CSBK, RAND (Random Access), failed to activate payload channel, chNo = %u, slot = %u", tscc->m_slotNo, chNo, slot);
@ -1261,7 +1271,7 @@ bool ControlSignaling::writeRF_CSBK_Data_Grant(uint32_t srcId, uint32_t dstId, u
bool voice = false;
req["voice"].set<bool>(voice);
g_RPC->req(RPC_DMR_TSCC_PAYLOAD_ACT, req, nullptr, voiceChData.address(), voiceChData.port());
g_RPC->req(RPC_DMR_TSCC_PAYLOAD_ACT, req, nullptr, voiceChData.address(), voiceChData.port(), true);
}
else {
::LogError(LOG_RF, "DMR Slot %u, CSBK, RAND (Random Access), failed to activate payload channel, chNo = %u, slot = %u", tscc->m_slotNo, chNo, slot);
@ -1307,7 +1317,7 @@ bool ControlSignaling::writeRF_CSBK_Data_Grant(uint32_t srcId, uint32_t dstId, u
bool voice = false;
req["voice"].set<bool>(voice);
g_RPC->req(RPC_DMR_TSCC_PAYLOAD_ACT, req, nullptr, voiceChData.address(), voiceChData.port());
g_RPC->req(RPC_DMR_TSCC_PAYLOAD_ACT, req, nullptr, voiceChData.address(), voiceChData.port(), true);
}
else {
::LogError(LOG_RF, "DMR Slot %u, CSBK, RAND (Random Access), failed to activate payload channel, chNo = %u, slot = %u", tscc->m_slotNo, chNo, slot);

@ -568,32 +568,36 @@ bool ControlSignaling::writeRF_Message_Grant(uint32_t srcId, uint32_t dstId, uin
json::object req = json::object();
req["dstId"].set<uint32_t>(dstId);
bool requestFailed = false;
std::string rcchStr = rcch->toString().c_str();
g_RPC->req(RPC_PERMIT_NXDN_TG, req, [=, &requestFailed, &rcchStr](json::object& req, json::object& reply) {
// send blocking RPC request
bool requestFailed = !g_RPC->req(RPC_PERMIT_NXDN_TG, req, [=, &requestFailed](json::object& req, json::object& reply) {
if (!req["status"].is<int>()) {
return;
}
int status = req["status"].get<int>();
if (status != network::RPC::OK) {
::LogError((net) ? LOG_NET : LOG_RF, "NXDN, %s, failed to permit TG for use, chNo = %u", rcchStr.c_str(), chNo);
if (req["message"].is<std::string>()) {
std::string retMsg = req["message"].get<std::string>();
::LogError((net) ? LOG_NET : LOG_RF, "NXDN, RPC failed, %s", retMsg.c_str());
}
m_nxdn->m_affiliations.releaseGrant(dstId, false);
if (!net) {
writeRF_Message_Deny(0U, srcId, CauseResponse::VD_QUE_GRP_BUSY, MessageType::RTCH_VCALL);
m_nxdn->m_rfState = RS_RF_REJECTED;
}
requestFailed = true;
} else {
requestFailed = false;
}
}, voiceChData.address(), voiceChData.port(), true);
// if the request failed block grant
if (requestFailed) {
::LogError((net) ? LOG_NET : LOG_RF, "NXDN, %s, failed to permit TG for use, chNo = %u", rcch->toString().c_str(), chNo);
m_nxdn->m_affiliations.releaseGrant(dstId, false);
if (!net) {
writeRF_Message_Deny(0U, srcId, CauseResponse::VD_QUE_GRP_BUSY, MessageType::RTCH_VCALL);
m_nxdn->m_rfState = RS_RF_REJECTED;
}
}, voiceChData.address(), voiceChData.port());
if (requestFailed)
return false;
}
}
else {
::LogError((net) ? LOG_NET : LOG_RF, "NXDN, %s, failed to permit TG for use, chNo = %u", rcch->toString().c_str(), chNo);

@ -2295,31 +2295,36 @@ bool ControlSignaling::writeRF_TSDU_Grant(uint32_t srcId, uint32_t dstId, uint8_
json::object req = json::object();
req["dstId"].set<uint32_t>(dstId);
bool requestFailed = false;
g_RPC->req(RPC_PERMIT_P25_TG, req, [=, &requestFailed](json::object& req, json::object& reply) {
// send blocking RPC request
bool requestFailed = !g_RPC->req(RPC_PERMIT_P25_TG, req, [=, &requestFailed](json::object& req, json::object& reply) {
if (!req["status"].is<int>()) {
return;
}
int status = req["status"].get<int>();
if (status != network::RPC::OK) {
::LogError((net) ? LOG_NET : LOG_RF, P25_TSDU_STR ", TSBKO, IOSP_GRP_VCH (Group Voice Channel Request), failed to permit TG for use, chNo = %u", chNo);
if (req["message"].is<std::string>()) {
std::string retMsg = req["message"].get<std::string>();
::LogError((net) ? LOG_NET : LOG_RF, "P25, RPC failed, %s", retMsg.c_str());
}
m_p25->m_affiliations.releaseGrant(dstId, false);
if (!net) {
writeRF_TSDU_Deny(srcId, dstId, ReasonCode::DENY_PTT_BONK, (grp) ? TSBKO::IOSP_GRP_VCH : TSBKO::IOSP_UU_VCH, grp, true);
m_p25->m_rfState = RS_RF_REJECTED;
}
requestFailed = true;
} else {
requestFailed = false;
}
}, voiceChData.address(), voiceChData.port(), true);
// if the request failed block grant
if (requestFailed) {
::LogError((net) ? LOG_NET : LOG_RF, P25_TSDU_STR ", TSBKO, IOSP_GRP_VCH (Group Voice Channel Request), failed to permit TG for use, chNo = %u", chNo);
m_p25->m_affiliations.releaseGrant(dstId, false);
if (!net) {
writeRF_TSDU_Deny(srcId, dstId, ReasonCode::DENY_PTT_BONK, (grp) ? TSBKO::IOSP_GRP_VCH : TSBKO::IOSP_UU_VCH, grp, true);
m_p25->m_rfState = RS_RF_REJECTED;
}
}, voiceChData.address(), voiceChData.port());
if (requestFailed)
return false;
}
}
else {
::LogError((net) ? LOG_NET : LOG_RF, P25_TSDU_STR ", TSBKO, IOSP_GRP_VCH (Group Voice Channel Request), failed to permit TG for use, chNo = %u", chNo);
@ -2360,31 +2365,36 @@ bool ControlSignaling::writeRF_TSDU_Grant(uint32_t srcId, uint32_t dstId, uint8_
json::object req = json::object();
req["dstId"].set<uint32_t>(dstId);
bool requestFailed = false;
g_RPC->req(RPC_PERMIT_P25_TG, req, [=, &requestFailed](json::object& req, json::object& reply) {
// send blocking RPC request
bool requestFailed = !g_RPC->req(RPC_PERMIT_P25_TG, req, [=, &requestFailed](json::object& req, json::object& reply) {
if (!req["status"].is<int>()) {
return;
}
int status = req["status"].get<int>();
if (status != network::RPC::OK) {
::LogError((net) ? LOG_NET : LOG_RF, P25_TSDU_STR ", TSBKO, IOSP_UU_VCH (Unit-to-Unit Voice Channel Request), failed to permit TG for use, chNo = %u", chNo);
if (req["message"].is<std::string>()) {
std::string retMsg = req["message"].get<std::string>();
::LogError((net) ? LOG_NET : LOG_RF, "P25, RPC failed, %s", retMsg.c_str());
}
m_p25->m_affiliations.releaseGrant(dstId, false);
if (!net) {
writeRF_TSDU_Deny(srcId, dstId, ReasonCode::DENY_PTT_BONK, (grp) ? TSBKO::IOSP_GRP_VCH : TSBKO::IOSP_UU_VCH, grp, true);
m_p25->m_rfState = RS_RF_REJECTED;
}
requestFailed = true;
} else {
requestFailed = false;
}
}, voiceChData.address(), voiceChData.port(), true);
// if the request failed block grant
if (requestFailed) {
::LogError((net) ? LOG_NET : LOG_RF, P25_TSDU_STR ", TSBKO, IOSP_UU_VCH (Unit-to-Unit Voice Channel Request), failed to permit TG for use, chNo = %u", chNo);
m_p25->m_affiliations.releaseGrant(dstId, false);
if (!net) {
writeRF_TSDU_Deny(srcId, dstId, ReasonCode::DENY_PTT_BONK, (grp) ? TSBKO::IOSP_GRP_VCH : TSBKO::IOSP_UU_VCH, grp, true);
m_p25->m_rfState = RS_RF_REJECTED;
}
}, voiceChData.address(), voiceChData.port());
if (requestFailed)
return false;
}
}
else {
::LogError((net) ? LOG_NET : LOG_RF, P25_TSDU_STR ", TSBKO, IOSP_UU_VCH (Unit-to-Unit Voice Channel Request), failed to permit TG for use, chNo = %u", chNo);
@ -2573,29 +2583,34 @@ bool ControlSignaling::writeRF_TSDU_SNDCP_Grant(uint32_t srcId, bool skip, uint3
bool dataCh = true;
req["dataPermit"].set<bool>(dataCh);
bool requestFailed = false;
g_RPC->req(RPC_PERMIT_P25_TG, req, [=, &requestFailed](json::object& req, json::object& reply) {
// send blocking RPC request
bool requestFailed = !g_RPC->req(RPC_PERMIT_P25_TG, req, [=, &requestFailed](json::object& req, json::object& reply) {
if (!req["status"].is<int>()) {
return;
}
int status = req["status"].get<int>();
if (status != network::RPC::OK) {
::LogError(LOG_RF, P25_TSDU_STR ", TSBKO, ISP_SNDCP_CH_REQ (SNDCP Data Channel Request), failed to permit for use, chNo = %u", chNo);
if (req["message"].is<std::string>()) {
std::string retMsg = req["message"].get<std::string>();
::LogError(LOG_RF, "P25, RPC failed, %s", retMsg.c_str());
}
m_p25->m_affiliations.releaseGrant(srcId, false);
writeRF_TSDU_Deny(srcId, srcId, ReasonCode::DENY_PTT_BONK, TSBKO::ISP_SNDCP_CH_REQ, false, true);
m_p25->m_rfState = RS_RF_REJECTED;
requestFailed = true;
} else {
requestFailed = false;
}
}, voiceChData.address(), voiceChData.port());
}, voiceChData.address(), voiceChData.port(), true);
// if the request failed block grant
if (requestFailed) {
::LogError(LOG_RF, P25_TSDU_STR ", TSBKO, ISP_SNDCP_CH_REQ (SNDCP Data Channel Request), failed to permit for use, chNo = %u", chNo);
m_p25->m_affiliations.releaseGrant(srcId, false);
writeRF_TSDU_Deny(srcId, srcId, ReasonCode::DENY_PTT_BONK, TSBKO::ISP_SNDCP_CH_REQ, false, true);
m_p25->m_rfState = RS_RF_REJECTED;
if (requestFailed)
return false;
}
}
else {
::LogError(LOG_RF, P25_TSDU_STR ", TSBKO, ISP_SNDCP_CH_REQ (SNDCP Data Channel Request), failed to permit for use, chNo = %u", chNo);

Loading…
Cancel
Save

Powered by TurnKey Linux.