/* * Copyright (C) 2018-2019 by Thomas A. Early N7TAE * * CQnetITAP::GetITAPData() is based on some code that is... * Copyright (C) 2011-2015,2018 by Jonathan Naylor G4KLX * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "QnetITAP.h" #include "QnetTypeDefs.h" #include "QnetConfigure.h" #include "Timer.h" #define ITAP_VERSION "QnetITAP-2.1.3" std::atomic CQnetITAP::keep_running(true); CQnetITAP::CQnetITAP(int mod) : assigned_module(mod) { } CQnetITAP::~CQnetITAP() { } bool CQnetITAP::Initialize(const char *cfgfile) { if (ReadConfig(cfgfile)) return true; struct sigaction act; act.sa_handler = &CQnetITAP::SignalCatch; sigemptyset(&act.sa_mask); if (sigaction(SIGTERM, &act, 0) != 0) { printf("sigaction-TERM failed, error=%d\n", errno); return true; } if (sigaction(SIGHUP, &act, 0) != 0) { printf("sigaction-HUP failed, error=%d\n", errno); return true; } if (sigaction(SIGINT, &act, 0) != 0) { printf("sigaction-INT failed, error=%d\n", errno); return true; } Modem2Gate.SetUp(modem2gate.c_str()); if (Gate2Modem.Open(gate2modem.c_str())) return true; return false; } int CQnetITAP::OpenITAP() { int fd = open(ITAP_DEVICE.c_str(), O_RDWR | O_NOCTTY | O_NDELAY, 0); if (fd < 0) { printf("Failed to open device [%s], error=%d, message=%s\n", ITAP_DEVICE.c_str(), errno, strerror(errno)); return -1; } if (isatty(fd) == 0) { printf("Device %s is not a tty device\n", ITAP_DEVICE.c_str()); close(fd); return -1; } static termios t; if (tcgetattr(fd, &t) < 0) { printf("tcgetattr failed for %s, error=%d, message-%s\n", ITAP_DEVICE.c_str(), errno, strerror(errno)); close(fd); return -1; } t.c_lflag &= ~(ECHO | ECHOE | ICANON | IEXTEN | ISIG); t.c_iflag &= ~(BRKINT | ICRNL | INPCK | ISTRIP | IXON | IXOFF | IXANY); t.c_cflag &= ~(CSIZE | CSTOPB | PARENB | CRTSCTS); t.c_cflag |= CS8; t.c_oflag &= ~(OPOST); t.c_cc[VMIN] = 0; t.c_cc[VTIME] = 10; cfsetospeed(&t, B38400); cfsetispeed(&t, B38400); if (tcsetattr(fd, TCSANOW, &t) < 0) { printf("tcsetattr failed for %s, error=%dm message=%s\n", ITAP_DEVICE.c_str(), errno, strerror(errno)); close(fd); return -1; } return fd; } REPLY_TYPE CQnetITAP::GetITAPData(unsigned char *buf) { // Some methods adapted from Jonathan G4KLX's CIcomController::GetResponse() // and CSerialController::read() const unsigned char ack_header[3] = { 0x03U, 0x11U, 0x0U }; unsigned char ack_voice[4] = { 0x04U, 0x13U, 0x0U, 0x0U }; // Get the buffer size or nothing at all int ret = read(serfd, buf, 1U); if (ret < 0) { printf("Error when reading first byte from the Icom radio %d: %s", errno, strerror(errno)); return RT_ERROR; } if (ret == 0) return RT_TIMEOUT; if (buf[0U] == 0xFFU) return RT_TIMEOUT; unsigned int length = buf[0U]; if (length >= 100U) { printf("Invalid data received from the Icom radio, length=%d\n", length); return RT_ERROR; } unsigned int offset = 1U; while (offset < length) { ret = read(serfd, buf + offset, length - offset); if (ret<0 && errno!=EAGAIN) { printf("Error when reading buffer from the Icom radio %d: %s\n", errno, strerror(errno)); return RT_ERROR; } if (ret > 0) offset += ret; } switch (buf[1U]) { case 0x03U: return RT_PONG; case 0x10U: SendTo(ack_header); return RT_HEADER; case 0x12U: ack_voice[2] = buf[2]; SendTo(ack_voice); return RT_DATA; case 0x21U: if (acknowledged) { fprintf(stderr, "ERROR: Header already acknowledged!\n"); } else { if (0x0U == buf[2]) acknowledged = true; } return RT_HEADER_ACK; case 0x23U: if (acknowledged) { fprintf(stderr, "ERROR: voice frame %d already acknowledged!\n", (int)buf[2]); } else { if (0x0U == buf[3]) acknowledged = true; } return RT_DATA_ACK; default: return RT_UNKNOWN; } } void CQnetITAP::Run(const char *cfgfile) { if (Initialize(cfgfile)) return; serfd = OpenITAP(); if (serfd < 0) return; int ug2m = Gate2Modem.GetFD(); printf("gate2modem=%d, serial=%d\n", ug2m, serfd); keep_running = true; unsigned poll_counter = 0; bool is_alive = false; acknowledged = true; CTimer lastdata; while (keep_running) { fd_set readfds; FD_ZERO(&readfds); FD_SET(serfd, &readfds); FD_SET(ug2m, &readfds); int maxfs = (serfd > ug2m) ? serfd : ug2m; struct timeval tv; tv.tv_sec = (poll_counter >= 18) ? 1 : 0; tv.tv_usec = (poll_counter >= 18) ? 0 : 100000; // don't care about writefds and exceptfds: // and we'll wait for 100 ms or 1 s, depending on ; int ret = select(maxfs+1, &readfds, NULL, NULL, &tv); if (ret < 0) { printf("ERROR: Run: select returned err=%d, %s\n", errno, strerror(errno)); break; } // check for a dead or disconnected radio double deadtime = lastdata.time(); if (10.0 < deadtime) { printf("no activity from radio for 10 sec. Exiting...\n"); break; } if (0 == ret) { // nothing to read, so do the polling or pinging if (poll_counter++ < 18) { const unsigned char poll[2] = { 0xffu, 0xffu }; SendTo(poll); } else { const unsigned char ping[3] = { 0x02u, 0x02u }; SendTo(ping); } continue; } // there is something to read! unsigned char buf[100]; if (keep_running && FD_ISSET(serfd, &readfds)) { lastdata.start(); switch (GetITAPData(buf)) { case RT_ERROR: keep_running = false; break; case RT_DATA: case RT_HEADER: if (ProcessITAP(buf)) keep_running = false; break; case RT_PONG: if (! is_alive) { printf("Icom Radio is connected.\n"); is_alive = true; struct stat sbuf; if (stat(FILE_QNVOICE_FILE.c_str(), &sbuf)) { // yes, there is no FILE_QNVOICE_FILE, so create it FILE *fp = fopen(FILE_QNVOICE_FILE.c_str(), "w"); if (fp) { fprintf(fp, "%c_radio_connected.dat_WELCOME_TO_QUADNET", RPTR_MOD); fclose(fp); } else fprintf(stderr, "could not open %s\n", FILE_QNVOICE_FILE.c_str()); } } break; default: break; } FD_CLR(serfd, &readfds); } if (keep_running && FD_ISSET(ug2m, &readfds)) { ssize_t len = Gate2Modem.Read(buf, 100); if (len < 0) { printf("ERROR: Run: recvfrom(gsock) returned error %d, %s\n", errno, strerror(errno)); break; } if (0 == memcmp(buf, "DSVT", 4)) { lastdata.start(); // got a packet, reset the timer //printf("read %d bytes from QnetGateway\n", (int)len); if (ProcessGateway(len, buf)) break; } FD_CLR(ug2m, &readfds); } // send queued frames if (keep_running && acknowledged) { if (! queue.empty()) { CFrame frame = queue.front(); queue.pop(); SendTo(frame.data()); acknowledged = false; } } } close(serfd); Gate2Modem.Close(); } int CQnetITAP::SendTo(const unsigned char *buf) { ssize_t n; unsigned int ptr = 0; unsigned int length = (0xffu == buf[0]) ? 2 : buf[0]; while (ptr < length) { n = write(serfd, buf + ptr, length - ptr); if (n < 0) { if (EAGAIN != errno) { printf("Error %d writing to dvap, message=%s\n", errno, strerror(errno)); return -1; } } ptr += n; } n = 0; // send an ending 0xffu while (0 == n) { const unsigned char push = 0xffu; n = write(serfd, &push, 1); if (n < 0) { if (EAGAIN != errno) { printf("Error %d writing to dvap, message=%s\n", errno, strerror(errno)); return -1; } } } return length; } bool CQnetITAP::ProcessGateway(const int len, const unsigned char *raw) { static unsigned char counter = 0; if (27==len || 56==len) { //here is dstar data SDSVT dsvt; memcpy(dsvt.title, raw, len); // transfer raw data to SDSVT struct SITAP itap; // destination if (56 == len) { // write a Header packet counter = 0; itap.length = 41U; itap.type = 0x20; memcpy(itap.header.flag, dsvt.hdr.flag, 3); if (RPTR_MOD == dsvt.hdr.rpt2[7]) { memcpy(itap.header.r1, dsvt.hdr.rpt2, 8); memcpy(itap.header.r2, dsvt.hdr.rpt1, 8); } else { memcpy(itap.header.r1, dsvt.hdr.rpt1, 8); memcpy(itap.header.r2, dsvt.hdr.rpt2, 8); } memcpy(itap.header.ur, dsvt.hdr.urcall, 8); memcpy(itap.header.my, dsvt.hdr.mycall, 8); memcpy(itap.header.nm, dsvt.hdr.sfx, 4); if (log_qso) printf("Queued ITAP to %s ur=%.8s r1=%.8s r2=%.8s my=%.8s/%.4s\n", ITAP_DEVICE.c_str(), itap.header.ur, itap.header.r1, itap.header.r2, itap.header.my, itap.header.nm); } else { // write an AMBE packet itap.length = 16U; itap.type = 0x22U; itap.voice.counter = counter++; itap.voice.sequence = dsvt.ctrl; if (log_qso && (dsvt.ctrl & 0x40)) printf("Queued ITAP end of stream\n"); if ((dsvt.ctrl & ~0x40U) > 20) printf("DEBUG: ProcessGateway: unexpected voice sequence number %d\n", itap.voice.sequence); memcpy(itap.voice.ambe, dsvt.vasd.voice, 12); } queue.push(CFrame(&itap.length)); } else printf("DEBUG: ProcessGateway: unusual packet size read len=%d\n", len); return false; } bool CQnetITAP::ProcessITAP(const unsigned char *buf) { static short stream_id = 0U; SITAP itap; unsigned int len = (0x10U == buf[1]) ? 41 : 16; memcpy(&itap.length, buf, len); // transfer raw data to SITAP struct // create a stream id if this is a header if (41 == len) stream_id = random.NewStreamID(); SDSVT dsvt; // destination // sets most of the params memcpy(dsvt.title, "DSVT", 4); dsvt.config = (len==41) ? 0x10U : 0x20U; memset(dsvt.flaga, 0U, 3U); dsvt.id = 0x20; dsvt.flagb[0] = 0x0; dsvt.flagb[1] = 0x1; dsvt.flagb[2] = ('B'==RPTR_MOD) ? 0x1 : (('C'==RPTR_MOD) ? 0x2 : 0x3); dsvt.streamid = htons(stream_id); if (41 == len) { // header dsvt.ctrl = 0x80; memcpy(dsvt.hdr.flag, itap.header.flag, 3); ////////////////// Terminal or Access ///////////////////////// if (0 == memcmp(itap.header.r1, "DIRECT", 6)) { // Terminal Mode! memcpy(dsvt.hdr.rpt1, RPTR.c_str(), 7); // build r1 dsvt.hdr.rpt1[7] = RPTR_MOD; // with module memcpy(dsvt.hdr.rpt2, RPTR.c_str(), 7); // build r2 dsvt.hdr.rpt2[7] = 'G'; // with gateway if (' '==itap.header.ur[2] && ' '!=itap.header.ur[0]) { // it's a command because it has as space in the 3rd position, we have to right-justify it! // Terminal Mode left justifies short commands. memset(dsvt.hdr.urcall, ' ', 8); // first file ur with spaces if (' ' == itap.header.ur[1]) dsvt.hdr.urcall[7] = itap.header.ur[0]; // one char command, like "E" or "I" else memcpy(dsvt.hdr.urcall+6, itap.header.ur, 2); // two char command, like "HX" or "S0" } else memcpy(dsvt.hdr.urcall, itap.header.ur, 8); // ur is at least 3 chars } else { // Access Point Mode memcpy(dsvt.hdr.rpt1, itap.header.r1, 8); memcpy(dsvt.hdr.rpt2, itap.header.r2, 8); memcpy(dsvt.hdr.urcall, itap.header.ur, 8); dsvt.hdr.flag[0] &= ~0x40U; // clear this bit } memcpy(dsvt.hdr.mycall, itap.header.my, 8); memcpy(dsvt.hdr.sfx, itap.header.nm, 4); calcPFCS(dsvt.hdr.flag, dsvt.hdr.pfcs); if (56 != Modem2Gate.Write(dsvt.title, 56)) { printf("ERROR: ProcessITAP: Could not write gateway header packet\n"); return true; } if (log_qso) printf("Sent DSVT to gateway, streamid=%04x ur=%.8s r1=%.8s r2=%.8s my=%.8s/%.4s\n", ntohs(dsvt.streamid), dsvt.hdr.urcall, dsvt.hdr.rpt1, dsvt.hdr.rpt2, dsvt.hdr.mycall, dsvt.hdr.sfx); } else if (16 == len) { // ambe dsvt.ctrl = itap.voice.sequence; memcpy(dsvt.vasd.voice, itap.voice.ambe, 12); if (27 != Modem2Gate.Write(dsvt.title, 27)) { printf("ERROR: ProcessMMDVM: Could not write gateway voice packet\n"); return true; } if (log_qso && (dsvt.ctrl & 0x40)) printf("Sent dsvt end of streamid=%04x\n", ntohs(dsvt.streamid)); } return false; } // process configuration file and return true if there was a problem bool CQnetITAP::ReadConfig(const char *cfgFile) { CQnetConfigure cfg; printf("Reading file %s\n", cfgFile); if (cfg.Initialize(cfgFile)) return true; const std::string estr; // an empty string std::string type; std::string itap_path("module_"); if (0 > assigned_module) { // we need to find the lone itap module for (int i=0; i<3; i++) { std::string test(itap_path); test.append(1, 'a'+i); if (cfg.KeyExists(test)) { cfg.GetValue(test, estr, type, 1, 16); if (type.compare("itap")) continue; // this ain't it! itap_path.assign(test); assigned_module = i; break; } } if (0 > assigned_module) { fprintf(stderr, "Error: no 'itap' module found\n!"); return true; } } else { // make sure itap module is defined itap_path.append(1, 'a' + assigned_module); if (cfg.KeyExists(itap_path)) { cfg.GetValue(itap_path, estr, type, 1, 16); if (type.compare("itap")) { fprintf(stderr, "%s = %s is not 'itap' type!\n", itap_path.c_str(), type.c_str()); return true; } } else { fprintf(stderr, "Module '%c' is not defined.\n", 'a'+assigned_module); return true; } } RPTR_MOD = 'A' + assigned_module; cfg.GetValue(itap_path+"_device", type, ITAP_DEVICE, 7, FILENAME_MAX); cfg.GetValue("gateway_gate2modem"+std::string(1, 'a'+assigned_module), estr, gate2modem, 1, FILENAME_MAX); cfg.GetValue("gateway_modem2gate", estr, modem2gate, 1, FILENAME_MAX); itap_path.append("_callsign"); if (cfg.KeyExists(itap_path)) { if (cfg.GetValue(itap_path, type, RPTR, 3, 6)) return true; } else { itap_path.assign("ircddb_login"); if (cfg.KeyExists(itap_path)) { if (cfg.GetValue(itap_path, estr, RPTR, 3, 6)) return true; } } int l = RPTR.length(); if (l<3 || l>6) { printf("Call '%s' is invalid length!\n", RPTR.c_str()); return true; } else { for (int i=0; i> 8) ^ crc_tabccitt[tmp]; } crc_dstar_ffff = ~crc_dstar_ffff; tmp = crc_dstar_ffff; pfcs[0] = (unsigned char)(crc_dstar_ffff & 0xff); pfcs[1] = (unsigned char)((tmp >> 8) & 0xff); return; } int main(int argc, const char **argv) { setbuf(stdout, NULL); if (2 != argc) { fprintf(stderr, "usage: %s path_to_config_file\n", argv[0]); return 1; } if ('-' == argv[1][0]) { printf("\nQnetITAP Version #%s Copyright (C) 2018-2019 by Thomas A. Early N7TAE\n", ITAP_VERSION); printf("QnetITAP comes with ABSOLUTELY NO WARRANTY; see the LICENSE for details.\n"); printf("This is free software, and you are welcome to distribute it\nunder certain conditions that are discussed in the LICENSE file.\n\n"); return 0; } const char *qn = strstr(argv[0], "qnitap"); if (NULL == qn) { fprintf(stderr, "Error finding 'qnitap' in %s!\n", argv[0]); return 1; } qn += 6; int assigned_module; switch (*qn) { case NULL: assigned_module = -1; break; case 'a': assigned_module = 0; break; case 'b': assigned_module = 1; break; case 'c': assigned_module = 2; break; default: fprintf(stderr, "assigned module must be a, b or c\n"); return 1; } CQnetITAP qnitap(assigned_module); qnitap.Run(argv[1]); printf("%s is closing.\n", argv[0]); return 0; }