/*********************************************************************** * gpiokeyer.c * by Robert A. French (KC4UPR) * * This file implements a General Purpose Input Output (GPIO) based * Morse Code / Continuous Wave (CW) keyer for Quisk. This was derived * from Richard (N1GP) Koch's Raspberry Pi iambic keyer code, available * at: github.com/n1gp/iambic-keyer * * Modifications included: * - Removing the GPIO sidetone functionality. Quisk will be creating * both the sidetone and the actual quadrature CW signal to feed to * the SDR. * - Modifying the keying logic (state machine) slightly. I don't know * if I improved it or not, but changes were based on my tracing thru * the code and updating things that seemed reasonable. * - Implementing GPIO key and T/R switching outputs, that could be * used to drive both an external CW generator, as well a T/R * switching with a configurable QSK/semi-QSK delay. * - Making parameters configurable/updateable during runtime by calls * from Quisk. * - Making the GPIO pins configurable at startup. * - Incorporating the keyer into Quisk's overall keying logic logic. * * Original N1GP changelog and licensing are below, as well as the notes * from the even previous source, a Verilog keyer for Hermes Lite. **********************************************************************/ /* 3/1/2020, Rob French / KC4UPR, I converted the code for use as a component of Quisk. 10/12/2016, Rick Koch / N1GP, I adapted Phil's verilog code from the openHPSDR Hermes iambic.v implementation to build and run on a raspberry PI 3. 1/7/2017, N1GP, adapted to work with Jack Audio, much better timing. ------------------------------------------------------------------------ This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library 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 Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; if not, write to the Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA. ------------------------------------------------------------------------ ------------------------------------------------------------------------ Copywrite (C) Phil Harman VK6PH May 2014 ------------------------------------------------------------------------ The code implements an Iambic CW keyer. The following features are supported: * Variable speed control from 1 to 60 WPM * Dot and Dash memory * Straight, Bug, Iambic Mode A or B Modes * Variable character weighting * Automatic Letter spacing * Paddle swap Dot and Dash memory works by registering an alternative paddle closure whilst a paddle is pressed. The alternate paddle closure can occur at any time during a paddle closure and is not limited to being half way through the current dot or dash. This feature could be added if required. In Straight mode, closing the DASH paddle will result in the output following the input state. This enables a straight morse key or external Iambic keyer to be connected. In Bug mode closing the dot paddle will send repeated dots. The difference between Iambic Mode A and B lies in what the keyer does when both paddles are released. In Mode A the keyer completes the element being sent when both paddles are released. In Mode B the keyer sends an additional element opposite to the one being sent when the paddles are released. This only effects letters and characters like C, period or AR. Automatic Letter Space works as follows: When enabled, if you pause for more than one dot time between a dot or dash the keyer will interpret this as a letter-space and will not send the next dot or dash until the letter-space time has been met. The normal letter-space is 3 dot periods. The keyer has a paddle event memory so that you can enter dots or dashes during the inter-letter space and the keyer will send them as they were entered. Speed calculation - Using standard PARIS timing, dot_period(mS) = 1200/WPM */ //#define DEBUG #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include /*********************************************************************** * Constants **********************************************************************/ #define NSEC_PER_SEC (1000000000) #define CW_HANGTIME_DEFAULT (250) #define NUM_PARMS (5) /*********************************************************************** * Types **********************************************************************/ /* KC4UPR: I added an enumeration for the keyer modes, instead of a * macro. Probably superfluous. */ enum { KEYER_STRAIGHT = 0, KEYER_MODE_A, KEYER_MODE_B, NUM_KEYER_MODES }; /* KC4UPR: Enumeration of the states in the iambic keyer state machine. * I added the HANGTIME state, which is used to differentiate between * the end of the last CW symbol, and the end of the transmission delay * for QSK/semi-QSK operations. */ enum { CHECK = 0, PREDOT, PREDASH, SENDDOT, SENDDASH, DOTDELAY, DASHDELAY, DOTHELD, DASHHELD, LETTERSPACE, HANGTIME, EXITLOOP }; /*********************************************************************** * Function Prototypes * * These are only prototypes for the internal functions... the functions * that are callable from quisk.c and/or Python are all declared in * quisk.h, and then defined at the end of this file. **********************************************************************/ static void keyer_update(void); static void keyer_event(int, int, uint32_t); static void keyer_event_left(void); static void keyer_event_right(void); static void clear_memory(void); static void set_keyer_out(int); static void* keyer_thread(void *); /*********************************************************************** * Variables **********************************************************************/ /* GPIO Pins * * KC4UPR: Converted the GPIO pin definitions from macros to variables, * so that we can set them dynamically at runtime. */ static int left_paddle_gpio = 22; static int right_paddle_gpio = 27; static int keyer_out_gpio = 23; static int tr_switch_gpio = 24; /* Keyer Configuration */ static int cw_keyer_mode = KEYER_MODE_A; static int cw_keyer_speed = 20; static int cw_keyer_weight = 55; static int cw_keys_reversed = 0; static int cw_keyer_spacing = 0; static int cw_keyer_enabled = 1; static int cw_hangtime_msec = CW_HANGTIME_DEFAULT; static int cw_active_state = 0; // not currently (re-)configurable /* Current Paddle State */ static int kcwl = 0; // left paddle static int kcwr = 0; // right paddle static int *kdot; // dots - point to either left or right static int *kdash; // dashes - point to either right or left /* Internal Keyer State */ static int dot_memory = 0; static int dash_memory = 0; static int key_state = 0; static int kdelay = 0; static int dot_delay = 0; static int dash_delay = 0; static int running, keyer_out = 0; /* Thread Variables */ static pthread_t keyer_thread_id; static sem_t cw_event; #if defined(DEBUG) static int cw_event_value; #endif /*********************************************************************** * Internal functions **********************************************************************/ /* KC4UPR: Update the keyer, i.e. calculate its internal values based * on current settings. This is called once at keyer open, and then * again any time certain settings are updated. */ static void keyer_update() { dot_delay = 1200 / cw_keyer_speed; // will be 3 * dot length at standard weight dash_delay = (dot_delay * 3 * cw_keyer_weight) / 50; if (cw_keys_reversed) { kdot = &kcwr; kdash = &kcwl; } else { kdot = &kcwl; kdash = &kcwr; } /* NOTE: need to actually dynamically set this at some point... * Currently, based on the code below, there is a hardcoded 250ms * semi-QSK delay. ~250ms after the end of the last symbol, the * T/R switch will be disabled. */ // cw_hangtime_msec = (int)(0.25f * 1000.0f); } /* KC4UPR: The "actual" event handler. Updates the value of the * currently depressed/released paddle. */ static void keyer_event(int gpio, int level, uint32_t tick) { // cw_active_state is for the logic level (active high vs low). int state = (cw_active_state == 0) ? (level == 0) : (level != 0); if (gpio == left_paddle_gpio) kcwl = state; else // RIGHT_PADDLE_GPIO kcwr = state; /* Post (increment) the semaphore. Why? Because the keyer thread * may be waiting on it, so this lets the keyer thread begin running * the state machine if it was currently between sequences. */ if (state) sem_post(&cw_event); } /* KC4UPR: Helper function added to support WiringPi instead of PiGPIO. * PiGPIO supports using the same callback for multiple pins, because * the pin is one of the parameters to the interrupt handler. However, * I needed to use WiringPi instead of PiGPIO due to PiGPIO not working * with my soundcard "hat" for the Raspberry Pi. */ static void keyer_event_left() { int level = digitalRead(left_paddle_gpio); keyer_event(left_paddle_gpio, level, 0); #if defined(DEBUG) /* KC4UPR: I added current value of the semaphore because I was * curious as to how many times this ISR is getting called. For * whatever reason, despite my debounce circuit on this pin, * this is getting called a lot... */ sem_getvalue(&cw_event, &cw_event_value); fprintf(stdout, "[GPIO Keyer] left paddle pressed; semaphore value: %d\n", cw_event_value); #endif } /* KC4UPR: Helper function added to support WiringPi instead of PiGPIO. * PiGPIO supports using the same callback for multiple pins, because * the pin is one of the parameters to the interrupt handler. However, * I needed to use WiringPi instead of PiGPIO due to PiGPIO not working * with my soundcard "hat" for the Raspberry Pi. */ static void keyer_event_right() { int level = digitalRead(right_paddle_gpio); keyer_event(right_paddle_gpio, level, 0); #if defined(DEBUG) /* KC4UPR: I added current value of the semaphore because I was * curious as to how many times this ISR is getting called. For * whatever reason, despite my debounce circuit on this pin, * this is getting called a lot... */ sem_getvalue(&cw_event, &cw_event_value); fprintf(stdout, "[GPIO Keyer] right paddle pressed; semaphore value: %d\n", cw_event_value); #endif } /* KC4UPR: Clear the dot and dash memory of the iambic keyer. */ static void clear_memory() { dot_memory = 0; dash_memory = 0; } /* KC4UPR: Set the keyer output (which will be read by the function * is_key_down_gpiokeyer(), when called by Quisk). Also writes the * digital output GPIO pins, if they are being used. */ static void set_keyer_out(int state) { /* First, write to the hardware T/R switch, if we are using it, and * if the specific keyer state is key_down. A logical AND is used * here instead of bitwise, because tr_switch_gpio could have * multiple valid values. */ if (state && tr_switch_gpio) digitalWrite(tr_switch_gpio, 1 & cw_keyer_enabled); /* Set the key state. This is what will be read by Quisk. */ keyer_out = state; /* Finally, write to the hardware CW key line, if it was specified. */ if (keyer_out_gpio) digitalWrite(keyer_out_gpio, keyer_out & cw_keyer_enabled); } /* KC4UPR: Keyer thread, which processes the key state and moves * through the state machines. */ static void* keyer_thread(void *arg) { struct timespec loop_delay; int interval = 1000000; // 1 ms int hangtime_elapsed = 0; while (running) { #if defined(DEBUG) /* KC4UPR: Writing out the current semaphore value because * I was curious at the impact of bouncing on how often the * interrupt handler was getting called. */ sem_getvalue(&cw_event, &cw_event_value); fprintf(stdout, "[GPIO Keyer] waiting; semaphore value: %d\n", cw_event_value); #endif sem_wait(&cw_event); key_state = CHECK; while (key_state != EXITLOOP) { /* Anytime the keyer output is set (i.e. we are transmitting * a symbol), reset the elapsed hangtime. Once we complete * sending the current symbol (dit/dah/straight key out), we * will start the hangtime counter for the T/R switch. */ if (keyer_out) hangtime_elapsed = 0; switch(key_state) { case HANGTIME: if (hangtime_elapsed >= cw_hangtime_msec) { if (tr_switch_gpio) { #if defined(DEBUG) sem_getvalue(&cw_event, &cw_event_value); fprintf(stdout, "[GPIO Keyer] hangtime complete, %d msec; semaphore value: %d\n", cw_hangtime_msec, cw_event_value); #endif digitalWrite(tr_switch_gpio, 0); key_state = EXITLOOP; } } /* KC4UPR: The HANGTIME state is intentionally first in * the switch() statement, and it intentionally does NOT * have a break at the end of it. I want it to fall- * through into the CHECK state logic, so it can look to * see if there are any switch actuations, which would * then interrupt the HANGTIME state and start the state * machine over. This is probably terrible coding * practice... */ case CHECK: // check for key press if (cw_keyer_mode == KEYER_STRAIGHT) { // Straight/External key or bug if (*kdash) { // send manual dashes if (!keyer_out) { set_keyer_out(1); key_state = HANGTIME; //EXITLOOP; } } else if (*kdot) // and automatic dots key_state = PREDOT; else if (key_state == HANGTIME) { if (keyer_out) set_keyer_out(0); } else { key_state = EXITLOOP; } } else { if (*kdot) key_state = PREDOT; else if (*kdash) key_state = PREDASH; else if (key_state != HANGTIME) { key_state = EXITLOOP; } } break; case PREDOT: // need to clear any pending dots or dashes clear_memory(); key_state = SENDDOT; break; case PREDASH: clear_memory(); key_state = SENDDASH; break; // dot paddle pressed so set keyer_out high for time dependant on speed // also check if dash paddle is pressed during this time case SENDDOT: set_keyer_out(1); if (kdelay == dot_delay) { kdelay = 0; set_keyer_out(0); key_state = DOTDELAY; // add inter-character spacing of one dot length } else kdelay++; // if Mode A and both paddels are relesed then clear dash memory if (cw_keyer_mode == KEYER_MODE_A) { if (!*kdot & !*kdash) dash_memory = 0; } if (*kdash) { // set dash memory dash_memory = 1; } break; // dash paddle pressed so set keyer_out high for time dependant on 3 x dot delay and weight // also check if dot paddle is pressed during this time case SENDDASH: set_keyer_out(1); if (kdelay == dash_delay) { kdelay = 0; set_keyer_out(0); key_state = DASHDELAY; // add inter-character spacing of one dot length } else kdelay++; // if Mode A and both padles are relesed then clear dot memory if (cw_keyer_mode == KEYER_MODE_A) { if (!*kdot & !*kdash) dot_memory = 0; } if (*kdot) { // set dot memory dot_memory = 1; } break; // add dot delay at end of the dot and check for dash memory, then check if paddle still held case DOTDELAY: if (kdelay == dot_delay) { kdelay = 0; if(!*kdot && cw_keyer_mode == KEYER_STRAIGHT) // just return if in bug mode key_state = HANGTIME; else if (dash_memory) // dash has been set during the dot so service key_state = PREDASH; else key_state = DOTHELD; // dot is still active so service } else kdelay++; if (*kdash) { // set dash memory dash_memory = 1; } break; // add dot delay at end of the dash and check for dot memory, then check if paddle still held case DASHDELAY: if (kdelay == dot_delay) { kdelay = 0; if (dot_memory) // dot has been set during the dash so service key_state = PREDOT; else key_state = DASHHELD; // dash is still active so service } else kdelay++; if (*kdot) { // set dot memory dot_memory = 1; } break; // check if dot paddle is still held, if so repeat the dot. Else check if Letter space is required case DOTHELD: if (*kdot) // dot has been set during the dash so service key_state = PREDOT; else if (*kdash) // has dash paddle been pressed key_state = PREDASH; else if (cw_keyer_spacing) { // Letter space enabled so clear any pending dots or dashes clear_memory(); key_state = LETTERSPACE; } else key_state = HANGTIME; break; // check if dash paddle is still held, if so repeat the dash. Else check if Letter space is required case DASHHELD: if (*kdash) // dash has been set during the dot so service key_state = PREDASH; else if (*kdot) // has dot paddle been pressed key_state = PREDOT; else if (cw_keyer_spacing) { // Letter space enabled so clear any pending dots or dashes clear_memory(); key_state = LETTERSPACE; } else key_state = HANGTIME; break; // Add letter space (3 x dot delay) to end of character and check if a paddle is pressed during this time. // Actually add 2 x dot_delay since we already have a dot delay at the end of the character. case LETTERSPACE: if (kdelay == 2 * dot_delay) { kdelay = 0; if (dot_memory) // check if a dot or dash paddle was pressed during the delay. key_state = PREDOT; else if (dash_memory) key_state = PREDASH; else key_state = HANGTIME; // no memories set so restart } else kdelay++; // save any key presses during the letter space delay if (*kdot) dot_memory = 1; if (*kdash) dash_memory = 1; break; default: key_state = EXITLOOP; } /* If the key out state is 0 (not sending), increment the * hangtime elapsed timer. This will continue incrementing * while we are in the HANGTIME state, until either the * timer completes, or keyer out goes to 1. */ if (!keyer_out) hangtime_elapsed++; if (key_state != EXITLOOP) { clock_gettime(CLOCK_MONOTONIC, &loop_delay); loop_delay.tv_nsec += interval; while (loop_delay.tv_nsec >= NSEC_PER_SEC) { loop_delay.tv_nsec -= NSEC_PER_SEC; loop_delay.tv_sec++; } clock_nanosleep(CLOCK_MONOTONIC, TIMER_ABSTIME, &loop_delay, NULL); } } } #if defined(DEBUG) fprintf(stdout, "[GPIO Keyer] thread should be ending now\n"); #endif return arg; // did this to ditch compiler warnings... bad idea? } /*********************************************************************** * is_key_down.c functions * * These functions are called from is_key_down.c, and implement the * method-specific open/close/set functions for the keyer. **********************************************************************/ /* KC4UPR: Open the GPIO keyer. Its name must start with "gpio". The * overall format is: * gpio:left,right,keyer_out,tr_switch * where: * left - GPIO input pin for the left paddle * right - GPIO input pin for the right paddle * keyer_out - GPIO output pin for the key (actual CW) signal * (unlike the tr_switch line, this is only keyed * for the actual dits and dahs) * (set to zero to not use this) * tr_switch - GPIO output pin for T/R switching (this is * enabled any time keyer_out is enabled, plus a * short, configurable delay after the last time * the keyer output was set) * (set to zero to not use this) * NOTE: The pin numbers to be used are GPIO pin numbers, NOT WiringPi * pin numbers even though WiringPi is used!!! */ int open_key_gpiokeyer(const char * name) { int i; int parms[NUM_PARMS]; /* KC4UPR: Read the GPIO keyer name, and parse out the pins for * left paddle, right paddle, keyer out, and T/R switch. The last * two can be zero if that functionality is not desired, but they * must be present in the name string. */ switch(sscanf(name, "gpio:%d,%d,%d,%d,%d", &parm[0], &parm[1], &parm[2], &parm[3], &parm[4])) { case 5: // v0.2 left_paddle_gpio = parm[0]; right_paddle_gpio = parm[1]; keyer_out_gpio = parm[2]; tr_switch_gpio = parm[3]; quisk_set_gpio_keyer_hangtime(parm[4]); break; case 4: // v0.1 /* KC4UPR: matched the first 4 inputs - original v0.1 interface. */ left_paddle_gpio = parm[0]; right_paddle_gpio = parm[1]; keyer_out_gpio = parm[2]; tr_switch_gpio = parm[3]; quisk_set_gpio_keyer_hangtime(CW_HANGTIME_DEFAULT); break; case 3: // v0.2 /* KC4UPR: three inputs--no discrete outputs, but hangtime set. */ left_paddle_gpio = parm[0]; right_paddle_gpio = parm[1]; keyer_out_gpio = 0; tr_switch_gpio = 0; quisk_set_gpio_keyer_hangtime(parm[2]); break; case 2: // v0.2 /* KC4UPR: only two inputs--the discrete outputs won't be used. */ left_paddle_gpio = parm[0]; right_paddle_gpio = parm[1]; keyer_out_gpio = 0; tr_switch_gpio = 0; quisk_set_gpio_keyer_hangtime(CW_HANGTIME_DEFAULT); break; default: fprintf(stderr, "[GPIO Keyer] insufficient parameters: %s\n", name); return -1; } #if defined(DEBUG) fprintf(stdout, "[GPIO Keyer] configuration:\n - left paddle GPIO: %d\n - right paddle GPIO: %d\n - keyer out GPIO: %d\n - T/R switch GPIO: %d\n", left_paddle_gpio, right_paddle_gpio, keyer_out_gpio, tr_switch_gpio); #endif /* KC4UPR: Setup WiringPi. Note that I have specified using the * GPIO pin names rather than the WiringPi pin names. */ if (wiringPiSetupGpio () < 0) { fprintf(stderr, "[GPIO Keyer] unable to setup wiringPi: %s\n", strerror(errno)); return -1; } /* KC4UPR: Setup the right paddle pin for input. An interrupt will * be triggered on both key down and key up. */ pinMode(right_paddle_gpio, INPUT); pullUpDnControl(right_paddle_gpio, PUD_UP); usleep(100000); wiringPiISR(right_paddle_gpio, INT_EDGE_BOTH, keyer_event_right); /* KC4UPR: Setup the left paddle pin for input. An interrupt will * be triggered on both key down and key up. */ pinMode(left_paddle_gpio, INPUT); pullUpDnControl(left_paddle_gpio, PUD_UP); usleep(100000); wiringPiISR(left_paddle_gpio, INT_EDGE_BOTH, keyer_event_left); /* KC4UPR: Setup the keyer output pin as an output pin, IF the * value is not 0. If the pin value is 0, then this output is * disabled. */ if (keyer_out_gpio) { pinMode(keyer_out_gpio, OUTPUT); digitalWrite(keyer_out_gpio, 0); } /* KC4UPR: Setup the T/R switch pin as an output pin, IF the * value is not 0. If the pin value is 0, then this output is * disabled. */ if (tr_switch_gpio) { pinMode(tr_switch_gpio, OUTPUT); digitalWrite(tr_switch_gpio, 0); } keyer_update(); i = sem_init(&cw_event, 0, 0); running = 1; i |= pthread_create(&keyer_thread_id, NULL, keyer_thread, NULL); if(i < 0) { fprintf(stderr,"[GPIO Keyer] pthread_create for keyer_thread failed %d\n", i); return -1; } return 0; } /* KC4UPR: Close the GPIO keyer. This sets the running flag for the * keyer thread to false, posts the semaphore (to ensure the keyer * thread gets off of a semaphore wait), and then joins the thread to * close it. * * NOTE: If Quisk is not closed after closing the GPIO keyer, the * interrupt handlers for the GPIO pins remain present. I think I can * possibly fix this by assigning a NULL ISR??? */ void close_key_gpiokeyer(void) { #if defined(DEBUG) fprintf(stdout, "[GPIO Keyer] closing keyer\n"); #endif running = 0; sem_post(&cw_event); pthread_join(keyer_thread_id, 0); sem_destroy(&cw_event); #if defined(DEBUG) fprintf(stdout, "[GPIO Keyer] keyer closed\n"); #endif } /* KC4UPR: Return the state of the keyer. This is a logic AND of the * current keyer_out state, and the cw_keyer_enabled signal. */ int is_key_down_gpiokeyer(void) { static int retval; retval = keyer_out & cw_keyer_enabled; return retval; } /*********************************************************************** * Externally callable functions * * These functions are callable from Quisk proper, as well as from any * Quisk Python modules. They are declared in quisk.h, and are used to * (re-)configure various keyer parameters. **********************************************************************/ /* KC4UPR: Set the keyer mode. Keyer modes are defined as an * enumeration in this file. Current enums are: * KEYER_STRAIGHT = 0 * KEYER_MODE_A = 1 * KEYER_MODE_B = 2 * If the requested mode is out of bounds, nothing happens... */ void quisk_set_gpio_keyer_mode(int mode) { #if defined(DEBUG) fprintf(stdout, "[GPIO Keyer] mode change: %d - %s\n", mode, ((mode>-1)&&(mode -1) && (mode < NUM_KEYER_MODES)) cw_keyer_mode = mode; } /* KC4UPR: Set the keyer speed in WPM. Valid from 1-60 WPM. */ void quisk_set_gpio_keyer_speed(int wpm) { #if defined(DEBUG) fprintf(stdout, "[GPIO Keyer] speed change: %d - %s\n", wpm, ((wpm>0)&&(wpm<61))?"success":"failed "); #endif if ((wpm > 0) && (wpm < 61)) { cw_keyer_speed = wpm; keyer_update(); } } /* KC4UPR: Set the keyer weight in percent (?). Valid from 33-66. */ void quisk_set_gpio_keyer_weight(int weight) { #if defined(DEBUG) fprintf(stdout, "[GPIO Keyer] weight change: %d - %s\n", weight, ((weight>32)&&(weight<67))?"success":"failed "); #endif if ((weight > 32) && (weight < 67)) { cw_keyer_weight = weight; keyer_update(); } } /* KC4UPR: Reverse the paddles. Default: left = dit, right = dah. */ void quisk_set_gpio_keyer_reversed(int flag) { #if defined(DEBUG) fprintf(stdout, "[GPIO Keyer] reverse paddles: %s\n", flag==0?"false":"true "); #endif cw_keys_reversed = (flag == 0 ? 0 : 1); keyer_update(); } /* KC4UPR: Set strict character spacing. Default: off. */ void quisk_set_gpio_keyer_strict(int flag) { #if defined(DEBUG) fprintf(stdout, "[GPIO Keyer] strict spacing: %s\n", flag==0?"false":"true "); #endif cw_keyer_spacing = (flag == 0 ? 0 : 1); } /* KC4UPR: Enable/disable the keyer. If disabled, then regardless of * state, nothing will be output. */ void quisk_set_gpio_keyer_enabled(int flag) { #if defined(DEBUG) fprintf(stdout, "[GPIO Keyer] enabled: %s\n", flag==0?"false":"true "); #endif cw_keyer_enabled = (flag == 0 ? 0 : 1); } /* KC4UPR: Set the keyer "hangtime". This is the delay that the T/R * switch remains set after the last symbol was transmitted (the default * is 250 msec, if this is never called). Can be set from 0 to 1000 * msec. */ void quisk_set_gpio_keyer_hangtime(int msec) { #if defined(DEBUG) fprintf(stdout, "[GPIO Keyer] hangtime: %d msec\n", msec) #endif if ((msec > -1) && (msec < 1001)) { cw_hangtime_msec = msec; } } /*********************************************************************** * EOF **********************************************************************/