/* Copyright 2025 flyinghead This file is part of Flycast. Flycast 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. Flycast 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 Flycast. If not, see . */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #define DEFAULT_TTY "/dev/ttyACM0" #define DEFAULT_HOST "dcnet.flyca.st" #define DEFAULT_PORT 7654 char ttyName[512] = DEFAULT_TTY; char hostName[64] = DEFAULT_HOST; uint16_t port = DEFAULT_PORT; struct termios tbufsave; int setNonBlocking(int fd) { int flags = fcntl(fd, F_GETFL, 0); if (flags == -1) flags = 0; flags |= O_NONBLOCK; if (fcntl(fd, F_SETFL, flags) != 0) { perror("fcntl(O_NONBLOCK)"); return -1; } return 0; } int configureTty(int fd, int local) { if (tcgetattr(fd, &tbufsave) == -1) perror("tcgetattr"); struct termios tbuf; memcpy(&tbuf, &tbufsave, sizeof(tbuf)); /* 8-bit, one stop bit, no parity, carrier detect, no hang up on close, disable RTS/CTS flow control. */ tbuf.c_cflag &= ~(CSIZE | CSTOPB | PARENB | CLOCAL | HUPCL | CRTSCTS); tbuf.c_cflag |= CS8 | CREAD; if (local) // ignore CD tbuf.c_cflag |= CLOCAL; /* don't translate NL to CR or CR to NL on input, get all 8 bits of input disable xon/xoff flow control on output, no interrupt on break signal, ignore parity, ignore break */ tbuf.c_iflag = IGNBRK | IGNPAR; /* disable all output processing */ tbuf.c_oflag = 0; /* non-canonical, ignore signals and no echoing on output */ tbuf.c_lflag = 0; tbuf.c_cc[VMIN] = 1; tbuf.c_cc[VTIME] = 0; /* set the parameters associated with the terminal port */ if (tcsetattr(fd, TCSANOW, &tbuf) == -1) { perror("tcsetattr"); return 1; } return 0; } struct sockaddr_in *resolve(const char *hostName) { static struct sockaddr_in resolved; struct addrinfo hints, *result; memset(&hints, 0, sizeof (hints)); hints.ai_family = AF_INET; hints.ai_socktype = SOCK_STREAM; int errcode = getaddrinfo(hostName, NULL, &hints, &result); if (errcode != 0) { fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(errcode)); return NULL; } if (result == NULL) { fprintf(stderr, "%s: host not found\n", hostName); return NULL; } memcpy(&resolved, (struct sockaddr_in *)result->ai_addr, sizeof(resolved)); freeaddrinfo(result); return &resolved; } const uint32_t MAGIC = 0xDC15C001; typedef struct { struct sockaddr_in address; char name[256]; int ping; int count; } AccessPoint; AccessPoint accessPoints[16]; int apCount; uint64_t getTimeMs() { struct timeval start; gettimeofday(&start, NULL); return (uint64_t)start.tv_sec * 1000 + (uint64_t)start.tv_usec / 1000; } void sendPing(int sock, const struct sockaddr_in *dest) { uint8_t buf[13]; memcpy(&buf[0], &MAGIC, sizeof(MAGIC)); buf[4] = 1; // ping uint64_t now = getTimeMs(); memcpy(&buf[5], &now, sizeof(uint64_t)); sendto(sock, buf, sizeof(buf), 0, (const struct sockaddr *)dest, sizeof(*dest)); } struct sockaddr_in *findBestAccessPoint() { struct sockaddr_in *discoaddr = resolve("dcnet.flyca.st"); if (discoaddr == NULL) return NULL; discoaddr->sin_port = htons(7655); // create UDP socket int sock = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); if (sock < 0) { perror("socket"); return NULL; } // bind it struct sockaddr_in serveraddr = {}; serveraddr.sin_family = AF_INET; if (bind(sock, (struct sockaddr *)&serveraddr, sizeof(serveraddr)) < 0) { perror("bind"); close(sock); return NULL; } // set non-blocking fcntl(sock, F_SETFL, O_NONBLOCK); // discover access points uint8_t buf[512]; memcpy(&buf[0], &MAGIC, sizeof(MAGIC)); buf[4] = 3; // discover access points if (sendto(sock, buf, 5, 0, (const struct sockaddr *)discoaddr, sizeof(struct sockaddr_in)) < 5) { perror("sendto"); close(sock); return NULL; } uint64_t start = getTimeMs(); int timeout = 500; int done = 0; while (!done) { struct pollfd pfd = { sock, POLLIN }; int rc = poll(&pfd, 1, timeout); if (rc < 0) { perror("poll"); break; } if (pfd.revents & (POLLERR|POLLHUP|POLLNVAL)) // error break; const uint64_t now = getTimeMs(); timeout = (int)(start + 1000u - now); if (timeout <= 0) // done break; if (timeout < 500) { // Re-ping access points that didn't answer after 500 ms for (int i = 0; i < apCount; i++) { if (accessPoints[i].count == 0) sendPing(sock, &accessPoints[i].address); } } else { timeout -= 500; } if (rc == 0) continue; struct sockaddr_in peeraddr; socklen_t sz = sizeof(peeraddr); ssize_t recvlen = recvfrom(sock, buf, sizeof(buf), 0, (struct sockaddr *)&peeraddr, &sz); if (recvlen <= 0) { if (recvlen < 0) perror("recvfrom"); break; } if (recvlen < 5 || memcmp(&buf[0], &MAGIC, sizeof(MAGIC))) { fprintf(stderr, "Invalid packet received (size %zd)\n", recvlen); continue; } switch (buf[4]) { case 2: // pong if (recvlen != 13) { fprintf(stderr, "Invalid ping received (size %zd)\n", recvlen); break; } for (int i = 0; i < apCount; i++) { if (accessPoints[i].address.sin_addr.s_addr == peeraddr.sin_addr.s_addr) { uint64_t sent; memcpy(&sent, &buf[5], sizeof(sent)); accessPoints[i].ping += (int)(now - sent); accessPoints[i].count++; if (accessPoints[i].count == 3) done = 1; else sendPing(sock, &accessPoints[i].address); break; } } break; case 3: // discover { apCount = 0; const uint8_t *p = &buf[5]; while (p - &buf[0] < (int)recvlen) { accessPoints[apCount].address.sin_family = AF_INET; accessPoints[apCount].address.sin_port = htons(7655); memcpy(&accessPoints[apCount].address.sin_addr.s_addr, p, sizeof(uint32_t)); p += 4; size_t l = *p++; memcpy(accessPoints[apCount].name, p, l); accessPoints[apCount].name[l] = '\0'; p += l; apCount++; } if (apCount == 1) { done = 1; } else { for (int i = 0; i < apCount; i++) sendPing(sock, &accessPoints[i].address); } break; } } } close(sock); int bestAP = 0; int bestPing = 1000000; for (int i = 0; i < apCount; i++) { const AccessPoint *ap = &accessPoints[i]; if (ap->count != 0) { int ping = ap->ping / ap->count; printf("%s: %d ms\n", ap->name, ping); if (ping < bestPing) { bestAP = i; bestPing = ping; } } else { printf("%s: no answer\n", ap->name); } } return bestAP < apCount ? &accessPoints[bestAP].address : NULL; } void usage(const char *arg0) { fprintf(stderr, "Usage: %s [-t ] [-h ]\n", arg0); fprintf(stderr, "Default tty is %s. Default host:port is %s:%d\n", DEFAULT_TTY, DEFAULT_HOST, DEFAULT_PORT); exit(1); } int main(int argc, char *argv[]) { int forceHost = 0; int opt; while ((opt = getopt(argc, argv, "t:h:p:")) != -1) { switch (opt) { case 't': strcpy(ttyName, optarg); break; case 'h': forceHost = 1; strcpy(hostName, optarg); break; case 'p': port = (uint16_t)atoi(optarg); break; default: usage(argv[0]); } } if (optind != argc) usage(argv[0]); fprintf(stderr, "DCNet starting\n"); struct sockaddr_in *serverAddress = NULL; if (!forceHost) serverAddress = findBestAccessPoint(); if (serverAddress == NULL) { serverAddress = resolve(hostName); if (serverAddress == NULL) return -1; char s[100]; inet_ntop(AF_INET, &serverAddress->sin_addr, s, 100); printf("%s is %s\n", hostName, s); } /* TTY */ int ttyfd = open(ttyName, O_RDWR); if (ttyfd == -1) { perror("Can't open tty"); return 1; } if (configureTty(ttyfd, 0)) return 1; /* SOCKET */ int sockfd = socket(AF_INET, SOCK_STREAM, 0); if (sockfd == -1) { fprintf(stderr, "socket() failed: %d\n", errno); return 1; } serverAddress->sin_port = htons(port); if (connect(sockfd, (struct sockaddr *)serverAddress, sizeof(*serverAddress)) != 0) { fprintf(stderr, "connect() failed: %d\n", errno); return 1; } if (setNonBlocking(sockfd) || setNonBlocking(ttyfd)) return -1; char outbuf[1504]; ssize_t outbuflen = 0; char inbuf[1504]; ssize_t inbuflen = 0; for (;;) { #ifdef DEBUG ssize_t old_olen = outbuflen; ssize_t old_ilen = inbuflen; #endif fd_set readfds; FD_ZERO(&readfds); if (inbuflen < sizeof(inbuf)) FD_SET(sockfd, &readfds); if (outbuflen < sizeof(outbuf)) FD_SET(ttyfd, &readfds); fd_set writefds; FD_ZERO(&writefds); ssize_t outbufReady = 0; if (outbuflen > 0) { outbufReady = outbuflen; /* if (outbuf[0] != 0x7e) { outbufReady = outbuflen; } else { for (int i = 1; i < outbuflen; i++) { if (outbuf[i] == 0x7e) { outbufReady = i + 1; break; } } } */ if (outbufReady != 0) FD_SET(sockfd, &writefds); } if (inbuflen > 0) FD_SET(ttyfd, &writefds); int nfds = (sockfd > ttyfd ? sockfd : ttyfd) + 1; if (select(nfds, &readfds, &writefds, NULL, NULL) == -1) { if (errno == EINTR) continue; fprintf(stderr, "select() failed: %d\n", errno); close(sockfd); return 1; } if (FD_ISSET(ttyfd, &readfds)) { ssize_t ret = read(ttyfd, outbuf + outbuflen, sizeof(outbuf) - (size_t)outbuflen); if (ret < 0) { if (errno != EINTR && errno != EWOULDBLOCK) { fprintf(stderr, "read from tty failed: %d\n", errno); break; } ret = 0; } else if (ret == 0) { fprintf(stderr, "modem hang up\n"); break; } if (ret > 0) { outbuflen += ret; FD_SET(sockfd, &writefds); } } if (FD_ISSET(sockfd, &readfds)) { ssize_t ret = read(sockfd, inbuf + inbuflen, sizeof(inbuf) - (size_t)inbuflen); if (ret < 0) { if (errno != EINTR && errno != EWOULDBLOCK) { fprintf(stderr, "read from socket failed: %d\n", errno); break; } ret = 0; } else if (ret == 0) { fprintf(stderr, "socket read EOF\n"); break; } if (ret > 0) { inbuflen += ret; FD_SET(ttyfd, &writefds); } } if (FD_ISSET(ttyfd, &writefds)) { ssize_t ret = write(ttyfd, inbuf, (size_t)inbuflen); if (ret < 0) { if (errno != EINTR && errno != EWOULDBLOCK) { fprintf(stderr, "write to tty failed: %d\n", errno); break; } ret = 0; } if (ret > 0) { inbuflen -= ret; if (inbuflen > 0) memmove(inbuf, inbuf + ret, (size_t)inbuflen); } } if (FD_ISSET(sockfd, &writefds)) { ssize_t ret = write(sockfd, outbuf, (size_t)outbufReady); if (ret < 0) { if (errno == EINTR && errno != EWOULDBLOCK) { fprintf(stderr, "write to socket failed: %d\n", errno); break; } ret = 0; } if (ret > 0) { outbuflen -= ret; if (outbuflen > 0) memmove(outbuf, outbuf + ret, (size_t)outbuflen); } } #ifdef DEBUG printf("OUT %03zd%c IN %03zd%c\r", outbuflen, outbuflen > old_olen ? '+' : outbuflen < old_olen ? '-' : ' ', inbuflen, inbuflen > old_ilen ? '+' : inbuflen < old_ilen ? '-' : ' '); fflush(stdout); #endif } close(ttyfd); ttyfd = open(ttyName, O_RDWR); if (ttyfd == -1) { perror("Can't reopen tty"); } else { tcsetattr(ttyfd, TCSANOW, &tbufsave); close(ttyfd); } close(sockfd); fprintf(stderr, "DCNet stopped\n"); return 0; }