// // ebkey.c // // Copyright (c) 2015 Paul Nicholson // All rights reserved. // // Redistribution and use in source and binary forms, with or without // modification, are permitted provided that the following conditions // are met: // 1. Redistributions of source code must retain the above copyright // notice, this list of conditions and the following disclaimer. // 2. Redistributions in binary form must reproduce the above copyright // notice, this list of conditions and the following disclaimer in the // documentation and/or other materials provided with the distribution. // // THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR // IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES // OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. // IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, // INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT // NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF // THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. // // http://abelian.org/ebnaut/ #define VERSION "0.4" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #ifndef TRUE #define FALSE 0 #define TRUE (!FALSE) #endif static double sym_period = 0; // Signal bit period static int Nsymbols = 0; // Number of output symbols (transmitted bits) static time_t Tstart = 0; // Transmit start time static int repeat = 0; // Non-zero seconds if -r given static int Wstate = 0; // Keyer state variable static int test_mode = FALSE; // TRUE if -t given static time_t last_utc = 0; // Last time UTC on screen updated static char *device = "/dev/ttyS0"; // Device to use static int gpio_port = -1; // GPIO port (not pin) number static int outstate = -1; // 0 or 1 and -1 indicates un-initialised // Output driver options #define DRIVER_RS232 1 #define DRIVER_RPGP 2 static int driver = DRIVER_RS232; // Array of symbols to send, read from stdin #define MAXSYM 100000 static int symbols[MAXSYM]; // Array of symbols to send // Display mode #define DISPLAY_NONE 0 #define DISPLAY_CURSES 1 static int display_mode = DISPLAY_NONE; // Set by -d option /////////////////////////////////////////////////////////////////////////////// // Utilities // /////////////////////////////////////////////////////////////////////////////// // // Terminate with a fatal error message. // static void bailout( char *, ...) __attribute__ ((format (printf, 1, 2))); static void bailout( char *format, ...) { va_list ap; char temp[ 200]; va_start( ap, format); vsprintf( temp, format, ap); va_end( ap); fprintf( stderr, "bailout: %s\n", temp); exit( 1); } static int loglevel = 0; // Incremented by -v options static void report( int, char *, ...) __attribute__ ((format (printf, 2, 3))); static void report( int n, char *format, ...) { va_list ap; char temp[ 200]; if( n > loglevel || display_mode != DISPLAY_NONE) return; va_start( ap, format); vsprintf( temp, format, ap); va_end( ap); fprintf( stderr, "%s\n", temp); fflush( stderr); } static char *format_hhmmss( double secs, int prec) { static char temp[50]; int isecs = (int) secs; sprintf( temp, "%02d:%02d:%0*.*f", isecs/3600, (isecs % 3600)/60, prec + 3, prec, secs - isecs/60 * 60 ); return temp; } static char *format_date( time_t when) { static char temp[100]; struct tm *tm = gmtime( &when); sprintf( temp, "%04d-%02d-%02d %02d:%02d:%02d", tm->tm_year + 1900, tm->tm_mon+1, tm->tm_mday, tm->tm_hour, tm->tm_min, tm->tm_sec); return temp; } static int atoin( char *s, int n) { int val = 0; while( n-- && isdigit( *s)) val = val * 10 + (*s++ - '0'); return val; } /////////////////////////////////////////////////////////////////////////////// // Display // /////////////////////////////////////////////////////////////////////////////// static void xyprintf( int x, int y, char *format, ...) { va_list ap; char temp[200]; va_start( ap, format); vsprintf( temp, format, ap); va_end( ap); int i; for( i = 0; temp[i]; i++) mvaddch( y, x+i, temp[i]); } // Hooks for curses or X display static void show_utc( time_t t) { switch( display_mode) { case DISPLAY_CURSES: xyprintf( 1, 3, "UTC: %s", format_date( t)); refresh(); break; } } static void show_info( void) { double duration = Nsymbols * sym_period; switch( display_mode) { case DISPLAY_CURSES: xyprintf( 1, 5, "Symbols: %d Symbol period: %.2f seconds", Nsymbols, sym_period); xyprintf( 1, 6, "Message duration: %.2f seconds, %s", duration, format_hhmmss( duration, 2)); refresh(); break; case DISPLAY_NONE: report( 1, "Loaded %d symbols", Nsymbols); report( 1, "Message duration: %.2f seconds, %s", duration, format_hhmmss( duration, 2)); break; } } static void show_start( void) { switch( display_mode) { case DISPLAY_CURSES: xyprintf( 1, 8, "Start time: %s", format_date( Tstart)); refresh(); break; case DISPLAY_NONE: report( 1, "Start time: %s", format_date( Tstart)); break; } } static void show_output( int state) { switch( display_mode) { case DISPLAY_CURSES: xyprintf( 1, 10, "Output state: %-3.3s", state ? "ON" : "OFF"); refresh(); break; } } static void show_status( char *, ...) __attribute__ ((format (printf, 1, 2))); static void show_status( char *format, ...) { va_list ap; char temp[ 200]; va_start( ap, format); vsprintf( temp, format, ap); va_end( ap); switch( display_mode) { case DISPLAY_CURSES: xyprintf( 1, 9, "Status: %-30.30s", temp); refresh(); break; case DISPLAY_NONE: printf( "%-79.79s\r", temp); fflush( stdout); break; } } /////////////////////////////////////////////////////////////////////////////// // Port Drivers // /////////////////////////////////////////////////////////////////////////////// static int dfd = -1; // Output file handle // // RS232 RTS and DTR // static int rs232_mode = 0; // 0 = DTR+RTS, 1 = DTR-RTS static void driver_rs232_set( int state) { int mcr; if( ioctl( dfd, TIOCMGET, &mcr) < 0) bailout( "i/o error %s: %s", device, strerror( errno)); if( state) { mcr |= TIOCM_RTS; if( !rs232_mode) mcr |= TIOCM_DTR; else mcr &= ~TIOCM_DTR; } else { mcr &= ~TIOCM_RTS; if( !rs232_mode) mcr &= ~TIOCM_DTR; else mcr |= TIOCM_DTR; } if( ioctl( dfd, TIOCMSET, &mcr) < 0) bailout( "i/o error %s: %s", device, strerror( errno)); } static void driver_rs232_init( void) { report( 1, "driver: RS232 %s", device); if( (dfd = open( device, O_RDWR | O_NOCTTY | O_NONBLOCK)) < 0) bailout( "cannot open %s: %s", device, strerror( errno)); struct termios t; tcgetattr( dfd, &t); cfmakeraw( &t); t.c_cflag |= CREAD | CLOCAL | HUPCL; t.c_cflag &= ~CRTSCTS; t.c_cflag &= ~PARENB; t.c_cflag &= ~CSTOPB; t.c_cflag |= CS8; cfsetospeed( &t, B9600); cfsetispeed( &t, B9600); if( tcsetattr( dfd, TCSAFLUSH, &t) < 0) bailout( "cannot initialise %s: %s", device, strerror( errno)); } // // Raspberry Pi GPIO. // static void driver_rpgp_set( int state) { if( write( dfd, state ? "1" : "0", 1) < 0) bailout( "i/o error gpio%d: %s", gpio_port, strerror( errno)); } static void driver_rpgp_init( void) { report( 1, "driver: RP gpio %d", gpio_port); FILE *f; // Try to close device in case already active, ignore errors if( (f = fopen( "/sys/class/gpio/unexport", "w")) != NULL) { fprintf( f, "%d", gpio_port); fclose( f); } // Open the GPIO port if( (f = fopen( "/sys/class/gpio/export", "w")) == NULL || fprintf( f, "%d", gpio_port) < 0 || fclose( f) < 0) bailout( "unable to export GPIO: %s", strerror( errno)); // Set direction to "out" char filename[100]; sprintf( filename, "/sys/class/gpio/gpio%d/direction", gpio_port); if( (f = fopen( filename, "w")) == NULL || fprintf( f, "out") < 0 || fclose( f) < 0) bailout( "unable to initialise GPIO: %s", strerror( errno)); // Prepare output handle for keyer sprintf( filename, "/sys/class/gpio/gpio%d/value", gpio_port); if( (dfd = open( filename, O_WRONLY)) < 0) bailout( "unable to open GPIO: %s", strerror( errno)); } /////////////////////////////////////////////////////////////////////////////// // Keyer // /////////////////////////////////////////////////////////////////////////////// static void set_output( int state) { show_output( state); switch( driver) { case DRIVER_RS232: driver_rs232_set( state); break; case DRIVER_RPGP: driver_rpgp_set( state); break; } outstate = state; } // // Called about every 10mS. // static int keyer( void) { static int cbit = -1; static long double last_status = 0; struct timeval tv; gettimeofday( &tv, NULL); // // Update the UTC display. // if( tv.tv_sec != last_utc) { last_utc = tv.tv_sec; show_utc( last_utc); } // // State 0: idle state. // if( Wstate == 0) return TRUE; // // State 1: waiting for start time to arrive. // if( Wstate == 1) { long double Tnow = tv.tv_sec + tv.tv_usec/1e6; double remains = Tstart - Tnow; if( remains > 0) { if( Tnow - last_status >= 0.2) { show_status( "Transmit starts in: %s", format_hhmmss( remains, 1)); last_status = Tnow; } return TRUE; } cbit = -1; Wstate = 2; } // // State 2: sending. // if( Wstate == 2) { long double elapsed = tv.tv_sec + tv.tv_usec/1e6 - Tstart; int bit = elapsed/sym_period; // Current bit number, 0 .. Nsymbols - 1 if( bit >= Nsymbols) // Send completed? { set_output( 0); if( repeat) // Repeat requested? If so, prepare the next start time { uint64_t now = tv.tv_sec; if( tv.tv_usec/1e6 > 0.5) now++; Tstart = (now / repeat + 1) * repeat; show_status( "Message completed"); report( 1, "Message completed"); show_start(); Wstate = 1; return TRUE; } Wstate = 3; return TRUE; } if( bit != cbit) // Time to send next output bit? { cbit = bit; int val = symbols[cbit] == '1' ? 1 : 0; set_output( val); show_status( "Sending bit %d/%d symbol=%d", cbit+1, Nsymbols, val); } } // // State 3: sending complete. // if( Wstate == 3) { show_status( "Transmit completed"); report( 1, "Transmit completed"); return FALSE; // Terminates main loop } // // State 10: test mode, toggle the output every sym_period. // if( Wstate == 10) { long double elapsed = tv.tv_sec + tv.tv_usec/1e6 - Tstart; int bit = elapsed/sym_period; if( bit != cbit) { cbit = bit; int val = bit & 1; set_output( val); if( display_mode == DISPLAY_NONE) show_status( "Output state: %s", val ? "ON" : "OFF"); else show_status( "Test mode running"); } } return TRUE; } /////////////////////////////////////////////////////////////////////////////// // Main // /////////////////////////////////////////////////////////////////////////////// static time_t parse_datetime( char *stamp) { time_t now = time( NULL); char *s = stamp; if( !strcasecmp( s, "now")) return now; // // Test for integer unix time. // if( strlen( s) == 10) { char *t = s; while( *t && isdigit( *t)) t++; if( !*t) return atol( s); } // // Test for time step // if( s[0] == '+') { int step = atoi( s+1); return (now / step + 1) * step; } struct tm *tm = gmtime( &now); // // Parse an optional date // yyyy-mm-dd or yyyymmdd // 0123456789 01234567 // int has_date = FALSE; if( isdigit( s[0]) && isdigit( s[1]) && isdigit( s[2]) && isdigit( s[3]) && (s[4] == '-' || s[4] == '/') && isdigit( s[5]) && isdigit( s[6]) && (s[7] == '-' || s[7] == '/') && isdigit( s[8]) && isdigit( s[9])) { tm->tm_year = atoin( s, 4) - 1900; tm->tm_mon = atoin( s+5, 2) - 1; tm->tm_mday = atoin( s+8, 2); s += 10; has_date = TRUE; } else if( isdigit( s[0]) && isdigit( s[1]) && isdigit( s[2]) && isdigit( s[3]) && isdigit( s[4]) && isdigit( s[5]) && isdigit( s[6]) && isdigit( s[7])) { tm->tm_year = atoin( s, 4) - 1900; tm->tm_mon = atoin( s+4, 2) - 1; tm->tm_mday = atoin( s+6, 2); s += 8; has_date = TRUE; } // Separator between date and time. if( has_date) { if( *s == ' ' || *s == '_' || *s == 'T' || *s == 't') s++; else if( !strncasecmp( s, "UT", 2)) s += 2; else bailout( "invalid date/time [%s]", stamp); } // // Parse a time // hh:mm:ss or hhmmss or hh:mm or hhmm // 01234567 012345 01234 or 0123 if( !*s) bailout( "missing time"); if( isdigit( s[0]) && isdigit( s[1]) && s[2] == ':' && isdigit( s[3]) && isdigit( s[4]) && s[5] == ':' && isdigit( s[6]) && isdigit( s[7])) { tm->tm_hour = atoin( s+0, 2); tm->tm_min = atoin( s+3, 2); tm->tm_sec = atoin( s+6, 2); s += 8; } else if( isdigit( s[0]) && isdigit( s[1]) && isdigit( s[2]) && isdigit( s[3]) && isdigit( s[4]) && isdigit( s[5])) { tm->tm_hour = atoin( s+0, 2); tm->tm_min = atoin( s+2, 2); tm->tm_sec = atoin( s+4, 2); s += 6; } else if( isdigit( s[0]) && isdigit( s[1]) && s[2] == ':' && isdigit( s[3]) && isdigit( s[4])) { tm->tm_hour = atoin( s+0, 2); tm->tm_min = atoin( s+3, 2); tm->tm_sec = 0; s += 5; } else if( isdigit( s[0]) && isdigit( s[1]) && isdigit( s[2]) && isdigit( s[3])) { tm->tm_hour = atoin( s+0, 2); tm->tm_min = atoin( s+2, 2); tm->tm_sec = 0; s += 4; } else bailout( "invalid time [%s]", s); if( *s) report( 0, "junk [%s] following date/time ignored", s); time_t secs = mktime( tm); if( secs < 0) bailout( "invalid date/time [%s]", stamp); // If the start time has already been passed, the user probably means // tomorrow. if( secs < now) { if( has_date) bailout( "start time has already passed"); secs += 86400; } return secs; } static void parse_mode( char *s) { while( s && *s) { char *p = strchr( s, ','); if( p) *p++ = 0; if( !strcmp( s, "dtr+rts") || !strcmp( s, "rts+dtr")) { driver = DRIVER_RS232; rs232_mode = 0; } else if( !strcmp( s, "dtr-rts") || !strcmp( s, "rts-dtr")) { driver = DRIVER_RS232; rs232_mode = 1; } else if( !strcmp( s, "rp")) driver = DRIVER_RPGP; else if( !strncmp( s, "device=", 7)) device = strdup( s+7); else if( !strncmp( s, "gpio=", 5)) gpio_port = atoi( s+5); else if( !strncmp( s, "dev=", 4)) device = strdup( s+4); else bailout( "unrecognised mode option: %s", s); s = p; } } static void usage( void) { fprintf( stderr, "ebkey version %s\n" "\n" "usage: ebkey [options] < bits.txt\n" "\n" "options:\n" "\n" " -S secs Symbol period (seconds, no default)\n" " -T time Start time (default now)\n" " -T yyyy-mm-dd hh:mm:ss\n" " -T yyyymmdd hhmmss\n" " -T hhmmss -T hhmm -T hh:mm:ss -T hh:mm\n" " -T +sss\n" " -r secs Repeat period\n" " -v Increase verbosity\n" " -t Test mode\n" " -d0 No display\n" " -d1 Curses display\n" "\n" "mode options:\n" "\n" " -m dtr+rts,dev=device RS232 DTR and RTS together\n" " -m dtr-rts,dev=device RS232 DTR and RTS opposite\n" " -m rp,gpio=pin Raspberry Pi GPIO\n" "\n" " mode examples:\n" " -m dtr+rts,dev=/dev/ttyS0\n" " -m rp,gpio=7\n", VERSION); exit( 1); } static void handle_signals( int sig) { if( outstate == 1) set_output( 0); if( display_mode == DISPLAY_CURSES) { curs_set( 0); endwin(); } bailout( "signal %d, %s", sig, strsignal( sig)); } int main( int argc, char *argv[]) { struct sigaction sa; sa.sa_handler = handle_signals; sigemptyset( &sa.sa_mask); sa.sa_flags = 0; sigaction( SIGINT, &sa, NULL); sigaction( SIGHUP, &sa, NULL); sigaction( SIGQUIT, &sa, NULL); sigaction( SIGTERM, &sa, NULL); sigaction( SIGFPE, &sa, NULL); sigaction( SIGBUS, &sa, NULL); sigaction( SIGSEGV, &sa, NULL); sigaction( SIGURG, &sa, NULL); sigaction( SIGSYS, &sa, NULL); sa.sa_handler = SIG_IGN; sigemptyset( &sa.sa_mask); sa.sa_flags = 0; sigaction( SIGPIPE, &sa, NULL); while( 1) { int c = getopt( argc, argv, "vtm:S:T:r:d:?"); if( c == 'v') loglevel++; else if( c == 'S') sym_period = atof( optarg); else if( c == 'T') Tstart = parse_datetime( optarg); else if( c == 'm') parse_mode( optarg); else if( c == 'r') repeat = atoi( optarg); else if( c == 't') test_mode = TRUE; else if( c == 'd') display_mode = atoi( optarg); else if( c == -1) break; else usage(); } if( display_mode != DISPLAY_NONE && display_mode != DISPLAY_CURSES) bailout( "invalid display mode %d selected", display_mode); if( !sym_period) bailout( "needs -S option for symbol period"); if( sym_period < 0.05) bailout( "invalid symbol period"); if( repeat < 0) bailout( "invalid repeat time"); if( !Tstart) { if( !test_mode) report( 0, "no -T option, assuming -T now"); Tstart = time( NULL); } // // Read symbols from stdin. // if( !test_mode) { int c = EOF; while( Nsymbols < MAXSYM && (c = getc( stdin)) != EOF && (c == '0' || c == '1')) symbols[Nsymbols++] = c; if( Nsymbols == MAXSYM) bailout( "max symbols %d", MAXSYM); if( c != EOF && c != '\n' && c != '\r') bailout( "bad character [%c] in input", c); if( !Nsymbols) bailout( "no symbols read from stdin"); } // // Setup display if requested. // if( display_mode == DISPLAY_CURSES) { initscr(); scrollok( stdscr, FALSE); clearok( stdscr, FALSE); nonl(); curs_set( 0); attrset( A_NORMAL); xyprintf( 30, 1, "EbNaut Keyer v%s", VERSION); xyprintf( 27, 22, "Use control-C to terminate"); refresh(); } // // Report a bunch of stuff. // if( !test_mode) { show_info(); show_start(); } else { show_status( "Test mode, %.2f second symbols", sym_period); // report( 1, "Test mode, %.2f second symbols", sym_period); } // // Set up the driver. // switch( driver) { case DRIVER_RS232: driver_rs232_init(); break; case DRIVER_RPGP: driver_rpgp_init(); break; } // // Drive the keyer state machine. // Wstate = test_mode ? 10 : 1; while( keyer()) usleep( 10000); return 0; }