From: c vw Date: Tue, 6 Nov 2018 08:38:34 +0000 (+0100) Subject: - updated the CAT CW stuff, now maintaining a ring buffer. X-Git-Url: https://git.rkrishnan.org/pf/content/en/seg/status?a=commitdiff_plain;h=5b037d0fee71b1ce7c16e56735942a69cbcf1de6;p=pihpsdr.git - updated the CAT CW stuff, now maintaining a ring buffer. Several corrections such that hitting a "local" CW key aborts CAT CW correctly and continues with manual CW. - moved choice of active level for CW buttons to GPIO menu - added support for "CW letter spacing" in CW menu - at many places corrected cast from a generic pointer to int. The correct way of passing an int when receiving a pointer is: void extern(void *p) { int state=(uintptr_t) p; call intern(state); } What is certainly wrong is "call intern((uintptr_t) p)" since this passes on many systems a 64-bit value where a 32-bit value is expected unless strong prototyping is used. - cleaned up configure.c (at least: "gtk_widget_destroy(dialog)" in configure_gpio had to be deleted) - use cw_breakin instead of vox_hang in CW-keyer - improved GPIO side tone creation, no longer use softTone but do this within the keyer thread (but only if GPIO side tone is requested). - Hide button names such as CWL_BUTTON from iambic.c - Major overhaul of iambic keyer: see extensive comment in iambic.c --- diff --git a/Makefile b/Makefile index d0686e9..5449783 100644 --- a/Makefile +++ b/Makefile @@ -141,14 +141,9 @@ endif ifeq ($(LOCALCW_INCLUDE),LOCALCW) LOCALCW_OPTIONS=-D LOCALCW -LOCALCW_SOURCES= \ -beep.c \ -iambic.c -LOCALCW_HEADERS= \ -beep.h \ -iambic.h -LOCALCW_OBJS= \ -iambic.o +LOCALCW_SOURCES= iambic.c +LOCALCW_HEADERS= iambic.h +LOCALCW_OBJS = iambic.o endif ifeq ($(GPIO_INCLUDE),GPIO) diff --git a/Makefile.mac b/Makefile.mac index 50ae5c2..edbb63f 100644 --- a/Makefile.mac +++ b/Makefile.mac @@ -139,14 +139,9 @@ endif ifeq ($(LOCALCW_INCLUDE),LOCALCW) LOCALCW_OPTIONS=-D LOCALCW -LOCALCW_SOURCES= \ -beep.c \ -iambic.c -LOCALCW_HEADERS= \ -beep.h \ -iambic.h -LOCALCW_OBJS= \ -iambic.o +LOCALCW_SOURCES= iambic.c +LOCALCW_HEADERS= iambic.h +LOCALCW_OBJS= iambic.o endif ifeq ($(GPIO_INCLUDE),GPIO) diff --git a/cw_menu.c b/cw_menu.c index 21ddb9e..14ec79a 100644 --- a/cw_menu.c +++ b/cw_menu.c @@ -33,6 +33,7 @@ #include "receiver.h" #include "new_protocol.h" #include "old_protocol.h" +#include "iambic.h" static GtkWidget *parent_window=NULL; @@ -40,6 +41,14 @@ static GtkWidget *menu_b=NULL; static GtkWidget *dialog=NULL; +void cw_changed() { +// inform the local keyer about CW parameter changes +// (only if LOCALCW is active). +#ifdef LOCALCW + keyer_update(); +#endif +} + static void cleanup() { if(dialog!=NULL) { gtk_widget_destroy(dialog); @@ -63,6 +72,11 @@ static void cw_keyer_internal_cb(GtkWidget *widget, gpointer data) { cw_changed(); } +static void cw_keyer_spacing_cb(GtkWidget *widget, gpointer data) { + cw_keyer_spacing=cw_keyer_spacing==1?0:1; + cw_changed(); +} + static void cw_keyer_speed_value_changed_cb(GtkWidget *widget, gpointer data) { cw_keyer_speed=gtk_spin_button_get_value_as_int(GTK_SPIN_BUTTON(widget)); cw_changed(); @@ -235,11 +249,16 @@ void cw_menu(GtkWidget *parent) { #ifdef LOCALCW GtkWidget *cw_keyer_internal_b=gtk_check_button_new_with_label("CW Internal"); - //gtk_widget_override_font(cw_keyer_internal_b, pango_font_description_from_string("Arial 18")); gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (cw_keyer_internal_b), cw_keyer_internal); gtk_widget_show(cw_keyer_internal_b); gtk_grid_attach(GTK_GRID(grid),cw_keyer_internal_b,0,10,1,1); g_signal_connect(cw_keyer_internal_b,"toggled",G_CALLBACK(cw_keyer_internal_cb),NULL); + + GtkWidget *cw_keyer_spacing_b=gtk_check_button_new_with_label("CW enforce letter spacing"); + gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (cw_keyer_spacing_b), cw_keyer_spacing); + gtk_widget_show(cw_keyer_spacing_b); + gtk_grid_attach(GTK_GRID(grid),cw_keyer_spacing_b,0,11,1,1); + g_signal_connect(cw_keyer_spacing_b,"toggled",G_CALLBACK(cw_keyer_spacing_cb),NULL); #endif gtk_container_add(GTK_CONTAINER(content),grid); @@ -249,4 +268,3 @@ void cw_menu(GtkWidget *parent) { gtk_widget_show_all(dialog); } - diff --git a/gpio.c b/gpio.c index 5828b8f..b00a0c4 100644 --- a/gpio.c +++ b/gpio.c @@ -29,9 +29,6 @@ #include #include #include -#ifdef LOCALCW -#include -#endif #include #include "band.h" @@ -1011,7 +1008,7 @@ static void cwAlert_left() { level=digitalRead(CWL_BUTTON); //fprintf(stderr,"cwl button : level=%d \n",level); //the second parameter of keyer_event ("state") is TRUE on key-down - keyer_event(CWL_BUTTON, CW_ACTIVE_LOW ? (level==0) : level); + keyer_event(1, CW_ACTIVE_LOW ? (level==0) : level); } static void cwAlert_right() { @@ -1019,11 +1016,13 @@ static void cwAlert_right() { if (cw_keyer_internal != 0) return; // as quickly as possible level=digitalRead(CWR_BUTTON); //fprintf(stderr,"cwr button : level=%d \n",level); - keyer_event(CWR_BUTTON, CW_ACTIVE_LOW ? (level==0) : level); + keyer_event(0, CW_ACTIVE_LOW ? (level==0) : level); } // -// Two functions added, might be useful somewhere +// The following functions are an interface for +// other parts to access CW gpio functions +// (query left and right paddle, set sidetone output) // int gpio_left_cw_key() { int val=digitalRead(CWL_BUTTON); @@ -1034,6 +1033,16 @@ int gpio_right_cw_key() { int val=digitalRead(CWR_BUTTON); return CW_ACTIVE_LOW? (val==0) : val; } + +int gpio_cw_sidetone_enabled() { + return ENABLE_GPIO_SIDETONE; +} + +void gpio_cw_sidetone_set(int level) { + if (ENABLE_GPIO_SIDETONE) { + digitalWrite(SIDETONE_GPIO, level); + } +} #endif int gpio_init() { @@ -1167,7 +1176,12 @@ int gpio_init() { setup_cw_pin(CWR_BUTTON, cwAlert_right); } if (ENABLE_GPIO_SIDETONE) { - softToneCreate(SIDETONE_GPIO); +// +// use this pin as an output pin and +// set its value to LOW +// + pinMode(SIDETONE_GPIO, OUTPUT); + digitalWrite(SIDETONE_GPIO, 0); } #endif @@ -1178,14 +1192,6 @@ void gpio_close() { running=0; } -#ifdef LOCALCW -void gpio_sidetone(int freq) { - if (ENABLE_GPIO_SIDETONE) { - softToneWrite (SIDETONE_GPIO, freq); - } -} -#endif - int vfo_encoder_get_pos() { int pos=vfoEncoderPos; @@ -1466,12 +1472,9 @@ static int e4_encoder_changed(void *data) { static gpointer rotary_encoder_thread(gpointer data) { int pos; - // ignore startup glitches sleep(2); - //g_mutex_lock(&m_running); running=1; - //g_mutex_unlock(&m_running); while(1) { pos=vfo_encoder_get_pos(); @@ -1514,17 +1517,12 @@ static gpointer rotary_encoder_thread(gpointer data) { mox_state = 0; lock_state = 0; #endif -//fprintf(stderr,"gpio_thread: lock\n"); - //g_mutex_lock(&m_running); if(running==0) { -fprintf(stderr,"gpio_thread: unlock (running==0)\n"); - //g_mutex_unlock(&m_running); + fprintf(stderr,"gpio_thread: quitting (running==0)\n"); g_thread_exit(NULL); } usleep(100000); -//fprintf(stderr,"gpio_thread: unlock (running==1)\n"); - //g_mutex_unlock(&m_running); } return NULL; } diff --git a/gpio.h b/gpio.h index 1075d5d..e2253f3 100644 --- a/gpio.h +++ b/gpio.h @@ -87,9 +87,10 @@ extern int SIDETONE_GPIO; extern int ENABLE_GPIO_SIDETONE; extern int ENABLE_CW_BUTTONS; extern int CW_ACTIVE_LOW; -void gpio_sidetone(int freq); +void gpio_cw_sidetone_set(int level); int gpio_left_cw_key(); int gpio_right_cw_key(); +int gpio_cw_sidetone_enabled(); #endif void gpio_restore_state(); diff --git a/iambic.c b/iambic.c index 0065748..70bdde6 100644 --- a/iambic.c +++ b/iambic.c @@ -56,6 +56,141 @@ * Speed calculation - Using standard PARIS timing, dot_period(mS) = 1200/WPM */ +/* + *************************************************************************************************************** + * Description of a major overhaul, + * Sep/Oct/Nov 2018, by DL1YCF Christoph van Wullen + *************************************************************************************************************** + * + * a) SOME MINOR TECHNICAL ISSUES: + * =============================== + * + * -keyer_close was actually unused. It is now called when local CW is no longer used + * and "joins" (terminates) the keyer thread. + * + * - GPIO pin names are no longer used in iambic.c + * + * - cw_keyer_spacing can now be set/un-set in the CW menu (cw_menu.c) + * + * - changing the scheduling policy now becomes only effective for the keyer thread, because + * sched_setscheduler() is called at the beginning and the end of the keyer thread with + * pid argument 0 (even better: use gettid() return value). + * NOTE: this is Linux-specific. Better switch to POSIX calls (e.g. pthread_setschedparam). + * mlockall and munlockall still apply process-wide and are therefore executed in + * keyer_init/keyer_close. + * + * b) SIDE TONE GENERATION + * ======================= + * + * There is a possibility to generate a side tone on one of the GPIO ports. This is necessary at + * speeds greater than 20 wpm since there is a 50 msec delay (due to the LINUX sound system) between + * generating the side tone and when it finally appears in the head-phone. The GPIO side tone comes + * without a delay. + * + * The volume of the CW side tone in the standard audio channel is reduced to zero while producing + * a square wave on the GPIO pin. The idea is to low-pass this signal and combine it with the audio + * output (hardware mixer). + * + * In the previous version, the side tone was generate using the softTone functionality of wiringPI + * within the GPIO module. However, this has a drawback: + * one creates two high-priority threads (the tone generator and the keyer) + * both firing at high speed (once a milli-sec and faster). This made the frequency of the tone quite + * unstable. Instead, the side tone is now explicitly generated within the keyer (if GPIO side tone is activated) + * by periodically writing "1" and "0" to the GPIO output while waiting for the end of the just-being-sent + * dot or dash. Furthermore, the frequency of the side tone has been stabilized by "sleeping" UNTIL the + * pre-calculated output-switching time (and not FOR the calculated amount, this is, using TIMER_ABSTIME + * in clock_nanosleep). + * + * c) CW VOX + * ========= + * + * Suppose you hit the paddle while in RX mode. In this case, the SDR automatically switches + * to TX, and remains so until a certain time (actually cw_keyer_hang_time, can be set in + * the CW menu) has passed since sending the last element (then, PTT is removed). + * + * - cw_keyer_spacing can now be set/un-set in the CW menu (cw_menu.c) + * + * - during a dot or dash when no GPIO side tone is produced, the keyer thread simply waits and + * does no busy spinning. + * + * d) DOT/DASH MEMORY + * ================== + * + * For reasons explained below, it is necessary to have TWO such memories for both dot and dash, + * they are called dot_memory/dot_held and dash_memory/dash_held. Everything explained here and below + * likewise holds for dot/dash exchanged. + * + * dot_memory is set whenever the dot paddle is hit. The natural way to do this is in the interrupt + * service routine. So, keyer_event is the ONLY place where dot_memory is set. When the keyer wants to + * know whether the dot paddle has been hit in some interval in time, it has to RESET dot_memory at the + * beginning of the interval and can then read it out any time later. + * + * While the dot paddle may have been hit long ago, it is of interest whether is was still held at + * the beginning of the last dash. Therefore, the keyer can store the state of the dot paddle in the + * variable dot_held. This is done at the beginning of sending a dash. dot_held is separate from + * dot_memory because only dot_held (but not dot_memory) is cleared in iambic mode A when both + * paddles are released. + * + * e) IAMBIC MODES A AND B, AND SINGLE-LEVER PADDLES + * ================================================= + * + * It seems that there are lively discussions about what is what, so I distilled out the + * following and added one clarification that becomes important when using this mode + * with single-lever paddles: + * + * Suppose you hit the dash paddle and squeeze immediately afterwards (both paddles held), + * then the keyer is supposed to produce a "dah dit dah dit dah dit .... " sequence. This is + * what "iambic" is all about. + * + * Now what happens if you release BOTH paddles just after the second "dah" starts to sound? + * In Mode-A you produce the letter "K" (dah dit dah), while in Mode-B, you produce the letter + * "C" (dah dit dah dit). This is consensus. In this implementation, I follow the plead of + * KO0B which I found on the internet and which essentially states that the "time window" for + * releasing both paddles should extend until the end of the delay following the second "dah". + * This is not only more convenient but also mimics the behaviour of the original Curtis + * mode A 8044 chip (KO0B says). + * + * This means that at the end of the delay following a dash, in Mode-B the condition for + * producing a dot is: + * + * -dot paddle has been held at the beginning of the last dash OR + * -dot paddle is just being held OR + * -dot paddle has been hit (and possibly already been released) during the time window + * from the beginning of the dash until now. + * + * This implies that in dot_held, the state of the dot key at the beginning of a dash is + * stored, and that dot_memory is cleared at the beginning of a dash. + * + * To implement MODE-A, we clear dot_held at the end of the delay following the dash, + * when both keys are released at that point in time. + * We do not clear dot_memory in this case! Why? + * + * All the definitions on mode A/B I found relate to releasing keys that have been + * squeezed for some time. Nothing is said about what happens if a key has been + * recently hit. I am sometimes using a single-lever paddle, and at higher speeds, + * I have also observed that I use a standard (dual-lever) paddle this way, namely + * alternatingly hitting the paddle (shortly releasing the dash paddle when hitting + * the dot one, and then possibly re-activating the dash paddle). + * + * It seems consensus that using a single-lever paddle, there should be no difference + * between mode-A and mode-B. + * + * So imagine you want to produce the letter "N" (dah dit) this way. You will hit the dash key, + * then immediately afterwards the dot key, and possibly have both keys released + * when the final dot of the letter should start. The final dot should not be suppressed + * even in mode-A. + * Thus: if both keys have been held at the beginning of a dash, and are relased + * during the dash or the delay following, this dash is the last element being produced in mode-A. + * BUT, if the dot key has been hit during the dash (or the delay following it), then a dot + * element will be produced both in mode-A and mode-B, it is not necessary to keep on holding + * the dot key. + * + * The present implementation will make both types happy: those doing pure squeeze and + * those releasing one paddle when hitting the other. + * + ************************************************************************************************************** + */ + #include #include #include @@ -70,9 +205,9 @@ #include #include -#include - +#ifdef GPIO #include "gpio.h" +#endif #include "radio.h" #include "new_protocol.h" #include "iambic.h" @@ -83,35 +218,21 @@ static void* keyer_thread(void *arg); static pthread_t keyer_thread_id; #define MY_PRIORITY (90) -#define MAX_SAFE_STACK (8*1024) #define NSEC_PER_SEC (1000000000) -/* -enum { - CHECK = 0, - PREDOT, - PREDASH, - SENDDOT, - SENDDASH, - DOTDELAY, - DASHDELAY, - DOTHELD, - DASHHELD, - LETTERSPACE, - EXITLOOP -}; -*/ - static int dot_memory = 0; static int dash_memory = 0; +static int dot_held = 0; +static int dash_held = 0; int key_state = 0; -static int kdelay = 0; -static int dot_delay = 0; -static int dash_delay = 0; +static int dot_length = 0; +static int dash_length = 0; static int kcwl = 0; static int kcwr = 0; int *kdot; int *kdash; +int *kmemr; +int *kmeml; static int running = 0; #ifdef __APPLE__ static sem_t *cw_event; @@ -128,50 +249,85 @@ extern int clock_nanosleep(clockid_t __clock_id, int __flags, __const struct timespec *__req, struct timespec *__rem); -void stack_prefault(void) { - unsigned char dummy[MAX_SAFE_STACK]; - - memset(dummy, 0, MAX_SAFE_STACK); - return; -} +#ifndef GPIO +// +// It makes absolutely no sense to activate LOCALCW but not GPIO. +// However, if this is done, the code should compile although it does +// not do anything. Therefore we provide dummy functions related to +// GPIO side tone generation. Without GPIO, keyer_event is never +// called so the keyer_thread does nothing. +// +int gpio_cw_sidetone_enabled() { return 0; } +void gpio_cw_sidetone_set(int level) {} +#endif void keyer_update() { - if (!running) - keyer_init(); - - dot_delay = 1200 / cw_keyer_speed; + // + // This function will take notice of changes in the following variables + // + // cw_keyer_internal + // cw_keyer_speed + // cw_keyer_weight + // cw_keys_reversed + // + // that might occur asynchronously by changing settings in the CW menu. + // Changes to cw_letter_spacing are notices without calling keyer_update. + // + // The most important thing here is to start/stop the keyer thread. + // + + dot_length = 1200 / cw_keyer_speed; // will be 3 * dot length at standard weight - dash_delay = (dot_delay * 3 * cw_keyer_weight) / 50; + dash_length = (dot_length * 3 * cw_keyer_weight) / 50; if (cw_keys_reversed) { - kdot = &kcwr; + kdot = &kcwr; kdash = &kcwl; + kmemr = &dot_memory; + kmeml = &dash_memory; } else { - kdot = &kcwl; + kdot = &kcwl; kdash = &kcwr; + kmeml = &dot_memory; + kmemr = &dash_memory; } + if (cw_keyer_internal == 0) { + if (!running) keyer_init(); + } else { + keyer_close(); + } } -#ifdef GPIO -void keyer_event(int gpio, int state) { +// +// This is called by the paddle interrupt service routine +// +// left=1: left paddle triggered event +// left=0: right paddle triggered event +// +// state=0: paddle has been released +// state=1: paddle has been hit +// +void keyer_event(int left, int state) { if (state) { - // This is for aborting CAT CW messages if the key is hit. + // This is for aborting CAT CW messages if a key is hit. cw_key_hit = 1; - // we do PTT as soon as possible, but disable cwvox if - // PTT has been engaged manually + // do PTT already here, unless TX mode was activated before if (running && !cwvox && !mox) { g_idle_add(ext_mox_update, (gpointer)(long) 1); - cwvox=(int) cw_keyer_hang_time; } + cwvox=(int) cw_keyer_hang_time; } - if (gpio == CWL_BUTTON) - kcwl = state; - //fprintf(stderr,"L=%d\n",state); - else // CWR_BUTTON - kcwr = state; - - if (state || cw_keyer_mode == KEYER_STRAIGHT) { + if (left) { + // left paddle hit or released + kcwl = state; + if (state) *kmeml=1; // trigger dot/dash memory + } else { + // right paddle hit or released + kcwr = state; + if (state) *kmemr=1; // trigger dot/dash memory + } + if (state) { #ifdef __APPLE__ sem_post(cw_event); #else @@ -179,28 +335,19 @@ void keyer_event(int gpio, int state) { #endif } } -#endif - -void clear_memory() { - dot_memory = 0; - dash_memory = 0; -} void set_keyer_out(int state) { if (keyer_out != state) { - // DL1YCF: Shouln't one activate PTT if not yet done? - // At the moment, it is required to *manually* activate - // MOX before starting local CW. keyer_out = state; if(protocol==NEW_PROTOCOL) schedule_high_priority(9); - //fprintf(stderr,"set_keyer_out keyer_out= %d\n", keyer_out); if (state) { - gpio_sidetone(cw_keyer_sidetone_frequency); cw_hold_key(1); // this starts a CW pulse in transmitter.c } else { - gpio_sidetone(0); cw_hold_key(0); // this stops a CW pulse in transmitter.c } + } else { + // We should not arrive here in a properly designed keyer + fprintf(stderr,"SET KEYER OUT: state unchanged: %d", state); } } @@ -208,8 +355,21 @@ static void* keyer_thread(void *arg) { int pos; struct timespec loop_delay; int interval = 1000000; // 1 ms + int sidewait; + int sideloop; + int i; + int kdelay; + int old_volume; + struct sched_param param; + + // In Linux, the new scheduling policy will be effective for the + // calling thread only. + param.sched_priority = MY_PRIORITY; + if(sched_setscheduler((pid_t)0, SCHED_FIFO, ¶m) == -1) { + perror("sched_setscheduler failed"); + } -fprintf(stderr,"keyer_thread state running= %d\n", running); + fprintf(stderr,"keyer_thread state running= %d\n", running); while(running) { #ifdef __APPLE__ sem_wait(cw_event); @@ -219,172 +379,274 @@ fprintf(stderr,"keyer_thread state running= %d\n", running); key_state = CHECK; - // If MOX still hanging, continue spinnning/checking and decrement cwvox - + clock_gettime(CLOCK_MONOTONIC, &loop_delay); while (key_state != EXITLOOP || cwvox > 0) { + // re-trigger VOX if *not* busy-spinning if (cwvox > 0 && key_state != EXITLOOP && key_state != CHECK) cwvox=(int) cw_keyer_hang_time; + switch (key_state) { + case EXITLOOP: - if (cwvox >0) cwvox--; + // If we arrive here, cwvox is greater than zero, since key_state==EXITLOOP + // AND cwvox==0 leaves the outer "while" loop. + cwvox--; + // If CW-vox still hanging, continue "busy-spinning" if (cwvox == 0) { + // we have just reduced cwvox from 1 to 0. g_idle_add(ext_mox_update,(gpointer)(long) 0); } else { key_state=CHECK; } break; + case CHECK: // check for key press + key_state = EXITLOOP; // default next state + // Do not decrement cwvox until zero here, otherwise + // we won't enter the code 10 lines above that de-activates MOX. if (cwvox > 1) cwvox--; if (cw_keyer_mode == KEYER_STRAIGHT) { // Straight/External key or bug if (*kdash) { // send manual dashes - set_keyer_out(1); - key_state = EXITLOOP; - } - else if (*kdot) // and automatic dots - key_state = PREDOT; - else { - set_keyer_out(0); - key_state = EXITLOOP; + set_keyer_out(1); + clock_gettime(CLOCK_MONOTONIC, &loop_delay); + // wait until dash is released + if (gpio_cw_sidetone_enabled()) { + // produce tone + // Note. Using clock_nanosleep with ABSTIME is absolutely + // necessary to produce a stable frequency. + // You still may painfully recognize that LINUX + // e.g. on a RaspberryPi is not a real-time + // operating system. + // Mute "normal" CW side tone, it will be reactivated + // at the end of the following delay. + old_volume=cw_keyer_sidetone_volume; + cw_keyer_sidetone_volume=0; + sidewait=500000000/cw_keyer_sidetone_frequency; + for (;;) { + gpio_cw_sidetone_set(1); + loop_delay.tv_nsec += sidewait; + 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); + gpio_cw_sidetone_set(0); + loop_delay.tv_nsec += sidewait; + while (loop_delay.tv_nsec >= NSEC_PER_SEC) { + loop_delay.tv_nsec -= NSEC_PER_SEC; + loop_delay.tv_sec++; + } + if (!*kdash) break; + clock_nanosleep(CLOCK_MONOTONIC, TIMER_ABSTIME, &loop_delay, NULL); + } + } else { + // No-GPIO-sidetone case: + // wait until dash is released. Check once a milli-sec + for (;;) { + loop_delay.tv_nsec += interval; + while (loop_delay.tv_nsec >= NSEC_PER_SEC) { + loop_delay.tv_nsec -= NSEC_PER_SEC; + loop_delay.tv_sec++; + } + if (!*kdash) break; + clock_nanosleep(CLOCK_MONOTONIC, TIMER_ABSTIME, &loop_delay, NULL); + } + } + // dash released. + set_keyer_out(0); + // wait at least 10ms before re-activating sidetone, + // to allow the envelope of the side tone reaching zero + if (gpio_cw_sidetone_enabled()) { + loop_delay.tv_nsec += 10*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); + cw_keyer_sidetone_volume=old_volume; + } } - } - else { - if (*kdot) - key_state = PREDOT; - else if (*kdash) - key_state = PREDASH; - else { - set_keyer_out(0); - key_state = EXITLOOP; + if (*kdot) { + // "bug" mode: dot key activates automatic dots + key_state = SENDDOT; } + // end of KEYER_STRAIGHT case + } else { + // Paddle + // If both following if-statements are true, which one should win? + // I think a "simultaneous squeeze" means a dot-dash sequence, since in + // a dash-dot sequence there is a larger time window to hit the dot. + if (*kdash) key_state = SENDDASH; + if (*kdot) key_state = SENDDOT; } - 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; + 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: + case SENDDOT: + dash_memory = 0; + dash_held = *kdash; 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; - else 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; - else if (*kdot) // set dot memory - dot_memory = 1; + clock_gettime(CLOCK_MONOTONIC, &loop_delay); + if (gpio_cw_sidetone_enabled()) { + old_volume=cw_keyer_sidetone_volume; + cw_keyer_sidetone_volume=0; + sidewait=500000000/cw_keyer_sidetone_frequency; + sideloop=(500000*dot_length)/sidewait; + for (i=0; i= NSEC_PER_SEC) { + loop_delay.tv_nsec -= NSEC_PER_SEC; + loop_delay.tv_sec++; + } + clock_nanosleep(CLOCK_MONOTONIC, TIMER_ABSTIME, &loop_delay, NULL); + gpio_cw_sidetone_set(0); + loop_delay.tv_nsec += sidewait; + 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); + } + } else { + // No-GPIO-sidetone case: just wait + loop_delay.tv_nsec += 1000000 * dot_length; + 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); + } + set_keyer_out(0); + key_state = DOTDELAY; // add inter-character spacing of one dot length + kdelay=0; 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 = EXITLOOP; - 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; + kdelay++; + if (kdelay > dot_length) { + if (gpio_cw_sidetone_enabled()) { + cw_keyer_sidetone_volume=old_volume; + } + if (cw_keyer_mode == KEYER_STRAIGHT) { + // bug mode: continue sending dots or exit, depending on current dot key status + key_state = EXITLOOP; + if (*kdot) key_state=SENDDOT; + // end of bug/straight case + } else { +// +// DL1YCF: +// This is my understanding where MODE A comes in: +// If at the end of the delay, BOTH keys are +// released, then do not start the next element. +// However, if the dash has been hit DURING the preceeding +// dot, produce a dash in either case +// + if (cw_keyer_mode == KEYER_MODE_A && !*kdot && !*kdash) dash_held=0; + + if (dash_memory || *kdash || dash_held) + key_state = SENDDASH; + else if (*kdot) // dot still held, so send a dot + key_state = SENDDOT; + else if (cw_keyer_spacing) { + dot_memory = dash_memory = 0; + key_state = LETTERSPACE; + kdelay=0; + } else + key_state = EXITLOOP; + // end of iambic case + } + } 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 = EXITLOOP; + case SENDDASH: + dot_memory = 0; + dot_held = *kdot; // remember if dot is still held at beginning of the dash + set_keyer_out(1); + clock_gettime(CLOCK_MONOTONIC, &loop_delay); + if (gpio_cw_sidetone_enabled()) { + old_volume=cw_keyer_sidetone_volume; + cw_keyer_sidetone_volume=0; + sidewait=500000000/cw_keyer_sidetone_frequency; + sideloop=(500000*dash_length)/sidewait; + for (i=0; i= NSEC_PER_SEC) { + loop_delay.tv_nsec -= NSEC_PER_SEC; + loop_delay.tv_sec++; + } + clock_nanosleep(CLOCK_MONOTONIC, TIMER_ABSTIME, &loop_delay, NULL); + gpio_cw_sidetone_set(0); + loop_delay.tv_nsec += sidewait; + 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); + } + } else { + // No-GPIO-sidetone case: just wait + loop_delay.tv_nsec += 1000000L * dash_length; + 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); + } + set_keyer_out(0); + key_state = DASHDELAY; // add inter-character spacing of one dot length + kdelay=0; + // do not fall through, update VOX at beginning of loop 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; + case DASHDELAY: + // we never arrive here in STRAIGHT/BUG mode + kdelay++; + if (kdelay > dot_length) { + if (gpio_cw_sidetone_enabled()) { + cw_keyer_sidetone_volume=old_volume; + } +// +// DL1YCF: +// This is my understanding where MODE A comes in: +// If at the end of the dash delay, BOTH keys are +// released, then do not start the next element. +// However, if the dot has been hit DURING the preceeding +// dash, produce a dot in either case +// + if (cw_keyer_mode == KEYER_MODE_A && !*kdot && !*kdash) dot_held=0; + if (dot_memory || *kdot || dot_held) + key_state = SENDDOT; + else if (*kdash) + key_state = SENDDASH; + else if (cw_keyer_spacing) { + dot_memory = dash_memory = 0; + key_state = LETTERSPACE; + kdelay=0; + } else key_state = EXITLOOP; } - else key_state = EXITLOOP; 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; + // 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_length since we already have a dot delay at the end of the character. + kdelay++; + if (kdelay > 2 * dot_length) { if (dot_memory) // check if a dot or dash paddle was pressed during the delay. - key_state = PREDOT; + key_state = SENDDOT; else if (dash_memory) - key_state = PREDASH; + key_state = SENDDASH; else key_state = EXITLOOP; // 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: + fprintf(stderr,"KEYER THREAD: unknown state=%d",(int) key_state); key_state = EXITLOOP; - } - clock_gettime(CLOCK_MONOTONIC, &loop_delay); + // time stamp in loop_delay is either the last time stamp from the + // top of the loop, or the last time stamp from tone generation + // NOTE: we are using ABSTIME here to produce accurate delays. loop_delay.tv_nsec += interval; while (loop_delay.tv_nsec >= NSEC_PER_SEC) { loop_delay.tv_nsec -= NSEC_PER_SEC; @@ -394,28 +656,39 @@ fprintf(stderr,"keyer_thread state running= %d\n", running); } } -fprintf(stderr,"keyer_thread: EXIT\n"); + fprintf(stderr,"keyer_thread: EXIT\n"); + param.sched_priority = 0; + sched_setscheduler((pid_t) 0, SCHED_OTHER, ¶m); } void keyer_close() { + fprintf(stderr,".... closing keyer thread.\n"); running=0; + // keyer thread may be sleeping, so wake it up +#ifdef __APPLE__ + sem_post(cw_event); +#else + sem_post(&cw_event); +#endif + pthread_join(keyer_thread_id, NULL); +#ifdef __APPLE__ + sem_close(cw_event); +#else + sem_close(&cw_event); +#endif + munlockall(); } int keyer_init() { int rc; - struct sched_param param; - - param.sched_priority = MY_PRIORITY; - if(sched_setscheduler(0, SCHED_FIFO, ¶m) == -1) { - perror("sched_setscheduler failed"); - running = 0; - } + fprintf(stderr,".... starting keyer thread.\n"); + if(mlockall(MCL_CURRENT|MCL_FUTURE) == -1) { perror("mlockall failed"); - running = 0; } + running = 1; #ifdef __APPLE__ cw_event=sem_open("CW", O_CREAT, 0700, 0); rc = (cw_event == SEM_FAILED); @@ -423,11 +696,9 @@ int keyer_init() { rc = sem_init(&cw_event, 0, 0); #endif rc |= pthread_create(&keyer_thread_id, NULL, keyer_thread, NULL); - running = 1; if(rc < 0) { fprintf(stderr,"pthread_create for keyer_thread failed %d\n", rc); exit(-1); } - return 0; } diff --git a/iambic.h b/iambic.h index bf29676..9eaba58 100644 --- a/iambic.h +++ b/iambic.h @@ -3,27 +3,18 @@ enum { CHECK = 0, - PREDOT, - PREDASH, SENDDOT, SENDDASH, DOTDELAY, DASHDELAY, - DOTHELD, - DASHHELD, LETTERSPACE, EXITLOOP }; -extern int cwl_state; -extern int cwr_state; extern int keyer_out; - extern int key_state; -extern int *kdot; -extern int *kdash; -void keyer_event(int gpio, int level); +void keyer_event(int left, int state); void keyer_update(); void keyer_close(); int keyer_init(); diff --git a/new_protocol.c b/new_protocol.c index a30d2ea..072ea09 100644 --- a/new_protocol.c +++ b/new_protocol.c @@ -287,14 +287,6 @@ void tuner_changed() { } */ -void cw_changed() { -#ifdef LOCALCW - // update the iambic keyer params - if (cw_keyer_internal == 0) - keyer_update(); -#endif -} - void new_protocol_init(int pixels) { int i; int rc; @@ -568,8 +560,6 @@ static void new_protocol_high_priority() { // set the ptt if we're not in breakin mode and mox is on if(cw_breakin == 0 && getMox()) high_priority_buffer_to_radio[4]|=0x02; high_priority_buffer_to_radio[5]|=(keyer_out) ? 0x01 : 0; - //high_priority_buffer_to_radio[5]|=(*kdot) ? 0x02 : 0; - //high_priority_buffer_to_radio[5]|=(*kdash) ? 0x04 : 0; high_priority_buffer_to_radio[5]|=(key_state==SENDDOT) ? 0x02 : 0; high_priority_buffer_to_radio[5]|=(key_state==SENDDASH) ? 0x04 : 0; } diff --git a/new_protocol.h b/new_protocol.h index 659c721..4a5bf5b 100644 --- a/new_protocol.h +++ b/new_protocol.h @@ -81,7 +81,6 @@ extern void new_protocol_run(); extern void filter_board_changed(); extern void pa_changed(); extern void tuner_changed(); -extern void cw_changed(); extern void setMox(int state); extern int getMox(); diff --git a/property.c b/property.c index e6bc40d..67abcbe 100644 --- a/property.c +++ b/property.c @@ -80,7 +80,7 @@ void saveProperties(char* filename) { char line[512]; char version[32]; - fprintf(stderr,"saveProperties: %s\n",filename); + //fprintf(stderr,"saveProperties: %s\n",filename); if(!f) { fprintf(stderr,"can't open %s\n",filename); diff --git a/rigctl.c b/rigctl.c index b285bcb..442611a 100644 --- a/rigctl.c +++ b/rigctl.c @@ -297,6 +297,7 @@ int vfo_sm=0; // VFO State Machine - this keeps track of static char cw_buf[30]; static int cw_busy=0; +static int cat_cw_seen=0; static long dotlen; static long dashlen; @@ -317,45 +318,54 @@ extern int cw_key_up, cw_key_down, cw_not_ready; // void send_dash() { int TimeToGo; - if (cw_key_hit || cw_not_ready) return; for(;;) { TimeToGo=cw_key_up+cw_key_down; + // TimeToGo is invalid if local CW keying has set in + if (cw_key_hit || cw_not_ready) return; if (TimeToGo == 0) break; // sleep until 10 msec before ignition if (TimeToGo > 500) usleep((long)(TimeToGo-500)*20L); // sleep 1 msec usleep(1000L); } + // If local CW keying has set in, do not interfere + if (cw_key_hit || cw_not_ready) return; cw_key_down = dashsamples; cw_key_up = dotsamples; } void send_dot() { int TimeToGo; - if (cw_key_hit || cw_not_ready) return; for(;;) { TimeToGo=cw_key_up+cw_key_down; + // TimeToGo is invalid if local CW keying has set in + if (cw_key_hit || cw_not_ready) return; if (TimeToGo == 0) break; // sleep until 10 msec before ignition if (TimeToGo > 500) usleep((long)(TimeToGo-500)*20L); // sleep 1 msec usleep(1000L); } + // If local CW keying has set in, do not interfere + if (cw_key_hit || cw_not_ready) return; cw_key_down = dotsamples; cw_key_up = dotsamples; } void send_space(int len) { int TimeToGo; - if (cw_key_hit || cw_not_ready) return; for(;;) { TimeToGo=cw_key_up+cw_key_down; + // TimeToGo is invalid if local CW keying has set in + if (cw_key_hit || cw_not_ready) return; if (TimeToGo == 0) break; // sleep until 10 msec before ignition if (TimeToGo > 500) usleep((long)(TimeToGo-500)*20L); // sleep 1 msec usleep(1000L); } + // If local CW keying has set in, do not interfere + if (cw_key_hit || cw_not_ready) return; cw_key_up = len*dotsamples; } @@ -481,16 +491,15 @@ void rigctl_send_cw_char(char cw_char) { // // This thread constantly looks whether CW data // is available, and produces CW in this case. -// The buffer is copied to a local buffer and -// immediately released, such that the next -// chunk can already be prepeared. This way, -// splitting a large CW text into words, and -// sending each word with a separate KY command -// produces perfectly readable CW. // -// UPDATE: we maintain a ring buffer such that -// the contents of several KY commands -// can be buffered +// A ring buffer is maintained such that the contents +// of several KY commands can be buffered. This allows +// sending a large CW text word-by-word (each word in a +// separate KY command). +// +// If the contents of the last KY command do not fit into +// the ring buffer, cw_busy is NOT reset. Eventually, there +// is enough space in the ring buffer, then cw_busy is reset. // static gpointer rigctl_cw_thread(gpointer data) { @@ -504,15 +513,14 @@ static gpointer rigctl_cw_thread(gpointer data) int num_buf=0; while (server_running) { - // wait for cw_buf become filled with data - // (periodically look every 100 msec) + // wait for CW data (periodically look every 100 msec) if (!cw_busy && num_buf ==0) { cw_key_hit=0; usleep(100000L); continue; } - // if new data available and space in buffer, copy it + // if new data is available and fits into the buffer, copy-in. // If there are several adjacent spaces, take only the first one. // This also swallows the "tails" of the KY commands which // (according to Kenwood) have to be padded with spaces up @@ -527,7 +535,7 @@ static gpointer rigctl_cw_thread(gpointer data) num_buf++; if (write_buf - ring_buf == 128) write_buf=ring_buf; // wrap around } - cw_busy=0; // mark buffer free again + cw_busy=0; // mark one-line buffer free again } // these values may have changed, so recompute them here @@ -552,29 +560,29 @@ static gpointer rigctl_cw_thread(gpointer data) CAT_cw_is_active=0; continue; } - // some extra time to settle down, in order NOT to loose - // the first dit or dah - usleep(100000L); } // At this point, mox==1 and CAT_cw_active == 1 if (cw_key_hit || cw_not_ready) { // // CW transmission has been aborted, either due to manually // removing MOX, changing the mode to non-CW, or because a CW key has been hit. - // Do not remove PTT if we abort CAT CW because a CW key has been hit. + // Do not remove PTT in the latter case CAT_cw_is_active=0; - // If an external CW key has been hit, we continue in TX mode - // doing CW manually. Otherwise, switch PTT off. - if (!cw_key_hit) { + // If a CW key has been hit, we continue in TX mode. + // Otherwise, switch PTT off. + if (!cw_key_hit && mox) { g_idle_add(ext_ptt_update ,(gpointer)0); } - // Stay for 1 sec swallowing incoming - // CAT CW commands. We need this long time since - // hamlib waits 0.5 secs after receiving a "KY1" message before trying to - // send the next bunch - cw_busy=-1; // mark buffer purge situation - usleep(1000000L); - cw_busy=0; + // Let the CAT system swallow incoming CW commands by setting cw_busy to -1. + // Do so until no CAT CW message has arrived for 1 second + cw_busy=-1; + for (;;) { + cat_cw_seen=0; + usleep(1000000L); + if (cat_cw_seen) continue; + cw_busy=0; + break; + } write_buf=read_buf=ring_buf; num_buf=0; } else { @@ -587,13 +595,16 @@ static gpointer rigctl_cw_thread(gpointer data) // Otherwise remove PTT and wait for next CAT CW command. if (cw_busy || num_buf > 0) continue; CAT_cw_is_active=0; - g_idle_add(ext_ptt_update ,(gpointer)0); - // wait up to 500 msec for MOX having gone - // otherwise there might be a race condition when sending - // the next character really soon - i=10; - while (mox && (i--) > 0) usleep(50000L); + if (!cw_key_hit) { + g_idle_add(ext_ptt_update ,(gpointer)0); + // wait up to 500 msec for MOX having gone + // otherwise there might be a race condition when sending + // the next character really soon + i=10; + while (mox && (i--) > 0) usleep(50000L); + } } + // end of while (server_running) } // We arrive here if the rigctl server shuts down. // This very rarely happens. But we should shut down the @@ -601,8 +612,10 @@ static gpointer rigctl_cw_thread(gpointer data) // of a transmission rigctl_cw_thread_id = NULL; cw_busy=0; - CAT_cw_is_active=0; - g_idle_add(ext_ptt_update ,(gpointer)0); + if (CAT_cw_is_active) { + CAT_cw_is_active=0; + g_idle_add(ext_ptt_update ,(gpointer)0); + } return NULL; } @@ -2204,6 +2217,7 @@ void parse_cmd ( char * cmd_input,int len,int client_sock) { } else if((strcmp(cmd_str,"KY")==0) && (zzid_flag == 0)) { + if (cw_busy < 0) cat_cw_seen=1; // DL1YCF: // Hamlib produces timeout errors if we are busy here for // seconds. Therefore we just move the data into a buffer