diff options
author | B. Watson <yalhcru@gmail.com> | 2021-07-21 00:12:31 -0400 |
---|---|---|
committer | B. Watson <yalhcru@gmail.com> | 2021-07-21 00:12:31 -0400 |
commit | aa956853cc37d163113cafb2f1982b7a3a306fd1 (patch) | |
tree | 48aa7c82c29c24f38e65018d4c2d049af4018c1c | |
download | slowbaud-aa956853cc37d163113cafb2f1982b7a3a306fd1.tar.gz |
initial commit
-rw-r--r-- | Makefile | 21 | ||||
-rw-r--r-- | README.txt | 79 | ||||
-rw-r--r-- | slowbaud.1 | 126 | ||||
-rw-r--r-- | slowbaud.c | 307 | ||||
-rw-r--r-- | slowbaud.rst | 117 |
5 files changed, 650 insertions, 0 deletions
diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..084bee4 --- /dev/null +++ b/Makefile @@ -0,0 +1,21 @@ +PROJ=slowbaud + +CC=gcc +CFLAGS=-Wall -O2 $(EXTRACFLAGS) +LIBS=-lutil +RST2MAN=rst2man.py + +all: $(PROJ) + +$(PROJ): $(PROJ).c $(PROJ).1 README.txt + $(CC) $(CFLAGS) -o $(PROJ) $(PROJ).c $(LIBS) + +$(PROJ).1: $(PROJ).rst + $(RST2MAN) $(PROJ).rst > $(PROJ).1 + +README.txt: $(PROJ).1 + man ./$(PROJ).1 | sed 's,_\010,,' > README.txt + +# Don't remove the man page or README here, it's in git. +clean: + rm -f $(PROJ) core *.o diff --git a/README.txt b/README.txt new file mode 100644 index 0000000..28132d3 --- /dev/null +++ b/README.txt @@ -0,0 +1,79 @@ +SLOWBAUD(1) Urchlay's Useless Stuff SLOWBAUD(1) + +NAME + slowbaud - simulate a low bitrate serial connection + +SYNOPSIS + slowbaud <bits-per-sec> [<file> ...] + + slowbaud <bits-per-sec> -c [<command> [<arg> ...]] + +DESCRIPTION + slowbaud by default acts as a filter, or like the cat(1) command. It reads files or + its standard input, and writes the contents unmodified to standard output... but + slowly, at the given bits-per-second rate. Output is unbuffered. + + With the -c option, it creates a pseudo-tty and runs the given command in it (or an + interactive shell, if no command is given). + + The <bits-per-sec> argument supports a range of 1 to 500000. Timing accuracy depends + on your OS, kernel config (HZ and/or NO_HZ on Linux), and system load. No "fancy" + techniques like realtime scheduling or hardware event timers are used. At bitrates up + to 57600, on a typical unloaded Linux system, the timing should be around 99.7% accu‐ + rate. + +ENVIRONMENT + SLOWBAUD_DEBUG + Set this (to any value) in the environment to see verbose debug output on + stderr, including timing accuracy stats. + + SHELL Standard *nix environment variable, used to determine what shell to run when -c + is given with no <command>. If unset, /bin/sh is used. + +EXIT STATUS + Without -c, 0 for success, non-zero on any error such as nonexistent/unreadable files. + slowbaud exits immediately on such errors (this is unlike cat(1)). + + With -c, exit status is that of the child process, or 127 if the child process + couldn't be spawned (e.g. command not found). Of course, the child process could also + exit with status 127... + +NOTES + We can't really insert a delay between the bits of a byte, since I/O is done with byte + granularity. For calculation purposes, <bits-per-sec> is divided by 10 to get bytes + per second. This simulates "8-N-1": one start bit, 8 data bits, no parity, and 1 stop + bit (total of 10 bits per byte). + + The timing code works by calculating how long to sleep after each character (in + microseconds), but actually sleeping slightly less than that, then busy-waiting until + the rest of the interval expires. At slower bitrates, this works well, and the CPU + overhead is barely noticeable (at least on reasonably fast modern systems). + + The timing error will almost always result in the bitrate being slightly too slow at + lower bitrates and slightly too fast at higher ones. + + Timing is more accurate on Linux than OSX. It's done with getitimer() and sigwait(). + This works out to be slightly more accurate than using usleep() on both Linux and OSX. + It would be possible to use the realtime timer_create() and clock_gettime() API on + Linux, for possibly even better accuracy, but OSX doesn't have these (and I want to be + portable). + + If this were a truly useful application, it would be worth trying to decrease latency + further, with realtime process scheduling. I didn't do this because slowbaud is just a + toy, and because the RT stuff tends to be unportable and require elevated privileges + (root, or something like setrtlimit or extended filesystem attributes to manage capa‐ + bilities). + + About the name... I'm aware that "baud" is not synonymous with bps. I just think + "slowbaud" sounds better than "slowbps", as a name. Anyway the stty command on Linux + misuses the term ("speed 38400 baud"), so I'm in good company. + +BUGS + With -c, signals aren't handled gracefully. Window size changes (SIGWINCH) don't get + propagated to the child process, and pressing ^C doesn't interrupt the process. Yet. + +COPYRIGHT + slowbaud is copyright 2021, B. Watson <yalhcru@gmail.com>. Released under the WTFPL. + See http://www.wtfpl.net/txt/copying/ for details. + +0.0.1 2021-07-21 SLOWBAUD(1) diff --git a/slowbaud.1 b/slowbaud.1 new file mode 100644 index 0000000..7fcec1e --- /dev/null +++ b/slowbaud.1 @@ -0,0 +1,126 @@ +.\" Man page generated from reStructuredText. +. +.TH SLOWBAUD 1 "2021-07-21" "0.0.1" "Urchlay's Useless Stuff" +.SH NAME +slowbaud \- simulate a low bitrate serial connection +. +.nr rst2man-indent-level 0 +. +.de1 rstReportMargin +\\$1 \\n[an-margin] +level \\n[rst2man-indent-level] +level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] +- +\\n[rst2man-indent0] +\\n[rst2man-indent1] +\\n[rst2man-indent2] +.. +.de1 INDENT +.\" .rstReportMargin pre: +. RS \\$1 +. nr rst2man-indent\\n[rst2man-indent-level] \\n[an-margin] +. nr rst2man-indent-level +1 +.\" .rstReportMargin post: +.. +.de UNINDENT +. RE +.\" indent \\n[an-margin] +.\" old: \\n[rst2man-indent\\n[rst2man-indent-level]] +.nr rst2man-indent-level -1 +.\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] +.in \\n[rst2man-indent\\n[rst2man-indent-level]]u +.. +.\" RST source for slowbaud(1) man page. Convert with: +. +.\" rst2man.py slowbaud.rst > slowbaud.1 +. +.\" rst2man.py comes from the SBo development/docutils package. +. +.SH SYNOPSIS +.sp +\fBslowbaud\fP \fI<bits\-per\-sec>\fP [\fI<file>\fP ...] +.sp +\fBslowbaud\fP \fI<bits\-per\-sec>\fP \fB\-c\fP [\fI<command>\fP [\fI<arg>\fP ...]] +.SH DESCRIPTION +.sp +slowbaud by default acts as a filter, or like the \fBcat(1)\fP command. It +reads files or its standard input, and writes the contents unmodified +to standard output... but slowly, at the given bits\-per\-second rate. +Output is unbuffered. +.sp +With the \fB\-c\fP option, it creates a pseudo\-tty and runs the given command +in it (or an interactive shell, if no command is given). +.sp +The \fI<bits\-per\-sec>\fP argument supports a range of 1 to 500000. Timing +accuracy depends on your OS, kernel config (HZ and/or NO_HZ on Linux), +and system load. No "fancy" techniques like realtime scheduling +or hardware event timers are used. At bitrates up to 57600, on a +typical unloaded Linux system, the timing should be around 99.7% +accurate. +.SH ENVIRONMENT +.INDENT 0.0 +.TP +.B \fBSLOWBAUD_DEBUG\fP +Set this (to any value) in the environment to see verbose debug +output on stderr, including timing accuracy stats. +.TP +.B \fBSHELL\fP +Standard *nix environment variable, used to determine what +shell to run when \fB\-c\fP is given with no \fI<command>\fP\&. If +unset, \fB/bin/sh\fP is used. +.UNINDENT +.SH EXIT STATUS +.sp +Without \fB\-c\fP, 0 for success, non\-zero on any error such as +nonexistent/unreadable files. slowbaud exits immediately on such +errors (this is unlike \fBcat(1)\fP). +.sp +With \fB\-c\fP, exit status is that of the child process, or 127 if +the child process couldn\(aqt be spawned (e.g. command not found). +Of course, the child process could also exit with status 127... +.SH NOTES +.sp +We can\(aqt really insert a delay between the bits of a byte, since +I/O is done with byte granularity. For calculation purposes, +\fI<bits\-per\-sec>\fP is divided by 10 to get bytes per second. This +simulates "8\-N\-1": one start bit, 8 data bits, no parity, and 1 stop +bit (total of 10 bits per byte). +.sp +The timing code works by calculating how long to sleep after each +character (in microseconds), but actually sleeping slightly less +than that, then busy\-waiting until the rest of the interval expires. +At slower bitrates, this works well, and the CPU overhead is barely +noticeable (at least on reasonably fast modern systems). +.sp +The timing error will almost always result in the bitrate +being slightly too slow at lower bitrates and slightly too fast at +higher ones. +.sp +Timing is more accurate on Linux than OSX. It\(aqs done with getitimer() +and sigwait(). This works out to be slightly more accurate than +using usleep() on both Linux and OSX. It would be possible to use +the realtime timer_create() and clock_gettime() API on Linux, for +possibly even better accuracy, but OSX doesn\(aqt have these (and I want to be +portable). +.sp +If this were a truly useful application, it would be worth trying to +decrease latency further, with realtime process scheduling. I didn\(aqt +do this because slowbaud is just a toy, and because the RT stuff tends +to be unportable and require elevated privileges (root, or something +like setrtlimit or extended filesystem attributes to manage capabilities). +.sp +About the name... I\(aqm aware that "baud" is not synonymous with bps. I +just think "slowbaud" sounds better than "slowbps", as a name. Anyway +the stty command on Linux misuses the term ("speed 38400 baud"), so +I\(aqm in good company. +.SH BUGS +.sp +With \fB\-c\fP, signals aren\(aqt handled gracefully. Window size changes +(SIGWINCH) don\(aqt get propagated to the child process, and pressing ^C +doesn\(aqt interrupt the process. Yet. +.SH COPYRIGHT +.sp +slowbaud is copyright 2021, B. Watson <\fI\%yalhcru@gmail.com\fP>. Released +under the WTFPL. See \fI\%http://www.wtfpl.net/txt/copying/\fP for details. +.\" Generated by docutils manpage writer. +. diff --git a/slowbaud.c b/slowbaud.c new file mode 100644 index 0000000..bbf31ac --- /dev/null +++ b/slowbaud.c @@ -0,0 +1,307 @@ +/* Simulate low bitrate serial connection, like a 1980s modem. + Author: B. Watson. License: WTFPL. */ + +#include <stdio.h> +#include <stdlib.h> +#include <unistd.h> +#include <sys/time.h> +#include <sys/types.h> +#include <sys/stat.h> +#include <fcntl.h> +#include <errno.h> +#include <sys/select.h> +#include <sys/wait.h> +#include <termios.h> +#include <sys/ioctl.h> +#include <signal.h> +#include <time.h> +#include <string.h> + +/* portability stuff only tested on Linux and OSX. Hope it's OK. */ +#if defined(__FreeBSD__) || defined(HAVE_LIBUTIL_H) + #include <libutil.h> +#elif defined (__OpenBSD__) || defined (__NetBSD__) || defined (__APPLE__) || defined(HAVE_UTIL_H) + #include <util.h> +#else + #include <pty.h> +#endif + +#define DEFAULT_BPS 1200 +#define MIN_BPS 1 +#define MAX_BPS 500000 + +const char *self; +unsigned int bps; +unsigned long interval, starttime, outbytes = 0; + +int debug = 0; + +int timer; +struct timeval tv; +struct itimerval itv, itv_disarm; +sigset_t sigmask; + +#define NOW_USEC() ( (gettimeofday(&tv, NULL)), tv.tv_sec * 1000000L + tv.tv_usec ) + +void die(const char *msg) { + if(errno) { + fprintf(stderr, "%s: %s: %s\n", self, strerror(errno), msg); + } else { + fprintf(stderr, "%s: %s\n", self, msg); + } + exit(1); +} + +void setup_timer(void) { + itv_disarm.it_interval.tv_sec = + itv_disarm.it_interval.tv_usec = + itv_disarm.it_value.tv_sec = + itv_disarm.it_value.tv_usec = 0; + + itv = itv_disarm; + itv.it_interval.tv_usec = interval; + itv.it_value.tv_usec = interval; + + sigemptyset(&sigmask); + sigaddset(&sigmask, SIGALRM); + sigprocmask(SIG_BLOCK, &sigmask, NULL); +} + +void slow_write(int outfd, char c) { + int j; + unsigned long now, target, overslept; + + setitimer(ITIMER_REAL, &itv, NULL); + + target = NOW_USEC() + interval; + + write(outfd, &c, 1); + outbytes++; + sigwait(&sigmask, &j); + setitimer(ITIMER_REAL, &itv_disarm, NULL); + + now = NOW_USEC(); + if(now > target) { + overslept = now - target; + if(overslept < interval) { + unsigned long old = itv.it_interval.tv_usec; + itv.it_interval.tv_usec -= overslept; + itv.it_value.tv_usec = itv.it_interval.tv_usec; + if(debug) { + fprintf(stderr, "tv_usec was %ldms, overslept %ld, new tv_usec %ldms\n", + old, overslept, itv.it_interval.tv_usec); + } + } + } else { + while(NOW_USEC() < target) + /* NOP */ ; + } +} + +void debug_stats(void) { + if(outbytes && debug) { + unsigned long elapsed_us = NOW_USEC() - starttime; + double elapsed_sec = ((double)elapsed_us/1000000.0L); + double finterval = (1000000.0L / ((double)bps / 10.0L)); + double actual = ((double)outbytes * 10.0L) / elapsed_sec; + double offby = 100.0L * (((double)bps / actual) - 1.0L); + fprintf(stderr, + "outbytes %lu, elapsed_us %lu, requested bps %d (%.2fms), " + "actual %.2f, accuracy %.2f%%\n", + outbytes, elapsed_us, bps, finterval, actual, 100.0 - offby); + } + outbytes = 0; +} + +void slowcat(int infd) { + int c; + + starttime = NOW_USEC(); + while(read(infd, &c, 1) == 1) { + slow_write(1, c); + } + + if(debug) debug_stats(); +} + +void slowtty(char **args) { + char *dflt_args[] = { NULL, NULL }; + pid_t kidpid; + int master, status; + struct termios term, oldterm; + struct winsize winsz; + char c; + + if(!*args) { + args = dflt_args; + args[0] = getenv("SHELL"); + if(!*args) args[0] = "/bin/sh"; + } + if(debug) { + char **a = args; + fprintf(stderr, "executing cmd '%s', argv[]: ", a[0]); + while(*a) { + fprintf(stderr, "%s ", *a++); + } + fprintf(stderr, "\n"); + } + + if( (tcgetattr(0, &term)) ) die("tcgetattr"); + + if( (ioctl(0, TIOCGWINSZ, &winsz)) ) die("ioctl(TIOCGWINSZ)"); + + kidpid = forkpty(&master, NULL, &term, &winsz); + if(kidpid < 0) die("forkpty"); + + if(kidpid == 0) { + /* we are the child, do childish things */ + execvp(args[0], args); + perror("execvp"); /* don't use die() */ + exit(127); + } + + /* we are the parent, watch the child */ + oldterm = term; /* so we can restore it later */ + cfmakeraw(&term); + tcsetattr(0, TCSANOW, &term); + + while(1) { + ssize_t nbytes; + fd_set readfds; + + FD_ZERO(&readfds); + FD_SET(0, &readfds); + FD_SET(master, &readfds); + + if(select(master + 1, &readfds, NULL, NULL, NULL) < 0) { + if(errno == EINTR) continue; + break; + } + + if(FD_ISSET(0, &readfds)) { + nbytes = read(0, &c, 1); + if(nbytes < 1) break; + write(master, &c, 1); + } + + if(FD_ISSET(master, &readfds)) { + nbytes = read(master, &c, 1); + if(nbytes < 1) break; + slow_write(1, c); + } + } + + close(master); + while (waitpid(kidpid, &status, 0) < 0 && errno == EINTR) /* NOP */ ; + + tcsetattr(STDIN_FILENO, TCSANOW, &oldterm); + tcsetattr(STDOUT_FILENO, TCSANOW, &oldterm); + tcsetattr(STDERR_FILENO, TCSANOW, &oldterm); + + exit(WIFEXITED(status) ? WEXITSTATUS(status) : 127); +} + +void echo_args(char **args) { + starttime = NOW_USEC(); + while(*args) { + char *a = *args; + while(*a) slow_write(1, *a++); + args++; + if(*args) slow_write(1, ' '); + } + + slow_write(1, '\n'); + + if(debug) debug_stats(); + exit(0); +} + +void usage(int exitstat) { + printf("Usage: %s [<bits-per-sec>] [<file> [<file> ...]]\n", self); + printf(" With no filenames, reads stdin.\n"); + printf("or: %s [<bits-per-sec>] -c [<command> [<arg> ...]]\n", self); + printf(" With no command, spawns a shell.\n"); + printf("or: %s [<bits-per-sec>] -e <string> [<string> ...]\n", self); + printf("With no <bits-per-sec>, default rate is %d\n", DEFAULT_BPS); + exit(exitstat); +} + +int main(int argc, char **argv) { + int infd; + const char *p, *env_bps; + + if(getenv("SLOWBAUD_DEBUG")) debug = 1; + + self = argv[0]; + for(p = argv[0]; *p; p++) { + if(*p == '/') self = p + 1; + } + argv++, argc--; + + /* set bps. command line option overrides env, env overrides builtin */ + bps = DEFAULT_BPS; + + if( (env_bps = getenv("SLOWBAUD_BPS")) ) + bps = atoi(env_bps); + + if(*argv && (**argv > '0' && **argv <= '9')) { + char *e; + long b = strtol(*argv, &e, 0); + if(!*e) { + bps = (int)b; + argv++, argc--; + } + } + + if(bps < MIN_BPS || bps > MAX_BPS) { + fprintf(stderr, "%s: invalid bps, range %d - %d\n", self, MIN_BPS, MAX_BPS); + exit(1); + } + + /* if we used only integer math here, we couldn't support bps not + a multiple of 10 (e.g. 75 would be taken as 70). */ + interval = (unsigned long)(1000000.0L / ((double)bps / 10.0L)); + if(debug) fprintf(stderr, "interval %ld us\n", interval); + + if(*argv && argv[0][0] == '-' && argv[0][1] == '-' && !argv[0][2]) { + if(debug) fprintf(stderr, "skipping -- argument\n"); + argv++, argc--; + } + + setup_timer(); + + if(!argc) { + if(debug) fprintf(stderr, "no args, reading stdin\n"); + slowcat(0); + } else if(**argv == '-') { + switch(argv[0][1]) { + case 'c': + if(debug) fprintf(stderr, "-c given, creating a pty\n"); + slowtty(++argv); + break; + case 'e': + if(argc < 2) die("-e requires at least one argument"); + if(debug) fprintf(stderr, "-e given, echoing args\n"); + echo_args(++argv); + break; + case 'h': + case '?': + usage(0); + break; + default: + die("invalid option, try -h for help"); + break; + } + } else { + while(*argv) { + if(debug) fprintf(stderr, "opening file %s for reading\n", *argv); + if( (infd = open(*argv, O_RDONLY)) < 0) { + die(*argv); + } + slowcat(infd); + close(infd); + argv++; + } + } + exit(0); +} diff --git a/slowbaud.rst b/slowbaud.rst new file mode 100644 index 0000000..13e9e64 --- /dev/null +++ b/slowbaud.rst @@ -0,0 +1,117 @@ +.. RST source for slowbaud(1) man page. Convert with: +.. rst2man.py slowbaud.rst > slowbaud.1 +.. rst2man.py comes from the SBo development/docutils package. + +.. |version| replace:: 0.0.1 +.. |date| date:: + +======== +slowbaud +======== + +---------------------------------------- +simulate a low bitrate serial connection +---------------------------------------- + +:Manual section: 1 +:Manual group: Urchlay's Useless Stuff +:Date: |date| +:Version: |version| + +SYNOPSIS +======== + +**slowbaud** *<bits-per-sec>* [*<file>* ...] + +**slowbaud** *<bits-per-sec>* **-c** [*<command>* [*<arg>* ...]] + +DESCRIPTION +=========== + +slowbaud by default acts as a filter, or like the **cat(1)** command. It +reads files or its standard input, and writes the contents unmodified +to standard output... but slowly, at the given bits-per-second rate. +Output is unbuffered. + +With the **-c** option, it creates a pseudo-tty and runs the given command +in it (or an interactive shell, if no command is given). + +The *<bits-per-sec>* argument supports a range of 1 to 500000. Timing +accuracy depends on your OS, kernel config (HZ and/or NO_HZ on Linux), +and system load. No "fancy" techniques like realtime scheduling +or hardware event timers are used. At bitrates up to 57600, on a +typical unloaded Linux system, the timing should be around 99.7% +accurate. + +ENVIRONMENT +=========== + +**SLOWBAUD_DEBUG** + Set this (to any value) in the environment to see verbose debug + output on stderr, including timing accuracy stats. + +**SHELL** + Standard \*nix environment variable, used to determine what + shell to run when **-c** is given with no *<command>*. If + unset, **/bin/sh** is used. + +EXIT STATUS +=========== + +Without **-c**, 0 for success, non-zero on any error such as +nonexistent/unreadable files. slowbaud exits immediately on such +errors (this is unlike **cat(1)**). + +With **-c**, exit status is that of the child process, or 127 if +the child process couldn't be spawned (e.g. command not found). +Of course, the child process could also exit with status 127... + +NOTES +===== + +We can't really insert a delay between the bits of a byte, since +I/O is done with byte granularity. For calculation purposes, +*<bits-per-sec>* is divided by 10 to get bytes per second. This +simulates "8-N-1": one start bit, 8 data bits, no parity, and 1 stop +bit (total of 10 bits per byte). + +The timing code works by calculating how long to sleep after each +character (in microseconds), but actually sleeping slightly less +than that, then busy-waiting until the rest of the interval expires. +At slower bitrates, this works well, and the CPU overhead is barely +noticeable (at least on reasonably fast modern systems). + +The timing error will almost always result in the bitrate +being slightly too slow at lower bitrates and slightly too fast at +higher ones. + +Timing is more accurate on Linux than OSX. It's done with getitimer() +and sigwait(). This works out to be slightly more accurate than +using usleep() on both Linux and OSX. It would be possible to use +the realtime timer_create() and clock_gettime() API on Linux, for +possibly even better accuracy, but OSX doesn't have these (and I want to be +portable). + +If this were a truly useful application, it would be worth trying to +decrease latency further, with realtime process scheduling. I didn't +do this because slowbaud is just a toy, and because the RT stuff tends +to be unportable and require elevated privileges (root, or something +like setrtlimit or extended filesystem attributes to manage capabilities). + +About the name... I'm aware that "baud" is not synonymous with bps. I +just think "slowbaud" sounds better than "slowbps", as a name. Anyway +the stty command on Linux misuses the term ("speed 38400 baud"), so +I'm in good company. + +BUGS +==== + +With **-c**, signals aren't handled gracefully. Window size changes +(SIGWINCH) don't get propagated to the child process, and pressing ^C +doesn't interrupt the process. Yet. + +COPYRIGHT +========= + +slowbaud is copyright 2021, B. Watson <yalhcru@gmail.com>. Released +under the WTFPL. See http://www.wtfpl.net/txt/copying/ for details. |