// // This file is part of Dire Wolf, an amateur radio packet TNC. // // Copyright (C) 2013, 2015 John Langner, WB2OSZ // // 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, see <http://www.gnu.org/licenses/>. // /*------------------------------------------------------------------ * * Module: aclients.c * * Purpose: Multiple concurrent APRS clients for comparing * TNC demodulator performance. * * Description: Establish connection with multiple servers and * compare results side by side. * * Usage: aclients port1=name1 port2=name2 ... * * Example: aclients 8000=AGWPE 192.168.1.64:8002=DireWolf COM1=D710A * * This will connect to multiple physical or virtual * TNCs, read packets from them, and display results. * * Each port can have the following forms: * * * host-name:tcp-port * * ip-addr:tcp-port * * tcp-port * * serial port name (e.g. COM1, /dev/ttyS0) * *---------------------------------------------------------------*/ /* * Native Windows: Use the Winsock interface. * Linux: Use the BSD socket interface. */ #include "direwolf.h" // Sets _WIN32_WINNT for XP API level needed by ws2tcpip.h #if __WIN32__ #include <winsock2.h> #include <ws2tcpip.h> // _WIN32_WINNT must be set to 0x0501 before including this #else #include <stdlib.h> #include <netdb.h> #include <sys/types.h> #include <sys/ioctl.h> #include <sys/socket.h> #include <arpa/inet.h> #include <netinet/in.h> #include <netinet/tcp.h> #include <fcntl.h> #include <termios.h> #include <errno.h> #endif #include <unistd.h> #include <stdio.h> #include <assert.h> #include <ctype.h> #include <string.h> #include <time.h> #include "ax25_pad.h" #include "textcolor.h" #include "version.h" struct agwpe_s { short portx; /* 0 for first, 1 for second, etc. */ short port_hi_reserved; short kind_lo; /* message type */ short kind_hi; char call_from[10]; char call_to[10]; int data_len; /* Number of data bytes following. */ int user_reserved; }; #if __WIN32__ static unsigned __stdcall client_thread_net (void *arg); static unsigned __stdcall client_thread_serial (void *arg); #else static void * client_thread_net (void *arg); static void * client_thread_serial (void *arg); #endif /* * Convert Internet address to text. * Can't use InetNtop because it is supported only on Windows Vista and later. */ static char * ia_to_text (int Family, void * pAddr, char * pStringBuf, size_t StringBufSize) { struct sockaddr_in *sa4; struct sockaddr_in6 *sa6; switch (Family) { case AF_INET: sa4 = (struct sockaddr_in *)pAddr; #if __WIN32__ snprintf (pStringBuf, StringBufSize, "%d.%d.%d.%d", sa4->sin_addr.S_un.S_un_b.s_b1, sa4->sin_addr.S_un.S_un_b.s_b2, sa4->sin_addr.S_un.S_un_b.s_b3, sa4->sin_addr.S_un.S_un_b.s_b4); #else inet_ntop (AF_INET, &(sa4->sin_addr), pStringBuf, StringBufSize); #endif break; case AF_INET6: sa6 = (struct sockaddr_in6 *)pAddr; #if __WIN32__ snprintf (pStringBuf, StringBufSize, "%x:%x:%x:%x:%x:%x:%x:%x", ntohs(((unsigned short *)(&(sa6->sin6_addr)))[0]), ntohs(((unsigned short *)(&(sa6->sin6_addr)))[1]), ntohs(((unsigned short *)(&(sa6->sin6_addr)))[2]), ntohs(((unsigned short *)(&(sa6->sin6_addr)))[3]), ntohs(((unsigned short *)(&(sa6->sin6_addr)))[4]), ntohs(((unsigned short *)(&(sa6->sin6_addr)))[5]), ntohs(((unsigned short *)(&(sa6->sin6_addr)))[6]), ntohs(((unsigned short *)(&(sa6->sin6_addr)))[7])); #else inet_ntop (AF_INET6, &(sa6->sin6_addr), pStringBuf, StringBufSize); #endif break; default: snprintf (pStringBuf, StringBufSize, "Invalid address family!"); } assert (strlen(pStringBuf) < StringBufSize); return pStringBuf; } /*------------------------------------------------------------------ * * Name: main * * Purpose: Start up multiple client threads listening to different * TNCs. Print packets. Tally up statistics. * * Usage: Described above. * *---------------------------------------------------------------*/ #define MAX_CLIENTS 6 /* Obtained from the command line. */ static int num_clients; static char hostname[MAX_CLIENTS][50]; /* DNS host name or IPv4 address. */ /* Some of the code is there for IPv6 but */ /* needs more work. */ /* Defaults to "localhost" if not specified. */ static char port[MAX_CLIENTS][30]; /* If it begins with a digit, it is considered */ /* a TCP port number at the hostname. */ /* Otherwise, we treat it as a serial port name. */ static char description[MAX_CLIENTS][50]; /* Name used in the output. */ #if __WIN32__ static HANDLE client_th[MAX_CLIENTS]; #else static pthread_t client_tid[MAX_CLIENTS]; #endif #define LINE_WIDTH 120 static int column_width; static char packets[LINE_WIDTH+4]; static int packet_count[MAX_CLIENTS]; //#define PRINT_MINUTES 2 #define PRINT_MINUTES 30 int main (int argc, char *argv[]) { int j; time_t start_time, now, next_print_time; #if __WIN32__ #else int e; setlinebuf (stdout); #endif /* * Extract command line args. */ num_clients = argc - 1; if (num_clients < 1 || num_clients > MAX_CLIENTS) { printf ("Specify up to %d TNCs on the command line.\n", MAX_CLIENTS); exit (1); } column_width = LINE_WIDTH / num_clients; for (j=0; j<num_clients; j++) { char stemp[100]; char *p; /* Each command line argument should be of the form "port=description." */ strlcpy (stemp, argv[j+1], sizeof(stemp)); p = strtok (stemp, "="); if (p == NULL) { printf ("Internal error 1\n"); exit (1); } strlcpy (hostname[j], "localhost", sizeof(hostname[j])); strlcpy (port[j], p, sizeof(port[j])); p = strtok (NULL, "="); if (p == NULL) { printf ("Missing description after %s\n", port[j]); exit (1); } strlcpy (description[j], p, sizeof(description[j])); /* If the port contains ":" split it into hostname (or addr) and port number. */ /* Haven't thought about IPv6 yet. */ strlcpy (stemp, port[j], sizeof(stemp)); char *h; h = strtok (stemp, ":"); if (h != NULL) { p = strtok (NULL, ":"); if (p != NULL) { strlcpy (hostname[j], h, sizeof(hostname[j])); strlcpy (port[j], p, sizeof(port[j])); } } } //printf ("_WIN32_WINNT = %04x\n", _WIN32_WINNT); //for (j=0; j<num_clients; j++) { // printf ("%s,%s,%s\n", hostname[j], port[j], description[j]); //} memset (packets, ' ', (size_t)LINE_WIDTH); packets[LINE_WIDTH] = '\0'; for (j=0; j<num_clients; j++) { packet_count[j] = 0; } for (j=0; j<num_clients; j++) { /* If port begins with digit, consider it to be TCP. */ /* Otherwise, treat as serial port name. */ #if __WIN32__ if (isdigit(port[j][0])) { client_th[j] = (HANDLE)_beginthreadex (NULL, 0, client_thread_net, (void *)j, 0, NULL); } else { client_th[j] = (HANDLE)_beginthreadex (NULL, 0, client_thread_serial, (void *)j, 0, NULL); } if (client_th[j] == NULL) { printf ("Internal error: Could not create client thread %d.\n", j); exit (1); } #else if (isdigit(port[j][0])) { e = pthread_create (&client_tid[j], NULL, client_thread_net, (void *)(long)j); } else { e = pthread_create (&client_tid[j], NULL, client_thread_serial, (void *)(long)j); } if (e != 0) { perror("Internal error: Could not create client thread."); exit (1); } #endif } start_time = time(NULL); next_print_time = start_time + (PRINT_MINUTES) * 60; /* * Print results from clients. */ while (1) { int k; int something; SLEEP_MS(100); something = 0; for (k=0; k<LINE_WIDTH && ! something; k++) { if (packets[k] != ' ') { something = 1; } } if (something) { /* time for others to catch up. */ SLEEP_MS(200); printf ("%s\n", packets); memset (packets, ' ', (size_t)LINE_WIDTH); } now = time(NULL); if (now >= next_print_time) { next_print_time = now + (PRINT_MINUTES) * 60; printf ("\nTotals after %d minutes", (int)((now - start_time) / 60)); for (j=0; j<num_clients; j++) { printf (", %s %d", description[j], packet_count[j]); } printf ("\n\n"); } } return 0; // unreachable } /*------------------------------------------------------------------- * * Name: client_thread_net * * Purpose: Establish connection with a TNC via network. * * Inputs: arg - My instance index, 0 thru MAX_CLIENTS-1. * * Outputs: packets - Received packets are put in the corresponding column. * *--------------------------------------------------------------------*/ #define MAX_HOSTS 30 #if __WIN32__ static unsigned __stdcall client_thread_net (void *arg) #else static void * client_thread_net (void *arg) #endif { int my_index; struct addrinfo hints; struct addrinfo *ai_head = NULL; struct addrinfo *ai; struct addrinfo *hosts[MAX_HOSTS]; int num_hosts, n; int err; char ipaddr_str[46]; /* text form of IP address */ #if __WIN32__ WSADATA wsadata; #endif /* * File descriptor for socket to server. * Set to -1 if not connected. * (Don't use SOCKET type because it is unsigned.) */ int server_sock = -1; struct agwpe_s mon_cmd; char data[1024]; int use_chan = -1; my_index = (int)(long)arg; #if DEBUGx printf ("DEBUG: client_thread_net %d start, port = '%s'\n", my_index, port[my_index]); #endif #if __WIN32__ err = WSAStartup (MAKEWORD(2,2), &wsadata); if (err != 0) { printf("WSAStartup failed: %d\n", err); return (0); } if (LOBYTE(wsadata.wVersion) != 2 || HIBYTE(wsadata.wVersion) != 2) { printf("Could not find a usable version of Winsock.dll\n"); WSACleanup(); //sleep (1); return (0); } #endif memset (&hints, 0, sizeof(hints)); hints.ai_family = AF_UNSPEC; /* Allow either IPv4 or IPv6. */ // hints.ai_family = AF_INET; /* IPv4 only. */ hints.ai_socktype = SOCK_STREAM; hints.ai_protocol = IPPROTO_TCP; /* * Connect to TNC server. */ ai_head = NULL; err = getaddrinfo(hostname[my_index], port[my_index], &hints, &ai_head); if (err != 0) { #if __WIN32__ printf ("Can't get address for server %s, err=%d\n", hostname[my_index], WSAGetLastError()); #else printf ("Can't get address for server %s, %s\n", hostname[my_index], gai_strerror(err)); #endif freeaddrinfo(ai_head); exit (1); } #if DEBUG_DNS printf ("getaddrinfo returns:\n"); #endif num_hosts = 0; for (ai = ai_head; ai != NULL; ai = ai->ai_next) { #if DEBUG_DNS ia_to_text (ai->ai_family, ai->ai_addr, ipaddr_str, sizeof(ipaddr_str)); printf (" %s\n", ipaddr_str); #endif hosts[num_hosts] = ai; if (num_hosts < MAX_HOSTS) num_hosts++; } #if DEBUG_DNS printf ("addresses for hostname:\n"); for (n=0; n<num_hosts; n++) { ia_to_text (hosts[n]->ai_family, hosts[n]->ai_addr, ipaddr_str, sizeof(ipaddr_str)); printf (" %s\n", ipaddr_str); } #endif // Try each address until we find one that is successful. for (n=0; n<num_hosts; n++) { #if __WIN32__ SOCKET is; #else int is; #endif ai = hosts[n]; ia_to_text (ai->ai_family, ai->ai_addr, ipaddr_str, sizeof(ipaddr_str)); is = socket(ai->ai_family, ai->ai_socktype, ai->ai_protocol); #if __WIN32__ if (is == INVALID_SOCKET) { printf ("Socket creation failed, err=%d", WSAGetLastError()); WSACleanup(); is = -1; continue; } #else if (err != 0) { printf ("Socket creation failed, err=%s", gai_strerror(err)); (void) close (is); is = -1; continue; } #endif #ifndef DEBUG_DNS err = connect(is, ai->ai_addr, (int)ai->ai_addrlen); #if __WIN32__ if (err == SOCKET_ERROR) { #if DEBUGx printf("Connect to %s on %s (%s), port %s failed.\n", description[my_index], hostname[my_index], ipaddr_str, port[my_index]); #endif closesocket (is); is = -1; continue; } #else if (err != 0) { #if DEBUGx printf("Connect to %s on %s (%s), port %s failed.\n", description[my_index], hostname[my_index], ipaddr_str, port[my_index]); #endif (void) close (is); is = -1; continue; } int flag = 1; err = setsockopt (is, IPPROTO_TCP, TCP_NODELAY, (void*)(long)(&flag), (socklen_t)sizeof(flag)); if (err < 0) { printf("setsockopt TCP_NODELAY failed.\n"); } #endif /* Success. */ printf("Client %d now connected to %s on %s (%s), port %s\n", my_index, description[my_index], hostname[my_index], ipaddr_str, port[my_index] ); server_sock = is; #endif break; } freeaddrinfo(ai_head); if (server_sock == -1) { printf("Client %d unable to connect to %s on %s (%s), port %s\n", my_index, description[my_index], hostname[my_index], ipaddr_str, port[my_index] ); exit (1); } /* * Send command to toggle reception of frames in raw format. * * Note: Monitor format is only for UI frames. * It also discards the via path. */ memset (&mon_cmd, 0, sizeof(mon_cmd)); mon_cmd.kind_lo = 'k'; SOCK_SEND (server_sock, (char*)(&mon_cmd), sizeof(mon_cmd)); /* * Print all of the monitored packets. */ while (1) { int n; n = SOCK_RECV (server_sock, (char*)(&mon_cmd), sizeof(mon_cmd)); if (n != sizeof(mon_cmd)) { printf ("Read error, client %d received %d command bytes. Terminating.\n", my_index, n); exit (1); } #if DEBUGx printf ("client %d received '%c' data, data_len = %d\n", my_index, mon_cmd.kind_lo, mon_cmd.data_len); #endif assert (mon_cmd.data_len >= 0 && mon_cmd.data_len < (int)(sizeof(data))); if (mon_cmd.data_len > 0) { n = SOCK_RECV (server_sock, data, mon_cmd.data_len); if (n != mon_cmd.data_len) { printf ("Read error, client %d received %d data bytes.\n", my_index, n); exit (1); } } /* * Print it and add to counter. * The AGWPE score was coming out double the proper value because * we were getting the same thing from ports 2 and 3. * 'use_chan' is the first channel we hear from. * Listen only to that one. */ if (mon_cmd.kind_lo == 'K' && (use_chan == -1 || use_chan == mon_cmd.portx)) { packet_t pp; char *pinfo; int info_len; char result[400]; char *p; int col, len; alevel_t alevel; //printf ("server %d, portx = %d\n", my_index, mon_cmd.portx); use_chan = mon_cmd.portx; memset (&alevel, 0xff, sizeof(alevel)); pp = ax25_from_frame ((unsigned char *)(data+1), mon_cmd.data_len-1, alevel); assert (pp != NULL); ax25_format_addrs (pp, result); info_len = ax25_get_info (pp, (unsigned char **)(&pinfo)); pinfo[info_len] = '\0'; strlcat (result, pinfo, sizeof(result)); for (p=result; *p!='\0'; p++) { if (! isprint(*p)) *p = ' '; } #if DEBUGx printf ("[%d] %s\n", my_index, result); #endif col = column_width * my_index; len = strlen(result); #define MARGIN 3 if (len > column_width - 3) { len = column_width - 3; } if (packets[col] == ' ') { memcpy (packets+col, result, (size_t)len); } else { memcpy (packets+col, "OVERRUN! ", (size_t)10); } ax25_delete (pp); packet_count[my_index]++; } } } /* end client_thread_net */ /*------------------------------------------------------------------- * * Name: client_thread_serial * * Purpose: Establish connection with a TNC via serial port. * * Inputs: arg - My instance index, 0 thru MAX_CLIENTS-1. * * Outputs: packets - Received packets are put in the corresponding column. * *--------------------------------------------------------------------*/ #if __WIN32__ typedef HANDLE MYFDTYPE; #define MYFDERROR INVALID_HANDLE_VALUE #else typedef int MYFDTYPE; #define MYFDERROR (-1) #endif #if __WIN32__ static unsigned __stdcall client_thread_serial (void *arg) #else static void * client_thread_serial (void *arg) #endif { int my_index = (int)(long)arg; #if __WIN32__ MYFDTYPE fd; DCB dcb; int ok; // Bug: Won't work for ports above COM9. // http://support.microsoft.com/kb/115831 fd = CreateFile(port[my_index], GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, 0, NULL); if (fd == MYFDERROR) { printf("Client %d unable to connect to %s on %s.\n", my_index, description[my_index], port[my_index] ); exit (1); } /* Reference: http://msdn.microsoft.com/en-us/library/windows/desktop/aa363201(v=vs.85).aspx */ memset (&dcb, 0, sizeof(dcb)); dcb.DCBlength = sizeof(DCB); ok = GetCommState (fd, &dcb); if (! ok) { printf ("GetCommState failed.\n"); } /* http://msdn.microsoft.com/en-us/library/windows/desktop/aa363214(v=vs.85).aspx */ dcb.DCBlength = sizeof(DCB); dcb.BaudRate = 9600; dcb.fBinary = 1; dcb.fParity = 0; dcb.fOutxCtsFlow = 0; dcb.fOutxDsrFlow = 0; dcb.fDtrControl = 0; dcb.fDsrSensitivity = 0; dcb.fOutX = 0; dcb.fInX = 0; dcb.fErrorChar = 0; dcb.fNull = 0; /* Don't drop nul characters! */ dcb.fRtsControl = 0; dcb.ByteSize = 8; dcb.Parity = NOPARITY; dcb.StopBits = ONESTOPBIT; ok = SetCommState (fd, &dcb); if (! ok) { printf ("SetCommState failed.\n"); } #else /* Linux version. */ int fd; struct termios ts; int e; fd = open (port[my_index], O_RDWR); if (fd == MYFDERROR) { printf("Client %d unable to connect to %s on %s.\n", my_index, description[my_index], port[my_index] ); exit (1); } e = tcgetattr (fd, &ts); if (e != 0) { perror ("nm tcgetattr"); } cfmakeraw (&ts); // TODO: speed? ts.c_cc[VMIN] = 1; /* wait for at least one character */ ts.c_cc[VTIME] = 0; /* no fancy timing. */ e = tcsetattr (fd, TCSANOW, &ts); if (e != 0) { perror ("nm tcsetattr"); } #endif /* Success. */ printf("Client %d now connected to %s on %s\n", my_index, description[my_index], port[my_index] ); /* * Assume we are already in monitor mode. */ /* * Print all of the monitored packets. */ while (1) { unsigned char ch; char result[500]; int col, len; int done; char *p; len = 0; done = 0; while ( ! done) { #if __WIN32__ DWORD n; if (! ReadFile (fd, &ch, 1, &n, NULL)) { printf ("Read error on %s.\n", description[my_index]); CloseHandle (fd); exit (1); } #else int n; if ( ( n = read(fd, & ch, 1)) < 0) { printf ("Read error on %s.\n", description[my_index]); close (fd); exit (1); } #endif if (n == 1) { /* * Try to build one line for each packet. * The KPC3+ breaks a packet into two lines like this: * * KB1ZXL-1>T2QY5P,W1MHL*,WIDE2-1: <<UI>>: * `c0+!h4>/]"4a}146.520MHz Listening, V-Alert & WLNK-1= * * N8VIM>BEACON,W1XM,WB2OSZ-1,WIDE2*: <UI>: * !4240.85N/07133.99W_PHG72604/ Pepperell, MA. WX. 442.9+ PL100 * * Don't know why some are <<UI>> and some <UI>. * * Anyhow, ignore the return character if preceded by >: */ if (ch == '\r') { if (len >= 10 && result[len-2] == '>' && result[len-1] == ':') { continue; } done = 1; continue; } if (ch == '\n') continue; result[len++] = ch; } } result[len] = '\0'; /* * Print it and add to counter. */ if (len > 0) { /* Blank any unprintable characters. */ for (p=result; *p!='\0'; p++) { if (! isprint(*p)) *p = ' '; } #if DEBUGx printf ("[%d] %s\n", my_index, result); #endif col = column_width * my_index; if (len > column_width - 3) { len = column_width - 3; } if (packets[col] == ' ') { memcpy (packets+col, result, (size_t)len); } else { memcpy (packets+col, "OVERRUN! ", (size_t)10); } packet_count[my_index]++; } } } /* end client_thread_serial */ /* end aclients.c */