libcyberradio 22.01.24
RadioTransport.cpp
1/***************************************************************************
2 * \file RadioTransport.cpp
3 * \brief Defines an interface for transporting data to and from a radio.
4 * \author DA
5 * \author NH
6 * \author MN
7 * \copyright (c) 2017 CyberRadio Solutions, Inc. All rights reserved.
8 *
9 * \note Requires C++11 compiler support.
10 *
11 ***************************************************************************/
12
13#include "LibCyberRadio/Driver/RadioTransport.h"
14#include "LibCyberRadio/Common/Pythonesque.h"
15#include "json/json.h"
16#include <sstream>
17#include <cstdio>
18#include <sys/socket.h>
19#include <arpa/inet.h>
20#include <netdb.h>
21#include <errno.h>
22#include <string.h>
23#include <sys/select.h>
24
25
26namespace LibCyberRadio
27{
28
29 namespace Driver
30 {
31
33 bool json,
34 bool debug
35 ) :
36 Debuggable(debug, "RadioTransport"),
37 _isJson(json),
38 _tcpSocket(0),
39 _udpSocket(0),
40 _serial(NULL),
41 _httpsSession(NULL),
42 _httpsConnTestUrl(""),
43 _httpsApiCmdUrl(""),
44 _lastCmdErrInfo("")
45 {
46 this->debug("CONSTRUCTED\n");
47 }
48
50 {
51 if ( isConnected() )
52 disconnect();
53 if (_httpsSession != NULL)
54 {
55 delete _httpsSession;
56 _httpsSession = NULL;
57 }
58 if (_serial != NULL)
59 {
60 delete _serial;
61 _serial = NULL;
62 }
63 this->debug("DESTROYED\n");
64 }
65
67 Debuggable(other)
68 {
69 _isJson = other._isJson;
70 _tcpSocket = other._tcpSocket;
71 _udpSocket = other._udpSocket;
72 _serial = other._serial;
73 _httpsSession = other._httpsSession;
74 _httpsConnTestUrl = other._httpsConnTestUrl;
75 _httpsApiCmdUrl = other._httpsApiCmdUrl;
76 _lastCmdErrInfo = other._lastCmdErrInfo;
77 }
78
80 {
82 // Protect against self-assignment
83 if (this != &other)
84 {
85 _isJson = other._isJson;
86 _tcpSocket = other._tcpSocket;
87 _udpSocket = other._udpSocket;
88 _serial = other._serial;
89 _httpsSession = other._httpsSession;
90 _httpsConnTestUrl = other._httpsConnTestUrl;
91 _httpsApiCmdUrl = other._httpsApiCmdUrl;
92 _lastCmdErrInfo = other._lastCmdErrInfo;
93 }
94 return *this;
95 }
96
97 void RadioTransport::setJson( bool json )
98 {
99 this->debug("[setJson] %s\n", json);
100 _isJson = json;
101 }
102
103
105 const std::string &mode,
106 const std::string &host_or_dev,
107 const int port_or_baudrate
108 )
109 {
110 this->debug("[connect] Called; mode=\"%s\", HorD=\"%s\", PorB=%d\n", mode.c_str(), host_or_dev.c_str(), port_or_baudrate);
111 bool ret = false;
112 if (mode == "https")
113 {
114 ret = connectHttps(host_or_dev, port_or_baudrate);
115 }
116 else if (mode == "udp")
117 {
118 ret = connectUdp(host_or_dev, port_or_baudrate);
119 }
120 else if (mode == "tcp")
121 {
122 ret = connectTcp(host_or_dev, port_or_baudrate);
123 }
124 else if (mode == "tty")
125 {
126 ret = connectTty(host_or_dev, port_or_baudrate);
127 }
128 this->debug("[connect] Returning %s\n", this->debugBool(ret));
129 return ret;
130 }
131
133 {
134 this->debug("[disconnect] Called\n");
135 if (_httpsSession != NULL)
136 {
137 delete _httpsSession;
138 _httpsSession = NULL;
139 }
140 else if (_udpSocket > 0)
141 {
142 int ok = shutdown(_udpSocket, SHUT_RDWR);
143 if (ok != 0)
145 }
146 else if (_tcpSocket > 0)
147 {
148 int ok = shutdown(_tcpSocket, SHUT_RDWR);
149 if (ok != 0)
151 }
152 else if (_serial != NULL)
153 {
154 _serial->close();
155 delete _serial;
156 _serial = NULL;
157 }
158 this->debug("[disconnect] Returning\n");
159 }
160
162 {
163 bool ret = (
164 (_httpsSession != NULL) ||
165 (_serial != NULL) ||
166 (_tcpSocket > 0) ||
167 (_udpSocket > 0)
168 );
169 return ret;
170 }
171
173 const std::string &cmdString,
174 bool clearRx
175 )
176 {
177 this->debug("[sendCommand] Called; cmd=\"%s\"\n",
178 this->rawString(cmdString).c_str());
179 bool ret = false;
180 if (_httpsSession != NULL)
181 {
182 ret = sendCommandHttps(cmdString, clearRx);
183 }
184 else if (_serial != NULL)
185 {
186 ret = sendCommandTty(cmdString, clearRx);
187 }
188 else if (_udpSocket > 0)
189 {
190 ret = sendCommandUdp(cmdString, clearRx);
191 }
192 else if (_tcpSocket > 0)
193 {
194 ret = sendCommandTcp(cmdString, clearRx);
195 }
196 else
197 _lastCmdErrInfo = "Transport is not connected";
198 this->debug("[sendCommand] Returning %s\n", this->debugBool(ret));
199 return ret;
200 }
201
203 double timeout
204 )
205 {
206 this->debug("[receive] Called\n");
207 BasicStringList ret;
208 if ( _isJson )
209 {
210 ret = receiveJson(timeout);
211 }
212 else
213 {
214 ret = receiveCli(timeout);
215 }
216 this->debug("[receive] Returning %u element%s\n", ret.size(),
217 ret.size() == 1 ? "" : "s");
218 return ret;
219 }
220
222 {
223 return _lastCmdErrInfo;
224 }
225
227 const std::string &host,
228 int port
229 )
230 {
231 this->debug("[connectTcp] Called; host=\"%s\", port=%d\n", host.c_str(), port);
232 _tcpSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
233 this->debug("[connectTcp] Socket created; FD=%d\n", _tcpSocket);
234 if (_tcpSocket > 0)
235 {
236 struct hostent *hent = gethostbyname(host.c_str());
237 char buf[128];
238 memset(buf, 0, sizeof(buf));
239 inet_ntop(AF_INET, hent->h_addr_list[0], buf, sizeof(buf));
240 this->debug("[connectTcp] Host IP address: %s\n", buf);
241 struct sockaddr_in addr;
242 memset(&addr, 0, sizeof(struct sockaddr_in));
243 addr.sin_family = AF_INET;
244 memcpy(&(addr.sin_addr.s_addr), hent->h_addr_list[0], hent->h_length);
245 addr.sin_port = htons(port);
246 this->debug("[connectTcp] Connecting\n");
247 int ok = ::connect(_tcpSocket, (const sockaddr*)&addr, sizeof(struct sockaddr_in));
248 this->debug("[connectTcp] -- ok = %d\n", ok);
249 if (ok != 0)
250 _tcpSocket = 0;
251 }
252 if (_tcpSocket <= 0)
253 {
254 _tcpSocket = 0;
256 }
257 bool ret = (_tcpSocket > 0);
258 return ret;
259 }
260
262 const std::string& host,
263 int port
264 )
265 {
266 this->debug("[connectUdp] Called; host=\"%s\", port=%d\n", host.c_str(), port);
267
268 _udpSocket = socket(AF_INET, SOCK_DGRAM, 0);
269 this->debug("[connectUdp] Socket created; FD=%d\n", _udpSocket);
270 if (_udpSocket > 0)
271 {
272 struct hostent *hent = gethostbyname(host.c_str());
273 char buf[128];
274 memset(buf, 0, sizeof(buf));
275 inet_ntop(AF_INET, hent->h_addr_list[0], buf, sizeof(buf));
276 this->debug("[connectUdp] Host IP address: %s\n", buf);
277 struct sockaddr_in addr;
278 memset(&addr, 0, sizeof(struct sockaddr_in));
279 addr.sin_family = AF_INET;
280 memcpy(&(addr.sin_addr.s_addr), hent->h_addr_list[0], hent->h_length);
281 addr.sin_port = htons(port);
282 this->debug("[connectUdp] Connecting\n");
283 int ok = ::connect(_udpSocket, (const sockaddr*)&addr, sizeof(struct sockaddr_in));
284 this->debug("[connectUdp] -- ok = %d\n", ok);
285 if (ok != 0)
286 _udpSocket = 0;
287 }
288 if (_udpSocket <= 0)
289 {
290 _udpSocket = 0;
292 }
293 bool ret = (_udpSocket > 0);
294 return ret;
295 }
296
298 const std::string &host,
299 int port
300 )
301 {
302 this->debug("[connectHttps] Called; host=\"%s\", port=%d\n", host.c_str(), port);
303 bool ret = false;
304 // Attempt to establish the HTTPS session
305 if ( _httpsSession == NULL )
306 _httpsSession = new HttpsSession( /*d_debug */ false);
307 if ( _httpsSession != NULL )
308 {
309 this->debug("[connectHttps] Session initialized\n");
310 // Form connection test and API command URLs
311 std::ostringstream oss;
312 oss << "https://" << host << ":" << port << "/";
313 _httpsConnTestUrl = oss.str();
314 oss << "api/command";
315 _httpsApiCmdUrl = oss.str();
316 // Test the connection
317 this->debug("[connectHttps] Testing connection; URL=%s\n",
318 _httpsConnTestUrl.c_str());
319 ret = _httpsSession->get(_httpsConnTestUrl, false);
320 // Destroy the session object if the connection test fails
321 if (!ret)
322 {
323 _lastCmdErrInfo = _httpsSession->getLastRequestErrorInfo();
324 delete _httpsSession;
325 _httpsSession = NULL;
326 }
327 }
328 else
329 _lastCmdErrInfo = "Failed to establish HTTPS session";
330 this->debug("[connectHttps] Returning %s\n", this->debugBool(ret));
331 return ret;
332 }
333
335 const std::string& dev,
336 int baudrate
337 )
338 {
339 this->debug("[connectTty] Called; dev=\"%s\", baudrate=%d\n",
340 dev.c_str(), baudrate);
341 bool ret = false;
342 // Attempt to establish the TTY link
343 if ( _serial == NULL )
344 _serial = new ::LibCyberRadio::SerialPort(
345 dev, baudrate, 'N', 8, 1,
346 false, false, false
347 );
348 ret = _serial->open();
349 if ( ret )
350 {
351 this->debug("[connectTty] Serial link established\n");
352 // Test connection by sending an empty command
353 this->debug("[connectTty] Testing connection\n");
354 if ( sendCommand("\r\n") &&
355 (receive().size() > 0) )
356 {
357 }
358 else
359 {
360 std::ostringstream oss;
361 oss << "Connection test FAILED: ";
362 oss << _serial->getLastError();
363 _lastCmdErrInfo = oss.str();
364 ret = false;
365 }
366 }
367 else
368 {
369 std::ostringstream oss;
370 oss << "Serial link FAILED: ";
371 if ( _serial != NULL )
372 oss << _serial->getLastError();
373 else
374 oss << "Failed to establish serial link";
375 _lastCmdErrInfo = oss.str();
376 ret = false;
377 }
378 this->debug("[connectTty] Returning %s\n", this->debugBool(ret));
379 return ret;
380 }
381
383 const std::string &cmdString,
384 bool clearRx
385 )
386 {
387 this->debug("[sendCommandTcp] Called; cmd=\"%s\"\n",
388 this->rawString(cmdString).c_str());
389 bool ret = false;
390 int bytes = send(_tcpSocket, cmdString.c_str(), cmdString.length(), 0);
391 this->debug("[sendCommandTcp] -- Bytes sent: %d\n", bytes);
392 if (bytes > 0)
393 {
394 ret = true;
395 }
396 else
397 {
399 }
400 return ret;
401 }
402
404 const std::string& cmdString,
405 bool clearRx
406 )
407 {
408 this->debug("[sendCommandUdp] Called; cmd=%s\n",
409 this->rawString(cmdString).c_str());
410 bool ret = false;
411 int bytes = send(_udpSocket, cmdString.c_str(), cmdString.length(), 0);
412 this->debug("[sendCommandUdp] -- Bytes sent: %d\n", bytes);
413 if (bytes > 0)
414 {
415 ret = true;
416 }
417 else
418 {
420 }
421 return ret;
422 }
423
425 const std::string &cmdString,
426 bool clearRx
427 )
428 {
429 this->debug("[sendCommandHttps] Called; cmd=\"%s\"\n",
430 this->rawString(cmdString).c_str());
431 bool ret = false;
432 if ( _httpsSession != NULL )
433 {
434 ret = _httpsSession->post(_httpsApiCmdUrl,
435 (void*)cmdString.c_str(),
436 cmdString.length(),
437 "application/json",
438 false);
439 this->debug("[sendCommandHttps] HTTPS response code = %d\n",
440 _httpsSession->getResponseCode());
441 // If the request failed, get the reason why.
442 if ( !ret )
443 {
444 // On a failed request, the radio may have returned a JSON
445 // response containing detailed response info. If this is
446 // the case, it's not a problem at the transport level, so
447 // we actually want to proceed as if the request succeeded.
448 // This allows extended error info to be passed up to the
449 // radio handler level.
450 Json::Reader reader;
451 Json::Value jsonResponse;
452 if ( reader.parse(_httpsSession->getResponseBody(),
453 jsonResponse, false) )
454 {
455 ret = true;
456 }
457 // If the radio did not return a JSON response, then
458 // indicate why the transport action failed.
459 else
460 {
461 _lastCmdErrInfo = _httpsSession->getLastRequestErrorInfo();
462 }
463 }
464 }
465 else
466 _lastCmdErrInfo = "Transport is not connected";
467 this->debug("[sendCommandHttps] Returning %s\n", this->debugBool(ret));
468 return ret;
469 }
470
472 const std::string& cmdString,
473 bool clearRx
474 )
475 {
476 this->debug("[sendCommandTty] Called; cmd=\"%s\"\n",
477 this->rawString(cmdString).c_str());
478 bool ret = false;
479 if ( _serial->write(cmdString) )
480 {
481 this->debug("[sendCommandTty] -- Command sent\n");
482 ret = true;
483 }
484 else
485 {
486 _lastCmdErrInfo = _serial->getLastError();
487 }
488 return ret;
489 }
490
492 double timeout
493 )
494 {
495 std::deque<std::string> ret;
496 if (_httpsSession != NULL) {
497 ret = receiveJsonHttps(timeout);
498 } else {
499 ret = receiveJsonUdp(timeout);
500 }
501 return ret;
502 }
503
504 BasicStringList RadioTransport::receiveJsonUdp(
505 double timeout
506 )
507 {
508 this->debug("[receiveJsonUdp] Called; timeout=%0.1f\n", timeout);
509 BasicStringList ret;
510 std::ostringstream oss;
511 fd_set ins;
512 struct timeval tv;
513 tv.tv_sec = (long)timeout;
514 tv.tv_usec = (long)(1000000 * (timeout - (long)timeout));
515
516 char buf[1024];
517 // data available
518 memset(buf, 0, sizeof(buf));
519 recv(_udpSocket, buf, sizeof(buf), 0);
520 this->debug("[receiveJsonUdp] Received chunk: \"%s\"\n", buf);
521 oss << buf;
522 this->debug("[receiveJsonUdp] Received: \"%s\"\n",
523 this->rawString(oss.str()).c_str());
524 Json::Reader reader;
525 Json::Value root;
526 std::string t = buf;
527 bool parsingSuccessful = reader.parse( t.c_str(), root ); //parse process
528 if ( !parsingSuccessful )
529 {
530 this->debug("[receiveJsonUdp] Parsing JSON Error\n");
531 }
532 Json::FastWriter fastWriter;
533 std::string output = fastWriter.write(root);
534 ret.push_back(output);
535 return ret;
536 }
537
538
540 double timeout
541 )
542 {
543 this->debug("[receiveJsonHttps] Called; timeout=%0.1f\n", timeout);
544 BasicStringList ret;
545 // When receiving JSON over HTTPS, the session object gets the
546 // HTTPS response body while servicing the request, so all we have
547 // to do is retrieve it.
548 if ( _httpsSession != NULL )
549 {
550 this->debug("[receiveJsonHttps] HTTPS response body = \"%s\"\n",
551 _httpsSession->getResponseBody().c_str());
552 ret = Pythonesque::Split(_httpsSession->getResponseBody(), "\n");
553 }
554 else
555 _lastCmdErrInfo = "Transport is not connected";
556 return ret;
557 }
558
560 double timeout
561 )
562 {
563 std::deque<std::string> ret;
564 if (_udpSocket > 0)
565 ret = receiveCliUdp(timeout);
566 else if (_tcpSocket > 0)
567 ret = receiveCliTcp(timeout);
568 else if (_serial != NULL)
569 ret = receiveCliTty(timeout);
570 return ret;
571 }
572
574 double timeout
575 )
576 {
577 this->debug("[receiveCliTcp] Called; timeout=%0.1f\n", timeout);
578 BasicStringList ret;
579 std::ostringstream oss;
580 fd_set ins;
581 struct timeval tv;
582 tv.tv_sec = (long)timeout;
583 tv.tv_usec = (long)(1000000 * (timeout - (long)timeout));
584 int nfds;
585 char buf[1024];
586 while (true)
587 {
588 FD_ZERO(&ins);
589 FD_SET(_tcpSocket, &ins);
590 this->debug("[receiveCliTcp] Selecting\n");
591 nfds = select(_tcpSocket + 1, &ins, NULL, NULL, timeout < 0 ? NULL : &tv);
592 this->debug("[receiveCliTcp] -- nfds = %d\n", nfds);
593 if (nfds > 0)
594 {
595 // data available
596 memset(buf, 0, sizeof(buf));
597 recv(_tcpSocket, buf, sizeof(buf), 0);
598 this->debug("[receiveCliTcp] Received chunk: \"%s\"\n", buf);
599 oss << buf;
600 // check for response terminator/prompt
601 if ( oss.str().rfind('>') != std::string::npos )
602 break;
603 }
604 else if (nfds == 0)
605 {
606 // timeout
607 this->debug("[receiveCliTcp] Timeout\n");
608 _lastCmdErrInfo = "Timeout";
609 break;
610 }
611 else
612 {
613 // Error in socket select
615 this->debug("[receiveCliTcp] Socket select error: %s\n", _lastCmdErrInfo.c_str());
616 break;
617 }
618 }
619 this->debug("[receiveCliTcp] Received: \"%s\"\n",
620 this->rawString(oss.str()).c_str());
621 // Split the response into a list of non-empty strings
622 // -- Remove carriage returns and prompt character
623 std::string tmp = Pythonesque::Replace(Pythonesque::Replace(oss.str(), "\r", ""), ">", "");
624 // -- Split on newlines
625 BasicStringList tmpList = Pythonesque::Split(tmp, "\n");
626 // -- Compile the non-empty strings into a list
627 for (BasicStringList::iterator it = tmpList.begin(); it != tmpList.end(); it++)
628 {
629 tmp = Pythonesque::Rstrip(*it);
630 if ( !tmp.empty() )
631 ret.push_back(tmp);
632 }
633 this->debug("[receiveCliTcp] Returning %u elements\n", ret.size());
634 return ret;
635 }
636
638 double timeout
639 )
640 {
641 BasicStringList ret;
642 return ret;
643 }
644
646 double timeout
647 )
648 {
649 this->debug("[receiveCliTty] Called; timeout=%0.1f\n", timeout);
650 BasicStringList ret;
651 std::ostringstream oss;
652 std::string rsp = "X";
653 while ( rsp != "" )
654 {
655 rsp = _serial->read();
656 oss << rsp;
657 if ( Pythonesque::Endswith(oss.str(), ">") )
658 break;
659 }
660 this->debug("[receiveCliTty] Received: \"%s\"\n",
661 this->rawString(oss.str()).c_str());
662 // Split the response into a list of non-empty strings
663 // -- Remove carriage returns and prompt character
664 std::string tmp = Pythonesque::Replace(Pythonesque::Replace(oss.str(), "\r", ""), ">", "");
665 // -- Split on newlines
666 BasicStringList tmpList = Pythonesque::Split(tmp, "\n");
667 // -- Compile the non-empty strings into a list
668 for (BasicStringList::iterator it = tmpList.begin(); it != tmpList.end(); it++)
669 {
670 tmp = Pythonesque::Rstrip(*it);
671 if ( !tmp.empty() )
672 ret.push_back(tmp);
673 }
674 this->debug("[receiveCliTty] Returning %u elements\n", ret.size());
675 return ret;
676 }
677
679 {
680 char buf[256];
681 memset(buf, 0, sizeof(buf));
682 this->debug(strerror_r(errno, buf, sizeof(buf)));
683 _lastCmdErrInfo = buf;
684 }
685
686 } /* namespace Driver */
687
688} /* namespace LibCyberRadio */
Debuggable & operator=(const Debuggable &other)
Assignment operator for Debuggable objects.
virtual std::string rawString(const std::string &data)
Gets a "raw" string representation of a given data string.
virtual const char * debugBool(bool x)
Gets a debug output string for a Boolean value.
virtual int debug(const char *format,...)
Outputs debug information.
Debuggable(bool debug=false, const std::string &debug_name="", FILE *debug_fp=DEBUG_FP, const std::string &debug_timefmt=DEBUG_TIME_FMT)
Constructs a Debuggable object.
virtual BasicStringList receiveCliUdp(double timeout=-1)
Receives a client (AT-command-style) command response over UDP.
virtual bool sendCommandTty(const std::string &cmdString, bool clearRx=true)
Sends a command to the radio over TTY.
virtual BasicStringList receiveCliTcp(double timeout=-1)
Receives a client (AT-command-style) command response over TCP.
virtual bool connectTcp(const std::string &host, int port)
Connects to the radio using TCP.
virtual bool sendCommand(const std::string &cmdString, bool clearRx=true)
Sends a command to the radio over the transport.
virtual ~RadioTransport()
Destroys a RadioTransport object.
virtual bool sendCommandUdp(const std::string &cmdString, bool clearRx=true)
Sends a command to the radio over UDP.
virtual bool connect(const std::string &mode, const std::string &host_or_dev, const int port_or_baudrate)
Connects to the radio.
RadioTransport(bool json=false, bool debug=false)
Constructs a RadioTransport object.
virtual std::string getLastCommandErrorInfo() const
Gets the error information for the last command.
virtual bool connectUdp(const std::string &host, int port)
Connects to the radio using UDP.
virtual bool connectTty(const std::string &dev, int baudrate)
Connects to the radio using a serial link.
virtual BasicStringList receive(double timeout=-1)
Receives a command response from the radio.
virtual bool isConnected() const
Gets whether the transport is connected.
virtual BasicStringList receiveCliTty(double timeout=-1)
Receives a client (AT-command-style) command response over TTY.
virtual BasicStringList receiveCli(double timeout=-1)
Receives a client (AT-command-style) command response from the radio.
virtual bool sendCommandTcp(const std::string &cmdString, bool clearRx=true)
Sends a command to the radio over TCP.
void setJson(bool json)
Allows user to set JSON.
virtual BasicStringList receiveJson(double timeout=-1)
Receives a JSON-formatted command response from the radio.
virtual void disconnect()
Disconnects from the radio.
virtual BasicStringList receiveJsonHttps(double timeout=-1)
Receives a JSON-formatted command response from the radio using HTTPS.
virtual void translateErrno()
Translates an errno value into an error message.
virtual bool connectHttps(const std::string &host, int port)
Connects to the radio using HTTPS.
virtual bool sendCommandHttps(const std::string &cmdString, bool clearRx=true)
Sends a command to the radio over HTTPS.
RadioTransport & operator=(const RadioTransport &other)
Assignment operator for RadioTransport objects.
Class that encapsulates an HTTPS session.
static bool Endswith(const std::string &str, const std::string &suffix, int start=0, int end=INT_MAX)
Determines if the given string ends with the specified suffix.
static BasicStringList Split(const std::string &str, const std::string &sep, int maxsplit=INT_MAX)
Splits the given string into a list of string tokens.
static std::string Rstrip(const std::string &str, const std::string &chars=" \r\n\t\v\f")
Strips trailing whitespace from the given string.
static std::string Replace(const std::string &str, const std::string &oldstr, const std::string &newstr, int count=INT_MAX)
Replaces occurrences of one substring with another within the given string.
Provides programming elements for driving CRS NDR-class radios.
Defines functionality for LibCyberRadio applications.
Definition App.h:24
BASIC_LIST_CONTAINER< std::string > BasicStringList
Type representing a list of strings.
Definition BasicList.h:25